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