ExoPlayer: PlayerNotificationManager not updating on fast consecutive actions

[REQUIRED] Issue description

I’m using ExoPlayer to play some HLS tracks. When i’m doing couple of fast actions (like tap 3-4 times Next button) by mediaController or in notification - the content i return in PlayerNotificationManager.MediaDescriptionAdapter doesn’t get displayed. Notification displays one track previous to the one that is actually playing. So Changing playback this way properly updates the player state, but doesn’t seem to update the notification properly. I can’t seem to make some 100% repro, but it does this consistently when i smash next/previous button quite fast.

[REQUIRED] Reproduction steps

This is my MediaDescriptionAdapter:

internal class MediaDescriptionAdapter(
    private val context: Context,
    private val sessionPendingIntent: PendingIntent?
) : PlayerNotificationManager.MediaDescriptionAdapter {

    private var currentBitmap: Bitmap? = null

    override fun createCurrentContentIntent(player: Player?): PendingIntent? = sessionPendingIntent

    override fun getCurrentContentText(player: Player?): String? = player?.let {
        val mediaDescription = it.currentTag as MediaDescriptionCompat? ?: return ""
        mediaDescription.subtitle.toString()
    }

    override fun getCurrentContentTitle(player: Player?): String {
        player?.let {
            val mediaDescription = it.currentTag as MediaDescriptionCompat? ?: return ""
            Timber.d("getCurrentContentTitle with ${mediaDescription.title.toString()}")
            return mediaDescription.title.toString()
        }
        return ""
    }

    override fun getCurrentLargeIcon(
        player: Player?,
        callback: PlayerNotificationManager.BitmapCallback?
    ): Bitmap? {
        val mediaDescription = player?.currentTag as MediaDescriptionCompat?
        val uri = mediaDescription?.iconUri ?: return null
        Glide
            .with(context)
            .asBitmap()
            .load(uri)
            .into(object : CustomTarget<Bitmap>() {
                override fun onLoadCleared(placeholder: Drawable?) {
                    // Nothing to do here
                }

                override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
                    callback?.onBitmap(resource)
                    currentBitmap = resource
                }
            })
        return currentBitmap
    }
}

As you can see i log the result of getCurrentContentTitle. I can confirm that this method gets called properly and i do return correct metadata for currently playing track. So it seems like notification don’t get some kind of update event properly.

Can you think of any reason this could be happening?

My first idea was that Glide callback could brake something, but when i return null on every getCurrentLargeIcon the problem still occurs.

#EDIT: i think i have some repro. When i change the track, wait for the time it takes to load hls and click next and the exact time - notification doesn’t get refreshed but track is changed

#EDIT2: Sometimes metadata is updated properly, but notification background isn’t set when callingcallback.onBitmap().

[REQUIRED] Link to test content

This bug seems to be unrelated to any particular content

[REQUIRED] Version of ExoPlayer being used

2.10.7

[REQUIRED] Device(s) and version(s) of Android being used

Multiple emulators, from android 6 to 10, OnePlus 7 with Android 10

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 15 (6 by maintainers)

Commits related to this issue

Most upvoted comments

Reducing the notification updates alone will make it harder for users to produce this

This has now been done in Marc’s change ref’d above. @marcbaechinger - Should we either close this or reclassify it as a platform bug at this point?

I was able to reproduce this with Google Play Music, Spotify and the ExoPlayer demo app using the PlayerNotificationManager:

2019-12-02 17:34:21.814 1061-3016/system_process E/NotificationService: Package enqueue rate is 5.5803037. Shedding 0|com.spotify.music|2131430002|null|10235. package=com.spotify.music
2019-12-02 17:35:14.937 1061-2130/system_process E/NotificationService: Package enqueue rate is 5.2860003. Shedding 0|com.google.android.music|1|null|10143. package=com.google.android.music
2019-12-02 17:37:30.132 1061-1735/system_process E/NotificationService: Package enqueue rate is 5.4202433. Shedding 0|com.google.android.exoplayer2.demo|10|null|10218. package=com.google.android.exoplayer2.demo

For the case when I just hit the play/pause button repeatedly the notification is updated once for each status update (in this case playWhenReady true/false). When hitting very quickly I can still go over the enqueue rate threshold which causes the notification to be out of sync.

So reducing the notification updates alone will make it harder for users to produce this (needs hitting faster), but it is still possible. I’m looking into providing a fix for this.

A second approach might be to somehow throttle updates of the notifications. I’m not sure what is the best way of doing this as I actually don’t want to significantly increase the lag between the command arriving and the update of the notification.

I’ll update this thread when we push something to the dev-v2 branch.

Thanks in advance for investigation.

I solved the glide multiple requests using coroutines and it does work fine

override fun getCurrentLargeIcon(
        player: Player?,
        callback: PlayerNotificationManager.BitmapCallback?
    ): Bitmap? {
        val mediaDescription = player?.currentTag as MediaDescriptionCompat?
        val uri = mediaDescription?.iconUri ?: return null
        val cachedBitmap = bitmapCache[uri]
        if (cachedBitmap != null) return cachedBitmap

        // check if request for uri exists and then wait for it (or create one)
        scope.launch {
            val deferredJob = downloadLock.withLock {
                val job = downloadCache[uri]
                if (job != null) {
                    job
                } else {
                    val newJob = getCoverAsDeferredAsync(uri)
                    downloadCache.put(uri, newJob)
                    newJob
                }
            }

            val bitmap = deferredJob.await()
            callback?.onBitmap(bitmap)
            previousBitmap = bitmap
        }
        return previousBitmap
    }

    private suspend fun getCoverAsDeferredAsync(uri: Uri): Deferred<Bitmap> = coroutineScope {
        async { getCover(uri) }
    }

    private suspend fun getCover(uri: Uri): Bitmap {
        return suspendCoroutine { continuation ->
            Glide
                .with(context)
                .asBitmap()
                .load(uri)
                .into(object : CustomTarget<Bitmap>() {
                    override fun onLoadCleared(placeholder: Drawable?) {
                        // Nothing to do here
                    }

                    override fun onResourceReady(
                        resource: Bitmap,
                        transition: Transition<in Bitmap>?
                    ) {
                        continuation.resume(resource)
                        bitmapCache.put(uri, resource)
                    }
                })
        }
    }

This way i create a Deferred job for every unique request that i pass to glide. Any consecutive requests are just waiting for that job to finish (by await())