From 0841d043b8eaf737065c031fa72e35af3f9817c0 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 28 Apr 2016 02:45:07 -0700 Subject: [PATCH] Refactor #6.4: Create child source instances on demand. Dash/SS SampleSources now instantiate ChunkSource and ChunkSampleSource (renamed to ChunkTrackStream) instances on demand as tracks are enabled. The TrackGroups exposed by the DASH/SS SampleSources are now constructed at the top level. Note that this change resolves the TODOs at the top of the ChunkSource classes, allowing multiple adaptation sets of the same type. Next steps will include: - Bring back UTC timing element support for DASH, which will be an extra request during preparation in the DASH SampleSource. - Simplification of manifest fetching to use a Loader directly in the two top level SampleSource classes. ManifestFetcher should eventually go away once HLS no longer needs it. - Eventually, some consolidation between DASH/SS. There's a lot of common code there now. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=121001777 --- .../exoplayer/demo/player/DemoPlayer.java | 4 +- .../android/exoplayer/SampleSource.java | 2 +- .../android/exoplayer/chunk/ChunkSource.java | 34 +-- ...ampleSource.java => ChunkTrackStream.java} | 157 ++++------ ...ava => ChunkTrackStreamEventListener.java} | 23 +- .../exoplayer/dash/DashChunkSource.java | 171 ++++------- .../exoplayer/dash/DashSampleSource.java | 267 ++++++++-------- .../exoplayer/hls/HlsSampleSource.java | 8 +- .../SmoothStreamingChunkSource.java | 151 ++++----- .../SmoothStreamingSampleSource.java | 286 ++++++++---------- .../google/android/exoplayer/util/Util.java | 21 ++ 11 files changed, 472 insertions(+), 652 deletions(-) rename library/src/main/java/com/google/android/exoplayer/chunk/{ChunkSampleSource.java => ChunkTrackStream.java} (76%) rename library/src/main/java/com/google/android/exoplayer/chunk/{ChunkSampleSourceEventListener.java => ChunkTrackStreamEventListener.java} (90%) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index ec28a47458..a8b79e6c45 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -31,7 +31,7 @@ import com.google.android.exoplayer.SingleSampleSource; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.audio.AudioTrack; -import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener; +import com.google.android.exoplayer.chunk.ChunkTrackStreamEventListener; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; import com.google.android.exoplayer.extractor.ExtractorSampleSource; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; @@ -61,7 +61,7 @@ import java.util.concurrent.CopyOnWriteArrayList; * A wrapper around {@link ExoPlayer} that provides a higher level interface. */ public class DemoPlayer implements ExoPlayer.Listener, DefaultTrackSelector.EventListener, - ChunkSampleSourceEventListener, ExtractorSampleSource.EventListener, + ChunkTrackStreamEventListener, ExtractorSampleSource.EventListener, SingleSampleSource.EventListener, DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, StreamingDrmSessionManager.EventListener, TextRenderer, MetadataRenderer>, diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSource.java b/library/src/main/java/com/google/android/exoplayer/SampleSource.java index 72202b58aa..a38e901ff1 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java @@ -104,7 +104,7 @@ public interface SampleSource { long getBufferedPositionUs(); /** - * Seeks to the specified time in microseconds. + * Seeks to the specified position in microseconds. *

