Commit 586e657b by aquilescanta Committed by Oliver Woodman

Allow opt-in HLS chunkless preparation

If allowed, the media period will try to finish preparation without downloading
chunks (similar to what DashMediaPeriod does). To create track groups,
HlsMediaPeriod will try to obtain as much information as possible from the
master playlist. If any vital information is missing for specific urls,
traditional preparation will take place instead.

This version does not support tracks with DrmInitData info. This affects tracks
with CDM DRM (e.g: Widevine, Clearkey, etc). AES_128 encryption is not affected.
This information needs to be obtained from media playlists, and this version
only takes the master playlist into account for preparation.

Issue:#3149

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=178098759
parent 8a0a8339
......@@ -2,6 +2,10 @@
### dev-v2 (not yet released) ###
* Add initial support for chunkless preparation in HLS. This allows an HLS media
source to finish preparation without donwloading any chunks, which might
considerably reduce the initial buffering time
([#3149](https://github.com/google/ExoPlayer/issues/2980)).
* Add ability for `SequenceableLoader` to reevaluate its buffer and discard
buffered media so that it can be re-buffered in a different quality.
* Replace `DefaultTrackSelector.Parameters` copy methods with a builder.
......
......@@ -63,8 +63,9 @@ public final class HlsMediaSource implements MediaSource,
private MediaSourceEventListener eventListener;
private Handler eventHandler;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private int minLoadableRetryCount;
private boolean allowChunklessPreparation;
private boolean isBuildCalled;
/**
......@@ -98,7 +99,6 @@ public final class HlsMediaSource implements MediaSource,
private Builder(Uri manifestUri, HlsDataSourceFactory hlsDataSourceFactory) {
this.manifestUri = manifestUri;
this.hlsDataSourceFactory = hlsDataSourceFactory;
minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT;
}
......@@ -171,6 +171,18 @@ public final class HlsMediaSource implements MediaSource,
}
/**
* Sets whether chunkless preparation is allowed. If true, preparation without chunk downloads
* will be enabled for streams that provide sufficient information in their master playlist.
*
* @param allowChunklessPreparation Whether chunkless preparation is allowed.
* @return This builder.
*/
public Builder setAllowChunklessPreparation(boolean allowChunklessPreparation) {
this.allowChunklessPreparation = allowChunklessPreparation;
return this;
}
/**
* Builds a new {@link HlsMediaSource} using the current parameters.
* <p>
* After this call, the builder should not be re-used.
......@@ -190,9 +202,16 @@ public final class HlsMediaSource implements MediaSource,
if (compositeSequenceableLoaderFactory == null) {
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
}
return new HlsMediaSource(manifestUri, hlsDataSourceFactory, extractorFactory,
compositeSequenceableLoaderFactory, minLoadableRetryCount, eventHandler, eventListener,
playlistParser);
return new HlsMediaSource(
manifestUri,
hlsDataSourceFactory,
extractorFactory,
compositeSequenceableLoaderFactory,
minLoadableRetryCount,
eventHandler,
eventListener,
playlistParser,
allowChunklessPreparation);
}
}
......@@ -209,6 +228,7 @@ public final class HlsMediaSource implements MediaSource,
private final int minLoadableRetryCount;
private final EventDispatcher eventDispatcher;
private final ParsingLoadable.Parser<HlsPlaylist> playlistParser;
private final boolean allowChunklessPreparation;
private HlsPlaylistTracker playlistTracker;
private Listener sourceListener;
......@@ -277,9 +297,16 @@ public final class HlsMediaSource implements MediaSource,
Handler eventHandler,
MediaSourceEventListener eventListener,
ParsingLoadable.Parser<HlsPlaylist> playlistParser) {
this(manifestUri, dataSourceFactory, extractorFactory,
new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, eventHandler,
eventListener, playlistParser);
this(
manifestUri,
dataSourceFactory,
extractorFactory,
new DefaultCompositeSequenceableLoaderFactory(),
minLoadableRetryCount,
eventHandler,
eventListener,
playlistParser,
false);
}
private HlsMediaSource(
......@@ -290,13 +317,15 @@ public final class HlsMediaSource implements MediaSource,
int minLoadableRetryCount,
Handler eventHandler,
MediaSourceEventListener eventListener,
ParsingLoadable.Parser<HlsPlaylist> playlistParser) {
ParsingLoadable.Parser<HlsPlaylist> playlistParser,
boolean allowChunklessPreparation) {
this.manifestUri = manifestUri;
this.dataSourceFactory = dataSourceFactory;
this.extractorFactory = extractorFactory;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
this.minLoadableRetryCount = minLoadableRetryCount;
this.playlistParser = playlistParser;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
this.allowChunklessPreparation = allowChunklessPreparation;
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
}
......@@ -317,8 +346,15 @@ public final class HlsMediaSource implements MediaSource,
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
Assertions.checkArgument(id.periodIndex == 0);
return new HlsMediaPeriod(extractorFactory, playlistTracker, dataSourceFactory,
minLoadableRetryCount, eventDispatcher, allocator, compositeSequenceableLoaderFactory);
return new HlsMediaPeriod(
extractorFactory,
playlistTracker,
dataSourceFactory,
minLoadableRetryCount,
eventDispatcher,
allocator,
compositeSequenceableLoaderFactory,
allowChunklessPreparation);
}
@Override
......
......@@ -52,7 +52,7 @@ import java.io.IOException;
@Override
public void maybeThrowError() throws IOException {
if (!ensureBoundSampleQueue()) {
if (!ensureBoundSampleQueue() && sampleStreamWrapper.isMappingFinished()) {
throw new SampleQueueMappingException(
sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType);
}
......
......@@ -173,13 +173,17 @@ import java.util.Arrays;
}
/**
* Prepares a sample stream wrapper for which the master playlist provides enough information to
* prepare.
* Prepares the sample stream wrapper with master playlist information.
*
* @param trackGroups This {@link TrackGroupArray} to expose.
* @param primaryTrackGroupIndex The index of the adaptive track group.
*/
public void prepareSingleTrack(Format format) {
track(0, C.TRACK_TYPE_UNKNOWN).format(format);
tracksEnded = true;
onTracksEnded();
public void prepareWithMasterPlaylistInfo(
TrackGroupArray trackGroups, int primaryTrackGroupIndex) {
prepared = true;
this.trackGroups = trackGroups;
this.primaryTrackGroupIndex = primaryTrackGroupIndex;
callback.onPrepared();
}
public void maybeThrowPrepareError() throws IOException {
......@@ -190,17 +194,30 @@ import java.util.Arrays;
return trackGroups;
}
public boolean isMappingFinished() {
return trackGroupToSampleQueueIndex != null;
}
public int bindSampleQueueToSampleStream(int trackGroupIndex) {
if (!isMappingFinished()) {
return C.INDEX_UNSET;
}
int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex];
if (sampleQueueIndex == C.INDEX_UNSET) {
return C.INDEX_UNSET;
}
setSampleQueueEnabledState(sampleQueueIndex, true);
if (sampleQueuesEnabledStates[sampleQueueIndex]) {
// This sample queue is already bound to a different sample stream.
return C.INDEX_UNSET;
}
sampleQueuesEnabledStates[sampleQueueIndex] = true;
return sampleQueueIndex;
}
public void unbindSampleQueue(int trackGroupIndex) {
setSampleQueueEnabledState(trackGroupToSampleQueueIndex[trackGroupIndex], false);
int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex];
Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex]);
sampleQueuesEnabledStates[sampleQueueIndex] = false;
}
/**
......@@ -693,7 +710,7 @@ import java.util.Arrays;
}
private void maybeFinishPrepare() {
if (released || prepared || !sampleQueuesBuilt) {
if (released || trackGroupToSampleQueueIndex != null || !sampleQueuesBuilt) {
return;
}
for (SampleQueue sampleQueue : sampleQueues) {
......@@ -701,9 +718,31 @@ import java.util.Arrays;
return;
}
}
buildTracks();
prepared = true;
callback.onPrepared();
if (trackGroups != null) {
// The track groups were created with master playlist information. They only need to be mapped
// to a sample queue.
mapSampleQueuesToMatchTrackGroups();
} else {
// Tracks are created using media segment information.
buildTracks();
prepared = true;
callback.onPrepared();
}
}
private void mapSampleQueuesToMatchTrackGroups() {
int trackGroupCount = trackGroups.length;
trackGroupToSampleQueueIndex = new int[trackGroupCount];
Arrays.fill(trackGroupToSampleQueueIndex, C.INDEX_UNSET);
for (int i = 0; i < trackGroupCount; i++) {
for (int queueIndex = 0; queueIndex < sampleQueues.length; queueIndex++) {
SampleQueue sampleQueue = sampleQueues[queueIndex];
if (formatsMatch(sampleQueue.getUpstreamFormat(), trackGroups.get(i).getFormat(0))) {
trackGroupToSampleQueueIndex[i] = queueIndex;
break;
}
}
}
}
/**
......@@ -794,17 +833,6 @@ import java.util.Arrays;
this.trackGroups = new TrackGroupArray(trackGroups);
}
/**
* Enables or disables a specified sample queue.
*
* @param sampleQueueIndex The index of the sample queue.
* @param enabledState True if the sample queue is being enabled, or false if it's being disabled.
*/
private void setSampleQueueEnabledState(int sampleQueueIndex, boolean enabledState) {
Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex] != enabledState);
sampleQueuesEnabledStates[sampleQueueIndex] = enabledState;
}
private HlsMediaChunk getLastMediaChunk() {
return mediaChunks.get(mediaChunks.size() - 1);
}
......@@ -868,4 +896,19 @@ import java.util.Arrays;
return chunk instanceof HlsMediaChunk;
}
private static boolean formatsMatch(Format manifestFormat, Format sampleFormat) {
String manifestFormatMimeType = manifestFormat.sampleMimeType;
String sampleFormatMimeType = sampleFormat.sampleMimeType;
int manifestFormatTrackType = MimeTypes.getTrackType(manifestFormatMimeType);
if (manifestFormatTrackType != C.TRACK_TYPE_TEXT) {
return manifestFormatTrackType == MimeTypes.getTrackType(sampleFormatMimeType);
} else if (!Util.areEqual(manifestFormatMimeType, sampleFormatMimeType)) {
return false;
}
if (MimeTypes.APPLICATION_CEA608.equals(manifestFormatMimeType)
|| MimeTypes.APPLICATION_CEA708.equals(manifestFormatMimeType)) {
return manifestFormat.accessibilityChannel == sampleFormat.accessibilityChannel;
}
return true;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment