117 lines
4.2 KiB
C#
117 lines
4.2 KiB
C#
using Sdcb.FFmpeg.Utils;
|
|
using T3.Core.Video;
|
|
using Xunit;
|
|
|
|
namespace T3.VideoServices.Tests;
|
|
|
|
/// <summary>
|
|
/// Exercises the real FFmpeg decode + seek path against the checked-in sample videos. These assert the
|
|
/// core M1 guarantees: correct metadata, monotonic sequential decode, frame-accurate seeking, and — most
|
|
/// importantly — that the same requested time deterministically resolves to the same frame.
|
|
/// </summary>
|
|
public class VideoDecoderSessionTests
|
|
{
|
|
[Fact]
|
|
public void Open720p_ReadsExpectedMetadata()
|
|
{
|
|
using var session = VideoDecoderSession.TryOpen(TestAssets.Video720p, VideoPlaybackOptimization.FastSeeking, out var error);
|
|
|
|
Assert.Null(error);
|
|
Assert.NotNull(session);
|
|
Assert.Equal(1280, session!.Width);
|
|
Assert.Equal(720, session.Height);
|
|
Assert.False(session.IsHdr);
|
|
Assert.InRange(session.FrameRate, 59.9, 60.1);
|
|
Assert.InRange(session.DurationSeconds, 59.0, 61.0);
|
|
Assert.True(session.TimeBaseDen > 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Open1080p_Succeeds()
|
|
{
|
|
using var session = VideoDecoderSession.TryOpen(TestAssets.Video1080p, VideoPlaybackOptimization.FastSeeking, out var error);
|
|
|
|
Assert.Null(error);
|
|
Assert.NotNull(session);
|
|
Assert.True(session!.Width > 0 && session.Height > 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Open_MissingFile_ReturnsError()
|
|
{
|
|
using var session = VideoDecoderSession.TryOpen("does-not-exist.mp4", VideoPlaybackOptimization.FastSeeking, out var error);
|
|
|
|
Assert.Null(session);
|
|
Assert.NotNull(error);
|
|
}
|
|
|
|
[Fact]
|
|
public void SequentialDecode_PtsAreMonotonic()
|
|
{
|
|
using var session = VideoDecoderSession.TryOpen(TestAssets.Video720p, VideoPlaybackOptimization.FastSeeking, out _);
|
|
Assert.NotNull(session);
|
|
|
|
var previous = long.MinValue;
|
|
var count = 0;
|
|
while (count < 30 && session!.TryReadNextFrame(out var pts))
|
|
{
|
|
Assert.True(pts > previous, $"PTS {pts} should be > previous {previous}");
|
|
previous = pts;
|
|
count++;
|
|
}
|
|
|
|
Assert.True(count >= 30, $"Expected at least 30 frames, decoded {count}");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1.0)]
|
|
[InlineData(2.0)]
|
|
[InlineData(4.0)]
|
|
public void Seek_IsFrameAccurate(double seconds)
|
|
{
|
|
using var session = VideoDecoderSession.TryOpen(TestAssets.Video720p, VideoPlaybackOptimization.FastSeeking, out _);
|
|
Assert.NotNull(session);
|
|
|
|
var target = TimeToFrameMapper.SecondsToPts(seconds, session!.StreamStartPts, session.TimeBaseNum, session.TimeBaseDen);
|
|
Assert.True(session.SeekAndDecodeTo(target, out var landed));
|
|
Assert.Equal(target, landed);
|
|
}
|
|
|
|
[Fact]
|
|
public void Seek_IsDeterministic_SameTimeYieldsSameFrame()
|
|
{
|
|
using var session = VideoDecoderSession.TryOpen(TestAssets.Video720p, VideoPlaybackOptimization.FastSeeking, out _);
|
|
Assert.NotNull(session);
|
|
|
|
var early = TimeToFrameMapper.SecondsToPts(1.0, session!.StreamStartPts, session.TimeBaseNum, session.TimeBaseDen);
|
|
var later = TimeToFrameMapper.SecondsToPts(3.0, session.StreamStartPts, session.TimeBaseNum, session.TimeBaseDen);
|
|
|
|
Assert.True(session.SeekAndDecodeTo(early, out var firstPts));
|
|
var firstChecksum = LumaChecksum(session.CurrentFrame);
|
|
|
|
// Seek elsewhere, then back — content must be identical, proving determinism across seeks.
|
|
Assert.True(session.SeekAndDecodeTo(later, out _));
|
|
Assert.True(session.SeekAndDecodeTo(early, out var secondPts));
|
|
var secondChecksum = LumaChecksum(session.CurrentFrame);
|
|
|
|
Assert.Equal(firstPts, secondPts);
|
|
Assert.Equal(firstChecksum, secondChecksum);
|
|
}
|
|
|
|
// Cheap deterministic hash of the luma plane (sparse sample) to prove frame-content identity.
|
|
private static unsafe long LumaChecksum(Frame frame)
|
|
{
|
|
var y = (byte*)frame.Data[0];
|
|
var stride = frame.Linesize[0];
|
|
long sum = 0;
|
|
for (var row = 0; row < frame.Height; row++)
|
|
{
|
|
var line = y + (long)row * stride;
|
|
for (var x = 0; x < frame.Width; x += 7)
|
|
sum += line[x];
|
|
}
|
|
|
|
return sum;
|
|
}
|
|
}
|