fix(mpv): avoid blocking runtime calls
This commit is contained in:
@@ -50,36 +50,31 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
metalLayer?.removeFromSuperlayer()
|
||||
|
||||
mpv_set_property_string(mpv, "vid", "no")
|
||||
|
||||
var pointer = Int64(Int(bitPattern: layerPtr))
|
||||
mpv_set_property(mpv, "wid", MPV_FORMAT_INT64, &pointer)
|
||||
|
||||
mpv_set_property_string(mpv, "vo", "pip")
|
||||
mpv_set_property_string(mpv, "vid", "auto")
|
||||
setProperty("vid", value: "no")
|
||||
setInt64PropertyAsync("wid", value: Int64(Int(bitPattern: layerPtr))) { _ in }
|
||||
setProperty("vo", value: "pip")
|
||||
setProperty("vid", value: "auto")
|
||||
|
||||
print("[MpvPlayerCore] Switched to pip VO successfully")
|
||||
return true
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
mpv_set_property_string(mpv, "vid", "no")
|
||||
|
||||
var layer = metalLayer
|
||||
mpv_set_property(mpv, "wid", MPV_FORMAT_INT64, &layer)
|
||||
|
||||
setProperty("vid", value: "no")
|
||||
setInt64PropertyAsync("wid", value: Int64(Int(bitPattern: Unmanaged.passUnretained(metalLayer).toOpaque()))) { _ in
|
||||
}
|
||||
applyGpuNextOptions()
|
||||
mpv_set_property_string(mpv, "vid", "auto")
|
||||
setProperty("vid", value: "auto")
|
||||
|
||||
if metalLayer.superlayer == nil, let containerView {
|
||||
containerView.layer.addSublayer(metalLayer)
|
||||
@@ -179,9 +174,7 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
}
|
||||
|
||||
print("[MpvPlayerCore] Entering background - disabling video")
|
||||
if mpv != nil {
|
||||
mpv_set_option_string(mpv, "vid", "no")
|
||||
}
|
||||
setProperty("vid", value: "no")
|
||||
}
|
||||
|
||||
@objc private func enterForeground() {
|
||||
@@ -191,8 +184,6 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
}
|
||||
|
||||
print("[MpvPlayerCore] Entering foreground - enabling video")
|
||||
if mpv != nil {
|
||||
mpv_set_option_string(mpv, "vid", "auto")
|
||||
}
|
||||
setProperty("vid", value: "auto")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,12 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
||||
stopPipTimebaseSync()
|
||||
pipController?.flushLayer()
|
||||
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
|
||||
if pendingInlineRestoreAfterPip {
|
||||
if isSceneActive {
|
||||
@@ -340,14 +345,18 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
||||
return
|
||||
}
|
||||
|
||||
playerCore?.setProperty(name, value: value)
|
||||
|
||||
if name == "pause" {
|
||||
pipController?.invalidatePlaybackState()
|
||||
if playerCore?.isPipActive == true { syncPipTimebase() }
|
||||
guard let core = playerCore else {
|
||||
result(nil)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
@@ -400,9 +409,10 @@ extension MpvPlayerPlugin: MpvPipDelegate {
|
||||
}
|
||||
|
||||
func pipSetPlaying(_ playing: Bool) {
|
||||
playerCore?.setProperty("pause", value: playing ? "no" : "yes")
|
||||
pipController?.invalidatePlaybackState()
|
||||
syncPipTimebase()
|
||||
playerCore?.setPropertyAsync("pause", value: playing ? "no" : "yes") { [weak self] _ in
|
||||
self?.pipController?.invalidatePlaybackState()
|
||||
self?.syncPipTimebase()
|
||||
}
|
||||
}
|
||||
|
||||
func pipSkip(byInterval seconds: Double) {
|
||||
|
||||
+109
-49
@@ -202,13 +202,25 @@ void MpvPlayer::Dispose() {
|
||||
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_);
|
||||
for (auto& pair : pending_commands_) {
|
||||
if (pair.second) pair.second(-1);
|
||||
std::lock_guard<std::mutex> request_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, "");
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
void MpvPlayer::Command(const std::vector<std::string>& args) {
|
||||
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::Command(const std::vector<std::string>& args) { CommandAsync(args, nullptr); }
|
||||
|
||||
void MpvPlayer::CommandAsync(const std::vector<std::string>& args, CommandCallback callback) {
|
||||
if (disposed_ || !mpv_) {
|
||||
@@ -297,39 +298,80 @@ void MpvPlayer::CommandAsync(const std::vector<std::string>& args, CommandCallba
|
||||
}
|
||||
c_args.push_back(nullptr);
|
||||
|
||||
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;
|
||||
|
||||
int result = mpv_command_async(mpv_, request_id, c_args.data());
|
||||
if (result < 0) {
|
||||
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 (disposed_ || !mpv_) return;
|
||||
mpv_set_property_string(mpv_, name.c_str(), value.c_str());
|
||||
SetPropertyAsync(name, value, nullptr);
|
||||
}
|
||||
|
||||
std::string MpvPlayer::GetProperty(const std::string& name) {
|
||||
if (disposed_ || !mpv_) return "";
|
||||
void MpvPlayer::SetPropertyAsync(const std::string& name, const std::string& value, StatusCallback callback) {
|
||||
if (disposed_ || !mpv_) {
|
||||
if (callback) callback(0);
|
||||
return;
|
||||
}
|
||||
|
||||
char* value = mpv_get_property_string(mpv_, name.c_str());
|
||||
if (!value) return "";
|
||||
uint64_t request_id = callback ? RegisterStatusRequest(std::move(callback)) : 0;
|
||||
|
||||
std::string result(value);
|
||||
mpv_free(value);
|
||||
return result;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -446,17 +488,10 @@ bool MpvPlayer::ProcessEvents() {
|
||||
|
||||
void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
||||
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;
|
||||
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) {
|
||||
int error = event->error;
|
||||
g_idle_add(
|
||||
@@ -470,6 +505,31 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
||||
}
|
||||
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: {
|
||||
auto* msg = static_cast<mpv_event_log_message*>(event->data);
|
||||
g_message("MPV [%s] %s: %s", msg->level, msg->prefix, msg->text);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
// Forward declaration for Flutter types
|
||||
@@ -68,11 +69,13 @@ class MpvPlayer {
|
||||
/// Returns true if mpv handle exists (even without render context).
|
||||
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);
|
||||
|
||||
/// 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.
|
||||
void CommandAsync(const std::vector<std::string>& args, CommandCallback callback);
|
||||
@@ -80,8 +83,11 @@ class MpvPlayer {
|
||||
/// Sets an mpv property by name.
|
||||
void SetProperty(const std::string& name, const std::string& value);
|
||||
|
||||
/// Gets an mpv property value by name.
|
||||
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 value 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);
|
||||
@@ -126,6 +132,11 @@ class MpvPlayer {
|
||||
/// Sends an event notification.
|
||||
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.
|
||||
::_FlValue* NodeToFlValue(mpv_node* node);
|
||||
|
||||
@@ -146,9 +157,10 @@ 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_;
|
||||
|
||||
// GSource for processing events on main thread
|
||||
guint event_source_id_ = 0;
|
||||
|
||||
@@ -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) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'value'", nullptr));
|
||||
} else {
|
||||
self->player->SetProperty(fl_value_get_string(name_value), fl_value_get_string(value_value));
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
g_object_ref(method_call);
|
||||
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) {
|
||||
@@ -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) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'name'", nullptr));
|
||||
} else {
|
||||
std::string value = self->player->GetProperty(fl_value_get_string(name_value));
|
||||
if (value.empty()) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
} else {
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_string(value.c_str())));
|
||||
}
|
||||
g_object_ref(method_call);
|
||||
self->player->GetPropertyAsync(
|
||||
fl_value_get_string(name_value), [method_call](int error, const std::string& value) {
|
||||
g_autoptr(FlMethodResponse) async_response = nullptr;
|
||||
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) {
|
||||
|
||||
@@ -184,10 +184,8 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
||||
|
||||
// Get video dimensions for aspect ratio
|
||||
var aspectRatio = NSSize(width: 16, height: 9) // default
|
||||
if let w = playerCore.getProperty("width"), let h = playerCore.getProperty("height"),
|
||||
let width = Double(w), let height = Double(h), width > 0 && height > 0
|
||||
{
|
||||
aspectRatio = NSSize(width: width, height: height)
|
||||
if let videoSize = playerCore.videoSize {
|
||||
aspectRatio = NSSize(width: videoSize.width, height: videoSize.height)
|
||||
}
|
||||
|
||||
enteredPipViaAuto = !manual
|
||||
@@ -296,15 +294,19 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
||||
return
|
||||
}
|
||||
|
||||
playerCore?.setProperty(name, value: value)
|
||||
|
||||
if name == "pause" {
|
||||
let isPlaying = value == "no"
|
||||
pipController?.setPlaying(isPlaying)
|
||||
playerCore?.setPaused(!isPlaying)
|
||||
guard let core = playerCore else {
|
||||
result(nil)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
@@ -366,8 +368,11 @@ extension MpvPlayerPlugin: MpvPipDelegate {
|
||||
}
|
||||
|
||||
func pipSetPlaying(_ playing: Bool) {
|
||||
playerCore?.setProperty("pause", value: playing ? "no" : "yes")
|
||||
pipController?.setPlaying(playing)
|
||||
guard let playerCore else { return }
|
||||
playerCore.setPropertyAsync("pause", value: playing ? "no" : "yes") { [weak self] _ in
|
||||
self?.pipController?.setPlaying(playing)
|
||||
playerCore.setPaused(!playing)
|
||||
}
|
||||
}
|
||||
|
||||
var isPipPlaying: Bool { !(playerCore?.isPaused ?? true) }
|
||||
|
||||
@@ -91,13 +91,34 @@ class MpvPlayerCoreBase: NSObject {
|
||||
"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)
|
||||
private let queueKey = DispatchSpecificKey<Void>()
|
||||
|
||||
private var pendingCommands: [UInt64: (Result<Void, Error>) -> Void] = [:]
|
||||
private let pendingCommandsLock = NSLock()
|
||||
private enum PendingRequest {
|
||||
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 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() {
|
||||
super.init()
|
||||
queue.setSpecific(key: queueKey, value: ())
|
||||
@@ -122,7 +143,7 @@ class MpvPlayerCoreBase: NSObject {
|
||||
checkError(mpv_request_log_messages(mpv, "warn"))
|
||||
#endif
|
||||
|
||||
var layer = metalLayer
|
||||
var layer = Int64(Int(bitPattern: Unmanaged.passUnretained(metalLayer).toOpaque()))
|
||||
checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &layer))
|
||||
applySharedMpvOptions()
|
||||
configurePlatformMpvOptions()
|
||||
@@ -145,7 +166,9 @@ class MpvPlayerCoreBase: NSObject {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -155,35 +178,75 @@ class MpvPlayerCoreBase: NSObject {
|
||||
}
|
||||
|
||||
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" {
|
||||
let enabled = value == "yes" || value == "true" || value == "1"
|
||||
setHDREnabled(enabled)
|
||||
setHDREnabled(enabled, completion: completion)
|
||||
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
|
||||
let sigPeak = lastSigPeak
|
||||
cacheLock.unlock()
|
||||
|
||||
print("[MpvPlayerCore] HDR enabled: \(enabled)")
|
||||
|
||||
if mpv != nil {
|
||||
mpv_set_property_string(mpv, "target-colorspace-hint", enabled ? "yes" : "no")
|
||||
}
|
||||
setRawStringPropertyAsync(
|
||||
"target-colorspace-hint",
|
||||
value: enabled ? "yes" : "no",
|
||||
completion: completion ?? { _ in }
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.updateEDRMode(sigPeak: self.lastSigPeak)
|
||||
self.updateEDRMode(sigPeak: sigPeak)
|
||||
}
|
||||
}
|
||||
|
||||
func getProperty(_ name: String) -> String? {
|
||||
guard mpv != nil else { return nil }
|
||||
let cstr = mpv_get_property_string(mpv, name)
|
||||
defer { mpv_free(cstr) }
|
||||
return cstr.map { safeString($0) }
|
||||
func getPropertyAsync(_ name: String, completion: @escaping (Result<String?, Error>) -> Void) {
|
||||
guard let mpv else {
|
||||
completion(.success(nil))
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -207,8 +270,7 @@ class MpvPlayerCoreBase: NSObject {
|
||||
}
|
||||
|
||||
func command(_ args: [String]) {
|
||||
guard mpv != nil, !args.isEmpty else { return }
|
||||
command(args[0], args: Array(args.dropFirst()))
|
||||
commandAsync(args) { _ in }
|
||||
}
|
||||
|
||||
func commandAsync(_ args: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
@@ -217,11 +279,7 @@ class MpvPlayerCoreBase: NSObject {
|
||||
return
|
||||
}
|
||||
|
||||
pendingCommandsLock.lock()
|
||||
let requestId = nextRequestId
|
||||
nextRequestId += 1
|
||||
pendingCommands[requestId] = completion
|
||||
pendingCommandsLock.unlock()
|
||||
let requestId = registerRequest(.void(completion))
|
||||
|
||||
var cargs: [UnsafeMutablePointer<CChar>?] = args.map { strdup($0) }
|
||||
cargs.append(nil)
|
||||
@@ -229,21 +287,7 @@ class MpvPlayerCoreBase: NSObject {
|
||||
cargs.withUnsafeBufferPointer { buffer in
|
||||
var constPointers = buffer.map { UnsafePointer($0) }
|
||||
let result = mpv_command_async(mpv, requestId, &constPointers)
|
||||
if result < 0 {
|
||||
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))
|
||||
}
|
||||
}
|
||||
completeRequestIfSubmissionFailed(requestId: requestId, status: result)
|
||||
}
|
||||
|
||||
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 {
|
||||
guard let mpv else { return true }
|
||||
var flag: Int32 = 0
|
||||
mpv_get_property(mpv, "pause", MPV_FORMAT_FLAG, &flag)
|
||||
return flag != 0
|
||||
cacheLock.lock()
|
||||
defer { cacheLock.unlock() }
|
||||
return cachedPaused
|
||||
}
|
||||
|
||||
var duration: Double {
|
||||
guard let mpv else { return 0 }
|
||||
var value: Double = 0
|
||||
mpv_get_property(mpv, "duration", MPV_FORMAT_DOUBLE, &value)
|
||||
return value
|
||||
cacheLock.lock()
|
||||
defer { cacheLock.unlock() }
|
||||
return cachedDuration
|
||||
}
|
||||
|
||||
var timePos: Double {
|
||||
guard let mpv else { return 0 }
|
||||
var value: Double = 0
|
||||
mpv_get_property(mpv, "time-pos", MPV_FORMAT_DOUBLE, &value)
|
||||
return value
|
||||
cacheLock.lock()
|
||||
defer { cacheLock.unlock() }
|
||||
return cachedTimePos
|
||||
}
|
||||
|
||||
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) {
|
||||
isDisposing = true
|
||||
cancelPendingCommands()
|
||||
cancelPendingRequests()
|
||||
|
||||
let mpvHandle = mpv
|
||||
mpv = nil
|
||||
@@ -299,9 +367,9 @@ class MpvPlayerCoreBase: NSObject {
|
||||
|
||||
func applyGpuNextOptions() {
|
||||
guard mpv != nil else { return }
|
||||
mpv_set_property_string(mpv, "gpu-api", "vulkan")
|
||||
mpv_set_property_string(mpv, "gpu-context", "moltenvk")
|
||||
mpv_set_property_string(mpv, "vo", "gpu-next")
|
||||
setProperty("gpu-api", value: "vulkan")
|
||||
setProperty("gpu-context", value: "moltenvk")
|
||||
setProperty("vo", value: "gpu-next")
|
||||
}
|
||||
|
||||
private func applySharedMpvOptions() {
|
||||
@@ -313,38 +381,99 @@ class MpvPlayerCoreBase: NSObject {
|
||||
checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "yes"))
|
||||
}
|
||||
|
||||
private func cancelPendingCommands() {
|
||||
pendingCommandsLock.lock()
|
||||
let pending = pendingCommands
|
||||
pendingCommands.removeAll()
|
||||
pendingCommandsLock.unlock()
|
||||
private func cancelPendingRequests() {
|
||||
pendingRequestsLock.lock()
|
||||
let pending = pendingRequests
|
||||
pendingRequests.removeAll()
|
||||
pendingRequestsLock.unlock()
|
||||
|
||||
let error = NSError(
|
||||
domain: "mpv",
|
||||
code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Player disposed"]
|
||||
)
|
||||
for (_, completion) in pending {
|
||||
for (_, request) in pending {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func command(_ command: String, args: [String] = []) {
|
||||
guard mpv != nil else { return }
|
||||
private func completeVoidRequest(requestId: UInt64, error status: CInt) {
|
||||
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) }
|
||||
cargs.append(nil)
|
||||
defer {
|
||||
for pointer in cargs {
|
||||
free(pointer)
|
||||
private func completeGetPropertyRequest(_ event: mpv_event) {
|
||||
guard let request = takeRequest(event.reply_userdata) else { return }
|
||||
guard case .getProperty(let completion) = request else { return }
|
||||
|
||||
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
|
||||
var constPointers = buffer.map { UnsafePointer($0) }
|
||||
_ = mpv_command(mpv, &constPointers)
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,29 +500,16 @@ class MpvPlayerCoreBase: NSObject {
|
||||
guard let data = event.data else { break }
|
||||
let property = data.assumingMemoryBound(to: mpv_event_property.self).pointee
|
||||
let name = safeString(property.name)
|
||||
handlePropertyChange(name: name, property: property)
|
||||
handlePropertyChange(name: name, property: property, replyUserdata: event.reply_userdata)
|
||||
|
||||
case MPV_EVENT_COMMAND_REPLY:
|
||||
let requestId = event.reply_userdata
|
||||
pendingCommandsLock.lock()
|
||||
let completion = pendingCommands.removeValue(forKey: requestId)
|
||||
pendingCommandsLock.unlock()
|
||||
completeVoidRequest(requestId: event.reply_userdata, error: event.error)
|
||||
|
||||
guard let completion else { break }
|
||||
if event.error < 0 {
|
||||
let error = NSError(
|
||||
domain: "mpv",
|
||||
code: Int(event.error),
|
||||
userInfo: [NSLocalizedDescriptionKey: safeString(mpv_error_string(event.error))]
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
case MPV_EVENT_SET_PROPERTY_REPLY:
|
||||
completeVoidRequest(requestId: event.reply_userdata, error: event.error)
|
||||
|
||||
case MPV_EVENT_GET_PROPERTY_REPLY:
|
||||
completeGetPropertyRequest(event)
|
||||
|
||||
case MPV_EVENT_FILE_LOADED:
|
||||
DispatchQueue.main.async {
|
||||
@@ -446,9 +562,7 @@ class MpvPlayerCoreBase: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func handlePropertyChange(name: String, property: mpv_event_property) {
|
||||
if isBackgrounded && !Self.criticalProperties.contains(name) { return }
|
||||
|
||||
private func handlePropertyChange(name: String, property: mpv_event_property, replyUserdata: UInt64) {
|
||||
var value: Any?
|
||||
|
||||
switch property.format {
|
||||
@@ -478,18 +592,51 @@ class MpvPlayerCoreBase: NSObject {
|
||||
break
|
||||
}
|
||||
|
||||
updateCachedProperty(name: name, value: value)
|
||||
|
||||
if name == "video-params/sig-peak", let sigPeak = value as? Double {
|
||||
cacheLock.lock()
|
||||
lastSigPeak = sigPeak
|
||||
cacheLock.unlock()
|
||||
DispatchQueue.main.async {
|
||||
self.updateEDRMode(sigPeak: sigPeak)
|
||||
}
|
||||
}
|
||||
|
||||
if Self.internalObserverIds.contains(replyUserdata) { return }
|
||||
if isBackgrounded && !Self.criticalProperties.contains(name) { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
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? {
|
||||
switch node.format {
|
||||
case MPV_FORMAT_STRING:
|
||||
|
||||
@@ -27,7 +27,14 @@ extension MpvPluginShared {
|
||||
)
|
||||
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) {
|
||||
|
||||
@@ -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_);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user