Caches video and image aspect ratios to recompose with the correct size

This commit is contained in:
Vitor Pamplona 2025-03-13 19:30:57 -04:00
parent 4d4c7d5b72
commit 311964ce33
4 changed files with 64 additions and 10 deletions

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model
import android.util.LruCache
object MediaAspectRatioCache {
val mediaAspectRatioCacheByUrl = LruCache<String, Float>(1000)
fun get(url: String) = mediaAspectRatioCacheByUrl.get(url)
fun add(
url: String,
width: Int,
height: Int,
) {
if (height > 1) {
mediaAspectRatioCacheByUrl.put(url, width.toFloat() / height.toFloat())
}
}
}

View File

@ -30,9 +30,11 @@ import androidx.media3.common.Player
import androidx.media3.common.Player.PositionInfo import androidx.media3.common.Player.PositionInfo
import androidx.media3.common.Player.STATE_IDLE import androidx.media3.common.Player.STATE_IDLE
import androidx.media3.common.Player.STATE_READY import androidx.media3.common.Player.STATE_READY
import androidx.media3.common.VideoSize
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.MainActivity
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -113,6 +115,10 @@ class MultiPlayerPlaybackManager(
// avoids saving positions for live streams otherwise caching goes crazy // avoids saving positions for live streams otherwise caching goes crazy
val mustCachePositions = !uri.contains(".m3u8", true) val mustCachePositions = !uri.contains(".m3u8", true)
override fun onVideoSizeChanged(videoSize: VideoSize) {
MediaAspectRatioCache.add(uri, videoSize.width, videoSize.height)
}
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
if (isPlaying) { if (isPlaying) {
player.setWakeMode(C.WAKE_MODE_NETWORK) player.setWakeMode(C.WAKE_MODE_NETWORK)

View File

@ -98,6 +98,7 @@ import com.linc.audiowaveform.infiniteLinearGradient
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache
import com.vitorpamplona.amethyst.commons.compose.produceCachedState import com.vitorpamplona.amethyst.commons.compose.produceCachedState
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.service.playback.PlaybackClientController import com.vitorpamplona.amethyst.service.playback.PlaybackClientController
import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk
@ -261,7 +262,8 @@ fun VideoView(
} }
if (blurhash == null) { if (blurhash == null) {
val ratio = dimensions?.aspectRatio() val ratio = dimensions?.aspectRatio() ?: MediaAspectRatioCache.get(videoUri)
val modifier = val modifier =
if (ratio != null && automaticallyStartPlayback.value) { if (ratio != null && automaticallyStartPlayback.value) {
Modifier.aspectRatio(ratio) Modifier.aspectRatio(ratio)
@ -295,7 +297,7 @@ fun VideoView(
} }
} }
} else { } else {
val ratio = dimensions?.aspectRatio() val ratio = dimensions?.aspectRatio() ?: MediaAspectRatioCache.get(videoUri)
val modifier = val modifier =
if (ratio != null) { if (ratio != null) {

View File

@ -43,6 +43,7 @@ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -77,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaPreloadedContent
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.service.Blurhash import com.vitorpamplona.amethyst.service.Blurhash
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.actions.InformationDialog
@ -87,6 +89,7 @@ import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
import com.vitorpamplona.amethyst.ui.note.HashCheckFailedIcon import com.vitorpamplona.amethyst.ui.note.HashCheckFailedIcon
import com.vitorpamplona.amethyst.ui.note.HashCheckIcon import com.vitorpamplona.amethyst.ui.note.HashCheckIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.UrlImageView
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.Size20dp
import com.vitorpamplona.amethyst.ui.theme.Size24dp import com.vitorpamplona.amethyst.ui.theme.Size24dp
@ -235,7 +238,7 @@ fun LocalImageView(
) )
} }
val ratio = remember(content) { aspectRatio(content.dim) } val ratio = remember(content) { content.dim?.aspectRatio() ?: MediaAspectRatioCache.get(content.localFile.toString()) }
CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center, accountViewModel = accountViewModel) { CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center, accountViewModel = accountViewModel) {
if (it) { if (it) {
SubcomposeAsyncImage( SubcomposeAsyncImage(
@ -276,6 +279,11 @@ fun LocalImageView(
is AsyncImagePainter.State.Success -> { is AsyncImagePainter.State.Success -> {
SubcomposeAsyncImageContent(loadedImageModifier) SubcomposeAsyncImageContent(loadedImageModifier)
SideEffect {
val drawable = (state as AsyncImagePainter.State.Success).result.image
MediaAspectRatioCache.add(content.localFile.toString(), drawable.width, drawable.height)
}
content.isVerified?.let { content.isVerified?.let {
AnimatedVisibility( AnimatedVisibility(
visible = controllerVisible.value, visible = controllerVisible.value,
@ -328,7 +336,7 @@ fun UrlImageView(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
alwayShowImage: Boolean = false, alwayShowImage: Boolean = false,
) { ) {
val ratio = content.dim?.aspectRatio() val ratio = content.dim?.aspectRatio() ?: MediaAspectRatioCache.get(content.url)
val showImage = val showImage =
remember { remember {
@ -385,6 +393,11 @@ fun UrlImageView(
SubcomposeAsyncImageContent(loadedImageModifier) SubcomposeAsyncImageContent(loadedImageModifier)
ShowHashAnimated(content, controllerVisible, Modifier.align(Alignment.TopEnd)) ShowHashAnimated(content, controllerVisible, Modifier.align(Alignment.TopEnd))
SideEffect {
val drawable = (state as AsyncImagePainter.State.Success).result.image
MediaAspectRatioCache.add(content.url, drawable.width, drawable.height)
}
} }
else -> {} else -> {}
} }
@ -519,12 +532,6 @@ fun ShowHash(content: MediaUrlContent) {
verifiedHash?.let { HashVerificationSymbol(it) } verifiedHash?.let { HashVerificationSymbol(it) }
} }
fun aspectRatio(dim: DimensionTag?): Float? {
if (dim == null) return null
return dim.width.toFloat() / dim.height.toFloat()
}
@Composable @Composable
fun WaitAndDisplay( fun WaitAndDisplay(
content: content: