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; + } }