diff options
| author | 2011-03-10 17:13:03 -0800 | |
|---|---|---|
| committer | 2011-03-10 17:13:03 -0800 | |
| commit | 37ee248b526d3cdefbb0d401ec14b740b655896b (patch) | |
| tree | c324def52b72050637b8464f4798040212cab2a0 | |
| parent | 6615362b14131052c5055c0b17c3a2ade474a57d (diff) | |
| parent | 2fc7976399e5f0f4c6e1dae528df727e7dcbdc8e (diff) | |
am 2fc79763: Merge "Inline HTML5 Video support" into honeycomb-mr1
* commit '2fc7976399e5f0f4c6e1dae528df727e7dcbdc8e':
Inline HTML5 Video support
| -rw-r--r-- | core/java/android/webkit/HTML5VideoView.java | 211 | ||||
| -rw-r--r-- | core/java/android/webkit/HTML5VideoViewProxy.java | 246 | ||||
| -rw-r--r-- | core/java/android/webkit/WebView.java | 23 |
3 files changed, 381 insertions, 99 deletions
diff --git a/core/java/android/webkit/HTML5VideoView.java b/core/java/android/webkit/HTML5VideoView.java new file mode 100644 index 000000000000..2312160f5c66 --- /dev/null +++ b/core/java/android/webkit/HTML5VideoView.java @@ -0,0 +1,211 @@ + +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; + } + +} diff --git a/core/java/android/webkit/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java index 85763da46e0d..b614d8f5467c 100644 --- a/core/java/android/webkit/HTML5VideoViewProxy.java +++ b/core/java/android/webkit/HTML5VideoViewProxy.java @@ -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,131 +124,149 @@ 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(); + 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; + + // TODO: [FULL SCREEN SUPPORT] + + // 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(); } - if (isVideoSelfEnded) - mCurrentProxy.dispatchOnEnded(); - else - mCurrentProxy.dispatchOnPaused(); - - // Re enable plugin views. - mCurrentProxy.getWebView().getViewManager().showAll(); - - isVideoSelfEnded = false; - mCurrentProxy = null; - mLayout.removeView(mVideoView); - mVideoView = null; - if (mProgressView != null) { - mLayout.removeView(mProgressView); - mProgressView = null; - } - mLayout = null; } - }; + } + } + + // 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) { - if (mCurrentProxy == proxy) { - if (!mVideoView.isPlaying()) { - mVideoView.start(); + 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(); } - return; - } + // 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); - if (mCurrentProxy != null) { - // Some other video is already playing. Notify the caller that its playback ended. - proxy.dispatchOnEnded(); - return; - } + mCurrentProxy = proxy; - 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())); - - boolean isPrivate = mCurrentProxy.getWebView().isPrivateBrowsingEnabled(); - String cookieValue = CookieManager.getInstance().getCookie(url, isPrivate); - Map<String, String> headers = new HashMap<String, String>(); - if (cookieValue != null) { - headers.put(COOKIE, cookieValue); - } - if (isPrivate) { - headers.put(HIDE_URL_LOGS, "true"); - } + // TODO: [FULL SCREEN SUPPORT] + + boolean isPrivate = mCurrentProxy.getWebView().isPrivateBrowsingEnabled(); + String cookieValue = CookieManager.getInstance().getCookie(url, isPrivate); + Map<String, String> headers = new HashMap<String, String>(); + if (cookieValue != null) { + headers.put(COOKIE, cookieValue); + } + if (isPrivate) { + headers.put(HIDE_URL_LOGS, "true"); + } + + mHTML5VideoView.setVideoURI(url, headers); + mHTML5VideoView.setOnCompletionListener(proxy); + mHTML5VideoView.setOnPreparedListener(proxy); + mHTML5VideoView.setOnErrorListener(proxy); + mHTML5VideoView.setFrameAvailableListener(proxy); - 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); + mHTML5VideoView.seekTo(time); + + mTimer = new Timer(); + + } 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(); } - mLayout.setVisibility(View.VISIBLE); - 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(); } 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); } diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index a667ec3688ef..18f92a4438e5 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -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() { @@ -8513,6 +8527,15 @@ public class WebView extends AbsoluteLayout } /** + * 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. * * @hide only used by the Browser |