fix(player): 顶层抓帧淡出揭幕
This commit is contained in:
1 parent
c8ec481fc9
commit
e8f192ccf8
2 files changed
+168
-36
No files matched your search
@@ -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,
|
||||||
|
|||||||
Reference in new issue
Block a user