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:
+18
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-5
@@ -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.
|
||||||
|
|||||||
+9
-3
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user