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