diff --git a/Encoder/Strategies.cs b/Encoder/Strategies.cs index 3c3a9b5..a43d1cb 100644 --- a/Encoder/Strategies.cs +++ b/Encoder/Strategies.cs @@ -162,11 +162,11 @@ public class AV1SwEncodingStrategy(string inputFilePath, string outputFilePath, public string OutputFilePath { get; set; } = outputFilePath; private static readonly Tuple[] QPTable = new Tuple[] { - new(1280*720, 64), - new(1920*1080, 96), - new(3840*2160, 128), - new(5760*2880, 96), //VR6K - new(8128*4096, 120) //VR8K + new(1280*720, 27), + new(1920*1080, 28), + new(3840*2160, 29), + new(5760*2880, 30), //VR6K + new(8128*4096, 32) //VR8K }.OrderBy(t => t.Item1).ToArray(); public async Task ExecuteAsync(CancellationToken cancellationToken, Action onProgress) { @@ -180,8 +180,11 @@ public class AV1SwEncodingStrategy(string inputFilePath, string outputFilePath, .OutputToFile(OutputFilePath, true, args => args .CopyChannel(Channel.Audio) .CopyChannel(Channel.Subtitle) - .WithVideoCodec(VideoCodec.LibaomAv1) - // TODO: adjust settings for software AV1 encoding + .WithVideoCodec("libsvtav1") + .WithCustomArgument($"-preset 7") + .WithConstantRateFactor(qp) + .WithCustomArgument("-svtav1-params tune=0") + .WithVideoFilters(filterOptions => {if (W > 0 && H > 0) filterOptions.Scale(W, H);}) .WithFastStart() ) .NotifyOnProgress(progress => onProgress(new() { @@ -204,11 +207,11 @@ public class HevcSwEncodingStrategy(string inputFilePath, string outputFilePath, //TODO: needs to be adjusted for software HEVC encoding private static readonly Tuple[] QPTable = new Tuple[] { - new(1280*720, 0), - new(1920*1080, 0), - new(3840*2160, 0), - new(5760*2880, 0), //VR6K - new(8128*4096, 0) //VR8K + new(1280*720, 20), + new(1920*1080, 22), + new(3840*2160, 26), + new(5760*2880, 26), //VR6K + new(8128*4096, 28) //VR8K }.OrderBy(t => t.Item1).ToArray(); public async Task ExecuteAsync(CancellationToken cancellationToken, Action onProgress) { @@ -223,10 +226,10 @@ public class HevcSwEncodingStrategy(string inputFilePath, string outputFilePath, .CopyChannel(Channel.Audio) .CopyChannel(Channel.Subtitle) .WithVideoCodec(VideoCodec.LibX265) - .WithArgument(new NvencSpeedPreset(NvencSpeed.p2)) - .WithArgument(new NvencTuneArgument(NvencTune.hq)) - .WithArgument(new NvencHighBitDepthArgument(true)) - .WithArgument(new NvencQPArgument((byte)qp)) + .WithSpeedPreset(Speed.Slow) + .WithConstantRateFactor(qp) + .WithCustomArgument("-x265-params profile=main10:high-tier=1:aq-mode=3:psy-rd=1:rc-lookahead=60:bframes=6") + .WithVideoFilters(filterOptions => {if (W > 0 && H > 0) filterOptions.Scale(W, H);}) .WithFastStart() ) .NotifyOnProgress(progress => onProgress(new() { diff --git a/EncodingSampleTest/NvencExt.cs b/EncodingSampleTest/NvencExt.cs index b758a64..a9335dc 100644 --- a/EncodingSampleTest/NvencExt.cs +++ b/EncodingSampleTest/NvencExt.cs @@ -29,7 +29,7 @@ class NvencSpeedPreset(NvencSpeed speed) : FFMpegCore.Arguments.IArgument { } class NvencTuneArgument(NvencTune tune) : FFMpegCore.Arguments.IArgument { - public string Text { get { return $"-tune {tune.ToString().ToLower()}"; } } + public string Text { get { return $"-tune {(int)tune}"; } } } class NvencHighBitDepthArgument(bool enable) : FFMpegCore.Arguments.IArgument { diff --git a/EncodingSampleTest/Program.cs b/EncodingSampleTest/Program.cs index 258bd64..75ac9d5 100644 --- a/EncodingSampleTest/Program.cs +++ b/EncodingSampleTest/Program.cs @@ -8,7 +8,8 @@ string tempPath = Path.Combine(Environment.CurrentDirectory, "tmp"); string outputDir = Path.Combine(Environment.CurrentDirectory, "output"); Directory.CreateDirectory(outputDir); Directory.CreateDirectory(tempPath); -const string inputFile = "testVid8k.mp4"; +const string inputFile = "testVid.mp4"; +const int MAX_JOBS = 3; GlobalFFOptions.Configure(options => { options.BinaryFolder = ffmpegPath; @@ -22,44 +23,175 @@ if (mediaInfo.PrimaryVideoStream == null) { Console.WriteLine("No video stream found."); return; } -var W = mediaInfo.PrimaryVideoStream.Width; -var H = mediaInfo.PrimaryVideoStream.Height; +var W = mediaInfo.PrimaryVideoStream.Width / 2; +var H = mediaInfo.PrimaryVideoStream.Height / 2; -for (int qp = 32; qp <= 160; qp += 32) { - await AV1Encode(W, H, qp); +Queue GpuEncodingTasks = new(); +List CpuTasks = new(); +CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + +CancellationToken cancellationToken = cancellationTokenSource.Token; +for (int cq = 24; cq <= 34; cq += 2) { + GpuEncodingTasks.Enqueue(Encode(Encoder.AV1_NVENC, 1920, 1080, cq, cancellationToken)); + GpuEncodingTasks.Enqueue(Encode(Encoder.HEVC_NVENC, 1920, 1080, cq, cancellationToken)); + GpuEncodingTasks.Enqueue(Encode(Encoder.H264_NVENC, 1920, 1080, cq, cancellationToken)); + CpuTasks.Add(Encode(Encoder.AV1_CPU, 1920, 1080, cq, cancellationToken)); + CpuTasks.Add(Encode(Encoder.HEVC_CPU, 1920, 1080, cq, cancellationToken)); + CpuTasks.Add(Encode(Encoder.H264_CPU, 1920, 1080, cq, cancellationToken)); +} +//Run all GPU tasks sequentially +while (GpuEncodingTasks.Count > 0) { + var task = GpuEncodingTasks.Dequeue(); + try { + task.RunSynchronously(); + } catch (Exception ex) { + Console.WriteLine($"Error during GPU encoding: {ex.Message}"); + } } -/* -for (int qp = 32; qp <= 250; qp += 32) { - await AV1Encode(1920, 1080, qp); -}*/ +//Run all CPU tasks in parallel +try { + while (CpuTasks.Count > 0) { + var runningTasks = CpuTasks.Take(MAX_JOBS).ToList(); + CpuTasks = CpuTasks.Skip(MAX_JOBS).ToList(); + await Task.WhenAll(runningTasks); + } + await Task.WhenAll(CpuTasks); +} +catch (Exception ex) { + Console.WriteLine($"Error during CPU encoding: {ex.Message}"); +} +cancellationTokenSource.Cancel(); +return; +Task Encode(Encoder encoder, int W = -1, int H = -1, int CQ = 23, CancellationToken cancellationToken = default) { + var outputFile = Path.Combine(outputDir, $"output_{encoder}-{W}x{H}_cq{CQ}.mp4"); + if (File.Exists(outputFile) && new FileInfo(outputFile).Length > 0) { + Console.WriteLine($"Skipping existing file {outputFile}"); + return Task.CompletedTask; + } + + FFMpegArgumentProcessor ffmpegArgs = encoder switch { + Encoder.AV1_NVENC => AV1Encode( outputFile, W, H, CQ), + Encoder.HEVC_NVENC => HEVCEncode( outputFile, W, H, CQ), + Encoder.H264_NVENC => H264Encode( outputFile, W, H, CQ), + Encoder.AV1_CPU => AV1EncodeCPU( outputFile, W, H, CQ), + Encoder.HEVC_CPU => HEVCEncodeCPU( outputFile, W, H, CQ), + Encoder.H264_CPU => H264EncodeCPU( outputFile, W, H, CQ), + _ => throw new ArgumentOutOfRangeException(nameof(encoder), encoder, null) + }; + + ffmpegArgs.WithLogLevel(FFMpegLogLevel.Info) + .NotifyOnProgress(progress => Console.WriteLine($"Encoding {outputFile}: {progress:g}/{mediaInfo.Duration:g} {progress / mediaInfo.Duration:P}")) + .CancellableThrough(cancellationToken); + + Console.WriteLine(ffmpegArgs.Arguments); + return new Task(_ => { + Console.WriteLine("Starting encoding: " + outputFile); + return ffmpegArgs.ProcessAsynchronously().Result; + }, cancellationToken); +} -Task AV1Encode(int W = -1, int H = -1, int QP = 23) { // AV1 is visually lossless at QP 23 - var outputFile = Path.Combine(outputDir, $"output_av1-{W}x{H}_qp{QP}.mp4"); - var ffmpegArgs = FFMpegArguments - .FromFileInput(inputFile, true, options => options - .WithHardwareAcceleration() - ) - .OutputToFile(outputFile, true, options => options +FFMpegArgumentProcessor AV1Encode(string filePath, int W = -1, int H = -1, int CQ = 23) { // AV1 is visually lossless at CQ 23 + return FFMpegArguments + .FromFileInput(inputFile, false, options => options.WithHardwareAcceleration()) + .OutputToFile(filePath, true, options => options + .CopyChannel(Channel.Audio) + .CopyChannel(Channel.Subtitle) + .WithVideoCodec("av1_nvenc") + .WithArgument(new NvencSpeedPreset(NvencSpeed.p7)) + .WithArgument(new NvencTuneArgument(NvencTune.hq)) + .WithArgument(new NvencHighBitDepthArgument(true)) + .WithCustomArgument("-rc vbr") + .WithCustomArgument($"-cq {CQ}") + .WithVideoFilters(filterOptions => { if (W > 0 && H > 0) filterOptions.Scale(W, H); }) + .WithFastStart() + ); +} + +FFMpegArgumentProcessor HEVCEncode(string filePath, int W = -1, int H = -1, int CQ = 23) { + return FFMpegArguments + .FromFileInput(inputFile, true, options => options.WithHardwareAcceleration()) + .OutputToFile(filePath, true, options => options .CopyChannel(Channel.Audio) .CopyChannel(Channel.Subtitle) .WithVideoCodec("hevc_nvenc") - .WithArgument(new NvencSpeedPreset(NvencSpeed.p4)) + .WithArgument(new NvencSpeedPreset(NvencSpeed.p7)) .WithArgument(new NvencTuneArgument(NvencTune.hq)) - .WithArgument(new NvencHighBitDepthArgument(true)) - .WithArgument(new NvencQPArgument((byte)QP)) - .WithVideoFilters(filterOptions => { - if (W > 0 && H > 0) filterOptions.Scale(W, H); - }) + .WithCustomArgument("-rc vbr_hq") + .WithCustomArgument($"-cq {(byte)CQ}") + .WithVideoFilters(filterOptions => { if (W > 0 && H > 0) filterOptions.Scale(W, H); }) .WithFastStart() - ) - .WithLogLevel(FFMpegLogLevel.Info) - .NotifyOnProgress(progress => Console.WriteLine($"Encoding {outputFile}: {progress:g}/{mediaInfo.Duration:g} {progress/mediaInfo.Duration:P}")); - - Console.WriteLine(ffmpegArgs.Arguments); - - var res = ffmpegArgs.ProcessSynchronously(); - return Task.CompletedTask; + ); } + +FFMpegArgumentProcessor H264Encode(string filePath, int W = -1, int H = -1, int CQ = 23) { + return FFMpegArguments + .FromFileInput(inputFile, true, options => options.WithHardwareAcceleration()) + .OutputToFile(filePath, true, options => options + .CopyChannel(Channel.Audio) + .CopyChannel(Channel.Subtitle) + .WithVideoCodec("h264_nvenc") + .WithArgument(new NvencSpeedPreset(NvencSpeed.p7)) + .WithArgument(new NvencTuneArgument(NvencTune.hq)) + .WithCustomArgument("-rc vbr_hq") + .WithCustomArgument($"-cq {(byte)CQ}") // approximate CRF to CQ + .WithVideoFilters(filterOptions => { if (W > 0 && H > 0) filterOptions.Scale(W, H); }) + .WithFastStart() + ); +} + +FFMpegArgumentProcessor AV1EncodeCPU(string filePath, int W = -1, int H = -1, int CQ = 23) { + return FFMpegArguments + .FromFileInput(inputFile) + .OutputToFile(filePath, true, options => options + .CopyChannel(Channel.Audio) + .CopyChannel(Channel.Subtitle) + .WithVideoCodec("libsvtav1") + .WithCustomArgument($"-preset 3") + .WithConstantRateFactor(CQ) + .WithCustomArgument("-svtav1-params tune=0") + .WithVideoFilters(filterOptions => {if (W > 0 && H > 0) filterOptions.Scale(W, H);}) + .WithFastStart() + ); +} + +FFMpegArgumentProcessor HEVCEncodeCPU(string filePath, int W = -1, int H = -1, int CQ = 23) { + return FFMpegArguments + .FromFileInput(inputFile) + .OutputToFile(filePath, true, options => options + .CopyChannel(Channel.Audio) + .CopyChannel(Channel.Subtitle) + .WithVideoCodec(VideoCodec.LibX265) + .WithSpeedPreset(Speed.Slow) + .WithConstantRateFactor(CQ) + .WithCustomArgument("-x265-params profile=main10:high-tier=1:aq-mode=3:psy-rd=1:rc-lookahead=60:bframes=6") + .WithVideoFilters(filterOptions => {if (W > 0 && H > 0) filterOptions.Scale(W, H);}) + .WithFastStart() + ); +} + +FFMpegArgumentProcessor H264EncodeCPU(string filePath, int W = -1, int H = -1, int CQ = 23) { + return FFMpegArguments + .FromFileInput(inputFile) + .OutputToFile(filePath, true, options => options + .CopyChannel(Channel.Audio) + .CopyChannel(Channel.Subtitle) + .WithVideoCodec(VideoCodec.LibX264) + .WithSpeedPreset(Speed.Slow) + .WithConstantRateFactor(CQ) + .WithCustomArgument("-x265-params profile=main10:high-tier=1:aq-mode=3:psy-rd=1:rc-lookahead=60:bframes=6") + .WithVideoFilters(filterOptions => {if (W > 0 && H > 0) filterOptions.Scale(W, H);}) + .WithFastStart() + ); +} + +public enum Encoder { + AV1_NVENC, + HEVC_NVENC, + H264_NVENC, + AV1_CPU, + HEVC_CPU, + H264_CPU +} \ No newline at end of file