Files
plezy/lib/services/playback_session.dart
T
edde746 fd4291aafb fix(playback): play the downloaded version when offline close #1440
Plain Play requests the default media version (index 0 or the saved
preference), but the offline resolvers rejected the single downloaded
row when a non-default version was downloaded, then threw "No video
URL available" with no client to fall back to.

Three-part fix sharing one matcher (downloadedVersionMatches):
- getPlaybackData falls back to the downloaded version when there is no
  client to stream from; the result now carries the effective
  mediaIndex/mediaSourceId so cached media info and the committed
  session describe the file actually played.
- navigateToVideoPlayer seeds the selection from the download record
  for isOffline plays with no explicit version, covering the external
  player branch and offline-library plays with a reachable server.
- PlaybackSession.fromContext prefers the result source id over the
  requested one, keeping in-player state in sync after a fallback.

Online pinning is untouched: with a live client an explicitly requested
non-downloaded version still streams from the server.
2026-07-03 10:19:55 +02:00

79 lines
2.9 KiB
Dart

import '../media/media_item.dart';
import '../media/media_server_client.dart';
import '../media/media_source_info.dart';
import '../media/media_version.dart';
import '../models/transcode_quality_preset.dart';
import 'playback_context.dart';
import 'playback_initialization_types.dart';
/// Immutable snapshot of everything that describes the item currently loaded
/// in the player: the resolver output plus the effective (post-fallback,
/// post-clamping) source selections.
///
/// Built once per resolve and swapped atomically on the screen — failure
/// before the swap means the previous session (and the state derived from
/// it) stays untouched, which replaces the old field-by-field
/// snapshot/rollback bookkeeping.
class PlaybackSession {
final PlaybackContext context;
/// Effective quality preset — the requested preset, downgraded to
/// original when the backend reported a transcode fallback.
final TranscodeQualityPreset qualityPreset;
/// Effective media version id, refined from the resolver's clamped
/// version index when the version list provides one.
final String? mediaSourceId;
const PlaybackSession({required this.context, required this.qualityPreset, this.mediaSourceId});
/// Derives the effective selections from a resolved [context]:
/// quality falls back to original when the backend rejected the requested
/// preset, and the media source id follows the clamped version index.
factory PlaybackSession.fromContext(
PlaybackContext context, {
required TranscodeQualityPreset requestedQualityPreset,
String? requestedMediaSourceId,
}) {
final result = context.result;
final fellBackToOriginal = result.fallbackReason != null && !requestedQualityPreset.isOriginal;
return PlaybackSession(
context: context,
qualityPreset: fellBackToOriginal ? TranscodeQualityPreset.original : requestedQualityPreset,
mediaSourceId:
result.selectedMediaSourceId ??
mediaSourceIdForIndex(result.availableVersions, result.selectedMediaIndex) ??
requestedMediaSourceId,
);
}
static String? mediaSourceIdForIndex(List<MediaVersion> versions, int index) {
if (index < 0 || index >= versions.length) return null;
return versions[index].id;
}
PlaybackInitializationResult get result => context.result;
MediaItem get metadata => context.metadata;
MediaServerClient? get reportingClient => context.reportingClient;
bool get isTranscoding => result.isTranscoding;
bool get isOffline => result.isOffline;
String? get playSessionId => result.playSessionId;
String? get playMethod => result.playMethod;
int? get audioStreamId => result.activeAudioStreamId;
int get mediaIndex => result.selectedMediaIndex;
List<MediaVersion> get availableVersions => result.availableVersions;
MediaSourceInfo? get mediaInfo => result.mediaInfo;
Map<String, String>? get streamHeaders => context.streamHeaders;
}