windowcapture
исходный код / UI/EditorForm.VideoPlayer.cs

EditorForm.VideoPlayer.cs

1465 строк · 66,303 байт · модуль UI
   1using System;
   2using System.Drawing;
   3using System.Drawing.Drawing2D;
   4using System.Drawing.Imaging;
   5using System.IO;
   6using System.Runtime.InteropServices;
   7using System.Windows.Forms;
   8using WindowCapture.Helpers;
   9using WindowCapture.Models;
  10using WindowCapture.Native;
  11
  12namespace WindowCapture.UI
  13{
  14    public partial class EditorForm
  15    {
  16        // DirectShow
  17        private object dsGraph;
  18        private IMediaControl dsMedia;
  19        private IMediaPosition dsPosition;
  20        private IVideoWindow dsVideo;
  21        private IBasicAudio dsAudio;
  22        private Panel dsPanel;
  23        private bool usingWebBrowser;
  24        private Form videoOverlayForm;
  25
  26        private Control VideoPanel { get { return videoOverlayForm ?? dsPanel ?? (Control)videoOverlay; } }
  27
  28        static readonly Guid CLSID_FilterGraph = new Guid("e436ebb3-524f-11ce-9f53-0020af0ba770");
  29
  30        [DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
  31        [DllImport("user32.dll")] static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
  32        [DllImport("user32.dll")] static extern int GetWindowLong(IntPtr hWnd, int nIndex);
  33        [DllImport("user32.dll")] static extern bool UpdateLayeredWindow(IntPtr hwnd, IntPtr hdcDst, ref WinApi.POINT pptDst, ref SIZE psize, IntPtr hdcSrc, ref WinApi.POINT pptSrc, int crKey, ref BLENDFUNCTION pblend, int dwFlags);
  34        [StructLayout(LayoutKind.Sequential)] struct SIZE { public int cx, cy; }
  35        [StructLayout(LayoutKind.Sequential)] struct BLENDFUNCTION { public byte BlendOp, BlendFlags, SourceConstantAlpha, AlphaFormat; }
  36
  37        private int nativeVideoW, nativeVideoH;
  38
  39        private void FitWindowToVideo()
  40        {
  41            if (nativeVideoW <= 0 || nativeVideoH <= 0 || isAudioFile) return;
  42            if (WindowState == FormWindowState.Maximized) return; // don't resize in fullscreen
  43
  44            var wa = Screen.FromControl(this).WorkingArea;
  45            int maxW = wa.Width - 20;
  46            int maxH = wa.Height - 20;
  47
  48            int w = nativeVideoW;
  49            int h = nativeVideoH;
  50
  51            // If video exceeds screen, scale down preserving aspect ratio
  52            if (w > maxW || h > maxH)
  53            {
  54                float scale = Math.Min((float)maxW / w, (float)maxH / h);
  55                w = (int)(w * scale);
  56                h = (int)(h * scale);
  57            }
  58
  59            // Apply with minimum size
  60            w = Math.Max(400, w);
  61            h = Math.Max(300, h);
  62
  63            Size = new Size(w, h);
  64            // Center on screen after resize
  65            var scr = Screen.FromControl(this).WorkingArea;
  66            Location = new Point(scr.X + (scr.Width - w) / 2, scr.Y + (scr.Height - h) / 2);
  67        }
  68
  69        private void SetupVideoPlayer(string videoPath)
  70        {
  71            if (canvas != null) canvas.Visible = false;
  72
  73            dsPanel = new Panel();
  74            dsPanel.Dock = DockStyle.Fill;
  75            dsPanel.BackColor = Color.Black;
  76            scrollContainer.Controls.Add(dsPanel);
  77
  78            videoBrowser = null;
  79            videoOverlay = null;
  80
  81            dsPanel.MouseClick += (s, me) => { if (me.Button == MouseButtons.Left) DsToggle(); };
  82            dsPanel.MouseWheel += (s, me) => AdjustVideoVolume(me.Delta > 0 ? 0.05f : -0.05f);
  83            dsPanel.Resize += (s, ev) => { DsResizeVideo(); UpdateOverlayBitmap(); };
  84
  85            NavigateVideo(videoPath);
  86
  87            // Layered overlay window (per-pixel alpha transparency)
  88            videoOverlayForm = new Form();
  89            videoOverlayForm.FormBorderStyle = FormBorderStyle.None;
  90            videoOverlayForm.ShowInTaskbar = false;
  91            videoOverlayForm.StartPosition = FormStartPosition.Manual;
  92            // Set WS_EX_LAYERED (for per-pixel alpha) but NOT WS_EX_TRANSPARENT (we handle clicks)
  93            videoOverlayForm.Load += (s, ev) =>
  94            {
  95                int exStyle = GetWindowLong(videoOverlayForm.Handle, -20);
  96                SetWindowLong(videoOverlayForm.Handle, -20, exStyle | 0x80000); // WS_EX_LAYERED only
  97            };
  98            videoOverlayForm.MouseMove += OverlayForm_MouseMove;
  99            videoOverlayForm.MouseDown += OverlayForm_MouseDown;
 100            videoOverlayForm.MouseUp += OverlayForm_MouseUp;
 101            videoOverlayForm.MouseWheel += (s, me) => AdjustVideoVolume(me.Delta > 0 ? 0.05f : -0.05f);
 102
 103            // Keep overlay in sync when main form moves/resizes
 104            Move += (s, ev) => SyncOverlayFormPosition();
 105            Resize += (s, ev) => { SyncOverlayFormPosition(); UpdateOverlayBitmap(); };
 106
 107            videoUpdateTimer = new System.Windows.Forms.Timer { Interval = 16 }; // ~60fps overlay
 108            videoUpdateTimer.Tick += VideoUpdateTimer_Tick;
 109            videoUpdateTimer.Start();
 110
 111            videoControlsFadeTimer = new System.Windows.Forms.Timer { Interval = 33 }; // ~30fps for fade
 112            videoControlsFadeTimer.Tick += VideoControlsFadeTimer_Tick;
 113            videoLastMouseMove = DateTime.Now;
 114
 115            if (glassLeaveTimer != null) glassLeaveTimer.Start();
 116            videoPlaying = true;
 117            videoControlsAlpha = 0f;
 118            videoControlsTarget = 1f;
 119            videoControlsFadeTimer.Start(); // Animate in on open
 120            lastVideoUpdateTime = DateTime.Now;
 121
 122            // Show overlay
 123            SyncOverlayFormPosition();
 124            videoOverlayForm.Show(this);
 125            UpdateOverlayBitmap();
 126        }
 127
 128        private void SyncOverlayFormPosition()
 129        {
 130            if (videoOverlayForm == null || dsPanel == null || !dsPanel.IsHandleCreated) return;
 131            try
 132            {
 133                var screenPos = dsPanel.PointToScreen(Point.Empty);
 134                videoOverlayForm.SetBounds(screenPos.X, screenPos.Y, dsPanel.Width, dsPanel.Height);
 135            }
 136            catch { }
 137        }
 138
 139        // Cached overlay: DIB section + DC persisted across frames to avoid GDI churn
 140        private Bitmap overlayBmp;
 141        private int overlayBmpW, overlayBmpH;
 142        private IntPtr cachedDc = IntPtr.Zero;
 143        private IntPtr cachedDib = IntPtr.Zero;
 144        private IntPtr cachedOldBmp = IntPtr.Zero;
 145        private IntPtr cachedScreenDc = IntPtr.Zero;
 146
 147        [DllImport("gdi32.dll")]
 148        private static extern IntPtr CreateDIBSection(IntPtr hdc, ref BITMAPINFO bmi, int usage,
 149            out IntPtr ppvBits, IntPtr hSection, int offset);
 150
 151        [StructLayout(LayoutKind.Sequential)]
 152        private struct BITMAPINFO
 153        {
 154            public int biSize, biWidth, biHeight;
 155            public short biPlanes, biBitCount;
 156            public int biCompression, biSizeImage, biXPelsPerMeter, biYPelsPerMeter, biClrUsed, biClrImportant;
 157        }
 158
 159        private void EnsureOverlayDC(int w, int h)
 160        {
 161            if (cachedDc != IntPtr.Zero && overlayBmpW == w && overlayBmpH == h) return;
 162            FreeOverlayDC();
 163
 164            cachedScreenDc = WinApi.GetDC(IntPtr.Zero);
 165            cachedDc = WinApi.CreateCompatibleDC(cachedScreenDc);
 166
 167            var bmi = new BITMAPINFO();
 168            bmi.biSize = 40; bmi.biWidth = w; bmi.biHeight = -h; // top-down
 169            bmi.biPlanes = 1; bmi.biBitCount = 32; bmi.biCompression = 0;
 170
 171            IntPtr bits;
 172            cachedDib = CreateDIBSection(cachedDc, ref bmi, 0, out bits, IntPtr.Zero, 0);
 173            cachedOldBmp = WinApi.SelectObject(cachedDc, cachedDib);
 174
 175            // Create managed Bitmap wrapping the DIB bits (zero-copy)
 176            if (overlayBmp != null) overlayBmp.Dispose();
 177            overlayBmp = new Bitmap(w, h, w * 4, PixelFormat.Format32bppPArgb, bits);
 178            overlayBmpW = w;
 179            overlayBmpH = h;
 180        }
 181
 182        private void FreeOverlayDC()
 183        {
 184            if (overlayBmp != null) { overlayBmp.Dispose(); overlayBmp = null; }
 185            if (cachedDc != IntPtr.Zero && cachedOldBmp != IntPtr.Zero)
 186                WinApi.SelectObject(cachedDc, cachedOldBmp);
 187            if (cachedDib != IntPtr.Zero) WinApi.DeleteObject(cachedDib);
 188            if (cachedDc != IntPtr.Zero) WinApi.DeleteDC(cachedDc);
 189            if (cachedScreenDc != IntPtr.Zero) WinApi.ReleaseDC(IntPtr.Zero, cachedScreenDc);
 190            cachedDc = cachedDib = cachedOldBmp = cachedScreenDc = IntPtr.Zero;
 191        }
 192
 193        /// <summary>Render UI and apply via UpdateLayeredWindow. Uses cached DIB+DC for zero-copy.</summary>
 194        private void UpdateOverlayBitmap()
 195        {
 196            if (videoOverlayForm == null || !videoOverlayForm.IsHandleCreated) return;
 197            int w = dsPanel != null ? dsPanel.Width : 800;
 198            int h = dsPanel != null ? dsPanel.Height : 600;
 199            if (w < 1 || h < 1) return;
 200
 201            EnsureOverlayDC(w, h);
 202
 203            using (var g = Graphics.FromImage(overlayBmp))
 204            {
 205                g.SmoothingMode = SmoothingMode.AntiAlias;
 206                g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
 207                g.Clear(Color.FromArgb(1, 0, 0, 0));
 208
 209                PaintGlassButtons(g);
 210                PaintVideoControls(g);
 211                PaintFileNameIndicator(g);
 212                PaintVolumeIndicator(g);
 213            }
 214
 215            // Apply — no GetHbitmap, no CreateCompatibleDC, just one call
 216            var screenPos = dsPanel.PointToScreen(Point.Empty);
 217            var ptDst = new WinApi.POINT(screenPos.X, screenPos.Y);
 218            var ptSrc = new WinApi.POINT(0, 0);
 219            var size = new SIZE { cx = w, cy = h };
 220            var blend = new BLENDFUNCTION { BlendOp = 0, BlendFlags = 0, SourceConstantAlpha = 255, AlphaFormat = 1 };
 221
 222            UpdateLayeredWindow(videoOverlayForm.Handle, cachedScreenDc, ref ptDst, ref size,
 223                cachedDc, ref ptSrc, 0, ref blend, 2);
 224        }
 225
 226        // EVR (Enhanced Video Renderer) — GPU-accelerated via DXVA2
 227        private static readonly Guid CLSID_EVR = new Guid("fa10746c-9b63-4b6c-bc49-fc300ea5f256");
 228        private static readonly Guid MR_VIDEO_RENDER_SERVICE = new Guid("1092a86c-ab1a-459a-a336-831fbc4d11ff");
 229        private static readonly Guid IID_IMFVideoDisplayControl = new Guid("a490b1e4-ab84-4d31-a1b2-181e03b1077a");
 230        private static readonly Guid IID_IBaseFilter = new Guid("56a86895-0ad4-11ce-b03a-0020af0ba770");
 231        private IMFVideoDisplayControl evrDisplay;
 232
 233        [DllImport("ole32.dll")]
 234        private static extern int CoMarshalInterThreadInterfaceInStream(ref Guid riid,
 235            [MarshalAs(UnmanagedType.IUnknown)] object pUnk, out IntPtr ppStm);
 236
 237        [DllImport("ole32.dll")]
 238        private static extern int CoGetInterfaceAndReleaseStream(IntPtr pStm,
 239            ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv);
 240
 241        private static void VLog(string msg)
 242        {
 243            try
 244            {
 245                string dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WindowCapture");
 246                if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
 247                File.AppendAllText(Path.Combine(dir, "videoplayer.log"),
 248                    DateTime.Now.ToString("HH:mm:ss.fff") + "  " + msg + "\r\n");
 249            }
 250            catch { }
 251        }
 252
 253        /// <summary>
 254        /// Create EVR on MTA thread and marshal IBaseFilter proxy back to STA via stream.
 255        /// This avoids the STA/MTA deadlock in AddFilter.
 256        /// </summary>
 257        private IBaseFilter CreateEvrProxy(IntPtr panelHandle)
 258        {
 259            IntPtr stream = IntPtr.Zero;
 260            IMFVideoDisplayControl dispResult = null;
 261            Exception mtaEx = null;
 262
 263            var thread = new System.Threading.Thread(() =>
 264            {
 265                try
 266                {
 267                    WinApi.CoInitializeEx(IntPtr.Zero, WinApi.COINIT_MULTITHREADED);
 268                    WinApi.MFStartup(WinApi.MF_VERSION, 0);
 269
 270                    var evrType = Type.GetTypeFromCLSID(CLSID_EVR);
 271                    var evr = Activator.CreateInstance(evrType);
 272                    VLog("[MTA] EVR created");
 273
 274                    // Configure EVR window BEFORE marshaling
 275                    var svc = (IMFGetService)evr;
 276                    object dispObj;
 277                    int svcHr = svc.GetService(MR_VIDEO_RENDER_SERVICE, IID_IMFVideoDisplayControl, out dispObj);
 278                    VLog("[MTA] GetService hr=0x" + svcHr.ToString("X8"));
 279                    if (svcHr >= 0 && dispObj != null)
 280                    {
 281                        dispResult = (IMFVideoDisplayControl)dispObj;
 282                        dispResult.SetVideoWindow(panelHandle);
 283                        dispResult.SetAspectRatioMode(1);
 284                        VLog("[MTA] EVR window configured");
 285                    }
 286
 287                    // Marshal IBaseFilter to stream for STA consumption
 288                    Guid iid = IID_IBaseFilter;
 289                    int mhr = CoMarshalInterThreadInterfaceInStream(ref iid, evr, out stream);
 290                    VLog("[MTA] CoMarshal hr=0x" + mhr.ToString("X8") + " stream=" + (stream != IntPtr.Zero));
 291                }
 292                catch (Exception ex) { mtaEx = ex; VLog("[MTA] EXCEPTION: " + ex.Message); }
 293            });
 294            thread.SetApartmentState(System.Threading.ApartmentState.MTA);
 295            thread.IsBackground = true;
 296            thread.Start();
 297            thread.Join(5000);
 298
 299            if (stream == IntPtr.Zero)
 300            {
 301                VLog("EVR marshal failed" + (mtaEx != null ? ": " + mtaEx.Message : ""));
 302                return null;
 303            }
 304
 305            // Unmarshal on STA thread — gives us a proxy that handles apartment transition
 306            Guid iidBf = IID_IBaseFilter;
 307            object proxyObj;
 308            int uhr = CoGetInterfaceAndReleaseStream(stream, ref iidBf, out proxyObj);
 309            VLog("[STA] CoGetInterfaceAndReleaseStream hr=0x" + uhr.ToString("X8"));
 310            if (uhr < 0 || proxyObj == null) return null;
 311
 312            evrDisplay = dispResult;
 313            return (IBaseFilter)proxyObj;
 314        }
 315
 316        private void NavigateVideo(string videoPath)
 317        {
 318            DsCleanup();
 319            nativeVideoW = 0;
 320            nativeVideoH = 0;
 321            string fullPath = Path.GetFullPath(videoPath);
 322            bool dsOk = false;
 323            VLog("=== NavigateVideo: " + fullPath + " isAudio=" + isAudioFile);
 324
 325            try
 326            {
 327                var type = Type.GetTypeFromCLSID(CLSID_FilterGraph);
 328                dsGraph = Activator.CreateInstance(type);
 329                var gb = (IGraphBuilder)dsGraph;
 330
 331                // Try EVR via cross-apartment proxy
 332                IBaseFilter evrProxy = null;
 333                if (!isAudioFile)
 334                {
 335                    try { evrProxy = CreateEvrProxy(dsPanel.Handle); }
 336                    catch (Exception ex) { VLog("CreateEvrProxy EXCEPTION: " + ex.Message); evrProxy = null; evrDisplay = null; }
 337
 338                    if (evrProxy != null)
 339                    {
 340                        int addHr = gb.AddFilter(evrProxy, "EVR");
 341                        VLog("[STA] AddFilter(EVR proxy) hr=0x" + addHr.ToString("X8"));
 342                        if (addHr < 0) { evrProxy = null; evrDisplay = null; }
 343                    }
 344                }
 345
 346                // RenderFile
 347                int hr = gb.RenderFile(fullPath, null);
 348                VLog("RenderFile hr=0x" + hr.ToString("X8") + " evr=" + (evrDisplay != null));
 349
 350                // If failed with EVR, retry without
 351                if (hr < 0 && evrDisplay != null)
 352                {
 353                    VLog("Retrying without EVR...");
 354                    evrDisplay = null;
 355                    try { Marshal.ReleaseComObject(dsGraph); } catch { }
 356                    dsGraph = Activator.CreateInstance(type);
 357                    gb = (IGraphBuilder)dsGraph;
 358                    hr = gb.RenderFile(fullPath, null);
 359                    VLog("RenderFile(legacy) hr=0x" + hr.ToString("X8"));
 360                }
 361
 362                if (hr >= 0)
 363                {
 364                    dsOk = true;
 365                    dsMedia = (IMediaControl)dsGraph;
 366                    dsPosition = (IMediaPosition)dsGraph;
 367
 368                    if (!isAudioFile)
 369                    {
 370                        if (evrDisplay != null)
 371                        {
 372                            DsResizeVideo();
 373                            MFSize szVideo, szAR;
 374                            try
 375                            {
 376                                if (evrDisplay.GetNativeVideoSize(out szVideo, out szAR) == 0 && szVideo.cx > 0)
 377                                { nativeVideoW = szVideo.cx; nativeVideoH = szVideo.cy; FitWindowToVideo(); }
 378                            }
 379                            catch { }
 380                            VLog("EVR path, size=" + nativeVideoW + "x" + nativeVideoH);
 381                        }
 382                        else
 383                        {
 384                            try
 385                            {
 386                                dsVideo = (IVideoWindow)dsGraph;
 387                                dsVideo.put_Owner(dsPanel.Handle);
 388                                dsVideo.put_WindowStyle(0x40000000 | 0x10000000);
 389                                dsVideo.put_MessageDrain(dsPanel.Handle);
 390                                DsResizeVideo();
 391                            }
 392                            catch { dsVideo = null; }
 393
 394                            if (dsVideo != null)
 395                            {
 396                                int vw, vh;
 397                                if (dsVideo.GetMinIdealImageSize(out vw, out vh) == 0 && vw > 0 && vh > 0)
 398                                { nativeVideoW = vw; nativeVideoH = vh; FitWindowToVideo(); }
 399                            }
 400                            VLog("Legacy path, size=" + nativeVideoW + "x" + nativeVideoH);
 401                        }
 402                    }
 403
 404                    try { dsAudio = (IBasicAudio)dsGraph; } catch { dsAudio = null; }
 405                    double dur;
 406                    if (dsPosition.get_Duration(out dur) == 0) videoDuration = dur;
 407                    DsSetVolume();
 408                    dsMedia.Run();
 409                    VLog("Playback started, dur=" + dur + "s");
 410                }
 411                else
 412                {
 413                    Marshal.ReleaseComObject(dsGraph); dsGraph = null;
 414                    evrDisplay = null;
 415                }
 416            }
 417            catch (Exception ex)
 418            {
 419                VLog("OUTER EXCEPTION: " + ex.Message);
 420                if (dsGraph != null) { try { Marshal.ReleaseComObject(dsGraph); } catch { } dsGraph = null; }
 421                evrDisplay = null;
 422            }
 423
 424            VLog("dsOk=" + dsOk);
 425
 426            // Load waveform in background
 427            LoadWaveformAsync(fullPath);
 428
 429            // Fallback 1: IMFMediaEngine (GPU-accelerated, all codecs, large files)
 430            if (!dsOk && !isAudioFile)
 431            {
 432                try
 433                {
 434                    dsOk = TryMediaEngine(fullPath);
 435                    VLog("MediaEngine result: " + dsOk);
 436                }
 437                catch (Exception ex) { VLog("MediaEngine EXCEPTION: " + ex.Message); }
 438            }
 439
 440            // Fallback 2: WebBrowser (IE11 H.264)
 441            if (!dsOk)
 442            {
 443                try
 444                {
 445                    string exeName = Path.GetFileName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);
 446                    using (var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(
 447                        @"SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION"))
 448                        key.SetValue(exeName, 11001, Microsoft.Win32.RegistryValueKind.DWord);
 449                }
 450                catch { }
 451
 452                if (videoBrowser == null)
 453                {
 454                    videoBrowser = new WebBrowser();
 455                    videoBrowser.Dock = DockStyle.Fill;
 456                    videoBrowser.IsWebBrowserContextMenuEnabled = false;
 457                    videoBrowser.ScriptErrorsSuppressed = true;
 458                    videoBrowser.WebBrowserShortcutsEnabled = false;
 459                    videoBrowser.ScrollBarsEnabled = false;
 460                    videoBrowser.ObjectForScripting = new VideoScriptBridge(this);
 461                    dsPanel.Controls.Add(videoBrowser);
 462                }
 463
 464                // Uri class properly encodes spaces, unicode, apostrophes for file:/// URLs
 465                string fileUri = new Uri(fullPath).AbsoluteUri;
 466                string bgStyle = "background:rgb(" + Settings.BlurTintColor.R + "," + Settings.BlurTintColor.G + "," + Settings.BlurTintColor.B + ")";
 467                string html = "<!DOCTYPE html><html><head><meta http-equiv='X-UA-Compatible' content='IE=edge'>" +
 468                    "<style>*{margin:0;padding:0;overflow:hidden}body{" + bgStyle + ";cursor:default}" +
 469                    "video{width:100%;height:100%;object-fit:contain}</style></head>" +
 470                    "<body><video id='v' src='" + fileUri + "' autoplay></video>" +
 471                    "<script>var v=document.getElementById('v');" +
 472                    "function vState(){try{return v.currentTime+'|'+v.duration+'|'+v.paused+'|'+v.volume+'|'+v.muted+'|'+v.ended+'|'+v.videoWidth+'|'+v.videoHeight}catch(e){return '0|0|true|1|false|false|0|0'}}" +
 473                    "function vPlay(){v.play()}function vPause(){v.pause()}" +
 474                    "function vToggle(){if(v.paused)v.play();else v.pause()}" +
 475                    "function vSeek(t){v.currentTime=t}function vVol(val){v.volume=val}" +
 476                    "function vMute(){v.muted=!v.muted}" +
 477                    "document.oncontextmenu=function(){return false};" +
 478                    "document.ondblclick=function(e){if(e.button==0)try{window.external.OnDoubleClick()}catch(x){}};" +
 479                    "document.onmousewheel=function(e){try{window.external.OnVolumeWheel(e.wheelDelta)}catch(x){};return false};" +
 480                    "</script></body></html>";
 481                videoBrowser.DocumentText = html;
 482                usingWebBrowser = true;
 483                VLog("WebBrowser fallback active, HTML5 video loaded");
 484            }
 485        }
 486
 487        // ===== IMFMediaEngine fallback — GPU-accelerated, all codecs, large files =====
 488        private static readonly Guid CLSID_MFMediaEngineClassFactory = new Guid("b44392da-499b-446b-a4cb-005fead0e6d5");
 489        private static readonly Guid MF_MEDIA_ENGINE_CALLBACK = new Guid("c60381b8-83a4-41f8-a3d0-de05076849a9");
 490        private static readonly Guid MF_MEDIA_ENGINE_PLAYBACK_HWND = new Guid("d988879b-67c9-4d92-baa7-6eadd446039d");
 491        private IMFMediaEngine mediaEngine;
 492        private bool usingMediaEngine;
 493
 494        private class MediaEngineNotify : IMFMediaEngineNotify
 495        {
 496            public EditorForm owner;
 497            public int EventNotify(uint eventCode, IntPtr param1, uint param2)
 498            {
 499                VLog("MediaEngine event: " + eventCode);
 500                try
 501                {
 502                    owner.BeginInvoke((Action)(() =>
 503                    {
 504                        if (owner.mediaEngine == null) return;
 505                        // 10 = LOADEDMETADATA
 506                        if (eventCode == 10)
 507                        {
 508                            double dur = owner.mediaEngine.GetDuration();
 509                            if (!double.IsNaN(dur) && dur > 0) owner.videoDuration = dur;
 510                            uint vw, vh;
 511                            if (owner.mediaEngine.GetNativeVideoSize(out vw, out vh) == 0 && vw > 0)
 512                            {
 513                                owner.nativeVideoW = (int)vw;
 514                                owner.nativeVideoH = (int)vh;
 515                                owner.FitWindowToVideo();
 516                            }
 517                            VLog("MediaEngine metadata: " + vw + "x" + vh + " dur=" + dur);
 518                        }
 519                        // 14 = CANPLAY, 15 = CANPLAYTHROUGH — safe to start playback
 520                        if (eventCode == 14 || eventCode == 15)
 521                        {
 522                            if (owner.mediaEngine.IsPaused())
 523                            {
 524                                owner.mediaEngine.Play();
 525                                owner.videoPlaying = true;
 526                                VLog("MediaEngine auto-play on CANPLAY");
 527                            }
 528                        }
 529                        // 5 = ERROR
 530                        if (eventCode == 5)
 531                        {
 532                            VLog("MediaEngine ERROR event, param1=" + param1 + " param2=" + param2);
 533                        }
 534                    }));
 535                }
 536                catch { }
 537                return 0;
 538            }
 539        }
 540
 541        private Panel mediaEnginePanel; // dedicated child panel for MediaEngine rendering
 542
 543        private bool TryMediaEngine(string fullPath)
 544        {
 545            VLog("TryMediaEngine: " + fullPath);
 546            WinApi.MFStartup(WinApi.MF_VERSION, 0);
 547
 548            // Create a dedicated child panel for MediaEngine to render into
 549            // (must be below the overlay form, at the same level as dsPanel children)
 550            if (mediaEnginePanel == null)
 551            {
 552                mediaEnginePanel = new Panel();
 553                mediaEnginePanel.Dock = DockStyle.Fill;
 554                mediaEnginePanel.BackColor = System.Drawing.Color.Black;
 555                dsPanel.Controls.Add(mediaEnginePanel);
 556                mediaEnginePanel.BringToFront();
 557            }
 558            mediaEnginePanel.Visible = true;
 559
 560            IMFAttributes attrs;
 561            int hr = WinApi.MFCreateAttributes(out attrs, 2);
 562            if (hr != 0) { VLog("MFCreateAttributes failed 0x" + hr.ToString("X8")); return false; }
 563
 564            // Set callback
 565            var notify = new MediaEngineNotify { owner = this };
 566            var cbKey = MF_MEDIA_ENGINE_CALLBACK;
 567            attrs.SetUnknown(ref cbKey, notify);
 568
 569            // Set playback HWND — render into dedicated panel, not dsPanel (overlay covers dsPanel)
 570            var hwndKey = MF_MEDIA_ENGINE_PLAYBACK_HWND;
 571            attrs.SetUINT64(ref hwndKey, (ulong)mediaEnginePanel.Handle.ToInt64());
 572
 573            // Create factory
 574            IMFMediaEngineClassFactory factory;
 575            try
 576            {
 577                var factoryType = Type.GetTypeFromCLSID(CLSID_MFMediaEngineClassFactory);
 578                factory = (IMFMediaEngineClassFactory)Activator.CreateInstance(factoryType);
 579            }
 580            catch (Exception ex) { VLog("MediaEngine factory failed: " + ex.Message); return false; }
 581
 582            // Create engine
 583            IMFMediaEngine engine;
 584            hr = factory.CreateInstance(0, attrs, out engine);
 585            VLog("MediaEngine CreateInstance hr=0x" + hr.ToString("X8"));
 586            if (hr != 0 || engine == null) return false;
 587
 588            // Set source and load
 589            string fileUri = new Uri(fullPath).AbsoluteUri;
 590            hr = engine.SetSource(fileUri);
 591            VLog("MediaEngine SetSource hr=0x" + hr.ToString("X8") + " uri=" + fileUri);
 592            if (hr != 0) { engine.Shutdown(); return false; }
 593
 594            hr = engine.Load();
 595            VLog("MediaEngine Load hr=0x" + hr.ToString("X8"));
 596
 597            mediaEngine = engine;
 598            usingMediaEngine = true;
 599            videoPlaying = false; // will be set true on CANPLAY callback
 600
 601            // Ensure panel is visible and on top of other dsPanel children
 602            mediaEnginePanel.Visible = true;
 603            mediaEnginePanel.BringToFront();
 604            dsPanel.Invalidate();
 605
 606            VLog("MediaEngine panel visible=" + mediaEnginePanel.Visible + " bounds=" + mediaEnginePanel.Bounds
 607                + " dsPanel.visible=" + dsPanel.Visible + " parent=" + (mediaEnginePanel.Parent != null ? mediaEnginePanel.Parent.Name : "null"));
 608            return true;
 609        }
 610
 611        private void DsResizeVideo()
 612        {
 613            if (dsPanel == null) return;
 614            if (evrDisplay != null)
 615            {
 616                try
 617                {
 618                    var rect = new MFRect(0, 0, dsPanel.Width, dsPanel.Height);
 619                    evrDisplay.SetVideoPosition(IntPtr.Zero, ref rect);
 620                }
 621                catch { }
 622            }
 623            else if (dsVideo != null)
 624            {
 625                try { dsVideo.SetWindowPosition(0, 0, dsPanel.Width, dsPanel.Height); } catch { }
 626            }
 627        }
 628
 629        private void DsToggle()
 630        {
 631            if (usingMediaEngine && mediaEngine != null)
 632            {
 633                if (mediaEngine.IsPaused()) mediaEngine.Play(); else mediaEngine.Pause();
 634                return;
 635            }
 636            if (usingWebBrowser) { VideoJS("vToggle"); return; }
 637            if (dsMedia == null) return;
 638            int state;
 639            dsMedia.GetState(100, out state);
 640            if (state == 2) dsMedia.Pause(); else dsMedia.Run();
 641        }
 642
 643        private void DsSetVolume()
 644        {
 645            if (usingMediaEngine && mediaEngine != null)
 646            {
 647                try
 648                {
 649                    mediaEngine.SetVolume(videoMuted ? 0.0 : (double)videoVolume);
 650                    mediaEngine.SetMuted(videoMuted);
 651                }
 652                catch { }
 653            }
 654            if (dsAudio != null)
 655            {
 656                int vol = videoMuted ? -10000 : (int)((videoVolume - 1.0) * 5000);
 657                if (vol < -10000) vol = -10000;
 658                try { dsAudio.put_Volume(vol); } catch { }
 659            }
 660            if (usingWebBrowser)
 661            {
 662                float v = videoMuted ? 0f : videoVolume;
 663                VideoJS("vVol", v.ToString(System.Globalization.CultureInfo.InvariantCulture));
 664            }
 665        }
 666
 667        private void DsCleanup()
 668        {
 669            if (dsMedia != null) try { dsMedia.Stop(); } catch { }
 670            if (mediaEngine != null) { try { mediaEngine.Shutdown(); Marshal.ReleaseComObject(mediaEngine); } catch { } mediaEngine = null; }
 671            if (mediaEnginePanel != null) { mediaEnginePanel.Visible = false; }
 672            usingMediaEngine = false;
 673            if (evrDisplay != null) { try { Marshal.ReleaseComObject(evrDisplay); } catch { } evrDisplay = null; }
 674            if (dsVideo != null) { try { dsVideo.put_Visible(0); dsVideo.put_Owner(IntPtr.Zero); } catch { } }
 675            if (dsGraph != null) { Marshal.ReleaseComObject(dsGraph); dsGraph = null; }
 676            dsMedia = null; dsPosition = null; dsVideo = null; dsAudio = null;
 677            if (videoBrowser != null) { try { videoBrowser.Dispose(); } catch { } videoBrowser = null; }
 678            FreeOverlayDC();
 679            usingWebBrowser = false;
 680        }
 681
 682        private object VideoJS(string funcName, params object[] args)
 683        {
 684            // MediaEngine path
 685            if (usingMediaEngine && mediaEngine != null)
 686            {
 687                try
 688                {
 689                    switch (funcName)
 690                    {
 691                        case "vPlay": mediaEngine.Play(); break;
 692                        case "vPause": mediaEngine.Pause(); break;
 693                        case "vToggle": DsToggle(); break;
 694                        case "vSeek":
 695                            if (args.Length > 0) { double t; if (double.TryParse(args[0].ToString(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out t)) mediaEngine.SetCurrentTime(t); }
 696                            break;
 697                        case "vVol":
 698                            if (args.Length > 0) { double v; if (double.TryParse(args[0].ToString(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v)) { videoVolume = (float)v; mediaEngine.SetVolume(v); } }
 699                            break;
 700                        case "vMute": videoMuted = !videoMuted; mediaEngine.SetMuted(videoMuted); break;
 701                    }
 702                }
 703                catch { }
 704                return null;
 705            }
 706            if (usingWebBrowser && videoBrowser != null && !videoBrowser.IsDisposed)
 707            {
 708                try
 709                {
 710                    if (videoBrowser.Document == null) return null;
 711                    return args.Length == 0 ? videoBrowser.Document.InvokeScript(funcName) : videoBrowser.Document.InvokeScript(funcName, args);
 712                }
 713                catch (Exception ex) { WaveLog("Waveform EXCEPTION: " + ex.Message + "\n" + ex.StackTrace); return null; }
 714            }
 715            if (dsMedia == null) return null;
 716            try
 717            {
 718                switch (funcName)
 719                {
 720                    case "vPlay": dsMedia.Run(); break;
 721                    case "vPause": dsMedia.Pause(); break;
 722                    case "vToggle": DsToggle(); break;
 723                    case "vSeek":
 724                        if (args.Length > 0 && dsPosition != null)
 725                        {
 726                            double t; if (double.TryParse(args[0].ToString(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out t))
 727                                dsPosition.put_CurrentPosition(t);
 728                        }
 729                        break;
 730                    case "vVol":
 731                        if (args.Length > 0) { double v; if (double.TryParse(args[0].ToString(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v)) { videoVolume = (float)v; DsSetVolume(); } }
 732                        break;
 733                    case "vMute": videoMuted = !videoMuted; DsSetVolume(); break;
 734                }
 735            }
 736            catch { }
 737            return null;
 738        }
 739
 740        private int pollSkipCounter; // poll JS every 3rd tick
 741        private double lastPollVideoTime;
 742        private DateTime lastPollTimestamp;
 743
 744        private void VideoUpdateTimer_Tick(object sender, EventArgs e)
 745        {
 746            if (!isVideoFile && !isAudioFile) return;
 747            if (dsMedia == null && !usingWebBrowser && !usingMediaEngine) return;
 748
 749            pollSkipCounter++;
 750            bool doPoll = (pollSkipCounter >= 3); // poll JS/DS every ~48ms, render at ~60fps
 751            if (doPoll) pollSkipCounter = 0;
 752
 753            // MediaEngine poll
 754            if (doPoll && usingMediaEngine && mediaEngine != null)
 755            {
 756                try
 757                {
 758                    double ct = mediaEngine.GetCurrentTime();
 759                    double dur = mediaEngine.GetDuration();
 760                    if (!double.IsNaN(dur) && dur > 0) videoDuration = dur;
 761                    lastPollVideoTime = ct; lastPollTimestamp = DateTime.Now;
 762                    prevVideoTime = videoCurrentTime; videoCurrentTime = ct; lastVideoUpdateTime = DateTime.Now;
 763                    videoPlaying = !mediaEngine.IsPaused();
 764                }
 765                catch { }
 766            }
 767            else if (doPoll && usingWebBrowser)
 768            {
 769                var state = VideoJS("vState") as string;
 770                if (state != null)
 771                {
 772                    var parts = state.Split('|');
 773                    if (parts.Length >= 6)
 774                    {
 775                        double ct, dur;
 776                        double.TryParse(parts[0], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out ct);
 777                        double.TryParse(parts[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out dur);
 778                        lastPollVideoTime = ct; lastPollTimestamp = DateTime.Now;
 779                        prevVideoTime = videoCurrentTime; videoCurrentTime = ct; lastVideoUpdateTime = DateTime.Now;
 780                        if (!double.IsNaN(dur) && dur > 0) videoDuration = dur;
 781                        videoPlaying = (parts[2] != "true"); videoMuted = (parts[4] == "true");
 782
 783                        // Get native video dimensions (once)
 784                        if (nativeVideoW == 0 && parts.Length >= 8)
 785                        {
 786                            int vw, vh;
 787                            if (int.TryParse(parts[6], out vw) && int.TryParse(parts[7], out vh) && vw > 0 && vh > 0)
 788                            {
 789                                nativeVideoW = vw;
 790                                nativeVideoH = vh;
 791                                FitWindowToVideo();
 792                            }
 793                        }
 794                    }
 795                }
 796            }
 797            else if (doPoll && dsPosition != null)
 798            {
 799                double ct;
 800                if (dsPosition.get_CurrentPosition(out ct) == 0)
 801                {
 802                    lastPollVideoTime = ct; lastPollTimestamp = DateTime.Now;
 803                    prevVideoTime = videoCurrentTime; videoCurrentTime = ct; lastVideoUpdateTime = DateTime.Now;
 804                }
 805                int state; dsMedia.GetState(0, out state); videoPlaying = (state == 2);
 806            }
 807
 808            // Smooth interpolation of seek bar between polls
 809            if (!rmbSeeking && !ovlSeeking && videoDuration > 0)
 810            {
 811                if (videoPlaying && lastPollTimestamp != DateTime.MinValue)
 812                {
 813                    double elapsed = (DateTime.Now - lastPollTimestamp).TotalSeconds;
 814                    double interpTime = lastPollVideoTime + elapsed;
 815                    if (interpTime > videoDuration) interpTime = videoDuration;
 816                    seekBarProgress = (float)(interpTime / videoDuration);
 817                }
 818                else
 819                {
 820                    seekBarProgress = (float)(videoCurrentTime / videoDuration);
 821                }
 822            }
 823
 824            // Glow + overlay update
 825            if (dsPanel != null && dsPanel.IsHandleCreated)
 826            {
 827                var scPt = scrollContainer.PointToClient(Cursor.Position);
 828                UpdateGlassButtons(scPt);
 829
 830                // Detect cursor movement (anywhere in window)
 831                Point curScreen = Cursor.Position;
 832                bool cursorInWindow = scrollContainer.ClientRectangle.Contains(scPt);
 833                if (cursorInWindow && (curScreen.X != lastCursorPos.X || curScreen.Y != lastCursorPos.Y))
 834                {
 835                    videoLastMouseMove = DateTime.Now;
 836                    videoControlsTarget = 1f;
 837                    if (videoControlsFadeTimer != null && !videoControlsFadeTimer.Enabled)
 838                        videoControlsFadeTimer.Start();
 839                }
 840                lastCursorPos = curScreen;
 841
 842                SyncOverlayFormPosition();
 843                // Skip heavy bitmap update when nothing visible
 844                bool hasVisibleUI = videoControlsAlpha > 0.01f || volumeIndicatorAlpha > 0.01f
 845                    || fileNameAlpha > 0.01f;
 846                bool hasGlow = false;
 847                if (hoverBtnGlow != null) foreach (float gl in hoverBtnGlow) if (gl > 0.01f) { hasGlow = true; break; }
 848                if (hasVisibleUI || hasGlow)
 849                    UpdateOverlayBitmap();
 850            }
 851        }
 852
 853        private float videoControlsTarget = 1f; // 1 = visible, 0 = hidden
 854        private Point lastCursorPos;
 855
 856        /// <summary>Smooth Bezier-style ease for control bar fade.</summary>
 857        private static float BezierEase(float t)
 858        {
 859            // Cubic ease-in-out: smooth S-curve
 860            return t * t * (3f - 2f * t);
 861        }
 862
 863        private void VideoControlsFadeTimer_Tick(object sender, EventArgs e)
 864        {
 865            double elapsed = (DateTime.Now - videoLastMouseMove).TotalMilliseconds;
 866
 867            // Decide target: keep visible while interacting or cursor recently moved
 868            if (videoControlBarHovered || ovlSeeking || ovlVolSeeking || ovlDragging)
 869                videoControlsTarget = 1f;
 870            else if (elapsed > 2500)
 871                videoControlsTarget = 0f;
 872
 873            // Animate toward target
 874            float diff = videoControlsTarget - videoControlsAlpha;
 875            if (Math.Abs(diff) < 0.005f)
 876            {
 877                videoControlsAlpha = videoControlsTarget;
 878                // Only stop timer when fully hidden — keep running while visible to detect timeout
 879                if (videoControlsAlpha <= 0f)
 880                    videoControlsFadeTimer.Stop();
 881            }
 882            else if (diff > 0)
 883            {
 884                videoControlsAlpha += 0.12f;
 885                if (videoControlsAlpha > 1f) videoControlsAlpha = 1f;
 886            }
 887            else
 888            {
 889                videoControlsAlpha -= 0.035f;
 890                if (videoControlsAlpha < 0f) videoControlsAlpha = 0f;
 891            }
 892
 893            UpdateOverlayBitmap();
 894        }
 895
 896        // === Overlay Form mouse handlers ===
 897
 898        private bool ovlSeeking, ovlVolSeeking, ovlLmbDown;
 899        private int ovlLmbStartX, ovlLmbStartY;
 900        private bool ovlDragging; // window move in progress
 901        private Point ovlDragFormStart; // form location when drag started
 902        private Point ovlDragMouseStart; // screen mouse pos when drag started
 903        private DateTime lastOvlSeekTime = DateTime.MinValue;
 904        private const int VideoResizeEdge = 7; // pixels from edge for resize zones
 905
 906        /// <summary>Returns HTXXX constant for edge resize, or 0 if not on edge.</summary>
 907        private int HitTestVideoEdge(int x, int y)
 908        {
 909            if (WindowState == FormWindowState.Maximized) return 0;
 910            int w = videoOverlayForm.Width, h = videoOverlayForm.Height;
 911            int e = VideoResizeEdge;
 912            bool left = x < e, right = x >= w - e, top = y < e, bottom = y >= h - e;
 913            if (top && left) return 13;     // HTTOPLEFT
 914            if (top && right) return 14;    // HTTOPRIGHT
 915            if (bottom && left) return 16;  // HTBOTTOMLEFT
 916            if (bottom && right) return 17; // HTBOTTOMRIGHT
 917            if (left) return 10;            // HTLEFT
 918            if (right) return 11;           // HTRIGHT
 919            if (top) return 12;             // HTTOP
 920            if (bottom) return 15;          // HTBOTTOM
 921            return 0;
 922        }
 923
 924        private Cursor EdgeCursor(int ht)
 925        {
 926            switch (ht)
 927            {
 928                case 10: case 11: return Cursors.SizeWE;
 929                case 12: case 15: return Cursors.SizeNS;
 930                case 13: case 17: return Cursors.SizeNWSE;
 931                case 14: case 16: return Cursors.SizeNESW;
 932                default: return Cursors.Default;
 933            }
 934        }
 935
 936        private void OverlayForm_MouseMove(object sender, MouseEventArgs me)
 937        {
 938            videoLastMouseMove = DateTime.Now;
 939            int h = videoOverlayForm.Height;
 940            int barY = h - VideoControlBarH;
 941            videoControlBarHovered = (me.Y >= barY);
 942
 943            var scPt = scrollContainer.PointToClient(videoOverlayForm.PointToScreen(me.Location));
 944            UpdateGlassButtons(scPt);
 945
 946            if (ovlSeeking && videoDuration > 0)
 947            {
 948                var seekRect = GetVideoSeekBarRect(videoOverlayForm);
 949                float p = (float)(me.X - seekRect.X) / seekRect.Width;
 950                seekBarProgress = Math.Max(0f, Math.Min(1f, p));
 951                if ((DateTime.Now - lastOvlSeekTime).TotalMilliseconds > 40)
 952                {
 953                    VideoJS("vSeek", (seekBarProgress * videoDuration).ToString(System.Globalization.CultureInfo.InvariantCulture));
 954                    lastOvlSeekTime = DateTime.Now;
 955                }
 956            }
 957            if (ovlVolSeeking)
 958            {
 959                var volRect = GetVideoVolBarRect(videoOverlayForm);
 960                float p = (float)(me.X - volRect.X) / volRect.Width;
 961                videoVolume = Math.Max(0f, Math.Min(1f, p));
 962                DsSetVolume();
 963            }
 964
 965            // Manual window drag using screen coordinates (avoids layered window capture issues)
 966            if (ovlDragging)
 967            {
 968                Point screenNow = Cursor.Position;
 969                int dx = screenNow.X - ovlDragMouseStart.X;
 970                int dy = screenNow.Y - ovlDragMouseStart.Y;
 971                Location = new Point(ovlDragFormStart.X + dx, ovlDragFormStart.Y + dy);
 972                SyncOverlayFormPosition();
 973                return;
 974            }
 975            if (ovlLmbDown && !ovlSeeking && !ovlVolSeeking)
 976            {
 977                Point screenNow = Cursor.Position;
 978                int dx = screenNow.X - ovlDragMouseStart.X;
 979                int dy = screenNow.Y - ovlDragMouseStart.Y;
 980                if (dx * dx + dy * dy > 16) // 4px threshold
 981                {
 982                    ovlDragging = true;
 983                    videoOverlayForm.Capture = true;
 984                }
 985            }
 986
 987            // Any mouse movement on overlay triggers controls show
 988            videoLastMouseMove = DateTime.Now;
 989            videoControlsTarget = 1f;
 990            if (videoControlsFadeTimer != null && !videoControlsFadeTimer.Enabled)
 991                videoControlsFadeTimer.Start();
 992
 993            // Cursor shape: edge resize > control bar > glass buttons
 994            int edgeHt = HitTestVideoEdge(me.X, me.Y);
 995            if (edgeHt != 0)
 996                videoOverlayForm.Cursor = EdgeCursor(edgeHt);
 997            else if (videoControlBarHovered || ovlSeeking || ovlVolSeeking || HitTestGlassButton(scPt) >= 0)
 998                videoOverlayForm.Cursor = Cursors.Hand;
 999            else
1000                videoOverlayForm.Cursor = Cursors.Default;
1001
1002            // Redraw only when actively interacting (seek/volume drag); timer handles periodic updates
1003            if (ovlSeeking || ovlVolSeeking)
1004                UpdateOverlayBitmap();
1005        }
1006
1007        private void OverlayForm_MouseDown(object sender, MouseEventArgs me)
1008        {
1009            videoLastMouseMove = DateTime.Now;
1010            if (me.Button == MouseButtons.Left)
1011            {
1012                // Edge resize (highest priority)
1013                int edgeHt = HitTestVideoEdge(me.X, me.Y);
1014                if (edgeHt != 0)
1015                {
1016                    WinApi.ReleaseCapture();
1017                    WinApi.SendMessage(Handle, 0xA1, edgeHt, 0); // WM_NCLBUTTONDOWN
1018                    return;
1019                }
1020
1021                var scPt = scrollContainer.PointToClient(videoOverlayForm.PointToScreen(me.Location));
1022                int btnIdx = HitTestGlassButton(scPt);
1023                if (btnIdx >= 0 && hoverBtnGlow[btnIdx] > 0.15f)
1024                {
1025                    hoverBtnClickIndex = btnIdx;
1026                    return;
1027                }
1028
1029                int h = videoOverlayForm.Height;
1030                int barY = h - VideoControlBarH;
1031                if (me.Y >= barY)
1032                {
1033                    var seekRect = GetVideoSeekBarRect(videoOverlayForm); seekRect.Inflate(0, 10);
1034                    if (seekRect.Contains(me.Location)) { ovlSeeking = true; return; }
1035                    var volRect = GetVideoVolBarRect(videoOverlayForm); volRect.Inflate(0, 10);
1036                    if (volRect.Contains(me.Location)) { ovlVolSeeking = true; return; }
1037
1038                    // Click on play button area
1039                    if (me.X < 50) { DsToggle(); return; }
1040                }
1041
1042                // LMB on video area — will become drag if moved, or toggle if released in place
1043                ovlLmbDown = true;
1044                ovlDragging = false;
1045                ovlLmbStartX = me.X;
1046                ovlLmbStartY = me.Y;
1047                ovlDragFormStart = Location;
1048                ovlDragMouseStart = Cursor.Position;
1049            }
1050        }
1051
1052        private void OverlayForm_MouseUp(object sender, MouseEventArgs me)
1053        {
1054            if (me.Button == MouseButtons.Left)
1055            {
1056                if (hoverBtnClickIndex >= 0)
1057                {
1058                    int idx = hoverBtnClickIndex; hoverBtnClickIndex = -1;
1059                    var scPt = scrollContainer.PointToClient(videoOverlayForm.PointToScreen(me.Location));
1060                    if (HitTestGlassButton(scPt) == idx) hoverBtnActions[idx]();
1061                    return;
1062                }
1063                if (ovlSeeking && videoDuration > 0)
1064                {
1065                    var seekRect = GetVideoSeekBarRect(videoOverlayForm);
1066                    float p = (float)(me.X - seekRect.X) / seekRect.Width;
1067                    seekBarProgress = Math.Max(0f, Math.Min(1f, p));
1068                    VideoJS("vSeek", (seekBarProgress * videoDuration).ToString(System.Globalization.CultureInfo.InvariantCulture));
1069                }
1070                if (!ovlSeeking && !ovlVolSeeking && !ovlDragging && ovlLmbDown && !isAnimatedGif)
1071                    DsToggle();
1072
1073                if (ovlDragging) videoOverlayForm.Capture = false;
1074                ovlSeeking = false; ovlVolSeeking = false; ovlLmbDown = false; ovlDragging = false;
1075            }
1076        }
1077
1078        public void AdjustVideoVolume(float delta)
1079        {
1080            videoVolume = Math.Max(0f, Math.Min(1f, videoVolume + delta));
1081            DsSetVolume();
1082            if (videoMuted && videoVolume > 0) { videoMuted = false; DsSetVolume(); }
1083            volumeIndicatorAlpha = 1f; volumeLastChangeTime = DateTime.Now;
1084            UpdateOverlayBitmap();
1085        }
1086
1087        public void ToggleVideoPlayback() { DsToggle(); }
1088
1089        public void StartRmbSeek(int browserX)
1090        { if (videoDuration <= 0) return; rmbSeeking = true; rmbSeekStartX = browserX; rmbSeekStartTime = videoCurrentTime; }
1091
1092        public void UpdateRmbSeek(int browserX)
1093        {
1094            if (!rmbSeeking || videoDuration <= 0) return;
1095            int browserW = dsPanel != null ? dsPanel.Width : 800;
1096            double newTime = Math.Max(0, Math.Min(videoDuration, rmbSeekStartTime + (browserX - rmbSeekStartX) * videoDuration / Math.Max(400, browserW)));
1097            seekBarProgress = (float)(newTime / videoDuration);
1098            if (dsPosition != null) dsPosition.put_CurrentPosition(newTime);
1099            UpdateOverlayBitmap();
1100        }
1101
1102        public void EndRmbSeek() { rmbSeeking = false; }
1103
1104        /// <summary>Fallback: build visual waveform from raw file bytes when MF fails.</summary>
1105        private static float[] ExtractWaveformRaw(string path, int numPeaks)
1106        {
1107            try
1108            {
1109                using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
1110                {
1111                    long len = fs.Length;
1112                    if (len < 4096) return null;
1113
1114                    // Skip header, read middle portion (likely contains interleaved audio data)
1115                    long skip = Math.Min(len / 10, 65536);
1116                    long bodyLen = len - skip * 2; // skip head and tail
1117                    if (bodyLen < 4096) bodyLen = len - skip;
1118                    fs.Seek(skip, SeekOrigin.Begin);
1119
1120                    int readLen = (int)Math.Min(bodyLen, 8 * 1024 * 1024);
1121                    byte[] data = new byte[readLen];
1122                    int bytesRead = fs.Read(data, 0, readLen);
1123
1124                    int chunkSize = Math.Max(2, bytesRead / numPeaks);
1125                    float[] result = new float[numPeaks];
1126                    float globalMax = 0;
1127
1128                    for (int i = 0; i < numPeaks; i++)
1129                    {
1130                        int start = i * chunkSize;
1131                        int end = Math.Min(start + chunkSize, bytesRead);
1132                        if (start >= bytesRead) break;
1133
1134                        float maxAbs = 0;
1135                        for (int j = start; j < end - 1; j += 2)
1136                        {
1137                            short val = (short)(data[j] | (data[j + 1] << 8));
1138                            float abs = Math.Abs(val / 32768f);
1139                            if (abs > maxAbs) maxAbs = abs;
1140                        }
1141                        result[i] = maxAbs;
1142                        if (maxAbs > globalMax) globalMax = maxAbs;
1143                    }
1144
1145                    if (globalMax > 0.01f)
1146                        for (int i = 0; i < numPeaks; i++)
1147                            result[i] = Math.Min(1f, result[i] / globalMax);
1148
1149                    return result;
1150                }
1151            }
1152            catch { return null; }
1153        }
1154
1155        /// <summary>Extract audio waveform peaks from video/audio file using Media Foundation.</summary>
1156        private void LoadWaveformAsync(string path)
1157        {
1158            if (path == waveformLoadedPath) return;
1159            waveformLoadedPath = path;
1160            waveformPeaks = null;
1161
1162            var wfPath = path; // capture for lambda
1163            new System.Threading.Thread(() =>
1164            {
1165                try
1166                {
1167                    // COM must be initialized in this thread for MF Source Reader
1168                    WinApi.CoInitializeEx(IntPtr.Zero, WinApi.COINIT_MULTITHREADED);
1169                    try
1170                    {
1171                        var peaks = ExtractWaveformMF(wfPath, 800);
1172                        WaveLog("MF result: " + (peaks != null ? peaks.Length + " peaks" : "NULL → trying raw"));
1173                        if (peaks == null) peaks = ExtractWaveformRaw(wfPath, 800);
1174                        WaveLog("Final waveform: " + (peaks != null ? peaks.Length + " peaks" : "NULL"));
1175                        waveformPeaks = peaks;
1176                    }
1177                    finally { WinApi.CoUninitialize(); }
1178                }
1179                catch { }
1180            }) { IsBackground = true, Name = "WaveformExtract" }.Start();
1181        }
1182
1183        /// <summary>Extract waveform via MF Source Reader (same as AudioPlayerForm).</summary>
1184        private static float[] ExtractWaveformMF(string path, int numPeaks)
1185        {
1186            WinApi.MFStartup(WinApi.MF_VERSION, 0);
1187            try
1188            {
1189                IMFSourceReader reader;
1190                int hr = WinApi.MFCreateSourceReaderFromURL_IntPtr(path, IntPtr.Zero, out reader);
1191                WaveLog("MF SourceReader hr=0x" + hr.ToString("X8") + " path=" + path);
1192
1193                IMFMediaType mt;
1194                WinApi.MFCreateMediaType(out mt);
1195
1196                Guid keyMajor = WinApi.MF_MT_MAJOR_TYPE;
1197                Guid valAudio = WinApi.MFMediaType_Audio;
1198                mt.SetGUID(ref keyMajor, ref valAudio);
1199
1200                Guid keySub = WinApi.MF_MT_SUBTYPE;
1201                Guid valPCM = WinApi.MFAudioFormat_PCM;
1202                mt.SetGUID(ref keySub, ref valPCM);
1203
1204                Guid keyCh = WinApi.MF_MT_AUDIO_NUM_CHANNELS;
1205                mt.SetUINT32(ref keyCh, 1);
1206
1207                Guid keySR = WinApi.MF_MT_AUDIO_SAMPLES_PER_SECOND;
1208                mt.SetUINT32(ref keySR, 16000);
1209
1210                Guid keyBPS = WinApi.MF_MT_AUDIO_BITS_PER_SAMPLE;
1211                mt.SetUINT32(ref keyBPS, 16);
1212
1213                Guid keyBA = WinApi.MF_MT_AUDIO_BLOCK_ALIGNMENT;
1214                mt.SetUINT32(ref keyBA, 2);
1215
1216                Guid keyABPS = WinApi.MF_MT_AUDIO_AVG_BYTES_PER_SECOND;
1217                mt.SetUINT32(ref keyABPS, 32000);
1218
1219                reader.SetStreamSelection(WinApi.MF_SOURCE_READER_FIRST_AUDIO_STREAM, true);
1220                hr = reader.SetCurrentMediaType(WinApi.MF_SOURCE_READER_FIRST_AUDIO_STREAM, IntPtr.Zero, mt);
1221                WaveLog("MF SetCurrentMediaType hr=0x" + hr.ToString("X8"));
1222                if (hr != 0) { Marshal.ReleaseComObject(reader); Marshal.ReleaseComObject(mt); return null; }
1223
1224                const int blockSamples = 160;
1225                var peaks = new System.Collections.Generic.List<float>(4096);
1226                float curPeak = 0f;
1227                int curCnt = 0;
1228
1229                while (true)
1230                {
1231                    uint actual; int sFlags; long ts; IMFSample sample;
1232                    int rhr = reader.ReadSample(WinApi.MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0,
1233                        out actual, out sFlags, out ts, out sample);
1234                    if (rhr != 0 || (sFlags & WinApi.MF_SOURCE_READERF_ENDOFSTREAM) != 0) break;
1235                    if (sample == null) continue;
1236
1237                    IMFMediaBuffer buf;
1238                    sample.ConvertToContiguousBuffer(out buf);
1239                    if (buf != null)
1240                    {
1241                        IntPtr pData; int max, cur;
1242                        buf.Lock(out pData, out max, out cur);
1243                        int numShorts = cur / 2;
1244                        for (int i = 0; i < numShorts; i++)
1245                        {
1246                            short s = Marshal.ReadInt16(pData, i * 2);
1247                            float v = Math.Abs(s / 32768f);
1248                            if (v > curPeak) curPeak = v;
1249                            curCnt++;
1250                            if (curCnt >= blockSamples) { peaks.Add(curPeak); curPeak = 0f; curCnt = 0; }
1251                        }
1252                        buf.Unlock();
1253                        Marshal.ReleaseComObject(buf);
1254                    }
1255                    Marshal.ReleaseComObject(sample);
1256                }
1257                if (curCnt > 0) peaks.Add(curPeak);
1258                Marshal.ReleaseComObject(reader);
1259                Marshal.ReleaseComObject(mt);
1260
1261                WaveLog("MF waveform done: " + peaks.Count + " peaks" + (peaks.Count > 0 ? " max=" + System.Linq.Enumerable.Max(peaks).ToString("F3") : ""));
1262                if (peaks.Count == 0) return null;
1263
1264                float[] result = new float[numPeaks];
1265                for (int c = 0; c < numPeaks; c++)
1266                {
1267                    int i0 = (int)((long)c * peaks.Count / numPeaks);
1268                    int i1 = (int)((long)(c + 1) * peaks.Count / numPeaks);
1269                    if (i1 >= peaks.Count) i1 = peaks.Count - 1;
1270                    float pk = 0f;
1271                    for (int i = i0; i <= i1; i++)
1272                        if (peaks[i] > pk) pk = peaks[i];
1273                    result[c] = pk;
1274                }
1275
1276                // Normalize to 0..1 so quiet and loud videos look equally prominent
1277                float maxPk = 0f;
1278                for (int i = 0; i < result.Length; i++)
1279                    if (result[i] > maxPk) maxPk = result[i];
1280                if (maxPk > 0.001f)
1281                    for (int i = 0; i < result.Length; i++)
1282                        result[i] /= maxPk;
1283
1284                return result;
1285            }
1286            catch { return null; }
1287            finally { WinApi.MFShutdown(); }
1288        }
1289
1290        private static void WaveLog(string msg)
1291        {
1292            try { File.AppendAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "VideoLog.txt"),
1293                DateTime.Now.ToString("HH:mm:ss.fff") + " " + msg + "\r\n"); } catch { }
1294        }
1295
1296        /// <summary>
1297        /// Extract waveform via DirectShow SampleGrabber.
1298        /// Builds a separate filter graph: Source → audio decoder → SampleGrabber → NullRenderer.
1299        /// Runs graph at max speed (no clock), collects decoded PCM peaks.
1300        /// </summary>
1301        private static float[] ExtractWaveformPeaks(string path, int numPeaks)
1302        {
1303            object graphObj = null;
1304            object grabberObj = null;
1305            object nullObj = null;
1306            try
1307            {
1308                // Create filter graph
1309                var graphType = Type.GetTypeFromCLSID(CLSID_FilterGraph);
1310                graphObj = Activator.CreateInstance(graphType);
1311                var graph = (IGraphBuilder)graphObj;
1312
1313                // Create SampleGrabber filter
1314                var grabberClsid = new Guid("C1F400A0-3F08-11D3-9F0B-006008039E37");
1315                var grabberType = Type.GetTypeFromCLSID(grabberClsid);
1316                grabberObj = Activator.CreateInstance(grabberType);
1317                var grabber = (ISampleGrabber)grabberObj;
1318                var grabberFilter = (IBaseFilter)grabberObj;
1319
1320                // Set media type to PCM audio — forces SampleGrabber into audio chain
1321                var mt = new AMMediaType();
1322                mt.majorType = new Guid("73647561-0000-0010-8000-00AA00389B71"); // MEDIATYPE_Audio
1323                mt.subType = new Guid("00000001-0000-0010-8000-00AA00389B71");   // MEDIASUBTYPE_PCM
1324                grabber.SetMediaType(mt);
1325
1326                // Add SampleGrabber to graph
1327                graph.AddFilter(grabberFilter, "Grabber");
1328
1329                // Create NullRenderer for audio output (no sound played)
1330                var nullClsid = new Guid("C1F400A4-3F08-11D3-9F0B-006008039E37");
1331                var nullType = Type.GetTypeFromCLSID(nullClsid);
1332                nullObj = Activator.CreateInstance(nullType);
1333                var nullFilter = (IBaseFilter)nullObj;
1334                graph.AddFilter(nullFilter, "Null");
1335
1336                // RenderFile builds the graph — SampleGrabber (set to Audio PCM)
1337                // will be inserted into the audio chain automatically
1338                int hr = graph.RenderFile(Path.GetFullPath(path), null);
1339                if (hr < 0) return null;
1340
1341                // Remove video renderer to speed up processing (we only need audio)
1342                // Find and remove any video rendering filters
1343                try
1344                {
1345                    // Standard video renderers CLSIDs
1346                    var videoRenderers = new Guid[] {
1347                        new Guid("70e102b0-5556-11ce-97c0-00aa0055595a"), // Video Renderer
1348                        new Guid("51b4abf3-748f-4e3b-a276-c828330e926a"), // EVR
1349                        new Guid("fa10746c-9b63-4b6c-bc49-fc300ea5f256"), // VMR-9
1350                    };
1351                    // We can't easily enumerate, but stopping video doesn't matter
1352                    // The graph will just process audio faster without video decoding overhead
1353                }
1354                catch { }
1355
1356                // Enable buffering mode
1357                grabber.SetBufferSamples(true);
1358
1359                // Check what media type SampleGrabber actually connected with
1360                var connMt = new AMMediaType();
1361                grabber.GetConnectedMediaType(connMt);
1362                WaveLog("Waveform connected: major=" + connMt.majorType + " sub=" + connMt.subType + " sampleSize=" + connMt.sampleSize);
1363
1364                // Run the graph
1365                var mediaControl = (IMediaControl)graphObj;
1366                var mediaPosition = (IMediaPosition)graphObj;
1367
1368                // Get duration
1369                double duration = 0;
1370                mediaPosition.get_Duration(out duration);
1371
1372                // Set buffer sampling with callback
1373                var callback = new WaveformGrabberCB();
1374                grabber.SetCallback(callback, 1); // 1 = BufferCB
1375
1376                // Run and wait for completion
1377                mediaControl.Run();
1378
1379                // Wait for graph to finish (max 60 seconds)
1380                int state;
1381                var sw = System.Diagnostics.Stopwatch.StartNew();
1382                while (sw.ElapsedMilliseconds < 60000)
1383                {
1384                    mediaControl.GetState(100, out state);
1385                    // Check if we reached the end
1386                    double pos;
1387                    mediaPosition.get_CurrentPosition(out pos);
1388                    if (duration > 0 && pos >= duration - 0.1) break;
1389                    if (callback.Peaks.Count > 100000) break; // safety limit
1390                    System.Threading.Thread.Sleep(10);
1391                }
1392
1393                mediaControl.Stop();
1394
1395                var peaks = callback.Peaks;
1396                if (peaks.Count == 0) return null;
1397
1398                // Resample to numPeaks
1399                float[] result = new float[numPeaks];
1400                for (int c = 0; c < numPeaks; c++)
1401                {
1402                    int i0 = (int)((long)c * peaks.Count / numPeaks);
1403                    int i1 = (int)((long)(c + 1) * peaks.Count / numPeaks);
1404                    if (i1 >= peaks.Count) i1 = peaks.Count - 1;
1405                    float pk = 0f;
1406                    for (int i = i0; i <= i1; i++)
1407                        if (peaks[i] > pk) pk = peaks[i];
1408                    result[c] = pk;
1409                }
1410
1411                // Normalize to 0..1
1412                float maxPk = 0f;
1413                for (int i = 0; i < result.Length; i++)
1414                    if (result[i] > maxPk) maxPk = result[i];
1415                if (maxPk > 0.001f)
1416                    for (int i = 0; i < result.Length; i++)
1417                        result[i] /= maxPk;
1418
1419                return result;
1420            }
1421            catch { return null; }
1422            finally
1423            {
1424                if (graphObj != null)
1425                {
1426                    try { ((IMediaControl)graphObj).Stop(); } catch { }
1427                    Marshal.ReleaseComObject(graphObj);
1428                }
1429                if (grabberObj != null) Marshal.ReleaseComObject(grabberObj);
1430                if (nullObj != null) Marshal.ReleaseComObject(nullObj);
1431            }
1432        }
1433
1434        /// <summary>SampleGrabber callback that collects audio peaks.</summary>
1435        private class WaveformGrabberCB : ISampleGrabberCB
1436        {
1437            public System.Collections.Generic.List<float> Peaks = new System.Collections.Generic.List<float>(8192);
1438            private float curPeak;
1439            private int curCnt;
1440            private const int BlockSize = 800; // samples per peak
1441
1442            public int SampleCB(double sampleTime, IntPtr pSample) { return 0; }
1443
1444            public int BufferCB(double sampleTime, IntPtr pBuffer, int bufferLen)
1445            {
1446                int numShorts = bufferLen / 2;
1447                for (int i = 0; i < numShorts; i++)
1448                {
1449                    short val = Marshal.ReadInt16(pBuffer, i * 2);
1450                    float v = Math.Abs(val / 32768f);
1451                    if (v > curPeak) curPeak = v;
1452                    curCnt++;
1453                    if (curCnt >= BlockSize)
1454                    {
1455                        Peaks.Add(curPeak);
1456                        curPeak = 0;
1457                        curCnt = 0;
1458                    }
1459                }
1460                return 0;
1461            }
1462        }
1463    }
1464}