fix(android): notification art uses album cover, not embedded stream tags
android / Build + lint + test (push) Successful in 3m38s
android / Build + lint + test (push) Successful in 3m38s
The MediaController notification / lock-screen background pulled artwork
from the stream's embedded ID3/FLAC tags (artworkData) because the
MediaItem never set artworkUri — a different source than the in-app
album cover (/api/albums/{id}/cover). For tracks whose embedded tag art
differs from the server album cover, the two surfaces disagreed.
- PlayerController.toMediaItem: set artworkUri to TrackRef.coverUrl.
MediaMetadata.populate() overwrites artworkUri+artworkData as a pair,
so the MediaItem URI clears the embedded bytes ExoPlayer extracts from
the stream — the album cover now wins on both surfaces.
- PlayerFactory.buildBitmapLoader: OkHttp-backed CacheBitmapLoader so the
authed placeholder cover URL resolves (the default DefaultHttpDataSource
loader can't rewrite placeholder.invalid or attach the auth cookie).
- MinstrelPlayerService: attach it via MediaSession.setBitmapLoader.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,7 @@ class MinstrelPlayerService : MediaSessionService() {
|
||||
val session = MediaSession.Builder(this, player)
|
||||
.setSessionActivity(buildNowPlayingPendingIntent())
|
||||
.setCallback(callback)
|
||||
.setBitmapLoader(playerFactory.buildBitmapLoader())
|
||||
.setMediaButtonPreferences(ImmutableList.of(buildLikeButton(isLiked = false)))
|
||||
.build()
|
||||
mediaSession = session
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
@@ -671,6 +672,18 @@ class PlayerController @Inject constructor(
|
||||
// paused under UPnP (it never probes a duration in that state).
|
||||
if (durationSec > 0) setDurationMs(durationSec.toLong() * MS_PER_SECOND)
|
||||
if (source != null) setExtras(sourceExtras(source))
|
||||
// Point the notification / lock-screen art at the SAME album
|
||||
// cover the in-app surfaces use (TrackRef.coverUrl ->
|
||||
// /api/albums/{id}/cover). Without this, Media3 falls back to
|
||||
// whatever art is embedded in the stream's tags, which can be a
|
||||
// different image than the server's album cover. Setting
|
||||
// artworkUri here is load-bearing: MediaMetadata.populate()
|
||||
// overwrites artworkUri + artworkData as a pair, so the
|
||||
// MediaItem's URI clears any embedded artworkData ExoPlayer
|
||||
// extracts from the stream -- the cover endpoint wins on both
|
||||
// surfaces. The session's OkHttp-backed BitmapLoader (see
|
||||
// PlayerFactory) is what makes this authed placeholder URL load.
|
||||
if (coverUrl.isNotEmpty()) setArtworkUri(coverUrl.toUri())
|
||||
}
|
||||
.build()
|
||||
// Server's stream_url is a relative path (/api/tracks/{id}/stream);
|
||||
|
||||
@@ -4,7 +4,9 @@ import android.content.Context
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.BitmapLoader
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import androidx.media3.datasource.DataSourceBitmapLoader
|
||||
import androidx.media3.datasource.cache.CacheDataSink
|
||||
import androidx.media3.datasource.cache.CacheDataSource
|
||||
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
||||
@@ -12,6 +14,7 @@ import androidx.media3.datasource.cache.SimpleCache
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.session.CacheBitmapLoader
|
||||
import com.fabledsword.minstrel.cache.audiocache.CacheConfig
|
||||
import com.fabledsword.minstrel.player.output.ActiveUpnpHolder
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
@@ -113,6 +116,24 @@ class PlayerFactory @Inject constructor(
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* BitmapLoader for the MediaSession's notification / lock-screen art.
|
||||
* Backed by the shared [okHttpClient] so it inherits the same
|
||||
* BaseUrlInterceptor placeholder rewrite + auth cookie that Coil uses
|
||||
* for in-app covers — without it the default DefaultHttpDataSource
|
||||
* loader can't resolve `http://placeholder.invalid/...` and would 401
|
||||
* on the cover endpoint. Wrapped in CacheBitmapLoader so a cover the
|
||||
* notification already fetched isn't re-loaded on every metadata
|
||||
* refresh. Lets the album-cover artworkUri set in
|
||||
* [PlayerController.toMediaItem] actually render on the media card.
|
||||
*/
|
||||
fun buildBitmapLoader(): BitmapLoader =
|
||||
CacheBitmapLoader(
|
||||
DataSourceBitmapLoader.Builder(context)
|
||||
.setDataSourceFactory(OkHttpDataSource.Factory(okHttpClient))
|
||||
.build(),
|
||||
)
|
||||
|
||||
private fun emitDrop(routeName: String) {
|
||||
dropEventsInternal.tryEmit(routeName)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user