google / ExoPlayer

This project is deprecated and stale. The latest ExoPlayer code is available in https://github.com/androidx/media
https://developer.android.com/media/media3/exoplayer
Apache License 2.0
21.7k stars 6.02k forks source link

MediaSourceFactory instances can't be re-used by multiple Player instances #9099

Open Kolyall opened 3 years ago

Kolyall commented 3 years ago

class TempActivity : AppCompatActivity(R.layout.activity_temp) {

    var exoPlayer: SimpleExoPlayer? = null

    private lateinit var cacheDashMediaSourceFactory: DashMediaSource.Factory
    private lateinit var downloadManager: DownloadManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        createSources()

        findViewById<View>(R.id.playFirstButton).setOnClickListener {
            val mediaSource = buildMediaSource("5fc8c15e69dfc11269541b0e")
            play(mediaSource)
        }

        findViewById<View>(R.id.playSecondButton).setOnClickListener {
            val mediaSource = buildMediaSource("609522d13fcc496540bfd5fb")
            play(mediaSource)
        }

        findViewById<View>(R.id.stopButton).setOnClickListener {
            pauseAndStop()
        }
    }

    private fun createSources() {
        val httpDataSource = DefaultHttpDataSource.Factory()
            .setUserAgent(Util.getUserAgent(applicationContext, BuildConfig.APPLICATION_ID))
            .setTransferListener(
                DefaultBandwidthMeter.Builder(applicationContext)
                    .setResetOnNetworkTypeChange(false)
                    .build()
            )

        val exoDatabaseProvider = ExoDatabaseProvider(applicationContext)

        val downloadCache = SimpleCache(File(applicationContext.cacheDir, "media"), NoOpCacheEvictor(), exoDatabaseProvider)

        val cacheDataSourceFactory = CacheDataSource.Factory()
            .setCache(
                downloadCache
            )
            .setUpstreamDataSourceFactory(
                DefaultDataSourceFactory(
                    applicationContext,
                    httpDataSource
                )
            )
            .setCacheWriteDataSinkFactory(null)
            .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)

        downloadManager = DownloadManager(
            applicationContext,
            exoDatabaseProvider,
            downloadCache,
            cacheDataSourceFactory,
            Executors.newFixedThreadPool(6)
        )
        cacheDashMediaSourceFactory = DashMediaSource.Factory(
            cacheDataSourceFactory
        )
    }

    private fun getOrCreateExoPlayer(): SimpleExoPlayer {
        return exoPlayer ?: SimpleExoPlayer.Builder(applicationContext).build()
            .apply {
                setAudioAttributes(
                    AudioAttributes.Builder()
                        .setContentType(C.CONTENT_TYPE_MUSIC)
                        .setUsage(C.USAGE_MEDIA)
                        .build(),
                    false
                )
                addListener(exoPlayerEventListener)
            }
    }

    private fun play(mediaSource: DashMediaSource) {
        exoPlayer = getOrCreateExoPlayer()
        exoPlayer?.volume = 1.0f
        /**
         * Prepares media to play (happens on background thread) and triggers
         * [SimpleExoPlayerEventListener.onPlayerStateChanged] callback when the stream is ready to play.
         * */
        exoPlayer?.setMediaSource(mediaSource, true)
        exoPlayer?.prepare()
        exoPlayer?.playWhenReady = true
    }

    private fun pauseAndStop() {
        //region pause
        exoPlayer?.playWhenReady = false
        //endregion
        //region stop
        exoPlayer?.release()
        exoPlayer = null
        //endregion
    }

    private fun buildMediaSource(mediaId: String): DashMediaSource {
        val download = downloadManager.downloadIndex.getDownload(mediaId)
        return cacheDashMediaSourceFactory
            .createMediaSource(
                MediaItem.Builder()
                    .setMediaId(mediaId)
                    .setUri(download?.request?.uri)
                    .setDrmUuid(C.WIDEVINE_UUID)
                    .setDrmMultiSession(true)
                    .setMimeType(MimeTypes.APPLICATION_MPD)
                    .setDrmKeySetId(download?.request?.data)
                    .build()
            )
    }

    private val exoPlayerEventListener = object : SimpleExoPlayerEventListener() {
        override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
            Log.d(TAG, "onPlayerStateChanged: $playbackState")
        }

        override fun onPlayerError(error: ExoPlaybackException) {
            when (error.type) {
                ExoPlaybackException.TYPE_SOURCE,
                ExoPlaybackException.TYPE_RENDERER,
                ExoPlaybackException.TYPE_REMOTE,
                ExoPlaybackException.TYPE_UNEXPECTED -> {
                    DLog.e(
                        TAG, "ExoPlayer error type: ${error.type}",
                        error
                    )
                    return
                }
            }

            DLog.e(TAG, "ExoPlayer Unknown error: exception.type: ${error.type}", error)
        }
    }

    companion object {
        const val TAG: String = "TempActivity"
    }
}

