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; 17 18 import static com.google.android.exoplayer2.util.Assertions.checkNotNull; 19 20 import android.annotation.SuppressLint; 21 import android.os.Handler; 22 import android.os.Looper; 23 import android.os.Message; 24 import android.util.Pair; 25 import androidx.annotation.Nullable; 26 import com.google.android.exoplayer2.PlayerMessage.Target; 27 import com.google.android.exoplayer2.analytics.AnalyticsCollector; 28 import com.google.android.exoplayer2.source.MediaSource; 29 import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; 30 import com.google.android.exoplayer2.source.MediaSourceFactory; 31 import com.google.android.exoplayer2.source.ShuffleOrder; 32 import com.google.android.exoplayer2.source.TrackGroupArray; 33 import com.google.android.exoplayer2.source.ads.AdsMediaSource; 34 import com.google.android.exoplayer2.trackselection.TrackSelection; 35 import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 36 import com.google.android.exoplayer2.trackselection.TrackSelector; 37 import com.google.android.exoplayer2.trackselection.TrackSelectorResult; 38 import com.google.android.exoplayer2.upstream.BandwidthMeter; 39 import com.google.android.exoplayer2.util.Assertions; 40 import com.google.android.exoplayer2.util.Clock; 41 import com.google.android.exoplayer2.util.Log; 42 import com.google.android.exoplayer2.util.Util; 43 import java.util.ArrayDeque; 44 import java.util.ArrayList; 45 import java.util.Collections; 46 import java.util.List; 47 import java.util.concurrent.CopyOnWriteArrayList; 48 import java.util.concurrent.TimeoutException; 49 50 /** 51 * An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayer.Builder}. 52 */ 53 /* package */ final class ExoPlayerImpl extends BasePlayer implements ExoPlayer { 54 55 private static final String TAG = "ExoPlayerImpl"; 56 57 /** 58 * This empty track selector result can only be used for {@link PlaybackInfo#trackSelectorResult} 59 * when the player does not have any track selection made (such as when player is reset, or when 60 * player seeks to an unprepared period). It will not be used as result of any {@link 61 * TrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)} 62 * operation. 63 */ 64 /* package */ final TrackSelectorResult emptyTrackSelectorResult; 65 66 private final Renderer[] renderers; 67 private final TrackSelector trackSelector; 68 private final Handler eventHandler; 69 private final ExoPlayerImplInternal internalPlayer; 70 private final Handler internalPlayerHandler; 71 private final CopyOnWriteArrayList<ListenerHolder> listeners; 72 private final Timeline.Period period; 73 private final ArrayDeque<Runnable> pendingListenerNotifications; 74 private final List<MediaSourceList.MediaSourceHolder> mediaSourceHolders; 75 private final boolean useLazyPreparation; 76 private final MediaSourceFactory mediaSourceFactory; 77 78 @RepeatMode private int repeatMode; 79 private boolean shuffleModeEnabled; 80 private int pendingOperationAcks; 81 private boolean hasPendingDiscontinuity; 82 @DiscontinuityReason private int pendingDiscontinuityReason; 83 @PlayWhenReadyChangeReason private int pendingPlayWhenReadyChangeReason; 84 private boolean foregroundMode; 85 private int pendingSetPlaybackSpeedAcks; 86 private float playbackSpeed; 87 private SeekParameters seekParameters; 88 private ShuffleOrder shuffleOrder; 89 private boolean pauseAtEndOfMediaItems; 90 private boolean hasAdsMediaSource; 91 92 // Playback information when there is no pending seek/set source operation. 93 private PlaybackInfo playbackInfo; 94 95 // Playback information when there is a pending seek/set source operation. 96 private int maskingWindowIndex; 97 private int maskingPeriodIndex; 98 private long maskingWindowPositionMs; 99 100 /** 101 * Constructs an instance. Must be called from a thread that has an associated {@link Looper}. 102 * 103 * @param renderers The {@link Renderer}s. 104 * @param trackSelector The {@link TrackSelector}. 105 * @param mediaSourceFactory The {@link MediaSourceFactory}. 106 * @param loadControl The {@link LoadControl}. 107 * @param bandwidthMeter The {@link BandwidthMeter}. 108 * @param analyticsCollector The {@link AnalyticsCollector}. 109 * @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest 110 * loads and other initial preparation steps happen immediately. If true, these initial 111 * preparations are triggered only when the player starts buffering the media. 112 * @param clock The {@link Clock}. 113 * @param looper The {@link Looper} which must be used for all calls to the player and which is 114 * used to call listeners on. 115 */ 116 @SuppressLint("HandlerLeak") ExoPlayerImpl( Renderer[] renderers, TrackSelector trackSelector, MediaSourceFactory mediaSourceFactory, LoadControl loadControl, BandwidthMeter bandwidthMeter, @Nullable AnalyticsCollector analyticsCollector, boolean useLazyPreparation, Clock clock, Looper looper)117 public ExoPlayerImpl( 118 Renderer[] renderers, 119 TrackSelector trackSelector, 120 MediaSourceFactory mediaSourceFactory, 121 LoadControl loadControl, 122 BandwidthMeter bandwidthMeter, 123 @Nullable AnalyticsCollector analyticsCollector, 124 boolean useLazyPreparation, 125 Clock clock, 126 Looper looper) { 127 Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " [" 128 + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]"); 129 Assertions.checkState(renderers.length > 0); 130 this.renderers = checkNotNull(renderers); 131 this.trackSelector = checkNotNull(trackSelector); 132 this.mediaSourceFactory = mediaSourceFactory; 133 this.useLazyPreparation = useLazyPreparation; 134 repeatMode = Player.REPEAT_MODE_OFF; 135 listeners = new CopyOnWriteArrayList<>(); 136 mediaSourceHolders = new ArrayList<>(); 137 shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); 138 emptyTrackSelectorResult = 139 new TrackSelectorResult( 140 new RendererConfiguration[renderers.length], 141 new TrackSelection[renderers.length], 142 null); 143 period = new Timeline.Period(); 144 playbackSpeed = Player.DEFAULT_PLAYBACK_SPEED; 145 seekParameters = SeekParameters.DEFAULT; 146 maskingWindowIndex = C.INDEX_UNSET; 147 eventHandler = 148 new Handler(looper) { 149 @Override 150 public void handleMessage(Message msg) { 151 ExoPlayerImpl.this.handleEvent(msg); 152 } 153 }; 154 playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult); 155 pendingListenerNotifications = new ArrayDeque<>(); 156 if (analyticsCollector != null) { 157 analyticsCollector.setPlayer(this); 158 } 159 internalPlayer = 160 new ExoPlayerImplInternal( 161 renderers, 162 trackSelector, 163 emptyTrackSelectorResult, 164 loadControl, 165 bandwidthMeter, 166 repeatMode, 167 shuffleModeEnabled, 168 analyticsCollector, 169 eventHandler, 170 clock); 171 internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); 172 } 173 174 /** 175 * Set a limit on the time a call to {@link #release()} can spend. If a call to {@link #release()} 176 * takes more than {@code timeoutMs} milliseconds to complete, the player will raise an error via 177 * {@link Player.EventListener#onPlayerError}. 178 * 179 * <p>This method is experimental, and will be renamed or removed in a future release. It should 180 * only be called before the player is used. 181 * 182 * @param timeoutMs The time limit in milliseconds, or 0 for no limit. 183 */ experimental_setReleaseTimeoutMs(long timeoutMs)184 public void experimental_setReleaseTimeoutMs(long timeoutMs) { 185 internalPlayer.experimental_setReleaseTimeoutMs(timeoutMs); 186 } 187 188 /** 189 * Configures the player to throw when it detects it's stuck buffering. 190 * 191 * <p>This method is experimental, and will be renamed or removed in a future release. It should 192 * only be called before the player is used. 193 */ experimental_throwWhenStuckBuffering()194 public void experimental_throwWhenStuckBuffering() { 195 internalPlayer.experimental_throwWhenStuckBuffering(); 196 } 197 198 @Override 199 @Nullable getAudioComponent()200 public AudioComponent getAudioComponent() { 201 return null; 202 } 203 204 @Override 205 @Nullable getVideoComponent()206 public VideoComponent getVideoComponent() { 207 return null; 208 } 209 210 @Override 211 @Nullable getTextComponent()212 public TextComponent getTextComponent() { 213 return null; 214 } 215 216 @Override 217 @Nullable getMetadataComponent()218 public MetadataComponent getMetadataComponent() { 219 return null; 220 } 221 222 @Override 223 @Nullable getDeviceComponent()224 public DeviceComponent getDeviceComponent() { 225 return null; 226 } 227 228 @Override getPlaybackLooper()229 public Looper getPlaybackLooper() { 230 return internalPlayer.getPlaybackLooper(); 231 } 232 233 @Override getApplicationLooper()234 public Looper getApplicationLooper() { 235 return eventHandler.getLooper(); 236 } 237 238 @Override addListener(Player.EventListener listener)239 public void addListener(Player.EventListener listener) { 240 listeners.addIfAbsent(new ListenerHolder(listener)); 241 } 242 243 @Override removeListener(Player.EventListener listener)244 public void removeListener(Player.EventListener listener) { 245 for (ListenerHolder listenerHolder : listeners) { 246 if (listenerHolder.listener.equals(listener)) { 247 listenerHolder.release(); 248 listeners.remove(listenerHolder); 249 } 250 } 251 } 252 253 @Override 254 @State getPlaybackState()255 public int getPlaybackState() { 256 return playbackInfo.playbackState; 257 } 258 259 @Override 260 @PlaybackSuppressionReason getPlaybackSuppressionReason()261 public int getPlaybackSuppressionReason() { 262 return playbackInfo.playbackSuppressionReason; 263 } 264 265 @Deprecated 266 @Override 267 @Nullable getPlaybackError()268 public ExoPlaybackException getPlaybackError() { 269 return getPlayerError(); 270 } 271 272 @Override 273 @Nullable getPlayerError()274 public ExoPlaybackException getPlayerError() { 275 return playbackInfo.playbackError; 276 } 277 278 /** @deprecated Use {@link #prepare()} instead. */ 279 @Deprecated 280 @Override retry()281 public void retry() { 282 prepare(); 283 } 284 285 @Override prepare()286 public void prepare() { 287 if (playbackInfo.playbackState != Player.STATE_IDLE) { 288 return; 289 } 290 PlaybackInfo playbackInfo = 291 getResetPlaybackInfo( 292 /* clearPlaylist= */ false, 293 /* resetError= */ true, 294 /* playbackState= */ this.playbackInfo.timeline.isEmpty() 295 ? Player.STATE_ENDED 296 : Player.STATE_BUFFERING); 297 // Trigger internal prepare first before updating the playback info and notifying external 298 // listeners to ensure that new operations issued in the listener notifications reach the 299 // player after this prepare. The internal player can't change the playback info immediately 300 // because it uses a callback. 301 pendingOperationAcks++; 302 internalPlayer.prepare(); 303 updatePlaybackInfo( 304 playbackInfo, 305 /* positionDiscontinuity= */ false, 306 /* ignored */ DISCONTINUITY_REASON_INTERNAL, 307 /* ignored */ TIMELINE_CHANGE_REASON_SOURCE_UPDATE, 308 /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, 309 /* seekProcessed= */ false); 310 } 311 312 /** 313 * @deprecated Use {@link #setMediaSource(MediaSource)} and {@link ExoPlayer#prepare()} instead. 314 */ 315 @Deprecated 316 @Override prepare(MediaSource mediaSource)317 public void prepare(MediaSource mediaSource) { 318 setMediaSource(mediaSource); 319 prepare(); 320 } 321 322 /** 323 * @deprecated Use {@link #setMediaSource(MediaSource, boolean)} and {@link ExoPlayer#prepare()} 324 * instead. 325 */ 326 @Deprecated 327 @Override prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState)328 public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { 329 setMediaSource(mediaSource, resetPosition); 330 prepare(); 331 } 332 333 @Override setMediaItems( List<MediaItem> mediaItems, int startWindowIndex, long startPositionMs)334 public void setMediaItems( 335 List<MediaItem> mediaItems, int startWindowIndex, long startPositionMs) { 336 setMediaSources(createMediaSources(mediaItems), startWindowIndex, startPositionMs); 337 } 338 339 @Override setMediaSource(MediaSource mediaSource)340 public void setMediaSource(MediaSource mediaSource) { 341 setMediaSources(Collections.singletonList(mediaSource)); 342 } 343 344 @Override setMediaSource(MediaSource mediaSource, long startPositionMs)345 public void setMediaSource(MediaSource mediaSource, long startPositionMs) { 346 setMediaSources( 347 Collections.singletonList(mediaSource), /* startWindowIndex= */ 0, startPositionMs); 348 } 349 350 @Override setMediaSource(MediaSource mediaSource, boolean resetPosition)351 public void setMediaSource(MediaSource mediaSource, boolean resetPosition) { 352 setMediaSources(Collections.singletonList(mediaSource), resetPosition); 353 } 354 355 @Override setMediaSources(List<MediaSource> mediaSources)356 public void setMediaSources(List<MediaSource> mediaSources) { 357 setMediaSources(mediaSources, /* resetPosition= */ true); 358 } 359 360 @Override setMediaSources(List<MediaSource> mediaSources, boolean resetPosition)361 public void setMediaSources(List<MediaSource> mediaSources, boolean resetPosition) { 362 setMediaSourcesInternal( 363 mediaSources, 364 /* startWindowIndex= */ C.INDEX_UNSET, 365 /* startPositionMs= */ C.TIME_UNSET, 366 /* resetToDefaultPosition= */ resetPosition); 367 } 368 369 @Override setMediaSources( List<MediaSource> mediaSources, int startWindowIndex, long startPositionMs)370 public void setMediaSources( 371 List<MediaSource> mediaSources, int startWindowIndex, long startPositionMs) { 372 setMediaSourcesInternal( 373 mediaSources, startWindowIndex, startPositionMs, /* resetToDefaultPosition= */ false); 374 } 375 376 @Override addMediaItems(List<MediaItem> mediaItems)377 public void addMediaItems(List<MediaItem> mediaItems) { 378 addMediaItems(/* index= */ mediaSourceHolders.size(), mediaItems); 379 } 380 381 @Override addMediaItems(int index, List<MediaItem> mediaItems)382 public void addMediaItems(int index, List<MediaItem> mediaItems) { 383 addMediaSources(index, createMediaSources(mediaItems)); 384 } 385 386 @Override addMediaSource(MediaSource mediaSource)387 public void addMediaSource(MediaSource mediaSource) { 388 addMediaSources(Collections.singletonList(mediaSource)); 389 } 390 391 @Override addMediaSource(int index, MediaSource mediaSource)392 public void addMediaSource(int index, MediaSource mediaSource) { 393 addMediaSources(index, Collections.singletonList(mediaSource)); 394 } 395 396 @Override addMediaSources(List<MediaSource> mediaSources)397 public void addMediaSources(List<MediaSource> mediaSources) { 398 addMediaSources(/* index= */ mediaSourceHolders.size(), mediaSources); 399 } 400 401 @Override addMediaSources(int index, List<MediaSource> mediaSources)402 public void addMediaSources(int index, List<MediaSource> mediaSources) { 403 Assertions.checkArgument(index >= 0); 404 validateMediaSources(mediaSources, /* mediaSourceReplacement= */ false); 405 int currentWindowIndex = getCurrentWindowIndex(); 406 long currentPositionMs = getCurrentPosition(); 407 Timeline oldTimeline = getCurrentTimeline(); 408 pendingOperationAcks++; 409 List<MediaSourceList.MediaSourceHolder> holders = addMediaSourceHolders(index, mediaSources); 410 PlaybackInfo playbackInfo = 411 maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); 412 internalPlayer.addMediaSources(index, holders, shuffleOrder); 413 updatePlaybackInfo( 414 playbackInfo, 415 /* positionDiscontinuity= */ false, 416 /* ignored */ DISCONTINUITY_REASON_INTERNAL, 417 /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, 418 /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, 419 /* seekProcessed= */ false); 420 } 421 422 @Override removeMediaItems(int fromIndex, int toIndex)423 public void removeMediaItems(int fromIndex, int toIndex) { 424 Assertions.checkArgument(toIndex > fromIndex); 425 removeMediaItemsInternal(fromIndex, toIndex); 426 } 427 428 @Override moveMediaItems(int fromIndex, int toIndex, int newFromIndex)429 public void moveMediaItems(int fromIndex, int toIndex, int newFromIndex) { 430 Assertions.checkArgument( 431 fromIndex >= 0 432 && fromIndex <= toIndex 433 && toIndex <= mediaSourceHolders.size() 434 && newFromIndex >= 0); 435 int currentWindowIndex = getCurrentWindowIndex(); 436 long currentPositionMs = getCurrentPosition(); 437 Timeline oldTimeline = getCurrentTimeline(); 438 pendingOperationAcks++; 439 newFromIndex = Math.min(newFromIndex, mediaSourceHolders.size() - (toIndex - fromIndex)); 440 MediaSourceList.moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex); 441 PlaybackInfo playbackInfo = 442 maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); 443 internalPlayer.moveMediaSources(fromIndex, toIndex, newFromIndex, shuffleOrder); 444 updatePlaybackInfo( 445 playbackInfo, 446 /* positionDiscontinuity= */ false, 447 /* ignored */ DISCONTINUITY_REASON_INTERNAL, 448 /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, 449 /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, 450 /* seekProcessed= */ false); 451 } 452 453 @Override clearMediaItems()454 public void clearMediaItems() { 455 if (mediaSourceHolders.isEmpty()) { 456 return; 457 } 458 removeMediaItemsInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size()); 459 } 460 461 @Override setShuffleOrder(ShuffleOrder shuffleOrder)462 public void setShuffleOrder(ShuffleOrder shuffleOrder) { 463 PlaybackInfo playbackInfo = maskTimeline(); 464 maskWithCurrentPosition(); 465 pendingOperationAcks++; 466 this.shuffleOrder = shuffleOrder; 467 internalPlayer.setShuffleOrder(shuffleOrder); 468 updatePlaybackInfo( 469 playbackInfo, 470 /* positionDiscontinuity= */ false, 471 /* ignored */ DISCONTINUITY_REASON_INTERNAL, 472 /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, 473 /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, 474 /* seekProcessed= */ false); 475 } 476 477 @Override setPlayWhenReady(boolean playWhenReady)478 public void setPlayWhenReady(boolean playWhenReady) { 479 setPlayWhenReady( 480 playWhenReady, 481 PLAYBACK_SUPPRESSION_REASON_NONE, 482 PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); 483 } 484 485 @Override setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems)486 public void setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems) { 487 if (this.pauseAtEndOfMediaItems == pauseAtEndOfMediaItems) { 488 return; 489 } 490 this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems; 491 internalPlayer.setPauseAtEndOfWindow(pauseAtEndOfMediaItems); 492 } 493 494 @Override getPauseAtEndOfMediaItems()495 public boolean getPauseAtEndOfMediaItems() { 496 return pauseAtEndOfMediaItems; 497 } 498 setPlayWhenReady( boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason, @PlayWhenReadyChangeReason int playWhenReadyChangeReason)499 public void setPlayWhenReady( 500 boolean playWhenReady, 501 @PlaybackSuppressionReason int playbackSuppressionReason, 502 @PlayWhenReadyChangeReason int playWhenReadyChangeReason) { 503 if (playbackInfo.playWhenReady == playWhenReady 504 && playbackInfo.playbackSuppressionReason == playbackSuppressionReason) { 505 return; 506 } 507 maskWithCurrentPosition(); 508 pendingOperationAcks++; 509 PlaybackInfo playbackInfo = 510 this.playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason); 511 internalPlayer.setPlayWhenReady(playWhenReady, playbackSuppressionReason); 512 updatePlaybackInfo( 513 playbackInfo, 514 /* positionDiscontinuity= */ false, 515 /* ignored */ DISCONTINUITY_REASON_INTERNAL, 516 /* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, 517 playWhenReadyChangeReason, 518 /* seekProcessed= */ false); 519 } 520 521 @Override getPlayWhenReady()522 public boolean getPlayWhenReady() { 523 return playbackInfo.playWhenReady; 524 } 525 526 @Override setRepeatMode(@epeatMode int repeatMode)527 public void setRepeatMode(@RepeatMode int repeatMode) { 528 if (this.repeatMode != repeatMode) { 529 this.repeatMode = repeatMode; 530 internalPlayer.setRepeatMode(repeatMode); 531 notifyListeners(listener -> listener.onRepeatModeChanged(repeatMode)); 532 } 533 } 534 535 @Override getRepeatMode()536 public @RepeatMode int getRepeatMode() { 537 return repeatMode; 538 } 539 540 @Override setShuffleModeEnabled(boolean shuffleModeEnabled)541 public void setShuffleModeEnabled(boolean shuffleModeEnabled) { 542 if (this.shuffleModeEnabled != shuffleModeEnabled) { 543 this.shuffleModeEnabled = shuffleModeEnabled; 544 internalPlayer.setShuffleModeEnabled(shuffleModeEnabled); 545 notifyListeners(listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled)); 546 } 547 } 548 549 @Override getShuffleModeEnabled()550 public boolean getShuffleModeEnabled() { 551 return shuffleModeEnabled; 552 } 553 554 @Override isLoading()555 public boolean isLoading() { 556 return playbackInfo.isLoading; 557 } 558 559 @Override seekTo(int windowIndex, long positionMs)560 public void seekTo(int windowIndex, long positionMs) { 561 Timeline timeline = playbackInfo.timeline; 562 if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { 563 throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); 564 } 565 pendingOperationAcks++; 566 if (isPlayingAd()) { 567 // TODO: Investigate adding support for seeking during ads. This is complicated to do in 568 // general because the midroll ad preceding the seek destination must be played before the 569 // content position can be played, if a different ad is playing at the moment. 570 Log.w(TAG, "seekTo ignored because an ad is playing"); 571 eventHandler 572 .obtainMessage( 573 ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED, 574 /* operationAcks */ 1, 575 /* positionDiscontinuityReason */ C.INDEX_UNSET, 576 playbackInfo) 577 .sendToTarget(); 578 return; 579 } 580 maskWindowIndexAndPositionForSeek(timeline, windowIndex, positionMs); 581 @Player.State 582 int newPlaybackState = 583 getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : Player.STATE_BUFFERING; 584 PlaybackInfo playbackInfo = this.playbackInfo.copyWithPlaybackState(newPlaybackState); 585 internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs)); 586 updatePlaybackInfo( 587 playbackInfo, 588 /* positionDiscontinuity= */ true, 589 /* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK, 590 /* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, 591 /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, 592 /* seekProcessed= */ true); 593 } 594 595 /** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */ 596 @SuppressWarnings("deprecation") 597 @Deprecated 598 @Override setPlaybackParameters(@ullable PlaybackParameters playbackParameters)599 public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { 600 setPlaybackSpeed( 601 playbackParameters != null ? playbackParameters.speed : Player.DEFAULT_PLAYBACK_SPEED); 602 } 603 604 /** @deprecated Use {@link #getPlaybackSpeed()} instead. */ 605 @SuppressWarnings("deprecation") 606 @Deprecated 607 @Override getPlaybackParameters()608 public PlaybackParameters getPlaybackParameters() { 609 return new PlaybackParameters(playbackSpeed); 610 } 611 612 @SuppressWarnings("deprecation") 613 @Override setPlaybackSpeed(float playbackSpeed)614 public void setPlaybackSpeed(float playbackSpeed) { 615 Assertions.checkState(playbackSpeed > 0); 616 if (this.playbackSpeed == playbackSpeed) { 617 return; 618 } 619 pendingSetPlaybackSpeedAcks++; 620 this.playbackSpeed = playbackSpeed; 621 PlaybackParameters playbackParameters = new PlaybackParameters(playbackSpeed); 622 internalPlayer.setPlaybackSpeed(playbackSpeed); 623 notifyListeners( 624 listener -> { 625 listener.onPlaybackParametersChanged(playbackParameters); 626 listener.onPlaybackSpeedChanged(playbackSpeed); 627 }); 628 } 629 630 @Override getPlaybackSpeed()631 public float getPlaybackSpeed() { 632 return playbackSpeed; 633 } 634 635 @Override setSeekParameters(@ullable SeekParameters seekParameters)636 public void setSeekParameters(@Nullable SeekParameters seekParameters) { 637 if (seekParameters == null) { 638 seekParameters = SeekParameters.DEFAULT; 639 } 640 if (!this.seekParameters.equals(seekParameters)) { 641 this.seekParameters = seekParameters; 642 internalPlayer.setSeekParameters(seekParameters); 643 } 644 } 645 646 @Override getSeekParameters()647 public SeekParameters getSeekParameters() { 648 return seekParameters; 649 } 650 651 @Override setForegroundMode(boolean foregroundMode)652 public void setForegroundMode(boolean foregroundMode) { 653 if (this.foregroundMode != foregroundMode) { 654 this.foregroundMode = foregroundMode; 655 internalPlayer.setForegroundMode(foregroundMode); 656 } 657 } 658 659 @Override stop(boolean reset)660 public void stop(boolean reset) { 661 PlaybackInfo playbackInfo = 662 getResetPlaybackInfo( 663 /* clearPlaylist= */ reset, 664 /* resetError= */ reset, 665 /* playbackState= */ Player.STATE_IDLE); 666 // Trigger internal stop first before updating the playback info and notifying external 667 // listeners to ensure that new operations issued in the listener notifications reach the 668 // player after this stop. The internal player can't change the playback info immediately 669 // because it uses a callback. 670 pendingOperationAcks++; 671 internalPlayer.stop(reset); 672 updatePlaybackInfo( 673 playbackInfo, 674 /* positionDiscontinuity= */ false, 675 /* ignored */ DISCONTINUITY_REASON_INTERNAL, 676 TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, 677 /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, 678 /* seekProcessed= */ false); 679 } 680 681 @Override release()682 public void release() { 683 Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " [" 684 + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] [" 685 + ExoPlayerLibraryInfo.registeredModules() + "]"); 686 if (!internalPlayer.release()) { 687 notifyListeners( 688 listener -> 689 listener.onPlayerError( 690 ExoPlaybackException.createForUnexpected( 691 new RuntimeException(new TimeoutException("Player release timed out."))))); 692 } 693 eventHandler.removeCallbacksAndMessages(null); 694 playbackInfo = 695 getResetPlaybackInfo( 696 /* clearPlaylist= */ false, 697 /* resetError= */ false, 698 /* playbackState= */ Player.STATE_IDLE); 699 } 700 701 @Override createMessage(Target target)702 public PlayerMessage createMessage(Target target) { 703 return new PlayerMessage( 704 internalPlayer, 705 target, 706 playbackInfo.timeline, 707 getCurrentWindowIndex(), 708 internalPlayerHandler); 709 } 710 711 @Override getCurrentPeriodIndex()712 public int getCurrentPeriodIndex() { 713 if (shouldMaskPosition()) { 714 return maskingPeriodIndex; 715 } else { 716 return playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid); 717 } 718 } 719 720 @Override getCurrentWindowIndex()721 public int getCurrentWindowIndex() { 722 int currentWindowIndex = getCurrentWindowIndexInternal(); 723 return currentWindowIndex == C.INDEX_UNSET ? 0 : currentWindowIndex; 724 } 725 726 @Override getDuration()727 public long getDuration() { 728 if (isPlayingAd()) { 729 MediaPeriodId periodId = playbackInfo.periodId; 730 playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period); 731 long adDurationUs = period.getAdDurationUs(periodId.adGroupIndex, periodId.adIndexInAdGroup); 732 return C.usToMs(adDurationUs); 733 } 734 return getContentDuration(); 735 } 736 737 @Override getCurrentPosition()738 public long getCurrentPosition() { 739 if (shouldMaskPosition()) { 740 return maskingWindowPositionMs; 741 } else if (playbackInfo.periodId.isAd()) { 742 return C.usToMs(playbackInfo.positionUs); 743 } else { 744 return periodPositionUsToWindowPositionMs(playbackInfo.periodId, playbackInfo.positionUs); 745 } 746 } 747 748 @Override getBufferedPosition()749 public long getBufferedPosition() { 750 if (isPlayingAd()) { 751 return playbackInfo.loadingMediaPeriodId.equals(playbackInfo.periodId) 752 ? C.usToMs(playbackInfo.bufferedPositionUs) 753 : getDuration(); 754 } 755 return getContentBufferedPosition(); 756 } 757 758 @Override getTotalBufferedDuration()759 public long getTotalBufferedDuration() { 760 return C.usToMs(playbackInfo.totalBufferedDurationUs); 761 } 762 763 @Override isPlayingAd()764 public boolean isPlayingAd() { 765 return !shouldMaskPosition() && playbackInfo.periodId.isAd(); 766 } 767 768 @Override getCurrentAdGroupIndex()769 public int getCurrentAdGroupIndex() { 770 return isPlayingAd() ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET; 771 } 772 773 @Override getCurrentAdIndexInAdGroup()774 public int getCurrentAdIndexInAdGroup() { 775 return isPlayingAd() ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET; 776 } 777 778 @Override getContentPosition()779 public long getContentPosition() { 780 if (isPlayingAd()) { 781 playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period); 782 return playbackInfo.requestedContentPositionUs == C.TIME_UNSET 783 ? playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDefaultPositionMs() 784 : period.getPositionInWindowMs() + C.usToMs(playbackInfo.requestedContentPositionUs); 785 } else { 786 return getCurrentPosition(); 787 } 788 } 789 790 @Override getContentBufferedPosition()791 public long getContentBufferedPosition() { 792 if (shouldMaskPosition()) { 793 return maskingWindowPositionMs; 794 } 795 if (playbackInfo.loadingMediaPeriodId.windowSequenceNumber 796 != playbackInfo.periodId.windowSequenceNumber) { 797 return playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); 798 } 799 long contentBufferedPositionUs = playbackInfo.bufferedPositionUs; 800 if (playbackInfo.loadingMediaPeriodId.isAd()) { 801 Timeline.Period loadingPeriod = 802 playbackInfo.timeline.getPeriodByUid(playbackInfo.loadingMediaPeriodId.periodUid, period); 803 contentBufferedPositionUs = 804 loadingPeriod.getAdGroupTimeUs(playbackInfo.loadingMediaPeriodId.adGroupIndex); 805 if (contentBufferedPositionUs == C.TIME_END_OF_SOURCE) { 806 contentBufferedPositionUs = loadingPeriod.durationUs; 807 } 808 } 809 return periodPositionUsToWindowPositionMs( 810 playbackInfo.loadingMediaPeriodId, contentBufferedPositionUs); 811 } 812 813 @Override getRendererCount()814 public int getRendererCount() { 815 return renderers.length; 816 } 817 818 @Override getRendererType(int index)819 public int getRendererType(int index) { 820 return renderers[index].getTrackType(); 821 } 822 823 @Override getCurrentTrackGroups()824 public TrackGroupArray getCurrentTrackGroups() { 825 return playbackInfo.trackGroups; 826 } 827 828 @Override getCurrentTrackSelections()829 public TrackSelectionArray getCurrentTrackSelections() { 830 return playbackInfo.trackSelectorResult.selections; 831 } 832 833 @Override getCurrentTimeline()834 public Timeline getCurrentTimeline() { 835 return playbackInfo.timeline; 836 } 837 838 // Not private so it can be called from an inner class without going through a thunk method. handleEvent(Message msg)839 /* package */ void handleEvent(Message msg) { 840 switch (msg.what) { 841 case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED: 842 handlePlaybackInfo((ExoPlayerImplInternal.PlaybackInfoUpdate) msg.obj); 843 break; 844 case ExoPlayerImplInternal.MSG_PLAYBACK_SPEED_CHANGED: 845 handlePlaybackSpeed((Float) msg.obj, /* operationAck= */ msg.arg1 != 0); 846 break; 847 default: 848 throw new IllegalStateException(); 849 } 850 } 851 getCurrentWindowIndexInternal()852 private int getCurrentWindowIndexInternal() { 853 if (shouldMaskPosition()) { 854 return maskingWindowIndex; 855 } else { 856 return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period) 857 .windowIndex; 858 } 859 } 860 createMediaSources(List<MediaItem> mediaItems)861 private List<MediaSource> createMediaSources(List<MediaItem> mediaItems) { 862 List<MediaSource> mediaSources = new ArrayList<>(); 863 for (int i = 0; i < mediaItems.size(); i++) { 864 mediaSources.add(mediaSourceFactory.createMediaSource(mediaItems.get(i))); 865 } 866 return mediaSources; 867 } 868 869 @SuppressWarnings("deprecation") handlePlaybackSpeed(float playbackSpeed, boolean operationAck)870 private void handlePlaybackSpeed(float playbackSpeed, boolean operationAck) { 871 if (operationAck) { 872 pendingSetPlaybackSpeedAcks--; 873 } 874 if (pendingSetPlaybackSpeedAcks == 0) { 875 if (this.playbackSpeed != playbackSpeed) { 876 this.playbackSpeed = playbackSpeed; 877 notifyListeners( 878 listener -> { 879 listener.onPlaybackParametersChanged(new PlaybackParameters(playbackSpeed)); 880 listener.onPlaybackSpeedChanged(playbackSpeed); 881 }); 882 } 883 } 884 } 885 handlePlaybackInfo(ExoPlayerImplInternal.PlaybackInfoUpdate playbackInfoUpdate)886 private void handlePlaybackInfo(ExoPlayerImplInternal.PlaybackInfoUpdate playbackInfoUpdate) { 887 pendingOperationAcks -= playbackInfoUpdate.operationAcks; 888 if (playbackInfoUpdate.positionDiscontinuity) { 889 hasPendingDiscontinuity = true; 890 pendingDiscontinuityReason = playbackInfoUpdate.discontinuityReason; 891 } 892 if (playbackInfoUpdate.hasPlayWhenReadyChangeReason) { 893 pendingPlayWhenReadyChangeReason = playbackInfoUpdate.playWhenReadyChangeReason; 894 } 895 if (pendingOperationAcks == 0) { 896 if (!this.playbackInfo.timeline.isEmpty() 897 && playbackInfoUpdate.playbackInfo.timeline.isEmpty()) { 898 // Update the masking variables, which are used when the timeline becomes empty. 899 resetMaskingPosition(); 900 } 901 boolean positionDiscontinuity = hasPendingDiscontinuity; 902 hasPendingDiscontinuity = false; 903 updatePlaybackInfo( 904 playbackInfoUpdate.playbackInfo, 905 positionDiscontinuity, 906 pendingDiscontinuityReason, 907 TIMELINE_CHANGE_REASON_SOURCE_UPDATE, 908 pendingPlayWhenReadyChangeReason, 909 /* seekProcessed= */ false); 910 } 911 } 912 getResetPlaybackInfo( boolean clearPlaylist, boolean resetError, @Player.State int playbackState)913 private PlaybackInfo getResetPlaybackInfo( 914 boolean clearPlaylist, boolean resetError, @Player.State int playbackState) { 915 if (clearPlaylist) { 916 // Reset list of media source holders which are used for creating the masking timeline. 917 removeMediaSourceHolders( 918 /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size()); 919 resetMaskingPosition(); 920 } else { 921 maskWithCurrentPosition(); 922 } 923 Timeline timeline = playbackInfo.timeline; 924 MediaPeriodId mediaPeriodId = playbackInfo.periodId; 925 long requestedContentPositionUs = playbackInfo.requestedContentPositionUs; 926 long positionUs = playbackInfo.positionUs; 927 if (clearPlaylist) { 928 timeline = Timeline.EMPTY; 929 mediaPeriodId = PlaybackInfo.getDummyPeriodForEmptyTimeline(); 930 requestedContentPositionUs = C.TIME_UNSET; 931 positionUs = 0; 932 } 933 return new PlaybackInfo( 934 timeline, 935 mediaPeriodId, 936 requestedContentPositionUs, 937 playbackState, 938 resetError ? null : playbackInfo.playbackError, 939 /* isLoading= */ false, 940 clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, 941 clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, 942 mediaPeriodId, 943 playbackInfo.playWhenReady, 944 playbackInfo.playbackSuppressionReason, 945 positionUs, 946 /* totalBufferedDurationUs= */ 0, 947 positionUs); 948 } 949 updatePlaybackInfo( PlaybackInfo playbackInfo, boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason, @TimelineChangeReason int timelineChangeReason, @PlayWhenReadyChangeReason int playWhenReadyChangeReason, boolean seekProcessed)950 private void updatePlaybackInfo( 951 PlaybackInfo playbackInfo, 952 boolean positionDiscontinuity, 953 @DiscontinuityReason int positionDiscontinuityReason, 954 @TimelineChangeReason int timelineChangeReason, 955 @PlayWhenReadyChangeReason int playWhenReadyChangeReason, 956 boolean seekProcessed) { 957 // Assign playback info immediately such that all getters return the right values. 958 PlaybackInfo previousPlaybackInfo = this.playbackInfo; 959 this.playbackInfo = playbackInfo; 960 notifyListeners( 961 new PlaybackInfoUpdate( 962 playbackInfo, 963 previousPlaybackInfo, 964 listeners, 965 trackSelector, 966 positionDiscontinuity, 967 positionDiscontinuityReason, 968 timelineChangeReason, 969 playWhenReadyChangeReason, 970 seekProcessed)); 971 } 972 setMediaSourcesInternal( List<MediaSource> mediaSources, int startWindowIndex, long startPositionMs, boolean resetToDefaultPosition)973 private void setMediaSourcesInternal( 974 List<MediaSource> mediaSources, 975 int startWindowIndex, 976 long startPositionMs, 977 boolean resetToDefaultPosition) { 978 validateMediaSources(mediaSources, /* mediaSourceReplacement= */ true); 979 int currentWindowIndex = getCurrentWindowIndexInternal(); 980 long currentPositionMs = getCurrentPosition(); 981 pendingOperationAcks++; 982 if (!mediaSourceHolders.isEmpty()) { 983 removeMediaSourceHolders( 984 /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size()); 985 } 986 List<MediaSourceList.MediaSourceHolder> holders = 987 addMediaSourceHolders(/* index= */ 0, mediaSources); 988 PlaybackInfo playbackInfo = maskTimeline(); 989 Timeline timeline = playbackInfo.timeline; 990 if (!timeline.isEmpty() && startWindowIndex >= timeline.getWindowCount()) { 991 throw new IllegalSeekPositionException(timeline, startWindowIndex, startPositionMs); 992 } 993 // Evaluate the actual start position. 994 if (resetToDefaultPosition) { 995 startWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); 996 startPositionMs = C.TIME_UNSET; 997 } else if (startWindowIndex == C.INDEX_UNSET) { 998 startWindowIndex = currentWindowIndex; 999 startPositionMs = currentPositionMs; 1000 } 1001 maskWindowIndexAndPositionForSeek( 1002 timeline, startWindowIndex == C.INDEX_UNSET ? 0 : startWindowIndex, startPositionMs); 1003 // Mask the playback state. 1004 int maskingPlaybackState = playbackInfo.playbackState; 1005 if (startWindowIndex != C.INDEX_UNSET && playbackInfo.playbackState != STATE_IDLE) { 1006 // Position reset to startWindowIndex (results in pending initial seek). 1007 if (timeline.isEmpty() || startWindowIndex >= timeline.getWindowCount()) { 1008 // Setting an empty timeline or invalid seek transitions to ended. 1009 maskingPlaybackState = STATE_ENDED; 1010 } else { 1011 maskingPlaybackState = STATE_BUFFERING; 1012 } 1013 } 1014 playbackInfo = playbackInfo.copyWithPlaybackState(maskingPlaybackState); 1015 internalPlayer.setMediaSources( 1016 holders, startWindowIndex, C.msToUs(startPositionMs), shuffleOrder); 1017 updatePlaybackInfo( 1018 playbackInfo, 1019 /* positionDiscontinuity= */ false, 1020 /* ignored */ Player.DISCONTINUITY_REASON_INTERNAL, 1021 /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, 1022 /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, 1023 /* seekProcessed= */ false); 1024 } 1025 addMediaSourceHolders( int index, List<MediaSource> mediaSources)1026 private List<MediaSourceList.MediaSourceHolder> addMediaSourceHolders( 1027 int index, List<MediaSource> mediaSources) { 1028 List<MediaSourceList.MediaSourceHolder> holders = new ArrayList<>(); 1029 for (int i = 0; i < mediaSources.size(); i++) { 1030 MediaSourceList.MediaSourceHolder holder = 1031 new MediaSourceList.MediaSourceHolder(mediaSources.get(i), useLazyPreparation); 1032 holders.add(holder); 1033 mediaSourceHolders.add(i + index, holder); 1034 } 1035 shuffleOrder = 1036 shuffleOrder.cloneAndInsert( 1037 /* insertionIndex= */ index, /* insertionCount= */ holders.size()); 1038 return holders; 1039 } 1040 removeMediaItemsInternal(int fromIndex, int toIndex)1041 private void removeMediaItemsInternal(int fromIndex, int toIndex) { 1042 Assertions.checkArgument( 1043 fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolders.size()); 1044 int currentWindowIndex = getCurrentWindowIndex(); 1045 long currentPositionMs = getCurrentPosition(); 1046 Timeline oldTimeline = getCurrentTimeline(); 1047 int currentMediaSourceCount = mediaSourceHolders.size(); 1048 pendingOperationAcks++; 1049 removeMediaSourceHolders(fromIndex, /* toIndexExclusive= */ toIndex); 1050 PlaybackInfo playbackInfo = 1051 maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); 1052 // Player transitions to STATE_ENDED if the current index is part of the removed tail. 1053 final boolean transitionsToEnded = 1054 playbackInfo.playbackState != STATE_IDLE 1055 && playbackInfo.playbackState != STATE_ENDED 1056 && fromIndex < toIndex 1057 && toIndex == currentMediaSourceCount 1058 && currentWindowIndex >= playbackInfo.timeline.getWindowCount(); 1059 if (transitionsToEnded) { 1060 playbackInfo = playbackInfo.copyWithPlaybackState(STATE_ENDED); 1061 } 1062 internalPlayer.removeMediaSources(fromIndex, toIndex, shuffleOrder); 1063 updatePlaybackInfo( 1064 playbackInfo, 1065 /* positionDiscontinuity= */ false, 1066 /* ignored */ Player.DISCONTINUITY_REASON_INTERNAL, 1067 /* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, 1068 /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, 1069 /* seekProcessed= */ false); 1070 } 1071 removeMediaSourceHolders( int fromIndex, int toIndexExclusive)1072 private List<MediaSourceList.MediaSourceHolder> removeMediaSourceHolders( 1073 int fromIndex, int toIndexExclusive) { 1074 List<MediaSourceList.MediaSourceHolder> removed = new ArrayList<>(); 1075 for (int i = toIndexExclusive - 1; i >= fromIndex; i--) { 1076 removed.add(mediaSourceHolders.remove(i)); 1077 } 1078 shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndexExclusive); 1079 if (mediaSourceHolders.isEmpty()) { 1080 hasAdsMediaSource = false; 1081 } 1082 return removed; 1083 } 1084 1085 /** 1086 * Validates media sources before any modification of the existing list of media sources is made. 1087 * This way we can throw an exception before changing the state of the player in case of a 1088 * validation failure. 1089 * 1090 * @param mediaSources The media sources to set or add. 1091 * @param mediaSourceReplacement Whether the given media sources will replace existing ones. 1092 */ validateMediaSources( List<MediaSource> mediaSources, boolean mediaSourceReplacement)1093 private void validateMediaSources( 1094 List<MediaSource> mediaSources, boolean mediaSourceReplacement) { 1095 if (hasAdsMediaSource && !mediaSourceReplacement && !mediaSources.isEmpty()) { 1096 // Adding media sources to an ads media source is not allowed 1097 // (see https://github.com/google/ExoPlayer/issues/3750). 1098 throw new IllegalStateException(); 1099 } 1100 int sizeAfterModification = 1101 mediaSources.size() + (mediaSourceReplacement ? 0 : mediaSourceHolders.size()); 1102 for (int i = 0; i < mediaSources.size(); i++) { 1103 MediaSource mediaSource = checkNotNull(mediaSources.get(i)); 1104 if (mediaSource instanceof AdsMediaSource) { 1105 if (sizeAfterModification > 1) { 1106 // Ads media sources only allowed with a single source 1107 // (see https://github.com/google/ExoPlayer/issues/3750). 1108 throw new IllegalArgumentException(); 1109 } 1110 hasAdsMediaSource = true; 1111 } 1112 } 1113 } 1114 maskTimeline()1115 private PlaybackInfo maskTimeline() { 1116 return playbackInfo.copyWithTimeline( 1117 mediaSourceHolders.isEmpty() 1118 ? Timeline.EMPTY 1119 : new MediaSourceList.PlaylistTimeline(mediaSourceHolders, shuffleOrder)); 1120 } 1121 maskTimelineAndWindowIndex( int currentWindowIndex, long currentPositionMs, Timeline oldTimeline)1122 private PlaybackInfo maskTimelineAndWindowIndex( 1123 int currentWindowIndex, long currentPositionMs, Timeline oldTimeline) { 1124 PlaybackInfo playbackInfo = maskTimeline(); 1125 Timeline maskingTimeline = playbackInfo.timeline; 1126 if (oldTimeline.isEmpty()) { 1127 // The index is the default index or was set by a seek in the empty old timeline. 1128 maskingWindowIndex = currentWindowIndex; 1129 if (!maskingTimeline.isEmpty() && currentWindowIndex >= maskingTimeline.getWindowCount()) { 1130 // The seek is not valid in the new timeline. 1131 maskWithDefaultPosition(maskingTimeline); 1132 } 1133 return playbackInfo; 1134 } 1135 @Nullable 1136 Pair<Object, Long> periodPosition = 1137 oldTimeline.getPeriodPosition( 1138 window, 1139 period, 1140 currentWindowIndex, 1141 C.msToUs(currentPositionMs), 1142 /* defaultPositionProjectionUs= */ 0); 1143 Object periodUid = Util.castNonNull(periodPosition).first; 1144 if (maskingTimeline.getIndexOfPeriod(periodUid) != C.INDEX_UNSET) { 1145 // Get the window index of the current period that exists in the new timeline also. 1146 maskingWindowIndex = maskingTimeline.getPeriodByUid(periodUid, period).windowIndex; 1147 maskingPeriodIndex = maskingTimeline.getIndexOfPeriod(periodUid); 1148 maskingWindowPositionMs = currentPositionMs; 1149 } else { 1150 // Period uid not found in new timeline. Try to get subsequent period. 1151 @Nullable 1152 Object nextPeriodUid = 1153 ExoPlayerImplInternal.resolveSubsequentPeriod( 1154 window, 1155 period, 1156 repeatMode, 1157 shuffleModeEnabled, 1158 periodUid, 1159 oldTimeline, 1160 maskingTimeline); 1161 if (nextPeriodUid != null) { 1162 // Set masking to the default position of the window of the subsequent period. 1163 maskingWindowIndex = maskingTimeline.getPeriodByUid(nextPeriodUid, period).windowIndex; 1164 maskingPeriodIndex = maskingTimeline.getWindow(maskingWindowIndex, window).firstPeriodIndex; 1165 maskingWindowPositionMs = window.getDefaultPositionMs(); 1166 } else { 1167 // Reset if no subsequent period is found. 1168 maskWithDefaultPosition(maskingTimeline); 1169 } 1170 } 1171 return playbackInfo; 1172 } 1173 maskWindowIndexAndPositionForSeek( Timeline timeline, int windowIndex, long positionMs)1174 private void maskWindowIndexAndPositionForSeek( 1175 Timeline timeline, int windowIndex, long positionMs) { 1176 maskingWindowIndex = windowIndex; 1177 if (timeline.isEmpty()) { 1178 maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs; 1179 maskingPeriodIndex = 0; 1180 } else if (windowIndex >= timeline.getWindowCount()) { 1181 // An initial seek now proves to be invalid in the actual timeline. 1182 maskWithDefaultPosition(timeline); 1183 } else { 1184 long windowPositionUs = 1185 positionMs == C.TIME_UNSET 1186 ? timeline.getWindow(windowIndex, window).getDefaultPositionUs() 1187 : C.msToUs(positionMs); 1188 Pair<Object, Long> periodUidAndPosition = 1189 timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); 1190 maskingWindowPositionMs = C.usToMs(windowPositionUs); 1191 maskingPeriodIndex = timeline.getIndexOfPeriod(periodUidAndPosition.first); 1192 } 1193 } 1194 maskWithCurrentPosition()1195 private void maskWithCurrentPosition() { 1196 maskingWindowIndex = getCurrentWindowIndexInternal(); 1197 maskingPeriodIndex = getCurrentPeriodIndex(); 1198 maskingWindowPositionMs = getCurrentPosition(); 1199 } 1200 maskWithDefaultPosition(Timeline timeline)1201 private void maskWithDefaultPosition(Timeline timeline) { 1202 if (timeline.isEmpty()) { 1203 resetMaskingPosition(); 1204 return; 1205 } 1206 maskingWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); 1207 timeline.getWindow(maskingWindowIndex, window); 1208 maskingWindowPositionMs = window.getDefaultPositionMs(); 1209 maskingPeriodIndex = window.firstPeriodIndex; 1210 } 1211 resetMaskingPosition()1212 private void resetMaskingPosition() { 1213 maskingWindowIndex = C.INDEX_UNSET; 1214 maskingWindowPositionMs = 0; 1215 maskingPeriodIndex = 0; 1216 } 1217 notifyListeners(ListenerInvocation listenerInvocation)1218 private void notifyListeners(ListenerInvocation listenerInvocation) { 1219 CopyOnWriteArrayList<ListenerHolder> listenerSnapshot = new CopyOnWriteArrayList<>(listeners); 1220 notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation)); 1221 } 1222 notifyListeners(Runnable listenerNotificationRunnable)1223 private void notifyListeners(Runnable listenerNotificationRunnable) { 1224 boolean isRunningRecursiveListenerNotification = !pendingListenerNotifications.isEmpty(); 1225 pendingListenerNotifications.addLast(listenerNotificationRunnable); 1226 if (isRunningRecursiveListenerNotification) { 1227 return; 1228 } 1229 while (!pendingListenerNotifications.isEmpty()) { 1230 pendingListenerNotifications.peekFirst().run(); 1231 pendingListenerNotifications.removeFirst(); 1232 } 1233 } 1234 periodPositionUsToWindowPositionMs(MediaPeriodId periodId, long positionUs)1235 private long periodPositionUsToWindowPositionMs(MediaPeriodId periodId, long positionUs) { 1236 long positionMs = C.usToMs(positionUs); 1237 playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period); 1238 positionMs += period.getPositionInWindowMs(); 1239 return positionMs; 1240 } 1241 shouldMaskPosition()1242 private boolean shouldMaskPosition() { 1243 return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0; 1244 } 1245 1246 private static final class PlaybackInfoUpdate implements Runnable { 1247 1248 private final PlaybackInfo playbackInfo; 1249 private final CopyOnWriteArrayList<ListenerHolder> listenerSnapshot; 1250 private final TrackSelector trackSelector; 1251 private final boolean positionDiscontinuity; 1252 @DiscontinuityReason private final int positionDiscontinuityReason; 1253 @TimelineChangeReason private final int timelineChangeReason; 1254 @PlayWhenReadyChangeReason private final int playWhenReadyChangeReason; 1255 private final boolean seekProcessed; 1256 private final boolean playbackStateChanged; 1257 private final boolean playbackErrorChanged; 1258 private final boolean timelineChanged; 1259 private final boolean isLoadingChanged; 1260 private final boolean trackSelectorResultChanged; 1261 private final boolean isPlayingChanged; 1262 private final boolean playWhenReadyChanged; 1263 private final boolean playbackSuppressionReasonChanged; 1264 PlaybackInfoUpdate( PlaybackInfo playbackInfo, PlaybackInfo previousPlaybackInfo, CopyOnWriteArrayList<ListenerHolder> listeners, TrackSelector trackSelector, boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason, @TimelineChangeReason int timelineChangeReason, @PlayWhenReadyChangeReason int playWhenReadyChangeReason, boolean seekProcessed)1265 public PlaybackInfoUpdate( 1266 PlaybackInfo playbackInfo, 1267 PlaybackInfo previousPlaybackInfo, 1268 CopyOnWriteArrayList<ListenerHolder> listeners, 1269 TrackSelector trackSelector, 1270 boolean positionDiscontinuity, 1271 @DiscontinuityReason int positionDiscontinuityReason, 1272 @TimelineChangeReason int timelineChangeReason, 1273 @PlayWhenReadyChangeReason int playWhenReadyChangeReason, 1274 boolean seekProcessed) { 1275 this.playbackInfo = playbackInfo; 1276 this.listenerSnapshot = new CopyOnWriteArrayList<>(listeners); 1277 this.trackSelector = trackSelector; 1278 this.positionDiscontinuity = positionDiscontinuity; 1279 this.positionDiscontinuityReason = positionDiscontinuityReason; 1280 this.timelineChangeReason = timelineChangeReason; 1281 this.playWhenReadyChangeReason = playWhenReadyChangeReason; 1282 this.seekProcessed = seekProcessed; 1283 playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState; 1284 playbackErrorChanged = 1285 previousPlaybackInfo.playbackError != playbackInfo.playbackError 1286 && playbackInfo.playbackError != null; 1287 isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading; 1288 timelineChanged = !previousPlaybackInfo.timeline.equals(playbackInfo.timeline); 1289 trackSelectorResultChanged = 1290 previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult; 1291 playWhenReadyChanged = previousPlaybackInfo.playWhenReady != playbackInfo.playWhenReady; 1292 playbackSuppressionReasonChanged = 1293 previousPlaybackInfo.playbackSuppressionReason != playbackInfo.playbackSuppressionReason; 1294 isPlayingChanged = isPlaying(previousPlaybackInfo) != isPlaying(playbackInfo); 1295 } 1296 1297 @SuppressWarnings("deprecation") 1298 @Override run()1299 public void run() { 1300 if (timelineChanged) { 1301 invokeAll( 1302 listenerSnapshot, 1303 listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason)); 1304 } 1305 if (positionDiscontinuity) { 1306 invokeAll( 1307 listenerSnapshot, 1308 listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason)); 1309 } 1310 if (playbackErrorChanged) { 1311 invokeAll(listenerSnapshot, listener -> listener.onPlayerError(playbackInfo.playbackError)); 1312 } 1313 if (trackSelectorResultChanged) { 1314 trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info); 1315 invokeAll( 1316 listenerSnapshot, 1317 listener -> 1318 listener.onTracksChanged( 1319 playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections)); 1320 } 1321 if (isLoadingChanged) { 1322 invokeAll( 1323 listenerSnapshot, listener -> listener.onIsLoadingChanged(playbackInfo.isLoading)); 1324 } 1325 if (playbackStateChanged || playWhenReadyChanged) { 1326 invokeAll( 1327 listenerSnapshot, 1328 listener -> 1329 listener.onPlayerStateChanged( 1330 playbackInfo.playWhenReady, playbackInfo.playbackState)); 1331 } 1332 if (playbackStateChanged) { 1333 invokeAll( 1334 listenerSnapshot, 1335 listener -> listener.onPlaybackStateChanged(playbackInfo.playbackState)); 1336 } 1337 if (playWhenReadyChanged) { 1338 invokeAll( 1339 listenerSnapshot, 1340 listener -> 1341 listener.onPlayWhenReadyChanged( 1342 playbackInfo.playWhenReady, playWhenReadyChangeReason)); 1343 } 1344 if (playbackSuppressionReasonChanged) { 1345 invokeAll( 1346 listenerSnapshot, 1347 listener -> 1348 listener.onPlaybackSuppressionReasonChanged( 1349 playbackInfo.playbackSuppressionReason)); 1350 } 1351 if (isPlayingChanged) { 1352 invokeAll( 1353 listenerSnapshot, listener -> listener.onIsPlayingChanged(isPlaying(playbackInfo))); 1354 } 1355 if (seekProcessed) { 1356 invokeAll(listenerSnapshot, EventListener::onSeekProcessed); 1357 } 1358 } 1359 isPlaying(PlaybackInfo playbackInfo)1360 private static boolean isPlaying(PlaybackInfo playbackInfo) { 1361 return playbackInfo.playbackState == Player.STATE_READY 1362 && playbackInfo.playWhenReady 1363 && playbackInfo.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; 1364 } 1365 } 1366 invokeAll( CopyOnWriteArrayList<ListenerHolder> listeners, ListenerInvocation listenerInvocation)1367 private static void invokeAll( 1368 CopyOnWriteArrayList<ListenerHolder> listeners, ListenerInvocation listenerInvocation) { 1369 for (ListenerHolder listenerHolder : listeners) { 1370 listenerHolder.invoke(listenerInvocation); 1371 } 1372 } 1373 } 1374