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