fix: Disable media tunneling by default with the option to enable it (#515)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-10-04 13:29:55 +02:00 committed by GitHub
parent 3ce0ed6dbc
commit 1942738fe4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 256 additions and 199 deletions

View file

@ -93,6 +93,7 @@ enum class SegmentSkip(val raw: Int) {
/** Generated class from Pigeon that represents data sent in messages. */ /** Generated class from Pigeon that represents data sent in messages. */
data class PlayerSettings ( data class PlayerSettings (
val enableTunneling: Boolean,
val skipTypes: Map<SegmentType, SegmentSkip>, val skipTypes: Map<SegmentType, SegmentSkip>,
val skipForward: Long, val skipForward: Long,
val skipBackward: Long val skipBackward: Long
@ -100,14 +101,16 @@ data class PlayerSettings (
{ {
companion object { companion object {
fun fromList(pigeonVar_list: List<Any?>): PlayerSettings { fun fromList(pigeonVar_list: List<Any?>): PlayerSettings {
val skipTypes = pigeonVar_list[0] as Map<SegmentType, SegmentSkip> val enableTunneling = pigeonVar_list[0] as Boolean
val skipForward = pigeonVar_list[1] as Long val skipTypes = pigeonVar_list[1] as Map<SegmentType, SegmentSkip>
val skipBackward = pigeonVar_list[2] as Long val skipForward = pigeonVar_list[2] as Long
return PlayerSettings(skipTypes, skipForward, skipBackward) val skipBackward = pigeonVar_list[3] as Long
return PlayerSettings(enableTunneling, skipTypes, skipForward, skipBackward)
} }
} }
fun toList(): List<Any?> { fun toList(): List<Any?> {
return listOf( return listOf(
enableTunneling,
skipTypes, skipTypes,
skipForward, skipForward,
skipBackward, skipBackward,

View file

@ -32,8 +32,8 @@ fun ItemHeader(state: PlayableData?) {
contentDescription = title ?: "logo", contentDescription = title ?: "logo",
alignment = Alignment.CenterStart, alignment = Alignment.CenterStart,
modifier = Modifier modifier = Modifier
.fillMaxHeight(0.25f) .fillMaxHeight(0.20f)
.fillMaxWidth(0.5f) .fillMaxWidth(0.45f)
) )
} else { } else {
title?.let { title?.let {

View file

@ -21,7 +21,6 @@ import androidx.core.content.getSystemService
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.Tracks import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSource
@ -41,6 +40,7 @@ import androidx.media3.ui.PlayerView
import io.github.peerless2012.ass.media.kt.buildWithAssSupport import io.github.peerless2012.ass.media.kt.buildWithAssSupport
import io.github.peerless2012.ass.media.type.AssRenderType import io.github.peerless2012.ass.media.type.AssRenderType
import nl.jknaapen.fladder.messengers.properlySetSubAndAudioTracks import nl.jknaapen.fladder.messengers.properlySetSubAndAudioTracks
import nl.jknaapen.fladder.objects.PlayerSettingsObject
import nl.jknaapen.fladder.objects.VideoPlayerObject import nl.jknaapen.fladder.objects.VideoPlayerObject
import nl.jknaapen.fladder.utility.getAudioTracks import nl.jknaapen.fladder.utility.getAudioTracks
import nl.jknaapen.fladder.utility.getSubtitleTracks import nl.jknaapen.fladder.utility.getSubtitleTracks
@ -76,41 +76,37 @@ internal fun ExoPlayer(
val audioAttributes = AudioAttributes.Builder() val audioAttributes = AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA) .setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.build() .build()
val renderersFactory = DefaultRenderersFactory(context) val renderersFactory = DefaultRenderersFactory(context)
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
.setEnableDecoderFallback(true) .setEnableDecoderFallback(true)
val trackSelector = DefaultTrackSelector(context).apply {
setParameters(buildUponParameters().apply {
setTunnelingEnabled(PlayerSettingsObject.settings.value?.enableTunneling ?: false)
setAllowInvalidateSelectionsOnRendererCapabilitiesChange(true)
})
}
val exoPlayer = remember { val exoPlayer = remember {
ExoPlayer.Builder(context, renderersFactory) ExoPlayer.Builder(context, renderersFactory)
.setTrackSelector(DefaultTrackSelector(context).apply { .setTrackSelector(trackSelector)
setParameters(buildUponParameters().apply { .setMediaSourceFactory(DefaultMediaSourceFactory(videoCache, extractorsFactory))
setAudioOffloadPreferences(
TrackSelectionParameters.AudioOffloadPreferences.DEFAULT.buildUpon().apply {
setAudioOffloadMode(TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED)
}.build()
)
setAllowInvalidateSelectionsOnRendererCapabilitiesChange(true)
setTunnelingEnabled(true)
})
})
.setMediaSourceFactory(
DefaultMediaSourceFactory(
videoCache,
extractorsFactory
),
)
.setAudioAttributes(audioAttributes, true) .setAudioAttributes(audioAttributes, true)
.setHandleAudioBecomingNoisy(true) .setHandleAudioBecomingNoisy(true)
.setPauseAtEndOfMediaItems(true) .setPauseAtEndOfMediaItems(true)
.setVideoScalingMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT) .setVideoScalingMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT)
.buildWithAssSupport(context, AssRenderType.LEGACY) .buildWithAssSupport(
context,
renderersFactory = renderersFactory,
renderType = AssRenderType.LEGACY
)
} }
DisposableEffect(exoPlayer) { DisposableEffect(exoPlayer) {
val listener = object : Player.Listener { val listener = object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) { override fun onPlaybackStateChanged(playbackState: Int) {
videoHost.setPlaybackState( videoHost.setPlaybackState(
PlaybackState( PlaybackState(

View file

@ -1344,5 +1344,7 @@
"quickConnectEnterCodeDescription": "Enter the code below to login", "quickConnectEnterCodeDescription": "Enter the code below to login",
"showMore": "Show more", "showMore": "Show more",
"itemColorsTitle": "Item colors", "itemColorsTitle": "Item colors",
"itemColorsDesc": "Use item's primary color to theme the details page" "itemColorsDesc": "Use item's primary color to theme the details page",
"mediaTunnelingTitle": "Media tunneling",
"mediaTunnelingDesc": "Enable media tunneling for native player"
} }

View file

@ -2,6 +2,9 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'arguments_model.freezed.dart'; part 'arguments_model.freezed.dart';
/// Prefer using the arguments provider over this boolean
bool leanBackMode = false;
@freezed @freezed
abstract class ArgumentsModel with _$ArgumentsModel { abstract class ArgumentsModel with _$ArgumentsModel {
const ArgumentsModel._(); const ArgumentsModel._();
@ -13,6 +16,7 @@ abstract class ArgumentsModel with _$ArgumentsModel {
factory ArgumentsModel.fromArguments(List<String> arguments, bool leanBackEnabled) { factory ArgumentsModel.fromArguments(List<String> arguments, bool leanBackEnabled) {
arguments = arguments.map((e) => e.trim()).toList(); arguments = arguments.map((e) => e.trim()).toList();
leanBackMode = leanBackEnabled;
return ArgumentsModel( return ArgumentsModel(
htpcMode: arguments.contains('--htpc') || leanBackEnabled, htpcMode: arguments.contains('--htpc') || leanBackEnabled,
leanBackMode: leanBackEnabled, leanBackMode: leanBackEnabled,

View file

@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/settings/arguments_model.dart';
import 'package:fladder/models/settings/key_combinations.dart'; import 'package:fladder/models/settings/key_combinations.dart';
import 'package:fladder/util/bitrate_helper.dart'; import 'package:fladder/util/bitrate_helper.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
@ -63,6 +64,7 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
@Default(false) bool fillScreen, @Default(false) bool fillScreen,
@Default(true) bool hardwareAccel, @Default(true) bool hardwareAccel,
@Default(false) bool useLibass, @Default(false) bool useLibass,
@Default(false) bool enableTunneling,
@Default(32) int bufferSize, @Default(32) int bufferSize,
PlayerOptions? playerOptions, PlayerOptions? playerOptions,
@Default(100) double internalVolume, @Default(100) double internalVolume,
@ -82,7 +84,8 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
factory VideoPlayerSettingsModel.fromJson(Map<String, dynamic> json) => _$VideoPlayerSettingsModelFromJson(json); factory VideoPlayerSettingsModel.fromJson(Map<String, dynamic> json) => _$VideoPlayerSettingsModelFromJson(json);
PlayerOptions get wantedPlayer => playerOptions ?? PlayerOptions.platformDefaults; PlayerOptions get wantedPlayer =>
leanBackMode ? PlayerOptions.nativePlayer : playerOptions ?? PlayerOptions.platformDefaults;
Map<VideoHotKeys, KeyCombination> get currentShortcuts => Map<VideoHotKeys, KeyCombination> get currentShortcuts =>
_defaultVideoHotKeys.map((key, value) => MapEntry(key, hotKeys[key] ?? value)); _defaultVideoHotKeys.map((key, value) => MapEntry(key, hotKeys[key] ?? value));
@ -91,6 +94,7 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
bool playerSame(VideoPlayerSettingsModel other) { bool playerSame(VideoPlayerSettingsModel other) {
return other.hardwareAccel == hardwareAccel && return other.hardwareAccel == hardwareAccel &&
other.enableTunneling == enableTunneling &&
other.useLibass == useLibass && other.useLibass == useLibass &&
other.bufferSize == bufferSize && other.bufferSize == bufferSize &&
other.wantedPlayer == wantedPlayer; other.wantedPlayer == wantedPlayer;
@ -106,6 +110,7 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
other.fillScreen == fillScreen && other.fillScreen == fillScreen &&
other.hardwareAccel == hardwareAccel && other.hardwareAccel == hardwareAccel &&
other.useLibass == useLibass && other.useLibass == useLibass &&
other.enableTunneling == enableTunneling &&
other.bufferSize == bufferSize && other.bufferSize == bufferSize &&
other.internalVolume == internalVolume && other.internalVolume == internalVolume &&
other.playerOptions == playerOptions && other.playerOptions == playerOptions &&
@ -119,6 +124,7 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
fillScreen.hashCode ^ fillScreen.hashCode ^
hardwareAccel.hashCode ^ hardwareAccel.hashCode ^
useLibass.hashCode ^ useLibass.hashCode ^
enableTunneling.hashCode ^
bufferSize.hashCode ^ bufferSize.hashCode ^
internalVolume.hashCode ^ internalVolume.hashCode ^
audioDevice.hashCode; audioDevice.hashCode;
@ -132,14 +138,17 @@ enum PlayerOptions {
const PlayerOptions(); const PlayerOptions();
static Iterable<PlayerOptions> get available => kIsWeb static Iterable<PlayerOptions> get available => leanBackMode
? {PlayerOptions.libMPV} ? {PlayerOptions.nativePlayer, PlayerOptions.libMPV}
: switch (defaultTargetPlatform) { : kIsWeb
TargetPlatform.android => PlayerOptions.values, ? {PlayerOptions.libMPV}
_ => {PlayerOptions.libMDK, PlayerOptions.libMPV}, : switch (defaultTargetPlatform) {
}; TargetPlatform.android => PlayerOptions.values,
_ => {PlayerOptions.libMDK, PlayerOptions.libMPV},
};
static PlayerOptions get platformDefaults { static PlayerOptions get platformDefaults {
if (leanBackMode) return PlayerOptions.nativePlayer;
if (kIsWeb) return PlayerOptions.libMPV; if (kIsWeb) return PlayerOptions.libMPV;
return switch (defaultTargetPlatform) { return switch (defaultTargetPlatform) {
_ => PlayerOptions.libMPV, _ => PlayerOptions.libMPV,
@ -191,8 +200,10 @@ Map<VideoHotKeys, KeyCombination> get _defaultVideoHotKeys => {
VideoHotKeys.mute => KeyCombination(key: LogicalKeyboardKey.keyM), VideoHotKeys.mute => KeyCombination(key: LogicalKeyboardKey.keyM),
VideoHotKeys.volumeUp => KeyCombination(key: LogicalKeyboardKey.arrowUp), VideoHotKeys.volumeUp => KeyCombination(key: LogicalKeyboardKey.arrowUp),
VideoHotKeys.volumeDown => KeyCombination(key: LogicalKeyboardKey.arrowDown), VideoHotKeys.volumeDown => KeyCombination(key: LogicalKeyboardKey.arrowDown),
VideoHotKeys.speedUp => KeyCombination(key: LogicalKeyboardKey.arrowUp, modifier: LogicalKeyboardKey.controlLeft), VideoHotKeys.speedUp =>
VideoHotKeys.speedDown => KeyCombination(key: LogicalKeyboardKey.arrowDown, modifier: LogicalKeyboardKey.controlLeft), KeyCombination(key: LogicalKeyboardKey.arrowUp, modifier: LogicalKeyboardKey.controlLeft),
VideoHotKeys.speedDown =>
KeyCombination(key: LogicalKeyboardKey.arrowDown, modifier: LogicalKeyboardKey.controlLeft),
VideoHotKeys.prevVideo => VideoHotKeys.prevVideo =>
KeyCombination(key: LogicalKeyboardKey.keyP, modifier: LogicalKeyboardKey.shiftLeft), KeyCombination(key: LogicalKeyboardKey.keyP, modifier: LogicalKeyboardKey.shiftLeft),
VideoHotKeys.nextVideo => VideoHotKeys.nextVideo =>

View file

@ -19,6 +19,7 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin {
bool get fillScreen; bool get fillScreen;
bool get hardwareAccel; bool get hardwareAccel;
bool get useLibass; bool get useLibass;
bool get enableTunneling;
int get bufferSize; int get bufferSize;
PlayerOptions? get playerOptions; PlayerOptions? get playerOptions;
double get internalVolume; double get internalVolume;
@ -50,6 +51,7 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin {
..add(DiagnosticsProperty('fillScreen', fillScreen)) ..add(DiagnosticsProperty('fillScreen', fillScreen))
..add(DiagnosticsProperty('hardwareAccel', hardwareAccel)) ..add(DiagnosticsProperty('hardwareAccel', hardwareAccel))
..add(DiagnosticsProperty('useLibass', useLibass)) ..add(DiagnosticsProperty('useLibass', useLibass))
..add(DiagnosticsProperty('enableTunneling', enableTunneling))
..add(DiagnosticsProperty('bufferSize', bufferSize)) ..add(DiagnosticsProperty('bufferSize', bufferSize))
..add(DiagnosticsProperty('playerOptions', playerOptions)) ..add(DiagnosticsProperty('playerOptions', playerOptions))
..add(DiagnosticsProperty('internalVolume', internalVolume)) ..add(DiagnosticsProperty('internalVolume', internalVolume))
@ -64,7 +66,7 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin {
@override @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys)'; return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys)';
} }
} }
@ -80,6 +82,7 @@ abstract mixin class $VideoPlayerSettingsModelCopyWith<$Res> {
bool fillScreen, bool fillScreen,
bool hardwareAccel, bool hardwareAccel,
bool useLibass, bool useLibass,
bool enableTunneling,
int bufferSize, int bufferSize,
PlayerOptions? playerOptions, PlayerOptions? playerOptions,
double internalVolume, double internalVolume,
@ -110,6 +113,7 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res>
Object? fillScreen = null, Object? fillScreen = null,
Object? hardwareAccel = null, Object? hardwareAccel = null,
Object? useLibass = null, Object? useLibass = null,
Object? enableTunneling = null,
Object? bufferSize = null, Object? bufferSize = null,
Object? playerOptions = freezed, Object? playerOptions = freezed,
Object? internalVolume = null, Object? internalVolume = null,
@ -142,6 +146,10 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res>
? _self.useLibass ? _self.useLibass
: useLibass // ignore: cast_nullable_to_non_nullable : useLibass // ignore: cast_nullable_to_non_nullable
as bool, as bool,
enableTunneling: null == enableTunneling
? _self.enableTunneling
: enableTunneling // ignore: cast_nullable_to_non_nullable
as bool,
bufferSize: null == bufferSize bufferSize: null == bufferSize
? _self.bufferSize ? _self.bufferSize
: bufferSize // ignore: cast_nullable_to_non_nullable : bufferSize // ignore: cast_nullable_to_non_nullable
@ -285,6 +293,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel {
bool fillScreen, bool fillScreen,
bool hardwareAccel, bool hardwareAccel,
bool useLibass, bool useLibass,
bool enableTunneling,
int bufferSize, int bufferSize,
PlayerOptions? playerOptions, PlayerOptions? playerOptions,
double internalVolume, double internalVolume,
@ -307,6 +316,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel {
_that.fillScreen, _that.fillScreen,
_that.hardwareAccel, _that.hardwareAccel,
_that.useLibass, _that.useLibass,
_that.enableTunneling,
_that.bufferSize, _that.bufferSize,
_that.playerOptions, _that.playerOptions,
_that.internalVolume, _that.internalVolume,
@ -343,6 +353,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel {
bool fillScreen, bool fillScreen,
bool hardwareAccel, bool hardwareAccel,
bool useLibass, bool useLibass,
bool enableTunneling,
int bufferSize, int bufferSize,
PlayerOptions? playerOptions, PlayerOptions? playerOptions,
double internalVolume, double internalVolume,
@ -364,6 +375,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel {
_that.fillScreen, _that.fillScreen,
_that.hardwareAccel, _that.hardwareAccel,
_that.useLibass, _that.useLibass,
_that.enableTunneling,
_that.bufferSize, _that.bufferSize,
_that.playerOptions, _that.playerOptions,
_that.internalVolume, _that.internalVolume,
@ -399,6 +411,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel {
bool fillScreen, bool fillScreen,
bool hardwareAccel, bool hardwareAccel,
bool useLibass, bool useLibass,
bool enableTunneling,
int bufferSize, int bufferSize,
PlayerOptions? playerOptions, PlayerOptions? playerOptions,
double internalVolume, double internalVolume,
@ -420,6 +433,7 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel {
_that.fillScreen, _that.fillScreen,
_that.hardwareAccel, _that.hardwareAccel,
_that.useLibass, _that.useLibass,
_that.enableTunneling,
_that.bufferSize, _that.bufferSize,
_that.playerOptions, _that.playerOptions,
_that.internalVolume, _that.internalVolume,
@ -446,6 +460,7 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel
this.fillScreen = false, this.fillScreen = false,
this.hardwareAccel = true, this.hardwareAccel = true,
this.useLibass = false, this.useLibass = false,
this.enableTunneling = false,
this.bufferSize = 32, this.bufferSize = 32,
this.playerOptions, this.playerOptions,
this.internalVolume = 100, this.internalVolume = 100,
@ -480,6 +495,9 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel
final bool useLibass; final bool useLibass;
@override @override
@JsonKey() @JsonKey()
final bool enableTunneling;
@override
@JsonKey()
final int bufferSize; final int bufferSize;
@override @override
final PlayerOptions? playerOptions; final PlayerOptions? playerOptions;
@ -552,6 +570,7 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel
..add(DiagnosticsProperty('fillScreen', fillScreen)) ..add(DiagnosticsProperty('fillScreen', fillScreen))
..add(DiagnosticsProperty('hardwareAccel', hardwareAccel)) ..add(DiagnosticsProperty('hardwareAccel', hardwareAccel))
..add(DiagnosticsProperty('useLibass', useLibass)) ..add(DiagnosticsProperty('useLibass', useLibass))
..add(DiagnosticsProperty('enableTunneling', enableTunneling))
..add(DiagnosticsProperty('bufferSize', bufferSize)) ..add(DiagnosticsProperty('bufferSize', bufferSize))
..add(DiagnosticsProperty('playerOptions', playerOptions)) ..add(DiagnosticsProperty('playerOptions', playerOptions))
..add(DiagnosticsProperty('internalVolume', internalVolume)) ..add(DiagnosticsProperty('internalVolume', internalVolume))
@ -566,7 +585,7 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel
@override @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys)'; return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys)';
} }
} }
@ -584,6 +603,7 @@ abstract mixin class _$VideoPlayerSettingsModelCopyWith<$Res>
bool fillScreen, bool fillScreen,
bool hardwareAccel, bool hardwareAccel,
bool useLibass, bool useLibass,
bool enableTunneling,
int bufferSize, int bufferSize,
PlayerOptions? playerOptions, PlayerOptions? playerOptions,
double internalVolume, double internalVolume,
@ -614,6 +634,7 @@ class __$VideoPlayerSettingsModelCopyWithImpl<$Res>
Object? fillScreen = null, Object? fillScreen = null,
Object? hardwareAccel = null, Object? hardwareAccel = null,
Object? useLibass = null, Object? useLibass = null,
Object? enableTunneling = null,
Object? bufferSize = null, Object? bufferSize = null,
Object? playerOptions = freezed, Object? playerOptions = freezed,
Object? internalVolume = null, Object? internalVolume = null,
@ -646,6 +667,10 @@ class __$VideoPlayerSettingsModelCopyWithImpl<$Res>
? _self.useLibass ? _self.useLibass
: useLibass // ignore: cast_nullable_to_non_nullable : useLibass // ignore: cast_nullable_to_non_nullable
as bool, as bool,
enableTunneling: null == enableTunneling
? _self.enableTunneling
: enableTunneling // ignore: cast_nullable_to_non_nullable
as bool,
bufferSize: null == bufferSize bufferSize: null == bufferSize
? _self.bufferSize ? _self.bufferSize
: bufferSize // ignore: cast_nullable_to_non_nullable : bufferSize // ignore: cast_nullable_to_non_nullable

View file

@ -15,6 +15,7 @@ _VideoPlayerSettingsModel _$VideoPlayerSettingsModelFromJson(
fillScreen: json['fillScreen'] as bool? ?? false, fillScreen: json['fillScreen'] as bool? ?? false,
hardwareAccel: json['hardwareAccel'] as bool? ?? true, hardwareAccel: json['hardwareAccel'] as bool? ?? true,
useLibass: json['useLibass'] as bool? ?? false, useLibass: json['useLibass'] as bool? ?? false,
enableTunneling: json['enableTunneling'] as bool? ?? false,
bufferSize: (json['bufferSize'] as num?)?.toInt() ?? 32, bufferSize: (json['bufferSize'] as num?)?.toInt() ?? 32,
playerOptions: playerOptions:
$enumDecodeNullable(_$PlayerOptionsEnumMap, json['playerOptions']), $enumDecodeNullable(_$PlayerOptionsEnumMap, json['playerOptions']),
@ -53,6 +54,7 @@ Map<String, dynamic> _$VideoPlayerSettingsModelToJson(
'fillScreen': instance.fillScreen, 'fillScreen': instance.fillScreen,
'hardwareAccel': instance.hardwareAccel, 'hardwareAccel': instance.hardwareAccel,
'useLibass': instance.useLibass, 'useLibass': instance.useLibass,
'enableTunneling': instance.enableTunneling,
'bufferSize': instance.bufferSize, 'bufferSize': instance.bufferSize,
'playerOptions': _$PlayerOptionsEnumMap[instance.playerOptions], 'playerOptions': _$PlayerOptionsEnumMap[instance.playerOptions],
'internalVolume': instance.internalVolume, 'internalVolume': instance.internalVolume,

View file

@ -36,6 +36,7 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
final userData = ref.read(userProvider); final userData = ref.read(userProvider);
pigeon.PlayerSettingsPigeon().sendPlayerSettings( pigeon.PlayerSettingsPigeon().sendPlayerSettings(
pigeon.PlayerSettings( pigeon.PlayerSettings(
enableTunneling: value.enableTunneling,
skipTypes: value.segmentSkipSettings.map( skipTypes: value.segmentSkipSettings.map(
(key, value) => MapEntry( (key, value) => MapEntry(
switch (key) { switch (key) {
@ -82,6 +83,7 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSetti
void setHardwareAccel(bool? value) => state = state.copyWith(hardwareAccel: value ?? true); void setHardwareAccel(bool? value) => state = state.copyWith(hardwareAccel: value ?? true);
void setUseLibass(bool? value) => state = state.copyWith(useLibass: value ?? false); void setUseLibass(bool? value) => state = state.copyWith(useLibass: value ?? false);
void setMediaTunneling(bool? value) => state = state.copyWith(enableTunneling: value ?? false);
void setBufferSize(int? value) => state = state.copyWith(bufferSize: value ?? 32); void setBufferSize(int? value) => state = state.copyWith(bufferSize: value ?? 32);
void setFitType(BoxFit? value) => state = state.copyWith(videoFit: value ?? BoxFit.contain); void setFitType(BoxFit? value) => state = state.copyWith(videoFit: value ?? BoxFit.contain);

View file

@ -171,7 +171,7 @@ class OverviewHeader extends ConsumerWidget {
].addInBetween(const SizedBox(height: 10)), ].addInBetween(const SizedBox(height: 10)),
), ),
), ),
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) ...[ if (AdaptiveLayout.viewSizeOf(context) <= ViewSize.phone) ...[
if (playButton != null) playButton!, if (playButton != null) playButton!,
if (centerButtons != null) centerButtons!, if (centerButtons != null) centerButtons!,
] else ] else

View file

@ -32,7 +32,7 @@ class SimpleVideoPlayer extends ConsumerStatefulWidget {
} }
class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with WindowListener, WidgetsBindingObserver { class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with WindowListener, WidgetsBindingObserver {
late final BasePlayer player = switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) { late final BasePlayer player = switch (ref.read(videoPlayerSettingsProvider).wantedPlayer) {
PlayerOptions.libMDK => LibMDK(), PlayerOptions.libMDK => LibMDK(),
PlayerOptions.libMPV => LibMPV(), PlayerOptions.libMPV => LibMPV(),
_ => LibMDK(), _ => LibMDK(),

View file

@ -209,30 +209,31 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
context.localized.keyboardShortCuts, context.localized.keyboardShortCuts,
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
children: VideoHotKeys.values.map( children: VideoHotKeys.values
(entry) => Padding( .map(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), (entry) => Padding(
child: Row( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
children: [ child: Row(
Expanded( children: [
child: Text( Expanded(
entry.label(context), child: Text(
style: Theme.of(context).textTheme.titleMedium, entry.label(context),
), style: Theme.of(context).textTheme.titleMedium,
),
),
Flexible(
child: KeyCombinationWidget(
currentKey: videoSettings.hotKeys[entry],
defaultKey: videoSettings.defaultShortCuts[entry]!,
onChanged: (value) =>
ref.read(videoPlayerSettingsProvider.notifier).setShortcuts(MapEntry(entry, value)),
),
),
],
), ),
Flexible( ),
child: KeyCombinationWidget( )
currentKey: videoSettings.hotKeys[entry], .toList(),
defaultKey: videoSettings.defaultShortCuts[entry]!,
onChanged: (value) => ref
.read(videoPlayerSettingsProvider.notifier)
.setShortcuts(MapEntry(entry, value)),
),
),
],
),
),
).toList(),
), ),
], ],
), ),
@ -266,106 +267,116 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
context, context,
SettingsLabelDivider(label: context.localized.advanced), SettingsLabelDivider(label: context.localized.advanced),
[ [
if (!ref.read(argumentsStateProvider).leanBackMode) ...[ if (PlayerOptions.available.length != 1)
if (PlayerOptions.available.length != 1) SettingsListTile(
SettingsListTile( label: Text(context.localized.playerSettingsBackendTitle),
label: Text(context.localized.playerSettingsBackendTitle), subLabel: Text(context.localized.playerSettingsBackendDesc),
subLabel: Text(context.localized.playerSettingsBackendDesc), trailing: Builder(builder: (context) {
trailing: Builder(builder: (context) { final wantedPlayer = videoSettings.wantedPlayer;
final wantedPlayer = videoSettings.wantedPlayer; final currentPlayer = videoSettings.playerOptions;
final currentPlayer = videoSettings.playerOptions; return EnumBox(
return EnumBox( current: currentPlayer == null
current: currentPlayer == null ? "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"
? "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})" : wantedPlayer.label(context),
: wantedPlayer.label(context), itemBuilder: (context) => [
itemBuilder: (context) => [ ItemActionButton(
ItemActionButton( label: Text(
label: Text( "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"),
"${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"), action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
videoSettings.copyWith(playerOptions: null),
),
...PlayerOptions.available.map(
(entry) => ItemActionButton(
label: Text(entry.label(context)),
action: () => ref.read(videoPlayerSettingsProvider.notifier).state = action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
videoSettings.copyWith(playerOptions: null), videoSettings.copyWith(playerOptions: entry),
), ),
...PlayerOptions.available.map( )
(entry) => ItemActionButton( ],
label: Text(entry.label(context)), );
action: () => ref.read(videoPlayerSettingsProvider.notifier).state = }),
videoSettings.copyWith(playerOptions: entry),
),
)
],
);
}),
),
AnimatedFadeSize(
child: switch (videoSettings.wantedPlayer) {
PlayerOptions.libMPV => Column(
children: [
SettingsListTile(
label: Text(context.localized.settingsPlayerVideoHWAccelTitle),
subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc),
onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel),
trailing: Switch(
value: videoSettings.hardwareAccel,
onChanged: (value) => provider.setHardwareAccel(value),
),
),
if (!kIsWeb)
SettingsListTile(
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
onTap: () => provider.setUseLibass(!videoSettings.useLibass),
trailing: Switch(
value: videoSettings.useLibass,
onChanged: (value) => provider.setUseLibass(value),
),
),
if (!videoSettings.useLibass)
SettingsListTile(
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
onTap: videoSettings.useLibass
? null
: () {
showDialog(
context: context,
barrierDismissible: true,
useSafeArea: false,
builder: (context) => const SubtitleEditor(),
);
},
),
AnimatedFadeSize(
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
? SettingsMessageBox(
context.localized.settingsPlayerMobileWarning,
messageType: MessageType.warning,
)
: Container(),
),
SettingsListTile(
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);
}
},
)),
),
],
),
PlayerOptions.libMDK => SettingsMessageBox(
messageType: MessageType.info,
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}"),
_ => const SizedBox.shrink()
},
), ),
], AnimatedFadeSize(
child: switch (videoSettings.wantedPlayer) {
PlayerOptions.libMPV => Column(
children: [
SettingsListTile(
label: Text(context.localized.settingsPlayerVideoHWAccelTitle),
subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc),
onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel),
trailing: Switch(
value: videoSettings.hardwareAccel,
onChanged: (value) => provider.setHardwareAccel(value),
),
),
if (!kIsWeb)
SettingsListTile(
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
onTap: () => provider.setUseLibass(!videoSettings.useLibass),
trailing: Switch(
value: videoSettings.useLibass,
onChanged: (value) => provider.setUseLibass(value),
),
),
if (!videoSettings.useLibass)
SettingsListTile(
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
onTap: videoSettings.useLibass
? null
: () {
showDialog(
context: context,
barrierDismissible: true,
useSafeArea: false,
builder: (context) => const SubtitleEditor(),
);
},
),
AnimatedFadeSize(
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
? SettingsMessageBox(
context.localized.settingsPlayerMobileWarning,
messageType: MessageType.warning,
)
: Container(),
),
SettingsListTile(
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);
}
},
)),
),
],
),
PlayerOptions.nativePlayer => Column(
children: [
SettingsListTile(
label: Text(context.localized.mediaTunnelingTitle),
subLabel: Text(context.localized.mediaTunnelingDesc),
onTap: () => provider.setMediaTunneling(!videoSettings.enableTunneling),
trailing: Switch(
value: videoSettings.enableTunneling,
onChanged: (value) => provider.setMediaTunneling(value),
),
),
],
),
PlayerOptions.libMDK => SettingsMessageBox(
messageType: MessageType.info,
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}"),
},
),
if (videoSettings.wantedPlayer != PlayerOptions.nativePlayer) ...[ if (videoSettings.wantedPlayer != PlayerOptions.nativePlayer) ...[
Column( Column(
children: [ children: [

View file

@ -62,7 +62,7 @@ class MediaPlayButton extends ConsumerWidget {
child: onPressed == null child: onPressed == null
? const SizedBox.shrink(key: ValueKey('empty')) ? const SizedBox.shrink(key: ValueKey('empty'))
: Row( : Row(
spacing: 2, spacing: 4,
children: [ children: [
FocusButton( FocusButton(
onTap: () => onPressed?.call(false), onTap: () => onPressed?.call(false),
@ -76,38 +76,35 @@ class MediaPlayButton extends ConsumerWidget {
); );
} }
}, },
child: Padding( child: Stack(
padding: EdgeInsets.all(padding), alignment: Alignment.center,
child: Stack( children: [
alignment: Alignment.center, // Progress background
children: [ Positioned.fill(
// Progress background child: DecoratedBox(
Positioned.fill( decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: radius,
),
),
),
// Button content
buttonTitle(theme.colorScheme.onPrimaryContainer),
Positioned.fill(
child: ClipRect(
clipper: _ProgressClipper(
progress,
),
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer, color: theme.colorScheme.primary,
borderRadius: radius, borderRadius: radius,
), ),
child: buttonTitle(theme.colorScheme.onPrimary),
), ),
), ),
// Button content ),
buttonTitle(theme.colorScheme.onPrimaryContainer), ],
Positioned.fill(
child: ClipRect(
clipper: _ProgressClipper(
progress,
),
child: DecoratedBox(
decoration: BoxDecoration(
color: theme.colorScheme.primary,
borderRadius: radius,
),
child: buttonTitle(theme.colorScheme.onPrimary),
),
),
),
],
),
), ),
), ),
if (progress != 0) if (progress != 0)

View file

@ -45,11 +45,14 @@ enum SegmentSkip {
class PlayerSettings { class PlayerSettings {
PlayerSettings({ PlayerSettings({
required this.enableTunneling,
required this.skipTypes, required this.skipTypes,
required this.skipForward, required this.skipForward,
required this.skipBackward, required this.skipBackward,
}); });
bool enableTunneling;
Map<SegmentType, SegmentSkip> skipTypes; Map<SegmentType, SegmentSkip> skipTypes;
int skipForward; int skipForward;
@ -58,6 +61,7 @@ class PlayerSettings {
List<Object?> _toList() { List<Object?> _toList() {
return <Object?>[ return <Object?>[
enableTunneling,
skipTypes, skipTypes,
skipForward, skipForward,
skipBackward, skipBackward,
@ -70,9 +74,10 @@ class PlayerSettings {
static PlayerSettings decode(Object result) { static PlayerSettings decode(Object result) {
result as List<Object?>; result as List<Object?>;
return PlayerSettings( return PlayerSettings(
skipTypes: (result[0] as Map<Object?, Object?>?)!.cast<SegmentType, SegmentSkip>(), enableTunneling: result[0]! as bool,
skipForward: result[1]! as int, skipTypes: (result[1] as Map<Object?, Object?>?)!.cast<SegmentType, SegmentSkip>(),
skipBackward: result[2]! as int, skipForward: result[2]! as int,
skipBackward: result[3]! as int,
); );
} }

View file

@ -14,7 +14,6 @@ import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/media_playback_model.dart'; import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/models/playback/playback_model.dart'; import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart'; import 'package:fladder/models/settings/video_player_settings.dart';
import 'package:fladder/providers/arguments_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/client_settings_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/providers/video_player_provider.dart'; import 'package:fladder/providers/video_player_provider.dart';
@ -73,13 +72,11 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro
); );
} }
final player = ref.read(argumentsStateProvider).leanBackMode final player = switch (ref.read(videoPlayerSettingsProvider).wantedPlayer) {
? NativePlayer() PlayerOptions.libMDK => LibMDK(),
: switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) { PlayerOptions.libMPV => LibMPV(),
PlayerOptions.libMDK => LibMDK(), PlayerOptions.nativePlayer => NativePlayer(),
PlayerOptions.libMPV => LibMPV(), };
PlayerOptions.nativePlayer => NativePlayer(),
};
setup(player); setup(player);
} }

View file

@ -12,11 +12,13 @@ import 'package:pigeon/pigeon.dart';
), ),
) )
class PlayerSettings { class PlayerSettings {
final bool enableTunneling;
final Map<SegmentType, SegmentSkip> skipTypes; final Map<SegmentType, SegmentSkip> skipTypes;
final int skipForward; final int skipForward;
final int skipBackward; final int skipBackward;
const PlayerSettings({ const PlayerSettings({
required this.enableTunneling,
required this.skipTypes, required this.skipTypes,
required this.skipForward, required this.skipForward,
required this.skipBackward, required this.skipBackward,