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
This commit is contained in:
parent
8a0a8339e8
commit
586e657bd7
@ -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.
|
||||
|
@ -16,7 +16,6 @@
|
||||
package com.google.android.exoplayer2.source.hls;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
|
||||
@ -32,6 +31,8 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -55,6 +56,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||
private final TimestampAdjusterProvider timestampAdjusterProvider;
|
||||
private final Handler continueLoadingHandler;
|
||||
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||
private final boolean allowChunklessPreparation;
|
||||
|
||||
private Callback callback;
|
||||
private int pendingPrepareCount;
|
||||
@ -63,10 +65,15 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||
private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
|
||||
private SequenceableLoader compositeSequenceableLoader;
|
||||
|
||||
public HlsMediaPeriod(HlsExtractorFactory extractorFactory, HlsPlaylistTracker playlistTracker,
|
||||
HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount,
|
||||
EventDispatcher eventDispatcher, Allocator allocator,
|
||||
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) {
|
||||
public HlsMediaPeriod(
|
||||
HlsExtractorFactory extractorFactory,
|
||||
HlsPlaylistTracker playlistTracker,
|
||||
HlsDataSourceFactory dataSourceFactory,
|
||||
int minLoadableRetryCount,
|
||||
EventDispatcher eventDispatcher,
|
||||
Allocator allocator,
|
||||
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
|
||||
boolean allowChunklessPreparation) {
|
||||
this.extractorFactory = extractorFactory;
|
||||
this.playlistTracker = playlistTracker;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
@ -74,6 +81,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||
this.eventDispatcher = eventDispatcher;
|
||||
this.allocator = allocator;
|
||||
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
||||
this.allowChunklessPreparation = allowChunklessPreparation;
|
||||
streamWrapperIndices = new IdentityHashMap<>();
|
||||
timestampAdjusterProvider = new TimestampAdjusterProvider();
|
||||
continueLoadingHandler = new Handler();
|
||||
@ -293,15 +301,92 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||
|
||||
private void buildAndPrepareSampleStreamWrappers(long positionUs) {
|
||||
HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist();
|
||||
// Build the default stream wrapper.
|
||||
List<HlsUrl> audioRenditions = masterPlaylist.audios;
|
||||
List<HlsUrl> subtitleRenditions = masterPlaylist.subtitles;
|
||||
|
||||
int wrapperCount = 1 /* variants */ + audioRenditions.size() + subtitleRenditions.size();
|
||||
sampleStreamWrappers = new HlsSampleStreamWrapper[wrapperCount];
|
||||
pendingPrepareCount = wrapperCount;
|
||||
|
||||
buildAndPrepareMainSampleStreamWrapper(masterPlaylist, positionUs);
|
||||
int currentWrapperIndex = 1;
|
||||
|
||||
// TODO: Build video stream wrappers here.
|
||||
|
||||
// Audio sample stream wrappers.
|
||||
for (int i = 0; i < audioRenditions.size(); i++) {
|
||||
HlsUrl audioRendition = audioRenditions.get(i);
|
||||
HlsSampleStreamWrapper sampleStreamWrapper =
|
||||
buildSampleStreamWrapper(
|
||||
C.TRACK_TYPE_AUDIO,
|
||||
new HlsUrl[] {audioRendition},
|
||||
null,
|
||||
Collections.<Format>emptyList(),
|
||||
positionUs);
|
||||
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
|
||||
Format renditionFormat = audioRendition.format;
|
||||
if (allowChunklessPreparation && renditionFormat.codecs != null) {
|
||||
sampleStreamWrapper.prepareWithMasterPlaylistInfo(
|
||||
new TrackGroupArray(new TrackGroup(audioRendition.format)), 0);
|
||||
} else {
|
||||
sampleStreamWrapper.continuePreparing();
|
||||
}
|
||||
}
|
||||
|
||||
// Subtitle stream wrappers. We can always use master playlist information to prepare these.
|
||||
for (int i = 0; i < subtitleRenditions.size(); i++) {
|
||||
HlsUrl url = subtitleRenditions.get(i);
|
||||
HlsSampleStreamWrapper sampleStreamWrapper =
|
||||
buildSampleStreamWrapper(
|
||||
C.TRACK_TYPE_TEXT,
|
||||
new HlsUrl[] {url},
|
||||
null,
|
||||
Collections.<Format>emptyList(),
|
||||
positionUs);
|
||||
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
|
||||
sampleStreamWrapper.prepareWithMasterPlaylistInfo(
|
||||
new TrackGroupArray(new TrackGroup(url.format)), 0);
|
||||
}
|
||||
|
||||
// All wrappers are enabled during preparation.
|
||||
enabledSampleStreamWrappers = sampleStreamWrappers;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method creates and starts preparation of the main {@link HlsSampleStreamWrapper}.
|
||||
*
|
||||
* <p>The main sample stream wrapper is the first element of {@link #sampleStreamWrappers}. It
|
||||
* provides {@link SampleStream}s for the variant urls in the master playlist. It may be adaptive
|
||||
* and may contain multiple muxed tracks.
|
||||
*
|
||||
* <p>If chunkless preparation is allowed, the media period will try preparation without segment
|
||||
* downloads. This is only possible if variants contain the CODECS attribute. If not, traditional
|
||||
* preparation with segment downloads will take place. The following points apply to chunkless
|
||||
* preparation:
|
||||
*
|
||||
* <ul>
|
||||
* <li>A muxed audio track will be exposed if the codecs list contain an audio entry and the
|
||||
* master playlist either contains an EXT-X-MEDIA tag without the URI attribute or does not
|
||||
* contain any EXT-X-MEDIA tag.
|
||||
* <li>Closed captions will only be exposed if they are declared by the master playlist.
|
||||
* <li>ID3 tracks are not exposed.
|
||||
* </ul>
|
||||
*
|
||||
* @param masterPlaylist The HLS master playlist.
|
||||
* @param positionUs If preparation requires any chunk downloads, the position in microseconds at
|
||||
* which downloading should start. Ignored otherwise.
|
||||
*/
|
||||
private void buildAndPrepareMainSampleStreamWrapper(
|
||||
HlsMasterPlaylist masterPlaylist, long positionUs) {
|
||||
List<HlsUrl> selectedVariants = new ArrayList<>(masterPlaylist.variants);
|
||||
ArrayList<HlsUrl> definiteVideoVariants = new ArrayList<>();
|
||||
ArrayList<HlsUrl> definiteAudioOnlyVariants = new ArrayList<>();
|
||||
for (int i = 0; i < selectedVariants.size(); i++) {
|
||||
HlsUrl variant = selectedVariants.get(i);
|
||||
if (variant.format.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) {
|
||||
Format format = variant.format;
|
||||
if (format.height > 0 || Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_VIDEO) != null) {
|
||||
definiteVideoVariants.add(variant);
|
||||
} else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) {
|
||||
} else if (Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_AUDIO) != null) {
|
||||
definiteAudioOnlyVariants.add(variant);
|
||||
}
|
||||
}
|
||||
@ -317,43 +402,56 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||
} else {
|
||||
// Leave the enabled variants unchanged. They're likely either all video or all audio.
|
||||
}
|
||||
List<HlsUrl> audioRenditions = masterPlaylist.audios;
|
||||
List<HlsUrl> subtitleRenditions = masterPlaylist.subtitles;
|
||||
sampleStreamWrappers = new HlsSampleStreamWrapper[1 /* variants */ + audioRenditions.size()
|
||||
+ subtitleRenditions.size()];
|
||||
int currentWrapperIndex = 0;
|
||||
pendingPrepareCount = sampleStreamWrappers.length;
|
||||
|
||||
Assertions.checkArgument(!selectedVariants.isEmpty());
|
||||
HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()];
|
||||
selectedVariants.toArray(variants);
|
||||
HlsUrl[] variants = selectedVariants.toArray(new HlsUrl[0]);
|
||||
String codecs = variants[0].format.codecs;
|
||||
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT,
|
||||
variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats, positionUs);
|
||||
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
|
||||
sampleStreamWrapper.setIsTimestampMaster(true);
|
||||
sampleStreamWrapper.continuePreparing();
|
||||
sampleStreamWrappers[0] = sampleStreamWrapper;
|
||||
if (allowChunklessPreparation && codecs != null) {
|
||||
boolean variantsContainVideoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO) != null;
|
||||
boolean variantsContainAudioCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO) != null;
|
||||
List<TrackGroup> muxedTrackGroups = new ArrayList<>();
|
||||
if (variantsContainVideoCodecs) {
|
||||
Format[] videoFormats = new Format[selectedVariants.size()];
|
||||
for (int i = 0; i < videoFormats.length; i++) {
|
||||
videoFormats[i] = deriveVideoFormat(variants[i].format);
|
||||
}
|
||||
muxedTrackGroups.add(new TrackGroup(videoFormats));
|
||||
|
||||
// TODO: Build video stream wrappers here.
|
||||
|
||||
// Build audio stream wrappers.
|
||||
for (int i = 0; i < audioRenditions.size(); i++) {
|
||||
sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO,
|
||||
new HlsUrl[] {audioRenditions.get(i)}, null, Collections.<Format>emptyList(), positionUs);
|
||||
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
|
||||
if (variantsContainAudioCodecs
|
||||
&& (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) {
|
||||
muxedTrackGroups.add(
|
||||
new TrackGroup(
|
||||
deriveMuxedAudioFormat(
|
||||
variants[0].format, masterPlaylist.muxedAudioFormat, Format.NO_VALUE)));
|
||||
}
|
||||
List<Format> ccFormats = masterPlaylist.muxedCaptionFormats;
|
||||
if (ccFormats != null) {
|
||||
for (int i = 0; i < ccFormats.size(); i++) {
|
||||
muxedTrackGroups.add(new TrackGroup(ccFormats.get(i)));
|
||||
}
|
||||
}
|
||||
} else if (variantsContainAudioCodecs) {
|
||||
// Variants only contain audio.
|
||||
Format[] audioFormats = new Format[selectedVariants.size()];
|
||||
for (int i = 0; i < audioFormats.length; i++) {
|
||||
Format variantFormat = variants[i].format;
|
||||
audioFormats[i] =
|
||||
deriveMuxedAudioFormat(
|
||||
variantFormat, masterPlaylist.muxedAudioFormat, variantFormat.bitrate);
|
||||
}
|
||||
muxedTrackGroups.add(new TrackGroup(audioFormats));
|
||||
} else {
|
||||
// Variants contain codecs but no video or audio entries could be identified.
|
||||
throw new IllegalArgumentException("Unexpected codecs attribute: " + codecs);
|
||||
}
|
||||
sampleStreamWrapper.prepareWithMasterPlaylistInfo(
|
||||
new TrackGroupArray(muxedTrackGroups.toArray(new TrackGroup[0])), 0);
|
||||
} else {
|
||||
sampleStreamWrapper.setIsTimestampMaster(true);
|
||||
sampleStreamWrapper.continuePreparing();
|
||||
}
|
||||
|
||||
// Build subtitle stream wrappers.
|
||||
for (int i = 0; i < subtitleRenditions.size(); i++) {
|
||||
HlsUrl url = subtitleRenditions.get(i);
|
||||
sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null,
|
||||
Collections.<Format>emptyList(), positionUs);
|
||||
sampleStreamWrapper.prepareSingleTrack(url.format);
|
||||
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
|
||||
}
|
||||
|
||||
// All wrappers are enabled during preparation.
|
||||
enabledSampleStreamWrappers = sampleStreamWrappers;
|
||||
}
|
||||
|
||||
private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants,
|
||||
@ -375,18 +473,49 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean variantHasExplicitCodecWithPrefix(HlsUrl variant, String prefix) {
|
||||
String codecs = variant.format.codecs;
|
||||
if (TextUtils.isEmpty(codecs)) {
|
||||
return false;
|
||||
private static Format deriveVideoFormat(Format variantFormat) {
|
||||
String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO);
|
||||
String mimeType = MimeTypes.getMediaMimeType(codecs);
|
||||
return Format.createVideoSampleFormat(
|
||||
variantFormat.id,
|
||||
mimeType,
|
||||
codecs,
|
||||
variantFormat.bitrate,
|
||||
Format.NO_VALUE,
|
||||
variantFormat.width,
|
||||
variantFormat.height,
|
||||
variantFormat.frameRate,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
private static Format deriveMuxedAudioFormat(
|
||||
Format variantFormat, Format mediaTagFormat, int bitrate) {
|
||||
String codecs;
|
||||
int channelCount = Format.NO_VALUE;
|
||||
int selectionFlags = 0;
|
||||
String language = null;
|
||||
if (mediaTagFormat != null) {
|
||||
codecs = mediaTagFormat.codecs;
|
||||
channelCount = mediaTagFormat.channelCount;
|
||||
selectionFlags = mediaTagFormat.selectionFlags;
|
||||
language = mediaTagFormat.language;
|
||||
} else {
|
||||
codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)");
|
||||
for (String codec : codecArray) {
|
||||
if (codec.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
String mimeType = MimeTypes.getMediaMimeType(codecs);
|
||||
return Format.createAudioSampleFormat(
|
||||
variantFormat.id,
|
||||
mimeType,
|
||||
codecs,
|
||||
bitrate,
|
||||
Format.NO_VALUE,
|
||||
channelCount,
|
||||
Format.NO_VALUE,
|
||||
null,
|
||||
null,
|
||||
selectionFlags,
|
||||
language);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
@ -170,6 +170,18 @@ public final class HlsMediaSource implements MediaSource,
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user