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.
54 lines
2.0 KiB
C#
54 lines
2.0 KiB
C#
using System.Globalization;
|
|
|
|
namespace ytLive.Services;
|
|
|
|
/// <summary>
|
|
/// TASK 21 (slice 2): parses the native frame rate of a video from ffprobe
|
|
/// <c>-show_entries stream=avg_frame_rate,r_frame_rate</c> output, which reports
|
|
/// each as a rational like <c>30/1</c> or <c>60000/1001</c> (or <c>0/0</c> when
|
|
/// unknown). Prefers <c>avg_frame_rate</c> (the true display rate over the whole
|
|
/// file, correct for VFR) and falls back to <c>r_frame_rate</c> (the container/
|
|
/// codec rate). Pure: no ffprobe, no I/O, unit-testable.
|
|
/// </summary>
|
|
public static class FfmpegFrameRateParser
|
|
{
|
|
public static double? ParseFrameRate(string probeOutput)
|
|
{
|
|
foreach (var key in new[] { "avg_frame_rate=", "r_frame_rate=" })
|
|
{
|
|
var value = FindValue(probeOutput, key);
|
|
if (value is not null && TryParseRational(value, out var fps))
|
|
return fps;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>The trimmed value of the first line beginning with <paramref name="key"/>.</summary>
|
|
private static string? FindValue(string text, string key)
|
|
{
|
|
foreach (var line in text.Split('\n'))
|
|
{
|
|
var trimmed = line.Trim();
|
|
if (trimmed.StartsWith(key, StringComparison.Ordinal))
|
|
return trimmed.Substring(key.Length).Trim();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Parses <c>v</c> as <c>N</c> (integer) or <c>N/M</c> (rational) into
|
|
/// a frames-per-second double. <c>0</c> and <c>0/M</c> count as unknown.</summary>
|
|
private static bool TryParseRational(string value, out double fps)
|
|
{
|
|
fps = 0;
|
|
var parts = value.Split('/');
|
|
if (parts.Length == 0 || !int.TryParse(parts[0], NumberStyles.None, CultureInfo.InvariantCulture, out var num))
|
|
return false;
|
|
var den = 1;
|
|
if (parts.Length > 1 && !int.TryParse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture, out den))
|
|
return false;
|
|
if (num <= 0 || den <= 0) return false;
|
|
fps = (double)num / den;
|
|
return true;
|
|
}
|
|
}
|