feat: Enable player orientation for native player on phones

This commit is contained in:
PartyDonut 2025-10-17 13:05:51 +02:00
parent 08301b9ad8
commit 83c5fafe46
11 changed files with 197 additions and 68 deletions

View file

@ -65,6 +65,19 @@ private object PlayerSettingsHelperPigeonUtils {
} }
enum class PlayerOrientations(val raw: Int) {
PORTRAIT_UP(0),
PORTRAIT_DOWN(1),
LAND_SCAPE_LEFT(2),
LAND_SCAPE_RIGHT(3);
companion object {
fun ofRaw(raw: Int): PlayerOrientations? {
return values().firstOrNull { it.raw == raw }
}
}
}
enum class AutoNextType(val raw: Int) { enum class AutoNextType(val raw: Int) {
OFF(0), OFF(0),
STATIC(1), STATIC(1),
@ -110,7 +123,8 @@ data class PlayerSettings (
val themeColor: Long? = null, val themeColor: Long? = null,
val skipForward: Long, val skipForward: Long,
val skipBackward: Long, val skipBackward: Long,
val autoNextType: AutoNextType val autoNextType: AutoNextType,
val acceptedOrientations: List<PlayerOrientations>
) )
{ {
companion object { companion object {
@ -121,7 +135,8 @@ data class PlayerSettings (
val skipForward = pigeonVar_list[3] as Long val skipForward = pigeonVar_list[3] as Long
val skipBackward = pigeonVar_list[4] as Long val skipBackward = pigeonVar_list[4] as Long
val autoNextType = pigeonVar_list[5] as AutoNextType val autoNextType = pigeonVar_list[5] as AutoNextType
return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward, autoNextType) val acceptedOrientations = pigeonVar_list[6] as List<PlayerOrientations>
return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward, autoNextType, acceptedOrientations)
} }
} }
fun toList(): List<Any?> { fun toList(): List<Any?> {
@ -132,6 +147,7 @@ data class PlayerSettings (
skipForward, skipForward,
skipBackward, skipBackward,
autoNextType, autoNextType,
acceptedOrientations,
) )
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@ -150,20 +166,25 @@ private open class PlayerSettingsHelperPigeonCodec : StandardMessageCodec() {
return when (type) { return when (type) {
129.toByte() -> { 129.toByte() -> {
return (readValue(buffer) as Long?)?.let { return (readValue(buffer) as Long?)?.let {
AutoNextType.ofRaw(it.toInt()) PlayerOrientations.ofRaw(it.toInt())
} }
} }
130.toByte() -> { 130.toByte() -> {
return (readValue(buffer) as Long?)?.let { return (readValue(buffer) as Long?)?.let {
SegmentType.ofRaw(it.toInt()) AutoNextType.ofRaw(it.toInt())
} }
} }
131.toByte() -> { 131.toByte() -> {
return (readValue(buffer) as Long?)?.let { return (readValue(buffer) as Long?)?.let {
SegmentSkip.ofRaw(it.toInt()) SegmentType.ofRaw(it.toInt())
} }
} }
132.toByte() -> { 132.toByte() -> {
return (readValue(buffer) as Long?)?.let {
SegmentSkip.ofRaw(it.toInt())
}
}
133.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { return (readValue(buffer) as? List<Any?>)?.let {
PlayerSettings.fromList(it) PlayerSettings.fromList(it)
} }
@ -173,20 +194,24 @@ private open class PlayerSettingsHelperPigeonCodec : StandardMessageCodec() {
} }
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) { when (value) {
is AutoNextType -> { is PlayerOrientations -> {
stream.write(129) stream.write(129)
writeValue(stream, value.raw) writeValue(stream, value.raw)
} }
is SegmentType -> { is AutoNextType -> {
stream.write(130) stream.write(130)
writeValue(stream, value.raw) writeValue(stream, value.raw)
} }
is SegmentSkip -> { is SegmentType -> {
stream.write(131) stream.write(131)
writeValue(stream, value.raw) writeValue(stream, value.raw)
} }
is PlayerSettings -> { is SegmentSkip -> {
stream.write(132) stream.write(132)
writeValue(stream, value.raw)
}
is PlayerSettings -> {
stream.write(133)
writeValue(stream, value.toList()) writeValue(stream, value.toList())
} }
else -> super.writeValue(stream, value) else -> super.writeValue(stream, value)

View file

@ -28,8 +28,8 @@ fun AudioPicker(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
val selectedIndex by VideoPlayerObject.currentAudioTrackIndex.collectAsState() val selectedIndex by VideoPlayerObject.currentAudioTrackIndex.collectAsState()
val audioTracks by VideoPlayerObject.audioTracks.collectAsState(listOf()) val audioTracks by VideoPlayerObject.audioTracks.collectAsState(emptyList())
val internalAudioTracks by VideoPlayerObject.exoAudioTracks val internalAudioTracks by VideoPlayerObject.exoAudioTracks.collectAsState(emptyList())
if (internalAudioTracks.isEmpty()) return if (internalAudioTracks.isEmpty()) return

View file

@ -29,8 +29,8 @@ fun SubtitlePicker(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
val selectedIndex by VideoPlayerObject.currentSubtitleTrackIndex.collectAsState() val selectedIndex by VideoPlayerObject.currentSubtitleTrackIndex.collectAsState()
val subTitles by VideoPlayerObject.subtitleTracks.collectAsState(listOf()) val subTitles by VideoPlayerObject.subtitleTracks.collectAsState(emptyList())
val internalSubTracks by VideoPlayerObject.exoSubTracks val internalSubTracks by VideoPlayerObject.exoSubTracks.collectAsState(emptyList())
if (internalSubTracks.isEmpty()) return if (internalSubTracks.isEmpty()) return

View file

@ -5,8 +5,9 @@ import VideoPlayerControlsCallback
import VideoPlayerListenerCallback import VideoPlayerListenerCallback
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.runtime.mutableStateOf import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import nl.jknaapen.fladder.VideoPlayerActivity import nl.jknaapen.fladder.VideoPlayerActivity
@ -52,8 +53,8 @@ object VideoPlayerObject {
val currentAudioTrackIndex = val currentAudioTrackIndex =
MutableStateFlow((implementation.playbackData.value?.defaultAudioTrack ?: -1).toInt()) MutableStateFlow((implementation.playbackData.value?.defaultAudioTrack ?: -1).toInt())
val exoAudioTracks = mutableStateOf<List<InternalTrack>>(listOf()) val exoAudioTracks = MutableStateFlow<List<InternalTrack>>(emptyList())
val exoSubTracks = mutableStateOf<List<InternalTrack>>(listOf()) val exoSubTracks = MutableStateFlow<List<InternalTrack>>(emptyList())
fun setSubtitleTrackIndex(value: Int, init: Boolean = false) { fun setSubtitleTrackIndex(value: Int, init: Boolean = false) {
currentSubtitleTrackIndex.value = value currentSubtitleTrackIndex.value = value
@ -72,9 +73,15 @@ object VideoPlayerObject {
val subtitleTracks = implementation.playbackData.map { it?.subtitleTracks ?: listOf() } val subtitleTracks = implementation.playbackData.map { it?.subtitleTracks ?: listOf() }
val audioTracks = implementation.playbackData.map { it?.audioTracks ?: listOf() } val audioTracks = implementation.playbackData.map { it?.audioTracks ?: listOf() }
val hasSubtracks = subtitleTracks.map { it.isNotEmpty() && exoSubTracks.value.isNotEmpty() } val hasSubtracks: Flow<Boolean> =
val hasAudioTracks = audioTracks.map { it.isNotEmpty() && exoAudioTracks.value.isNotEmpty() } combine(subtitleTracks, exoSubTracks.asStateFlow()) { sub, exo ->
sub.isNotEmpty() && exo.isNotEmpty()
}
val hasAudioTracks: Flow<Boolean> =
combine(audioTracks, exoAudioTracks.asStateFlow()) { audio, exo ->
audio.isNotEmpty() && exo.isNotEmpty()
}
fun setPlaybackState(state: PlaybackState) { fun setPlaybackState(state: PlaybackState) {
_currentState.value = state _currentState.value = state

View file

@ -42,6 +42,7 @@ import nl.jknaapen.fladder.composables.overlays.NextUpOverlay
import nl.jknaapen.fladder.messengers.properlySetSubAndAudioTracks import nl.jknaapen.fladder.messengers.properlySetSubAndAudioTracks
import nl.jknaapen.fladder.objects.PlayerSettingsObject import nl.jknaapen.fladder.objects.PlayerSettingsObject
import nl.jknaapen.fladder.objects.VideoPlayerObject import nl.jknaapen.fladder.objects.VideoPlayerObject
import nl.jknaapen.fladder.utility.AllowedOrientations
import nl.jknaapen.fladder.utility.getAudioTracks import nl.jknaapen.fladder.utility.getAudioTracks
import nl.jknaapen.fladder.utility.getSubtitleTracks import nl.jknaapen.fladder.utility.getSubtitleTracks
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -194,7 +195,9 @@ internal fun ExoPlayer(
} }
} }
AllowedOrientations(
PlayerSettingsObject.settings.value?.acceptedOrientations ?: emptyList()
) {
NextUpOverlay( NextUpOverlay(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -233,3 +236,4 @@ internal fun ExoPlayer(
} }
} }
} }
}

View file

@ -0,0 +1,60 @@
package nl.jknaapen.fladder.utility
import PlayerOrientations
import android.content.pm.ActivityInfo
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@Composable
fun AllowedOrientations(
allowed: List<PlayerOrientations>,
content: @Composable () -> Unit
) {
val activity = LocalActivity.current
DisposableEffect(allowed) {
val previousOrientation = activity?.requestedOrientation
val newOrientation = allowed.toRequestedOrientation()
activity?.requestedOrientation = newOrientation
onDispose {
previousOrientation?.let { activity.requestedOrientation = it }
}
}
content()
}
private fun List<PlayerOrientations>.toRequestedOrientation(): Int {
val hasPortraitUp = contains(PlayerOrientations.PORTRAIT_UP)
val hasPortraitDown = contains(PlayerOrientations.PORTRAIT_DOWN)
val hasLandscapeLeft = contains(PlayerOrientations.LAND_SCAPE_LEFT)
val hasLandscapeRight = contains(PlayerOrientations.LAND_SCAPE_RIGHT)
return when {
hasPortraitUp && hasPortraitDown && !hasLandscapeLeft && !hasLandscapeRight ->
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
hasLandscapeLeft && hasLandscapeRight && !hasPortraitUp && !hasPortraitDown ->
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
hasPortraitUp && !hasPortraitDown && !hasLandscapeLeft && !hasLandscapeRight ->
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
hasPortraitDown && !hasPortraitUp && !hasLandscapeLeft && !hasLandscapeRight ->
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
hasLandscapeLeft && !hasLandscapeRight && !hasPortraitUp && !hasPortraitDown ->
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
hasLandscapeRight && !hasLandscapeLeft && !hasPortraitUp && !hasPortraitDown ->
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
hasPortraitUp && hasLandscapeLeft && hasLandscapeRight && hasPortraitDown ->
ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}

View file

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -50,6 +51,16 @@ final pigeonPlayerSettingsSyncProvider = Provider<void>((ref) {
}, },
skipBackward: (userData?.userSettings?.skipBackDuration ?? const Duration(seconds: 15)).inMilliseconds, skipBackward: (userData?.userSettings?.skipBackDuration ?? const Duration(seconds: 15)).inMilliseconds,
skipForward: (userData?.userSettings?.skipForwardDuration ?? const Duration(seconds: 30)).inMilliseconds, skipForward: (userData?.userSettings?.skipForwardDuration ?? const Duration(seconds: 30)).inMilliseconds,
acceptedOrientations: (value.allowedOrientations?.toList() ?? DeviceOrientation.values)
.map(
(e) => switch (e) {
DeviceOrientation.portraitUp => pigeon.PlayerOrientations.portraitUp,
DeviceOrientation.portraitDown => pigeon.PlayerOrientations.portraitDown,
DeviceOrientation.landscapeLeft => pigeon.PlayerOrientations.landScapeLeft,
DeviceOrientation.landscapeRight => pigeon.PlayerOrientations.landScapeRight,
},
)
.toList(),
), ),
); );
} }

View file

@ -408,18 +408,13 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
), ),
], ],
), ),
if (videoSettings.wantedPlayer != PlayerOptions.nativePlayer) ...[ if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb && !ref.read(argumentsStateProvider).htpcMode)
if (!AdaptiveLayout.of(context).isDesktop &&
!kIsWeb &&
!ref.read(argumentsStateProvider).htpcMode &&
videoSettings.wantedPlayer != PlayerOptions.nativePlayer)
SettingsListTile( SettingsListTile(
label: Text(context.localized.playerSettingsOrientationTitle), label: Text(context.localized.playerSettingsOrientationTitle),
subLabel: Text(context.localized.playerSettingsOrientationDesc), subLabel: Text(context.localized.playerSettingsOrientationDesc),
onTap: () => showOrientationOptions(context, ref), onTap: () => showOrientationOptions(context, ref),
), ),
], ],
],
), ),
], ],
); );

