diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/MinstrelPlayerService.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/MinstrelPlayerService.kt index 7458a730..59741113 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/MinstrelPlayerService.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/MinstrelPlayerService.kt @@ -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 diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt index 0305255b..e0f49216 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt @@ -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); diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerFactory.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerFactory.kt index 91176bb6..f3af0185 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerFactory.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerFactory.kt @@ -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) }