/ src / modules / ZoomIt / ZoomIt / VideoRecordingSession.cpp
VideoRecordingSession.cpp
   1  //==============================================================================
   2  //
   3  // Zoomit
   4  // Sysinternals - www.sysinternals.com
   5  //
   6  // Video capture code derived from https://github.com/robmikh/capturevideosample
   7  //
   8  //==============================================================================
   9  #include "pch.h"
  10  #include "VideoRecordingSession.h"
  11  #include "CaptureFrameWait.h"
  12  #include "Utility.h"
  13  #include <winrt/Windows.Graphics.Imaging.h>
  14  #include <winrt/Windows.Media.h>
  15  #include <cstdlib>
  16  #include <filesystem>
  17  #include <shlwapi.h>   // For SHCreateStreamOnFileEx
  18  #include <mmsystem.h>   // For timeBeginPeriod/timeEndPeriod
  19  
  20  #pragma comment(lib, "shlwapi.lib")
  21  #pragma comment(lib, "winmm.lib")
  22  
  23  extern DWORD g_RecordScaling;
  24  extern DWORD g_TrimDialogWidth;
  25  extern DWORD g_TrimDialogHeight;
  26  extern DWORD g_TrimDialogVolume;
  27  extern class ClassRegistry reg;
  28  extern REG_SETTING RegSettings[];
  29  extern HINSTANCE g_hInstance;
  30  
  31  HWND hDlgTrimDialog = nullptr;
  32  
  33  namespace winrt
  34  {
  35      using namespace Windows::Foundation;
  36      using namespace Windows::Graphics;
  37      using namespace Windows::Graphics::Capture;
  38      using namespace Windows::Graphics::DirectX;
  39      using namespace Windows::Graphics::DirectX::Direct3D11;
  40      using namespace Windows::Graphics::Imaging;
  41      using namespace Windows::Storage;
  42      using namespace Windows::UI::Composition;
  43      using namespace Windows::Media::Core;
  44      using namespace Windows::Media::Transcoding;
  45      using namespace Windows::Media::MediaProperties;
  46      using namespace Windows::Media::Editing;
  47      using namespace Windows::Media::Playback;
  48      using namespace Windows::Storage::FileProperties;
  49  }
  50  
  51  namespace util
  52  {
  53      using namespace robmikh::common::uwp;
  54  }
  55  
  56  const float CLEAR_COLOR[] = { 0.0f, 0.0f, 0.0f, 1.0f };
  57  constexpr UINT kGifDefaultDelayCs = 10;       // 100ms (~10 FPS) when metadata delay is missing
  58  constexpr UINT kGifMinDelayCs = 2;            // 20ms minimum; browsers treat <2cs as 10cs (100ms)
  59  constexpr UINT kGifBrowserFixupThreshold = 2; // Delays < this are treated as 10cs by browsers
  60  constexpr UINT kGifMaxPreviewDimension = 1280; // cap decoded GIF preview size to keep playback smooth
  61  
  62  int32_t EnsureEven(int32_t value)
  63  {
  64      if (value % 2 == 0)
  65      {
  66          return value;
  67      }
  68      else
  69      {
  70          return value + 1;
  71      }
  72  }
  73  
  74  static bool IsGifPath(const std::wstring& path)
  75  {
  76      try
  77      {
  78          const auto ext = std::filesystem::path(path).extension().wstring();
  79          return _wcsicmp(ext.c_str(), L".gif") == 0;
  80      }
  81      catch (...)
  82      {
  83          return false;
  84      }
  85  }
  86  
  87  static void CleanupGifFrames(VideoRecordingSession::TrimDialogData* pData)
  88  {
  89      if (!pData)
  90      {
  91          return;
  92      }
  93  
  94      for (auto& frame : pData->gifFrames)
  95      {
  96          if (frame.hBitmap)
  97          {
  98              DeleteObject(frame.hBitmap);
  99              frame.hBitmap = nullptr;
 100          }
 101      }
 102      pData->gifFrames.clear();
 103  }
 104  
 105  static size_t FindGifFrameIndex(const std::vector<VideoRecordingSession::TrimDialogData::GifFrame>& frames, int64_t ticks)
 106  {
 107      if (frames.empty())
 108      {
 109          return 0;
 110      }
 111  
 112      // Linear scan is fine for typical GIF counts; keeps logic simple and predictable
 113      for (size_t i = 0; i < frames.size(); ++i)
 114      {
 115          const auto start = frames[i].start.count();
 116          const auto end = start + frames[i].duration.count();
 117          if (ticks >= start && ticks < end)
 118          {
 119              return i;
 120          }
 121      }
 122  
 123      // If we fall through, clamp to last frame
 124      return frames.size() - 1;
 125  }
 126  
 127  static bool LoadGifFrames(const std::wstring& gifPath, VideoRecordingSession::TrimDialogData* pData)
 128  {
 129      OutputDebugStringW((L"[GIF Trim] LoadGifFrames called for: " + gifPath + L"\n").c_str());
 130  
 131      if (!pData)
 132      {
 133          OutputDebugStringW(L"[GIF Trim] pData is null\n");
 134          return false;
 135      }
 136  
 137      try
 138      {
 139          CleanupGifFrames(pData);
 140  
 141          winrt::com_ptr<IWICImagingFactory> factory;
 142          HRESULT hrFactory = CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(factory.put()));
 143          if (FAILED(hrFactory))
 144          {
 145              OutputDebugStringW((L"[GIF Trim] CoCreateInstance WICImagingFactory failed hr=0x" + std::to_wstring(hrFactory) + L"\n").c_str());
 146              return false;
 147          }
 148  
 149          winrt::com_ptr<IWICBitmapDecoder> decoder;
 150  
 151          auto logHr = [&](const wchar_t* step, HRESULT hr)
 152          {
 153              wchar_t buf[512]{};
 154              swprintf_s(buf, L"[GIF Trim] %s failed hr=0x%08X path=%s\n", step, static_cast<unsigned>(hr), gifPath.c_str());
 155              OutputDebugStringW(buf);
 156          };
 157  
 158          auto tryCreateDecoder = [&]() -> bool
 159          {
 160              OutputDebugStringW(L"[GIF Trim] Trying CreateDecoderFromFilename...\n");
 161              HRESULT hr = factory->CreateDecoderFromFilename(gifPath.c_str(), nullptr, GENERIC_READ, WICDecodeMetadataCacheOnLoad, decoder.put());
 162              if (SUCCEEDED(hr))
 163              {
 164                  OutputDebugStringW(L"[GIF Trim] CreateDecoderFromFilename succeeded\n");
 165                  return true;
 166              }
 167  
 168              logHr(L"CreateDecoderFromFilename", hr);
 169  
 170              // Fallback: try opening with FILE_SHARE_READ | FILE_SHARE_WRITE to handle locked files
 171              OutputDebugStringW(L"[GIF Trim] Trying CreateStreamOnFile fallback...\n");
 172              HANDLE hFile = CreateFileW(gifPath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
 173              if (hFile != INVALID_HANDLE_VALUE)
 174              {
 175                  winrt::com_ptr<IStream> fileStream;
 176                  // Create an IStream over the file handle using SHCreateStreamOnFileEx
 177                  CloseHandle(hFile);
 178                  hr = SHCreateStreamOnFileEx(gifPath.c_str(), STGM_READ | STGM_SHARE_DENY_NONE, 0, FALSE, nullptr, fileStream.put());
 179                  if (SUCCEEDED(hr) && fileStream)
 180                  {
 181                      hr = factory->CreateDecoderFromStream(fileStream.get(), nullptr, WICDecodeMetadataCacheOnLoad, decoder.put());
 182                      if (SUCCEEDED(hr))
 183                      {
 184                          OutputDebugStringW(L"[GIF Trim] CreateDecoderFromStream (SHCreateStreamOnFileEx) succeeded\n");
 185                          return true;
 186                      }
 187                      logHr(L"CreateDecoderFromStream(SHCreateStreamOnFileEx)", hr);
 188                  }
 189                  else
 190                  {
 191                      logHr(L"SHCreateStreamOnFileEx", hr);
 192                  }
 193              }
 194  
 195              return false;
 196          };
 197  
 198      auto tryCopyAndDecode = [&]() -> bool
 199      {
 200          OutputDebugStringW(L"[GIF Trim] Trying temp file copy fallback...\n");
 201          // Copy file to temp using Win32 APIs (no WinRT async)
 202          wchar_t tempDir[MAX_PATH];
 203          if (GetTempPathW(MAX_PATH, tempDir) == 0)
 204          {
 205              return false;
 206          }
 207  
 208          std::wstring tempPath = std::wstring(tempDir) + L"ZoomIt\\";
 209          CreateDirectoryW(tempPath.c_str(), nullptr);
 210  
 211          std::wstring tempName = L"gif_trim_cache_" + std::to_wstring(GetTickCount64()) + L".gif";
 212          tempPath += tempName;
 213  
 214          if (!CopyFileW(gifPath.c_str(), tempPath.c_str(), FALSE))
 215          {
 216              logHr(L"CopyFileW", HRESULT_FROM_WIN32(GetLastError()));
 217              return false;
 218          }
 219  
 220          HRESULT hr = factory->CreateDecoderFromFilename(tempPath.c_str(), nullptr, GENERIC_READ, WICDecodeMetadataCacheOnLoad, decoder.put());
 221          if (SUCCEEDED(hr))
 222          {
 223              OutputDebugStringW(L"[GIF Trim] CreateDecoderFromFilename(temp copy) succeeded\n");
 224              return true;
 225          }
 226          logHr(L"CreateDecoderFromFilename(temp copy)", hr);
 227  
 228          // Clean up temp file on failure
 229          DeleteFileW(tempPath.c_str());
 230          return false;
 231      };
 232  
 233      if (!tryCreateDecoder())
 234      {
 235          if (!tryCopyAndDecode())
 236          {
 237              return false;
 238          }
 239      }
 240  
 241      UINT frameCount = 0;
 242      if (FAILED(decoder->GetFrameCount(&frameCount)) || frameCount == 0)
 243      {
 244          return false;
 245      }
 246  
 247      int64_t cumulativeTicks = 0;
 248      UINT frameWidth = 0;
 249      UINT frameHeight = 0;
 250  
 251      for (UINT i = 0; i < frameCount; ++i)
 252      {
 253          winrt::com_ptr<IWICBitmapFrameDecode> frame;
 254          if (FAILED(decoder->GetFrame(i, frame.put())))
 255          {
 256              continue;
 257          }
 258  
 259          if (i == 0)
 260          {
 261              frame->GetSize(&frameWidth, &frameHeight);
 262          }
 263  
 264          UINT delayCs = kGifDefaultDelayCs;
 265          try
 266          {
 267              winrt::com_ptr<IWICMetadataQueryReader> metadata;
 268              if (SUCCEEDED(frame->GetMetadataQueryReader(metadata.put())) && metadata)
 269              {
 270                  PROPVARIANT prop{};
 271                  PropVariantInit(&prop);
 272                  if (SUCCEEDED(metadata->GetMetadataByName(L"/grctlext/Delay", &prop)))
 273                  {
 274                      if (prop.vt == VT_UI2)
 275                      {
 276                          delayCs = prop.uiVal;
 277                      }
 278                      else if (prop.vt == VT_UI1)
 279                      {
 280                          delayCs = prop.bVal;
 281                      }
 282                  }
 283                  PropVariantClear(&prop);
 284              }
 285          }
 286          catch (...)
 287          {
 288              // Keep fallback delay
 289          }
 290  
 291          if (delayCs == 0)
 292          {
 293              // GIF spec: delay of 0 means "as fast as possible"; browsers use ~10ms
 294              delayCs = kGifDefaultDelayCs;
 295          }
 296          else if (delayCs < kGifBrowserFixupThreshold)
 297          {
 298              // Browsers treat delays < 2cs (20ms) as 10cs (100ms) to prevent CPU-hogging GIFs
 299              delayCs = kGifDefaultDelayCs;
 300          }
 301  
 302          // Log the first few frame delays for debugging
 303          if (i < 3)
 304          {
 305              OutputDebugStringW((L"[GIF Trim] Frame " + std::to_wstring(i) + L" delay: " + std::to_wstring(delayCs) + L" cs (" + std::to_wstring(delayCs * 10) + L" ms)\n").c_str());
 306          }
 307  
 308          // Respect a max preview size to avoid huge allocations on large GIFs
 309          UINT targetWidth = frameWidth;
 310          UINT targetHeight = frameHeight;
 311          if (targetWidth > kGifMaxPreviewDimension || targetHeight > kGifMaxPreviewDimension)
 312          {
 313              const double scaleX = static_cast<double>(kGifMaxPreviewDimension) / static_cast<double>(targetWidth);
 314              const double scaleY = static_cast<double>(kGifMaxPreviewDimension) / static_cast<double>(targetHeight);
 315              const double scale = (std::min)(scaleX, scaleY);
 316              targetWidth = static_cast<UINT>(std::lround(static_cast<double>(targetWidth) * scale));
 317              targetHeight = static_cast<UINT>(std::lround(static_cast<double>(targetHeight) * scale));
 318              targetWidth = (std::max)(1u, targetWidth);
 319              targetHeight = (std::max)(1u, targetHeight);
 320          }
 321  
 322          winrt::com_ptr<IWICBitmapSource> source = frame;
 323          if (targetWidth != frameWidth || targetHeight != frameHeight)
 324          {
 325              winrt::com_ptr<IWICBitmapScaler> scaler;
 326              if (SUCCEEDED(factory->CreateBitmapScaler(scaler.put())))
 327              {
 328                  if (SUCCEEDED(scaler->Initialize(frame.get(), targetWidth, targetHeight, WICBitmapInterpolationModeFant)))
 329                  {
 330                      source = scaler;
 331                  }
 332              }
 333          }
 334  
 335          winrt::com_ptr<IWICFormatConverter> converter;
 336          if (FAILED(factory->CreateFormatConverter(converter.put())))
 337          {
 338              continue;
 339          }
 340  
 341          if (FAILED(converter->Initialize(source.get(), GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom)))
 342          {
 343              continue;
 344          }
 345  
 346          UINT convertedWidth = 0;
 347          UINT convertedHeight = 0;
 348          converter->GetSize(&convertedWidth, &convertedHeight);
 349          if (convertedWidth == 0 || convertedHeight == 0)
 350          {
 351              continue;
 352          }
 353  
 354          const UINT stride = convertedWidth * 4;
 355          std::vector<BYTE> buffer(static_cast<size_t>(stride) * convertedHeight);
 356          if (FAILED(converter->CopyPixels(nullptr, stride, static_cast<UINT>(buffer.size()), buffer.data())))
 357          {
 358              continue;
 359          }
 360  
 361          BITMAPINFO bmi{};
 362          bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
 363          bmi.bmiHeader.biWidth = static_cast<LONG>(convertedWidth);
 364          bmi.bmiHeader.biHeight = -static_cast<LONG>(convertedHeight);
 365          bmi.bmiHeader.biPlanes = 1;
 366          bmi.bmiHeader.biBitCount = 32;
 367          bmi.bmiHeader.biCompression = BI_RGB;
 368  
 369          void* bits = nullptr;
 370          HDC hdcScreen = GetDC(nullptr);
 371          HBITMAP hBitmap = CreateDIBSection(hdcScreen, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
 372          ReleaseDC(nullptr, hdcScreen);
 373  
 374          if (!hBitmap || !bits)
 375          {
 376              if (hBitmap)
 377              {
 378                  DeleteObject(hBitmap);
 379              }
 380              continue;
 381          }
 382  
 383          for (UINT row = 0; row < convertedHeight; ++row)
 384          {
 385              memcpy(static_cast<BYTE*>(bits) + static_cast<size_t>(row) * stride,
 386                     buffer.data() + static_cast<size_t>(row) * stride,
 387                     stride);
 388          }
 389  
 390          VideoRecordingSession::TrimDialogData::GifFrame gifFrame;
 391          gifFrame.hBitmap = hBitmap;
 392          gifFrame.start = winrt::TimeSpan{ cumulativeTicks };
 393          gifFrame.duration = winrt::TimeSpan{ static_cast<int64_t>(delayCs) * 100'000 }; // centiseconds to 100ns
 394          gifFrame.width = convertedWidth;
 395          gifFrame.height = convertedHeight;
 396  
 397          cumulativeTicks += gifFrame.duration.count();
 398          pData->gifFrames.push_back(gifFrame);
 399      }
 400  
 401      if (pData->gifFrames.empty())
 402      {
 403          OutputDebugStringW(L"[GIF Trim] No frames loaded\n");
 404          return false;
 405      }
 406  
 407      const auto& lastFrame = pData->gifFrames.back();
 408      pData->videoDuration = winrt::TimeSpan{ lastFrame.start.count() + lastFrame.duration.count() };
 409      pData->trimEnd = pData->videoDuration;
 410      pData->gifFramesLoaded = true;
 411      pData->gifLastFrameIndex = 0;
 412  
 413      OutputDebugStringW((L"[GIF Trim] Successfully loaded " + std::to_wstring(pData->gifFrames.size()) + L" frames\n").c_str());
 414      return true;
 415      }
 416      catch (const winrt::hresult_error& e)
 417      {
 418          OutputDebugStringW((L"[GIF Trim] Exception in LoadGifFrames: " + e.message() + L"\n").c_str());
 419          return false;
 420      }
 421      catch (const std::exception& e)
 422      {
 423          OutputDebugStringA("[GIF Trim] std::exception in LoadGifFrames: ");
 424          OutputDebugStringA(e.what());
 425          OutputDebugStringA("\n");
 426          return false;
 427      }
 428      catch (...)
 429      {
 430          OutputDebugStringW(L"[GIF Trim] Unknown exception in LoadGifFrames\n");
 431          return false;
 432      }
 433  }
 434  
 435  namespace
 436  {
 437      struct __declspec(uuid("5b0d3235-4dba-4d44-8657-1f1d0f83e9a3")) IMemoryBufferByteAccess : IUnknown
 438      {
 439          virtual HRESULT STDMETHODCALLTYPE GetBuffer(BYTE** value, UINT32* capacity) = 0;
 440      };
 441  
 442      constexpr int kTimelinePadding = 12;
 443      constexpr int kTimelineTrackHeight = 24;
 444      constexpr int kTimelineTrackTopOffset = 18;
 445      constexpr int kTimelineHandleHalfWidth = 5;
 446      constexpr int kTimelineHandleHeight = 40;
 447      constexpr int kTimelineHandleHitRadius = 18;
 448      constexpr int64_t kJogStepTicks = 20'000'000;   // 2 seconds (or 1s for short videos)
 449      constexpr int64_t kPreviewMinDeltaTicks = 2'000'000; // 20ms between thumbnails while playing
 450      constexpr UINT32 kPreviewRequestWidthPlaying = 320;
 451      constexpr UINT32 kPreviewRequestHeightPlaying = 180;
 452      constexpr int64_t kTicksPerMicrosecond = 10; // 100ns units per microsecond
 453      constexpr int64_t kPlaybackSyncIntervalMs = 40;             // refresh baseline frequently for smoother prediction
 454      constexpr int64_t kPlaybackDriftCheckMs = 40;              // sample MediaPlayer at least every 40ms (overridden to every tick currently)
 455      constexpr int64_t kPlaybackDriftSnapTicks = 2'000'000;     // snap if drift exceeds 200ms
 456      constexpr int kPlaybackDriftBlendNumerator = 1;            // blend 20% toward real position
 457      constexpr int kPlaybackDriftBlendDenominator = 5;
 458      constexpr UINT WMU_PREVIEW_READY = WM_USER + 1;
 459      constexpr UINT WMU_PREVIEW_SCHEDULED = WM_USER + 2;
 460      constexpr UINT WMU_DURATION_CHANGED = WM_USER + 3;
 461      constexpr UINT WMU_PLAYBACK_POSITION = WM_USER + 4;
 462      constexpr UINT WMU_PLAYBACK_STOP = WM_USER + 5;
 463      constexpr UINT_PTR kPreviewDebounceTimerId = 100;
 464      constexpr UINT kPreviewDebounceDelayMs = 50;  // Debounce delay for preview updates during dragging
 465  
 466      std::atomic<int> g_highResTimerRefs{ 0 };
 467  
 468      void AcquireHighResTimer()
 469      {
 470          if (g_highResTimerRefs.fetch_add(1, std::memory_order_relaxed) == 0)
 471          {
 472              timeBeginPeriod(1);
 473          }
 474      }
 475  
 476      void ReleaseHighResTimer()
 477      {
 478          const int prev = g_highResTimerRefs.fetch_sub(1, std::memory_order_relaxed);
 479          if (prev == 1)
 480          {
 481              timeEndPeriod(1);
 482          }
 483      }
 484  
 485      bool EnsurePlaybackDevice(VideoRecordingSession::TrimDialogData* pData)
 486      {
 487          if (!pData)
 488          {
 489              return false;
 490          }
 491  
 492          if (pData->previewD3DDevice && pData->previewD3DContext)
 493          {
 494              return true;
 495          }
 496  
 497          UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
 498  #if defined(_DEBUG)
 499          creationFlags |= D3D11_CREATE_DEVICE_DEBUG;
 500  #endif
 501  
 502          D3D_FEATURE_LEVEL levels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0 };
 503          D3D_FEATURE_LEVEL levelCreated = D3D_FEATURE_LEVEL_11_0;
 504  
 505          winrt::com_ptr<ID3D11Device> device;
 506          winrt::com_ptr<ID3D11DeviceContext> context;
 507          if (SUCCEEDED(D3D11CreateDevice(
 508              nullptr,
 509              D3D_DRIVER_TYPE_HARDWARE,
 510              nullptr,
 511              creationFlags,
 512              levels,
 513              ARRAYSIZE(levels),
 514              D3D11_SDK_VERSION,
 515              device.put(),
 516              &levelCreated,
 517              context.put())))
 518          {
 519              pData->previewD3DDevice = device;
 520              pData->previewD3DContext = context;
 521              return true;
 522          }
 523  
 524          return false;
 525      }
 526  
 527      bool EnsureFrameTextures(VideoRecordingSession::TrimDialogData* pData, UINT width, UINT height)
 528      {
 529          if (!pData || !pData->previewD3DDevice)
 530          {
 531              return false;
 532          }
 533  
 534          auto recreate = [&]()
 535          {
 536              pData->previewFrameTexture = nullptr;
 537              pData->previewFrameStaging = nullptr;
 538  
 539              D3D11_TEXTURE2D_DESC desc{};
 540              desc.Width = width;
 541              desc.Height = height;
 542              desc.MipLevels = 1;
 543              desc.ArraySize = 1;
 544              desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
 545              desc.SampleDesc.Count = 1;
 546              desc.Usage = D3D11_USAGE_DEFAULT;
 547              desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
 548  
 549              winrt::com_ptr<ID3D11Texture2D> frameTex;
 550              if (FAILED(pData->previewD3DDevice->CreateTexture2D(&desc, nullptr, frameTex.put())))
 551              {
 552                  return false;
 553              }
 554  
 555              desc.BindFlags = 0;
 556              desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
 557              desc.Usage = D3D11_USAGE_STAGING;
 558  
 559              winrt::com_ptr<ID3D11Texture2D> staging;
 560              if (FAILED(pData->previewD3DDevice->CreateTexture2D(&desc, nullptr, staging.put())))
 561              {
 562                  return false;
 563              }
 564  
 565              pData->previewFrameTexture = frameTex;
 566              pData->previewFrameStaging = staging;
 567              return true;
 568          };
 569  
 570          if (!pData->previewFrameTexture || !pData->previewFrameStaging)
 571          {
 572              return recreate();
 573          }
 574  
 575          D3D11_TEXTURE2D_DESC existing{};
 576          pData->previewFrameTexture->GetDesc(&existing);
 577          if (existing.Width != width || existing.Height != height)
 578          {
 579              return recreate();
 580          }
 581  
 582          return true;
 583      }
 584  
 585      void CenterTrimDialog(HWND hDlg)
 586      {
 587          if (!hDlg)
 588          {
 589              return;
 590          }
 591  
 592          RECT rcDlg{};
 593          if (!GetWindowRect(hDlg, &rcDlg))
 594          {
 595              return;
 596          }
 597  
 598          const int dlgWidth = rcDlg.right - rcDlg.left;
 599          const int dlgHeight = rcDlg.bottom - rcDlg.top;
 600  
 601          // Always center on the monitor containing the dialog, not the parent window
 602          RECT rcTarget{};
 603          HMONITOR monitor = MonitorFromWindow(hDlg, MONITOR_DEFAULTTONEAREST);
 604          MONITORINFO mi{ sizeof(mi) };
 605          if (GetMonitorInfo(monitor, &mi))
 606          {
 607              rcTarget = mi.rcWork;
 608          }
 609          else
 610          {
 611              rcTarget.left = 0;
 612              rcTarget.top = 0;
 613              rcTarget.right = GetSystemMetrics(SM_CXSCREEN);
 614              rcTarget.bottom = GetSystemMetrics(SM_CYSCREEN);
 615          }
 616  
 617          const int targetWidth = rcTarget.right - rcTarget.left;
 618          const int targetHeight = rcTarget.bottom - rcTarget.top;
 619  
 620          int newX = rcTarget.left + (targetWidth - dlgWidth) / 2;
 621          int newY = rcTarget.top + (targetHeight - dlgHeight) / 2;
 622  
 623          if (dlgWidth >= targetWidth)
 624          {
 625              newX = rcTarget.left;
 626          }
 627          else
 628          {
 629              newX = static_cast<int>((std::clamp)(static_cast<LONG>(newX), rcTarget.left, rcTarget.right - dlgWidth));
 630          }
 631  
 632          if (dlgHeight >= targetHeight)
 633          {
 634              newY = rcTarget.top;
 635          }
 636          else
 637          {
 638              newY = static_cast<int>((std::clamp)(static_cast<LONG>(newY), rcTarget.top, rcTarget.bottom - dlgHeight));
 639          }
 640  
 641          SetWindowPos(hDlg, nullptr, newX, newY, 0, 0, SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER);
 642      }
 643  
 644      std::wstring FormatTrimTime(const winrt::TimeSpan& value, bool includeMilliseconds)
 645      {
 646          const int64_t ticks = (std::max)(value.count(), int64_t{ 0 });
 647          const int64_t totalMilliseconds = ticks / 10000LL;
 648          const int milliseconds = static_cast<int>(totalMilliseconds % 1000);
 649          const int64_t totalSeconds = totalMilliseconds / 1000LL;
 650          const int seconds = static_cast<int>(totalSeconds % 60LL);
 651          const int64_t totalMinutes = totalSeconds / 60LL;
 652          const int minutes = static_cast<int>(totalMinutes % 60LL);
 653          const int hours = static_cast<int>(totalMinutes / 60LL);
 654  
 655          wchar_t buffer[32]{};
 656          if (hours > 0)
 657          {
 658              swprintf_s(buffer, L"%d:%02d:%02d", hours, minutes, seconds);
 659          }
 660          else
 661          {
 662              swprintf_s(buffer, L"%02d:%02d", minutes, seconds);
 663          }
 664  
 665          if (!includeMilliseconds)
 666          {
 667              return std::wstring(buffer);
 668          }
 669  
 670          wchar_t msBuffer[8]{};
 671          swprintf_s(msBuffer, L".%03d", milliseconds);
 672          return std::wstring(buffer) + msBuffer;
 673      }
 674  
 675      std::wstring FormatDurationString(const winrt::TimeSpan& duration)
 676      {
 677          return L"Selection: " + FormatTrimTime(duration, true);
 678      }
 679  
 680      void SetTimeText(HWND hDlg, int controlId, const winrt::TimeSpan& value, bool includeMilliseconds)
 681      {
 682          const std::wstring formatted = FormatTrimTime(value, includeMilliseconds);
 683          // Only update if the text has changed to prevent flashing
 684          wchar_t currentText[64] = {};
 685          GetDlgItemText(hDlg, controlId, currentText, _countof(currentText));
 686          if (formatted != currentText)
 687          {
 688              SetDlgItemText(hDlg, controlId, formatted.c_str());
 689          }
 690      }
 691  
 692      int TimelineTimeToClientX(const VideoRecordingSession::TrimDialogData* pData, winrt::TimeSpan value, int clientWidth, UINT dpi = DPI_BASELINE)
 693      {
 694          const int padding = ScaleForDpi(kTimelinePadding, dpi);
 695          const int trackWidth = (std::max)(clientWidth - padding * 2, 1);
 696          return padding + pData->TimeToPixel(value, trackWidth);
 697      }
 698  
 699      winrt::TimeSpan TimelinePixelToTime(const VideoRecordingSession::TrimDialogData* pData, int x, int clientWidth, UINT dpi = DPI_BASELINE)
 700      {
 701          const int padding = ScaleForDpi(kTimelinePadding, dpi);
 702          const int trackWidth = (std::max)(clientWidth - padding * 2, 1);
 703          const int localX = std::clamp(x - padding, 0, trackWidth);
 704          return pData->PixelToTime(localX, trackWidth);
 705      }
 706  
 707      void UpdateDurationDisplay(HWND hDlg, VideoRecordingSession::TrimDialogData* pData)
 708      {
 709          if (!pData || !hDlg)
 710          {
 711              return;
 712          }
 713  
 714          const int64_t selectionTicks = (std::max)(pData->trimEnd.count() - pData->trimStart.count(), int64_t{ 0 });
 715          const std::wstring durationText = FormatDurationString(winrt::TimeSpan{ selectionTicks });
 716          // Only update if the text has changed to prevent flashing
 717          wchar_t currentText[64] = {};
 718          GetDlgItemText(hDlg, IDC_TRIM_DURATION_LABEL, currentText, _countof(currentText));
 719          if (durationText != currentText)
 720          {
 721              SetDlgItemText(hDlg, IDC_TRIM_DURATION_LABEL, durationText.c_str());
 722          }
 723  
 724          // Enable OK when trimming is active (even if unchanged since dialog opened),
 725          // or when the user changed the selection (including reverting to full length).
 726          const bool trimChanged = (pData->trimStart.count() != pData->originalTrimStart.count()) ||
 727                                   (pData->trimEnd.count() != pData->originalTrimEnd.count());
 728          const bool trimIsActive = (pData->trimStart.count() > 0) ||
 729                                    (pData->videoDuration.count() > 0 && pData->trimEnd.count() < pData->videoDuration.count());
 730          EnableWindow(GetDlgItem(hDlg, IDOK), trimChanged || trimIsActive);
 731      }
 732  
 733          RECT GetTimelineTrackRect(const RECT& clientRect, UINT dpi)
 734          {
 735              const int padding = ScaleForDpi(kTimelinePadding, dpi);
 736              const int trackOffset = ScaleForDpi(kTimelineTrackTopOffset, dpi);
 737              const int trackHeight = ScaleForDpi(kTimelineTrackHeight, dpi);
 738              const int trackLeft = clientRect.left + padding;
 739              const int trackRight = clientRect.right - padding;
 740              const int trackTop = clientRect.top + trackOffset;
 741              const int trackBottom = trackTop + trackHeight;
 742              RECT track{ trackLeft, trackTop, trackRight, trackBottom };
 743              return track;
 744          }
 745  
 746          RECT GetPlayheadBoundsRect(const RECT& clientRect, int x, UINT dpi)
 747          {
 748              RECT track = GetTimelineTrackRect(clientRect, dpi);
 749              const int lineThick = ScaleForDpi(3, dpi);
 750              const int topExt = ScaleForDpi(12, dpi);
 751              const int botExt = ScaleForDpi(22, dpi);
 752              const int circleR = ScaleForDpi(6, dpi);
 753              const int circleBotOff = ScaleForDpi(12, dpi);
 754              const int circleBotEnd = ScaleForDpi(24, dpi);
 755              RECT lineRect{ x - lineThick + 1, track.top - topExt, x + lineThick, track.bottom + botExt };
 756              RECT circleRect{ x - circleR, track.bottom + circleBotOff, x + circleR, track.bottom + circleBotEnd };
 757              RECT combined{};
 758              UnionRect(&combined, &lineRect, &circleRect);
 759              return combined;
 760          }
 761  
 762          void InvalidatePlayheadRegion(HWND hTimeline, const RECT& clientRect, int previousX, int newX, UINT dpi)
 763          {
 764              if (!hTimeline)
 765              {
 766                  return;
 767              }
 768  
 769              RECT invalidRect{};
 770              bool hasRect = false;
 771  
 772              if (previousX >= 0)
 773              {
 774                  RECT oldRect = GetPlayheadBoundsRect(clientRect, previousX, dpi);
 775                  invalidRect = oldRect;
 776                  hasRect = true;
 777              }
 778  
 779              if (newX >= 0)
 780              {
 781                  RECT newRect = GetPlayheadBoundsRect(clientRect, newX, dpi);
 782                  if (hasRect)
 783                  {
 784                      RECT unionRect{};
 785                      UnionRect(&unionRect, &invalidRect, &newRect);
 786                      invalidRect = unionRect;
 787                  }
 788                  else
 789                  {
 790                      invalidRect = newRect;
 791                      hasRect = true;
 792                  }
 793              }
 794  
 795              if (hasRect)
 796              {
 797                  InflateRect(&invalidRect, 2, 2);
 798                  InvalidateRect(hTimeline, &invalidRect, FALSE);
 799              }
 800          }
 801  }
 802  
 803  static int64_t SteadyClockMicros()
 804  {
 805      return std::chrono::duration_cast<std::chrono::microseconds>(
 806          std::chrono::steady_clock::now().time_since_epoch()).count();
 807  }
 808  
 809  static void ResetSmoothPlayback(VideoRecordingSession::TrimDialogData* pData)
 810  {
 811      if (!pData)
 812      {
 813          return;
 814      }
 815  
 816      pData->smoothActive.store(false, std::memory_order_relaxed);
 817      pData->smoothBaseTicks.store(0, std::memory_order_relaxed);
 818      pData->smoothLastSyncMicroseconds.store(0, std::memory_order_relaxed);
 819      pData->smoothHasNonZeroSample.store(false, std::memory_order_relaxed);
 820  }
 821  
 822  static void LogSmoothingEvent(const wchar_t* label, int64_t predictedTicks, int64_t mediaTicks, int64_t driftTicks);
 823  
 824  static void SyncSmoothPlayback(VideoRecordingSession::TrimDialogData* pData, int64_t mediaTicks, int64_t /*minTicks*/, int64_t /*maxTicks*/)
 825  {
 826      if (!pData)
 827      {
 828          return;
 829      }
 830  
 831      const int64_t nowUs = SteadyClockMicros();
 832      pData->smoothBaseTicks.store(mediaTicks, std::memory_order_relaxed);
 833      pData->smoothLastSyncMicroseconds.store(nowUs, std::memory_order_relaxed);
 834      pData->smoothActive.store(true, std::memory_order_relaxed);
 835      pData->smoothHasNonZeroSample.store(mediaTicks > 0, std::memory_order_relaxed);
 836  
 837      LogSmoothingEvent(L"setBase", mediaTicks, mediaTicks, 0);
 838  }
 839  
 840  static void LogSmoothingEvent(const wchar_t* label, int64_t predictedTicks, int64_t mediaTicks, int64_t driftTicks)
 841  {
 842      wchar_t buf[256]{};
 843      swprintf_s(buf, L"[TrimSmooth] %s pred=%lld media=%lld drift=%lld\n",
 844          label ? label : L"", static_cast<long long>(predictedTicks), static_cast<long long>(mediaTicks), static_cast<long long>(driftTicks));
 845      OutputDebugStringW(buf);
 846  }
 847  
 848  static void StopPlayback(HWND hDlg, VideoRecordingSession::TrimDialogData* pData, bool capturePosition = true);
 849  static winrt::fire_and_forget StartPlaybackAsync(HWND hDlg, VideoRecordingSession::TrimDialogData* pData);
 850  
 851  
 852  //----------------------------------------------------------------------------
 853  //
 854  // VideoRecordingSession::VideoRecordingSession
 855  //
 856  //----------------------------------------------------------------------------
 857  VideoRecordingSession::VideoRecordingSession(
 858      winrt::IDirect3DDevice const& device,
 859      winrt::GraphicsCaptureItem const& item,
 860      RECT const cropRect,
 861      uint32_t frameRate,
 862      bool captureAudio,
 863      bool captureSystemAudio,
 864      winrt::Streams::IRandomAccessStream const& stream)
 865  {
 866      m_device = device;
 867      m_d3dDevice = GetDXGIInterfaceFromObject<ID3D11Device>(m_device);
 868      m_d3dDevice->GetImmediateContext(m_d3dContext.put());
 869      m_item = item;
 870      auto itemSize = item.Size();
 871      auto inputWidth = EnsureEven(itemSize.Width);
 872      auto inputHeight = EnsureEven(itemSize.Height);
 873      m_frameWait = std::make_shared<CaptureFrameWait>(m_device, m_item, winrt::SizeInt32{ inputWidth, inputHeight });
 874      auto weakPointer{ std::weak_ptr{ m_frameWait } };
 875      m_itemClosed = item.Closed(winrt::auto_revoke, [weakPointer](auto&, auto&)
 876          {
 877              auto sharedPointer{ weakPointer.lock() };
 878  
 879              if (sharedPointer)
 880              {
 881                  sharedPointer->StopCapture();
 882              }
 883          });
 884  
 885      // Get crop dimension
 886      if( (cropRect.right - cropRect.left) != 0 )
 887      {
 888          m_rcCrop = cropRect;
 889          m_frameWait->ShowCaptureBorder( false );
 890      }
 891      else
 892      {
 893          m_rcCrop.left = 0;
 894          m_rcCrop.top = 0;
 895          m_rcCrop.right = inputWidth;
 896          m_rcCrop.bottom = inputHeight;
 897      }
 898  
 899      // Ensure the video is not too small and try to maintain the aspect ratio
 900      constexpr int c_minimumSize = 34;
 901      auto scaledWidth = MulDiv(m_rcCrop.right - m_rcCrop.left, g_RecordScaling, 100);
 902      auto scaledHeight = MulDiv(m_rcCrop.bottom - m_rcCrop.top, g_RecordScaling, 100);
 903      auto outputWidth = scaledWidth;
 904      auto outputHeight = scaledHeight;
 905      if (outputWidth < c_minimumSize)
 906      {
 907          outputWidth = c_minimumSize;
 908          outputHeight = MulDiv(outputHeight, outputWidth, scaledWidth);
 909      }
 910      if (outputHeight < c_minimumSize)
 911      {
 912          outputHeight = c_minimumSize;
 913          outputWidth = MulDiv(outputWidth, outputHeight, scaledHeight);
 914      }
 915      if (outputWidth > inputWidth)
 916      {
 917          outputWidth = inputWidth;
 918          outputHeight = c_minimumSize, MulDiv(outputHeight, scaledWidth, outputWidth);
 919      }
 920      if (outputHeight > inputHeight)
 921      {
 922          outputHeight = inputHeight;
 923          outputWidth = c_minimumSize, MulDiv(outputWidth, scaledHeight, outputHeight);
 924      }
 925      outputWidth = EnsureEven(outputWidth);
 926      outputHeight = EnsureEven(outputHeight);
 927  
 928      // Describe out output: H264 video with an MP4 container
 929      m_encodingProfile = winrt::MediaEncodingProfile();
 930      m_encodingProfile.Container().Subtype(L"MPEG4");
 931      auto video = m_encodingProfile.Video();
 932      video.Subtype(L"H264");
 933      video.Width(outputWidth);
 934      video.Height(outputHeight);
 935      video.Bitrate(static_cast<uint32_t>(outputWidth * outputHeight * frameRate * 2 * 0.07));
 936      video.FrameRate().Numerator(frameRate);
 937      video.FrameRate().Denominator(1);
 938      video.PixelAspectRatio().Numerator(1);
 939      video.PixelAspectRatio().Denominator(1);
 940      m_encodingProfile.Video(video);
 941  
 942      // Always set up audio profile for loopback capture (stereo AAC)
 943      auto audio = m_encodingProfile.Audio();
 944      audio = winrt::AudioEncodingProperties::CreateAac(48000, 2, 192000);
 945      m_encodingProfile.Audio(audio);
 946  
 947      // Describe our input: uncompressed BGRA8 buffers
 948      auto properties = winrt::VideoEncodingProperties::CreateUncompressed(
 949          winrt::MediaEncodingSubtypes::Bgra8(),
 950          static_cast<uint32_t>(m_rcCrop.right - m_rcCrop.left),
 951          static_cast<uint32_t>(m_rcCrop.bottom - m_rcCrop.top));
 952      m_videoDescriptor = winrt::VideoStreamDescriptor(properties);
 953  
 954      m_stream = stream;
 955  
 956      m_previewSwapChain = util::CreateDXGISwapChain(
 957          m_d3dDevice,
 958          static_cast<uint32_t>(m_rcCrop.right - m_rcCrop.left),
 959          static_cast<uint32_t>(m_rcCrop.bottom - m_rcCrop.top),
 960          DXGI_FORMAT_B8G8R8A8_UNORM,
 961          2);
 962      winrt::com_ptr<ID3D11Texture2D> backBuffer;
 963      winrt::check_hresult(m_previewSwapChain->GetBuffer(0, winrt::guid_of<ID3D11Texture2D>(), backBuffer.put_void()));
 964      winrt::check_hresult(m_d3dDevice->CreateRenderTargetView(backBuffer.get(), nullptr, m_renderTargetView.put()));
 965  
 966      // Always create audio generator for loopback capture; captureAudio controls microphone
 967      m_audioGenerator = std::make_unique<AudioSampleGenerator>(captureAudio, captureSystemAudio);
 968  }
 969  
 970  
 971  //----------------------------------------------------------------------------
 972  //
 973  // VideoRecordingSession::~VideoRecordingSession
 974  //
 975  //----------------------------------------------------------------------------
 976  VideoRecordingSession::~VideoRecordingSession()
 977  {
 978      Close();
 979  }
 980  
 981  
 982  //----------------------------------------------------------------------------
 983  //
 984  // VideoRecordingSession::StartAsync
 985  //
 986  //----------------------------------------------------------------------------
 987  winrt::IAsyncAction VideoRecordingSession::StartAsync()
 988  {
 989      auto expected = false;
 990      if (m_isRecording.compare_exchange_strong(expected, true))
 991      {
 992  
 993          // Create our MediaStreamSource
 994          if(m_audioGenerator) {
 995  
 996              co_await m_audioGenerator->InitializeAsync();
 997              m_streamSource = winrt::MediaStreamSource(m_videoDescriptor, winrt::AudioStreamDescriptor(m_audioGenerator->GetEncodingProperties()));
 998          }
 999          else {
1000  
1001              m_streamSource = winrt::MediaStreamSource(m_videoDescriptor);
1002          }
1003          m_streamSource.BufferTime(std::chrono::seconds(0));
1004          m_streamSource.Starting({ this, &VideoRecordingSession::OnMediaStreamSourceStarting });
1005          m_streamSource.SampleRequested({ this, &VideoRecordingSession::OnMediaStreamSourceSampleRequested });
1006  
1007          // Create our transcoder
1008          m_transcoder = winrt::MediaTranscoder();
1009          m_transcoder.HardwareAccelerationEnabled(true);
1010  
1011          auto self = shared_from_this();
1012  
1013          // Start encoding
1014          // If the user stops recording immediately after starting, MediaTranscoder may fail
1015          // with MF_E_NO_SAMPLE_PROCESSED (0xC00D4A44). Avoid surfacing this as an error.
1016          if (m_closed.load())
1017          {
1018              co_return;
1019          }
1020  
1021          winrt::PrepareTranscodeResult transcode{ nullptr };
1022          try
1023          {
1024              transcode = co_await m_transcoder.PrepareMediaStreamSourceTranscodeAsync(m_streamSource, m_stream, m_encodingProfile);
1025  
1026              if (m_closed.load())
1027              {
1028                  co_return;
1029              }
1030  
1031              co_await transcode.TranscodeAsync();
1032          }
1033          catch (winrt::hresult_error const& error)
1034          {
1035              constexpr HRESULT MF_E_NO_SAMPLE_PROCESSED = static_cast<HRESULT>(0xC00D4A44);
1036              if (m_closed.load() || error.code() == MF_E_NO_SAMPLE_PROCESSED)
1037              {
1038                  co_return;
1039              }
1040              throw;
1041          }
1042      }
1043      co_return;
1044  }
1045  
1046  
1047  //----------------------------------------------------------------------------
1048  //
1049  // VideoRecordingSession::Close
1050  //
1051  //----------------------------------------------------------------------------
1052  void VideoRecordingSession::Close()
1053  {
1054      auto expected = false;
1055      if (m_closed.compare_exchange_strong(expected, true))
1056      {
1057          expected = true;
1058          if (!m_isRecording.compare_exchange_strong(expected, false))
1059          {
1060              CloseInternal();
1061          }
1062          else
1063          {
1064              m_frameWait->StopCapture();
1065          }
1066      }
1067  }
1068  
1069  //----------------------------------------------------------------------------
1070  //
1071  // VideoRecordingSession::CloseInternal
1072  //
1073  //----------------------------------------------------------------------------
1074  void VideoRecordingSession::CloseInternal()
1075  {
1076      if(m_audioGenerator) {
1077          m_audioGenerator->Stop();
1078      }
1079      m_frameWait->StopCapture();
1080      m_itemClosed.revoke();
1081  }
1082  
1083  
1084  //----------------------------------------------------------------------------
1085  //
1086  // VideoRecordingSession::OnMediaStreamSourceStarting
1087  //
1088  //----------------------------------------------------------------------------
1089  void VideoRecordingSession::OnMediaStreamSourceStarting(
1090      winrt::MediaStreamSource const&,
1091      winrt::MediaStreamSourceStartingEventArgs const& args)
1092  {
1093      auto frame = m_frameWait->TryGetNextFrame();
1094      if (frame) {
1095          args.Request().SetActualStartPosition(frame->SystemRelativeTime);
1096          if (m_audioGenerator) {
1097  
1098              m_audioGenerator->Start();
1099          }
1100      }
1101  }
1102  
1103  //----------------------------------------------------------------------------
1104  //
1105  // VideoRecordingSession::Create
1106  //
1107  //----------------------------------------------------------------------------
1108  std::shared_ptr<VideoRecordingSession> VideoRecordingSession::Create(
1109      winrt::IDirect3DDevice const& device,
1110      winrt::GraphicsCaptureItem const& item,
1111      RECT const& crop,
1112      uint32_t frameRate,
1113      bool captureAudio,
1114      bool captureSystemAudio,
1115      winrt::Streams::IRandomAccessStream const& stream)
1116  {
1117      return std::shared_ptr<VideoRecordingSession>(new VideoRecordingSession(device, item, crop, frameRate, captureAudio, captureSystemAudio, stream));
1118  }
1119  
1120  //----------------------------------------------------------------------------
1121  //
1122  // VideoRecordingSession::OnMediaStreamSourceSampleRequested
1123  //
1124  //----------------------------------------------------------------------------
1125  void VideoRecordingSession::OnMediaStreamSourceSampleRequested(
1126      winrt::MediaStreamSource const&,
1127      winrt::MediaStreamSourceSampleRequestedEventArgs const& args)
1128  {
1129      auto request = args.Request();
1130      auto streamDescriptor = request.StreamDescriptor();
1131      if (auto videoStreamDescriptor = streamDescriptor.try_as<winrt::VideoStreamDescriptor>())
1132      {
1133          if (auto frame = m_frameWait->TryGetNextFrame())
1134          {
1135              try
1136              {
1137                  auto timeStamp = frame->SystemRelativeTime;
1138                  auto contentSize = frame->ContentSize;
1139                  auto frameTexture = GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame->FrameTexture);
1140                  D3D11_TEXTURE2D_DESC desc = {};
1141                  frameTexture->GetDesc(&desc);
1142  
1143                  winrt::com_ptr<ID3D11Texture2D> backBuffer;
1144                  winrt::check_hresult(m_previewSwapChain->GetBuffer(0, winrt::guid_of<ID3D11Texture2D>(), backBuffer.put_void()));
1145  
1146                  // Use the smaller of the crop size or content size. The content
1147                  // size can change while recording, for example by resizing the
1148                  // window. This ensures that only valid content is copied.
1149                  auto width = min(m_rcCrop.right - m_rcCrop.left, contentSize.Width);
1150                  auto height = min(m_rcCrop.bottom - m_rcCrop.top, contentSize.Height);
1151  
1152                  // Set the content region to copy and clamp the coordinates to the
1153                  // texture surface.
1154                  D3D11_BOX region = {};
1155                  region.left = std::clamp(m_rcCrop.left, static_cast<LONG>(0), static_cast<LONG>(desc.Width));
1156                  region.right = std::clamp(m_rcCrop.left + width, static_cast<LONG>(0), static_cast<LONG>(desc.Width));
1157                  region.top = std::clamp(m_rcCrop.top, static_cast<LONG>(0), static_cast<LONG>(desc.Height));
1158                  region.bottom = std::clamp(m_rcCrop.top + height, static_cast<LONG>(0), static_cast<LONG>(desc.Height));
1159                  region.back = 1;
1160  
1161                  m_d3dContext->ClearRenderTargetView(m_renderTargetView.get(), CLEAR_COLOR);
1162                  m_d3dContext->CopySubresourceRegion(
1163                      backBuffer.get(),
1164                      0,
1165                      0, 0, 0,
1166                      frameTexture.get(),
1167                      0,
1168                      &region);
1169  
1170                  desc = {};
1171                  backBuffer->GetDesc(&desc);
1172  
1173                  desc.Usage = D3D11_USAGE_DEFAULT;
1174                  desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
1175                  desc.CPUAccessFlags = 0;
1176                  desc.MiscFlags = 0;
1177                  winrt::com_ptr<ID3D11Texture2D> sampleTexture;
1178                  winrt::check_hresult(m_d3dDevice->CreateTexture2D(&desc, nullptr, sampleTexture.put()));
1179                  m_d3dContext->CopyResource(sampleTexture.get(), backBuffer.get());
1180                  auto dxgiSurface = sampleTexture.as<IDXGISurface>();
1181                  auto sampleSurface = CreateDirect3DSurface(dxgiSurface.get());
1182  
1183                  DXGI_PRESENT_PARAMETERS presentParameters{};
1184                  winrt::check_hresult(m_previewSwapChain->Present1(0, 0, &presentParameters));
1185  
1186                  auto sample = winrt::MediaStreamSample::CreateFromDirect3D11Surface(sampleSurface, timeStamp);
1187                  m_hasVideoSample.store(true);
1188                  request.Sample(sample);
1189              }
1190              catch (winrt::hresult_error const& error)
1191              {
1192                  OutputDebugStringW(error.message().c_str());
1193                  request.Sample(nullptr);
1194                  CloseInternal();
1195                  return;
1196              }
1197          }
1198          else
1199          {
1200              request.Sample(nullptr);
1201              CloseInternal();
1202          }
1203      }
1204      else if (auto audioStreamDescriptor = streamDescriptor.try_as<winrt::AudioStreamDescriptor>())
1205      {
1206          try
1207          {
1208              if (auto sample = m_audioGenerator->TryGetNextSample())
1209              {
1210                  request.Sample(sample.value());
1211              }
1212              else
1213              {
1214                  request.Sample(nullptr);
1215              }
1216          }
1217          catch (winrt::hresult_error const& error)
1218          {
1219              OutputDebugStringW(error.message().c_str());
1220              request.Sample(nullptr);
1221              CloseInternal();
1222              return;
1223          }
1224      }
1225  }
1226  
1227  //----------------------------------------------------------------------------
1228  //
1229  // Custom file dialog events handler for Trim button
1230  //
1231  //----------------------------------------------------------------------------
1232  class CTrimFileDialogEvents : public IFileDialogEvents, public IFileDialogControlEvents
1233  {
1234  private:
1235      long m_cRef;
1236      HWND m_hParent;
1237      std::wstring m_videoPath;
1238      std::wstring* m_pTrimmedPath;
1239      winrt::TimeSpan* m_pTrimStart;
1240      winrt::TimeSpan* m_pTrimEnd;
1241      bool* m_pShouldTrim;
1242      bool m_bIconSet;
1243  
1244  public:
1245      CTrimFileDialogEvents(HWND hParent, const std::wstring& videoPath,
1246                            std::wstring* pTrimmedPath, winrt::TimeSpan* pTrimStart,
1247                            winrt::TimeSpan* pTrimEnd, bool* pShouldTrim)
1248          : m_cRef(1), m_hParent(hParent), m_videoPath(videoPath),
1249            m_pTrimmedPath(pTrimmedPath), m_pTrimStart(pTrimStart),
1250            m_pTrimEnd(pTrimEnd), m_pShouldTrim(pShouldTrim), m_bIconSet(false)
1251      {
1252      }
1253  
1254      // IUnknown
1255      IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv)
1256      {
1257          static const QITAB qit[] = {
1258              QITABENT(CTrimFileDialogEvents, IFileDialogEvents),
1259              QITABENT(CTrimFileDialogEvents, IFileDialogControlEvents),
1260              { 0 },
1261          };
1262          return QISearch(this, qit, riid, ppv);
1263      }
1264  
1265      IFACEMETHODIMP_(ULONG) AddRef()
1266      {
1267          return InterlockedIncrement(&m_cRef);
1268      }
1269  
1270      IFACEMETHODIMP_(ULONG) Release()
1271      {
1272          long cRef = InterlockedDecrement(&m_cRef);
1273          if (!cRef)
1274              delete this;
1275          return cRef;
1276      }
1277  
1278      // IFileDialogEvents
1279      IFACEMETHODIMP OnFileOk(IFileDialog*) { return S_OK; }
1280  
1281      IFACEMETHODIMP OnFolderChange(IFileDialog* pfd)
1282      {
1283          // Set the ZoomIt icon on the save dialog (only once)
1284          if (!m_bIconSet)
1285          {
1286              m_bIconSet = true;
1287              wil::com_ptr<IOleWindow> pOleWnd;
1288              if (SUCCEEDED(pfd->QueryInterface(IID_PPV_ARGS(&pOleWnd))))
1289              {
1290                  HWND hDlg = nullptr;
1291                  if (SUCCEEDED(pOleWnd->GetWindow(&hDlg)) && hDlg)
1292                  {
1293                      HICON hIcon = LoadIcon(g_hInstance, L"APPICON");
1294                      if (hIcon)
1295                      {
1296                          SendMessage(hDlg, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(hIcon));
1297                          SendMessage(hDlg, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(hIcon));
1298                      }
1299  
1300                      // Make dialog appear in taskbar
1301                      LONG_PTR exStyle = GetWindowLongPtr(hDlg, GWL_EXSTYLE);
1302                      SetWindowLongPtr(hDlg, GWL_EXSTYLE, exStyle | WS_EX_APPWINDOW);
1303                  }
1304              }
1305          }
1306          return S_OK;
1307      }
1308      
1309      IFACEMETHODIMP OnFolderChanging(IFileDialog*, IShellItem*) { return S_OK; }
1310      IFACEMETHODIMP OnSelectionChange(IFileDialog*) { return S_OK; }
1311      IFACEMETHODIMP OnShareViolation(IFileDialog*, IShellItem*, FDE_SHAREVIOLATION_RESPONSE*) { return S_OK; }
1312      IFACEMETHODIMP OnTypeChange(IFileDialog*) { return S_OK; }
1313      IFACEMETHODIMP OnOverwrite(IFileDialog*, IShellItem*, FDE_OVERWRITE_RESPONSE*) { return S_OK; }
1314  
1315      // IFileDialogControlEvents
1316      IFACEMETHODIMP OnItemSelected(IFileDialogCustomize*, DWORD, DWORD) { return S_OK; }
1317      IFACEMETHODIMP OnCheckButtonToggled(IFileDialogCustomize*, DWORD, BOOL) { return S_OK; }
1318      IFACEMETHODIMP OnControlActivating(IFileDialogCustomize*, DWORD) { return S_OK; }
1319  
1320      IFACEMETHODIMP OnButtonClicked(IFileDialogCustomize* pfdc, DWORD dwIDCtl)
1321      {
1322          if (dwIDCtl == 2000) // Trim button ID
1323          {
1324              try
1325              {
1326                  // Get the file dialog's window handle to make trim dialog modal to it
1327                  wil::com_ptr<IFileDialog> pfd;
1328                  HWND hFileDlg = nullptr;
1329                  if (SUCCEEDED(pfdc->QueryInterface(IID_PPV_ARGS(&pfd))))
1330                  {
1331                      wil::com_ptr<IOleWindow> pOleWnd;
1332                      if (SUCCEEDED(pfd->QueryInterface(IID_PPV_ARGS(&pOleWnd))))
1333                      {
1334                          pOleWnd->GetWindow(&hFileDlg);
1335                      }
1336                  }
1337  
1338                  // Use file dialog window as parent if found
1339                  HWND hParent = hFileDlg ? hFileDlg : m_hParent;
1340  
1341                  auto trimResult = VideoRecordingSession::ShowTrimDialog(hParent, m_videoPath, *m_pTrimStart, *m_pTrimEnd);
1342                  if (trimResult == IDOK)
1343                  {
1344                      *m_pShouldTrim = true;
1345                  }
1346                  else if( trimResult == IDCANCEL )
1347                  {
1348                      // Cancel should reset to the default selection (fresh state) and
1349                      // disable trimming for the eventual save.
1350                      *m_pTrimStart = winrt::TimeSpan{ 0 };
1351                      *m_pTrimEnd = winrt::TimeSpan{ 0 };
1352                      *m_pShouldTrim = false;
1353                  }
1354              }
1355              catch (const std::exception& e)
1356              {
1357                  (void)e;
1358              }
1359              catch (...)
1360              {
1361              }
1362          }
1363          return S_OK;
1364      }
1365  };
1366  
1367  //----------------------------------------------------------------------------
1368  //
1369  // VideoRecordingSession::ShowSaveDialogWithTrim
1370  //
1371  // Main entry point for trim+save workflow
1372  //
1373  //----------------------------------------------------------------------------
1374  std::wstring VideoRecordingSession::ShowSaveDialogWithTrim(
1375      HWND hParent,
1376      const std::wstring& suggestedFileName,
1377      const std::wstring& originalVideoPath,
1378      std::wstring& trimmedVideoPath)
1379  {
1380      trimmedVideoPath.clear();
1381  
1382      const bool isGif = IsGifPath(originalVideoPath);
1383  
1384      std::wstring videoPathToSave = originalVideoPath;
1385      winrt::TimeSpan trimStart{ 0 };
1386      winrt::TimeSpan trimEnd{ 0 };
1387      bool shouldTrim = false;
1388  
1389      // Create save dialog with custom Trim button
1390      auto saveDialog = wil::CoCreateInstance<::IFileSaveDialog>(CLSID_FileSaveDialog);
1391  
1392      FILEOPENDIALOGOPTIONS options;
1393      if (SUCCEEDED(saveDialog->GetOptions(&options)))
1394          saveDialog->SetOptions(options | FOS_FORCEFILESYSTEM);
1395  
1396      wil::com_ptr<::IShellItem> videosItem;
1397      if (SUCCEEDED(SHGetKnownFolderItem(FOLDERID_Videos, KF_FLAG_DEFAULT, nullptr,
1398          IID_IShellItem, (void**)videosItem.put())))
1399          saveDialog->SetDefaultFolder(videosItem.get());
1400  
1401      if (isGif)
1402      {
1403          saveDialog->SetDefaultExtension(L".gif");
1404          COMDLG_FILTERSPEC fileTypes[] = {
1405              { L"GIF Animation", L"*.gif" }
1406          };
1407          saveDialog->SetFileTypes(_countof(fileTypes), fileTypes);
1408      }
1409      else
1410      {
1411          saveDialog->SetDefaultExtension(L".mp4");
1412          COMDLG_FILTERSPEC fileTypes[] = {
1413              { L"MP4 Video", L"*.mp4" }
1414          };
1415          saveDialog->SetFileTypes(_countof(fileTypes), fileTypes);
1416      }
1417      saveDialog->SetFileName(suggestedFileName.c_str());
1418      saveDialog->SetTitle(L"ZoomIt: Save Video As...");
1419  
1420      // Add custom Trim button
1421      wil::com_ptr<IFileDialogCustomize> pfdCustomize;
1422      if (SUCCEEDED(saveDialog->QueryInterface(IID_PPV_ARGS(&pfdCustomize))))
1423      {
1424          pfdCustomize->AddPushButton(2000, L"Trim...");
1425      }
1426  
1427      // Set up event handler
1428      CTrimFileDialogEvents* pEvents = new CTrimFileDialogEvents(hParent, originalVideoPath,
1429          &trimmedVideoPath, &trimStart, &trimEnd, &shouldTrim);
1430      DWORD dwCookie;
1431      saveDialog->Advise(pEvents, &dwCookie);
1432  
1433      HRESULT hr = saveDialog->Show(hParent);
1434  
1435      saveDialog->Unadvise(dwCookie);
1436      pEvents->Release();
1437  
1438      if (FAILED(hr))
1439      {
1440          return std::wstring(); // User cancelled save dialog
1441      }
1442  
1443      // If user clicked Trim button and confirmed, perform the trim
1444      if (shouldTrim)
1445      {
1446          try
1447          {
1448              auto trimOp = isGif ? TrimGifAsync(originalVideoPath, trimStart, trimEnd)
1449                                  : TrimVideoAsync(originalVideoPath, trimStart, trimEnd);
1450  
1451              // Pump messages while waiting for async operation
1452              while (trimOp.Status() == winrt::AsyncStatus::Started)
1453              {
1454                  MSG msg;
1455                  while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
1456                  {
1457                      TranslateMessage(&msg);
1458                      DispatchMessage(&msg);
1459                  }
1460                  Sleep(10);
1461              }
1462  
1463              auto trimmedPath = std::wstring(trimOp.GetResults());
1464  
1465              if (trimmedPath.empty())
1466              {
1467                  MessageBox(hParent, L"Failed to trim video", L"Error", MB_OK | MB_ICONERROR);
1468                  return std::wstring();
1469              }
1470  
1471              trimmedVideoPath = trimmedPath;
1472              videoPathToSave = trimmedPath;
1473          }
1474          catch (...)
1475          {
1476              MessageBox(hParent, L"Failed to trim video", L"Error", MB_OK | MB_ICONERROR);
1477              return std::wstring();
1478          }
1479      }
1480  
1481      wil::com_ptr<::IShellItem> item;
1482      THROW_IF_FAILED(saveDialog->GetResult(item.put()));
1483  
1484      wil::unique_cotaskmem_string filePath;
1485      THROW_IF_FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, filePath.put()));
1486  
1487      return std::wstring(filePath.get());
1488  }
1489  
1490  //----------------------------------------------------------------------------
1491  //
1492  // VideoRecordingSession::ShowTrimDialog
1493  //
1494  // Shows the trim UI dialog
1495  //
1496  //----------------------------------------------------------------------------
1497  INT_PTR VideoRecordingSession::ShowTrimDialog(
1498      HWND hParent,
1499      const std::wstring& videoPath,
1500      winrt::TimeSpan& trimStart,
1501      winrt::TimeSpan& trimEnd)
1502  {
1503      std::promise<INT_PTR> resultPromise;
1504      auto resultFuture = resultPromise.get_future();
1505  
1506      std::thread staThread([hParent, videoPath, &trimStart, &trimEnd, promise = std::move(resultPromise)]() mutable
1507      {
1508          bool coInitialized = false;
1509          try
1510          {
1511              winrt::init_apartment(winrt::apartment_type::single_threaded);
1512          }
1513          catch (const winrt::hresult_error&)
1514          {
1515              HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
1516              if (SUCCEEDED(hr))
1517              {
1518                  coInitialized = true;
1519              }
1520          }
1521  
1522          try
1523          {
1524              INT_PTR dlgResult = ShowTrimDialogInternal(hParent, videoPath, trimStart, trimEnd);
1525              promise.set_value(dlgResult);
1526          }
1527          catch (const winrt::hresult_error& e)
1528          {
1529              (void)e;
1530              promise.set_exception(std::current_exception());
1531          }
1532          catch (const std::exception& e)
1533          {
1534              (void)e;
1535              promise.set_exception(std::current_exception());
1536          }
1537          catch (...)
1538          {
1539              promise.set_exception(std::current_exception());
1540          }
1541  
1542          if (coInitialized)
1543          {
1544              CoUninitialize();
1545          }
1546      });
1547  
1548      bool quitReceived = false;
1549      while (!quitReceived && resultFuture.wait_for(std::chrono::milliseconds(20)) != std::future_status::ready)
1550      {
1551          MSG msg;
1552          while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
1553          {
1554              if (msg.message == WM_QUIT)
1555              {
1556                  // WM_QUIT must be reposted so the main application loop can exit cleanly.
1557                  quitReceived = true;
1558              }
1559              TranslateMessage(&msg);
1560              DispatchMessage(&msg);
1561          }
1562      }
1563  
1564      // Repost WM_QUIT after waiting for the dialog thread to finish, so the main loop can handle it.
1565      if (quitReceived && hDlgTrimDialog != nullptr)
1566      {
1567          EndDialog(hDlgTrimDialog, IDCANCEL);
1568          PostQuitMessage(0);
1569      }
1570  
1571      INT_PTR dialogResult = quitReceived ? IDCANCEL : resultFuture.get();
1572      if (staThread.joinable())
1573      {
1574          staThread.join();
1575      }
1576      return dialogResult;
1577  }
1578  
1579  INT_PTR VideoRecordingSession::ShowTrimDialogInternal(
1580      HWND hParent,
1581      const std::wstring& videoPath,
1582      winrt::TimeSpan& trimStart,
1583      winrt::TimeSpan& trimEnd)
1584  {
1585      TrimDialogData data;
1586      data.videoPath = videoPath;
1587      // Initialize from the caller so reopening the trim dialog can preserve prior work.
1588      data.trimStart = trimStart;
1589      data.trimEnd = trimEnd;
1590      data.isGif = IsGifPath(videoPath);
1591  
1592      if (data.isGif)
1593      {
1594          if (!LoadGifFrames(videoPath, &data))
1595          {
1596              MessageBox(hParent, L"Unable to load the GIF for trimming. The file may be locked or unreadable.", L"Error", MB_OK | MB_ICONERROR);
1597              return IDCANCEL;
1598          }
1599      }
1600      else
1601      {
1602          // Get video duration - use simple file size estimation to avoid blocking
1603          // The actual trim operation will handle the real duration
1604          WIN32_FILE_ATTRIBUTE_DATA fileInfo;
1605          if (GetFileAttributesEx(videoPath.c_str(), GetFileExInfoStandard, &fileInfo))
1606          {
1607              ULARGE_INTEGER fileSize;
1608              fileSize.LowPart = fileInfo.nFileSizeLow;
1609              fileSize.HighPart = fileInfo.nFileSizeHigh;
1610  
1611              // Estimate: ~10MB per minute for typical 1080p recording
1612              // Duration in 100-nanosecond units (10,000,000 = 1 second)
1613              int64_t estimatedSeconds = fileSize.QuadPart / (10 * 1024 * 1024 / 60);
1614              if (estimatedSeconds < 1) estimatedSeconds = 10; // minimum 10 seconds
1615              if (estimatedSeconds > 3600) estimatedSeconds = 3600; // max 1 hour
1616  
1617              data.videoDuration = winrt::TimeSpan{ estimatedSeconds * 10000000LL };
1618              if( data.trimEnd.count() <= 0 )
1619              {
1620                  data.trimEnd = data.videoDuration;
1621              }
1622          }
1623          else
1624          {
1625              // Default to 60 seconds if we can't get file size
1626              data.videoDuration = winrt::TimeSpan{ 600000000LL };
1627              if( data.trimEnd.count() <= 0 )
1628              {
1629                  data.trimEnd = data.videoDuration;
1630              }
1631          }
1632      }
1633  
1634      // Clamp incoming selection to valid bounds now that duration is known.
1635      if( data.videoDuration.count() > 0 )
1636      {
1637          const int64_t durationTicks = data.videoDuration.count();
1638          const int64_t endTicks = (data.trimEnd.count() > 0) ? data.trimEnd.count() : durationTicks;
1639          const int64_t clampedEnd = std::clamp<int64_t>( endTicks, 0, durationTicks );
1640          const int64_t clampedStart = std::clamp<int64_t>( data.trimStart.count(), 0, clampedEnd );
1641          data.trimStart = winrt::TimeSpan{ clampedStart };
1642          data.trimEnd = winrt::TimeSpan{ clampedEnd };
1643      }
1644  
1645      // Track initial selection so we can enable OK only when trimming changes.
1646      data.originalTrimStart = data.trimStart;
1647      data.originalTrimEnd = data.trimEnd;
1648      data.currentPosition = data.trimStart;
1649      data.playbackStartPosition = data.currentPosition;
1650      data.playbackStartPositionValid = true;
1651  
1652      // Center dialog on the screen containing the parent window
1653      HMONITOR hMonitor = MonitorFromWindow(hParent, MONITOR_DEFAULTTONEAREST);
1654      MONITORINFO mi = { sizeof(mi) };
1655      GetMonitorInfo(hMonitor, &mi);
1656  
1657      // Calculate center position
1658      const int dialogWidth = 521;
1659      const int dialogHeight = 381;
1660      int x = mi.rcWork.left + (mi.rcWork.right - mi.rcWork.left - dialogWidth) / 2;
1661      int y = mi.rcWork.top + (mi.rcWork.bottom - mi.rcWork.top - dialogHeight) / 2;
1662  
1663      // Store position for use in dialog proc
1664      data.dialogX = x;
1665      data.dialogY = y;
1666  
1667      // Pre-load the first frame preview before showing the dialog to avoid "Preview not available" flash
1668      // Must run on a background thread because WinRT async .get() cannot be called on STA (UI) thread
1669      if (!data.isGif)
1670      {
1671          std::thread preloadThread([&data, &videoPath]()
1672          {
1673              winrt::init_apartment(winrt::apartment_type::multi_threaded);
1674              try
1675              {
1676                  auto file = winrt::StorageFile::GetFileFromPathAsync(videoPath).get();
1677                  auto clip = winrt::MediaClip::CreateFromFileAsync(file).get();
1678  
1679                  data.composition = winrt::MediaComposition();
1680                  data.composition.Clips().Append(clip);
1681  
1682                  // Update to actual duration from clip
1683                  auto actualDuration = clip.OriginalDuration();
1684                  if (actualDuration.count() > 0)
1685                  {
1686                      // If trimEnd was at full length (whether estimated or passed in), snap it to the actual end.
1687                      // This handles cases where the file-size estimate was longer or shorter than actual.
1688                      const int64_t oldDurationTicks = data.videoDuration.count();
1689                      const int64_t oldTrimEndTicks = data.trimEnd.count();
1690                      const bool endWasFullLength = (oldTrimEndTicks <= 0) ||
1691                          (oldDurationTicks > 0 && oldTrimEndTicks >= oldDurationTicks);
1692  
1693                      data.videoDuration = actualDuration;
1694  
1695                      const int64_t newTrimEndTicks = endWasFullLength ? actualDuration.count()
1696                          : (std::min)(oldTrimEndTicks, actualDuration.count());
1697                      data.trimEnd = winrt::TimeSpan{ newTrimEndTicks };
1698  
1699                      const int64_t oldOrigEndTicks = data.originalTrimEnd.count();
1700                      const bool origEndWasFullLength = (oldOrigEndTicks <= 0) ||
1701                          (oldDurationTicks > 0 && oldOrigEndTicks >= oldDurationTicks);
1702                      const int64_t newOrigEndTicks = origEndWasFullLength ? actualDuration.count()
1703                          : (std::min)(oldOrigEndTicks, actualDuration.count());
1704                      data.originalTrimEnd = winrt::TimeSpan{ newOrigEndTicks };
1705                  }
1706  
1707                  // Get first frame thumbnail
1708                  const int64_t requestTicks = std::clamp<int64_t>(data.currentPosition.count(), 0, data.videoDuration.count());
1709                  auto stream = data.composition.GetThumbnailAsync(
1710                      winrt::TimeSpan{ requestTicks },
1711                      0, 0,
1712                      winrt::VideoFramePrecision::NearestFrame).get();
1713  
1714                  if (stream)
1715                  {
1716                      winrt::com_ptr<IWICImagingFactory> wicFactory;
1717                      if (SUCCEEDED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(wicFactory.put()))))
1718                      {
1719                          winrt::com_ptr<IStream> istream;
1720                          auto streamAsUnknown = static_cast<::IUnknown*>(winrt::get_abi(stream));
1721                          if (SUCCEEDED(CreateStreamOverRandomAccessStream(streamAsUnknown, IID_PPV_ARGS(istream.put()))) && istream)
1722                          {
1723                              winrt::com_ptr<IWICBitmapDecoder> decoder;
1724                              if (SUCCEEDED(wicFactory->CreateDecoderFromStream(istream.get(), nullptr, WICDecodeMetadataCacheOnDemand, decoder.put())))
1725                              {
1726                                  winrt::com_ptr<IWICBitmapFrameDecode> frame;
1727                                  if (SUCCEEDED(decoder->GetFrame(0, frame.put())))
1728                                  {
1729                                      winrt::com_ptr<IWICFormatConverter> converter;
1730                                      if (SUCCEEDED(wicFactory->CreateFormatConverter(converter.put())))
1731                                      {
1732                                          if (SUCCEEDED(converter->Initialize(frame.get(), GUID_WICPixelFormat32bppBGRA,
1733                                                                               WICBitmapDitherTypeNone, nullptr, 0.0,
1734                                                                               WICBitmapPaletteTypeCustom)))
1735                                          {
1736                                              UINT width, height;
1737                                              converter->GetSize(&width, &height);
1738  
1739                                              BITMAPINFO bmi = {};
1740                                              bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
1741                                              bmi.bmiHeader.biWidth = width;
1742                                              bmi.bmiHeader.biHeight = -static_cast<LONG>(height);
1743                                              bmi.bmiHeader.biPlanes = 1;
1744                                              bmi.bmiHeader.biBitCount = 32;
1745                                              bmi.bmiHeader.biCompression = BI_RGB;
1746  
1747                                              void* bits = nullptr;
1748                                              HDC hdcScreen = GetDC(nullptr);
1749                                              HBITMAP hBitmap = CreateDIBSection(hdcScreen, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
1750                                              ReleaseDC(nullptr, hdcScreen);
1751  
1752                                              if (hBitmap && bits)
1753                                              {
1754                                                  converter->CopyPixels(nullptr, width * 4, width * height * 4, static_cast<BYTE*>(bits));
1755                                                  data.hPreviewBitmap = hBitmap;
1756                                                  data.previewBitmapOwned = true;
1757                                                  data.lastRenderedPreview.store(requestTicks, std::memory_order_relaxed);
1758                                              }
1759                                          }
1760                                      }
1761                                  }
1762                              }
1763                          }
1764                      }
1765                  }
1766              }
1767              catch (...)
1768              {
1769                  // If preloading fails, the dialog will show "Preview not available" briefly
1770                  // but will recover via the async UpdateVideoPreview path
1771              }
1772          });
1773          preloadThread.join();
1774      }
1775  
1776      auto result = DialogBoxParam(
1777          GetModuleHandle(nullptr),
1778          MAKEINTRESOURCE(IDD_VIDEO_TRIM),
1779          hParent,
1780          TrimDialogProc,
1781          reinterpret_cast<LPARAM>(&data));
1782  
1783      if (result == IDOK)
1784      {
1785          trimStart = data.trimStart;
1786          trimEnd = data.trimEnd;
1787      }
1788  
1789      return result;
1790  }
1791  
1792  static void UpdatePositionUI(HWND hDlg, VideoRecordingSession::TrimDialogData* pData, bool invalidateTimeline = true)
1793  {
1794      if (!pData || !hDlg)
1795      {
1796          return;
1797      }
1798  
1799      const auto previewTime = pData->previewOverrideActive ? pData->previewOverride : pData->currentPosition;
1800      // Show time relative to left grip (trimStart)
1801      const auto relativeTime = winrt::TimeSpan{ (std::max)(previewTime.count() - pData->trimStart.count(), int64_t{ 0 }) };
1802      SetTimeText(hDlg, IDC_TRIM_POSITION_LABEL, relativeTime, true);
1803      if (invalidateTimeline)
1804      {
1805          InvalidateRect(GetDlgItem(hDlg, IDC_TRIM_TIMELINE), nullptr, FALSE);
1806      }
1807  }
1808  
1809  static void SyncMediaPlayerPosition(VideoRecordingSession::TrimDialogData* pData)
1810  {
1811      if (!pData || !pData->mediaPlayer)
1812      {
1813          return;
1814      }
1815  
1816      try
1817      {
1818          auto session = pData->mediaPlayer.PlaybackSession();
1819          if (session)
1820          {
1821              // The selection (trimStart..trimEnd) determines what will be trimmed,
1822              // but playback may start before trimStart. Clamp only to valid media bounds.
1823              const int64_t upper = (pData->trimEnd.count() > 0) ? pData->trimEnd.count() : pData->videoDuration.count();
1824              const int64_t clampedTicks = std::clamp<int64_t>(pData->currentPosition.count(), 0, upper);
1825              session.Position(winrt::TimeSpan{ clampedTicks });
1826          }
1827      }
1828      catch (...)
1829      {
1830      }
1831  }
1832  
1833  static void CleanupMediaPlayer(VideoRecordingSession::TrimDialogData* pData)
1834  {
1835      if (!pData || !pData->mediaPlayer)
1836      {
1837          return;
1838      }
1839  
1840      try
1841      {
1842          auto session = pData->mediaPlayer.PlaybackSession();
1843          if (session)
1844          {
1845              if (pData->positionChangedToken.value)
1846              {
1847                  session.PositionChanged(pData->positionChangedToken);
1848                  pData->positionChangedToken = {};
1849              }
1850              if (pData->stateChangedToken.value)
1851              {
1852                  session.PlaybackStateChanged(pData->stateChangedToken);
1853                  pData->stateChangedToken = {};
1854              }
1855          }
1856  
1857          if (pData->frameAvailableToken.value)
1858          {
1859              pData->mediaPlayer.VideoFrameAvailable(pData->frameAvailableToken);
1860              pData->frameAvailableToken = {};
1861          }
1862  
1863          pData->mediaPlayer.Close();
1864      }
1865      catch (...)
1866      {
1867      }
1868  
1869      pData->mediaPlayer = nullptr;
1870      pData->frameCopyInProgress.store(false, std::memory_order_relaxed);
1871  }
1872  
1873  //----------------------------------------------------------------------------
1874  //
1875  // Helper: Update video frame preview
1876  //
1877  //----------------------------------------------------------------------------
1878  static void UpdateVideoPreview(HWND hDlg, VideoRecordingSession::TrimDialogData* pData, bool invalidateTimeline = true)
1879  {
1880      if (!pData)
1881      {
1882          return;
1883      }
1884  
1885      const auto previewTime = pData->previewOverrideActive ? pData->previewOverride : pData->currentPosition;
1886  
1887      // Update position label and timeline
1888      UpdatePositionUI(hDlg, pData, invalidateTimeline);
1889  
1890      // When playing with the frame server, frames arrive via VideoFrameAvailable; avoid extra thumbnails.
1891      if (pData->isPlaying.load(std::memory_order_relaxed) && pData->mediaPlayer)
1892      {
1893          return;
1894      }
1895  
1896      const int64_t requestTicks = previewTime.count();
1897      pData->latestPreviewRequest.store(requestTicks, std::memory_order_relaxed);
1898  
1899      if (pData->loadingPreview.exchange(true))
1900      {
1901          // A preview request is already running; we'll schedule the latest once it completes.
1902          return;
1903      }
1904  
1905      if (pData->isGif)
1906      {
1907          // Use request time directly (don't clamp to trim bounds) so thumbnail updates outside trim region
1908          const int64_t clampedTicks = std::clamp<int64_t>(requestTicks, 0, pData->videoDuration.count());
1909          if (!pData->gifFrames.empty())
1910          {
1911              const size_t frameIndex = FindGifFrameIndex(pData->gifFrames, clampedTicks);
1912              pData->gifLastFrameIndex = frameIndex;
1913              {
1914                  std::lock_guard<std::mutex> lock(pData->previewBitmapMutex);
1915                  if (pData->hPreviewBitmap && pData->previewBitmapOwned)
1916                  {
1917                      DeleteObject(pData->hPreviewBitmap);
1918                  }
1919                  pData->hPreviewBitmap = pData->gifFrames[frameIndex].hBitmap;
1920                  pData->previewBitmapOwned = false;
1921              }
1922  
1923              pData->lastRenderedPreview.store(clampedTicks, std::memory_order_relaxed);
1924              pData->loadingPreview.store(false, std::memory_order_relaxed);
1925  
1926              if (hDlg)
1927              {
1928                  InvalidateRect(GetDlgItem(hDlg, IDC_TRIM_PREVIEW), nullptr, FALSE);
1929              }
1930              return;
1931          }
1932  
1933          pData->loadingPreview.store(false, std::memory_order_relaxed);
1934          return;
1935      }
1936  
1937      std::thread([](HWND hDlg, VideoRecordingSession::TrimDialogData* pData, int64_t requestTicks)
1938      {
1939          winrt::init_apartment(winrt::apartment_type::multi_threaded);
1940  
1941          const int64_t requestTicksRaw = requestTicks;
1942          bool updatedBitmap = false;
1943  
1944          bool durationChanged = false;
1945  
1946          try
1947          {
1948              if (!pData->composition)
1949              {
1950                  auto file = winrt::StorageFile::GetFileFromPathAsync(pData->videoPath).get();
1951                  auto clip = winrt::MediaClip::CreateFromFileAsync(file).get();
1952  
1953                  pData->composition = winrt::MediaComposition();
1954                  pData->composition.Clips().Append(clip);
1955  
1956                  auto actualDuration = clip.OriginalDuration();
1957                  if (actualDuration.count() > 0)
1958                  {
1959                      const int64_t oldDurationTicks = pData->videoDuration.count();
1960                      if (oldDurationTicks != actualDuration.count())
1961                      {
1962                          durationChanged = true;
1963                      }
1964  
1965                      // Update duration, but preserve a user-chosen trim end.
1966                      // If the trim end was "full length" (old duration or 0), keep it full length.
1967                      pData->videoDuration = actualDuration;
1968  
1969                      const int64_t oldTrimEndTicks = pData->trimEnd.count();
1970                      const bool endWasFullLength = (oldTrimEndTicks <= 0) || (oldDurationTicks > 0 && oldTrimEndTicks >= oldDurationTicks);
1971                      const int64_t newTrimEndTicks = endWasFullLength ? actualDuration.count()
1972                          : (std::min)(oldTrimEndTicks, actualDuration.count());
1973                      pData->trimEnd = winrt::TimeSpan{ newTrimEndTicks };
1974  
1975                      const int64_t oldOrigEndTicks = pData->originalTrimEnd.count();
1976                      const bool origEndWasFullLength = (oldOrigEndTicks <= 0) || (oldDurationTicks > 0 && oldOrigEndTicks >= oldDurationTicks);
1977                      const int64_t newOrigEndTicks = origEndWasFullLength ? actualDuration.count()
1978                          : (std::min)(oldOrigEndTicks, actualDuration.count());
1979                      pData->originalTrimEnd = winrt::TimeSpan{ newOrigEndTicks };
1980  
1981                      // Clamp starts to the new end.
1982                      if (pData->originalTrimStart.count() > pData->originalTrimEnd.count())
1983                      {
1984                          pData->originalTrimStart = pData->originalTrimEnd;
1985                      }
1986                      if (pData->trimStart.count() > pData->trimEnd.count())
1987                      {
1988                          pData->trimStart = pData->trimEnd;
1989                      }
1990                  }
1991              }
1992  
1993              auto composition = pData->composition;
1994              if (composition)
1995              {
1996                  auto durationTicks = composition.Duration().count();
1997                  if (durationTicks > 0)
1998                  {
1999                      requestTicks = std::clamp<int64_t>(requestTicks, 0, durationTicks);
2000                  }
2001  
2002                  const bool isPlaying = pData->isPlaying.load(std::memory_order_relaxed);
2003                  const UINT32 reqW = isPlaying ? kPreviewRequestWidthPlaying : 0;
2004                  const UINT32 reqH = isPlaying ? kPreviewRequestHeightPlaying : 0;
2005  
2006                  auto stream = composition.GetThumbnailAsync(
2007                      winrt::TimeSpan{ requestTicks },
2008                      reqW,
2009                      reqH,
2010                      winrt::VideoFramePrecision::NearestFrame).get();
2011  
2012                  if (stream)
2013                  {
2014                      winrt::com_ptr<IWICImagingFactory> wicFactory;
2015                      if (SUCCEEDED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(wicFactory.put()))))
2016                      {
2017                          winrt::com_ptr<IStream> istream;
2018                          auto streamAsUnknown = static_cast<::IUnknown*>(winrt::get_abi(stream));
2019                          if (SUCCEEDED(CreateStreamOverRandomAccessStream(streamAsUnknown, IID_PPV_ARGS(istream.put()))) && istream)
2020                          {
2021                              winrt::com_ptr<IWICBitmapDecoder> decoder;
2022                              if (SUCCEEDED(wicFactory->CreateDecoderFromStream(istream.get(), nullptr, WICDecodeMetadataCacheOnDemand, decoder.put())))
2023                              {
2024                                  winrt::com_ptr<IWICBitmapFrameDecode> frame;
2025                                  if (SUCCEEDED(decoder->GetFrame(0, frame.put())))
2026                                  {
2027                                      winrt::com_ptr<IWICFormatConverter> converter;
2028                                      if (SUCCEEDED(wicFactory->CreateFormatConverter(converter.put())))
2029                                      {
2030                                          if (SUCCEEDED(converter->Initialize(frame.get(), GUID_WICPixelFormat32bppBGRA,
2031                                                                               WICBitmapDitherTypeNone, nullptr, 0.0,
2032                                                                               WICBitmapPaletteTypeCustom)))
2033                                          {
2034                                              UINT width, height;
2035                                              converter->GetSize(&width, &height);
2036  
2037                                              BITMAPINFO bmi = {};
2038                                              bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
2039                                              bmi.bmiHeader.biWidth = width;
2040                                              bmi.bmiHeader.biHeight = -static_cast<LONG>(height);
2041                                              bmi.bmiHeader.biPlanes = 1;
2042                                              bmi.bmiHeader.biBitCount = 32;
2043                                              bmi.bmiHeader.biCompression = BI_RGB;
2044  
2045                                              void* bits = nullptr;
2046                                              HDC hdcScreen = GetDC(nullptr);
2047                                              HBITMAP hBitmap = CreateDIBSection(hdcScreen, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
2048                                              ReleaseDC(nullptr, hdcScreen);
2049  
2050                                              if (hBitmap && bits)
2051                                              {
2052                                                  converter->CopyPixels(nullptr, width * 4, width * height * 4, static_cast<BYTE*>(bits));
2053  
2054                                                  {
2055                                                      std::lock_guard<std::mutex> lock(pData->previewBitmapMutex);
2056                                                      if (pData->hPreviewBitmap && pData->previewBitmapOwned)
2057                                                      {
2058                                                          DeleteObject(pData->hPreviewBitmap);
2059                                                      }
2060                                                      pData->hPreviewBitmap = hBitmap;
2061                                                      pData->previewBitmapOwned = true;
2062                                                  }
2063                                                  updatedBitmap = true;
2064                                              }
2065                                          }
2066                                      }
2067                                  }
2068                              }
2069                          }
2070                      }
2071                  }
2072              }
2073          }
2074          catch (...)
2075          {
2076          }
2077  
2078          pData->loadingPreview.store(false, std::memory_order_relaxed);
2079  
2080          if (updatedBitmap)
2081          {
2082              pData->lastRenderedPreview.store(requestTicks, std::memory_order_relaxed);
2083              PostMessage(hDlg, WMU_PREVIEW_READY, 0, 0);
2084          }
2085  
2086          if (pData->latestPreviewRequest.load(std::memory_order_relaxed) != requestTicksRaw)
2087          {
2088              PostMessage(hDlg, WMU_PREVIEW_SCHEDULED, 0, 0);
2089          }
2090  
2091          if (durationChanged)
2092          {
2093              PostMessage(hDlg, WMU_DURATION_CHANGED, 0, 0);
2094          }
2095      }, hDlg, pData, requestTicks).detach();
2096  }
2097  
2098  //----------------------------------------------------------------------------
2099  //
2100  // Helper: Draw custom timeline with handles
2101  //
2102  //----------------------------------------------------------------------------
2103  static void DrawTimeline(HDC hdc, RECT rc, VideoRecordingSession::TrimDialogData* pData, UINT dpi)
2104  {
2105      const int width = rc.right - rc.left;
2106      const int height = rc.bottom - rc.top;
2107  
2108      // Scale constants for DPI
2109      const int timelinePadding = ScaleForDpi(kTimelinePadding, dpi);
2110      const int timelineTrackHeight = ScaleForDpi(kTimelineTrackHeight, dpi);
2111      const int timelineTrackTopOffset = ScaleForDpi(kTimelineTrackTopOffset, dpi);
2112      const int timelineHandleHalfWidth = ScaleForDpi(kTimelineHandleHalfWidth, dpi);
2113      const int timelineHandleHeight = ScaleForDpi(kTimelineHandleHeight, dpi);
2114  
2115      // Create memory DC for double buffering
2116      HDC hdcMem = CreateCompatibleDC(hdc);
2117      HBITMAP hbmMem = CreateCompatibleBitmap(hdc, width, height);
2118      HBITMAP hbmOld = static_cast<HBITMAP>(SelectObject(hdcMem, hbmMem));
2119  
2120      // Draw to memory DC - use dark mode colors if enabled
2121      const bool darkMode = IsDarkModeEnabled();
2122      HBRUSH hBackground = CreateSolidBrush(darkMode ? DarkMode::BackgroundColor : GetSysColor(COLOR_BTNFACE));
2123      RECT rcMem = { 0, 0, width, height };
2124      FillRect(hdcMem, &rcMem, hBackground);
2125      DeleteObject(hBackground);
2126  
2127      const int trackLeft = timelinePadding;
2128      const int trackRight = width - timelinePadding;
2129      const int trackTop = timelineTrackTopOffset;
2130      const int trackBottom = trackTop + timelineTrackHeight;
2131  
2132      RECT rcTrack = { trackLeft, trackTop, trackRight, trackBottom };
2133      HBRUSH hTrackBase = CreateSolidBrush(darkMode ? RGB(60, 60, 65) : RGB(214, 219, 224));
2134      FillRect(hdcMem, &rcTrack, hTrackBase);
2135      DeleteObject(hTrackBase);
2136  
2137      int startX = std::clamp(TimelineTimeToClientX(pData, pData->trimStart, width, dpi), trackLeft, trackRight);
2138      int endX = std::clamp(TimelineTimeToClientX(pData, pData->trimEnd, width, dpi), trackLeft, trackRight);
2139      if (endX < startX)
2140      {
2141          std::swap(startX, endX);
2142      }
2143  
2144      RECT rcBefore{ trackLeft, trackTop, startX, trackBottom };
2145      RECT rcAfter{ endX, trackTop, trackRight, trackBottom };
2146      HBRUSH hMuted = CreateSolidBrush(darkMode ? RGB(50, 50, 55) : RGB(198, 202, 206));
2147      FillRect(hdcMem, &rcBefore, hMuted);
2148      FillRect(hdcMem, &rcAfter, hMuted);
2149      DeleteObject(hMuted);
2150  
2151      RECT rcActive{ startX, trackTop, endX, trackBottom };
2152      HBRUSH hActive = CreateSolidBrush(RGB(90, 147, 250));
2153      FillRect(hdcMem, &rcActive, hActive);
2154      DeleteObject(hActive);
2155  
2156      HPEN hOutline = CreatePen(PS_SOLID, 1, darkMode ? RGB(80, 80, 85) : RGB(150, 150, 150));
2157      HPEN hOldPen = static_cast<HPEN>(SelectObject(hdcMem, hOutline));
2158      MoveToEx(hdcMem, trackLeft, trackTop, nullptr);
2159      LineTo(hdcMem, trackRight, trackTop);
2160      LineTo(hdcMem, trackRight, trackBottom);
2161      LineTo(hdcMem, trackLeft, trackBottom);
2162      LineTo(hdcMem, trackLeft, trackTop);
2163      SelectObject(hdcMem, hOldPen);
2164      DeleteObject(hOutline);
2165  
2166      const int trackWidth = trackRight - trackLeft;
2167      if (trackWidth > 0 && pData && pData->videoDuration.count() > 0)
2168      {
2169          const int tickTop = trackBottom + ScaleForDpi(2, dpi);
2170          const int tickMajorBottom = tickTop + ScaleForDpi(10, dpi);
2171          const int tickMinorBottom = tickTop + ScaleForDpi(6, dpi);
2172  
2173          const std::array<double, 5> fractions{ 0.0, 0.25, 0.5, 0.75, 1.0 };
2174          HPEN hTickPen = CreatePen(PS_SOLID, 1, darkMode ? RGB(100, 100, 105) : RGB(150, 150, 150));
2175          HPEN hOldTickPen = static_cast<HPEN>(SelectObject(hdcMem, hTickPen));
2176          SetBkMode(hdcMem, TRANSPARENT);
2177          SetTextColor(hdcMem, darkMode ? RGB(140, 140, 140) : RGB(80, 80, 80));
2178  
2179          // Use consistent font for all timeline text - scale for DPI (12pt)
2180          const int fontSize = -MulDiv(12, static_cast<int>(dpi), USER_DEFAULT_SCREEN_DPI);
2181          HFONT hTimelineFont = CreateFont(fontSize, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
2182              OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
2183              DEFAULT_PITCH | FF_SWISS, L"Segoe UI");
2184          HFONT hOldTimelineFont = static_cast<HFONT>(SelectObject(hdcMem, hTimelineFont));
2185  
2186          for (size_t i = 0; i < fractions.size(); ++i)
2187          {
2188              const double fraction = fractions[i];
2189              const int x = trackLeft + static_cast<int>(std::round(fraction * trackWidth));
2190              const bool isMajor = (fraction == 0.0) || (fraction == 0.5) || (fraction == 1.0);
2191              MoveToEx(hdcMem, x, tickTop, nullptr);
2192              LineTo(hdcMem, x, isMajor ? tickMajorBottom : tickMinorBottom);
2193  
2194              if (fraction > 0.0 && fraction < 1.0)
2195              {
2196                  // Calculate marker time within the full video duration (untrimmed)
2197                  const auto markerTime = winrt::TimeSpan{ static_cast<int64_t>(fraction * pData->videoDuration.count()) };
2198                  // For short videos (under 60 seconds), show fractional seconds to distinguish markers
2199                  const bool showMilliseconds = (pData->videoDuration.count() < 600000000LL); // 60 seconds in 100ns ticks
2200                  const std::wstring markerText = FormatTrimTime(markerTime, showMilliseconds);
2201                  const int markerHalfWidth = ScaleForDpi(showMilliseconds ? 45 : 35, dpi);
2202                  const int markerHeight = ScaleForDpi(26, dpi);
2203                  RECT rcMarker{ x - markerHalfWidth, tickMajorBottom + ScaleForDpi(10, dpi), x + markerHalfWidth, tickMajorBottom + ScaleForDpi(2, dpi) + markerHeight };
2204                  DrawText(hdcMem, markerText.c_str(), -1, &rcMarker, DT_CENTER | DT_TOP | DT_SINGLELINE | DT_NOPREFIX);
2205              }
2206          }
2207  
2208          SelectObject(hdcMem, hOldTimelineFont);
2209          DeleteObject(hTimelineFont);
2210          SelectObject(hdcMem, hOldTickPen);
2211          DeleteObject(hTickPen);
2212      }
2213  
2214      auto drawGripper = [&](int x)
2215      {
2216          RECT handleRect{
2217              x - timelineHandleHalfWidth,
2218              trackTop - (timelineHandleHeight - timelineTrackHeight) / 2,
2219              x + timelineHandleHalfWidth,
2220              trackTop - (timelineHandleHeight - timelineTrackHeight) / 2 + timelineHandleHeight
2221          };
2222  
2223          const COLORREF fillColor = darkMode ? RGB(165, 165, 165) : RGB(200, 200, 200);
2224          const COLORREF lineColor = darkMode ? RGB(90, 90, 90) : RGB(120, 120, 120);
2225          const int cornerRadius = (std::max)(ScaleForDpi(6, dpi), timelineHandleHalfWidth);
2226          const int lineInset = ScaleForDpi(6, dpi);
2227          const int lineWidth = (std::max)(1, ScaleForDpi(2, dpi));
2228  
2229          HBRUSH hFill = CreateSolidBrush(fillColor);
2230          HPEN hNullPen = static_cast<HPEN>(SelectObject(hdcMem, GetStockObject(NULL_PEN)));
2231          HBRUSH hOldBrush2 = static_cast<HBRUSH>(SelectObject(hdcMem, hFill));
2232          RoundRect(hdcMem, handleRect.left, handleRect.top, handleRect.right, handleRect.bottom, cornerRadius, cornerRadius);
2233          SelectObject(hdcMem, hOldBrush2);
2234          SelectObject(hdcMem, hNullPen);
2235          DeleteObject(hFill);
2236  
2237          // Dark vertical line in the middle.
2238          HPEN hLinePen = CreatePen(PS_SOLID, lineWidth, lineColor);
2239          HPEN hOldLinePen = static_cast<HPEN>(SelectObject(hdcMem, hLinePen));
2240          const int y1 = handleRect.top + lineInset;
2241          const int y2 = handleRect.bottom - lineInset;
2242          MoveToEx(hdcMem, x, y1, nullptr);
2243          LineTo(hdcMem, x, y2);
2244          SelectObject(hdcMem, hOldLinePen);
2245          DeleteObject(hLinePen);
2246      };
2247  
2248      drawGripper(startX);
2249      drawGripper(endX);
2250  
2251      const int posX = std::clamp(TimelineTimeToClientX(pData, pData->currentPosition, width, dpi), trackLeft, trackRight);
2252      const int posLineWidth = ScaleForDpi(2, dpi);
2253      const int posLineExtend = ScaleForDpi(12, dpi);
2254      const int posLineBelow = ScaleForDpi(22, dpi);
2255      HPEN hPositionPen = CreatePen(PS_SOLID, posLineWidth, RGB(33, 150, 243));
2256      hOldPen = static_cast<HPEN>(SelectObject(hdcMem, hPositionPen));
2257      MoveToEx(hdcMem, posX, trackTop - posLineExtend, nullptr);
2258      LineTo(hdcMem, posX, trackBottom + posLineBelow);
2259      SelectObject(hdcMem, hOldPen);
2260      DeleteObject(hPositionPen);
2261  
2262      const int ellipseRadius = ScaleForDpi(6, dpi);
2263      const int ellipseTop = ScaleForDpi(12, dpi);
2264      const int ellipseBottom = ScaleForDpi(24, dpi);
2265      HBRUSH hPositionBrush = CreateSolidBrush(RGB(33, 150, 243));
2266      HBRUSH hOldBrush = static_cast<HBRUSH>(SelectObject(hdcMem, hPositionBrush));
2267      HPEN hOldPenForEllipse = static_cast<HPEN>(SelectObject(hdcMem, GetStockObject(NULL_PEN)));
2268      Ellipse(hdcMem, posX - ellipseRadius, trackBottom + ellipseTop, posX + ellipseRadius, trackBottom + ellipseBottom);
2269      SelectObject(hdcMem, hOldPenForEllipse);
2270      SelectObject(hdcMem, hOldBrush);
2271      DeleteObject(hPositionBrush);
2272  
2273      // Set font for start/end labels (same font used for tick labels - 12pt)
2274      SetBkMode(hdcMem, TRANSPARENT);
2275      SetTextColor(hdcMem, darkMode ? RGB(140, 140, 140) : RGB(80, 80, 80));
2276      int labelFontSize = -MulDiv(12, static_cast<int>(dpi), USER_DEFAULT_SCREEN_DPI);
2277      HFONT hFont = CreateFont(labelFontSize, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
2278          OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
2279          DEFAULT_PITCH | FF_SWISS, L"Segoe UI");
2280      HFONT hOldFont = static_cast<HFONT>(SelectObject(hdcMem, hFont));
2281  
2282      // Align with intermediate marker labels: use same calculation as rcMarker
2283      // tickTop = trackBottom + 10, tickMajorBottom = tickTop + 10, marker starts at tickMajorBottom + 2
2284      const int tickTopForLabels = trackBottom + ScaleForDpi(10, dpi);
2285      const int tickMajorBottomForLabels = tickTopForLabels + ScaleForDpi(10, dpi);
2286      int labelTop = tickMajorBottomForLabels + ScaleForDpi(2, dpi);
2287      int labelBottom = labelTop + ScaleForDpi(26, dpi);
2288      // For short videos (under 60 seconds), show fractional seconds
2289      const bool showMilliseconds = (pData->videoDuration.count() < 600000000LL); // 60 seconds in 100ns ticks
2290      int labelWidth = ScaleForDpi(showMilliseconds ? 80 : 70, dpi);
2291      // Start label: draw to the right of trackLeft (left-aligned)
2292      RECT rcStartLabel{ trackLeft, labelTop, trackLeft + labelWidth, labelBottom };
2293      const std::wstring startLabel = FormatTrimTime(pData->trimStart, showMilliseconds);
2294      DrawText(hdcMem, startLabel.c_str(), -1, &rcStartLabel, DT_LEFT | DT_TOP | DT_SINGLELINE);
2295  
2296      // End label: draw to the left of trackRight (right-aligned)
2297      RECT rcEndLabel{ trackRight - labelWidth, labelTop, trackRight, labelBottom };
2298      const std::wstring endLabel = FormatTrimTime(pData->trimEnd, showMilliseconds);
2299      DrawText(hdcMem, endLabel.c_str(), -1, &rcEndLabel, DT_RIGHT | DT_TOP | DT_SINGLELINE);
2300  
2301      SelectObject(hdcMem, hOldFont);
2302      DeleteObject(hFont);
2303  
2304      // Copy the buffered image to the screen
2305      BitBlt(hdc, rc.left, rc.top, width, height, hdcMem, 0, 0, SRCCOPY);
2306  
2307      // Clean up
2308      SelectObject(hdcMem, hbmOld);
2309      DeleteObject(hbmMem);
2310      DeleteDC(hdcMem);
2311  }
2312  
2313  //----------------------------------------------------------------------------
2314  //
2315  // Helper: Mouse interaction for the trim timeline
2316  //
2317  //----------------------------------------------------------------------------
2318  namespace
2319  {
2320      constexpr UINT_PTR kPlaybackTimerId = 1;
2321      constexpr UINT kPlaybackTimerIntervalMs = 16;  // Fallback for GIF; MP4 uses multimedia timer
2322      constexpr int64_t kPlaybackStepTicks = static_cast<int64_t>(kPlaybackTimerIntervalMs) * 10'000;
2323      constexpr UINT WMU_MM_TIMER_TICK = WM_USER + 10;  // Posted by multimedia timer callback
2324      constexpr UINT kMMTimerIntervalMs = 8;  // 8ms for ~120Hz update rate
2325  }
2326  
2327  // Multimedia timer callback - runs in a separate thread, just posts a message
2328  static void CALLBACK MMTimerCallback(UINT /*uTimerID*/, UINT /*uMsg*/, DWORD_PTR dwUser, DWORD_PTR /*dw1*/, DWORD_PTR /*dw2*/)
2329  {
2330      HWND hDlg = reinterpret_cast<HWND>(dwUser);
2331      if (hDlg && IsWindow(hDlg))
2332      {
2333          PostMessage(hDlg, WMU_MM_TIMER_TICK, 0, 0);
2334      }
2335  }
2336  
2337  static void StopMMTimer(VideoRecordingSession::TrimDialogData* pData)
2338  {
2339      if (pData && pData->mmTimerId != 0)
2340      {
2341          timeKillEvent(pData->mmTimerId);
2342          pData->mmTimerId = 0;
2343      }
2344  }
2345  
2346  static bool StartMMTimer(HWND hDlg, VideoRecordingSession::TrimDialogData* pData)
2347  {
2348      if (!pData || !hDlg)
2349      {
2350          return false;
2351      }
2352  
2353      StopMMTimer(pData);
2354  
2355      pData->mmTimerId = timeSetEvent(
2356          kMMTimerIntervalMs,
2357          1,  // 1ms resolution
2358          MMTimerCallback,
2359          reinterpret_cast<DWORD_PTR>(hDlg),
2360          TIME_PERIODIC | TIME_KILL_SYNCHRONOUS);
2361  
2362      return pData->mmTimerId != 0;
2363  }
2364  
2365  static void RefreshPlaybackButtons(HWND hDlg)
2366  {
2367      if (!hDlg)
2368      {
2369          return;
2370      }
2371  
2372      InvalidateRect(GetDlgItem(hDlg, IDC_TRIM_SKIP_START), nullptr, FALSE);
2373      InvalidateRect(GetDlgItem(hDlg, IDC_TRIM_REWIND), nullptr, FALSE);
2374      InvalidateRect(GetDlgItem(hDlg, IDC_TRIM_PLAY_PAUSE), nullptr, FALSE);
2375      InvalidateRect(GetDlgItem(hDlg, IDC_TRIM_FORWARD), nullptr, FALSE);
2376      InvalidateRect(GetDlgItem(hDlg, IDC_TRIM_SKIP_END), nullptr, FALSE);
2377  }
2378  
2379  static void HandlePlaybackCommand(int controlId, VideoRecordingSession::TrimDialogData* pData)
2380  {
2381      if (!pData || !pData->hDialog)
2382      {
2383          return;
2384      }
2385  
2386      HWND hDlg = pData->hDialog;
2387  
2388      // Helper lambda to invalidate cached start frame when position changes
2389      auto invalidateCachedFrame = [pData]()
2390      {
2391          std::lock_guard<std::mutex> lock(pData->previewBitmapMutex);
2392          if (pData->hCachedStartFrame)
2393          {
2394              DeleteObject(pData->hCachedStartFrame);
2395              pData->hCachedStartFrame = nullptr;
2396          }
2397      };
2398  
2399      switch (controlId)
2400      {
2401      case IDC_TRIM_PLAY_PAUSE:
2402          if (pData->isPlaying.load(std::memory_order_relaxed))
2403          {
2404              StopPlayback(hDlg, pData, true);
2405          }
2406          else
2407          {
2408              // Always start playback from current time selector position
2409              pData->playbackStartPosition = pData->currentPosition;
2410              pData->playbackStartPositionValid = true;
2411              invalidateCachedFrame();
2412              StartPlaybackAsync(hDlg, pData);
2413          }
2414          break;
2415  
2416      case IDC_TRIM_REWIND:
2417      {
2418          StopPlayback(hDlg, pData, false);
2419          // Use 1 second step for timelines < 20 seconds, 2 seconds 
2420          const int64_t duration = pData->trimEnd.count() - pData->trimStart.count();
2421          const int64_t stepTicks = (duration < 200'000'000) ? 10'000'000 : kJogStepTicks;
2422          const int64_t newTicks = (std::max)(pData->trimStart.count(), pData->currentPosition.count() - stepTicks);
2423          pData->currentPosition = winrt::TimeSpan{ newTicks };
2424          pData->playbackStartPosition = pData->currentPosition;
2425          pData->playbackStartPositionValid = true;
2426          invalidateCachedFrame();
2427          SyncMediaPlayerPosition(pData);
2428          UpdateVideoPreview(hDlg, pData);
2429          break;
2430      }
2431  
2432      case IDC_TRIM_FORWARD:
2433      {
2434          StopPlayback(hDlg, pData, false);
2435          // Use 1 second step for timelines < 20 seconds, 2 seconds 
2436          const int64_t duration = pData->trimEnd.count() - pData->trimStart.count();
2437          const int64_t stepTicks = (duration < 200'000'000) ? 10'000'000 : kJogStepTicks;
2438          const int64_t newTicks = (std::min)(pData->trimEnd.count(), pData->currentPosition.count() + stepTicks);
2439          pData->currentPosition = winrt::TimeSpan{ newTicks };
2440          pData->playbackStartPosition = pData->currentPosition;
2441          pData->playbackStartPositionValid = true;
2442          invalidateCachedFrame();
2443          SyncMediaPlayerPosition(pData);
2444          UpdateVideoPreview(hDlg, pData);
2445          break;
2446      }
2447  
2448      case IDC_TRIM_SKIP_END:
2449      {
2450          StopPlayback(hDlg, pData, false);
2451          pData->currentPosition = pData->trimEnd;
2452          pData->playbackStartPosition = pData->currentPosition;
2453          pData->playbackStartPositionValid = true;
2454          invalidateCachedFrame();
2455          SyncMediaPlayerPosition(pData);
2456          UpdateVideoPreview(hDlg, pData);
2457          break;
2458      }
2459  
2460      default:
2461          StopPlayback(hDlg, pData, false);
2462          pData->currentPosition = pData->trimStart;
2463          pData->playbackStartPosition = pData->currentPosition;
2464          pData->playbackStartPositionValid = true;
2465          invalidateCachedFrame();
2466          SyncMediaPlayerPosition(pData);
2467          UpdateVideoPreview(hDlg, pData);
2468          break;
2469      }
2470  
2471      RefreshPlaybackButtons(hDlg);
2472  }
2473  
2474  static void StopPlayback(HWND hDlg, VideoRecordingSession::TrimDialogData* pData, bool capturePosition)
2475  {
2476      if (!pData)
2477      {
2478          return;
2479      }
2480  
2481      // Invalidate any in-flight StartPlaybackAsync continuation (e.g., after awaiting file load).
2482      pData->playbackCommandSerial.fetch_add(1, std::memory_order_acq_rel);
2483  
2484      const bool wasPlaying = pData->isPlaying.exchange(false, std::memory_order_acq_rel);
2485      ResetSmoothPlayback(pData);
2486  
2487      // Cancel any pending initial seek suppression.
2488      pData->pendingInitialSeek.store(false, std::memory_order_relaxed);
2489      pData->pendingInitialSeekTicks.store(0, std::memory_order_relaxed);
2490  
2491      // Stop audio playback and align media position with UI state, but keep player alive for resume
2492      if (pData->mediaPlayer)
2493      {
2494          try
2495          {
2496              auto session = pData->mediaPlayer.PlaybackSession();
2497              if (session)
2498              {
2499                  if (capturePosition)
2500                  {
2501                      pData->currentPosition = session.Position();
2502                  }
2503                  session.Position(pData->currentPosition);
2504              }
2505              pData->mediaPlayer.Pause();
2506          }
2507          catch (...)
2508          {
2509          }
2510      }
2511  
2512      if (hDlg)
2513      {
2514          if (wasPlaying)
2515          {
2516              StopMMTimer(pData);  // Stop multimedia timer for MP4
2517              KillTimer(hDlg, kPlaybackTimerId);  // Stop regular timer for GIF
2518          }
2519          RefreshPlaybackButtons(hDlg);
2520      }
2521  }
2522  
2523  static winrt::fire_and_forget StartPlaybackAsync(HWND hDlg, VideoRecordingSession::TrimDialogData* pData)
2524  {
2525      if (!pData || !hDlg)
2526      {
2527          co_return;
2528      }
2529  
2530      if (pData->trimEnd.count() <= pData->trimStart.count())
2531      {
2532          co_return;
2533      }
2534  
2535      ResetSmoothPlayback(pData);
2536  
2537      // If playhead is at/past selection end, restart from trimStart.
2538      if (pData->currentPosition.count() >= pData->trimEnd.count())
2539      {
2540          pData->currentPosition = pData->trimStart;
2541          UpdateVideoPreview(hDlg, pData);
2542      }
2543  
2544      // Capture resume position (where playback should start/resume from).
2545      const auto resumePosition = pData->currentPosition;
2546  
2547      // Suppress the brief Position==0 report before the initial seek takes effect.
2548      pData->pendingInitialSeek.store(resumePosition.count() > 0, std::memory_order_relaxed);
2549      pData->pendingInitialSeekTicks.store(resumePosition.count(), std::memory_order_relaxed);
2550  
2551      // Capture loop anchor only if not already set by an explicit user positioning.
2552      // This keeps the loop point stable across pause/resume.
2553      if (!pData->playbackStartPositionValid)
2554      {
2555          pData->playbackStartPosition = resumePosition;
2556          pData->playbackStartPositionValid = true;
2557      }
2558  
2559      // Cache the current preview frame for instant restore when playback stops.
2560      // Only cache if we have a valid preview and it matches the playback start position.
2561      {
2562          std::lock_guard<std::mutex> lock(pData->previewBitmapMutex);
2563          // Clear any previous cached frame
2564          if (pData->hCachedStartFrame)
2565          {
2566              DeleteObject(pData->hCachedStartFrame);
2567              pData->hCachedStartFrame = nullptr;
2568          }
2569          // Cache if we have a valid preview at the current position
2570          if (pData->hPreviewBitmap && pData->lastRenderedPreview.load(std::memory_order_relaxed) >= 0)
2571          {
2572              // Duplicate the bitmap so we have our own copy
2573              BITMAP bm{};
2574              if (GetObject(pData->hPreviewBitmap, sizeof(bm), &bm))
2575              {
2576                  HDC hdcScreen = GetDC(nullptr);
2577                  HDC hdcSrc = CreateCompatibleDC(hdcScreen);
2578                  HDC hdcDst = CreateCompatibleDC(hdcScreen);
2579                  HBITMAP hCopy = CreateCompatibleBitmap(hdcScreen, bm.bmWidth, bm.bmHeight);
2580                  if (hCopy)
2581                  {
2582                      HBITMAP hOldSrc = static_cast<HBITMAP>(SelectObject(hdcSrc, pData->hPreviewBitmap));
2583                      HBITMAP hOldDst = static_cast<HBITMAP>(SelectObject(hdcDst, hCopy));
2584                      BitBlt(hdcDst, 0, 0, bm.bmWidth, bm.bmHeight, hdcSrc, 0, 0, SRCCOPY);
2585                      SelectObject(hdcSrc, hOldSrc);
2586                      SelectObject(hdcDst, hOldDst);
2587                      pData->hCachedStartFrame = hCopy;
2588                      pData->cachedStartFramePosition = pData->playbackStartPosition;
2589                  }
2590                  DeleteDC(hdcSrc);
2591                  DeleteDC(hdcDst);
2592                  ReleaseDC(nullptr, hdcScreen);
2593              }
2594          }
2595      }
2596  
2597  #if _DEBUG
2598      OutputDebugStringW((L"[Trim] StartPlayback: currentPos=" + std::to_wstring(pData->currentPosition.count()) +
2599          L" playbackStartPos=" + std::to_wstring(pData->playbackStartPosition.count()) +
2600          L" trimStart=" + std::to_wstring(pData->trimStart.count()) +
2601          L" trimEnd=" + std::to_wstring(pData->trimEnd.count()) + L"\n").c_str());
2602  #endif
2603  
2604      bool expected = false;
2605      if (!pData->isPlaying.compare_exchange_strong(expected, true, std::memory_order_relaxed))
2606      {
2607          co_return;
2608      }
2609  
2610      const uint64_t startSerial = pData->playbackCommandSerial.fetch_add(1, std::memory_order_acq_rel) + 1;
2611  
2612      if (pData->isGif)
2613      {
2614          // Initialize GIF timing so playback begins at the current playhead position
2615          // (not at the start of the containing frame).
2616          auto now = std::chrono::steady_clock::now();
2617          if (!pData->gifFrames.empty() && pData->videoDuration.count() > 0)
2618          {
2619              const int64_t clampedTicks = std::clamp<int64_t>(resumePosition.count(), 0, pData->videoDuration.count());
2620              const size_t frameIndex = FindGifFrameIndex(pData->gifFrames, clampedTicks);
2621              const auto& frame = pData->gifFrames[frameIndex];
2622              const int64_t offsetTicks = std::clamp<int64_t>(clampedTicks - frame.start.count(), 0, frame.duration.count());
2623              const auto offsetMs = std::chrono::milliseconds(offsetTicks / 10'000);
2624              pData->gifFrameStartTime = now - offsetMs;
2625          }
2626          else
2627          {
2628              pData->gifFrameStartTime = now;
2629          }
2630  
2631          // Update lastPlayheadX to current position so timer ticks can track movement properly
2632          {
2633              HWND hTimeline = GetDlgItem(hDlg, IDC_TRIM_TIMELINE);
2634              if (hTimeline)
2635              {
2636                  RECT rc;
2637                  GetClientRect(hTimeline, &rc);
2638                  const UINT dpi = GetDpiForWindowHelper(hTimeline);
2639                  pData->lastPlayheadX = TimelineTimeToClientX(pData, pData->currentPosition, rc.right - rc.left, dpi);
2640              }
2641          }
2642  
2643          // Use multimedia timer for smooth GIF playback
2644          if (!StartMMTimer(hDlg, pData))
2645          {
2646              pData->isPlaying.store(false, std::memory_order_relaxed);
2647              RefreshPlaybackButtons(hDlg);
2648              co_return;
2649          }
2650  
2651          PostMessage(hDlg, WMU_PLAYBACK_POSITION, 0, 0);
2652          RefreshPlaybackButtons(hDlg);
2653          co_return;
2654      }
2655  
2656      // If a player already exists (paused), resume from the current playhead position.
2657      if (pData->mediaPlayer)
2658      {
2659          // If the user already canceled playback, do nothing.
2660          if (!pData->isPlaying.load(std::memory_order_acquire) ||
2661              pData->playbackCommandSerial.load(std::memory_order_acquire) != startSerial)
2662          {
2663              pData->isPlaying.store(false, std::memory_order_relaxed);
2664              RefreshPlaybackButtons(hDlg);
2665              co_return;
2666          }
2667  
2668          try
2669          {
2670              auto session = pData->mediaPlayer.PlaybackSession();
2671              if (session)
2672              {
2673                  // Resume from the current playhead position (do not change the loop anchor)
2674                  const int64_t clampedTicks = std::clamp<int64_t>(resumePosition.count(), 0, pData->trimEnd.count());
2675                  session.Position(winrt::TimeSpan{ clampedTicks });
2676                  pData->currentPosition = winrt::TimeSpan{ clampedTicks };
2677                  // Defer smoothing until the first real media sample to avoid extrapolating from zero
2678                  pData->smoothActive.store(false, std::memory_order_relaxed);
2679                  pData->smoothHasNonZeroSample.store(false, std::memory_order_relaxed);
2680              }
2681              pData->mediaPlayer.Play();
2682          }
2683          catch (...)
2684          {
2685          }
2686  
2687          // Use multimedia timer for smooth updates
2688          if (!StartMMTimer(hDlg, pData))
2689          {
2690              pData->isPlaying.store(false, std::memory_order_relaxed);
2691              ResetSmoothPlayback(pData);
2692              RefreshPlaybackButtons(hDlg);
2693              co_return;
2694          }
2695  
2696          // Update lastPlayheadX to current position so timer ticks can track movement properly
2697          {
2698              HWND hTimeline = GetDlgItem(hDlg, IDC_TRIM_TIMELINE);
2699              if (hTimeline)
2700              {
2701                  RECT rc;
2702                  GetClientRect(hTimeline, &rc);
2703                  const UINT dpi = GetDpiForWindowHelper(hTimeline);
2704                  pData->lastPlayheadX = TimelineTimeToClientX(pData, pData->currentPosition, rc.right - rc.left, dpi);
2705              }
2706          }
2707  
2708          PostMessage(hDlg, WMU_PLAYBACK_POSITION, 0, 0);
2709          RefreshPlaybackButtons(hDlg);
2710          co_return;
2711      }
2712  
2713      CleanupMediaPlayer(pData);
2714  
2715      winrt::MediaPlayer newPlayer{ nullptr };
2716  
2717      try
2718      {
2719          if (!pData->playbackFile)
2720          {
2721              auto file = co_await winrt::StorageFile::GetFileFromPathAsync(pData->videoPath);
2722              pData->playbackFile = file;
2723          }
2724  
2725          // The user may have clicked Pause while the async file lookup was in-flight.
2726          if (!pData->isPlaying.load(std::memory_order_acquire) ||
2727              pData->playbackCommandSerial.load(std::memory_order_acquire) != startSerial)
2728          {
2729              pData->isPlaying.store(false, std::memory_order_relaxed);
2730              RefreshPlaybackButtons(hDlg);
2731              co_return;
2732          }
2733  
2734          if (!pData->playbackFile)
2735          {
2736              throw winrt::hresult_error(E_FAIL);
2737          }
2738  
2739          newPlayer = winrt::MediaPlayer();
2740          newPlayer.AudioCategory(winrt::MediaPlayerAudioCategory::Media);
2741          newPlayer.IsVideoFrameServerEnabled(true);
2742          newPlayer.AutoPlay(false);
2743          newPlayer.Volume(pData->volume);
2744          newPlayer.IsMuted(pData->volume == 0.0);
2745  
2746          pData->frameCopyInProgress.store(false, std::memory_order_relaxed);
2747          pData->mediaPlayer = newPlayer;
2748  
2749          auto mediaSource = winrt::MediaSource::CreateFromStorageFile(pData->playbackFile);
2750          VideoRecordingSession::TrimDialogData* dataPtr = pData;
2751  
2752          pData->frameAvailableToken = pData->mediaPlayer.VideoFrameAvailable([hDlg, dataPtr](auto const& sender, auto const&)
2753          {
2754              if (!dataPtr)
2755              {
2756                  return;
2757              }
2758  
2759              if (dataPtr->frameCopyInProgress.exchange(true, std::memory_order_relaxed))
2760              {
2761                  return;
2762              }
2763  
2764              try
2765              {
2766                  if (!EnsurePlaybackDevice(dataPtr))
2767                  {
2768                      dataPtr->frameCopyInProgress.store(false, std::memory_order_relaxed);
2769                      return;
2770                  }
2771  
2772                  auto session = sender.PlaybackSession();
2773                  UINT width = session.NaturalVideoWidth();
2774                  UINT height = session.NaturalVideoHeight();
2775                  if (width == 0 || height == 0)
2776                  {
2777                      width = 640;
2778                      height = 360;
2779                  }
2780  
2781                  if (!EnsureFrameTextures(dataPtr, width, height))
2782                  {
2783                      dataPtr->frameCopyInProgress.store(false, std::memory_order_relaxed);
2784                      return;
2785                  }
2786  
2787                  winrt::com_ptr<IDXGISurface> dxgiSurface;
2788                  if (dataPtr->previewFrameTexture)
2789                  {
2790                      dxgiSurface = dataPtr->previewFrameTexture.as<IDXGISurface>();
2791                  }
2792  
2793                  if (dxgiSurface)
2794                  {
2795                      winrt::com_ptr<IInspectable> inspectableSurface;
2796                      if (SUCCEEDED(CreateDirect3D11SurfaceFromDXGISurface(dxgiSurface.get(), inspectableSurface.put())))
2797                      {
2798                          auto surface = inspectableSurface.as<winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DSurface>();
2799                          sender.CopyFrameToVideoSurface(surface);
2800  
2801                          if (dataPtr->previewD3DContext && dataPtr->previewFrameStaging)
2802                          {
2803                              dataPtr->previewD3DContext->CopyResource(dataPtr->previewFrameStaging.get(), dataPtr->previewFrameTexture.get());
2804  
2805                              D3D11_MAPPED_SUBRESOURCE mapped{};
2806                              if (SUCCEEDED(dataPtr->previewD3DContext->Map(dataPtr->previewFrameStaging.get(), 0, D3D11_MAP_READ, 0, &mapped)))
2807                              {
2808                                  const UINT rowPitch = mapped.RowPitch;
2809                                  const UINT bytesPerPixel = 4;
2810                                  const UINT destStride = width * bytesPerPixel;
2811  
2812                                  BITMAPINFO bmi{};
2813                                  bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
2814                                  bmi.bmiHeader.biWidth = static_cast<LONG>(width);
2815                                  bmi.bmiHeader.biHeight = -static_cast<LONG>(height);
2816                                  bmi.bmiHeader.biPlanes = 1;
2817                                  bmi.bmiHeader.biBitCount = 32;
2818                                  bmi.bmiHeader.biCompression = BI_RGB;
2819  
2820                                  void* bits = nullptr;
2821                                  HDC hdcScreen = GetDC(nullptr);
2822                                  HBITMAP hBitmap = CreateDIBSection(hdcScreen, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
2823                                  ReleaseDC(nullptr, hdcScreen);
2824  
2825                                  if (hBitmap && bits)
2826                                  {
2827                                      BYTE* dest = static_cast<BYTE*>(bits);
2828                                      const BYTE* src = static_cast<const BYTE*>(mapped.pData);
2829                                      for (UINT y = 0; y < height; ++y)
2830                                      {
2831                                          memcpy(dest + static_cast<size_t>(y) * destStride, src + static_cast<size_t>(y) * rowPitch, destStride);
2832                                      }
2833  
2834                                      {
2835                                          std::lock_guard<std::mutex> lock(dataPtr->previewBitmapMutex);
2836                                          if (dataPtr->hPreviewBitmap && dataPtr->previewBitmapOwned)
2837                                          {
2838                                              DeleteObject(dataPtr->hPreviewBitmap);
2839                                          }
2840                                          dataPtr->hPreviewBitmap = hBitmap;
2841                                          dataPtr->previewBitmapOwned = true;
2842                                      }
2843  
2844                                      PostMessage(hDlg, WMU_PREVIEW_READY, 0, 0);
2845                                  }
2846                                  else if (hBitmap)
2847                                  {
2848                                      DeleteObject(hBitmap);
2849                                  }
2850  
2851                                  dataPtr->previewD3DContext->Unmap(dataPtr->previewFrameStaging.get(), 0);
2852                              }
2853                          }
2854                      }
2855                  }
2856              }
2857              catch (...)
2858              {
2859              }
2860  
2861              dataPtr->frameCopyInProgress.store(false, std::memory_order_relaxed);
2862          });
2863  
2864          auto session = pData->mediaPlayer.PlaybackSession();
2865          pData->positionChangedToken = session.PositionChanged([hDlg, dataPtr](auto const& sender, auto const&)
2866          {
2867              if (!dataPtr)
2868              {
2869                  return;
2870              }
2871  
2872              try
2873              {
2874                  // When not playing, ignore media callbacks so UI-driven seeks remain authoritative.
2875                  if (!dataPtr->isPlaying.load(std::memory_order_relaxed))
2876                  {
2877                      return;
2878                  }
2879  
2880                  auto pos = sender.Position();
2881  
2882                  // Suppress the transient 0-position report before the initial seek takes effect.
2883                  if (dataPtr->pendingInitialSeek.load(std::memory_order_relaxed) &&
2884                      dataPtr->pendingInitialSeekTicks.load(std::memory_order_relaxed) > 0 &&
2885                      pos.count() == 0)
2886                  {
2887                      return;
2888                  }
2889  
2890                  // First non-zero sample observed; allow normal updates.
2891                  if (pos.count() != 0)
2892                  {
2893                      dataPtr->pendingInitialSeek.store(false, std::memory_order_relaxed);
2894                      dataPtr->pendingInitialSeekTicks.store(0, std::memory_order_relaxed);
2895                  }
2896  
2897                  // Check for end-of-clip BEFORE updating currentPosition to avoid
2898                  // storing a value >= trimEnd that could flash in the UI
2899                  if (pos >= dataPtr->trimEnd)
2900                  {
2901                      // Immediately mark as not playing to prevent further position updates
2902                      // before WMU_PLAYBACK_STOP is processed.
2903                      dataPtr->isPlaying.store(false, std::memory_order_release);
2904  #if _DEBUG
2905                      OutputDebugStringW((L"[Trim] PositionChanged: pos >= trimEnd, posting stop. pos=" +
2906                          std::to_wstring(pos.count()) + L"\n").c_str());
2907  #endif
2908                      PostMessage(hDlg, WMU_PLAYBACK_STOP, 0, 0);
2909                      return;
2910                  }
2911  
2912                  dataPtr->currentPosition = pos;
2913  
2914                  if (dataPtr->isPlaying.load(std::memory_order_relaxed) &&
2915                      !dataPtr->smoothHasNonZeroSample.load(std::memory_order_relaxed) &&
2916                      pos.count() > 0)
2917                  {
2918                      // Seed smoothing on first real position, but keep baseline exact to avoid a jump
2919                      dataPtr->smoothHasNonZeroSample.store(true, std::memory_order_relaxed);
2920                      SyncSmoothPlayback(dataPtr, pos.count(), dataPtr->trimStart.count(), dataPtr->trimEnd.count());
2921                      LogSmoothingEvent(L"eventFirst", pos.count(), pos.count(), 0);
2922                  }
2923  
2924                  PostMessage(hDlg, WMU_PLAYBACK_POSITION, 0, 0);
2925              }
2926              catch (...)
2927              {
2928              }
2929          });
2930  
2931          pData->stateChangedToken = session.PlaybackStateChanged([hDlg](auto const&, auto const&)
2932          {
2933              PostMessage(hDlg, WMU_PLAYBACK_POSITION, 0, 0);
2934          });
2935  
2936          // Capture the resume position now since currentPosition may change before MediaOpened fires
2937          const int64_t resumePositionTicks = std::clamp<int64_t>(resumePosition.count(), 0, pData->trimEnd.count());
2938  #if _DEBUG
2939          OutputDebugStringW((L"[Trim] Setting up MediaOpened callback with resumePos=" +
2940              std::to_wstring(resumePositionTicks) + L"\n").c_str());
2941  #endif
2942          pData->mediaPlayer.MediaOpened([dataPtr, hDlg, resumePositionTicks, startSerial](auto const& sender, auto const&)
2943          {
2944              if (!dataPtr)
2945              {
2946                  return;
2947              }
2948              try
2949              {
2950                  if (!dataPtr->isPlaying.load(std::memory_order_acquire) ||
2951                      dataPtr->playbackCommandSerial.load(std::memory_order_acquire) != startSerial)
2952                  {
2953                      sender.Pause();
2954                      return;
2955                  }
2956                  // Seek to the captured resume position (loop anchor is stored separately)
2957  #if _DEBUG
2958                  OutputDebugStringW((L"[Trim] MediaOpened: seeking to resumePos=" +
2959                      std::to_wstring(resumePositionTicks) + L"\n").c_str());
2960  #endif
2961                  sender.PlaybackSession().Position(winrt::TimeSpan{ resumePositionTicks });
2962  
2963                  // Re-check immediately before playing to reduce Play->Pause races.
2964                  if (!dataPtr->isPlaying.load(std::memory_order_acquire) ||
2965                      dataPtr->playbackCommandSerial.load(std::memory_order_acquire) != startSerial)
2966                  {
2967                      sender.Pause();
2968                      return;
2969                  }
2970                  sender.Play();
2971  
2972                  // Once MediaOpened has applied the initial seek, allow position updates again.
2973                  dataPtr->pendingInitialSeek.store(false, std::memory_order_relaxed);
2974                  dataPtr->pendingInitialSeekTicks.store(0, std::memory_order_relaxed);
2975              }
2976              catch (...)
2977              {
2978              }
2979          });
2980  
2981          pData->mediaPlayer.Source(mediaSource);
2982      }
2983      catch (...)
2984      {
2985          pData->isPlaying.store(false, std::memory_order_relaxed);
2986          CleanupMediaPlayer(pData);
2987          if (newPlayer)
2988          {
2989              try
2990              {
2991                  newPlayer.Close();
2992              }
2993              catch (...)
2994              {
2995              }
2996          }
2997          RefreshPlaybackButtons(hDlg);
2998          co_return;
2999      }
3000  
3001      // Use multimedia timer for smooth updates
3002      if (!StartMMTimer(hDlg, pData))
3003      {
3004          pData->isPlaying.store(false, std::memory_order_relaxed);
3005          CleanupMediaPlayer(pData);
3006          ResetSmoothPlayback(pData);
3007          RefreshPlaybackButtons(hDlg);
3008          co_return;
3009      }
3010  
3011      // If a quick Pause happened right after Play, don't start timers/UI updates.
3012      if (!pData->isPlaying.load(std::memory_order_acquire) ||
3013          pData->playbackCommandSerial.load(std::memory_order_acquire) != startSerial)
3014      {
3015          StopMMTimer(pData);
3016          pData->isPlaying.store(false, std::memory_order_relaxed);
3017          RefreshPlaybackButtons(hDlg);
3018          co_return;
3019      }
3020  
3021      // Defer smoothing until first real playback position is reported to prevent early extrapolation
3022      pData->smoothActive.store(false, std::memory_order_relaxed);
3023      pData->smoothHasNonZeroSample.store(false, std::memory_order_relaxed);
3024  
3025      // Update lastPlayheadX to current position so timer ticks can track movement properly
3026      {
3027          HWND hTimeline = GetDlgItem(hDlg, IDC_TRIM_TIMELINE);
3028          if (hTimeline)
3029          {
3030              RECT rc;
3031              GetClientRect(hTimeline, &rc);
3032              const UINT dpi = GetDpiForWindowHelper(hTimeline);
3033              pData->lastPlayheadX = TimelineTimeToClientX(pData, pData->currentPosition, rc.right - rc.left, dpi);
3034          }
3035      }
3036  
3037      PostMessage(hDlg, WMU_PLAYBACK_POSITION, 0, 0);
3038      RefreshPlaybackButtons(hDlg);
3039  }
3040  
3041  static LRESULT CALLBACK TimelineSubclassProc(
3042      HWND hWnd,
3043      UINT message,
3044      WPARAM wParam,
3045      LPARAM lParam,
3046      UINT_PTR uIdSubclass,
3047      DWORD_PTR dwRefData)
3048  {
3049      auto* pData = reinterpret_cast<VideoRecordingSession::TrimDialogData*>(dwRefData);
3050      if (!pData)
3051      {
3052          return DefSubclassProc(hWnd, message, wParam, lParam);
3053      }
3054  
3055      auto restorePreviewIfNeeded = [&]()
3056      {
3057          if (!pData->restorePreviewOnRelease)
3058          {
3059              pData->previewOverrideActive = false;
3060              pData->playheadPushed = false;
3061              return;
3062          }
3063  
3064          if (pData->playheadPushed)
3065          {
3066              // Keep pushed playhead; just clear override flags
3067              pData->previewOverrideActive = false;
3068              pData->restorePreviewOnRelease = false;
3069              pData->playheadPushed = false;
3070              return;
3071          }
3072  
3073          if (pData->hDialog)
3074          {
3075              // Restore playhead to where it was before the gripper drag.
3076              // Only clamp to video bounds, not selection bounds, so the playhead
3077              // can remain outside the selection if it was there before.
3078              const int64_t restoredTicks = std::clamp<int64_t>(
3079                  pData->positionBeforeOverride.count(),
3080                  0LL,
3081                  pData->videoDuration.count());
3082              pData->currentPosition = winrt::TimeSpan{ restoredTicks };
3083              pData->previewOverrideActive = false;
3084              pData->restorePreviewOnRelease = false;
3085              pData->playheadPushed = false;
3086              UpdateVideoPreview(pData->hDialog, pData);
3087          }
3088      };
3089  
3090      switch (message)
3091      {
3092      case WM_NCDESTROY:
3093          RemoveWindowSubclass(hWnd, TimelineSubclassProc, uIdSubclass);
3094          break;
3095  
3096      case WM_LBUTTONDOWN:
3097      {
3098          // Pause without recapturing position; we might be parked on a handle
3099          StopPlayback(pData->hDialog, pData, false);
3100  
3101          RECT rcClient{};
3102          GetClientRect(hWnd, &rcClient);
3103          const int width = rcClient.right - rcClient.left;
3104          if (width <= 0)
3105          {
3106              break;
3107          }
3108  
3109          const int x = GET_X_LPARAM(lParam);
3110          const int y = GET_Y_LPARAM(lParam);
3111          const int clampedX = std::clamp(x, 0, width);
3112  
3113          // Get DPI for scaling hit test regions
3114          const UINT dpi = GetDpiForWindowHelper(hWnd);
3115          const int timelineTrackTopOffset = ScaleForDpi(kTimelineTrackTopOffset, dpi);
3116          const int timelineTrackHeight = ScaleForDpi(kTimelineTrackHeight, dpi);
3117          const int timelineHandleHeight = ScaleForDpi(kTimelineHandleHeight, dpi);
3118          const int timelineHandleHitRadius = ScaleForDpi(kTimelineHandleHitRadius, dpi);
3119  
3120          const int trackTop = timelineTrackTopOffset;
3121          const int trackBottom = trackTop + timelineTrackHeight;
3122  
3123          // Gripper vertical band: centered on track
3124          const int gripperTop = trackTop - (timelineHandleHeight - timelineTrackHeight) / 2;
3125          const int gripperBottom = gripperTop + timelineHandleHeight;
3126          const bool inGripperBand = (y >= gripperTop && y <= gripperBottom);
3127  
3128          // Playhead knob vertical band: below the track (ellipse drawn at trackBottom + 12 to trackBottom + 24)
3129          const int knobTop = trackBottom + ScaleForDpi(8, dpi); // slightly above ellipse for easier hit
3130          const int knobBottom = trackBottom + ScaleForDpi(28, dpi);
3131          const bool inKnobBand = (y >= knobTop && y <= knobBottom);
3132  
3133          // Playhead stem is also hittable (trackTop - 12 to trackBottom + posLineBelow)
3134          const int stemTop = trackTop - ScaleForDpi(12, dpi);
3135          const int stemBottom = trackBottom + ScaleForDpi(22, dpi);
3136          const bool inStemBand = (y >= stemTop && y <= stemBottom);
3137  
3138          const int startX = TimelineTimeToClientX(pData, pData->trimStart, width, dpi);
3139          const int posX = TimelineTimeToClientX(pData, pData->currentPosition, width, dpi);
3140          const int endX = TimelineTimeToClientX(pData, pData->trimEnd, width, dpi);
3141  
3142          pData->dragMode = VideoRecordingSession::TrimDialogData::None;
3143          pData->previewOverrideActive = false;
3144          pData->restorePreviewOnRelease = false;
3145  
3146          // Calculate horizontal distances to each handle
3147          const int distToPos = abs(clampedX - posX);
3148          const int distToStart = abs(clampedX - startX);
3149          const int distToEnd = abs(clampedX - endX);
3150  
3151          // Hit-test with vertical position awareness:
3152          // - Grippers are only hittable in the gripper band (around the track)
3153          // - Playhead is hittable in the knob band (below track) or stem band
3154          // - When clicking in the knob area (below track), playhead always wins
3155          // - When in the gripper band, grippers take priority for horizontal overlaps
3156  
3157          const bool startHit = inGripperBand && distToStart <= timelineHandleHitRadius;
3158          const bool endHit = inGripperBand && distToEnd <= timelineHandleHitRadius;
3159          const bool posHitKnob = inKnobBand && distToPos <= timelineHandleHitRadius;
3160          const bool posHitStem = inStemBand && distToPos <= ScaleForDpi(4, dpi); // tighter radius for stem
3161  
3162          // Prioritize playhead when clicking in the knob area (lollipop head below the track)
3163          if (posHitKnob)
3164          {
3165              pData->dragMode = VideoRecordingSession::TrimDialogData::Position;
3166          }
3167          else if (startHit && (!endHit || distToStart <= distToEnd))
3168          {
3169              pData->dragMode = VideoRecordingSession::TrimDialogData::TrimStart;
3170          }
3171          else if (endHit)
3172          {
3173              pData->dragMode = VideoRecordingSession::TrimDialogData::TrimEnd;
3174          }
3175          else if (posHitStem)
3176          {
3177              pData->dragMode = VideoRecordingSession::TrimDialogData::Position;
3178          }
3179  
3180          if (pData->dragMode != VideoRecordingSession::TrimDialogData::None)
3181          {
3182              pData->isDragging = true;
3183              pData->playheadPushed = false;
3184              if (pData->dragMode == VideoRecordingSession::TrimDialogData::TrimStart ||
3185                  pData->dragMode == VideoRecordingSession::TrimDialogData::TrimEnd)
3186              {
3187                  pData->positionBeforeOverride = pData->currentPosition;
3188                  pData->previewOverrideActive = true;
3189                  pData->restorePreviewOnRelease = true;
3190                  pData->previewOverride = (pData->dragMode == VideoRecordingSession::TrimDialogData::TrimStart) ?
3191                      pData->trimStart : pData->trimEnd;
3192                  UpdateVideoPreview(pData->hDialog, pData);
3193                  // Show resize cursor during grip drag
3194                  SetCursor(LoadCursor(nullptr, IDC_SIZEWE));
3195              }
3196              SetCapture(hWnd);
3197              return 0;
3198          }
3199          break;
3200      }
3201  
3202      case WM_LBUTTONUP:
3203      {
3204          if (pData->isDragging)
3205          {
3206              // Kill debounce timer and do immediate final update
3207              KillTimer(hWnd, kPreviewDebounceTimerId);
3208              const bool wasPositionDrag = (pData->dragMode == VideoRecordingSession::TrimDialogData::Position);
3209              pData->isDragging = false;
3210              ReleaseCapture();
3211              SetCursor(LoadCursor(nullptr, IDC_ARROW));
3212              restorePreviewIfNeeded();
3213              pData->dragMode = VideoRecordingSession::TrimDialogData::None;
3214              InvalidateRect(hWnd, nullptr, FALSE);
3215              // Ensure final preview update for playhead drag (restorePreviewIfNeeded doesn't update for this case)
3216              if (wasPositionDrag && pData->hDialog)
3217              {
3218                  UpdateVideoPreview(pData->hDialog, pData, false);
3219              }
3220              return 0;
3221          }
3222          break;
3223      }
3224  
3225      case WM_MOUSEMOVE:
3226      {
3227          TRACKMOUSEEVENT tme{};
3228          tme.cbSize = sizeof(tme);
3229          tme.dwFlags = TME_LEAVE;
3230          tme.hwndTrack = hWnd;
3231          TrackMouseEvent(&tme);
3232  
3233          RECT rcClient{};
3234          GetClientRect(hWnd, &rcClient);
3235          const int width = rcClient.right - rcClient.left;
3236          if (width <= 0)
3237          {
3238              break;
3239          }
3240  
3241          const int rawX = GET_X_LPARAM(lParam);
3242          const int clampedX = std::clamp(rawX, 0, width);
3243  
3244          if (!pData->isDragging)
3245          {
3246              // Get DPI for scaling hit test regions
3247              const UINT dpi = GetDpiForWindowHelper(hWnd);
3248              const int timelineHandleHitRadius = ScaleForDpi(kTimelineHandleHitRadius, dpi);
3249  
3250              const int startX = TimelineTimeToClientX(pData, pData->trimStart, width, dpi);
3251              const int posX = TimelineTimeToClientX(pData, pData->currentPosition, width, dpi);
3252              const int endX = TimelineTimeToClientX(pData, pData->trimEnd, width, dpi);
3253  
3254              if (abs(clampedX - posX) <= timelineHandleHitRadius)
3255              {
3256                  SetCursor(LoadCursor(nullptr, IDC_HAND));
3257              }
3258              else if (abs(clampedX - startX) < timelineHandleHitRadius || abs(clampedX - endX) < timelineHandleHitRadius)
3259              {
3260                  SetCursor(LoadCursor(nullptr, IDC_HAND));
3261              }
3262              else
3263              {
3264                  SetCursor(LoadCursor(nullptr, IDC_ARROW));
3265              }
3266              return 0;
3267          }
3268  
3269          // Set appropriate cursor during drag
3270          if (pData->dragMode == VideoRecordingSession::TrimDialogData::TrimStart ||
3271              pData->dragMode == VideoRecordingSession::TrimDialogData::TrimEnd)
3272          {
3273              SetCursor(LoadCursor(nullptr, IDC_SIZEWE));
3274          }
3275          else if (pData->dragMode == VideoRecordingSession::TrimDialogData::Position)
3276          {
3277              SetCursor(LoadCursor(nullptr, IDC_HAND));
3278          }
3279  
3280          // Get DPI for pixel-to-time conversion during drag
3281          const UINT dpi = GetDpiForWindowHelper(hWnd);
3282          const auto newTime = TimelinePixelToTime(pData, clampedX, width, dpi);
3283  
3284          bool requestPreviewUpdate = false;
3285          bool applyOverride = false;
3286          winrt::TimeSpan overrideTime{ 0 };
3287  
3288          switch (pData->dragMode)
3289          {
3290          case VideoRecordingSession::TrimDialogData::TrimStart:
3291              if (newTime.count() < pData->trimEnd.count())
3292              {
3293                  const auto oldTrimStart = pData->trimStart;
3294                  if (newTime.count() != pData->trimStart.count())
3295                  {
3296                      pData->trimStart = newTime;
3297                      UpdateDurationDisplay(pData->hDialog, pData);
3298                  }
3299                  // Push playhead if gripper crossed over it in either direction:
3300                  // - Moving right: playhead was >= oldTrimStart and is now < newTrimStart
3301                  // - Moving left: playhead was <= oldTrimStart and is now >= newTrimStart
3302                  //   (use <= so that once pushed, the playhead continues moving with the gripper)
3303                  const bool movingRight = pData->trimStart.count() > oldTrimStart.count();
3304                  const bool movingLeft = pData->trimStart.count() < oldTrimStart.count();
3305                  const bool pushRight = movingRight &&
3306                      pData->currentPosition.count() >= oldTrimStart.count() &&
3307                      pData->currentPosition.count() < pData->trimStart.count();
3308                  const bool pushLeft = movingLeft &&
3309                      pData->currentPosition.count() <= oldTrimStart.count() &&
3310                      pData->currentPosition.count() >= pData->trimStart.count();
3311                  if (pushRight || pushLeft)
3312                  {
3313                      pData->playheadPushed = true;
3314                      pData->currentPosition = pData->trimStart;
3315                      // Also update playback start position so loop resets to pushed position
3316                      pData->playbackStartPosition = pData->currentPosition;
3317                      pData->playbackStartPositionValid = true;
3318                      // Invalidate cached start frame
3319                      std::lock_guard<std::mutex> lock(pData->previewBitmapMutex);
3320                      if (pData->hCachedStartFrame)
3321                      {
3322                          DeleteObject(pData->hCachedStartFrame);
3323                          pData->hCachedStartFrame = nullptr;
3324                      }
3325                  }
3326                  overrideTime = pData->trimStart;
3327                  applyOverride = true;
3328                  requestPreviewUpdate = true;
3329              }
3330              break;
3331  
3332          case VideoRecordingSession::TrimDialogData::Position:
3333          {
3334              const int previousPosX = TimelineTimeToClientX(pData, pData->currentPosition, width, dpi);
3335  
3336              // Allow playhead to move anywhere within video bounds (0 to videoDuration)
3337              const int64_t clampedTicks = std::clamp(newTime.count(), 0LL, pData->videoDuration.count());
3338              pData->currentPosition = winrt::TimeSpan{ clampedTicks };
3339  
3340              // User explicitly positioned the playhead; update the loop anchor.
3341              pData->playbackStartPosition = pData->currentPosition;
3342              pData->playbackStartPositionValid = true;
3343  
3344              // Invalidate cached start frame since position changed - will be re-cached when playback starts.
3345              {
3346                  std::lock_guard<std::mutex> lock(pData->previewBitmapMutex);
3347                  if (pData->hCachedStartFrame)
3348                  {
3349                      DeleteObject(pData->hCachedStartFrame);
3350                      pData->hCachedStartFrame = nullptr;
3351                  }
3352              }
3353  
3354              const int newPosX = TimelineTimeToClientX(pData, pData->currentPosition, width, dpi);
3355              RECT clientRect{};
3356              GetClientRect(hWnd, &clientRect);
3357              InvalidatePlayheadRegion(hWnd, clientRect, previousPosX, newPosX, dpi);
3358              UpdateWindow(hWnd);  // Force immediate visual update for smooth dragging
3359              pData->previewOverrideActive = false;
3360              // Debounce preview update for playhead drag as well
3361              SetTimer(hWnd, kPreviewDebounceTimerId, kPreviewDebounceDelayMs, nullptr);
3362              break;
3363          }
3364  
3365          case VideoRecordingSession::TrimDialogData::TrimEnd:
3366              if (newTime.count() > pData->trimStart.count())
3367              {
3368                  const auto oldTrimEnd = pData->trimEnd;
3369                  if (newTime.count() != pData->trimEnd.count())
3370                  {
3371                      pData->trimEnd = newTime;
3372                      UpdateDurationDisplay(pData->hDialog, pData);
3373                  }
3374                  // Only push playhead if it was inside selection (<= old trimEnd) and handle crossed over it
3375                  if (pData->currentPosition.count() <= oldTrimEnd.count() &&
3376                      pData->currentPosition.count() > pData->trimEnd.count())
3377                  {
3378                      pData->playheadPushed = true;
3379                      pData->currentPosition = pData->trimEnd;
3380                      // Also update playback start position so loop resets to pushed position
3381                      pData->playbackStartPosition = pData->currentPosition;
3382                      pData->playbackStartPositionValid = true;
3383                      // Invalidate cached start frame
3384                      std::lock_guard<std::mutex> lock(pData->previewBitmapMutex);
3385                      if (pData->hCachedStartFrame)
3386                      {
3387                          DeleteObject(pData->hCachedStartFrame);
3388                          pData->hCachedStartFrame = nullptr;
3389                      }
3390                  }
3391                  overrideTime = pData->trimEnd;
3392                  applyOverride = true;
3393                  requestPreviewUpdate = true;
3394              }
3395              break;
3396  
3397          default:
3398              break;
3399          }
3400  
3401          if (applyOverride)
3402          {
3403              pData->previewOverrideActive = true;
3404              pData->previewOverride = overrideTime;
3405          }
3406  
3407          // Force immediate visual update of gripper for smooth dragging
3408          InvalidateRect(hWnd, nullptr, FALSE);
3409          UpdateWindow(hWnd);
3410  
3411          // Debounce preview update - use a timer to avoid overwhelming the system with requests
3412          // Each mouse move resets the timer; preview only updates after dragging pauses
3413          if (requestPreviewUpdate)
3414          {
3415              SetTimer(hWnd, kPreviewDebounceTimerId, kPreviewDebounceDelayMs, nullptr);
3416          }
3417  
3418          return 0;
3419      }
3420  
3421      case WM_TIMER:
3422      {
3423          if (wParam == kPreviewDebounceTimerId)
3424          {
3425              KillTimer(hWnd, kPreviewDebounceTimerId);
3426              if (pData && pData->hDialog)
3427              {
3428                  UpdateVideoPreview(pData->hDialog, pData, false);
3429              }
3430              return 0;
3431          }
3432          break;
3433      }
3434  
3435      case WM_ERASEBKGND:
3436          return 1;
3437  
3438      case WM_MOUSELEAVE:
3439          if (!pData->isDragging)
3440          {
3441              SetCursor(LoadCursor(nullptr, IDC_ARROW));
3442          }
3443          break;
3444  
3445      case WM_CAPTURECHANGED:
3446          if (pData->isDragging)
3447          {
3448              KillTimer(hWnd, kPreviewDebounceTimerId);
3449              pData->isDragging = false;
3450              pData->dragMode = VideoRecordingSession::TrimDialogData::None;
3451              restorePreviewIfNeeded();
3452          }
3453          break;
3454      }
3455  
3456      return DefSubclassProc(hWnd, message, wParam, lParam);
3457  }
3458  
3459  //----------------------------------------------------------------------------
3460  //
3461  // Helper: Draw custom playback buttons (play/pause and restart)
3462  //
3463  //----------------------------------------------------------------------------
3464  static void DrawPlaybackButton(
3465      const DRAWITEMSTRUCT* pDIS,
3466      VideoRecordingSession::TrimDialogData* pData)
3467  {
3468      if (!pDIS || !pData)
3469      {
3470          return;
3471      }
3472  
3473      const bool isPlayControl = (pDIS->CtlID == IDC_TRIM_PLAY_PAUSE);
3474      const bool isRewindControl = (pDIS->CtlID == IDC_TRIM_REWIND);
3475      const bool isForwardControl = (pDIS->CtlID == IDC_TRIM_FORWARD);
3476      const bool isSkipStartControl = (pDIS->CtlID == IDC_TRIM_SKIP_START);
3477      const bool isSkipEndControl = (pDIS->CtlID == IDC_TRIM_SKIP_END);
3478  
3479      // Check if skip buttons should be disabled based on position
3480      const bool atStart = (pData->currentPosition.count() <= pData->trimStart.count());
3481      const bool atEnd = (pData->currentPosition.count() >= pData->trimEnd.count());
3482  
3483      const bool isHover = isPlayControl ? pData->hoverPlay :
3484                          (isRewindControl ? pData->hoverRewind :
3485                          (isForwardControl ? pData->hoverForward :
3486                          (isSkipStartControl ? pData->hoverSkipStart : pData->hoverSkipEnd)));
3487      bool isDisabled = (pDIS->itemState & ODS_DISABLED) != 0;
3488  
3489      // Disable skip start when at start, skip end when at end
3490      if (isSkipStartControl && atStart) isDisabled = true;
3491      if (isSkipEndControl && atEnd) isDisabled = true;
3492  
3493      const bool isPressed = (pDIS->itemState & ODS_SELECTED) != 0;
3494      const bool isPlaying = pData->isPlaying.load(std::memory_order_relaxed);
3495  
3496      // Media Player color scheme - dark background with gradient
3497      COLORREF bgColorTop = RGB(45, 45, 50);
3498      COLORREF bgColorBottom = RGB(35, 35, 40);
3499      COLORREF iconColor = RGB(220, 220, 220);
3500      COLORREF borderColor = RGB(120, 120, 125);
3501  
3502      if (isHover && !isDisabled)
3503      {
3504          bgColorTop = RGB(60, 60, 65);
3505          bgColorBottom = RGB(50, 50, 55);
3506          iconColor = RGB(255, 255, 255);
3507          borderColor = RGB(150, 150, 155);
3508      }
3509      if (isPressed && !isDisabled)
3510      {
3511          bgColorTop = RGB(30, 30, 35);
3512          bgColorBottom = RGB(25, 25, 30);
3513          iconColor = RGB(200, 200, 200);
3514      }
3515      if (isDisabled)
3516      {
3517          bgColorTop = RGB(40, 40, 45);
3518          bgColorBottom = RGB(35, 35, 40);
3519          iconColor = RGB(100, 100, 100);
3520      }
3521  
3522      int width = pDIS->rcItem.right - pDIS->rcItem.left;
3523      int height = pDIS->rcItem.bottom - pDIS->rcItem.top;
3524      float centerX = pDIS->rcItem.left + width / 2.0f;
3525      float centerY = pDIS->rcItem.top + height / 2.0f;
3526      float radius = min(width, height) / 2.0f - 1.0f;
3527  
3528      // Use GDI+ for antialiased rendering
3529      Gdiplus::Graphics graphics(pDIS->hDC);
3530      graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
3531  
3532      // Draw flat background circle (no gradient)
3533      Gdiplus::SolidBrush bgBrush(Gdiplus::Color(255, GetRValue(bgColorBottom), GetGValue(bgColorBottom), GetBValue(bgColorBottom)));
3534      graphics.FillEllipse(&bgBrush, centerX - radius, centerY - radius, radius * 2, radius * 2);
3535  
3536      // Draw subtle border
3537      Gdiplus::Pen borderPen(Gdiplus::Color(100, GetRValue(borderColor), GetGValue(borderColor), GetBValue(borderColor)), 0.5f);
3538      graphics.DrawEllipse(&borderPen, centerX - radius, centerY - radius, radius * 2, radius * 2);
3539  
3540      // Draw icons
3541      Gdiplus::SolidBrush iconBrush(Gdiplus::Color(255, GetRValue(iconColor), GetGValue(iconColor), GetBValue(iconColor)));
3542      float iconSize = radius * 0.8f; // slightly larger icons
3543  
3544      if (isPlayControl)
3545      {
3546          if (isPlaying)
3547          {
3548              // Draw pause icon (two vertical bars)
3549              float barWidth = iconSize / 4.0f;
3550              float barHeight = iconSize;
3551              float gap = iconSize / 5.0f;
3552  
3553              graphics.FillRectangle(&iconBrush,
3554                  centerX - gap - barWidth, centerY - barHeight / 2.0f,
3555                  barWidth, barHeight);
3556              graphics.FillRectangle(&iconBrush,
3557                  centerX + gap, centerY - barHeight / 2.0f,
3558                  barWidth, barHeight);
3559          }
3560          else
3561          {
3562              // Draw play triangle
3563              float triWidth = iconSize;
3564              float triHeight = iconSize;
3565              Gdiplus::PointF playTri[3] = {
3566                  Gdiplus::PointF(centerX - triWidth / 3.0f, centerY - triHeight / 2.0f),
3567                  Gdiplus::PointF(centerX + triWidth * 2.0f / 3.0f, centerY),
3568                  Gdiplus::PointF(centerX - triWidth / 3.0f, centerY + triHeight / 2.0f)
3569              };
3570              graphics.FillPolygon(&iconBrush, playTri, 3);
3571          }
3572      }
3573      else if (isRewindControl || isForwardControl)
3574      {
3575          // Draw small play triangle in appropriate direction
3576          float triWidth = iconSize * 3.0f / 5.0f;
3577          float triHeight = iconSize * 3.0f / 5.0f;
3578  
3579          if (isRewindControl)
3580          {
3581              // Triangle pointing left
3582              Gdiplus::PointF tri[3] = {
3583                  Gdiplus::PointF(centerX + triWidth / 3.0f, centerY - triHeight / 2.0f),
3584                  Gdiplus::PointF(centerX - triWidth * 2.0f / 3.0f, centerY),
3585                  Gdiplus::PointF(centerX + triWidth / 3.0f, centerY + triHeight / 2.0f)
3586              };
3587              graphics.FillPolygon(&iconBrush, tri, 3);
3588          }
3589          else
3590          {
3591              // Triangle pointing right
3592              Gdiplus::PointF tri[3] = {
3593                  Gdiplus::PointF(centerX - triWidth / 3.0f, centerY - triHeight / 2.0f),
3594                  Gdiplus::PointF(centerX + triWidth * 2.0f / 3.0f, centerY),
3595                  Gdiplus::PointF(centerX - triWidth / 3.0f, centerY + triHeight / 2.0f)
3596              };
3597              graphics.FillPolygon(&iconBrush, tri, 3);
3598          }
3599      }
3600      else if (isSkipStartControl || isSkipEndControl)
3601      {
3602          // Draw skip to start/end icon (triangle + bar)
3603          float triWidth = iconSize * 2.0f / 3.0f;
3604          float triHeight = iconSize;
3605          float barWidth = iconSize / 6.0f;
3606  
3607          if (isSkipStartControl)
3608          {
3609              // Bar on left, triangle pointing left
3610              graphics.FillRectangle(&iconBrush,
3611                  centerX - triWidth / 2.0f - barWidth, centerY - triHeight / 2.0f,
3612                  barWidth, triHeight);
3613  
3614              Gdiplus::PointF tri[3] = {
3615                  Gdiplus::PointF(centerX + triWidth / 2.0f, centerY - triHeight / 2.0f),
3616                  Gdiplus::PointF(centerX - triWidth / 2.0f, centerY),
3617                  Gdiplus::PointF(centerX + triWidth / 2.0f, centerY + triHeight / 2.0f)
3618              };
3619              graphics.FillPolygon(&iconBrush, tri, 3);
3620          }
3621          else
3622          {
3623              // Triangle pointing right, bar on right
3624              Gdiplus::PointF tri[3] = {
3625                  Gdiplus::PointF(centerX - triWidth / 2.0f, centerY - triHeight / 2.0f),
3626                  Gdiplus::PointF(centerX + triWidth / 2.0f, centerY),
3627                  Gdiplus::PointF(centerX - triWidth / 2.0f, centerY + triHeight / 2.0f)
3628              };
3629              graphics.FillPolygon(&iconBrush, tri, 3);
3630  
3631              graphics.FillRectangle(&iconBrush,
3632                  centerX + triWidth / 2.0f, centerY - triHeight / 2.0f,
3633                  barWidth, triHeight);
3634          }
3635      }
3636  }
3637  
3638  //----------------------------------------------------------------------------
3639  //
3640  // Helper: Mouse interaction for volume icon
3641  //
3642  //----------------------------------------------------------------------------
3643  static LRESULT CALLBACK VolumeIconSubclassProc(
3644      HWND hWnd,
3645      UINT message,
3646      WPARAM wParam,
3647      LPARAM lParam,
3648      UINT_PTR uIdSubclass,
3649      DWORD_PTR dwRefData)
3650  {
3651      auto* pData = reinterpret_cast<VideoRecordingSession::TrimDialogData*>(dwRefData);
3652      if (!pData)
3653      {
3654          return DefSubclassProc(hWnd, message, wParam, lParam);
3655      }
3656  
3657      switch (message)
3658      {
3659      case WM_NCDESTROY:
3660          RemoveWindowSubclass(hWnd, VolumeIconSubclassProc, uIdSubclass);
3661          break;
3662  
3663      case WM_MOUSEMOVE:
3664      {
3665          TRACKMOUSEEVENT tme{ sizeof(tme), TME_LEAVE, hWnd, 0 };
3666          TrackMouseEvent(&tme);
3667  
3668          if (!pData->hoverVolumeIcon)
3669          {
3670              pData->hoverVolumeIcon = true;
3671              InvalidateRect(hWnd, nullptr, FALSE);
3672          }
3673          return 0;
3674      }
3675  
3676      case WM_MOUSELEAVE:
3677          if (pData->hoverVolumeIcon)
3678          {
3679              pData->hoverVolumeIcon = false;
3680              InvalidateRect(hWnd, nullptr, FALSE);
3681          }
3682          return 0;
3683  
3684      case WM_SETCURSOR:
3685          SetCursor(LoadCursor(nullptr, IDC_HAND));
3686          return TRUE;
3687      }
3688  
3689      return DefSubclassProc(hWnd, message, wParam, lParam);
3690  }
3691  
3692  //----------------------------------------------------------------------------
3693  //
3694  // Helper: Mouse interaction for playback controls
3695  //
3696  //----------------------------------------------------------------------------
3697  static LRESULT CALLBACK PlaybackButtonSubclassProc(
3698      HWND hWnd,
3699      UINT message,
3700      WPARAM wParam,
3701      LPARAM lParam,
3702      UINT_PTR uIdSubclass,
3703      DWORD_PTR dwRefData)
3704  {
3705      auto* pData = reinterpret_cast<VideoRecordingSession::TrimDialogData*>(dwRefData);
3706      if (!pData)
3707      {
3708          return DefSubclassProc(hWnd, message, wParam, lParam);
3709      }
3710  
3711      switch (message)
3712      {
3713      case WM_NCDESTROY:
3714          RemoveWindowSubclass(hWnd, PlaybackButtonSubclassProc, uIdSubclass);
3715          break;
3716  
3717      case WM_LBUTTONDOWN:
3718          SetFocus(hWnd);
3719          SetCapture(hWnd);
3720          return 0;
3721  
3722      case WM_LBUTTONUP:
3723      {
3724          if (GetCapture() == hWnd)
3725          {
3726              ReleaseCapture();
3727          }
3728  
3729          POINT pt{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
3730          RECT rc{};
3731          GetClientRect(hWnd, &rc);
3732  
3733          if (PtInRect(&rc, pt))
3734          {
3735              HandlePlaybackCommand(GetDlgCtrlID(hWnd), pData);
3736          }
3737          return 0;
3738      }
3739  
3740      case WM_KEYUP:
3741          if (wParam == VK_SPACE || wParam == VK_RETURN)
3742          {
3743              HandlePlaybackCommand(GetDlgCtrlID(hWnd), pData);
3744              return 0;
3745          }
3746          break;
3747  
3748      case WM_MOUSEMOVE:
3749      {
3750          TRACKMOUSEEVENT tme{ sizeof(tme), TME_LEAVE, hWnd, 0 };
3751          TrackMouseEvent(&tme);
3752  
3753          const int controlId = GetDlgCtrlID(hWnd);
3754          const bool isPlayControl = (controlId == IDC_TRIM_PLAY_PAUSE);
3755          const bool isRewindControl = (controlId == IDC_TRIM_REWIND);
3756          const bool isForwardControl = (controlId == IDC_TRIM_FORWARD);
3757          const bool isSkipStartControl = (controlId == IDC_TRIM_SKIP_START);
3758  
3759          bool& hoverFlag = isPlayControl ? pData->hoverPlay :
3760                           (isRewindControl ? pData->hoverRewind :
3761                           (isForwardControl ? pData->hoverForward :
3762                           (isSkipStartControl ? pData->hoverSkipStart : pData->hoverSkipEnd)));
3763          if (!hoverFlag)
3764          {
3765              hoverFlag = true;
3766              InvalidateRect(hWnd, nullptr, FALSE);
3767          }
3768          return 0;
3769      }
3770  
3771      case WM_MOUSELEAVE:
3772      {
3773          const int controlId = GetDlgCtrlID(hWnd);
3774          const bool isPlayControl = (controlId == IDC_TRIM_PLAY_PAUSE);
3775          const bool isRewindControl = (controlId == IDC_TRIM_REWIND);
3776          const bool isForwardControl = (controlId == IDC_TRIM_FORWARD);
3777          const bool isSkipStartControl = (controlId == IDC_TRIM_SKIP_START);
3778  
3779          bool& hoverFlag = isPlayControl ? pData->hoverPlay :
3780                           (isRewindControl ? pData->hoverRewind :
3781                           (isForwardControl ? pData->hoverForward :
3782                           (isSkipStartControl ? pData->hoverSkipStart : pData->hoverSkipEnd)));
3783          if (hoverFlag)
3784          {
3785              hoverFlag = false;
3786              InvalidateRect(hWnd, nullptr, FALSE);
3787          }
3788          return 0;
3789      }
3790  
3791      case WM_SETCURSOR:
3792          SetCursor(LoadCursor(nullptr, IDC_HAND));
3793          return TRUE;
3794  
3795      case WM_ERASEBKGND:
3796          return 1;
3797  
3798      }
3799  
3800      return DefSubclassProc(hWnd, message, wParam, lParam);
3801  }
3802  
3803  //----------------------------------------------------------------------------
3804  //
3805  // TrimDialogSubclassProc
3806  //
3807  // Subclass procedure for the trim dialog to handle resize grip hit testing
3808  //
3809  //----------------------------------------------------------------------------
3810  static LRESULT CALLBACK TrimDialogSubclassProc(
3811      HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam,
3812      UINT_PTR uIdSubclass, DWORD_PTR /*dwRefData*/)
3813  {
3814      switch (message)
3815      {
3816      case WM_NCDESTROY:
3817          RemoveWindowSubclass(hWnd, TrimDialogSubclassProc, uIdSubclass);
3818          break;
3819  
3820      case WM_NCHITTEST:
3821      {
3822          // First let the default handler process it
3823          LRESULT ht = DefSubclassProc(hWnd, message, wParam, lParam);
3824  
3825          // If it's in the client area and not maximized, check for resize grip
3826          if (ht == HTCLIENT && !IsZoomed(hWnd))
3827          {
3828              RECT rcClient;
3829              GetClientRect(hWnd, &rcClient);
3830  
3831              POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
3832              ScreenToClient(hWnd, &pt);
3833  
3834              const int gripWidth = GetSystemMetrics(SM_CXHSCROLL);
3835              const int gripHeight = GetSystemMetrics(SM_CYVSCROLL);
3836  
3837              if (pt.x >= rcClient.right - gripWidth && pt.y >= rcClient.bottom - gripHeight)
3838              {
3839                  return HTBOTTOMRIGHT;
3840              }
3841          }
3842          return ht;
3843      }
3844      }
3845  
3846      return DefSubclassProc(hWnd, message, wParam, lParam);
3847  }
3848  
3849  //----------------------------------------------------------------------------
3850  //
3851  // VideoRecordingSession::TrimDialogProc
3852  //
3853  // Dialog procedure for trim dialog
3854  //
3855  //----------------------------------------------------------------------------
3856  INT_PTR CALLBACK VideoRecordingSession::TrimDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
3857  {
3858      static TrimDialogData* pData = nullptr;
3859      static UINT currentDpi = DPI_BASELINE;
3860  
3861      switch (message)
3862      {
3863      case WM_INITDIALOG:
3864      {
3865          pData = reinterpret_cast<TrimDialogData*>(lParam);
3866          if (!pData)
3867          {
3868              EndDialog(hDlg, IDCANCEL);
3869              return FALSE;
3870          }
3871  
3872          hDlgTrimDialog = hDlg;
3873          SetWindowLongPtr(hDlg, DWLP_USER, lParam);
3874  
3875          pData->hDialog = hDlg;
3876          pData->hoverPlay = false;
3877          pData->hoverRewind = false;
3878          pData->hoverForward = false;
3879          pData->hoverSkipStart = false;
3880          pData->hoverSkipEnd = false;
3881          pData->isPlaying.store(false, std::memory_order_relaxed);
3882          pData->lastRenderedPreview.store(-1, std::memory_order_relaxed);
3883  
3884          AcquireHighResTimer();
3885  
3886          // Make OK the default button
3887          SendMessage(hDlg, DM_SETDEFID, IDOK, 0);
3888  
3889          // Subclass the dialog to handle resize grip hit testing
3890          SetWindowSubclass(hDlg, TrimDialogSubclassProc, 0, reinterpret_cast<DWORD_PTR>(pData));
3891  
3892          HWND hTimeline = GetDlgItem(hDlg, IDC_TRIM_TIMELINE);
3893          if (hTimeline)
3894          {
3895              // Remove WS_EX_TRANSPARENT to prevent flicker during resize
3896              SetWindowLongPtr(hTimeline, GWL_EXSTYLE, GetWindowLongPtr(hTimeline, GWL_EXSTYLE) & ~WS_EX_TRANSPARENT);
3897              SetWindowSubclass(hTimeline, TimelineSubclassProc, 1, reinterpret_cast<DWORD_PTR>(pData));
3898          }
3899          HWND hPlayPause = GetDlgItem(hDlg, IDC_TRIM_PLAY_PAUSE);
3900          if (hPlayPause)
3901          {
3902              SetWindowSubclass(hPlayPause, PlaybackButtonSubclassProc, 2, reinterpret_cast<DWORD_PTR>(pData));
3903          }
3904          HWND hRewind = GetDlgItem(hDlg, IDC_TRIM_REWIND);
3905          if (hRewind)
3906          {
3907              SetWindowSubclass(hRewind, PlaybackButtonSubclassProc, 3, reinterpret_cast<DWORD_PTR>(pData));
3908          }
3909          HWND hForward = GetDlgItem(hDlg, IDC_TRIM_FORWARD);
3910          if (hForward)
3911          {
3912              SetWindowSubclass(hForward, PlaybackButtonSubclassProc, 4, reinterpret_cast<DWORD_PTR>(pData));
3913          }
3914          HWND hSkipStart = GetDlgItem(hDlg, IDC_TRIM_SKIP_START);
3915          if (hSkipStart)
3916          {
3917              SetWindowSubclass(hSkipStart, PlaybackButtonSubclassProc, 5, reinterpret_cast<DWORD_PTR>(pData));
3918          }
3919          HWND hSkipEnd = GetDlgItem(hDlg, IDC_TRIM_SKIP_END);
3920          if (hSkipEnd)
3921          {
3922              SetWindowSubclass(hSkipEnd, PlaybackButtonSubclassProc, 6, reinterpret_cast<DWORD_PTR>(pData));
3923          }
3924          HWND hVolumeIcon = GetDlgItem(hDlg, IDC_TRIM_VOLUME_ICON);
3925          if (hVolumeIcon)
3926          {
3927              SetWindowSubclass(hVolumeIcon, VolumeIconSubclassProc, 7, reinterpret_cast<DWORD_PTR>(pData));
3928          }
3929  
3930          // Initialize volume from saved setting
3931          pData->volume = std::clamp(static_cast<double>(g_TrimDialogVolume) / 100.0, 0.0, 1.0);
3932          pData->previousVolume = (pData->volume > 0.0) ? pData->volume : 0.70;  // Remember initial volume for unmute
3933  
3934          // Initialize volume slider
3935          HWND hVolume = GetDlgItem(hDlg, IDC_TRIM_VOLUME);
3936          if (hVolume)
3937          {
3938              SendMessage(hVolume, TBM_SETRANGE, TRUE, MAKELPARAM(0, 100));
3939              SendMessage(hVolume, TBM_SETPOS, TRUE, static_cast<LPARAM>(pData->volume * 100));
3940          }
3941  
3942          // Hide volume controls for GIF (no audio)
3943          if (pData->isGif)
3944          {
3945              if (hVolumeIcon)
3946              {
3947                  ShowWindow(hVolumeIcon, SW_HIDE);
3948              }
3949              if (hVolume)
3950              {
3951                  ShowWindow(hVolume, SW_HIDE);
3952              }
3953          }
3954  
3955          // Ensure incoming times are sane and within bounds.
3956          if (pData->videoDuration.count() > 0)
3957          {
3958              const int64_t durationTicks = pData->videoDuration.count();
3959              const int64_t endTicks = (pData->trimEnd.count() > 0) ? pData->trimEnd.count() : durationTicks;
3960              const int64_t clampedEnd = std::clamp<int64_t>(endTicks, 0, durationTicks);
3961              const int64_t clampedStart = std::clamp<int64_t>(pData->trimStart.count(), 0, clampedEnd);
3962              pData->trimStart = winrt::TimeSpan{ clampedStart };
3963              pData->trimEnd = winrt::TimeSpan{ clampedEnd };
3964          }
3965  
3966          // Keep the playhead at a valid position.
3967          const int64_t upper = (pData->trimEnd.count() > 0) ? pData->trimEnd.count() : pData->videoDuration.count();
3968          pData->currentPosition = winrt::TimeSpan{ std::clamp<int64_t>(pData->currentPosition.count(), 0, upper) };
3969  
3970          UpdateDurationDisplay(hDlg, pData);
3971  
3972          // Update labels and timeline; skip async preview load if we already have a preloaded frame
3973          if (pData->hPreviewBitmap)
3974          {
3975              // Already have a preview from preloading - just update the UI
3976              UpdatePositionUI(hDlg, pData, true);
3977              InvalidateRect(GetDlgItem(hDlg, IDC_TRIM_PREVIEW), nullptr, FALSE);
3978          }
3979          else
3980          {
3981              // No preloaded preview - start async video load
3982              UpdateVideoPreview(hDlg, pData);
3983          }
3984          // Show time relative to left grip (trimStart)
3985          const auto relativePos = winrt::TimeSpan{ (std::max)(pData->currentPosition.count() - pData->trimStart.count(), int64_t{ 0 }) };
3986          SetTimeText(hDlg, IDC_TRIM_POSITION_LABEL, relativePos, true);
3987  
3988          // Initialize currentDpi to actual dialog DPI (for WM_DPICHANGED handling)
3989          currentDpi = GetDpiForWindowHelper(hDlg);
3990  
3991          // Create a larger font for the time position label
3992          {
3993              int fontSize = -MulDiv(12, static_cast<int>(currentDpi), USER_DEFAULT_SCREEN_DPI);  // 12pt font
3994              pData->hTimeLabelFont = CreateFont(fontSize, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
3995                  OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI");
3996              if (pData->hTimeLabelFont)
3997              {
3998                  HWND hPosition = GetDlgItem(hDlg, IDC_TRIM_POSITION_LABEL);
3999                  if (hPosition)
4000                  {
4001                      SendMessage(hPosition, WM_SETFONT, reinterpret_cast<WPARAM>(pData->hTimeLabelFont), TRUE);
4002                  }
4003                  HWND hDuration = GetDlgItem(hDlg, IDC_TRIM_DURATION_LABEL);
4004                  if (hDuration)
4005                  {
4006                      SendMessage(hDuration, WM_SETFONT, reinterpret_cast<WPARAM>(pData->hTimeLabelFont), TRUE);
4007                  }
4008              }
4009          }
4010  
4011          // Apply dark mode
4012          ApplyDarkModeToDialog( hDlg );
4013  
4014          // Apply saved dialog size if available, then center
4015          if (g_TrimDialogWidth > 0 && g_TrimDialogHeight > 0)
4016          {
4017              // Get current window rect to preserve position initially
4018              RECT rcDlg{};
4019              GetWindowRect(hDlg, &rcDlg);
4020  
4021              // Apply saved size (stored in screen pixels)
4022              SetWindowPos(hDlg, nullptr, 0, 0,
4023                  static_cast<int>(g_TrimDialogWidth),
4024                  static_cast<int>(g_TrimDialogHeight),
4025                  SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
4026          }
4027  
4028          // Center dialog on screen
4029          CenterTrimDialog(hDlg);
4030          return TRUE;
4031      }
4032  
4033      case WM_CTLCOLORDLG:
4034      case WM_CTLCOLORBTN:
4035      case WM_CTLCOLOREDIT:
4036      case WM_CTLCOLORLISTBOX:
4037      {
4038          HDC hdc = reinterpret_cast<HDC>(wParam);
4039          HWND hCtrl = reinterpret_cast<HWND>(lParam);
4040          HBRUSH hBrush = HandleDarkModeCtlColor(hdc, hCtrl, message);
4041          if (hBrush)
4042          {
4043              return reinterpret_cast<INT_PTR>(hBrush);
4044          }
4045          break;
4046      }
4047  
4048      case WM_CTLCOLORSTATIC:
4049      {
4050          HDC hdc = reinterpret_cast<HDC>(wParam);
4051          HWND hCtrl = reinterpret_cast<HWND>(lParam);
4052          // Use timeline marker color for duration and position labels
4053          if (IsDarkModeEnabled())
4054          {
4055              int ctrlId = GetDlgCtrlID(hCtrl);
4056              if (ctrlId == IDC_TRIM_DURATION_LABEL || ctrlId == IDC_TRIM_POSITION_LABEL)
4057              {
4058                  SetBkMode(hdc, TRANSPARENT);
4059                  SetTextColor(hdc, RGB(140, 140, 140));  // Match timeline marker color
4060                  return reinterpret_cast<INT_PTR>(GetDarkModeBrush());
4061              }
4062          }
4063          HBRUSH hBrush = HandleDarkModeCtlColor(hdc, hCtrl, message);
4064          if (hBrush)
4065          {
4066              return reinterpret_cast<INT_PTR>(hBrush);
4067          }
4068          break;
4069      }
4070  
4071      case WM_ERASEBKGND:
4072          if (IsDarkModeEnabled())
4073          {
4074              HDC hdc = reinterpret_cast<HDC>(wParam);
4075              RECT rc;
4076              GetClientRect(hDlg, &rc);
4077              FillRect(hdc, &rc, GetDarkModeBrush());
4078  
4079              // Draw the resize grip at the bottom-right corner (dark mode only for now)
4080              if (!IsZoomed(hDlg))
4081              {
4082                  const int gripWidth = GetSystemMetrics(SM_CXHSCROLL);
4083                  const int gripHeight = GetSystemMetrics(SM_CYVSCROLL);
4084                  RECT rcGrip = {
4085                      rc.right - gripWidth,
4086                      rc.bottom - gripHeight,
4087                      rc.right,
4088                      rc.bottom
4089                  };
4090  
4091                  HTHEME hTheme = OpenThemeData(hDlg, L"STATUS");
4092                  if (hTheme)
4093                  {
4094                      DrawThemeBackground(hTheme, hdc, SP_GRIPPER, 0, &rcGrip, nullptr);
4095                      CloseThemeData(hTheme);
4096                  }
4097                  else
4098                  {
4099                      DrawFrameControl(hdc, &rcGrip, DFC_SCROLL, DFCS_SCROLLSIZEGRIP);
4100                  }
4101              }
4102  
4103              return TRUE;
4104          }
4105          break;
4106  
4107      case WM_GETMINMAXINFO:
4108      {
4109          // Set minimum dialog size to prevent controls from overlapping
4110          MINMAXINFO* mmi = reinterpret_cast<MINMAXINFO*>(lParam);
4111          // Use MapDialogRect to convert dialog units to pixels
4112          // Minimum size: 440x300 dialog units (smaller than original 521x380)
4113          RECT rcMin = { 0, 0, 440, 300 };
4114          MapDialogRect(hDlg, &rcMin);
4115          // Add frame/border size
4116          RECT rcFrame = { 0, 0, 0, 0 };
4117          AdjustWindowRectEx(&rcFrame, GetWindowLong(hDlg, GWL_STYLE), FALSE, GetWindowLong(hDlg, GWL_EXSTYLE));
4118          const int frameWidth = (rcFrame.right - rcFrame.left);
4119          const int frameHeight = (rcFrame.bottom - rcFrame.top);
4120          mmi->ptMinTrackSize.x = rcMin.right + frameWidth;
4121          mmi->ptMinTrackSize.y = rcMin.bottom + frameHeight;
4122          return 0;
4123      }
4124  
4125      case WM_SIZE:
4126      {
4127          if (wParam == SIZE_MINIMIZED)
4128          {
4129              return 0;
4130          }
4131  
4132          pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
4133          if (!pData)
4134          {
4135              return 0;
4136          }
4137  
4138          const int clientWidth = LOWORD(lParam);
4139          const int clientHeight = HIWORD(lParam);
4140  
4141          // Use MapDialogRect to convert dialog units to pixels properly
4142          // This accounts for font metrics and DPI
4143          auto DluToPixels = [hDlg](int dluX, int dluY, int* pxX, int* pxY) {
4144              RECT rc = { 0, 0, dluX, dluY };
4145              MapDialogRect(hDlg, &rc);
4146              if (pxX) *pxX = rc.right;
4147              if (pxY) *pxY = rc.bottom;
4148          };
4149  
4150          // Convert dialog unit values to pixels
4151          int marginLeft, marginRight, marginTop;
4152          DluToPixels(12, 12, &marginLeft, &marginTop);
4153          DluToPixels(11, 0, &marginRight, nullptr);
4154  
4155          // Suppress redraw on the entire dialog during layout to prevent tearing
4156          SendMessage(hDlg, WM_SETREDRAW, FALSE, 0);
4157  
4158          // Fixed heights from RC file (in dialog units) converted to pixels
4159          int labelHeight, timelineHeight, buttonRowHeight, okCancelHeight, bottomMargin;
4160          int spacing4, spacing2, spacing8;
4161          DluToPixels(0, 10, nullptr, &labelHeight);     // Label height: 10 DLU (for 8pt font)
4162          DluToPixels(0, 50, nullptr, &timelineHeight);  // Timeline height: 50 DLU
4163          DluToPixels(0, 32, nullptr, &buttonRowHeight); // Play button height: 32 DLU
4164          DluToPixels(0, 14, nullptr, &okCancelHeight);  // OK/Cancel height: 14 DLU
4165          DluToPixels(0, 8, nullptr, &bottomMargin);     // Bottom margin
4166          DluToPixels(0, 4, nullptr, &spacing4);         // 4 DLU spacing
4167          DluToPixels(0, 2, nullptr, &spacing2);         // 2 DLU spacing
4168          DluToPixels(0, 8, nullptr, &spacing8);         // 8 DLU spacing
4169  
4170          // Calculate vertical positions from bottom up
4171          const int okCancelY = clientHeight - bottomMargin - okCancelHeight;
4172          const int buttonRowY = okCancelY - spacing4 - buttonRowHeight;
4173          const int timelineY = buttonRowY - spacing4 - timelineHeight;
4174          const int labelY = timelineY - spacing2 - labelHeight;
4175  
4176          // Preview fills from top to above labels
4177          const int previewHeight = labelY - spacing8 - marginTop;
4178          const int previewWidth = clientWidth - marginLeft - marginRight;
4179          const int timelineWidth = previewWidth;
4180  
4181          // Resize preview
4182          HWND hPreview = GetDlgItem(hDlg, IDC_TRIM_PREVIEW);
4183          if (hPreview)
4184          {
4185              SetWindowPos(hPreview, nullptr, marginLeft, marginTop, previewWidth, previewHeight,
4186                  SWP_NOZORDER | SWP_NOACTIVATE);
4187          }
4188  
4189          // Position duration label (left-aligned)
4190          HWND hDuration = GetDlgItem(hDlg, IDC_TRIM_DURATION_LABEL);
4191          if (hDuration)
4192          {
4193              int labelWidth;
4194              DluToPixels(160, 0, &labelWidth, nullptr);
4195              SetWindowPos(hDuration, nullptr, marginLeft, labelY, labelWidth, labelHeight,
4196                  SWP_NOZORDER | SWP_NOACTIVATE);
4197          }
4198  
4199          // Position time label (centered)
4200          HWND hPosition = GetDlgItem(hDlg, IDC_TRIM_POSITION_LABEL);
4201          if (hPosition)
4202          {
4203              int posLabelWidth;
4204              DluToPixels(200, 0, &posLabelWidth, nullptr);
4205              const int posLabelX = (clientWidth - posLabelWidth) / 2;
4206              SetWindowPos(hPosition, nullptr, posLabelX, labelY, posLabelWidth, labelHeight,
4207                  SWP_NOZORDER | SWP_NOACTIVATE);
4208          }
4209  
4210          // Resize timeline
4211          HWND hTimeline = GetDlgItem(hDlg, IDC_TRIM_TIMELINE);
4212          if (hTimeline)
4213          {
4214              SetWindowPos(hTimeline, nullptr, marginLeft, timelineY, timelineWidth, timelineHeight,
4215                  SWP_NOZORDER | SWP_NOACTIVATE);
4216          }
4217  
4218          // Position playback buttons (centered horizontally)
4219          // Button sizes: play=44x32, small=30x26 (in dialog units)
4220          int playButtonWidth, playButtonHeight, smallButtonWidth, smallButtonHeight, buttonSpacing;
4221          DluToPixels(44, 32, &playButtonWidth, &playButtonHeight);
4222          DluToPixels(30, 26, &smallButtonWidth, &smallButtonHeight);
4223          DluToPixels(2, 0, &buttonSpacing, nullptr);
4224  
4225          // Count actual buttons present to calculate total width
4226          HWND hSkipStart = GetDlgItem(hDlg, IDC_TRIM_SKIP_START);
4227          HWND hRewind = GetDlgItem(hDlg, IDC_TRIM_REWIND);
4228          HWND hPlayPause = GetDlgItem(hDlg, IDC_TRIM_PLAY_PAUSE);
4229          HWND hForward = GetDlgItem(hDlg, IDC_TRIM_FORWARD);
4230          HWND hSkipEnd = GetDlgItem(hDlg, IDC_TRIM_SKIP_END);
4231  
4232          int numSmallButtons = 0;
4233          int numPlayButtons = 0;
4234          if (hSkipStart) numSmallButtons++;
4235          if (hRewind) numSmallButtons++;
4236          if (hPlayPause) numPlayButtons++;
4237          if (hForward) numSmallButtons++;
4238          if (hSkipEnd) numSmallButtons++;
4239  
4240          const int numButtons = numSmallButtons + numPlayButtons;
4241          const int totalButtonWidth = smallButtonWidth * numSmallButtons + playButtonWidth * numPlayButtons +
4242              buttonSpacing * (numButtons > 0 ? numButtons - 1 : 0);
4243          int buttonX = (clientWidth - totalButtonWidth) / 2;
4244  
4245          if (hSkipStart)
4246          {
4247              const int yOffset = (buttonRowHeight - smallButtonHeight) / 2;
4248              SetWindowPos(hSkipStart, nullptr, buttonX, buttonRowY + yOffset, smallButtonWidth, smallButtonHeight,
4249                  SWP_NOZORDER | SWP_NOACTIVATE);
4250              buttonX += smallButtonWidth + buttonSpacing;
4251          }
4252  
4253          if (hRewind)
4254          {
4255              const int yOffset = (buttonRowHeight - smallButtonHeight) / 2;
4256              SetWindowPos(hRewind, nullptr, buttonX, buttonRowY + yOffset, smallButtonWidth, smallButtonHeight,
4257                  SWP_NOZORDER | SWP_NOACTIVATE);
4258              buttonX += smallButtonWidth + buttonSpacing;
4259          }
4260  
4261          if (hPlayPause)
4262          {
4263              SetWindowPos(hPlayPause, nullptr, buttonX, buttonRowY, playButtonWidth, playButtonHeight,
4264                  SWP_NOZORDER | SWP_NOACTIVATE);
4265              buttonX += playButtonWidth + buttonSpacing;
4266          }
4267  
4268          if (hForward)
4269          {
4270              const int yOffset = (buttonRowHeight - smallButtonHeight) / 2;
4271              SetWindowPos(hForward, nullptr, buttonX, buttonRowY + yOffset, smallButtonWidth, smallButtonHeight,
4272                  SWP_NOZORDER | SWP_NOACTIVATE);
4273              buttonX += smallButtonWidth + buttonSpacing;
4274          }
4275  
4276          if (hSkipEnd)
4277          {
4278              const int yOffset = (buttonRowHeight - smallButtonHeight) / 2;
4279              SetWindowPos(hSkipEnd, nullptr, buttonX, buttonRowY + yOffset, smallButtonWidth, smallButtonHeight,
4280                  SWP_NOZORDER | SWP_NOACTIVATE);
4281              buttonX += smallButtonWidth + buttonSpacing;
4282          }
4283  
4284          // Position volume icon and slider (to the right of playback buttons)
4285          int volumeIconWidth, volumeIconHeight, volumeSliderWidth, volumeSliderHeight, volumeSpacing;
4286          DluToPixels(14, 12, &volumeIconWidth, &volumeIconHeight);
4287          DluToPixels(70, 14, &volumeSliderWidth, &volumeSliderHeight);
4288          DluToPixels(8, 0, &volumeSpacing, nullptr);
4289  
4290          HWND hVolumeIcon = GetDlgItem(hDlg, IDC_TRIM_VOLUME_ICON);
4291          HWND hVolumeSlider = GetDlgItem(hDlg, IDC_TRIM_VOLUME);
4292  
4293          if (hVolumeIcon)
4294          {
4295              const int iconX = buttonX + volumeSpacing;
4296              const int iconY = buttonRowY + (buttonRowHeight - volumeIconHeight) / 2;
4297              SetWindowPos(hVolumeIcon, nullptr, iconX, iconY, volumeIconWidth, volumeIconHeight,
4298                  SWP_NOZORDER | SWP_NOACTIVATE);
4299          }
4300  
4301          if (hVolumeSlider)
4302          {
4303              const int sliderX = buttonX + volumeSpacing + volumeIconWidth + 4;
4304              const int sliderY = buttonRowY + (buttonRowHeight - volumeSliderHeight) / 2;
4305              SetWindowPos(hVolumeSlider, nullptr, sliderX, sliderY, volumeSliderWidth, volumeSliderHeight,
4306                  SWP_NOZORDER | SWP_NOACTIVATE);
4307          }
4308  
4309          // Position OK/Cancel buttons (right-aligned)
4310          int okCancelWidth, okCancelSpacingH;
4311          DluToPixels(50, 0, &okCancelWidth, nullptr);
4312          DluToPixels(4, 0, &okCancelSpacingH, nullptr);
4313  
4314          HWND hCancel = GetDlgItem(hDlg, IDCANCEL);
4315          if (hCancel)
4316          {
4317              const int cancelX = clientWidth - marginRight - okCancelWidth;
4318              SetWindowPos(hCancel, nullptr, cancelX, okCancelY, okCancelWidth, okCancelHeight,
4319                  SWP_NOZORDER | SWP_NOACTIVATE);
4320          }
4321  
4322          HWND hOK = GetDlgItem(hDlg, IDOK);
4323          if (hOK)
4324          {
4325              const int okX = clientWidth - marginRight - okCancelWidth - okCancelSpacingH - okCancelWidth;
4326              SetWindowPos(hOK, nullptr, okX, okCancelY, okCancelWidth, okCancelHeight,
4327                  SWP_NOZORDER | SWP_NOACTIVATE);
4328          }
4329  
4330          // Re-enable redraw and repaint the entire dialog
4331          SendMessage(hDlg, WM_SETREDRAW, TRUE, 0);
4332          // Use RDW_ERASE for the dialog, but invalidate timeline separately without erase to prevent flicker
4333          HWND hTimelineCtrl = GetDlgItem(hDlg, IDC_TRIM_TIMELINE);
4334          RedrawWindow(hDlg, nullptr, nullptr, RDW_ERASE | RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN);
4335          if (hTimelineCtrl)
4336          {
4337              // Redraw timeline without erase - double buffering handles the background
4338              RedrawWindow(hTimelineCtrl, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW);
4339          }
4340          return 0;
4341      }
4342  
4343      case WMU_PREVIEW_READY:
4344      {
4345          // Video preview loaded - refresh preview area
4346          pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
4347          if (pData)
4348          {
4349                  KillTimer(hDlg, kPlaybackTimerId);
4350              InvalidateRect(GetDlgItem(hDlg, IDC_TRIM_PREVIEW), nullptr, FALSE);
4351          }
4352          return TRUE;
4353      }
4354  
4355      case WMU_PREVIEW_SCHEDULED:
4356      {
4357          pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
4358          if (pData)
4359          {
4360              UpdateVideoPreview(hDlg, pData);
4361          }
4362          return TRUE;
4363      }
4364  
4365      case WMU_DURATION_CHANGED:
4366      {
4367          pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
4368          if (pData)
4369          {
4370              // If the user hasn't manually trimmed (selection was at estimated full duration),
4371              // update the selection to the actual full video duration
4372              if (pData->trimEnd.count() >= pData->originalTrimEnd.count())
4373              {
4374                  pData->trimEnd = pData->videoDuration;
4375                  pData->originalTrimEnd = pData->videoDuration;
4376              }
4377              // Clamp trimEnd to actual duration if it exceeds
4378              if (pData->trimEnd.count() > pData->videoDuration.count())
4379              {
4380                  pData->trimEnd = pData->videoDuration;
4381              }
4382  
4383              if (pData->currentPosition.count() > pData->trimEnd.count())
4384              {
4385                  pData->currentPosition = pData->trimEnd;
4386              }
4387              UpdateDurationDisplay(hDlg, pData);
4388              UpdatePositionUI(hDlg, pData);
4389          }
4390          return TRUE;
4391      }
4392  
4393      case WMU_PLAYBACK_POSITION:
4394      {
4395          pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
4396          if (pData)
4397          {
4398              // Always move the playhead smoothly
4399              UpdatePositionUI(hDlg, pData);
4400  
4401              // Throttle expensive thumbnail generation while playing
4402              const int64_t currentTicks = pData->currentPosition.count();
4403              const int64_t lastTicks = pData->lastRenderedPreview.load(std::memory_order_relaxed);
4404              if (!pData->loadingPreview.load(std::memory_order_relaxed))
4405              {
4406                  const int64_t delta = (lastTicks < 0) ? kPreviewMinDeltaTicks : std::llabs(currentTicks - lastTicks);
4407                  if (delta >= kPreviewMinDeltaTicks)
4408                  {
4409                      UpdateVideoPreview(hDlg, pData, false);
4410                  }
4411              }
4412          }
4413          return TRUE;
4414      }
4415  
4416      case WMU_PLAYBACK_STOP:
4417      {
4418          pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
4419          if (!pData)
4420          {
4421              return TRUE;
4422          }
4423  
4424          // Force UI + session back to the left grip (trim start) position.
4425          pData->currentPosition = pData->trimStart;
4426  #if _DEBUG
4427          OutputDebugStringW((L"[Trim] WMU_PLAYBACK_STOP: resetting to trimStart=" +
4428              std::to_wstring(pData->trimStart.count()) + L"\n").c_str());
4429  #endif
4430          StopPlayback(hDlg, pData, false);
4431  
4432          // Fast path: if we have a cached frame at the trim start position, restore it instantly.
4433          bool usedCachedFrame = false;
4434          if (pData->hCachedStartFrame &&
4435              pData->cachedStartFramePosition.count() == pData->trimStart.count())
4436          {
4437              std::lock_guard<std::mutex> lock(pData->previewBitmapMutex);
4438              if (pData->hCachedStartFrame)  // Double-check under lock
4439              {
4440                  // Swap the cached frame into the preview
4441                  if (pData->hPreviewBitmap && pData->previewBitmapOwned)
4442                  {
4443                      DeleteObject(pData->hPreviewBitmap);
4444                  }
4445                  pData->hPreviewBitmap = pData->hCachedStartFrame;
4446                  pData->previewBitmapOwned = true;
4447                  pData->hCachedStartFrame = nullptr;  // Transferred ownership
4448                  pData->lastRenderedPreview.store(pData->trimStart.count(), std::memory_order_relaxed);
4449                  usedCachedFrame = true;
4450              }
4451          }
4452  
4453          if (usedCachedFrame)
4454          {
4455              // Just update UI - we already have the correct frame
4456              UpdatePositionUI(hDlg, pData, true);
4457              InvalidateRect(GetDlgItem(hDlg, IDC_TRIM_PREVIEW), nullptr, FALSE);
4458          }
4459          else
4460          {
4461              // Fall back to regenerating the preview
4462              UpdateVideoPreview(hDlg, pData);
4463          }
4464          return TRUE;
4465      }
4466  
4467      case WM_DRAWITEM:
4468      {
4469          pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
4470          if (!pData) break;
4471  
4472          DRAWITEMSTRUCT* pDIS = reinterpret_cast<DRAWITEMSTRUCT *> (lParam);
4473  
4474          if (pDIS->CtlID == IDC_TRIM_TIMELINE)
4475          {
4476              // Draw custom timeline
4477              UINT timelineDpi = GetDpiForWindowHelper(pDIS->hwndItem);
4478              DrawTimeline(pDIS->hDC, pDIS->rcItem, pData, timelineDpi);
4479              return TRUE;
4480          }
4481          else if (pDIS->CtlID == IDC_TRIM_PREVIEW)
4482          {
4483              RECT rcFill = pDIS->rcItem;
4484              const int controlWidth = rcFill.right - rcFill.left;
4485              const int controlHeight = rcFill.bottom - rcFill.top;
4486  
4487              std::unique_lock<std::mutex> previewLock(pData->previewBitmapMutex);
4488  
4489              // Create memory DC for double buffering to eliminate flicker
4490              HDC hdcMem = CreateCompatibleDC(pDIS->hDC);
4491              HBITMAP hbmMem = CreateCompatibleBitmap(pDIS->hDC, controlWidth, controlHeight);
4492              HBITMAP hbmOld = static_cast<HBITMAP>(SelectObject(hdcMem, hbmMem));
4493  
4494              // Draw to memory DC
4495              RECT rcMem = { 0, 0, controlWidth, controlHeight };
4496              FillRect(hdcMem, &rcMem, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)));
4497  
4498              if (pData->hPreviewBitmap)
4499              {
4500                  HDC hdcBitmap = CreateCompatibleDC(hdcMem);
4501                  HBITMAP hOldBitmap = static_cast<HBITMAP>(SelectObject(hdcBitmap, pData->hPreviewBitmap));
4502  
4503                  BITMAP bm{};
4504                  GetObject(pData->hPreviewBitmap, sizeof(bm), &bm);
4505  
4506                  int destWidth = 0;
4507                  int destHeight = 0;
4508  
4509                  if (bm.bmWidth > 0 && bm.bmHeight > 0)
4510                  {
4511                      const double scaleX = static_cast<double>(controlWidth) / static_cast<double>(bm.bmWidth);
4512                      const double scaleY = static_cast<double>(controlHeight) / static_cast<double>(bm.bmHeight);
4513                      // Use min to fit entirely within control (letterbox), not max which crops
4514                      const double scale = (std::min)(scaleX, scaleY);
4515  
4516                      destWidth = (std::max)(1, static_cast<int>(std::lround(static_cast<double>(bm.bmWidth) * scale)));
4517                      destHeight = (std::max)(1, static_cast<int>(std::lround(static_cast<double>(bm.bmHeight) * scale)));
4518                  }
4519                  else
4520                  {
4521                      destWidth = controlWidth;
4522                      destHeight = controlHeight;
4523                  }
4524  
4525                  const int offsetX = (controlWidth - destWidth) / 2;
4526                  const int offsetY = (controlHeight - destHeight) / 2;
4527  
4528                  SetStretchBltMode(hdcMem, HALFTONE);
4529                  SetBrushOrgEx(hdcMem, 0, 0, nullptr);
4530                  StretchBlt(hdcMem,
4531                      offsetX,
4532                      offsetY,
4533                      destWidth,
4534                      destHeight,
4535                      hdcBitmap,
4536                      0,
4537                      0,
4538                      bm.bmWidth,
4539                      bm.bmHeight,
4540                      SRCCOPY);
4541  
4542                  SelectObject(hdcBitmap, hOldBitmap);
4543                  DeleteDC(hdcBitmap);
4544              }
4545              else
4546              {
4547                  SetTextColor(hdcMem, RGB(200, 200, 200));
4548                  SetBkMode(hdcMem, TRANSPARENT);
4549                  DrawText(hdcMem, L"Preview not available", -1, &rcMem, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
4550              }
4551  
4552              // Copy the buffered image to the screen
4553              BitBlt(pDIS->hDC, rcFill.left, rcFill.top, controlWidth, controlHeight, hdcMem, 0, 0, SRCCOPY);
4554  
4555              // Clean up
4556              SelectObject(hdcMem, hbmOld);
4557              DeleteObject(hbmMem);
4558              DeleteDC(hdcMem);
4559  
4560              return TRUE;
4561          }
4562          else if (pDIS->CtlID == IDC_TRIM_PLAY_PAUSE || pDIS->CtlID == IDC_TRIM_REWIND ||
4563                   pDIS->CtlID == IDC_TRIM_FORWARD || pDIS->CtlID == IDC_TRIM_SKIP_START ||
4564                   pDIS->CtlID == IDC_TRIM_SKIP_END)
4565          {
4566              DrawPlaybackButton(pDIS, pData);
4567              return TRUE;
4568          }
4569          else if (pDIS->CtlID == IDC_TRIM_VOLUME_ICON)
4570          {
4571              // Draw speaker icon for volume control
4572              int width = pDIS->rcItem.right - pDIS->rcItem.left;
4573              int height = pDIS->rcItem.bottom - pDIS->rcItem.top;
4574              float centerX = pDIS->rcItem.left + width / 2.0f;
4575              float centerY = pDIS->rcItem.top + height / 2.0f;
4576  
4577              Gdiplus::Graphics graphics(pDIS->hDC);
4578              graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
4579  
4580              // Dark background
4581              Gdiplus::SolidBrush bgBrush(Gdiplus::Color(255, 35, 35, 40));
4582              graphics.FillRectangle(&bgBrush, pDIS->rcItem.left, pDIS->rcItem.top, width, height);
4583  
4584              // Icon color - brighter on hover
4585              const bool isHover = pData && pData->hoverVolumeIcon;
4586              COLORREF iconColor = isHover ? RGB(255, 255, 255) : RGB(180, 180, 180);
4587              Gdiplus::SolidBrush iconBrush(Gdiplus::Color(255, GetRValue(iconColor), GetGValue(iconColor), GetBValue(iconColor)));
4588              Gdiplus::Pen iconPen(Gdiplus::Color(255, GetRValue(iconColor), GetGValue(iconColor), GetBValue(iconColor)), 1.2f);
4589  
4590              // Scale for icon
4591              float scale = min(width, height) / 16.0f;
4592  
4593              // Draw speaker body (rectangle + triangle)
4594              float speakerLeft = centerX - 4.0f * scale;
4595              float speakerWidth = 3.0f * scale;
4596              float speakerHeight = 5.0f * scale;
4597              graphics.FillRectangle(&iconBrush, speakerLeft, centerY - speakerHeight / 2.0f, speakerWidth, speakerHeight);
4598  
4599              // Speaker cone (triangle)
4600              Gdiplus::PointF cone[3] = {
4601                  Gdiplus::PointF(speakerLeft + speakerWidth, centerY - speakerHeight / 2.0f),
4602                  Gdiplus::PointF(speakerLeft + speakerWidth + 3.0f * scale, centerY - 4.0f * scale),
4603                  Gdiplus::PointF(speakerLeft + speakerWidth + 3.0f * scale, centerY + 4.0f * scale)
4604              };
4605              Gdiplus::PointF cone2[3] = {
4606                  Gdiplus::PointF(speakerLeft + speakerWidth, centerY + speakerHeight / 2.0f),
4607                  cone[1],
4608                  cone[2]
4609              };
4610              graphics.FillPolygon(&iconBrush, cone, 3);
4611              graphics.FillPolygon(&iconBrush, cone2, 3);
4612  
4613              // Draw sound waves based on volume
4614              if (pData && pData->volume > 0.0)
4615              {
4616                  float waveX = speakerLeft + speakerWidth + 4.0f * scale;
4617  
4618                  // First wave (always visible when volume > 0)
4619                  graphics.DrawArc(&iconPen, waveX, centerY - 2.5f * scale, 3.0f * scale, 5.0f * scale, -60.0f, 120.0f);
4620  
4621                  // Second wave (visible when volume > 33%)
4622                  if (pData->volume > 0.33)
4623                  {
4624                      graphics.DrawArc(&iconPen, waveX + 1.5f * scale, centerY - 4.0f * scale, 4.5f * scale, 8.0f * scale, -60.0f, 120.0f);
4625                  }
4626  
4627                  // Third wave (visible when volume > 66%)
4628                  if (pData->volume > 0.66)
4629                  {
4630                      graphics.DrawArc(&iconPen, waveX + 3.0f * scale, centerY - 5.5f * scale, 6.0f * scale, 11.0f * scale, -60.0f, 120.0f);
4631                  }
4632              }
4633              else if (pData && pData->volume == 0.0)
4634              {
4635                  // Draw X for muted
4636                  float xOffset = speakerLeft + speakerWidth + 5.0f * scale;
4637                  graphics.DrawLine(&iconPen, xOffset, centerY - 2.5f * scale, xOffset + 3.5f * scale, centerY + 2.5f * scale);
4638                  graphics.DrawLine(&iconPen, xOffset, centerY + 2.5f * scale, xOffset + 3.5f * scale, centerY - 2.5f * scale);
4639              }
4640              return TRUE;
4641          }
4642          break;
4643      }
4644  
4645      case WM_DPICHANGED:
4646      {
4647          HandleDialogDpiChange( hDlg, wParam, lParam, currentDpi );
4648          // Invalidate preview and timeline to redraw at new DPI
4649          pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
4650          if (pData)
4651          {
4652              InvalidateRect(GetDlgItem(hDlg, IDC_TRIM_PREVIEW), nullptr, TRUE);
4653              InvalidateRect(GetDlgItem(hDlg, IDC_TRIM_TIMELINE), nullptr, TRUE);
4654          }
4655          return TRUE;
4656      }
4657  
4658      case WM_DESTROY:
4659      {
4660          // Save dialog size before closing
4661          RECT rcDlg{};
4662          if (GetWindowRect(hDlg, &rcDlg))
4663          {
4664              g_TrimDialogWidth = static_cast<DWORD>(rcDlg.right - rcDlg.left);
4665              g_TrimDialogHeight = static_cast<DWORD>(rcDlg.bottom - rcDlg.top);
4666              reg.WriteRegSettings(RegSettings);
4667          }
4668  
4669          pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
4670          if (pData)
4671          {
4672              StopPlayback(hDlg, pData);
4673  
4674              // Ensure MediaPlayer and event handlers are fully released
4675              CleanupMediaPlayer(pData);
4676  
4677              HWND hTimeline = GetDlgItem(hDlg, IDC_TRIM_TIMELINE);
4678              if (hTimeline)
4679              {
4680                  RemoveWindowSubclass(hTimeline, TimelineSubclassProc, 1);
4681              }
4682              HWND hPlayPause = GetDlgItem(hDlg, IDC_TRIM_PLAY_PAUSE);
4683              if (hPlayPause)
4684              {
4685                  RemoveWindowSubclass(hPlayPause, PlaybackButtonSubclassProc, 2);
4686              }
4687              HWND hRewind = GetDlgItem(hDlg, IDC_TRIM_REWIND);
4688              if (hRewind)
4689              {
4690                  RemoveWindowSubclass(hRewind, PlaybackButtonSubclassProc, 3);
4691              }
4692              HWND hForward = GetDlgItem(hDlg, IDC_TRIM_FORWARD);
4693              if (hForward)
4694              {
4695                  RemoveWindowSubclass(hForward, PlaybackButtonSubclassProc, 4);
4696              }
4697              HWND hVolumeIcon = GetDlgItem(hDlg, IDC_TRIM_VOLUME_ICON);
4698              if (hVolumeIcon)
4699              {
4700                  RemoveWindowSubclass(hVolumeIcon, VolumeIconSubclassProc, 7);
4701              }
4702          }
4703          if (pData && pData->hPreviewBitmap)
4704          {
4705              std::lock_guard<std::mutex> lock(pData->previewBitmapMutex);
4706              if (pData->previewBitmapOwned)
4707              {
4708                  DeleteObject(pData->hPreviewBitmap);
4709              }
4710              pData->hPreviewBitmap = nullptr;
4711              // Also clean up cached playback start frame
4712              if (pData->hCachedStartFrame)
4713              {
4714                  DeleteObject(pData->hCachedStartFrame);
4715                  pData->hCachedStartFrame = nullptr;
4716              }
4717          }
4718          if (pData)
4719          {
4720              StopMMTimer(pData);  // Stop multimedia timer if running
4721              pData->playbackFile = nullptr;
4722              CleanupGifFrames(pData);
4723              // Clean up time label font
4724              if (pData->hTimeLabelFont)
4725              {
4726                  DeleteObject(pData->hTimeLabelFont);
4727                  pData->hTimeLabelFont = nullptr;
4728              }
4729          }
4730          hDlgTrimDialog = nullptr;
4731  
4732          ReleaseHighResTimer();
4733          break;
4734      }
4735  
4736      // Multimedia timer tick - handles MP4 and GIF playback with high precision
4737      case WMU_MM_TIMER_TICK:
4738      {
4739          pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
4740          if (!pData)
4741          {
4742              return TRUE;
4743          }
4744  
4745          if (!pData->isPlaying.load(std::memory_order_relaxed))
4746          {
4747              StopMMTimer(pData);
4748              RefreshPlaybackButtons(hDlg);
4749              return TRUE;
4750          }
4751  
4752          // Handle GIF playback
4753          if (pData->isGif && !pData->gifFrames.empty())
4754          {
4755              // Allow playing from before trimStart - only clamp to video bounds
4756              const int64_t clampedTicks = std::clamp<int64_t>(
4757                  pData->currentPosition.count(),
4758                  0,
4759                  pData->videoDuration.count());
4760              const size_t frameIndex = FindGifFrameIndex(pData->gifFrames, clampedTicks);
4761              const auto& frame = pData->gifFrames[frameIndex];
4762  
4763              // Check if enough real time has passed to advance to the next frame
4764              auto now = std::chrono::steady_clock::now();
4765              auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(now - pData->gifFrameStartTime).count();
4766              auto frameDurationMs = frame.duration.count() / 10'000; // Convert 100-ns ticks to ms
4767  
4768              // Update playhead position smoothly based on elapsed time within current frame
4769              const int64_t frameElapsedTicks = static_cast<int64_t>(elapsedMs) * 10'000;
4770              const int64_t smoothPosition = frame.start.count() + (std::min)(frameElapsedTicks, frame.duration.count());
4771              // Allow positions before trimStart - only clamp to trimEnd
4772              const int64_t clampedPosition = (std::min)(smoothPosition, pData->trimEnd.count());
4773  
4774              // Check for end-of-clip BEFORE updating UI to avoid showing the end position
4775              // then immediately jumping back to start
4776              if (clampedPosition >= pData->trimEnd.count())
4777              {
4778                  // Immediately mark as not playing to prevent further position updates
4779                  pData->isPlaying.store(false, std::memory_order_release);
4780                  PostMessage(hDlg, WMU_PLAYBACK_STOP, 0, 0);
4781                  return TRUE;
4782              }
4783  
4784              pData->currentPosition = winrt::TimeSpan{ clampedPosition };
4785  
4786              // Update playhead
4787              HWND hTimeline = GetDlgItem(hDlg, IDC_TRIM_TIMELINE);
4788              if (hTimeline)
4789              {
4790                  const UINT dpi = GetDpiForWindowHelper(hTimeline);
4791                  RECT rc;
4792                  GetClientRect(hTimeline, &rc);
4793                  const int newX = TimelineTimeToClientX(pData, pData->currentPosition, rc.right - rc.left, dpi);
4794                  if (newX != pData->lastPlayheadX)
4795                  {
4796                      InvalidatePlayheadRegion(hTimeline, rc, pData->lastPlayheadX, newX, dpi);
4797                      pData->lastPlayheadX = newX;
4798                      UpdateWindow(hTimeline);
4799                  }
4800              }
4801  
4802              // Show time relative to left grip (trimStart)
4803              {
4804                  const auto relativePos = winrt::TimeSpan{ (std::max)(pData->currentPosition.count() - pData->trimStart.count(), int64_t{ 0 }) };
4805                  SetTimeText(hDlg, IDC_TRIM_POSITION_LABEL, relativePos, true);
4806              }
4807              
4808              if (elapsedMs >= frameDurationMs)
4809              {
4810                  // Time to advance to next frame
4811                  const int64_t nextTicks = frame.start.count() + frame.duration.count();
4812  
4813                  if (nextTicks >= pData->trimEnd.count())
4814                  {
4815                      // Immediately mark as not playing to prevent further position updates
4816                      pData->isPlaying.store(false, std::memory_order_release);
4817                      PostMessage(hDlg, WMU_PLAYBACK_STOP, 0, 0);
4818                  }
4819                  else
4820                  {
4821                      pData->currentPosition = winrt::TimeSpan{ nextTicks };
4822                      pData->gifFrameStartTime = now; // Reset timer for new frame
4823                      UpdateVideoPreview(hDlg, pData);
4824                  }
4825              }
4826              return TRUE;
4827          }
4828  
4829          // Handle MP4 playback
4830          if (pData->mediaPlayer)
4831          {
4832              try
4833              {
4834                  auto session = pData->mediaPlayer.PlaybackSession();
4835                  if (!session)
4836                  {
4837                      StopPlayback(hDlg, pData, false);
4838                      UpdateVideoPreview(hDlg, pData);
4839                      return TRUE;
4840                  }
4841  
4842                  // Simply use MediaPlayer position directly
4843                  auto position = session.Position();
4844                  const int64_t mediaTicks = position.count();
4845  
4846                  // Suppress the transient 0-position report before the initial seek takes effect.
4847                  if (pData->pendingInitialSeek.load(std::memory_order_relaxed) &&
4848                      pData->pendingInitialSeekTicks.load(std::memory_order_relaxed) > 0 &&
4849                      mediaTicks == 0)
4850                  {
4851                      return TRUE;
4852                  }
4853  
4854                  if (mediaTicks != 0)
4855                  {
4856                      pData->pendingInitialSeek.store(false, std::memory_order_relaxed);
4857                      pData->pendingInitialSeekTicks.store(0, std::memory_order_relaxed);
4858                  }
4859  
4860                  // Allow playing from before trimStart - only clamp to video bounds and trimEnd
4861                  const int64_t clampedTicks = std::clamp<int64_t>(
4862                      mediaTicks,
4863                      0,
4864                      pData->trimEnd.count());
4865  
4866                  // Check for end-of-clip BEFORE updating UI to avoid showing the end position
4867                  // then immediately jumping back to start
4868                  if (clampedTicks >= pData->trimEnd.count())
4869                  {
4870                      // Immediately mark as not playing to prevent further position updates
4871                      pData->isPlaying.store(false, std::memory_order_release);
4872                      PostMessage(hDlg, WMU_PLAYBACK_STOP, 0, 0);
4873                  }
4874                  else
4875                  {
4876                      pData->currentPosition = winrt::TimeSpan{ clampedTicks };
4877  
4878                      // Invalidate only the old and new playhead regions for efficiency
4879                      HWND hTimeline = GetDlgItem(hDlg, IDC_TRIM_TIMELINE);
4880                      if (hTimeline)
4881                      {
4882                          const UINT dpi = GetDpiForWindowHelper(hTimeline);
4883                          RECT rc;
4884                          GetClientRect(hTimeline, &rc);
4885                          const int newX = TimelineTimeToClientX(pData, pData->currentPosition, rc.right - rc.left, dpi);
4886                          // Only repaint if position actually changed
4887                          if (newX != pData->lastPlayheadX)
4888                          {
4889                              InvalidatePlayheadRegion(hTimeline, rc, pData->lastPlayheadX, newX, dpi);
4890                              pData->lastPlayheadX = newX;
4891                              UpdateWindow(hTimeline);
4892                          }
4893                      }
4894                      // Show time relative to left grip (trimStart)
4895                      {
4896                          const auto relativePos = winrt::TimeSpan{ (std::max)(pData->currentPosition.count() - pData->trimStart.count(), int64_t{ 0 }) };
4897                          SetTimeText(hDlg, IDC_TRIM_POSITION_LABEL, relativePos, true);
4898                      }
4899                  }
4900              }
4901              catch (...)
4902              {
4903              }
4904          }
4905          return TRUE;
4906      }
4907  
4908      case WM_TIMER:
4909          // WM_TIMER is no longer used for playback; both MP4 and GIF use multimedia timer (WMU_MM_TIMER_TICK)
4910          // This handler is kept for any other timers that might be added in the future
4911          if (wParam == kPlaybackTimerId)
4912          {
4913              // Legacy timer - should not fire anymore, but clean up if it does
4914              KillTimer(hDlg, kPlaybackTimerId);
4915              return TRUE;
4916          }
4917          break;
4918  
4919      case WM_HSCROLL:
4920      {
4921          HWND hVolumeSlider = GetDlgItem(hDlg, IDC_TRIM_VOLUME);
4922          if (reinterpret_cast<HWND>(lParam) == hVolumeSlider)
4923          {
4924              pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
4925              if (pData)
4926              {
4927                  int pos = static_cast<int>(SendMessage(hVolumeSlider, TBM_GETPOS, 0, 0));
4928                  pData->volume = pos / 100.0;
4929  
4930                  // Persist volume setting
4931                  g_TrimDialogVolume = static_cast<DWORD>(pos);
4932                  reg.WriteRegSettings(RegSettings);
4933  
4934                  if (pData->mediaPlayer)
4935                  {
4936                      try
4937                      {
4938                          pData->mediaPlayer.Volume(pData->volume);
4939                          pData->mediaPlayer.IsMuted(pData->volume == 0.0);
4940                      }
4941                      catch (...)
4942                      {
4943                      }
4944                  }
4945                  // Invalidate volume icon to update its appearance
4946                  InvalidateRect(GetDlgItem(hDlg, IDC_TRIM_VOLUME_ICON), nullptr, FALSE);
4947              }
4948              return TRUE;
4949          }
4950          break;
4951      }
4952  
4953      case WM_COMMAND:
4954          switch (LOWORD(wParam))
4955          {
4956          case IDC_TRIM_VOLUME_ICON:
4957          {
4958              if (HIWORD(wParam) == STN_CLICKED)
4959              {
4960                  pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
4961                  if (pData)
4962                  {
4963                      HWND hVolumeSlider = GetDlgItem(hDlg, IDC_TRIM_VOLUME);
4964  
4965                      if (pData->volume > 0.0)
4966                      {
4967                          // Mute: save current volume and set to 0
4968                          pData->previousVolume = pData->volume;
4969                          pData->volume = 0.0;
4970                      }
4971                      else
4972                      {
4973                          // Unmute: restore previous volume (default to 70% if never set)
4974                          pData->volume = (pData->previousVolume > 0.0) ? pData->previousVolume : 0.70;
4975                      }
4976  
4977                      // Update slider position
4978                      if (hVolumeSlider)
4979                      {
4980                          SendMessage(hVolumeSlider, TBM_SETPOS, TRUE, static_cast<LPARAM>(pData->volume * 100));
4981                          // Force full redraw to avoid leftover thumb artifacts
4982                          InvalidateRect(hVolumeSlider, nullptr, TRUE);
4983                      }
4984  
4985                      // Persist volume setting
4986                      g_TrimDialogVolume = static_cast<DWORD>(pData->volume * 100);
4987                      reg.WriteRegSettings(RegSettings);
4988  
4989                      // Apply to media player
4990                      if (pData->mediaPlayer)
4991                      {
4992                          try
4993                          {
4994                              pData->mediaPlayer.Volume(pData->volume);
4995                              pData->mediaPlayer.IsMuted(pData->volume == 0.0);
4996                          }
4997                          catch (...)
4998                          {
4999                          }
5000                      }
5001  
5002                      // Update icon appearance
5003                      InvalidateRect(GetDlgItem(hDlg, IDC_TRIM_VOLUME_ICON), nullptr, FALSE);
5004                  }
5005                  return TRUE;
5006              }
5007              break;
5008          }
5009  
5010          case IDC_TRIM_REWIND:
5011          case IDC_TRIM_PLAY_PAUSE:
5012          case IDC_TRIM_FORWARD:
5013          case IDC_TRIM_SKIP_START:
5014          case IDC_TRIM_SKIP_END:
5015          {
5016              if (HIWORD(wParam) == BN_CLICKED)
5017              {
5018                  pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
5019                  HandlePlaybackCommand(static_cast<int>(LOWORD(wParam)), pData);
5020                  return TRUE;
5021              }
5022              break;
5023          }
5024  
5025          case IDOK:
5026              pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
5027              StopPlayback(hDlg, pData);
5028              // Trim times are already set by mouse dragging
5029              EndDialog(hDlg, IDOK);
5030              return TRUE;
5031  
5032          case IDCANCEL:
5033              pData = reinterpret_cast<TrimDialogData*>(GetWindowLongPtr(hDlg, DWLP_USER));
5034              StopPlayback(hDlg, pData);
5035              EndDialog(hDlg, IDCANCEL);
5036              return TRUE;
5037          }
5038          break;
5039      }
5040  
5041      return FALSE;
5042  }
5043  
5044  //----------------------------------------------------------------------------
5045  //
5046  // VideoRecordingSession::TrimVideoAsync
5047  //
5048  // Performs the actual video trimming operation
5049  //
5050  //----------------------------------------------------------------------------
5051  winrt::IAsyncOperation<winrt::hstring> VideoRecordingSession::TrimVideoAsync(
5052      const std::wstring& sourceVideoPath,
5053      winrt::TimeSpan trimTimeStart,
5054      winrt::TimeSpan trimTimeEnd)
5055  {
5056      try
5057      {
5058          // Load the source video file
5059          auto sourceFile = co_await winrt::StorageFile::GetFileFromPathAsync(sourceVideoPath);
5060  
5061          // Create a media composition
5062          winrt::MediaComposition composition;
5063          auto clip = co_await winrt::MediaClip::CreateFromFileAsync(sourceFile);
5064  
5065          // Set the trim times
5066          clip.TrimTimeFromStart(trimTimeStart);
5067          clip.TrimTimeFromEnd(clip.OriginalDuration() - trimTimeEnd);
5068  
5069          // Add the trimmed clip to the composition
5070          composition.Clips().Append(clip);
5071  
5072          // Create output file in temp folder
5073          auto tempFolder = co_await winrt::StorageFolder::GetFolderFromPathAsync(
5074              std::filesystem::temp_directory_path().wstring());
5075          auto zoomitFolder = co_await tempFolder.CreateFolderAsync(
5076              L"ZoomIt", winrt::CreationCollisionOption::OpenIfExists);
5077  
5078          // Generate unique filename
5079          std::wstring filename = L"zoomit_trimmed_" +
5080              std::to_wstring(GetTickCount64()) + L".mp4";
5081          auto outputFile = co_await zoomitFolder.CreateFileAsync(
5082              filename, winrt::CreationCollisionOption::ReplaceExisting);
5083  
5084          // Render the composition to the output file with fast trimming (no re-encode)
5085          auto renderResult = co_await composition.RenderToFileAsync(
5086              outputFile, winrt::MediaTrimmingPreference::Fast);
5087  
5088          if (renderResult == winrt::TranscodeFailureReason::None)
5089          {
5090              co_return winrt::hstring(outputFile.Path());
5091          }
5092          else
5093          {
5094              co_return winrt::hstring();
5095          }
5096      }
5097      catch (...)
5098      {
5099          co_return winrt::hstring();
5100      }
5101  }
5102  
5103  winrt::IAsyncOperation<winrt::hstring> VideoRecordingSession::TrimGifAsync(
5104      const std::wstring& sourceGifPath,
5105      winrt::TimeSpan trimTimeStart,
5106      winrt::TimeSpan trimTimeEnd)
5107  {
5108      co_await winrt::resume_background();
5109  
5110      try
5111      {
5112          if (trimTimeEnd.count() <= trimTimeStart.count())
5113          {
5114              co_return winrt::hstring();
5115          }
5116  
5117          winrt::com_ptr<IWICImagingFactory> factory;
5118          winrt::check_hresult(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(factory.put())));
5119  
5120          auto sourceFile = co_await winrt::StorageFile::GetFileFromPathAsync(sourceGifPath);
5121          auto sourceStream = co_await sourceFile.OpenAsync(winrt::FileAccessMode::Read);
5122  
5123          winrt::com_ptr<IStream> sourceIStream;
5124          winrt::check_hresult(CreateStreamOverRandomAccessStream(winrt::get_unknown(sourceStream), IID_PPV_ARGS(sourceIStream.put())));
5125  
5126          winrt::com_ptr<IWICBitmapDecoder> decoder;
5127          winrt::check_hresult(factory->CreateDecoderFromStream(sourceIStream.get(), nullptr, WICDecodeMetadataCacheOnLoad, decoder.put()));
5128  
5129          UINT frameCount = 0;
5130          winrt::check_hresult(decoder->GetFrameCount(&frameCount));
5131          if (frameCount == 0)
5132          {
5133              co_return winrt::hstring();
5134          }
5135  
5136          // Prepare output file
5137          auto tempFolder = co_await winrt::StorageFolder::GetFolderFromPathAsync(std::filesystem::temp_directory_path().wstring());
5138          auto zoomitFolder = co_await tempFolder.CreateFolderAsync(L"ZoomIt", winrt::CreationCollisionOption::OpenIfExists);
5139          std::wstring filename = L"zoomit_trimmed_" + std::to_wstring(GetTickCount64()) + L".gif";
5140          auto outputFile = co_await zoomitFolder.CreateFileAsync(filename, winrt::CreationCollisionOption::ReplaceExisting);
5141          auto outputStream = co_await outputFile.OpenAsync(winrt::FileAccessMode::ReadWrite);
5142  
5143          winrt::com_ptr<IStream> outputIStream;
5144          winrt::check_hresult(CreateStreamOverRandomAccessStream(winrt::get_unknown(outputStream), IID_PPV_ARGS(outputIStream.put())));
5145  
5146          winrt::com_ptr<IWICBitmapEncoder> encoder;
5147          winrt::check_hresult(factory->CreateEncoder(GUID_ContainerFormatGif, nullptr, encoder.put()));
5148          winrt::check_hresult(encoder->Initialize(outputIStream.get(), WICBitmapEncoderNoCache));
5149  
5150          // Try to set looping metadata
5151          try
5152          {
5153              winrt::com_ptr<IWICMetadataQueryWriter> encoderMetadataWriter;
5154              if (SUCCEEDED(encoder->GetMetadataQueryWriter(encoderMetadataWriter.put())) && encoderMetadataWriter)
5155              {
5156                  PROPVARIANT prop{};
5157                  PropVariantInit(&prop);
5158                  prop.vt = VT_UI1 | VT_VECTOR;
5159                  prop.caub.cElems = 11;
5160                  prop.caub.pElems = static_cast<UCHAR*>(CoTaskMemAlloc(11));
5161                  if (prop.caub.pElems)
5162                  {
5163                      memcpy(prop.caub.pElems, "NETSCAPE2.0", 11);
5164                      encoderMetadataWriter->SetMetadataByName(L"/appext/application", &prop);
5165                  }
5166                  PropVariantClear(&prop);
5167  
5168                  PropVariantInit(&prop);
5169                  prop.vt = VT_UI1 | VT_VECTOR;
5170                  prop.caub.cElems = 5;
5171                  prop.caub.pElems = static_cast<UCHAR*>(CoTaskMemAlloc(5));
5172                  if (prop.caub.pElems)
5173                  {
5174                      prop.caub.pElems[0] = 3;
5175                      prop.caub.pElems[1] = 1;
5176                      prop.caub.pElems[2] = 0;
5177                      prop.caub.pElems[3] = 0;
5178                      prop.caub.pElems[4] = 0;
5179                      encoderMetadataWriter->SetMetadataByName(L"/appext/data", &prop);
5180                  }
5181                  PropVariantClear(&prop);
5182              }
5183          }
5184          catch (...)
5185          {
5186              // Loop metadata is optional; continue without failing
5187          }
5188  
5189          int64_t cumulativeTicks = 0;
5190          bool wroteFrame = false;
5191  
5192          for (UINT i = 0; i < frameCount; ++i)
5193          {
5194              winrt::com_ptr<IWICBitmapFrameDecode> frame;
5195              if (FAILED(decoder->GetFrame(i, frame.put())))
5196              {
5197                  continue;
5198              }
5199  
5200              UINT delayCs = kGifDefaultDelayCs;
5201              try
5202              {
5203                  winrt::com_ptr<IWICMetadataQueryReader> metadata;
5204                  if (SUCCEEDED(frame->GetMetadataQueryReader(metadata.put())) && metadata)
5205                  {
5206                      PROPVARIANT prop{};
5207                      PropVariantInit(&prop);
5208                      if (SUCCEEDED(metadata->GetMetadataByName(L"/grctlext/Delay", &prop)))
5209                      {
5210                          if (prop.vt == VT_UI2)
5211                          {
5212                              delayCs = prop.uiVal;
5213                          }
5214                          else if (prop.vt == VT_UI1)
5215                          {
5216                              delayCs = prop.bVal;
5217                          }
5218                      }
5219                      PropVariantClear(&prop);
5220                  }
5221              }
5222              catch (...)
5223              {
5224              }
5225  
5226              if (delayCs == 0)
5227              {
5228                  delayCs = kGifDefaultDelayCs;
5229              }
5230  
5231              const int64_t frameStart = cumulativeTicks;
5232              const int64_t frameEnd = frameStart + static_cast<int64_t>(delayCs) * 100'000;
5233              cumulativeTicks = frameEnd;
5234  
5235              if (frameEnd <= trimTimeStart.count() || frameStart >= trimTimeEnd.count())
5236              {
5237                  continue;
5238              }
5239  
5240              const int64_t visibleStart = (std::max)(frameStart, trimTimeStart.count());
5241              const int64_t visibleEnd = (std::min)(frameEnd, trimTimeEnd.count());
5242              const int64_t visibleTicks = visibleEnd - visibleStart;
5243              if (visibleTicks <= 0)
5244              {
5245                  continue;
5246              }
5247  
5248              UINT width = 0;
5249              UINT height = 0;
5250              frame->GetSize(&width, &height);
5251  
5252              winrt::com_ptr<IWICBitmapFrameEncode> frameEncode;
5253              winrt::com_ptr<IPropertyBag2> propertyBag;
5254              winrt::check_hresult(encoder->CreateNewFrame(frameEncode.put(), propertyBag.put()));
5255              winrt::check_hresult(frameEncode->Initialize(propertyBag.get()));
5256              winrt::check_hresult(frameEncode->SetSize(width, height));
5257  
5258              WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat8bppIndexed;
5259              winrt::check_hresult(frameEncode->SetPixelFormat(&pixelFormat));
5260  
5261              winrt::com_ptr<IWICFormatConverter> converter;
5262              winrt::check_hresult(factory->CreateFormatConverter(converter.put()));
5263              winrt::check_hresult(converter->Initialize(frame.get(), GUID_WICPixelFormat32bppBGRA, WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom));
5264  
5265              winrt::check_hresult(frameEncode->WriteSource(converter.get(), nullptr));
5266  
5267              try
5268              {
5269                  winrt::com_ptr<IWICMetadataQueryWriter> frameMetadataWriter;
5270                  if (SUCCEEDED(frameEncode->GetMetadataQueryWriter(frameMetadataWriter.put())) && frameMetadataWriter)
5271                  {
5272                      PROPVARIANT prop{};
5273                      PropVariantInit(&prop);
5274                      prop.vt = VT_UI2;
5275                      // Convert ticks (100ns) to centiseconds with rounding and minimum 1
5276                      const int64_t roundedCs = (visibleTicks + 50'000) / 100'000;
5277                      prop.uiVal = static_cast<USHORT>((std::max<int64_t>)(1, roundedCs));
5278                      frameMetadataWriter->SetMetadataByName(L"/grctlext/Delay", &prop);
5279                      PropVariantClear(&prop);
5280  
5281                      PropVariantInit(&prop);
5282                      prop.vt = VT_UI1;
5283                      prop.bVal = 2; // restore to background
5284                      frameMetadataWriter->SetMetadataByName(L"/grctlext/Disposal", &prop);
5285                      PropVariantClear(&prop);
5286                  }
5287              }
5288              catch (...)
5289              {
5290              }
5291  
5292              winrt::check_hresult(frameEncode->Commit());
5293              wroteFrame = true;
5294          }
5295  
5296          winrt::check_hresult(encoder->Commit());
5297  
5298          if (!wroteFrame)
5299          {
5300              co_return winrt::hstring();
5301          }
5302  
5303          co_return winrt::hstring(outputFile.Path());
5304      }
5305      catch (...)
5306      {
5307          co_return winrt::hstring();
5308      }
5309  }