fix(player): 分离首页与播放页播放上下文
This commit is contained in:
1 parent
440188f261
commit
6667330ce0
8 files changed
+312
-362
No files matched your search
@@ -32,6 +32,33 @@ void main() {
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test('should_keep_only_current_player_when_neighbor_warmup_disabled', () async {
|
||||
final factory = FakeFactory();
|
||||
final session = PlaybackSession(
|
||||
episodes: episodes,
|
||||
factory: factory.create,
|
||||
canPlay: (episode) => episode.isFree,
|
||||
warmNeighborsEnabled: false,
|
||||
);
|
||||
|
||||
await session.select(0, preloadIndex: 1);
|
||||
await session.preloadSettled;
|
||||
expect(factory.created, hasLength(1));
|
||||
expect(factory.live, 1);
|
||||
expect(session.preloadedIndex, isNull);
|
||||
|
||||
final first = factory.created.single;
|
||||
await session.select(1, preloadIndex: 2, previousIndex: 0);
|
||||
await session.preloadSettled;
|
||||
|
||||
expect(first.disposed, isTrue);
|
||||
expect(factory.created, hasLength(2));
|
||||
expect(factory.live, 1);
|
||||
expect(session.livePlayerCount, 1);
|
||||
expect(session.active, same(factory.created.last));
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test('should_release_previous_player_immediately_when_selecting_locked', () async {
|
||||
final lockedEpisodes = [
|
||||
Episode(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user