@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/focus/focusable_button.dart';
|
||||
import 'package:plezy/providers/playback_state_provider.dart';
|
||||
import 'package:plezy/mpv/mpv.dart';
|
||||
import 'package:plezy/screens/video_player_screen.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -41,6 +42,69 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('item-change subtitle preference carries committed semantics without item identity', () {
|
||||
const committed = SubtitleTrack(
|
||||
id: 'source:4',
|
||||
title: 'French - SRT',
|
||||
language: 'fra',
|
||||
codec: 'srt',
|
||||
isForced: true,
|
||||
isExternal: true,
|
||||
uri: 'https://example.test/old-episode/subtitle.srt',
|
||||
);
|
||||
|
||||
final result = subtitlePreferenceForItemChange(
|
||||
hasCommittedSelection: true,
|
||||
committedTrack: committed,
|
||||
nativeTrack: SubtitleTrack.off,
|
||||
);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result!.id, 'navigation');
|
||||
expect(result.title, committed.title);
|
||||
expect(result.language, committed.language);
|
||||
expect(result.codec, committed.codec);
|
||||
expect(result.isForced, isTrue);
|
||||
expect(result.isExternal, isTrue);
|
||||
expect(result.uri, isNull);
|
||||
});
|
||||
|
||||
test('item-change subtitle preference preserves committed off and empty secondary slots', () {
|
||||
expect(
|
||||
subtitlePreferenceForItemChange(
|
||||
hasCommittedSelection: true,
|
||||
committedTrack: SubtitleTrack.off,
|
||||
nativeTrack: const SubtitleTrack(id: '7', language: 'eng'),
|
||||
),
|
||||
same(SubtitleTrack.off),
|
||||
);
|
||||
expect(
|
||||
subtitlePreferenceForItemChange(
|
||||
hasCommittedSelection: true,
|
||||
committedTrack: null,
|
||||
nativeTrack: const SubtitleTrack(id: '8', language: 'swe'),
|
||||
),
|
||||
same(SubtitleTrack.off),
|
||||
);
|
||||
});
|
||||
|
||||
test('item-change subtitle preference uses native metadata only without a committed selection', () {
|
||||
final result = subtitlePreferenceForItemChange(
|
||||
hasCommittedSelection: false,
|
||||
committedTrack: null,
|
||||
nativeTrack: const SubtitleTrack(
|
||||
id: '9',
|
||||
title: 'English',
|
||||
language: 'eng',
|
||||
uri: 'https://example.test/old-episode/native.srt',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result?.id, 'navigation');
|
||||
expect(result?.language, 'eng');
|
||||
expect(result?.uri, isNull);
|
||||
});
|
||||
|
||||
testWidgets('initialization ownership serializes rollback, retry, and route removal', (tester) async {
|
||||
final failedDispose = Completer<void>();
|
||||
final replacementInitialize = Completer<bool>();
|
||||
|
||||
@@ -924,7 +924,7 @@ void main() {
|
||||
expect(subtitleUri.queryParameters['api_key'], 'tok-abc');
|
||||
});
|
||||
|
||||
test('getPlaybackInitialization negotiates the requested DirectStream subtitle and exposes delivery', () async {
|
||||
test('getPlaybackInitialization maps semantic subtitle preferences to current source rows', () async {
|
||||
Uri? playbackInfoUri;
|
||||
String? playbackInfoBody;
|
||||
final scoped = JellyfinClient.forTesting(
|
||||
@@ -944,6 +944,7 @@ void main() {
|
||||
{'Index': 0, 'Type': 'Video'},
|
||||
{'Index': 3, 'Type': 'Subtitle', 'Codec': 'srt', 'Language': 'eng'},
|
||||
{'Index': 4, 'Type': 'Subtitle', 'Codec': 'srt', 'Language': 'fra'},
|
||||
{'Index': 5, 'Type': 'Subtitle', 'Codec': 'srt', 'Language': 'eng', 'IsForced': true},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -984,6 +985,16 @@ void main() {
|
||||
'DeliveryMethod': 'External',
|
||||
'DeliveryUrl': '/Videos/item-1/src-1/Subtitles/4/Stream.srt',
|
||||
},
|
||||
{
|
||||
'Index': 5,
|
||||
'Type': 'Subtitle',
|
||||
'Codec': 'srt',
|
||||
'Language': 'eng',
|
||||
'DisplayTitle': 'English Forced - SRT',
|
||||
'IsForced': true,
|
||||
'DeliveryMethod': 'External',
|
||||
'DeliveryUrl': '/Videos/item-1/src-1/Subtitles/5/Stream.srt',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -997,34 +1008,64 @@ void main() {
|
||||
);
|
||||
addTearDown(scoped.close);
|
||||
|
||||
final result = await scoped.getPlaybackInitialization(
|
||||
PlaybackInitializationOptions(
|
||||
metadata: testMediaItem(
|
||||
id: 'item-1',
|
||||
backend: MediaBackend.jellyfin,
|
||||
kind: MediaKind.movie,
|
||||
serverId: 'srv-1',
|
||||
),
|
||||
selectedMediaIndex: 0,
|
||||
preferredSubtitleTrack: const SubtitleTrack(
|
||||
id: 'source:4',
|
||||
title: 'French - SRT',
|
||||
language: 'fra',
|
||||
codec: 'srt',
|
||||
Future<PlaybackInitializationResult> initialize(SubtitleTrack preference) {
|
||||
return scoped.getPlaybackInitialization(
|
||||
PlaybackInitializationOptions(
|
||||
metadata: testMediaItem(
|
||||
id: 'item-1',
|
||||
backend: MediaBackend.jellyfin,
|
||||
kind: MediaKind.movie,
|
||||
serverId: 'srv-1',
|
||||
),
|
||||
selectedMediaIndex: 0,
|
||||
preferredSubtitleTrack: preference,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void expectRequestedSubtitleIndex(int? expected) {
|
||||
expect(playbackInfoUri!.queryParameters['SubtitleStreamIndex'], expected?.toString());
|
||||
final playbackInfoJson = jsonDecode(playbackInfoBody!) as Map<String, dynamic>;
|
||||
expect(playbackInfoJson['SubtitleStreamIndex'], expected);
|
||||
}
|
||||
|
||||
final result = await initialize(
|
||||
const SubtitleTrack(id: 'source:4', title: 'French - SRT', language: 'fra', codec: 'srt'),
|
||||
);
|
||||
expectRequestedSubtitleIndex(4);
|
||||
|
||||
await initialize(const SubtitleTrack(id: 'navigation', title: 'English - SRT', language: 'eng', codec: 'srt'));
|
||||
expectRequestedSubtitleIndex(3);
|
||||
|
||||
await initialize(const SubtitleTrack(id: 'source:3', title: 'French - SRT', language: 'fra', codec: 'srt'));
|
||||
expectRequestedSubtitleIndex(4);
|
||||
|
||||
await initialize(
|
||||
const SubtitleTrack(
|
||||
id: 'source:3',
|
||||
title: 'English Forced - SRT',
|
||||
language: 'eng',
|
||||
codec: 'srt',
|
||||
isForced: true,
|
||||
),
|
||||
);
|
||||
expectRequestedSubtitleIndex(5);
|
||||
|
||||
await initialize(SubtitleTrack.off);
|
||||
expectRequestedSubtitleIndex(-1);
|
||||
|
||||
await initialize(const SubtitleTrack(id: 'navigation', title: 'Japanese - SRT', language: 'jpn', codec: 'srt'));
|
||||
expectRequestedSubtitleIndex(null);
|
||||
|
||||
expect(playbackInfoUri!.queryParameters['SubtitleStreamIndex'], '4');
|
||||
final playbackInfoJson = jsonDecode(playbackInfoBody!) as Map<String, dynamic>;
|
||||
expect(playbackInfoJson['SubtitleStreamIndex'], 4);
|
||||
expect(result.playMethod, 'DirectStream');
|
||||
expect(result.mediaInfo!.subtitleTracks, hasLength(2));
|
||||
expect(result.mediaInfo!.subtitleTracks, hasLength(3));
|
||||
expect(result.mediaInfo!.subtitleTracks.every((track) => track.usesExternalDelivery), isTrue);
|
||||
expect(result.subtitleSidecars.map((sidecar) => sidecar.sourceStreamId), [3, 4]);
|
||||
expect(result.externalSubtitles, hasLength(2));
|
||||
expect(Uri.parse(result.externalSubtitles.first.uri!).path, '/Videos/item-1/src-1/Subtitles/3/Stream.srt');
|
||||
expect(Uri.parse(result.externalSubtitles.last.uri!).path, '/Videos/item-1/src-1/Subtitles/4/Stream.srt');
|
||||
expect(result.subtitleSidecars.map((sidecar) => sidecar.sourceStreamId), [3, 4, 5]);
|
||||
expect(result.externalSubtitles.map((subtitle) => Uri.parse(subtitle.uri!).path), [
|
||||
'/Videos/item-1/src-1/Subtitles/3/Stream.srt',
|
||||
'/Videos/item-1/src-1/Subtitles/4/Stream.srt',
|
||||
'/Videos/item-1/src-1/Subtitles/5/Stream.srt',
|
||||
]);
|
||||
});
|
||||
|
||||
test('getPlaybackInitialization ignores TranscodingUrl for original playback static fallback', () async {
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../test_helpers/media_items.dart';
|
||||
MediaSubtitleTrack _sourceSubtitle(
|
||||
int id, {
|
||||
String language = 'eng',
|
||||
bool forced = false,
|
||||
bool selected = false,
|
||||
bool external = false,
|
||||
bool usesExternalDelivery = false,
|
||||
@@ -21,7 +22,7 @@ MediaSubtitleTrack _sourceSubtitle(
|
||||
languageCode: language,
|
||||
title: 'Subtitle $id',
|
||||
selected: selected,
|
||||
forced: false,
|
||||
forced: forced,
|
||||
external: external,
|
||||
usesExternalDelivery: usesExternalDelivery,
|
||||
);
|
||||
@@ -178,6 +179,54 @@ void main() {
|
||||
expect(result.sidecarsAtOpen.single.uri, 'https://example.test/subtitles/3.srt');
|
||||
});
|
||||
|
||||
test('item-change semantic preference selects the matching new sidecar', () {
|
||||
final result = PlaybackSubtitleResolver.resolve(
|
||||
metadata: metadata,
|
||||
mediaInfo: _mediaInfo([
|
||||
_sourceSubtitle(7, language: 'eng', usesExternalDelivery: true),
|
||||
_sourceSubtitle(9, language: 'fra', usesExternalDelivery: true),
|
||||
]),
|
||||
sidecars: [
|
||||
_sidecar(7),
|
||||
_sidecar(9, language: 'fra'),
|
||||
],
|
||||
preferredSubtitleTrack: const SubtitleTrack(
|
||||
id: 'navigation',
|
||||
title: 'English from the previous episode',
|
||||
language: 'eng',
|
||||
codec: 'srt',
|
||||
isExternal: true,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.primarySourceStreamId, 7);
|
||||
expect(result.primarySidecar?.track.uri, 'https://example.test/subtitles/7.srt');
|
||||
expect(result.sidecarsAtOpen, hasLength(1));
|
||||
});
|
||||
|
||||
test('item-change semantic preference distinguishes forced and full subtitles in one language', () {
|
||||
final result = PlaybackSubtitleResolver.resolve(
|
||||
metadata: metadata,
|
||||
mediaInfo: _mediaInfo([
|
||||
_sourceSubtitle(7, language: 'eng', usesExternalDelivery: true),
|
||||
_sourceSubtitle(8, language: 'eng', forced: true, usesExternalDelivery: true),
|
||||
]),
|
||||
sidecars: [_sidecar(7), _sidecar(8)],
|
||||
preferredSubtitleTrack: const SubtitleTrack(
|
||||
id: 'navigation',
|
||||
title: 'English forced from the previous episode',
|
||||
language: 'eng',
|
||||
codec: 'srt',
|
||||
isForced: true,
|
||||
isExternal: true,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.primarySourceStreamId, 8);
|
||||
expect(result.primaryTrack.isForced, isTrue);
|
||||
expect(result.primarySidecar?.track.uri, 'https://example.test/subtitles/8.srt');
|
||||
});
|
||||
|
||||
test('selected embedded subtitle keeps sidecars out of the open', () {
|
||||
final result = PlaybackSubtitleResolver.resolve(
|
||||
metadata: metadata,
|
||||
|
||||
Reference in New Issue
Block a user