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 5 package org.chromium.content.browser; 6 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; 28 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; 35 36 /** 37 * This class implements accelerated fullscreen video playback using surface view. 38 */ 39 @JNINamespace("content") 40 public class ContentVideoView extends FrameLayout 41 implements SurfaceHolder.Callback, ViewAndroidDelegate { 42 43 private static final String TAG = "ContentVideoView"; 44 45 /* Do not change these values without updating their counterparts 46 * in include/media/mediaplayer.h! 47 */ 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; 56 57 /** 58 * Keep these error codes in sync with the code we defined in 59 * MediaPlayerListener.java. 60 */ 61 public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 2; 62 public static final int MEDIA_ERROR_INVALID_CODE = 3; 63 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; 70 71 private SurfaceHolder mSurfaceHolder; 72 private int mVideoWidth; 73 private int mVideoHeight; 74 private int mDuration; 75 76 // Native pointer to C++ ContentVideoView object. 77 private long mNativeContentVideoView; 78 79 // webkit should have prepared the media 80 private int mCurrentState = STATE_IDLE; 81 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; 88 89 // This view will contain the video. 90 private VideoSurfaceView mVideoSurfaceView; 91 92 // Progress view when the video is loading. 93 private View mProgressView; 94 95 // The ViewAndroid is used to keep screen on during video playback. 96 private ViewAndroid mViewAndroid; 97 98 private final ContentVideoViewClient mClient; 99 100 private boolean mInitialOrientation; 101 private boolean mPossibleAccidentalChange; 102 private boolean mUmaRecorded; 103 private long mOrientationChangedTime; 104 private long mPlaybackStartTime; 105 106 private class VideoSurfaceView extends SurfaceView { 107 VideoSurfaceView(Context context)108 public VideoSurfaceView(Context context) { 109 super(context); 110 } 111 112 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)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. 116 int width = 1; 117 int height = 1; 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; 125 } 126 } 127 if (mUmaRecorded) { 128 // If we have never switched orientation, record the orientation 129 // time. 130 if (mPlaybackStartTime == mOrientationChangedTime) { 131 if (isOrientationPortrait() != mInitialOrientation) { 132 mOrientationChangedTime = System.currentTimeMillis(); 133 } 134 } else { 135 // if user quickly switched the orientation back and force, don't 136 // count it in UMA. 137 if (!mPossibleAccidentalChange && 138 isOrientationPortrait() == mInitialOrientation && 139 System.currentTimeMillis() - mOrientationChangedTime < 5000) { 140 mPossibleAccidentalChange = true; 141 } 142 } 143 } 144 setMeasuredDimension(width, height); 145 } 146 } 147 148 private static class ProgressView extends LinearLayout { 149 150 private final ProgressBar mProgressBar; 151 private final TextView mTextView; 152 ProgressView(Context context, String videoLoadingText)153 public ProgressView(Context context, String videoLoadingText) { 154 super(context); 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); 163 addView(mTextView); 164 } 165 } 166 167 private final Runnable mExitFullscreenRunnable = new Runnable() { 168 @Override 169 public void run() { 170 exitFullscreen(true); 171 } 172 }; 173 ContentVideoView(Context context, long nativeContentVideoView, ContentVideoViewClient client)174 protected ContentVideoView(Context context, long nativeContentVideoView, 175 ContentVideoViewClient client) { 176 super(context); 177 mNativeContentVideoView = nativeContentVideoView; 178 mViewAndroid = new ViewAndroid(new WindowAndroid(context.getApplicationContext()), this); 179 mClient = client; 180 mUmaRecorded = false; 181 mPossibleAccidentalChange = false; 182 initResources(context); 183 mVideoSurfaceView = new VideoSurfaceView(context); 184 showContentVideoView(); 185 setVisibility(View.VISIBLE); 186 } 187 getContentVideoViewClient()188 protected ContentVideoViewClient getContentVideoViewClient() { 189 return mClient; 190 } 191 initResources(Context context)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); 204 } 205 showContentVideoView()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, 211 Gravity.CENTER)); 212 213 mProgressView = mClient.getVideoLoadingProgressView(); 214 if (mProgressView == null) { 215 mProgressView = new ProgressView(getContext(), mVideoLoadingText); 216 } 217 this.addView(mProgressView, new FrameLayout.LayoutParams( 218 ViewGroup.LayoutParams.WRAP_CONTENT, 219 ViewGroup.LayoutParams.WRAP_CONTENT, 220 Gravity.CENTER)); 221 } 222 getSurfaceView()223 protected SurfaceView getSurfaceView() { 224 return mVideoSurfaceView; 225 } 226 227 @CalledByNative onMediaPlayerError(int errorType)228 public void onMediaPlayerError(int errorType) { 229 Log.d(TAG, "OnMediaPlayerError: " + errorType); 230 if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) { 231 return; 232 } 233 234 // Ignore some invalid error codes. 235 if (errorType == MEDIA_ERROR_INVALID_CODE) { 236 return; 237 } 238 239 mCurrentState = STATE_ERROR; 240 241 if (!isActivityContext(getContext())) { 242 Log.w(TAG, "Unable to show alert dialog because it requires an activity context"); 243 return; 244 } 245 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. 250 * 251 * TODO(qinmin): We need to review whether this Dialog is OK with 252 * the rest of the browser UI elements. 253 */ 254 if (getWindowToken() != null) { 255 String message; 256 257 if (errorType == MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) { 258 message = mPlaybackErrorText; 259 } else { 260 message = mUnknownErrorText; 261 } 262 263 try { 264 new AlertDialog.Builder(getContext()) 265 .setTitle(mErrorTitle) 266 .setMessage(message) 267 .setPositiveButton(mErrorButton, 268 new DialogInterface.OnClickListener() { 269 @Override 270 public void onClick(DialogInterface dialog, int whichButton) { 271 /* Inform that the video is over. 272 */ 273 onCompletion(); 274 } 275 }) 276 .setCancelable(false) 277 .show(); 278 } catch (RuntimeException e) { 279 Log.e(TAG, "Cannot show the alert dialog, error message: " + message, e); 280 } 281 } 282 } 283 284 @CalledByNative onVideoSizeChanged(int width, int height)285 private void onVideoSizeChanged(int width, int height) { 286 mVideoWidth = width; 287 mVideoHeight = height; 288 // This will trigger the SurfaceView.onMeasure() call. 289 mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight); 290 } 291 292 @CalledByNative onBufferingUpdate(int percent)293 protected void onBufferingUpdate(int percent) { 294 } 295 296 @CalledByNative onPlaybackComplete()297 private void onPlaybackComplete() { 298 onCompletion(); 299 } 300 301 @CalledByNative onUpdateMediaMetadata( int videoWidth, int videoHeight, int duration, boolean canPause, boolean canSeekBack, boolean canSeekForward)302 protected void onUpdateMediaMetadata( 303 int videoWidth, 304 int videoHeight, 305 int duration, 306 boolean canPause, 307 boolean canSeekBack, 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; 314 try { 315 if (Settings.System.getInt(getContext().getContentResolver(), 316 Settings.System.ACCELEROMETER_ROTATION) == 0) { 317 return; 318 } 319 } catch (Settings.SettingNotFoundException e) { 320 return; 321 } 322 mInitialOrientation = isOrientationPortrait(); 323 mUmaRecorded = true; 324 mPlaybackStartTime = System.currentTimeMillis(); 325 mOrientationChangedTime = mPlaybackStartTime; 326 nativeRecordFullscreenPlayback( 327 mNativeContentVideoView, videoHeight > videoWidth, mInitialOrientation); 328 } 329 330 @Override surfaceChanged(SurfaceHolder holder, int format, int width, int height)331 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 332 } 333 334 @Override surfaceCreated(SurfaceHolder holder)335 public void surfaceCreated(SurfaceHolder holder) { 336 mSurfaceHolder = holder; 337 openVideo(); 338 } 339 340 @Override surfaceDestroyed(SurfaceHolder holder)341 public void surfaceDestroyed(SurfaceHolder holder) { 342 if (mNativeContentVideoView != 0) { 343 nativeSetSurface(mNativeContentVideoView, null); 344 } 345 mSurfaceHolder = null; 346 post(mExitFullscreenRunnable); 347 } 348 349 @CalledByNative openVideo()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()); 357 } 358 } 359 } 360 onCompletion()361 protected void onCompletion() { 362 mCurrentState = STATE_PLAYBACK_COMPLETED; 363 } 364 365 isInPlaybackState()366 protected boolean isInPlaybackState() { 367 return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE); 368 } 369 start()370 protected void start() { 371 if (isInPlaybackState()) { 372 if (mNativeContentVideoView != 0) { 373 nativePlay(mNativeContentVideoView); 374 } 375 mCurrentState = STATE_PLAYING; 376 } 377 } 378 pause()379 protected void pause() { 380 if (isInPlaybackState()) { 381 if (isPlaying()) { 382 if (mNativeContentVideoView != 0) { 383 nativePause(mNativeContentVideoView); 384 } 385 mCurrentState = STATE_PAUSED; 386 } 387 } 388 } 389 390 // cache duration as mDuration for faster access getDuration()391 protected int getDuration() { 392 if (isInPlaybackState()) { 393 if (mDuration > 0) { 394 return mDuration; 395 } 396 if (mNativeContentVideoView != 0) { 397 mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView); 398 } else { 399 mDuration = 0; 400 } 401 return mDuration; 402 } 403 mDuration = -1; 404 return mDuration; 405 } 406 getCurrentPosition()407 protected int getCurrentPosition() { 408 if (isInPlaybackState() && mNativeContentVideoView != 0) { 409 return nativeGetCurrentPosition(mNativeContentVideoView); 410 } 411 return 0; 412 } 413 seekTo(int msec)414 protected void seekTo(int msec) { 415 if (mNativeContentVideoView != 0) { 416 nativeSeekTo(mNativeContentVideoView, msec); 417 } 418 } 419 isPlaying()420 public boolean isPlaying() { 421 return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView); 422 } 423 424 @CalledByNative createContentVideoView( Context context, long nativeContentVideoView, ContentVideoViewClient client)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)) { 430 return videoView; 431 } 432 return null; 433 } 434 isActivityContext(Context context)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); 441 } 442 return context instanceof Activity; 443 } 444 removeSurfaceView()445 public void removeSurfaceView() { 446 removeView(mVideoSurfaceView); 447 removeView(mProgressView); 448 mVideoSurfaceView = null; 449 mProgressView = null; 450 } 451 exitFullscreen(boolean relaseMediaPlayer)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; 462 } 463 nativeRecordExitFullscreenPlayback(mNativeContentVideoView, mInitialOrientation, 464 timeBeforeOrientationChange, timeAfterOrientationChange); 465 } 466 nativeExitFullscreen(mNativeContentVideoView, relaseMediaPlayer); 467 mNativeContentVideoView = 0; 468 } 469 } 470 471 @CalledByNative onExitFullscreen()472 private void onExitFullscreen() { 473 exitFullscreen(false); 474 } 475 476 /** 477 * This method shall only be called by native and exitFullscreen, 478 * To exit fullscreen, use exitFullscreen in Java. 479 */ 480 @CalledByNative destroyContentVideoView(boolean nativeViewDestroyed)481 protected void destroyContentVideoView(boolean nativeViewDestroyed) { 482 if (mVideoSurfaceView != null) { 483 removeSurfaceView(); 484 setVisibility(View.GONE); 485 486 // To prevent re-entrance, call this after removeSurfaceView. 487 mClient.onDestroyContentVideoView(); 488 } 489 if (nativeViewDestroyed) { 490 mNativeContentVideoView = 0; 491 } 492 } 493 getContentVideoView()494 public static ContentVideoView getContentVideoView() { 495 return nativeGetSingletonJavaContentVideoView(); 496 } 497 498 @Override onKeyUp(int keyCode, KeyEvent event)499 public boolean onKeyUp(int keyCode, KeyEvent event) { 500 if (keyCode == KeyEvent.KEYCODE_BACK) { 501 exitFullscreen(false); 502 return true; 503 } 504 return super.onKeyUp(keyCode, event); 505 } 506 507 @Override acquireAnchorView()508 public View acquireAnchorView() { 509 View anchorView = new View(getContext()); 510 addView(anchorView); 511 return anchorView; 512 } 513 514 @Override setAnchorViewPosition(View view, float x, float y, float width, float height)515 public void setAnchorViewPosition(View view, float x, float y, float width, float height) { 516 Log.e(TAG, "setAnchorViewPosition isn't implemented"); 517 } 518 519 @Override releaseAnchorView(View anchorView)520 public void releaseAnchorView(View anchorView) { 521 removeView(anchorView); 522 } 523 524 @CalledByNative getNativeViewAndroid()525 private long getNativeViewAndroid() { 526 return mViewAndroid.getNativePointer(); 527 } 528 isOrientationPortrait()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; 536 } 537 nativeGetSingletonJavaContentVideoView()538 private static native ContentVideoView nativeGetSingletonJavaContentVideoView(); nativeExitFullscreen(long nativeContentVideoView, boolean relaseMediaPlayer)539 private native void nativeExitFullscreen(long nativeContentVideoView, 540 boolean relaseMediaPlayer); nativeGetCurrentPosition(long nativeContentVideoView)541 private native int nativeGetCurrentPosition(long nativeContentVideoView); nativeGetDurationInMilliSeconds(long nativeContentVideoView)542 private native int nativeGetDurationInMilliSeconds(long nativeContentVideoView); nativeRequestMediaMetadata(long nativeContentVideoView)543 private native void nativeRequestMediaMetadata(long nativeContentVideoView); nativeGetVideoWidth(long nativeContentVideoView)544 private native int nativeGetVideoWidth(long nativeContentVideoView); nativeGetVideoHeight(long nativeContentVideoView)545 private native int nativeGetVideoHeight(long nativeContentVideoView); nativeIsPlaying(long nativeContentVideoView)546 private native boolean nativeIsPlaying(long nativeContentVideoView); nativePause(long nativeContentVideoView)547 private native void nativePause(long nativeContentVideoView); nativePlay(long nativeContentVideoView)548 private native void nativePlay(long nativeContentVideoView); nativeSeekTo(long nativeContentVideoView, int msec)549 private native void nativeSeekTo(long nativeContentVideoView, int msec); nativeSetSurface(long nativeContentVideoView, Surface surface)550 private native void nativeSetSurface(long nativeContentVideoView, Surface surface); nativeRecordFullscreenPlayback( long nativeContentVideoView, boolean isVideoPortrait, boolean isOrientationPortrait)551 private native void nativeRecordFullscreenPlayback( 552 long nativeContentVideoView, boolean isVideoPortrait, boolean isOrientationPortrait); nativeRecordExitFullscreenPlayback( long nativeContentVideoView, boolean isOrientationPortrait, long playbackDurationBeforeOrientationChange, long playbackDurationAfterOrientationChange)553 private native void nativeRecordExitFullscreenPlayback( 554 long nativeContentVideoView, boolean isOrientationPortrait, 555 long playbackDurationBeforeOrientationChange, 556 long playbackDurationAfterOrientationChange); 557 } 558