Files
VideoEncoderService/Encoder/EncoderService.cs

104 lines
4.0 KiB
C#

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<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;
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<EncodingJob> 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;
}
}