• 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 android.os.Handler;
19 import android.os.HandlerThread;
20 import android.os.Looper;
21 import android.os.Message;
22 import android.os.Process;
23 import android.os.SystemClock;
24 import android.util.Pair;
25 import androidx.annotation.CheckResult;
26 import androidx.annotation.Nullable;
27 import com.google.android.exoplayer2.DefaultMediaClock.PlaybackSpeedListener;
28 import com.google.android.exoplayer2.Player.DiscontinuityReason;
29 import com.google.android.exoplayer2.Player.PlayWhenReadyChangeReason;
30 import com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
31 import com.google.android.exoplayer2.Player.RepeatMode;
32 import com.google.android.exoplayer2.analytics.AnalyticsCollector;
33 import com.google.android.exoplayer2.source.MediaPeriod;
34 import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
35 import com.google.android.exoplayer2.source.SampleStream;
36 import com.google.android.exoplayer2.source.ShuffleOrder;
37 import com.google.android.exoplayer2.source.TrackGroupArray;
38 import com.google.android.exoplayer2.trackselection.TrackSelection;
39 import com.google.android.exoplayer2.trackselection.TrackSelector;
40 import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
41 import com.google.android.exoplayer2.upstream.BandwidthMeter;
42 import com.google.android.exoplayer2.util.Assertions;
43 import com.google.android.exoplayer2.util.Clock;
44 import com.google.android.exoplayer2.util.HandlerWrapper;
45 import com.google.android.exoplayer2.util.Log;
46 import com.google.android.exoplayer2.util.TraceUtil;
47 import com.google.android.exoplayer2.util.Util;
48 import java.io.IOException;
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.List;
52 import java.util.concurrent.atomic.AtomicBoolean;
53 
54 /** Implements the internal behavior of {@link ExoPlayerImpl}. */
55 /* package */ final class ExoPlayerImplInternal
56     implements Handler.Callback,
57         MediaPeriod.Callback,
58         TrackSelector.InvalidationListener,
59         MediaSourceList.MediaSourceListInfoRefreshListener,
60         PlaybackSpeedListener,
61         PlayerMessage.Sender {
62 
63   private static final String TAG = "ExoPlayerImplInternal";
64 
65   // External messages
66   public static final int MSG_PLAYBACK_INFO_CHANGED = 0;
67   public static final int MSG_PLAYBACK_SPEED_CHANGED = 1;
68 
69   // Internal messages
70   private static final int MSG_PREPARE = 0;
71   private static final int MSG_SET_PLAY_WHEN_READY = 1;
72   private static final int MSG_DO_SOME_WORK = 2;
73   private static final int MSG_SEEK_TO = 3;
74   private static final int MSG_SET_PLAYBACK_SPEED = 4;
75   private static final int MSG_SET_SEEK_PARAMETERS = 5;
76   private static final int MSG_STOP = 6;
77   private static final int MSG_RELEASE = 7;
78   private static final int MSG_PERIOD_PREPARED = 8;
79   private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9;
80   private static final int MSG_TRACK_SELECTION_INVALIDATED = 10;
81   private static final int MSG_SET_REPEAT_MODE = 11;
82   private static final int MSG_SET_SHUFFLE_ENABLED = 12;
83   private static final int MSG_SET_FOREGROUND_MODE = 13;
84   private static final int MSG_SEND_MESSAGE = 14;
85   private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15;
86   private static final int MSG_PLAYBACK_SPEED_CHANGED_INTERNAL = 16;
87   private static final int MSG_SET_MEDIA_SOURCES = 17;
88   private static final int MSG_ADD_MEDIA_SOURCES = 18;
89   private static final int MSG_MOVE_MEDIA_SOURCES = 19;
90   private static final int MSG_REMOVE_MEDIA_SOURCES = 20;
91   private static final int MSG_SET_SHUFFLE_ORDER = 21;
92   private static final int MSG_PLAYLIST_UPDATE_REQUESTED = 22;
93   private static final int MSG_SET_PAUSE_AT_END_OF_WINDOW = 23;
94 
95   private static final int ACTIVE_INTERVAL_MS = 10;
96   private static final int IDLE_INTERVAL_MS = 1000;
97 
98   private final Renderer[] renderers;
99   private final RendererCapabilities[] rendererCapabilities;
100   private final TrackSelector trackSelector;
101   private final TrackSelectorResult emptyTrackSelectorResult;
102   private final LoadControl loadControl;
103   private final BandwidthMeter bandwidthMeter;
104   private final HandlerWrapper handler;
105   private final HandlerThread internalPlaybackThread;
106   private final Handler eventHandler;
107   private final Timeline.Window window;
108   private final Timeline.Period period;
109   private final long backBufferDurationUs;
110   private final boolean retainBackBufferFromKeyframe;
111   private final DefaultMediaClock mediaClock;
112   private final ArrayList<PendingMessageInfo> pendingMessages;
113   private final Clock clock;
114   private final MediaPeriodQueue queue;
115   private final MediaSourceList mediaSourceList;
116 
117   @SuppressWarnings("unused")
118   private SeekParameters seekParameters;
119 
120   private PlaybackInfo playbackInfo;
121   private PlaybackInfoUpdate playbackInfoUpdate;
122   private boolean released;
123   private boolean pauseAtEndOfWindow;
124   private boolean pendingPauseAtEndOfPeriod;
125   private boolean rebuffering;
126   private boolean shouldContinueLoading;
127   @Player.RepeatMode private int repeatMode;
128   private boolean shuffleModeEnabled;
129   private boolean foregroundMode;
130 
131   private int enabledRendererCount;
132   @Nullable private SeekPosition pendingInitialSeekPosition;
133   private long rendererPositionUs;
134   private int nextPendingMessageIndex;
135   private boolean deliverPendingMessageAtStartPositionRequired;
136 
137   private long releaseTimeoutMs;
138   private boolean throwWhenStuckBuffering;
139 
ExoPlayerImplInternal( Renderer[] renderers, TrackSelector trackSelector, TrackSelectorResult emptyTrackSelectorResult, LoadControl loadControl, BandwidthMeter bandwidthMeter, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, @Nullable AnalyticsCollector analyticsCollector, Handler eventHandler, Clock clock)140   public ExoPlayerImplInternal(
141       Renderer[] renderers,
142       TrackSelector trackSelector,
143       TrackSelectorResult emptyTrackSelectorResult,
144       LoadControl loadControl,
145       BandwidthMeter bandwidthMeter,
146       @Player.RepeatMode int repeatMode,
147       boolean shuffleModeEnabled,
148       @Nullable AnalyticsCollector analyticsCollector,
149       Handler eventHandler,
150       Clock clock) {
151     this.renderers = renderers;
152     this.trackSelector = trackSelector;
153     this.emptyTrackSelectorResult = emptyTrackSelectorResult;
154     this.loadControl = loadControl;
155     this.bandwidthMeter = bandwidthMeter;
156     this.repeatMode = repeatMode;
157     this.shuffleModeEnabled = shuffleModeEnabled;
158     this.eventHandler = eventHandler;
159     this.clock = clock;
160     this.queue = new MediaPeriodQueue();
161 
162     backBufferDurationUs = loadControl.getBackBufferDurationUs();
163     retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe();
164 
165     seekParameters = SeekParameters.DEFAULT;
166     playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult);
167     playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo);
168     rendererCapabilities = new RendererCapabilities[renderers.length];
169     for (int i = 0; i < renderers.length; i++) {
170       renderers[i].setIndex(i);
171       rendererCapabilities[i] = renderers[i].getCapabilities();
172     }
173     mediaClock = new DefaultMediaClock(this, clock);
174     pendingMessages = new ArrayList<>();
175     window = new Timeline.Window();
176     period = new Timeline.Period();
177     trackSelector.init(/* listener= */ this, bandwidthMeter);
178 
179     // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
180     // not normally change to this priority" is incorrect.
181     internalPlaybackThread = new HandlerThread("ExoPlayer:Playback", Process.THREAD_PRIORITY_AUDIO);
182     internalPlaybackThread.start();
183     handler = clock.createHandler(internalPlaybackThread.getLooper(), this);
184     deliverPendingMessageAtStartPositionRequired = true;
185     mediaSourceList = new MediaSourceList(this);
186     if (analyticsCollector != null) {
187       mediaSourceList.setAnalyticsCollector(eventHandler, analyticsCollector);
188     }
189   }
190 
experimental_setReleaseTimeoutMs(long releaseTimeoutMs)191   public void experimental_setReleaseTimeoutMs(long releaseTimeoutMs) {
192     this.releaseTimeoutMs = releaseTimeoutMs;
193   }
194 
experimental_throwWhenStuckBuffering()195   public void experimental_throwWhenStuckBuffering() {
196     throwWhenStuckBuffering = true;
197   }
198 
prepare()199   public void prepare() {
200     handler.obtainMessage(MSG_PREPARE).sendToTarget();
201   }
202 
setPlayWhenReady( boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason)203   public void setPlayWhenReady(
204       boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) {
205     handler
206         .obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, playbackSuppressionReason)
207         .sendToTarget();
208   }
209 
setPauseAtEndOfWindow(boolean pauseAtEndOfWindow)210   public void setPauseAtEndOfWindow(boolean pauseAtEndOfWindow) {
211     handler
212         .obtainMessage(MSG_SET_PAUSE_AT_END_OF_WINDOW, pauseAtEndOfWindow ? 1 : 0, /* ignored */ 0)
213         .sendToTarget();
214   }
215 
setRepeatMode(@layer.RepeatMode int repeatMode)216   public void setRepeatMode(@Player.RepeatMode int repeatMode) {
217     handler.obtainMessage(MSG_SET_REPEAT_MODE, repeatMode, 0).sendToTarget();
218   }
219 
setShuffleModeEnabled(boolean shuffleModeEnabled)220   public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
221     handler.obtainMessage(MSG_SET_SHUFFLE_ENABLED, shuffleModeEnabled ? 1 : 0, 0).sendToTarget();
222   }
223 
seekTo(Timeline timeline, int windowIndex, long positionUs)224   public void seekTo(Timeline timeline, int windowIndex, long positionUs) {
225     handler
226         .obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs))
227         .sendToTarget();
228   }
229 
setPlaybackSpeed(float playbackSpeed)230   public void setPlaybackSpeed(float playbackSpeed) {
231     handler.obtainMessage(MSG_SET_PLAYBACK_SPEED, playbackSpeed).sendToTarget();
232   }
233 
setSeekParameters(SeekParameters seekParameters)234   public void setSeekParameters(SeekParameters seekParameters) {
235     handler.obtainMessage(MSG_SET_SEEK_PARAMETERS, seekParameters).sendToTarget();
236   }
237 
stop(boolean reset)238   public void stop(boolean reset) {
239     handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget();
240   }
241 
setMediaSources( List<MediaSourceList.MediaSourceHolder> mediaSources, int windowIndex, long positionUs, ShuffleOrder shuffleOrder)242   public void setMediaSources(
243       List<MediaSourceList.MediaSourceHolder> mediaSources,
244       int windowIndex,
245       long positionUs,
246       ShuffleOrder shuffleOrder) {
247     handler
248         .obtainMessage(
249             MSG_SET_MEDIA_SOURCES,
250             new MediaSourceListUpdateMessage(mediaSources, shuffleOrder, windowIndex, positionUs))
251         .sendToTarget();
252   }
253 
addMediaSources( int index, List<MediaSourceList.MediaSourceHolder> mediaSources, ShuffleOrder shuffleOrder)254   public void addMediaSources(
255       int index, List<MediaSourceList.MediaSourceHolder> mediaSources, ShuffleOrder shuffleOrder) {
256     handler
257         .obtainMessage(
258             MSG_ADD_MEDIA_SOURCES,
259             index,
260             /* ignored */ 0,
261             new MediaSourceListUpdateMessage(
262                 mediaSources,
263                 shuffleOrder,
264                 /* windowIndex= */ C.INDEX_UNSET,
265                 /* positionUs= */ C.TIME_UNSET))
266         .sendToTarget();
267   }
268 
removeMediaSources(int fromIndex, int toIndex, ShuffleOrder shuffleOrder)269   public void removeMediaSources(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) {
270     handler
271         .obtainMessage(MSG_REMOVE_MEDIA_SOURCES, fromIndex, toIndex, shuffleOrder)
272         .sendToTarget();
273   }
274 
moveMediaSources( int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder)275   public void moveMediaSources(
276       int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) {
277     MoveMediaItemsMessage moveMediaItemsMessage =
278         new MoveMediaItemsMessage(fromIndex, toIndex, newFromIndex, shuffleOrder);
279     handler.obtainMessage(MSG_MOVE_MEDIA_SOURCES, moveMediaItemsMessage).sendToTarget();
280   }
281 
setShuffleOrder(ShuffleOrder shuffleOrder)282   public void setShuffleOrder(ShuffleOrder shuffleOrder) {
283     handler.obtainMessage(MSG_SET_SHUFFLE_ORDER, shuffleOrder).sendToTarget();
284   }
285 
286   @Override
sendMessage(PlayerMessage message)287   public synchronized void sendMessage(PlayerMessage message) {
288     if (released || !internalPlaybackThread.isAlive()) {
289       Log.w(TAG, "Ignoring messages sent after release.");
290       message.markAsProcessed(/* isDelivered= */ false);
291       return;
292     }
293     handler.obtainMessage(MSG_SEND_MESSAGE, message).sendToTarget();
294   }
295 
setForegroundMode(boolean foregroundMode)296   public synchronized void setForegroundMode(boolean foregroundMode) {
297     if (released || !internalPlaybackThread.isAlive()) {
298       return;
299     }
300     if (foregroundMode) {
301       handler.obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 1, 0).sendToTarget();
302     } else {
303       AtomicBoolean processedFlag = new AtomicBoolean();
304       handler
305           .obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 0, 0, processedFlag)
306           .sendToTarget();
307       boolean wasInterrupted = false;
308       while (!processedFlag.get()) {
309         try {
310           wait();
311         } catch (InterruptedException e) {
312           wasInterrupted = true;
313         }
314       }
315       if (wasInterrupted) {
316         // Restore the interrupted status.
317         Thread.currentThread().interrupt();
318       }
319     }
320   }
321 
release()322   public synchronized boolean release() {
323     if (released || !internalPlaybackThread.isAlive()) {
324       return true;
325     }
326 
327     handler.sendEmptyMessage(MSG_RELEASE);
328     try {
329       if (releaseTimeoutMs > 0) {
330         waitUntilReleased(releaseTimeoutMs);
331       } else {
332         waitUntilReleased();
333       }
334     } catch (InterruptedException e) {
335       Thread.currentThread().interrupt();
336     }
337 
338     return released;
339   }
340 
getPlaybackLooper()341   public Looper getPlaybackLooper() {
342     return internalPlaybackThread.getLooper();
343   }
344 
345   // Playlist.PlaylistInfoRefreshListener implementation.
346 
347   @Override
onPlaylistUpdateRequested()348   public void onPlaylistUpdateRequested() {
349     handler.sendEmptyMessage(MSG_PLAYLIST_UPDATE_REQUESTED);
350   }
351 
352   // MediaPeriod.Callback implementation.
353 
354   @Override
onPrepared(MediaPeriod source)355   public void onPrepared(MediaPeriod source) {
356     handler.obtainMessage(MSG_PERIOD_PREPARED, source).sendToTarget();
357   }
358 
359   @Override
onContinueLoadingRequested(MediaPeriod source)360   public void onContinueLoadingRequested(MediaPeriod source) {
361     handler.obtainMessage(MSG_SOURCE_CONTINUE_LOADING_REQUESTED, source).sendToTarget();
362   }
363 
364   // TrackSelector.InvalidationListener implementation.
365 
366   @Override
onTrackSelectionsInvalidated()367   public void onTrackSelectionsInvalidated() {
368     handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED);
369   }
370 
371   // DefaultMediaClock.PlaybackSpeedListener implementation.
372 
373   @Override
onPlaybackSpeedChanged(float playbackSpeed)374   public void onPlaybackSpeedChanged(float playbackSpeed) {
375     sendPlaybackSpeedChangedInternal(playbackSpeed, /* acknowledgeCommand= */ false);
376   }
377 
378   // Handler.Callback implementation.
379 
380   @Override
handleMessage(Message msg)381   public boolean handleMessage(Message msg) {
382     try {
383       switch (msg.what) {
384         case MSG_PREPARE:
385           prepareInternal();
386           break;
387         case MSG_SET_PLAY_WHEN_READY:
388           setPlayWhenReadyInternal(
389               /* playWhenReady= */ msg.arg1 != 0,
390               /* playbackSuppressionReason= */ msg.arg2,
391               /* operationAck= */ true,
392               Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
393           break;
394         case MSG_SET_REPEAT_MODE:
395           setRepeatModeInternal(msg.arg1);
396           break;
397         case MSG_SET_SHUFFLE_ENABLED:
398           setShuffleModeEnabledInternal(msg.arg1 != 0);
399           break;
400         case MSG_DO_SOME_WORK:
401           doSomeWork();
402           break;
403         case MSG_SEEK_TO:
404           seekToInternal((SeekPosition) msg.obj);
405           break;
406         case MSG_SET_PLAYBACK_SPEED:
407           setPlaybackSpeedInternal((Float) msg.obj);
408           break;
409         case MSG_SET_SEEK_PARAMETERS:
410           setSeekParametersInternal((SeekParameters) msg.obj);
411           break;
412         case MSG_SET_FOREGROUND_MODE:
413           setForegroundModeInternal(
414               /* foregroundMode= */ msg.arg1 != 0, /* processedFlag= */ (AtomicBoolean) msg.obj);
415           break;
416         case MSG_STOP:
417           stopInternal(
418               /* forceResetRenderers= */ false,
419               /* resetPositionAndState= */ msg.arg1 != 0,
420               /* acknowledgeStop= */ true);
421           break;
422         case MSG_PERIOD_PREPARED:
423           handlePeriodPrepared((MediaPeriod) msg.obj);
424           break;
425         case MSG_SOURCE_CONTINUE_LOADING_REQUESTED:
426           handleContinueLoadingRequested((MediaPeriod) msg.obj);
427           break;
428         case MSG_TRACK_SELECTION_INVALIDATED:
429           reselectTracksInternal();
430           break;
431         case MSG_PLAYBACK_SPEED_CHANGED_INTERNAL:
432           handlePlaybackSpeed((Float) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0);
433           break;
434         case MSG_SEND_MESSAGE:
435           sendMessageInternal((PlayerMessage) msg.obj);
436           break;
437         case MSG_SEND_MESSAGE_TO_TARGET_THREAD:
438           sendMessageToTargetThread((PlayerMessage) msg.obj);
439           break;
440         case MSG_SET_MEDIA_SOURCES:
441           setMediaItemsInternal((MediaSourceListUpdateMessage) msg.obj);
442           break;
443         case MSG_ADD_MEDIA_SOURCES:
444           addMediaItemsInternal((MediaSourceListUpdateMessage) msg.obj, msg.arg1);
445           break;
446         case MSG_MOVE_MEDIA_SOURCES:
447           moveMediaItemsInternal((MoveMediaItemsMessage) msg.obj);
448           break;
449         case MSG_REMOVE_MEDIA_SOURCES:
450           removeMediaItemsInternal(msg.arg1, msg.arg2, (ShuffleOrder) msg.obj);
451           break;
452         case MSG_SET_SHUFFLE_ORDER:
453           setShuffleOrderInternal((ShuffleOrder) msg.obj);
454           break;
455         case MSG_PLAYLIST_UPDATE_REQUESTED:
456           mediaSourceListUpdateRequestedInternal();
457           break;
458         case MSG_SET_PAUSE_AT_END_OF_WINDOW:
459           setPauseAtEndOfWindowInternal(msg.arg1 != 0);
460           break;
461         case MSG_RELEASE:
462           releaseInternal();
463           // Return immediately to not send playback info updates after release.
464           return true;
465         default:
466           return false;
467       }
468       maybeNotifyPlaybackInfoChanged();
469     } catch (ExoPlaybackException e) {
470       Log.e(TAG, "Playback error", e);
471       stopInternal(
472           /* forceResetRenderers= */ true,
473           /* resetPositionAndState= */ false,
474           /* acknowledgeStop= */ false);
475       playbackInfo = playbackInfo.copyWithPlaybackError(e);
476       maybeNotifyPlaybackInfoChanged();
477     } catch (IOException e) {
478       ExoPlaybackException error = ExoPlaybackException.createForSource(e);
479       Log.e(TAG, "Playback error", error);
480       stopInternal(
481           /* forceResetRenderers= */ false,
482           /* resetPositionAndState= */ false,
483           /* acknowledgeStop= */ false);
484       playbackInfo = playbackInfo.copyWithPlaybackError(error);
485       maybeNotifyPlaybackInfoChanged();
486     } catch (RuntimeException | OutOfMemoryError e) {
487       ExoPlaybackException error =
488           e instanceof OutOfMemoryError
489               ? ExoPlaybackException.createForOutOfMemoryError((OutOfMemoryError) e)
490               : ExoPlaybackException.createForUnexpected((RuntimeException) e);
491       Log.e(TAG, "Playback error", error);
492       stopInternal(
493           /* forceResetRenderers= */ true,
494           /* resetPositionAndState= */ false,
495           /* acknowledgeStop= */ false);
496       playbackInfo = playbackInfo.copyWithPlaybackError(error);
497       maybeNotifyPlaybackInfoChanged();
498     }
499     return true;
500   }
501 
502   // Private methods.
503 
504   /**
505    * Blocks the current thread until {@link #releaseInternal()} is executed on the playback Thread.
506    *
507    * <p>If the current thread is interrupted while waiting for {@link #releaseInternal()} to
508    * complete, this method will delay throwing the {@link InterruptedException} to ensure that the
509    * underlying resources have been released, and will an {@link InterruptedException} <b>after</b>
510    * {@link #releaseInternal()} is complete.
511    *
512    * @throws {@link InterruptedException} if the current Thread was interrupted while waiting for
513    *     {@link #releaseInternal()} to complete.
514    */
waitUntilReleased()515   private synchronized void waitUntilReleased() throws InterruptedException {
516     InterruptedException interruptedException = null;
517     while (!released) {
518       try {
519         wait();
520       } catch (InterruptedException e) {
521         interruptedException = e;
522       }
523     }
524 
525     if (interruptedException != null) {
526       throw interruptedException;
527     }
528   }
529 
530   /**
531    * Blocks the current thread until {@link #releaseInternal()} is performed on the playback Thread
532    * or the specified amount of time has elapsed.
533    *
534    * <p>If the current thread is interrupted while waiting for {@link #releaseInternal()} to
535    * complete, this method will delay throwing the {@link InterruptedException} to ensure that the
536    * underlying resources have been released or the operation timed out, and will throw an {@link
537    * InterruptedException} afterwards.
538    *
539    * @param timeoutMs the time in milliseconds to wait for {@link #releaseInternal()} to complete.
540    * @throws {@link InterruptedException} if the current Thread was interrupted while waiting for
541    *     {@link #releaseInternal()} to complete.
542    */
waitUntilReleased(long timeoutMs)543   private synchronized void waitUntilReleased(long timeoutMs) throws InterruptedException {
544     long deadlineMs = clock.elapsedRealtime() + timeoutMs;
545     long remainingMs = timeoutMs;
546     InterruptedException interruptedException = null;
547     while (!released && remainingMs > 0) {
548       try {
549         wait(remainingMs);
550       } catch (InterruptedException e) {
551         interruptedException = e;
552       }
553       remainingMs = deadlineMs - clock.elapsedRealtime();
554     }
555 
556     if (interruptedException != null) {
557       throw interruptedException;
558     }
559   }
560 
setState(int state)561   private void setState(int state) {
562     if (playbackInfo.playbackState != state) {
563       playbackInfo = playbackInfo.copyWithPlaybackState(state);
564     }
565   }
566 
maybeNotifyPlaybackInfoChanged()567   private void maybeNotifyPlaybackInfoChanged() {
568     playbackInfoUpdate.setPlaybackInfo(playbackInfo);
569     if (playbackInfoUpdate.hasPendingChange) {
570       eventHandler.obtainMessage(MSG_PLAYBACK_INFO_CHANGED, playbackInfoUpdate).sendToTarget();
571       playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo);
572     }
573   }
574 
prepareInternal()575   private void prepareInternal() {
576     playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
577     resetInternal(
578         /* resetRenderers= */ false,
579         /* resetPosition= */ false,
580         /* releaseMediaSourceList= */ false,
581         /* clearMediaSourceList= */ false,
582         /* resetError= */ true);
583     loadControl.onPrepared();
584     setState(playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING);
585     mediaSourceList.prepare(bandwidthMeter.getTransferListener());
586     handler.sendEmptyMessage(MSG_DO_SOME_WORK);
587   }
588 
setMediaItemsInternal(MediaSourceListUpdateMessage mediaSourceListUpdateMessage)589   private void setMediaItemsInternal(MediaSourceListUpdateMessage mediaSourceListUpdateMessage)
590       throws ExoPlaybackException {
591     playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
592     if (mediaSourceListUpdateMessage.windowIndex != C.INDEX_UNSET) {
593       pendingInitialSeekPosition =
594           new SeekPosition(
595               new MediaSourceList.PlaylistTimeline(
596                   mediaSourceListUpdateMessage.mediaSourceHolders,
597                   mediaSourceListUpdateMessage.shuffleOrder),
598               mediaSourceListUpdateMessage.windowIndex,
599               mediaSourceListUpdateMessage.positionUs);
600     }
601     Timeline timeline =
602         mediaSourceList.setMediaSources(
603             mediaSourceListUpdateMessage.mediaSourceHolders,
604             mediaSourceListUpdateMessage.shuffleOrder);
605     handleMediaSourceListInfoRefreshed(timeline);
606   }
607 
addMediaItemsInternal(MediaSourceListUpdateMessage addMessage, int insertionIndex)608   private void addMediaItemsInternal(MediaSourceListUpdateMessage addMessage, int insertionIndex)
609       throws ExoPlaybackException {
610     playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
611     Timeline timeline =
612         mediaSourceList.addMediaSources(
613             insertionIndex == C.INDEX_UNSET ? mediaSourceList.getSize() : insertionIndex,
614             addMessage.mediaSourceHolders,
615             addMessage.shuffleOrder);
616     handleMediaSourceListInfoRefreshed(timeline);
617   }
618 
moveMediaItemsInternal(MoveMediaItemsMessage moveMediaItemsMessage)619   private void moveMediaItemsInternal(MoveMediaItemsMessage moveMediaItemsMessage)
620       throws ExoPlaybackException {
621     playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
622     Timeline timeline =
623         mediaSourceList.moveMediaSourceRange(
624             moveMediaItemsMessage.fromIndex,
625             moveMediaItemsMessage.toIndex,
626             moveMediaItemsMessage.newFromIndex,
627             moveMediaItemsMessage.shuffleOrder);
628     handleMediaSourceListInfoRefreshed(timeline);
629   }
630 
removeMediaItemsInternal(int fromIndex, int toIndex, ShuffleOrder shuffleOrder)631   private void removeMediaItemsInternal(int fromIndex, int toIndex, ShuffleOrder shuffleOrder)
632       throws ExoPlaybackException {
633     playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
634     Timeline timeline = mediaSourceList.removeMediaSourceRange(fromIndex, toIndex, shuffleOrder);
635     handleMediaSourceListInfoRefreshed(timeline);
636   }
637 
mediaSourceListUpdateRequestedInternal()638   private void mediaSourceListUpdateRequestedInternal() throws ExoPlaybackException {
639     handleMediaSourceListInfoRefreshed(mediaSourceList.createTimeline());
640   }
641 
setShuffleOrderInternal(ShuffleOrder shuffleOrder)642   private void setShuffleOrderInternal(ShuffleOrder shuffleOrder) throws ExoPlaybackException {
643     playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
644     Timeline timeline = mediaSourceList.setShuffleOrder(shuffleOrder);
645     handleMediaSourceListInfoRefreshed(timeline);
646   }
647 
setPlayWhenReadyInternal( boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason, boolean operationAck, @Player.PlayWhenReadyChangeReason int reason)648   private void setPlayWhenReadyInternal(
649       boolean playWhenReady,
650       @PlaybackSuppressionReason int playbackSuppressionReason,
651       boolean operationAck,
652       @Player.PlayWhenReadyChangeReason int reason)
653       throws ExoPlaybackException {
654     playbackInfoUpdate.incrementPendingOperationAcks(operationAck ? 1 : 0);
655     playbackInfoUpdate.setPlayWhenReadyChangeReason(reason);
656     playbackInfo = playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason);
657     rebuffering = false;
658     if (!shouldPlayWhenReady()) {
659       stopRenderers();
660       updatePlaybackPositions();
661     } else {
662       if (playbackInfo.playbackState == Player.STATE_READY) {
663         startRenderers();
664         handler.sendEmptyMessage(MSG_DO_SOME_WORK);
665       } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) {
666         handler.sendEmptyMessage(MSG_DO_SOME_WORK);
667       }
668     }
669   }
670 
setPauseAtEndOfWindowInternal(boolean pauseAtEndOfWindow)671   private void setPauseAtEndOfWindowInternal(boolean pauseAtEndOfWindow)
672       throws ExoPlaybackException {
673     this.pauseAtEndOfWindow = pauseAtEndOfWindow;
674     if (queue.getReadingPeriod() != queue.getPlayingPeriod()) {
675       seekToCurrentPosition(/* sendDiscontinuity= */ true);
676     }
677     resetPendingPauseAtEndOfPeriod();
678     handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
679   }
680 
setRepeatModeInternal(@layer.RepeatMode int repeatMode)681   private void setRepeatModeInternal(@Player.RepeatMode int repeatMode)
682       throws ExoPlaybackException {
683     this.repeatMode = repeatMode;
684     if (!queue.updateRepeatMode(playbackInfo.timeline, repeatMode)) {
685       seekToCurrentPosition(/* sendDiscontinuity= */ true);
686     }
687     handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
688   }
689 
setShuffleModeEnabledInternal(boolean shuffleModeEnabled)690   private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled)
691       throws ExoPlaybackException {
692     this.shuffleModeEnabled = shuffleModeEnabled;
693     if (!queue.updateShuffleModeEnabled(playbackInfo.timeline, shuffleModeEnabled)) {
694       seekToCurrentPosition(/* sendDiscontinuity= */ true);
695     }
696     handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
697   }
698 
seekToCurrentPosition(boolean sendDiscontinuity)699   private void seekToCurrentPosition(boolean sendDiscontinuity) throws ExoPlaybackException {
700     // Renderers may have read from a period that's been removed. Seek back to the current
701     // position of the playing period to make sure none of the removed period is played.
702     MediaPeriodId periodId = queue.getPlayingPeriod().info.id;
703     long newPositionUs =
704         seekToPeriodPosition(
705             periodId,
706             playbackInfo.positionUs,
707             /* forceDisableRenderers= */ true,
708             /* forceBufferingState= */ false);
709     if (newPositionUs != playbackInfo.positionUs) {
710       playbackInfo =
711           handlePositionDiscontinuity(
712               periodId, newPositionUs, playbackInfo.requestedContentPositionUs);
713       if (sendDiscontinuity) {
714         playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
715       }
716     }
717   }
718 
startRenderers()719   private void startRenderers() throws ExoPlaybackException {
720     rebuffering = false;
721     mediaClock.start();
722     for (Renderer renderer : renderers) {
723       if (isRendererEnabled(renderer)) {
724         renderer.start();
725       }
726     }
727   }
728 
stopRenderers()729   private void stopRenderers() throws ExoPlaybackException {
730     mediaClock.stop();
731     for (Renderer renderer : renderers) {
732       if (isRendererEnabled(renderer)) {
733         ensureStopped(renderer);
734       }
735     }
736   }
737 
updatePlaybackPositions()738   private void updatePlaybackPositions() throws ExoPlaybackException {
739     MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
740     if (playingPeriodHolder == null) {
741       return;
742     }
743 
744     // Update the playback position.
745     long discontinuityPositionUs =
746         playingPeriodHolder.prepared
747             ? playingPeriodHolder.mediaPeriod.readDiscontinuity()
748             : C.TIME_UNSET;
749     if (discontinuityPositionUs != C.TIME_UNSET) {
750       resetRendererPosition(discontinuityPositionUs);
751       // A MediaPeriod may report a discontinuity at the current playback position to ensure the
752       // renderers are flushed. Only report the discontinuity externally if the position changed.
753       if (discontinuityPositionUs != playbackInfo.positionUs) {
754         playbackInfo =
755             handlePositionDiscontinuity(
756                 playbackInfo.periodId,
757                 discontinuityPositionUs,
758                 playbackInfo.requestedContentPositionUs);
759         playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
760       }
761     } else {
762       rendererPositionUs =
763           mediaClock.syncAndGetPositionUs(
764               /* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod());
765       long periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs);
766       maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs);
767       playbackInfo.positionUs = periodPositionUs;
768     }
769 
770     // Update the buffered position and total buffered duration.
771     MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod();
772     playbackInfo.bufferedPositionUs = loadingPeriod.getBufferedPositionUs();
773     playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs();
774   }
775 
doSomeWork()776   private void doSomeWork() throws ExoPlaybackException, IOException {
777     long operationStartTimeMs = clock.uptimeMillis();
778     updatePeriods();
779 
780     if (playbackInfo.playbackState == Player.STATE_IDLE
781         || playbackInfo.playbackState == Player.STATE_ENDED) {
782       // Remove all messages. Prepare (in case of IDLE) or seek (in case of ENDED) will resume.
783       handler.removeMessages(MSG_DO_SOME_WORK);
784       return;
785     }
786 
787     @Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
788     if (playingPeriodHolder == null) {
789       // We're still waiting until the playing period is available.
790       scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
791       return;
792     }
793 
794     TraceUtil.beginSection("doSomeWork");
795 
796     updatePlaybackPositions();
797 
798     boolean renderersEnded = true;
799     boolean renderersAllowPlayback = true;
800     if (playingPeriodHolder.prepared) {
801       long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
802       playingPeriodHolder.mediaPeriod.discardBuffer(
803           playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
804       for (int i = 0; i < renderers.length; i++) {
805         Renderer renderer = renderers[i];
806         if (!isRendererEnabled(renderer)) {
807           continue;
808         }
809         // TODO: Each renderer should return the maximum delay before which it wishes to be called
810         // again. The minimum of these values should then be used as the delay before the next
811         // invocation of this method.
812         renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
813         renderersEnded = renderersEnded && renderer.isEnded();
814         // Determine whether the renderer allows playback to continue. Playback can continue if the
815         // renderer is ready or ended. Also continue playback if the renderer is reading ahead into
816         // the next stream or is waiting for the next stream. This is to avoid getting stuck if
817         // tracks in the current period have uneven durations and are still being read by another
818         // renderer. See: https://github.com/google/ExoPlayer/issues/1874.
819         boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream();
820         boolean isWaitingForNextStream =
821             !isReadingAhead
822                 && playingPeriodHolder.getNext() != null
823                 && renderer.hasReadStreamToEnd();
824         boolean allowsPlayback =
825             isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded();
826         renderersAllowPlayback = renderersAllowPlayback && allowsPlayback;
827         if (!allowsPlayback) {
828           renderer.maybeThrowStreamError();
829         }
830       }
831     } else {
832       playingPeriodHolder.mediaPeriod.maybeThrowPrepareError();
833     }
834 
835     long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
836     boolean finishedRendering =
837         renderersEnded
838             && playingPeriodHolder.prepared
839             && (playingPeriodDurationUs == C.TIME_UNSET
840                 || playingPeriodDurationUs <= playbackInfo.positionUs);
841     if (finishedRendering && pendingPauseAtEndOfPeriod) {
842       pendingPauseAtEndOfPeriod = false;
843       setPlayWhenReadyInternal(
844           /* playWhenReady= */ false,
845           playbackInfo.playbackSuppressionReason,
846           /* operationAck= */ false,
847           Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM);
848     }
849     if (finishedRendering && playingPeriodHolder.info.isFinal) {
850       setState(Player.STATE_ENDED);
851       stopRenderers();
852     } else if (playbackInfo.playbackState == Player.STATE_BUFFERING
853         && shouldTransitionToReadyState(renderersAllowPlayback)) {
854       setState(Player.STATE_READY);
855       if (shouldPlayWhenReady()) {
856         startRenderers();
857       }
858     } else if (playbackInfo.playbackState == Player.STATE_READY
859         && !(enabledRendererCount == 0 ? isTimelineReady() : renderersAllowPlayback)) {
860       rebuffering = shouldPlayWhenReady();
861       setState(Player.STATE_BUFFERING);
862       stopRenderers();
863     }
864 
865     if (playbackInfo.playbackState == Player.STATE_BUFFERING) {
866       for (int i = 0; i < renderers.length; i++) {
867         if (isRendererEnabled(renderers[i])
868             && renderers[i].getStream() == playingPeriodHolder.sampleStreams[i]) {
869           renderers[i].maybeThrowStreamError();
870         }
871       }
872       if (throwWhenStuckBuffering
873           && !playbackInfo.isLoading
874           && playbackInfo.totalBufferedDurationUs < 500_000
875           && isLoadingPossible()) {
876         // Throw if the LoadControl prevents loading even if the buffer is empty or almost empty. We
877         // can't compare against 0 to account for small differences between the renderer position
878         // and buffered position in the media at the point where playback gets stuck.
879         throw new IllegalStateException("Playback stuck buffering and not loading");
880       }
881     }
882 
883     if ((shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY)
884         || playbackInfo.playbackState == Player.STATE_BUFFERING) {
885       scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
886     } else if (enabledRendererCount != 0 && playbackInfo.playbackState != Player.STATE_ENDED) {
887       scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
888     } else {
889       handler.removeMessages(MSG_DO_SOME_WORK);
890     }
891 
892     TraceUtil.endSection();
893   }
894 
scheduleNextWork(long thisOperationStartTimeMs, long intervalMs)895   private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) {
896     handler.removeMessages(MSG_DO_SOME_WORK);
897     handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs);
898   }
899 
seekToInternal(SeekPosition seekPosition)900   private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
901     playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
902 
903     MediaPeriodId periodId;
904     long periodPositionUs;
905     long requestedContentPosition;
906     boolean seekPositionAdjusted;
907     @Nullable
908     Pair<Object, Long> resolvedSeekPosition =
909         resolveSeekPosition(
910             playbackInfo.timeline,
911             seekPosition,
912             /* trySubsequentPeriods= */ true,
913             repeatMode,
914             shuffleModeEnabled,
915             window,
916             period);
917     if (resolvedSeekPosition == null) {
918       // The seek position was valid for the timeline that it was performed into, but the
919       // timeline has changed or is not ready and a suitable seek position could not be resolved.
920       Pair<MediaPeriodId, Long> firstPeriodAndPosition =
921           getDummyFirstMediaPeriodPosition(playbackInfo.timeline);
922       periodId = firstPeriodAndPosition.first;
923       periodPositionUs = firstPeriodAndPosition.second;
924       requestedContentPosition = C.TIME_UNSET;
925       seekPositionAdjusted = !playbackInfo.timeline.isEmpty();
926     } else {
927       // Update the resolved seek position to take ads into account.
928       Object periodUid = resolvedSeekPosition.first;
929       long resolvedContentPosition = resolvedSeekPosition.second;
930       requestedContentPosition =
931           seekPosition.windowPositionUs == C.TIME_UNSET ? C.TIME_UNSET : resolvedContentPosition;
932       periodId =
933           queue.resolveMediaPeriodIdForAds(
934               playbackInfo.timeline, periodUid, resolvedContentPosition);
935       if (periodId.isAd()) {
936         playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period);
937         periodPositionUs =
938             period.getFirstAdIndexToPlay(periodId.adGroupIndex) == periodId.adIndexInAdGroup
939                 ? period.getAdResumePositionUs()
940                 : 0;
941         seekPositionAdjusted = true;
942       } else {
943         periodPositionUs = resolvedContentPosition;
944         seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET;
945       }
946     }
947 
948     try {
949       if (playbackInfo.timeline.isEmpty()) {
950         // Save seek position for later, as we are still waiting for a prepared source.
951         pendingInitialSeekPosition = seekPosition;
952       } else if (resolvedSeekPosition == null) {
953         // End playback, as we didn't manage to find a valid seek position.
954         if (playbackInfo.playbackState != Player.STATE_IDLE) {
955           setState(Player.STATE_ENDED);
956         }
957         resetInternal(
958             /* resetRenderers= */ false,
959             /* resetPosition= */ true,
960             /* releaseMediaSourceList= */ false,
961             /* clearMediaSourceList= */ false,
962             /* resetError= */ true);
963       } else {
964         // Execute the seek in the current media periods.
965         long newPeriodPositionUs = periodPositionUs;
966         if (periodId.equals(playbackInfo.periodId)) {
967           MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
968           if (playingPeriodHolder != null
969               && playingPeriodHolder.prepared
970               && newPeriodPositionUs != 0) {
971             newPeriodPositionUs =
972                 playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs(
973                     newPeriodPositionUs, seekParameters);
974           }
975           if (C.usToMs(newPeriodPositionUs) == C.usToMs(playbackInfo.positionUs)
976               && (playbackInfo.playbackState == Player.STATE_BUFFERING
977                   || playbackInfo.playbackState == Player.STATE_READY)) {
978             // Seek will be performed to the current position. Do nothing.
979             periodPositionUs = playbackInfo.positionUs;
980             return;
981           }
982         }
983         newPeriodPositionUs =
984             seekToPeriodPosition(
985                 periodId,
986                 newPeriodPositionUs,
987                 /* forceBufferingState= */ playbackInfo.playbackState == Player.STATE_ENDED);
988         seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;
989         periodPositionUs = newPeriodPositionUs;
990       }
991     } finally {
992       playbackInfo =
993           handlePositionDiscontinuity(periodId, periodPositionUs, requestedContentPosition);
994       if (seekPositionAdjusted) {
995         playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT);
996       }
997     }
998   }
999 
seekToPeriodPosition( MediaPeriodId periodId, long periodPositionUs, boolean forceBufferingState)1000   private long seekToPeriodPosition(
1001       MediaPeriodId periodId, long periodPositionUs, boolean forceBufferingState)
1002       throws ExoPlaybackException {
1003     // Force disable renderers if they are reading from a period other than the one being played.
1004     return seekToPeriodPosition(
1005         periodId,
1006         periodPositionUs,
1007         queue.getPlayingPeriod() != queue.getReadingPeriod(),
1008         forceBufferingState);
1009   }
1010 
seekToPeriodPosition( MediaPeriodId periodId, long periodPositionUs, boolean forceDisableRenderers, boolean forceBufferingState)1011   private long seekToPeriodPosition(
1012       MediaPeriodId periodId,
1013       long periodPositionUs,
1014       boolean forceDisableRenderers,
1015       boolean forceBufferingState)
1016       throws ExoPlaybackException {
1017     stopRenderers();
1018     rebuffering = false;
1019     if (forceBufferingState || playbackInfo.playbackState == Player.STATE_READY) {
1020       setState(Player.STATE_BUFFERING);
1021     }
1022 
1023     // Find the requested period if it already exists.
1024     @Nullable MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod();
1025     @Nullable MediaPeriodHolder newPlayingPeriodHolder = oldPlayingPeriodHolder;
1026     while (newPlayingPeriodHolder != null) {
1027       if (periodId.equals(newPlayingPeriodHolder.info.id)) {
1028         break;
1029       }
1030       newPlayingPeriodHolder = newPlayingPeriodHolder.getNext();
1031     }
1032 
1033     // Disable all renderers if the period being played is changing, if the seek results in negative
1034     // renderer timestamps, or if forced.
1035     if (forceDisableRenderers
1036         || oldPlayingPeriodHolder != newPlayingPeriodHolder
1037         || (newPlayingPeriodHolder != null
1038             && newPlayingPeriodHolder.toRendererTime(periodPositionUs) < 0)) {
1039       for (Renderer renderer : renderers) {
1040         disableRenderer(renderer);
1041       }
1042       if (newPlayingPeriodHolder != null) {
1043         // Update the queue and reenable renderers if the requested media period already exists.
1044         while (queue.getPlayingPeriod() != newPlayingPeriodHolder) {
1045           queue.advancePlayingPeriod();
1046         }
1047         queue.removeAfter(newPlayingPeriodHolder);
1048         newPlayingPeriodHolder.setRendererOffset(/* rendererPositionOffsetUs= */ 0);
1049         enableRenderers();
1050       }
1051     }
1052 
1053     // Do the actual seeking.
1054     if (newPlayingPeriodHolder != null) {
1055       queue.removeAfter(newPlayingPeriodHolder);
1056       if (!newPlayingPeriodHolder.prepared) {
1057         newPlayingPeriodHolder.info =
1058             newPlayingPeriodHolder.info.copyWithStartPositionUs(periodPositionUs);
1059       } else {
1060         if (newPlayingPeriodHolder.info.durationUs != C.TIME_UNSET
1061             && periodPositionUs >= newPlayingPeriodHolder.info.durationUs) {
1062           // Make sure seek position doesn't exceed period duration.
1063           periodPositionUs = Math.max(0, newPlayingPeriodHolder.info.durationUs - 1);
1064         }
1065         if (newPlayingPeriodHolder.hasEnabledTracks) {
1066           periodPositionUs = newPlayingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs);
1067           newPlayingPeriodHolder.mediaPeriod.discardBuffer(
1068               periodPositionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
1069         }
1070       }
1071       resetRendererPosition(periodPositionUs);
1072       maybeContinueLoading();
1073     } else {
1074       // New period has not been prepared.
1075       queue.clear();
1076       resetRendererPosition(periodPositionUs);
1077     }
1078 
1079     handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
1080     handler.sendEmptyMessage(MSG_DO_SOME_WORK);
1081     return periodPositionUs;
1082   }
1083 
resetRendererPosition(long periodPositionUs)1084   private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException {
1085     MediaPeriodHolder playingMediaPeriod = queue.getPlayingPeriod();
1086     rendererPositionUs =
1087         playingMediaPeriod == null
1088             ? periodPositionUs
1089             : playingMediaPeriod.toRendererTime(periodPositionUs);
1090     mediaClock.resetPosition(rendererPositionUs);
1091     for (Renderer renderer : renderers) {
1092       if (isRendererEnabled(renderer)) {
1093         renderer.resetPosition(rendererPositionUs);
1094       }
1095     }
1096     notifyTrackSelectionDiscontinuity();
1097   }
1098 
setPlaybackSpeedInternal(float playbackSpeed)1099   private void setPlaybackSpeedInternal(float playbackSpeed) {
1100     mediaClock.setPlaybackSpeed(playbackSpeed);
1101     sendPlaybackSpeedChangedInternal(mediaClock.getPlaybackSpeed(), /* acknowledgeCommand= */ true);
1102   }
1103 
setSeekParametersInternal(SeekParameters seekParameters)1104   private void setSeekParametersInternal(SeekParameters seekParameters) {
1105     this.seekParameters = seekParameters;
1106   }
1107 
setForegroundModeInternal( boolean foregroundMode, @Nullable AtomicBoolean processedFlag)1108   private void setForegroundModeInternal(
1109       boolean foregroundMode, @Nullable AtomicBoolean processedFlag) {
1110     if (this.foregroundMode != foregroundMode) {
1111       this.foregroundMode = foregroundMode;
1112       if (!foregroundMode) {
1113         for (Renderer renderer : renderers) {
1114           if (!isRendererEnabled(renderer)) {
1115             renderer.reset();
1116           }
1117         }
1118       }
1119     }
1120     if (processedFlag != null) {
1121       synchronized (this) {
1122         processedFlag.set(true);
1123         notifyAll();
1124       }
1125     }
1126   }
1127 
stopInternal( boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop)1128   private void stopInternal(
1129       boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) {
1130     resetInternal(
1131         /* resetRenderers= */ forceResetRenderers || !foregroundMode,
1132         /* resetPosition= */ resetPositionAndState,
1133         /* releaseMediaSourceList= */ true,
1134         /* clearMediaSourceList= */ resetPositionAndState,
1135         /* resetError= */ resetPositionAndState);
1136     playbackInfoUpdate.incrementPendingOperationAcks(acknowledgeStop ? 1 : 0);
1137     loadControl.onStopped();
1138     setState(Player.STATE_IDLE);
1139   }
1140 
releaseInternal()1141   private void releaseInternal() {
1142     resetInternal(
1143         /* resetRenderers= */ true,
1144         /* resetPosition= */ true,
1145         /* releaseMediaSourceList= */ true,
1146         /* clearMediaSourceList= */ true,
1147         /* resetError= */ false);
1148     loadControl.onReleased();
1149     setState(Player.STATE_IDLE);
1150     internalPlaybackThread.quit();
1151     synchronized (this) {
1152       released = true;
1153       notifyAll();
1154     }
1155   }
1156 
resetInternal( boolean resetRenderers, boolean resetPosition, boolean releaseMediaSourceList, boolean clearMediaSourceList, boolean resetError)1157   private void resetInternal(
1158       boolean resetRenderers,
1159       boolean resetPosition,
1160       boolean releaseMediaSourceList,
1161       boolean clearMediaSourceList,
1162       boolean resetError) {
1163     handler.removeMessages(MSG_DO_SOME_WORK);
1164     rebuffering = false;
1165     mediaClock.stop();
1166     rendererPositionUs = 0;
1167     for (Renderer renderer : renderers) {
1168       try {
1169         disableRenderer(renderer);
1170       } catch (ExoPlaybackException | RuntimeException e) {
1171         // There's nothing we can do.
1172         Log.e(TAG, "Disable failed.", e);
1173       }
1174     }
1175     if (resetRenderers) {
1176       for (Renderer renderer : renderers) {
1177         try {
1178           renderer.reset();
1179         } catch (RuntimeException e) {
1180           // There's nothing we can do.
1181           Log.e(TAG, "Reset failed.", e);
1182         }
1183       }
1184     }
1185     enabledRendererCount = 0;
1186 
1187     Timeline timeline = playbackInfo.timeline;
1188     if (clearMediaSourceList) {
1189       timeline = mediaSourceList.clear(/* shuffleOrder= */ null);
1190       for (PendingMessageInfo pendingMessageInfo : pendingMessages) {
1191         pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false);
1192       }
1193       pendingMessages.clear();
1194       nextPendingMessageIndex = 0;
1195       resetPosition = true;
1196     }
1197     MediaPeriodId mediaPeriodId = playbackInfo.periodId;
1198     long startPositionUs = playbackInfo.positionUs;
1199     long requestedContentPositionUs =
1200         shouldUseRequestedContentPosition(playbackInfo, period, window)
1201             ? playbackInfo.requestedContentPositionUs
1202             : playbackInfo.positionUs;
1203     boolean resetTrackInfo = clearMediaSourceList;
1204     if (resetPosition) {
1205       pendingInitialSeekPosition = null;
1206       Pair<MediaPeriodId, Long> firstPeriodAndPosition = getDummyFirstMediaPeriodPosition(timeline);
1207       mediaPeriodId = firstPeriodAndPosition.first;
1208       startPositionUs = firstPeriodAndPosition.second;
1209       requestedContentPositionUs = C.TIME_UNSET;
1210       if (!mediaPeriodId.equals(playbackInfo.periodId)) {
1211         resetTrackInfo = true;
1212       }
1213     }
1214 
1215     queue.clear();
1216     shouldContinueLoading = false;
1217 
1218     playbackInfo =
1219         new PlaybackInfo(
1220             timeline,
1221             mediaPeriodId,
1222             requestedContentPositionUs,
1223             playbackInfo.playbackState,
1224             resetError ? null : playbackInfo.playbackError,
1225             /* isLoading= */ false,
1226             resetTrackInfo ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
1227             resetTrackInfo ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
1228             mediaPeriodId,
1229             playbackInfo.playWhenReady,
1230             playbackInfo.playbackSuppressionReason,
1231             startPositionUs,
1232             /* totalBufferedDurationUs= */ 0,
1233             startPositionUs);
1234     if (releaseMediaSourceList) {
1235       mediaSourceList.release();
1236     }
1237   }
1238 
getDummyFirstMediaPeriodPosition(Timeline timeline)1239   private Pair<MediaPeriodId, Long> getDummyFirstMediaPeriodPosition(Timeline timeline) {
1240     if (timeline.isEmpty()) {
1241       return Pair.create(PlaybackInfo.getDummyPeriodForEmptyTimeline(), 0L);
1242     }
1243     int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
1244     Pair<Object, Long> firstPeriodAndPosition =
1245         timeline.getPeriodPosition(
1246             window, period, firstWindowIndex, /* windowPositionUs= */ C.TIME_UNSET);
1247     // Add ad metadata if any and propagate the window sequence number to new period id.
1248     MediaPeriodId firstPeriodId =
1249         queue.resolveMediaPeriodIdForAds(
1250             timeline, firstPeriodAndPosition.first, /* positionUs= */ 0);
1251     long positionUs = firstPeriodAndPosition.second;
1252     if (firstPeriodId.isAd()) {
1253       timeline.getPeriodByUid(firstPeriodId.periodUid, period);
1254       positionUs =
1255           firstPeriodId.adIndexInAdGroup == period.getFirstAdIndexToPlay(firstPeriodId.adGroupIndex)
1256               ? period.getAdResumePositionUs()
1257               : 0;
1258     }
1259     return Pair.create(firstPeriodId, positionUs);
1260   }
1261 
sendMessageInternal(PlayerMessage message)1262   private void sendMessageInternal(PlayerMessage message) throws ExoPlaybackException {
1263     if (message.getPositionMs() == C.TIME_UNSET) {
1264       // If no delivery time is specified, trigger immediate message delivery.
1265       sendMessageToTarget(message);
1266     } else if (playbackInfo.timeline.isEmpty()) {
1267       // Still waiting for initial timeline to resolve position.
1268       pendingMessages.add(new PendingMessageInfo(message));
1269     } else {
1270       PendingMessageInfo pendingMessageInfo = new PendingMessageInfo(message);
1271       if (resolvePendingMessagePosition(
1272           pendingMessageInfo,
1273           /* newTimeline= */ playbackInfo.timeline,
1274           /* previousTimeline= */ playbackInfo.timeline,
1275           repeatMode,
1276           shuffleModeEnabled,
1277           window,
1278           period)) {
1279         pendingMessages.add(pendingMessageInfo);
1280         // Ensure new message is inserted according to playback order.
1281         Collections.sort(pendingMessages);
1282       } else {
1283         message.markAsProcessed(/* isDelivered= */ false);
1284       }
1285     }
1286   }
1287 
sendMessageToTarget(PlayerMessage message)1288   private void sendMessageToTarget(PlayerMessage message) throws ExoPlaybackException {
1289     if (message.getHandler().getLooper() == handler.getLooper()) {
1290       deliverMessage(message);
1291       if (playbackInfo.playbackState == Player.STATE_READY
1292           || playbackInfo.playbackState == Player.STATE_BUFFERING) {
1293         // The message may have caused something to change that now requires us to do work.
1294         handler.sendEmptyMessage(MSG_DO_SOME_WORK);
1295       }
1296     } else {
1297       handler.obtainMessage(MSG_SEND_MESSAGE_TO_TARGET_THREAD, message).sendToTarget();
1298     }
1299   }
1300 
sendMessageToTargetThread(final PlayerMessage message)1301   private void sendMessageToTargetThread(final PlayerMessage message) {
1302     Handler handler = message.getHandler();
1303     if (!handler.getLooper().getThread().isAlive()) {
1304       Log.w("TAG", "Trying to send message on a dead thread.");
1305       message.markAsProcessed(/* isDelivered= */ false);
1306       return;
1307     }
1308     handler.post(
1309         () -> {
1310           try {
1311             deliverMessage(message);
1312           } catch (ExoPlaybackException e) {
1313             Log.e(TAG, "Unexpected error delivering message on external thread.", e);
1314             throw new RuntimeException(e);
1315           }
1316         });
1317   }
1318 
deliverMessage(PlayerMessage message)1319   private void deliverMessage(PlayerMessage message) throws ExoPlaybackException {
1320     if (message.isCanceled()) {
1321       return;
1322     }
1323     try {
1324       message.getTarget().handleMessage(message.getType(), message.getPayload());
1325     } finally {
1326       message.markAsProcessed(/* isDelivered= */ true);
1327     }
1328   }
1329 
resolvePendingMessagePositions(Timeline newTimeline, Timeline previousTimeline)1330   private void resolvePendingMessagePositions(Timeline newTimeline, Timeline previousTimeline) {
1331     if (newTimeline.isEmpty() && previousTimeline.isEmpty()) {
1332       // Keep all messages unresolved until we have a non-empty timeline.
1333       return;
1334     }
1335     for (int i = pendingMessages.size() - 1; i >= 0; i--) {
1336       if (!resolvePendingMessagePosition(
1337           pendingMessages.get(i),
1338           newTimeline,
1339           previousTimeline,
1340           repeatMode,
1341           shuffleModeEnabled,
1342           window,
1343           period)) {
1344         // Unable to resolve a new position for the message. Remove it.
1345         pendingMessages.get(i).message.markAsProcessed(/* isDelivered= */ false);
1346         pendingMessages.remove(i);
1347       }
1348     }
1349     // Re-sort messages by playback order.
1350     Collections.sort(pendingMessages);
1351   }
1352 
maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs)1353   private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs)
1354       throws ExoPlaybackException {
1355     if (pendingMessages.isEmpty() || playbackInfo.periodId.isAd()) {
1356       return;
1357     }
1358     // If this is the first call after resetting the renderer position, include oldPeriodPositionUs
1359     // in potential trigger positions, but make sure we deliver it only once.
1360     if (deliverPendingMessageAtStartPositionRequired) {
1361       oldPeriodPositionUs--;
1362       deliverPendingMessageAtStartPositionRequired = false;
1363     }
1364 
1365     // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages)
1366     int currentPeriodIndex =
1367         playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid);
1368     PendingMessageInfo previousInfo =
1369         nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null;
1370     while (previousInfo != null
1371         && (previousInfo.resolvedPeriodIndex > currentPeriodIndex
1372             || (previousInfo.resolvedPeriodIndex == currentPeriodIndex
1373                 && previousInfo.resolvedPeriodTimeUs > oldPeriodPositionUs))) {
1374       nextPendingMessageIndex--;
1375       previousInfo =
1376           nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null;
1377     }
1378     PendingMessageInfo nextInfo =
1379         nextPendingMessageIndex < pendingMessages.size()
1380             ? pendingMessages.get(nextPendingMessageIndex)
1381             : null;
1382     while (nextInfo != null
1383         && nextInfo.resolvedPeriodUid != null
1384         && (nextInfo.resolvedPeriodIndex < currentPeriodIndex
1385             || (nextInfo.resolvedPeriodIndex == currentPeriodIndex
1386                 && nextInfo.resolvedPeriodTimeUs <= oldPeriodPositionUs))) {
1387       nextPendingMessageIndex++;
1388       nextInfo =
1389           nextPendingMessageIndex < pendingMessages.size()
1390               ? pendingMessages.get(nextPendingMessageIndex)
1391               : null;
1392     }
1393     // Check if any message falls within the covered time span.
1394     while (nextInfo != null
1395         && nextInfo.resolvedPeriodUid != null
1396         && nextInfo.resolvedPeriodIndex == currentPeriodIndex
1397         && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs
1398         && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) {
1399       try {
1400         sendMessageToTarget(nextInfo.message);
1401       } finally {
1402         if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) {
1403           pendingMessages.remove(nextPendingMessageIndex);
1404         } else {
1405           nextPendingMessageIndex++;
1406         }
1407       }
1408       nextInfo =
1409           nextPendingMessageIndex < pendingMessages.size()
1410               ? pendingMessages.get(nextPendingMessageIndex)
1411               : null;
1412     }
1413   }
1414 
1415   private void ensureStopped(Renderer renderer) throws ExoPlaybackException {
1416     if (renderer.getState() == Renderer.STATE_STARTED) {
1417       renderer.stop();
1418     }
1419   }
1420 
1421   private void disableRenderer(Renderer renderer) throws ExoPlaybackException {
1422     if (!isRendererEnabled(renderer)) {
1423       return;
1424     }
1425     mediaClock.onRendererDisabled(renderer);
1426     ensureStopped(renderer);
1427     renderer.disable();
1428     enabledRendererCount--;
1429   }
1430 
1431   private void reselectTracksInternal() throws ExoPlaybackException {
1432     float playbackSpeed = mediaClock.getPlaybackSpeed();
1433     // Reselect tracks on each period in turn, until the selection changes.
1434     MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
1435     MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
1436     boolean selectionsChangedForReadPeriod = true;
1437     TrackSelectorResult newTrackSelectorResult;
1438     while (true) {
1439       if (periodHolder == null || !periodHolder.prepared) {
1440         // The reselection did not change any prepared periods.
1441         return;
1442       }
1443       newTrackSelectorResult = periodHolder.selectTracks(playbackSpeed, playbackInfo.timeline);
1444       if (!newTrackSelectorResult.isEquivalent(periodHolder.getTrackSelectorResult())) {
1445         // Selected tracks have changed for this period.
1446         break;
1447       }
1448       if (periodHolder == readingPeriodHolder) {
1449         // The track reselection didn't affect any period that has been read.
1450         selectionsChangedForReadPeriod = false;
1451       }
1452       periodHolder = periodHolder.getNext();
1453     }
1454 
1455     if (selectionsChangedForReadPeriod) {
1456       // Update streams and rebuffer for the new selection, recreating all streams if reading ahead.
1457       MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
1458       boolean recreateStreams = queue.removeAfter(playingPeriodHolder);
1459 
1460       boolean[] streamResetFlags = new boolean[renderers.length];
1461       long periodPositionUs =
1462           playingPeriodHolder.applyTrackSelection(
1463               newTrackSelectorResult, playbackInfo.positionUs, recreateStreams, streamResetFlags);
1464       playbackInfo =
1465           handlePositionDiscontinuity(
1466               playbackInfo.periodId, periodPositionUs, playbackInfo.requestedContentPositionUs);
1467       if (playbackInfo.playbackState != Player.STATE_ENDED
1468           && periodPositionUs != playbackInfo.positionUs) {
1469         playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
1470         resetRendererPosition(periodPositionUs);
1471       }
1472 
1473       boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
1474       for (int i = 0; i < renderers.length; i++) {
1475         Renderer renderer = renderers[i];
1476         rendererWasEnabledFlags[i] = isRendererEnabled(renderer);
1477         SampleStream sampleStream = playingPeriodHolder.sampleStreams[i];
1478         if (rendererWasEnabledFlags[i]) {
1479           if (sampleStream != renderer.getStream()) {
1480             // We need to disable the renderer.
1481             disableRenderer(renderer);
1482           } else if (streamResetFlags[i]) {
1483             // The renderer will continue to consume from its current stream, but needs to be reset.
1484             renderer.resetPosition(rendererPositionUs);
1485           }
1486         }
1487       }
1488       enableRenderers(rendererWasEnabledFlags);
1489     } else {
1490       // Release and re-prepare/buffer periods after the one whose selection changed.
1491       queue.removeAfter(periodHolder);
1492       if (periodHolder.prepared) {
1493         long loadingPeriodPositionUs =
1494             Math.max(
1495                 periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs));
1496         periodHolder.applyTrackSelection(newTrackSelectorResult, loadingPeriodPositionUs, false);
1497       }
1498     }
1499     handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ true);
1500     if (playbackInfo.playbackState != Player.STATE_ENDED) {
1501       maybeContinueLoading();
1502       updatePlaybackPositions();
1503       handler.sendEmptyMessage(MSG_DO_SOME_WORK);
1504     }
1505   }
1506 
1507   private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) {
1508     MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
1509     while (periodHolder != null) {
1510       TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();
1511       for (TrackSelection trackSelection : trackSelections) {
1512         if (trackSelection != null) {
1513           trackSelection.onPlaybackSpeed(playbackSpeed);
1514         }
1515       }
1516       periodHolder = periodHolder.getNext();
1517     }
1518   }
1519 
1520   private void notifyTrackSelectionDiscontinuity() {
1521     MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
1522     while (periodHolder != null) {
1523       TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();
1524       for (TrackSelection trackSelection : trackSelections) {
1525         if (trackSelection != null) {
1526           trackSelection.onDiscontinuity();
1527         }
1528       }
1529       periodHolder = periodHolder.getNext();
1530     }
1531   }
1532 
1533   private boolean shouldTransitionToReadyState(boolean renderersReadyOrEnded) {
1534     if (enabledRendererCount == 0) {
1535       // If there are no enabled renderers, determine whether we're ready based on the timeline.
1536       return isTimelineReady();
1537     }
1538     if (!renderersReadyOrEnded) {
1539       return false;
1540     }
1541     if (!playbackInfo.isLoading) {
1542       // Renderers are ready and we're not loading. Transition to ready, since the alternative is
1543       // getting stuck waiting for additional media that's not being loaded.
1544       return true;
1545     }
1546     // Renderers are ready and we're loading. Ask the LoadControl whether to transition.
1547     MediaPeriodHolder loadingHolder = queue.getLoadingPeriod();
1548     boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal;
1549     return bufferedToEnd
1550         || loadControl.shouldStartPlayback(
1551             getTotalBufferedDurationUs(), mediaClock.getPlaybackSpeed(), rebuffering);
1552   }
1553 
1554   private boolean isTimelineReady() {
1555     MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
1556     long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
1557     return playingPeriodHolder.prepared
1558         && (playingPeriodDurationUs == C.TIME_UNSET
1559             || playbackInfo.positionUs < playingPeriodDurationUs
1560             || !shouldPlayWhenReady());
1561   }
1562 
1563   private void maybeThrowSourceInfoRefreshError() throws IOException {
1564     MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
1565     if (loadingPeriodHolder != null) {
1566       // Defer throwing until we read all available media periods.
1567       for (Renderer renderer : renderers) {
1568         if (isRendererEnabled(renderer) && !renderer.hasReadStreamToEnd()) {
1569           return;
1570         }
1571       }
1572     }
1573     mediaSourceList.maybeThrowSourceInfoRefreshError();
1574   }
1575 
1576   private void handleMediaSourceListInfoRefreshed(Timeline timeline) throws ExoPlaybackException {
1577     PositionUpdateForPlaylistChange positionUpdate =
1578         resolvePositionForPlaylistChange(
1579             timeline,
1580             playbackInfo,
1581             pendingInitialSeekPosition,
1582             queue,
1583             repeatMode,
1584             shuffleModeEnabled,
1585             window,
1586             period);
1587     MediaPeriodId newPeriodId = positionUpdate.periodId;
1588     long newRequestedContentPositionUs = positionUpdate.requestedContentPositionUs;
1589     boolean forceBufferingState = positionUpdate.forceBufferingState;
1590     long newPositionUs = positionUpdate.periodPositionUs;
1591     boolean periodPositionChanged =
1592         !playbackInfo.periodId.equals(newPeriodId) || newPositionUs != playbackInfo.positionUs;
1593 
1594     try {
1595       if (positionUpdate.endPlayback) {
1596         if (playbackInfo.playbackState != Player.STATE_IDLE) {
1597           setState(Player.STATE_ENDED);
1598         }
1599         resetInternal(
1600             /* resetRenderers= */ false,
1601             /* resetPosition= */ false,
1602             /* releaseMediaSourceList= */ false,
1603             /* clearMediaSourceList= */ false,
1604             /* resetError= */ true);
1605       }
1606       if (!periodPositionChanged) {
1607         // We can keep the current playing period. Update the rest of the queued periods.
1608         if (!queue.updateQueuedPeriods(
1609             timeline, rendererPositionUs, getMaxRendererReadPositionUs())) {
1610           seekToCurrentPosition(/* sendDiscontinuity= */ false);
1611         }
1612       } else if (!timeline.isEmpty()) {
1613         // Something changed. Seek to new start position.
1614         @Nullable MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
1615         while (periodHolder != null) {
1616           // Update the new playing media period info if it already exists.
1617           if (periodHolder.info.id.equals(newPeriodId)) {
1618             periodHolder.info = queue.getUpdatedMediaPeriodInfo(timeline, periodHolder.info);
1619           }
1620           periodHolder = periodHolder.getNext();
1621         }
1622         newPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs, forceBufferingState);
1623       }
1624     } finally {
1625       if (periodPositionChanged
1626           || newRequestedContentPositionUs != playbackInfo.requestedContentPositionUs) {
1627         playbackInfo =
1628             handlePositionDiscontinuity(newPeriodId, newPositionUs, newRequestedContentPositionUs);
1629       }
1630       resetPendingPauseAtEndOfPeriod();
1631       resolvePendingMessagePositions(
1632           /* newTimeline= */ timeline, /* previousTimeline= */ playbackInfo.timeline);
1633       playbackInfo = playbackInfo.copyWithTimeline(timeline);
1634       if (!timeline.isEmpty()) {
1635         // Retain pending seek position only while the timeline is still empty.
1636         pendingInitialSeekPosition = null;
1637       }
1638       handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
1639     }
1640   }
1641 
1642   private long getMaxRendererReadPositionUs() {
1643     MediaPeriodHolder readingHolder = queue.getReadingPeriod();
1644     if (readingHolder == null) {
1645       return 0;
1646     }
1647     long maxReadPositionUs = readingHolder.getRendererOffset();
1648     if (!readingHolder.prepared) {
1649       return maxReadPositionUs;
1650     }
1651     for (int i = 0; i < renderers.length; i++) {
1652       if (!isRendererEnabled(renderers[i])
1653           || renderers[i].getStream() != readingHolder.sampleStreams[i]) {
1654         // Ignore disabled renderers and renderers with sample streams from previous periods.
1655         continue;
1656       }
1657       long readingPositionUs = renderers[i].getReadingPositionUs();
1658       if (readingPositionUs == C.TIME_END_OF_SOURCE) {
1659         return C.TIME_END_OF_SOURCE;
1660       } else {
1661         maxReadPositionUs = Math.max(readingPositionUs, maxReadPositionUs);
1662       }
1663     }
1664     return maxReadPositionUs;
1665   }
1666 
1667   private void updatePeriods() throws ExoPlaybackException, IOException {
1668     if (playbackInfo.timeline.isEmpty() || !mediaSourceList.isPrepared()) {
1669       // We're waiting to get information about periods.
1670       mediaSourceList.maybeThrowSourceInfoRefreshError();
1671       return;
1672     }
1673     maybeUpdateLoadingPeriod();
1674     maybeUpdateReadingPeriod();
1675     maybeUpdateReadingRenderers();
1676     maybeUpdatePlayingPeriod();
1677   }
1678 
1679   private void maybeUpdateLoadingPeriod() throws ExoPlaybackException, IOException {
1680     queue.reevaluateBuffer(rendererPositionUs);
1681     if (queue.shouldLoadNextMediaPeriod()) {
1682       MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo);
1683       if (info == null) {
1684         maybeThrowSourceInfoRefreshError();
1685       } else {
1686         MediaPeriodHolder mediaPeriodHolder =
1687             queue.enqueueNextMediaPeriodHolder(
1688                 rendererCapabilities,
1689                 trackSelector,
1690                 loadControl.getAllocator(),
1691                 mediaSourceList,
1692                 info,
1693                 emptyTrackSelectorResult);
1694         mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
1695         if (queue.getPlayingPeriod() == mediaPeriodHolder) {
1696           resetRendererPosition(mediaPeriodHolder.getStartPositionRendererTime());
1697         }
1698         handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
1699       }
1700     }
1701     if (shouldContinueLoading) {
1702       // We should still be loading, except in the case that it's no longer possible (i.e., because
1703       // we've loaded the current playlist to the end).
1704       shouldContinueLoading = isLoadingPossible();
1705       updateIsLoading();
1706     } else {
1707       maybeContinueLoading();
1708     }
1709   }
1710 
1711   private void maybeUpdateReadingPeriod() {
1712     @Nullable MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
1713     if (readingPeriodHolder == null) {
1714       return;
1715     }
1716 
1717     if (readingPeriodHolder.getNext() == null || pendingPauseAtEndOfPeriod) {
1718       // We don't have a successor to advance the reading period to or we want to let them end
1719       // intentionally to pause at the end of the period.
1720       if (readingPeriodHolder.info.isFinal || pendingPauseAtEndOfPeriod) {
1721         for (int i = 0; i < renderers.length; i++) {
1722           Renderer renderer = renderers[i];
1723           SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
1724           // Defer setting the stream as final until the renderer has actually consumed the whole
1725           // stream in case of playlist changes that cause the stream to be no longer final.
1726           if (sampleStream != null
1727               && renderer.getStream() == sampleStream
1728               && renderer.hasReadStreamToEnd()) {
1729             renderer.setCurrentStreamFinal();
1730           }
1731         }
1732       }
1733       return;
1734     }
1735 
1736     if (!hasReadingPeriodFinishedReading()) {
1737       return;
1738     }
1739 
1740     if (!readingPeriodHolder.getNext().prepared
1741         && rendererPositionUs < readingPeriodHolder.getNext().getStartPositionRendererTime()) {
1742       // The successor is not prepared yet and playback hasn't reached the transition point.
1743       return;
1744     }
1745 
1746     TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();
1747     readingPeriodHolder = queue.advanceReadingPeriod();
1748     TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();
1749 
1750     if (readingPeriodHolder.prepared
1751         && readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) {
1752       // The new period starts with a discontinuity, so the renderers will play out all data, then
1753       // be disabled and re-enabled when they start playing the next period.
1754       setAllRendererStreamsFinal();
1755       return;
1756     }
1757     for (int i = 0; i < renderers.length; i++) {
1758       boolean oldRendererEnabled = oldTrackSelectorResult.isRendererEnabled(i);
1759       boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(i);
1760       if (oldRendererEnabled && !renderers[i].isCurrentStreamFinal()) {
1761         boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE;
1762         RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i];
1763         RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i];
1764         if (!newRendererEnabled || !newConfig.equals(oldConfig) || isNoSampleRenderer) {
1765           // The renderer will be disabled when transitioning to playing the next period, because
1766           // there's no new selection, or because a configuration change is required, or because
1767           // it's a no-sample renderer for which rendererOffsetUs should be updated only when
1768           // starting to play the next period. Mark the SampleStream as final to play out any
1769           // remaining data.
1770           renderers[i].setCurrentStreamFinal();
1771         }
1772       }
1773     }
1774   }
1775 
1776   private void maybeUpdateReadingRenderers() throws ExoPlaybackException {
1777     @Nullable MediaPeriodHolder readingPeriod = queue.getReadingPeriod();
1778     if (readingPeriod == null
1779         || queue.getPlayingPeriod() == readingPeriod
1780         || readingPeriod.allRenderersEnabled) {
1781       // Not reading ahead or all renderers updated.
1782       return;
1783     }
1784     if (replaceStreamsOrDisableRendererForTransition()) {
1785       enableRenderers();
1786     }
1787   }
1788 
1789   private boolean replaceStreamsOrDisableRendererForTransition() throws ExoPlaybackException {
1790     MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
1791     TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();
1792     boolean needsToWaitForRendererToEnd = false;
1793     for (int i = 0; i < renderers.length; i++) {
1794       Renderer renderer = renderers[i];
1795       if (!isRendererEnabled(renderer)) {
1796         continue;
1797       }
1798       boolean rendererIsReadingOldStream =
1799           renderer.getStream() != readingPeriodHolder.sampleStreams[i];
1800       boolean rendererShouldBeEnabled = newTrackSelectorResult.isRendererEnabled(i);
1801       if (rendererShouldBeEnabled && !rendererIsReadingOldStream) {
1802         // All done.
1803         continue;
1804       }
1805       if (!renderer.isCurrentStreamFinal()) {
1806         // The renderer stream is not final, so we can replace the sample streams immediately.
1807         Format[] formats = getFormats(newTrackSelectorResult.selections.get(i));
1808         renderer.replaceStream(
1809             formats, readingPeriodHolder.sampleStreams[i], readingPeriodHolder.getRendererOffset());
1810       } else if (renderer.isEnded()) {
1811         // The renderer has finished playback, so we can disable it now.
1812         disableRenderer(renderer);
1813       } else {
1814         // We need to wait until rendering finished before disabling the renderer.
1815         needsToWaitForRendererToEnd = true;
1816       }
1817     }
1818     return !needsToWaitForRendererToEnd;
1819   }
1820 
1821   private void maybeUpdatePlayingPeriod() throws ExoPlaybackException {
1822     boolean advancedPlayingPeriod = false;
1823     while (shouldAdvancePlayingPeriod()) {
1824       if (advancedPlayingPeriod) {
1825         // If we advance more than one period at a time, notify listeners after each update.
1826         maybeNotifyPlaybackInfoChanged();
1827       }
1828       MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod();
1829       MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod();
1830       playbackInfo =
1831           handlePositionDiscontinuity(
1832               newPlayingPeriodHolder.info.id,
1833               newPlayingPeriodHolder.info.startPositionUs,
1834               newPlayingPeriodHolder.info.requestedContentPositionUs);
1835       int discontinuityReason =
1836           oldPlayingPeriodHolder.info.isLastInTimelinePeriod
1837               ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
1838               : Player.DISCONTINUITY_REASON_AD_INSERTION;
1839       playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason);
1840       resetPendingPauseAtEndOfPeriod();
1841       updatePlaybackPositions();
1842       advancedPlayingPeriod = true;
1843     }
1844   }
1845 
1846   private void resetPendingPauseAtEndOfPeriod() {
1847     @Nullable MediaPeriodHolder playingPeriod = queue.getPlayingPeriod();
1848     pendingPauseAtEndOfPeriod =
1849         playingPeriod != null && playingPeriod.info.isLastInTimelineWindow && pauseAtEndOfWindow;
1850   }
1851 
1852   private boolean shouldAdvancePlayingPeriod() {
1853     if (!shouldPlayWhenReady()) {
1854       return false;
1855     }
1856     if (pendingPauseAtEndOfPeriod) {
1857       return false;
1858     }
1859     MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
1860     if (playingPeriodHolder == null) {
1861       return false;
1862     }
1863     MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext();
1864     return nextPlayingPeriodHolder != null
1865         && rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime()
1866         && nextPlayingPeriodHolder.allRenderersEnabled;
1867   }
1868 
1869   private boolean hasReadingPeriodFinishedReading() {
1870     MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
1871     if (!readingPeriodHolder.prepared) {
1872       return false;
1873     }
1874     for (int i = 0; i < renderers.length; i++) {
1875       Renderer renderer = renderers[i];
1876       SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
1877       if (renderer.getStream() != sampleStream
1878           || (sampleStream != null && !renderer.hasReadStreamToEnd())) {
1879         // The current reading period is still being read by at least one renderer.
1880         return false;
1881       }
1882     }
1883     return true;
1884   }
1885 
1886   private void setAllRendererStreamsFinal() {
1887     for (Renderer renderer : renderers) {
1888       if (renderer.getStream() != null) {
1889         renderer.setCurrentStreamFinal();
1890       }
1891     }
1892   }
1893 
1894   private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException {
1895     if (!queue.isLoading(mediaPeriod)) {
1896       // Stale event.
1897       return;
1898     }
1899     MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
1900     loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackSpeed(), playbackInfo.timeline);
1901     updateLoadControlTrackSelection(
1902         loadingPeriodHolder.getTrackGroups(), loadingPeriodHolder.getTrackSelectorResult());
1903     if (loadingPeriodHolder == queue.getPlayingPeriod()) {
1904       // This is the first prepared period, so update the position and the renderers.
1905       resetRendererPosition(loadingPeriodHolder.info.startPositionUs);
1906       enableRenderers();
1907       playbackInfo =
1908           handlePositionDiscontinuity(
1909               playbackInfo.periodId,
1910               loadingPeriodHolder.info.startPositionUs,
1911               playbackInfo.requestedContentPositionUs);
1912     }
1913     maybeContinueLoading();
1914   }
1915 
1916   private void handleContinueLoadingRequested(MediaPeriod mediaPeriod) {
1917     if (!queue.isLoading(mediaPeriod)) {
1918       // Stale event.
1919       return;
1920     }
1921     queue.reevaluateBuffer(rendererPositionUs);
1922     maybeContinueLoading();
1923   }
1924 
1925   private void handlePlaybackSpeed(float playbackSpeed, boolean acknowledgeCommand)
1926       throws ExoPlaybackException {
1927     eventHandler
1928         .obtainMessage(MSG_PLAYBACK_SPEED_CHANGED, acknowledgeCommand ? 1 : 0, 0, playbackSpeed)
1929         .sendToTarget();
1930     updateTrackSelectionPlaybackSpeed(playbackSpeed);
1931     for (Renderer renderer : renderers) {
1932       if (renderer != null) {
1933         renderer.setOperatingRate(playbackSpeed);
1934       }
1935     }
1936   }
1937 
1938   private void maybeContinueLoading() {
1939     shouldContinueLoading = shouldContinueLoading();
1940     if (shouldContinueLoading) {
1941       queue.getLoadingPeriod().continueLoading(rendererPositionUs);
1942     }
1943     updateIsLoading();
1944   }
1945 
1946   private boolean shouldContinueLoading() {
1947     if (!isLoadingPossible()) {
1948       return false;
1949     }
1950     MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
1951     long bufferedDurationUs =
1952         getTotalBufferedDurationUs(loadingPeriodHolder.getNextLoadPositionUs());
1953     long playbackPositionUs =
1954         loadingPeriodHolder == queue.getPlayingPeriod()
1955             ? loadingPeriodHolder.toPeriodTime(rendererPositionUs)
1956             : loadingPeriodHolder.toPeriodTime(rendererPositionUs)
1957                 - loadingPeriodHolder.info.startPositionUs;
1958     return loadControl.shouldContinueLoading(
1959         playbackPositionUs, bufferedDurationUs, mediaClock.getPlaybackSpeed());
1960   }
1961 
1962   private boolean isLoadingPossible() {
1963     MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
1964     if (loadingPeriodHolder == null) {
1965       return false;
1966     }
1967     long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs();
1968     if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
1969       return false;
1970     }
1971     return true;
1972   }
1973 
1974   private void updateIsLoading() {
1975     MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod();
1976     boolean isLoading =
1977         shouldContinueLoading || (loadingPeriod != null && loadingPeriod.mediaPeriod.isLoading());
1978     if (isLoading != playbackInfo.isLoading) {
1979       playbackInfo = playbackInfo.copyWithIsLoading(isLoading);
1980     }
1981   }
1982 
1983   @CheckResult
1984   private PlaybackInfo handlePositionDiscontinuity(
1985       MediaPeriodId mediaPeriodId, long positionUs, long contentPositionUs) {
1986     deliverPendingMessageAtStartPositionRequired =
1987         deliverPendingMessageAtStartPositionRequired
1988             || positionUs != playbackInfo.positionUs
1989             || !mediaPeriodId.equals(playbackInfo.periodId);
1990     resetPendingPauseAtEndOfPeriod();
1991     TrackGroupArray trackGroupArray = playbackInfo.trackGroups;
1992     TrackSelectorResult trackSelectorResult = playbackInfo.trackSelectorResult;
1993     if (mediaSourceList.isPrepared()) {
1994       @Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
1995       trackGroupArray =
1996           playingPeriodHolder == null
1997               ? TrackGroupArray.EMPTY
1998               : playingPeriodHolder.getTrackGroups();
1999       trackSelectorResult =
2000           playingPeriodHolder == null
2001               ? emptyTrackSelectorResult
2002               : playingPeriodHolder.getTrackSelectorResult();
2003     } else if (!mediaPeriodId.equals(playbackInfo.periodId)) {
2004       // Reset previously kept track info if unprepared and the period changes.
2005       trackGroupArray = TrackGroupArray.EMPTY;
2006       trackSelectorResult = emptyTrackSelectorResult;
2007     }
2008     return playbackInfo.copyWithNewPosition(
2009         mediaPeriodId,
2010         positionUs,
2011         contentPositionUs,
2012         getTotalBufferedDurationUs(),
2013         trackGroupArray,
2014         trackSelectorResult);
2015   }
2016 
2017   private void enableRenderers() throws ExoPlaybackException {
2018     enableRenderers(/* rendererWasEnabledFlags= */ new boolean[renderers.length]);
2019   }
2020 
2021   private void enableRenderers(boolean[] rendererWasEnabledFlags) throws ExoPlaybackException {
2022     MediaPeriodHolder readingMediaPeriod = queue.getReadingPeriod();
2023     TrackSelectorResult trackSelectorResult = readingMediaPeriod.getTrackSelectorResult();
2024     // Reset all disabled renderers before enabling any new ones. This makes sure resources released
2025     // by the disabled renderers will be available to renderers that are being enabled.
2026     for (int i = 0; i < renderers.length; i++) {
2027       if (!trackSelectorResult.isRendererEnabled(i)) {
2028         renderers[i].reset();
2029       }
2030     }
2031     // Enable the renderers.
2032     for (int i = 0; i < renderers.length; i++) {
2033       if (trackSelectorResult.isRendererEnabled(i)) {
2034         enableRenderer(i, rendererWasEnabledFlags[i]);
2035       }
2036     }
2037     readingMediaPeriod.allRenderersEnabled = true;
2038   }
2039 
2040   private void enableRenderer(int rendererIndex, boolean wasRendererEnabled)
2041       throws ExoPlaybackException {
2042     Renderer renderer = renderers[rendererIndex];
2043     if (isRendererEnabled(renderer)) {
2044       return;
2045     }
2046     MediaPeriodHolder periodHolder = queue.getReadingPeriod();
2047     boolean mayRenderStartOfStream = periodHolder == queue.getPlayingPeriod();
2048     TrackSelectorResult trackSelectorResult = periodHolder.getTrackSelectorResult();
2049     RendererConfiguration rendererConfiguration =
2050         trackSelectorResult.rendererConfigurations[rendererIndex];
2051     TrackSelection newSelection = trackSelectorResult.selections.get(rendererIndex);
2052     Format[] formats = getFormats(newSelection);
2053     // The renderer needs enabling with its new track selection.
2054     boolean playing = shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY;
2055     // Consider as joining only if the renderer was previously disabled.
2056     boolean joining = !wasRendererEnabled && playing;
2057     // Enable the renderer.
2058     enabledRendererCount++;
2059     renderer.enable(
2060         rendererConfiguration,
2061         formats,
2062         periodHolder.sampleStreams[rendererIndex],
2063         rendererPositionUs,
2064         joining,
2065         mayRenderStartOfStream,
2066         periodHolder.getRendererOffset());
2067     mediaClock.onRendererEnabled(renderer);
2068     // Start the renderer if playing.
2069     if (playing) {
2070       renderer.start();
2071     }
2072   }
2073 
2074   private void handleLoadingMediaPeriodChanged(boolean loadingTrackSelectionChanged) {
2075     MediaPeriodHolder loadingMediaPeriodHolder = queue.getLoadingPeriod();
2076     MediaPeriodId loadingMediaPeriodId =
2077         loadingMediaPeriodHolder == null ? playbackInfo.periodId : loadingMediaPeriodHolder.info.id;
2078     boolean loadingMediaPeriodChanged =
2079         !playbackInfo.loadingMediaPeriodId.equals(loadingMediaPeriodId);
2080     if (loadingMediaPeriodChanged) {
2081       playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(loadingMediaPeriodId);
2082     }
2083     playbackInfo.bufferedPositionUs =
2084         loadingMediaPeriodHolder == null
2085             ? playbackInfo.positionUs
2086             : loadingMediaPeriodHolder.getBufferedPositionUs();
2087     playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs();
2088     if ((loadingMediaPeriodChanged || loadingTrackSelectionChanged)
2089         && loadingMediaPeriodHolder != null
2090         && loadingMediaPeriodHolder.prepared) {
2091       updateLoadControlTrackSelection(
2092           loadingMediaPeriodHolder.getTrackGroups(),
2093           loadingMediaPeriodHolder.getTrackSelectorResult());
2094     }
2095   }
2096 
2097   private long getTotalBufferedDurationUs() {
2098     return getTotalBufferedDurationUs(playbackInfo.bufferedPositionUs);
2099   }
2100 
2101   private long getTotalBufferedDurationUs(long bufferedPositionInLoadingPeriodUs) {
2102     MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
2103     if (loadingPeriodHolder == null) {
2104       return 0;
2105     }
2106     long totalBufferedDurationUs =
2107         bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);
2108     return Math.max(0, totalBufferedDurationUs);
2109   }
2110 
2111   private void updateLoadControlTrackSelection(
2112       TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) {
2113     loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections);
2114   }
2115 
2116   private void sendPlaybackSpeedChangedInternal(float playbackSpeed, boolean acknowledgeCommand) {
2117     handler
2118         .obtainMessage(
2119             MSG_PLAYBACK_SPEED_CHANGED_INTERNAL, acknowledgeCommand ? 1 : 0, 0, playbackSpeed)
2120         .sendToTarget();
2121   }
2122 
2123   private boolean shouldPlayWhenReady() {
2124     return playbackInfo.playWhenReady
2125         && playbackInfo.playbackSuppressionReason == Player.PLAYBACK_SUPPRESSION_REASON_NONE;
2126   }
2127 
2128   private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange(
2129       Timeline timeline,
2130       PlaybackInfo playbackInfo,
2131       @Nullable SeekPosition pendingInitialSeekPosition,
2132       MediaPeriodQueue queue,
2133       @RepeatMode int repeatMode,
2134       boolean shuffleModeEnabled,
2135       Timeline.Window window,
2136       Timeline.Period period) {
2137     if (timeline.isEmpty()) {
2138       return new PositionUpdateForPlaylistChange(
2139           PlaybackInfo.getDummyPeriodForEmptyTimeline(),
2140           /* periodPositionUs= */ 0,
2141           /* requestedContentPositionUs= */ C.TIME_UNSET,
2142           /* forceBufferingState= */ false,
2143           /* endPlayback= */ true);
2144     }
2145     MediaPeriodId oldPeriodId = playbackInfo.periodId;
2146     Object newPeriodUid = oldPeriodId.periodUid;
2147     boolean shouldUseRequestedContentPosition =
2148         shouldUseRequestedContentPosition(playbackInfo, period, window);
2149     long oldContentPositionUs =
2150         shouldUseRequestedContentPosition
2151             ? playbackInfo.requestedContentPositionUs
2152             : playbackInfo.positionUs;
2153     long newContentPositionUs = oldContentPositionUs;
2154     int startAtDefaultPositionWindowIndex = C.INDEX_UNSET;
2155     boolean forceBufferingState = false;
2156     boolean endPlayback = false;
2157     if (pendingInitialSeekPosition != null) {
2158       // Resolve initial seek position.
2159       @Nullable
2160       Pair<Object, Long> periodPosition =
2161           resolveSeekPosition(
2162               timeline,
2163               pendingInitialSeekPosition,
2164               /* trySubsequentPeriods= */ true,
2165               repeatMode,
2166               shuffleModeEnabled,
2167               window,
2168               period);
2169       if (periodPosition == null) {
2170         // The initial seek in the empty old timeline is invalid in the new timeline.
2171         endPlayback = true;
2172         startAtDefaultPositionWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
2173       } else {
2174         // The pending seek has been resolved successfully in the new timeline.
2175         if (pendingInitialSeekPosition.windowPositionUs == C.TIME_UNSET) {
2176           startAtDefaultPositionWindowIndex =
2177               timeline.getPeriodByUid(periodPosition.first, period).windowIndex;
2178         } else {
2179           newPeriodUid = periodPosition.first;
2180           newContentPositionUs = periodPosition.second;
2181         }
2182         forceBufferingState = playbackInfo.playbackState == Player.STATE_ENDED;
2183       }
2184     } else if (playbackInfo.timeline.isEmpty()) {
2185       // Resolve to default position if the old timeline is empty and no seek is requested above.
2186       startAtDefaultPositionWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
2187     } else if (timeline.getIndexOfPeriod(newPeriodUid) == C.INDEX_UNSET) {
2188       // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose
2189       // window we can restart from.
2190       @Nullable
2191       Object subsequentPeriodUid =
2192           resolveSubsequentPeriod(
2193               window,
2194               period,
2195               repeatMode,
2196               shuffleModeEnabled,
2197               newPeriodUid,
2198               playbackInfo.timeline,
2199               timeline);
2200       if (subsequentPeriodUid == null) {
2201         // We failed to resolve a suitable restart position but the timeline is not empty.
2202         endPlayback = true;
2203         startAtDefaultPositionWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
2204       } else {
2205         // We resolved a subsequent period. Start at the default position in the corresponding
2206         // window.
2207         startAtDefaultPositionWindowIndex =
2208             timeline.getPeriodByUid(subsequentPeriodUid, period).windowIndex;
2209       }
2210     } else if (shouldUseRequestedContentPosition) {
2211       // We previously requested a content position, but haven't used it yet. Re-resolve the
2212       // requested window position to the period uid and position in case they changed.
2213       if (oldContentPositionUs == C.TIME_UNSET) {
2214         startAtDefaultPositionWindowIndex =
2215             timeline.getPeriodByUid(newPeriodUid, period).windowIndex;
2216       } else {
2217         playbackInfo.timeline.getPeriodByUid(oldPeriodId.periodUid, period);
2218         long windowPositionUs = oldContentPositionUs + period.getPositionInWindowUs();
2219         int windowIndex = timeline.getPeriodByUid(newPeriodUid, period).windowIndex;
2220         Pair<Object, Long> periodPosition =
2221             timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
2222         newPeriodUid = periodPosition.first;
2223         newContentPositionUs = periodPosition.second;
2224       }
2225     }
2226 
2227     // Set period uid for default positions and resolve position for ad resolution.
2228     long contentPositionForAdResolutionUs = newContentPositionUs;
2229     if (startAtDefaultPositionWindowIndex != C.INDEX_UNSET) {
2230       Pair<Object, Long> defaultPosition =
2231           timeline.getPeriodPosition(
2232               window,
2233               period,
2234               startAtDefaultPositionWindowIndex,
2235               /* windowPositionUs= */ C.TIME_UNSET);
2236       newPeriodUid = defaultPosition.first;
2237       contentPositionForAdResolutionUs = defaultPosition.second;
2238       newContentPositionUs = C.TIME_UNSET;
2239     }
2240 
2241     // Ensure ad insertion metadata is up to date.
2242     MediaPeriodId periodIdWithAds =
2243         queue.resolveMediaPeriodIdForAds(timeline, newPeriodUid, contentPositionForAdResolutionUs);
2244     boolean oldAndNewPeriodIdAreSame =
2245         oldPeriodId.periodUid.equals(newPeriodUid)
2246             && !oldPeriodId.isAd()
2247             && !periodIdWithAds.isAd();
2248     // Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and
2249     // only MediaPeriodId.nextAdGroupIndex may have changed. This postpones a potential
2250     // discontinuity until we reach the former next ad group position.
2251     MediaPeriodId newPeriodId = oldAndNewPeriodIdAreSame ? oldPeriodId : periodIdWithAds;
2252 
2253     long periodPositionUs = contentPositionForAdResolutionUs;
2254     if (newPeriodId.isAd()) {
2255       if (newPeriodId.equals(oldPeriodId)) {
2256         periodPositionUs = playbackInfo.positionUs;
2257       } else {
2258         timeline.getPeriodByUid(newPeriodId.periodUid, period);
2259         periodPositionUs =
2260             newPeriodId.adIndexInAdGroup == period.getFirstAdIndexToPlay(newPeriodId.adGroupIndex)
2261                 ? period.getAdResumePositionUs()
2262                 : 0;
2263       }
2264     }
2265 
2266     return new PositionUpdateForPlaylistChange(
2267         newPeriodId, periodPositionUs, newContentPositionUs, forceBufferingState, endPlayback);
2268   }
2269 
2270   private static boolean shouldUseRequestedContentPosition(
2271       PlaybackInfo playbackInfo, Timeline.Period period, Timeline.Window window) {
2272     // Only use the actual position as content position if it's not an ad and we already have
2273     // prepared media information. Otherwise use the requested position.
2274     MediaPeriodId periodId = playbackInfo.periodId;
2275     Timeline timeline = playbackInfo.timeline;
2276     return periodId.isAd()
2277         || timeline.isEmpty()
2278         || timeline.getWindow(
2279                 timeline.getPeriodByUid(periodId.periodUid, period).windowIndex, window)
2280             .isPlaceholder;
2281   }
2282 
2283   /**
2284    * Updates pending message to a new timeline.
2285    *
2286    * @param pendingMessageInfo The pending message.
2287    * @param newTimeline The new timeline.
2288    * @param previousTimeline The previous timeline used to set the message positions.
2289    * @param repeatMode The current repeat mode.
2290    * @param shuffleModeEnabled The current shuffle mode.
2291    * @param window A scratch window.
2292    * @param period A scratch period.
2293    * @return Whether the message position could be resolved to the current timeline.
2294    */
2295   private static boolean resolvePendingMessagePosition(
2296       PendingMessageInfo pendingMessageInfo,
2297       Timeline newTimeline,
2298       Timeline previousTimeline,
2299       @Player.RepeatMode int repeatMode,
2300       boolean shuffleModeEnabled,
2301       Timeline.Window window,
2302       Timeline.Period period) {
2303     if (pendingMessageInfo.resolvedPeriodUid == null) {
2304       // Position is still unresolved. Try to find window in new timeline.
2305       long requestPositionUs =
2306           pendingMessageInfo.message.getPositionMs() == C.TIME_END_OF_SOURCE
2307               ? C.TIME_UNSET
2308               : C.msToUs(pendingMessageInfo.message.getPositionMs());
2309       @Nullable
2310       Pair<Object, Long> periodPosition =
2311           resolveSeekPosition(
2312               newTimeline,
2313               new SeekPosition(
2314                   pendingMessageInfo.message.getTimeline(),
2315                   pendingMessageInfo.message.getWindowIndex(),
2316                   requestPositionUs),
2317               /* trySubsequentPeriods= */ false,
2318               repeatMode,
2319               shuffleModeEnabled,
2320               window,
2321               period);
2322       if (periodPosition == null) {
2323         return false;
2324       }
2325       pendingMessageInfo.setResolvedPosition(
2326           /* periodIndex= */ newTimeline.getIndexOfPeriod(periodPosition.first),
2327           /* periodTimeUs= */ periodPosition.second,
2328           /* periodUid= */ periodPosition.first);
2329       if (pendingMessageInfo.message.getPositionMs() == C.TIME_END_OF_SOURCE) {
2330         resolvePendingMessageEndOfStreamPosition(newTimeline, pendingMessageInfo, window, period);
2331       }
2332       return true;
2333     }
2334     // Position has been resolved for a previous timeline. Try to find the updated period index.
2335     int index = newTimeline.getIndexOfPeriod(pendingMessageInfo.resolvedPeriodUid);
2336     if (index == C.INDEX_UNSET) {
2337       return false;
2338     }
2339     if (pendingMessageInfo.message.getPositionMs() == C.TIME_END_OF_SOURCE) {
2340       // Re-resolve end of stream in case the duration changed.
2341       resolvePendingMessageEndOfStreamPosition(newTimeline, pendingMessageInfo, window, period);
2342       return true;
2343     }
2344     pendingMessageInfo.resolvedPeriodIndex = index;
2345     previousTimeline.getPeriodByUid(pendingMessageInfo.resolvedPeriodUid, period);
2346     if (previousTimeline.getWindow(period.windowIndex, window).isPlaceholder) {
2347       // The position needs to be re-resolved because the window in the previous timeline wasn't
2348       // fully prepared.
2349       long windowPositionUs =
2350           pendingMessageInfo.resolvedPeriodTimeUs + period.getPositionInWindowUs();
2351       int windowIndex =
2352           newTimeline.getPeriodByUid(pendingMessageInfo.resolvedPeriodUid, period).windowIndex;
2353       Pair<Object, Long> periodPosition =
2354           newTimeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
2355       pendingMessageInfo.setResolvedPosition(
2356           /* periodIndex= */ newTimeline.getIndexOfPeriod(periodPosition.first),
2357           /* periodTimeUs= */ periodPosition.second,
2358           /* periodUid= */ periodPosition.first);
2359     }
2360     return true;
2361   }
2362 
2363   private static void resolvePendingMessageEndOfStreamPosition(
2364       Timeline timeline,
2365       PendingMessageInfo messageInfo,
2366       Timeline.Window window,
2367       Timeline.Period period) {
2368     int windowIndex = timeline.getPeriodByUid(messageInfo.resolvedPeriodUid, period).windowIndex;
2369     int lastPeriodIndex = timeline.getWindow(windowIndex, window).lastPeriodIndex;
2370     Object lastPeriodUid = timeline.getPeriod(lastPeriodIndex, period, /* setIds= */ true).uid;
2371     long positionUs = period.durationUs != C.TIME_UNSET ? period.durationUs - 1 : Long.MAX_VALUE;
2372     messageInfo.setResolvedPosition(lastPeriodIndex, positionUs, lastPeriodUid);
2373   }
2374 
2375   /**
2376    * Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the
2377    * internal timeline.
2378    *
2379    * @param seekPosition The position to resolve.
2380    * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching
2381    *     period if the original period is no longer available.
2382    * @return The resolved position, or null if resolution was not successful.
2383    * @throws IllegalSeekPositionException If the window index of the seek position is outside the
2384    *     bounds of the timeline.
2385    */
2386   @Nullable
2387   private static Pair<Object, Long> resolveSeekPosition(
2388       Timeline timeline,
2389       SeekPosition seekPosition,
2390       boolean trySubsequentPeriods,
2391       @RepeatMode int repeatMode,
2392       boolean shuffleModeEnabled,
2393       Timeline.Window window,
2394       Timeline.Period period) {
2395     Timeline seekTimeline = seekPosition.timeline;
2396     if (timeline.isEmpty()) {
2397       // We don't have a valid timeline yet, so we can't resolve the position.
2398       return null;
2399     }
2400     if (seekTimeline.isEmpty()) {
2401       // The application performed a blind seek with an empty timeline (most likely based on
2402       // knowledge of what the future timeline will be). Use the internal timeline.
2403       seekTimeline = timeline;
2404     }
2405     // Map the SeekPosition to a position in the corresponding timeline.
2406     Pair<Object, Long> periodPosition;
2407     try {
2408       periodPosition =
2409           seekTimeline.getPeriodPosition(
2410               window, period, seekPosition.windowIndex, seekPosition.windowPositionUs);
2411     } catch (IndexOutOfBoundsException e) {
2412       // The window index of the seek position was outside the bounds of the timeline.
2413       return null;
2414     }
2415     if (timeline.equals(seekTimeline)) {
2416       // Our internal timeline is the seek timeline, so the mapped position is correct.
2417       return periodPosition;
2418     }
2419     // Attempt to find the mapped period in the internal timeline.
2420     int periodIndex = timeline.getIndexOfPeriod(periodPosition.first);
2421     if (periodIndex != C.INDEX_UNSET) {
2422       // We successfully located the period in the internal timeline.
2423       seekTimeline.getPeriodByUid(periodPosition.first, period);
2424       if (seekTimeline.getWindow(period.windowIndex, window).isPlaceholder) {
2425         // The seek timeline was using a placeholder, so we need to re-resolve using the updated
2426         // timeline in case the resolved position changed.
2427         int newWindowIndex = timeline.getPeriodByUid(periodPosition.first, period).windowIndex;
2428         periodPosition =
2429             timeline.getPeriodPosition(
2430                 window, period, newWindowIndex, seekPosition.windowPositionUs);
2431       }
2432       return periodPosition;
2433     }
2434     if (trySubsequentPeriods) {
2435       // Try and find a subsequent period from the seek timeline in the internal timeline.
2436       @Nullable
2437       Object periodUid =
2438           resolveSubsequentPeriod(
2439               window,
2440               period,
2441               repeatMode,
2442               shuffleModeEnabled,
2443               periodPosition.first,
2444               seekTimeline,
2445               timeline);
2446       if (periodUid != null) {
2447         // We found one. Use the default position of the corresponding window.
2448         return timeline.getPeriodPosition(
2449             window,
2450             period,
2451             timeline.getPeriodByUid(periodUid, period).windowIndex,
2452             /* windowPositionUs= */ C.TIME_UNSET);
2453       }
2454     }
2455     // We didn't find one. Give up.
2456     return null;
2457   }
2458 
2459   /**
2460    * Given a period index into an old timeline, finds the first subsequent period that also exists
2461    * in a new timeline. The uid of this period in the new timeline is returned.
2462    *
2463    * @param window A {@link Timeline.Window} to be used internally.
2464    * @param period A {@link Timeline.Period} to be used internally.
2465    * @param repeatMode The repeat mode to use.
2466    * @param shuffleModeEnabled Whether the shuffle mode is enabled.
2467    * @param oldPeriodUid The index of the period in the old timeline.
2468    * @param oldTimeline The old timeline.
2469    * @param newTimeline The new timeline.
2470    * @return The uid in the new timeline of the first subsequent period, or null if no such period
2471    *     was found.
2472    */
2473   /* package */ static @Nullable Object resolveSubsequentPeriod(
2474       Timeline.Window window,
2475       Timeline.Period period,
2476       @Player.RepeatMode int repeatMode,
2477       boolean shuffleModeEnabled,
2478       Object oldPeriodUid,
2479       Timeline oldTimeline,
2480       Timeline newTimeline) {
2481     int oldPeriodIndex = oldTimeline.getIndexOfPeriod(oldPeriodUid);
2482     int newPeriodIndex = C.INDEX_UNSET;
2483     int maxIterations = oldTimeline.getPeriodCount();
2484     for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) {
2485       oldPeriodIndex =
2486           oldTimeline.getNextPeriodIndex(
2487               oldPeriodIndex, period, window, repeatMode, shuffleModeEnabled);
2488       if (oldPeriodIndex == C.INDEX_UNSET) {
2489         // We've reached the end of the old timeline.
2490         break;
2491       }
2492       newPeriodIndex = newTimeline.getIndexOfPeriod(oldTimeline.getUidOfPeriod(oldPeriodIndex));
2493     }
2494     return newPeriodIndex == C.INDEX_UNSET ? null : newTimeline.getUidOfPeriod(newPeriodIndex);
2495   }
2496 
2497   private static Format[] getFormats(TrackSelection newSelection) {
2498     // Build an array of formats contained by the selection.
2499     int length = newSelection != null ? newSelection.length() : 0;
2500     Format[] formats = new Format[length];
2501     for (int i = 0; i < length; i++) {
2502       formats[i] = newSelection.getFormat(i);
2503     }
2504     return formats;
2505   }
2506 
2507   private static boolean isRendererEnabled(Renderer renderer) {
2508     return renderer.getState() != Renderer.STATE_DISABLED;
2509   }
2510 
2511   private static final class SeekPosition {
2512 
2513     public final Timeline timeline;
2514     public final int windowIndex;
2515     public final long windowPositionUs;
2516 
2517     public SeekPosition(Timeline timeline, int windowIndex, long windowPositionUs) {
2518       this.timeline = timeline;
2519       this.windowIndex = windowIndex;
2520       this.windowPositionUs = windowPositionUs;
2521     }
2522   }
2523 
2524   private static final class PositionUpdateForPlaylistChange {
2525     public final MediaPeriodId periodId;
2526     public final long periodPositionUs;
2527     public final long requestedContentPositionUs;
2528     public final boolean forceBufferingState;
2529     public final boolean endPlayback;
2530 
2531     public PositionUpdateForPlaylistChange(
2532         MediaPeriodId periodId,
2533         long periodPositionUs,
2534         long requestedContentPositionUs,
2535         boolean forceBufferingState,
2536         boolean endPlayback) {
2537       this.periodId = periodId;
2538       this.periodPositionUs = periodPositionUs;
2539       this.requestedContentPositionUs = requestedContentPositionUs;
2540       this.forceBufferingState = forceBufferingState;
2541       this.endPlayback = endPlayback;
2542     }
2543   }
2544 
2545   private static final class PendingMessageInfo implements Comparable<PendingMessageInfo> {
2546 
2547     public final PlayerMessage message;
2548 
2549     public int resolvedPeriodIndex;
2550     public long resolvedPeriodTimeUs;
2551     @Nullable public Object resolvedPeriodUid;
2552 
2553     public PendingMessageInfo(PlayerMessage message) {
2554       this.message = message;
2555     }
2556 
2557     public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) {
2558       resolvedPeriodIndex = periodIndex;
2559       resolvedPeriodTimeUs = periodTimeUs;
2560       resolvedPeriodUid = periodUid;
2561     }
2562 
2563     @Override
2564     public int compareTo(PendingMessageInfo other) {
2565       if ((resolvedPeriodUid == null) != (other.resolvedPeriodUid == null)) {
2566         // PendingMessageInfos with a resolved period position are always smaller.
2567         return resolvedPeriodUid != null ? -1 : 1;
2568       }
2569       if (resolvedPeriodUid == null) {
2570         // Don't sort message with unresolved positions.
2571         return 0;
2572       }
2573       // Sort resolved media times by period index and then by period position.
2574       int comparePeriodIndex = resolvedPeriodIndex - other.resolvedPeriodIndex;
2575       if (comparePeriodIndex != 0) {
2576         return comparePeriodIndex;
2577       }
2578       return Util.compareLong(resolvedPeriodTimeUs, other.resolvedPeriodTimeUs);
2579     }
2580   }
2581 
2582   private static final class MediaSourceListUpdateMessage {
2583 
2584     private final List<MediaSourceList.MediaSourceHolder> mediaSourceHolders;
2585     private final ShuffleOrder shuffleOrder;
2586     private final int windowIndex;
2587     private final long positionUs;
2588 
2589     private MediaSourceListUpdateMessage(
2590         List<MediaSourceList.MediaSourceHolder> mediaSourceHolders,
2591         ShuffleOrder shuffleOrder,
2592         int windowIndex,
2593         long positionUs) {
2594       this.mediaSourceHolders = mediaSourceHolders;
2595       this.shuffleOrder = shuffleOrder;
2596       this.windowIndex = windowIndex;
2597       this.positionUs = positionUs;
2598     }
2599   }
2600 
2601   private static class MoveMediaItemsMessage {
2602 
2603     public final int fromIndex;
2604     public final int toIndex;
2605     public final int newFromIndex;
2606     public final ShuffleOrder shuffleOrder;
2607 
2608     public MoveMediaItemsMessage(
2609         int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) {
2610       this.fromIndex = fromIndex;
2611       this.toIndex = toIndex;
2612       this.newFromIndex = newFromIndex;
2613       this.shuffleOrder = shuffleOrder;
2614     }
2615   }
2616 
2617   /* package */ static final class PlaybackInfoUpdate {
2618 
2619     private boolean hasPendingChange;
2620 
2621     public PlaybackInfo playbackInfo;
2622     public int operationAcks;
2623     public boolean positionDiscontinuity;
2624     @DiscontinuityReason public int discontinuityReason;
2625     public boolean hasPlayWhenReadyChangeReason;
2626     @PlayWhenReadyChangeReason public int playWhenReadyChangeReason;
2627 
2628     public PlaybackInfoUpdate(PlaybackInfo playbackInfo) {
2629       this.playbackInfo = playbackInfo;
2630     }
2631 
2632     public void incrementPendingOperationAcks(int operationAcks) {
2633       hasPendingChange |= operationAcks > 0;
2634       this.operationAcks += operationAcks;
2635     }
2636 
2637     public void setPlaybackInfo(PlaybackInfo playbackInfo) {
2638       hasPendingChange |= this.playbackInfo != playbackInfo;
2639       this.playbackInfo = playbackInfo;
2640     }
2641 
2642     public void setPositionDiscontinuity(@DiscontinuityReason int discontinuityReason) {
2643       if (positionDiscontinuity
2644           && this.discontinuityReason != Player.DISCONTINUITY_REASON_INTERNAL) {
2645         // We always prefer non-internal discontinuity reasons. We also assume that we won't report
2646         // more than one non-internal discontinuity per message iteration.
2647         Assertions.checkArgument(discontinuityReason == Player.DISCONTINUITY_REASON_INTERNAL);
2648         return;
2649       }
2650       hasPendingChange = true;
2651       positionDiscontinuity = true;
2652       this.discontinuityReason = discontinuityReason;
2653     }
2654 
2655     public void setPlayWhenReadyChangeReason(
2656         @PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
2657       hasPendingChange = true;
2658       this.hasPlayWhenReadyChangeReason = true;
2659       this.playWhenReadyChangeReason = playWhenReadyChangeReason;
2660     }
2661   }
2662 }
2663