feat(player): support playback speeds up to 8x

close #1545
This commit is contained in:
edde746
2026-07-26 04:24:55 +02:00
parent 2a25f21e27
commit 3b1e71b3fa
12 changed files with 194 additions and 16 deletions
@@ -207,6 +207,35 @@ void main() {
});
});
testWidgets('speed increase reaches the supported 8x boundary', (tester) async {
final service = await KeyboardShortcutsService.getInstance();
addTearDown(service.dispose);
await service.setHotkey('speed_increase', const HotKey(key: PhysicalKeyboardKey.f12));
final player = _FakePlayer(rate: 7.75);
final result = service.handleVideoPlayerKeyEvent(
const KeyDownEvent(
physicalKey: PhysicalKeyboardKey.f12,
logicalKey: LogicalKeyboardKey.f12,
timeStamp: Duration.zero,
),
player,
null,
null,
null,
null,
null,
null,
canControlPlayback: true,
canNavigateMediaItems: true,
);
await tester.pump();
expect(result, KeyEventResult.handled);
expect(player.rateChanges, [8.0]);
expect(SettingsService.instance.read(SettingsService.defaultPlaybackSpeed), 8.0);
});
testWidgets('Ctrl+S takes a screenshot once while held', (tester) async {
final service = await KeyboardShortcutsService.getInstance();
addTearDown(service.dispose);
@@ -692,11 +721,13 @@ void main() {
}
class _FakePlayer implements Player {
_FakePlayer({this.volume = 100});
_FakePlayer({this.volume = 100, this.rate = 1});
final commands = <List<String>>[];
final volumeChanges = <double>[];
final rateChanges = <double>[];
double volume;
double rate;
@override
Future<void> command(List<String> args) async {
@@ -704,7 +735,7 @@ class _FakePlayer implements Player {
}
@override
PlayerState get state => PlayerState(volume: volume);
PlayerState get state => PlayerState(volume: volume, rate: rate);
@override
Future<void> setVolume(double volume) async {
@@ -712,6 +743,12 @@ class _FakePlayer implements Player {
volumeChanges.add(volume);
}
@override
Future<void> setRate(double rate) async {
this.rate = rate;
rateChanges.add(rate);
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
@@ -468,7 +468,7 @@ void main() {
for (final targetMs in [-1, durationMs + 1]) {
h.coordinator.onControlRequest('guest', ControlRequest(kind: ControlRequestKind.seek, positionMs: targetMs));
}
for (final rate in [0.25 - 0.000001, 4.0 + 0.000001, double.nan, double.infinity, double.negativeInfinity]) {
for (final rate in [0.25 - 0.000001, 8.0 + 0.000001, double.nan, double.infinity, double.negativeInfinity]) {
h.coordinator.onControlRequest('guest', ControlRequest(kind: ControlRequestKind.rate, rate: rate));
}
async.flushMicrotasks();
@@ -504,7 +504,7 @@ void main() {
expect(h.last.actorPeerId, 'guest');
expect(h.last.actionHint, PlaybackActionHint.seek);
}
for (final rate in [0.25, 4.0]) {
for (final rate in [0.25, 8.0]) {
h.coordinator.onControlRequest('guest', ControlRequest(kind: ControlRequestKind.rate, rate: rate));
async.flushMicrotasks();
expect(h.last.rate, rate);
@@ -512,7 +512,7 @@ void main() {
expect(h.last.actionHint, PlaybackActionHint.rate);
}
expect(h.player.commandLog, ['seek:0', 'seek:$durationMs', 'rate:0.25', 'rate:4.0']);
expect(h.player.commandLog, ['seek:0', 'seek:$durationMs', 'rate:0.25', 'rate:8.0']);
expect(h.last.seq, seqBefore + 4);
expect(actions, [
('guest', PlaybackActionHint.seek),
@@ -237,7 +237,7 @@ void main() {
);
room.guestService.sendTo(
'host',
wireControl(const ControlRequest(kind: ControlRequestKind.rate, rate: 4.000001)),
wireControl(const ControlRequest(kind: ControlRequestKind.rate, rate: 8.000001)),
);
async.flushMicrotasks();
+58 -1
View File
@@ -11,6 +11,7 @@ import 'package:plezy/screens/settings/subtitle_styling_screen.dart';
import 'package:plezy/services/sleep_timer_service.dart';
import 'package:plezy/services/settings_service.dart';
import 'package:plezy/theme/mono_tokens.dart';
import 'package:plezy/widgets/overlay_sheet.dart';
import 'package:plezy/widgets/video_controls/sheets/video_settings_sheet.dart';
import '../test_helpers/prefs.dart';
@@ -78,6 +79,31 @@ void main() {
}
});
testWidgets('offers requested playback speeds through 8x and persists the selection', (tester) async {
final appliedRates = <double>[];
final player = _FakeSettingsPlayer(
onSetRate: (rate) async {
appliedRates.add(rate);
},
);
await _pumpHostedSheet(tester, player);
await tester.tap(find.text('Playback Speed'));
await tester.pumpAndSettle();
final scrollable = find.byType(Scrollable).last;
for (final label in ['3.5x', '4x', '4.5x', '5x', '6x', '7x', '8x']) {
await tester.scrollUntilVisible(find.text(label), 200, scrollable: scrollable);
expect(find.text(label), findsOneWidget);
}
await tester.tap(find.text('8x'));
await tester.pumpAndSettle();
expect(appliedRates, [8.0]);
expect(SettingsService.instance.read(SettingsService.defaultPlaybackSpeed), 8.0);
});
testWidgets('localizes every ASS subtitle override enum label', (tester) async {
LocaleSettings.setLocaleSync(AppLocale.ru);
await tester.pumpWidget(
@@ -178,8 +204,33 @@ Future<void> _pumpSheet(
await tester.pumpAndSettle();
}
Future<void> _pumpHostedSheet(WidgetTester tester, Player player) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(extensions: const [_testTokens]),
home: OverlaySheetHost(
child: Scaffold(
body: Builder(
builder: (context) => TextButton(
onPressed: () => unawaited(
OverlaySheetController.of(context).show<void>(
builder: (_) =>
VideoSettingsSheet(player: player, audioSyncOffset: 0, subtitleSyncOffset: 0, canControl: true),
),
),
child: const Text('Open settings'),
),
),
),
),
),
);
await tester.tap(find.text('Open settings'));
await tester.pumpAndSettle();
}
class _FakeSettingsPlayer implements Player {
_FakeSettingsPlayer({this.onSetProperty})
_FakeSettingsPlayer({this.onSetProperty, this.onSetRate})
: _streams = PlayerStreams(
playing: const Stream<bool>.empty(),
completed: const Stream<bool>.empty(),
@@ -203,6 +254,7 @@ class _FakeSettingsPlayer implements Player {
final PlayerStreams _streams;
final Future<void> Function(String name, String value)? onSetProperty;
final Future<void> Function(double rate)? onSetRate;
@override
PlayerState get state => const PlayerState();
@@ -221,6 +273,11 @@ class _FakeSettingsPlayer implements Player {
return onSetProperty?.call(name, value) ?? Future<void>.value();
}
@override
Future<void> setRate(double rate) {
return onSetRate?.call(rate) ?? Future<void>.value();
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}