From 1eff6cf210940893afdf28d30251626f1dbc1552 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 16 Mar 2016 07:01:16 -0700 Subject: [PATCH] Bring back multi-audio and VTT support for HLS. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=117338865 --- .../exoplayer/demo/player/DemoPlayer.java | 4 +- .../demo/player/HlsSourceBuilder.java | 29 +++- .../demo/ui/TrackSelectionHelper.java | 4 +- .../exoplayer/ExoPlayerImplInternal.java | 5 +- .../com/google/android/exoplayer/Format.java | 2 +- .../android/exoplayer/hls/HlsChunkSource.java | 143 ++++++++---------- .../exoplayer/hls/HlsMasterPlaylist.java | 12 +- .../exoplayer/hls/HlsPlaylistParser.java | 38 ++++- .../exoplayer/hls/HlsSampleSource.java | 39 ++++- 9 files changed, 171 insertions(+), 105 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index 4912d06c10..03953cd00d 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -428,14 +428,14 @@ public class DemoPlayer implements ExoPlayer.Listener, DefaultTrackSelector.Even @Override public void onCues(List cues) { - if (captionListener != null && trackInfo.getTrackSelection(TYPE_TEXT) != null) { + if (captionListener != null) { captionListener.onCues(cues); } } @Override public void onMetadata(List id3Frames) { - if (id3MetadataListener != null && trackInfo.getTrackSelection(TYPE_METADATA) != null) { + if (id3MetadataListener != null) { id3MetadataListener.onId3Metadata(id3Frames); } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsSourceBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsSourceBuilder.java index 804ef20fa4..d3098dc2fa 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsSourceBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsSourceBuilder.java @@ -17,7 +17,9 @@ package com.google.android.exoplayer.demo.player; import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.MultiSampleSource; 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.hls.HlsChunkSource; import com.google.android.exoplayer.hls.HlsPlaylist; @@ -40,7 +42,9 @@ import android.os.Handler; public class HlsSourceBuilder implements SourceBuilder { 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 String userAgent; @@ -64,11 +68,26 @@ public class HlsSourceBuilder implements SourceBuilder { LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider(); - DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); - HlsChunkSource chunkSource = new HlsChunkSource(manifestFetcher, HlsChunkSource.TYPE_DEFAULT, - dataSource, bandwidthMeter, timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE); - return new HlsSampleSource(chunkSource, loadControl, + DataSource defaultDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); + HlsChunkSource defaultChunkSource = new HlsChunkSource(manifestFetcher, + HlsChunkSource.TYPE_DEFAULT, defaultDataSource, timestampAdjusterProvider, + new FormatEvaluator.AdaptiveEvaluator(bandwidthMeter)); + HlsSampleSource defaultSampleSource = new HlsSampleSource(defaultChunkSource, loadControl, 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); } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/ui/TrackSelectionHelper.java b/demo/src/main/java/com/google/android/exoplayer/demo/ui/TrackSelectionHelper.java index f96749af4d..fe1c1609cb 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/ui/TrackSelectionHelper.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/ui/TrackSelectionHelper.java @@ -121,10 +121,12 @@ public class TrackSelectionHelper implements View.OnClickListener, DialogInterfa boolean haveSupportedTracks = false; trackViews = new CheckedTextView[trackGroups.length][]; for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { - root.addView(inflater.inflate(R.layout.list_divider, root, false)); TrackGroup group = trackGroups.get(groupIndex); trackViews[groupIndex] = new CheckedTextView[group.length]; 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] ? android.R.layout.simple_list_item_single_choice : android.R.layout.simple_list_item_multiple_choice; diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java index 1044875e4c..68c1a08a6c 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -558,6 +558,8 @@ import java.util.concurrent.atomic.AtomicInteger; } } + // The new selections are being activated. + trackSelector.onSelectionActivated(result.second); enabledRenderers = new TrackRenderer[enabledRendererCount]; enabledRendererCount = 0; @@ -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 { diff --git a/library/src/main/java/com/google/android/exoplayer/Format.java b/library/src/main/java/com/google/android/exoplayer/Format.java index 7e17edfd19..14dfe1edce 100644 --- a/library/src/main/java/com/google/android/exoplayer/Format.java +++ b/library/src/main/java/com/google/android/exoplayer/Format.java @@ -295,7 +295,7 @@ public final class Format { encoderDelay, encoderPadding, language, subsampleOffsetUs, initializationData, requiresSecureDecryption); } - + /** * @return A {@link MediaFormat} representation of this format. */ 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 7250410622..1b67aca712 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 @@ -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.PtsTimestampAdjuster; import com.google.android.exoplayer.extractor.ts.TsExtractor; -import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException; @@ -64,41 +63,8 @@ public class HlsChunkSource { public interface EventListener extends BaseChunkSampleSourceEventListener {} public static final int TYPE_DEFAULT = 0; - public static final int TYPE_VTT = 1; - - /** - * Adaptive switching is disabled. - *

- * 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. - *

- * 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. - *

- * 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. - *

- * 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. - *

- * 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; + public static final int TYPE_AUDIO = 1; + public static final int TYPE_SUBTITLE = 2; /** * The default time for which a media playlist should be blacklisted. @@ -118,7 +84,6 @@ public class HlsChunkSource { private final Evaluation evaluation; private final HlsPlaylistParser playlistParser; private final PtsTimestampAdjusterProvider timestampAdjusterProvider; - private final int adaptiveMode; private boolean manifestFetcherEnabled; private byte[] scratchSpace; @@ -146,26 +111,22 @@ public class HlsChunkSource { /** * @param manifestFetcher A fetcher for the playlist. - * @param type The type of chunk provided by the source. One of {@link #TYPE_DEFAULT} and - * {@link #TYPE_VTT}. + * @param type The type of chunk provided by the source. One of {@link #TYPE_DEFAULT}, + * {@link #TYPE_AUDIO} and {@link #TYPE_SUBTITLE}. * @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 * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. - * @param adaptiveMode The mode for switching from one variant to another. One of - * {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and - * {@link #ADAPTIVE_MODE_SPLICE}. + * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. */ public HlsChunkSource(ManifestFetcher manifestFetcher, int type, - DataSource dataSource, BandwidthMeter bandwidthMeter, - PtsTimestampAdjusterProvider timestampAdjusterProvider, int adaptiveMode) { + DataSource dataSource, PtsTimestampAdjusterProvider timestampAdjusterProvider, + FormatEvaluator adaptiveFormatEvaluator) { this.manifestFetcher = manifestFetcher; this.type = type; this.dataSource = dataSource; - this.adaptiveFormatEvaluator = new FormatEvaluator.AdaptiveEvaluator(bandwidthMeter); + this.adaptiveFormatEvaluator = adaptiveFormatEvaluator; this.timestampAdjusterProvider = timestampAdjusterProvider; - this.adaptiveMode = adaptiveMode; playlistParser = new HlsPlaylistParser(); evaluation = new Evaluation(); } @@ -182,6 +143,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. * @@ -209,11 +179,13 @@ public class HlsChunkSource { List variants = new ArrayList<>(); variants.add(new Variant(baseUri, format, null)); masterPlaylist = new HlsMasterPlaylist(baseUri, variants, - Collections.emptyList()); + Collections.emptyList(), Collections.emptyList(), null, null); } processMasterPlaylist(masterPlaylist); - // TODO[REFACTOR]: Come up with a sane default here. - selectTracks(new int[] {0}); + if (exposedVariants.length > 0) { + // TODO[REFACTOR]: Come up with a sane default here. + selectTracks(new int[] {0}); + } } } return true; @@ -264,6 +236,28 @@ public class HlsChunkSource { return exposedVariants[index].format; } + /** + * Returns the format of the audio muxed into variants, or null if unknown. + *

