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.media3.common.Player
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
@@ -96,13 +97,26 @@ private fun applyInitialQuality(
// 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.
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
return
}
val renditions =
(0 until videoGroup.length).joinToString(", ") {
val f = videoGroup.getTrackFormat(it)
"${f.width}x${f.height}"
}
when (policy) {
VideoQualityPolicy.AUTO -> {
if (hasVideoOverride(player)) clearVideoOverride(player)
Log.d("VideoQuality") {
"policy=AUTO mediaId=$mediaId cleared override (from ${videoGroup.length} renditions: [$renditions])"
}
appliedForMediaId.value = mediaId
}
@@ -111,6 +125,10 @@ private fun applyInitialQuality(
// retry on the next onTracksChanged when real video dimensions arrive.
val lowestIndex = findLowestResolutionTrackIndex(videoGroup) ?: return
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
}
}
@@ -28,6 +28,7 @@ import androidx.media3.datasource.DataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import com.vitorpamplona.quartz.utils.Log
import java.io.File
@SuppressLint("UnsafeOptInUsageError")
@@ -35,8 +36,10 @@ class VideoCache {
companion object {
// Target fraction of currently-available disk space.
private const val CACHE_SIZE_PERCENT = 0.20
// 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
// Floor so the cache is still useful on low-storage devices.
private const val CACHE_SIZE_MIN_BYTES = 256L * 1024 * 1024 // 256 MB
}
@@ -76,12 +79,16 @@ class VideoCache {
*/
private fun calculateCacheSize(cachePath: File): Long {
cachePath.mkdirs()
val availableBytes =
runCatching {
StatFs(cachePath.absolutePath).availableBytes
}.getOrDefault(0L)
val statFs = runCatching { StatFs(cachePath.absolutePath) }.getOrNull()
val availableBytes = statFs?.availableBytes ?: 0L
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.
@@ -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.diskCache.VideoCache
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
@@ -66,10 +67,15 @@ class CustomMediaSourceFactory(
override fun getSupportedTypes(): IntArray = nonCachingFactory.supportedTypes
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
if (isLiveStream(mediaItem)) {
return nonCachingFactory.createMediaSource(mediaItem)
val live = isLiveStream(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 {