Commit 1eff6cf2 by olly Committed by Oliver Woodman

Bring back multi-audio and VTT support for HLS.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=117338865
parent 1d4305cb
...@@ -428,14 +428,14 @@ public class DemoPlayer implements ExoPlayer.Listener, DefaultTrackSelector.Even ...@@ -428,14 +428,14 @@ public class DemoPlayer implements ExoPlayer.Listener, DefaultTrackSelector.Even
@Override @Override
public void onCues(List<Cue> cues) { public void onCues(List<Cue> cues) {
if (captionListener != null && trackInfo.getTrackSelection(TYPE_TEXT) != null) { if (captionListener != null) {
captionListener.onCues(cues); captionListener.onCues(cues);
} }
} }
@Override @Override
public void onMetadata(List<Id3Frame> id3Frames) { public void onMetadata(List<Id3Frame> id3Frames) {
if (id3MetadataListener != null && trackInfo.getTrackSelection(TYPE_METADATA) != null) { if (id3MetadataListener != null) {
id3MetadataListener.onId3Metadata(id3Frames); id3MetadataListener.onId3Metadata(id3Frames);
} }
} }
......
...@@ -17,7 +17,9 @@ package com.google.android.exoplayer.demo.player; ...@@ -17,7 +17,9 @@ package com.google.android.exoplayer.demo.player;
import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.DefaultLoadControl;
import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.LoadControl;
import com.google.android.exoplayer.MultiSampleSource;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.demo.player.DemoPlayer.SourceBuilder; import com.google.android.exoplayer.demo.player.DemoPlayer.SourceBuilder;
import com.google.android.exoplayer.hls.HlsChunkSource; import com.google.android.exoplayer.hls.HlsChunkSource;
import com.google.android.exoplayer.hls.HlsPlaylist; import com.google.android.exoplayer.hls.HlsPlaylist;
...@@ -40,7 +42,9 @@ import android.os.Handler; ...@@ -40,7 +42,9 @@ import android.os.Handler;
public class HlsSourceBuilder implements SourceBuilder { public class HlsSourceBuilder implements SourceBuilder {
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int MAIN_BUFFER_SEGMENTS = 256; private static final int MAIN_BUFFER_SEGMENTS = 254;
private static final int AUDIO_BUFFER_SEGMENTS = 54;
private static final int TEXT_BUFFER_SEGMENTS = 2;
private final Context context; private final Context context;
private final String userAgent; private final String userAgent;
...@@ -64,11 +68,26 @@ public class HlsSourceBuilder implements SourceBuilder { ...@@ -64,11 +68,26 @@ public class HlsSourceBuilder implements SourceBuilder {
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider(); PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider();
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); DataSource defaultDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
HlsChunkSource chunkSource = new HlsChunkSource(manifestFetcher, HlsChunkSource.TYPE_DEFAULT, HlsChunkSource defaultChunkSource = new HlsChunkSource(manifestFetcher,
dataSource, bandwidthMeter, timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); HlsChunkSource.TYPE_DEFAULT, defaultDataSource, timestampAdjusterProvider,
return new HlsSampleSource(chunkSource, loadControl, new FormatEvaluator.AdaptiveEvaluator(bandwidthMeter));
HlsSampleSource defaultSampleSource = new HlsSampleSource(defaultChunkSource, loadControl,
MAIN_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_VIDEO); MAIN_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_VIDEO);
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
HlsChunkSource audioChunkSource = new HlsChunkSource(manifestFetcher, HlsChunkSource.TYPE_AUDIO,
audioDataSource, timestampAdjusterProvider, null);
HlsSampleSource audioSampleSource = new HlsSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_AUDIO);
DataSource subtitleDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
HlsChunkSource subtitleChunkSource = new HlsChunkSource(manifestFetcher,
HlsChunkSource.TYPE_SUBTITLE, subtitleDataSource, timestampAdjusterProvider, null);
HlsSampleSource subtitleSampleSource = new HlsSampleSource(subtitleChunkSource, loadControl,
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_TEXT);
return new MultiSampleSource(defaultSampleSource, audioSampleSource, subtitleSampleSource);
} }
} }
...@@ -121,10 +121,12 @@ public class TrackSelectionHelper implements View.OnClickListener, DialogInterfa ...@@ -121,10 +121,12 @@ public class TrackSelectionHelper implements View.OnClickListener, DialogInterfa
boolean haveSupportedTracks = false; boolean haveSupportedTracks = false;
trackViews = new CheckedTextView[trackGroups.length][]; trackViews = new CheckedTextView[trackGroups.length][];
for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
root.addView(inflater.inflate(R.layout.list_divider, root, false));
TrackGroup group = trackGroups.get(groupIndex); TrackGroup group = trackGroups.get(groupIndex);
trackViews[groupIndex] = new CheckedTextView[group.length]; trackViews[groupIndex] = new CheckedTextView[group.length];
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
if (trackIndex == 0 || !group.adaptive) {
root.addView(inflater.inflate(R.layout.list_divider, root, false));
}
int trackViewLayoutId = group.length < 2 || !trackGroupsAdaptive[groupIndex] int trackViewLayoutId = group.length < 2 || !trackGroupsAdaptive[groupIndex]
? android.R.layout.simple_list_item_single_choice ? android.R.layout.simple_list_item_single_choice
: android.R.layout.simple_list_item_multiple_choice; : android.R.layout.simple_list_item_multiple_choice;
......
...@@ -558,6 +558,8 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -558,6 +558,8 @@ import java.util.concurrent.atomic.AtomicInteger;
} }
} }
// The new selections are being activated.
trackSelector.onSelectionActivated(result.second);
enabledRenderers = new TrackRenderer[enabledRendererCount]; enabledRenderers = new TrackRenderer[enabledRendererCount];
enabledRendererCount = 0; enabledRendererCount = 0;
...@@ -597,9 +599,6 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -597,9 +599,6 @@ import java.util.concurrent.atomic.AtomicInteger;
} }
} }
} }
// The new selections have been activated.
trackSelector.onSelectionActivated(result.second);
} }
private void reselectTracksInternal() throws ExoPlaybackException { private void reselectTracksInternal() throws ExoPlaybackException {
......
...@@ -295,7 +295,7 @@ public final class Format { ...@@ -295,7 +295,7 @@ public final class Format {
encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData, encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData,
requiresSecureDecryption); requiresSecureDecryption);
} }
/** /**
* @return A {@link MediaFormat} representation of this format. * @return A {@link MediaFormat} representation of this format.
*/ */
......
...@@ -29,7 +29,6 @@ import com.google.android.exoplayer.extractor.mp3.Mp3Extractor; ...@@ -29,7 +29,6 @@ import com.google.android.exoplayer.extractor.mp3.Mp3Extractor;
import com.google.android.exoplayer.extractor.ts.AdtsExtractor; import com.google.android.exoplayer.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer.extractor.ts.PtsTimestampAdjuster; import com.google.android.exoplayer.extractor.ts.PtsTimestampAdjuster;
import com.google.android.exoplayer.extractor.ts.TsExtractor; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException;
...@@ -64,41 +63,8 @@ public class HlsChunkSource { ...@@ -64,41 +63,8 @@ public class HlsChunkSource {
public interface EventListener extends BaseChunkSampleSourceEventListener {} public interface EventListener extends BaseChunkSampleSourceEventListener {}
public static final int TYPE_DEFAULT = 0; public static final int TYPE_DEFAULT = 0;
public static final int TYPE_VTT = 1; public static final int TYPE_AUDIO = 1;
public static final int TYPE_SUBTITLE = 2;
/**
* 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 time for which a media playlist should be blacklisted. * The default time for which a media playlist should be blacklisted.
...@@ -118,7 +84,6 @@ public class HlsChunkSource { ...@@ -118,7 +84,6 @@ public class HlsChunkSource {
private final Evaluation evaluation; private final Evaluation evaluation;
private final HlsPlaylistParser playlistParser; private final HlsPlaylistParser playlistParser;
private final PtsTimestampAdjusterProvider timestampAdjusterProvider; private final PtsTimestampAdjusterProvider timestampAdjusterProvider;
private final int adaptiveMode;
private boolean manifestFetcherEnabled; private boolean manifestFetcherEnabled;
private byte[] scratchSpace; private byte[] scratchSpace;
...@@ -146,26 +111,22 @@ public class HlsChunkSource { ...@@ -146,26 +111,22 @@ public class HlsChunkSource {
/** /**
* @param manifestFetcher A fetcher for the playlist. * @param manifestFetcher A fetcher for the playlist.
* @param type The type of chunk provided by the source. One of {@link #TYPE_DEFAULT} and * @param type The type of chunk provided by the source. One of {@link #TYPE_DEFAULT},
* {@link #TYPE_VTT}. * {@link #TYPE_AUDIO} and {@link #TYPE_SUBTITLE}.
* @param dataSource A {@link DataSource} suitable for loading the media data. * @param dataSource A {@link DataSource} suitable for loading the media data.
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
* @param timestampAdjusterProvider A provider of {@link PtsTimestampAdjuster} instances. If * @param timestampAdjusterProvider A provider of {@link PtsTimestampAdjuster} instances. If
* multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the
* same provider. * same provider.
* @param adaptiveMode The mode for switching from one variant to another. One of * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats.
* {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and
* {@link #ADAPTIVE_MODE_SPLICE}.
*/ */
public HlsChunkSource(ManifestFetcher<HlsPlaylist> manifestFetcher, int type, public HlsChunkSource(ManifestFetcher<HlsPlaylist> manifestFetcher, int type,
DataSource dataSource, BandwidthMeter bandwidthMeter, DataSource dataSource, PtsTimestampAdjusterProvider timestampAdjusterProvider,
PtsTimestampAdjusterProvider timestampAdjusterProvider, int adaptiveMode) { FormatEvaluator adaptiveFormatEvaluator) {
this.manifestFetcher = manifestFetcher; this.manifestFetcher = manifestFetcher;
this.type = type; this.type = type;
this.dataSource = dataSource; this.dataSource = dataSource;
this.adaptiveFormatEvaluator = new FormatEvaluator.AdaptiveEvaluator(bandwidthMeter); this.adaptiveFormatEvaluator = adaptiveFormatEvaluator;
this.timestampAdjusterProvider = timestampAdjusterProvider; this.timestampAdjusterProvider = timestampAdjusterProvider;
this.adaptiveMode = adaptiveMode;
playlistParser = new HlsPlaylistParser(); playlistParser = new HlsPlaylistParser();
evaluation = new Evaluation(); evaluation = new Evaluation();
} }
...@@ -183,6 +144,15 @@ public class HlsChunkSource { ...@@ -183,6 +144,15 @@ public class HlsChunkSource {
} }
/** /**
* Returns whether this source supports adaptation between its tracks.
*
* @return Whether this source supports adaptation between its tracks.
*/
public boolean isAdaptive() {
return adaptiveFormatEvaluator != null;
}
/**
* Prepares the source. * Prepares the source.
* *
* @return True if the source was prepared, false otherwise. * @return True if the source was prepared, false otherwise.
...@@ -209,11 +179,13 @@ public class HlsChunkSource { ...@@ -209,11 +179,13 @@ public class HlsChunkSource {
List<Variant> variants = new ArrayList<>(); List<Variant> variants = new ArrayList<>();
variants.add(new Variant(baseUri, format, null)); variants.add(new Variant(baseUri, format, null));
masterPlaylist = new HlsMasterPlaylist(baseUri, variants, masterPlaylist = new HlsMasterPlaylist(baseUri, variants,
Collections.<Variant>emptyList()); Collections.<Variant>emptyList(), Collections.<Variant>emptyList(), null, null);
} }
processMasterPlaylist(masterPlaylist); processMasterPlaylist(masterPlaylist);
// TODO[REFACTOR]: Come up with a sane default here. if (exposedVariants.length > 0) {
selectTracks(new int[] {0}); // TODO[REFACTOR]: Come up with a sane default here.
selectTracks(new int[] {0});
}
} }
} }
return true; return true;
...@@ -265,6 +237,28 @@ public class HlsChunkSource { ...@@ -265,6 +237,28 @@ public class HlsChunkSource {
} }
/** /**
* Returns the format of the audio muxed into variants, or null if unknown.
* <p>
* This method should only be called after the source has been prepared.
*
* @return The format of the audio muxed into variants, or null if unknown.
*/
public Format getMuxedAudioFormat() {
return masterPlaylist.muxedAudioFormat;
}
/**
* Returns the format of the captions muxed into variants, or null if unknown.
* <p>
* This method should only be called after the source has been prepared.
*
* @return The format of the captions muxed into variants, or null if unknown.
*/
public Format getMuxedCaptionFormat() {
return masterPlaylist.muxedCaptionFormat;
}
/**
* Selects tracks for use. * Selects tracks for use.
* <p> * <p>
* This method should only be called after the source has been prepared. * This method should only be called after the source has been prepared.
...@@ -333,17 +327,9 @@ public class HlsChunkSource { ...@@ -333,17 +327,9 @@ public class HlsChunkSource {
*/ */
public void getChunkOperation(TsChunk previousTsChunk, long playbackPositionUs, public void getChunkOperation(TsChunk previousTsChunk, long playbackPositionUs,
ChunkOperationHolder out) { ChunkOperationHolder out) {
int nextVariantIndex; int nextVariantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs);
boolean switchingVariantSpliced; boolean switchingVariant = previousTsChunk != null
if (adaptiveMode == ADAPTIVE_MODE_NONE) { && enabledVariants[nextVariantIndex].format != previousTsChunk.format;
nextVariantIndex = selectedVariantIndex;
switchingVariantSpliced = false;
} else {
nextVariantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs);
switchingVariantSpliced = previousTsChunk != null
&& enabledVariants[nextVariantIndex].format != previousTsChunk.format
&& adaptiveMode == ADAPTIVE_MODE_SPLICE;
}
HlsMediaPlaylist mediaPlaylist = enabledVariantPlaylists[nextVariantIndex]; HlsMediaPlaylist mediaPlaylist = enabledVariantPlaylists[nextVariantIndex];
if (mediaPlaylist == null) { if (mediaPlaylist == null) {
...@@ -358,8 +344,8 @@ public class HlsChunkSource { ...@@ -358,8 +344,8 @@ public class HlsChunkSource {
if (previousTsChunk == null) { if (previousTsChunk == null) {
chunkMediaSequence = getLiveStartChunkMediaSequence(nextVariantIndex); chunkMediaSequence = getLiveStartChunkMediaSequence(nextVariantIndex);
} else { } else {
chunkMediaSequence = switchingVariantSpliced chunkMediaSequence = switchingVariant ? previousTsChunk.chunkIndex
? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1; : previousTsChunk.chunkIndex + 1;
if (chunkMediaSequence < mediaPlaylist.mediaSequence) { if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
fatalError = new BehindLiveWindowException(); fatalError = new BehindLiveWindowException();
return; return;
...@@ -371,8 +357,8 @@ public class HlsChunkSource { ...@@ -371,8 +357,8 @@ public class HlsChunkSource {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs, chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs,
true, true) + mediaPlaylist.mediaSequence; true, true) + mediaPlaylist.mediaSequence;
} else { } else {
chunkMediaSequence = switchingVariantSpliced chunkMediaSequence = switchingVariant ? previousTsChunk.chunkIndex
? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1; : previousTsChunk.chunkIndex + 1;
} }
} }
...@@ -413,7 +399,7 @@ public class HlsChunkSource { ...@@ -413,7 +399,7 @@ public class HlsChunkSource {
if (live) { if (live) {
if (previousTsChunk == null) { if (previousTsChunk == null) {
startTimeUs = 0; startTimeUs = 0;
} else if (switchingVariantSpliced) { } else if (switchingVariant) {
startTimeUs = previousTsChunk.startTimeUs; startTimeUs = previousTsChunk.startTimeUs;
} else { } else {
startTimeUs = previousTsChunk.endTimeUs; startTimeUs = previousTsChunk.endTimeUs;
...@@ -434,11 +420,11 @@ public class HlsChunkSource { ...@@ -434,11 +420,11 @@ public class HlsChunkSource {
// case below. // case below.
Extractor extractor = new AdtsExtractor(startTimeUs); Extractor extractor = new AdtsExtractor(startTimeUs);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariantSpliced); switchingVariant);
} else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) {
Extractor extractor = new Mp3Extractor(startTimeUs); Extractor extractor = new Mp3Extractor(startTimeUs);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariantSpliced); switchingVariant);
} else if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) } else if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION)
|| lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) {
PtsTimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(false, PtsTimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(false,
...@@ -451,7 +437,7 @@ public class HlsChunkSource { ...@@ -451,7 +437,7 @@ public class HlsChunkSource {
} }
Extractor extractor = new WebvttExtractor(format.language, timestampAdjuster); Extractor extractor = new WebvttExtractor(format.language, timestampAdjuster);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariantSpliced); switchingVariant);
} else if (previousTsChunk == null } else if (previousTsChunk == null
|| previousTsChunk.discontinuitySequenceNumber != segment.discontinuitySequenceNumber || previousTsChunk.discontinuitySequenceNumber != segment.discontinuitySequenceNumber
|| format != previousTsChunk.format) { || format != previousTsChunk.format) {
...@@ -468,16 +454,16 @@ public class HlsChunkSource { ...@@ -468,16 +454,16 @@ public class HlsChunkSource {
// Sometimes AAC and H264 streams are declared in TS chunks even though they don't really // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
// exist. If we know from the codec attribute that they don't exist, then we can explicitly // exist. If we know from the codec attribute that they don't exist, then we can explicitly
// ignore them even if they're declared. // ignore them even if they're declared.
if (MimeTypes.getAudioMediaMimeType(codecs) != MimeTypes.AUDIO_AAC) { if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_AAC_STREAM; workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_AAC_STREAM;
} }
if (MimeTypes.getVideoMediaMimeType(codecs) != MimeTypes.VIDEO_H264) { if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_H264_STREAM; workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_H264_STREAM;
} }
} }
Extractor extractor = new TsExtractor(timestampAdjuster, workaroundFlags); Extractor extractor = new TsExtractor(timestampAdjuster, workaroundFlags);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariantSpliced); switchingVariant);
} 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;
...@@ -562,11 +548,11 @@ public class HlsChunkSource { ...@@ -562,11 +548,11 @@ public class HlsChunkSource {
// Private methods. // Private methods.
private void processMasterPlaylist(HlsMasterPlaylist playlist) { private void processMasterPlaylist(HlsMasterPlaylist playlist) {
if (type == TYPE_VTT) { if (type == TYPE_SUBTITLE || type == TYPE_AUDIO) {
List<Variant> subtitleVariants = playlist.subtitles; List<Variant> variants = type == TYPE_AUDIO ? playlist.audios : playlist.subtitles;
if (subtitleVariants != null) { if (variants != null && !variants.isEmpty()) {
exposedVariants = new Variant[subtitleVariants.size()]; exposedVariants = new Variant[variants.size()];
subtitleVariants.toArray(exposedVariants); variants.toArray(exposedVariants);
} else { } else {
exposedVariants = new Variant[0]; exposedVariants = new Variant[0];
} }
...@@ -622,8 +608,7 @@ public class HlsChunkSource { ...@@ -622,8 +608,7 @@ public class HlsChunkSource {
long switchingOverlapUs; long switchingOverlapUs;
List<TsChunk> queue; List<TsChunk> queue;
if (previousTsChunk != null) { if (previousTsChunk != null) {
switchingOverlapUs = adaptiveMode == ADAPTIVE_MODE_SPLICE switchingOverlapUs = previousTsChunk.endTimeUs - previousTsChunk.startTimeUs;
? previousTsChunk.endTimeUs - previousTsChunk.startTimeUs : 0;
queue = Collections.singletonList(previousTsChunk); queue = Collections.singletonList(previousTsChunk);
} else { } else {
switchingOverlapUs = 0; switchingOverlapUs = 0;
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer.hls; package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.Format;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
...@@ -24,12 +26,20 @@ import java.util.List; ...@@ -24,12 +26,20 @@ import java.util.List;
public final class HlsMasterPlaylist extends HlsPlaylist { public final class HlsMasterPlaylist extends HlsPlaylist {
public final List<Variant> variants; public final List<Variant> variants;
public final List<Variant> audios;
public final List<Variant> subtitles; public final List<Variant> subtitles;
public HlsMasterPlaylist(String baseUri, List<Variant> variants, List<Variant> subtitles) { public final Format muxedAudioFormat;
public final Format muxedCaptionFormat;
public HlsMasterPlaylist(String baseUri, List<Variant> variants, List<Variant> audios,
List<Variant> subtitles, Format muxedAudioFormat, Format muxedCaptionFormat) {
super(baseUri, HlsPlaylist.TYPE_MASTER); super(baseUri, HlsPlaylist.TYPE_MASTER);
this.variants = Collections.unmodifiableList(variants); this.variants = Collections.unmodifiableList(variants);
this.audios = Collections.unmodifiableList(audios);
this.subtitles = Collections.unmodifiableList(subtitles); this.subtitles = Collections.unmodifiableList(subtitles);
this.muxedAudioFormat = muxedAudioFormat;
this.muxedCaptionFormat = muxedCaptionFormat;
} }
} }
...@@ -59,6 +59,7 @@ public final class HlsPlaylistParser implements UriLoadable.Parser<HlsPlaylist> ...@@ -59,6 +59,7 @@ public final class HlsPlaylistParser implements UriLoadable.Parser<HlsPlaylist>
private static final String METHOD_ATTR = "METHOD"; private static final String METHOD_ATTR = "METHOD";
private static final String URI_ATTR = "URI"; private static final String URI_ATTR = "URI";
private static final String IV_ATTR = "IV"; private static final String IV_ATTR = "IV";
private static final String INSTREAM_ID_ATTR = "INSTREAM-ID";
private static final String AUDIO_TYPE = "AUDIO"; private static final String AUDIO_TYPE = "AUDIO";
private static final String VIDEO_TYPE = "VIDEO"; private static final String VIDEO_TYPE = "VIDEO";
...@@ -98,6 +99,8 @@ public final class HlsPlaylistParser implements UriLoadable.Parser<HlsPlaylist> ...@@ -98,6 +99,8 @@ public final class HlsPlaylistParser implements UriLoadable.Parser<HlsPlaylist>
Pattern.compile(LANGUAGE_ATTR + "=\"(.+?)\""); Pattern.compile(LANGUAGE_ATTR + "=\"(.+?)\"");
private static final Pattern NAME_ATTR_REGEX = private static final Pattern NAME_ATTR_REGEX =
Pattern.compile(NAME_ATTR + "=\"(.+?)\""); Pattern.compile(NAME_ATTR + "=\"(.+?)\"");
private static final Pattern INSTREAM_ID_ATTR_REGEX =
Pattern.compile(INSTREAM_ID_ATTR + "=\"(.+?)\"");
// private static final Pattern AUTOSELECT_ATTR_REGEX = // private static final Pattern AUTOSELECT_ATTR_REGEX =
// HlsParserUtil.compileBooleanAttrPattern(AUTOSELECT_ATTR); // HlsParserUtil.compileBooleanAttrPattern(AUTOSELECT_ATTR);
// private static final Pattern DEFAULT_ATTR_REGEX = // private static final Pattern DEFAULT_ATTR_REGEX =
...@@ -140,12 +143,15 @@ public final class HlsPlaylistParser implements UriLoadable.Parser<HlsPlaylist> ...@@ -140,12 +143,15 @@ public final class HlsPlaylistParser implements UriLoadable.Parser<HlsPlaylist>
private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri) private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri)
throws IOException { throws IOException {
ArrayList<Variant> variants = new ArrayList<>(); ArrayList<Variant> variants = new ArrayList<>();
ArrayList<Variant> audios = new ArrayList<>();
ArrayList<Variant> subtitles = new ArrayList<>(); ArrayList<Variant> subtitles = new ArrayList<>();
int bitrate = 0; int bitrate = 0;
String codecs = null; String codecs = null;
int width = -1; int width = -1;
int height = -1; int height = -1;
String name = null; String name = null;
Format muxedAudioFormat = null;
Format muxedCaptionFormat = null;
boolean expectingStreamInfUrl = false; boolean expectingStreamInfUrl = false;
String line; String line;
...@@ -153,16 +159,37 @@ public final class HlsPlaylistParser implements UriLoadable.Parser<HlsPlaylist> ...@@ -153,16 +159,37 @@ public final class HlsPlaylistParser implements UriLoadable.Parser<HlsPlaylist>
line = iterator.next(); line = iterator.next();
if (line.startsWith(MEDIA_TAG)) { if (line.startsWith(MEDIA_TAG)) {
String type = HlsParserUtil.parseStringAttr(line, TYPE_ATTR_REGEX, TYPE_ATTR); String type = HlsParserUtil.parseStringAttr(line, TYPE_ATTR_REGEX, TYPE_ATTR);
if (SUBTITLES_TYPE.equals(type)) { if (CLOSED_CAPTIONS_TYPE.equals(type)) {
String instreamId = HlsParserUtil.parseStringAttr(line, INSTREAM_ID_ATTR_REGEX,
INSTREAM_ID_ATTR);
if ("CC1".equals(instreamId)) {
// We assume all subtitles belong to the same group.
String captionName = HlsParserUtil.parseStringAttr(line, NAME_ATTR_REGEX, NAME_ATTR);
String language = HlsParserUtil.parseOptionalStringAttr(line, LANGUAGE_ATTR_REGEX);
muxedCaptionFormat = Format.createTextContainerFormat(captionName,
MimeTypes.APPLICATION_M3U8, MimeTypes.APPLICATION_EIA608, -1, language);
}
} else if (SUBTITLES_TYPE.equals(type)) {
// We assume all subtitles belong to the same group. // We assume all subtitles belong to the same group.
String subtitleName = HlsParserUtil.parseStringAttr(line, NAME_ATTR_REGEX, NAME_ATTR); String subtitleName = HlsParserUtil.parseStringAttr(line, NAME_ATTR_REGEX, NAME_ATTR);
String uri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX, URI_ATTR); String uri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX, URI_ATTR);
String language = HlsParserUtil.parseOptionalStringAttr(line, LANGUAGE_ATTR_REGEX); String language = HlsParserUtil.parseOptionalStringAttr(line, LANGUAGE_ATTR_REGEX);
Format format = Format.createTextContainerFormat(subtitleName, MimeTypes.APPLICATION_M3U8, Format format = Format.createTextContainerFormat(subtitleName, MimeTypes.APPLICATION_M3U8,
MimeTypes.TEXT_VTT, bitrate, language); MimeTypes.TEXT_VTT, bitrate, language);
subtitles.add(new Variant(uri, format, null)); subtitles.add(new Variant(uri, format, codecs));
} else { } else if (AUDIO_TYPE.equals(type)) {
// TODO: Support other types of media tag. // We assume all audios belong to the same group.
String uri = HlsParserUtil.parseOptionalStringAttr(line, URI_ATTR_REGEX);
String language = HlsParserUtil.parseOptionalStringAttr(line, LANGUAGE_ATTR_REGEX);
String audioName = HlsParserUtil.parseStringAttr(line, NAME_ATTR_REGEX, NAME_ATTR);
int audioBitrate = uri != null ? bitrate : -1;
Format format = Format.createAudioContainerFormat(audioName, MimeTypes.APPLICATION_M3U8,
null, audioBitrate, -1, -1, null, language);
if (uri != null) {
audios.add(new Variant(uri, format, codecs));
} else {
muxedAudioFormat = format;
}
} }
} else if (line.startsWith(STREAM_INF_TAG)) { } else if (line.startsWith(STREAM_INF_TAG)) {
bitrate = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR); bitrate = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR);
...@@ -202,7 +229,8 @@ public final class HlsPlaylistParser implements UriLoadable.Parser<HlsPlaylist> ...@@ -202,7 +229,8 @@ public final class HlsPlaylistParser implements UriLoadable.Parser<HlsPlaylist>
expectingStreamInfUrl = false; expectingStreamInfUrl = false;
} }
} }
return new HlsMasterPlaylist(baseUri, variants, subtitles); return new HlsMasterPlaylist(baseUri, variants, audios, subtitles, muxedAudioFormat,
muxedCaptionFormat);
} }
private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String baseUri) private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String baseUri)
......
...@@ -58,8 +58,9 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -58,8 +58,9 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
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_NONE = 0;
private static final int PRIMARY_TYPE_AUDIO = 1; private static final int PRIMARY_TYPE_TEXT = 1;
private static final int PRIMARY_TYPE_VIDEO = 2; 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;
...@@ -136,6 +137,10 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -136,6 +137,10 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
return true; return true;
} else if (!chunkSource.prepare()) { } else if (!chunkSource.prepare()) {
return false; return false;
} else if (chunkSource.getTrackCount() == 0) {
trackGroups = new TrackGroupArray();
prepared = true;
return true;
} }
if (!extractors.isEmpty()) { if (!extractors.isEmpty()) {
while (true) { while (true) {
...@@ -368,15 +373,20 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -368,15 +373,20 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
} else if (loadingFinished) { } else if (loadingFinished) {
return C.END_OF_SOURCE_US; return C.END_OF_SOURCE_US;
} else { } else {
long largestParsedTimestampUs = extractors.getLast().getLargestParsedTimestampUs(); long bufferedPositionUs = extractors.getLast().getLargestParsedTimestampUs();
if (extractors.size() > 1) { if (extractors.size() > 1) {
// When adapting from one format to the next, the penultimate extractor may have the largest // When adapting from one format to the next, the penultimate extractor may have the largest
// parsed timestamp (e.g. if the last extractor hasn't parsed any timestamps yet). // parsed timestamp (e.g. if the last extractor hasn't parsed any timestamps yet).
largestParsedTimestampUs = Math.max(largestParsedTimestampUs, bufferedPositionUs = Math.max(bufferedPositionUs,
extractors.get(extractors.size() - 2).getLargestParsedTimestampUs()); extractors.get(extractors.size() - 2).getLargestParsedTimestampUs());
} }
return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs if (previousTsLoadable != null) {
: largestParsedTimestampUs; // Buffered position should be at least as large as the end time of the previously loaded
// chunk.
bufferedPositionUs = Math.max(previousTsLoadable.endTimeUs, bufferedPositionUs);
}
return bufferedPositionUs == Long.MIN_VALUE ? downstreamPositionUs
: bufferedPositionUs;
} }
} }
...@@ -489,6 +499,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -489,6 +499,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
trackType = PRIMARY_TYPE_VIDEO; trackType = PRIMARY_TYPE_VIDEO;
} else if (MimeTypes.isAudio(sampleMimeType)) { } else if (MimeTypes.isAudio(sampleMimeType)) {
trackType = PRIMARY_TYPE_AUDIO; trackType = PRIMARY_TYPE_AUDIO;
} else if (MimeTypes.isText(sampleMimeType)) {
trackType = PRIMARY_TYPE_TEXT;
} else { } else {
trackType = PRIMARY_TYPE_NONE; trackType = PRIMARY_TYPE_NONE;
} }
...@@ -520,10 +532,18 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -520,10 +532,18 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
for (int j = 0; j < chunkSourceTrackCount; j++) { for (int j = 0; j < chunkSourceTrackCount; j++) {
formats[j] = getSampleFormat(chunkSource.getTrackFormat(j), sampleFormat); formats[j] = getSampleFormat(chunkSource.getTrackFormat(j), sampleFormat);
} }
trackGroups[i] = new TrackGroup(true, formats); trackGroups[i] = new TrackGroup(chunkSource.isAdaptive(), formats);
primaryTrackGroupIndex = i; primaryTrackGroupIndex = i;
} else { } else {
trackGroups[i] = new TrackGroup(sampleFormat); Format trackFormat = null;
if (primaryExtractorTrackType == PRIMARY_TYPE_VIDEO) {
if (MimeTypes.isAudio(sampleFormat.sampleMimeType)) {
trackFormat = chunkSource.getMuxedAudioFormat();
} else if (MimeTypes.APPLICATION_EIA608.equals(sampleFormat.sampleMimeType)) {
trackFormat = chunkSource.getMuxedCaptionFormat();
}
}
trackGroups[i] = new TrackGroup(getSampleFormat(trackFormat, sampleFormat));
} }
} }
this.trackGroups = new TrackGroupArray(trackGroups); this.trackGroups = new TrackGroupArray(trackGroups);
...@@ -550,6 +570,9 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -550,6 +570,9 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
* @return The derived sample format. * @return The derived sample format.
*/ */
private static Format getSampleFormat(Format containerFormat, Format sampleFormat) { private static Format getSampleFormat(Format containerFormat, Format sampleFormat) {
if (containerFormat == null) {
return sampleFormat;
}
int width = containerFormat.width == -1 ? Format.NO_VALUE : containerFormat.width; int width = containerFormat.width == -1 ? Format.NO_VALUE : containerFormat.width;
int height = containerFormat.height == -1 ? Format.NO_VALUE : containerFormat.height; int height = containerFormat.height == -1 ? Format.NO_VALUE : containerFormat.height;
return sampleFormat.copyWithContainerInfo(containerFormat.id, containerFormat.bitrate, width, return sampleFormat.copyWithContainerInfo(containerFormat.id, containerFormat.bitrate, width,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment