switch to fullscreen and back without dropping frames

This commit is contained in:
Christian Schabesberger 2024-07-17 18:24:20 +02:00
parent 0290eaf9ea
commit 19a48c45a6
7 changed files with 91 additions and 30 deletions

View File

@ -18,8 +18,6 @@
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>. * along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*/ */
import java.util.regex.Pattern.compile
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.jetbrains.kotlin.android)

View File

@ -6,6 +6,7 @@
<application <application
android:name=".NewPlayerApp" android:name=".NewPlayerApp"
android:enableOnBackInvokedCallback="true"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@ -13,8 +14,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.NewPlayer" android:theme="@style/Theme.NewPlayer">
tools:targetApi="31">
<activity <activity
android:name=".VideoPlayerActivity" android:name=".VideoPlayerActivity"
android:exported="false" android:exported="false"

View File

@ -19,6 +19,9 @@ class VideoPlayerActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewModel.initUIState(intent.extras!!)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
VideoPlayerTheme { VideoPlayerTheme {

View File

@ -56,7 +56,7 @@ class VideoPlayerFragment() : Fragment() {
insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
if (viewModel.uiState.value.fullscreen) { if (viewModel.uiState.value.fullscreen) {
println("gurken fragment created for fullscreen") //println("gurken fragment created for fullscreen")
//insetsController.hide(WindowInsetsCompat.Type.systemBars()) //insetsController.hide(WindowInsetsCompat.Type.systemBars())
} }
@ -72,7 +72,7 @@ class VideoPlayerFragment() : Fragment() {
} }
override fun switchToFullscreen() { override fun switchToFullscreen() {
println("gurken fullscreen") //println("gurken fullscreen")
} }
} }
@ -80,7 +80,7 @@ class VideoPlayerFragment() : Fragment() {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent { setContent {
VideoPlayerTheme { VideoPlayerTheme {
VideoPlayerUI(viewModel = viewModel, isFullscreen = false) VideoPlayerUI(viewModel = viewModel)
} }
} }
} }

View File

