mirror of https://github.com/NekoX-Dev/NekoX.git
2052 lines
81 KiB
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;
|
|
}
|
|
}
|
|
|
|
}
|