Applied strategy pattern to Encoder, refactored and reorganized code

This commit is contained in:
Samuele Lorefice
2025-12-15 21:32:49 +01:00
parent 24df58056c
commit fc915be3aa
5 changed files with 184 additions and 58 deletions

View File

@@ -1,16 +1,32 @@
using System.Numerics;
using FFMpegCore; using FFMpegCore;
using FFMpegCore.Enums;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Encoder; namespace Encoder;
public interface IEncoderService {
public Guid EnqueueJob(EncodingJob job);
public EncodingJob? GetJobStatus(Guid jobId);
}
public class EncoderService : BackgroundService, IEncoderService { public class EncoderService : BackgroundService, IEncoderService {
Queue<EncodingJob> JobQueue; Queue<EncodingJob> JobQueue;
List<EncodingJob> Jobs = new(); List<EncodingJob> Jobs = new();
ILogger<EncoderService> Logger; ILogger<EncoderService> Logger;
FFmpegOptions options; 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<EncoderService> logger, IOptions<FFmpegOptions> ffmpegOptions) { public EncoderService(ILogger<EncoderService> logger, IOptions<FFmpegOptions> ffmpegOptions) {
Logger = logger; Logger = logger;
options = ffmpegOptions.Value; options = ffmpegOptions.Value;
@@ -64,44 +80,16 @@ public class EncoderService : BackgroundService, IEncoderService {
async Task ProcessJob(EncodingJob job, CancellationToken cancellationToken) { async Task ProcessJob(EncodingJob job, CancellationToken cancellationToken) {
job.Status = JobStatus.InProgress; job.Status = JobStatus.InProgress;
var file = job.FilePath; 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)); string outputPath = Path.Combine(options.TemporaryFilesPath, Path.GetFileName(job.FilePath));
int qp = Utils.ToQPValue(W, H); IEncodingStrategy strategy = job.RequestedEncoding switch {
var result = FFMpegArguments EncoderType.H264 => new H264EncodingStrategy(file, outputPath, job),
.FromFileInput(file, true, args => args.WithHardwareAcceleration()) EncoderType.HEVC => new HEVCEncodingStrategy(file, outputPath, job),
.OutputToFile(outputPath, true, args => args EncoderType.AV1 => new AV1EncodingStrategy(file, outputPath, job),
.CopyChannel(Channel.Audio) _ => throw new ArgumentOutOfRangeException(nameof(job), job, null)
.CopyChannel(Channel.Subtitle) };
.WithVideoCodec("av1_nvenc") bool result = await strategy.ExecuteAsync(cancellationToken, Progress);
.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);
if(result.Result) { if(result) {
job.Status = JobStatus.Completed; job.Status = JobStatus.Completed;
job.EncodedFilePath = outputPath; job.EncodedFilePath = outputPath;
File.Delete(file); // Clean up original file File.Delete(file); // Clean up original file

View File

@@ -1,6 +0,0 @@
namespace Encoder;
public interface IEncoderService {
public Guid EnqueueJob(EncodingJob job);
public EncodingJob? GetJobStatus(Guid jobId);
}

View File

@@ -1,5 +0,0 @@
### GET request to example server
GET https://examples.http-client.intellij.net/get
?generated-in=JetBrains Rider
###

157
Encoder/Strategies.cs Normal file
View File

@@ -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<bool> ExecuteAsync(CancellationToken cancellationToken, Action<EncodingProgress> 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<int, int>[] QPTable = new Tuple<int, int>[] {
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<bool> ExecuteAsync(CancellationToken cancellationToken, Action<EncodingProgress> 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<int, int>[] QPTable = new Tuple<int, int>[] {
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<bool> ExecuteAsync(CancellationToken cancellationToken, Action<EncodingProgress> 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<int, int>[] QPTable = new Tuple<int, int>[] {
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<bool> ExecuteAsync(CancellationToken cancellationToken, Action<EncodingProgress> 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();
}
}

View File

@@ -4,21 +4,13 @@ using Microsoft.Net.Http.Headers;
namespace Encoder; namespace Encoder;
public static class Utils { public static class Utils {
private static readonly Tuple<int, int>[] QPTable = new Tuple<int, int>[] {
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) { public static float Lerp(float v0, float v1, float t) {
return v0 + t * (v1 - v0); 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 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<int, int>[] QPTable) {
int pixels = W * H; int pixels = W * H;
for (var i = 0; i < QPTable.Length; i++) { for (var i = 0; i < QPTable.Length; i++) {
if (pixels <= QPTable[i].Item1) { if (pixels <= QPTable[i].Item1) {