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.res.Resources; 23 import android.graphics.Canvas; 24 import android.media.AudioManager; 25 import android.media.Cea708CaptionRenderer; 26 import android.media.ClosedCaptionRenderer; 27 import android.media.MediaFormat; 28 import android.media.MediaPlayer; 29 import android.media.MediaPlayer.OnCompletionListener; 30 import android.media.MediaPlayer.OnErrorListener; 31 import android.media.MediaPlayer.OnInfoListener; 32 import android.media.Metadata; 33 import android.media.SubtitleController; 34 import android.media.SubtitleTrack.RenderingWidget; 35 import android.media.TtmlRenderer; 36 import android.media.WebVttRenderer; 37 import android.net.Uri; 38 import android.os.Looper; 39 import android.util.AttributeSet; 40 import android.util.Log; 41 import android.util.Pair; 42 import android.view.KeyEvent; 43 import android.view.MotionEvent; 44 import android.view.SurfaceHolder; 45 import android.view.SurfaceView; 46 import android.view.View; 47 import android.widget.MediaController.MediaPlayerControl; 48 49 import java.io.IOException; 50 import java.io.InputStream; 51 import java.util.Map; 52 import java.util.Vector; 53 54 /** 55 * Displays a video file. The VideoView class 56 * can load images from various sources (such as resources or content 57 * providers), takes care of computing its measurement from the video so that 58 * it can be used in any layout manager, and provides various display options 59 * such as scaling and tinting.<p> 60 * 61 * <em>Note: VideoView does not retain its full state when going into the 62 * background.</em> In particular, it does not restore the current play state, 63 * play position, selected tracks, or any subtitle tracks added via 64 * {@link #addSubtitleSource addSubtitleSource()}. Applications should 65 * save and restore these on their own in 66 * {@link android.app.Activity#onSaveInstanceState} and 67 * {@link android.app.Activity#onRestoreInstanceState}.<p> 68 * Also note that the audio session id (from {@link #getAudioSessionId}) may 69 * change from its previously returned value when the VideoView is restored. 70 */ 71 public class VideoView extends SurfaceView 72 implements MediaPlayerControl, SubtitleController.Anchor { 73 private String TAG = "VideoView"; 74 // settable by the client 75 private Uri mUri; 76 private Map<String, String> mHeaders; 77 78 // all possible internal states 79 private static final int STATE_ERROR = -1; 80 private static final int STATE_IDLE = 0; 81 private static final int STATE_PREPARING = 1; 82 private static final int STATE_PREPARED = 2; 83 private static final int STATE_PLAYING = 3; 84 private static final int STATE_PAUSED = 4; 85 private static final int STATE_PLAYBACK_COMPLETED = 5; 86 87 // mCurrentState is a VideoView object's current state. 88 // mTargetState is the state that a method caller intends to reach. 89 // For instance, regardless the VideoView object's current state, 90 // calling pause() intends to bring the object to a target state 91 // of STATE_PAUSED. 92 private int mCurrentState = STATE_IDLE; 93 private int mTargetState = STATE_IDLE; 94 95 // All the stuff we need for playing and showing a video 96 private SurfaceHolder mSurfaceHolder = null; 97 private MediaPlayer mMediaPlayer = null; 98 private int mAudioSession; 99 private int mVideoWidth; 100 private int mVideoHeight; 101 private int mSurfaceWidth; 102 private int mSurfaceHeight; 103 private MediaController mMediaController; 104 private OnCompletionListener mOnCompletionListener; 105 private MediaPlayer.OnPreparedListener mOnPreparedListener; 106 private int mCurrentBufferPercentage; 107 private OnErrorListener mOnErrorListener; 108 private OnInfoListener mOnInfoListener; 109 private int mSeekWhenPrepared; // recording the seek position while preparing 110 private boolean mCanPause; 111 private boolean mCanSeekBack; 112 private boolean mCanSeekForward; 113 114 /** Subtitle rendering widget overlaid on top of the video. */ 115 private RenderingWidget mSubtitleWidget; 116 117 /** Listener for changes to subtitle data, used to redraw when needed. */ 118 private RenderingWidget.OnChangedListener mSubtitlesChangedListener; 119 120 public VideoView(Context context) { 121 super(context); 122 initVideoView(); 123 } 124 125 public VideoView(Context context, AttributeSet attrs) { 126 this(context, attrs, 0); 127 initVideoView(); 128 } 129 130 public VideoView(Context context, AttributeSet attrs, int defStyleAttr) { 131 this(context, attrs, defStyleAttr, 0); 132 } 133 134 public VideoView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 135 super(context, attrs, defStyleAttr, defStyleRes); 136 initVideoView(); 137 } 138 139 @Override 140 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 141 //Log.i("@@@@", "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", " 142 // + MeasureSpec.toString(heightMeasureSpec) + ")"); 143 144 int width = getDefaultSize(mVideoWidth, widthMeasureSpec); 145 int height = getDefaultSize(mVideoHeight, heightMeasureSpec); 146 if (mVideoWidth > 0 && mVideoHeight > 0) { 147 148 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 149 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 150 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 151 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 152 153 if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) { 154 // the size is fixed 155 width = widthSpecSize; 156 height = heightSpecSize; 157 158 // for compatibility, we adjust size based on aspect ratio 159 if ( mVideoWidth * height < width * mVideoHeight ) { 160 //Log.i("@@@", "image too wide, correcting"); 161 width = height * mVideoWidth / mVideoHeight; 162 } else if ( mVideoWidth * height > width * mVideoHeight ) { 163 //Log.i("@@@", "image too tall, correcting"); 164 height = width * mVideoHeight / mVideoWidth; 165 } 166 } else if (widthSpecMode == MeasureSpec.EXACTLY) { 167 // only the width is fixed, adjust the height to match aspect ratio if possible 168 width = widthSpecSize; 169 height = width * mVideoHeight / mVideoWidth; 170 if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { 171 // couldn't match aspect ratio within the constraints 172 height = heightSpecSize; 173 } 174 } else if (heightSpecMode == MeasureSpec.EXACTLY) { 175 // only the height is fixed, adjust the width to match aspect ratio if possible 176 height = heightSpecSize; 177 width = height * mVideoWidth / mVideoHeight; 178 if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { 179 // couldn't match aspect ratio within the constraints 180 width = widthSpecSize; 181 } 182 } else { 183 // neither the width nor the height are fixed, try to use actual video size 184 width = mVideoWidth; 185 height = mVideoHeight; 186 if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { 187 // too tall, decrease both width and height 188 height = heightSpecSize; 189 width = height * mVideoWidth / mVideoHeight; 190 } 191 if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { 192 // too wide, decrease both width and height 193 width = widthSpecSize; 194 height = width * mVideoHeight / mVideoWidth; 195 } 196 } 197 } else { 198 // no size yet, just adopt the given spec sizes 199 } 200 setMeasuredDimension(width, height); 201 } 202 203 @Override 204 public CharSequence getAccessibilityClassName() { 205 return VideoView.class.getName(); 206 } 207 208 public int resolveAdjustedSize(int desiredSize, int measureSpec) { 209 return getDefaultSize(desiredSize, measureSpec); 210 } 211 212 private void initVideoView() { 213 mVideoWidth = 0; 214 mVideoHeight = 0; 215 getHolder().addCallback(mSHCallback); 216 getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 217 setFocusable(true); 218 setFocusableInTouchMode(true); 219 requestFocus(); 220 mPendingSubtitleTracks = new Vector<Pair<InputStream, MediaFormat>>(); 221 mCurrentState = STATE_IDLE; 222 mTargetState = STATE_IDLE; 223 } 224 225 /** 226 * Sets video path. 227 * 228 * @param path the path of the video. 229 */ 230 public void setVideoPath(String path) { 231 setVideoURI(Uri.parse(path)); 232 } 233 234 /** 235 * Sets video URI. 236 * 237 * @param uri the URI of the video. 238 */ 239 public void setVideoURI(Uri uri) { 240 setVideoURI(uri, null); 241 } 242 243 /** 244 * Sets video URI using specific headers. 245 * 246 * @param uri the URI of the video. 247 * @param headers the headers for the URI request. 248 * Note that the cross domain redirection is allowed by default, but that can be 249 * changed with key/value pairs through the headers parameter with 250 * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value 251 * to disallow or allow cross domain redirection. 252 */ 253 public void setVideoURI(Uri uri, Map<String, String> headers) { 254 mUri = uri; 255 mHeaders = headers; 256 mSeekWhenPrepared = 0; 257 openVideo(); 258 requestLayout(); 259 invalidate(); 260 } 261 262 /** 263 * Adds an external subtitle source file (from the provided input stream.) 264 * 265 * Note that a single external subtitle source may contain multiple or no 266 * supported tracks in it. If the source contained at least one track in 267 * it, one will receive an {@link MediaPlayer#MEDIA_INFO_METADATA_UPDATE} 268 * info message. Otherwise, if reading the source takes excessive time, 269 * one will receive a {@link MediaPlayer#MEDIA_INFO_SUBTITLE_TIMED_OUT} 270 * message. If the source contained no supported track (including an empty 271 * source file or null input stream), one will receive a {@link 272 * MediaPlayer#MEDIA_INFO_UNSUPPORTED_SUBTITLE} message. One can find the 273 * total number of available tracks using {@link MediaPlayer#getTrackInfo()} 274 * to see what additional tracks become available after this method call. 275 * 276 * @param is input stream containing the subtitle data. It will be 277 * closed by the media framework. 278 * @param format the format of the subtitle track(s). Must contain at least 279 * the mime type ({@link MediaFormat#KEY_MIME}) and the 280 * language ({@link MediaFormat#KEY_LANGUAGE}) of the file. 281 * If the file itself contains the language information, 282 * specify "und" for the language. 283 */ 284 public void addSubtitleSource(InputStream is, MediaFormat format) { 285 if (mMediaPlayer == null) { 286 mPendingSubtitleTracks.add(Pair.create(is, format)); 287 } else { 288 try { 289 mMediaPlayer.addSubtitleSource(is, format); 290 } catch (IllegalStateException e) { 291 mInfoListener.onInfo( 292 mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0); 293 } 294 } 295 } 296 297 private Vector<Pair<InputStream, MediaFormat>> mPendingSubtitleTracks; 298 299 public void stopPlayback() { 300 if (mMediaPlayer != null) { 301 mMediaPlayer.stop(); 302 mMediaPlayer.release(); 303 mMediaPlayer = null; 304 mCurrentState = STATE_IDLE; 305 mTargetState = STATE_IDLE; 306 AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 307 am.abandonAudioFocus(null); 308 } 309 } 310 311 private void openVideo() { 312 if (mUri == null || mSurfaceHolder == null) { 313 // not ready for playback just yet, will try again later 314 return; 315 } 316 // we shouldn't clear the target state, because somebody might have 317 // called start() previously 318 release(false); 319 320 AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 321 am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); 322 323 try { 324 mMediaPlayer = new MediaPlayer(); 325 // TODO: create SubtitleController in MediaPlayer, but we need 326 // a context for the subtitle renderers 327 final Context context = getContext(); 328 final SubtitleController controller = new SubtitleController( 329 context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer); 330 controller.registerRenderer(new WebVttRenderer(context)); 331 controller.registerRenderer(new TtmlRenderer(context)); 332 controller.registerRenderer(new Cea708CaptionRenderer(context)); 333 controller.registerRenderer(new ClosedCaptionRenderer(context)); 334 mMediaPlayer.setSubtitleAnchor(controller, this); 335 336 if (mAudioSession != 0) { 337 mMediaPlayer.setAudioSessionId(mAudioSession); 338 } else { 339 mAudioSession = mMediaPlayer.getAudioSessionId(); 340 } 341 mMediaPlayer.setOnPreparedListener(mPreparedListener); 342 mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener); 343 mMediaPlayer.setOnCompletionListener(mCompletionListener); 344 mMediaPlayer.setOnErrorListener(mErrorListener); 345 mMediaPlayer.setOnInfoListener(mInfoListener); 346 mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener); 347 mCurrentBufferPercentage = 0; 348 mMediaPlayer.setDataSource(mContext, mUri, mHeaders); 349 mMediaPlayer.setDisplay(mSurfaceHolder); 350 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 351 mMediaPlayer.setScreenOnWhilePlaying(true); 352 mMediaPlayer.prepareAsync(); 353 354 for (Pair<InputStream, MediaFormat> pending: mPendingSubtitleTracks) { 355 try { 356 mMediaPlayer.addSubtitleSource(pending.first, pending.second); 357 } catch (IllegalStateException e) { 358 mInfoListener.onInfo( 359 mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0); 360 } 361 } 362 363 // we don't set the target state here either, but preserve the 364 // target state that was there before. 365 mCurrentState = STATE_PREPARING; 366 attachMediaController(); 367 } catch (IOException ex) { 368 Log.w(TAG, "Unable to open content: " + mUri, ex); 369 mCurrentState = STATE_ERROR; 370 mTargetState = STATE_ERROR; 371 mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); 372 return; 373 } catch (IllegalArgumentException ex) { 374 Log.w(TAG, "Unable to open content: " + mUri, ex); 375 mCurrentState = STATE_ERROR; 376 mTargetState = STATE_ERROR; 377 mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); 378 return; 379 } finally { 380 mPendingSubtitleTracks.clear(); 381 } 382 } 383 384 public void setMediaController(MediaController controller) { 385 if (mMediaController != null) { 386 mMediaController.hide(); 387 } 388 mMediaController = controller; 389 attachMediaController(); 390 } 391 392 private void attachMediaController() { 393 if (mMediaPlayer != null && mMediaController != null) { 394 mMediaController.setMediaPlayer(this); 395 View anchorView = this.getParent() instanceof View ? 396 (View)this.getParent() : this; 397 mMediaController.setAnchorView(anchorView); 398 mMediaController.setEnabled(isInPlaybackState()); 399 } 400 } 401 402 MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener = 403 new MediaPlayer.OnVideoSizeChangedListener() { 404 public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { 405 mVideoWidth = mp.getVideoWidth(); 406 mVideoHeight = mp.getVideoHeight(); 407 if (mVideoWidth != 0 && mVideoHeight != 0) { 408 getHolder().setFixedSize(mVideoWidth, mVideoHeight); 409 requestLayout(); 410 } 411 } 412 }; 413 414 MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() { 415 public void onPrepared(MediaPlayer mp) { 416 mCurrentState = STATE_PREPARED; 417 418 // Get the capabilities of the player for this stream 419 Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL, 420 MediaPlayer.BYPASS_METADATA_FILTER); 421 422 if (data != null) { 423 mCanPause = !data.has(Metadata.PAUSE_AVAILABLE) 424 || data.getBoolean(Metadata.PAUSE_AVAILABLE); 425 mCanSeekBack = !data.has(Metadata.SEEK_BACKWARD_AVAILABLE) 426 || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE); 427 mCanSeekForward = !data.has(Metadata.SEEK_FORWARD_AVAILABLE) 428 || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE); 429 } else { 430 mCanPause = mCanSeekBack = mCanSeekForward = true; 431 } 432 433 if (mOnPreparedListener != null) { 434 mOnPreparedListener.onPrepared(mMediaPlayer); 435 } 436 if (mMediaController != null) { 437 mMediaController.setEnabled(true); 438 } 439 mVideoWidth = mp.getVideoWidth(); 440 mVideoHeight = mp.getVideoHeight(); 441 442 int seekToPosition = mSeekWhenPrepared; // mSeekWhenPrepared may be changed after seekTo() call 443 if (seekToPosition != 0) { 444 seekTo(seekToPosition); 445 } 446 if (mVideoWidth != 0 && mVideoHeight != 0) { 447 //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight); 448 getHolder().setFixedSize(mVideoWidth, mVideoHeight); 449 if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) { 450 // We didn't actually change the size (it was already at the size 451 // we need), so we won't get a "surface changed" callback, so 452 // start the video here instead of in the callback. 453 if (mTargetState == STATE_PLAYING) { 454 start(); 455 if (mMediaController != null) { 456 mMediaController.show(); 457 } 458 } else if (!isPlaying() && 459 (seekToPosition != 0 || getCurrentPosition() > 0)) { 460 if (mMediaController != null) { 461 // Show the media controls when we're paused into a video and make 'em stick. 462 mMediaController.show(0); 463 } 464 } 465 } 466 } else { 467 // We don't know the video size yet, but should start anyway. 468 // The video size might be reported to us later. 469 if (mTargetState == STATE_PLAYING) { 470 start(); 471 } 472 } 473 } 474 }; 475 476 private MediaPlayer.OnCompletionListener mCompletionListener = 477 new MediaPlayer.OnCompletionListener() { 478 public void onCompletion(MediaPlayer mp) { 479 mCurrentState = STATE_PLAYBACK_COMPLETED; 480 mTargetState = STATE_PLAYBACK_COMPLETED; 481 if (mMediaController != null) { 482 mMediaController.hide(); 483 } 484 if (mOnCompletionListener != null) { 485 mOnCompletionListener.onCompletion(mMediaPlayer); 486 } 487 } 488 }; 489 490 private MediaPlayer.OnInfoListener mInfoListener = 491 new MediaPlayer.OnInfoListener() { 492 public boolean onInfo(MediaPlayer mp, int arg1, int arg2) { 493 if (mOnInfoListener != null) { 494 mOnInfoListener.onInfo(mp, arg1, arg2); 495 } 496 return true; 497 } 498 }; 499 500 private MediaPlayer.OnErrorListener mErrorListener = 501 new MediaPlayer.OnErrorListener() { 502 public boolean onError(MediaPlayer mp, int framework_err, int impl_err) { 503 Log.d(TAG, "Error: " + framework_err + "," + impl_err); 504 mCurrentState = STATE_ERROR; 505 mTargetState = STATE_ERROR; 506 if (mMediaController != null) { 507 mMediaController.hide(); 508 } 509 510 /* If an error handler has been supplied, use it and finish. */ 511 if (mOnErrorListener != null) { 512 if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) { 513 return true; 514 } 515 } 516 517 /* Otherwise, pop up an error dialog so the user knows that 518 * something bad has happened. Only try and pop up the dialog 519 * if we're attached to a window. When we're going away and no 520 * longer have a window, don't bother showing the user an error. 521 */ 522 if (getWindowToken() != null) { 523 Resources r = mContext.getResources(); 524 int messageId; 525 526 if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) { 527 messageId = com.android.internal.R.string.VideoView_error_text_invalid_progressive_playback; 528 } else { 529 messageId = com.android.internal.R.string.VideoView_error_text_unknown; 530 } 531 532 new AlertDialog.Builder(mContext) 533 .setMessage(messageId) 534 .setPositiveButton(com.android.internal.R.string.VideoView_error_button, 535 new DialogInterface.OnClickListener() { 536 public void onClick(DialogInterface dialog, int whichButton) { 537 /* If we get here, there is no onError listener, so 538 * at least inform them that the video is over. 539 */ 540 if (mOnCompletionListener != null) { 541 mOnCompletionListener.onCompletion(mMediaPlayer); 542 } 543 } 544 }) 545 .setCancelable(false) 546 .show(); 547 } 548 return true; 549 } 550 }; 551 552 private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener = 553 new MediaPlayer.OnBufferingUpdateListener() { 554 public void onBufferingUpdate(MediaPlayer mp, int percent) { 555 mCurrentBufferPercentage = percent; 556 } 557 }; 558 559 /** 560 * Register a callback to be invoked when the media file 561 * is loaded and ready to go. 562 * 563 * @param l The callback that will be run 564 */ 565 public void setOnPreparedListener(MediaPlayer.OnPreparedListener l) 566 { 567 mOnPreparedListener = l; 568 } 569 570 /** 571 * Register a callback to be invoked when the end of a media file 572 * has been reached during playback. 573 * 574 * @param l The callback that will be run 575 */ 576 public void setOnCompletionListener(OnCompletionListener l) 577 { 578 mOnCompletionListener = l; 579 } 580 581 /** 582 * Register a callback to be invoked when an error occurs 583 * during playback or setup. If no listener is specified, 584 * or if the listener returned false, VideoView will inform 585 * the user of any errors. 586 * 587 * @param l The callback that will be run 588 */ 589 public void setOnErrorListener(OnErrorListener l) 590 { 591 mOnErrorListener = l; 592 } 593 594 /** 595 * Register a callback to be invoked when an informational event 596 * occurs during playback or setup. 597 * 598 * @param l The callback that will be run 599 */ 600 public void setOnInfoListener(OnInfoListener l) { 601 mOnInfoListener = l; 602 } 603 604 SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback() 605 { 606 public void surfaceChanged(SurfaceHolder holder, int format, 607 int w, int h) 608 { 609 mSurfaceWidth = w; 610 mSurfaceHeight = h; 611 boolean isValidState = (mTargetState == STATE_PLAYING); 612 boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h); 613 if (mMediaPlayer != null && isValidState && hasValidSize) { 614 if (mSeekWhenPrepared != 0) { 615 seekTo(mSeekWhenPrepared); 616 } 617 start(); 618 } 619 } 620 621 public void surfaceCreated(SurfaceHolder holder) 622 { 623 mSurfaceHolder = holder; 624 openVideo(); 625 } 626 627 public void surfaceDestroyed(SurfaceHolder holder) 628 { 629 // after we return from this we can't use the surface any more 630 mSurfaceHolder = null; 631 if (mMediaController != null) mMediaController.hide(); 632 release(true); 633 } 634 }; 635 636 /* 637 * release the media player in any state 638 */ 639 private void release(boolean cleartargetstate) { 640 if (mMediaPlayer != null) { 641 mMediaPlayer.reset(); 642 mMediaPlayer.release(); 643 mMediaPlayer = null; 644 mPendingSubtitleTracks.clear(); 645 mCurrentState = STATE_IDLE; 646 if (cleartargetstate) { 647 mTargetState = STATE_IDLE; 648 } 649 AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 650 am.abandonAudioFocus(null); 651 } 652 } 653 654 @Override 655 public boolean onTouchEvent(MotionEvent ev) { 656 if (isInPlaybackState() && mMediaController != null) { 657 toggleMediaControlsVisiblity(); 658 } 659 return false; 660 } 661 662 @Override 663 public boolean onTrackballEvent(MotionEvent ev) { 664 if (isInPlaybackState() && mMediaController != null) { 665 toggleMediaControlsVisiblity(); 666 } 667 return false; 668 } 669 670 @Override 671 public boolean onKeyDown(int keyCode, KeyEvent event) 672 { 673 boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK && 674 keyCode != KeyEvent.KEYCODE_VOLUME_UP && 675 keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && 676 keyCode != KeyEvent.KEYCODE_VOLUME_MUTE && 677 keyCode != KeyEvent.KEYCODE_MENU && 678 keyCode != KeyEvent.KEYCODE_CALL && 679 keyCode != KeyEvent.KEYCODE_ENDCALL; 680 if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) { 681 if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || 682 keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { 683 if (mMediaPlayer.isPlaying()) { 684 pause(); 685 mMediaController.show(); 686 } else { 687 start(); 688 mMediaController.hide(); 689 } 690 return true; 691 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { 692 if (!mMediaPlayer.isPlaying()) { 693 start(); 694 mMediaController.hide(); 695 } 696 return true; 697 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP 698 || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { 699 if (mMediaPlayer.isPlaying()) { 700 pause(); 701 mMediaController.show(); 702 } 703 return true; 704 } else { 705 toggleMediaControlsVisiblity(); 706 } 707 } 708 709 return super.onKeyDown(keyCode, event); 710 } 711 712 private void toggleMediaControlsVisiblity() { 713 if (mMediaController.isShowing()) { 714 mMediaController.hide(); 715 } else { 716 mMediaController.show(); 717 } 718 } 719 720 @Override 721 public void start() { 722 if (isInPlaybackState()) { 723 mMediaPlayer.start(); 724 mCurrentState = STATE_PLAYING; 725 } 726 mTargetState = STATE_PLAYING; 727 } 728 729 @Override 730 public void pause() { 731 if (isInPlaybackState()) { 732 if (mMediaPlayer.isPlaying()) { 733 mMediaPlayer.pause(); 734 mCurrentState = STATE_PAUSED; 735 } 736 } 737 mTargetState = STATE_PAUSED; 738 } 739 740 public void suspend() { 741 release(false); 742 } 743 744 public void resume() { 745 openVideo(); 746 } 747 748 @Override 749 public int getDuration() { 750 if (isInPlaybackState()) { 751 return mMediaPlayer.getDuration(); 752 } 753 754 return -1; 755 } 756 757 @Override 758 public int getCurrentPosition() { 759 if (isInPlaybackState()) { 760 return mMediaPlayer.getCurrentPosition(); 761 } 762 return 0; 763 } 764 765 @Override 766 public void seekTo(int msec) { 767 if (isInPlaybackState()) { 768 mMediaPlayer.seekTo(msec); 769 mSeekWhenPrepared = 0; 770 } else { 771 mSeekWhenPrepared = msec; 772 } 773 } 774 775 @Override 776 public boolean isPlaying() { 777 return isInPlaybackState() && mMediaPlayer.isPlaying(); 778 } 779 780 @Override 781 public int getBufferPercentage() { 782 if (mMediaPlayer != null) { 783 return mCurrentBufferPercentage; 784 } 785 return 0; 786 } 787 788 private boolean isInPlaybackState() { 789 return (mMediaPlayer != null && 790 mCurrentState != STATE_ERROR && 791 mCurrentState != STATE_IDLE && 792 mCurrentState != STATE_PREPARING); 793 } 794 795 @Override 796 public boolean canPause() { 797 return mCanPause; 798 } 799 800 @Override 801 public boolean canSeekBackward() { 802 return mCanSeekBack; 803 } 804 805 @Override 806 public boolean canSeekForward() { 807 return mCanSeekForward; 808 } 809 810 @Override 811 public int getAudioSessionId() { 812 if (mAudioSession == 0) { 813 MediaPlayer foo = new MediaPlayer(); 814 mAudioSession = foo.getAudioSessionId(); 815 foo.release(); 816 } 817 return mAudioSession; 818 } 819 820 @Override 821 protected void onAttachedToWindow() { 822 super.onAttachedToWindow(); 823 824 if (mSubtitleWidget != null) { 825 mSubtitleWidget.onAttachedToWindow(); 826 } 827 } 828 829 @Override 830 protected void onDetachedFromWindow() { 831 super.onDetachedFromWindow(); 832 833 if (mSubtitleWidget != null) { 834 mSubtitleWidget.onDetachedFromWindow(); 835 } 836 } 837 838 @Override 839 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 840 super.onLayout(changed, left, top, right, bottom); 841 842 if (mSubtitleWidget != null) { 843 measureAndLayoutSubtitleWidget(); 844 } 845 } 846 847 @Override 848 public void draw(Canvas canvas) { 849 super.draw(canvas); 850 851 if (mSubtitleWidget != null) { 852 final int saveCount = canvas.save(); 853 canvas.translate(getPaddingLeft(), getPaddingTop()); 854 mSubtitleWidget.draw(canvas); 855 canvas.restoreToCount(saveCount); 856 } 857 } 858 859 /** 860 * Forces a measurement and layout pass for all overlaid views. 861 * 862 * @see #setSubtitleWidget(RenderingWidget) 863 */ 864 private void measureAndLayoutSubtitleWidget() { 865 final int width = getWidth() - getPaddingLeft() - getPaddingRight(); 866 final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 867 868 mSubtitleWidget.setSize(width, height); 869 } 870 871 /** @hide */ 872 @Override 873 public void setSubtitleWidget(RenderingWidget subtitleWidget) { 874 if (mSubtitleWidget == subtitleWidget) { 875 return; 876 } 877 878 final boolean attachedToWindow = isAttachedToWindow(); 879 if (mSubtitleWidget != null) { 880 if (attachedToWindow) { 881 mSubtitleWidget.onDetachedFromWindow(); 882 } 883 884 mSubtitleWidget.setOnChangedListener(null); 885 } 886 887 mSubtitleWidget = subtitleWidget; 888 889 if (subtitleWidget != null) { 890 if (mSubtitlesChangedListener == null) { 891 mSubtitlesChangedListener = new RenderingWidget.OnChangedListener() { 892 @Override 893 public void onChanged(RenderingWidget renderingWidget) { 894 invalidate(); 895 } 896 }; 897 } 898 899 setWillNotDraw(false); 900 subtitleWidget.setOnChangedListener(mSubtitlesChangedListener); 901 902 if (attachedToWindow) { 903 subtitleWidget.onAttachedToWindow(); 904 requestLayout(); 905 } 906 } else { 907 setWillNotDraw(true); 908 } 909 910 invalidate(); 911 } 912 913 /** @hide */ 914 @Override 915 public Looper getSubtitleLooper() { 916 return Looper.getMainLooper(); 917 } 918 } 919