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.os.Looper; 17 import android.util.AttributeSet; 18 import android.view.SurfaceHolder; 19 import android.view.SurfaceView; 20 21 /** 22 * Display the video stream on a SurfaceView. 23 */ 24 public class SurfaceViewRenderer extends SurfaceView 25 implements SurfaceHolder.Callback, VideoSink, RendererCommon.RendererEvents { 26 private static final String TAG = "SurfaceViewRenderer"; 27 28 // Cached resource name. 29 private final String resourceName; 30 private final RendererCommon.VideoLayoutMeasure videoLayoutMeasure = 31 new RendererCommon.VideoLayoutMeasure(); 32 private final SurfaceEglRenderer eglRenderer; 33 34 // Callback for reporting renderer events. Read-only after initialization so no lock required. 35 private RendererCommon.RendererEvents rendererEvents; 36 37 // Accessed only on the main thread. 38 private int rotatedFrameWidth; 39 private int rotatedFrameHeight; 40 private boolean enableFixedSize; 41 private int surfaceWidth; 42 private int surfaceHeight; 43 44 /** 45 * Standard View constructor. In order to render something, you must first call init(). 46 */ SurfaceViewRenderer(Context context)47 public SurfaceViewRenderer(Context context) { 48 super(context); 49 this.resourceName = getResourceName(); 50 eglRenderer = new SurfaceEglRenderer(resourceName); 51 getHolder().addCallback(this); 52 getHolder().addCallback(eglRenderer); 53 } 54 55 /** 56 * Standard View constructor. In order to render something, you must first call init(). 57 */ SurfaceViewRenderer(Context context, AttributeSet attrs)58 public SurfaceViewRenderer(Context context, AttributeSet attrs) { 59 super(context, attrs); 60 this.resourceName = getResourceName(); 61 eglRenderer = new SurfaceEglRenderer(resourceName); 62 getHolder().addCallback(this); 63 getHolder().addCallback(eglRenderer); 64 } 65 66 /** 67 * Initialize this class, sharing resources with |sharedContext|. It is allowed to call init() to 68 * reinitialize the renderer after a previous init()/release() cycle. 69 */ init(EglBase.Context sharedContext, RendererCommon.RendererEvents rendererEvents)70 public void init(EglBase.Context sharedContext, RendererCommon.RendererEvents rendererEvents) { 71 init(sharedContext, rendererEvents, EglBase.CONFIG_PLAIN, new GlRectDrawer()); 72 } 73 74 /** 75 * Initialize this class, sharing resources with |sharedContext|. The custom |drawer| will be used 76 * for drawing frames on the EGLSurface. This class is responsible for calling release() on 77 * |drawer|. It is allowed to call init() to reinitialize the renderer after a previous 78 * init()/release() cycle. 79 */ init(final EglBase.Context sharedContext, RendererCommon.RendererEvents rendererEvents, final int[] configAttributes, RendererCommon.GlDrawer drawer)80 public void init(final EglBase.Context sharedContext, 81 RendererCommon.RendererEvents rendererEvents, final int[] configAttributes, 82 RendererCommon.GlDrawer drawer) { 83 ThreadUtils.checkIsOnMainThread(); 84 this.rendererEvents = rendererEvents; 85 rotatedFrameWidth = 0; 86 rotatedFrameHeight = 0; 87 eglRenderer.init(sharedContext, this /* rendererEvents */, configAttributes, drawer); 88 } 89 90 /** 91 * Block until any pending frame is returned and all GL resources released, even if an interrupt 92 * occurs. If an interrupt occurs during release(), the interrupt flag will be set. This function 93 * should be called before the Activity is destroyed and the EGLContext is still valid. If you 94 * don't call this function, the GL resources might leak. 95 */ release()96 public void release() { 97 eglRenderer.release(); 98 } 99 100 /** 101 * Register a callback to be invoked when a new video frame has been received. 102 * 103 * @param listener The callback to be invoked. The callback will be invoked on the render thread. 104 * It should be lightweight and must not call removeFrameListener. 105 * @param scale The scale of the Bitmap passed to the callback, or 0 if no Bitmap is 106 * required. 107 * @param drawer Custom drawer to use for this frame listener. 108 */ addFrameListener( EglRenderer.FrameListener listener, float scale, RendererCommon.GlDrawer drawerParam)109 public void addFrameListener( 110 EglRenderer.FrameListener listener, float scale, RendererCommon.GlDrawer drawerParam) { 111 eglRenderer.addFrameListener(listener, scale, drawerParam); 112 } 113 114 /** 115 * Register a callback to be invoked when a new video frame has been received. This version uses 116 * the drawer of the EglRenderer that was passed in init. 117 * 118 * @param listener The callback to be invoked. The callback will be invoked on the render thread. 119 * It should be lightweight and must not call removeFrameListener. 120 * @param scale The scale of the Bitmap passed to the callback, or 0 if no Bitmap is 121 * required. 122 */ addFrameListener(EglRenderer.FrameListener listener, float scale)123 public void addFrameListener(EglRenderer.FrameListener listener, float scale) { 124 eglRenderer.addFrameListener(listener, scale); 125 } 126 removeFrameListener(EglRenderer.FrameListener listener)127 public void removeFrameListener(EglRenderer.FrameListener listener) { 128 eglRenderer.removeFrameListener(listener); 129 } 130 131 /** 132 * Enables fixed size for the surface. This provides better performance but might be buggy on some 133 * devices. By default this is turned off. 134 */ setEnableHardwareScaler(boolean enabled)135 public void setEnableHardwareScaler(boolean enabled) { 136 ThreadUtils.checkIsOnMainThread(); 137 enableFixedSize = enabled; 138 updateSurfaceSize(); 139 } 140 141 /** 142 * Set if the video stream should be mirrored or not. 143 */ setMirror(final boolean mirror)144 public void setMirror(final boolean mirror) { 145 eglRenderer.setMirror(mirror); 146 } 147 148 /** 149 * Set how the video will fill the allowed layout area. 150 */ setScalingType(RendererCommon.ScalingType scalingType)151 public void setScalingType(RendererCommon.ScalingType scalingType) { 152 ThreadUtils.checkIsOnMainThread(); 153 videoLayoutMeasure.setScalingType(scalingType); 154 requestLayout(); 155 } 156 setScalingType(RendererCommon.ScalingType scalingTypeMatchOrientation, RendererCommon.ScalingType scalingTypeMismatchOrientation)157 public void setScalingType(RendererCommon.ScalingType scalingTypeMatchOrientation, 158 RendererCommon.ScalingType scalingTypeMismatchOrientation) { 159 ThreadUtils.checkIsOnMainThread(); 160 videoLayoutMeasure.setScalingType(scalingTypeMatchOrientation, scalingTypeMismatchOrientation); 161 requestLayout(); 162 } 163 164 /** 165 * Limit render framerate. 166 * 167 * @param fps Limit render framerate to this value, or use Float.POSITIVE_INFINITY to disable fps 168 * reduction. 169 */ setFpsReduction(float fps)170 public void setFpsReduction(float fps) { 171 eglRenderer.setFpsReduction(fps); 172 } 173 disableFpsReduction()174 public void disableFpsReduction() { 175 eglRenderer.disableFpsReduction(); 176 } 177 pauseVideo()178 public void pauseVideo() { 179 eglRenderer.pauseVideo(); 180 } 181 182 // VideoSink interface. 183 @Override onFrame(VideoFrame frame)184 public void onFrame(VideoFrame frame) { 185 eglRenderer.onFrame(frame); 186 } 187 188 // View layout interface. 189 @Override onMeasure(int widthSpec, int heightSpec)190 protected void onMeasure(int widthSpec, int heightSpec) { 191 ThreadUtils.checkIsOnMainThread(); 192 Point size = 193 videoLayoutMeasure.measure(widthSpec, heightSpec, rotatedFrameWidth, rotatedFrameHeight); 194 setMeasuredDimension(size.x, size.y); 195 logD("onMeasure(). New size: " + size.x + "x" + size.y); 196 } 197 198 @Override onLayout(boolean changed, int left, int top, int right, int bottom)199 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 200 ThreadUtils.checkIsOnMainThread(); 201 eglRenderer.setLayoutAspectRatio((right - left) / (float) (bottom - top)); 202 updateSurfaceSize(); 203 } 204 updateSurfaceSize()205 private void updateSurfaceSize() { 206 ThreadUtils.checkIsOnMainThread(); 207 if (enableFixedSize && rotatedFrameWidth != 0 && rotatedFrameHeight != 0 && getWidth() != 0 208 && getHeight() != 0) { 209 final float layoutAspectRatio = getWidth() / (float) getHeight(); 210 final float frameAspectRatio = rotatedFrameWidth / (float) rotatedFrameHeight; 211 final int drawnFrameWidth; 212 final int drawnFrameHeight; 213 if (frameAspectRatio > layoutAspectRatio) { 214 drawnFrameWidth = (int) (rotatedFrameHeight * layoutAspectRatio); 215 drawnFrameHeight = rotatedFrameHeight; 216 } else { 217 drawnFrameWidth = rotatedFrameWidth; 218 drawnFrameHeight = (int) (rotatedFrameWidth / layoutAspectRatio); 219 } 220 // Aspect ratio of the drawn frame and the view is the same. 221 final int width = Math.min(getWidth(), drawnFrameWidth); 222 final int height = Math.min(getHeight(), drawnFrameHeight); 223 logD("updateSurfaceSize. Layout size: " + getWidth() + "x" + getHeight() + ", frame size: " 224 + rotatedFrameWidth + "x" + rotatedFrameHeight + ", requested surface size: " + width 225 + "x" + height + ", old surface size: " + surfaceWidth + "x" + surfaceHeight); 226 if (width != surfaceWidth || height != surfaceHeight) { 227 surfaceWidth = width; 228 surfaceHeight = height; 229 getHolder().setFixedSize(width, height); 230 } 231 } else { 232 surfaceWidth = surfaceHeight = 0; 233 getHolder().setSizeFromLayout(); 234 } 235 } 236 237 // SurfaceHolder.Callback interface. 238 @Override surfaceCreated(final SurfaceHolder holder)239 public void surfaceCreated(final SurfaceHolder holder) { 240 ThreadUtils.checkIsOnMainThread(); 241 surfaceWidth = surfaceHeight = 0; 242 updateSurfaceSize(); 243 } 244 245 @Override surfaceDestroyed(SurfaceHolder holder)246 public void surfaceDestroyed(SurfaceHolder holder) {} 247 248 @Override surfaceChanged(SurfaceHolder holder, int format, int width, int height)249 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} 250 getResourceName()251 private String getResourceName() { 252 try { 253 return getResources().getResourceEntryName(getId()); 254 } catch (NotFoundException e) { 255 return ""; 256 } 257 } 258 259 /** 260 * Post a task to clear the SurfaceView to a transparent uniform color. 261 */ clearImage()262 public void clearImage() { 263 eglRenderer.clearImage(); 264 } 265 266 @Override onFirstFrameRendered()267 public void onFirstFrameRendered() { 268 if (rendererEvents != null) { 269 rendererEvents.onFirstFrameRendered(); 270 } 271 } 272 273 @Override onFrameResolutionChanged(int videoWidth, int videoHeight, int rotation)274 public void onFrameResolutionChanged(int videoWidth, int videoHeight, int rotation) { 275 if (rendererEvents != null) { 276 rendererEvents.onFrameResolutionChanged(videoWidth, videoHeight, rotation); 277 } 278 int rotatedWidth = rotation == 0 || rotation == 180 ? videoWidth : videoHeight; 279 int rotatedHeight = rotation == 0 || rotation == 180 ? videoHeight : videoWidth; 280 // run immediately if possible for ui thread tests 281 postOrRun(() -> { 282 rotatedFrameWidth = rotatedWidth; 283 rotatedFrameHeight = rotatedHeight; 284 updateSurfaceSize(); 285 requestLayout(); 286 }); 287 } 288 postOrRun(Runnable r)289 private void postOrRun(Runnable r) { 290 if (Thread.currentThread() == Looper.getMainLooper().getThread()) { 291 r.run(); 292 } else { 293 post(r); 294 } 295 } 296 logD(String string)297 private void logD(String string) { 298 Logging.d(TAG, resourceName + ": " + string); 299 } 300 } 301