using FFMpegCore; 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 JobQueue; List Jobs = new(); ILogger 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 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; string outputPath = Path.Combine(options.TemporaryFilesPath, Path.GetFileName(job.FilePath)); 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) { 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; } }