part of 'player_page.dart'; extension _PlayerPageSources on _PlayerPageState { PlaybackRequest _itemToRequest(PlaylistItem item) { return PlaybackRequest( itemId: item.itemId, mediaSourceId: item.mediaSourceId, startPositionTicks: item.startPositionTicks, preferredAudioStreamIndex: item.preferredAudioStreamIndex, preferredSubtitleStreamIndex: item.preferredSubtitleStreamIndex, subtitleSettings: _subtitleSettings, playerSettings: _lastRequest?.playerSettings ?? _initialPlayerSettings, danmakuSettings: _lastRequest?.danmakuSettings ?? _initialDanmakuSettings, audioSettings: _lastRequest?.audioSettings ?? _initialAudioSettings, playFromStart: item.playFromStart, targetServer: item.serverIdentity, preloadedItem: item.preloadedItem, directStreamUrl: widget.directStream?.url, directStreamHeaders: widget.directStream?.headers ?? const {}, ); } void _onCurrentItemChanged() { final pl = _playlist; if (pl == null) return; final item = pl.current.value; if (item.itemId.isEmpty) return; _relink.reset(); unawaited(_switchTo(_itemToRequest(item))); } Future _onEpisodePicked(EmbyRawItem episode) async { final pl = _playlist; if (pl == null) return; if (episode.Id == _session?.context.itemId) return; if (pl is SeriesEpisodePlaylist && episode.SeasonId != null) { await pl.goToInSeason(seasonId: episode.SeasonId!, episodeId: episode.Id); } else { await pl.goToItemId(episode.Id); } } Future _switchVersion(String sourceId) async { if (sourceId == _session?.context.mediaSourceId) return; final base = _lastRequest; if (base == null) return; _relink.reset(); final pos = _session?.engine.state.position; final resumeTicks = pos == null ? null : ticksFromDuration(pos); final req = base.copyWith( mediaSourceId: sourceId, startPositionTicks: (resumeTicks != null && resumeTicks > 0) ? resumeTicks : null, playFromStart: resumeTicks != null && resumeTicks <= 0, ); await _switchTo(req); } int? _currentResumeTicks() { final pos = _session?.engine.state.position ?? Duration.zero; final ticks = ticksFromDuration(pos); return ticks > 0 ? ticks : null; } CrossServerSearchKey? _buildCrossServerKey(PlaybackContext ctx) { final activeServerId = ctx.serverId.isEmpty ? null : ctx.serverId; if (activeServerId == null) return null; final providerIds = ctx.providerIds; final type = ctx.itemType; if (type == 'Movie') { if (providerIds.isEmpty && ctx.itemName.isEmpty) return null; return ( activeServerId: activeServerId, anchorType: 'Movie', providerIdQuery: encodeProviderIdsForKey(providerIds), seriesProviderIdQuery: '', parentIndexNumber: -1, indexNumber: -1, itemName: ctx.itemName, excludedServerId: ctx.serverId, ); } if (type == 'Episode') { final s = ctx.parentIndexNumber; final e = ctx.itemIndexNumber; if (s == null || e == null) return null; final seriesProviderIds = ctx.seriesProviderIds; if (providerIds.isEmpty && seriesProviderIds.isEmpty) return null; return ( activeServerId: activeServerId, anchorType: 'Episode', providerIdQuery: encodeProviderIdsForKey(providerIds), seriesProviderIdQuery: encodeProviderIdsForKey(seriesProviderIds), parentIndexNumber: s, indexNumber: e, itemName: ctx.itemName, excludedServerId: ctx.serverId, ); } return null; } List _filteredCrossServerCards( List cards, PlaybackContext ctx, ) { final curServerId = ctx.serverId; final curSourceId = ctx.mediaSourceId; return cards .where( (c) => !(c.serverId == curServerId && c.mediaSourceId == curSourceId), ) .toList(growable: false); } Future _switchToCrossServer(CrossServerSourceCard card) async { if (_switchInFlight) return; _relink.reset(); final resumeTicks = _currentResumeTicks(); final identity = PlaybackServerIdentity.fromCard(card); final oldPlaylist = _playlist; oldPlaylist?.current.removeListener(_onCurrentItemChanged); final newPlaylist = CrossServerPlaylist( sequence: [ CrossServerPlaylistEntry( serverId: card.serverId, serverName: card.serverName, serverUrl: card.serverUrl, token: card.token, userId: card.userId, itemId: card.landingItemId, label: card.item.Name, preloadedItem: card.item.Id == card.landingItemId ? card.item : null, ), ], initialIndex: 0, ); _playlist = newPlaylist; try { oldPlaylist?.dispose(); } catch (e) { AppLogger.warn(_tag, 'dispose old playlist failed', e); } final base = _lastRequest; final req = PlaybackRequest( itemId: card.landingItemId, mediaSourceId: card.mediaSourceId, startPositionTicks: resumeTicks, subtitleSettings: _subtitleSettings, playerSettings: base?.playerSettings ?? _initialPlayerSettings, danmakuSettings: base?.danmakuSettings ?? _initialDanmakuSettings, audioSettings: base?.audioSettings ?? _initialAudioSettings, playFromStart: resumeTicks == null, targetServer: identity, ); newPlaylist.current.addListener(_onCurrentItemChanged); await _switchTo(req); } void _toggleVersionBar() { if (_versionBarOpen.value) { _versionBarOpen.value = false; } else { _drawer.close(); _versionBarOpen.value = true; } } void _onAudioTrackSelected(PlaybackSession session, AudioTrack track) { final audioSettings = _lastRequest?.audioSettings ?? _initialAudioSettings; if (!audioSettings.rememberAudioTrack) return; final embyTracks = session.context.embyAudioTracks; if (embyTracks.isEmpty) return; var embyTrack = embyTracks.where((t) => t.index == track.index).firstOrNull; if (embyTrack == null) { final engineTracks = session.engine.state.tracks.embeddedAudio; final pos = engineTracks.indexWhere((t) => t.id == track.id); if (pos >= 0 && pos < embyTracks.length) embyTrack = embyTracks[pos]; } if (embyTrack == null) return; unawaited( _audioSettingsNotifier.saveLastAudioTrack( session.context.memoryKey, RememberedAudioTrack( streamIndex: embyTrack.index, language: embyTrack.language, codec: embyTrack.codec, channels: embyTrack.channels, displayTitle: embyTrack.displayTitle, updatedAt: DateTime.now(), ), ), ); } void _onSubtitleSelected(PlaybackSession session, int? streamIndex) { unawaited(session.selectSubtitleByStreamIndex(streamIndex)); if (streamIndex == null) { unawaited( _subtitleSettingsNotifier.saveLastSubtitleTrack( session.context.memoryKey, RememberedSubtitleTrack(streamIndex: -1, updatedAt: DateTime.now()), ), ); return; } final embyTrack = session.context.embySubtitles .where((track) => track.index == streamIndex) .firstOrNull; if (embyTrack == null) { AppLogger.warn( _tag, 'subtitle memory: streamIdx=$streamIndex not in context.embySubtitles; ' 'skipping memory write', ); return; } unawaited( _subtitleSettingsNotifier.saveLastSubtitleTrack( session.context.memoryKey, RememberedSubtitleTrack( streamIndex: embyTrack.index, language: embyTrack.language, title: embyTrack.title, updatedAt: DateTime.now(), ), ), ); } }