Files
sm-emby-share/lib/features/player/player_page_lifecycle.dart
2026-07-14 11:11:36 +08:00

309 lines
10 KiB
Dart

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<void> _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<Playlist> 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<Playlist>.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<String, dynamic> 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;
}
}