From 920658485fb6f98a9599912e558e11a6550cecdc Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:30:28 +0100 Subject: [PATCH] windows --- lib/mpv/src/player/mpv_player.dart | 8 +- lib/mpv/src/player/mpv_player_native.dart | 3 + lib/mpv/src/player/mpv_player_windows.dart | 34 ++ lib/mpv/src/video/mpv_video.dart | 55 +++ windows/CMakeLists.txt | 17 + windows/runner/CMakeLists.txt | 12 +- windows/runner/flutter_window.cpp | 8 + windows/runner/mpv/mpv_container.cpp | 158 +++++++++ windows/runner/mpv/mpv_container.h | 44 +++ windows/runner/mpv/mpv_core.cpp | 259 ++++++++++++++ windows/runner/mpv/mpv_core.h | 71 ++++ windows/runner/mpv/mpv_player.cpp | 383 +++++++++++++++++++++ windows/runner/mpv/mpv_player.h | 90 +++++ windows/runner/mpv/mpv_plugin.cpp | 380 ++++++++++++++++++++ windows/runner/mpv/mpv_plugin.h | 53 +++ windows/runner/mpv/utils.cpp | 113 ++++++ windows/runner/mpv/utils.h | 19 + 17 files changed, 1704 insertions(+), 3 deletions(-) create mode 100644 lib/mpv/src/player/mpv_player_windows.dart create mode 100644 windows/runner/mpv/mpv_container.cpp create mode 100644 windows/runner/mpv/mpv_container.h create mode 100644 windows/runner/mpv/mpv_core.cpp create mode 100644 windows/runner/mpv/mpv_core.h create mode 100644 windows/runner/mpv/mpv_player.cpp create mode 100644 windows/runner/mpv/mpv_player.h create mode 100644 windows/runner/mpv/mpv_plugin.cpp create mode 100644 windows/runner/mpv/mpv_plugin.h create mode 100644 windows/runner/mpv/utils.cpp create mode 100644 windows/runner/mpv/utils.h diff --git a/lib/mpv/src/player/mpv_player.dart b/lib/mpv/src/player/mpv_player.dart index 60b30385..61ca8ddf 100644 --- a/lib/mpv/src/player/mpv_player.dart +++ b/lib/mpv/src/player/mpv_player.dart @@ -10,6 +10,7 @@ import 'mpv_player_macos.dart'; import 'mpv_player_state.dart'; import 'mpv_player_streams.dart'; import 'mpv_player_stub.dart'; +import 'mpv_player_windows.dart'; /// Abstract interface for the MPV player. /// @@ -190,6 +191,8 @@ abstract class MpvPlayer { /// Returns a platform-specific implementation: /// - macOS: [MpvPlayerMacOS] using MPVKit with Metal rendering /// - iOS: [MpvPlayerIOS] using MPVKit with Metal rendering + /// - Android: [MpvPlayerAndroid] using libmpv + /// - Windows: [MpvPlayerWindows] using libmpv with native window embedding /// - Other platforms: [MpvPlayerStub] (placeholder) factory MpvPlayer() { if (Platform.isMacOS) { @@ -201,7 +204,10 @@ abstract class MpvPlayer { if (Platform.isAndroid) { return MpvPlayerAndroid(); } - // Future: Add Windows, Linux implementations + if (Platform.isWindows) { + return MpvPlayerWindows(); + } + // Future: Add Linux implementation return MpvPlayerStub(); } } diff --git a/lib/mpv/src/player/mpv_player_native.dart b/lib/mpv/src/player/mpv_player_native.dart index 2889b389..6bcbc7d9 100644 --- a/lib/mpv/src/player/mpv_player_native.dart +++ b/lib/mpv/src/player/mpv_player_native.dart @@ -422,6 +422,7 @@ class MpvPlayerNative implements MpvPlayer { @override Future setProperty(String name, String value) async { _checkDisposed(); + await _ensureInitialized(); await _methodChannel.invokeMethod('setProperty', { 'name': name, 'value': value, @@ -431,6 +432,7 @@ class MpvPlayerNative implements MpvPlayer { @override Future getProperty(String name) async { _checkDisposed(); + await _ensureInitialized(); return await _methodChannel.invokeMethod('getProperty', { 'name': name, }); @@ -439,6 +441,7 @@ class MpvPlayerNative implements MpvPlayer { @override Future command(List args) async { _checkDisposed(); + await _ensureInitialized(); await _methodChannel.invokeMethod('command', {'args': args}); } diff --git a/lib/mpv/src/player/mpv_player_windows.dart b/lib/mpv/src/player/mpv_player_windows.dart new file mode 100644 index 00000000..278838f7 --- /dev/null +++ b/lib/mpv/src/player/mpv_player_windows.dart @@ -0,0 +1,34 @@ +import 'package:flutter/services.dart'; + +import 'mpv_player_native.dart'; + +/// Windows implementation of [MpvPlayer]. +/// +/// Uses libmpv via platform channels with native window embedding. +/// The mpv video window is positioned behind the Flutter window, +/// with transparent regions allowing the video to show through. +class MpvPlayerWindows extends MpvPlayerNative { + static const _methodChannel = MethodChannel('com.plezy/mpv_player'); + + @override + int? get textureId => null; // Uses native window embedding, not Flutter texture + + /// Updates the video window position and size. + /// + /// This is called by [MpvVideo] when the widget layout changes. + Future setVideoRect({ + required int left, + required int top, + required int right, + required int bottom, + required double devicePixelRatio, + }) async { + await _methodChannel.invokeMethod('setVideoRect', { + 'left': left, + 'top': top, + 'right': right, + 'bottom': bottom, + 'devicePixelRatio': devicePixelRatio, + }); + } +} diff --git a/lib/mpv/src/video/mpv_video.dart b/lib/mpv/src/video/mpv_video.dart index c97ad346..c6df6f0a 100644 --- a/lib/mpv/src/video/mpv_video.dart +++ b/lib/mpv/src/video/mpv_video.dart @@ -3,6 +3,7 @@ import 'dart:io' show Platform; import 'package:flutter/material.dart'; import '../player/mpv_player.dart'; +import '../player/mpv_player_windows.dart'; /// Video widget for displaying MPV player output. /// @@ -45,6 +46,8 @@ class MpvVideo extends StatefulWidget { } class _MpvVideoState extends State { + Rect? _lastRect; + @override Widget build(BuildContext context) { return Container( @@ -63,6 +66,58 @@ class _MpvVideoState extends State { } Widget _buildVideoSurface() { + if (Platform.isWindows) { + // On Windows, use native window embedding. + // The mpv window is positioned behind Flutter, and we need to + // communicate the video rect to the native side. + return LayoutBuilder( + builder: (context, constraints) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateVideoRect(context, constraints); + }); + return const SizedBox.expand(); + }, + ); + } return const SizedBox.expand(); } + + void _updateVideoRect(BuildContext context, BoxConstraints constraints) { + final renderBox = context.findRenderObject() as RenderBox?; + if (renderBox == null || !renderBox.hasSize) return; + + final position = renderBox.localToGlobal(Offset.zero); + final size = renderBox.size; + final dpr = MediaQuery.of(context).devicePixelRatio; + + final newRect = Rect.fromLTWH( + position.dx, + position.dy, + size.width, + size.height, + ); + + // Only update if the rect has changed significantly + if (_lastRect != null && + (newRect.left - _lastRect!.left).abs() < 1 && + (newRect.top - _lastRect!.top).abs() < 1 && + (newRect.width - _lastRect!.width).abs() < 1 && + (newRect.height - _lastRect!.height).abs() < 1) { + return; + } + + _lastRect = newRect; + + // Update the native mpv window position + if (widget.player is MpvPlayerWindows) { + final windowsPlayer = widget.player as MpvPlayerWindows; + windowsPlayer.setVideoRect( + left: (position.dx * dpr).toInt(), + top: (position.dy * dpr).toInt(), + right: ((position.dx + size.width) * dpr).toInt(), + bottom: ((position.dy + size.height) * dpr).toInt(), + devicePixelRatio: dpr, + ); + } + } } diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 3d42510d..b2d25f79 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -2,6 +2,18 @@ cmake_minimum_required(VERSION 3.14) project(plezy LANGUAGES CXX) +# Download and extract mpv-dev +include(FetchContent) +FetchContent_Declare( + mpv_dev + URL https://github.com/shinchiro/mpv-winbuild-cmake/releases/download/20251201/mpv-dev-x86_64-v3-20251201-git-72dbcf1.7z + DOWNLOAD_EXTRACT_TIMESTAMP TRUE +) +FetchContent_MakeAvailable(mpv_dev) + +set(MPV_INCLUDE_DIR "${mpv_dev_SOURCE_DIR}/include") +set(MPV_LIB_DIR "${mpv_dev_SOURCE_DIR}") + # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "plezy") @@ -106,3 +118,8 @@ install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) + +# Install mpv DLL +install(FILES "${MPV_LIB_DIR}/libmpv-2.dll" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt index 394917c0..782241b8 100644 --- a/windows/runner/CMakeLists.txt +++ b/windows/runner/CMakeLists.txt @@ -11,6 +11,11 @@ add_executable(${BINARY_NAME} WIN32 "main.cpp" "utils.cpp" "win32_window.cpp" + "mpv/utils.cpp" + "mpv/mpv_container.cpp" + "mpv/mpv_core.cpp" + "mpv/mpv_player.cpp" + "mpv/mpv_plugin.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" @@ -32,9 +37,12 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # Add dependency libraries and include directories. Add any application-specific # dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) -target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app flutter_wrapper_plugin) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib" "comctl32.lib") +target_link_libraries(${BINARY_NAME} PRIVATE "${MPV_LIB_DIR}/libmpv.dll.a") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_include_directories(${BINARY_NAME} PRIVATE "${MPV_INCLUDE_DIR}") # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 955ee303..2419628d 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -3,6 +3,7 @@ #include #include "flutter/generated_plugin_registrant.h" +#include "mpv/mpv_plugin.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} @@ -25,6 +26,13 @@ bool FlutterWindow::OnCreate() { return false; } RegisterPlugins(flutter_controller_->engine()); + + // Register mpv player plugin. + OutputDebugStringA("FlutterWindow: About to register MpvPlayerPlugin\n"); + MpvPlayerPluginRegisterWithRegistrar( + flutter_controller_->engine()->GetRegistrarForPlugin("MpvPlayerPlugin")); + OutputDebugStringA("FlutterWindow: MpvPlayerPlugin registered\n"); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); flutter_controller_->engine()->SetNextFrameCallback([&]() { diff --git a/windows/runner/mpv/mpv_container.cpp b/windows/runner/mpv/mpv_container.cpp new file mode 100644 index 00000000..ca7dcd18 --- /dev/null +++ b/windows/runner/mpv/mpv_container.cpp @@ -0,0 +1,158 @@ +#include "mpv_container.h" + +#include +#include +#include + +#include "mpv_core.h" +#include "utils.h" + +static void LogToFile(const char* message) { + std::ofstream log("C:\\Users\\admin\\mpv_debug.log", std::ios::app); + if (log.is_open()) { + log << message << std::endl; + log.close(); + } + OutputDebugStringA(message); + OutputDebugStringA("\n"); +} + +namespace mpv { + +MpvContainer* MpvContainer::GetInstance() { return instance_.get(); } + +HWND MpvContainer::Create() { + LogToFile("MpvContainer::Create called"); + + auto window_class = WNDCLASSEX{}; + ::SecureZeroMemory(&window_class, sizeof(window_class)); + window_class.cbSize = sizeof(window_class); + // Don't use CS_DROPSHADOW, and avoid redraw styles that might cause issues + 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)); + + ATOM atom = ::RegisterClassExW(&window_class); + char msg[256]; + snprintf(msg, sizeof(msg), "MpvContainer::Create - RegisterClassExW returned: %d", atom); + LogToFile(msg); + + // 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); + + if (!handle_) { + DWORD error = GetLastError(); + snprintf(msg, sizeof(msg), "MpvContainer::Create - CreateWindow failed with error %lu", error); + LogToFile(msg); + } else { + snprintf(msg, sizeof(msg), "MpvContainer::Create - handle_: %p", handle_); + LogToFile(msg); + } + + // 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) { + LogToFile("MpvContainer::Get called"); + char msg[256]; + snprintf(msg, sizeof(msg), "MpvContainer::Get - flutter_window: %p, handle_: %p", flutter_window, handle_); + LogToFile(msg); + + if (!handle_) { + LogToFile("MpvContainer::Get - handle_ is null, calling Create()"); + Create(); + } + + RECT window_rect; + ::GetWindowRect(flutter_window, &window_rect); + snprintf(msg, sizeof(msg), "MpvContainer::Get - window_rect: %ld,%ld,%ld,%ld", + window_rect.left, window_rect.top, window_rect.right, window_rect.bottom); + LogToFile(msg); + + ::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)); + + // Remove taskbar entry using ITaskbarList3. + ITaskbarList3* taskbar = nullptr; + HRESULT hr = ::CoCreateInstance(CLSID_TaskbarList, 0, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&taskbar)); + if (SUCCEEDED(hr) && taskbar) { + taskbar->DeleteTab(handle_); + taskbar->Release(); + } + + ::ShowWindow(handle_, SW_SHOWNOACTIVATE); + ::SetFocus(flutter_window); + + snprintf(msg, sizeof(msg), "MpvContainer::Get - returning handle_: %p", handle_); + LogToFile(msg); + 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: { + // Redirect focus to Flutter window. + auto* core = MpvCore::GetInstance(); + if (core) { + core->SetHitTestBehavior(0); + } + auto user_data = ::GetWindowLongPtr(window, GWLP_USERDATA); + if (user_data) { + ::SetForegroundWindow(reinterpret_cast(user_data)); + } + break; + } + case WM_ERASEBKGND: { + // Prevent erasing to avoid flicker. + return 1; + } + case WM_SIZE: + case WM_MOVE: + case WM_MOVING: + case WM_ACTIVATE: + case WM_WINDOWPOSCHANGED: { + auto* core = MpvCore::GetInstance(); + if (core) { + core->SetHitTestBehavior(0); + } + auto user_data = ::GetWindowLongPtr(window, GWLP_USERDATA); + if (user_data) { + ::SetForegroundWindow(reinterpret_cast(user_data)); + } + break; + } + 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 new file mode 100644 index 00000000..1def5696 --- /dev/null +++ b/windows/runner/mpv/mpv_container.h @@ -0,0 +1,44 @@ +#ifndef MPV_CONTAINER_H_ +#define MPV_CONTAINER_H_ + +#include + +#include + +namespace mpv { + +// Forward declaration +class MpvCore; + +// 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; + + // 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 new file mode 100644 index 00000000..3f57a065 --- /dev/null +++ b/windows/runner/mpv/mpv_core.cpp @@ -0,0 +1,259 @@ +#include "mpv_core.h" + +#include + +#include "mpv_container.h" +#include "utils.h" + +static void LogToFile(const char* message) { + std::ofstream log("C:\\Users\\admin\\mpv_debug.log", std::ios::app); + if (log.is_open()) { + log << message << std::endl; + log.close(); + } + OutputDebugStringA(message); + OutputDebugStringA("\n"); +} + +namespace mpv { + +MpvCore* MpvCore::GetInstance() { return instance_.get(); } + +void MpvCore::SetInstance(std::unique_ptr instance) { + instance_ = std::move(instance); +} + +std::optional MpvCore::GetProcId() { return proc_id_; } + +void MpvCore::SetProcId(std::optional proc_id) { proc_id_ = proc_id; } + +MpvCore::MpvCore(HWND flutter_window, HWND flutter_child_window) + : flutter_window_(flutter_window), + flutter_child_window_(flutter_child_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() { + LogToFile("MpvCore::EnsureInitialized called"); + char msg[256]; + snprintf(msg, sizeof(msg), "MpvCore::EnsureInitialized - flutter_window_: %p", flutter_window_); + LogToFile(msg); + + // Enable per-pixel transparency on Flutter window. + LogToFile("MpvCore::EnsureInitialized - calling SetWindowComposition"); + SetWindowComposition(flutter_window_, 6, 0); + + LogToFile("MpvCore::EnsureInitialized - getting container"); + container_ = MpvContainer::GetInstance()->Get(flutter_window_); + + snprintf(msg, sizeof(msg), "MpvCore::EnsureInitialized - container: %p", container_); + LogToFile(msg); +} + +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 | + WS_EX_APPWINDOW); + ::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::SetHitTestBehavior(int32_t hittest_behavior) { + LONG ex_style = ::GetWindowLong(flutter_window_, GWL_EXSTYLE); + if (hittest_behavior) { + ex_style |= (WS_EX_TRANSPARENT | WS_EX_LAYERED); + } else { + ex_style &= ~(WS_EX_TRANSPARENT | WS_EX_LAYERED); + } + ::SetWindowLong(flutter_window_, GWL_EXSTYLE, ex_style); +} + +void MpvCore::SetVisible(bool visible) { + char msg[256]; + snprintf(msg, sizeof(msg), "MpvCore::SetVisible - visible: %d, container_: %p", visible, container_); + LogToFile(msg); + + visible_ = visible; + if (container_) { + if (visible) { + SetWindowComposition(flutter_window_, 6, 0); + ::ShowWindow(container_, SW_SHOWNOACTIVATE); + LogToFile("MpvCore::SetVisible - showed container, set composition to 6"); + } else { + SetWindowComposition(flutter_window_, 0, 0); + ::ShowWindow(container_, SW_HIDE); + LogToFile("MpvCore::SetVisible - hid container, set composition to 0"); + } + } +} + +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: { + char msg[256]; + snprintf(msg, sizeof(msg), "WM_SIZE - wparam: %llu, last: %llu, visible_: %d, was_hidden: %d", + (unsigned long long)wparam, (unsigned long long)last_wm_size_wparam_, + visible_, was_window_hidden_due_to_minimize_); + LogToFile(msg); + + // Handle Windows's minimize & maximize animations properly. + // During these transitions, we hide the container and make Flutter opaque, + // then restore after the animation completes. + 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; + SetWindowComposition(flutter_window_, 0, 0); + ::ShowWindow(container_, SW_HIDE); + LogToFile("WM_SIZE - hiding container, starting delay thread"); + last_thread_time_ = + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + std::thread( + [this](uint64_t time) { + std::this_thread::sleep_for( + std::chrono::milliseconds(kPositionAndShowDelay)); + + // Check if this thread is still the latest (another WM_SIZE may have come in) + if (time != last_thread_time_) { + LogToFile("WM_SIZE thread - superseded by newer thread, skipping"); + return; + } + + char msg2[256]; + snprintf(msg2, sizeof(msg2), "WM_SIZE thread - after delay, visible_: %d", + visible_); + LogToFile(msg2); + + // Update container position to match current Flutter window bounds + RECT window_rect; + ::GetWindowRect(flutter_window_, &window_rect); + snprintf(msg2, sizeof(msg2), "WM_SIZE thread - flutter rect: %ld,%ld,%ld,%ld", + window_rect.left, window_rect.top, window_rect.right, window_rect.bottom); + LogToFile(msg2); + + ::SetWindowPos(container_, flutter_window_, window_rect.left, + window_rect.top, window_rect.right - window_rect.left, + window_rect.bottom - window_rect.top, SWP_NOACTIVATE); + LogToFile("WM_SIZE thread - updated container position"); + + // Always restore transparency if video is visible + if (visible_) { + SetWindowComposition(flutter_window_, 6, 0); + LogToFile("WM_SIZE thread - restored composition to 6"); + ::ShowWindow(container_, SW_SHOWNOACTIVATE); + LogToFile("WM_SIZE thread - showed container"); + } + }, + last_thread_time_) + .detach(); + } + last_wm_size_wparam_ = wparam; + break; + } + case WM_MOVE: + case WM_MOVING: + 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) { + SetWindowComposition(flutter_window_, 0, 0); + ::ShowWindow(container_, SW_HIDE); + 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; +} + +void MpvCore::RedrawMpvViews() { + ::RedrawWindow(container_, 0, 0, RDW_INVALIDATE | RDW_ALLCHILDREN); +} + +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; +} + +std::unique_ptr MpvCore::instance_ = nullptr; +std::optional MpvCore::proc_id_ = std::nullopt; + +} // namespace mpv diff --git a/windows/runner/mpv/mpv_core.h b/windows/runner/mpv/mpv_core.h new file mode 100644 index 00000000..2adca0be --- /dev/null +++ b/windows/runner/mpv/mpv_core.h @@ -0,0 +1,71 @@ +#ifndef MPV_CORE_H_ +#define MPV_CORE_H_ + +#include + +#include +#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 MpvCore* GetInstance(); + static void SetInstance(std::unique_ptr instance); + static std::optional GetProcId(); + static void SetProcId(std::optional proc_id); + + MpvCore(HWND flutter_window, HWND flutter_child_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); + + // Sets hit test behavior for mouse passthrough. + void SetHitTestBehavior(int32_t hittest_behavior); + + // 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: + void RedrawMpvViews(); + RECT GetGlobalRect(int32_t left, int32_t top, int32_t right, int32_t bottom); + + HWND flutter_window_ = nullptr; + HWND flutter_child_window_ = nullptr; + HWND container_ = nullptr; + double device_pixel_ratio_ = 1.0; + std::map mpv_views_; + uint64_t last_thread_time_ = 0; + WPARAM last_wm_size_wparam_ = SIZE_RESTORED; + bool was_window_hidden_due_to_minimize_ = false; + bool visible_ = true; + + static std::unique_ptr instance_; + static std::optional proc_id_; +}; + +} // namespace mpv + +#endif // MPV_CORE_H_ diff --git a/windows/runner/mpv/mpv_player.cpp b/windows/runner/mpv/mpv_player.cpp new file mode 100644 index 00000000..7da9abeb --- /dev/null +++ b/windows/runner/mpv/mpv_player.cpp @@ -0,0 +1,383 @@ +#include "mpv_player.h" + +#include +#include + +static void LogToFile(const char* message) { + std::ofstream log("C:\\Users\\admin\\mpv_debug.log", std::ios::app); + if (log.is_open()) { + log << message << std::endl; + log.close(); + } + OutputDebugStringA(message); + OutputDebugStringA("\n"); +} + +namespace mpv { + +MpvPlayer::MpvPlayer() {} + +MpvPlayer::~MpvPlayer() { Dispose(); } + +bool MpvPlayer::Initialize(HWND container, HWND flutter_window) { + LogToFile("MpvPlayer::Initialize called"); + + if (mpv_) { + LogToFile("MpvPlayer::Initialize - already initialized"); + return true; // Already initialized. + } + + container_ = container; + flutter_window_ = flutter_window; + char msg[256]; + snprintf(msg, sizeof(msg), "MpvPlayer::Initialize - container: %p", container); + LogToFile(msg); + + // Create mpv instance. + LogToFile("MpvPlayer::Initialize - calling mpv_create()"); + mpv_ = mpv_create(); + if (!mpv_) { + LogToFile("MPV: mpv_create() failed"); + return false; + } + LogToFile("MpvPlayer::Initialize - mpv_create() succeeded"); + + // Create a child window for mpv to render into. + LogToFile("MpvPlayer::Initialize - creating child window"); + hwnd_ = ::CreateWindowW(L"STATIC", L"", WS_CHILD | WS_VISIBLE, 0, 0, 100, 100, + container, nullptr, GetModuleHandle(nullptr), + nullptr); + if (!hwnd_) { + DWORD error = GetLastError(); + snprintf(msg, sizeof(msg), "MPV: CreateWindowW failed with error %lu", error); + LogToFile(msg); + mpv_destroy(mpv_); + mpv_ = nullptr; + return false; + } + snprintf(msg, sizeof(msg), "MpvPlayer::Initialize - child window created: %p", hwnd_); + LogToFile(msg); + + // Set the wid option to embed mpv in our window. + int64_t wid = reinterpret_cast(hwnd_); + int err = mpv_set_option(mpv_, "wid", MPV_FORMAT_INT64, &wid); + if (err < 0) { + snprintf(msg, sizeof(msg), "MPV: Failed to set wid option: %d %s", err, mpv_error_string(err)); + LogToFile(msg); + } else { + LogToFile("MpvPlayer::Initialize - wid option set successfully"); + } + + // Configure mpv for embedded playback. + LogToFile("MpvPlayer::Initialize - setting mpv options"); + mpv_set_option_string(mpv_, "hwdec", "auto"); + mpv_set_option_string(mpv_, "keep-open", "yes"); + mpv_set_option_string(mpv_, "idle", "yes"); + mpv_set_option_string(mpv_, "input-default-bindings", "no"); + mpv_set_option_string(mpv_, "input-vo-keyboard", "no"); + mpv_set_option_string(mpv_, "osc", "no"); + + // Enable logging + mpv_request_log_messages(mpv_, "v"); + + // Initialize mpv. + LogToFile("MpvPlayer::Initialize - calling mpv_initialize()"); + err = mpv_initialize(mpv_); + if (err < 0) { + snprintf(msg, sizeof(msg), "MPV: mpv_initialize() failed with error %d: %s", + err, mpv_error_string(err)); + LogToFile(msg); + ::DestroyWindow(hwnd_); + hwnd_ = nullptr; + mpv_destroy(mpv_); + mpv_ = nullptr; + return false; + } + + LogToFile("MPV: Initialization successful"); + + // Start event loop. + StartEventLoop(); + LogToFile("MpvPlayer::Initialize - event loop started"); + + return true; +} + +void MpvPlayer::Dispose() { + StopEventLoop(); + + if (mpv_) { + mpv_terminate_destroy(mpv_); + mpv_ = nullptr; + } + + if (hwnd_) { + ::DestroyWindow(hwnd_); + hwnd_ = nullptr; + } + + observed_properties_.clear(); +} + +void MpvPlayer::Command(const std::vector& args) { + if (!mpv_) return; + + std::vector c_args; + c_args.reserve(args.size() + 1); + for (const auto& arg : args) { + c_args.push_back(arg.c_str()); + } + c_args.push_back(nullptr); + + mpv_command(mpv_, c_args.data()); +} + +void MpvPlayer::SetProperty(const std::string& name, const std::string& value) { + if (!mpv_) return; + mpv_set_property_string(mpv_, name.c_str(), value.c_str()); +} + +std::string MpvPlayer::GetProperty(const std::string& name) { + if (!mpv_) return ""; + + char* value = mpv_get_property_string(mpv_, name.c_str()); + if (!value) return ""; + + std::string result(value); + mpv_free(value); + return result; +} + +void MpvPlayer::ObserveProperty(const std::string& name, + const std::string& format) { + if (!mpv_) return; + + // Check if already observing. + if (observed_properties_.find(name) != observed_properties_.end()) { + return; + } + + mpv_format mpv_fmt = MPV_FORMAT_NONE; + if (format == "string") { + mpv_fmt = MPV_FORMAT_STRING; + } else if (format == "flag" || format == "bool") { + mpv_fmt = MPV_FORMAT_FLAG; + } else if (format == "int64") { + mpv_fmt = MPV_FORMAT_INT64; + } else if (format == "double") { + mpv_fmt = MPV_FORMAT_DOUBLE; + } else if (format == "node") { + mpv_fmt = MPV_FORMAT_NODE; + } + + uint64_t userdata = next_reply_userdata_++; + observed_properties_[name] = userdata; + mpv_observe_property(mpv_, userdata, name.c_str(), mpv_fmt); +} + +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); + } +} + +void MpvPlayer::SetVisible(bool visible) { + if (hwnd_) { + ::ShowWindow(hwnd_, visible ? SW_SHOW : SW_HIDE); + } +} + +void MpvPlayer::SetEventCallback(EventCallback callback) { + std::lock_guard lock(callback_mutex_); + event_callback_ = std::move(callback); +} + +void MpvPlayer::StartEventLoop() { + running_ = true; + event_thread_ = std::thread(&MpvPlayer::EventLoop, this); +} + +void MpvPlayer::StopEventLoop() { + running_ = false; + if (event_thread_.joinable()) { + // Wake up the event loop. + if (mpv_) { + mpv_wakeup(mpv_); + } + event_thread_.join(); + } +} + +void MpvPlayer::EventLoop() { + while (running_) { + mpv_event* event = mpv_wait_event(mpv_, 0.1); + if (event->event_id == MPV_EVENT_NONE) { + continue; + } + if (event->event_id == MPV_EVENT_SHUTDOWN) { + break; + } + HandleMpvEvent(event); + } +} + +void MpvPlayer::HandleMpvEvent(mpv_event* event) { + switch (event->event_id) { + case MPV_EVENT_LOG_MESSAGE: { + auto* msg = static_cast(event->data); + char log_msg[512]; + snprintf(log_msg, sizeof(log_msg), "MPV [%s] %s: %s", + msg->level, msg->prefix, msg->text); + OutputDebugStringA(log_msg); + + flutter::EncodableMap data; + data[flutter::EncodableValue("prefix")] = + flutter::EncodableValue(msg->prefix ? msg->prefix : ""); + data[flutter::EncodableValue("level")] = + flutter::EncodableValue(msg->level ? msg->level : ""); + data[flutter::EncodableValue("text")] = + flutter::EncodableValue(msg->text ? msg->text : ""); + SendEvent("log-message", data); + break; + } + case MPV_EVENT_PROPERTY_CHANGE: { + auto* prop = static_cast(event->data); + mpv_node node; + node.format = prop->format; + + switch (prop->format) { + case MPV_FORMAT_STRING: + node.u.string = prop->data ? *static_cast(prop->data) : nullptr; + break; + case MPV_FORMAT_FLAG: + node.u.flag = prop->data ? *static_cast(prop->data) : 0; + break; + case MPV_FORMAT_INT64: + node.u.int64 = prop->data ? *static_cast(prop->data) : 0; + break; + case MPV_FORMAT_DOUBLE: + node.u.double_ = prop->data ? *static_cast(prop->data) : 0.0; + break; + case MPV_FORMAT_NODE: + if (prop->data) { + node = *static_cast(prop->data); + } + break; + default: + node.format = MPV_FORMAT_NONE; + break; + } + + SendPropertyChange(prop->name, &node); + break; + } + case MPV_EVENT_END_FILE: { + auto* end = static_cast(event->data); + flutter::EncodableMap data; + data[flutter::EncodableValue("reason")] = + flutter::EncodableValue(static_cast(end->reason)); + if (end->reason == MPV_END_FILE_REASON_ERROR) { + data[flutter::EncodableValue("error")] = + flutter::EncodableValue(static_cast(end->error)); + } + SendEvent("end-file", data); + break; + } + case MPV_EVENT_FILE_LOADED: { + SendEvent("file-loaded"); + break; + } + case MPV_EVENT_PLAYBACK_RESTART: { + SendEvent("playback-restart"); + break; + } + case MPV_EVENT_SEEK: { + SendEvent("seek"); + break; + } + default: + break; + } +} + +void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) { + flutter::EncodableMap event; + event[flutter::EncodableValue("type")] = + flutter::EncodableValue("property"); + event[flutter::EncodableValue("name")] = + flutter::EncodableValue(name ? name : ""); + + flutter::EncodableValue value; + if (data) { + switch (data->format) { + case MPV_FORMAT_STRING: + value = flutter::EncodableValue( + data->u.string ? std::string(data->u.string) : std::string()); + break; + case MPV_FORMAT_FLAG: + value = flutter::EncodableValue(data->u.flag != 0); + break; + case MPV_FORMAT_INT64: + value = flutter::EncodableValue(data->u.int64); + break; + case MPV_FORMAT_DOUBLE: + value = flutter::EncodableValue(data->u.double_); + break; + default: + value = flutter::EncodableValue(); + break; + } + } + event[flutter::EncodableValue("value")] = value; + + std::lock_guard lock(callback_mutex_); + if (event_callback_) { + event_callback_(event); + } +} + +void MpvPlayer::SendEvent(const std::string& name, + const flutter::EncodableMap& data) { + flutter::EncodableMap event; + event[flutter::EncodableValue("type")] = flutter::EncodableValue("event"); + event[flutter::EncodableValue("name")] = flutter::EncodableValue(name); + if (!data.empty()) { + event[flutter::EncodableValue("data")] = flutter::EncodableValue(data); + } + + std::lock_guard lock(callback_mutex_); + if (event_callback_) { + event_callback_(event); + } +} + +} // namespace mpv diff --git a/windows/runner/mpv/mpv_player.h b/windows/runner/mpv/mpv_player.h new file mode 100644 index 00000000..eabe14dc --- /dev/null +++ b/windows/runner/mpv/mpv_player.h @@ -0,0 +1,90 @@ +#ifndef MPV_PLAYER_H_ +#define MPV_PLAYER_H_ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace mpv { + +// Wrapper for libmpv that handles initialization, commands, properties, +// and event dispatching. +class MpvPlayer { + public: + using EventCallback = + std::function; + + MpvPlayer(); + ~MpvPlayer(); + + // Initializes mpv and creates the video window. + bool Initialize(HWND container, HWND flutter_window); + + // Disposes mpv and the video window. + void Dispose(); + + // Returns true if mpv is initialized. + bool IsInitialized() const { return mpv_ != nullptr; } + + // Executes an mpv command. + void Command(const std::vector& args); + + // Sets an mpv property. + void SetProperty(const std::string& name, const std::string& value); + + // Gets an mpv property. + std::string GetProperty(const std::string& name); + + // Observes an mpv property for changes. + void ObserveProperty(const std::string& name, const std::string& format); + + // Returns the mpv video window handle. + HWND GetHwnd() const { return hwnd_; } + + // Updates the video window position. + void SetRect(RECT rect, double device_pixel_ratio); + + // Shows or hides the video window. + void SetVisible(bool visible); + + // Sets the event callback for property changes and events. + void SetEventCallback(EventCallback callback); + + private: + void StartEventLoop(); + void StopEventLoop(); + void EventLoop(); + void HandleMpvEvent(mpv_event* event); + void SendPropertyChange(const char* name, mpv_node* data); + void SendEvent(const std::string& name, + const flutter::EncodableMap& data = {}); + + 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}; + EventCallback event_callback_; + std::mutex callback_mutex_; + + uint64_t next_reply_userdata_ = 1; + std::map observed_properties_; +}; + +} // namespace mpv + +#endif // MPV_PLAYER_H_ diff --git a/windows/runner/mpv/mpv_plugin.cpp b/windows/runner/mpv/mpv_plugin.cpp new file mode 100644 index 00000000..6fbe9102 --- /dev/null +++ b/windows/runner/mpv/mpv_plugin.cpp @@ -0,0 +1,380 @@ +#include "mpv_plugin.h" + +#include + +#include "mpv_container.h" +#include "mpv_core.h" + +static void LogToFile(const char* message) { + std::ofstream log("C:\\Users\\admin\\mpv_debug.log", std::ios::app); + if (log.is_open()) { + log << message << std::endl; + log.close(); + } + OutputDebugStringA(message); + OutputDebugStringA("\n"); +} + +void MpvPlayerPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + LogToFile("MpvPlayerPlugin: RegisterWithRegistrar called"); + mpv::MpvPlayerPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); + LogToFile("MpvPlayerPlugin: RegisterWithRegistrar completed"); +} + +namespace mpv { + +void MpvPlayerPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows* registrar) { + auto plugin = std::make_unique(registrar); + registrar->AddPlugin(std::move(plugin)); +} + +MpvPlayerPlugin::MpvPlayerPlugin(flutter::PluginRegistrarWindows* registrar) + : registrar_(registrar) { + LogToFile("MpvPlayerPlugin: Constructor called"); + + // Create method channel. + method_channel_ = + std::make_unique>( + registrar->messenger(), "com.plezy/mpv_player", + &flutter::StandardMethodCodec::GetInstance()); + + LogToFile("MpvPlayerPlugin: Method channel created"); + + method_channel_->SetMethodCallHandler( + [this](const auto& call, auto result) { + char msg[256]; + snprintf(msg, sizeof(msg), "MpvPlayerPlugin: Method call received: %s", + call.method_name().c_str()); + LogToFile(msg); + HandleMethodCall(call, std::move(result)); + }); + + // Create event channel. + event_channel_ = + std::make_unique>( + registrar->messenger(), "com.plezy/mpv_player/events", + &flutter::StandardMethodCodec::GetInstance()); + + auto handler = std::make_unique< + flutter::StreamHandlerFunctions>( + [this](const flutter::EncodableValue* arguments, + std::unique_ptr>&& + events) -> std::unique_ptr> { + event_sink_ = std::move(events); + return nullptr; + }, + [this](const flutter::EncodableValue* arguments) + -> std::unique_ptr< + flutter::StreamHandlerError> { + event_sink_ = nullptr; + return nullptr; + }); + + event_channel_->SetStreamHandler(std::move(handler)); +} + +MpvPlayerPlugin::~MpvPlayerPlugin() { + // Unregister window proc delegate. + if (proc_id_) { + registrar_->UnregisterTopLevelWindowProcDelegate(proc_id_.value()); + proc_id_ = std::nullopt; + } +} + +HWND MpvPlayerPlugin::GetChildWindow() { + return registrar_->GetView()->GetNativeWindow(); +} + +HWND MpvPlayerPlugin::GetWindow() { + return ::GetAncestor(GetChildWindow(), GA_ROOT); +} + +void MpvPlayerPlugin::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + const auto& method = method_call.method_name(); + + if (method == "initialize") { + LogToFile("MPV Plugin: initialize called"); + + // Set up MpvCore for z-order management. + if (proc_id_) { + registrar_->UnregisterTopLevelWindowProcDelegate(proc_id_.value()); + proc_id_ = std::nullopt; + } + + HWND flutter_window = GetWindow(); + HWND child_window = GetChildWindow(); + + char msg[256]; + snprintf(msg, sizeof(msg), "MPV Plugin: Flutter window: %p, Child window: %p", + flutter_window, child_window); + LogToFile(msg); + + MpvCore::SetInstance( + std::make_unique(flutter_window, child_window)); + + proc_id_ = registrar_->RegisterTopLevelWindowProcDelegate( + [](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + auto* core = MpvCore::GetInstance(); + if (core) { + return core->WindowProc(hwnd, message, wparam, lparam); + } + return std::optional(std::nullopt); + }); + + LogToFile("MPV Plugin: Calling EnsureInitialized"); + MpvCore::GetInstance()->EnsureInitialized(); + + // Create player - use the container from MpvCore which was set up by EnsureInitialized + player_ = std::make_unique(); + HWND container = MpvContainer::GetInstance()->handle(); + + snprintf(msg, sizeof(msg), "MPV Plugin: Container handle: %p", container); + LogToFile(msg); + + if (!container) { + LogToFile("MPV Plugin: ERROR - container is null"); + result->Error("INIT_FAILED", "Failed to create container window"); + return; + } + + LogToFile("MPV Plugin: Initializing player"); + bool success = player_->Initialize(container, flutter_window); + + if (success) { + LogToFile("MPV Plugin: Player initialized successfully"); + // Set up event callback. + player_->SetEventCallback([this](const flutter::EncodableMap& 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); + result->Success(flutter::EncodableValue(true)); + } else { + OutputDebugStringA("MPV Plugin: Player initialization FAILED\n"); + player_.reset(); // Clear the player so we don't have a half-initialized state + result->Error("INIT_FAILED", "Failed to initialize MPV player"); + } + } 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") { + if (!player_ || !player_->IsInitialized()) { + result->Error("NOT_INITIALIZED", "Player not initialized"); + return; + } + + const auto* args = method_call.arguments(); + if (!args || !std::holds_alternative(*args)) { + result->Error("INVALID_ARGS", "Expected map argument"); + return; + } + + const auto& map = std::get(*args); + auto it = map.find(flutter::EncodableValue("args")); + if (it == map.end() || + !std::holds_alternative(it->second)) { + result->Error("INVALID_ARGS", "Missing 'args' list"); + return; + } + + const auto& list = std::get(it->second); + std::vector command_args; + for (const auto& item : list) { + if (std::holds_alternative(item)) { + command_args.push_back(std::get(item)); + } + } + + player_->Command(command_args); + result->Success(); + } else if (method == "setProperty") { + if (!player_ || !player_->IsInitialized()) { + result->Error("NOT_INITIALIZED", "Player not initialized"); + return; + } + + const auto* args = method_call.arguments(); + if (!args || !std::holds_alternative(*args)) { + result->Error("INVALID_ARGS", "Expected map argument"); + return; + } + + const auto& map = std::get(*args); + auto name_it = map.find(flutter::EncodableValue("name")); + auto value_it = map.find(flutter::EncodableValue("value")); + + if (name_it == map.end() || + !std::holds_alternative(name_it->second)) { + result->Error("INVALID_ARGS", "Missing 'name'"); + return; + } + if (value_it == map.end() || + !std::holds_alternative(value_it->second)) { + result->Error("INVALID_ARGS", "Missing 'value'"); + return; + } + + player_->SetProperty(std::get(name_it->second), + std::get(value_it->second)); + result->Success(); + } else if (method == "getProperty") { + if (!player_ || !player_->IsInitialized()) { + result->Error("NOT_INITIALIZED", "Player not initialized"); + return; + } + + const auto* args = method_call.arguments(); + if (!args || !std::holds_alternative(*args)) { + result->Error("INVALID_ARGS", "Expected map argument"); + return; + } + + const auto& map = std::get(*args); + auto name_it = map.find(flutter::EncodableValue("name")); + + if (name_it == map.end() || + !std::holds_alternative(name_it->second)) { + result->Error("INVALID_ARGS", "Missing 'name'"); + return; + } + + std::string value = + player_->GetProperty(std::get(name_it->second)); + if (value.empty()) { + result->Success(); + } else { + result->Success(flutter::EncodableValue(value)); + } + } else if (method == "observeProperty") { + if (!player_ || !player_->IsInitialized()) { + result->Error("NOT_INITIALIZED", "Player not initialized"); + return; + } + + const auto* args = method_call.arguments(); + if (!args || !std::holds_alternative(*args)) { + result->Error("INVALID_ARGS", "Expected map argument"); + return; + } + + const auto& map = std::get(*args); + auto name_it = map.find(flutter::EncodableValue("name")); + auto format_it = map.find(flutter::EncodableValue("format")); + + if (name_it == map.end() || + !std::holds_alternative(name_it->second)) { + result->Error("INVALID_ARGS", "Missing 'name'"); + return; + } + if (format_it == map.end() || + !std::holds_alternative(format_it->second)) { + result->Error("INVALID_ARGS", "Missing 'format'"); + return; + } + + player_->ObserveProperty(std::get(name_it->second), + std::get(format_it->second)); + result->Success(); + } else if (method == "setVisible") { + const auto* args = method_call.arguments(); + if (!args || !std::holds_alternative(*args)) { + result->Error("INVALID_ARGS", "Expected map argument"); + return; + } + + const auto& map = std::get(*args); + auto visible_it = map.find(flutter::EncodableValue("visible")); + + if (visible_it == map.end() || + !std::holds_alternative(visible_it->second)) { + result->Error("INVALID_ARGS", "Missing 'visible'"); + return; + } + + bool visible = std::get(visible_it->second); + + if (player_) { + player_->SetVisible(visible); + } + if (MpvCore::GetInstance()) { + MpvCore::GetInstance()->SetVisible(visible); + } + + result->Success(); + } else if (method == "setVideoRect") { + const auto* args = method_call.arguments(); + if (!args || !std::holds_alternative(*args)) { + result->Error("INVALID_ARGS", "Expected map argument"); + return; + } + + const auto& map = std::get(*args); + + auto get_int = [&map](const char* key) -> int { + auto it = map.find(flutter::EncodableValue(key)); + if (it != map.end()) { + if (std::holds_alternative(it->second)) { + return std::get(it->second); + } else if (std::holds_alternative(it->second)) { + return static_cast(std::get(it->second)); + } + } + return 0; + }; + + auto get_double = [&map](const char* key) -> double { + auto it = map.find(flutter::EncodableValue(key)); + if (it != map.end() && std::holds_alternative(it->second)) { + return std::get(it->second); + } + return 1.0; + }; + + RECT rect; + rect.left = get_int("left"); + rect.top = get_int("top"); + rect.right = get_int("right"); + rect.bottom = get_int("bottom"); + double dpr = get_double("devicePixelRatio"); + + if (player_ && MpvCore::GetInstance()) { + MpvCore::GetInstance()->ResizeMpvView(player_->GetHwnd(), rect); + player_->SetRect(rect, dpr); + } + + result->Success(); + } else if (method == "isInitialized") { + bool initialized = player_ && player_->IsInitialized(); + result->Success(flutter::EncodableValue(initialized)); + } else { + result->NotImplemented(); + } +} + +void MpvPlayerPlugin::SendEvent(const flutter::EncodableMap& event) { + if (event_sink_) { + event_sink_->Success(flutter::EncodableValue(event)); + } +} + +} // namespace mpv diff --git a/windows/runner/mpv/mpv_plugin.h b/windows/runner/mpv/mpv_plugin.h new file mode 100644 index 00000000..45e49655 --- /dev/null +++ b/windows/runner/mpv/mpv_plugin.h @@ -0,0 +1,53 @@ +#ifndef MPV_PLUGIN_H_ +#define MPV_PLUGIN_H_ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "mpv_core.h" +#include "mpv_player.h" + +// C-style registration function for the plugin. +void MpvPlayerPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +namespace mpv { + +class MpvPlayerPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); + + MpvPlayerPlugin(flutter::PluginRegistrarWindows* registrar); + virtual ~MpvPlayerPlugin(); + + private: + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + + void SendEvent(const flutter::EncodableMap& event); + + HWND GetWindow(); + HWND GetChildWindow(); + + flutter::PluginRegistrarWindows* registrar_; + std::unique_ptr> + method_channel_; + std::unique_ptr> + event_channel_; + std::unique_ptr> event_sink_; + + std::unique_ptr player_; + std::optional proc_id_; +}; + +} // namespace mpv + +#endif // MPV_PLUGIN_H_ diff --git a/windows/runner/mpv/utils.cpp b/windows/runner/mpv/utils.cpp new file mode 100644 index 00000000..4960d9c1 --- /dev/null +++ b/windows/runner/mpv/utils.cpp @@ -0,0 +1,113 @@ +#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() { + 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; +} + +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 new file mode 100644 index 00000000..5fd07796 --- /dev/null +++ b/windows/runner/mpv/utils.h @@ -0,0 +1,19 @@ +#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_