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