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 }
|
||||
|
||||
|
|
|
|||
42
scripts/bridge-emulator.sh
Executable file
42
scripts/bridge-emulator.sh
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# bridge-emulator.sh — (host-side) expose the emulator's adbd to the LAN so agents
|
||||
# inside the container/other machines can use it: binds :5557 -> 127.0.0.1:5601.
|
||||
# The emulator console transport (emulator-5600) stays local-only; this bridges the
|
||||
# adbd TCP port 5601.
|
||||
set -euo pipefail
|
||||
ADBD_PORT=5601
|
||||
BRIDGE_PORT=5557
|
||||
|
||||
# start emulator if not running
|
||||
ADB="${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}/platform-tools/adb"
|
||||
if ! "$ADB" devices | grep -q "emulator-5600.*device"; then
|
||||
echo "emulator-5600 not running — starting it…"
|
||||
ADB_VENDOR_KEYS="$HOME/.android/adbkey" setsid nohup \
|
||||
"${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}/emulator/emulator" -avd jtubesc -port 5600 \
|
||||
-no-window -gpu swiftshader_indirect -no-snapshot -no-audio -no-boot-anim \
|
||||
> /tmp/jtube-emu.log 2>&1 < /dev/null &
|
||||
echo "waiting for boot (log: /tmp/jtube-emu.log)…"
|
||||
for i in $(seq 1 60); do
|
||||
ok=$("$ADB" -s emulator-5600 shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')
|
||||
[ "$ok" = "1" ] && { echo "BOOTED"; break; }
|
||||
sleep 5
|
||||
done
|
||||
fi
|
||||
|
||||
# keep JTube TV installed + running
|
||||
APK="$HOME/Projects/jtube-tv/app/build/outputs/apk/debug/app-debug.apk"
|
||||
if [ -f "$APK" ]; then
|
||||
"$ADB" -s emulator-5600 install -r "$APK" >/dev/null && echo "JTube TV installed"
|
||||
"$ADB" -s emulator-5600 shell am start -n net.jeena.jtubetv/.MainActivity && echo "app started"
|
||||
fi
|
||||
|
||||
# the bridge (bind all LAN addresses)
|
||||
if ! ss -tln 2>/dev/null | grep -q ":$BRIDGE_PORT"; then
|
||||
setsid nohup socat TCP-LISTEN:$BRIDGE_PORT,bind=0.0.0.0,fork,reuseaddr \
|
||||
TCP:127.0.0.1:$ADBD_PORT > /tmp/jtube-bridge.log 2>&1 < /dev/null &
|
||||
echo "bridge: :$BRIDGE_PORT -> 127.0.0.1:$ADBD_PORT (pid $!)"
|
||||
fi
|
||||
|
||||
echo "From this host: adb connect localhost:$BRIDGE_PORT"
|
||||
echo "From other machines: adb connect rutherford:$BRIDGE_PORT"
|
||||
Loading…
Add table
Add a link
Reference in a new issue