From e3af0f0b698aa60f1e36637e9b4dc09ede2ef3bb Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:43:12 -0500 Subject: [PATCH] add nvidia av1 encoder (#2469) --- CHANGELOG.md | 2 ++ .../Commands/UpdateFFmpegProfileHandler.cs | 6 ++++++ ErsatzTV.Application/Streaming/HlsSessionWorker.cs | 2 +- ErsatzTV.Core/Domain/FFmpegProfileVideoFormat.cs | 1 + ErsatzTV.Core/FFmpeg/FFmpegLibraryHelper.cs | 1 + .../FFmpeg/FFmpegLibraryProcessService.cs | 1 + ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaHelper.cs | 6 +++++- .../Capabilities/NvidiaHardwareCapabilities.cs | 2 ++ ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderAv1Nvenc.cs | 14 ++++++++++++++ ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs | 8 ++++++++ ErsatzTV/Pages/Channels.razor | 1 + ErsatzTV/Pages/FFmpegEditor.razor | 4 ++++ .../FFmpegProfileEditViewModelValidator.cs | 5 +++-- 13 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderAv1Nvenc.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 780f8fc75..ae6a8a765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Allow HEVC playback in channel preview - This is restricted to compatible browsers - Preview button will be red when preview is disabled due to browser incompatibility +- Add AV1 encoding support with NVIDIA acceleration + - This also requires `HLS Segmenter (fmp4)` ### Fixed - Fix green output when libplacebo tonemapping is used with NVIDIA acceleration and 10-bit output in FFmpeg Profile diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs index 7dfd4abb2..6f3c6cb86 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs @@ -57,6 +57,12 @@ public class ? FFmpegProfileBitDepth.EightBit : update.BitDepth; + if (p.HardwareAcceleration is not HardwareAccelerationKind.Nvenc && + p.VideoFormat is FFmpegProfileVideoFormat.Av1) + { + p.VideoFormat = FFmpegProfileVideoFormat.Hevc; + } + p.VideoBitrate = update.VideoBitrate; p.VideoBufferSize = update.VideoBufferSize; p.TonemapAlgorithm = update.TonemapAlgorithm; diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs index 4c9f20211..6a676e5f7 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs @@ -587,7 +587,7 @@ public class HlsSessionWorker : IHlsSessionWorker } catch (Exception ex) { - _logger.LogError(ex, "Error transcoding channel {Channel}", _channelNumber); + _logger.LogError(ex, "Error transcoding channel {Channel} - {Message}", _channelNumber, ex.Message); try { diff --git a/ErsatzTV.Core/Domain/FFmpegProfileVideoFormat.cs b/ErsatzTV.Core/Domain/FFmpegProfileVideoFormat.cs index 475070239..665faec71 100644 --- a/ErsatzTV.Core/Domain/FFmpegProfileVideoFormat.cs +++ b/ErsatzTV.Core/Domain/FFmpegProfileVideoFormat.cs @@ -7,6 +7,7 @@ public enum FFmpegProfileVideoFormat H264 = 1, Hevc = 2, Mpeg2Video = 3, + Av1 = 4, Copy = 99 } diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryHelper.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryHelper.cs index 37072380b..aeb400645 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryHelper.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryHelper.cs @@ -33,6 +33,7 @@ public static class FFmpegLibraryHelper { FFmpegProfileVideoFormat.H264 => VideoFormat.H264, FFmpegProfileVideoFormat.Hevc => VideoFormat.Hevc, + FFmpegProfileVideoFormat.Av1 => VideoFormat.Av1, _ => VideoFormat.Mpeg2Video }; diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 2f87c45b9..594dd82d6 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -1100,6 +1100,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService private static string GetVideoFormat(FFmpegPlaybackSettings playbackSettings) => playbackSettings.VideoFormat switch { + FFmpegProfileVideoFormat.Av1 => VideoFormat.Av1, FFmpegProfileVideoFormat.Hevc => VideoFormat.Hevc, FFmpegProfileVideoFormat.H264 => VideoFormat.H264, FFmpegProfileVideoFormat.Mpeg2Video => VideoFormat.Mpeg2Video, diff --git a/ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaHelper.cs b/ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaHelper.cs index d3178d0b7..97b4fe0bf 100644 --- a/ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaHelper.cs +++ b/ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaHelper.cs @@ -12,10 +12,14 @@ internal static class CudaHelper private static bool _initialized; private static readonly Lock Lock = new(); + public static Guid Av1CodecGuid = Guid.Parse("0A352289-0AA7-4759-862D-5D15CD16D254"); + public static Guid Av1ProfileGuid = Guid.Parse("5f2a39f5-f14e-4f95-9a9e-b76d568fcf97"); + private static readonly Dictionary AllEncoders = new() { [VideoFormat.H264] = NvEncCodecGuids.H264, - [VideoFormat.Hevc] = NvEncCodecGuids.Hevc + [VideoFormat.Hevc] = NvEncCodecGuids.Hevc, + [VideoFormat.Av1] = Av1CodecGuid }; private sealed record Decoder(CuVideoChromaFormat ChromaFormat, CuVideoCodec VideoCodec, int BitDepth); diff --git a/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs index 9a4e9736d..c4227a7c6 100644 --- a/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs @@ -105,6 +105,8 @@ public class NvidiaHardwareCapabilities(CudaDevice cudaDevice, IFFmpegCapabiliti // high10 is for libx264, nvenc needs high444 (VideoFormat.H264, VideoProfile.High10, _) => NvEncProfileGuids.H264High444, + (VideoFormat.Av1, _, _) => CudaHelper.Av1ProfileGuid, + _ => NvEncProfileGuids.H264Main }; diff --git a/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderAv1Nvenc.cs b/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderAv1Nvenc.cs new file mode 100644 index 000000000..ecbacc2f9 --- /dev/null +++ b/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderAv1Nvenc.cs @@ -0,0 +1,14 @@ +using ErsatzTV.FFmpeg.Format; + +namespace ErsatzTV.FFmpeg.Encoder.Nvenc; + +public class EncoderAv1Nvenc : EncoderBase +{ + public override string Name => "av1_nvenc"; + public override StreamKind Kind => StreamKind.Video; + + public override FrameState NextState(FrameState currentState) => currentState with + { + VideoFormat = VideoFormat.Av1 + }; +} diff --git a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs index 3ce44c74b..9e3319ca6 100644 --- a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs @@ -74,6 +74,11 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder encodeCapability = FFmpegCapability.Software; } + if (desiredState.VideoFormat is VideoFormat.Av1 && ffmpegState.OutputFormat is not OutputFormatKind.HlsMp4) + { + throw new NotSupportedException("AV1 output is only supported with HLS Segmenter (fmp4)"); + } + // mpeg2_cuvid seems to have issues when yadif_cuda is used, so just use software decoding if (context.ShouldDeinterlace && videoStream.Codec == VideoFormat.Mpeg2Video) { @@ -311,6 +316,9 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder (HardwareAccelerationMode.Nvenc, VideoFormat.H264) => new EncoderH264Nvenc(desiredState.VideoProfile, desiredState.VideoPreset), + (HardwareAccelerationMode.Nvenc, VideoFormat.Av1) => new EncoderAv1Nvenc(), + (HardwareAccelerationMode.None, VideoFormat.Av1) => throw new NotSupportedException("AV1 software encoding is not supported"), + // don't pass NVENC profile down to libx264 (_, _) => GetSoftwareEncoder(ffmpegState, currentState, desiredState with { VideoProfile = Option.None }) }; diff --git a/ErsatzTV/Pages/Channels.razor b/ErsatzTV/Pages/Channels.razor index 34c1e81f1..26c18dd56 100644 --- a/ErsatzTV/Pages/Channels.razor +++ b/ErsatzTV/Pages/Channels.razor @@ -209,6 +209,7 @@ { string videoCodec = profile.VideoFormat switch { + FFmpegProfileVideoFormat.Av1 => "av01.0.01M.08", FFmpegProfileVideoFormat.Hevc => "hvc1.1.6.L93.B0", FFmpegProfileVideoFormat.H264 => "avc1.4D4028", _ => string.Empty diff --git a/ErsatzTV/Pages/FFmpegEditor.razor b/ErsatzTV/Pages/FFmpegEditor.razor index 11f40a812..1dc2aaf5e 100644 --- a/ErsatzTV/Pages/FFmpegEditor.razor +++ b/ErsatzTV/Pages/FFmpegEditor.razor @@ -67,6 +67,10 @@ h264 hevc mpeg-2 + @if (_model.HardwareAcceleration is HardwareAccelerationKind.Nvenc) + { + av1 + } diff --git a/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs b/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs index 98a23456f..4a6582478 100644 --- a/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs +++ b/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs @@ -18,7 +18,8 @@ public class FFmpegProfileEditViewModelValidator : AbstractValidator NvencFormats = [ FFmpegProfileVideoFormat.H264, - FFmpegProfileVideoFormat.Hevc + FFmpegProfileVideoFormat.Hevc, + FFmpegProfileVideoFormat.Av1 ]; private static readonly List VaapiFormats = @@ -77,7 +78,7 @@ public class FFmpegProfileEditViewModelValidator : AbstractValidator { RuleFor(x => x.VideoFormat).Must(c => NvencFormats.Contains(c)) - .WithMessage("NVENC supports formats (h264, hevc)"); + .WithMessage("NVENC supports formats (h264, hevc, av1)"); }); When(