HTML5VideoViewProxy.java revision 290c34ac3a36b407b74b42c37501c2d0cdac70ad
1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.webkit; 18 19import android.content.Context; 20import android.graphics.Bitmap; 21import android.graphics.BitmapFactory; 22import android.media.MediaPlayer; 23import android.media.MediaPlayer.OnPreparedListener; 24import android.media.MediaPlayer.OnCompletionListener; 25import android.media.MediaPlayer.OnErrorListener; 26import android.net.http.EventHandler; 27import android.net.http.Headers; 28import android.net.http.RequestHandle; 29import android.net.http.RequestQueue; 30import android.net.http.SslCertificate; 31import android.net.http.SslError; 32import android.net.Uri; 33import android.os.Bundle; 34import android.os.Handler; 35import android.os.Looper; 36import android.os.Message; 37import android.util.Log; 38import android.view.MotionEvent; 39import android.view.View; 40import android.view.ViewGroup; 41import android.webkit.ViewManager.ChildView; 42import android.widget.AbsoluteLayout; 43import android.widget.ImageView; 44import android.widget.MediaController; 45import android.widget.VideoView; 46 47import java.io.ByteArrayOutputStream; 48import java.io.IOException; 49import java.util.HashMap; 50import java.util.Map; 51 52/** 53 * <p>Proxy for HTML5 video views. 54 */ 55class HTML5VideoViewProxy extends Handler 56 implements MediaPlayer.OnPreparedListener, 57 MediaPlayer.OnCompletionListener { 58 // Logging tag. 59 private static final String LOGTAG = "HTML5VideoViewProxy"; 60 61 // Message Ids for WebCore thread -> UI thread communication. 62 private static final int INIT = 100; 63 private static final int PLAY = 101; 64 private static final int SET_POSTER = 102; 65 private static final int SEEK = 103; 66 private static final int PAUSE = 104; 67 68 // Message Ids to be handled on the WebCore thread 69 private static final int PREPARED = 200; 70 private static final int ENDED = 201; 71 72 // The C++ MediaPlayerPrivateAndroid object. 73 int mNativePointer; 74 // The handler for WebCore thread messages; 75 private Handler mWebCoreHandler; 76 // The WebView instance that created this view. 77 private WebView mWebView; 78 // The ChildView instance used by the ViewManager. 79 private ChildView mChildView; 80 // The poster image to be shown when the video is not playing. 81 private ImageView mPosterView; 82 // The poster downloader. 83 private PosterDownloader mPosterDownloader; 84 // The seek position. 85 private int mSeekPosition; 86 // A helper class to control the playback. This executes on the UI thread! 87 private static final class VideoPlayer { 88 // The proxy that is currently playing (if any). 89 private static HTML5VideoViewProxy mCurrentProxy; 90 // The VideoView instance. This is a singleton for now, at least until 91 // http://b/issue?id=1973663 is fixed. 92 private static VideoView mVideoView; 93 94 private static final WebChromeClient.CustomViewCallback mCallback = 95 new WebChromeClient.CustomViewCallback() { 96 public void onCustomViewHidden() { 97 // At this point the videoview is pretty much destroyed. 98 // It listens to SurfaceHolder.Callback.SurfaceDestroyed event 99 // which happens when the video view is detached from its parent 100 // view. This happens in the WebChromeClient before this method 101 // is invoked. 102 mCurrentProxy.playbackEnded(); 103 mCurrentProxy = null; 104 mVideoView = null; 105 } 106 }; 107 108 public static void play(String url, int time, HTML5VideoViewProxy proxy, 109 WebChromeClient client) { 110 if (mCurrentProxy != null) { 111 // Some other video is already playing. Notify the caller that its playback ended. 112 proxy.playbackEnded(); 113 return; 114 } 115 mCurrentProxy = proxy; 116 mVideoView = new VideoView(proxy.getContext()); 117 mVideoView.setWillNotDraw(false); 118 mVideoView.setMediaController(new MediaController(proxy.getContext())); 119 mVideoView.setVideoURI(Uri.parse(url)); 120 mVideoView.setOnCompletionListener(proxy); 121 mVideoView.setOnPreparedListener(proxy); 122 mVideoView.seekTo(time); 123 mVideoView.start(); 124 client.onShowCustomView(mVideoView, mCallback); 125 } 126 127 public static void seek(int time, HTML5VideoViewProxy proxy) { 128 if (mCurrentProxy == proxy && time >= 0 && mVideoView != null) { 129 mVideoView.seekTo(time); 130 } 131 } 132 133 public static void pause(HTML5VideoViewProxy proxy) { 134 if (mCurrentProxy == proxy && mVideoView != null) { 135 mVideoView.pause(); 136 } 137 } 138 } 139 140 // A bunch event listeners for our VideoView 141 // MediaPlayer.OnPreparedListener 142 public void onPrepared(MediaPlayer mp) { 143 Message msg = Message.obtain(mWebCoreHandler, PREPARED); 144 Map<String, Object> map = new HashMap<String, Object>(); 145 map.put("dur", new Integer(mp.getDuration())); 146 map.put("width", new Integer(mp.getVideoWidth())); 147 map.put("height", new Integer(mp.getVideoHeight())); 148 msg.obj = map; 149 mWebCoreHandler.sendMessage(msg); 150 } 151 152 // MediaPlayer.OnCompletionListener; 153 public void onCompletion(MediaPlayer mp) { 154 playbackEnded(); 155 } 156 157 public void playbackEnded() { 158 Message msg = Message.obtain(mWebCoreHandler, ENDED); 159 mWebCoreHandler.sendMessage(msg); 160 } 161 162 // Handler for the messages from WebCore thread to the UI thread. 163 @Override 164 public void handleMessage(Message msg) { 165 // This executes on the UI thread. 166 switch (msg.what) { 167 case INIT: { 168 mPosterView = new ImageView(mWebView.getContext()); 169 mChildView.mView = mPosterView; 170 break; 171 } 172 case PLAY: { 173 String url = (String) msg.obj; 174 WebChromeClient client = mWebView.getWebChromeClient(); 175 if (client != null) { 176 VideoPlayer.play(url, mSeekPosition, this, client); 177 } 178 break; 179 } 180 case SET_POSTER: { 181 Bitmap poster = (Bitmap) msg.obj; 182 mPosterView.setImageBitmap(poster); 183 break; 184 } 185 case SEEK: { 186 Integer time = (Integer) msg.obj; 187 mSeekPosition = time; 188 VideoPlayer.seek(mSeekPosition, this); 189 break; 190 } 191 case PAUSE: { 192 VideoPlayer.pause(this); 193 break; 194 } 195 } 196 } 197 198 // Everything below this comment executes on the WebCore thread, except for 199 // the EventHandler methods, which are called on the network thread. 200 201 // A helper class that knows how to download posters 202 private static final class PosterDownloader implements EventHandler { 203 // The request queue. This is static as we have one queue for all posters. 204 private static RequestQueue mRequestQueue; 205 private static int mQueueRefCount = 0; 206 // The poster URL 207 private String mUrl; 208 // The proxy we're doing this for. 209 private final HTML5VideoViewProxy mProxy; 210 // The poster bytes. We only touch this on the network thread. 211 private ByteArrayOutputStream mPosterBytes; 212 // The request handle. We only touch this on the WebCore thread. 213 private RequestHandle mRequestHandle; 214 // The response status code. 215 private int mStatusCode; 216 // The response headers. 217 private Headers mHeaders; 218 // The handler to handle messages on the WebCore thread. 219 private Handler mHandler; 220 221 public PosterDownloader(String url, HTML5VideoViewProxy proxy) { 222 mUrl = url; 223 mProxy = proxy; 224 mHandler = new Handler(); 225 } 226 // Start the download. Called on WebCore thread. 227 public void start() { 228 retainQueue(); 229 mRequestHandle = mRequestQueue.queueRequest(mUrl, "GET", null, this, null, 0); 230 } 231 // Cancel the download if active and release the queue. Called on WebCore thread. 232 public void cancelAndReleaseQueue() { 233 if (mRequestHandle != null) { 234 mRequestHandle.cancel(); 235 mRequestHandle = null; 236 } 237 releaseQueue(); 238 } 239 // EventHandler methods. Executed on the network thread. 240 public void status(int major_version, 241 int minor_version, 242 int code, 243 String reason_phrase) { 244 mStatusCode = code; 245 } 246 247 public void headers(Headers headers) { 248 mHeaders = headers; 249 } 250 251 public void data(byte[] data, int len) { 252 if (mPosterBytes == null) { 253 mPosterBytes = new ByteArrayOutputStream(); 254 } 255 mPosterBytes.write(data, 0, len); 256 } 257 258 public void endData() { 259 if (mStatusCode == 200) { 260 if (mPosterBytes.size() > 0) { 261 Bitmap poster = BitmapFactory.decodeByteArray( 262 mPosterBytes.toByteArray(), 0, mPosterBytes.size()); 263 if (poster != null) { 264 mProxy.doSetPoster(poster); 265 } 266 } 267 cleanup(); 268 } else if (mStatusCode >= 300 && mStatusCode < 400) { 269 // We have a redirect. 270 mUrl = mHeaders.getLocation(); 271 if (mUrl != null) { 272 mHandler.post(new Runnable() { 273 public void run() { 274 if (mRequestHandle != null) { 275 mRequestHandle.setupRedirect(mUrl, mStatusCode, 276 new HashMap<String, String>()); 277 } 278 } 279 }); 280 } 281 } 282 } 283 284 public void certificate(SslCertificate certificate) { 285 // Don't care. 286 } 287 288 public void error(int id, String description) { 289 cleanup(); 290 } 291 292 public boolean handleSslErrorRequest(SslError error) { 293 // Don't care. If this happens, data() will never be called so 294 // mPosterBytes will never be created, so no need to call cleanup. 295 return false; 296 } 297 // Tears down the poster bytes stream. Called on network thread. 298 private void cleanup() { 299 if (mPosterBytes != null) { 300 try { 301 mPosterBytes.close(); 302 } catch (IOException ignored) { 303 // Ignored. 304 } finally { 305 mPosterBytes = null; 306 } 307 } 308 } 309 310 // Queue management methods. Called on WebCore thread. 311 private void retainQueue() { 312 if (mRequestQueue == null) { 313 mRequestQueue = new RequestQueue(mProxy.getContext()); 314 } 315 mQueueRefCount++; 316 } 317 318 private void releaseQueue() { 319 if (mQueueRefCount == 0) { 320 return; 321 } 322 if (--mQueueRefCount == 0) { 323 mRequestQueue.shutdown(); 324 mRequestQueue = null; 325 } 326 } 327 } 328 329 /** 330 * Private constructor. 331 * @param webView is the WebView that hosts the video. 332 * @param nativePtr is the C++ pointer to the MediaPlayerPrivate object. 333 */ 334 private HTML5VideoViewProxy(WebView webView, int nativePtr) { 335 // This handler is for the main (UI) thread. 336 super(Looper.getMainLooper()); 337 // Save the WebView object. 338 mWebView = webView; 339 // Save the native ptr 340 mNativePointer = nativePtr; 341 // create the message handler for this thread 342 createWebCoreHandler(); 343 } 344 345 private void createWebCoreHandler() { 346 mWebCoreHandler = new Handler() { 347 @Override 348 public void handleMessage(Message msg) { 349 switch (msg.what) { 350 case PREPARED: { 351 Map<String, Object> map = (Map<String, Object>) msg.obj; 352 Integer duration = (Integer) map.get("dur"); 353 Integer width = (Integer) map.get("width"); 354 Integer height = (Integer) map.get("height"); 355 nativeOnPrepared(duration.intValue(), width.intValue(), 356 height.intValue(), mNativePointer); 357 break; 358 } 359 case ENDED: 360 nativeOnEnded(mNativePointer); 361 break; 362 } 363 } 364 }; 365 } 366 367 private void doSetPoster(Bitmap poster) { 368 if (poster == null) { 369 return; 370 } 371 // Send the bitmap over to the UI thread. 372 Message message = obtainMessage(SET_POSTER); 373 message.obj = poster; 374 sendMessage(message); 375 } 376 377 public Context getContext() { 378 return mWebView.getContext(); 379 } 380 381 // The public methods below are all called from WebKit only. 382 /** 383 * Play a video stream. 384 * @param url is the URL of the video stream. 385 */ 386 public void play(String url) { 387 if (url == null) { 388 return; 389 } 390 Message message = obtainMessage(PLAY); 391 message.obj = url; 392 sendMessage(message); 393 } 394 395 /** 396 * Seek into the video stream. 397 * @param time is the position in the video stream. 398 */ 399 public void seek(int time) { 400 Message message = obtainMessage(SEEK); 401 message.obj = new Integer(time); 402 sendMessage(message); 403 } 404 405 /** 406 * Pause the playback. 407 */ 408 public void pause() { 409 Message message = obtainMessage(PAUSE); 410 sendMessage(message); 411 } 412 413 /** 414 * Create the child view that will cary the poster. 415 */ 416 public void createView() { 417 mChildView = mWebView.mViewManager.createView(); 418 sendMessage(obtainMessage(INIT)); 419 } 420 421 /** 422 * Attach the poster view. 423 * @param x, y are the screen coordinates where the poster should be hung. 424 * @param width, height denote the size of the poster. 425 */ 426 public void attachView(int x, int y, int width, int height) { 427 if (mChildView == null) { 428 return; 429 } 430 mChildView.attachView(x, y, width, height); 431 } 432 433 /** 434 * Remove the child view and, thus, the poster. 435 */ 436 public void removeView() { 437 if (mChildView == null) { 438 return; 439 } 440 mChildView.removeView(); 441 // This is called by the C++ MediaPlayerPrivate dtor. 442 // Cancel any active poster download. 443 if (mPosterDownloader != null) { 444 mPosterDownloader.cancelAndReleaseQueue(); 445 } 446 } 447 448 /** 449 * Load the poster image. 450 * @param url is the URL of the poster image. 451 */ 452 public void loadPoster(String url) { 453 if (url == null) { 454 return; 455 } 456 // Cancel any active poster download. 457 if (mPosterDownloader != null) { 458 mPosterDownloader.cancelAndReleaseQueue(); 459 } 460 // Load the poster asynchronously 461 mPosterDownloader = new PosterDownloader(url, this); 462 mPosterDownloader.start(); 463 } 464 465 /** 466 * The factory for HTML5VideoViewProxy instances. 467 * @param webViewCore is the WebViewCore that is requesting the proxy. 468 * 469 * @return a new HTML5VideoViewProxy object. 470 */ 471 public static HTML5VideoViewProxy getInstance(WebViewCore webViewCore, int nativePtr) { 472 return new HTML5VideoViewProxy(webViewCore.getWebView(), nativePtr); 473 } 474 475 private native void nativeOnPrepared(int duration, int width, int height, int nativePointer); 476 private native void nativeOnEnded(int nativePointer); 477} 478