Search code examples
androidandroid-mediaplayerandroid-mediasession

Adding MediaItem when using the media3 library caused an error


I am using the latest Android Media3 library, but I found a problem in using it...

I created a MediaSessionService, and then got the MediaController in the Activity, and then when I tried to call the media controller and add some MediaItems, an error occurred:

 java.lang.NullPointerException
        at androidx.media3.common.util.Assertions.checkNotNull(Assertions.java:155)
        at androidx.media3.exoplayer.source.DefaultMediaSourceFactory.createMediaSource(DefaultMediaSourceFactory.java:338)
        at androidx.media3.exoplayer.ExoPlayerImpl.createMediaSources(ExoPlayerImpl.java:1164)
        at androidx.media3.exoplayer.ExoPlayerImpl.addMediaItems(ExoPlayerImpl.java:463)
        at androidx.media3.exoplayer.SimpleExoPlayer.addMediaItems(SimpleExoPlayer.java:1146)
        at androidx.media3.common.BasePlayer.addMediaItems(BasePlayer.java:69)
        at androidx.media3.common.BasePlayer.addMediaItem(BasePlayer.java:64)
        at androidx.media3.common.ForwardingPlayer.addMediaItem(ForwardingPlayer.java:90)
        at androidx.media3.session.PlayerWrapper.addMediaItem(PlayerWrapper.java:346)
        at androidx.media3.session.MediaSessionStub.lambda$addMediaItem$28(MediaSessionStub.java:1052)
        at androidx.media3.session.MediaSessionStub$$ExternalSyntheticLambda8.run(Unknown Source:2)
        at androidx.media3.session.MediaSessionStub.lambda$getSessionTaskWithPlayerCommandRunnable$2$androidx-media3-session-MediaSessionStub(MediaSessionStub.java:234)
        at androidx.media3.session.MediaSessionStub$$ExternalSyntheticLambda52.run(Unknown Source:14)
        at androidx.media3.session.MediaSessionStub.lambda$flushCommandQueue$50(MediaSessionStub.java:1479)
        at androidx.media3.session.MediaSessionStub$$ExternalSyntheticLambda58.run(Unknown Source:2)
        at androidx.media3.common.util.Util.postOrRun(Util.java:517)
        at androidx.media3.session.MediaSessionStub.flushCommandQueue(MediaSessionStub.java:1473)
        at androidx.media3.session.MediaControllerImplBase$FlushCommandQueueHandler.handleMessage(MediaControllerImplBase.java:3035)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7813)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

So I checked the createMediaSource function of DefaultMediaSourceFactory and found that it is checking whether the localConfiguration of MediaItem is null:

  @Override
  public MediaSource createMediaSource(MediaItem mediaItem) {
    checkNotNull(mediaItem.localConfiguration);
    ...
  }

And this is localConfiguration:

/**
   * Optional configuration for local playback. May be {@code null} if shared over process
   * boundaries.
   */
  @Nullable public final LocalConfiguration localConfiguration;

I am pretty sure that there is no problem with the way I created the MediaItem, and it works well inside the Service, but when I try to insert the MediaItem in the Activity, an error occurs. According to the comments, I guess this may be a cross-process communication problem, but I don't have any clue about this. Does anyone have experience with Media3?


