Skip to content

Commit

Permalink
feat: expose CEA708 window position in the cue's region (#5924)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gkatsev committed Nov 23, 2023
1 parent b75d9be commit 2a524bf
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 37 deletions.
2 changes: 1 addition & 1 deletion lib/cea/cea708_service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
102 changes: 95 additions & 7 deletions lib/cea/cea708_window.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');


/**
Expand All @@ -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.
Expand Down Expand Up @@ -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_);
}

/**
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
}
};

/**
Expand All @@ -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.
Expand Down
67 changes: 49 additions & 18 deletions test/cea/cea708_service_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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),
];
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
];
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
];
Expand Down Expand Up @@ -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),
];
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down
Loading

0 comments on commit 2a524bf

Please sign in to comment.