Skip to content

Commit 788e51e

Browse files
committed
fix(downloads): resolve format mismatch errors and improve playback
- Use downloadUri directly for playback instead of ExoPlayer downloadCache to prevent format mismatch errors (MatroskaExtractor parsing M4A data) - Check targetItag before cache in DownloadUtil to respect user format selection - Clear playerCache when specific format requested to ensure fresh fetch - Fix URL cache expiry check (was using wrong comparison operator) - Reorder ExtractorsFactory: FragmentedMp4Extractor first for M4A files - Add detailed logging for download and cache resolution debugging
1 parent 675e668 commit 788e51e

File tree

2 files changed

+57
-16
lines changed

2 files changed

+57
-16
lines changed

app/src/main/kotlin/com/metrolist/music/playback/DownloadUtil.kt

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,40 @@ constructor(
129129
val mediaId = dataSpec.key ?: error("No media id")
130130
val length = if (dataSpec.length >= 0) dataSpec.length else 1
131131

132-
if (playerCache.isCached(mediaId, dataSpec.position, length)) {
133-
return@Factory dataSpec
134-
}
132+
// Check if user selected a specific format for this download
133+
// IMPORTANT: Check targetItag FIRST - if set, we must bypass cache to get that specific format
134+
val targetItag = targetItagOverride[mediaId] ?: 0
135+
val hasTargetItag = targetItag > 0
136+
timber.log.Timber.tag("DownloadUtil").d("Download resolver for $mediaId, targetItag=${if (hasTargetItag) targetItag else "auto"}")
135137

136-
songUrlCache[mediaId]?.takeIf { it.second < System.currentTimeMillis() }?.let {
137-
return@Factory dataSpec.withUri(it.first.toUri())
138+
// Only use cache if no specific format was requested
139+
if (!hasTargetItag) {
140+
if (playerCache.isCached(mediaId, dataSpec.position, length)) {
141+
timber.log.Timber.tag("DownloadUtil").d("Using player cache for $mediaId")
142+
return@Factory dataSpec
143+
}
144+
145+
// Fixed: use > for "not expired" (was < which meant "use if expired")
146+
songUrlCache[mediaId]?.takeIf { it.second > System.currentTimeMillis() }?.let {
147+
timber.log.Timber.tag("DownloadUtil").d("Using URL cache for $mediaId")
148+
return@Factory dataSpec.withUri(it.first.toUri())
149+
}
150+
} else {
151+
// Clear caches when specific format requested to ensure fresh fetch
152+
timber.log.Timber.tag("DownloadUtil").d("Bypassing cache for $mediaId - specific format requested (itag=$targetItag)")
153+
songUrlCache.remove(mediaId)
154+
// Also clear playerCache for this media to prevent format mismatch
155+
runBlocking(Dispatchers.IO) {
156+
try {
157+
if (playerCache.getCachedSpans(mediaId).isNotEmpty()) {
158+
playerCache.removeResource(mediaId)
159+
timber.log.Timber.tag("DownloadUtil").d("Cleared player cache for $mediaId")
160+
}
161+
} catch (_: Exception) {}
162+
}
138163
}
139164

140-
// Check if user selected a specific format for this download
141-
val targetItag = targetItagOverride[mediaId] ?: 0
142-
timber.log.Timber.tag("DownloadUtil").d("Fetching stream for $mediaId, targetItag=${if (targetItag > 0) targetItag else "auto"}")
165+
timber.log.Timber.tag("DownloadUtil").d("Fetching fresh stream for $mediaId, targetItag=${if (hasTargetItag) targetItag else "auto"}")
143166

144167
val playbackData = runBlocking(Dispatchers.IO) {
145168
YTPlayerUtils.playerResponseForPlayback(
@@ -150,6 +173,9 @@ constructor(
150173
)
151174
}.getOrThrow()
152175
val format = playbackData.format
176+
timber.log.Timber.tag("DownloadUtil").i(
177+
"Download stream for $mediaId: itag=${format.itag}, mimeType=${format.mimeType.split(";")[0]}, bitrate=${format.bitrate/1000}kbps"
178+
)
153179

154180
database.query {
155181
upsert(

app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2627,19 +2627,32 @@ class MusicService :
26272627
// Check if we need to bypass cache for quality change
26282628
val shouldBypassCache = bypassCacheForQualityChange.contains(mediaId)
26292629

2630+
// ZEMER APPROACH: Check for downloaded file URI first (local playback)
2631+
// Only use local file if starting from beginning (position 0) to avoid
2632+
// switching sources mid-stream when a download completes during playback
26302633
if (!shouldBypassCache) {
2631-
if (downloadCache.isCached(
2632-
mediaId,
2633-
dataSpec.position,
2634-
if (dataSpec.length >= 0) dataSpec.length else 1
2635-
) ||
2636-
playerCache.isCached(mediaId, dataSpec.position, CHUNK_LENGTH)
2637-
) {
2634+
val song = runBlocking(Dispatchers.IO) {
2635+
database.song(mediaId).first()
2636+
}
2637+
2638+
// Use downloaded file directly if available (bypasses ExoPlayer cache entirely)
2639+
if (song?.song?.downloadUri != null && dataSpec.position == 0L) {
2640+
Timber.tag("CacheResolver").d("Using downloaded file for $mediaId: ${song.song.downloadUri}")
2641+
scope.launch(Dispatchers.IO) { recoverSong(mediaId) }
2642+
return@Factory dataSpec.withUri(song.song.downloadUri.toUri())
2643+
}
2644+
2645+
// Check player cache (streaming buffer) - NOT download cache
2646+
// Download cache can have format mismatches, use downloadUri instead
2647+
if (playerCache.isCached(mediaId, dataSpec.position, CHUNK_LENGTH)) {
2648+
Timber.tag("CacheResolver").d("Using player cache for $mediaId at pos=${dataSpec.position}")
26382649
scope.launch(Dispatchers.IO) { recoverSong(mediaId) }
26392650
return@Factory dataSpec
26402651
}
26412652

2653+
// Check URL cache
26422654
songUrlCache[mediaId]?.takeIf { it.second > System.currentTimeMillis() }?.let {
2655+
Timber.tag("CacheResolver").d("Using URL cache for $mediaId")
26432656
scope.launch(Dispatchers.IO) { recoverSong(mediaId) }
26442657
return@Factory dataSpec.withUri(it.first.toUri())
26452658
}
@@ -2731,7 +2744,9 @@ class MusicService :
27312744
DefaultMediaSourceFactory(
27322745
createDataSourceFactory(),
27332746
ExtractorsFactory {
2734-
arrayOf(MatroskaExtractor(), FragmentedMp4Extractor())
2747+
// FragmentedMp4Extractor first for M4A downloads, MatroskaExtractor for WebM/OPUS streams
2748+
// Order matters: ExoPlayer tries extractors in order until sniff() succeeds
2749+
arrayOf(FragmentedMp4Extractor(), MatroskaExtractor())
27352750
},
27362751
)
27372752

0 commit comments

Comments
 (0)