Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FFmpeg seek improvements #27

Merged
merged 6 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
3 changes: 2 additions & 1 deletion src/Aurio.FFmpeg.UnitTest/AudioDecode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ public void Mp3ReadData()
var s = new FFmpegSourceStream(
new FileInfo("./Resources/sine440-44100-16-mono-200ms.mp3")
);
var expectedMinLength = 44100 * 0.2 * 4; // Fs * 200ms * sampleSize (min, because MP3 adds additional samples)

var length = StreamUtil.ReadAllAndCount(s);
Assert.True(length > 46000);
Assert.True(length >= expectedMinLength);
}

[Fact]
Expand Down
1 change: 1 addition & 0 deletions src/Aurio.FFmpeg.UnitTest/Aurio.FFmpeg.UnitTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
167 changes: 165 additions & 2 deletions src/Aurio.FFmpeg.UnitTest/FFmpegSourceStreamTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using Moq;
using Xunit;

namespace Aurio.FFmpeg.UnitTest
Expand Down Expand Up @@ -80,7 +82,7 @@ public void SuggestWaveProxyFileInfo_WithDirectory_Unix()
}

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

Expand All @@ -92,7 +94,7 @@ public void MKV_NoStreamDuration_SeekingSupported()
}

[Fact]
public void TS_NonZeroStartTime_SeekingSupported()
public void NonZeroStartTime_SeekingSupported()
{
var fileInfo = new FileInfo("./Resources/sine440-44100-16-mono-200ms.ts");
var s = new FFmpegSourceStream(fileInfo);
Expand All @@ -101,5 +103,166 @@ public void TS_NonZeroStartTime_SeekingSupported()

Assert.Equal(1000, s.Position);
}

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

Assert.Equal(0, s.Position);
}

[Fact]
public void ReadTwoFrames_UpdatePosition()
{
var fileInfo = new FileInfo("./Resources/sine440-44100-16-mono-200ms.ts");
var s = new FFmpegSourceStream(fileInfo);
var frameSize = 1152 * s.SampleBlockSize;
var buffer = new byte[frameSize];

var bytesRead = s.Read(buffer, 0, buffer.Length);

Assert.Equal(frameSize, s.Position);
Assert.Equal(frameSize, bytesRead);

bytesRead = s.Read(buffer, 0, buffer.Length);

Assert.Equal(frameSize * 2, s.Position);
Assert.Equal(frameSize, bytesRead);
}

[Fact]
public void ReadIncompleteFrames_UpdatePosition()
{
var fileInfo = new FileInfo("./Resources/sine440-44100-16-mono-200ms.ts");
var s = new FFmpegSourceStream(fileInfo);
var frameSize = 1152 * s.SampleBlockSize;
var buffer = new byte[frameSize];

// Read half frame
var bytesRead = s.Read(buffer, 0, buffer.Length / 2);

Assert.Equal(frameSize / 2, s.Position);
Assert.Equal(frameSize / 2, bytesRead);

// Read one sample
bytesRead = s.Read(buffer, 0, s.SampleBlockSize);

Assert.Equal(frameSize / 2 + s.SampleBlockSize, s.Position);
Assert.Equal(s.SampleBlockSize, bytesRead);
}

[Fact]
public void ThrowWhenFirstPtsCannotBeDetermined()
{
var readerMock = new Mock<FFmpegReader>(
new FileInfo("./Resources/sine440-44100-16-mono-200ms.ts"),
Type.Audio
);
readerMock
.Setup(
m =>
m.ReadFrame(
out It.Ref<long>.IsAny,
It.IsAny<byte[]>(),
It.IsAny<int>(),
out It.Ref<Type>.IsAny
)
)
.Returns(-1);

var act = () => new FFmpegSourceStream(readerMock.Object);

Assert.Throws<FFmpegSourceStream.FileNotSeekableException>(act);
readerMock.Verify(
m =>
m.ReadFrame(
out It.Ref<long>.IsAny,
It.IsAny<byte[]>(),
It.IsAny<int>(),
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)
);
}

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

s.Position = position;

Assert.Equal(position, s.Position);
}

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

s.Position = position;
var bytesRead = s.Read(new byte[1000], 0, 1000);

Assert.Equal(0, bytesRead);
}
}
}
2 changes: 1 addition & 1 deletion src/Aurio.FFmpeg/FFmpegReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ public long FrameBufferSize
}
}

public int ReadFrame(
public virtual int ReadFrame(
out long timestamp,
byte[] output_buffer,
int output_buffer_size,
Expand Down
Loading
Loading