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.
5 package org.chromium.content.browser;
7 import android.app.Activity;
8 import android.app.AlertDialog;
9 import android.content.Context;
10 import android.content.ContextWrapper;
11 import android.content.DialogInterface;
12 import android.graphics.Point;
13 import android.provider.Settings;
14 import android.util.Log;
15 import android.view.Display;
16 import android.view.Gravity;
17 import android.view.KeyEvent;
18 import android.view.Surface;
19 import android.view.SurfaceHolder;
20 import android.view.SurfaceView;
21 import android.view.View;
22 import android.view.ViewGroup;
23 import android.view.WindowManager;
24 import android.widget.FrameLayout;
25 import android.widget.LinearLayout;
26 import android.widget.ProgressBar;
27 import android.widget.TextView;
29 import org.chromium.base.CalledByNative;
30 import org.chromium.base.JNINamespace;
31 import org.chromium.base.ThreadUtils;
34 * This class implements accelerated fullscreen video playback using surface view.
36 @JNINamespace("content")
37 public class ContentVideoView extends FrameLayout
38 implements SurfaceHolder.Callback {
40 private static final String TAG = "ContentVideoView";
42 /* Do not change these values without updating their counterparts
43 * in include/media/mediaplayer.h!
45 private static final int MEDIA_NOP = 0; // interface test message
46 private static final int MEDIA_PREPARED = 1;
47 private static final int MEDIA_PLAYBACK_COMPLETE = 2;
48 private static final int MEDIA_BUFFERING_UPDATE = 3;
49 private static final int MEDIA_SEEK_COMPLETE = 4;
50 private static final int MEDIA_SET_VIDEO_SIZE = 5;
51 private static final int MEDIA_ERROR = 100;
52 private static final int MEDIA_INFO = 200;
55 * Keep these error codes in sync with the code we defined in
56 * MediaPlayerListener.java.
58 public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 2;
59 public static final int MEDIA_ERROR_INVALID_CODE = 3;
61 // all possible internal states
62 private static final int STATE_ERROR = -1;
63 private static final int STATE_IDLE = 0;
64 private static final int STATE_PLAYING = 1;
65 private static final int STATE_PAUSED = 2;
66 private static final int STATE_PLAYBACK_COMPLETED = 3;
68 private SurfaceHolder mSurfaceHolder;
69 private int mVideoWidth;
70 private int mVideoHeight;
71 private int mDuration;
73 // Native pointer to C++ ContentVideoView object.
74 private long mNativeContentVideoView;
76 // webkit should have prepared the media
77 private int mCurrentState = STATE_IDLE;
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;
86 // This view will contain the video.
87 private VideoSurfaceView mVideoSurfaceView;
89 // Progress view when the video is loading.
90 private View mProgressView;
92 private final ContentVideoViewClient mClient;
94 private boolean mInitialOrientation;
95 private boolean mPossibleAccidentalChange;
96 private boolean mUmaRecorded;
97 private long mOrientationChangedTime;
98 private long mPlaybackStartTime;
100 private class VideoSurfaceView extends SurfaceView {
102 public VideoSurfaceView(Context context) {
107 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
108 // set the default surface view size to (1, 1) so that it won't block
109 // the infobar. (0, 0) is not a valid size for surface view.
112 if (mVideoWidth > 0 && mVideoHeight > 0) {
113 width = getDefaultSize(mVideoWidth, widthMeasureSpec);
114 height = getDefaultSize(mVideoHeight, heightMeasureSpec);
115 if (mVideoWidth * height > width * mVideoHeight) {
116 height = width * mVideoHeight / mVideoWidth;
117 } else if (mVideoWidth * height < width * mVideoHeight) {
118 width = height * mVideoWidth / mVideoHeight;
122 // If we have never switched orientation, record the orientation
124 if (mPlaybackStartTime == mOrientationChangedTime) {
125 if (isOrientationPortrait() != mInitialOrientation) {
126 mOrientationChangedTime = System.currentTimeMillis();
129 // if user quickly switched the orientation back and force, don't
131 if (!mPossibleAccidentalChange
132 && isOrientationPortrait() == mInitialOrientation
133 && System.currentTimeMillis() - mOrientationChangedTime < 5000) {
134 mPossibleAccidentalChange = true;
138 setMeasuredDimension(width, height);
142 private static class ProgressView extends LinearLayout {
144 private final ProgressBar mProgressBar;
145 private final TextView mTextView;
147 public ProgressView(Context context, String videoLoadingText) {
149 setOrientation(LinearLayout.VERTICAL);
150 setLayoutParams(new LinearLayout.LayoutParams(
151 LinearLayout.LayoutParams.WRAP_CONTENT,
152 LinearLayout.LayoutParams.WRAP_CONTENT));
153 mProgressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge);
154 mTextView = new TextView(context);
155 mTextView.setText(videoLoadingText);
156 addView(mProgressBar);
161 private final Runnable mExitFullscreenRunnable = new Runnable() {
164 exitFullscreen(true);
168 protected ContentVideoView(Context context, long nativeContentVideoView,
169 ContentVideoViewClient client) {
171 mNativeContentVideoView = nativeContentVideoView;
173 mUmaRecorded = false;
174 mPossibleAccidentalChange = false;
175 initResources(context);
176 mVideoSurfaceView = new VideoSurfaceView(context);
177 showContentVideoView();
178 setVisibility(View.VISIBLE);
181 protected ContentVideoViewClient getContentVideoViewClient() {
185 private void initResources(Context context) {
186 if (mPlaybackErrorText != null) return;
187 mPlaybackErrorText = context.getString(
188 org.chromium.content.R.string.media_player_error_text_invalid_progressive_playback);
189 mUnknownErrorText = context.getString(
190 org.chromium.content.R.string.media_player_error_text_unknown);
191 mErrorButton = context.getString(
192 org.chromium.content.R.string.media_player_error_button);
193 mErrorTitle = context.getString(
194 org.chromium.content.R.string.media_player_error_title);
195 mVideoLoadingText = context.getString(
196 org.chromium.content.R.string.media_player_loading_video);
199 protected void showContentVideoView() {
200 mVideoSurfaceView.getHolder().addCallback(this);
201 this.addView(mVideoSurfaceView, new FrameLayout.LayoutParams(
202 ViewGroup.LayoutParams.WRAP_CONTENT,
203 ViewGroup.LayoutParams.WRAP_CONTENT,
206 mProgressView = mClient.getVideoLoadingProgressView();
207 if (mProgressView == null) {
208 mProgressView = new ProgressView(getContext(), mVideoLoadingText);
210 this.addView(mProgressView, new FrameLayout.LayoutParams(
211 ViewGroup.LayoutParams.WRAP_CONTENT,
212 ViewGroup.LayoutParams.WRAP_CONTENT,
216 protected SurfaceView getSurfaceView() {
217 return mVideoSurfaceView;
221 public void onMediaPlayerError(int errorType) {
222 Log.d(TAG, "OnMediaPlayerError: " + errorType);
223 if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) {
227 // Ignore some invalid error codes.
228 if (errorType == MEDIA_ERROR_INVALID_CODE) {
232 mCurrentState = STATE_ERROR;
234 if (!isActivityContext(getContext())) {
235 Log.w(TAG, "Unable to show alert dialog because it requires an activity context");
239 /* Pop up an error dialog so the user knows that
240 * something bad has happened. Only try and pop up the dialog
241 * if we're attached to a window. When we're going away and no
242 * longer have a window, don't bother showing the user an error.
244 * TODO(qinmin): We need to review whether this Dialog is OK with
245 * the rest of the browser UI elements.
247 if (getWindowToken() != null) {
250 if (errorType == MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
251 message = mPlaybackErrorText;
253 message = mUnknownErrorText;
257 new AlertDialog.Builder(getContext())
258 .setTitle(mErrorTitle)
260 .setPositiveButton(mErrorButton,
261 new DialogInterface.OnClickListener() {
263 public void onClick(DialogInterface dialog, int whichButton) {
264 // Inform that the video is over.
268 .setCancelable(false)
270 } catch (RuntimeException e) {
271 Log.e(TAG, "Cannot show the alert dialog, error message: " + message, e);
277 private void onVideoSizeChanged(int width, int height) {
279 mVideoHeight = height;
280 // This will trigger the SurfaceView.onMeasure() call.
281 mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight);
285 protected void onBufferingUpdate(int percent) {
289 private void onPlaybackComplete() {
294 protected void onUpdateMediaMetadata(
300 boolean canSeekForward) {
301 mDuration = duration;
302 mProgressView.setVisibility(View.GONE);
303 mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED;
304 onVideoSizeChanged(videoWidth, videoHeight);
305 if (mUmaRecorded) return;
307 if (Settings.System.getInt(getContext().getContentResolver(),
308 Settings.System.ACCELEROMETER_ROTATION) == 0) {
311 } catch (Settings.SettingNotFoundException e) {
314 mInitialOrientation = isOrientationPortrait();
316 mPlaybackStartTime = System.currentTimeMillis();
317 mOrientationChangedTime = mPlaybackStartTime;
318 nativeRecordFullscreenPlayback(
319 mNativeContentVideoView, videoHeight > videoWidth, mInitialOrientation);
323 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
327 public void surfaceCreated(SurfaceHolder holder) {
328 mSurfaceHolder = holder;
333 public void surfaceDestroyed(SurfaceHolder holder) {
334 if (mNativeContentVideoView != 0) {
335 nativeSetSurface(mNativeContentVideoView, null);
337 mSurfaceHolder = null;
338 post(mExitFullscreenRunnable);
342 protected void openVideo() {
343 if (mSurfaceHolder != null) {
344 mCurrentState = STATE_IDLE;
345 if (mNativeContentVideoView != 0) {
346 nativeRequestMediaMetadata(mNativeContentVideoView);
347 nativeSetSurface(mNativeContentVideoView,
348 mSurfaceHolder.getSurface());
353 protected void onCompletion() {
354 mCurrentState = STATE_PLAYBACK_COMPLETED;
358 protected boolean isInPlaybackState() {
359 return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE);
362 protected void start() {
363 if (isInPlaybackState()) {
364 if (mNativeContentVideoView != 0) {
365 nativePlay(mNativeContentVideoView);
367 mCurrentState = STATE_PLAYING;
371 protected void pause() {
372 if (isInPlaybackState()) {
374 if (mNativeContentVideoView != 0) {
375 nativePause(mNativeContentVideoView);
377 mCurrentState = STATE_PAUSED;
382 // cache duration as mDuration for faster access
383 protected int getDuration() {
384 if (isInPlaybackState()) {
388 if (mNativeContentVideoView != 0) {
389 mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView);
399 protected int getCurrentPosition() {
400 if (isInPlaybackState() && mNativeContentVideoView != 0) {
401 return nativeGetCurrentPosition(mNativeContentVideoView);
406 protected void seekTo(int msec) {
407 if (mNativeContentVideoView != 0) {
408 nativeSeekTo(mNativeContentVideoView, msec);
412 public boolean isPlaying() {
413 return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView);
417 private static ContentVideoView createContentVideoView(
418 ContentViewCore contentViewCore, long nativeContentVideoView) {
419 ThreadUtils.assertOnUiThread();
420 Context context = contentViewCore.getContext();
421 ContentVideoViewClient client = contentViewCore.getContentVideoViewClient();
422 ContentVideoView videoView = new ContentVideoView(context, nativeContentVideoView, client);
423 client.enterFullscreenVideo(videoView);
427 private static boolean isActivityContext(Context context) {
428 // Only retrieve the base context if the supplied context is a ContextWrapper but not
429 // an Activity, given that Activity is already a subclass of ContextWrapper.
430 if (context instanceof ContextWrapper && !(context instanceof Activity)) {
431 context = ((ContextWrapper) context).getBaseContext();
432 return isActivityContext(context);
434 return context instanceof Activity;
437 public void removeSurfaceView() {
438 removeView(mVideoSurfaceView);
439 removeView(mProgressView);
440 mVideoSurfaceView = null;
441 mProgressView = null;
444 public void exitFullscreen(boolean relaseMediaPlayer) {
445 destroyContentVideoView(false);
446 if (mNativeContentVideoView != 0) {
447 if (mUmaRecorded && !mPossibleAccidentalChange) {
448 long currentTime = System.currentTimeMillis();
449 long timeBeforeOrientationChange = mOrientationChangedTime - mPlaybackStartTime;
450 long timeAfterOrientationChange = currentTime - mOrientationChangedTime;
451 if (timeBeforeOrientationChange == 0) {
452 timeBeforeOrientationChange = timeAfterOrientationChange;
453 timeAfterOrientationChange = 0;
455 nativeRecordExitFullscreenPlayback(mNativeContentVideoView, mInitialOrientation,
456 timeBeforeOrientationChange, timeAfterOrientationChange);
458 nativeExitFullscreen(mNativeContentVideoView, relaseMediaPlayer);
459 mNativeContentVideoView = 0;
464 private void onExitFullscreen() {
465 exitFullscreen(false);
469 * This method shall only be called by native and exitFullscreen,
470 * To exit fullscreen, use exitFullscreen in Java.
473 protected void destroyContentVideoView(boolean nativeViewDestroyed) {
474 if (mVideoSurfaceView != null) {
476 setVisibility(View.GONE);
478 // To prevent re-entrance, call this after removeSurfaceView.
479 mClient.exitFullscreenVideo();
481 if (nativeViewDestroyed) {
482 mNativeContentVideoView = 0;
486 public static ContentVideoView getContentVideoView() {
487 return nativeGetSingletonJavaContentVideoView();
491 public boolean onKeyUp(int keyCode, KeyEvent event) {
492 if (keyCode == KeyEvent.KEYCODE_BACK) {
493 exitFullscreen(false);
496 return super.onKeyUp(keyCode, event);
499 private boolean isOrientationPortrait() {
500 Context context = getContext();
501 WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
502 Display display = manager.getDefaultDisplay();
503 Point outputSize = new Point(0, 0);
504 display.getSize(outputSize);
505 return outputSize.x <= outputSize.y;
508 private static native ContentVideoView nativeGetSingletonJavaContentVideoView();
509 private native void nativeExitFullscreen(long nativeContentVideoView,
510 boolean relaseMediaPlayer);
511 private native int nativeGetCurrentPosition(long nativeContentVideoView);
512 private native int nativeGetDurationInMilliSeconds(long nativeContentVideoView);
513 private native void nativeRequestMediaMetadata(long nativeContentVideoView);
514 private native int nativeGetVideoWidth(long nativeContentVideoView);
515 private native int nativeGetVideoHeight(long nativeContentVideoView);
516 private native boolean nativeIsPlaying(long nativeContentVideoView);
517 private native void nativePause(long nativeContentVideoView);
518 private native void nativePlay(long nativeContentVideoView);
519 private native void nativeSeekTo(long nativeContentVideoView, int msec);
520 private native void nativeSetSurface(long nativeContentVideoView, Surface surface);
521 private native void nativeRecordFullscreenPlayback(
522 long nativeContentVideoView, boolean isVideoPortrait, boolean isOrientationPortrait);
523 private native void nativeRecordExitFullscreenPlayback(
524 long nativeContentVideoView, boolean isOrientationPortrait,
525 long playbackDurationBeforeOrientationChange,
526 long playbackDurationAfterOrientationChange);