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.DialogInterface; 11 import android.graphics.Color; 12 import android.util.Log; 13 import android.view.Gravity; 14 import android.view.KeyEvent; 15 import android.view.MotionEvent; 16 import android.view.Surface; 17 import android.view.SurfaceHolder; 18 import android.view.SurfaceView; 19 import android.view.View; 20 import android.view.ViewGroup; 21 import android.widget.FrameLayout; 22 import android.widget.LinearLayout; 23 import android.widget.MediaController; 24 import android.widget.ProgressBar; 25 import android.widget.TextView; 26 27 import org.chromium.base.CalledByNative; 28 import org.chromium.base.JNINamespace; 29 import org.chromium.base.ThreadUtils; 30 import org.chromium.ui.base.ViewAndroid; 31 import org.chromium.ui.base.ViewAndroidDelegate; 32 import org.chromium.ui.base.WindowAndroid; 33 34 @JNINamespace("content") 35 public class ContentVideoView extends FrameLayout implements MediaController.MediaPlayerControl, 36 SurfaceHolder.Callback, View.OnTouchListener, View.OnKeyListener, ViewAndroidDelegate { 37 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 mCurrentBufferPercentage; 71 private int mDuration; 72 private MediaController mMediaController; 73 private boolean mCanPause; 74 private boolean mCanSeekBack; 75 private boolean mCanSeekForward; 76 77 // Native pointer to C++ ContentVideoView object. 78 private long mNativeContentVideoView; 79 80 // webkit should have prepared the media 81 private int mCurrentState = STATE_IDLE; 82 83 // Strings for displaying media player errors 84 private String mPlaybackErrorText; 85 private String mUnknownErrorText; 86 private String mErrorButton; 87 private String mErrorTitle; 88 private String mVideoLoadingText; 89 90 // This view will contain the video. 91 private VideoSurfaceView mVideoSurfaceView; 92 93 // Progress view when the video is loading. 94 private View mProgressView; 95 96 // The ViewAndroid is used to keep screen on during video playback. 97 private ViewAndroid mViewAndroid; 98 99 private final ContentVideoViewClient mClient; 100 101 private class VideoSurfaceView extends SurfaceView { 102 VideoSurfaceView(Context context)103 public VideoSurfaceView(Context context) { 104 super(context); 105 } 106 107 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)108 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 109 int width = getDefaultSize(mVideoWidth, widthMeasureSpec); 110 int height = getDefaultSize(mVideoHeight, heightMeasureSpec); 111 if (mVideoWidth > 0 && mVideoHeight > 0) { 112 if (mVideoWidth * height > width * mVideoHeight) { 113 height = width * mVideoHeight / mVideoWidth; 114 } else if (mVideoWidth * height < width * mVideoHeight) { 115 width = height * mVideoWidth / mVideoHeight; 116 } 117 } 118 setMeasuredDimension(width, height); 119 } 120 } 121 122 private static class ProgressView extends LinearLayout { 123 124 private final ProgressBar mProgressBar; 125 private final TextView mTextView; 126 ProgressView(Context context, String videoLoadingText)127 public ProgressView(Context context, String videoLoadingText) { 128 super(context); 129 setOrientation(LinearLayout.VERTICAL); 130 setLayoutParams(new LinearLayout.LayoutParams( 131 LinearLayout.LayoutParams.WRAP_CONTENT, 132 LinearLayout.LayoutParams.WRAP_CONTENT)); 133 mProgressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge); 134 mTextView = new TextView(context); 135 mTextView.setText(videoLoadingText); 136 addView(mProgressBar); 137 addView(mTextView); 138 } 139 } 140 141 private static class FullScreenMediaController extends MediaController { 142 143 View mVideoView; 144 FullScreenMediaController(Context context, View video)145 public FullScreenMediaController(Context context, View video) { 146 super(context); 147 mVideoView = video; 148 } 149 150 @Override show()151 public void show() { 152 super.show(); 153 if (mVideoView != null) { 154 mVideoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); 155 } 156 } 157 158 @Override hide()159 public void hide() { 160 if (mVideoView != null) { 161 mVideoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); 162 } 163 super.hide(); 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 private 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 initResources(context); 181 mCurrentBufferPercentage = 0; 182 mVideoSurfaceView = new VideoSurfaceView(context); 183 setBackgroundColor(Color.BLACK); 184 showContentVideoView(); 185 setVisibility(View.VISIBLE); 186 mClient.onShowCustomView(this); 187 } 188 initResources(Context context)189 private void initResources(Context context) { 190 if (mPlaybackErrorText != null) return; 191 mPlaybackErrorText = context.getString( 192 org.chromium.content.R.string.media_player_error_text_invalid_progressive_playback); 193 mUnknownErrorText = context.getString( 194 org.chromium.content.R.string.media_player_error_text_unknown); 195 mErrorButton = context.getString( 196 org.chromium.content.R.string.media_player_error_button); 197 mErrorTitle = context.getString( 198 org.chromium.content.R.string.media_player_error_title); 199 mVideoLoadingText = context.getString( 200 org.chromium.content.R.string.media_player_loading_video); 201 } 202 showContentVideoView()203 private void showContentVideoView() { 204 FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( 205 ViewGroup.LayoutParams.MATCH_PARENT, 206 ViewGroup.LayoutParams.MATCH_PARENT, 207 Gravity.CENTER); 208 this.addView(mVideoSurfaceView, layoutParams); 209 View progressView = mClient.getVideoLoadingProgressView(); 210 if (progressView != null) { 211 mProgressView = progressView; 212 } else { 213 mProgressView = new ProgressView(getContext(), mVideoLoadingText); 214 } 215 this.addView(mProgressView, new FrameLayout.LayoutParams( 216 ViewGroup.LayoutParams.WRAP_CONTENT, 217 ViewGroup.LayoutParams.WRAP_CONTENT, 218 Gravity.CENTER)); 219 mVideoSurfaceView.setZOrderOnTop(true); 220 mVideoSurfaceView.setOnKeyListener(this); 221 mVideoSurfaceView.setOnTouchListener(this); 222 mVideoSurfaceView.getHolder().addCallback(this); 223 mVideoSurfaceView.setFocusable(true); 224 mVideoSurfaceView.setFocusableInTouchMode(true); 225 mVideoSurfaceView.requestFocus(); 226 } 227 228 @CalledByNative onMediaPlayerError(int errorType)229 public void onMediaPlayerError(int errorType) { 230 Log.d(TAG, "OnMediaPlayerError: " + errorType); 231 if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) { 232 return; 233 } 234 235 // Ignore some invalid error codes. 236 if (errorType == MEDIA_ERROR_INVALID_CODE) { 237 return; 238 } 239 240 mCurrentState = STATE_ERROR; 241 if (mMediaController != null) { 242 mMediaController.hide(); 243 } 244 245 /* Pop up an error dialog so the user knows that 246 * something bad has happened. Only try and pop up the dialog 247 * if we're attached to a window. When we're going away and no 248 * longer have a window, don't bother showing the user an error. 249 * 250 * TODO(qinmin): We need to review whether this Dialog is OK with 251 * the rest of the browser UI elements. 252 */ 253 if (getWindowToken() != null) { 254 String message; 255 256 if (errorType == MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) { 257 message = mPlaybackErrorText; 258 } else { 259 message = mUnknownErrorText; 260 } 261 262 try { 263 new AlertDialog.Builder(getContext()) 264 .setTitle(mErrorTitle) 265 .setMessage(message) 266 .setPositiveButton(mErrorButton, 267 new DialogInterface.OnClickListener() { 268 @Override 269 public void onClick(DialogInterface dialog, int whichButton) { 270 /* Inform that the video is over. 271 */ 272 onCompletion(); 273 } 274 }) 275 .setCancelable(false) 276 .show(); 277 } catch (RuntimeException e) { 278 Log.e(TAG, "Cannot show the alert dialog, error message: " + message, e); 279 } 280 } 281 } 282 283 @CalledByNative onVideoSizeChanged(int width, int height)284 private void onVideoSizeChanged(int width, int height) { 285 mVideoWidth = width; 286 mVideoHeight = height; 287 // This will trigger the SurfaceView.onMeasure() call. 288 mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight); 289 } 290 291 @CalledByNative onBufferingUpdate(int percent)292 private void onBufferingUpdate(int percent) { 293 mCurrentBufferPercentage = 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 private void onUpdateMediaMetadata( 303 int videoWidth, 304 int videoHeight, 305 int duration, 306 boolean canPause, 307 boolean canSeekBack, 308 boolean canSeekForward) { 309 mProgressView.setVisibility(View.GONE); 310 mDuration = duration; 311 mCanPause = canPause; 312 mCanSeekBack = canSeekBack; 313 mCanSeekForward = canSeekForward; 314 mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED; 315 if (mMediaController != null) { 316 mMediaController.setEnabled(true); 317 // If paused , should show the controller for ever. 318 if (isPlaying()) 319 mMediaController.show(); 320 else 321 mMediaController.show(0); 322 } 323 324 onVideoSizeChanged(videoWidth, videoHeight); 325 } 326 327 @Override surfaceChanged(SurfaceHolder holder, int format, int width, int height)328 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 329 mVideoSurfaceView.setFocusable(true); 330 mVideoSurfaceView.setFocusableInTouchMode(true); 331 if (isInPlaybackState() && mMediaController != null) { 332 mMediaController.show(); 333 } 334 } 335 336 @Override surfaceCreated(SurfaceHolder holder)337 public void surfaceCreated(SurfaceHolder holder) { 338 mSurfaceHolder = holder; 339 openVideo(); 340 } 341 342 @Override surfaceDestroyed(SurfaceHolder holder)343 public void surfaceDestroyed(SurfaceHolder holder) { 344 if (mNativeContentVideoView != 0) { 345 nativeSetSurface(mNativeContentVideoView, null); 346 } 347 mSurfaceHolder = null; 348 post(mExitFullscreenRunnable); 349 } 350 setMediaController(MediaController controller)351 private void setMediaController(MediaController controller) { 352 if (mMediaController != null) { 353 mMediaController.hide(); 354 } 355 mMediaController = controller; 356 attachMediaController(); 357 } 358 attachMediaController()359 private void attachMediaController() { 360 if (mMediaController != null) { 361 mMediaController.setMediaPlayer(this); 362 mMediaController.setAnchorView(mVideoSurfaceView); 363 mMediaController.setEnabled(false); 364 } 365 } 366 367 @CalledByNative openVideo()368 private void openVideo() { 369 if (mSurfaceHolder != null) { 370 mCurrentState = STATE_IDLE; 371 mCurrentBufferPercentage = 0; 372 setMediaController(new FullScreenMediaController(getContext(), this)); 373 if (mNativeContentVideoView != 0) { 374 nativeUpdateMediaMetadata(mNativeContentVideoView); 375 nativeSetSurface(mNativeContentVideoView, 376 mSurfaceHolder.getSurface()); 377 } 378 } 379 } 380 onCompletion()381 private void onCompletion() { 382 mCurrentState = STATE_PLAYBACK_COMPLETED; 383 if (mMediaController != null) { 384 mMediaController.hide(); 385 } 386 } 387 388 @Override onTouch(View v, MotionEvent event)389 public boolean onTouch(View v, MotionEvent event) { 390 if (isInPlaybackState() && mMediaController != null && 391 event.getAction() == MotionEvent.ACTION_DOWN) { 392 toggleMediaControlsVisiblity(); 393 } 394 return true; 395 } 396 397 @Override onTrackballEvent(MotionEvent ev)398 public boolean onTrackballEvent(MotionEvent ev) { 399 if (isInPlaybackState() && mMediaController != null) { 400 toggleMediaControlsVisiblity(); 401 } 402 return false; 403 } 404 405 @Override onKey(View v, int keyCode, KeyEvent event)406 public boolean onKey(View v, int keyCode, KeyEvent event) { 407 boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK && 408 keyCode != KeyEvent.KEYCODE_VOLUME_UP && 409 keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && 410 keyCode != KeyEvent.KEYCODE_VOLUME_MUTE && 411 keyCode != KeyEvent.KEYCODE_CALL && 412 keyCode != KeyEvent.KEYCODE_MENU && 413 keyCode != KeyEvent.KEYCODE_SEARCH && 414 keyCode != KeyEvent.KEYCODE_ENDCALL; 415 if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) { 416 if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || 417 keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { 418 if (isPlaying()) { 419 pause(); 420 mMediaController.show(); 421 } else { 422 start(); 423 mMediaController.hide(); 424 } 425 return true; 426 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { 427 if (!isPlaying()) { 428 start(); 429 mMediaController.hide(); 430 } 431 return true; 432 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP 433 || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { 434 if (isPlaying()) { 435 pause(); 436 mMediaController.show(); 437 } 438 return true; 439 } else { 440 toggleMediaControlsVisiblity(); 441 } 442 } else if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { 443 exitFullscreen(false); 444 return true; 445 } else if (keyCode == KeyEvent.KEYCODE_MENU || keyCode == KeyEvent.KEYCODE_SEARCH) { 446 return true; 447 } 448 return super.onKeyDown(keyCode, event); 449 } 450 toggleMediaControlsVisiblity()451 private void toggleMediaControlsVisiblity() { 452 if (mMediaController.isShowing()) { 453 mMediaController.hide(); 454 } else { 455 mMediaController.show(); 456 } 457 } 458 isInPlaybackState()459 private boolean isInPlaybackState() { 460 return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE); 461 } 462 463 @Override start()464 public void start() { 465 if (isInPlaybackState()) { 466 if (mNativeContentVideoView != 0) { 467 nativePlay(mNativeContentVideoView); 468 } 469 mCurrentState = STATE_PLAYING; 470 } 471 } 472 473 @Override pause()474 public void pause() { 475 if (isInPlaybackState()) { 476 if (isPlaying()) { 477 if (mNativeContentVideoView != 0) { 478 nativePause(mNativeContentVideoView); 479 } 480 mCurrentState = STATE_PAUSED; 481 } 482 } 483 } 484 485 // cache duration as mDuration for faster access 486 @Override getDuration()487 public int getDuration() { 488 if (isInPlaybackState()) { 489 if (mDuration > 0) { 490 return mDuration; 491 } 492 if (mNativeContentVideoView != 0) { 493 mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView); 494 } else { 495 mDuration = 0; 496 } 497 return mDuration; 498 } 499 mDuration = -1; 500 return mDuration; 501 } 502 503 @Override getCurrentPosition()504 public int getCurrentPosition() { 505 if (isInPlaybackState() && mNativeContentVideoView != 0) { 506 return nativeGetCurrentPosition(mNativeContentVideoView); 507 } 508 return 0; 509 } 510 511 @Override seekTo(int msec)512 public void seekTo(int msec) { 513 if (mNativeContentVideoView != 0) { 514 nativeSeekTo(mNativeContentVideoView, msec); 515 } 516 } 517 518 @Override isPlaying()519 public boolean isPlaying() { 520 return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView); 521 } 522 523 @Override getBufferPercentage()524 public int getBufferPercentage() { 525 return mCurrentBufferPercentage; 526 } 527 528 @Override canPause()529 public boolean canPause() { 530 return mCanPause; 531 } 532 533 @Override canSeekBackward()534 public boolean canSeekBackward() { 535 return mCanSeekBack; 536 } 537 538 @Override canSeekForward()539 public boolean canSeekForward() { 540 return mCanSeekForward; 541 } 542 543 @Override getAudioSessionId()544 public int getAudioSessionId() { 545 return 0; 546 } 547 548 @CalledByNative createContentVideoView( Context context, long nativeContentVideoView, ContentVideoViewClient client)549 private static ContentVideoView createContentVideoView( 550 Context context, long nativeContentVideoView, ContentVideoViewClient client) { 551 ThreadUtils.assertOnUiThread(); 552 // The context needs be Activity to create the ContentVideoView correctly. 553 if (!(context instanceof Activity)) { 554 Log.w(TAG, "Wrong type of context, can't create fullscreen video"); 555 return null; 556 } 557 return new ContentVideoView(context, nativeContentVideoView, client); 558 } 559 removeMediaController()560 private void removeMediaController() { 561 if (mMediaController != null) { 562 mMediaController.setEnabled(false); 563 mMediaController.hide(); 564 mMediaController = null; 565 } 566 } 567 removeSurfaceView()568 public void removeSurfaceView() { 569 removeView(mVideoSurfaceView); 570 removeView(mProgressView); 571 mVideoSurfaceView = null; 572 mProgressView = null; 573 } 574 exitFullscreen(boolean relaseMediaPlayer)575 public void exitFullscreen(boolean relaseMediaPlayer) { 576 destroyContentVideoView(false); 577 if (mNativeContentVideoView != 0) { 578 nativeExitFullscreen(mNativeContentVideoView, relaseMediaPlayer); 579 mNativeContentVideoView = 0; 580 } 581 } 582 583 /** 584 * This method shall only be called by native and exitFullscreen, 585 * To exit fullscreen, use exitFullscreen in Java. 586 */ 587 @CalledByNative destroyContentVideoView(boolean nativeViewDestroyed)588 private void destroyContentVideoView(boolean nativeViewDestroyed) { 589 if (mVideoSurfaceView != null) { 590 removeMediaController(); 591 removeSurfaceView(); 592 setVisibility(View.GONE); 593 594 // To prevent re-entrance, call this after removeSurfaceView. 595 mClient.onDestroyContentVideoView(); 596 } 597 if (nativeViewDestroyed) { 598 mNativeContentVideoView = 0; 599 } 600 } 601 getContentVideoView()602 public static ContentVideoView getContentVideoView() { 603 return nativeGetSingletonJavaContentVideoView(); 604 } 605 606 @Override onTouchEvent(MotionEvent ev)607 public boolean onTouchEvent(MotionEvent ev) { 608 return true; 609 } 610 611 @Override onKeyDown(int keyCode, KeyEvent event)612 public boolean onKeyDown(int keyCode, KeyEvent event) { 613 if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { 614 exitFullscreen(false); 615 return true; 616 } 617 return super.onKeyDown(keyCode, event); 618 } 619 620 @Override acquireAnchorView()621 public View acquireAnchorView() { 622 View anchorView = new View(getContext()); 623 addView(anchorView); 624 return anchorView; 625 } 626 627 @Override setAnchorViewPosition(View view, float x, float y, float width, float height)628 public void setAnchorViewPosition(View view, float x, float y, float width, float height) { 629 Log.e(TAG, "setAnchorViewPosition isn't implemented"); 630 } 631 632 @Override releaseAnchorView(View anchorView)633 public void releaseAnchorView(View anchorView) { 634 removeView(anchorView); 635 } 636 637 @CalledByNative getNativeViewAndroid()638 private long getNativeViewAndroid() { 639 return mViewAndroid.getNativePointer(); 640 } 641 nativeGetSingletonJavaContentVideoView()642 private static native ContentVideoView nativeGetSingletonJavaContentVideoView(); nativeExitFullscreen(long nativeContentVideoView, boolean relaseMediaPlayer)643 private native void nativeExitFullscreen(long nativeContentVideoView, 644 boolean relaseMediaPlayer); nativeGetCurrentPosition(long nativeContentVideoView)645 private native int nativeGetCurrentPosition(long nativeContentVideoView); nativeGetDurationInMilliSeconds(long nativeContentVideoView)646 private native int nativeGetDurationInMilliSeconds(long nativeContentVideoView); nativeUpdateMediaMetadata(long nativeContentVideoView)647 private native void nativeUpdateMediaMetadata(long nativeContentVideoView); nativeGetVideoWidth(long nativeContentVideoView)648 private native int nativeGetVideoWidth(long nativeContentVideoView); nativeGetVideoHeight(long nativeContentVideoView)649 private native int nativeGetVideoHeight(long nativeContentVideoView); nativeIsPlaying(long nativeContentVideoView)650 private native boolean nativeIsPlaying(long nativeContentVideoView); nativePause(long nativeContentVideoView)651 private native void nativePause(long nativeContentVideoView); nativePlay(long nativeContentVideoView)652 private native void nativePlay(long nativeContentVideoView); nativeSeekTo(long nativeContentVideoView, int msec)653 private native void nativeSeekTo(long nativeContentVideoView, int msec); nativeSetSurface(long nativeContentVideoView, Surface surface)654 private native void nativeSetSurface(long nativeContentVideoView, Surface surface); 655 } 656