refactor(windows): remove legacy mpv compositing, require DComp engine
This commit is contained in:
@@ -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,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) {
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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_
|
|
||||||
@@ -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
|
|
||||||
@@ -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_
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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_
|
|
||||||
@@ -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)."
|
||||||
@@ -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'."
|
||||||
Reference in New Issue
Block a user