Search code examples
androidsurfaceviewexoplayertextureview

Play same video on multiple views on Android


I have one player (in my case ExoPlayer) and I have one video URL. I want to play this video into multiple views at the same time as shown in the screenshot below. Here are some notes:

  • I don't care if the 4 videos are in perfect sync. They can be slightly off-sync.
  • I am not looking for a solution of have 4 player instances. But only one instance.
  • One potential approach that I came across is described here. But I am not sure what is meant by "have the player output to a SurfaceTexture, and then having multiple views make use of the texture". I can output the player to a SurfaceTexture, but how can multiple views make use of the texture?
  • A solution based on Views or Jetpack compose are welcome!

enter image description here


Solution

  • As you probably know the player can only render to a single surface (as mentioned in the issue).

    A relatively simple way to overcome this limitation is to have the player render to one of the views and copy the frames to the rest of the views (assuming they all have equal size).

    And in order to know when each frame is actually drawn we can extend the MediaCodecVideoRenderer.

    Here's an example:

    private fun setupPlayer() {
        player = ExoPlayer.Builder(context)
            .setRenderersFactory(ReplicatingRendererFactory(context))
            .build()
    
        player.setMediaItem(MediaItem.fromUri(videoUri))
    }
    
    private fun setupViews() {
        val textureView1 = findViewById<TextureView>(R.id.texture_view_1)
        val textureView2 = findViewById<TextureView>(R.id.texture_view_2)
        val textureView3 = findViewById<TextureView>(R.id.texture_view_3)
        val textureView4 = findViewById<TextureView>(R.id.texture_view_4)
    
        sourceView = textureView1
        destinationViews = listOf(textureView2, textureView2, textureView3, textureView4)
    
        player.setVideoTextureView(textureView1)
        player.prepare()
        player.playWhenReady = true
    }
    
    private fun copyFrame() {
        val bitmap = sourceView.bitmap ?: return
        destinationViews.forEach { view ->
            check(view.width == sourceView.width && view.height == sourceView.height) {
                "Source and destination views must have equal size."
            }
            val canvas = view.lockCanvas() ?: return@forEach
            canvas.drawBitmap(bitmap, 0f, 0f, paint)
            view.unlockCanvasAndPost(canvas)
        }
        bitmap.recycle()
    }
    
    inner class ReplicatingRendererFactory(
        context: Context
    ): DefaultRenderersFactory(context) {
        override fun buildVideoRenderers(
            context: Context,
            extensionRendererMode: Int,
            mediaCodecSelector: MediaCodecSelector,
            enableDecoderFallback: Boolean,
            eventHandler: Handler,
            eventListener: VideoRendererEventListener,
            allowedVideoJoiningTimeMs: Long,
            out: ArrayList<Renderer>
        ) {
            out.add(ReplicatingRenderer(context, mediaCodecSelector))
            // This results in creating 2 video renderers. You are free to optimize.
            super.buildVideoRenderers(
                context,
                extensionRendererMode,
                mediaCodecSelector,
                enableDecoderFallback,
                eventHandler,
                eventListener,
                allowedVideoJoiningTimeMs,
                out
            )
        }
    }
    
    inner class ReplicatingRenderer(
        context: Context,
        mediaCodecSelector: MediaCodecSelector
    ) : MediaCodecVideoRenderer(context, mediaCodecSelector) {
        override fun onProcessedOutputBuffer(presentationTimeUs: Long) {
            super.onProcessedOutputBuffer(presentationTimeUs)
            copyFrame()
        }
    }