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