Applied strategy pattern to Encoder, refactored and reorganized code
This commit is contained in:
@@ -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<EncodingJob> JobQueue;
|
||||
List<EncodingJob> Jobs = new();
|
||||
ILogger<EncoderService> 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<EncoderService> logger, IOptions<FFmpegOptions> 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
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Encoder;
|
||||
|
||||
public interface IEncoderService {
|
||||
public Guid EnqueueJob(EncodingJob job);
|
||||
public EncodingJob? GetJobStatus(Guid jobId);
|
||||
}
|
||||
@@ -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
157
Encoder/Strategies.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -4,21 +4,13 @@ using Microsoft.Net.Http.Headers;
|
||||
namespace Encoder;
|
||||
|
||||
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) {
|
||||
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<int, int>[] QPTable) {
|
||||
int pixels = W * H;
|
||||
for (var i = 0; i < QPTable.Length; i++) {
|
||||
if (pixels <= QPTable[i].Item1) {
|
||||
|
||||
Reference in New Issue
Block a user