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