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:
parent
3ae7bcb3dd
commit
ca5ebd5b03
4 changed files with 18 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
Loading…
Reference in a new issue