mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Prefetch trickplay images (#445)
Prefetch trickplay images and cache them so that quick scrolling doesn't require network calls and images will appear significantly faster Fixes #432
This commit is contained in:
parent
652becd685
commit
0d8800863b
4 changed files with 81 additions and 13 deletions
|
|
@ -8,8 +8,12 @@ import coil3.compose.setSingletonImageLoaderFactory
|
|||
import coil3.disk.DiskCache
|
||||
import coil3.disk.directory
|
||||
import coil3.memory.MemoryCache
|
||||
import coil3.network.CacheStrategy
|
||||
import coil3.network.NetworkRequest
|
||||
import coil3.network.NetworkResponse
|
||||
import coil3.network.cachecontrol.CacheControlCacheStrategy
|
||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import coil3.request.Options
|
||||
import coil3.request.crossfade
|
||||
import coil3.util.DebugLogger
|
||||
import okhttp3.OkHttpClient
|
||||
|
|
@ -67,10 +71,39 @@ fun CoilConfig(
|
|||
.components {
|
||||
add(
|
||||
OkHttpNetworkFetcherFactory(
|
||||
cacheStrategy = { CacheControlCacheStrategy() },
|
||||
cacheStrategy = { WholphinCacheStrategy(CacheControlCacheStrategy()) },
|
||||
callFactory = { client },
|
||||
),
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This [CacheStrategy] always prefers the cached response for Trickplay images,
|
||||
* otherwise the decision is delegated to the provided [CacheStrategy]
|
||||
*
|
||||
* The expectation is that Trickplay images will be prefetched so the cache will always be warm
|
||||
*/
|
||||
@OptIn(ExperimentalCoilApi::class)
|
||||
private class WholphinCacheStrategy(
|
||||
private val delegate: CacheStrategy,
|
||||
) : CacheStrategy {
|
||||
override suspend fun read(
|
||||
cacheResponse: NetworkResponse,
|
||||
networkRequest: NetworkRequest,
|
||||
options: Options,
|
||||
): CacheStrategy.ReadResult =
|
||||
if (networkRequest.url.contains("/Trickplay/")) {
|
||||
CacheStrategy.ReadResult(cacheResponse)
|
||||
} else {
|
||||
delegate.read(cacheResponse, networkRequest, options)
|
||||
}
|
||||
|
||||
override suspend fun write(
|
||||
cacheResponse: NetworkResponse?,
|
||||
networkRequest: NetworkRequest,
|
||||
networkResponse: NetworkResponse,
|
||||
options: Options,
|
||||
): CacheStrategy.WriteResult = delegate.write(cacheResponse, networkRequest, networkResponse, options)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import androidx.media3.exoplayer.DecoderCounters
|
|||
import androidx.media3.exoplayer.DecoderReuseEvaluation
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.analytics.AnalyticsListener
|
||||
import coil3.imageLoader
|
||||
import coil3.request.ImageRequest
|
||||
import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
||||
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
|
|
@ -92,6 +94,7 @@ import org.jellyfin.sdk.model.api.PlayMethod
|
|||
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||
import org.jellyfin.sdk.model.api.PlaystateCommand
|
||||
import org.jellyfin.sdk.model.api.PlaystateMessage
|
||||
import org.jellyfin.sdk.model.api.TrickplayInfo
|
||||
import org.jellyfin.sdk.model.extensions.inWholeTicks
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
|
|
@ -99,6 +102,7 @@ import timber.log.Timber
|
|||
import java.util.Date
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
|
|
@ -404,6 +408,13 @@ class PlaybackViewModel
|
|||
?.get(mediaSource.id)
|
||||
?.values
|
||||
?.firstOrNull()
|
||||
trickPlayInfo?.let { trickplayInfo ->
|
||||
mediaSource.runTimeTicks?.ticks?.let { duration ->
|
||||
viewModelScope.launchIO {
|
||||
prefetchTrickplay(duration, trickplayInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val chapters = Chapter.fromDto(base, api)
|
||||
withContext(Dispatchers.Main) {
|
||||
|
|
@ -723,18 +734,39 @@ class PlaybackViewModel
|
|||
)
|
||||
}
|
||||
|
||||
fun getTrickplayUrl(index: Int): String? {
|
||||
val itemId = item.id
|
||||
val mediaSourceId = currentItemPlayback.value?.sourceId
|
||||
val trickPlayInfo = currentMediaInfo.value?.trickPlayInfo ?: return null
|
||||
return api.trickplayApi.getTrickplayTileImageUrl(
|
||||
itemId,
|
||||
trickPlayInfo.width,
|
||||
index,
|
||||
mediaSourceId,
|
||||
)
|
||||
private suspend fun prefetchTrickplay(
|
||||
duration: Duration,
|
||||
trickplayInfo: TrickplayInfo,
|
||||
) {
|
||||
val tilesPerImage = trickplayInfo.tileWidth * trickplayInfo.tileHeight
|
||||
val totalCount =
|
||||
(duration.inWholeMilliseconds / trickplayInfo.interval).toInt() / tilesPerImage + 1
|
||||
(0..<totalCount).forEach {
|
||||
val url = getTrickplayUrl(it, trickplayInfo)
|
||||
context.imageLoader.enqueue(
|
||||
ImageRequest
|
||||
.Builder(context)
|
||||
.data(url)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTrickplayUrl(
|
||||
index: Int,
|
||||
trickPlayInfo: TrickplayInfo? = currentMediaInfo.value?.trickPlayInfo,
|
||||
): String? =
|
||||
trickPlayInfo?.let {
|
||||
val itemId = item.id
|
||||
val mediaSourceId = currentItemPlayback.value?.sourceId
|
||||
return api.trickplayApi.getTrickplayTileImageUrl(
|
||||
itemId,
|
||||
trickPlayInfo.width,
|
||||
index,
|
||||
mediaSourceId,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
if (playbackState == Player.STATE_ENDED) {
|
||||
viewModelScope.launchIO {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import androidx.compose.ui.platform.LocalDensity
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.CachePolicy
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.transformations
|
||||
import com.github.damontecres.wholphin.ui.CoilTrickplayTransformation
|
||||
|
|
@ -104,7 +103,6 @@ fun SeekPreviewImage(
|
|||
ImageRequest
|
||||
.Builder(context)
|
||||
.data(previewImageUrl)
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.transformations(
|
||||
CoilTrickplayTransformation(
|
||||
widthPx,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue