Initial commit

This commit is contained in:
admin1
2026-07-14 11:11:36 +08:00
commit 656499cf94
604 changed files with 119518 additions and 0 deletions
@@ -0,0 +1,245 @@
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(),
),
),
);
}
}