1 /* 2 * Copyright (C) 2016 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 package com.google.android.exoplayer2.ui; 17 18 import android.annotation.SuppressLint; 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.content.res.TypedArray; 22 import android.graphics.Bitmap; 23 import android.graphics.BitmapFactory; 24 import android.graphics.Matrix; 25 import android.graphics.RectF; 26 import android.graphics.drawable.BitmapDrawable; 27 import android.graphics.drawable.Drawable; 28 import android.os.Looper; 29 import android.util.AttributeSet; 30 import android.view.KeyEvent; 31 import android.view.LayoutInflater; 32 import android.view.MotionEvent; 33 import android.view.SurfaceView; 34 import android.view.TextureView; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.widget.FrameLayout; 38 import android.widget.ImageView; 39 import android.widget.TextView; 40 import androidx.annotation.IntDef; 41 import androidx.annotation.Nullable; 42 import androidx.annotation.RequiresApi; 43 import androidx.core.content.ContextCompat; 44 import com.google.android.exoplayer2.C; 45 import com.google.android.exoplayer2.ControlDispatcher; 46 import com.google.android.exoplayer2.DefaultControlDispatcher; 47 import com.google.android.exoplayer2.ExoPlaybackException; 48 import com.google.android.exoplayer2.PlaybackPreparer; 49 import com.google.android.exoplayer2.Player; 50 import com.google.android.exoplayer2.Player.DiscontinuityReason; 51 import com.google.android.exoplayer2.metadata.Metadata; 52 import com.google.android.exoplayer2.metadata.flac.PictureFrame; 53 import com.google.android.exoplayer2.metadata.id3.ApicFrame; 54 import com.google.android.exoplayer2.source.TrackGroupArray; 55 import com.google.android.exoplayer2.source.ads.AdsLoader; 56 import com.google.android.exoplayer2.text.Cue; 57 import com.google.android.exoplayer2.text.TextOutput; 58 import com.google.android.exoplayer2.trackselection.TrackSelection; 59 import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 60 import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; 61 import com.google.android.exoplayer2.ui.spherical.SingleTapListener; 62 import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView; 63 import com.google.android.exoplayer2.util.Assertions; 64 import com.google.android.exoplayer2.util.ErrorMessageProvider; 65 import com.google.android.exoplayer2.util.RepeatModeUtil; 66 import com.google.android.exoplayer2.util.Util; 67 import com.google.android.exoplayer2.video.VideoDecoderGLSurfaceView; 68 import com.google.android.exoplayer2.video.VideoListener; 69 import java.lang.annotation.Documented; 70 import java.lang.annotation.Retention; 71 import java.lang.annotation.RetentionPolicy; 72 import java.util.ArrayList; 73 import java.util.List; 74 import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; 75 import org.checkerframework.checker.nullness.qual.RequiresNonNull; 76 77 /** 78 * A high level view for {@link Player} media playbacks. It displays video, subtitles and album art 79 * during playback, and displays playback controls using a {@link PlayerControlView}. 80 * 81 * <p>A PlayerView can be customized by setting attributes (or calling corresponding methods), 82 * overriding drawables, overriding the view's layout file, or by specifying a custom view layout 83 * file. 84 * 85 * <h3>Attributes</h3> 86 * 87 * The following attributes can be set on a PlayerView when used in a layout XML file: 88 * 89 * <ul> 90 * <li><b>{@code use_artwork}</b> - Whether artwork is used if available in audio streams. 91 * <ul> 92 * <li>Corresponding method: {@link #setUseArtwork(boolean)} 93 * <li>Default: {@code true} 94 * </ul> 95 * <li><b>{@code default_artwork}</b> - Default artwork to use if no artwork available in audio 96 * streams. 97 * <ul> 98 * <li>Corresponding method: {@link #setDefaultArtwork(Drawable)} 99 * <li>Default: {@code null} 100 * </ul> 101 * <li><b>{@code use_controller}</b> - Whether the playback controls can be shown. 102 * <ul> 103 * <li>Corresponding method: {@link #setUseController(boolean)} 104 * <li>Default: {@code true} 105 * </ul> 106 * <li><b>{@code hide_on_touch}</b> - Whether the playback controls are hidden by touch events. 107 * <ul> 108 * <li>Corresponding method: {@link #setControllerHideOnTouch(boolean)} 109 * <li>Default: {@code true} 110 * </ul> 111 * <li><b>{@code auto_show}</b> - Whether the playback controls are automatically shown when 112 * playback starts, pauses, ends, or fails. If set to false, the playback controls can be 113 * manually operated with {@link #showController()} and {@link #hideController()}. 114 * <ul> 115 * <li>Corresponding method: {@link #setControllerAutoShow(boolean)} 116 * <li>Default: {@code true} 117 * </ul> 118 * <li><b>{@code hide_during_ads}</b> - Whether the playback controls are hidden during ads. 119 * Controls are always shown during ads if they are enabled and the player is paused. 120 * <ul> 121 * <li>Corresponding method: {@link #setControllerHideDuringAds(boolean)} 122 * <li>Default: {@code true} 123 * </ul> 124 * <li><b>{@code show_buffering}</b> - Whether the buffering spinner is displayed when the player 125 * is buffering. Valid values are {@code never}, {@code when_playing} and {@code always}. 126 * <ul> 127 * <li>Corresponding method: {@link #setShowBuffering(int)} 128 * <li>Default: {@code never} 129 * </ul> 130 * <li><b>{@code resize_mode}</b> - Controls how video and album art is resized within the view. 131 * Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}. 132 * <ul> 133 * <li>Corresponding method: {@link #setResizeMode(int)} 134 * <li>Default: {@code fit} 135 * </ul> 136 * <li><b>{@code surface_type}</b> - The type of surface view used for video playbacks. Valid 137 * values are {@code surface_view}, {@code texture_view}, {@code spherical_gl_surface_view}, 138 * {@code video_decoder_gl_surface_view} and {@code none}. Using {@code none} is recommended 139 * for audio only applications, since creating the surface can be expensive. Using {@code 140 * surface_view} is recommended for video applications. Note, TextureView can only be used in 141 * a hardware accelerated window. When rendered in software, TextureView will draw nothing. 142 * <ul> 143 * <li>Corresponding method: None 144 * <li>Default: {@code surface_view} 145 * </ul> 146 * <li><b>{@code use_sensor_rotation}</b> - Whether to use the orientation sensor for rotation 147 * during spherical playbacks (if available). 148 * <ul> 149 * <li>Corresponding method: {@link #setUseSensorRotation(boolean)} 150 * <li>Default: {@code true} 151 * </ul> 152 * <li><b>{@code shutter_background_color}</b> - The background color of the {@code exo_shutter} 153 * view. 154 * <ul> 155 * <li>Corresponding method: {@link #setShutterBackgroundColor(int)} 156 * <li>Default: {@code unset} 157 * </ul> 158 * <li><b>{@code keep_content_on_player_reset}</b> - Whether the currently displayed video frame 159 * or media artwork is kept visible when the player is reset. 160 * <ul> 161 * <li>Corresponding method: {@link #setKeepContentOnPlayerReset(boolean)} 162 * <li>Default: {@code false} 163 * </ul> 164 * <li><b>{@code player_layout_id}</b> - Specifies the id of the layout to be inflated. See below 165 * for more details. 166 * <ul> 167 * <li>Corresponding method: None 168 * <li>Default: {@code R.layout.exo_player_view} 169 * </ul> 170 * <li><b>{@code controller_layout_id}</b> - Specifies the id of the layout resource to be 171 * inflated by the child {@link PlayerControlView}. See below for more details. 172 * <ul> 173 * <li>Corresponding method: None 174 * <li>Default: {@code R.layout.exo_player_control_view} 175 * </ul> 176 * <li>All attributes that can be set on {@link PlayerControlView} and {@link DefaultTimeBar} can 177 * also be set on a PlayerView, and will be propagated to the inflated {@link 178 * PlayerControlView} unless the layout is overridden to specify a custom {@code 179 * exo_controller} (see below). 180 * </ul> 181 * 182 * <h3>Overriding drawables</h3> 183 * 184 * The drawables used by {@link PlayerControlView} (with its default layout file) can be overridden 185 * by drawables with the same names defined in your application. See the {@link PlayerControlView} 186 * documentation for a list of drawables that can be overridden. 187 * 188 * <h3>Overriding the layout file</h3> 189 * 190 * To customize the layout of PlayerView throughout your app, or just for certain configurations, 191 * you can define {@code exo_player_view.xml} layout files in your application {@code res/layout*} 192 * directories. These layouts will override the one provided by the ExoPlayer library, and will be 193 * inflated for use by PlayerView. The view identifies and binds its children by looking for the 194 * following ids: 195 * 196 * <ul> 197 * <li><b>{@code exo_content_frame}</b> - A frame whose aspect ratio is resized based on the video 198 * or album art of the media being played, and the configured {@code resize_mode}. The video 199 * surface view is inflated into this frame as its first child. 200 * <ul> 201 * <li>Type: {@link AspectRatioFrameLayout} 202 * </ul> 203 * <li><b>{@code exo_shutter}</b> - A view that's made visible when video should be hidden. This 204 * view is typically an opaque view that covers the video surface, thereby obscuring it when 205 * visible. Obscuring the surface in this way also helps to prevent flicker at the start of 206 * playback when {@code surface_type="surface_view"}. 207 * <ul> 208 * <li>Type: {@link View} 209 * </ul> 210 * <li><b>{@code exo_buffering}</b> - A view that's made visible when the player is buffering. 211 * This view typically displays a buffering spinner or animation. 212 * <ul> 213 * <li>Type: {@link View} 214 * </ul> 215 * <li><b>{@code exo_subtitles}</b> - Displays subtitles. 216 * <ul> 217 * <li>Type: {@link SubtitleView} 218 * </ul> 219 * <li><b>{@code exo_artwork}</b> - Displays album art. 220 * <ul> 221 * <li>Type: {@link ImageView} 222 * </ul> 223 * <li><b>{@code exo_error_message}</b> - Displays an error message to the user if playback fails. 224 * <ul> 225 * <li>Type: {@link TextView} 226 * </ul> 227 * <li><b>{@code exo_controller_placeholder}</b> - A placeholder that's replaced with the inflated 228 * {@link PlayerControlView}. Ignored if an {@code exo_controller} view exists. 229 * <ul> 230 * <li>Type: {@link View} 231 * </ul> 232 * <li><b>{@code exo_controller}</b> - An already inflated {@link PlayerControlView}. Allows use 233 * of a custom extension of {@link PlayerControlView}. {@link PlayerControlView} and {@link 234 * DefaultTimeBar} attributes set on the PlayerView will not be automatically propagated 235 * through to this instance. If a view exists with this id, any {@code 236 * exo_controller_placeholder} view will be ignored. 237 * <ul> 238 * <li>Type: {@link PlayerControlView} 239 * </ul> 240 * <li><b>{@code exo_ad_overlay}</b> - A {@link FrameLayout} positioned on top of the player which 241 * is used to show ad UI (if applicable). 242 * <ul> 243 * <li>Type: {@link FrameLayout} 244 * </ul> 245 * <li><b>{@code exo_overlay}</b> - A {@link FrameLayout} positioned on top of the player which 246 * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. 247 * <ul> 248 * <li>Type: {@link FrameLayout} 249 * </ul> 250 * </ul> 251 * 252 * <p>All child views are optional and so can be omitted if not required, however where defined they 253 * must be of the expected type. 254 * 255 * <h3>Specifying a custom layout file</h3> 256 * 257 * Defining your own {@code exo_player_view.xml} is useful to customize the layout of PlayerView 258 * throughout your application. It's also possible to customize the layout for a single instance in 259 * a layout file. This is achieved by setting the {@code player_layout_id} attribute on a 260 * PlayerView. This will cause the specified layout to be inflated instead of {@code 261 * exo_player_view.xml} for only the instance on which the attribute is set. 262 */ 263 public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider { 264 265 // LINT.IfChange 266 /** 267 * Determines when the buffering view is shown. One of {@link #SHOW_BUFFERING_NEVER}, {@link 268 * #SHOW_BUFFERING_WHEN_PLAYING} or {@link #SHOW_BUFFERING_ALWAYS}. 269 */ 270 @Documented 271 @Retention(RetentionPolicy.SOURCE) 272 @IntDef({SHOW_BUFFERING_NEVER, SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS}) 273 public @interface ShowBuffering {} 274 /** The buffering view is never shown. */ 275 public static final int SHOW_BUFFERING_NEVER = 0; 276 /** 277 * The buffering view is shown when the player is in the {@link Player#STATE_BUFFERING buffering} 278 * state and {@link Player#getPlayWhenReady() playWhenReady} is {@code true}. 279 */ 280 public static final int SHOW_BUFFERING_WHEN_PLAYING = 1; 281 /** 282 * The buffering view is always shown when the player is in the {@link Player#STATE_BUFFERING 283 * buffering} state. 284 */ 285 public static final int SHOW_BUFFERING_ALWAYS = 2; 286 // LINT.ThenChange(../../../../../../res/values/attrs.xml) 287 288 // LINT.IfChange 289 private static final int SURFACE_TYPE_NONE = 0; 290 private static final int SURFACE_TYPE_SURFACE_VIEW = 1; 291 private static final int SURFACE_TYPE_TEXTURE_VIEW = 2; 292 private static final int SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW = 3; 293 private static final int SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW = 4; 294 // LINT.ThenChange(../../../../../../res/values/attrs.xml) 295 296 private final ComponentListener componentListener; 297 @Nullable private final AspectRatioFrameLayout contentFrame; 298 @Nullable private final View shutterView; 299 @Nullable private final View surfaceView; 300 @Nullable private final ImageView artworkView; 301 @Nullable private final SubtitleView subtitleView; 302 @Nullable private final View bufferingView; 303 @Nullable private final TextView errorMessageView; 304 @Nullable private final PlayerControlView controller; 305 @Nullable private final FrameLayout adOverlayFrameLayout; 306 @Nullable private final FrameLayout overlayFrameLayout; 307 308 @Nullable private Player player; 309 private boolean useController; 310 @Nullable private PlayerControlView.VisibilityListener controllerVisibilityListener; 311 private boolean useArtwork; 312 @Nullable private Drawable defaultArtwork; 313 private @ShowBuffering int showBuffering; 314 private boolean keepContentOnPlayerReset; 315 private boolean useSensorRotation; 316 @Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider; 317 @Nullable private CharSequence customErrorMessage; 318 private int controllerShowTimeoutMs; 319 private boolean controllerAutoShow; 320 private boolean controllerHideDuringAds; 321 private boolean controllerHideOnTouch; 322 private int textureViewRotation; 323 private boolean isTouching; 324 private static final int PICTURE_TYPE_FRONT_COVER = 3; 325 private static final int PICTURE_TYPE_NOT_SET = -1; 326 PlayerView(Context context)327 public PlayerView(Context context) { 328 this(context, /* attrs= */ null); 329 } 330 PlayerView(Context context, @Nullable AttributeSet attrs)331 public PlayerView(Context context, @Nullable AttributeSet attrs) { 332 this(context, attrs, /* defStyleAttr= */ 0); 333 } 334 335 @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:method.invocation.invalid"}) PlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)336 public PlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 337 super(context, attrs, defStyleAttr); 338 339 componentListener = new ComponentListener(); 340 341 if (isInEditMode()) { 342 contentFrame = null; 343 shutterView = null; 344 surfaceView = null; 345 artworkView = null; 346 subtitleView = null; 347 bufferingView = null; 348 errorMessageView = null; 349 controller = null; 350 adOverlayFrameLayout = null; 351 overlayFrameLayout = null; 352 ImageView logo = new ImageView(context); 353 if (Util.SDK_INT >= 23) { 354 configureEditModeLogoV23(getResources(), logo); 355 } else { 356 configureEditModeLogo(getResources(), logo); 357 } 358 addView(logo); 359 return; 360 } 361 362 boolean shutterColorSet = false; 363 int shutterColor = 0; 364 int playerLayoutId = R.layout.exo_player_view; 365 boolean useArtwork = true; 366 int defaultArtworkId = 0; 367 boolean useController = true; 368 int surfaceType = SURFACE_TYPE_SURFACE_VIEW; 369 int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; 370 int controllerShowTimeoutMs = PlayerControlView.DEFAULT_SHOW_TIMEOUT_MS; 371 boolean controllerHideOnTouch = true; 372 boolean controllerAutoShow = true; 373 boolean controllerHideDuringAds = true; 374 int showBuffering = SHOW_BUFFERING_NEVER; 375 useSensorRotation = true; 376 if (attrs != null) { 377 TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlayerView, 0, 0); 378 try { 379 shutterColorSet = a.hasValue(R.styleable.PlayerView_shutter_background_color); 380 shutterColor = a.getColor(R.styleable.PlayerView_shutter_background_color, shutterColor); 381 playerLayoutId = a.getResourceId(R.styleable.PlayerView_player_layout_id, playerLayoutId); 382 useArtwork = a.getBoolean(R.styleable.PlayerView_use_artwork, useArtwork); 383 defaultArtworkId = 384 a.getResourceId(R.styleable.PlayerView_default_artwork, defaultArtworkId); 385 useController = a.getBoolean(R.styleable.PlayerView_use_controller, useController); 386 surfaceType = a.getInt(R.styleable.PlayerView_surface_type, surfaceType); 387 resizeMode = a.getInt(R.styleable.PlayerView_resize_mode, resizeMode); 388 controllerShowTimeoutMs = 389 a.getInt(R.styleable.PlayerView_show_timeout, controllerShowTimeoutMs); 390 controllerHideOnTouch = 391 a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch); 392 controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow); 393 showBuffering = a.getInteger(R.styleable.PlayerView_show_buffering, showBuffering); 394 keepContentOnPlayerReset = 395 a.getBoolean( 396 R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset); 397 controllerHideDuringAds = 398 a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds); 399 useSensorRotation = 400 a.getBoolean(R.styleable.PlayerView_use_sensor_rotation, useSensorRotation); 401 } finally { 402 a.recycle(); 403 } 404 } 405 406 LayoutInflater.from(context).inflate(playerLayoutId, this); 407 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 408 409 // Content frame. 410 contentFrame = findViewById(R.id.exo_content_frame); 411 if (contentFrame != null) { 412 setResizeModeRaw(contentFrame, resizeMode); 413 } 414 415 // Shutter view. 416 shutterView = findViewById(R.id.exo_shutter); 417 if (shutterView != null && shutterColorSet) { 418 shutterView.setBackgroundColor(shutterColor); 419 } 420 421 // Create a surface view and insert it into the content frame, if there is one. 422 if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) { 423 ViewGroup.LayoutParams params = 424 new ViewGroup.LayoutParams( 425 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 426 switch (surfaceType) { 427 case SURFACE_TYPE_TEXTURE_VIEW: 428 surfaceView = new TextureView(context); 429 break; 430 case SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW: 431 SphericalGLSurfaceView sphericalGLSurfaceView = new SphericalGLSurfaceView(context); 432 sphericalGLSurfaceView.setSingleTapListener(componentListener); 433 sphericalGLSurfaceView.setUseSensorRotation(useSensorRotation); 434 surfaceView = sphericalGLSurfaceView; 435 break; 436 case SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW: 437 surfaceView = new VideoDecoderGLSurfaceView(context); 438 break; 439 default: 440 surfaceView = new SurfaceView(context); 441 break; 442 } 443 surfaceView.setLayoutParams(params); 444 contentFrame.addView(surfaceView, 0); 445 } else { 446 surfaceView = null; 447 } 448 449 // Ad overlay frame layout. 450 adOverlayFrameLayout = findViewById(R.id.exo_ad_overlay); 451 452 // Overlay frame layout. 453 overlayFrameLayout = findViewById(R.id.exo_overlay); 454 455 // Artwork view. 456 artworkView = findViewById(R.id.exo_artwork); 457 this.useArtwork = useArtwork && artworkView != null; 458 if (defaultArtworkId != 0) { 459 defaultArtwork = ContextCompat.getDrawable(getContext(), defaultArtworkId); 460 } 461 462 // Subtitle view. 463 subtitleView = findViewById(R.id.exo_subtitles); 464 if (subtitleView != null) { 465 subtitleView.setUserDefaultStyle(); 466 subtitleView.setUserDefaultTextSize(); 467 } 468 469 // Buffering view. 470 bufferingView = findViewById(R.id.exo_buffering); 471 if (bufferingView != null) { 472 bufferingView.setVisibility(View.GONE); 473 } 474 this.showBuffering = showBuffering; 475 476 // Error message view. 477 errorMessageView = findViewById(R.id.exo_error_message); 478 if (errorMessageView != null) { 479 errorMessageView.setVisibility(View.GONE); 480 } 481 482 // Playback control view. 483 PlayerControlView customController = findViewById(R.id.exo_controller); 484 View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); 485 if (customController != null) { 486 this.controller = customController; 487 } else if (controllerPlaceholder != null) { 488 // Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are 489 // transferred, but standard attributes (e.g. background) are not. 490 this.controller = new PlayerControlView(context, null, 0, attrs); 491 controller.setId(R.id.exo_controller); 492 controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); 493 ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); 494 int controllerIndex = parent.indexOfChild(controllerPlaceholder); 495 parent.removeView(controllerPlaceholder); 496 parent.addView(controller, controllerIndex); 497 } else { 498 this.controller = null; 499 } 500 this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; 501 this.controllerHideOnTouch = controllerHideOnTouch; 502 this.controllerAutoShow = controllerAutoShow; 503 this.controllerHideDuringAds = controllerHideDuringAds; 504 this.useController = useController && controller != null; 505 hideController(); 506 updateContentDescription(); 507 if (controller != null) { 508 controller.addVisibilityListener(/* listener= */ componentListener); 509 } 510 } 511 512 /** 513 * Switches the view targeted by a given {@link Player}. 514 * 515 * @param player The player whose target view is being switched. 516 * @param oldPlayerView The old view to detach from the player. 517 * @param newPlayerView The new view to attach to the player. 518 */ switchTargetView( Player player, @Nullable PlayerView oldPlayerView, @Nullable PlayerView newPlayerView)519 public static void switchTargetView( 520 Player player, @Nullable PlayerView oldPlayerView, @Nullable PlayerView newPlayerView) { 521 if (oldPlayerView == newPlayerView) { 522 return; 523 } 524 // We attach the new view before detaching the old one because this ordering allows the player 525 // to swap directly from one surface to another, without transitioning through a state where no 526 // surface is attached. This is significantly more efficient and achieves a more seamless 527 // transition when using platform provided video decoders. 528 if (newPlayerView != null) { 529 newPlayerView.setPlayer(player); 530 } 531 if (oldPlayerView != null) { 532 oldPlayerView.setPlayer(null); 533 } 534 } 535 536 /** Returns the player currently set on this view, or null if no player is set. */ 537 @Nullable getPlayer()538 public Player getPlayer() { 539 return player; 540 } 541 542 /** 543 * Set the {@link Player} to use. 544 * 545 * <p>To transition a {@link Player} from targeting one view to another, it's recommended to use 546 * {@link #switchTargetView(Player, PlayerView, PlayerView)} rather than this method. If you do 547 * wish to use this method directly, be sure to attach the player to the new view <em>before</em> 548 * calling {@code setPlayer(null)} to detach it from the old one. This ordering is significantly 549 * more efficient and may allow for more seamless transitions. 550 * 551 * @param player The {@link Player} to use, or {@code null} to detach the current player. Only 552 * players which are accessed on the main thread are supported ({@code 553 * player.getApplicationLooper() == Looper.getMainLooper()}). 554 */ setPlayer(@ullable Player player)555 public void setPlayer(@Nullable Player player) { 556 Assertions.checkState(Looper.myLooper() == Looper.getMainLooper()); 557 Assertions.checkArgument( 558 player == null || player.getApplicationLooper() == Looper.getMainLooper()); 559 if (this.player == player) { 560 return; 561 } 562 @Nullable Player oldPlayer = this.player; 563 if (oldPlayer != null) { 564 oldPlayer.removeListener(componentListener); 565 @Nullable Player.VideoComponent oldVideoComponent = oldPlayer.getVideoComponent(); 566 if (oldVideoComponent != null) { 567 oldVideoComponent.removeVideoListener(componentListener); 568 if (surfaceView instanceof TextureView) { 569 oldVideoComponent.clearVideoTextureView((TextureView) surfaceView); 570 } else if (surfaceView instanceof SphericalGLSurfaceView) { 571 ((SphericalGLSurfaceView) surfaceView).setVideoComponent(null); 572 } else if (surfaceView instanceof VideoDecoderGLSurfaceView) { 573 oldVideoComponent.setVideoDecoderOutputBufferRenderer(null); 574 } else if (surfaceView instanceof SurfaceView) { 575 oldVideoComponent.clearVideoSurfaceView((SurfaceView) surfaceView); 576 } 577 } 578 @Nullable Player.TextComponent oldTextComponent = oldPlayer.getTextComponent(); 579 if (oldTextComponent != null) { 580 oldTextComponent.removeTextOutput(componentListener); 581 } 582 } 583 this.player = player; 584 if (useController()) { 585 controller.setPlayer(player); 586 } 587 if (subtitleView != null) { 588 subtitleView.setCues(null); 589 } 590 updateBuffering(); 591 updateErrorMessage(); 592 updateForCurrentTrackSelections(/* isNewPlayer= */ true); 593 if (player != null) { 594 @Nullable Player.VideoComponent newVideoComponent = player.getVideoComponent(); 595 if (newVideoComponent != null) { 596 if (surfaceView instanceof TextureView) { 597 newVideoComponent.setVideoTextureView((TextureView) surfaceView); 598 } else if (surfaceView instanceof SphericalGLSurfaceView) { 599 ((SphericalGLSurfaceView) surfaceView).setVideoComponent(newVideoComponent); 600 } else if (surfaceView instanceof VideoDecoderGLSurfaceView) { 601 newVideoComponent.setVideoDecoderOutputBufferRenderer( 602 ((VideoDecoderGLSurfaceView) surfaceView).getVideoDecoderOutputBufferRenderer()); 603 } else if (surfaceView instanceof SurfaceView) { 604 newVideoComponent.setVideoSurfaceView((SurfaceView) surfaceView); 605 } 606 newVideoComponent.addVideoListener(componentListener); 607 } 608 @Nullable Player.TextComponent newTextComponent = player.getTextComponent(); 609 if (newTextComponent != null) { 610 newTextComponent.addTextOutput(componentListener); 611 } 612 player.addListener(componentListener); 613 maybeShowController(false); 614 } else { 615 hideController(); 616 } 617 } 618 619 @Override setVisibility(int visibility)620 public void setVisibility(int visibility) { 621 super.setVisibility(visibility); 622 if (surfaceView instanceof SurfaceView) { 623 // Work around https://github.com/google/ExoPlayer/issues/3160. 624 surfaceView.setVisibility(visibility); 625 } 626 } 627 628 /** 629 * Sets the {@link ResizeMode}. 630 * 631 * @param resizeMode The {@link ResizeMode}. 632 */ setResizeMode(@esizeMode int resizeMode)633 public void setResizeMode(@ResizeMode int resizeMode) { 634 Assertions.checkStateNotNull(contentFrame); 635 contentFrame.setResizeMode(resizeMode); 636 } 637 638 /** Returns the {@link ResizeMode}. */ getResizeMode()639 public @ResizeMode int getResizeMode() { 640 Assertions.checkStateNotNull(contentFrame); 641 return contentFrame.getResizeMode(); 642 } 643 644 /** Returns whether artwork is displayed if present in the media. */ getUseArtwork()645 public boolean getUseArtwork() { 646 return useArtwork; 647 } 648 649 /** 650 * Sets whether artwork is displayed if present in the media. 651 * 652 * @param useArtwork Whether artwork is displayed. 653 */ setUseArtwork(boolean useArtwork)654 public void setUseArtwork(boolean useArtwork) { 655 Assertions.checkState(!useArtwork || artworkView != null); 656 if (this.useArtwork != useArtwork) { 657 this.useArtwork = useArtwork; 658 updateForCurrentTrackSelections(/* isNewPlayer= */ false); 659 } 660 } 661 662 /** Returns the default artwork to display. */ 663 @Nullable getDefaultArtwork()664 public Drawable getDefaultArtwork() { 665 return defaultArtwork; 666 } 667 668 /** 669 * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is 670 * present in the media. 671 * 672 * @param defaultArtwork the default artwork to display. 673 * @deprecated use (@link {@link #setDefaultArtwork(Drawable)} instead. 674 */ 675 @Deprecated setDefaultArtwork(@ullable Bitmap defaultArtwork)676 public void setDefaultArtwork(@Nullable Bitmap defaultArtwork) { 677 setDefaultArtwork( 678 defaultArtwork == null ? null : new BitmapDrawable(getResources(), defaultArtwork)); 679 } 680 681 /** 682 * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is 683 * present in the media. 684 * 685 * @param defaultArtwork the default artwork to display 686 */ setDefaultArtwork(@ullable Drawable defaultArtwork)687 public void setDefaultArtwork(@Nullable Drawable defaultArtwork) { 688 if (this.defaultArtwork != defaultArtwork) { 689 this.defaultArtwork = defaultArtwork; 690 updateForCurrentTrackSelections(/* isNewPlayer= */ false); 691 } 692 } 693 694 /** Returns whether the playback controls can be shown. */ getUseController()695 public boolean getUseController() { 696 return useController; 697 } 698 699 /** 700 * Sets whether the playback controls can be shown. If set to {@code false} the playback controls 701 * are never visible and are disconnected from the player. 702 * 703 * @param useController Whether the playback controls can be shown. 704 */ setUseController(boolean useController)705 public void setUseController(boolean useController) { 706 Assertions.checkState(!useController || controller != null); 707 if (this.useController == useController) { 708 return; 709 } 710 this.useController = useController; 711 if (useController()) { 712 controller.setPlayer(player); 713 } else if (controller != null) { 714 controller.hide(); 715 controller.setPlayer(/* player= */ null); 716 } 717 updateContentDescription(); 718 } 719 720 /** 721 * Sets the background color of the {@code exo_shutter} view. 722 * 723 * @param color The background color. 724 */ setShutterBackgroundColor(int color)725 public void setShutterBackgroundColor(int color) { 726 if (shutterView != null) { 727 shutterView.setBackgroundColor(color); 728 } 729 } 730 731 /** 732 * Sets whether the currently displayed video frame or media artwork is kept visible when the 733 * player is reset. A player reset is defined to mean the player being re-prepared with different 734 * media, the player transitioning to unprepared media, {@link Player#stop(boolean)} being called 735 * with {@code reset=true}, or the player being replaced or cleared by calling {@link 736 * #setPlayer(Player)}. 737 * 738 * <p>If enabled, the currently displayed video frame or media artwork will be kept visible until 739 * the player set on the view has been successfully prepared with new media and loaded enough of 740 * it to have determined the available tracks. Hence enabling this option allows transitioning 741 * from playing one piece of media to another, or from using one player instance to another, 742 * without clearing the view's content. 743 * 744 * <p>If disabled, the currently displayed video frame or media artwork will be hidden as soon as 745 * the player is reset. Note that the video frame is hidden by making {@code exo_shutter} visible. 746 * Hence the video frame will not be hidden if using a custom layout that omits this view. 747 * 748 * @param keepContentOnPlayerReset Whether the currently displayed video frame or media artwork is 749 * kept visible when the player is reset. 750 */ setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset)751 public void setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset) { 752 if (this.keepContentOnPlayerReset != keepContentOnPlayerReset) { 753 this.keepContentOnPlayerReset = keepContentOnPlayerReset; 754 updateForCurrentTrackSelections(/* isNewPlayer= */ false); 755 } 756 } 757 758 /** 759 * Sets whether to use the orientation sensor for rotation during spherical playbacks (if 760 * available) 761 * 762 * @param useSensorRotation Whether to use the orientation sensor for rotation during spherical 763 * playbacks. 764 */ setUseSensorRotation(boolean useSensorRotation)765 public void setUseSensorRotation(boolean useSensorRotation) { 766 if (this.useSensorRotation != useSensorRotation) { 767 this.useSensorRotation = useSensorRotation; 768 if (surfaceView instanceof SphericalGLSurfaceView) { 769 ((SphericalGLSurfaceView) surfaceView).setUseSensorRotation(useSensorRotation); 770 } 771 } 772 } 773 774 /** 775 * Sets whether a buffering spinner is displayed when the player is in the buffering state. The 776 * buffering spinner is not displayed by default. 777 * 778 * @deprecated Use {@link #setShowBuffering(int)} 779 * @param showBuffering Whether the buffering icon is displayed 780 */ 781 @Deprecated setShowBuffering(boolean showBuffering)782 public void setShowBuffering(boolean showBuffering) { 783 setShowBuffering(showBuffering ? SHOW_BUFFERING_WHEN_PLAYING : SHOW_BUFFERING_NEVER); 784 } 785 786 /** 787 * Sets whether a buffering spinner is displayed when the player is in the buffering state. The 788 * buffering spinner is not displayed by default. 789 * 790 * @param showBuffering The mode that defines when the buffering spinner is displayed. One of 791 * {@link #SHOW_BUFFERING_NEVER}, {@link #SHOW_BUFFERING_WHEN_PLAYING} and {@link 792 * #SHOW_BUFFERING_ALWAYS}. 793 */ setShowBuffering(@howBuffering int showBuffering)794 public void setShowBuffering(@ShowBuffering int showBuffering) { 795 if (this.showBuffering != showBuffering) { 796 this.showBuffering = showBuffering; 797 updateBuffering(); 798 } 799 } 800 801 /** 802 * Sets the optional {@link ErrorMessageProvider}. 803 * 804 * @param errorMessageProvider The error message provider. 805 */ setErrorMessageProvider( @ullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider)806 public void setErrorMessageProvider( 807 @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) { 808 if (this.errorMessageProvider != errorMessageProvider) { 809 this.errorMessageProvider = errorMessageProvider; 810 updateErrorMessage(); 811 } 812 } 813 814 /** 815 * Sets a custom error message to be displayed by the view. The error message will be displayed 816 * permanently, unless it is cleared by passing {@code null} to this method. 817 * 818 * @param message The message to display, or {@code null} to clear a previously set message. 819 */ setCustomErrorMessage(@ullable CharSequence message)820 public void setCustomErrorMessage(@Nullable CharSequence message) { 821 Assertions.checkState(errorMessageView != null); 822 customErrorMessage = message; 823 updateErrorMessage(); 824 } 825 826 @Override dispatchKeyEvent(KeyEvent event)827 public boolean dispatchKeyEvent(KeyEvent event) { 828 if (player != null && player.isPlayingAd()) { 829 return super.dispatchKeyEvent(event); 830 } 831 832 boolean isDpadKey = isDpadKey(event.getKeyCode()); 833 boolean handled = false; 834 if (isDpadKey && useController() && !controller.isVisible()) { 835 // Handle the key event by showing the controller. 836 maybeShowController(true); 837 handled = true; 838 } else if (dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event)) { 839 // The key event was handled as a media key or by the super class. We should also show the 840 // controller, or extend its show timeout if already visible. 841 maybeShowController(true); 842 handled = true; 843 } else if (isDpadKey && useController()) { 844 // The key event wasn't handled, but we should extend the controller's show timeout. 845 maybeShowController(true); 846 } 847 return handled; 848 } 849 850 /** 851 * Called to process media key events. Any {@link KeyEvent} can be passed but only media key 852 * events will be handled. Does nothing if playback controls are disabled. 853 * 854 * @param event A key event. 855 * @return Whether the key event was handled. 856 */ dispatchMediaKeyEvent(KeyEvent event)857 public boolean dispatchMediaKeyEvent(KeyEvent event) { 858 return useController() && controller.dispatchMediaKeyEvent(event); 859 } 860 861 /** Returns whether the controller is currently visible. */ isControllerVisible()862 public boolean isControllerVisible() { 863 return controller != null && controller.isVisible(); 864 } 865 866 /** 867 * Shows the playback controls. Does nothing if playback controls are disabled. 868 * 869 * <p>The playback controls are automatically hidden during playback after {{@link 870 * #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet, 871 * is paused, has ended or failed. 872 */ showController()873 public void showController() { 874 showController(shouldShowControllerIndefinitely()); 875 } 876 877 /** Hides the playback controls. Does nothing if playback controls are disabled. */ hideController()878 public void hideController() { 879 if (controller != null) { 880 controller.hide(); 881 } 882 } 883 884 /** 885 * Returns the playback controls timeout. The playback controls are automatically hidden after 886 * this duration of time has elapsed without user input and with playback or buffering in 887 * progress. 888 * 889 * @return The timeout in milliseconds. A non-positive value will cause the controller to remain 890 * visible indefinitely. 891 */ getControllerShowTimeoutMs()892 public int getControllerShowTimeoutMs() { 893 return controllerShowTimeoutMs; 894 } 895 896 /** 897 * Sets the playback controls timeout. The playback controls are automatically hidden after this 898 * duration of time has elapsed without user input and with playback or buffering in progress. 899 * 900 * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the 901 * controller to remain visible indefinitely. 902 */ setControllerShowTimeoutMs(int controllerShowTimeoutMs)903 public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { 904 Assertions.checkStateNotNull(controller); 905 this.controllerShowTimeoutMs = controllerShowTimeoutMs; 906 if (controller.isVisible()) { 907 // Update the controller's timeout if necessary. 908 showController(); 909 } 910 } 911 912 /** Returns whether the playback controls are hidden by touch events. */ getControllerHideOnTouch()913 public boolean getControllerHideOnTouch() { 914 return controllerHideOnTouch; 915 } 916 917 /** 918 * Sets whether the playback controls are hidden by touch events. 919 * 920 * @param controllerHideOnTouch Whether the playback controls are hidden by touch events. 921 */ setControllerHideOnTouch(boolean controllerHideOnTouch)922 public void setControllerHideOnTouch(boolean controllerHideOnTouch) { 923 Assertions.checkStateNotNull(controller); 924 this.controllerHideOnTouch = controllerHideOnTouch; 925 updateContentDescription(); 926 } 927 928 /** 929 * Returns whether the playback controls are automatically shown when playback starts, pauses, 930 * ends, or fails. If set to false, the playback controls can be manually operated with {@link 931 * #showController()} and {@link #hideController()}. 932 */ getControllerAutoShow()933 public boolean getControllerAutoShow() { 934 return controllerAutoShow; 935 } 936 937 /** 938 * Sets whether the playback controls are automatically shown when playback starts, pauses, ends, 939 * or fails. If set to false, the playback controls can be manually operated with {@link 940 * #showController()} and {@link #hideController()}. 941 * 942 * @param controllerAutoShow Whether the playback controls are allowed to show automatically. 943 */ setControllerAutoShow(boolean controllerAutoShow)944 public void setControllerAutoShow(boolean controllerAutoShow) { 945 this.controllerAutoShow = controllerAutoShow; 946 } 947 948 /** 949 * Sets whether the playback controls are hidden when ads are playing. Controls are always shown 950 * during ads if they are enabled and the player is paused. 951 * 952 * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing. 953 */ setControllerHideDuringAds(boolean controllerHideDuringAds)954 public void setControllerHideDuringAds(boolean controllerHideDuringAds) { 955 this.controllerHideDuringAds = controllerHideDuringAds; 956 } 957 958 /** 959 * Set the {@link PlayerControlView.VisibilityListener}. 960 * 961 * @param listener The listener to be notified about visibility changes, or null to remove the 962 * current listener. 963 */ setControllerVisibilityListener( @ullable PlayerControlView.VisibilityListener listener)964 public void setControllerVisibilityListener( 965 @Nullable PlayerControlView.VisibilityListener listener) { 966 Assertions.checkStateNotNull(controller); 967 if (this.controllerVisibilityListener == listener) { 968 return; 969 } 970 if (this.controllerVisibilityListener != null) { 971 controller.removeVisibilityListener(this.controllerVisibilityListener); 972 } 973 this.controllerVisibilityListener = listener; 974 if (listener != null) { 975 controller.addVisibilityListener(listener); 976 } 977 } 978 979 /** 980 * Sets the {@link PlaybackPreparer}. 981 * 982 * @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback 983 * preparer. 984 */ setPlaybackPreparer(@ullable PlaybackPreparer playbackPreparer)985 public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) { 986 Assertions.checkStateNotNull(controller); 987 controller.setPlaybackPreparer(playbackPreparer); 988 } 989 990 /** 991 * Sets the {@link ControlDispatcher}. 992 * 993 * @param controlDispatcher The {@link ControlDispatcher}. 994 */ setControlDispatcher(ControlDispatcher controlDispatcher)995 public void setControlDispatcher(ControlDispatcher controlDispatcher) { 996 Assertions.checkStateNotNull(controller); 997 controller.setControlDispatcher(controlDispatcher); 998 } 999 1000 /** 1001 * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link 1002 * DefaultControlDispatcher#DefaultControlDispatcher(long, long)}. 1003 */ 1004 @SuppressWarnings("deprecation") 1005 @Deprecated setRewindIncrementMs(int rewindMs)1006 public void setRewindIncrementMs(int rewindMs) { 1007 Assertions.checkStateNotNull(controller); 1008 controller.setRewindIncrementMs(rewindMs); 1009 } 1010 1011 /** 1012 * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link 1013 * DefaultControlDispatcher#DefaultControlDispatcher(long, long)}. 1014 */ 1015 @SuppressWarnings("deprecation") 1016 @Deprecated setFastForwardIncrementMs(int fastForwardMs)1017 public void setFastForwardIncrementMs(int fastForwardMs) { 1018 Assertions.checkStateNotNull(controller); 1019 controller.setFastForwardIncrementMs(fastForwardMs); 1020 } 1021 1022 /** 1023 * Sets which repeat toggle modes are enabled. 1024 * 1025 * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. 1026 */ setRepeatToggleModes(@epeatModeUtil.RepeatToggleModes int repeatToggleModes)1027 public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { 1028 Assertions.checkStateNotNull(controller); 1029 controller.setRepeatToggleModes(repeatToggleModes); 1030 } 1031 1032 /** 1033 * Sets whether the shuffle button is shown. 1034 * 1035 * @param showShuffleButton Whether the shuffle button is shown. 1036 */ setShowShuffleButton(boolean showShuffleButton)1037 public void setShowShuffleButton(boolean showShuffleButton) { 1038 Assertions.checkStateNotNull(controller); 1039 controller.setShowShuffleButton(showShuffleButton); 1040 } 1041 1042 /** 1043 * Sets whether the time bar should show all windows, as opposed to just the current one. 1044 * 1045 * @param showMultiWindowTimeBar Whether to show all windows. 1046 */ setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar)1047 public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { 1048 Assertions.checkStateNotNull(controller); 1049 controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar); 1050 } 1051 1052 /** 1053 * Sets the millisecond positions of extra ad markers relative to the start of the window (or 1054 * timeline, if in multi-window mode) and whether each extra ad has been played or not. The 1055 * markers are shown in addition to any ad markers for ads in the player's timeline. 1056 * 1057 * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or 1058 * {@code null} to show no extra ad markers. 1059 * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad 1060 * markers. 1061 */ setExtraAdGroupMarkers( @ullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups)1062 public void setExtraAdGroupMarkers( 1063 @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { 1064 Assertions.checkStateNotNull(controller); 1065 controller.setExtraAdGroupMarkers(extraAdGroupTimesMs, extraPlayedAdGroups); 1066 } 1067 1068 /** 1069 * Set the {@link AspectRatioFrameLayout.AspectRatioListener}. 1070 * 1071 * @param listener The listener to be notified about aspect ratios changes of the video content or 1072 * the content frame. 1073 */ setAspectRatioListener( @ullable AspectRatioFrameLayout.AspectRatioListener listener)1074 public void setAspectRatioListener( 1075 @Nullable AspectRatioFrameLayout.AspectRatioListener listener) { 1076 Assertions.checkStateNotNull(contentFrame); 1077 contentFrame.setAspectRatioListener(listener); 1078 } 1079 1080 /** 1081 * Gets the view onto which video is rendered. This is a: 1082 * 1083 * <ul> 1084 * <li>{@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code 1085 * surface_view}. 1086 * <li>{@link TextureView} if {@code surface_type} is {@code texture_view}. 1087 * <li>{@link SphericalGLSurfaceView} if {@code surface_type} is {@code 1088 * spherical_gl_surface_view}. 1089 * <li>{@link VideoDecoderGLSurfaceView} if {@code surface_type} is {@code 1090 * video_decoder_gl_surface_view}. 1091 * <li>{@code null} if {@code surface_type} is {@code none}. 1092 * </ul> 1093 * 1094 * @return The {@link SurfaceView}, {@link TextureView}, {@link SphericalGLSurfaceView}, {@link 1095 * VideoDecoderGLSurfaceView} or {@code null}. 1096 */ 1097 @Nullable getVideoSurfaceView()1098 public View getVideoSurfaceView() { 1099 return surfaceView; 1100 } 1101 1102 /** 1103 * Gets the overlay {@link FrameLayout}, which can be populated with UI elements to show on top of 1104 * the player. 1105 * 1106 * @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and 1107 * the overlay is not present. 1108 */ 1109 @Nullable getOverlayFrameLayout()1110 public FrameLayout getOverlayFrameLayout() { 1111 return overlayFrameLayout; 1112 } 1113 1114 /** 1115 * Gets the {@link SubtitleView}. 1116 * 1117 * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the 1118 * subtitle view is not present. 1119 */ 1120 @Nullable getSubtitleView()1121 public SubtitleView getSubtitleView() { 1122 return subtitleView; 1123 } 1124 1125 @Override onTouchEvent(MotionEvent event)1126 public boolean onTouchEvent(MotionEvent event) { 1127 if (!useController() || player == null) { 1128 return false; 1129 } 1130 switch (event.getAction()) { 1131 case MotionEvent.ACTION_DOWN: 1132 isTouching = true; 1133 return true; 1134 case MotionEvent.ACTION_UP: 1135 if (isTouching) { 1136 isTouching = false; 1137 performClick(); 1138 return true; 1139 } 1140 return false; 1141 default: 1142 return false; 1143 } 1144 } 1145 1146 @Override performClick()1147 public boolean performClick() { 1148 super.performClick(); 1149 return toggleControllerVisibility(); 1150 } 1151 1152 @Override onTrackballEvent(MotionEvent ev)1153 public boolean onTrackballEvent(MotionEvent ev) { 1154 if (!useController() || player == null) { 1155 return false; 1156 } 1157 maybeShowController(true); 1158 return true; 1159 } 1160 1161 /** 1162 * Should be called when the player is visible to the user and if {@code surface_type} is {@code 1163 * spherical_gl_surface_view}. It is the counterpart to {@link #onPause()}. 1164 * 1165 * <p>This method should typically be called in {@code Activity.onStart()}, or {@code 1166 * Activity.onResume()} for API versions <= 23. 1167 */ onResume()1168 public void onResume() { 1169 if (surfaceView instanceof SphericalGLSurfaceView) { 1170 ((SphericalGLSurfaceView) surfaceView).onResume(); 1171 } 1172 } 1173 1174 /** 1175 * Should be called when the player is no longer visible to the user and if {@code surface_type} 1176 * is {@code spherical_gl_surface_view}. It is the counterpart to {@link #onResume()}. 1177 * 1178 * <p>This method should typically be called in {@code Activity.onStop()}, or {@code 1179 * Activity.onPause()} for API versions <= 23. 1180 */ onPause()1181 public void onPause() { 1182 if (surfaceView instanceof SphericalGLSurfaceView) { 1183 ((SphericalGLSurfaceView) surfaceView).onPause(); 1184 } 1185 } 1186 1187 /** 1188 * Called when there's a change in the aspect ratio of the content being displayed. The default 1189 * implementation sets the aspect ratio of the content frame to that of the content, unless the 1190 * content view is a {@link SphericalGLSurfaceView} in which case the frame's aspect ratio is 1191 * cleared. 1192 * 1193 * @param contentAspectRatio The aspect ratio of the content. 1194 * @param contentFrame The content frame, or {@code null}. 1195 * @param contentView The view that holds the content being displayed, or {@code null}. 1196 */ onContentAspectRatioChanged( float contentAspectRatio, @Nullable AspectRatioFrameLayout contentFrame, @Nullable View contentView)1197 protected void onContentAspectRatioChanged( 1198 float contentAspectRatio, 1199 @Nullable AspectRatioFrameLayout contentFrame, 1200 @Nullable View contentView) { 1201 if (contentFrame != null) { 1202 contentFrame.setAspectRatio( 1203 contentView instanceof SphericalGLSurfaceView ? 0 : contentAspectRatio); 1204 } 1205 } 1206 1207 // AdsLoader.AdViewProvider implementation. 1208 1209 @Override getAdViewGroup()1210 public ViewGroup getAdViewGroup() { 1211 return Assertions.checkStateNotNull( 1212 adOverlayFrameLayout, "exo_ad_overlay must be present for ad playback"); 1213 } 1214 1215 @Override getAdOverlayViews()1216 public View[] getAdOverlayViews() { 1217 ArrayList<View> overlayViews = new ArrayList<>(); 1218 if (overlayFrameLayout != null) { 1219 overlayViews.add(overlayFrameLayout); 1220 } 1221 if (controller != null) { 1222 overlayViews.add(controller); 1223 } 1224 return overlayViews.toArray(new View[0]); 1225 } 1226 1227 // Internal methods. 1228 1229 @EnsuresNonNullIf(expression = "controller", result = true) useController()1230 private boolean useController() { 1231 if (useController) { 1232 Assertions.checkStateNotNull(controller); 1233 return true; 1234 } 1235 return false; 1236 } 1237 1238 @EnsuresNonNullIf(expression = "artworkView", result = true) useArtwork()1239 private boolean useArtwork() { 1240 if (useArtwork) { 1241 Assertions.checkStateNotNull(artworkView); 1242 return true; 1243 } 1244 return false; 1245 } 1246 toggleControllerVisibility()1247 private boolean toggleControllerVisibility() { 1248 if (!useController() || player == null) { 1249 return false; 1250 } 1251 if (!controller.isVisible()) { 1252 maybeShowController(true); 1253 } else if (controllerHideOnTouch) { 1254 controller.hide(); 1255 } 1256 return true; 1257 } 1258 1259 /** Shows the playback controls, but only if forced or shown indefinitely. */ maybeShowController(boolean isForced)1260 private void maybeShowController(boolean isForced) { 1261 if (isPlayingAd() && controllerHideDuringAds) { 1262 return; 1263 } 1264 if (useController()) { 1265 boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; 1266 boolean shouldShowIndefinitely = shouldShowControllerIndefinitely(); 1267 if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) { 1268 showController(shouldShowIndefinitely); 1269 } 1270 } 1271 } 1272 shouldShowControllerIndefinitely()1273 private boolean shouldShowControllerIndefinitely() { 1274 if (player == null) { 1275 return true; 1276 } 1277 int playbackState = player.getPlaybackState(); 1278 return controllerAutoShow 1279 && (playbackState == Player.STATE_IDLE 1280 || playbackState == Player.STATE_ENDED 1281 || !player.getPlayWhenReady()); 1282 } 1283 showController(boolean showIndefinitely)1284 private void showController(boolean showIndefinitely) { 1285 if (!useController()) { 1286 return; 1287 } 1288 controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs); 1289 controller.show(); 1290 } 1291 isPlayingAd()1292 private boolean isPlayingAd() { 1293 return player != null && player.isPlayingAd() && player.getPlayWhenReady(); 1294 } 1295 updateForCurrentTrackSelections(boolean isNewPlayer)1296 private void updateForCurrentTrackSelections(boolean isNewPlayer) { 1297 @Nullable Player player = this.player; 1298 if (player == null || player.getCurrentTrackGroups().isEmpty()) { 1299 if (!keepContentOnPlayerReset) { 1300 hideArtwork(); 1301 closeShutter(); 1302 } 1303 return; 1304 } 1305 1306 if (isNewPlayer && !keepContentOnPlayerReset) { 1307 // Hide any video from the previous player. 1308 closeShutter(); 1309 } 1310 1311 TrackSelectionArray selections = player.getCurrentTrackSelections(); 1312 for (int i = 0; i < selections.length; i++) { 1313 if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { 1314 // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in 1315 // onRenderedFirstFrame(). 1316 hideArtwork(); 1317 return; 1318 } 1319 } 1320 1321 // Video disabled so the shutter must be closed. 1322 closeShutter(); 1323 // Display artwork if enabled and available, else hide it. 1324 if (useArtwork()) { 1325 for (int i = 0; i < selections.length; i++) { 1326 @Nullable TrackSelection selection = selections.get(i); 1327 if (selection != null) { 1328 for (int j = 0; j < selection.length(); j++) { 1329 @Nullable Metadata metadata = selection.getFormat(j).metadata; 1330 if (metadata != null && setArtworkFromMetadata(metadata)) { 1331 return; 1332 } 1333 } 1334 } 1335 } 1336 if (setDrawableArtwork(defaultArtwork)) { 1337 return; 1338 } 1339 } 1340 // Artwork disabled or unavailable. 1341 hideArtwork(); 1342 } 1343 1344 @RequiresNonNull("artworkView") setArtworkFromMetadata(Metadata metadata)1345 private boolean setArtworkFromMetadata(Metadata metadata) { 1346 boolean isArtworkSet = false; 1347 int currentPictureType = PICTURE_TYPE_NOT_SET; 1348 for (int i = 0; i < metadata.length(); i++) { 1349 Metadata.Entry metadataEntry = metadata.get(i); 1350 int pictureType; 1351 byte[] bitmapData; 1352 if (metadataEntry instanceof ApicFrame) { 1353 bitmapData = ((ApicFrame) metadataEntry).pictureData; 1354 pictureType = ((ApicFrame) metadataEntry).pictureType; 1355 } else if (metadataEntry instanceof PictureFrame) { 1356 bitmapData = ((PictureFrame) metadataEntry).pictureData; 1357 pictureType = ((PictureFrame) metadataEntry).pictureType; 1358 } else { 1359 continue; 1360 } 1361 // Prefer the first front cover picture. If there aren't any, prefer the first picture. 1362 if (currentPictureType == PICTURE_TYPE_NOT_SET || pictureType == PICTURE_TYPE_FRONT_COVER) { 1363 Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); 1364 isArtworkSet = setDrawableArtwork(new BitmapDrawable(getResources(), bitmap)); 1365 currentPictureType = pictureType; 1366 if (currentPictureType == PICTURE_TYPE_FRONT_COVER) { 1367 break; 1368 } 1369 } 1370 } 1371 return isArtworkSet; 1372 } 1373 1374 @RequiresNonNull("artworkView") setDrawableArtwork(@ullable Drawable drawable)1375 private boolean setDrawableArtwork(@Nullable Drawable drawable) { 1376 if (drawable != null) { 1377 int drawableWidth = drawable.getIntrinsicWidth(); 1378 int drawableHeight = drawable.getIntrinsicHeight(); 1379 if (drawableWidth > 0 && drawableHeight > 0) { 1380 float artworkAspectRatio = (float) drawableWidth / drawableHeight; 1381 onContentAspectRatioChanged(artworkAspectRatio, contentFrame, artworkView); 1382 artworkView.setImageDrawable(drawable); 1383 artworkView.setVisibility(VISIBLE); 1384 return true; 1385 } 1386 } 1387 return false; 1388 } 1389 hideArtwork()1390 private void hideArtwork() { 1391 if (artworkView != null) { 1392 artworkView.setImageResource(android.R.color.transparent); // Clears any bitmap reference. 1393 artworkView.setVisibility(INVISIBLE); 1394 } 1395 } 1396 closeShutter()1397 private void closeShutter() { 1398 if (shutterView != null) { 1399 shutterView.setVisibility(View.VISIBLE); 1400 } 1401 } 1402 updateBuffering()1403 private void updateBuffering() { 1404 if (bufferingView != null) { 1405 boolean showBufferingSpinner = 1406 player != null 1407 && player.getPlaybackState() == Player.STATE_BUFFERING 1408 && (showBuffering == SHOW_BUFFERING_ALWAYS 1409 || (showBuffering == SHOW_BUFFERING_WHEN_PLAYING && player.getPlayWhenReady())); 1410 bufferingView.setVisibility(showBufferingSpinner ? View.VISIBLE : View.GONE); 1411 } 1412 } 1413 updateErrorMessage()1414 private void updateErrorMessage() { 1415 if (errorMessageView != null) { 1416 if (customErrorMessage != null) { 1417 errorMessageView.setText(customErrorMessage); 1418 errorMessageView.setVisibility(View.VISIBLE); 1419 return; 1420 } 1421 @Nullable ExoPlaybackException error = player != null ? player.getPlayerError() : null; 1422 if (error != null && errorMessageProvider != null) { 1423 CharSequence errorMessage = errorMessageProvider.getErrorMessage(error).second; 1424 errorMessageView.setText(errorMessage); 1425 errorMessageView.setVisibility(View.VISIBLE); 1426 } else { 1427 errorMessageView.setVisibility(View.GONE); 1428 } 1429 } 1430 } 1431 updateContentDescription()1432 private void updateContentDescription() { 1433 if (controller == null || !useController) { 1434 setContentDescription(/* contentDescription= */ null); 1435 } else if (controller.getVisibility() == View.VISIBLE) { 1436 setContentDescription( 1437 /* contentDescription= */ controllerHideOnTouch 1438 ? getResources().getString(R.string.exo_controls_hide) 1439 : null); 1440 } else { 1441 setContentDescription( 1442 /* contentDescription= */ getResources().getString(R.string.exo_controls_show)); 1443 } 1444 } 1445 updateControllerVisibility()1446 private void updateControllerVisibility() { 1447 if (isPlayingAd() && controllerHideDuringAds) { 1448 hideController(); 1449 } else { 1450 maybeShowController(false); 1451 } 1452 } 1453 1454 @RequiresApi(23) configureEditModeLogoV23(Resources resources, ImageView logo)1455 private static void configureEditModeLogoV23(Resources resources, ImageView logo) { 1456 logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null)); 1457 logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null)); 1458 } 1459 configureEditModeLogo(Resources resources, ImageView logo)1460 private static void configureEditModeLogo(Resources resources, ImageView logo) { 1461 logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo)); 1462 logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color)); 1463 } 1464 1465 @SuppressWarnings("ResourceType") setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode)1466 private static void setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode) { 1467 aspectRatioFrame.setResizeMode(resizeMode); 1468 } 1469 1470 /** Applies a texture rotation to a {@link TextureView}. */ applyTextureViewRotation(TextureView textureView, int textureViewRotation)1471 private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) { 1472 Matrix transformMatrix = new Matrix(); 1473 float textureViewWidth = textureView.getWidth(); 1474 float textureViewHeight = textureView.getHeight(); 1475 if (textureViewWidth != 0 && textureViewHeight != 0 && textureViewRotation != 0) { 1476 float pivotX = textureViewWidth / 2; 1477 float pivotY = textureViewHeight / 2; 1478 transformMatrix.postRotate(textureViewRotation, pivotX, pivotY); 1479 1480 // After rotation, scale the rotated texture to fit the TextureView size. 1481 RectF originalTextureRect = new RectF(0, 0, textureViewWidth, textureViewHeight); 1482 RectF rotatedTextureRect = new RectF(); 1483 transformMatrix.mapRect(rotatedTextureRect, originalTextureRect); 1484 transformMatrix.postScale( 1485 textureViewWidth / rotatedTextureRect.width(), 1486 textureViewHeight / rotatedTextureRect.height(), 1487 pivotX, 1488 pivotY); 1489 } 1490 textureView.setTransform(transformMatrix); 1491 } 1492 1493 @SuppressLint("InlinedApi") isDpadKey(int keyCode)1494 private boolean isDpadKey(int keyCode) { 1495 return keyCode == KeyEvent.KEYCODE_DPAD_UP 1496 || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT 1497 || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT 1498 || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT 1499 || keyCode == KeyEvent.KEYCODE_DPAD_DOWN 1500 || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT 1501 || keyCode == KeyEvent.KEYCODE_DPAD_LEFT 1502 || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT 1503 || keyCode == KeyEvent.KEYCODE_DPAD_CENTER; 1504 } 1505 1506 private final class ComponentListener 1507 implements Player.EventListener, 1508 TextOutput, 1509 VideoListener, 1510 OnLayoutChangeListener, 1511 SingleTapListener, 1512 PlayerControlView.VisibilityListener { 1513 1514 // TextOutput implementation 1515 1516 @Override onCues(List<Cue> cues)1517 public void onCues(List<Cue> cues) { 1518 if (subtitleView != null) { 1519 subtitleView.onCues(cues); 1520 } 1521 } 1522 1523 // VideoListener implementation 1524 1525 @Override onVideoSizeChanged( int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio)1526 public void onVideoSizeChanged( 1527 int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { 1528 float videoAspectRatio = 1529 (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height; 1530 1531 if (surfaceView instanceof TextureView) { 1532 // Try to apply rotation transformation when our surface is a TextureView. 1533 if (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) { 1534 // We will apply a rotation 90/270 degree to the output texture of the TextureView. 1535 // In this case, the output video's width and height will be swapped. 1536 videoAspectRatio = 1 / videoAspectRatio; 1537 } 1538 if (textureViewRotation != 0) { 1539 surfaceView.removeOnLayoutChangeListener(this); 1540 } 1541 textureViewRotation = unappliedRotationDegrees; 1542 if (textureViewRotation != 0) { 1543 // The texture view's dimensions might be changed after layout step. 1544 // So add an OnLayoutChangeListener to apply rotation after layout step. 1545 surfaceView.addOnLayoutChangeListener(this); 1546 } 1547 applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); 1548 } 1549 1550 onContentAspectRatioChanged(videoAspectRatio, contentFrame, surfaceView); 1551 } 1552 1553 @Override onRenderedFirstFrame()1554 public void onRenderedFirstFrame() { 1555 if (shutterView != null) { 1556 shutterView.setVisibility(INVISIBLE); 1557 } 1558 } 1559 1560 @Override onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections)1561 public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { 1562 updateForCurrentTrackSelections(/* isNewPlayer= */ false); 1563 } 1564 1565 // Player.EventListener implementation 1566 1567 @Override onPlaybackStateChanged(@layer.State int playbackState)1568 public void onPlaybackStateChanged(@Player.State int playbackState) { 1569 updateBuffering(); 1570 updateErrorMessage(); 1571 updateControllerVisibility(); 1572 } 1573 1574 @Override onPlayWhenReadyChanged( boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason)1575 public void onPlayWhenReadyChanged( 1576 boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { 1577 updateBuffering(); 1578 updateControllerVisibility(); 1579 } 1580 1581 @Override onPositionDiscontinuity(@iscontinuityReason int reason)1582 public void onPositionDiscontinuity(@DiscontinuityReason int reason) { 1583 if (isPlayingAd() && controllerHideDuringAds) { 1584 hideController(); 1585 } 1586 } 1587 1588 // OnLayoutChangeListener implementation 1589 1590 @Override onLayoutChange( View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)1591 public void onLayoutChange( 1592 View view, 1593 int left, 1594 int top, 1595 int right, 1596 int bottom, 1597 int oldLeft, 1598 int oldTop, 1599 int oldRight, 1600 int oldBottom) { 1601 applyTextureViewRotation((TextureView) view, textureViewRotation); 1602 } 1603 1604 // SingleTapListener implementation 1605 1606 @Override onSingleTapUp(MotionEvent e)1607 public boolean onSingleTapUp(MotionEvent e) { 1608 return toggleControllerVisibility(); 1609 } 1610 1611 // PlayerControlView.VisibilityListener implementation 1612 1613 @Override onVisibilityChange(int visibility)1614 public void onVisibilityChange(int visibility) { 1615 updateContentDescription(); 1616 } 1617 } 1618 } 1619