diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 636ca384d..0dcd612c4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,7 +20,7 @@ android { applicationId = "com.dot.gallery" minSdk = 30 targetSdk = 34 - versionCode = 30006 + versionCode = 30011 versionName = "3.0.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -179,6 +179,10 @@ dependencies { // Exo Player implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.ui) + implementation(libs.androidx.media3.session) + implementation(libs.androidx.media3.exoplayer.dash) + implementation(libs.androidx.media3.exoplayer.hls) + implementation(libs.compose.video) // Exif Interface implementation(libs.androidx.exifinterface) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 049b82db9..5242b8890 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -60,6 +60,7 @@ @@ -72,6 +73,7 @@ android:name=".feature_node.presentation.standalone.StandaloneActivity" android:exported="true" android:launchMode="singleTask" + android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:theme="@style/Theme.Gallery"> @@ -117,6 +119,7 @@ android:name=".feature_node.presentation.picker.PickerActivity" android:exported="true" android:launchMode="singleTask" + android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:theme="@style/Theme.Gallery"> @@ -135,6 +138,7 @@ @@ -145,6 +149,7 @@ diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/MediaViewScreen.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/MediaViewScreen.kt index c2355d5d9..15a3b6dc9 100644 --- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/MediaViewScreen.kt +++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/MediaViewScreen.kt @@ -64,7 +64,6 @@ import com.dot.gallery.feature_node.presentation.util.rememberWindowInsetsContro import com.dot.gallery.feature_node.presentation.util.toggleSystemBars import kotlinx.coroutines.delay import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -161,9 +160,9 @@ fun MediaViewScreen( pageSpacing = 16.dp, ) { index -> var playWhenReady by rememberSaveable { mutableStateOf(false) } - LaunchedEffect(Unit) { + LaunchedEffect(pagerState) { snapshotFlow { pagerState.currentPage } - .collectLatest { currentPage -> + .collect { currentPage -> playWhenReady = currentPage == index } } diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/components/media/MediaPreviewComponent.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/components/media/MediaPreviewComponent.kt index fcd63c41a..2f4d65f84 100644 --- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/components/media/MediaPreviewComponent.kt +++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/components/media/MediaPreviewComponent.kt @@ -8,6 +8,7 @@ package com.dot.gallery.feature_node.presentation.mediaview.components.media import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableLongState import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier import androidx.media3.exoplayer.ExoPlayer @@ -20,7 +21,7 @@ fun MediaPreviewComponent( uiEnabled: Boolean, playWhenReady: Boolean, onItemClick: () -> Unit, - videoController: @Composable (ExoPlayer, MutableState, MutableState, Long, Int, Float) -> Unit, + videoController: @Composable (ExoPlayer, MutableState, MutableLongState, Long, Int, Float) -> Unit, ) { Box( modifier = Modifier diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/components/video/VideoPlayer.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/components/video/VideoPlayer.kt index 049d4bb1f..d1f62202c 100644 --- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/components/video/VideoPlayer.kt +++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/components/video/VideoPlayer.kt @@ -5,17 +5,17 @@ package com.dot.gallery.feature_node.presentation.mediaview.components.video -import android.annotation.SuppressLint import android.media.MediaMetadataRetriever -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.widget.FrameLayout +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableLongState import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -24,32 +24,31 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver -import androidx.media3.common.C -import androidx.media3.common.MediaItem +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.ui.AspectRatioFrameLayout -import androidx.media3.ui.PlayerView +import com.dot.gallery.core.Constants.Animation.enterAnimation +import com.dot.gallery.core.Constants.Animation.exitAnimation import com.dot.gallery.feature_node.domain.model.Media +import io.sanghun.compose.video.RepeatMode +import io.sanghun.compose.video.uri.VideoPlayerMediaItem import kotlinx.coroutines.delay import kotlin.time.Duration.Companion.seconds +import io.sanghun.compose.video.VideoPlayer as SanghunComposeVideoVideoPlayer -@SuppressLint("OpaqueUnitKey") @OptIn(ExperimentalFoundationApi::class) @androidx.annotation.OptIn(UnstableApi::class) @Composable fun VideoPlayer( media: Media, playWhenReady: Boolean, - videoController: @Composable (ExoPlayer, MutableState, MutableState, Long, Int, Float) -> Unit, + videoController: @Composable (ExoPlayer, MutableState, MutableLongState, Long, Int, Float) -> Unit, onItemClick: () -> Unit ) { var totalDuration by rememberSaveable { mutableLongStateOf(0L) } @@ -70,39 +69,92 @@ fun VideoPlayer( )?.toFloat() ?: 60f } - val exoPlayer = remember(context) { - ExoPlayer.Builder(context) - .build().apply { - videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT - repeatMode = Player.REPEAT_MODE_ONE - setMediaItem(MediaItem.fromUri(media.uri)) - prepare() - } + var exoPlayer by remember { + mutableStateOf(null) } - DisposableEffect( - Box { - AndroidView( + Box { + var showPlayer by remember { + mutableStateOf(false) + } + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + showPlayer = true + } + if (event == Lifecycle.Event.ON_PAUSE) { + showPlayer = false + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + LaunchedEffect(showPlayer) { + if (showPlayer) { + delay(100) + exoPlayer?.playWhenReady = true + exoPlayer?.seekTo(currentTime.longValue) + } + } + + if (showPlayer) { + SanghunComposeVideoVideoPlayer( + mediaItems = listOf( + VideoPlayerMediaItem.StorageMediaItem( + storageUri = media.uri, + mimeType = media.mimeType + ) + ), + handleLifecycle = true, + autoPlay = true, + usePlayerController = false, + enablePip = false, + handleAudioFocus = true, + repeatMode = RepeatMode.ONE, + playerInstance = { + exoPlayer = this + addListener( + object : Player.Listener { + override fun onEvents(player: Player, events: Player.Events) { + totalDuration = duration.coerceAtLeast(0L) + lastPlayingState = isPlaying.value + isPlaying.value = player.isPlaying + } + } + ) + }, modifier = Modifier + .fillMaxSize() + .align(Alignment.Center) .combinedClickable( interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = onItemClick, ), - factory = { - PlayerView(context).apply { - useController = false - resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT - - player = exoPlayer - layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) - - keepScreenOn = true + ) + } + AnimatedVisibility( + visible = exoPlayer != null, + enter = enterAnimation, + exit = exitAnimation + ) { + LaunchedEffect(isPlaying.value) { + exoPlayer!!.playWhenReady = isPlaying.value + } + if (isPlaying.value) { + LaunchedEffect(Unit) { + while (true) { + bufferedPercentage = exoPlayer!!.bufferedPercentage + currentTime.longValue = exoPlayer!!.currentPosition + delay(1.seconds / 30) } } - ) + } videoController( - exoPlayer, + exoPlayer!!, isPlaying, currentTime, totalDuration, @@ -110,54 +162,5 @@ fun VideoPlayer( frameRate ) } - ) { - exoPlayer.addListener( - object : Player.Listener { - override fun onEvents(player: Player, events: Player.Events) { - totalDuration = exoPlayer.duration.coerceAtLeast(0L) - lastPlayingState = isPlaying.value - isPlaying.value = player.isPlaying - } - } - ) - onDispose { - exoPlayer.release() - } } - - val lifecycleOwner = LocalLifecycleOwner.current - DisposableEffect(lifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME && isPlaying.value) { - exoPlayer.play() - } else if (event == Lifecycle.Event.ON_STOP || event == Lifecycle.Event.ON_PAUSE) { - exoPlayer.pause() - } - } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } - } - - LaunchedEffect(LocalConfiguration.current, isPlaying.value) { - if (exoPlayer.currentPosition != currentTime.longValue) { - exoPlayer.seekTo(currentTime.longValue) - } - - delay(50) - - exoPlayer.playWhenReady = isPlaying.value - } - - if (isPlaying.value) { - LaunchedEffect(Unit) { - while (true) { - currentTime.longValue = exoPlayer.currentPosition.coerceAtLeast(0L) - bufferedPercentage = exoPlayer.bufferedPercentage - delay(1.seconds / 30) - } - } - } - } \ No newline at end of file diff --git a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/components/video/VideoPlayerController.kt b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/components/video/VideoPlayerController.kt index 879c60093..d1db9a2f5 100644 --- a/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/components/video/VideoPlayerController.kt +++ b/app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/components/video/VideoPlayerController.kt @@ -35,6 +35,7 @@ import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableLongState import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -65,7 +66,7 @@ fun VideoPlayerController( paddingValues: PaddingValues, player: ExoPlayer, isPlaying: MutableState, - currentTime: MutableState, + currentTime: MutableLongState, totalTime: Long, buffer: Int, toggleRotate: () -> Unit, @@ -209,13 +210,14 @@ fun VideoPlayerController( ) Slider( modifier = Modifier.fillMaxWidth(), - value = currentTime.value.toFloat(), + value = currentTime.longValue.toFloat(), onValueChange = { scope.launch { - currentTime.value = it.toLong() - player.seekTo(it.toLong()) - delay(50) - player.play() + if (player.currentPosition != it.toLong()) { + player.seekTo(it.toLong()) + delay(50) + player.play() + } } }, valueRange = 0f..totalTime.toFloat(), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ee8ed8c7e..d0a1bfdf4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ benchmarkMacroJunit4 = "1.2.4" biometric = "1.2.0-alpha05" appcompat = "1.7.0" coilVersion = "3.0.0-alpha06" +composeVideo = "1.2.0" coreSplashscreen = "1.0.1" fuzzywuzzy = "1.4.0" exifinterface = "1.3.7" @@ -22,7 +23,6 @@ compose-bom = "2024.06.00" hilt = "2.51" material = "1.12.0" material3 = "1.2.1" -media3Ui = "1.3.1" media3Exoplayer = "1.3.1" navigation-runtime-ktx = "2.7.7" pinchzoomgrid = "0.0.5" @@ -45,8 +45,11 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "exifinterface" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime" } -androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Ui" } +androidx-media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "media3Exoplayer" } +androidx-media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "media3Exoplayer" } +androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Exoplayer" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" } +androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "media3Exoplayer" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-runtime-ktx" } androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "profileinstaller" } @@ -72,6 +75,7 @@ compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } compose-ui-tooling-preview = { group = "androidx.compose.ui", name= "ui-tooling-preview" } compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "composealpha" } compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +compose-video = { module = "io.sanghun:compose-video", version.ref = "composeVideo" } # Compose-shimmer compose-shimmer = { group = "com.valentinilk.shimmer", name = "compose-shimmer", version = "1.3.0"}