diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 725321e53fc..b14342faa27 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -187,6 +187,7 @@ public final class Cea608Decoder extends CeaDecoder { private CueBuilder currentCueBuilder; private List cues; private List lastCues; + private long inputTimestampUs; private int captionMode; private int captionRowCount; @@ -252,6 +253,7 @@ protected Subtitle createSubtitle() { @Override protected void decode(SubtitleInputBuffer inputBuffer) { + inputTimestampUs = inputBuffer.timeUs; ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit()); boolean captionDataProcessed = false; boolean isRepeatableControl = false; @@ -329,6 +331,7 @@ protected void decode(SubtitleInputBuffer inputBuffer) { } if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { cues = getDisplayCues(); + onNewSubtitleDataAvailable(inputTimestampUs); // update screen } } } @@ -454,12 +457,14 @@ private void handleMiscCode(byte cc2) { if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { resetCueBuilders(); } + onNewSubtitleDataAvailable(inputTimestampUs); // update screen break; case CTRL_ERASE_NON_DISPLAYED_MEMORY: resetCueBuilders(); break; case CTRL_END_OF_CAPTION: cues = getDisplayCues(); + onNewSubtitleDataAvailable(inputTimestampUs); // update screen resetCueBuilders(); break; case CTRL_CARRIAGE_RETURN: @@ -506,6 +511,7 @@ private void setCaptionMode(int captionMode) { || captionMode == CC_MODE_UNKNOWN) { // When switching from paint-on or to roll-up or unknown, we also need to clear the caption. cues = null; + onNewSubtitleDataAvailable(inputTimestampUs); // update screen } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index f21804b01be..07e9fe5eef0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -34,12 +34,12 @@ import com.google.android.exoplayer2.text.SubtitleDecoder; import com.google.android.exoplayer2.text.SubtitleInputBuffer; 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.CircularByteQueue; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedList; import java.util.List; - /** * A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708"). */ @@ -103,7 +103,7 @@ public final class Cea708Decoder extends CeaDecoder { private static final int COMMAND_DF1 = 0x99; // DefineWindow 1 (+6 bytes) private static final int COMMAND_DF2 = 0x9A; // DefineWindow 2 (+6 bytes) private static final int COMMAND_DF3 = 0x9B; // DefineWindow 3 (+6 bytes) - private static final int COMMAND_DF4 = 0x9C; // DefineWindow 4 (+6 bytes) + private static final int COMMAND_DS4 = 0x9C; // DefineWindow 4 (+6 bytes) private static final int COMMAND_DF5 = 0x9D; // DefineWindow 5 (+6 bytes) private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes) private static final int COMMAND_DF7 = 0x9F; // DefineWindow 7 (+6 bytes) @@ -140,7 +140,6 @@ public final class Cea708Decoder extends CeaDecoder { private static final int CHARACTER_UPPER_LEFT_BORDER = 0x7F; private final ParsableByteArray ccData; - private final ParsableBitArray serviceBlockPacket; private final int selectedServiceNumber; private final CueBuilder[] cueBuilders; @@ -149,14 +148,45 @@ public final class Cea708Decoder extends CeaDecoder { private List cues; private List lastCues; - private DtvCcPacket currentDtvCcPacket; + + private int lastSequenceNo = -1; private int currentWindow; + private long delayUs; // delayed processing due to DLC command + private long startOfDelayUs; // to keep track of the delay timeout + private static final int SERVICE_INPUT_BUFF_SIZE = 128; + private long inputTimestampUs;// currently processing input buffer timestamp + + // Caption Channel Packet processing related + private int ccpDataIndex; + private int ccpSize; + + // service block processing related + private static final int SERVICE_BLOCK_HEADER = 0; + private static final int SERVICE_BLOCK_EXT_HEADER = 1; + private static final int SERVICE_BLOCK_DATA = 2; + private int serviceBlockProcessingState = SERVICE_BLOCK_HEADER; + private int serviceBlockSize; + private int serviceNumber; + private int serviceBlockDataBufferOffset = 0; + + // service input buffer queue + CircularByteQueue serviceInputBufferQ = new CircularByteQueue(SERVICE_INPUT_BUFF_SIZE); + + // Closed Caption data related + private static final int CC_DATA_STATE_COMMAND = 0; + private static final int CC_DATA_STATE_WAITING_FOR_PARAM = 1; + private static final int CC_DATA_STATE_SKIPPING = 2; + private int ccDataState = CC_DATA_STATE_COMMAND; + private int ccDataSkipCount = 0; + private int currentCommand = -1; + private boolean isExtendedCommand = false; + private boolean cuesNeedUpdate; + private static final boolean DEBUG = false; + public Cea708Decoder(int accessibilityChannel, List initializationData) { ccData = new ParsableByteArray(); - serviceBlockPacket = new ParsableBitArray(); - selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel; - + selectedServiceNumber = (accessibilityChannel == Format.NO_VALUE) ? 1 : accessibilityChannel; cueBuilders = new CueBuilder[NUM_WINDOWS]; for (int i = 0; i < NUM_WINDOWS; i++) { cueBuilders[i] = new CueBuilder(); @@ -179,7 +209,8 @@ public void flush() { currentWindow = 0; currentCueBuilder = cueBuilders[currentWindow]; resetCueBuilders(); - currentDtvCcPacket = null; + finishCCPacket(); + resetCCDataState(); } @Override @@ -195,10 +226,8 @@ protected Subtitle createSubtitle() { @Override protected void decode(SubtitleInputBuffer inputBuffer) { - // Subtitle input buffers are non-direct and the position is zero, so calling array() is safe. - @SuppressWarnings("ByteBufferBackingArray") - byte[] inputBufferData = inputBuffer.data.array(); - ccData.reset(inputBufferData, inputBuffer.data.limit()); + inputTimestampUs = inputBuffer.timeUs; + ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit()); while (ccData.bytesLeft() >= 3) { int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07); @@ -206,7 +235,6 @@ protected void decode(SubtitleInputBuffer inputBuffer) { boolean ccValid = (ccTypeAndValid & CC_VALID_FLAG) == CC_VALID_FLAG; byte ccData1 = (byte) ccData.readUnsignedByte(); byte ccData2 = (byte) ccData.readUnsignedByte(); - // Ignore any non-CEA-708 data if (ccType != DTVCC_PACKET_DATA && ccType != DTVCC_PACKET_START) { continue; @@ -217,158 +245,345 @@ protected void decode(SubtitleInputBuffer inputBuffer) { continue; } + if (ccType == DTVCC_PACKET_START) { - finalizeCurrentPacket(); + finishCCPacket(); int sequenceNumber = (ccData1 & 0xC0) >> 6; // first 2 bits - int packetSize = ccData1 & 0x3F; // last 6 bits - if (packetSize == 0) { - packetSize = 64; + if (lastSequenceNo != -1 && sequenceNumber != (lastSequenceNo + 1) % 4) { + resetCueBuilders(); + Log.w(TAG, "discontinuity in sequence number detected : lastSequenceNo = " + lastSequenceNo + + " sequenceNumber = " + sequenceNumber); } + lastSequenceNo = sequenceNumber; - currentDtvCcPacket = new DtvCcPacket(sequenceNumber, packetSize); - currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2; + ccpSize = ccData1 & 0x3F; // last 6 bits + if (ccpSize == 0) { + ccpSize = 127; + } else { + ccpSize *= 2; + ccpSize--; + } + if (DEBUG) { + Log.d(TAG,"DTVCC_PACKET_START : sequenceNumber = " + sequenceNumber+ " ccpSize = " + ccpSize); + } + processCCPacket(ccData2); + ccpDataIndex = 1; } else { // The only remaining valid packet type is DTVCC_PACKET_DATA Assertions.checkArgument(ccType == DTVCC_PACKET_DATA); - - if (currentDtvCcPacket == null) { - Log.e(TAG, "Encountered DTVCC_PACKET_DATA before DTVCC_PACKET_START"); + if (ccpSize == 0) { + Log.w(TAG,"Encountered DTVCC_PACKET_DATE before DTVCC_PACKET_START, ignoring..."); continue; } - - currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData1; - currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2; + processCCPacket(ccData1, ccData2); + ccpDataIndex += 2; + if (DEBUG) { + Log.d(TAG, "DTVCC_PACKET_DATA : ccpDataIndex = " + ccpDataIndex); + } } + } + if (cuesNeedUpdate) { + updateCues(); + } + } + private void finishCCPacket() { + ccpDataIndex = 0; + ccpSize = 0; + resetServiceBlockState(); + } + private void updateCues() { + cuesNeedUpdate = false; + cues = getDisplayCues(); + onNewSubtitleDataAvailable(inputTimestampUs); + } + private void processCCPacket(byte... dtvccPkt) { + for (int i = 0; i < dtvccPkt.length; i++) { + switch(serviceBlockProcessingState){ + case SERVICE_BLOCK_HEADER: { + serviceNumber = (dtvccPkt[i] & 0xE0) >> 5; // 3 bits + serviceBlockSize = (dtvccPkt[i] & 0x1F); // 5 bits + if (DEBUG) { + Log.d(TAG,"SERVICE_BLOCK_HEADER: serviceNumber = " + serviceNumber + + ", serviceBlockSize = " + serviceBlockSize); + } + if (serviceNumber == 7 && serviceBlockSize != 0) { + serviceBlockProcessingState = SERVICE_BLOCK_EXT_HEADER; + } else if (serviceBlockSize != 0) { + serviceBlockProcessingState = SERVICE_BLOCK_DATA; + serviceBlockDataBufferOffset = 0; + } else { // 0 size block. remain in service block header state + serviceNumber = 0; + } + } + break; + case SERVICE_BLOCK_EXT_HEADER: { + // extended service numbers + serviceNumber = (dtvccPkt[i] & 0x3F); // 6 bits + if (serviceBlockSize != 0) { + serviceBlockProcessingState = SERVICE_BLOCK_DATA; + serviceBlockDataBufferOffset = 0; + if (DEBUG) { + Log.d(TAG, "SERVICE_BLOCK_EXT_HEADER: serviceNumber = " + serviceNumber + + ", serviceBlockSize = " + serviceBlockSize); + } + } else { + // reset service block processing state + serviceBlockProcessingState = SERVICE_BLOCK_HEADER; + serviceNumber = 0; + } + } + break; + case SERVICE_BLOCK_DATA: { + serviceBlockDataBufferOffset++; + if (serviceNumber == selectedServiceNumber) { + processCCData(dtvccPkt[i]); + } - if (currentDtvCcPacket.currentIndex == (currentDtvCcPacket.packetSize * 2 - 1)) { - finalizeCurrentPacket(); + if (serviceBlockDataBufferOffset == serviceBlockSize) { + if (DEBUG) { + Log.d(TAG, "End of Service Block"); + } + resetServiceBlockState(); + if (cuesNeedUpdate) { + updateCues(); + } + } + } + break; } } } + // resets service block processing state + private void resetServiceBlockState() { + serviceBlockDataBufferOffset = 0; + serviceBlockProcessingState = SERVICE_BLOCK_HEADER; + serviceNumber = 0; + serviceBlockSize = 0; + } - private void finalizeCurrentPacket() { - if (currentDtvCcPacket == null) { - // No packet to finalize; + private void resetCCDataState() { + currentCommand = -1; + cuesNeedUpdate = false; + ccDataSkipCount = 0; + ccDataState = CC_DATA_STATE_COMMAND; + } + // process the closed caption data + // It manages the service input buffer queue for the selected service number. + // It has a RST and DLS pre-processing block + private void processCCData(byte data) { + // DLC and RST command pre-processor + int command = data & 0xFF; + if (command == COMMAND_RST || command == COMMAND_DLC) { + // cancel the delay + delayUs = 0; + // additionally RST command also clears the service input buffer queue. + if (command == COMMAND_RST) { + serviceInputBufferQ.reset(); + } + } + // now add the incoming data to service input buffer queue. + if (serviceInputBufferQ.canWrite()) { + // push to service input buffer queue + serviceInputBufferQ.write(data); + } else { + Log.w(TAG, "Service Input buffer FULL!!!"); + // if service input buffer is full, cancel delay command + delayUs = 0; + } + // detect timeout of delay and cancel it so that the processing happens immediately. + if (delayUs != 0 && inputTimestampUs - startOfDelayUs >= delayUs) { + delayUs = 0; + } + // if delay is active, skip processing the command + if (delayUs != 0) { return; } + // process the closed caption data stored in service input buffer + while (serviceInputBufferQ.canRead(1)) { + switch(ccDataState) { + case CC_DATA_STATE_COMMAND: { + currentCommand = serviceInputBufferQ.read(); + //if delay is not enabled or RST command, process command immediately + if (currentCommand == COMMAND_RST || delayUs == 0) { + cuesNeedUpdate |= handleCommand(currentCommand); + } else if (currentCommand == COMMAND_DLC || + serviceInputBufferQ.size() >= SERVICE_INPUT_BUFF_SIZE || + (inputTimestampUs - startOfDelayUs >= delayUs)) { + // cancel delay if DLC or service input buffer is full or delay timer expired + delayUs = 0; + // now process all delayed commands already stored in service input buffer queue. + cuesNeedUpdate |= handleCommand(currentCommand); + } else { + // we are in delay mode + } + } + break; + case CC_DATA_STATE_WAITING_FOR_PARAM: { + // we reset state to command here, because if the command still expects more params, + // handleCommand will change the state to waiting for param again + ccDataState = CC_DATA_STATE_COMMAND; + cuesNeedUpdate |= handleCommand(currentCommand); + } + break; + case CC_DATA_STATE_SKIPPING: { + serviceInputBufferQ.read(); + ccDataSkipCount--; + if (ccDataSkipCount == 0) { + ccDataState = CC_DATA_STATE_COMMAND; + isExtendedCommand = false; + } + } + break; + } + if (ccDataState == CC_DATA_STATE_WAITING_FOR_PARAM) { + break; + } + } - processCurrentPacket(); - currentDtvCcPacket = null; } - - private void processCurrentPacket() { - if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) { - Log.w(TAG, "DtvCcPacket ended prematurely; size is " + (currentDtvCcPacket.packetSize * 2 - 1) - + ", but current index is " + currentDtvCcPacket.currentIndex + " (sequence number " - + currentDtvCcPacket.sequenceNumber + "); ignoring packet"); - return; + private void skipBytes(int count) { + if (count > 0) { + ccDataState = CC_DATA_STATE_SKIPPING; + ccDataSkipCount = count; } - - 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); + } + private void printCommandName(int command) { + String commandName = null; + switch (command) { + case COMMAND_NUL: commandName = "Null"; break; + case COMMAND_ETX: commandName = "EndOfText"; break; + case COMMAND_BS: commandName = "Backspace"; break; + case COMMAND_FF: commandName = "FormFeed (Flush)"; break; + case COMMAND_CR: commandName = "Carriage Return"; break; + case COMMAND_HCR: commandName = "ClearLine"; break; + + case COMMAND_CW0: commandName = "SetCurrentWindow 0"; break; + case COMMAND_CW1: commandName = "SetCurrentWindow 1"; break; + case COMMAND_CW2: commandName = "SetCurrentWindow 2"; break; + case COMMAND_CW3: commandName = "SetCurrentWindow 3"; break; + case COMMAND_CW4: commandName = "SetCurrentWindow 4"; break; + case COMMAND_CW5: commandName = "SetCurrentWindow 5"; break; + case COMMAND_CW6: commandName = "SetCurrentWindow 6"; break; + case COMMAND_CW7: commandName = "SetCurrentWindow 7"; break; + + case COMMAND_CLW: commandName = "ClearWindows"; break; + case COMMAND_DSW: commandName = "DisplayWindows"; break; + case COMMAND_HDW: commandName = "HideWindows"; break; + case COMMAND_TGW: commandName = "ToggleWindows"; break; + case COMMAND_DLW: commandName = "DeleteWindows"; break; + case COMMAND_DLY: commandName = "Delay"; break; + case COMMAND_DLC: commandName = "DelayCancel"; break; + case COMMAND_RST: commandName = "Reset"; break; + case COMMAND_SPA: commandName = "SetPenAttributes"; break; + case COMMAND_SPC: commandName = "SetPenColor"; break; + case COMMAND_SPL: commandName = "SetPenLocation"; break; + case COMMAND_SWA: commandName = "SetWindowAttributes"; break; + case COMMAND_DF0: commandName = "DefineWindow 0"; break; + case COMMAND_DF1: commandName = "DefineWindow 1"; break; + case COMMAND_DF2: commandName = "DefineWindow 2"; break; + case COMMAND_DF3: commandName = "DefineWindow 3"; break; + case COMMAND_DS4: commandName = "DefineWindow 4"; break; + case COMMAND_DF5: commandName = "DefineWindow 5"; break; + case COMMAND_DF6: commandName = "DefineWindow 6"; break; + case COMMAND_DF7: commandName = "DefineWindow 7"; break; } - // 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; + if (commandName != null) { + Log.d(TAG, "handleCommand: " + commandName); } + } - if (serviceNumber != selectedServiceNumber) { - return; + private boolean handleCommand(int command) { + if (DEBUG) { + printCommandName(command); } - - // 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) { + boolean shouldUpdateCue = false; + try { + if (command == COMMAND_EXT1) { + // Read the extended command + if (!serviceInputBufferQ.canRead(1)) { + ccDataState = CC_DATA_STATE_WAITING_FOR_PARAM; + return shouldUpdateCue; + } + command = serviceInputBufferQ.read(); + // update the current command + currentCommand = command; + isExtendedCommand = true; + } + if (!isExtendedCommand) { if (command <= GROUP_C0_END) { - handleC0Command(command); - // If the C0 command was an ETX command, the cues are updated in handleC0Command. + // if the C0 command was an ETX command, the cues are updated in handleC0Command. + shouldUpdateCue |= handleC0Command(command); } else if (command <= GROUP_G0_END) { - handleG0Character(command); - cuesNeedUpdate = true; + shouldUpdateCue |= handleG0Character(command); } else if (command <= GROUP_C1_END) { - handleC1Command(command); - cuesNeedUpdate = true; + shouldUpdateCue |= handleC1Command(command); } else if (command <= GROUP_G1_END) { - handleG1Character(command); - cuesNeedUpdate = true; + shouldUpdateCue |= handleG1Character(command); } else { Log.w(TAG, "Invalid base command: " + command); } } else { - // Read the extended command - command = serviceBlockPacket.readBits(8); if (command <= GROUP_C2_END) { - handleC2Command(command); + shouldUpdateCue |= handleC2Command(command); } else if (command <= GROUP_G2_END) { - handleG2Character(command); - cuesNeedUpdate = true; + shouldUpdateCue |= handleG2Character(command); + isExtendedCommand = false; } else if (command <= GROUP_C3_END) { - handleC3Command(command); + shouldUpdateCue |= handleC3Command(command); } else if (command <= GROUP_G3_END) { - handleG3Character(command); - cuesNeedUpdate = true; + shouldUpdateCue |= handleG3Character(command); + isExtendedCommand = false; } else { Log.w(TAG, "Invalid extended command: " + command); + isExtendedCommand = false; } } + } catch (IllegalStateException ex) { + Log.w(TAG, "CEA708 stream seems to be broken, captions might be incorrect as data in invalid"); } - - if (cuesNeedUpdate) { - cues = getDisplayCues(); - } + return shouldUpdateCue; } - private void handleC0Command(int command) { + private boolean handleC0Command(int command) { switch (command) { case COMMAND_NUL: // Do nothing. break; case COMMAND_ETX: - cues = getDisplayCues(); + updateCues(); break; case COMMAND_BS: currentCueBuilder.backspace(); break; case COMMAND_FF: - resetCueBuilders(); + cueBuilders[currentWindow].clear(); + currentCueBuilder.setPenLocation(0, 0); break; case COMMAND_CR: currentCueBuilder.append('\n'); break; case COMMAND_HCR: - // TODO: Add support for this command. + currentCueBuilder.hcr(); break; default: if (command >= COMMAND_EXT1_START && command <= COMMAND_EXT1_END) { Log.w(TAG, "Currently unsupported COMMAND_EXT1 Command: " + command); - serviceBlockPacket.skipBits(8); + skipBytes(1); } else if (command >= COMMAND_P16_START && command <= COMMAND_P16_END) { Log.w(TAG, "Currently unsupported COMMAND_P16 Command: " + command); - serviceBlockPacket.skipBits(16); + skipBytes(2); } else { Log.w(TAG, "Invalid C0 command: " + command); } } + return false; } - private void handleC1Command(int command) { + private boolean handleC1Command(int command) { int window; + switch (command) { case COMMAND_CW0: case COMMAND_CW1: @@ -382,94 +597,184 @@ private void handleC1Command(int command) { if (currentWindow != window) { currentWindow = window; currentCueBuilder = cueBuilders[window]; + if (DEBUG) { + Log.d(TAG, "Setting current window to " + window); + } } break; - case COMMAND_CLW: - for (int i = 1; i <= NUM_WINDOWS; i++) { - if (serviceBlockPacket.readBit()) { - cueBuilders[NUM_WINDOWS - i].clear(); + case COMMAND_CLW: { + if (!serviceInputBufferQ.canRead(1)) { + ccDataState = CC_DATA_STATE_WAITING_FOR_PARAM; + break; + } + int windowMap = serviceInputBufferQ.read(); + for (int i = 0; i < NUM_WINDOWS; i++) { + if ((windowMap & (1 << i)) != 0) { + cueBuilders[i].clear(); + if (DEBUG) { + Log.d(TAG, "Clearing window with ID: " + i); + } } } break; - case COMMAND_DSW: - for (int i = 1; i <= NUM_WINDOWS; i++) { - if (serviceBlockPacket.readBit()) { - cueBuilders[NUM_WINDOWS - i].setVisibility(true); + } + case COMMAND_DSW: { + if (!serviceInputBufferQ.canRead(1)) { + ccDataState = CC_DATA_STATE_WAITING_FOR_PARAM; + break; + } + int windowMap = serviceInputBufferQ.read(); + for (int i = 0; i < NUM_WINDOWS; i++) { + if ((windowMap & (1 << i)) != 0) { + CueBuilder builder = cueBuilders[i]; + if (!builder.defined) { + if (DEBUG) { + Log.d(TAG, "DisplayWindow command skipped for undefined window" + " ID: " + i); + } + continue; + } + cueBuilders[i].setVisibility(true); + if (DEBUG) { + Log.d(TAG, "Showing window with ID: " + i); + } } } break; - case COMMAND_HDW: - for (int i = 1; i <= NUM_WINDOWS; i++) { - if (serviceBlockPacket.readBit()) { - cueBuilders[NUM_WINDOWS - i].setVisibility(false); + } + case COMMAND_HDW: { + if (!serviceInputBufferQ.canRead(1)) { + ccDataState = CC_DATA_STATE_WAITING_FOR_PARAM; + break; + } + int windowMap = serviceInputBufferQ.read(); + for (int i = 0; i < NUM_WINDOWS; i++) { + if ((windowMap & (1 << i)) != 0) { + cueBuilders[i].setVisibility(false); + if (DEBUG) { + Log.d(TAG, "Hiding window with ID: " + i); + } } } break; - case COMMAND_TGW: - for (int i = 1; i <= NUM_WINDOWS; i++) { - if (serviceBlockPacket.readBit()) { - CueBuilder cueBuilder = cueBuilders[NUM_WINDOWS - i]; - cueBuilder.setVisibility(!cueBuilder.isVisible()); + } + case COMMAND_TGW: { + if (!serviceInputBufferQ.canRead(1)) { + ccDataState = CC_DATA_STATE_WAITING_FOR_PARAM; + break; + } + int windowMap = serviceInputBufferQ.read(); + for (int i = 0; i < NUM_WINDOWS; i++) { + if ((windowMap & (1 << i)) != 0) { + CueBuilder builder = cueBuilders[i]; + if (!builder.defined) { + if (DEBUG) { + Log.d(TAG, "ToggleWindow command skipped for undefined window" + " ID: " + i); + } + continue; + } + builder.setVisibility(!builder.isVisible()); + if (DEBUG) { + Log.d(TAG, "Toggling window with ID: " + i); + } } } break; - case COMMAND_DLW: - for (int i = 1; i <= NUM_WINDOWS; i++) { - if (serviceBlockPacket.readBit()) { - cueBuilders[NUM_WINDOWS - i].reset(); + } + case COMMAND_DLW: { + if (!serviceInputBufferQ.canRead(1)) { + ccDataState = CC_DATA_STATE_WAITING_FOR_PARAM; + break; + } + int windowMap = serviceInputBufferQ.read(); + for (int i = 0; i < NUM_WINDOWS; i++) { + if ((windowMap & (1 << i)) != 0) { + cueBuilders[i].reset(); + if (DEBUG) { + Log.d(TAG, "Deleting window: " + i); + } } } break; - case COMMAND_DLY: - // TODO: Add support for delay commands. - serviceBlockPacket.skipBits(8); - break; - case COMMAND_DLC: - // TODO: Add support for delay commands. + } + case COMMAND_DLY: { + if (!serviceInputBufferQ.canRead(1)) { + ccDataState = CC_DATA_STATE_WAITING_FOR_PARAM; + break; + } + // delay is in tenths of a second + delayUs = serviceInputBufferQ.read() * (C.MICROS_PER_SECOND / 10); + startOfDelayUs = inputTimestampUs; break; - case COMMAND_RST: + } + case COMMAND_RST: { resetCueBuilders(); break; - case COMMAND_SPA: + } + case COMMAND_SPA: { + int paramLen = 2; if (!currentCueBuilder.isDefined()) { // ignore this command if the current window/cue isn't defined - serviceBlockPacket.skipBits(16); + skipBytes(paramLen); + } else if (!serviceInputBufferQ.canRead(paramLen)) { + // param not received yet. wait.... + ccDataState = CC_DATA_STATE_WAITING_FOR_PARAM; } else { handleSetPenAttributes(); } break; - case COMMAND_SPC: + } + case COMMAND_SPC: { + int paramLen = 3; if (!currentCueBuilder.isDefined()) { // ignore this command if the current window/cue isn't defined - serviceBlockPacket.skipBits(24); + skipBytes(paramLen); + } else if (!serviceInputBufferQ.canRead(paramLen)) { + // param not received yet. wait.... + ccDataState = CC_DATA_STATE_WAITING_FOR_PARAM; } else { handleSetPenColor(); } break; - case COMMAND_SPL: + } + case COMMAND_SPL: { + int paramLen = 2; if (!currentCueBuilder.isDefined()) { // ignore this command if the current window/cue isn't defined - serviceBlockPacket.skipBits(16); + skipBytes(paramLen); + } else if (!serviceInputBufferQ.canRead(paramLen)) { + // param not received yet. wait.... + ccDataState = CC_DATA_STATE_WAITING_FOR_PARAM; } else { handleSetPenLocation(); } break; - case COMMAND_SWA: + } + case COMMAND_SWA: { + int paramLen = 4; if (!currentCueBuilder.isDefined()) { // ignore this command if the current window/cue isn't defined - serviceBlockPacket.skipBits(32); + skipBytes(paramLen); + } else if (!serviceInputBufferQ.canRead(paramLen)) { + // param not received yet. wait.... + ccDataState = CC_DATA_STATE_WAITING_FOR_PARAM; } else { handleSetWindowAttributes(); } break; + } case COMMAND_DF0: case COMMAND_DF1: case COMMAND_DF2: case COMMAND_DF3: - case COMMAND_DF4: + case COMMAND_DS4: case COMMAND_DF5: case COMMAND_DF6: - case COMMAND_DF7: + case COMMAND_DF7: { + if (!serviceInputBufferQ.canRead(6)) { + // param not received yet. wait.... + ccDataState = CC_DATA_STATE_WAITING_FOR_PARAM; + break; + } window = (command - COMMAND_DF0); handleDefineWindow(window); // We also set the current window to the newly defined window. @@ -478,53 +783,64 @@ private void handleC1Command(int command) { currentCueBuilder = cueBuilders[window]; } break; + } default: Log.w(TAG, "Invalid C1 command: " + command); } + + // if the state is either skipping or waiting for param, don't render cue + return ccDataState == CC_DATA_STATE_COMMAND; } - private void handleC2Command(int command) { + private boolean handleC2Command(int command) { // C2 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes if (command <= 0x07) { // Do nothing. } else if (command <= 0x0F) { - serviceBlockPacket.skipBits(8); + skipBytes(1); } else if (command <= 0x17) { - serviceBlockPacket.skipBits(16); + skipBytes(2); } else if (command <= 0x1F) { - serviceBlockPacket.skipBits(24); + skipBytes(3); } + return false; } - private void handleC3Command(int command) { + private boolean handleC3Command(int command) { // C3 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes if (command <= 0x87) { - serviceBlockPacket.skipBits(32); + skipBytes(4); } else if (command <= 0x8F) { - serviceBlockPacket.skipBits(40); + skipBytes(5); } else if (command <= 0x9F) { // 90-9F are variable length codes; the first byte defines the header with the first // 2 bits specifying the type and the last 6 bits specifying the remaining length of the // command in bytes - serviceBlockPacket.skipBits(2); - int length = serviceBlockPacket.readBits(6); - serviceBlockPacket.skipBits(8 * length); + if (!serviceInputBufferQ.canRead(1)) { + ccDataState = CC_DATA_STATE_WAITING_FOR_PARAM; + return false; + } + int val = serviceInputBufferQ.read(); + skipBytes((int) (val & 0x3F)); // 6 bits } + return false; } - private void handleG0Character(int characterCode) { + private boolean handleG0Character(int characterCode) { if (characterCode == CHARACTER_MN) { currentCueBuilder.append('\u266B'); } else { currentCueBuilder.append((char) (characterCode & 0xFF)); } + return true; } - private void handleG1Character(int characterCode) { + private boolean handleG1Character(int characterCode) { currentCueBuilder.append((char) (characterCode & 0xFF)); + return true; } - private void handleG2Character(int characterCode) { + private boolean handleG2Character(int characterCode) { switch (characterCode) { case CHARACTER_TSP: currentCueBuilder.append('\u0020'); @@ -608,10 +924,12 @@ private void handleG2Character(int characterCode) { Log.w(TAG, "Invalid G2 character: " + characterCode); // The CEA-708 specification doesn't specify what to do in the case of an unexpected // value in the G2 character range, so we ignore it. + return false; } + return true; } - private void handleG3Character(int characterCode) { + private boolean handleG3Character(int characterCode) { if (characterCode == 0xA0) { currentCueBuilder.append('\u33C4'); } else { @@ -619,19 +937,21 @@ private void handleG3Character(int characterCode) { // Substitute any unsupported G3 character with an underscore as per CEA-708 specification. currentCueBuilder.append('_'); } + return true; } - private void handleSetPenAttributes() { // the SetPenAttributes command contains 2 bytes of data // first byte - int textTag = serviceBlockPacket.readBits(4); - int offset = serviceBlockPacket.readBits(2); - int penSize = serviceBlockPacket.readBits(2); + int param = serviceInputBufferQ.read(); + int textTag = (param & 0xF0) >> 4; // xxxx 0000 + int offset = (param & 0x0C) >> 2; // 0000 xx00 + int penSize = (param & 0x03); // 0000 00xx // second byte - boolean italicsToggle = serviceBlockPacket.readBit(); - boolean underlineToggle = serviceBlockPacket.readBit(); - int edgeType = serviceBlockPacket.readBits(3); - int fontStyle = serviceBlockPacket.readBits(3); + param = serviceInputBufferQ.read(); + boolean italicsToggle = ((param & 0x80) >> 7 == 1); // x000 0000 + boolean underlineToggle = ((param & 0x40) >> 6 == 1); // 0x00 0000 + int edgeType = (param & 0x38) >> 3; // 00xx x000 + int fontStyle = (param & 0x07); // 0000 0xxx currentCueBuilder.setPenAttributes(textTag, offset, penSize, italicsToggle, underlineToggle, edgeType, fontStyle); @@ -640,24 +960,27 @@ private void handleSetPenAttributes() { private void handleSetPenColor() { // the SetPenColor command contains 3 bytes of data // first byte - int foregroundO = serviceBlockPacket.readBits(2); - int foregroundR = serviceBlockPacket.readBits(2); - int foregroundG = serviceBlockPacket.readBits(2); - int foregroundB = serviceBlockPacket.readBits(2); + int param = serviceInputBufferQ.read(); + int foregroundA = (param & 0xC0) >> 6; // xx00 0000 + int foregroundR = (param & 0x30) >> 4; // 00xx 0000 + int foregroundG = (param & 0x0C) >> 2; // 0000 xx00 + int foregroundB = (param & 0x03); // 0000 00xx int foregroundColor = CueBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB, - foregroundO); + foregroundA); // second byte - int backgroundO = serviceBlockPacket.readBits(2); - int backgroundR = serviceBlockPacket.readBits(2); - int backgroundG = serviceBlockPacket.readBits(2); - int backgroundB = serviceBlockPacket.readBits(2); + param = serviceInputBufferQ.read(); + int backgroundA = (param & 0xC0) >> 6; // xx00 0000 + int backgroundR = (param & 0x30) >> 4; // 00xx 0000 + int backgroundG = (param & 0x0C) >> 2; // 0000 xx00 + int backgroundB = (param & 0x03); // 0000 00xx int backgroundColor = CueBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB, - backgroundO); + backgroundA); // third byte - serviceBlockPacket.skipBits(2); // null padding - int edgeR = serviceBlockPacket.readBits(2); - int edgeG = serviceBlockPacket.readBits(2); - int edgeB = serviceBlockPacket.readBits(2); + param = serviceInputBufferQ.read(); + // skip 2 bits null padding // xx00 0000 + int edgeR = (param & 0x30) >> 4; // 00xx 0000 + int edgeG = (param & 0x0C) >> 2; // 0000 xx00 + int edgeB = (param & 0x03); // 0000 00xx int edgeColor = CueBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB); currentCueBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor); @@ -666,11 +989,13 @@ private void handleSetPenColor() { private void handleSetPenLocation() { // the SetPenLocation command contains 2 bytes of data // first byte - serviceBlockPacket.skipBits(4); - int row = serviceBlockPacket.readBits(4); + int param = serviceInputBufferQ.read(); + // skip 4 bits // xxxx 0000 + int row = (param & 0x0F); // 0000 xxxx // second byte - serviceBlockPacket.skipBits(2); - int column = serviceBlockPacket.readBits(6); + param = serviceInputBufferQ.read(); + // skip 2 bits // xx00 0000 + int column = (param & 0x3F); // 00xx xxxx currentCueBuilder.setPenLocation(row, column); } @@ -678,28 +1003,31 @@ private void handleSetPenLocation() { private void handleSetWindowAttributes() { // the SetWindowAttributes command contains 4 bytes of data // first byte - int fillO = serviceBlockPacket.readBits(2); - int fillR = serviceBlockPacket.readBits(2); - int fillG = serviceBlockPacket.readBits(2); - int fillB = serviceBlockPacket.readBits(2); - int fillColor = CueBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO); + int param = serviceInputBufferQ.read(); + int fillA = (param & 0xC0) >> 6; // xx00 0000 + int fillR = (param & 0x30) >> 4; // 00xx 0000 + int fillG = (param & 0x0C) >> 2; // 0000 xx00 + int fillB = (param & 0x03); // 0000 00xx + int fillColor = CueBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillA); // second byte - int borderType = serviceBlockPacket.readBits(2); // only the lower 2 bits of borderType - int borderR = serviceBlockPacket.readBits(2); - int borderG = serviceBlockPacket.readBits(2); - int borderB = serviceBlockPacket.readBits(2); + param = serviceInputBufferQ.read(); + int borderType = (param & 0xC0) >> 6; // xx00 0000 + int borderR = (param & 0x30) >> 4; // 00xx 0000 + int borderG = (param & 0x0C) >> 2; // 0000 xx00 + int borderB = (param & 0x03); // 0000 00xx int borderColor = CueBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB); // third byte - if (serviceBlockPacket.readBit()) { + param = serviceInputBufferQ.read(); + if (((param & 0x80) >> 7 == 1)) { // x000 0000 borderType |= 0x04; // set the top bit of the 3-bit borderType } - boolean wordWrapToggle = serviceBlockPacket.readBit(); - int printDirection = serviceBlockPacket.readBits(2); - int scrollDirection = serviceBlockPacket.readBits(2); - int justification = serviceBlockPacket.readBits(2); + boolean wordWrapToggle = ((param & 0x40) >> 6 == 1); // 0x00 0000 + int printDirection = (param & 0x30) >> 4; // 00xx 0000 + int scrollDirection = (param & 0x0C) >> 2; // 0000 xx00 + int justification = (param & 0x03); // 0000 00xx // fourth byte // Note that we don't intend to support display effects - serviceBlockPacket.skipBits(8); // effectSpeed(4), effectDirection(2), displayEffect(2) + param = serviceInputBufferQ.read(); // skip display effects currentCueBuilder.setWindowAttributes(fillColor, borderColor, wordWrapToggle, borderType, printDirection, scrollDirection, justification); @@ -710,63 +1038,54 @@ private void handleDefineWindow(int window) { // the DefineWindow command contains 6 bytes of data // first byte - serviceBlockPacket.skipBits(2); // null padding - boolean visible = serviceBlockPacket.readBit(); - boolean rowLock = serviceBlockPacket.readBit(); - boolean columnLock = serviceBlockPacket.readBit(); - int priority = serviceBlockPacket.readBits(3); + int param = serviceInputBufferQ.read(); + // skip 2 bits null padding // xx00 0000 + boolean visible = ((param & 0x20) >> 5 == 1); // 00x0 0000 + boolean rowLock = ((param & 0x10) >> 4 == 1); // 000x 0000 + boolean columnLock = ((param & 0x08) >> 3 == 1); // 0000 x000 + int priority = (param & 0x07); // 0000 0xxx // second byte - boolean relativePositioning = serviceBlockPacket.readBit(); - int verticalAnchor = serviceBlockPacket.readBits(7); + param = serviceInputBufferQ.read(); + boolean relativePositioning = ((param & 0x80) >> 7 == 1); // x000 0000 + int verticalAnchor = (param & 0x7F); // 0xxx xxxx // third byte - int horizontalAnchor = serviceBlockPacket.readBits(8); + int horizontalAnchor = serviceInputBufferQ.read(); // xxxx xxxx // fourth byte - int anchorId = serviceBlockPacket.readBits(4); - int rowCount = serviceBlockPacket.readBits(4); + param = serviceInputBufferQ.read(); + int anchorId = (param & 0xF0) >> 4; // xxxx 0000 + int rowCount = (param & 0x0F); // 0000 xxxx // fifth byte - serviceBlockPacket.skipBits(2); // null padding - int columnCount = serviceBlockPacket.readBits(6); + param = serviceInputBufferQ.read(); + // skip 2 bits null padding // xx00 0000 + int columnCount = (param & 0x3F); // 00xx xxxx // sixth byte - serviceBlockPacket.skipBits(2); // null padding - int windowStyle = serviceBlockPacket.readBits(3); - int penStyle = serviceBlockPacket.readBits(3); + param = serviceInputBufferQ.read(); + // skip 2 bits null padding // xx00 0000 + int windowStyle = (param & 0x38) >> 3; // 00xx x000 + int penStyle = (param & 0x07); // 0000 0xxx cueBuilder.defineWindow(visible, rowLock, columnLock, priority, relativePositioning, verticalAnchor, horizontalAnchor, rowCount, columnCount, anchorId, windowStyle, penStyle); } - private List getDisplayCues() { List displayCues = new ArrayList<>(); for (int i = 0; i < NUM_WINDOWS; i++) { - if (!cueBuilders[i].isEmpty() && cueBuilders[i].isVisible()) { + // we need to render empty window, so allow empty captions. + if (cueBuilders[i].isVisible()) { displayCues.add(cueBuilders[i].build()); } } Collections.sort(displayCues); - return Collections.unmodifiableList(displayCues); + return Collections.unmodifiableList(displayCues); } private void resetCueBuilders() { for (int i = 0; i < NUM_WINDOWS; i++) { cueBuilders[i].reset(); } - } - - private static final class DtvCcPacket { - - public final int sequenceNumber; - public final int packetSize; - public final byte[] packetData; - - int currentIndex; - - public DtvCcPacket(int sequenceNumber, int packetSize) { - this.sequenceNumber = sequenceNumber; - this.packetSize = packetSize; - packetData = new byte[2 * packetSize - 1]; - currentIndex = 0; - } - + delayUs = 0; + //serviceInputBufLen = 0; + serviceInputBufferQ.reset(); } // TODO: There is a lot of overlap between Cea708Decoder.CueBuilder and Cea608Decoder.CueBuilder @@ -910,7 +1229,9 @@ public void reset() { foregroundColor = COLOR_SOLID_WHITE; backgroundColor = COLOR_SOLID_BLACK; } - + public void hcr() { + captionStringBuilder.clear(); + } public void clear() { rolledUpCaptions.clear(); captionStringBuilder.clear(); @@ -1091,7 +1412,7 @@ public void append(char text) { } while ((rowLock && (rolledUpCaptions.size() >= rowCount)) - || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) { + || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) { rolledUpCaptions.remove(0); } } else { @@ -1130,11 +1451,6 @@ public SpannableString buildSpannableString() { } public Cea708Cue build() { - if (isEmpty()) { - // The cue is empty. - return null; - } - SpannableStringBuilder cueString = new SpannableStringBuilder(); // Add any rolled up captions, separated by new lines. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java index 3efc16bdd0f..cd60c14bd89 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.text.cea; +import android.util.Log; + import android.support.annotation.NonNull; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -25,24 +27,34 @@ import com.google.android.exoplayer2.text.SubtitleOutputBuffer; import com.google.android.exoplayer2.util.Assertions; import java.util.ArrayDeque; +import java.util.LinkedList; import java.util.PriorityQueue; /** * Base class for subtitle parsers for CEA captions. */ /* package */ abstract class CeaDecoder implements SubtitleDecoder { - + private static final String TAG = CeaDecoder.class.getSimpleName(); private static final int NUM_INPUT_BUFFERS = 10; - private static final int NUM_OUTPUT_BUFFERS = 2; + // since we handle delay commands, we need more output buffers + // We tried 8 buffers, but that still failed in some Sarnoff tests. + // So using 16 for now untill we find another failing Sarnoff test due to this + // This is still a workaround. The right way to fix this is to re-design the decoder. + private static final int NUM_OUTPUT_BUFFERS = 16; + private static final int MIN_REORDER_DELAY = NUM_INPUT_BUFFERS / 2; private final ArrayDeque availableInputBuffers; private final ArrayDeque availableOutputBuffers; private final PriorityQueue queuedInputBuffers; + private final LinkedList queuedOutputBuffers; private CeaInputBuffer dequeuedInputBuffer; - private long playbackPositionUs; + protected long playbackPositionUs; private long queuedInputBufferCount; + private long lastDecodedTimestampUs; + private boolean isEndOfStream; + public CeaDecoder() { availableInputBuffers = new ArrayDeque<>(); for (int i = 0; i < NUM_INPUT_BUFFERS; i++) { @@ -53,6 +65,7 @@ public CeaDecoder() { availableOutputBuffers.add(new CeaOutputBuffer()); } queuedInputBuffers = new PriorityQueue<>(); + queuedOutputBuffers = new LinkedList<>(); } @Override @@ -76,7 +89,9 @@ public SubtitleInputBuffer dequeueInputBuffer() throws SubtitleDecoderException @Override public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException { Assertions.checkArgument(inputBuffer == dequeuedInputBuffer); - if (inputBuffer.isDecodeOnly()) { + isEndOfStream = inputBuffer.isEndOfStream(); + if (inputBuffer.isDecodeOnly() || + (inputBuffer.timeUs < lastDecodedTimestampUs)) { // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow // for decoding to begin mid-stream. releaseInputBuffer(dequeuedInputBuffer); @@ -89,44 +104,34 @@ public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDec @Override public SubtitleOutputBuffer dequeueOutputBuffer() throws SubtitleDecoderException { - if (availableOutputBuffers.isEmpty()) { - return null; - } // iterate through all available input buffers whose timestamps are less than or equal // to the current playback position; processing input buffers for future content should // be deferred until they would be applicable while (!queuedInputBuffers.isEmpty() && queuedInputBuffers.peek().timeUs <= playbackPositionUs) { + + if(!isEndOfStream && queuedInputBuffers.size() < MIN_REORDER_DELAY) { + break; + } CeaInputBuffer inputBuffer = queuedInputBuffers.poll(); // If the input buffer indicates we've reached the end of the stream, we can // return immediately with an output buffer propagating that if (inputBuffer.isEndOfStream()) { SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst(); - outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + if (outputBuffer != null) { + outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + } releaseInputBuffer(inputBuffer); return outputBuffer; } + lastDecodedTimestampUs = inputBuffer.timeUs; decode(inputBuffer); - // check if we have any caption updates to report - if (isNewSubtitleDataAvailable()) { - // Even if the subtitle is decode-only; we need to generate it to consume the data so it - // isn't accidentally prepended to the next subtitle - Subtitle subtitle = createSubtitle(); - if (!inputBuffer.isDecodeOnly()) { - SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst(); - outputBuffer.setContent(inputBuffer.timeUs, subtitle, Format.OFFSET_SAMPLE_RELATIVE); - releaseInputBuffer(inputBuffer); - return outputBuffer; - } - } - releaseInputBuffer(inputBuffer); } - - return null; + return queuedOutputBuffers.pollFirst(); } private void releaseInputBuffer(CeaInputBuffer inputBuffer) { @@ -139,10 +144,25 @@ protected void releaseOutputBuffer(SubtitleOutputBuffer outputBuffer) { availableOutputBuffers.add(outputBuffer); } + public void onNewSubtitleDataAvailable(long timeUs) { + if (isNewSubtitleDataAvailable()) { + SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst(); + if (outputBuffer != null) { + Subtitle subtitle = createSubtitle(); + outputBuffer.setContent(timeUs, subtitle, Format.OFFSET_SAMPLE_RELATIVE); + queuedOutputBuffers.add(outputBuffer); + } else { + Log.w(TAG, "Insufficient Output Buffers for subtitle!!!"); + } + } + } + @Override public void flush() { queuedInputBufferCount = 0; playbackPositionUs = 0; + lastDecodedTimestampUs = 0; + isEndOfStream = false; while (!queuedInputBuffers.isEmpty()) { releaseInputBuffer(queuedInputBuffers.poll()); } @@ -174,7 +194,7 @@ public void release() { protected abstract void decode(SubtitleInputBuffer inputBuffer); private static final class CeaInputBuffer extends SubtitleInputBuffer - implements Comparable { + implements Comparable { private long queuedInputBufferCount; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/CircularByteQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/util/CircularByteQueue.java new file mode 100644 index 00000000000..614cf1f1ada --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/CircularByteQueue.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.util; + +import android.util.Log; +/** + * Wraps a byte array, providing methods that allows it to be used as a circular queue + */ +public final class CircularByteQueue { + // circular array + private byte[] data; + // maximum capacity of the queue + private int capacity; + // current size of the queue + private int availableCount; + // offset to write into + private int writeOffset; + // offset to read into + private int readOffset; + + /** + * Creates a new instance. + */ + public CircularByteQueue(int capacity) { + // allocate one extra element to handle the queue wrap-arounds + this.data = new byte[capacity + 1]; + this.capacity = capacity + 1; + } + + /** + * clears the queue and resets read/write offsets + * + */ + public void reset() { + readOffset = 0; + availableCount = 0; + writeOffset = 0; + } + + public boolean canWrite() { + if (availableCount == capacity - 1) { + return false; + } + return true; + } + + public boolean canRead(int count) { + if (availableCount < count) { + return false; + } + return true; + } + + public boolean write(byte value) { + // check for space and early return + if (!canWrite()) { + return false; + } + data[writeOffset] = value; + availableCount++; + writeOffset = (writeOffset + 1) % capacity; + return true; + } + + public int read() { + // check for space and early return + if (!canRead(1)) { + return 0; + } + // byte to unsigned int + int value = data[readOffset] & 0xff; + availableCount--; + readOffset = (readOffset + 1) % capacity; + return value; + } + + public int size() { + return availableCount; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("capacity = " + capacity + " ,availableCount = " + availableCount + ", readOffset = " +readOffset + + ", writeOffset = " + writeOffset); + + for (int i= 0; i < data.length; i++) { + builder.append(" data["+ i +"] = " + data[i]); + } + return builder.toString(); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/CircularByteQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/CircularByteQueueTest.java new file mode 100644 index 00000000000..2e7c4268745 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/CircularByteQueueTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.util; + +import android.util.Log; + +import junit.framework.Test; +import junit.framework.TestCase; + +/** + * Tests for {@link ParsableByteArray}. + */ +public class CircularByteQueueTest extends TestCase { + + private static final byte[] TEST_DATA = + new byte[]{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + + public void testEmptyRead() { + CircularByteQueue testQueue = new CircularByteQueue(8); + + // queue is empty. must fail + assertFalse(testQueue.canRead(1)); + + for (int i = 0; i < TEST_DATA.length; i++) { + assertTrue(testQueue.write(TEST_DATA[i])); + } + // queue is full - check size + assertEquals(testQueue.size(), TEST_DATA.length); + + assertTrue(testQueue.canRead(1)); + assertTrue(testQueue.canRead(8)); + + // cannot read beyond queue size + assertFalse(testQueue.canRead(9)); + + for (int i = 0; i < TEST_DATA.length; i++) { + assertTrue(testQueue.canRead(1)); + assertEquals(TEST_DATA[i],testQueue.read()); + } + // queue is empty- check can read again + assertFalse(testQueue.canRead(1)); + } + + public void testFullWrite() { + CircularByteQueue testQueue = new CircularByteQueue(8); + + assertFalse(testQueue.canRead(1)); + + for (int i = 0; i < TEST_DATA.length; i++) { + assertTrue(testQueue.write(TEST_DATA[i])); + } + // queue is full here + + // writing when queue is full should fail + assertFalse(testQueue.write(TEST_DATA[0])); + + // read one byte - check it matches first queued data + assertEquals(testQueue.read(), TEST_DATA[0]); + + // queue has 1 space now + + // now write should succeed + assertTrue(testQueue.write(TEST_DATA[0])); + + // queue is full now + // writing when queue is full should fail + assertFalse(testQueue.write(TEST_DATA[0])); + + } + + public void testReadWriteCircular1() { + CircularByteQueue testQueue = new CircularByteQueue(8); + // write two and read two and check sanity + for (int i = 0; i < TEST_DATA.length / 2; i += 2) { + assertTrue(testQueue.write(TEST_DATA[i])); + assertTrue(testQueue.write(TEST_DATA[i + 1])); + assertEquals(testQueue.read(),TEST_DATA[i]); + assertEquals(testQueue.read(),TEST_DATA[i + 1]); + } + assertEquals(testQueue.size(), 0); + // starts wrapping around, write two and read two + for (int i = 0; i < TEST_DATA.length / 2; i += 2) { + assertTrue(testQueue.write(TEST_DATA[i])); + assertTrue(testQueue.write(TEST_DATA[i + 1])); + assertEquals(testQueue.read(),TEST_DATA[i]); + assertEquals(testQueue.read(),TEST_DATA[i + 1]); + } + assertEquals(testQueue.size(), 0); + } + public void testReadWriteCircular2() { + CircularByteQueue testQueue = new CircularByteQueue(8); + // write 7 bytes + for (int i = 0; i < 7; i++ ) { + assertTrue(testQueue.write(TEST_DATA[i])); + } + // check size is 7 + assertEquals(testQueue.size(), 7); + + // read 3 bytes + for (int i = 0; i < 3; i++ ) { + assertEquals(testQueue.read(), TEST_DATA[i]); + } + // check size is 4 + assertEquals(testQueue.size(), 4); + + // write 4 bytes: 7 and 0,1,2 + assertTrue(testQueue.write(TEST_DATA[7])); + for (int i = 0; i < 3; i++ ) { + assertTrue(testQueue.write(TEST_DATA[i])); + } + // check you can't write further + assertFalse(testQueue.write(TEST_DATA[7])); + // read all + for (int i = 3; i < 8; i++ ) { + assertEquals(testQueue.read(), TEST_DATA[i]); + } + for (int i = 0; i < 3; i++ ) { + assertEquals(testQueue.read(), TEST_DATA[i]); + } + // check queue is empty + assertEquals(testQueue.size(), 0); + } +}