VideoView.java revision 8a78fd4d9572dff95432fcc4ba0e87563415b728
1/* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.widget; 18 19import android.app.AlertDialog; 20import android.content.Context; 21import android.content.DialogInterface; 22import android.content.Intent; 23import android.content.res.Resources; 24import android.media.AudioManager; 25import android.media.MediaPlayer; 26import android.media.Metadata; 27import android.media.MediaPlayer.OnCompletionListener; 28import android.media.MediaPlayer.OnErrorListener; 29import android.net.Uri; 30import android.util.AttributeSet; 31import android.util.Log; 32import android.view.KeyEvent; 33import android.view.MotionEvent; 34import android.view.SurfaceHolder; 35import android.view.SurfaceView; 36import android.view.View; 37import android.view.accessibility.AccessibilityEvent; 38import android.view.accessibility.AccessibilityNodeInfo; 39import android.widget.MediaController.MediaPlayerControl; 40 41import java.io.IOException; 42import java.util.Map; 43 44/** 45 * Displays a video file. The VideoView class 46 * can load images from various sources (such as resources or content 47 * providers), takes care of computing its measurement from the video so that 48 * it can be used in any layout manager, and provides various display options 49 * such as scaling and tinting. 50 */ 51public class VideoView extends SurfaceView implements MediaPlayerControl { 52 private String TAG = "VideoView"; 53 // settable by the client 54 private Uri mUri; 55 private Map<String, String> mHeaders; 56 private int mDuration; 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_PREPARING = 1; 62 private static final int STATE_PREPARED = 2; 63 private static final int STATE_PLAYING = 3; 64 private static final int STATE_PAUSED = 4; 65 private static final int STATE_PLAYBACK_COMPLETED = 5; 66 67 // mCurrentState is a VideoView object's current state. 68 // mTargetState is the state that a method caller intends to reach. 69 // For instance, regardless the VideoView object's current state, 70 // calling pause() intends to bring the object to a target state 71 // of STATE_PAUSED. 72 private int mCurrentState = STATE_IDLE; 73 private int mTargetState = STATE_IDLE; 74 75 // All the stuff we need for playing and showing a video 76 private SurfaceHolder mSurfaceHolder = null; 77 private MediaPlayer mMediaPlayer = null; 78 private int mVideoWidth; 79 private int mVideoHeight; 80 private int mSurfaceWidth; 81 private int mSurfaceHeight; 82 private MediaController mMediaController; 83 private OnCompletionListener mOnCompletionListener; 84 private MediaPlayer.OnPreparedListener mOnPreparedListener; 85 private int mCurrentBufferPercentage; 86 private OnErrorListener mOnErrorListener; 87 private int mSeekWhenPrepared; // recording the seek position while preparing 88 private boolean mCanPause; 89 private boolean mCanSeekBack; 90 private boolean mCanSeekForward; 91 92 public VideoView(Context context) { 93 super(context); 94 initVideoView(); 95 } 96 97 public VideoView(Context context, AttributeSet attrs) { 98 this(context, attrs, 0); 99 initVideoView(); 100 } 101 102 public VideoView(Context context, AttributeSet attrs, int defStyle) { 103 super(context, attrs, defStyle); 104 initVideoView(); 105 } 106 107 @Override 108 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 109 //Log.i("@@@@", "onMeasure"); 110 int width = getDefaultSize(mVideoWidth, widthMeasureSpec); 111 int height = getDefaultSize(mVideoHeight, heightMeasureSpec); 112 if (mVideoWidth > 0 && mVideoHeight > 0) { 113 if ( mVideoWidth * height > width * mVideoHeight ) { 114 //Log.i("@@@", "image too tall, correcting"); 115 height = width * mVideoHeight / mVideoWidth; 116 } else if ( mVideoWidth * height < width * mVideoHeight ) { 117 //Log.i("@@@", "image too wide, correcting"); 118 width = height * mVideoWidth / mVideoHeight; 119 } else { 120 //Log.i("@@@", "aspect ratio is correct: " + 121 //width+"/"+height+"="+ 122 //mVideoWidth+"/"+mVideoHeight); 123 } 124 } 125 //Log.i("@@@@@@@@@@", "setting size: " + width + 'x' + height); 126 setMeasuredDimension(width, height); 127 } 128 129 @Override 130 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 131 super.onInitializeAccessibilityEvent(event); 132 event.setClassName(VideoView.class.getName()); 133 } 134 135 @Override 136 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 137 super.onInitializeAccessibilityNodeInfo(info); 138 info.setClassName(VideoView.class.getName()); 139 } 140 141 public int resolveAdjustedSize(int desiredSize, int measureSpec) { 142 int result = desiredSize; 143 int specMode = MeasureSpec.getMode(measureSpec); 144 int specSize = MeasureSpec.getSize(measureSpec); 145 146 switch (specMode) { 147 case MeasureSpec.UNSPECIFIED: 148 /* Parent says we can be as big as we want. Just don't be larger 149 * than max size imposed on ourselves. 150 */ 151 result = desiredSize; 152 break; 153 154 case MeasureSpec.AT_MOST: 155 /* Parent says we can be as big as we want, up to specSize. 156 * Don't be larger than specSize, and don't be larger than 157 * the max size imposed on ourselves. 158 */ 159 result = Math.min(desiredSize, specSize); 160 break; 161 162 case MeasureSpec.EXACTLY: 163 // No choice. Do what we are told. 164 result = specSize; 165 break; 166 } 167 return result; 168} 169 170 private void initVideoView() { 171 mVideoWidth = 0; 172 mVideoHeight = 0; 173 getHolder().addCallback(mSHCallback); 174 getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 175 setFocusable(true); 176 setFocusableInTouchMode(true); 177 requestFocus(); 178 mCurrentState = STATE_IDLE; 179 mTargetState = STATE_IDLE; 180 } 181 182 public void setVideoPath(String path) { 183 setVideoURI(Uri.parse(path)); 184 } 185 186 public void setVideoURI(Uri uri) { 187 setVideoURI(uri, null); 188 } 189 190 /** 191 * @hide 192 */ 193 public void setVideoURI(Uri uri, Map<String, String> headers) { 194 mUri = uri; 195 mHeaders = headers; 196 mSeekWhenPrepared = 0; 197 openVideo(); 198 requestLayout(); 199 invalidate(); 200 } 201 202 public void stopPlayback() { 203 if (mMediaPlayer != null) { 204 mMediaPlayer.stop(); 205 mMediaPlayer.release(); 206 mMediaPlayer = null; 207 mCurrentState = STATE_IDLE; 208 mTargetState = STATE_IDLE; 209 } 210 } 211 212 private void openVideo() { 213 if (mUri == null || mSurfaceHolder == null) { 214 // not ready for playback just yet, will try again later 215 return; 216 } 217 // Tell the music playback service to pause 218 // TODO: these constants need to be published somewhere in the framework. 219 Intent i = new Intent("com.android.music.musicservicecommand"); 220 i.putExtra("command", "pause"); 221 mContext.sendBroadcast(i); 222 223 // we shouldn't clear the target state, because somebody might have 224 // called start() previously 225 release(false); 226 try { 227 mMediaPlayer = new MediaPlayer(); 228 mMediaPlayer.setOnPreparedListener(mPreparedListener); 229 mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener); 230 mDuration = -1; 231 mMediaPlayer.setOnCompletionListener(mCompletionListener); 232 mMediaPlayer.setOnErrorListener(mErrorListener); 233 mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener); 234 mCurrentBufferPercentage = 0; 235 mMediaPlayer.setDataSource(mContext, mUri, mHeaders); 236 mMediaPlayer.setDisplay(mSurfaceHolder); 237 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 238 mMediaPlayer.setScreenOnWhilePlaying(true); 239 mMediaPlayer.prepareAsync(); 240 // we don't set the target state here either, but preserve the 241 // target state that was there before. 242 mCurrentState = STATE_PREPARING; 243 attachMediaController(); 244 } catch (IOException ex) { 245 Log.w(TAG, "Unable to open content: " + mUri, ex); 246 mCurrentState = STATE_ERROR; 247 mTargetState = STATE_ERROR; 248 mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); 249 return; 250 } catch (IllegalArgumentException ex) { 251 Log.w(TAG, "Unable to open content: " + mUri, ex); 252 mCurrentState = STATE_ERROR; 253 mTargetState = STATE_ERROR; 254 mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); 255 return; 256 } 257 } 258 259 public void setMediaController(MediaController controller) { 260 if (mMediaController != null) { 261 mMediaController.hide(); 262 } 263 mMediaController = controller; 264 attachMediaController(); 265 } 266 267 private void attachMediaController() { 268 if (mMediaPlayer != null && mMediaController != null) { 269 mMediaController.setMediaPlayer(this); 270 View anchorView = this.getParent() instanceof View ? 271 (View)this.getParent() : this; 272 mMediaController.setAnchorView(anchorView); 273 mMediaController.setEnabled(isInPlaybackState()); 274 } 275 } 276 277 MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener = 278 new MediaPlayer.OnVideoSizeChangedListener() { 279 public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { 280 mVideoWidth = mp.getVideoWidth(); 281 mVideoHeight = mp.getVideoHeight(); 282 if (mVideoWidth != 0 && mVideoHeight != 0) { 283 getHolder().setFixedSize(mVideoWidth, mVideoHeight); 284 } 285 } 286 }; 287 288 MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() { 289 public void onPrepared(MediaPlayer mp) { 290 mCurrentState = STATE_PREPARED; 291 292 // Get the capabilities of the player for this stream 293 Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL, 294 MediaPlayer.BYPASS_METADATA_FILTER); 295 296 if (data != null) { 297 mCanPause = !data.has(Metadata.PAUSE_AVAILABLE) 298 || data.getBoolean(Metadata.PAUSE_AVAILABLE); 299 mCanSeekBack = !data.has(Metadata.SEEK_BACKWARD_AVAILABLE) 300 || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE); 301 mCanSeekForward = !data.has(Metadata.SEEK_FORWARD_AVAILABLE) 302 || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE); 303 } else { 304 mCanPause = mCanSeekBack = mCanSeekForward = true; 305 } 306 307 if (mOnPreparedListener != null) { 308 mOnPreparedListener.onPrepared(mMediaPlayer); 309 } 310 if (mMediaController != null) { 311 mMediaController.setEnabled(true); 312 } 313 mVideoWidth = mp.getVideoWidth(); 314 mVideoHeight = mp.getVideoHeight(); 315 316 int seekToPosition = mSeekWhenPrepared; // mSeekWhenPrepared may be changed after seekTo() call 317 if (seekToPosition != 0) { 318 seekTo(seekToPosition); 319 } 320 if (mVideoWidth != 0 && mVideoHeight != 0) { 321 //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight); 322 getHolder().setFixedSize(mVideoWidth, mVideoHeight); 323 if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) { 324 // We didn't actually change the size (it was already at the size 325 // we need), so we won't get a "surface changed" callback, so 326 // start the video here instead of in the callback. 327 if (mTargetState == STATE_PLAYING) { 328 start(); 329 if (mMediaController != null) { 330 mMediaController.show(); 331 } 332 } else if (!isPlaying() && 333 (seekToPosition != 0 || getCurrentPosition() > 0)) { 334 if (mMediaController != null) { 335 // Show the media controls when we're paused into a video and make 'em stick. 336 mMediaController.show(0); 337 } 338 } 339 } 340 } else { 341 // We don't know the video size yet, but should start anyway. 342 // The video size might be reported to us later. 343 if (mTargetState == STATE_PLAYING) { 344 start(); 345 } 346 } 347 } 348 }; 349 350 private MediaPlayer.OnCompletionListener mCompletionListener = 351 new MediaPlayer.OnCompletionListener() { 352 public void onCompletion(MediaPlayer mp) { 353 mCurrentState = STATE_PLAYBACK_COMPLETED; 354 mTargetState = STATE_PLAYBACK_COMPLETED; 355 if (mMediaController != null) { 356 mMediaController.hide(); 357 } 358 if (mOnCompletionListener != null) { 359 mOnCompletionListener.onCompletion(mMediaPlayer); 360 } 361 } 362 }; 363 364 private MediaPlayer.OnErrorListener mErrorListener = 365 new MediaPlayer.OnErrorListener() { 366 public boolean onError(MediaPlayer mp, int framework_err, int impl_err) { 367 Log.d(TAG, "Error: " + framework_err + "," + impl_err); 368 mCurrentState = STATE_ERROR; 369 mTargetState = STATE_ERROR; 370 if (mMediaController != null) { 371 mMediaController.hide(); 372 } 373 374 /* If an error handler has been supplied, use it and finish. */ 375 if (mOnErrorListener != null) { 376 if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) { 377 return true; 378 } 379 } 380 381 /* Otherwise, pop up an error dialog so the user knows that 382 * something bad has happened. Only try and pop up the dialog 383 * if we're attached to a window. When we're going away and no 384 * longer have a window, don't bother showing the user an error. 385 */ 386 if (getWindowToken() != null) { 387 Resources r = mContext.getResources(); 388 int messageId; 389 390 if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) { 391 messageId = com.android.internal.R.string.VideoView_error_text_invalid_progressive_playback; 392 } else { 393 messageId = com.android.internal.R.string.VideoView_error_text_unknown; 394 } 395 396 new AlertDialog.Builder(mContext) 397 .setMessage(messageId) 398 .setPositiveButton(com.android.internal.R.string.VideoView_error_button, 399 new DialogInterface.OnClickListener() { 400 public void onClick(DialogInterface dialog, int whichButton) { 401 /* If we get here, there is no onError listener, so 402 * at least inform them that the video is over. 403 */ 404 if (mOnCompletionListener != null) { 405 mOnCompletionListener.onCompletion(mMediaPlayer); 406 } 407 } 408 }) 409 .setCancelable(false) 410 .show(); 411 } 412 return true; 413 } 414 }; 415 416 private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener = 417 new MediaPlayer.OnBufferingUpdateListener() { 418 public void onBufferingUpdate(MediaPlayer mp, int percent) { 419 mCurrentBufferPercentage = percent; 420 } 421 }; 422 423 /** 424 * Register a callback to be invoked when the media file 425 * is loaded and ready to go. 426 * 427 * @param l The callback that will be run 428 */ 429 public void setOnPreparedListener(MediaPlayer.OnPreparedListener l) 430 { 431 mOnPreparedListener = l; 432 } 433 434 /** 435 * Register a callback to be invoked when the end of a media file 436 * has been reached during playback. 437 * 438 * @param l The callback that will be run 439 */ 440 public void setOnCompletionListener(OnCompletionListener l) 441 { 442 mOnCompletionListener = l; 443 } 444 445 /** 446 * Register a callback to be invoked when an error occurs 447 * during playback or setup. If no listener is specified, 448 * or if the listener returned false, VideoView will inform 449 * the user of any errors. 450 * 451 * @param l The callback that will be run 452 */ 453 public void setOnErrorListener(OnErrorListener l) 454 { 455 mOnErrorListener = l; 456 } 457 458 SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback() 459 { 460 public void surfaceChanged(SurfaceHolder holder, int format, 461 int w, int h) 462 { 463 mSurfaceWidth = w; 464 mSurfaceHeight = h; 465 boolean isValidState = (mTargetState == STATE_PLAYING); 466 boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h); 467 if (mMediaPlayer != null && isValidState && hasValidSize) { 468 if (mSeekWhenPrepared != 0) { 469 seekTo(mSeekWhenPrepared); 470 } 471 start(); 472 } 473 } 474 475 public void surfaceCreated(SurfaceHolder holder) 476 { 477 mSurfaceHolder = holder; 478 openVideo(); 479 } 480 481 public void surfaceDestroyed(SurfaceHolder holder) 482 { 483 // after we return from this we can't use the surface any more 484 mSurfaceHolder = null; 485 if (mMediaController != null) mMediaController.hide(); 486 release(true); 487 } 488 }; 489 490 /* 491 * release the media player in any state 492 */ 493 private void release(boolean cleartargetstate) { 494 if (mMediaPlayer != null) { 495 mMediaPlayer.reset(); 496 mMediaPlayer.release(); 497 mMediaPlayer = null; 498 mCurrentState = STATE_IDLE; 499 if (cleartargetstate) { 500 mTargetState = STATE_IDLE; 501 } 502 } 503 } 504 505 @Override 506 public boolean onTouchEvent(MotionEvent ev) { 507 if (isInPlaybackState() && mMediaController != null) { 508 toggleMediaControlsVisiblity(); 509 } 510 return false; 511 } 512 513 @Override 514 public boolean onTrackballEvent(MotionEvent ev) { 515 if (isInPlaybackState() && mMediaController != null) { 516 toggleMediaControlsVisiblity(); 517 } 518 return false; 519 } 520 521 @Override 522 public boolean onKeyDown(int keyCode, KeyEvent event) 523 { 524 boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK && 525 keyCode != KeyEvent.KEYCODE_VOLUME_UP && 526 keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && 527 keyCode != KeyEvent.KEYCODE_VOLUME_MUTE && 528 keyCode != KeyEvent.KEYCODE_MENU && 529 keyCode != KeyEvent.KEYCODE_CALL && 530 keyCode != KeyEvent.KEYCODE_ENDCALL; 531 if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) { 532 if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || 533 keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { 534 if (mMediaPlayer.isPlaying()) { 535 pause(); 536 mMediaController.show(); 537 } else { 538 start(); 539 mMediaController.hide(); 540 } 541 return true; 542 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { 543 if (!mMediaPlayer.isPlaying()) { 544 start(); 545 mMediaController.hide(); 546 } 547 return true; 548 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP 549 || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { 550 if (mMediaPlayer.isPlaying()) { 551 pause(); 552 mMediaController.show(); 553 } 554 return true; 555 } else { 556 toggleMediaControlsVisiblity(); 557 } 558 } 559 560 return super.onKeyDown(keyCode, event); 561 } 562 563 private void toggleMediaControlsVisiblity() { 564 if (mMediaController.isShowing()) { 565 mMediaController.hide(); 566 } else { 567 mMediaController.show(); 568 } 569 } 570 571 public void start() { 572 if (isInPlaybackState()) { 573 mMediaPlayer.start(); 574 mCurrentState = STATE_PLAYING; 575 } 576 mTargetState = STATE_PLAYING; 577 } 578 579 public void pause() { 580 if (isInPlaybackState()) { 581 if (mMediaPlayer.isPlaying()) { 582 mMediaPlayer.pause(); 583 mCurrentState = STATE_PAUSED; 584 } 585 } 586 mTargetState = STATE_PAUSED; 587 } 588 589 public void suspend() { 590 release(false); 591 } 592 593 public void resume() { 594 openVideo(); 595 } 596 597 // cache duration as mDuration for faster access 598 public int getDuration() { 599 if (isInPlaybackState()) { 600 if (mDuration > 0) { 601 return mDuration; 602 } 603 mDuration = mMediaPlayer.getDuration(); 604 return mDuration; 605 } 606 mDuration = -1; 607 return mDuration; 608 } 609 610 public int getCurrentPosition() { 611 if (isInPlaybackState()) { 612 return mMediaPlayer.getCurrentPosition(); 613 } 614 return 0; 615 } 616 617 public void seekTo(int msec) { 618 if (isInPlaybackState()) { 619 mMediaPlayer.seekTo(msec); 620 mSeekWhenPrepared = 0; 621 } else { 622 mSeekWhenPrepared = msec; 623 } 624 } 625 626 public boolean isPlaying() { 627 return isInPlaybackState() && mMediaPlayer.isPlaying(); 628 } 629 630 public int getBufferPercentage() { 631 if (mMediaPlayer != null) { 632 return mCurrentBufferPercentage; 633 } 634 return 0; 635 } 636 637 private boolean isInPlaybackState() { 638 return (mMediaPlayer != null && 639 mCurrentState != STATE_ERROR && 640 mCurrentState != STATE_IDLE && 641 mCurrentState != STATE_PREPARING); 642 } 643 644 public boolean canPause() { 645 return mCanPause; 646 } 647 648 public boolean canSeekBackward() { 649 return mCanSeekBack; 650 } 651 652 public boolean canSeekForward() { 653 return mCanSeekForward; 654 } 655} 656