Play ALAC songs: FFmpeg audio decoder fallback

Android has no Apple Lossless (ALAC) decoder, so ALAC .m4a files (for
example Eminem's Recovery) "played" with the seek bar moving but no
sound: the player had no decoder for the audio track.

Adds Jellyfin's prebuilt Media3 FFmpeg decoder (GPL-3.0) and turns on
extension renderers in fallback mode, so Android's own decoders are still
used for FLAC, MP3, AAC and so on, and FFmpeg only steps in for formats
Android can't decode.

Checked on the emulator: "25 to Life" now produces a 44.1 kHz audio
track (FFmpeg), FLAC still uses c2.android.flac.decoder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
TySmi 2026-10-13 19:37:51 -04:00
parent 3ae7bcb3dd
commit ca5ebd5b03
4 changed files with 18 additions and 1 deletions

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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" }