Fix MPEG opening preroll synchronization

This commit is contained in:
gamer147
2026-08-11 16:39:30 -04:00
parent 650c10bc7a
commit d38e698b91
9 changed files with 128 additions and 55 deletions

View File

@@ -260,11 +260,10 @@ internal sealed class FfmpegMovieDecoder : IMovieDecoder
{
if (decodedFrames == 0)
throw new InvalidDataException("FFmpeg stream ended before producing a video frame");
long completionTime = _presentationDurationMs
?? PresentationDeadline(
Math.Max(_source.Info.StopTimeMs,
lastTimestamp + FrameIntervalMilliseconds(_source.Info)),
firstTimestamp);
long sourceEndpoint = Math.Max(
_source.Info.StopTimeMs,
lastTimestamp + FrameIntervalMilliseconds(_source.Info));
long completionTime = CompletionDeadline(sourceEndpoint, firstTimestamp);
if (_clock.WaitUntil(completionTime, _cancel))
{
_videoTimelineCompleted = true;
@@ -446,6 +445,19 @@ internal sealed class FfmpegMovieDecoder : IMovieDecoder
return checked(sourceElapsed * duration / _source.Info.StopTimeMs);
}
private long CompletionDeadline(long sourceEndpoint, long firstVideoTimestamp)
{
if (_presentationDurationMs is { } duration) return duration;
if (AudioInfo != null)
{
// The audio pin begins at the common mux origin even when the video stream declares
// a later start. Keep the terminal image while the audio-master clock drains through
// the full graph endpoint rather than completing at the last video sample.
return Math.Max(0, sourceEndpoint - _initialPositionMs);
}
return PresentationDeadline(sourceEndpoint, firstVideoTimestamp);
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0) return;

View File

@@ -449,6 +449,28 @@ public class MovieOpcodeTests
Assert.Null(decoder.Failure);
}
[Fact]
public void FfmpegAudioBearingDecoderHoldsTerminalFrameUntilGraphAudioEndpoint()
{
var source = new FakeFfmpegFrameSource(
new FfmpegMovieInfo(1, 1, 700, 30, 1, true, 1000, 2, 700),
SyntheticMovieFrame(1, 600), SyntheticMovieFrame(2, 634));
source.EnqueueAudio(new FfmpegAudioChunk(new float[1400], 700, 0));
using var clock = new ManualMoviePacingClock();
using var decoder = new FfmpegMovieDecoder(source, clock);
Assert.True(SpinWait.SpinUntil(() => decoder.TryTakeFrame(out _), 1000));
Assert.True(SpinWait.SpinUntil(() => decoder.TryTakeAudioChunk(out _), 1000));
Assert.True(SpinWait.SpinUntil(() => decoder.AudioDecodingCompleted, 1000));
decoder.MarkAudioSubmitted();
clock.AdvanceTo(699);
Assert.True(clock.WaitForDeadline(700));
Assert.False(decoder.IsCompleted);
clock.AdvanceTo(700);
Assert.True(SpinWait.SpinUntil(() => decoder.IsCompleted, 1000));
}
[Fact]
public void MovieAudioTimelineDoesNotSpliceContinuousMpegBlocksAtRoundedMillisecondTimestamps()
{
@@ -801,11 +823,15 @@ public class MovieOpcodeTests
Assert.Equal(600, movie.Info.Height);
Assert.Equal(1000, movie.Info.StopTimeMs);
Assert.False(movie.Info.HasAudio);
Assert.True(movie.TryDecodeNextVideoFrame(out var frame));
byte[] mask = MovieMaskSurface.ExtractGreen(frame.Image, 800, 600, 255);
Assert.Equal(800 * 600, mask.Length);
Assert.True(mask.Distinct().Skip(1).Any(), "TEST.AGF should publish a nonuniform green mask");
byte[]? mask = null;
for (int frameIndex = 0; frameIndex < 60 && movie.TryDecodeNextVideoFrame(out var frame); frameIndex++)
{
mask = MovieMaskSurface.ExtractGreen(frame.Image, 800, 600, 255);
Assert.Equal(800 * 600, mask.Length);
if (mask.Distinct().Skip(1).Any()) break;
}
Assert.NotNull(mask);
Assert.True(mask!.Distinct().Skip(1).Any(), "TEST.AGF should publish a nonuniform green mask");
}
[Theory]
@@ -832,6 +858,8 @@ public class MovieOpcodeTests
$"{expectedName} ended before opening frame {frameIndex}");
if (priorTimestamp >= 0)
Assert.InRange(frame.PresentationTimeMs - priorTimestamp, 33, 34);
else
Assert.Equal(0, frame.PresentationTimeMs);
priorTimestamp = frame.PresentationTimeMs;
firstPixels ??= frame.Image.Pixels;
openingChanged |= !firstPixels.AsSpan().SequenceEqual(frame.Image.Pixels);
@@ -868,7 +896,7 @@ public class MovieOpcodeTests
Assert.True(first.PresentationTimeMs >= 0);
bool changed = false;
long priorTimestamp = first.PresentationTimeMs;
for (int frameIndex = 0; frameIndex < 30 && movie.TryDecodeNextVideoFrame(out var later); frameIndex++)
for (int frameIndex = 0; frameIndex < 120 && movie.TryDecodeNextVideoFrame(out var later); frameIndex++)
{
Assert.True(later.PresentationTimeMs >= priorTimestamp);
priorTimestamp = later.PresentationTimeMs;