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