• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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