Skip to content

Commit

Permalink
feat: improve FFmpeg seeking (#26)
Browse files Browse the repository at this point in the history
Add three seek improvements which increase the range of file formats that can be directly decoded through FFmpeg and thus reduce the likelihood that audio proxies are required:
- report container duration when stream duration is unavailable,
- consider non-zero start times,
- handle cases where seeks end up too early.
  • Loading branch information
protyposis authored Jan 27, 2024
1 parent 2d531d0 commit 682cf2d
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 18 deletions.
8 changes: 6 additions & 2 deletions nativesrc/aurioffmpegproxy/proxy.c
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,12 @@ ProxyInstance *stream_open(ProxyInstance *pi)
pi->audio_output.format.channels);
}

pi->audio_output.length = pi->audio_stream->duration != AV_NOPTS_VALUE ?
pts_to_samples(pi->audio_output.format.sample_rate, pi->audio_stream->time_base, pi->audio_stream->duration) : AV_NOPTS_VALUE;
pi->audio_output.length =
pi->audio_stream->duration != AV_NOPTS_VALUE
? pts_to_samples(pi->audio_output.format.sample_rate, pi->audio_stream->time_base, pi->audio_stream->duration)
: pi->fmt_ctx->duration != AV_NOPTS_VALUE
? pts_to_samples(pi->audio_output.format.sample_rate, AV_TIME_BASE_Q, pi->fmt_ctx->duration)
: AV_NOPTS_VALUE;

