Skip to content

Commit

Permalink
Add support for multiple CC channels in HLS
Browse files Browse the repository at this point in the history
Issue:#2161

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=148203980
  • Loading branch information
AquilesCanta authored and ojw28 committed Feb 23, 2017
1 parent e86629e commit 8965508
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
assertNotNull(variants);
Expand Down Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.io.IOException;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

/**
Expand Down Expand Up @@ -85,6 +86,7 @@ public void clear() {
private final HlsUrl[] variants;
private final HlsPlaylistTracker playlistTracker;
private final TrackGroup trackGroup;
private final List<Format> muxedCaptionFormats;

private boolean isTimestampMaster;
private byte[] scratchSpace;
Expand All @@ -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<Format> 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++) {
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -84,6 +86,7 @@
private final Extractor previousExtractor;
private final boolean shouldSpliceIn;
private final boolean needNewExtractor;
private final List<Format> muxedCaptionFormats;

private final boolean isPackedAudio;
private final Id3Decoder id3Decoder;
Expand All @@ -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.
Expand All @@ -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<Format> 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();
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -343,13 +343,12 @@ private void buildAndPrepareSampleStreamWrappers() {
}

private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants,
Format muxedAudioFormat, Format muxedCaptionFormat) {
Format muxedAudioFormat, List<Format> 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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ public interface Callback extends SequenceableLoader.Callback<HlsSampleStreamWra
private final HlsChunkSource chunkSource;
private final Allocator allocator;
private final Format muxedAudioFormat;
private final Format muxedCaptionFormat;
private final int minLoadableRetryCount;
private final Loader loader;
private final EventDispatcher eventDispatcher;
Expand Down Expand Up @@ -113,21 +112,18 @@ public interface Callback extends SequenceableLoader.Callback<HlsSampleStreamWra
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param positionUs The position from which to start loading media.
* @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist.
* @param muxedCaptionFormat Optional muxed closed caption {@link Format} as defined by the master
* playlist.
* @param minLoadableRetryCount The minimum number of times that the source should retry a load
* before propagating an error.
* @param eventDispatcher A dispatcher to notify of events.
*/
public HlsSampleStreamWrapper(int trackType, Callback callback, HlsChunkSource chunkSource,
Allocator allocator, long positionUs, Format muxedAudioFormat, Format muxedCaptionFormat,
int minLoadableRetryCount, EventDispatcher eventDispatcher) {
Allocator allocator, long positionUs, Format muxedAudioFormat, int minLoadableRetryCount,
EventDispatcher eventDispatcher) {
this.trackType = trackType;
this.callback = callback;
this.chunkSource = chunkSource;
this.allocator = allocator;
this.muxedAudioFormat = muxedAudioFormat;
this.muxedCaptionFormat = muxedCaptionFormat;
this.minLoadableRetryCount = minLoadableRetryCount;
this.eventDispatcher = eventDispatcher;
loader = new Loader("Loader:HlsSampleStreamWrapper");
Expand Down Expand Up @@ -589,14 +585,8 @@ private void buildTracks() {
trackGroups[i] = new TrackGroup(formats);
primaryTrackGroupIndex = i;
} else {
Format trackFormat = null;
if (primaryExtractorTrackType == PRIMARY_TYPE_VIDEO) {
if (MimeTypes.isAudio(sampleFormat.sampleMimeType)) {
trackFormat = muxedAudioFormat;
} else if (MimeTypes.APPLICATION_CEA608.equals(sampleFormat.sampleMimeType)) {
trackFormat = muxedCaptionFormat;
}
}
Format trackFormat = primaryExtractorTrackType == PRIMARY_TYPE_VIDEO
&& MimeTypes.isAudio(sampleFormat.sampleMimeType) ? muxedAudioFormat : null;
trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,23 @@ public HlsUrl(String url, Format format) {
public final List<HlsUrl> subtitles;

public final Format muxedAudioFormat;
public final Format muxedCaptionFormat;
public final List<Format> muxedCaptionFormats;

public HlsMasterPlaylist(String baseUri, List<HlsUrl> variants, List<HlsUrl> audios,
List<HlsUrl> subtitles, Format muxedAudioFormat, Format muxedCaptionFormat) {
List<HlsUrl> subtitles, Format muxedAudioFormat, List<Format> 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<HlsUrl> variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUri));
List<HlsUrl> emptyList = Collections.emptyList();
return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null);
return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null,
Collections.<Format>emptyList());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
+ "|" + TYPE_SUBTITLES + "|" + TYPE_CLOSED_CAPTIONS + ")");
private static final Pattern REGEX_LANGUAGE = Pattern.compile("LANGUAGE=\"(.+?)\"");
private static final Pattern REGEX_NAME = Pattern.compile("NAME=\"(.+?)\"");
private static final Pattern REGEX_INSTREAM_ID = Pattern.compile("INSTREAM-ID=\"(.+?)\"");
private static final Pattern REGEX_INSTREAM_ID =
Pattern.compile("INSTREAM-ID=\"((?:CC|SERVICE)\\d+)\"");
private static final Pattern REGEX_AUTOSELECT = compileBooleanAttrPattern("AUTOSELECT");
private static final Pattern REGEX_DEFAULT = compileBooleanAttrPattern("DEFAULT");
private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED");
Expand Down Expand Up @@ -171,7 +172,7 @@ private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Stri
ArrayList<HlsMasterPlaylist.HlsUrl> audios = new ArrayList<>();
ArrayList<HlsMasterPlaylist.HlsUrl> subtitles = new ArrayList<>();
Format muxedAudioFormat = null;
Format muxedCaptionFormat = null;
ArrayList<Format> muxedCaptionFormats = new ArrayList<>();

String line;
while (iterator.hasNext()) {
Expand All @@ -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.
Expand Down Expand Up @@ -234,7 +243,7 @@ private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Stri
}
}
return new HlsMasterPlaylist(baseUri, variants, audios, subtitles, muxedAudioFormat,
muxedCaptionFormat);
muxedCaptionFormats);
}

@C.SelectionFlags
Expand Down

0 comments on commit 8965508

Please sign in to comment.