View file

@ -29,6 +29,13 @@ bool _deepEquals(Object? a, Object? b) {
} }
enum PlayerOrientations {
portraitUp,
portraitDown,
landScapeLeft,
landScapeRight,
}
enum AutoNextType { enum AutoNextType {
off, off,
static, static,
@ -57,6 +64,7 @@ class PlayerSettings {
required this.skipForward, required this.skipForward,
required this.skipBackward, required this.skipBackward,
required this.autoNextType, required this.autoNextType,
required this.acceptedOrientations,
}); });
bool enableTunneling; bool enableTunneling;
@ -71,6 +79,8 @@ class PlayerSettings {
AutoNextType autoNextType; AutoNextType autoNextType;
List<PlayerOrientations> acceptedOrientations;
List<Object?> _toList() { List<Object?> _toList() {
return <Object?>[ return <Object?>[
enableTunneling, enableTunneling,
@ -79,6 +89,7 @@ class PlayerSettings {
skipForward, skipForward,
skipBackward, skipBackward,
autoNextType, autoNextType,
acceptedOrientations,
]; ];
} }
@ -94,6 +105,7 @@ class PlayerSettings {
skipForward: result[3]! as int, skipForward: result[3]! as int,
skipBackward: result[4]! as int, skipBackward: result[4]! as int,
autoNextType: result[5]! as AutoNextType, autoNextType: result[5]! as AutoNextType,
acceptedOrientations: (result[6] as List<Object?>?)!.cast<PlayerOrientations>(),
); );
} }
@ -123,17 +135,20 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) { if (value is int) {
buffer.putUint8(4); buffer.putUint8(4);
buffer.putInt64(value); buffer.putInt64(value);
} else if (value is AutoNextType) { } else if (value is PlayerOrientations) {
buffer.putUint8(129); buffer.putUint8(129);
writeValue(buffer, value.index); writeValue(buffer, value.index);
} else if (value is SegmentType) { } else if (value is AutoNextType) {
buffer.putUint8(130); buffer.putUint8(130);
writeValue(buffer, value.index); writeValue(buffer, value.index);
} else if (value is SegmentSkip) { } else if (value is SegmentType) {
buffer.putUint8(131); buffer.putUint8(131);
writeValue(buffer, value.index); writeValue(buffer, value.index);
} else if (value is PlayerSettings) { } else if (value is SegmentSkip) {
buffer.putUint8(132); buffer.putUint8(132);
writeValue(buffer, value.index);
} else if (value is PlayerSettings) {
buffer.putUint8(133);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else { } else {
super.writeValue(buffer, value); super.writeValue(buffer, value);
@ -145,14 +160,17 @@ class _PigeonCodec extends StandardMessageCodec {
switch (type) { switch (type) {
case 129: case 129:
final int? value = readValue(buffer) as int?; final int? value = readValue(buffer) as int?;
return value == null ? null : AutoNextType.values[value]; return value == null ? null : PlayerOrientations.values[value];
case 130: case 130:
final int? value = readValue(buffer) as int?; final int? value = readValue(buffer) as int?;
return value == null ? null : SegmentType.values[value]; return value == null ? null : AutoNextType.values[value];
case 131: case 131:
final int? value = readValue(buffer) as int?; final int? value = readValue(buffer) as int?;
return value == null ? null : SegmentSkip.values[value]; return value == null ? null : SegmentType.values[value];
case 132: case 132:
final int? value = readValue(buffer) as int?;
return value == null ? null : SegmentSkip.values[value];
case 133:
return PlayerSettings.decode(readValue(buffer)!); return PlayerSettings.decode(readValue(buffer)!);
default: default:
return super.readValueOfType(type, buffer); return super.readValueOfType(type, buffer);

View file

@ -19,6 +19,7 @@ class PlayerSettings {
final int skipForward; final int skipForward;
final int skipBackward; final int skipBackward;
final AutoNextType autoNextType; final AutoNextType autoNextType;
final List<PlayerOrientations> acceptedOrientations;
const PlayerSettings({ const PlayerSettings({
required this.enableTunneling, required this.enableTunneling,
@ -27,9 +28,17 @@ class PlayerSettings {
required this.skipForward, required this.skipForward,
required this.skipBackward, required this.skipBackward,
required this.autoNextType, required this.autoNextType,
required this.acceptedOrientations,
}); });
} }
enum PlayerOrientations {
portraitUp,
portraitDown,
landScapeLeft,
landScapeRight,
}
enum AutoNextType { enum AutoNextType {
off, off,
static, static,