using System.Diagnostics; namespace splitter.probe; public sealed class VideoRotationSampler { private readonly FrameRotationDetector _detector = new FrameRotationDetector(); public static int RotationDetectorSampleCount = 10; public static double RotationDetectorSampleLength = 0.15; // seconds to decode per probe public static int RotationDetectorFrameWidth = 320; public static int RotationDetectorFrameHeight = 180; // --- Zero-allocation buffers --- private readonly byte[] _buffer; private readonly Mat _frameMat; public VideoRotationSampler(IDictionary? overrides) { if (overrides != null) { if (overrides.TryGetValue("RotationDetectorSampleCount", out var s)) RotationDetectorSampleCount = int.Parse(s); if (overrides.TryGetValue("RotationDetectorSampleLength", out s)) RotationDetectorSampleLength = double.Parse(s); if (overrides.TryGetValue("RotationDetectorFrameWidth", out s)) RotationDetectorFrameWidth = int.Parse(s); if (overrides.TryGetValue("RotationDetectorFrameHeight", out s)) RotationDetectorFrameHeight = int.Parse(s); } var w = RotationDetectorFrameWidth; var h = RotationDetectorFrameHeight; _buffer = new byte[w * h * 3]; // raw BGR24 buffer _frameMat = new Mat(h, w, MatType.CV_8UC3); // wraps buffer } public async Task DetectRotationAsync( string inputFile, double videoLengthSeconds, CancellationToken token) { if (videoLengthSeconds <= 0) return 0; var rotations = new List(); for (var i = 0; i < RotationDetectorSampleCount; i++) { var t = videoLengthSeconds * (i + 1) / (RotationDetectorSampleCount + 1); var frame = await DecodeSingleFrameAsync( inputFile, t, RotationDetectorSampleLength, RotationDetectorFrameWidth, RotationDetectorFrameHeight, token); if (frame != null && !frame.Empty()) { var rot = _detector.GetRotation(frame); rotations.Add(rot); } } if (rotations.Count == 0) return 0; return Majority(rotations); } private static int Majority(List values) { var counts = new Dictionary(); foreach (var v in values) { if (!counts.ContainsKey(v)) counts[v] = 0; counts[v]++; } var best = 0; var bestCount = 0; foreach (var kv in counts) { if (kv.Value > bestCount) { best = kv.Key; bestCount = kv.Value; } } return best; } private async Task DecodeSingleFrameAsync( string inputFile, double start, double length, int width, int height, CancellationToken token) { var p = StartFfmpegDecode(inputFile, start, length, rotate: null, plainText: false); var needed = _buffer.Length; var read = 0; using var stdout = p.StandardOutput.BaseStream; while (read < needed) { token.ThrowIfCancellationRequested(); var r = await stdout.ReadAsync(_buffer, read, needed - read, token); if (r == 0) return null; read += r; } try { p.Kill(); } catch { } // Copy buffer → Mat (no new Mat) System.Runtime.InteropServices.Marshal.Copy(_buffer, 0, _frameMat.Data, _buffer.Length); return _frameMat; } private Process StartFfmpegDecode( string inputFile, double start, double length, int? rotate, bool plainText) { var ss = start.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture); var t = length.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture); // FFmpeg does the resize + format conversion var args = $"-ss {ss} -t {t} -i \"{inputFile}\" " + "-an -sn " + $"-vf scale={RotationDetectorFrameWidth}:{RotationDetectorFrameHeight},format=bgr24 " + "-f rawvideo -"; var psi = new ProcessStartInfo { FileName = "ffmpeg", Arguments = args, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; var p = new Process { StartInfo = psi }; p.Start(); // Optional stderr logging _ = Task.Run(() => { try { string? line; while ((line = p.StandardError.ReadLine()) != null) if (plainText) Console.WriteLine($"[ffmpeg-decode] {line}"); } catch { } }); return p; } }