+ * 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. + *

+ * 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. *

@@ -333,17 +327,9 @@ public class HlsChunkSource { */ public void getChunkOperation(TsChunk previousTsChunk, long playbackPositionUs, ChunkOperationHolder out) { - int nextVariantIndex; - boolean switchingVariantSpliced; - if (adaptiveMode == ADAPTIVE_MODE_NONE) { - nextVariantIndex = selectedVariantIndex; - switchingVariantSpliced = false; - } else { - nextVariantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs); - switchingVariantSpliced = previousTsChunk != null - && enabledVariants[nextVariantIndex].format != previousTsChunk.format - && adaptiveMode == ADAPTIVE_MODE_SPLICE; - } + int nextVariantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs); + boolean switchingVariant = previousTsChunk != null + && enabledVariants[nextVariantIndex].format != previousTsChunk.format; HlsMediaPlaylist mediaPlaylist = enabledVariantPlaylists[nextVariantIndex]; if (mediaPlaylist == null) { @@ -358,8 +344,8 @@ public class HlsChunkSource { if (previousTsChunk == null) { chunkMediaSequence = getLiveStartChunkMediaSequence(nextVariantIndex); } else { - chunkMediaSequence = switchingVariantSpliced - ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1; + chunkMediaSequence = switchingVariant ? previousTsChunk.chunkIndex + : previousTsChunk.chunkIndex + 1; if (chunkMediaSequence < mediaPlaylist.mediaSequence) { fatalError = new BehindLiveWindowException(); return; @@ -371,8 +357,8 @@ public class HlsChunkSource { chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs, true, true) + mediaPlaylist.mediaSequence; } else { - chunkMediaSequence = switchingVariantSpliced - ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1; + chunkMediaSequence = switchingVariant ? previousTsChunk.chunkIndex + : previousTsChunk.chunkIndex + 1; } } @@ -413,7 +399,7 @@ public class HlsChunkSource { if (live) { if (previousTsChunk == null) { startTimeUs = 0; - } else if (switchingVariantSpliced) { + } else if (switchingVariant) { startTimeUs = previousTsChunk.startTimeUs; } else { startTimeUs = previousTsChunk.endTimeUs; @@ -434,11 +420,11 @@ public class HlsChunkSource { // case below. Extractor extractor = new AdtsExtractor(startTimeUs); extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, - switchingVariantSpliced); + switchingVariant); } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { Extractor extractor = new Mp3Extractor(startTimeUs); extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, - switchingVariantSpliced); + switchingVariant); } else if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { PtsTimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(false, @@ -451,7 +437,7 @@ public class HlsChunkSource { } Extractor extractor = new WebvttExtractor(format.language, timestampAdjuster); extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, - switchingVariantSpliced); + switchingVariant); } else if (previousTsChunk == null || previousTsChunk.discontinuitySequenceNumber != segment.discontinuitySequenceNumber || format != previousTsChunk.format) { @@ -468,16 +454,16 @@ public class HlsChunkSource { // 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 // 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; } - if (MimeTypes.getVideoMediaMimeType(codecs) != MimeTypes.VIDEO_H264) { + if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_H264_STREAM; } } Extractor extractor = new TsExtractor(timestampAdjuster, workaroundFlags); extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, - switchingVariantSpliced); + switchingVariant); } else { // MPEG-2 TS segments, and we need to continue using the same extractor. extractorWrapper = previousTsChunk.extractorWrapper; @@ -562,11 +548,11 @@ public class HlsChunkSource { // Private methods. private void processMasterPlaylist(HlsMasterPlaylist playlist) { - if (type == TYPE_VTT) { - List subtitleVariants = playlist.subtitles; - if (subtitleVariants != null) { - exposedVariants = new Variant[subtitleVariants.size()]; - subtitleVariants.toArray(exposedVariants); + if (type == TYPE_SUBTITLE || type == TYPE_AUDIO) { + List variants = type == TYPE_AUDIO ? playlist.audios : playlist.subtitles; + if (variants != null && !variants.isEmpty()) { + exposedVariants = new Variant[variants.size()]; + variants.toArray(exposedVariants); } else { exposedVariants = new Variant[0]; } @@ -622,8 +608,7 @@ public class HlsChunkSource { long switchingOverlapUs; List queue; if (previousTsChunk != null) { - switchingOverlapUs = adaptiveMode == ADAPTIVE_MODE_SPLICE - ? previousTsChunk.endTimeUs - previousTsChunk.startTimeUs : 0; + switchingOverlapUs = previousTsChunk.endTimeUs - previousTsChunk.startTimeUs; queue = Collections.singletonList(previousTsChunk); } else { switchingOverlapUs = 0; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java index 4222c5ab5b..d88ba2b526 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer.hls; +import com.google.android.exoplayer.Format; + import java.util.Collections; import java.util.List; @@ -24,12 +26,20 @@ import java.util.List; public final class HlsMasterPlaylist extends HlsPlaylist { public final List variants; + public final List audios; public final List subtitles; - public HlsMasterPlaylist(String baseUri, List variants, List subtitles) { + public final Format muxedAudioFormat; + public final Format muxedCaptionFormat; + + public HlsMasterPlaylist(String baseUri, List variants, List audios, + List subtitles, Format muxedAudioFormat, Format muxedCaptionFormat) { super(baseUri, HlsPlaylist.TYPE_MASTER); this.variants = Collections.unmodifiableList(variants); + this.audios = Collections.unmodifiableList(audios); this.subtitles = Collections.unmodifiableList(subtitles); + this.muxedAudioFormat = muxedAudioFormat; + this.muxedCaptionFormat = muxedCaptionFormat; } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java index b70d3141ba..4d79dc7246 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java @@ -59,6 +59,7 @@ public final class HlsPlaylistParser implements UriLoadable.Parser private static final String METHOD_ATTR = "METHOD"; private static final String URI_ATTR = "URI"; 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 VIDEO_TYPE = "VIDEO"; @@ -98,6 +99,8 @@ public final class HlsPlaylistParser implements UriLoadable.Parser Pattern.compile(LANGUAGE_ATTR + "=\"(.+?)\""); private static final Pattern NAME_ATTR_REGEX = Pattern.compile(NAME_ATTR + "=\"(.+?)\""); + private static final Pattern INSTREAM_ID_ATTR_REGEX = + Pattern.compile(INSTREAM_ID_ATTR + "=\"(.+?)\""); // private static final Pattern AUTOSELECT_ATTR_REGEX = // HlsParserUtil.compileBooleanAttrPattern(AUTOSELECT_ATTR); // private static final Pattern DEFAULT_ATTR_REGEX = @@ -140,12 +143,15 @@ public final class HlsPlaylistParser implements UriLoadable.Parser private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri) throws IOException { ArrayList variants = new ArrayList<>(); + ArrayList audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); int bitrate = 0; String codecs = null; int width = -1; int height = -1; String name = null; + Format muxedAudioFormat = null; + Format muxedCaptionFormat = null; boolean expectingStreamInfUrl = false; String line; @@ -153,16 +159,37 @@ public final class HlsPlaylistParser implements UriLoadable.Parser line = iterator.next(); if (line.startsWith(MEDIA_TAG)) { 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. String subtitleName = HlsParserUtil.parseStringAttr(line, NAME_ATTR_REGEX, NAME_ATTR); String uri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX, URI_ATTR); String language = HlsParserUtil.parseOptionalStringAttr(line, LANGUAGE_ATTR_REGEX); Format format = Format.createTextContainerFormat(subtitleName, MimeTypes.APPLICATION_M3U8, MimeTypes.TEXT_VTT, bitrate, language); - subtitles.add(new Variant(uri, format, null)); - } else { - // TODO: Support other types of media tag. + subtitles.add(new Variant(uri, format, codecs)); + } else if (AUDIO_TYPE.equals(type)) { + // 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)) { bitrate = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR); @@ -202,7 +229,8 @@ public final class HlsPlaylistParser implements UriLoadable.Parser 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) 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 32b0756d2c..242940ce30 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 @@ -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 int PRIMARY_TYPE_NONE = 0; - private static final int PRIMARY_TYPE_AUDIO = 1; - private static final int PRIMARY_TYPE_VIDEO = 2; + 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; @@ -136,6 +137,10 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { return true; } else if (!chunkSource.prepare()) { return false; + } else if (chunkSource.getTrackCount() == 0) { + trackGroups = new TrackGroupArray(); + prepared = true; + return true; } if (!extractors.isEmpty()) { while (true) { @@ -368,15 +373,20 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { } else if (loadingFinished) { return C.END_OF_SOURCE_US; } else { - long largestParsedTimestampUs = extractors.getLast().getLargestParsedTimestampUs(); + long bufferedPositionUs = extractors.getLast().getLargestParsedTimestampUs(); if (extractors.size() > 1) { // 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). - largestParsedTimestampUs = Math.max(largestParsedTimestampUs, + bufferedPositionUs = Math.max(bufferedPositionUs, extractors.get(extractors.size() - 2).getLargestParsedTimestampUs()); } - return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs - : largestParsedTimestampUs; + if (previousTsLoadable != null) { + // 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 { trackType = PRIMARY_TYPE_VIDEO; } else if (MimeTypes.isAudio(sampleMimeType)) { trackType = PRIMARY_TYPE_AUDIO; + } else if (MimeTypes.isText(sampleMimeType)) { + trackType = PRIMARY_TYPE_TEXT; } else { trackType = PRIMARY_TYPE_NONE; } @@ -520,10 +532,18 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { for (int j = 0; j < chunkSourceTrackCount; j++) { formats[j] = getSampleFormat(chunkSource.getTrackFormat(j), sampleFormat); } - trackGroups[i] = new TrackGroup(true, formats); + trackGroups[i] = new TrackGroup(chunkSource.isAdaptive(), formats); primaryTrackGroupIndex = i; } 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); @@ -550,6 +570,9 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { * @return The derived sample format. */ private static Format getSampleFormat(Format containerFormat, Format sampleFormat) { + if (containerFormat == null) { + return sampleFormat; + } int width = containerFormat.width == -1 ? Format.NO_VALUE : containerFormat.width; int height = containerFormat.height == -1 ? Format.NO_VALUE : containerFormat.height; return sampleFormat.copyWithContainerInfo(containerFormat.id, containerFormat.bitrate, width,