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:
parent
d82bb3f657
commit
0841d043b8
@ -31,7 +31,7 @@ import com.google.android.exoplayer.SingleSampleSource;
|
|||||||
import com.google.android.exoplayer.TrackRenderer;
|
import com.google.android.exoplayer.TrackRenderer;
|
||||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||||
import com.google.android.exoplayer.audio.AudioTrack;
|
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.drm.StreamingDrmSessionManager;
|
||||||
import com.google.android.exoplayer.extractor.ExtractorSampleSource;
|
import com.google.android.exoplayer.extractor.ExtractorSampleSource;
|
||||||
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
|
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.
|
* A wrapper around {@link ExoPlayer} that provides a higher level interface.
|
||||||
*/
|
*/
|
||||||
public class DemoPlayer implements ExoPlayer.Listener, DefaultTrackSelector.EventListener,
|
public class DemoPlayer implements ExoPlayer.Listener, DefaultTrackSelector.EventListener,
|
||||||
ChunkSampleSourceEventListener, ExtractorSampleSource.EventListener,
|
ChunkTrackStreamEventListener, ExtractorSampleSource.EventListener,
|
||||||
SingleSampleSource.EventListener, DefaultBandwidthMeter.EventListener,
|
SingleSampleSource.EventListener, DefaultBandwidthMeter.EventListener,
|
||||||
MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener,
|
MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener,
|
||||||
StreamingDrmSessionManager.EventListener, TextRenderer, MetadataRenderer<List<Id3Frame>>,
|
StreamingDrmSessionManager.EventListener, TextRenderer, MetadataRenderer<List<Id3Frame>>,
|
||||||
|
@ -104,7 +104,7 @@ public interface SampleSource {
|
|||||||
long getBufferedPositionUs();
|
long getBufferedPositionUs();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seeks to the specified time in microseconds.
|
* Seeks to the specified position in microseconds.
|
||||||
* <p>
|
* <p>
|
||||||
* This method should only be called when at least one track is selected.
|
* This method should only be called when at least one track is selected.
|
||||||
*
|
*
|
||||||
|
@ -15,13 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.chunk;
|
package com.google.android.exoplayer.chunk;
|
||||||
|
|
||||||
import com.google.android.exoplayer.TrackGroup;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
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
|
* TODO: Share more state between this interface and {@link ChunkSampleSource}. In particular
|
||||||
@ -40,25 +38,6 @@ public interface ChunkSource {
|
|||||||
*/
|
*/
|
||||||
void maybeThrowError() throws IOException;
|
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.
|
* Evaluates whether {@link MediaChunk}s should be removed from the back of the queue.
|
||||||
* <p>
|
* <p>
|
||||||
@ -88,7 +67,7 @@ public interface ChunkSource {
|
|||||||
void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out);
|
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.
|
* source.
|
||||||
* <p>
|
* <p>
|
||||||
* This method should only be called when the source is enabled.
|
* This method should only be called when the source is enabled.
|
||||||
@ -98,7 +77,7 @@ public interface ChunkSource {
|
|||||||
void onChunkLoadCompleted(Chunk chunk);
|
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 source.
|
||||||
* <p>
|
* <p>
|
||||||
* This method should only be called when the source is enabled.
|
* 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);
|
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();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,8 @@ import com.google.android.exoplayer.DecoderInputBuffer;
|
|||||||
import com.google.android.exoplayer.Format;
|
import com.google.android.exoplayer.Format;
|
||||||
import com.google.android.exoplayer.FormatHolder;
|
import com.google.android.exoplayer.FormatHolder;
|
||||||
import com.google.android.exoplayer.LoadControl;
|
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.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.extractor.DefaultTrackOutput;
|
||||||
import com.google.android.exoplayer.upstream.Loader;
|
import com.google.android.exoplayer.upstream.Loader;
|
||||||
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
||||||
@ -40,10 +36,9 @@ import java.util.LinkedList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link SampleSource} that loads media in {@link Chunk}s, which are themselves obtained from a
|
* A {@link TrackStream} that loads media in {@link Chunk}s, obtained from a {@link ChunkSource}.
|
||||||
* {@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.
|
* 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 LinkedList<BaseMediaChunk> mediaChunks;
|
||||||
private final List<BaseMediaChunk> readOnlyMediaChunks;
|
private final List<BaseMediaChunk> readOnlyMediaChunks;
|
||||||
private final DefaultTrackOutput sampleQueue;
|
private final DefaultTrackOutput sampleQueue;
|
||||||
private final int bufferSizeContribution;
|
|
||||||
private final ChunkHolder nextChunkHolder;
|
private final ChunkHolder nextChunkHolder;
|
||||||
private final EventDispatcher eventDispatcher;
|
private final EventDispatcher eventDispatcher;
|
||||||
private final LoadControl loadControl;
|
private final LoadControl loadControl;
|
||||||
|
|
||||||
private boolean notifyReset;
|
private boolean readingEnabled;
|
||||||
private long lastPreferredQueueSizeEvaluationTimeMs;
|
private long lastPreferredQueueSizeEvaluationTimeMs;
|
||||||
private Format downstreamFormat;
|
private Format downstreamFormat;
|
||||||
|
|
||||||
private TrackGroupArray trackGroups;
|
|
||||||
private boolean trackEnabled;
|
|
||||||
|
|
||||||
private long downstreamPositionUs;
|
private long downstreamPositionUs;
|
||||||
private long lastSeekPositionUs;
|
private long lastSeekPositionUs;
|
||||||
private long pendingResetPositionUs;
|
private long pendingResetPositionUs;
|
||||||
@ -74,30 +65,22 @@ public class ChunkSampleSource implements TrackStream, Loader.Callback {
|
|||||||
private Chunk currentLoadable;
|
private Chunk currentLoadable;
|
||||||
private long currentLoadStartTimeMs;
|
private long currentLoadStartTimeMs;
|
||||||
private boolean loadingFinished;
|
private boolean loadingFinished;
|
||||||
|
private boolean released;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param chunkSource A {@link ChunkSource} from which chunks to load are obtained.
|
* @param chunkSource A {@link ChunkSource} from which chunks to load are obtained.
|
||||||
* @param loadControl Controls when the source is permitted to load data.
|
* @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 bufferSizeContribution The contribution of this source to the media buffer, in bytes.
|
||||||
*/
|
* @param positionUs The position from which to start loading media.
|
||||||
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 eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||||
* null if delivery of events is not required.
|
* 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 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.
|
* @param eventSourceId An identifier that gets passed to {@code eventListener} methods.
|
||||||
*/
|
*/
|
||||||
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
|
public ChunkTrackStream(ChunkSource chunkSource, LoadControl loadControl,
|
||||||
int bufferSizeContribution, Handler eventHandler,
|
int bufferSizeContribution, long positionUs, Handler eventHandler,
|
||||||
ChunkSampleSourceEventListener eventListener, int eventSourceId) {
|
ChunkTrackStreamEventListener eventListener, int eventSourceId) {
|
||||||
this(chunkSource, loadControl, bufferSizeContribution, eventHandler, eventListener,
|
this(chunkSource, loadControl, bufferSizeContribution, positionUs, eventHandler, eventListener,
|
||||||
eventSourceId, DEFAULT_MIN_LOADABLE_RETRY_COUNT);
|
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 chunkSource A {@link ChunkSource} from which chunks to load are obtained.
|
||||||
* @param loadControl Controls when the source is permitted to load data.
|
* @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 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
|
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||||
* null if delivery of events is not required.
|
* 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 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
|
* @param minLoadableRetryCount The minimum number of times that the source should retry a load
|
||||||
* before propagating an error.
|
* before propagating an error.
|
||||||
*/
|
*/
|
||||||
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
|
public ChunkTrackStream(ChunkSource chunkSource, LoadControl loadControl,
|
||||||
int bufferSizeContribution, Handler eventHandler,
|
int bufferSizeContribution, long positionUs, Handler eventHandler,
|
||||||
ChunkSampleSourceEventListener eventListener, int eventSourceId, int minLoadableRetryCount) {
|
ChunkTrackStreamEventListener eventListener, int eventSourceId, int minLoadableRetryCount) {
|
||||||
this.chunkSource = chunkSource;
|
this.chunkSource = chunkSource;
|
||||||
this.loadControl = loadControl;
|
this.loadControl = loadControl;
|
||||||
this.bufferSizeContribution = bufferSizeContribution;
|
loader = new Loader("Loader:ChunkTrackStream", minLoadableRetryCount);
|
||||||
loader = new Loader("Loader:ChunkSampleSource", minLoadableRetryCount);
|
|
||||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId);
|
eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId);
|
||||||
nextChunkHolder = new ChunkHolder();
|
nextChunkHolder = new ChunkHolder();
|
||||||
mediaChunks = new LinkedList<>();
|
mediaChunks = new LinkedList<>();
|
||||||
readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
|
readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
|
||||||
sampleQueue = new DefaultTrackOutput(loadControl.getAllocator());
|
sampleQueue = new DefaultTrackOutput(loadControl.getAllocator());
|
||||||
pendingResetPositionUs = C.UNSET_TIME_US;
|
pendingResetPositionUs = C.UNSET_TIME_US;
|
||||||
|
readingEnabled = true;
|
||||||
|
downstreamPositionUs = positionUs;
|
||||||
|
lastSeekPositionUs = positionUs;
|
||||||
|
loadControl.register(this, bufferSizeContribution);
|
||||||
|
restartFrom(positionUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SampleSource implementation.
|
/**
|
||||||
|
* Enables or disables reading of data from {@link #readData(FormatHolder, DecoderInputBuffer)}.
|
||||||
public void prepare() {
|
*
|
||||||
TrackGroup tracks = chunkSource.getTracks();
|
* @param readingEnabled Whether reading should be enabled.
|
||||||
if (tracks != null) {
|
*/
|
||||||
trackGroups = new TrackGroupArray(tracks);
|
public void setReadingEnabled(boolean readingEnabled) {
|
||||||
} else {
|
this.readingEnabled = readingEnabled;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO[REFACTOR]: Find a way to get rid of this.
|
||||||
public void continueBuffering(long positionUs) {
|
public void continueBuffering(long positionUs) {
|
||||||
downstreamPositionUs = positionUs;
|
downstreamPositionUs = positionUs;
|
||||||
if (!loader.isLoading()) {
|
if (!loader.isLoading()) {
|
||||||
@ -193,14 +132,12 @@ public class ChunkSampleSource implements TrackStream, Loader.Callback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public long readReset() {
|
/**
|
||||||
if (notifyReset) {
|
* Returns an estimate of the position up to which data is buffered.
|
||||||
notifyReset = false;
|
*
|
||||||
return lastSeekPositionUs;
|
* @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.
|
||||||
return C.UNSET_TIME_US;
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
public long getBufferedPositionUs() {
|
public long getBufferedPositionUs() {
|
||||||
if (loadingFinished) {
|
if (loadingFinished) {
|
||||||
return C.END_OF_SOURCE_US;
|
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) {
|
public void seekToUs(long positionUs) {
|
||||||
downstreamPositionUs = positionUs;
|
downstreamPositionUs = positionUs;
|
||||||
lastSeekPositionUs = positionUs;
|
lastSeekPositionUs = positionUs;
|
||||||
@ -233,16 +175,23 @@ public class ChunkSampleSource implements TrackStream, Loader.Callback {
|
|||||||
// We failed, and need to restart.
|
// We failed, and need to restart.
|
||||||
restartFrom(positionUs);
|
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() {
|
public void release() {
|
||||||
if (trackEnabled) {
|
loadControl.unregister(this);
|
||||||
loadControl.unregister(this);
|
if (loader.isLoading()) {
|
||||||
trackEnabled = false;
|
loader.cancelLoading();
|
||||||
|
} else {
|
||||||
|
clearState();
|
||||||
|
loadControl.trimAllocator();
|
||||||
}
|
}
|
||||||
loader.release();
|
loader.release();
|
||||||
|
released = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrackStream implementation.
|
// TrackStream implementation.
|
||||||
@ -260,7 +209,7 @@ public class ChunkSampleSource implements TrackStream, Loader.Callback {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
|
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
|
||||||
if (notifyReset || isPendingReset()) {
|
if (!readingEnabled || isPendingReset()) {
|
||||||
return NOTHING_READ;
|
return NOTHING_READ;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,7 +255,7 @@ public class ChunkSampleSource implements TrackStream, Loader.Callback {
|
|||||||
@Override
|
@Override
|
||||||
public void onLoadCanceled(Loadable loadable) {
|
public void onLoadCanceled(Loadable loadable) {
|
||||||
eventDispatcher.loadCanceled(currentLoadable.bytesLoaded());
|
eventDispatcher.loadCanceled(currentLoadable.bytesLoaded());
|
||||||
if (trackEnabled) {
|
if (!released) {
|
||||||
restartFrom(pendingResetPositionUs);
|
restartFrom(pendingResetPositionUs);
|
||||||
} else {
|
} else {
|
||||||
clearState();
|
clearState();
|
@ -17,7 +17,6 @@ package com.google.android.exoplayer.chunk;
|
|||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.Format;
|
import com.google.android.exoplayer.Format;
|
||||||
import com.google.android.exoplayer.SampleSource;
|
|
||||||
import com.google.android.exoplayer.TrackStream;
|
import com.google.android.exoplayer.TrackStream;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
@ -26,14 +25,14 @@ import android.os.Handler;
|
|||||||
import java.io.IOException;
|
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.
|
* 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
|
* @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.
|
* the length of the data is not known in advance.
|
||||||
* @param type The type of the data being loaded.
|
* @param type The type of the data being loaded.
|
||||||
@ -51,7 +50,7 @@ public interface ChunkSampleSourceEventListener {
|
|||||||
/**
|
/**
|
||||||
* Invoked when the current load operation completes.
|
* 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 bytesLoaded The number of bytes that were loaded.
|
||||||
* @param type The type of the loaded data.
|
* @param type The type of the loaded data.
|
||||||
* @param trigger The reason for the data being loaded.
|
* @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.
|
* 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.
|
* @param bytesLoaded The number of bytes that were loaded prior to the cancellation.
|
||||||
*/
|
*/
|
||||||
void onLoadCanceled(int sourceId, long bytesLoaded);
|
void onLoadCanceled(int sourceId, long bytesLoaded);
|
||||||
@ -78,7 +77,7 @@ public interface ChunkSampleSourceEventListener {
|
|||||||
/**
|
/**
|
||||||
* Invoked when an error occurs loading media data.
|
* 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.
|
* @param e The cause of the failure.
|
||||||
*/
|
*/
|
||||||
void onLoadError(int sourceId, IOException e);
|
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
|
* Invoked when data is removed from the back of the buffer, typically so that it can be
|
||||||
* re-buffered using a different representation.
|
* 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 mediaStartTimeMs The media time of the start of the discarded data.
|
||||||
* @param mediaEndTimeMs The media time of the end 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
|
* Invoked when the downstream format changes (i.e. when the format being supplied to the
|
||||||
* caller of {@link TrackStream#readData} changes).
|
* 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 format The format.
|
||||||
* @param trigger The trigger specified in the corresponding upstream load, as specified by the
|
* @param trigger The trigger specified in the corresponding upstream load, as specified by the
|
||||||
* {@link ChunkSource}.
|
* {@link ChunkSource}.
|
||||||
@ -106,15 +105,15 @@ public interface ChunkSampleSourceEventListener {
|
|||||||
void onDownstreamFormatChanged(int sourceId, Format format, int trigger, long mediaTimeMs);
|
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 {
|
final class EventDispatcher {
|
||||||
|
|
||||||
private final Handler handler;
|
private final Handler handler;
|
||||||
private final ChunkSampleSourceEventListener listener;
|
private final ChunkTrackStreamEventListener listener;
|
||||||
private final int sourceId;
|
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.handler = listener != null ? handler : null;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
this.sourceId = sourceId;
|
this.sourceId = sourceId;
|
@ -52,52 +52,85 @@ import java.util.List;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link ChunkSource} for DASH streams.
|
* 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 {
|
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 DataSource dataSource;
|
||||||
private final FormatEvaluator adaptiveFormatEvaluator;
|
private final FormatEvaluator adaptiveFormatEvaluator;
|
||||||
private final Evaluation evaluation;
|
private final Evaluation evaluation;
|
||||||
|
|
||||||
private MediaPresentationDescription currentManifest;
|
private MediaPresentationDescription manifest;
|
||||||
private DrmInitData drmInitData;
|
private DrmInitData drmInitData;
|
||||||
|
|
||||||
private boolean lastChunkWasInitialization;
|
private boolean lastChunkWasInitialization;
|
||||||
private IOException fatalError;
|
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
|
* @param manifest The initial manifest.
|
||||||
* {@link C#TRACK_TYPE_AUDIO}, {@link C#TRACK_TYPE_VIDEO} and {@link C#TRACK_TYPE_TEXT}.
|
* @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 dataSource A {@link DataSource} suitable for loading the media data.
|
||||||
* @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats.
|
* @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) {
|
FormatEvaluator adaptiveFormatEvaluator) {
|
||||||
this.adaptationSetType = adaptationSetType;
|
this.manifest = manifest;
|
||||||
|
this.adaptationSetIndex = adaptationSetIndex;
|
||||||
|
this.trackGroup = trackGroup;
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.adaptiveFormatEvaluator = adaptiveFormatEvaluator;
|
this.adaptiveFormatEvaluator = adaptiveFormatEvaluator;
|
||||||
this.evaluation = new Evaluation();
|
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.
|
// 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
|
@Override
|
||||||
public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
|
public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
|
||||||
if (fatalError != null || enabledFormats.length < 2) {
|
if (fatalError != null || enabledFormats.length < 2) {
|
||||||
@ -200,9 +206,9 @@ public class DashChunkSource implements ChunkSource {
|
|||||||
if (indexUnbounded) {
|
if (indexUnbounded) {
|
||||||
// The index is itself unbounded. We need to use the current time to calculate the range of
|
// The index is itself unbounded. We need to use the current time to calculate the range of
|
||||||
// available segments.
|
// available segments.
|
||||||
long liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000;
|
long liveEdgeTimestampUs = nowUs - manifest.availabilityStartTime * 1000;
|
||||||
if (currentManifest.timeShiftBufferDepth != -1) {
|
if (manifest.timeShiftBufferDepth != -1) {
|
||||||
long bufferDepthUs = currentManifest.timeShiftBufferDepth * 1000;
|
long bufferDepthUs = manifest.timeShiftBufferDepth * 1000;
|
||||||
firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum,
|
firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum,
|
||||||
representationHolder.getSegmentNum(liveEdgeTimestampUs - bufferDepthUs));
|
representationHolder.getSegmentNum(liveEdgeTimestampUs - bufferDepthUs));
|
||||||
}
|
}
|
||||||
@ -226,7 +232,7 @@ public class DashChunkSource implements ChunkSource {
|
|||||||
|
|
||||||
if (segmentNum > lastAvailableSegmentNum) {
|
if (segmentNum > lastAvailableSegmentNum) {
|
||||||
// This is beyond the last chunk in the current manifest.
|
// This is beyond the last chunk in the current manifest.
|
||||||
out.endOfStream = !currentManifest.dynamic;
|
out.endOfStream = !manifest.dynamic;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,59 +276,8 @@ public class DashChunkSource implements ChunkSource {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void disable() {
|
|
||||||
if (enabledFormats.length > 1) {
|
|
||||||
adaptiveFormatEvaluator.disable();
|
|
||||||
}
|
|
||||||
evaluation.clear();
|
|
||||||
fatalError = null;
|
|
||||||
enabledFormats = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private methods.
|
// 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,
|
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
|
||||||
Representation representation, ChunkExtractorWrapper extractor, DataSource dataSource,
|
Representation representation, ChunkExtractorWrapper extractor, DataSource dataSource,
|
||||||
int trigger) {
|
int trigger) {
|
||||||
|
@ -17,23 +17,29 @@ package com.google.android.exoplayer.dash;
|
|||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.DefaultLoadControl;
|
import com.google.android.exoplayer.DefaultLoadControl;
|
||||||
|
import com.google.android.exoplayer.Format;
|
||||||
import com.google.android.exoplayer.LoadControl;
|
import com.google.android.exoplayer.LoadControl;
|
||||||
import com.google.android.exoplayer.SampleSource;
|
import com.google.android.exoplayer.SampleSource;
|
||||||
import com.google.android.exoplayer.TrackGroup;
|
import com.google.android.exoplayer.TrackGroup;
|
||||||
import com.google.android.exoplayer.TrackGroupArray;
|
import com.google.android.exoplayer.TrackGroupArray;
|
||||||
import com.google.android.exoplayer.TrackSelection;
|
import com.google.android.exoplayer.TrackSelection;
|
||||||
import com.google.android.exoplayer.TrackStream;
|
import com.google.android.exoplayer.TrackStream;
|
||||||
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
import com.google.android.exoplayer.chunk.ChunkTrackStream;
|
||||||
import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener;
|
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.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.MediaPresentationDescription;
|
||||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
|
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.BandwidthMeter;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DataSourceFactory;
|
import com.google.android.exoplayer.upstream.DataSourceFactory;
|
||||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||||
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
@ -41,62 +47,47 @@ import android.os.SystemClock;
|
|||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.Arrays;
|
||||||
import java.util.IdentityHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combines multiple {@link SampleSource} instances.
|
* A {@link SampleSource} for DASH media.
|
||||||
*/
|
*/
|
||||||
public final class DashSampleSource implements SampleSource {
|
public final class DashSampleSource implements SampleSource {
|
||||||
|
|
||||||
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
|
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
|
||||||
private final DashChunkSource[] chunkSources;
|
private final DataSourceFactory dataSourceFactory;
|
||||||
private final ChunkSampleSource[] sources;
|
private final BandwidthMeter bandwidthMeter;
|
||||||
private final IdentityHashMap<TrackStream, ChunkSampleSource> trackStreamSources;
|
private final Handler eventHandler;
|
||||||
private final int[] selectedTrackCounts;
|
private final ChunkTrackStreamEventListener eventListener;
|
||||||
|
private final LoadControl loadControl;
|
||||||
|
|
||||||
private MediaPresentationDescription currentManifest;
|
|
||||||
private boolean prepared;
|
private boolean prepared;
|
||||||
private boolean seenFirstTrackSelection;
|
|
||||||
private long durationUs;
|
private long durationUs;
|
||||||
|
private MediaPresentationDescription currentManifest;
|
||||||
private TrackGroupArray trackGroups;
|
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,
|
public DashSampleSource(Uri uri, DataSourceFactory dataSourceFactory,
|
||||||
BandwidthMeter bandwidthMeter, Handler eventHandler,
|
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();
|
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
|
||||||
DataSource manifestDataSource = dataSourceFactory.createDataSource();
|
DataSource manifestDataSource = dataSourceFactory.createDataSource();
|
||||||
manifestFetcher = new ManifestFetcher<>(uri, manifestDataSource, parser);
|
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
|
@Override
|
||||||
@ -111,30 +102,12 @@ public final class DashSampleSource implements SampleSource {
|
|||||||
manifestFetcher.maybeThrowError();
|
manifestFetcher.maybeThrowError();
|
||||||
manifestFetcher.requestRefresh();
|
manifestFetcher.requestRefresh();
|
||||||
return false;
|
return false;
|
||||||
} else {
|
|
||||||
durationUs = currentManifest.dynamic ? C.UNSET_TIME_US : currentManifest.duration * 1000;
|
|
||||||
for (DashChunkSource chunkSource : chunkSources) {
|
|
||||||
chunkSource.init(currentManifest);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ChunkSampleSource source : sources) {
|
durationUs = currentManifest.dynamic ? C.UNSET_TIME_US : currentManifest.duration * 1000;
|
||||||
source.prepare();
|
buildTrackGroups(currentManifest);
|
||||||
}
|
|
||||||
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;
|
prepared = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -153,26 +126,37 @@ public final class DashSampleSource implements SampleSource {
|
|||||||
public TrackStream[] selectTracks(List<TrackStream> oldStreams,
|
public TrackStream[] selectTracks(List<TrackStream> oldStreams,
|
||||||
List<TrackSelection> newSelections, long positionUs) {
|
List<TrackSelection> newSelections, long positionUs) {
|
||||||
Assertions.checkState(prepared);
|
Assertions.checkState(prepared);
|
||||||
TrackStream[] newStreams = new TrackStream[newSelections.size()];
|
|
||||||
// Select tracks for each source.
|
int newEnabledSourceCount = trackStreams.length + newSelections.size() - oldStreams.size();
|
||||||
int enabledSourceCount = 0;
|
DashChunkSource[] newChunkSources = new DashChunkSource[newEnabledSourceCount];
|
||||||
for (int i = 0; i < sources.length; i++) {
|
ChunkTrackStream[] newTrackStreams = new ChunkTrackStream[newEnabledSourceCount];
|
||||||
selectedTrackCounts[i] += selectTracks(sources[i], oldStreams, newSelections, positionUs,
|
int newEnabledSourceIndex = 0;
|
||||||
newStreams);
|
|
||||||
if (selectedTrackCounts[i] > 0) {
|
// Iterate over currently enabled streams, either releasing them or adding them to the new list.
|
||||||
enabledSourceCount++;
|
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];
|
// Instantiate and return new streams.
|
||||||
enabledSourceCount = 0;
|
TrackStream[] streamsToReturn = new TrackStream[newSelections.size()];
|
||||||
for (int i = 0; i < sources.length; i++) {
|
for (int i = 0; i < newSelections.size(); i++) {
|
||||||
if (selectedTrackCounts[i] > 0) {
|
Pair<DashChunkSource, ChunkTrackStream> trackComponents =
|
||||||
enabledSources[enabledSourceCount++] = sources[i];
|
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
|
@Override
|
||||||
@ -201,103 +185,102 @@ public final class DashSampleSource implements SampleSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ChunkSampleSource source : enabledSources) {
|
for (ChunkTrackStream trackStream : trackStreams) {
|
||||||
source.continueBuffering(positionUs);
|
trackStream.continueBuffering(positionUs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long readReset() {
|
public long readReset() {
|
||||||
long resetPositionUs = C.UNSET_TIME_US;
|
if (pendingReset) {
|
||||||
for (ChunkSampleSource source : enabledSources) {
|
pendingReset = false;
|
||||||
long childResetPositionUs = source.readReset();
|
for (ChunkTrackStream trackStream : trackStreams) {
|
||||||
if (resetPositionUs == C.UNSET_TIME_US) {
|
trackStream.setReadingEnabled(true);
|
||||||
resetPositionUs = childResetPositionUs;
|
|
||||||
} else if (childResetPositionUs != C.UNSET_TIME_US) {
|
|
||||||
resetPositionUs = Math.min(resetPositionUs, childResetPositionUs);
|
|
||||||
}
|
}
|
||||||
|
return lastSeekPositionUs;
|
||||||
}
|
}
|
||||||
return resetPositionUs;
|
return C.UNSET_TIME_US;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getBufferedPositionUs() {
|
public long getBufferedPositionUs() {
|
||||||
long bufferedPositionUs = durationUs != C.UNSET_TIME_US ? durationUs : Long.MAX_VALUE;
|
long bufferedPositionUs = Long.MAX_VALUE;
|
||||||
for (ChunkSampleSource source : enabledSources) {
|
for (ChunkTrackStream trackStream : trackStreams) {
|
||||||
long rendererBufferedPositionUs = source.getBufferedPositionUs();
|
long rendererBufferedPositionUs = trackStream.getBufferedPositionUs();
|
||||||
if (rendererBufferedPositionUs == C.UNSET_TIME_US) {
|
if (rendererBufferedPositionUs != C.END_OF_SOURCE_US) {
|
||||||
return C.UNSET_TIME_US;
|
|
||||||
} else if (rendererBufferedPositionUs == C.END_OF_SOURCE_US) {
|
|
||||||
// This source is fully buffered.
|
|
||||||
} else {
|
|
||||||
bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
|
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
|
@Override
|
||||||
public void seekToUs(long positionUs) {
|
public void seekToUs(long positionUs) {
|
||||||
for (ChunkSampleSource source : enabledSources) {
|
lastSeekPositionUs = positionUs;
|
||||||
source.seekToUs(positionUs);
|
pendingReset = true;
|
||||||
|
for (ChunkTrackStream trackStream : trackStreams) {
|
||||||
|
trackStream.setReadingEnabled(false);
|
||||||
|
trackStream.seekToUs(positionUs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void release() {
|
public void release() {
|
||||||
manifestFetcher.release();
|
manifestFetcher.release();
|
||||||
for (ChunkSampleSource source : sources) {
|
for (DashChunkSource chunkSource : chunkSources) {
|
||||||
source.release();
|
chunkSource.release();
|
||||||
|
}
|
||||||
|
for (ChunkTrackStream trackStream : trackStreams) {
|
||||||
|
trackStream.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private int selectTracks(ChunkSampleSource source, List<TrackStream> allOldStreams,
|
private void buildTrackGroups(MediaPresentationDescription manifest) {
|
||||||
List<TrackSelection> allNewSelections, long positionUs, TrackStream[] allNewStreams) {
|
Period period = manifest.getPeriod(0);
|
||||||
// Get the subset of the old streams for the source.
|
int trackGroupCount = 0;
|
||||||
ArrayList<TrackStream> oldStreams = new ArrayList<>();
|
trackGroupAdaptationSetIndices = new int[period.adaptationSets.size()];
|
||||||
for (int i = 0; i < allOldStreams.size(); i++) {
|
TrackGroup[] trackGroupArray = new TrackGroup[period.adaptationSets.size()];
|
||||||
TrackStream stream = allOldStreams.get(i);
|
for (int i = 0; i < period.adaptationSets.size(); i++) {
|
||||||
if (trackStreamSources.get(stream) == source) {
|
AdaptationSet adaptationSet = period.adaptationSets.get(i);
|
||||||
trackStreamSources.remove(stream);
|
int adaptationSetType = adaptationSet.type;
|
||||||
oldStreams.add(stream);
|
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.
|
if (trackGroupCount < trackGroupArray.length) {
|
||||||
ArrayList<TrackSelection> newSelections = new ArrayList<>();
|
trackGroupAdaptationSetIndices = Arrays.copyOf(trackGroupAdaptationSetIndices,
|
||||||
int[] newSelectionOriginalIndices = new int[allNewSelections.size()];
|
trackGroupCount);
|
||||||
for (int i = 0; i < allNewSelections.size(); i++) {
|
trackGroupArray = Arrays.copyOf(trackGroupArray, trackGroupCount);
|
||||||
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()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Do nothing if nothing has changed, except during the first selection.
|
trackGroups = new TrackGroupArray(trackGroupArray);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pair<ChunkSampleSource, Integer> getSourceAndGroup(int group) {
|
private Pair<DashChunkSource, ChunkTrackStream> buildTrackStream(TrackSelection selection,
|
||||||
int totalTrackGroupCount = 0;
|
long positionUs) {
|
||||||
for (ChunkSampleSource source : sources) {
|
int[] selectedTracks = selection.getTracks();
|
||||||
int sourceTrackGroupCount = source.getTrackGroups().length;
|
FormatEvaluator adaptiveEvaluator = selectedTracks.length > 1
|
||||||
if (group < totalTrackGroupCount + sourceTrackGroupCount) {
|
? new AdaptiveEvaluator(bandwidthMeter) : null;
|
||||||
return Pair.create(source, group - totalTrackGroupCount);
|
int adaptationSetIndex = trackGroupAdaptationSetIndices[selection.group];
|
||||||
}
|
AdaptationSet adaptationSet = currentManifest.getPeriod(0).adaptationSets.get(
|
||||||
totalTrackGroupCount += sourceTrackGroupCount;
|
adaptationSetIndex);
|
||||||
}
|
int adaptationSetType = adaptationSet.type;
|
||||||
throw new IndexOutOfBoundsException();
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,8 @@ import com.google.android.exoplayer.TrackSelection;
|
|||||||
import com.google.android.exoplayer.TrackStream;
|
import com.google.android.exoplayer.TrackStream;
|
||||||
import com.google.android.exoplayer.chunk.Chunk;
|
import com.google.android.exoplayer.chunk.Chunk;
|
||||||
import com.google.android.exoplayer.chunk.ChunkHolder;
|
import com.google.android.exoplayer.chunk.ChunkHolder;
|
||||||
import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener;
|
import com.google.android.exoplayer.chunk.ChunkTrackStreamEventListener;
|
||||||
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.extractor.DefaultTrackOutput;
|
||||||
import com.google.android.exoplayer.upstream.Loader;
|
import com.google.android.exoplayer.upstream.Loader;
|
||||||
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
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,
|
public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl,
|
||||||
int bufferSizeContribution, Handler eventHandler,
|
int bufferSizeContribution, Handler eventHandler,
|
||||||
ChunkSampleSourceEventListener eventListener, int eventSourceId) {
|
ChunkTrackStreamEventListener eventListener, int eventSourceId) {
|
||||||
this(chunkSource, loadControl, bufferSizeContribution, eventHandler, eventListener,
|
this(chunkSource, loadControl, bufferSizeContribution, eventHandler, eventListener,
|
||||||
eventSourceId, DEFAULT_MIN_LOADABLE_RETRY_COUNT);
|
eventSourceId, DEFAULT_MIN_LOADABLE_RETRY_COUNT);
|
||||||
}
|
}
|
||||||
@ -127,7 +127,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
|
|||||||
*/
|
*/
|
||||||
public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl,
|
public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl,
|
||||||
int bufferSizeContribution, Handler eventHandler,
|
int bufferSizeContribution, Handler eventHandler,
|
||||||
ChunkSampleSourceEventListener eventListener, int eventSourceId, int minLoadableRetryCount) {
|
ChunkTrackStreamEventListener eventListener, int eventSourceId, int minLoadableRetryCount) {
|
||||||
this.chunkSource = chunkSource;
|
this.chunkSource = chunkSource;
|
||||||
this.loadControl = loadControl;
|
this.loadControl = loadControl;
|
||||||
this.bufferSizeContribution = bufferSizeContribution;
|
this.bufferSizeContribution = bufferSizeContribution;
|
||||||
|
@ -46,86 +46,75 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* An {@link ChunkSource} for SmoothStreaming.
|
* An {@link ChunkSource} for SmoothStreaming.
|
||||||
*/
|
*/
|
||||||
// TODO[REFACTOR]: Handle multiple stream elements of the same type (at a higher level).
|
|
||||||
public class SmoothStreamingChunkSource implements ChunkSource {
|
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 DataSource dataSource;
|
||||||
private final Evaluation evaluation;
|
private final Evaluation evaluation;
|
||||||
private final FormatEvaluator adaptiveFormatEvaluator;
|
private final FormatEvaluator adaptiveFormatEvaluator;
|
||||||
|
|
||||||
private SmoothStreamingManifest currentManifest;
|
private SmoothStreamingManifest manifest;
|
||||||
private TrackEncryptionBox[] trackEncryptionBoxes;
|
|
||||||
private DrmInitData drmInitData;
|
private DrmInitData drmInitData;
|
||||||
private int currentManifestChunkOffset;
|
private int currentManifestChunkOffset;
|
||||||
private boolean needManifestRefresh;
|
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;
|
private IOException fatalError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param streamElementType The type of stream element exposed by this source. One of
|
* @param manifest The initial manifest.
|
||||||
* {@link C#TRACK_TYPE_VIDEO}, {@link C#TRACK_TYPE_AUDIO} and {@link C#TRACK_TYPE_TEXT}.
|
* @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 dataSource A {@link DataSource} suitable for loading the media data.
|
||||||
* @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats.
|
* @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,
|
public SmoothStreamingChunkSource(SmoothStreamingManifest manifest, int elementIndex,
|
||||||
FormatEvaluator adaptiveFormatEvaluator) {
|
TrackGroup trackGroup, int[] tracks, DataSource dataSource,
|
||||||
this.streamElementType = streamElementType;
|
FormatEvaluator adaptiveFormatEvaluator, TrackEncryptionBox[] trackEncryptionBoxes,
|
||||||
|
DrmInitData drmInitData) {
|
||||||
|
this.manifest = manifest;
|
||||||
|
this.elementIndex = elementIndex;
|
||||||
|
this.trackGroup = trackGroup;
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.adaptiveFormatEvaluator = adaptiveFormatEvaluator;
|
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;
|
this.drmInitData = drmInitData;
|
||||||
initForManifest(currentManifest);
|
this.evaluation = new Evaluation();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
StreamElement streamElement = manifest.streamElements[elementIndex];
|
||||||
public final TrackGroup getTracks() {
|
Format[] formats = streamElement.formats;
|
||||||
return trackGroup;
|
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];
|
enabledFormats = new Format[tracks.length];
|
||||||
for (int i = 0; i < tracks.length; i++) {
|
for (int i = 0; i < tracks.length; i++) {
|
||||||
enabledFormats[i] = trackGroup.getFormat(tracks[i]);
|
enabledFormats[i] = trackGroup.getFormat(tracks[i]);
|
||||||
}
|
}
|
||||||
Arrays.sort(enabledFormats, new DecreasingBandwidthComparator());
|
Arrays.sort(enabledFormats, new DecreasingBandwidthComparator());
|
||||||
if (enabledFormats.length > 1) {
|
if (adaptiveFormatEvaluator != null) {
|
||||||
adaptiveFormatEvaluator.enable(enabledFormats);
|
adaptiveFormatEvaluator.enable(enabledFormats);
|
||||||
adaptiveFormatBlacklistFlags = new boolean[tracks.length];
|
adaptiveFormatBlacklistFlags = new boolean[tracks.length];
|
||||||
|
} else {
|
||||||
|
adaptiveFormatBlacklistFlags = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateManifest(SmoothStreamingManifest newManifest) {
|
public void updateManifest(SmoothStreamingManifest newManifest) {
|
||||||
StreamElement currentElement = currentManifest.streamElements[elementIndex];
|
StreamElement currentElement = manifest.streamElements[elementIndex];
|
||||||
int currentElementChunkCount = currentElement.chunkCount;
|
int currentElementChunkCount = currentElement.chunkCount;
|
||||||
StreamElement newElement = newManifest.streamElements[elementIndex];
|
StreamElement newElement = newManifest.streamElements[elementIndex];
|
||||||
if (currentElementChunkCount == 0 || newElement.chunkCount == 0) {
|
if (currentElementChunkCount == 0 || newElement.chunkCount == 0) {
|
||||||
@ -143,10 +132,29 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs);
|
currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentManifest = newManifest;
|
manifest = newManifest;
|
||||||
needManifestRefresh = false;
|
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
|
@Override
|
||||||
public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
|
public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
|
||||||
if (fatalError != null || enabledFormats.length < 2) {
|
if (fatalError != null || enabledFormats.length < 2) {
|
||||||
@ -176,9 +184,9 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamElement streamElement = currentManifest.streamElements[elementIndex];
|
StreamElement streamElement = manifest.streamElements[elementIndex];
|
||||||
if (streamElement.chunkCount == 0) {
|
if (streamElement.chunkCount == 0) {
|
||||||
if (currentManifest.isLive) {
|
if (manifest.isLive) {
|
||||||
needManifestRefresh = true;
|
needManifestRefresh = true;
|
||||||
} else {
|
} else {
|
||||||
out.endOfStream = true;
|
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) {
|
if (chunkIndex >= streamElement.chunkCount) {
|
||||||
// This is beyond the last chunk in the current manifest.
|
// This is beyond the last chunk in the current manifest.
|
||||||
out.endOfStream = !currentManifest.isLive;
|
out.endOfStream = !manifest.isLive;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,45 +239,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void disable() {
|
|
||||||
if (enabledFormats.length > 1) {
|
|
||||||
adaptiveFormatEvaluator.disable();
|
|
||||||
}
|
|
||||||
evaluation.clear();
|
|
||||||
fatalError = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private methods.
|
// 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.
|
* Gets the index of a format in a track group, using referential equality.
|
||||||
*/
|
*/
|
||||||
|
@ -17,19 +17,22 @@ package com.google.android.exoplayer.smoothstreaming;
|
|||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.DefaultLoadControl;
|
import com.google.android.exoplayer.DefaultLoadControl;
|
||||||
|
import com.google.android.exoplayer.Format;
|
||||||
import com.google.android.exoplayer.LoadControl;
|
import com.google.android.exoplayer.LoadControl;
|
||||||
import com.google.android.exoplayer.SampleSource;
|
import com.google.android.exoplayer.SampleSource;
|
||||||
import com.google.android.exoplayer.TrackGroup;
|
import com.google.android.exoplayer.TrackGroup;
|
||||||
import com.google.android.exoplayer.TrackGroupArray;
|
import com.google.android.exoplayer.TrackGroupArray;
|
||||||
import com.google.android.exoplayer.TrackSelection;
|
import com.google.android.exoplayer.TrackSelection;
|
||||||
import com.google.android.exoplayer.TrackStream;
|
import com.google.android.exoplayer.TrackStream;
|
||||||
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
import com.google.android.exoplayer.chunk.ChunkTrackStream;
|
||||||
import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener;
|
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.chunk.FormatEvaluator.AdaptiveEvaluator;
|
||||||
import com.google.android.exoplayer.drm.DrmInitData;
|
import com.google.android.exoplayer.drm.DrmInitData;
|
||||||
import com.google.android.exoplayer.drm.DrmInitData.SchemeInitData;
|
import com.google.android.exoplayer.drm.DrmInitData.SchemeInitData;
|
||||||
import com.google.android.exoplayer.extractor.mp4.TrackEncryptionBox;
|
import com.google.android.exoplayer.extractor.mp4.TrackEncryptionBox;
|
||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
|
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.BandwidthMeter;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DataSourceFactory;
|
import com.google.android.exoplayer.upstream.DataSourceFactory;
|
||||||
@ -46,69 +49,55 @@ import android.util.Base64;
|
|||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.Arrays;
|
||||||
import java.util.IdentityHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combines multiple {@link SampleSource} instances.
|
* A {@link SampleSource} for SmoothStreaming media.
|
||||||
*/
|
*/
|
||||||
public final class SmoothStreamingSampleSource implements SampleSource {
|
public final class SmoothStreamingSampleSource implements SampleSource {
|
||||||
|
|
||||||
private static final int MINIMUM_MANIFEST_REFRESH_PERIOD_MS = 5000;
|
private static final int MINIMUM_MANIFEST_REFRESH_PERIOD_MS = 5000;
|
||||||
private static final int INITIALIZATION_VECTOR_SIZE = 8;
|
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 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 prepared;
|
||||||
private boolean seenFirstTrackSelection;
|
|
||||||
private long durationUs;
|
private long durationUs;
|
||||||
|
private SmoothStreamingManifest currentManifest;
|
||||||
|
private TrackEncryptionBox[] trackEncryptionBoxes;
|
||||||
|
private DrmInitData.Mapped drmInitData;
|
||||||
private TrackGroupArray trackGroups;
|
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,
|
public SmoothStreamingSampleSource(Uri uri, DataSourceFactory dataSourceFactory,
|
||||||
BandwidthMeter bandwidthMeter, Handler eventHandler,
|
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")) {
|
if (!Util.toLowerInvariant(uri.getLastPathSegment()).equals("manifest")) {
|
||||||
uri = Uri.withAppendedPath(uri, "Manifest");
|
uri = Uri.withAppendedPath(uri, "Manifest");
|
||||||
}
|
}
|
||||||
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
|
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
|
||||||
DataSource manifestDataSource = dataSourceFactory.createDataSource();
|
DataSource manifestDataSource = dataSourceFactory.createDataSource();
|
||||||
manifestFetcher = new ManifestFetcher<>(uri, manifestDataSource, parser);
|
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
|
@Override
|
||||||
@ -123,46 +112,22 @@ public final class SmoothStreamingSampleSource implements SampleSource {
|
|||||||
manifestFetcher.maybeThrowError();
|
manifestFetcher.maybeThrowError();
|
||||||
manifestFetcher.requestRefresh();
|
manifestFetcher.requestRefresh();
|
||||||
return false;
|
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) {
|
durationUs = currentManifest.durationUs;
|
||||||
source.prepare();
|
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;
|
prepared = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -181,26 +146,38 @@ public final class SmoothStreamingSampleSource implements SampleSource {
|
|||||||
public TrackStream[] selectTracks(List<TrackStream> oldStreams,
|
public TrackStream[] selectTracks(List<TrackStream> oldStreams,
|
||||||
List<TrackSelection> newSelections, long positionUs) {
|
List<TrackSelection> newSelections, long positionUs) {
|
||||||
Assertions.checkState(prepared);
|
Assertions.checkState(prepared);
|
||||||
TrackStream[] newStreams = new TrackStream[newSelections.size()];
|
|
||||||
// Select tracks for each source.
|
int newEnabledSourceCount = trackStreams.length + newSelections.size() - oldStreams.size();
|
||||||
int enabledSourceCount = 0;
|
SmoothStreamingChunkSource[] newChunkSources =
|
||||||
for (int i = 0; i < sources.length; i++) {
|
new SmoothStreamingChunkSource[newEnabledSourceCount];
|
||||||
selectedTrackCounts[i] += selectTracks(sources[i], oldStreams, newSelections, positionUs,
|
ChunkTrackStream[] newTrackStreams = new ChunkTrackStream[newEnabledSourceCount];
|
||||||
newStreams);
|
int newEnabledSourceIndex = 0;
|
||||||
if (selectedTrackCounts[i] > 0) {
|
|
||||||
enabledSourceCount++;
|
// 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];
|
// Instantiate and return new streams.
|
||||||
enabledSourceCount = 0;
|
TrackStream[] streamsToReturn = new TrackStream[newSelections.size()];
|
||||||
for (int i = 0; i < sources.length; i++) {
|
for (int i = 0; i < newSelections.size(); i++) {
|
||||||
if (selectedTrackCounts[i] > 0) {
|
Pair<SmoothStreamingChunkSource, ChunkTrackStream> trackComponents =
|
||||||
enabledSources[enabledSourceCount++] = sources[i];
|
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
|
@Override
|
||||||
@ -225,103 +202,96 @@ public final class SmoothStreamingSampleSource implements SampleSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ChunkSampleSource source : enabledSources) {
|
for (ChunkTrackStream trackStream : trackStreams) {
|
||||||
source.continueBuffering(positionUs);
|
trackStream.continueBuffering(positionUs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long readReset() {
|
public long readReset() {
|
||||||
long resetPositionUs = C.UNSET_TIME_US;
|
if (pendingReset) {
|
||||||
for (ChunkSampleSource source : enabledSources) {
|
pendingReset = false;
|
||||||
long childResetPositionUs = source.readReset();
|
for (ChunkTrackStream trackStream : trackStreams) {
|
||||||
if (resetPositionUs == C.UNSET_TIME_US) {
|
trackStream.setReadingEnabled(true);
|
||||||
resetPositionUs = childResetPositionUs;
|
|
||||||
} else if (childResetPositionUs != C.UNSET_TIME_US) {
|
|
||||||
resetPositionUs = Math.min(resetPositionUs, childResetPositionUs);
|
|
||||||
}
|
}
|
||||||
|
return lastSeekPositionUs;
|
||||||
}
|
}
|
||||||
return resetPositionUs;
|
return C.UNSET_TIME_US;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getBufferedPositionUs() {
|
public long getBufferedPositionUs() {
|
||||||
long bufferedPositionUs = durationUs != C.UNSET_TIME_US ? durationUs : Long.MAX_VALUE;
|
long bufferedPositionUs = Long.MAX_VALUE;
|
||||||
for (ChunkSampleSource source : enabledSources) {
|
for (ChunkTrackStream trackStream : trackStreams) {
|
||||||
long rendererBufferedPositionUs = source.getBufferedPositionUs();
|
long rendererBufferedPositionUs = trackStream.getBufferedPositionUs();
|
||||||
if (rendererBufferedPositionUs == C.UNSET_TIME_US) {
|
if (rendererBufferedPositionUs != C.END_OF_SOURCE_US) {
|
||||||
return C.UNSET_TIME_US;
|
|
||||||
} else if (rendererBufferedPositionUs == C.END_OF_SOURCE_US) {
|
|
||||||
// This source is fully buffered.
|
|
||||||
} else {
|
|
||||||
bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
|
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
|
@Override
|
||||||
public void seekToUs(long positionUs) {
|
public void seekToUs(long positionUs) {
|
||||||
for (ChunkSampleSource source : enabledSources) {
|
lastSeekPositionUs = positionUs;
|
||||||
source.seekToUs(positionUs);
|
pendingReset = true;
|
||||||
|
for (ChunkTrackStream trackStream : trackStreams) {
|
||||||
|
trackStream.setReadingEnabled(false);
|
||||||
|
trackStream.seekToUs(positionUs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void release() {
|
public void release() {
|
||||||
manifestFetcher.release();
|
manifestFetcher.release();
|
||||||
for (ChunkSampleSource source : sources) {
|
for (SmoothStreamingChunkSource chunkSource : chunkSources) {
|
||||||
source.release();
|
chunkSource.release();
|
||||||
|
}
|
||||||
|
for (ChunkTrackStream trackStream : trackStreams) {
|
||||||
|
trackStream.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private int selectTracks(ChunkSampleSource source, List<TrackStream> allOldStreams,
|
private void buildTrackGroups(SmoothStreamingManifest manifest) {
|
||||||
List<TrackSelection> allNewSelections, long positionUs, TrackStream[] allNewStreams) {
|
int trackGroupCount = 0;
|
||||||
// Get the subset of the old streams for the source.
|
trackGroupElementIndices = new int[manifest.streamElements.length];
|
||||||
ArrayList<TrackStream> oldStreams = new ArrayList<>();
|
TrackGroup[] trackGroupArray = new TrackGroup[manifest.streamElements.length];
|
||||||
for (int i = 0; i < allOldStreams.size(); i++) {
|
for (int i = 0; i < manifest.streamElements.length; i++) {
|
||||||
TrackStream stream = allOldStreams.get(i);
|
StreamElement streamElement = manifest.streamElements[i];
|
||||||
if (trackStreamSources.get(stream) == source) {
|
int streamElementType = streamElement.type;
|
||||||
trackStreamSources.remove(stream);
|
Format[] formats = streamElement.formats;
|
||||||
oldStreams.add(stream);
|
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.
|
if (trackGroupCount < trackGroupArray.length) {
|
||||||
ArrayList<TrackSelection> newSelections = new ArrayList<>();
|
trackGroupElementIndices = Arrays.copyOf(trackGroupElementIndices, trackGroupCount);
|
||||||
int[] newSelectionOriginalIndices = new int[allNewSelections.size()];
|
trackGroupArray = Arrays.copyOf(trackGroupArray, trackGroupCount);
|
||||||
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()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Do nothing if nothing has changed, except during the first selection.
|
trackGroups = new TrackGroupArray(trackGroupArray);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pair<ChunkSampleSource, Integer> getSourceAndGroup(int group) {
|
private Pair<SmoothStreamingChunkSource, ChunkTrackStream> buildTrackStream(
|
||||||
int totalTrackGroupCount = 0;
|
TrackSelection selection, long positionUs) {
|
||||||
for (ChunkSampleSource source : sources) {
|
int[] selectedTracks = selection.getTracks();
|
||||||
int sourceTrackGroupCount = source.getTrackGroups().length;
|
FormatEvaluator adaptiveEvaluator = selectedTracks.length > 1
|
||||||
if (group < totalTrackGroupCount + sourceTrackGroupCount) {
|
? new AdaptiveEvaluator(bandwidthMeter) : null;
|
||||||
return Pair.create(source, group - totalTrackGroupCount);
|
int streamElementIndex = trackGroupElementIndices[selection.group];
|
||||||
}
|
StreamElement streamElement = currentManifest.streamElements[streamElementIndex];
|
||||||
totalTrackGroupCount += sourceTrackGroupCount;
|
int streamElementType = streamElement.type;
|
||||||
}
|
int bufferSize = Util.getDefaultBufferSize(streamElementType);
|
||||||
throw new IndexOutOfBoundsException();
|
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) {
|
private static byte[] getProtectionElementKeyId(byte[] initData) {
|
||||||
|
@ -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
|
* 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.
|
* filesystems. FAT32 is the most restrictive of all filesystems still commonly used today.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user