question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

SurfaceHolder callback throws when using SimpleExoPlayer from a background thread

See original GitHub issue

This is related to https://github.com/google/ExoPlayer/issues/8843

I create an exo player instance like this.

val playerThread = HandlerThread("video player thread")
playerThread.start()
val handler = Handler(playerThread.looper)
val player = SimpleExoPlayer.Builder(context).setLooper(playerThread.looper).build()

later I add some media items and set the surface

handler.post {
	player.setVideoSurfaceHolder(surfaceView.holder)
    val mediaItem: MediaItem = MediaItem.fromUri(Uri.parse(url))
    playerInfo.player.setMediaItem(mediaItem)
    playerInfo.player.prepare()
    playerInfo.player.play()
}

I also tried just setting the surfaceView, but that gives the same results.

when detached from window

handler.post {
	player.release()
	playerThread.quitSafely()
}

Then I see a stack traces, e.g.

   at com.google.android.exoplayer2.SimpleExoPlayer.verifyApplicationThread(SimpleExoPlayer.java:2028)
        at com.google.android.exoplayer2.SimpleExoPlayer.getCurrentTimeline(SimpleExoPlayer.java:1670)
        at com.google.android.exoplayer2.analytics.AnalyticsCollector.generateEventTime(AnalyticsCollector.java:863)
        at com.google.android.exoplayer2.analytics.AnalyticsCollector.generateEventTime(AnalyticsCollector.java:912)
        at com.google.android.exoplayer2.analytics.AnalyticsCollector.generateReadingMediaPeriodEventTime(AnalyticsCollector.java:920)
        at com.google.android.exoplayer2.analytics.AnalyticsCollector.onSurfaceSizeChanged(AnalyticsCollector.java:492)
        at com.google.android.exoplayer2.SimpleExoPlayer.maybeNotifySurfaceSizeChanged(SimpleExoPlayer.java:1962)
        at com.google.android.exoplayer2.SimpleExoPlayer.access$4100(SimpleExoPlayer.java:92)
        at com.google.android.exoplayer2.SimpleExoPlayer$ComponentListener.surfaceDestroyed(SimpleExoPlayer.java:2280)
        at android.view.SurfaceView.notifySurfaceDestroyed(SurfaceView.java:1830)
        at android.view.SurfaceView.updateSurface(SurfaceView.java:1126)
        at android.view.SurfaceView.onWindowVisibilityChanged(SurfaceView.java:306)
        at android.view.View.dispatchDetachedFromWindow(View.java:20525)
        at android.view.ViewGroup.dispatchDetachedFromWindow(ViewGroup.java:3934)
        at android.view.ViewGroup.dispatchDetachedFromWindow(ViewGroup.java:3934)
        at android.view.ViewGroup.dispatchDetachedFromWindow(ViewGroup.java:3934)
        at android.view.ViewGroup.removeViewInternal(ViewGroup.java:5560)
        at android.view.ViewGroup.removeViewAt(ViewGroup.java:5507)
        at androidx.recyclerview.widget.RecyclerView$5.removeViewAt(RecyclerView.java:882)
        at androidx.recyclerview.widget.ChildHelper.removeViewIfHidden(ChildHelper.java:386)
        at androidx.recyclerview.widget.RecyclerView.removeAnimatingView(RecyclerView.java:1452)
        at androidx.recyclerview.widget.RecyclerView$ItemAnimatorRestoreListener.onAnimationFinished(RecyclerView.java:12699)
        at androidx.recyclerview.widget.RecyclerView$ItemAnimator.dispatchAnimationFinished(RecyclerView.java:13199)
        at androidx.recyclerview.widget.SimpleItemAnimator.dispatchMoveFinished(SimpleItemAnimator.java:292)
        at androidx.recyclerview.widget.DefaultItemAnimator$6.onAnimationEnd(DefaultItemAnimator.java:311)
        at android.view.ViewPropertyAnimator$AnimatorEventListener.onAnimationEnd(ViewPropertyAnimator.java:1111)
        at android.animation.Animator$AnimatorListener.onAnimationEnd(Animator.java:554)
        at android.animation.ValueAnimator.endAnimation(ValueAnimator.java:1250)
        at android.animation.ValueAnimator.doAnimationFrame(ValueAnimator.java:1492)
        at android.animation.AnimationHandler.doAnimationFrame(AnimationHandler.java:146)
        at android.animation.AnimationHandler.access$100(AnimationHandler.java:37)
        at android.animation.AnimationHandler$1.doFrame(AnimationHandler.java:54)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:970)
        at android.view.Choreographer.doCallbacks(Choreographer.java:796)
        at android.view.Choreographer.doFrame(Choreographer.java:727)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7660)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