* This method should only be called when at least one track is selected. * diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java index 4195241804..05177644a0 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java @@ -15,13 +15,11 @@ */ package com.google.android.exoplayer.chunk; -import com.google.android.exoplayer.TrackGroup; - import java.io.IOException; import java.util.List; /** - * A provider of {@link Chunk}s for a {@link ChunkSampleSource} to load. + * A provider of {@link Chunk}s for a {@link ChunkTrackStream} to load. */ /* * TODO: Share more state between this interface and {@link ChunkSampleSource}. In particular @@ -40,25 +38,6 @@ public interface ChunkSource { */ void maybeThrowError() throws IOException; - /** - * Gets the group of tracks provided by the source. - *

- * This method should only be called after the source has been prepared. - * - * @return The track group. - */ - TrackGroup getTracks(); - - /** - * Enable the source for the specified tracks. - *

- * This method should only be called after the source has been prepared and when the source is - * disabled. - * - * @param tracks The track indices. - */ - void enable(int[] tracks); - /** * Evaluates whether {@link MediaChunk}s should be removed from the back of the queue. *

@@ -88,7 +67,7 @@ public interface ChunkSource { void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out); /** - * Invoked when the {@link ChunkSampleSource} has finished loading a chunk obtained from this + * Invoked when the {@link ChunkTrackStream} has finished loading a chunk obtained from this * source. *

* This method should only be called when the source is enabled. @@ -98,7 +77,7 @@ public interface ChunkSource { void onChunkLoadCompleted(Chunk chunk); /** - * Invoked when the {@link ChunkSampleSource} encounters an error loading a chunk obtained from + * Invoked when the {@link ChunkTrackStream} encounters an error loading a chunk obtained from * this source. *

* This method should only be called when the source is enabled. @@ -110,11 +89,4 @@ public interface ChunkSource { */ boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e); - /** - * Disables the source. - *

- * This method should only be called when the source is enabled. - */ - void disable(); - } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java similarity index 76% rename from library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java rename to library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java index fe61b310f9..0c7c351ee6 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java @@ -20,12 +20,8 @@ import com.google.android.exoplayer.DecoderInputBuffer; import com.google.android.exoplayer.Format; import com.google.android.exoplayer.FormatHolder; import com.google.android.exoplayer.LoadControl; -import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.TrackGroup; -import com.google.android.exoplayer.TrackGroupArray; -import com.google.android.exoplayer.TrackSelection; import com.google.android.exoplayer.TrackStream; -import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener.EventDispatcher; +import com.google.android.exoplayer.chunk.ChunkTrackStreamEventListener.EventDispatcher; import com.google.android.exoplayer.extractor.DefaultTrackOutput; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; @@ -40,10 +36,9 @@ import java.util.LinkedList; import java.util.List; /** - * A {@link SampleSource} that loads media in {@link Chunk}s, which are themselves obtained from a - * {@link ChunkSource}. + * A {@link TrackStream} that loads media in {@link Chunk}s, obtained from a {@link ChunkSource}. */ -public class ChunkSampleSource implements TrackStream, Loader.Callback { +public class ChunkTrackStream implements TrackStream, Loader.Callback { /** * The default minimum number of times to retry loading data prior to failing. @@ -55,18 +50,14 @@ public class ChunkSampleSource implements TrackStream, Loader.Callback { private final LinkedList mediaChunks; private final List readOnlyMediaChunks; private final DefaultTrackOutput sampleQueue; - private final int bufferSizeContribution; private final ChunkHolder nextChunkHolder; private final EventDispatcher eventDispatcher; private final LoadControl loadControl; - private boolean notifyReset; + private boolean readingEnabled; private long lastPreferredQueueSizeEvaluationTimeMs; private Format downstreamFormat; - private TrackGroupArray trackGroups; - private boolean trackEnabled; - private long downstreamPositionUs; private long lastSeekPositionUs; private long pendingResetPositionUs; @@ -74,30 +65,22 @@ public class ChunkSampleSource implements TrackStream, Loader.Callback { private Chunk currentLoadable; private long currentLoadStartTimeMs; private boolean loadingFinished; + private boolean released; /** * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. * @param loadControl Controls when the source is permitted to load data. * @param bufferSizeContribution The contribution of this source to the media buffer, in bytes. - */ - public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, - int bufferSizeContribution) { - this(chunkSource, loadControl, bufferSizeContribution, null, null, 0); - } - - /** - * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. - * @param loadControl Controls when the source is permitted to load data. - * @param bufferSizeContribution The contribution of this source to the media buffer, in bytes. + * @param positionUs The position from which to start loading media. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. */ - public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, - int bufferSizeContribution, Handler eventHandler, - ChunkSampleSourceEventListener eventListener, int eventSourceId) { - this(chunkSource, loadControl, bufferSizeContribution, eventHandler, eventListener, + public ChunkTrackStream(ChunkSource chunkSource, LoadControl loadControl, + int bufferSizeContribution, long positionUs, Handler eventHandler, + ChunkTrackStreamEventListener eventListener, int eventSourceId) { + this(chunkSource, loadControl, bufferSizeContribution, positionUs, eventHandler, eventListener, eventSourceId, DEFAULT_MIN_LOADABLE_RETRY_COUNT); } @@ -105,6 +88,7 @@ public class ChunkSampleSource implements TrackStream, Loader.Callback { * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. * @param loadControl Controls when the source is permitted to load data. * @param bufferSizeContribution The contribution of this source to the media buffer, in bytes. + * @param positionUs The position from which to start loading media. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. @@ -112,80 +96,35 @@ public class ChunkSampleSource implements TrackStream, Loader.Callback { * @param minLoadableRetryCount The minimum number of times that the source should retry a load * before propagating an error. */ - public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, - int bufferSizeContribution, Handler eventHandler, - ChunkSampleSourceEventListener eventListener, int eventSourceId, int minLoadableRetryCount) { + public ChunkTrackStream(ChunkSource chunkSource, LoadControl loadControl, + int bufferSizeContribution, long positionUs, Handler eventHandler, + ChunkTrackStreamEventListener eventListener, int eventSourceId, int minLoadableRetryCount) { this.chunkSource = chunkSource; this.loadControl = loadControl; - this.bufferSizeContribution = bufferSizeContribution; - loader = new Loader("Loader:ChunkSampleSource", minLoadableRetryCount); + loader = new Loader("Loader:ChunkTrackStream", minLoadableRetryCount); eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId); nextChunkHolder = new ChunkHolder(); mediaChunks = new LinkedList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); sampleQueue = new DefaultTrackOutput(loadControl.getAllocator()); pendingResetPositionUs = C.UNSET_TIME_US; + readingEnabled = true; + downstreamPositionUs = positionUs; + lastSeekPositionUs = positionUs; + loadControl.register(this, bufferSizeContribution); + restartFrom(positionUs); } - // SampleSource implementation. - - public void prepare() { - TrackGroup tracks = chunkSource.getTracks(); - if (tracks != null) { - trackGroups = new TrackGroupArray(tracks); - } else { - trackGroups = new TrackGroupArray(); - } - } - - public TrackGroupArray getTrackGroups() { - return trackGroups; - } - - public TrackStream[] selectTracks(List oldStreams, - List newSelections, long positionUs) { - Assertions.checkState(oldStreams.size() <= 1); - Assertions.checkState(newSelections.size() <= 1); - boolean trackWasEnabled = trackEnabled; - // Unselect old tracks. - if (!oldStreams.isEmpty()) { - Assertions.checkState(trackEnabled); - trackEnabled = false; - chunkSource.disable(); - } - // Select new tracks. - TrackStream[] newStreams = new TrackStream[newSelections.size()]; - if (!newSelections.isEmpty()) { - Assertions.checkState(!trackEnabled); - trackEnabled = true; - chunkSource.enable(newSelections.get(0).getTracks()); - newStreams[0] = this; - } - // Cancel or start requests as necessary. - if (!trackEnabled) { - if (trackWasEnabled) { - loadControl.unregister(this); - } - if (loader.isLoading()) { - loader.cancelLoading(); - } else { - clearState(); - loadControl.trimAllocator(); - } - } else { - if (!trackWasEnabled) { - loadControl.register(this, bufferSizeContribution); - } - downstreamFormat = null; - sampleQueue.needDownstreamFormat(); - downstreamPositionUs = positionUs; - lastSeekPositionUs = positionUs; - notifyReset = false; - restartFrom(positionUs); - } - return newStreams; + /** + * Enables or disables reading of data from {@link #readData(FormatHolder, DecoderInputBuffer)}. + * + * @param readingEnabled Whether reading should be enabled. + */ + public void setReadingEnabled(boolean readingEnabled) { + this.readingEnabled = readingEnabled; } + // TODO[REFACTOR]: Find a way to get rid of this. public void continueBuffering(long positionUs) { downstreamPositionUs = positionUs; if (!loader.isLoading()) { @@ -193,14 +132,12 @@ public class ChunkSampleSource implements TrackStream, Loader.Callback { } } - public long readReset() { - if (notifyReset) { - notifyReset = false; - return lastSeekPositionUs; - } - return C.UNSET_TIME_US; - } - + /** + * Returns an estimate of the position up to which data is buffered. + * + * @return An estimate of the absolute position in microseconds up to which data is buffered, or + * {@link C#END_OF_SOURCE_US} if the track is fully buffered. + */ public long getBufferedPositionUs() { if (loadingFinished) { return C.END_OF_SOURCE_US; @@ -218,6 +155,11 @@ public class ChunkSampleSource implements TrackStream, Loader.Callback { } } + /** + * Seeks to the specified position in microseconds. + * + * @param positionUs The seek position in microseconds. + */ public void seekToUs(long positionUs) { downstreamPositionUs = positionUs; lastSeekPositionUs = positionUs; @@ -233,16 +175,23 @@ public class ChunkSampleSource implements TrackStream, Loader.Callback { // We failed, and need to restart. restartFrom(positionUs); } - // Either way, we need to send a discontinuity to the downstream components. - notifyReset = true; } + /** + * Releases the stream. + *

+ * This method should be called when the stream is no longer required. + */ public void release() { - if (trackEnabled) { - loadControl.unregister(this); - trackEnabled = false; + loadControl.unregister(this); + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + clearState(); + loadControl.trimAllocator(); } loader.release(); + released = true; } // TrackStream implementation. @@ -260,7 +209,7 @@ public class ChunkSampleSource implements TrackStream, Loader.Callback { @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - if (notifyReset || isPendingReset()) { + if (!readingEnabled || isPendingReset()) { return NOTHING_READ; } @@ -306,7 +255,7 @@ public class ChunkSampleSource implements TrackStream, Loader.Callback { @Override public void onLoadCanceled(Loadable loadable) { eventDispatcher.loadCanceled(currentLoadable.bytesLoaded()); - if (trackEnabled) { + if (!released) { restartFrom(pendingResetPositionUs); } else { clearState(); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSourceEventListener.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStreamEventListener.java similarity index 90% rename from library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSourceEventListener.java rename to library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStreamEventListener.java index f961ce2f58..db9d51153c 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSourceEventListener.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStreamEventListener.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer.chunk; import com.google.android.exoplayer.C; import com.google.android.exoplayer.Format; -import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackStream; import com.google.android.exoplayer.util.Util; @@ -26,14 +25,14 @@ import android.os.Handler; import java.io.IOException; /** - * Interface for callbacks to be notified of chunk based {@link SampleSource} events. + * Interface for callbacks to be notified of chunk based {@link ChunkTrackStream} events. */ -public interface ChunkSampleSourceEventListener { +public interface ChunkTrackStreamEventListener { /** * Invoked when an upstream load is started. * - * @param sourceId The id of the reporting {@link SampleSource}. + * @param sourceId The id of the reporting {@link ChunkTrackStream}. * @param length The length of the data being loaded in bytes, or {@link C#LENGTH_UNBOUNDED} if * the length of the data is not known in advance. * @param type The type of the data being loaded. @@ -51,7 +50,7 @@ public interface ChunkSampleSourceEventListener { /** * Invoked when the current load operation completes. * - * @param sourceId The id of the reporting {@link SampleSource}. + * @param sourceId The id of the reporting {@link ChunkTrackStream}. * @param bytesLoaded The number of bytes that were loaded. * @param type The type of the loaded data. * @param trigger The reason for the data being loaded. @@ -70,7 +69,7 @@ public interface ChunkSampleSourceEventListener { /** * Invoked when the current upstream load operation is canceled. * - * @param sourceId The id of the reporting {@link SampleSource}. + * @param sourceId The id of the reporting {@link ChunkTrackStream}. * @param bytesLoaded The number of bytes that were loaded prior to the cancellation. */ void onLoadCanceled(int sourceId, long bytesLoaded); @@ -78,7 +77,7 @@ public interface ChunkSampleSourceEventListener { /** * Invoked when an error occurs loading media data. * - * @param sourceId The id of the reporting {@link SampleSource}. + * @param sourceId The id of the reporting {@link ChunkTrackStream}. * @param e The cause of the failure. */ void onLoadError(int sourceId, IOException e); @@ -87,7 +86,7 @@ public interface ChunkSampleSourceEventListener { * Invoked when data is removed from the back of the buffer, typically so that it can be * re-buffered using a different representation. * - * @param sourceId The id of the reporting {@link SampleSource}. + * @param sourceId The id of the reporting {@link ChunkTrackStream}. * @param mediaStartTimeMs The media time of the start of the discarded data. * @param mediaEndTimeMs The media time of the end of the discarded data. */ @@ -97,7 +96,7 @@ public interface ChunkSampleSourceEventListener { * Invoked when the downstream format changes (i.e. when the format being supplied to the * caller of {@link TrackStream#readData} changes). * - * @param sourceId The id of the reporting {@link SampleSource}. + * @param sourceId The id of the reporting {@link ChunkTrackStream}. * @param format The format. * @param trigger The trigger specified in the corresponding upstream load, as specified by the * {@link ChunkSource}. @@ -106,15 +105,15 @@ public interface ChunkSampleSourceEventListener { void onDownstreamFormatChanged(int sourceId, Format format, int trigger, long mediaTimeMs); /** - * Dispatches events to a {@link ChunkSampleSourceEventListener}. + * Dispatches events to a {@link ChunkTrackStreamEventListener}. */ final class EventDispatcher { private final Handler handler; - private final ChunkSampleSourceEventListener listener; + private final ChunkTrackStreamEventListener listener; private final int sourceId; - public EventDispatcher(Handler handler, ChunkSampleSourceEventListener listener, int sourceId) { + public EventDispatcher(Handler handler, ChunkTrackStreamEventListener listener, int sourceId) { this.handler = listener != null ? handler : null; this.listener = listener; this.sourceId = sourceId; diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index d7cb4a8ee2..3130c0a3cf 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -52,52 +52,85 @@ import java.util.List; /** * An {@link ChunkSource} for DASH streams. - *

- * This implementation currently supports fMP4, webm, webvtt and ttml. - *

- * This implementation makes the following assumptions about multi-period manifests: - *

    - *
  1. that new periods will contain the same representations as previous periods (i.e. no new or - * missing representations) and
  2. - *
  3. that representations are contiguous across multiple periods
  4. - *
*/ -// TODO: handle cases where the above assumption are false -// TODO[REFACTOR]: Handle multiple adaptation sets of the same type (at a higher level). public class DashChunkSource implements ChunkSource { - private final int adaptationSetType; + private final int adaptationSetIndex; + private final TrackGroup trackGroup; + private final RepresentationHolder[] representationHolders; + private final Format[] enabledFormats; + private final boolean[] adaptiveFormatBlacklistFlags; private final DataSource dataSource; private final FormatEvaluator adaptiveFormatEvaluator; private final Evaluation evaluation; - private MediaPresentationDescription currentManifest; + private MediaPresentationDescription manifest; private DrmInitData drmInitData; private boolean lastChunkWasInitialization; private IOException fatalError; - // Properties of exposed tracks. - private int adaptationSetIndex; - private TrackGroup trackGroup; - private RepresentationHolder[] representationHolders; - - // Properties of enabled tracks. - private Format[] enabledFormats; - private boolean[] adaptiveFormatBlacklistFlags; - /** - * @param adaptationSetType The type of the adaptation set exposed by this source. One of - * {@link C#TRACK_TYPE_AUDIO}, {@link C#TRACK_TYPE_VIDEO} and {@link C#TRACK_TYPE_TEXT}. + * @param manifest The initial manifest. + * @param adaptationSetIndex The index of the adaptation set in the manifest. + * @param trackGroup The track group corresponding to the adaptation set. + * @param tracks The indices of the selected tracks within the adaptation set. * @param dataSource A {@link DataSource} suitable for loading the media data. * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. */ - public DashChunkSource(int adaptationSetType, DataSource dataSource, + public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex, + TrackGroup trackGroup, int[] tracks, DataSource dataSource, FormatEvaluator adaptiveFormatEvaluator) { - this.adaptationSetType = adaptationSetType; + this.manifest = manifest; + this.adaptationSetIndex = adaptationSetIndex; + this.trackGroup = trackGroup; this.dataSource = dataSource; this.adaptiveFormatEvaluator = adaptiveFormatEvaluator; this.evaluation = new Evaluation(); + + Period period = manifest.getPeriod(0); + long periodDurationUs = getPeriodDurationUs(manifest, 0); + AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); + drmInitData = getDrmInitData(adaptationSet); + + List representations = adaptationSet.representations; + representationHolders = new RepresentationHolder[representations.size()]; + for (int i = 0; i < representations.size(); i++) { + Representation representation = representations.get(i); + representationHolders[i] = new RepresentationHolder(periodDurationUs, representation); + } + enabledFormats = new Format[tracks.length]; + for (int i = 0; i < tracks.length; i++) { + enabledFormats[i] = trackGroup.getFormat(tracks[i]); + } + Arrays.sort(enabledFormats, new DecreasingBandwidthComparator()); + if (adaptiveFormatEvaluator != null) { + adaptiveFormatEvaluator.enable(enabledFormats); + adaptiveFormatBlacklistFlags = new boolean[tracks.length]; + } else { + adaptiveFormatBlacklistFlags = null; + } + } + + public void updateManifest(MediaPresentationDescription newManifest) { + try { + manifest = newManifest; + long periodDurationUs = getPeriodDurationUs(manifest, 0); + List representations = manifest.getPeriod(0).adaptationSets + .get(adaptationSetIndex).representations; + for (int i = 0; i < representationHolders.length; i++) { + Representation representation = representations.get(i); + representationHolders[i].updateRepresentation(periodDurationUs, representation); + } + } catch (BehindLiveWindowException e) { + fatalError = e; + } + } + + public void release() { + if (adaptiveFormatEvaluator != null) { + adaptiveFormatEvaluator.disable(); + } } // ChunkSource implementation. @@ -109,33 +142,6 @@ public class DashChunkSource implements ChunkSource { } } - public void init(MediaPresentationDescription initialManifest) { - currentManifest = initialManifest; - initForManifest(currentManifest); - } - - @Override - public final TrackGroup getTracks() { - return trackGroup; - } - - @Override - public void enable(int[] tracks) { - enabledFormats = new Format[tracks.length]; - for (int i = 0; i < tracks.length; i++) { - enabledFormats[i] = trackGroup.getFormat(tracks[i]); - } - Arrays.sort(enabledFormats, new DecreasingBandwidthComparator()); - if (enabledFormats.length > 1) { - adaptiveFormatEvaluator.enable(enabledFormats); - adaptiveFormatBlacklistFlags = new boolean[tracks.length]; - } - } - - public void updateManifest(MediaPresentationDescription newManifest) { - processManifest(newManifest); - } - @Override public int getPreferredQueueSize(long playbackPositionUs, List queue) { if (fatalError != null || enabledFormats.length < 2) { @@ -200,9 +206,9 @@ public class DashChunkSource implements ChunkSource { if (indexUnbounded) { // The index is itself unbounded. We need to use the current time to calculate the range of // available segments. - long liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000; - if (currentManifest.timeShiftBufferDepth != -1) { - long bufferDepthUs = currentManifest.timeShiftBufferDepth * 1000; + long liveEdgeTimestampUs = nowUs - manifest.availabilityStartTime * 1000; + if (manifest.timeShiftBufferDepth != -1) { + long bufferDepthUs = manifest.timeShiftBufferDepth * 1000; firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum, representationHolder.getSegmentNum(liveEdgeTimestampUs - bufferDepthUs)); } @@ -226,7 +232,7 @@ public class DashChunkSource implements ChunkSource { if (segmentNum > lastAvailableSegmentNum) { // This is beyond the last chunk in the current manifest. - out.endOfStream = !currentManifest.dynamic; + out.endOfStream = !manifest.dynamic; return; } @@ -270,59 +276,8 @@ public class DashChunkSource implements ChunkSource { return false; } - @Override - public void disable() { - if (enabledFormats.length > 1) { - adaptiveFormatEvaluator.disable(); - } - evaluation.clear(); - fatalError = null; - enabledFormats = null; - } - // Private methods. - private void initForManifest(MediaPresentationDescription manifest) { - Period period = manifest.getPeriod(0); - for (int i = 0; i < period.adaptationSets.size(); i++) { - AdaptationSet adaptationSet = period.adaptationSets.get(i); - if (adaptationSet.type == adaptationSetType) { - adaptationSetIndex = i; - List representations = adaptationSet.representations; - if (!representations.isEmpty()) { - // We've found a non-empty adaptation set of the exposed type. - long periodDurationUs = getPeriodDurationUs(manifest, 0); - representationHolders = new RepresentationHolder[representations.size()]; - Format[] trackFormats = new Format[representations.size()]; - for (int j = 0; j < trackFormats.length; j++) { - Representation representation = representations.get(j); - representationHolders[j] = new RepresentationHolder(periodDurationUs, representation); - trackFormats[j] = representation.format; - } - trackGroup = new TrackGroup(adaptiveFormatEvaluator != null, trackFormats); - drmInitData = getDrmInitData(adaptationSet); - return; - } - } - } - trackGroup = null; - } - - private void processManifest(MediaPresentationDescription newManifest) { - try { - currentManifest = newManifest; - long periodDurationUs = getPeriodDurationUs(currentManifest, 0); - List representations = currentManifest.getPeriod(0).adaptationSets - .get(adaptationSetIndex).representations; - for (int i = 0; i < representationHolders.length; i++) { - Representation representation = representations.get(i); - representationHolders[i].updateRepresentation(periodDurationUs, representation); - } - } catch (BehindLiveWindowException e) { - fatalError = e; - } - } - private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri, Representation representation, ChunkExtractorWrapper extractor, DataSource dataSource, int trigger) { diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java index 2526edfa02..f16bc35659 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java @@ -17,23 +17,29 @@ package com.google.android.exoplayer.dash; import com.google.android.exoplayer.C; import com.google.android.exoplayer.DefaultLoadControl; +import com.google.android.exoplayer.Format; import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackGroup; import com.google.android.exoplayer.TrackGroupArray; import com.google.android.exoplayer.TrackSelection; import com.google.android.exoplayer.TrackStream; -import com.google.android.exoplayer.chunk.ChunkSampleSource; -import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener; +import com.google.android.exoplayer.chunk.ChunkTrackStream; +import com.google.android.exoplayer.chunk.ChunkTrackStreamEventListener; +import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; +import com.google.android.exoplayer.dash.mpd.AdaptationSet; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; +import com.google.android.exoplayer.dash.mpd.Period; +import com.google.android.exoplayer.dash.mpd.Representation; import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSourceFactory; import com.google.android.exoplayer.upstream.DefaultAllocator; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.ManifestFetcher; +import com.google.android.exoplayer.util.Util; import android.net.Uri; import android.os.Handler; @@ -41,62 +47,47 @@ import android.os.SystemClock; import android.util.Pair; import java.io.IOException; -import java.util.ArrayList; -import java.util.IdentityHashMap; +import java.util.Arrays; import java.util.List; /** - * Combines multiple {@link SampleSource} instances. + * A {@link SampleSource} for DASH media. */ public final class DashSampleSource implements SampleSource { private final ManifestFetcher manifestFetcher; - private final DashChunkSource[] chunkSources; - private final ChunkSampleSource[] sources; - private final IdentityHashMap trackStreamSources; - private final int[] selectedTrackCounts; + private final DataSourceFactory dataSourceFactory; + private final BandwidthMeter bandwidthMeter; + private final Handler eventHandler; + private final ChunkTrackStreamEventListener eventListener; + private final LoadControl loadControl; - private MediaPresentationDescription currentManifest; private boolean prepared; - private boolean seenFirstTrackSelection; private long durationUs; + private MediaPresentationDescription currentManifest; private TrackGroupArray trackGroups; - private ChunkSampleSource[] enabledSources; + private int[] trackGroupAdaptationSetIndices; + private boolean pendingReset; + private long lastSeekPositionUs; + + private DashChunkSource[] chunkSources; + private ChunkTrackStream[] trackStreams; public DashSampleSource(Uri uri, DataSourceFactory dataSourceFactory, BandwidthMeter bandwidthMeter, Handler eventHandler, - ChunkSampleSourceEventListener eventListener) { + ChunkTrackStreamEventListener eventListener) { + this.dataSourceFactory = dataSourceFactory; + this.bandwidthMeter = bandwidthMeter; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + + loadControl = new DefaultLoadControl(new DefaultAllocator(C.DEFAULT_BUFFER_SEGMENT_SIZE)); + chunkSources = new DashChunkSource[0]; + trackStreams = new ChunkTrackStream[0]; + MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); DataSource manifestDataSource = dataSourceFactory.createDataSource(); manifestFetcher = new ManifestFetcher<>(uri, manifestDataSource, parser); - - LoadControl loadControl = new DefaultLoadControl( - new DefaultAllocator(C.DEFAULT_BUFFER_SEGMENT_SIZE)); - - // Build the video renderer. - DataSource videoDataSource = dataSourceFactory.createDataSource(bandwidthMeter); - DashChunkSource videoChunkSource = new DashChunkSource(C.TRACK_TYPE_VIDEO, videoDataSource, - new AdaptiveEvaluator(bandwidthMeter)); - ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, - C.DEFAULT_VIDEO_BUFFER_SIZE, eventHandler, eventListener, C.TRACK_TYPE_VIDEO); - - // Build the audio renderer. - DataSource audioDataSource = dataSourceFactory.createDataSource(bandwidthMeter); - DashChunkSource audioChunkSource = new DashChunkSource(C.TRACK_TYPE_AUDIO, audioDataSource, - null); - ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, - C.DEFAULT_AUDIO_BUFFER_SIZE, eventHandler, eventListener, C.TRACK_TYPE_AUDIO); - - // Build the text renderer. - DataSource textDataSource = dataSourceFactory.createDataSource(bandwidthMeter); - DashChunkSource textChunkSource = new DashChunkSource(C.TRACK_TYPE_TEXT, textDataSource, null); - ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, - C.DEFAULT_TEXT_BUFFER_SIZE, eventHandler, eventListener, C.TRACK_TYPE_TEXT); - - chunkSources = new DashChunkSource[] {videoChunkSource, audioChunkSource, textChunkSource}; - sources = new ChunkSampleSource[] {videoSampleSource, audioSampleSource, textSampleSource}; - trackStreamSources = new IdentityHashMap<>(); - selectedTrackCounts = new int[sources.length]; } @Override @@ -111,30 +102,12 @@ public final class DashSampleSource implements SampleSource { manifestFetcher.maybeThrowError(); manifestFetcher.requestRefresh(); return false; - } else { - durationUs = currentManifest.dynamic ? C.UNSET_TIME_US : currentManifest.duration * 1000; - for (DashChunkSource chunkSource : chunkSources) { - chunkSource.init(currentManifest); - } } } - for (ChunkSampleSource source : sources) { - source.prepare(); - } - int totalTrackGroupCount = 0; - for (ChunkSampleSource source : sources) { - totalTrackGroupCount += source.getTrackGroups().length; - } - TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount]; - int trackGroupIndex = 0; - for (ChunkSampleSource source : sources) { - int sourceTrackGroupCount = source.getTrackGroups().length; - for (int j = 0; j < sourceTrackGroupCount; j++) { - trackGroupArray[trackGroupIndex++] = source.getTrackGroups().get(j); - } - } - trackGroups = new TrackGroupArray(trackGroupArray); + durationUs = currentManifest.dynamic ? C.UNSET_TIME_US : currentManifest.duration * 1000; + buildTrackGroups(currentManifest); + prepared = true; return true; } @@ -153,26 +126,37 @@ public final class DashSampleSource implements SampleSource { public TrackStream[] selectTracks(List oldStreams, List newSelections, long positionUs) { Assertions.checkState(prepared); - TrackStream[] newStreams = new TrackStream[newSelections.size()]; - // Select tracks for each source. - int enabledSourceCount = 0; - for (int i = 0; i < sources.length; i++) { - selectedTrackCounts[i] += selectTracks(sources[i], oldStreams, newSelections, positionUs, - newStreams); - if (selectedTrackCounts[i] > 0) { - enabledSourceCount++; + + int newEnabledSourceCount = trackStreams.length + newSelections.size() - oldStreams.size(); + DashChunkSource[] newChunkSources = new DashChunkSource[newEnabledSourceCount]; + ChunkTrackStream[] newTrackStreams = new ChunkTrackStream[newEnabledSourceCount]; + int newEnabledSourceIndex = 0; + + // Iterate over currently enabled streams, either releasing them or adding them to the new list. + for (int i = 0; i < trackStreams.length; i++) { + ChunkTrackStream trackStream = trackStreams[i]; + if (oldStreams.contains(trackStream)) { + chunkSources[i].release(); + trackStream.release(); + } else { + newChunkSources[newEnabledSourceIndex] = chunkSources[i]; + newTrackStreams[newEnabledSourceIndex++] = trackStream; } } - // Update the enabled sources. - enabledSources = new ChunkSampleSource[enabledSourceCount]; - enabledSourceCount = 0; - for (int i = 0; i < sources.length; i++) { - if (selectedTrackCounts[i] > 0) { - enabledSources[enabledSourceCount++] = sources[i]; - } + + // Instantiate and return new streams. + TrackStream[] streamsToReturn = new TrackStream[newSelections.size()]; + for (int i = 0; i < newSelections.size(); i++) { + Pair trackComponents = + buildTrackStream(newSelections.get(i), positionUs); + newChunkSources[newEnabledSourceIndex] = trackComponents.first; + newTrackStreams[newEnabledSourceIndex++] = trackComponents.second; + streamsToReturn[i] = trackComponents.second; } - seenFirstTrackSelection = true; - return newStreams; + + chunkSources = newChunkSources; + trackStreams = newTrackStreams; + return streamsToReturn; } @Override @@ -201,103 +185,102 @@ public final class DashSampleSource implements SampleSource { } } - for (ChunkSampleSource source : enabledSources) { - source.continueBuffering(positionUs); + for (ChunkTrackStream trackStream : trackStreams) { + trackStream.continueBuffering(positionUs); } } @Override public long readReset() { - long resetPositionUs = C.UNSET_TIME_US; - for (ChunkSampleSource source : enabledSources) { - long childResetPositionUs = source.readReset(); - if (resetPositionUs == C.UNSET_TIME_US) { - resetPositionUs = childResetPositionUs; - } else if (childResetPositionUs != C.UNSET_TIME_US) { - resetPositionUs = Math.min(resetPositionUs, childResetPositionUs); + if (pendingReset) { + pendingReset = false; + for (ChunkTrackStream trackStream : trackStreams) { + trackStream.setReadingEnabled(true); } + return lastSeekPositionUs; } - return resetPositionUs; + return C.UNSET_TIME_US; } @Override public long getBufferedPositionUs() { - long bufferedPositionUs = durationUs != C.UNSET_TIME_US ? durationUs : Long.MAX_VALUE; - for (ChunkSampleSource source : enabledSources) { - long rendererBufferedPositionUs = source.getBufferedPositionUs(); - if (rendererBufferedPositionUs == C.UNSET_TIME_US) { - return C.UNSET_TIME_US; - } else if (rendererBufferedPositionUs == C.END_OF_SOURCE_US) { - // This source is fully buffered. - } else { + long bufferedPositionUs = Long.MAX_VALUE; + for (ChunkTrackStream trackStream : trackStreams) { + long rendererBufferedPositionUs = trackStream.getBufferedPositionUs(); + if (rendererBufferedPositionUs != C.END_OF_SOURCE_US) { bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs); } } - return bufferedPositionUs == Long.MAX_VALUE ? C.UNSET_TIME_US : bufferedPositionUs; + return bufferedPositionUs == Long.MAX_VALUE ? C.END_OF_SOURCE_US : bufferedPositionUs; } @Override public void seekToUs(long positionUs) { - for (ChunkSampleSource source : enabledSources) { - source.seekToUs(positionUs); + lastSeekPositionUs = positionUs; + pendingReset = true; + for (ChunkTrackStream trackStream : trackStreams) { + trackStream.setReadingEnabled(false); + trackStream.seekToUs(positionUs); } } @Override public void release() { manifestFetcher.release(); - for (ChunkSampleSource source : sources) { - source.release(); + for (DashChunkSource chunkSource : chunkSources) { + chunkSource.release(); + } + for (ChunkTrackStream trackStream : trackStreams) { + trackStream.release(); } } // Internal methods. - private int selectTracks(ChunkSampleSource source, List allOldStreams, - List allNewSelections, long positionUs, TrackStream[] allNewStreams) { - // Get the subset of the old streams for the source. - ArrayList oldStreams = new ArrayList<>(); - for (int i = 0; i < allOldStreams.size(); i++) { - TrackStream stream = allOldStreams.get(i); - if (trackStreamSources.get(stream) == source) { - trackStreamSources.remove(stream); - oldStreams.add(stream); + private void buildTrackGroups(MediaPresentationDescription manifest) { + Period period = manifest.getPeriod(0); + int trackGroupCount = 0; + trackGroupAdaptationSetIndices = new int[period.adaptationSets.size()]; + TrackGroup[] trackGroupArray = new TrackGroup[period.adaptationSets.size()]; + for (int i = 0; i < period.adaptationSets.size(); i++) { + AdaptationSet adaptationSet = period.adaptationSets.get(i); + int adaptationSetType = adaptationSet.type; + List representations = adaptationSet.representations; + if (!representations.isEmpty() && (adaptationSetType == C.TRACK_TYPE_AUDIO + || adaptationSetType == C.TRACK_TYPE_VIDEO || adaptationSetType == C.TRACK_TYPE_TEXT)) { + Format[] formats = new Format[representations.size()]; + for (int j = 0; j < formats.length; j++) { + formats[j] = representations.get(j).format; + } + trackGroupAdaptationSetIndices[trackGroupCount] = i; + boolean adaptive = adaptationSetType == C.TRACK_TYPE_VIDEO; + trackGroupArray[trackGroupCount++] = new TrackGroup(adaptive, formats); } } - // Get the subset of the new selections for the source. - ArrayList newSelections = new ArrayList<>(); - int[] newSelectionOriginalIndices = new int[allNewSelections.size()]; - for (int i = 0; i < allNewSelections.size(); i++) { - TrackSelection selection = allNewSelections.get(i); - Pair sourceAndGroup = getSourceAndGroup(selection.group); - if (sourceAndGroup.first == source) { - newSelectionOriginalIndices[newSelections.size()] = i; - newSelections.add(new TrackSelection(sourceAndGroup.second, selection.getTracks())); - } + if (trackGroupCount < trackGroupArray.length) { + trackGroupAdaptationSetIndices = Arrays.copyOf(trackGroupAdaptationSetIndices, + trackGroupCount); + trackGroupArray = Arrays.copyOf(trackGroupArray, trackGroupCount); } - // Do nothing if nothing has changed, except during the first selection. - if (seenFirstTrackSelection && oldStreams.isEmpty() && newSelections.isEmpty()) { - return 0; - } - // Perform the selection. - TrackStream[] newStreams = source.selectTracks(oldStreams, newSelections, positionUs); - for (int j = 0; j < newStreams.length; j++) { - allNewStreams[newSelectionOriginalIndices[j]] = newStreams[j]; - trackStreamSources.put(newStreams[j], source); - } - return newSelections.size() - oldStreams.size(); + trackGroups = new TrackGroupArray(trackGroupArray); } - private Pair getSourceAndGroup(int group) { - int totalTrackGroupCount = 0; - for (ChunkSampleSource source : sources) { - int sourceTrackGroupCount = source.getTrackGroups().length; - if (group < totalTrackGroupCount + sourceTrackGroupCount) { - return Pair.create(source, group - totalTrackGroupCount); - } - totalTrackGroupCount += sourceTrackGroupCount; - } - throw new IndexOutOfBoundsException(); + private Pair buildTrackStream(TrackSelection selection, + long positionUs) { + int[] selectedTracks = selection.getTracks(); + FormatEvaluator adaptiveEvaluator = selectedTracks.length > 1 + ? new AdaptiveEvaluator(bandwidthMeter) : null; + int adaptationSetIndex = trackGroupAdaptationSetIndices[selection.group]; + AdaptationSet adaptationSet = currentManifest.getPeriod(0).adaptationSets.get( + adaptationSetIndex); + int adaptationSetType = adaptationSet.type; + int bufferSize = Util.getDefaultBufferSize(adaptationSetType); + DataSource dataSource = dataSourceFactory.createDataSource(bandwidthMeter); + DashChunkSource chunkSource = new DashChunkSource(currentManifest, adaptationSetIndex, + trackGroups.get(selection.group), selectedTracks, dataSource, adaptiveEvaluator); + ChunkTrackStream trackStream = new ChunkTrackStream(chunkSource, loadControl, bufferSize, + positionUs, eventHandler, eventListener, adaptationSetType); + return Pair.create(chunkSource, trackStream); } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index a35236fa50..75c978a3d7 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -27,8 +27,8 @@ import com.google.android.exoplayer.TrackSelection; import com.google.android.exoplayer.TrackStream; import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.ChunkHolder; -import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener; -import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener.EventDispatcher; +import com.google.android.exoplayer.chunk.ChunkTrackStreamEventListener; +import com.google.android.exoplayer.chunk.ChunkTrackStreamEventListener.EventDispatcher; import com.google.android.exoplayer.extractor.DefaultTrackOutput; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; @@ -109,7 +109,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { */ public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, int bufferSizeContribution, Handler eventHandler, - ChunkSampleSourceEventListener eventListener, int eventSourceId) { + ChunkTrackStreamEventListener eventListener, int eventSourceId) { this(chunkSource, loadControl, bufferSizeContribution, eventHandler, eventListener, eventSourceId, DEFAULT_MIN_LOADABLE_RETRY_COUNT); } @@ -127,7 +127,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { */ public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, int bufferSizeContribution, Handler eventHandler, - ChunkSampleSourceEventListener eventListener, int eventSourceId, int minLoadableRetryCount) { + ChunkTrackStreamEventListener eventListener, int eventSourceId, int minLoadableRetryCount) { this.chunkSource = chunkSource; this.loadControl = loadControl; this.bufferSizeContribution = bufferSizeContribution; diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index f5427d8950..bc4ada7ca4 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -46,86 +46,75 @@ import java.util.List; /** * An {@link ChunkSource} for SmoothStreaming. */ -// TODO[REFACTOR]: Handle multiple stream elements of the same type (at a higher level). public class SmoothStreamingChunkSource implements ChunkSource { - private final int streamElementType; + private final int elementIndex; + private final TrackGroup trackGroup; + private final ChunkExtractorWrapper[] extractorWrappers; + private final Format[] enabledFormats; + private final boolean[] adaptiveFormatBlacklistFlags; private final DataSource dataSource; private final Evaluation evaluation; private final FormatEvaluator adaptiveFormatEvaluator; - private SmoothStreamingManifest currentManifest; - private TrackEncryptionBox[] trackEncryptionBoxes; + private SmoothStreamingManifest manifest; private DrmInitData drmInitData; private int currentManifestChunkOffset; private boolean needManifestRefresh; - // Properties of exposed tracks. - private int elementIndex; - private TrackGroup trackGroup; - private ChunkExtractorWrapper[] extractorWrappers; - - // Properties of enabled tracks. - private Format[] enabledFormats; - private boolean[] adaptiveFormatBlacklistFlags; - private IOException fatalError; /** - * @param streamElementType The type of stream element exposed by this source. One of - * {@link C#TRACK_TYPE_VIDEO}, {@link C#TRACK_TYPE_AUDIO} and {@link C#TRACK_TYPE_TEXT}. + * @param manifest The initial manifest. + * @param elementIndex The index of the stream element in the manifest. + * @param trackGroup The track group corresponding to the stream element. + * @param tracks The indices of the selected tracks within the stream element. * @param dataSource A {@link DataSource} suitable for loading the media data. * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. + * @param trackEncryptionBoxes Track encryption boxes for the stream. + * @param drmInitData Drm initialization data for the stream. */ - public SmoothStreamingChunkSource(int streamElementType, DataSource dataSource, - FormatEvaluator adaptiveFormatEvaluator) { - this.streamElementType = streamElementType; + public SmoothStreamingChunkSource(SmoothStreamingManifest manifest, int elementIndex, + TrackGroup trackGroup, int[] tracks, DataSource dataSource, + FormatEvaluator adaptiveFormatEvaluator, TrackEncryptionBox[] trackEncryptionBoxes, + DrmInitData drmInitData) { + this.manifest = manifest; + this.elementIndex = elementIndex; + this.trackGroup = trackGroup; this.dataSource = dataSource; this.adaptiveFormatEvaluator = adaptiveFormatEvaluator; - evaluation = new Evaluation(); - } - - public boolean needManifestRefresh() { - return needManifestRefresh; - } - - // ChunkSource implementation. - - @Override - public void maybeThrowError() throws IOException { - if (fatalError != null) { - throw fatalError; - } - } - - public void init(SmoothStreamingManifest initialManifest, - TrackEncryptionBox[] trackEncryptionBoxes, DrmInitData drmInitData) { - this.currentManifest = initialManifest; - this.trackEncryptionBoxes = trackEncryptionBoxes; this.drmInitData = drmInitData; - initForManifest(currentManifest); - } + this.evaluation = new Evaluation(); - @Override - public final TrackGroup getTracks() { - return trackGroup; - } + StreamElement streamElement = manifest.streamElements[elementIndex]; + Format[] formats = streamElement.formats; + extractorWrappers = new ChunkExtractorWrapper[formats.length]; + for (int j = 0; j < formats.length; j++) { + int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : -1; + Track track = new Track(j, streamElement.type, streamElement.timescale, C.UNSET_TIME_US, + manifest.durationUs, formats[j], trackEncryptionBoxes, nalUnitLengthFieldLength, + null, null); + FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( + FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME + | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track); + extractorWrappers[j] = new ChunkExtractorWrapper(extractor); + } - @Override - public void enable(int[] tracks) { enabledFormats = new Format[tracks.length]; for (int i = 0; i < tracks.length; i++) { enabledFormats[i] = trackGroup.getFormat(tracks[i]); } Arrays.sort(enabledFormats, new DecreasingBandwidthComparator()); - if (enabledFormats.length > 1) { + if (adaptiveFormatEvaluator != null) { adaptiveFormatEvaluator.enable(enabledFormats); adaptiveFormatBlacklistFlags = new boolean[tracks.length]; + } else { + adaptiveFormatBlacklistFlags = null; } } public void updateManifest(SmoothStreamingManifest newManifest) { - StreamElement currentElement = currentManifest.streamElements[elementIndex]; + StreamElement currentElement = manifest.streamElements[elementIndex]; int currentElementChunkCount = currentElement.chunkCount; StreamElement newElement = newManifest.streamElements[elementIndex]; if (currentElementChunkCount == 0 || newElement.chunkCount == 0) { @@ -143,10 +132,29 @@ public class SmoothStreamingChunkSource implements ChunkSource { currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs); } } - currentManifest = newManifest; + manifest = newManifest; needManifestRefresh = false; } + public boolean needManifestRefresh() { + return needManifestRefresh; + } + + public void release() { + if (adaptiveFormatEvaluator != null) { + adaptiveFormatEvaluator.disable(); + } + } + + // ChunkSource implementation. + + @Override + public void maybeThrowError() throws IOException { + if (fatalError != null) { + throw fatalError; + } + } + @Override public int getPreferredQueueSize(long playbackPositionUs, List queue) { if (fatalError != null || enabledFormats.length < 2) { @@ -176,9 +184,9 @@ public class SmoothStreamingChunkSource implements ChunkSource { return; } - StreamElement streamElement = currentManifest.streamElements[elementIndex]; + StreamElement streamElement = manifest.streamElements[elementIndex]; if (streamElement.chunkCount == 0) { - if (currentManifest.isLive) { + if (manifest.isLive) { needManifestRefresh = true; } else { out.endOfStream = true; @@ -198,10 +206,10 @@ public class SmoothStreamingChunkSource implements ChunkSource { } } - needManifestRefresh = currentManifest.isLive && chunkIndex >= streamElement.chunkCount - 1; + needManifestRefresh = manifest.isLive && chunkIndex >= streamElement.chunkCount - 1; if (chunkIndex >= streamElement.chunkCount) { // This is beyond the last chunk in the current manifest. - out.endOfStream = !currentManifest.isLive; + out.endOfStream = !manifest.isLive; return; } @@ -231,45 +239,8 @@ public class SmoothStreamingChunkSource implements ChunkSource { return false; } - @Override - public void disable() { - if (enabledFormats.length > 1) { - adaptiveFormatEvaluator.disable(); - } - evaluation.clear(); - fatalError = null; - } - // Private methods. - private void initForManifest(SmoothStreamingManifest manifest) { - for (int i = 0; i < manifest.streamElements.length; i++) { - if (manifest.streamElements[i].type == streamElementType) { - Format[] formats = manifest.streamElements[i].formats; - if (formats.length > 0) { - // We've found an element of the desired type. - long timescale = manifest.streamElements[i].timescale; - extractorWrappers = new ChunkExtractorWrapper[formats.length]; - for (int j = 0; j < formats.length; j++) { - int nalUnitLengthFieldLength = streamElementType == C.TRACK_TYPE_VIDEO ? 4 : -1; - Track track = new Track(j, streamElementType, timescale, C.UNSET_TIME_US, - manifest.durationUs, formats[j], trackEncryptionBoxes, nalUnitLengthFieldLength, - null, null); - FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( - FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME - | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track); - extractorWrappers[j] = new ChunkExtractorWrapper(extractor); - } - elementIndex = i; - trackGroup = new TrackGroup(adaptiveFormatEvaluator != null, formats); - return; - } - } - } - extractorWrappers = null; - trackGroup = null; - } - /** * Gets the index of a format in a track group, using referential equality. */ diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java index f95cc398e2..cfe4e4d50e 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java @@ -17,19 +17,22 @@ package com.google.android.exoplayer.smoothstreaming; import com.google.android.exoplayer.C; import com.google.android.exoplayer.DefaultLoadControl; +import com.google.android.exoplayer.Format; import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackGroup; import com.google.android.exoplayer.TrackGroupArray; import com.google.android.exoplayer.TrackSelection; import com.google.android.exoplayer.TrackStream; -import com.google.android.exoplayer.chunk.ChunkSampleSource; -import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener; +import com.google.android.exoplayer.chunk.ChunkTrackStream; +import com.google.android.exoplayer.chunk.ChunkTrackStreamEventListener; +import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; import com.google.android.exoplayer.drm.DrmInitData; import com.google.android.exoplayer.drm.DrmInitData.SchemeInitData; import com.google.android.exoplayer.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement; +import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSourceFactory; @@ -46,69 +49,55 @@ import android.util.Base64; import android.util.Pair; import java.io.IOException; -import java.util.ArrayList; -import java.util.IdentityHashMap; +import java.util.Arrays; import java.util.List; /** - * Combines multiple {@link SampleSource} instances. + * A {@link SampleSource} for SmoothStreaming media. */ public final class SmoothStreamingSampleSource implements SampleSource { private static final int MINIMUM_MANIFEST_REFRESH_PERIOD_MS = 5000; private static final int INITIALIZATION_VECTOR_SIZE = 8; + private final DataSourceFactory dataSourceFactory; + private final BandwidthMeter bandwidthMeter; + private final Handler eventHandler; + private final ChunkTrackStreamEventListener eventListener; + private final LoadControl loadControl; private final ManifestFetcher manifestFetcher; - private final SmoothStreamingChunkSource[] chunkSources; - private final ChunkSampleSource[] sources; - private final IdentityHashMap trackStreamSources; - private final int[] selectedTrackCounts; - private SmoothStreamingManifest currentManifest; private boolean prepared; - private boolean seenFirstTrackSelection; private long durationUs; + private SmoothStreamingManifest currentManifest; + private TrackEncryptionBox[] trackEncryptionBoxes; + private DrmInitData.Mapped drmInitData; private TrackGroupArray trackGroups; - private ChunkSampleSource[] enabledSources; + private int[] trackGroupElementIndices; + private boolean pendingReset; + private long lastSeekPositionUs; + + private SmoothStreamingChunkSource[] chunkSources; + private ChunkTrackStream[] trackStreams; public SmoothStreamingSampleSource(Uri uri, DataSourceFactory dataSourceFactory, BandwidthMeter bandwidthMeter, Handler eventHandler, - ChunkSampleSourceEventListener eventListener) { + ChunkTrackStreamEventListener eventListener) { + this.dataSourceFactory = dataSourceFactory; + this.bandwidthMeter = bandwidthMeter; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + + loadControl = new DefaultLoadControl(new DefaultAllocator(C.DEFAULT_BUFFER_SEGMENT_SIZE)); + chunkSources = new SmoothStreamingChunkSource[0]; + trackStreams = new ChunkTrackStream[0]; + if (!Util.toLowerInvariant(uri.getLastPathSegment()).equals("manifest")) { uri = Uri.withAppendedPath(uri, "Manifest"); } SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); DataSource manifestDataSource = dataSourceFactory.createDataSource(); manifestFetcher = new ManifestFetcher<>(uri, manifestDataSource, parser); - LoadControl loadControl = new DefaultLoadControl( - new DefaultAllocator(C.DEFAULT_BUFFER_SEGMENT_SIZE)); - - // Build the video renderer. - DataSource videoDataSource = dataSourceFactory.createDataSource(bandwidthMeter); - SmoothStreamingChunkSource videoChunkSource = new SmoothStreamingChunkSource(C.TRACK_TYPE_VIDEO, - videoDataSource, new AdaptiveEvaluator(bandwidthMeter)); - ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, - C.DEFAULT_VIDEO_BUFFER_SIZE, eventHandler, eventListener, C.TRACK_TYPE_VIDEO); - - // Build the audio renderer. - DataSource audioDataSource = dataSourceFactory.createDataSource(bandwidthMeter); - SmoothStreamingChunkSource audioChunkSource = new SmoothStreamingChunkSource(C.TRACK_TYPE_AUDIO, - audioDataSource, null); - ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, - C.DEFAULT_AUDIO_BUFFER_SIZE, eventHandler, eventListener, C.TRACK_TYPE_AUDIO); - - // Build the text renderer. - DataSource textDataSource = dataSourceFactory.createDataSource(bandwidthMeter); - SmoothStreamingChunkSource textChunkSource = new SmoothStreamingChunkSource(C.TRACK_TYPE_TEXT, - textDataSource, null); - ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl, - C.DEFAULT_TEXT_BUFFER_SIZE, eventHandler, eventListener, C.TRACK_TYPE_TEXT); - - chunkSources = new SmoothStreamingChunkSource[] {videoChunkSource, audioChunkSource, - textChunkSource}; - sources = new ChunkSampleSource[] {videoSampleSource, audioSampleSource, textSampleSource}; - trackStreamSources = new IdentityHashMap<>(); - selectedTrackCounts = new int[sources.length]; } @Override @@ -123,46 +112,22 @@ public final class SmoothStreamingSampleSource implements SampleSource { manifestFetcher.maybeThrowError(); manifestFetcher.requestRefresh(); return false; - } else { - durationUs = currentManifest.durationUs; - - TrackEncryptionBox[] trackEncryptionBoxes; - DrmInitData.Mapped drmInitData; - ProtectionElement protectionElement = currentManifest.protectionElement; - if (protectionElement != null) { - byte[] keyId = getProtectionElementKeyId(protectionElement.data); - trackEncryptionBoxes = new TrackEncryptionBox[1]; - trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId); - drmInitData = new DrmInitData.Mapped(); - drmInitData.put(protectionElement.uuid, - new SchemeInitData(MimeTypes.VIDEO_MP4, protectionElement.data)); - } else { - trackEncryptionBoxes = null; - drmInitData = null; - } - - for (SmoothStreamingChunkSource chunkSource : chunkSources) { - chunkSource.init(currentManifest, trackEncryptionBoxes, drmInitData); - } } } - for (ChunkSampleSource source : sources) { - source.prepare(); + durationUs = currentManifest.durationUs; + buildTrackGroups(currentManifest); + + ProtectionElement protectionElement = currentManifest.protectionElement; + if (protectionElement != null) { + byte[] keyId = getProtectionElementKeyId(protectionElement.data); + trackEncryptionBoxes = new TrackEncryptionBox[1]; + trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId); + drmInitData = new DrmInitData.Mapped(); + drmInitData.put(protectionElement.uuid, + new SchemeInitData(MimeTypes.VIDEO_MP4, protectionElement.data)); } - int totalTrackGroupCount = 0; - for (ChunkSampleSource source : sources) { - totalTrackGroupCount += source.getTrackGroups().length; - } - TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount]; - int trackGroupIndex = 0; - for (ChunkSampleSource source : sources) { - int sourceTrackGroupCount = source.getTrackGroups().length; - for (int j = 0; j < sourceTrackGroupCount; j++) { - trackGroupArray[trackGroupIndex++] = source.getTrackGroups().get(j); - } - } - trackGroups = new TrackGroupArray(trackGroupArray); + prepared = true; return true; } @@ -181,26 +146,38 @@ public final class SmoothStreamingSampleSource implements SampleSource { public TrackStream[] selectTracks(List oldStreams, List newSelections, long positionUs) { Assertions.checkState(prepared); - TrackStream[] newStreams = new TrackStream[newSelections.size()]; - // Select tracks for each source. - int enabledSourceCount = 0; - for (int i = 0; i < sources.length; i++) { - selectedTrackCounts[i] += selectTracks(sources[i], oldStreams, newSelections, positionUs, - newStreams); - if (selectedTrackCounts[i] > 0) { - enabledSourceCount++; + + int newEnabledSourceCount = trackStreams.length + newSelections.size() - oldStreams.size(); + SmoothStreamingChunkSource[] newChunkSources = + new SmoothStreamingChunkSource[newEnabledSourceCount]; + ChunkTrackStream[] newTrackStreams = new ChunkTrackStream[newEnabledSourceCount]; + int newEnabledSourceIndex = 0; + + // Iterate over currently enabled streams, either releasing them or adding them to the new list. + for (int i = 0; i < trackStreams.length; i++) { + ChunkTrackStream trackStream = trackStreams[i]; + if (oldStreams.contains(trackStream)) { + chunkSources[i].release(); + trackStream.release(); + } else { + newChunkSources[newEnabledSourceIndex] = chunkSources[i]; + newTrackStreams[newEnabledSourceIndex++] = trackStream; } } - // Update the enabled sources. - enabledSources = new ChunkSampleSource[enabledSourceCount]; - enabledSourceCount = 0; - for (int i = 0; i < sources.length; i++) { - if (selectedTrackCounts[i] > 0) { - enabledSources[enabledSourceCount++] = sources[i]; - } + + // Instantiate and return new streams. + TrackStream[] streamsToReturn = new TrackStream[newSelections.size()]; + for (int i = 0; i < newSelections.size(); i++) { + Pair trackComponents = + buildTrackStream(newSelections.get(i), positionUs); + newChunkSources[newEnabledSourceIndex] = trackComponents.first; + newTrackStreams[newEnabledSourceIndex++] = trackComponents.second; + streamsToReturn[i] = trackComponents.second; } - seenFirstTrackSelection = true; - return newStreams; + + chunkSources = newChunkSources; + trackStreams = newTrackStreams; + return streamsToReturn; } @Override @@ -225,103 +202,96 @@ public final class SmoothStreamingSampleSource implements SampleSource { } } - for (ChunkSampleSource source : enabledSources) { - source.continueBuffering(positionUs); + for (ChunkTrackStream trackStream : trackStreams) { + trackStream.continueBuffering(positionUs); } } @Override public long readReset() { - long resetPositionUs = C.UNSET_TIME_US; - for (ChunkSampleSource source : enabledSources) { - long childResetPositionUs = source.readReset(); - if (resetPositionUs == C.UNSET_TIME_US) { - resetPositionUs = childResetPositionUs; - } else if (childResetPositionUs != C.UNSET_TIME_US) { - resetPositionUs = Math.min(resetPositionUs, childResetPositionUs); + if (pendingReset) { + pendingReset = false; + for (ChunkTrackStream trackStream : trackStreams) { + trackStream.setReadingEnabled(true); } + return lastSeekPositionUs; } - return resetPositionUs; + return C.UNSET_TIME_US; } @Override public long getBufferedPositionUs() { - long bufferedPositionUs = durationUs != C.UNSET_TIME_US ? durationUs : Long.MAX_VALUE; - for (ChunkSampleSource source : enabledSources) { - long rendererBufferedPositionUs = source.getBufferedPositionUs(); - if (rendererBufferedPositionUs == C.UNSET_TIME_US) { - return C.UNSET_TIME_US; - } else if (rendererBufferedPositionUs == C.END_OF_SOURCE_US) { - // This source is fully buffered. - } else { + long bufferedPositionUs = Long.MAX_VALUE; + for (ChunkTrackStream trackStream : trackStreams) { + long rendererBufferedPositionUs = trackStream.getBufferedPositionUs(); + if (rendererBufferedPositionUs != C.END_OF_SOURCE_US) { bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs); } } - return bufferedPositionUs == Long.MAX_VALUE ? C.UNSET_TIME_US : bufferedPositionUs; + return bufferedPositionUs == Long.MAX_VALUE ? C.END_OF_SOURCE_US : bufferedPositionUs; } @Override public void seekToUs(long positionUs) { - for (ChunkSampleSource source : enabledSources) { - source.seekToUs(positionUs); + lastSeekPositionUs = positionUs; + pendingReset = true; + for (ChunkTrackStream trackStream : trackStreams) { + trackStream.setReadingEnabled(false); + trackStream.seekToUs(positionUs); } } @Override public void release() { manifestFetcher.release(); - for (ChunkSampleSource source : sources) { - source.release(); + for (SmoothStreamingChunkSource chunkSource : chunkSources) { + chunkSource.release(); + } + for (ChunkTrackStream trackStream : trackStreams) { + trackStream.release(); } } // Internal methods. - private int selectTracks(ChunkSampleSource source, List allOldStreams, - List allNewSelections, long positionUs, TrackStream[] allNewStreams) { - // Get the subset of the old streams for the source. - ArrayList oldStreams = new ArrayList<>(); - for (int i = 0; i < allOldStreams.size(); i++) { - TrackStream stream = allOldStreams.get(i); - if (trackStreamSources.get(stream) == source) { - trackStreamSources.remove(stream); - oldStreams.add(stream); + private void buildTrackGroups(SmoothStreamingManifest manifest) { + int trackGroupCount = 0; + trackGroupElementIndices = new int[manifest.streamElements.length]; + TrackGroup[] trackGroupArray = new TrackGroup[manifest.streamElements.length]; + for (int i = 0; i < manifest.streamElements.length; i++) { + StreamElement streamElement = manifest.streamElements[i]; + int streamElementType = streamElement.type; + Format[] formats = streamElement.formats; + if (formats.length > 0 && (streamElementType == C.TRACK_TYPE_AUDIO + || streamElementType == C.TRACK_TYPE_VIDEO || streamElementType == C.TRACK_TYPE_TEXT)) { + trackGroupElementIndices[trackGroupCount] = i; + boolean adaptive = streamElementType == C.TRACK_TYPE_VIDEO; + trackGroupArray[trackGroupCount++] = new TrackGroup(adaptive, formats); } } - // Get the subset of the new selections for the source. - ArrayList newSelections = new ArrayList<>(); - int[] newSelectionOriginalIndices = new int[allNewSelections.size()]; - for (int i = 0; i < allNewSelections.size(); i++) { - TrackSelection selection = allNewSelections.get(i); - Pair sourceAndGroup = getSourceAndGroup(selection.group); - if (sourceAndGroup.first == source) { - newSelectionOriginalIndices[newSelections.size()] = i; - newSelections.add(new TrackSelection(sourceAndGroup.second, selection.getTracks())); - } + if (trackGroupCount < trackGroupArray.length) { + trackGroupElementIndices = Arrays.copyOf(trackGroupElementIndices, trackGroupCount); + trackGroupArray = Arrays.copyOf(trackGroupArray, trackGroupCount); } - // Do nothing if nothing has changed, except during the first selection. - if (seenFirstTrackSelection && oldStreams.isEmpty() && newSelections.isEmpty()) { - return 0; - } - // Perform the selection. - TrackStream[] newStreams = source.selectTracks(oldStreams, newSelections, positionUs); - for (int j = 0; j < newStreams.length; j++) { - allNewStreams[newSelectionOriginalIndices[j]] = newStreams[j]; - trackStreamSources.put(newStreams[j], source); - } - return newSelections.size() - oldStreams.size(); + trackGroups = new TrackGroupArray(trackGroupArray); } - private Pair getSourceAndGroup(int group) { - int totalTrackGroupCount = 0; - for (ChunkSampleSource source : sources) { - int sourceTrackGroupCount = source.getTrackGroups().length; - if (group < totalTrackGroupCount + sourceTrackGroupCount) { - return Pair.create(source, group - totalTrackGroupCount); - } - totalTrackGroupCount += sourceTrackGroupCount; - } - throw new IndexOutOfBoundsException(); + private Pair buildTrackStream( + TrackSelection selection, long positionUs) { + int[] selectedTracks = selection.getTracks(); + FormatEvaluator adaptiveEvaluator = selectedTracks.length > 1 + ? new AdaptiveEvaluator(bandwidthMeter) : null; + int streamElementIndex = trackGroupElementIndices[selection.group]; + StreamElement streamElement = currentManifest.streamElements[streamElementIndex]; + int streamElementType = streamElement.type; + int bufferSize = Util.getDefaultBufferSize(streamElementType); + DataSource dataSource = dataSourceFactory.createDataSource(bandwidthMeter); + SmoothStreamingChunkSource chunkSource = new SmoothStreamingChunkSource(currentManifest, + streamElementIndex, trackGroups.get(selection.group), selectedTracks, dataSource, + adaptiveEvaluator, trackEncryptionBoxes, drmInitData); + ChunkTrackStream trackStream = new ChunkTrackStream(chunkSource, loadControl, bufferSize, + positionUs, eventHandler, eventListener, streamElementType); + return Pair.create(chunkSource, trackStream); } private static byte[] getProtectionElementKeyId(byte[] initData) { 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 870c44345b..889d90b06c 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 @@ -797,6 +797,27 @@ public final class Util { } } + /** + * Maps a {@link C} TRACK_TYPE_* constant to its corresponding DEFAULT_*_BUFFER_SIZE value. + * + * @param trackType The track type. + * @return The corresponding default buffer size in bytes. + */ + public static int getDefaultBufferSize(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_DEFAULT: + return C.DEFAULT_MUXED_BUFFER_SIZE; + case C.TRACK_TYPE_AUDIO: + return C.DEFAULT_AUDIO_BUFFER_SIZE; + case C.TRACK_TYPE_VIDEO: + return C.DEFAULT_VIDEO_BUFFER_SIZE; + case C.TRACK_TYPE_TEXT: + return C.DEFAULT_TEXT_BUFFER_SIZE; + default: + throw new IllegalStateException(); + } + } + /** * Escapes a string so that it's safe for use as a file or directory name on at least FAT32 * filesystems. FAT32 is the most restrictive of all filesystems still commonly used today.