mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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
This commit is contained in:
parent
fff602358f
commit
2621d962e7
1 changed files with 85 additions and 43 deletions
|
|
@ -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;
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
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 {
|
||||||
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 3:
|
|
||||||
case 4:
|
|
||||||
selectedField = 2;
|
|
||||||
break;
|
|
||||||
case 1:
|
case 1:
|
||||||
|
selectedChannel = NTSC_CC_CHANNEL_1;
|
||||||
|
selectedField = NTSC_CC_FIELD_1;
|
||||||
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
case Format.NO_VALUE:
|
selectedChannel = NTSC_CC_CHANNEL_2;
|
||||||
default:
|
selectedField = NTSC_CC_FIELD_1;
|
||||||
selectedField = 1;
|
break;
|
||||||
|
case 3:
|
||||||
|
selectedChannel = NTSC_CC_CHANNEL_1;
|
||||||
|
selectedField = NTSC_CC_FIELD_2;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
selectedChannel = NTSC_CC_CHANNEL_2;
|
||||||
|
selectedField = NTSC_CC_FIELD_2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
// 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 {
|
||||||
// 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,15 +382,16 @@ 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) {
|
||||||
// Extended Spanish/Miscellaneous and French character set (S = 0).
|
// Extended Spanish/Miscellaneous and French character set (S = 0).
|
||||||
currentCueBuilder.append(getExtendedEsFrChar(ccData2));
|
currentCueBuilder.append(getExtendedEsFrChar(ccData2));
|
||||||
} else {
|
} else {
|
||||||
// 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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCtrl(byte cc1, byte cc2, boolean repeatedControlPossible) {
|
// Each byte pair has a single bit showing which channel the pair belongs to.
|
||||||
boolean isRepeatableControl = isRepeatable(cc1);
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
// Most control commands are sent twice in succession to ensure they are received properly.
|
private void handleCtrl(byte cc1, byte cc2, boolean repeatedControlPossible) {
|
||||||
// We don't want to process duplicate commands, so if we see the same repeatable command twice
|
incomingDataTargetChannel = getChannelBit(cc1);
|
||||||
// in a row, ignore the second one.
|
|
||||||
if (isRepeatableControl) {
|
// Most control commands are sent twice in succession to ensure they are received properly. We
|
||||||
if (repeatedControlPossible
|
// don't want to process duplicate commands, so if we see the same repeatable command twice in a
|
||||||
&& repeatableControlCc1 == cc1
|
// row then we ignore the second one.
|
||||||
&& repeatableControlCc2 == cc2) {
|
if (isRepeatable(cc1)) {
|
||||||
// This is a duplicate. Repeatable control flag should be already cleared, let's return.
|
if (repeatedControlPossible && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) {
|
||||||
|
// This is a repeated command, so we ignore it.
|
||||||
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 {
|
||||||
// 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 {
|
||||||
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) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue