Skip to content

Commit

Permalink
feat: Save CEA708 caption cues on all windows when deleting/clearing …
Browse files Browse the repository at this point in the history
…windows (#7909)

Close #7907
  • Loading branch information
darrinliam authored Jan 21, 2025
1 parent 30be525 commit 19bd472
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 33 deletions.
54 changes: 31 additions & 23 deletions lib/cea/cea708_service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<shaka.extern.ICaptionDecoder.ClosedCaption>}
* @throws {!shaka.util.Error}
*/
handleCea708ControlCode(dtvccPacket) {
Expand Down Expand Up @@ -79,7 +79,7 @@ shaka.cea.Cea708Service = class {
this.handleG3_(controlCode & 0xff);
}

return null;
return [];
}

/**
Expand Down Expand Up @@ -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<shaka.extern.ICaptionDecoder.ClosedCaption>}
* @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) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -224,7 +224,7 @@ shaka.cea.Cea708Service = class {
window.setPenLocation(0, 0);
break;
}
return parsedClosedCaption;
return parsedClosedCaption ? [parsedClosedCaption] : [];
}

/**
Expand All @@ -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<shaka.extern.ICaptionDecoder.ClosedCaption>}
* @throws {!shaka.util.Error} a possible out-of-range buffer read.
* @private
*/
Expand Down Expand Up @@ -274,7 +274,7 @@ shaka.cea.Cea708Service = class {
const windowNum = (captionCommand & 0x0f) - 8;
this.defineWindow_(dtvccPacket, windowNum, pts);
}
return null;
return [];
}

/**
Expand Down Expand Up @@ -344,22 +344,26 @@ shaka.cea.Cea708Service = class {
/**
* @param {number} windowsBitmap
* @param {number} pts
* @return {?shaka.extern.ICaptionDecoder.ClosedCaption}
* @return {!Array<shaka.extern.ICaptionDecoder.ClosedCaption>}
* @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;
}

/**
Expand All @@ -382,7 +386,7 @@ shaka.cea.Cea708Service = class {
/**
* @param {number} windowsBitmap
* @param {number} pts
* @return {?shaka.extern.ICaptionDecoder.ClosedCaption}
* @return {!Array<shaka.extern.ICaptionDecoder.ClosedCaption>}
* @private
*/
hideWindows_(windowsBitmap, pts) {
Expand All @@ -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<shaka.extern.ICaptionDecoder.ClosedCaption>}
* @private
*/
toggleWindows_(windowsBitmap, pts) {
Expand All @@ -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<shaka.extern.ICaptionDecoder.ClosedCaption>}
* @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<shaka.extern.ICaptionDecoder.ClosedCaption>}
* @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;
}

/**
Expand Down
6 changes: 2 additions & 4 deletions lib/cea/cea_decoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
132 changes: 126 additions & 6 deletions test/cea/cea708_service_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -44,6 +48,7 @@ describe('Cea708Service', () => {

/** @type {number} */
const windowId = 0;
const windowId2 = 1;

/** @type {number} */
const rowCount = 16;
Expand Down Expand Up @@ -81,16 +86,16 @@ describe('Cea708Service', () => {
* @return {!Array<shaka.extern.ICaptionDecoder.ClosedCaption>}
*/
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(() => {
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 19bd472

Please sign in to comment.