diff --git a/lib/cea/cea708_service.js b/lib/cea/cea708_service.js index f022f17fc4..545542012d 100644 --- a/lib/cea/cea708_service.js +++ b/lib/cea/cea708_service.js @@ -45,7 +45,7 @@ shaka.cea.Cea708Service = class { /** * Processes a CEA-708 control code. * @param {!shaka.cea.DtvccPacket} dtvccPacket - * @return {?shaka.extern.ICaptionDecoder.ClosedCaption} + * @return {!Array} * @throws {!shaka.util.Error} */ handleCea708ControlCode(dtvccPacket) { @@ -79,7 +79,7 @@ shaka.cea.Cea708Service = class { this.handleG3_(controlCode & 0xff); } - return null; + return []; } /** @@ -158,13 +158,13 @@ shaka.cea.Cea708Service = class { * @param {!shaka.cea.DtvccPacket} dtvccPacket * @param {number} controlCode * @param {number} pts - * @return {?shaka.extern.ICaptionDecoder.ClosedCaption} + * @return {!Array} * @private */ handleC0_(dtvccPacket, controlCode, pts) { // All these commands pertain to the current window, so ensure it exists. if (!this.currentWindow_) { - return null; + return []; } if (controlCode == 0x18) { @@ -185,7 +185,7 @@ shaka.cea.Cea708Service = class { // Takes a unicode hex string and creates a single character. const char = String.fromCharCode(parseInt(unicode, 16)); this.currentWindow_.setCharacter(char); - return null; + return []; } else { dtvccPacket.rewind(2); } @@ -224,7 +224,7 @@ shaka.cea.Cea708Service = class { window.setPenLocation(0, 0); break; } - return parsedClosedCaption; + return parsedClosedCaption ? [parsedClosedCaption] : []; } /** @@ -233,7 +233,7 @@ shaka.cea.Cea708Service = class { * @param {!shaka.cea.DtvccPacket} dtvccPacket * @param {number} captionCommand * @param {number} pts in seconds - * @return {?shaka.extern.ICaptionDecoder.ClosedCaption} + * @return {!Array} * @throws {!shaka.util.Error} a possible out-of-range buffer read. * @private */ @@ -274,7 +274,7 @@ shaka.cea.Cea708Service = class { const windowNum = (captionCommand & 0x0f) - 8; this.defineWindow_(dtvccPacket, windowNum, pts); } - return null; + return []; } /** @@ -344,22 +344,26 @@ shaka.cea.Cea708Service = class { /** * @param {number} windowsBitmap * @param {number} pts - * @return {?shaka.extern.ICaptionDecoder.ClosedCaption} + * @return {!Array} * @private */ clearWindows_(windowsBitmap, pts) { - let parsedClosedCaption = null; + const parsedClosedCaptions = []; // Clears windows from the 8 bit bitmap. for (const windowId of this.getSpecifiedWindowIds_(windowsBitmap)) { // If window visible and being cleared, emit buffer and reset start time! const window = this.windows_[windowId]; if (window.isVisible()) { - parsedClosedCaption = window.forceEmit(pts, this.serviceNumber_); + const newParsedClosedCaption = + window.forceEmit(pts, this.serviceNumber_); + if (newParsedClosedCaption) { + parsedClosedCaptions.push(newParsedClosedCaption); + } } window.resetMemory(); } - return parsedClosedCaption; + return parsedClosedCaptions; } /** @@ -382,7 +386,7 @@ shaka.cea.Cea708Service = class { /** * @param {number} windowsBitmap * @param {number} pts - * @return {?shaka.extern.ICaptionDecoder.ClosedCaption} + * @return {!Array} * @private */ hideWindows_(windowsBitmap, pts) { @@ -397,13 +401,13 @@ shaka.cea.Cea708Service = class { } window.hide(); } - return parsedClosedCaption; + return parsedClosedCaption ? [parsedClosedCaption] : []; } /** * @param {number} windowsBitmap * @param {number} pts - * @return {?shaka.extern.ICaptionDecoder.ClosedCaption} + * @return {!Array} * @private */ toggleWindows_(windowsBitmap, pts) { @@ -422,42 +426,46 @@ shaka.cea.Cea708Service = class { window.toggle(); } - return parsedClosedCaption; + return parsedClosedCaption ? [parsedClosedCaption] : []; } /** * @param {number} windowsBitmap * @param {number} pts - * @return {?shaka.extern.ICaptionDecoder.ClosedCaption} + * @return {!Array} * @private */ deleteWindows_(windowsBitmap, pts) { - let parsedClosedCaption = null; + const parsedClosedCaptions = []; // Deletes windows from the 8 bit bitmap. for (const windowId of this.getSpecifiedWindowIds_(windowsBitmap)) { const window = this.windows_[windowId]; if (window.isVisible()) { // We are turning off the visibility, emit! - parsedClosedCaption = window.forceEmit(pts, this.serviceNumber_); + const newParsedClosedCaption = + window.forceEmit(pts, this.serviceNumber_); + if (newParsedClosedCaption) { + parsedClosedCaptions.push(newParsedClosedCaption); + } } // Delete the window from the list of windows this.windows_[windowId] = null; } - return parsedClosedCaption; + return parsedClosedCaptions; } /** * Emits anything currently present in any of the windows, and then * deletes all windows, cancels all delays, reinitializes the service. * @param {number} pts - * @return {?shaka.extern.ICaptionDecoder.ClosedCaption} + * @return {!Array} * @private */ reset_(pts) { const allWindowsBitmap = 0xff; // All windows should be deleted. - const caption = this.deleteWindows_(allWindowsBitmap, pts); + const captions = this.deleteWindows_(allWindowsBitmap, pts); this.clear(); - return caption; + return captions; } /** diff --git a/lib/cea/cea_decoder.js b/lib/cea/cea_decoder.js index 2d330835cd..890712419c 100644 --- a/lib/cea/cea_decoder.js +++ b/lib/cea/cea_decoder.js @@ -371,10 +371,8 @@ shaka.cea.CeaDecoder = class { // Execute this loop `blockSize` times, to decode the control codes. while (dtvccPacket.getPosition() - startPos < blockSize) { - const closedCaption = service.handleCea708ControlCode(dtvccPacket); - if (closedCaption) { - parsedClosedCaptions.push(closedCaption); - } + const closedCaptions = service.handleCea708ControlCode(dtvccPacket); + parsedClosedCaptions.push(...closedCaptions); } // position < end of block } // serviceNumber != 0 } // hasMoreData diff --git a/test/cea/cea708_service_unit.js b/test/cea/cea708_service_unit.js index 0eef26fdc1..7fd30540bb 100644 --- a/test/cea/cea708_service_unit.js +++ b/test/cea/cea708_service_unit.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -// cspell:ignore testtest +// cspell:ignore testtest toasttesttest toasttest describe('Cea708Service', () => { const CeaUtils = shaka.test.CeaUtils; @@ -27,6 +27,10 @@ describe('Cea708Service', () => { 0x98, 0x38, 0x00, 0x00, 0x1f, 0x1f, 0x00, ]; + const defineWindow2 = [ + 0x99, 0x38, 0x00, 0x00, 0x1f, 0x1f, 0x00, + ]; + /** @type {number} */ const startTime = 1; @@ -44,6 +48,7 @@ describe('Cea708Service', () => { /** @type {number} */ const windowId = 0; + const windowId2 = 1; /** @type {number} */ const rowCount = 16; @@ -81,16 +86,16 @@ describe('Cea708Service', () => { * @return {!Array} */ const getCaptionsFromPackets = (service, ...packets) => { - const captions = []; + const allCaptions = []; for (const packet of packets) { while (packet.hasMoreData()) { - const caption = service.handleCea708ControlCode(packet); - if (caption) { - captions.push(caption); + const captions = service.handleCea708ControlCode(packet); + if (captions) { + allCaptions.push(...captions); } } } - return captions; + return allCaptions; }; beforeEach(() => { @@ -638,6 +643,11 @@ describe('Cea708Service', () => { 0x74, 0x65, 0x73, 0x74, // t, e, s, t ]; + const textControlCodes2 = [ + // Series of G0 control codes that add text. + 0x74, 0x6F, 0x61, 0x73, 0x74, // t, o, a, s, t + ]; + // These commands affect ALL windows, per the 0xff bitmap. const toggleWindow = [0x8b, 0xff]; const displayWindow = [0x89, 0xff]; @@ -697,6 +707,116 @@ describe('Cea708Service', () => { expect(captions).toEqual(expectedCaptions); }); + it('if more than one window, ' + + 'delete should extract cues on all windows', () => { + // Define a visible window, and add some text + const packet1a = createCea708PacketFromBytes(defineWindow, time1); + const packet1b = createCea708PacketFromBytes(textControlCodes, time1); + const packet1c = createCea708PacketFromBytes(textControlCodes, time2); + + // Define a second visible window, and add some text + const packet2a = createCea708PacketFromBytes(defineWindow2, time1); + const packet2b = createCea708PacketFromBytes(textControlCodes2, time1); + const packet3a = createCea708PacketFromBytes(textControlCodes, time2); + + // Delete all the windows. + // This should force the first window to emit 'testtest' and the second + // to emit 'toasttesttest'. + const packet4 = createCea708PacketFromBytes(deleteWindow, time2); + + const text1 = 'testtest'; + const text2 = 'toasttest'; + const topLevelCue1 = CeaUtils.createWindowedCue( + /* startTime= */ time1, /* endTime= */ time2, '', + serviceNumber, windowId, rowCount, colCount, anchorId, + ); + topLevelCue1.nestedCues = [ + CeaUtils.createDefaultCue( + /* startTime= */ time1, /* endTime= */ time2, /* payload= */ text1), + ]; + + const topLevelCue2 = CeaUtils.createWindowedCue( + /* startTime= */ time1, /* endTime= */ time2, '', + serviceNumber, windowId2, rowCount, colCount, anchorId, + ); + topLevelCue2.nestedCues = [ + CeaUtils.createDefaultCue( + /* startTime= */ time1, /* endTime= */ time2, /* payload= */ text2), + ]; + + const expectedCaptions = [ + { + stream, + cue: topLevelCue1, + }, + + { + stream, + cue: topLevelCue2, + }, + ]; + + const captions = getCaptionsFromPackets( + service, packet1a, packet1b, packet1c, packet2a, packet2b, + packet3a, packet4); + expect(captions).toEqual(expectedCaptions); + }); + + it('if more than one window, ' + + 'clear should extract cues on all windows', () => { + // Define a visible window, and add some text + const packet1a = createCea708PacketFromBytes(defineWindow, time1); + const packet1b = createCea708PacketFromBytes(textControlCodes, time1); + const packet1c = createCea708PacketFromBytes(textControlCodes, time2); + + // Define a second visible window, and add some text + const packet2a = createCea708PacketFromBytes(defineWindow2, time1); + const packet2b = createCea708PacketFromBytes(textControlCodes2, time1); + const packet3a = createCea708PacketFromBytes(textControlCodes, time2); + + // Delete all the windows. + // This should force the first window to emit 'testtest' and the second + // to emit 'toasttesttest'. + const packet4 = createCea708PacketFromBytes(clearWindow, time2); + + const text1 = 'testtest'; + const text2 = 'toasttest'; + const topLevelCue1 = CeaUtils.createWindowedCue( + /* startTime= */ time1, /* endTime= */ time2, '', + serviceNumber, windowId, rowCount, colCount, anchorId, + ); + topLevelCue1.nestedCues = [ + CeaUtils.createDefaultCue( + /* startTime= */ time1, /* endTime= */ time2, /* payload= */ text1), + ]; + + const topLevelCue2 = CeaUtils.createWindowedCue( + /* startTime= */ time1, /* endTime= */ time2, '', + serviceNumber, windowId2, rowCount, colCount, anchorId, + ); + topLevelCue2.nestedCues = [ + CeaUtils.createDefaultCue( + /* startTime= */ time1, /* endTime= */ time2, /* payload= */ text2), + ]; + + const expectedCaptions = [ + { + stream, + cue: topLevelCue1, + }, + + { + stream, + cue: topLevelCue2, + }, + ]; + + const captions = getCaptionsFromPackets( + service, packet1a, packet1b, packet1c, packet2a, packet2b, + packet3a, packet4); + expect(captions).toEqual(expectedCaptions); + }); + it('handles the clear command on a window', () => { // Define a visible window, add text to it, and then clear it. // This should emit a caption, since a visible window is being cleared.