Skip to content

Commit 978737a

Browse files
authored
fix(player): reload stream when audio quality setting changes
- Watch for audio quality preference changes in real-time - Reload current song at same position when quality changes - Clear URL cache and player/download cache to force fresh fetch - Add bypass cache flag to skip cache checks during quality switch - Prevents playback errors from cached data with different format
1 parent 8b89ba2 commit 978737a

File tree

1 file changed

+86
-13
lines changed

1 file changed

+86
-13
lines changed

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

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,9 @@ class MusicService :
358358
// URL cache for stream URLs - class-level so it can be invalidated on errors
359359
private val songUrlCache = HashMap<String, Pair<String, Long>>()
360360

361+
// Flag to bypass cache when quality changes - forces fresh stream fetch
362+
private val bypassCacheForQualityChange = mutableSetOf<String>()
363+
361364
// Enhanced error tracking for strict retry management
362365
private var currentMediaIdRetryCount = mutableMapOf<String, Int>()
363366
private val MAX_RETRY_PER_SONG = 3
@@ -538,6 +541,63 @@ class MusicService :
538541
}
539542
}
540543

544+
// Watch for audio quality setting changes
545+
var isFirstQualityEmit = true
546+
scope.launch {
547+
dataStore.data
548+
.map { it[AudioQualityKey]?.let { value ->
549+
com.metrolist.music.constants.AudioQuality.entries.find { it.name == value }
550+
} ?: com.metrolist.music.constants.AudioQuality.AUTO }
551+
.distinctUntilChanged()
552+
.collect { newQuality ->
553+
val oldQuality = audioQuality
554+
audioQuality = newQuality
555+
556+
// Skip reload on first emit (app startup)
557+
if (isFirstQualityEmit) {
558+
isFirstQualityEmit = false
559+
Timber.tag("MusicService").i("QUALITY INIT: $newQuality")
560+
return@collect
561+
}
562+
563+
Timber.tag("MusicService").i("QUALITY CHANGED: $oldQuality -> $newQuality")
564+
565+
// Reload current song with new quality
566+
val mediaId = player.currentMediaItem?.mediaId ?: return@collect
567+
val currentPosition = player.currentPosition
568+
val wasPlaying = player.isPlaying
569+
val currentIndex = player.currentMediaItemIndex
570+
571+
Timber.tag("MusicService").i("RELOADING STREAM: $mediaId at position ${currentPosition}ms")
572+
573+
// Clear cached URL to force fresh fetch
574+
songUrlCache.remove(mediaId)
575+
576+
// CRITICAL: Clear caches synchronously to prevent format parsing errors
577+
runBlocking(Dispatchers.IO) {
578+
try {
579+
playerCache.removeResource(mediaId)
580+
downloadCache.removeResource(mediaId)
581+
Timber.tag("MusicService").d("Cleared player and download cache for $mediaId")
582+
} catch (e: Exception) {
583+
Timber.tag("MusicService").e(e, "Failed to clear cache for $mediaId")
584+
}
585+
}
586+
587+
// Set bypass flag so resolver skips cache checks
588+
bypassCacheForQualityChange.add(mediaId)
589+
Timber.tag("MusicService").d("Set bypass cache flag for $mediaId")
590+
591+
// Reload player at same position
592+
player.stop()
593+
player.seekTo(currentIndex, currentPosition)
594+
player.prepare()
595+
if (wasPlaying) {
596+
player.play()
597+
}
598+
}
599+
}
600+
541601
combine(playerVolume, isMuted) { volume, muted ->
542602
if (muted) 0f else volume
543603
}.collectLatest(scope) {
@@ -2564,22 +2624,30 @@ class MusicService :
25642624
return ResolvingDataSource.Factory(createCacheDataSource()) { dataSpec ->
25652625
val mediaId = dataSpec.key ?: error("No media id")
25662626

2567-
if (downloadCache.isCached(
2568-
mediaId,
2569-
dataSpec.position,
2570-
if (dataSpec.length >= 0) dataSpec.length else 1
2571-
) ||
2572-
playerCache.isCached(mediaId, dataSpec.position, CHUNK_LENGTH)
2573-
) {
2574-
scope.launch(Dispatchers.IO) { recoverSong(mediaId) }
2575-
return@Factory dataSpec
2576-
}
2627+
// Check if we need to bypass cache for quality change
2628+
val shouldBypassCache = bypassCacheForQualityChange.contains(mediaId)
25772629

2578-
songUrlCache[mediaId]?.takeIf { it.second > System.currentTimeMillis() }?.let {
2579-
scope.launch(Dispatchers.IO) { recoverSong(mediaId) }
2580-
return@Factory dataSpec.withUri(it.first.toUri())
2630+
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+
) {
2638+
scope.launch(Dispatchers.IO) { recoverSong(mediaId) }
2639+
return@Factory dataSpec
2640+
}
2641+
2642+
songUrlCache[mediaId]?.takeIf { it.second > System.currentTimeMillis() }?.let {
2643+
scope.launch(Dispatchers.IO) { recoverSong(mediaId) }
2644+
return@Factory dataSpec.withUri(it.first.toUri())
2645+
}
2646+
} else {
2647+
Timber.tag("MusicService").i("BYPASSING CACHE for $mediaId due to quality change")
25812648
}
25822649

2650+
Timber.tag("MusicService").i("FETCHING STREAM: $mediaId | quality=$audioQuality")
25832651
val playbackData = runBlocking(Dispatchers.IO) {
25842652
YTPlayerUtils.playerResponseForPlayback(
25852653
mediaId,
@@ -2645,6 +2713,11 @@ class MusicService :
26452713
}
26462714
scope.launch(Dispatchers.IO) { recoverSong(mediaId, nonNullPlayback) }
26472715

2716+
// Clear bypass flag now that we've fetched fresh stream
2717+
if (bypassCacheForQualityChange.remove(mediaId)) {
2718+
Timber.tag("MusicService").d("Cleared bypass cache flag for $mediaId after fresh fetch")
2719+
}
2720+
26482721
val streamUrl = nonNullPlayback.streamUrl
26492722

26502723
songUrlCache[mediaId] =

0 commit comments

Comments
 (0)