From 2a524bf51fc613b8ebddb5794524cbb3f6366d4b Mon Sep 17 00:00:00 2001 From: Gary Katsevman Date: Thu, 23 Nov 2023 11:19:46 -0500 Subject: [PATCH] feat: expose CEA708 window position in the cue's region (#5924) CEA708 captions have positioning data available in their windows. However, this isn't currently translated and exposed by shaka though it is parsed from the bitstream. Translates the windows into WebVTT regions and uses the mappings outlined https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-708 This is also partially implements #2583. --- lib/cea/cea708_service.js | 2 +- lib/cea/cea708_window.js | 102 +++++++++++++++++++++-- test/cea/cea708_service_unit.js | 67 +++++++++++---- test/cea/cea708_window_unit.js | 140 +++++++++++++++++++++++++++++--- test/cea/cea_decoder_unit.js | 3 +- test/test/util/cea_utils.js | 69 ++++++++++++++++ 6 files changed, 346 insertions(+), 37 deletions(-) diff --git a/lib/cea/cea708_service.js b/lib/cea/cea708_service.js index 6e141f681..6ef6001a4 100644 --- a/lib/cea/cea708_service.js +++ b/lib/cea/cea708_service.js @@ -568,7 +568,7 @@ shaka.cea.Cea708Service = class { // Create the window if it doesn't exist. const windowAlreadyExists = this.windows_[windowNum] !== null; if (!windowAlreadyExists) { - const window = new shaka.cea.Cea708Window(windowNum); + const window = new shaka.cea.Cea708Window(windowNum, this.serviceNumber_); window.setStartTime(pts); this.windows_[windowNum] = window; } diff --git a/lib/cea/cea708_window.js b/lib/cea/cea708_window.js index 71e40b672..8d071e2a2 100644 --- a/lib/cea/cea708_window.js +++ b/lib/cea/cea708_window.js @@ -9,7 +9,7 @@ goog.provide('shaka.cea.Cea708Window'); goog.require('shaka.cea.CeaUtils'); goog.require('shaka.cea.CeaUtils.StyledChar'); goog.require('shaka.text.Cue'); -goog.require('shaka.util.Functional'); +goog.require('shaka.text.CueRegion'); /** @@ -19,7 +19,13 @@ shaka.cea.Cea708Window = class { /** * @param {number} windowNum */ - constructor(windowNum) { + constructor(windowNum, parentService) { + /** + * Number for the parent service (1 - 63). + * @private {number} + */ + this.parentService_ = parentService; + /** * A number from 0 - 7 indicating the window number in the * service that owns this window. @@ -132,11 +138,6 @@ shaka.cea.Cea708Window = class { this.backgroundColor_ = shaka.cea.CeaUtils.DEFAULT_BG_COLOR; this.resetMemory(); - - // TODO Support window positioning by mapping them to Regions. - // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-708 - shaka.util.Functional.ignored(this.verticalAnchor_, this.relativeToggle_, - this.horizontalAnchor_, this.anchorId_, this.windowNum_); } /** @@ -306,6 +307,8 @@ shaka.cea.Cea708Window = class { topLevelCue.textAlign = shaka.text.Cue.textAlign.CENTER; } + this.adjustRegion_(topLevelCue.region); + const caption = shaka.cea.CeaUtils.getParsedCaption( topLevelCue, stream, this.memory_, this.startTime_, endTime); if (caption) { @@ -398,6 +401,75 @@ shaka.cea.Cea708Window = class { setStartTime(pts) { this.startTime_ = pts; } + + /** + * Support window positioning by mapping anchor related values to CueRegion. + * https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-708 + * @param {shaka.text.CueRegion} region + * @private + */ + adjustRegion_(region) { + if (this.parentService_) { + region.id += 'svc' + this.parentService_; + } + region.id += 'win' + this.windowNum_; + + region.height = this.rowCount_; + region.width = this.colCount_; + region.heightUnits = shaka.text.CueRegion.units.LINES; + region.widthUnits = shaka.text.CueRegion.units.LINES; + + region.viewportAnchorX = this.horizontalAnchor_; + region.viewportAnchorY = this.verticalAnchor_; + // WebVTT's region viewport anchors are technically always in percentages. + // However, we don't know the aspect ratio of the video at this point, + // which determines how we interpret the horizontal anchor. + // So, we expose the additonal flag to reflect whether these viewport anchor + // values can be used be used as is or should be converted to percentages. + region.viewportAnchorUnits = this.relativeToggle_ ? + shaka.text.CueRegion.units.PERCENTAGE : shaka.text.CueRegion.units.LINES; + + const AnchorId = shaka.cea.Cea708Window.AnchorId; + + switch (this.anchorId_) { + case AnchorId.UPPER_LEFT: + region.regionAnchorX = 0; + region.regionAnchorY = 0; + break; + case AnchorId.UPPER_CENTER: + region.regionAnchorX = 50; + region.regionAnchorY = 0; + break; + case AnchorId.UPPER_RIGHT: + region.regionAnchorX = 100; + region.regionAnchorY = 0; + break; + case AnchorId.MIDDLE_LEFT: + region.regionAnchorX = 0; + region.regionAnchorY = 50; + break; + case AnchorId.MIDDLE_CENTER: + region.regionAnchorX = 50; + region.regionAnchorY = 50; + break; + case AnchorId.MIDDLE_RIGHT: + region.regionAnchorX = 100; + region.regionAnchorY = 50; + break; + case AnchorId.LOWER_LEFT: + region.regionAnchorX = 0; + region.regionAnchorY = 100; + break; + case AnchorId.LOWER_CENTER: + region.regionAnchorX = 50; + region.regionAnchorY = 100; + break; + case AnchorId.LOWER_RIGHT: + region.regionAnchorX = 100; + region.regionAnchorY = 100; + break; + } + } }; /** @@ -411,6 +483,22 @@ shaka.cea.Cea708Window.TextJustification = { FULL: 3, }; +/** + * Possible AnchorId values. + * @const @enum {number} + */ +shaka.cea.Cea708Window.AnchorId = { + UPPER_LEFT: 0, + UPPER_CENTER: 1, + UPPER_RIGHT: 2, + MIDDLE_LEFT: 3, + MIDDLE_CENTER: 4, + MIDDLE_RIGHT: 5, + LOWER_LEFT: 6, + LOWER_CENTER: 7, + LOWER_RIGHT: 8, +}; + /** * Can be indexed 0-31 for 4:3 format, and 0-41 for 16:9 formats. * Thus the absolute maximum is 42 columns for the 16:9 format. diff --git a/test/cea/cea708_service_unit.js b/test/cea/cea708_service_unit.js index 550a08cc5..4cb2d7dd2 100644 --- a/test/cea/cea708_service_unit.js +++ b/test/cea/cea708_service_unit.js @@ -40,6 +40,18 @@ describe('Cea708Service', () => { /** @type {string} */ const stream = `svc${serviceNumber}`; + /** @type {number} */ + const windowId = 0; + + /** @type {number} */ + const rowCount = 16; + + /** @type {number} */ + const colCount = 32; + + /** @type {shaka.cea.Cea708Window.AnchorId} */ + const anchorId = shaka.cea.Cea708Window.AnchorId.UPPER_CENTER; + /** * Takes in a array of bytes and a presentation timestamp (in seconds), * and converts it into a CEA-708 DTVCC Packet. @@ -92,7 +104,8 @@ describe('Cea708Service', () => { const packet2 = createCea708PacketFromBytes(hideWindow, endTime); const text = 'test'; - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, windowId, rowCount, colCount, anchorId); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), ]; @@ -129,7 +142,8 @@ describe('Cea708Service', () => { // [1]: // [2]: test const text = 'test'; - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, windowId, rowCount, colCount, anchorId); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), CeaUtils.createLineBreakCue(startTime, endTime), @@ -174,7 +188,8 @@ describe('Cea708Service', () => { // Three nested cues, where the middle one should be underlined+italicized. const text = 'test'; - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, windowId, rowCount, colCount, anchorId); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), CeaUtils.createStyledCue( @@ -217,7 +232,8 @@ describe('Cea708Service', () => { // Two nested cues, the second one should have colors. const text1 = 'test'; const text2 = 'color'; - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, windowId, rowCount, colCount, anchorId); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text1), CeaUtils.createStyledCue( @@ -269,7 +285,8 @@ describe('Cea708Service', () => { const text2 = '©¶÷'; const text3 = '⅞┐™'; const text4 = '[CC]'; - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, windowId, rowCount, colCount, anchorId); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text1), CeaUtils.createLineBreakCue(startTime, endTime), @@ -311,7 +328,8 @@ describe('Cea708Service', () => { // be replaced by an underline. const text1 = '_œ_'; const text2 = '[CC]__'; - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, windowId, rowCount, colCount, anchorId); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text1), CeaUtils.createLineBreakCue(startTime, endTime), @@ -343,7 +361,8 @@ describe('Cea708Service', () => { // The text in the current window should have been emitted, and then clear // should have been called. const text = 'test'; - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, windowId, rowCount, colCount, anchorId); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), ]; @@ -378,7 +397,8 @@ describe('Cea708Service', () => { // Right-justified text is expected. const text = 'test'; - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, windowId, rowCount, colCount, anchorId); topLevelCue.textAlign = shaka.text.Cue.textAlign.RIGHT; topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), @@ -413,7 +433,8 @@ describe('Cea708Service', () => { const text1 = 'te'; const text2 = 'st'; - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, windowId, rowCount, colCount, anchorId); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text1), CeaUtils.createLineBreakCue(startTime, endTime), @@ -456,7 +477,8 @@ describe('Cea708Service', () => { // HCR wipes the row and moves the pen to the row start. const text1 = 'te'; const text2 = 'st'; - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, windowId, rowCount, colCount, anchorId); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text1), CeaUtils.createLineBreakCue(startTime, endTime), @@ -489,7 +511,8 @@ describe('Cea708Service', () => { // Backspace should have erased the last 't' in 'test'. const text = 'tes'; - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, windowId, rowCount, colCount, anchorId); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), ]; @@ -530,7 +553,8 @@ describe('Cea708Service', () => { // The form feed control code would have wiped the entire window // including new lines, and the text after is just 'test'. const text = 'test'; - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, windowId, rowCount, colCount, anchorId); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), ]; @@ -605,15 +629,19 @@ describe('Cea708Service', () => { const text1 = 'test'; const text2 = 'testtest'; - const topLevelCue1 = new shaka.text.Cue( - /* startTime= */ time1, /* endTime= */ time2, ''); + 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 = new shaka.text.Cue( - /* startTime= */ time3, /* endTime= */ time4, ''); + const topLevelCue2 = CeaUtils.createWindowedCue( + /* startTime= */ time3, /* endTime= */ time4, '', + serviceNumber, windowId, rowCount, colCount, anchorId, + ); topLevelCue2.nestedCues = [ CeaUtils.createDefaultCue( /* startTime= */ time3, /* endTime= */ time4, /* payload= */ text2), @@ -652,8 +680,11 @@ describe('Cea708Service', () => { // Only one cue should have been emitted as per the explanation above. const text = 'test'; - const topLevelCue = new shaka.text.Cue( - /* startTime= */ time1, /* endTime= */ time2, ''); + const topLevelCue = CeaUtils.createWindowedCue( + /* startTime= */ time1, /* endTime= */ time2, '', + serviceNumber, windowId, rowCount, colCount, anchorId, + ); + topLevelCue.nestedCues = [ CeaUtils.createDefaultCue( /* startTime= */ time1, /* endTime= */ time2, /* payload= */ text), diff --git a/test/cea/cea708_window_unit.js b/test/cea/cea708_window_unit.js index e8f6c9891..b885544bb 100644 --- a/test/cea/cea708_window_unit.js +++ b/test/cea/cea708_window_unit.js @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ + describe('Cea708Window', () => { const CeaUtils = shaka.test.CeaUtils; @@ -29,7 +30,7 @@ describe('Cea708Window', () => { const endTime = 2; beforeEach(() => { - window = new shaka.cea.Cea708Window(/* windowNum= */ 0); + window = new shaka.cea.Cea708Window(/* windowNum= */ 0, serviceNumber); window.defineWindow( /* visible= */ true, /* verticalAnchor= */ 0, /* horAnchor= */ 0, /* anchorId= */ 0, /* relativeToggle= */ false, @@ -43,7 +44,8 @@ describe('Cea708Window', () => { window.setCharacter(c); } - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, 0, rowCount, colCount); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, text), ]; @@ -71,7 +73,8 @@ describe('Cea708Window', () => { const caption = window.forceEmit(endTime, serviceNumber); - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, 0, rowCount, colCount); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, text1), CeaUtils.createLineBreakCue(startTime, endTime), @@ -103,7 +106,8 @@ describe('Cea708Window', () => { const caption = window.forceEmit(endTime, serviceNumber); - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, 0, rowCount, colCount); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, text1), CeaUtils.createLineBreakCue(startTime, endTime), @@ -167,7 +171,8 @@ describe('Cea708Window', () => { } // These three stylings should correspond to three nested cues. - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, 0, rowCount, colCount); topLevelCue.nestedCues = [ CeaUtils.createStyledCue(startTime, endTime, text1, /* underline= */ true, /* italics= */ true, textColor1, @@ -190,7 +195,8 @@ describe('Cea708Window', () => { describe('handles justification of cues', () => { const text = 'test'; - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, 0, rowCount, colCount); it('justifies the text left', () => { for (const c of text) { window.setCharacter(c); @@ -268,7 +274,8 @@ describe('Cea708Window', () => { // The second text should have overwritten the first text, // and all the styles should have been cleared. - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, 0, rowCount, colCount); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, text2), ]; @@ -289,7 +296,8 @@ describe('Cea708Window', () => { } window.backspace(); - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, 0, rowCount, colCount); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, backspacedText), ]; @@ -321,7 +329,8 @@ describe('Cea708Window', () => { // There should be two spaces between the words on the first row, // and then the last row with text should appear 3 linebreaks later. - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, 0, rowCount, colCount); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, text1+' '+text2), CeaUtils.createLineBreakCue(startTime, endTime), @@ -346,7 +355,8 @@ describe('Cea708Window', () => { // Since column size is 32, the buffer should have only taken the first // 32 chars, and omitted the two extra ones at the end. - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, 0, rowCount, colCount); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, trimmedText), ]; @@ -369,5 +379,115 @@ describe('Cea708Window', () => { window.toggle(); // The window was hidden, but is now toggled to visible. expect(window.isVisible()).toBe(true); }); + + describe('correctly handles the window anchors', () => { + it('handles bottom of video window anchors', () => { + window = new shaka.cea.Cea708Window(/* windowNum= */ 0, + /* serviceNumber= */ 1); + window.defineWindow( + /* visible= */ true, /* verticalAnchor= */ 99, + /* horAnchor= */ 50, /* anchorId= */ 7, /* relativeToggle= */ true, + rowCount, colCount); + window.setStartTime(startTime); + + const text = 'test word'; + for (const c of text) { + window.setCharacter(c); + } + + const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + topLevelCue.nestedCues = [ + CeaUtils.createDefaultCue(startTime, endTime, text), + ]; + + const caption = window.forceEmit(endTime, serviceNumber); + + const region = new shaka.text.CueRegion(); + region.id = 'svc1win0'; + region.height = rowCount; + region.width = colCount; + region.heightUnits = shaka.text.CueRegion.units.LINES; + region.widthUnits = shaka.text.CueRegion.units.LINES; + region.viewportAnchorX = 50; + region.viewportAnchorY = 99; + region.regionAnchorX = 50; + region.regionAnchorY = 100; + region.viewportAnchorUnits = shaka.text.CueRegion.units.PERCENTAGE; + + expect(caption.cue.region).toEqual(region); + }); + + it('handles top of video window anchors', () => { + window = new shaka.cea.Cea708Window(/* windowNum= */ 0, + /* serviceNumber= */ 2); + window.defineWindow( + /* visible= */ true, /* verticalAnchor= */ 0, + /* horAnchor= */ 50, /* anchorId= */ 1, /* relativeToggle= */ true, + rowCount, colCount); + window.setStartTime(startTime); + + const text = 'test word'; + for (const c of text) { + window.setCharacter(c); + } + + const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + topLevelCue.nestedCues = [ + CeaUtils.createDefaultCue(startTime, endTime, text), + ]; + + const caption = window.forceEmit(endTime, serviceNumber); + + const region = new shaka.text.CueRegion(); + region.id = 'svc2win0'; + region.height = rowCount; + region.width = colCount; + region.heightUnits = shaka.text.CueRegion.units.LINES; + region.widthUnits = shaka.text.CueRegion.units.LINES; + region.viewportAnchorX = 50; + region.viewportAnchorY = 0; + region.regionAnchorX = 50; + region.regionAnchorY = 0; + region.viewportAnchorUnits = shaka.text.CueRegion.units.PERCENTAGE; + + expect(caption.cue.region).toEqual(region); + }); + + it('handles bottom right of video window anchors using line values', () => { + window = new shaka.cea.Cea708Window(/* windowNum= */ 0, + /* serviceNumbe= */ 3); + window.defineWindow( + /* visible= */ true, /* verticalAnchor= */ 74, + /* horAnchor= */ 209, /* anchorId= */ 8, /* relativeToggle= */ false, + rowCount, colCount); + window.setStartTime(startTime); + + const text = 'test word'; + for (const c of text) { + window.setCharacter(c); + } + + const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + topLevelCue.nestedCues = [ + CeaUtils.createDefaultCue(startTime, endTime, text), + ]; + + const caption = window.forceEmit(endTime, serviceNumber); + + const region = new shaka.text.CueRegion(); + region.id = 'svc3win0'; + region.height = rowCount; + region.width = colCount; + region.heightUnits = shaka.text.CueRegion.units.LINES; + region.widthUnits = shaka.text.CueRegion.units.LINES; + region.viewportAnchorX = 209; + region.viewportAnchorY = 74; + region.regionAnchorX = 100; + region.regionAnchorY = 100; + region.viewportAnchorUnits = shaka.text.CueRegion.units.LINES; + + expect(caption.cue.region).toEqual(region); + }); + }); }); diff --git a/test/cea/cea_decoder_unit.js b/test/cea/cea_decoder_unit.js index 65dc03d7e..6bd2a20e6 100644 --- a/test/cea/cea_decoder_unit.js +++ b/test/cea/cea_decoder_unit.js @@ -565,7 +565,8 @@ describe('CeaDecoder', () => { decoder.extract(hideWindow, endTime); const text = 'test'; - const topLevelCue = new shaka.text.Cue(startTime, endTime, ''); + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, 0, 11, 11); topLevelCue.nestedCues = [ CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), ]; diff --git a/test/test/util/cea_utils.js b/test/test/util/cea_utils.js index 8da6e8a28..e923e2f6e 100644 --- a/test/test/util/cea_utils.js +++ b/test/test/util/cea_utils.js @@ -57,4 +57,73 @@ shaka.test.CeaUtils = class { cue.lineBreak = true; return cue; } + + /** + * Create shaka Cue with region updated to a specific value. + * @param {number} startTime + * @param {number} endTime + * @param {string} payload + * @param {number} serviceNumber + * @param {number} windowId + * @param {number} rowCount + * @param {number} colCount + * @param {number=} anchorId + * @return {!shaka.text.Cue} + */ + static createWindowedCue(startTime, endTime, payload, + serviceNumber, windowId, rowCount, colCount, anchorId) { + const cue = new shaka.text.Cue(startTime, endTime, payload); + const region = cue.region; + const AnchorId = shaka.cea.Cea708Window.AnchorId; + + region.id = 'svc' + serviceNumber + 'win' + windowId; + region.height = rowCount; + region.width = colCount; + region.heightUnits = shaka.text.CueRegion.units.LINES; + region.widthUnits = shaka.text.CueRegion.units.LINES; + region.viewportAnchorUnits = shaka.text.CueRegion.units.LINES; + + if (typeof anchorId === 'number') { + switch (anchorId) { + case AnchorId.UPPER_LEFT: + region.regionAnchorX = 0; + region.regionAnchorY = 0; + break; + case AnchorId.UPPER_CENTER: + region.regionAnchorX = 50; + region.regionAnchorY = 0; + break; + case AnchorId.UPPER_RIGHT: + region.regionAnchorX = 100; + region.regionAnchorY = 0; + break; + case AnchorId.MIDDLE_LEFT: + region.regionAnchorX = 0; + region.regionAnchorY = 50; + break; + case AnchorId.MIDDLE_CENTER: + region.regionAnchorX = 50; + region.regionAnchorY = 50; + break; + case AnchorId.MIDDLE_RIGHT: + region.regionAnchorX = 100; + region.regionAnchorY = 50; + break; + case AnchorId.LOWER_LEFT: + region.regionAnchorX = 0; + region.regionAnchorY = 100; + break; + case AnchorId.LOWER_CENTER: + region.regionAnchorX = 50; + region.regionAnchorY = 100; + break; + case AnchorId.LOWER_RIGHT: + region.regionAnchorX = 100; + region.regionAnchorY = 100; + break; + } + } + + return cue; + } };