/* * Copyright (C) 2009 The Android Open Source Project * Copyright (c) 2011, 2012, Code Aurora Forum. All rights reserved. * * 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.graphics.SurfaceTexture; import android.media.MediaPlayer; 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.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Map; /** * <p>Proxy for HTML5 video views.</p> */ class HTML5VideoViewProxy extends Handler implements MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener, MediaPlayer.OnInfoListener, MediaPlayer.OnVideoSizeChangedListener { // 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; private static final int BUFFERING_START = 105; private static final int BUFFERING_END = 106; private static final int INIT = 107; private static final int TERM = 108; private static final int SET_VOLUME = 109; private static final int LOAD = 110; private static final int LOAD_METADATA = 111; private static final int ENTER_FULLSCREEN = 112; private static final int EXIT_FULLSCREEN = 113; // 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 int PAUSED = 203; private static final int STOPFULLSCREEN = 204; private static final int SIZE_CHANGED = 205; private static final int PLAYING = 206; // 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 WebViewClassic instance that created this view. private WebViewClassic 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; // The video layer ID private int mVideoLayerId; // A helper class to control the playback. This executes on the UI thread! private final class VideoPlayer { private HTML5VideoViewProxy mProxy; private HTML5VideoView mHTML5VideoView; private boolean isVideoSelfEnded = false; // The cached volume before HTML5VideoView is initialized. // This should be set back to -1.0f every time after the // function mHTML5VideoView.setVolume is called. private float mCachedVolume = -1.0f; // Cached media position used to preserve playback position when // resuming suspended video private int mCachedPosition; private void setPlayerBuffering(boolean playerBuffering) { mHTML5VideoView.setPlayerBuffering(playerBuffering); } VideoPlayer(HTML5VideoViewProxy proxy) { mProxy = proxy; } // Every time webView setBaseLayer, this will be called. // When we found the Video layer, then we set the Surface Texture to it. // By using the baseLayer and the current video Layer ID, we can // identify the exact layer on the UI thread to use the SurfaceTexture. // We should never save the base layer handle since its lifetime is not // guaranteed outside of the function call from WebView::setBaseLayer. // // This function allows layer value to be null. If layer is null, only // the player state will be set in native code. This allows the proxy to // save the player state in the native video layer. public void setBaseLayer(int layer) { if (mHTML5VideoView != null) { int playerState = mHTML5VideoView.getCurrentState(); if (mHTML5VideoView.getPlayerBuffering()) playerState = HTML5VideoView.STATE_BUFFERING; nativeSendSurfaceTexture(mHTML5VideoView.getSurfaceTexture(), layer, mVideoLayerId, mHTML5VideoView.getTextureName(), playerState, mNativePointer); // Re-attach the inline GL context // TODO: Find a better place to call this. mHTML5VideoView.attachToInlineGlContextIfNeeded(); } } public void suspend() { if (mHTML5VideoView != null) { mHTML5VideoView.pause(); mCachedPosition = getCurrentPosition(); mHTML5VideoView.release(); // Call setBaseLayer to update VideoLayerAndroid player state // This is important for flagging the associated texture for recycling setBaseLayer(0); mHTML5VideoView = null; // isVideoSelfEnded is false when video playback // has ended but is not complete. // isVideoSelfEnded is true only when playback is complete. isVideoSelfEnded = false; end(); } } public void enterFullscreenVideo(String url, float x, float y, float w, float h) { if (ensureHTML5VideoView(url, mCachedPosition, false)) { mHTML5VideoView.prepareDataAndDisplayMode(); } mHTML5VideoView.enterFullscreenVideoState(mWebView, x, y, w, h); } public void exitFullscreenVideo(float x, float y, float w, float h) { if (mHTML5VideoView != null) { mHTML5VideoView.exitFullscreenVideoState(x, y, w, h); } } public void webkitExitFullscreenVideo() { if (!mHTML5VideoView.fullscreenExited() && mHTML5VideoView.isFullscreenMode()) { WebChromeClient client = mWebView.getWebChromeClient(); if (client != null) { client.onHideCustomView(); } } } // This is on the UI thread. public void loadMetadata(String url) { if (ensureHTML5VideoView(url, 0, false)) { mHTML5VideoView.retrieveMetadata(mProxy); } } public void load(String url) { if (ensureHTML5VideoView(url, 0, false)) { mHTML5VideoView.prepareDataAndDisplayMode(); } } public void play(String url, int time) { if (ensureHTML5VideoView(url, time, true)) { mHTML5VideoView.prepareDataAndDisplayMode(); mHTML5VideoView.seekTo(time); } else { // Here, we handle the case when we keep playing with one video if (!mHTML5VideoView.isPlaying()) { mHTML5VideoView.start(); setBaseLayer(0); } } } public boolean isPlaying() { return (mHTML5VideoView != null && mHTML5VideoView.isPlaying()); } public int getCurrentPosition() { int currentPosMs = 0; if (mHTML5VideoView != null) { currentPosMs = mHTML5VideoView.getCurrentPosition(); } return currentPosMs; } public void seek(int time) { if (time >= 0 && mHTML5VideoView != null) { mHTML5VideoView.seekTo(time); } } public void pause() { if (mHTML5VideoView != null) { mHTML5VideoView.pause(); } } public void onPrepared() { if (mCachedVolume >= 0.0f) { mHTML5VideoView.setVolume(mCachedVolume); mCachedVolume = -1.0f; } setBaseLayer(0); } public void end() { if (mHTML5VideoView != null) mHTML5VideoView.showControllerInFullscreen(); if (mProxy != null) { if (isVideoSelfEnded) mProxy.dispatchOnEnded(); else mProxy.dispatchOnPaused(); } isVideoSelfEnded = false; } public void setVolume(float volume) { if (mHTML5VideoView != null) { mHTML5VideoView.setVolume(volume); mCachedVolume = -1.0f; } else { mCachedVolume = volume; } } // Return true if we have to allocate a new HTML5VideoView. // Otherwise return false and we can reuse the previously allocated HTML5VideoView private boolean ensureHTML5VideoView(String url, int time, boolean willPlay) { if (mHTML5VideoView == null) { mHTML5VideoView = new HTML5VideoView(mProxy, time); mHTML5VideoView.setStartWhenPrepared(willPlay); mHTML5VideoView.setVideoURI(url); return true; } return false; } public boolean isPrepared() { return mHTML5VideoView.getCurrentState() >= HTML5VideoView.STATE_PREPARED; } } private VideoPlayer mVideoPlayer; // A bunch event listeners for our VideoView // MediaPlayer.OnPreparedListener public void onPrepared(MediaPlayer mp) { mVideoPlayer.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.OnVideoSizeChangedListener public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { Message msg = Message.obtain(mWebCoreHandler, SIZE_CHANGED); Map<String, Object> map = new HashMap<String, Object>(); if (mVideoPlayer.isPrepared()) map.put("dur", new Integer(mp.getDuration())); else map.put("dur", new Integer(0)); map.put("width", new Integer(width)); map.put("height", new Integer(height)); msg.obj = map; mWebCoreHandler.sendMessage(msg); } // MediaPlayer.OnCompletionListener; public void onCompletion(MediaPlayer mp) { // The video ended by itself, so we need to // send a message to the UI thread to dismiss // the video view and to return to the WebView. // arg1 == 1 means the video ends by itself. sendMessage(obtainMessage(ENDED, 1, 0)); } // MediaPlayer.OnErrorListener public boolean onError(MediaPlayer mp, int what, int extra) { sendMessage(obtainMessage(ERROR)); return false; } public void dispatchOnEnded() { Message msg = Message.obtain(mWebCoreHandler, ENDED); mWebCoreHandler.sendMessage(msg); } public void dispatchOnPaused() { Message msg = Message.obtain(mWebCoreHandler, PAUSED); mWebCoreHandler.sendMessage(msg); } public void dispatchOnPlaying() { Message msg = Message.obtain(mWebCoreHandler, PLAYING); mWebCoreHandler.sendMessage(msg); } public void dispatchOnStopFullscreen() { Message msg = Message.obtain(mWebCoreHandler, STOPFULLSCREEN); mWebCoreHandler.sendMessage(msg); } public void updateSizeAndDuration(int width, int height, int duration) { Message msg = Message.obtain(mWebCoreHandler, SIZE_CHANGED); Map<String, Object> map = new HashMap<String, Object>(); map.put("dur", new Integer(duration)); map.put("width", new Integer(width)); map.put("height", new Integer(height)); msg.obj = map; mWebCoreHandler.sendMessage(msg); } 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; int seekPosition = msg.arg1; mVideoPlayer.play(url, seekPosition); break; } case LOAD_METADATA: { String url = (String) msg.obj; mVideoPlayer.loadMetadata(url); break; } case LOAD: { String url = (String) msg.obj; mVideoPlayer.load(url); break; } case SEEK: { Integer time = (Integer) msg.obj; mSeekPosition = time; mVideoPlayer.seek(mSeekPosition); break; } case PAUSE: { mVideoPlayer.pause(); break; } case ENDED: if (msg.arg1 == 1) mVideoPlayer.isVideoSelfEnded = true; mVideoPlayer.end(); 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; } case TIMEUPDATE: { if (mVideoPlayer.isPlaying()) { sendTimeupdate(); } break; } case BUFFERING_START: { mVideoPlayer.setPlayerBuffering(true); break; } case BUFFERING_END: { mVideoPlayer.setPlayerBuffering(false); break; } case INIT: { // 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.registerHTML5VideoViewProxy(this); break; } case TERM: { mVideoPlayer.suspend(); mWebView.unregisterHTML5VideoViewProxy(this); break; } case SET_VOLUME: { float vol = ((Float)msg.obj).floatValue(); mVideoPlayer.setVolume(vol); break; } case ENTER_FULLSCREEN: { InlineVideoInfo info = (InlineVideoInfo)msg.obj; mVideoPlayer.enterFullscreenVideo(info.getUrl(), info.getX(), info.getY(), info.getWidth(), info.getHeight()); break; } case EXIT_FULLSCREEN: { InlineVideoInfo info = (InlineVideoInfo)msg.obj; mVideoPlayer.exitFullscreenVideo(info.getX(), info.getY(), info.getWidth(), info.getHeight()); 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 URL 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) { try { mUrl = new URL(url); } catch (MalformedURLException e) { mUrl = null; } mProxy = proxy; mHandler = new Handler(); } // Start the download. Called on WebCore thread. public void start() { retainQueue(); if (mUrl == null) { return; } // Only support downloading posters over http/https. // FIXME: Add support for other schemes. WebKit seems able to load // posters over other schemes e.g. file://, but gets the dimensions wrong. String protocol = mUrl.getProtocol(); if ("http".equals(protocol) || "https".equals(protocol)) { mRequestHandle = mRequestQueue.queueRequest(mUrl.toString(), "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. try { mUrl = new URL(mHeaders.getLocation()); } catch (MalformedURLException e) { mUrl = null; } if (mUrl != null) { mHandler.post(new Runnable() { public void run() { if (mRequestHandle != null) { mRequestHandle.setupRedirect(mUrl.toString(), 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(WebViewClassic webView, int nativePtr, int videoLayerId) { // This handler is for the main (UI) thread. super(Looper.getMainLooper()); // Save the WebView object. mWebView = webView; // Save the native ptr mNativePointer = nativePtr; // Save the videoLayerId. This is needed early in order to support fullscreen mode // before video playback mVideoLayerId = videoLayerId; // create the message handler for this thread createWebCoreHandler(); mVideoPlayer = new VideoPlayer(this); Message message = obtainMessage(INIT); sendMessage(message); } 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 SIZE_CHANGED: { 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"); nativeOnSizeChanged(duration.intValue(), width.intValue(), height.intValue(), mNativePointer); break; } case ENDED: mSeekPosition = 0; nativeOnEnded(mNativePointer); break; case PAUSED: nativeOnPaused(mNativePointer); break; case PLAYING: nativeOnPlaying(mNativePointer); break; case POSTER_FETCHED: Bitmap poster = (Bitmap) msg.obj; nativeOnPosterFetched(poster, mNativePointer); break; case TIMEUPDATE: nativeOnTimeupdate(msg.arg1, mNativePointer); break; case STOPFULLSCREEN: nativeOnStopFullscreen(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 = mVideoPlayer.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, int position) { if (url == null) { return; } Message message = obtainMessage(PLAY); message.arg1 = position; message.obj = url; sendMessage(message); } /** * Load a video stream. * @param url is the URL of the video stream. */ public void loadVideo(String url) { if (url == null) { return; } Message message = obtainMessage(LOAD); message.obj = url; sendMessage(message); } /** * Load video metadata. * @param url is the URL of the video stream. */ public void loadMetadata(String url) { if (url == null) { return; } Message message = obtainMessage(LOAD_METADATA); 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(); } Message message = obtainMessage(TERM); sendMessage(message); 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(); } public void enterFullscreen(String url, float x, float y, float w, float h) { if (url == null) return; Message message = obtainMessage(ENTER_FULLSCREEN); message.obj = new InlineVideoInfo(url, x, y, w, h); sendMessage(message); } public void exitFullscreen(float x, float y, float w, float h) { Message message = obtainMessage(EXIT_FULLSCREEN); message.obj = new InlineVideoInfo(null, x, y, w, h); sendMessage(message); } private static final class InlineVideoInfo { private String mUrl; private float mX; private float mY; private float mWidth; private float mHeight; public InlineVideoInfo(String url, float x, float y, float w, float h) { mUrl = url; mX = x; mY = y; mWidth = w; mHeight = h; } public String getUrl() { return mUrl; } public float getX() { return mX; } public float getY() { return mY; } public float getWidth() { return mWidth; } public float getHeight() { return mHeight; } } // These functions are called from UI thread only by WebView. public void setBaseLayer(int layer) { mVideoPlayer.setBaseLayer(layer); } public void pauseAndDispatch() { // mVideoPlayer.pause will always dispatch notification mVideoPlayer.pause(); } public void suspend() { mVideoPlayer.suspend(); } public void webkitEnterFullscreen() { nativePrepareEnterFullscreen(mNativePointer); } public void prepareExitFullscreen() { nativePrepareExitFullscreen(mNativePointer); } public void webKitExitFullscreen() { mVideoPlayer.webkitExitFullscreenVideo(); } public int getVideoLayerId() { return mVideoLayerId; } // End functions called from UI thread only by WebView /** * Change the volume of the playback */ public void setVolume(float volume) { Message message = obtainMessage(SET_VOLUME); message.obj = new Float(volume); sendMessage(message); } /** * 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, int videoLayerId) { return new HTML5VideoViewProxy(webViewCore.getWebViewClassic(), nativePtr, videoLayerId); } /* package */ WebViewClassic getWebView() { return mWebView; } private native void nativeOnPrepared(int duration, int width, int height, int nativePointer); private native void nativeOnSizeChanged(int duration, int width, int height, int nativePointer); private native void nativeOnEnded(int nativePointer); private native void nativeOnPaused(int nativePointer); private native void nativeOnPlaying(int nativePointer); private native void nativeOnPosterFetched(Bitmap poster, int nativePointer); private native void nativeOnTimeupdate(int position, int nativePointer); private native void nativeOnStopFullscreen(int nativePointer); private native static boolean nativeSendSurfaceTexture(SurfaceTexture texture, int baseLayer, int videoLayerId, int textureName, int playerState, int nativePointer); private native void nativePrepareEnterFullscreen(int nativePointer); private native void nativePrepareExitFullscreen(int nativePoint); @Override public boolean onInfo(MediaPlayer mp, int what, int extra) { if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) { sendMessage(obtainMessage(BUFFERING_START, what, extra)); } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) { sendMessage(obtainMessage(BUFFERING_END, what, extra)); } return false; } }