Working version

This commit is contained in:
2026-08-11 12:34:02 +02:00
parent 438f163630
commit ad205bb534
12 changed files with 1006 additions and 179 deletions
+238
View File
@@ -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);
}