fix(windows): support structured mpv observations

This commit is contained in:
edde746
2026-07-12 08:42:24 +02:00
parent 6890cd8690
commit f7f4e3a992
4 changed files with 122 additions and 26 deletions
+3 -3
View File
@@ -73,9 +73,9 @@ class PlayerNative extends PlayerBase {
@override
bool get attachesExternalSubtitlesAtOpen => true;
/// Node properties are returned as structured maps on macOS/iOS/Linux,
/// but as JSON strings on Android/Windows.
static final String _nodeFormat = (Platform.isAndroid || Platform.isWindows) ? 'string' : 'node';
/// Node properties are returned as structured maps on desktop and Apple
/// platforms, but as JSON strings on Android.
static final String _nodeFormat = Platform.isAndroid ? 'string' : 'node';
static String _normalizeDvConversionMode(String value) {
return switch (value.toLowerCase()) {
+66
View File
@@ -53,6 +53,72 @@ void main() {
);
});
test('MPV accepts nested node observations and null unsupported values', () async {
final observations = <String, int>{};
await withMockPlayerChannels(
methodChannelName: 'com.plezy/mpv_player',
eventChannelName: 'com.plezy/mpv_player/events',
methodHandler: (call) async {
if (call.method == 'initialize') return true;
if (call.method == 'observeProperty') {
final arguments = call.arguments as Map;
observations[arguments['name'] as String] = arguments['id'] as int;
}
return null;
},
testBody: () async {
final player = PlayerNative();
try {
await player.setLogLevel('warn');
final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
const codec = StandardMethodCodec();
Future<void> sendObservation(String name, Object? value) async {
final done = Completer<void>();
await messenger.handlePlatformMessage(
'com.plezy/mpv_player/events',
codec.encodeSuccessEnvelope([observations[name], value]),
(_) => done.complete(),
);
await done.future;
await Future<void>.delayed(Duration.zero);
}
await sendObservation('track-list', const [
{
'type': 'audio',
'id': 7,
'title': 'Main',
'selected': true,
'metadata': {
'nested': [true, 2, 3.5, null],
},
},
]);
await sendObservation('demuxer-cache-state', const {
'cache-end': 12.5,
'seekable-ranges': [
{'start': 1, 'end': 9.25},
],
});
expect(player.state.tracks.audio.single.id, '7');
expect(player.state.tracks.audio.single.title, 'Main');
expect(player.state.buffer, const Duration(milliseconds: 12500));
expect(player.state.bufferRanges.single.start, const Duration(seconds: 1));
expect(player.state.bufferRanges.single.end, const Duration(milliseconds: 9250));
// Unsupported mpv_node formats cross the native bridge as null and
// must not erase the last valid structured observation.
await sendObservation('track-list', null);
expect(player.state.tracks.audio.single.id, '7');
} finally {
await player.dispose();
}
},
);
});
test('Android command failure reaches seek recovery', () async {
await withMockPlayerChannels(
methodChannelName: 'com.plezy/mpv_player',
+9
View File
@@ -1,3 +1,5 @@
import 'dart:io' show Platform;
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/mpv/player/platform/player_android.dart';
@@ -94,5 +96,12 @@ void main() {
final registered = names(observations);
expect(registered, containsAll(coreNames));
expect(registered, containsAll({'secondary-sid', 'demuxer-cache-state', 'audio-device-list', 'audio-device'}));
final structuredFormat = Platform.isAndroid ? 'string' : 'node';
for (final call in observations.where((call) {
final name = (call.arguments as Map)['name'];
return name == 'track-list' || name == 'demuxer-cache-state' || name == 'audio-device-list';
})) {
expect((call.arguments as Map)['format'], structuredFormat);
}
});
}
+44 -23
View File
@@ -25,6 +25,46 @@ constexpr std::chrono::milliseconds kNullFirstDelay{500};
constexpr std::chrono::milliseconds kNullBackoffCap{8000};
constexpr std::chrono::milliseconds kDeviceListDebounce{250};
flutter::EncodableValue NodeToEncodableValue(const mpv_node* node) {
if (!node) return flutter::EncodableValue();
switch (node->format) {
case MPV_FORMAT_STRING:
return flutter::EncodableValue(SanitizeUtf8(node->u.string));
case MPV_FORMAT_FLAG:
return flutter::EncodableValue(node->u.flag != 0);
case MPV_FORMAT_INT64:
return flutter::EncodableValue(node->u.int64);
case MPV_FORMAT_DOUBLE:
return flutter::EncodableValue(node->u.double_);
case MPV_FORMAT_NODE_ARRAY: {
const mpv_node_list* node_list = node->u.list;
if (!node_list || node_list->num < 0 || (node_list->num > 0 && !node_list->values)) {
return flutter::EncodableValue();
}
flutter::EncodableList list;
list.reserve(static_cast<size_t>(node_list->num));
for (int i = 0; i < node_list->num; ++i) {
list.push_back(NodeToEncodableValue(&node_list->values[i]));
}
return flutter::EncodableValue(list);
}
case MPV_FORMAT_NODE_MAP: {
const mpv_node_list* node_list = node->u.list;
if (!node_list || node_list->num < 0 || (node_list->num > 0 && (!node_list->values || !node_list->keys))) {
return flutter::EncodableValue();
}
flutter::EncodableMap map;
for (int i = 0; i < node_list->num; ++i) {
map[flutter::EncodableValue(SanitizeUtf8(node_list->keys[i]))] = NodeToEncodableValue(&node_list->values[i]);
}
return flutter::EncodableValue(map);
}
default:
return flutter::EncodableValue();
}
}
// DComp-mode input forwarding. mpv's inner window lives on mpv's own thread
// and consumes the mouse input over the video (WS_EX_TRANSPARENT hit-test
// skipping is same-thread-only, and disabling the subtree makes the system
@@ -528,7 +568,7 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
}
case MPV_EVENT_PROPERTY_CHANGE: {
auto* prop = static_cast<mpv_event_property*>(event->data);
mpv_node node;
mpv_node node{};
node.format = prop->format;
switch (prop->format) {
@@ -636,30 +676,11 @@ void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) {
auto it = name_to_id_.find(name);
if (it == name_to_id_.end()) return;
flutter::EncodableValue value;
if (data) {
switch (data->format) {
case MPV_FORMAT_STRING:
value = flutter::EncodableValue(SanitizeUtf8(data->u.string));
break;
case MPV_FORMAT_FLAG:
value = flutter::EncodableValue(data->u.flag != 0);
break;
case MPV_FORMAT_INT64:
value = flutter::EncodableValue(data->u.int64);
break;
case MPV_FORMAT_DOUBLE:
value = flutter::EncodableValue(data->u.double_);
break;
default:
value = flutter::EncodableValue();
break;
}
}
// mpv owns event node storage; copy the full tree before the callback can
// queue it beyond the current mpv_wait_event result's lifetime.
flutter::EncodableList list;
list.push_back(flutter::EncodableValue(it->second));
list.push_back(value);
list.push_back(NodeToEncodableValue(data));
std::lock_guard<std::mutex> lock(callback_mutex_);
if (event_callback_) {