Play same video on multiple views on Android

108 views Asked by At

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

2

There are 2 answers

0
dev.bmax On BEST ANSWER

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()
    }
}
2
Rudra On

There could be some solution i find out :

  1. You can set different surfaces to the Exo Player , by doing this the Exo player will retain all the lifecycles of surfaces and render the video on all of surfaces, but this could be very lagging and Exo player do not render the content on the same time on all surfaces instead , renders on a surface then stops and then renders at surface 2 . The Implementation is following:

            //Jetpack compose
           //player -> exo player builder

           AndroidView(
               factory = {SurfaceView(applicationContext) }, modifier = Modifier
                   .padding(1.dp)
                   .aspectRatio(16f / 9f)
                   .clip(
                       RoundedCornerShape(10.dp)
                   ),
               update = { surfaceView1 ->  // This is the lambda you're asking about
                   player.setVideoSurface(surfaceView1.holder.surface)
               }
           )

           AndroidView(factory = { SurfaceView(applicationContext) }, modifier = Modifier
               .padding(1.dp)
               .aspectRatio(16f / 9f)
               .clip(
                   RoundedCornerShape(10.dp)
               )) {

                   player.setVideoSurface(it.holder.surface)
               }


            AndroidView(factory = { SurfaceView(applicationContext) }, modifier = Modifier
               .padding(1.dp)
               .aspectRatio(16f / 9f)
               .clip(
                   RoundedCornerShape(10.dp)
               )) {
                   player.setVideoSurface(it.holder.surface)

Screenshot : multiple player with player.setVideoSurface()

  1. Copy the contents of Surface and Render it to another Surface using Canvas , this approach is recommended if you are gonna use same video link for playing the video.