summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java2045
1 files changed, 2045 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java
new file mode 100644
index 0000000000..a4462ad1c4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -0,0 +1,2045 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Pair;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.DiscontinuityReason;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelector;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectorResult;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.HandlerWrapper;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TraceUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Implements the internal behavior of {@link ExoPlayerImpl}. */
+/* package */ final class ExoPlayerImplInternal
+ implements Handler.Callback,
+ MediaPeriod.Callback,
+ TrackSelector.InvalidationListener,
+ MediaSourceCaller,
+ PlaybackParameterListener,
+ PlayerMessage.Sender {
+
+ private static final String TAG = "ExoPlayerImplInternal";
+
+ // External messages
+ public static final int MSG_PLAYBACK_INFO_CHANGED = 0;
+ public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 1;
+
+ // Internal messages
+ private static final int MSG_PREPARE = 0;
+ private static final int MSG_SET_PLAY_WHEN_READY = 1;
+ private static final int MSG_DO_SOME_WORK = 2;
+ private static final int MSG_SEEK_TO = 3;
+ private static final int MSG_SET_PLAYBACK_PARAMETERS = 4;
+ private static final int MSG_SET_SEEK_PARAMETERS = 5;
+ private static final int MSG_STOP = 6;
+ private static final int MSG_RELEASE = 7;
+ private static final int MSG_REFRESH_SOURCE_INFO = 8;
+ private static final int MSG_PERIOD_PREPARED = 9;
+ private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 10;
+ private static final int MSG_TRACK_SELECTION_INVALIDATED = 11;
+ private static final int MSG_SET_REPEAT_MODE = 12;
+ private static final int MSG_SET_SHUFFLE_ENABLED = 13;
+ private static final int MSG_SET_FOREGROUND_MODE = 14;
+ private static final int MSG_SEND_MESSAGE = 15;
+ private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16;
+ private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17;
+
+ private static final int ACTIVE_INTERVAL_MS = 10;
+ private static final int IDLE_INTERVAL_MS = 1000;
+
+ private final Renderer[] renderers;
+ private final RendererCapabilities[] rendererCapabilities;
+ private final TrackSelector trackSelector;
+ private final TrackSelectorResult emptyTrackSelectorResult;
+ private final LoadControl loadControl;
+ private final BandwidthMeter bandwidthMeter;
+ private final HandlerWrapper handler;
+ private final HandlerThread internalPlaybackThread;
+ private final Handler eventHandler;
+ private final Timeline.Window window;
+ private final Timeline.Period period;
+ private final long backBufferDurationUs;
+ private final boolean retainBackBufferFromKeyframe;
+ private final DefaultMediaClock mediaClock;
+ private final PlaybackInfoUpdate playbackInfoUpdate;
+ private final ArrayList<PendingMessageInfo> pendingMessages;
+ private final Clock clock;
+ private final MediaPeriodQueue queue;
+
+ @SuppressWarnings("unused")
+ private SeekParameters seekParameters;
+
+ private PlaybackInfo playbackInfo;
+ private MediaSource mediaSource;
+ private Renderer[] enabledRenderers;
+ private boolean released;
+ private boolean playWhenReady;
+ private boolean rebuffering;
+ private boolean shouldContinueLoading;
+ @Player.RepeatMode private int repeatMode;
+ private boolean shuffleModeEnabled;
+ private boolean foregroundMode;
+
+ private int pendingPrepareCount;
+ private SeekPosition pendingInitialSeekPosition;
+ private long rendererPositionUs;
+ private int nextPendingMessageIndex;
+ private boolean deliverPendingMessageAtStartPositionRequired;
+
+ public ExoPlayerImplInternal(
+ Renderer[] renderers,
+ TrackSelector trackSelector,
+ TrackSelectorResult emptyTrackSelectorResult,
+ LoadControl loadControl,
+ BandwidthMeter bandwidthMeter,
+ boolean playWhenReady,
+ @Player.RepeatMode int repeatMode,
+ boolean shuffleModeEnabled,
+ Handler eventHandler,
+ Clock clock) {
+ this.renderers = renderers;
+ this.trackSelector = trackSelector;
+ this.emptyTrackSelectorResult = emptyTrackSelectorResult;
+ this.loadControl = loadControl;
+ this.bandwidthMeter = bandwidthMeter;
+ this.playWhenReady = playWhenReady;
+ this.repeatMode = repeatMode;
+ this.shuffleModeEnabled = shuffleModeEnabled;
+ this.eventHandler = eventHandler;
+ this.clock = clock;
+ this.queue = new MediaPeriodQueue();
+
+ backBufferDurationUs = loadControl.getBackBufferDurationUs();
+ retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe();
+
+ seekParameters = SeekParameters.DEFAULT;
+ playbackInfo =
+ PlaybackInfo.createDummy(/* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult);
+ playbackInfoUpdate = new PlaybackInfoUpdate();
+ rendererCapabilities = new RendererCapabilities[renderers.length];
+ for (int i = 0; i < renderers.length; i++) {
+ renderers[i].setIndex(i);
+ rendererCapabilities[i] = renderers[i].getCapabilities();
+ }
+ mediaClock = new DefaultMediaClock(this, clock);
+ pendingMessages = new ArrayList<>();
+ enabledRenderers = new Renderer[0];
+ window = new Timeline.Window();
+ period = new Timeline.Period();
+ trackSelector.init(/* listener= */ this, bandwidthMeter);
+
+ // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
+ // not normally change to this priority" is incorrect.
+ internalPlaybackThread =
+ new HandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO);
+ internalPlaybackThread.start();
+ handler = clock.createHandler(internalPlaybackThread.getLooper(), this);
+ deliverPendingMessageAtStartPositionRequired = true;
+ }
+
+ public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+ handler
+ .obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource)
+ .sendToTarget();
+ }
+
+ public void setPlayWhenReady(boolean playWhenReady) {
+ handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget();
+ }
+
+ public void setRepeatMode(@Player.RepeatMode int repeatMode) {
+ handler.obtainMessage(MSG_SET_REPEAT_MODE, repeatMode, 0).sendToTarget();
+ }
+
+ public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
+ handler.obtainMessage(MSG_SET_SHUFFLE_ENABLED, shuffleModeEnabled ? 1 : 0, 0).sendToTarget();
+ }
+
+ public void seekTo(Timeline timeline, int windowIndex, long positionUs) {
+ handler
+ .obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs))
+ .sendToTarget();
+ }
+
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
+ handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget();
+ }
+
+ public void setSeekParameters(SeekParameters seekParameters) {
+ handler.obtainMessage(MSG_SET_SEEK_PARAMETERS, seekParameters).sendToTarget();
+ }
+
+ public void stop(boolean reset) {
+ handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget();
+ }
+
+ @Override
+ public synchronized void sendMessage(PlayerMessage message) {
+ if (released || !internalPlaybackThread.isAlive()) {
+ Log.w(TAG, "Ignoring messages sent after release.");
+ message.markAsProcessed(/* isDelivered= */ false);
+ return;
+ }
+ handler.obtainMessage(MSG_SEND_MESSAGE, message).sendToTarget();
+ }
+
+ public synchronized void setForegroundMode(boolean foregroundMode) {
+ if (released || !internalPlaybackThread.isAlive()) {
+ return;
+ }
+ if (foregroundMode) {
+ handler.obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 1, 0).sendToTarget();
+ } else {
+ AtomicBoolean processedFlag = new AtomicBoolean();
+ handler
+ .obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 0, 0, processedFlag)
+ .sendToTarget();
+ boolean wasInterrupted = false;
+ while (!processedFlag.get()) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ wasInterrupted = true;
+ }
+ }
+ if (wasInterrupted) {
+ // Restore the interrupted status.
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ public synchronized void release() {
+ if (released || !internalPlaybackThread.isAlive()) {
+ return;
+ }
+ handler.sendEmptyMessage(MSG_RELEASE);
+ boolean wasInterrupted = false;
+ while (!released) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ wasInterrupted = true;
+ }
+ }
+ if (wasInterrupted) {
+ // Restore the interrupted status.
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ public Looper getPlaybackLooper() {
+ return internalPlaybackThread.getLooper();
+ }
+
+ // MediaSource.MediaSourceCaller implementation.
+
+ @Override
+ public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) {
+ handler
+ .obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline))
+ .sendToTarget();
+ }
+
+ // MediaPeriod.Callback implementation.
+
+ @Override
+ public void onPrepared(MediaPeriod source) {
+ handler.obtainMessage(MSG_PERIOD_PREPARED, source).sendToTarget();
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {
+ handler.obtainMessage(MSG_SOURCE_CONTINUE_LOADING_REQUESTED, source).sendToTarget();
+ }
+
+ // TrackSelector.InvalidationListener implementation.
+
+ @Override
+ public void onTrackSelectionsInvalidated() {
+ handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED);
+ }
+
+ // DefaultMediaClock.PlaybackParameterListener implementation.
+
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ sendPlaybackParametersChangedInternal(playbackParameters, /* acknowledgeCommand= */ false);
+ }
+
+ // Handler.Callback implementation.
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ try {
+ switch (msg.what) {
+ case MSG_PREPARE:
+ prepareInternal(
+ (MediaSource) msg.obj,
+ /* resetPosition= */ msg.arg1 != 0,
+ /* resetState= */ msg.arg2 != 0);
+ break;
+ case MSG_SET_PLAY_WHEN_READY:
+ setPlayWhenReadyInternal(msg.arg1 != 0);
+ break;
+ case MSG_SET_REPEAT_MODE:
+ setRepeatModeInternal(msg.arg1);
+ break;
+ case MSG_SET_SHUFFLE_ENABLED:
+ setShuffleModeEnabledInternal(msg.arg1 != 0);
+ break;
+ case MSG_DO_SOME_WORK:
+ doSomeWork();
+ break;
+ case MSG_SEEK_TO:
+ seekToInternal((SeekPosition) msg.obj);
+ break;
+ case MSG_SET_PLAYBACK_PARAMETERS:
+ setPlaybackParametersInternal((PlaybackParameters) msg.obj);
+ break;
+ case MSG_SET_SEEK_PARAMETERS:
+ setSeekParametersInternal((SeekParameters) msg.obj);
+ break;
+ case MSG_SET_FOREGROUND_MODE:
+ setForegroundModeInternal(
+ /* foregroundMode= */ msg.arg1 != 0, /* processedFlag= */ (AtomicBoolean) msg.obj);
+ break;
+ case MSG_STOP:
+ stopInternal(
+ /* forceResetRenderers= */ false,
+ /* resetPositionAndState= */ msg.arg1 != 0,
+ /* acknowledgeStop= */ true);
+ break;
+ case MSG_PERIOD_PREPARED:
+ handlePeriodPrepared((MediaPeriod) msg.obj);
+ break;
+ case MSG_REFRESH_SOURCE_INFO:
+ handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj);
+ break;
+ case MSG_SOURCE_CONTINUE_LOADING_REQUESTED:
+ handleContinueLoadingRequested((MediaPeriod) msg.obj);
+ break;
+ case MSG_TRACK_SELECTION_INVALIDATED:
+ reselectTracksInternal();
+ break;
+ case MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL:
+ handlePlaybackParameters(
+ (PlaybackParameters) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0);
+ break;
+ case MSG_SEND_MESSAGE:
+ sendMessageInternal((PlayerMessage) msg.obj);
+ break;
+ case MSG_SEND_MESSAGE_TO_TARGET_THREAD:
+ sendMessageToTargetThread((PlayerMessage) msg.obj);
+ break;
+ case MSG_RELEASE:
+ releaseInternal();
+ // Return immediately to not send playback info updates after release.
+ return true;
+ default:
+ return false;
+ }
+ maybeNotifyPlaybackInfoChanged();
+ } catch (ExoPlaybackException e) {
+ Log.e(TAG, getExoPlaybackExceptionMessage(e), e);
+ stopInternal(
+ /* forceResetRenderers= */ true,
+ /* resetPositionAndState= */ false,
+ /* acknowledgeStop= */ false);
+ playbackInfo = playbackInfo.copyWithPlaybackError(e);
+ maybeNotifyPlaybackInfoChanged();
+ } catch (IOException e) {
+ Log.e(TAG, "Source error", e);
+ stopInternal(
+ /* forceResetRenderers= */ false,
+ /* resetPositionAndState= */ false,
+ /* acknowledgeStop= */ false);
+ playbackInfo = playbackInfo.copyWithPlaybackError(ExoPlaybackException.createForSource(e));
+ maybeNotifyPlaybackInfoChanged();
+ } catch (RuntimeException | OutOfMemoryError e) {
+ Log.e(TAG, "Internal runtime error", e);
+ ExoPlaybackException error =
+ e instanceof OutOfMemoryError
+ ? ExoPlaybackException.createForOutOfMemoryError((OutOfMemoryError) e)
+ : ExoPlaybackException.createForUnexpected((RuntimeException) e);
+ stopInternal(
+ /* forceResetRenderers= */ true,
+ /* resetPositionAndState= */ false,
+ /* acknowledgeStop= */ false);
+ playbackInfo = playbackInfo.copyWithPlaybackError(error);
+ maybeNotifyPlaybackInfoChanged();
+ }
+ return true;
+ }
+
+ // Private methods.
+
+ private String getExoPlaybackExceptionMessage(ExoPlaybackException e) {
+ if (e.type != ExoPlaybackException.TYPE_RENDERER) {
+ return "Playback error.";
+ }
+ return "Renderer error: index="
+ + e.rendererIndex
+ + ", type="
+ + Util.getTrackTypeString(renderers[e.rendererIndex].getTrackType())
+ + ", format="
+ + e.rendererFormat
+ + ", rendererSupport="
+ + RendererCapabilities.getFormatSupportString(e.rendererFormatSupport);
+ }
+
+ private void setState(int state) {
+ if (playbackInfo.playbackState != state) {
+ playbackInfo = playbackInfo.copyWithPlaybackState(state);
+ }
+ }
+
+ private void maybeNotifyPlaybackInfoChanged() {
+ if (playbackInfoUpdate.hasPendingUpdate(playbackInfo)) {
+ eventHandler
+ .obtainMessage(
+ MSG_PLAYBACK_INFO_CHANGED,
+ playbackInfoUpdate.operationAcks,
+ playbackInfoUpdate.positionDiscontinuity
+ ? playbackInfoUpdate.discontinuityReason
+ : C.INDEX_UNSET,
+ playbackInfo)
+ .sendToTarget();
+ playbackInfoUpdate.reset(playbackInfo);
+ }
+ }
+
+ private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+ pendingPrepareCount++;
+ resetInternal(
+ /* resetRenderers= */ false,
+ /* releaseMediaSource= */ true,
+ resetPosition,
+ resetState,
+ /* resetError= */ true);
+ loadControl.onPrepared();
+ this.mediaSource = mediaSource;
+ setState(Player.STATE_BUFFERING);
+ mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener());
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ }
+
+ private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
+ rebuffering = false;
+ this.playWhenReady = playWhenReady;
+ if (!playWhenReady) {
+ stopRenderers();
+ updatePlaybackPositions();
+ } else {
+ if (playbackInfo.playbackState == Player.STATE_READY) {
+ startRenderers();
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) {
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ }
+ }
+ }
+
+ private void setRepeatModeInternal(@Player.RepeatMode int repeatMode)
+ throws ExoPlaybackException {
+ this.repeatMode = repeatMode;
+ if (!queue.updateRepeatMode(repeatMode)) {
+ seekToCurrentPosition(/* sendDiscontinuity= */ true);
+ }
+ handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
+ }
+
+ private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled)
+ throws ExoPlaybackException {
+ this.shuffleModeEnabled = shuffleModeEnabled;
+ if (!queue.updateShuffleModeEnabled(shuffleModeEnabled)) {
+ seekToCurrentPosition(/* sendDiscontinuity= */ true);
+ }
+ handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
+ }
+
+ private void seekToCurrentPosition(boolean sendDiscontinuity) throws ExoPlaybackException {
+ // Renderers may have read from a period that's been removed. Seek back to the current
+ // position of the playing period to make sure none of the removed period is played.
+ MediaPeriodId periodId = queue.getPlayingPeriod().info.id;
+ long newPositionUs =
+ seekToPeriodPosition(periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true);
+ if (newPositionUs != playbackInfo.positionUs) {
+ playbackInfo = copyWithNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs);
+ if (sendDiscontinuity) {
+ playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
+ }
+ }
+ }
+
+ private void startRenderers() throws ExoPlaybackException {
+ rebuffering = false;
+ mediaClock.start();
+ for (Renderer renderer : enabledRenderers) {
+ renderer.start();
+ }
+ }
+
+ private void stopRenderers() throws ExoPlaybackException {
+ mediaClock.stop();
+ for (Renderer renderer : enabledRenderers) {
+ ensureStopped(renderer);
+ }
+ }
+
+ private void updatePlaybackPositions() throws ExoPlaybackException {
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ if (playingPeriodHolder == null) {
+ return;
+ }
+
+ // Update the playback position.
+ long discontinuityPositionUs =
+ playingPeriodHolder.prepared
+ ? playingPeriodHolder.mediaPeriod.readDiscontinuity()
+ : C.TIME_UNSET;
+ if (discontinuityPositionUs != C.TIME_UNSET) {
+ resetRendererPosition(discontinuityPositionUs);
+ // A MediaPeriod may report a discontinuity at the current playback position to ensure the
+ // renderers are flushed. Only report the discontinuity externally if the position changed.
+ if (discontinuityPositionUs != playbackInfo.positionUs) {
+ playbackInfo =
+ copyWithNewPosition(
+ playbackInfo.periodId, discontinuityPositionUs, playbackInfo.contentPositionUs);
+ playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
+ }
+ } else {
+ rendererPositionUs =
+ mediaClock.syncAndGetPositionUs(
+ /* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod());
+ long periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs);
+ maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs);
+ playbackInfo.positionUs = periodPositionUs;
+ }
+
+ // Update the buffered position and total buffered duration.
+ MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod();
+ playbackInfo.bufferedPositionUs = loadingPeriod.getBufferedPositionUs();
+ playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs();
+ }
+
+ private void doSomeWork() throws ExoPlaybackException, IOException {
+ long operationStartTimeMs = clock.uptimeMillis();
+ updatePeriods();
+
+ if (playbackInfo.playbackState == Player.STATE_IDLE
+ || playbackInfo.playbackState == Player.STATE_ENDED) {
+ // Remove all messages. Prepare (in case of IDLE) or seek (in case of ENDED) will resume.
+ handler.removeMessages(MSG_DO_SOME_WORK);
+ return;
+ }
+
+ @Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ if (playingPeriodHolder == null) {
+ // We're still waiting until the playing period is available.
+ scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
+ return;
+ }
+
+ TraceUtil.beginSection("doSomeWork");
+
+ updatePlaybackPositions();
+
+ boolean renderersEnded = true;
+ boolean renderersAllowPlayback = true;
+ if (playingPeriodHolder.prepared) {
+ long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
+ playingPeriodHolder.mediaPeriod.discardBuffer(
+ playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
+ for (int i = 0; i < renderers.length; i++) {
+ Renderer renderer = renderers[i];
+ if (renderer.getState() == Renderer.STATE_DISABLED) {
+ continue;
+ }
+ // TODO: Each renderer should return the maximum delay before which it wishes to be called
+ // again. The minimum of these values should then be used as the delay before the next
+ // invocation of this method.
+ renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
+ renderersEnded = renderersEnded && renderer.isEnded();
+ // Determine whether the renderer allows playback to continue. Playback can continue if the
+ // renderer is ready or ended. Also continue playback if the renderer is reading ahead into
+ // the next stream or is waiting for the next stream. This is to avoid getting stuck if
+ // tracks in the current period have uneven durations and are still being read by another
+ // renderer. See: https://github.com/google/ExoPlayer/issues/1874.
+ boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream();
+ boolean isWaitingForNextStream =
+ !isReadingAhead
+ && playingPeriodHolder.getNext() != null
+ && renderer.hasReadStreamToEnd();
+ boolean allowsPlayback =
+ isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded();
+ renderersAllowPlayback = renderersAllowPlayback && allowsPlayback;
+ if (!allowsPlayback) {
+ renderer.maybeThrowStreamError();
+ }
+ }
+ } else {
+ playingPeriodHolder.mediaPeriod.maybeThrowPrepareError();
+ }
+
+ long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
+ if (renderersEnded
+ && playingPeriodHolder.prepared
+ && (playingPeriodDurationUs == C.TIME_UNSET
+ || playingPeriodDurationUs <= playbackInfo.positionUs)
+ && playingPeriodHolder.info.isFinal) {
+ setState(Player.STATE_ENDED);
+ stopRenderers();
+ } else if (playbackInfo.playbackState == Player.STATE_BUFFERING
+ && shouldTransitionToReadyState(renderersAllowPlayback)) {
+ setState(Player.STATE_READY);
+ if (playWhenReady) {
+ startRenderers();
+ }
+ } else if (playbackInfo.playbackState == Player.STATE_READY
+ && !(enabledRenderers.length == 0 ? isTimelineReady() : renderersAllowPlayback)) {
+ rebuffering = playWhenReady;
+ setState(Player.STATE_BUFFERING);
+ stopRenderers();
+ }
+
+ if (playbackInfo.playbackState == Player.STATE_BUFFERING) {
+ for (Renderer renderer : enabledRenderers) {
+ renderer.maybeThrowStreamError();
+ }
+ }
+
+ if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY)
+ || playbackInfo.playbackState == Player.STATE_BUFFERING) {
+ scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
+ } else if (enabledRenderers.length != 0 && playbackInfo.playbackState != Player.STATE_ENDED) {
+ scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
+ } else {
+ handler.removeMessages(MSG_DO_SOME_WORK);
+ }
+
+ TraceUtil.endSection();
+ }
+
+ private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) {
+ handler.removeMessages(MSG_DO_SOME_WORK);
+ handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs);
+ }
+
+ private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
+ playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
+
+ MediaPeriodId periodId;
+ long periodPositionUs;
+ long contentPositionUs;
+ boolean seekPositionAdjusted;
+ Pair<Object, Long> resolvedSeekPosition =
+ resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true);
+ if (resolvedSeekPosition == null) {
+ // The seek position was valid for the timeline that it was performed into, but the
+ // timeline has changed or is not ready and a suitable seek position could not be resolved.
+ periodId = playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period);
+ periodPositionUs = C.TIME_UNSET;
+ contentPositionUs = C.TIME_UNSET;
+ seekPositionAdjusted = true;
+ } else {
+ // Update the resolved seek position to take ads into account.
+ Object periodUid = resolvedSeekPosition.first;
+ contentPositionUs = resolvedSeekPosition.second;
+ periodId = queue.resolveMediaPeriodIdForAds(periodUid, contentPositionUs);
+ if (periodId.isAd()) {
+ periodPositionUs = 0;
+ seekPositionAdjusted = true;
+ } else {
+ periodPositionUs = resolvedSeekPosition.second;
+ seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET;
+ }
+ }
+
+ try {
+ if (mediaSource == null || pendingPrepareCount > 0) {
+ // Save seek position for later, as we are still waiting for a prepared source.
+ pendingInitialSeekPosition = seekPosition;
+ } else if (periodPositionUs == C.TIME_UNSET) {
+ // End playback, as we didn't manage to find a valid seek position.
+ setState(Player.STATE_ENDED);
+ resetInternal(
+ /* resetRenderers= */ false,
+ /* releaseMediaSource= */ false,
+ /* resetPosition= */ true,
+ /* resetState= */ false,
+ /* resetError= */ true);
+ } else {
+ // Execute the seek in the current media periods.
+ long newPeriodPositionUs = periodPositionUs;
+ if (periodId.equals(playbackInfo.periodId)) {
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ if (playingPeriodHolder != null
+ && playingPeriodHolder.prepared
+ && newPeriodPositionUs != 0) {
+ newPeriodPositionUs =
+ playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs(
+ newPeriodPositionUs, seekParameters);
+ }
+ if (C.usToMs(newPeriodPositionUs) == C.usToMs(playbackInfo.positionUs)) {
+ // Seek will be performed to the current position. Do nothing.
+ periodPositionUs = playbackInfo.positionUs;
+ return;
+ }
+ }
+ newPeriodPositionUs = seekToPeriodPosition(periodId, newPeriodPositionUs);
+ seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;
+ periodPositionUs = newPeriodPositionUs;
+ }
+ } finally {
+ playbackInfo = copyWithNewPosition(periodId, periodPositionUs, contentPositionUs);
+ if (seekPositionAdjusted) {
+ playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT);
+ }
+ }
+ }
+
+ private long seekToPeriodPosition(MediaPeriodId periodId, long periodPositionUs)
+ throws ExoPlaybackException {
+ // Force disable renderers if they are reading from a period other than the one being played.
+ return seekToPeriodPosition(
+ periodId, periodPositionUs, queue.getPlayingPeriod() != queue.getReadingPeriod());
+ }
+
+ private long seekToPeriodPosition(
+ MediaPeriodId periodId, long periodPositionUs, boolean forceDisableRenderers)
+ throws ExoPlaybackException {
+ stopRenderers();
+ rebuffering = false;
+ if (playbackInfo.playbackState != Player.STATE_IDLE && !playbackInfo.timeline.isEmpty()) {
+ setState(Player.STATE_BUFFERING);
+ }
+
+ // Clear the timeline, but keep the requested period if it is already prepared.
+ MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod();
+ MediaPeriodHolder newPlayingPeriodHolder = oldPlayingPeriodHolder;
+ while (newPlayingPeriodHolder != null) {
+ if (periodId.equals(newPlayingPeriodHolder.info.id) && newPlayingPeriodHolder.prepared) {
+ queue.removeAfter(newPlayingPeriodHolder);
+ break;
+ }
+ newPlayingPeriodHolder = queue.advancePlayingPeriod();
+ }
+
+ // Disable all renderers if the period being played is changing, if the seek results in negative
+ // renderer timestamps, or if forced.
+ if (forceDisableRenderers
+ || oldPlayingPeriodHolder != newPlayingPeriodHolder
+ || (newPlayingPeriodHolder != null
+ && newPlayingPeriodHolder.toRendererTime(periodPositionUs) < 0)) {
+ for (Renderer renderer : enabledRenderers) {
+ disableRenderer(renderer);
+ }
+ enabledRenderers = new Renderer[0];
+ oldPlayingPeriodHolder = null;
+ if (newPlayingPeriodHolder != null) {
+ newPlayingPeriodHolder.setRendererOffset(/* rendererPositionOffsetUs= */ 0);
+ }
+ }
+
+ // Update the holders.
+ if (newPlayingPeriodHolder != null) {
+ updatePlayingPeriodRenderers(oldPlayingPeriodHolder);
+ if (newPlayingPeriodHolder.hasEnabledTracks) {
+ periodPositionUs = newPlayingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs);
+ newPlayingPeriodHolder.mediaPeriod.discardBuffer(
+ periodPositionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
+ }
+ resetRendererPosition(periodPositionUs);
+ maybeContinueLoading();
+ } else {
+ queue.clear(/* keepFrontPeriodUid= */ true);
+ // New period has not been prepared.
+ playbackInfo =
+ playbackInfo.copyWithTrackInfo(TrackGroupArray.EMPTY, emptyTrackSelectorResult);
+ resetRendererPosition(periodPositionUs);
+ }
+
+ handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ return periodPositionUs;
+ }
+
+ private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException {
+ MediaPeriodHolder playingMediaPeriod = queue.getPlayingPeriod();
+ rendererPositionUs =
+ playingMediaPeriod == null
+ ? periodPositionUs
+ : playingMediaPeriod.toRendererTime(periodPositionUs);
+ mediaClock.resetPosition(rendererPositionUs);
+ for (Renderer renderer : enabledRenderers) {
+ renderer.resetPosition(rendererPositionUs);
+ }
+ notifyTrackSelectionDiscontinuity();
+ }
+
+ private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) {
+ mediaClock.setPlaybackParameters(playbackParameters);
+ sendPlaybackParametersChangedInternal(
+ mediaClock.getPlaybackParameters(), /* acknowledgeCommand= */ true);
+ }
+
+ private void setSeekParametersInternal(SeekParameters seekParameters) {
+ this.seekParameters = seekParameters;
+ }
+
+ private void setForegroundModeInternal(
+ boolean foregroundMode, @Nullable AtomicBoolean processedFlag) {
+ if (this.foregroundMode != foregroundMode) {
+ this.foregroundMode = foregroundMode;
+ if (!foregroundMode) {
+ for (Renderer renderer : renderers) {
+ if (renderer.getState() == Renderer.STATE_DISABLED) {
+ renderer.reset();
+ }
+ }
+ }
+ }
+ if (processedFlag != null) {
+ synchronized (this) {
+ processedFlag.set(true);
+ notifyAll();
+ }
+ }
+ }
+
+ private void stopInternal(
+ boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) {
+ resetInternal(
+ /* resetRenderers= */ forceResetRenderers || !foregroundMode,
+ /* releaseMediaSource= */ true,
+ /* resetPosition= */ resetPositionAndState,
+ /* resetState= */ resetPositionAndState,
+ /* resetError= */ resetPositionAndState);
+ playbackInfoUpdate.incrementPendingOperationAcks(
+ pendingPrepareCount + (acknowledgeStop ? 1 : 0));
+ pendingPrepareCount = 0;
+ loadControl.onStopped();
+ setState(Player.STATE_IDLE);
+ }
+
+ private void releaseInternal() {
+ resetInternal(
+ /* resetRenderers= */ true,
+ /* releaseMediaSource= */ true,
+ /* resetPosition= */ true,
+ /* resetState= */ true,
+ /* resetError= */ false);
+ loadControl.onReleased();
+ setState(Player.STATE_IDLE);
+ internalPlaybackThread.quit();
+ synchronized (this) {
+ released = true;
+ notifyAll();
+ }
+ }
+
+ private void resetInternal(
+ boolean resetRenderers,
+ boolean releaseMediaSource,
+ boolean resetPosition,
+ boolean resetState,
+ boolean resetError) {
+ handler.removeMessages(MSG_DO_SOME_WORK);
+ rebuffering = false;
+ mediaClock.stop();
+ rendererPositionUs = 0;
+ for (Renderer renderer : enabledRenderers) {
+ try {
+ disableRenderer(renderer);
+ } catch (ExoPlaybackException | RuntimeException e) {
+ // There's nothing we can do.
+ Log.e(TAG, "Disable failed.", e);
+ }
+ }
+ if (resetRenderers) {
+ for (Renderer renderer : renderers) {
+ try {
+ renderer.reset();
+ } catch (RuntimeException e) {
+ // There's nothing we can do.
+ Log.e(TAG, "Reset failed.", e);
+ }
+ }
+ }
+ enabledRenderers = new Renderer[0];
+
+ if (resetPosition) {
+ pendingInitialSeekPosition = null;
+ } else if (resetState) {
+ // When resetting the state, also reset the period-based PlaybackInfo position and convert
+ // existing position to initial seek instead.
+ resetPosition = true;
+ if (pendingInitialSeekPosition == null && !playbackInfo.timeline.isEmpty()) {
+ playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);
+ long windowPositionUs = playbackInfo.positionUs + period.getPositionInWindowUs();
+ pendingInitialSeekPosition =
+ new SeekPosition(Timeline.EMPTY, period.windowIndex, windowPositionUs);
+ }
+ }
+
+ queue.clear(/* keepFrontPeriodUid= */ !resetState);
+ shouldContinueLoading = false;
+ if (resetState) {
+ queue.setTimeline(Timeline.EMPTY);
+ for (PendingMessageInfo pendingMessageInfo : pendingMessages) {
+ pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false);
+ }
+ pendingMessages.clear();
+ nextPendingMessageIndex = 0;
+ }
+ MediaPeriodId mediaPeriodId =
+ resetPosition
+ ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period)
+ : playbackInfo.periodId;
+ // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored.
+ long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs;
+ long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;
+ playbackInfo =
+ new PlaybackInfo(
+ resetState ? Timeline.EMPTY : playbackInfo.timeline,
+ mediaPeriodId,
+ startPositionUs,
+ contentPositionUs,
+ playbackInfo.playbackState,
+ resetError ? null : playbackInfo.playbackError,
+ /* isLoading= */ false,
+ resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
+ resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
+ mediaPeriodId,
+ startPositionUs,
+ /* totalBufferedDurationUs= */ 0,
+ startPositionUs);
+ if (releaseMediaSource) {
+ if (mediaSource != null) {
+ mediaSource.releaseSource(/* caller= */ this);
+ mediaSource = null;
+ }
+ }
+ }
+
+ private void sendMessageInternal(PlayerMessage message) throws ExoPlaybackException {
+ if (message.getPositionMs() == C.TIME_UNSET) {
+ // If no delivery time is specified, trigger immediate message delivery.
+ sendMessageToTarget(message);
+ } else if (mediaSource == null || pendingPrepareCount > 0) {
+ // Still waiting for initial timeline to resolve position.
+ pendingMessages.add(new PendingMessageInfo(message));
+ } else {
+ PendingMessageInfo pendingMessageInfo = new PendingMessageInfo(message);
+ if (resolvePendingMessagePosition(pendingMessageInfo)) {
+ pendingMessages.add(pendingMessageInfo);
+ // Ensure new message is inserted according to playback order.
+ Collections.sort(pendingMessages);
+ } else {
+ message.markAsProcessed(/* isDelivered= */ false);
+ }
+ }
+ }
+
+ private void sendMessageToTarget(PlayerMessage message) throws ExoPlaybackException {
+ if (message.getHandler().getLooper() == handler.getLooper()) {
+ deliverMessage(message);
+ if (playbackInfo.playbackState == Player.STATE_READY
+ || playbackInfo.playbackState == Player.STATE_BUFFERING) {
+ // The message may have caused something to change that now requires us to do work.
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ }
+ } else {
+ handler.obtainMessage(MSG_SEND_MESSAGE_TO_TARGET_THREAD, message).sendToTarget();
+ }
+ }
+
+ private void sendMessageToTargetThread(final PlayerMessage message) {
+ Handler handler = message.getHandler();
+ if (!handler.getLooper().getThread().isAlive()) {
+ Log.w("TAG", "Trying to send message on a dead thread.");
+ message.markAsProcessed(/* isDelivered= */ false);
+ return;
+ }
+ handler.post(
+ () -> {
+ try {
+ deliverMessage(message);
+ } catch (ExoPlaybackException e) {
+ Log.e(TAG, "Unexpected error delivering message on external thread.", e);
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ private void deliverMessage(PlayerMessage message) throws ExoPlaybackException {
+ if (message.isCanceled()) {
+ return;
+ }
+ try {
+ message.getTarget().handleMessage(message.getType(), message.getPayload());
+ } finally {
+ message.markAsProcessed(/* isDelivered= */ true);
+ }
+ }
+
+ private void resolvePendingMessagePositions() {
+ for (int i = pendingMessages.size() - 1; i >= 0; i--) {
+ if (!resolvePendingMessagePosition(pendingMessages.get(i))) {
+ // Unable to resolve a new position for the message. Remove it.
+ pendingMessages.get(i).message.markAsProcessed(/* isDelivered= */ false);
+ pendingMessages.remove(i);
+ }
+ }
+ // Re-sort messages by playback order.
+ Collections.sort(pendingMessages);
+ }
+
+ private boolean resolvePendingMessagePosition(PendingMessageInfo pendingMessageInfo) {
+ if (pendingMessageInfo.resolvedPeriodUid == null) {
+ // Position is still unresolved. Try to find window in current timeline.
+ Pair<Object, Long> periodPosition =
+ resolveSeekPosition(
+ new SeekPosition(
+ pendingMessageInfo.message.getTimeline(),
+ pendingMessageInfo.message.getWindowIndex(),
+ C.msToUs(pendingMessageInfo.message.getPositionMs())),
+ /* trySubsequentPeriods= */ false);
+ if (periodPosition == null) {
+ return false;
+ }
+ pendingMessageInfo.setResolvedPosition(
+ playbackInfo.timeline.getIndexOfPeriod(periodPosition.first),
+ periodPosition.second,
+ periodPosition.first);
+ } else {
+ // Position has been resolved for a previous timeline. Try to find the updated period index.
+ int index = playbackInfo.timeline.getIndexOfPeriod(pendingMessageInfo.resolvedPeriodUid);
+ if (index == C.INDEX_UNSET) {
+ return false;
+ }
+ pendingMessageInfo.resolvedPeriodIndex = index;
+ }
+ return true;
+ }
+
+ private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs)
+ throws ExoPlaybackException {
+ if (pendingMessages.isEmpty() || playbackInfo.periodId.isAd()) {
+ return;
+ }
+ // If this is the first call from the start position, include oldPeriodPositionUs in potential
+ // trigger positions, but make sure we deliver it only once.
+ if (playbackInfo.startPositionUs == oldPeriodPositionUs
+ && deliverPendingMessageAtStartPositionRequired) {
+ oldPeriodPositionUs--;
+ }
+ deliverPendingMessageAtStartPositionRequired = false;
+
+ // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages)
+ int currentPeriodIndex =
+ playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid);
+ PendingMessageInfo previousInfo =
+ nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null;
+ while (previousInfo != null
+ && (previousInfo.resolvedPeriodIndex > currentPeriodIndex
+ || (previousInfo.resolvedPeriodIndex == currentPeriodIndex
+ && previousInfo.resolvedPeriodTimeUs > oldPeriodPositionUs))) {
+ nextPendingMessageIndex--;
+ previousInfo =
+ nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null;
+ }
+ PendingMessageInfo nextInfo =
+ nextPendingMessageIndex < pendingMessages.size()
+ ? pendingMessages.get(nextPendingMessageIndex)
+ : null;
+ while (nextInfo != null
+ && nextInfo.resolvedPeriodUid != null
+ && (nextInfo.resolvedPeriodIndex < currentPeriodIndex
+ || (nextInfo.resolvedPeriodIndex == currentPeriodIndex
+ && nextInfo.resolvedPeriodTimeUs <= oldPeriodPositionUs))) {
+ nextPendingMessageIndex++;
+ nextInfo =
+ nextPendingMessageIndex < pendingMessages.size()
+ ? pendingMessages.get(nextPendingMessageIndex)
+ : null;
+ }
+ // Check if any message falls within the covered time span.
+ while (nextInfo != null
+ && nextInfo.resolvedPeriodUid != null
+ && nextInfo.resolvedPeriodIndex == currentPeriodIndex
+ && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs
+ && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) {
+ try {
+ sendMessageToTarget(nextInfo.message);
+ } finally {
+ if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) {
+ pendingMessages.remove(nextPendingMessageIndex);
+ } else {
+ nextPendingMessageIndex++;
+ }
+ }
+ nextInfo =
+ nextPendingMessageIndex < pendingMessages.size()
+ ? pendingMessages.get(nextPendingMessageIndex)
+ : null;
+ }
+ }
+
+ private void ensureStopped(Renderer renderer) throws ExoPlaybackException {
+ if (renderer.getState() == Renderer.STATE_STARTED) {
+ renderer.stop();
+ }
+ }
+
+ private void disableRenderer(Renderer renderer) throws ExoPlaybackException {
+ mediaClock.onRendererDisabled(renderer);
+ ensureStopped(renderer);
+ renderer.disable();
+ }
+
+ private void reselectTracksInternal() throws ExoPlaybackException {
+ float playbackSpeed = mediaClock.getPlaybackParameters().speed;
+ // Reselect tracks on each period in turn, until the selection changes.
+ MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
+ MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
+ boolean selectionsChangedForReadPeriod = true;
+ TrackSelectorResult newTrackSelectorResult;
+ while (true) {
+ if (periodHolder == null || !periodHolder.prepared) {
+ // The reselection did not change any prepared periods.
+ return;
+ }
+ newTrackSelectorResult = periodHolder.selectTracks(playbackSpeed, playbackInfo.timeline);
+ if (!newTrackSelectorResult.isEquivalent(periodHolder.getTrackSelectorResult())) {
+ // Selected tracks have changed for this period.
+ break;
+ }
+ if (periodHolder == readingPeriodHolder) {
+ // The track reselection didn't affect any period that has been read.
+ selectionsChangedForReadPeriod = false;
+ }
+ periodHolder = periodHolder.getNext();
+ }
+
+ if (selectionsChangedForReadPeriod) {
+ // Update streams and rebuffer for the new selection, recreating all streams if reading ahead.
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ boolean recreateStreams = queue.removeAfter(playingPeriodHolder);
+
+ boolean[] streamResetFlags = new boolean[renderers.length];
+ long periodPositionUs =
+ playingPeriodHolder.applyTrackSelection(
+ newTrackSelectorResult, playbackInfo.positionUs, recreateStreams, streamResetFlags);
+ if (playbackInfo.playbackState != Player.STATE_ENDED
+ && periodPositionUs != playbackInfo.positionUs) {
+ playbackInfo =
+ copyWithNewPosition(
+ playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs);
+ playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
+ resetRendererPosition(periodPositionUs);
+ }
+
+ int enabledRendererCount = 0;
+ boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
+ for (int i = 0; i < renderers.length; i++) {
+ Renderer renderer = renderers[i];
+ rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
+ SampleStream sampleStream = playingPeriodHolder.sampleStreams[i];
+ if (sampleStream != null) {
+ enabledRendererCount++;
+ }
+ if (rendererWasEnabledFlags[i]) {
+ if (sampleStream != renderer.getStream()) {
+ // We need to disable the renderer.
+ disableRenderer(renderer);
+ } else if (streamResetFlags[i]) {
+ // The renderer will continue to consume from its current stream, but needs to be reset.
+ renderer.resetPosition(rendererPositionUs);
+ }
+ }
+ }
+ playbackInfo =
+ playbackInfo.copyWithTrackInfo(
+ playingPeriodHolder.getTrackGroups(), playingPeriodHolder.getTrackSelectorResult());
+ enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
+ } else {
+ // Release and re-prepare/buffer periods after the one whose selection changed.
+ queue.removeAfter(periodHolder);
+ if (periodHolder.prepared) {
+ long loadingPeriodPositionUs =
+ Math.max(
+ periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs));
+ periodHolder.applyTrackSelection(newTrackSelectorResult, loadingPeriodPositionUs, false);
+ }
+ }
+ handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ true);
+ if (playbackInfo.playbackState != Player.STATE_ENDED) {
+ maybeContinueLoading();
+ updatePlaybackPositions();
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ }
+ }
+
+ private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) {
+ MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
+ while (periodHolder != null) {
+ TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();
+ for (TrackSelection trackSelection : trackSelections) {
+ if (trackSelection != null) {
+ trackSelection.onPlaybackSpeed(playbackSpeed);
+ }
+ }
+ periodHolder = periodHolder.getNext();
+ }
+ }
+
+ private void notifyTrackSelectionDiscontinuity() {
+ MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
+ while (periodHolder != null) {
+ TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();
+ for (TrackSelection trackSelection : trackSelections) {
+ if (trackSelection != null) {
+ trackSelection.onDiscontinuity();
+ }
+ }
+ periodHolder = periodHolder.getNext();
+ }
+ }
+
+ private boolean shouldTransitionToReadyState(boolean renderersReadyOrEnded) {
+ if (enabledRenderers.length == 0) {
+ // If there are no enabled renderers, determine whether we're ready based on the timeline.
+ return isTimelineReady();
+ }
+ if (!renderersReadyOrEnded) {
+ return false;
+ }
+ if (!playbackInfo.isLoading) {
+ // Renderers are ready and we're not loading. Transition to ready, since the alternative is
+ // getting stuck waiting for additional media that's not being loaded.
+ return true;
+ }
+ // Renderers are ready and we're loading. Ask the LoadControl whether to transition.
+ MediaPeriodHolder loadingHolder = queue.getLoadingPeriod();
+ boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal;
+ return bufferedToEnd
+ || loadControl.shouldStartPlayback(
+ getTotalBufferedDurationUs(), mediaClock.getPlaybackParameters().speed, rebuffering);
+ }
+
+ private boolean isTimelineReady() {
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
+ return playingPeriodHolder.prepared
+ && (playingPeriodDurationUs == C.TIME_UNSET
+ || playbackInfo.positionUs < playingPeriodDurationUs);
+ }
+
+ private void maybeThrowSourceInfoRefreshError() throws IOException {
+ MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
+ if (loadingPeriodHolder != null) {
+ // Defer throwing until we read all available media periods.
+ for (Renderer renderer : enabledRenderers) {
+ if (!renderer.hasReadStreamToEnd()) {
+ return;
+ }
+ }
+ }
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ }
+
+ private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo)
+ throws ExoPlaybackException {
+ if (sourceRefreshInfo.source != mediaSource) {
+ // Stale event.
+ return;
+ }
+ playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);
+ pendingPrepareCount = 0;
+
+ Timeline oldTimeline = playbackInfo.timeline;
+ Timeline timeline = sourceRefreshInfo.timeline;
+ queue.setTimeline(timeline);
+ playbackInfo = playbackInfo.copyWithTimeline(timeline);
+ resolvePendingMessagePositions();
+
+ MediaPeriodId newPeriodId = playbackInfo.periodId;
+ long oldContentPositionUs =
+ playbackInfo.periodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs;
+ long newContentPositionUs = oldContentPositionUs;
+ if (pendingInitialSeekPosition != null) {
+ // Resolve initial seek position.
+ Pair<Object, Long> periodPosition =
+ resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true);
+ pendingInitialSeekPosition = null;
+ if (periodPosition == null) {
+ // The seek position was valid for the timeline that it was performed into, but the
+ // timeline has changed and a suitable seek position could not be resolved in the new one.
+ handleSourceInfoRefreshEndedPlayback();
+ return;
+ }
+ newContentPositionUs = periodPosition.second;
+ newPeriodId = queue.resolveMediaPeriodIdForAds(periodPosition.first, newContentPositionUs);
+ } else if (oldContentPositionUs == C.TIME_UNSET && !timeline.isEmpty()) {
+ // Resolve unset start position to default position.
+ Pair<Object, Long> defaultPosition =
+ getPeriodPosition(
+ timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET);
+ newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, defaultPosition.second);
+ if (!newPeriodId.isAd()) {
+ // Keep unset start position if we need to play an ad first.
+ newContentPositionUs = defaultPosition.second;
+ }
+ } else if (timeline.getIndexOfPeriod(newPeriodId.periodUid) == C.INDEX_UNSET) {
+ // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose
+ // window we can restart from.
+ Object newPeriodUid = resolveSubsequentPeriod(newPeriodId.periodUid, oldTimeline, timeline);
+ if (newPeriodUid == null) {
+ // We failed to resolve a suitable restart position.
+ handleSourceInfoRefreshEndedPlayback();
+ return;
+ }
+ // We resolved a subsequent period. Start at the default position in the corresponding window.
+ Pair<Object, Long> defaultPosition =
+ getPeriodPosition(
+ timeline, timeline.getPeriodByUid(newPeriodUid, period).windowIndex, C.TIME_UNSET);
+ newContentPositionUs = defaultPosition.second;
+ newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs);
+ } else {
+ // Recheck if the current ad still needs to be played or if we need to start playing an ad.
+ newPeriodId =
+ queue.resolveMediaPeriodIdForAds(playbackInfo.periodId.periodUid, newContentPositionUs);
+ if (!playbackInfo.periodId.isAd() && !newPeriodId.isAd()) {
+ // Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and
+ // only MediaPeriodId.nextAdGroupIndex may have changed. This postpones a potential
+ // discontinuity until we reach the former next ad group position.
+ newPeriodId = playbackInfo.periodId;
+ }
+ }
+
+ if (playbackInfo.periodId.equals(newPeriodId) && oldContentPositionUs == newContentPositionUs) {
+ // We can keep the current playing period. Update the rest of the queued periods.
+ if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) {
+ seekToCurrentPosition(/* sendDiscontinuity= */ false);
+ }
+ } else {
+ // Something changed. Seek to new start position.
+ MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
+ if (periodHolder != null) {
+ // Update the new playing media period info if it already exists.
+ while (periodHolder.getNext() != null) {
+ periodHolder = periodHolder.getNext();
+ if (periodHolder.info.id.equals(newPeriodId)) {
+ periodHolder.info = queue.getUpdatedMediaPeriodInfo(periodHolder.info);
+ }
+ }
+ }
+ // Actually do the seek.
+ long newPositionUs = newPeriodId.isAd() ? 0 : newContentPositionUs;
+ long seekedToPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs);
+ playbackInfo = copyWithNewPosition(newPeriodId, seekedToPositionUs, newContentPositionUs);
+ }
+ handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
+ }
+
+ private long getMaxRendererReadPositionUs() {
+ MediaPeriodHolder readingHolder = queue.getReadingPeriod();
+ if (readingHolder == null) {
+ return 0;
+ }
+ long maxReadPositionUs = readingHolder.getRendererOffset();
+ if (!readingHolder.prepared) {
+ return maxReadPositionUs;
+ }
+ for (int i = 0; i < renderers.length; i++) {
+ if (renderers[i].getState() == Renderer.STATE_DISABLED
+ || renderers[i].getStream() != readingHolder.sampleStreams[i]) {
+ // Ignore disabled renderers and renderers with sample streams from previous periods.
+ continue;
+ }
+ long readingPositionUs = renderers[i].getReadingPositionUs();
+ if (readingPositionUs == C.TIME_END_OF_SOURCE) {
+ return C.TIME_END_OF_SOURCE;
+ } else {
+ maxReadPositionUs = Math.max(readingPositionUs, maxReadPositionUs);
+ }
+ }
+ return maxReadPositionUs;
+ }
+
+ private void handleSourceInfoRefreshEndedPlayback() {
+ if (playbackInfo.playbackState != Player.STATE_IDLE) {
+ setState(Player.STATE_ENDED);
+ }
+ // Reset, but retain the source so that it can still be used should a seek occur.
+ resetInternal(
+ /* resetRenderers= */ false,
+ /* releaseMediaSource= */ false,
+ /* resetPosition= */ true,
+ /* resetState= */ false,
+ /* resetError= */ true);
+ }
+
+ /**
+ * Given a period index into an old timeline, finds the first subsequent period that also exists
+ * in a new timeline. The uid of this period in the new timeline is returned.
+ *
+ * @param oldPeriodUid The index of the period in the old timeline.
+ * @param oldTimeline The old timeline.
+ * @param newTimeline The new timeline.
+ * @return The uid in the new timeline of the first subsequent period, or null if no such period
+ * was found.
+ */
+ private @Nullable Object resolveSubsequentPeriod(
+ Object oldPeriodUid, Timeline oldTimeline, Timeline newTimeline) {
+ int oldPeriodIndex = oldTimeline.getIndexOfPeriod(oldPeriodUid);
+ int newPeriodIndex = C.INDEX_UNSET;
+ int maxIterations = oldTimeline.getPeriodCount();
+ for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) {
+ oldPeriodIndex =
+ oldTimeline.getNextPeriodIndex(
+ oldPeriodIndex, period, window, repeatMode, shuffleModeEnabled);
+ if (oldPeriodIndex == C.INDEX_UNSET) {
+ // We've reached the end of the old timeline.
+ break;
+ }
+ newPeriodIndex = newTimeline.getIndexOfPeriod(oldTimeline.getUidOfPeriod(oldPeriodIndex));
+ }
+ return newPeriodIndex == C.INDEX_UNSET ? null : newTimeline.getUidOfPeriod(newPeriodIndex);
+ }
+
+ /**
+ * Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the
+ * internal timeline.
+ *
+ * @param seekPosition The position to resolve.
+ * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching
+ * period if the original period is no longer available.
+ * @return The resolved position, or null if resolution was not successful.
+ * @throws IllegalSeekPositionException If the window index of the seek position is outside the
+ * bounds of the timeline.
+ */
+ @Nullable
+ private Pair<Object, Long> resolveSeekPosition(
+ SeekPosition seekPosition, boolean trySubsequentPeriods) {
+ Timeline timeline = playbackInfo.timeline;
+ Timeline seekTimeline = seekPosition.timeline;
+ if (timeline.isEmpty()) {
+ // We don't have a valid timeline yet, so we can't resolve the position.
+ return null;
+ }
+ if (seekTimeline.isEmpty()) {
+ // The application performed a blind seek with an empty timeline (most likely based on
+ // knowledge of what the future timeline will be). Use the internal timeline.
+ seekTimeline = timeline;
+ }
+ // Map the SeekPosition to a position in the corresponding timeline.
+ Pair<Object, Long> periodPosition;
+ try {
+ periodPosition =
+ seekTimeline.getPeriodPosition(
+ window, period, seekPosition.windowIndex, seekPosition.windowPositionUs);
+ } catch (IndexOutOfBoundsException e) {
+ // The window index of the seek position was outside the bounds of the timeline.
+ return null;
+ }
+ if (timeline == seekTimeline) {
+ // Our internal timeline is the seek timeline, so the mapped position is correct.
+ return periodPosition;
+ }
+ // Attempt to find the mapped period in the internal timeline.
+ int periodIndex = timeline.getIndexOfPeriod(periodPosition.first);
+ if (periodIndex != C.INDEX_UNSET) {
+ // We successfully located the period in the internal timeline.
+ return periodPosition;
+ }
+ if (trySubsequentPeriods) {
+ // Try and find a subsequent period from the seek timeline in the internal timeline.
+ @Nullable
+ Object periodUid = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline);
+ if (periodUid != null) {
+ // We found one. Use the default position of the corresponding window.
+ return getPeriodPosition(
+ timeline, timeline.getPeriodByUid(periodUid, period).windowIndex, C.TIME_UNSET);
+ }
+ }
+ // We didn't find one. Give up.
+ return null;
+ }
+
+ /**
+ * Calls {@link Timeline#getPeriodPosition(Timeline.Window, Timeline.Period, int, long)} using the
+ * current timeline.
+ */
+ private Pair<Object, Long> getPeriodPosition(
+ Timeline timeline, int windowIndex, long windowPositionUs) {
+ return timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
+ }
+
+ private void updatePeriods() throws ExoPlaybackException, IOException {
+ if (mediaSource == null) {
+ // The player has no media source yet.
+ return;
+ }
+ if (pendingPrepareCount > 0) {
+ // We're waiting to get information about periods.
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ return;
+ }
+ maybeUpdateLoadingPeriod();
+ maybeUpdateReadingPeriod();
+ maybeUpdatePlayingPeriod();
+ }
+
+ private void maybeUpdateLoadingPeriod() throws ExoPlaybackException, IOException {
+ queue.reevaluateBuffer(rendererPositionUs);
+ if (queue.shouldLoadNextMediaPeriod()) {
+ MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo);
+ if (info == null) {
+ maybeThrowSourceInfoRefreshError();
+ } else {
+ MediaPeriodHolder mediaPeriodHolder =
+ queue.enqueueNextMediaPeriodHolder(
+ rendererCapabilities,
+ trackSelector,
+ loadControl.getAllocator(),
+ mediaSource,
+ info,
+ emptyTrackSelectorResult);
+ mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
+ if (queue.getPlayingPeriod() == mediaPeriodHolder) {
+ resetRendererPosition(mediaPeriodHolder.getStartPositionRendererTime());
+ }
+ handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
+ }
+ }
+ if (shouldContinueLoading) {
+ shouldContinueLoading = isLoadingPossible();
+ updateIsLoading();
+ } else {
+ maybeContinueLoading();
+ }
+ }
+
+ private void maybeUpdateReadingPeriod() throws ExoPlaybackException {
+ MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
+ if (readingPeriodHolder == null) {
+ return;
+ }
+
+ if (readingPeriodHolder.getNext() == null) {
+ // We don't have a successor to advance the reading period to.
+ if (readingPeriodHolder.info.isFinal) {
+ for (int i = 0; i < renderers.length; i++) {
+ Renderer renderer = renderers[i];
+ SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
+ // Defer setting the stream as final until the renderer has actually consumed the whole
+ // stream in case of playlist changes that cause the stream to be no longer final.
+ if (sampleStream != null
+ && renderer.getStream() == sampleStream
+ && renderer.hasReadStreamToEnd()) {
+ renderer.setCurrentStreamFinal();
+ }
+ }
+ }
+ return;
+ }
+
+ if (!hasReadingPeriodFinishedReading()) {
+ return;
+ }
+
+ if (!readingPeriodHolder.getNext().prepared) {
+ // The successor is not prepared yet.
+ return;
+ }
+
+ TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();
+ readingPeriodHolder = queue.advanceReadingPeriod();
+ TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();
+
+ if (readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) {
+ // The new period starts with a discontinuity, so the renderers will play out all data, then
+ // be disabled and re-enabled when they start playing the next period.
+ setAllRendererStreamsFinal();
+ return;
+ }
+ for (int i = 0; i < renderers.length; i++) {
+ Renderer renderer = renderers[i];
+ boolean rendererWasEnabled = oldTrackSelectorResult.isRendererEnabled(i);
+ if (rendererWasEnabled && !renderer.isCurrentStreamFinal()) {
+ // The renderer is enabled and its stream is not final, so we still have a chance to replace
+ // the sample streams.
+ TrackSelection newSelection = newTrackSelectorResult.selections.get(i);
+ boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(i);
+ boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE;
+ RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i];
+ RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i];
+ if (newRendererEnabled && newConfig.equals(oldConfig) && !isNoSampleRenderer) {
+ // Replace the renderer's SampleStream so the transition to playing the next period can
+ // be seamless.
+ // This should be avoided for no-sample renderer, because skipping ahead for such
+ // renderer doesn't have any benefit (the renderer does not consume the sample stream),
+ // and it will change the provided rendererOffsetUs while the renderer is still
+ // rendering from the playing media period.
+ Format[] formats = getFormats(newSelection);
+ renderer.replaceStream(
+ formats,
+ readingPeriodHolder.sampleStreams[i],
+ readingPeriodHolder.getRendererOffset());
+ } else {
+ // The renderer will be disabled when transitioning to playing the next period, because
+ // there's no new selection, or because a configuration change is required, or because
+ // it's a no-sample renderer for which rendererOffsetUs should be updated only when
+ // starting to play the next period. Mark the SampleStream as final to play out any
+ // remaining data.
+ renderer.setCurrentStreamFinal();
+ }
+ }
+ }
+ }
+
+ private void maybeUpdatePlayingPeriod() throws ExoPlaybackException {
+ boolean advancedPlayingPeriod = false;
+ while (shouldAdvancePlayingPeriod()) {
+ if (advancedPlayingPeriod) {
+ // If we advance more than one period at a time, notify listeners after each update.
+ maybeNotifyPlaybackInfoChanged();
+ }
+ MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod();
+ if (oldPlayingPeriodHolder == queue.getReadingPeriod()) {
+ // The reading period hasn't advanced yet, so we can't seamlessly replace the SampleStreams
+ // anymore and need to re-enable the renderers. Set all current streams final to do that.
+ setAllRendererStreamsFinal();
+ }
+ MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod();
+ updatePlayingPeriodRenderers(oldPlayingPeriodHolder);
+ playbackInfo =
+ copyWithNewPosition(
+ newPlayingPeriodHolder.info.id,
+ newPlayingPeriodHolder.info.startPositionUs,
+ newPlayingPeriodHolder.info.contentPositionUs);
+ int discontinuityReason =
+ oldPlayingPeriodHolder.info.isLastInTimelinePeriod
+ ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
+ : Player.DISCONTINUITY_REASON_AD_INSERTION;
+ playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason);
+ updatePlaybackPositions();
+ advancedPlayingPeriod = true;
+ }
+ }
+
+ private boolean shouldAdvancePlayingPeriod() {
+ if (!playWhenReady) {
+ return false;
+ }
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ if (playingPeriodHolder == null) {
+ return false;
+ }
+ MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext();
+ if (nextPlayingPeriodHolder == null) {
+ return false;
+ }
+ MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
+ if (playingPeriodHolder == readingPeriodHolder && !hasReadingPeriodFinishedReading()) {
+ return false;
+ }
+ return rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime();
+ }
+
+ private boolean hasReadingPeriodFinishedReading() {
+ MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
+ if (!readingPeriodHolder.prepared) {
+ return false;
+ }
+ for (int i = 0; i < renderers.length; i++) {
+ Renderer renderer = renderers[i];
+ SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
+ if (renderer.getStream() != sampleStream
+ || (sampleStream != null && !renderer.hasReadStreamToEnd())) {
+ // The current reading period is still being read by at least one renderer.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void setAllRendererStreamsFinal() {
+ for (Renderer renderer : renderers) {
+ if (renderer.getStream() != null) {
+ renderer.setCurrentStreamFinal();
+ }
+ }
+ }
+
+ private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException {
+ if (!queue.isLoading(mediaPeriod)) {
+ // Stale event.
+ return;
+ }
+ MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
+ loadingPeriodHolder.handlePrepared(
+ mediaClock.getPlaybackParameters().speed, playbackInfo.timeline);
+ updateLoadControlTrackSelection(
+ loadingPeriodHolder.getTrackGroups(), loadingPeriodHolder.getTrackSelectorResult());
+ if (loadingPeriodHolder == queue.getPlayingPeriod()) {
+ // This is the first prepared period, so update the position and the renderers.
+ resetRendererPosition(loadingPeriodHolder.info.startPositionUs);
+ updatePlayingPeriodRenderers(/* oldPlayingPeriodHolder= */ null);
+ }
+ maybeContinueLoading();
+ }
+
+ private void handleContinueLoadingRequested(MediaPeriod mediaPeriod) {
+ if (!queue.isLoading(mediaPeriod)) {
+ // Stale event.
+ return;
+ }
+ queue.reevaluateBuffer(rendererPositionUs);
+ maybeContinueLoading();
+ }
+
+ private void handlePlaybackParameters(
+ PlaybackParameters playbackParameters, boolean acknowledgeCommand)
+ throws ExoPlaybackException {
+ eventHandler
+ .obtainMessage(
+ MSG_PLAYBACK_PARAMETERS_CHANGED, acknowledgeCommand ? 1 : 0, 0, playbackParameters)
+ .sendToTarget();
+ updateTrackSelectionPlaybackSpeed(playbackParameters.speed);
+ for (Renderer renderer : renderers) {
+ if (renderer != null) {
+ renderer.setOperatingRate(playbackParameters.speed);
+ }
+ }
+ }
+
+ private void maybeContinueLoading() {
+ shouldContinueLoading = shouldContinueLoading();
+ if (shouldContinueLoading) {
+ queue.getLoadingPeriod().continueLoading(rendererPositionUs);
+ }
+ updateIsLoading();
+ }
+
+ private boolean shouldContinueLoading() {
+ if (!isLoadingPossible()) {
+ return false;
+ }
+ long bufferedDurationUs =
+ getTotalBufferedDurationUs(queue.getLoadingPeriod().getNextLoadPositionUs());
+ float playbackSpeed = mediaClock.getPlaybackParameters().speed;
+ return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed);
+ }
+
+ private boolean isLoadingPossible() {
+ MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
+ if (loadingPeriodHolder == null) {
+ return false;
+ }
+ long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs();
+ if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
+ return false;
+ }
+ return true;
+ }
+
+ private void updateIsLoading() {
+ MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod();
+ boolean isLoading =
+ shouldContinueLoading || (loadingPeriod != null && loadingPeriod.mediaPeriod.isLoading());
+ if (isLoading != playbackInfo.isLoading) {
+ playbackInfo = playbackInfo.copyWithIsLoading(isLoading);
+ }
+ }
+
+ private PlaybackInfo copyWithNewPosition(
+ MediaPeriodId mediaPeriodId, long positionUs, long contentPositionUs) {
+ deliverPendingMessageAtStartPositionRequired = true;
+ return playbackInfo.copyWithNewPosition(
+ mediaPeriodId, positionUs, contentPositionUs, getTotalBufferedDurationUs());
+ }
+
+ @SuppressWarnings("ParameterNotNullable")
+ private void updatePlayingPeriodRenderers(@Nullable MediaPeriodHolder oldPlayingPeriodHolder)
+ throws ExoPlaybackException {
+ MediaPeriodHolder newPlayingPeriodHolder = queue.getPlayingPeriod();
+ if (newPlayingPeriodHolder == null || oldPlayingPeriodHolder == newPlayingPeriodHolder) {
+ return;
+ }
+ int enabledRendererCount = 0;
+ boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
+ for (int i = 0; i < renderers.length; i++) {
+ Renderer renderer = renderers[i];
+ rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
+ if (newPlayingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i)) {
+ enabledRendererCount++;
+ }
+ if (rendererWasEnabledFlags[i]
+ && (!newPlayingPeriodHolder.getTrackSelectorResult().isRendererEnabled(i)
+ || (renderer.isCurrentStreamFinal()
+ && renderer.getStream() == oldPlayingPeriodHolder.sampleStreams[i]))) {
+ // The renderer should be disabled before playing the next period, either because it's not
+ // needed to play the next period, or because we need to re-enable it as its current stream
+ // is final and it's not reading ahead.
+ disableRenderer(renderer);
+ }
+ }
+ playbackInfo =
+ playbackInfo.copyWithTrackInfo(
+ newPlayingPeriodHolder.getTrackGroups(),
+ newPlayingPeriodHolder.getTrackSelectorResult());
+ enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
+ }
+
+ private void enableRenderers(boolean[] rendererWasEnabledFlags, int totalEnabledRendererCount)
+ throws ExoPlaybackException {
+ enabledRenderers = new Renderer[totalEnabledRendererCount];
+ int enabledRendererCount = 0;
+ TrackSelectorResult trackSelectorResult = queue.getPlayingPeriod().getTrackSelectorResult();
+ // Reset all disabled renderers before enabling any new ones. This makes sure resources released
+ // by the disabled renderers will be available to renderers that are being enabled.
+ for (int i = 0; i < renderers.length; i++) {
+ if (!trackSelectorResult.isRendererEnabled(i)) {
+ renderers[i].reset();
+ }
+ }
+ // Enable the renderers.
+ for (int i = 0; i < renderers.length; i++) {
+ if (trackSelectorResult.isRendererEnabled(i)) {
+ enableRenderer(i, rendererWasEnabledFlags[i], enabledRendererCount++);
+ }
+ }
+ }
+
+ private void enableRenderer(
+ int rendererIndex, boolean wasRendererEnabled, int enabledRendererIndex)
+ throws ExoPlaybackException {
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ Renderer renderer = renderers[rendererIndex];
+ enabledRenderers[enabledRendererIndex] = renderer;
+ if (renderer.getState() == Renderer.STATE_DISABLED) {
+ TrackSelectorResult trackSelectorResult = playingPeriodHolder.getTrackSelectorResult();
+ RendererConfiguration rendererConfiguration =
+ trackSelectorResult.rendererConfigurations[rendererIndex];
+ TrackSelection newSelection = trackSelectorResult.selections.get(rendererIndex);
+ Format[] formats = getFormats(newSelection);
+ // The renderer needs enabling with its new track selection.
+ boolean playing = playWhenReady && playbackInfo.playbackState == Player.STATE_READY;
+ // Consider as joining only if the renderer was previously disabled.
+ boolean joining = !wasRendererEnabled && playing;
+ // Enable the renderer.
+ renderer.enable(
+ rendererConfiguration,
+ formats,
+ playingPeriodHolder.sampleStreams[rendererIndex],
+ rendererPositionUs,
+ joining,
+ playingPeriodHolder.getRendererOffset());
+ mediaClock.onRendererEnabled(renderer);
+ // Start the renderer if playing.
+ if (playing) {
+ renderer.start();
+ }
+ }
+ }
+
+ private void handleLoadingMediaPeriodChanged(boolean loadingTrackSelectionChanged) {
+ MediaPeriodHolder loadingMediaPeriodHolder = queue.getLoadingPeriod();
+ MediaPeriodId loadingMediaPeriodId =
+ loadingMediaPeriodHolder == null ? playbackInfo.periodId : loadingMediaPeriodHolder.info.id;
+ boolean loadingMediaPeriodChanged =
+ !playbackInfo.loadingMediaPeriodId.equals(loadingMediaPeriodId);
+ if (loadingMediaPeriodChanged) {
+ playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(loadingMediaPeriodId);
+ }
+ playbackInfo.bufferedPositionUs =
+ loadingMediaPeriodHolder == null
+ ? playbackInfo.positionUs
+ : loadingMediaPeriodHolder.getBufferedPositionUs();
+ playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs();
+ if ((loadingMediaPeriodChanged || loadingTrackSelectionChanged)
+ && loadingMediaPeriodHolder != null
+ && loadingMediaPeriodHolder.prepared) {
+ updateLoadControlTrackSelection(
+ loadingMediaPeriodHolder.getTrackGroups(),
+ loadingMediaPeriodHolder.getTrackSelectorResult());
+ }
+ }
+
+ private long getTotalBufferedDurationUs() {
+ return getTotalBufferedDurationUs(playbackInfo.bufferedPositionUs);
+ }
+
+ private long getTotalBufferedDurationUs(long bufferedPositionInLoadingPeriodUs) {
+ MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
+ if (loadingPeriodHolder == null) {
+ return 0;
+ }
+ long totalBufferedDurationUs =
+ bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);
+ return Math.max(0, totalBufferedDurationUs);
+ }
+
+ private void updateLoadControlTrackSelection(
+ TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) {
+ loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections);
+ }
+
+ private void sendPlaybackParametersChangedInternal(
+ PlaybackParameters playbackParameters, boolean acknowledgeCommand) {
+ handler
+ .obtainMessage(
+ MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL,
+ acknowledgeCommand ? 1 : 0,
+ 0,
+ playbackParameters)
+ .sendToTarget();
+ }
+
+ private static Format[] getFormats(TrackSelection newSelection) {
+ // Build an array of formats contained by the selection.
+ int length = newSelection != null ? newSelection.length() : 0;
+ Format[] formats = new Format[length];
+ for (int i = 0; i < length; i++) {
+ formats[i] = newSelection.getFormat(i);
+ }
+ return formats;
+ }
+
+ private static final class SeekPosition {
+
+ public final Timeline timeline;
+ public final int windowIndex;
+ public final long windowPositionUs;
+
+ public SeekPosition(Timeline timeline, int windowIndex, long windowPositionUs) {
+ this.timeline = timeline;
+ this.windowIndex = windowIndex;
+ this.windowPositionUs = windowPositionUs;
+ }
+ }
+
+ private static final class PendingMessageInfo implements Comparable<PendingMessageInfo> {
+
+ public final PlayerMessage message;
+
+ public int resolvedPeriodIndex;
+ public long resolvedPeriodTimeUs;
+ @Nullable public Object resolvedPeriodUid;
+
+ public PendingMessageInfo(PlayerMessage message) {
+ this.message = message;
+ }
+
+ public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) {
+ resolvedPeriodIndex = periodIndex;
+ resolvedPeriodTimeUs = periodTimeUs;
+ resolvedPeriodUid = periodUid;
+ }
+
+ @Override
+ public int compareTo(PendingMessageInfo other) {
+ if ((resolvedPeriodUid == null) != (other.resolvedPeriodUid == null)) {
+ // PendingMessageInfos with a resolved period position are always smaller.
+ return resolvedPeriodUid != null ? -1 : 1;
+ }
+ if (resolvedPeriodUid == null) {
+ // Don't sort message with unresolved positions.
+ return 0;
+ }
+ // Sort resolved media times by period index and then by period position.
+ int comparePeriodIndex = resolvedPeriodIndex - other.resolvedPeriodIndex;
+ if (comparePeriodIndex != 0) {
+ return comparePeriodIndex;
+ }
+ return Util.compareLong(resolvedPeriodTimeUs, other.resolvedPeriodTimeUs);
+ }
+ }
+
+ private static final class MediaSourceRefreshInfo {
+
+ public final MediaSource source;
+ public final Timeline timeline;
+
+ public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) {
+ this.source = source;
+ this.timeline = timeline;
+ }
+ }
+
+ private static final class PlaybackInfoUpdate {
+
+ private PlaybackInfo lastPlaybackInfo;
+ private int operationAcks;
+ private boolean positionDiscontinuity;
+ private @DiscontinuityReason int discontinuityReason;
+
+ public boolean hasPendingUpdate(PlaybackInfo playbackInfo) {
+ return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity;
+ }
+
+ public void reset(PlaybackInfo playbackInfo) {
+ lastPlaybackInfo = playbackInfo;
+ operationAcks = 0;
+ positionDiscontinuity = false;
+ }
+
+ public void incrementPendingOperationAcks(int operationAcks) {
+ this.operationAcks += operationAcks;
+ }
+
+ public void setPositionDiscontinuity(@DiscontinuityReason int discontinuityReason) {
+ if (positionDiscontinuity
+ && this.discontinuityReason != Player.DISCONTINUITY_REASON_INTERNAL) {
+ // We always prefer non-internal discontinuity reasons. We also assume that we won't report
+ // more than one non-internal discontinuity per message iteration.
+ Assertions.checkArgument(discontinuityReason == Player.DISCONTINUITY_REASON_INTERNAL);
+ return;
+ }
+ positionDiscontinuity = true;
+ this.discontinuityReason = discontinuityReason;
+ }
+ }
+
+}