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
10 changes: 7 additions & 3 deletions lib/di/modules/playback_module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import '../../preference/preference_constants.dart';
import '../../preference/user_preferences.dart';
import '../../syncplay/syncplay_manager.dart';
import '../../util/platform_detection.dart';
import '../../util/episode_playability.dart';

final _getIt = GetIt.instance;

Expand Down Expand Up @@ -316,11 +317,14 @@ Future<List<dynamic>> _nextSeasonItemsProvider(
seriesId: seriesId,
seasonId: nextSeasonId,
);
if (nextSeasonEpisodes.isEmpty ||
nextSeasonEpisodes.first.mediaSources.isEmpty) {
final playableEpisodes = nextSeasonEpisodes
.where(isEligibleNextEpisodeCandidate)
.toList();
if (playableEpisodes.isEmpty ||
playableEpisodes.first.mediaSources.isEmpty) {
return const <dynamic>[];
}
return nextSeasonEpisodes;
return playableEpisodes;
} catch (_) {
return const <dynamic>[];
}
Expand Down
3 changes: 2 additions & 1 deletion lib/playback/html_video_backend_stub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:playback_core/playback_core.dart';
import '../preference/user_preferences.dart';
import 'html_video_backend_profile.dart';

class HtmlVideoBackend implements PlayerBackend {
class HtmlVideoBackend extends PlayerBackend {
HtmlVideoBackend(this._prefs);

final UserPreferences _prefs;
Expand Down Expand Up @@ -85,6 +85,7 @@ class HtmlVideoBackend implements PlayerBackend {
@override
Future<void> setAudioTrack(int index) async {}


@override
Future<void> setSubtitleTrack(
int index, {
Expand Down
3 changes: 2 additions & 1 deletion lib/playback/html_video_backend_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ extension type _MoonfinHlsBridge._(JSObject _) implements JSObject {
external void destroy(JSAny? controller);
}

class HtmlVideoBackend implements PlayerBackend {
class HtmlVideoBackend extends PlayerBackend {
HtmlVideoBackend(this._prefs)
: _viewType = 'moonfin-html-video-${_nextViewId++}' {
_videoElement = _createVideoElement();
Expand Down Expand Up @@ -440,6 +440,7 @@ class HtmlVideoBackend implements PlayerBackend {
Future<void> setAudioTrack(int index) async {
}


@override
Future<void> setSubtitleTrack(
int index, {
Expand Down
3 changes: 2 additions & 1 deletion lib/playback/media3_player_backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import 'audio_capability_profile.dart';
import 'device_profile_builder.dart';
import 'known_defects.dart';

class Media3PlayerBackend implements PlayerBackend {
class Media3PlayerBackend extends PlayerBackend {
static const _discontinuityWindowMs = 15000;
static const _discontinuityThreshold = 3;

Expand Down Expand Up @@ -543,6 +543,7 @@ class Media3PlayerBackend implements PlayerBackend {
await _invoke<void>('setAudioTrack', {'index': index});
}


@override
Future<void> setSubtitleTrack(
int index, {
Expand Down
42 changes: 41 additions & 1 deletion lib/playback/media_kit_player_backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ class _MediaKitDeviceProfileCapabilities {
}
}

class MediaKitPlayerBackend implements PlayerBackend {
class MediaKitPlayerBackend extends PlayerBackend {
static const Duration _linuxHwdecFirstFrameTimeout = Duration(
milliseconds: 1500,
);
Expand Down Expand Up @@ -543,6 +543,17 @@ class MediaKitPlayerBackend implements PlayerBackend {
await _applyAudioPassthroughOptions();
await _applyCustomMpvConfIfEnabled();
await _applyAssOverrideMode();

if (_player.platform is NativePlayer) {
final native = _player.platform as NativePlayer;
await _nativeSetProperty(native, 'sid', 'auto');
await _nativeSetProperty(native, 'secondary-sid', 'no');
await _nativeSetProperty(native, 'sub-visibility', 'yes');
if (_useLibass) {
await _nativeSetProperty(native, 'sub-ass', 'yes');
}
}

final media = Media(url);
final openPaused = startPosition > Duration.zero;
await _player.open(media, play: !openPaused);
Expand Down Expand Up @@ -1226,6 +1237,35 @@ class MediaKitPlayerBackend implements PlayerBackend {
}
}

@override
int? get activeSubtitleTrackIndex {
if (_player.platform is! NativePlayer) {
return null;
}
try {
final active = _player.state.track.subtitle;
if (active.id == 'no') {
return -1;
}
if (active.id == 'auto') {
return null;
}
final subtitleTracks = _player.state.tracks.subtitle;
final playableSubtitleTracks = subtitleTracks
.where((t) => t.id != 'auto' && t.id != 'no')
.toList();
final idx = playableSubtitleTracks.indexWhere((t) => t.id == active.id);
if (idx >= 0) {
return idx + 1;
}
} catch (_) {}
return null;
}

@override

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1265 - 1288 can be deleted. dead code not used by anything

Future<int?> getActiveSubtitleTrackIndexAsync() async => activeSubtitleTrackIndex;


@override
Future<void> setSubtitleTrack(
int mpvTrackId, {
Expand Down
4 changes: 2 additions & 2 deletions lib/playback/tizen_player_backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'dart:async';
import 'package:video_player/video_player.dart';
import 'package:playback_core/playback_core.dart';

import '../preference/preference_constants.dart';
import '../preference/user_preferences.dart';
import 'audio_capability_profile.dart';
import 'device_profile_builder.dart';
Expand All @@ -22,7 +21,7 @@ import 'known_defects.dart';
/// is false) — the `video_player` API does not expose track lists.
/// * No bitmap (PGS/VOBSUB) subtitle rendering, no ASS styling, no audio/
/// subtitle delay, and no external subtitle sideloading.
class TizenPlayerBackend implements PlayerBackend {
class TizenPlayerBackend extends PlayerBackend {
TizenPlayerBackend(this._prefs);

final UserPreferences _prefs;
Expand Down Expand Up @@ -279,6 +278,7 @@ class TizenPlayerBackend implements PlayerBackend {
await _controller?.setPlaybackSpeed(speed);
}


// The standard video_player API exposes no runtime track selection; tracks are
// selected server-side via the device profile / transcoding instead.
@override
Expand Down
14 changes: 8 additions & 6 deletions lib/ui/screens/detail/item_detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5883,7 +5883,7 @@ class _ActionButtonsState extends State<_ActionButtons> {
);

case 'Season':
final episodes = viewModel.episodes;
final episodes = viewModel.episodes.where(isEligibleNextEpisodeCandidate).toList();
if (episodes.isEmpty) return;
final startIndex = resume
? episodes.indexWhere(
Expand Down Expand Up @@ -5939,11 +5939,12 @@ class _ActionButtonsState extends State<_ActionButtons> {
if (!context.mounted) return;

if (episodes.length > 1) {
final startIndex = episodes.indexWhere((e) => e.id == item.id);
final playableEpisodes = episodes.where((e) => e.id == item.id || isEligibleNextEpisodeCandidate(e)).toList();
final startIndex = playableEpisodes.indexWhere((e) => e.id == item.id);
final idx = startIndex >= 0 ? startIndex : 0;
final selectedEpisode = episodes[idx];
final selectedEpisode = playableEpisodes[idx];
final episodeQueue = _truncateQueueIfImmediateNextUnplayable(
episodes,
playableEpisodes,
startIndex: idx,
);

Expand Down Expand Up @@ -6102,10 +6103,11 @@ class _ActionButtonsState extends State<_ActionButtons> {
) async {
final manager = GetIt.instance<PlaybackManager>();
final queue = await _shuffleQueueForItem(item);
if (queue.length < 2) return;
final playableQueue = queue.where((e) => isEligibleNextEpisodeCandidate(e) || e.id == item.id).toList();
if (playableQueue.length < 2) return;
if (!context.mounted) return;

final shuffled = List<AggregatedItem>.from(queue)..shuffle();
final shuffled = List<AggregatedItem>.from(playableQueue)..shuffle();
final isAudio = shuffled.every((queuedItem) {
final mediaType = queuedItem.rawData['MediaType'] as String?;
return queuedItem.type == 'Audio' || mediaType == 'Audio';
Expand Down
158 changes: 80 additions & 78 deletions lib/ui/screens/playback/video_player_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5837,88 +5837,90 @@ class _VideoPlayerScreenState extends State<VideoPlayerScreen>
final canDownloadRemote =
!audio && item is AggregatedItem && _canDownloadRemoteSubtitles(item);

final int? currentStreamIndex;
if (audio) {
currentStreamIndex =
_manager.audioStreamIndex ??
streams.where((s) => s['IsDefault'] == true).firstOrNull?['Index']
as int?;
} else {
final subIdx = _manager.subtitleStreamIndex;
currentStreamIndex =
subIdx ??
streams.where((s) => s['IsDefault'] == true).firstOrNull?['Index']
as int?;
}
final isSubsOff = !audio && _manager.subtitleStreamIndex == -1;

final options = <TrackOption>[
if (!audio) TrackOption(label: l10n.off),
...optionStreams.asMap().entries.map((entry) {
final index = entry.key;
final trackNumber = index + 1;
final s = entry.value;
final displayTitle = s['DisplayTitle'] as String?;
final title = s['Title'] as String?;
final language = s['Language'] as String?;
final codec = s['Codec'] as String?;
final label =
displayTitle ??
title ??
language ??
l10n.streamTypeFallback(streamType, index + 1);
final subtitle = audio
? [
if (language != null && displayTitle != null) language,
if (codec != null) codec.toUpperCase(),
if (s['Channels'] != null) '${s['Channels']}ch',
].join(' · ')
: (() {
final subtitleType =
((codec == null || codec.isEmpty) ? 'Unknown' : codec)
.toUpperCase();
final deliveryMethod = (s['DeliveryMethod'] as String?)
?.trim()
.toLowerCase();
final location = s['IsExternal'] == true
? 'External'
: (deliveryMethod == 'embed' ? 'Embedded' : 'Internal');
return '$subtitleType · $location';
})();
return TrackOption(
label: '$trackNumber - $label',
subtitle: subtitle.isNotEmpty ? subtitle : null,
scrollLabel: true,
scrollSubtitle: true,
);
}),
if (canDownloadRemote)
TrackOption(
label: l10n.downloadSubtitlesLabel,
subtitle: l10n.searchOpenSubtitlesPlugin,
),
];
unawaited(() async {
final int? currentStreamIndex;
if (audio) {
currentStreamIndex =
_manager.audioStreamIndex ??
streams.where((s) => s['IsDefault'] == true).firstOrNull?['Index']
as int?;
} else {
final subIdx = await _manager.getSubtitleStreamIndexAsync();
currentStreamIndex =
subIdx ??
streams.where((s) => s['IsDefault'] == true).firstOrNull?['Index']
as int?;
}
final isSubsOff = !audio && currentStreamIndex == -1;

final options = <TrackOption>[
if (!audio) TrackOption(label: l10n.off),
...optionStreams.asMap().entries.map((entry) {
final index = entry.key;
final trackNumber = index + 1;
final s = entry.value;
final displayTitle = s['DisplayTitle'] as String?;
final title = s['Title'] as String?;
final language = s['Language'] as String?;
final codec = s['Codec'] as String?;
final label =
displayTitle ??
title ??
language ??
l10n.streamTypeFallback(streamType, index + 1);
final subtitle = audio
? [
if (language != null && displayTitle != null) language,
if (codec != null) codec.toUpperCase(),
if (s['Channels'] != null) '${s['Channels']}ch',
].join(' · ')
: (() {
final subtitleType =
((codec == null || codec.isEmpty) ? 'Unknown' : codec)
.toUpperCase();
final deliveryMethod = (s['DeliveryMethod'] as String?)
?.trim()
.toLowerCase();
final location = s['IsExternal'] == true
? 'External'
: (deliveryMethod == 'embed' ? 'Embedded' : 'Internal');
return '$subtitleType · $location';
})();
return TrackOption(
label: '$trackNumber - $label',
subtitle: subtitle.isNotEmpty ? subtitle : null,
scrollLabel: true,
scrollSubtitle: true,
);
}),
if (canDownloadRemote)
TrackOption(
label: l10n.downloadSubtitlesLabel,
subtitle: l10n.searchOpenSubtitlesPlugin,
),
];

final int? selectedIndex;
if (audio) {
final idx = currentStreamIndex != null
? streams.indexWhere((s) => s['Index'] == currentStreamIndex)
: -1;
selectedIndex = idx >= 0 ? idx : null;
} else {
if (isSubsOff || (currentStreamIndex == null && streams.isNotEmpty)) {
selectedIndex = 0;
} else if (currentStreamIndex != null) {
final idx = optionStreams.indexWhere(
(s) => s['Index'] == currentStreamIndex,
);
selectedIndex = idx >= 0 ? idx + 1 : null;
final int? selectedIndex;
if (audio) {
final idx = currentStreamIndex != null
? streams.indexWhere((s) => s['Index'] == currentStreamIndex)
: -1;
selectedIndex = idx >= 0 ? idx : null;
} else {
selectedIndex = null;
if (isSubsOff || (currentStreamIndex == null && streams.isNotEmpty)) {
selectedIndex = 0;
} else if (currentStreamIndex != null) {
final idx = optionStreams.indexWhere(
(s) => s['Index'] == currentStreamIndex,
);
selectedIndex = idx >= 0 ? idx + 1 : null;
} else {
selectedIndex = null;
}
}
}

unawaited(() async {
if (!mounted) return;

final result = await TrackSelectorDialog.show(
context,
title: audio ? l10n.audioTrack : l10n.subtitleTrack,
Expand Down
Loading
Loading