diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 92e3654a5b1..ea9ec4bdf5a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -117,6 +117,7 @@ public static final int TYPE_tx3g = Util.getIntegerCodeForString("tx3g"); public static final int TYPE_wvtt = Util.getIntegerCodeForString("wvtt"); public static final int TYPE_stpp = Util.getIntegerCodeForString("stpp"); + public static final int TYPE_c608 = Util.getIntegerCodeForString("c608"); public static final int TYPE_samr = Util.getIntegerCodeForString("samr"); public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb"); public static final int TYPE_udta = Util.getIntegerCodeForString("udta"); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 1d7b71dc37b..08ea8f5b865 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -44,6 +44,7 @@ private static final int TYPE_text = Util.getIntegerCodeForString("text"); private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl"); private static final int TYPE_subt = Util.getIntegerCodeForString("subt"); + private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); /** * Parses a trak atom (defined in 14496-12). @@ -84,8 +85,8 @@ public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); return stsdData.format == null ? null : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs, - stsdData.format, stsdData.trackEncryptionBoxes, stsdData.nalUnitLengthFieldLength, - edtsData.first, edtsData.second); + stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, + stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); } /** @@ -544,7 +545,8 @@ private static int parseHdlr(ParsableByteArray hdlr) { return C.TRACK_TYPE_AUDIO; } else if (trackType == TYPE_vide) { return C.TRACK_TYPE_VIDEO; - } else if (trackType == TYPE_text || trackType == TYPE_sbtl || trackType == TYPE_subt) { + } else if (trackType == TYPE_text || trackType == TYPE_sbtl || trackType == TYPE_subt + || trackType == TYPE_clcp) { return C.TRACK_TYPE_TEXT; } else { return C.TRACK_TYPE_UNKNOWN; @@ -621,6 +623,10 @@ private static StsdData parseStsd(ParsableByteArray stsd, int trackId, int rotat out.format = Format.createTextSampleFormat(Integer.toString(trackId), MimeTypes.APPLICATION_TTML, null, Format.NO_VALUE, 0, language, drmInitData, 0 /* subsample timing is absolute */); + } else if (childAtomType == Atom.TYPE_c608) { + out.format = Format.createTextSampleFormat(Integer.toString(trackId), + MimeTypes.APPLICATION_EIA608, null, Format.NO_VALUE, 0, language, drmInitData); + out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT; } stsd.setPosition(childStartPosition + childAtomSize); } @@ -1173,10 +1179,12 @@ private static final class StsdData { public Format format; public int nalUnitLengthFieldLength; + public int requiredSampleTransformation; public StsdData(int numberOfEntries) { trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries]; nalUnitLengthFieldLength = -1; + requiredSampleTransformation = Track.TRANSFORMATION_NONE; } } 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 9f62393ea58..9a1125b57d8 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 @@ -913,6 +913,10 @@ private boolean readSample(ExtractorInput input) throws IOException, Interrupted } else { sampleBytesWritten = 0; } + if (currentTrackBundle.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { + sampleSize -= Atom.HEADER_SIZE; + input.skipFully(Atom.HEADER_SIZE); + } parserState = STATE_READING_SAMPLE_CONTINUE; sampleCurrentNalBytesRemaining = 0; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index d66864da943..1386d166396 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -387,13 +387,19 @@ private int readSample(ExtractorInput input, PositionHolder positionHolder) TrackOutput trackOutput = track.trackOutput; int sampleIndex = track.sampleIndex; long position = track.sampleTable.offsets[sampleIndex]; + int sampleSize = track.sampleTable.sizes[sampleIndex]; + if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { + // The sample information is contained in a cdat atom. The header must be discarded for + // committing. + position += Atom.HEADER_SIZE; + sampleSize -= Atom.HEADER_SIZE; + } long skipAmount = position - input.getPosition() + sampleBytesWritten; if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) { positionHolder.position = position; return RESULT_SEEK; } input.skipFully((int) skipAmount); - int sampleSize = track.sampleTable.sizes[sampleIndex]; if (track.track.nalUnitLengthFieldLength != -1) { // Zero the top three bytes of the array that we'll use to parse nal unit lengths, in case // they're only 1 or 2 bytes long. diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java index 4c5ba5b642a..c77e2ce4ec1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -23,6 +23,15 @@ */ public final class Track { + /** + * A no-op sample transformation. + */ + public static final int TRANSFORMATION_NONE = 0; + /** + * A transformation for caption samples in cdat atoms. + */ + public static final int TRANSFORMATION_CEA608_CDAT = 1; + /** * The track identifier. */ @@ -53,6 +62,12 @@ public final class Track { */ public final Format format; + /** + * One of {@code TRANSFORMATION_*}. Defines the transformation to apply before outputting each + * sample. + */ + public final int sampleTransformation; + /** * Track encryption boxes for the different track sample descriptions. Entries may be null. */ @@ -75,14 +90,16 @@ public final class Track { public final int nalUnitLengthFieldLength; public Track(int id, int type, long timescale, long movieTimescale, long durationUs, - Format format, TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, - int nalUnitLengthFieldLength, long[] editListDurations, long[] editListMediaTimes) { + Format format, int sampleTransformation, + TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength, + long[] editListDurations, long[] editListMediaTimes) { this.id = id; this.type = type; this.timescale = timescale; this.movieTimescale = movieTimescale; this.durationUs = durationUs; this.format = format; + this.sampleTransformation = sampleTransformation; this.sampleDescriptionEncryptionBoxes = sampleDescriptionEncryptionBoxes; this.nalUnitLengthFieldLength = nalUnitLengthFieldLength; this.editListDurations = editListDurations; 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 02ac0837ecd..0df83d4f430 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 @@ -52,8 +52,26 @@ public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { } while (b == 0xFF); // Process the payload. if (Eia608Parser.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) { - output.sampleData(seiBuffer, payloadSize); - output.sampleMetadata(pesTimeUs, C.BUFFER_FLAG_KEY_FRAME, payloadSize, 0, null); + // Ignore country_code (1) + provider_code (2) + user_identifier (4) + // + user_data_type_code (1). + seiBuffer.skipBytes(8); + // Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1). + int ccCount = seiBuffer.readUnsignedByte() & 0x1F; + seiBuffer.skipBytes(1); + int sampleBytes = 0; + for (int i = 0; i < ccCount; i++) { + int ccValidityAndType = seiBuffer.readUnsignedByte() & 0x07; + // Check that validity == 1 and type == 0. + if (ccValidityAndType != 0x04) { + seiBuffer.skipBytes(2); + } else { + sampleBytes += 2; + output.sampleData(seiBuffer, 2); + } + } + output.sampleMetadata(pesTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytes, 0, null); + // Ignore trailing information in SEI, if any. + seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3)); } else { seiBuffer.skipBytes(payloadSize); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SmoothStreamingChunkSource.java index 2701ba3db46..2f8a1c07f9b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SmoothStreamingChunkSource.java @@ -90,8 +90,8 @@ public SmoothStreamingChunkSource(Loader manifestLoader, SmoothStreamingManifest for (int j = 0; j < formats.length; j++) { int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : -1; Track track = new Track(j, streamElement.type, streamElement.timescale, C.UNSET_TIME_US, - manifest.durationUs, formats[j], trackEncryptionBoxes, nalUnitLengthFieldLength, - null, null); + manifest.durationUs, formats[j], Track.TRANSFORMATION_NONE, trackEncryptionBoxes, + nalUnitLengthFieldLength, null, null); FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track); diff --git a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Parser.java b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Parser.java index f8220093875..0d77b9c2335 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Parser.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/eia608/Eia608Parser.java @@ -21,12 +21,10 @@ import com.google.android.exoplayer2.text.SubtitleOutputBuffer; import com.google.android.exoplayer2.text.SubtitleParser; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import android.text.TextUtils; -import java.nio.ByteBuffer; import java.util.LinkedList; import java.util.TreeSet; @@ -168,7 +166,7 @@ public final class Eia608Parser implements SubtitleParser { private final LinkedList availableOutputBuffers; private final TreeSet queuedInputBuffers; - private final ParsableBitArray seiBuffer; + private final ParsableByteArray ccData; private final StringBuilder captionStringBuilder; @@ -197,7 +195,7 @@ public Eia608Parser() { } queuedInputBuffers = new TreeSet<>(); - seiBuffer = new ParsableBitArray(); + ccData = new ParsableByteArray(); captionStringBuilder = new StringBuilder(); @@ -310,58 +308,30 @@ public void release() { } private void decode(SubtitleInputBuffer inputBuffer) { - ByteBuffer inputData = inputBuffer.data; - int inputSize = inputData.limit(); - if (inputSize < 10) { - return; - } - - seiBuffer.reset(inputData.array(), inputSize); - // country_code (8) + provider_code (16) + user_identifier (32) + user_data_type_code (8) + - // reserved (1) + process_cc_data_flag (1) + zero_bit (1) - seiBuffer.skipBits(67); - int ccCount = seiBuffer.readBits(5); - seiBuffer.skipBits(8); - + ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit()); boolean captionDataProcessed = false; boolean isRepeatableControl = false; - for (int i = 0; i < ccCount; i++) { - seiBuffer.skipBits(5); // one_bit + reserved - boolean ccValid = seiBuffer.readBit(); - if (!ccValid) { - seiBuffer.skipBits(18); - continue; - } - int ccType = seiBuffer.readBits(2); - if (ccType != 0) { - seiBuffer.skipBits(16); - continue; - } - seiBuffer.skipBits(1); - byte ccData1 = (byte) seiBuffer.readBits(7); - seiBuffer.skipBits(1); - byte ccData2 = (byte) seiBuffer.readBits(7); + while (ccData.bytesLeft() > 0) { + byte ccData1 = (byte) (ccData.readUnsignedByte() & 0x7F); + byte ccData2 = (byte) (ccData.readUnsignedByte() & 0x7F); // Ignore empty captions. if (ccData1 == 0 && ccData2 == 0) { continue; } - // If we've reached this point then there is data to process; flag that work has been done. captionDataProcessed = true; // Special North American character set. // ccData2 - P|0|1|1|X|X|X|X - if ((ccData1 == 0x11 || ccData1 == 0x19) - && ((ccData2 & 0x70) == 0x30)) { + if ((ccData1 == 0x11 || ccData1 == 0x19) && ((ccData2 & 0x70) == 0x30)) { captionStringBuilder.append(getSpecialChar(ccData2)); continue; } // Extended Spanish/Miscellaneous and French character set. // ccData2 - P|0|1|X|X|X|X|X - if ((ccData1 == 0x12 || ccData1 == 0x1A) - && ((ccData2 & 0x60) == 0x20)) { + if ((ccData1 == 0x12 || ccData1 == 0x1A) && ((ccData2 & 0x60) == 0x20)) { backspace(); // Remove standard equivalent of the special extended char. captionStringBuilder.append(getExtendedEsFrChar(ccData2)); continue; @@ -369,8 +339,7 @@ private void decode(SubtitleInputBuffer inputBuffer) { // Extended Portuguese and German/Danish character set. // ccData2 - P|0|1|X|X|X|X|X - if ((ccData1 == 0x13 || ccData1 == 0x1B) - && ((ccData2 & 0x60) == 0x20)) { + if ((ccData1 == 0x13 || ccData1 == 0x1B) && ((ccData2 & 0x60) == 0x20)) { backspace(); // Remove standard equivalent of the special extended char. captionStringBuilder.append(getExtendedPtDeChar(ccData2)); continue;