Skip to content

Commit

Permalink
Add Support for HLS Discontinuity
Browse files Browse the repository at this point in the history
Closes shaka-project#2397
Closes shaka-project#1335

Change-Id: I6f540c42c72bf0ae36239b682d4016cca3981c0f
  • Loading branch information
michellezhuogg committed Apr 9, 2020
1 parent e24fec4 commit 734d129
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 10 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ HLS features supported:
- VOD, Live, and Event types
- Encrypted content with Widevine
- ISO-BMFF / MP4 / CMAF support
- MPEG-2 TS support (transmuxing provided by [mux.js][] v5.1.3+, must be
- MPEG-2 TS support (transmuxing provided by [mux.js][] v5.5.3+, must be
separately included)
- WebVTT and TTML
- CEA-608/708 captions
Expand Down Expand Up @@ -174,7 +174,7 @@ Shaka Player supports:
SegmentTemplate@index
- Not supported in HLS
- MPEG-2 TS
- With help from [mux.js][] v5.1.3+, can be played on any browser which
- With help from [mux.js][] v5.5.3+, can be played on any browser which
supports MP4
- Can find and parse timestamps to find segment start time in HLS
- WebVTT
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ The only browsers capable of playing TS natively are Edge and Chromecast. You
will get a `CONTENT_UNSUPPORTED_BY_BROWSER` error on other browsers due to
their lack of TS support.

You can enable transmuxing by [including mux.js][] v5.1.3+ in your application.
You can enable transmuxing by [including mux.js][] v5.5.3+ in your application.
If Shaka Player detects that mux.js has been loaded, we will use it to transmux
TS content into MP4 on-the-fly, so that the content can be played by the
browser.
Expand Down
58 changes: 54 additions & 4 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ shaka.hls.HlsParser = class {
* its BYTERANGE if available.
* {!Map.<string, !shaka.media.InitSegmentReference>} */
this.mapTagToInitSegmentRefMap_ = new Map();

/**
* A cache mapping a discontinuity sequence number of a segment with
* EXT-X-DISCONTINUITY tag into its timestamp offset.
* Key: the discontinuity sequence number of a segment
* Value: the segment reference's timestamp offset.
* {!Map.<number, number>}
*/
this.discontinuityToTso_ = new Map();
}


Expand Down Expand Up @@ -1471,6 +1480,14 @@ shaka.hls.HlsParser = class {
mimeType, mediaSequenceNumber);
shaka.log.debug('First segment', firstSegmentUri.split('/').pop(),
'starts at', firstStartTime);

const discontinuitySequenceTag = shaka.hls.Utils.getFirstTagWithName(
playlist.tags, 'EXT-X-DISCONTINUITY-SEQUENCE');
let discontintuitySequenceNum =
discontinuitySequenceTag ? Number(discontinuitySequenceTag.value) : 0;
let timestampOffset =
this.discontinuityToTso_.get(discontintuitySequenceNum) || 0;

const enumerate = (it) => shaka.util.Iterables.enumerate(it);
for (const {i, item} of enumerate(hlsSegments)) {
const previousReference = references[references.length - 1];
Expand All @@ -1483,13 +1500,43 @@ shaka.hls.HlsParser = class {
initSegmentRef = mapTag ?
this.getInitSegmentReference_(playlist.absoluteUri, mapTag) : null;

const discontintuityTag = shaka.hls.Utils.getFirstTagWithName(item.tags,
'EXT-X-DISCONTINUITY');

if (discontintuityTag) {
discontintuitySequenceNum++;
if (this.discontinuityToTso_.has(discontintuitySequenceNum)) {
timestampOffset =
this.discontinuityToTso_.get(discontintuitySequenceNum);
} else {
// Create a tmp SegmentReference to fetch the start time of the
// segment.
const tmpSegmentRef = this.createSegmentReference_(
initSegmentRef,
/* previousReference= */ null,
item,
position,
/* startTime= */ 0,
/* timestampOffset= */ 0);

// eslint-disable-next-line no-await-in-loop
const mediaStartTime = await this.getStartTime_(
verbatimMediaPlaylistUri, initSegmentRef, tmpSegmentRef, mimeType,
mediaSequenceNumber, /* isDiscontinuity= */ true);
timestampOffset = startTime - mediaStartTime;
shaka.log.v1('Segment timestampOffset =', timestampOffset);
this.discontinuityToTso_.set(
discontintuitySequenceNum, timestampOffset);
}
}

const reference = this.createSegmentReference_(
initSegmentRef,
previousReference,
item,
position,
startTime,
/* timestampOffset= */ 0);
timestampOffset);
references.push(reference);
}

Expand Down Expand Up @@ -1613,26 +1660,29 @@ shaka.hls.HlsParser = class {
* @param {!shaka.media.SegmentReference} segmentRef
* @param {string} mimeType
* @param {number} mediaSequenceNumber
* @param {boolean=} isDiscontinuity
* @return {!Promise.<number>}
* @private
*/
async getStartTime_(
verbatimMediaPlaylistUri, initSegmentRef, segmentRef, mimeType,
mediaSequenceNumber) {
mediaSequenceNumber, isDiscontinuity) {
// If we are updating the manifest, we can usually skip fetching the segment
// by examining the references we already have. This won't be possible if
// there was some kind of lag or delay updating the manifest on the server,
// in which extreme case we would fall back to fetching a segment. This
// allows us to both avoid fetching segments when possible, and recover from
// certain server-side issues gracefully.
if (this.manifest_) {
// Do not use cached start time for the segments with discontinuity tags.
if (this.manifest_ && !isDiscontinuity) {
const streamInfo =
this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
const startTime = streamInfo.mediaSequenceToStartTime.get(
mediaSequenceNumber);
if (startTime != undefined) {
// We found it! Avoid fetching and parsing the segment.
shaka.log.v1('Found segment start time in previous manifest');
shaka.log.v1('Found segment start time in previous manifest',
startTime);
return startTime;
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"less": "^3.9.0",
"less-plugin-clean-css": "^1.5.1",
"material-design-lite": "^1.3.0",
"mux.js": "^5.1.3",
"mux.js": "^5.5.3",
"pwacompat": "^2.0.10",
"rimraf": "^2.6.3",
"sprintf-js": "^1.1.2",
Expand Down
95 changes: 95 additions & 0 deletions test/hls/hls_live_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,29 @@ describe('HlsParser live', () => {
mediaWithManySegments += 'main.mp4\n';
}

const mediaWithDiscontinuity = [
'#EXTM3U\n',
'#EXT-X-TARGETDURATION:5\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXT-X-MEDIA-SEQUENCE:0\n',
'#EXT-X-DISCONTINUITY-SEQUENCE:30\n',
'#EXTINF:2,\n',
'main.mp4\n',
'#EXT-X-DISCONTINUITY\n',
'#EXTINF:2,\n',
'main2.mp4\n',
].join('');

const mediaWithUpdatedDiscontinuitySegment = [
'#EXTM3U\n',
'#EXT-X-TARGETDURATION:5\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXT-X-MEDIA-SEQUENCE:1\n',
'#EXT-X-DISCONTINUITY-SEQUENCE:31\n',
'#EXTINF:2,\n',
'main2.mp4\n',
].join('');

it('starts presentation as VOD when ENDLIST is present', async () => {
fakeNetEngine
.setResponseText('test:/master', master)
Expand Down Expand Up @@ -495,6 +518,32 @@ describe('HlsParser live', () => {
});
});

it('sets timestamp offset for segments with discontinuity', async () => {
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video', mediaWithDiscontinuity)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData)
.setResponseValue('test:/main2.mp4', segmentData);

const ref1 = ManifestParser.makeReference(
'test:/main.mp4', segmentDataStartTime, segmentDataStartTime + 2,
/* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null,
/* timestampOffset= */ 0);

// Expect the timestamp offset to be set for the segment after the
// EXT-X-DISCONTINUITY tag.
const ref2 = ManifestParser.makeReference(
'test:/main2.mp4', segmentDataStartTime + 2, segmentDataStartTime + 4,
/* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null,
/* timestampOffset= */ 2);

const manifest = await parser.start('test:/master', playerInterface);
const video = manifest.variants[0].video;
await video.createSegmentIndex();
ManifestParser.verifySegmentIndex(video, [ref1, ref2]);
});

it('offsets VTT text with rolled over TS timestamps', async () => {
const masterWithVtt = [
'#EXTM3U\n',
Expand Down Expand Up @@ -643,6 +692,52 @@ describe('HlsParser live', () => {
shaka.net.NetworkingEngine.RequestType.MANIFEST);
});

it('reuses cached timestamp offset for segments with discontinuity',
async () => {
fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video', mediaWithDiscontinuity)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData)
.setResponseValue('test:/main2.mp4', segmentData);

const ref1 = ManifestParser.makeReference('test:/main.mp4',
segmentDataStartTime, segmentDataStartTime + 2);

const ref2 = ManifestParser.makeReference('test:/main2.mp4',
segmentDataStartTime + 2, segmentDataStartTime + 4);

const manifest =
await parser.start('test:/master', playerInterface);

const video = manifest.variants[0].video;
await video.createSegmentIndex();
ManifestParser.verifySegmentIndex(video, [ref1, ref2]);

fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video',
mediaWithUpdatedDiscontinuitySegment)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main2.mp4', segmentData);

fakeNetEngine.request.calls.reset();
await delayForUpdatePeriod();

ManifestParser.verifySegmentIndex(video, [ref2]);

// Only one request should be made, and it's for the playlist.
// Expect to use the cached timestamp offset for the main2.mp4
// segment, without fetching the start time again.
expect(fakeNetEngine.request).toHaveBeenCalledTimes(1);
fakeNetEngine.expectRequest(
'test:/video',
shaka.net.NetworkingEngine.RequestType.MANIFEST);
fakeNetEngine.expectNoRequest(
'test:/main.mp4',
shaka.net.NetworkingEngine.RequestType.SEGMENT);
});

it('parses start time from ts segments', async () => {
const tsMediaPlaylist =
mediaWithRemovedSegment.replace(/\.mp4/g, '.ts');
Expand Down
6 changes: 4 additions & 2 deletions test/test/util/manifest_parser_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ shaka.test.ManifestParser = class {
* @param {string=} baseUri
* @param {number=} startByte
* @param {?number=} endByte
* @param {number=} timestampOffset
* @return {!shaka.media.SegmentReference}
*/
static makeReference(uri, start, end, baseUri = '',
startByte = 0, endByte = null) {
startByte = 0, endByte = null, timestampOffset) {
const getUris = () => [baseUri + uri];

// If a test wants to verify these, they can be set explicitly after
Expand All @@ -65,7 +66,8 @@ shaka.test.ManifestParser = class {
},
});

const timestampOffset = /** @type {?} */(jasmine.any(Number));
timestampOffset =
timestampOffset || /** @type {?} */(jasmine.any(Number));
const appendWindowStart = /** @type {?} */(jasmine.any(Number));
const appendWindowEnd = /** @type {?} */(jasmine.any(Number));

Expand Down

0 comments on commit 734d129

Please sign in to comment.