From fc915be3aaa797f403c52b1f4428e0c9adcdbc29 Mon Sep 17 00:00:00 2001 From: Samuele Lorefice Date: Mon, 15 Dec 2025 21:32:49 +0100 Subject: [PATCH] Applied strategy pattern to Encoder, refactored and reorganized code --- Encoder/EncoderService.cs | 64 ++++++--------- Encoder/IEncoderService.cs | 6 -- Encoder/OpenApi.http | 5 -- Encoder/Strategies.cs | 157 +++++++++++++++++++++++++++++++++++++ Encoder/Utils.cs | 10 +-- 5 files changed, 184 insertions(+), 58 deletions(-) delete mode 100644 Encoder/IEncoderService.cs delete mode 100644 Encoder/OpenApi.http create mode 100644 Encoder/Strategies.cs diff --git a/Encoder/EncoderService.cs b/Encoder/EncoderService.cs index 4b5037c..e7414ec 100644 --- a/Encoder/EncoderService.cs +++ b/Encoder/EncoderService.cs @@ -1,16 +1,32 @@ -using System.Numerics; using FFMpegCore; -using FFMpegCore.Enums; using Microsoft.Extensions.Options; namespace Encoder; +public interface IEncoderService { + public Guid EnqueueJob(EncodingJob job); + public EncodingJob? GetJobStatus(Guid jobId); +} + public class EncoderService : BackgroundService, IEncoderService { Queue JobQueue; List Jobs = new(); ILogger Logger; FFmpegOptions options; + void Progress(EncodingProgress data) { + Logger.Log(LogLevel.Information, + //Using AV1 NVENC with QP={qp} for {W}x{H}@{framerate}. + $""" + Job {data.job.Id}: {data.progress / data.duration:P} + Processed {data.progress:g} | Total {data.duration:g} + + In path: {data.inputPath} + Output path: {data.outputPath} + """); + data.job.Progress = (float)(data.progress / data.duration); + } + public EncoderService(ILogger logger, IOptions ffmpegOptions) { Logger = logger; options = ffmpegOptions.Value; @@ -64,44 +80,16 @@ public class EncoderService : BackgroundService, IEncoderService { async Task ProcessJob(EncodingJob job, CancellationToken cancellationToken) { job.Status = JobStatus.InProgress; var file = job.FilePath; - var mediaInfo = await FFProbe.AnalyseAsync(file, cancellationToken: cancellationToken); - if (mediaInfo.PrimaryVideoStream == null) { - job.Status = JobStatus.Failed; - return; - } - - int W = mediaInfo.PrimaryVideoStream.Width; - int H = mediaInfo.PrimaryVideoStream.Height; string outputPath = Path.Combine(options.TemporaryFilesPath, Path.GetFileName(job.FilePath)); - int qp = Utils.ToQPValue(W, H); - var result = FFMpegArguments - .FromFileInput(file, true, args => args.WithHardwareAcceleration()) - .OutputToFile(outputPath, true, args => args - .CopyChannel(Channel.Audio) - .CopyChannel(Channel.Subtitle) - .WithVideoCodec("av1_nvenc") - .WithArgument(new NvencSpeedPreset(NvencSpeed.p2)) - .WithArgument(new NvencTuneArgument(NvencTune.hq)) - .WithArgument(new NvencHighBitDepthArgument(true)) - .WithArgument(new NvencQPArgument((byte)qp)) - .WithFastStart() - ) - .NotifyOnProgress(progress => - { - Logger.Log(LogLevel.Information, - $""" - Job {job.Id}: {progress / mediaInfo.Duration:P} - Processed {progress:g} | Total {mediaInfo.Duration:g} - Using AV1 NVENC with QP={qp} for {W}x{H}@{mediaInfo.PrimaryVideoStream.FrameRate}. - In path: {file} - Output path: {outputPath} - """); - job.Progress = (float)(progress / mediaInfo.Duration); - }).CancellableThrough(cancellationToken) - .ProcessAsynchronously(); - result.Wait(cancellationToken); + IEncodingStrategy strategy = job.RequestedEncoding switch { + EncoderType.H264 => new H264EncodingStrategy(file, outputPath, job), + EncoderType.HEVC => new HEVCEncodingStrategy(file, outputPath, job), + EncoderType.AV1 => new AV1EncodingStrategy(file, outputPath, job), + _ => throw new ArgumentOutOfRangeException(nameof(job), job, null) + }; + bool result = await strategy.ExecuteAsync(cancellationToken, Progress); - if(result.Result) { + if(result) { job.Status = JobStatus.Completed; job.EncodedFilePath = outputPath; File.Delete(file); // Clean up original file diff --git a/Encoder/IEncoderService.cs b/Encoder/IEncoderService.cs deleted file mode 100644 index f25415e..0000000 --- a/Encoder/IEncoderService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Encoder; - -public interface IEncoderService { - public Guid EnqueueJob(EncodingJob job); - public EncodingJob? GetJobStatus(Guid jobId); -} \ No newline at end of file diff --git a/Encoder/OpenApi.http b/Encoder/OpenApi.http deleted file mode 100644 index 2a53f4f..0000000 --- a/Encoder/OpenApi.http +++ /dev/null @@ -1,5 +0,0 @@ -### GET request to example server -GET https://examples.http-client.intellij.net/get - ?generated-in=JetBrains Rider - -### \ No newline at end of file diff --git a/Encoder/Strategies.cs b/Encoder/Strategies.cs new file mode 100644 index 0000000..bd26381 --- /dev/null +++ b/Encoder/Strategies.cs @@ -0,0 +1,157 @@ +using FFMpegCore; +using FFMpegCore.Enums; + +namespace Encoder; + +public struct EncodingProgress { + public TimeSpan progress; + public TimeSpan duration; + public float framerate; + public EncodingJob job; + public string inputPath; + public string outputPath; +} + +public class EncodingStrategyAttribute(EncoderType encoderType) : Attribute { + public EncoderType EncoderType { get; } = encoderType; +} + +public interface IEncodingStrategy { + public string InputFilePath { get; set; } + public string OutputFilePath { get; set; } + public Task ExecuteAsync(CancellationToken cancellationToken, Action onProgress); +} + +[EncodingStrategy(EncoderType.AV1)] +public class AV1EncodingStrategy(string inputFilePath, string outputFilePath, EncodingJob job) : IEncodingStrategy { + public string InputFilePath { get; set; } = inputFilePath; + public string OutputFilePath { get; set; } = outputFilePath; + + private static readonly Tuple[] QPTable = new Tuple[] { + new(1280*720, 64), + new(1920*1080, 96), + new(3840*2160, 128), + new(5760*2880, 96), //VR6K + new(8128*4096, 120) //VR8K + }.OrderBy(t => t.Item1).ToArray(); + + public async Task ExecuteAsync(CancellationToken cancellationToken, Action onProgress) { + var mediaInfo = await FFProbe.AnalyseAsync(InputFilePath, cancellationToken: cancellationToken); + if (mediaInfo.PrimaryVideoStream == null) return false; + int W = mediaInfo.PrimaryVideoStream.Width; + int H = mediaInfo.PrimaryVideoStream.Height; + int qp = Utils.ToQPValue(W, H, QPTable); + return await FFMpegArguments + .FromFileInput(InputFilePath, true, args => args.WithHardwareAcceleration()) + .OutputToFile(OutputFilePath, true, args => args + .CopyChannel(Channel.Audio) + .CopyChannel(Channel.Subtitle) + .WithVideoCodec("av1_nvenc") + .WithArgument(new NvencSpeedPreset(NvencSpeed.p2)) + .WithArgument(new NvencTuneArgument(NvencTune.hq)) + .WithArgument(new NvencHighBitDepthArgument(true)) + .WithArgument(new NvencQPArgument((byte)qp)) + .WithFastStart() + ) + .NotifyOnProgress(progress => onProgress(new() { + progress = progress, + duration = mediaInfo.Duration, + framerate = (float)mediaInfo.PrimaryVideoStream.FrameRate, + job = job, + inputPath = InputFilePath, + outputPath = OutputFilePath + })) + .CancellableThrough(cancellationToken) + .ProcessAsynchronously(); + } +} + +[EncodingStrategy(EncoderType.HEVC)] +public class HEVCEncodingStrategy(string inputFilePath, string outputFilePath, EncodingJob job) : IEncodingStrategy { + public string InputFilePath { get; set; } = inputFilePath; + public string OutputFilePath { get; set; } = outputFilePath; + + //TODO: needs to be adjusted + private static readonly Tuple[] QPTable = new Tuple[] { + new(1280*720, 0), + new(1920*1080, 0), + new(3840*2160, 0), + new(5760*2880, 0), //VR6K + new(8128*4096, 0) //VR8K + }.OrderBy(t => t.Item1).ToArray(); + + public async Task ExecuteAsync(CancellationToken cancellationToken, Action onProgress) { + var mediaInfo = await FFProbe.AnalyseAsync(InputFilePath, cancellationToken: cancellationToken); + if (mediaInfo.PrimaryVideoStream == null) return false; + int W = mediaInfo.PrimaryVideoStream.Width; + int H = mediaInfo.PrimaryVideoStream.Height; + int qp = Utils.ToQPValue(W, H, QPTable); + return await FFMpegArguments + .FromFileInput(InputFilePath, true, args => args.WithHardwareAcceleration()) + .OutputToFile(OutputFilePath, true, args => args + .CopyChannel(Channel.Audio) + .CopyChannel(Channel.Subtitle) + .WithVideoCodec("hevc_nvenc") + .WithArgument(new NvencSpeedPreset(NvencSpeed.p2)) + .WithArgument(new NvencTuneArgument(NvencTune.hq)) + .WithArgument(new NvencHighBitDepthArgument(true)) + .WithArgument(new NvencQPArgument((byte)qp)) + .WithFastStart() + ) + .NotifyOnProgress(progress => onProgress(new() { + progress = progress, + duration = mediaInfo.Duration, + framerate = (float)mediaInfo.PrimaryVideoStream.FrameRate, + job = job, + inputPath = InputFilePath, + outputPath = OutputFilePath + })) + .CancellableThrough(cancellationToken) + .ProcessAsynchronously(); + } +} + +[EncodingStrategy(EncoderType.H264)] +public class H264EncodingStrategy(string inputFilePath, string outputFilePath, EncodingJob job) : IEncodingStrategy { + public string InputFilePath { get; set; } = inputFilePath; + public string OutputFilePath { get; set; } = outputFilePath; + + //TODO: needs to be adjusted + private static readonly Tuple[] QPTable = new Tuple[] { + new(1280*720, 0), + new(1920*1080, 0), + new(3840*2160, 0), + new(5760*2880, 0), //VR6K + new(8128*4096, 0) //VR8K + }.OrderBy(t => t.Item1).ToArray(); + + public async Task ExecuteAsync(CancellationToken cancellationToken, Action onProgress) { + var mediaInfo = await FFProbe.AnalyseAsync(InputFilePath, cancellationToken: cancellationToken); + if (mediaInfo.PrimaryVideoStream == null) return false; + int W = mediaInfo.PrimaryVideoStream.Width; + int H = mediaInfo.PrimaryVideoStream.Height; + int qp = Utils.ToQPValue(W, H, QPTable); + return await FFMpegArguments + .FromFileInput(InputFilePath, true, args => args.WithHardwareAcceleration()) + .OutputToFile(OutputFilePath, true, args => args + .CopyChannel(Channel.Audio) + .CopyChannel(Channel.Subtitle) + .WithVideoCodec("h264_nvenc") + .WithArgument(new NvencSpeedPreset(NvencSpeed.p2)) + .WithArgument(new NvencTuneArgument(NvencTune.hq)) + .WithArgument(new NvencHighBitDepthArgument(true)) + .WithArgument(new NvencQPArgument((byte)qp)) + .WithFastStart() + ) + .NotifyOnProgress(progress => onProgress(new() { + progress = progress, + duration = mediaInfo.Duration, + framerate = (float)mediaInfo.PrimaryVideoStream.FrameRate, + job = job, + inputPath = InputFilePath, + outputPath = OutputFilePath + })) + .CancellableThrough(cancellationToken) + .ProcessAsynchronously(); + } +} \ No newline at end of file diff --git a/Encoder/Utils.cs b/Encoder/Utils.cs index 1199322..73cd613 100644 --- a/Encoder/Utils.cs +++ b/Encoder/Utils.cs @@ -4,21 +4,13 @@ using Microsoft.Net.Http.Headers; namespace Encoder; public static class Utils { - private static readonly Tuple[] QPTable = new Tuple[] { - new(1280*720, 64), - new(1920*1080, 96), - new(3840*2160, 128), - new(5760*2880, 96), //VR6K - new(8128*4096, 120) //VR8K - }.OrderBy(t => t.Item1).ToArray(); - public static float Lerp(float v0, float v1, float t) { return v0 + t * (v1 - v0); } public static float Remap(float value, float from1, float to1, float from2, float to2) => from2 + (value - from1) * (to2 - from2) / (to1 - from1); - public static int ToQPValue(int W, int H) { + public static int ToQPValue(int W, int H, Tuple[] QPTable) { int pixels = W * H; for (var i = 0; i < QPTable.Length; i++) { if (pixels <= QPTable[i].Item1) {