From 2690f569af8e861bfba3e650176bb935996db42d Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 14 Jan 2016 07:24:03 -0800 Subject: [PATCH] Enable track selection + WebVTT for HLS. See the documentation of buildTracks for the gory details. Issue: #151 Issue: #676 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=112149293 --- .../demo/player/HlsRendererBuilder.java | 33 ++- .../google/android/exoplayer/MediaFormat.java | 7 + .../android/exoplayer/hls/HlsChunkSource.java | 24 +- .../exoplayer/hls/HlsSampleSource.java | 223 +++++++++++++++--- 4 files changed, 239 insertions(+), 48 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java index ad9ef442e0..242faeace4 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java @@ -25,12 +25,14 @@ import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.hls.DefaultHlsTrackSelector; import com.google.android.exoplayer.hls.HlsChunkSource; +import com.google.android.exoplayer.hls.HlsMasterPlaylist; import com.google.android.exoplayer.hls.HlsPlaylist; import com.google.android.exoplayer.hls.HlsPlaylistParser; import com.google.android.exoplayer.hls.HlsSampleSource; import com.google.android.exoplayer.hls.PtsTimestampAdjusterProvider; import com.google.android.exoplayer.metadata.Id3Parser; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; +import com.google.android.exoplayer.text.TextTrackRenderer; import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultAllocator; @@ -53,7 +55,8 @@ import java.util.Map; public class HlsRendererBuilder implements RendererBuilder { private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; - private static final int BUFFER_SEGMENTS = 256; + private static final int MAIN_BUFFER_SEGMENTS = 256; + private static final int TEXT_BUFFER_SEGMENTS = 2; private final Context context; private final String userAgent; @@ -127,13 +130,15 @@ public class HlsRendererBuilder implements RendererBuilder { Handler mainHandler = player.getMainHandler(); LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider(); + // Build the video/audio/metadata renderers. DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); HlsChunkSource chunkSource = new HlsChunkSource(true /* isMaster */, dataSource, url, manifest, DefaultHlsTrackSelector.newDefaultInstance(context), bandwidthMeter, - new PtsTimestampAdjusterProvider(), HlsChunkSource.ADAPTIVE_MODE_SPLICE); + timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl, - BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_VIDEO); + MAIN_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_VIDEO); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, mainHandler, player, 50); @@ -142,14 +147,30 @@ public class HlsRendererBuilder implements RendererBuilder { AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); MetadataTrackRenderer> id3Renderer = new MetadataTrackRenderer<>( sampleSource, new Id3Parser(), player, mainHandler.getLooper()); - Eia608TrackRenderer closedCaptionRenderer = new Eia608TrackRenderer(sampleSource, player, - mainHandler.getLooper()); + + // Build the text renderer, preferring Webvtt where available. + boolean preferWebvtt = false; + if (manifest instanceof HlsMasterPlaylist) { + preferWebvtt = !((HlsMasterPlaylist) manifest).subtitles.isEmpty(); + } + TrackRenderer textRenderer; + if (preferWebvtt) { + DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); + HlsChunkSource textChunkSource = new HlsChunkSource(false /* isMaster */, textDataSource, + url, manifest, DefaultHlsTrackSelector.newVttInstance(), bandwidthMeter, + timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); + HlsSampleSource textSampleSource = new HlsSampleSource(textChunkSource, loadControl, + TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_TEXT); + textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper()); + } else { + textRenderer = new Eia608TrackRenderer(sampleSource, player, mainHandler.getLooper()); + } TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; renderers[DemoPlayer.TYPE_METADATA] = id3Renderer; - renderers[DemoPlayer.TYPE_TEXT] = closedCaptionRenderer; + renderers[DemoPlayer.TYPE_TEXT] = textRenderer; player.onRenderers(renderers, bandwidthMeter); } diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index d1233d00a5..9978221a74 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -234,6 +234,13 @@ public final class MediaFormat { subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight); } + public MediaFormat copyWithFixedTrackInfo(String trackId, int bitrate, int width, int height, + String language) { + return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height, + rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language, + subsampleOffsetUs, initializationData, adaptive, NO_VALUE, NO_VALUE); + } + public MediaFormat copyAsAdaptive(String trackId) { return new MediaFormat(trackId, mimeType, NO_VALUE, NO_VALUE, durationUs, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, null, true, maxWidth, diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java index 89a70fc69f..fe3271a3f7 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -133,7 +133,7 @@ public class HlsChunkSource implements HlsTrackSelector.Output { // TODO: Expose tracks. private final ArrayList tracks; - private ExposedTrack enabledTrack; + private int selectedTrackIndex; // 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], @@ -295,6 +295,18 @@ public class HlsChunkSource implements HlsTrackSelector.Output { return variants.length == 1 ? variants[0] : null; } + + /** + * Returns the currently selected track index. + *

