• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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