fix(player): 隔离播放资源所有权并修复黑屏

This commit is contained in:
Zhou Weidong committed 2026-10-26 17:03:19 +08:00
1 parent 54eb5a60a8
commit bb2c729a7e
9 files changed
+223 -25

No files matched your search

@@ -15,6 +15,16 @@ class PlaybackCoordinator {
if (!valid()) return false;
if (_owner != session) {
final previous = _owner;
if (previous != null && previous.owner != session.owner) {
if (kDebugMode) {
debugPrint(
'[player-owner] phase=reject-coordinator-handoff '
'owner=${session.owner.name} '
'activeOwner=${previous.owner.name}',
);
}
return false;
}
await previous?._releasePlayers();
if (previous != null && previous.livePlayerCount != 0) return false;
_owner = null;
@@ -56,6 +66,10 @@ Future<void> _parkNeighbors(
Future<void> _placeNeighbor(PlaybackSession session, _PlayerSlot? slot) async {
if (slot == null || slot == session._active || slot.retiring) return;
if (slot.owner != session.owner) {
session._logOwnerRejection('park', slot);
return;
}
if (session._next == null && slot.index == session._nextIndex) {
session._next = slot;
return;
+48 -3
View File
@@ -17,6 +17,8 @@ typedef PlayerFactory = PlayerPort Function(Episode episode);
typedef PlayerPreparation =
Future<void> Function(Episode episode, PlayerPort player);
enum PlaybackSessionOwner { home, player }
/// Owns current plus previous/next frames; allocation stays serialized.
class PlaybackSession extends ChangeNotifier {
PlaybackSession({
@@ -27,6 +29,8 @@ class PlaybackSession extends ChangeNotifier {
this.activate,
PlaybackCoordinator? coordinator,
this.waitForFrame,
this.owner = PlaybackSessionOwner.player,
this.dramaIdAt,
}) : _coordinator = coordinator ?? PlaybackCoordinator();
final List<Episode> episodes;
@@ -39,6 +43,8 @@ class PlaybackSession extends ChangeNotifier {
/// Runs only when a slot becomes current (e.g. progress listeners).
final PlayerPreparation? activate;
final Future<void> Function()? waitForFrame;
final PlaybackSessionOwner owner;
final String? Function(int index)? dramaIdAt;
final PlaybackCoordinator _coordinator;
final Set<_PlayerSlot> _live = {};
_PlayerSlot? _active;
@@ -161,7 +167,12 @@ class PlaybackSession extends ChangeNotifier {
if (_live.length >= maxLivePlayers) {
throw StateError('Playback resource limit reached');
}
final slot = _PlayerSlot(index, factory(episodes[index]));
final slot = _PlayerSlot(
index,
factory(episodes[index]),
owner: owner,
dramaId: dramaIdAt?.call(index),
);
_live.add(slot);
slot.ready = () async {
try {
@@ -180,7 +191,19 @@ class PlaybackSession extends ChangeNotifier {
}
Future<void> _retire(_PlayerSlot? slot) async {
if (slot == null || !_live.contains(slot)) return;
if (slot == null) return;
if (slot.owner != owner) {
_logOwnerRejection('retire', slot);
return;
}
if (!_live.contains(slot)) return;
if (kDebugMode) {
debugPrint(
'[player-owner] phase=retire owner=${slot.owner.name} '
'drama=${slot.dramaId ?? '-'} episodeIndex=${slot.index} '
'controller=${_playerIdentity(slot.player)}',
);
}
slot.retiring = true;
final cleared =
_active == slot ||
@@ -460,6 +483,15 @@ class PlaybackSession extends ChangeNotifier {
if (!_closed) notifyListeners();
}
void _logOwnerRejection(String phase, _PlayerSlot slot) {
if (!kDebugMode) return;
debugPrint(
'[player-owner] phase=reject-$phase expected=${owner.name} '
'actual=${slot.owner.name} drama=${slot.dramaId ?? '-'} '
'episodeIndex=${slot.index} controller=${_playerIdentity(slot.player)}',
);
}
Future<void> close() async {
if (_closed) return;
_closed = true;
@@ -470,9 +502,16 @@ class PlaybackSession extends ChangeNotifier {
}
class _PlayerSlot {
_PlayerSlot(this.index, this.player);
_PlayerSlot(
this.index,
this.player, {
required this.owner,
required this.dramaId,
});
int index;
final PlayerPort player;
final PlaybackSessionOwner owner;
final String? dramaId;
late final Future<void> ready;
bool prepared = false;
bool activated = false;
@@ -481,3 +520,9 @@ class _PlayerSlot {
bool reusedWarm = false;
Object? failure;
}
String _playerIdentity(PlayerPort player) {
return player is VideoPlayerPort
? '${identityHashCode(player.controller)}'
: '${identityHashCode(player)}';
}
+4 -1
View File
@@ -290,6 +290,10 @@ class PlayerLogic extends ChangeNotifier {
activate: activatePlayer,
coordinator: coordinator,
waitForFrame: _waitForFrame,
owner: homeMode ? PlaybackSessionOwner.home : PlaybackSessionOwner.player,
dramaIdAt: (index) => index >= 0 && index < queue.length
? queue.entryAt(index).drama.id
: null,
);
}
@@ -1231,7 +1235,6 @@ class PlayerLogic extends ChangeNotifier {
initialIndex: index,
wallet: wallet,
downloads: downloads,
isolatePlaybackSession: true,
),
),
);
+42 -7
View File
@@ -90,7 +90,7 @@ class PlayerPage extends StatefulWidget {
this.wallet,
this.downloads,
this.handoff,
this.isolatePlaybackSession = false,
this.revealAfterFirstFrame = false,
});
final Drama drama;
@@ -118,7 +118,7 @@ class PlayerPage extends StatefulWidget {
final WalletController? wallet;
final DownloadController? downloads;
final PlayerPageHandoff? handoff;
final bool isolatePlaybackSession;
final bool revealAfterFirstFrame;
@override
State<PlayerPage> createState() => _PlayerPageState();
@@ -141,6 +141,7 @@ class _PlayerPageState extends State<PlayerPage>
DateTime? _swipeStartedAt;
bool _swipePrewarmCaptured = false;
bool _presentationActive = false;
late bool _firstFrameReady;
bool _leavingHandoff = false;
bool _preparingFullPlayer = false;
final Set<String> _precachedPosterUrls = {};
@@ -152,6 +153,7 @@ class _PlayerPageState extends State<PlayerPage>
@override
void initState() {
super.initState();
_firstFrameReady = !widget.revealAfterFirstFrame;
final handoff = widget.handoff;
logic = PlayerBinding().create(
history: context.read<WatchHistoryRepository>(),
@@ -169,12 +171,18 @@ class _PlayerPageState extends State<PlayerPage>
WidgetsBinding.instance.addObserver(this);
logic.start(
factory: context.read<PlayerFactory>(),
coordinator: handoff == null && !widget.isolatePlaybackSession
coordinator: widget.embedded
? context.read<PlaybackCoordinator>()
: PlaybackCoordinator(),
initialUserWantsPlay: handoff?.sourceUserWantsPlay ?? true,
);
_pageController = logic.pages;
if (kDebugMode && widget.revealAfterFirstFrame) {
debugPrint(
'[player-entry] phase=waiting-first-frame '
'session=${identityHashCode(logic.session)}',
);
}
}
@override
@@ -594,6 +602,7 @@ class _PlayerPageState extends State<PlayerPage>
PlayerRenderStack(
pageController: _pageController,
slots: _renderSlots(indices),
onCurrentVideoVisible: _handleCurrentVideoVisible,
),
pageLayer,
],
@@ -660,16 +669,37 @@ class _PlayerPageState extends State<PlayerPage>
),
),
);
if (widget.handoff == null) return player;
final revealContent =
!widget.revealAfterFirstFrame ||
_firstFrameReady ||
logic.session.isLocked ||
logic.ticketError != null ||
logic.blockedEpisodeId != null ||
logic.session.error != null;
final presentedPlayer = widget.revealAfterFirstFrame
? Opacity(opacity: revealContent ? 1 : 0, child: player)
: player;
if (widget.handoff == null) return presentedPlayer;
return PopScope<void>(
canPop: _leavingHandoff,
onPopInvokedWithResult: (didPop, _) {
if (!didPop) unawaited(_leaveHandoff(context));
},
child: player,
child: presentedPlayer,
);
}
void _handleCurrentVideoVisible() {
if (!mounted || _firstFrameReady) return;
setState(() => _firstFrameReady = true);
if (kDebugMode) {
debugPrint(
'[player-entry] phase=ready '
'session=${identityHashCode(logic.session)}',
);
}
}
void _openCurrentMore() {
final entry = logic.queue.entryAt(logic.session.currentIndex);
_openMore(context, entry.episode);
@@ -1176,13 +1206,18 @@ class _PlayerPageState extends State<PlayerPage>
_logFullPlayerRoute('present');
setState(() => _presentationActive = true);
final route = Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => PlayerPage(
PageRouteBuilder<void>(
opaque: false,
barrierDismissible: false,
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
pageBuilder: (_, _, _) => PlayerPage(
drama: seriesDrama,
initialIndex: episodeIndex,
handoff: handoff,
wallet: logic.wallet,
downloads: logic.downloads,
revealAfterFirstFrame: true,
),
),
);
@@ -52,10 +52,12 @@ class PlayerRenderStack extends StatefulWidget {
super.key,
required this.pageController,
required this.slots,
this.onCurrentVideoVisible,
});
final PageController pageController;
final Map<PlayerRenderSlotRole, PlayerRenderSlotData> slots;
final VoidCallback? onCurrentVideoVisible;
@override
State<PlayerRenderStack> createState() => _PlayerRenderStackState();
@@ -68,7 +70,10 @@ class _PlayerRenderStackState extends State<PlayerRenderStack> {
void initState() {
super.initState();
_lastCurrentIndex = _currentSlot.index;
_log('init current=${_lastCurrentIndex ?? '-'}');
_log(
'init current=${_lastCurrentIndex ?? '-'} '
'pageController=${identityHashCode(widget.pageController)}',
);
}
@override
@@ -86,7 +91,10 @@ class _PlayerRenderStackState extends State<PlayerRenderStack> {
@override
void dispose() {
_log('dispose current=${_lastCurrentIndex ?? '-'}');
_log(
'dispose current=${_lastCurrentIndex ?? '-'} '
'pageController=${identityHashCode(widget.pageController)}',
);
super.dispose();
}
@@ -189,6 +197,9 @@ class _PlayerRenderStackState extends State<PlayerRenderStack> {
fallback: slot.fallback,
warmSlotReused: slot.warmSlotReused,
waitForPlaybackFrame: slot.waitForPlaybackFrame,
onVideoVisible: slot.role == PlayerRenderSlotRole.current
? widget.onCurrentVideoVisible
: null,
),
),
);
@@ -28,6 +28,7 @@ class PlayerVideoSurface extends StatefulWidget {
this.debugSlot = 'default',
this.debugIndex,
this.showFallbackBackground = true,
this.onVideoVisible,
});
final VideoPlayerController? controller;
@@ -57,6 +58,7 @@ class PlayerVideoSurface extends StatefulWidget {
final bool showFallbackBackground;
final String videoUrl;
final bool theater;
final VoidCallback? onVideoVisible;
@override
State<PlayerVideoSurface> createState() => _PlayerVideoSurfaceState();
@@ -73,6 +75,7 @@ class _PlayerVideoSurfaceState extends State<PlayerVideoSurface>
bool _confirmationScheduled = false;
bool _transitioning = false;
bool _videoVisible = false;
bool _videoVisibleNotified = false;
String? _lastLayerState;
@override
@@ -279,6 +282,7 @@ class _PlayerVideoSurfaceState extends State<PlayerVideoSurface>
});
}
_logLayer('video', widget.controller?.value);
_notifyVideoVisible();
return;
}
if (initial) {
@@ -303,6 +307,7 @@ class _PlayerVideoSurfaceState extends State<PlayerVideoSurface>
_placeholderSnapshot = null;
});
_logLayer('fade-end', widget.controller?.value);
_notifyVideoVisible();
}
void _resetForControllerChange() {
@@ -314,9 +319,20 @@ class _PlayerVideoSurfaceState extends State<PlayerVideoSurface>
_confirmationScheduled = false;
_transitioning = false;
_videoVisible = false;
_videoVisibleNotified = false;
_lastLayerState = null;
}
void _notifyVideoVisible() {
if (_videoVisibleNotified) return;
_videoVisibleNotified = true;
final callback = widget.onVideoVisible;
if (callback == null) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) callback();
});
}
void _logLayer(String phase, VideoPlayerValue? value) {
if (!kDebugMode) return;
final fallback = phase == 'video' || phase == 'fade-end'