Error:

  SimpleExoPlayerEventListener: ExoPlayer error type: 2
    com.google.android.exoplayer2.ExoPlaybackException: Unexpected runtime error
        at com.google.android.exoplayer2.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:587)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:223)
        at android.os.HandlerThread.run(HandlerThread.java:67)
     Caused by: java.lang.IllegalStateException
        at com.google.android.exoplayer2.util.Assertions.checkState(Assertions.java:84)
        at com.google.android.exoplayer2.drm.DefaultDrmSessionManager.initPlaybackLooper(DefaultDrmSessionManager.java:668)
        at com.google.android.exoplayer2.drm.DefaultDrmSessionManager.acquireSession(DefaultDrmSessionManager.java:510)
        at com.google.android.exoplayer2.source.SampleQueue.onFormatResult(SampleQueue.java:918)
        at com.google.android.exoplayer2.source.SampleQueue.peekSampleMetadata(SampleQueue.java:686)
        at com.google.android.exoplayer2.source.SampleQueue.read(SampleQueue.java:412)
        at com.google.android.exoplayer2.source.chunk.ChunkSampleStream.readData(ChunkSampleStream.java:398)
        at com.google.android.exoplayer2.BaseRenderer.readSource(BaseRenderer.java:395)
        at com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.readSourceOmittingSampleData(MediaCodecRenderer.java:996)
        at com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.render(MediaCodecRenderer.java:830)
        at com.google.android.exoplayer2.ExoPlayerImplInternal.doSomeWork(ExoPlayerImplInternal.java:945)
        at com.google.android.exoplayer2.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:478)
        at android.os.Handler.dispatchMessage(Handler.java:102) 
        at android.os.Looper.loop(Looper.java:223) 
        at android.os.HandlerThread.run(HandlerThread.java:67) 
Kolyall commented 3 years ago

All works fine if: click on playFirstButton click on playSecondButton click on stopButton click on playFirstButton click on playSecondButton

But error occurs if: click on playFirstButton click on stopButton click on playFirstButton

icbaker commented 3 years ago

This line in the stack trace indicates something is interacting with the same DefaultDrmSessionManager instance from different 'playback' threads:

com.google.android.exoplayer2.drm.DefaultDrmSessionManager.initPlaybackLooper(DefaultDrmSessionManager.java:668)

https://github.com/google/ExoPlayer/blob/b2333c86c1eac9a1f95992960a8495f1e5b79200/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java#L668

The playback thread is created inside the ExoPlayer library - there's one playback thread associated with each instance of SimpleExoPlayer.

It looks like this multi-threaded access is happening roughly because:

  1. The same DashMediaSource.Factory instance is being used across multiple SimpleExoPlayer instances (because pauseAndStop() nulls out the player, so getOrCreateExoPlayer() always creates a new one).
  2. The DefaultDrmSessionManagerProvider used inside DashMediaSource.Factory will re-use a previously created DefaultDrmSessionManager instance if the MediaItem DRM fields match: https://github.com/google/ExoPlayer/blob/b2333c86c1eac9a1f95992960a8495f1e5b79200/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerProvider.java#L86-L90

(2) explains why you don't see the problem when playing different items - the different drmKeySetId values cause a different DefaultDrmSessionManager to be instantiated each time (which itself might not be necessary, since that particular value is only needed for a setter, not the constructor, but that's getting a bit off topic for this issue).

To make your code work, I think you should couple your DashMediaSource.Factory lifecycle to your SimpleExoPlayer instance. i.e. in pauseAndStop() *also** null the DashMediaSource.Factory and recreate it each time when you create the player.

We should either make the currently broken sequence of operations work correctly (which we could do by removing the caching from DefaultDrmSessionManagerProvider, but unfortunately that's there to fix a different issue: https://github.com/google/ExoPlayer/issues/8523)) or clearly document which part of what you're doing isn't/shouldn't be allowed. I'll keep this open for now to track that work and discuss with the rest of the team.

icbaker commented 3 years ago

Another ExoPlayer change that might make this work (untested) is to null out the playbackLooper field in DefaultDrmSessionManager#release(). Since in the sequence above the player (and thus the DefaultDrmSessionManager) is fully released before being re-instantiated.

Kolyall commented 3 years ago

@icbaker as you suggested I made this: Don't null out the player instance (you should definitely still release() it).

But after exoplayer.release() exoPlayer?.playbackState remains Player.STATE_READY exoPlayer?.currentMediaItem is not null

How can I determinate that player was released?

icbaker commented 3 years ago

Sorry, when I made that suggestion I'd got mixed up between DrmSessionManager (which can be re-prepared after being released) and Player (which can't). So my suggestion to release it without nulling it out doesn't make any sense, because there's no way to re-prepare it. From the Player#release() javadoc:

The player must not be used after calling this method.

I'll edit my comment above to remove that suggestion - I think that means your only workaround option for now is to instantiate the DashMediaSource.Factory each time.