Files
plezy/test/services/playback_session_test.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

124 lines
4.8 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/media/media_version.dart';
import 'package:plezy/models/transcode_quality_preset.dart';
import 'package:plezy/services/playback_context.dart';
import 'package:plezy/services/playback_initialization_types.dart';
import 'package:plezy/services/playback_session.dart';
PlaybackContext _context(PlaybackInitializationResult result) {
return PlaybackContext(
metadata: MediaItem(id: 'item-1', backend: MediaBackend.plex, kind: MediaKind.movie, serverId: 'srv'),
result: result,
sourceKind: result.usesLocalMedia ? PlaybackSourceKind.localFile : PlaybackSourceKind.remoteDirect,
reportingMode: PlaybackReportingMode.online,
streamHeaders: const {'X-Test': 'token'},
);
}
void main() {
group('PlaybackSession.fromContext', () {
test('keeps the requested preset when no fallback occurred', () {
final session = PlaybackSession.fromContext(
_context(PlaybackInitializationResult(availableVersions: const [], videoUrl: 'u')),
requestedQualityPreset: TranscodeQualityPreset.p1080_8mbps,
);
expect(session.qualityPreset, TranscodeQualityPreset.p1080_8mbps);
});
test('falls back to original when the backend rejected the preset', () {
final session = PlaybackSession.fromContext(
_context(
PlaybackInitializationResult(
availableVersions: const [],
videoUrl: 'u',
fallbackReason: TranscodeFallbackReason.values.first,
),
),
requestedQualityPreset: TranscodeQualityPreset.p1080_8mbps,
);
expect(session.qualityPreset, TranscodeQualityPreset.original);
});
test('an original-quality request ignores the fallback reason', () {
final session = PlaybackSession.fromContext(
_context(
PlaybackInitializationResult(
availableVersions: const [],
videoUrl: 'u',
fallbackReason: TranscodeFallbackReason.values.first,
),
),
requestedQualityPreset: TranscodeQualityPreset.original,
);
expect(session.qualityPreset, TranscodeQualityPreset.original);
});
test('refines the media source id from the clamped version index', () {
final versions = [MediaVersion(id: 'v0'), MediaVersion(id: 'v1')];
final session = PlaybackSession.fromContext(
_context(PlaybackInitializationResult(availableVersions: versions, videoUrl: 'u', selectedMediaIndex: 1)),
requestedQualityPreset: TranscodeQualityPreset.original,
requestedMediaSourceId: 'requested',
);
expect(session.mediaSourceId, 'v1');
expect(session.mediaIndex, 1);
});
test('keeps the requested source id when the index is out of range', () {
final session = PlaybackSession.fromContext(
_context(PlaybackInitializationResult(availableVersions: const [], videoUrl: 'u', selectedMediaIndex: 2)),
requestedQualityPreset: TranscodeQualityPreset.original,
requestedMediaSourceId: 'requested',
);
expect(session.mediaSourceId, 'requested');
});
test('prefers the result source id over derived and requested ids', () {
// Offline fallback playback: the result names the downloaded version,
// which must win over the (stale) requested id even when a version
// list would derive something else.
final versions = [MediaVersion(id: 'v0'), MediaVersion(id: 'v1')];
final session = PlaybackSession.fromContext(
_context(
PlaybackInitializationResult(
availableVersions: versions,
videoUrl: 'u',
selectedMediaIndex: 1,
selectedMediaSourceId: 'downloaded',
),
),
requestedQualityPreset: TranscodeQualityPreset.original,
requestedMediaSourceId: 'requested',
);
expect(session.mediaSourceId, 'downloaded');
});
});
test('forwarding getters mirror the resolver output', () {
final result = PlaybackInitializationResult(
availableVersions: [MediaVersion(id: 'v0')],
videoUrl: 'u',
isTranscoding: true,
playSessionId: 'psid',
playMethod: 'Transcode',
activeAudioStreamId: 7,
);
final session = PlaybackSession.fromContext(
_context(result),
requestedQualityPreset: TranscodeQualityPreset.original,
);
expect(session.isTranscoding, isTrue);
expect(session.isOffline, isFalse);
expect(session.playSessionId, 'psid');
expect(session.playMethod, 'Transcode');
expect(session.audioStreamId, 7);
expect(session.availableVersions, hasLength(1));
expect(session.streamHeaders, containsPair('X-Test', 'token'));
expect(session.metadata.id, 'item-1');
});
}