refactor(windows): remove legacy mpv compositing, require DComp engine

This commit is contained in:
edde746
2026-07-01 06:37:35 +02:00
parent 28e6e349b3
commit ba112d7aa6
14 changed files with 247 additions and 603 deletions
-3
View File
@@ -11,9 +11,6 @@ add_executable(${BINARY_NAME} WIN32
"main.cpp" "main.cpp"
"utils.cpp" "utils.cpp"
"win32_window.cpp" "win32_window.cpp"
"mpv/utils.cpp"
"mpv/mpv_container.cpp"
"mpv/mpv_core.cpp"
"mpv/display_mode_manager.cpp" "mpv/display_mode_manager.cpp"
"mpv/mpv_player.cpp" "mpv/mpv_player.cpp"
"mpv/mpv_plugin.cpp" "mpv/mpv_plugin.cpp"
+8
View File
@@ -8,6 +8,14 @@
int APIENTRY int APIENTRY
wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t* command_line, _In_ int show_command) { 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 // Single instance enforcement
HANDLE mutex = CreateMutex(nullptr, TRUE, L"com.edde746.Plezy.SingleInstance"); HANDLE mutex = CreateMutex(nullptr, TRUE, L"com.edde746.Plezy.SingleInstance");
if (GetLastError() == ERROR_ALREADY_EXISTS) { if (GetLastError() == ERROR_ALREADY_EXISTS) {
-96
View File
@@ -1,96 +0,0 @@
#include "mpv_container.h"
#include <dwmapi.h>
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<LONG_PTR>(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<HWND>(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> MpvContainer::instance_ = std::make_unique<MpvContainer>();
} // namespace mpv
-41
View File
@@ -1,41 +0,0 @@
#ifndef MPV_CONTAINER_H_
#define MPV_CONTAINER_H_
#include <Windows.h>
#include <memory>
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<MpvContainer> instance_;
};
} // namespace mpv
#endif // MPV_CONTAINER_H_
-187
View File
@@ -1,187 +0,0 @@
#include "mpv_core.h"
#include <dwmapi.h>
#include "mpv_container.h"
#include "utils.h"
namespace mpv {
MpvCore* MpvCore::GetInstance() { return instance_.get(); }
void MpvCore::SetInstance(std::unique_ptr<MpvCore> 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<HRESULT> 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<int32_t>(ceil(device_pixel_ratio_));
top -= static_cast<int32_t>(ceil(device_pixel_ratio_));
right += static_cast<int32_t>(ceil(device_pixel_ratio_));
bottom += static_cast<int32_t>(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> MpvCore::instance_ = nullptr;
} // namespace mpv
-63
View File
@@ -1,63 +0,0 @@
#ifndef MPV_CORE_H_
#define MPV_CORE_H_
#include <Windows.h>
#include <cmath>
#include <map>
#include <memory>
#include <optional>
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<MpvCore> 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<HRESULT> 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<HWND, RECT> 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<MpvCore> instance_;
};
} // namespace mpv
#endif // MPV_CORE_H_
+84 -39
View File
@@ -1,35 +1,92 @@
#include "mpv_player.h" #include "mpv_player.h"
#include <windowsx.h>
#include "sanitize_utf8.h" #include "sanitize_utf8.h"
namespace mpv { 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<WNDPROC>(
::SetWindowLongPtrW(inner, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(MpvInnerSubclassProc)));
}
}
} // namespace
MpvPlayer::MpvPlayer() {} MpvPlayer::MpvPlayer() {}
MpvPlayer::~MpvPlayer() { Dispose(); } MpvPlayer::~MpvPlayer() { Dispose(); }
bool MpvPlayer::Initialize(HWND container, HWND flutter_window) { bool MpvPlayer::Initialize(HWND view) {
if (mpv_) { if (mpv_) {
return true; // Already initialized. return true; // Already initialized.
} }
container_ = container;
flutter_window_ = flutter_window;
// Create mpv instance. // Create mpv instance.
mpv_ = mpv_create(); mpv_ = mpv_create();
if (!mpv_) { if (!mpv_) {
return false; return false;
} }
// Create a child window for mpv to render into. // Create a child window for mpv to render into, parented to the Flutter
hwnd_ = ::CreateWindowW( // |view|. The video child then sits in the view's own per-window layer
L"STATIC", L"", WS_CHILD | WS_VISIBLE, 0, 0, 100, 100, container, nullptr, GetModuleHandle(nullptr), nullptr); // 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_) { if (!hwnd_) {
mpv_destroy(mpv_); mpv_destroy(mpv_);
mpv_ = nullptr; mpv_ = nullptr;
return false; return false;
} }
g_forward_target_view = view;
// Set the wid option to embed mpv in our window. // Set the wid option to embed mpv in our window.
int64_t wid = reinterpret_cast<int64_t>(hwnd_); int64_t wid = reinterpret_cast<int64_t>(hwnd_);
@@ -114,6 +171,10 @@ void MpvPlayer::Dispose() {
hwnd_ = nullptr; 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(); 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) { void MpvPlayer::SetRect(RECT rect, double device_pixel_ratio) {
rect_ = rect; if (!hwnd_) {
device_pixel_ratio_ = device_pixel_ratio; return;
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);
} }
// 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) { void MpvPlayer::SetVisible(bool visible) {
@@ -439,6 +480,10 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
break; break;
} }
case MPV_EVENT_PLAYBACK_RESTART: { 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"); SendEvent("playback-restart");
break; break;
} }
+5 -6
View File
@@ -25,8 +25,11 @@ class MpvPlayer {
MpvPlayer(); MpvPlayer();
~MpvPlayer(); ~MpvPlayer();
// Initializes mpv and creates the video window. // Initializes mpv and creates the video window as a child of the Flutter
bool Initialize(HWND container, HWND flutter_window); // |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. // Disposes mpv and the video window.
void Dispose(); void Dispose();
@@ -87,10 +90,6 @@ class MpvPlayer {
mpv_handle* mpv_ = nullptr; mpv_handle* mpv_ = nullptr;
HWND hwnd_ = 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::thread event_thread_;
std::atomic<bool> running_{false}; std::atomic<bool> running_{false};
+15 -38
View File
@@ -1,8 +1,5 @@
#include "mpv_plugin.h" #include "mpv_plugin.h"
#include "mpv_container.h"
#include "mpv_core.h"
static flutter::EncodableMap DisplayModeToMap(const mpv::DisplayMode& mode) { static flutter::EncodableMap DisplayModeToMap(const mpv::DisplayMode& mode) {
flutter::EncodableMap m; flutter::EncodableMap m;
m[flutter::EncodableValue("width")] = flutter::EncodableValue(static_cast<int32_t>(mode.width)); m[flutter::EncodableValue("width")] = flutter::EncodableValue(static_cast<int32_t>(mode.width));
@@ -106,54 +103,43 @@ void MpvPlayerPlugin::HandleMethodCall(
const auto& method = method_call.method_name(); const auto& method = method_call.method_name();
if (method == "initialize") { if (method == "initialize") {
// Set up MpvCore for z-order management.
if (proc_id_) { if (proc_id_) {
registrar_->UnregisterTopLevelWindowProcDelegate(proc_id_.value()); registrar_->UnregisterTopLevelWindowProcDelegate(proc_id_.value());
proc_id_ = std::nullopt; proc_id_ = std::nullopt;
} }
HWND flutter_window = GetWindow(); flutter_window_ = GetWindow();
flutter_window_ = flutter_window;
MpvCore::SetInstance(std::make_unique<MpvCore>(flutter_window));
// 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_ = proc_id_ =
registrar_->RegisterTopLevelWindowProcDelegate([this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { registrar_->RegisterTopLevelWindowProcDelegate([this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
if (message == kPlatformTaskMessage) { if (message == kPlatformTaskMessage) {
DrainPlatformTasks(); DrainPlatformTasks();
return std::optional<HRESULT>(0); return std::optional<HRESULT>(0);
} }
auto* core = MpvCore::GetInstance();
if (core) {
return core->WindowProc(hwnd, message, wparam, lparam);
}
return std::optional<HRESULT>(std::nullopt); return std::optional<HRESULT>(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<MpvPlayer>(); player_ = std::make_unique<MpvPlayer>();
HWND container = MpvContainer::GetInstance()->handle(); bool success = player_->Initialize(view);
if (!container) {
result->Error("INIT_FAILED", "Failed to create container window");
return;
}
bool success = player_->Initialize(container, flutter_window);
if (success) { if (success) {
// Set up event callback. // Set up event callback.
player_->SetEventCallback([this](const flutter::EncodableValue& event) { SendEvent(event); }); 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. // Start hidden.
MpvCore::GetInstance()->SetVisible(false); player_->SetVisible(false);
result->Success(flutter::EncodableValue(true)); result->Success(flutter::EncodableValue(true));
} else { } else {
player_.reset(); // Clear the player so we don't have a half-initialized state 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") { } else if (method == "dispose") {
if (player_) { if (player_) {
auto hwnd = player_->GetHwnd();
player_->Dispose(); player_->Dispose();
player_.reset(); player_.reset();
if (MpvCore::GetInstance() && hwnd) {
MpvCore::GetInstance()->DisposeMpvView(hwnd);
}
} }
result->Success(); result->Success();
} else if (method == "command") { } else if (method == "command") {
@@ -353,9 +334,6 @@ void MpvPlayerPlugin::HandleMethodCall(
if (player_) { if (player_) {
player_->SetVisible(visible); player_->SetVisible(visible);
} }
if (MpvCore::GetInstance()) {
MpvCore::GetInstance()->SetVisible(visible);
}
result->Success(); result->Success();
} else if (method == "setVideoRect") { } else if (method == "setVideoRect") {
@@ -394,8 +372,7 @@ void MpvPlayerPlugin::HandleMethodCall(
rect.bottom = get_int("bottom"); rect.bottom = get_int("bottom");
double dpr = get_double("devicePixelRatio"); double dpr = get_double("devicePixelRatio");
if (player_ && MpvCore::GetInstance()) { if (player_) {
MpvCore::GetInstance()->ResizeMpvView(player_->GetHwnd(), rect);
player_->SetRect(rect, dpr); player_->SetRect(rect, dpr);
} }
-1
View File
@@ -15,7 +15,6 @@
#include <queue> #include <queue>
#include "display_mode_manager.h" #include "display_mode_manager.h"
#include "mpv_core.h"
#include "mpv_player.h" #include "mpv_player.h"
// C-style registration function for the plugin. // C-style registration function for the plugin.
-111
View File
@@ -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>(accent_state), 2, static_cast<DWORD>(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
-18
View File
@@ -1,18 +0,0 @@
#ifndef MPV_UTILS_H_
#define MPV_UTILS_H_
#include <Windows.h>
#include <dwmapi.h>
#include <cstdint>
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_
+74
View File
@@ -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)."
+61
View File
@@ -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 <windows-x64-flutter.zip> (from flutter-plezy out\<v>\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'."