Skip to content

Commit

Permalink
feat: handle rollover for VTT cues (#1472)
Browse files Browse the repository at this point in the history
* feat: handle rollover for vtt cues

* add comments

* handle rollover only if mpegts is not equal to 0

---------

Co-authored-by: Dzianis Dashkevich <ddashkevich@brightcove.com>
  • Loading branch information
dzianis-dashkevich and Dzianis Dashkevich authored Jan 12, 2024
1 parent da10a36 commit 8e8a341
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 5 deletions.
66 changes: 61 additions & 5 deletions src/vtt-segment-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -477,13 +477,25 @@ export default class VTTSegmentLoader extends SegmentLoader {
return;
}

const timestampmap = segmentInfo.timestampmap;
const diff = (timestampmap.MPEGTS / ONE_SECOND_IN_TS) - timestampmap.LOCAL + mappingObj.mapping;
const { MPEGTS, LOCAL } = segmentInfo.timestampmap;

/**
* From the spec:
* The MPEGTS media timestamp MUST use a 90KHz timescale,
* even when non-WebVTT Media Segments use a different timescale.
*/
const mpegTsInSeconds = MPEGTS / ONE_SECOND_IN_TS;

const diff = mpegTsInSeconds - LOCAL + mappingObj.mapping;

segmentInfo.cues.forEach((cue) => {
// First convert cue time to TS time using the timestamp-map provided within the vtt
cue.startTime += diff;
cue.endTime += diff;
const duration = cue.endTime - cue.startTime;
const startTime = MPEGTS === 0 ?
cue.startTime + diff :
this.handleRollover_(cue.startTime + diff, mappingObj.time);

cue.startTime = Math.max(startTime, 0);
cue.endTime = Math.max(startTime + duration, 0);
});

if (!playlist.syncInfo) {
Expand All @@ -496,4 +508,48 @@ export default class VTTSegmentLoader extends SegmentLoader {
};
}
}

/**
* MPEG-TS PES timestamps are limited to 2^33.
* Once they reach 2^33, they roll over to 0.
* mux.js handles PES timestamp rollover for the following scenarios:
* [forward rollover(right)] ->
* PES timestamps monotonically increase, and once they reach 2^33, they roll over to 0
* [backward rollover(left)] -->
* we seek back to position before rollover.
*
* According to the HLS SPEC:
* When synchronizing WebVTT with PES timestamps, clients SHOULD account
* for cases where the 33-bit PES timestamps have wrapped and the WebVTT
* cue times have not. When the PES timestamp wraps, the WebVTT Segment
* SHOULD have a X-TIMESTAMP-MAP header that maps the current WebVTT
* time to the new (low valued) PES timestamp.
*
* So we want to handle rollover here and align VTT Cue start/end time to the player's time.
*/
handleRollover_(value, reference) {
if (reference === null) {
return value;
}

let valueIn90khz = value * ONE_SECOND_IN_TS;
const referenceIn90khz = reference * ONE_SECOND_IN_TS;

let offset;

if (referenceIn90khz < valueIn90khz) {
// - 2^33
offset = -8589934592;
} else {
// + 2^33
offset = 8589934592;
}

// distance(value - reference) > 2^32
while (Math.abs(valueIn90khz - referenceIn90khz) > 4294967296) {
valueIn90khz += offset;
}

return valueIn90khz / ONE_SECOND_IN_TS;
}
}
69 changes: 69 additions & 0 deletions test/vtt-segment-loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,75 @@ QUnit.module('VTTSegmentLoader', function(hooks) {
}
);

QUnit.test(
'should handle rollover when MPEGTS is not equal to 0',
function(assert) {
const cues = [
{
startTime: 0,
endTime: 2
},
{
startTime: 2,
endTime: 4
},
{
startTime: 4,
endTime: 6
}
];
const expectedCueTimes = [
{
startTime: 107.59999175347222,
endTime: 109.59999175347222
},
{
startTime: 109.59999175347222,
endTime: 111.59999175347222
},
{
startTime: 111.59999175347222,
endTime: 113.59999175347222
}
];
const expectedSegment = {
duration: 6
};
const expectedPlaylist = {
mediaSequence: 100,
syncInfo: { mediaSequence: 102, time: 105.59999175347222 }
};
const mappingObj = { time: 103.68000000000006, mapping: -405994299.533186 };

const playlist = { mediaSequence: 100 };
const segment = { duration: 6 };
const segmentInfo = {
timestampmap: { MPEGTS: 6504822210, LOCAL: 0 },
mediaIndex: 2,
cues,
segment
};

loader.updateTimeMapping_(segmentInfo, mappingObj, playlist);

assert.deepEqual(
cues,
expectedCueTimes,
'adjusted cue timing based on timestampmap'
);
assert.deepEqual(
segment,
expectedSegment,
'set segment start and end based on cue content'
);
assert.deepEqual(
playlist,
expectedPlaylist,
'set syncInfo for playlist based on learned segment start'
);
}
);

QUnit.test(
'loader logs vtt.js ParsingErrors and does not trigger an error event',
function(assert) {
Expand Down

0 comments on commit 8e8a341

Please sign in to comment.