diff --git a/nativesrc/aurioffmpegproxy/proxy.c b/nativesrc/aurioffmpegproxy/proxy.c index 9409e9f1..6b9dcbf8 100644 --- a/nativesrc/aurioffmpegproxy/proxy.c +++ b/nativesrc/aurioffmpegproxy/proxy.c @@ -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. diff --git a/src/Aurio.FFmpeg.UnitTest/AudioDecode.cs b/src/Aurio.FFmpeg.UnitTest/AudioDecode.cs index 307bf9c0..2e5c95d5 100644 --- a/src/Aurio.FFmpeg.UnitTest/AudioDecode.cs +++ b/src/Aurio.FFmpeg.UnitTest/AudioDecode.cs @@ -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 + } } } diff --git a/src/Aurio.FFmpeg.UnitTest/Aurio.FFmpeg.UnitTest.csproj b/src/Aurio.FFmpeg.UnitTest/Aurio.FFmpeg.UnitTest.csproj index ced1c761..cc61d17b 100644 --- a/src/Aurio.FFmpeg.UnitTest/Aurio.FFmpeg.UnitTest.csproj +++ b/src/Aurio.FFmpeg.UnitTest/Aurio.FFmpeg.UnitTest.csproj @@ -26,7 +26,7 @@ - + PreserveNewest diff --git a/src/Aurio.FFmpeg.UnitTest/FFmpegReaderTests.cs b/src/Aurio.FFmpeg.UnitTest/FFmpegReaderTests.cs new file mode 100644 index 00000000..c8c7b52a --- /dev/null +++ b/src/Aurio.FFmpeg.UnitTest/FFmpegReaderTests.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using Xunit; + +namespace Aurio.FFmpeg.UnitTest +{ + public class FFmpegReaderTests + { + /// + /// MKV does not carry individual stream lenghts, only a total container length. + /// + [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); + } + } +} diff --git a/src/Aurio.FFmpeg.UnitTest/FFmpegSourceStreamTests.cs b/src/Aurio.FFmpeg.UnitTest/FFmpegSourceStreamTests.cs index c0bee4d3..b60ecc32 100644 --- a/src/Aurio.FFmpeg.UnitTest/FFmpegSourceStreamTests.cs +++ b/src/Aurio.FFmpeg.UnitTest/FFmpegSourceStreamTests.cs @@ -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); + } } } diff --git a/src/Aurio.FFmpeg.UnitTest/Resources/sine440-44100-16-mono-200ms.mkv b/src/Aurio.FFmpeg.UnitTest/Resources/sine440-44100-16-mono-200ms.mkv new file mode 100644 index 00000000..3137d9c8 Binary files /dev/null and b/src/Aurio.FFmpeg.UnitTest/Resources/sine440-44100-16-mono-200ms.mkv differ diff --git a/src/Aurio.FFmpeg.UnitTest/Resources/sine440-44100-16-mono-200ms.ts b/src/Aurio.FFmpeg.UnitTest/Resources/sine440-44100-16-mono-200ms.ts new file mode 100644 index 00000000..b99a12d1 Binary files /dev/null and b/src/Aurio.FFmpeg.UnitTest/Resources/sine440-44100-16-mono-200ms.ts differ diff --git a/src/Aurio.FFmpeg/FFmpegReader.cs b/src/Aurio.FFmpeg/FFmpegReader.cs index e55dcf70..c3ef8464 100644 --- a/src/Aurio.FFmpeg/FFmpegReader.cs +++ b/src/Aurio.FFmpeg/FFmpegReader.cs @@ -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, diff --git a/src/Aurio.FFmpeg/FFmpegSourceStream.cs b/src/Aurio.FFmpeg/FFmpegSourceStream.cs index 36b98865..7bdd17e1 100644 --- a/src/Aurio.FFmpeg/FFmpegSourceStream.cs +++ b/src/Aurio.FFmpeg/FFmpegSourceStream.cs @@ -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 @@ -133,7 +129,50 @@ public long Length private long SamplePosition { - get { return readerPosition + sourceBufferPosition; } + get { return readerPosition + sourceBufferPosition - readerFirstPTS; } + } + + /// + /// 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. + /// + 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 @@ -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