Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 661e8b1f authored by Teng-Hui Zhu's avatar Teng-Hui Zhu
Browse files

Inline HTML5 Video support

Use the HTML5VideoView to make inline HTML5 video possible.
Full screen support will be the next step.

The native side change is at 101310.

bug:3506407, 2126902
Change-Id: I012f33a4d0c7b83d37b184fceb3923e1fb277b80
parent dc8b70ca
Loading
Loading
Loading
Loading
+211 −0
Original line number Diff line number Diff line

package android.webkit;

import android.graphics.SurfaceTexture;
import android.media.MediaPlayer;
import android.util.Log;
import android.webkit.HTML5VideoViewProxy;
import android.widget.MediaController;
import android.opengl.GLES20;
import java.io.IOException;
import java.util.Map;

/**
 * @hide This is only used by the browser
 */
public class HTML5VideoView implements MediaPlayer.OnPreparedListener{
    // Due to the fact that SurfaceTexture consume a lot of memory, we make it
    // as static. m_textureNames is the texture bound with this SurfaceTexture.
    private static SurfaceTexture mSurfaceTexture = null;
    private static int[] mTextureNames;

    // Only when the video is prepared, we render using SurfaceTexture.
    // This in fact is used to avoid showing the obsolete content when
    // switching videos.
    private static boolean mReadyToUseSurfTex = false;

    // For handling the seekTo before prepared, we need to know whether or not
    // the video is prepared. Therefore, we differentiate the state between
    // prepared and not prepared.
    // When the video is not prepared, we will have to save the seekTo time,
    // and use it when prepared to play.
    private static final int STATE_NOTPREPARED        = 0;
    private static final int STATE_PREPARED           = 1;

    // We only need state for handling seekTo
    private int mCurrentState;

    // Basically for calling back the OnPrepared in the proxy
    private HTML5VideoViewProxy mProxy;

    // Save the seek time when not prepared. This can happen when switching
    // video besides initial load.
    private int mSaveSeekTime;

    // This is used to find the VideoLayer on the native side.
    private int mVideoLayerId;

    // Every video will have one MediaPlayer. Given the fact we only have one
    // SurfaceTexture, there is only one MediaPlayer in action. Every time we
    // switch videos, a new instance of MediaPlayer will be created in reset().
    private MediaPlayer mPlayer;

    private static HTML5VideoView mInstance = new HTML5VideoView();

    // Video control FUNCTIONS:
    public void start() {
        if (mCurrentState == STATE_PREPARED) {
            mPlayer.start();
            mReadyToUseSurfTex = true;
        }
    }

    public void pause() {
        mPlayer.pause();
    }

    public int getDuration() {
        return mPlayer.getDuration();
    }

    public int getCurrentPosition() {
        return mPlayer.getCurrentPosition();
    }

    public void seekTo(int pos) {
        if (mCurrentState == STATE_PREPARED)
            mPlayer.seekTo(pos);
        else
            mSaveSeekTime = pos;
    }

    public boolean isPlaying() {
        return mPlayer.isPlaying();
    }

    public void release() {
        mPlayer.release();
    }

    public void stopPlayback() {
        mPlayer.stop();
    }

    private void reset(int videoLayerId) {
        mPlayer = new MediaPlayer();
        mCurrentState = STATE_NOTPREPARED;
        mProxy = null;
        mVideoLayerId = videoLayerId;
        mReadyToUseSurfTex = false;
    }

    public static HTML5VideoView getInstance(int videoLayerId) {
        // Every time we switch between the videos, a new MediaPlayer will be
        // created. Make sure we call the m_player.release() when it is done.
        mInstance.reset(videoLayerId);
        return mInstance;
    }

    private HTML5VideoView() {
        // This is a singleton across WebViews (i.e. Tabs).
        // HTML5VideoViewProxy will reset the internal state every time a new
        // video start.
    }

    public void setMediaController(MediaController m) {
        this.setMediaController(m);
    }

