8fa78423e6
- FfmpegFrameRateParser (pure): prefers avg_frame_rate= then r_frame_rate=, rational N/N/M, unknown/0 -> null. - IFrameRateProbe + FfmpegFrameRateProbe: derives sibling ffprobe.exe from the located ffmpeg dir, reuses the IDecodeProcess seam for the ffprobe subprocess text; null if ffprobe absent. - FfmpegLocator now also extracts ffprobe.exe (ProbeFileName) from the pinned archive, conditional so old caches without it degrade to no pacing. - Tests: 6 pure parser units + 1 probe integration via fake locator/process; FfmpegLocatorTests still green. 15/15, 0 warnings. Pacing (probe->delay) is slice 2b. Derivative reference: standard ffprobe avg_frame_rate probing used across OBS/media tooling.
117 lines
3.9 KiB
C#
117 lines
3.9 KiB
C#
using System.Diagnostics;
|
|
using System.IO;
|
|
using Xunit;
|
|
using ytLive.Services;
|
|
using ytLive.Services.Encoder;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// TASK 21 (slice 2): the ffprobe frame-rate parser is pure (synthetic text, no
|
|
/// probe); the one integration test drives the real <see cref="FfmpegFrameRateProbe"/>
|
|
/// against a fake locator + fake probe subprocess end to end.
|
|
/// </summary>
|
|
public class FfmpegFrameRateParserTests
|
|
{
|
|
// ─── Pure parser (unit) ───
|
|
|
|
[Fact]
|
|
public void Parse_AvgFrameRate_Rational()
|
|
{
|
|
var out0 = "codec_name=h264\navg_frame_rate=30/1\nr_frame_rate=30/1\n";
|
|
Assert.Equal(30.0, FfmpegFrameRateParser.ParseFrameRate(out0));
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_AvgFrameRate_29_97()
|
|
{
|
|
var out0 = "avg_frame_rate=30000/1001\nr_frame_rate=30/1\n";
|
|
Assert.Equal(30000.0 / 1001.0, FfmpegFrameRateParser.ParseFrameRate(out0)!.Value, 3);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_PrefersAvgOverRFrame()
|
|
{
|
|
var out0 = "avg_frame_rate=25/1\nr_frame_rate=30000/1001\n";
|
|
Assert.Equal(25.0, FfmpegFrameRateParser.ParseFrameRate(out0));
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_FallsBackToRFrame_WhenAvgUnknown()
|
|
{
|
|
var out0 = "avg_frame_rate=0/0\nr_frame_rate=50/1\n";
|
|
Assert.Equal(50.0, FfmpegFrameRateParser.ParseFrameRate(out0));
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_IntegerValue_HasUnitDenominator()
|
|
{
|
|
var out0 = "avg_frame_rate=60\n";
|
|
Assert.Equal(60.0, FfmpegFrameRateParser.ParseFrameRate(out0));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("avg_frame_rate=0/0\nr_frame_rate=0/0\n")]
|
|
[InlineData("")]
|
|
[InlineData("codec_name=h264\n")]
|
|
public void Parse_Unknown_ReturnsNull(string output)
|
|
{
|
|
Assert.Null(FfmpegFrameRateParser.ParseFrameRate(output));
|
|
}
|
|
|
|
// ─── The one integration test: probe against fake locator + fake subprocess ───
|
|
|
|
[Fact]
|
|
public async Task Probe_ReturnsParsedFps_FromFakeSubprocess()
|
|
{
|
|
var toolsDir = CreateTempDir();
|
|
try
|
|
{
|
|
// ffprobe.exe must exist next to the located ffmpeg for the sibling
|
|
// derivation to return a usable path.
|
|
var probeExe = Path.Combine(toolsDir, "ffprobe.exe");
|
|
File.WriteAllText(probeExe, "ffprobe");
|
|
|
|
var fakeOutput = "codec_name=h264\navg_frame_rate=24000/1001\nr_frame_rate=24000/1001\n";
|
|
var probe = new FfmpegFrameRateProbe(
|
|
new FakeLocator(toolsDir),
|
|
() => new FakeProbeProcess(System.Text.Encoding.UTF8.GetBytes(fakeOutput)));
|
|
|
|
var fps = await probe.ProbeAsync(@"C:\videos\clip.mp4");
|
|
Assert.Equal(24000.0 / 1001.0, fps!.Value, 3);
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(toolsDir, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static string CreateTempDir()
|
|
{
|
|
var dir = Path.Combine(Path.GetTempPath(), "ytllive-tests-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(dir);
|
|
return dir;
|
|
}
|
|
|
|
private sealed class FakeLocator : IFfmpegLocator
|
|
{
|
|
private readonly string _dir;
|
|
public FakeLocator(string dir) => _dir = dir;
|
|
public Task<string> LocateAsync(CancellationToken ct = default)
|
|
=> Task.FromResult(Path.Combine(_dir, "ffmpeg.exe"));
|
|
}
|
|
|
|
private sealed class FakeProbeProcess : IDecodeProcess
|
|
{
|
|
private readonly MemoryStream _stream;
|
|
public FakeProbeProcess(byte[] bytes) => _stream = new MemoryStream(bytes);
|
|
public void Start(ProcessStartInfo startInfo) { }
|
|
public Stream StandardOutput => _stream;
|
|
public bool HasExited => _stream.Position >= _stream.Length;
|
|
public int ExitCode => 0;
|
|
public void Kill() { }
|
|
public Task WaitForExitAsync(CancellationToken ct = default) => Task.CompletedTask;
|
|
public void Dispose() => _stream.Dispose();
|
|
}
|
|
}
|