Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(378)

Side by Side Diff: webrtc/api/android/java/src/org/webrtc/SurfaceViewRenderer.java

Issue 2106333005: Revert of Combine webrtc/api/java/android and webrtc/api/java/src. (Closed) Base URL: https://chromium.googlesource.com/external/webrtc.git@master
Patch Set: Created 4 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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
11 package org.webrtc;
12
13 import android.content.Context;
14 import android.content.res.Resources.NotFoundException;
15 import android.graphics.Point;
16 import android.opengl.GLES20;
17 import android.os.Handler;
18 import android.os.HandlerThread;
19 import android.util.AttributeSet;
20 import android.view.SurfaceHolder;
21 import android.view.SurfaceView;
22
23 import org.webrtc.Logging;
24
25 import java.util.concurrent.CountDownLatch;
26
27 import javax.microedition.khronos.egl.EGLContext;
28
29 /**
30 * Implements org.webrtc.VideoRenderer.Callbacks by displaying the video stream on a SurfaceView.
31 * renderFrame() is asynchronous to avoid blocking the calling thread.
32 * This class is thread safe and handles access from potentially four different threads:
33 * Interaction from the main app in init, release, setMirror, and setScalingtype .
34 * Interaction from C++ rtc::VideoSinkInterface in renderFrame.
35 * Interaction from the Activity lifecycle in surfaceCreated, surfaceChanged, an d surfaceDestroyed.
36 * Interaction with the layout framework in onMeasure and onSizeChanged.
37 */
38 public class SurfaceViewRenderer extends SurfaceView
39 implements SurfaceHolder.Callback, VideoRenderer.Callbacks {
40 private static final String TAG = "SurfaceViewRenderer";
41
42 // Dedicated render thread.
43 private HandlerThread renderThread;
44 // |renderThreadHandler| is a handler for communicating with |renderThread|, a nd is synchronized
45 // on |handlerLock|.
46 private final Object handlerLock = new Object();
47 private Handler renderThreadHandler;
48
49 // EGL and GL resources for drawing YUV/OES textures. After initilization, the se are only accessed
50 // from the render thread.
51 private EglBase eglBase;
52 private final RendererCommon.YuvUploader yuvUploader = new RendererCommon.YuvU ploader();
53 private RendererCommon.GlDrawer drawer;
54 // Texture ids for YUV frames. Allocated on first arrival of a YUV frame.
55 private int[] yuvTextures = null;
56
57 // Pending frame to render. Serves as a queue with size 1. Synchronized on |fr ameLock|.
58 private final Object frameLock = new Object();
59 private VideoRenderer.I420Frame pendingFrame;
60
61 // These variables are synchronized on |layoutLock|.
62 private final Object layoutLock = new Object();
63 // These dimension values are used to keep track of the state in these functio ns: onMeasure(),
64 // onLayout(), and surfaceChanged(). A new layout is triggered with requestLay out(). This happens
65 // internally when the incoming frame size changes. requestLayout() can also b e triggered
66 // externally. The layout change is a two pass process: first onMeasure() is c alled in a top-down
67 // traversal of the View tree, followed by an onLayout() pass that is also top -down. During the
68 // onLayout() pass, each parent is responsible for positioning its children us ing the sizes
69 // computed in the measure pass.
70 // |desiredLayoutsize| is the layout size we have requested in onMeasure() and are waiting for to
71 // take effect.
72 private Point desiredLayoutSize = new Point();
73 // |layoutSize|/|surfaceSize| is the actual current layout/surface size. They are updated in
74 // onLayout() and surfaceChanged() respectively.
75 private final Point layoutSize = new Point();
76 // TODO(magjed): Enable hardware scaler with SurfaceHolder.setFixedSize(). Thi s will decouple
77 // layout and surface size.
78 private final Point surfaceSize = new Point();
79 // |isSurfaceCreated| keeps track of the current status in surfaceCreated()/su rfaceDestroyed().
80 private boolean isSurfaceCreated;
81 // Last rendered frame dimensions, or 0 if no frame has been rendered yet.
82 private int frameWidth;
83 private int frameHeight;
84 private int frameRotation;
85 // |scalingType| determines how the video will fill the allowed layout area in onMeasure().
86 private RendererCommon.ScalingType scalingType = RendererCommon.ScalingType.SC ALE_ASPECT_BALANCED;
87 // If true, mirrors the video stream horizontally.
88 private boolean mirror;
89 // Callback for reporting renderer events.
90 private RendererCommon.RendererEvents rendererEvents;
91
92 // These variables are synchronized on |statisticsLock|.
93 private final Object statisticsLock = new Object();
94 // Total number of video frames received in renderFrame() call.
95 private int framesReceived;
96 // Number of video frames dropped by renderFrame() because previous frame has not been rendered
97 // yet.
98 private int framesDropped;
99 // Number of rendered video frames.
100 private int framesRendered;
101 // Time in ns when the first video frame was rendered.
102 private long firstFrameTimeNs;
103 // Time in ns spent in renderFrameOnRenderThread() function.
104 private long renderTimeNs;
105
106 // Runnable for posting frames to render thread.
107 private final Runnable renderFrameRunnable = new Runnable() {
108 @Override public void run() {
109 renderFrameOnRenderThread();
110 }
111 };
112 // Runnable for clearing Surface to black.
113 private final Runnable makeBlackRunnable = new Runnable() {
114 @Override public void run() {
115 makeBlack();
116 }
117 };
118
119 /**
120 * Standard View constructor. In order to render something, you must first cal l init().
121 */
122 public SurfaceViewRenderer(Context context) {
123 super(context);
124 getHolder().addCallback(this);
125 }
126
127 /**
128 * Standard View constructor. In order to render something, you must first cal l init().
129 */
130 public SurfaceViewRenderer(Context context, AttributeSet attrs) {
131 super(context, attrs);
132 getHolder().addCallback(this);
133 }
134
135 /**
136 * Initialize this class, sharing resources with |sharedContext|. It is allowe d to call init() to
137 * reinitialize the renderer after a previous init()/release() cycle.
138 */
139 public void init(
140 EglBase.Context sharedContext, RendererCommon.RendererEvents rendererEvent s) {
141 init(sharedContext, rendererEvents, EglBase.CONFIG_PLAIN, new GlRectDrawer() );
142 }
143
144 /**
145 * Initialize this class, sharing resources with |sharedContext|. The custom | drawer| will be used
146 * for drawing frames on the EGLSurface. This class is responsible for calling release() on
147 * |drawer|. It is allowed to call init() to reinitialize the renderer after a previous
148 * init()/release() cycle.
149 */
150 public void init(EglBase.Context sharedContext, RendererCommon.RendererEvents rendererEvents,
151 int[] configAttributes, RendererCommon.GlDrawer drawer) {
152 synchronized (handlerLock) {
153 if (renderThreadHandler != null) {
154 throw new IllegalStateException(getResourceName() + "Already initialized ");
155 }
156 Logging.d(TAG, getResourceName() + "Initializing.");
157 this.rendererEvents = rendererEvents;
158 this.drawer = drawer;
159 renderThread = new HandlerThread(TAG);
160 renderThread.start();
161 eglBase = EglBase.create(sharedContext, configAttributes);
162 renderThreadHandler = new Handler(renderThread.getLooper());
163 }
164 tryCreateEglSurface();
165 }
166
167 /**
168 * Create and make an EGLSurface current if both init() and surfaceCreated() h ave been called.
169 */
170 public void tryCreateEglSurface() {
171 // |renderThreadHandler| is only created after |eglBase| is created in init( ), so the
172 // following code will only execute if eglBase != null.
173 runOnRenderThread(new Runnable() {
174 @Override public void run() {
175 synchronized (layoutLock) {
176 if (isSurfaceCreated && !eglBase.hasSurface()) {
177 eglBase.createSurface(getHolder().getSurface());
178 eglBase.makeCurrent();
179 // Necessary for YUV frames with odd width.
180 GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
181 }
182 }
183 }
184 });
185 }
186
187 /**
188 * Block until any pending frame is returned and all GL resources released, ev en if an interrupt
189 * occurs. If an interrupt occurs during release(), the interrupt flag will be set. This function
190 * should be called before the Activity is destroyed and the EGLContext is sti ll valid. If you
191 * don't call this function, the GL resources might leak.
192 */
193 public void release() {
194 final CountDownLatch eglCleanupBarrier = new CountDownLatch(1);
195 synchronized (handlerLock) {
196 if (renderThreadHandler == null) {
197 Logging.d(TAG, getResourceName() + "Already released");
198 return;
199 }
200 // Release EGL and GL resources on render thread.
201 // TODO(magjed): This might not be necessary - all OpenGL resources are au tomatically deleted
202 // when the EGL context is lost. It might be dangerous to delete them manu ally in
203 // Activity.onDestroy().
204 renderThreadHandler.postAtFrontOfQueue(new Runnable() {
205 @Override public void run() {
206 drawer.release();
207 drawer = null;
208 if (yuvTextures != null) {
209 GLES20.glDeleteTextures(3, yuvTextures, 0);
210 yuvTextures = null;
211 }
212 // Clear last rendered image to black.
213 makeBlack();
214 eglBase.release();
215 eglBase = null;
216 eglCleanupBarrier.countDown();
217 }
218 });
219 // Don't accept any more frames or messages to the render thread.
220 renderThreadHandler = null;
221 }
222 // Make sure the EGL/GL cleanup posted above is executed.
223 ThreadUtils.awaitUninterruptibly(eglCleanupBarrier);
224 renderThread.quit();
225 synchronized (frameLock) {
226 if (pendingFrame != null) {
227 VideoRenderer.renderFrameDone(pendingFrame);
228 pendingFrame = null;
229 }
230 }
231 // The |renderThread| cleanup is not safe to cancel and we need to wait unti l it's done.
232 ThreadUtils.joinUninterruptibly(renderThread);
233 renderThread = null;
234 // Reset statistics and event reporting.
235 synchronized (layoutLock) {
236 frameWidth = 0;
237 frameHeight = 0;
238 frameRotation = 0;
239 rendererEvents = null;
240 }
241 resetStatistics();
242 }
243
244 /**
245 * Reset statistics. This will reset the logged statistics in logStatistics(), and
246 * RendererEvents.onFirstFrameRendered() will be called for the next frame.
247 */
248 public void resetStatistics() {
249 synchronized (statisticsLock) {
250 framesReceived = 0;
251 framesDropped = 0;
252 framesRendered = 0;
253 firstFrameTimeNs = 0;
254 renderTimeNs = 0;
255 }
256 }
257
258 /**
259 * Set if the video stream should be mirrored or not.
260 */
261 public void setMirror(final boolean mirror) {
262 synchronized (layoutLock) {
263 this.mirror = mirror;
264 }
265 }
266
267 /**
268 * Set how the video will fill the allowed layout area.
269 */
270 public void setScalingType(RendererCommon.ScalingType scalingType) {
271 synchronized (layoutLock) {
272 this.scalingType = scalingType;
273 }
274 }
275
276 // VideoRenderer.Callbacks interface.
277 @Override
278 public void renderFrame(VideoRenderer.I420Frame frame) {
279 synchronized (statisticsLock) {
280 ++framesReceived;
281 }
282 synchronized (handlerLock) {
283 if (renderThreadHandler == null) {
284 Logging.d(TAG, getResourceName()
285 + "Dropping frame - Not initialized or already released.");
286 VideoRenderer.renderFrameDone(frame);
287 return;
288 }
289 synchronized (frameLock) {
290 if (pendingFrame != null) {
291 // Drop old frame.
292 synchronized (statisticsLock) {
293 ++framesDropped;
294 }
295 VideoRenderer.renderFrameDone(pendingFrame);
296 }
297 pendingFrame = frame;
298 updateFrameDimensionsAndReportEvents(frame);
299 renderThreadHandler.post(renderFrameRunnable);
300 }
301 }
302 }
303
304 // Returns desired layout size given current measure specification and video a spect ratio.
305 private Point getDesiredLayoutSize(int widthSpec, int heightSpec) {
306 synchronized (layoutLock) {
307 final int maxWidth = getDefaultSize(Integer.MAX_VALUE, widthSpec);
308 final int maxHeight = getDefaultSize(Integer.MAX_VALUE, heightSpec);
309 final Point size =
310 RendererCommon.getDisplaySize(scalingType, frameAspectRatio(), maxWidt h, maxHeight);
311 if (MeasureSpec.getMode(widthSpec) == MeasureSpec.EXACTLY) {
312 size.x = maxWidth;
313 }
314 if (MeasureSpec.getMode(heightSpec) == MeasureSpec.EXACTLY) {
315 size.y = maxHeight;
316 }
317 return size;
318 }
319 }
320
321 // View layout interface.
322 @Override
323 protected void onMeasure(int widthSpec, int heightSpec) {
324 synchronized (layoutLock) {
325 if (frameWidth == 0 || frameHeight == 0) {
326 super.onMeasure(widthSpec, heightSpec);
327 return;
328 }
329 desiredLayoutSize = getDesiredLayoutSize(widthSpec, heightSpec);
330 if (desiredLayoutSize.x != getMeasuredWidth() || desiredLayoutSize.y != ge tMeasuredHeight()) {
331 // Clear the surface asap before the layout change to avoid stretched vi deo and other
332 // render artifacs. Don't wait for it to finish because the IO thread sh ould never be
333 // blocked, so it's a best-effort attempt.
334 synchronized (handlerLock) {
335 if (renderThreadHandler != null) {
336 renderThreadHandler.postAtFrontOfQueue(makeBlackRunnable);
337 }
338 }
339 }
340 setMeasuredDimension(desiredLayoutSize.x, desiredLayoutSize.y);
341 }
342 }
343
344 @Override
345 protected void onLayout(boolean changed, int left, int top, int right, int bot tom) {
346 synchronized (layoutLock) {
347 layoutSize.x = right - left;
348 layoutSize.y = bottom - top;
349 }
350 // Might have a pending frame waiting for a layout of correct size.
351 runOnRenderThread(renderFrameRunnable);
352 }
353
354 // SurfaceHolder.Callback interface.
355 @Override
356 public void surfaceCreated(final SurfaceHolder holder) {
357 Logging.d(TAG, getResourceName() + "Surface created.");
358 synchronized (layoutLock) {
359 isSurfaceCreated = true;
360 }
361 tryCreateEglSurface();
362 }
363
364 @Override
365 public void surfaceDestroyed(SurfaceHolder holder) {
366 Logging.d(TAG, getResourceName() + "Surface destroyed.");
367 synchronized (layoutLock) {
368 isSurfaceCreated = false;
369 surfaceSize.x = 0;
370 surfaceSize.y = 0;
371 }
372 runOnRenderThread(new Runnable() {
373 @Override public void run() {
374 eglBase.releaseSurface();
375 }
376 });
377 }
378
379 @Override
380 public void surfaceChanged(SurfaceHolder holder, int format, int width, int he ight) {
381 Logging.d(TAG, getResourceName() + "Surface changed: " + width + "x" + heigh t);
382 synchronized (layoutLock) {
383 surfaceSize.x = width;
384 surfaceSize.y = height;
385 }
386 // Might have a pending frame waiting for a surface of correct size.
387 runOnRenderThread(renderFrameRunnable);
388 }
389
390 /**
391 * Private helper function to post tasks safely.
392 */
393 private void runOnRenderThread(Runnable runnable) {
394 synchronized (handlerLock) {
395 if (renderThreadHandler != null) {
396 renderThreadHandler.post(runnable);
397 }
398 }
399 }
400
401 private String getResourceName() {
402 try {
403 return getResources().getResourceEntryName(getId()) + ": ";
404 } catch (NotFoundException e) {
405 return "";
406 }
407 }
408
409 private void makeBlack() {
410 if (Thread.currentThread() != renderThread) {
411 throw new IllegalStateException(getResourceName() + "Wrong thread.");
412 }
413 if (eglBase != null && eglBase.hasSurface()) {
414 GLES20.glClearColor(0, 0, 0, 0);
415 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
416 eglBase.swapBuffers();
417 }
418 }
419
420 /**
421 * Requests new layout if necessary. Returns true if layout and surface size a re consistent.
422 */
423 private boolean checkConsistentLayout() {
424 if (Thread.currentThread() != renderThread) {
425 throw new IllegalStateException(getResourceName() + "Wrong thread.");
426 }
427 synchronized (layoutLock) {
428 // Return false while we are in the middle of a layout change.
429 return layoutSize.equals(desiredLayoutSize) && surfaceSize.equals(layoutSi ze);
430 }
431 }
432
433 /**
434 * Renders and releases |pendingFrame|.
435 */
436 private void renderFrameOnRenderThread() {
437 if (Thread.currentThread() != renderThread) {
438 throw new IllegalStateException(getResourceName() + "Wrong thread.");
439 }
440 // Fetch and render |pendingFrame|.
441 final VideoRenderer.I420Frame frame;
442 synchronized (frameLock) {
443 if (pendingFrame == null) {
444 return;
445 }
446 frame = pendingFrame;
447 pendingFrame = null;
448 }
449 if (eglBase == null || !eglBase.hasSurface()) {
450 Logging.d(TAG, getResourceName() + "No surface to draw on");
451 VideoRenderer.renderFrameDone(frame);
452 return;
453 }
454 if (!checkConsistentLayout()) {
455 // Output intermediate black frames while the layout is updated.
456 makeBlack();
457 VideoRenderer.renderFrameDone(frame);
458 return;
459 }
460 // After a surface size change, the EGLSurface might still have a buffer of the old size in the
461 // pipeline. Querying the EGLSurface will show if the underlying buffer dime nsions haven't yet
462 // changed. Such a buffer will be rendered incorrectly, so flush it with a b lack frame.
463 synchronized (layoutLock) {
464 if (eglBase.surfaceWidth() != surfaceSize.x || eglBase.surfaceHeight() != surfaceSize.y) {
465 makeBlack();
466 }
467 }
468
469 final long startTimeNs = System.nanoTime();
470 final float[] texMatrix;
471 synchronized (layoutLock) {
472 final float[] rotatedSamplingMatrix =
473 RendererCommon.rotateTextureMatrix(frame.samplingMatrix, frame.rotatio nDegree);
474 final float[] layoutMatrix = RendererCommon.getLayoutMatrix(
475 mirror, frameAspectRatio(), (float) layoutSize.x / layoutSize.y);
476 texMatrix = RendererCommon.multiplyMatrices(rotatedSamplingMatrix, layoutM atrix);
477 }
478
479 // TODO(magjed): glClear() shouldn't be necessary since every pixel is cover ed anyway, but it's
480 // a workaround for bug 5147. Performance will be slightly worse.
481 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
482 if (frame.yuvFrame) {
483 // Make sure YUV textures are allocated.
484 if (yuvTextures == null) {
485 yuvTextures = new int[3];
486 for (int i = 0; i < 3; i++) {
487 yuvTextures[i] = GlUtil.generateTexture(GLES20.GL_TEXTURE_2D);
488 }
489 }
490 yuvUploader.uploadYuvData(
491 yuvTextures, frame.width, frame.height, frame.yuvStrides, frame.yuvPla nes);
492 drawer.drawYuv(yuvTextures, texMatrix, frame.rotatedWidth(), frame.rotated Height(),
493 0, 0, surfaceSize.x, surfaceSize.y);
494 } else {
495 drawer.drawOes(frame.textureId, texMatrix, frame.rotatedWidth(), frame.rot atedHeight(),
496 0, 0, surfaceSize.x, surfaceSize.y);
497 }
498
499 eglBase.swapBuffers();
500 VideoRenderer.renderFrameDone(frame);
501 synchronized (statisticsLock) {
502 if (framesRendered == 0) {
503 firstFrameTimeNs = startTimeNs;
504 synchronized (layoutLock) {
505 Logging.d(TAG, getResourceName() + "Reporting first rendered frame.");
506 if (rendererEvents != null) {
507 rendererEvents.onFirstFrameRendered();
508 }
509 }
510 }
511 ++framesRendered;
512 renderTimeNs += (System.nanoTime() - startTimeNs);
513 if (framesRendered % 300 == 0) {
514 logStatistics();
515 }
516 }
517 }
518
519 // Return current frame aspect ratio, taking rotation into account.
520 private float frameAspectRatio() {
521 synchronized (layoutLock) {
522 if (frameWidth == 0 || frameHeight == 0) {
523 return 0.0f;
524 }
525 return (frameRotation % 180 == 0) ? (float) frameWidth / frameHeight
526 : (float) frameHeight / frameWidth;
527 }
528 }
529
530 // Update frame dimensions and report any changes to |rendererEvents|.
531 private void updateFrameDimensionsAndReportEvents(VideoRenderer.I420Frame fram e) {
532 synchronized (layoutLock) {
533 if (frameWidth != frame.width || frameHeight != frame.height
534 || frameRotation != frame.rotationDegree) {
535 Logging.d(TAG, getResourceName() + "Reporting frame resolution changed t o "
536 + frame.width + "x" + frame.height + " with rotation " + frame.rotat ionDegree);
537 if (rendererEvents != null) {
538 rendererEvents.onFrameResolutionChanged(frame.width, frame.height, fra me.rotationDegree);
539 }
540 frameWidth = frame.width;
541 frameHeight = frame.height;
542 frameRotation = frame.rotationDegree;
543 post(new Runnable() {
544 @Override public void run() {
545 requestLayout();
546 }
547 });
548 }
549 }
550 }
551
552 private void logStatistics() {
553 synchronized (statisticsLock) {
554 Logging.d(TAG, getResourceName() + "Frames received: "
555 + framesReceived + ". Dropped: " + framesDropped + ". Rendered: " + fr amesRendered);
556 if (framesReceived > 0 && framesRendered > 0) {
557 final long timeSinceFirstFrameNs = System.nanoTime() - firstFrameTimeNs;
558 Logging.d(TAG, getResourceName() + "Duration: " + (int) (timeSinceFirstF rameNs / 1e6) +
559 " ms. FPS: " + framesRendered * 1e9 / timeSinceFirstFrameNs);
560 Logging.d(TAG, getResourceName() + "Average render time: "
561 + (int) (renderTimeNs / (1000 * framesRendered)) + " us.");
562 }
563 }
564 }
565 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698