root/android_webview/java/src/org/chromium/android_webview/ExternalVideoSurfaceContainer.java

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. JNINamespace
  2. dispatchDraw
  3. create
  4. requestExternalVideoSurface
  5. releaseExternalVideoSurface
  6. destroy
  7. initializeCurrentPositionOfSurfaceView
  8. setActiveContainer
  9. releaseIfActiveContainer
  10. createSurfaceView
  11. removeSurfaceView
  12. onExternalVideoSurfacePositionChanged
  13. onFrameInfoUpdated
  14. layOutSurfaceView
  15. surfaceChanged
  16. surfaceCreated
  17. surfaceDestroyed
  18. nativeSurfaceCreated
  19. nativeSurfaceDestroyed

// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.android_webview;

import android.content.Context;
import android.graphics.Canvas;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;

import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;
import org.chromium.content.browser.ContentViewCore;
import org.chromium.content.browser.RenderCoordinates;

import java.lang.ref.WeakReference;

/**
 * This is a container for external video surfaces.
 * The object is owned by the native peer and it is owned by WebContents.
 *
 * The expected behavior of the media player on the video hole punching is as follows.
 * 1) If it requests the surface, it will call requestExternalVideoSurface().
 *    When the resolution of the video is changed, it'll call requestExternalVideoSurface().
 * 2) Whenever the size or the position of the video element is changed, it'll notify through
 *    onExternalVideoSurfacePositionChanged().
 * 3) Whenever the page that contains the video element is scrolled or zoomed,
 *    onFrameInfoUpdated() will be called.
 * 4) Usually steps 1) ~ 3) are repeated during the playback.
 * 5) If the player no longer needs the surface any more, it'll call
 *    releaseExternalVideoSurface().
 *
 * Please contact ycheo@chromium.org or wonsik@chromium.org if you have any
 * questions or issues for this class.
 */
@JNINamespace("android_webview")
public class ExternalVideoSurfaceContainer implements SurfaceHolder.Callback {
    private static final int INVALID_PLAYER_ID = -1;

    // Because WebView does hole-punching by itself, instead, the hole-punching logic
    // in SurfaceView can clear out some web elements like media control or subtitle.
    // So we need to disable its hole-punching logic.
    private static class NoPunchingSurfaceView extends SurfaceView {
        public NoPunchingSurfaceView(Context context) {
            super(context);
        }
        // SurfaceView.dispatchDraw implementation punches a hole in the view hierarchy.
        // Disable this by making this a no-op.
        @Override
        protected void dispatchDraw(Canvas canvas) {}
    }

    // There can be at most 1 external video surface for now.
    // If there are the multiple requests for the surface, then the second video will
    // kick the first one off.
    // To support the mulitple video surfaces seems impractical, because z-order between
    // the multiple SurfaceViews is non-deterministic.
    private static WeakReference<ExternalVideoSurfaceContainer> sActiveContainer =
            new WeakReference<ExternalVideoSurfaceContainer>(null);

    private final int mNativeExternalVideoSurfaceContainer;
    private final ContentViewCore mContentViewCore;
    private int mPlayerId = INVALID_PLAYER_ID;
    private SurfaceView mSurfaceView;

    // The absolute CSS coordinates of the video element.
    private float mLeft;
    private float mTop;
    private float mRight;
    private float mBottom;

    // The physical location/size of the external video surface in pixels.
    private int mX;
    private int mY;
    private int mWidth;
    private int mHeight;

    @CalledByNative
    private static ExternalVideoSurfaceContainer create(
            int nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore) {
        return new ExternalVideoSurfaceContainer(
                nativeExternalVideoSurfaceContainer, contentViewCore);
    }

    private ExternalVideoSurfaceContainer(
            int nativeExternalVideoSurfaceContainer, ContentViewCore contentViewCore) {
        assert contentViewCore != null;
        mNativeExternalVideoSurfaceContainer = nativeExternalVideoSurfaceContainer;
        mContentViewCore = contentViewCore;
        initializeCurrentPositionOfSurfaceView();
    }

    /**
     * Called when a media player wants to request an external video surface.
     * @param playerId The ID of the media player.
     */
    @CalledByNative
    private void requestExternalVideoSurface(int playerId) {
        if (mPlayerId == playerId) return;

        if (mPlayerId == INVALID_PLAYER_ID) {
            setActiveContainer(this);
        }

        mPlayerId = playerId;
        initializeCurrentPositionOfSurfaceView();

        createSurfaceView();
    }

    /**
     * Called when a media player wants to release an external video surface.
     * @param playerId The ID of the media player.
     */
    @CalledByNative
    private void releaseExternalVideoSurface(int playerId) {
        if (mPlayerId != playerId) return;

        releaseIfActiveContainer(this);

        mPlayerId = INVALID_PLAYER_ID;
    }

