Skip to content

Commit

Permalink
CEA-708 Decoder fixes for issue google#1807.
Browse files Browse the repository at this point in the history
Fix requires changes to the merged pull request google#8415
that are still part of the unmerged pull request google#8356

In this commit -

Handling the sequence number discontinuity in caption channel packet header.

Handle the multiple Service Blocks within a single caption channel packet. An outer while loop is added to function processCurrentPacket() to achieve that.

The processCurrentPacket returns if the packet length does not match with the currentIndex. That assumption is wrong.

As per spec the the packet can end on reception of next cc_type = 0x3.

Added a logic to handle multi byte commands. These commands can be within the service block or spread across service blocks.

Processing of service block needs to wait until all the command bytes arrive. An ArrayList is used to accumulate the bytes belonging to same service block.

In this change the current flow is not altered, i.e. the parsing of service block is not started until the complete caption channel packet arrives.

These changes are verified through Sarnoff Test streams. Many tests are still failing after this change. Subsequent changes are planned to address that.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Author:    sneelavara <sneelavara@gmail.com>
# Date:      Wed Dec 16 13:03:37 2020 -0800
#
# On branch t-fix-cea708Merge
# Your branch and 'tivo-pvt/t-fix-cea708Merge' have diverged,
# and have 1 and 2 different commits each, respectively.
#   (use "git pull" to merge the remote branch into yours)
#
# Changes to be committed:
#	modified:   library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
#
# Untracked files:
#	CCURStream_3000kbps720p29_97fps-1_1661286386_init.cmfv?ccur_keyrot_t=1666096234
#	CCURStream_3000kbps720p29_97fps-1_T1666098015585589~D6006000.cmfv
#	MERGE_COMMIT_MESSAGE
#	VelocixVTP.mp4
#	X
#	test_url
#	tivo-docs/DASH Encoding Guidelines.md
#	tivo-docs/Encoding HLS Renditions.pdf
#	tivo-docs/HANDLING_2.15.1_MERGE_CONFLICTS.pdf
#	tivo-docs/Logging Updates For Audio Position.md
#	tivo-docs/Logging Updates For Audio Position.pdf
#	tivo-docs/TiVoExoPlayerReleaseMapping.md
#	tivo-docs/TiVoExoPlayerReleaseMapping.pdf
#	tivo-docs/Understanding ExoPlayer Position Tracking.md
#	tivo-docs/Understanding ExoPlayer Position Tracking.pdf
#	widevine.cmfv
#
  • Loading branch information
