blob: f664b014e1e25134435d3f2ca92892980a77e5f3 [file] [log] [blame]
Magnus Jedvertc06a7162015-09-11 09:51:52 +02001/*
kjellanderb24317b2016-02-10 07:54:43 -08002 * Copyright 2015 The WebRTC project authors. All Rights Reserved.
Magnus Jedvertc06a7162015-09-11 09:51:52 +02003 *
kjellanderb24317b2016-02-10 07:54:43 -08004 * 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.
Magnus Jedvertc06a7162015-09-11 09:51:52 +02009 */
10
11package org.webrtc;
12
Magnus Jedvertc06a7162015-09-11 09:51:52 +020013import android.content.Context;
magjedb2d1c502015-11-11 03:06:42 -080014import android.content.res.Resources.NotFoundException;
Magnus Jedvertc06a7162015-09-11 09:51:52 +020015import android.graphics.Point;
Magnus Jedvertc06a7162015-09-11 09:51:52 +020016import android.util.AttributeSet;
Magnus Jedvertc06a7162015-09-11 09:51:52 +020017import android.view.SurfaceHolder;
18import android.view.SurfaceView;
sakal28ec6bd2016-11-09 01:47:12 -080019import java.util.concurrent.CountDownLatch;
Magnus Jedvertc06a7162015-09-11 09:51:52 +020020
21/**
22 * Implements org.webrtc.VideoRenderer.Callbacks by displaying the video stream on a SurfaceView.
23 * renderFrame() is asynchronous to avoid blocking the calling thread.
24 * This class is thread safe and handles access from potentially four different threads:
25 * Interaction from the main app in init, release, setMirror, and setScalingtype.
Niels Möller8f597622016-03-23 10:33:07 +010026 * Interaction from C++ rtc::VideoSinkInterface in renderFrame.
Magnus Jedvertc06a7162015-09-11 09:51:52 +020027 * Interaction from the Activity lifecycle in surfaceCreated, surfaceChanged, and surfaceDestroyed.
28 * Interaction with the layout framework in onMeasure and onSizeChanged.
29 */
sakalb6760f92016-09-29 04:12:44 -070030public class SurfaceViewRenderer
sakal6bdcefc2017-08-15 01:56:02 -070031 extends SurfaceView implements SurfaceHolder.Callback, VideoRenderer.Callbacks, VideoSink {
Magnus Jedvertc06a7162015-09-11 09:51:52 +020032 private static final String TAG = "SurfaceViewRenderer";
33
magjeddf494b02016-10-07 05:32:35 -070034 // Cached resource name.
35 private final String resourceName;
36 private final RendererCommon.VideoLayoutMeasure videoLayoutMeasure =
37 new RendererCommon.VideoLayoutMeasure();
38 private final EglRenderer eglRenderer;
Magnus Jedvertc06a7162015-09-11 09:51:52 +020039
magjeddf494b02016-10-07 05:32:35 -070040 // Callback for reporting renderer events. Read-only after initilization so no lock required.
41 private RendererCommon.RendererEvents rendererEvents;
Magnus Jedvertc06a7162015-09-11 09:51:52 +020042
Magnus Jedvertc06a7162015-09-11 09:51:52 +020043 private final Object layoutLock = new Object();
sakal521dffa2017-04-26 06:05:18 -070044 private boolean isRenderingPaused = false;
magjeddf494b02016-10-07 05:32:35 -070045 private boolean isFirstFrameRendered;
Magnus Jedvert62b1c352016-10-05 15:56:06 +020046 private int rotatedFrameWidth;
47 private int rotatedFrameHeight;
Magnus Jedvertc06a7162015-09-11 09:51:52 +020048 private int frameRotation;
Magnus Jedvertc06a7162015-09-11 09:51:52 +020049
sakalb1e6d5e2016-11-22 01:54:42 -080050 // Accessed only on the main thread.
51 private boolean enableFixedSize;
52 private int surfaceWidth;
53 private int surfaceHeight;
54
Magnus Jedvertc06a7162015-09-11 09:51:52 +020055 /**
56 * Standard View constructor. In order to render something, you must first call init().
57 */
58 public SurfaceViewRenderer(Context context) {
59 super(context);
magjeddf494b02016-10-07 05:32:35 -070060 this.resourceName = getResourceName();
61 eglRenderer = new EglRenderer(resourceName);
Magnus Jedvert4382d802015-10-08 14:45:34 +020062 getHolder().addCallback(this);
Magnus Jedvertc06a7162015-09-11 09:51:52 +020063 }
64
65 /**
66 * Standard View constructor. In order to render something, you must first call init().
67 */
68 public SurfaceViewRenderer(Context context, AttributeSet attrs) {
69 super(context, attrs);
magjeddf494b02016-10-07 05:32:35 -070070 this.resourceName = getResourceName();
71 eglRenderer = new EglRenderer(resourceName);
Magnus Jedvert4382d802015-10-08 14:45:34 +020072 getHolder().addCallback(this);
Magnus Jedvertc06a7162015-09-11 09:51:52 +020073 }
74
75 /**
Magnus Jedvert4382d802015-10-08 14:45:34 +020076 * Initialize this class, sharing resources with |sharedContext|. It is allowed to call init() to
77 * reinitialize the renderer after a previous init()/release() cycle.
Magnus Jedvertc06a7162015-09-11 09:51:52 +020078 */
sakalb6760f92016-09-29 04:12:44 -070079 public void init(EglBase.Context sharedContext, RendererCommon.RendererEvents rendererEvents) {
Magnus Jedvert51254332015-12-15 16:22:29 +010080 init(sharedContext, rendererEvents, EglBase.CONFIG_PLAIN, new GlRectDrawer());
81 }
82
83 /**
84 * Initialize this class, sharing resources with |sharedContext|. The custom |drawer| will be used
85 * for drawing frames on the EGLSurface. This class is responsible for calling release() on
86 * |drawer|. It is allowed to call init() to reinitialize the renderer after a previous
87 * init()/release() cycle.
88 */
sakalb6760f92016-09-29 04:12:44 -070089 public void init(final EglBase.Context sharedContext,
90 RendererCommon.RendererEvents rendererEvents, final int[] configAttributes,
91 RendererCommon.GlDrawer drawer) {
magjeddf494b02016-10-07 05:32:35 -070092 ThreadUtils.checkIsOnMainThread();
93 this.rendererEvents = rendererEvents;
94 synchronized (layoutLock) {
tserngff7acb12017-07-14 02:35:53 -070095 isFirstFrameRendered = false;
magjeddf494b02016-10-07 05:32:35 -070096 rotatedFrameWidth = 0;
97 rotatedFrameHeight = 0;
98 frameRotation = 0;
Magnus Jedvertc06a7162015-09-11 09:51:52 +020099 }
magjeddf494b02016-10-07 05:32:35 -0700100 eglRenderer.init(sharedContext, configAttributes, drawer);
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200101 }
102
103 /**
Magnus Jedvertc19922c2015-09-21 14:32:07 +0200104 * Block until any pending frame is returned and all GL resources released, even if an interrupt
105 * occurs. If an interrupt occurs during release(), the interrupt flag will be set. This function
106 * should be called before the Activity is destroyed and the EGLContext is still valid. If you
107 * don't call this function, the GL resources might leak.
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200108 */
109 public void release() {
magjeddf494b02016-10-07 05:32:35 -0700110 eglRenderer.release();
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200111 }
112
sakal3a9bc172016-11-30 08:30:05 -0800113 /**
114 * Register a callback to be invoked when a new video frame has been received.
115 *
sakald1516522017-03-13 05:11:48 -0700116 * @param listener The callback to be invoked. The callback will be invoked on the render thread.
117 * It should be lightweight and must not call removeFrameListener.
sakal3a9bc172016-11-30 08:30:05 -0800118 * @param scale The scale of the Bitmap passed to the callback, or 0 if no Bitmap is
119 * required.
120 * @param drawer Custom drawer to use for this frame listener.
121 */
122 public void addFrameListener(
sakald1516522017-03-13 05:11:48 -0700123 EglRenderer.FrameListener listener, float scale, RendererCommon.GlDrawer drawerParam) {
124 eglRenderer.addFrameListener(listener, scale, drawerParam);
sakal3a9bc172016-11-30 08:30:05 -0800125 }
126
127 /**
128 * Register a callback to be invoked when a new video frame has been received. This version uses
129 * the drawer of the EglRenderer that was passed in init.
130 *
sakald1516522017-03-13 05:11:48 -0700131 * @param listener The callback to be invoked. The callback will be invoked on the render thread.
132 * It should be lightweight and must not call removeFrameListener.
sakal3a9bc172016-11-30 08:30:05 -0800133 * @param scale The scale of the Bitmap passed to the callback, or 0 if no Bitmap is
134 * required.
135 */
sakal4fe3b8d2016-11-23 06:18:20 -0800136 public void addFrameListener(EglRenderer.FrameListener listener, float scale) {
137 eglRenderer.addFrameListener(listener, scale);
138 }
139
140 public void removeFrameListener(EglRenderer.FrameListener listener) {
141 eglRenderer.removeFrameListener(listener);
142 }
143
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200144 /**
sakalb1e6d5e2016-11-22 01:54:42 -0800145 * Enables fixed size for the surface. This provides better performance but might be buggy on some
146 * devices. By default this is turned off.
147 */
148 public void setEnableHardwareScaler(boolean enabled) {
149 ThreadUtils.checkIsOnMainThread();
150 enableFixedSize = enabled;
151 updateSurfaceSize();
152 }
153
154 /**
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200155 * Set if the video stream should be mirrored or not.
156 */
157 public void setMirror(final boolean mirror) {
magjeddf494b02016-10-07 05:32:35 -0700158 eglRenderer.setMirror(mirror);
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200159 }
160
161 /**
162 * Set how the video will fill the allowed layout area.
163 */
164 public void setScalingType(RendererCommon.ScalingType scalingType) {
Magnus Jedvert62b1c352016-10-05 15:56:06 +0200165 ThreadUtils.checkIsOnMainThread();
166 videoLayoutMeasure.setScalingType(scalingType);
sakalfcf97c32017-07-18 05:01:08 -0700167 requestLayout();
Magnus Jedvert62b1c352016-10-05 15:56:06 +0200168 }
169
170 public void setScalingType(RendererCommon.ScalingType scalingTypeMatchOrientation,
171 RendererCommon.ScalingType scalingTypeMismatchOrientation) {
172 ThreadUtils.checkIsOnMainThread();
173 videoLayoutMeasure.setScalingType(scalingTypeMatchOrientation, scalingTypeMismatchOrientation);
sakalfcf97c32017-07-18 05:01:08 -0700174 requestLayout();
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200175 }
176
sakal8b646282016-11-23 06:19:27 -0800177 /**
178 * Limit render framerate.
179 *
180 * @param fps Limit render framerate to this value, or use Float.POSITIVE_INFINITY to disable fps
181 * reduction.
182 */
183 public void setFpsReduction(float fps) {
sakal521dffa2017-04-26 06:05:18 -0700184 synchronized (layoutLock) {
185 isRenderingPaused = fps == 0f;
186 }
sakal8b646282016-11-23 06:19:27 -0800187 eglRenderer.setFpsReduction(fps);
188 }
189
190 public void disableFpsReduction() {
sakal521dffa2017-04-26 06:05:18 -0700191 synchronized (layoutLock) {
192 isRenderingPaused = false;
193 }
sakal8b646282016-11-23 06:19:27 -0800194 eglRenderer.disableFpsReduction();
195 }
196
197 public void pauseVideo() {
sakal521dffa2017-04-26 06:05:18 -0700198 synchronized (layoutLock) {
199 isRenderingPaused = true;
200 }
sakal8b646282016-11-23 06:19:27 -0800201 eglRenderer.pauseVideo();
202 }
203
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200204 // VideoRenderer.Callbacks interface.
205 @Override
206 public void renderFrame(VideoRenderer.I420Frame frame) {
magjeddf494b02016-10-07 05:32:35 -0700207 updateFrameDimensionsAndReportEvents(frame);
208 eglRenderer.renderFrame(frame);
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200209 }
210
sakal6bdcefc2017-08-15 01:56:02 -0700211 // VideoSink interface.
212 @Override
213 public void onFrame(VideoFrame frame) {
214 updateFrameDimensionsAndReportEvents(frame);
215 eglRenderer.onFrame(frame);
216 }
217
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200218 // View layout interface.
219 @Override
220 protected void onMeasure(int widthSpec, int heightSpec) {
Magnus Jedvert62b1c352016-10-05 15:56:06 +0200221 ThreadUtils.checkIsOnMainThread();
magjeddf494b02016-10-07 05:32:35 -0700222 final Point size;
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200223 synchronized (layoutLock) {
magjeddf494b02016-10-07 05:32:35 -0700224 size =
Magnus Jedvert62b1c352016-10-05 15:56:06 +0200225 videoLayoutMeasure.measure(widthSpec, heightSpec, rotatedFrameWidth, rotatedFrameHeight);
Magnus Jedvert897d9322016-07-01 15:52:13 +0200226 }
magjeddf494b02016-10-07 05:32:35 -0700227 setMeasuredDimension(size.x, size.y);
228 logD("onMeasure(). New size: " + size.x + "x" + size.y);
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200229 }
230
231 @Override
232 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
magjeddf494b02016-10-07 05:32:35 -0700233 ThreadUtils.checkIsOnMainThread();
234 eglRenderer.setLayoutAspectRatio((right - left) / (float) (bottom - top));
sakalb1e6d5e2016-11-22 01:54:42 -0800235 updateSurfaceSize();
236 }
237
238 private void updateSurfaceSize() {
239 ThreadUtils.checkIsOnMainThread();
240 synchronized (layoutLock) {
241 if (enableFixedSize && rotatedFrameWidth != 0 && rotatedFrameHeight != 0 && getWidth() != 0
242 && getHeight() != 0) {
243 final float layoutAspectRatio = getWidth() / (float) getHeight();
244 final float frameAspectRatio = rotatedFrameWidth / (float) rotatedFrameHeight;
245 final int drawnFrameWidth;
246 final int drawnFrameHeight;
247 if (frameAspectRatio > layoutAspectRatio) {
248 drawnFrameWidth = (int) (rotatedFrameHeight * layoutAspectRatio);
249 drawnFrameHeight = rotatedFrameHeight;
250 } else {
251 drawnFrameWidth = rotatedFrameWidth;
252 drawnFrameHeight = (int) (rotatedFrameWidth / layoutAspectRatio);
253 }
254 // Aspect ratio of the drawn frame and the view is the same.
255 final int width = Math.min(getWidth(), drawnFrameWidth);
256 final int height = Math.min(getHeight(), drawnFrameHeight);
257 logD("updateSurfaceSize. Layout size: " + getWidth() + "x" + getHeight() + ", frame size: "
258 + rotatedFrameWidth + "x" + rotatedFrameHeight + ", requested surface size: " + width
259 + "x" + height + ", old surface size: " + surfaceWidth + "x" + surfaceHeight);
260 if (width != surfaceWidth || height != surfaceHeight) {
261 surfaceWidth = width;
262 surfaceHeight = height;
263 getHolder().setFixedSize(width, height);
264 }
265 } else {
266 surfaceWidth = surfaceHeight = 0;
267 getHolder().setSizeFromLayout();
268 }
269 }
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200270 }
271
272 // SurfaceHolder.Callback interface.
273 @Override
274 public void surfaceCreated(final SurfaceHolder holder) {
magjeddf494b02016-10-07 05:32:35 -0700275 ThreadUtils.checkIsOnMainThread();
276 eglRenderer.createEglSurface(holder.getSurface());
sakalb1e6d5e2016-11-22 01:54:42 -0800277 surfaceWidth = surfaceHeight = 0;
278 updateSurfaceSize();
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200279 }
280
281 @Override
282 public void surfaceDestroyed(SurfaceHolder holder) {
magjeddf494b02016-10-07 05:32:35 -0700283 ThreadUtils.checkIsOnMainThread();
sakal28ec6bd2016-11-09 01:47:12 -0800284 final CountDownLatch completionLatch = new CountDownLatch(1);
285 eglRenderer.releaseEglSurface(new Runnable() {
286 @Override
287 public void run() {
288 completionLatch.countDown();
289 }
290 });
291 ThreadUtils.awaitUninterruptibly(completionLatch);
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200292 }
293
294 @Override
295 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
magjeddf494b02016-10-07 05:32:35 -0700296 ThreadUtils.checkIsOnMainThread();
sakalb1e6d5e2016-11-22 01:54:42 -0800297 logD("surfaceChanged: format: " + format + " size: " + width + "x" + height);
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200298 }
299
magjedb2d1c502015-11-11 03:06:42 -0800300 private String getResourceName() {
301 try {
302 return getResources().getResourceEntryName(getId()) + ": ";
303 } catch (NotFoundException e) {
304 return "";
305 }
306 }
307
sakal9de49e32017-02-13 06:15:02 -0800308 /**
309 * Post a task to clear the SurfaceView to a transparent uniform color.
310 */
311 public void clearImage() {
312 eglRenderer.clearImage();
313 }
314
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200315 // Update frame dimensions and report any changes to |rendererEvents|.
316 private void updateFrameDimensionsAndReportEvents(VideoRenderer.I420Frame frame) {
317 synchronized (layoutLock) {
sakal521dffa2017-04-26 06:05:18 -0700318 if (isRenderingPaused) {
319 return;
320 }
magjeddf494b02016-10-07 05:32:35 -0700321 if (!isFirstFrameRendered) {
322 isFirstFrameRendered = true;
323 logD("Reporting first rendered frame.");
324 if (rendererEvents != null) {
325 rendererEvents.onFirstFrameRendered();
326 }
327 }
Magnus Jedvert62b1c352016-10-05 15:56:06 +0200328 if (rotatedFrameWidth != frame.rotatedWidth() || rotatedFrameHeight != frame.rotatedHeight()
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200329 || frameRotation != frame.rotationDegree) {
magjeddf494b02016-10-07 05:32:35 -0700330 logD("Reporting frame resolution changed to " + frame.width + "x" + frame.height
331 + " with rotation " + frame.rotationDegree);
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200332 if (rendererEvents != null) {
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200333 rendererEvents.onFrameResolutionChanged(frame.width, frame.height, frame.rotationDegree);
334 }
Magnus Jedvert62b1c352016-10-05 15:56:06 +0200335 rotatedFrameWidth = frame.rotatedWidth();
336 rotatedFrameHeight = frame.rotatedHeight();
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200337 frameRotation = frame.rotationDegree;
Magnus Jedvert4c5eea32015-11-24 17:45:26 +0100338 post(new Runnable() {
sakalb6760f92016-09-29 04:12:44 -0700339 @Override
340 public void run() {
sakalb1e6d5e2016-11-22 01:54:42 -0800341 updateSurfaceSize();
Magnus Jedvert4c5eea32015-11-24 17:45:26 +0100342 requestLayout();
343 }
344 });
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200345 }
346 }
347 }
348
sakal6bdcefc2017-08-15 01:56:02 -0700349 // Update frame dimensions and report any changes to |rendererEvents|.
350 private void updateFrameDimensionsAndReportEvents(VideoFrame frame) {
351 synchronized (layoutLock) {
352 if (isRenderingPaused) {
353 return;
354 }
355 if (!isFirstFrameRendered) {
356 isFirstFrameRendered = true;
357 logD("Reporting first rendered frame.");
358 if (rendererEvents != null) {
359 rendererEvents.onFirstFrameRendered();
360 }
361 }
362 if (rotatedFrameWidth != frame.getRotatedWidth()
363 || rotatedFrameHeight != frame.getRotatedHeight()
364 || frameRotation != frame.getRotation()) {
365 logD("Reporting frame resolution changed to " + frame.getBuffer().getWidth() + "x"
366 + frame.getBuffer().getHeight() + " with rotation " + frame.getRotation());
367 if (rendererEvents != null) {
368 rendererEvents.onFrameResolutionChanged(
369 frame.getBuffer().getWidth(), frame.getBuffer().getHeight(), frame.getRotation());
370 }
371 rotatedFrameWidth = frame.getRotatedWidth();
372 rotatedFrameHeight = frame.getRotatedHeight();
373 frameRotation = frame.getRotation();
374 post(() -> {
375 updateSurfaceSize();
376 requestLayout();
377 });
378 }
379 }
380 }
381
magjeddf494b02016-10-07 05:32:35 -0700382 private void logD(String string) {
383 Logging.d(TAG, resourceName + string);
Magnus Jedvertc06a7162015-09-11 09:51:52 +0200384 }
385}