NekoX/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java

2052 lines
81 KiB
Java

/*
* 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 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 com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.TraceUtil;
import 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 nextPendingMessageIndexHint;
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();
}
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) {
try {
mediaSource.releaseSource(/* caller= */ this);
} catch (RuntimeException e) {
// There's nothing we can do.
Log.e(TAG, "Failed to release child source.", e);
}
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);
int nextPendingMessageIndex = Math.min(nextPendingMessageIndexHint, pendingMessages.size());
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;
}
nextPendingMessageIndexHint = nextPendingMessageIndex;
}
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;
}
}
}