Files
VideoEncoderService/Encoder/EncoderService.cs
2025-12-15 05:42:46 +01:00

108 lines
4.2 KiB
C#

using System.Numerics;
using FFMpegCore;
using FFMpegCore.Enums;
using Microsoft.Extensions.Options;
namespace Encoder;
public class EncoderService : BackgroundService, IEncoderService {
Queue<EncodingJob> JobQueue;
List<EncodingJob> Jobs = new();
ILogger<EncoderService> Logger;
FFmpegOptions options;
public EncoderService(ILogger<EncoderService> logger, IOptions<FFmpegOptions> ffmpegOptions) {
Logger = logger;
options = ffmpegOptions.Value;
options.FfmpegPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), options.FfmpegPath));
options.TemporaryFilesPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), options.TemporaryFilesPath));
Directory.CreateDirectory(options.TemporaryFilesPath); // Ensure the temporary files directory exists
JobQueue = new();
logger.Log(LogLevel.Information,
$"""
Starting Encoder Service with options:
TemporaryFilesPath: {options.TemporaryFilesPath}
FfmpegPath: {options.FfmpegPath}"
""");
GlobalFFOptions.Configure(ffOptions => {
ffOptions.BinaryFolder = options.FfmpegPath;
ffOptions.TemporaryFilesFolder = options.TemporaryFilesPath;
});
}
protected override Task ExecuteAsync(CancellationToken stoppingToken) => ProcessJobs(stoppingToken);
Task ProcessJobs(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
if (JobQueue.Count > 0) {
// Grab a reference to the next job in queue
var job = JobQueue.Peek();
ProcessJob(job); // Process the job
JobQueue.Dequeue(); // Remove it from the queue
Jobs.Add(job); // Add it to the completed jobs list
}
Thread.Sleep(5); // Prevent tight loop
}
return Task.CompletedTask;
}
public Guid EnqueueJob(EncodingJob job) {
JobQueue.Enqueue(job);
return job.Id;
}
public EncodingJob? GetJobStatus(Guid jobId) {
return Jobs.FirstOrDefault(j => j.Id == jobId);
}
public IEnumerable<EncodingJob> GetJobs() {
return JobQueue.Concat(Jobs);
}
void ProcessJob(EncodingJob job) {
job.Status = JobStatus.InProgress;
var file = job.OrigFilePath;
var mediaInfo = FFProbe.Analyse(file);
if (mediaInfo.PrimaryVideoStream == null) {
job.Status = JobStatus.Failed;
return;
}
var W = mediaInfo.PrimaryVideoStream.Width;
var H = mediaInfo.PrimaryVideoStream.Height;
string outputPath = Path.Combine(options.TemporaryFilesPath, Path.GetFileName(job.OrigFilePath));
int qp = Utils.ToQPValue(W, H);
var status = 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);
})
.ProcessSynchronously();
if(status) {
job.Status = JobStatus.Completed;
job.EncodedFilePath = outputPath;
} else {
job.Status = JobStatus.Failed;
}
job.CompletedAt = DateTime.Now;
job.Progress = 1.0f;
}
}