diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index f37bf2c..ee22e73 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -147,9 +147,15 @@ class _PlayerPageState extends State bool _swipePrewarmCaptured = false; bool _presentationActive = false; late bool _firstFrameReady; + late bool _decorVisible; + late bool _decorRemoved; + bool _decorFadeScheduled = false; bool _leavingHandoff = false; bool _preparingFullPlayer = false; bool _loggedInitialFrameBuild = false; + bool _loggedDecorCovered = false; + DateTime? _entryDiagnosticsStartedAt; + final Map _entryElementStates = {}; final Set _precachedPosterUrls = {}; /// 收藏切换后的本地增量:基准是详情快照,切换成功按服务端状态 ±1。 @@ -160,6 +166,13 @@ class _PlayerPageState extends State void initState() { super.initState(); _firstFrameReady = !widget.revealAfterFirstFrame; + final hasInitialFrame = + widget.initialFrameBytes != null || widget.initialFrameProvider != null; + _decorVisible = widget.revealAfterFirstFrame && hasInitialFrame; + _decorRemoved = !_decorVisible; + if (widget.revealAfterFirstFrame) { + _entryDiagnosticsStartedAt = DateTime.now(); + } final handoff = widget.handoff; logic = PlayerBinding().create( history: context.read(), @@ -611,6 +624,7 @@ class _PlayerPageState extends State pageController: _pageController, slots: _renderSlots(indices), onCurrentVideoVisible: _handleCurrentVideoVisible, + showNeighbors: !widget.revealAfterFirstFrame || _decorRemoved, ), ), pageLayer, @@ -678,13 +692,16 @@ class _PlayerPageState extends State ), ), ); - final revealContent = - !widget.revealAfterFirstFrame || - _firstFrameReady || - logic.session.isLocked || - logic.ticketError != null || - logic.blockedEpisodeId != null || - logic.session.error != null; + _logEntryElement('player-ui', true); + _logEntryElement( + 'home-feed-header', + widget.homeMode && + widget.showHomeFeedHeader && + !_scrubbing && + !_boosting && + !logic.pip && + !logic.pipPreparing, + ); final hasInitialFrame = widget.initialFrameBytes != null || widget.initialFrameProvider != null; if (kDebugMode && @@ -697,32 +714,40 @@ class _PlayerPageState extends State 'kind=${widget.initialFrameBytes != null ? 'capture' : 'poster'}', ); } - final presentedPlayer = widget.revealAfterFirstFrame + final usesInitialFrame = widget.revealAfterFirstFrame && hasInitialFrame; + if (usesInitialFrame && + _decorVisible && + !_loggedDecorCovered && + kDebugMode) { + _loggedDecorCovered = true; + debugPrint('[player-entry] phase=decor-covers-video'); + } + final entryUnavailableReason = logic.ticketError != null + ? 'ticket-error' + : logic.blockedEpisodeId != null + ? 'blocked-episode' + : null; + if (usesInitialFrame && _decorVisible && entryUnavailableReason != null) { + _scheduleDecorFade(entryUnavailableReason); + } + final presentedPlayer = usesInitialFrame ? Stack( fit: StackFit.expand, children: [ - Positioned.fill( - child: revealContent - ? const SizedBox.expand() - : widget.initialFrameBytes == null - ? widget.initialFrameProvider == null - ? _buildInitialFrameFallback() - : Image( - image: widget.initialFrameProvider!, - fit: BoxFit.cover, - gaplessPlayback: true, - errorBuilder: (_, _, _) => - _buildInitialFrameFallback(), - ) - : Image.memory( - widget.initialFrameBytes!, - key: const Key('player-entry-frame'), - fit: BoxFit.cover, - gaplessPlayback: true, - errorBuilder: (_, _, _) => _buildInitialFrameFallback(), - ), - ), - Opacity(opacity: revealContent ? 1 : 0, child: player), + player, + if (!_decorRemoved) + Positioned.fill( + child: IgnorePointer( + child: AnimatedOpacity( + key: const Key('player-entry-decor'), + opacity: _decorVisible ? 1 : 0, + duration: const Duration(milliseconds: 140), + curve: Curves.easeOut, + onEnd: _handleDecorFadeEnd, + child: _buildInitialFrameImage(), + ), + ), + ), ], ) : player; @@ -738,13 +763,84 @@ class _PlayerPageState extends State void _handleCurrentVideoVisible() { if (!mounted || _firstFrameReady) return; - setState(() => _firstFrameReady = true); + _firstFrameReady = true; if (kDebugMode) { debugPrint( '[player-entry] phase=video-first-frame ' 'session=${identityHashCode(logic.session)}', ); } + WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _startDecorFade('video-first-frame'); + }); + }); + } + + void _scheduleDecorFade(String reason) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _startDecorFade(reason); + }); + } + + void _startDecorFade(String reason) { + if (!mounted || !_decorVisible || _decorRemoved || _decorFadeScheduled) { + return; + } + _decorFadeScheduled = true; + if (kDebugMode) { + debugPrint('[player-entry] phase=decor-fade-start reason=$reason'); + } + setState(() => _decorVisible = false); + } + + void _handleDecorFadeEnd() { + if (!mounted || _decorVisible || _decorRemoved) return; + setState(() => _decorRemoved = true); + if (kDebugMode) { + debugPrint('[player-entry] phase=decor-removed'); + } + } + + Widget _buildInitialFrameImage() { + if (widget.initialFrameBytes != null) { + return Image.memory( + widget.initialFrameBytes!, + key: const Key('player-entry-frame'), + fit: BoxFit.cover, + gaplessPlayback: true, + errorBuilder: (_, _, _) => _buildInitialFrameFallback(), + ); + } + final provider = widget.initialFrameProvider; + if (provider != null) { + return Image( + image: provider, + key: const Key('player-entry-frame'), + fit: BoxFit.cover, + gaplessPlayback: true, + errorBuilder: (_, _, _) => _buildInitialFrameFallback(), + ); + } + return _buildInitialFrameFallback(); + } + + void _logEntryElement(String element, bool visible) { + final startedAt = _entryDiagnosticsStartedAt; + if (startedAt == null || + DateTime.now().difference(startedAt) > + const Duration(milliseconds: 800)) { + return; + } + if (_entryElementStates[element] == visible) return; + _entryElementStates[element] = visible; + if (kDebugMode) { + debugPrint( + '[player-entry] phase=element-state element=$element ' + 'visible=${visible ? 1 : 0} ' + 'ms=${DateTime.now().difference(startedAt).inMilliseconds}', + ); + } } Widget _buildInitialFrameFallback() { @@ -801,6 +897,11 @@ class _PlayerPageState extends State logic.session.previousPreloadedIndex; final next = logic.nextVisibleIndex(current) ?? logic.session.preloadedIndex; + _logEntryElement( + 'adjacent-slots', + (!widget.revealAfterFirstFrame || _decorRemoved) && + (previous != null || next != null), + ); return { PlayerRenderSlotRole.previous: _renderSlot( PlayerRenderSlotRole.previous, @@ -847,6 +948,33 @@ class _PlayerPageState extends State final handoffPort = handoff is VideoPlayerPort ? handoff : null; final videoReady = videoPort?.controller.value.isInitialized ?? false; final theater = logic.pip || logic.pipPreparing; + if (selected) { + final videoLoading = + !locked && + !theater && + !videoReady && + port == null && + !logic.ticketLoading && + logic.ticketError == null && + logic.session.error == null && + logic.blockedEpisodeId != episode.id && + !logic.session.isLocked; + _logEntryElement('loading-spinner', videoLoading); + _logEntryElement( + 'opening-frame', + !locked && !videoReady && episode.posterUrl?.isNotEmpty == true || + !locked && !videoReady && episode.thumbnailsUrl?.isNotEmpty == true, + ); + _logEntryElement('pause-button', !locked && !logic.session.userWantsPlay); + _logEntryElement( + 'player-controls', + !widget.homeMode && !theater && !locked, + ); + _logEntryElement( + 'buffering', + videoPort?.controller.value.isBuffering ?? false, + ); + } final videoFit = theater ? BoxFit.contain : BoxFit.cover; final preparedFrameAvailable = !locked && diff --git a/lib/pages/player/widgets/player_render_stack.dart b/lib/pages/player/widgets/player_render_stack.dart index fc19e0a..4cd02f5 100644 --- a/lib/pages/player/widgets/player_render_stack.dart +++ b/lib/pages/player/widgets/player_render_stack.dart @@ -53,11 +53,13 @@ class PlayerRenderStack extends StatefulWidget { required this.pageController, required this.slots, this.onCurrentVideoVisible, + this.showNeighbors = true, }); final PageController pageController; final Map slots; final VoidCallback? onCurrentVideoVisible; + final bool showNeighbors; @override State createState() => _PlayerRenderStackState(); @@ -115,11 +117,13 @@ class _PlayerRenderStackState extends State { : MediaQuery.sizeOf(context).height; final pageOffset = _pageOffset(); final currentPage = _currentSlot.pagePosition ?? pageOffset; - final slots = [ - _slotData(PlayerRenderSlotRole.previous), - _slotData(PlayerRenderSlotRole.current), - _slotData(PlayerRenderSlotRole.next), - ]; + final slots = widget.showNeighbors + ? [ + _slotData(PlayerRenderSlotRole.previous), + _slotData(PlayerRenderSlotRole.current), + _slotData(PlayerRenderSlotRole.next), + ] + : [_slotData(PlayerRenderSlotRole.current)]; return Stack( key: const Key('player-render-stack'), fit: StackFit.expand,