fix(windows): support structured mpv observations
This commit is contained in:
@@ -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()) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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_) {
|
||||
|
||||
Reference in New Issue
Block a user