From 7ae9bf409bb10b5e84183d945754f527fcd0f216 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 13 Jun 2016 04:36:06 -0700 Subject: [PATCH] Play a sequence of sources (playlists #3). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=124713796 --- .../android/exoplayer/util/UtilTest.java | 17 - .../exoplayer/ExoPlayerImplInternal.java | 675 ++++++++++++++---- .../android/exoplayer/TrackRenderer.java | 67 +- .../google/android/exoplayer/util/Util.java | 21 - 4 files changed, 603 insertions(+), 177 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer/util/UtilTest.java b/library/src/androidTest/java/com/google/android/exoplayer/util/UtilTest.java index 7efa0aafc8d..26779a96676 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/util/UtilTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/util/UtilTest.java @@ -144,23 +144,6 @@ public void testParseXsDateTime() throws ParseException { 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() { assertNull(Util.unescapeFileName("%a")); assertNull(Util.unescapeFileName("%xyz")); diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java index 74fb92f4e7b..e7783e9c1aa 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -70,13 +70,13 @@ private final HandlerThread internalPlaybackThread; private final Handler eventHandler; private final AtomicInteger pendingSeekCount; + private final Timeline timeline; - private TrackSelectionArray trackSelections; private TrackRenderer rendererMediaClockSource; private MediaClock rendererMediaClock; - private SampleSource source; + private SampleSourceProvider sampleSourceProvider; + private SampleSource sampleSource; private TrackRenderer[] enabledRenderers; - private boolean preparedSource; private boolean released; private boolean playWhenReady; private boolean rebuffering; @@ -84,8 +84,13 @@ private int customMessagesSent; private int customMessagesProcessed; private long lastSeekPositionMs; + private int lastSeekSourceIndex; private long elapsedRealtimeUs; + private long sourceOffsetUs; + private long internalPositionUs; + private int sourceIndex; + private volatile long durationUs; private volatile long positionUs; private volatile long bufferedPositionUs; @@ -109,6 +114,7 @@ public ExoPlayerImplInternal(TrackRenderer[] renderers, TrackSelector trackSelec standaloneMediaClock = new StandaloneMediaClock(); pendingSeekCount = new AtomicInteger(); enabledRenderers = new TrackRenderer[0]; + timeline = new Timeline(); trackSelector.init(this); @@ -126,8 +132,8 @@ public long getCurrentPosition() { public long getBufferedPosition() { long bufferedPositionUs = this.bufferedPositionUs; - return bufferedPositionUs == C.UNSET_TIME_US ? ExoPlayer.UNKNOWN_TIME - : bufferedPositionUs / 1000; + return bufferedPositionUs == C.UNSET_TIME_US || bufferedPositionUs == C.END_OF_SOURCE_US + ? ExoPlayer.UNKNOWN_TIME : bufferedPositionUs / 1000; } public long getDuration() { @@ -144,10 +150,20 @@ public void setPlayWhenReady(boolean playWhenReady) { } 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; pendingSeekCount.incrementAndGet(); - handler.obtainMessage(MSG_SEEK_TO, Util.getTopInt(positionMs), - Util.getBottomInt(positionMs)).sendToTarget(); + handler.obtainMessage(MSG_SEEK_TO, lastSeekSourceIndex, -1, positionMs).sendToTarget(); } public void stop() { @@ -194,11 +210,15 @@ public synchronized void release() { internalPlaybackThread.quit(); } + // InvalidationListener implementation. + @Override public void onTrackSelectionsInvalidated() { handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED); } + // Handler.Callback implementation. + @Override public boolean handleMessage(Message msg) { try { @@ -216,7 +236,7 @@ public boolean handleMessage(Message msg) { return true; } case MSG_SEEK_TO: { - seekToInternal(Util.getLong(msg.arg1, msg.arg2)); + seekToInternal(msg.arg1, (Long) msg.obj); return true; } case MSG_STOP: { @@ -257,6 +277,8 @@ public boolean handleMessage(Message msg) { } } + // Private methods. + private void setState(int state) { if (this.state != state) { this.state = state; @@ -269,6 +291,7 @@ private boolean isReadyOrEnded(TrackRenderer renderer) { } private boolean haveSufficientBuffer() { + // TODO[playlists]: Take into account the buffered position in the timeline. long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs; return minBufferDurationUs <= 0 || bufferedPositionUs == C.UNSET_TIME_US @@ -279,8 +302,7 @@ private boolean haveSufficientBuffer() { private void setSourceProviderInternal(SampleSourceProvider sourceProvider) { resetInternal(); - // TODO[playlists]: Create and use sources after the first one. - this.source = sourceProvider.createSource(0); + sampleSourceProvider = sourceProvider; setState(ExoPlayer.STATE_BUFFERING); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -322,28 +344,28 @@ private void stopRenderers() throws ExoPlaybackException { private void updatePositionUs() { if (rendererMediaClockSource != null && !rendererMediaClockSource.isEnded()) { - positionUs = rendererMediaClock.getPositionUs(); - standaloneMediaClock.setPositionUs(positionUs); + internalPositionUs = rendererMediaClock.getPositionUs(); + standaloneMediaClock.setPositionUs(internalPositionUs); } else { - positionUs = standaloneMediaClock.getPositionUs(); + internalPositionUs = standaloneMediaClock.getPositionUs(); } + positionUs = internalPositionUs - sourceOffsetUs; elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; } private void updateBufferedPositionUs() { - long sourceBufferedPositionUs = enabledRenderers.length > 0 ? source.getBufferedPositionUs() - : C.END_OF_SOURCE_US; + long sourceBufferedPositionUs = enabledRenderers.length > 0 + ? sampleSource.getBufferedPositionUs() : C.END_OF_SOURCE_US; bufferedPositionUs = sourceBufferedPositionUs == C.END_OF_SOURCE_US && durationUs != C.UNSET_TIME_US ? durationUs : sourceBufferedPositionUs; } private void doSomeWork() throws ExoPlaybackException, IOException { long operationStartTimeMs = SystemClock.elapsedRealtime(); - if (!preparedSource) { - preparedSource = source.prepare(positionUs); - if (preparedSource) { - durationUs = source.getDurationUs(); - selectTracksInternal(); + if (sampleSource == null) { + timeline.updateSources(); + sampleSource = timeline.getSampleSource(internalPositionUs); + if (sampleSource != null) { resumeInternal(); } else { // We're still waiting for the source to be prepared. @@ -358,9 +380,10 @@ private void doSomeWork() throws ExoPlaybackException, IOException { // Process reset if there is one, else update the position. if (!checkForSourceResetInternal()) { updatePositionUs(); + sampleSource = timeline.getSampleSource(internalPositionUs); } updateBufferedPositionUs(); - source.continueBuffering(positionUs); + timeline.updateSources(); } else { updatePositionUs(); } @@ -368,10 +391,10 @@ private void doSomeWork() throws ExoPlaybackException, IOException { boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; for (TrackRenderer renderer : enabledRenderers) { - // TODO: Each renderer should return the maximum delay before which it wishes to be - // invoked again. The minimum of these values should then be used as the delay before the next + // TODO: Each renderer should return the maximum delay before which it wishes to be invoked + // again. The minimum of these values should then be used as the delay before the next // invocation of this method. - renderer.render(positionUs, elapsedRealtimeUs); + renderer.render(internalPositionUs, 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. @@ -386,12 +409,13 @@ private void doSomeWork() throws ExoPlaybackException, IOException { setState(ExoPlayer.STATE_ENDED); stopRenderers(); } else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded - && haveSufficientBuffer()) { + && haveSufficientBuffer() && timeline.isReady(internalPositionUs)) { setState(ExoPlayer.STATE_READY); if (playWhenReady) { startRenderers(); } - } else if (state == ExoPlayer.STATE_READY && !allRenderersReadyOrEnded) { + } else if (state == ExoPlayer.STATE_READY && (!allRenderersReadyOrEnded + || !timeline.isReady(internalPositionUs))) { rebuffering = playWhenReady; setState(ExoPlayer.STATE_BUFFERING); stopRenderers(); @@ -418,7 +442,7 @@ private void scheduleNextOperation(int operationType, long thisOperationStartTim } } - private void seekToInternal(long positionMs) throws ExoPlaybackException { + private void seekToInternal(int sourceIndex, long positionMs) throws ExoPlaybackException { try { if (positionMs == (positionUs / 1000)) { // Seek is to the current position. Do nothing. @@ -427,9 +451,12 @@ private void seekToInternal(long positionMs) throws ExoPlaybackException { rebuffering = false; positionUs = positionMs * 1000; + internalPositionUs = sourceOffsetUs + positionUs; standaloneMediaClock.stop(); - standaloneMediaClock.setPositionUs(positionUs); - if (!preparedSource) { + standaloneMediaClock.setPositionUs(internalPositionUs); + sampleSource = timeline.seekTo(sourceIndex, positionUs); + if (sampleSource == null) { + // The source isn't prepared. return; } @@ -437,7 +464,6 @@ private void seekToInternal(long positionMs) throws ExoPlaybackException { for (TrackRenderer renderer : enabledRenderers) { ensureStopped(renderer); } - source.seekToUs(positionUs); checkForSourceResetInternal(); } @@ -459,8 +485,9 @@ private void resumeInternal() throws ExoPlaybackException { if (allRenderersEnded && (durationUs == C.UNSET_TIME_US || durationUs <= positionUs)) { setState(ExoPlayer.STATE_ENDED); } else { - setState(allRenderersReadyOrEnded && haveSufficientBuffer() ? ExoPlayer.STATE_READY - : ExoPlayer.STATE_BUFFERING); + setState(allRenderersReadyOrEnded && haveSufficientBuffer() + && timeline.isReady(internalPositionUs) ? ExoPlayer.STATE_READY + : ExoPlayer.STATE_BUFFERING); } // Start the renderers if ready, and schedule the first piece of work. @@ -471,14 +498,14 @@ private void resumeInternal() throws ExoPlaybackException { } private boolean checkForSourceResetInternal() throws ExoPlaybackException { - long resetPositionUs = source.readReset(); + long resetPositionUs = sampleSource.readReset(); if (resetPositionUs == C.UNSET_TIME_US) { return false; } - positionUs = resetPositionUs; - standaloneMediaClock.setPositionUs(resetPositionUs); + internalPositionUs = sourceOffsetUs + resetPositionUs; + standaloneMediaClock.setPositionUs(internalPositionUs); for (TrackRenderer renderer : enabledRenderers) { - renderer.reset(resetPositionUs); + renderer.reset(internalPositionUs); } return true; } @@ -499,9 +526,7 @@ private void releaseInternal() { private void resetInternal() { handler.removeMessages(MSG_DO_SOME_WORK); - preparedSource = false; rebuffering = false; - trackSelections = null; standaloneMediaClock.stop(); rendererMediaClock = null; rendererMediaClockSource = null; @@ -517,15 +542,8 @@ private void resetInternal() { Log.e(TAG, "Stop failed.", e); } } - if (source != null) { - try { - source.release(); - } catch (RuntimeException e) { - // There's nothing we can do. - Log.e(TAG, "Source release failed.", e); - } - source = null; - } + sampleSource = null; + timeline.reset(); } private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException { @@ -533,7 +551,7 @@ private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybac for (ExoPlayerMessage message : messages) { 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. handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -545,113 +563,510 @@ private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybac } } - private void selectTracksInternal() throws ExoPlaybackException { - TrackGroupArray groups = source.getTrackGroups(); - - Pair result = trackSelector.selectTracks(renderers, groups); - TrackSelectionArray newTrackSelections = result.first; + private void ensureStopped(TrackRenderer renderer) throws ExoPlaybackException { + if (renderer.getState() == TrackRenderer.STATE_STARTED) { + renderer.stop(); + } + } - if (newTrackSelections.equals(trackSelections)) { - trackSelector.onSelectionActivated(result.second); + 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); + } - // 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 oldStreams = new ArrayList<>(); - ArrayList 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()); + /** + * 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 oldStreams; + private final ArrayList 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; } - rendererMediaClock = null; - rendererMediaClockSource = null; + bufferingSource = newSource; } - ensureStopped(renderer); - oldStreams.add(renderer.disable()); } - if (newSelection != null) { - newSelections.add(newSelection); + } + + 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 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); + } } } } - // Update the source selection. - TrackStream[] newStreams = source.selectTracks(oldStreams, newSelections, positionUs); - trackSelector.onSelectionActivated(result.second); - trackSelections = newTrackSelections; + public boolean isReady(long positionUs) { + return playingSourceEndPositionUs == C.UNSET_TIME_US + || positionUs < playingSourceEndPositionUs || playingSource.nextSource != null; + } - // 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)); + 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); } - // Enable the renderer. + } + } + + // 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 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); - 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.")); + 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; } - rendererMediaClock = mediaClock; - rendererMediaClockSource = renderer; + ensureStopped(renderer); + // TODO[playlists]: Consider using TrackStreams from the timeline source instead of + // returning a TrackStream from TrackRenderer.disable. + oldStreams.add(renderer.disable()); } - // Start the renderer if playing. - if (playing) { - renderer.start(); + if (newSelection != null) { + newSelections.add(newSelection); } } } + return enabledRendererCount; } - } - private void reselectTracksInternal() throws ExoPlaybackException { - if (!preparedSource) { - // We don't have tracks yet, so we don't care. - return; + 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(); + } + } + } + } } - selectTracksInternal(); - updateBufferedPositionUs(); - handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } - private void ensureStopped(TrackRenderer renderer) throws ExoPlaybackException { - if (renderer.getState() == TrackRenderer.STATE_STARTED) { - renderer.stop(); + /** + * 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 oldStreams = new ArrayList<>(); + ArrayList 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 oldStreams, + TrackSelectionArray newTrackSelections, ArrayList 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); + } + } + } } diff --git a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java index 11de6ca0974..6b4289c24fd 100644 --- a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java @@ -108,6 +108,10 @@ public abstract class TrackRenderer implements ExoPlayerComponent { private int index; private int state; private TrackStream stream; + private long streamOffsetUs; + private long maximumTimeUs; + private boolean readEndOfStream; + private boolean streamIsFinal; /** * Sets the index of this renderer within the player. @@ -168,14 +172,16 @@ protected int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { * @param stream The track stream from which the renderer should consume. * @param positionUs The player's current position. * @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. */ /* 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); state = STATE_ENABLED; onEnabled(joining); - replaceTrackStream(formats, stream); + replaceTrackStream(formats, stream, offsetUs); onReset(positionUs, joining); } @@ -192,15 +198,20 @@ protected void onEnabled(boolean joining) throws ExoPlaybackException { } /** - * 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 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. */ - /* package */ final void replaceTrackStream(Format[] formats, TrackStream trackStream) + /* package */ final void replaceTrackStream(Format[] formats, TrackStream stream, long offsetUs) throws ExoPlaybackException { - stream = trackStream; + Assertions.checkState(!streamIsFinal); + this.stream = stream; + readEndOfStream = false; + streamOffsetUs = offsetUs; onStreamChanged(formats); } @@ -223,13 +234,16 @@ protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { * @throws ExoPlaybackException If an error occurs handling the reset. */ /* package */ final void reset(long positionUs) throws ExoPlaybackException { + streamIsFinal = false; + maximumTimeUs = C.UNSET_TIME_US; 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. *

- * 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 joining Whether this renderer is being enabled to join an ongoing playback. @@ -239,6 +253,29 @@ protected void onReset(long positionUs, boolean joining) throws ExoPlaybackExcep // 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 * rendered. @@ -293,6 +330,7 @@ protected void onStopped() throws ExoPlaybackException { onDisabled(); TrackStream trackStream = stream; stream = null; + streamIsFinal = false; return trackStream; } @@ -327,7 +365,18 @@ protected final void maybeThrowStreamError() throws IOException { * @see TrackStream#readData(FormatHolder, DecoderInputBuffer) */ 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; } /** diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index fda3af856d2..4d10ca1e2a7 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -610,27 +610,6 @@ public static int getIntegerCodeForString(String string) { 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. *