From 37353dac15565fa8d58495c59dadf545864af5ae Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Mon, 26 Oct 2026 21:25:38 +0800 Subject: [PATCH] =?UTF-8?q?fix(player):=20=E8=A7=82=E7=9C=8B=E5=85=A8?= =?UTF-8?q?=E9=9B=86=E8=BF=9B=E5=85=A5=E5=90=8E=E8=87=AA=E5=8A=A8=E6=92=AD?= =?UTF-8?q?=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/services/playback_neighbors.dart | 18 ++++++++- lib/core/data/services/playback_session.dart | 40 +++++++++++++++++++ lib/pages/player/player_logic.dart | 2 + lib/pages/player/player_view.dart | 33 +++++++++++++-- test/player/player_fullscreen_test.dart | 39 +++++++++++++++++- 5 files changed, 126 insertions(+), 6 deletions(-) diff --git a/lib/core/data/services/playback_neighbors.dart b/lib/core/data/services/playback_neighbors.dart index 2d54d46..c5c9748 100644 --- a/lib/core/data/services/playback_neighbors.dart +++ b/lib/core/data/services/playback_neighbors.dart @@ -13,8 +13,18 @@ class PlaybackCoordinator { Future _acquire(PlaybackSession session, bool Function() valid) { final result = _handoff.then((_) async { if (!valid()) return false; + final previous = _owner; + if (kDebugMode) { + debugPrint( + '[player-coordinator] phase=acquire ' + 'session=${identityHashCode(session)} owner=${session.owner.name} ' + 'previous=${previous == null ? '-' : identityHashCode(previous)} ' + 'previousOwner=${previous?.owner.name ?? '-'} ' + 'previousLive=${previous?.livePlayerCount ?? 0} ' + 'same=${identical(previous, session)}', + ); + } if (_owner != session) { - final previous = _owner; if (previous != null && previous.owner != session.owner) { if (kDebugMode) { debugPrint( @@ -31,6 +41,12 @@ class PlaybackCoordinator { if (!valid()) return false; _owner = session; } + if (kDebugMode) { + debugPrint( + '[player-coordinator] phase=acquired ' + 'session=${identityHashCode(session)} live=${session.livePlayerCount}', + ); + } return true; }); _handoff = result.then((_) {}); diff --git a/lib/core/data/services/playback_session.dart b/lib/core/data/services/playback_session.dart index 205a6cd..9480288 100644 --- a/lib/core/data/services/playback_session.dart +++ b/lib/core/data/services/playback_session.dart @@ -84,6 +84,11 @@ class PlaybackSession extends ChangeNotifier { bool get canAutoAdvance => _mayPlay && !_loading && active != null; bool get isLocked => !canPlay(currentEpisode); String? get error => _error; + bool get debugAppResumed => _appResumed; + bool get debugRouteVisible => _routeVisible; + bool get debugOverlayOpen => _overlayOpen; + bool get debugVisible => _visible; + bool get debugMayPlay => _mayPlay; // Includes initialization and asynchronous disposal, until native release ends. int get livePlayerCount => _live.length; bool get activeSlotActivated => _active?.activated ?? false; @@ -254,6 +259,16 @@ class PlaybackSession extends ChangeNotifier { bool? userWantsPlay, }) async { if (_closed || index < 0 || index >= episodes.length) return; + if (kDebugMode) { + debugPrint( + '[player-session] phase=select-enter session=${identityHashCode(this)} ' + 'owner=${owner.name} index=$index user=$_userWantsPlay ' + 'selected=$_selected current=$_currentIndex ' + 'appResumed=$_appResumed routeVisible=$_routeVisible ' + 'overlayOpen=$_overlayOpen visible=$_visible mayPlay=$_mayPlay ' + 'live=${_live.length}', + ); + } // 切换到不同剧集时恢复播放;仍停留在同一集时保留用户暂停。 if (userWantsPlay != null) { _userWantsPlay = userWantsPlay; @@ -465,6 +480,22 @@ class PlaybackSession extends ChangeNotifier { Future _applyPlayback(_PlayerSlot slot) async { if (_closed || _active != slot || !slot.activated) return; final shouldPlay = _mayPlay && slot.index == _currentIndex && !isLocked; + final video = slot.player is VideoPlayerPort + ? slot.player as VideoPlayerPort + : null; + if (kDebugMode) { + debugPrint( + '[player-session] phase=apply-playback ' + 'session=${identityHashCode(this)} owner=${owner.name} ' + 'index=${slot.index} shouldPlay=$shouldPlay user=$_userWantsPlay ' + 'appResumed=$_appResumed routeVisible=$_routeVisible ' + 'overlayOpen=$_overlayOpen locked=$isLocked ' + 'player=${identityHashCode(slot.player)} ' + 'controller=${video == null ? '-' : identityHashCode(video.controller)} ' + 'isPlaying=${video?.controller.value.isPlaying ?? '-'} ' + 'slotPlaying=${slot.playing}', + ); + } if (shouldPlay == slot.playing) return; if (shouldPlay) { await slot.player.play(); @@ -472,6 +503,15 @@ class PlaybackSession extends ChangeNotifier { await slot.player.pause(); } slot.playing = shouldPlay; + if (kDebugMode) { + debugPrint( + '[player-session] phase=apply-playback-result ' + 'session=${identityHashCode(this)} index=${slot.index} ' + 'shouldPlay=$shouldPlay ' + 'isPlaying=${video?.controller.value.isPlaying ?? '-'} ' + 'slotPlaying=${slot.playing}', + ); + } } void _syncPlayback() { diff --git a/lib/pages/player/player_logic.dart b/lib/pages/player/player_logic.dart index ac76aa9..86fd109 100644 --- a/lib/pages/player/player_logic.dart +++ b/lib/pages/player/player_logic.dart @@ -249,6 +249,8 @@ class PlayerLogic extends ChangeNotifier { '[feed-session] phase=$phase session=${identityHashCode(session)} ' 'index=${session.currentIndex} episode=${session.currentEpisode.id} ' 'controller=${video == null ? '-' : identityHashCode(video.controller)} ' + 'user=${session.userWantsPlay} routeVisible=${session.debugRouteVisible} ' + 'appResumed=${session.debugAppResumed} overlayOpen=${session.debugOverlayOpen} ' 'live=${session.livePlayerCount}', ); } diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index b3c2fd3..e2feb43 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -148,6 +148,7 @@ class _PlayerPageState extends State bool _loadingRemoved = true; bool _loadingFadeScheduled = false; bool _loggedLoadingUiVisible = false; + bool _loggedStartConditions = false; final PlayerHeartOverlayController _heartOverlayController = PlayerHeartOverlayController(); final Set _precachedPosterUrls = {}; @@ -160,6 +161,7 @@ class _PlayerPageState extends State void initState() { super.initState(); final handoff = widget.handoff; + final initialUserWantsPlay = handoff?.sourceUserWantsPlay ?? true; logic = PlayerBinding().create( history: context.read(), playback: context.read(), @@ -179,8 +181,21 @@ class _PlayerPageState extends State coordinator: widget.embedded ? context.read() : PlaybackCoordinator(), - initialUserWantsPlay: handoff?.sourceUserWantsPlay ?? true, + initialUserWantsPlay: initialUserWantsPlay, ); + if (kDebugMode) { + debugPrint( + '[player-start] phase=initialized ' + 'session=${identityHashCode(logic.session)} ' + 'initialUserWantsPlay=$initialUserWantsPlay ' + 'sessionUserWantsPlay=${logic.session.userWantsPlay} ' + 'active=${logic.active} embedded=${widget.embedded} ' + 'appResumed=${logic.session.debugAppResumed} ' + 'routeVisible=${logic.session.debugRouteVisible} ' + 'overlayOpen=${logic.session.debugOverlayOpen} ' + 'live=${logic.session.livePlayerCount}', + ); + } _pageController = logic.pages; if (!widget.embedded && !logic.isEpisodeLocked(logic.session.currentEpisode)) { @@ -575,6 +590,19 @@ class _PlayerPageState extends State Widget _buildPlayer(BuildContext context) { final interactions = context.watch(); final l10n = AppLocalizations.of(context)!; + if (kDebugMode && !_loggedStartConditions) { + _loggedStartConditions = true; + debugPrint( + '[player-start] phase=first-build ' + 'session=${identityHashCode(logic.session)} ' + 'tickerMode=${TickerMode.valuesOf(context).enabled} ' + 'routeVisible=${logic.session.debugRouteVisible} ' + 'appResumed=${logic.session.debugAppResumed} ' + 'overlayOpen=${logic.session.debugOverlayOpen} ' + 'visible=${logic.session.debugVisible} ' + 'mayPlay=${logic.session.debugMayPlay}', + ); + } final indices = logic.visibleIndices; final activePort = logic.session.active; final activeVideo = activePort is VideoPlayerPort ? activePort : null; @@ -1341,8 +1369,7 @@ class _PlayerPageState extends State seriesEpisodes.insert(0, sourceEpisode); episodeIndex = 0; } - final sourceUserWantsPlay = logic.session.userWantsPlay; - final handoff = PlayerPageHandoff(sourceUserWantsPlay: sourceUserWantsPlay); + final handoff = PlayerPageHandoff(sourceUserWantsPlay: true); _logFullPlayerRoute('present'); setState(() => _presentationActive = true); final route = Navigator.of(context).push( diff --git a/test/player/player_fullscreen_test.dart b/test/player/player_fullscreen_test.dart index 53d4b6f..b967a9f 100644 --- a/test/player/player_fullscreen_test.dart +++ b/test/player/player_fullscreen_test.dart @@ -229,7 +229,7 @@ void main() { expect(find.byKey(const Key('recommend-top-nav')), findsOneWidget); }); - testWidgets('should_show_one_paused_overlay_when_entering_full_player', ( + testWidgets('should_autoplay_full_player_and_restore_paused_feed', ( tester, ) async { final factory = _RouteVideoFactory(); @@ -245,7 +245,9 @@ void main() { await tester.pumpAndSettle(); expect(_fullPlayerFinder(), findsOneWidget); - expect(find.byKey(const Key('player-play-toggle')), findsOneWidget); + final fullLogic = _pageLogic(tester, _fullPlayerFinder()); + expect(fullLogic.session.userWantsPlay, isTrue); + expect(find.byKey(const Key('player-play-toggle')), findsNothing); expect( find.byKey(const Key('home-feed-center-play')).hitTestable(), findsNothing, @@ -519,6 +521,37 @@ void main() { expect(find.byKey(const Key('progress-inactive')), findsOneWidget); }); + testWidgets('should_auto_play_full_player_when_feed_is_paused', ( + tester, + ) async { + await _pumpApp(tester, playerFactory: (_) => _RecordingPlayer()); + await tester.tap(find.byKey(const Key('tab-recommend'))); + await tester.pumpAndSettle(); + + final feedPage = find.byKey(const Key('recommend-feed-player')); + final feedLogic = _pageLogic(tester, feedPage); + await feedLogic.session.preloadSettled; + feedLogic.session.setUserWantsPlay(false); + await feedLogic.session.playbackSettled; + await tester.pump(); + expect(feedLogic.session.userWantsPlay, isFalse); + + await tester.tap(find.byKey(const Key('home-feed-full-series'))); + await tester.pumpAndSettle(); + + final fullLogic = _pageLogic(tester, _fullPlayerFinder()); + final fullPlayer = fullLogic.session.active! as _RecordingPlayer; + expect(fullLogic.session, isNot(same(feedLogic.session))); + expect(fullLogic.session.userWantsPlay, isTrue); + expect(fullPlayer.playCalls, greaterThan(0)); + expect(fullPlayer.playing, isTrue); + + await tester.tap(find.byKey(const Key('player-back')).hitTestable()); + await tester.pumpAndSettle(); + expect(feedLogic.session.userWantsPlay, isFalse); + expect(feedLogic.session.activeSlotPlaying, isFalse); + }); + testWidgets('should_only_show_progress_buffer_after_video_first_frame', ( tester, ) async { @@ -2103,12 +2136,14 @@ class _RouteVideoPlayer extends VideoPlayerPort { class _RecordingPlayer implements PlayerPort { bool playing = false; + int playCalls = 0; @override Future initialize() async {} @override Future play() async { + playCalls++; playing = true; }