| /* |
| * 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; |
| import java.util.Timer; |
| import java.util.TimerTask; |
| |
| /** |
| * <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; |
| |
| private static final String COOKIE = "Cookie"; |
| |
| // Timer thread -> UI thread |
| private static final int TIMEUPDATE = 300; |
| |
| // 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; |
| // The timer for timeupate events. |
| // See http://www.whatwg.org/specs/web-apps/current-work/#event-media-timeupdate |
| private static Timer mTimer; |
| private static final class TimeupdateTask extends TimerTask { |
| private HTML5VideoViewProxy mProxy; |
| |
| public TimeupdateTask(HTML5VideoViewProxy proxy) { |
| mProxy = proxy; |
| } |
| |
| public void run() { |
| mProxy.onTimeupdate(); |
| } |
| } |
| // The spec says the timer should fire every 250 ms or less. |
| private static final int TIMEUPDATE_PERIOD = 250; // ms |
| |
| 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; |
| mCurrentProxy.playbackEnded(); |
| mCurrentProxy = null; |
| mVideoView.stopPlayback(); |
| 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 == proxy) { |
| if (!mVideoView.isPlaying()) { |
| mVideoView.start(); |
| } |
| return; |
| } |
| |
| 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())); |
| |
| String cookieValue = CookieManager.getInstance().getCookie(url); |
| Map<String, String> headers = null; |
| if (cookieValue != null) { |
| headers = new HashMap<String, String>(); |
| headers.put(COOKIE, cookieValue); |
| } |
| |
| 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); |
| mTimer = new Timer(); |
| mVideoView.start(); |
| client.onShowCustomView(mLayout, mCallback); |
| } |
| |
| public static boolean isPlaying(HTML5VideoViewProxy proxy) { |
| return (mCurrentProxy == proxy && mVideoView != null && mVideoView.isPlaying()); |
| } |
| |
| public static int getCurrentPosition() { |
| int currentPosMs = 0; |
| if (mVideoView != null) { |
| currentPosMs = mVideoView.getCurrentPosition(); |
| } |
| return currentPosMs; |
| } |
| |
| 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(); |
| mTimer.purge(); |
| } |
| } |
| |
| public static void onPrepared() { |
| if (mProgressView == null || mLayout == null) { |
| return; |
| } |
| mTimer.schedule(new TimeupdateTask(mCurrentProxy), TIMEUPDATE_PERIOD, TIMEUPDATE_PERIOD); |
| 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); |
| // also send a message to ourselves to return to the WebView |
| sendMessage(obtainMessage(ENDED)); |
| } |
| |
| public void onTimeupdate() { |
| sendMessage(obtainMessage(TIMEUPDATE)); |
| } |
| |
| // Handler for the messages from WebCore or Timer 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 ENDED: |
| 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; |
| } |
| case TIMEUPDATE: { |
| if (VideoPlayer.isPlaying(this)) { |
| sendTimeupdate(); |
| } |
| 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; |
| case TIMEUPDATE: |
| nativeOnTimeupdate(msg.arg1, 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); |
| } |
| |
| private void sendTimeupdate() { |
| Message msg = Message.obtain(mWebCoreHandler, TIMEUPDATE); |
| msg.arg1 = VideoPlayer.getCurrentPosition(); |
| 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); |
| private native void nativeOnTimeupdate(int position, int nativePointer); |
| } |