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 5 package org.chromium.android_webview; 6 7 import android.content.Context; 8 import android.graphics.Canvas; 9 import android.view.Surface; 10 import android.view.SurfaceHolder; 11 import android.view.SurfaceView; 12 import android.view.ViewGroup; 13 14 import org.chromium.base.CalledByNative; 15 import org.chromium.base.JNINamespace; 16 import org.chromium.base.VisibleForTesting; 17 import org.chromium.content.browser.ContentViewCore; 18 import org.chromium.content.browser.RenderCoordinates; 19 20 import 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") 41 public 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 { NoPunchingSurfaceView(Context context)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 dispatchDraw(Canvas canvas)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 { create( long nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore)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 setFactory(Factory factory)95 public static void setFactory(Factory factory) { 96 sFactory = factory; 97 } 98 99 @CalledByNative create( long nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore)100 private static ExternalVideoSurfaceContainer create( 101 long nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore) { 102 return sFactory.create(nativeExternalVideoSurfaceContainer, contentViewCore); 103 } 104 ExternalVideoSurfaceContainer( long nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore)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 requestExternalVideoSurface(int playerId)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 releaseExternalVideoSurface(int playerId)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 destroy()145 protected void destroy() { 146 releaseExternalVideoSurface(mPlayerId); 147 } 148 initializeCurrentPositionOfSurfaceView()149 private void initializeCurrentPositionOfSurfaceView() { 150 mX = Integer.MIN_VALUE; 151 mY = Integer.MIN_VALUE; 152 mWidth = 0; 153 mHeight = 0; 154 } 155 setActiveContainer(ExternalVideoSurfaceContainer container)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 releaseIfActiveContainer(ExternalVideoSurfaceContainer container)164 private static void releaseIfActiveContainer(ExternalVideoSurfaceContainer container) { 165 ExternalVideoSurfaceContainer activeContainer = sActiveContainer.get(); 166 if (activeContainer == container) { 167 setActiveContainer(null); 168 } 169 } 170 createSurfaceView()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 removeSurfaceView()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 onExternalVideoSurfacePositionChanged( int playerId, float left, float top, float right, float bottom)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 onFrameInfoUpdated()212 protected void onFrameInfoUpdated() { 213 if (mPlayerId == INVALID_PLAYER_ID) return; 214 215 layOutSurfaceView(); 216 } 217 layOutSurfaceView()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 surfaceChanged(SurfaceHolder holder, int format, int width, int height)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. surfaceCreated(SurfaceHolder holder)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 surfaceDestroyed(SurfaceHolder holder)264 public void surfaceDestroyed(SurfaceHolder holder) { 265 if (mPlayerId != INVALID_PLAYER_ID) { 266 nativeSurfaceDestroyed(mNativeExternalVideoSurfaceContainer, mPlayerId); 267 } 268 } 269 nativeSurfaceCreated( long nativeExternalVideoSurfaceContainerImpl, int playerId, Surface surface)270 private native void nativeSurfaceCreated( 271 long nativeExternalVideoSurfaceContainerImpl, int playerId, Surface surface); 272 nativeSurfaceDestroyed( long nativeExternalVideoSurfaceContainerImpl, int playerId)273 private native void nativeSurfaceDestroyed( 274 long nativeExternalVideoSurfaceContainerImpl, int playerId); 275 } 276 277