    @CalledByNative
    private void destroy() {
        releaseExternalVideoSurface(mPlayerId);
    }

    private void initializeCurrentPositionOfSurfaceView() {
        mX = Integer.MIN_VALUE;
        mY = Integer.MIN_VALUE;
        mWidth = 0;
        mHeight = 0;
    }

    private static void setActiveContainer(ExternalVideoSurfaceContainer container) {
        ExternalVideoSurfaceContainer activeContainer = sActiveContainer.get();
        if (activeContainer != null) {
            activeContainer.removeSurfaceView();
        }
        sActiveContainer = new WeakReference<ExternalVideoSurfaceContainer>(container);
    }

    private static void releaseIfActiveContainer(ExternalVideoSurfaceContainer container) {
        ExternalVideoSurfaceContainer activeContainer = sActiveContainer.get();
        if (activeContainer == container) {
            setActiveContainer(null);
        }
    }

    private void createSurfaceView() {
        mSurfaceView = new NoPunchingSurfaceView(mContentViewCore.getContext());
        mSurfaceView.getHolder().addCallback(this);
        // SurfaceHoder.surfaceCreated() will be called after the SurfaceView is attached to
        // the Window and becomes visible.
        mContentViewCore.getContainerView().addView(mSurfaceView);
    }

    private void removeSurfaceView() {
        // SurfaceHoder.surfaceDestroyed() will be called in ViewGroup.removeView()
        // as soon as the SurfaceView is detached from the Window.
        mContentViewCore.getContainerView().removeView(mSurfaceView);
        mSurfaceView = null;
    }

    /**
     * Called when the position of the video element which uses the external
     * video surface is changed.
     * @param playerId The ID of the media player.
     * @param left The absolute CSS X coordinate of the left side of the video element.
     * @param top The absolute CSS Y coordinate of the top side of the video element.
     * @param right The absolute CSS X coordinate of the right side of the video element.
     * @param bottom The absolute CSS Y coordinate of the bottom side of the video element.
     */
    @CalledByNative
    private void onExternalVideoSurfacePositionChanged(
            int playerId, float left, float top, float right, float bottom) {
        if (mPlayerId != playerId) return;

        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;

        layOutSurfaceView();
    }

    /**
     * Called when the page that contains the video element is scrolled or zoomed.
     */
    @CalledByNative
    private void onFrameInfoUpdated() {
        if (mPlayerId == INVALID_PLAYER_ID) return;

        layOutSurfaceView();
    }

    private void layOutSurfaceView() {
        RenderCoordinates renderCoordinates = mContentViewCore.getRenderCoordinates();
        RenderCoordinates.NormalizedPoint topLeft = renderCoordinates.createNormalizedPoint();
        RenderCoordinates.NormalizedPoint bottomRight = renderCoordinates.createNormalizedPoint();
        topLeft.setAbsoluteCss(mLeft, mTop);
        bottomRight.setAbsoluteCss(mRight, mBottom);
        float top = topLeft.getYPix();
        float left = topLeft.getXPix();
        float bottom = bottomRight.getYPix();
        float right = bottomRight.getXPix();

        int x = Math.round(left + renderCoordinates.getScrollXPix());
        int y = Math.round(top + renderCoordinates.getScrollYPix());
        int width = Math.round(right - left);
        int height = Math.round(bottom - top);
        if (mX == x && mY == y && mWidth == width && mHeight == height) return;
        mX = x;
        mY = y;
        mWidth = width;
        mHeight = height;

        mSurfaceView.setX(x);
        mSurfaceView.setY(y);
        ViewGroup.LayoutParams layoutParams = mSurfaceView.getLayoutParams();
        layoutParams.width = width;
        layoutParams.height = height;
        mSurfaceView.requestLayout();
    }

    // SurfaceHolder.Callback methods.
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}

    @Override
    // surfaceCreated() callback can be called regardless of requestExternalVideoSurface,
    // if the activity comes back from the background and becomes visible.
    public void surfaceCreated(SurfaceHolder holder) {
        if (mPlayerId != INVALID_PLAYER_ID) {
            nativeSurfaceCreated(
                    mNativeExternalVideoSurfaceContainer, mPlayerId, holder.getSurface());
        }
    }

    // surfaceDestroyed() callback can be called regardless of releaseExternalVideoSurface,
    // if the activity moves to the backgound and becomes invisible.
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if (mPlayerId != INVALID_PLAYER_ID) {
            nativeSurfaceDestroyed(mNativeExternalVideoSurfaceContainer, mPlayerId);
        }
    }

    private native void nativeSurfaceCreated(
            long nativeExternalVideoSurfaceContainerImpl, int playerId, Surface surface);

    private native void nativeSurfaceDestroyed(
            long nativeExternalVideoSurfaceContainerImpl, int playerId);
}


/* [<][>][^][v][top][bottom][index][help] */