From 896550883f6d183e3ef46351eb3e494df8eaefba Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 22 Feb 2017 04:11:55 -0800 Subject: [PATCH] Add support for multiple CC channels in HLS Issue:#2161 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148203980 --- .../playlist/HlsMasterPlaylistParserTest.java | 29 ++++++++++++++----- .../exoplayer2/source/hls/HlsChunkSource.java | 10 +++++-- .../exoplayer2/source/hls/HlsMediaChunk.java | 18 ++++++++---- .../exoplayer2/source/hls/HlsMediaPeriod.java | 9 +++--- .../source/hls/HlsSampleStreamWrapper.java | 18 +++--------- .../hls/playlist/HlsMasterPlaylist.java | 9 +++--- .../hls/playlist/HlsPlaylistParser.java | 21 ++++++++++---- 7 files changed, 68 insertions(+), 46 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index f0adf274eea..aa279f23f40 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.util.MimeTypes; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.Charset; @@ -53,12 +54,14 @@ public class HlsMasterPlaylistParserTest extends TestCase { + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "http://example.com/low.m3u8\n"; - public void testParseMasterPlaylist() throws IOException{ - HlsPlaylist playlist = parsePlaylist(PLAYLIST_URI, MASTER_PLAYLIST); - assertNotNull(playlist); - assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type); + private static final String MASTER_PLAYLIST_WITH_CC = " #EXTM3U \n" + + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n"; - HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; + public void testParseMasterPlaylist() throws IOException{ + HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST); List variants = masterPlaylist.variants; assertNotNull(variants); @@ -98,18 +101,28 @@ public void testParseMasterPlaylist() throws IOException{ public void testPlaylistWithInvalidHeader() throws IOException { try { - parsePlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); + parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); fail("Expected exception not thrown."); } catch (ParserException e) { // Expected due to invalid header. } } - private static HlsPlaylist parsePlaylist(String uri, String playlistString) throws IOException { + public void testPlaylistWithClosedCaption() throws IOException { + HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST_WITH_CC); + assertEquals(1, playlist.muxedCaptionFormats.size()); + Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0); + assertEquals(MimeTypes.APPLICATION_CEA708, closedCaptionFormat.sampleMimeType); + assertEquals(4, closedCaptionFormat.accessibilityChannel); + assertEquals("es", closedCaptionFormat.language); + } + + private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) + throws IOException { Uri playlistUri = Uri.parse(uri); ByteArrayInputStream inputStream = new ByteArrayInputStream( playlistString.getBytes(Charset.forName(C.UTF8_NAME))); - return new HlsPlaylistParser().parse(playlistUri, inputStream); + return (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index c7c66fbd617..7ba5cf2df1e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -38,6 +38,7 @@ import java.io.IOException; import java.math.BigInteger; import java.util.Arrays; +import java.util.List; import java.util.Locale; /** @@ -85,6 +86,7 @@ public void clear() { private final HlsUrl[] variants; private final HlsPlaylistTracker playlistTracker; private final TrackGroup trackGroup; + private final List muxedCaptionFormats; private boolean isTimestampMaster; private byte[] scratchSpace; @@ -107,14 +109,16 @@ public void clear() { * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. */ public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants, - DataSource dataSource, TimestampAdjusterProvider timestampAdjusterProvider) { + DataSource dataSource, TimestampAdjusterProvider timestampAdjusterProvider, + List muxedCaptionFormats) { this.playlistTracker = playlistTracker; this.variants = variants; this.dataSource = dataSource; this.timestampAdjusterProvider = timestampAdjusterProvider; - + this.muxedCaptionFormats = muxedCaptionFormats; Format[] variantFormats = new Format[variants.length]; int[] initialTrackSelection = new int[variants.length]; for (int i = 0; i < variants.length; i++) { @@ -282,7 +286,7 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, selectedUrl, - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), + muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 5885797896a..357a32f0862 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -17,6 +17,7 @@ import android.text.TextUtils; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; @@ -39,6 +40,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** @@ -84,6 +86,7 @@ private final Extractor previousExtractor; private final boolean shouldSpliceIn; private final boolean needNewExtractor; + private final List muxedCaptionFormats; private final boolean isPackedAudio; private final Id3Decoder id3Decoder; @@ -102,6 +105,7 @@ * @param dataSpec Defines the data to be loaded. * @param initDataSpec Defines the initialization data to be fed to new extractors. May be null. * @param hlsUrl The url of the playlist from which this chunk was obtained. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. * @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the chunk in microseconds. @@ -115,17 +119,19 @@ * @param encryptionIv For AES encryption chunks, the encryption initialization vector. */ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, - HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, long startTimeUs, - long endTimeUs, int chunkIndex, int discontinuitySequenceNumber, - boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, - HlsMediaChunk previousChunk, byte[] encryptionKey, byte[] encryptionIv) { + HlsUrl hlsUrl, List muxedCaptionFormats, int trackSelectionReason, + Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex, + int discontinuitySequenceNumber, boolean isMasterTimestampSource, + TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, byte[] encryptionKey, + byte[] encryptionIv) { super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); + this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.initDataSpec = initDataSpec; this.hlsUrl = hlsUrl; + this.muxedCaptionFormats = muxedCaptionFormats; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; - this.discontinuitySequenceNumber = discontinuitySequenceNumber; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; lastPathSegment = dataSpec.uri.getLastPathSegment(); @@ -363,7 +369,7 @@ private Extractor createExtractor() { } } extractor = new TsExtractor(timestampAdjuster, - new DefaultTsPayloadReaderFactory(esReaderFactoryFlags), true); + new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats), true); } if (usingNewExtractor) { extractor.init(extractorOutput); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 6082372b05c..0ae8becfc05 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -317,7 +317,7 @@ private void buildAndPrepareSampleStreamWrappers() { HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()]; selectedVariants.toArray(variants); HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, - variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat); + variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.setIsTimestampMaster(true); sampleStreamWrapper.continuePreparing(); @@ -343,13 +343,12 @@ private void buildAndPrepareSampleStreamWrappers() { } private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, - Format muxedAudioFormat, Format muxedCaptionFormat) { + Format muxedAudioFormat, List muxedCaptionFormats) { DataSource dataSource = dataSourceFactory.createDataSource(); HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, dataSource, - timestampAdjusterProvider); + timestampAdjusterProvider, muxedCaptionFormats); return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, - preparePositionUs, muxedAudioFormat, muxedCaptionFormat, minLoadableRetryCount, - eventDispatcher); + preparePositionUs, muxedAudioFormat, minLoadableRetryCount, eventDispatcher); } private void continuePreparingOrLoading() { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 6980fdd7a4b..0e3ee6fa9c3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -77,7 +77,6 @@ public interface Callback extends SequenceableLoader.Callback subtitles; public final Format muxedAudioFormat; - public final Format muxedCaptionFormat; + public final List muxedCaptionFormats; public HlsMasterPlaylist(String baseUri, List variants, List audios, - List subtitles, Format muxedAudioFormat, Format muxedCaptionFormat) { + List subtitles, Format muxedAudioFormat, List muxedCaptionFormats) { 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; + this.muxedCaptionFormats = Collections.unmodifiableList(muxedCaptionFormats); } public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUri) { List variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUri)); List emptyList = Collections.emptyList(); - return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null); + return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, + Collections.emptyList()); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 6efd1fecb2b..6c29535326f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -94,7 +94,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); Format muxedAudioFormat = null; - Format muxedCaptionFormat = null; + ArrayList muxedCaptionFormats = new ArrayList<>(); String line; while (iterator.hasNext()) { @@ -198,10 +199,18 @@ private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Stri subtitles.add(new HlsMasterPlaylist.HlsUrl(uri, format)); break; case TYPE_CLOSED_CAPTIONS: - if ("CC1".equals(parseOptionalStringAttr(line, REGEX_INSTREAM_ID))) { - muxedCaptionFormat = Format.createTextContainerFormat(id, MimeTypes.APPLICATION_M3U8, - MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, selectionFlags, language); + String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID); + String mimeType; + int accessibilityChannel; + if (instreamId.startsWith("CC")) { + mimeType = MimeTypes.APPLICATION_CEA608; + accessibilityChannel = Integer.parseInt(instreamId.substring(2)); + } else /* starts with SERVICE */ { + mimeType = MimeTypes.APPLICATION_CEA708; + accessibilityChannel = Integer.parseInt(instreamId.substring(7)); } + muxedCaptionFormats.add(Format.createTextContainerFormat(id, null, mimeType, null, + Format.NO_VALUE, selectionFlags, language, accessibilityChannel)); break; default: // Do nothing. @@ -234,7 +243,7 @@ private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Stri } } return new HlsMasterPlaylist(baseUri, variants, audios, subtitles, muxedAudioFormat, - muxedCaptionFormat); + muxedCaptionFormats); } @C.SelectionFlags