chore(playback): add diagnostic logs for cache + quality selection

Three debug-level log lines for verifying the HLS caching and
resolution-policy plumbing on a real device. Easy to revert as a single
commit before merging if the noise is unwanted.

- VideoCache: one line at app start with computed size, available, total disk
- CustomMediaSourceFactory: one line per MediaSource creation, CACHE or BYPASS
- InitialVideoQualitySelector: one line per applied policy, with selected
  resolution and the rendition list

Tail with: adb logcat -s VideoCache CustomMediaSourceFactory VideoQuality

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
davotoula
2026-04-21 19:42:49 +01:00
parent 63454761ef
commit 2ed1b17f03
3 changed files with 39 additions and 8 deletions
@@ -28,6 +28,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Tracks import androidx.media3.common.Tracks
import com.vitorpamplona.quartz.utils.Log
/** /**
* Which HLS rendition to pick automatically the first time tracks become available for a media * Which HLS rendition to pick automatically the first time tracks become available for a media
@@ -96,13 +97,26 @@ private fun applyInitialQuality(
// No point forcing a choice when there's only one rendition, and no future update will // No point forcing a choice when there's only one rendition, and no future update will
// change that for this media id, so mark it as settled. // change that for this media id, so mark it as settled.
if (videoGroup.length <= 1) { if (videoGroup.length <= 1) {
Log.d("VideoQuality") {
val f = videoGroup.getTrackFormat(0)
"policy=$policy mediaId=$mediaId SINGLE rendition ${f.width}x${f.height} (no choice)"
}
appliedForMediaId.value = mediaId appliedForMediaId.value = mediaId
return return
} }
val renditions =
(0 until videoGroup.length).joinToString(", ") {
val f = videoGroup.getTrackFormat(it)
"${f.width}x${f.height}"
}
when (policy) { when (policy) {
VideoQualityPolicy.AUTO -> { VideoQualityPolicy.AUTO -> {
if (hasVideoOverride(player)) clearVideoOverride(player) if (hasVideoOverride(player)) clearVideoOverride(player)
Log.d("VideoQuality") {
"policy=AUTO mediaId=$mediaId cleared override (from ${videoGroup.length} renditions: [$renditions])"
}
appliedForMediaId.value = mediaId appliedForMediaId.value = mediaId
} }
@@ -111,6 +125,10 @@ private fun applyInitialQuality(
// retry on the next onTracksChanged when real video dimensions arrive. // retry on the next onTracksChanged when real video dimensions arrive.
val lowestIndex = findLowestResolutionTrackIndex(videoGroup) ?: return val lowestIndex = findLowestResolutionTrackIndex(videoGroup) ?: return
selectVideoTrack(player, videoGroup, lowestIndex) selectVideoTrack(player, videoGroup, lowestIndex)
Log.d("VideoQuality") {
val f = videoGroup.getTrackFormat(lowestIndex)
"policy=LOWEST mediaId=$mediaId selected=${f.width}x${f.height} (from ${videoGroup.length} renditions: [$renditions])"
}
appliedForMediaId.value = mediaId appliedForMediaId.value = mediaId
} }
} }
@@ -28,6 +28,7 @@ import androidx.media3.datasource.DataSource
import androidx.media3.datasource.cache.CacheDataSource import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache import androidx.media3.datasource.cache.SimpleCache
import com.vitorpamplona.quartz.utils.Log
import java.io.File import java.io.File
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
@@ -35,8 +36,10 @@ class VideoCache {
companion object { companion object {
// Target fraction of currently-available disk space. // Target fraction of currently-available disk space.
private const val CACHE_SIZE_PERCENT = 0.20 private const val CACHE_SIZE_PERCENT = 0.20
// Hard cap so we never consume more than this even on large devices. // Hard cap so we never consume more than this even on large devices.
private const val CACHE_SIZE_MAX_BYTES = 4L * 1024 * 1024 * 1024 // 4 GB private const val CACHE_SIZE_MAX_BYTES = 4L * 1024 * 1024 * 1024 // 4 GB
// Floor so the cache is still useful on low-storage devices. // Floor so the cache is still useful on low-storage devices.
private const val CACHE_SIZE_MIN_BYTES = 256L * 1024 * 1024 // 256 MB private const val CACHE_SIZE_MIN_BYTES = 256L * 1024 * 1024 // 256 MB
} }
@@ -76,12 +79,16 @@ class VideoCache {
*/ */
private fun calculateCacheSize(cachePath: File): Long { private fun calculateCacheSize(cachePath: File): Long {
cachePath.mkdirs() cachePath.mkdirs()
val availableBytes = val statFs = runCatching { StatFs(cachePath.absolutePath) }.getOrNull()
runCatching { val availableBytes = statFs?.availableBytes ?: 0L
StatFs(cachePath.absolutePath).availableBytes
}.getOrDefault(0L)
val target = (availableBytes * CACHE_SIZE_PERCENT).toLong() val target = (availableBytes * CACHE_SIZE_PERCENT).toLong()
return target.coerceIn(CACHE_SIZE_MIN_BYTES, CACHE_SIZE_MAX_BYTES) val cacheSize = target.coerceIn(CACHE_SIZE_MIN_BYTES, CACHE_SIZE_MAX_BYTES)
Log.d("VideoCache") {
"VideoCache size: ${cacheSize / (1024 * 1024)} MB " +
"(available: ${availableBytes / (1024 * 1024)} MB / " +
"total: ${(statFs?.totalBytes ?: 0L) / (1024 * 1024)} MB)"
}
return cacheSize
} }
// This method should be called when proxy setting changes. // This method should be called when proxy setting changes.
@@ -30,6 +30,7 @@ import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemCache import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemCache
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
import com.vitorpamplona.amethyst.service.playback.diskCache.isLiveStreaming import com.vitorpamplona.amethyst.service.playback.diskCache.isLiveStreaming
import com.vitorpamplona.quartz.utils.Log
/** /**
* True live streams (kind 30311) must not be cached. On-demand HLS * True live streams (kind 30311) must not be cached. On-demand HLS
@@ -66,10 +67,15 @@ class CustomMediaSourceFactory(
override fun getSupportedTypes(): IntArray = nonCachingFactory.supportedTypes override fun getSupportedTypes(): IntArray = nonCachingFactory.supportedTypes
override fun createMediaSource(mediaItem: MediaItem): MediaSource { override fun createMediaSource(mediaItem: MediaItem): MediaSource {
if (isLiveStream(mediaItem)) { val live = isLiveStream(mediaItem)
return nonCachingFactory.createMediaSource(mediaItem) Log.d("CustomMediaSourceFactory") {
"createMediaSource(${if (live) "BYPASS" else "CACHE"}): ${mediaItem.mediaId}"
}
return if (live) {
nonCachingFactory.createMediaSource(mediaItem)
} else {
cachingFactory.createMediaSource(mediaItem)
} }
return cachingFactory.createMediaSource(mediaItem)
} }
private fun isLiveStream(mediaItem: MediaItem): Boolean { private fun isLiveStream(mediaItem: MediaItem): Boolean {