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}