part of 'player_page.dart'; extension _PlayerPageLifecycle on _PlayerPageState { void _onPanelChanged() { if (_drawer.isOpen && _drawer.category == PlayerPanelCategory.episode) { WidgetsBinding.instance.addPostFrameCallback( (_) => _episodeBodyKey.currentState?.ensureLoaded(), ); } if (mounted) setState(() {}); } Future _initialize() async { try { final directStream = widget.directStream; final activeSession = ref.read(activeSessionProvider); if (activeSession == null && directStream == null) { if (mounted) { setState(() => _errorMessage = '没有活跃的会话'); } return; } AppLogger.debug( _tag, directStream != null ? 'init direct stream' : 'init server=${activeSession!.serverUrl} ' 'userId=${activeSession.userId}', ); _prewarmPlayerEngine(); final bootstrap = await ( ref.read(deviceIdProvider.future), ref.read(appVersionProvider.future), ref.read(playbackResolverProvider.future), ref.read(embyGatewayProvider.future), ).wait; final deviceId = bootstrap.$1; final appVersion = bootstrap.$2; final resolver = bootstrap.$3; final gateway = bootstrap.$4; if (!mounted) return; final traktDepsFuture = directStream == null ? _loadTraktDeps() : Future.value(null); final prefetchFuture = directStream == null ? _loadPrefetchedPlaybackInfo(activeSession!) : Future.value(null); final scrobbleStatus = ref.read(traktScrobbleStatusProvider.notifier); final pendingSeq = ref.read(pendingCrossServerSequenceProvider); if (pendingSeq != null) { ref.read(pendingCrossServerSequenceProvider.notifier).state = null; } final settingsFuture = ( ref .read(subtitleSettingsProvider.future) .then( (value) => value, onError: (Object e) { AppLogger.warn(_tag, 'load subtitle settings failed', e); return const SubtitleStyleSettings(); }, ), Future.value( ref.read(playerSettingsProvider).value ?? const PlayerSettingsState(), ), ref .read(danmakuSettingsProvider.future) .then( (value) => value, onError: (Object e) { AppLogger.warn(_tag, 'load danmaku settings failed', e); return const DanmakuSettingsState(); }, ), ref .read(audioSettingsProvider.future) .then( (value) => value, onError: (Object e) { AppLogger.warn(_tag, 'load audio settings failed', e); return const AudioSettingsState(); }, ), ).wait; final Future playlistFuture; if (directStream != null) { final itemType = directStream.episode != null ? 'Episode' : 'Movie'; final syntheticItem = EmbyRawItem( Id: widget.itemId, Name: directStream.title, Type: itemType, SeriesName: directStream.seriesName, IndexNumber: directStream.episode, RunTimeTicks: directStream.durationSeconds == null ? null : directStream.durationSeconds! * kTicksPerSecond, ); playlistFuture = Future.value( SingleItemPlaylist( item: PlaylistItem( itemId: widget.itemId, displayLabel: directStream.title, playFromStart: true, preloadedItem: syntheticItem, ), ), ); } else { playlistFuture = playlistFromLaunch( itemId: widget.itemId, mediaSourceId: widget.mediaSourceId, startPositionTicks: widget.startPositionTicks, preferredAudioStreamIndex: widget.preferredAudioStreamIndex, preferredSubtitleStreamIndex: widget.preferredSubtitleStreamIndex, playFromStart: widget.fromStart, crossServerSequence: pendingSeq, loadDetail: (id) { final serverId = ref.read(activeSessionProvider)?.serverId ?? ''; return ref.read( mediaDetailDataProvider((serverId: serverId, itemId: id)).future, ); }, loadEpisodes: ({required seriesId, required seasonId}) { final serverId = ref.read(activeSessionProvider)?.serverId ?? ''; return ref.read( seriesEpisodesDataProvider(( serverId: serverId, seriesId: seriesId, seasonId: seasonId, )).future, ); }, loadSeasons: (seriesId) { final serverId = ref.read(activeSessionProvider)?.serverId ?? ''; return ref.read( seriesSeasonsDataProvider(( serverId: serverId, seriesId: seriesId, )).future, ); }, ); } final ready = await (settingsFuture, playlistFuture).wait; final settings = ready.$1; _subtitleSettings = settings.$1; final playerSettings = ref.read(playerSettingsProvider).value ?? settings.$2; final danmakuSettings = settings.$3; if (!mounted) return; final audioSettings = settings.$4; _prefetchedPlaybackInfo = await prefetchFuture; final traktDeps = await traktDepsFuture; if (!mounted) return; _builder = PlaybackSessionBuilder( resolver: resolver, gateway: gateway, metadataResolver: MediaSourceMetadataResolver( probeService: MediaProbeService.instance, ), engineHolder: _engineHolder, readActiveSession: () { if (!mounted) throw const CancelledException('player unmounted'); return ref.read(activeSessionProvider)!; }, deviceId: deviceId, deviceName: kEmbyDeviceName, playerSettingsNotifier: _playerSettingsNotifier, audioSettingsNotifier: _audioSettingsNotifier, subtitleSettingsNotifier: ref.read(subtitleSettingsProvider.notifier), loadItemDetail: (serverId, itemId) async { if (!mounted) throw const CancelledException('player unmounted'); final res = await ref.read( mediaDetailDataProvider(( serverId: serverId, itemId: itemId, )).future, ); return res.base; }, readEngineOverride: () { if (!mounted) throw const CancelledException('player unmounted'); return ref.read(playerEngineOverrideProvider).value ?? PlayerEngineOverride.auto; }, traktGateway: traktDeps?.gateway, traktQueue: traktDeps?.queue, traktAppVersion: appVersion, traktOnSuccess: scrobbleStatus.recordSuccess, traktOnError: scrobbleStatus.recordError, ); _initialPlayerSettings = playerSettings; _initialDanmakuSettings = danmakuSettings; _initialAudioSettings = audioSettings; _panelSettings = danmakuSettings.panelSettings; _playlist = ready.$2; if (!mounted) { _playlist?.dispose(); _playlist = null; return; } _playlist!.current.addListener(_onCurrentItemChanged); _onCurrentItemChanged(); } catch (e, s) { AppLogger.error(_tag, 'initialize failed', e, s); if (mounted) setState(() => _errorMessage = formatUserError(e)); } } void _prewarmPlayerEngine() { if (_engineHolder.current != null) return; final override = ref.read(playerEngineOverrideProvider).value ?? PlayerEngineOverride.auto; final kind = resolvePlayerEngineKind( override, isDolbyVision: false, videoCodec: null, ); unawaited(() async { try { final engine = await _engineHolder.acquire( kind, () => PlaybackSessionBuilder.createEngine(kind), ); AppLogger.debug(_tag, 'engine prewarmed kind=${kind.name}'); if (engine is Media3PlayerEngine) { await engine.prewarmNative(); } } catch (e) { AppLogger.debug(_tag, 'engine prewarm failed', e); } }()); } Future<({AuthedTraktGateway gateway, TraktPendingSyncQueue queue})?> _loadTraktDeps() async { try { final traktGateway = ref.read(authedTraktGatewayProvider); final queue = await ref.read(traktSyncQueueProvider.future); unawaited(queue.drain()); return (gateway: traktGateway, queue: queue); } catch (e) { AppLogger.warn(_tag, 'trakt deps unavailable, scrobble disabled', e); return null; } } Future<({int startTimeTicks, Map info})?> _loadPrefetchedPlaybackInfo(AuthedSession activeSession) async { try { final startTimeTicks = widget.fromStart ? 0 : (widget.startPositionTicks ?? 0); final serverId = widget.serverId ?? activeSession.serverId; final key = ( serverId: serverId, itemId: widget.itemId, mediaSourceId: widget.mediaSourceId, startTimeTicks: startTimeTicks, ); AppLogger.debug( _tag, 'playbackInfo prefetch await ${describePlaybackInfoPrefetchKey(key)}', ); final prefetched = await ref.read( playbackInfoPrefetchProvider(key).future, ); if (prefetched == null || prefetched.info.isEmpty) { AppLogger.debug( _tag, 'playbackInfo prefetch miss ${describePlaybackInfoPrefetchKey(key)}', ); return null; } AppLogger.debug( _tag, 'playbackInfo prefetch hit ${describePlaybackInfoPrefetchKey(key)}', ); return (startTimeTicks: prefetched.startTimeTicks, info: prefetched.info); } catch (e) { AppLogger.debug(_tag, 'playbackInfo prefetch failed; fallback', e); return null; } } int _prefetchStartTimeTicksFor(PlaybackRequest req) { if (req.playFromStart) return 0; final explicitTicks = req.startPositionTicks; if (explicitTicks != null && explicitTicks > 0) return explicitTicks; return req.preloadedItem?.UserData?.PlaybackPositionTicks ?? 0; } }