scripts: bridge-emulator.sh (host) + image prefetching
Perf work for the TV FPS complaint: every page that loads from the API also warms Coil's disk/memory caches (video thumbnails at the same 480x270 the card renders, channel banners/avatars) so rows scroll without popping in over the network. Prefetch keys match render keys exactly so cache hits are guaranteed. bridge-emulator.sh runs OUTSIDE the container on the emulator host: boots jtubesc if needed (ADB_VENDOR_KEYS whitelists the host adb key), installs/starts JTube TV and binds :5557 -> adbd 5601 so the container and LAN can drive it. Run it once in your rutherford shell.
This commit is contained in:
parent
0d583c9660
commit
cc1c1d1c9d
7 changed files with 122 additions and 1 deletions
|
|
@ -13,8 +13,11 @@ object AppGraph {
|
|||
private set
|
||||
lateinit var tokenStore: TokenStore
|
||||
private set
|
||||
var appContext: android.content.Context? = null
|
||||
private set
|
||||
|
||||
fun init(context: Application) {
|
||||
appContext = context
|
||||
tokenStore = TokenStore(context)
|
||||
repository = PeerTubeRepository(tokenStore, PeerTubeNetwork(tokenStore))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package net.jeena.jtubetv.ui.list
|
||||
|
||||
import android.content.Context
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import net.jeena.jtubetv.AppGraph
|
||||
import net.jeena.jtubetv.data.api.Avatar
|
||||
import net.jeena.jtubetv.data.api.VideoChannelDto
|
||||
import net.jeena.jtubetv.data.api.VideoDto
|
||||
|
||||
/**
|
||||
* Warms Coil's caches for images that are already loaded from the API but not yet
|
||||
* visible further down a row/list, so they pop in instantly instead of popping in
|
||||
* over the network while scrolling.
|
||||
*
|
||||
* Uses the same request shape (size) as the visible [VideoCard]/[ChannelCard]
|
||||
* renderers so the memory cache keys match exactly.
|
||||
*/
|
||||
object ImagePrefetcher {
|
||||
|
||||
private val enqueued = HashSet<String>()
|
||||
|
||||
fun videoThumbnails(videos: List<VideoDto>, context: Context = AppGraph.appContext!!) {
|
||||
for (video in videos) {
|
||||
val path = video.thumbnailPath?.takeIf { it.isNotBlank() } ?: continue
|
||||
enqueue(context, AppGraph.repository.absoluteUrl(path), 480, 270)
|
||||
}
|
||||
}
|
||||
|
||||
fun channelArt(channels: List<VideoChannelDto>, context: Context = AppGraph.appContext!!) {
|
||||
for (channel in channels) {
|
||||
val banner = channel.banners.maxByOrNull { it.width }
|
||||
if (banner != null) enqueue(context, absolute(banner), 480, 270)
|
||||
val avatar = channel.avatars.maxByOrNull { it.width }
|
||||
if (avatar != null) enqueue(context, absolute(avatar), 80, 80)
|
||||
}
|
||||
}
|
||||
|
||||
private fun absolute(a: Avatar): String =
|
||||
(a.fileUrl ?: a.path).let {
|
||||
if (it.startsWith("http")) it
|
||||
else (AppGraph.tokenStore.baseUrl ?: "") + (if (it.startsWith("/")) "" else "/") + it
|
||||
}
|
||||
|
||||
private fun enqueue(context: Context, url: String, width: Int, height: Int) {
|
||||
if (!enqueued.add(url)) return
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(url)
|
||||
.size(width, height)
|
||||
.build()
|
||||
context.imageLoader.enqueue(request)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,9 @@ abstract class PagedListViewModel<T> : ViewModel() {
|
|||
|
||||
protected abstract suspend fun fetch(start: Long, count: Int): Result<List<T>>
|
||||
|
||||
/** Warm image caches for just-loaded rows so later scrolls don't fetch from network. */
|
||||
protected open fun onChunkLoaded(chunk: List<T>) {}
|
||||
|
||||
fun loadMore(force: Boolean = false) {
|
||||
if (inFlight || (!force && exhausted)) return
|
||||
inFlight = true
|
||||
|
|
@ -39,6 +42,7 @@ abstract class PagedListViewModel<T> : ViewModel() {
|
|||
exhausted = chunk.size < pageSize
|
||||
items = items + chunk
|
||||
loadingInitial = false
|
||||
onChunkLoaded(chunk)
|
||||
},
|
||||
onFailure = { e ->
|
||||
if (items.isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,17 @@ class VideoListViewModel(
|
|||
val query = MutableStateFlow("")
|
||||
private var activeSearch: String? = null
|
||||
|
||||
override fun onChunkLoaded(chunk: List<VideoListItem>) {
|
||||
ImagePrefetcher.videoThumbnails(
|
||||
chunk.mapNotNull {
|
||||
when (it) {
|
||||
is VideoListItem.Video -> it.video
|
||||
is VideoListItem.HistoryEntry -> it.history.video
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun fetch(start: Long, count: Int): Result<List<VideoListItem>> {
|
||||
return when (val s = source) {
|
||||
is ListSource.LocalVideos ->
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
|
|||
userChannels = (userChannels + mine).distinctBy { it.name to it.host }
|
||||
localChannels = rest.filter { it.host == host }.sortedBy { it.displayName }
|
||||
followedChannels = rest.filter { it.host != host }.sortedBy { it.displayName }
|
||||
net.jeena.jtubetv.ui.list.ImagePrefetcher.channelArt(all)
|
||||
} catch (e: Exception) {
|
||||
error = e.message ?: "Could not load channels"
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,9 @@ class HomeRowsViewModel(private val repository: PeerTubeRepository) : ViewModel(
|
|||
|
||||
private fun fetchSubs() {
|
||||
viewModelScope.launch {
|
||||
subItems = repository.subscriptionVideos(0, 12).getOrNull()?.data ?: emptyList()
|
||||
val subs = repository.subscriptionVideos(0, 12).getOrNull()?.data ?: emptyList()
|
||||
subItems = subs
|
||||
net.jeena.jtubetv.ui.list.ImagePrefetcher.videoThumbnails(subs)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,6 +110,7 @@ class HomeRowsViewModel(private val repository: PeerTubeRepository) : ViewModel(
|
|||
}.awaitAll()
|
||||
continueItems = items
|
||||
continueLoading = false
|
||||
net.jeena.jtubetv.ui.list.ImagePrefetcher.videoThumbnails(items.map { it.video })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,6 +125,10 @@ class HomeRowsViewModel(private val repository: PeerTubeRepository) : ViewModel(
|
|||
|
||||
/** Paged "New videos" row content. */
|
||||
class RecentVideosViewModel(private val repository: PeerTubeRepository) : PagedListViewModel<VideoDto>() {
|
||||
override fun onChunkLoaded(chunk: List<VideoDto>) {
|
||||
net.jeena.jtubetv.ui.list.ImagePrefetcher.videoThumbnails(chunk)
|
||||
}
|
||||
|
||||
override suspend fun fetch(start: Long, count: Int): Result<List<VideoDto>> =
|
||||
repository.localVideos(start, count).map { it.data }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue