Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it ok to use runBlocking{} on ExoPlayer's playback thread? #10907

Closed
mudar opened this issue Jan 6, 2023 · 11 comments
Closed

Is it ok to use runBlocking{} on ExoPlayer's playback thread? #10907

mudar opened this issue Jan 6, 2023 · 11 comments
Assignees
Labels

Comments

@mudar
Copy link

mudar commented Jan 6, 2023

Didn't get any answers to my stackoverflow question 😞 so here goes again!

I need to make a network call just before playback (similar in part to this issue). In this case, the network result is also used to to build AdsConfiguration and DrmConfiguration, which are provided to the MediaItem.Builder.

The following code works fine, but I cannot find documentation that confirms it's ok!

override fun createMediaSource(mediaItem: MediaItem): MediaSource {
    val builder = mediaItem.buildUpon()

// Is it ok to use `runBlocking` here?
    val remoteData = runBlocking {
         fetchPlayableMedia()
    }

    remoteData?.let {
        builder.setUri(it.mediaUri)
            .setAdsConfiguration(it.toAdsConfiguration())
            .setDrmConfiguration(it.toDrmConfiguration())
    }

    return mediaSourceDelegate.createMediaSource(builder.build())
}

MediaSource doc says

All methods are called on the player's internal playback thread

which is different from the background thread.

I tried using a ResolvingDataSource, where documentation for resolveDataSpec() clearly mentions

This method is allowed to block until the DataSpec has been resolved.

But resolveDataSpec() is called only after createMediaSource(mediaItem), so it seems too late to set Ads/DRM configuration.

The DataSourceFactory does call createDataSource(), but I'm not sure if httpDataSource.setRequestProperty() can be used to configure Ads/DRM.

To put it differently: how can one do a network call on ExoPlayer's background thread, just before playback, and use the data to set Ads/DRM configuration?

@marcbaechinger
Copy link
Contributor

marcbaechinger commented Jan 6, 2023

Technically, you won't get an ANR when blocking the playback thread because it is a separate thread that is started by the player. However, you would block the playback thread which blocks playback. The system doesn't care, but your user would (I see there is probably nothing playing when you execute the above code but still, in the future you may).

Besides this I actually don't think that MediaSource.Factory.createMediaSource()mediaItem) is called on the playback thread. At least internally in ExoPlayerImpl this is called on the application thread (which may or may not be the main thread). So the blocking code would potentially run on a thread that produces a ANR if blocking too long.

So I would discourage to block the execution in createMediaSource.

I wonder why you don't just do the network call on separate thread before you set the MediaItem to the player:

launch {
   MediaItem mediaItemWithAdConfiguration = suspendMethodThatFetchesPlayableMedia(mediaItem);
   player.setMediaItem(mediaItemWithAdConfiguration);
}

@mudar
Copy link
Author

mudar commented Jan 6, 2023

The intention is to use ExoPlayer's own playlist, and use addMediaItems() instead of setMediaItem()

The network call fetchPlayableMedia() returns tokens with short expiry. Currently, we have our own playlist manager that listens to player commands/events, handles the network call and provides the complete MediaItem to ExoPlayer.

So if we get ExoPlayer to do that network call when it needs it for any playlist item, that would give us a cleaner structure, and hopefully fix playback issues we have with long playlists.

@marcbaechinger
Copy link
Contributor

createMediaSource is always called at the moment when the addMediaItems call is made. So doing the request in createMediaSource doesn't help regarding the expiration of the tokens I think. You'd have to do that just before prepare is called.

I don't see a way to do this from the top of my head with shortly lifing tokens I'm afraid. As far as I understand you'd have to make your own MediaSource that wraps an AdsMediaSource but you would only want to create the wrapped media source just at the moment when prepare is called. Then again, when the user would pause the player after prepare is called (like around 20 seconds before transition to the next item), then the token could still expire if playback is not resumed.

So increasing the life time of the tokens would make your life much easier.

@mudar
Copy link
Author

mudar commented Jan 9, 2023

Thanks for your help 👍 To make life difficult, I might still implement a custom CompositeMediaSource

My question would then become:
Is prepareSourceInternal() called on the player or the loader thread?

My code would be something like

override fun prepareSourceInternal(mediaTransferListener: TransferListener?) {
    super.prepareSourceInternal(mediaTransferListener)
    val builder = mediaItem.buildUpon()

// Does this block the playback or the loader thread?
    val remoteData = runBlocking {
        fetchPlayableMedia()
    }

    remoteData?.let {
        builder.setUri(it.mediaUri)
            .setAdsConfiguration(it.toAdsConfiguration())
            .setDrmConfiguration(it.toDrmConfiguration())
    }
    
    prepareChildSource(null, updateInternalMediaSource(builder))
}

private fun updateInternalMediaSource(builder: MediaItem.Builder): MediaSource { … }

private suspend fun fetchPlayableMedia(): PlayableMedia? { … }

@marcbaechinger
Copy link
Contributor

A CompositeMEdiaSource is the best choice for your requirements I think. prepareSourceInternal is called on the playback thread.

@mudar
Copy link
Author

mudar commented Jan 10, 2023

After some testing, I can confirm that prepareSourceInternal() is called on the playback thread. So if CompositeMediaSource (or MediaSource) does not provide methods that are called on the loader thread, I think I won't be able to replace our PlaylistManager by ExoPlayer's playlist.

My solution would be to have the PlaylistManager start with setMediaItem() for the first item. Then, around 15-20 seconds before the end of the current item, do the suspending network call on a different thread then call addMediaItem() with the MediaItem that contains the Ads/DRM configuration.

Not the easiest solution as I'll have to handle token expiry and the Previous button, but I hope this will help with wakelock and buffering..

Thanks again for your answers 👍

@tonihei
Copy link
Collaborator

tonihei commented Jan 10, 2023

You can start a separate thread in prepareSourceInternal to handle background loading. If you like, you can also use ExoPlayer's Loader interface for that, but you don't have to. This is actually a common pattern for adaptive media like DASH or HLS: prepareSourceInternal starts loading a manifest file in the background and once it's available the source is marked as prepared. See for example

@mudar
Copy link
Author

mudar commented Jan 11, 2023

I'm looking into extending WrappingMediaSource or MaskingMediaSource. For now, I'm facing a NullPointerException in the parent class, related to the Looper.

java.lang.NullPointerException
  at com.google.android.exoplayer2.util.Assertions.checkNotNull(Assertions.java:154)
  at com.google.android.exoplayer2.source.hls.HlsMediaSource.prepareSourceInternal(HlsMediaSource.java:420)
  at com.google.android.exoplayer2.source.BaseMediaSource.prepareSource(BaseMediaSource.java:221)
  at com.google.android.exoplayer2.source.CompositeMediaSource.prepareChildSource(CompositeMediaSource.java:120)

So I'm wondering what's the correct way to mark the source as prepared?

I've tried delaying super.prepareSourceInternal() till after the suspending call, or calling prepareChildSource() or onChildSourceInfoRefreshed(). To no avail 😞

I also looked into implementing a Loader with a Loadable, EventDispatcher etc. I do reach onLoadCompleted() successfully, but I still have some reading to do before I manage to get this to part to work. I hope to get prepareSourceInternal() right first (manually load on a background thread), before trying a more elegant implementation with a Loader.

@tonihei
Copy link
Collaborator

tonihei commented Jan 12, 2023

WrappingMediaSource is a convenience version of CompositeMediaSource<Void> that handles some boiler-plate code. As a convenience class, it can't provide the full flexibility of its parent and one of things it explicitly can't do is delaying the prepare step of the wrapped source (=what you want to do).

So I think you need to subclass CompositeMediaSource<Void> instead. You can look at the source of WrappingMediaSource to see how methods are generally handled or examples like this one: #7279 (comment) In your case you want to delay the call to prepareChildSource until after your suspending call I think.

@mudar
Copy link
Author

mudar commented Jan 13, 2023

Things are looking better, thanks again for your help 👍

This is what my (simplified) code looks like

class CustomMediaSource : CompositeMediaSource<Void?>() {

    private val loader: Loader

    private val loadable: LoadablePlayableMedia

    private val loaderCallback = object : Loader.Callback<LoadablePlayableMedia> {
        override fun onLoadCompleted(
            loadable: LoadablePlayableMedia, elapsedRealtimeMs: Long, loadDurationMs: Long
        ) {
            loadable.playableMedia?.let { playableMedia ->
                mediaSource = mediaSourceFactory.createMediaSource(playableMedia.toMediaItem())
                prepareChildSource(null, mediaSource)
            }
        }
    }

    override fun onChildSourceInfoRefreshed(
        id: Void?,
        mediaSource: MediaSource,
        timeline: Timeline
    ) {
        refreshSourceInfo(timeline)
    }

    override fun prepareSourceInternal(mediaTransferListener: TransferListener?) {
        super.prepareSourceInternal(mediaTransferListener)
        loader.startLoading(loadable, loaderCallback, 0)
    }
}

And the suspending network method is handled by the loadable

class LoadablePlayableMedia : Loadable {

    var playableMedia: PlayableMedia? = null
        private set

    override fun load() {
        runBlocking {
            playableMedia = fetchPlayableMedia()
        }
    }

    private suspend fun fetchPlayableMedia(): PlayableMedia? { ... }
}

The network call is done on the background thread, then onLoadCompleted() creates a complete MediaItem with AdsConfiguration and DrmConfiguration on the playback thread.

The last issue I'm still working on is how to get back to the main thread when I call AdsLoader.setPlayer(). It currently throws an IllegalStateException because it's not called on the ImaThread. I probably need to change the way my AdsLoader.Provider is setup...

@marcbaechinger
Copy link
Contributor

marcbaechinger commented Jan 16, 2023

You can create a Handler in the constructor of your CustomMediaSource and then post a message to that handler.

To work together well with IMA I'd do something like this in the constructor:

mainHandler = new Handler(Looper.getMainLooper());

@mudar mudar closed this as completed Jan 24, 2023
@google google locked and limited conversation to collaborators Mar 26, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

3 participants