fix(player): 记住拖动后的实际播放位置
This commit is contained in:
1 parent
47ca21406a
commit
5268adc999
8 files changed
+352
-67
No files matched your search
@@ -4,6 +4,42 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/services/playback_position_cache.dart';
|
||||
|
||||
void main() {
|
||||
test('should_clamp_invalid_or_out_of_range_position_when_caching', () {
|
||||
expect(
|
||||
clampPlaybackPosition(
|
||||
const Duration(seconds: -1),
|
||||
const Duration(seconds: 10),
|
||||
),
|
||||
Duration.zero,
|
||||
);
|
||||
expect(
|
||||
clampPlaybackPosition(
|
||||
const Duration(seconds: 12),
|
||||
const Duration(seconds: 10),
|
||||
),
|
||||
const Duration(seconds: 10),
|
||||
);
|
||||
expect(
|
||||
clampPlaybackPosition(
|
||||
const Duration(seconds: 5),
|
||||
const Duration(seconds: 10),
|
||||
),
|
||||
const Duration(seconds: 5),
|
||||
);
|
||||
|
||||
final cache = PlaybackPositionCache();
|
||||
cache.save(
|
||||
episodeId: 'episode-3',
|
||||
dramaId: 'drama-a',
|
||||
position: const Duration(minutes: 4),
|
||||
duration: const Duration(minutes: 3),
|
||||
);
|
||||
expect(
|
||||
cache.read('episode-3', dramaId: 'drama-a')?.position,
|
||||
const Duration(minutes: 3),
|
||||
);
|
||||
});
|
||||
|
||||
test('should_store_and_read_recent_position_when_drama_matches', () {
|
||||
final now = DateTime(2026, 10, 22, 12);
|
||||
final cache = PlaybackPositionCache(now: () => now);
|
||||
|
||||
@@ -218,6 +218,75 @@ void main() {
|
||||
logic.dispose();
|
||||
await _drain(logic);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'should_cache_actual_position_after_seek_completes',
|
||||
(tester) async {
|
||||
late BuildContext context;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (pageContext) {
|
||||
context = pageContext;
|
||||
return const Scaffold(body: SizedBox.expand());
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
final cache = PlaybackPositionCache();
|
||||
final history = FakeWatchHistory();
|
||||
final factory = _VideoRecordingFactory();
|
||||
late PlayerLogic logic;
|
||||
await tester.runAsync(() async {
|
||||
logic = _logic(history, positionCache: cache);
|
||||
logic.attach(
|
||||
PlayerPage(drama: _drama('signal'), initialIndex: 0),
|
||||
isMounted: () => true,
|
||||
);
|
||||
logic.start(
|
||||
factory: factory.create,
|
||||
coordinator: PlaybackCoordinator(),
|
||||
);
|
||||
await _drain(logic);
|
||||
});
|
||||
final player = logic.session.active! as _VideoRecordingPlayer;
|
||||
logic.beginScrub(player);
|
||||
|
||||
await tester.runAsync(
|
||||
() => logic.seekTo(
|
||||
context,
|
||||
player,
|
||||
const Duration(seconds: 42),
|
||||
),
|
||||
);
|
||||
await tester.runAsync(() => logic.pendingWrite);
|
||||
|
||||
expect(
|
||||
cache.read('signal-1', dramaId: 'signal')?.position,
|
||||
const Duration(seconds: 42),
|
||||
);
|
||||
expect(history.writes.last, 'signal-1|42000|false');
|
||||
|
||||
logic.beginScrub(player);
|
||||
await tester.runAsync(
|
||||
() => logic.seekTo(
|
||||
context,
|
||||
player,
|
||||
const Duration(minutes: 2),
|
||||
),
|
||||
);
|
||||
await tester.runAsync(() => logic.pendingWrite);
|
||||
|
||||
expect(player.controller.value.position, const Duration(minutes: 1));
|
||||
expect(
|
||||
cache.read('signal-1', dramaId: 'signal')?.position,
|
||||
const Duration(minutes: 1),
|
||||
);
|
||||
expect(history.writes.last, 'signal-1|60000|false');
|
||||
logic.dispose();
|
||||
await tester.pump();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('should_play_local_file_without_ticket_when_episode_is_downloaded',
|
||||
@@ -1257,6 +1326,18 @@ class _VideoRecordingPlayer extends VideoPlayerPort {
|
||||
@override
|
||||
Future<void> pause() async {}
|
||||
|
||||
@override
|
||||
Future<void> seekTo(Duration position) async {
|
||||
final current = controller.value;
|
||||
controller.value = VideoPlayerValue(
|
||||
duration: current.duration,
|
||||
size: current.size,
|
||||
position: position,
|
||||
isInitialized: current.isInitialized,
|
||||
isPlaying: current.isPlaying,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
if (disposed) return;
|
||||
|
||||
@@ -48,7 +48,7 @@ void main() {
|
||||
channel: const MethodChannel('test/player_open_detail/pip'),
|
||||
),
|
||||
);
|
||||
final factory = _RecordingFactory();
|
||||
final factory = _VideoRecordingFactory();
|
||||
logic.attach(
|
||||
PlayerPage(drama: drama, initialIndex: 0),
|
||||
isMounted: () => true,
|
||||
@@ -59,15 +59,8 @@ void main() {
|
||||
final active = logic.session.active;
|
||||
final index = logic.session.currentIndex;
|
||||
final player = factory.created.first;
|
||||
final playsBefore = player.plays;
|
||||
final tracked = VideoPlayerPort(drama.episodes.first);
|
||||
tracked.controller.value = const VideoPlayerValue(
|
||||
duration: Duration(minutes: 5),
|
||||
position: Duration(seconds: 37),
|
||||
isInitialized: true,
|
||||
);
|
||||
logic
|
||||
..trackedPlayer = tracked
|
||||
..trackedPlayer = player
|
||||
..trackedEpisode = drama.episodes.first
|
||||
..trackedDrama = drama;
|
||||
|
||||
@@ -96,22 +89,31 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
logic.beginScrub(player);
|
||||
await logic.seekTo(
|
||||
pageContext,
|
||||
player,
|
||||
const Duration(minutes: 1, seconds: 22),
|
||||
);
|
||||
|
||||
unawaited(logic.openDetail(pageContext));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
await _pumpSession(tester);
|
||||
await tester.runAsync(() => logic.session.playbackSettled);
|
||||
|
||||
expect(find.byType(DramaDetailPage), findsOneWidget);
|
||||
expect(logic.session.active, same(active));
|
||||
expect(logic.session.currentIndex, index);
|
||||
expect(logic.session.userWantsPlay, isTrue);
|
||||
expect(player.pauses, 1);
|
||||
expect(player.plays, playsBefore);
|
||||
expect(tracked.controller.value.position, const Duration(seconds: 37));
|
||||
expect(history.writes, ['signal-1|37000|false']);
|
||||
expect(
|
||||
player.controller.value.position,
|
||||
const Duration(minutes: 1, seconds: 22),
|
||||
);
|
||||
expect(history.writes, ['signal-1|82000|false', 'signal-1|82000|false']);
|
||||
expect(
|
||||
positionCache.read('signal-1', dramaId: 'signal')?.position,
|
||||
const Duration(seconds: 37),
|
||||
const Duration(minutes: 1, seconds: 22),
|
||||
);
|
||||
|
||||
await tester.pageBack();
|
||||
@@ -123,11 +125,14 @@ void main() {
|
||||
expect(logic.session.active, same(active));
|
||||
expect(logic.session.currentIndex, index);
|
||||
expect(logic.session.userWantsPlay, isTrue);
|
||||
expect(player.plays, playsBefore + 1);
|
||||
expect(tracked.controller.value.position, const Duration(seconds: 37));
|
||||
expect(
|
||||
player.controller.value.position,
|
||||
const Duration(minutes: 1, seconds: 22),
|
||||
);
|
||||
expect(history.writes, [
|
||||
'signal-1|37000|false',
|
||||
'signal-1|37000|false',
|
||||
'signal-1|82000|false',
|
||||
'signal-1|82000|false',
|
||||
'signal-1|82000|false',
|
||||
]);
|
||||
|
||||
logic
|
||||
@@ -136,7 +141,6 @@ void main() {
|
||||
..trackedDrama = null
|
||||
..dispose();
|
||||
await _pumpSession(tester);
|
||||
await tracked.dispose();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -164,29 +168,64 @@ Future<void> _pumpSession(WidgetTester tester) async {
|
||||
}
|
||||
}
|
||||
|
||||
class _RecordingFactory {
|
||||
final List<_RecordingPlayer> created = [];
|
||||
class _VideoRecordingFactory {
|
||||
final List<_VideoRecordingPlayer> created = [];
|
||||
|
||||
PlayerPort create(Episode episode) {
|
||||
final player = _RecordingPlayer();
|
||||
final player = _VideoRecordingPlayer(episode);
|
||||
created.add(player);
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
class _RecordingPlayer implements PlayerPort {
|
||||
int plays = 0;
|
||||
int pauses = 0;
|
||||
class _VideoRecordingPlayer extends VideoPlayerPort {
|
||||
_VideoRecordingPlayer(super.episode);
|
||||
|
||||
@override
|
||||
Future<void> initialize() async {}
|
||||
Future<void> initialize() async {
|
||||
controller.value = const VideoPlayerValue(
|
||||
duration: Duration(minutes: 5),
|
||||
size: Size(1280, 720),
|
||||
position: Duration(seconds: 1),
|
||||
isInitialized: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() async => plays++;
|
||||
Future<void> play() async {
|
||||
_setPlaying(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async => pauses++;
|
||||
Future<void> pause() async {
|
||||
_setPlaying(false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {}
|
||||
Future<void> seekTo(Duration position) async {
|
||||
final current = controller.value;
|
||||
controller.value = VideoPlayerValue(
|
||||
duration: current.duration,
|
||||
size: current.size,
|
||||
position: position,
|
||||
isInitialized: current.isInitialized,
|
||||
isPlaying: current.isPlaying,
|
||||
);
|
||||
}
|
||||
|
||||
void _setPlaying(bool playing) {
|
||||
final current = controller.value;
|
||||
controller.value = VideoPlayerValue(
|
||||
duration: current.duration,
|
||||
size: current.size,
|
||||
position: current.position,
|
||||
isInitialized: current.isInitialized,
|
||||
isPlaying: playing,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await controller.dispose();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user