@ -21,7 +21,10 @@
package net.newpipe.newplayer.model package net.newpipe.newplayer.model
import android.app.Application import android.app.Application
import android.os.Build
import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.RequiresApi
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -62,6 +65,7 @@ interface VideoPlayerViewModel {
var listener: Listener? var listener: Listener?
val events: SharedFlow<Events>? val events: SharedFlow<Events>?
fun initUIState(instanceState: Bundle)
fun preparePlayer() fun preparePlayer()
fun play() fun play()
fun pause() fun pause()
@ -99,6 +103,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
override val events: SharedFlow<VideoPlayerViewModel.Events> = mutableEvent override val events: SharedFlow<VideoPlayerViewModel.Events> = mutableEvent
override val uiState = mutableUiState.asStateFlow() override val uiState = mutableUiState.asStateFlow()
override var listener: VideoPlayerViewModel.Listener? = null override var listener: VideoPlayerViewModel.Listener? = null
@ -143,6 +148,22 @@ class VideoPlayerViewModelImpl @Inject constructor(
}) })
} }
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun initUIState(instanceState: Bundle) {
val uiState =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
instanceState.getParcelable(VIDEOPLAYER_UI_STATE, VideoPlayerUIState::class.java)
else
instanceState.getParcelable(VIDEOPLAYER_UI_STATE)
uiState?.let { uiState ->
mutableUiState.update {
uiState
}
}
}
override fun preparePlayer() { override fun preparePlayer() {
if (player.playbackState == Player.STATE_IDLE) { if (player.playbackState == Player.STATE_IDLE) {
player.prepare() player.prepare()
@ -172,19 +193,12 @@ class VideoPlayerViewModelImpl @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
mutableEvent.emit(VideoPlayerViewModel.Events.SwitchToEmbeddedView) mutableEvent.emit(VideoPlayerViewModel.Events.SwitchToEmbeddedView)
} }
//mutableUiState.update {
// it.copy(fullscreen = false)
//}
} }
override fun switchToFullscreen() { override fun switchToFullscreen() {
viewModelScope.launch { viewModelScope.launch {
mutableEvent.emit(VideoPlayerViewModel.Events.SwitchToFullscreen) mutableEvent.emit(VideoPlayerViewModel.Events.SwitchToFullscreen)
} }
//mutableUiState.update {
// it.copy(fullscreen = true)
//}
//listener?.switchToFullscreen()
} }
companion object { companion object {
@ -194,6 +208,10 @@ class VideoPlayerViewModelImpl @Inject constructor(
override var listener: VideoPlayerViewModel.Listener? = null override var listener: VideoPlayerViewModel.Listener? = null
override val events: SharedFlow<VideoPlayerViewModel.Events>? = null override val events: SharedFlow<VideoPlayerViewModel.Events>? = null
override fun initUIState(instanceState: Bundle) {
println("dummy impl")
}
override fun preparePlayer() { override fun preparePlayer() {
println("dummy impl") println("dummy impl")
} }

View File

@ -20,8 +20,15 @@
package net.newpipe.newplayer.ui package net.newpipe.newplayer.ui
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.view.SurfaceView import android.view.SurfaceView
import androidx.activity.compose.BackHandler
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -40,6 +47,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import net.newpipe.newplayer.VideoPlayerActivity import net.newpipe.newplayer.VideoPlayerActivity
import net.newpipe.newplayer.model.VIDEOPLAYER_UI_STATE import net.newpipe.newplayer.model.VIDEOPLAYER_UI_STATE
@ -72,19 +80,27 @@ fun VideoPlayerUI(
} }
} }
BackHandler {
closeFullscreen(viewModel, activity!!)
}
val fullscreenLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
println("gurken returned for result")
viewModel.initUIState(result.data?.extras!!)
}
LaunchedEffect(key1 = Unit) { LaunchedEffect(key1 = Unit) {
viewModel.events?.collectLatest { event -> viewModel.events?.collectLatest { event ->
when (event) { when (event) {
VideoPlayerViewModel.Events.SwitchToEmbeddedView -> { VideoPlayerViewModel.Events.SwitchToEmbeddedView -> {
activity?.finish() closeFullscreen(viewModel, activity!!)
} }
VideoPlayerViewModel.Events.SwitchToFullscreen -> { VideoPlayerViewModel.Events.SwitchToFullscreen -> {
val fullscreen_activity_intent = openFullscreen(viewModel, activity!!, fullscreenLauncher)
Intent(activity!!.findActivity(), VideoPlayerActivity::class.java)
fullscreen_activity_intent.putExtra(VIDEOPLAYER_UI_STATE, viewModel.uiState.value)
activity.startActivity(fullscreen_activity_intent)
} }
} }
} }
@ -97,14 +113,18 @@ fun VideoPlayerUI(
AndroidView( AndroidView(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
factory = { context -> factory = { context ->
SurfaceView(context).also { SurfaceView(context).also { view ->
viewModel.player?.setVideoSurfaceView(it) viewModel.player?.setVideoSurfaceView(view)
} }
}, update = { }, update = { view ->
when (lifecycle) { when (lifecycle) {
Lifecycle.Event.ON_PAUSE -> { Lifecycle.Event.ON_PAUSE -> {
println("gurken state on pause") println("gurken state on pause")
viewModel.pause() }
Lifecycle.Event.ON_RESUME -> {
println("gurken resume")
viewModel.player?.setVideoSurfaceView(view)
} }
else -> Unit else -> Unit
@ -112,7 +132,7 @@ fun VideoPlayerUI(
}) })
val isPlaying = viewModel.player!!.isPlaying val isPlaying = viewModel.player!!.isPlaying
println("is Player playing: $isPlaying")
VideoPlayerControllerUI( VideoPlayerControllerUI(
isPlaying = uiState.playing, isPlaying = uiState.playing,
fullscreen = uiState.fullscreen, fullscreen = uiState.fullscreen,
@ -126,6 +146,28 @@ fun VideoPlayerUI(
} }
} }
fun closeFullscreen(viewModel: VideoPlayerViewModel, activity: Activity) {
val return_fullscreen_intent = Intent()
var uiState = viewModel.uiState.value
uiState.fullscreen = false
return_fullscreen_intent.putExtra(VIDEOPLAYER_UI_STATE, uiState)
activity.setResult(0, return_fullscreen_intent)
activity.finish()
}
fun openFullscreen(
viewModel: VideoPlayerViewModel,
activity: Activity,
fullscreenLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>
) {
val fullscreen_activity_intent =
Intent(activity!!.findActivity(), VideoPlayerActivity::class.java)
var uiState = viewModel.uiState.value
uiState.fullscreen = true
fullscreen_activity_intent.putExtra(VIDEOPLAYER_UI_STATE, uiState)
fullscreenLauncher.launch(fullscreen_activity_intent)
}
@Preview(device = "spec:width=1080px,height=700px,dpi=440,orientation=landscape") @Preview(device = "spec:width=1080px,height=700px,dpi=440,orientation=landscape")
@Composable @Composable
fun PlayerUIPreviewEmbeded() { fun PlayerUIPreviewEmbeded() {

View File

@ -19,7 +19,7 @@
[versions] [versions]
agp = "8.5.0" agp = "8.5.0"
kotlin = "2.0.0" kotlin = "2.0.20-Beta2"
coreKtx = "1.13.1" coreKtx = "1.13.1"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.2.1" junitVersion = "1.2.1"
@ -30,16 +30,16 @@ androidx = "1.9.0"
constraintlayout = "2.1.4" constraintlayout = "2.1.4"
material3 = "1.2.1" material3 = "1.2.1"
uiTooling = "1.6.8" uiTooling = "1.6.8"
materialIconsExtendedAndroid = "1.7.0-beta04" materialIconsExtendedAndroid = "1.7.0-beta05"
media3Ui = "1.4.0-beta01" media3Ui = "1.4.0-beta01"
hiltAndroid = "2.51.1" hiltAndroid = "2.51.1"
hiltCompiler = "1.2.0" hiltCompiler = "1.2.0"
hiltNavigationCompose = "1.2.0" hiltNavigationCompose = "1.2.0"
lifecycleViewmodelCompose = "2.8.3" lifecycleViewmodelCompose = "2.8.3"
kspVersion = "1.9.0-1.0.13" kspVersion = "2.0.20-Beta2-1.0.23"
fragmentKtx = "1.8.1" fragmentKtx = "1.8.1"
lifecycleRuntimeKtx = "2.8.3" lifecycleRuntimeKtx = "2.8.3"
composeBom = "2024.04.01" composeBom = "2024.06.01"
kotlinParcelize = "2.0.20-Beta2" kotlinParcelize = "2.0.20-Beta2"
[libraries] [libraries]
@ -65,7 +65,7 @@ androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "l
androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "uiTooling" } androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "uiTooling" }
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" } androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version = "2024.06.00" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }