1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package org.chromium.android_webview; 6 7import android.content.Context; 8import android.graphics.Canvas; 9import android.view.Surface; 10import android.view.SurfaceHolder; 11import android.view.SurfaceView; 12import android.view.ViewGroup; 13 14import org.chromium.base.CalledByNative; 15import org.chromium.base.JNINamespace; 16import org.chromium.base.VisibleForTesting; 17import org.chromium.content.browser.ContentViewCore; 18import org.chromium.content.browser.RenderCoordinates; 19 20import java.lang.ref.WeakReference; 21 22/** 23 * This is a container for external video surfaces. 24 * The object is owned by the native peer and it is owned by WebContents. 25 * 26 * The expected behavior of the media player on the video hole punching is as follows. 27 * 1) If it requests the surface, it will call requestExternalVideoSurface(). 28 * When the resolution of the video is changed, it'll call requestExternalVideoSurface(). 29 * 2) Whenever the size or the position of the video element is changed, it'll notify through 30 * onExternalVideoSurfacePositionChanged(). 31 * 3) Whenever the page that contains the video element is scrolled or zoomed, 32 * onFrameInfoUpdated() will be called. 33 * 4) Usually steps 1) ~ 3) are repeated during the playback. 34 * 5) If the player no longer needs the surface any more, it'll call 35 * releaseExternalVideoSurface(). 36 * 37 * Please contact ycheo@chromium.org or wonsik@chromium.org if you have any 38 * questions or issues for this class. 39 */ 40@JNINamespace("android_webview") 41public class ExternalVideoSurfaceContainer implements SurfaceHolder.Callback { 42 protected static final int INVALID_PLAYER_ID = -1; 43 44 // Because WebView does hole-punching by itself, instead, the hole-punching logic 45 // in SurfaceView can clear out some web elements like media control or subtitle. 46 // So we need to disable its hole-punching logic. 47 private static class NoPunchingSurfaceView extends SurfaceView { 48 public NoPunchingSurfaceView(Context context) { 49 super(context); 50 } 51 // SurfaceView.dispatchDraw implementation punches a hole in the view hierarchy. 52 // Disable this by making this a no-op. 53 @Override 54 protected void dispatchDraw(Canvas canvas) {} 55 } 56 57 // There can be at most 1 external video surface for now. 58 // If there are the multiple requests for the surface, then the second video will 59 // kick the first one off. 60 // To support the mulitple video surfaces seems impractical, because z-order between 61 // the multiple SurfaceViews is non-deterministic. 62 private static WeakReference<ExternalVideoSurfaceContainer> sActiveContainer = 63 new WeakReference<ExternalVideoSurfaceContainer>(null); 64 65 private final long mNativeExternalVideoSurfaceContainer; 66 private final ContentViewCore mContentViewCore; 67 private int mPlayerId = INVALID_PLAYER_ID; 68 private SurfaceView mSurfaceView; 69 70 // The absolute CSS coordinates of the video element. 71 private float mLeft; 72 private float mTop; 73 private float mRight; 74 private float mBottom; 75 76 // The physical location/size of the external video surface in pixels. 77 private int mX; 78 private int mY; 79 private int mWidth; 80 private int mHeight; 81 82 /** 83 * Factory class to facilitate dependency injection. 84 */ 85 public static class Factory { 86 public ExternalVideoSurfaceContainer create( 87 long nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore) { 88 return new ExternalVideoSurfaceContainer( 89 nativeExternalVideoSurfaceContainer, contentViewCore); 90 } 91 } 92 private static Factory sFactory = new Factory(); 93 94 @VisibleForTesting 95 public static void setFactory(Factory factory) { 96 sFactory = factory; 97 } 98 99 @CalledByNative 100 private static ExternalVideoSurfaceContainer create( 101 long nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore) { 102 return sFactory.create(nativeExternalVideoSurfaceContainer, contentViewCore); 103 } 104 105 protected ExternalVideoSurfaceContainer( 106 long nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore) { 107 assert contentViewCore != null; 108 mNativeExternalVideoSurfaceContainer = nativeExternalVideoSurfaceContainer; 109 mContentViewCore = contentViewCore; 110 initializeCurrentPositionOfSurfaceView(); 111 } 112 113 /** 114 * Called when a media player wants to request an external video surface. 115 * @param playerId The ID of the media player. 116 */ 117 @CalledByNative 118 protected void requestExternalVideoSurface(int playerId) { 119 if (mPlayerId == playerId) return; 120 121 if (mPlayerId == INVALID_PLAYER_ID) { 122 setActiveContainer(this); 123 } 124 125 mPlayerId = playerId; 126 initializeCurrentPositionOfSurfaceView(); 127 128 createSurfaceView(); 129 } 130 131 /** 132 * Called when a media player wants to release an external video surface. 133 * @param playerId The ID of the media player. 134 */ 135 @CalledByNative 136 protected void releaseExternalVideoSurface(int playerId) { 137 if (mPlayerId != playerId) return; 138 139 releaseIfActiveContainer(this); 140 141 mPlayerId = INVALID_PLAYER_ID; 142 } 143 144 @CalledByNative 145 protected void destroy() { 146 releaseExternalVideoSurface(mPlayerId); 147 } 148 149 private void initializeCurrentPositionOfSurfaceView() { 150 mX = Integer.MIN_VALUE; 151 mY = Integer.MIN_VALUE; 152 mWidth = 0; 153 mHeight = 0; 154 } 155 156 private static void setActiveContainer(ExternalVideoSurfaceContainer container) { 157 ExternalVideoSurfaceContainer activeContainer = sActiveContainer.get(); 158 if (activeContainer != null) { 159 activeContainer.removeSurfaceView(); 160 } 161 sActiveContainer = new WeakReference<ExternalVideoSurfaceContainer>(container); 162 } 163 164 private static void releaseIfActiveContainer(ExternalVideoSurfaceContainer container) { 165 ExternalVideoSurfaceContainer activeContainer = sActiveContainer.get(); 166 if (activeContainer == container) { 167 setActiveContainer(null); 168 } 169 } 170 171 private void createSurfaceView() { 172 mSurfaceView = new NoPunchingSurfaceView(mContentViewCore.getContext()); 173 mSurfaceView.getHolder().addCallback(this); 174 // SurfaceHoder.surfaceCreated() will be called after the SurfaceView is attached to 175 // the Window and becomes visible. 176 mContentViewCore.getContainerView().addView(mSurfaceView); 177 } 178 179 private void removeSurfaceView() { 180 // SurfaceHoder.surfaceDestroyed() will be called in ViewGroup.removeView() 181 // as soon as the SurfaceView is detached from the Window. 182 mContentViewCore.getContainerView().removeView(mSurfaceView); 183 mSurfaceView = null; 184 } 185 186 /** 187 * Called when the position of the video element which uses the external 188 * video surface is changed. 189 * @param playerId The ID of the media player. 190 * @param left The absolute CSS X coordinate of the left side of the video element. 191 * @param top The absolute CSS Y coordinate of the top side of the video element. 192 * @param right The absolute CSS X coordinate of the right side of the video element. 193 * @param bottom The absolute CSS Y coordinate of the bottom side of the video element. 194 */ 195 @CalledByNative 196 protected void onExternalVideoSurfacePositionChanged( 197 int playerId, float left, float top, float right, float bottom) { 198 if (mPlayerId != playerId) return; 199 200 mLeft = left; 201 mTop = top; 202 mRight = right; 203 mBottom = bottom; 204 205 layOutSurfaceView(); 206 } 207 208 /** 209 * Called when the page that contains the video element is scrolled or zoomed. 210 */ 211 @CalledByNative 212 protected void onFrameInfoUpdated() { 213 if (mPlayerId == INVALID_PLAYER_ID) return; 214 215 layOutSurfaceView(); 216 } 217 218 private void layOutSurfaceView() { 219 RenderCoordinates renderCoordinates = mContentViewCore.getRenderCoordinates(); 220 RenderCoordinates.NormalizedPoint topLeft = renderCoordinates.createNormalizedPoint(); 221 RenderCoordinates.NormalizedPoint bottomRight = renderCoordinates.createNormalizedPoint(); 222 topLeft.setAbsoluteCss(mLeft, mTop); 223 bottomRight.setAbsoluteCss(mRight, mBottom); 224 float top = topLeft.getYPix(); 225 float left = topLeft.getXPix(); 226 float bottom = bottomRight.getYPix(); 227 float right = bottomRight.getXPix(); 228 229 int x = Math.round(left + renderCoordinates.getScrollXPix()); 230 int y = Math.round(top + renderCoordinates.getScrollYPix()); 231 int width = Math.round(right - left); 232 int height = Math.round(bottom - top); 233 if (mX == x && mY == y && mWidth == width && mHeight == height) return; 234 mX = x; 235 mY = y; 236 mWidth = width; 237 mHeight = height; 238 239 mSurfaceView.setX(x); 240 mSurfaceView.setY(y); 241 ViewGroup.LayoutParams layoutParams = mSurfaceView.getLayoutParams(); 242 layoutParams.width = width; 243 layoutParams.height = height; 244 mSurfaceView.requestLayout(); 245 } 246 247 // SurfaceHolder.Callback methods. 248 @Override 249 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} 250 251 @Override 252 // surfaceCreated() callback can be called regardless of requestExternalVideoSurface, 253 // if the activity comes back from the background and becomes visible. 254 public void surfaceCreated(SurfaceHolder holder) { 255 if (mPlayerId != INVALID_PLAYER_ID) { 256 nativeSurfaceCreated( 257 mNativeExternalVideoSurfaceContainer, mPlayerId, holder.getSurface()); 258 } 259 } 260 261 // surfaceDestroyed() callback can be called regardless of releaseExternalVideoSurface, 262 // if the activity moves to the backgound and becomes invisible. 263 @Override 264 public void surfaceDestroyed(SurfaceHolder holder) { 265 if (mPlayerId != INVALID_PLAYER_ID) { 266 nativeSurfaceDestroyed(mNativeExternalVideoSurfaceContainer, mPlayerId); 267 } 268 } 269 270 private native void nativeSurfaceCreated( 271 long nativeExternalVideoSurfaceContainerImpl, int playerId, Surface surface); 272 273 private native void nativeSurfaceDestroyed( 274 long nativeExternalVideoSurfaceContainerImpl, int playerId); 275} 276 277