mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
feat: adding pip support for android and handling other deprecation warnings
This commit is contained in:
parent
77a337d505
commit
786b4395fb
11 changed files with 164 additions and 11 deletions
|
|
@ -14,6 +14,8 @@
|
|||
<activity
|
||||
android:exported="true"
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:theme="@style/Theme.Nuvio.Splash">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import androidx.activity.compose.setContent
|
|||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.nuvio.app.core.auth.AuthStorage
|
||||
import com.nuvio.app.core.deeplink.handleAppUrl
|
||||
import com.nuvio.app.core.storage.PlatformLocalAccountDataCleaner
|
||||
|
|
@ -19,6 +18,7 @@ import com.nuvio.app.features.mdblist.MdbListSettingsStorage
|
|||
import com.nuvio.app.features.notifications.EpisodeReleaseNotificationPlatform
|
||||
import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsStorage
|
||||
import com.nuvio.app.features.player.PlayerSettingsStorage
|
||||
import com.nuvio.app.features.player.PlayerPictureInPictureManager
|
||||
import com.nuvio.app.features.plugins.PluginStorage
|
||||
import com.nuvio.app.features.profiles.ProfileStorage
|
||||
import com.nuvio.app.features.details.SeasonViewModeStorage
|
||||
|
|
@ -42,9 +42,6 @@ class MainActivity : ComponentActivity() {
|
|||
)
|
||||
super.onCreate(savedInstanceState)
|
||||
window.setBackgroundDrawableResource(R.color.nuvio_background)
|
||||
window.navigationBarColor = getColor(R.color.nuvio_background)
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = false
|
||||
AddonStorage.initialize(applicationContext)
|
||||
AuthStorage.initialize(applicationContext)
|
||||
LibraryStorage.initialize(applicationContext)
|
||||
|
|
@ -81,6 +78,11 @@ class MainActivity : ComponentActivity() {
|
|||
handleIncomingAppIntent(intent)
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
PlayerPictureInPictureManager.onUserLeaveHint(this)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
EpisodeReleaseNotificationPlatform.unbindActivity(this)
|
||||
super.onDestroy()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -195,10 +199,17 @@ actual fun PlatformPlayerSurface(
|
|||
}
|
||||
|
||||
DisposableEffect(exoPlayer, lifecycleOwner) {
|
||||
val activity = context.findActivity()
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_START -> exoPlayer.playWhenReady = playWhenReady
|
||||
Lifecycle.Event.ON_STOP -> exoPlayer.pause()
|
||||
Lifecycle.Event.ON_STOP -> {
|
||||
val isInPictureInPicture =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity?.isInPictureInPictureMode == true
|
||||
if (!isInPictureInPicture) {
|
||||
exoPlayer.pause()
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -388,6 +399,13 @@ actual fun PlatformPlayerSurface(
|
|||
)
|
||||
}
|
||||
|
||||
private tailrec fun Context.findActivity(): Activity? =
|
||||
when (this) {
|
||||
is Activity -> this
|
||||
is ContextWrapper -> baseContext.findActivity()
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun ExoPlayer.snapshot(): PlayerPlaybackSnapshot =
|
||||
PlayerPlaybackSnapshot(
|
||||
isLoading = playbackState == Player.STATE_IDLE || playbackState == Player.STATE_BUFFERING,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PictureInPictureParams
|
||||
import android.os.Build
|
||||
import android.util.Rational
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
|
||||
internal object PlayerPictureInPictureManager {
|
||||
private data class SessionState(
|
||||
val isActive: Boolean = false,
|
||||
val isPlaying: Boolean = false,
|
||||
val playerSize: IntSize = IntSize.Zero,
|
||||
)
|
||||
|
||||
private var sessionState = SessionState()
|
||||
|
||||
fun updateSession(
|
||||
activity: Activity,
|
||||
isActive: Boolean,
|
||||
isPlaying: Boolean,
|
||||
playerSize: IntSize,
|
||||
) {
|
||||
sessionState = SessionState(
|
||||
isActive = isActive,
|
||||
isPlaying = isPlaying,
|
||||
playerSize = playerSize,
|
||||
)
|
||||
applyPictureInPictureParams(activity)
|
||||
}
|
||||
|
||||
fun clearSession(activity: Activity) {
|
||||
sessionState = SessionState()
|
||||
applyPictureInPictureParams(activity)
|
||||
}
|
||||
|
||||
fun onUserLeaveHint(activity: Activity): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
return false
|
||||
}
|
||||
return enterIfEligible(activity)
|
||||
}
|
||||
|
||||
private fun applyPictureInPictureParams(activity: Activity) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
activity.setPictureInPictureParams(buildParams())
|
||||
}
|
||||
|
||||
private fun enterIfEligible(activity: Activity): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false
|
||||
if (!sessionState.isActive || !sessionState.isPlaying) return false
|
||||
if (activity.isFinishing) return false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInPictureInPictureMode) return false
|
||||
return activity.enterPictureInPictureMode(buildParams())
|
||||
}
|
||||
|
||||
private fun buildParams(): PictureInPictureParams {
|
||||
val builder = PictureInPictureParams.Builder()
|
||||
buildAspectRatio(sessionState.playerSize)?.let(builder::setAspectRatio)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setAutoEnterEnabled(sessionState.isActive && sessionState.isPlaying)
|
||||
builder.setSeamlessResizeEnabled(true)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun buildAspectRatio(playerSize: IntSize): Rational? {
|
||||
if (playerSize.width <= 0 || playerSize.height <= 0) return null
|
||||
|
||||
val width = playerSize.width.coerceAtLeast(1)
|
||||
val height = playerSize.height.coerceAtLeast(1)
|
||||
val ratio = width.toDouble() / height.toDouble()
|
||||
|
||||
return when {
|
||||
ratio > MaxPictureInPictureAspectRatio -> Rational(239, 100)
|
||||
ratio < MinPictureInPictureAspectRatio -> Rational(100, 239)
|
||||
else -> Rational(width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val MaxPictureInPictureAspectRatio = 2.39
|
||||
private const val MinPictureInPictureAspectRatio = 1.0 / MaxPictureInPictureAspectRatio
|
||||
|
|
@ -5,11 +5,14 @@ import android.content.Context
|
|||
import android.content.ContextWrapper
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
|
@ -19,6 +22,7 @@ import kotlin.math.roundToInt
|
|||
@Composable
|
||||
actual fun LockPlayerToLandscape() {
|
||||
val activity = LocalContext.current.findActivity() ?: return
|
||||
if (!activity.shouldForceLandscapePlayer()) return
|
||||
|
||||
DisposableEffect(activity) {
|
||||
val previousOrientation = activity.requestedOrientation
|
||||
|
|
@ -50,6 +54,29 @@ actual fun EnterImmersivePlayerMode() {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun ManagePlayerPictureInPicture(
|
||||
isPlaying: Boolean,
|
||||
playerSize: IntSize,
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity() ?: return
|
||||
|
||||
DisposableEffect(activity) {
|
||||
onDispose {
|
||||
PlayerPictureInPictureManager.clearSession(activity)
|
||||
}
|
||||
}
|
||||
|
||||
SideEffect {
|
||||
PlayerPictureInPictureManager.updateSession(
|
||||
activity = activity,
|
||||
isActive = true,
|
||||
isPlaying = isPlaying,
|
||||
playerSize = playerSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun rememberPlayerGestureController(): PlayerGestureController? {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -79,6 +106,12 @@ private tailrec fun Context.findActivity(): Activity? =
|
|||
else -> null
|
||||
}
|
||||
|
||||
private fun Activity.shouldForceLandscapePlayer(): Boolean {
|
||||
if (resources.configuration.smallestScreenWidthDp >= 600) return false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode) return false
|
||||
return true
|
||||
}
|
||||
|
||||
private class AndroidPlayerGestureController(
|
||||
private val activity: Activity,
|
||||
private val audioManager: AudioManager,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
<resources>
|
||||
<style name="Theme.Nuvio" parent="@android:style/Theme.Material.NoActionBar">
|
||||
<item name="android:windowBackground">@color/nuvio_background</item>
|
||||
<item name="android:navigationBarColor">@color/nuvio_background</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Nuvio.Splash" parent="Theme.SplashScreen">
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
<resources>
|
||||
<style name="Theme.Nuvio" parent="@android:style/Theme.Material.NoActionBar">
|
||||
<item name="android:windowBackground">@color/nuvio_background</item>
|
||||
<item name="android:navigationBarColor">@color/nuvio_background</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Nuvio.Splash" parent="@style/Theme.Nuvio" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
|
||||
interface PlayerGestureController {
|
||||
fun currentBrightness(): Float?
|
||||
|
|
@ -20,5 +21,11 @@ expect fun LockPlayerToLandscape()
|
|||
@Composable
|
||||
expect fun EnterImmersivePlayerMode()
|
||||
|
||||
@Composable
|
||||
expect fun ManagePlayerPictureInPicture(
|
||||
isPlaying: Boolean,
|
||||
playerSize: IntSize,
|
||||
)
|
||||
|
||||
@Composable
|
||||
expect fun rememberPlayerGestureController(): PlayerGestureController?
|
||||
|
|
|
|||
|
|
@ -203,6 +203,11 @@ fun PlayerScreen(
|
|||
var nextEpisodeAutoPlayCountdown by remember { mutableStateOf<Int?>(null) }
|
||||
var nextEpisodeAutoPlayJob by remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
ManagePlayerPictureInPicture(
|
||||
isPlaying = playbackSnapshot.isPlaying,
|
||||
playerSize = layoutSize,
|
||||
)
|
||||
|
||||
val playbackSession = remember(
|
||||
contentType,
|
||||
parentMetaId,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.nuvio.app.features.player
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import platform.Foundation.NSNotificationCenter
|
||||
import platform.MediaPlayer.MPVolumeView
|
||||
import platform.UIKit.UIApplication
|
||||
|
|
@ -41,6 +42,12 @@ actual fun EnterImmersivePlayerMode() {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun ManagePlayerPictureInPicture(
|
||||
isPlaying: Boolean,
|
||||
playerSize: IntSize,
|
||||
) = Unit
|
||||
|
||||
@Composable
|
||||
actual fun rememberPlayerGestureController(): PlayerGestureController? {
|
||||
val controller = remember { IOSPlayerGestureController() }
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
CURRENT_PROJECT_VERSION=4
|
||||
MARKETING_VERSION=0.1.0-alpha04
|
||||
CURRENT_PROJECT_VERSION=5
|
||||
MARKETING_VERSION=0.1.0-alpha05
|
||||
Loading…
Reference in a new issue