Commit 2621d962 by Zsolt Matyas

CEA608: handling channel bits properly

[Problem]
CC1 is shown even when CC2-3-4 is selected

[Solution]
 * use incoming channel and frame information
 * Misc Codes need to handle use Frame information
 * Add checks everywhere

[Test]
* Live channels and already existing test should not show any difference
in behavior when CC1 is selected.
* Live channels might have CC2-CC3-CC4
* Sarnoff test content has specific test cases for channel support
parent fff60235
...@@ -25,11 +25,11 @@ import android.text.style.ForegroundColorSpan; ...@@ -25,11 +25,11 @@ import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan; import android.text.style.UnderlineSpan;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.Subtitle;
import com.google.android.exoplayer2.text.SubtitleDecoder; import com.google.android.exoplayer2.text.SubtitleDecoder;
import com.google.android.exoplayer2.text.SubtitleInputBuffer; import com.google.android.exoplayer2.text.SubtitleInputBuffer;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -41,12 +41,16 @@ import java.util.List; ...@@ -41,12 +41,16 @@ import java.util.List;
*/ */
public final class Cea608Decoder extends CeaDecoder { public final class Cea608Decoder extends CeaDecoder {
private static final String TAG = "Cea608Decoder";
private static final int CC_VALID_FLAG = 0x04; private static final int CC_VALID_FLAG = 0x04;
private static final int CC_TYPE_FLAG = 0x02; private static final int CC_TYPE_FLAG = 0x02;
private static final int CC_FIELD_FLAG = 0x01; private static final int CC_FIELD_FLAG = 0x01;
private static final int NTSC_CC_FIELD_1 = 0x00; private static final int NTSC_CC_FIELD_1 = 0x00;
private static final int NTSC_CC_FIELD_2 = 0x01; private static final int NTSC_CC_FIELD_2 = 0x01;
private static final int NTSC_CC_CHANNEL_1 = 0x00;
private static final int NTSC_CC_CHANNEL_2 = 0x01;
private static final int CC_MODE_UNKNOWN = 0; private static final int CC_MODE_UNKNOWN = 0;
private static final int CC_MODE_ROLL_UP = 1; private static final int CC_MODE_ROLL_UP = 1;
...@@ -217,6 +221,7 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -217,6 +221,7 @@ public final class Cea608Decoder extends CeaDecoder {
private final ParsableByteArray ccData; private final ParsableByteArray ccData;
private final int packetLength; private final int packetLength;
private final int selectedField; private final int selectedField;
private final int selectedChannel;
private final ArrayList<CueBuilder> cueBuilders; private final ArrayList<CueBuilder> cueBuilders;
private CueBuilder currentCueBuilder; private CueBuilder currentCueBuilder;
...@@ -231,23 +236,41 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -231,23 +236,41 @@ public final class Cea608Decoder extends CeaDecoder {
private byte repeatableControlCc1; private byte repeatableControlCc1;
private byte repeatableControlCc2; private byte repeatableControlCc2;
private int incomingDataTargetChannel;
public Cea608Decoder(String mimeType, int accessibilityChannel) { public Cea608Decoder(String mimeType, int accessibilityChannel) {
ccData = new ParsableByteArray(); ccData = new ParsableByteArray();
cueBuilders = new ArrayList<>(); cueBuilders = new ArrayList<>();
currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT); currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT);
packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3; packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3;
// CEA608 has 2 fields and 2 channels on each field
switch (accessibilityChannel) { switch (accessibilityChannel) {
case 1:
selectedChannel = NTSC_CC_CHANNEL_1;
selectedField = NTSC_CC_FIELD_1;
break;
case 2:
selectedChannel = NTSC_CC_CHANNEL_2;
selectedField = NTSC_CC_FIELD_1;
break;
case 3: case 3:
selectedChannel = NTSC_CC_CHANNEL_1;
selectedField = NTSC_CC_FIELD_2;
break;
case 4: case 4:
selectedField = 2; selectedChannel = NTSC_CC_CHANNEL_2;
selectedField = NTSC_CC_FIELD_2;
break; break;
case 1:
case 2:
case Format.NO_VALUE:
default: default:
selectedField = 1; selectedChannel = NTSC_CC_CHANNEL_1;
selectedField = NTSC_CC_FIELD_1;
Log.w(TAG, "Subtitle channel was incorrectly set. Defaulting to CC1");
} }
Log.i(TAG, "Selected CEA608 channel is " + selectedChannel
+ " on field " + selectedField );
setCaptionMode(CC_MODE_UNKNOWN); setCaptionMode(CC_MODE_UNKNOWN);
resetCueBuilders(); resetCueBuilders();
} }
...@@ -269,6 +292,7 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -269,6 +292,7 @@ public final class Cea608Decoder extends CeaDecoder {
repeatableControlSet = false; repeatableControlSet = false;
repeatableControlCc1 = 0; repeatableControlCc1 = 0;
repeatableControlCc2 = 0; repeatableControlCc2 = 0;
incomingDataTargetChannel = NTSC_CC_CHANNEL_1;
} }
@Override @Override
...@@ -307,8 +331,7 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -307,8 +331,7 @@ public final class Cea608Decoder extends CeaDecoder {
continue; continue;
} }
if ((selectedField == 1 && (ccHeader & CC_FIELD_FLAG) != NTSC_CC_FIELD_1) if ((ccHeader & CC_FIELD_FLAG) != selectedField) {
|| (selectedField == 2 && (ccHeader & CC_FIELD_FLAG) != NTSC_CC_FIELD_2)) {
// Do not process packets not within the selected field. // Do not process packets not within the selected field.
continue; continue;
} }
...@@ -322,6 +345,9 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -322,6 +345,9 @@ public final class Cea608Decoder extends CeaDecoder {
continue; continue;
} }
boolean repeatedControlPossible = repeatableControlSet;
repeatableControlSet = false;
boolean previousCaptionValid = captionValid; boolean previousCaptionValid = captionValid;
captionValid = (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG; captionValid = (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG;
if (!captionValid) { if (!captionValid) {
...@@ -336,9 +362,6 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -336,9 +362,6 @@ public final class Cea608Decoder extends CeaDecoder {
// 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;
boolean repeatedControlPossible = repeatableControlSet;
repeatableControlSet = false;
if (!ODD_PARITY_BYTE_TABLE[ccByte1] || !ODD_PARITY_BYTE_TABLE[ccByte2]) { if (!ODD_PARITY_BYTE_TABLE[ccByte1] || !ODD_PARITY_BYTE_TABLE[ccByte2]) {
// The data is invalid. // The data is invalid.
resetCueBuilders(); resetCueBuilders();
...@@ -349,8 +372,9 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -349,8 +372,9 @@ public final class Cea608Decoder extends CeaDecoder {
// ccData1 - 0|0|0|1|C|0|0|1 // ccData1 - 0|0|0|1|C|0|0|1
// ccData2 - 0|0|1|1|X|X|X|X // ccData2 - 0|0|1|1|X|X|X|X
if (((ccData1 & 0xF7) == 0x11) && ((ccData2 & 0xF0) == 0x30)) { if (((ccData1 & 0xF7) == 0x11) && ((ccData2 & 0xF0) == 0x30)) {
// TODO: Make use of the channel toggle if (isBytePairForSelectedChannel(ccData1)) {
currentCueBuilder.append(getSpecialChar(ccData2)); currentCueBuilder.append(getSpecialChar(ccData2));
}
continue; continue;
} }
...@@ -358,7 +382,7 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -358,7 +382,7 @@ public final class Cea608Decoder extends CeaDecoder {
// ccData1 - 0|0|0|1|C|0|1|S // ccData1 - 0|0|0|1|C|0|1|S
// ccData2 - 0|0|1|X|X|X|X|X // ccData2 - 0|0|1|X|X|X|X|X
if (((ccData1 & 0xF6) == 0x12) && (ccData2 & 0xE0) == 0x20) { if (((ccData1 & 0xF6) == 0x12) && (ccData2 & 0xE0) == 0x20) {
// TODO: Make use of the channel toggle if (isBytePairForSelectedChannel(ccData1)) {
// Remove standard equivalent of the special extended char before appending new one // Remove standard equivalent of the special extended char before appending new one
currentCueBuilder.backspace(); currentCueBuilder.backspace();
if ((ccData1 & 0x01) == 0x00) { if ((ccData1 & 0x01) == 0x00) {
...@@ -368,6 +392,7 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -368,6 +392,7 @@ public final class Cea608Decoder extends CeaDecoder {
// Extended Portuguese and German/Danish character set (S = 1). // Extended Portuguese and German/Danish character set (S = 1).
currentCueBuilder.append(getExtendedPtDeChar(ccData2)); currentCueBuilder.append(getExtendedPtDeChar(ccData2));
} }
}
continue; continue;
} }
...@@ -378,6 +403,10 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -378,6 +403,10 @@ public final class Cea608Decoder extends CeaDecoder {
continue; continue;
} }
if (incomingDataTargetChannel != selectedChannel) {
continue;
}
// Basic North American character set. // Basic North American character set.
currentCueBuilder.append(getChar(ccData1)); currentCueBuilder.append(getChar(ccData1));
if ((ccData2 & 0xE0) != 0x00) { if ((ccData2 & 0xE0) != 0x00) {
...@@ -392,35 +421,46 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -392,35 +421,46 @@ public final class Cea608Decoder extends CeaDecoder {
} }
} }
// Each byte pair has a single bit showing which channel the pair belongs to.
// 5th bit of ccData1 selects the channel: x|x|x|x|C|x|x|x
private int getChannelBit(byte ccData1) {
return (ccData1 >> 3) & 0x1;
}
private boolean isBytePairForSelectedChannel(byte ccData1) {
return getChannelBit(ccData1) == selectedChannel;
}
private void handleCtrl(byte cc1, byte cc2, boolean repeatedControlPossible) { private void handleCtrl(byte cc1, byte cc2, boolean repeatedControlPossible) {
boolean isRepeatableControl = isRepeatable(cc1); incomingDataTargetChannel = getChannelBit(cc1);
// Most control commands are sent twice in succession to ensure they are received properly. // Most control commands are sent twice in succession to ensure they are received properly. We
// We don't want to process duplicate commands, so if we see the same repeatable command twice // don't want to process duplicate commands, so if we see the same repeatable command twice in a
// in a row, ignore the second one. // row then we ignore the second one.
if (isRepeatableControl) { if (isRepeatable(cc1)) {
if (repeatedControlPossible if (repeatedControlPossible && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) {
&& repeatableControlCc1 == cc1 // This is a repeated command, so we ignore it.
&& repeatableControlCc2 == cc2) {
// This is a duplicate. Repeatable control flag should be already cleared, let's return.
return; return;
} else { } else {
// This is a repeatable command, but we haven't seen it yet, so set the repeatable control // This is the first occurrence of a repeatable command. Set the repeatable control
// flag (to ensure we ignore the next one should it be a duplicate) and continue processing // variables so that we can recognize and ignore a duplicate (if there is one), and then
// the command. // continue to process the command below.
repeatableControlSet = true; repeatableControlSet = true;
repeatableControlCc1 = cc1; repeatableControlCc1 = cc1;
repeatableControlCc2 = cc2; repeatableControlCc2 = cc2;
} }
} }
if (!isBytePairForSelectedChannel(cc1)) {
return;
}
if (isMidrowCtrlCode(cc1, cc2)) { if (isMidrowCtrlCode(cc1, cc2)) {
handleMidrowCtrl(cc2); handleMidrowCtrl(cc2);
} else if (isPreambleAddressCode(cc1, cc2)) { } else if (isPreambleAddressCode(cc1, cc2)) {
handlePreambleAddressCode(cc1, cc2); handlePreambleAddressCode(cc1, cc2);
} else if (isTabCtrlCode(cc1, cc2)) { } else if (isTabCtrlCode(cc1, cc2)) {
currentCueBuilder.tabOffset = cc2 - 0x20; currentCueBuilder.tabOffset = cc2 - 0x20;
} else if (isMiscCode(cc1, cc2)) { } else if (isMiscCode(cc1, cc2, selectedField)) {
handleMiscCode(cc2); handleMiscCode(cc2);
} }
} }
...@@ -441,7 +481,6 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -441,7 +481,6 @@ public final class Cea608Decoder extends CeaDecoder {
// cc1 - 0|0|0|1|C|E|ROW // cc1 - 0|0|0|1|C|E|ROW
// C is the channel toggle, E is the extended flag, and ROW is the encoded row // C is the channel toggle, E is the extended flag, and ROW is the encoded row
int row = ROW_INDICES[cc1 & 0x07]; int row = ROW_INDICES[cc1 & 0x07];
// TODO: Make use of the channel toggle
// TODO: support the extended address and style // TODO: support the extended address and style
// cc2 - 0|1|N|ATTRBTE|U // cc2 - 0|1|N|ATTRBTE|U
...@@ -643,10 +682,13 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -643,10 +682,13 @@ public final class Cea608Decoder extends CeaDecoder {
return ((cc1 & 0xF7) == 0x17) && (cc2 >= 0x21 && cc2 <= 0x23); return ((cc1 & 0xF7) == 0x17) && (cc2 >= 0x21 && cc2 <= 0x23);
} }
private static boolean isMiscCode(byte cc1, byte cc2) { private static boolean isMiscCode(byte cc1, byte cc2, int targetField) {
// cc1 - 0|0|0|1|C|1|0|0 // bits of cc1: 0|0|0|1|C|1|0|F
// cc2 - 0|0|1|0|X|X|X|X // bits of cc2: 0|0|1|0|X|X|X|X
return ((cc1 & 0xF7) == 0x14) && ((cc2 & 0xF0) == 0x20); // Legends: F=field bit; C=channel bit; X=arbitrary value; 0/1=expected value
return ((cc1 & 0xF6) == 0x14)
&& ((cc1 & 0x1) == targetField)
&& ((cc2 & 0xF0) == 0x20);
} }
private static boolean isRepeatable(byte cc1) { private static boolean isRepeatable(byte cc1) {
......
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