From 04f034817ca46c19a32129425b02f27b613569b1 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Mon, 26 Oct 2026 14:01:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(player):=20=E4=BC=98=E5=8C=96=E5=B0=81?= =?UTF-8?q?=E9=9D=A2=E8=BF=87=E6=B8=A1=E4=B8=8E=E6=8D=A2=E9=9B=86=E8=B5=B7?= =?UTF-8?q?=E6=92=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/data/services/playback_session.dart | 5 ++-- .../services/playback_session_controls.dart | 10 +------ .../player/widgets/player_video_surface.dart | 7 +++-- test/playback/playback_preload_test.dart | 9 ++++-- test/player/player_opening_frame_test.dart | 28 +++++++++++++++++++ test/player/player_video_surface_test.dart | 25 +++++++++++++++-- 6 files changed, 65 insertions(+), 19 deletions(-) diff --git a/lib/core/data/services/playback_session.dart b/lib/core/data/services/playback_session.dart index a7266e7..1a603a5 100644 --- a/lib/core/data/services/playback_session.dart +++ b/lib/core/data/services/playback_session.dart @@ -41,7 +41,6 @@ class PlaybackSession extends ChangeNotifier { final Future Function()? waitForFrame; final PlaybackCoordinator _coordinator; final Set<_PlayerSlot> _live = {}; - final Set _pausedEpisodeIds = {}; _PlayerSlot? _active; _PlayerSlot? _next; _PlayerSlot? _previous; @@ -207,9 +206,9 @@ class PlaybackSession extends ChangeNotifier { int? previousIndex, }) async { if (_closed || index < 0 || index >= episodes.length) return; - // 新集默认起播;用户手动暂停过的同一集滑回时继续尊重暂停。 + // 切换到不同剧集时恢复播放;仍停留在同一集时保留用户暂停。 if (!_selected || index != _currentIndex) { - _userWantsPlay = !_pausedEpisodeIds.contains(episodes[index].id); + _userWantsPlay = true; } final request = ++_generation; _selected = true; diff --git a/lib/core/data/services/playback_session_controls.dart b/lib/core/data/services/playback_session_controls.dart index dd9175b..b0f12f1 100644 --- a/lib/core/data/services/playback_session_controls.dart +++ b/lib/core/data/services/playback_session_controls.dart @@ -37,16 +37,8 @@ extension PlaybackSessionControls on PlaybackSession { _syncPlayback(); } - /// 记录用户播放意图;手动暂停的剧集滑回时不再自动起播。 + /// 记录用户播放意图;切换剧集时会在 [PlaybackSession.select] 中恢复播放。 void setUserWantsPlay(bool value) { - if (episodes.isNotEmpty) { - final episodeId = episodes[_currentIndex].id; - if (value) { - _pausedEpisodeIds.remove(episodeId); - } else { - _pausedEpisodeIds.add(episodeId); - } - } _userWantsPlay = value; _syncPlayback(); _notify(); diff --git a/lib/pages/player/widgets/player_video_surface.dart b/lib/pages/player/widgets/player_video_surface.dart index 69890f9..d57551d 100644 --- a/lib/pages/player/widgets/player_video_surface.dart +++ b/lib/pages/player/widgets/player_video_surface.dart @@ -64,9 +64,12 @@ class _PlayerVideoSurfaceState extends State { key: Key('player-fallback-background'), color: AppColors.mediaBlack, ), - if (!reveal && widget.openingFrame != null) - KeyedSubtree( + if (widget.openingFrame != null) + AnimatedOpacity( key: const Key('player-opening-frame'), + opacity: reveal ? 0 : 1, + duration: handoff != null ? Duration.zero : _videoFadeDuration, + curve: Curves.easeOut, child: widget.openingFrame!, ), AnimatedOpacity( diff --git a/test/playback/playback_preload_test.dart b/test/playback/playback_preload_test.dart index 5f9ab14..15bdd23 100644 --- a/test/playback/playback_preload_test.dart +++ b/test/playback/playback_preload_test.dart @@ -174,7 +174,7 @@ void main() { await session.close(); }); - test('should_keep_manual_pause_when_returning_to_same_episode', () async { + test('should_auto_play_previous_after_switching_from_paused_episode', () async { final factory = _Factory(); final session = PlaybackSession( episodes: episodes, @@ -195,13 +195,16 @@ void main() { factory.players.firstWhere((player) => player.id == 'ep-2').playing, isTrue, ); + session.setUserWantsPlay(false); + await session.playbackSettled; + expect(session.userWantsPlay, isFalse); await session.select(1, preloadIndex: 2, previousIndex: 0); await session.preloadSettled; - expect(session.userWantsPlay, isFalse); + expect(session.userWantsPlay, isTrue); expect( factory.players.firstWhere((player) => player.id == 'ep-1').playing, - isFalse, + isTrue, ); await session.close(); }); diff --git a/test/player/player_opening_frame_test.dart b/test/player/player_opening_frame_test.dart index b8b3acb..e65c4da 100644 --- a/test/player/player_opening_frame_test.dart +++ b/test/player/player_opening_frame_test.dart @@ -83,6 +83,34 @@ void main() { expect(find.byKey(const Key('player-opening-poster')), findsOneWidget); }); + + testWidgets('should_reuse_decoded_sprite_when_parent_rebuilds', ( + tester, + ) async { + final loader = _FakeSpriteLoader(); + final cache = OpeningFrameCache(); + addTearDown(cache.clear); + Widget build() => MaterialApp( + home: PlayerOpeningFrame( + thumbnailsUrl: 'https://cdn.test/thumbnails.vtt', + posterUrl: 'https://cdn.test/poster.jpg', + loader: loader, + cache: cache, + ), + ); + + await tester.pumpWidget(build()); + await tester.pumpAndSettle(); + expect(loader.vttCalls, 1); + expect(loader.spriteCalls, hasLength(1)); + + await tester.pumpWidget(build()); + await tester.pumpAndSettle(); + + expect(loader.vttCalls, 1); + expect(loader.spriteCalls, hasLength(1)); + expect(find.byKey(const Key('player-opening-sprite')), findsOneWidget); + }); } class _FakeSpriteLoader implements SpriteThumbnailAssetLoader { diff --git a/test/player/player_video_surface_test.dart b/test/player/player_video_surface_test.dart index 6ec0315..120e76a 100644 --- a/test/player/player_video_surface_test.dart +++ b/test/player/player_video_surface_test.dart @@ -30,6 +30,7 @@ void main() { expect(find.byKey(const Key('player-opening-frame')), findsOneWidget); expect(find.byKey(const Key('fake-opening')), findsOneWidget); + expect(_openingOpacity(tester), 1); expect(_opacity(tester), 0); }); @@ -48,7 +49,7 @@ void main() { expect(find.byKey(const Key('player-opening-frame')), findsNothing); }); - testWidgets('should_fade_video_in_within_200ms_after_first_frame', ( + testWidgets('should_cross_fade_video_over_opening_frame', ( tester, ) async { final controller = await _pumpSurface( @@ -67,7 +68,7 @@ void main() { await tester.pump(); expect(_opacity(tester), 1); - expect(find.byKey(const Key('player-opening-frame')), findsNothing); + expect(find.byKey(const Key('player-opening-frame')), findsOneWidget); expect( tester .widget( @@ -80,8 +81,12 @@ void main() { final halfway = _fadeValue(tester); expect(halfway, greaterThan(0)); expect(halfway, lessThan(1)); + final openingHalfway = _openingFadeValue(tester); + expect(openingHalfway, greaterThan(0)); + expect(openingHalfway, lessThan(1)); await tester.pump(const Duration(milliseconds: 100)); expect(_fadeValue(tester), 1); + expect(_openingFadeValue(tester), 0); }); testWidgets('should_show_video_error_above_retained_fallback', ( @@ -182,6 +187,14 @@ double _opacity(WidgetTester tester) { .opacity; } +double _openingOpacity(WidgetTester tester) { + return tester + .widget( + find.byKey(const Key('player-opening-frame')), + ) + .opacity; +} + double _fadeValue(WidgetTester tester) { final fade = find.descendant( of: find.byKey(const Key('player-video-opacity')), @@ -189,3 +202,11 @@ double _fadeValue(WidgetTester tester) { ); return tester.widget(fade).opacity.value; } + +double _openingFadeValue(WidgetTester tester) { + final fade = find.descendant( + of: find.byKey(const Key('player-opening-frame')), + matching: find.byType(FadeTransition), + ); + return tester.widget(fade).opacity.value; +}