Skip to content

Commit

Permalink
fix: FFmpeg sometimes seeks one frame too far
Browse files Browse the repository at this point in the history
  • Loading branch information
protyposis committed Jan 30, 2024
1 parent 4c16721 commit 8d33d6a
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 9 deletions.
11 changes: 11 additions & 0 deletions nativesrc/aurioffmpegproxy/proxy.c
Original file line number Diff line number Diff line change
Expand Up @@ -519,9 +519,20 @@ void stream_seek(ProxyInstance *pi, int64_t timestamp, int type)
*
* This applies to both seek directions, backward and forward from the
* current position in the stream.
*
* FIXME: AVSEEK_FLAG_BACKWARD does not always work correctly and seeks
* sometimes still end up at the next frame PTS(b) (also reported at
* https://stackoverflow.com/a/21451032). Using `avformat_seek_file` with
* `ts == max_ts` constraint does not prevent this either.
* To end up at the desired frame, seek to an earlier frame and then read
* until the desired frame.
*/

// do seek
// FIXME: In some files (e.g., some MTS), it is not possible to seek to the
// first packet. When opening a file and reading the packets, the first packet
// is read, but when seeking, the first packet cannot be reached and the first
// read packet is actually the second packet.
av_seek_frame(pi->fmt_ctx, seek_stream->index, timestamp, AVSEEK_FLAG_BACKWARD);

// flush codec
Expand Down
54 changes: 54 additions & 0 deletions src/Aurio.FFmpeg.UnitTest/FFmpegSourceStreamTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using Moq;
using Xunit;
Expand Down Expand Up @@ -185,5 +186,58 @@ out It.Ref<Type>.IsAny
Times.Once()
);
}

[Fact]
public void SeekBeyondTarget_ReseekToPreviousFrame()
{
var readerMock = new Mock<FFmpegReader>(
new FileInfo("./Resources/sine440-44100-16-mono-200ms.mkv"),
Type.Audio
);
var timestamps = new Queue<long>(
new long[]
{
// First read determines PTS offset.
0,
// Second read expects frame to contain sample 25000 (100000 / sample block size),
// so return 25001 to simulate that the next frame was read instead.
25001,
// After the expected seek and re-read, indicate that frame contains expected sample.
25000
}
);
readerMock
.Setup(
m =>
m.ReadFrame(
out It.Ref<long>.IsAny,
It.IsAny<byte[]>(),
It.IsAny<int>(),
out It.Ref<Type>.IsAny
)
)
.Callback(
(out long timestamp, byte[] buffer, int bufferSize, out Type type) =>
{
timestamp = timestamps.Dequeue();
type = Type.Audio;
}
)
.Returns(1);
var s = new FFmpegSourceStream(readerMock.Object);

s.Position = 100000;

readerMock.Verify(
m =>
m.ReadFrame(
out It.Ref<long>.IsAny,
It.IsAny<byte[]>(),
It.IsAny<int>(),
out It.Ref<Type>.IsAny
),
Times.Exactly(3)
);
}
}
}
38 changes: 29 additions & 9 deletions src/Aurio.FFmpeg/FFmpegSourceStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,8 @@ private void ForwardReadUntilTimestamp(long targetTimestamp)
}
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(
// Prevent scanning to end of stream in case the target timestamp gets skipped.
throw new SeekTargetMissedException(
"Read position is beyond the target timestamp"
);
}
Expand All @@ -206,8 +205,21 @@ public long Position
long seekTarget = (value / SampleBlockSize) + readerFirstPTS;

// seek to target position
reader.Seek(seekTarget, FFmpeg.Type.Audio);
ForwardReadUntilTimestamp(seekTarget);
try
{
reader.Seek(seekTarget, Type.Audio);
ForwardReadUntilTimestamp(seekTarget);
}
catch (SeekTargetMissedException)
{
// FFmpeg seek is sometimes off by one frame. Try again, this time
// by seeking to the previous frame.
// This is an issue with the FFmpeg seek function, see `stream_seek` in `proxy.c`
// for details.
Console.WriteLine("Seek target miss. Retry seeking to earlier position.");
reader.Seek(seekTarget - reader.AudioOutputConfig.frame_size, Type.Audio);
ForwardReadUntilTimestamp(seekTarget);
}

// check if seek ended up at seek target (or earlier because of frame size, depends on file format and stream codec)
if (seekTarget == readerPosition)
Expand Down Expand Up @@ -253,10 +265,6 @@ public long Position
);
}
}

// seek back to seek point for successive reads to return expected data (not one frame in advance) PROBABLY NOT NEEDED
// TODO handle this case, e.g. when it is necessery and when it isn't (e.g. when block is chached because of bufferPosition > 0)
//reader.Seek(readerPosition);
}
}

Expand Down Expand Up @@ -495,5 +503,17 @@ public FileSeekException(string message)
public FileSeekException(string message, Exception innerException)
: base(message, innerException) { }
}

private class SeekTargetMissedException : Exception
{
public SeekTargetMissedException()
: base() { }

public SeekTargetMissedException(string message)
: base(message) { }

public SeekTargetMissedException(string message, Exception innerException)
: base(message, innerException) { }
}
}
}

0 comments on commit 8d33d6a

Please sign in to comment.