129 lines
5.1 KiB
C#
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;
|
|
}
|
|
} |