mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
Improve trickplay image preview speed (#467)
Moves the clipping of trickplay images from the image loading layer to the composition/graphics layer. Effectively, this PR loads and renders the entire trickplay image which is then moved around to the right position and finally is clipped so only a single tile is shown. Since moving the image around on screen plsu clipping is 1) very fast and 2) done during recompositions, this means it is _much_ faster at rendering each trickplay tile image and Coil only has to load the image once. Fixes #432
This commit is contained in:
parent
ff43f8b1b5
commit
7ab39eba57
4 changed files with 77 additions and 50 deletions
|
|
@ -53,6 +53,7 @@ fun ItemDetailsDialog(
|
|||
val audioLabel = stringResource(R.string.audio)
|
||||
val subtitleLabel = stringResource(R.string.subtitle)
|
||||
val bitrateLabel = stringResource(R.string.bitrate)
|
||||
val unknown = stringResource(R.string.unknown)
|
||||
|
||||
ScrollableDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
|
@ -100,6 +101,7 @@ fun ItemDetailsDialog(
|
|||
source.container?.let { add(containerLabel to it) }
|
||||
if (showFilePath) {
|
||||
source.path?.let { add(pathLabel to it) }
|
||||
add("ID" to (source.id ?: unknown))
|
||||
}
|
||||
source.size?.let {
|
||||
add(fileSizeLabel to formatBytes(it))
|
||||
|
|
|
|||
|
|
@ -398,17 +398,13 @@ fun PlaybackOverlay(
|
|||
val tilesPerImage = trickplayInfo.tileWidth * trickplayInfo.tileHeight
|
||||
val index =
|
||||
(seekProgressMs / trickplayInfo.interval).toInt() / tilesPerImage
|
||||
val imageUrl = trickplayUrlFor(index)
|
||||
val imageUrl = remember(index) { trickplayUrlFor(index) }
|
||||
|
||||
if (imageUrl != null) {
|
||||
SeekPreviewImage(
|
||||
modifier =
|
||||
Modifier,
|
||||
modifier = Modifier,
|
||||
previewImageUrl = imageUrl,
|
||||
duration = playerControls.duration,
|
||||
seekProgressMs = seekProgressMs,
|
||||
videoWidth = trickplayInfo.width,
|
||||
videoHeight = trickplayInfo.height,
|
||||
trickPlayInfo = trickplayInfo,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -411,7 +411,11 @@ class PlaybackViewModel
|
|||
trickPlayInfo?.let { trickplayInfo ->
|
||||
mediaSource.runTimeTicks?.ticks?.let { duration ->
|
||||
viewModelScope.launchIO {
|
||||
prefetchTrickplay(duration, trickplayInfo)
|
||||
prefetchTrickplay(
|
||||
duration,
|
||||
trickplayInfo,
|
||||
mediaSource.id?.toUUIDOrNull(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -737,16 +741,18 @@ class PlaybackViewModel
|
|||
private suspend fun prefetchTrickplay(
|
||||
duration: Duration,
|
||||
trickplayInfo: TrickplayInfo,
|
||||
mediaSourceId: UUID?,
|
||||
) {
|
||||
val tilesPerImage = trickplayInfo.tileWidth * trickplayInfo.tileHeight
|
||||
val totalCount =
|
||||
(duration.inWholeMilliseconds / trickplayInfo.interval).toInt() / tilesPerImage + 1
|
||||
(0..<totalCount).forEach {
|
||||
val url = getTrickplayUrl(it, trickplayInfo)
|
||||
val url = getTrickplayUrl(it, trickplayInfo, mediaSourceId)
|
||||
context.imageLoader.enqueue(
|
||||
ImageRequest
|
||||
.Builder(context)
|
||||
.data(url)
|
||||
.size(coil3.size.Size.ORIGINAL)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
|
@ -755,10 +761,10 @@ class PlaybackViewModel
|
|||
fun getTrickplayUrl(
|
||||
index: Int,
|
||||
trickPlayInfo: TrickplayInfo? = currentMediaInfo.value?.trickPlayInfo,
|
||||
mediaSourceId: UUID? = currentItemPlayback.value?.sourceId,
|
||||
): String? =
|
||||
trickPlayInfo?.let {
|
||||
val itemId = item.id
|
||||
val mediaSourceId = currentItemPlayback.value?.sourceId
|
||||
return api.trickplayApi.getTrickplayTileImageUrl(
|
||||
itemId,
|
||||
trickPlayInfo.width,
|
||||
|
|
|
|||
|
|
@ -1,23 +1,29 @@
|
|||
package com.github.damontecres.wholphin.ui.playback
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.drawscope.scale
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.compose.rememberAsyncImagePainter
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.transformations
|
||||
import com.github.damontecres.wholphin.ui.CoilTrickplayTransformation
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import org.jellyfin.sdk.model.api.TrickplayInfo
|
||||
|
||||
|
|
@ -69,53 +75,70 @@ fun Modifier.offsetByPercent(xPercentage: Float) =
|
|||
@Composable
|
||||
fun SeekPreviewImage(
|
||||
previewImageUrl: String,
|
||||
duration: Long,
|
||||
seekProgressMs: Long,
|
||||
videoWidth: Int?,
|
||||
videoHeight: Int?,
|
||||
trickPlayInfo: TrickplayInfo,
|
||||
modifier: Modifier = Modifier,
|
||||
placeHolder: Painter? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
if (previewImageUrl.isNotNullOrBlank() &&
|
||||
videoWidth != null &&
|
||||
videoHeight != null
|
||||
) {
|
||||
if (previewImageUrl.isNotNullOrBlank()) {
|
||||
val height = 160.dp
|
||||
val width = height * (videoWidth.toFloat() / videoHeight)
|
||||
val heightPx = with(LocalDensity.current) { height.toPx().toInt() }
|
||||
val widthPx = with(LocalDensity.current) { width.toPx().toInt() }
|
||||
val width = height * (trickPlayInfo.width.toFloat() / trickPlayInfo.height)
|
||||
val scale = LocalDensity.current.density
|
||||
|
||||
val index = (seekProgressMs.toDouble() / trickPlayInfo.interval).toInt() // Which tile
|
||||
val numberOfTitlesPerImage = trickPlayInfo.tileHeight * trickPlayInfo.tileWidth
|
||||
val imageIndex = index % numberOfTitlesPerImage
|
||||
|
||||
AsyncImage(
|
||||
modifier =
|
||||
modifier
|
||||
.width(width)
|
||||
.height(height)
|
||||
.background(Color.Black)
|
||||
.border(1.5.dp, color = MaterialTheme.colorScheme.border),
|
||||
model =
|
||||
val model =
|
||||
remember(previewImageUrl) {
|
||||
ImageRequest
|
||||
.Builder(context)
|
||||
.data(previewImageUrl)
|
||||
.transformations(
|
||||
CoilTrickplayTransformation(
|
||||
widthPx,
|
||||
heightPx,
|
||||
trickPlayInfo.tileHeight,
|
||||
trickPlayInfo.tileWidth,
|
||||
imageIndex,
|
||||
index,
|
||||
),
|
||||
).build(),
|
||||
contentScale = ContentScale.None,
|
||||
contentDescription = null,
|
||||
placeholder = placeHolder,
|
||||
)
|
||||
.size(coil3.size.Size.ORIGINAL)
|
||||
.build()
|
||||
}
|
||||
val painter =
|
||||
rememberAsyncImagePainter(
|
||||
model = model,
|
||||
contentScale = ContentScale.None,
|
||||
)
|
||||
val index =
|
||||
(seekProgressMs.toDouble() / trickPlayInfo.interval).toInt() // Index of tile across images
|
||||
val numberOfTilesPerImage = trickPlayInfo.tileHeight * trickPlayInfo.tileWidth
|
||||
val tileIndex =
|
||||
index % numberOfTilesPerImage // Index of tile within the current image
|
||||
val x = (tileIndex % trickPlayInfo.tileWidth) // x position within tile grid
|
||||
val y = (tileIndex / trickPlayInfo.tileHeight) // y position
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.border(1.5.dp, color = MaterialTheme.colorScheme.border)
|
||||
.background(Color.Black)
|
||||
.height(height)
|
||||
.width(width),
|
||||
) {
|
||||
Canvas(
|
||||
modifier =
|
||||
Modifier
|
||||
.height(height)
|
||||
.width(width)
|
||||
.clip(RectangleShape),
|
||||
) {
|
||||
with(painter) {
|
||||
// Scale and translate to the right position in the tile grid
|
||||
scale(scale, scale, pivot = Offset.Zero) {
|
||||
translate(
|
||||
left = -x.toFloat() * trickPlayInfo.width,
|
||||
top = -y.toFloat() * trickPlayInfo.height,
|
||||
) {
|
||||
draw(
|
||||
size =
|
||||
Size(
|
||||
trickPlayInfo.width * trickPlayInfo.tileWidth.toFloat(),
|
||||
trickPlayInfo.height * trickPlayInfo.tileHeight.toFloat(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue