Skip to content

Commit 68388b8

Browse files
committed
Bump version; lock quality & handle fallbacks
Bump app version to 5.1.91 (versionCode 511) and update release notes. Improve playback robustness by adding detection for runtime/check and container-corruption errors, retrying/refreshing URLs, and clearing player cache when cache vs stream formats mismatch. Lock stream quality for the currently playing song (based on DB format) to prevent mid-stream container changes and force a player restart if a format fallback occurs during playback. Fix audio-fallback toast behavior by scoping toast flags per-resolution attempt so notifications can show appropriately. Update JioSaavn service base URL to saavn.sumit.co.
1 parent 1dd0149 commit 68388b8

5 files changed

Lines changed: 74 additions & 45 deletions

File tree

RELEASE_INFO.md

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,6 @@
1-
# Echo Music v5.1.9
1+
# Echo Music v5.1.91
22

3-
### New Features
4-
- **Pin Playlists** — Long-press to pin playlists to the top of your library.
5-
- **Sync to YouTube Music** — Sync local playlists via the playlist menu.
6-
- **Settings Search** — Filter settings categories and sub-settings instantly.
7-
- **Song Suggestions** — Discover and add related songs at the bottom of local playlists.
8-
- **Canvas Provider (Tidal)** — Animated album canvas with improved matching accuracy.
9-
- **Audio Fallback** — Opus failure auto-reroutes to JioSaavn 320 kbps, and vice versa.
10-
- **Markdown in Changelog** — Bold, italics, inline code, and links rendered natively.
11-
12-
### UI & Design
13-
- **Echo Find Screen** — Material You pill button, animated wave visualizer, edge-to-edge blur.
14-
- **Recognition Screen** — Glassmorphism layout with full-square album art and playback FAB.
15-
- **Suggestions & Menus** — Rounded edges on suggestions list; removed grey header backgrounds from bottom sheets.
16-
- **Online Playlist Header** — Count and duration shown above action buttons.
17-
- **Logout Dialog** — Stacked buttons fix text clipping.
18-
19-
### Bug Fixes
20-
- **Crash on Recognition** — Fixed `IllegalStateException` via main thread dispatch.
21-
- **Offline Playback** — Fixed downloaded songs failing due to bad cache length reads.
22-
- **Apple Music Black Screen** — Album art now shows for downloaded songs.
23-
- **Volume Slider** — Stays in sync with system volume.
24-
- **Background Updater** — Runs as background service with auto-retry; fixed notification spam.
25-
- **Comments Button** — Removed from Listen Together screen.
26-
27-
### Build
28-
- **FFmpeg** — Switched to `ffmpeg-kit-audio`; removed unused `aria2c` dependency.
29-
- **ABI Filters** — Fixed per-arch filters to prevent universal APK generation.
3+
- Fixed specific playback error codes: 3001, 2008, 1004 by retrying the network or AudioTrack securely.
4+
- Fixed playback going soundless by locking the stream quality for the currently playing song to prevent mid-stream container changes.
5+
- Fixed audio fallback toast notifications only showing once per app session.
6+
- Fixed ExoPlayer crashing when stream container format falls back to a different format than the cache.

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ android {
3333
applicationId = "iad1tya.echo.music"
3434
minSdk = 26
3535
targetSdk = 36
36-
versionCode = 510
37-
versionName = "5.1.9"
36+
versionCode = 511
37+
versionName = "5.1.91"
3838

3939
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
4040
vectorDrawables.useSupportLibrary = true

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

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2098,7 +2098,13 @@ class MusicService :
20982098
return error.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED ||
20992099
error.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED ||
21002100
(error.cause as? PlaybackException)?.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED ||
2101-
(error.cause as? PlaybackException)?.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED
2101+
(error.cause as? PlaybackException)?.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED ||
2102+
error.errorCode == PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK
2103+
}
2104+
2105+
private fun isCacheOrStreamCorruptionError(error: PlaybackException): Boolean {
2106+
return error.errorCode == PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED ||
2107+
error.errorCode == PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE
21022108
}
21032109

