fix(player): 修复返回首页黑屏并恢复预热

This commit is contained in:
Zhou Weidong committed 2026-10-26 16:45:00 +08:00
1 parent 6667330ce0
commit 54eb5a60a8
9 files changed
+56 -101

No files matched your search

@@ -69,7 +69,6 @@ Future<void> _placeNeighbor(PlaybackSession session, _PlayerSlot? slot) async {
/// 当前集已经出画后,按手势方向静默准备上一集和下一集。
void _warmNeighbors(PlaybackSession session, {bool preferredNext = true}) {
if (!session.warmNeighborsEnabled) return;
final request = session._generation;
final nextTask = _warmOne(session, request, session._nextIndex, next: true);
final previousTask = _warmOne(
+2 -4
View File
@@ -27,7 +27,6 @@ class PlaybackSession extends ChangeNotifier {
this.activate,
PlaybackCoordinator? coordinator,
this.waitForFrame,
this.warmNeighborsEnabled = true,
}) : _coordinator = coordinator ?? PlaybackCoordinator();
final List<Episode> episodes;
@@ -40,7 +39,6 @@ class PlaybackSession extends ChangeNotifier {
/// Runs only when a slot becomes current (e.g. progress listeners).
final PlayerPreparation? activate;
final Future<void> Function()? waitForFrame;
final bool warmNeighborsEnabled;
final PlaybackCoordinator _coordinator;
final Set<_PlayerSlot> _live = {};
_PlayerSlot? _active;
@@ -142,8 +140,8 @@ class PlaybackSession extends ChangeNotifier {
}
void _rememberNeighbors(int? preloadIndex, int? previousIndex) {
_nextIndex = warmNeighborsEnabled ? preloadIndex : null;
_previousIndex = warmNeighborsEnabled ? previousIndex : null;
_nextIndex = preloadIndex;
_previousIndex = previousIndex;
}
Future<void> _enqueue(Future<void> Function() operation) {
@@ -6,12 +6,7 @@ part of 'playback_session.dart';
extension PlaybackSessionControls on PlaybackSession {
/// 滑动手势开始时按方向优先准备相邻集,不改变当前集播放状态。
void prewarmNeighbors({required bool forward}) {
if (!warmNeighborsEnabled ||
!_visible ||
_overlayOpen ||
_active?.prepared != true) {
return;
}
if (!_visible || _overlayOpen || _active?.prepared != true) return;
_warmNeighbors(this, preferredNext: forward);
}
@@ -62,6 +57,7 @@ extension PlaybackSessionControls on PlaybackSession {
slot.activated = true;
}
await _applyPlayback(slot);
if (_visible && !_overlayOpen && !_loading) _warmNeighbors(this);
if (kDebugMode) {
debugPrint(
'[feed-handoff] phase=resumed activated=${slot.activated} '
@@ -2,6 +2,7 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:video_player/video_player.dart';
import '../../config/app_config.dart';
@@ -68,6 +69,13 @@ class VideoPlayerPort implements PlayerPort {
Future<void> dispose() async {
if (_disposed) return;
_disposed = true;
if (kDebugMode) {
debugPrint(
'[player-controller] phase=dispose '
'controller=${identityHashCode(controller)} '
'source=${controller.dataSource}\n${StackTrace.current}',
);
}
try {
await _initialization;
} catch (_) {
+17 -12
View File
@@ -2,6 +2,7 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:flutter/scheduler.dart';
@@ -221,30 +222,36 @@ class PlayerLogic extends ChangeNotifier {
/// 独立播放页覆盖首页时只暂停并保存进度,首页 session 与 controller 保持存活。
Future<void> pauseForFullPlayer() async {
if (!_started) return;
_logFeedHandoff('pause');
cancelScrub();
await session.pauseForRouteHandoff();
_logFeedHandoff('paused');
await saveCurrentProgress();
}
/// 返回首页前把仍存活的 feed controller 定位到第 1 集起点。
Future<void> prepareFeedResume() async {
if (!_started) return;
await session.playbackSettled;
final player = session.active;
if (player is VideoPlayerPort && player.controller.value.isInitialized) {
await player.seekTo(Duration.zero);
}
}
/// 独立播放页关闭并恢复首页路由后,等原生播放器实际恢复再结束交接。
Future<void> resumeAfterFullPlayer() async {
if (!_started) return;
_logFeedHandoff('resume');
if (visibleIndices.isNotEmpty) {
await session.resumeForRouteHandoff();
} else {
session.setRouteVisible(false);
await session.playbackSettled;
}
_logFeedHandoff('resumed');
}
void _logFeedHandoff(String phase) {
if (!kDebugMode) return;
final player = session.active;
final video = player is VideoPlayerPort ? player : null;
debugPrint(
'[feed-session] phase=$phase session=${identityHashCode(session)} '
'index=${session.currentIndex} episode=${session.currentEpisode.id} '
'controller=${video == null ? '-' : identityHashCode(video.controller)} '
'live=${session.livePlayerCount}',
);
}
/// 串行替换首页 feed 队列;频道变化时重置到第 1 个,刷新时尽量保留当前剧。
@@ -283,7 +290,6 @@ class PlayerLogic extends ChangeNotifier {
activate: activatePlayer,
coordinator: coordinator,
waitForFrame: _waitForFrame,
warmNeighborsEnabled: !homeMode,
);
}
@@ -1230,7 +1236,6 @@ class PlayerLogic extends ChangeNotifier {
),
);
if (!isMounted()) return;
await prepareFeedResume();
await resumeAfterFullPlayer();
}
+3 -47
View File
@@ -57,12 +57,8 @@ const double _playerLoadingStrokeWidth = 2;
const double _feedRefreshTopGap = 56;
class PlayerPageHandoff {
PlayerPageHandoff({
required this.prepareSource,
required this.sourceUserWantsPlay,
});
PlayerPageHandoff({required this.sourceUserWantsPlay});
final Future<void> Function() prepareSource;
final bool sourceUserWantsPlay;
}
@@ -145,8 +141,6 @@ class _PlayerPageState extends State<PlayerPage>
DateTime? _swipeStartedAt;
bool _swipePrewarmCaptured = false;
bool _presentationActive = false;
bool _presentationSuspended = false;
bool _presentationReturning = false;
bool _leavingHandoff = false;
bool _preparingFullPlayer = false;
final Set<String> _precachedPosterUrls = {};
@@ -538,8 +532,6 @@ class _PlayerPageState extends State<PlayerPage>
}
Widget _buildPlayer(BuildContext context) {
if (_presentationSuspended) return const SizedBox.expand();
if (_presentationReturning) return _buildPresentationReturnLayer();
final interactions = context.watch<InteractionController>();
final l10n = AppLocalizations.of(context)!;
final indices = logic.visibleIndices;
@@ -736,17 +728,6 @@ class _PlayerPageState extends State<PlayerPage>
};
}
Widget _buildPresentationReturnLayer() {
final indices = logic.visibleIndices;
return AnimatedBuilder(
animation: logic.session,
builder: (context, _) => PlayerRenderStack(
pageController: _pageController,
slots: _renderSlots(indices),
),
);
}
PlayerRenderSlotData _renderSlot(
PlayerRenderSlotRole role,
int? index,
@@ -1191,10 +1172,7 @@ class _PlayerPageState extends State<PlayerPage>
_preparingFullPlayer = false;
}
if (!mounted || !context.mounted) return;
final handoff = PlayerPageHandoff(
prepareSource: _prepareSourceAfterFullPlayer,
sourceUserWantsPlay: sourceUserWantsPlay,
);
final handoff = PlayerPageHandoff(sourceUserWantsPlay: sourceUserWantsPlay);
_logFullPlayerRoute('present');
setState(() => _presentationActive = true);
final route = Navigator.of(context).push<void>(
@@ -1208,41 +1186,19 @@ class _PlayerPageState extends State<PlayerPage>
),
),
);
await WidgetsBinding.instance.endOfFrame;
if (mounted) {
setState(() => _presentationSuspended = true);
await WidgetsBinding.instance.endOfFrame;
}
await route;
if (!mounted) return;
logic.attach(widget, isMounted: () => mounted);
setState(() {
_presentationActive = false;
_presentationSuspended = false;
_presentationReturning = false;
});
setState(() => _presentationActive = false);
await logic.resumeAfterFullPlayer();
if (!mounted) return;
_logFullPlayerRoute('restored');
}
Future<void> _prepareSourceAfterFullPlayer() async {
if (!mounted) return;
logic.attach(widget, isMounted: () => mounted);
await logic.prepareFeedResume();
if (!mounted) return;
setState(() {
_presentationSuspended = false;
_presentationReturning = true;
});
await WidgetsBinding.instance.endOfFrame;
}
Future<void> _leaveHandoff(BuildContext context) async {
if (_leavingHandoff) return;
setState(() => _leavingHandoff = true);
await logic.session.pauseForRouteHandoff();
await widget.handoff?.prepareSource();
if (context.mounted) Navigator.of(context).pop();
}
@@ -348,7 +348,9 @@ class _PlayerVideoSurfaceState extends State<PlayerVideoSurface>
debugPrint(
'[player-layer] ts=${DateTime.now().millisecondsSinceEpoch} '
'slot=${widget.debugSlot} index=${widget.debugIndex ?? '-'} '
'phase=$phase init=${value?.isInitialized == true ? 1 : 0}',
'phase=$phase state=${identityHashCode(this)} '
'controller=${widget.controller == null ? '-' : identityHashCode(widget.controller)} '
'init=${value?.isInitialized == true ? 1 : 0}',
);
}