mirror of
https://github.com/samsonjs/media.git
synced 2026-03-28 09:55:48 +00:00
Add support for multiple CC channels in HLS
Issue:#2161 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148203980
This commit is contained in:
parent
e86629ef3a
commit
896550883f
7 changed files with 68 additions and 46 deletions
|
|
@ -19,6 +19,7 @@ import android.net.Uri;
|
|||
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<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
|
||||
assertNotNull(variants);
|
||||
|
|
@ -98,18 +101,28 @@ public class HlsMasterPlaylistParserTest extends TestCase {
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
|
|
@ -85,6 +86,7 @@ import java.util.Locale;
|
|||
private final HlsUrl[] variants;
|
||||
private final HlsPlaylistTracker playlistTracker;
|
||||
private final TrackGroup trackGroup;
|
||||
private final List<Format> muxedCaptionFormats;
|
||||
|
||||
private boolean isTimestampMaster;
|
||||
private byte[] scratchSpace;
|
||||
|
|
@ -107,14 +109,16 @@ import java.util.Locale;
|
|||
* @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++) {
|
||||
|
|
@ -282,7 +286,7 @@ import java.util.Locale;
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls;
|
|||
|
||||
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.ParsableByteArray;
|
|||
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 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
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;
|
||||
|
|
@ -102,6 +105,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
* @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 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
* @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();
|
||||
|
|
@ -363,7 +369,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
}
|
||||
}
|
||||
extractor = new TsExtractor(timestampAdjuster,
|
||||
new DefaultTsPayloadReaderFactory(esReaderFactoryFlags), true);
|
||||
new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats), true);
|
||||
}
|
||||
if (usingNewExtractor) {
|
||||
extractor.init(extractorOutput);
|
||||
|
|
|
|||
|
|
@ -317,7 +317,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||
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 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||
}
|
||||
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -77,7 +77,6 @@ import java.util.LinkedList;
|
|||
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;
|
||||
|
|
@ -113,21 +112,18 @@ import java.util.LinkedList;
|
|||
* @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");
|
||||
|
|
@ -589,14 +585,8 @@ import java.util.LinkedList;
|
|||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,22 +52,23 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
|
|||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
@ -171,7 +172,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
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()) {
|
||||
|
|
@ -198,10 +199,18 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
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 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
}
|
||||
}
|
||||
return new HlsMasterPlaylist(baseUri, variants, audios, subtitles, muxedAudioFormat,
|
||||
muxedCaptionFormat);
|
||||
muxedCaptionFormats);
|
||||
}
|
||||
|
||||
@C.SelectionFlags
|
||||
|
|
|
|||
Loading…
Reference in a new issue