ContentVideoView.java revision 5c02ac1a9c1b504631c0a3d2b6e737b5d738bae1
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            // set the default surface view size to (1, 1) so that it won't block
104            // the infobar. (0, 0) is not a valid size for surface view.
105            int width = 1;
106            int height = 1;
107            if (mVideoWidth > 0 && mVideoHeight > 0) {
108                width = getDefaultSize(mVideoWidth, widthMeasureSpec);
109                height = getDefaultSize(mVideoHeight, heightMeasureSpec);
110                if (mVideoWidth * height  > width * mVideoHeight) {
111                    height = width * mVideoHeight / mVideoWidth;
112                } else if (mVideoWidth * height  < width * mVideoHeight) {
113                    width = height * mVideoWidth / mVideoHeight;
114                }
115            }
116            setMeasuredDimension(width, height);
117        }
118    }
119
120    private static class ProgressView extends LinearLayout {
121
122        private final ProgressBar mProgressBar;
123        private final TextView mTextView;
124
125        public ProgressView(Context context, String videoLoadingText) {
126            super(context);
127            setOrientation(LinearLayout.VERTICAL);
128            setLayoutParams(new LinearLayout.LayoutParams(
129                    LinearLayout.LayoutParams.WRAP_CONTENT,
130                    LinearLayout.LayoutParams.WRAP_CONTENT));
131            mProgressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge);
132            mTextView = new TextView(context);
133            mTextView.setText(videoLoadingText);
134            addView(mProgressBar);
135            addView(mTextView);
136        }
137    }
138
139    private final Runnable mExitFullscreenRunnable = new Runnable() {
140        @Override
141        public void run() {
142            exitFullscreen(true);
143        }
144    };
145
146    protected ContentVideoView(Context context, long nativeContentVideoView,
147            ContentVideoViewClient client) {
148        super(context);
149        mNativeContentVideoView = nativeContentVideoView;
150        mViewAndroid = new ViewAndroid(new WindowAndroid(context.getApplicationContext()), this);
151        mClient = client;
152        initResources(context);
153        mVideoSurfaceView = new VideoSurfaceView(context);
154        showContentVideoView();
155        setVisibility(View.VISIBLE);
156    }
157
158    protected ContentVideoViewClient getContentVideoViewClient() {
159        return mClient;
160    }
161
162    private void initResources(Context context) {
163        if (mPlaybackErrorText != null) return;
164        mPlaybackErrorText = context.getString(
165                org.chromium.content.R.string.media_player_error_text_invalid_progressive_playback);
166        mUnknownErrorText = context.getString(
167                org.chromium.content.R.string.media_player_error_text_unknown);
168        mErrorButton = context.getString(
169                org.chromium.content.R.string.media_player_error_button);
170        mErrorTitle = context.getString(
171                org.chromium.content.R.string.media_player_error_title);
172        mVideoLoadingText = context.getString(
173                org.chromium.content.R.string.media_player_loading_video);
174    }
175
176    protected void showContentVideoView() {
177        mVideoSurfaceView.getHolder().addCallback(this);
178        this.addView(mVideoSurfaceView, new FrameLayout.LayoutParams(
179                ViewGroup.LayoutParams.WRAP_CONTENT,
180                ViewGroup.LayoutParams.WRAP_CONTENT,
181                Gravity.CENTER));
182
183        mProgressView = mClient.getVideoLoadingProgressView();
184        if (mProgressView == null) {
185            mProgressView = new ProgressView(getContext(), mVideoLoadingText);
186        }
187        this.addView(mProgressView, new FrameLayout.LayoutParams(
188                ViewGroup.LayoutParams.WRAP_CONTENT,
189                ViewGroup.LayoutParams.WRAP_CONTENT,
190                Gravity.CENTER));
191    }
192
193    protected SurfaceView getSurfaceView() {
194        return mVideoSurfaceView;
195    }
196
197    @CalledByNative
198    public void onMediaPlayerError(int errorType) {
199        Log.d(TAG, "OnMediaPlayerError: " + errorType);
200        if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) {
201            return;
202        }
203
204        // Ignore some invalid error codes.
205        if (errorType == MEDIA_ERROR_INVALID_CODE) {
206            return;
207        }
208
209        mCurrentState = STATE_ERROR;
210
211        /* Pop up an error dialog so the user knows that
212         * something bad has happened. Only try and pop up the dialog
213         * if we're attached to a window. When we're going away and no
214         * longer have a window, don't bother showing the user an error.
215         *
216         * TODO(qinmin): We need to review whether this Dialog is OK with
217         * the rest of the browser UI elements.
218         */
219        if (getWindowToken() != null) {
220            String message;
221
222            if (errorType == MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
223                message = mPlaybackErrorText;
224            } else {
225                message = mUnknownErrorText;
226            }
227
228            try {
229                new AlertDialog.Builder(getContext())
230                    .setTitle(mErrorTitle)
231                    .setMessage(message)
232                    .setPositiveButton(mErrorButton,
233                            new DialogInterface.OnClickListener() {
234                        @Override
235                        public void onClick(DialogInterface dialog, int whichButton) {
236                            /* Inform that the video is over.
237                             */
238                            onCompletion();
239                        }
240                    })
241                    .setCancelable(false)
242                    .show();
243            } catch (RuntimeException e) {
244                Log.e(TAG, "Cannot show the alert dialog, error message: " + message, e);
245            }
246        }
247    }
248
249    @CalledByNative
250    private void onVideoSizeChanged(int width, int height) {
251        mVideoWidth = width;
252        mVideoHeight = height;
253        // This will trigger the SurfaceView.onMeasure() call.
254        mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight);
255    }
256
257    @CalledByNative
258    protected void onBufferingUpdate(int percent) {
259    }
260
261    @CalledByNative
262    private void onPlaybackComplete() {
263        onCompletion();
264    }
265
266    @CalledByNative
267    protected void onUpdateMediaMetadata(
268            int videoWidth,
269            int videoHeight,
270            int duration,
271            boolean canPause,
272            boolean canSeekBack,
273            boolean canSeekForward) {
274        mDuration = duration;
275        mProgressView.setVisibility(View.GONE);
276        mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED;
277        onVideoSizeChanged(videoWidth, videoHeight);
278    }
279
280    @Override
281    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
282    }
283
284    @Override
285    public void surfaceCreated(SurfaceHolder holder) {
286        mSurfaceHolder = holder;
287        openVideo();
288    }
289
290    @Override
291    public void surfaceDestroyed(SurfaceHolder holder) {
292        if (mNativeContentVideoView != 0) {
293            nativeSetSurface(mNativeContentVideoView, null);
294        }
295        mSurfaceHolder = null;
296        post(mExitFullscreenRunnable);
297    }
298
299    @CalledByNative
300    protected void openVideo() {
301        if (mSurfaceHolder != null) {
302            mCurrentState = STATE_IDLE;
303            if (mNativeContentVideoView != 0) {
304                nativeRequestMediaMetadata(mNativeContentVideoView);
305                nativeSetSurface(mNativeContentVideoView,
306                        mSurfaceHolder.getSurface());
307            }
308        }
309    }
310
311    protected void onCompletion() {
312        mCurrentState = STATE_PLAYBACK_COMPLETED;
313    }
314
315
316    protected boolean isInPlaybackState() {
317        return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE);
318    }
319
320    protected void start() {
321        if (isInPlaybackState()) {
322            if (mNativeContentVideoView != 0) {
323                nativePlay(mNativeContentVideoView);
324            }
325            mCurrentState = STATE_PLAYING;
326        }
327    }
328
329    protected void pause() {
330        if (isInPlaybackState()) {
331            if (isPlaying()) {
332                if (mNativeContentVideoView != 0) {
333                    nativePause(mNativeContentVideoView);
334                }
335                mCurrentState = STATE_PAUSED;
336            }
337        }
338    }
339
340    // cache duration as mDuration for faster access
341    protected int getDuration() {
342        if (isInPlaybackState()) {
343            if (mDuration > 0) {
344                return mDuration;
345            }
346            if (mNativeContentVideoView != 0) {
347                mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView);
348            } else {
349                mDuration = 0;
350            }
351            return mDuration;
352        }
353        mDuration = -1;
354        return mDuration;
355    }
356
357    protected int getCurrentPosition() {
358        if (isInPlaybackState() && mNativeContentVideoView != 0) {
359            return nativeGetCurrentPosition(mNativeContentVideoView);
360        }
361        return 0;
362    }
363
364    protected void seekTo(int msec) {
365        if (mNativeContentVideoView != 0) {
366            nativeSeekTo(mNativeContentVideoView, msec);
367        }
368    }
369
370    public boolean isPlaying() {
371        return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView);
372    }
373
374    @CalledByNative
375    private static ContentVideoView createContentVideoView(
376            Context context, long nativeContentVideoView, ContentVideoViewClient client,
377            boolean legacy) {
378        ThreadUtils.assertOnUiThread();
379        // The context needs be Activity to create the ContentVideoView correctly.
380        if (!(context instanceof Activity)) {
381            Log.w(TAG, "Wrong type of context, can't create fullscreen video");
382            return null;
383        }
384        ContentVideoView videoView = null;
385        if (legacy) {
386            videoView = new ContentVideoViewLegacy(context, nativeContentVideoView, client);
387        } else {
388            videoView = new ContentVideoView(context, nativeContentVideoView, client);
389        }
390
391        if (videoView.getContentVideoViewClient().onShowCustomView(videoView)) {
392            return videoView;
393        }
394        return null;
395    }
396
397    public void removeSurfaceView() {
398        removeView(mVideoSurfaceView);
399        removeView(mProgressView);
400        mVideoSurfaceView = null;
401        mProgressView = null;
402    }
403
404    public void exitFullscreen(boolean relaseMediaPlayer) {
405        destroyContentVideoView(false);
406        if (mNativeContentVideoView != 0) {
407            nativeExitFullscreen(mNativeContentVideoView, relaseMediaPlayer);
408            mNativeContentVideoView = 0;
409        }
410    }
411
412    @CalledByNative
413    private void onExitFullscreen() {
414        exitFullscreen(false);
415    }
416
417    /**
418     * This method shall only be called by native and exitFullscreen,
419     * To exit fullscreen, use exitFullscreen in Java.
420     */
421    @CalledByNative
422    protected void destroyContentVideoView(boolean nativeViewDestroyed) {
423        if (mVideoSurfaceView != null) {
424            removeSurfaceView();
425            setVisibility(View.GONE);
426
427            // To prevent re-entrance, call this after removeSurfaceView.
428            mClient.onDestroyContentVideoView();
429        }
430        if (nativeViewDestroyed) {
431            mNativeContentVideoView = 0;
432        }
433    }
434
435    public static ContentVideoView getContentVideoView() {
436        return nativeGetSingletonJavaContentVideoView();
437    }
438
439    @Override
440    public boolean onKeyUp(int keyCode, KeyEvent event) {
441        if (keyCode == KeyEvent.KEYCODE_BACK) {
442            exitFullscreen(false);
443            return true;
444        }
445        return super.onKeyUp(keyCode, event);
446    }
447
448    @Override
449    public View acquireAnchorView() {
450        View anchorView = new View(getContext());
451        addView(anchorView);
452        return anchorView;
453    }
454
455    @Override
456    public void setAnchorViewPosition(View view, float x, float y, float width, float height) {
457        Log.e(TAG, "setAnchorViewPosition isn't implemented");
458    }
459
460    @Override
461    public void releaseAnchorView(View anchorView) {
462        removeView(anchorView);
463    }
464
465    @CalledByNative
466    private long getNativeViewAndroid() {
467        return mViewAndroid.getNativePointer();
468    }
469
470    private static native ContentVideoView nativeGetSingletonJavaContentVideoView();
471    private native void nativeExitFullscreen(long nativeContentVideoView,
472            boolean relaseMediaPlayer);
473    private native int nativeGetCurrentPosition(long nativeContentVideoView);
474    private native int nativeGetDurationInMilliSeconds(long nativeContentVideoView);
475    private native void nativeRequestMediaMetadata(long nativeContentVideoView);
476    private native int nativeGetVideoWidth(long nativeContentVideoView);
477    private native int nativeGetVideoHeight(long nativeContentVideoView);
478    private native boolean nativeIsPlaying(long nativeContentVideoView);
479    private native void nativePause(long nativeContentVideoView);
480    private native void nativePlay(long nativeContentVideoView);
481    private native void nativeSeekTo(long nativeContentVideoView, int msec);
482    private native void nativeSetSurface(long nativeContentVideoView, Surface surface);
483}
484