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