fix(mpv): avoid blocking runtime calls

This commit is contained in:
edde746
2026-05-03 08:23:44 +02:00
parent f5901389e0
commit 5010833da9
12 changed files with 662 additions and 292 deletions
+112 -60
View File
@@ -82,13 +82,25 @@ bool MpvPlayer::Initialize(HWND container, HWND flutter_window) {
void MpvPlayer::Dispose() {
StopEventLoop();
// Cancel pending async commands
// Cancel pending async requests
std::vector<StatusCallback> status_callbacks;
std::vector<GetPropertyCallback> get_callbacks;
{
std::lock_guard<std::mutex> lock(pending_commands_mutex_);
for (auto& pair : pending_commands_) {
if (pair.second) pair.second(-1); // Call with error
std::lock_guard<std::mutex> lock(pending_requests_mutex_);
for (auto& pair : pending_status_requests_) {
if (pair.second) status_callbacks.push_back(std::move(pair.second));
}
pending_commands_.clear();
for (auto& pair : pending_get_property_requests_) {
if (pair.second) get_callbacks.push_back(std::move(pair.second));
}
pending_status_requests_.clear();
pending_get_property_requests_.clear();
}
for (auto& callback : status_callbacks) {
callback(-1);
}
for (auto& callback : get_callbacks) {
callback(-1, "");
}
if (mpv_) {
@@ -104,18 +116,7 @@ void MpvPlayer::Dispose() {
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::Command(const std::vector<std::string>& args) { CommandAsync(args, nullptr); }
void MpvPlayer::CommandAsync(const std::vector<std::string>& args, CommandCallback callback) {
if (!mpv_) {
@@ -130,50 +131,88 @@ void MpvPlayer::CommandAsync(const std::vector<std::string>& args, CommandCallba
}
c_args.push_back(nullptr);
// Generate unique request ID and store callback
uint64_t request_id;
{
std::lock_guard<std::mutex> lock(pending_commands_mutex_);
request_id = next_reply_userdata_++;
pending_commands_[request_id] = std::move(callback);
}
uint64_t request_id = callback ? RegisterStatusRequest(std::move(callback)) : 0;
// mpv_command_async returns immediately
int result = mpv_command_async(mpv_, request_id, c_args.data());
if (result < 0) {
// Submission failed, complete immediately with error
std::lock_guard<std::mutex> lock(pending_commands_mutex_);
auto it = pending_commands_.find(request_id);
if (it != pending_commands_.end()) {
auto cb = std::move(it->second);
pending_commands_.erase(it);
if (cb) cb(result);
}
auto cb = TakeStatusRequest(request_id);
if (cb) cb(result);
}
}
void MpvPlayer::SetProperty(const std::string& name, const std::string& value) {
if (!mpv_) return;
SetPropertyAsync(name, value, nullptr);
}
void MpvPlayer::SetPropertyAsync(const std::string& name, const std::string& value, StatusCallback callback) {
if (!mpv_) {
if (callback) callback(0);
return;
}
// Handle custom HDR toggle property (same pattern as iOS/macOS)
if (name == "hdr-enabled") {
bool enabled = (value == "yes" || value == "true" || value == "1");
SetHDREnabled(enabled);
SetHDREnabled(enabled, std::move(callback));
return;
}
mpv_set_property_string(mpv_, name.c_str(), value.c_str());
uint64_t request_id = callback ? RegisterStatusRequest(std::move(callback)) : 0;
char* property_value = const_cast<char*>(value.c_str());
int result = mpv_set_property_async(mpv_, request_id, name.c_str(), MPV_FORMAT_STRING, &property_value);
if (result < 0) {
auto cb = TakeStatusRequest(request_id);
if (cb) cb(result);
}
}
std::string MpvPlayer::GetProperty(const std::string& name) {
if (!mpv_) return "";
void MpvPlayer::GetPropertyAsync(const std::string& name, GetPropertyCallback callback) {
if (!mpv_) {
if (callback) callback(-1, "");
return;
}
char* value = mpv_get_property_string(mpv_, name.c_str());
if (!value) return "";
uint64_t request_id = RegisterGetPropertyRequest(std::move(callback));
std::string result(value);
mpv_free(value);
return result;
int result = mpv_get_property_async(mpv_, request_id, name.c_str(), MPV_FORMAT_STRING);
if (result < 0) {
auto cb = TakeGetPropertyRequest(request_id);
if (cb) cb(result, "");
}
}
uint64_t MpvPlayer::RegisterStatusRequest(StatusCallback callback) {
std::lock_guard<std::mutex> lock(pending_requests_mutex_);
uint64_t request_id = next_reply_userdata_++;
pending_status_requests_[request_id] = std::move(callback);
return request_id;
}
MpvPlayer::StatusCallback MpvPlayer::TakeStatusRequest(uint64_t request_id) {
std::lock_guard<std::mutex> lock(pending_requests_mutex_);
auto it = pending_status_requests_.find(request_id);
if (it == pending_status_requests_.end()) return nullptr;
auto callback = std::move(it->second);
pending_status_requests_.erase(it);
return callback;
}
uint64_t MpvPlayer::RegisterGetPropertyRequest(GetPropertyCallback callback) {
std::lock_guard<std::mutex> lock(pending_requests_mutex_);
uint64_t request_id = next_reply_userdata_++;
pending_get_property_requests_[request_id] = std::move(callback);
return request_id;
}
MpvPlayer::GetPropertyCallback MpvPlayer::TakeGetPropertyRequest(uint64_t request_id) {
std::lock_guard<std::mutex> lock(pending_requests_mutex_);
auto it = pending_get_property_requests_.find(request_id);
if (it == pending_get_property_requests_.end()) return nullptr;
auto callback = std::move(it->second);
pending_get_property_requests_.erase(it);
return callback;
}
void MpvPlayer::ObserveProperty(const std::string& name, const std::string& format, int id) {
@@ -287,23 +326,31 @@ void MpvPlayer::EventLoop() {
void MpvPlayer::HandleMpvEvent(mpv_event* event) {
switch (event->event_id) {
case MPV_EVENT_COMMAND_REPLY: {
// Handle async command completion
case MPV_EVENT_COMMAND_REPLY:
case MPV_EVENT_SET_PROPERTY_REPLY: {
uint64_t request_id = event->reply_userdata;
CommandCallback callback;
{
std::lock_guard<std::mutex> lock(pending_commands_mutex_);
auto it = pending_commands_.find(request_id);
if (it != pending_commands_.end()) {
callback = std::move(it->second);
pending_commands_.erase(it);
}
}
StatusCallback callback = TakeStatusRequest(request_id);
if (callback) {
callback(event->error);
}
break;
}
case MPV_EVENT_GET_PROPERTY_REPLY: {
uint64_t request_id = event->reply_userdata;
GetPropertyCallback callback = TakeGetPropertyRequest(request_id);
if (callback) {
std::string value;
if (event->error >= 0) {
auto* prop = static_cast<mpv_event_property*>(event->data);
if (prop && prop->format == MPV_FORMAT_STRING && prop->data) {
auto c_value = *static_cast<char**>(prop->data);
if (c_value) value = SanitizeUtf8(c_value);
}
}
callback(event->error, value);
}
break;
}
case MPV_EVENT_LOG_MESSAGE: {
auto* msg = static_cast<mpv_event_log_message*>(event->data);
char log_msg[512];
@@ -356,11 +403,14 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
// to null output (e.g. after sleep/wake or device unplug), re-set
// audio-device to switch back to the real output.
// Mirrors mpv's TOOLS/lua/ao-null-reload.lua for embedded libmpv.
if (strcmp(prop->name, "audio-device-list") == 0 && GetProperty("current-ao") == "null") {
auto device = GetProperty("audio-device");
if (!device.empty()) {
mpv_set_property_string(mpv_, "audio-device", device.c_str());
}
if (strcmp(prop->name, "audio-device-list") == 0) {
GetPropertyAsync("current-ao", [this](int ao_error, const std::string& current_ao) {
if (ao_error < 0 || current_ao != "null") return;
GetPropertyAsync("audio-device", [this](int device_error, const std::string& device) {
if (device_error < 0 || device.empty()) return;
SetProperty("audio-device", device);
});
});
}
SendPropertyChange(prop->name, &node);
@@ -445,11 +495,13 @@ void MpvPlayer::SendEvent(const std::string& name, const flutter::EncodableMap&
}
}
void MpvPlayer::SetHDREnabled(bool enabled) {
void MpvPlayer::SetHDREnabled(bool enabled, StatusCallback callback) {
hdr_enabled_ = enabled;
if (mpv_) {
mpv_set_property_string(mpv_, "target-colorspace-hint", enabled ? "yes" : "no");
SetPropertyAsync("target-colorspace-hint", enabled ? "yes" : "no", std::move(callback));
} else if (callback) {
callback(0);
}
UpdateHDRMode(last_sig_peak_);
+20 -11
View File
@@ -34,21 +34,25 @@ class MpvPlayer {
// Returns true if mpv is initialized.
bool IsInitialized() const { return mpv_ != nullptr; }
// Executes an mpv command.
// Queues an mpv command without waiting for completion.
void Command(const std::vector<std::string>& args);
// Callback type for async command completion.
using CommandCallback = std::function<void(int error)>;
// Callback types for async mpv requests.
using StatusCallback = std::function<void(int error)>;
using CommandCallback = StatusCallback;
using GetPropertyCallback = std::function<void(int error, const std::string& value)>;
// Executes an mpv command asynchronously to prevent UI blocking.
// The callback is called on the main thread when the command completes.
void CommandAsync(const std::vector<std::string>& args, CommandCallback callback);
// Sets an mpv property.
// Queues an mpv property update without waiting for completion.
void SetProperty(const std::string& name, const std::string& value);
// Gets an mpv property.
std::string GetProperty(const std::string& name);
// Sets an mpv property asynchronously.
void SetPropertyAsync(const std::string& name, const std::string& value, StatusCallback callback);
// Gets an mpv property asynchronously.
void GetPropertyAsync(const std::string& name, GetPropertyCallback callback);
// Observes an mpv property for changes.
void ObserveProperty(const std::string& name, const std::string& format, int id);
@@ -75,6 +79,10 @@ class MpvPlayer {
void HandleMpvEvent(mpv_event* event);
void SendPropertyChange(const char* name, mpv_node* data);
void SendEvent(const std::string& name, const flutter::EncodableMap& data = {});
uint64_t RegisterStatusRequest(StatusCallback callback);
StatusCallback TakeStatusRequest(uint64_t request_id);
uint64_t RegisterGetPropertyRequest(GetPropertyCallback callback);
GetPropertyCallback TakeGetPropertyRequest(uint64_t request_id);
mpv_handle* mpv_ = nullptr;
HWND hwnd_ = nullptr;
@@ -92,16 +100,17 @@ class MpvPlayer {
std::map<std::string, uint64_t> observed_properties_;
std::map<std::string, int> name_to_id_;
// Pending async commands: request_id -> callback
std::map<uint64_t, CommandCallback> pending_commands_;
std::mutex pending_commands_mutex_;
// Pending async requests: request_id -> callback
std::map<uint64_t, StatusCallback> pending_status_requests_;
std::map<uint64_t, GetPropertyCallback> pending_get_property_requests_;
std::mutex pending_requests_mutex_;
// HDR state
bool hdr_enabled_ = true; // User preference
double last_sig_peak_ = 0.0; // Last known sig-peak for HDR content detection
// HDR methods
void SetHDREnabled(bool enabled);
void SetHDREnabled(bool enabled, StatusCallback callback = nullptr);
void UpdateHDRMode(double sigPeak);
};
+72 -17
View File
@@ -18,12 +18,17 @@ void MpvPlayerPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef regis
namespace mpv {
namespace {
constexpr UINT kPlatformTaskMessage = WM_APP + 0x4D50;
}
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) {
MpvPlayerPlugin::MpvPlayerPlugin(flutter::PluginRegistrarWindows* registrar)
: registrar_(registrar), platform_thread_id_(::GetCurrentThreadId()) {
// Create method channel.
method_channel_ = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
registrar->messenger(), "com.plezy/mpv_player", &flutter::StandardMethodCodec::GetInstance());
@@ -53,6 +58,8 @@ MpvPlayerPlugin::MpvPlayerPlugin(flutter::PluginRegistrarWindows* registrar) : r
}
MpvPlayerPlugin::~MpvPlayerPlugin() {
DrainPlatformTasks();
// Unregister window proc delegate.
if (proc_id_) {
registrar_->UnregisterTopLevelWindowProcDelegate(proc_id_.value());
@@ -64,6 +71,35 @@ HWND MpvPlayerPlugin::GetChildWindow() { return registrar_->GetView()->GetNative
HWND MpvPlayerPlugin::GetWindow() { return ::GetAncestor(GetChildWindow(), GA_ROOT); }
void MpvPlayerPlugin::PostToPlatformThread(std::function<void()> task) {
if (::GetCurrentThreadId() == platform_thread_id_) {
task();
return;
}
{
std::lock_guard<std::mutex> lock(platform_tasks_mutex_);
platform_tasks_.push(std::move(task));
}
if (flutter_window_) {
::PostMessage(flutter_window_, kPlatformTaskMessage, 0, 0);
}
}
void MpvPlayerPlugin::DrainPlatformTasks() {
std::queue<std::function<void()>> tasks;
{
std::lock_guard<std::mutex> lock(platform_tasks_mutex_);
tasks.swap(platform_tasks_);
}
while (!tasks.empty()) {
tasks.front()();
tasks.pop();
}
}
void MpvPlayerPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
@@ -77,11 +113,17 @@ void MpvPlayerPlugin::HandleMethodCall(
}
HWND flutter_window = GetWindow();
flutter_window_ = flutter_window;
MpvCore::SetInstance(std::make_unique<MpvCore>(flutter_window));
proc_id_ =
registrar_->RegisterTopLevelWindowProcDelegate([](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
registrar_->RegisterTopLevelWindowProcDelegate([this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
if (message == kPlatformTaskMessage) {
DrainPlatformTasks();
return std::optional<HRESULT>(0);
}
auto* core = MpvCore::GetInstance();
if (core) {
return core->WindowProc(hwnd, message, wparam, lparam);
@@ -160,13 +202,15 @@ void MpvPlayerPlugin::HandleMethodCall(
auto result_ptr =
std::make_shared<std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>>(std::move(result));
std::string cmd_name = command_args.empty() ? "unknown" : command_args[0];
player_->CommandAsync(command_args, [result_ptr, cmd_name](int error) {
if (error < 0) {
(*result_ptr)
->Error("COMMAND_FAILED", "MPV command failed: " + cmd_name + " (error " + std::to_string(error) + ")");
} else {
(*result_ptr)->Success();
}
player_->CommandAsync(command_args, [this, result_ptr, cmd_name](int error) {
PostToPlatformThread([result_ptr, cmd_name, error]() {
if (error < 0) {
(*result_ptr)
->Error("COMMAND_FAILED", "MPV command failed: " + cmd_name + " (error " + std::to_string(error) + ")");
} else {
(*result_ptr)->Success();
}
});
});
return; // Response will be sent asynchronously
} else if (method == "setProperty") {
@@ -194,8 +238,12 @@ void MpvPlayerPlugin::HandleMethodCall(
return;
}
player_->SetProperty(std::get<std::string>(name_it->second), std::get<std::string>(value_it->second));
result->Success();
auto result_ptr =
std::make_shared<std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>>(std::move(result));
player_->SetPropertyAsync(
std::get<std::string>(name_it->second), std::get<std::string>(value_it->second),
[this, result_ptr](int error) { PostToPlatformThread([result_ptr]() { (*result_ptr)->Success(); }); });
return;
} else if (method == "setLogLevel") {
if (!player_ || !player_->IsInitialized()) {
result->Error("NOT_INITIALIZED", "Player not initialized");
@@ -238,12 +286,19 @@ void MpvPlayerPlugin::HandleMethodCall(
return;
}
std::string value = player_->GetProperty(std::get<std::string>(name_it->second));
if (value.empty()) {
result->Success();
} else {
result->Success(flutter::EncodableValue(value));
}
auto result_ptr =
std::make_shared<std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>>(std::move(result));
player_->GetPropertyAsync(
std::get<std::string>(name_it->second), [this, result_ptr](int error, const std::string& value) {
PostToPlatformThread([result_ptr, error, value]() {
if (error < 0 || value.empty()) {
(*result_ptr)->Success();
} else {
(*result_ptr)->Success(flutter::EncodableValue(value));
}
});
});
return;
} else if (method == "observeProperty") {
if (!player_ || !player_->IsInitialized()) {
result->Error("NOT_INITIALIZED", "Player not initialized");
+9
View File
@@ -8,8 +8,11 @@
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <queue>
#include "display_mode_manager.h"
#include "mpv_core.h"
@@ -33,11 +36,15 @@ class MpvPlayerPlugin : public flutter::Plugin {
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
void SendEvent(const flutter::EncodableValue& event);
void PostToPlatformThread(std::function<void()> task);
void DrainPlatformTasks();
HWND GetWindow();
HWND GetChildWindow();
flutter::PluginRegistrarWindows* registrar_;
DWORD platform_thread_id_;
HWND flutter_window_ = nullptr;
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_;
@@ -45,6 +52,8 @@ class MpvPlayerPlugin : public flutter::Plugin {
std::unique_ptr<MpvPlayer> player_;
DisplayModeManager display_mode_manager_;
std::optional<int32_t> proc_id_;
std::mutex platform_tasks_mutex_;
std::queue<std::function<void()>> platform_tasks_;
};
} // namespace mpv