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

View file

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

View file

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

View file

@ -5,8 +5,9 @@ import VideoPlayerControlsCallback
import VideoPlayerListenerCallback
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import nl.jknaapen.fladder.VideoPlayerActivity
@ -52,8 +53,8 @@ object VideoPlayerObject {
val currentAudioTrackIndex =
MutableStateFlow((implementation.playbackData.value?.defaultAudioTrack ?: -1).toInt())
val exoAudioTracks = mutableStateOf<List<InternalTrack>>(listOf())
val exoSubTracks = mutableStateOf<List<InternalTrack>>(listOf())
val exoAudioTracks = MutableStateFlow<List<InternalTrack>>(emptyList())
val exoSubTracks = MutableStateFlow<List<InternalTrack>>(emptyList())
fun setSubtitleTrackIndex(value: Int, init: Boolean = false) {
currentSubtitleTrackIndex.value = value
@ -72,9 +73,15 @@ object VideoPlayerObject {
val subtitleTracks = implementation.playbackData.map { it?.subtitleTracks ?: listOf() }
val audioTracks = implementation.playbackData.map { it?.audioTracks ?: listOf() }
val hasSubtracks = subtitleTracks.map { it.isNotEmpty() && exoSubTracks.value.isNotEmpty() }
val hasAudioTracks = audioTracks.map { it.isNotEmpty() && exoAudioTracks.value.isNotEmpty() }
val hasSubtracks: Flow<Boolean> =
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) {
_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.objects.PlayerSettingsObject
import nl.jknaapen.fladder.objects.VideoPlayerObject
import nl.jknaapen.fladder.utility.AllowedOrientations
import nl.jknaapen.fladder.utility.getAudioTracks
import nl.jknaapen.fladder.utility.getSubtitleTracks
import kotlin.time.Duration.Companion.seconds
@ -194,42 +195,45 @@ internal fun ExoPlayer(
}
}
NextUpOverlay(
modifier = Modifier
.fillMaxSize()
) { showControls ->
AndroidView(
AllowedOrientations(
PlayerSettingsObject.settings.value?.acceptedOrientations ?: emptyList()
) {
NextUpOverlay(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Black),
factory = {
PlayerView(it).apply {
player = exoPlayer
useController = false
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
keepScreenOn = true
subtitleView?.apply {
setStyle(
CaptionStyleCompat(
android.graphics.Color.WHITE,
android.graphics.Color.TRANSPARENT,
android.graphics.Color.TRANSPARENT,
CaptionStyleCompat.EDGE_TYPE_OUTLINE,
android.graphics.Color.BLACK,
null
)
) { showControls ->
AndroidView(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Black),
factory = {
PlayerView(it).apply {
player = exoPlayer
useController = false
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
keepScreenOn = true
subtitleView?.apply {
setStyle(
CaptionStyleCompat(
android.graphics.Color.WHITE,
android.graphics.Color.TRANSPARENT,
android.graphics.Color.TRANSPARENT,
CaptionStyleCompat.EDGE_TYPE_OUTLINE,
android.graphics.Color.BLACK,
null
)
)
}
}
},
)
if (showControls)
CompositionLocalProvider(LocalPlayer provides exoPlayer) {
controls(exoPlayer)
}
},
)
if (showControls)
CompositionLocalProvider(LocalPlayer provides exoPlayer) {
controls(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
}
}