/* * 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 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 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 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 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 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 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 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 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 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 { 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; } } }