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