diff options
Diffstat (limited to 'core/java/android/webkit/HTML5VideoViewProxy.java')
-rw-r--r-- | core/java/android/webkit/HTML5VideoViewProxy.java | 506 |
1 files changed, 506 insertions, 0 deletions
diff --git a/core/java/android/webkit/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java new file mode 100644 index 0000000..b7a9065 --- /dev/null +++ b/core/java/android/webkit/HTML5VideoViewProxy.java @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnPreparedListener; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.net.http.EventHandler; +import android.net.http.Headers; +import android.net.http.RequestHandle; +import android.net.http.RequestQueue; +import android.net.http.SslCertificate; +import android.net.http.SslError; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.view.MotionEvent; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsoluteLayout; +import android.widget.FrameLayout; +import android.widget.MediaController; +import android.widget.VideoView; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * <p>Proxy for HTML5 video views. + */ +class HTML5VideoViewProxy extends Handler + implements MediaPlayer.OnPreparedListener, + MediaPlayer.OnCompletionListener, + MediaPlayer.OnErrorListener { + // Logging tag. + private static final String LOGTAG = "HTML5VideoViewProxy"; + + // Message Ids for WebCore thread -> UI thread communication. + private static final int PLAY = 100; + private static final int SEEK = 101; + private static final int PAUSE = 102; + private static final int ERROR = 103; + private static final int LOAD_DEFAULT_POSTER = 104; + + // Message Ids to be handled on the WebCore thread + private static final int PREPARED = 200; + private static final int ENDED = 201; + private static final int POSTER_FETCHED = 202; + + // The C++ MediaPlayerPrivateAndroid object. + int mNativePointer; + // The handler for WebCore thread messages; + private Handler mWebCoreHandler; + // The WebView instance that created this view. + private WebView mWebView; + // The poster image to be shown when the video is not playing. + // This ref prevents the bitmap from being GC'ed. + private Bitmap mPoster; + // The poster downloader. + private PosterDownloader mPosterDownloader; + // The seek position. + private int mSeekPosition; + // A helper class to control the playback. This executes on the UI thread! + private static final class VideoPlayer { + // The proxy that is currently playing (if any). + 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; + // The progress view. + private static View mProgressView; + // The container for the progress view and video view + private static FrameLayout mLayout; + + 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. + mCurrentProxy.playbackEnded(); + mCurrentProxy = null; + mLayout.removeView(mVideoView); + mVideoView = null; + if (mProgressView != null) { + mLayout.removeView(mProgressView); + mProgressView = null; + } + mLayout = null; + } + }; + + public static void play(String url, int time, HTML5VideoViewProxy proxy, + WebChromeClient client) { + if (mCurrentProxy != null) { + // Some other video is already playing. Notify the caller that its playback ended. + proxy.playbackEnded(); + return; + } + 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())); + mVideoView.setVideoURI(Uri.parse(url)); + 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); + mVideoView.start(); + client.onShowCustomView(mLayout, mCallback); + } + + public static void seek(int time, HTML5VideoViewProxy proxy) { + if (mCurrentProxy == proxy && time >= 0 && mVideoView != null) { + mVideoView.seekTo(time); + } + } + + public static void pause(HTML5VideoViewProxy proxy) { + if (mCurrentProxy == proxy && mVideoView != null) { + mVideoView.pause(); + } + } + + public static void onPrepared() { + if (mProgressView == null || mLayout == null) { + return; + } + mProgressView.setVisibility(View.GONE); + mLayout.removeView(mProgressView); + mProgressView = null; + } + } + + // A bunch event listeners for our VideoView + // MediaPlayer.OnPreparedListener + public void onPrepared(MediaPlayer mp) { + VideoPlayer.onPrepared(); + Message msg = Message.obtain(mWebCoreHandler, PREPARED); + Map<String, Object> map = new HashMap<String, Object>(); + map.put("dur", new Integer(mp.getDuration())); + map.put("width", new Integer(mp.getVideoWidth())); + map.put("height", new Integer(mp.getVideoHeight())); + msg.obj = map; + mWebCoreHandler.sendMessage(msg); + } + + // MediaPlayer.OnCompletionListener; + public void onCompletion(MediaPlayer mp) { + playbackEnded(); + } + + // MediaPlayer.OnErrorListener + public boolean onError(MediaPlayer mp, int what, int extra) { + sendMessage(obtainMessage(ERROR)); + return false; + } + + public void playbackEnded() { + Message msg = Message.obtain(mWebCoreHandler, ENDED); + mWebCoreHandler.sendMessage(msg); + } + + // Handler for the messages from WebCore thread to the UI thread. + @Override + public void handleMessage(Message msg) { + // This executes on the UI thread. + switch (msg.what) { + case PLAY: { + String url = (String) msg.obj; + WebChromeClient client = mWebView.getWebChromeClient(); + if (client != null) { + VideoPlayer.play(url, mSeekPosition, this, client); + } + break; + } + case SEEK: { + Integer time = (Integer) msg.obj; + mSeekPosition = time; + VideoPlayer.seek(mSeekPosition, this); + break; + } + case PAUSE: { + VideoPlayer.pause(this); + break; + } + case ERROR: { + WebChromeClient client = mWebView.getWebChromeClient(); + if (client != null) { + client.onHideCustomView(); + } + break; + } + case LOAD_DEFAULT_POSTER: { + WebChromeClient client = mWebView.getWebChromeClient(); + if (client != null) { + doSetPoster(client.getDefaultVideoPoster()); + } + break; + } + } + } + + // Everything below this comment executes on the WebCore thread, except for + // the EventHandler methods, which are called on the network thread. + + // A helper class that knows how to download posters + private static final class PosterDownloader implements EventHandler { + // The request queue. This is static as we have one queue for all posters. + private static RequestQueue mRequestQueue; + private static int mQueueRefCount = 0; + // The poster URL + private String mUrl; + // The proxy we're doing this for. + private final HTML5VideoViewProxy mProxy; + // The poster bytes. We only touch this on the network thread. + private ByteArrayOutputStream mPosterBytes; + // The request handle. We only touch this on the WebCore thread. + private RequestHandle mRequestHandle; + // The response status code. + private int mStatusCode; + // The response headers. + private Headers mHeaders; + // The handler to handle messages on the WebCore thread. + private Handler mHandler; + + public PosterDownloader(String url, HTML5VideoViewProxy proxy) { + mUrl = url; + mProxy = proxy; + mHandler = new Handler(); + } + // Start the download. Called on WebCore thread. + public void start() { + retainQueue(); + mRequestHandle = mRequestQueue.queueRequest(mUrl, "GET", null, this, null, 0); + } + // Cancel the download if active and release the queue. Called on WebCore thread. + public void cancelAndReleaseQueue() { + if (mRequestHandle != null) { + mRequestHandle.cancel(); + mRequestHandle = null; + } + releaseQueue(); + } + // EventHandler methods. Executed on the network thread. + public void status(int major_version, + int minor_version, + int code, + String reason_phrase) { + mStatusCode = code; + } + + public void headers(Headers headers) { + mHeaders = headers; + } + + public void data(byte[] data, int len) { + if (mPosterBytes == null) { + mPosterBytes = new ByteArrayOutputStream(); + } + mPosterBytes.write(data, 0, len); + } + + public void endData() { + if (mStatusCode == 200) { + if (mPosterBytes.size() > 0) { + Bitmap poster = BitmapFactory.decodeByteArray( + mPosterBytes.toByteArray(), 0, mPosterBytes.size()); + mProxy.doSetPoster(poster); + } + cleanup(); + } else if (mStatusCode >= 300 && mStatusCode < 400) { + // We have a redirect. + mUrl = mHeaders.getLocation(); + if (mUrl != null) { + mHandler.post(new Runnable() { + public void run() { + if (mRequestHandle != null) { + mRequestHandle.setupRedirect(mUrl, mStatusCode, + new HashMap<String, String>()); + } + } + }); + } + } + } + + public void certificate(SslCertificate certificate) { + // Don't care. + } + + public void error(int id, String description) { + cleanup(); + } + + public boolean handleSslErrorRequest(SslError error) { + // Don't care. If this happens, data() will never be called so + // mPosterBytes will never be created, so no need to call cleanup. + return false; + } + // Tears down the poster bytes stream. Called on network thread. + private void cleanup() { + if (mPosterBytes != null) { + try { + mPosterBytes.close(); + } catch (IOException ignored) { + // Ignored. + } finally { + mPosterBytes = null; + } + } + } + + // Queue management methods. Called on WebCore thread. + private void retainQueue() { + if (mRequestQueue == null) { + mRequestQueue = new RequestQueue(mProxy.getContext()); + } + mQueueRefCount++; + } + + private void releaseQueue() { + if (mQueueRefCount == 0) { + return; + } + if (--mQueueRefCount == 0) { + mRequestQueue.shutdown(); + mRequestQueue = null; + } + } + } + + /** + * Private constructor. + * @param webView is the WebView that hosts the video. + * @param nativePtr is the C++ pointer to the MediaPlayerPrivate object. + */ + private HTML5VideoViewProxy(WebView webView, int nativePtr) { + // This handler is for the main (UI) thread. + super(Looper.getMainLooper()); + // Save the WebView object. + mWebView = webView; + // Save the native ptr + mNativePointer = nativePtr; + // create the message handler for this thread + createWebCoreHandler(); + } + + private void createWebCoreHandler() { + mWebCoreHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case PREPARED: { + Map<String, Object> map = (Map<String, Object>) msg.obj; + Integer duration = (Integer) map.get("dur"); + Integer width = (Integer) map.get("width"); + Integer height = (Integer) map.get("height"); + nativeOnPrepared(duration.intValue(), width.intValue(), + height.intValue(), mNativePointer); + break; + } + case ENDED: + nativeOnEnded(mNativePointer); + break; + case POSTER_FETCHED: + Bitmap poster = (Bitmap) msg.obj; + nativeOnPosterFetched(poster, mNativePointer); + break; + } + } + }; + } + + private void doSetPoster(Bitmap poster) { + if (poster == null) { + return; + } + // Save a ref to the bitmap and send it over to the WebCore thread. + mPoster = poster; + Message msg = Message.obtain(mWebCoreHandler, POSTER_FETCHED); + msg.obj = poster; + mWebCoreHandler.sendMessage(msg); + } + + public Context getContext() { + return mWebView.getContext(); + } + + // The public methods below are all called from WebKit only. + /** + * Play a video stream. + * @param url is the URL of the video stream. + */ + public void play(String url) { + if (url == null) { + return; + } + Message message = obtainMessage(PLAY); + message.obj = url; + sendMessage(message); + } + + /** + * Seek into the video stream. + * @param time is the position in the video stream. + */ + public void seek(int time) { + Message message = obtainMessage(SEEK); + message.obj = new Integer(time); + sendMessage(message); + } + + /** + * Pause the playback. + */ + public void pause() { + Message message = obtainMessage(PAUSE); + sendMessage(message); + } + + /** + * Tear down this proxy object. + */ + public void teardown() { + // This is called by the C++ MediaPlayerPrivate dtor. + // Cancel any active poster download. + if (mPosterDownloader != null) { + mPosterDownloader.cancelAndReleaseQueue(); + } + mNativePointer = 0; + } + + /** + * Load the poster image. + * @param url is the URL of the poster image. + */ + public void loadPoster(String url) { + if (url == null) { + Message message = obtainMessage(LOAD_DEFAULT_POSTER); + sendMessage(message); + return; + } + // Cancel any active poster download. + if (mPosterDownloader != null) { + mPosterDownloader.cancelAndReleaseQueue(); + } + // Load the poster asynchronously + mPosterDownloader = new PosterDownloader(url, this); + mPosterDownloader.start(); + } + + /** + * The factory for HTML5VideoViewProxy instances. + * @param webViewCore is the WebViewCore that is requesting the proxy. + * + * @return a new HTML5VideoViewProxy object. + */ + public static HTML5VideoViewProxy getInstance(WebViewCore webViewCore, int nativePtr) { + return new HTML5VideoViewProxy(webViewCore.getWebView(), nativePtr); + } + + private native void nativeOnPrepared(int duration, int width, int height, int nativePointer); + private native void nativeOnEnded(int nativePointer); + private native void nativeOnPosterFetched(Bitmap poster, int nativePointer); +} |