fix(player): 顶层抓帧淡出揭幕

This commit is contained in:
Zhou Weidong committed 2026-10-26 17:31:59 +08:00
1 parent c8ec481fc9
commit e8f192ccf8
2 files changed
+168 -36

No files matched your search

+159 -31
View File
@@ -147,9 +147,15 @@ class _PlayerPageState extends State<PlayerPage>
bool _swipePrewarmCaptured = false; bool _swipePrewarmCaptured = false;
bool _presentationActive = false; bool _presentationActive = false;
late bool _firstFrameReady; late bool _firstFrameReady;
late bool _decorVisible;
late bool _decorRemoved;
bool _decorFadeScheduled = false;
bool _leavingHandoff = false; bool _leavingHandoff = false;
bool _preparingFullPlayer = false; bool _preparingFullPlayer = false;
bool _loggedInitialFrameBuild = false; bool _loggedInitialFrameBuild = false;
bool _loggedDecorCovered = false;
DateTime? _entryDiagnosticsStartedAt;
final Map<String, bool> _entryElementStates = {};
final Set<String> _precachedPosterUrls = {}; final Set<String> _precachedPosterUrls = {};
/// 收藏切换后的本地增量:基准是详情快照,切换成功按服务端状态 ±1。 /// 收藏切换后的本地增量:基准是详情快照,切换成功按服务端状态 ±1。
@@ -160,6 +166,13 @@ class _PlayerPageState extends State<PlayerPage>
void initState() { void initState() {
super.initState(); super.initState();
_firstFrameReady = !widget.revealAfterFirstFrame; _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; final handoff = widget.handoff;
logic = PlayerBinding().create( logic = PlayerBinding().create(
history: context.read<WatchHistoryRepository>(), history: context.read<WatchHistoryRepository>(),
@@ -611,6 +624,7 @@ class _PlayerPageState extends State<PlayerPage>
pageController: _pageController, pageController: _pageController,
slots: _renderSlots(indices), slots: _renderSlots(indices),
onCurrentVideoVisible: _handleCurrentVideoVisible, onCurrentVideoVisible: _handleCurrentVideoVisible,
showNeighbors: !widget.revealAfterFirstFrame || _decorRemoved,
), ),
), ),
pageLayer, pageLayer,
@@ -678,13 +692,16 @@ class _PlayerPageState extends State<PlayerPage>
), ),
), ),
); );
final revealContent = _logEntryElement('player-ui', true);
!widget.revealAfterFirstFrame || _logEntryElement(
_firstFrameReady || 'home-feed-header',
logic.session.isLocked || widget.homeMode &&
logic.ticketError != null || widget.showHomeFeedHeader &&
logic.blockedEpisodeId != null || !_scrubbing &&
logic.session.error != null; !_boosting &&
!logic.pip &&
!logic.pipPreparing,
);
final hasInitialFrame = final hasInitialFrame =
widget.initialFrameBytes != null || widget.initialFrameProvider != null; widget.initialFrameBytes != null || widget.initialFrameProvider != null;
if (kDebugMode && if (kDebugMode &&
@@ -697,32 +714,40 @@ class _PlayerPageState extends State<PlayerPage>
'kind=${widget.initialFrameBytes != null ? 'capture' : 'poster'}', '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( ? Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
Positioned.fill( player,
child: revealContent if (!_decorRemoved)
? const SizedBox.expand() Positioned.fill(
: widget.initialFrameBytes == null child: IgnorePointer(
? widget.initialFrameProvider == null child: AnimatedOpacity(
? _buildInitialFrameFallback() key: const Key('player-entry-decor'),
: Image( opacity: _decorVisible ? 1 : 0,
image: widget.initialFrameProvider!, duration: const Duration(milliseconds: 140),
fit: BoxFit.cover, curve: Curves.easeOut,
gaplessPlayback: true, onEnd: _handleDecorFadeEnd,
errorBuilder: (_, _, _) => child: _buildInitialFrameImage(),
_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; : player;
@@ -738,13 +763,84 @@ class _PlayerPageState extends State<PlayerPage>
void _handleCurrentVideoVisible() { void _handleCurrentVideoVisible() {
if (!mounted || _firstFrameReady) return; if (!mounted || _firstFrameReady) return;
setState(() => _firstFrameReady = true); _firstFrameReady = true;
if (kDebugMode) { if (kDebugMode) {
debugPrint( debugPrint(
'[player-entry] phase=video-first-frame ' '[player-entry] phase=video-first-frame '
'session=${identityHashCode(logic.session)}', '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() { Widget _buildInitialFrameFallback() {
@@ -801,6 +897,11 @@ class _PlayerPageState extends State<PlayerPage>
logic.session.previousPreloadedIndex; logic.session.previousPreloadedIndex;
final next = final next =
logic.nextVisibleIndex(current) ?? logic.session.preloadedIndex; logic.nextVisibleIndex(current) ?? logic.session.preloadedIndex;
_logEntryElement(
'adjacent-slots',
(!widget.revealAfterFirstFrame || _decorRemoved) &&
(previous != null || next != null),
);
return { return {
PlayerRenderSlotRole.previous: _renderSlot( PlayerRenderSlotRole.previous: _renderSlot(
PlayerRenderSlotRole.previous, PlayerRenderSlotRole.previous,
@@ -847,6 +948,33 @@ class _PlayerPageState extends State<PlayerPage>
final handoffPort = handoff is VideoPlayerPort ? handoff : null; final handoffPort = handoff is VideoPlayerPort ? handoff : null;
final videoReady = videoPort?.controller.value.isInitialized ?? false; final videoReady = videoPort?.controller.value.isInitialized ?? false;
final theater = logic.pip || logic.pipPreparing; 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 videoFit = theater ? BoxFit.contain : BoxFit.cover;
final preparedFrameAvailable = final preparedFrameAvailable =
!locked && !locked &&
@@ -53,11 +53,13 @@ class PlayerRenderStack extends StatefulWidget {
required this.pageController, required this.pageController,
required this.slots, required this.slots,
this.onCurrentVideoVisible, this.onCurrentVideoVisible,
this.showNeighbors = true,
}); });
final PageController pageController; final PageController pageController;
final Map<PlayerRenderSlotRole, PlayerRenderSlotData> slots; final Map<PlayerRenderSlotRole, PlayerRenderSlotData> slots;
final VoidCallback? onCurrentVideoVisible; final VoidCallback? onCurrentVideoVisible;
final bool showNeighbors;
@override @override
State<PlayerRenderStack> createState() => _PlayerRenderStackState(); State<PlayerRenderStack> createState() => _PlayerRenderStackState();
@@ -115,11 +117,13 @@ class _PlayerRenderStackState extends State<PlayerRenderStack> {
: MediaQuery.sizeOf(context).height; : MediaQuery.sizeOf(context).height;
final pageOffset = _pageOffset(); final pageOffset = _pageOffset();
final currentPage = _currentSlot.pagePosition ?? pageOffset; final currentPage = _currentSlot.pagePosition ?? pageOffset;
final slots = [ final slots = widget.showNeighbors
_slotData(PlayerRenderSlotRole.previous), ? [
_slotData(PlayerRenderSlotRole.current), _slotData(PlayerRenderSlotRole.previous),
_slotData(PlayerRenderSlotRole.next), _slotData(PlayerRenderSlotRole.current),
]; _slotData(PlayerRenderSlotRole.next),
]
: [_slotData(PlayerRenderSlotRole.current)];
return Stack( return Stack(
key: const Key('player-render-stack'), key: const Key('player-render-stack'),
fit: StackFit.expand, fit: StackFit.expand,