mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
feat: Enable player orientation for native player on phones
This commit is contained in:
parent
08301b9ad8
commit
83c5fafe46
11 changed files with 197 additions and 68 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue