Support CEA 608/708 captions in MPEG2 TS stream

[Problem] Sarnoff spec complaince tests streams are MPEG2 TS.
Currently, parsing CC data from MPEG2 TS is not supported

[Solution] Parsed CC data from MPEG2 stream because all Sarnoff streams are MPEG2
parent 18d208ab
...@@ -80,6 +80,12 @@ public final class C { ...@@ -80,6 +80,12 @@ public final class C {
/** The number of bits per byte. */ /** The number of bits per byte. */
public static final int BITS_PER_BYTE = 8; public static final int BITS_PER_BYTE = 8;
/** non-Wide aspect ratio */
public static final int NON_WIDE_ASPECT_RATIO_TYPE = 1;
/** Wide aspect ratio */
public static final int WIDE_ASPECT_RATIO_TYPE = 2;
/** /**
* The name of the ASCII charset. * The name of the ASCII charset.
*/ */
......
...@@ -15,8 +15,11 @@ ...@@ -15,8 +15,11 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import android.os.Bundle;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; 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 com.google.android.exoplayer2.util.MimeTypes;
...@@ -79,6 +82,13 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact ...@@ -79,6 +82,13 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
*/ */
public DefaultTsPayloadReaderFactory(@Flags int flags, List<Format> closedCaptionFormats) { public DefaultTsPayloadReaderFactory(@Flags int flags, List<Format> closedCaptionFormats) {
this.flags = flags; this.flags = flags;
if (!isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS) && closedCaptionFormats.isEmpty()) {
closedCaptionFormats = new ArrayList();
closedCaptionFormats.add(Format.createTextSampleFormat(null,
MimeTypes.APPLICATION_CEA608, 0, null));
closedCaptionFormats.add(Format.createTextSampleFormat(null,
MimeTypes.APPLICATION_CEA708, 0, null));
}
this.closedCaptionFormats = closedCaptionFormats; this.closedCaptionFormats = closedCaptionFormats;
} }
...@@ -106,7 +116,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact ...@@ -106,7 +116,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
case TsExtractor.TS_STREAM_TYPE_HDMV_DTS: case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
return new PesReader(new DtsReader(esInfo.language)); return new PesReader(new DtsReader(esInfo.language));
case TsExtractor.TS_STREAM_TYPE_H262: case TsExtractor.TS_STREAM_TYPE_H262:
return new PesReader(new H262Reader()); return new PesReader(new H262Reader(buildUserDataReader(esInfo)));
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(buildSeiReader(esInfo), : new PesReader(new H264Reader(buildSeiReader(esInfo),
...@@ -136,8 +146,34 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact ...@@ -136,8 +146,34 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
* @return A {@link SeiReader} for closed caption tracks. * @return A {@link SeiReader} for closed caption tracks.
*/ */
private SeiReader buildSeiReader(EsInfo esInfo) { private SeiReader buildSeiReader(EsInfo esInfo) {
return new SeiReader(getCCformats(esInfo));
}
/**
* If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link UserDataReader} for
* {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a
* {@link UserDataReader} for the declared formats, or {@link #closedCaptionFormats} if the descriptor
* is not present.
*
* @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}.
* @return A {@link UserDataReader} for closed caption tracks.
*/
private UserDataReader buildUserDataReader(EsInfo esInfo) {
return new UserDataReader(getCCformats(esInfo));
}
/**
* If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link List<Format>} of
* {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a
* {@link List<Format>} for the declared formats, or {@link #closedCaptionFormats} if the descriptor
* is not present.
*
* @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}.
* @return A {@link List<Format>} containing list of closed caption formats.
*/
private List<Format> getCCformats(EsInfo esInfo) {
if (isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS)) { if (isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS)) {
return new SeiReader(closedCaptionFormats); return closedCaptionFormats;
} }
ParsableByteArray scratchDescriptorData = new ParsableByteArray(esInfo.descriptorBytes); ParsableByteArray scratchDescriptorData = new ParsableByteArray(esInfo.descriptorBytes);
List<Format> closedCaptionFormats = this.closedCaptionFormats; List<Format> closedCaptionFormats = this.closedCaptionFormats;
...@@ -162,17 +198,24 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact ...@@ -162,17 +198,24 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
mimeType = MimeTypes.APPLICATION_CEA608; mimeType = MimeTypes.APPLICATION_CEA608;
accessibilityChannel = 1; accessibilityChannel = 1;
} }
closedCaptionFormats.add(Format.createTextSampleFormat(null, mimeType, null,
Format.NO_VALUE, 0, language, accessibilityChannel, null));
// Skip easy_reader(1), wide_aspect_ratio(1), reserved(14). // Skip easy_reader(1), wide_aspect_ratio(1), reserved(14).
scratchDescriptorData.skipBytes(2); byte misc = (byte)scratchDescriptorData.readUnsignedByte();
boolean isWideAspectRatio = ((misc & 0x60) == 0x60);
Bundle params = new Bundle();
params.putInt(Format.KEY_ASPECT_RATIO_TYPE,
isWideAspectRatio ? C.WIDE_ASPECT_RATIO_TYPE: C.NON_WIDE_ASPECT_RATIO_TYPE);
closedCaptionFormats.add(Format.createTextSampleFormat(null, mimeType, null,
Format.NO_VALUE, 0, language, accessibilityChannel, null,
params));
scratchDescriptorData.skipBytes(1);
} }
} else { } else {
// Unknown descriptor. Ignore. // Unknown descriptor. Ignore.
} }
scratchDescriptorData.setPosition(nextDescriptorPosition); scratchDescriptorData.setPosition(nextDescriptorPosition);
} }
return new SeiReader(closedCaptionFormats);
return closedCaptionFormats;
} }
private boolean isSet(@Flags int flag) { private boolean isSet(@Flags int flag) {
......
...@@ -36,6 +36,7 @@ public final class H262Reader implements ElementaryStreamReader { ...@@ -36,6 +36,7 @@ public final class H262Reader implements ElementaryStreamReader {
private static final int START_SEQUENCE_HEADER = 0xB3; private static final int START_SEQUENCE_HEADER = 0xB3;
private static final int START_EXTENSION = 0xB5; private static final int START_EXTENSION = 0xB5;
private static final int START_GROUP = 0xB8; private static final int START_GROUP = 0xB8;
private static final int START_USER_DATA = 0xB2;
private String formatId; private String formatId;
private TrackOutput output; private TrackOutput output;
...@@ -62,16 +63,30 @@ public final class H262Reader implements ElementaryStreamReader { ...@@ -62,16 +63,30 @@ public final class H262Reader implements ElementaryStreamReader {
private long sampleTimeUs; private long sampleTimeUs;
private boolean sampleIsKeyframe; private boolean sampleIsKeyframe;
private boolean sampleHasPicture; private boolean sampleHasPicture;
private NalUnitTargetBuffer userData = null;
private UserDataReader userDataReader = null;
// Scratch variables to avoid allocations.
private ParsableByteArray userDataParsable = null;
public H262Reader() { public H262Reader() {
this(null);
}
public H262Reader(UserDataReader userDataReader) {
this.userDataReader = userDataReader;
prefixFlags = new boolean[4]; prefixFlags = new boolean[4];
csdBuffer = new CsdBuffer(128); csdBuffer = new CsdBuffer(128);
if (userDataReader != null) {
userData = new NalUnitTargetBuffer(START_USER_DATA, 128);
userDataParsable = new ParsableByteArray();
}
} }
@Override @Override
public void seek() { public void seek() {
NalUnitUtil.clearPrefixFlags(prefixFlags); NalUnitUtil.clearPrefixFlags(prefixFlags);
csdBuffer.reset(); csdBuffer.reset();
if (userData != null) {
userData.reset();
}
totalBytesWritten = 0; totalBytesWritten = 0;
startedFirstSample = false; startedFirstSample = false;
} }
...@@ -81,6 +96,9 @@ public final class H262Reader implements ElementaryStreamReader { ...@@ -81,6 +96,9 @@ public final class H262Reader implements ElementaryStreamReader {
idGenerator.generateNewId(); idGenerator.generateNewId();
formatId = idGenerator.getFormatId(); formatId = idGenerator.getFormatId();
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);
if (userDataReader != null) {
userDataReader.createTracks(extractorOutput, idGenerator);
}
} }
@Override @Override
...@@ -106,6 +124,9 @@ public final class H262Reader implements ElementaryStreamReader { ...@@ -106,6 +124,9 @@ public final class H262Reader implements ElementaryStreamReader {
if (!hasOutputFormat) { if (!hasOutputFormat) {
csdBuffer.onData(dataArray, offset, limit); csdBuffer.onData(dataArray, offset, limit);
} }
if (userData != null) {
userData.appendToNalUnit(dataArray, offset, limit);
}
return; return;
} }
...@@ -130,7 +151,25 @@ public final class H262Reader implements ElementaryStreamReader { ...@@ -130,7 +151,25 @@ public final class H262Reader implements ElementaryStreamReader {
hasOutputFormat = true; hasOutputFormat = true;
} }
} }
if (userDataReader != null && userData != null) {
int lengthToStartCode = startCodeOffset - offset;
int bytesAlreadyPassed = 0;
if (lengthToStartCode > 0) {
userData.appendToNalUnit(dataArray, offset, startCodeOffset);
} else {
bytesAlreadyPassed = -lengthToStartCode;
}
if (userData.endNalUnit(bytesAlreadyPassed)) {
int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength);
userDataParsable.reset(userData.nalData, unescapedLength);
userDataReader.consume(sampleTimeUs, userDataParsable);
}
if (startCodeValue == START_USER_DATA && data.data[startCodeOffset + 2] == 0x1) {
userData.startNalUnit(startCodeValue);
}
}
if (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER) { if (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER) {
int bytesWrittenPastStartCode = limit - startCodeOffset; int bytesWrittenPastStartCode = limit - startCodeOffset;
if (startedFirstSample && sampleHasPicture && hasOutputFormat) { if (startedFirstSample && sampleHasPicture && hasOutputFormat) {
......
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
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 user data structure, outputting contained CEA-608/708 messages to a {@link TrackOutput}.
*/
/* package */ final class UserDataReader {
private final List<Format> closedCaptionFormats;
private final TrackOutput[] outputs;
private final int USER_DATA_START_CODE = 0x0001B2;
private final int USER_DATA_IDENTIFIER_GA94 = 0x47413934;
private final int USER_DATA_TYPE_CODE_MPEG_CC = 0x03;
public UserDataReader(List<Format> closedCaptionFormats) {
this.closedCaptionFormats = closedCaptionFormats;
outputs = new TrackOutput[closedCaptionFormats.size()];
}
public void createTracks(ExtractorOutput extractorOutput,
TsPayloadReader.TrackIdGenerator idGenerator) {
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, channelFormat.params));
outputs[i] = output;
}
}
public void consume(long pesTimeUs, ParsableByteArray userDataPayload) {
if (userDataPayload.bytesLeft() < 9) {
return;
}
//check if payload is used_data_type (0x01B2)
int userDataStartCode = userDataPayload.readInt();
int userDataIdentifier = userDataPayload.readInt();
int userDataTypeCode = userDataPayload.readUnsignedByte();
if (userDataStartCode == USER_DATA_START_CODE && userDataIdentifier == USER_DATA_IDENTIFIER_GA94
&& userDataTypeCode == USER_DATA_TYPE_CODE_MPEG_CC) {
if (userDataPayload.bytesLeft() < 2) {
return;
}
// read cc_count and process_cc_data_flag byte.
int ccByte = userDataPayload.readUnsignedByte();
boolean processCCDataFlag = ((ccByte & 0x40) != 0);
int ccCount = (ccByte & 0x1F);
// skip reserved em_data byte of MPEG_CC structure
userDataPayload.skipBytes(1);
int payLoadSize = ccCount * 3;
if (processCCDataFlag && payLoadSize != 0) {
int ccPos = userDataPayload.getPosition();
for (TrackOutput output : outputs) {
output.sampleData(userDataPayload, payLoadSize);
output.sampleMetadata(pesTimeUs, C.BUFFER_FLAG_KEY_FRAME, payLoadSize, 0, null);
userDataPayload.setPosition(ccPos);
}
}
}
}
}
...@@ -2,7 +2,7 @@ seekMap: ...@@ -2,7 +2,7 @@ seekMap:
isSeekable = false isSeekable = false
duration = 66733 duration = 66733
getPosition(0) = [[timeUs=0, position=0]] getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 2 numberOfTracks = 3
track 256: track 256:
format: format:
bitrate = -1 bitrate = -1
...@@ -76,4 +76,28 @@ track 257: ...@@ -76,4 +76,28 @@ track 257:
time = 100822 time = 100822
flags = 1 flags = 1
data = length 1254, hash 73FB07B8 data = length 1254, hash 73FB07B8
track 8448:
format:
bitrate = -1
id = 1/8448
containerMimeType = null
sampleMimeType = application/cea-608
maxInputSize = -1
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = -1
sampleRate = -1
pcmEncoding = -1
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
total output bytes = 0
sample count = 0
tracksEnded = true tracksEnded = true
...@@ -2,7 +2,7 @@ seekMap: ...@@ -2,7 +2,7 @@ seekMap:
isSeekable = false isSeekable = false
duration = UNSET TIME duration = UNSET TIME
getPosition(0) = [[timeUs=0, position=0]] getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 2 numberOfTracks = 3
track 256: track 256:
format: format:
bitrate = -1 bitrate = -1
...@@ -76,4 +76,28 @@ track 257: ...@@ -76,4 +76,28 @@ track 257:
time = 100822 time = 100822
flags = 1 flags = 1
data = length 1254, hash 73FB07B8 data = length 1254, hash 73FB07B8
track 8448:
format:
bitrate = -1
id = 1/8448
containerMimeType = null
sampleMimeType = application/cea-608
maxInputSize = -1
width = -1
height = -1
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = -1
sampleRate = -1
pcmEncoding = -1
encoderDelay = 0
encoderPadding = 0
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
total output bytes = 0
sample count = 0
tracksEnded = true tracksEnded = true
...@@ -20,7 +20,9 @@ import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4; ...@@ -20,7 +20,9 @@ import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4;
import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_WEBM; import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_WEBM;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.os.Bundle;
import android.os.Parcel; import android.os.Parcel;
import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
...@@ -63,11 +65,12 @@ public final class FormatTest { ...@@ -63,11 +65,12 @@ public final class FormatTest {
new TextInformationFrame("id2", "description2", "value2")); new TextInformationFrame("id2", "description2", "value2"));
ColorInfo colorInfo = new ColorInfo(C.COLOR_SPACE_BT709, ColorInfo colorInfo = new ColorInfo(C.COLOR_SPACE_BT709,
C.COLOR_RANGE_LIMITED, C.COLOR_TRANSFER_SDR, new byte[] {1, 2, 3, 4, 5, 6, 7}); C.COLOR_RANGE_LIMITED, C.COLOR_TRANSFER_SDR, new byte[] {1, 2, 3, 4, 5, 6, 7});
Bundle params = new Bundle();
params.putInt(Format.KEY_ASPECT_RATIO_TYPE, 100);
Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null,
1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, colorInfo, 6, 1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, colorInfo, 6,
44100, C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.NO_VALUE, 44100, C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.NO_VALUE,
Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA, drmInitData, metadata); Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA, drmInitData, metadata, params);
Parcel parcel = Parcel.obtain(); Parcel parcel = Parcel.obtain();
formatToParcel.writeToParcel(parcel, 0); formatToParcel.writeToParcel(parcel, 0);
...@@ -75,6 +78,8 @@ public final class FormatTest { ...@@ -75,6 +78,8 @@ public final class FormatTest {
Format formatFromParcel = Format.CREATOR.createFromParcel(parcel); Format formatFromParcel = Format.CREATOR.createFromParcel(parcel);
assertThat(formatFromParcel).isEqualTo(formatToParcel); assertThat(formatFromParcel).isEqualTo(formatToParcel);
int aspectRatio = formatFromParcel.params.getInt(Format.KEY_ASPECT_RATIO_TYPE);
assertThat(aspectRatio).isEqualTo(100);
parcel.recycle(); parcel.recycle();
} }
......
...@@ -49,7 +49,7 @@ public class DefaultTrackNameProvider implements TrackNameProvider { ...@@ -49,7 +49,7 @@ public class DefaultTrackNameProvider implements TrackNameProvider {
} else { } else {
trackName = buildLanguageString(format); trackName = buildLanguageString(format);
} }
return trackName.length() == 0 ? resources.getString(R.string.exo_track_unknown) : trackName; return trackName.length() == 0 ? resources.getString(R.string.exo_track_unknown) : trackName + " - " + format.id;
} }
private String buildResolutionString(Format format) { private String buildResolutionString(Format format) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment