perf: compact [propId, value] wire format for property events

This commit is contained in:
edde746
2026-02-16 15:40:52 +01:00
parent c3b10d6c45
commit a61fca0588
15 changed files with 193 additions and 91 deletions
@@ -29,6 +29,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
private var usingMpvFallback: Boolean = false
private var activity: Activity? = null
private var activityBinding: ActivityPluginBinding? = null
private val nameToId = mutableMapOf<String, Int>()
// FlutterPlugin
@@ -119,6 +120,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
"getStats" -> handleGetStats(result)
"getPlayerType" -> result.success(if (usingMpvFallback) "mpv" else "exoplayer")
"setSubtitleStyle" -> handleSetSubtitleStyle(call, result)
"observeProperty" -> handleObserveProperty(call, result)
else -> result.notImplemented()
}
}
@@ -413,6 +415,19 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
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) {
val fontSize = call.argument<Number>("fontSize")?.toFloat() ?: 55f
val textColor = call.argument<String>("textColor") ?: "#FFFFFF"
@@ -508,13 +523,8 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
// ExoPlayerDelegate
override fun onPropertyChange(name: String, value: Any?) {
eventSink?.success(
mapOf(
"type" to "property",
"name" to name,
"value" to value
)
)
val propId = nameToId[name] ?: return
eventSink?.success(listOf(propId, value))
}
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 activity: Activity? = null
private var activityBinding: ActivityPluginBinding? = null
private val nameToId = mutableMapOf<String, Int>()
// FlutterPlugin
@@ -175,12 +176,14 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
private fun handleObserveProperty(call: MethodCall, result: MethodChannel.Result) {
val name = call.argument<String>("name")
val format = call.argument<String>("format")
val id = call.argument<Int>("id")
if (name == null || format == null) {
result.error("INVALID_ARGS", "Missing 'name' or 'format'", null)
if (name == null || format == null || id == null) {
result.error("INVALID_ARGS", "Missing 'name', 'format', or 'id'", null)
return
}
nameToId[name] = id
playerCore?.observeProperty(name, format)
result.success(null)
}
@@ -272,13 +275,8 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
// MpvPlayerDelegate
override fun onPropertyChange(name: String, value: Any?) {
eventSink?.success(
mapOf(
"type" to "property",
"name" to name,
"value" to value
)
)
val propId = nameToId[name] ?: return
eventSink?.success(listOf(propId, value))
}
override fun onEvent(name: String, data: Map<String, Any>?) {
+7 -8
View File
@@ -9,6 +9,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
private var playerCore: MpvPlayerCore?
private var eventSink: FlutterEventSink?
private weak var registrar: FlutterPluginRegistrar?
private var nameToId: [String: Int] = [:]
// MARK: - FlutterPlugin Registration
@@ -162,11 +163,13 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
private func handleObserveProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let name = args["name"] as? String,
let format = args["format"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' or 'format' argument", details: nil))
let format = args["format"] as? String,
let id = args["id"] as? Int else {
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name', 'format', or 'id' argument", details: nil))
return
}
nameToId[name] = id
playerCore?.observeProperty(name, format: format)
result(nil)
}
@@ -220,13 +223,9 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
func onPropertyChange(name: String, value: Any?) {
guard let eventSink = eventSink else { return }
var event: [String: Any] = ["type": "property", "name": name]
if let value = value {
event["value"] = value
if let propId = nameToId[name] {
eventSink([propId, value as Any])
}
eventSink(event)
}
func onEvent(name: String, data: [String: Any]?) {
+13
View File
@@ -51,6 +51,19 @@ class PlayerAndroid extends PlayerBase {
if (!initialized) {
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) {
errorController.add('Initialization failed: $e');
rethrow;
+38 -18
View File
@@ -37,7 +37,11 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
StreamSubscription? _eventSubscription;
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.
/// 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) {
if (event is! Map) return;
final type = event['type'] as String?;
final name = event['name'] as String?;
if (type == 'property' && name != null) {
handlePropertyChange(name, event['value']);
} else if (type == 'event' && name != null) {
handlePlayerEvent(name, event['data'] as Map?);
if (event is List && event.length == 2) {
final name = _propIdToName[event[0]];
if (name != null) {
handlePropertyChange(name, event[1]);
}
} else if (event is Map) {
final type = event['type'] as String?;
final name = event['name'] as String?;
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':
if (value is num) {
final position = Duration(milliseconds: (value * 1000).toInt());
_state = _state.copyWith(position: position);
// Throttle stream emissions to ~4Hz (250ms) to reduce listener/rebuild pressure.
// _state is always updated above so synchronous reads stay current.
final now = DateTime.now();
if (_lastPositionEmit == null || now.difference(_lastPositionEmit!).inMilliseconds >= 250) {
_lastPositionEmit = now;
positionController.add(position);
_positionMs = (value * 1000).round();
// Only allocate Duration + copyWith + emit at ~4Hz (250ms).
// Raw int is stored every tick so synchronous reads via _positionMs stay current.
final nowMs = _throttleSw.elapsedMilliseconds;
if (nowMs - _lastEmitMs >= 250) {
_lastEmitMs = nowMs;
final pos = Duration(milliseconds: _positionMs);
_state = _state.copyWith(position: pos);
positionController.add(pos);
}
}
break;
+10 -14
View File
@@ -42,26 +42,22 @@ class PlayerNative extends PlayerBase {
await _configureSubtitleFonts();
// Subscribe to MPV properties
await _observeProperty('time-pos', 'double');
await _observeProperty('duration', 'double');
await _observeProperty('pause', 'flag');
await _observeProperty('paused-for-cache', 'flag');
await _observeProperty('track-list', (Platform.isAndroid || Platform.isWindows) ? 'string' : 'node');
await _observeProperty('eof-reached', 'flag');
await _observeProperty('volume', 'double');
await _observeProperty('speed', 'double');
await _observeProperty('aid', 'string');
await _observeProperty('sid', 'string');
await observeProperty('time-pos', 'double');
await observeProperty('duration', 'double');
await observeProperty('pause', 'flag');
await observeProperty('paused-for-cache', 'flag');
await observeProperty('track-list', (Platform.isAndroid || Platform.isWindows) ? 'string' : 'node');
await observeProperty('eof-reached', 'flag');
await observeProperty('volume', 'double');
await observeProperty('speed', 'double');
await observeProperty('aid', 'string');
await observeProperty('sid', 'string');
} catch (e) {
errorController.add('Initialization failed: $e');
rethrow;
}
}
Future<void> _observeProperty(String name, String format) async {
await methodChannel.invokeMethod('observeProperty', {'name': name, 'format': format});
}
/// Configures subtitle fonts for libass support.
/// Provides a comprehensive Unicode font (Go Noto) with CJK coverage to ensure
/// proper rendering of non-Latin characters in subtitles.
+14 -9
View File
@@ -249,7 +249,8 @@ std::string MpvPlayer::GetProperty(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;
// Check if already observing.
@@ -257,6 +258,8 @@ void MpvPlayer::ObserveProperty(const std::string& name,
return;
}
name_to_id_[name] = id;
mpv_format mpv_fmt = MPV_FORMAT_NONE;
if (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) {
FlValue* event_map = fl_value_new_map();
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 : ""));
if (!name) return;
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) {
fl_value_set_string_take(event_map, "value", NodeToFlValue(data));
fl_value_append_take(list, NodeToFlValue(data));
} 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_);
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) {
+4 -1
View File
@@ -71,7 +71,9 @@ class MpvPlayer {
/// Changes will be reported via the event callback.
/// @param name Property name to observe.
/// @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.
/// Must be called from the GTK render callback.
@@ -133,6 +135,7 @@ class MpvPlayer {
uint64_t next_reply_userdata_ = 1;
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_;
+7 -1
View File
@@ -411,6 +411,7 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
} else {
FlValue* name_value = fl_value_lookup_string(args, "name");
FlValue* format_value = fl_value_lookup_string(args, "format");
FlValue* id_value = fl_value_lookup_string(args, "id");
if (name_value == nullptr ||
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) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
"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 {
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));
}
}
@@ -98,11 +98,48 @@ class MpvPlayerCore: NSObject {
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
print("[MpvPlayerCore] Initialized successfully with MPV")
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 {
guard let metalLayer = metalLayer else { return false }
@@ -508,6 +545,8 @@ class MpvPlayerCore: NSObject {
// MARK: - Cleanup
func dispose() {
NotificationCenter.default.removeObserver(self)
// Cancel any pending async commands
pendingCommandsLock.lock()
let pending = pendingCommands
+7 -8
View File
@@ -9,6 +9,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
private var playerCore: MpvPlayerCore?
private var eventSink: FlutterEventSink?
private weak var registrar: FlutterPluginRegistrar?
private var nameToId: [String: Int] = [:]
// MARK: - FlutterPlugin Registration
@@ -162,11 +163,13 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
private func handleObserveProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let name = args["name"] as? String,
let format = args["format"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' or 'format' argument", details: nil))
let format = args["format"] as? String,
let id = args["id"] as? Int else {
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name', 'format', or 'id' argument", details: nil))
return
}
nameToId[name] = id
playerCore?.observeProperty(name, format: format)
result(nil)
}
@@ -220,13 +223,9 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
func onPropertyChange(name: String, value: Any?) {
guard let eventSink = eventSink else { return }
var event: [String: Any] = ["type": "property", "name": name]
if let value = value {
event["value"] = value
if let propId = nameToId[name] {
eventSink([propId, value as Any])
}
eventSink(event)
}
func onEvent(name: String, data: [String: Any]?) {
+14 -9
View File
@@ -212,7 +212,8 @@ std::string MpvPlayer::GetProperty(const std::string& name) {
}
void MpvPlayer::ObserveProperty(const std::string& name,
const std::string& format) {
const std::string& format,
int id) {
if (!mpv_) return;
// Check if already observing.
@@ -220,6 +221,8 @@ void MpvPlayer::ObserveProperty(const std::string& name,
return;
}
name_to_id_[name] = id;
mpv_format mpv_fmt = MPV_FORMAT_NONE;
if (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) {
flutter::EncodableMap event;
event[flutter::EncodableValue("type")] =
flutter::EncodableValue("property");
event[flutter::EncodableValue("name")] =
flutter::EncodableValue(name ? name : "");
if (!name) return;
auto it = name_to_id_.find(name);
if (it == name_to_id_.end()) return;
flutter::EncodableValue value;
if (data) {
@@ -445,11 +447,14 @@ void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) {
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_);
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_);
if (event_callback_) {
event_callback_(event);
event_callback_(flutter::EncodableValue(event));
}
}
+4 -2
View File
@@ -22,7 +22,7 @@ namespace mpv {
class MpvPlayer {
public:
using EventCallback =
std::function<void(const flutter::EncodableMap&)>;
std::function<void(const flutter::EncodableValue&)>;
MpvPlayer();
~MpvPlayer();
@@ -53,7 +53,8 @@ class MpvPlayer {
std::string GetProperty(const std::string& name);
// 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.
HWND GetHwnd() const { return hwnd_; }
@@ -90,6 +91,7 @@ class MpvPlayer {
uint64_t next_reply_userdata_ = 1;
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_;
+11 -4
View File
@@ -150,7 +150,7 @@ void MpvPlayerPlugin::HandleMethodCall(
if (success) {
LogToFile("MPV Plugin: Player initialized successfully");
// Set up event callback.
player_->SetEventCallback([this](const flutter::EncodableMap& event) {
player_->SetEventCallback([this](const flutter::EncodableValue& event) {
SendEvent(event);
});
@@ -291,6 +291,7 @@ void MpvPlayerPlugin::HandleMethodCall(
const auto& map = std::get<flutter::EncodableMap>(*args);
auto name_it = map.find(flutter::EncodableValue("name"));
auto format_it = map.find(flutter::EncodableValue("format"));
auto id_it = map.find(flutter::EncodableValue("id"));
if (name_it == map.end() ||
!std::holds_alternative<std::string>(name_it->second)) {
@@ -302,9 +303,15 @@ void MpvPlayerPlugin::HandleMethodCall(
result->Error("INVALID_ARGS", "Missing 'format'");
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),
std::get<std::string>(format_it->second));
std::get<std::string>(format_it->second),
std::get<int32_t>(id_it->second));
result->Success();
} else if (method == "setVisible") {
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_) {
event_sink_->Success(flutter::EncodableValue(event));
event_sink_->Success(event);
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ class MpvPlayerPlugin : public flutter::Plugin {
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
void SendEvent(const flutter::EncodableMap& event);
void SendEvent(const flutter::EncodableValue& event);
HWND GetWindow();
HWND GetChildWindow();