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