mirror of
https://github.com/samsonjs/media.git
synced 2026-03-30 10:15:48 +00:00
Seeks are permitted when there's no timeline, and in this case playback necessarily fails on the playback thread when the timeline becomes known. Pushing failure to the same place when the timeline is known means there's only one failure path for this case. A later CL will likely introduce an InvalidSeekPositionException for this case. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=141037708
1482 lines
56 KiB
Java
1482 lines
56 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 PlaybackInfo copyWithPeriodIndex(int periodIndex) {
|
|
PlaybackInfo playbackInfo = new PlaybackInfo(periodIndex, startPositionUs);
|
|
playbackInfo.positionUs = positionUs;
|
|
playbackInfo.bufferedPositionUs = bufferedPositionUs;
|
|
return playbackInfo;
|
|
}
|
|
|
|
}
|
|
|
|
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 ExoPlayer player;
|
|
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 MediaPeriodHolder loadingPeriodHolder;
|
|
private MediaPeriodHolder readingPeriodHolder;
|
|
private MediaPeriodHolder playingPeriodHolder;
|
|
|
|
private Timeline timeline;
|
|
|
|
public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector,
|
|
LoadControl loadControl, boolean playWhenReady, Handler eventHandler,
|
|
PlaybackInfo playbackInfo, ExoPlayer player) {
|
|
this.renderers = renderers;
|
|
this.trackSelector = trackSelector;
|
|
this.loadControl = loadControl;
|
|
this.playWhenReady = playWhenReady;
|
|
this.eventHandler = eventHandler;
|
|
this.state = ExoPlayer.STATE_IDLE;
|
|
this.playbackInfo = playbackInfo;
|
|
this.player = player;
|
|
|
|
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(player, true, 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)
|
|
&& playingPeriodHolder.isLast) {
|
|
setState(ExoPlayer.STATE_ENDED);
|
|
stopRenderers();
|
|
} else if (state == ExoPlayer.STATE_BUFFERING) {
|
|
boolean isNewlyReady = enabledRenderers.length > 0
|
|
? (allRenderersReadyOrEnded && haveSufficientBuffer(rebuffering))
|
|
: isTimelineReady(playingPeriodDurationUs);
|
|
if (isNewlyReady) {
|
|
setState(ExoPlayer.STATE_READY);
|
|
if (playWhenReady) {
|
|
startRenderers();
|
|
}
|
|
}
|
|
} else if (state == ExoPlayer.STATE_READY) {
|
|
boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded
|
|
: isTimelineReady(playingPeriodDurationUs);
|
|
if (!isStillReady) {
|
|
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);
|
|
|
|
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 being played is changing, or if the renderers are
|
|
// reading from a period other than the one being played.
|
|
if (playingPeriodHolder != newPlayingPeriodHolder
|
|
|| playingPeriodHolder != readingPeriodHolder) {
|
|
for (Renderer renderer : enabledRenderers) {
|
|
renderer.disable();
|
|
}
|
|
enabledRenderers = new Renderer[0];
|
|
rendererMediaClock = null;
|
|
rendererMediaClockSource = null;
|
|
}
|
|
|
|
// Update the holders.
|
|
if (newPlayingPeriodHolder != null) {
|
|
newPlayingPeriodHolder.next = null;
|
|
loadingPeriodHolder = newPlayingPeriodHolder;
|
|
readingPeriodHolder = newPlayingPeriodHolder;
|
|
setPlayingPeriodHolder(newPlayingPeriodHolder);
|
|
if (playingPeriodHolder.hasEnabledTracks) {
|
|
periodPositionUs = playingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs);
|
|
}
|
|
resetRendererPosition(periodPositionUs);
|
|
maybeContinueLoading();
|
|
} else {
|
|
loadingPeriodHolder = null;
|
|
readingPeriodHolder = null;
|
|
playingPeriodHolder = null;
|
|
resetRendererPosition(periodPositionUs);
|
|
}
|
|
|
|
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;
|
|
}
|
|
loadingPeriodHolder = null;
|
|
readingPeriodHolder = null;
|
|
playingPeriodHolder = null;
|
|
timeline = null;
|
|
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;
|
|
loadingPeriodHolder = playingPeriodHolder;
|
|
readingPeriodHolder = playingPeriodHolder;
|
|
|
|
boolean[] streamResetFlags = new boolean[renderers.length];
|
|
long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection(
|
|
playbackInfo.positionUs, 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;
|
|
}
|
|
loadingPeriodHolder.next = null;
|
|
if (loadingPeriodHolder.prepared) {
|
|
long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.startPositionUs,
|
|
loadingPeriodHolder.toPeriodTime(rendererPositionUs));
|
|
loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false);
|
|
}
|
|
}
|
|
maybeContinueLoading();
|
|
updatePlaybackPositions();
|
|
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
|
}
|
|
|
|
private boolean isTimelineReady(long playingPeriodDurationUs) {
|
|
return playingPeriodDurationUs == C.TIME_UNSET
|
|
|| playbackInfo.positionUs < playingPeriodDurationUs
|
|
|| (playingPeriodHolder.next != null && playingPeriodHolder.next.prepared);
|
|
}
|
|
|
|
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 {
|
|
Timeline oldTimeline = timeline;
|
|
timeline = timelineAndManifest.first;
|
|
Object manifest = timelineAndManifest.second;
|
|
|
|
int processedInitialSeekCount = 0;
|
|
if (oldTimeline == null) {
|
|
if (pendingInitialSeekCount > 0) {
|
|
Pair<Integer, Long> periodPosition = resolveSeekPosition(pendingSeekPosition);
|
|
if (periodPosition == null) {
|
|
// We failed to resolve the seek position. Stop the player.
|
|
notifySourceInfoRefresh(manifest, 0);
|
|
// TODO: We should probably propagate an error here.
|
|
stopInternal();
|
|
return;
|
|
}
|
|
playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second);
|
|
processedInitialSeekCount = pendingInitialSeekCount;
|
|
pendingInitialSeekCount = 0;
|
|
pendingSeekPosition = null;
|
|
} else if (playbackInfo.startPositionUs == C.TIME_UNSET) {
|
|
Pair<Integer, Long> defaultPosition = getPeriodPosition(0, C.TIME_UNSET);
|
|
playbackInfo = new PlaybackInfo(defaultPosition.first, defaultPosition.second);
|
|
}
|
|
}
|
|
|
|
MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder
|
|
: loadingPeriodHolder;
|
|
if (periodHolder == null) {
|
|
// We don't have any period holders, so we're done.
|
|
notifySourceInfoRefresh(manifest, processedInitialSeekCount);
|
|
return;
|
|
}
|
|
|
|
int periodIndex = timeline.getIndexOfPeriod(periodHolder.uid);
|
|
if (periodIndex == C.INDEX_UNSET) {
|
|
// We didn't find the current period in the new timeline. Attempt to resolve a subsequent
|
|
// period whose window we can restart from.
|
|
int newPeriodIndex = resolveSubsequentPeriod(periodHolder.index, oldTimeline, timeline);
|
|
if (newPeriodIndex == C.INDEX_UNSET) {
|
|
// We failed to resolve a subsequent period. Stop the player.
|
|
notifySourceInfoRefresh(manifest, processedInitialSeekCount);
|
|
// TODO: We should probably propagate an error here.
|
|
stopInternal();
|
|
return;
|
|
}
|
|
// We resolved a subsequent period. Seek to the default position in the corresponding window.
|
|
Pair<Integer, Long> defaultPosition = getPeriodPosition(
|
|
timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET);
|
|
newPeriodIndex = defaultPosition.first;
|
|
long newPositionUs = defaultPosition.second;
|
|
timeline.getPeriod(newPeriodIndex, period, true);
|
|
// Clear the index of each holder that doesn't contain the default position. If a holder
|
|
// contains the default position then update its index so it can be re-used when seeking.
|
|
Object newPeriodUid = period.uid;
|
|
periodHolder.index = C.INDEX_UNSET;
|
|
while (periodHolder.next != null) {
|
|
periodHolder = periodHolder.next;
|
|
periodHolder.index = periodHolder.uid.equals(newPeriodUid) ? newPeriodIndex : C.INDEX_UNSET;
|
|
}
|
|
// Actually do the seek.
|
|
newPositionUs = seekToPeriodPosition(newPeriodIndex, newPositionUs);
|
|
playbackInfo = new PlaybackInfo(newPeriodIndex, newPositionUs);
|
|
notifySourceInfoRefresh(manifest, processedInitialSeekCount);
|
|
return;
|
|
}
|
|
|
|
// The current period is in the new timeline. Update the holder and playbackInfo.
|
|
timeline.getPeriod(periodIndex, period);
|
|
boolean isLastPeriod = periodIndex == timeline.getPeriodCount() - 1
|
|
&& !timeline.getWindow(period.windowIndex, window).isDynamic;
|
|
periodHolder.setIndex(periodIndex, isLastPeriod);
|
|
boolean seenReadingPeriod = periodHolder == readingPeriodHolder;
|
|
if (periodIndex != playbackInfo.periodIndex) {
|
|
playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex);
|
|
}
|
|
|
|
// If there are subsequent holders, update the index for each of them. If we find a holder
|
|
// that's inconsistent with the new timeline then take appropriate action.
|
|
while (periodHolder.next != null) {
|
|
MediaPeriodHolder previousPeriodHolder = periodHolder;
|
|
periodHolder = periodHolder.next;
|
|
periodIndex++;
|
|
timeline.getPeriod(periodIndex, period, true);
|
|
isLastPeriod = periodIndex == timeline.getPeriodCount() - 1
|
|
&& !timeline.getWindow(period.windowIndex, window).isDynamic;
|
|
if (periodHolder.uid.equals(period.uid)) {
|
|
// The holder is consistent with the new timeline. Update its index and continue.
|
|
periodHolder.setIndex(periodIndex, isLastPeriod);
|
|
seenReadingPeriod |= (periodHolder == readingPeriodHolder);
|
|
} else {
|
|
// The holder is inconsistent with the new timeline.
|
|
if (!seenReadingPeriod) {
|
|
// 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.
|
|
periodIndex = playingPeriodHolder.index;
|
|
long newPositionUs = seekToPeriodPosition(periodIndex, playbackInfo.positionUs);
|
|
playbackInfo = new PlaybackInfo(periodIndex, newPositionUs);
|
|
} else {
|
|
// Update the loading period to be the last period that's still valid, and release all
|
|
// subsequent periods.
|
|
loadingPeriodHolder = previousPeriodHolder;
|
|
loadingPeriodHolder.next = null;
|
|
// Release the rest of the timeline.
|
|
releasePeriodHoldersFrom(periodHolder);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
notifySourceInfoRefresh(manifest, processedInitialSeekCount);
|
|
}
|
|
|
|
private void notifySourceInfoRefresh(Object manifest, int processedInitialSeekCount) {
|
|
eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED,
|
|
new SourceInfo(timeline, manifest, playbackInfo, processedInitialSeekCount)).sendToTarget();
|
|
}
|
|
|
|
/**
|
|
* 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.isEmpty()) {
|
|
// The application performed a blind seek without a non-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<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) {
|
|
Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount());
|
|
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;
|
|
}
|
|
|
|
// Update the loading period if required.
|
|
maybeUpdateLoadingPeriod();
|
|
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
|
|
&& 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);
|
|
playbackInfo = new PlaybackInfo(playingPeriodHolder.index,
|
|
playingPeriodHolder.startPositionUs);
|
|
updatePlaybackPositions();
|
|
eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget();
|
|
}
|
|
|
|
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 maybeUpdateLoadingPeriod() throws IOException {
|
|
int newLoadingPeriodIndex;
|
|
if (loadingPeriodHolder == null) {
|
|
newLoadingPeriodIndex = playbackInfo.periodIndex;
|
|
} else {
|
|
int loadingPeriodIndex = loadingPeriodHolder.index;
|
|
if (loadingPeriodHolder.isLast || !loadingPeriodHolder.isFullyBuffered()
|
|
|| timeline.getPeriod(loadingPeriodIndex, period).getDurationUs() == C.TIME_UNSET) {
|
|
// Either the existing loading period is the last period, or we are not ready to advance to
|
|
// loading the next period because it hasn't been fully buffered or its duration is unknown.
|
|
return;
|
|
}
|
|
if (playingPeriodHolder != null
|
|
&& loadingPeriodIndex - playingPeriodHolder.index == MAXIMUM_BUFFER_AHEAD_PERIODS) {
|
|
// We are already buffering the maximum number of periods ahead.
|
|
return;
|
|
}
|
|
newLoadingPeriodIndex = loadingPeriodHolder.index + 1;
|
|
}
|
|
|
|
if (newLoadingPeriodIndex >= timeline.getPeriodCount()) {
|
|
// The next period is not available yet.
|
|
mediaSource.maybeThrowSourceInfoRefreshError();
|
|
return;
|
|
}
|
|
|
|
long newLoadingPeriodStartPositionUs;
|
|
if (loadingPeriodHolder == null) {
|
|
newLoadingPeriodStartPositionUs = playbackInfo.startPositionUs;
|
|
} else {
|
|
int newLoadingWindowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex;
|
|
if (newLoadingPeriodIndex
|
|
!= timeline.getWindow(newLoadingWindowIndex, window).firstPeriodIndex) {
|
|
// We're starting to buffer a new period in the current window. Always start from the
|
|
// beginning of the period.
|
|
newLoadingPeriodStartPositionUs = 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.getRendererOffset()
|
|
+ timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()
|
|
- rendererPositionUs;
|
|
Pair<Integer, Long> defaultPosition = getPeriodPosition(timeline, newLoadingWindowIndex,
|
|
C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs));
|
|
if (defaultPosition == null) {
|
|
return;
|
|
}
|
|
|
|
newLoadingPeriodIndex = defaultPosition.first;
|
|
newLoadingPeriodStartPositionUs = defaultPosition.second;
|
|
}
|
|
}
|
|
|
|
long rendererPositionOffsetUs = loadingPeriodHolder == null ? newLoadingPeriodStartPositionUs
|
|
: (loadingPeriodHolder.getRendererOffset()
|
|
+ timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs());
|
|
timeline.getPeriod(newLoadingPeriodIndex, period, true);
|
|
boolean isLastPeriod = newLoadingPeriodIndex == timeline.getPeriodCount() - 1
|
|
&& !timeline.getWindow(period.windowIndex, window).isDynamic;
|
|
MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities,
|
|
rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid,
|
|
newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs);
|
|
if (loadingPeriodHolder != null) {
|
|
loadingPeriodHolder.next = newPeriodHolder;
|
|
}
|
|
loadingPeriodHolder = newPeriodHolder;
|
|
loadingPeriodHolder.mediaPeriod.prepare(this);
|
|
setIsLoading(true);
|
|
}
|
|
|
|
private void handlePeriodPrepared(MediaPeriod period) throws ExoPlaybackException {
|
|
if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) {
|
|
// Stale event.
|
|
return;
|
|
}
|
|
loadingPeriodHolder.handlePrepared();
|
|
if (playingPeriodHolder == null) {
|
|
// This is the first prepared period, so start playing it.
|
|
readingPeriodHolder = loadingPeriodHolder;
|
|
resetRendererPosition(readingPeriodHolder.startPositionUs);
|
|
setPlayingPeriodHolder(readingPeriodHolder);
|
|
}
|
|
maybeContinueLoading();
|
|
}
|
|
|
|
private void handleContinueLoadingRequested(MediaPeriod period) {
|
|
if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) {
|
|
// Stale event.
|
|
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 {
|
|
playingPeriodHolder = periodHolder;
|
|
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 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 final long rendererPositionOffsetUs;
|
|
|
|
public int index;
|
|
public long startPositionUs;
|
|
public boolean isLast;
|
|
public boolean prepared;
|
|
public boolean hasEnabledTracks;
|
|
public MediaPeriodHolder next;
|
|
public boolean needsContinueLoading;
|
|
|
|
private final Renderer[] renderers;
|
|
private final RendererCapabilities[] rendererCapabilities;
|
|
private final TrackSelector trackSelector;
|
|
private final LoadControl loadControl;
|
|
private final MediaSource mediaSource;
|
|
|
|
private Object trackSelectionsInfo;
|
|
private TrackGroupArray trackGroups;
|
|
private TrackSelectionArray trackSelections;
|
|
private TrackSelectionArray periodTrackSelections;
|
|
|
|
public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities,
|
|
long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl,
|
|
MediaSource mediaSource, Object periodUid, int periodIndex, boolean isLastPeriod,
|
|
long startPositionUs) {
|
|
this.renderers = renderers;
|
|
this.rendererCapabilities = rendererCapabilities;
|
|
this.rendererPositionOffsetUs = rendererPositionOffsetUs;
|
|
this.trackSelector = trackSelector;
|
|
this.loadControl = loadControl;
|
|
this.mediaSource = mediaSource;
|
|
this.uid = Assertions.checkNotNull(periodUid);
|
|
this.index = periodIndex;
|
|
this.isLast = isLastPeriod;
|
|
this.startPositionUs = startPositionUs;
|
|
sampleStreams = new SampleStream[renderers.length];
|
|
mayRetainStreamFlags = new boolean[renderers.length];
|
|
mediaPeriod = mediaSource.createPeriod(periodIndex, loadControl.getAllocator(),
|
|
startPositionUs);
|
|
}
|
|
|
|
public long toRendererTime(long periodTimeUs) {
|
|
return periodTimeUs + getRendererOffset();
|
|
}
|
|
|
|
public long toPeriodTime(long rendererTimeUs) {
|
|
return rendererTimeUs - getRendererOffset();
|
|
}
|
|
|
|
public long getRendererOffset() {
|
|
return rendererPositionOffsetUs - startPositionUs;
|
|
}
|
|
|
|
public void setIndex(int index, boolean isLast) {
|
|
this.index = index;
|
|
this.isLast = isLast;
|
|
}
|
|
|
|
public boolean isFullyBuffered() {
|
|
return prepared
|
|
&& (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE);
|
|
}
|
|
|
|
public void handlePrepared() throws ExoPlaybackException {
|
|
prepared = true;
|
|
trackGroups = mediaPeriod.getTrackGroups();
|
|
selectTracks();
|
|
startPositionUs = updatePeriodTrackSelection(startPositionUs, 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, boolean forceRecreateStreams) {
|
|
return updatePeriodTrackSelection(positionUs, forceRecreateStreams,
|
|
new boolean[renderers.length]);
|
|
}
|
|
|
|
public long updatePeriodTrackSelection(long positionUs, 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;
|
|
}
|
|
|
|
}
|
|
|
|
}
|