Allow exposing multiple CEA608 tracks for Transport Streams
This CL allows passing multiple formats describing CC channels to the TS payload reader factory. As a simple usecase, ATSC can expose both 608 channels by passing a two element list with the corresponding accessibility channels. The HLS media source can construct this list from the EXT-X-MEDIA:TYPE="CLOSED-CAPTIONS" tags, including language and selection flags. The interface extends without modification to 708. Pending work: * Multiple CC channels in HLS. * caption_service_descriptor parsing for overriding the user's selection. * 708 support in SEI reader. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=148030293
This commit is contained in:
parent
a84216c3a9
commit
e86629ef3a
@ -157,7 +157,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||||||
// Extractor output.
|
// Extractor output.
|
||||||
private ExtractorOutput extractorOutput;
|
private ExtractorOutput extractorOutput;
|
||||||
private TrackOutput eventMessageTrackOutput;
|
private TrackOutput eventMessageTrackOutput;
|
||||||
private TrackOutput cea608TrackOutput;
|
private TrackOutput[] cea608TrackOutputs;
|
||||||
|
|
||||||
// Whether extractorOutput.seekMap has been called.
|
// Whether extractorOutput.seekMap has been called.
|
||||||
private boolean haveOutputSeekMap;
|
private boolean haveOutputSeekMap;
|
||||||
@ -459,10 +459,12 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||||||
eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG,
|
eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG,
|
||||||
Format.OFFSET_SAMPLE_RELATIVE));
|
Format.OFFSET_SAMPLE_RELATIVE));
|
||||||
}
|
}
|
||||||
if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutput == null) {
|
if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) {
|
||||||
cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, C.TRACK_TYPE_TEXT);
|
TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1,
|
||||||
|
C.TRACK_TYPE_TEXT);
|
||||||
cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608,
|
cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608,
|
||||||
null, Format.NO_VALUE, 0, null, null));
|
null, Format.NO_VALUE, 0, null, null));
|
||||||
|
cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1085,7 +1087,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||||||
output.sampleData(nalStartCode, 4);
|
output.sampleData(nalStartCode, 4);
|
||||||
// Write the NAL unit type byte.
|
// Write the NAL unit type byte.
|
||||||
output.sampleData(nalPrefix, 1);
|
output.sampleData(nalPrefix, 1);
|
||||||
processSeiNalUnitPayload = cea608TrackOutput != null
|
processSeiNalUnitPayload = cea608TrackOutputs != null
|
||||||
&& NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]);
|
&& NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]);
|
||||||
sampleBytesWritten += 5;
|
sampleBytesWritten += 5;
|
||||||
sampleSize += nalUnitLengthFieldLengthDiff;
|
sampleSize += nalUnitLengthFieldLengthDiff;
|
||||||
@ -1103,7 +1105,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||||||
nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0);
|
nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0);
|
||||||
nalBuffer.setLimit(unescapedLength);
|
nalBuffer.setLimit(unescapedLength);
|
||||||
CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer,
|
CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer,
|
||||||
cea608TrackOutput);
|
cea608TrackOutputs);
|
||||||
} else {
|
} else {
|
||||||
// Write the payload of the NAL unit.
|
// Write the payload of the NAL unit.
|
||||||
writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false);
|
writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false);
|
||||||
|
@ -17,9 +17,13 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||||||
|
|
||||||
import android.support.annotation.IntDef;
|
import android.support.annotation.IntDef;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
|
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
|
||||||
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default implementation for {@link TsPayloadReader.Factory}.
|
* Default implementation for {@link TsPayloadReader.Factory}.
|
||||||
@ -35,19 +39,36 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
|
|||||||
public @interface Flags {
|
public @interface Flags {
|
||||||
}
|
}
|
||||||
public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1;
|
public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1;
|
||||||
public static final int FLAG_IGNORE_AAC_STREAM = 2;
|
public static final int FLAG_IGNORE_AAC_STREAM = 1 << 1;
|
||||||
public static final int FLAG_IGNORE_H264_STREAM = 4;
|
public static final int FLAG_IGNORE_H264_STREAM = 1 << 2;
|
||||||
public static final int FLAG_DETECT_ACCESS_UNITS = 8;
|
public static final int FLAG_DETECT_ACCESS_UNITS = 1 << 3;
|
||||||
public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 16;
|
public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 1 << 4;
|
||||||
|
|
||||||
@Flags private final int flags;
|
@Flags private final int flags;
|
||||||
|
private final List<Format> closedCaptionFormats;
|
||||||
|
|
||||||
public DefaultTsPayloadReaderFactory() {
|
public DefaultTsPayloadReaderFactory() {
|
||||||
this(0);
|
this(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param flags A combination of {@code FLAG_*} values, which control the behavior of the created
|
||||||
|
* readers.
|
||||||
|
*/
|
||||||
public DefaultTsPayloadReaderFactory(@Flags int flags) {
|
public DefaultTsPayloadReaderFactory(@Flags int flags) {
|
||||||
|
this(flags, Collections.singletonList(Format.createTextSampleFormat(null,
|
||||||
|
MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param flags A combination of {@code FLAG_*} values, which control the behavior of the created
|
||||||
|
* readers.
|
||||||
|
* @param closedCaptionFormats {@link Format}s to be exposed by elementary stream readers for
|
||||||
|
* streams with embedded closed captions.
|
||||||
|
*/
|
||||||
|
public DefaultTsPayloadReaderFactory(@Flags int flags, List<Format> closedCaptionFormats) {
|
||||||
this.flags = flags;
|
this.flags = flags;
|
||||||
|
this.closedCaptionFormats = closedCaptionFormats;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -74,10 +95,10 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
|
|||||||
return new PesReader(new H262Reader());
|
return new PesReader(new H262Reader());
|
||||||
case TsExtractor.TS_STREAM_TYPE_H264:
|
case TsExtractor.TS_STREAM_TYPE_H264:
|
||||||
return isSet(FLAG_IGNORE_H264_STREAM) ? null
|
return isSet(FLAG_IGNORE_H264_STREAM) ? null
|
||||||
: new PesReader(new H264Reader(new SeiReader(), isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES),
|
: new PesReader(new H264Reader(buildSeiReader(), isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES),
|
||||||
isSet(FLAG_DETECT_ACCESS_UNITS)));
|
isSet(FLAG_DETECT_ACCESS_UNITS)));
|
||||||
case TsExtractor.TS_STREAM_TYPE_H265:
|
case TsExtractor.TS_STREAM_TYPE_H265:
|
||||||
return new PesReader(new H265Reader(new SeiReader()));
|
return new PesReader(new H265Reader(buildSeiReader()));
|
||||||
case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO:
|
case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO:
|
||||||
return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM)
|
return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM)
|
||||||
? null : new SectionReader(new SpliceInfoSectionReader());
|
? null : new SectionReader(new SpliceInfoSectionReader());
|
||||||
@ -88,6 +109,11 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SeiReader buildSeiReader() {
|
||||||
|
// TODO: Add descriptor parsing to detect channels automatically.
|
||||||
|
return new SeiReader(closedCaptionFormats);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isSet(@Flags int flag) {
|
private boolean isSet(@Flags int flag) {
|
||||||
return (flags & flag) != 0;
|
return (flags & flag) != 0;
|
||||||
}
|
}
|
||||||
|
@ -21,25 +21,45 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
|||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||||
import com.google.android.exoplayer2.text.cea.CeaUtil;
|
import com.google.android.exoplayer2.text.cea.CeaUtil;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}.
|
* Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}.
|
||||||
*/
|
*/
|
||||||
/* package */ final class SeiReader {
|
/* package */ final class SeiReader {
|
||||||
|
|
||||||
private TrackOutput output;
|
private final List<Format> closedCaptionFormats;
|
||||||
|
private final TrackOutput[] outputs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param closedCaptionFormats A list of formats for the closed caption channels to expose.
|
||||||
|
*/
|
||||||
|
public SeiReader(List<Format> closedCaptionFormats) {
|
||||||
|
this.closedCaptionFormats = closedCaptionFormats;
|
||||||
|
outputs = new TrackOutput[closedCaptionFormats.size()];
|
||||||
|
}
|
||||||
|
|
||||||
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||||
idGenerator.generateNewId();
|
for (int i = 0; i < outputs.length; i++) {
|
||||||
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
|
idGenerator.generateNewId();
|
||||||
output.format(Format.createTextSampleFormat(idGenerator.getFormatId(),
|
TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
|
||||||
MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null));
|
Format channelFormat = closedCaptionFormats.get(i);
|
||||||
|
String channelMimeType = channelFormat.sampleMimeType;
|
||||||
|
Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType)
|
||||||
|
|| MimeTypes.APPLICATION_CEA708.equals(channelMimeType),
|
||||||
|
"Invalid closed caption mime type provided: " + channelMimeType);
|
||||||
|
output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), channelMimeType, null,
|
||||||
|
Format.NO_VALUE, channelFormat.selectionFlags, channelFormat.language,
|
||||||
|
channelFormat.accessibilityChannel, null));
|
||||||
|
outputs[i] = output;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void consume(long pesTimeUs, ParsableByteArray seiBuffer) {
|
public void consume(long pesTimeUs, ParsableByteArray seiBuffer) {
|
||||||
CeaUtil.consume(pesTimeUs, seiBuffer, output);
|
CeaUtil.consume(pesTimeUs, seiBuffer, outputs);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -35,14 +35,14 @@ public final class CeaUtil {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Consumes the unescaped content of an SEI NAL unit, writing the content of any CEA-608 messages
|
* Consumes the unescaped content of an SEI NAL unit, writing the content of any CEA-608 messages
|
||||||
* as samples to the provided output.
|
* as samples to all of the provided outputs.
|
||||||
*
|
*
|
||||||
* @param presentationTimeUs The presentation time in microseconds for any samples.
|
* @param presentationTimeUs The presentation time in microseconds for any samples.
|
||||||
* @param seiBuffer The unescaped SEI NAL unit data, excluding the NAL unit start code and type.
|
* @param seiBuffer The unescaped SEI NAL unit data, excluding the NAL unit start code and type.
|
||||||
* @param output The output to which any samples should be written.
|
* @param outputs The outputs to which any samples should be written.
|
||||||
*/
|
*/
|
||||||
public static void consume(long presentationTimeUs, ParsableByteArray seiBuffer,
|
public static void consume(long presentationTimeUs, ParsableByteArray seiBuffer,
|
||||||
TrackOutput output) {
|
TrackOutput[] outputs) {
|
||||||
while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {
|
while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {
|
||||||
int payloadType = readNon255TerminatedValue(seiBuffer);
|
int payloadType = readNon255TerminatedValue(seiBuffer);
|
||||||
int payloadSize = readNon255TerminatedValue(seiBuffer);
|
int payloadSize = readNon255TerminatedValue(seiBuffer);
|
||||||
@ -62,8 +62,12 @@ public final class CeaUtil {
|
|||||||
// Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2)
|
// Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2)
|
||||||
// + cc_data_1 (8) + cc_data_2 (8).
|
// + cc_data_1 (8) + cc_data_2 (8).
|
||||||
int sampleLength = ccCount * 3;
|
int sampleLength = ccCount * 3;
|
||||||
output.sampleData(seiBuffer, sampleLength);
|
int sampleStartPosition = seiBuffer.getPosition();
|
||||||
output.sampleMetadata(presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null);
|
for (TrackOutput output : outputs) {
|
||||||
|
seiBuffer.setPosition(sampleStartPosition);
|
||||||
|
output.sampleData(seiBuffer, sampleLength);
|
||||||
|
output.sampleMetadata(presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null);
|
||||||
|
}
|
||||||
// Ignore trailing information in SEI, if any.
|
// Ignore trailing information in SEI, if any.
|
||||||
seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3));
|
seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3));
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user