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