ContentVideoView.java revision 2d519ce2457219605d4f472da8d2ffd469796035
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.content.browser;
6
7import android.app.Activity;
8import android.app.AlertDialog;
9import android.content.Context;
10import android.content.DialogInterface;
11import android.graphics.Color;
12import android.os.Handler;
13import android.os.Message;
14import android.os.RemoteException;
15import android.util.Log;
16import android.view.Gravity;
17import android.view.KeyEvent;
18import android.view.MotionEvent;
19import android.view.Surface;
20import android.view.SurfaceHolder;
21import android.view.SurfaceView;
22import android.view.View;
23import android.view.ViewGroup;
24import android.widget.FrameLayout;
25import android.widget.LinearLayout;
26import android.widget.MediaController;
27import android.widget.MediaController.MediaPlayerControl;
28import android.widget.ProgressBar;
29import android.widget.TextView;
30
31import java.lang.ref.WeakReference;
32
33import org.chromium.base.CalledByNative;
34import org.chromium.base.JNINamespace;
35import org.chromium.content.common.IChildProcessService;
36import org.chromium.content.R;
37
38@JNINamespace("content")
39public class ContentVideoView extends FrameLayout implements MediaPlayerControl,
40        SurfaceHolder.Callback, View.OnTouchListener, View.OnKeyListener {
41
42    private static final String TAG = "ContentVideoView";
43
44    /* Do not change these values without updating their counterparts
45     * in include/media/mediaplayer.h!
46     */
47    private static final int MEDIA_NOP = 0; // interface test message
48    private static final int MEDIA_PREPARED = 1;
49    private static final int MEDIA_PLAYBACK_COMPLETE = 2;
50    private static final int MEDIA_BUFFERING_UPDATE = 3;
51    private static final int MEDIA_SEEK_COMPLETE = 4;
52    private static final int MEDIA_SET_VIDEO_SIZE = 5;
53    private static final int MEDIA_ERROR = 100;
54    private static final int MEDIA_INFO = 200;
55
56    /** The video is streamed and its container is not valid for progressive
57     * playback i.e the video's index (e.g moov atom) is not at the start of the
58     * file.
59     */
60    public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 2;
61
62    // all possible internal states
63    private static final int STATE_ERROR              = -1;
64    private static final int STATE_IDLE               = 0;
65    private static final int STATE_PLAYING            = 1;
66    private static final int STATE_PAUSED             = 2;
67    private static final int STATE_PLAYBACK_COMPLETED = 3;
68
69    private SurfaceHolder mSurfaceHolder = null;
70    private int mVideoWidth = 0;
71    private int mVideoHeight = 0;
72    private int mCurrentBufferPercentage;
73    private int mDuration;
74    private MediaController mMediaController = null;
75    private boolean mCanPause;
76    private boolean mCanSeekBack;
77    private boolean mCanSeekForward;
78
79    // Native pointer to C++ ContentVideoView object.
80    private int mNativeContentVideoView = 0;
81
82    // webkit should have prepared the media
83    private int mCurrentState = STATE_IDLE;
84
85    // Strings for displaying media player errors
86    static String mPlaybackErrorText;
87    static String mUnknownErrorText;
88    static String mErrorButton;
89    static String mErrorTitle;
90    static String mVideoLoadingText;
91
92    // This view will contain the video.
93    private VideoSurfaceView mVideoSurfaceView;
94
95    // Progress view when the video is loading.
96    private View mProgressView;
97
98    private Surface mSurface = null;
99
100    // There are can be at most 1 fullscreen video
101    // TODO(qinmin): will change this once  we move the creation of this class
102    // to the host application
103    private static ContentVideoView sContentVideoView = null;
104
105    // The delegate will follow sContentVideoView. We would need to
106    // move this to an instance variable if we allow multiple ContentVideoViews.
107    private static ContentVideoViewContextDelegate sDelegate = null;
108
109    private class VideoSurfaceView extends SurfaceView {
110
111        public VideoSurfaceView(Context context) {
112            super(context);
113        }
114
115        @Override
116        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
117            if (mVideoWidth == 0 && mVideoHeight == 0) {
118                setMeasuredDimension(1, 1);
119                return;
120            }
121            int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
122            int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
123            if (mVideoWidth > 0 && mVideoHeight > 0) {
124                if ( mVideoWidth * height  > width * mVideoHeight ) {
125                    height = width * mVideoHeight / mVideoWidth;
126                } else if ( mVideoWidth * height  < width * mVideoHeight ) {
127                    width = height * mVideoWidth / mVideoHeight;
128                }
129            }
130            setMeasuredDimension(width, height);
131        }
132    }
133
134    private static class ProgressView extends LinearLayout {
135
136        private ProgressBar mProgressBar;
137        private TextView mTextView;
138
139        public ProgressView(Context context) {
140            super(context);
141            setOrientation(LinearLayout.VERTICAL);
142            setLayoutParams(new LinearLayout.LayoutParams(
143                    LinearLayout.LayoutParams.WRAP_CONTENT,
144                    LinearLayout.LayoutParams.WRAP_CONTENT));
145            mProgressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge);
146            mTextView = new TextView(context);
147            mTextView.setText(mVideoLoadingText);
148            addView(mProgressBar);
149            addView(mTextView);
150        }
151    }
152
153    private static class FullScreenMediaController extends MediaController {
154
155        View mVideoView;
156
157        public FullScreenMediaController(Context context, View video) {
158            super(context);
159            mVideoView = video;
160        }
161
162        @Override
163        public void show() {
164            super.show();
165            if (mVideoView != null) {
166                mVideoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
167            }
168        }
169
170        @Override
171        public void hide() {
172            if (mVideoView != null) {
173                mVideoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
174            }
175            super.hide();
176        }
177    }
178
179    private Runnable mExitFullscreenRunnable = new Runnable() {
180        @Override
181        public void run() {
182            destroyContentVideoView();
183        }
184    };
185
186    public ContentVideoView(Context context) {
187        this(context, 0);
188    }
189
190    private ContentVideoView(Context context, int nativeContentVideoView) {
191        super(context);
192        initResources(context);
193
194        if (nativeContentVideoView == 0) return;
195        mNativeContentVideoView = nativeContentVideoView;
196
197        mCurrentBufferPercentage = 0;
198        mVideoSurfaceView = new VideoSurfaceView(context);
199    }
200
201    private static void initResources(Context context) {
202        if (mPlaybackErrorText != null) return;
203        mPlaybackErrorText = context.getString(
204                org.chromium.content.R.string.media_player_error_text_invalid_progressive_playback);
205        mUnknownErrorText = context.getString(
206                org.chromium.content.R.string.media_player_error_text_unknown);
207        mErrorButton = context.getString(
208                org.chromium.content.R.string.media_player_error_button);
209        mErrorTitle = context.getString(
210                org.chromium.content.R.string.media_player_error_title);
211        mVideoLoadingText = context.getString(
212                org.chromium.content.R.string.media_player_loading_video);
213    }
214
215    void showContentVideoView() {
216        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
217                ViewGroup.LayoutParams.WRAP_CONTENT,
218                ViewGroup.LayoutParams.WRAP_CONTENT,
219                Gravity.CENTER);
220        this.addView(mVideoSurfaceView, layoutParams);
221        View progressView = sDelegate.getVideoLoadingProgressView();
222        if (progressView != null) {
223            mProgressView = progressView;
224        } else {
225            mProgressView = new ProgressView(getContext());
226        }
227        this.addView(mProgressView, layoutParams);
228        mVideoSurfaceView.setZOrderOnTop(true);
229        mVideoSurfaceView.setOnKeyListener(this);
230        mVideoSurfaceView.setOnTouchListener(this);
231        mVideoSurfaceView.getHolder().addCallback(this);
232        mVideoSurfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
233        mVideoSurfaceView.setFocusable(true);
234        mVideoSurfaceView.setFocusableInTouchMode(true);
235        mVideoSurfaceView.requestFocus();
236    }
237
238    @CalledByNative
239    public void onMediaPlayerError(int errorType) {
240        Log.d(TAG, "OnMediaPlayerError: " + errorType);
241        if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) {
242            return;
243        }
244
245        mCurrentState = STATE_ERROR;
246        if (mMediaController != null) {
247            mMediaController.hide();
248        }
249
250        /* Pop up an error dialog so the user knows that
251         * something bad has happened. Only try and pop up the dialog
252         * if we're attached to a window. When we're going away and no
253         * longer have a window, don't bother showing the user an error.
254         *
255         * TODO(qinmin): We need to review whether this Dialog is OK with
256         * the rest of the browser UI elements.
257         */
258        if (getWindowToken() != null) {
259            String message;
260
261            if (errorType == MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
262                message = mPlaybackErrorText;
263            } else {
264                message = mUnknownErrorText;
265            }
266
267            new AlertDialog.Builder(getContext())
268                .setTitle(mErrorTitle)
269                .setMessage(message)
270                .setPositiveButton(mErrorButton,
271                        new DialogInterface.OnClickListener() {
272                    public void onClick(DialogInterface dialog, int whichButton) {
273                        /* Inform that the video is over.
274                         */
275                        onCompletion();
276                    }
277                })
278                .setCancelable(false)
279                .show();
280        }
281    }
282
283    @CalledByNative
284    public void onVideoSizeChanged(int width, int height) {
285        mVideoWidth = width;
286        mVideoHeight = height;
287        if (mVideoWidth != 0 && mVideoHeight != 0) {
288            mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight);
289        }
290    }
291
292    @CalledByNative
293    public void onBufferingUpdate(int percent) {
294        mCurrentBufferPercentage = percent;
295    }
296
297    @CalledByNative
298    public void onPlaybackComplete() {
299        onCompletion();
300    }
301
302    @CalledByNative
303    public void updateMediaMetadata(
304            int videoWidth,
305            int videoHeight,
306            int duration,
307            boolean canPause,
308            boolean canSeekBack,
309            boolean canSeekForward) {
310        mProgressView.setVisibility(View.GONE);
311        mDuration = duration;
312        mCanPause = canPause;
313        mCanSeekBack = canSeekBack;
314        mCanSeekForward = canSeekForward;
315        mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED;
316        if (mMediaController != null) {
317            mMediaController.setEnabled(true);
318            // If paused , should show the controller for ever.
319            if (isPlaying())
320                mMediaController.show();
321            else
322                mMediaController.show(0);
323        }
324
325        onVideoSizeChanged(videoWidth, videoHeight);
326    }
327
328    public void destroyNativeView() {
329        if (mNativeContentVideoView != 0) {
330            mNativeContentVideoView = 0;
331            destroyContentVideoView();
332        }
333    }
334
335    @Override
336    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
337        mVideoSurfaceView.setFocusable(true);
338        mVideoSurfaceView.setFocusableInTouchMode(true);
339        if (isInPlaybackState() && mMediaController != null) {
340            mMediaController.show();
341        }
342    }
343
344    @Override
345    public void surfaceCreated(SurfaceHolder holder) {
346        mSurfaceHolder = holder;
347        openVideo();
348    }
349
350    @Override
351    public void surfaceDestroyed(SurfaceHolder holder) {
352        mSurfaceHolder = null;
353        if (mNativeContentVideoView != 0) {
354            nativeExitFullscreen(mNativeContentVideoView, true);
355            mNativeContentVideoView = 0;
356            post(mExitFullscreenRunnable);
357        }
358        removeMediaController();
359    }
360
361    public void setMediaController(MediaController controller) {
362        if (mMediaController != null) {
363            mMediaController.hide();
364        }
365        mMediaController = controller;
366        attachMediaController();
367    }
368
369    private void attachMediaController() {
370        if (mMediaController != null) {
371            mMediaController.setMediaPlayer(this);
372            mMediaController.setAnchorView(mVideoSurfaceView);
373            mMediaController.setEnabled(false);
374        }
375    }
376
377    @CalledByNative
378    public void openVideo() {
379        if (mSurfaceHolder != null) {
380            mCurrentState = STATE_IDLE;
381            setMediaController(new FullScreenMediaController(sDelegate.getContext(), this));
382            if (mNativeContentVideoView != 0) {
383                nativeUpdateMediaMetadata(mNativeContentVideoView);
384            }
385            mCurrentBufferPercentage = 0;
386            if (mNativeContentVideoView != 0) {
387                nativeSetSurface(mNativeContentVideoView,
388                                 mSurfaceHolder.getSurface());
389            }
390        }
391    }
392
393    private void onCompletion() {
394        mCurrentState = STATE_PLAYBACK_COMPLETED;
395        if (mMediaController != null) {
396            mMediaController.hide();
397        }
398    }
399
400    @Override
401    public boolean onTouch(View v, MotionEvent event) {
402        if (isInPlaybackState() && mMediaController != null &&
403                event.getAction() == MotionEvent.ACTION_DOWN) {
404            toggleMediaControlsVisiblity();
405        }
406        return true;
407    }
408
409    @Override
410    public boolean onTrackballEvent(MotionEvent ev) {
411        if (isInPlaybackState() && mMediaController != null) {
412            toggleMediaControlsVisiblity();
413        }
414        return false;
415    }
416
417    @Override
418    public boolean onKey(View v, int keyCode, KeyEvent event) {
419        boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK &&
420                                     keyCode != KeyEvent.KEYCODE_VOLUME_UP &&
421                                     keyCode != KeyEvent.KEYCODE_VOLUME_DOWN &&
422                                     keyCode != KeyEvent.KEYCODE_VOLUME_MUTE &&
423                                     keyCode != KeyEvent.KEYCODE_CALL &&
424                                     keyCode != KeyEvent.KEYCODE_MENU &&
425                                     keyCode != KeyEvent.KEYCODE_SEARCH &&
426                                     keyCode != KeyEvent.KEYCODE_ENDCALL;
427        if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) {
428            if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
429                    keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
430                if (isPlaying()) {
431                    pause();
432                    mMediaController.show();
433                } else {
434                    start();
435                    mMediaController.hide();
436                }
437                return true;
438            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
439                if (!isPlaying()) {
440                    start();
441                    mMediaController.hide();
442                }
443                return true;
444            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
445                    || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
446                if (isPlaying()) {
447                    pause();
448                    mMediaController.show();
449                }
450                return true;
451            } else {
452                toggleMediaControlsVisiblity();
453            }
454        } else if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
455            if (mNativeContentVideoView != 0) {
456                nativeExitFullscreen(mNativeContentVideoView, false);
457                destroyNativeView();
458            }
459            return true;
460        } else if (keyCode == KeyEvent.KEYCODE_MENU || keyCode == KeyEvent.KEYCODE_SEARCH) {
461            return true;
462        }
463        return super.onKeyDown(keyCode, event);
464    }
465
466    private void toggleMediaControlsVisiblity() {
467        if (mMediaController.isShowing()) {
468            mMediaController.hide();
469        } else {
470            mMediaController.show();
471        }
472    }
473
474    private boolean isInPlaybackState() {
475        return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE);
476    }
477
478    public void start() {
479        if (isInPlaybackState()) {
480            if (mNativeContentVideoView != 0) {
481                nativePlay(mNativeContentVideoView);
482            }
483            mCurrentState = STATE_PLAYING;
484        }
485    }
486
487    public void pause() {
488        if (isInPlaybackState()) {
489            if (isPlaying()) {
490                if (mNativeContentVideoView != 0) {
491                    nativePause(mNativeContentVideoView);
492                }
493                mCurrentState = STATE_PAUSED;
494            }
495        }
496    }
497
498    // cache duration as mDuration for faster access
499    public int getDuration() {
500        if (isInPlaybackState()) {
501            if (mDuration > 0) {
502                return mDuration;
503            }
504            if (mNativeContentVideoView != 0) {
505                mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView);
506            } else {
507                mDuration = 0;
508            }
509            return mDuration;
510        }
511        mDuration = -1;
512        return mDuration;
513    }
514
515    public int getCurrentPosition() {
516        if (isInPlaybackState() && mNativeContentVideoView != 0) {
517            return nativeGetCurrentPosition(mNativeContentVideoView);
518        }
519        return 0;
520    }
521
522    public void seekTo(int msec) {
523        if (mNativeContentVideoView != 0) {
524            nativeSeekTo(mNativeContentVideoView, msec);
525        }
526    }
527
528    public boolean isPlaying() {
529        return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView);
530    }
531
532    public int getBufferPercentage() {
533        return mCurrentBufferPercentage;
534    }
535    public boolean canPause() {
536        return mCanPause;
537    }
538
539    public boolean canSeekBackward() {
540        return mCanSeekBack;
541    }
542
543    public boolean canSeekForward() {
544        return mCanSeekForward;
545    }
546
547    public int getAudioSessionId() {
548        return 0;
549    }
550
551    @CalledByNative
552    public static ContentVideoView createContentVideoView(int nativeContentVideoView) {
553        if (sContentVideoView != null)
554            return sContentVideoView;
555
556        if (sDelegate != null && sDelegate.getContext() != null) {
557            sContentVideoView = new ContentVideoView(sDelegate.getContext(),
558                    nativeContentVideoView);
559
560            sDelegate.onShowCustomView(sContentVideoView);
561            sContentVideoView.setBackgroundColor(Color.BLACK);
562            sContentVideoView.showContentVideoView();
563            sContentVideoView.setVisibility(View.VISIBLE);
564            return sContentVideoView;
565        }
566        return null;
567    }
568
569    public void removeMediaController() {
570        if (mMediaController != null) {
571            mMediaController.setEnabled(false);
572            mMediaController.hide();
573            mMediaController = null;
574        }
575    }
576
577    public void removeSurfaceView() {
578        removeView(mVideoSurfaceView);
579        removeView(mProgressView);
580        mVideoSurfaceView = null;
581        mProgressView = null;
582    }
583
584    @CalledByNative
585    public static void destroyContentVideoView() {
586        sDelegate.onDestroyContentVideoView();
587        if (sContentVideoView != null) {
588            sContentVideoView.removeMediaController();
589            sContentVideoView.removeSurfaceView();
590            sContentVideoView.setVisibility(View.GONE);
591        }
592        sContentVideoView = null;
593    }
594
595    public static ContentVideoView getContentVideoView() {
596        return sContentVideoView;
597    }
598
599    public static void registerContentVideoViewContextDelegate(
600       ContentVideoViewContextDelegate delegate) {
601        sDelegate = delegate;
602    }
603
604    @Override
605    public boolean onTouchEvent(MotionEvent ev) {
606        return true;
607    }
608
609    @Override
610    public boolean onKeyDown(int keyCode, KeyEvent event) {
611        if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
612            destroyContentVideoView();
613            return true;
614        }
615        return super.onKeyDown(keyCode, event);
616    }
617
618    private native void nativeExitFullscreen(int nativeContentVideoView, boolean relaseMediaPlayer);
619    private native int nativeGetCurrentPosition(int nativeContentVideoView);
620    private native int nativeGetDurationInMilliSeconds(int nativeContentVideoView);
621    private native void nativeUpdateMediaMetadata(int nativeContentVideoView);
622    private native int nativeGetVideoWidth(int nativeContentVideoView);
623    private native int nativeGetVideoHeight(int nativeContentVideoView);
624    private native boolean nativeIsPlaying(int nativeContentVideoView);
625    private native void nativePause(int nativeContentVideoView);
626    private native void nativePlay(int nativeContentVideoView);
627    private native void nativeSeekTo(int nativeContentVideoView, int msec);
628    private native void nativeSetSurface(int nativeContentVideoView, Surface surface);
629}
630