diff --git a/CLAUDE.md b/CLAUDE.md index a5df825..bc3d7ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,11 @@ features, so keep layers clean and the shared models free of API details. routes, Compose shared-element transitions. - Network: Retrofit 3 + OkHttp 5 + kotlinx.serialization. Images: Coil 3 (disk cache). - Playback: Media3 1.11.1: ExoPlayer in a `MediaLibraryService`, `MediaController` in the UI. + Android has no ALAC (Apple Lossless .m4a) decoder: `org.jellyfin.media3:media3-ffmpeg-decoder` + (1.9.0+1, GPL-3.0, ~1.5 MB/ABI) is the fallback via `EXTENSION_RENDERER_MODE_ON` (Android's + decoders still first). Without it ALAC "plays" silently (position moves, no AudioTrack). Check + with `adb shell dumpsys media.audio_flinger` (an active track = sound). Bump it when + Jellyfin publishes a build for a newer Media3. - Storage: DataStore + Android Keystore (AES-GCM) for the encrypted login. - Colors from cover art: AndroidX Palette. - No DI framework: `AppContainer` wires everything by hand. @@ -161,6 +166,9 @@ adb -s emulator-5554 install -r app/build/outputs/apk/debug/app-debug.apk resent when the network comes back (needs ACCESS_NETWORK_STATE) or at next app start. Previews never report. Verified on the emulator against the owner's server: now playing, offline save with original time, sent 7 s after reconnecting, preview sent nothing. +13. ALAC fix (2026-10-13): ALAC songs (e.g. Eminem, Recovery) played silently: Android has no + ALAC decoder. Added the FFmpeg decoder fallback (see Tech). Verified on the emulator: "25 to + Life" now outputs 44.1 kHz audio via FFmpeg; FLAC still uses `c2.android.flac.decoder`. 12. Home tab (2026-10-13), now the start tab: rows Recently Played (`recent`), Recently Added (`newest`), Most Played (`frequent`), Favorites (getStarred2 albums + songs), Rediscover (`random`), each with See all (`AlbumListRoute` grid, `FavoritesRoute`). Rows load in diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d4c663e..d2b40ae 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { implementation(libs.media3.exoplayer) implementation(libs.media3.datasource.okhttp) implementation(libs.media3.session) + implementation(libs.media3.ffmpeg.decoder) implementation(libs.kotlinx.coroutines.guava) testImplementation(libs.junit) diff --git a/app/src/main/java/com/tysmi/syncwave/playback/PlaybackService.kt b/app/src/main/java/com/tysmi/syncwave/playback/PlaybackService.kt index f2d8e6c..276eb81 100644 --- a/app/src/main/java/com/tysmi/syncwave/playback/PlaybackService.kt +++ b/app/src/main/java/com/tysmi/syncwave/playback/PlaybackService.kt @@ -13,6 +13,7 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.cache.CacheDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.DefaultLoadControl +import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.analytics.AnalyticsListener import androidx.media3.exoplayer.source.DefaultMediaSourceFactory @@ -62,7 +63,12 @@ class PlaybackService : MediaLibraryService() { .setUpstreamDataSourceFactory(httpDataSource) .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) - val player = ExoPlayer.Builder(this) + // Android can't decode some formats our libraries use (Apple Lossless / ALAC .m4a): without + // a decoder the player "plays" silently. The FFmpeg decoders fill in, but only for formats + // Android itself can't handle (EXTENSION_RENDERER_MODE_ON keeps Android's decoders first). + val renderers = DefaultRenderersFactory(this) + .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) + val player = ExoPlayer.Builder(this, renderers) .setMediaSourceFactory(DefaultMediaSourceFactory(this).setDataSourceFactory(audioSource)) .setLoadControl( DefaultLoadControl.Builder() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7ab87b0..45a33df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,6 +50,8 @@ coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp" media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "media3" } media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" } +# FFmpeg audio decoders for Media3 (prebuilt by Jellyfin, GPL-3.0), for formats Android can't decode (ALAC). +media3-ffmpeg-decoder = { group = "org.jellyfin.media3", name = "media3-ffmpeg-decoder", version = "1.9.0+1" } kotlinx-coroutines-guava = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-guava", version.ref = "coroutines" } androidx-palette = { group = "androidx.palette", name = "palette-ktx", version.ref = "palette" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreen" }