< Summary

Line coverage
94%
Covered lines: 69
Uncovered lines: 4
Coverable lines: 73
Total lines: 240
Line coverage: 94.5%
Branch coverage
100%
Covered branches: 10
Total branches: 10
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: .cctor()100%210%
File 2: .ctor(...)100%11100%
File 2: .cctor()100%11100%
File 2: TranscodeAsync()100%22100%
File 2: DetectEncoderAsync()100%66100%
File 2: ProbeEncoderAsync()100%11100%
File 2: RunFfmpegAsync()100%22100%
File 2: Dispose()100%11100%

File(s)

/home/runner/work/anichron/anichron/src/.artifacts/obj/Anichron.Worker/debug/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs

File '/home/runner/work/anichron/anichron/src/.artifacts/obj/Anichron.Worker/debug/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs' does not exist (any more).

/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
 811internal sealed partial class FfmpegVideoProcessor(
 812    IProcessLauncher processLauncher,
 813    IOptions<WorkerSettings> options,
 814    ILogger<FfmpegVideoProcessor> logger) : IVideoProcessor, IDisposable
 15{
 116    private static readonly string[] encoderPriority =
 117    [
 118        H264Encoder.QuickSync,
 119        H264Encoder.Nvenc,
 120        H264Encoder.Amf,
 121        H264Encoder.Software,
 122    ];
 23
 824    private readonly SemaphoreSlim encoderDetectionLock = new(1, 1);
 25    private string? detectedEncoder;
 26
 27    public async Task TranscodeAsync(string sourcePath, string outputPath, CancellationToken ct)
 828    {
 29        string encoder;
 830        await encoderDetectionLock.WaitAsync(ct);
 31        try
 832        {
 833            detectedEncoder ??= await DetectEncoderAsync(ct);
 834            encoder = detectedEncoder;
 835        }
 36        finally
 837        {
 838            encoderDetectionLock.Release();
 839        }
 40
 841        await RunFfmpegAsync(sourcePath, outputPath, encoder, ct);
 742    }
 43
 44    private async Task<string> DetectEncoderAsync(CancellationToken ct)
 745    {
 3746        foreach (var encoder in encoderPriority)
 1147        {
 1148            if (await ProbeEncoderAsync(encoder, ct))
 649            {
 650                Log.EncoderSelected(logger, encoder);
 651                return encoder;
 52            }
 53
 554            if (encoder != H264Encoder.Software)
 455                Log.EncoderUnavailable(logger, encoder);
 556        }
 57
 158        return H264Encoder.Software;
 759    }
 60
 61    private async Task<bool> ProbeEncoderAsync(string encoder, CancellationToken ct)
 1162    {
 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).
 1165        string[] arguments =
 1166        [
 1167            "-f", "lavfi",
 1168            "-i", "color=black:s=64x64:r=1:d=0.1",
 1169            "-c:v", encoder,
 1170            "-f", "mp4",
 1171            "/dev/null",
 1172            "-y",
 1173        ];
 1174        var result = await processLauncher.RunAsync(options.Value.FfmpegPath, arguments, ct);
 1175        return result.ExitCode == 0;
 1176    }
 77
 78    private async Task RunFfmpegAsync(string sourcePath, string outputPath, string encoder, CancellationToken ct)
 879    {
 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.
 882        string[] arguments =
 883        [
 884            "-i", sourcePath,
 885            "-vf", $"scale=-2:'min({options.Value.VideoMaxHeight},ih)'",
 886            "-c:v", encoder,
 887            "-pix_fmt", "yuv420p",
 888            "-profile:v", "high",
 889            "-level:v", "4.0",
 890            "-b:v", $"{options.Value.VideoBitrateKbps}k",
 891            "-c:a", "aac",
 892            "-b:a", "128k",
 893            "-movflags", "+faststart",
 894            "-y",
 895            outputPath,
 896        ];
 897        var result = await processLauncher.RunAsync(options.Value.FfmpegPath, arguments, ct);
 898        if (result.ExitCode != 0)
 199            throw new FfmpegException(options.Value.FfmpegPath, string.Join(" ", arguments), result.ExitCode, result.Sta
 7100    }
 101
 1102    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
 127    public FfmpegException() { }
 128    public FfmpegException(string message) : base(message) { }
 129    public FfmpegException(string message, Exception innerException) : base(message, innerException) { }
 130
 131    public FfmpegException(string ffmpegPath, string arguments, int exitCode, string stderr)
 132        : base($"FFmpeg exited with code {exitCode}. Args: {ffmpegPath} {arguments}\nStderr: {stderr}")
 133    {
 134        ExitCode = exitCode;
 135        Stderr = stderr;
 136    }
 137}