androidx / media

Jetpack Media3 support libraries for media use cases, including ExoPlayer, an extensible media player for Android
https://developer.android.com/media/media3
Apache License 2.0
1.67k stars 399 forks source link

Proper way to convert MediaItem to MediaQueueItem #264

Open NielsMasdorp opened 1 year ago

NielsMasdorp commented 1 year ago

Hi,

Apologies in advance if this is too vague of a question, but I am having some issues in properly converting from MediaItem to MediaQueueItem. My use case is quite simple (I think) I have a simple MediaItem which streams a live radio station. I create the MediaItem like so:

fun Stream.toMediaItem(): MediaItem {
    return MediaItem.Builder()
        .setMediaId(id)
        .setRequestMetadata(
            MediaItem.RequestMetadata.Builder()
                .setMediaUri(url.toUri())
                .build()
        )
        .setMediaMetadata(
            MediaMetadata.Builder()
                .setSubtitle(station)
                .setArtist(station)
                .setArtworkUri(imageUrl.toUri())
                .build()
        )
        .build()
}

So the artist is set to the radio station name. Now when using an local ExoPlayer the MediaMetadata.title is getting dynamically set internally with the current song playing in the stream. This song is then shown in the app through Player.Listener.onMediaMetadataChanged(mediaMetadata: MediaMetadata), The song name is also correctly shown in the media notifications on my device. This all works fine.

Now I also have the option of switching to CastPlayer via pretty much the same implementation of the ReplacableForwardingPlayer in uamp. But I am not sure what the simplest implementation of a MediaItemConverter to properly convert to the MediaQueueItem. I was hoping I could just use the DefaultMediaItemConverter but alas. I see uamp uses the default one from converting to MediaQueueItem to MediaItem, yet uses a custom implementation the other way around. I tried using the default one too, but it requires mediaInfo.getCustomData() to be set, which i don't use. But the uamp/media3 branch hasn't been updated for a long time, so I don't know if the new media3 beta release changes anything for that app.

This is my current implementation. Since CastPlayer does not internally handles dynamic track information from the stream I use a static title instead of the song name.

class StreamMediaItemConverter(private val staticTitle: String) : MediaItemConverter {

    override fun toMediaItem(mediaQueueItem: MediaQueueItem): MediaItem {
        val mediaInfo = mediaQueueItem.media!!
        val metadata = mediaInfo.metadata!!
        val contentUrl = mediaInfo.contentUrl!!
        val imageUrl = metadata.images.first().url
        val title = metadata.getString(CastMetadata.KEY_TITLE)!!
        val mediaId = metadata.getString(KEY_MEDIA_ID)!!

        return MediaItem.Builder()
            .setMediaId(mediaId)
            .setRequestMetadata(
                MediaItem.RequestMetadata.Builder()
                    .setMediaUri(contentUrl.toUri())
                    .build()
            )
            .setMediaMetadata(
                MediaMetadata.Builder()
                    .setArtist(title)
                    .setSubtitle(title)
                    .setMediaType(MediaMetadata.MEDIA_TYPE_RADIO_STATION)
                    .setArtworkUri(imageUrl)
                    .build()
            )
            .build()

    }

    override fun toMediaQueueItem(mediaItem: MediaItem): MediaQueueItem {
        // CastMetadata is a typealias for com.google.android.gms.cast.MediaMetadata
        val metadata = CastMetadata(MEDIA_TYPE_GENERIC).apply {
            putString(KEY_MEDIA_ID, mediaItem.mediaId)
            putString(CastMetadata.KEY_ARTIST, mediaItem.mediaMetadata.artist.toString())
            putString(CastMetadata.KEY_TITLE, staticTitle)
            addImage(WebImage(mediaItem.mediaMetadata.artworkUri!!))
        }
        val mediaInfo = MediaInfo.Builder(mediaItem.mediaId)
            .setContentUrl(mediaItem.localConfiguration?.uri.toString())
            .setMetadata(metadata)
            .build()
        return MediaQueueItem.Builder(mediaInfo).build()
    }

    companion object {
        private const val KEY_MEDIA_ID = "mediaId"
    }

There are a couple of issues though, the first one being MediaQueueItem.Builder shows a warning that it should only be used in tests, which is concerning and secondly: I cant seem to show both the radio station name (which is set in mediaItem.mediaMetadata.artist and the static title in my media notification which is shown when switching from ExoPlayer to CastPlayer. I only see the CastMetadata.KEY_ARTIST. Both strings are shown when casting to my TV and in the Google Home app as well, so I am confused why it doesn't show in my notification. I have tried a lot of combinations of values but they do not seem to work. Keep in mind I am using CastMediaOptions.Builder()..setMediaSessionEnabled(false).setNotificationOptions(null) like uamp does in order to not have 2 separate notifications when casting. So I guess this issue is kind of related to this old issue made by yours truly. However I am not changing the player instance in the mediaSession anymore, but instead using a ReplacableForwardingPlayer like uamp. Honestly the process of having a uniform notification experience for local and cast playback is just confusing to me as an developer.

A small aside: it took me a while to figure out that the MediaInfo.Builder(String contentId) is needed when using CastPlayer.setMediaItems() with more then 1 item in the playlist. I was using the empty MediaInfo.Builder() constructor, but since the CastTimelineTracker uses the contentId as keys that led to some weird behaviour. But you can probably don't touch the public API anymore after RC status..

What am I missing here? I want the simplest conversion possible when and I only need to show artist, title and artwork.

Thank you for reading.

NielsMasdorp commented 1 year ago

What effect I want to achieve while casting:

1

This is achieved by

CastMediaOptions.Builder()
    .setMediaSessionEnabled(true)
    .setNotificationOptions(options)

But results in two separate notifications, one created by the Cast SDK and one still in use by my own MediaSession. Both notifications are in a working state. But I don't want two notifications, I want one notification. This, I can achieve with:

CastMediaOptions.Builder()
    .setMediaSessionEnabled(false)
    .setNotificationOptions(null)

Which results in keeping the same notification when switching from ExoPlayer to CastPlayer in my ReplacableForwardingPlayer, the notification is functional but as I said in my question above, I cant find a way to set the title of the notification, so what I actually end up with is:

2

PS: I have no idea why the device name in the upper right corner is shown as null, I see it with other apps as well, but not with Youtube Music for example.

NielsMasdorp commented 1 year ago

@marcbaechinger Is there a way to disable the notification for the MediaSession in my service while a current casting session is in use and the Cast SDK has it's own MediaSession and notification?