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
This commit is contained in:
parent
b6b97a8683
commit
2690f569af
@ -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.demo.player.DemoPlayer.RendererBuilder;
|
||||||
import com.google.android.exoplayer.hls.DefaultHlsTrackSelector;
|
import com.google.android.exoplayer.hls.DefaultHlsTrackSelector;
|
||||||
import com.google.android.exoplayer.hls.HlsChunkSource;
|
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.HlsPlaylist;
|
||||||
import com.google.android.exoplayer.hls.HlsPlaylistParser;
|
import com.google.android.exoplayer.hls.HlsPlaylistParser;
|
||||||
import com.google.android.exoplayer.hls.HlsSampleSource;
|
import com.google.android.exoplayer.hls.HlsSampleSource;
|
||||||
import com.google.android.exoplayer.hls.PtsTimestampAdjusterProvider;
|
import com.google.android.exoplayer.hls.PtsTimestampAdjusterProvider;
|
||||||
import com.google.android.exoplayer.metadata.Id3Parser;
|
import com.google.android.exoplayer.metadata.Id3Parser;
|
||||||
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
|
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.text.eia608.Eia608TrackRenderer;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||||
@ -53,7 +55,8 @@ import java.util.Map;
|
|||||||
public class HlsRendererBuilder implements RendererBuilder {
|
public class HlsRendererBuilder implements RendererBuilder {
|
||||||
|
|
||||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
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 Context context;
|
||||||
private final String userAgent;
|
private final String userAgent;
|
||||||
@ -127,13 +130,15 @@ public class HlsRendererBuilder implements RendererBuilder {
|
|||||||
Handler mainHandler = player.getMainHandler();
|
Handler mainHandler = player.getMainHandler();
|
||||||
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
||||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||||
|
PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider();
|
||||||
|
|
||||||
|
// Build the video/audio/metadata renderers.
|
||||||
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||||
HlsChunkSource chunkSource = new HlsChunkSource(true /* isMaster */, dataSource, url,
|
HlsChunkSource chunkSource = new HlsChunkSource(true /* isMaster */, dataSource, url,
|
||||||
manifest, DefaultHlsTrackSelector.newDefaultInstance(context), bandwidthMeter,
|
manifest, DefaultHlsTrackSelector.newDefaultInstance(context), bandwidthMeter,
|
||||||
new PtsTimestampAdjusterProvider(), HlsChunkSource.ADAPTIVE_MODE_SPLICE);
|
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
|
||||||
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl,
|
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,
|
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context,
|
||||||
sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT,
|
sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT,
|
||||||
5000, mainHandler, player, 50);
|
5000, mainHandler, player, 50);
|
||||||
@ -142,14 +147,30 @@ public class HlsRendererBuilder implements RendererBuilder {
|
|||||||
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||||
MetadataTrackRenderer<Map<String, Object>> id3Renderer = new MetadataTrackRenderer<>(
|
MetadataTrackRenderer<Map<String, Object>> id3Renderer = new MetadataTrackRenderer<>(
|
||||||
sampleSource, new Id3Parser(), player, mainHandler.getLooper());
|
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];
|
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT];
|
||||||
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
|
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||||
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
|
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||||
renderers[DemoPlayer.TYPE_METADATA] = id3Renderer;
|
renderers[DemoPlayer.TYPE_METADATA] = id3Renderer;
|
||||||
renderers[DemoPlayer.TYPE_TEXT] = closedCaptionRenderer;
|
renderers[DemoPlayer.TYPE_TEXT] = textRenderer;
|
||||||
player.onRenderers(renderers, bandwidthMeter);
|
player.onRenderers(renderers, bandwidthMeter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,6 +234,13 @@ public final class MediaFormat {
|
|||||||
subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight);
|
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) {
|
public MediaFormat copyAsAdaptive(String trackId) {
|
||||||
return new MediaFormat(trackId, mimeType, NO_VALUE, NO_VALUE, durationUs, NO_VALUE, NO_VALUE,
|
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,
|
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, null, true, maxWidth,
|
||||||
|
@ -133,7 +133,7 @@ public class HlsChunkSource implements HlsTrackSelector.Output {
|
|||||||
// TODO: Expose tracks.
|
// TODO: Expose tracks.
|
||||||
private final ArrayList<ExposedTrack> tracks;
|
private final ArrayList<ExposedTrack> tracks;
|
||||||
|
|
||||||
private ExposedTrack enabledTrack;
|
private int selectedTrackIndex;
|
||||||
|
|
||||||
// A list of variants considered during playback, ordered by decreasing bandwidth. The following
|
// 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],
|
// 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;
|
return variants.length == 1 ? variants[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently selected track index.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
* Selects a track for use.
|
||||||
* <p>
|
* <p>
|
||||||
@ -303,9 +315,10 @@ public class HlsChunkSource implements HlsTrackSelector.Output {
|
|||||||
* @param index The track index.
|
* @param index The track index.
|
||||||
*/
|
*/
|
||||||
public void selectTrack(int index) {
|
public void selectTrack(int index) {
|
||||||
enabledTrack = tracks.get(index);
|
selectedTrackIndex = index;
|
||||||
selectedVariantIndex = enabledTrack.defaultVariantIndex;
|
ExposedTrack selectedTrack = tracks.get(selectedTrackIndex);
|
||||||
variants = enabledTrack.variants;
|
selectedVariantIndex = selectedTrack.defaultVariantIndex;
|
||||||
|
variants = selectedTrack.variants;
|
||||||
variantPlaylists = new HlsMediaPlaylist[variants.length];
|
variantPlaylists = new HlsMediaPlaylist[variants.length];
|
||||||
variantLastPlaylistLoadTimesMs = new long[variants.length];
|
variantLastPlaylistLoadTimesMs = new long[variants.length];
|
||||||
variantBlacklistTimes = 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.
|
// The master source has yet to instantiate an adjuster for the discontinuity sequence.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ExposedTrack selectedTrack = tracks.get(selectedTrackIndex);
|
||||||
Extractor extractor = new TsExtractor(timestampAdjuster);
|
Extractor extractor = new TsExtractor(timestampAdjuster);
|
||||||
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
|
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
|
||||||
switchingVariantSpliced, enabledTrack.adaptiveMaxWidth, enabledTrack.adaptiveMaxHeight);
|
switchingVariantSpliced, selectedTrack.adaptiveMaxWidth, selectedTrack.adaptiveMaxHeight);
|
||||||
} else {
|
} else {
|
||||||
// MPEG-2 TS segments, and we need to continue using the same extractor.
|
// MPEG-2 TS segments, and we need to continue using the same extractor.
|
||||||
extractorWrapper = previousTsChunk.extractorWrapper;
|
extractorWrapper = previousTsChunk.extractorWrapper;
|
||||||
|
@ -36,6 +36,7 @@ import android.os.Handler;
|
|||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.LinkedList;
|
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 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 HlsChunkSource chunkSource;
|
||||||
private final LinkedList<HlsExtractorWrapper> extractors;
|
private final LinkedList<HlsExtractorWrapper> extractors;
|
||||||
private final int minLoadableRetryCount;
|
private final int minLoadableRetryCount;
|
||||||
@ -71,11 +77,22 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
|
|||||||
private boolean loadControlRegistered;
|
private boolean loadControlRegistered;
|
||||||
private int trackCount;
|
private int trackCount;
|
||||||
private int enabledTrackCount;
|
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[] trackEnabledStates;
|
||||||
private boolean[] pendingDiscontinuities;
|
private boolean[] pendingDiscontinuities;
|
||||||
private MediaFormat[] trackFormat;
|
|
||||||
private MediaFormat[] downstreamMediaFormats;
|
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 downstreamPositionUs;
|
||||||
private long lastSeekPositionUs;
|
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.
|
// We're not prepared, but we might have loaded what we need.
|
||||||
HlsExtractorWrapper extractor = extractors.getFirst();
|
HlsExtractorWrapper extractor = extractors.getFirst();
|
||||||
if (extractor.isPrepared()) {
|
if (extractor.isPrepared()) {
|
||||||
trackCount = extractor.getTrackCount();
|
buildTracks(extractor);
|
||||||
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;
|
|
||||||
}
|
|
||||||
prepared = true;
|
prepared = true;
|
||||||
|
maybeStartLoading(); // Update the load control.
|
||||||
return true;
|
return true;
|
||||||
} else if (extractors.size() > 1) {
|
} else if (extractors.size() > 1) {
|
||||||
extractors.removeFirst().clear();
|
extractors.removeFirst().clear();
|
||||||
@ -185,15 +191,13 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
|
|||||||
@Override
|
@Override
|
||||||
public MediaFormat getFormat(int track) {
|
public MediaFormat getFormat(int track) {
|
||||||
Assertions.checkState(prepared);
|
Assertions.checkState(prepared);
|
||||||
return trackFormat[track];
|
return trackFormats[track];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enable(int track, long positionUs) {
|
public void enable(int track, long positionUs) {
|
||||||
Assertions.checkState(prepared);
|
Assertions.checkState(prepared);
|
||||||
Assertions.checkState(!trackEnabledStates[track]);
|
setTrackEnabledState(track, true);
|
||||||
enabledTrackCount++;
|
|
||||||
trackEnabledStates[track] = true;
|
|
||||||
downstreamMediaFormats[track] = null;
|
downstreamMediaFormats[track] = null;
|
||||||
pendingDiscontinuities[track] = false;
|
pendingDiscontinuities[track] = false;
|
||||||
downstreamFormat = null;
|
downstreamFormat = null;
|
||||||
@ -202,6 +206,16 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
|
|||||||
loadControl.register(this, bufferSizeContribution);
|
loadControl.register(this, bufferSizeContribution);
|
||||||
loadControlRegistered = true;
|
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) {
|
if (enabledTrackCount == 1) {
|
||||||
lastSeekPositionUs = positionUs;
|
lastSeekPositionUs = positionUs;
|
||||||
if (wasLoadControlRegistered && downstreamPositionUs == positionUs) {
|
if (wasLoadControlRegistered && downstreamPositionUs == positionUs) {
|
||||||
@ -220,9 +234,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
|
|||||||
@Override
|
@Override
|
||||||
public void disable(int track) {
|
public void disable(int track) {
|
||||||
Assertions.checkState(prepared);
|
Assertions.checkState(prepared);
|
||||||
Assertions.checkState(trackEnabledStates[track]);
|
setTrackEnabledState(track, false);
|
||||||
enabledTrackCount--;
|
|
||||||
trackEnabledStates[track] = false;
|
|
||||||
if (enabledTrackCount == 0) {
|
if (enabledTrackCount == 0) {
|
||||||
chunkSource.reset();
|
chunkSource.reset();
|
||||||
downstreamPositionUs = Long.MIN_VALUE;
|
downstreamPositionUs = Long.MIN_VALUE;
|
||||||
@ -259,7 +271,8 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
|
|||||||
if (!extractor.isPrepared()) {
|
if (!extractor.isPrepared()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (extractor.hasSamples(track)) {
|
int extractorTrack = extractorTrackIndices[track];
|
||||||
|
if (extractor.hasSamples(extractorTrack)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -302,8 +315,9 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
|
|||||||
extractor.configureSpliceTo(extractors.get(1));
|
extractor.configureSpliceTo(extractors.get(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int extractorTrack = extractorTrackIndices[track];
|
||||||
int extractorIndex = 0;
|
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
|
// We're finished reading from the extractor for this particular track, so advance to the
|
||||||
// next one for the current read.
|
// next one for the current read.
|
||||||
extractor = extractors.get(++extractorIndex);
|
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])) {
|
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormats[track])) {
|
||||||
formatHolder.format = mediaFormat;
|
formatHolder.format = mediaFormat;
|
||||||
downstreamMediaFormats[track] = mediaFormat;
|
downstreamMediaFormats[track] = mediaFormat;
|
||||||
return FORMAT_READ;
|
return FORMAT_READ;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extractor.getSample(track, sampleHolder)) {
|
if (extractor.getSample(extractorTrack, sampleHolder)) {
|
||||||
boolean decodeOnly = sampleHolder.timeUs < lastSeekPositionUs;
|
boolean decodeOnly = sampleHolder.timeUs < lastSeekPositionUs;
|
||||||
sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
|
sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
|
||||||
return SAMPLE_READ;
|
return SAMPLE_READ;
|
||||||
@ -346,6 +360,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
|
|||||||
Assertions.checkState(prepared);
|
Assertions.checkState(prepared);
|
||||||
Assertions.checkState(enabledTrackCount > 0);
|
Assertions.checkState(enabledTrackCount > 0);
|
||||||
|
|
||||||
|
// Ignore seeks to the current position.
|
||||||
long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs;
|
long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs;
|
||||||
downstreamPositionUs = positionUs;
|
downstreamPositionUs = positionUs;
|
||||||
lastSeekPositionUs = positionUs;
|
lastSeekPositionUs = positionUs;
|
||||||
@ -353,13 +368,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Optimize the seek for the case where the position is already buffered.
|
seekToInternal(positionUs);
|
||||||
downstreamPositionUs = positionUs;
|
|
||||||
for (int i = 0; i < pendingDiscontinuities.length; i++) {
|
|
||||||
pendingDiscontinuities[i] = true;
|
|
||||||
}
|
|
||||||
chunkSource.seek();
|
|
||||||
restartFrom(positionUs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -448,6 +457,146 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
|
|||||||
|
|
||||||
// Internal stuff.
|
// Internal stuff.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds tracks that are exposed by this {@link HlsSampleSource} instance, as well as internal
|
||||||
|
* data-structures required for operation.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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}:
|
||||||
|
* <ul>
|
||||||
|
* <li>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.</li>
|
||||||
|
* <li>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.</li>
|
||||||
|
* <li>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.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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.
|
* Gets the current extractor from which samples should be read.
|
||||||
* <p>
|
* <p>
|
||||||
@ -472,8 +621,8 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
|
|||||||
if (!extractor.isPrepared()) {
|
if (!extractor.isPrepared()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < trackEnabledStates.length; i++) {
|
for (int i = 0; i < extractorTrackEnabledStates.length; i++) {
|
||||||
if (!trackEnabledStates[i]) {
|
if (!extractorTrackEnabledStates[i]) {
|
||||||
extractor.discardUntil(i, timeUs);
|
extractor.discardUntil(i, timeUs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -483,8 +632,8 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader,
|
|||||||
if (!extractor.isPrepared()) {
|
if (!extractor.isPrepared()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < trackEnabledStates.length; i++) {
|
for (int i = 0; i < extractorTrackEnabledStates.length; i++) {
|
||||||
if (trackEnabledStates[i] && extractor.hasSamples(i)) {
|
if (extractorTrackEnabledStates[i] && extractor.hasSamples(i)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user