using System.Diagnostics;
using System.IO;
using Xunit;
using ytLive.Services;
using ytLive.Services.Encoder;
namespace ytLive.Tests;
///
/// TASK 21 (slice 2): the ffprobe frame-rate parser is pure (synthetic text, no
/// probe); the one integration test drives the real
/// against a fake locator + fake probe subprocess end to end.
///
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 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();
}
}