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 108 public VideoSurfaceView(Context context) { 109 super(context); 110 } 111 112 @Override 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 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 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 188 protected ContentVideoViewClient getContentVideoViewClient() { 189 return mClient; 190 } 191 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 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 223 protected SurfaceView getSurfaceView() { 224 return mVideoSurfaceView; 225 } 226 227 @CalledByNative 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 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 293 protected void onBufferingUpdate(int percent) { 294 } 295 296 @CalledByNative 297 private void onPlaybackComplete() { 298 onCompletion(); 299 } 300 301 @CalledByNative 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 331 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 332 } 333 334 @Override 335 public void surfaceCreated(SurfaceHolder holder) { 336 mSurfaceHolder = holder; 337 openVideo(); 338 } 339 340 @Override 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 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 361 protected void onCompletion() { 362 mCurrentState = STATE_PLAYBACK_COMPLETED; 363 } 364 365 366 protected boolean isInPlaybackState() { 367 return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE); 368 } 369 370 protected void start() { 371 if (isInPlaybackState()) { 372 if (mNativeContentVideoView != 0) { 373 nativePlay(mNativeContentVideoView); 374 } 375 mCurrentState = STATE_PLAYING; 376 } 377 } 378 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 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 407 protected int getCurrentPosition() { 408 if (isInPlaybackState() && mNativeContentVideoView != 0) { 409 return nativeGetCurrentPosition(mNativeContentVideoView); 410 } 411 return 0; 412 } 413 414 protected void seekTo(int msec) { 415 if (mNativeContentVideoView != 0) { 416 nativeSeekTo(mNativeContentVideoView, msec); 417 } 418 } 419 420 public boolean isPlaying() { 421 return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView); 422 } 423 424 @CalledByNative 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 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 445 public void removeSurfaceView() { 446 removeView(mVideoSurfaceView); 447 removeView(mProgressView); 448 mVideoSurfaceView = null; 449 mProgressView = null; 450 } 451 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 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 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 494 public static ContentVideoView getContentVideoView() { 495 return nativeGetSingletonJavaContentVideoView(); 496 } 497 498 @Override 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 508 public View acquireAnchorView() { 509 View anchorView = new View(getContext()); 510 addView(anchorView); 511 return anchorView; 512 } 513 514 @Override 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 520 public void releaseAnchorView(View anchorView) { 521 removeView(anchorView); 522 } 523 524 @CalledByNative 525 private long getNativeViewAndroid() { 526 return mViewAndroid.getNativePointer(); 527 } 528 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 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); 557 } 558