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