    public void setVideoURI(String uri, Map<String, String> headers) {
        // When switching players, surface texture will be reused.
        mPlayer.setTexture(getSurfaceTextureInstance());

        // When there is exception, we could just bail out silently.
        // No Video will be played though. Write the stack for debug
        try {
            mPlayer.setDataSource(uri, headers);
            mPlayer.prepareAsync();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // TODO [FULL SCREEN SUPPORT]

    // Listeners setup FUNCTIONS:
    public void setOnCompletionListener(HTML5VideoViewProxy proxy) {
        mPlayer.setOnCompletionListener(proxy);
    }

    public void setOnErrorListener(HTML5VideoViewProxy proxy) {
        mPlayer.setOnErrorListener(proxy);
    }

    public void setOnPreparedListener(HTML5VideoViewProxy proxy) {
        mProxy = proxy;
        mPlayer.setOnPreparedListener(this);
    }

    // Inline Video specific FUNCTIONS:

    public SurfaceTexture getSurfaceTexture() {
        return mSurfaceTexture;
    }

    public void deleteSurfaceTexture() {
        mSurfaceTexture = null;
        return;
    }

    // SurfaceTexture is a singleton here , too
    private SurfaceTexture getSurfaceTextureInstance() {
        // Create the surface texture.
        if (mSurfaceTexture == null)
        {
            mTextureNames = new int[1];
            GLES20.glGenTextures(1, mTextureNames, 0);
            mSurfaceTexture = new SurfaceTexture(mTextureNames[0]);
        }
        return mSurfaceTexture;
    }

    public int getTextureName() {
        return mTextureNames[0];
    }

    public int getVideoLayerId() {
        return mVideoLayerId;
    }

    public boolean getReadyToUseSurfTex() {
        return mReadyToUseSurfTex;
    }

    public void setFrameAvailableListener(SurfaceTexture.OnFrameAvailableListener l) {
        mSurfaceTexture.setOnFrameAvailableListener(l);
    }

    @Override
    public void onPrepared(MediaPlayer mp) {
        mCurrentState = STATE_PREPARED;
        seekTo(mSaveSeekTime);
        if (mProxy != null)
            mProxy.onPrepared(mp);
    }

    // Pause the play and update the play/pause button
    public void pauseAndDispatch(HTML5VideoViewProxy proxy) {
        if (isPlaying()) {
            pause();
            if (proxy != null) {
                proxy.dispatchOnPaused();
            }
        }
        mReadyToUseSurfTex = false;
    }

}
+147 −99
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package android.webkit;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.SurfaceTexture;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnPreparedListener;
import android.media.MediaPlayer.OnCompletionListener;
@@ -59,7 +60,8 @@ import java.util.TimerTask;
class HTML5VideoViewProxy extends Handler
                          implements MediaPlayer.OnPreparedListener,
                          MediaPlayer.OnCompletionListener,
                          MediaPlayer.OnErrorListener {
                          MediaPlayer.OnErrorListener,
                          SurfaceTexture.OnFrameAvailableListener {
    // Logging tag.
    private static final String LOGTAG = "HTML5VideoViewProxy";

@@ -101,7 +103,7 @@ class HTML5VideoViewProxy extends Handler
        private static HTML5VideoViewProxy mCurrentProxy;
        // The VideoView instance. This is a singleton for now, at least until
        // http://b/issue?id=1973663 is fixed.
        private static VideoView mVideoView;
        private static HTML5VideoView mHTML5VideoView;
        // The progress view.
        private static View mProgressView;
        // The container for the progress view and video view
@@ -122,67 +124,76 @@ class HTML5VideoViewProxy extends Handler
        }
        // The spec says the timer should fire every 250 ms or less.
        private static final int TIMEUPDATE_PERIOD = 250;  // ms
        static boolean isVideoSelfEnded = false;

        private static final WebChromeClient.CustomViewCallback mCallback =
            new WebChromeClient.CustomViewCallback() {
                public void onCustomViewHidden() {
                    // At this point the videoview is pretty much destroyed.
                    // It listens to SurfaceHolder.Callback.SurfaceDestroyed event
                    // which happens when the video view is detached from its parent
                    // view. This happens in the WebChromeClient before this method
                    // is invoked.
                    mTimer.cancel();
                    mTimer = null;
                    if (mVideoView.isPlaying()) {
                        mVideoView.stopPlayback();
                    }
                    if (isVideoSelfEnded)
                        mCurrentProxy.dispatchOnEnded();
                    else
                        mCurrentProxy.dispatchOnPaused();
        private static boolean isVideoSelfEnded = false;
        // By using the baseLayer and the current video Layer ID, we can
        // identify the exact layer on the UI thread to use the SurfaceTexture.
        private static int mBaseLayer = 0;

                    // Re enable plugin views.
                    mCurrentProxy.getWebView().getViewManager().showAll();
        // TODO: [FULL SCREEN SUPPORT]

                    isVideoSelfEnded = false;
                    mCurrentProxy = null;
                    mLayout.removeView(mVideoView);
                    mVideoView = null;
                    if (mProgressView != null) {
                        mLayout.removeView(mProgressView);
                        mProgressView = null;
        // Every time webView setBaseLayer, this will be called.
        // When we found the Video layer, then we set the Surface Texture to it.
        // Otherwise, we may want to delete the Surface Texture to save memory.
        public static void setBaseLayer(int layer) {
            if (mHTML5VideoView != null) {
                mBaseLayer = layer;
                SurfaceTexture surfTexture = mHTML5VideoView.getSurfaceTexture();
                int textureName = mHTML5VideoView.getTextureName();

                int currentVideoLayerId = mHTML5VideoView.getVideoLayerId();
                if (layer != 0 && surfTexture != null && currentVideoLayerId != -1) {
                    boolean readyToUseSurfTex =
                        mHTML5VideoView.getReadyToUseSurfTex();
                    boolean foundInTree = nativeSendSurfaceTexture(surfTexture,
                            layer, currentVideoLayerId, textureName,
                            readyToUseSurfTex);
                    if (readyToUseSurfTex && !foundInTree) {
                        mHTML5VideoView.pauseAndDispatch(mCurrentProxy);
                        mHTML5VideoView.deleteSurfaceTexture();
                    }
                    mLayout = null;
                }
            };

        public static void play(String url, int time, HTML5VideoViewProxy proxy,
                WebChromeClient client) {
            if (mCurrentProxy == proxy) {
                if (!mVideoView.isPlaying()) {
                    mVideoView.start();
            }
                return;
        }

            if (mCurrentProxy != null) {
                // Some other video is already playing. Notify the caller that its playback ended.
                proxy.dispatchOnEnded();
                return;
        // When a WebView is paused, we also want to pause the video in it.
        public static void pauseAndDispatch() {
            if (mHTML5VideoView != null) {
                mHTML5VideoView.pauseAndDispatch(mCurrentProxy);
                // When switching out, clean the video content on the old page
                // by telling the layer not readyToUseSurfTex.
                setBaseLayer(mBaseLayer);
            }
        }

        // This is on the UI thread.
        // When native tell Java to play, we need to check whether or not it is
        // still the same video by using videoLayerId and treat it differently.
        public static void play(String url, int time, HTML5VideoViewProxy proxy,
                WebChromeClient client, int videoLayerId) {
            int currentVideoLayerId = -1;
            if (mHTML5VideoView != null)
                currentVideoLayerId = mHTML5VideoView.getVideoLayerId();

            if (currentVideoLayerId != videoLayerId
                || mHTML5VideoView.getSurfaceTexture() == null) {
                // Here, we handle the case when switching to a new video,
                // either inside a WebView or across WebViews
                // For switching videos within a WebView or across the WebView,
                // we need to pause the old one and re-create a new media player
                // inside the HTML5VideoView.
                if (mHTML5VideoView != null) {
                    mHTML5VideoView.pauseAndDispatch(mCurrentProxy);
                    // release the media player to avoid finalize error
                    mHTML5VideoView.release();
                }
                // HTML5VideoView is singleton, however, the internal state will
                // be reset since we are switching from one video to another.
                // Then we need to set up all the source/listener etc...
                mHTML5VideoView = HTML5VideoView.getInstance(videoLayerId);

                mCurrentProxy = proxy;
            // Create a FrameLayout that will contain the VideoView and the
            // progress view (if any).
            mLayout = new FrameLayout(proxy.getContext());
            FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
                    ViewGroup.LayoutParams.WRAP_CONTENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT,
                    Gravity.CENTER);
            mVideoView = new VideoView(proxy.getContext());
            mVideoView.setWillNotDraw(false);
            mVideoView.setMediaController(new MediaController(proxy.getContext()));

                // TODO: [FULL SCREEN SUPPORT]

                boolean isPrivate = mCurrentProxy.getWebView().isPrivateBrowsingEnabled();
                String cookieValue = CookieManager.getInstance().getCookie(url, isPrivate);
@@ -194,59 +205,68 @@ class HTML5VideoViewProxy extends Handler
                    headers.put(HIDE_URL_LOGS, "true");
                }

            mVideoView.setVideoURI(Uri.parse(url), headers);
            mVideoView.setOnCompletionListener(proxy);
            mVideoView.setOnPreparedListener(proxy);
            mVideoView.setOnErrorListener(proxy);
            mVideoView.seekTo(time);
            mLayout.addView(mVideoView, layoutParams);
            mProgressView = client.getVideoLoadingProgressView();
            if (mProgressView != null) {
                mLayout.addView(mProgressView, layoutParams);
                mProgressView.setVisibility(View.VISIBLE);
            }
            mLayout.setVisibility(View.VISIBLE);
                mHTML5VideoView.setVideoURI(url, headers);
                mHTML5VideoView.setOnCompletionListener(proxy);
                mHTML5VideoView.setOnPreparedListener(proxy);
                mHTML5VideoView.setOnErrorListener(proxy);
                mHTML5VideoView.setFrameAvailableListener(proxy);

                mHTML5VideoView.seekTo(time);

                mTimer = new Timer();
            mVideoView.start();
            client.onShowCustomView(mLayout, mCallback);
            // Plugins like Flash will draw over the video so hide
            // them while we're playing.
            mCurrentProxy.getWebView().getViewManager().hideAll();

            } else if (mCurrentProxy == proxy) {
                // Here, we handle the case when we keep playing with one video
                if (!mHTML5VideoView.isPlaying()) {
                    mHTML5VideoView.seekTo(time);
                    mHTML5VideoView.start();
                }
            } else if (mCurrentProxy != null) {
                // Some other video is already playing. Notify the caller that its playback ended.
                proxy.dispatchOnEnded();
            }
        }

        public static boolean isPlaying(HTML5VideoViewProxy proxy) {
            return (mCurrentProxy == proxy && mVideoView != null && mVideoView.isPlaying());
            return (mCurrentProxy == proxy && mHTML5VideoView != null
                    && mHTML5VideoView.isPlaying());
        }

        public static int getCurrentPosition() {
            int currentPosMs = 0;
            if (mVideoView != null) {
                currentPosMs = mVideoView.getCurrentPosition();
            if (mHTML5VideoView != null) {
                currentPosMs = mHTML5VideoView.getCurrentPosition();
            }
            return currentPosMs;
        }

        public static void seek(int time, HTML5VideoViewProxy proxy) {
            if (mCurrentProxy == proxy && time >= 0 && mVideoView != null) {
                mVideoView.seekTo(time);
            if (mCurrentProxy == proxy && time >= 0 && mHTML5VideoView != null) {
                mHTML5VideoView.seekTo(time);
            }
        }

        public static void pause(HTML5VideoViewProxy proxy) {
            if (mCurrentProxy == proxy && mVideoView != null) {
                mVideoView.pause();
            if (mCurrentProxy == proxy && mHTML5VideoView != null) {
                mHTML5VideoView.pause();
                mTimer.purge();
            }
        }

        public static void onPrepared() {
            if (mProgressView == null || mLayout == null) {
                return;
            }
            mHTML5VideoView.start();
            mTimer.schedule(new TimeupdateTask(mCurrentProxy), TIMEUPDATE_PERIOD, TIMEUPDATE_PERIOD);
            mProgressView.setVisibility(View.GONE);
            mLayout.removeView(mProgressView);
            mProgressView = null;
            // TODO: [FULL SCREEN SUPPORT]
        }

        public static void end() {
            if (mCurrentProxy != null) {
                if (isVideoSelfEnded)
                    mCurrentProxy.dispatchOnEnded();
                else
                    mCurrentProxy.dispatchOnPaused();
            }
            isVideoSelfEnded = false;
        }
    }

@@ -292,6 +312,14 @@ class HTML5VideoViewProxy extends Handler
        sendMessage(obtainMessage(TIMEUPDATE));
    }

    // When there is a frame ready from surface texture, we should tell WebView
    // to refresh.
    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        // TODO: This should support partial invalidation too.
        mWebView.invalidate();
    }

    // Handler for the messages from WebCore or Timer thread to the UI thread.
    @Override
    public void handleMessage(Message msg) {
@@ -300,8 +328,9 @@ class HTML5VideoViewProxy extends Handler
            case PLAY: {
                String url = (String) msg.obj;
                WebChromeClient client = mWebView.getWebChromeClient();
                int videoLayerID = msg.arg1;
                if (client != null) {
                    VideoPlayer.play(url, mSeekPosition, this, client);
                    VideoPlayer.play(url, mSeekPosition, this, client, videoLayerID);
                }
                break;
            }
@@ -318,6 +347,10 @@ class HTML5VideoViewProxy extends Handler
            case ENDED:
                if (msg.arg1 == 1)
                    VideoPlayer.isVideoSelfEnded = true;
                VideoPlayer.end();
                break;
                // TODO: [FULL SCREEN SUPPORT]
                // For full screen case, end may need hide the view.
            case ERROR: {
                WebChromeClient client = mWebView.getWebChromeClient();
                if (client != null) {
@@ -500,6 +533,10 @@ class HTML5VideoViewProxy extends Handler
        super(Looper.getMainLooper());
        // Save the WebView object.
        mWebView = webView;
        // Pass Proxy into webview, such that every time we have a setBaseLayer
        // call, we tell this Proxy to call the native to update the layer tree
        // for the Video Layer's surface texture info
        mWebView.setHTML5VideoViewProxy(this);
        // Save the native ptr
        mNativePointer = nativePtr;
        // create the message handler for this thread
@@ -565,7 +602,7 @@ class HTML5VideoViewProxy extends Handler
     * Play a video stream.
     * @param url is the URL of the video stream.
     */
    public void play(String url, int position) {
    public void play(String url, int position, int videoLayerID) {
        if (url == null) {
            return;
        }
@@ -573,8 +610,8 @@ class HTML5VideoViewProxy extends Handler
        if (position > 0) {
            seek(position);
        }

        Message message = obtainMessage(PLAY);
        message.arg1 = videoLayerID;
        message.obj = url;
        sendMessage(message);
    }
@@ -628,6 +665,14 @@ class HTML5VideoViewProxy extends Handler
        mPosterDownloader.start();
    }

    // These two function are called from UI thread only by WebView.
    public void setBaseLayer(int layer) {
        VideoPlayer.setBaseLayer(layer);
    }

    public void pauseAndDispatch() {
        VideoPlayer.pauseAndDispatch();
    }
    /**
     * The factory for HTML5VideoViewProxy instances.
     * @param webViewCore is the WebViewCore that is requesting the proxy.
@@ -647,4 +692,7 @@ class HTML5VideoViewProxy extends Handler
    private native void nativeOnPaused(int nativePointer);
    private native void nativeOnPosterFetched(Bitmap poster, int nativePointer);
    private native void nativeOnTimeupdate(int position, int nativePointer);
    private native static boolean nativeSendSurfaceTexture(SurfaceTexture texture,
            int baseLayer, int videoLayerId, int textureName,
            boolean updateTexture);
}
+23 −0
Original line number Diff line number Diff line
@@ -43,6 +43,7 @@ import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.SurfaceTexture;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.net.Proxy;
@@ -608,6 +609,10 @@ public class WebView extends AbsoluteLayout
    private int mTouchHighlightX;
    private int mTouchHighlightY;

    // Basically this proxy is used to tell the Video to update layer tree at
    // SetBaseLayer time and to pause when WebView paused.
    private HTML5VideoViewProxy mHTML5VideoViewProxy;

    /*
     * Private message ids
     */
@@ -1184,6 +1189,7 @@ public class WebView extends AbsoluteLayout
        // Initially use a size of two, since the user is likely to only hold
        // down two keys at a time (shift + another key)
        mKeysPressed = new Vector<Integer>(2);
        mHTML5VideoViewProxy = null ;
    }

    /**
@@ -2900,6 +2906,11 @@ public class WebView extends AbsoluteLayout
        if (!mIsPaused) {
            mIsPaused = true;
            mWebViewCore.sendMessage(EventHub.ON_PAUSE);
            // We want to pause the current playing video when switching out
            // from the current WebView/tab.
            if (mHTML5VideoViewProxy != null) {
                mHTML5VideoViewProxy.pauseAndDispatch();
            }
        }
    }

@@ -4034,6 +4045,9 @@ public class WebView extends AbsoluteLayout
        if (mNativeClass == 0)
            return;
        nativeSetBaseLayer(layer, invalRegion, showVisualIndicator);
        if (mHTML5VideoViewProxy != null) {
            mHTML5VideoViewProxy.setBaseLayer(layer);
        }
    }

    private void onZoomAnimationStart() {
@@ -8453,6 +8467,15 @@ public class WebView extends AbsoluteLayout
        nativeDraw(canvas, 0, 0, false);
    }

    /**
     * Enable the communication b/t the webView and VideoViewProxy
     *
     * @hide only used by the Browser
     */
    public void setHTML5VideoViewProxy(HTML5VideoViewProxy proxy) {
        mHTML5VideoViewProxy = proxy;
    }

    /**
     * Enable expanded tiles bound for smoother scrolling.
     *