mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* implement videotoolbox hardware capabilities * add videotoolbox troubleshooting info * update changelogpull/2240/head
16 changed files with 395 additions and 68 deletions
@ -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<string> GetAvailableEncoders() |
||||||
|
{ |
||||||
|
var encoderNames = new List<string>(); |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
} |
@ -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<string, bool> 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<string> videoProfile, Option<IPixelFormat> 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<string> videoProfile, Option<IPixelFormat> 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<RateControlMode> GetRateControlMode(string videoFormat, Option<IPixelFormat> maybePixelFormat) => |
||||||
|
Option<RateControlMode>.None; |
||||||
|
} |
Loading…
Reference in new issue