diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index d7123c8078..8a2f3196d0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -80,6 +80,12 @@ public final class C { /** The number of bits per byte. */ 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. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index f58ecd8da6..1f8f894020 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -15,8 +15,11 @@ */ package com.google.android.exoplayer2.extractor.ts; +import android.os.Bundle; import android.support.annotation.IntDef; import android.util.SparseArray; + +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.util.MimeTypes; @@ -79,6 +82,13 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact */ public DefaultTsPayloadReaderFactory(@Flags int flags, List closedCaptionFormats) { 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; } @@ -106,7 +116,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact case TsExtractor.TS_STREAM_TYPE_HDMV_DTS: return new PesReader(new DtsReader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_H262: - return new PesReader(new H262Reader()); + return new PesReader(new H262Reader(buildUserDataReader(esInfo))); case TsExtractor.TS_STREAM_TYPE_H264: return isSet(FLAG_IGNORE_H264_STREAM) ? null : new PesReader(new H264Reader(buildSeiReader(esInfo), @@ -136,8 +146,34 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact * @return A {@link SeiReader} for closed caption tracks. */ 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} of + * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a + * {@link List} 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} containing list of closed caption formats. + */ + private List getCCformats(EsInfo esInfo) { if (isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS)) { - return new SeiReader(closedCaptionFormats); + return closedCaptionFormats; } ParsableByteArray scratchDescriptorData = new ParsableByteArray(esInfo.descriptorBytes); List closedCaptionFormats = this.closedCaptionFormats; @@ -162,17 +198,24 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact mimeType = MimeTypes.APPLICATION_CEA608; 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). - 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 { // Unknown descriptor. Ignore. } scratchDescriptorData.setPosition(nextDescriptorPosition); } - return new SeiReader(closedCaptionFormats); + + return closedCaptionFormats; } private boolean isSet(@Flags int flag) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index a3502a3242..5d9c407d6b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -36,6 +36,7 @@ public final class H262Reader implements ElementaryStreamReader { private static final int START_SEQUENCE_HEADER = 0xB3; private static final int START_EXTENSION = 0xB5; private static final int START_GROUP = 0xB8; + private static final int START_USER_DATA = 0xB2; private String formatId; private TrackOutput output; @@ -62,16 +63,30 @@ public final class H262Reader implements ElementaryStreamReader { private long sampleTimeUs; private boolean sampleIsKeyframe; private boolean sampleHasPicture; - + private NalUnitTargetBuffer userData = null; + private UserDataReader userDataReader = null; + // Scratch variables to avoid allocations. + private ParsableByteArray userDataParsable = null; public H262Reader() { + this(null); + } + public H262Reader(UserDataReader userDataReader) { + this.userDataReader = userDataReader; prefixFlags = new boolean[4]; csdBuffer = new CsdBuffer(128); + if (userDataReader != null) { + userData = new NalUnitTargetBuffer(START_USER_DATA, 128); + userDataParsable = new ParsableByteArray(); + } } @Override public void seek() { NalUnitUtil.clearPrefixFlags(prefixFlags); csdBuffer.reset(); + if (userData != null) { + userData.reset(); + } totalBytesWritten = 0; startedFirstSample = false; } @@ -81,6 +96,9 @@ public final class H262Reader implements ElementaryStreamReader { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); + if (userDataReader != null) { + userDataReader.createTracks(extractorOutput, idGenerator); + } } @Override @@ -106,6 +124,9 @@ public final class H262Reader implements ElementaryStreamReader { if (!hasOutputFormat) { csdBuffer.onData(dataArray, offset, limit); } + if (userData != null) { + userData.appendToNalUnit(dataArray, offset, limit); + } return; } @@ -130,7 +151,25 @@ public final class H262Reader implements ElementaryStreamReader { 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) { int bytesWrittenPastStartCode = limit - startCodeOffset; if (startedFirstSample && sampleHasPicture && hasOutputFormat) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java new file mode 100644 index 0000000000..f21da7a998 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java @@ -0,0 +1,92 @@ +/* + * 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 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 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); + } + + } + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java new file mode 100644 index 0000000000..10bed14adc --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 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.text.cea; + +import java.util.Collections; +import java.util.List; + +/** Initialization data for CEA-708 decoders. */ +public final class Cea708InitializationData { + + /** + * Whether the closed caption service is formatted for displays with 16:9 aspect ratio. If false, + * the closed caption service is formatted for 4:3 displays. + */ + public final boolean isWideAspectRatio; + + private Cea708InitializationData(List initializationData) { + isWideAspectRatio = initializationData.get(0)[0] != 0; + } + + /** + * Returns an object representation of CEA-708 initialization data + * + * @param initializationData Binary CEA-708 initialization data. + * @return The object representation. + */ + public static Cea708InitializationData fromData(List initializationData) { + return new Cea708InitializationData(initializationData); + } + + /** + * Builds binary CEA-708 initialization data. + * + * @param isWideAspectRatio Whether the closed caption service is formatted for displays with 16:9 + * aspect ratio. + * @return Binary CEA-708 initializaton data. + */ + public static List buildData(boolean isWideAspectRatio) { + return Collections.singletonList(new byte[] {(byte) (isWideAspectRatio ? 1 : 0)}); + } +} diff --git a/library/core/src/test/assets/ts/sample.ts.0.dump b/library/core/src/test/assets/ts/sample.ts.0.dump index e42761ac7b..d7b17eff6a 100644 --- a/library/core/src/test/assets/ts/sample.ts.0.dump +++ b/library/core/src/test/assets/ts/sample.ts.0.dump @@ -2,7 +2,7 @@ seekMap: isSeekable = false duration = 66733 getPosition(0) = [[timeUs=0, position=0]] -numberOfTracks = 2 +numberOfTracks = 3 track 256: format: bitrate = -1 @@ -76,4 +76,28 @@ track 257: time = 100822 flags = 1 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 diff --git a/library/core/src/test/assets/ts/sample.ts.unklen.dump b/library/core/src/test/assets/ts/sample.ts.unklen.dump index a74268a702..56f6b01a9c 100644 --- a/library/core/src/test/assets/ts/sample.ts.unklen.dump +++ b/library/core/src/test/assets/ts/sample.ts.unklen.dump @@ -2,7 +2,7 @@ seekMap: isSeekable = false duration = UNSET TIME getPosition(0) = [[timeUs=0, position=0]] -numberOfTracks = 2 +numberOfTracks = 3 track 256: format: bitrate = -1 @@ -76,4 +76,28 @@ track 257: time = 100822 flags = 1 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 diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java index 5d68387869..c8e953d976 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java @@ -49,7 +49,7 @@ public class DefaultTrackNameProvider implements TrackNameProvider { } else { trackName = buildLabelString(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) {