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