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

Commit 2fc79763 authored by Teng-Hui Zhu's avatar Teng-Hui Zhu Committed by Android (Google) Code Review
Browse files

Merge "Inline HTML5 Video support" into honeycomb-mr1

parents cf63b66c 661e8b1f
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() {
@@ -8512,6 +8526,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.
     *