246 lines
7.8 KiB
Dart
246 lines
7.8 KiB
Dart
|
|
|
||
|
|
|
||
|
|
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 <String, String>{},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
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<void> _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<void> _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<CrossServerSourceCard> _filteredCrossServerCards(
|
||
|
|
List<CrossServerSourceCard> 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<void> _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(),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|