/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.chunk.BaseChunkSampleSourceEventListener; import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.DataChunk; import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.ts.AdtsExtractor; import com.google.android.exoplayer.extractor.ts.PtsTimestampAdjuster; import com.google.android.exoplayer.extractor.ts.TsExtractor; import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.UriUtil; import com.google.android.exoplayer.util.Util; import android.net.Uri; import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Locale; /** * A temporary test source of HLS chunks. * <p> * TODO: Figure out whether this should merge with the chunk package, or whether the hls * implementation is going to naturally diverge. */ public class HlsChunkSource { /** * Interface definition for a callback to be notified of {@link HlsChunkSource} events. */ public interface EventListener extends BaseChunkSampleSourceEventListener {} /** * Adaptive switching is disabled. * <p> * The initially selected variant will be used throughout playback. */ public static final int ADAPTIVE_MODE_NONE = 0; /** * Adaptive switches splice overlapping segments of the old and new variants. * <p> * When performing a switch from one variant to another, overlapping segments will be requested * from both the old and new variants. These segments will then be spliced together, allowing * a seamless switch from one variant to another even if keyframes are misaligned or if keyframes * are not positioned at the start of each segment. * <p> * Note that where it can be guaranteed that the source content has keyframes positioned at the * start of each segment, {@link #ADAPTIVE_MODE_ABRUPT} should always be used in preference to * this mode. */ public static final int ADAPTIVE_MODE_SPLICE = 1; /** * Adaptive switches are performed at segment boundaries. * <p> * For this mode to perform seamless switches, the source content is required to have keyframes * positioned at the start of each segment. If this is not the case a visual discontinuity may * be experienced when switching from one variant to another. * <p> * Note that where it can be guaranteed that the source content does have keyframes positioned at * the start of each segment, this mode should always be used in preference to * {@link #ADAPTIVE_MODE_SPLICE} because it requires fetching less data. */ public static final int ADAPTIVE_MODE_ABRUPT = 3; /** * The default minimum duration of media that needs to be buffered for a switch to a higher * quality variant to be considered. */ public static final long DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS = 5000; /** * The default maximum duration of media that needs to be buffered for a switch to a lower * quality variant to be considered. */ public static final long DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS = 20000; /** * The default time for which a media playlist should be blacklisted. */ public static final long DEFAULT_PLAYLIST_BLACKLIST_MS = 60000; private static final String TAG = "HlsChunkSource"; private static final String AAC_FILE_EXTENSION = ".aac"; private static final float BANDWIDTH_FRACTION = 0.8f; private final DataSource dataSource; private final HlsPlaylistParser playlistParser; private final BandwidthMeter bandwidthMeter; private final int adaptiveMode; private final String baseUri; private final int adaptiveMaxWidth; private final int adaptiveMaxHeight; private final long minBufferDurationToSwitchUpUs; private final long maxBufferDurationToSwitchDownUs; // A list of variants considered during playback, ordered by decreasing bandwidth. The following // three arrays are of the same length and are ordered in the same way (i.e. variantPlaylists[i], // variantLastPlaylistLoadTimesMs[i] and variantBlacklistTimes[i] all correspond to variants[i]). private final Variant[] variants; private final HlsMediaPlaylist[] variantPlaylists; private final long[] variantLastPlaylistLoadTimesMs; private final long[] variantBlacklistTimes; // The index in variants of the currently selected variant. private int selectedVariantIndex; private byte[] scratchSpace; private boolean live; private long durationUs; private IOException fatalError; private PtsTimestampAdjuster ptsTimestampAdjuster; private Uri encryptionKeyUri; private byte[] encryptionKey; private String encryptionIvString; private byte[] encryptionIv; public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist, BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode) { this(dataSource, playlistUrl, playlist, bandwidthMeter, variantIndices, adaptiveMode, DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS); } /** * @param dataSource A {@link DataSource} suitable for loading the media data. * @param playlistUrl The playlist URL. * @param playlist The hls playlist. * @param bandwidthMeter provides an estimate of the currently available bandwidth. * @param variantIndices If {@code playlist} is a {@link HlsMasterPlaylist}, the subset of variant * indices to consider, or null to consider all of the variants. For other playlist types * this parameter is ignored. * @param adaptiveMode The mode for switching from one variant to another. One of * {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and * {@link #ADAPTIVE_MODE_SPLICE}. * @param minBufferDurationToSwitchUpMs The minimum duration of media that needs to be buffered * for a switch to a higher quality variant to be considered. * @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered * for a switch to a lower quality variant to be considered. */ public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist, BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode, long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs) { this.dataSource = dataSource; this.bandwidthMeter = bandwidthMeter; this.adaptiveMode = adaptiveMode; minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000; maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000; baseUri = playlist.baseUri; playlistParser = new HlsPlaylistParser(); if (playlist.type == HlsPlaylist.TYPE_MEDIA) { variants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)}; variantPlaylists = new HlsMediaPlaylist[1]; variantLastPlaylistLoadTimesMs = new long[1]; variantBlacklistTimes = new long[1]; setMediaPlaylist(0, (HlsMediaPlaylist) playlist); // We won't be adapting between different variants. adaptiveMaxWidth = MediaFormat.NO_VALUE; adaptiveMaxHeight = MediaFormat.NO_VALUE; } else { List<Variant> masterPlaylistVariants = ((HlsMasterPlaylist) playlist).variants; variants = buildOrderedVariants(masterPlaylistVariants, variantIndices); variantPlaylists = new HlsMediaPlaylist[variants.length]; variantLastPlaylistLoadTimesMs = new long[variants.length]; variantBlacklistTimes = new long[variants.length]; int maxWidth = -1; int maxHeight = -1; // Select the variant that comes first in their original order in the master playlist. int minOriginalVariantIndex = Integer.MAX_VALUE; for (int i = 0; i < variants.length; i++) { int originalVariantIndex = masterPlaylistVariants.indexOf(variants[i]); if (originalVariantIndex < minOriginalVariantIndex) { minOriginalVariantIndex = originalVariantIndex; selectedVariantIndex = i; } Format variantFormat = variants[i].format; maxWidth = Math.max(variantFormat.width, maxWidth); maxHeight = Math.max(variantFormat.height, maxHeight); } if (variants.length <= 1 || adaptiveMode == ADAPTIVE_MODE_NONE) { // We won't be adapting between different variants. this.adaptiveMaxWidth = MediaFormat.NO_VALUE; this.adaptiveMaxHeight = MediaFormat.NO_VALUE; } else { // We will be adapting between different variants. // TODO: We should allow the default values to be passed through the constructor. this.adaptiveMaxWidth = maxWidth > 0 ? maxWidth : 1920; this.adaptiveMaxHeight = maxHeight > 0 ? maxHeight : 1080; } } } public long getDurationUs() { return durationUs; } /** * If the source is currently having difficulty providing chunks, then this method throws the * underlying error. Otherwise does nothing. * * @throws IOException The underlying error. */ public void maybeThrowError() throws IOException { if (fatalError != null) { throw fatalError; } } /** * Updates the provided {@link ChunkOperationHolder} to contain the next operation that should * be performed by the calling {@link HlsSampleSource}. * * @param previousTsChunk The previously loaded chunk that the next chunk should follow. * @param seekPositionUs If there is no previous chunk, this parameter must specify the seek * position. If there is a previous chunk then this parameter is ignored. * @param playbackPositionUs The current playback position. * @param out The holder to populate with the result. {@link ChunkOperationHolder#queueSize} is * unused. */ public void getChunkOperation(TsChunk previousTsChunk, long seekPositionUs, long playbackPositionUs, ChunkOperationHolder out) { int nextVariantIndex; boolean switchingVariantSpliced; if (adaptiveMode == ADAPTIVE_MODE_NONE) { nextVariantIndex = selectedVariantIndex; switchingVariantSpliced = false; } else { nextVariantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs); switchingVariantSpliced = previousTsChunk != null && !variants[nextVariantIndex].format.equals(previousTsChunk.format) && adaptiveMode == ADAPTIVE_MODE_SPLICE; } HlsMediaPlaylist mediaPlaylist = variantPlaylists[nextVariantIndex]; if (mediaPlaylist == null) { // We don't have the media playlist for the next variant. Request it now. out.chunk = newMediaPlaylistChunk(nextVariantIndex); return; } selectedVariantIndex = nextVariantIndex; int chunkMediaSequence = 0; boolean liveDiscontinuity = false; if (live) { if (previousTsChunk == null) { chunkMediaSequence = getLiveStartChunkMediaSequence(nextVariantIndex); } else { chunkMediaSequence = switchingVariantSpliced ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1; if (chunkMediaSequence < mediaPlaylist.mediaSequence) { // TODO: Decide what we want to do with: https://github.com/google/ExoPlayer/issues/765 // if (allowSkipAhead) { // If the chunk is no longer in the playlist. Skip ahead and start again. chunkMediaSequence = getLiveStartChunkMediaSequence(nextVariantIndex); liveDiscontinuity = true; // } else { // fatalError = new BehindLiveWindowException(); // return null; // } } } } else { // Not live. if (previousTsChunk == null) { chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true, true) + mediaPlaylist.mediaSequence; } else { chunkMediaSequence = switchingVariantSpliced ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1; } } int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence; if (chunkIndex >= mediaPlaylist.segments.size()) { if (!mediaPlaylist.live) { out.endOfStream = true; } else if (shouldRerequestLiveMediaPlaylist(nextVariantIndex)) { out.chunk = newMediaPlaylistChunk(nextVariantIndex); } return; } HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); // Check if encryption is specified. if (segment.isEncrypted) { Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); if (!keyUri.equals(encryptionKeyUri)) { // Encryption is specified and the key has changed. out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex); return; } if (!Util.areEqual(segment.encryptionIV, encryptionIvString)) { setEncryptionData(keyUri, segment.encryptionIV, encryptionKey); } } else { clearEncryptionData(); } // Configure the data source and spec for the chunk. DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); // Compute start and end times, and the sequence number of the next chunk. long startTimeUs; if (live) { if (previousTsChunk == null) { startTimeUs = 0; } else if (switchingVariantSpliced) { startTimeUs = previousTsChunk.startTimeUs; } else { startTimeUs = previousTsChunk.endTimeUs; } } else /* Not live */ { startTimeUs = segment.startTimeUs; } long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND); int trigger = Chunk.TRIGGER_UNSPECIFIED; Format format = variants[selectedVariantIndex].format; // Configure the extractor that will read the chunk. HlsExtractorWrapper extractorWrapper; if (previousTsChunk == null || segment.discontinuity || liveDiscontinuity || !format.equals(previousTsChunk.format)) { Extractor extractor; if (chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION)) { extractor = new AdtsExtractor(startTimeUs); } else { if (previousTsChunk == null || segment.discontinuity || liveDiscontinuity || ptsTimestampAdjuster == null) { // TODO: Use this for AAC as well, along with the ID3 PRIV priv tag values with owner // identifier com.apple.streaming.transportStreamTimestamp. ptsTimestampAdjuster = new PtsTimestampAdjuster(startTimeUs); } extractor = new TsExtractor(ptsTimestampAdjuster); } extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, switchingVariantSpliced, adaptiveMaxWidth, adaptiveMaxHeight); } else { extractorWrapper = previousTsChunk.extractorWrapper; } out.chunk = new TsChunk(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkMediaSequence, extractorWrapper, encryptionKey, encryptionIv); } /** * Invoked when the {@link HlsSampleSource} has finished loading a chunk obtained from this * source. * * @param chunk The chunk whose load has been completed. */ public void onChunkLoadCompleted(Chunk chunk) { if (chunk instanceof MediaPlaylistChunk) { MediaPlaylistChunk mediaPlaylistChunk = (MediaPlaylistChunk) chunk; scratchSpace = mediaPlaylistChunk.getDataHolder(); setMediaPlaylist(mediaPlaylistChunk.variantIndex, mediaPlaylistChunk.getResult()); } else if (chunk instanceof EncryptionKeyChunk) { EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk; scratchSpace = encryptionKeyChunk.getDataHolder(); setEncryptionData(encryptionKeyChunk.dataSpec.uri, encryptionKeyChunk.iv, encryptionKeyChunk.getResult()); } } /** * Invoked when the {@link HlsSampleSource} encounters an error loading a chunk obtained from * this source. * * @param chunk The chunk whose load encountered the error. * @param e The error. * @return True if the error was handled by the source. False otherwise. */ public boolean onChunkLoadError(Chunk chunk, IOException e) { if (chunk.bytesLoaded() == 0 && (chunk instanceof TsChunk || chunk instanceof MediaPlaylistChunk || chunk instanceof EncryptionKeyChunk) && (e instanceof InvalidResponseCodeException)) { InvalidResponseCodeException responseCodeException = (InvalidResponseCodeException) e; int responseCode = responseCodeException.responseCode; if (responseCode == 404 || responseCode == 410) { int variantIndex; if (chunk instanceof TsChunk) { TsChunk tsChunk = (TsChunk) chunk; variantIndex = getVariantIndex(tsChunk.format); } else if (chunk instanceof MediaPlaylistChunk) { MediaPlaylistChunk playlistChunk = (MediaPlaylistChunk) chunk; variantIndex = playlistChunk.variantIndex; } else { EncryptionKeyChunk encryptionChunk = (EncryptionKeyChunk) chunk; variantIndex = encryptionChunk.variantIndex; } boolean alreadyBlacklisted = variantBlacklistTimes[variantIndex] != 0; variantBlacklistTimes[variantIndex] = SystemClock.elapsedRealtime(); if (alreadyBlacklisted) { // The playlist was already blacklisted. Log.w(TAG, "Already blacklisted variant (" + responseCode + "): " + chunk.dataSpec.uri); return false; } else if (!allVariantsBlacklisted()) { // We've handled the 404/410 by blacklisting the variant. Log.w(TAG, "Blacklisted variant (" + responseCode + "): " + chunk.dataSpec.uri); return true; } else { // This was the last non-blacklisted playlist. Don't blacklist it. Log.w(TAG, "Final variant not blacklisted (" + responseCode + "): " + chunk.dataSpec.uri); variantBlacklistTimes[variantIndex] = 0; return false; } } } return false; } public void reset() { fatalError = null; } private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) { clearStaleBlacklistedVariants(); long bitrateEstimate = bandwidthMeter.getBitrateEstimate(); if (variantBlacklistTimes[selectedVariantIndex] != 0) { // The current variant has been blacklisted, so we have no choice but to re-evaluate. return getVariantIndexForBandwidth(bitrateEstimate); } if (previousTsChunk == null) { // Don't consider switching if we don't have a previous chunk. return selectedVariantIndex; } if (bitrateEstimate == BandwidthMeter.NO_ESTIMATE) { // Don't consider switching if we don't have a bandwidth estimate. return selectedVariantIndex; } int idealIndex = getVariantIndexForBandwidth(bitrateEstimate); if (idealIndex == selectedVariantIndex) { // We're already using the ideal variant. return selectedVariantIndex; } // We're not using the ideal variant for the available bandwidth, but only switch if the // conditions are appropriate. long bufferedPositionUs = adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs : previousTsChunk.endTimeUs; long bufferedUs = bufferedPositionUs - playbackPositionUs; if (variantBlacklistTimes[selectedVariantIndex] != 0 || (idealIndex > selectedVariantIndex && bufferedUs < maxBufferDurationToSwitchDownUs) || (idealIndex < selectedVariantIndex && bufferedUs > minBufferDurationToSwitchUpUs)) { // Switch variant. return idealIndex; } // Stick with the current variant for now. return selectedVariantIndex; } private int getVariantIndexForBandwidth(long bitrateEstimate) { if (bitrateEstimate == BandwidthMeter.NO_ESTIMATE) { // Select the lowest quality. bitrateEstimate = 0; } int effectiveBitrate = (int) (bitrateEstimate * BANDWIDTH_FRACTION); int lowestQualityEnabledVariantIndex = -1; for (int i = 0; i < variants.length; i++) { if (variantBlacklistTimes[i] == 0) { if (variants[i].format.bitrate <= effectiveBitrate) { return i; } lowestQualityEnabledVariantIndex = i; } } // At least one variant should always be enabled. Assertions.checkState(lowestQualityEnabledVariantIndex != -1); return lowestQualityEnabledVariantIndex; } private boolean shouldRerequestLiveMediaPlaylist(int nextVariantIndex) { // Don't re-request media playlist more often than one-half of the target duration. HlsMediaPlaylist mediaPlaylist = variantPlaylists[nextVariantIndex]; long timeSinceLastMediaPlaylistLoadMs = SystemClock.elapsedRealtime() - variantLastPlaylistLoadTimesMs[nextVariantIndex]; return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2; } private int getLiveStartChunkMediaSequence(int variantIndex) { // For live start playback from the third chunk from the end. HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex]; int chunkIndex = mediaPlaylist.segments.size() > 3 ? mediaPlaylist.segments.size() - 3 : 0; return chunkIndex + mediaPlaylist.mediaSequence; } private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) { Uri mediaPlaylistUri = UriUtil.resolveToUri(baseUri, variants[variantIndex].url); DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null, DataSpec.FLAG_ALLOW_GZIP); return new MediaPlaylistChunk(dataSource, dataSpec, scratchSpace, playlistParser, variantIndex, mediaPlaylistUri.toString()); } private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex) { DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null, DataSpec.FLAG_ALLOW_GZIP); return new EncryptionKeyChunk(dataSource, dataSpec, scratchSpace, iv, variantIndex); } private void setEncryptionData(Uri keyUri, String iv, byte[] secretKey) { String trimmedIv; if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) { trimmedIv = iv.substring(2); } else { trimmedIv = iv; } byte[] ivData = new BigInteger(trimmedIv, 16).toByteArray(); byte[] ivDataWithPadding = new byte[16]; int offset = ivData.length > 16 ? ivData.length - 16 : 0; System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length + offset, ivData.length - offset); encryptionKeyUri = keyUri; encryptionKey = secretKey; encryptionIvString = iv; encryptionIv = ivDataWithPadding; } private void clearEncryptionData() { encryptionKeyUri = null; encryptionKey = null; encryptionIvString = null; encryptionIv = null; } private void setMediaPlaylist(int variantIndex, HlsMediaPlaylist mediaPlaylist) { variantLastPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime(); variantPlaylists[variantIndex] = mediaPlaylist; live |= mediaPlaylist.live; durationUs = live ? C.UNKNOWN_TIME_US : mediaPlaylist.durationUs; } /** * Selects a list of variants to use, returning them in order of decreasing bandwidth. * * @param originalVariants The original list of variants. * @param originalVariantIndices Indices of variants that in the original list that can be * considered, or null to allow all variants to be considered. * @return The set of enabled variants in decreasing bandwidth order. */ private static Variant[] buildOrderedVariants(List<Variant> originalVariants, int[] originalVariantIndices) { ArrayList<Variant> enabledVariantList = new ArrayList<>(); if (originalVariantIndices != null) { for (int i = 0; i < originalVariantIndices.length; i++) { enabledVariantList.add(originalVariants.get(originalVariantIndices[i])); } } else { // If variantIndices is null then all variants are initially considered. enabledVariantList.addAll(originalVariants); } ArrayList<Variant> definiteVideoVariants = new ArrayList<>(); ArrayList<Variant> definiteAudioOnlyVariants = new ArrayList<>(); for (int i = 0; i < enabledVariantList.size(); i++) { Variant variant = enabledVariantList.get(i); if (variant.format.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) { definiteVideoVariants.add(variant); } else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) { definiteAudioOnlyVariants.add(variant); } } if (!definiteVideoVariants.isEmpty()) { // We've identified some variants as definitely containing video. Assume variants within the // master playlist are marked consistently, and hence that we have the full set. Filter out // any other variants, which are likely to be audio only. enabledVariantList = definiteVideoVariants; } else if (definiteAudioOnlyVariants.size() < enabledVariantList.size()) { // We've identified some variants, but not all, as being audio only. Filter them out to leave // the remaining variants, which are likely to contain video. enabledVariantList.removeAll(definiteAudioOnlyVariants); } else { // Leave the enabled variants unchanged. They're likely either all video or all audio. } Variant[] enabledVariants = new Variant[enabledVariantList.size()]; enabledVariantList.toArray(enabledVariants); Arrays.sort(enabledVariants, new Comparator<Variant>() { private final Comparator<Format> formatComparator = new Format.DecreasingBandwidthComparator(); @Override public int compare(Variant first, Variant second) { return formatComparator.compare(first.format, second.format); } }); return enabledVariants; } private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) { String codecs = variant.format.codecs; if (TextUtils.isEmpty(codecs)) { return false; } String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)"); for (int i = 0; i < codecArray.length; i++) { if (codecArray[i].startsWith(prefix)) { return true; } } return false; } private boolean allVariantsBlacklisted() { for (int i = 0; i < variantBlacklistTimes.length; i++) { if (variantBlacklistTimes[i] == 0) { return false; } } return true; } private void clearStaleBlacklistedVariants() { long currentTime = SystemClock.elapsedRealtime(); for (int i = 0; i < variantBlacklistTimes.length; i++) { if (variantBlacklistTimes[i] != 0 && currentTime - variantBlacklistTimes[i] > DEFAULT_PLAYLIST_BLACKLIST_MS) { variantBlacklistTimes[i] = 0; } } } private int getVariantIndex(Format format) { for (int i = 0; i < variants.length; i++) { if (variants[i].format.equals(format)) { return i; } } // Should never happen. throw new IllegalStateException("Invalid format: " + format); } private static class MediaPlaylistChunk extends DataChunk { public final int variantIndex; private final HlsPlaylistParser playlistParser; private final String playlistUrl; private HlsMediaPlaylist result; public MediaPlaylistChunk(DataSource dataSource, DataSpec dataSpec, byte[] scratchSpace, HlsPlaylistParser playlistParser, int variantIndex, String playlistUrl) { super(dataSource, dataSpec, Chunk.TYPE_MANIFEST, Chunk.TRIGGER_UNSPECIFIED, null, Chunk.NO_PARENT_ID, scratchSpace); this.variantIndex = variantIndex; this.playlistParser = playlistParser; this.playlistUrl = playlistUrl; } @Override protected void consume(byte[] data, int limit) throws IOException { result = (HlsMediaPlaylist) playlistParser.parse(playlistUrl, new ByteArrayInputStream(data, 0, limit)); } public HlsMediaPlaylist getResult() { return result; } } private static class EncryptionKeyChunk extends DataChunk { public final String iv; public final int variantIndex; private byte[] result; public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, byte[] scratchSpace, String iv, int variantIndex) { super(dataSource, dataSpec, Chunk.TYPE_DRM, Chunk.TRIGGER_UNSPECIFIED, null, Chunk.NO_PARENT_ID, scratchSpace); this.iv = iv; this.variantIndex = variantIndex; } @Override protected void consume(byte[] data, int limit) throws IOException { result = Arrays.copyOf(data, limit); } public byte[] getResult() { return result; } } }