fix(player): 分离首页与播放页播放上下文

This commit is contained in:
Zhou Weidong committed 2026-10-26 16:37:37 +08:00
1 parent 440188f261
commit 6667330ce0
8 files changed
+312 -362

No files matched your search

+131 -159
View File
@@ -20,9 +20,9 @@ import 'package:fruitshorts/core/data/services/playback_session.dart';
import 'package:fruitshorts/core/data/services/video_player_port.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/pages/auth/auth_view.dart';
import 'package:fruitshorts/pages/player/player_logic.dart';
import 'package:fruitshorts/pages/player/player_view.dart';
import 'package:fruitshorts/pages/player/widgets/player_center_toggle.dart';
import 'package:fruitshorts/pages/player/widgets/player_render_stack.dart';
import 'package:fruitshorts/pages/player/widgets/player_video_surface.dart';
import 'package:fruitshorts/pages/player/widgets/seek_progress_bar.dart';
import 'package:video_player/video_player.dart';
@@ -86,7 +86,7 @@ void main() {
_expectPlayerNavigationBar(tester);
});
testWidgets('should_preserve_recommend_feed_when_full_player_pops', (
testWidgets('should_keep_feed_session_unchanged_when_switching_episode', (
tester,
) async {
final factory = _RouteVideoFactory();
@@ -95,85 +95,86 @@ void main() {
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.pumpAndSettle();
final feedPage = find
.descendant(
of: find.byKey(const Key('recommend-feed-player')),
matching: find.byType(PageView),
)
.first;
await tester.drag(feedPage, const Offset(0, -620));
await tester.pumpAndSettle();
await tester.drag(feedPage, const Offset(0, -620));
await tester.pumpAndSettle();
final recommendPlayer = find.byKey(const Key('recommend-feed-player'));
final renderStack = find.descendant(
of: recommendPlayer,
matching: find.byType(PlayerRenderStack),
final sourceState = tester.state(recommendPlayer);
final sourceLogic = _pageLogic(tester, recommendPlayer);
final sourceIndex = sourceLogic.session.currentIndex;
final sourceDramaId = sourceLogic.currentDramaId;
final sourceEpisodeId = sourceLogic.session.currentEpisode.id;
final feedPlayer = sourceLogic.session.active! as _RouteVideoPlayer;
final feedPlayCalls = feedPlayer.playCalls;
expect(sourceDramaId, 'vengeance-demo');
expect(sourceIndex, 2);
expect(sourceLogic.session.livePlayerCount, 1);
await tester.tap(find.byKey(const Key('home-feed-full-series')));
await tester.pumpAndSettle();
final fullPlayer = find.byType(PlayerPage);
final fullLogic = _pageLogic(tester, fullPlayer);
expect(fullLogic.session, isNot(same(sourceLogic.session)));
expect(feedPlayer.disposed, isFalse);
expect(feedPlayer.playing, isFalse);
for (var index = 0; index < 4; index++) {
await tester.drag(
find.byKey(const Key('player-video-tap')),
const Offset(0, -560),
);
await tester.pumpAndSettle();
}
expect(fullLogic.session.currentEpisode.number, 5);
expect(
fullLogic.queue.entryAt(fullLogic.session.currentIndex).drama.id,
sourceDramaId,
);
expect(recommendPlayer, findsOneWidget);
expect(renderStack, findsOneWidget);
final playerState = tester.state(recommendPlayer);
expect(fullLogic.session.userWantsPlay, isTrue);
expect(sourceLogic.session.currentIndex, sourceIndex);
expect(sourceLogic.currentDramaId, sourceDramaId);
expect(sourceLogic.session.currentEpisode.id, sourceEpisodeId);
expect(sourceLogic.session.active, same(feedPlayer));
expect(feedPlayer.disposed, isFalse);
final requestsBeforeReturn = playback.requests.length;
await tester.tap(find.byKey(const Key('player-back')));
await tester.pumpAndSettle();
final currentSurface = find.descendant(
of: recommendPlayer,
matching: find.byKey(const ValueKey('player-render-slot-current')),
);
final sourceRender = tester.widget<PlayerRenderStack>(renderStack);
final sourceIndex = sourceRender.slots[PlayerRenderSlotRole.current]?.index;
final currentEpisodeId = factory.episodes.first.id;
final controller = factory.created.first.controller;
final requestsBefore = playback.requests.length;
expect(controller.value.position, const Duration(seconds: 5));
expect(
find.descendant(
of: currentSurface,
matching: find.byKey(const Key('player-single-placeholder')),
),
findsNothing,
);
await tester.tap(find.byKey(const Key('home-feed-full-series')));
await tester.pumpAndSettle();
expect(find.byType(PlayerPage), findsOneWidget);
final fullPlayer = tester.widget<PlayerPage>(find.byType(PlayerPage));
expect(fullPlayer.homeMode, isFalse);
expect(fullPlayer.handoff, isNotNull);
expect(fullPlayer.handoff!.sourceIndex, sourceIndex);
expect(
fullPlayer.handoff!.logic.session.currentIndex,
fullPlayer.handoff!.episodeIndex,
);
expect(find.byKey(const Key('home-tabs-bar')), findsNothing);
expect(find.byKey(const Key('recommend-top-nav')), findsNothing);
expect(find.byKey(const Key('home-feed-gesture')), findsNothing);
expect(find.byKey(const Key('player-back')), findsOneWidget);
expect(
factory.episodes.where((episode) => episode.id == currentEpisodeId),
hasLength(1),
);
expect(playback.requests.length, requestsBefore);
expect(controller.value.position, const Duration(seconds: 5));
final fullCurrentSurface = find.descendant(
of: find.byType(PlayerPage),
matching: find.byKey(const ValueKey('player-render-slot-current')),
);
expect(
find.descendant(
of: fullCurrentSurface,
matching: find.byKey(const Key('player-single-placeholder')),
),
findsNothing,
);
expect(
tester.widget<PlayerVideoSurface>(fullCurrentSurface).controller,
same(controller),
);
await tester.tap(find.byKey(const Key('player-back')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
expect(recommendPlayer, findsOneWidget);
expect(renderStack, findsOneWidget);
expect(
factory.episodes.where((episode) => episode.id == currentEpisodeId),
hasLength(1),
);
expect(playback.requests.length, requestsBefore);
expect(controller.value.position, const Duration(seconds: 5));
expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget);
expect(find.byKey(const Key('recommend-top-nav')), findsOneWidget);
expect(tester.state(recommendPlayer), same(playerState));
expect(find.byKey(const Key('recommend-feed-skeleton')), findsNothing);
expect(tester.state(recommendPlayer), same(sourceState));
expect(sourceLogic.session.currentIndex, sourceIndex);
expect(sourceLogic.currentDramaId, sourceDramaId);
expect(sourceLogic.session.currentEpisode.id, sourceEpisodeId);
expect(sourceLogic.session.active, same(feedPlayer));
expect(feedPlayer.disposed, isFalse);
expect(sourceLogic.active, isTrue);
expect(sourceLogic.session.userWantsPlay, isTrue);
expect(sourceLogic.session.canResumeAfterBackground, isTrue);
expect(sourceLogic.session.canAutoAdvance, isTrue);
expect(sourceLogic.session.isLocked, isFalse);
expect(sourceLogic.session.loading, isFalse);
expect(sourceLogic.session.activeSlotActivated, isTrue);
expect(sourceLogic.session.activeSlotPlaying, isTrue);
expect(feedPlayer.seekCalls, 1);
expect(feedPlayer.playCalls, greaterThan(feedPlayCalls));
expect(playback.requests.length, requestsBeforeReturn);
expect(feedPlayer.controller.value.position, Duration.zero);
expect(feedPlayer.playing, isTrue);
expect(
find.descendant(
of: currentSurface,
@@ -183,72 +184,10 @@ void main() {
);
expect(
tester.widget<PlayerVideoSurface>(currentSurface).controller,
same(controller),
);
});
testWidgets('should_swipe_full_player_without_creating_another_session', (
tester,
) async {
final factory = _RouteVideoFactory();
await _pumpApp(tester, playerFactory: factory.create);
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('home-feed-full-series')));
await tester.pumpAndSettle();
final fullPlayer = tester.widget<PlayerPage>(find.byType(PlayerPage));
final handoff = fullPlayer.handoff!;
final dramaId = handoff.seriesDrama.id;
final firstEpisodeId = handoff.logic.session.currentEpisode.id;
final createdBefore = factory.created.length;
await tester.drag(
find.byKey(const Key('player-video-tap')),
const Offset(0, -560),
);
await tester.pumpAndSettle();
expect(find.byType(PlayerPage), findsOneWidget);
expect(factory.created.length, lessThanOrEqualTo(createdBefore + 1));
expect(find.byKey(const Key('player-render-stack')), findsOneWidget);
final switchedEpisode = handoff.logic.session.currentEpisode;
expect(switchedEpisode.id, isNot(firstEpisodeId));
expect(switchedEpisode.number, 2);
expect(
handoff.logic.queue.entryAt(handoff.logic.session.currentIndex).drama.id,
dramaId,
);
expect(handoff.logic.session.userWantsPlay, isTrue);
expect(
factory.episodes.where((episode) => episode.id == switchedEpisode.id),
hasLength(1),
);
final switchedVideo = handoff.logic.session.active! as VideoPlayerPort;
final switchedController = switchedVideo.controller;
await tester.tap(find.byKey(const Key('player-back')));
await tester.pumpAndSettle();
expect(handoff.logic.session.currentIndex, handoff.sourceIndex);
expect(
handoff.logic.queue.entryAt(handoff.sourceIndex).episode.id,
switchedEpisode.id,
);
final sourceCurrentSurface = find.descendant(
of: find.byType(PlayerPage),
matching: find.byKey(const ValueKey('player-render-slot-current')),
);
expect(
tester.widget<PlayerVideoSurface>(sourceCurrentSurface).controller,
same(switchedController),
);
expect(switchedController.value.position, const Duration(seconds: 5));
expect(
find.descendant(
of: sourceCurrentSurface,
matching: find.byKey(const Key('player-single-placeholder')),
),
findsNothing,
same(feedPlayer.controller),
);
expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget);
expect(find.byKey(const Key('recommend-top-nav')), findsOneWidget);
});
testWidgets('should_show_one_paused_overlay_when_entering_full_player', (
@@ -279,7 +218,8 @@ void main() {
});
testWidgets('should_keep_drama_and_episode_indices_separate', (tester) async {
await _pumpApp(tester);
final factory = _RouteVideoFactory();
await _pumpApp(tester, playerFactory: factory.create);
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.pumpAndSettle();
@@ -291,33 +231,34 @@ void main() {
.first;
await tester.drag(feedPage, const Offset(0, -620));
await tester.pumpAndSettle();
final sourceRender = tester.widget<PlayerRenderStack>(
find.byType(PlayerRenderStack),
);
final sourceIndex = sourceRender.slots[PlayerRenderSlotRole.current]?.index;
expect(sourceIndex, isNotNull);
expect(sourceIndex, greaterThan(0));
final feedIndex = sourceIndex!;
final recommendPlayer = find.byKey(const Key('recommend-feed-player'));
final sourceLogic = _pageLogic(tester, recommendPlayer);
final feedIndex = sourceLogic.session.currentIndex;
final feedEpisodeId = sourceLogic.session.currentEpisode.id;
final feedDramaId = sourceLogic.currentDramaId;
expect(feedIndex, 1);
await tester.tap(find.byKey(const Key('home-feed-full-series')));
await tester.pumpAndSettle();
final fullPlayer = tester.widget<PlayerPage>(find.byType(PlayerPage));
final handoff = fullPlayer.handoff!;
expect(handoff.sourceIndex, feedIndex);
expect(handoff.episodeIndex, 0);
expect(handoff.logic.session.currentIndex, handoff.episodeIndex);
expect(
handoff.seriesDrama.id,
handoff.sourceQueue.entryAt(feedIndex).drama.id,
);
expect(
handoff.logic.session.currentEpisode.id,
handoff.sourceEpisodes[feedIndex].id,
final fullLogic = _pageLogic(tester, find.byType(PlayerPage));
await tester.drag(
find.byKey(const Key('player-video-tap')),
const Offset(0, -560),
);
await tester.pumpAndSettle();
expect(fullLogic.session.currentIndex, 1);
expect(fullLogic.session.currentEpisode.number, 2);
expect(fullLogic.queue.entryAt(1).drama.id, feedDramaId);
expect(sourceLogic.session.currentIndex, feedIndex);
expect(sourceLogic.session.currentEpisode.id, feedEpisodeId);
expect(sourceLogic.currentDramaId, feedDramaId);
await tester.tap(find.byKey(const Key('player-back')));
await tester.pumpAndSettle();
expect(sourceLogic.session.currentIndex, feedIndex);
expect(sourceLogic.session.currentEpisode.id, feedEpisodeId);
});
testWidgets('should_render_player_loading_indicators_at_compact_size', (
@@ -1682,6 +1623,11 @@ Future<void> _pumpApp(
if (settle) await tester.pumpAndSettle();
}
PlayerLogic _pageLogic(WidgetTester tester, Finder page) {
final dynamic state = tester.state(page);
return state.logic as PlayerLogic;
}
class _RouteVideoFactory {
final List<_RouteVideoPlayer> created = [];
final List<Episode> episodes = [];
@@ -1697,6 +1643,12 @@ class _RouteVideoFactory {
class _RouteVideoPlayer extends VideoPlayerPort {
_RouteVideoPlayer(super.episode);
bool disposed = false;
int seekCalls = 0;
int playCalls = 0;
bool get playing => controller.value.isPlaying;
@override
Future<void> initialize() async {
controller.value = const VideoPlayerValue(
@@ -1710,6 +1662,7 @@ class _RouteVideoPlayer extends VideoPlayerPort {
@override
Future<void> play() async {
playCalls++;
final value = controller.value;
controller.value = VideoPlayerValue(
duration: value.duration,
@@ -1731,6 +1684,25 @@ class _RouteVideoPlayer extends VideoPlayerPort {
isPlaying: false,
);
}
@override
Future<void> seekTo(Duration position) async {
seekCalls++;
final value = controller.value;
controller.value = VideoPlayerValue(
duration: value.duration,
size: value.size,
position: position,
isInitialized: value.isInitialized,
isPlaying: value.isPlaying,
);
}
@override
Future<void> dispose() async {
disposed = true;
await super.dispose();
}
}
class _RecordingPlayer implements PlayerPort {