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
+12 -21
View File
@@ -50,36 +50,31 @@ class MpvPlayerCore: MpvPlayerCoreBase {
} }
func switchToPipVO(layerPtr: UnsafeMutableRawPointer) -> Bool { func switchToPipVO(layerPtr: UnsafeMutableRawPointer) -> Bool {
guard let mpv else { return false } guard mpv != nil else { return false }
print("[MpvPlayerCore] Switching to pip VO for PiP") print("[MpvPlayerCore] Switching to pip VO for PiP")
metalLayer?.removeFromSuperlayer() metalLayer?.removeFromSuperlayer()
mpv_set_property_string(mpv, "vid", "no") setProperty("vid", value: "no")
setInt64PropertyAsync("wid", value: Int64(Int(bitPattern: layerPtr))) { _ in }
var pointer = Int64(Int(bitPattern: layerPtr)) setProperty("vo", value: "pip")
mpv_set_property(mpv, "wid", MPV_FORMAT_INT64, &pointer) setProperty("vid", value: "auto")
mpv_set_property_string(mpv, "vo", "pip")
mpv_set_property_string(mpv, "vid", "auto")
print("[MpvPlayerCore] Switched to pip VO successfully") print("[MpvPlayerCore] Switched to pip VO successfully")
return true return true
} }
func switchToGpuNextVO() -> Bool { func switchToGpuNextVO() -> Bool {
guard let mpv, let metalLayer else { return false } guard mpv != nil, let metalLayer else { return false }
print("[MpvPlayerCore] Switching back to gpu-next VO") print("[MpvPlayerCore] Switching back to gpu-next VO")
mpv_set_property_string(mpv, "vid", "no") setProperty("vid", value: "no")
setInt64PropertyAsync("wid", value: Int64(Int(bitPattern: Unmanaged.passUnretained(metalLayer).toOpaque()))) { _ in
var layer = metalLayer }
mpv_set_property(mpv, "wid", MPV_FORMAT_INT64, &layer)
applyGpuNextOptions() applyGpuNextOptions()
mpv_set_property_string(mpv, "vid", "auto") setProperty("vid", value: "auto")
if metalLayer.superlayer == nil, let containerView { if metalLayer.superlayer == nil, let containerView {
containerView.layer.addSublayer(metalLayer) containerView.layer.addSublayer(metalLayer)
@@ -179,9 +174,7 @@ class MpvPlayerCore: MpvPlayerCoreBase {
} }
print("[MpvPlayerCore] Entering background - disabling video") print("[MpvPlayerCore] Entering background - disabling video")
if mpv != nil { setProperty("vid", value: "no")
mpv_set_option_string(mpv, "vid", "no")
}
} }
@objc private func enterForeground() { @objc private func enterForeground() {
@@ -191,8 +184,6 @@ class MpvPlayerCore: MpvPlayerCoreBase {
} }
print("[MpvPlayerCore] Entering foreground - enabling video") print("[MpvPlayerCore] Entering foreground - enabling video")
if mpv != nil { setProperty("vid", value: "auto")
mpv_set_option_string(mpv, "vid", "auto")
}
} }
} }
+20 -10
View File
@@ -230,7 +230,12 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
stopPipTimebaseSync() stopPipTimebaseSync()
pipController?.flushLayer() pipController?.flushLayer()
let restoredInlineVO = playerCore?.switchToGpuNextVO() ?? false let restoredInlineVO = playerCore?.switchToGpuNextVO() ?? false
if pause { playerCore?.setProperty("pause", value: "yes") } if pause {
playerCore?.setPropertyAsync("pause", value: "yes") { [weak self] _ in
self?.pipController?.invalidatePlaybackState()
self?.syncPipTimebase()
}
}
pendingInlineRestoreAfterPip = restoredInlineVO pendingInlineRestoreAfterPip = restoredInlineVO
if pendingInlineRestoreAfterPip { if pendingInlineRestoreAfterPip {
if isSceneActive { if isSceneActive {
@@ -340,14 +345,18 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
return return
} }
playerCore?.setProperty(name, value: value) guard let core = playerCore else {
result(nil)
if name == "pause" { return
pipController?.invalidatePlaybackState()
if playerCore?.isPipActive == true { syncPipTimebase() }
} }
result(nil) core.setPropertyAsync(name, value: value) { [weak self] _ in
if name == "pause" {
self?.pipController?.invalidatePlaybackState()
if core.isPipActive == true { self?.syncPipTimebase() }
}
result(nil)
}
} }
// MARK: - Helpers // MARK: - Helpers
@@ -400,9 +409,10 @@ extension MpvPlayerPlugin: MpvPipDelegate {
} }
func pipSetPlaying(_ playing: Bool) { func pipSetPlaying(_ playing: Bool) {
playerCore?.setProperty("pause", value: playing ? "no" : "yes") playerCore?.setPropertyAsync("pause", value: playing ? "no" : "yes") { [weak self] _ in
pipController?.invalidatePlaybackState() self?.pipController?.invalidatePlaybackState()
syncPipTimebase() self?.syncPipTimebase()
}
} }
func pipSkip(byInterval seconds: Double) { func pipSkip(byInterval seconds: Double) {
+109 -49
View File
@@ -202,13 +202,25 @@ void MpvPlayer::Dispose() {
event_callback_ = nullptr; event_callback_ = nullptr;
} }
// 4. Cancel pending async commands // 4. Cancel pending async requests
std::vector<StatusCallback> status_callbacks;
std::vector<GetPropertyCallback> get_callbacks;
{ {
std::lock_guard<std::mutex> cmd_lock(pending_commands_mutex_); std::lock_guard<std::mutex> request_lock(pending_requests_mutex_);
for (auto& pair : pending_commands_) { for (auto& pair : pending_status_requests_) {
if (pair.second) pair.second(-1); 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, "");
} }
// 5. Remove pending idle callbacks // 5. Remove pending idle callbacks
@@ -271,18 +283,7 @@ void MpvPlayer::Render(int width, int height, int fbo) {
mpv_render_context_render(mpv_gl_, params); mpv_render_context_render(mpv_gl_, params);
} }
void MpvPlayer::Command(const std::vector<std::string>& args) { void MpvPlayer::Command(const std::vector<std::string>& args) { CommandAsync(args, nullptr); }
if (disposed_ || !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::CommandAsync(const std::vector<std::string>& args, CommandCallback callback) { void MpvPlayer::CommandAsync(const std::vector<std::string>& args, CommandCallback callback) {
if (disposed_ || !mpv_) { if (disposed_ || !mpv_) {
@@ -297,39 +298,80 @@ void MpvPlayer::CommandAsync(const std::vector<std::string>& args, CommandCallba
} }
c_args.push_back(nullptr); c_args.push_back(nullptr);
uint64_t request_id; uint64_t request_id = callback ? RegisterStatusRequest(std::move(callback)) : 0;
{
std::lock_guard<std::mutex> lock(pending_commands_mutex_);
request_id = next_reply_userdata_++;
pending_commands_[request_id] = std::move(callback);
}
int result = mpv_command_async(mpv_, request_id, c_args.data()); int result = mpv_command_async(mpv_, request_id, c_args.data());
if (result < 0) { if (result < 0) {
std::lock_guard<std::mutex> lock(pending_commands_mutex_); auto cb = TakeStatusRequest(request_id);
auto it = pending_commands_.find(request_id); if (cb) cb(result);
if (it != pending_commands_.end()) {
auto cb = std::move(it->second);
pending_commands_.erase(it);
if (cb) cb(result);
}
} }
} }
void MpvPlayer::SetProperty(const std::string& name, const std::string& value) { void MpvPlayer::SetProperty(const std::string& name, const std::string& value) {
if (disposed_ || !mpv_) return; SetPropertyAsync(name, value, nullptr);
mpv_set_property_string(mpv_, name.c_str(), value.c_str());
} }
std::string MpvPlayer::GetProperty(const std::string& name) { void MpvPlayer::SetPropertyAsync(const std::string& name, const std::string& value, StatusCallback callback) {
if (disposed_ || !mpv_) return ""; if (disposed_ || !mpv_) {
if (callback) callback(0);
return;
}
char* value = mpv_get_property_string(mpv_, name.c_str()); uint64_t request_id = callback ? RegisterStatusRequest(std::move(callback)) : 0;
if (!value) return "";
std::string result(value); char* property_value = const_cast<char*>(value.c_str());
mpv_free(value); int result = mpv_set_property_async(mpv_, request_id, name.c_str(), MPV_FORMAT_STRING, &property_value);
return result; if (result < 0) {
auto cb = TakeStatusRequest(request_id);
if (cb) cb(result);
}
}
void MpvPlayer::GetPropertyAsync(const std::string& name, GetPropertyCallback callback) {
if (disposed_ || !mpv_) {
if (callback) callback(-1, "");
return;
}
uint64_t request_id = RegisterGetPropertyRequest(std::move(callback));
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) { void MpvPlayer::ObserveProperty(const std::string& name, const std::string& format, int id) {
@@ -446,17 +488,10 @@ bool MpvPlayer::ProcessEvents() {
void MpvPlayer::HandleMpvEvent(mpv_event* event) { void MpvPlayer::HandleMpvEvent(mpv_event* event) {
switch (event->event_id) { switch (event->event_id) {
case MPV_EVENT_COMMAND_REPLY: { case MPV_EVENT_COMMAND_REPLY:
case MPV_EVENT_SET_PROPERTY_REPLY: {
uint64_t request_id = event->reply_userdata; uint64_t request_id = event->reply_userdata;
CommandCallback callback; StatusCallback callback = TakeStatusRequest(request_id);
{
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);
}
}
if (callback) { if (callback) {
int error = event->error; int error = event->error;
g_idle_add( g_idle_add(
@@ -470,6 +505,31 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
} }
break; break;
} }
case MPV_EVENT_GET_PROPERTY_REPLY: {
uint64_t request_id = event->reply_userdata;
GetPropertyCallback callback = TakeGetPropertyRequest(request_id);
if (callback) {
int error = event->error;
std::string value;
if (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);
}
}
g_idle_add(
[](gpointer data) -> gboolean {
auto* tuple = static_cast<std::tuple<GetPropertyCallback, int, std::string>*>(data);
const auto& callback = std::get<0>(*tuple);
if (callback) callback(std::get<1>(*tuple), std::get<2>(*tuple));
delete tuple;
return G_SOURCE_REMOVE;
},
new std::tuple<GetPropertyCallback, int, std::string>(std::move(callback), error, std::move(value)));
}
break;
}
case MPV_EVENT_LOG_MESSAGE: { case MPV_EVENT_LOG_MESSAGE: {
auto* msg = static_cast<mpv_event_log_message*>(event->data); auto* msg = static_cast<mpv_event_log_message*>(event->data);
g_message("MPV [%s] %s: %s", msg->level, msg->prefix, msg->text); g_message("MPV [%s] %s: %s", msg->level, msg->prefix, msg->text);
+20 -8
View File
@@ -15,6 +15,7 @@
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <thread> #include <thread>
#include <tuple>
#include <vector> #include <vector>
// Forward declaration for Flutter types // Forward declaration for Flutter types
@@ -68,11 +69,13 @@ class MpvPlayer {
/// Returns true if mpv handle exists (even without render context). /// Returns true if mpv handle exists (even without render context).
bool HasMpvHandle() const { return mpv_ != nullptr; } bool HasMpvHandle() const { return mpv_ != nullptr; }
/// Executes an mpv command. /// Queues an mpv command without waiting for completion.
void Command(const std::vector<std::string>& args); void Command(const std::vector<std::string>& args);
/// Callback type for async command completion. /// Callback types for async mpv requests.
using CommandCallback = std::function<void(int error)>; 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. /// Executes an mpv command asynchronously to prevent UI blocking.
void CommandAsync(const std::vector<std::string>& args, CommandCallback callback); void CommandAsync(const std::vector<std::string>& args, CommandCallback callback);
@@ -80,8 +83,11 @@ class MpvPlayer {
/// Sets an mpv property by name. /// Sets an mpv property by name.
void SetProperty(const std::string& name, const std::string& value); void SetProperty(const std::string& name, const std::string& value);
/// Gets an mpv property value by name. /// Sets an mpv property asynchronously.
std::string GetProperty(const std::string& name); void SetPropertyAsync(const std::string& name, const std::string& value, StatusCallback callback);
/// Gets an mpv property value asynchronously.
void GetPropertyAsync(const std::string& name, GetPropertyCallback callback);
/// Observes an mpv property for changes. /// Observes an mpv property for changes.
void ObserveProperty(const std::string& name, const std::string& format, int id); void ObserveProperty(const std::string& name, const std::string& format, int id);
@@ -126,6 +132,11 @@ class MpvPlayer {
/// Sends an event notification. /// Sends an event notification.
void SendEvent(const std::string& name, ::_FlValue* data = nullptr); void SendEvent(const std::string& name, ::_FlValue* data = nullptr);
uint64_t RegisterStatusRequest(StatusCallback callback);
StatusCallback TakeStatusRequest(uint64_t request_id);
uint64_t RegisterGetPropertyRequest(GetPropertyCallback callback);
GetPropertyCallback TakeGetPropertyRequest(uint64_t request_id);
/// Helper to convert mpv_node to FlValue. /// Helper to convert mpv_node to FlValue.
::_FlValue* NodeToFlValue(mpv_node* node); ::_FlValue* NodeToFlValue(mpv_node* node);
@@ -146,9 +157,10 @@ class MpvPlayer {
std::map<std::string, uint64_t> observed_properties_; std::map<std::string, uint64_t> observed_properties_;
std::map<std::string, int> name_to_id_; std::map<std::string, int> name_to_id_;
// Pending async commands: request_id -> callback // Pending async requests: request_id -> callback
std::map<uint64_t, CommandCallback> pending_commands_; std::map<uint64_t, StatusCallback> pending_status_requests_;
std::mutex pending_commands_mutex_; std::map<uint64_t, GetPropertyCallback> pending_get_property_requests_;
std::mutex pending_requests_mutex_;
// GSource for processing events on main thread // GSource for processing events on main thread
guint event_source_id_ = 0; guint event_source_id_ = 0;
+21 -8
View File
@@ -203,8 +203,14 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
} else if (value_value == nullptr || fl_value_get_type(value_value) != FL_VALUE_TYPE_STRING) { } else if (value_value == nullptr || fl_value_get_type(value_value) != FL_VALUE_TYPE_STRING) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'value'", nullptr)); response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'value'", nullptr));
} else { } else {
self->player->SetProperty(fl_value_get_string(name_value), fl_value_get_string(value_value)); g_object_ref(method_call);
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); self->player->SetPropertyAsync(
fl_value_get_string(name_value), fl_value_get_string(value_value), [method_call](int error) {
g_autoptr(FlMethodResponse) async_response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
fl_method_call_respond(method_call, async_response, nullptr);
g_object_unref(method_call);
});
return; // Response sent asynchronously
} }
} }
} else if (strcmp(method, "setLogLevel") == 0) { } else if (strcmp(method, "setLogLevel") == 0) {
@@ -229,12 +235,19 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
if (name_value == nullptr || fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) { if (name_value == nullptr || fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'name'", nullptr)); response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'name'", nullptr));
} else { } else {
std::string value = self->player->GetProperty(fl_value_get_string(name_value)); g_object_ref(method_call);
if (value.empty()) { self->player->GetPropertyAsync(
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); fl_value_get_string(name_value), [method_call](int error, const std::string& value) {
} else { g_autoptr(FlMethodResponse) async_response = nullptr;
response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_string(value.c_str()))); if (error < 0 || value.empty()) {
} async_response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
} else {
async_response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_string(value.c_str())));
}
fl_method_call_respond(method_call, async_response, nullptr);
g_object_unref(method_call);
});
return; // Response sent asynchronously
} }
} }
} else if (strcmp(method, "observeProperty") == 0) { } else if (strcmp(method, "observeProperty") == 0) {
+18 -13
View File
@@ -184,10 +184,8 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
// Get video dimensions for aspect ratio // Get video dimensions for aspect ratio
var aspectRatio = NSSize(width: 16, height: 9) // default var aspectRatio = NSSize(width: 16, height: 9) // default
if let w = playerCore.getProperty("width"), let h = playerCore.getProperty("height"), if let videoSize = playerCore.videoSize {
let width = Double(w), let height = Double(h), width > 0 && height > 0 aspectRatio = NSSize(width: videoSize.width, height: videoSize.height)
{
aspectRatio = NSSize(width: width, height: height)
} }
enteredPipViaAuto = !manual enteredPipViaAuto = !manual
@@ -296,15 +294,19 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
return return
} }
playerCore?.setProperty(name, value: value) guard let core = playerCore else {
result(nil)
if name == "pause" { return
let isPlaying = value == "no"
pipController?.setPlaying(isPlaying)
playerCore?.setPaused(!isPlaying)
} }
result(nil) core.setPropertyAsync(name, value: value) { [weak self] _ in
if name == "pause" {
let isPlaying = value == "no"
self?.pipController?.setPlaying(isPlaying)
core.setPaused(!isPlaying)
}
result(nil)
}
} }
// MARK: - Helpers // MARK: - Helpers
@@ -366,8 +368,11 @@ extension MpvPlayerPlugin: MpvPipDelegate {
} }
func pipSetPlaying(_ playing: Bool) { func pipSetPlaying(_ playing: Bool) {
playerCore?.setProperty("pause", value: playing ? "no" : "yes") guard let playerCore else { return }
pipController?.setPlaying(playing) playerCore.setPropertyAsync("pause", value: playing ? "no" : "yes") { [weak self] _ in
self?.pipController?.setPlaying(playing)
playerCore.setPaused(!playing)
}
} }
var isPipPlaying: Bool { !(playerCore?.isPaused ?? true) } var isPipPlaying: Bool { !(playerCore?.isPaused ?? true) }
+241 -94
View File
@@ -91,13 +91,34 @@ class MpvPlayerCoreBase: NSObject {
"pause", "eof-reached", "paused-for-cache", "pause", "eof-reached", "paused-for-cache",
] ]
private static let internalSigPeakObserverId: UInt64 = UInt64.max - 1
private static let internalWidthObserverId: UInt64 = UInt64.max - 2
private static let internalHeightObserverId: UInt64 = UInt64.max - 3
private static let internalObserverIds: Set<UInt64> = [
internalSigPeakObserverId,
internalWidthObserverId,
internalHeightObserverId,
]
let queue = DispatchQueue(label: "mpv", qos: .userInitiated) let queue = DispatchQueue(label: "mpv", qos: .userInitiated)
private let queueKey = DispatchSpecificKey<Void>() private let queueKey = DispatchSpecificKey<Void>()
private var pendingCommands: [UInt64: (Result<Void, Error>) -> Void] = [:] private enum PendingRequest {
private let pendingCommandsLock = NSLock() case void((Result<Void, Error>) -> Void)
case getProperty((Result<String?, Error>) -> Void)
}
private var pendingRequests: [UInt64: PendingRequest] = [:]
private let pendingRequestsLock = NSLock()
private var nextRequestId: UInt64 = 1 private var nextRequestId: UInt64 = 1
private let cacheLock = NSLock()
private var cachedPaused = true
private var cachedDuration = 0.0
private var cachedTimePos = 0.0
private var cachedWidth = 0.0
private var cachedHeight = 0.0
override init() { override init() {
super.init() super.init()
queue.setSpecific(key: queueKey, value: ()) queue.setSpecific(key: queueKey, value: ())
@@ -122,7 +143,7 @@ class MpvPlayerCoreBase: NSObject {
checkError(mpv_request_log_messages(mpv, "warn")) checkError(mpv_request_log_messages(mpv, "warn"))
#endif #endif
var layer = metalLayer var layer = Int64(Int(bitPattern: Unmanaged.passUnretained(metalLayer).toOpaque()))
checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &layer)) checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &layer))
applySharedMpvOptions() applySharedMpvOptions()
configurePlatformMpvOptions() configurePlatformMpvOptions()
@@ -145,7 +166,9 @@ class MpvPlayerCoreBase: NSObject {
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
) )
mpv_observe_property(mpv, 0, "video-params/sig-peak", MPV_FORMAT_DOUBLE) mpv_observe_property(mpv, Self.internalSigPeakObserverId, "video-params/sig-peak", MPV_FORMAT_DOUBLE)
mpv_observe_property(mpv, Self.internalWidthObserverId, "width", MPV_FORMAT_DOUBLE)
mpv_observe_property(mpv, Self.internalHeightObserverId, "height", MPV_FORMAT_DOUBLE)
return true return true
} }
@@ -155,35 +178,75 @@ class MpvPlayerCoreBase: NSObject {
} }
func setProperty(_ name: String, value: String) { func setProperty(_ name: String, value: String) {
guard mpv != nil else { return } setPropertyAsync(name, value: value) { _ in }
}
func setPropertyAsync(
_ name: String,
value: String,
completion: @escaping (Result<Void, Error>) -> Void
) {
if name == "pause" {
setCachedPaused(value == "yes" || value == "true" || value == "1")
}
if name == "hdr-enabled" { if name == "hdr-enabled" {
let enabled = value == "yes" || value == "true" || value == "1" let enabled = value == "yes" || value == "true" || value == "1"
setHDREnabled(enabled) setHDREnabled(enabled, completion: completion)
return return
} }
mpv_set_property_string(mpv, name, value) setRawStringPropertyAsync(name, value: value, completion: completion)
} }
func setHDREnabled(_ enabled: Bool) { func setInt64PropertyAsync(
_ name: String,
value: Int64,
completion: @escaping (Result<Void, Error>) -> Void
) {
guard let mpv else {
completion(.success(()))
return
}
let requestId = registerRequest(.void(completion))
var propertyValue = value
let status = name.withCString { namePointer in
mpv_set_property_async(mpv, requestId, namePointer, MPV_FORMAT_INT64, &propertyValue)
}
completeRequestIfSubmissionFailed(requestId: requestId, status: status)
}
func setHDREnabled(_ enabled: Bool, completion: ((Result<Void, Error>) -> Void)? = nil) {
cacheLock.lock()
hdrEnabled = enabled hdrEnabled = enabled
let sigPeak = lastSigPeak
cacheLock.unlock()
print("[MpvPlayerCore] HDR enabled: \(enabled)") print("[MpvPlayerCore] HDR enabled: \(enabled)")
if mpv != nil { setRawStringPropertyAsync(
mpv_set_property_string(mpv, "target-colorspace-hint", enabled ? "yes" : "no") "target-colorspace-hint",
} value: enabled ? "yes" : "no",
completion: completion ?? { _ in }
)
DispatchQueue.main.async { DispatchQueue.main.async {
self.updateEDRMode(sigPeak: self.lastSigPeak) self.updateEDRMode(sigPeak: sigPeak)
} }
} }
func getProperty(_ name: String) -> String? { func getPropertyAsync(_ name: String, completion: @escaping (Result<String?, Error>) -> Void) {
guard mpv != nil else { return nil } guard let mpv else {
let cstr = mpv_get_property_string(mpv, name) completion(.success(nil))
defer { mpv_free(cstr) } return
return cstr.map { safeString($0) } }
let requestId = registerRequest(.getProperty(completion))
let status = name.withCString { namePointer in
mpv_get_property_async(mpv, requestId, namePointer, MPV_FORMAT_STRING)
}
completeRequestIfSubmissionFailed(requestId: requestId, status: status)
} }
func observeProperty(_ name: String, format: String) { func observeProperty(_ name: String, format: String) {
@@ -207,8 +270,7 @@ class MpvPlayerCoreBase: NSObject {
} }
func command(_ args: [String]) { func command(_ args: [String]) {
guard mpv != nil, !args.isEmpty else { return } commandAsync(args) { _ in }
command(args[0], args: Array(args.dropFirst()))
} }
func commandAsync(_ args: [String], completion: @escaping (Result<Void, Error>) -> Void) { func commandAsync(_ args: [String], completion: @escaping (Result<Void, Error>) -> Void) {
@@ -217,11 +279,7 @@ class MpvPlayerCoreBase: NSObject {
return return
} }
pendingCommandsLock.lock() let requestId = registerRequest(.void(completion))
let requestId = nextRequestId
nextRequestId += 1
pendingCommands[requestId] = completion
pendingCommandsLock.unlock()
var cargs: [UnsafeMutablePointer<CChar>?] = args.map { strdup($0) } var cargs: [UnsafeMutablePointer<CChar>?] = args.map { strdup($0) }
cargs.append(nil) cargs.append(nil)
@@ -229,21 +287,7 @@ class MpvPlayerCoreBase: NSObject {
cargs.withUnsafeBufferPointer { buffer in cargs.withUnsafeBufferPointer { buffer in
var constPointers = buffer.map { UnsafePointer($0) } var constPointers = buffer.map { UnsafePointer($0) }
let result = mpv_command_async(mpv, requestId, &constPointers) let result = mpv_command_async(mpv, requestId, &constPointers)
if result < 0 { completeRequestIfSubmissionFailed(requestId: requestId, status: result)
pendingCommandsLock.lock()
let pending = pendingCommands.removeValue(forKey: requestId)
pendingCommandsLock.unlock()
guard let pending else { return }
let error = NSError(
domain: "mpv",
code: Int(result),
userInfo: [NSLocalizedDescriptionKey: safeString(mpv_error_string(result))]
)
DispatchQueue.main.async {
pending(.failure(error))
}
}
} }
for pointer in cargs { for pointer in cargs {
@@ -251,30 +295,54 @@ class MpvPlayerCoreBase: NSObject {
} }
} }
private func setRawStringPropertyAsync(
_ name: String,
value: String,
completion: @escaping (Result<Void, Error>) -> Void
) {
guard let mpv else {
completion(.success(()))
return
}
let requestId = registerRequest(.void(completion))
let status = name.withCString { namePointer in
value.withCString { valuePointer in
var propertyValue: UnsafePointer<CChar>? = valuePointer
return mpv_set_property_async(mpv, requestId, namePointer, MPV_FORMAT_STRING, &propertyValue)
}
}
completeRequestIfSubmissionFailed(requestId: requestId, status: status)
}
var isPaused: Bool { var isPaused: Bool {
guard let mpv else { return true } cacheLock.lock()
var flag: Int32 = 0 defer { cacheLock.unlock() }
mpv_get_property(mpv, "pause", MPV_FORMAT_FLAG, &flag) return cachedPaused
return flag != 0
} }
var duration: Double { var duration: Double {
guard let mpv else { return 0 } cacheLock.lock()
var value: Double = 0 defer { cacheLock.unlock() }
mpv_get_property(mpv, "duration", MPV_FORMAT_DOUBLE, &value) return cachedDuration
return value
} }
var timePos: Double { var timePos: Double {
guard let mpv else { return 0 } cacheLock.lock()
var value: Double = 0 defer { cacheLock.unlock() }
mpv_get_property(mpv, "time-pos", MPV_FORMAT_DOUBLE, &value) return cachedTimePos
return value }
var videoSize: CGSize? {
cacheLock.lock()
defer { cacheLock.unlock() }
guard cachedWidth > 0, cachedHeight > 0 else { return nil }
return CGSize(width: cachedWidth, height: cachedHeight)
} }
func disposeSharedState(destroySynchronously: Bool) { func disposeSharedState(destroySynchronously: Bool) {
isDisposing = true isDisposing = true
cancelPendingCommands() cancelPendingRequests()
let mpvHandle = mpv let mpvHandle = mpv
mpv = nil mpv = nil
@@ -299,9 +367,9 @@ class MpvPlayerCoreBase: NSObject {
func applyGpuNextOptions() { func applyGpuNextOptions() {
guard mpv != nil else { return } guard mpv != nil else { return }
mpv_set_property_string(mpv, "gpu-api", "vulkan") setProperty("gpu-api", value: "vulkan")
mpv_set_property_string(mpv, "gpu-context", "moltenvk") setProperty("gpu-context", value: "moltenvk")
mpv_set_property_string(mpv, "vo", "gpu-next") setProperty("vo", value: "gpu-next")
} }
private func applySharedMpvOptions() { private func applySharedMpvOptions() {
@@ -313,38 +381,99 @@ class MpvPlayerCoreBase: NSObject {
checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "yes")) checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "yes"))
} }
private func cancelPendingCommands() { private func cancelPendingRequests() {
pendingCommandsLock.lock() pendingRequestsLock.lock()
let pending = pendingCommands let pending = pendingRequests
pendingCommands.removeAll() pendingRequests.removeAll()
pendingCommandsLock.unlock() pendingRequestsLock.unlock()
let error = NSError( let error = NSError(
domain: "mpv", domain: "mpv",
code: -1, code: -1,
userInfo: [NSLocalizedDescriptionKey: "Player disposed"] userInfo: [NSLocalizedDescriptionKey: "Player disposed"]
) )
for (_, completion) in pending { for (_, request) in pending {
DispatchQueue.main.async { DispatchQueue.main.async {
switch request {
case .void(let completion):
completion(.failure(error))
case .getProperty(let completion):
completion(.failure(error))
}
}
}
}
private func registerRequest(_ request: PendingRequest) -> UInt64 {
pendingRequestsLock.lock()
defer { pendingRequestsLock.unlock() }
let requestId = nextRequestId
nextRequestId += 1
pendingRequests[requestId] = request
return requestId
}
private func takeRequest(_ requestId: UInt64) -> PendingRequest? {
pendingRequestsLock.lock()
defer { pendingRequestsLock.unlock() }
return pendingRequests.removeValue(forKey: requestId)
}
private func mpvError(_ status: CInt) -> NSError {
NSError(
domain: "mpv",
code: Int(status),
userInfo: [NSLocalizedDescriptionKey: safeString(mpv_error_string(status))]
)
}
private func completeRequestIfSubmissionFailed(requestId: UInt64, status: CInt) {
guard status < 0, let request = takeRequest(requestId) else { return }
let error = mpvError(status)
DispatchQueue.main.async {
switch request {
case .void(let completion):
completion(.failure(error))
case .getProperty(let completion):
completion(.failure(error)) completion(.failure(error))
} }
} }
} }
private func command(_ command: String, args: [String] = []) { private func completeVoidRequest(requestId: UInt64, error status: CInt) {
guard mpv != nil else { return } guard let request = takeRequest(requestId) else { return }
DispatchQueue.main.async {
switch request {
case .void(let completion):
if status < 0 {
completion(.failure(self.mpvError(status)))
} else {
completion(.success(()))
}
case .getProperty:
break
}
}
}
var cargs: [UnsafeMutablePointer<CChar>?] = ([command] + args).map { strdup($0) } private func completeGetPropertyRequest(_ event: mpv_event) {
cargs.append(nil) guard let request = takeRequest(event.reply_userdata) else { return }
defer { guard case .getProperty(let completion) = request else { return }
for pointer in cargs {
free(pointer) var value: String?
if event.error >= 0,
let propertyPointer = event.data?.assumingMemoryBound(to: mpv_event_property.self)
{
let property = propertyPointer.pointee
if property.format == MPV_FORMAT_STRING, let data = property.data {
let cstring = data.assumingMemoryBound(to: UnsafePointer<CChar>?.self).pointee
value = cstring.map { safeString($0) }
} }
} }
cargs.withUnsafeBufferPointer { buffer in DispatchQueue.main.async {
var constPointers = buffer.map { UnsafePointer($0) } completion(.success(value))
_ = mpv_command(mpv, &constPointers)
} }
} }
@@ -371,29 +500,16 @@ class MpvPlayerCoreBase: NSObject {
guard let data = event.data else { break } guard let data = event.data else { break }
let property = data.assumingMemoryBound(to: mpv_event_property.self).pointee let property = data.assumingMemoryBound(to: mpv_event_property.self).pointee
let name = safeString(property.name) let name = safeString(property.name)
handlePropertyChange(name: name, property: property) handlePropertyChange(name: name, property: property, replyUserdata: event.reply_userdata)
case MPV_EVENT_COMMAND_REPLY: case MPV_EVENT_COMMAND_REPLY:
let requestId = event.reply_userdata completeVoidRequest(requestId: event.reply_userdata, error: event.error)
pendingCommandsLock.lock()
let completion = pendingCommands.removeValue(forKey: requestId)
pendingCommandsLock.unlock()
guard let completion else { break } case MPV_EVENT_SET_PROPERTY_REPLY:
if event.error < 0 { completeVoidRequest(requestId: event.reply_userdata, error: event.error)
let error = NSError(
domain: "mpv", case MPV_EVENT_GET_PROPERTY_REPLY:
code: Int(event.error), completeGetPropertyRequest(event)
userInfo: [NSLocalizedDescriptionKey: safeString(mpv_error_string(event.error))]
)
DispatchQueue.main.async {
completion(.failure(error))
}
} else {
DispatchQueue.main.async {
completion(.success(()))
}
}
case MPV_EVENT_FILE_LOADED: case MPV_EVENT_FILE_LOADED:
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -446,9 +562,7 @@ class MpvPlayerCoreBase: NSObject {
} }
} }
private func handlePropertyChange(name: String, property: mpv_event_property) { private func handlePropertyChange(name: String, property: mpv_event_property, replyUserdata: UInt64) {
if isBackgrounded && !Self.criticalProperties.contains(name) { return }
var value: Any? var value: Any?
switch property.format { switch property.format {
@@ -478,18 +592,51 @@ class MpvPlayerCoreBase: NSObject {
break break
} }
updateCachedProperty(name: name, value: value)
if name == "video-params/sig-peak", let sigPeak = value as? Double { if name == "video-params/sig-peak", let sigPeak = value as? Double {
cacheLock.lock()
lastSigPeak = sigPeak lastSigPeak = sigPeak
cacheLock.unlock()
DispatchQueue.main.async { DispatchQueue.main.async {
self.updateEDRMode(sigPeak: sigPeak) self.updateEDRMode(sigPeak: sigPeak)
} }
} }
if Self.internalObserverIds.contains(replyUserdata) { return }
if isBackgrounded && !Self.criticalProperties.contains(name) { return }
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.onPropertyChange(name: name, value: value) self.delegate?.onPropertyChange(name: name, value: value)
} }
} }
private func updateCachedProperty(name: String, value: Any?) {
cacheLock.lock()
defer { cacheLock.unlock() }
switch name {
case "pause":
if let paused = value as? Bool { cachedPaused = paused }
case "duration":
if let duration = value as? Double { cachedDuration = duration }
case "time-pos":
if let timePos = value as? Double { cachedTimePos = timePos }
case "width":
if let width = value as? Double { cachedWidth = width }
case "height":
if let height = value as? Double { cachedHeight = height }
default:
break
}
}
private func setCachedPaused(_ paused: Bool) {
cacheLock.lock()
cachedPaused = paused
cacheLock.unlock()
}
private func convertNode(_ node: mpv_node) -> Any? { private func convertNode(_ node: mpv_node) -> Any? {
switch node.format { switch node.format {
case MPV_FORMAT_STRING: case MPV_FORMAT_STRING:
@@ -27,7 +27,14 @@ extension MpvPluginShared {
) )
return return
} }
result(coreBase?.getProperty(name)) coreBase?.getPropertyAsync(name) { propertyResult in
switch propertyResult {
case .success(let value):
result(value)
case .failure:
result(nil)
}
} ?? result(nil)
} }
func handleObserveProperty(call: FlutterMethodCall, result: @escaping FlutterResult) { func handleObserveProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
+112 -60
View File
@@ -82,13 +82,25 @@ bool MpvPlayer::Initialize(HWND container, HWND flutter_window) {
void MpvPlayer::Dispose() { void MpvPlayer::Dispose() {
StopEventLoop(); 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_); std::lock_guard<std::mutex> lock(pending_requests_mutex_);
for (auto& pair : pending_commands_) { for (auto& pair : pending_status_requests_) {
if (pair.second) pair.second(-1); // Call with error 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_) { if (mpv_) {
@@ -104,18 +116,7 @@ void MpvPlayer::Dispose() {
observed_properties_.clear(); observed_properties_.clear();
} }
void MpvPlayer::Command(const std::vector<std::string>& args) { void MpvPlayer::Command(const std::vector<std::string>& args) { CommandAsync(args, nullptr); }
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::CommandAsync(const std::vector<std::string>& args, CommandCallback callback) { void MpvPlayer::CommandAsync(const std::vector<std::string>& args, CommandCallback callback) {
if (!mpv_) { if (!mpv_) {
@@ -130,50 +131,88 @@ void MpvPlayer::CommandAsync(const std::vector<std::string>& args, CommandCallba
} }
c_args.push_back(nullptr); c_args.push_back(nullptr);
// Generate unique request ID and store callback uint64_t request_id = callback ? RegisterStatusRequest(std::move(callback)) : 0;
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);
}
// mpv_command_async returns immediately // mpv_command_async returns immediately
int result = mpv_command_async(mpv_, request_id, c_args.data()); int result = mpv_command_async(mpv_, request_id, c_args.data());
if (result < 0) { if (result < 0) {
// Submission failed, complete immediately with error auto cb = TakeStatusRequest(request_id);
std::lock_guard<std::mutex> lock(pending_commands_mutex_); if (cb) cb(result);
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);
}
} }
} }
void MpvPlayer::SetProperty(const std::string& name, const std::string& value) { 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) // Handle custom HDR toggle property (same pattern as iOS/macOS)
if (name == "hdr-enabled") { if (name == "hdr-enabled") {
bool enabled = (value == "yes" || value == "true" || value == "1"); bool enabled = (value == "yes" || value == "true" || value == "1");
SetHDREnabled(enabled); SetHDREnabled(enabled, std::move(callback));
return; 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) { void MpvPlayer::GetPropertyAsync(const std::string& name, GetPropertyCallback callback) {
if (!mpv_) return ""; if (!mpv_) {
if (callback) callback(-1, "");
return;
}
char* value = mpv_get_property_string(mpv_, name.c_str()); uint64_t request_id = RegisterGetPropertyRequest(std::move(callback));
if (!value) return "";
std::string result(value); int result = mpv_get_property_async(mpv_, request_id, name.c_str(), MPV_FORMAT_STRING);
mpv_free(value); if (result < 0) {
return result; 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) { 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) { void MpvPlayer::HandleMpvEvent(mpv_event* event) {
switch (event->event_id) { switch (event->event_id) {
case MPV_EVENT_COMMAND_REPLY: { case MPV_EVENT_COMMAND_REPLY:
// Handle async command completion case MPV_EVENT_SET_PROPERTY_REPLY: {
uint64_t request_id = event->reply_userdata; uint64_t request_id = event->reply_userdata;
CommandCallback callback; StatusCallback callback = TakeStatusRequest(request_id);
{
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);
}
}
if (callback) { if (callback) {
callback(event->error); callback(event->error);
} }
break; 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: { case MPV_EVENT_LOG_MESSAGE: {
auto* msg = static_cast<mpv_event_log_message*>(event->data); auto* msg = static_cast<mpv_event_log_message*>(event->data);
char log_msg[512]; 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 // to null output (e.g. after sleep/wake or device unplug), re-set
// audio-device to switch back to the real output. // audio-device to switch back to the real output.
// Mirrors mpv's TOOLS/lua/ao-null-reload.lua for embedded libmpv. // Mirrors mpv's TOOLS/lua/ao-null-reload.lua for embedded libmpv.
if (strcmp(prop->name, "audio-device-list") == 0 && GetProperty("current-ao") == "null") { if (strcmp(prop->name, "audio-device-list") == 0) {
auto device = GetProperty("audio-device"); GetPropertyAsync("current-ao", [this](int ao_error, const std::string& current_ao) {
if (!device.empty()) { if (ao_error < 0 || current_ao != "null") return;
mpv_set_property_string(mpv_, "audio-device", device.c_str()); 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); 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; hdr_enabled_ = enabled;
if (mpv_) { 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_); UpdateHDRMode(last_sig_peak_);
+20 -11
View File
@@ -34,21 +34,25 @@ class MpvPlayer {
// Returns true if mpv is initialized. // Returns true if mpv is initialized.
bool IsInitialized() const { return mpv_ != nullptr; } 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); void Command(const std::vector<std::string>& args);
// Callback type for async command completion. // Callback types for async mpv requests.
using CommandCallback = std::function<void(int error)>; 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. // 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); 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); void SetProperty(const std::string& name, const std::string& value);
// Gets an mpv property. // Sets an mpv property asynchronously.
std::string GetProperty(const std::string& name); 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. // Observes an mpv property for changes.
void ObserveProperty(const std::string& name, const std::string& format, int id); void ObserveProperty(const std::string& name, const std::string& format, int id);
@@ -75,6 +79,10 @@ class MpvPlayer {
void HandleMpvEvent(mpv_event* event); void HandleMpvEvent(mpv_event* event);
void SendPropertyChange(const char* name, mpv_node* data); void SendPropertyChange(const char* name, mpv_node* data);
void SendEvent(const std::string& name, const flutter::EncodableMap& 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; mpv_handle* mpv_ = nullptr;
HWND hwnd_ = nullptr; HWND hwnd_ = nullptr;
@@ -92,16 +100,17 @@ class MpvPlayer {
std::map<std::string, uint64_t> observed_properties_; std::map<std::string, uint64_t> observed_properties_;
std::map<std::string, int> name_to_id_; std::map<std::string, int> name_to_id_;
// Pending async commands: request_id -> callback // Pending async requests: request_id -> callback
std::map<uint64_t, CommandCallback> pending_commands_; std::map<uint64_t, StatusCallback> pending_status_requests_;
std::mutex pending_commands_mutex_; std::map<uint64_t, GetPropertyCallback> pending_get_property_requests_;
std::mutex pending_requests_mutex_;
// HDR state // HDR state
bool hdr_enabled_ = true; // User preference bool hdr_enabled_ = true; // User preference
double last_sig_peak_ = 0.0; // Last known sig-peak for HDR content detection double last_sig_peak_ = 0.0; // Last known sig-peak for HDR content detection
// HDR methods // HDR methods
void SetHDREnabled(bool enabled); void SetHDREnabled(bool enabled, StatusCallback callback = nullptr);
void UpdateHDRMode(double sigPeak); void UpdateHDRMode(double sigPeak);
}; };
+72 -17
View File
@@ -18,12 +18,17 @@ void MpvPlayerPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef regis
namespace mpv { namespace mpv {
namespace {
constexpr UINT kPlatformTaskMessage = WM_APP + 0x4D50;
}
void MpvPlayerPlugin::RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar) { void MpvPlayerPlugin::RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar) {
auto plugin = std::make_unique<MpvPlayerPlugin>(registrar); auto plugin = std::make_unique<MpvPlayerPlugin>(registrar);
registrar->AddPlugin(std::move(plugin)); 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. // Create method channel.
method_channel_ = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>( method_channel_ = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
registrar->messenger(), "com.plezy/mpv_player", &flutter::StandardMethodCodec::GetInstance()); registrar->messenger(), "com.plezy/mpv_player", &flutter::StandardMethodCodec::GetInstance());
@@ -53,6 +58,8 @@ MpvPlayerPlugin::MpvPlayerPlugin(flutter::PluginRegistrarWindows* registrar) : r
} }
MpvPlayerPlugin::~MpvPlayerPlugin() { MpvPlayerPlugin::~MpvPlayerPlugin() {
DrainPlatformTasks();
// Unregister window proc delegate. // Unregister window proc delegate.
if (proc_id_) { if (proc_id_) {
registrar_->UnregisterTopLevelWindowProcDelegate(proc_id_.value()); registrar_->UnregisterTopLevelWindowProcDelegate(proc_id_.value());
@@ -64,6 +71,35 @@ HWND MpvPlayerPlugin::GetChildWindow() { return registrar_->GetView()->GetNative
HWND MpvPlayerPlugin::GetWindow() { return ::GetAncestor(GetChildWindow(), GA_ROOT); } 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( void MpvPlayerPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call, const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) { std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
@@ -77,11 +113,17 @@ void MpvPlayerPlugin::HandleMethodCall(
} }
HWND flutter_window = GetWindow(); HWND flutter_window = GetWindow();
flutter_window_ = flutter_window;
MpvCore::SetInstance(std::make_unique<MpvCore>(flutter_window)); MpvCore::SetInstance(std::make_unique<MpvCore>(flutter_window));
proc_id_ = 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(); auto* core = MpvCore::GetInstance();
if (core) { if (core) {
return core->WindowProc(hwnd, message, wparam, lparam); return core->WindowProc(hwnd, message, wparam, lparam);
@@ -160,13 +202,15 @@ void MpvPlayerPlugin::HandleMethodCall(
auto result_ptr = auto result_ptr =
std::make_shared<std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>>(std::move(result)); std::make_shared<std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>>(std::move(result));
std::string cmd_name = command_args.empty() ? "unknown" : command_args[0]; std::string cmd_name = command_args.empty() ? "unknown" : command_args[0];
player_->CommandAsync(command_args, [result_ptr, cmd_name](int error) { player_->CommandAsync(command_args, [this, result_ptr, cmd_name](int error) {
if (error < 0) { PostToPlatformThread([result_ptr, cmd_name, error]() {
(*result_ptr) if (error < 0) {
->Error("COMMAND_FAILED", "MPV command failed: " + cmd_name + " (error " + std::to_string(error) + ")"); (*result_ptr)
} else { ->Error("COMMAND_FAILED", "MPV command failed: " + cmd_name + " (error " + std::to_string(error) + ")");
(*result_ptr)->Success(); } else {
} (*result_ptr)->Success();
}
});
}); });
return; // Response will be sent asynchronously return; // Response will be sent asynchronously
} else if (method == "setProperty") { } else if (method == "setProperty") {
@@ -194,8 +238,12 @@ void MpvPlayerPlugin::HandleMethodCall(
return; return;
} }
player_->SetProperty(std::get<std::string>(name_it->second), std::get<std::string>(value_it->second)); auto result_ptr =
result->Success(); 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") { } else if (method == "setLogLevel") {
if (!player_ || !player_->IsInitialized()) { if (!player_ || !player_->IsInitialized()) {
result->Error("NOT_INITIALIZED", "Player not initialized"); result->Error("NOT_INITIALIZED", "Player not initialized");
@@ -238,12 +286,19 @@ void MpvPlayerPlugin::HandleMethodCall(
return; return;
} }
std::string value = player_->GetProperty(std::get<std::string>(name_it->second)); auto result_ptr =
if (value.empty()) { std::make_shared<std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>>(std::move(result));
result->Success(); player_->GetPropertyAsync(
} else { std::get<std::string>(name_it->second), [this, result_ptr](int error, const std::string& value) {
result->Success(flutter::EncodableValue(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") { } else if (method == "observeProperty") {
if (!player_ || !player_->IsInitialized()) { if (!player_ || !player_->IsInitialized()) {
result->Error("NOT_INITIALIZED", "Player not initialized"); result->Error("NOT_INITIALIZED", "Player not initialized");
+9
View File
@@ -8,8 +8,11 @@
#include <flutter/plugin_registrar_windows.h> #include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h> #include <flutter/standard_method_codec.h>
#include <functional>
#include <memory> #include <memory>
#include <mutex>
#include <optional> #include <optional>
#include <queue>
#include "display_mode_manager.h" #include "display_mode_manager.h"
#include "mpv_core.h" #include "mpv_core.h"
@@ -33,11 +36,15 @@ class MpvPlayerPlugin : public flutter::Plugin {
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result); std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
void SendEvent(const flutter::EncodableValue& event); void SendEvent(const flutter::EncodableValue& event);
void PostToPlatformThread(std::function<void()> task);
void DrainPlatformTasks();
HWND GetWindow(); HWND GetWindow();
HWND GetChildWindow(); HWND GetChildWindow();
flutter::PluginRegistrarWindows* registrar_; flutter::PluginRegistrarWindows* registrar_;
DWORD platform_thread_id_;
HWND flutter_window_ = nullptr;
std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> method_channel_; std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> method_channel_;
std::unique_ptr<flutter::EventChannel<flutter::EncodableValue>> event_channel_; std::unique_ptr<flutter::EventChannel<flutter::EncodableValue>> event_channel_;
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>> event_sink_; std::unique_ptr<flutter::EventSink<flutter::EncodableValue>> event_sink_;
@@ -45,6 +52,8 @@ class MpvPlayerPlugin : public flutter::Plugin {
std::unique_ptr<MpvPlayer> player_; std::unique_ptr<MpvPlayer> player_;
DisplayModeManager display_mode_manager_; DisplayModeManager display_mode_manager_;
std::optional<int32_t> proc_id_; std::optional<int32_t> proc_id_;
std::mutex platform_tasks_mutex_;
std::queue<std::function<void()>> platform_tasks_;
}; };
} // namespace mpv } // namespace mpv