Solution

  • When you add/set MediaItems from a controller, the localConfiguration (uri, mimeType, drm config, etc) of MediaItem is removed for security/privacy reasons. Without localConfiguration the player can't play the media item. We need to add the missing information back to the MediaItem.

    Updated answer (media3 1.0.0-beta01 or higher)

    Open the Callback you defined when creating the MediaLibrarySession in your Service.

    // My MediaLibraryService
    // onCreate()
    mediaLibrarySession = MediaLibrarySession.Builder(
        this, 
        player, 
        librarySessionCallback // <--
    ).build()
    
    
    // NOTE: If you are using MediaSessionService instead of MediaLibraryService,
    // use `setCallback(librarySessionCallback)` from the MediaSession.Builder.
    
    

    Override onAddMediaItems inside your MediaLibrarySession.Callback. Every time you use setMediaItem/addMediaItem from a controller, your onAddMediaItems will be called and the MediaItems returned there are the ones that will be played.

    class CustomMediaLibrarySessionCallback : MediaLibraryService.MediaLibrarySession.Callback {
    
        // [...]
    
        override fun onAddMediaItems(
            mediaSession: MediaSession,
            controller: MediaSession.ControllerInfo,
            mediaItems: MutableList<MediaItem>
        ): ListenableFuture<List<MediaItem>> {
            // NOTE: You can use the id from the mediaItems to look up missing 
            // information (e.g. get URI from a database) and return a Future with 
            // a list of playable MediaItems.
    
            // If your use case is really simple and the security/privacy reasons 
            // mentioned earlier don't apply to you, you can add the URI to the 
            // MediaItem request metadata in your activity/fragment and use it
            // to rebuild the playable MediaItem.
            val updatedMediaItems = mediaItems.map { mediaItem ->
                mediaItem.buildUpon()
                    .setUri(mediaItem.requestMetadata.mediaUri)
                    .build()
            }
            return Futures.immediateFuture(updatedMediaItems)
        }
    }
    

    Create and play your MediaItem from the activity/fragment.

    // My Activity
    
    val mmd = MediaMetadata.Builder()
        .setTitle("Example")
        .setArtist("Artist name")
        .build()
    
    // Request metadata. New in (1.0.0-beta01)
    // This is optional. I'm adding a RequestMetadata to the MediaItem so I 
    // can get the mediaUri from my `onAddMediaItems` simple use case (see  
    // onAddMediaItems for more info).
    // If you are going to get the final URI from a database, you can move your 
    // query to your `MediaLibrarySession.Callback#onAddMediaItems` and skip this.
    val rmd = RequestMetadata.Builder()
        .setMediaUri("...".toUri())
        .build()
    
    val mediaItem = MediaItem.Builder()
        .setMediaId("123")
        .setMediaMetadata(mmd)
        .setRequestMetadata(rmd)
        .build()
    
    browser.setMediaItem(mediaItem)
    browser.prepare()
    browser.play()
    

    Old answer (media3 1.0.0-alpha)

    When you create the MediaLibrarySession inside your MediaLibraryService, you can add a MediaItemFiller. This MediaItemFiller has a fillInLocalConfiguration method that will be "Called to fill in the MediaItem.localConfiguration of the media item from controllers."

    Knowing this, you need to:

    Add a MediaItemFiller to your MediaLibrarySession builder inside your service.

    // My MediaLibraryService
    // onCreate()
    mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
        .setMediaItemFiller(CustomMediaItemFiller()) // <--
        .setSessionActivity(pendingIntent)
        .build()
    

    Create a custom MediaSession.MediaItemFiller. Any time you use a setMediaItem/addMediaItem from a controller this will be called and the MediaItem returned here will be the one played.

    class CustomMediaItemFiller : MediaSession.MediaItemFiller {
      override fun fillInLocalConfiguration(
        session: MediaSession,
        controller: MediaSession.ControllerInfo,
        mediaItem: MediaItem
      ): MediaItem {
        // Return the media item to be played
        return mediaItem.buildUpon()
            // Use the metadata values to fill our media item
            .setUri(mediaItem.mediaMetadata.mediaUri)
            .build()
      }
    }
    

    And finally, create and play your MediaItem from the activity.

    // My Activity
    
    // Fill some metadata that the MediaItemFiller 
    // will use to create the new MediaItem
    val mmd = MediaMetadata.Builder()
        .setTitle("Example")
        .setArtist("Artist name")
        .setMediaUri("...".toUri())
        .build()
    
    val mediaItem: MediaItem =
        MediaItem.Builder()
            .setMediaMetadata(mmd)
            .build()
    
    browser.setMediaItem(mediaItem)
    browser.prepare()
    browser.play()
    
    

    I don't know why it has to be this awkward, but if you have a look to the CustomMediaItemFiller they use in the official repo, you will see that they use the mediaItem.mediaId to fetch a valid MediaItem from a media catalog. That's why their demo works when they use setMediaItem from an activity.

    Also, as far as I know, anything you do inside fillInLocalConfiguration has to block the main thread (I believe setMediaItem has to be called from main) so, if you can, try to move any heavy work (ie, get media info from your database) to your Activity/ViewModel where you have more control, fill all the metadata you need there, and use your MediaSession.MediaItemFiller to do a simple transformation. Or move everything to your service and forget about everything.

    I hope the flow is understood. I don't have much experience with media3 and maybe I'm missing something, but with the limitations of MediaItemFiller I found it a bit useless and I would really like to know more about its purpose.