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