+ * This method should only be called after the source has been prepared. + * + * @return The currently selected track index. + */ + public int getSelectedTrackIndex() { + return selectedTrackIndex; + } + /** * Selects a track for use. *

@@ -303,9 +315,10 @@ public class HlsChunkSource implements HlsTrackSelector.Output { * @param index The track index. */ public void selectTrack(int index) { - enabledTrack = tracks.get(index); - selectedVariantIndex = enabledTrack.defaultVariantIndex; - variants = enabledTrack.variants; + selectedTrackIndex = index; + ExposedTrack selectedTrack = tracks.get(selectedTrackIndex); + selectedVariantIndex = selectedTrack.defaultVariantIndex; + variants = selectedTrack.variants; variantPlaylists = new HlsMediaPlaylist[variants.length]; variantLastPlaylistLoadTimesMs = new long[variants.length]; variantBlacklistTimes = new long[variants.length]; @@ -472,9 +485,10 @@ public class HlsChunkSource implements HlsTrackSelector.Output { // The master source has yet to instantiate an adjuster for the discontinuity sequence. return; } + ExposedTrack selectedTrack = tracks.get(selectedTrackIndex); Extractor extractor = new TsExtractor(timestampAdjuster); extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, - switchingVariantSpliced, enabledTrack.adaptiveMaxWidth, enabledTrack.adaptiveMaxHeight); + switchingVariantSpliced, selectedTrack.adaptiveMaxWidth, selectedTrack.adaptiveMaxHeight); } else { // MPEG-2 TS segments, and we need to continue using the same extractor. extractorWrapper = previousTsChunk.extractorWrapper; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index 4557113bb0..cecbaa55a0 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -36,6 +36,7 @@ import android.os.Handler; import android.os.SystemClock; import java.io.IOException; +import java.util.Arrays; import java.util.LinkedList; /** @@ -55,6 +56,11 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, private static final long NO_RESET_PENDING = Long.MIN_VALUE; + private static final int PRIMARY_TYPE_NONE = 0; + private static final int PRIMARY_TYPE_TEXT = 1; + private static final int PRIMARY_TYPE_AUDIO = 2; + private static final int PRIMARY_TYPE_VIDEO = 3; + private final HlsChunkSource chunkSource; private final LinkedList extractors; private final int minLoadableRetryCount; @@ -71,11 +77,22 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, private boolean loadControlRegistered; private int trackCount; private int enabledTrackCount; + + private Format downstreamFormat; + + // Tracks are complicated in HLS. See documentation of buildTracks for details. + // Indexed by track (as exposed by this source). + private MediaFormat[] trackFormats; private boolean[] trackEnabledStates; private boolean[] pendingDiscontinuities; - private MediaFormat[] trackFormat; private MediaFormat[] downstreamMediaFormats; - private Format downstreamFormat; + // Maps track index (as exposed by this source) to the corresponding chunk source track index for + // primary tracks, or to -1 otherwise. + private int[] chunkSourceTrackIndices; + // Maps track index (as exposed by this source) to the corresponding extractor track index. + private int[] extractorTrackIndices; + // Indexed by extractor track index. + private boolean[] extractorTrackEnabledStates; private long downstreamPositionUs; private long lastSeekPositionUs; @@ -137,20 +154,9 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, // We're not prepared, but we might have loaded what we need. HlsExtractorWrapper extractor = extractors.getFirst(); if (extractor.isPrepared()) { - trackCount = extractor.getTrackCount(); - trackEnabledStates = new boolean[trackCount]; - pendingDiscontinuities = new boolean[trackCount]; - downstreamMediaFormats = new MediaFormat[trackCount]; - trackFormat = new MediaFormat[trackCount]; - long durationUs = chunkSource.getDurationUs(); - for (int i = 0; i < trackCount; i++) { - MediaFormat format = extractor.getMediaFormat(i).copyWithDurationUs(durationUs); - if (MimeTypes.isVideo(format.mimeType)) { - format = format.copyAsAdaptive(null); - } - trackFormat[i] = format; - } + buildTracks(extractor); prepared = true; + maybeStartLoading(); // Update the load control. return true; } else if (extractors.size() > 1) { extractors.removeFirst().clear(); @@ -185,15 +191,13 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, @Override public MediaFormat getFormat(int track) { Assertions.checkState(prepared); - return trackFormat[track]; + return trackFormats[track]; } @Override public void enable(int track, long positionUs) { Assertions.checkState(prepared); - Assertions.checkState(!trackEnabledStates[track]); - enabledTrackCount++; - trackEnabledStates[track] = true; + setTrackEnabledState(track, true); downstreamMediaFormats[track] = null; pendingDiscontinuities[track] = false; downstreamFormat = null; @@ -202,6 +206,16 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, loadControl.register(this, bufferSizeContribution); loadControlRegistered = true; } + int chunkSourceTrack = chunkSourceTrackIndices[track]; + if (chunkSourceTrack != -1 && chunkSourceTrack != chunkSource.getSelectedTrackIndex()) { + // This is a primary track whose corresponding chunk source track is different to the one + // currently selected. We need to change the selection and restart. Since other exposed tracks + // may be enabled too, we need to implement the restart as a seek so that all downstream + // renderers receive a discontinuity event. + chunkSource.selectTrack(chunkSourceTrack); + seekToInternal(positionUs); + return; + } if (enabledTrackCount == 1) { lastSeekPositionUs = positionUs; if (wasLoadControlRegistered && downstreamPositionUs == positionUs) { @@ -220,9 +234,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, @Override public void disable(int track) { Assertions.checkState(prepared); - Assertions.checkState(trackEnabledStates[track]); - enabledTrackCount--; - trackEnabledStates[track] = false; + setTrackEnabledState(track, false); if (enabledTrackCount == 0) { chunkSource.reset(); downstreamPositionUs = Long.MIN_VALUE; @@ -259,7 +271,8 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, if (!extractor.isPrepared()) { break; } - if (extractor.hasSamples(track)) { + int extractorTrack = extractorTrackIndices[track]; + if (extractor.hasSamples(extractorTrack)) { return true; } } @@ -302,8 +315,9 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, extractor.configureSpliceTo(extractors.get(1)); } + int extractorTrack = extractorTrackIndices[track]; int extractorIndex = 0; - while (extractors.size() > extractorIndex + 1 && !extractor.hasSamples(track)) { + while (extractors.size() > extractorIndex + 1 && !extractor.hasSamples(extractorTrack)) { // We're finished reading from the extractor for this particular track, so advance to the // next one for the current read. extractor = extractors.get(++extractorIndex); @@ -312,14 +326,14 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, } } - MediaFormat mediaFormat = extractor.getMediaFormat(track); + MediaFormat mediaFormat = extractor.getMediaFormat(extractorTrack); if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormats[track])) { formatHolder.format = mediaFormat; downstreamMediaFormats[track] = mediaFormat; return FORMAT_READ; } - if (extractor.getSample(track, sampleHolder)) { + if (extractor.getSample(extractorTrack, sampleHolder)) { boolean decodeOnly = sampleHolder.timeUs < lastSeekPositionUs; sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0; return SAMPLE_READ; @@ -346,6 +360,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, Assertions.checkState(prepared); Assertions.checkState(enabledTrackCount > 0); + // Ignore seeks to the current position. long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs; downstreamPositionUs = positionUs; lastSeekPositionUs = positionUs; @@ -353,13 +368,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, return; } - // TODO: Optimize the seek for the case where the position is already buffered. - downstreamPositionUs = positionUs; - for (int i = 0; i < pendingDiscontinuities.length; i++) { - pendingDiscontinuities[i] = true; - } - chunkSource.seek(); - restartFrom(positionUs); + seekToInternal(positionUs); } @Override @@ -448,6 +457,146 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, // Internal stuff. + /** + * Builds tracks that are exposed by this {@link HlsSampleSource} instance, as well as internal + * data-structures required for operation. + *

+ * Tracks in HLS are complicated. A HLS master playlist contains a number of "variants". Each + * variant stream typically contains muxed video, audio and (possibly) additional audio, metadata + * and caption tracks. We wish to allow the user to select between an adaptive track that spans + * all variants, as well as each individual variant. If multiple audio tracks are present within + * each variant then we wish to allow the user to select between those also. + *

+ * To do this, tracks are constructed as follows. The {@link HlsChunkSource} exposes (N+1) tracks, + * where N is the number of variants defined in the HLS master playlist. These consist of one + * adaptive track defined to span all variants and a track for each individual variant. The + * adaptive track is initially selected. The extractor is then prepared to discover the tracks + * inside of each variant stream. The two sets of tracks are then combined by this method to + * create a third set, which is the set exposed by this {@link HlsSampleSource}: + *

    + *
  • The extractor tracks are inspected to infer a "primary" track type. If a video track is + * present then it is always the primary type. If not, audio is the primary type if present. + * Else text is the primary type if present. Else there is no primary type.
  • + *
  • If there is exactly one extractor track of the primary type, it's expanded into (N+1) + * exposed tracks, all of which correspond to the primary extractor track and each of which + * corresponds to a different chunk source track. Selecting one of these tracks has the effect + * of switching the selected track on the chunk source.
  • + *
  • All other extractor tracks are exposed directly. Selecting one of these tracks has the + * effect of selecting an extractor track, leaving the selected track on the chunk source + * unchanged.
  • + *
+ * + * @param extractor The prepared extractor. + */ + private void buildTracks(HlsExtractorWrapper extractor) { + // Iterate through the extractor tracks to discover the "primary" track type, and the index + // of the single track of this type. + int primaryExtractorTrackType = PRIMARY_TYPE_NONE; + int primaryExtractorTrackIndex = -1; + int extractorTrackCount = extractor.getTrackCount(); + for (int i = 0; i < extractorTrackCount; i++) { + String mimeType = extractor.getMediaFormat(i).mimeType; + int trackType; + if (MimeTypes.isVideo(mimeType)) { + trackType = PRIMARY_TYPE_VIDEO; + } else if (MimeTypes.isAudio(mimeType)) { + trackType = PRIMARY_TYPE_AUDIO; + } else if (MimeTypes.isText(mimeType)) { + trackType = PRIMARY_TYPE_TEXT; + } else { + trackType = PRIMARY_TYPE_NONE; + } + if (trackType > primaryExtractorTrackType) { + primaryExtractorTrackType = trackType; + primaryExtractorTrackIndex = i; + } else if (trackType == primaryExtractorTrackType && primaryExtractorTrackIndex != -1) { + // We have multiple tracks of the primary type. We only want an index if there only + // exists a single track of the primary type, so set the index back to -1. + primaryExtractorTrackIndex = -1; + } + } + + // Calculate the number of tracks that will be exposed. + int chunkSourceTrackCount = chunkSource.getTrackCount(); + boolean expandPrimaryExtractorTrack = primaryExtractorTrackIndex != -1; + trackCount = extractorTrackCount; + if (expandPrimaryExtractorTrack) { + trackCount += chunkSourceTrackCount - 1; + } + + // Instantiate the necessary internal data-structures. + trackFormats = new MediaFormat[trackCount]; + trackEnabledStates = new boolean[trackCount]; + pendingDiscontinuities = new boolean[trackCount]; + downstreamMediaFormats = new MediaFormat[trackCount]; + chunkSourceTrackIndices = new int[trackCount]; + extractorTrackIndices = new int[trackCount]; + extractorTrackEnabledStates = new boolean[extractorTrackCount]; + + // Construct the set of exposed tracks. + long durationUs = chunkSource.getDurationUs(); + int trackIndex = 0; + for (int i = 0; i < extractorTrackCount; i++) { + MediaFormat format = extractor.getMediaFormat(i).copyWithDurationUs(durationUs); + if (i == primaryExtractorTrackIndex) { + for (int j = 0; j < chunkSourceTrackCount; j++) { + extractorTrackIndices[trackIndex] = i; + chunkSourceTrackIndices[trackIndex] = j; + Variant fixedTrackVariant = chunkSource.getFixedTrackVariant(j); + trackFormats[trackIndex++] = fixedTrackVariant == null ? format.copyAsAdaptive(null) + : copyWithFixedTrackInfo(format, fixedTrackVariant.format); + } + } else { + extractorTrackIndices[trackIndex] = i; + chunkSourceTrackIndices[trackIndex] = -1; + trackFormats[trackIndex++] = format; + } + } + } + + /** + * Enables or disables the track at a given index. + * + * @param track The index of the track. + * @param enabledState True if the track is being enabled, or false if it's being disabled. + */ + private void setTrackEnabledState(int track, boolean enabledState) { + Assertions.checkState(trackEnabledStates[track] != enabledState); + int extractorTrack = extractorTrackIndices[track]; + Assertions.checkState(extractorTrackEnabledStates[extractorTrack] != enabledState); + trackEnabledStates[track] = enabledState; + extractorTrackEnabledStates[extractorTrack] = enabledState; + enabledTrackCount = enabledTrackCount + (enabledState ? 1 : -1); + } + + /** + * Copies a provided {@link MediaFormat}, incorporating information from the {@link Format} of + * a fixed (i.e. non-adaptive) track. + * + * @param format The {@link MediaFormat} to copy. + * @param fixedTrackFormat The {@link Format} to incorporate into the copy. + * @return The copied {@link MediaFormat}. + */ + private static MediaFormat copyWithFixedTrackInfo(MediaFormat format, Format fixedTrackFormat) { + int width = fixedTrackFormat.width == -1 ? MediaFormat.NO_VALUE : fixedTrackFormat.width; + int height = fixedTrackFormat.height == -1 ? MediaFormat.NO_VALUE : fixedTrackFormat.height; + return format.copyWithFixedTrackInfo(fixedTrackFormat.id, fixedTrackFormat.bitrate, width, + height, fixedTrackFormat.language); + } + + /** + * Performs a seek. The operation is performed even if the seek is to the current position. + * + * @param positionUs The position to seek to. + */ + private void seekToInternal(long positionUs) { + lastSeekPositionUs = positionUs; + downstreamPositionUs = positionUs; + Arrays.fill(pendingDiscontinuities, true); + chunkSource.seek(); + restartFrom(positionUs); + } + /** * Gets the current extractor from which samples should be read. *

@@ -472,8 +621,8 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, if (!extractor.isPrepared()) { return; } - for (int i = 0; i < trackEnabledStates.length; i++) { - if (!trackEnabledStates[i]) { + for (int i = 0; i < extractorTrackEnabledStates.length; i++) { + if (!extractorTrackEnabledStates[i]) { extractor.discardUntil(i, timeUs); } } @@ -483,8 +632,8 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, if (!extractor.isPrepared()) { return false; } - for (int i = 0; i < trackEnabledStates.length; i++) { - if (trackEnabledStates[i] && extractor.hasSamples(i)) { + for (int i = 0; i < extractorTrackEnabledStates.length; i++) { + if (extractorTrackEnabledStates[i] && extractor.hasSamples(i)) { return true; } }