mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Play a sequence of sources (playlists #3).
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=124713796
This commit is contained in:
parent
df4e4a72fa
commit
7ae9bf409b
4 changed files with 624 additions and 198 deletions
|
|
@ -144,23 +144,6 @@ public class UtilTest extends TestCase {
|
||||||
assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z"));
|
assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testLongSplitting() {
|
|
||||||
assertLongSplittingForValue(Long.MIN_VALUE);
|
|
||||||
assertLongSplittingForValue(Long.MIN_VALUE + 1);
|
|
||||||
assertLongSplittingForValue(-1);
|
|
||||||
assertLongSplittingForValue(0);
|
|
||||||
assertLongSplittingForValue(1);
|
|
||||||
assertLongSplittingForValue(Long.MAX_VALUE - 1);
|
|
||||||
assertLongSplittingForValue(Long.MAX_VALUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void assertLongSplittingForValue(long value) {
|
|
||||||
int topBits = Util.getTopInt(value);
|
|
||||||
int bottomBots = Util.getBottomInt(value);
|
|
||||||
long reconstructedValue = Util.getLong(topBits, bottomBots);
|
|
||||||
assertEquals(value, reconstructedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testUnescapeInvalidFileName() {
|
public void testUnescapeInvalidFileName() {
|
||||||
assertNull(Util.unescapeFileName("%a"));
|
assertNull(Util.unescapeFileName("%a"));
|
||||||
assertNull(Util.unescapeFileName("%xyz"));
|
assertNull(Util.unescapeFileName("%xyz"));
|
||||||
|
|
|
||||||
|
|
@ -70,13 +70,13 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
private final HandlerThread internalPlaybackThread;
|
private final HandlerThread internalPlaybackThread;
|
||||||
private final Handler eventHandler;
|
private final Handler eventHandler;
|
||||||
private final AtomicInteger pendingSeekCount;
|
private final AtomicInteger pendingSeekCount;
|
||||||
|
private final Timeline timeline;
|
||||||
|
|
||||||
private TrackSelectionArray trackSelections;
|
|
||||||
private TrackRenderer rendererMediaClockSource;
|
private TrackRenderer rendererMediaClockSource;
|
||||||
private MediaClock rendererMediaClock;
|
private MediaClock rendererMediaClock;
|
||||||
private SampleSource source;
|
private SampleSourceProvider sampleSourceProvider;
|
||||||
|
private SampleSource sampleSource;
|
||||||
private TrackRenderer[] enabledRenderers;
|
private TrackRenderer[] enabledRenderers;
|
||||||
private boolean preparedSource;
|
|
||||||
private boolean released;
|
private boolean released;
|
||||||
private boolean playWhenReady;
|
private boolean playWhenReady;
|
||||||
private boolean rebuffering;
|
private boolean rebuffering;
|
||||||
|
|
@ -84,8 +84,13 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
private int customMessagesSent;
|
private int customMessagesSent;
|
||||||
private int customMessagesProcessed;
|
private int customMessagesProcessed;
|
||||||
private long lastSeekPositionMs;
|
private long lastSeekPositionMs;
|
||||||
|
private int lastSeekSourceIndex;
|
||||||
private long elapsedRealtimeUs;
|
private long elapsedRealtimeUs;
|
||||||
|
|
||||||
|
private long sourceOffsetUs;
|
||||||
|
private long internalPositionUs;
|
||||||
|
private int sourceIndex;
|
||||||
|
|
||||||
private volatile long durationUs;
|
private volatile long durationUs;
|
||||||
private volatile long positionUs;
|
private volatile long positionUs;
|
||||||
private volatile long bufferedPositionUs;
|
private volatile long bufferedPositionUs;
|
||||||
|
|
@ -109,6 +114,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
standaloneMediaClock = new StandaloneMediaClock();
|
standaloneMediaClock = new StandaloneMediaClock();
|
||||||
pendingSeekCount = new AtomicInteger();
|
pendingSeekCount = new AtomicInteger();
|
||||||
enabledRenderers = new TrackRenderer[0];
|
enabledRenderers = new TrackRenderer[0];
|
||||||
|
timeline = new Timeline();
|
||||||
|
|
||||||
trackSelector.init(this);
|
trackSelector.init(this);
|
||||||
|
|
||||||
|
|
@ -126,8 +132,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
public long getBufferedPosition() {
|
public long getBufferedPosition() {
|
||||||
long bufferedPositionUs = this.bufferedPositionUs;
|
long bufferedPositionUs = this.bufferedPositionUs;
|
||||||
return bufferedPositionUs == C.UNSET_TIME_US ? ExoPlayer.UNKNOWN_TIME
|
return bufferedPositionUs == C.UNSET_TIME_US || bufferedPositionUs == C.END_OF_SOURCE_US
|
||||||
: bufferedPositionUs / 1000;
|
? ExoPlayer.UNKNOWN_TIME : bufferedPositionUs / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getDuration() {
|
public long getDuration() {
|
||||||
|
|
@ -144,10 +150,20 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void seekTo(long positionMs) {
|
public void seekTo(long positionMs) {
|
||||||
|
// TODO[playlists]: Move to ExoPlayerImpl.
|
||||||
|
int sourceIndex;
|
||||||
|
synchronized (timeline) {
|
||||||
|
sourceIndex = this.sourceIndex;
|
||||||
|
}
|
||||||
|
seekTo(sourceIndex, positionMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void seekTo(int sourceIndex, long positionMs) {
|
||||||
|
// TODO[playlists]: Expose the current source index and seeking to sources in ExoPlayer.
|
||||||
|
lastSeekSourceIndex = sourceIndex;
|
||||||
lastSeekPositionMs = positionMs;
|
lastSeekPositionMs = positionMs;
|
||||||
pendingSeekCount.incrementAndGet();
|
pendingSeekCount.incrementAndGet();
|
||||||
handler.obtainMessage(MSG_SEEK_TO, Util.getTopInt(positionMs),
|
handler.obtainMessage(MSG_SEEK_TO, lastSeekSourceIndex, -1, positionMs).sendToTarget();
|
||||||
Util.getBottomInt(positionMs)).sendToTarget();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
|
|
@ -194,11 +210,15 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
internalPlaybackThread.quit();
|
internalPlaybackThread.quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InvalidationListener implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTrackSelectionsInvalidated() {
|
public void onTrackSelectionsInvalidated() {
|
||||||
handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED);
|
handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler.Callback implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handleMessage(Message msg) {
|
public boolean handleMessage(Message msg) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -216,7 +236,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case MSG_SEEK_TO: {
|
case MSG_SEEK_TO: {
|
||||||
seekToInternal(Util.getLong(msg.arg1, msg.arg2));
|
seekToInternal(msg.arg1, (Long) msg.obj);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case MSG_STOP: {
|
case MSG_STOP: {
|
||||||
|
|
@ -257,6 +277,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Private methods.
|
||||||
|
|
||||||
private void setState(int state) {
|
private void setState(int state) {
|
||||||
if (this.state != state) {
|
if (this.state != state) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
|
@ -269,6 +291,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean haveSufficientBuffer() {
|
private boolean haveSufficientBuffer() {
|
||||||
|
// TODO[playlists]: Take into account the buffered position in the timeline.
|
||||||
long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs;
|
long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs;
|
||||||
return minBufferDurationUs <= 0
|
return minBufferDurationUs <= 0
|
||||||
|| bufferedPositionUs == C.UNSET_TIME_US
|
|| bufferedPositionUs == C.UNSET_TIME_US
|
||||||
|
|
@ -279,8 +302,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
private void setSourceProviderInternal(SampleSourceProvider sourceProvider) {
|
private void setSourceProviderInternal(SampleSourceProvider sourceProvider) {
|
||||||
resetInternal();
|
resetInternal();
|
||||||
// TODO[playlists]: Create and use sources after the first one.
|
sampleSourceProvider = sourceProvider;
|
||||||
this.source = sourceProvider.createSource(0);
|
|
||||||
setState(ExoPlayer.STATE_BUFFERING);
|
setState(ExoPlayer.STATE_BUFFERING);
|
||||||
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||||
}
|
}
|
||||||
|
|
@ -322,28 +344,28 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
private void updatePositionUs() {
|
private void updatePositionUs() {
|
||||||
if (rendererMediaClockSource != null && !rendererMediaClockSource.isEnded()) {
|
if (rendererMediaClockSource != null && !rendererMediaClockSource.isEnded()) {
|
||||||
positionUs = rendererMediaClock.getPositionUs();
|
internalPositionUs = rendererMediaClock.getPositionUs();
|
||||||
standaloneMediaClock.setPositionUs(positionUs);
|
standaloneMediaClock.setPositionUs(internalPositionUs);
|
||||||
} else {
|
} else {
|
||||||
positionUs = standaloneMediaClock.getPositionUs();
|
internalPositionUs = standaloneMediaClock.getPositionUs();
|
||||||
}
|
}
|
||||||
|
positionUs = internalPositionUs - sourceOffsetUs;
|
||||||
elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
|
elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateBufferedPositionUs() {
|
private void updateBufferedPositionUs() {
|
||||||
long sourceBufferedPositionUs = enabledRenderers.length > 0 ? source.getBufferedPositionUs()
|
long sourceBufferedPositionUs = enabledRenderers.length > 0
|
||||||
: C.END_OF_SOURCE_US;
|
? sampleSource.getBufferedPositionUs() : C.END_OF_SOURCE_US;
|
||||||
bufferedPositionUs = sourceBufferedPositionUs == C.END_OF_SOURCE_US
|
bufferedPositionUs = sourceBufferedPositionUs == C.END_OF_SOURCE_US
|
||||||
&& durationUs != C.UNSET_TIME_US ? durationUs : sourceBufferedPositionUs;
|
&& durationUs != C.UNSET_TIME_US ? durationUs : sourceBufferedPositionUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doSomeWork() throws ExoPlaybackException, IOException {
|
private void doSomeWork() throws ExoPlaybackException, IOException {
|
||||||
long operationStartTimeMs = SystemClock.elapsedRealtime();
|
long operationStartTimeMs = SystemClock.elapsedRealtime();
|
||||||
if (!preparedSource) {
|
if (sampleSource == null) {
|
||||||
preparedSource = source.prepare(positionUs);
|
timeline.updateSources();
|
||||||
if (preparedSource) {
|
sampleSource = timeline.getSampleSource(internalPositionUs);
|
||||||
durationUs = source.getDurationUs();
|
if (sampleSource != null) {
|
||||||
selectTracksInternal();
|
|
||||||
resumeInternal();
|
resumeInternal();
|
||||||
} else {
|
} else {
|
||||||
// We're still waiting for the source to be prepared.
|
// We're still waiting for the source to be prepared.
|
||||||
|
|
@ -358,9 +380,10 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
// Process reset if there is one, else update the position.
|
// Process reset if there is one, else update the position.
|
||||||
if (!checkForSourceResetInternal()) {
|
if (!checkForSourceResetInternal()) {
|
||||||
updatePositionUs();
|
updatePositionUs();
|
||||||
|
sampleSource = timeline.getSampleSource(internalPositionUs);
|
||||||
}
|
}
|
||||||
updateBufferedPositionUs();
|
updateBufferedPositionUs();
|
||||||
source.continueBuffering(positionUs);
|
timeline.updateSources();
|
||||||
} else {
|
} else {
|
||||||
updatePositionUs();
|
updatePositionUs();
|
||||||
}
|
}
|
||||||
|
|
@ -368,10 +391,10 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
boolean allRenderersEnded = true;
|
boolean allRenderersEnded = true;
|
||||||
boolean allRenderersReadyOrEnded = true;
|
boolean allRenderersReadyOrEnded = true;
|
||||||
for (TrackRenderer renderer : enabledRenderers) {
|
for (TrackRenderer renderer : enabledRenderers) {
|
||||||
// TODO: Each renderer should return the maximum delay before which it wishes to be
|
// TODO: Each renderer should return the maximum delay before which it wishes to be invoked
|
||||||
// invoked again. The minimum of these values should then be used as the delay before the next
|
// again. The minimum of these values should then be used as the delay before the next
|
||||||
// invocation of this method.
|
// invocation of this method.
|
||||||
renderer.render(positionUs, elapsedRealtimeUs);
|
renderer.render(internalPositionUs, elapsedRealtimeUs);
|
||||||
allRenderersEnded = allRenderersEnded && renderer.isEnded();
|
allRenderersEnded = allRenderersEnded && renderer.isEnded();
|
||||||
// Determine whether the renderer is ready (or ended). If it's not, throw an error that's
|
// 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.
|
// preventing the renderer from making progress, if such an error exists.
|
||||||
|
|
@ -386,12 +409,13 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
setState(ExoPlayer.STATE_ENDED);
|
setState(ExoPlayer.STATE_ENDED);
|
||||||
stopRenderers();
|
stopRenderers();
|
||||||
} else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded
|
} else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded
|
||||||
&& haveSufficientBuffer()) {
|
&& haveSufficientBuffer() && timeline.isReady(internalPositionUs)) {
|
||||||
setState(ExoPlayer.STATE_READY);
|
setState(ExoPlayer.STATE_READY);
|
||||||
if (playWhenReady) {
|
if (playWhenReady) {
|
||||||
startRenderers();
|
startRenderers();
|
||||||
}
|
}
|
||||||
} else if (state == ExoPlayer.STATE_READY && !allRenderersReadyOrEnded) {
|
} else if (state == ExoPlayer.STATE_READY && (!allRenderersReadyOrEnded
|
||||||
|
|| !timeline.isReady(internalPositionUs))) {
|
||||||
rebuffering = playWhenReady;
|
rebuffering = playWhenReady;
|
||||||
setState(ExoPlayer.STATE_BUFFERING);
|
setState(ExoPlayer.STATE_BUFFERING);
|
||||||
stopRenderers();
|
stopRenderers();
|
||||||
|
|
@ -418,7 +442,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void seekToInternal(long positionMs) throws ExoPlaybackException {
|
private void seekToInternal(int sourceIndex, long positionMs) throws ExoPlaybackException {
|
||||||
try {
|
try {
|
||||||
if (positionMs == (positionUs / 1000)) {
|
if (positionMs == (positionUs / 1000)) {
|
||||||
// Seek is to the current position. Do nothing.
|
// Seek is to the current position. Do nothing.
|
||||||
|
|
@ -427,9 +451,12 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
rebuffering = false;
|
rebuffering = false;
|
||||||
positionUs = positionMs * 1000;
|
positionUs = positionMs * 1000;
|
||||||
|
internalPositionUs = sourceOffsetUs + positionUs;
|
||||||
standaloneMediaClock.stop();
|
standaloneMediaClock.stop();
|
||||||
standaloneMediaClock.setPositionUs(positionUs);
|
standaloneMediaClock.setPositionUs(internalPositionUs);
|
||||||
if (!preparedSource) {
|
sampleSource = timeline.seekTo(sourceIndex, positionUs);
|
||||||
|
if (sampleSource == null) {
|
||||||
|
// The source isn't prepared.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -437,7 +464,6 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
for (TrackRenderer renderer : enabledRenderers) {
|
for (TrackRenderer renderer : enabledRenderers) {
|
||||||
ensureStopped(renderer);
|
ensureStopped(renderer);
|
||||||
}
|
}
|
||||||
source.seekToUs(positionUs);
|
|
||||||
checkForSourceResetInternal();
|
checkForSourceResetInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -459,8 +485,9 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
if (allRenderersEnded && (durationUs == C.UNSET_TIME_US || durationUs <= positionUs)) {
|
if (allRenderersEnded && (durationUs == C.UNSET_TIME_US || durationUs <= positionUs)) {
|
||||||
setState(ExoPlayer.STATE_ENDED);
|
setState(ExoPlayer.STATE_ENDED);
|
||||||
} else {
|
} else {
|
||||||
setState(allRenderersReadyOrEnded && haveSufficientBuffer() ? ExoPlayer.STATE_READY
|
setState(allRenderersReadyOrEnded && haveSufficientBuffer()
|
||||||
: ExoPlayer.STATE_BUFFERING);
|
&& timeline.isReady(internalPositionUs) ? ExoPlayer.STATE_READY
|
||||||
|
: ExoPlayer.STATE_BUFFERING);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the renderers if ready, and schedule the first piece of work.
|
// Start the renderers if ready, and schedule the first piece of work.
|
||||||
|
|
@ -471,14 +498,14 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkForSourceResetInternal() throws ExoPlaybackException {
|
private boolean checkForSourceResetInternal() throws ExoPlaybackException {
|
||||||
long resetPositionUs = source.readReset();
|
long resetPositionUs = sampleSource.readReset();
|
||||||
if (resetPositionUs == C.UNSET_TIME_US) {
|
if (resetPositionUs == C.UNSET_TIME_US) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
positionUs = resetPositionUs;
|
internalPositionUs = sourceOffsetUs + resetPositionUs;
|
||||||
standaloneMediaClock.setPositionUs(resetPositionUs);
|
standaloneMediaClock.setPositionUs(internalPositionUs);
|
||||||
for (TrackRenderer renderer : enabledRenderers) {
|
for (TrackRenderer renderer : enabledRenderers) {
|
||||||
renderer.reset(resetPositionUs);
|
renderer.reset(internalPositionUs);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -499,9 +526,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
private void resetInternal() {
|
private void resetInternal() {
|
||||||
handler.removeMessages(MSG_DO_SOME_WORK);
|
handler.removeMessages(MSG_DO_SOME_WORK);
|
||||||
preparedSource = false;
|
|
||||||
rebuffering = false;
|
rebuffering = false;
|
||||||
trackSelections = null;
|
|
||||||
standaloneMediaClock.stop();
|
standaloneMediaClock.stop();
|
||||||
rendererMediaClock = null;
|
rendererMediaClock = null;
|
||||||
rendererMediaClockSource = null;
|
rendererMediaClockSource = null;
|
||||||
|
|
@ -517,15 +542,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
Log.e(TAG, "Stop failed.", e);
|
Log.e(TAG, "Stop failed.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (source != null) {
|
sampleSource = null;
|
||||||
try {
|
timeline.reset();
|
||||||
source.release();
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
// There's nothing we can do.
|
|
||||||
Log.e(TAG, "Source release failed.", e);
|
|
||||||
}
|
|
||||||
source = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException {
|
private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException {
|
||||||
|
|
@ -533,7 +551,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
for (ExoPlayerMessage message : messages) {
|
for (ExoPlayerMessage message : messages) {
|
||||||
message.target.handleMessage(message.messageType, message.message);
|
message.target.handleMessage(message.messageType, message.message);
|
||||||
}
|
}
|
||||||
if (preparedSource) {
|
if (sampleSource != null) {
|
||||||
// The message may have caused something to change that now requires us to do work.
|
// The message may have caused something to change that now requires us to do work.
|
||||||
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||||
}
|
}
|
||||||
|
|
@ -545,113 +563,510 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void selectTracksInternal() throws ExoPlaybackException {
|
|
||||||
TrackGroupArray groups = source.getTrackGroups();
|
|
||||||
|
|
||||||
Pair<TrackSelectionArray, Object> result = trackSelector.selectTracks(renderers, groups);
|
|
||||||
TrackSelectionArray newTrackSelections = result.first;
|
|
||||||
|
|
||||||
if (newTrackSelections.equals(trackSelections)) {
|
|
||||||
trackSelector.onSelectionActivated(result.second);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable any renderers whose selections have changed, adding the corresponding TrackStream
|
|
||||||
// instances to oldStreams. Where we need to obtain a new TrackStream instance for a renderer,
|
|
||||||
// we add the corresponding TrackSelection to newSelections.
|
|
||||||
ArrayList<TrackStream> oldStreams = new ArrayList<>();
|
|
||||||
ArrayList<TrackSelection> newSelections = new ArrayList<>();
|
|
||||||
boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
|
|
||||||
int enabledRendererCount = 0;
|
|
||||||
for (int i = 0; i < renderers.length; i++) {
|
|
||||||
TrackRenderer renderer = renderers[i];
|
|
||||||
TrackSelection oldSelection = trackSelections == null ? null : trackSelections.get(i);
|
|
||||||
TrackSelection newSelection = newTrackSelections.get(i);
|
|
||||||
if (newSelection != null) {
|
|
||||||
enabledRendererCount++;
|
|
||||||
}
|
|
||||||
rendererWasEnabledFlags[i] = renderer.getState() != TrackRenderer.STATE_DISABLED;
|
|
||||||
if (!Util.areEqual(oldSelection, newSelection)) {
|
|
||||||
// The track selection has changed for this renderer.
|
|
||||||
if (rendererWasEnabledFlags[i]) {
|
|
||||||
// We need to disable the renderer so that we can enable it with its new selection.
|
|
||||||
if (renderer == rendererMediaClockSource) {
|
|
||||||
// The renderer is providing the media clock.
|
|
||||||
if (newSelection == 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);
|
|
||||||
oldStreams.add(renderer.disable());
|
|
||||||
}
|
|
||||||
if (newSelection != null) {
|
|
||||||
newSelections.add(newSelection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the source selection.
|
|
||||||
TrackStream[] newStreams = source.selectTracks(oldStreams, newSelections, positionUs);
|
|
||||||
trackSelector.onSelectionActivated(result.second);
|
|
||||||
trackSelections = newTrackSelections;
|
|
||||||
|
|
||||||
// Enable renderers with their new selections.
|
|
||||||
enabledRenderers = new TrackRenderer[enabledRendererCount];
|
|
||||||
enabledRendererCount = 0;
|
|
||||||
for (int i = 0; i < renderers.length; i++) {
|
|
||||||
TrackRenderer renderer = renderers[i];
|
|
||||||
TrackSelection newSelection = trackSelections.get(i);
|
|
||||||
if (newSelection != null) {
|
|
||||||
enabledRenderers[enabledRendererCount++] = renderer;
|
|
||||||
if (renderer.getState() == TrackRenderer.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 new selection.
|
|
||||||
Format[] formats = new Format[newSelection.length];
|
|
||||||
for (int j = 0; j < formats.length; j++) {
|
|
||||||
formats[j] = groups.get(newSelection.group).getFormat(newSelection.getTrack(j));
|
|
||||||
}
|
|
||||||
// Enable the renderer.
|
|
||||||
int newStreamIndex = newSelections.indexOf(newSelection);
|
|
||||||
renderer.enable(formats, newStreams[newStreamIndex], positionUs, joining);
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reselectTracksInternal() throws ExoPlaybackException {
|
|
||||||
if (!preparedSource) {
|
|
||||||
// We don't have tracks yet, so we don't care.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectTracksInternal();
|
|
||||||
updateBufferedPositionUs();
|
|
||||||
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ensureStopped(TrackRenderer renderer) throws ExoPlaybackException {
|
private void ensureStopped(TrackRenderer renderer) throws ExoPlaybackException {
|
||||||
if (renderer.getState() == TrackRenderer.STATE_STARTED) {
|
if (renderer.getState() == TrackRenderer.STATE_STARTED) {
|
||||||
renderer.stop();
|
renderer.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void reselectTracksInternal() throws ExoPlaybackException {
|
||||||
|
if (sampleSource == null) {
|
||||||
|
// We don't have tracks yet, so we don't care.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
timeline.reselectTracks();
|
||||||
|
updateBufferedPositionUs();
|
||||||
|
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps track of the {@link Source}s of media being played in the timeline.
|
||||||
|
*/
|
||||||
|
private final class Timeline {
|
||||||
|
|
||||||
|
// Used during track reselection.
|
||||||
|
private final boolean[] rendererWasEnabledFlags;
|
||||||
|
private final ArrayList<TrackStream> oldStreams;
|
||||||
|
private final ArrayList<TrackSelection> newSelections;
|
||||||
|
|
||||||
|
private int nextSourceIndex;
|
||||||
|
|
||||||
|
private Source playingSource;
|
||||||
|
private Source readingSource;
|
||||||
|
private Source bufferingSource;
|
||||||
|
|
||||||
|
private long playingSourceEndPositionUs;
|
||||||
|
private long nextSourceOffsetUs;
|
||||||
|
|
||||||
|
public Timeline() {
|
||||||
|
rendererWasEnabledFlags = new boolean[renderers.length];
|
||||||
|
oldStreams = new ArrayList<>();
|
||||||
|
newSelections = new ArrayList<>();
|
||||||
|
playingSourceEndPositionUs = C.UNSET_TIME_US;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSources() throws ExoPlaybackException, IOException {
|
||||||
|
// TODO[playlists]: Let sample source providers invalidate sources that are already buffering.
|
||||||
|
|
||||||
|
// Continue buffering the first source that is not yet buffered, if any.
|
||||||
|
int sourceCount = sampleSourceProvider.getSourceCount();
|
||||||
|
if (bufferingSource == null || bufferingSource.isFullyBuffered()) {
|
||||||
|
if (sourceCount == SampleSourceProvider.UNKNOWN_SOURCE_COUNT
|
||||||
|
|| nextSourceIndex < sourceCount) {
|
||||||
|
// Create the next source and prepare to buffer it.
|
||||||
|
int index = nextSourceIndex++;
|
||||||
|
SampleSource sampleSource = sampleSourceProvider.createSource(index);
|
||||||
|
if (sampleSource != null) {
|
||||||
|
Source newSource = new Source(sampleSource, index, renderers.length);
|
||||||
|
if (bufferingSource != null) {
|
||||||
|
bufferingSource.nextSource = newSource;
|
||||||
|
}
|
||||||
|
bufferingSource = newSource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferingSource != null) {
|
||||||
|
if (!bufferingSource.prepared) {
|
||||||
|
// Continue preparation.
|
||||||
|
// TODO[playlists]: Add support for setting the start position to play in a source.
|
||||||
|
long startPositionUs = playingSource == null ? positionUs : 0;
|
||||||
|
if (bufferingSource.prepare(startPositionUs)) {
|
||||||
|
Pair<TrackSelectionArray, Object> result = trackSelector.selectTracks(renderers,
|
||||||
|
bufferingSource.sampleSource.getTrackGroups());
|
||||||
|
bufferingSource.selectTracks(result.first, result.second, startPositionUs);
|
||||||
|
if (playingSource == null) {
|
||||||
|
// This is the first prepared source, so start playing it.
|
||||||
|
sourceOffsetUs = 0;
|
||||||
|
setPlayingSource(bufferingSource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferingSource.hasEnabledTracks) {
|
||||||
|
long bufferingPositionUs;
|
||||||
|
if (bufferingSource == playingSource) {
|
||||||
|
bufferingPositionUs = internalPositionUs - sourceOffsetUs;
|
||||||
|
} else if (bufferingSource == readingSource) {
|
||||||
|
// TODO[playlists]: Make sure continueBuffering supports a negative downstream position.
|
||||||
|
bufferingPositionUs = internalPositionUs - nextSourceOffsetUs;
|
||||||
|
} else {
|
||||||
|
bufferingPositionUs = 0;
|
||||||
|
}
|
||||||
|
bufferingSource.sampleSource.continueBuffering(bufferingPositionUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playingSource == null || readingSource != playingSource) {
|
||||||
|
// We are either waiting for preparation to complete, or already reading ahead.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether all enabled renderers have read to the end of their TrackStreams.
|
||||||
|
for (TrackRenderer renderer : enabledRenderers) {
|
||||||
|
if (!renderer.hasReadStreamToEnd()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (playingSourceEndPositionUs == C.UNSET_TIME_US) {
|
||||||
|
// Calculate the next source's start position in the timeline.
|
||||||
|
long playingSourceDurationUs = playingSource.sampleSource.getDurationUs();
|
||||||
|
if (playingSourceDurationUs == C.UNSET_TIME_US) {
|
||||||
|
// The duration of the current source is unknown, so use the maximum rendered timestamp
|
||||||
|
// plus a small extra offset to make sure that renderers don't read two buffers with the
|
||||||
|
// same timestamp.
|
||||||
|
playingSourceEndPositionUs = 0;
|
||||||
|
for (TrackRenderer renderer : enabledRenderers) {
|
||||||
|
playingSourceEndPositionUs =
|
||||||
|
Math.max(playingSourceEndPositionUs, renderer.getMaximumTimeUs());
|
||||||
|
}
|
||||||
|
nextSourceOffsetUs = playingSourceEndPositionUs + 10000;
|
||||||
|
} else {
|
||||||
|
playingSourceEndPositionUs = sourceOffsetUs + playingSourceDurationUs;
|
||||||
|
nextSourceOffsetUs = playingSourceEndPositionUs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sourceCount != SampleSourceProvider.UNKNOWN_SOURCE_COUNT
|
||||||
|
&& readingSource.index == sourceCount - 1) {
|
||||||
|
// This is the last source, so signal the renderers to read the end of the stream.
|
||||||
|
for (TrackRenderer renderer : enabledRenderers) {
|
||||||
|
renderer.setCurrentTrackStreamIsFinal();
|
||||||
|
}
|
||||||
|
readingSource = null;
|
||||||
|
playingSourceEndPositionUs = C.UNSET_TIME_US;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (playingSource.nextSource != null && playingSource.nextSource.prepared) {
|
||||||
|
readingSource = playingSource.nextSource;
|
||||||
|
// Suppress reading a reset so that the transition can be seamless.
|
||||||
|
readingSource.sampleSource.readReset();
|
||||||
|
// Replace enabled renderers' TrackStreams if they will continue to be enabled when the
|
||||||
|
// new source starts playing, so that the transition can be seamless.
|
||||||
|
TrackSelectionArray newTrackSelections = readingSource.trackSelections;
|
||||||
|
TrackGroupArray groups = readingSource.sampleSource.getTrackGroups();
|
||||||
|
for (int i = 0; i < renderers.length; i++) {
|
||||||
|
TrackRenderer renderer = renderers[i];
|
||||||
|
TrackSelection selection = newTrackSelections.get(i);
|
||||||
|
if (selection != null && renderer.getState() != TrackRenderer.STATE_DISABLED) {
|
||||||
|
// The renderer is enabled and will continue to be enabled after the transition.
|
||||||
|
Format[] formats = new Format[selection.length];
|
||||||
|
for (int j = 0; j < formats.length; j++) {
|
||||||
|
formats[j] = groups.get(selection.group).getFormat(selection.getTrack(j));
|
||||||
|
}
|
||||||
|
renderer.replaceTrackStream(formats, readingSource.trackStreams[i], nextSourceOffsetUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReady(long positionUs) {
|
||||||
|
return playingSourceEndPositionUs == C.UNSET_TIME_US
|
||||||
|
|| positionUs < playingSourceEndPositionUs || playingSource.nextSource != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SampleSource getSampleSource(long positionUs) throws ExoPlaybackException {
|
||||||
|
if (playingSource == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (readingSource != playingSource && playingSourceEndPositionUs != C.UNSET_TIME_US
|
||||||
|
&& positionUs >= playingSourceEndPositionUs) {
|
||||||
|
// Renderers are playing the next source, so update the timeline.
|
||||||
|
playingSource.release();
|
||||||
|
sourceOffsetUs = nextSourceOffsetUs;
|
||||||
|
setPlayingSource(readingSource);
|
||||||
|
}
|
||||||
|
return playingSource.sampleSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SampleSource seekTo(int sourceIndex, long sourcePositionUs) throws ExoPlaybackException {
|
||||||
|
// Clear the timeline, but keep the requested source if it is already prepared.
|
||||||
|
Source source = playingSource;
|
||||||
|
Source newPlayingSource = null;
|
||||||
|
while (source != null) {
|
||||||
|
if (source.index == sourceIndex && source.prepared) {
|
||||||
|
newPlayingSource = source;
|
||||||
|
} else {
|
||||||
|
source.release();
|
||||||
|
}
|
||||||
|
source = source.nextSource;
|
||||||
|
}
|
||||||
|
if (newPlayingSource != null) {
|
||||||
|
nextSourceIndex = sourceIndex + 1;
|
||||||
|
newPlayingSource.nextSource = null;
|
||||||
|
setPlayingSource(newPlayingSource);
|
||||||
|
bufferingSource = playingSource;
|
||||||
|
if (playingSource.hasEnabledTracks) {
|
||||||
|
sampleSource.seekToUs(sourcePositionUs);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playingSource = null;
|
||||||
|
readingSource = null;
|
||||||
|
bufferingSource = null;
|
||||||
|
durationUs = C.UNSET_TIME_US;
|
||||||
|
sampleSource = null;
|
||||||
|
// Set the next source index so that the required source is created in updateSources.
|
||||||
|
nextSourceIndex = sourceIndex;
|
||||||
|
}
|
||||||
|
return sampleSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reselectTracks() throws ExoPlaybackException {
|
||||||
|
if (readingSource != null && readingSource != playingSource) {
|
||||||
|
// Newly enabled tracks in playingSource can increase the calculated start timestamp for the
|
||||||
|
// next source, so we have to discard the reading source. Reset TrackStreams for renderers
|
||||||
|
// that are reading the next source already back to the playing source.
|
||||||
|
TrackSelectionArray newTrackSelections = readingSource.trackSelections;
|
||||||
|
TrackGroupArray groups = readingSource.sampleSource.getTrackGroups();
|
||||||
|
for (int i = 0; i < renderers.length; i++) {
|
||||||
|
TrackRenderer renderer = renderers[i];
|
||||||
|
TrackSelection selection = newTrackSelections.get(i);
|
||||||
|
if (selection != null && renderer.getState() != TrackRenderer.STATE_DISABLED) {
|
||||||
|
// The renderer is enabled and will continue to be enabled after the transition.
|
||||||
|
Format[] formats = new Format[selection.length];
|
||||||
|
for (int j = 0; j < formats.length; j++) {
|
||||||
|
formats[j] = groups.get(selection.group).getFormat(selection.getTrack(j));
|
||||||
|
}
|
||||||
|
renderer.replaceTrackStream(formats, playingSource.trackStreams[i], sourceOffsetUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard the rest of the timeline after the playing source, as the player may need to
|
||||||
|
// rebuffer after track selection.
|
||||||
|
Source source = playingSource.nextSource;
|
||||||
|
while (source != null) {
|
||||||
|
source.release();
|
||||||
|
source = source.nextSource;
|
||||||
|
}
|
||||||
|
playingSource.nextSource = null;
|
||||||
|
readingSource = playingSource;
|
||||||
|
bufferingSource = playingSource;
|
||||||
|
nextSourceIndex = playingSource.index + 1;
|
||||||
|
playingSourceEndPositionUs = C.UNSET_TIME_US;
|
||||||
|
|
||||||
|
// Update the track selection for the playing source.
|
||||||
|
Pair<TrackSelectionArray, Object> result =
|
||||||
|
trackSelector.selectTracks(renderers, playingSource.sampleSource.getTrackGroups());
|
||||||
|
TrackSelectionArray newTrackSelections = result.first;
|
||||||
|
Object trackSelectionData = result.second;
|
||||||
|
if (newTrackSelections.equals(playingSource.trackSelections)) {
|
||||||
|
trackSelector.onSelectionActivated(trackSelectionData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int enabledRendererCount = disableRenderers(false, newTrackSelections);
|
||||||
|
TrackStream[] newStreams = playingSource.updateTrackStreams(oldStreams, newTrackSelections,
|
||||||
|
newSelections, positionUs);
|
||||||
|
trackSelector.onSelectionActivated(trackSelectionData);
|
||||||
|
|
||||||
|
// Update the stored TrackStreams.
|
||||||
|
for (int i = 0; i < renderers.length; i++) {
|
||||||
|
TrackRenderer renderer = renderers[i];
|
||||||
|
TrackSelection newSelection = newTrackSelections.get(i);
|
||||||
|
if (newSelection != null && renderer.getState() == TrackRenderer.STATE_DISABLED) {
|
||||||
|
int newStreamIndex = newSelections.indexOf(newSelection);
|
||||||
|
playingSource.trackStreams[i] = newStreams[newStreamIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enableRenderers(newTrackSelections, enabledRendererCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
Source source = playingSource != null ? playingSource : bufferingSource;
|
||||||
|
while (source != null) {
|
||||||
|
source.release();
|
||||||
|
source = source.nextSource;
|
||||||
|
}
|
||||||
|
playingSource = null;
|
||||||
|
readingSource = null;
|
||||||
|
bufferingSource = null;
|
||||||
|
durationUs = C.UNSET_TIME_US;
|
||||||
|
nextSourceIndex = 0;
|
||||||
|
sourceOffsetUs = 0;
|
||||||
|
playingSourceEndPositionUs = C.UNSET_TIME_US;
|
||||||
|
nextSourceOffsetUs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Timeline[");
|
||||||
|
Source source = playingSource != null ? playingSource : bufferingSource;
|
||||||
|
while (source != null) {
|
||||||
|
sb.append(source);
|
||||||
|
source = source.nextSource;
|
||||||
|
if (source != null) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append("]");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPlayingSource(Source source) throws ExoPlaybackException {
|
||||||
|
playingSourceEndPositionUs = C.UNSET_TIME_US;
|
||||||
|
durationUs = source.sampleSource.getDurationUs();
|
||||||
|
|
||||||
|
// Disable/enable renderers for the new source.
|
||||||
|
int enabledRendererCount = disableRenderers(true, source.trackSelections);
|
||||||
|
if (playingSource != source) {
|
||||||
|
trackSelector.onSelectionActivated(source.trackSelectionData);
|
||||||
|
}
|
||||||
|
readingSource = source;
|
||||||
|
playingSource = source;
|
||||||
|
enableRenderers(source.trackSelections, enabledRendererCount);
|
||||||
|
|
||||||
|
// Update the timeline position for the new source index.
|
||||||
|
synchronized (timeline) {
|
||||||
|
sourceIndex = source.index;
|
||||||
|
updatePositionUs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int disableRenderers(boolean sourceTransition, TrackSelectionArray newTrackSelections)
|
||||||
|
throws ExoPlaybackException {
|
||||||
|
// Disable any renderers whose selections have changed, adding the corresponding TrackStream
|
||||||
|
// instances to oldStreams. Where we need to obtain a new TrackStream instance for a renderer,
|
||||||
|
// we add the corresponding TrackSelection to newSelections.
|
||||||
|
oldStreams.clear();
|
||||||
|
newSelections.clear();
|
||||||
|
int enabledRendererCount = 0;
|
||||||
|
for (int i = 0; i < renderers.length; i++) {
|
||||||
|
TrackRenderer renderer = renderers[i];
|
||||||
|
rendererWasEnabledFlags[i] = renderer.getState() != TrackRenderer.STATE_DISABLED;
|
||||||
|
TrackSelection oldSelection = playingSource == null ? null
|
||||||
|
: playingSource.trackSelections.get(i);
|
||||||
|
TrackSelection newSelection = newTrackSelections.get(i);
|
||||||
|
if (newSelection != null) {
|
||||||
|
enabledRendererCount++;
|
||||||
|
}
|
||||||
|
// If the player is transitioning to a new source, disable renderers that are not used when
|
||||||
|
// playing the new source. Otherwise, disable renderers whose selections are changing.
|
||||||
|
if ((sourceTransition && oldSelection != null && newSelection == null)
|
||||||
|
|| (!sourceTransition && !Util.areEqual(oldSelection, newSelection))) {
|
||||||
|
// Either this is a source transition and the renderer is not needed any more, or the
|
||||||
|
if (rendererWasEnabledFlags[i]) {
|
||||||
|
// We need to disable the renderer so that we can enable it with its new selection.
|
||||||
|
if (renderer == rendererMediaClockSource) {
|
||||||
|
// The renderer is providing the media clock.
|
||||||
|
if (newSelection == 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);
|
||||||
|
// TODO[playlists]: Consider using TrackStreams from the timeline source instead of
|
||||||
|
// returning a TrackStream from TrackRenderer.disable.
|
||||||
|
oldStreams.add(renderer.disable());
|
||||||
|
}
|
||||||
|
if (newSelection != null) {
|
||||||
|
newSelections.add(newSelection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enabledRendererCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enableRenderers(TrackSelectionArray newTrackSelections, int enabledRendererCount)
|
||||||
|
throws ExoPlaybackException {
|
||||||
|
playingSource.trackSelections = newTrackSelections;
|
||||||
|
enabledRenderers = new TrackRenderer[enabledRendererCount];
|
||||||
|
enabledRendererCount = 0;
|
||||||
|
TrackGroupArray trackGroups = playingSource.sampleSource.getTrackGroups();
|
||||||
|
for (int i = 0; i < renderers.length; i++) {
|
||||||
|
TrackRenderer renderer = renderers[i];
|
||||||
|
TrackSelection newSelection = playingSource.trackSelections.get(i);
|
||||||
|
if (newSelection != null) {
|
||||||
|
enabledRenderers[enabledRendererCount++] = renderer;
|
||||||
|
if (renderer.getState() == TrackRenderer.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 new selection.
|
||||||
|
Format[] formats = new Format[newSelection.length];
|
||||||
|
for (int j = 0; j < formats.length; j++) {
|
||||||
|
formats[j] = trackGroups.get(newSelection.group).getFormat(newSelection.getTrack(j));
|
||||||
|
}
|
||||||
|
// Enable the renderer.
|
||||||
|
renderer.enable(formats, playingSource.trackStreams[i], internalPositionUs, joining,
|
||||||
|
sourceOffsetUs);
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a {@link SampleSource} with information required to play it as part of a timeline.
|
||||||
|
*/
|
||||||
|
private static final class Source {
|
||||||
|
|
||||||
|
public final SampleSource sampleSource;
|
||||||
|
public final int index;
|
||||||
|
public final TrackStream[] trackStreams;
|
||||||
|
|
||||||
|
public boolean prepared;
|
||||||
|
public boolean hasEnabledTracks;
|
||||||
|
public TrackSelectionArray trackSelections;
|
||||||
|
public Object trackSelectionData;
|
||||||
|
|
||||||
|
public Source nextSource;
|
||||||
|
|
||||||
|
public Source(SampleSource sampleSource, int index, int rendererCount) {
|
||||||
|
this.sampleSource = sampleSource;
|
||||||
|
this.index = index;
|
||||||
|
trackStreams = new TrackStream[rendererCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFullyBuffered() {
|
||||||
|
return prepared && sampleSource.getBufferedPositionUs() == C.END_OF_SOURCE_US;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean prepare(long startPositionUs) throws IOException {
|
||||||
|
if (sampleSource.prepare(startPositionUs)) {
|
||||||
|
prepared = true;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void selectTracks(TrackSelectionArray newTrackSelections, Object trackSelectionData,
|
||||||
|
long positionUs) throws ExoPlaybackException {
|
||||||
|
this.trackSelectionData = trackSelectionData;
|
||||||
|
if (newTrackSelections.equals(trackSelections)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<TrackStream> oldStreams = new ArrayList<>();
|
||||||
|
ArrayList<TrackSelection> newSelections = new ArrayList<>();
|
||||||
|
for (int i = 0; i < newTrackSelections.length; i++) {
|
||||||
|
TrackSelection oldSelection = trackSelections == null ? null : trackSelections.get(i);
|
||||||
|
TrackSelection newSelection = newTrackSelections.get(i);
|
||||||
|
if (!Util.areEqual(oldSelection, newSelection)) {
|
||||||
|
if (oldSelection != null) {
|
||||||
|
oldStreams.add(trackStreams[i]);
|
||||||
|
}
|
||||||
|
if (newSelection != null) {
|
||||||
|
newSelections.add(newSelection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateTrackStreams(oldStreams, newTrackSelections, newSelections, positionUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackStream[] updateTrackStreams(ArrayList<TrackStream> oldStreams,
|
||||||
|
TrackSelectionArray newTrackSelections, ArrayList<TrackSelection> newSelections,
|
||||||
|
long positionUs) {
|
||||||
|
TrackStream[] newStreams = sampleSource.selectTracks(oldStreams, newSelections,
|
||||||
|
positionUs);
|
||||||
|
hasEnabledTracks = false;
|
||||||
|
for (int i = 0; i < newTrackSelections.length; i++) {
|
||||||
|
TrackSelection selection = newTrackSelections.get(i);
|
||||||
|
if (selection != null) {
|
||||||
|
hasEnabledTracks = true;
|
||||||
|
int index = newSelections.indexOf(selection);
|
||||||
|
if (index != -1) {
|
||||||
|
trackStreams[i] = newStreams[index];
|
||||||
|
} else {
|
||||||
|
// This selection/stream is unchanged.
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trackStreams[i] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trackSelections = newTrackSelections;
|
||||||
|
return newStreams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void release() {
|
||||||
|
try {
|
||||||
|
sampleSource.release();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
// There's nothing we can do.
|
||||||
|
Log.e(TAG, "Source release failed.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,10 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||||
private int index;
|
private int index;
|
||||||
private int state;
|
private int state;
|
||||||
private TrackStream stream;
|
private TrackStream stream;
|
||||||
|
private long streamOffsetUs;
|
||||||
|
private long maximumTimeUs;
|
||||||
|
private boolean readEndOfStream;
|
||||||
|
private boolean streamIsFinal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the index of this renderer within the player.
|
* Sets the index of this renderer within the player.
|
||||||
|
|
@ -168,14 +172,16 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||||
* @param stream The track stream from which the renderer should consume.
|
* @param stream The track stream from which the renderer should consume.
|
||||||
* @param positionUs The player's current position.
|
* @param positionUs The player's current position.
|
||||||
* @param joining Whether this renderer is being enabled to join an ongoing playback.
|
* @param joining Whether this renderer is being enabled to join an ongoing playback.
|
||||||
|
* @param offsetUs The offset to be added to timestamps of buffers read from {@code stream}
|
||||||
|
* before they are renderered.
|
||||||
* @throws ExoPlaybackException If an error occurs.
|
* @throws ExoPlaybackException If an error occurs.
|
||||||
*/
|
*/
|
||||||
/* package */ final void enable(Format[] formats, TrackStream stream, long positionUs,
|
/* package */ final void enable(Format[] formats, TrackStream stream, long positionUs,
|
||||||
boolean joining) throws ExoPlaybackException {
|
boolean joining, long offsetUs) throws ExoPlaybackException {
|
||||||
Assertions.checkState(state == STATE_DISABLED);
|
Assertions.checkState(state == STATE_DISABLED);
|
||||||
state = STATE_ENABLED;
|
state = STATE_ENABLED;
|
||||||
onEnabled(joining);
|
onEnabled(joining);
|
||||||
replaceTrackStream(formats, stream);
|
replaceTrackStream(formats, stream, offsetUs);
|
||||||
onReset(positionUs, joining);
|
onReset(positionUs, joining);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,15 +198,20 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces the {@link TrackStream} from which samples will be consumed.
|
* Sets the {@link TrackStream} from which samples will be consumed.
|
||||||
*
|
*
|
||||||
* @param formats The enabled formats.
|
* @param formats The enabled formats.
|
||||||
* @param trackStream The track stream from which the renderer should consume.
|
* @param stream The track stream from which the renderer should consume.
|
||||||
|
* @param offsetUs The offset to be added to timestamps of buffers read from {@code stream} before
|
||||||
|
* they are renderered.
|
||||||
* @throws ExoPlaybackException If an error occurs.
|
* @throws ExoPlaybackException If an error occurs.
|
||||||
*/
|
*/
|
||||||
/* package */ final void replaceTrackStream(Format[] formats, TrackStream trackStream)
|
/* package */ final void replaceTrackStream(Format[] formats, TrackStream stream, long offsetUs)
|
||||||
throws ExoPlaybackException {
|
throws ExoPlaybackException {
|
||||||
stream = trackStream;
|
Assertions.checkState(!streamIsFinal);
|
||||||
|
this.stream = stream;
|
||||||
|
readEndOfStream = false;
|
||||||
|
streamOffsetUs = offsetUs;
|
||||||
onStreamChanged(formats);
|
onStreamChanged(formats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,13 +234,16 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||||
* @throws ExoPlaybackException If an error occurs handling the reset.
|
* @throws ExoPlaybackException If an error occurs handling the reset.
|
||||||
*/
|
*/
|
||||||
/* package */ final void reset(long positionUs) throws ExoPlaybackException {
|
/* package */ final void reset(long positionUs) throws ExoPlaybackException {
|
||||||
|
streamIsFinal = false;
|
||||||
|
maximumTimeUs = C.UNSET_TIME_US;
|
||||||
onReset(positionUs, false);
|
onReset(positionUs, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a reset is encountered, and also when the renderer is enabled.
|
* Invoked when a reset is encountered, and also when the renderer is enabled.
|
||||||
* <p>
|
* <p>
|
||||||
* The default implementation is a no-op.
|
* This method may be called when the renderer is in the following states:
|
||||||
|
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
|
||||||
*
|
*
|
||||||
* @param positionUs The playback position in microseconds.
|
* @param positionUs The playback position in microseconds.
|
||||||
* @param joining Whether this renderer is being enabled to join an ongoing playback.
|
* @param joining Whether this renderer is being enabled to join an ongoing playback.
|
||||||
|
|
@ -239,6 +253,29 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the renderer has read the current {@link TrackStream} to the end.
|
||||||
|
*/
|
||||||
|
/* package */ final boolean hasReadStreamToEnd() {
|
||||||
|
return readEndOfStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum buffer timestamp read from the stream since the last reset, or
|
||||||
|
* {@link C#UNSET_TIME_US} if no buffers have been read.
|
||||||
|
*/
|
||||||
|
/* package */ final long getMaximumTimeUs() {
|
||||||
|
return maximumTimeUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals to the renderer that the current {@link TrackStream} will be the final one supplied
|
||||||
|
* before it is next disabled or reset.
|
||||||
|
*/
|
||||||
|
/* package */ final void setCurrentTrackStreamIsFinal() {
|
||||||
|
streamIsFinal = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the renderer, meaning that calls to {@link #render(long, long)} will cause media to be
|
* Starts the renderer, meaning that calls to {@link #render(long, long)} will cause media to be
|
||||||
* rendered.
|
* rendered.
|
||||||
|
|
@ -293,6 +330,7 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||||
onDisabled();
|
onDisabled();
|
||||||
TrackStream trackStream = stream;
|
TrackStream trackStream = stream;
|
||||||
stream = null;
|
stream = null;
|
||||||
|
streamIsFinal = false;
|
||||||
return trackStream;
|
return trackStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -327,7 +365,18 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
|
||||||
* @see TrackStream#readData(FormatHolder, DecoderInputBuffer)
|
* @see TrackStream#readData(FormatHolder, DecoderInputBuffer)
|
||||||
*/
|
*/
|
||||||
protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer) {
|
protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer) {
|
||||||
return stream.readData(formatHolder, buffer);
|
int result = stream.readData(formatHolder, buffer);
|
||||||
|
if (result == TrackStream.BUFFER_READ) {
|
||||||
|
if (buffer.isEndOfStream()) {
|
||||||
|
readEndOfStream = true;
|
||||||
|
return streamIsFinal ? TrackStream.BUFFER_READ : TrackStream.NOTHING_READ;
|
||||||
|
}
|
||||||
|
buffer.timeUs += streamOffsetUs;
|
||||||
|
if (buffer.timeUs > maximumTimeUs) {
|
||||||
|
maximumTimeUs = buffer.timeUs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -610,27 +610,6 @@ public final class Util {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the top 32 bits of a long as an integer.
|
|
||||||
*/
|
|
||||||
public static int getTopInt(long value) {
|
|
||||||
return (int) (value >>> 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the bottom 32 bits of a long as an integer.
|
|
||||||
*/
|
|
||||||
public static int getBottomInt(long value) {
|
|
||||||
return (int) value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a long created by concatenating the bits of two integers.
|
|
||||||
*/
|
|
||||||
public static long getLong(int topInteger, int bottomInteger) {
|
|
||||||
return ((long) topInteger << 32) | (bottomInteger & 0xFFFFFFFFL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a byte array containing values parsed from the hex string provided.
|
* Returns a byte array containing values parsed from the hex string provided.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue