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 @@
@@ -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 @@
@@ -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