feat: Videoplayer remember subtitle and audio selection(#339)

This commit is contained in:
Julien9969 2025-05-22 13:24:42 -04:00 committed by GitHub
parent 93a38a0b6b
commit b1491b0ada
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 247 additions and 39 deletions

View file

@ -55,6 +55,10 @@ flutter pub run build_runner build
```bash ```bash
flutter pub run build_runner watch flutter pub run build_runner watch
``` ```
Update localization definitions:
```bash
flutter gen-l10n
```
## 🌐 Using a demo Server ## 🌐 Using a demo Server
You can use a fake server from Jellyfin. You can use a fake server from Jellyfin.

View file

@ -1201,5 +1201,15 @@
} }
}, },
"maxConcurrentDownloadsTitle": "Max concurrent downloads", "maxConcurrentDownloadsTitle": "Max concurrent downloads",
"maxConcurrentDownloadsDesc": "Sets the maximum number of downloads that can run at the same time. Set to 0 to disable the limit." "maxConcurrentDownloadsDesc": "Sets the maximum number of downloads that can run at the same time. Set to 0 to disable the limit.",
"playbackTrackSelection": "Playback track selection",
"@playbackTrackSelection": {},
"rememberSubtitleSelections": "Set subtitle track based on previous item",
"@rememberSubtitleSelections": {},
"rememberAudioSelections": "Set audio track based on previous item",
"@rememberAudioSelections": {},
"rememberSubtitleSelectionsDesc": "Try to set the subtitle track to the closest match to the last video.",
"@rememberSubtitleSelectionsDesc": {},
"rememberAudioSelectionsDesc": "Try to set the audio track to the closest match to the last video.",
"@rememberAudioSelectionsDesc": {}
} }

View file

@ -34,6 +34,7 @@ class AccountModel with _$AccountModel {
@Default([]) List<LibraryFiltersModel> savedFilters, @Default([]) List<LibraryFiltersModel> savedFilters,
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy, @JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
@JsonKey(includeFromJson: false, includeToJson: false) ServerConfiguration? serverConfiguration, @JsonKey(includeFromJson: false, includeToJson: false) ServerConfiguration? serverConfiguration,
@JsonKey(includeFromJson: false, includeToJson: false) UserConfiguration? userConfiguration,
}) = _AccountModel; }) = _AccountModel;
factory AccountModel.fromJson(Map<String, dynamic> json) => _$AccountModelFromJson(json); factory AccountModel.fromJson(Map<String, dynamic> json) => _$AccountModelFromJson(json);

View file

@ -37,6 +37,9 @@ mixin _$AccountModel {
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
ServerConfiguration? get serverConfiguration => ServerConfiguration? get serverConfiguration =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@JsonKey(includeFromJson: false, includeToJson: false)
UserConfiguration? get userConfiguration =>
throw _privateConstructorUsedError;
/// Serializes this AccountModel to a JSON map. /// Serializes this AccountModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -68,7 +71,9 @@ abstract class $AccountModelCopyWith<$Res> {
List<LibraryFiltersModel> savedFilters, List<LibraryFiltersModel> savedFilters,
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy, @JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
ServerConfiguration? serverConfiguration}); ServerConfiguration? serverConfiguration,
@JsonKey(includeFromJson: false, includeToJson: false)
UserConfiguration? userConfiguration});
} }
/// @nodoc /// @nodoc
@ -99,6 +104,7 @@ class _$AccountModelCopyWithImpl<$Res, $Val extends AccountModel>
Object? savedFilters = null, Object? savedFilters = null,
Object? policy = freezed, Object? policy = freezed,
Object? serverConfiguration = freezed, Object? serverConfiguration = freezed,
Object? userConfiguration = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
name: null == name name: null == name
@ -153,6 +159,10 @@ class _$AccountModelCopyWithImpl<$Res, $Val extends AccountModel>
? _value.serverConfiguration ? _value.serverConfiguration
: serverConfiguration // ignore: cast_nullable_to_non_nullable : serverConfiguration // ignore: cast_nullable_to_non_nullable
as ServerConfiguration?, as ServerConfiguration?,
userConfiguration: freezed == userConfiguration
? _value.userConfiguration
: userConfiguration // ignore: cast_nullable_to_non_nullable
as UserConfiguration?,
) as $Val); ) as $Val);
} }
} }
@ -179,7 +189,9 @@ abstract class _$$AccountModelImplCopyWith<$Res>
List<LibraryFiltersModel> savedFilters, List<LibraryFiltersModel> savedFilters,
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy, @JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
ServerConfiguration? serverConfiguration}); ServerConfiguration? serverConfiguration,
@JsonKey(includeFromJson: false, includeToJson: false)
UserConfiguration? userConfiguration});
} }
/// @nodoc /// @nodoc
@ -208,6 +220,7 @@ class __$$AccountModelImplCopyWithImpl<$Res>
Object? savedFilters = null, Object? savedFilters = null,
Object? policy = freezed, Object? policy = freezed,
Object? serverConfiguration = freezed, Object? serverConfiguration = freezed,
Object? userConfiguration = freezed,
}) { }) {
return _then(_$AccountModelImpl( return _then(_$AccountModelImpl(
name: null == name name: null == name
@ -262,6 +275,10 @@ class __$$AccountModelImplCopyWithImpl<$Res>
? _value.serverConfiguration ? _value.serverConfiguration
: serverConfiguration // ignore: cast_nullable_to_non_nullable : serverConfiguration // ignore: cast_nullable_to_non_nullable
as ServerConfiguration?, as ServerConfiguration?,
userConfiguration: freezed == userConfiguration
? _value.userConfiguration
: userConfiguration // ignore: cast_nullable_to_non_nullable
as UserConfiguration?,
)); ));
} }
} }
@ -283,7 +300,9 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
final List<LibraryFiltersModel> savedFilters = const [], final List<LibraryFiltersModel> savedFilters = const [],
@JsonKey(includeFromJson: false, includeToJson: false) this.policy, @JsonKey(includeFromJson: false, includeToJson: false) this.policy,
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
this.serverConfiguration}) this.serverConfiguration,
@JsonKey(includeFromJson: false, includeToJson: false)
this.userConfiguration})
: _latestItemsExcludes = latestItemsExcludes, : _latestItemsExcludes = latestItemsExcludes,
_searchQueryHistory = searchQueryHistory, _searchQueryHistory = searchQueryHistory,
_savedFilters = savedFilters, _savedFilters = savedFilters,
@ -346,10 +365,13 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
@override @override
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
final ServerConfiguration? serverConfiguration; final ServerConfiguration? serverConfiguration;
@override
@JsonKey(includeFromJson: false, includeToJson: false)
final UserConfiguration? userConfiguration;
@override @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'AccountModel(name: $name, id: $id, avatar: $avatar, lastUsed: $lastUsed, authMethod: $authMethod, localPin: $localPin, credentials: $credentials, latestItemsExcludes: $latestItemsExcludes, searchQueryHistory: $searchQueryHistory, quickConnectState: $quickConnectState, savedFilters: $savedFilters, policy: $policy, serverConfiguration: $serverConfiguration)'; return 'AccountModel(name: $name, id: $id, avatar: $avatar, lastUsed: $lastUsed, authMethod: $authMethod, localPin: $localPin, credentials: $credentials, latestItemsExcludes: $latestItemsExcludes, searchQueryHistory: $searchQueryHistory, quickConnectState: $quickConnectState, savedFilters: $savedFilters, policy: $policy, serverConfiguration: $serverConfiguration, userConfiguration: $userConfiguration)';
} }
@override @override
@ -369,7 +391,8 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
..add(DiagnosticsProperty('quickConnectState', quickConnectState)) ..add(DiagnosticsProperty('quickConnectState', quickConnectState))
..add(DiagnosticsProperty('savedFilters', savedFilters)) ..add(DiagnosticsProperty('savedFilters', savedFilters))
..add(DiagnosticsProperty('policy', policy)) ..add(DiagnosticsProperty('policy', policy))
..add(DiagnosticsProperty('serverConfiguration', serverConfiguration)); ..add(DiagnosticsProperty('serverConfiguration', serverConfiguration))
..add(DiagnosticsProperty('userConfiguration', userConfiguration));
} }
@override @override
@ -398,7 +421,9 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
.equals(other._savedFilters, _savedFilters) && .equals(other._savedFilters, _savedFilters) &&
(identical(other.policy, policy) || other.policy == policy) && (identical(other.policy, policy) || other.policy == policy) &&
(identical(other.serverConfiguration, serverConfiguration) || (identical(other.serverConfiguration, serverConfiguration) ||
other.serverConfiguration == serverConfiguration)); other.serverConfiguration == serverConfiguration) &&
(identical(other.userConfiguration, userConfiguration) ||
other.userConfiguration == userConfiguration));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -417,7 +442,8 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
quickConnectState, quickConnectState,
const DeepCollectionEquality().hash(_savedFilters), const DeepCollectionEquality().hash(_savedFilters),
policy, policy,
serverConfiguration); serverConfiguration,
userConfiguration);
/// Create a copy of AccountModel /// Create a copy of AccountModel
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -451,7 +477,9 @@ abstract class _AccountModel extends AccountModel {
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
final UserPolicy? policy, final UserPolicy? policy,
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
final ServerConfiguration? serverConfiguration}) = _$AccountModelImpl; final ServerConfiguration? serverConfiguration,
@JsonKey(includeFromJson: false, includeToJson: false)
final UserConfiguration? userConfiguration}) = _$AccountModelImpl;
const _AccountModel._() : super._(); const _AccountModel._() : super._();
factory _AccountModel.fromJson(Map<String, dynamic> json) = factory _AccountModel.fromJson(Map<String, dynamic> json) =
@ -485,6 +513,9 @@ abstract class _AccountModel extends AccountModel {
@override @override
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
ServerConfiguration? get serverConfiguration; ServerConfiguration? get serverConfiguration;
@override
@JsonKey(includeFromJson: false, includeToJson: false)
UserConfiguration? get userConfiguration;
/// Create a copy of AccountModel /// Create a copy of AccountModel
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View file

@ -175,6 +175,20 @@ class StreamModel {
}); });
} }
class AudioAndSubStreamModel extends StreamModel {
final String language;
final String displayTitle;
AudioAndSubStreamModel({
required this.displayTitle,
required super.name,
required super.codec,
required super.isDefault,
required super.isExternal,
required super.index,
required this.language,
});
}
class VersionStreamModel { class VersionStreamModel {
final String name; final String name;
final int index; final int index;
@ -250,19 +264,17 @@ extension SortByExternalExtension<T extends StreamModel> on Iterable<T> {
} }
} }
class AudioStreamModel extends StreamModel { class AudioStreamModel extends AudioAndSubStreamModel {
final String displayTitle;
final String language;
final String channelLayout; final String channelLayout;
AudioStreamModel({ AudioStreamModel({
required this.displayTitle, required super.displayTitle,
required super.name, required super.name,
required super.codec, required super.codec,
required super.isDefault, required super.isDefault,
required super.isExternal, required super.isExternal,
required super.index, required super.index,
required this.language, required super.language,
required this.channelLayout, required this.channelLayout,
}); });
@ -292,8 +304,8 @@ class AudioStreamModel extends StreamModel {
AudioStreamModel.no({ AudioStreamModel.no({
super.name = 'Off', super.name = 'Off',
this.displayTitle = 'Off', super.displayTitle = 'Off',
this.language = '', super.language = '',
super.codec = '', super.codec = '',
this.channelLayout = '', this.channelLayout = '',
super.isDefault = false, super.isDefault = false,
@ -302,19 +314,17 @@ class AudioStreamModel extends StreamModel {
}); });
} }
class SubStreamModel extends StreamModel { class SubStreamModel extends AudioAndSubStreamModel {
String id; String id;
String title; String title;
String displayTitle;
String language;
String? url; String? url;
bool supportsExternalStream; bool supportsExternalStream;
SubStreamModel({ SubStreamModel({
required super.name, required super.name,
required this.id, required this.id,
required this.title, required this.title,
required this.displayTitle, required super.displayTitle,
required this.language, required super.language,
this.url, this.url,
required super.codec, required super.codec,
required super.isDefault, required super.isDefault,
@ -327,8 +337,8 @@ class SubStreamModel extends StreamModel {
super.name = 'Off', super.name = 'Off',
this.id = 'Off', this.id = 'Off',
this.title = 'Off', this.title = 'Off',
this.displayTitle = 'Off', super.displayTitle = 'Off',
this.language = '', super.language = '',
this.url = '', this.url = '',
super.codec = '', super.codec = '',
super.isDefault = false, super.isDefault = false,

View file

@ -31,6 +31,7 @@ import 'package:fladder/util/bitrate_helper.dart';
import 'package:fladder/util/duration_extensions.dart'; import 'package:fladder/util/duration_extensions.dart';
import 'package:fladder/util/list_extensions.dart'; import 'package:fladder/util/list_extensions.dart';
import 'package:fladder/util/map_bool_helper.dart'; import 'package:fladder/util/map_bool_helper.dart';
import 'package:fladder/util/streams_selection.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart'; import 'package:fladder/wrappers/media_control_wrapper.dart';
class Media { class Media {
@ -196,13 +197,25 @@ class PlaybackModelHelper {
); );
final streamModel = firstItemToPlay.streamModel; final streamModel = firstItemToPlay.streamModel;
final audioStreamIndex = selectAudioStream(
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
oldModel?.mediaStreams?.currentAudioStream,
streamModel?.audioStreams,
streamModel?.defaultAudioStreamIndex
);
final subStreamIndex = selectSubStream(
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)),
oldModel?.mediaStreams?.currentSubStream,
streamModel?.subStreams,
streamModel?.defaultSubStreamIndex
);
final Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost( final Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
itemId: firstItemToPlay.id, itemId: firstItemToPlay.id,
body: PlaybackInfoDto( body: PlaybackInfoDto(
startTimeTicks: startPosition?.toRuntimeTicks, startTimeTicks: startPosition?.toRuntimeTicks,
audioStreamIndex: streamModel?.defaultAudioStreamIndex, audioStreamIndex: audioStreamIndex,
subtitleStreamIndex: streamModel?.defaultSubStreamIndex, subtitleStreamIndex: subStreamIndex,
enableTranscoding: true, enableTranscoding: true,
autoOpenLiveStream: true, autoOpenLiveStream: true,
deviceProfile: ref.read(videoProfileProvider), deviceProfile: ref.read(videoProfileProvider),
@ -223,8 +236,8 @@ class PlaybackModelHelper {
if (mediaSource == null) return null; if (mediaSource == null) return null;
final mediaStreamsWithUrls = MediaStreamsModel.fromMediaStreamsList(playbackInfo.mediaSources, ref).copyWith( final mediaStreamsWithUrls = MediaStreamsModel.fromMediaStreamsList(playbackInfo.mediaSources, ref).copyWith(
defaultAudioStreamIndex: streamModel?.defaultAudioStreamIndex, defaultAudioStreamIndex: audioStreamIndex,
defaultSubStreamIndex: streamModel?.defaultSubStreamIndex, defaultSubStreamIndex: subStreamIndex,
); );
final mediaSegments = await api.mediaSegmentsGet(id: item.id); final mediaSegments = await api.mediaSegmentsGet(id: item.id);
@ -328,8 +341,18 @@ class PlaybackModelHelper {
final currentPosition = ref.read(mediaPlaybackProvider.select((value) => value.position)); final currentPosition = ref.read(mediaPlaybackProvider.select((value) => value.position));
final audioIndex = playbackModel.mediaStreams?.defaultAudioStreamIndex; final audioIndex = selectAudioStream(
final subIndex = playbackModel.mediaStreams?.defaultSubStreamIndex; ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
playbackModel.mediaStreams?.currentAudioStream,
playbackModel.audioStreams,
playbackModel.mediaStreams?.defaultAudioStreamIndex
);
final subIndex = selectSubStream(
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)),
playbackModel.mediaStreams?.currentSubStream,
playbackModel.subStreams,
playbackModel.mediaStreams?.defaultSubStreamIndex
);
Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost( Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
itemId: item.id, itemId: item.id,

View file

@ -902,4 +902,38 @@ class JellyService {
Future<Response<bool>> quickConnectEnabled() async => api.quickConnectEnabledGet(); Future<Response<bool>> quickConnectEnabled() async => api.quickConnectEnabledGet();
Future<Response<dynamic>> deleteItem(String itemId) => api.itemsItemIdDelete(itemId: itemId); Future<Response<dynamic>> deleteItem(String itemId) => api.itemsItemIdDelete(itemId: itemId);
Future<UserConfiguration?> _updateUserConfiguration(UserConfiguration newUserConfiguration) async {
if (account?.id == null) return null;
final response = await api.usersConfigurationPost(
userId: account!.id,
body: newUserConfiguration,
);
if (response.isSuccessful) {
return newUserConfiguration;
}
return null;
}
Future<UserConfiguration?> updateRememberAudioSelections() {
final currentUserConfiguration = account?.userConfiguration;
if (currentUserConfiguration == null) return Future.value(null);
final updated = currentUserConfiguration.copyWith(
rememberAudioSelections: !(currentUserConfiguration.rememberAudioSelections ?? false),
);
return _updateUserConfiguration(updated);
}
Future<UserConfiguration?> updateRememberSubtitleSelections() {
final current = account?.userConfiguration;
if (current == null) return Future.value(null);
final updated = current.copyWith(
rememberSubtitleSelections: !(current.rememberSubtitleSelections ?? false),
);
return _updateUserConfiguration(updated);
}
} }

View file

@ -45,6 +45,7 @@ class User extends _$User {
name: response.body?.name ?? state?.name ?? "", name: response.body?.name ?? state?.name ?? "",
policy: response.body?.policy, policy: response.body?.policy,
serverConfiguration: systemConfiguration.body, serverConfiguration: systemConfiguration.body,
userConfiguration: response.body?.configuration,
quickConnectState: quickConnectStatus.body ?? false, quickConnectState: quickConnectStatus.body ?? false,
latestItemsExcludes: response.body?.configuration?.latestItemsExcludes ?? [], latestItemsExcludes: response.body?.configuration?.latestItemsExcludes ?? [],
); );
@ -53,6 +54,20 @@ class User extends _$User {
return null; return null;
} }
void setRememberAudioSelections() async {
final newUserConfiguration = await api.updateRememberAudioSelections();
if (newUserConfiguration != null) {
userState = state?.copyWith(userConfiguration: newUserConfiguration);
}
}
void setRememberSubtitleSelections() async {
final newUserConfiguration = await api.updateRememberSubtitleSelections();
if (newUserConfiguration != null) {
userState = state?.copyWith(userConfiguration: newUserConfiguration);
}
}
Future<Response> refreshMetaData( Future<Response> refreshMetaData(
String itemId, { String itemId, {
MetadataRefresh? metadataRefreshMode, MetadataRefresh? metadataRefreshMode,

View file

@ -10,6 +10,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart'; import 'package:fladder/models/settings/video_player_settings.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/connectivity_provider.dart'; import 'package:fladder/providers/connectivity_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart';
@ -166,6 +167,29 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
), ),
), ),
), ),
SettingsLabelDivider(label: context.localized.playbackTrackSelection),
SettingsListTile(
label: Text(context.localized.rememberAudioSelections),
subLabel: Text(context.localized.rememberAudioSelectionsDesc),
onTap: () => ref.read(userProvider.notifier).setRememberAudioSelections(),
trailing: Switch(
value: ref.watch(userProvider.select(
(value) => value?.userConfiguration?.rememberAudioSelections ?? true,
)),
onChanged: (_) => ref.read(userProvider.notifier).setRememberAudioSelections(),
),
),
SettingsListTile(
label: Text(context.localized.rememberSubtitleSelections),
subLabel: Text(context.localized.rememberSubtitleSelectionsDesc),
onTap: () => ref.read(userProvider.notifier).setRememberSubtitleSelections(),
trailing: Switch(
value: ref.watch(userProvider.select(
(value) => value?.userConfiguration?.rememberSubtitleSelections ?? true,
)),
onChanged: (_) => ref.read(userProvider.notifier).setRememberSubtitleSelections(),
),
),
const Divider(), const Divider(),
SettingsLabelDivider(label: context.localized.advanced), SettingsLabelDivider(label: context.localized.advanced),
if (PlayerOptions.available.length != 1) if (PlayerOptions.available.length != 1)
@ -235,16 +259,16 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
label: Text(context.localized.settingsPlayerBufferSizeTitle), label: Text(context.localized.settingsPlayerBufferSizeTitle),
subLabel: Text(context.localized.settingsPlayerBufferSizeDesc), subLabel: Text(context.localized.settingsPlayerBufferSizeDesc),
trailing: SizedBox( trailing: SizedBox(
width: 70, width: 70,
child: IntInputField( child: IntInputField(
suffix: 'MB', suffix: 'MB',
controller: TextEditingController(text: videoSettings.bufferSize.toString()), controller: TextEditingController(text: videoSettings.bufferSize.toString()),
onSubmitted: (value) { onSubmitted: (value) {
if (value != null) { if (value != null) {
provider.setBufferSize(value); provider.setBufferSize(value);
} }
}, },
)), )),
), ),
SettingsListTile( SettingsListTile(
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle), label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),

View file

@ -0,0 +1,56 @@
import 'package:fladder/models/items/media_streams_model.dart';
int? selectAudioStream(bool rememberAudioSelection, AudioAndSubStreamModel? previousStream, List<AudioAndSubStreamModel>? currentStream, int? defaultStream) {
if (!rememberAudioSelection){
return defaultStream;
}
return _selectStream(previousStream, currentStream, defaultStream);
}
int? selectSubStream(bool rememberSubSelection, AudioAndSubStreamModel? previousStream, List<AudioAndSubStreamModel>? currentStream, int? defaultStream) {
if (!rememberSubSelection){
return defaultStream;
}
return _selectStream(previousStream, currentStream, defaultStream);
}
int? _selectStream(AudioAndSubStreamModel? previousStream, List<AudioAndSubStreamModel>? currentStream, int? defaultStream) {
if (currentStream == null || previousStream == null){
return defaultStream;
}
int? bestStreamIndex;
int bestStreamScore = 0;
// Find the relative index of the previous stream
int prevRelIndex = 0;
for (var stream in currentStream) {
if (stream.index == previousStream.index) break;
prevRelIndex += 1;
}
int newRelIndex = 0;
for (var stream in currentStream) {
int score = 0;
if (previousStream.codec == stream.codec) score += 1;
if (prevRelIndex == newRelIndex) score += 1;
if (previousStream.displayTitle == stream.displayTitle) {
score += 2;
}
if (previousStream.language != 'und' &&
previousStream.language == stream.language) {
score += 2;
}
if (score > bestStreamScore && score >= 3) {
bestStreamScore = score;
bestStreamIndex = stream.index;
}
newRelIndex += 1;
}
return bestStreamIndex ?? defaultStream;
}