ContentVideoView.java revision 868fa2fe829687343ffae624259930155e16dbd8
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.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
226        mVideoSurfaceView.setFocusable(true);
227        mVideoSurfaceView.setFocusableInTouchMode(true);
228        mVideoSurfaceView.requestFocus();
229    }
230
231    @CalledByNative
232    public void onMediaPlayerError(int errorType) {
233        Log.d(TAG, "OnMediaPlayerError: " + errorType);
234        if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) {
235            return;
236        }
237
238        mCurrentState = STATE_ERROR;
239        if (mMediaController != null) {
240            mMediaController.hide();
241        }
242
243        /* Pop up an error dialog so the user knows that
244         * something bad has happened. Only try and pop up the dialog
245         * if we're attached to a window. When we're going away and no
246         * longer have a window, don't bother showing the user an error.
247         *
248         * TODO(qinmin): We need to review whether this Dialog is OK with
249         * the rest of the browser UI elements.
250         */
251        if (getWindowToken() != null) {
252            String message;
253
254            if (errorType == MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
255                message = mPlaybackErrorText;
256            } else {
257                message = mUnknownErrorText;
258            }
259
260            new AlertDialog.Builder(getContext())
261                .setTitle(mErrorTitle)
262                .setMessage(message)
263                .setPositiveButton(mErrorButton,
264                        new DialogInterface.OnClickListener() {
265                    public void onClick(DialogInterface dialog, int whichButton) {
266                        /* Inform that the video is over.
267                         */
268                        onCompletion();
269                    }
270                })
271                .setCancelable(false)
272                .show();
273        }
274    }
275
276    @CalledByNative
277    private void onVideoSizeChanged(int width, int height) {
278        mVideoWidth = width;
279        mVideoHeight = height;
280        if (mVideoWidth != 0 && mVideoHeight != 0) {
281            mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight);
282        }
283    }
284
285    @CalledByNative
286    private void onBufferingUpdate(int percent) {
287        mCurrentBufferPercentage = percent;
288    }
289
290    @CalledByNative
291    private void onPlaybackComplete() {
292        onCompletion();
293    }
294
295    @CalledByNative
296    private void onUpdateMediaMetadata(
297            int videoWidth,
298            int videoHeight,
299            int duration,
300            boolean canPause,
301            boolean canSeekBack,
302            boolean canSeekForward) {
303        mProgressView.setVisibility(View.GONE);
304        mDuration = duration;
305        mCanPause = canPause;
306        mCanSeekBack = canSeekBack;
307        mCanSeekForward = canSeekForward;
308        mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED;
309        if (mMediaController != null) {
310            mMediaController.setEnabled(true);
311            // If paused , should show the controller for ever.
312            if (isPlaying())
313                mMediaController.show();
314            else
315                mMediaController.show(0);
316        }
317
318        onVideoSizeChanged(videoWidth, videoHeight);
319    }
320
321    @Override
322    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
323        mVideoSurfaceView.setFocusable(true);
324        mVideoSurfaceView.setFocusableInTouchMode(true);
325        if (isInPlaybackState() && mMediaController != null) {
326            mMediaController.show();
327        }
328    }
329
330    @Override
331    public void surfaceCreated(SurfaceHolder holder) {
332        mSurfaceHolder = holder;
333        openVideo();
334    }
335
336    @Override
337    public void surfaceDestroyed(SurfaceHolder holder) {
338        if (mNativeContentVideoView != 0) {
339            nativeSetSurface(mNativeContentVideoView, null);
340        }
341        mSurfaceHolder = null;
342        post(mExitFullscreenRunnable);
343    }
344
345    private void setMediaController(MediaController controller) {
346        if (mMediaController != null) {
347            mMediaController.hide();
348        }
349        mMediaController = controller;
350        attachMediaController();
351    }
352
353    private void attachMediaController() {
354        if (mMediaController != null) {
355            mMediaController.setMediaPlayer(this);
356            mMediaController.setAnchorView(mVideoSurfaceView);
357            mMediaController.setEnabled(false);
358        }
359    }
360
361    @CalledByNative
362    private void openVideo() {
363        if (mSurfaceHolder != null) {
364            mCurrentState = STATE_IDLE;
365            mCurrentBufferPercentage = 0;
366            setMediaController(new FullScreenMediaController(getContext(), this));
367            if (mNativeContentVideoView != 0) {
368                nativeUpdateMediaMetadata(mNativeContentVideoView);
369                nativeSetSurface(mNativeContentVideoView,
370                        mSurfaceHolder.getSurface());
371            }
372        }
373    }
374
375    private void onCompletion() {
376        mCurrentState = STATE_PLAYBACK_COMPLETED;
377        if (mMediaController != null) {
378            mMediaController.hide();
379        }
380    }
381
382    @Override
383    public boolean onTouch(View v, MotionEvent event) {
384        if (isInPlaybackState() && mMediaController != null &&
385                event.getAction() == MotionEvent.ACTION_DOWN) {
386            toggleMediaControlsVisiblity();
387        }
388        return true;
389    }
390
391    @Override
392    public boolean onTrackballEvent(MotionEvent ev) {
393        if (isInPlaybackState() && mMediaController != null) {
394            toggleMediaControlsVisiblity();
395        }
396        return false;
397    }
398
399    @Override
400    public boolean onKey(View v, int keyCode, KeyEvent event) {
401        boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK &&
402                                     keyCode != KeyEvent.KEYCODE_VOLUME_UP &&
403                                     keyCode != KeyEvent.KEYCODE_VOLUME_DOWN &&
404                                     keyCode != KeyEvent.KEYCODE_VOLUME_MUTE &&
405                                     keyCode != KeyEvent.KEYCODE_CALL &&
406                                     keyCode != KeyEvent.KEYCODE_MENU &&
407                                     keyCode != KeyEvent.KEYCODE_SEARCH &&
408                                     keyCode != KeyEvent.KEYCODE_ENDCALL;
409        if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) {
410            if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
411                    keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
412                if (isPlaying()) {
413                    pause();
414                    mMediaController.show();
415                } else {
416                    start();
417                    mMediaController.hide();
418                }
419                return true;
420            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
421                if (!isPlaying()) {
422                    start();
423                    mMediaController.hide();
424                }
425                return true;
426            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
427                    || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
428                if (isPlaying()) {
429                    pause();
430                    mMediaController.show();
431                }
432                return true;
433            } else {
434                toggleMediaControlsVisiblity();
435            }
436        } else if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
437            exitFullscreen(false);
438            return true;
439        } else if (keyCode == KeyEvent.KEYCODE_MENU || keyCode == KeyEvent.KEYCODE_SEARCH) {
440            return true;
441        }
442        return super.onKeyDown(keyCode, event);
443    }
444
445    private void toggleMediaControlsVisiblity() {
446        if (mMediaController.isShowing()) {
447            mMediaController.hide();
448        } else {
449            mMediaController.show();
450        }
451    }
452
453    private boolean isInPlaybackState() {
454        return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE);
455    }
456
457    @Override
458    public void start() {
459        if (isInPlaybackState()) {
460            if (mNativeContentVideoView != 0) {
461                nativePlay(mNativeContentVideoView);
462            }
463            mCurrentState = STATE_PLAYING;
464        }
465    }
466
467    @Override
468    public void pause() {
469        if (isInPlaybackState()) {
470            if (isPlaying()) {
471                if (mNativeContentVideoView != 0) {
472                    nativePause(mNativeContentVideoView);
473                }
474                mCurrentState = STATE_PAUSED;
475            }
476        }
477    }
478
479    // cache duration as mDuration for faster access
480    @Override
481    public int getDuration() {
482        if (isInPlaybackState()) {
483            if (mDuration > 0) {
484                return mDuration;
485            }
486            if (mNativeContentVideoView != 0) {
487                mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView);
488            } else {
489                mDuration = 0;
490            }
491            return mDuration;
492        }
493        mDuration = -1;
494        return mDuration;
495    }
496
497    @Override
498    public int getCurrentPosition() {
499        if (isInPlaybackState() && mNativeContentVideoView != 0) {
500            return nativeGetCurrentPosition(mNativeContentVideoView);
501        }
502        return 0;
503    }
504
505    @Override
506    public void seekTo(int msec) {
507        if (mNativeContentVideoView != 0) {
508            nativeSeekTo(mNativeContentVideoView, msec);
509        }
510    }
511
512    @Override
513    public boolean isPlaying() {
514        return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView);
515    }
516
517    @Override
518    public int getBufferPercentage() {
519        return mCurrentBufferPercentage;
520    }
521
522    @Override
523    public boolean canPause() {
524        return mCanPause;
525    }
526
527    @Override
528    public boolean canSeekBackward() {
529        return mCanSeekBack;
530    }
531
532    @Override
533    public boolean canSeekForward() {
534        return mCanSeekForward;
535    }
536
537    public int getAudioSessionId() {
538        return 0;
539    }
540
541    @CalledByNative
542    private static ContentVideoView createContentVideoView(
543            Context context, int nativeContentVideoView, ContentVideoViewClient client) {
544        ThreadUtils.assertOnUiThread();
545        // The context needs be Activity to create the ContentVideoView correctly.
546        if (!(context instanceof Activity)) {
547            Log.w(TAG, "Wrong type of context, can't create fullscreen video");
548            return null;
549        }
550        return new ContentVideoView(context, nativeContentVideoView, client);
551    }
552
553    private void removeMediaController() {
554        if (mMediaController != null) {
555            mMediaController.setEnabled(false);
556            mMediaController.hide();
557            mMediaController = null;
558        }
559    }
560
561    public void removeSurfaceView() {
562        removeView(mVideoSurfaceView);
563        removeView(mProgressView);
564        mVideoSurfaceView = null;
565        mProgressView = null;
566    }
567
568    public void exitFullscreen(boolean relaseMediaPlayer) {
569        destroyContentVideoView();
570        if (mNativeContentVideoView != 0) {
571            nativeExitFullscreen(mNativeContentVideoView, relaseMediaPlayer);
572            mNativeContentVideoView = 0;
573        }
574    }
575
576    @CalledByNative
577    public static void keepScreenOnContentVideoView(boolean screenOn) {
578        ContentVideoView content_video_view = getContentVideoView();
579        if ( content_video_view != null) {
580            content_video_view.mClient.keepScreenOn(screenOn);
581        }
582    }
583
584    /**
585     * This method shall only be called by native and exitFullscreen,
586     * To exit fullscreen, use exitFullscreen in Java.
587     */
588    @CalledByNative
589    private void destroyContentVideoView() {
590        if (mVideoSurfaceView != null) {
591            mClient.onDestroyContentVideoView();
592            removeMediaController();
593            removeSurfaceView();
594            setVisibility(View.GONE);
595        }
596    }
597
598    public static ContentVideoView getContentVideoView() {
599        return nativeGetSingletonJavaContentVideoView();
600    }
601
602    @Override
603    public boolean onTouchEvent(MotionEvent ev) {
604        return true;
605    }
606
607    @Override
608    public boolean onKeyDown(int keyCode, KeyEvent event) {
609        if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
610            exitFullscreen(false);
611            return true;
612        }
613        return super.onKeyDown(keyCode, event);
614    }
615
616    private static native ContentVideoView nativeGetSingletonJavaContentVideoView();
617    private native void nativeExitFullscreen(int nativeContentVideoView, boolean relaseMediaPlayer);
618    private native int nativeGetCurrentPosition(int nativeContentVideoView);
619    private native int nativeGetDurationInMilliSeconds(int nativeContentVideoView);
620    private native void nativeUpdateMediaMetadata(int nativeContentVideoView);
621    private native int nativeGetVideoWidth(int nativeContentVideoView);
622    private native int nativeGetVideoHeight(int nativeContentVideoView);
623    private native boolean nativeIsPlaying(int nativeContentVideoView);
624    private native void nativePause(int nativeContentVideoView);
625    private native void nativePlay(int nativeContentVideoView);
626    private native void nativeSeekTo(int nativeContentVideoView, int msec);
627    private native void nativeSetSurface(int nativeContentVideoView, Surface surface);
628}
629