diff --git a/src/vtt-segment-loader.js b/src/vtt-segment-loader.js index 9f3337465..a7e336bc4 100644 --- a/src/vtt-segment-loader.js +++ b/src/vtt-segment-loader.js @@ -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) { @@ -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; + } } diff --git a/test/vtt-segment-loader.test.js b/test/vtt-segment-loader.test.js index d1c4115b6..46f895794 100644 --- a/test/vtt-segment-loader.test.js +++ b/test/vtt-segment-loader.test.js @@ -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) {