Skip to content

Commit

Permalink
[Hls] Move MPEG TS Timestamp rollover to Vtt Text Parser
Browse files Browse the repository at this point in the history
Change-Id: Id840f8201d9d31f00fa38c88f632ff9e515e07a6
  • Loading branch information
michellezhuogg committed Oct 8, 2019
1 parent 65370b4 commit 1ada5e7
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 170 deletions.
5 changes: 2 additions & 3 deletions externs/shaka/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
34 changes: 0 additions & 34 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
79 changes: 43 additions & 36 deletions lib/text/vtt_text_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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(
Expand Down
84 changes: 0 additions & 84 deletions test/hls/hls_live_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
35 changes: 22 additions & 13 deletions test/text/vtt_text_parser_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
Expand All @@ -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', () => {
Expand Down

0 comments on commit 1ada5e7

Please sign in to comment.