21042110
override fun onPlayerError(error: PlaybackException) {
@@ -2139,6 +2145,11 @@ class MusicService :
21392145
handleRangeNotSatisfiableError(mediaId)
21402146
return
21412147
}
2148+
isCacheOrStreamCorruptionError(error) -> {
2149+
Timber.tag(TAG).d("Cache or stream corruption detected, clearing cache and refreshing URL")
2150+
handleExpiredUrlError(mediaId)
2151+
return
2152+
}
21422153
isPageReloadError(error) -> {
21432154
Timber.tag(TAG).d("Page reload error detected, performing strict recovery")
21442155
handlePageReloadError(mediaId)
@@ -2522,16 +2533,33 @@ class MusicService :
25222533
val cachedLength = androidx.media3.datasource.cache.ContentMetadata.getContentLength(downloadCache.getContentMetadata(mediaId))
25232534
val isFullyDownloaded = cachedLength != androidx.media3.common.C.LENGTH_UNSET.toLong() && cachedLength > 0 && downloadCache.isCached(mediaId, 0, cachedLength)
25242535

2525-
if (!shouldBypassCache && !isFullyDownloaded && audioQuality == iad1tya.echo.music.constants.AudioQuality.LOSSLESS) {
2526-
val format = runBlocking(Dispatchers.IO) { database.format(mediaId).firstOrNull() }
2527-
if (format?.codecs != "flac") {
2528-
shouldBypassCache = true
2536+
val isCurrentlyPlaying = runBlocking(Dispatchers.Main) { player.currentMediaItem?.mediaId == mediaId }
2537+
val dbFormat = runBlocking(Dispatchers.IO) { database.format(mediaId).firstOrNull() }
2538+
2539+
val lockedQuality = if (isCurrentlyPlaying && dbFormat != null) {
2540+
when {
2541+
dbFormat.mimeType.contains("flac", ignoreCase = true) -> iad1tya.echo.music.constants.AudioQuality.LOSSLESS
2542+
dbFormat.mimeType.contains("mp4", ignoreCase = true) || dbFormat.mimeType.contains("m4a", ignoreCase = true) -> iad1tya.echo.music.constants.AudioQuality.SAAVN
2543+
else -> iad1tya.echo.music.constants.AudioQuality.OPUS
25292544
}
2545+
} else {
2546+
audioQuality
25302547
}
2531-
if (!shouldBypassCache && !isFullyDownloaded && audioQuality == iad1tya.echo.music.constants.AudioQuality.SAAVN) {
2532-
val format = runBlocking(Dispatchers.IO) { database.format(mediaId).firstOrNull() }
2533-
if (format?.codecs != "mp4a.40.2") {
2548+
2549+
if (!shouldBypassCache && !isFullyDownloaded && dbFormat != null) {
2550+
val isLosslessCache = dbFormat.codecs == "flac"
2551+
val isSaavnCache = dbFormat.codecs == "mp4a.40.2" || dbFormat.mimeType.contains("mp4", ignoreCase = true)
2552+
2553+
val cacheMatchesTarget = when (lockedQuality) {
2554+
iad1tya.echo.music.constants.AudioQuality.LOSSLESS -> isLosslessCache
2555+
iad1tya.echo.music.constants.AudioQuality.SAAVN -> isSaavnCache
2556+
iad1tya.echo.music.constants.AudioQuality.OPUS -> !isLosslessCache && !isSaavnCache
2557+
}
2558+
2559+
if (!cacheMatchesTarget) {
25342560
shouldBypassCache = true
2561+
Timber.tag(TAG).i("Quality changed to $lockedQuality for $mediaId. Clearing playerCache to prevent container mismatch.")
2562+
playerCache.removeResource(mediaId)
25352563
}
25362564
}
25372565

@@ -2575,7 +2603,7 @@ class MusicService :
25752603
Timber.tag("MusicService").i("BYPASSING CACHE for $mediaId due to quality change")
25762604
}
25772605

2578-
Timber.tag("MusicService").i("FETCHING STREAM: $mediaId | quality=$audioQuality")
2606+
Timber.tag("MusicService").i("FETCHING STREAM: $mediaId | quality=$lockedQuality")
25792607
val playbackData = runBlocking(Dispatchers.IO) {
25802608
val dbSong = database.song(mediaId).firstOrNull()
25812609
val knownArtist = dbSong?.artists?.joinToString { it.name }?.replace(" - Topic", "")
@@ -2584,7 +2612,7 @@ class MusicService :
25842612

25852613
YTPlayerUtils.playerResponseForPlayback(
25862614
mediaId,
2587-
audioQuality = audioQuality,
2615+
audioQuality = lockedQuality,
25882616
connectivityManager = connectivityManager,
25892617
context = this@MusicService,
25902618
knownArtist = knownArtist,
@@ -2624,6 +2652,29 @@ class MusicService :
26242652
}
26252653
run {
26262654
val format = nonNullPlayback.format
2655+
2656+
val isFinalLossless = format.mimeType.contains("flac", ignoreCase = true)
2657+
val isFinalSaavn = format.mimeType.contains("mp4", ignoreCase = true) || format.mimeType.contains("m4a", ignoreCase = true)
2658+
2659+
if (dbFormat != null && !shouldBypassCache) {
2660+
val cacheIsLossless = dbFormat.codecs == "flac"
2661+
val cacheIsSaavn = dbFormat.codecs == "mp4a.40.2" || dbFormat.mimeType.contains("mp4", ignoreCase = true)
2662+
2663+
if (isFinalLossless != cacheIsLossless || isFinalSaavn != cacheIsSaavn) {
2664+
Timber.tag(TAG).w("Format fallback detected AFTER fetch. Clearing playerCache to prevent mismatch crash.")
2665+
playerCache.removeResource(mediaId)
2666+
2667+
if (isCurrentlyPlaying) {
2668+
Timber.tag(TAG).e("Format changed mid-stream for $mediaId. Throwing to force player restart.")
2669+
throw PlaybackException(
2670+
"Container format changed mid-stream due to fallback",
2671+
null,
2672+
PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED
2673+
)
2674+
}
2675+
}
2676+
}
2677+
26272678
val loudnessDb = nonNullPlayback.audioConfig?.loudnessDb
26282679
val perceptualLoudnessDb = nonNullPlayback.audioConfig?.perceptualLoudnessDb
26292680

app/src/main/kotlin/com/music/echo/utils/YTPlayerUtils.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,6 @@ import kotlinx.coroutines.flow.first
4949
object YTPlayerUtils {
5050
private const val logTag = "YTPlayerUtils"
5151
private const val TAG = "YTPlayerUtils"
52-
private var hasShownLosslessToast = false
53-
private var hasShownSaavnToast = false
54-
private var hasShownOpusToast = false
5552

5653
private val httpClient: OkHttpClient = OkHttpClient.Builder()
5754
.dns(object : Dns {
@@ -127,6 +124,10 @@ object YTPlayerUtils {
127124
it.dataStore.data.first()[iad1tya.echo.music.constants.ShowAudioFallbackToastKey]
128125
} ?: true
129126

127+
var hasShownLosslessToast = false
128+
var hasShownSaavnToast = false
129+
var hasShownOpusToast = false
130+
130131
suspend fun tryOpus(): Result<PlaybackData> {
131132
val firstAttempt = resolvePlaybackData(videoId, playlistId, audioQuality, connectivityManager)
132133
if (firstAttempt.isFailure && YouTube.cookie == null) {

jiosaavn/src/main/kotlin/com/music/jiosaavn/SaavnService.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under GPL-3.0 | See git history for contributors
44
*
55
* JioSaavn audio streaming service.
6-
* Uses the Melo API (meloapi.vercel.app) which is an open wrapper around JioSaavn.
6+
* Uses the Melo API (saavn.sumit.co) which is an open wrapper around JioSaavn.
77
*
88
* API endpoints used:
99
* - GET /api/search/songs?query={q} → search songs by name+artist
@@ -91,7 +91,7 @@ data class SaavnSongResponse(
9191

9292
object SaavnService {
9393

94-
private const val BASE_URL = "https://meloapi.vercel.app/api/"
94+
private const val BASE_URL = "https://saavn.sumit.co/api/"
9595

9696
private val json = Json {
9797
isLenient = true

0 commit comments

Comments
 (0)