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
This commit is contained in:
olly 2016-04-28 02:45:07 -07:00 committed by Oliver Woodman
parent d82bb3f657
commit 0841d043b8
11 changed files with 472 additions and 652 deletions

View File

@ -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<List<Id3Frame>>,

View File

@ -104,7 +104,7 @@ public interface SampleSource {
long getBufferedPositionUs();
/**
* Seeks to the specified time in microseconds.
* Seeks to the specified position in microseconds.
* <p>
* This method should only be called when at least one track is selected.
*

View File

@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* This method should only be called when the source is enabled.
*/
void disable();
}

View File

@ -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<BaseMediaChunk> mediaChunks;
private final List<BaseMediaChunk> 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<TrackStream> oldStreams,
List<TrackSelection> 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.
* <p>
* 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();

View File

@ -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;

View File

@ -52,52 +52,85 @@ import java.util.List;
/**
* An {@link ChunkSource} for DASH streams.
* <p>
* This implementation currently supports fMP4, webm, webvtt and ttml.
* <p>
* This implementation makes the following assumptions about multi-period manifests:
* <ol>
* <li>that new periods will contain the same representations as previous periods (i.e. no new or
* missing representations) and</li>
* <li>that representations are contiguous across multiple periods</li>
* </ol>
*/
// 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<Representation> 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<Representation> 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<? extends MediaChunk> 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<Representation> 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<Representation> 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) {

View File

@ -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<MediaPresentationDescription> manifestFetcher;
private final DashChunkSource[] chunkSources;
private final ChunkSampleSource[] sources;
private final IdentityHashMap<TrackStream, ChunkSampleSource> 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<TrackStream> oldStreams,
List<TrackSelection> 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<DashChunkSource, ChunkTrackStream> 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<TrackStream> allOldStreams,
List<TrackSelection> allNewSelections, long positionUs, TrackStream[] allNewStreams) {
// Get the subset of the old streams for the source.
ArrayList<TrackStream> 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<Representation> 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<TrackSelection> newSelections = new ArrayList<>();
int[] newSelectionOriginalIndices = new int[allNewSelections.size()];
for (int i = 0; i < allNewSelections.size(); i++) {
TrackSelection selection = allNewSelections.get(i);
Pair<ChunkSampleSource, Integer> 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<ChunkSampleSource, Integer> 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<DashChunkSource, ChunkTrackStream> 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);
}
}

View File

@ -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;

View File

@ -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<? extends MediaChunk> 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.
*/

View File

@ -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<SmoothStreamingManifest> manifestFetcher;
private final SmoothStreamingChunkSource[] chunkSources;
private final ChunkSampleSource[] sources;
private final IdentityHashMap<TrackStream, ChunkSampleSource> 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<TrackStream> oldStreams,
List<TrackSelection> 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<SmoothStreamingChunkSource, ChunkTrackStream> 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<TrackStream> allOldStreams,
List<TrackSelection> allNewSelections, long positionUs, TrackStream[] allNewStreams) {
// Get the subset of the old streams for the source.
ArrayList<TrackStream> 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<TrackSelection> newSelections = new ArrayList<>();
int[] newSelectionOriginalIndices = new int[allNewSelections.size()];
for (int i = 0; i < allNewSelections.size(); i++) {
TrackSelection selection = allNewSelections.get(i);
Pair<ChunkSampleSource, Integer> 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<ChunkSampleSource, Integer> 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<SmoothStreamingChunkSource, ChunkTrackStream> 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) {

View File

@ -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.