From 586e657bd7fe9aeb194e40f2bff9dc3d3bbaff8e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 6 Dec 2017 08:06:51 -0800 Subject: [PATCH] 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 --- RELEASENOTES.md | 4 + .../exoplayer2/source/hls/HlsMediaPeriod.java | 229 ++++++++++++++---- .../exoplayer2/source/hls/HlsMediaSource.java | 60 ++++- .../source/hls/HlsSampleStream.java | 2 +- .../source/hls/HlsSampleStreamWrapper.java | 89 +++++-- 5 files changed, 298 insertions(+), 86 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 80c55c4706..920d80ee48 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index fd8f2bdbe9..24acf0f84d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -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 audioRenditions = masterPlaylist.audios; + List 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.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.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}. + * + *

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. + * + *

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: + * + *

    + *
  • 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. + *
  • Closed captions will only be exposed if they are declared by the master playlist. + *
  • ID3 tracks are not exposed. + *
+ * + * @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 selectedVariants = new ArrayList<>(masterPlaylist.variants); ArrayList definiteVideoVariants = new ArrayList<>(); ArrayList 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 audioRenditions = masterPlaylist.audios; - List 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 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.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 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.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); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 4e5783698a..1cddf6e94e 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -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. *

@@ -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 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 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 playlistParser) { + ParsingLoadable.Parser 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 diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index 301cd2920b..6563a5fba0 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -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); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 2e69e41d30..eba4596b7f 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -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; + } }