using System.Numerics; using FFMpegCore; using FFMpegCore.Enums; using Microsoft.Extensions.Options; namespace Encoder; public class EncoderService : BackgroundService, IEncoderService { Queue JobQueue; List Jobs = new(); ILogger Logger; FFmpegOptions options; public EncoderService(ILogger logger, IOptions ffmpegOptions) { Logger = logger; options = ffmpegOptions.Value; options.FfmpegPath = options.FfmpegPath.AbsoluteOrProcessPath(); options.TemporaryFilesPath = options.TemporaryFilesPath.AbsoluteOrProcessPath(); 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, stoppingToken).Wait(stoppingToken); // 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 GetJobs() { return JobQueue.Concat(Jobs); } 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); if(result.Result) { job.Status = JobStatus.Completed; job.EncodedFilePath = outputPath; File.Delete(file); // Clean up original file } else { job.Status = JobStatus.Failed; // Clean up any partially created output file. Leave original file for retry. if (File.Exists(outputPath)) File.Delete(outputPath); } job.CompletedAt = DateTime.Now; job.Progress = 1.0f; } }