Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions lib/data/viewmodels/item_detail_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,25 @@ class ItemDetailViewModel extends ChangeNotifier {
AggregatedItem? _item;
AggregatedItem? get item => _item;

int? _selectedAudioIndex;
int? get selectedAudioIndex => _selectedAudioIndex;
set selectedAudioIndex(int? value) {
if (_selectedAudioIndex != value) {
_selectedAudioIndex = value;
notifyListeners();
}
}

int? _selectedSubtitleIndex;
int? get selectedSubtitleIndex => _selectedSubtitleIndex;
set selectedSubtitleIndex(int? value) {
if (_selectedSubtitleIndex != value) {
_selectedSubtitleIndex = value;
notifyListeners();
}
}


List<AggregatedItem> _similar = const [];
List<AggregatedItem> get similar => _similar;

Expand Down
20 changes: 16 additions & 4 deletions lib/ui/screens/detail/item_detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4348,8 +4348,12 @@ class _DolbyVisionPlayDecision {
}

class _ActionButtonsState extends State<_ActionButtons> {
int? _selectedAudioIndex;
int? _selectedSubtitleIndex;
int? get _selectedAudioIndex => viewModel.selectedAudioIndex;
set _selectedAudioIndex(int? value) => viewModel.selectedAudioIndex = value;

int? get _selectedSubtitleIndex => viewModel.selectedSubtitleIndex;
set _selectedSubtitleIndex(int? value) => viewModel.selectedSubtitleIndex = value;

bool _expanded = false;
bool _playLaunchInFlight = false;
bool _autoPlayTriggered = false;
Expand Down Expand Up @@ -5199,7 +5203,11 @@ class _ActionButtonsState extends State<_ActionButtons> {
final prefs = GetIt.instance<UserPreferences>();
final preferred = prefs.get(UserPreferences.defaultAudioLanguage).trim();
if (preferred.isEmpty) {
return null;
final defaultStream = audioStreams.firstWhere(
(s) => s['IsDefault'] == true,
orElse: () => <String, dynamic>{},
);
return defaultStream['Index'] as int?;
}

final preferredNormalized = normalizeLanguage(preferred);
Expand Down Expand Up @@ -5236,7 +5244,11 @@ class _ActionButtonsState extends State<_ActionButtons> {

final preferred = prefs.get(UserPreferences.defaultSubtitleLanguage).trim();
if (preferred.isEmpty) {
return null;
final defaultStream = subtitleStreams.firstWhere(
(s) => s['IsDefault'] == true,
orElse: () => <String, dynamic>{},
);
return defaultStream['Index'] as int?;
}

final preferredNormalized = normalizeLanguage(preferred);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,13 @@ class Media3VideoView(
private var selectedSubtitleIsBitmap = false
private var selectedExternalSubtitleUrl: String? = null
private var subtitleTrackEnabled = false

private var pendingSubtitleIndex: Int? = null
private var pendingSubtitleCodec: String? = null
private var pendingSubtitleIsExternal: Boolean? = null
private var pendingSubtitleIsBitmap: Boolean? = null
private var pendingExternalSubtitleUrl: String? = null
private var pendingAudioIndex: Int? = null
private var zoomMode = ZoomMode.FIT
private var videoWidthPx = 0
private var videoHeightPx = 0
Expand Down Expand Up @@ -665,6 +672,28 @@ class Media3VideoView(
}

override fun onTracksChanged(tracks: androidx.media3.common.Tracks) {
pendingSubtitleIndex?.let { index ->
if (selectTrack(C.TRACK_TYPE_TEXT, index)) {
selectedSubtitleCodec = pendingSubtitleCodec?.trim()?.lowercase()
selectedSubtitleIsExternal = pendingSubtitleIsExternal ?: false
selectedSubtitleIsBitmap = pendingSubtitleIsBitmap ?: false
selectedExternalSubtitleUrl = pendingExternalSubtitleUrl?.takeIf { it.isNotBlank() }
subtitleTrackEnabled = true
applyTrackSelectorForCurrentSource()
refreshSubtitleRendererMode()

pendingSubtitleIndex = null
pendingSubtitleCodec = null
pendingSubtitleIsExternal = null
pendingSubtitleIsBitmap = null
pendingExternalSubtitleUrl = null
}
}
pendingAudioIndex?.let { index ->
if (selectTrack(C.TRACK_TYPE_AUDIO, index)) {
pendingAudioIndex = null
}
}
emitTracksChanged()
emitState()
}
Expand Down Expand Up @@ -1028,7 +1057,11 @@ class Media3VideoView(

"setAudioTrack" -> {
val index = ((call.arguments as? Map<*, *>)?.get("index") as? Number)?.toInt() ?: 0
selectTrack(C.TRACK_TYPE_AUDIO, index)
pendingAudioIndex = index
val selected = selectTrack(C.TRACK_TYPE_AUDIO, index)
if (selected) {
pendingAudioIndex = null
}
result.success(null)
}

Expand All @@ -1039,6 +1072,13 @@ class Media3VideoView(
val isExternal = args?.get("isExternalSubtitle") as? Boolean ?: false
val isBitmap = args?.get("isBitmapSubtitle") as? Boolean ?: false
val externalUrl = args?.get("externalSubtitleUrl")?.toString()

pendingSubtitleIndex = index
pendingSubtitleCodec = codec
pendingSubtitleIsExternal = isExternal
pendingSubtitleIsBitmap = isBitmap
pendingExternalSubtitleUrl = externalUrl

val selected = selectTrack(C.TRACK_TYPE_TEXT, index)
if (selected) {
selectedSubtitleCodec = codec?.trim()?.lowercase()
Expand All @@ -1048,6 +1088,12 @@ class Media3VideoView(
subtitleTrackEnabled = true
applyTrackSelectorForCurrentSource()
refreshSubtitleRendererMode()

pendingSubtitleIndex = null
pendingSubtitleCodec = null
pendingSubtitleIsExternal = null
pendingSubtitleIsBitmap = null
pendingExternalSubtitleUrl = null
}
result.success(null)
}
Expand All @@ -1063,6 +1109,13 @@ class Media3VideoView(
selectedSubtitleIsBitmap = false
selectedExternalSubtitleUrl = null
subtitleTrackEnabled = false

pendingSubtitleIndex = null
pendingSubtitleCodec = null
pendingSubtitleIsExternal = null
pendingSubtitleIsBitmap = null
pendingExternalSubtitleUrl = null

applyTrackSelectorForCurrentSource()
clearAssSubtitleScript()
refreshSubtitleRendererMode()
Expand Down Expand Up @@ -1303,6 +1356,12 @@ class Media3VideoView(
selectedSubtitleIsBitmap = false
selectedExternalSubtitleUrl = null
subtitleTrackEnabled = false
pendingSubtitleIndex = null
pendingSubtitleCodec = null
pendingSubtitleIsExternal = null
pendingSubtitleIsBitmap = null
pendingExternalSubtitleUrl = null
pendingAudioIndex = null
firstFrameRendered = false
firstFrameCover.visibility = View.VISIBLE
cancelPendingSubtitleCue(clearView = true)
Expand Down
36 changes: 18 additions & 18 deletions packages/playback_core/lib/src/media_stream_resolver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,29 @@ abstract class MediaStreamResolver {
}

static String applyStreamIndices(String url, int? audioStreamIndex, int? subtitleStreamIndex) {
var result = url;
try {
final uri = Uri.parse(url);
final params = Map<String, String>.from(uri.queryParameters);

if (audioStreamIndex != null) {
final audioRegex = RegExp(r'AudioStreamIndex=\d+');
if (audioRegex.hasMatch(result)) {
result = result.replaceFirst(audioRegex, 'AudioStreamIndex=$audioStreamIndex');
} else {
result = '$result&AudioStreamIndex=$audioStreamIndex';
if (audioStreamIndex != null) {
params['AudioStreamIndex'] = '$audioStreamIndex';
params['audioStreamIndex'] = '$audioStreamIndex';
}
}

if (subtitleStreamIndex != null && subtitleStreamIndex >= 0) {
final subRegex = RegExp(r'SubtitleStreamIndex=\d+');
if (subRegex.hasMatch(result)) {
result = result.replaceFirst(subRegex, 'SubtitleStreamIndex=$subtitleStreamIndex');
} else {
result = '$result&SubtitleStreamIndex=$subtitleStreamIndex';
if (subtitleStreamIndex != null) {
if (subtitleStreamIndex >= 0) {
params['SubtitleStreamIndex'] = '$subtitleStreamIndex';
params['subtitleStreamIndex'] = '$subtitleStreamIndex';
} else if (subtitleStreamIndex == -1) {
params.remove('SubtitleStreamIndex');
params.remove('subtitleStreamIndex');
}
}
} else if (subtitleStreamIndex == -1) {
result = result.replaceAll(RegExp(r'[&?]SubtitleStreamIndex=\d+'), '');
}

return result;
return uri.replace(queryParameters: params).toString();
} catch (_) {
return url;
}
}

static List<ExternalSubtitle> extractExternalSubtitles(
Expand Down
16 changes: 10 additions & 6 deletions packages/playback_core/lib/src/playback_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -539,17 +539,18 @@ class PlaybackManager implements AudioOwnable {
_sessionEndedController.add(null);
}

void _applyPendingItemOverridesIfNeeded(String itemId) {
bool _applyPendingItemOverridesIfNeeded(String itemId) {
final pendingItemId = _pendingItemOverrideId;
if (pendingItemId == null || pendingItemId != itemId) {
return;
return false;
}

_audioStreamIndex = _pendingItemAudioStreamIndex;
_subtitleStreamIndex = _pendingItemSubtitleStreamIndex;
_subtitleSelectionExplicit = false;
_mediaSourceId = _pendingItemMediaSourceId;
_clearPendingItemOverrides();
return true;
}

void _onBackendErrorEvent(Map<String, dynamic> event) {
Expand Down Expand Up @@ -845,8 +846,8 @@ class PlaybackManager implements AudioOwnable {
_lastKnownPosition = Duration.zero;
final sessionToken = ++_playbackSessionToken;
final itemId = _traceItemId(item);
_applyPendingItemOverridesIfNeeded(itemId);
if (_lastItemId != null && _lastItemId != itemId) {
final appliedOverrides = _applyPendingItemOverridesIfNeeded(itemId);
if (!appliedOverrides && _lastItemId != null && _lastItemId != itemId) {
_translateTrackSelectionsForNewItem(item);
}
_lastItemId = itemId;
Expand Down Expand Up @@ -895,6 +896,7 @@ class PlaybackManager implements AudioOwnable {
enableDirectStream: enableDirectStream,
enableTranscoding: enableTranscoding,
);

_setBringupState(
PlaybackBringupState(
phase: PlaybackBringupPhase.opening,
Expand Down Expand Up @@ -1153,7 +1155,8 @@ class PlaybackManager implements AudioOwnable {
if (_subtitleStreamIndex == -1) {
_waitAndDisableSubtitles(sessionToken);
}
} else if (resolution.playMethod == StreamPlayMethod.transcode) {
} else if (resolution.playMethod == StreamPlayMethod.transcode ||
resolution.playMethod == StreamPlayMethod.directStream) {
if (_subtitleStreamIndex != null && _subtitleStreamIndex != -1) {
final isBurnedIn =
(_isSubtitleBitmap(_subtitleStreamIndex!) &&
Expand Down Expand Up @@ -1576,7 +1579,8 @@ class PlaybackManager implements AudioOwnable {
} else {
_waitAndApplyTrackSelections(_playbackSessionToken);
}
} else if (_currentResolution?.playMethod == StreamPlayMethod.transcode) {
} else if (_currentResolution?.playMethod == StreamPlayMethod.transcode ||
_currentResolution?.playMethod == StreamPlayMethod.directStream) {
final previousWasBurned =
(previousSubtitleStreamIndex != null &&
previousSubtitleStreamIndex >= 0 &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ class EmbyMediaStreamResolver implements MediaStreamResolver {
final source = _selectBestSource(info.mediaSources, preferredId: mediaSourceId);
var (url, playMethod) = _resolveStreamUrl(itemId, source);

if (playMethod == StreamPlayMethod.transcode) {
if (playMethod == StreamPlayMethod.transcode || playMethod == StreamPlayMethod.directStream) {
url = MediaStreamResolver.applyStreamIndices(url, audioStreamIndex, subtitleStreamIndex);
url = url
.replaceFirst(RegExp(r'\?StartTimeTicks=\d+&'), '?')
.replaceFirst(RegExp(r'[&?]StartTimeTicks=\d+'), '');
.replaceFirst(RegExp(r'\?StartTimeTicks=\d+&', caseSensitive: false), '?')
.replaceFirst(RegExp(r'[&?]StartTimeTicks=\d+', caseSensitive: false), '');
}

url = _appendAuth(url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ class JellyfinMediaStreamResolver implements MediaStreamResolver {
final isAudio = isAudioByStreams || _isAudioMediaItem(mediaItem);
var (url, playMethod) = _resolveStreamUrl(itemId, source, isAudio: isAudio);

if (playMethod == StreamPlayMethod.transcode) {
if (playMethod == StreamPlayMethod.transcode || playMethod == StreamPlayMethod.directStream) {
url = MediaStreamResolver.applyStreamIndices(url, audioStreamIndex, subtitleStreamIndex);
url = url
.replaceFirst(RegExp(r'\?StartTimeTicks=\d+&'), '?')
.replaceFirst(RegExp(r'[&?]StartTimeTicks=\d+'), '');
.replaceFirst(RegExp(r'\?StartTimeTicks=\d+&', caseSensitive: false), '?')
.replaceFirst(RegExp(r'[&?]StartTimeTicks=\d+', caseSensitive: false), '');
}

// Append auth token for mpv (which doesn't use our Dio interceptors).
Expand Down
10 changes: 10 additions & 0 deletions packages/server_emby/lib/src/api/emby_playback_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,22 @@ class EmbyPlaybackApi implements PlaybackApi {
'UserId': ?userId,
...?requestBody,
};

final audioStreamIndex = requestBody?['AudioStreamIndex'] ?? requestBody?['audioStreamIndex'];
final subtitleStreamIndex = requestBody?['SubtitleStreamIndex'] ?? requestBody?['subtitleStreamIndex'];
final mediaSourceId = requestBody?['MediaSourceId'] ?? requestBody?['mediaSourceId'];
final maxStreamingBitrate = requestBody?['MaxStreamingBitrate'] ?? requestBody?['maxStreamingBitrate'];

final response = await _dio.post(
'/Items/$itemId/PlaybackInfo',
data: body,
queryParameters: {
'userId': ?userId,
'startTimeTicks': ?startTimeTicks,
'audioStreamIndex': ?audioStreamIndex?.toString(),
'subtitleStreamIndex': ?subtitleStreamIndex?.toString(),
'mediaSourceId': ?mediaSourceId,
'maxStreamingBitrate': ?maxStreamingBitrate?.toString(),
},
);
return response.data as Map<String, dynamic>;
Expand Down
10 changes: 10 additions & 0 deletions packages/server_jellyfin/lib/src/api/jellyfin_playback_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,22 @@ class JellyfinPlaybackApi implements PlaybackApi {
'UserId': ?userId,
...?requestBody,
};

final audioStreamIndex = requestBody?['AudioStreamIndex'] ?? requestBody?['audioStreamIndex'];
final subtitleStreamIndex = requestBody?['SubtitleStreamIndex'] ?? requestBody?['subtitleStreamIndex'];
final mediaSourceId = requestBody?['MediaSourceId'] ?? requestBody?['mediaSourceId'];
final maxStreamingBitrate = requestBody?['MaxStreamingBitrate'] ?? requestBody?['maxStreamingBitrate'];

final response = await _dio.post(
'/Items/$itemId/PlaybackInfo',
data: body,
queryParameters: {
'userId': ?userId,
'startTimeTicks': ?startTimeTicks,
'audioStreamIndex': ?audioStreamIndex?.toString(),
'subtitleStreamIndex': ?subtitleStreamIndex?.toString(),
'mediaSourceId': ?mediaSourceId,
'maxStreamingBitrate': ?maxStreamingBitrate?.toString(),
},
);
return response.data as Map<String, dynamic>;
Expand Down
Loading