Files
VideoEncoderService/Encoder/EncoderService.cs
2025-12-18 02:11:52 +01:00

129 lines
5.1 KiB
C#

using System.Diagnostics;
using FFMpegCore;
using FFMpegCore.Helpers;
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));
// Determine if an NVDA graphics card is available for hardware acceleration
ProcessStartInfo psi = new ProcessStartInfo {
FileName = Path.Combine(options.FfmpegPath, "ffmpeg.exe"),
Arguments = @"-hide_banner -init_hw_device ""list""",
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true
};
using Process ffmpeg = Process.Start(psi)!;
string output = await ffmpeg.StandardOutput.ReadToEndAsync(cancellationToken);
await ffmpeg.WaitForExitAsync(cancellationToken);
bool nvenc = output.Contains("cuda");
IEncodingStrategy strategy;
if(nvenc) strategy= job.RequestedEncoding switch {
EncoderType.H264 => new H264NvencEncodingStrategy(file, outputPath, job),
EncoderType.HEVC => new HevcNvencEncodingStrategy(file, outputPath, job),
EncoderType.AV1 => new AV1NvencEncodingStrategy(file, outputPath, job),
_ => throw new ArgumentOutOfRangeException(nameof(job), job, null)
};
else strategy = job.RequestedEncoding switch {
EncoderType.H264 => new H264SwEncodingStrategy(file, outputPath, job),
EncoderType.HEVC => new HevcSwEncodingStrategy(file, outputPath, job),
EncoderType.AV1 => new AV1SwEncodingStrategy(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;
}
}