diff --git a/CHANGELOG.md b/CHANGELOG.md index b9f941c4b..8e60d9335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Skip checking for subtitles to extract when subtitles are not enabled on a channel/schedule item - Properly scale subtitles when using hardware acceleration - Fix color normalization of content with missing color metadata when using NVIDIA acceleration +- `VAAPI`: explicitly use `CQP` rate control mode when it's the only compatible mode ### Changed - `HLS Direct` streaming mode diff --git a/ErsatzTV.FFmpeg.Tests/Capabilities/Vaapi/VaapiCapabilityParserTests.cs b/ErsatzTV.FFmpeg.Tests/Capabilities/Vaapi/VaapiCapabilityParserTests.cs new file mode 100644 index 000000000..fba5becd0 --- /dev/null +++ b/ErsatzTV.FFmpeg.Tests/Capabilities/Vaapi/VaapiCapabilityParserTests.cs @@ -0,0 +1,204 @@ +using System.Collections.Generic; +using System.Linq; +using ErsatzTV.FFmpeg.Capabilities.Vaapi; +using FluentAssertions; +using NUnit.Framework; + +namespace ErsatzTV.FFmpeg.Tests.Capabilities.Vaapi; + +[TestFixture] +public class VaapiCapabilityParserTests +{ + private const string BriefOutput = @"Trying display: wayland +vainfo: VA-API version: 1.18 (libva 2.18.2) +vainfo: Driver version: Mesa Gallium driver 23.1.2 for AMD Radeon RX 6750 XT (navi22, LLVM 15.0.7, DRM 3.52, 6.3.8-arch1-1) +vainfo: Supported profile and entrypoints + VAProfileMPEG2Simple : VAEntrypointVLD + VAProfileMPEG2Main : VAEntrypointVLD + VAProfileVC1Simple : VAEntrypointVLD + VAProfileVC1Main : VAEntrypointVLD + VAProfileVC1Advanced : VAEntrypointVLD + VAProfileH264ConstrainedBaseline: VAEntrypointVLD + VAProfileH264ConstrainedBaseline: VAEntrypointEncSlice + VAProfileH264Main : VAEntrypointVLD + VAProfileH264Main : VAEntrypointEncSlice + VAProfileH264High : VAEntrypointVLD + VAProfileH264High : VAEntrypointEncSlice + VAProfileHEVCMain : VAEntrypointVLD + VAProfileHEVCMain : VAEntrypointEncSlice + VAProfileHEVCMain10 : VAEntrypointVLD + VAProfileHEVCMain10 : VAEntrypointEncSlice + VAProfileJPEGBaseline : VAEntrypointVLD + VAProfileVP9Profile0 : VAEntrypointVLD + VAProfileVP9Profile2 : VAEntrypointVLD + VAProfileAV1Profile0 : VAEntrypointVLD + VAProfileNone : VAEntrypointVideoProc"; + + private const string FullOutput = @"Trying display: wayland +vainfo: VA-API version: 1.18 (libva 2.18.2) +vainfo: Driver version: Mesa Gallium driver 23.1.2 for AMD Radeon RX 6750 XT (navi22, LLVM 15.0.7, DRM 3.52, 6.3.8-arch1-1) +vainfo: Supported config attributes per profile/entrypoint pair +VAProfileMPEG2Simple/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + +VAProfileMPEG2Main/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + +VAProfileVC1Simple/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + +VAProfileVC1Main/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + +VAProfileVC1Advanced/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + +VAProfileH264ConstrainedBaseline/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + +VAProfileH264ConstrainedBaseline/VAEntrypointEncSlice + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + VAConfigAttribRateControl : VA_RC_CBR + VA_RC_VBR + VA_RC_CQP + VAConfigAttribEncPackedHeaders : VA_ENC_PACKED_HEADER_NONE + VAConfigAttribEncMaxRefFrames : l0=1 + l1=0 + VAConfigAttribEncMaxSlices : 128 + VAConfigAttribEncSliceStructure : VA_ENC_SLICE_STRUCTURE_POWER_OF_TWO_ROWS + VA_ENC_SLICE_STRUCTURE_EQUAL_ROWS + VAConfigAttribEncQualityRange : number of supported quality levels is 32 + VAConfigAttribEncRateControlExt : max_num_temporal_layers_minus1=3 temporal_layer_bitrate_control_flag=1 + VAConfigAttribMaxFrameSize : max_frame_size=1 + multiple_pass=0 + +VAProfileH264Main/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + +VAProfileH264Main/VAEntrypointEncSlice + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + VAConfigAttribRateControl : VA_RC_CBR + VA_RC_VBR + VA_RC_CQP + VAConfigAttribEncPackedHeaders : VA_ENC_PACKED_HEADER_NONE + VAConfigAttribEncMaxRefFrames : l0=1 + l1=0 + VAConfigAttribEncMaxSlices : 128 + VAConfigAttribEncSliceStructure : VA_ENC_SLICE_STRUCTURE_POWER_OF_TWO_ROWS + VA_ENC_SLICE_STRUCTURE_EQUAL_ROWS + VAConfigAttribEncQualityRange : number of supported quality levels is 32 + VAConfigAttribEncRateControlExt : max_num_temporal_layers_minus1=3 temporal_layer_bitrate_control_flag=1 + VAConfigAttribMaxFrameSize : max_frame_size=1 + multiple_pass=0 + +VAProfileH264High/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + +VAProfileH264High/VAEntrypointEncSlice + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + VA_RT_FORMAT_YUV420_10 + VA_RT_FORMAT_YUV420_10BPP + VAConfigAttribRateControl : VA_RC_CBR + VA_RC_VBR + VA_RC_CQP + VAConfigAttribEncPackedHeaders : VA_ENC_PACKED_HEADER_NONE + VAConfigAttribEncMaxRefFrames : l0=1 + l1=0 + VAConfigAttribEncMaxSlices : 128 + VAConfigAttribEncSliceStructure : VA_ENC_SLICE_STRUCTURE_POWER_OF_TWO_ROWS + VA_ENC_SLICE_STRUCTURE_EQUAL_ROWS + VAConfigAttribEncQualityRange : number of supported quality levels is 32 + VAConfigAttribEncRateControlExt : max_num_temporal_layers_minus1=3 temporal_layer_bitrate_control_flag=1 + VAConfigAttribMaxFrameSize : max_frame_size=1 + multiple_pass=0 + +VAProfileHEVCMain/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + +VAProfileHEVCMain/VAEntrypointEncSlice + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + VAConfigAttribRateControl : VA_RC_CBR + VA_RC_VBR + VA_RC_CQP + VAConfigAttribEncPackedHeaders : VA_ENC_PACKED_HEADER_SEQUENCE + VAConfigAttribEncMaxRefFrames : l0=1 + l1=0 + VAConfigAttribEncMaxSlices : 128 + VAConfigAttribEncSliceStructure : VA_ENC_SLICE_STRUCTURE_POWER_OF_TWO_ROWS + VA_ENC_SLICE_STRUCTURE_EQUAL_ROWS + VAConfigAttribEncQualityRange : number of supported quality levels is 32 + VAConfigAttribMaxFrameSize : max_frame_size=1 + multiple_pass=0 + +VAProfileHEVCMain10/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + VA_RT_FORMAT_YUV420_10 + VA_RT_FORMAT_YUV420_10BPP + +VAProfileHEVCMain10/VAEntrypointEncSlice + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + VA_RT_FORMAT_YUV420_10 + VA_RT_FORMAT_YUV420_10BPP + VAConfigAttribRateControl : VA_RC_CBR + VA_RC_VBR + VA_RC_CQP + VAConfigAttribEncPackedHeaders : VA_ENC_PACKED_HEADER_SEQUENCE + VAConfigAttribEncMaxRefFrames : l0=1 + l1=0 + VAConfigAttribEncMaxSlices : 128 + VAConfigAttribEncSliceStructure : VA_ENC_SLICE_STRUCTURE_POWER_OF_TWO_ROWS + VA_ENC_SLICE_STRUCTURE_EQUAL_ROWS + VAConfigAttribEncQualityRange : number of supported quality levels is 32 + VAConfigAttribMaxFrameSize : max_frame_size=1 + multiple_pass=0 + +VAProfileJPEGBaseline/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + VA_RT_FORMAT_YUV422 + VA_RT_FORMAT_YUV444 + VA_RT_FORMAT_YUV400 + +VAProfileVP9Profile0/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + +VAProfileVP9Profile2/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420_10 + VA_RT_FORMAT_YUV420_10BPP + +VAProfileAV1Profile0/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + VA_RT_FORMAT_YUV420_10 + VA_RT_FORMAT_YUV420_10BPP + +VAProfileNone/VAEntrypointVideoProc + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + VA_RT_FORMAT_YUV422 + VA_RT_FORMAT_YUV444 + VA_RT_FORMAT_YUV400 + VA_RT_FORMAT_YUV420_10 + VA_RT_FORMAT_RGB32 + VA_RT_FORMAT_YUV420_10BPP"; + + [Test] + public void ShouldParseEntrypoints() + { + List brief = VaapiCapabilityParser.Parse(BriefOutput); + List full = VaapiCapabilityParser.ParseFull(FullOutput); + + brief.Count.Should().Be(20); + full.Count.Should().Be(20); + } + + [Test] + public void Full_ShouldParseRateControlModes() + { + List full = VaapiCapabilityParser.ParseFull(FullOutput); + + full.Count.Should().Be(20); + full.Count(e => e.VaapiEntrypoint.StartsWith("VAEntrypointEnc")).Should().BeGreaterThan(0); + foreach (VaapiProfileEntrypoint entrypoint in full.Where(e => e.VaapiEntrypoint.StartsWith("VAEntrypointEnc"))) + { + entrypoint.RateControlModes.Count.Should().BeGreaterThan(0); + } + } +} diff --git a/ErsatzTV.FFmpeg/Capabilities/AmfHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/AmfHardwareCapabilities.cs index b826ced1f..c86898b71 100644 --- a/ErsatzTV.FFmpeg/Capabilities/AmfHardwareCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/AmfHardwareCapabilities.cs @@ -27,4 +27,7 @@ public class AmfHardwareCapabilities : IHardwareCapabilities _ => FFmpegCapability.Hardware }; } + + public Option GetRateControlMode(string videoFormat, Option maybePixelFormat) => + Option.None; } diff --git a/ErsatzTV.FFmpeg/Capabilities/DefaultHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/DefaultHardwareCapabilities.cs index 74a5c122d..c3cc39067 100644 --- a/ErsatzTV.FFmpeg/Capabilities/DefaultHardwareCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/DefaultHardwareCapabilities.cs @@ -35,4 +35,7 @@ public class DefaultHardwareCapabilities : IHardwareCapabilities _ => FFmpegCapability.Hardware }; } + + public Option GetRateControlMode(string videoFormat, Option maybePixelFormat) => + Option.None; } diff --git a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs index 3193eebf0..7b241651d 100644 --- a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs +++ b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs @@ -90,7 +90,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory } BufferedCommandResult result = await Cli.Wrap("vainfo") - .WithArguments($"--display drm --device {vaapiDevice}") + .WithArguments($"--display drm --device {vaapiDevice} -a") .WithEnvironmentVariables(envVars) .WithValidation(CommandResultValidation.None) .ExecuteBufferedAsync(Encoding.UTF8); @@ -164,22 +164,12 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory return new DefaultHardwareCapabilities(); } - profileEntrypoints = new List(); - - foreach (string line in string.Join("", output).Split("\n")) + foreach (string o in output) { - const string PROFILE_ENTRYPOINT_PATTERN = @"(VAProfile\w*).*(VAEntrypoint\w*)"; - Match match = Regex.Match(line, PROFILE_ENTRYPOINT_PATTERN); - if (match.Success) - { - profileEntrypoints.Add( - new VaapiProfileEntrypoint( - match.Groups[1].Value.Trim(), - match.Groups[2].Value.Trim())); - } + profileEntrypoints = VaapiCapabilityParser.ParseFull(o); } - if (profileEntrypoints.Any()) + if (profileEntrypoints?.Any() ?? false) { _logger.LogInformation( "Detected {Count} VAAPI profile entrypoints for using {Driver} {Device}", diff --git a/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilities.cs index 3065a5ddf..5be9fd125 100644 --- a/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilities.cs @@ -13,4 +13,8 @@ public interface IHardwareCapabilities string videoFormat, Option videoProfile, Option maybePixelFormat); + + public Option GetRateControlMode( + string videoFormat, + Option maybePixelFormat); } diff --git a/ErsatzTV.FFmpeg/Capabilities/NoHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/NoHardwareCapabilities.cs index f05cac9c8..6667b7843 100644 --- a/ErsatzTV.FFmpeg/Capabilities/NoHardwareCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/NoHardwareCapabilities.cs @@ -15,4 +15,7 @@ public class NoHardwareCapabilities : IHardwareCapabilities Option videoProfile, Option maybePixelFormat) => FFmpegCapability.Software; + + public Option GetRateControlMode(string videoFormat, Option maybePixelFormat) => + Option.None; } diff --git a/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs index ee77cf353..c10f18afb 100644 --- a/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs @@ -115,4 +115,7 @@ public class NvidiaHardwareCapabilities : IHardwareCapabilities _logger.LogWarning("FFmpeg does not contain codec {Codec}; will fall back to software codec", codec); return FFmpegCapability.Software; } + + public Option GetRateControlMode(string videoFormat, Option maybePixelFormat) => + Option.None; } diff --git a/ErsatzTV.FFmpeg/Capabilities/Vaapi/VaapiCapabilityParser.cs b/ErsatzTV.FFmpeg/Capabilities/Vaapi/VaapiCapabilityParser.cs new file mode 100644 index 000000000..c0ab8609a --- /dev/null +++ b/ErsatzTV.FFmpeg/Capabilities/Vaapi/VaapiCapabilityParser.cs @@ -0,0 +1,68 @@ +using System.Text.RegularExpressions; + +namespace ErsatzTV.FFmpeg.Capabilities.Vaapi; + +public static class VaapiCapabilityParser +{ + public static List Parse(string output) + { + var profileEntrypoints = new List(); + + foreach (string line in string.Join("", output).Split("\n")) + { + const string PROFILE_ENTRYPOINT_PATTERN = @"(VAProfile\w*).*(VAEntrypoint\w*)"; + Match match = Regex.Match(line, PROFILE_ENTRYPOINT_PATTERN); + if (match.Success) + { + profileEntrypoints.Add( + new VaapiProfileEntrypoint( + match.Groups[1].Value.Trim(), + match.Groups[2].Value.Trim())); + } + } + + return profileEntrypoints; + } + + public static List ParseFull(string output) + { + var profileEntrypoints = new List(); + var profile = new VaapiProfileEntrypoint(string.Empty, string.Empty); + string[] allLines = string.Join("", output).Split("\n"); + + for (var i = 0; i < allLines.Length; i++) + { + string line = allLines[i]; + const string PROFILE_ENTRYPOINT_PATTERN = @"(VAProfile\w*).*(VAEntrypoint\w*)"; + const string PROFILE_RATE_CONTROL_PATTERN = @".*VA_RC_(\w*).*"; + Match match = Regex.Match(line, PROFILE_ENTRYPOINT_PATTERN); + if (match.Success) + { + profile = new VaapiProfileEntrypoint(match.Groups[1].Value.Trim(), match.Groups[2].Value.Trim()); + profileEntrypoints.Add(profile); + } + else + { + // check for rate control + match = Regex.Match(line, PROFILE_RATE_CONTROL_PATTERN); + if (match.Success) + { + switch (match.Groups[1].Value.Trim().ToLowerInvariant()) + { + case "cqp": + profile.AddRateControlMode(RateControlMode.CQP); + break; + case "vbr": + profile.AddRateControlMode(RateControlMode.VBR); + break; + case "cbr": + profile.AddRateControlMode(RateControlMode.CBR); + break; + } + } + } + } + + return profileEntrypoints; + } +} diff --git a/ErsatzTV.FFmpeg/Capabilities/Vaapi/VaapiProfileEntrypoint.cs b/ErsatzTV.FFmpeg/Capabilities/Vaapi/VaapiProfileEntrypoint.cs index b439f9510..c8017aa5f 100644 --- a/ErsatzTV.FFmpeg/Capabilities/Vaapi/VaapiProfileEntrypoint.cs +++ b/ErsatzTV.FFmpeg/Capabilities/Vaapi/VaapiProfileEntrypoint.cs @@ -1,3 +1,13 @@ namespace ErsatzTV.FFmpeg.Capabilities.Vaapi; -public record VaapiProfileEntrypoint(string VaapiProfile, string VaapiEntrypoint); +public record VaapiProfileEntrypoint(string VaapiProfile, string VaapiEntrypoint) +{ + private readonly System.Collections.Generic.HashSet _rateControlModes = new(); + + public IReadOnlyCollection RateControlModes => _rateControlModes; + + public bool AddRateControlMode(RateControlMode mode) + { + return _rateControlModes.Add(mode); + } +} diff --git a/ErsatzTV.FFmpeg/Capabilities/VaapiHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/VaapiHardwareCapabilities.cs index a48ce92dd..5776ac386 100644 --- a/ErsatzTV.FFmpeg/Capabilities/VaapiHardwareCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/VaapiHardwareCapabilities.cs @@ -122,28 +122,32 @@ public class VaapiHardwareCapabilities : IHardwareCapabilities VideoFormat.H264 when bitDepth == 10 => false, VideoFormat.H264 => - _profileEntrypoints.Contains( - new VaapiProfileEntrypoint(VaapiProfile.H264Main, VaapiEntrypoint.Encode)) || - _profileEntrypoints.Contains( - new VaapiProfileEntrypoint(VaapiProfile.H264Main, VaapiEntrypoint.EncodeLowPower)), + _profileEntrypoints.Any(e => e is + { + VaapiProfile: VaapiProfile.H264Main, + VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower + }), VideoFormat.Hevc when bitDepth == 10 => - _profileEntrypoints.Contains( - new VaapiProfileEntrypoint(VaapiProfile.HevcMain10, VaapiEntrypoint.Encode)) || - _profileEntrypoints.Contains( - new VaapiProfileEntrypoint(VaapiProfile.HevcMain10, VaapiEntrypoint.EncodeLowPower)), + _profileEntrypoints.Any(e => e is + { + VaapiProfile: VaapiProfile.HevcMain10, + VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower + }), VideoFormat.Hevc => - _profileEntrypoints.Contains( - new VaapiProfileEntrypoint(VaapiProfile.HevcMain, VaapiEntrypoint.Encode)) || - _profileEntrypoints.Contains( - new VaapiProfileEntrypoint(VaapiProfile.HevcMain, VaapiEntrypoint.EncodeLowPower)), + _profileEntrypoints.Any(e => e is + { + VaapiProfile: VaapiProfile.HevcMain, + VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower + }), VideoFormat.Mpeg2Video => - _profileEntrypoints.Contains( - new VaapiProfileEntrypoint(VaapiProfile.Mpeg2Main, VaapiEntrypoint.Encode)) || - _profileEntrypoints.Contains( - new VaapiProfileEntrypoint(VaapiProfile.Mpeg2Main, VaapiEntrypoint.EncodeLowPower)), + _profileEntrypoints.Any(e => e is + { + VaapiProfile: VaapiProfile.Mpeg2Main, + VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower + }), _ => false }; @@ -158,4 +162,68 @@ public class VaapiHardwareCapabilities : IHardwareCapabilities return isHardware ? FFmpegCapability.Hardware : FFmpegCapability.Software; } + + public Option GetRateControlMode(string videoFormat, Option maybePixelFormat) + { + int bitDepth = maybePixelFormat.Map(pf => pf.BitDepth).IfNone(8); + Option maybeEntrypoint = videoFormat switch + { + // vaapi cannot encode 10-bit h264 + VideoFormat.H264 when bitDepth == 10 => None, + + VideoFormat.H264 => + _profileEntrypoints.Where( + e => e is + { + VaapiProfile: VaapiProfile.H264Main, + VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower + }) + .HeadOrNone(), + + VideoFormat.Hevc when bitDepth == 10 => + _profileEntrypoints.Where( + e => e is + { + VaapiProfile: VaapiProfile.HevcMain10, + VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower + }) + .HeadOrNone(), + + VideoFormat.Hevc => + _profileEntrypoints.Where( + e => e is + { + VaapiProfile: VaapiProfile.HevcMain, + VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower + }) + .HeadOrNone(), + + VideoFormat.Mpeg2Video => + _profileEntrypoints.Where( + e => e is + { + VaapiProfile: VaapiProfile.Mpeg2Main, + VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower + }) + .HeadOrNone(), + + _ => None + }; + + foreach (VaapiProfileEntrypoint entrypoint in maybeEntrypoint) + { + if (entrypoint.RateControlModes.Contains(RateControlMode.VBR) || + entrypoint.RateControlModes.Contains(RateControlMode.CBR)) + { + return Option.None; + } + + if (entrypoint.RateControlModes.Contains(RateControlMode.CQP)) + { + return RateControlMode.CQP; + } + } + + return Option.None; + } } diff --git a/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderH264Vaapi.cs b/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderH264Vaapi.cs index 42f5994f0..7db2f52e4 100644 --- a/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderH264Vaapi.cs +++ b/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderH264Vaapi.cs @@ -4,8 +4,29 @@ namespace ErsatzTV.FFmpeg.Encoder.Vaapi; public class EncoderH264Vaapi : EncoderBase { + private readonly RateControlMode _rateControlMode; + + public EncoderH264Vaapi(RateControlMode rateControlMode) => _rateControlMode = rateControlMode; + public override string Name => "h264_vaapi"; + public override StreamKind Kind => StreamKind.Video; + + public override IList OutputOptions + { + get + { + IList result = base.OutputOptions; + + if (_rateControlMode == RateControlMode.CQP) + { + result.Add("-rc_mode"); + result.Add("1"); + } + + return result; + } + } public override FrameState NextState(FrameState currentState) => currentState with { diff --git a/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderHevcVaapi.cs b/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderHevcVaapi.cs index 131c88534..485960c51 100644 --- a/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderHevcVaapi.cs +++ b/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderHevcVaapi.cs @@ -4,9 +4,30 @@ namespace ErsatzTV.FFmpeg.Encoder.Vaapi; public class EncoderHevcVaapi : EncoderBase { + private readonly RateControlMode _rateControlMode; + + public EncoderHevcVaapi(RateControlMode rateControlMode) => _rateControlMode = rateControlMode; + public override string Name => "hevc_vaapi"; + public override StreamKind Kind => StreamKind.Video; + public override IList OutputOptions + { + get + { + IList result = base.OutputOptions; + + if (_rateControlMode == RateControlMode.CQP) + { + result.Add("-rc_mode"); + result.Add("1"); + } + + return result; + } + } + public override FrameState NextState(FrameState currentState) => currentState with { VideoFormat = VideoFormat.Hevc diff --git a/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderMpeg2Vaapi.cs b/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderMpeg2Vaapi.cs index 2389354d9..b78598a34 100644 --- a/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderMpeg2Vaapi.cs +++ b/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderMpeg2Vaapi.cs @@ -4,8 +4,29 @@ namespace ErsatzTV.FFmpeg.Encoder.Vaapi; public class EncoderMpeg2Vaapi : EncoderBase { + private readonly RateControlMode _rateControlMode; + + public EncoderMpeg2Vaapi(RateControlMode rateControlMode) => _rateControlMode = rateControlMode; + public override string Name => "mpeg2_vaapi"; + public override StreamKind Kind => StreamKind.Video; + + public override IList OutputOptions + { + get + { + IList result = base.OutputOptions; + + if (_rateControlMode == RateControlMode.CQP) + { + result.Add("-rc_mode"); + result.Add("1"); + } + + return result; + } + } public override FrameState NextState(FrameState currentState) => currentState with { diff --git a/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs index f334f05af..0943a10b2 100644 --- a/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs @@ -197,12 +197,16 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder // after everything else is done, apply the encoder if (pipelineSteps.OfType().All(e => e.Kind != StreamKind.Video)) { + RateControlMode rateControlMode = + _hardwareCapabilities.GetRateControlMode(videoStream.Codec, videoStream.PixelFormat) + .IfNone(RateControlMode.VBR); + Option maybeEncoder = (ffmpegState.EncoderHardwareAccelerationMode, desiredState.VideoFormat) switch { - (HardwareAccelerationMode.Vaapi, VideoFormat.Hevc) => new EncoderHevcVaapi(), - (HardwareAccelerationMode.Vaapi, VideoFormat.H264) => new EncoderH264Vaapi(), - (HardwareAccelerationMode.Vaapi, VideoFormat.Mpeg2Video) => new EncoderMpeg2Vaapi(), + (HardwareAccelerationMode.Vaapi, VideoFormat.Hevc) => new EncoderHevcVaapi(rateControlMode), + (HardwareAccelerationMode.Vaapi, VideoFormat.H264) => new EncoderH264Vaapi(rateControlMode), + (HardwareAccelerationMode.Vaapi, VideoFormat.Mpeg2Video) => new EncoderMpeg2Vaapi(rateControlMode), (_, _) => GetSoftwareEncoder(currentState, desiredState) }; diff --git a/ErsatzTV.FFmpeg/RateControlMode.cs b/ErsatzTV.FFmpeg/RateControlMode.cs new file mode 100644 index 000000000..75afdd3f2 --- /dev/null +++ b/ErsatzTV.FFmpeg/RateControlMode.cs @@ -0,0 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + +namespace ErsatzTV.FFmpeg; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public enum RateControlMode +{ + CBR, + CQP, + VBR +}