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