windows
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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([&]() {
|
||||
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
Reference in New Issue
Block a user