fix: async MPV commands to prevent ANR
This commit is contained in:
@@ -22,8 +22,10 @@ import android.view.TextureView
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import dev.jdtech.mpv.MPVLib
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
interface MpvPlayerDelegate {
|
||||
fun onPropertyChange(name: String, value: Any?)
|
||||
@@ -48,6 +50,9 @@ class MpvPlayerCore(private val activity: Activity) :
|
||||
var isInitialized: Boolean = false
|
||||
private set
|
||||
|
||||
// Executor for running MPV commands off the UI thread to prevent ANR
|
||||
private val commandExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
// Frame rate matching
|
||||
private var currentVideoFps: Float = 0f
|
||||
private var displayListener: DisplayManager.DisplayListener? = null
|
||||
@@ -451,6 +456,32 @@ class MpvPlayerCore(private val activity: Activity) :
|
||||
MPVLib.command(args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an MPV command asynchronously off the UI thread.
|
||||
* This prevents ANR when commands like loadfile block waiting for network I/O.
|
||||
* The result is called back on the UI thread when the command completes.
|
||||
*/
|
||||
fun commandAsync(args: Array<String>, result: MethodChannel.Result) {
|
||||
if (!isInitialized || args.isEmpty()) {
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
|
||||
commandExecutor.execute {
|
||||
try {
|
||||
MPVLib.command(args)
|
||||
activity.runOnUiThread {
|
||||
result.success(null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Async command failed: ${e.message}", e)
|
||||
activity.runOnUiThread {
|
||||
result.error("COMMAND_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setVisible(visible: Boolean) {
|
||||
activity.runOnUiThread {
|
||||
surfaceView?.visibility = if (visible) View.VISIBLE else View.INVISIBLE
|
||||
@@ -652,6 +683,9 @@ class MpvPlayerCore(private val activity: Activity) :
|
||||
fun dispose() {
|
||||
Log.d(TAG, "Disposing")
|
||||
|
||||
// Shutdown command executor
|
||||
commandExecutor.shutdown()
|
||||
|
||||
// Clean up frame rate listener
|
||||
clearVideoFrameRate()
|
||||
|
||||
|
||||
@@ -190,8 +190,10 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
return
|
||||
}
|
||||
|
||||
playerCore?.command(args.toTypedArray())
|
||||
result.success(null)
|
||||
// Use async command to prevent ANR - command executes off UI thread
|
||||
// and result is called back when complete
|
||||
playerCore?.commandAsync(args.toTypedArray(), result)
|
||||
?: result.success(null)
|
||||
}
|
||||
|
||||
private fun handleSetVisible(call: MethodCall, result: MethodChannel.Result) {
|
||||
|
||||
@@ -55,6 +55,11 @@ class MpvPlayerCore: NSObject {
|
||||
private var hdrEnabled = true // User preference for HDR
|
||||
private var lastSigPeak: Double = 0.0 // Last known sig-peak for re-evaluation
|
||||
|
||||
// Async command tracking to prevent UI blocking
|
||||
private var pendingCommands: [UInt64: (Result<Void, Error>) -> Void] = [:]
|
||||
private var pendingCommandsLock = NSLock()
|
||||
private var nextRequestId: UInt64 = 1
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
func initialize(in window: UIWindow) -> Bool {
|
||||
@@ -251,6 +256,50 @@ class MpvPlayerCore: NSObject {
|
||||
command(args[0], args: Array(args.dropFirst()))
|
||||
}
|
||||
|
||||
/// Execute an MPV command asynchronously to prevent UI blocking.
|
||||
/// Uses mpv_command_async which returns immediately; the completion is called
|
||||
/// when MPV_EVENT_COMMAND_REPLY is received.
|
||||
func commandAsync(_ args: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let mpv = mpv, !args.isEmpty else {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
// Generate unique request ID
|
||||
pendingCommandsLock.lock()
|
||||
let requestId = nextRequestId
|
||||
nextRequestId += 1
|
||||
pendingCommands[requestId] = completion
|
||||
pendingCommandsLock.unlock()
|
||||
|
||||
// Build array of C strings for mpv_command_async
|
||||
var cargs: [UnsafeMutablePointer<CChar>?] = args.map { strdup($0) }
|
||||
cargs.append(nil) // null-terminate
|
||||
|
||||
// mpv_command_async returns immediately
|
||||
cargs.withUnsafeBufferPointer { buffer in
|
||||
var constPtrs = buffer.map { UnsafePointer($0) }
|
||||
let result = mpv_command_async(mpv, requestId, &constPtrs)
|
||||
if result < 0 {
|
||||
// Command submission failed, complete immediately with error
|
||||
pendingCommandsLock.lock()
|
||||
if let pending = pendingCommands.removeValue(forKey: requestId) {
|
||||
pendingCommandsLock.unlock()
|
||||
let error = NSError(domain: "mpv", code: Int(result),
|
||||
userInfo: [NSLocalizedDescriptionKey: String(cString: mpv_error_string(result))])
|
||||
DispatchQueue.main.async { pending(.failure(error)) }
|
||||
} else {
|
||||
pendingCommandsLock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Free the C strings
|
||||
for ptr in cargs {
|
||||
free(ptr)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Visibility
|
||||
|
||||
func setVisible(_ visible: Bool) {
|
||||
@@ -333,6 +382,23 @@ class MpvPlayerCore: NSObject {
|
||||
let name = String(cString: property.name)
|
||||
handlePropertyChange(name: name, property: property)
|
||||
|
||||
case MPV_EVENT_COMMAND_REPLY:
|
||||
// Handle async command completion
|
||||
let requestId = event.reply_userdata
|
||||
pendingCommandsLock.lock()
|
||||
let completion = pendingCommands.removeValue(forKey: requestId)
|
||||
pendingCommandsLock.unlock()
|
||||
|
||||
if let completion = completion {
|
||||
if event.error < 0 {
|
||||
let error = NSError(domain: "mpv", code: Int(event.error),
|
||||
userInfo: [NSLocalizedDescriptionKey: String(cString: mpv_error_string(event.error))])
|
||||
DispatchQueue.main.async { completion(.failure(error)) }
|
||||
} else {
|
||||
DispatchQueue.main.async { completion(.success(())) }
|
||||
}
|
||||
}
|
||||
|
||||
case MPV_EVENT_FILE_LOADED:
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.onEvent(name: "file-loaded", data: nil)
|
||||
@@ -494,6 +560,19 @@ class MpvPlayerCore: NSObject {
|
||||
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
|
||||
// Cancel any pending async commands
|
||||
pendingCommandsLock.lock()
|
||||
let pending = pendingCommands
|
||||
pendingCommands.removeAll()
|
||||
pendingCommandsLock.unlock()
|
||||
|
||||
// Complete pending commands with cancellation error
|
||||
let cancelError = NSError(domain: "mpv", code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Player disposed"])
|
||||
for (_, completion) in pending {
|
||||
DispatchQueue.main.async { completion(.failure(cancelError)) }
|
||||
}
|
||||
|
||||
let mpvHandle = mpv
|
||||
mpv = nil
|
||||
|
||||
|
||||
@@ -178,8 +178,15 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
||||
return
|
||||
}
|
||||
|
||||
playerCore?.command(commandArgs)
|
||||
result(nil)
|
||||
// Use async command to prevent UI blocking during network operations
|
||||
playerCore?.commandAsync(commandArgs) { commandResult in
|
||||
switch commandResult {
|
||||
case .success:
|
||||
result(nil)
|
||||
case .failure(let error):
|
||||
result(FlutterError(code: "COMMAND_FAILED", message: error.localizedDescription, details: nil))
|
||||
}
|
||||
} ?? result(nil)
|
||||
}
|
||||
|
||||
private func handleSetVisible(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
|
||||
@@ -144,6 +144,15 @@ void MpvPlayer::Dispose() {
|
||||
return; // Already disposed
|
||||
}
|
||||
|
||||
// Cancel pending async commands
|
||||
{
|
||||
std::lock_guard<std::mutex> cmd_lock(pending_commands_mutex_);
|
||||
for (auto& pair : pending_commands_) {
|
||||
if (pair.second) pair.second(-1); // Call with error
|
||||
}
|
||||
pending_commands_.clear();
|
||||
}
|
||||
|
||||
// Clear mpv callbacks BEFORE freeing to prevent new callbacks being scheduled
|
||||
if (mpv_gl_) {
|
||||
mpv_render_context_set_update_callback(mpv_gl_, nullptr, nullptr);
|
||||
@@ -187,6 +196,42 @@ void MpvPlayer::Command(const std::vector<std::string>& args) {
|
||||
mpv_command(mpv_, c_args.data());
|
||||
}
|
||||
|
||||
void MpvPlayer::CommandAsync(const std::vector<std::string>& args,
|
||||
CommandCallback callback) {
|
||||
if (disposed_ || !mpv_) {
|
||||
if (callback) callback(0);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
@@ -329,6 +374,32 @@ bool MpvPlayer::ProcessEvents() {
|
||||
|
||||
void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
||||
switch (event->event_id) {
|
||||
case MPV_EVENT_COMMAND_REPLY: {
|
||||
// Handle async command completion
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (callback) {
|
||||
// Call callback on main thread
|
||||
int error = event->error;
|
||||
g_idle_add(
|
||||
[](gpointer data) -> gboolean {
|
||||
auto* pair = static_cast<std::pair<CommandCallback, int>*>(data);
|
||||
if (pair->first) pair->first(pair->second);
|
||||
delete pair;
|
||||
return G_SOURCE_REMOVE;
|
||||
},
|
||||
new std::pair<CommandCallback, int>(std::move(callback), error));
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -48,6 +48,15 @@ class MpvPlayer {
|
||||
/// @param args Command arguments (e.g., ["loadfile", "url", "replace"]).
|
||||
void Command(const std::vector<std::string>& args);
|
||||
|
||||
/// Callback type for async command completion.
|
||||
using CommandCallback = std::function<void(int error)>;
|
||||
|
||||
/// Executes an mpv command asynchronously to prevent UI blocking.
|
||||
/// The callback is called on the main thread when the command completes.
|
||||
/// @param args Command arguments.
|
||||
/// @param callback Callback called with error code (0 = success).
|
||||
void CommandAsync(const std::vector<std::string>& args, CommandCallback callback);
|
||||
|
||||
/// Sets an mpv property by name.
|
||||
/// @param name Property name.
|
||||
/// @param value Property value as string.
|
||||
@@ -125,6 +134,10 @@ class MpvPlayer {
|
||||
uint64_t next_reply_userdata_ = 1;
|
||||
std::map<std::string, uint64_t> observed_properties_;
|
||||
|
||||
// Pending async commands: request_id -> callback
|
||||
std::map<uint64_t, CommandCallback> pending_commands_;
|
||||
std::mutex pending_commands_mutex_;
|
||||
|
||||
// GSource for processing events on main thread
|
||||
guint event_source_id_ = 0;
|
||||
};
|
||||
|
||||
@@ -342,8 +342,21 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
|
||||
command_args.push_back(fl_value_get_string(item));
|
||||
}
|
||||
}
|
||||
self->player->Command(command_args);
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
// Use async command to prevent UI blocking during network operations
|
||||
// Take ownership of method_call to respond asynchronously
|
||||
g_object_ref(method_call);
|
||||
self->player->CommandAsync(command_args, [method_call](int error) {
|
||||
g_autoptr(FlMethodResponse) async_response = nullptr;
|
||||
if (error < 0) {
|
||||
async_response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"COMMAND_FAILED", "MPV command failed", nullptr));
|
||||
} else {
|
||||
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 will be sent asynchronously
|
||||
}
|
||||
}
|
||||
} else if (strcmp(method, "setProperty") == 0) {
|
||||
|
||||
@@ -52,6 +52,11 @@ class MpvPlayerCore: NSObject {
|
||||
private var hdrEnabled = true // User preference for HDR
|
||||
private var lastSigPeak: Double = 0.0 // Last known sig-peak for re-evaluation
|
||||
|
||||
// Async command tracking to prevent UI blocking
|
||||
private var pendingCommands: [UInt64: (Result<Void, Error>) -> Void] = [:]
|
||||
private var pendingCommandsLock = NSLock()
|
||||
private var nextRequestId: UInt64 = 1
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
func initialize(in window: NSWindow) -> Bool {
|
||||
@@ -204,6 +209,50 @@ class MpvPlayerCore: NSObject {
|
||||
command(args[0], args: Array(args.dropFirst()))
|
||||
}
|
||||
|
||||
/// Execute an MPV command asynchronously to prevent UI blocking.
|
||||
/// Uses mpv_command_async which returns immediately; the completion is called
|
||||
/// when MPV_EVENT_COMMAND_REPLY is received.
|
||||
func commandAsync(_ args: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let mpv = mpv, !args.isEmpty else {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
// Generate unique request ID
|
||||
pendingCommandsLock.lock()
|
||||
let requestId = nextRequestId
|
||||
nextRequestId += 1
|
||||
pendingCommands[requestId] = completion
|
||||
pendingCommandsLock.unlock()
|
||||
|
||||
// Build array of C strings for mpv_command_async
|
||||
var cargs: [UnsafeMutablePointer<CChar>?] = args.map { strdup($0) }
|
||||
cargs.append(nil) // null-terminate
|
||||
|
||||
// mpv_command_async returns immediately
|
||||
cargs.withUnsafeBufferPointer { buffer in
|
||||
var constPtrs = buffer.map { UnsafePointer($0) }
|
||||
let result = mpv_command_async(mpv, requestId, &constPtrs)
|
||||
if result < 0 {
|
||||
// Command submission failed, complete immediately with error
|
||||
pendingCommandsLock.lock()
|
||||
if let pending = pendingCommands.removeValue(forKey: requestId) {
|
||||
pendingCommandsLock.unlock()
|
||||
let error = NSError(domain: "mpv", code: Int(result),
|
||||
userInfo: [NSLocalizedDescriptionKey: String(cString: mpv_error_string(result))])
|
||||
DispatchQueue.main.async { pending(.failure(error)) }
|
||||
} else {
|
||||
pendingCommandsLock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Free the C strings
|
||||
for ptr in cargs {
|
||||
free(ptr)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Visibility
|
||||
|
||||
func setVisible(_ visible: Bool) {
|
||||
@@ -288,6 +337,23 @@ class MpvPlayerCore: NSObject {
|
||||
let name = String(cString: property.name)
|
||||
handlePropertyChange(name: name, property: property)
|
||||
|
||||
case MPV_EVENT_COMMAND_REPLY:
|
||||
// Handle async command completion
|
||||
let requestId = event.reply_userdata
|
||||
pendingCommandsLock.lock()
|
||||
let completion = pendingCommands.removeValue(forKey: requestId)
|
||||
pendingCommandsLock.unlock()
|
||||
|
||||
if let completion = completion {
|
||||
if event.error < 0 {
|
||||
let error = NSError(domain: "mpv", code: Int(event.error),
|
||||
userInfo: [NSLocalizedDescriptionKey: String(cString: mpv_error_string(event.error))])
|
||||
DispatchQueue.main.async { completion(.failure(error)) }
|
||||
} else {
|
||||
DispatchQueue.main.async { completion(.success(())) }
|
||||
}
|
||||
}
|
||||
|
||||
case MPV_EVENT_FILE_LOADED:
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.onEvent(name: "file-loaded", data: nil)
|
||||
@@ -441,6 +507,19 @@ class MpvPlayerCore: NSObject {
|
||||
// MARK: - Cleanup
|
||||
|
||||
func dispose() {
|
||||
// Cancel any pending async commands
|
||||
pendingCommandsLock.lock()
|
||||
let pending = pendingCommands
|
||||
pendingCommands.removeAll()
|
||||
pendingCommandsLock.unlock()
|
||||
|
||||
// Complete pending commands with cancellation error
|
||||
let cancelError = NSError(domain: "mpv", code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Player disposed"])
|
||||
for (_, completion) in pending {
|
||||
DispatchQueue.main.async { completion(.failure(cancelError)) }
|
||||
}
|
||||
|
||||
// Capture handle before clearing to avoid weak captures during deinit
|
||||
let mpvHandle = mpv
|
||||
mpv = nil
|
||||
|
||||
@@ -178,8 +178,15 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
||||
return
|
||||
}
|
||||
|
||||
playerCore?.command(commandArgs)
|
||||
result(nil)
|
||||
// Use async command to prevent UI blocking during network operations
|
||||
playerCore?.commandAsync(commandArgs) { commandResult in
|
||||
switch commandResult {
|
||||
case .success:
|
||||
result(nil)
|
||||
case .failure(let error):
|
||||
result(FlutterError(code: "COMMAND_FAILED", message: error.localizedDescription, details: nil))
|
||||
}
|
||||
} ?? result(nil)
|
||||
}
|
||||
|
||||
private func handleSetVisible(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
|
||||
@@ -116,6 +116,15 @@ bool MpvPlayer::Initialize(HWND container, HWND flutter_window) {
|
||||
void MpvPlayer::Dispose() {
|
||||
StopEventLoop();
|
||||
|
||||
// Cancel pending async commands
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pending_commands_mutex_);
|
||||
for (auto& pair : pending_commands_) {
|
||||
if (pair.second) pair.second(-1); // Call with error
|
||||
}
|
||||
pending_commands_.clear();
|
||||
}
|
||||
|
||||
if (mpv_) {
|
||||
mpv_terminate_destroy(mpv_);
|
||||
mpv_ = nullptr;
|
||||
@@ -142,6 +151,42 @@ void MpvPlayer::Command(const std::vector<std::string>& args) {
|
||||
mpv_command(mpv_, c_args.data());
|
||||
}
|
||||
|
||||
void MpvPlayer::CommandAsync(const std::vector<std::string>& args,
|
||||
CommandCallback callback) {
|
||||
if (!mpv_) {
|
||||
if (callback) callback(0);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MpvPlayer::SetProperty(const std::string& name, const std::string& value) {
|
||||
if (!mpv_) return;
|
||||
|
||||
@@ -270,6 +315,23 @@ void MpvPlayer::EventLoop() {
|
||||
|
||||
void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
||||
switch (event->event_id) {
|
||||
case MPV_EVENT_COMMAND_REPLY: {
|
||||
// Handle async command completion
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (callback) {
|
||||
callback(event->error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MPV_EVENT_LOG_MESSAGE: {
|
||||
auto* msg = static_cast<mpv_event_log_message*>(event->data);
|
||||
char log_msg[512];
|
||||
|
||||
@@ -39,6 +39,13 @@ class MpvPlayer {
|
||||
// Executes an mpv command.
|
||||
void Command(const std::vector<std::string>& args);
|
||||
|
||||
// Callback type for async command completion.
|
||||
using CommandCallback = std::function<void(int error)>;
|
||||
|
||||
// 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.
|
||||
void SetProperty(const std::string& name, const std::string& value);
|
||||
|
||||
@@ -84,6 +91,10 @@ class MpvPlayer {
|
||||
uint64_t next_reply_userdata_ = 1;
|
||||
std::map<std::string, uint64_t> observed_properties_;
|
||||
|
||||
// Pending async commands: request_id -> callback
|
||||
std::map<uint64_t, CommandCallback> pending_commands_;
|
||||
std::mutex pending_commands_mutex_;
|
||||
|
||||
// HDR state
|
||||
bool hdr_enabled_ = true; // User preference
|
||||
double last_sig_peak_ = 0.0; // Last known sig-peak for HDR content detection
|
||||
|
||||
@@ -205,8 +205,17 @@ void MpvPlayerPlugin::HandleMethodCall(
|
||||
}
|
||||
}
|
||||
|
||||
player_->Command(command_args);
|
||||
result->Success();
|
||||
// Use async command to prevent UI blocking during network operations
|
||||
// Move result into shared_ptr for safe capture in callback
|
||||
auto result_ptr = std::make_shared<std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>>(std::move(result));
|
||||
player_->CommandAsync(command_args, [result_ptr](int error) {
|
||||
if (error < 0) {
|
||||
(*result_ptr)->Error("COMMAND_FAILED", "MPV command failed");
|
||||
} else {
|
||||
(*result_ptr)->Success();
|
||||
}
|
||||
});
|
||||
return; // Response will be sent asynchronously
|
||||
} else if (method == "setProperty") {
|
||||
if (!player_ || !player_->IsInitialized()) {
|
||||
result->Error("NOT_INITIALIZED", "Player not initialized");
|
||||
|
||||
Reference in New Issue
Block a user