diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/InitialVideoQualitySelector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/InitialVideoQualitySelector.kt index 0b5f200caa..a99a04218e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/InitialVideoQualitySelector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/InitialVideoQualitySelector.kt @@ -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 } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt index c4f342d510..170a489127 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt @@ -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. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/CustomMediaSourceFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/CustomMediaSourceFactory.kt index 20cc9516df..43212d6578 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/CustomMediaSourceFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/CustomMediaSourceFactory.kt @@ -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 {