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.
5 package org.chromium.android_webview;
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;
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;
20 import java.lang.ref.WeakReference;
23 * This is a container for external video surfaces.
24 * The object is owned by the native peer and it is owned by WebContents.
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().
37 * Please contact ycheo@chromium.org or wonsik@chromium.org if you have any
38 * questions or issues for this class.
40 @JNINamespace("android_webview")
41 public class ExternalVideoSurfaceContainer implements SurfaceHolder.Callback {
42 protected static final int INVALID_PLAYER_ID = -1;
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) {
51 // SurfaceView.dispatchDraw implementation punches a hole in the view hierarchy.
52 // Disable this by making this a no-op.
54 protected void dispatchDraw(Canvas canvas) {}
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);
65 private final long mNativeExternalVideoSurfaceContainer;
66 private final ContentViewCore mContentViewCore;
67 private int mPlayerId = INVALID_PLAYER_ID;
68 private SurfaceView mSurfaceView;
70 // The absolute CSS coordinates of the video element.
74 private float mBottom;
76 // The physical location/size of the external video surface in pixels.
83 * Factory class to facilitate dependency injection.
85 public static class Factory {
86 public ExternalVideoSurfaceContainer create(
87 long nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore) {
88 return new ExternalVideoSurfaceContainer(
89 nativeExternalVideoSurfaceContainer, contentViewCore);
92 private static Factory sFactory = new Factory();
95 public static void setFactory(Factory factory) {
100 private static ExternalVideoSurfaceContainer create(
101 long nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore) {
102 return sFactory.create(nativeExternalVideoSurfaceContainer, contentViewCore);
105 protected ExternalVideoSurfaceContainer(
106 long nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore) {
107 assert contentViewCore != null;
108 mNativeExternalVideoSurfaceContainer = nativeExternalVideoSurfaceContainer;
109 mContentViewCore = contentViewCore;
110 initializeCurrentPositionOfSurfaceView();
114 * Called when a media player wants to request an external video surface.
115 * @param playerId The ID of the media player.
118 protected void requestExternalVideoSurface(int playerId) {
119 if (mPlayerId == playerId) return;
121 if (mPlayerId == INVALID_PLAYER_ID) {
122 setActiveContainer(this);
125 mPlayerId = playerId;
126 initializeCurrentPositionOfSurfaceView();
132 * Called when a media player wants to release an external video surface.
133 * @param playerId The ID of the media player.
136 protected void releaseExternalVideoSurface(int playerId) {
137 if (mPlayerId != playerId) return;
139 releaseIfActiveContainer(this);
141 mPlayerId = INVALID_PLAYER_ID;
145 protected void destroy() {
146 releaseExternalVideoSurface(mPlayerId);
149 private void initializeCurrentPositionOfSurfaceView() {
150 mX = Integer.MIN_VALUE;
151 mY = Integer.MIN_VALUE;
156 private static void setActiveContainer(ExternalVideoSurfaceContainer container) {
157 ExternalVideoSurfaceContainer activeContainer = sActiveContainer.get();
158 if (activeContainer != null) {
159 activeContainer.removeSurfaceView();
161 sActiveContainer = new WeakReference<ExternalVideoSurfaceContainer>(container);
164 private static void releaseIfActiveContainer(ExternalVideoSurfaceContainer container) {
165 ExternalVideoSurfaceContainer activeContainer = sActiveContainer.get();
166 if (activeContainer == container) {
167 setActiveContainer(null);
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);
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);
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.
196 protected void onExternalVideoSurfacePositionChanged(
197 int playerId, float left, float top, float right, float bottom) {
198 if (mPlayerId != playerId) return;
209 * Called when the page that contains the video element is scrolled or zoomed.
212 protected void onFrameInfoUpdated() {
213 if (mPlayerId == INVALID_PLAYER_ID) return;
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();
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;
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();
247 // SurfaceHolder.Callback methods.
249 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
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());
261 // surfaceDestroyed() callback can be called regardless of releaseExternalVideoSurface,
262 // if the activity moves to the backgound and becomes invisible.
264 public void surfaceDestroyed(SurfaceHolder holder) {
265 if (mPlayerId != INVALID_PLAYER_ID) {
266 nativeSurfaceDestroyed(mNativeExternalVideoSurfaceContainer, mPlayerId);
270 private native void nativeSurfaceCreated(
271 long nativeExternalVideoSurfaceContainerImpl, int playerId, Surface surface);
273 private native void nativeSurfaceDestroyed(
274 long nativeExternalVideoSurfaceContainerImpl, int playerId);