diff --git a/externs/shaka/text.js b/externs/shaka/text.js index c6c20456f2..34d7eef603 100644 --- a/externs/shaka/text.js +++ b/externs/shaka/text.js @@ -373,15 +373,14 @@ shaka.extern.TextParser = class { * * @typedef {{ * periodStart: number, - * segmentStart: ?number, + * segmentStart: number, * segmentEnd: number * }} * * @property {number} periodStart * The absolute start time of the period in seconds. - * @property {?number} segmentStart + * @property {number} segmentStart * The absolute start time of the segment in seconds. - * Null if the manifest does not provide this information, such as in HLS. * @property {number} segmentEnd * The absolute end time of the segment in seconds. * diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index cd6e623a85..644aa6e9fb 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -378,15 +378,12 @@ shaka.hls.HlsParser = class { // Find the min and max timestamp of the earliest segment in all streams. // Find the minimum duration of all streams as well. let minFirstTimestamp = Infinity; - let maxFirstTimestamp = 0; let maxLastTimestamp = 0; let minDuration = Infinity; for (const streamInfo of this.uriToStreamInfosMap_.values()) { minFirstTimestamp = Math.min(minFirstTimestamp, streamInfo.minTimestamp); - maxFirstTimestamp = - Math.max(maxFirstTimestamp, streamInfo.minTimestamp); maxLastTimestamp = Math.max(maxLastTimestamp, streamInfo.maxTimestamp); if (streamInfo.stream.type != 'text') { @@ -432,29 +429,6 @@ shaka.hls.HlsParser = class { this.presentationTimeline_.setSegmentAvailabilityDuration( segmentAvailabilityDuration); } - - const rolloverSeconds = - shaka.hls.HlsParser.TS_ROLLOVER_ / shaka.hls.HlsParser.TS_TIMESCALE_; - let offset = 0; - while (maxFirstTimestamp >= rolloverSeconds) { - offset += rolloverSeconds; - maxFirstTimestamp -= rolloverSeconds; - } - if (offset) { - shaka.log.debug('Offsetting live streams by', offset, - 'to compensate for rollover'); - - for (const streamInfo of this.uriToStreamInfosMap_.values()) { - if (streamInfo.minTimestamp < rolloverSeconds) { - shaka.log.v1('Offset applied to', streamInfo.stream.type); - // The segments were created with actual media times, rather than - // period-aligned times, so offset them all to period time. - streamInfo.stream.segmentIndex.offset(offset); - } else { - shaka.log.v1('Offset NOT applied to', streamInfo.stream.type); - } - } - } } else { // For VOD/EVENT content, offset everything back to 0. // Use the minimum timestamp as the offset for all streams. @@ -2383,14 +2357,6 @@ shaka.hls.HlsParser.PresentationType_ = { shaka.hls.HlsParser.TS_TIMESCALE_ = 90000; -/** - * At this value, timestamps roll over in TS content. - * @const {number} - * @private - */ -shaka.hls.HlsParser.TS_ROLLOVER_ = 0x200000000; - - /** * The amount of data from the start of a segment we will try to fetch when we * need to know the segment start time. This allows us to avoid fetching the diff --git a/lib/text/vtt_text_parser.js b/lib/text/vtt_text_parser.js index 7c3b09a0fc..81372adae3 100644 --- a/lib/text/vtt_text_parser.js +++ b/lib/text/vtt_text_parser.js @@ -54,45 +54,45 @@ shaka.text.VttTextParser = class { } let offset = time.segmentStart; - if (offset == null) { - // This is a probe, such as the HLS parser makes. We don't know the - // segment start time, so we will use the X-TIMESTAMP-MAP header, if - // present, to get the segment start time. By only doing this when - // segmentStart == null, we protect against rollover in the MPEGTS field. - - // In case the attempt below doesn't work out, assume an offset of 0. - offset = 0; - - if (blocks[0].includes('X-TIMESTAMP-MAP')) { - // 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 = + + if (blocks[0].includes('X-TIMESTAMP-MAP')) { + // 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); - } - - const mpegTime = Number(mpegTimeMatch[1]); - const mpegTimescale = shaka.text.VttTextParser.MPEG_TIMESCALE_; - // Apple-encoded HLS content uses absolute timestamps, so assume the - // presence of the map tag means the content uses absolute timestamps. - offset = time.periodStart + (mpegTime / mpegTimescale - cueTime); + 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; + while (segmentStart >= rolloverSeconds) { + segmentStart -= rolloverSeconds; + mpegTime += shaka.text.VttTextParser.TS_ROLLOVER_; + } + + // Apple-encoded HLS content uses absolute timestamps, so assume the + // presence of the map tag means the content uses absolute timestamps. + offset = time.periodStart + mpegTime / mpegTimescale - cueTime; } } @@ -434,6 +434,13 @@ shaka.text.VttTextParser = class { */ shaka.text.VttTextParser.MPEG_TIMESCALE_ = 90000; +/** + * At this value, timestamps roll over in TS content. + * @const {number} + * @private + */ +shaka.text.VttTextParser.TS_ROLLOVER_ = 0x200000000; + shaka.text.TextEngine.registerParser('text/vtt', shaka.text.VttTextParser); shaka.text.TextEngine.registerParser( diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index 21e0c58446..cb17f0d9eb 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -679,90 +679,6 @@ describe('HlsParser live', () => { expectedStartByte, partialEndByte); // Partial segment request }); - - // TODO: add this test case back after fixing the rollover logic. - xit('handles rollover on update', async () => { - const masterWithVtt = [ - '#EXTM3U\n', - '#EXT-X-MEDIA:TYPE=SUBTITLES,LANGUAGE="fra",URI="text",', - 'GROUP-ID="sub1"\n', - '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",', - 'RESOLUTION=960x540,FRAME-RATE=60,SUBTITLES="sub1"\n', - 'video\n', - ].join(''); - - const textPlaylist1 = [ - '#EXTM3U\n', - '#EXT-X-TARGETDURATION:5\n', - '#EXT-X-MEDIA-SEQUENCE:0\n', - '#EXTINF:2,\n', - 'main1.vtt\n', - ].join(''); - - const textPlaylist2 = [ - '#EXTM3U\n', - '#EXT-X-TARGETDURATION:5\n', - '#EXT-X-MEDIA-SEQUENCE:0\n', - '#EXTINF:2,\n', - 'main1.vtt\n', - '#EXTINF:2,\n', - 'main2.vtt\n', - ].join(''); - - // ~0.7s from rollover - const vtt1 = [ - 'WEBVTT\n', - 'X-TIMESTAMP-MAP=MPEGTS:8589870000,LOCAL:00:00:00.000\n', - '\n', - '00:00.000 --> 00:01.000\n', - 'Hello, world!\n', - ].join(''); - - // ~1.3s after rollover - const vtt2 = [ - 'WEBVTT\n', - 'X-TIMESTAMP-MAP=MPEGTS:115408,LOCAL:00:00:00.000\n', - '\n', - '00:00.000 --> 00:01.000\n', - 'Hello, again!\n', - ].join(''); - - fakeNetEngine - .setResponseText('test:/master', masterWithVtt) - .setResponseText('test:/video', media) - .setResponseText('test:/text', textPlaylist1) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', pastRolloverSegmentData) - .setResponseText('test:/main1.vtt', vtt1); - - - const baseTime = 95443 + rolloverOffset; - const ref1 = ManifestParser.makeReference('test:/main1.vtt', - /* position */ 0, - /* startTime */ baseTime, - /* endTime */ baseTime + 2); - const ref2 = ManifestParser.makeReference('test:/main2.vtt', - /* position */ 1, - /* startTime */ baseTime + 2, - /* endTime */ baseTime + 4); - - const manifest = await parser.start('test:/master', playerInterface); - const text = manifest.periods[0].textStreams[0]; - await text.createSegmentIndex(); - ManifestParser.verifySegmentIndex(text, [ref1]); - - // Change the entries that are affected by the roll over. - fakeNetEngine - .setResponseText('test:/video', mediaWithAdditionalSegment) - .setResponseText('test:/text', textPlaylist2) - .setResponseValue('test:/main2.mp4', pastRolloverSegmentData) - .setResponseText('test:/main2.vtt', vtt2); - - fakeNetEngine.request.calls.reset(); - await delayForUpdatePeriod(); - - ManifestParser.verifySegmentIndex(text, [ref1, ref2]); - }); }); // describe('update') }); // describe('playlist type LIVE') }); // describe('HlsParser live') diff --git a/test/text/vtt_text_parser_unit.js b/test/text/vtt_text_parser_unit.js index 9535693f19..ecfe4261ae 100644 --- a/test/text/vtt_text_parser_unit.js +++ b/test/text/vtt_text_parser_unit.js @@ -536,7 +536,7 @@ describe('VttTextParser', () => { expect(logWarningSpy).toHaveBeenCalledTimes(7); }); - it('respects X-TIMESTAMP-MAP header in probes', () => { + it('parses X-TIMESTAMP-MAP header', () => { verifyHelper( [ {startTime: 30, endTime: 50, payload: 'Test'}, @@ -550,27 +550,36 @@ describe('VttTextParser', () => { 'Test\n\n' + '00:00:40.000 --> 00:00:50.000 line:-1\n' + 'Test2', - // segmentStart of null marks this as a probe. - {periodStart: 0, segmentStart: null, segmentEnd: 0}); + {periodStart: 0, segmentStart: 25, segmentEnd: 65}); }); - it('ignores X-TIMESTAMP-MAP header when segment times are known', () => { + it('handles timestamp rollover with X-TIMESTAMP-MAP header', () => { verifyHelper( [ - {startTime: 120, endTime: 140, payload: 'Test'}, - {startTime: 140, endTime: 150, payload: 'Test2'}, + {startTime: 95443, endTime: 95445, payload: 'Test'}, ], - // 900000 = 10 sec, so expect every timestamp to be 10 + // 8589870000/900000 = 95443 sec, so expect every timestamp to be 95443 // seconds ahead of what is specified. 'WEBVTT\n' + - 'X-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000\n\n' + - '00:00:20.000 --> 00:00:40.000 line:0\n' + - 'Test\n\n' + - '00:00:40.000 --> 00:00:50.000 line:-1\n' + - 'Test2', + 'X-TIMESTAMP-MAP=MPEGTS:8589870000,LOCAL:00:00:00.000\n\n' + + '00:00:00.000 --> 00:00:02.000 line:0\n' + + 'Test', // Non-null segmentStart takes precedence over X-TIMESTAMP-MAP. // This protects us from rollover in the MPEGTS field. - {periodStart: 0, segmentStart: 100, segmentEnd: 0}); + {periodStart: 0, segmentStart: 95440, segmentEnd: 95550}); + + verifyHelper( + [ + {startTime: 95552, endTime: 95554, payload: 'Test2'}, + ], + // 95550 is larger than the roll over timestamp, so the timestamp offset + // gets rolled over. + // (9745408 + 0x200000000) / 90000 = 95552 sec + 'WEBVTT\n' + + 'X-TIMESTAMP-MAP=MPEGTS:9745408,LOCAL:00:00:00.000\n\n' + + '00:00:00.000 --> 00:00:02.000 line:0\n' + + 'Test2', + {periodStart: 0, segmentStart: 95550, segmentEnd: 95560}); }); it('skips style blocks', () => {