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;
|
||||||
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
|
||||||
|
|||||||
@@ -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;
|
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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user