Files
plezy/test/mpv/player_android_subtitle_visibility_test.dart
T
edde746 2b4875d389 fix(player): keep hidden and cycled subtitles off in the next episode
Episode navigation carries the subtitle choice this screen has committed, so
a way of turning subtitles off that the screen never sees is undone by the
next episode.

ExoPlayer has no renderer-level visibility switch, so the player's hide
toggle is emulated by deselecting the track. That emulation lasted until the
next selection: the automatic pass after an episode change put subtitles
straight back on screen while the toggle still read "hidden", and un-hiding
then restored a track id belonging to the episode that had already ended.
Hiding is now sticky across media opens the way mpv's global sub-visibility
is, selections made while hidden become what un-hiding restores, and the
toggle no longer refuses to restore because the hidden track reads as Off.

Cycling subtitles over the native track list — downloads, and items whose
server exposes no subtitle rows — went straight to the track manager, which
owns the player selection and the server write-back but not the committed
choice. The screen records the cycled track now.
2026-08-03 17:07:14 +02:00

147 lines
5.3 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/mpv/mpv.dart';
import 'package:plezy/mpv/player/platform/player_android.dart';
import 'package:plezy/services/settings_service.dart';
import '../test_helpers/mock_player_channels.dart';
import '../test_helpers/prefs.dart';
/// ExoPlayer has no renderer-level subtitle visibility switch, so the player's
/// hide toggle is emulated by deselecting the track. That emulation used to be
/// per-selection: the next automatic selection — an episode advance carrying
/// the previous episode's choice — put subtitles straight back on screen while
/// the toggle still read "hidden", and un-hiding then restored a track id from
/// the episode that had already ended (#1779). mpv keeps `sub-visibility`
/// across files; this pins the same behaviour on ExoPlayer.
Future<void> _withPlayer(Future<void> Function(PlayerAndroid player, _PlayerHarness harness) body) async {
final harness = _PlayerHarness();
await withMockPlayerChannels(
methodChannelName: 'com.plezy/exo_player',
eventChannelName: 'com.plezy/exo_player/events',
methodHandler: harness.handle,
testBody: () async {
final player = PlayerAndroid();
try {
// What actually drives native initialize, and with it the property
// observations the track list arrives through.
await player.requestAudioFocus();
await body(player, harness);
} finally {
await player.dispose();
}
},
);
}
class _PlayerHarness {
final Map<String, int> observations = {};
final List<String> subtitleSelections = [];
Future<Object?> handle(MethodCall call) async {
switch (call.method) {
case 'initialize':
return true;
case 'observeProperty':
final arguments = call.arguments as Map;
observations[arguments['name'] as String] = arguments['id'] as int;
case 'selectSubtitleTrack':
subtitleSelections.add((call.arguments as Map)['trackId'] as String);
}
return null;
}
Future<void> sendTrackList(List<Map<String, Object?>> tracks) async {
final done = Completer<void>();
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'com.plezy/exo_player/events',
const StandardMethodCodec().encodeSuccessEnvelope([observations['track-list'], jsonEncode(tracks)]),
(_) => done.complete(),
);
await done.future;
await Future<void>.delayed(Duration.zero);
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() async {
resetSharedPreferencesForTest();
SettingsService.resetForTesting();
await SettingsService.getInstance();
});
test('hidden subtitles stay hidden when the next episode selects its own track', () async {
await _withPlayer((player, harness) async {
await harness.sendTrackList([
{'type': 'audio', 'id': '1', 'lang': 'eng', 'selected': true},
{'type': 'sub', 'id': '2', 'lang': 'eng', 'selected': true},
]);
expect(player.state.track.subtitle?.id, '2');
await player.setProperty('sub-visibility', 'no');
expect(harness.subtitleSelections, ['no']);
// Episode advance: a new track list, then the carried-over choice.
await harness.sendTrackList([
{'type': 'audio', 'id': '1', 'lang': 'eng', 'selected': true},
{'type': 'sub', 'id': '5', 'lang': 'eng'},
]);
await player.selectSubtitleTrack(const SubtitleTrack(id: '5', language: 'eng'));
expect(harness.subtitleSelections, ['no', 'no']);
});
});
test('un-hiding restores the current media selection, not the one hiding began with', () async {
await _withPlayer((player, harness) async {
await harness.sendTrackList([
{'type': 'sub', 'id': '2', 'lang': 'eng', 'selected': true},
]);
await player.setProperty('sub-visibility', 'no');
await harness.sendTrackList([
{'type': 'sub', 'id': '5', 'lang': 'eng'},
]);
await player.selectSubtitleTrack(const SubtitleTrack(id: '5', language: 'eng'));
await player.setProperty('sub-visibility', 'yes');
expect(harness.subtitleSelections.last, '5');
});
});
test('an explicit off while hidden leaves nothing to restore', () async {
await _withPlayer((player, harness) async {
await harness.sendTrackList([
{'type': 'sub', 'id': '2', 'lang': 'eng', 'selected': true},
]);
await player.setProperty('sub-visibility', 'no');
await player.selectSubtitleTrack(SubtitleTrack.off);
harness.subtitleSelections.clear();
await player.setProperty('sub-visibility', 'yes');
expect(harness.subtitleSelections, isEmpty);
});
});
test('selections pass straight through while subtitles are visible', () async {
await _withPlayer((player, harness) async {
await harness.sendTrackList([
{'type': 'sub', 'id': '2', 'lang': 'eng', 'selected': true},
{'type': 'sub', 'id': '3', 'lang': 'swe'},
]);
await player.selectSubtitleTrack(const SubtitleTrack(id: '3', language: 'swe'));
// A redundant show is a no-op rather than a replayed selection.
await player.setProperty('sub-visibility', 'yes');
expect(harness.subtitleSelections, ['3']);
});
});
}