mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 08:01: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 audioLabel = stringResource(R.string.audio)
|
||||||
val subtitleLabel = stringResource(R.string.subtitle)
|
val subtitleLabel = stringResource(R.string.subtitle)
|
||||||
val bitrateLabel = stringResource(R.string.bitrate)
|
val bitrateLabel = stringResource(R.string.bitrate)
|
||||||
|
val unknown = stringResource(R.string.unknown)
|
||||||
|
|
||||||
ScrollableDialog(
|
ScrollableDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
|
@ -100,6 +101,7 @@ fun ItemDetailsDialog(
|
||||||
source.container?.let { add(containerLabel to it) }
|
source.container?.let { add(containerLabel to it) }
|
||||||
if (showFilePath) {
|
if (showFilePath) {
|
||||||
source.path?.let { add(pathLabel to it) }
|
source.path?.let { add(pathLabel to it) }
|
||||||
|
add("ID" to (source.id ?: unknown))
|
||||||
}
|
}
|
||||||
source.size?.let {
|
source.size?.let {
|
||||||
add(fileSizeLabel to formatBytes(it))
|
add(fileSizeLabel to formatBytes(it))
|
||||||
|
|
|
||||||
|
|
@ -398,17 +398,13 @@ fun PlaybackOverlay(
|
||||||
val tilesPerImage = trickplayInfo.tileWidth * trickplayInfo.tileHeight
|
val tilesPerImage = trickplayInfo.tileWidth * trickplayInfo.tileHeight
|
||||||
val index =
|
val index =
|
||||||
(seekProgressMs / trickplayInfo.interval).toInt() / tilesPerImage
|
(seekProgressMs / trickplayInfo.interval).toInt() / tilesPerImage
|
||||||
val imageUrl = trickplayUrlFor(index)
|
val imageUrl = remember(index) { trickplayUrlFor(index) }
|
||||||
|
|
||||||
if (imageUrl != null) {
|
if (imageUrl != null) {
|
||||||
SeekPreviewImage(
|
SeekPreviewImage(
|
||||||
modifier =
|
modifier = Modifier,
|
||||||
Modifier,
|
|
||||||
previewImageUrl = imageUrl,
|
previewImageUrl = imageUrl,
|
||||||
duration = playerControls.duration,
|
|
||||||
seekProgressMs = seekProgressMs,
|
seekProgressMs = seekProgressMs,
|
||||||
videoWidth = trickplayInfo.width,
|
|
||||||
videoHeight = trickplayInfo.height,
|
|
||||||
trickPlayInfo = trickplayInfo,
|
trickPlayInfo = trickplayInfo,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -411,7 +411,11 @@ class PlaybackViewModel
|
||||||
trickPlayInfo?.let { trickplayInfo ->
|
trickPlayInfo?.let { trickplayInfo ->
|
||||||
mediaSource.runTimeTicks?.ticks?.let { duration ->
|
mediaSource.runTimeTicks?.ticks?.let { duration ->
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
prefetchTrickplay(duration, trickplayInfo)
|
prefetchTrickplay(
|
||||||
|
duration,
|
||||||
|
trickplayInfo,
|
||||||
|
mediaSource.id?.toUUIDOrNull(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -737,16 +741,18 @@ class PlaybackViewModel
|
||||||
private suspend fun prefetchTrickplay(
|
private suspend fun prefetchTrickplay(
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
trickplayInfo: TrickplayInfo,
|
trickplayInfo: TrickplayInfo,
|
||||||
|
mediaSourceId: UUID?,
|
||||||
) {
|
) {
|
||||||
val tilesPerImage = trickplayInfo.tileWidth * trickplayInfo.tileHeight
|
val tilesPerImage = trickplayInfo.tileWidth * trickplayInfo.tileHeight
|
||||||
val totalCount =
|
val totalCount =
|
||||||
(duration.inWholeMilliseconds / trickplayInfo.interval).toInt() / tilesPerImage + 1
|
(duration.inWholeMilliseconds / trickplayInfo.interval).toInt() / tilesPerImage + 1
|
||||||
(0..<totalCount).forEach {
|
(0..<totalCount).forEach {
|
||||||
val url = getTrickplayUrl(it, trickplayInfo)
|
val url = getTrickplayUrl(it, trickplayInfo, mediaSourceId)
|
||||||
context.imageLoader.enqueue(
|
context.imageLoader.enqueue(
|
||||||
ImageRequest
|
ImageRequest
|
||||||
.Builder(context)
|
.Builder(context)
|
||||||
.data(url)
|
.data(url)
|
||||||
|
.size(coil3.size.Size.ORIGINAL)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -755,10 +761,10 @@ class PlaybackViewModel
|
||||||
fun getTrickplayUrl(
|
fun getTrickplayUrl(
|
||||||
index: Int,
|
index: Int,
|
||||||
trickPlayInfo: TrickplayInfo? = currentMediaInfo.value?.trickPlayInfo,
|
trickPlayInfo: TrickplayInfo? = currentMediaInfo.value?.trickPlayInfo,
|
||||||
|
mediaSourceId: UUID? = currentItemPlayback.value?.sourceId,
|
||||||
): String? =
|
): String? =
|
||||||
trickPlayInfo?.let {
|
trickPlayInfo?.let {
|
||||||
val itemId = item.id
|
val itemId = item.id
|
||||||
val mediaSourceId = currentItemPlayback.value?.sourceId
|
|
||||||
return api.trickplayApi.getTrickplayTileImageUrl(
|
return api.trickplayApi.getTrickplayTileImageUrl(
|
||||||
itemId,
|
itemId,
|
||||||
trickPlayInfo.width,
|
trickPlayInfo.width,
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,29 @@
|
||||||
package com.github.damontecres.wholphin.ui.playback
|
package com.github.damontecres.wholphin.ui.playback
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
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.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.ContentScale
|
||||||
import androidx.compose.ui.layout.layout
|
import androidx.compose.ui.layout.layout
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.rememberAsyncImagePainter
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.transformations
|
|
||||||
import com.github.damontecres.wholphin.ui.CoilTrickplayTransformation
|
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import org.jellyfin.sdk.model.api.TrickplayInfo
|
import org.jellyfin.sdk.model.api.TrickplayInfo
|
||||||
|
|
||||||
|
|
@ -69,53 +75,70 @@ fun Modifier.offsetByPercent(xPercentage: Float) =
|
||||||
@Composable
|
@Composable
|
||||||
fun SeekPreviewImage(
|
fun SeekPreviewImage(
|
||||||
previewImageUrl: String,
|
previewImageUrl: String,
|
||||||
duration: Long,
|
|
||||||
seekProgressMs: Long,
|
seekProgressMs: Long,
|
||||||
videoWidth: Int?,
|
|
||||||
videoHeight: Int?,
|
|
||||||
trickPlayInfo: TrickplayInfo,
|
trickPlayInfo: TrickplayInfo,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
placeHolder: Painter? = null,
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
if (previewImageUrl.isNotNullOrBlank() &&
|
if (previewImageUrl.isNotNullOrBlank()) {
|
||||||
videoWidth != null &&
|
|
||||||
videoHeight != null
|
|
||||||
) {
|
|
||||||
val height = 160.dp
|
val height = 160.dp
|
||||||
val width = height * (videoWidth.toFloat() / videoHeight)
|
val width = height * (trickPlayInfo.width.toFloat() / trickPlayInfo.height)
|
||||||
val heightPx = with(LocalDensity.current) { height.toPx().toInt() }
|
val scale = LocalDensity.current.density
|
||||||
val widthPx = with(LocalDensity.current) { width.toPx().toInt() }
|
|
||||||
|
|
||||||
val index = (seekProgressMs.toDouble() / trickPlayInfo.interval).toInt() // Which tile
|
val model =
|
||||||
val numberOfTitlesPerImage = trickPlayInfo.tileHeight * trickPlayInfo.tileWidth
|
remember(previewImageUrl) {
|
||||||
val imageIndex = index % numberOfTitlesPerImage
|
|
||||||
|
|
||||||
AsyncImage(
|
|
||||||
modifier =
|
|
||||||
modifier
|
|
||||||
.width(width)
|
|
||||||
.height(height)
|
|
||||||
.background(Color.Black)
|
|
||||||
.border(1.5.dp, color = MaterialTheme.colorScheme.border),
|
|
||||||
model =
|
|
||||||
ImageRequest
|
ImageRequest
|
||||||
.Builder(context)
|
.Builder(context)
|
||||||
.data(previewImageUrl)
|
.data(previewImageUrl)
|
||||||
.transformations(
|
.size(coil3.size.Size.ORIGINAL)
|
||||||
CoilTrickplayTransformation(
|
.build()
|
||||||
widthPx,
|
}
|
||||||
heightPx,
|
val painter =
|
||||||
trickPlayInfo.tileHeight,
|
rememberAsyncImagePainter(
|
||||||
trickPlayInfo.tileWidth,
|
model = model,
|
||||||
imageIndex,
|
|
||||||
index,
|
|
||||||
),
|
|
||||||
).build(),
|
|
||||||
contentScale = ContentScale.None,
|
contentScale = ContentScale.None,
|
||||||
contentDescription = null,
|
)
|
||||||
placeholder = placeHolder,
|
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