mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 13:38:13 -08:00
feat: Videoplayer remember subtitle and audio selection(#339)
This commit is contained in:
parent
93a38a0b6b
commit
b1491b0ada
10 changed files with 247 additions and 39 deletions
|
|
@ -55,6 +55,10 @@ flutter pub run build_runner build
|
|||
```bash
|
||||
flutter pub run build_runner watch
|
||||
```
|
||||
Update localization definitions:
|
||||
```bash
|
||||
flutter gen-l10n
|
||||
```
|
||||
|
||||
## 🌐 Using a demo Server
|
||||
You can use a fake server from Jellyfin.
|
||||
|
|
|
|||
|
|
@ -1201,5 +1201,15 @@
|
|||
}
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ class AccountModel with _$AccountModel {
|
|||
@Default([]) List<LibraryFiltersModel> savedFilters,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false) ServerConfiguration? serverConfiguration,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false) UserConfiguration? userConfiguration,
|
||||
}) = _AccountModel;
|
||||
|
||||
factory AccountModel.fromJson(Map<String, dynamic> json) => _$AccountModelFromJson(json);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ mixin _$AccountModel {
|
|||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
ServerConfiguration? get serverConfiguration =>
|
||||
throw _privateConstructorUsedError;
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
UserConfiguration? get userConfiguration =>
|
||||
throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this AccountModel to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
|
@ -68,7 +71,9 @@ abstract class $AccountModelCopyWith<$Res> {
|
|||
List<LibraryFiltersModel> savedFilters,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
ServerConfiguration? serverConfiguration});
|
||||
ServerConfiguration? serverConfiguration,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
UserConfiguration? userConfiguration});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
|
@ -99,6 +104,7 @@ class _$AccountModelCopyWithImpl<$Res, $Val extends AccountModel>
|
|||
Object? savedFilters = null,
|
||||
Object? policy = freezed,
|
||||
Object? serverConfiguration = freezed,
|
||||
Object? userConfiguration = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
name: null == name
|
||||
|
|
@ -153,6 +159,10 @@ class _$AccountModelCopyWithImpl<$Res, $Val extends AccountModel>
|
|||
? _value.serverConfiguration
|
||||
: serverConfiguration // ignore: cast_nullable_to_non_nullable
|
||||
as ServerConfiguration?,
|
||||
userConfiguration: freezed == userConfiguration
|
||||
? _value.userConfiguration
|
||||
: userConfiguration // ignore: cast_nullable_to_non_nullable
|
||||
as UserConfiguration?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
|
@ -179,7 +189,9 @@ abstract class _$$AccountModelImplCopyWith<$Res>
|
|||
List<LibraryFiltersModel> savedFilters,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
ServerConfiguration? serverConfiguration});
|
||||
ServerConfiguration? serverConfiguration,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
UserConfiguration? userConfiguration});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
|
@ -208,6 +220,7 @@ class __$$AccountModelImplCopyWithImpl<$Res>
|
|||
Object? savedFilters = null,
|
||||
Object? policy = freezed,
|
||||
Object? serverConfiguration = freezed,
|
||||
Object? userConfiguration = freezed,
|
||||
}) {
|
||||
return _then(_$AccountModelImpl(
|
||||
name: null == name
|
||||
|
|
@ -262,6 +275,10 @@ class __$$AccountModelImplCopyWithImpl<$Res>
|
|||
? _value.serverConfiguration
|
||||
: serverConfiguration // ignore: cast_nullable_to_non_nullable
|
||||
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 [],
|
||||
@JsonKey(includeFromJson: false, includeToJson: false) this.policy,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
this.serverConfiguration})
|
||||
this.serverConfiguration,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
this.userConfiguration})
|
||||
: _latestItemsExcludes = latestItemsExcludes,
|
||||
_searchQueryHistory = searchQueryHistory,
|
||||
_savedFilters = savedFilters,
|
||||
|
|
@ -346,10 +365,13 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
|
|||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final ServerConfiguration? serverConfiguration;
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final UserConfiguration? userConfiguration;
|
||||
|
||||
@override
|
||||
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
|
||||
|
|
@ -369,7 +391,8 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
|
|||
..add(DiagnosticsProperty('quickConnectState', quickConnectState))
|
||||
..add(DiagnosticsProperty('savedFilters', savedFilters))
|
||||
..add(DiagnosticsProperty('policy', policy))
|
||||
..add(DiagnosticsProperty('serverConfiguration', serverConfiguration));
|
||||
..add(DiagnosticsProperty('serverConfiguration', serverConfiguration))
|
||||
..add(DiagnosticsProperty('userConfiguration', userConfiguration));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -398,7 +421,9 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
|
|||
.equals(other._savedFilters, _savedFilters) &&
|
||||
(identical(other.policy, policy) || other.policy == policy) &&
|
||||
(identical(other.serverConfiguration, serverConfiguration) ||
|
||||
other.serverConfiguration == serverConfiguration));
|
||||
other.serverConfiguration == serverConfiguration) &&
|
||||
(identical(other.userConfiguration, userConfiguration) ||
|
||||
other.userConfiguration == userConfiguration));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
|
|
@ -417,7 +442,8 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
|
|||
quickConnectState,
|
||||
const DeepCollectionEquality().hash(_savedFilters),
|
||||
policy,
|
||||
serverConfiguration);
|
||||
serverConfiguration,
|
||||
userConfiguration);
|
||||
|
||||
/// Create a copy of AccountModel
|
||||
/// 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)
|
||||
final UserPolicy? policy,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final ServerConfiguration? serverConfiguration}) = _$AccountModelImpl;
|
||||
final ServerConfiguration? serverConfiguration,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final UserConfiguration? userConfiguration}) = _$AccountModelImpl;
|
||||
const _AccountModel._() : super._();
|
||||
|
||||
factory _AccountModel.fromJson(Map<String, dynamic> json) =
|
||||
|
|
@ -485,6 +513,9 @@ abstract class _AccountModel extends AccountModel {
|
|||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
ServerConfiguration? get serverConfiguration;
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
UserConfiguration? get userConfiguration;
|
||||
|
||||
/// Create a copy of AccountModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
final String name;
|
||||
final int index;
|
||||
|
|
@ -250,19 +264,17 @@ extension SortByExternalExtension<T extends StreamModel> on Iterable<T> {
|
|||
}
|
||||
}
|
||||
|
||||
class AudioStreamModel extends StreamModel {
|
||||
final String displayTitle;
|
||||
final String language;
|
||||
class AudioStreamModel extends AudioAndSubStreamModel {
|
||||
final String channelLayout;
|
||||
|
||||
AudioStreamModel({
|
||||
required this.displayTitle,
|
||||
required super.displayTitle,
|
||||
required super.name,
|
||||
required super.codec,
|
||||
required super.isDefault,
|
||||
required super.isExternal,
|
||||
required super.index,
|
||||
required this.language,
|
||||
required super.language,
|
||||
required this.channelLayout,
|
||||
});
|
||||
|
||||
|
|
@ -292,8 +304,8 @@ class AudioStreamModel extends StreamModel {
|
|||
|
||||
AudioStreamModel.no({
|
||||
super.name = 'Off',
|
||||
this.displayTitle = 'Off',
|
||||
this.language = '',
|
||||
super.displayTitle = 'Off',
|
||||
super.language = '',
|
||||
super.codec = '',
|
||||
this.channelLayout = '',
|
||||
super.isDefault = false,
|
||||
|
|
@ -302,19 +314,17 @@ class AudioStreamModel extends StreamModel {
|
|||
});
|
||||
}
|
||||
|
||||
class SubStreamModel extends StreamModel {
|
||||
class SubStreamModel extends AudioAndSubStreamModel {
|
||||
String id;
|
||||
String title;
|
||||
String displayTitle;
|
||||
String language;
|
||||
String? url;
|
||||
bool supportsExternalStream;
|
||||
SubStreamModel({
|
||||
required super.name,
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.displayTitle,
|
||||
required this.language,
|
||||
required super.displayTitle,
|
||||
required super.language,
|
||||
this.url,
|
||||
required super.codec,
|
||||
required super.isDefault,
|
||||
|
|
@ -327,8 +337,8 @@ class SubStreamModel extends StreamModel {
|
|||
super.name = 'Off',
|
||||
this.id = 'Off',
|
||||
this.title = 'Off',
|
||||
this.displayTitle = 'Off',
|
||||
this.language = '',
|
||||
super.displayTitle = 'Off',
|
||||
super.language = '',
|
||||
this.url = '',
|
||||
super.codec = '',
|
||||
super.isDefault = false,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import 'package:fladder/util/bitrate_helper.dart';
|
|||
import 'package:fladder/util/duration_extensions.dart';
|
||||
import 'package:fladder/util/list_extensions.dart';
|
||||
import 'package:fladder/util/map_bool_helper.dart';
|
||||
import 'package:fladder/util/streams_selection.dart';
|
||||
import 'package:fladder/wrappers/media_control_wrapper.dart';
|
||||
|
||||
class Media {
|
||||
|
|
@ -196,13 +197,25 @@ class PlaybackModelHelper {
|
|||
);
|
||||
|
||||
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(
|
||||
itemId: firstItemToPlay.id,
|
||||
body: PlaybackInfoDto(
|
||||
startTimeTicks: startPosition?.toRuntimeTicks,
|
||||
audioStreamIndex: streamModel?.defaultAudioStreamIndex,
|
||||
subtitleStreamIndex: streamModel?.defaultSubStreamIndex,
|
||||
audioStreamIndex: audioStreamIndex,
|
||||
subtitleStreamIndex: subStreamIndex,
|
||||
enableTranscoding: true,
|
||||
autoOpenLiveStream: true,
|
||||
deviceProfile: ref.read(videoProfileProvider),
|
||||
|
|
@ -223,8 +236,8 @@ class PlaybackModelHelper {
|
|||
if (mediaSource == null) return null;
|
||||
|
||||
final mediaStreamsWithUrls = MediaStreamsModel.fromMediaStreamsList(playbackInfo.mediaSources, ref).copyWith(
|
||||
defaultAudioStreamIndex: streamModel?.defaultAudioStreamIndex,
|
||||
defaultSubStreamIndex: streamModel?.defaultSubStreamIndex,
|
||||
defaultAudioStreamIndex: audioStreamIndex,
|
||||
defaultSubStreamIndex: subStreamIndex,
|
||||
);
|
||||
|
||||
final mediaSegments = await api.mediaSegmentsGet(id: item.id);
|
||||
|
|
@ -328,8 +341,18 @@ class PlaybackModelHelper {
|
|||
|
||||
final currentPosition = ref.read(mediaPlaybackProvider.select((value) => value.position));
|
||||
|
||||
final audioIndex = playbackModel.mediaStreams?.defaultAudioStreamIndex;
|
||||
final subIndex = playbackModel.mediaStreams?.defaultSubStreamIndex;
|
||||
final audioIndex = selectAudioStream(
|
||||
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(
|
||||
itemId: item.id,
|
||||
|
|
|
|||
|
|
@ -902,4 +902,38 @@ class JellyService {
|
|||
Future<Response<bool>> quickConnectEnabled() async => api.quickConnectEnabledGet();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class User extends _$User {
|
|||
name: response.body?.name ?? state?.name ?? "",
|
||||
policy: response.body?.policy,
|
||||
serverConfiguration: systemConfiguration.body,
|
||||
userConfiguration: response.body?.configuration,
|
||||
quickConnectState: quickConnectStatus.body ?? false,
|
||||
latestItemsExcludes: response.body?.configuration?.latestItemsExcludes ?? [],
|
||||
);
|
||||
|
|
@ -53,6 +54,20 @@ class User extends _$User {
|
|||
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(
|
||||
String itemId, {
|
||||
MetadataRefresh? metadataRefreshMode,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:fladder/models/items/media_segments_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.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/settings/video_player_settings_provider.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(),
|
||||
SettingsLabelDivider(label: context.localized.advanced),
|
||||
if (PlayerOptions.available.length != 1)
|
||||
|
|
@ -235,16 +259,16 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
label: Text(context.localized.settingsPlayerBufferSizeTitle),
|
||||
subLabel: Text(context.localized.settingsPlayerBufferSizeDesc),
|
||||
trailing: SizedBox(
|
||||
width: 70,
|
||||
child: IntInputField(
|
||||
suffix: 'MB',
|
||||
controller: TextEditingController(text: videoSettings.bufferSize.toString()),
|
||||
onSubmitted: (value) {
|
||||
if (value != null) {
|
||||
provider.setBufferSize(value);
|
||||
}
|
||||
},
|
||||
)),
|
||||
width: 70,
|
||||
child: IntInputField(
|
||||
suffix: 'MB',
|
||||
controller: TextEditingController(text: videoSettings.bufferSize.toString()),
|
||||
onSubmitted: (value) {
|
||||
if (value != null) {
|
||||
provider.setBufferSize(value);
|
||||
}
|
||||
},
|
||||
)),
|
||||
),
|
||||
SettingsListTile(
|
||||
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
|
||||
|
|
|
|||
56
lib/util/streams_selection.dart
Normal file
56
lib/util/streams_selection.dart
Normal 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;
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue