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;
32 import org.chromium.ui.base.ViewAndroid;
33 import org.chromium.ui.base.ViewAndroidDelegate;
34 import org.chromium.ui.base.WindowAndroid;
37 * This class implements accelerated fullscreen video playback using surface view.
39 @JNINamespace("content")
40 public class ContentVideoView extends FrameLayout
41 implements SurfaceHolder.Callback, ViewAndroidDelegate {
43 private static final String TAG = "ContentVideoView";
45 /* Do not change these values without updating their counterparts
46 * in include/media/mediaplayer.h!
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;
58 * Keep these error codes in sync with the code we defined in
59 * MediaPlayerListener.java.
61 public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 2;
62 public static final int MEDIA_ERROR_INVALID_CODE = 3;
64 // all possible internal states
65 private static final int STATE_ERROR = -1;
66 private static final int STATE_IDLE = 0;
67 private static final int STATE_PLAYING = 1;
68 private static final int STATE_PAUSED = 2;
69 private static final int STATE_PLAYBACK_COMPLETED = 3;
71 private SurfaceHolder mSurfaceHolder;
72 private int mVideoWidth;
73 private int mVideoHeight;
74 private int mDuration;
76 // Native pointer to C++ ContentVideoView object.
77 private long mNativeContentVideoView;
79 // webkit should have prepared the media
80 private int mCurrentState = STATE_IDLE;
82 // Strings for displaying media player errors
83 private String mPlaybackErrorText;
84 private String mUnknownErrorText;
85 private String mErrorButton;
86 private String mErrorTitle;
87 private String mVideoLoadingText;
89 // This view will contain the video.
90 private VideoSurfaceView mVideoSurfaceView;
92 // Progress view when the video is loading.
93 private View mProgressView;
95 // The ViewAndroid is used to keep screen on during video playback.
96 private ViewAndroid mViewAndroid;
98 private final ContentVideoViewClient mClient;
100 private boolean mInitialOrientation;
101 private boolean mPossibleAccidentalChange;
102 private boolean mUmaRecorded;
103 private long mOrientationChangedTime;
104 private long mPlaybackStartTime;
106 private class VideoSurfaceView extends SurfaceView {
108 public VideoSurfaceView(Context context) {
113 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
114 // set the default surface view size to (1, 1) so that it won't block
115 // the infobar. (0, 0) is not a valid size for surface view.
118 if (mVideoWidth > 0 && mVideoHeight > 0) {
119 width = getDefaultSize(mVideoWidth, widthMeasureSpec);
120 height = getDefaultSize(mVideoHeight, heightMeasureSpec);
121 if (mVideoWidth * height > width * mVideoHeight) {
122 height = width * mVideoHeight / mVideoWidth;
123 } else if (mVideoWidth * height < width * mVideoHeight) {
124 width = height * mVideoWidth / mVideoHeight;
128 // If we have never switched orientation, record the orientation
130 if (mPlaybackStartTime == mOrientationChangedTime) {
131 if (isOrientationPortrait() != mInitialOrientation) {
132 mOrientationChangedTime = System.currentTimeMillis();
135 // if user quickly switched the orientation back and force, don't
137 if (!mPossibleAccidentalChange &&
138 isOrientationPortrait() == mInitialOrientation &&
139 System.currentTimeMillis() - mOrientationChangedTime < 5000) {
140 mPossibleAccidentalChange = true;
144 setMeasuredDimension(width, height);
148 private static class ProgressView extends LinearLayout {
150 private final ProgressBar mProgressBar;
151 private final TextView mTextView;
153 public ProgressView(Context context, String videoLoadingText) {
155 setOrientation(LinearLayout.VERTICAL);
156 setLayoutParams(new LinearLayout.LayoutParams(
157 LinearLayout.LayoutParams.WRAP_CONTENT,
158 LinearLayout.LayoutParams.WRAP_CONTENT));
159 mProgressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge);
160 mTextView = new TextView(context);
161 mTextView.setText(videoLoadingText);
162 addView(mProgressBar);
167 private final Runnable mExitFullscreenRunnable = new Runnable() {
170 exitFullscreen(true);
174 protected ContentVideoView(Context context, long nativeContentVideoView,
175 ContentVideoViewClient client) {
177 mNativeContentVideoView = nativeContentVideoView;
178 mViewAndroid = new ViewAndroid(new WindowAndroid(context.getApplicationContext()), this);
180 mUmaRecorded = false;
181 mPossibleAccidentalChange = false;
182 initResources(context);
183 mVideoSurfaceView = new VideoSurfaceView(context);
184 showContentVideoView();
185 setVisibility(View.VISIBLE);
188 protected ContentVideoViewClient getContentVideoViewClient() {
192 private void initResources(Context context) {
193 if (mPlaybackErrorText != null) return;
194 mPlaybackErrorText = context.getString(
195 org.chromium.content.R.string.media_player_error_text_invalid_progressive_playback);
196 mUnknownErrorText = context.getString(
197 org.chromium.content.R.string.media_player_error_text_unknown);
198 mErrorButton = context.getString(
199 org.chromium.content.R.string.media_player_error_button);
200 mErrorTitle = context.getString(
201 org.chromium.content.R.string.media_player_error_title);
202 mVideoLoadingText = context.getString(
203 org.chromium.content.R.string.media_player_loading_video);
206 protected void showContentVideoView() {
207 mVideoSurfaceView.getHolder().addCallback(this);
208 this.addView(mVideoSurfaceView, new FrameLayout.LayoutParams(
209 ViewGroup.LayoutParams.WRAP_CONTENT,
210 ViewGroup.LayoutParams.WRAP_CONTENT,
213 mProgressView = mClient.getVideoLoadingProgressView();
214 if (mProgressView == null) {
215 mProgressView = new ProgressView(getContext(), mVideoLoadingText);
217 this.addView(mProgressView, new FrameLayout.LayoutParams(
218 ViewGroup.LayoutParams.WRAP_CONTENT,
219 ViewGroup.LayoutParams.WRAP_CONTENT,
223 protected SurfaceView getSurfaceView() {
224 return mVideoSurfaceView;
228 public void onMediaPlayerError(int errorType) {
229 Log.d(TAG, "OnMediaPlayerError: " + errorType);
230 if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) {
234 // Ignore some invalid error codes.
235 if (errorType == MEDIA_ERROR_INVALID_CODE) {
239 mCurrentState = STATE_ERROR;
241 if (!isActivityContext(getContext())) {
242 Log.w(TAG, "Unable to show alert dialog because it requires an activity context");
246 /* Pop up an error dialog so the user knows that
247 * something bad has happened. Only try and pop up the dialog
248 * if we're attached to a window. When we're going away and no
249 * longer have a window, don't bother showing the user an error.
251 * TODO(qinmin): We need to review whether this Dialog is OK with
252 * the rest of the browser UI elements.
254 if (getWindowToken() != null) {
257 if (errorType == MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
258 message = mPlaybackErrorText;
260 message = mUnknownErrorText;
264 new AlertDialog.Builder(getContext())
265 .setTitle(mErrorTitle)
267 .setPositiveButton(mErrorButton,
268 new DialogInterface.OnClickListener() {
270 public void onClick(DialogInterface dialog, int whichButton) {
271 /* Inform that the video is over.
276 .setCancelable(false)
278 } catch (RuntimeException e) {
279 Log.e(TAG, "Cannot show the alert dialog, error message: " + message, e);
285 private void onVideoSizeChanged(int width, int height) {
287 mVideoHeight = height;
288 // This will trigger the SurfaceView.onMeasure() call.
289 mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight);
293 protected void onBufferingUpdate(int percent) {
297 private void onPlaybackComplete() {
302 protected void onUpdateMediaMetadata(
308 boolean canSeekForward) {
309 mDuration = duration;
310 mProgressView.setVisibility(View.GONE);
311 mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED;
312 onVideoSizeChanged(videoWidth, videoHeight);
313 if (mUmaRecorded) return;
315 if (Settings.System.getInt(getContext().getContentResolver(),
316 Settings.System.ACCELEROMETER_ROTATION) == 0) {
319 } catch (Settings.SettingNotFoundException e) {
322 mInitialOrientation = isOrientationPortrait();
324 mPlaybackStartTime = System.currentTimeMillis();
325 mOrientationChangedTime = mPlaybackStartTime;
326 nativeRecordFullscreenPlayback(
327 mNativeContentVideoView, videoHeight > videoWidth, mInitialOrientation);
331 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
335 public void surfaceCreated(SurfaceHolder holder) {
336 mSurfaceHolder = holder;
341 public void surfaceDestroyed(SurfaceHolder holder) {
342 if (mNativeContentVideoView != 0) {
343 nativeSetSurface(mNativeContentVideoView, null);
345 mSurfaceHolder = null;
346 post(mExitFullscreenRunnable);
350 protected void openVideo() {
351 if (mSurfaceHolder != null) {
352 mCurrentState = STATE_IDLE;
353 if (mNativeContentVideoView != 0) {
354 nativeRequestMediaMetadata(mNativeContentVideoView);
355 nativeSetSurface(mNativeContentVideoView,
356 mSurfaceHolder.getSurface());
361 protected void onCompletion() {
362 mCurrentState = STATE_PLAYBACK_COMPLETED;
366 protected boolean isInPlaybackState() {
367 return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE);
370 protected void start() {
371 if (isInPlaybackState()) {
372 if (mNativeContentVideoView != 0) {
373 nativePlay(mNativeContentVideoView);
375 mCurrentState = STATE_PLAYING;
379 protected void pause() {
380 if (isInPlaybackState()) {
382 if (mNativeContentVideoView != 0) {
383 nativePause(mNativeContentVideoView);
385 mCurrentState = STATE_PAUSED;
390 // cache duration as mDuration for faster access
391 protected int getDuration() {
392 if (isInPlaybackState()) {
396 if (mNativeContentVideoView != 0) {
397 mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView);
407 protected int getCurrentPosition() {
408 if (isInPlaybackState() && mNativeContentVideoView != 0) {
409 return nativeGetCurrentPosition(mNativeContentVideoView);
414 protected void seekTo(int msec) {
415 if (mNativeContentVideoView != 0) {
416 nativeSeekTo(mNativeContentVideoView, msec);
420 public boolean isPlaying() {
421 return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView);
425 private static ContentVideoView createContentVideoView(
426 Context context, long nativeContentVideoView, ContentVideoViewClient client) {
427 ThreadUtils.assertOnUiThread();
428 ContentVideoView videoView = new ContentVideoView(context, nativeContentVideoView, client);
429 if (videoView.getContentVideoViewClient().onShowCustomView(videoView)) {
435 private static boolean isActivityContext(Context context) {
436 // Only retrieve the base context if the supplied context is a ContextWrapper but not
437 // an Activity, given that Activity is already a subclass of ContextWrapper.
438 if (context instanceof ContextWrapper && !(context instanceof Activity)) {
439 context = ((ContextWrapper) context).getBaseContext();
440 return isActivityContext(context);
442 return context instanceof Activity;
445 public void removeSurfaceView() {
446 removeView(mVideoSurfaceView);
447 removeView(mProgressView);
448 mVideoSurfaceView = null;
449 mProgressView = null;
452 public void exitFullscreen(boolean relaseMediaPlayer) {
453 destroyContentVideoView(false);
454 if (mNativeContentVideoView != 0) {
455 if (mUmaRecorded && !mPossibleAccidentalChange) {
456 long currentTime = System.currentTimeMillis();
457 long timeBeforeOrientationChange = mOrientationChangedTime - mPlaybackStartTime;
458 long timeAfterOrientationChange = currentTime - mOrientationChangedTime;
459 if (timeBeforeOrientationChange == 0) {
460 timeBeforeOrientationChange = timeAfterOrientationChange;
461 timeAfterOrientationChange = 0;
463 nativeRecordExitFullscreenPlayback(mNativeContentVideoView, mInitialOrientation,
464 timeBeforeOrientationChange, timeAfterOrientationChange);
466 nativeExitFullscreen(mNativeContentVideoView, relaseMediaPlayer);
467 mNativeContentVideoView = 0;
472 private void onExitFullscreen() {
473 exitFullscreen(false);
477 * This method shall only be called by native and exitFullscreen,
478 * To exit fullscreen, use exitFullscreen in Java.
481 protected void destroyContentVideoView(boolean nativeViewDestroyed) {
482 if (mVideoSurfaceView != null) {
484 setVisibility(View.GONE);
486 // To prevent re-entrance, call this after removeSurfaceView.
487 mClient.onDestroyContentVideoView();
489 if (nativeViewDestroyed) {
490 mNativeContentVideoView = 0;
494 public static ContentVideoView getContentVideoView() {
495 return nativeGetSingletonJavaContentVideoView();
499 public boolean onKeyUp(int keyCode, KeyEvent event) {
500 if (keyCode == KeyEvent.KEYCODE_BACK) {
501 exitFullscreen(false);
504 return super.onKeyUp(keyCode, event);
508 public View acquireAnchorView() {
509 View anchorView = new View(getContext());
515 public void setAnchorViewPosition(View view, float x, float y, float width, float height) {
516 Log.e(TAG, "setAnchorViewPosition isn't implemented");
520 public void releaseAnchorView(View anchorView) {
521 removeView(anchorView);
525 private long getNativeViewAndroid() {
526 return mViewAndroid.getNativePointer();
529 private boolean isOrientationPortrait() {
530 Context context = getContext();
531 WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
532 Display display = manager.getDefaultDisplay();
533 Point outputSize = new Point(0, 0);
534 display.getSize(outputSize);
535 return outputSize.x <= outputSize.y;
538 private static native ContentVideoView nativeGetSingletonJavaContentVideoView();
539 private native void nativeExitFullscreen(long nativeContentVideoView,
540 boolean relaseMediaPlayer);
541 private native int nativeGetCurrentPosition(long nativeContentVideoView);
542 private native int nativeGetDurationInMilliSeconds(long nativeContentVideoView);
543 private native void nativeRequestMediaMetadata(long nativeContentVideoView);
544 private native int nativeGetVideoWidth(long nativeContentVideoView);
545 private native int nativeGetVideoHeight(long nativeContentVideoView);
546 private native boolean nativeIsPlaying(long nativeContentVideoView);
547 private native void nativePause(long nativeContentVideoView);
548 private native void nativePlay(long nativeContentVideoView);
549 private native void nativeSeekTo(long nativeContentVideoView, int msec);
550 private native void nativeSetSurface(long nativeContentVideoView, Surface surface);
551 private native void nativeRecordFullscreenPlayback(
552 long nativeContentVideoView, boolean isVideoPortrait, boolean isOrientationPortrait);
553 private native void nativeRecordExitFullscreenPlayback(
554 long nativeContentVideoView, boolean isOrientationPortrait,
555 long playbackDurationBeforeOrientationChange,
556 long playbackDurationAfterOrientationChange);