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