refactor(mpv): unify property decoding
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import 'dart:convert';
|
||||
|
||||
/// Decodes an mpv node delivered either as a platform-channel value or JSON.
|
||||
abstract final class MpvNodeDecoder {
|
||||
static List<Object?>? decodeList(Object? value) {
|
||||
final decoded = _decode(value);
|
||||
return decoded is List<Object?> ? decoded : null;
|
||||
}
|
||||
|
||||
static Map<Object?, Object?>? decodeMap(Object? value) {
|
||||
final decoded = _decode(value);
|
||||
return decoded is Map<Object?, Object?> ? decoded : null;
|
||||
}
|
||||
|
||||
static Object? _decode(Object? value) {
|
||||
if (value is List || value is Map) return value;
|
||||
if (value is! String || value.isEmpty) return null;
|
||||
|
||||
try {
|
||||
return jsonDecode(value);
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
@@ -11,6 +10,7 @@ import '../../utils/app_logger.dart';
|
||||
import '../../utils/track_label_builder.dart';
|
||||
import '../font_loader.dart';
|
||||
import '../models.dart';
|
||||
import 'mpv_node_decoder.dart';
|
||||
import 'player.dart';
|
||||
import 'player_state.dart';
|
||||
import 'player_stream_controllers.dart';
|
||||
@@ -258,17 +258,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
||||
break;
|
||||
|
||||
case 'track-list':
|
||||
List? trackList;
|
||||
if (value is List) {
|
||||
trackList = value;
|
||||
} else if (value is String && value.isNotEmpty) {
|
||||
try {
|
||||
final parsed = jsonDecode(value);
|
||||
if (parsed is List) trackList = parsed;
|
||||
} catch (e) {
|
||||
appLogger.d('Player: track-list parse failed', error: e);
|
||||
}
|
||||
}
|
||||
final trackList = MpvNodeDecoder.decodeList(value);
|
||||
if (trackList != null) {
|
||||
final result = parseTrackList(trackList);
|
||||
_state = _state.copyWith(tracks: result.tracks);
|
||||
@@ -298,17 +288,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
||||
break;
|
||||
|
||||
case 'audio-device-list':
|
||||
List? deviceList;
|
||||
if (value is List) {
|
||||
deviceList = value;
|
||||
} else if (value is String && value.isNotEmpty) {
|
||||
try {
|
||||
final parsed = jsonDecode(value);
|
||||
if (parsed is List) deviceList = parsed;
|
||||
} catch (e) {
|
||||
appLogger.d('Player: device-list parse failed', error: e);
|
||||
}
|
||||
}
|
||||
final deviceList = MpvNodeDecoder.decodeList(value);
|
||||
if (deviceList != null) {
|
||||
final devices = deviceList
|
||||
.whereType<Map>()
|
||||
@@ -331,19 +311,13 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
||||
|
||||
/// Parse demuxer-cache-state property to extract seekable ranges and buffer end.
|
||||
void _handleDemuxerCacheState(dynamic value) {
|
||||
Map? cacheState;
|
||||
if (value is Map) {
|
||||
cacheState = value;
|
||||
} else if (value is String && value.isNotEmpty) {
|
||||
if (value is String && value.isNotEmpty) {
|
||||
// Throttle JSON parsing to avoid ANR on low-end devices
|
||||
final nowMs = _throttleSw.elapsedMilliseconds;
|
||||
if (nowMs - _lastCacheStateMs < 250) return;
|
||||
_lastCacheStateMs = nowMs;
|
||||
try {
|
||||
final parsed = jsonDecode(value);
|
||||
if (parsed is Map) cacheState = parsed;
|
||||
} catch (_) {}
|
||||
}
|
||||
final cacheState = MpvNodeDecoder.decodeMap(value);
|
||||
if (cacheState == null) return;
|
||||
|
||||
// Extract cache-end for the single buffer duration (replaces demuxer-cache-time)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/mpv/player/mpv_node_decoder.dart';
|
||||
|
||||
void main() {
|
||||
const nestedList = [
|
||||
{
|
||||
'type': 'audio',
|
||||
'metadata': {
|
||||
'values': [true, 2, 3.5, null],
|
||||
},
|
||||
},
|
||||
];
|
||||
const nestedMap = {
|
||||
'cache-end': 12.5,
|
||||
'seekable-ranges': [
|
||||
{'start': 1, 'end': 9.25},
|
||||
],
|
||||
};
|
||||
|
||||
final cases = <({String name, Object? input, Object? list, Object? map})>[
|
||||
(name: 'nested structured list', input: nestedList, list: nestedList, map: null),
|
||||
(name: 'nested structured map', input: nestedMap, list: null, map: nestedMap),
|
||||
(
|
||||
name: 'valid list JSON',
|
||||
input: '[{"type":"audio","metadata":{"values":[true,2,3.5,null]}}]',
|
||||
list: nestedList,
|
||||
map: null,
|
||||
),
|
||||
(
|
||||
name: 'valid map JSON',
|
||||
input: '{"cache-end":12.5,"seekable-ranges":[{"start":1,"end":9.25}]}',
|
||||
list: null,
|
||||
map: nestedMap,
|
||||
),
|
||||
(name: 'malformed JSON', input: '{not-json', list: null, map: null),
|
||||
(name: 'JSON scalar', input: '42', list: null, map: null),
|
||||
(name: 'structured scalar', input: 42, list: null, map: null),
|
||||
(name: 'empty string', input: '', list: null, map: null),
|
||||
(name: 'null', input: null, list: null, map: null),
|
||||
(name: 'JSON null', input: 'null', list: null, map: null),
|
||||
(name: 'empty structured list', input: const [], list: const [], map: null),
|
||||
(name: 'empty structured map', input: const {}, list: null, map: const {}),
|
||||
(name: 'empty JSON list', input: '[]', list: const [], map: null),
|
||||
(name: 'empty JSON map', input: '{}', list: null, map: const {}),
|
||||
];
|
||||
|
||||
for (final testCase in cases) {
|
||||
test(testCase.name, () {
|
||||
expect(MpvNodeDecoder.decodeList(testCase.input), testCase.list);
|
||||
expect(MpvNodeDecoder.decodeMap(testCase.input), testCase.map);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -101,17 +101,29 @@ void main() {
|
||||
{'start': 1, 'end': 9.25},
|
||||
],
|
||||
});
|
||||
await sendObservation('audio-device-list', const [
|
||||
{'name': 'speakers', 'description': 'Main speakers'},
|
||||
]);
|
||||
|
||||
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));
|
||||
expect(player.state.audioDevices.single.name, 'speakers');
|
||||
|
||||
// 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');
|
||||
for (final invalid in [null, '{not-json', 42]) {
|
||||
await sendObservation('track-list', invalid);
|
||||
await sendObservation('demuxer-cache-state', invalid);
|
||||
await sendObservation('audio-device-list', invalid);
|
||||
|
||||
expect(player.state.tracks.audio.single.id, '7');
|
||||
expect(player.state.buffer, const Duration(milliseconds: 12500));
|
||||
expect(player.state.bufferRanges.single.end, const Duration(milliseconds: 9250));
|
||||
expect(player.state.audioDevices.single.name, 'speakers');
|
||||
}
|
||||
} finally {
|
||||
await player.dispose();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user