media/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
olly 8cc7dfda7d Fix threading issues between ExoPlayerImpl/ExoPlayerImplInternal
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=138758739
2016-11-10 21:15:35 +00:00

1516 lines
58 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.Message;
import android.os.Process;
import android.os.SystemClock;
import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
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.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MediaClock;
import com.google.android.exoplayer2.util.PriorityHandlerThread;
import com.google.android.exoplayer2.util.StandaloneMediaClock;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/**
* Implements the internal behavior of {@link ExoPlayerImpl}.
*/
/* package */ final class ExoPlayerImplInternal implements Handler.Callback,
MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener {
/**
* Playback position information which is read on the application's thread by
* {@link ExoPlayerImpl} and read/written internally on the player's thread.
*/
public static final class PlaybackInfo {
public final int periodIndex;
public final long startPositionUs;
public volatile long positionUs;
public volatile long bufferedPositionUs;
public PlaybackInfo(int periodIndex, long startPositionUs) {
this.periodIndex = periodIndex;
this.startPositionUs = startPositionUs;
positionUs = startPositionUs;
bufferedPositionUs = startPositionUs;
}
}
public static final class TrackInfo {
public final TrackGroupArray groups;
public final TrackSelectionArray selections;
public final Object info;
public TrackInfo(TrackGroupArray groups, TrackSelectionArray selections, Object info) {
this.groups = groups;
this.selections = selections;
this.info = info;
}
}
public static final class SourceInfo {
public final Timeline timeline;
public final Object manifest;
public final PlaybackInfo playbackInfo;
public final int seekAcks;
public SourceInfo(Timeline timeline, Object manifest, PlaybackInfo playbackInfo, int seekAcks) {
this.timeline = timeline;
this.manifest = manifest;
this.playbackInfo = playbackInfo;
this.seekAcks = seekAcks;
}
}
private static final String TAG = "ExoPlayerImplInternal";
// External messages
public static final int MSG_STATE_CHANGED = 1;
public static final int MSG_LOADING_CHANGED = 2;
public static final int MSG_TRACKS_CHANGED = 3;
public static final int MSG_SEEK_ACK = 4;
public static final int MSG_POSITION_DISCONTINUITY = 5;
public static final int MSG_SOURCE_INFO_REFRESHED = 6;
public static final int MSG_ERROR = 7;
// 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_STOP = 4;
private static final int MSG_RELEASE = 5;
private static final int MSG_REFRESH_SOURCE_INFO = 6;
private static final int MSG_PERIOD_PREPARED = 7;
private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 8;
private static final int MSG_TRACK_SELECTION_INVALIDATED = 9;
private static final int MSG_CUSTOM = 10;
private static final int PREPARING_SOURCE_INTERVAL_MS = 10;
private static final int RENDERING_INTERVAL_MS = 10;
private static final int IDLE_INTERVAL_MS = 1000;
/**
* Limits the maximum number of periods to buffer ahead of the current playing period. The
* buffering policy normally prevents buffering too far ahead, but the policy could allow too many
* small periods to be buffered if the period count were not limited.
*/
private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100;
private final Renderer[] renderers;
private final RendererCapabilities[] rendererCapabilities;
private final TrackSelector trackSelector;
private final LoadControl loadControl;
private final StandaloneMediaClock standaloneMediaClock;
private final Handler handler;
private final HandlerThread internalPlaybackThread;
private final Handler eventHandler;
private final Timeline.Window window;
private final Timeline.Period period;
private PlaybackInfo playbackInfo;
private Renderer rendererMediaClockSource;
private MediaClock rendererMediaClock;
private MediaSource mediaSource;
private Renderer[] enabledRenderers;
private boolean released;
private boolean playWhenReady;
private boolean rebuffering;
private boolean isLoading;
private int state;
private int customMessagesSent;
private int customMessagesProcessed;
private long elapsedRealtimeUs;
private int pendingInitialSeekCount;
private SeekPosition pendingSeekPosition;
private long rendererPositionUs;
private boolean isTimelineReady;
private boolean isTimelineEnded;
private int bufferAheadPeriodCount;
private MediaPeriodHolder playingPeriodHolder;
private MediaPeriodHolder readingPeriodHolder;
private MediaPeriodHolder loadingPeriodHolder;
private Timeline timeline;
public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector,
LoadControl loadControl, boolean playWhenReady, Handler eventHandler,
PlaybackInfo playbackInfo) {
this.renderers = renderers;
this.trackSelector = trackSelector;
this.loadControl = loadControl;
this.playWhenReady = playWhenReady;
this.eventHandler = eventHandler;
this.state = ExoPlayer.STATE_IDLE;
this.playbackInfo = playbackInfo;
rendererCapabilities = new RendererCapabilities[renderers.length];
for (int i = 0; i < renderers.length; i++) {
renderers[i].setIndex(i);
rendererCapabilities[i] = renderers[i].getCapabilities();
}
standaloneMediaClock = new StandaloneMediaClock();
enabledRenderers = new Renderer[0];
window = new Timeline.Window();
period = new Timeline.Period();
trackSelector.init(this);
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
// not normally change to this priority" is incorrect.
internalPlaybackThread = new PriorityHandlerThread("ExoPlayerImplInternal:Handler",
Process.THREAD_PRIORITY_AUDIO);
internalPlaybackThread.start();
handler = new Handler(internalPlaybackThread.getLooper(), this);
}
public void prepare(MediaSource mediaSource, boolean resetPosition) {
handler.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, 0, mediaSource)
.sendToTarget();
}
public void setPlayWhenReady(boolean playWhenReady) {
handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 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 stop() {
handler.sendEmptyMessage(MSG_STOP);
}
public void sendMessages(ExoPlayerMessage... messages) {
if (released) {
Log.w(TAG, "Ignoring messages sent after release.");
return;
}
customMessagesSent++;
handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget();
}
public synchronized void blockingSendMessages(ExoPlayerMessage... messages) {
if (released) {
Log.w(TAG, "Ignoring messages sent after release.");
return;
}
int messageNumber = customMessagesSent++;
handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget();
while (customMessagesProcessed <= messageNumber) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public synchronized void release() {
if (released) {
return;
}
handler.sendEmptyMessage(MSG_RELEASE);
while (!released) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
internalPlaybackThread.quit();
}
// MediaSource.Listener implementation.
@Override
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
handler.obtainMessage(MSG_REFRESH_SOURCE_INFO, Pair.create(timeline, manifest)).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);
}
// Handler.Callback implementation.
@SuppressWarnings("unchecked")
@Override
public boolean handleMessage(Message msg) {
try {
switch (msg.what) {
case MSG_PREPARE: {
prepareInternal((MediaSource) msg.obj, msg.arg1 != 0);
return true;
}
case MSG_SET_PLAY_WHEN_READY: {
setPlayWhenReadyInternal(msg.arg1 != 0);
return true;
}
case MSG_DO_SOME_WORK: {
doSomeWork();
return true;
}
case MSG_SEEK_TO: {
seekToInternal((SeekPosition) msg.obj);
return true;
}
case MSG_STOP: {
stopInternal();
return true;
}
case MSG_RELEASE: {
releaseInternal();
return true;
}
case MSG_PERIOD_PREPARED: {
handlePeriodPrepared((MediaPeriod) msg.obj);
return true;
}
case MSG_REFRESH_SOURCE_INFO: {
handleSourceInfoRefreshed((Pair<Timeline, Object>) msg.obj);
return true;
}
case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: {
handleContinueLoadingRequested((MediaPeriod) msg.obj);
return true;
}
case MSG_TRACK_SELECTION_INVALIDATED: {
reselectTracksInternal();
return true;
}
case MSG_CUSTOM: {
sendMessagesInternal((ExoPlayerMessage[]) msg.obj);
return true;
}
default:
return false;
}
} catch (ExoPlaybackException e) {
Log.e(TAG, "Renderer error.", e);
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
stopInternal();
return true;
} catch (IOException e) {
Log.e(TAG, "Source error.", e);
eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget();
stopInternal();
return true;
} catch (RuntimeException e) {
Log.e(TAG, "Internal runtime error.", e);
eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e))
.sendToTarget();
stopInternal();
return true;
}
}
// Private methods.
private void setState(int state) {
if (this.state != state) {
this.state = state;
eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget();
}
}
private void setIsLoading(boolean isLoading) {
if (this.isLoading != isLoading) {
this.isLoading = isLoading;
eventHandler.obtainMessage(MSG_LOADING_CHANGED, isLoading ? 1 : 0, 0).sendToTarget();
}
}
private void prepareInternal(MediaSource mediaSource, boolean resetPosition) {
resetInternal();
loadControl.onPrepared();
if (resetPosition) {
playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
}
this.mediaSource = mediaSource;
mediaSource.prepareSource(this);
setState(ExoPlayer.STATE_BUFFERING);
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
rebuffering = false;
this.playWhenReady = playWhenReady;
if (!playWhenReady) {
stopRenderers();
updatePlaybackPositions();
} else {
if (state == ExoPlayer.STATE_READY) {
startRenderers();
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
} else if (state == ExoPlayer.STATE_BUFFERING) {
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
}
}
private void startRenderers() throws ExoPlaybackException {
rebuffering = false;
standaloneMediaClock.start();
for (Renderer renderer : enabledRenderers) {
renderer.start();
}
}
private void stopRenderers() throws ExoPlaybackException {
standaloneMediaClock.stop();
for (Renderer renderer : enabledRenderers) {
ensureStopped(renderer);
}
}
private void updatePlaybackPositions() throws ExoPlaybackException {
if (playingPeriodHolder == null) {
return;
}
// Update the playback position.
long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity();
if (periodPositionUs != C.TIME_UNSET) {
resetRendererPosition(periodPositionUs);
} else {
if (rendererMediaClockSource != null && !rendererMediaClockSource.isEnded()) {
rendererPositionUs = rendererMediaClock.getPositionUs();
standaloneMediaClock.setPositionUs(rendererPositionUs);
} else {
rendererPositionUs = standaloneMediaClock.getPositionUs();
}
periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs);
}
playbackInfo.positionUs = periodPositionUs;
elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
// Update the buffered position.
long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE
: playingPeriodHolder.mediaPeriod.getBufferedPositionUs();
playbackInfo.bufferedPositionUs = bufferedPositionUs == C.TIME_END_OF_SOURCE
? timeline.getPeriod(playingPeriodHolder.index, period).getDurationUs()
: bufferedPositionUs;
}
private void doSomeWork() throws ExoPlaybackException, IOException {
long operationStartTimeMs = SystemClock.elapsedRealtime();
updatePeriods();
if (playingPeriodHolder == null) {
// We're still waiting for the first period to be prepared.
maybeThrowPeriodPrepareError();
scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS);
return;
}
TraceUtil.beginSection("doSomeWork");
updatePlaybackPositions();
boolean allRenderersEnded = true;
boolean allRenderersReadyOrEnded = true;
for (Renderer renderer : enabledRenderers) {
// 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, elapsedRealtimeUs);
allRenderersEnded = allRenderersEnded && renderer.isEnded();
// Determine whether the renderer is ready (or ended). If it's not, throw an error that's
// preventing the renderer from making progress, if such an error exists.
boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded();
if (!rendererReadyOrEnded) {
renderer.maybeThrowStreamError();
}
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded;
}
if (!allRenderersReadyOrEnded) {
maybeThrowPeriodPrepareError();
}
long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period)
.getDurationUs();
if (allRenderersEnded
&& (playingPeriodDurationUs == C.TIME_UNSET
|| playingPeriodDurationUs <= playbackInfo.positionUs)
&& isTimelineEnded) {
setState(ExoPlayer.STATE_ENDED);
stopRenderers();
} else if (state == ExoPlayer.STATE_BUFFERING) {
if ((enabledRenderers.length > 0
? (allRenderersReadyOrEnded && haveSufficientBuffer(rebuffering)) : isTimelineReady)) {
setState(ExoPlayer.STATE_READY);
if (playWhenReady) {
startRenderers();
}
}
} else if (state == ExoPlayer.STATE_READY
&& (enabledRenderers.length > 0 ? !allRenderersReadyOrEnded : !isTimelineReady)) {
rebuffering = playWhenReady;
setState(ExoPlayer.STATE_BUFFERING);
stopRenderers();
}
if (state == ExoPlayer.STATE_BUFFERING) {
for (Renderer renderer : enabledRenderers) {
renderer.maybeThrowStreamError();
}
}
if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) {
scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS);
} else if (enabledRenderers.length != 0) {
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);
long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs;
long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime();
if (nextOperationDelayMs <= 0) {
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
} else {
handler.sendEmptyMessageDelayed(MSG_DO_SOME_WORK, nextOperationDelayMs);
}
}
private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
if (timeline == null) {
pendingInitialSeekCount++;
pendingSeekPosition = seekPosition;
return;
}
Pair<Integer, Long> periodPosition = resolveSeekPosition(seekPosition);
if (periodPosition == null) {
// TODO: We should probably propagate an error here.
// We failed to resolve the seek position. Stop the player.
stopInternal();
return;
}
int periodIndex = periodPosition.first;
long periodPositionUs = periodPosition.second;
try {
if (periodIndex == playbackInfo.periodIndex
&& ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) {
// Seek position equals the current position. Do nothing.
return;
}
periodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs);
} finally {
playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs);
eventHandler.obtainMessage(MSG_SEEK_ACK, playbackInfo).sendToTarget();
}
}
private long seekToPeriodPosition(int periodIndex, long periodPositionUs)
throws ExoPlaybackException {
stopRenderers();
rebuffering = false;
setState(ExoPlayer.STATE_BUFFERING);
if (readingPeriodHolder != playingPeriodHolder && (periodIndex == playingPeriodHolder.index
|| periodIndex == readingPeriodHolder.index)) {
// Clear the timeline because a renderer is reading ahead to the next period and the seek is
// to either the playing or reading period.
periodIndex = C.INDEX_UNSET;
}
MediaPeriodHolder newPlayingPeriodHolder = null;
if (playingPeriodHolder == null) {
// We're still waiting for the first period to be prepared.
if (loadingPeriodHolder != null) {
loadingPeriodHolder.release();
}
} else {
// Clear the timeline, but keep the requested period if it is already prepared.
MediaPeriodHolder periodHolder = playingPeriodHolder;
while (periodHolder != null) {
if (periodHolder.index == periodIndex && periodHolder.prepared) {
newPlayingPeriodHolder = periodHolder;
} else {
periodHolder.release();
}
periodHolder = periodHolder.next;
}
}
// Disable all the renderers if the period is changing.
if (newPlayingPeriodHolder != playingPeriodHolder) {
for (Renderer renderer : enabledRenderers) {
renderer.disable();
}
enabledRenderers = new Renderer[0];
rendererMediaClock = null;
rendererMediaClockSource = null;
}
// Update loaded periods.
bufferAheadPeriodCount = 0;
if (newPlayingPeriodHolder != null) {
newPlayingPeriodHolder.next = null;
setPlayingPeriodHolder(newPlayingPeriodHolder);
updateTimelineState();
readingPeriodHolder = playingPeriodHolder;
loadingPeriodHolder = playingPeriodHolder;
if (playingPeriodHolder.hasEnabledTracks) {
periodPositionUs = playingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs);
}
resetRendererPosition(periodPositionUs);
maybeContinueLoading();
} else {
playingPeriodHolder = null;
readingPeriodHolder = null;
loadingPeriodHolder = null;
resetRendererPosition(periodPositionUs);
}
updatePlaybackPositions();
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
return periodPositionUs;
}
private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException {
rendererPositionUs = playingPeriodHolder == null ? periodPositionUs
: playingPeriodHolder.toRendererTime(periodPositionUs);
standaloneMediaClock.setPositionUs(rendererPositionUs);
for (Renderer renderer : enabledRenderers) {
renderer.resetPosition(rendererPositionUs);
}
}
private void stopInternal() {
resetInternal();
loadControl.onStopped();
setState(ExoPlayer.STATE_IDLE);
}
private void releaseInternal() {
resetInternal();
loadControl.onReleased();
setState(ExoPlayer.STATE_IDLE);
synchronized (this) {
released = true;
notifyAll();
}
}
private void resetInternal() {
handler.removeMessages(MSG_DO_SOME_WORK);
rebuffering = false;
standaloneMediaClock.stop();
rendererMediaClock = null;
rendererMediaClockSource = null;
for (Renderer renderer : enabledRenderers) {
try {
ensureStopped(renderer);
renderer.disable();
} catch (ExoPlaybackException | RuntimeException e) {
// There's nothing we can do.
Log.e(TAG, "Stop failed.", e);
}
}
enabledRenderers = new Renderer[0];
releasePeriodHoldersFrom(playingPeriodHolder != null ? playingPeriodHolder
: loadingPeriodHolder);
if (mediaSource != null) {
mediaSource.releaseSource();
mediaSource = null;
}
isTimelineReady = false;
isTimelineEnded = false;
playingPeriodHolder = null;
readingPeriodHolder = null;
loadingPeriodHolder = null;
timeline = null;
bufferAheadPeriodCount = 0;
setIsLoading(false);
}
private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException {
try {
for (ExoPlayerMessage message : messages) {
message.target.handleMessage(message.messageType, message.message);
}
if (mediaSource != null) {
// The message may have caused something to change that now requires us to do work.
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
} finally {
synchronized (this) {
customMessagesProcessed++;
notifyAll();
}
}
}
private void ensureStopped(Renderer renderer) throws ExoPlaybackException {
if (renderer.getState() == Renderer.STATE_STARTED) {
renderer.stop();
}
}
private void reselectTracksInternal() throws ExoPlaybackException {
if (playingPeriodHolder == null) {
// We don't have tracks yet, so we don't care.
return;
}
// Reselect tracks on each period in turn, until the selection changes.
MediaPeriodHolder periodHolder = playingPeriodHolder;
boolean selectionsChangedForReadPeriod = true;
while (true) {
if (periodHolder == null || !periodHolder.prepared) {
// The reselection did not change any prepared periods.
return;
}
if (periodHolder.selectTracks()) {
// 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.next;
}
if (selectionsChangedForReadPeriod) {
// Update streams and rebuffer for the new selection, recreating all streams if reading ahead.
boolean recreateStreams = readingPeriodHolder != playingPeriodHolder;
releasePeriodHoldersFrom(playingPeriodHolder.next);
playingPeriodHolder.next = null;
readingPeriodHolder = playingPeriodHolder;
loadingPeriodHolder = playingPeriodHolder;
bufferAheadPeriodCount = 0;
boolean[] streamResetFlags = new boolean[renderers.length];
long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection(
playbackInfo.positionUs, loadControl, recreateStreams, streamResetFlags);
if (periodPositionUs != playbackInfo.positionUs) {
playbackInfo.positionUs = periodPositionUs;
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.
if (renderer == rendererMediaClockSource) {
// The renderer is providing the media clock.
if (sampleStream == null) {
// The renderer won't be re-enabled. Sync standaloneMediaClock so that it can take
// over timing responsibilities.
standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs());
}
rendererMediaClock = null;
rendererMediaClockSource = null;
}
ensureStopped(renderer);
renderer.disable();
} else if (streamResetFlags[i]) {
// The renderer will continue to consume from its current stream, but needs to be reset.
renderer.resetPosition(rendererPositionUs);
}
}
}
eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.getTrackInfo()).sendToTarget();
enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
} else {
// Release and re-prepare/buffer periods after the one whose selection changed.
loadingPeriodHolder = periodHolder;
periodHolder = loadingPeriodHolder.next;
while (periodHolder != null) {
periodHolder.release();
periodHolder = periodHolder.next;
bufferAheadPeriodCount--;
}
loadingPeriodHolder.next = null;
if (loadingPeriodHolder.prepared) {
long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.startPositionUs,
loadingPeriodHolder.toPeriodTime(rendererPositionUs));
loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, loadControl, false);
}
}
maybeContinueLoading();
updatePlaybackPositions();
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
private boolean haveSufficientBuffer(boolean rebuffering) {
if (loadingPeriodHolder == null) {
return false;
}
long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared
? loadingPeriodHolder.startPositionUs
: loadingPeriodHolder.mediaPeriod.getBufferedPositionUs();
if (loadingPeriodBufferedPositionUs == C.TIME_END_OF_SOURCE) {
if (loadingPeriodHolder.isLast) {
return true;
}
loadingPeriodBufferedPositionUs = timeline.getPeriod(loadingPeriodHolder.index, period)
.getDurationUs();
}
return loadControl.shouldStartPlayback(
loadingPeriodBufferedPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs),
rebuffering);
}
private void maybeThrowPeriodPrepareError() throws IOException {
if (loadingPeriodHolder != null && !loadingPeriodHolder.prepared
&& (readingPeriodHolder == null || readingPeriodHolder.next == loadingPeriodHolder)) {
for (Renderer renderer : enabledRenderers) {
if (!renderer.hasReadStreamToEnd()) {
return;
}
}
loadingPeriodHolder.mediaPeriod.maybeThrowPrepareError();
}
}
private void handleSourceInfoRefreshed(Pair<Timeline, Object> timelineAndManifest)
throws ExoPlaybackException, IOException {
Timeline oldTimeline = timeline;
timeline = timelineAndManifest.first;
Object manifest = timelineAndManifest.second;
if (oldTimeline == null) {
if (pendingInitialSeekCount > 0) {
Pair<Integer, Long> periodPosition = resolveSeekPosition(pendingSeekPosition);
if (periodPosition == null) {
// TODO: We should probably propagate an error here.
// We failed to resolve the seek position. Stop the player.
finishSourceInfoRefresh(manifest, false);
stopInternal();
return;
}
playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second);
} else if (playbackInfo.startPositionUs == C.TIME_UNSET) {
Pair<Integer, Long> defaultPosition = getPeriodPosition(0, C.TIME_UNSET);
playbackInfo = new PlaybackInfo(defaultPosition.first, defaultPosition.second);
}
}
// Update the loaded periods to take into account the new timeline.
if (playingPeriodHolder != null) {
int index = timeline.getIndexOfPeriod(playingPeriodHolder.uid);
if (index == C.INDEX_UNSET) {
boolean restarted = attemptRestart(playingPeriodHolder.index, oldTimeline, timeline);
finishSourceInfoRefresh(manifest, true);
if (!restarted) {
// TODO: We should probably propagate an error here.
stopInternal();
}
return;
}
// The playing period is also in the new timeline. Update the index for each loaded period
// until a period is found that does not match the old timeline.
timeline.getPeriod(index, period, true);
playingPeriodHolder.setIndex(timeline, timeline.getWindow(period.windowIndex, window),
index);
MediaPeriodHolder previousPeriodHolder = playingPeriodHolder;
boolean seenReadingPeriod = false;
bufferAheadPeriodCount = 0;
while (previousPeriodHolder.next != null) {
MediaPeriodHolder periodHolder = previousPeriodHolder.next;
index++;
timeline.getPeriod(index, period, true);
if (!periodHolder.uid.equals(period.uid)) {
if (!seenReadingPeriod) {
// Renderers may have read a period that has been removed, so release all loaded periods
// and seek to the current position of the playing period index.
index = playingPeriodHolder.index;
releasePeriodHoldersFrom(playingPeriodHolder);
playingPeriodHolder = null;
readingPeriodHolder = null;
loadingPeriodHolder = null;
long newPositionUs = seekToPeriodPosition(index, playbackInfo.positionUs);
if (newPositionUs != playbackInfo.positionUs) {
playbackInfo = new PlaybackInfo(index, newPositionUs);
}
finishSourceInfoRefresh(manifest, true);
return;
}
// Update the loading period to be the latest period that is still valid.
loadingPeriodHolder = previousPeriodHolder;
loadingPeriodHolder.next = null;
// Release the rest of the timeline.
releasePeriodHoldersFrom(periodHolder);
break;
}
bufferAheadPeriodCount++;
int windowIndex = timeline.getPeriod(index, period).windowIndex;
periodHolder.setIndex(timeline, timeline.getWindow(windowIndex, window), index);
if (periodHolder == readingPeriodHolder) {
seenReadingPeriod = true;
}
previousPeriodHolder = periodHolder;
}
} else if (loadingPeriodHolder != null) {
Object uid = loadingPeriodHolder.uid;
int index = timeline.getIndexOfPeriod(uid);
if (index == C.INDEX_UNSET) {
boolean restarted = attemptRestart(playingPeriodHolder.index, oldTimeline, timeline);
finishSourceInfoRefresh(manifest, true);
if (!restarted) {
// TODO: We should probably propagate an error here.
stopInternal();
}
return;
} else {
int windowIndex = timeline.getPeriod(index, this.period).windowIndex;
loadingPeriodHolder.setIndex(timeline, timeline.getWindow(windowIndex, window),
index);
}
}
if (oldTimeline != null) {
int newPlayingIndex = playingPeriodHolder != null ? playingPeriodHolder.index
: loadingPeriodHolder != null ? loadingPeriodHolder.index : C.INDEX_UNSET;
if (newPlayingIndex != C.INDEX_UNSET
&& newPlayingIndex != playbackInfo.periodIndex) {
playbackInfo = new PlaybackInfo(newPlayingIndex, playbackInfo.positionUs);
updatePlaybackPositions();
}
}
finishSourceInfoRefresh(manifest, true);
}
private boolean attemptRestart(int oldPeriodIndex, Timeline oldTimeline, Timeline newTimeline) {
int newPeriodIndex = resolveSubsequentPeriod(oldPeriodIndex, oldTimeline, newTimeline);
if (newPeriodIndex == C.INDEX_UNSET) {
// We failed to find a replacement period. Stop the player.
return false;
}
// Release all loaded periods.
releasePeriodHoldersFrom(playingPeriodHolder != null ? playingPeriodHolder
: loadingPeriodHolder);
bufferAheadPeriodCount = 0;
playingPeriodHolder = null;
readingPeriodHolder = null;
loadingPeriodHolder = null;
// Find the default initial position in the window and seek to it.
Pair<Integer, Long> defaultPosition = getPeriodPosition(
timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET);
newPeriodIndex = defaultPosition.first;
long newPlayingPositionUs = defaultPosition.second;
playbackInfo = new PlaybackInfo(newPeriodIndex, newPlayingPositionUs);
return true;
}
private void finishSourceInfoRefresh(Object manifest, boolean processedInitialSeeks) {
SourceInfo sourceInfo = new SourceInfo(timeline, manifest, playbackInfo,
processedInitialSeeks ? pendingInitialSeekCount : 0);
eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, sourceInfo).sendToTarget();
if (processedInitialSeeks) {
pendingInitialSeekCount = 0;
pendingSeekPosition = null;
}
}
/**
* Given a period index into an old timeline, finds the first subsequent period that also exists
* in a new timeline. The index of this period in the new timeline is returned.
*
* @param oldPeriodIndex The index of the period in the old timeline.
* @param oldTimeline The old timeline.
* @param newTimeline The new timeline.
* @return The index in the new timeline of the first subsequent period, or {@link C#INDEX_UNSET}
* if no such period was found.
*/
private int resolveSubsequentPeriod(int oldPeriodIndex, Timeline oldTimeline,
Timeline newTimeline) {
int newPeriodIndex = C.INDEX_UNSET;
while (newPeriodIndex == C.INDEX_UNSET && oldPeriodIndex < oldTimeline.getPeriodCount() - 1) {
newPeriodIndex = newTimeline.getIndexOfPeriod(
oldTimeline.getPeriod(++oldPeriodIndex, period, true).uid);
}
return newPeriodIndex;
}
/**
* Converts a {@link SeekPosition} into the corresponding (periodIndex, periodPositionUs) for the
* internal timeline.
*
* @param seekPosition The position to resolve.
* @return The resolved position, or null if resolution was not successful.
*/
private Pair<Integer, Long> resolveSeekPosition(SeekPosition seekPosition) {
Timeline seekTimeline = seekPosition.timeline;
if (seekTimeline == null) {
// The application performed a blind seek without a timeline (most likely based on knowledge
// of what the timeline will be). Use the internal timeline.
seekTimeline = timeline;
Assertions.checkIndex(seekPosition.windowIndex, 0, timeline.getWindowCount());
}
// Map the SeekPosition to a position in the corresponding timeline.
Pair<Integer, Long> periodPosition = getPeriodPosition(seekTimeline, seekPosition.windowIndex,
seekPosition.windowPositionUs);
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(
seekTimeline.getPeriod(periodPosition.first, period, true).uid);
if (periodIndex != C.INDEX_UNSET) {
// We successfully located the period in the internal timeline.
return Pair.create(periodIndex, periodPosition.second);
}
// Try and find a subsequent period from the seek timeline in the internal timeline.
periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline);
if (periodIndex != C.INDEX_UNSET) {
// We found one. Map the SeekPosition onto the corresponding default position.
return getPeriodPosition(timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET);
}
// We didn't find one. Give up.
return null;
}
/**
* Calls {@link #getPeriodPosition(Timeline, int, long)} using the current timeline.
*/
private Pair<Integer, Long> getPeriodPosition(int windowIndex, long windowPositionUs) {
return getPeriodPosition(timeline, windowIndex, windowPositionUs);
}
/**
* Calls {@link #getPeriodPosition(Timeline, int, long, long)} with a zero default position
* projection.
*/
private Pair<Integer, Long> getPeriodPosition(Timeline timeline, int windowIndex,
long windowPositionUs) {
return getPeriodPosition(timeline, windowIndex, windowPositionUs, 0);
}
/**
* Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs).
*
* @param timeline The timeline containing the window.
* @param windowIndex The window index.
* @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default
* start position.
* @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the
* duration into the future by which the window's position should be projected.
* @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs}
* is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's
* position could not be projected by {@code defaultPositionProjectionUs}.
*/
private Pair<Integer, Long> getPeriodPosition(Timeline timeline, int windowIndex,
long windowPositionUs, long defaultPositionProjectionUs) {
timeline.getWindow(windowIndex, window, false, defaultPositionProjectionUs);
if (windowPositionUs == C.TIME_UNSET) {
windowPositionUs = window.getDefaultPositionUs();
if (windowPositionUs == C.TIME_UNSET) {
return null;
}
}
int periodIndex = window.firstPeriodIndex;
long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs;
long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs();
while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs
&& periodIndex < window.lastPeriodIndex) {
periodPositionUs -= periodDurationUs;
periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs();
}
return Pair.create(periodIndex, periodPositionUs);
}
private void updatePeriods() throws ExoPlaybackException, IOException {
if (timeline == null) {
// We're waiting to get information about periods.
mediaSource.maybeThrowSourceInfoRefreshError();
return;
}
if (loadingPeriodHolder == null
|| (loadingPeriodHolder.isFullyBuffered() && !loadingPeriodHolder.isLast
&& bufferAheadPeriodCount < MAXIMUM_BUFFER_AHEAD_PERIODS)) {
// We don't have a loading period or it's fully loaded, so try and create the next one.
int newLoadingPeriodIndex = loadingPeriodHolder == null ? playbackInfo.periodIndex
: loadingPeriodHolder.index + 1;
if (newLoadingPeriodIndex >= timeline.getPeriodCount()) {
// The period is not available yet.
mediaSource.maybeThrowSourceInfoRefreshError();
} else {
int windowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex;
boolean isFirstPeriodInWindow = newLoadingPeriodIndex
== timeline.getWindow(windowIndex, window).firstPeriodIndex;
long periodStartPositionUs;
if (loadingPeriodHolder == null) {
periodStartPositionUs = playbackInfo.startPositionUs;
} else if (!isFirstPeriodInWindow) {
// We're starting to buffer a new period in the current window. Always start from the
// beginning of the period.
periodStartPositionUs = 0;
} else {
// We're starting to buffer a new window. When playback transitions to this window we'll
// want it to be from its default start position. The expected delay until playback
// transitions is equal the duration of media that's currently buffered (assuming no
// interruptions). Hence we project the default start position forward by the duration of
// the buffer, and start buffering from this point.
long defaultPositionProjectionUs = loadingPeriodHolder.rendererPositionOffsetUs
+ timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()
- loadingPeriodHolder.startPositionUs - rendererPositionUs;
Pair<Integer, Long> defaultPosition = getPeriodPosition(timeline, windowIndex,
C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs));
if (defaultPosition == null) {
newLoadingPeriodIndex = C.INDEX_UNSET;
periodStartPositionUs = C.TIME_UNSET;
} else {
newLoadingPeriodIndex = defaultPosition.first;
periodStartPositionUs = defaultPosition.second;
}
}
if (newLoadingPeriodIndex != C.INDEX_UNSET) {
Object newPeriodUid = timeline.getPeriod(newLoadingPeriodIndex, period, true).uid;
MediaPeriod newMediaPeriod = mediaSource.createPeriod(newLoadingPeriodIndex,
loadControl.getAllocator(), periodStartPositionUs);
newMediaPeriod.prepare(this);
MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities,
trackSelector, mediaSource, newMediaPeriod, newPeriodUid, periodStartPositionUs);
timeline.getWindow(windowIndex, window);
newPeriodHolder.setIndex(timeline, window, newLoadingPeriodIndex);
if (loadingPeriodHolder != null) {
loadingPeriodHolder.setNext(newPeriodHolder);
newPeriodHolder.rendererPositionOffsetUs = loadingPeriodHolder.rendererPositionOffsetUs
+ timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()
- loadingPeriodHolder.startPositionUs;
} else {
newPeriodHolder.rendererPositionOffsetUs = periodStartPositionUs;
}
bufferAheadPeriodCount++;
loadingPeriodHolder = newPeriodHolder;
setIsLoading(true);
}
}
}
if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) {
setIsLoading(false);
} else if (loadingPeriodHolder != null && loadingPeriodHolder.needsContinueLoading) {
maybeContinueLoading();
}
if (playingPeriodHolder == null) {
// We're waiting for the first period to be prepared.
return;
}
// Update the playing and reading periods.
while (playingPeriodHolder != readingPeriodHolder && playingPeriodHolder.next != null
&& rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) {
// All enabled renderers' streams have been read to the end, and the playback position reached
// the end of the playing period, so advance playback to the next period.
playingPeriodHolder.release();
setPlayingPeriodHolder(playingPeriodHolder.next);
bufferAheadPeriodCount--;
playbackInfo = new PlaybackInfo(playingPeriodHolder.index,
playingPeriodHolder.startPositionUs);
updatePlaybackPositions();
eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget();
}
updateTimelineState();
if (readingPeriodHolder.isLast) {
// The renderers have their final SampleStreams.
for (Renderer renderer : enabledRenderers) {
renderer.setCurrentStreamIsFinal();
}
return;
}
for (Renderer renderer : enabledRenderers) {
if (!renderer.hasReadStreamToEnd()) {
return;
}
}
if (readingPeriodHolder.next != null && readingPeriodHolder.next.prepared) {
TrackSelectionArray oldTrackSelections = readingPeriodHolder.trackSelections;
readingPeriodHolder = readingPeriodHolder.next;
TrackSelectionArray newTrackSelections = readingPeriodHolder.trackSelections;
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
TrackSelection oldSelection = oldTrackSelections.get(i);
TrackSelection newSelection = newTrackSelections.get(i);
if (oldSelection != null) {
if (newSelection != null) {
// Replace the renderer's SampleStream so the transition to playing the next period can
// be seamless.
Format[] formats = new Format[newSelection.length()];
for (int j = 0; j < formats.length; j++) {
formats[j] = newSelection.getFormat(j);
}
renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i],
readingPeriodHolder.getRendererOffset());
} else {
// The renderer will be disabled when transitioning to playing the next period. Mark the
// SampleStream as final to play out any remaining data.
renderer.setCurrentStreamIsFinal();
}
}
}
}
}
private void handlePeriodPrepared(MediaPeriod period) throws ExoPlaybackException {
if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) {
// Stale event.
return;
}
loadingPeriodHolder.handlePrepared(loadingPeriodHolder.startPositionUs, loadControl);
if (playingPeriodHolder == null) {
// This is the first prepared period, so start playing it.
readingPeriodHolder = loadingPeriodHolder;
setPlayingPeriodHolder(readingPeriodHolder);
if (playbackInfo.startPositionUs == C.TIME_UNSET) {
// Update the playback info when seeking to a default position.
playbackInfo = new PlaybackInfo(playingPeriodHolder.index,
playingPeriodHolder.startPositionUs);
updatePlaybackPositions();
eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget();
}
updateTimelineState();
}
maybeContinueLoading();
}
private void handleContinueLoadingRequested(MediaPeriod period) {
if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) {
return;
}
maybeContinueLoading();
}
private void maybeContinueLoading() {
long nextLoadPositionUs = loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs();
if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
setIsLoading(false);
} else {
long loadingPeriodPositionUs = loadingPeriodHolder.toPeriodTime(rendererPositionUs);
long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs;
boolean continueLoading = loadControl.shouldContinueLoading(bufferedDurationUs);
setIsLoading(continueLoading);
if (continueLoading) {
loadingPeriodHolder.needsContinueLoading = false;
loadingPeriodHolder.mediaPeriod.continueLoading(loadingPeriodPositionUs);
} else {
loadingPeriodHolder.needsContinueLoading = true;
}
}
}
private void releasePeriodHoldersFrom(MediaPeriodHolder periodHolder) {
while (periodHolder != null) {
periodHolder.release();
periodHolder = periodHolder.next;
}
}
private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) throws ExoPlaybackException {
boolean isFirstPeriod = playingPeriodHolder == null;
playingPeriodHolder = periodHolder;
if (isFirstPeriod) {
resetRendererPosition(playingPeriodHolder.startPositionUs);
}
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;
TrackSelection newSelection = periodHolder.trackSelections.get(i);
if (newSelection != null) {
// The renderer should be enabled when playing the new period.
enabledRendererCount++;
} else if (rendererWasEnabledFlags[i]) {
// The renderer should be disabled when playing the new period.
if (renderer == rendererMediaClockSource) {
// Sync standaloneMediaClock so that it can take over timing responsibilities.
standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs());
rendererMediaClock = null;
rendererMediaClockSource = null;
}
ensureStopped(renderer);
renderer.disable();
}
}
eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.getTrackInfo()).sendToTarget();
enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
}
private void updateTimelineState() {
long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period)
.getDurationUs();
isTimelineReady = playingPeriodDurationUs == C.TIME_UNSET
|| playbackInfo.positionUs < playingPeriodDurationUs
|| (playingPeriodHolder.next != null && playingPeriodHolder.next.prepared);
isTimelineEnded = playingPeriodHolder.isLast;
}
private void enableRenderers(boolean[] rendererWasEnabledFlags, int enabledRendererCount)
throws ExoPlaybackException {
enabledRenderers = new Renderer[enabledRendererCount];
enabledRendererCount = 0;
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
TrackSelection newSelection = playingPeriodHolder.trackSelections.get(i);
if (newSelection != null) {
enabledRenderers[enabledRendererCount++] = renderer;
if (renderer.getState() == Renderer.STATE_DISABLED) {
// The renderer needs enabling with its new track selection.
boolean playing = playWhenReady && state == ExoPlayer.STATE_READY;
// Consider as joining only if the renderer was previously disabled.
boolean joining = !rendererWasEnabledFlags[i] && playing;
// Build an array of formats contained by the selection.
Format[] formats = new Format[newSelection.length()];
for (int j = 0; j < formats.length; j++) {
formats[j] = newSelection.getFormat(j);
}
// Enable the renderer.
renderer.enable(formats, playingPeriodHolder.sampleStreams[i], rendererPositionUs,
joining, playingPeriodHolder.getRendererOffset());
MediaClock mediaClock = renderer.getMediaClock();
if (mediaClock != null) {
if (rendererMediaClock != null) {
throw ExoPlaybackException.createForUnexpected(
new IllegalStateException("Multiple renderer media clocks enabled."));
}
rendererMediaClock = mediaClock;
rendererMediaClockSource = renderer;
}
// Start the renderer if playing.
if (playing) {
renderer.start();
}
}
}
}
}
/**
* Holds a {@link MediaPeriod} with information required to play it as part of a timeline.
*/
private static final class MediaPeriodHolder {
public final MediaPeriod mediaPeriod;
public final Object uid;
public final SampleStream[] sampleStreams;
public final boolean[] mayRetainStreamFlags;
public int index;
public long startPositionUs;
public boolean isLast;
public boolean prepared;
public boolean hasEnabledTracks;
public long rendererPositionOffsetUs;
public MediaPeriodHolder next;
public boolean needsContinueLoading;
private final Renderer[] renderers;
private final RendererCapabilities[] rendererCapabilities;
private final TrackSelector trackSelector;
private final MediaSource mediaSource;
private Object trackSelectionsInfo;
private TrackGroupArray trackGroups;
private TrackSelectionArray trackSelections;
private TrackSelectionArray periodTrackSelections;
public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities,
TrackSelector trackSelector, MediaSource mediaSource, MediaPeriod mediaPeriod,
Object uid, long positionUs) {
this.renderers = renderers;
this.rendererCapabilities = rendererCapabilities;
this.trackSelector = trackSelector;
this.mediaSource = mediaSource;
this.mediaPeriod = mediaPeriod;
this.uid = Assertions.checkNotNull(uid);
sampleStreams = new SampleStream[renderers.length];
mayRetainStreamFlags = new boolean[renderers.length];
startPositionUs = positionUs;
}
public long toRendererTime(long periodTimeUs) {
return periodTimeUs + getRendererOffset();
}
public long toPeriodTime(long rendererTimeUs) {
return rendererTimeUs - getRendererOffset();
}
public long getRendererOffset() {
return rendererPositionOffsetUs - startPositionUs;
}
public void setNext(MediaPeriodHolder next) {
this.next = next;
}
public void setIndex(Timeline timeline, Timeline.Window window, int periodIndex) {
this.index = periodIndex;
isLast = index == timeline.getPeriodCount() - 1 && !window.isDynamic;
}
public boolean isFullyBuffered() {
return prepared
&& (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE);
}
public void handlePrepared(long positionUs, LoadControl loadControl)
throws ExoPlaybackException {
prepared = true;
trackGroups = mediaPeriod.getTrackGroups();
selectTracks();
startPositionUs = updatePeriodTrackSelection(positionUs, loadControl, false);
}
public boolean selectTracks() throws ExoPlaybackException {
Pair<TrackSelectionArray, Object> selectorResult = trackSelector.selectTracks(
rendererCapabilities, trackGroups);
TrackSelectionArray newTrackSelections = selectorResult.first;
if (newTrackSelections.equals(periodTrackSelections)) {
return false;
}
trackSelections = newTrackSelections;
trackSelectionsInfo = selectorResult.second;
return true;
}
public long updatePeriodTrackSelection(long positionUs, LoadControl loadControl,
boolean forceRecreateStreams) throws ExoPlaybackException {
return updatePeriodTrackSelection(positionUs, loadControl, forceRecreateStreams,
new boolean[renderers.length]);
}
public long updatePeriodTrackSelection(long positionUs, LoadControl loadControl,
boolean forceRecreateStreams, boolean[] streamResetFlags) {
for (int i = 0; i < trackSelections.length; i++) {
mayRetainStreamFlags[i] = !forceRecreateStreams
&& Util.areEqual(periodTrackSelections == null ? null : periodTrackSelections.get(i),
trackSelections.get(i));
}
// Disable streams on the period and get new streams for updated/newly-enabled tracks.
positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags,
sampleStreams, streamResetFlags, positionUs);
periodTrackSelections = trackSelections;
// Update whether we have enabled tracks and sanity check the expected streams are non-null.
hasEnabledTracks = false;
for (int i = 0; i < sampleStreams.length; i++) {
if (sampleStreams[i] != null) {
Assertions.checkState(trackSelections.get(i) != null);
hasEnabledTracks = true;
} else {
Assertions.checkState(trackSelections.get(i) == null);
}
}
// The track selection has changed.
loadControl.onTracksSelected(renderers, trackGroups, trackSelections);
return positionUs;
}
public TrackInfo getTrackInfo() {
return new TrackInfo(trackGroups, trackSelections, trackSelectionsInfo);
}
public void release() {
try {
mediaSource.releasePeriod(mediaPeriod);
} catch (RuntimeException e) {
// There's nothing we can do.
Log.e(TAG, "Period release failed.", e);
}
}
}
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;
}
}
}