From 0489741123512a073c0383c455f0588eea32b99a Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:16:24 +0000 Subject: [PATCH] add videotoolbox capabilities (#2239) * implement videotoolbox hardware capabilities * add videotoolbox troubleshooting info * update changelog --- CHANGELOG.md | 3 + .../Commands/CreateFFmpegProfileHandler.cs | 7 +- .../Commands/UpdateFFmpegProfileHandler.cs | 2 +- .../Queries/GetTroubleshootingInfoHandler.cs | 44 +++-- .../Troubleshooting/TroubleshootingInfo.cs | 3 +- .../FFmpeg/FFmpegLibraryProcessService.cs | 1 + .../Capabilities/FFmpegKnownEncoder.cs | 14 +- .../HardwareCapabilitiesFactory.cs | 22 +++ .../IHardwareCapabilitiesFactory.cs | 2 + .../VideoToolbox/VideoToolboxUtil.cs | 150 ++++++++++++++++++ .../VideoToolboxHardwareCapabilities.cs | 78 +++++++++ ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj | 1 + ErsatzTV.FFmpeg/Format/VideoProfile.cs | 1 + ErsatzTV/Pages/FFmpegEditor.razor | 3 +- ErsatzTV/Pages/Troubleshooting.razor | 89 +++++++---- .../FFmpegProfileEditViewModelValidator.cs | 43 +++-- 16 files changed, 395 insertions(+), 68 deletions(-) create mode 100644 ErsatzTV.FFmpeg/Capabilities/VideoToolbox/VideoToolboxUtil.cs create mode 100644 ErsatzTV.FFmpeg/Capabilities/VideoToolboxHardwareCapabilities.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d99f1b16..1d822517 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - If the playlist has 3 items and none set to play all, it will schedule 3 items when `Count = 1` - If the playlist has 3 items and none set to play all, it will schedule 6 items when `Count = 2` - Using the same playlist in the same schedule for anything other than filler may cause undesired behavior +- Detect supported VideoToolbox hardware encoders + - Software encoders will automatically be used when hardware encoders are unavailable +- Add VideoToolbox Capabilities to Troubleshooting page ### Fixed - Fix app startup with MySql/MariaDB diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs index 76863758..e6fc9360 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs @@ -56,7 +56,12 @@ public class CreateFFmpegProfileHandler : VideoProfile = request.VideoProfile, VideoPreset = request.VideoPreset, AllowBFrames = request.AllowBFrames, - BitDepth = request.BitDepth, + + // mpeg2video only supports 8-bit content + BitDepth = request.VideoFormat is FFmpegProfileVideoFormat.Mpeg2Video + ? FFmpegProfileBitDepth.EightBit + : request.BitDepth, + VideoBitrate = request.VideoBitrate, VideoBufferSize = request.VideoBufferSize, TonemapAlgorithm = request.TonemapAlgorithm, diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs index 2d7b818c..d0631a04 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs @@ -48,7 +48,7 @@ public class p.AllowBFrames = update.AllowBFrames; // mpeg2video only supports 8-bit content - p.BitDepth = update.VideoFormat == FFmpegProfileVideoFormat.Mpeg2Video + p.BitDepth = update.VideoFormat is FFmpegProfileVideoFormat.Mpeg2Video ? FFmpegProfileBitDepth.EightBit : update.BitDepth; diff --git a/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs b/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs index 533a657e..3407eac6 100644 --- a/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs +++ b/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs @@ -1,6 +1,8 @@ using System.Collections; +using System.Globalization; using System.Reflection; using System.Runtime.InteropServices; +using System.Text; using ErsatzTV.Application.FFmpegProfiles; using ErsatzTV.Core.Domain; using ErsatzTV.Core.FFmpeg; @@ -72,8 +74,9 @@ public class GetTroubleshootingInfoHandler : IRequestHandler maybeFFmpegPath = await _configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath); if (maybeFFmpegPath.IsNone) @@ -99,10 +102,11 @@ public class GetTroubleshootingInfoHandler : IRequestHandler Watermarks, string NvidiaCapabilities, string QsvCapabilities, - string VaapiCapabilities); + string VaapiCapabilities, + string VideoToolboxCapabilities); diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 98d23606..3a9d93b3 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -1139,6 +1139,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService { (VideoFormat.H264, VideoProfile.Main) => VideoProfile.Main, (VideoFormat.H264, VideoProfile.High) => VideoProfile.High, + (VideoFormat.H264, VideoProfile.High10) => VideoProfile.High10, _ => Option.None }; diff --git a/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownEncoder.cs b/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownEncoder.cs index 8163a6d0..9a574b3d 100644 --- a/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownEncoder.cs +++ b/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownEncoder.cs @@ -6,11 +6,15 @@ public record FFmpegKnownEncoder public string Name { get; } + public static readonly FFmpegKnownEncoder H264VideoToolbox = new("h264_videotoolbox"); + public static readonly FFmpegKnownEncoder HevcVideoToolbox = new("hevc_videotoolbox"); + // only list the encoders that we actually check for public static IList AllEncoders => - new[] - { - "h264_amf", - "hevc_amf" - }; + [ + "h264_amf", + "hevc_amf", + "h264_videotoolbox", + "hevc_videotoolbox" + ]; } diff --git a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs index 3766ebe1..bb63af4e 100644 --- a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs +++ b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs @@ -7,6 +7,7 @@ using CliWrap; using CliWrap.Buffered; using ErsatzTV.FFmpeg.Capabilities.Qsv; using ErsatzTV.FFmpeg.Capabilities.Vaapi; +using ErsatzTV.FFmpeg.Capabilities.VideoToolbox; using ErsatzTV.FFmpeg.GlobalOption.HardwareAcceleration; using ErsatzTV.FFmpeg.Runtime; using Hardware.Info; @@ -106,6 +107,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory HardwareAccelerationMode.Nvenc => await GetNvidiaCapabilities(ffmpegPath, ffmpegCapabilities), HardwareAccelerationMode.Qsv => await GetQsvCapabilities(ffmpegPath, vaapiDevice), HardwareAccelerationMode.Vaapi => await GetVaapiCapabilities(vaapiDisplay, vaapiDriver, vaapiDevice), + HardwareAccelerationMode.VideoToolbox => new VideoToolboxHardwareCapabilities(ffmpegCapabilities, _logger), HardwareAccelerationMode.Amf => new AmfHardwareCapabilities(), _ => new DefaultHardwareCapabilities() }; @@ -113,6 +115,11 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory public async Task GetNvidiaOutput(string ffmpegPath) { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return string.Empty; + } + string[] arguments = { "-f", "lavfi", @@ -136,6 +143,11 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory public async Task GetQsvOutput(string ffmpegPath, Option qsvDevice) { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return new QsvOutput(0, string.Empty); + } + var option = new QsvHardwareAccelerationOption(qsvDevice, FFmpegCapability.Software); var arguments = option.GlobalOptions.ToList(); @@ -155,6 +167,11 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory public async Task> GetVaapiOutput(string display, Option vaapiDriver, string vaapiDevice) { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return Option.None; + } + BufferedCommandResult whichResult = await Cli.Wrap("which") .WithArguments("vainfo") .WithValidation(CommandResultValidation.None) @@ -244,6 +261,11 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory return []; } + public List GetVideoToolboxEncoders() + { + return VideoToolboxUtil.GetAvailableEncoders(); + } + private async Task> GetFFmpegCapabilities( string ffmpegPath, string capabilities, diff --git a/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs index eda37dec..0cf43c07 100644 --- a/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs +++ b/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs @@ -25,4 +25,6 @@ public interface IHardwareCapabilitiesFactory List GetCpuList(); List GetVideoControllerList(); + + List GetVideoToolboxEncoders(); } diff --git a/ErsatzTV.FFmpeg/Capabilities/VideoToolbox/VideoToolboxUtil.cs b/ErsatzTV.FFmpeg/Capabilities/VideoToolbox/VideoToolboxUtil.cs new file mode 100644 index 00000000..fbdad717 --- /dev/null +++ b/ErsatzTV.FFmpeg/Capabilities/VideoToolbox/VideoToolboxUtil.cs @@ -0,0 +1,150 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace ErsatzTV.FFmpeg.Capabilities.VideoToolbox; + +internal static partial class VideoToolboxUtil +{ + private const string CoreFoundation = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; + private const string VideoToolbox = "/System/Library/Frameworks/VideoToolbox.framework/VideoToolbox"; + private const string LibSystem = "/usr/lib/libSystem.dylib"; + + [LibraryImport(CoreFoundation)] + private static partial long CFArrayGetCount(IntPtr array); + + [LibraryImport(CoreFoundation)] + private static partial IntPtr CFArrayGetValueAtIndex(IntPtr array, int index); + + [LibraryImport(CoreFoundation)] + private static partial IntPtr CFDictionaryGetValue(IntPtr dict, IntPtr key); + + [LibraryImport(CoreFoundation)] + private static partial IntPtr CFStringGetLength(IntPtr theString); + + [LibraryImport(CoreFoundation)] + private static partial IntPtr CFStringGetCStringPtr(IntPtr theString, uint encoding); + + [LibraryImport(CoreFoundation, StringMarshalling = StringMarshalling.Utf8)] + [return: MarshalAs(UnmanagedType.I1)] + private static partial bool CFStringGetCString(IntPtr theString, byte[] buffer, long bufferSize, uint encoding); + + [LibraryImport(CoreFoundation)] + private static partial void CFRelease(IntPtr cf); + + [LibraryImport(VideoToolbox)] + private static partial int VTCopyVideoEncoderList(IntPtr options, out IntPtr listOfEncoders); + + [LibraryImport(LibSystem, StringMarshalling = StringMarshalling.Utf8)] + private static partial IntPtr dlopen(string path, int mode); + + [LibraryImport(LibSystem, StringMarshalling = StringMarshalling.Utf8)] + private static partial IntPtr dlsym(IntPtr handle, string symbol); + + [LibraryImport(LibSystem)] + private static partial int dlclose(IntPtr handle); + + private static IntPtr GetCFString(string frameworkPath, string symbolName) + { + IntPtr frameworkHandle = dlopen(frameworkPath, 0); // RTLD_NOW + if (frameworkHandle == IntPtr.Zero) + { + return IntPtr.Zero; + } + + try + { + IntPtr symbol = dlsym(frameworkHandle, symbolName); + return Marshal.ReadIntPtr(symbol); + } + finally + { + _ = dlclose(frameworkHandle); + } + } + + private static string? CFStringToString(IntPtr cfString) + { + if (cfString == IntPtr.Zero) + { + return null; + } + + const uint kCFStringEncodingUTF8 = 0x08000100; + + IntPtr cStringPtr = CFStringGetCStringPtr(cfString, kCFStringEncodingUTF8); + if (cStringPtr != IntPtr.Zero) + { + return Marshal.PtrToStringAnsi(cStringPtr); + } + + long length = CFStringGetLength(cfString); + if (length == 0) + { + return string.Empty; + } + + long maxSize = length * 4 + 1; + byte[] buffer = new byte[maxSize]; + if (CFStringGetCString(cfString, buffer, maxSize, kCFStringEncodingUTF8)) + { + int terminator = Array.IndexOf(buffer, (byte)0); + int actualLength = terminator >= 0 ? terminator : buffer.Length; + return Encoding.UTF8.GetString(buffer, 0, actualLength); + } + + return null; + } + + internal static List GetAvailableEncoders() + { + var encoderNames = new List(); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return encoderNames; + } + + IntPtr kVTVideoEncoderList_EncoderName = GetCFString(VideoToolbox, "kVTVideoEncoderList_EncoderName"); + if (kVTVideoEncoderList_EncoderName == IntPtr.Zero) + { + Console.Error.WriteLine("Failed to load kVTVideoEncoderList_EncoderName symbol."); + return encoderNames; + } + + IntPtr encoderList = IntPtr.Zero; + try + { + int status = VTCopyVideoEncoderList(IntPtr.Zero, out encoderList); + + if (status != 0 || encoderList == IntPtr.Zero) + { + Console.Error.WriteLine($"VTCopyVideoEncoderList failed with status: {status}"); + return encoderNames; + } + + var count = (int)CFArrayGetCount(encoderList); + for (int i = 0; i < count; i++) + { + IntPtr encoderDict = CFArrayGetValueAtIndex(encoderList, i); + if (encoderDict == IntPtr.Zero) continue; + + IntPtr encoderNameCfString = CFDictionaryGetValue(encoderDict, kVTVideoEncoderList_EncoderName); + string? encoderName = CFStringToString(encoderNameCfString); + + if (!string.IsNullOrEmpty(encoderName)) + { + encoderNames.Add(encoderName); + } + } + } + finally + { + if (encoderList != IntPtr.Zero) + { + CFRelease(encoderList); + } + } + + return encoderNames; + } +} \ No newline at end of file diff --git a/ErsatzTV.FFmpeg/Capabilities/VideoToolboxHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/VideoToolboxHardwareCapabilities.cs new file mode 100644 index 00000000..d40441d1 --- /dev/null +++ b/ErsatzTV.FFmpeg/Capabilities/VideoToolboxHardwareCapabilities.cs @@ -0,0 +1,78 @@ +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using ErsatzTV.FFmpeg.Capabilities.VideoToolbox; +using ErsatzTV.FFmpeg.Format; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.FFmpeg.Capabilities; + +public class VideoToolboxHardwareCapabilities : IHardwareCapabilities +{ + private static readonly ConcurrentDictionary Encoders = new (); + + private readonly IFFmpegCapabilities _ffmpegCapabilities; + private readonly ILogger _logger; + + public VideoToolboxHardwareCapabilities(IFFmpegCapabilities ffmpegCapabilities, ILogger logger) + { + _ffmpegCapabilities = ffmpegCapabilities; + _logger = logger; + } + + public FFmpegCapability CanDecode(string videoFormat, Option videoProfile, Option maybePixelFormat, bool isHdr) + { + int bitDepth = maybePixelFormat.Map(pf => pf.BitDepth).IfNone(8); + return (videoFormat, bitDepth) switch + { + // 10-bit h264 decoding is likely not support by any hardware + (VideoFormat.H264, 10) => FFmpegCapability.Software, + + _ => FFmpegCapability.Hardware + }; + } + + public FFmpegCapability CanEncode(string videoFormat, Option videoProfile, Option maybePixelFormat) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && Encoders.IsEmpty) + { + var encoderList = VideoToolboxUtil.GetAvailableEncoders(); + _logger.LogDebug("VideoToolbox reports {Count} encoders", encoderList.Count); + + // we only really care about h264 and hevc hardware encoders + foreach (var encoder in encoderList) + { + if (encoder.Contains("HEVC (HW)", StringComparison.OrdinalIgnoreCase)) + { + Encoders.AddOrUpdate(VideoFormat.Hevc, true, (_, _) => true); + } + + if (encoder.Contains("H.264 (HW)", StringComparison.OrdinalIgnoreCase)) + { + Encoders.AddOrUpdate(VideoFormat.H264, true, (_, _) => true); + } + } + } + + int bitDepth = maybePixelFormat.Map(pf => pf.BitDepth).IfNone(8); + return (videoFormat, bitDepth) switch + { + // 10-bit h264 encoding is not support by any hardware + (VideoFormat.H264, 10) => FFmpegCapability.Software, + + (VideoFormat.H264, 8) => + _ffmpegCapabilities.HasEncoder(FFmpegKnownEncoder.H264VideoToolbox) && Encoders.ContainsKey(videoFormat) + ? FFmpegCapability.Hardware + : FFmpegCapability.Software, + + (VideoFormat.Hevc, _) => + _ffmpegCapabilities.HasEncoder(FFmpegKnownEncoder.HevcVideoToolbox) && Encoders.ContainsKey(videoFormat) + ? FFmpegCapability.Hardware + : FFmpegCapability.Software, + + _ => FFmpegCapability.Software + }; + } + + public Option GetRateControlMode(string videoFormat, Option maybePixelFormat) => + Option.None; +} \ No newline at end of file diff --git a/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj b/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj index cf6334bc..1cda40c9 100644 --- a/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj +++ b/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj @@ -6,6 +6,7 @@ enable latest-Recommended true + true diff --git a/ErsatzTV.FFmpeg/Format/VideoProfile.cs b/ErsatzTV.FFmpeg/Format/VideoProfile.cs index 24fe9806..49f26ac3 100644 --- a/ErsatzTV.FFmpeg/Format/VideoProfile.cs +++ b/ErsatzTV.FFmpeg/Format/VideoProfile.cs @@ -4,4 +4,5 @@ public static class VideoProfile { public const string Main = "main"; public const string High = "high"; + public const string High10 = "high10"; } diff --git a/ErsatzTV/Pages/FFmpegEditor.razor b/ErsatzTV/Pages/FFmpegEditor.razor index 3e50c6d6..31faa6f6 100644 --- a/ErsatzTV/Pages/FFmpegEditor.razor +++ b/ErsatzTV/Pages/FFmpegEditor.razor @@ -77,10 +77,11 @@ main high + high10 diff --git a/ErsatzTV/Pages/Troubleshooting.razor b/ErsatzTV/Pages/Troubleshooting.razor index bbfdea2a..436da41e 100644 --- a/ErsatzTV/Pages/Troubleshooting.razor +++ b/ErsatzTV/Pages/Troubleshooting.razor @@ -1,4 +1,5 @@ @page "/system/troubleshooting" +@using System.Runtime.InteropServices @using System.Text.Json @using System.Text.Json.Serialization @using ErsatzTV.Application.Troubleshooting @@ -27,42 +28,63 @@ Copy - NVIDIA Capabilities - - -
-
-                        @_nvidiaCapabilities
-                    
-
- - Copy - -
- QSV Capabilities - - -
-
+            @if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            {
+                VideoToolbox Capabilities
+                
+                
+                    
+
+                            @_videoToolboxCapabilities
+                        
+
+ + Copy + +
+ } + else + { + NVIDIA Capabilities + + +
+
+                            @_nvidiaCapabilities
+                        
+
+ + Copy + +
+ QSV Capabilities + + +
+
                         @_qsvCapabilities
                     
-
- - Copy - -
- VAAPI Capabilities - - -
-
+                    
+ + Copy + +
+ } + @if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + VAAPI Capabilities + + +
+
                         @_vaapiCapabilities
                     
-
- - Copy - -
+
+ + Copy + +
+ } @@ -73,10 +95,12 @@ private string _nvidiaCapabilities; private string _qsvCapabilities; private string _vaapiCapabilities; + private string _videoToolboxCapabilities; private ElementReference _troubleshootingView; private ElementReference _nvidiaView; private ElementReference _qsvView; private ElementReference _vaapiView; + private ElementReference _videoToolboxView; public void Dispose() { @@ -112,6 +136,7 @@ _nvidiaCapabilities = info.NvidiaCapabilities; _qsvCapabilities = info.QsvCapabilities; _vaapiCapabilities = info.VaapiCapabilities; + _videoToolboxCapabilities = info.VideoToolboxCapabilities; } catch (Exception ex) { diff --git a/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs b/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs index d76f5f21..9683f6aa 100644 --- a/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs +++ b/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs @@ -1,4 +1,5 @@ using ErsatzTV.Core.Domain; +using ErsatzTV.FFmpeg.Format; using ErsatzTV.ViewModels; using FluentValidation; using FluentValidation.Results; @@ -7,37 +8,37 @@ namespace ErsatzTV.Validators; public class FFmpegProfileEditViewModelValidator : AbstractValidator { - private static readonly List QsvFormats = new() - { + private static readonly List QsvFormats = + [ FFmpegProfileVideoFormat.H264, FFmpegProfileVideoFormat.Hevc, FFmpegProfileVideoFormat.Mpeg2Video - }; + ]; - private static readonly List NvencFormats = new() - { + private static readonly List NvencFormats = + [ FFmpegProfileVideoFormat.H264, FFmpegProfileVideoFormat.Hevc - }; + ]; - private static readonly List VaapiFormats = new() - { + private static readonly List VaapiFormats = + [ FFmpegProfileVideoFormat.H264, FFmpegProfileVideoFormat.Hevc, FFmpegProfileVideoFormat.Mpeg2Video - }; + ]; - private static readonly List VideoToolboxFormats = new() - { + private static readonly List VideoToolboxFormats = + [ FFmpegProfileVideoFormat.H264, FFmpegProfileVideoFormat.Hevc - }; + ]; - private static readonly List AmfFormats = new() - { + private static readonly List AmfFormats = + [ FFmpegProfileVideoFormat.H264, FFmpegProfileVideoFormat.Hevc - }; + ]; public FFmpegProfileEditViewModelValidator() { @@ -97,6 +98,18 @@ public class FFmpegProfileEditViewModelValidator : AbstractValidator RuleFor(x => x.BitDepth) .Must(bd => bd is FFmpegProfileBitDepth.EightBit) .WithMessage("Mpeg2Video does not support 10-bit content")); + + When( + x => x.VideoFormat == FFmpegProfileVideoFormat.H264 && x.BitDepth == FFmpegProfileBitDepth.TenBit, + () => RuleFor(x => x.VideoProfile) + .Must(vp => vp == VideoProfile.High10) + .WithMessage("VideoProfile must be high10 with 10-bit h264")); + + When( + x => x.VideoFormat == FFmpegProfileVideoFormat.H264 && x.BitDepth == FFmpegProfileBitDepth.EightBit, + () => RuleFor(x => x.VideoProfile) + .Must(vp => vp != VideoProfile.High10) + .WithMessage("VideoProfile cannot be high10 with 8-bit h264")); } public Func>> ValidateValue => async (model, propertyName) =>