fix(player): 播放页改为非阻塞加载
This commit is contained in:
1 parent
e8f192ccf8
commit
ee4e573f4f
7 files changed
+239
-455
No files matched your search
@@ -32,13 +32,13 @@ import '../../l10n/app_localizations.dart';
|
||||
import '../home/home_feed_channel.dart';
|
||||
import 'comments_auth.dart';
|
||||
import 'player_binding.dart';
|
||||
import 'player_frame_capture.dart';
|
||||
import 'player_logic.dart';
|
||||
import 'widgets/home_feed_header.dart';
|
||||
import 'widgets/home_feed_overlay.dart';
|
||||
import 'widgets/exit_clear_screen_button.dart';
|
||||
import 'widgets/feed_pull_refresh.dart';
|
||||
import 'widgets/player_center_toggle.dart';
|
||||
import 'widgets/player_loading_overlay.dart';
|
||||
import 'widgets/player_more_sheet.dart';
|
||||
import 'widgets/player_overlay.dart';
|
||||
import 'widgets/player_overlay_models.dart';
|
||||
@@ -91,9 +91,6 @@ class PlayerPage extends StatefulWidget {
|
||||
this.wallet,
|
||||
this.downloads,
|
||||
this.handoff,
|
||||
this.revealAfterFirstFrame = false,
|
||||
this.initialFrameBytes,
|
||||
this.initialFrameProvider,
|
||||
});
|
||||
|
||||
final Drama drama;
|
||||
@@ -121,9 +118,6 @@ class PlayerPage extends StatefulWidget {
|
||||
final WalletController? wallet;
|
||||
final DownloadController? downloads;
|
||||
final PlayerPageHandoff? handoff;
|
||||
final bool revealAfterFirstFrame;
|
||||
final Uint8List? initialFrameBytes;
|
||||
final ImageProvider<Object>? initialFrameProvider;
|
||||
|
||||
@override
|
||||
State<PlayerPage> createState() => _PlayerPageState();
|
||||
@@ -146,16 +140,13 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
DateTime? _swipeStartedAt;
|
||||
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<String, bool> _entryElementStates = {};
|
||||
int? _loadingIndex;
|
||||
int? _videoReadyIndex;
|
||||
bool _loadingVisible = false;
|
||||
bool _loadingRemoved = true;
|
||||
bool _loadingFadeScheduled = false;
|
||||
final Set<String> _precachedPosterUrls = {};
|
||||
|
||||
/// 收藏切换后的本地增量:基准是详情快照,切换成功按服务端状态 ±1。
|
||||
@@ -165,14 +156,6 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
@override
|
||||
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<WatchHistoryRepository>(),
|
||||
@@ -196,11 +179,10 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
initialUserWantsPlay: handoff?.sourceUserWantsPlay ?? true,
|
||||
);
|
||||
_pageController = logic.pages;
|
||||
if (kDebugMode && widget.revealAfterFirstFrame) {
|
||||
debugPrint(
|
||||
'[player-entry] phase=waiting-first-frame '
|
||||
'session=${identityHashCode(logic.session)}',
|
||||
);
|
||||
if (!widget.embedded) {
|
||||
_loadingIndex = logic.session.currentIndex;
|
||||
_loadingVisible = true;
|
||||
_loadingRemoved = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,6 +544,33 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
final interactions = context.watch<InteractionController>();
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final indices = logic.visibleIndices;
|
||||
final currentIndex = logic.session.currentIndex;
|
||||
if (!widget.embedded && _loadingIndex != currentIndex) {
|
||||
_loadingIndex = currentIndex;
|
||||
_videoReadyIndex = null;
|
||||
_loadingVisible = true;
|
||||
_loadingRemoved = false;
|
||||
_loadingFadeScheduled = false;
|
||||
}
|
||||
final entryUnavailable =
|
||||
logic.ticketError != null ||
|
||||
logic.blockedEpisodeId != null ||
|
||||
logic.session.error != null ||
|
||||
logic.isEpisodeLocked(logic.session.currentEpisode);
|
||||
if (!widget.embedded &&
|
||||
!_loadingRemoved &&
|
||||
!_loadingFadeScheduled &&
|
||||
entryUnavailable) {
|
||||
_scheduleLoadingFade('entry-unavailable');
|
||||
}
|
||||
final activePort = logic.session.active;
|
||||
if (!widget.embedded &&
|
||||
!_loadingRemoved &&
|
||||
!_loadingFadeScheduled &&
|
||||
activePort != null &&
|
||||
activePort is! VideoPlayerPort) {
|
||||
_scheduleLoadingFade('non-video-player-ready');
|
||||
}
|
||||
final content = AnimatedBuilder(
|
||||
animation: logic.session,
|
||||
builder: (context, _) {
|
||||
@@ -618,14 +627,11 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
RepaintBoundary(
|
||||
key: logic.renderBoundaryKey,
|
||||
child: PlayerRenderStack(
|
||||
pageController: _pageController,
|
||||
slots: _renderSlots(indices),
|
||||
onCurrentVideoVisible: _handleCurrentVideoVisible,
|
||||
showNeighbors: !widget.revealAfterFirstFrame || _decorRemoved,
|
||||
),
|
||||
PlayerRenderStack(
|
||||
pageController: _pageController,
|
||||
slots: _renderSlots(indices),
|
||||
onCurrentVideoVisible: _handleCurrentVideoVisible,
|
||||
showNeighbors: widget.embedded || _loadingRemoved,
|
||||
),
|
||||
pageLayer,
|
||||
],
|
||||
@@ -669,6 +675,14 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!widget.embedded && !_loadingRemoved)
|
||||
Positioned.fill(
|
||||
child: PlayerLoadingOverlay(
|
||||
key: ValueKey('player-loading-${_loadingIndex ?? currentIndex}'),
|
||||
opacity: _loadingVisible ? 1 : 0,
|
||||
onFadeEnd: _handleLoadingFadeEnd,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
final player = AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
@@ -692,78 +706,25 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
),
|
||||
),
|
||||
);
|
||||
_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 &&
|
||||
widget.revealAfterFirstFrame &&
|
||||
hasInitialFrame &&
|
||||
!_loggedInitialFrameBuild) {
|
||||
_loggedInitialFrameBuild = true;
|
||||
debugPrint(
|
||||
'[player-entry] phase=first-build-with-initial-frame '
|
||||
'kind=${widget.initialFrameBytes != null ? 'capture' : 'poster'}',
|
||||
);
|
||||
}
|
||||
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: [
|
||||
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;
|
||||
if (widget.handoff == null) return presentedPlayer;
|
||||
if (widget.handoff == null) return player;
|
||||
return PopScope<void>(
|
||||
canPop: _leavingHandoff,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) unawaited(_leaveHandoff(context));
|
||||
},
|
||||
child: presentedPlayer,
|
||||
child: player,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleCurrentVideoVisible() {
|
||||
if (!mounted || _firstFrameReady) return;
|
||||
_firstFrameReady = true;
|
||||
void _handleCurrentVideoVisible(int? index) {
|
||||
if (!mounted) return;
|
||||
final currentIndex = logic.session.currentIndex;
|
||||
if (index == null ||
|
||||
index != currentIndex ||
|
||||
_videoReadyIndex == currentIndex) {
|
||||
return;
|
||||
}
|
||||
_videoReadyIndex = currentIndex;
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[player-entry] phase=video-first-frame '
|
||||
@@ -772,95 +733,41 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_startDecorFade('video-first-frame');
|
||||
_startLoadingFade(currentIndex);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _scheduleDecorFade(String reason) {
|
||||
void _scheduleLoadingFade(String reason) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_startDecorFade(reason);
|
||||
_startLoadingFade(logic.session.currentIndex, reason: reason);
|
||||
});
|
||||
}
|
||||
|
||||
void _startDecorFade(String reason) {
|
||||
if (!mounted || !_decorVisible || _decorRemoved || _decorFadeScheduled) {
|
||||
void _startLoadingFade(int index, {String reason = 'video-first-frame'}) {
|
||||
if (!mounted ||
|
||||
widget.embedded ||
|
||||
index != logic.session.currentIndex ||
|
||||
!_loadingVisible ||
|
||||
_loadingRemoved ||
|
||||
_loadingFadeScheduled) {
|
||||
return;
|
||||
}
|
||||
_decorFadeScheduled = true;
|
||||
_loadingFadeScheduled = true;
|
||||
if (kDebugMode) {
|
||||
debugPrint('[player-entry] phase=decor-fade-start reason=$reason');
|
||||
debugPrint('[player-entry] phase=loading-fade-start reason=$reason');
|
||||
}
|
||||
setState(() => _decorVisible = false);
|
||||
setState(() => _loadingVisible = false);
|
||||
}
|
||||
|
||||
void _handleDecorFadeEnd() {
|
||||
if (!mounted || _decorVisible || _decorRemoved) return;
|
||||
setState(() => _decorRemoved = true);
|
||||
void _handleLoadingFadeEnd() {
|
||||
if (!mounted || _loadingVisible || _loadingRemoved) return;
|
||||
setState(() => _loadingRemoved = true);
|
||||
if (kDebugMode) {
|
||||
debugPrint('[player-entry] phase=decor-removed');
|
||||
debugPrint('[player-entry] phase=loading-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() {
|
||||
final index = widget.initialIndex;
|
||||
if (index < 0 || index >= widget.drama.episodes.length) {
|
||||
return const ColoredBox(
|
||||
key: Key('player-entry-black'),
|
||||
color: AppColors.mediaBlack,
|
||||
);
|
||||
}
|
||||
final episode = widget.drama.episodes[index];
|
||||
return playerOpeningFrame(
|
||||
episode.thumbnailsUrl,
|
||||
posterUrl: episode.posterUrl,
|
||||
spriteSource: _spriteAxisService,
|
||||
fit: BoxFit.cover,
|
||||
) ??
|
||||
const ColoredBox(color: AppColors.mediaBlack);
|
||||
}
|
||||
|
||||
void _openCurrentMore() {
|
||||
final entry = logic.queue.entryAt(logic.session.currentIndex);
|
||||
_openMore(context, entry.episode);
|
||||
@@ -897,11 +804,6 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
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,
|
||||
@@ -948,33 +850,6 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
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 &&
|
||||
@@ -1388,24 +1263,7 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
seriesEpisodes.insert(0, sourceEpisode);
|
||||
episodeIndex = 0;
|
||||
}
|
||||
final fallbackPosterUrl =
|
||||
episodeIndex >= 0 && episodeIndex < seriesEpisodes.length
|
||||
? seriesEpisodes[episodeIndex].posterUrl
|
||||
: sourceEpisode.posterUrl;
|
||||
final preparedFrame = await PlayerFrameCapture.captureAndPrecache(
|
||||
context: context,
|
||||
boundaryKey: logic.renderBoundaryKey,
|
||||
source: 'full-player',
|
||||
fallbackPosterUrl: fallbackPosterUrl,
|
||||
);
|
||||
if (!mounted || !context.mounted) return;
|
||||
final sourceUserWantsPlay = logic.session.userWantsPlay;
|
||||
try {
|
||||
await logic.pauseForFullPlayer();
|
||||
} finally {
|
||||
_preparingFullPlayer = false;
|
||||
}
|
||||
if (!mounted || !context.mounted) return;
|
||||
final handoff = PlayerPageHandoff(sourceUserWantsPlay: sourceUserWantsPlay);
|
||||
_logFullPlayerRoute('present');
|
||||
setState(() => _presentationActive = true);
|
||||
@@ -1421,15 +1279,13 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
handoff: handoff,
|
||||
wallet: logic.wallet,
|
||||
downloads: logic.downloads,
|
||||
revealAfterFirstFrame: preparedFrame.isReady,
|
||||
initialFrameBytes: preparedFrame.bytes,
|
||||
initialFrameProvider: preparedFrame.provider,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (kDebugMode) {
|
||||
debugPrint('[player-entry] phase=route-pushed source=full-player ms=0');
|
||||
}
|
||||
unawaited(logic.pauseForFullPlayer());
|
||||
unawaited(logic.saveCurrentProgress());
|
||||
await route;
|
||||
if (!mounted) return;
|
||||
|
||||
Reference in new issue
Block a user