This commit is contained in:
edde746
2025-12-02 15:46:19 +01:00
parent e0fd77d7de
commit 920658485f
17 changed files with 1704 additions and 3 deletions
+7 -1
View File
@@ -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();
}
}
@@ -422,6 +422,7 @@ class MpvPlayerNative implements MpvPlayer {
@override
Future<void> 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<String?> getProperty(String name) async {
_checkDisposed();
await _ensureInitialized();
return await _methodChannel.invokeMethod<String>('getProperty', {
'name': name,
});
@@ -439,6 +441,7 @@ class MpvPlayerNative implements MpvPlayer {
@override
Future<void> command(List<String> args) async {
_checkDisposed();
await _ensureInitialized();
await _methodChannel.invokeMethod('command', {'args': args});
}
@@ -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<void> 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,
});
}
}
+55
View File
@@ -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<MpvVideo> {
Rect? _lastRect;
@override
Widget build(BuildContext context) {
return Container(
@@ -63,6 +66,58 @@ class _MpvVideoState extends State<MpvVideo> {
}
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,
);
}
}
}
+17
View File
@@ -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)
+10 -2
View File
@@ -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)
+8
View File
@@ -3,6 +3,7 @@
#include <optional>
#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([&]() {
+158
View File
@@ -0,0 +1,158 @@
#include "mpv_container.h"
#include <ShObjIdl.h>
#include <dwmapi.h>
#include <fstream>
#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<LONG_PTR>(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<HWND>(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<HWND>(user_data));
}
break;
}
default:
break;
}
return ::DefWindowProc(window, message, wparam, lparam);
}
std::unique_ptr<MpvContainer> MpvContainer::instance_ =
std::make_unique<MpvContainer>();
} // namespace mpv
+44
View File
@@ -0,0 +1,44 @@
#ifndef MPV_CONTAINER_H_
#define MPV_CONTAINER_H_
#include <Windows.h>
#include <memory>
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<MpvContainer> instance_;
};
} // namespace mpv
#endif // MPV_CONTAINER_H_
+259
View File
@@ -0,0 +1,259 @@
#include "mpv_core.h"
#include <fstream>
#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<MpvCore> instance) {
instance_ = std::move(instance);
}
std::optional<int32_t> MpvCore::GetProcId() { return proc_id_; }
void MpvCore::SetProcId(std::optional<int32_t> 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<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: {
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::milliseconds>(
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<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;
}
std::unique_ptr<MpvCore> MpvCore::instance_ = nullptr;
std::optional<int32_t> MpvCore::proc_id_ = std::nullopt;
} // namespace mpv
+71
View File
@@ -0,0 +1,71 @@
#ifndef MPV_CORE_H_
#define MPV_CORE_H_
#include <Windows.h>
#include <chrono>
#include <cmath>
#include <map>
#include <memory>
#include <optional>
#include <thread>
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<MpvCore> instance);
static std::optional<int32_t> GetProcId();
static void SetProcId(std::optional<int32_t> 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<HRESULT> 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<HWND, RECT> 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<MpvCore> instance_;
static std::optional<int32_t> proc_id_;
};
} // namespace mpv
#endif // MPV_CORE_H_
+383
View File
@@ -0,0 +1,383 @@
#include "mpv_player.h"
#include <cstring>
#include <fstream>
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<int64_t>(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<std::string>& args) {
if (!mpv_) return;
std::vector<const char*> 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<std::mutex> 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<mpv_event_log_message*>(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<mpv_event_property*>(event->data);
mpv_node node;
node.format = prop->format;
switch (prop->format) {
case MPV_FORMAT_STRING:
node.u.string = prop->data ? *static_cast<char**>(prop->data) : nullptr;
break;
case MPV_FORMAT_FLAG:
node.u.flag = prop->data ? *static_cast<int*>(prop->data) : 0;
break;
case MPV_FORMAT_INT64:
node.u.int64 = prop->data ? *static_cast<int64_t*>(prop->data) : 0;
break;
case MPV_FORMAT_DOUBLE:
node.u.double_ = prop->data ? *static_cast<double*>(prop->data) : 0.0;
break;
case MPV_FORMAT_NODE:
if (prop->data) {
node = *static_cast<mpv_node*>(prop->data);
}
break;
default:
node.format = MPV_FORMAT_NONE;
break;
}
SendPropertyChange(prop->name, &node);
break;
}
case MPV_EVENT_END_FILE: {
auto* end = static_cast<mpv_event_end_file*>(event->data);
flutter::EncodableMap data;
data[flutter::EncodableValue("reason")] =
flutter::EncodableValue(static_cast<int>(end->reason));
if (end->reason == MPV_END_FILE_REASON_ERROR) {
data[flutter::EncodableValue("error")] =
flutter::EncodableValue(static_cast<int>(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<std::mutex> 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<std::mutex> lock(callback_mutex_);
if (event_callback_) {
event_callback_(event);
}
}
} // namespace mpv
+90
View File
@@ -0,0 +1,90 @@
#ifndef MPV_PLAYER_H_
#define MPV_PLAYER_H_
#include <Windows.h>
#include <mpv/client.h>
#include <atomic>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include <flutter/encodable_value.h>
namespace mpv {
// Wrapper for libmpv that handles initialization, commands, properties,
// and event dispatching.
class MpvPlayer {
public:
using EventCallback =
std::function<void(const flutter::EncodableMap&)>;
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<std::string>& 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<bool> running_{false};
EventCallback event_callback_;
std::mutex callback_mutex_;
uint64_t next_reply_userdata_ = 1;
std::map<std::string, uint64_t> observed_properties_;
};
} // namespace mpv
#endif // MPV_PLAYER_H_
+380
View File
@@ -0,0 +1,380 @@
#include "mpv_plugin.h"
#include <fstream>
#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<flutter::PluginRegistrarWindows>(registrar));
LogToFile("MpvPlayerPlugin: RegisterWithRegistrar completed");
}
namespace mpv {
void MpvPlayerPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarWindows* registrar) {
auto plugin = std::make_unique<MpvPlayerPlugin>(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<flutter::MethodChannel<flutter::EncodableValue>>(
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<flutter::EventChannel<flutter::EncodableValue>>(
registrar->messenger(), "com.plezy/mpv_player/events",
&flutter::StandardMethodCodec::GetInstance());
auto handler = std::make_unique<
flutter::StreamHandlerFunctions<flutter::EncodableValue>>(
[this](const flutter::EncodableValue* arguments,
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&&
events) -> std::unique_ptr<flutter::StreamHandlerError<
flutter::EncodableValue>> {
event_sink_ = std::move(events);
return nullptr;
},
[this](const flutter::EncodableValue* arguments)
-> std::unique_ptr<
flutter::StreamHandlerError<flutter::EncodableValue>> {
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<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> 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<MpvCore>(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<HRESULT>(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<MpvPlayer>();
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<flutter::EncodableMap>(*args)) {
result->Error("INVALID_ARGS", "Expected map argument");
return;
}
const auto& map = std::get<flutter::EncodableMap>(*args);
auto it = map.find(flutter::EncodableValue("args"));
if (it == map.end() ||
!std::holds_alternative<flutter::EncodableList>(it->second)) {
result->Error("INVALID_ARGS", "Missing 'args' list");
return;
}
const auto& list = std::get<flutter::EncodableList>(it->second);
std::vector<std::string> command_args;
for (const auto& item : list) {
if (std::holds_alternative<std::string>(item)) {
command_args.push_back(std::get<std::string>(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<flutter::EncodableMap>(*args)) {
result->Error("INVALID_ARGS", "Expected map argument");
return;
}
const auto& map = std::get<flutter::EncodableMap>(*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<std::string>(name_it->second)) {
result->Error("INVALID_ARGS", "Missing 'name'");
return;
}
if (value_it == map.end() ||
!std::holds_alternative<std::string>(value_it->second)) {
result->Error("INVALID_ARGS", "Missing 'value'");
return;
}
player_->SetProperty(std::get<std::string>(name_it->second),
std::get<std::string>(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<flutter::EncodableMap>(*args)) {
result->Error("INVALID_ARGS", "Expected map argument");
return;
}
const auto& map = std::get<flutter::EncodableMap>(*args);
auto name_it = map.find(flutter::EncodableValue("name"));
if (name_it == map.end() ||
!std::holds_alternative<std::string>(name_it->second)) {
result->Error("INVALID_ARGS", "Missing 'name'");
return;
}
std::string value =
player_->GetProperty(std::get<std::string>(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<flutter::EncodableMap>(*args)) {
result->Error("INVALID_ARGS", "Expected map argument");
return;
}
const auto& map = std::get<flutter::EncodableMap>(*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<std::string>(name_it->second)) {
result->Error("INVALID_ARGS", "Missing 'name'");
return;
}
if (format_it == map.end() ||
!std::holds_alternative<std::string>(format_it->second)) {
result->Error("INVALID_ARGS", "Missing 'format'");
return;
}
player_->ObserveProperty(std::get<std::string>(name_it->second),
std::get<std::string>(format_it->second));
result->Success();
} else if (method == "setVisible") {
const auto* args = method_call.arguments();
if (!args || !std::holds_alternative<flutter::EncodableMap>(*args)) {
result->Error("INVALID_ARGS", "Expected map argument");
return;
}
const auto& map = std::get<flutter::EncodableMap>(*args);
auto visible_it = map.find(flutter::EncodableValue("visible"));
if (visible_it == map.end() ||
!std::holds_alternative<bool>(visible_it->second)) {
result->Error("INVALID_ARGS", "Missing 'visible'");
return;
}
bool visible = std::get<bool>(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<flutter::EncodableMap>(*args)) {
result->Error("INVALID_ARGS", "Expected map argument");
return;
}
const auto& map = std::get<flutter::EncodableMap>(*args);
auto get_int = [&map](const char* key) -> int {
auto it = map.find(flutter::EncodableValue(key));
if (it != map.end()) {
if (std::holds_alternative<int32_t>(it->second)) {
return std::get<int32_t>(it->second);
} else if (std::holds_alternative<int64_t>(it->second)) {
return static_cast<int>(std::get<int64_t>(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<double>(it->second)) {
return std::get<double>(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
+53
View File
@@ -0,0 +1,53 @@
#ifndef MPV_PLUGIN_H_
#define MPV_PLUGIN_H_
#include <flutter/encodable_value.h>
#include <flutter/event_channel.h>
#include <flutter/event_stream_handler_functions.h>
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <memory>
#include <optional>
#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<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
void SendEvent(const flutter::EncodableMap& event);
HWND GetWindow();
HWND GetChildWindow();
flutter::PluginRegistrarWindows* registrar_;
std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>>
method_channel_;
std::unique_ptr<flutter::EventChannel<flutter::EncodableValue>>
event_channel_;
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>> event_sink_;
std::unique_ptr<MpvPlayer> player_;
std::optional<int32_t> proc_id_;
};
} // namespace mpv
#endif // MPV_PLUGIN_H_
+113
View File
@@ -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>(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
+19
View File
@@ -0,0 +1,19 @@
#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_