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.util.Log; 13 import android.view.Gravity; 14 import android.view.KeyEvent; 15 import android.view.Surface; 16 import android.view.SurfaceHolder; 17 import android.view.SurfaceView; 18 import android.view.View; 19 import android.view.ViewGroup; 20 import android.widget.FrameLayout; 21 import android.widget.LinearLayout; 22 import android.widget.ProgressBar; 23 import android.widget.TextView; 24 25 import org.chromium.base.CalledByNative; 26 import org.chromium.base.JNINamespace; 27 import org.chromium.base.ThreadUtils; 28 import org.chromium.ui.base.ViewAndroid; 29 import org.chromium.ui.base.ViewAndroidDelegate; 30 import org.chromium.ui.base.WindowAndroid; 31 32 /** 33 * This class implements accelerated fullscreen video playback using surface view. 34 */ 35 @JNINamespace("content") 36 public 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 VideoSurfaceView(Context context)98 public VideoSurfaceView(Context context) { 99 super(context); 100 } 101 102 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 ProgressView(Context context, String videoLoadingText)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 ContentVideoView(Context context, long nativeContentVideoView, ContentVideoViewClient client)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 getContentVideoViewClient()159 protected ContentVideoViewClient getContentVideoViewClient() { 160 return mClient; 161 } 162 initResources(Context context)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 showContentVideoView()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 getSurfaceView()194 protected SurfaceView getSurfaceView() { 195 return mVideoSurfaceView; 196 } 197 198 @CalledByNative onMediaPlayerError(int errorType)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 onVideoSizeChanged(int width, int height)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 onBufferingUpdate(int percent)259 protected void onBufferingUpdate(int percent) { 260 } 261 262 @CalledByNative onPlaybackComplete()263 private void onPlaybackComplete() { 264 onCompletion(); 265 } 266 267 @CalledByNative onUpdateMediaMetadata( int videoWidth, int videoHeight, int duration, boolean canPause, boolean canSeekBack, boolean canSeekForward)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 surfaceChanged(SurfaceHolder holder, int format, int width, int height)282 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 283 } 284 285 @Override surfaceCreated(SurfaceHolder holder)286 public void surfaceCreated(SurfaceHolder holder) { 287 mSurfaceHolder = holder; 288 openVideo(); 289 } 290 291 @Override surfaceDestroyed(SurfaceHolder holder)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 openVideo()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 onCompletion()312 protected void onCompletion() { 313 mCurrentState = STATE_PLAYBACK_COMPLETED; 314 } 315 316 isInPlaybackState()317 protected boolean isInPlaybackState() { 318 return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE); 319 } 320 start()321 protected void start() { 322 if (isInPlaybackState()) { 323 if (mNativeContentVideoView != 0) { 324 nativePlay(mNativeContentVideoView); 325 } 326 mCurrentState = STATE_PLAYING; 327 } 328 } 329 pause()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 getDuration()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 getCurrentPosition()358 protected int getCurrentPosition() { 359 if (isInPlaybackState() && mNativeContentVideoView != 0) { 360 return nativeGetCurrentPosition(mNativeContentVideoView); 361 } 362 return 0; 363 } 364 seekTo(int msec)365 protected void seekTo(int msec) { 366 if (mNativeContentVideoView != 0) { 367 nativeSeekTo(mNativeContentVideoView, msec); 368 } 369 } 370 isPlaying()371 public boolean isPlaying() { 372 return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView); 373 } 374 375 @CalledByNative createContentVideoView( Context context, long nativeContentVideoView, ContentVideoViewClient client, boolean legacy)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 isActivityContext(Context context)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 removeSurfaceView()407 public void removeSurfaceView() { 408 removeView(mVideoSurfaceView); 409 removeView(mProgressView); 410 mVideoSurfaceView = null; 411 mProgressView = null; 412 } 413 exitFullscreen(boolean relaseMediaPlayer)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 onExitFullscreen()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 destroyContentVideoView(boolean nativeViewDestroyed)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 getContentVideoView()445 public static ContentVideoView getContentVideoView() { 446 return nativeGetSingletonJavaContentVideoView(); 447 } 448 449 @Override onKeyUp(int keyCode, KeyEvent event)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 acquireAnchorView()459 public View acquireAnchorView() { 460 View anchorView = new View(getContext()); 461 addView(anchorView); 462 return anchorView; 463 } 464 465 @Override setAnchorViewPosition(View view, float x, float y, float width, float height)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 releaseAnchorView(View anchorView)471 public void releaseAnchorView(View anchorView) { 472 removeView(anchorView); 473 } 474 475 @CalledByNative getNativeViewAndroid()476 private long getNativeViewAndroid() { 477 return mViewAndroid.getNativePointer(); 478 } 479 nativeGetSingletonJavaContentVideoView()480 private static native ContentVideoView nativeGetSingletonJavaContentVideoView(); nativeExitFullscreen(long nativeContentVideoView, boolean relaseMediaPlayer)481 private native void nativeExitFullscreen(long nativeContentVideoView, 482 boolean relaseMediaPlayer); nativeGetCurrentPosition(long nativeContentVideoView)483 private native int nativeGetCurrentPosition(long nativeContentVideoView); nativeGetDurationInMilliSeconds(long nativeContentVideoView)484 private native int nativeGetDurationInMilliSeconds(long nativeContentVideoView); nativeRequestMediaMetadata(long nativeContentVideoView)485 private native void nativeRequestMediaMetadata(long nativeContentVideoView); nativeGetVideoWidth(long nativeContentVideoView)486 private native int nativeGetVideoWidth(long nativeContentVideoView); nativeGetVideoHeight(long nativeContentVideoView)487 private native int nativeGetVideoHeight(long nativeContentVideoView); nativeIsPlaying(long nativeContentVideoView)488 private native boolean nativeIsPlaying(long nativeContentVideoView); nativePause(long nativeContentVideoView)489 private native void nativePause(long nativeContentVideoView); nativePlay(long nativeContentVideoView)490 private native void nativePlay(long nativeContentVideoView); nativeSeekTo(long nativeContentVideoView, int msec)491 private native void nativeSeekTo(long nativeContentVideoView, int msec); nativeSetSurface(long nativeContentVideoView, Surface surface)492 private native void nativeSetSurface(long nativeContentVideoView, Surface surface); 493 } 494