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