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 52 /** 53 * <p>Proxy for HTML5 video views. 54 */ 55 class HTML5VideoViewProxy extends Handler 56 implements MediaPlayer.OnPreparedListener, 57 MediaPlayer.OnCompletionListener, 58 MediaPlayer.OnErrorListener { 59 // Logging tag. 60 private static final String LOGTAG = "HTML5VideoViewProxy"; 61 62 // Message Ids for WebCore thread -> UI thread communication. 63 private static final int PLAY = 100; 64 private static final int SEEK = 101; 65 private static final int PAUSE = 102; 66 private static final int ERROR = 103; 67 private static final int LOAD_DEFAULT_POSTER = 104; 68 69 // Message Ids to be handled on the WebCore thread 70 private static final int PREPARED = 200; 71 private static final int ENDED = 201; 72 private static final int POSTER_FETCHED = 202; 73 74 // The C++ MediaPlayerPrivateAndroid object. 75 int mNativePointer; 76 // The handler for WebCore thread messages; 77 private Handler mWebCoreHandler; 78 // The WebView instance that created this view. 79 private WebView mWebView; 80 // The poster image to be shown when the video is not playing. 81 // This ref prevents the bitmap from being GC'ed. 82 private Bitmap mPoster; 83 // The poster downloader. 84 private PosterDownloader mPosterDownloader; 85 // The seek position. 86 private int mSeekPosition; 87 // A helper class to control the playback. This executes on the UI thread! 88 private static final class VideoPlayer { 89 // The proxy that is currently playing (if any). 90 private static HTML5VideoViewProxy mCurrentProxy; 91 // The VideoView instance. This is a singleton for now, at least until 92 // http://b/issue?id=1973663 is fixed. 93 private static VideoView mVideoView; 94 // The progress view. 95 private static View mProgressView; 96 // The container for the progress view and video view 97 private static FrameLayout mLayout; 98 99 private static final WebChromeClient.CustomViewCallback mCallback = 100 new WebChromeClient.CustomViewCallback() { 101 public void onCustomViewHidden() { 102 // At this point the videoview is pretty much destroyed. 103 // It listens to SurfaceHolder.Callback.SurfaceDestroyed event 104 // which happens when the video view is detached from its parent 105 // view. This happens in the WebChromeClient before this method 106 // is invoked. 107 mCurrentProxy.playbackEnded(); 108 mCurrentProxy = null; 109 mLayout.removeView(mVideoView); 110 mVideoView = null; 111 if (mProgressView != null) { 112 mLayout.removeView(mProgressView); 113 mProgressView = null; 114 } 115 mLayout = null; 116 } 117 }; 118 play(String url, int time, HTML5VideoViewProxy proxy, WebChromeClient client)119 public static void play(String url, int time, HTML5VideoViewProxy proxy, 120 WebChromeClient client) { 121 if (mCurrentProxy != null) { 122 // Some other video is already playing. Notify the caller that its playback ended. 123 proxy.playbackEnded(); 124 return; 125 } 126 mCurrentProxy = proxy; 127 // Create a FrameLayout that will contain the VideoView and the 128 // progress view (if any). 129 mLayout = new FrameLayout(proxy.getContext()); 130 FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( 131 ViewGroup.LayoutParams.WRAP_CONTENT, 132 ViewGroup.LayoutParams.WRAP_CONTENT, 133 Gravity.CENTER); 134 mVideoView = new VideoView(proxy.getContext()); 135 mVideoView.setWillNotDraw(false); 136 mVideoView.setMediaController(new MediaController(proxy.getContext())); 137 mVideoView.setVideoURI(Uri.parse(url)); 138 mVideoView.setOnCompletionListener(proxy); 139 mVideoView.setOnPreparedListener(proxy); 140 mVideoView.setOnErrorListener(proxy); 141 mVideoView.seekTo(time); 142 mLayout.addView(mVideoView, layoutParams); 143 mProgressView = client.getVideoLoadingProgressView(); 144 if (mProgressView != null) { 145 mLayout.addView(mProgressView, layoutParams); 146 mProgressView.setVisibility(View.VISIBLE); 147 } 148 mLayout.setVisibility(View.VISIBLE); 149 mVideoView.start(); 150 client.onShowCustomView(mLayout, mCallback); 151 } 152 seek(int time, HTML5VideoViewProxy proxy)153 public static void seek(int time, HTML5VideoViewProxy proxy) { 154 if (mCurrentProxy == proxy && time >= 0 && mVideoView != null) { 155 mVideoView.seekTo(time); 156 } 157 } 158 pause(HTML5VideoViewProxy proxy)159 public static void pause(HTML5VideoViewProxy proxy) { 160 if (mCurrentProxy == proxy && mVideoView != null) { 161 mVideoView.pause(); 162 } 163 } 164 onPrepared()165 public static void onPrepared() { 166 if (mProgressView == null || mLayout == null) { 167 return; 168 } 169 mProgressView.setVisibility(View.GONE); 170 mLayout.removeView(mProgressView); 171 mProgressView = null; 172 } 173 } 174 175 // A bunch event listeners for our VideoView 176 // MediaPlayer.OnPreparedListener onPrepared(MediaPlayer mp)177 public void onPrepared(MediaPlayer mp) { 178 VideoPlayer.onPrepared(); 179 Message msg = Message.obtain(mWebCoreHandler, PREPARED); 180 Map<String, Object> map = new HashMap<String, Object>(); 181 map.put("dur", new Integer(mp.getDuration())); 182 map.put("width", new Integer(mp.getVideoWidth())); 183 map.put("height", new Integer(mp.getVideoHeight())); 184 msg.obj = map; 185 mWebCoreHandler.sendMessage(msg); 186 } 187 188 // MediaPlayer.OnCompletionListener; onCompletion(MediaPlayer mp)189 public void onCompletion(MediaPlayer mp) { 190 playbackEnded(); 191 } 192 193 // MediaPlayer.OnErrorListener onError(MediaPlayer mp, int what, int extra)194 public boolean onError(MediaPlayer mp, int what, int extra) { 195 sendMessage(obtainMessage(ERROR)); 196 return false; 197 } 198 playbackEnded()199 public void playbackEnded() { 200 Message msg = Message.obtain(mWebCoreHandler, ENDED); 201 mWebCoreHandler.sendMessage(msg); 202 } 203 204 // Handler for the messages from WebCore thread to the UI thread. 205 @Override handleMessage(Message msg)206 public void handleMessage(Message msg) { 207 // This executes on the UI thread. 208 switch (msg.what) { 209 case PLAY: { 210 String url = (String) msg.obj; 211 WebChromeClient client = mWebView.getWebChromeClient(); 212 if (client != null) { 213 VideoPlayer.play(url, mSeekPosition, this, client); 214 } 215 break; 216 } 217 case SEEK: { 218 Integer time = (Integer) msg.obj; 219 mSeekPosition = time; 220 VideoPlayer.seek(mSeekPosition, this); 221 break; 222 } 223 case PAUSE: { 224 VideoPlayer.pause(this); 225 break; 226 } 227 case ERROR: { 228 WebChromeClient client = mWebView.getWebChromeClient(); 229 if (client != null) { 230 client.onHideCustomView(); 231 } 232 break; 233 } 234 case LOAD_DEFAULT_POSTER: { 235 WebChromeClient client = mWebView.getWebChromeClient(); 236 if (client != null) { 237 doSetPoster(client.getDefaultVideoPoster()); 238 } 239 break; 240 } 241 } 242 } 243 244 // Everything below this comment executes on the WebCore thread, except for 245 // the EventHandler methods, which are called on the network thread. 246 247 // A helper class that knows how to download posters 248 private static final class PosterDownloader implements EventHandler { 249 // The request queue. This is static as we have one queue for all posters. 250 private static RequestQueue mRequestQueue; 251 private static int mQueueRefCount = 0; 252 // The poster URL 253 private String mUrl; 254 // The proxy we're doing this for. 255 private final HTML5VideoViewProxy mProxy; 256 // The poster bytes. We only touch this on the network thread. 257 private ByteArrayOutputStream mPosterBytes; 258 // The request handle. We only touch this on the WebCore thread. 259 private RequestHandle mRequestHandle; 260 // The response status code. 261 private int mStatusCode; 262 // The response headers. 263 private Headers mHeaders; 264 // The handler to handle messages on the WebCore thread. 265 private Handler mHandler; 266 PosterDownloader(String url, HTML5VideoViewProxy proxy)267 public PosterDownloader(String url, HTML5VideoViewProxy proxy) { 268 mUrl = url; 269 mProxy = proxy; 270 mHandler = new Handler(); 271 } 272 // Start the download. Called on WebCore thread. start()273 public void start() { 274 retainQueue(); 275 mRequestHandle = mRequestQueue.queueRequest(mUrl, "GET", null, this, null, 0); 276 } 277 // Cancel the download if active and release the queue. Called on WebCore thread. cancelAndReleaseQueue()278 public void cancelAndReleaseQueue() { 279 if (mRequestHandle != null) { 280 mRequestHandle.cancel(); 281 mRequestHandle = null; 282 } 283 releaseQueue(); 284 } 285 // EventHandler methods. Executed on the network thread. status(int major_version, int minor_version, int code, String reason_phrase)286 public void status(int major_version, 287 int minor_version, 288 int code, 289 String reason_phrase) { 290 mStatusCode = code; 291 } 292 headers(Headers headers)293 public void headers(Headers headers) { 294 mHeaders = headers; 295 } 296 data(byte[] data, int len)297 public void data(byte[] data, int len) { 298 if (mPosterBytes == null) { 299 mPosterBytes = new ByteArrayOutputStream(); 300 } 301 mPosterBytes.write(data, 0, len); 302 } 303 endData()304 public void endData() { 305 if (mStatusCode == 200) { 306 if (mPosterBytes.size() > 0) { 307 Bitmap poster = BitmapFactory.decodeByteArray( 308 mPosterBytes.toByteArray(), 0, mPosterBytes.size()); 309 mProxy.doSetPoster(poster); 310 } 311 cleanup(); 312 } else if (mStatusCode >= 300 && mStatusCode < 400) { 313 // We have a redirect. 314 mUrl = mHeaders.getLocation(); 315 if (mUrl != null) { 316 mHandler.post(new Runnable() { 317 public void run() { 318 if (mRequestHandle != null) { 319 mRequestHandle.setupRedirect(mUrl, mStatusCode, 320 new HashMap<String, String>()); 321 } 322 } 323 }); 324 } 325 } 326 } 327 certificate(SslCertificate certificate)328 public void certificate(SslCertificate certificate) { 329 // Don't care. 330 } 331 error(int id, String description)332 public void error(int id, String description) { 333 cleanup(); 334 } 335 handleSslErrorRequest(SslError error)336 public boolean handleSslErrorRequest(SslError error) { 337 // Don't care. If this happens, data() will never be called so 338 // mPosterBytes will never be created, so no need to call cleanup. 339 return false; 340 } 341 // Tears down the poster bytes stream. Called on network thread. cleanup()342 private void cleanup() { 343 if (mPosterBytes != null) { 344 try { 345 mPosterBytes.close(); 346 } catch (IOException ignored) { 347 // Ignored. 348 } finally { 349 mPosterBytes = null; 350 } 351 } 352 } 353 354 // Queue management methods. Called on WebCore thread. retainQueue()355 private void retainQueue() { 356 if (mRequestQueue == null) { 357 mRequestQueue = new RequestQueue(mProxy.getContext()); 358 } 359 mQueueRefCount++; 360 } 361 releaseQueue()362 private void releaseQueue() { 363 if (mQueueRefCount == 0) { 364 return; 365 } 366 if (--mQueueRefCount == 0) { 367 mRequestQueue.shutdown(); 368 mRequestQueue = null; 369 } 370 } 371 } 372 373 /** 374 * Private constructor. 375 * @param webView is the WebView that hosts the video. 376 * @param nativePtr is the C++ pointer to the MediaPlayerPrivate object. 377 */ HTML5VideoViewProxy(WebView webView, int nativePtr)378 private HTML5VideoViewProxy(WebView webView, int nativePtr) { 379 // This handler is for the main (UI) thread. 380 super(Looper.getMainLooper()); 381 // Save the WebView object. 382 mWebView = webView; 383 // Save the native ptr 384 mNativePointer = nativePtr; 385 // create the message handler for this thread 386 createWebCoreHandler(); 387 } 388 createWebCoreHandler()389 private void createWebCoreHandler() { 390 mWebCoreHandler = new Handler() { 391 @Override 392 public void handleMessage(Message msg) { 393 switch (msg.what) { 394 case PREPARED: { 395 Map<String, Object> map = (Map<String, Object>) msg.obj; 396 Integer duration = (Integer) map.get("dur"); 397 Integer width = (Integer) map.get("width"); 398 Integer height = (Integer) map.get("height"); 399 nativeOnPrepared(duration.intValue(), width.intValue(), 400 height.intValue(), mNativePointer); 401 break; 402 } 403 case ENDED: 404 nativeOnEnded(mNativePointer); 405 break; 406 case POSTER_FETCHED: 407 Bitmap poster = (Bitmap) msg.obj; 408 nativeOnPosterFetched(poster, mNativePointer); 409 break; 410 } 411 } 412 }; 413 } 414 doSetPoster(Bitmap poster)415 private void doSetPoster(Bitmap poster) { 416 if (poster == null) { 417 return; 418 } 419 // Save a ref to the bitmap and send it over to the WebCore thread. 420 mPoster = poster; 421 Message msg = Message.obtain(mWebCoreHandler, POSTER_FETCHED); 422 msg.obj = poster; 423 mWebCoreHandler.sendMessage(msg); 424 } 425 getContext()426 public Context getContext() { 427 return mWebView.getContext(); 428 } 429 430 // The public methods below are all called from WebKit only. 431 /** 432 * Play a video stream. 433 * @param url is the URL of the video stream. 434 */ play(String url)435 public void play(String url) { 436 if (url == null) { 437 return; 438 } 439 Message message = obtainMessage(PLAY); 440 message.obj = url; 441 sendMessage(message); 442 } 443 444 /** 445 * Seek into the video stream. 446 * @param time is the position in the video stream. 447 */ seek(int time)448 public void seek(int time) { 449 Message message = obtainMessage(SEEK); 450 message.obj = new Integer(time); 451 sendMessage(message); 452 } 453 454 /** 455 * Pause the playback. 456 */ pause()457 public void pause() { 458 Message message = obtainMessage(PAUSE); 459 sendMessage(message); 460 } 461 462 /** 463 * Tear down this proxy object. 464 */ teardown()465 public void teardown() { 466 // This is called by the C++ MediaPlayerPrivate dtor. 467 // Cancel any active poster download. 468 if (mPosterDownloader != null) { 469 mPosterDownloader.cancelAndReleaseQueue(); 470 } 471 mNativePointer = 0; 472 } 473 474 /** 475 * Load the poster image. 476 * @param url is the URL of the poster image. 477 */ loadPoster(String url)478 public void loadPoster(String url) { 479 if (url == null) { 480 Message message = obtainMessage(LOAD_DEFAULT_POSTER); 481 sendMessage(message); 482 return; 483 } 484 // Cancel any active poster download. 485 if (mPosterDownloader != null) { 486 mPosterDownloader.cancelAndReleaseQueue(); 487 } 488 // Load the poster asynchronously 489 mPosterDownloader = new PosterDownloader(url, this); 490 mPosterDownloader.start(); 491 } 492 493 /** 494 * The factory for HTML5VideoViewProxy instances. 495 * @param webViewCore is the WebViewCore that is requesting the proxy. 496 * 497 * @return a new HTML5VideoViewProxy object. 498 */ getInstance(WebViewCore webViewCore, int nativePtr)499 public static HTML5VideoViewProxy getInstance(WebViewCore webViewCore, int nativePtr) { 500 return new HTML5VideoViewProxy(webViewCore.getWebView(), nativePtr); 501 } 502 nativeOnPrepared(int duration, int width, int height, int nativePointer)503 private native void nativeOnPrepared(int duration, int width, int height, int nativePointer); nativeOnEnded(int nativePointer)504 private native void nativeOnEnded(int nativePointer); nativeOnPosterFetched(Bitmap poster, int nativePointer)505 private native void nativeOnPosterFetched(Bitmap poster, int nativePointer); 506 } 507