Add support for CEA608 in MP4 and fMP4
Issue: #1658 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=127312721
This commit is contained in:
parent
e4a3483d6f
commit
a0c1595725
@ -117,6 +117,7 @@ import java.util.List;
|
|||||||
public static final int TYPE_tx3g = Util.getIntegerCodeForString("tx3g");
|
public static final int TYPE_tx3g = Util.getIntegerCodeForString("tx3g");
|
||||||
public static final int TYPE_wvtt = Util.getIntegerCodeForString("wvtt");
|
public static final int TYPE_wvtt = Util.getIntegerCodeForString("wvtt");
|
||||||
public static final int TYPE_stpp = Util.getIntegerCodeForString("stpp");
|
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_samr = Util.getIntegerCodeForString("samr");
|
||||||
public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb");
|
public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb");
|
||||||
public static final int TYPE_udta = Util.getIntegerCodeForString("udta");
|
public static final int TYPE_udta = Util.getIntegerCodeForString("udta");
|
||||||
|
@ -44,6 +44,7 @@ import java.util.List;
|
|||||||
private static final int TYPE_text = Util.getIntegerCodeForString("text");
|
private static final int TYPE_text = Util.getIntegerCodeForString("text");
|
||||||
private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl");
|
private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl");
|
||||||
private static final int TYPE_subt = Util.getIntegerCodeForString("subt");
|
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).
|
* Parses a trak atom (defined in 14496-12).
|
||||||
@ -84,8 +85,8 @@ import java.util.List;
|
|||||||
Pair<long[], long[]> edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts));
|
Pair<long[], long[]> edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts));
|
||||||
return stsdData.format == null ? null
|
return stsdData.format == null ? null
|
||||||
: new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs,
|
: new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs,
|
||||||
stsdData.format, stsdData.trackEncryptionBoxes, stsdData.nalUnitLengthFieldLength,
|
stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes,
|
||||||
edtsData.first, edtsData.second);
|
stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -544,7 +545,8 @@ import java.util.List;
|
|||||||
return C.TRACK_TYPE_AUDIO;
|
return C.TRACK_TYPE_AUDIO;
|
||||||
} else if (trackType == TYPE_vide) {
|
} else if (trackType == TYPE_vide) {
|
||||||
return C.TRACK_TYPE_VIDEO;
|
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;
|
return C.TRACK_TYPE_TEXT;
|
||||||
} else {
|
} else {
|
||||||
return C.TRACK_TYPE_UNKNOWN;
|
return C.TRACK_TYPE_UNKNOWN;
|
||||||
@ -621,6 +623,10 @@ import java.util.List;
|
|||||||
out.format = Format.createTextSampleFormat(Integer.toString(trackId),
|
out.format = Format.createTextSampleFormat(Integer.toString(trackId),
|
||||||
MimeTypes.APPLICATION_TTML, null, Format.NO_VALUE, 0, language, drmInitData,
|
MimeTypes.APPLICATION_TTML, null, Format.NO_VALUE, 0, language, drmInitData,
|
||||||
0 /* subsample timing is absolute */);
|
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);
|
stsd.setPosition(childStartPosition + childAtomSize);
|
||||||
}
|
}
|
||||||
@ -1173,10 +1179,12 @@ import java.util.List;
|
|||||||
|
|
||||||
public Format format;
|
public Format format;
|
||||||
public int nalUnitLengthFieldLength;
|
public int nalUnitLengthFieldLength;
|
||||||
|
public int requiredSampleTransformation;
|
||||||
|
|
||||||
public StsdData(int numberOfEntries) {
|
public StsdData(int numberOfEntries) {
|
||||||
trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries];
|
trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries];
|
||||||
nalUnitLengthFieldLength = -1;
|
nalUnitLengthFieldLength = -1;
|
||||||
|
requiredSampleTransformation = Track.TRANSFORMATION_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -913,6 +913,10 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||||||
} else {
|
} else {
|
||||||
sampleBytesWritten = 0;
|
sampleBytesWritten = 0;
|
||||||
}
|
}
|
||||||
|
if (currentTrackBundle.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) {
|
||||||
|
sampleSize -= Atom.HEADER_SIZE;
|
||||||
|
input.skipFully(Atom.HEADER_SIZE);
|
||||||
|
}
|
||||||
parserState = STATE_READING_SAMPLE_CONTINUE;
|
parserState = STATE_READING_SAMPLE_CONTINUE;
|
||||||
sampleCurrentNalBytesRemaining = 0;
|
sampleCurrentNalBytesRemaining = 0;
|
||||||
}
|
}
|
||||||
|
@ -387,13 +387,19 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||||||
TrackOutput trackOutput = track.trackOutput;
|
TrackOutput trackOutput = track.trackOutput;
|
||||||
int sampleIndex = track.sampleIndex;
|
int sampleIndex = track.sampleIndex;
|
||||||
long position = track.sampleTable.offsets[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;
|
long skipAmount = position - input.getPosition() + sampleBytesWritten;
|
||||||
if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {
|
if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {
|
||||||
positionHolder.position = position;
|
positionHolder.position = position;
|
||||||
return RESULT_SEEK;
|
return RESULT_SEEK;
|
||||||
}
|
}
|
||||||
input.skipFully((int) skipAmount);
|
input.skipFully((int) skipAmount);
|
||||||
int sampleSize = track.sampleTable.sizes[sampleIndex];
|
|
||||||
if (track.track.nalUnitLengthFieldLength != -1) {
|
if (track.track.nalUnitLengthFieldLength != -1) {
|
||||||
// Zero the top three bytes of the array that we'll use to parse nal unit lengths, in case
|
// 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.
|
// they're only 1 or 2 bytes long.
|
||||||
|
@ -23,6 +23,15 @@ import com.google.android.exoplayer2.Format;
|
|||||||
*/
|
*/
|
||||||
public final class Track {
|
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.
|
* The track identifier.
|
||||||
*/
|
*/
|
||||||
@ -53,6 +62,12 @@ public final class Track {
|
|||||||
*/
|
*/
|
||||||
public final Format format;
|
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.
|
* 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 final int nalUnitLengthFieldLength;
|
||||||
|
|
||||||
public Track(int id, int type, long timescale, long movieTimescale, long durationUs,
|
public Track(int id, int type, long timescale, long movieTimescale, long durationUs,
|
||||||
Format format, TrackEncryptionBox[] sampleDescriptionEncryptionBoxes,
|
Format format, int sampleTransformation,
|
||||||
int nalUnitLengthFieldLength, long[] editListDurations, long[] editListMediaTimes) {
|
TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength,
|
||||||
|
long[] editListDurations, long[] editListMediaTimes) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.timescale = timescale;
|
this.timescale = timescale;
|
||||||
this.movieTimescale = movieTimescale;
|
this.movieTimescale = movieTimescale;
|
||||||
this.durationUs = durationUs;
|
this.durationUs = durationUs;
|
||||||
this.format = format;
|
this.format = format;
|
||||||
|
this.sampleTransformation = sampleTransformation;
|
||||||
this.sampleDescriptionEncryptionBoxes = sampleDescriptionEncryptionBoxes;
|
this.sampleDescriptionEncryptionBoxes = sampleDescriptionEncryptionBoxes;
|
||||||
this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;
|
this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;
|
||||||
this.editListDurations = editListDurations;
|
this.editListDurations = editListDurations;
|
||||||
|
@ -52,8 +52,26 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||||||
} while (b == 0xFF);
|
} while (b == 0xFF);
|
||||||
// Process the payload.
|
// Process the payload.
|
||||||
if (Eia608Parser.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) {
|
if (Eia608Parser.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) {
|
||||||
output.sampleData(seiBuffer, payloadSize);
|
// Ignore country_code (1) + provider_code (2) + user_identifier (4)
|
||||||
output.sampleMetadata(pesTimeUs, C.BUFFER_FLAG_KEY_FRAME, payloadSize, 0, null);
|
// + 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 {
|
} else {
|
||||||
seiBuffer.skipBytes(payloadSize);
|
seiBuffer.skipBytes(payloadSize);
|
||||||
}
|
}
|
||||||
|
@ -90,8 +90,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
for (int j = 0; j < formats.length; j++) {
|
for (int j = 0; j < formats.length; j++) {
|
||||||
int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : -1;
|
int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : -1;
|
||||||
Track track = new Track(j, streamElement.type, streamElement.timescale, C.UNSET_TIME_US,
|
Track track = new Track(j, streamElement.type, streamElement.timescale, C.UNSET_TIME_US,
|
||||||
manifest.durationUs, formats[j], trackEncryptionBoxes, nalUnitLengthFieldLength,
|
manifest.durationUs, formats[j], Track.TRANSFORMATION_NONE, trackEncryptionBoxes,
|
||||||
null, null);
|
nalUnitLengthFieldLength, null, null);
|
||||||
FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(
|
FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(
|
||||||
FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME
|
FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME
|
||||||
| FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track);
|
| FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track);
|
||||||
|
@ -21,12 +21,10 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer;
|
|||||||
import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
|
import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
|
||||||
import com.google.android.exoplayer2.text.SubtitleParser;
|
import com.google.android.exoplayer2.text.SubtitleParser;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
|
||||||
@ -168,7 +166,7 @@ public final class Eia608Parser implements SubtitleParser {
|
|||||||
private final LinkedList<SubtitleOutputBuffer> availableOutputBuffers;
|
private final LinkedList<SubtitleOutputBuffer> availableOutputBuffers;
|
||||||
private final TreeSet<SubtitleInputBuffer> queuedInputBuffers;
|
private final TreeSet<SubtitleInputBuffer> queuedInputBuffers;
|
||||||
|
|
||||||
private final ParsableBitArray seiBuffer;
|
private final ParsableByteArray ccData;
|
||||||
|
|
||||||
private final StringBuilder captionStringBuilder;
|
private final StringBuilder captionStringBuilder;
|
||||||
|
|
||||||
@ -197,7 +195,7 @@ public final class Eia608Parser implements SubtitleParser {
|
|||||||
}
|
}
|
||||||
queuedInputBuffers = new TreeSet<>();
|
queuedInputBuffers = new TreeSet<>();
|
||||||
|
|
||||||
seiBuffer = new ParsableBitArray();
|
ccData = new ParsableByteArray();
|
||||||
|
|
||||||
captionStringBuilder = new StringBuilder();
|
captionStringBuilder = new StringBuilder();
|
||||||
|
|
||||||
@ -310,58 +308,30 @@ public final class Eia608Parser implements SubtitleParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void decode(SubtitleInputBuffer inputBuffer) {
|
private void decode(SubtitleInputBuffer inputBuffer) {
|
||||||
ByteBuffer inputData = inputBuffer.data;
|
ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit());
|
||||||
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);
|
|
||||||
|
|
||||||
boolean captionDataProcessed = false;
|
boolean captionDataProcessed = false;
|
||||||
boolean isRepeatableControl = false;
|
boolean isRepeatableControl = false;
|
||||||
for (int i = 0; i < ccCount; i++) {
|
while (ccData.bytesLeft() > 0) {
|
||||||
seiBuffer.skipBits(5); // one_bit + reserved
|
byte ccData1 = (byte) (ccData.readUnsignedByte() & 0x7F);
|
||||||
boolean ccValid = seiBuffer.readBit();
|
byte ccData2 = (byte) (ccData.readUnsignedByte() & 0x7F);
|
||||||
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);
|
|
||||||
|
|
||||||
// Ignore empty captions.
|
// Ignore empty captions.
|
||||||
if (ccData1 == 0 && ccData2 == 0) {
|
if (ccData1 == 0 && ccData2 == 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've reached this point then there is data to process; flag that work has been done.
|
// If we've reached this point then there is data to process; flag that work has been done.
|
||||||
captionDataProcessed = true;
|
captionDataProcessed = true;
|
||||||
|
|
||||||
// Special North American character set.
|
// Special North American character set.
|
||||||
// ccData2 - P|0|1|1|X|X|X|X
|
// ccData2 - P|0|1|1|X|X|X|X
|
||||||
if ((ccData1 == 0x11 || ccData1 == 0x19)
|
if ((ccData1 == 0x11 || ccData1 == 0x19) && ((ccData2 & 0x70) == 0x30)) {
|
||||||
&& ((ccData2 & 0x70) == 0x30)) {
|
|
||||||
captionStringBuilder.append(getSpecialChar(ccData2));
|
captionStringBuilder.append(getSpecialChar(ccData2));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extended Spanish/Miscellaneous and French character set.
|
// Extended Spanish/Miscellaneous and French character set.
|
||||||
// ccData2 - P|0|1|X|X|X|X|X
|
// ccData2 - P|0|1|X|X|X|X|X
|
||||||
if ((ccData1 == 0x12 || ccData1 == 0x1A)
|
if ((ccData1 == 0x12 || ccData1 == 0x1A) && ((ccData2 & 0x60) == 0x20)) {
|
||||||
&& ((ccData2 & 0x60) == 0x20)) {
|
|
||||||
backspace(); // Remove standard equivalent of the special extended char.
|
backspace(); // Remove standard equivalent of the special extended char.
|
||||||
captionStringBuilder.append(getExtendedEsFrChar(ccData2));
|
captionStringBuilder.append(getExtendedEsFrChar(ccData2));
|
||||||
continue;
|
continue;
|
||||||
@ -369,8 +339,7 @@ public final class Eia608Parser implements SubtitleParser {
|
|||||||
|
|
||||||
// Extended Portuguese and German/Danish character set.
|
// Extended Portuguese and German/Danish character set.
|
||||||
// ccData2 - P|0|1|X|X|X|X|X
|
// ccData2 - P|0|1|X|X|X|X|X
|
||||||
if ((ccData1 == 0x13 || ccData1 == 0x1B)
|
if ((ccData1 == 0x13 || ccData1 == 0x1B) && ((ccData2 & 0x60) == 0x20)) {
|
||||||
&& ((ccData2 & 0x60) == 0x20)) {
|
|
||||||
backspace(); // Remove standard equivalent of the special extended char.
|
backspace(); // Remove standard equivalent of the special extended char.
|
||||||
captionStringBuilder.append(getExtendedPtDeChar(ccData2));
|
captionStringBuilder.append(getExtendedPtDeChar(ccData2));
|
||||||
continue;
|
continue;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user