From ba112d7aa69ffa04bd95b80f0faa4177c9a9f16a Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Wed, 1 Jul 2026 06:35:05 +0200 Subject: [PATCH] refactor(windows): remove legacy mpv compositing, require DComp engine --- windows/runner/CMakeLists.txt | 3 - windows/runner/main.cpp | 8 + windows/runner/mpv/mpv_container.cpp | 96 ------------ windows/runner/mpv/mpv_container.h | 41 ------ windows/runner/mpv/mpv_core.cpp | 187 ------------------------ windows/runner/mpv/mpv_core.h | 63 -------- windows/runner/mpv/mpv_player.cpp | 123 +++++++++++----- windows/runner/mpv/mpv_player.h | 11 +- windows/runner/mpv/mpv_plugin.cpp | 53 ++----- windows/runner/mpv/mpv_plugin.h | 1 - windows/runner/mpv/utils.cpp | 111 -------------- windows/runner/mpv/utils.h | 18 --- windows/tool/install-patched-engine.ps1 | 74 ++++++++++ windows/tool/swap-engine.ps1 | 61 ++++++++ 14 files changed, 247 insertions(+), 603 deletions(-) delete mode 100644 windows/runner/mpv/mpv_container.cpp delete mode 100644 windows/runner/mpv/mpv_container.h delete mode 100644 windows/runner/mpv/mpv_core.cpp delete mode 100644 windows/runner/mpv/mpv_core.h delete mode 100644 windows/runner/mpv/utils.cpp delete mode 100644 windows/runner/mpv/utils.h create mode 100644 windows/tool/install-patched-engine.ps1 create mode 100644 windows/tool/swap-engine.ps1 diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt index 1c942a08..def9e4be 100644 --- a/windows/runner/CMakeLists.txt +++ b/windows/runner/CMakeLists.txt @@ -11,9 +11,6 @@ add_executable(${BINARY_NAME} WIN32 "main.cpp" "utils.cpp" "win32_window.cpp" - "mpv/utils.cpp" - "mpv/mpv_container.cpp" - "mpv/mpv_core.cpp" "mpv/display_mode_manager.cpp" "mpv/mpv_player.cpp" "mpv/mpv_plugin.cpp" diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 176a3213..3c1551c9 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -8,6 +8,14 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t* command_line, _In_ int show_command) { + // Plezy requires the bundled flutter-plezy engine, which presents the Flutter + // UI on a topmost DirectComposition visual when FLUTTER_WINDOWS_DCOMP is set. + // The mpv video window is then a plain child composed beneath the UI in the + // same HWND: window capture (Discord/OBS) works, there is no transparency + // hack, and min/max animations are native. Must be set before the engine is + // created. (On a stock engine the flag is a no-op and compositing breaks.) + ::SetEnvironmentVariableW(L"FLUTTER_WINDOWS_DCOMP", L"1"); + // Single instance enforcement HANDLE mutex = CreateMutex(nullptr, TRUE, L"com.edde746.Plezy.SingleInstance"); if (GetLastError() == ERROR_ALREADY_EXISTS) { diff --git a/windows/runner/mpv/mpv_container.cpp b/windows/runner/mpv/mpv_container.cpp deleted file mode 100644 index 25abdb2b..00000000 --- a/windows/runner/mpv/mpv_container.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "mpv_container.h" - -#include - -namespace mpv { - -MpvContainer* MpvContainer::GetInstance() { return instance_.get(); } - -HWND MpvContainer::Create() { - auto window_class = WNDCLASSEX{}; - ::SecureZeroMemory(&window_class, sizeof(window_class)); - window_class.cbSize = sizeof(window_class); - window_class.style = 0; - window_class.lpfnWndProc = WindowProc; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.lpszClassName = kClassName; - window_class.hCursor = ::LoadCursorW(nullptr, IDC_ARROW); - window_class.hbrBackground = ::CreateSolidBrush(RGB(0, 0, 0)); - - ::RegisterClassExW(&window_class); - - // Use WS_POPUP for a borderless window without title bar. - // Use WS_EX_TOOLWINDOW | WS_EX_NOREDIRECTIONBITMAP to prevent shadow and DWM effects. - handle_ = ::CreateWindowExW( - WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_NOREDIRECTIONBITMAP, kClassName, kWindowName, WS_POPUP, 0, 0, 100, - 100, nullptr, nullptr, GetModuleHandle(nullptr), nullptr); - - // Disable DWM animations on the container. - auto disable_window_transitions = TRUE; - DwmSetWindowAttribute( - handle_, DWMWA_TRANSITIONS_FORCEDISABLED, &disable_window_transitions, sizeof(disable_window_transitions)); - - return handle_; -} - -HWND MpvContainer::Get(HWND flutter_window) { - if (!handle_) { - Create(); - } - - RECT window_rect; - ::GetWindowRect(flutter_window, &window_rect); - ::SetWindowPos( - handle_, flutter_window, window_rect.left, window_rect.top, window_rect.right - window_rect.left, - window_rect.bottom - window_rect.top, SWP_NOACTIVATE); - ::SetWindowLongPtr(handle_, GWLP_USERDATA, reinterpret_cast(flutter_window)); - - ::ShowWindow(handle_, SW_SHOWNOACTIVATE); - ::SetFocus(flutter_window); - - return handle_; -} - -LRESULT CALLBACK -MpvContainer::WindowProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: { - ::PostQuitMessage(0); - return 0; - } - case WM_MOUSEMOVE: - case WM_SIZE: - case WM_MOVE: - case WM_MOVING: - case WM_ACTIVATE: - case WM_WINDOWPOSCHANGED: { - // Redirect focus to Flutter window. - auto user_data = ::GetWindowLongPtr(window, GWLP_USERDATA); - if (user_data) { - HWND flutter_window = reinterpret_cast(user_data); - // Don't try to foreground a minimized window (Windows refuses and flashes - // the taskbar button instead), and don't steal foreground from another - // application. Only redirect focus when our own window already owns the - // foreground. - if (!::IsIconic(flutter_window)) { - HWND foreground = ::GetForegroundWindow(); - if (foreground == flutter_window || foreground == window) { - ::SetForegroundWindow(flutter_window); - } - } - } - break; - } - case WM_ERASEBKGND: { - // Prevent erasing to avoid flicker. - return 1; - } - default: - break; - } - return ::DefWindowProc(window, message, wparam, lparam); -} - -std::unique_ptr MpvContainer::instance_ = std::make_unique(); - -} // namespace mpv diff --git a/windows/runner/mpv/mpv_container.h b/windows/runner/mpv/mpv_container.h deleted file mode 100644 index c7ac77fa..00000000 --- a/windows/runner/mpv/mpv_container.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef MPV_CONTAINER_H_ -#define MPV_CONTAINER_H_ - -#include - -#include - -namespace mpv { - -// Container window that holds the mpv video window behind Flutter. -// This is a singleton that creates a hidden window with no taskbar entry. -class MpvContainer { - public: - static MpvContainer* GetInstance(); - - MpvContainer() = default; - ~MpvContainer() = default; - - // Creates the container window. - HWND Create(); - - // Gets the container window handle, positioning it relative to the Flutter window. - HWND Get(HWND flutter_window); - - // Returns the raw handle. - HWND handle() const { return handle_; } - - private: - static LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept; - - HWND handle_ = nullptr; - - static constexpr wchar_t kClassName[] = L"MPV_CONTAINER"; - static constexpr wchar_t kWindowName[] = L""; - - static std::unique_ptr instance_; -}; - -} // namespace mpv - -#endif // MPV_CONTAINER_H_ diff --git a/windows/runner/mpv/mpv_core.cpp b/windows/runner/mpv/mpv_core.cpp deleted file mode 100644 index 611866a7..00000000 --- a/windows/runner/mpv/mpv_core.cpp +++ /dev/null @@ -1,187 +0,0 @@ -#include "mpv_core.h" - -#include - -#include "mpv_container.h" -#include "utils.h" - -namespace mpv { - -MpvCore* MpvCore::GetInstance() { return instance_.get(); } - -void MpvCore::SetInstance(std::unique_ptr instance) { instance_ = std::move(instance); } - -MpvCore::MpvCore(HWND flutter_window) : flutter_window_(flutter_window) {} - -MpvCore::~MpvCore() { - // Close all mpv views. - for (const auto& [mpv_view, rect] : mpv_views_) { - ::SendMessage(mpv_view, WM_CLOSE, 0, 0); - } - mpv_views_.clear(); -} - -void MpvCore::EnsureInitialized() { - // Get container - composition will be enabled in SetVisible() to batch DwmFlush calls - container_ = MpvContainer::GetInstance()->Get(flutter_window_); -} - -void MpvCore::CreateMpvView(HWND mpv_hwnd, RECT rect, double device_pixel_ratio) { - ::SetParent(mpv_hwnd, container_); - ::ShowWindow(mpv_hwnd, SW_SHOW); - - // Remove window decorations. - auto style = ::GetWindowLongPtr(mpv_hwnd, GWL_STYLE); - style &= ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX); - ::SetWindowLongPtr(mpv_hwnd, GWL_STYLE, style); - - device_pixel_ratio_ = device_pixel_ratio; - mpv_views_[mpv_hwnd] = rect; - - // Position the mpv view behind the Flutter window. - auto global_rect = GetGlobalRect(rect.left, rect.top, rect.right, rect.bottom); - ::SetWindowPos( - mpv_hwnd, flutter_window_, global_rect.left, global_rect.top, global_rect.right - global_rect.left, - global_rect.bottom - global_rect.top, SWP_NOACTIVATE); -} - -void MpvCore::ResizeMpvView(HWND mpv_hwnd, RECT rect) { - mpv_views_[mpv_hwnd] = rect; - auto global_rect = GetGlobalRect(rect.left, rect.top, rect.right, rect.bottom); - // Use MoveWindow to trigger redraw. - ::MoveWindow( - mpv_hwnd, global_rect.left, global_rect.top, global_rect.right - global_rect.left, - global_rect.bottom - global_rect.top, TRUE); -} - -void MpvCore::DisposeMpvView(HWND mpv_hwnd) { - ::SendMessage(mpv_hwnd, WM_CLOSE, 0, 0); - mpv_views_.erase(mpv_hwnd); -} - -void MpvCore::SetVisible(bool visible) { - visible_ = visible; - if (container_) { - if (visible) { - EnableComposition(); - } else { - DisableComposition(); - } - } -} - -std::optional MpvCore::WindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { - switch (message) { - case WM_ACTIVATE: { - RECT window_rect; - ::GetWindowRect(flutter_window_, &window_rect); - // Position container behind Flutter window. - ::SetWindowPos( - container_, flutter_window_, window_rect.left, window_rect.top, window_rect.right - window_rect.left, - window_rect.bottom - window_rect.top, SWP_NOACTIVATE); - break; - } - case WM_SIZE: { - // Handle Windows's minimize & maximize animations properly. - // During these transitions, we hide the container and make Flutter opaque, - // then restore after the animation completes using a Windows timer. - if (wparam != SIZE_RESTORED || last_wm_size_wparam_ == SIZE_MINIMIZED || last_wm_size_wparam_ == SIZE_MAXIMIZED || - was_window_hidden_due_to_minimize_) { - was_window_hidden_due_to_minimize_ = false; - DisableComposition(); - - // Cancel any pending timer and set a new one. - ::KillTimer(flutter_window_, kCompositionRestoreTimerId); - ::SetTimer(flutter_window_, kCompositionRestoreTimerId, kPositionAndShowDelay, nullptr); - } - last_wm_size_wparam_ = wparam; - break; - } - case WM_TIMER: { - if (wparam == kCompositionRestoreTimerId) { - ::KillTimer(flutter_window_, kCompositionRestoreTimerId); - - // Update container position to match current Flutter window bounds - RECT window_rect; - ::GetWindowRect(flutter_window_, &window_rect); - ::SetWindowPos( - container_, flutter_window_, window_rect.left, window_rect.top, window_rect.right - window_rect.left, - window_rect.bottom - window_rect.top, SWP_NOACTIVATE); - - // Restore transparency if video is visible - if (visible_) { - EnableComposition(); - - // Force a redraw to ensure Flutter's render surface is correctly sized - ::RedrawWindow(flutter_window_, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN); - } - } - break; - } - case WM_WINDOWPOSCHANGED: { - RECT window_rect; - ::GetWindowRect(flutter_window_, &window_rect); - if (window_rect.right - window_rect.left > 0 && window_rect.bottom - window_rect.top > 0) { - ::SetWindowPos( - container_, flutter_window_, window_rect.left, window_rect.top, window_rect.right - window_rect.left, - window_rect.bottom - window_rect.top, SWP_NOACTIVATE); - // Window is minimized (negative coordinates). - if (window_rect.left < 0 && window_rect.top < 0 && window_rect.right < 0 && window_rect.bottom < 0) { - DisableComposition(); - was_window_hidden_due_to_minimize_ = true; - } - } - break; - } - case WM_CLOSE: { - ::SendMessage(container_, WM_CLOSE, 0, 0); - for (const auto& [mpv_view, rect] : mpv_views_) { - ::SendMessage(mpv_view, WM_CLOSE, 0, 0); - } - mpv_views_.clear(); - break; - } - default: - break; - } - return std::nullopt; -} - -RECT MpvCore::GetGlobalRect(int32_t left, int32_t top, int32_t right, int32_t bottom) { - // Expand client area to prevent transparent gaps. - left -= static_cast(ceil(device_pixel_ratio_)); - top -= static_cast(ceil(device_pixel_ratio_)); - right += static_cast(ceil(device_pixel_ratio_)); - bottom += static_cast(ceil(device_pixel_ratio_)); - - RECT window_rect; - ::GetClientRect(flutter_window_, &window_rect); - RECT rect; - rect.left = window_rect.left + left; - rect.top = window_rect.top + top; - rect.right = window_rect.left + right; - rect.bottom = window_rect.top + bottom; - return rect; -} - -void MpvCore::EnableComposition() { - ::SetWindowPos( - flutter_window_, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); - if (!composition_enabled_) { - SetWindowComposition(flutter_window_, 2, 0); - composition_enabled_ = true; - } - ::ShowWindow(container_, SW_SHOWNOACTIVATE); - ::DwmFlush(); -} - -void MpvCore::DisableComposition() { - SetWindowComposition(flutter_window_, 0, 0); - composition_enabled_ = false; - ::ShowWindow(container_, SW_HIDE); - ::DwmFlush(); -} - -std::unique_ptr MpvCore::instance_ = nullptr; - -} // namespace mpv diff --git a/windows/runner/mpv/mpv_core.h b/windows/runner/mpv/mpv_core.h deleted file mode 100644 index f12b5ec5..00000000 --- a/windows/runner/mpv/mpv_core.h +++ /dev/null @@ -1,63 +0,0 @@ -#ifndef MPV_CORE_H_ -#define MPV_CORE_H_ - -#include - -#include -#include -#include -#include - -namespace mpv { - -// Core class for managing z-order and window positioning for mpv video window. -// Handles transparency, minimize/maximize animations, and position syncing. -class MpvCore { - public: - static constexpr auto kPositionAndShowDelay = 300; - static constexpr UINT_PTR kCompositionRestoreTimerId = 1001; - - static MpvCore* GetInstance(); - static void SetInstance(std::unique_ptr instance); - - explicit MpvCore(HWND flutter_window); - ~MpvCore(); - - // Initializes transparency on the Flutter window. - void EnsureInitialized(); - - // Creates and positions the mpv video view. - void CreateMpvView(HWND mpv_hwnd, RECT rect, double device_pixel_ratio); - - // Updates the mpv view position. - void ResizeMpvView(HWND mpv_hwnd, RECT rect); - - // Disposes the mpv view. - void DisposeMpvView(HWND mpv_hwnd); - - // Shows or hides the mpv view. - void SetVisible(bool visible); - - // Window procedure handler for Flutter window messages. - std::optional WindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); - - private: - RECT GetGlobalRect(int32_t left, int32_t top, int32_t right, int32_t bottom); - void EnableComposition(); - void DisableComposition(); - - HWND flutter_window_ = nullptr; - HWND container_ = nullptr; - double device_pixel_ratio_ = 1.0; - std::map mpv_views_; - WPARAM last_wm_size_wparam_ = SIZE_RESTORED; - bool was_window_hidden_due_to_minimize_ = false; - bool visible_ = true; - bool composition_enabled_ = false; - - static std::unique_ptr instance_; -}; - -} // namespace mpv - -#endif // MPV_CORE_H_ diff --git a/windows/runner/mpv/mpv_player.cpp b/windows/runner/mpv/mpv_player.cpp index 68352e9a..92510b8c 100644 --- a/windows/runner/mpv/mpv_player.cpp +++ b/windows/runner/mpv/mpv_player.cpp @@ -1,35 +1,92 @@ #include "mpv_player.h" +#include + #include "sanitize_utf8.h" namespace mpv { +namespace { + +// DComp-mode input forwarding. mpv's inner window lives on mpv's own thread +// and consumes the mouse input over the video (WS_EX_TRANSPARENT hit-test +// skipping is same-thread-only, and disabling the subtree makes the system +// drop the input entirely instead of routing it to a sibling). Subclass the +// inner window (legal within one process, even across threads) and forward +// mouse input to the Flutter view with coordinates translated into view +// space. The subclass proc runs on mpv's thread and only uses thread-safe +// calls (PostMessage / MapWindowPoints / CallWindowProc). +WNDPROC g_mpv_inner_original_proc = nullptr; +HWND g_mpv_inner_hwnd = nullptr; +HWND g_forward_target_view = nullptr; + +LRESULT CALLBACK MpvInnerSubclassProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + if (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST) { + HWND view = g_forward_target_view; + if (view) { + LPARAM forwarded = lparam; + if (message != WM_MOUSEWHEEL && message != WM_MOUSEHWHEEL) { + // Client coordinates: translate inner-window-space -> view-space. + // (Wheel messages carry screen coordinates; pass through unchanged.) + POINT pt = {GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)}; + ::MapWindowPoints(hwnd, view, &pt, 1); + forwarded = MAKELPARAM(pt.x, pt.y); + } + ::PostMessage(view, message, wparam, forwarded); + } + return 0; + } + return ::CallWindowProc(g_mpv_inner_original_proc, hwnd, message, wparam, lparam); +} + +// Subclass mpv's lazily-created inner window if it exists and isn't yet +// subclassed (or was recreated). Idempotent; callable from any thread in +// this process. +void EnsureMpvInnerSubclassed(HWND host) { + if (!host) { + return; + } + HWND inner = ::FindWindowExW(host, nullptr, nullptr, nullptr); + if (inner && inner != g_mpv_inner_hwnd) { + g_mpv_inner_hwnd = inner; + g_mpv_inner_original_proc = reinterpret_cast( + ::SetWindowLongPtrW(inner, GWLP_WNDPROC, reinterpret_cast(MpvInnerSubclassProc))); + } +} + +} // namespace + MpvPlayer::MpvPlayer() {} MpvPlayer::~MpvPlayer() { Dispose(); } -bool MpvPlayer::Initialize(HWND container, HWND flutter_window) { +bool MpvPlayer::Initialize(HWND view) { if (mpv_) { return true; // Already initialized. } - container_ = container; - flutter_window_ = flutter_window; - // Create mpv instance. mpv_ = mpv_create(); if (!mpv_) { return false; } - // Create a child window for mpv to render into. - hwnd_ = ::CreateWindowW( - L"STATIC", L"", WS_CHILD | WS_VISIBLE, 0, 0, 100, 100, container, nullptr, GetModuleHandle(nullptr), nullptr); + // Create a child window for mpv to render into, parented to the Flutter + // |view|. The video child then sits in the view's own per-window layer + // stack, above the view's (never-painted) layer-1 content and below the + // engine's topmost DComp visual carrying the UI. WS_CLIPSIBLINGS keeps it + // from painting over neighboring view children. Mouse input over the video + // is delivered to mpv's own inner window (on mpv's thread); the subclass + // installed in EnsureMpvInnerSubclassed forwards it back to the view. + hwnd_ = ::CreateWindowExW( + WS_EX_NOPARENTNOTIFY, L"STATIC", L"", WS_CHILD | WS_CLIPSIBLINGS, 0, 0, 100, 100, view, nullptr, + GetModuleHandle(nullptr), nullptr); if (!hwnd_) { mpv_destroy(mpv_); mpv_ = nullptr; return false; } + g_forward_target_view = view; // Set the wid option to embed mpv in our window. int64_t wid = reinterpret_cast(hwnd_); @@ -114,6 +171,10 @@ void MpvPlayer::Dispose() { hwnd_ = nullptr; } + // The subclassed inner window died with hwnd_; clear the forwarding state. + g_mpv_inner_hwnd = nullptr; + g_mpv_inner_original_proc = nullptr; + observed_properties_.clear(); } @@ -245,39 +306,19 @@ void MpvPlayer::ObserveProperty(const std::string& name, const std::string& form } void MpvPlayer::SetRect(RECT rect, double device_pixel_ratio) { - rect_ = rect; - device_pixel_ratio_ = device_pixel_ratio; - - if (hwnd_ && container_ && flutter_window_) { - // The rect from Dart is in Flutter client area coordinates (0,0 is top-left of Flutter - // content). The container window is positioned to match the Flutter window's full bounds - // (including title bar). We need to offset the mpv window within the container to align with - // Flutter's client area. - - // Get the Flutter window's window rect (screen coordinates, includes title bar) - RECT window_rect; - ::GetWindowRect(flutter_window_, &window_rect); - - // Get the Flutter window's client rect (client coordinates, 0,0 based) - RECT client_rect; - ::GetClientRect(flutter_window_, &client_rect); - - // Convert client area origin to screen coordinates - POINT client_origin = {0, 0}; - ::ClientToScreen(flutter_window_, &client_origin); - - // Calculate the offset from window origin to client area origin - int client_offset_x = client_origin.x - window_rect.left; - int client_offset_y = client_origin.y - window_rect.top; - - // Position the mpv window within the container, offset by the title bar/border size - int left = rect.left + client_offset_x; - int top = rect.top + client_offset_y; - int width = rect.right - rect.left; - int height = rect.bottom - rect.top; - - ::MoveWindow(hwnd_, left, top, width, height, TRUE); + if (!hwnd_) { + return; } + + // The video window is a child of the Flutter view; the Dart rect is already + // in view physical pixels, which is exactly the child coordinate space. No + // screen mapping, no padding. + ::SetWindowPos(hwnd_, HWND_TOP, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOACTIVATE); + + // mpv creates its inner window lazily on its own thread; subclass it (and + // re-subclass if mpv ever recreates it) so mouse input over the video is + // forwarded to the Flutter view. + EnsureMpvInnerSubclassed(hwnd_); } void MpvPlayer::SetVisible(bool visible) { @@ -439,6 +480,10 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) { break; } case MPV_EVENT_PLAYBACK_RESTART: { + // mpv's inner window exists by now (vo is configured); make sure the + // DComp-mode input forwarding subclass is installed. SetRect alone can + // miss it: the rect often settles before mpv creates the window. + EnsureMpvInnerSubclassed(hwnd_); SendEvent("playback-restart"); break; } diff --git a/windows/runner/mpv/mpv_player.h b/windows/runner/mpv/mpv_player.h index 54d28e6a..0a89a550 100644 --- a/windows/runner/mpv/mpv_player.h +++ b/windows/runner/mpv/mpv_player.h @@ -25,8 +25,11 @@ class MpvPlayer { MpvPlayer(); ~MpvPlayer(); - // Initializes mpv and creates the video window. - bool Initialize(HWND container, HWND flutter_window); + // Initializes mpv and creates the video window as a child of the Flutter + // |view| window. The flutter-plezy engine presents the UI on a topmost + // DirectComposition visual, so the video child composites beneath it in the + // same HWND. + bool Initialize(HWND view); // Disposes mpv and the video window. void Dispose(); @@ -87,10 +90,6 @@ class MpvPlayer { mpv_handle* mpv_ = nullptr; HWND hwnd_ = nullptr; - HWND container_ = nullptr; - HWND flutter_window_ = nullptr; - double device_pixel_ratio_ = 1.0; - RECT rect_ = {0, 0, 0, 0}; std::thread event_thread_; std::atomic running_{false}; diff --git a/windows/runner/mpv/mpv_plugin.cpp b/windows/runner/mpv/mpv_plugin.cpp index 8236431e..c732c2ad 100644 --- a/windows/runner/mpv/mpv_plugin.cpp +++ b/windows/runner/mpv/mpv_plugin.cpp @@ -1,8 +1,5 @@ #include "mpv_plugin.h" -#include "mpv_container.h" -#include "mpv_core.h" - static flutter::EncodableMap DisplayModeToMap(const mpv::DisplayMode& mode) { flutter::EncodableMap m; m[flutter::EncodableValue("width")] = flutter::EncodableValue(static_cast(mode.width)); @@ -106,54 +103,43 @@ void MpvPlayerPlugin::HandleMethodCall( const auto& method = method_call.method_name(); if (method == "initialize") { - // Set up MpvCore for z-order management. if (proc_id_) { registrar_->UnregisterTopLevelWindowProcDelegate(proc_id_.value()); proc_id_ = std::nullopt; } - HWND flutter_window = GetWindow(); - flutter_window_ = flutter_window; - - MpvCore::SetInstance(std::make_unique(flutter_window)); + flutter_window_ = GetWindow(); + // The only top-level message we care about is the platform-task wakeup; + // mouse-over-video input is forwarded by the mpv inner-window subclass + // (see MpvPlayer), and compositing/z-order is handled by the engine's + // topmost DComp visual — there is no separate container window to manage. proc_id_ = registrar_->RegisterTopLevelWindowProcDelegate([this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { if (message == kPlatformTaskMessage) { DrainPlatformTasks(); return std::optional(0); } - - auto* core = MpvCore::GetInstance(); - if (core) { - return core->WindowProc(hwnd, message, wparam, lparam); - } return std::optional(std::nullopt); }); - MpvCore::GetInstance()->EnsureInitialized(); + // The video window is a child of the FLUTTER VIEW itself, so DWM's + // per-window layer model puts it above the view's own (layer 1) content + // and below the view's topmost DComp visual carrying the UI (layer 4). As + // a *sibling* of the view, either the view's never-painted white content + // covers the video or the video covers the UI — the in-subtree placement + // is the only ordering that yields white < video < UI. + HWND view = GetChildWindow(); - // Create player - use the container from MpvCore which was set up by EnsureInitialized player_ = std::make_unique(); - HWND container = MpvContainer::GetInstance()->handle(); - - if (!container) { - result->Error("INIT_FAILED", "Failed to create container window"); - return; - } - - bool success = player_->Initialize(container, flutter_window); + bool success = player_->Initialize(view); if (success) { // Set up event callback. player_->SetEventCallback([this](const flutter::EncodableValue& event) { SendEvent(event); }); - // Register the mpv window with core for z-order management. - RECT rect = {0, 0, 100, 100}; - MpvCore::GetInstance()->CreateMpvView(player_->GetHwnd(), rect, 1.0); - // Start hidden. - MpvCore::GetInstance()->SetVisible(false); + player_->SetVisible(false); result->Success(flutter::EncodableValue(true)); } else { player_.reset(); // Clear the player so we don't have a half-initialized state @@ -161,13 +147,8 @@ void MpvPlayerPlugin::HandleMethodCall( } } else if (method == "dispose") { if (player_) { - auto hwnd = player_->GetHwnd(); player_->Dispose(); player_.reset(); - - if (MpvCore::GetInstance() && hwnd) { - MpvCore::GetInstance()->DisposeMpvView(hwnd); - } } result->Success(); } else if (method == "command") { @@ -353,9 +334,6 @@ void MpvPlayerPlugin::HandleMethodCall( if (player_) { player_->SetVisible(visible); } - if (MpvCore::GetInstance()) { - MpvCore::GetInstance()->SetVisible(visible); - } result->Success(); } else if (method == "setVideoRect") { @@ -394,8 +372,7 @@ void MpvPlayerPlugin::HandleMethodCall( rect.bottom = get_int("bottom"); double dpr = get_double("devicePixelRatio"); - if (player_ && MpvCore::GetInstance()) { - MpvCore::GetInstance()->ResizeMpvView(player_->GetHwnd(), rect); + if (player_) { player_->SetRect(rect, dpr); } diff --git a/windows/runner/mpv/mpv_plugin.h b/windows/runner/mpv/mpv_plugin.h index c610c082..72430f3d 100644 --- a/windows/runner/mpv/mpv_plugin.h +++ b/windows/runner/mpv/mpv_plugin.h @@ -15,7 +15,6 @@ #include #include "display_mode_manager.h" -#include "mpv_core.h" #include "mpv_player.h" // C-style registration function for the plugin. diff --git a/windows/runner/mpv/utils.cpp b/windows/runner/mpv/utils.cpp deleted file mode 100644 index 8578aa17..00000000 --- a/windows/runner/mpv/utils.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#include "utils.h" - -namespace mpv { - -typedef enum _WINDOWCOMPOSITIONATTRIB { - WCA_UNDEFINED = 0, - WCA_NCRENDERING_ENABLED = 1, - WCA_NCRENDERING_POLICY = 2, - WCA_TRANSITIONS_FORCEDISABLED = 3, - WCA_ALLOW_NCPAINT = 4, - WCA_CAPTION_BUTTON_BOUNDS = 5, - WCA_NONCLIENT_RTL_LAYOUT = 6, - WCA_FORCE_ICONIC_REPRESENTATION = 7, - WCA_EXTENDED_FRAME_BOUNDS = 8, - WCA_HAS_ICONIC_BITMAP = 9, - WCA_THEME_ATTRIBUTES = 10, - WCA_NCRENDERING_EXILED = 11, - WCA_NCADORNMENTINFO = 12, - WCA_EXCLUDED_FROM_LIVEPREVIEW = 13, - WCA_VIDEO_OVERLAY_ACTIVE = 14, - WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15, - WCA_DISALLOW_PEEK = 16, - WCA_CLOAK = 17, - WCA_CLOAKED = 18, - WCA_ACCENT_POLICY = 19, - WCA_FREEZE_REPRESENTATION = 20, - WCA_EVER_UNCLOAKED = 21, - WCA_VISUAL_OWNER = 22, - WCA_HOLOGRAPHIC = 23, - WCA_EXCLUDED_FROM_DDA = 24, - WCA_PASSIVEUPDATEMODE = 25, - WCA_USEDARKMODECOLORS = 26, - WCA_LAST = 27 -} WINDOWCOMPOSITIONATTRIB; - -typedef struct _WINDOWCOMPOSITIONATTRIBDATA { - WINDOWCOMPOSITIONATTRIB Attrib; - PVOID pvData; - SIZE_T cbData; -} WINDOWCOMPOSITIONATTRIBDATA; - -typedef enum _ACCENT_STATE { - ACCENT_DISABLED = 0, - ACCENT_ENABLE_GRADIENT = 1, - ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, - ACCENT_ENABLE_BLURBEHIND = 3, - ACCENT_ENABLE_ACRYLICBLURBEHIND = 4, - ACCENT_ENABLE_HOSTBACKDROP = 5, - ACCENT_INVALID_STATE = 6 -} ACCENT_STATE; - -typedef struct _ACCENT_POLICY { - ACCENT_STATE AccentState; - DWORD AccentFlags; - DWORD GradientColor; - DWORD AnimationId; -} ACCENT_POLICY; - -typedef BOOL(WINAPI* _SetWindowCompositionAttribute)(HWND, WINDOWCOMPOSITIONATTRIBDATA*); - -static _SetWindowCompositionAttribute g_set_window_composition_attribute = NULL; -static bool g_set_window_composition_attribute_initialized = false; - -typedef LONG NTSTATUS, *PNTSTATUS; -#define STATUS_SUCCESS (0x00000000) - -typedef NTSTATUS(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW); - -static RTL_OSVERSIONINFOW GetWindowsVersion() { - static RTL_OSVERSIONINFOW cached = []() { - HMODULE hmodule = ::GetModuleHandleW(L"ntdll.dll"); - if (hmodule) { - RtlGetVersionPtr rtl_get_version_ptr = (RtlGetVersionPtr)::GetProcAddress(hmodule, "RtlGetVersion"); - if (rtl_get_version_ptr != nullptr) { - RTL_OSVERSIONINFOW rovi = {0}; - rovi.dwOSVersionInfoSize = sizeof(rovi); - if (STATUS_SUCCESS == rtl_get_version_ptr(&rovi)) { - return rovi; - } - } - } - RTL_OSVERSIONINFOW rovi = {0}; - return rovi; - }(); - return cached; -} - -void SetWindowComposition(HWND window, int32_t accent_state, int32_t gradient_color) { - if (GetWindowsVersion().dwBuildNumber >= 18362) { - if (!g_set_window_composition_attribute_initialized) { - auto user32 = ::GetModuleHandleA("user32.dll"); - if (user32) { - g_set_window_composition_attribute = - reinterpret_cast<_SetWindowCompositionAttribute>(::GetProcAddress(user32, "SetWindowCompositionAttribute")); - if (g_set_window_composition_attribute) { - g_set_window_composition_attribute_initialized = true; - } - } - } - if (g_set_window_composition_attribute) { - ACCENT_POLICY accent = {static_cast(accent_state), 2, static_cast(gradient_color), 0}; - WINDOWCOMPOSITIONATTRIBDATA data; - data.Attrib = WCA_ACCENT_POLICY; - data.pvData = &accent; - data.cbData = sizeof(accent); - g_set_window_composition_attribute(window, &data); - } - } -} - -} // namespace mpv diff --git a/windows/runner/mpv/utils.h b/windows/runner/mpv/utils.h deleted file mode 100644 index 7f9d3a37..00000000 --- a/windows/runner/mpv/utils.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef MPV_UTILS_H_ -#define MPV_UTILS_H_ - -#include -#include - -#include - -namespace mpv { - -// Sets window composition attribute for transparency. -// accent_state = 6 enables per-pixel transparency. -// accent_state = 0 makes window opaque. -void SetWindowComposition(HWND window, int32_t accent_state, int32_t gradient_color); - -} // namespace mpv - -#endif // MPV_UTILS_H_ diff --git a/windows/tool/install-patched-engine.ps1 b/windows/tool/install-patched-engine.ps1 new file mode 100644 index 00000000..91f519a1 --- /dev/null +++ b/windows/tool/install-patched-engine.ps1 @@ -0,0 +1,74 @@ +# Download the published flutter-plezy patched Windows engine (DirectComposition) +# and install it into the active Flutter SDK's artifact cache. +# +# Plezy requires this engine on Windows: it honors the FLUTTER_WINDOWS_DCOMP +# environment variable (set in windows/runner/main.cpp) and presents the Flutter +# UI on a topmost DirectComposition visual, so the mpv video window composites +# *beneath* the UI in a single HWND (window capture works, no transparency +# hacks). On a stock engine the flag is a no-op and compositing breaks. +# +# Used by CI (.github/workflows/build.yml) and by contributors building locally. +# Engine developers who build their own artifacts use swap-engine.ps1 instead. +# +# The flutter tool validates the engine cache by engine.stamp string only (no +# file hashing), so the swap sticks - but `flutter upgrade` / +# `flutter precache --force` silently restore the stock engine; re-run this +# afterwards. Run `flutter precache --windows` once before this script so the +# cache layout and engine.stamp exist. +# +# Usage: +# flutter precache --windows +# windows/tool/install-patched-engine.ps1 +param( + # Engine zip published at flutter-plezy release windows-v3.44.0+1 - x64 + arm64 + # (cache dirs windows-{x64,arm64}{,-release}). The asset name is the same across + # tags, so a version bump only changes the tag segment of the URL. + [string]$Url = 'https://github.com/edde746/flutter-plezy/releases/download/windows-v3.44.0+1/flutter-plezy-windows-3.44.0.zip', + [string]$Sha256 = '8de498d28f314c33971226856bea7aa54099e08e60cd7d208cbaf04693dc8274', + # Engine revision the artifacts were built from. Must match the SDK's + # engine.stamp (gen_snapshot/dart in the SDK must come from the same + # checkout), or the swapped binaries are ABI-incompatible with the build. + [string]$ExpectedEngine = '4c525dac5ebe5971c5708ef73558ed8edcf4a362' +) + +$ErrorActionPreference = 'Stop' + +$flutterCmd = Get-Command flutter.bat -ErrorAction SilentlyContinue +if (-not $flutterCmd) { $flutterCmd = Get-Command flutter } +$sdkRoot = Split-Path -Parent (Split-Path -Parent $flutterCmd.Source) + +$stampPath = Join-Path $sdkRoot 'bin\cache\engine.stamp' +if (-not (Test-Path $stampPath)) { + Write-Error "engine.stamp not found at $stampPath - run 'flutter precache --windows' first" +} +$stamp = (Get-Content $stampPath -Raw).Trim() +if ($stamp -ne $ExpectedEngine) { + Write-Error "SDK engine.stamp is $stamp but the patched engine targets $ExpectedEngine - this flutter-plezy build is for a different SDK" +} + +$engineDir = Join-Path $sdkRoot 'bin\cache\artifacts\engine' +if (-not (Test-Path $engineDir)) { + Write-Error "engine cache not found: $engineDir - run 'flutter precache --windows' first" +} + +$zip = Join-Path ([System.IO.Path]::GetTempPath()) 'flutter-plezy-windows.zip' + +# Invoke-WebRequest renders a per-byte progress bar in Windows PowerShell that +# makes large downloads crawl; silence it. +$ProgressPreference = 'SilentlyContinue' +Write-Output "Downloading patched engine from $Url ..." +Invoke-WebRequest -Uri $Url -OutFile $zip + +$actual = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower() +if ($actual -ne $Sha256.ToLower()) { + Remove-Item $zip -Force + Write-Error "SHA256 mismatch: expected $Sha256 but got $actual" +} + +# The zip holds top-level windows-x64/ (debug) and windows-x64-release/ folders; +# extracting over the engine dir swaps both cache variants in place. +Write-Output "Extracting patched engine into $engineDir ..." +Expand-Archive -Path $zip -DestinationPath $engineDir -Force +Remove-Item $zip -Force + +Write-Output "Installed patched DComp engine (windows-x64, windows-x64-release)." diff --git a/windows/tool/swap-engine.ps1 b/windows/tool/swap-engine.ps1 new file mode 100644 index 00000000..0367a44c --- /dev/null +++ b/windows/tool/swap-engine.ps1 @@ -0,0 +1,61 @@ +# Swap the flutter-plezy custom Windows engine into the Flutter SDK's artifact +# cache (or restore stock). See flutter-plezy's README for how the artifacts +# are built. +# +# The flutter tool validates the cache by stamp STRING only (no file hashing), +# so swapped files stick - but `flutter upgrade` / `flutter precache --force` +# silently restore stock; re-run this script afterwards. +# +# Usage: +# swap-engine.ps1 -Mode debug -Zip C:\path\to\windows-x64-flutter.zip +# swap-engine.ps1 -Mode release -Zip ... +# swap-engine.ps1 -Mode debug -Restore +param( + [ValidateSet('debug', 'release')][string]$Mode = 'debug', + [string]$Zip, + [switch]$Restore, + # Engine revision these artifacts were built from. Guards against swapping + # into a mismatched SDK (gen_snapshot/dart must come from the same checkout). + [string]$ExpectedEngine = '4c525dac5ebe5971c5708ef73558ed8edcf4a362' +) + +$ErrorActionPreference = 'Stop' + +$flutterCmd = Get-Command flutter.bat -ErrorAction SilentlyContinue +if (-not $flutterCmd) { $flutterCmd = Get-Command flutter } +$sdkRoot = Split-Path -Parent (Split-Path -Parent $flutterCmd.Source) + +$stamp = (Get-Content (Join-Path $sdkRoot 'bin\cache\engine.stamp') -Raw).Trim() +if ($stamp -ne $ExpectedEngine) { + Write-Error "SDK engine.stamp is $stamp but artifacts target $ExpectedEngine - rebuild flutter-plezy for this SDK first" +} + +$cacheDir = if ($Mode -eq 'debug') { 'windows-x64' } else { 'windows-x64-release' } +$target = Join-Path $sdkRoot "bin\cache\artifacts\engine\$cacheDir" +if (-not (Test-Path $target)) { Write-Error "cache dir not found: $target (run 'flutter precache --windows')" } +$backup = "$target.stock-backup" + +if ($Restore) { + if (-not (Test-Path $backup)) { Write-Error "no backup at $backup - nothing to restore" } + Get-ChildItem $backup -File | ForEach-Object { Copy-Item $_.FullName $target -Force } + Write-Output "restored stock engine into $target" + return +} + +if (-not $Zip -or -not (Test-Path $Zip)) { Write-Error "pass -Zip (from flutter-plezy out\\host_*\zip_archives\ or a Release)" } + +# One-time backup of the stock files we are about to overwrite. +if (-not (Test-Path $backup)) { + New-Item -ItemType Directory $backup | Out-Null + Add-Type -AssemblyName System.IO.Compression.FileSystem + $entries = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $Zip)).Entries | ForEach-Object { $_.FullName } + foreach ($name in $entries) { + $orig = Join-Path $target $name + if (Test-Path $orig) { Copy-Item $orig $backup -Force } + } + Write-Output "stock files backed up to $backup" +} + +Expand-Archive $Zip -DestinationPath $target -Force +Write-Output "swapped $(Split-Path $Zip -Leaf) into $target" +Write-Output "NOTE: re-run after 'flutter upgrade' or 'flutter precache --force'."