using System; using System.IO; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Threading; using Age.Engine.Sys4; [ComVisible(true), Guid("0579154A-2B53-4994-B0D0-E773148EFF85"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface ISampleGrabberCB { [PreserveSig] int SampleCB(double time, IntPtr sample); [PreserveSig] int BufferCB(double time, IntPtr buffer, int length); } /// /// Windows DirectShow bridge for AGE's MPEG program-stream movie payloads. The payload is supplied by /// IAssetStore; a private temporary file only adapts those owned bytes to DirectShow's stock MPEG source. /// Decoded RGB32 samples are copied into process memory and consumed by Godot's retained compositor. /// [SupportedOSPlatform("windows")] internal sealed class DirectShowMovieDecoder : IMovieDecoder, ISampleGrabberCB { private readonly byte[] _payload; private readonly Thread _thread; private readonly ManualResetEventSlim _ready = new(false); private readonly object _frameLock = new(); private byte[]? _latestRgba; private int _width, _height; private bool _bottomUp; private volatile bool _stopping; private volatile bool _completed; private string? _error; private string? _tempPath; private object? _graphObject; private IMediaControl? _control; public bool IsCompleted => _completed; public string? Failure => _error; /// The graph's IMediaPosition stop time converted exactly as native op 0x23f does: /// seconds * 1000, truncated toward zero. Null means DirectShow supplied no usable value. public long? StopTimeMs { get; private set; } public DirectShowMovieDecoder(MoviePayload movie) { _payload = movie.Bytes; _thread = new Thread(DecodeThread) { IsBackground = true, Name = $"AGE movie {movie.Name}" }; _thread.SetApartmentState(ApartmentState.MTA); _thread.Start(); // Native 0x236 builds the graph synchronously. Preserve that initialization boundary while keeping // actual playback asynchronous; timeout becomes a deterministic load error rather than a VM hang. if (!_ready.Wait(TimeSpan.FromSeconds(10))) throw new InvalidOperationException("movie graph initialization timed out"); if (_error != null) throw new InvalidOperationException(_error); } public bool TryTakeFrame(out RgbaImage frame) { lock (_frameLock) { if (_latestRgba == null) { frame = default!; return false; } frame = new RgbaImage(_width, _height, _latestRgba); _latestRgba = null; return true; } } private void DecodeThread() { int co = CoInitializeEx(IntPtr.Zero, 0); // COINIT_MULTITHREADED; DirectShow graph and callbacks share this apartment. try { _tempPath = Path.Combine(Path.GetTempPath(), $"age-movie-{Guid.NewGuid():N}.mpg"); File.WriteAllBytes(_tempPath, _payload); _graphObject = Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_FilterGraph, throwOnError: true)!)!; var graph = (IGraphBuilder)_graphObject; var grabberObject = Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_SampleGrabber, true)!)!; var nullObject = Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_NullRenderer, true)!)!; var grabberFilter = (IBaseFilter)grabberObject; var nullFilter = (IBaseFilter)nullObject; var grabber = (ISampleGrabber)grabberObject; Check(graph.AddFilter(grabberFilter, "AGE Sample Grabber"), "add sample grabber"); Check(graph.AddFilter(nullFilter, "AGE Null Renderer"), "add null renderer"); var requested = new AMMediaType { MajorType = MEDIATYPE_Video, SubType = MEDIASUBTYPE_RGB32, FormatType = FORMAT_VideoInfo }; Check(grabber.SetMediaType(ref requested), "request RGB32 movie output"); Check(graph.AddSourceFilter(_tempPath, "AGE VFS MPEG Source", out var source), "open MPEG source"); var sourceOut = FirstPin(source, PinDirection.Output); var grabberIn = FirstPin(grabberFilter, PinDirection.Input); var grabberOut = FirstPin(grabberFilter, PinDirection.Output); var nullIn = FirstPin(nullFilter, PinDirection.Input); // Intelligent connection inserts only the MPEG splitter/video decoder needed to reach RGB32. // The splitter's audio pin remains unrendered, keeping movie audio outside this slice. Check(graph.Connect(sourceOut, grabberIn), "connect MPEG video decoder"); Check(graph.ConnectDirect(grabberOut, nullIn, IntPtr.Zero), "connect null renderer"); var connected = new AMMediaType(); Check(grabber.GetConnectedMediaType(ref connected), "query movie format"); try { if (connected.FormatPtr == IntPtr.Zero) throw new InvalidDataException("movie decoder returned no VIDEOINFOHEADER"); var vi = Marshal.PtrToStructure(connected.FormatPtr); _width = vi.BitmapInfo.Width; _bottomUp = vi.BitmapInfo.Height > 0; _height = Math.Abs(vi.BitmapInfo.Height); if (_width <= 0 || _height <= 0) throw new InvalidDataException($"invalid movie dimensions {_width}x{_height}"); } finally { FreeMediaType(ref connected); } Check(grabber.SetOneShot(false), "configure continuous samples"); Check(grabber.SetBufferSamples(false), "disable redundant frame buffering"); Check(grabber.SetCallback(this, 1), "install decoded frame callback"); _control = (IMediaControl)_graphObject; Check(_control.Run(), "start movie graph"); int stateHr = _control.GetState(5000, out int graphState); if (stateHr < 0) Check(stateHr, "wait for running movie graph"); if (graphState != 2) throw new InvalidOperationException($"movie graph entered unexpected state {graphState}"); var mediaPosition = (IMediaPosition)_graphObject; int stopHr = mediaPosition.get_StopTime(out double stopTimeSeconds); double stopTimeMilliseconds = stopTimeSeconds * 1000.0; if (stopHr >= 0 && double.IsFinite(stopTimeMilliseconds) && stopTimeMilliseconds >= int.MinValue && stopTimeMilliseconds <= int.MaxValue) StopTimeMs = (long)System.Math.Truncate(stopTimeMilliseconds); _ready.Set(); var mediaEvent = (IMediaEvent)_graphObject; while (!_stopping) { int eventHr = mediaEvent.WaitForCompletion(0, out _); if (eventHr >= 0) { _completed = true; break; } if (eventHr != E_ABORT) Check(eventHr, "poll movie completion"); Thread.Sleep(10); } } catch (Exception e) { _error = $"DirectShow MPEG decode failed: {e}"; _ready.Set(); } finally { try { _control?.Stop(); } catch { } ReleaseCom(_control); _control = null; ReleaseCom(_graphObject); _graphObject = null; if (_tempPath != null) try { File.Delete(_tempPath); } catch { } if (co >= 0) CoUninitialize(); } } public int SampleCB(double sampleTime, IntPtr sample) => 0; public int BufferCB(double sampleTime, IntPtr buffer, int length) { AcceptBgra(buffer, length); return 0; } private void AcceptBgra(IntPtr buffer, int length) { int rowBytes = checked(_width * 4); if (buffer == IntPtr.Zero || length < rowBytes * _height) return; byte[] bgra = new byte[rowBytes * _height]; Marshal.Copy(buffer, bgra, 0, bgra.Length); byte[] rgba = new byte[bgra.Length]; for (int y = 0; y < _height; y++) { int src = (_bottomUp ? _height - 1 - y : y) * rowBytes; int dst = y * rowBytes; for (int x = 0; x < _width; x++, src += 4, dst += 4) { rgba[dst] = bgra[src + 2]; rgba[dst + 1] = bgra[src + 1]; rgba[dst + 2] = bgra[src]; rgba[dst + 3] = 255; } } lock (_frameLock) _latestRgba = rgba; // newest decoded frame wins if Godot renders more slowly } public void Dispose() { _stopping = true; if (_thread.IsAlive) _thread.Join(TimeSpan.FromSeconds(3)); _ready.Dispose(); } private static IPin FirstPin(IBaseFilter filter, PinDirection direction) { Check(filter.EnumPins(out var pins), "enumerate source pins"); var one = new IPin[1]; while (pins.Next(1, one, IntPtr.Zero) == 0) { Check(one[0].QueryDirection(out var found), "query source pin direction"); if (found == direction) return one[0]; } throw new InvalidOperationException($"DirectShow source has no {direction} pin"); } private static void Check(int hr, string operation) { if (hr < 0) throw new COMException($"{operation} failed (HRESULT 0x{hr:x8})", hr); } private static void ReleaseCom(object? value) { if (value != null && Marshal.IsComObject(value)) try { Marshal.ReleaseComObject(value); } catch { } } private static void FreeMediaType(ref AMMediaType mt) { if (mt.FormatPtr != IntPtr.Zero) { Marshal.FreeCoTaskMem(mt.FormatPtr); mt.FormatPtr = IntPtr.Zero; } if (mt.UnknownPtr != IntPtr.Zero) { Marshal.Release(mt.UnknownPtr); mt.UnknownPtr = IntPtr.Zero; } } private static readonly Guid CLSID_FilterGraph = new("E436EBB3-524F-11CE-9F53-0020AF0BA770"); private static readonly Guid CLSID_SampleGrabber = new("C1F400A0-3F08-11D3-9F0B-006008039E37"); private static readonly Guid CLSID_NullRenderer = new("C1F400A4-3F08-11D3-9F0B-006008039E37"); private static readonly Guid MEDIATYPE_Video = new("73646976-0000-0010-8000-00AA00389B71"); private static readonly Guid MEDIASUBTYPE_RGB32 = new("E436EB7E-524F-11CE-9F53-0020AF0BA770"); private static readonly Guid FORMAT_VideoInfo = new("05589F80-C356-11CE-BF01-00AA0055595A"); private const int E_ABORT = unchecked((int)0x80004004); [DllImport("ole32.dll")] private static extern int CoInitializeEx(IntPtr reserved, uint coInit); [DllImport("ole32.dll")] private static extern void CoUninitialize(); private enum PinDirection { Input, Output } [StructLayout(LayoutKind.Sequential)] private struct AMMediaType { public Guid MajorType, SubType; [MarshalAs(UnmanagedType.Bool)] public bool FixedSizeSamples; [MarshalAs(UnmanagedType.Bool)] public bool TemporalCompression; public int SampleSize; public Guid FormatType; public IntPtr UnknownPtr; public int FormatSize; public IntPtr FormatPtr; } [StructLayout(LayoutKind.Sequential)] private struct DsRect { public int Left, Top, Right, Bottom; } [StructLayout(LayoutKind.Sequential)] private struct BitmapInfoHeader { public int Size, Width, Height; public short Planes, BitCount; public int Compression, ImageSize, XPelsPerMeter, YPelsPerMeter, ColorsUsed, ColorsImportant; } [StructLayout(LayoutKind.Sequential)] private struct VideoInfoHeader { public DsRect Source, Target; public int BitRate, BitErrorRate; public long AvgTimePerFrame; public BitmapInfoHeader BitmapInfo; } [ComImport, Guid("6B652FFF-11FE-4FCE-92AD-0266B5D7C78F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface ISampleGrabber { [PreserveSig] int SetOneShot([MarshalAs(UnmanagedType.Bool)] bool value); [PreserveSig] int SetMediaType(ref AMMediaType type); [PreserveSig] int GetConnectedMediaType(ref AMMediaType type); [PreserveSig] int SetBufferSamples([MarshalAs(UnmanagedType.Bool)] bool value); [PreserveSig] int GetCurrentBuffer(ref int size, IntPtr buffer); [PreserveSig] int GetCurrentSample(out IntPtr sample); [PreserveSig] int SetCallback(ISampleGrabberCB callback, int method); } [ComImport, Guid("56A868A9-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IGraphBuilder { [PreserveSig] int AddFilter(IBaseFilter filter, [MarshalAs(UnmanagedType.LPWStr)] string name); [PreserveSig] int RemoveFilter(IBaseFilter filter); [PreserveSig] int EnumFilters(out IntPtr filters); [PreserveSig] int FindFilterByName([MarshalAs(UnmanagedType.LPWStr)] string name, out IBaseFilter filter); [PreserveSig] int ConnectDirect(IPin output, IPin input, IntPtr mediaType); [PreserveSig] int Reconnect(IPin pin); [PreserveSig] int Disconnect(IPin pin); [PreserveSig] int SetDefaultSyncSource(); [PreserveSig] int Connect(IPin output, IPin input); [PreserveSig] int Render(IPin output); [PreserveSig] int RenderFile([MarshalAs(UnmanagedType.LPWStr)] string file, [MarshalAs(UnmanagedType.LPWStr)] string? playList); [PreserveSig] int AddSourceFilter([MarshalAs(UnmanagedType.LPWStr)] string file, [MarshalAs(UnmanagedType.LPWStr)] string name, out IBaseFilter filter); [PreserveSig] int SetLogFile(IntPtr file); [PreserveSig] int Abort(); [PreserveSig] int ShouldOperationContinue(); } [ComImport, Guid("56A86895-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IBaseFilter { [PreserveSig] int GetClassID(out Guid clsid); [PreserveSig] int Stop(); [PreserveSig] int Pause(); [PreserveSig] int Run(long start); [PreserveSig] int GetState(int timeout, out int state); [PreserveSig] int SetSyncSource(IntPtr clock); [PreserveSig] int GetSyncSource(out IntPtr clock); [PreserveSig] int EnumPins(out IEnumPins pins); [PreserveSig] int FindPin([MarshalAs(UnmanagedType.LPWStr)] string id, out IPin pin); [PreserveSig] int QueryFilterInfo(IntPtr info); [PreserveSig] int JoinFilterGraph(IntPtr graph, [MarshalAs(UnmanagedType.LPWStr)] string name); [PreserveSig] int QueryVendorInfo([MarshalAs(UnmanagedType.LPWStr)] out string vendor); } [ComImport, Guid("56A86892-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IEnumPins { [PreserveSig] int Next(int count, [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IPin[] pins, IntPtr fetched); [PreserveSig] int Skip(int count); [PreserveSig] int Reset(); [PreserveSig] int Clone(out IEnumPins clone); } [ComImport, Guid("56A86891-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IPin { [PreserveSig] int Connect(IPin receive, IntPtr mediaType); [PreserveSig] int ReceiveConnection(IPin connector, IntPtr mediaType); [PreserveSig] int Disconnect(); [PreserveSig] int ConnectedTo(out IPin pin); [PreserveSig] int ConnectionMediaType(IntPtr mediaType); [PreserveSig] int QueryPinInfo(IntPtr info); [PreserveSig] int QueryDirection(out PinDirection direction); [PreserveSig] int QueryId([MarshalAs(UnmanagedType.LPWStr)] out string id); [PreserveSig] int QueryAccept(IntPtr mediaType); [PreserveSig] int EnumMediaTypes(out IntPtr types); [PreserveSig] int QueryInternalConnections(IntPtr pins, ref int count); [PreserveSig] int EndOfStream(); [PreserveSig] int BeginFlush(); [PreserveSig] int EndFlush(); [PreserveSig] int NewSegment(long start, long stop, double rate); } [ComImport, Guid("56A868B1-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsDual)] private interface IMediaControl { [PreserveSig] int Run(); [PreserveSig] int Pause(); [PreserveSig] int Stop(); [PreserveSig] int GetState(int timeout, out int state); } [ComImport, Guid("56A868B2-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsDual)] private interface IMediaPosition { [PreserveSig] int get_Duration(out double seconds); [PreserveSig] int put_CurrentPosition(double seconds); [PreserveSig] int get_CurrentPosition(out double seconds); [PreserveSig] int get_StopTime(out double seconds); [PreserveSig] int put_StopTime(double seconds); [PreserveSig] int get_PrerollTime(out double seconds); [PreserveSig] int put_PrerollTime(double seconds); [PreserveSig] int put_Rate(double rate); [PreserveSig] int get_Rate(out double rate); [PreserveSig] int CanSeekForward(out int canSeekForward); [PreserveSig] int CanSeekBackward(out int canSeekBackward); } [ComImport, Guid("56A868B6-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsDual)] private interface IMediaEvent { [PreserveSig] int GetEventHandle(out IntPtr eventHandle); [PreserveSig] int GetEvent(out int eventCode, out IntPtr param1, out IntPtr param2, int timeoutMs); [PreserveSig] int WaitForCompletion(int timeoutMs, out int eventCode); [PreserveSig] int CancelDefaultHandling(int eventCode); [PreserveSig] int RestoreDefaultHandling(int eventCode); [PreserveSig] int FreeEventParams(int eventCode, IntPtr param1, IntPtr param2); } }