From the previous bug it looks like because the window of the Surface view is on the main thread. Well of course it is, that’s the only thread it’s allowed to be on.

From what it looks like to me Exo Player doesn’t work when accessed on the non-ui thread when it’s using a surfaceview.

Worse results when using texture view as it doesn’t seem to work at all.

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:5 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
mpalczewcommented, Jun 7, 2021

In case someone sees this and wants to implement callbacks the way I did.

private class DelegatingSurfaceHolder(
        val surfaceHolder: SurfaceHolder,
        val handler: Handler
    ) : SurfaceHolder {

        private val callbackMap: MutableMap<SurfaceHolder.Callback, DelegatingCallback> =
            mutableMapOf()

        override fun addCallback(callback: SurfaceHolder.Callback?) {
            if (callback != null) {
                val delegatingCallback = DelegatingCallback(
                    callback = callback,
                    handler = handler,
                    holder = this
                )
                callbackMap[callback] = delegatingCallback
                surfaceHolder.addCallback(delegatingCallback)
            }
        }

        override fun removeCallback(callback: SurfaceHolder.Callback?) {
            val delegatingCallback = callbackMap[callback] ?: return
            surfaceHolder.removeCallback(delegatingCallback)
            callbackMap.remove(callback)
        }

        override fun isCreating(): Boolean = surfaceHolder.isCreating()
        override fun setType(type: Int) = surfaceHolder.setType(type)
        override fun setFixedSize(width: Int, height: Int) =
            surfaceHolder.setFixedSize(width, height)
        override fun setSizeFromLayout() = surfaceHolder.setSizeFromLayout()
        override fun setFormat(format: Int) = surfaceHolder.setFormat(format)
        override fun setKeepScreenOn(screenOn: Boolean) = surfaceHolder.setKeepScreenOn(screenOn)
        override fun lockCanvas(): Canvas = surfaceHolder.lockCanvas()
        override fun lockCanvas(dirty: Rect?): Canvas = surfaceHolder.lockCanvas(dirty)
        override fun unlockCanvasAndPost(canvas: Canvas?) =
            surfaceHolder.unlockCanvasAndPost(canvas)
        override fun getSurfaceFrame(): Rect = surfaceHolder.surfaceFrame
        override fun getSurface(): Surface = surfaceHolder.surface
    }

    private class DelegatingCallback(
        val callback: SurfaceHolder.Callback,
        val handler: Handler,
        val holder: SurfaceHolder
    ) : SurfaceHolder.Callback {
        override fun surfaceCreated(holder: SurfaceHolder?) {
            handler.post { callback.surfaceCreated(this.holder) }
        }

        override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
            handler.post { callback.surfaceChanged(this.holder, format, width, height) }
        }

        override fun surfaceDestroyed(holder: SurfaceHolder?) {
            val countdownLatch = CountDownLatch(1)
            handler.post {
                callback.surfaceDestroyed(this.holder)
                countdownLatch.countDown()
            }
            countdownLatch.await(1, TimeUnit.SECONDS)
        }
    }

Wrapping the surface holder with what I have here is working for me. Of particular note, DelegatingCallback only implements SufraceHolder.Callback and not Callback2, because that’s what exo implements. Implementing Callback2 and delegating those methods will not work.

0reactions
icbakercommented, Jul 13, 2021

Documentation is updated on dev-v2.

Read more comments on GitHub >

github_iconTop Results From Across the Web

SurfaceHolder callback throws when using SimpleExoPlayer ...
SurfaceHolder callback throws when using SimpleExoPlayer from a background thread.
Read more >
SimpleExoPlayer | Android Developers
Returns the Looper associated with the application thread that's used to access the player and on which player events are received.
Read more >
SimpleExoPlayer (ExoPlayer library)
Modifier and Type Method Description void decreaseDeviceVolume() Deprecated. Decreases the volume of the devi... Clock getClock() Deprecated. Returns the Clock used for playb... CueGroup getCurrentCues() Deprecated....
Read more >
exoplayer.setThrowsWhenUsingWrongThread(false) is ...
1 Answer 1 ... setThrowsWhenUsingWrongThread() has become deprecated for the following reason according to the official documentation: "Disabling ...
Read more >
mozilla-central: changeset 534055 ...
@throws ExoPlaybackException If an error occurs. ... For example a MediaSource - * may use a background thread to load data.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found