sneelavara authored and stevemayhew committed Oct 26, 2022
1 parent ee6b732 commit b624141
Showing 1 changed file with 163 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,29 @@ public final class Cea708Decoder extends CeaDecoder {
private static final int CHARACTER_LOWER_RIGHT_BORDER = 0x7E;
private static final int CHARACTER_UPPER_LEFT_BORDER = 0x7F;

// Command lengths in bits exluding the command byte.
private static final int CLW_COMMAND_LEN = 8;
private static final int DSW_COMMAND_LEN = 8;
private static final int HDW_COMMAND_LEN = 8;
private static final int TGW_COMMAND_LEN = 8;
private static final int DLW_COMMAND_LEN = 8;
private static final int DLY_COMMAND_LEN = 8;
private static final int SPA_COMMAND_LEN = 16;
private static final int SPL_COMMAND_LEN = 16;
private static final int SPC_COMMAND_LEN = 24;
private static final int SWA_COMMAND_LEN = 32;
private static final int DFS_COMMAND_LEN = 48;

private final ParsableByteArray ccData;
private final ParsableBitArray serviceBlockPacket;
private int previousSequenceNumber;
// This holds the service blocks. This used to accumulate the commands spread across the service
// blocks as well as the service blocks and/or commands spread across the DTVCC packets
private List<Byte> serviceBlockList = new ArrayList<>();
private static final int CC_DATA_STATE_COMPLETE_PARAM = 0;
private static final int CC_DATA_STATE_WAITING_PARAM = 1;
private int ccDataState = CC_DATA_STATE_COMPLETE_PARAM;
private int lastSequenceNo = -1;

// TODO: Use isWideAspectRatio in decoding.
@SuppressWarnings({"unused", "FieldCanBeLocal"})
private final boolean isWideAspectRatio;
Expand All @@ -162,7 +182,6 @@ public final class Cea708Decoder extends CeaDecoder {
public Cea708Decoder(int accessibilityChannel, @Nullable List<byte[]> initializationData) {
ccData = new ParsableByteArray();
serviceBlockPacket = new ParsableBitArray();
previousSequenceNumber = C.INDEX_UNSET;
selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel;
isWideAspectRatio =
initializationData != null
Expand Down Expand Up @@ -190,6 +209,8 @@ public void flush() {
currentCueInfoBuilder = cueInfoBuilders[currentWindow];
resetCueBuilders();
currentDtvCcPacket = null;
ccDataState = CC_DATA_STATE_COMPLETE_PARAM;
serviceBlockList.clear();
}

@Override
Expand Down Expand Up @@ -232,17 +253,12 @@ protected void decode(SubtitleInputBuffer inputBuffer) {
finalizeCurrentPacket();

int sequenceNumber = (ccData1 & 0xC0) >> 6; // first 2 bits
if (previousSequenceNumber != C.INDEX_UNSET
&& sequenceNumber != (previousSequenceNumber + 1) % 4) {
if (lastSequenceNo != -1 && sequenceNumber != (lastSequenceNo + 1) % 4) {
resetCueBuilders();
Log.w(
TAG,
"Sequence number discontinuity. previous="
+ previousSequenceNumber
+ " current="
+ sequenceNumber);
Log.w(TAG, "discontinuity in sequence number detected : lastSequenceNo = " + lastSequenceNo
+ " sequenceNumber = " + sequenceNumber);
}
previousSequenceNumber = sequenceNumber;
lastSequenceNo = sequenceNumber;

int packetSize = ccData1 & 0x3F; // last 6 bits
if (packetSize == 0) {
Expand Down Expand Up @@ -297,75 +313,106 @@ private void processCurrentPacket() {
// we have received.
}

serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex);

int serviceNumber = serviceBlockPacket.readBits(3);
int blockSize = serviceBlockPacket.readBits(5);
if (serviceNumber == 7) {
// extended service numbers
serviceBlockPacket.skipBits(2);
serviceNumber = serviceBlockPacket.readBits(6);
if (serviceNumber < 7) {
Log.w(TAG, "Invalid extended service number: " + serviceNumber);
int currOffset = 0;
// Caption channel packet can contain more than one service blocks. Handle all the service
// blocks in the current packet. If the previous command spread across the paket, that is
// accumulated and handled after all the bytes are received
while (currOffset < currentDtvCcPacket.currentIndex) {
int serviceNumber = (currentDtvCcPacket.packetData[currOffset] & 0xE0) >> 5; //3 bits
int blockSize = (currentDtvCcPacket.packetData[currOffset] & 0x1F); // 5 bits
currOffset++;
if (serviceNumber == 7) {
// extended service numbers
serviceNumber = (currentDtvCcPacket.packetData[currOffset] & 0x3F); //skip 2 bits
if (serviceNumber < 7) {
Log.w(TAG, "Invalid extended service number: " + serviceNumber);
}
currOffset++;
}
}

// Ignore packets in which blockSize is 0
if (blockSize == 0) {
if (serviceNumber != 0) {
Log.w(TAG, "serviceNumber is non-zero (" + serviceNumber + ") when blockSize is 0");
// Ignore packets in which blockSize is 0
if (blockSize == 0) {
if (serviceNumber != 0) {
Log.w(TAG, "serviceNumber is non-zero (" + serviceNumber + ") when blockSize is 0");
}
return;
}
return;
}

if (serviceNumber != selectedServiceNumber) {
return;
}

// The cues should be updated if we receive a C0 ETX command, any C1 command, or if after
// processing the service block any text has been added to the buffer. See CEA-708-B Section
// 8.10.4 for more details.
boolean cuesNeedUpdate = false;

while (serviceBlockPacket.bitsLeft() > 0) {
int command = serviceBlockPacket.readBits(8);
if (command != COMMAND_EXT1) {
if (command <= GROUP_C0_END) {
handleC0Command(command);
// If the C0 command was an ETX command, the cues are updated in handleC0Command.
} else if (command <= GROUP_G0_END) {
handleG0Character(command);
cuesNeedUpdate = true;
} else if (command <= GROUP_C1_END) {
handleC1Command(command);
cuesNeedUpdate = true;
} else if (command <= GROUP_G1_END) {
handleG1Character(command);
cuesNeedUpdate = true;
} else {
Log.w(TAG, "Invalid base command: " + command);
if (serviceNumber == selectedServiceNumber) {
int b = 0;
while ( b < blockSize) {
// start accumulating the service blocks
if (currOffset >= currentDtvCcPacket.currentIndex) {
// extra protection to avoid array out of bound.
break;
}
serviceBlockList.add(currentDtvCcPacket.packetData[currOffset++]);
b++;
}
} else {
// Read the extended command
command = serviceBlockPacket.readBits(8);
if (command <= GROUP_C2_END) {
handleC2Command(command);
} else if (command <= GROUP_G2_END) {
handleG2Character(command);
cuesNeedUpdate = true;
} else if (command <= GROUP_C3_END) {
handleC3Command(command);
} else if (command <= GROUP_G3_END) {
handleG3Character(command);
cuesNeedUpdate = true;
// Convert the ArrayList to byte[].
byte[] blk = new byte[serviceBlockList.size()];
for (int i = 0; i < serviceBlockList.size(); i++) {
blk[i] = serviceBlockList.get(i);
}
serviceBlockPacket.reset(blk, serviceBlockList.size());
}
else {
// different service number skip the entire service block
currOffset += blockSize;
}

// The cues should be updated if we receive a C0 ETX command, any C1 command, or if after
// processing the service block any text has been added to the buffer. See CEA-708-B Section
// 8.10.4 for more details.
boolean cuesNeedUpdate = false;

while (serviceBlockPacket.bitsLeft() > 0) {
int command = serviceBlockPacket.readBits(8);
if (command != COMMAND_EXT1) {
if (command <= GROUP_C0_END) {
handleC0Command(command);
// If the C0 command was an ETX command, the cues are updated in handleC0Command.
} else if (command <= GROUP_G0_END) {
handleG0Character(command);
cuesNeedUpdate = true;
} else if (command <= GROUP_C1_END) {
handleC1Command(command);
if (ccDataState == CC_DATA_STATE_WAITING_PARAM) {
// command structure is partial. Wait for more data
break;
}
cuesNeedUpdate = true;
} else if (command <= GROUP_G1_END) {
handleG1Character(command);
cuesNeedUpdate = true;
} else {
Log.w(TAG, "Invalid base command: " + command);
}
} else {
Log.w(TAG, "Invalid extended command: " + command);
// Read the extended command
command = serviceBlockPacket.readBits(8);
if (command <= GROUP_C2_END) {
handleC2Command(command);
} else if (command <= GROUP_G2_END) {
handleG2Character(command);
cuesNeedUpdate = true;
} else if (command <= GROUP_C3_END) {
handleC3Command(command);
} else if (command <= GROUP_G3_END) {
handleG3Character(command);
cuesNeedUpdate = true;
} else {
Log.w(TAG, "Invalid extended command: " + command);
}
}
}
}

if (cuesNeedUpdate) {
cues = getDisplayCues();
if (cuesNeedUpdate) {
cues = getDisplayCues();
}
if (ccDataState == CC_DATA_STATE_COMPLETE_PARAM) {
serviceBlockList.clear();
}
}
}

Expand Down Expand Up @@ -404,6 +451,7 @@ private void handleC0Command(int command) {

private void handleC1Command(int command) {
int window;
ccDataState = CC_DATA_STATE_COMPLETE_PARAM;
switch (command) {
case COMMAND_CW0:
case COMMAND_CW1:
Expand All @@ -420,27 +468,43 @@ private void handleC1Command(int command) {
}
break;
case COMMAND_CLW:
if (serviceBlockPacket.bitsLeft() < CLW_COMMAND_LEN) {
ccDataState = CC_DATA_STATE_WAITING_PARAM;
break;
}
for (int i = 1; i <= NUM_WINDOWS; i++) {
if (serviceBlockPacket.readBit()) {
cueInfoBuilders[NUM_WINDOWS - i].clear();
}
}
break;
case COMMAND_DSW:
if (serviceBlockPacket.bitsLeft() < DSW_COMMAND_LEN) {
ccDataState = CC_DATA_STATE_WAITING_PARAM;
break;
}
for (int i = 1; i <= NUM_WINDOWS; i++) {
if (serviceBlockPacket.readBit()) {
cueInfoBuilders[NUM_WINDOWS - i].setVisibility(true);
}
}
break;
case COMMAND_HDW:
if (serviceBlockPacket.bitsLeft() < HDW_COMMAND_LEN) {
ccDataState = CC_DATA_STATE_WAITING_PARAM;
break;
}
for (int i = 1; i <= NUM_WINDOWS; i++) {
if (serviceBlockPacket.readBit()) {
cueInfoBuilders[NUM_WINDOWS - i].setVisibility(false);
}
}
break;
case COMMAND_TGW:
if (serviceBlockPacket.bitsLeft() < TGW_COMMAND_LEN) {
ccDataState = CC_DATA_STATE_WAITING_PARAM;
break;
}
for (int i = 1; i <= NUM_WINDOWS; i++) {
if (serviceBlockPacket.readBit()) {
CueInfoBuilder cueInfoBuilder = cueInfoBuilders[NUM_WINDOWS - i];
Expand All @@ -449,13 +513,21 @@ private void handleC1Command(int command) {
}
break;
case COMMAND_DLW:
if (serviceBlockPacket.bitsLeft() < DLW_COMMAND_LEN) {
ccDataState = CC_DATA_STATE_WAITING_PARAM;
break;
}
for (int i = 1; i <= NUM_WINDOWS; i++) {
if (serviceBlockPacket.readBit()) {
cueInfoBuilders[NUM_WINDOWS - i].reset();
}
}
break;
case COMMAND_DLY:
if (serviceBlockPacket.bitsLeft() < DLY_COMMAND_LEN) {
ccDataState = CC_DATA_STATE_WAITING_PARAM;
break;
}
// TODO: Add support for delay commands.
serviceBlockPacket.skipBits(8);
break;
Expand All @@ -466,6 +538,10 @@ private void handleC1Command(int command) {
resetCueBuilders();
break;
case COMMAND_SPA:
if (serviceBlockPacket.bitsLeft() < SPA_COMMAND_LEN) {
ccDataState = CC_DATA_STATE_COMPLETE_PARAM;
break;
}
if (!currentCueInfoBuilder.isDefined()) {
// ignore this command if the current window/cue isn't defined
serviceBlockPacket.skipBits(16);
Expand All @@ -474,6 +550,10 @@ private void handleC1Command(int command) {
}
break;
case COMMAND_SPC:
if (serviceBlockPacket.bitsLeft() < SPC_COMMAND_LEN) {
ccDataState = CC_DATA_STATE_WAITING_PARAM;
break;
}
if (!currentCueInfoBuilder.isDefined()) {
// ignore this command if the current window/cue isn't defined
serviceBlockPacket.skipBits(24);
Expand All @@ -482,6 +562,10 @@ private void handleC1Command(int command) {
}
break;
case COMMAND_SPL:
if (serviceBlockPacket.bitsLeft() < SPL_COMMAND_LEN) {
ccDataState = CC_DATA_STATE_WAITING_PARAM;
break;
}
if (!currentCueInfoBuilder.isDefined()) {
// ignore this command if the current window/cue isn't defined
serviceBlockPacket.skipBits(16);
Expand All @@ -490,6 +574,10 @@ private void handleC1Command(int command) {
}
break;
case COMMAND_SWA:
if (serviceBlockPacket.bitsLeft() < SWA_COMMAND_LEN) {
ccDataState = CC_DATA_STATE_WAITING_PARAM;
break;
}
if (!currentCueInfoBuilder.isDefined()) {
// ignore this command if the current window/cue isn't defined
serviceBlockPacket.skipBits(32);
Expand All @@ -505,6 +593,10 @@ private void handleC1Command(int command) {
case COMMAND_DF5:
case COMMAND_DF6:
case COMMAND_DF7:
if (serviceBlockPacket.bitsLeft() < DFS_COMMAND_LEN) {
ccDataState = CC_DATA_STATE_WAITING_PARAM;
break;
}
window = (command - COMMAND_DF0);
handleDefineWindow(window);
// We also set the current window to the newly defined window.
Expand Down

0 comments on commit b624141

Please sign in to comment.