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