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.74k stars 416 forks source link

MediaController.addListener.onMediaItemTransition() may incorrect call #92

Open XuQK opened 2 years ago

XuQK commented 2 years ago

Media3 Version

1.0.0-beta01

Devices that reproduce the issue

Samsung galaxy note 10 5G running Android 11

Devices that do not reproduce the issue

No response

Reproducible in the demo app?

Not tested

Reproduction steps

  1. In service LibrarySessionCallback.onAddMediaItems() method like this, just transfer uri to MediaItem :

    override fun onAddMediaItems(
    mediaSession: MediaSession,
    controller: MediaSession.ControllerInfo,
    mediaItems: MutableList<MediaItem>
    ): ListenableFuture<MutableList<MediaItem>> {
    return MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()).submit<MutableList<MediaItem>> {
        val infoList = contentResolver.query(
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
            arrayOf(
                MediaStore.Audio.Media._ID,
                MediaStore.Audio.Media.DATA,
                MediaStore.Audio.Media.TITLE,
                MediaStore.Audio.Media.DURATION,
                MediaStore.Audio.Media.ARTIST,
            ),
            "${MediaStore.Audio.Media.DATA} IN (${mediaItems.joinToString { "'${it.requestMetadata.mediaUri.toString()}'" }})",
            null,
            null
        )?.use { cursor ->
            val columnIndexOfId = cursor.getColumnIndex(MediaStore.Audio.Media._ID)
            val columnIndexOfData = cursor.getColumnIndex(MediaStore.Audio.Media.DATA)
            val columnIndexOfTitle = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
            val columnIndexOfDuration = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION)
            val columnIndexOfArtist = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)
            val list = mutableListOf<Array<String>>()
            while (cursor.moveToNext()) {
                list.add(
                    arrayOf(
                        cursor.getLong(columnIndexOfId).toString(),
                        cursor.getString(columnIndexOfData).orEmpty(),
                        cursor.getString(columnIndexOfTitle).orEmpty(),
                        cursor.getLong(columnIndexOfDuration).formatToVideoDuration(),
                        cursor.getString(columnIndexOfArtist).orEmpty(),
                    )
                )
            }
            list
        } ?: return@submit mutableListOf()
    
        val resultItems = mediaItems.map { mediaItem ->
            val infoArray = infoList.find { it[1] == mediaItem.requestMetadata.mediaUri.toString() } ?: return@map null
            val metadataBuilder = MediaMetadata.Builder().apply {
                setTitle(infoArray[2])
                setDurationStr(infoArray[3])
                setArtist(infoArray[4])
                setFolderType(MediaMetadata.FOLDER_TYPE_NONE)
                setIsPlayable(true)
            }
    
            mediaItem.buildUpon()
                .setMediaMetadata(metadataBuilder.build())
                .setMediaId(infoArray[0])
                .setUri(mediaItem.requestMetadata.mediaUri)
                .build()
        }.filterNotNull().toMutableList()
    
        resultItems
    }
    }
  2. Initialize controller in Activity.onStart(), set MediaItem list in controller future`s listener:

    private fun initController() {
    vm.controllerFuture =
        MediaController.Builder(
            this,
            SessionToken(this, ComponentName(this, AudioPlayerService::class.java))
        ).buildAsync()
    vm.controllerFuture.addListener(
        {
            val controller = vm.controller ?: return@addListener
            controller.addListener(object : Player.Listener {
                override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
                    // code A
                }
            })
    
            val mediaItems = uriList.map { uriString ->
                MediaItem.Builder()
                    .setRequestMetadata(
                        MediaItem.RequestMetadata.Builder()
                            .setMediaUri(Uri.parse(uriString))
                            .build()
                    )
                    .build()
            }
            // code B
            controller.setMediaItems(mediaItems)
        },
        MoreExecutors.directExecutor()
    )
    }
  3. when code B is call automaticaly or by hand, a problem occured.

  4. when controller.setMediaItems(mediaItems) called, the code A will be call immediately, the given mediaItem paremeter not converted by onAddMediaItems callback, them onAddMediaItems has been called, and then code A be called again, the mediaItem is converted by onAddMediaItems.

Expected result

When controller.setMediaItems(mediaItems)(code B) has been called , the method call order that I except is:

  1. MediaLibrarySession.Callback.onAddMediaItems()
  2. onMediaItemTransition(mediaItem, reason) has been set by controller.addListener(), and given mediaItem is converted by MediaLibrarySession.Callback.onAddMediaItems().

Actual result

  1. onMediaItemTransition(mediaItem, reason) has been set by controller.addListener(), and given mediaItem is not converted by MediaLibrarySession.Callback.onAddMediaItems().
  2. MediaLibrarySession.Callback.onAddMediaItems()
  3. onMediaItemTransition(mediaItem, reason) has been set by controller.addListener(), and given mediaItem is converted by MediaLibrarySession.Callback.onAddMediaItems() now.

PS: when I set a Listener to ExoPlayer instance in service, it's onMediaItemTransition() method has called as I expected. So different behaviors exist between MediaController and ExoPlayer

This is the log I print manually, after controller.setMediaItems(mediaItems) called, may be useful.

2022-06-21 17:10:17.293 12071-12071/myPackageName D/Log-to-File: Thread: main
├ myPackageName.ui.player.audio.AudioPlayerViewModel$setController$1.onMediaItemTransition(AudioPlayerViewModel.kt:154)
├ androidx.media3.session.MediaControllerImplBase.lambda$updatePlayerInfo$46(MediaControllerImplBase.java:2114)
├ androidx.media3.session.MediaControllerImplBase$$ExternalSyntheticLambda41.invoke(Unknown Source:6)
├ androidx.media3.common.util.ListenerSet$ListenerHolder.invoke(ListenerSet.java:283)
├ androidx.media3.common.util.ListenerSet.lambda$queueEvent$0(ListenerSet.java:192)
├ androidx.media3.common.util.ListenerSet$$ExternalSyntheticLambda1.run(Unknown Source:6)
├ androidx.media3.common.util.ListenerSet.flushEvents(ListenerSet.java:213)
├ androidx.media3.session.MediaControllerImplBase.updatePlayerInfo(MediaControllerImplBase.java:2154)
├ androidx.media3.session.MediaControllerImplBase.setMediaItemsInternal(MediaControllerImplBase.java:1924)
├ androidx.media3.session.MediaControllerImplBase.setMediaItems(MediaControllerImplBase.java:931)
├ androidx.media3.session.MediaController.setMediaItems(MediaController.java:984)
├ myPackageName.ui.player.audio.AudioPlayerActivity.setMediaItemsAndPlay(AudioPlayerActivity.kt:266)
├ myPackageName.ui.player.audio.AudioPlayerActivity.access$setMediaItemsAndPlay(AudioPlayerActivity.kt:101)
├ myPackageName.ui.player.audio.AudioPlayerActivity$content$1$1$1.invoke(AudioPlayerActivity.kt:157)
├ myPackageName.ui.player.audio.AudioPlayerActivity$content$1$1$1.invoke(AudioPlayerActivity.kt:156)
├ myPackageName.ui.player.audio.AudioPlayerActivityKt$AudioPlayerScreen$1$1$4.invoke(AudioPlayerActivity.kt:303)
├ myPackageName.ui.player.audio.AudioPlayerActivityKt$AudioPlayerScreen$1$1$4.invoke(AudioPlayerActivity.kt:295)
├ myPackageName.utils.popup.MenuPopupWithTextAndIconKt$MenuPopupWithTextAndIcon$1$1$1$1.invoke(MenuPopupWithTextAndIcon.kt:96)
├ myPackageName.utils.popup.MenuPopupWithTextAndIconKt$MenuPopupWithTextAndIcon$1$1$1$1.invoke(MenuPopupWithTextAndIcon.kt:96)
└ androidx.compose.foundation.ClickableKt$clickable$4$gesture$1$2.invoke-k-4lQ0M(Clickable.kt:153)
**// Here is called by controller's onMediaItemTransition()**
onMediaItemTransition: title=null  mediaId=null  /storage/emulated/0/Music/好多首歌/大島ミチル - Patema Inverse-copy6-copy-copy.mp3 reason=3
2022-06-21 17:10:17.385 12071-12071/myPackageName D/Log-to-File: Thread: main
├ myPackageName.ui.player.audio.AudioPlayerService$librarySessionCallback$1.onPlayerCommandRequest(AudioPlayerService.kt:213)
├ androidx.media3.session.MediaSessionImpl.onPlayerCommandRequestOnHandler(MediaSessionImpl.java:447)
├ androidx.media3.session.MediaSessionStub.lambda$getSessionTaskWithPlayerCommandRunnable$4$androidx-media3-session-MediaSessionStub(MediaSessionStub.java:261)
├ androidx.media3.session.MediaSessionStub$$ExternalSyntheticLambda71.run(Unknown Source:14)
├ androidx.media3.session.MediaSessionStub.lambda$flushCommandQueue$67(MediaSessionStub.java:1460)
├ androidx.media3.session.MediaSessionStub$$ExternalSyntheticLambda78.run(Unknown Source:2)
├ androidx.media3.common.util.Util.postOrRun(Util.java:548)
├ androidx.media3.session.MediaSessionStub.flushCommandQueue(MediaSessionStub.java:1454)
├ androidx.media3.session.MediaControllerImplBase$FlushCommandQueueHandler.handleMessage(MediaControllerImplBase.java:3059)
├ android.os.Handler.dispatchMessage(Handler.java:106)
├ android.os.Looper.loop(Looper.java:246)
├ android.app.ActivityThread.main(ActivityThread.java:8653)
├ java.lang.reflect.Method.invoke(Native Method)
├ com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
└ com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
onPlayerCommandRequest: playerCommand=20
2022-06-21 17:10:17.546 12071-13653/myPackageName D/Log-to-File: Thread: pool-13-thread-1
├ myPackageName.ui.player.audio.AudioPlayerService$librarySessionCallback$1.onAddMediaItems$lambda-6(AudioPlayerService.kt:192)
├ myPackageName.ui.player.audio.AudioPlayerService$librarySessionCallback$1.$r8$lambda$dZTOHF_a25TC4PIQE_k1kOcWRAE(Unknown Source:0)
├ myPackageName.ui.player.audio.AudioPlayerService$librarySessionCallback$1$$ExternalSyntheticLambda0.call(Unknown Source:4)
├ com.google.common.util.concurrent.TrustedListenableFutureTask$TrustedFutureInterruptibleTask.runInterruptibly(TrustedListenableFutureTask.java:131)
├ com.google.common.util.concurrent.InterruptibleTask.run(InterruptibleTask.java:74)
├ com.google.common.util.concurrent.TrustedListenableFutureTask.run(TrustedListenableFutureTask.java:82)
├ java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
├ java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
└ java.lang.Thread.run(Thread.java:923)
onAddMediaItems: listSize=1536
2022-06-21 17:10:17.724 12071-12071/myPackageName D/Log-to-File: Thread: main
├ myPackageName.ui.player.audio.AudioPlayerService$initSessionAndPlayer$1.onMediaItemTransition(AudioPlayerService.kt:285)
├ androidx.media3.exoplayer.ExoPlayerImpl.lambda$updatePlaybackInfo$14(ExoPlayerImpl.java:1890)
├ androidx.media3.exoplayer.ExoPlayerImpl$$ExternalSyntheticLambda25.invoke(Unknown Source:6)
├ androidx.media3.common.util.ListenerSet$ListenerHolder.invoke(ListenerSet.java:283)
├ androidx.media3.common.util.ListenerSet.lambda$queueEvent$0(ListenerSet.java:192)
├ androidx.media3.common.util.ListenerSet$$ExternalSyntheticLambda1.run(Unknown Source:6)
├ androidx.media3.common.util.ListenerSet.flushEvents(ListenerSet.java:213)
├ androidx.media3.exoplayer.ExoPlayerImpl.updatePlaybackInfo(ExoPlayerImpl.java:1963)
├ androidx.media3.exoplayer.ExoPlayerImpl.setMediaSourcesInternal(ExoPlayerImpl.java:2175)
├ androidx.media3.exoplayer.ExoPlayerImpl.setMediaSources(ExoPlayerImpl.java:605)
├ androidx.media3.exoplayer.ExoPlayerImpl.setMediaItems(ExoPlayerImpl.java:563)
├ androidx.media3.common.ForwardingPlayer.setMediaItems(ForwardingPlayer.java:76)
├ androidx.media3.session.PlayerWrapper.setMediaItems(PlayerWrapper.java:389)
├ androidx.media3.session.MediaSessionStub.lambda$setMediaItemsWithStartIndex$36(MediaSessionStub.java:1011)
├ androidx.media3.session.MediaSessionStub$$ExternalSyntheticLambda22.run(Unknown Source:4)
├ androidx.media3.session.MediaSessionStub.lambda$handleMediaItemsWhenReady$1(MediaSessionStub.java:164)
├ androidx.media3.session.MediaSessionStub$$ExternalSyntheticLambda70.run(Unknown Source:6)
├ android.os.Handler.handleCallback(Handler.java:938)
├ android.os.Handler.dispatchMessage(Handler.java:99)
└ android.os.Looper.loop(Looper.java:246)
**// Here is called by player's onMediaItemTransition()**
onMediaItemTransition: title=Patema Inverse  mediaId=148808  /storage/emulated/0/Music/好多首歌/大島ミチル - Patema Inverse-copy6-copy-copy.mp3 reason=3
2022-06-21 17:10:18.132 12071-12071/myPackageName D/Log-to-File: Thread: main
├ myPackageName.ui.player.audio.AudioPlayerViewModel$setController$1.onMediaItemTransition(AudioPlayerViewModel.kt:154)
├ androidx.media3.session.MediaControllerImplBase.lambda$onPlayerInfoChanged$59$androidx-media3-session-MediaControllerImplBase(MediaControllerImplBase.java:2388)
├ androidx.media3.session.MediaControllerImplBase$$ExternalSyntheticLambda34.invoke(Unknown Source:6)
├ androidx.media3.common.util.ListenerSet$ListenerHolder.invoke(ListenerSet.java:283)
├ androidx.media3.common.util.ListenerSet.lambda$queueEvent$0(ListenerSet.java:192)
├ androidx.media3.common.util.ListenerSet$$ExternalSyntheticLambda1.run(Unknown Source:6)
├ androidx.media3.common.util.ListenerSet.flushEvents(ListenerSet.java:213)
├ androidx.media3.session.MediaControllerImplBase.onPlayerInfoChanged(MediaControllerImplBase.java:2520)
├ androidx.media3.session.MediaControllerStub.lambda$onPlayerInfoChanged$9(MediaControllerStub.java:180)
├ androidx.media3.session.MediaControllerStub$$ExternalSyntheticLambda12.run(Unknown Source:4)
├ androidx.media3.session.MediaControllerStub.lambda$dispatchControllerTaskOnHandler$13(MediaControllerStub.java:280)
├ androidx.media3.session.MediaControllerStub$$ExternalSyntheticLambda5.run(Unknown Source:4)
├ androidx.media3.common.util.Util.postOrRun(Util.java:548)
├ androidx.media3.session.MediaControllerStub.dispatchControllerTaskOnHandler(MediaControllerStub.java:272)
├ androidx.media3.session.MediaControllerStub.onPlayerInfoChanged(MediaControllerStub.java:178)
├ androidx.media3.session.MediaSessionStub$Controller2Cb.onPlayerInfoChanged(MediaSessionStub.java:1723)
├ androidx.media3.session.MediaSessionImpl.dispatchOnPlayerInfoChanged(MediaSessionImpl.java:400)
├ androidx.media3.session.MediaSessionImpl.access$600(MediaSessionImpl.java:79)
├ androidx.media3.session.MediaSessionImpl$PlayerInfoChangedHandler.handleMessage(MediaSessionImpl.java:1193)
└ android.os.Handler.dispatchMessage(Handler.java:106)
**// Here is called by controller's onMediaItemTransition()**
onMediaItemTransition: title=Patema Inverse  148808  /storage/emulated/0/Music/好多首歌/大島ミチル - Patema Inverse-copy6-copy-copy.mp3 reason=3

Media

Not applicable

Bug Report

tonihei commented 2 years ago

Thanks for reporting!

The onMediaItemTransition callback should definitely only be called once for a new item. However, I think the eventual fix will not look like your "expected result", but more like:

  1. onMediaItemTransition(mediaItem, reason) with the given mediaItem that is not converted by MediaLibrarySession.Callback.onAddMediaItems().
  2. MediaLibrarySession.Callback.onAddMediaItems()
  3. optional: another onTimelineChanged(...) or onMediaMetadataChanged(...) callback notifying the change in the mediaItem information

The reason is that the media item transition (from nothing to the initial item) happens immediately at the point where you call mediaController.setMediaItems.

XuQK commented 2 years ago

I thought "the media item transition" should happens after MediaLibrarySession.Callback.onAddMediaItems(). Now I understand. thanks for your explanation.