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