Initial commit
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user