< Summary

Information
Class: Anichron.Worker.Ingestion.Proxy.FfmpegException
Assembly: Anichron.Worker
File(s): /home/runner/work/anichron/anichron/src/Anichron.Worker/Ingestion/Proxy/VideoProcessor.cs
Tag: 228_36821334185
Line coverage
62%
Covered lines: 5
Uncovered lines: 3
Coverable lines: 8
Total lines: 137
Line coverage: 62.5%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%210%
.ctor(...)100%210%
.ctor(...)100%210%
.ctor(...)100%11100%

File(s)

/home/runner/work/anichron/anichron/src/Anichron.Worker/Ingestion/Proxy/VideoProcessor.cs

#LineLine coverage
 1using Anichron.Worker.Settings;
 2using Microsoft.Extensions.Options;
 3
 4namespace Anichron.Worker.Ingestion.Proxy;
 5
 6internal interface IVideoProcessor
 7{
 8    Task TranscodeAsync(string sourcePath, string outputPath, CancellationToken ct);
 9}
 10
 11internal sealed partial class FfmpegVideoProcessor(
 12    IProcessLauncher processLauncher,
 13    IOptions<WorkerSettings> options,
 14    ILogger<FfmpegVideoProcessor> logger) : IVideoProcessor, IDisposable
 15{
 16    private static readonly string[] encoderPriority =
 17    [
 18        H264Encoder.QuickSync,
 19        H264Encoder.Nvenc,
 20        H264Encoder.Amf,
 21        H264Encoder.Software,
 22    ];
 23
 24    private readonly SemaphoreSlim encoderDetectionLock = new(1, 1);
 25    private string? detectedEncoder;
 26
 27    public async Task TranscodeAsync(string sourcePath, string outputPath, CancellationToken ct)
 28    {
 29        string encoder;
 30        await encoderDetectionLock.WaitAsync(ct);
 31        try
 32        {
 33            detectedEncoder ??= await DetectEncoderAsync(ct);
 34            encoder = detectedEncoder;
 35        }
 36        finally
 37        {
 38            encoderDetectionLock.Release();
 39        }
 40
 41        await RunFfmpegAsync(sourcePath, outputPath, encoder, ct);
 42    }
 43
 44    private async Task<string> DetectEncoderAsync(CancellationToken ct)
 45    {
 46        foreach (var encoder in encoderPriority)
 47        {
 48            if (await ProbeEncoderAsync(encoder, ct))
 49            {
 50                Log.EncoderSelected(logger, encoder);
 51                return encoder;
 52            }
 53
 54            if (encoder != H264Encoder.Software)
 55                Log.EncoderUnavailable(logger, encoder);
 56        }
 57
 58        return H264Encoder.Software;
 59    }
 60
 61    private async Task<bool> ProbeEncoderAsync(string encoder, CancellationToken ct)
 62    {
 63        // Encode a tiny synthetic clip to /dev/null — success means the encoder is available.
 64        // /dev/null is Linux-only; the deployment target is linux/amd64 + linux/arm64 (see Dockerfile).
 65        string[] arguments =
 66        [
 67            "-f", "lavfi",
 68            "-i", "color=black:s=64x64:r=1:d=0.1",
 69            "-c:v", encoder,
 70            "-f", "mp4",
 71            "/dev/null",
 72            "-y",
 73        ];
 74        var result = await processLauncher.RunAsync(options.Value.FfmpegPath, arguments, ct);
 75        return result.ExitCode == 0;
 76    }
 77
 78    private async Task RunFfmpegAsync(string sourcePath, string outputPath, string encoder, CancellationToken ct)
 79    {
 80        // Scale to max height, encode H.264 High/4.0 with yuv420p (universal browser compat),
 81        // target bitrate, AAC audio, faststart for progressive web loading.
 82        string[] arguments =
 83        [
 84            "-i", sourcePath,
 85            "-vf", $"scale=-2:'min({options.Value.VideoMaxHeight},ih)'",
 86            "-c:v", encoder,
 87            "-pix_fmt", "yuv420p",
 88            "-profile:v", "high",
 89            "-level:v", "4.0",
 90            "-b:v", $"{options.Value.VideoBitrateKbps}k",
 91            "-c:a", "aac",
 92            "-b:a", "128k",
 93            "-movflags", "+faststart",
 94            "-y",
 95            outputPath,
 96        ];
 97        var result = await processLauncher.RunAsync(options.Value.FfmpegPath, arguments, ct);
 98        if (result.ExitCode != 0)
 99            throw new FfmpegException(options.Value.FfmpegPath, string.Join(" ", arguments), result.ExitCode, result.Sta
 100    }
 101
 102    public void Dispose() => encoderDetectionLock.Dispose();
 103
 104    private static partial class Log
 105    {
 106        [LoggerMessage(Level = LogLevel.Information, Message = "Selected H.264 encoder: {Encoder}.")]
 107        public static partial void EncoderSelected(ILogger logger, string encoder);
 108
 109        [LoggerMessage(Level = LogLevel.Debug, Message = "H.264 encoder {Encoder} is unavailable; trying next.")]
 110        public static partial void EncoderUnavailable(ILogger logger, string encoder);
 111    }
 112}
 113
 114internal static class H264Encoder
 115{
 116    internal const string QuickSync = "h264_qsv";
 117    internal const string Nvenc = "h264_nvenc";
 118    internal const string Amf = "h264_amf";
 119    internal const string Software = "libx264";
 120}
 121
 122public sealed class FfmpegException : Exception
 123{
 124    public int ExitCode { get; }
 125    public string Stderr { get; } = string.Empty;
 126
 0127    public FfmpegException() { }
 0128    public FfmpegException(string message) : base(message) { }
 0129    public FfmpegException(string message, Exception innerException) : base(message, innerException) { }
 130
 131    public FfmpegException(string ffmpegPath, string arguments, int exitCode, string stderr)
 4132        : base($"FFmpeg exited with code {exitCode}. Args: {ffmpegPath} {arguments}\nStderr: {stderr}")
 4133    {
 4134        ExitCode = exitCode;
 4135        Stderr = stderr;
 4136    }
 137}