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