/*
* TODO To get the frame size, read the first frame, take the size, and seek back to the start.
Expand Down
12 changes: 12 additions & 0 deletions src/Aurio.FFmpeg.UnitTest/AudioDecode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,17 @@ public void Mp3ReadData()
var length = StreamUtil.ReadAllAndCount(s);
Assert.True(length > 46000);
}

[Fact]
public void TS_ReadDataUntilEnd()
{
var s = new FFmpegSourceStream(
new FileInfo("./Resources/sine440-44100-16-mono-200ms.mp3")
);

StreamUtil.ReadAllAndCount(s);

// Test succeeds when reading does not get stuck in infinite loop at EOF
}
}
}
2 changes: 1 addition & 1 deletion src/Aurio.FFmpeg.UnitTest/Aurio.FFmpeg.UnitTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
</ItemGroup>

<ItemGroup>
<None Update="Resources\sine440-44100-16-mono-200ms.mp3">
<None Update="Resources\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
Expand Down
56 changes: 56 additions & 0 deletions src/Aurio.FFmpeg.UnitTest/FFmpegReaderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.IO;
using Xunit;

namespace Aurio.FFmpeg.UnitTest
{
public class FFmpegReaderTests
{
/// <summary>
/// MKV does not carry individual stream lenghts, only a total container length.
/// </summary>
[Fact]
public void MKV_LengthFromContainerAvailable()
{
var fileInfo = new FileInfo("./Resources/sine440-44100-16-mono-200ms.mkv");

var reader = new FFmpegReader(fileInfo, Type.Audio);

Assert.NotEqual(long.MinValue, reader.AudioOutputConfig.length);
}

[Fact]
public void TS_NonZeroStartTime()
{
var fileInfo = new FileInfo("./Resources/sine440-44100-16-mono-200ms.ts");
var reader = new FFmpegReader(fileInfo, Type.Audio);
var sourceBuffer = new byte[reader.FrameBufferSize];
var startTimeSecs = 2000;

reader.ReadFrame(out long readerPosition, sourceBuffer, sourceBuffer.Length, out _);

Assert.Equal(
startTimeSecs * reader.AudioOutputConfig.format.sample_rate,
readerPosition
);
}

[Fact]
public void SignalEOF()
{
var fileInfo = new FileInfo("./Resources/sine440-44100-16-mono-200ms.ts");
var reader = new FFmpegReader(fileInfo, Type.Audio);
var sourceBuffer = new byte[reader.FrameBufferSize];
int result;

// Read over all frames (seeking does not work here due to short stream)
do
{
result = reader.ReadFrame(out _, sourceBuffer, sourceBuffer.Length, out _);
} while (result > 0);

// -1 signals EOF
Assert.Equal(-1, result);
}
}
}
23 changes: 23 additions & 0 deletions src/Aurio.FFmpeg.UnitTest/FFmpegSourceStreamTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,28 @@ public void SuggestWaveProxyFileInfo_WithDirectory_Unix()
proxyFileInfo.FullName
);
}

[Fact]
public void MKV_NoStreamDuration_SeekingSupported()
{
var fileInfo = new FileInfo("./Resources/sine440-44100-16-mono-200ms.mkv");

var act = () => new FFmpegSourceStream(fileInfo);
var ex = Record.Exception(act);

// Assert no FileNotSeekableException being thrown
Assert.Null(ex);
}

[Fact]
public void TS_NonZeroStartTime_SeekingSupported()
{
var fileInfo = new FileInfo("./Resources/sine440-44100-16-mono-200ms.ts");
var s = new FFmpegSourceStream(fileInfo);

s.Position = 1000;

Assert.Equal(1000, s.Position);
}
}
}
Binary file not shown.
Binary file not shown.
10 changes: 10 additions & 0 deletions src/Aurio.FFmpeg/FFmpegReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,16 @@ public VideoOutputConfig VideoOutputConfig
get { return videoOutputConfig; }
}

public long FrameBufferSize
{
get
{
return AudioOutputConfig.frame_size
* AudioOutputConfig.format.channels
* AudioOutputConfig.format.sample_size;
}
}

public int ReadFrame(
out long timestamp,
byte[] output_buffer,
Expand Down
61 changes: 46 additions & 15 deletions src/Aurio.FFmpeg/FFmpegSourceStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,7 @@ public FFmpegSourceStream(Stream stream, string fileName)
);

readerPosition = 0;
sourceBuffer = new byte[
reader.AudioOutputConfig.frame_size
* reader.AudioOutputConfig.format.channels
* reader.AudioOutputConfig.format.sample_size
];
sourceBuffer = new byte[reader.FrameBufferSize];
sourceBufferPosition = 0;
sourceBufferLength = -1; // -1 means buffer empty, >= 0 means valid buffer data

Expand Down Expand Up @@ -133,7 +129,50 @@ public long Length

private long SamplePosition
{
get { return readerPosition + sourceBufferPosition; }
get { return readerPosition + sourceBufferPosition - readerFirstPTS; }
}

/// <summary>
/// Read frames (repeatedly) into the buffer until it contains the sample with the
/// desired timestamp.
///
/// This is a helper method for sample-exact seeking, because FFmpeg seeks may end
/// up a long way before the desired seek target.
/// </summary>
private void ForwardReadUntilTimestamp(long targetTimestamp)
{
long previousReaderPosition = long.MinValue;

while (true)
{
sourceBufferLength = reader.ReadFrame(
out readerPosition,
sourceBuffer,
sourceBuffer.Length,
out Type type
);

if (readerPosition == previousReaderPosition)
{
// Prevent an endless read-loop in case the reported position does not change.
// I did not encounter this behavior, but who knows how FFmpeg acts on the myriad of supported formats.
throw new InvalidOperationException("Read head is stuck");
}
else if (targetTimestamp < readerPosition)
{
// Prevent endless loop in case the target timestamp gets skipped. Again, I
// have not seen it happen, so this is just another proactive measure.
throw new InvalidOperationException(
"Read position is beyond the target timestamp"
);
}
else if (targetTimestamp < readerPosition + sourceBufferLength)
{
break;
}

previousReaderPosition = readerPosition;
}
}

public long Position
Expand All @@ -145,17 +184,9 @@ public long Position

// seek to target position
reader.Seek(seekTarget, FFmpeg.Type.Audio);

// get target position
sourceBufferLength = reader.ReadFrame(
out readerPosition,
sourceBuffer,
sourceBuffer.Length,
out Type type
);
ForwardReadUntilTimestamp(seekTarget);

// check if seek ended up at seek target (or earlier because of frame size, depends on file format and stream codec)
// TODO handle seek offset with bufferPosition
if (seekTarget == readerPosition)
{
// perfect case
Expand Down

0 comments on commit 682cf2d

Please sign in to comment.