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