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 17 package android.webkit; 18 19 import android.content.Context; 20 import android.graphics.Bitmap; 21 import android.graphics.BitmapFactory; 22 import android.media.MediaPlayer; 23 import android.media.MediaPlayer.OnPreparedListener; 24 import android.media.MediaPlayer.OnCompletionListener; 25 import android.media.MediaPlayer.OnErrorListener; 26 import android.net.http.EventHandler; 27 import android.net.http.Headers; 28 import android.net.http.RequestHandle; 29 import android.net.http.RequestQueue; 30 import android.net.http.SslCertificate; 31 import android.net.http.SslError; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.os.Message; 37 import android.util.Log; 38 import android.view.MotionEvent; 39 import android.view.Gravity; 40 import android.view.View; 41 import android.view.ViewGroup; 42 import android.widget.AbsoluteLayout; 43 import android.widget.FrameLayout; 44 import android.widget.MediaController; 45 import android.widget.VideoView; 46 47 import java.io.ByteArrayOutputStream; 48 import java.io.IOException; 49 import java.util.HashMap; 50 import java.util.Map; 51 import java.util.Timer; 52 import java.util.TimerTask; 53 54 /** 55 * <p>Proxy for HTML5 video views. 56 */ 57 class HTML5VideoViewProxy extends Handler 58 implements MediaPlayer.OnPreparedListener, 59 MediaPlayer.OnCompletionListener, 60 MediaPlayer.OnErrorListener { 61 // Logging tag. 62 private static final String LOGTAG = "HTML5VideoViewProxy"; 63 64 // Message Ids for WebCore thread -> UI thread communication. 65 private static final int PLAY = 100; 66 private static final int SEEK = 101; 67 private static final int PAUSE = 102; 68 private static final int ERROR = 103; 69 private static final int LOAD_DEFAULT_POSTER = 104; 70 71 // Message Ids to be handled on the WebCore thread 72 private static final int PREPARED = 200; 73 private static final int ENDED = 201; 74 private static final int POSTER_FETCHED = 202; 75 private static final int PAUSED = 203; 76 77 private static final String COOKIE = "Cookie"; 78 79 // Timer thread -> UI thread 80 private static final int TIMEUPDATE = 300; 81 82 // The C++ MediaPlayerPrivateAndroid object. 83 int mNativePointer; 84 // The handler for WebCore thread messages; 85 private Handler mWebCoreHandler; 86 // The WebView instance that created this view. 87 private WebView mWebView; 88 // The poster image to be shown when the video is not playing. 89 // This ref prevents the bitmap from being GC'ed. 90 private Bitmap mPoster; 91 // The poster downloader. 92 private PosterDownloader mPosterDownloader; 93 // The seek position. 94 private int mSeekPosition; 95 // A helper class to control the playback. This executes on the UI thread! 96 private static final class VideoPlayer { 97 // The proxy that is currently playing (if any). 98 private static HTML5VideoViewProxy mCurrentProxy; 99 // The VideoView instance. This is a singleton for now, at least until 100 // http://b/issue?id=1973663 is fixed. 101 private static VideoView mVideoView; 102 // The progress view. 103 private static View mProgressView; 104 // The container for the progress view and video view 105 private static FrameLayout mLayout; 106 // The timer for timeupate events. 107 // See http://www.whatwg.org/specs/web-apps/current-work/#event-media-timeupdate 108 private static Timer mTimer; 109 private static final class TimeupdateTask extends TimerTask { 110 private HTML5VideoViewProxy mProxy; 111 TimeupdateTask(HTML5VideoViewProxy proxy)112 public TimeupdateTask(HTML5VideoViewProxy proxy) { 113 mProxy = proxy; 114 } 115 run()116 public void run() { 117 mProxy.onTimeupdate(); 118 } 119 } 120 // The spec says the timer should fire every 250 ms or less. 121 private static final int TIMEUPDATE_PERIOD = 250; // ms 122 static boolean isVideoSelfEnded = false; 123 124 private static final WebChromeClient.CustomViewCallback mCallback = 125 new WebChromeClient.CustomViewCallback() { 126 public void onCustomViewHidden() { 127 // At this point the videoview is pretty much destroyed. 128 // It listens to SurfaceHolder.Callback.SurfaceDestroyed event 129 // which happens when the video view is detached from its parent 130 // view. This happens in the WebChromeClient before this method 131 // is invoked. 132 mTimer.cancel(); 133 mTimer = null; 134 if (mVideoView.isPlaying()) { 135 mVideoView.stopPlayback(); 136 } 137 if (isVideoSelfEnded) 138 mCurrentProxy.dispatchOnEnded(); 139 else 140 mCurrentProxy.dispatchOnPaused(); 141 142 // Re enable plugin views. 143 mCurrentProxy.getWebView().getViewManager().showAll(); 144 145 isVideoSelfEnded = false; 146 mCurrentProxy = null; 147 mLayout.removeView(mVideoView); 148 mVideoView = null; 149 if (mProgressView != null) { 150 mLayout.removeView(mProgressView); 151 mProgressView = null; 152 } 153 mLayout = null; 154 } 155 }; 156 play(String url, int time, HTML5VideoViewProxy proxy, WebChromeClient client)157 public static void play(String url, int time, HTML5VideoViewProxy proxy, 158 WebChromeClient client) { 159 if (mCurrentProxy == proxy) { 160 if (!mVideoView.isPlaying()) { 161 mVideoView.start(); 162 } 163 return; 164 } 165 166 if (mCurrentProxy != null) { 167 // Some other video is already playing. Notify the caller that its playback ended. 168 proxy.dispatchOnEnded(); 169 return; 170 } 171 172 mCurrentProxy = proxy; 173 // Create a FrameLayout that will contain the VideoView and the 174 // progress view (if any). 175 mLayout = new FrameLayout(proxy.getContext()); 176 FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( 177 ViewGroup.LayoutParams.WRAP_CONTENT, 178 ViewGroup.LayoutParams.WRAP_CONTENT, 179 Gravity.CENTER); 180 mVideoView = new VideoView(proxy.getContext()); 181 mVideoView.setWillNotDraw(false); 182 mVideoView.setMediaController(new MediaController(proxy.getContext())); 183 184 String cookieValue = CookieManager.getInstance().getCookie(url); 185 Map<String, String> headers = null; 186 if (cookieValue != null) { 187 headers = new HashMap<String, String>(); 188 headers.put(COOKIE, cookieValue); 189 } 190 191 mVideoView.setVideoURI(Uri.parse(url), headers); 192 mVideoView.setOnCompletionListener(proxy); 193 mVideoView.setOnPreparedListener(proxy); 194 mVideoView.setOnErrorListener(proxy); 195 mVideoView.seekTo(time); 196 mLayout.addView(mVideoView, layoutParams); 197 mProgressView = client.getVideoLoadingProgressView(); 198 if (mProgressView != null) { 199 mLayout.addView(mProgressView, layoutParams); 200 mProgressView.setVisibility(View.VISIBLE); 201 } 202 mLayout.setVisibility(View.VISIBLE); 203 mTimer = new Timer(); 204 mVideoView.start(); 205 client.onShowCustomView(mLayout, mCallback); 206 // Plugins like Flash will draw over the video so hide 207 // them while we're playing. 208 mCurrentProxy.getWebView().getViewManager().hideAll(); 209 } 210 isPlaying(HTML5VideoViewProxy proxy)211 public static boolean isPlaying(HTML5VideoViewProxy proxy) { 212 return (mCurrentProxy == proxy && mVideoView != null && mVideoView.isPlaying()); 213 } 214 getCurrentPosition()215 public static int getCurrentPosition() { 216 int currentPosMs = 0; 217 if (mVideoView != null) { 218 currentPosMs = mVideoView.getCurrentPosition(); 219 } 220 return currentPosMs; 221 } 222 seek(int time, HTML5VideoViewProxy proxy)223 public static void seek(int time, HTML5VideoViewProxy proxy) { 224 if (mCurrentProxy == proxy && time >= 0 && mVideoView != null) { 225 mVideoView.seekTo(time); 226 } 227 } 228 pause(HTML5VideoViewProxy proxy)229 public static void pause(HTML5VideoViewProxy proxy) { 230 if (mCurrentProxy == proxy && mVideoView != null) { 231 mVideoView.pause(); 232 mTimer.purge(); 233 } 234 } 235 onPrepared()236 public static void onPrepared() { 237 if (mProgressView == null || mLayout == null) { 238 return; 239 } 240 mTimer.schedule(new TimeupdateTask(mCurrentProxy), TIMEUPDATE_PERIOD, TIMEUPDATE_PERIOD); 241 mProgressView.setVisibility(View.GONE); 242 mLayout.removeView(mProgressView); 243 mProgressView = null; 244 } 245 } 246 247 // A bunch event listeners for our VideoView 248 // MediaPlayer.OnPreparedListener onPrepared(MediaPlayer mp)249 public void onPrepared(MediaPlayer mp) { 250 VideoPlayer.onPrepared(); 251 Message msg = Message.obtain(mWebCoreHandler, PREPARED); 252 Map<String, Object> map = new HashMap<String, Object>(); 253 map.put("dur", new Integer(mp.getDuration())); 254 map.put("width", new Integer(mp.getVideoWidth())); 255 map.put("height", new Integer(mp.getVideoHeight())); 256 msg.obj = map; 257 mWebCoreHandler.sendMessage(msg); 258 } 259 260 // MediaPlayer.OnCompletionListener; onCompletion(MediaPlayer mp)261 public void onCompletion(MediaPlayer mp) { 262 // The video ended by itself, so we need to 263 // send a message to the UI thread to dismiss 264 // the video view and to return to the WebView. 265 // arg1 == 1 means the video ends by itself. 266 sendMessage(obtainMessage(ENDED, 1, 0)); 267 } 268 269 // MediaPlayer.OnErrorListener onError(MediaPlayer mp, int what, int extra)270 public boolean onError(MediaPlayer mp, int what, int extra) { 271 sendMessage(obtainMessage(ERROR)); 272 return false; 273 } 274 dispatchOnEnded()275 public void dispatchOnEnded() { 276 Message msg = Message.obtain(mWebCoreHandler, ENDED); 277 mWebCoreHandler.sendMessage(msg); 278 } 279 dispatchOnPaused()280 public void dispatchOnPaused() { 281 Message msg = Message.obtain(mWebCoreHandler, PAUSED); 282 mWebCoreHandler.sendMessage(msg); 283 } 284 onTimeupdate()285 public void onTimeupdate() { 286 sendMessage(obtainMessage(TIMEUPDATE)); 287 } 288 289 // Handler for the messages from WebCore or Timer thread to the UI thread. 290 @Override handleMessage(Message msg)291 public void handleMessage(Message msg) { 292 // This executes on the UI thread. 293 switch (msg.what) { 294 case PLAY: { 295 String url = (String) msg.obj; 296 WebChromeClient client = mWebView.getWebChromeClient(); 297 if (client != null) { 298 VideoPlayer.play(url, mSeekPosition, this, client); 299 } 300 break; 301 } 302 case SEEK: { 303 Integer time = (Integer) msg.obj; 304 mSeekPosition = time; 305 VideoPlayer.seek(mSeekPosition, this); 306 break; 307 } 308 case PAUSE: { 309 VideoPlayer.pause(this); 310 break; 311 } 312 case ENDED: 313 if (msg.arg1 == 1) 314 VideoPlayer.isVideoSelfEnded = true; 315 case ERROR: { 316 WebChromeClient client = mWebView.getWebChromeClient(); 317 if (client != null) { 318 client.onHideCustomView(); 319 } 320 break; 321 } 322 case LOAD_DEFAULT_POSTER: { 323 WebChromeClient client = mWebView.getWebChromeClient(); 324 if (client != null) { 325 doSetPoster(client.getDefaultVideoPoster()); 326 } 327 break; 328 } 329 case TIMEUPDATE: { 330 if (VideoPlayer.isPlaying(this)) { 331 sendTimeupdate(); 332 } 333 break; 334 } 335 } 336 } 337 338 // Everything below this comment executes on the WebCore thread, except for 339 // the EventHandler methods, which are called on the network thread. 340 341 // A helper class that knows how to download posters 342 private static final class PosterDownloader implements EventHandler { 343 // The request queue. This is static as we have one queue for all posters. 344 private static RequestQueue mRequestQueue; 345 private static int mQueueRefCount = 0; 346 // The poster URL 347 private String mUrl; 348 // The proxy we're doing this for. 349 private final HTML5VideoViewProxy mProxy; 350 // The poster bytes. We only touch this on the network thread. 351 private ByteArrayOutputStream mPosterBytes; 352 // The request handle. We only touch this on the WebCore thread. 353 private RequestHandle mRequestHandle; 354 // The response status code. 355 private int mStatusCode; 356 // The response headers. 357 private Headers mHeaders; 358 // The handler to handle messages on the WebCore thread. 359 private Handler mHandler; 360 PosterDownloader(String url, HTML5VideoViewProxy proxy)361 public PosterDownloader(String url, HTML5VideoViewProxy proxy) { 362 mUrl = url; 363 mProxy = proxy; 364 mHandler = new Handler(); 365 } 366 // Start the download. Called on WebCore thread. start()367 public void start() { 368 retainQueue(); 369 mRequestHandle = mRequestQueue.queueRequest(mUrl, "GET", null, this, null, 0); 370 } 371 // Cancel the download if active and release the queue. Called on WebCore thread. cancelAndReleaseQueue()372 public void cancelAndReleaseQueue() { 373 if (mRequestHandle != null) { 374 mRequestHandle.cancel(); 375 mRequestHandle = null; 376 } 377 releaseQueue(); 378 } 379 // EventHandler methods. Executed on the network thread. status(int major_version, int minor_version, int code, String reason_phrase)380 public void status(int major_version, 381 int minor_version, 382 int code, 383 String reason_phrase) { 384 mStatusCode = code; 385 } 386 headers(Headers headers)387 public void headers(Headers headers) { 388 mHeaders = headers; 389 } 390 data(byte[] data, int len)391 public void data(byte[] data, int len) { 392 if (mPosterBytes == null) { 393 mPosterBytes = new ByteArrayOutputStream(); 394 } 395 mPosterBytes.write(data, 0, len); 396 } 397 endData()398 public void endData() { 399 if (mStatusCode == 200) { 400 if (mPosterBytes.size() > 0) { 401 Bitmap poster = BitmapFactory.decodeByteArray( 402 mPosterBytes.toByteArray(), 0, mPosterBytes.size()); 403 mProxy.doSetPoster(poster); 404 } 405 cleanup(); 406 } else if (mStatusCode >= 300 && mStatusCode < 400) { 407 // We have a redirect. 408 mUrl = mHeaders.getLocation(); 409 if (mUrl != null) { 410 mHandler.post(new Runnable() { 411 public void run() { 412 if (mRequestHandle != null) { 413 mRequestHandle.setupRedirect(mUrl, mStatusCode, 414 new HashMap<String, String>()); 415 } 416 } 417 }); 418 } 419 } 420 } 421 certificate(SslCertificate certificate)422 public void certificate(SslCertificate certificate) { 423 // Don't care. 424 } 425 error(int id, String description)426 public void error(int id, String description) { 427 cleanup(); 428 } 429 handleSslErrorRequest(SslError error)430 public boolean handleSslErrorRequest(SslError error) { 431 // Don't care. If this happens, data() will never be called so 432 // mPosterBytes will never be created, so no need to call cleanup. 433 return false; 434 } 435 // Tears down the poster bytes stream. Called on network thread. cleanup()436 private void cleanup() { 437 if (mPosterBytes != null) { 438 try { 439 mPosterBytes.close(); 440 } catch (IOException ignored) { 441 // Ignored. 442 } finally { 443 mPosterBytes = null; 444 } 445 } 446 } 447 448 // Queue management methods. Called on WebCore thread. retainQueue()449 private void retainQueue() { 450 if (mRequestQueue == null) { 451 mRequestQueue = new RequestQueue(mProxy.getContext()); 452 } 453 mQueueRefCount++; 454 } 455 releaseQueue()456 private void releaseQueue() { 457 if (mQueueRefCount == 0) { 458 return; 459 } 460 if (--mQueueRefCount == 0) { 461 mRequestQueue.shutdown(); 462 mRequestQueue = null; 463 } 464 } 465 } 466 467 /** 468 * Private constructor. 469 * @param webView is the WebView that hosts the video. 470 * @param nativePtr is the C++ pointer to the MediaPlayerPrivate object. 471 */ HTML5VideoViewProxy(WebView webView, int nativePtr)472 private HTML5VideoViewProxy(WebView webView, int nativePtr) { 473 // This handler is for the main (UI) thread. 474 super(Looper.getMainLooper()); 475 // Save the WebView object. 476 mWebView = webView; 477 // Save the native ptr 478 mNativePointer = nativePtr; 479 // create the message handler for this thread 480 createWebCoreHandler(); 481 } 482 createWebCoreHandler()483 private void createWebCoreHandler() { 484 mWebCoreHandler = new Handler() { 485 @Override 486 public void handleMessage(Message msg) { 487 switch (msg.what) { 488 case PREPARED: { 489 Map<String, Object> map = (Map<String, Object>) msg.obj; 490 Integer duration = (Integer) map.get("dur"); 491 Integer width = (Integer) map.get("width"); 492 Integer height = (Integer) map.get("height"); 493 nativeOnPrepared(duration.intValue(), width.intValue(), 494 height.intValue(), mNativePointer); 495 break; 496 } 497 case ENDED: 498 nativeOnEnded(mNativePointer); 499 break; 500 case PAUSED: 501 nativeOnPaused(mNativePointer); 502 break; 503 case POSTER_FETCHED: 504 Bitmap poster = (Bitmap) msg.obj; 505 nativeOnPosterFetched(poster, mNativePointer); 506 break; 507 case TIMEUPDATE: 508 nativeOnTimeupdate(msg.arg1, mNativePointer); 509 break; 510 } 511 } 512 }; 513 } 514 doSetPoster(Bitmap poster)515 private void doSetPoster(Bitmap poster) { 516 if (poster == null) { 517 return; 518 } 519 // Save a ref to the bitmap and send it over to the WebCore thread. 520 mPoster = poster; 521 Message msg = Message.obtain(mWebCoreHandler, POSTER_FETCHED); 522 msg.obj = poster; 523 mWebCoreHandler.sendMessage(msg); 524 } 525 sendTimeupdate()526 private void sendTimeupdate() { 527 Message msg = Message.obtain(mWebCoreHandler, TIMEUPDATE); 528 msg.arg1 = VideoPlayer.getCurrentPosition(); 529 mWebCoreHandler.sendMessage(msg); 530 } 531 getContext()532 public Context getContext() { 533 return mWebView.getContext(); 534 } 535 536 // The public methods below are all called from WebKit only. 537 /** 538 * Play a video stream. 539 * @param url is the URL of the video stream. 540 */ play(String url)541 public void play(String url) { 542 if (url == null) { 543 return; 544 } 545 Message message = obtainMessage(PLAY); 546 message.obj = url; 547 sendMessage(message); 548 } 549 550 /** 551 * Seek into the video stream. 552 * @param time is the position in the video stream. 553 */ seek(int time)554 public void seek(int time) { 555 Message message = obtainMessage(SEEK); 556 message.obj = new Integer(time); 557 sendMessage(message); 558 } 559 560 /** 561 * Pause the playback. 562 */ pause()563 public void pause() { 564 Message message = obtainMessage(PAUSE); 565 sendMessage(message); 566 } 567 568 /** 569 * Tear down this proxy object. 570 */ teardown()571 public void teardown() { 572 // This is called by the C++ MediaPlayerPrivate dtor. 573 // Cancel any active poster download. 574 if (mPosterDownloader != null) { 575 mPosterDownloader.cancelAndReleaseQueue(); 576 } 577 mNativePointer = 0; 578 } 579 580 /** 581 * Load the poster image. 582 * @param url is the URL of the poster image. 583 */ loadPoster(String url)584 public void loadPoster(String url) { 585 if (url == null) { 586 Message message = obtainMessage(LOAD_DEFAULT_POSTER); 587 sendMessage(message); 588 return; 589 } 590 // Cancel any active poster download. 591 if (mPosterDownloader != null) { 592 mPosterDownloader.cancelAndReleaseQueue(); 593 } 594 // Load the poster asynchronously 595 mPosterDownloader = new PosterDownloader(url, this); 596 mPosterDownloader.start(); 597 } 598 599 /** 600 * The factory for HTML5VideoViewProxy instances. 601 * @param webViewCore is the WebViewCore that is requesting the proxy. 602 * 603 * @return a new HTML5VideoViewProxy object. 604 */ getInstance(WebViewCore webViewCore, int nativePtr)605 public static HTML5VideoViewProxy getInstance(WebViewCore webViewCore, int nativePtr) { 606 return new HTML5VideoViewProxy(webViewCore.getWebView(), nativePtr); 607 } 608 getWebView()609 /* package */ WebView getWebView() { 610 return mWebView; 611 } 612 nativeOnPrepared(int duration, int width, int height, int nativePointer)613 private native void nativeOnPrepared(int duration, int width, int height, int nativePointer); nativeOnEnded(int nativePointer)614 private native void nativeOnEnded(int nativePointer); nativeOnPaused(int nativePointer)615 private native void nativeOnPaused(int nativePointer); nativeOnPosterFetched(Bitmap poster, int nativePointer)616 private native void nativeOnPosterFetched(Bitmap poster, int nativePointer); nativeOnTimeupdate(int position, int nativePointer)617 private native void nativeOnTimeupdate(int position, int nativePointer); 618 } 619