• 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 
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