OLD | NEW |
(Empty) | |
| 1 /* |
| 2 * libjingle |
| 3 * Copyright 2015 Google Inc. |
| 4 * |
| 5 * Redistribution and use in source and binary forms, with or without |
| 6 * modification, are permitted provided that the following conditions are met: |
| 7 * |
| 8 * 1. Redistributions of source code must retain the above copyright notice, |
| 9 * this list of conditions and the following disclaimer. |
| 10 * 2. Redistributions in binary form must reproduce the above copyright notice, |
| 11 * this list of conditions and the following disclaimer in the documentation |
| 12 * and/or other materials provided with the distribution. |
| 13 * 3. The name of the author may not be used to endorse or promote products |
| 14 * derived from this software without specific prior written permission. |
| 15 * |
| 16 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED |
| 17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| 18 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO |
| 19 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 20 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| 21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; |
| 22 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, |
| 23 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR |
| 24 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| 25 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 26 */ |
| 27 |
| 28 package org.webrtc; |
| 29 |
| 30 import java.nio.ByteBuffer; |
| 31 |
| 32 import android.content.Context; |
| 33 import android.graphics.Point; |
| 34 import android.graphics.SurfaceTexture; |
| 35 import android.opengl.EGLContext; |
| 36 import android.opengl.GLES20; |
| 37 import android.opengl.Matrix; |
| 38 import android.os.Handler; |
| 39 import android.os.HandlerThread; |
| 40 import android.util.AttributeSet; |
| 41 import android.util.Log; |
| 42 import android.view.SurfaceHolder; |
| 43 import android.view.SurfaceView; |
| 44 |
| 45 /** |
| 46 * Implements org.webrtc.VideoRenderer.Callbacks by displaying the video stream
on a SurfaceView. |
| 47 * renderFrame() is asynchronous to avoid blocking the calling thread. |
| 48 * This class is thread safe and handles access from potentially four different
threads: |
| 49 * Interaction from the main app in init, release, setMirror, and setScalingtype
. |
| 50 * Interaction from C++ webrtc::VideoRendererInterface in renderFrame and canApp
lyRotation. |
| 51 * Interaction from the Activity lifecycle in surfaceCreated, surfaceChanged, an
d surfaceDestroyed. |
| 52 * Interaction with the layout framework in onMeasure and onSizeChanged. |
| 53 */ |
| 54 public class SurfaceViewRenderer extends SurfaceView |
| 55 implements SurfaceHolder.Callback, VideoRenderer.Callbacks { |
| 56 private static final String TAG = "SurfaceViewRenderer"; |
| 57 |
| 58 // These variables are synchronized on |threadLock|. |
| 59 private final Object threadLock = new Object(); |
| 60 // Dedicated render thread. |
| 61 private HandlerThread renderThread; |
| 62 // Handler for inter-thread communication. |
| 63 private Handler renderThreadHandler; |
| 64 |
| 65 // EGL and GL resources for drawing YUV/OES textures. After initilization, the
se are only accessed |
| 66 // from the render thread. |
| 67 private EglBase eglBase; |
| 68 private GlRectDrawer drawer; |
| 69 // Texture ids for YUV frames. Allocated on first arrival of a YUV frame. |
| 70 private int[] yuvTextures = null; |
| 71 |
| 72 // Pending frame to render. Serves as a queue with size 1. Synchronized on |fr
ameLock|. |
| 73 private final Object frameLock = new Object(); |
| 74 private VideoRenderer.I420Frame pendingFrame; |
| 75 |
| 76 // These variables are synchronized on |layoutLock|. |
| 77 private final Object layoutLock = new Object(); |
| 78 // These three different dimension values are used to keep track of the state
in these functions: |
| 79 // requestLayout() -> onMeasure() -> onLayout() -> surfaceChanged(). |
| 80 // requestLayout() is triggered internally by frame size changes, but can also
be triggered |
| 81 // externally by layout update requests. |
| 82 // Most recent measurement specification from onMeasure(). |
| 83 private int widthSpec; |
| 84 private int heightSpec; |
| 85 // Current size on screen in pixels. Updated in onLayout(), and should be cons
istent with |
| 86 // |widthSpec|/|heightSpec| after that. |
| 87 private int layoutWidth; |
| 88 private int layoutHeight; |
| 89 // Current surface size of the underlying Surface. Updated in surfaceChanged()
, and should be |
| 90 // consistent with |layoutWidth|/|layoutHeight| after that. |
| 91 // TODO(magjed): Enable hardware scaler with SurfaceHolder.setFixedSize(). Thi
s will decouple |
| 92 // layout and surface size. |
| 93 private int surfaceWidth; |
| 94 private int surfaceHeight; |
| 95 // Last rendered frame dimensions, or 0 if no frame has been rendered yet. |
| 96 private int frameWidth; |
| 97 private int frameHeight; |
| 98 private int frameRotation; |
| 99 // |scalingType| determines how the video will fill the allowed layout area in
onMeasure(). |
| 100 private RendererCommon.ScalingType scalingType = RendererCommon.ScalingType.SC
ALE_ASPECT_BALANCED; |
| 101 // If true, mirrors the video stream horizontally. |
| 102 private boolean mirror; |
| 103 // Callback for reporting renderer events. |
| 104 private RendererCommon.RendererEvents rendererEvents; |
| 105 |
| 106 // These variables are synchronized on |statisticsLock|. |
| 107 private final Object statisticsLock = new Object(); |
| 108 // Total number of video frames received in renderFrame() call. |
| 109 private int framesReceived; |
| 110 // Number of video frames dropped by renderFrame() because previous frame has
not been rendered |
| 111 // yet. |
| 112 private int framesDropped; |
| 113 // Number of rendered video frames. |
| 114 private int framesRendered; |
| 115 // Time in ns when the first video frame was rendered. |
| 116 private long firstFrameTimeNs; |
| 117 // Time in ns spent in renderFrameOnRenderThread() function. |
| 118 private long renderTimeNs; |
| 119 |
| 120 // Runnable for posting frames to render thread.. |
| 121 private final Runnable renderFrameRunnable = new Runnable() { |
| 122 @Override public void run() { |
| 123 renderFrameOnRenderThread(); |
| 124 } |
| 125 }; |
| 126 |
| 127 /** |
| 128 * Standard View constructor. In order to render something, you must first cal
l init(). |
| 129 */ |
| 130 public SurfaceViewRenderer(Context context) { |
| 131 super(context); |
| 132 } |
| 133 |
| 134 /** |
| 135 * Standard View constructor. In order to render something, you must first cal
l init(). |
| 136 */ |
| 137 public SurfaceViewRenderer(Context context, AttributeSet attrs) { |
| 138 super(context, attrs); |
| 139 } |
| 140 |
| 141 /** |
| 142 * Initialize this class, sharing resources with |sharedContext|. |
| 143 */ |
| 144 public void init( |
| 145 EGLContext sharedContext, RendererCommon.RendererEvents rendererEvents) { |
| 146 if (renderThreadHandler != null) { |
| 147 throw new IllegalStateException("Already initialized"); |
| 148 } |
| 149 Log.d(TAG, "Initializing"); |
| 150 this.rendererEvents = rendererEvents; |
| 151 renderThread = new HandlerThread(TAG); |
| 152 renderThread.start(); |
| 153 renderThreadHandler = new Handler(renderThread.getLooper()); |
| 154 eglBase = new EglBase(sharedContext, EglBase.ConfigType.PLAIN); |
| 155 drawer = new GlRectDrawer(); |
| 156 getHolder().addCallback(this); |
| 157 } |
| 158 |
| 159 /** |
| 160 * Release all resources. This needs to be done manually, otherwise the resour
ces are leaked. You |
| 161 * should call this before the Activity is destroyed, while the EGLContext is
still valid. |
| 162 */ |
| 163 public void release() { |
| 164 synchronized (threadLock) { |
| 165 if (renderThreadHandler == null) { |
| 166 Log.d(TAG, "Already released"); |
| 167 return; |
| 168 } |
| 169 // Release EGL and GL resources on render thread. |
| 170 // TODO(magjed): This might not be necessary - all OpenGL resources are au
tomatically deleted |
| 171 // when the EGL context is lost. It might be dangerous to delete them manu
ally in |
| 172 // Activity.onDestroy(). |
| 173 renderThreadHandler.post(new Runnable() { |
| 174 @Override public void run() { |
| 175 drawer.release(); |
| 176 drawer = null; |
| 177 if (yuvTextures != null) { |
| 178 GLES20.glDeleteTextures(3, yuvTextures, 0); |
| 179 yuvTextures = null; |
| 180 } |
| 181 eglBase.release(); |
| 182 eglBase = null; |
| 183 } |
| 184 }); |
| 185 // Don't accept any more messages to the render thread. |
| 186 renderThreadHandler = null; |
| 187 // Quit safely to make sure the EGL/GL cleanup posted above is executed. |
| 188 renderThread.quitSafely(); |
| 189 renderThread = null; |
| 190 } |
| 191 getHolder().removeCallback(this); |
| 192 synchronized (frameLock) { |
| 193 if (pendingFrame != null) { |
| 194 VideoRenderer.renderFrameDone(pendingFrame); |
| 195 pendingFrame = null; |
| 196 } |
| 197 } |
| 198 } |
| 199 |
| 200 /** |
| 201 * Set if the video stream should be mirrored or not. |
| 202 */ |
| 203 public void setMirror(final boolean mirror) { |
| 204 synchronized (layoutLock) { |
| 205 this.mirror = mirror; |
| 206 } |
| 207 } |
| 208 |
| 209 /** |
| 210 * Set how the video will fill the allowed layout area. |
| 211 */ |
| 212 public void setScalingType(RendererCommon.ScalingType scalingType) { |
| 213 synchronized (layoutLock) { |
| 214 this.scalingType = scalingType; |
| 215 } |
| 216 } |
| 217 |
| 218 // VideoRenderer.Callbacks interface. |
| 219 @Override |
| 220 public void renderFrame(VideoRenderer.I420Frame frame) { |
| 221 synchronized (statisticsLock) { |
| 222 ++framesReceived; |
| 223 } |
| 224 synchronized (threadLock) { |
| 225 if (renderThreadHandler == null) { |
| 226 Log.d(TAG, "Dropping frame - SurfaceViewRenderer not initialized or alre
ady released."); |
| 227 } else { |
| 228 synchronized (frameLock) { |
| 229 if (pendingFrame == null) { |
| 230 updateFrameDimensionsAndReportEvents(frame); |
| 231 pendingFrame = frame; |
| 232 renderThreadHandler.post(renderFrameRunnable); |
| 233 return; |
| 234 } |
| 235 } |
| 236 } |
| 237 } |
| 238 // Drop frame. |
| 239 synchronized (statisticsLock) { |
| 240 ++framesDropped; |
| 241 } |
| 242 VideoRenderer.renderFrameDone(frame); |
| 243 } |
| 244 |
| 245 // Returns desired layout size given current measure specification and video a
spect ratio. |
| 246 private Point getDesiredLayoutSize() { |
| 247 synchronized (layoutLock) { |
| 248 final int maxWidth = getDefaultSize(Integer.MAX_VALUE, widthSpec); |
| 249 final int maxHeight = getDefaultSize(Integer.MAX_VALUE, heightSpec); |
| 250 final Point size = |
| 251 RendererCommon.getDisplaySize(scalingType, frameAspectRatio(), maxWidt
h, maxHeight); |
| 252 if (MeasureSpec.getMode(widthSpec) == MeasureSpec.EXACTLY) { |
| 253 size.x = maxWidth; |
| 254 } |
| 255 if (MeasureSpec.getMode(heightSpec) == MeasureSpec.EXACTLY) { |
| 256 size.y = maxHeight; |
| 257 } |
| 258 return size; |
| 259 } |
| 260 } |
| 261 |
| 262 // View layout interface. |
| 263 @Override |
| 264 protected void onMeasure(int widthSpec, int heightSpec) { |
| 265 synchronized (layoutLock) { |
| 266 this.widthSpec = widthSpec; |
| 267 this.heightSpec = heightSpec; |
| 268 } |
| 269 final Point size = getDesiredLayoutSize(); |
| 270 setMeasuredDimension(size.x, size.y); |
| 271 } |
| 272 |
| 273 @Override |
| 274 protected void onLayout(boolean changed, int left, int top, int right, int bot
tom) { |
| 275 synchronized (layoutLock) { |
| 276 layoutWidth = right - left; |
| 277 layoutHeight = bottom - top; |
| 278 } |
| 279 // Might have a pending frame waiting for a layout of correct size. |
| 280 runOnRenderThread(renderFrameRunnable); |
| 281 } |
| 282 |
| 283 // SurfaceHolder.Callback interface. |
| 284 @Override |
| 285 public void surfaceCreated(final SurfaceHolder holder) { |
| 286 Log.d(TAG, "Surface created"); |
| 287 runOnRenderThread(new Runnable() { |
| 288 @Override public void run() { |
| 289 eglBase.createSurface(holder.getSurface()); |
| 290 eglBase.makeCurrent(); |
| 291 // Necessary for YUV frames with odd width. |
| 292 GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1); |
| 293 } |
| 294 }); |
| 295 } |
| 296 |
| 297 @Override |
| 298 public void surfaceDestroyed(SurfaceHolder holder) { |
| 299 Log.d(TAG, "Surface destroyed"); |
| 300 synchronized (layoutLock) { |
| 301 surfaceWidth = 0; |
| 302 surfaceHeight = 0; |
| 303 } |
| 304 runOnRenderThread(new Runnable() { |
| 305 @Override public void run() { |
| 306 eglBase.releaseSurface(); |
| 307 } |
| 308 }); |
| 309 } |
| 310 |
| 311 @Override |
| 312 public void surfaceChanged(SurfaceHolder holder, int format, int width, int he
ight) { |
| 313 Log.d(TAG, "Surface changed: " + width + "x" + height); |
| 314 synchronized (layoutLock) { |
| 315 surfaceWidth = width; |
| 316 surfaceHeight = height; |
| 317 } |
| 318 // Might have a pending frame waiting for a surface of correct size. |
| 319 runOnRenderThread(renderFrameRunnable); |
| 320 } |
| 321 |
| 322 /** |
| 323 * Private helper function to post tasks safely. |
| 324 */ |
| 325 private void runOnRenderThread(Runnable runnable) { |
| 326 synchronized (threadLock) { |
| 327 if (renderThreadHandler != null) { |
| 328 renderThreadHandler.post(runnable); |
| 329 } |
| 330 } |
| 331 } |
| 332 |
| 333 /** |
| 334 * Requests new layout if necessary. Returns true if layout and surface size a
re consistent. |
| 335 */ |
| 336 private boolean checkConsistentLayout() { |
| 337 synchronized (layoutLock) { |
| 338 final Point desiredLayoutSize = getDesiredLayoutSize(); |
| 339 if (desiredLayoutSize.x != layoutWidth || desiredLayoutSize.y != layoutHei
ght) { |
| 340 Log.d(TAG, "Requesting new layout with size: " |
| 341 + desiredLayoutSize.x + "x" + desiredLayoutSize.y); |
| 342 // Request layout update on UI thread. |
| 343 post(new Runnable() { |
| 344 @Override public void run() { |
| 345 requestLayout(); |
| 346 } |
| 347 }); |
| 348 return false; |
| 349 } |
| 350 // Wait for requestLayout() to propagate through this sequence before retu
rning true: |
| 351 // requestLayout() -> onMeasure() -> onLayout() -> surfaceChanged(). |
| 352 return surfaceWidth == layoutWidth && surfaceHeight == layoutHeight; |
| 353 } |
| 354 } |
| 355 |
| 356 /** |
| 357 * Renders and releases |pendingFrame|. |
| 358 */ |
| 359 private void renderFrameOnRenderThread() { |
| 360 if (eglBase == null || !eglBase.hasSurface()) { |
| 361 Log.d(TAG, "No surface to draw on"); |
| 362 return; |
| 363 } |
| 364 if (!checkConsistentLayout()) { |
| 365 // Output intermediate black frames while the layout is updated. |
| 366 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); |
| 367 eglBase.swapBuffers(); |
| 368 return; |
| 369 } |
| 370 // After a surface size change, the EGLSurface might still have a buffer of
the old size in the |
| 371 // pipeline. Querying the EGLSurface will show if the underlying buffer dime
nsions haven't yet |
| 372 // changed. Such a buffer will be rendered incorrectly, so flush it with a b
lack frame. |
| 373 synchronized (layoutLock) { |
| 374 if (eglBase.surfaceWidth() != surfaceWidth || eglBase.surfaceHeight() != s
urfaceHeight) { |
| 375 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); |
| 376 eglBase.swapBuffers(); |
| 377 } |
| 378 } |
| 379 // Fetch and render |pendingFrame|. |
| 380 final VideoRenderer.I420Frame frame; |
| 381 synchronized (frameLock) { |
| 382 if (pendingFrame == null) { |
| 383 return; |
| 384 } |
| 385 frame = pendingFrame; |
| 386 pendingFrame = null; |
| 387 } |
| 388 |
| 389 final long startTimeNs = System.nanoTime(); |
| 390 if (!frame.yuvFrame) { |
| 391 // TODO(magjed): Move updateTexImage() to the video source instead. |
| 392 SurfaceTexture surfaceTexture = (SurfaceTexture) frame.textureObject; |
| 393 surfaceTexture.updateTexImage(); |
| 394 } |
| 395 |
| 396 final float[] texMatrix = new float[16]; |
| 397 synchronized (layoutLock) { |
| 398 final float[] samplingMatrix = RendererCommon.getSamplingMatrix( |
| 399 (SurfaceTexture) frame.textureObject, frame.rotationDegree); |
| 400 final float[] layoutMatrix = RendererCommon.getLayoutMatrix( |
| 401 mirror, frameAspectRatio(), (float) layoutWidth / layoutHeight); |
| 402 Matrix.multiplyMM(texMatrix, 0, samplingMatrix, 0, layoutMatrix, 0); |
| 403 } |
| 404 |
| 405 GLES20.glViewport(0, 0, surfaceWidth, surfaceHeight); |
| 406 if (frame.yuvFrame) { |
| 407 // Make sure YUV textures are allocated. |
| 408 if (yuvTextures == null) { |
| 409 yuvTextures = new int[3]; |
| 410 for (int i = 0; i < 3; i++) { |
| 411 yuvTextures[i] = GlUtil.generateTexture(GLES20.GL_TEXTURE_2D); |
| 412 } |
| 413 } |
| 414 drawer.uploadYuvData( |
| 415 yuvTextures, frame.width, frame.height, frame.yuvStrides, frame.yuvPla
nes); |
| 416 drawer.drawYuv(yuvTextures, texMatrix); |
| 417 } else { |
| 418 drawer.drawOes(frame.textureId, texMatrix); |
| 419 } |
| 420 |
| 421 eglBase.swapBuffers(); |
| 422 VideoRenderer.renderFrameDone(frame); |
| 423 synchronized (statisticsLock) { |
| 424 if (framesRendered == 0) { |
| 425 firstFrameTimeNs = startTimeNs; |
| 426 } |
| 427 ++framesRendered; |
| 428 renderTimeNs += (System.nanoTime() - startTimeNs); |
| 429 if (framesRendered % 300 == 0) { |
| 430 logStatistics(); |
| 431 } |
| 432 } |
| 433 } |
| 434 |
| 435 // Return current frame aspect ratio, taking rotation into account. |
| 436 private float frameAspectRatio() { |
| 437 synchronized (layoutLock) { |
| 438 if (frameWidth == 0 || frameHeight == 0) { |
| 439 return 0.0f; |
| 440 } |
| 441 return (frameRotation % 180 == 0) ? (float) frameWidth / frameHeight |
| 442 : (float) frameHeight / frameWidth; |
| 443 } |
| 444 } |
| 445 |
| 446 // Update frame dimensions and report any changes to |rendererEvents|. |
| 447 private void updateFrameDimensionsAndReportEvents(VideoRenderer.I420Frame fram
e) { |
| 448 synchronized (layoutLock) { |
| 449 if (frameWidth != frame.width || frameHeight != frame.height |
| 450 || frameRotation != frame.rotationDegree) { |
| 451 if (rendererEvents != null) { |
| 452 final String id = getResources().getResourceEntryName(getId()); |
| 453 if (frameWidth == 0 || frameHeight == 0) { |
| 454 Log.d(TAG, "ID: " + id + ". Reporting first rendered frame."); |
| 455 rendererEvents.onFirstFrameRendered(); |
| 456 } |
| 457 Log.d(TAG, "ID: " + id + ". Reporting frame resolution changed to " |
| 458 + frame.width + "x" + frame.height + " with rotation " + frame.rot
ationDegree); |
| 459 rendererEvents.onFrameResolutionChanged(frame.width, frame.height, fra
me.rotationDegree); |
| 460 } |
| 461 frameWidth = frame.width; |
| 462 frameHeight = frame.height; |
| 463 frameRotation = frame.rotationDegree; |
| 464 } |
| 465 } |
| 466 } |
| 467 |
| 468 private void logStatistics() { |
| 469 synchronized (statisticsLock) { |
| 470 Log.d(TAG, "ID: " + getResources().getResourceEntryName(getId()) + ". Fram
es received: " |
| 471 + framesReceived + ". Dropped: " + framesDropped + ". Rendered: " + fr
amesRendered); |
| 472 if (framesReceived > 0 && framesRendered > 0) { |
| 473 final long timeSinceFirstFrameNs = System.nanoTime() - firstFrameTimeNs; |
| 474 Log.d(TAG, "Duration: " + (int) (timeSinceFirstFrameNs / 1e6) + |
| 475 " ms. FPS: " + (float) framesRendered * 1e9 / timeSinceFirstFrameNs)
; |
| 476 Log.d(TAG, "Average render time: " |
| 477 + (int) (renderTimeNs / (1000 * framesRendered)) + " us."); |
| 478 } |
| 479 } |
| 480 } |
| 481 } |
OLD | NEW |