David Liu

Add a textureviewrenderer for easier view manipulation

  1 +/*
  2 + * Copyright 2015 The WebRTC project authors. All Rights Reserved.
  3 + *
  4 + * Use of this source code is governed by a BSD-style license
  5 + * that can be found in the LICENSE file in the root of the source
  6 + * tree. An additional intellectual property rights grant can be found
  7 + * in the file PATENTS. All contributing project authors may
  8 + * be found in the AUTHORS file in the root of the source tree.
  9 + */
  10 +package io.livekit.android.renderer
  11 +
  12 +import android.content.Context
  13 +import android.view.SurfaceView
  14 +import android.view.SurfaceHolder
  15 +import org.webrtc.RendererCommon.RendererEvents
  16 +import org.webrtc.RendererCommon.VideoLayoutMeasure
  17 +import kotlin.jvm.JvmOverloads
  18 +import org.webrtc.RendererCommon.GlDrawer
  19 +import org.webrtc.RendererCommon.ScalingType
  20 +import android.content.res.Resources.NotFoundException
  21 +import android.graphics.Matrix
  22 +import android.os.Looper
  23 +import android.util.AttributeSet
  24 +import android.view.TextureView
  25 +import org.webrtc.*
  26 +import android.graphics.SurfaceTexture
  27 +import android.view.Surface
  28 +
  29 +import org.webrtc.ThreadUtils
  30 +import java.util.concurrent.CountDownLatch
  31 +
  32 +
  33 +/**
  34 + * Display the video stream on a SurfaceView.
  35 + */
  36 +class TextureViewRenderer : TextureView, SurfaceHolder.Callback, TextureView.SurfaceTextureListener, VideoSink, RendererEvents {
  37 + // Cached resource name.
  38 + private val resourceName: String
  39 + private val videoLayoutMeasure = VideoLayoutMeasure()
  40 + private val eglRenderer: SurfaceEglRenderer
  41 +
  42 + // Callback for reporting renderer events. Read-only after initialization so no lock required.
  43 + private var rendererEvents: RendererEvents? = null
  44 +
  45 + // Accessed only on the main thread.
  46 + private var rotatedFrameWidth = 0
  47 + private var rotatedFrameHeight = 0
  48 + private var enableFixedSize = false
  49 + private var surfaceWidth = 0
  50 + private var surfaceHeight = 0
  51 +
  52 + /**
  53 + * Standard View constructor. In order to render something, you must first call init().
  54 + */
  55 + constructor(context: Context) : super(context) {
  56 + resourceName = getResourceName()
  57 + eglRenderer = SurfaceEglRenderer(resourceName)
  58 + surfaceTextureListener = this
  59 + }
  60 +
  61 + /**
  62 + * Standard View constructor. In order to render something, you must first call init().
  63 + */
  64 + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
  65 + resourceName = getResourceName()
  66 + eglRenderer = SurfaceEglRenderer(resourceName)
  67 +
  68 + surfaceTextureListener = this
  69 + }
  70 + /**
  71 + * Initialize this class, sharing resources with `sharedContext`. The custom `drawer` will be used
  72 + * for drawing frames on the EGLSurface. This class is responsible for calling release() on
  73 + * `drawer`. It is allowed to call init() to reinitialize the renderer after a previous
  74 + * init()/release() cycle.
  75 + */
  76 + /**
  77 + * Initialize this class, sharing resources with `sharedContext`. It is allowed to call init() to
  78 + * reinitialize the renderer after a previous init()/release() cycle.
  79 + */
  80 + @JvmOverloads
  81 + fun init(
  82 + sharedContext: EglBase.Context?,
  83 + rendererEvents: RendererEvents?, configAttributes: IntArray? = EglBase.CONFIG_PLAIN,
  84 + drawer: GlDrawer? = GlRectDrawer()
  85 + ) {
  86 + ThreadUtils.checkIsOnMainThread()
  87 + this.rendererEvents = rendererEvents
  88 + rotatedFrameWidth = 0
  89 + rotatedFrameHeight = 0
  90 + eglRenderer.init(sharedContext, this /* rendererEvents */, configAttributes, drawer)
  91 + }
  92 +
  93 + /**
  94 + * Block until any pending frame is returned and all GL resources released, even if an interrupt
  95 + * occurs. If an interrupt occurs during release(), the interrupt flag will be set. This function
  96 + * should be called before the Activity is destroyed and the EGLContext is still valid. If you
  97 + * don't call this function, the GL resources might leak.
  98 + */
  99 + fun release() {
  100 + eglRenderer.release()
  101 + }
  102 +
  103 + /**
  104 + * Register a callback to be invoked when a new video frame has been received.
  105 + *
  106 + * @param listener The callback to be invoked. The callback will be invoked on the render thread.
  107 + * It should be lightweight and must not call removeFrameListener.
  108 + * @param scale The scale of the Bitmap passed to the callback, or 0 if no Bitmap is
  109 + * required.
  110 + * @param drawer Custom drawer to use for this frame listener.
  111 + */
  112 + fun addFrameListener(
  113 + listener: EglRenderer.FrameListener?, scale: Float, drawerParam: GlDrawer?
  114 + ) {
  115 + eglRenderer.addFrameListener(listener, scale, drawerParam)
  116 + }
  117 +
  118 + /**
  119 + * Register a callback to be invoked when a new video frame has been received. This version uses
  120 + * the drawer of the EglRenderer that was passed in init.
  121 + *
  122 + * @param listener The callback to be invoked. The callback will be invoked on the render thread.
  123 + * It should be lightweight and must not call removeFrameListener.
  124 + * @param scale The scale of the Bitmap passed to the callback, or 0 if no Bitmap is
  125 + * required.
  126 + */
  127 + fun addFrameListener(listener: EglRenderer.FrameListener?, scale: Float) {
  128 + eglRenderer.addFrameListener(listener, scale)
  129 + }
  130 +
  131 + fun removeFrameListener(listener: EglRenderer.FrameListener?) {
  132 + eglRenderer.removeFrameListener(listener)
  133 + }
  134 +
  135 + /**
  136 + * Enables fixed size for the surface. This provides better performance but might be buggy on some
  137 + * devices. By default this is turned off.
  138 + */
  139 + fun setEnableHardwareScaler(enabled: Boolean) {
  140 + ThreadUtils.checkIsOnMainThread()
  141 + enableFixedSize = enabled
  142 + updateSurfaceSize()
  143 + }
  144 +
  145 + /**
  146 + * Set if the video stream should be mirrored or not.
  147 + */
  148 + fun setMirror(mirror: Boolean) {
  149 + eglRenderer.setMirror(mirror)
  150 + }
  151 +
  152 + /**
  153 + * Set how the video will fill the allowed layout area.
  154 + */
  155 + fun setScalingType(scalingType: ScalingType?) {
  156 + ThreadUtils.checkIsOnMainThread()
  157 + videoLayoutMeasure.setScalingType(scalingType)
  158 + requestLayout()
  159 + }
  160 +
  161 + fun setScalingType(
  162 + scalingTypeMatchOrientation: ScalingType?,
  163 + scalingTypeMismatchOrientation: ScalingType?
  164 + ) {
  165 + ThreadUtils.checkIsOnMainThread()
  166 + videoLayoutMeasure.setScalingType(
  167 + scalingTypeMatchOrientation,
  168 + scalingTypeMismatchOrientation
  169 + )
  170 + requestLayout()
  171 + }
  172 +
  173 + /**
  174 + * Limit render framerate.
  175 + *
  176 + * @param fps Limit render framerate to this value, or use Float.POSITIVE_INFINITY to disable fps
  177 + * reduction.
  178 + */
  179 + fun setFpsReduction(fps: Float) {
  180 + eglRenderer.setFpsReduction(fps)
  181 + }
  182 +
  183 + fun disableFpsReduction() {
  184 + eglRenderer.disableFpsReduction()
  185 + }
  186 +
  187 + fun pauseVideo() {
  188 + eglRenderer.pauseVideo()
  189 + }
  190 +
  191 + // VideoSink interface.
  192 + override fun onFrame(frame: VideoFrame) {
  193 + eglRenderer.onFrame(frame)
  194 + }
  195 +
  196 + // View layout interface.
  197 + override fun onMeasure(widthSpec: Int, heightSpec: Int) {
  198 + ThreadUtils.checkIsOnMainThread()
  199 + val size =
  200 + videoLayoutMeasure.measure(widthSpec, heightSpec, rotatedFrameWidth, rotatedFrameHeight)
  201 + setMeasuredDimension(size.x, size.y)
  202 + logD("onMeasure(). New size: " + size.x + "x" + size.y)
  203 + }
  204 +
  205 + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
  206 + ThreadUtils.checkIsOnMainThread()
  207 + eglRenderer.setLayoutAspectRatio((right - left) / (bottom - top).toFloat())
  208 + updateSurfaceSize()
  209 + }
  210 +
  211 + private fun updateSurfaceSize() {
  212 + ThreadUtils.checkIsOnMainThread()
  213 + if (enableFixedSize && rotatedFrameWidth != 0 && rotatedFrameHeight != 0 && width != 0 && height != 0) {
  214 + val layoutAspectRatio = width / height.toFloat()
  215 + val frameAspectRatio = rotatedFrameWidth / rotatedFrameHeight.toFloat()
  216 + val drawnFrameWidth: Int
  217 + val drawnFrameHeight: Int
  218 + if (frameAspectRatio > layoutAspectRatio) {
  219 + drawnFrameWidth = (rotatedFrameHeight * layoutAspectRatio).toInt()
  220 + drawnFrameHeight = rotatedFrameHeight
  221 + } else {
  222 + drawnFrameWidth = rotatedFrameWidth
  223 + drawnFrameHeight = (rotatedFrameWidth / layoutAspectRatio).toInt()
  224 + }
  225 + // Aspect ratio of the drawn frame and the view is the same.
  226 + val width = Math.min(width, drawnFrameWidth)
  227 + val height = Math.min(height, drawnFrameHeight)
  228 + logD(
  229 + "updateSurfaceSize. Layout size: " + getWidth() + "x" + getHeight() + ", frame size: "
  230 + + rotatedFrameWidth + "x" + rotatedFrameHeight + ", requested surface size: " + width
  231 + + "x" + height + ", old surface size: " + surfaceWidth + "x" + surfaceHeight
  232 + )
  233 + if (width != surfaceWidth || height != surfaceHeight) {
  234 + surfaceWidth = width
  235 + surfaceHeight = height
  236 + adjustAspectRatio(surfaceWidth, surfaceHeight);
  237 + }
  238 + } else {
  239 + surfaceHeight = 0
  240 + surfaceWidth = surfaceHeight
  241 + }
  242 + }
  243 +
  244 + /**
  245 + * Sets the TextureView transform to preserve the aspect ratio of the video.
  246 + */
  247 + private fun adjustAspectRatio(videoWidth: Int, videoHeight: Int) {
  248 + val viewWidth = width
  249 + val viewHeight = height
  250 + val aspectRatio = videoHeight.toDouble() / videoWidth
  251 + val newWidth: Int
  252 + val newHeight: Int
  253 + if (viewHeight > (viewWidth * aspectRatio).toInt()) {
  254 + // limited by narrow width; restrict height
  255 + newWidth = viewWidth
  256 + newHeight = (viewWidth * aspectRatio).toInt()
  257 + } else {
  258 + // limited by short height; restrict width
  259 + newWidth = (viewHeight / aspectRatio).toInt()
  260 + newHeight = viewHeight
  261 + }
  262 + val xoff = (viewWidth - newWidth) / 2
  263 + val yoff = (viewHeight - newHeight) / 2
  264 + logD(
  265 + "video=" + videoWidth + "x" + videoHeight + " view=" + viewWidth + "x" + viewHeight
  266 + + " newView=" + newWidth + "x" + newHeight + " off=" + xoff + "," + yoff
  267 + )
  268 + val txform = Matrix()
  269 + getTransform(txform)
  270 + txform.setScale(newWidth.toFloat() / viewWidth, newHeight.toFloat() / viewHeight)
  271 + // txform.postRotate(10); // just for fun
  272 + txform.postTranslate(xoff.toFloat(), yoff.toFloat())
  273 + setTransform(txform)
  274 + }
  275 +
  276 + // SurfaceHolder.Callback interface.
  277 + override fun surfaceCreated(holder: SurfaceHolder) {
  278 + ThreadUtils.checkIsOnMainThread()
  279 + surfaceHeight = 0
  280 + surfaceWidth = surfaceHeight
  281 + updateSurfaceSize()
  282 + }
  283 +
  284 + override fun surfaceDestroyed(holder: SurfaceHolder) {}
  285 + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
  286 +
  287 + // TextureView.SurfaceTextureListener implementation
  288 + override fun onSurfaceTextureAvailable(surface: SurfaceTexture, i: Int, i1: Int) {
  289 + ThreadUtils.checkIsOnMainThread()
  290 + eglRenderer.createEglSurface(Surface(surfaceTexture))
  291 + surfaceHeight = 0
  292 + surfaceWidth = surfaceHeight
  293 + updateSurfaceSize()
  294 + }
  295 +
  296 + override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
  297 + ThreadUtils.checkIsOnMainThread()
  298 + logD("surfaceChanged: size: " + width + "x" + height)
  299 + }
  300 +
  301 + override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
  302 + ThreadUtils.checkIsOnMainThread()
  303 + val completionLatch = CountDownLatch(1)
  304 + eglRenderer.releaseEglSurface { completionLatch.countDown() }
  305 + ThreadUtils.awaitUninterruptibly(completionLatch)
  306 + return true
  307 + }
  308 +
  309 + override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
  310 +
  311 + private fun getResourceName(): String {
  312 + return try {
  313 + resources.getResourceEntryName(id)
  314 + } catch (e: NotFoundException) {
  315 + ""
  316 + }
  317 + }
  318 +
  319 + /**
  320 + * Post a task to clear the SurfaceView to a transparent uniform color.
  321 + */
  322 + fun clearImage() {
  323 + eglRenderer.clearImage()
  324 + }
  325 +
  326 + override fun onFirstFrameRendered() {
  327 + if (rendererEvents != null) {
  328 + rendererEvents!!.onFirstFrameRendered()
  329 + }
  330 + }
  331 +
  332 + override fun onFrameResolutionChanged(videoWidth: Int, videoHeight: Int, rotation: Int) {
  333 + if (rendererEvents != null) {
  334 + rendererEvents!!.onFrameResolutionChanged(videoWidth, videoHeight, rotation)
  335 + }
  336 + val rotatedWidth = if (rotation == 0 || rotation == 180) videoWidth else videoHeight
  337 + val rotatedHeight = if (rotation == 0 || rotation == 180) videoHeight else videoWidth
  338 + // run immediately if possible for ui thread tests
  339 + postOrRun {
  340 + rotatedFrameWidth = rotatedWidth
  341 + rotatedFrameHeight = rotatedHeight
  342 + updateSurfaceSize()
  343 + requestLayout()
  344 + }
  345 + }
  346 +
  347 + private fun postOrRun(r: Runnable) {
  348 + if (Thread.currentThread() === Looper.getMainLooper().thread) {
  349 + r.run()
  350 + } else {
  351 + post(r)
  352 + }
  353 + }
  354 +
  355 + private fun logD(string: String) {
  356 + Logging.d(TAG, "$resourceName: $string")
  357 + }
  358 +
  359 + companion object {
  360 + private const val TAG = "SurfaceViewRenderer"
  361 + }
  362 +}
