ContentVideoView.java revision 5f1c94371a64b3196d4be9466099bb892df9b88e
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.ContextWrapper; 11import android.content.DialogInterface; 12import android.util.Log; 13import android.view.Gravity; 14import android.view.KeyEvent; 15import android.view.Surface; 16import android.view.SurfaceHolder; 17import android.view.SurfaceView; 18import android.view.View; 19import android.view.ViewGroup; 20import android.widget.FrameLayout; 21import android.widget.LinearLayout; 22import android.widget.ProgressBar; 23import android.widget.TextView; 24 25import org.chromium.base.CalledByNative; 26import org.chromium.base.JNINamespace; 27import org.chromium.base.ThreadUtils; 28import org.chromium.ui.base.ViewAndroid; 29import org.chromium.ui.base.ViewAndroidDelegate; 30import org.chromium.ui.base.WindowAndroid; 31 32/** 33 * This class implements accelerated fullscreen video playback using surface view. 34 */ 35@JNINamespace("content") 36public class ContentVideoView extends FrameLayout 37 implements SurfaceHolder.Callback, ViewAndroidDelegate { 38 39 private static final String TAG = "ContentVideoView"; 40 41 /* Do not change these values without updating their counterparts 42 * in include/media/mediaplayer.h! 43 */ 44 private static final int MEDIA_NOP = 0; // interface test message 45 private static final int MEDIA_PREPARED = 1; 46 private static final int MEDIA_PLAYBACK_COMPLETE = 2; 47 private static final int MEDIA_BUFFERING_UPDATE = 3; 48 private static final int MEDIA_SEEK_COMPLETE = 4; 49 private static final int MEDIA_SET_VIDEO_SIZE = 5; 50 private static final int MEDIA_ERROR = 100; 51 private static final int MEDIA_INFO = 200; 52 53 /** 54 * Keep these error codes in sync with the code we defined in 55 * MediaPlayerListener.java. 56 */ 57 public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 2; 58 public static final int MEDIA_ERROR_INVALID_CODE = 3; 59 60 // all possible internal states 61 private static final int STATE_ERROR = -1; 62 private static final int STATE_IDLE = 0; 63 private static final int STATE_PLAYING = 1; 64 private static final int STATE_PAUSED = 2; 65 private static final int STATE_PLAYBACK_COMPLETED = 3; 66 67 private SurfaceHolder mSurfaceHolder; 68 private int mVideoWidth; 69 private int mVideoHeight; 70 private int mDuration; 71 72 // Native pointer to C++ ContentVideoView object. 73 private long mNativeContentVideoView; 74 75 // webkit should have prepared the media 76 private int mCurrentState = STATE_IDLE; 77 78 // Strings for displaying media player errors 79 private String mPlaybackErrorText; 80 private String mUnknownErrorText; 81 private String mErrorButton; 82 private String mErrorTitle; 83 private String mVideoLoadingText; 84 85 // This view will contain the video. 86 private VideoSurfaceView mVideoSurfaceView; 87 88 // Progress view when the video is loading. 89 private View mProgressView; 90 91 // The ViewAndroid is used to keep screen on during video playback. 92 private ViewAndroid mViewAndroid; 93 94 private final ContentVideoViewClient mClient; 95 96 private class VideoSurfaceView extends SurfaceView { 97 98 public VideoSurfaceView(Context context) { 99 super(context); 100 } 101 102 @Override 103 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 104 // set the default surface view size to (1, 1) so that it won't block 105 // the infobar. (0, 0) is not a valid size for surface view. 106 int width = 1; 107 int height = 1; 108 if (mVideoWidth > 0 && mVideoHeight > 0) { 109 width = getDefaultSize(mVideoWidth, widthMeasureSpec); 110 height = getDefaultSize(mVideoHeight, heightMeasureSpec); 111 if (mVideoWidth * height > width * mVideoHeight) { 112 height = width * mVideoHeight / mVideoWidth; 113 } else if (mVideoWidth * height < width * mVideoHeight) { 114 width = height * mVideoWidth / mVideoHeight; 115 } 116 } 117 setMeasuredDimension(width, height); 118 } 119 } 120 121 private static class ProgressView extends LinearLayout { 122 123 private final ProgressBar mProgressBar; 124 private final TextView mTextView; 125 126 public ProgressView(Context context, String videoLoadingText) { 127 super(context); 128 setOrientation(LinearLayout.VERTICAL); 129 setLayoutParams(new LinearLayout.LayoutParams( 130 LinearLayout.LayoutParams.WRAP_CONTENT, 131 LinearLayout.LayoutParams.WRAP_CONTENT)); 132 mProgressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge); 133 mTextView = new TextView(context); 134 mTextView.setText(videoLoadingText); 135 addView(mProgressBar); 136 addView(mTextView); 137 } 138 } 139 140 private final Runnable mExitFullscreenRunnable = new Runnable() { 141 @Override 142 public void run() { 143 exitFullscreen(true); 144 } 145 }; 146 147 protected ContentVideoView(Context context, long nativeContentVideoView, 148 ContentVideoViewClient client) { 149 super(context); 150 mNativeContentVideoView = nativeContentVideoView; 151 mViewAndroid = new ViewAndroid(new WindowAndroid(context.getApplicationContext()), this); 152 mClient = client; 153 initResources(context); 154 mVideoSurfaceView = new VideoSurfaceView(context); 155 showContentVideoView(); 156 setVisibility(View.VISIBLE); 157 } 158 159 protected ContentVideoViewClient getContentVideoViewClient() { 160 return mClient; 161 } 162 163 private void initResources(Context context) { 164 if (mPlaybackErrorText != null) return; 165 mPlaybackErrorText = context.getString( 166 org.chromium.content.R.string.media_player_error_text_invalid_progressive_playback); 167 mUnknownErrorText = context.getString( 168 org.chromium.content.R.string.media_player_error_text_unknown); 169 mErrorButton = context.getString( 170 org.chromium.content.R.string.media_player_error_button); 171 mErrorTitle = context.getString( 172 org.chromium.content.R.string.media_player_error_title); 173 mVideoLoadingText = context.getString( 174 org.chromium.content.R.string.media_player_loading_video); 175 } 176 177 protected void showContentVideoView() { 178 mVideoSurfaceView.getHolder().addCallback(this); 179 this.addView(mVideoSurfaceView, new FrameLayout.LayoutParams( 180 ViewGroup.LayoutParams.WRAP_CONTENT, 181 ViewGroup.LayoutParams.WRAP_CONTENT, 182 Gravity.CENTER)); 183 184 mProgressView = mClient.getVideoLoadingProgressView(); 185 if (mProgressView == null) { 186 mProgressView = new ProgressView(getContext(), mVideoLoadingText); 187 } 188 this.addView(mProgressView, new FrameLayout.LayoutParams( 189 ViewGroup.LayoutParams.WRAP_CONTENT, 190 ViewGroup.LayoutParams.WRAP_CONTENT, 191 Gravity.CENTER)); 192 } 193 194 protected SurfaceView getSurfaceView() { 195 return mVideoSurfaceView; 196 } 197 198 @CalledByNative 199 public void onMediaPlayerError(int errorType) { 200 Log.d(TAG, "OnMediaPlayerError: " + errorType); 201 if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) { 202 return; 203 } 204 205 // Ignore some invalid error codes. 206 if (errorType == MEDIA_ERROR_INVALID_CODE) { 207 return; 208 } 209 210 mCurrentState = STATE_ERROR; 211 212 /* Pop up an error dialog so the user knows that 213 * something bad has happened. Only try and pop up the dialog 214 * if we're attached to a window. When we're going away and no 215 * longer have a window, don't bother showing the user an error. 216 * 217 * TODO(qinmin): We need to review whether this Dialog is OK with 218 * the rest of the browser UI elements. 219 */ 220 if (getWindowToken() != null) { 221 String message; 222 223 if (errorType == MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) { 224 message = mPlaybackErrorText; 225 } else { 226 message = mUnknownErrorText; 227 } 228 229 try { 230 new AlertDialog.Builder(getContext()) 231 .setTitle(mErrorTitle) 232 .setMessage(message) 233 .setPositiveButton(mErrorButton, 234 new DialogInterface.OnClickListener() { 235 @Override 236 public void onClick(DialogInterface dialog, int whichButton) { 237 /* Inform that the video is over. 238 */ 239 onCompletion(); 240 } 241 }) 242 .setCancelable(false) 243 .show(); 244 } catch (RuntimeException e) { 245 Log.e(TAG, "Cannot show the alert dialog, error message: " + message, e); 246 } 247 } 248 } 249 250 @CalledByNative 251 private void onVideoSizeChanged(int width, int height) { 252 mVideoWidth = width; 253 mVideoHeight = height; 254 // This will trigger the SurfaceView.onMeasure() call. 255 mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight); 256 } 257 258 @CalledByNative 259 protected void onBufferingUpdate(int percent) { 260 } 261 262 @CalledByNative 263 private void onPlaybackComplete() { 264 onCompletion(); 265 } 266 267 @CalledByNative 268 protected void onUpdateMediaMetadata( 269 int videoWidth, 270 int videoHeight, 271 int duration, 272 boolean canPause, 273 boolean canSeekBack, 274 boolean canSeekForward) { 275 mDuration = duration; 276 mProgressView.setVisibility(View.GONE); 277 mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED; 278 onVideoSizeChanged(videoWidth, videoHeight); 279 } 280 281 @Override 282 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 283 } 284 285 @Override 286 public void surfaceCreated(SurfaceHolder holder) { 287 mSurfaceHolder = holder; 288 openVideo(); 289 } 290 291 @Override 292 public void surfaceDestroyed(SurfaceHolder holder) { 293 if (mNativeContentVideoView != 0) { 294 nativeSetSurface(mNativeContentVideoView, null); 295 } 296 mSurfaceHolder = null; 297 post(mExitFullscreenRunnable); 298 } 299 300 @CalledByNative 301 protected void openVideo() { 302 if (mSurfaceHolder != null) { 303 mCurrentState = STATE_IDLE; 304 if (mNativeContentVideoView != 0) { 305 nativeRequestMediaMetadata(mNativeContentVideoView); 306 nativeSetSurface(mNativeContentVideoView, 307 mSurfaceHolder.getSurface()); 308 } 309 } 310 } 311 312 protected void onCompletion() { 313 mCurrentState = STATE_PLAYBACK_COMPLETED; 314 } 315 316 317 protected boolean isInPlaybackState() { 318 return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE); 319 } 320 321 protected void start() { 322 if (isInPlaybackState()) { 323 if (mNativeContentVideoView != 0) { 324 nativePlay(mNativeContentVideoView); 325 } 326 mCurrentState = STATE_PLAYING; 327 } 328 } 329 330 protected void pause() { 331 if (isInPlaybackState()) { 332 if (isPlaying()) { 333 if (mNativeContentVideoView != 0) { 334 nativePause(mNativeContentVideoView); 335 } 336 mCurrentState = STATE_PAUSED; 337 } 338 } 339 } 340 341 // cache duration as mDuration for faster access 342 protected int getDuration() { 343 if (isInPlaybackState()) { 344 if (mDuration > 0) { 345 return mDuration; 346 } 347 if (mNativeContentVideoView != 0) { 348 mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView); 349 } else { 350 mDuration = 0; 351 } 352 return mDuration; 353 } 354 mDuration = -1; 355 return mDuration; 356 } 357 358 protected int getCurrentPosition() { 359 if (isInPlaybackState() && mNativeContentVideoView != 0) { 360 return nativeGetCurrentPosition(mNativeContentVideoView); 361 } 362 return 0; 363 } 364 365 protected void seekTo(int msec) { 366 if (mNativeContentVideoView != 0) { 367 nativeSeekTo(mNativeContentVideoView, msec); 368 } 369 } 370 371 public boolean isPlaying() { 372 return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView); 373 } 374 375 @CalledByNative 376 private static ContentVideoView createContentVideoView( 377 Context context, long nativeContentVideoView, ContentVideoViewClient client, 378 boolean legacy) { 379 ThreadUtils.assertOnUiThread(); 380 // The context needs be Activity to create the ContentVideoView correctly. 381 if (!isActivityContext(context)) { 382 Log.e(TAG, "Wrong type of context, can't create fullscreen video"); 383 return null; 384 } 385 ContentVideoView videoView = null; 386 if (legacy) { 387 videoView = new ContentVideoViewLegacy(context, nativeContentVideoView, client); 388 } else { 389 videoView = new ContentVideoView(context, nativeContentVideoView, client); 390 } 391 392 if (videoView.getContentVideoViewClient().onShowCustomView(videoView)) { 393 return videoView; 394 } 395 return null; 396 } 397 398 private static boolean isActivityContext(Context context) { 399 // Only retrieve the base context if the supplied context is a ContextWrapper but not 400 // an Activity, given that Activity is already a subclass of ContextWrapper. 401 if (context instanceof ContextWrapper && !(context instanceof Activity)) { 402 context = ((ContextWrapper) context).getBaseContext(); 403 } 404 return context instanceof Activity; 405 } 406 407 public void removeSurfaceView() { 408 removeView(mVideoSurfaceView); 409 removeView(mProgressView); 410 mVideoSurfaceView = null; 411 mProgressView = null; 412 } 413 414 public void exitFullscreen(boolean relaseMediaPlayer) { 415 destroyContentVideoView(false); 416 if (mNativeContentVideoView != 0) { 417 nativeExitFullscreen(mNativeContentVideoView, relaseMediaPlayer); 418 mNativeContentVideoView = 0; 419 } 420 } 421 422 @CalledByNative 423 private void onExitFullscreen() { 424 exitFullscreen(false); 425 } 426 427 /** 428 * This method shall only be called by native and exitFullscreen, 429 * To exit fullscreen, use exitFullscreen in Java. 430 */ 431 @CalledByNative 432 protected void destroyContentVideoView(boolean nativeViewDestroyed) { 433 if (mVideoSurfaceView != null) { 434 removeSurfaceView(); 435 setVisibility(View.GONE); 436 437 // To prevent re-entrance, call this after removeSurfaceView. 438 mClient.onDestroyContentVideoView(); 439 } 440 if (nativeViewDestroyed) { 441 mNativeContentVideoView = 0; 442 } 443 } 444 445 public static ContentVideoView getContentVideoView() { 446 return nativeGetSingletonJavaContentVideoView(); 447 } 448 449 @Override 450 public boolean onKeyUp(int keyCode, KeyEvent event) { 451 if (keyCode == KeyEvent.KEYCODE_BACK) { 452 exitFullscreen(false); 453 return true; 454 } 455 return super.onKeyUp(keyCode, event); 456 } 457 458 @Override 459 public View acquireAnchorView() { 460 View anchorView = new View(getContext()); 461 addView(anchorView); 462 return anchorView; 463 } 464 465 @Override 466 public void setAnchorViewPosition(View view, float x, float y, float width, float height) { 467 Log.e(TAG, "setAnchorViewPosition isn't implemented"); 468 } 469 470 @Override 471 public void releaseAnchorView(View anchorView) { 472 removeView(anchorView); 473 } 474 475 @CalledByNative 476 private long getNativeViewAndroid() { 477 return mViewAndroid.getNativePointer(); 478 } 479 480 private static native ContentVideoView nativeGetSingletonJavaContentVideoView(); 481 private native void nativeExitFullscreen(long nativeContentVideoView, 482 boolean relaseMediaPlayer); 483 private native int nativeGetCurrentPosition(long nativeContentVideoView); 484 private native int nativeGetDurationInMilliSeconds(long nativeContentVideoView); 485 private native void nativeRequestMediaMetadata(long nativeContentVideoView); 486 private native int nativeGetVideoWidth(long nativeContentVideoView); 487 private native int nativeGetVideoHeight(long nativeContentVideoView); 488 private native boolean nativeIsPlaying(long nativeContentVideoView); 489 private native void nativePause(long nativeContentVideoView); 490 private native void nativePlay(long nativeContentVideoView); 491 private native void nativeSeekTo(long nativeContentVideoView, int msec); 492 private native void nativeSetSurface(long nativeContentVideoView, Surface surface); 493} 494