ContentVideoView.java revision 5f1c94371a64b3196d4be9466099bb892df9b88e
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.ContextWrapper;
11import android.content.DialogInterface;
12import android.util.Log;
13import android.view.Gravity;
14import android.view.KeyEvent;
15import android.view.Surface;
16import android.view.SurfaceHolder;
17import android.view.SurfaceView;
18import android.view.View;
19import android.view.ViewGroup;
20import android.widget.FrameLayout;
21import android.widget.LinearLayout;
22import android.widget.ProgressBar;
23import android.widget.TextView;
24
25import org.chromium.base.CalledByNative;
26import org.chromium.base.JNINamespace;
27import org.chromium.base.ThreadUtils;
28import org.chromium.ui.base.ViewAndroid;
29import org.chromium.ui.base.ViewAndroidDelegate;
30import org.chromium.ui.base.WindowAndroid;
31
32/**
33 * This class implements accelerated fullscreen video playback using surface view.
34 */
35@JNINamespace("content")
36public class ContentVideoView extends FrameLayout
37        implements SurfaceHolder.Callback, ViewAndroidDelegate {
38
39    private static final String TAG = "ContentVideoView";
40
41    /* Do not change these values without updating their counterparts
42     * in include/media/mediaplayer.h!
43     */
44    private static final int MEDIA_NOP = 0; // interface test message
45    private static final int MEDIA_PREPARED = 1;
46    private static final int MEDIA_PLAYBACK_COMPLETE = 2;
47    private static final int MEDIA_BUFFERING_UPDATE = 3;
48    private static final int MEDIA_SEEK_COMPLETE = 4;
49    private static final int MEDIA_SET_VIDEO_SIZE = 5;
50    private static final int MEDIA_ERROR = 100;
51    private static final int MEDIA_INFO = 200;
52
53    /**
54     * Keep these error codes in sync with the code we defined in
55     * MediaPlayerListener.java.
56     */
57    public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 2;
58    public static final int MEDIA_ERROR_INVALID_CODE = 3;
59
60    // all possible internal states
61    private static final int STATE_ERROR              = -1;
62    private static final int STATE_IDLE               = 0;
63    private static final int STATE_PLAYING            = 1;
64    private static final int STATE_PAUSED             = 2;
65    private static final int STATE_PLAYBACK_COMPLETED = 3;
66
67    private SurfaceHolder mSurfaceHolder;
68    private int mVideoWidth;
69    private int mVideoHeight;
70    private int mDuration;
71
72    // Native pointer to C++ ContentVideoView object.
73    private long mNativeContentVideoView;
74
75    // webkit should have prepared the media
76    private int mCurrentState = STATE_IDLE;
77
78    // Strings for displaying media player errors
79    private String mPlaybackErrorText;
80    private String mUnknownErrorText;
81    private String mErrorButton;
82    private String mErrorTitle;
83    private String mVideoLoadingText;
84
85    // This view will contain the video.
86    private VideoSurfaceView mVideoSurfaceView;
87
88    // Progress view when the video is loading.
89    private View mProgressView;
90
91    // The ViewAndroid is used to keep screen on during video playback.
92    private ViewAndroid mViewAndroid;
93
94    private final ContentVideoViewClient mClient;
95
96    private class VideoSurfaceView extends SurfaceView {
97
98        public VideoSurfaceView(Context context) {
99            super(context);
100        }
101
102        @Override
103        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
104            // set the default surface view size to (1, 1) so that it won't block
105            // the infobar. (0, 0) is not a valid size for surface view.
106            int width = 1;
107            int height = 1;
108            if (mVideoWidth > 0 && mVideoHeight > 0) {
109                width = getDefaultSize(mVideoWidth, widthMeasureSpec);
110                height = getDefaultSize(mVideoHeight, heightMeasureSpec);
111                if (mVideoWidth * height  > width * mVideoHeight) {
112                    height = width * mVideoHeight / mVideoWidth;
113                } else if (mVideoWidth * height  < width * mVideoHeight) {
114                    width = height * mVideoWidth / mVideoHeight;
115                }
116            }
117            setMeasuredDimension(width, height);
118        }
119    }
120
121    private static class ProgressView extends LinearLayout {
122
123        private final ProgressBar mProgressBar;
124        private final TextView mTextView;
125
126        public ProgressView(Context context, String videoLoadingText) {
127            super(context);
128            setOrientation(LinearLayout.VERTICAL);
129            setLayoutParams(new LinearLayout.LayoutParams(
130                    LinearLayout.LayoutParams.WRAP_CONTENT,
131                    LinearLayout.LayoutParams.WRAP_CONTENT));
132            mProgressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge);
133            mTextView = new TextView(context);
134            mTextView.setText(videoLoadingText);
135            addView(mProgressBar);
136            addView(mTextView);
137        }
138    }
139
140    private final Runnable mExitFullscreenRunnable = new Runnable() {
141        @Override
142        public void run() {
143            exitFullscreen(true);
144        }
145    };
146
147    protected ContentVideoView(Context context, long nativeContentVideoView,
148            ContentVideoViewClient client) {
149        super(context);
150        mNativeContentVideoView = nativeContentVideoView;
151        mViewAndroid = new ViewAndroid(new WindowAndroid(context.getApplicationContext()), this);
152        mClient = client;
153        initResources(context);
154        mVideoSurfaceView = new VideoSurfaceView(context);
155        showContentVideoView();
156        setVisibility(View.VISIBLE);
157    }
158
159    protected ContentVideoViewClient getContentVideoViewClient() {
160        return mClient;
161    }
162
163    private void initResources(Context context) {
164        if (mPlaybackErrorText != null) return;
165        mPlaybackErrorText = context.getString(
166                org.chromium.content.R.string.media_player_error_text_invalid_progressive_playback);
167        mUnknownErrorText = context.getString(
168                org.chromium.content.R.string.media_player_error_text_unknown);
169        mErrorButton = context.getString(
170                org.chromium.content.R.string.media_player_error_button);
171        mErrorTitle = context.getString(
172                org.chromium.content.R.string.media_player_error_title);
173        mVideoLoadingText = context.getString(
174                org.chromium.content.R.string.media_player_loading_video);
175    }
176
177    protected void showContentVideoView() {
178        mVideoSurfaceView.getHolder().addCallback(this);
179        this.addView(mVideoSurfaceView, new FrameLayout.LayoutParams(
180                ViewGroup.LayoutParams.WRAP_CONTENT,
181                ViewGroup.LayoutParams.WRAP_CONTENT,
182                Gravity.CENTER));
183
184        mProgressView = mClient.getVideoLoadingProgressView();
185        if (mProgressView == null) {
186            mProgressView = new ProgressView(getContext(), mVideoLoadingText);
187        }
188        this.addView(mProgressView, new FrameLayout.LayoutParams(
189                ViewGroup.LayoutParams.WRAP_CONTENT,
190                ViewGroup.LayoutParams.WRAP_CONTENT,
191                Gravity.CENTER));
192    }
193
194    protected SurfaceView getSurfaceView() {
195        return mVideoSurfaceView;
196    }
197
198    @CalledByNative
199    public void onMediaPlayerError(int errorType) {
200        Log.d(TAG, "OnMediaPlayerError: " + errorType);
201        if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) {
202            return;
203        }
204
205        // Ignore some invalid error codes.
206        if (errorType == MEDIA_ERROR_INVALID_CODE) {
207            return;
208        }
209
210        mCurrentState = STATE_ERROR;
211
212        /* Pop up an error dialog so the user knows that
213         * something bad has happened. Only try and pop up the dialog
214         * if we're attached to a window. When we're going away and no
215         * longer have a window, don't bother showing the user an error.
216         *
217         * TODO(qinmin): We need to review whether this Dialog is OK with
218         * the rest of the browser UI elements.
219         */
220        if (getWindowToken() != null) {
221            String message;
222
223            if (errorType == MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
224                message = mPlaybackErrorText;
225            } else {
226                message = mUnknownErrorText;
227            }
228
229            try {
230                new AlertDialog.Builder(getContext())
231                    .setTitle(mErrorTitle)
232                    .setMessage(message)
233                    .setPositiveButton(mErrorButton,
234                            new DialogInterface.OnClickListener() {
235                        @Override
236                        public void onClick(DialogInterface dialog, int whichButton) {
237                            /* Inform that the video is over.
238                             */
239                            onCompletion();
240                        }
241                    })
242                    .setCancelable(false)
243                    .show();
244            } catch (RuntimeException e) {
245                Log.e(TAG, "Cannot show the alert dialog, error message: " + message, e);
246            }
247        }
248    }
249
250    @CalledByNative
251    private void onVideoSizeChanged(int width, int height) {
252        mVideoWidth = width;
253        mVideoHeight = height;
254        // This will trigger the SurfaceView.onMeasure() call.
255        mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight);
256    }
257
258    @CalledByNative
259    protected void onBufferingUpdate(int percent) {
260    }
261
262    @CalledByNative
263    private void onPlaybackComplete() {
264        onCompletion();
265    }
266
267    @CalledByNative
268    protected void onUpdateMediaMetadata(
269            int videoWidth,
270            int videoHeight,
271            int duration,
272            boolean canPause,
273            boolean canSeekBack,
274            boolean canSeekForward) {
275        mDuration = duration;
276        mProgressView.setVisibility(View.GONE);
277        mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED;
278        onVideoSizeChanged(videoWidth, videoHeight);
279    }
280
281    @Override
282    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
283    }
284
285    @Override
286    public void surfaceCreated(SurfaceHolder holder) {
287        mSurfaceHolder = holder;
288        openVideo();
289    }
290
291    @Override
292    public void surfaceDestroyed(SurfaceHolder holder) {
293        if (mNativeContentVideoView != 0) {
294            nativeSetSurface(mNativeContentVideoView, null);
295        }
296        mSurfaceHolder = null;
297        post(mExitFullscreenRunnable);
298    }
299
300    @CalledByNative
301    protected void openVideo() {
302        if (mSurfaceHolder != null) {
303            mCurrentState = STATE_IDLE;
304            if (mNativeContentVideoView != 0) {
305                nativeRequestMediaMetadata(mNativeContentVideoView);
306                nativeSetSurface(mNativeContentVideoView,
307                        mSurfaceHolder.getSurface());
308            }
309        }
310    }
311
312    protected void onCompletion() {
313        mCurrentState = STATE_PLAYBACK_COMPLETED;
314    }
315
316
317    protected boolean isInPlaybackState() {
318        return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE);
319    }
320
321    protected void start() {
322        if (isInPlaybackState()) {
323            if (mNativeContentVideoView != 0) {
324                nativePlay(mNativeContentVideoView);
325            }
326            mCurrentState = STATE_PLAYING;
327        }
328    }
329
330    protected void pause() {
331        if (isInPlaybackState()) {
332            if (isPlaying()) {
333                if (mNativeContentVideoView != 0) {
334                    nativePause(mNativeContentVideoView);
335                }
336                mCurrentState = STATE_PAUSED;
337            }
338        }
339    }
340
341    // cache duration as mDuration for faster access
342    protected int getDuration() {
343        if (isInPlaybackState()) {
344            if (mDuration > 0) {
345                return mDuration;
346            }
347            if (mNativeContentVideoView != 0) {
348                mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView);
349            } else {
350                mDuration = 0;
351            }
352            return mDuration;
353        }
354        mDuration = -1;
355        return mDuration;
356    }
357
358    protected int getCurrentPosition() {
359        if (isInPlaybackState() && mNativeContentVideoView != 0) {
360            return nativeGetCurrentPosition(mNativeContentVideoView);
361        }
362        return 0;
363    }
364
365    protected void seekTo(int msec) {
366        if (mNativeContentVideoView != 0) {
367            nativeSeekTo(mNativeContentVideoView, msec);
368        }
369    }
370
371    public boolean isPlaying() {
372        return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView);
373    }
374
375    @CalledByNative
376    private static ContentVideoView createContentVideoView(
377            Context context, long nativeContentVideoView, ContentVideoViewClient client,
378            boolean legacy) {
379        ThreadUtils.assertOnUiThread();
380        // The context needs be Activity to create the ContentVideoView correctly.
381        if (!isActivityContext(context)) {
382            Log.e(TAG, "Wrong type of context, can't create fullscreen video");
383            return null;
384        }
385        ContentVideoView videoView = null;
386        if (legacy) {
387            videoView = new ContentVideoViewLegacy(context, nativeContentVideoView, client);
388        } else {
389            videoView = new ContentVideoView(context, nativeContentVideoView, client);
390        }
391
392        if (videoView.getContentVideoViewClient().onShowCustomView(videoView)) {
393            return videoView;
394        }
395        return null;
396    }
397
398    private static boolean isActivityContext(Context context) {
399        // Only retrieve the base context if the supplied context is a ContextWrapper but not
400        // an Activity, given that Activity is already a subclass of ContextWrapper.
401        if (context instanceof ContextWrapper && !(context instanceof Activity)) {
402            context = ((ContextWrapper) context).getBaseContext();
403        }
404        return context instanceof Activity;
405    }
406
407    public void removeSurfaceView() {
408        removeView(mVideoSurfaceView);
409        removeView(mProgressView);
410        mVideoSurfaceView = null;
411        mProgressView = null;
412    }
413
414    public void exitFullscreen(boolean relaseMediaPlayer) {
415        destroyContentVideoView(false);
416        if (mNativeContentVideoView != 0) {
417            nativeExitFullscreen(mNativeContentVideoView, relaseMediaPlayer);
418            mNativeContentVideoView = 0;
419        }
420    }
421
422    @CalledByNative
423    private void onExitFullscreen() {
424        exitFullscreen(false);
425    }
426
427    /**
428     * This method shall only be called by native and exitFullscreen,
429     * To exit fullscreen, use exitFullscreen in Java.
430     */
431    @CalledByNative
432    protected void destroyContentVideoView(boolean nativeViewDestroyed) {
433        if (mVideoSurfaceView != null) {
434            removeSurfaceView();
435            setVisibility(View.GONE);
436
437            // To prevent re-entrance, call this after removeSurfaceView.
438            mClient.onDestroyContentVideoView();
439        }
440        if (nativeViewDestroyed) {
441            mNativeContentVideoView = 0;
442        }
443    }
444
445    public static ContentVideoView getContentVideoView() {
446        return nativeGetSingletonJavaContentVideoView();
447    }
448
449    @Override
450    public boolean onKeyUp(int keyCode, KeyEvent event) {
451        if (keyCode == KeyEvent.KEYCODE_BACK) {
452            exitFullscreen(false);
453            return true;
454        }
455        return super.onKeyUp(keyCode, event);
456    }
457
458    @Override
459    public View acquireAnchorView() {
460        View anchorView = new View(getContext());
461        addView(anchorView);
462        return anchorView;
463    }
464
465    @Override
466    public void setAnchorViewPosition(View view, float x, float y, float width, float height) {
467        Log.e(TAG, "setAnchorViewPosition isn't implemented");
468    }
469
470    @Override
471    public void releaseAnchorView(View anchorView) {
472        removeView(anchorView);
473    }
474
475    @CalledByNative
476    private long getNativeViewAndroid() {
477        return mViewAndroid.getNativePointer();
478    }
479
480    private static native ContentVideoView nativeGetSingletonJavaContentVideoView();
481    private native void nativeExitFullscreen(long nativeContentVideoView,
482            boolean relaseMediaPlayer);
483    private native int nativeGetCurrentPosition(long nativeContentVideoView);
484    private native int nativeGetDurationInMilliSeconds(long nativeContentVideoView);
485    private native void nativeRequestMediaMetadata(long nativeContentVideoView);
486    private native int nativeGetVideoWidth(long nativeContentVideoView);
487    private native int nativeGetVideoHeight(long nativeContentVideoView);
488    private native boolean nativeIsPlaying(long nativeContentVideoView);
489    private native void nativePause(long nativeContentVideoView);
490    private native void nativePlay(long nativeContentVideoView);
491    private native void nativeSeekTo(long nativeContentVideoView, int msec);
492    private native void nativeSetSurface(long nativeContentVideoView, Surface surface);
493}
494