Skip to content

Commit

Permalink
refactor: Move sequence mode VTT offset calculations (shaka-project#6332
Browse files Browse the repository at this point in the history
)

This moves VTT sequence mode offset calculations into a method.

It also makes all X-TIMESTAMP-MAP usage dependent on HLS specifically,
rather than sequence mode, simplifying the conditions. Sequence mode is
typically only used with HLS, and X-TIMESTAMP-MAP is explicitly only for
HLS. So excluding X-TIMESTAMP-MAP for DASH makes sense, instead of
conflating HLS and sequence mode.

This required updating some tests to explicitly set both the manifest
type and sequence mode flag.

This does *not* change the offset calculations themselves. Changes will
be made in follow-up PRs.

Issue shaka-project#6320
  • Loading branch information
joeyparrish authored Mar 7, 2024
1 parent 6c4333c commit 4ae15c2
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 53 deletions.
103 changes: 59 additions & 44 deletions lib/text/vtt_text_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,51 +84,16 @@ shaka.text.VttTextParser = class {
// HLS content, which should use X-TIMESTAMP-MAP and periodStart instead.
let offset = time.vttOffset;

// Only use 'X-TIMESTAMP-MAP' in sequence mode.
// Note that an offset based on the first video
// timestamp has already been extracted, and appears in periodStart.
if (blocks[0].includes('X-TIMESTAMP-MAP') && this.sequenceMode_) {
// https://bit.ly/2K92l7y
// The 'X-TIMESTAMP-MAP' header is used in HLS to align text with
// the rest of the media.
// The header format is 'X-TIMESTAMP-MAP=MPEGTS:n,LOCAL:m'
// (the attributes can go in any order)
// where n is MPEG-2 time and m is cue time it maps to.
// For example 'X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:900000'
// means an offset of 10 seconds
// 900000/MPEG_TIMESCALE - cue time.
const cueTimeMatch =
blocks[0].match(/LOCAL:((?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{3}))/m);

const mpegTimeMatch = blocks[0].match(/MPEGTS:(\d+)/m);
if (cueTimeMatch && mpegTimeMatch) {
const parser = new shaka.util.TextParser(cueTimeMatch[1]);
const cueTime = shaka.text.VttTextParser.parseTime_(parser);
if (cueTime == null) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_TEXT_HEADER);
}

let mpegTime = Number(mpegTimeMatch[1]);
const mpegTimescale = shaka.text.VttTextParser.MPEG_TIMESCALE_;

const rolloverSeconds =
shaka.text.VttTextParser.TS_ROLLOVER_ / mpegTimescale;
let segmentStart = time.segmentStart - time.periodStart;
while (segmentStart >= rolloverSeconds) {
segmentStart -= rolloverSeconds;
mpegTime += shaka.text.VttTextParser.TS_ROLLOVER_;
}

offset = time.periodStart + mpegTime / mpegTimescale - cueTime;
}
} else if (blocks[0].includes('X-TIMESTAMP-MAP') &&
// Only use 'X-TIMESTAMP-MAP' with HLS. This overrides offset above.
if (blocks[0].includes('X-TIMESTAMP-MAP') &&
this.manifestType_ == shaka.media.ManifestParser.HLS) {
// In we use HLS segments mode, with the presence of this tag we need
// calculate the offset from the segment startTime.
offset = time.segmentStart;
if (this.sequenceMode_) {
// Compute a different, rollover-based offset for sequence mode.
offset = this.computeHlsSequenceModeOffset_(blocks[0], time);
} else {
// Calculate the offset from the segment startTime.
offset = time.segmentStart;
}
}

// Parse VTT regions.
Expand Down Expand Up @@ -159,6 +124,56 @@ shaka.text.VttTextParser = class {
return ret;
}

/**
* @param {string} headerBlock Contains X-TIMESTAMP-MAP.
* @param {shaka.extern.TextParser.TimeContext} time
* @return {number}
* @private
*/
computeHlsSequenceModeOffset_(headerBlock, time) {
// https://bit.ly/2K92l7y
// The 'X-TIMESTAMP-MAP' header is used in HLS to align text with
// the rest of the media.
// The header format is 'X-TIMESTAMP-MAP=MPEGTS:n,LOCAL:m'
// (the attributes can go in any order)
// where n is MPEG-2 time and m is cue time it maps to.
// For example 'X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:900000'
// means an offset of 10 seconds
// 900000/MPEG_TIMESCALE - cue time.
const cueTimeMatch = headerBlock.match(
/LOCAL:((?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{3}))/m);
const mpegTimeMatch = headerBlock.match(/MPEGTS:(\d+)/m);

if (!cueTimeMatch || !mpegTimeMatch) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_TEXT_HEADER);
}