@@ -11,6 +11,7 @@ import dagger.assisted.AssistedFactory @@ -11,6 +11,7 @@ import dagger.assisted.AssistedFactory
11 import dagger.assisted.AssistedInject 11 import dagger.assisted.AssistedInject
12 import io.livekit.android.ConnectOptions 12 import io.livekit.android.ConnectOptions
13 import io.livekit.android.Version 13 import io.livekit.android.Version
  14 +import io.livekit.android.renderer.TextureViewRenderer
14 import io.livekit.android.room.participant.LocalParticipant 15 import io.livekit.android.room.participant.LocalParticipant
15 import io.livekit.android.room.participant.Participant 16 import io.livekit.android.room.participant.Participant
16 import io.livekit.android.room.participant.ParticipantListener 17 import io.livekit.android.room.participant.ParticipantListener
@@ -390,6 +391,17 @@ constructor( @@ -390,6 +391,17 @@ constructor(
390 viewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) 391 viewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
391 viewRenderer.setEnableHardwareScaler(false /* enabled */) 392 viewRenderer.setEnableHardwareScaler(false /* enabled */)
392 } 393 }
  394 +
  395 + /**
  396 + * @suppress
  397 + * // TODO(@dl): can this be moved out of Room/SDK?
  398 + */
  399 + fun initVideoRenderer(viewRenderer: TextureViewRenderer) {
  400 + viewRenderer.init(eglBase.eglBaseContext, null)
  401 + viewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
  402 + viewRenderer.setEnableHardwareScaler(false /* enabled */)
  403 + }
  404 +
393 } 405 }
394 406
395 /** 407 /**
@@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
17 app:layout_constraintBottom_toBottomOf="parent" 17 app:layout_constraintBottom_toBottomOf="parent"
18 app:layout_constraintTop_toBottomOf="@id/tabs" /> 18 app:layout_constraintTop_toBottomOf="@id/tabs" />
19 19
20 - <org.webrtc.SurfaceViewRenderer 20 + <io.livekit.android.renderer.TextureViewRenderer
21 android:id="@+id/pip_video_view" 21 android:id="@+id/pip_video_view"
22 android:layout_height="144dp" 22 android:layout_height="144dp"
23 android:layout_width="144dp" 23 android:layout_width="144dp"
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 android:layout_width="match_parent" 3 android:layout_width="match_parent"
4 android:layout_height="match_parent"> 4 android:layout_height="match_parent">
5 5
6 - <org.webrtc.SurfaceViewRenderer 6 + <io.livekit.android.renderer.TextureViewRenderer
7 android:id="@+id/renderer" 7 android:id="@+id/renderer"
8 android:layout_width="match_parent" 8 android:layout_width="match_parent"
9 android:layout_height="match_parent" /> 9 android:layout_height="match_parent" />