perf: compact [propId, value] wire format for property events
This commit is contained in:
@@ -29,6 +29,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||||||
private var usingMpvFallback: Boolean = false
|
private var usingMpvFallback: Boolean = false
|
||||||
private var activity: Activity? = null
|
private var activity: Activity? = null
|
||||||
private var activityBinding: ActivityPluginBinding? = null
|
private var activityBinding: ActivityPluginBinding? = null
|
||||||
|
private val nameToId = mutableMapOf<String, Int>()
|
||||||
|
|
||||||
// FlutterPlugin
|
// FlutterPlugin
|
||||||
|
|
||||||
@@ -119,6 +120,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||||||
"getStats" -> handleGetStats(result)
|
"getStats" -> handleGetStats(result)
|
||||||
"getPlayerType" -> result.success(if (usingMpvFallback) "mpv" else "exoplayer")
|
"getPlayerType" -> result.success(if (usingMpvFallback) "mpv" else "exoplayer")
|
||||||
"setSubtitleStyle" -> handleSetSubtitleStyle(call, result)
|
"setSubtitleStyle" -> handleSetSubtitleStyle(call, result)
|
||||||
|
"observeProperty" -> handleObserveProperty(call, result)
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,6 +415,19 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleObserveProperty(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val name = call.argument<String>("name")
|
||||||
|
val id = call.argument<Int>("id")
|
||||||
|
|
||||||
|
if (name == null || id == null) {
|
||||||
|
result.error("INVALID_ARGS", "Missing 'name' or 'id'", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nameToId[name] = id
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleSetSubtitleStyle(call: MethodCall, result: MethodChannel.Result) {
|
private fun handleSetSubtitleStyle(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val fontSize = call.argument<Number>("fontSize")?.toFloat() ?: 55f
|
val fontSize = call.argument<Number>("fontSize")?.toFloat() ?: 55f
|
||||||
val textColor = call.argument<String>("textColor") ?: "#FFFFFF"
|
val textColor = call.argument<String>("textColor") ?: "#FFFFFF"
|
||||||
@@ -508,13 +523,8 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||||||
// ExoPlayerDelegate
|
// ExoPlayerDelegate
|
||||||
|
|
||||||
override fun onPropertyChange(name: String, value: Any?) {
|
override fun onPropertyChange(name: String, value: Any?) {
|
||||||
eventSink?.success(
|
val propId = nameToId[name] ?: return
|
||||||
mapOf(
|
eventSink?.success(listOf(propId, value))
|
||||||
"type" to "property",
|
|
||||||
"name" to name,
|
|
||||||
"value" to value
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEvent(name: String, data: Map<String, Any>?) {
|
override fun onEvent(name: String, data: Map<String, Any>?) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||||||
private var playerCore: MpvPlayerCore? = null
|
private var playerCore: MpvPlayerCore? = null
|
||||||
private var activity: Activity? = null
|
private var activity: Activity? = null
|
||||||
private var activityBinding: ActivityPluginBinding? = null
|
private var activityBinding: ActivityPluginBinding? = null
|
||||||
|
private val nameToId = mutableMapOf<String, Int>()
|
||||||
|
|
||||||
// FlutterPlugin
|
// FlutterPlugin
|
||||||
|
|
||||||
@@ -175,12 +176,14 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||||||
private fun handleObserveProperty(call: MethodCall, result: MethodChannel.Result) {
|
private fun handleObserveProperty(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val name = call.argument<String>("name")
|
val name = call.argument<String>("name")
|
||||||
val format = call.argument<String>("format")
|
val format = call.argument<String>("format")
|
||||||
|
val id = call.argument<Int>("id")
|
||||||
|
|
||||||
if (name == null || format == null) {
|
if (name == null || format == null || id == null) {
|
||||||
result.error("INVALID_ARGS", "Missing 'name' or 'format'", null)
|
result.error("INVALID_ARGS", "Missing 'name', 'format', or 'id'", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nameToId[name] = id
|
||||||
playerCore?.observeProperty(name, format)
|
playerCore?.observeProperty(name, format)
|
||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
@@ -272,13 +275,8 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||||||
// MpvPlayerDelegate
|
// MpvPlayerDelegate
|
||||||
|
|
||||||
override fun onPropertyChange(name: String, value: Any?) {
|
override fun onPropertyChange(name: String, value: Any?) {
|
||||||
eventSink?.success(
|
val propId = nameToId[name] ?: return
|
||||||
mapOf(
|
eventSink?.success(listOf(propId, value))
|
||||||
"type" to "property",
|
|
||||||
"name" to name,
|
|
||||||
"value" to value
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEvent(name: String, data: Map<String, Any>?) {
|
override fun onEvent(name: String, data: Map<String, Any>?) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
|||||||
private var playerCore: MpvPlayerCore?
|
private var playerCore: MpvPlayerCore?
|
||||||
private var eventSink: FlutterEventSink?
|
private var eventSink: FlutterEventSink?
|
||||||
private weak var registrar: FlutterPluginRegistrar?
|
private weak var registrar: FlutterPluginRegistrar?
|
||||||
|
private var nameToId: [String: Int] = [:]
|
||||||
|
|
||||||
// MARK: - FlutterPlugin Registration
|
// MARK: - FlutterPlugin Registration
|
||||||
|
|
||||||
@@ -162,11 +163,13 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
|||||||
private func handleObserveProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
private func handleObserveProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
guard let args = call.arguments as? [String: Any],
|
guard let args = call.arguments as? [String: Any],
|
||||||
let name = args["name"] as? String,
|
let name = args["name"] as? String,
|
||||||
let format = args["format"] as? String else {
|
let format = args["format"] as? String,
|
||||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' or 'format' argument", details: nil))
|
let id = args["id"] as? Int else {
|
||||||
|
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name', 'format', or 'id' argument", details: nil))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nameToId[name] = id
|
||||||
playerCore?.observeProperty(name, format: format)
|
playerCore?.observeProperty(name, format: format)
|
||||||
result(nil)
|
result(nil)
|
||||||
}
|
}
|
||||||
@@ -220,13 +223,9 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
|||||||
func onPropertyChange(name: String, value: Any?) {
|
func onPropertyChange(name: String, value: Any?) {
|
||||||
guard let eventSink = eventSink else { return }
|
guard let eventSink = eventSink else { return }
|
||||||
|
|
||||||
var event: [String: Any] = ["type": "property", "name": name]
|
if let propId = nameToId[name] {
|
||||||
|
eventSink([propId, value as Any])
|
||||||
if let value = value {
|
|
||||||
event["value"] = value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
eventSink(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func onEvent(name: String, data: [String: Any]?) {
|
func onEvent(name: String, data: [String: Any]?) {
|
||||||
|
|||||||
@@ -51,6 +51,19 @@ class PlayerAndroid extends PlayerBase {
|
|||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
throw Exception('Failed to initialize ExoPlayer');
|
throw Exception('Failed to initialize ExoPlayer');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register property observers so the plugin knows propId mappings
|
||||||
|
await observeProperty('time-pos', 'double');
|
||||||
|
await observeProperty('duration', 'double');
|
||||||
|
await observeProperty('pause', 'flag');
|
||||||
|
await observeProperty('paused-for-cache', 'flag');
|
||||||
|
await observeProperty('track-list', 'string');
|
||||||
|
await observeProperty('eof-reached', 'flag');
|
||||||
|
await observeProperty('volume', 'double');
|
||||||
|
await observeProperty('speed', 'double');
|
||||||
|
await observeProperty('aid', 'string');
|
||||||
|
await observeProperty('sid', 'string');
|
||||||
|
await observeProperty('demuxer-cache-time', 'double');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorController.add('Initialization failed: $e');
|
errorController.add('Initialization failed: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
|||||||
|
|
||||||
StreamSubscription? _eventSubscription;
|
StreamSubscription? _eventSubscription;
|
||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
DateTime? _lastPositionEmit;
|
final _throttleSw = Stopwatch()..start();
|
||||||
|
int _lastEmitMs = 0;
|
||||||
|
int _positionMs = 0;
|
||||||
|
int _nextPropId = 0;
|
||||||
|
final Map<int, String> _propIdToName = {};
|
||||||
|
|
||||||
/// Whether the player has been initialized.
|
/// Whether the player has been initialized.
|
||||||
/// Subclasses should set this to true after initialization.
|
/// Subclasses should set this to true after initialization.
|
||||||
@@ -90,16 +94,31 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Observes a property on the native player and assigns it a compact propId
|
||||||
|
/// for efficient event channel communication.
|
||||||
|
@protected
|
||||||
|
Future<void> observeProperty(String name, String format) async {
|
||||||
|
final propId = _nextPropId++;
|
||||||
|
_propIdToName[propId] = name;
|
||||||
|
await methodChannel.invokeMethod('observeProperty', {
|
||||||
|
'name': name,
|
||||||
|
'format': format,
|
||||||
|
'id': propId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _handleEvent(dynamic event) {
|
void _handleEvent(dynamic event) {
|
||||||
if (event is! Map) return;
|
if (event is List && event.length == 2) {
|
||||||
|
final name = _propIdToName[event[0]];
|
||||||
final type = event['type'] as String?;
|
if (name != null) {
|
||||||
final name = event['name'] as String?;
|
handlePropertyChange(name, event[1]);
|
||||||
|
}
|
||||||
if (type == 'property' && name != null) {
|
} else if (event is Map) {
|
||||||
handlePropertyChange(name, event['value']);
|
final type = event['type'] as String?;
|
||||||
} else if (type == 'event' && name != null) {
|
final name = event['name'] as String?;
|
||||||
handlePlayerEvent(name, event['data'] as Map?);
|
if (type == 'event' && name != null) {
|
||||||
|
handlePlayerEvent(name, event['data'] as Map?);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,14 +146,15 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
|||||||
|
|
||||||
case 'time-pos':
|
case 'time-pos':
|
||||||
if (value is num) {
|
if (value is num) {
|
||||||
final position = Duration(milliseconds: (value * 1000).toInt());
|
_positionMs = (value * 1000).round();
|
||||||
_state = _state.copyWith(position: position);
|
// Only allocate Duration + copyWith + emit at ~4Hz (250ms).
|
||||||
// Throttle stream emissions to ~4Hz (250ms) to reduce listener/rebuild pressure.
|
// Raw int is stored every tick so synchronous reads via _positionMs stay current.
|
||||||
// _state is always updated above so synchronous reads stay current.
|
final nowMs = _throttleSw.elapsedMilliseconds;
|
||||||
final now = DateTime.now();
|
if (nowMs - _lastEmitMs >= 250) {
|
||||||
if (_lastPositionEmit == null || now.difference(_lastPositionEmit!).inMilliseconds >= 250) {
|
_lastEmitMs = nowMs;
|
||||||
_lastPositionEmit = now;
|
final pos = Duration(milliseconds: _positionMs);
|
||||||
positionController.add(position);
|
_state = _state.copyWith(position: pos);
|
||||||
|
positionController.add(pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -42,26 +42,22 @@ class PlayerNative extends PlayerBase {
|
|||||||
await _configureSubtitleFonts();
|
await _configureSubtitleFonts();
|
||||||
|
|
||||||
// Subscribe to MPV properties
|
// Subscribe to MPV properties
|
||||||
await _observeProperty('time-pos', 'double');
|
await observeProperty('time-pos', 'double');
|
||||||
await _observeProperty('duration', 'double');
|
await observeProperty('duration', 'double');
|
||||||
await _observeProperty('pause', 'flag');
|
await observeProperty('pause', 'flag');
|
||||||
await _observeProperty('paused-for-cache', 'flag');
|
await observeProperty('paused-for-cache', 'flag');
|
||||||
await _observeProperty('track-list', (Platform.isAndroid || Platform.isWindows) ? 'string' : 'node');
|
await observeProperty('track-list', (Platform.isAndroid || Platform.isWindows) ? 'string' : 'node');
|
||||||
await _observeProperty('eof-reached', 'flag');
|
await observeProperty('eof-reached', 'flag');
|
||||||
await _observeProperty('volume', 'double');
|
await observeProperty('volume', 'double');
|
||||||
await _observeProperty('speed', 'double');
|
await observeProperty('speed', 'double');
|
||||||
await _observeProperty('aid', 'string');
|
await observeProperty('aid', 'string');
|
||||||
await _observeProperty('sid', 'string');
|
await observeProperty('sid', 'string');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorController.add('Initialization failed: $e');
|
errorController.add('Initialization failed: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _observeProperty(String name, String format) async {
|
|
||||||
await methodChannel.invokeMethod('observeProperty', {'name': name, 'format': format});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configures subtitle fonts for libass support.
|
/// Configures subtitle fonts for libass support.
|
||||||
/// Provides a comprehensive Unicode font (Go Noto) with CJK coverage to ensure
|
/// Provides a comprehensive Unicode font (Go Noto) with CJK coverage to ensure
|
||||||
/// proper rendering of non-Latin characters in subtitles.
|
/// proper rendering of non-Latin characters in subtitles.
|
||||||
|
|||||||
@@ -249,7 +249,8 @@ std::string MpvPlayer::GetProperty(const std::string& name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MpvPlayer::ObserveProperty(const std::string& name,
|
void MpvPlayer::ObserveProperty(const std::string& name,
|
||||||
const std::string& format) {
|
const std::string& format,
|
||||||
|
int id) {
|
||||||
if (disposed_ || !mpv_) return;
|
if (disposed_ || !mpv_) return;
|
||||||
|
|
||||||
// Check if already observing.
|
// Check if already observing.
|
||||||
@@ -257,6 +258,8 @@ void MpvPlayer::ObserveProperty(const std::string& name,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
name_to_id_[name] = id;
|
||||||
|
|
||||||
mpv_format mpv_fmt = MPV_FORMAT_NONE;
|
mpv_format mpv_fmt = MPV_FORMAT_NONE;
|
||||||
if (format == "string") {
|
if (format == "string") {
|
||||||
mpv_fmt = MPV_FORMAT_STRING;
|
mpv_fmt = MPV_FORMAT_STRING;
|
||||||
@@ -515,22 +518,24 @@ FlValue* MpvPlayer::NodeToFlValue(mpv_node* node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) {
|
void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) {
|
||||||
FlValue* event_map = fl_value_new_map();
|
if (!name) return;
|
||||||
fl_value_set_string_take(event_map, "type", fl_value_new_string("property"));
|
|
||||||
fl_value_set_string_take(event_map, "name",
|
|
||||||
fl_value_new_string(name ? name : ""));
|
|
||||||
|
|
||||||
|
auto it = name_to_id_.find(name);
|
||||||
|
if (it == name_to_id_.end()) return;
|
||||||
|
|
||||||
|
FlValue* list = fl_value_new_list();
|
||||||
|
fl_value_append_take(list, fl_value_new_int(it->second));
|
||||||
if (data) {
|
if (data) {
|
||||||
fl_value_set_string_take(event_map, "value", NodeToFlValue(data));
|
fl_value_append_take(list, NodeToFlValue(data));
|
||||||
} else {
|
} else {
|
||||||
fl_value_set_string_take(event_map, "value", fl_value_new_null());
|
fl_value_append_take(list, fl_value_new_null());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||||
if (event_callback_) {
|
if (event_callback_) {
|
||||||
event_callback_(event_map);
|
event_callback_(list);
|
||||||
}
|
}
|
||||||
fl_value_unref(event_map);
|
fl_value_unref(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MpvPlayer::SendEvent(const std::string& name, FlValue* data) {
|
void MpvPlayer::SendEvent(const std::string& name, FlValue* data) {
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ class MpvPlayer {
|
|||||||
/// Changes will be reported via the event callback.
|
/// Changes will be reported via the event callback.
|
||||||
/// @param name Property name to observe.
|
/// @param name Property name to observe.
|
||||||
/// @param format Format type ("string", "flag", "int64", "double", "node").
|
/// @param format Format type ("string", "flag", "int64", "double", "node").
|
||||||
void ObserveProperty(const std::string& name, const std::string& format);
|
/// @param id Property ID assigned by Dart for compact event encoding.
|
||||||
|
void ObserveProperty(const std::string& name, const std::string& format,
|
||||||
|
int id);
|
||||||
|
|
||||||
/// Renders a frame to the current OpenGL context.
|
/// Renders a frame to the current OpenGL context.
|
||||||
/// Must be called from the GTK render callback.
|
/// Must be called from the GTK render callback.
|
||||||
@@ -133,6 +135,7 @@ class MpvPlayer {
|
|||||||
|
|
||||||
uint64_t next_reply_userdata_ = 1;
|
uint64_t next_reply_userdata_ = 1;
|
||||||
std::map<std::string, uint64_t> observed_properties_;
|
std::map<std::string, uint64_t> observed_properties_;
|
||||||
|
std::map<std::string, int> name_to_id_;
|
||||||
|
|
||||||
// Pending async commands: request_id -> callback
|
// Pending async commands: request_id -> callback
|
||||||
std::map<uint64_t, CommandCallback> pending_commands_;
|
std::map<uint64_t, CommandCallback> pending_commands_;
|
||||||
|
|||||||
@@ -411,6 +411,7 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
|
|||||||
} else {
|
} else {
|
||||||
FlValue* name_value = fl_value_lookup_string(args, "name");
|
FlValue* name_value = fl_value_lookup_string(args, "name");
|
||||||
FlValue* format_value = fl_value_lookup_string(args, "format");
|
FlValue* format_value = fl_value_lookup_string(args, "format");
|
||||||
|
FlValue* id_value = fl_value_lookup_string(args, "id");
|
||||||
|
|
||||||
if (name_value == nullptr ||
|
if (name_value == nullptr ||
|
||||||
fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) {
|
fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) {
|
||||||
@@ -420,9 +421,14 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
|
|||||||
fl_value_get_type(format_value) != FL_VALUE_TYPE_STRING) {
|
fl_value_get_type(format_value) != FL_VALUE_TYPE_STRING) {
|
||||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||||
"INVALID_ARGS", "Missing 'format'", nullptr));
|
"INVALID_ARGS", "Missing 'format'", nullptr));
|
||||||
|
} else if (id_value == nullptr ||
|
||||||
|
fl_value_get_type(id_value) != FL_VALUE_TYPE_INT) {
|
||||||
|
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||||
|
"INVALID_ARGS", "Missing 'id'", nullptr));
|
||||||
} else {
|
} else {
|
||||||
self->player->ObserveProperty(fl_value_get_string(name_value),
|
self->player->ObserveProperty(fl_value_get_string(name_value),
|
||||||
fl_value_get_string(format_value));
|
fl_value_get_string(format_value),
|
||||||
|
static_cast<int>(fl_value_get_int(id_value)));
|
||||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,11 +98,48 @@ class MpvPlayerCore: NSObject {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register for fullscreen notifications to avoid MoltenVK swapchain crash
|
||||||
|
let nc = NotificationCenter.default
|
||||||
|
nc.addObserver(self, selector: #selector(windowWillEnterFullScreen),
|
||||||
|
name: NSWindow.willEnterFullScreenNotification, object: window)
|
||||||
|
nc.addObserver(self, selector: #selector(windowDidEnterFullScreen),
|
||||||
|
name: NSWindow.didEnterFullScreenNotification, object: window)
|
||||||
|
nc.addObserver(self, selector: #selector(windowWillExitFullScreen),
|
||||||
|
name: NSWindow.willExitFullScreenNotification, object: window)
|
||||||
|
nc.addObserver(self, selector: #selector(windowDidExitFullScreen),
|
||||||
|
name: NSWindow.didExitFullScreenNotification, object: window)
|
||||||
|
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
print("[MpvPlayerCore] Initialized successfully with MPV")
|
print("[MpvPlayerCore] Initialized successfully with MPV")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Fullscreen Transition Handling
|
||||||
|
|
||||||
|
@objc private func windowWillEnterFullScreen(_ notification: Notification) {
|
||||||
|
guard mpv != nil else { return }
|
||||||
|
print("[MpvPlayerCore] willEnterFullScreen — disabling video output")
|
||||||
|
mpv_set_property_string(mpv, "vid", "no")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func windowDidEnterFullScreen(_ notification: Notification) {
|
||||||
|
guard mpv != nil else { return }
|
||||||
|
print("[MpvPlayerCore] didEnterFullScreen — re-enabling video output")
|
||||||
|
mpv_set_property_string(mpv, "vid", "auto")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func windowWillExitFullScreen(_ notification: Notification) {
|
||||||
|
guard mpv != nil else { return }
|
||||||
|
print("[MpvPlayerCore] willExitFullScreen — disabling video output")
|
||||||
|
mpv_set_property_string(mpv, "vid", "no")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func windowDidExitFullScreen(_ notification: Notification) {
|
||||||
|
guard mpv != nil else { return }
|
||||||
|
print("[MpvPlayerCore] didExitFullScreen — re-enabling video output")
|
||||||
|
mpv_set_property_string(mpv, "vid", "auto")
|
||||||
|
}
|
||||||
|
|
||||||
private func setupMpv() -> Bool {
|
private func setupMpv() -> Bool {
|
||||||
guard let metalLayer = metalLayer else { return false }
|
guard let metalLayer = metalLayer else { return false }
|
||||||
|
|
||||||
@@ -508,6 +545,8 @@ class MpvPlayerCore: NSObject {
|
|||||||
// MARK: - Cleanup
|
// MARK: - Cleanup
|
||||||
|
|
||||||
func dispose() {
|
func dispose() {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
|
||||||
// Cancel any pending async commands
|
// Cancel any pending async commands
|
||||||
pendingCommandsLock.lock()
|
pendingCommandsLock.lock()
|
||||||
let pending = pendingCommands
|
let pending = pendingCommands
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
|||||||
private var playerCore: MpvPlayerCore?
|
private var playerCore: MpvPlayerCore?
|
||||||
private var eventSink: FlutterEventSink?
|
private var eventSink: FlutterEventSink?
|
||||||
private weak var registrar: FlutterPluginRegistrar?
|
private weak var registrar: FlutterPluginRegistrar?
|
||||||
|
private var nameToId: [String: Int] = [:]
|
||||||
|
|
||||||
// MARK: - FlutterPlugin Registration
|
// MARK: - FlutterPlugin Registration
|
||||||
|
|
||||||
@@ -162,11 +163,13 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
|||||||
private func handleObserveProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
private func handleObserveProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
guard let args = call.arguments as? [String: Any],
|
guard let args = call.arguments as? [String: Any],
|
||||||
let name = args["name"] as? String,
|
let name = args["name"] as? String,
|
||||||
let format = args["format"] as? String else {
|
let format = args["format"] as? String,
|
||||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' or 'format' argument", details: nil))
|
let id = args["id"] as? Int else {
|
||||||
|
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name', 'format', or 'id' argument", details: nil))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nameToId[name] = id
|
||||||
playerCore?.observeProperty(name, format: format)
|
playerCore?.observeProperty(name, format: format)
|
||||||
result(nil)
|
result(nil)
|
||||||
}
|
}
|
||||||
@@ -220,13 +223,9 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
|||||||
func onPropertyChange(name: String, value: Any?) {
|
func onPropertyChange(name: String, value: Any?) {
|
||||||
guard let eventSink = eventSink else { return }
|
guard let eventSink = eventSink else { return }
|
||||||
|
|
||||||
var event: [String: Any] = ["type": "property", "name": name]
|
if let propId = nameToId[name] {
|
||||||
|
eventSink([propId, value as Any])
|
||||||
if let value = value {
|
|
||||||
event["value"] = value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
eventSink(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func onEvent(name: String, data: [String: Any]?) {
|
func onEvent(name: String, data: [String: Any]?) {
|
||||||
|
|||||||
@@ -212,7 +212,8 @@ std::string MpvPlayer::GetProperty(const std::string& name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MpvPlayer::ObserveProperty(const std::string& name,
|
void MpvPlayer::ObserveProperty(const std::string& name,
|
||||||
const std::string& format) {
|
const std::string& format,
|
||||||
|
int id) {
|
||||||
if (!mpv_) return;
|
if (!mpv_) return;
|
||||||
|
|
||||||
// Check if already observing.
|
// Check if already observing.
|
||||||
@@ -220,6 +221,8 @@ void MpvPlayer::ObserveProperty(const std::string& name,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
name_to_id_[name] = id;
|
||||||
|
|
||||||
mpv_format mpv_fmt = MPV_FORMAT_NONE;
|
mpv_format mpv_fmt = MPV_FORMAT_NONE;
|
||||||
if (format == "string") {
|
if (format == "string") {
|
||||||
mpv_fmt = MPV_FORMAT_STRING;
|
mpv_fmt = MPV_FORMAT_STRING;
|
||||||
@@ -418,11 +421,10 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) {
|
void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) {
|
||||||
flutter::EncodableMap event;
|
if (!name) return;
|
||||||
event[flutter::EncodableValue("type")] =
|
|
||||||
flutter::EncodableValue("property");
|
auto it = name_to_id_.find(name);
|
||||||
event[flutter::EncodableValue("name")] =
|
if (it == name_to_id_.end()) return;
|
||||||
flutter::EncodableValue(name ? name : "");
|
|
||||||
|
|
||||||
flutter::EncodableValue value;
|
flutter::EncodableValue value;
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -445,11 +447,14 @@ void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
event[flutter::EncodableValue("value")] = value;
|
|
||||||
|
flutter::EncodableList list;
|
||||||
|
list.push_back(flutter::EncodableValue(it->second));
|
||||||
|
list.push_back(value);
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||||
if (event_callback_) {
|
if (event_callback_) {
|
||||||
event_callback_(event);
|
event_callback_(flutter::EncodableValue(list));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,7 +469,7 @@ void MpvPlayer::SendEvent(const std::string& name,
|
|||||||
|
|
||||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||||
if (event_callback_) {
|
if (event_callback_) {
|
||||||
event_callback_(event);
|
event_callback_(flutter::EncodableValue(event));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace mpv {
|
|||||||
class MpvPlayer {
|
class MpvPlayer {
|
||||||
public:
|
public:
|
||||||
using EventCallback =
|
using EventCallback =
|
||||||
std::function<void(const flutter::EncodableMap&)>;
|
std::function<void(const flutter::EncodableValue&)>;
|
||||||
|
|
||||||
MpvPlayer();
|
MpvPlayer();
|
||||||
~MpvPlayer();
|
~MpvPlayer();
|
||||||
@@ -53,7 +53,8 @@ class MpvPlayer {
|
|||||||
std::string GetProperty(const std::string& name);
|
std::string GetProperty(const std::string& name);
|
||||||
|
|
||||||
// Observes an mpv property for changes.
|
// Observes an mpv property for changes.
|
||||||
void ObserveProperty(const std::string& name, const std::string& format);
|
void ObserveProperty(const std::string& name, const std::string& format,
|
||||||
|
int id);
|
||||||
|
|
||||||
// Returns the mpv video window handle.
|
// Returns the mpv video window handle.
|
||||||
HWND GetHwnd() const { return hwnd_; }
|
HWND GetHwnd() const { return hwnd_; }
|
||||||
@@ -90,6 +91,7 @@ class MpvPlayer {
|
|||||||
|
|
||||||
uint64_t next_reply_userdata_ = 1;
|
uint64_t next_reply_userdata_ = 1;
|
||||||
std::map<std::string, uint64_t> observed_properties_;
|
std::map<std::string, uint64_t> observed_properties_;
|
||||||
|
std::map<std::string, int> name_to_id_;
|
||||||
|
|
||||||
// Pending async commands: request_id -> callback
|
// Pending async commands: request_id -> callback
|
||||||
std::map<uint64_t, CommandCallback> pending_commands_;
|
std::map<uint64_t, CommandCallback> pending_commands_;
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ void MpvPlayerPlugin::HandleMethodCall(
|
|||||||
if (success) {
|
if (success) {
|
||||||
LogToFile("MPV Plugin: Player initialized successfully");
|
LogToFile("MPV Plugin: Player initialized successfully");
|
||||||
// Set up event callback.
|
// Set up event callback.
|
||||||
player_->SetEventCallback([this](const flutter::EncodableMap& event) {
|
player_->SetEventCallback([this](const flutter::EncodableValue& event) {
|
||||||
SendEvent(event);
|
SendEvent(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -291,6 +291,7 @@ void MpvPlayerPlugin::HandleMethodCall(
|
|||||||
const auto& map = std::get<flutter::EncodableMap>(*args);
|
const auto& map = std::get<flutter::EncodableMap>(*args);
|
||||||
auto name_it = map.find(flutter::EncodableValue("name"));
|
auto name_it = map.find(flutter::EncodableValue("name"));
|
||||||
auto format_it = map.find(flutter::EncodableValue("format"));
|
auto format_it = map.find(flutter::EncodableValue("format"));
|
||||||
|
auto id_it = map.find(flutter::EncodableValue("id"));
|
||||||
|
|
||||||
if (name_it == map.end() ||
|
if (name_it == map.end() ||
|
||||||
!std::holds_alternative<std::string>(name_it->second)) {
|
!std::holds_alternative<std::string>(name_it->second)) {
|
||||||
@@ -302,9 +303,15 @@ void MpvPlayerPlugin::HandleMethodCall(
|
|||||||
result->Error("INVALID_ARGS", "Missing 'format'");
|
result->Error("INVALID_ARGS", "Missing 'format'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (id_it == map.end() ||
|
||||||
|
!std::holds_alternative<int32_t>(id_it->second)) {
|
||||||
|
result->Error("INVALID_ARGS", "Missing 'id'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
player_->ObserveProperty(std::get<std::string>(name_it->second),
|
player_->ObserveProperty(std::get<std::string>(name_it->second),
|
||||||
std::get<std::string>(format_it->second));
|
std::get<std::string>(format_it->second),
|
||||||
|
std::get<int32_t>(id_it->second));
|
||||||
result->Success();
|
result->Success();
|
||||||
} else if (method == "setVisible") {
|
} else if (method == "setVisible") {
|
||||||
const auto* args = method_call.arguments();
|
const auto* args = method_call.arguments();
|
||||||
@@ -382,9 +389,9 @@ void MpvPlayerPlugin::HandleMethodCall(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MpvPlayerPlugin::SendEvent(const flutter::EncodableMap& event) {
|
void MpvPlayerPlugin::SendEvent(const flutter::EncodableValue& event) {
|
||||||
if (event_sink_) {
|
if (event_sink_) {
|
||||||
event_sink_->Success(flutter::EncodableValue(event));
|
event_sink_->Success(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class MpvPlayerPlugin : public flutter::Plugin {
|
|||||||
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);
|
||||||
|
|
||||||
void SendEvent(const flutter::EncodableMap& event);
|
void SendEvent(const flutter::EncodableValue& event);
|
||||||
|
|
||||||
HWND GetWindow();
|
HWND GetWindow();
|
||||||
HWND GetChildWindow();
|
HWND GetChildWindow();
|
||||||
|
|||||||
Reference in New Issue
Block a user