diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt index b561a4538..9c8bd568f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt @@ -52,7 +52,7 @@ fun GetVideoController( if (BackgroundMedia.isPlaying()) { // There is a video playing, start this one on mute. state.controller.volume = 0f - Log.d("PlaybackService", "OnEach Muted due to BackgroundMedia.isPlaying") + Log.d("PlaybackService") { "OnEach Muted due to BackgroundMedia.isPlaying" } } else { // There is no other video playing. Use the default mute state to // decide if sound is on or not. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt index 723899fd8..c86b117e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.unit.IntSize import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.ui.compose.ContentFrame @@ -90,19 +89,22 @@ fun RenderVideoPlayer( hasBlurhash: Boolean = false, accountViewModel: AccountViewModel, ) { - val containerSize = remember { mutableStateOf(IntSize.Zero) } - val isLive = isLiveStreaming(mediaItem.src.videoUri) + // Hold the container size in a non-state holder so layout passes don't trigger an + // unnecessary recomposition of the whole player tree just to update a value that is only + // ever read inside the onDoubleTap callback below. + val containerWidth = remember { intArrayOf(0) } + val isLive = remember(mediaItem.src.videoUri) { isLiveStreaming(mediaItem.src.videoUri) } Box( modifier = borderModifier - .onSizeChanged { containerSize.value = it } + .onSizeChanged { containerWidth[0] = it.width } .pointerInput(isLive, controllerState) { detectTapGestures( onTap = { controllerVisible.value = !controllerVisible.value }, onDoubleTap = { offset -> if (!isLive) { - val isLeftSide = offset.x < containerSize.value.width / 2 + val isLeftSide = offset.x < containerWidth[0] / 2 if (isLeftSide) { controllerState.controller.seekBackward() } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt index 885ecaaba..0160a1246 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt @@ -105,15 +105,29 @@ fun VideoView( thumbhash: String? = null, ) { val initialAutoStart = if (alwaysShowVideo) true else accountViewModel.settings.startVideoPlayback() - val automaticallyStartPlayback = remember { mutableStateOf(initialAutoStart) } + // Reset the manual-show toggle when the video URI changes so a recycled feed slot + // doesn't inherit "tapped to show" state from a prior video. + val automaticallyStartPlayback = remember(videoUri) { mutableStateOf(initialAutoStart) } // Once the video is being shown, only honor the user's autoplay preference when it was auto-loaded. // If the user manually tapped the download button, they want it to play. val autoplay = alwaysShowVideo || (initialAutoStart && accountViewModel.settings.autoPlayVideos()) || (!initialAutoStart && automaticallyStartPlayback.value) - if (blurhash == null && thumbhash == null) { - val ratio = dimensions?.aspectRatio() ?: MediaAspectRatioCache.get(videoUri) + // Resolve the aspect ratio once per composition. Prime the URL-keyed cache from the imeta + // dim tag so the next time this video appears (PiP, dialog, list re-enter) the cache hits + // without waiting for ExoPlayer's onVideoSizeChanged. + val ratio = + remember(videoUri, dimensions) { + val fromDim = dimensions?.takeIf { it.hasSize() } + if (fromDim != null) { + MediaAspectRatioCache.add(videoUri, fromDim.width, fromDim.height) + fromDim.aspectRatio() + } else { + MediaAspectRatioCache.get(videoUri) + } + } + if (blurhash == null && thumbhash == null) { val modifier = if (ratio != null && automaticallyStartPlayback.value) { Modifier.aspectRatio(ratio) @@ -149,8 +163,6 @@ fun VideoView( } } } else { - val ratio = dimensions?.aspectRatio() ?: MediaAspectRatioCache.get(videoUri) - val modifier = if (ratio != null) { Modifier.aspectRatio(ratio) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt index 8923783f0..49e5651a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt @@ -59,6 +59,11 @@ fun VideoViewInner( // keeps a copy of the value to avoid recompositions here when the DEFAULT value changes val muted = remember(videoUri) { DEFAULT_MUTED_SETTING.value } + // The proxy port is decided once per video URI; recomputing on every recomposition does + // pointless work (the result is anyway dropped because GetMediaItem.remember is keyed on the + // URI alone, so the cached MediaItemData is locked in on the first frame). + val proxyPort = remember(videoUri) { accountViewModel.httpClientBuilder.proxyPortForVideo(videoUri) } + GetMediaItem( videoUri = videoUri, title = title, @@ -67,7 +72,7 @@ fun VideoViewInner( callbackUri = nostrUriCallback, mimeType = mimeType, aspectRatio = aspectRatio, - proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(videoUri), + proxyPort = proxyPort, keepPlaying = true, waveformData = waveform, isLiveStream = isLiveStream, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt index 5c4d4250e..d7c1645f0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderTopButtons.kt @@ -105,7 +105,7 @@ fun RenderTopButtons( accountViewModel: AccountViewModel, ) { val context = LocalContext.current - val isLive = isLiveStreaming(mediaData.videoUri) + val isLive = remember(mediaData.videoUri) { isLiveStreaming(mediaData.videoUri) } val pipSupported = remember { context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)