Working version
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AmigaDB.VideoRenderer.Services;
|
||||
|
||||
internal sealed partial class FfmpegLibraryProbe : IDisposable
|
||||
{
|
||||
private const int AvMediaTypeAudio = 1;
|
||||
private const int AvLogInfo = 32;
|
||||
|
||||
private static readonly object ProbeLock = new();
|
||||
private static readonly AvLogCallback SharedLogCallback = HandleLogMessage;
|
||||
private static StringBuilder? s_activeLogBuffer;
|
||||
private static FfmpegBindings? s_activeBindings;
|
||||
|
||||
private readonly IntPtr _avformatHandle;
|
||||
private readonly IntPtr _avutilHandle;
|
||||
private readonly FfmpegBindings _bindings;
|
||||
private readonly string _libraryDirectory;
|
||||
|
||||
private FfmpegLibraryProbe(string libraryDirectory, IntPtr avformatHandle, IntPtr avutilHandle)
|
||||
{
|
||||
_libraryDirectory = libraryDirectory;
|
||||
_avformatHandle = avformatHandle;
|
||||
_avutilHandle = avutilHandle;
|
||||
_bindings = new FfmpegBindings(avformatHandle, avutilHandle);
|
||||
}
|
||||
|
||||
public static bool TryCreate(string executablePath, out FfmpegLibraryProbe? probe)
|
||||
{
|
||||
probe = null;
|
||||
string? directory = Path.GetDirectoryName(executablePath);
|
||||
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
|
||||
return false;
|
||||
|
||||
string? avformatPath = FindLibrary(directory, "avformat");
|
||||
string? avutilPath = FindLibrary(directory, "avutil");
|
||||
if (avformatPath is null || avutilPath is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
IntPtr avformatHandle = NativeLibrary.Load(avformatPath);
|
||||
IntPtr avutilHandle = NativeLibrary.Load(avutilPath);
|
||||
probe = new FfmpegLibraryProbe(directory, avformatHandle, avutilHandle);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
probe?.Dispose();
|
||||
probe = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public double ProbeDuration(string inputPath)
|
||||
{
|
||||
string dump = ProbeFormatDump(inputPath);
|
||||
Match match = DurationRegex().Match(dump);
|
||||
if (!match.Success)
|
||||
throw new InvalidOperationException("FFmpeg library probing could not determine media duration.");
|
||||
|
||||
int hours = int.Parse(match.Groups["hours"].Value, CultureInfo.InvariantCulture);
|
||||
int minutes = int.Parse(match.Groups["minutes"].Value, CultureInfo.InvariantCulture);
|
||||
double seconds = double.Parse(match.Groups["seconds"].Value, CultureInfo.InvariantCulture);
|
||||
return new TimeSpan(0, hours, minutes, 0).TotalSeconds + seconds;
|
||||
}
|
||||
|
||||
public bool HasAudio(string inputPath)
|
||||
{
|
||||
IntPtr context = OpenInput(inputPath);
|
||||
try
|
||||
{
|
||||
int streamIndex = _bindings.AvFindBestStream(context, AvMediaTypeAudio, -1, -1, IntPtr.Zero, 0);
|
||||
return streamIndex >= 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_bindings.AvFormatCloseInput(ref context);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_avformatHandle != IntPtr.Zero) NativeLibrary.Free(_avformatHandle);
|
||||
if (_avutilHandle != IntPtr.Zero) NativeLibrary.Free(_avutilHandle);
|
||||
}
|
||||
|
||||
private string ProbeFormatDump(string inputPath)
|
||||
{
|
||||
IntPtr context = OpenInput(inputPath);
|
||||
try
|
||||
{
|
||||
lock (ProbeLock)
|
||||
{
|
||||
StringBuilder buffer = new();
|
||||
s_activeBindings = _bindings;
|
||||
s_activeLogBuffer = buffer;
|
||||
_bindings.AvLogSetLevel(AvLogInfo);
|
||||
_bindings.AvLogSetCallback(SharedLogCallback);
|
||||
_bindings.AvDumpFormat(context, 0, inputPath, 0);
|
||||
s_activeLogBuffer = null;
|
||||
s_activeBindings = null;
|
||||
return buffer.ToString();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_bindings.AvFormatCloseInput(ref context);
|
||||
}
|
||||
}
|
||||
|
||||
private IntPtr OpenInput(string inputPath)
|
||||
{
|
||||
IntPtr context = IntPtr.Zero;
|
||||
int openResult = _bindings.AvFormatOpenInput(ref context, inputPath, IntPtr.Zero, IntPtr.Zero);
|
||||
if (openResult < 0)
|
||||
throw new InvalidOperationException($"FFmpeg library probing failed to open '{inputPath}': {FormatError(openResult)}");
|
||||
|
||||
int infoResult = _bindings.AvFormatFindStreamInfo(context, IntPtr.Zero);
|
||||
if (infoResult < 0)
|
||||
{
|
||||
_bindings.AvFormatCloseInput(ref context);
|
||||
throw new InvalidOperationException($"FFmpeg library probing failed to read stream info for '{inputPath}': {FormatError(infoResult)}");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private string FormatError(int errorCode)
|
||||
{
|
||||
IntPtr buffer = Marshal.AllocHGlobal(1024);
|
||||
try
|
||||
{
|
||||
int result = _bindings.AvStrError(errorCode, buffer, (UIntPtr)1024);
|
||||
if (result < 0)
|
||||
return $"error {errorCode}";
|
||||
return Marshal.PtrToStringUTF8(buffer) ?? $"error {errorCode}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? FindLibrary(string directory, string prefix) =>
|
||||
Directory.EnumerateFiles(directory, $"{prefix}*.dll", SearchOption.TopDirectoryOnly)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault();
|
||||
|
||||
private static void HandleLogMessage(IntPtr avcl, int level, IntPtr format, IntPtr args)
|
||||
{
|
||||
StringBuilder? buffer = s_activeLogBuffer;
|
||||
FfmpegBindings? bindings = s_activeBindings;
|
||||
if (buffer is null || bindings is null)
|
||||
return;
|
||||
|
||||
IntPtr lineBuffer = Marshal.AllocHGlobal(4096);
|
||||
try
|
||||
{
|
||||
Marshal.Copy(new byte[4096], 0, lineBuffer, 4096);
|
||||
int printPrefix = 1;
|
||||
bindings.AvLogFormatLine2(avcl, level, format, args, lineBuffer, 4096, ref printPrefix);
|
||||
string? line = Marshal.PtrToStringUTF8(lineBuffer);
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
buffer.Append(line);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(lineBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"Duration:\s(?<hours>\d{2}):(?<minutes>\d{2}):(?<seconds>\d{2}(?:\.\d+)?)", RegexOptions.CultureInvariant)]
|
||||
private static partial Regex DurationRegex();
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void AvLogCallback(IntPtr avcl, int level, IntPtr format, IntPtr args);
|
||||
|
||||
private sealed class FfmpegBindings
|
||||
{
|
||||
public FfmpegBindings(IntPtr avformatHandle, IntPtr avutilHandle)
|
||||
{
|
||||
AvFormatOpenInput = GetDelegate<AvFormatOpenInputDelegate>(avformatHandle, "avformat_open_input");
|
||||
AvFormatFindStreamInfo = GetDelegate<AvFormatFindStreamInfoDelegate>(avformatHandle, "avformat_find_stream_info");
|
||||
AvFormatCloseInput = GetDelegate<AvFormatCloseInputDelegate>(avformatHandle, "avformat_close_input");
|
||||
AvFindBestStream = GetDelegate<AvFindBestStreamDelegate>(avformatHandle, "av_find_best_stream");
|
||||
AvDumpFormat = GetDelegate<AvDumpFormatDelegate>(avformatHandle, "av_dump_format");
|
||||
AvLogSetCallback = GetDelegate<AvLogSetCallbackDelegate>(avutilHandle, "av_log_set_callback");
|
||||
AvLogSetLevel = GetDelegate<AvLogSetLevelDelegate>(avutilHandle, "av_log_set_level");
|
||||
AvLogFormatLine2 = GetDelegate<AvLogFormatLine2Delegate>(avutilHandle, "av_log_format_line2");
|
||||
AvStrError = GetDelegate<AvStrErrorDelegate>(avutilHandle, "av_strerror");
|
||||
}
|
||||
|
||||
public AvFormatOpenInputDelegate AvFormatOpenInput { get; }
|
||||
public AvFormatFindStreamInfoDelegate AvFormatFindStreamInfo { get; }
|
||||
public AvFormatCloseInputDelegate AvFormatCloseInput { get; }
|
||||
public AvFindBestStreamDelegate AvFindBestStream { get; }
|
||||
public AvDumpFormatDelegate AvDumpFormat { get; }
|
||||
public AvLogSetCallbackDelegate AvLogSetCallback { get; }
|
||||
public AvLogSetLevelDelegate AvLogSetLevel { get; }
|
||||
public AvLogFormatLine2Delegate AvLogFormatLine2 { get; }
|
||||
public AvStrErrorDelegate AvStrError { get; }
|
||||
|
||||
private static T GetDelegate<T>(IntPtr handle, string exportName) where T : Delegate =>
|
||||
Marshal.GetDelegateForFunctionPointer<T>(NativeLibrary.GetExport(handle, exportName));
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int AvFormatOpenInputDelegate(ref IntPtr context, [MarshalAs(UnmanagedType.LPUTF8Str)] string url, IntPtr format, IntPtr options);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int AvFormatFindStreamInfoDelegate(IntPtr context, IntPtr options);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void AvFormatCloseInputDelegate(ref IntPtr context);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int AvFindBestStreamDelegate(IntPtr context, int mediaType, int wantedStream, int relatedStream, IntPtr decoder, int flags);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void AvDumpFormatDelegate(IntPtr context, int index, [MarshalAs(UnmanagedType.LPUTF8Str)] string url, int isOutput);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void AvLogSetCallbackDelegate(AvLogCallback callback);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void AvLogSetLevelDelegate(int level);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int AvLogFormatLine2Delegate(IntPtr avcl, int level, IntPtr format, IntPtr args, IntPtr line, int lineSize, ref int printPrefix);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int AvStrErrorDelegate(int errorCode, IntPtr errorBuffer, UIntPtr errorBufferSize);
|
||||
}
|
||||
Reference in New Issue
Block a user