diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
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.java | 2045 |
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; + } + } + +} |