diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 6a5662ce10..a228a9b775 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -157,7 +157,7 @@ public final class FragmentedMp4Extractor implements Extractor { // Extractor output. private ExtractorOutput extractorOutput; private TrackOutput eventMessageTrackOutput; - private TrackOutput cea608TrackOutput; + private TrackOutput[] cea608TrackOutputs; // Whether extractorOutput.seekMap has been called. private boolean haveOutputSeekMap; @@ -459,10 +459,12 @@ public final class FragmentedMp4Extractor implements Extractor { eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE)); } - if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutput == null) { - cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, C.TRACK_TYPE_TEXT); + if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) { + TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, + C.TRACK_TYPE_TEXT); cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 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); // Write the NAL unit type byte. output.sampleData(nalPrefix, 1); - processSeiNalUnitPayload = cea608TrackOutput != null + processSeiNalUnitPayload = cea608TrackOutputs != null && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); sampleBytesWritten += 5; sampleSize += nalUnitLengthFieldLengthDiff; @@ -1103,7 +1105,7 @@ public final class FragmentedMp4Extractor implements Extractor { nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0); nalBuffer.setLimit(unescapedLength); CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer, - cea608TrackOutput); + cea608TrackOutputs); } else { // Write the payload of the NAL unit. writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 4e94891889..21050d2bbb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -17,9 +17,13 @@ package com.google.android.exoplayer2.extractor.ts; import android.support.annotation.IntDef; import android.util.SparseArray; +import com.google.android.exoplayer2.Format; 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.RetentionPolicy; +import java.util.Collections; +import java.util.List; /** * Default implementation for {@link TsPayloadReader.Factory}. @@ -35,19 +39,36 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact public @interface Flags { } 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_H264_STREAM = 4; - public static final int FLAG_DETECT_ACCESS_UNITS = 8; - public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 16; + public static final int FLAG_IGNORE_AAC_STREAM = 1 << 1; + public static final int FLAG_IGNORE_H264_STREAM = 1 << 2; + public static final int FLAG_DETECT_ACCESS_UNITS = 1 << 3; + public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 1 << 4; @Flags private final int flags; + private final List closedCaptionFormats; public DefaultTsPayloadReaderFactory() { this(0); } + /** + * @param flags A combination of {@code FLAG_*} values, which control the behavior of the created + * readers. + */ 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 closedCaptionFormats) { this.flags = flags; + this.closedCaptionFormats = closedCaptionFormats; } @Override @@ -74,10 +95,10 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact return new PesReader(new H262Reader()); case TsExtractor.TS_STREAM_TYPE_H264: 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))); 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: return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM) ? 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) { return (flags & flag) != 0; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index a3f4deffcb..1e5d480ea1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -21,25 +21,45 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; 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.ParsableByteArray; +import java.util.List; /** * Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. */ /* package */ final class SeiReader { - private TrackOutput output; + private final List closedCaptionFormats; + private final TrackOutput[] outputs; + + /** + * @param closedCaptionFormats A list of formats for the closed caption channels to expose. + */ + public SeiReader(List closedCaptionFormats) { + this.closedCaptionFormats = closedCaptionFormats; + outputs = new TrackOutput[closedCaptionFormats.size()]; + } public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - idGenerator.generateNewId(); - output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); - output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), - MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); + for (int i = 0; i < outputs.length; i++) { + idGenerator.generateNewId(); + TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); + 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) { - CeaUtil.consume(pesTimeUs, seiBuffer, output); + CeaUtil.consume(pesTimeUs, seiBuffer, outputs); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java index a39c8c8669..130c7461f9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java @@ -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 - * 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 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, - TrackOutput output) { + TrackOutput[] outputs) { while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { int payloadType = 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) // + cc_data_1 (8) + cc_data_2 (8). int sampleLength = ccCount * 3; - output.sampleData(seiBuffer, sampleLength); - output.sampleMetadata(presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null); + int sampleStartPosition = seiBuffer.getPosition(); + 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. seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3)); } else {