OLD | NEW |
| (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 } | |
OLD | NEW |