const parser = new shaka.util.TextParser(cueTimeMatch[1]);
const cueTime = shaka.text.VttTextParser.parseTime_(parser);
if (cueTime == null) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_TEXT_HEADER);
}

let mpegTime = Number(mpegTimeMatch[1]);
const mpegTimescale = shaka.text.VttTextParser.MPEG_TIMESCALE_;

const rolloverSeconds =
shaka.text.VttTextParser.TS_ROLLOVER_ / mpegTimescale;
let segmentStart = time.segmentStart - time.periodStart;
while (segmentStart >= rolloverSeconds) {
segmentStart -= rolloverSeconds;
mpegTime += shaka.text.VttTextParser.TS_ROLLOVER_;
}

return time.periodStart + mpegTime / mpegTimescale - cueTime;
}

/**
* Add default color
*
Expand Down
24 changes: 15 additions & 9 deletions test/text/vtt_text_parser_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,10 +527,10 @@ describe('VttTextParser', () => {
'00:00:40.000 --> 00:00:50.000 line:-1\n' +
'Test2',
{periodStart: 0, segmentStart: 25, segmentEnd: 65, vttOffset: 0},
/* sequenceMode= */ true);
/* hls= */ true, /* sequenceMode= */ true);
});

it('ignores X-TIMESTAMP-MAP header if not in sequence mode', () => {
it('ignores X-TIMESTAMP-MAP header if not HLS', () => {
verifyHelper(
[
{startTime: 20, endTime: 40, payload: 'Test'},
Expand All @@ -543,7 +543,7 @@ describe('VttTextParser', () => {
'00:00:40.000 --> 00:00:50.000 line:-1\n' +
'Test2',
{periodStart: 0, segmentStart: 25, segmentEnd: 65, vttOffset: 0},
/* sequenceMode= */ false);
/* hls= */ false, /* sequenceMode= */ false);
});

it('parses X-TIMESTAMP-MAP header with non-zero local base', () => {
Expand All @@ -562,7 +562,7 @@ describe('VttTextParser', () => {
'01:00:20.000 --> 01:00:30.000 line:-1\n' +
'Test2',
{periodStart: 0, segmentStart: 25, segmentEnd: 65, vttOffset: 0},
/* sequenceMode= */ true);
/* hls= */ true, /* sequenceMode= */ true);
});

it('combines X-TIMESTAMP-MAP header with periodStart', () => {
Expand All @@ -580,7 +580,7 @@ describe('VttTextParser', () => {
'00:00:40.000 --> 00:00:50.000 line:-1\n' +
'Test2',
{periodStart: 100, segmentStart: 25, segmentEnd: 65, vttOffset: 0},
/* sequenceMode= */ true);
/* hls= */ true, /* sequenceMode= */ true);
});

it('handles timestamp rollover with X-TIMESTAMP-MAP header', () => {
Expand All @@ -597,7 +597,7 @@ describe('VttTextParser', () => {
// Non-null segmentStart takes precedence over X-TIMESTAMP-MAP.
// This protects us from rollover in the MPEGTS field.
{periodStart: 0, segmentStart: 95440, segmentEnd: 95550, vttOffset: 0},
/* sequenceMode= */ true);
/* hls= */ true, /* sequenceMode= */ true);

verifyHelper(
[
Expand All @@ -611,7 +611,7 @@ describe('VttTextParser', () => {
'00:00:00.000 --> 00:00:02.000 line:0\n' +
'Test2',
{periodStart: 0, segmentStart: 95550, segmentEnd: 95560, vttOffset: 0},
/* sequenceMode= */ true);
/* hls= */ true, /* sequenceMode= */ true);
});

// A mock-up of HLS live subs as seen in b/253104251.
Expand All @@ -633,7 +633,7 @@ describe('VttTextParser', () => {
segmentEnd: 3610,
vttOffset: -1234567,
},
/* sequenceMode= */ true);
/* hls= */ true, /* sequenceMode= */ true);
});

it('supports global style blocks', () => {
Expand Down Expand Up @@ -1370,13 +1370,19 @@ describe('VttTextParser', () => {
* @param {!Array} cues
* @param {string} text
* @param {shaka.extern.TextParser.TimeContext} time
* @param {boolean=} hls
* @param {boolean=} sequenceMode
*/
function verifyHelper(cues, text, time, sequenceMode = false) {
function verifyHelper(cues, text, time, hls = false, sequenceMode = false) {
const data =
shaka.util.BufferUtils.toUint8(shaka.util.StringUtils.toUTF8(text));

const parser = new shaka.text.VttTextParser();
if (hls) {
parser.setManifestType(shaka.media.ManifestParser.HLS);
}
parser.setSequenceMode(sequenceMode);

const result = parser.parseMedia(data, time);

const checkCue = (cue) => {
Expand Down

0 comments on commit 4ae15c2

Please sign in to comment.