Skip to content

Commit

Permalink
feat(LL-HLS): create SegmentReferences for Partial Segments
Browse files Browse the repository at this point in the history
If a segment has partial segment tags, create a SegmentReference for
each partial tag, and add the list of partial SegmentReferences to the
parent SegmentReference as an attribute.
If the parent segment contains the segment tag(EXTINF tag), use the
duration information from EXTINF tag to create the SegmentReference.
Otherwise, calculate the parent segment's duration based on the partial
segments' durations.

Issue shaka-project#1525

Change-Id: I946cc007aad2ff911b69bf1c6a46df145452bfaa
  • Loading branch information
michellezhuogg committed Jun 17, 2020
1 parent 810f09f commit 244ebac
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 43 deletions.
145 changes: 107 additions & 38 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1456,32 +1456,110 @@ shaka.hls.HlsParser = class {
* @param {number} startTime
* @param {number} timestampOffset
* @param {!Map.<string, string>} variables
* @param {string} absoluteMediaPlaylistUri
* @return {!shaka.media.SegmentReference}
* @private
*/
createSegmentReference_(
initSegmentReference, previousReference, hlsSegment, startTime,
timestampOffset, variables) {
timestampOffset, variables, absoluteMediaPlaylistUri) {
const tags = hlsSegment.tags;
const absoluteSegmentUri = this.variableSubstitution_(
hlsSegment.absoluteUri, variables);
const extinfTag =
shaka.hls.Utils.getFirstTagWithName(tags, 'EXTINF');
// Create SegmentReferences for the Partial Segments.
const partialSegmentRefs = [];
if (hlsSegment.partialSegments.length) {
const enumerate = (it) => shaka.util.Iterables.enumerate(it);
for (const {i, item} of enumerate(hlsSegment.partialSegments)) {
const pPreviousReference = i == 0 ?
previousReference : partialSegmentRefs[partialSegmentRefs.length - 1];
const pStartTime = (i == 0) ? startTime : pPreviousReference.endTime;
const pDuration = Number(item.getAttributeValue('DURATION'));
const pEndTime = pStartTime + pDuration;
const pByterange = item.getAttributeValue('BYTERANGE');
const [pStartByte, pEndByte] =
this.parseByteRange_(pPreviousReference, pByterange);
const pUri = item.getAttributeValue('URI');
const pAbsoluteUri = shaka.hls.Utils.constructAbsoluteUri(
absoluteMediaPlaylistUri, pUri);

const partial = new shaka.media.SegmentReference(
pStartTime,
pEndTime,
() => [pAbsoluteUri],
pStartByte,
pEndByte,
initSegmentReference,
timestampOffset,
/* appendWindowStart= */ 0,
/* appendWindowEnd= */ Infinity);
partialSegmentRefs.push(partial);
} // for-loop of hlsSegment.partialSegments
} else {
// EXTINF tag must be available if the segment has no partial segments.
if (!extinfTag) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_REQUIRED_TAG_MISSING, 'EXTINF');
}
}
let endTime = 0;
let startByte = 0;
let endByte = null;

// If the segment has EXTINF tag, set the segment's end time, start byte
// and end byte based on the duration and byterange information.
// Otherwise, calculate the end time, start / end byte based on its partial
// segments.
// Note that the sum of partial segments durations may be slightly different
// from the parent segment's duration. In this case, use the duration from
// the parent segment tag.
if (extinfTag) {
// The EXTINF tag format is '#EXTINF:<duration>,[<title>]'.
// We're interested in the duration part.
const extinfValues = extinfTag.value.split(',');
const duration = Number(extinfValues[0]);
endTime = startTime + duration;
const byterangeTag =
shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-BYTERANGE');
const byterange = byterangeTag ? byterangeTag.value : null;
[startByte, endByte] = this.parseByteRange_(previousReference, byterange);
} else {
endTime = partialSegmentRefs[partialSegmentRefs.length - 1].endTime;
}

return new shaka.media.SegmentReference(
startTime,
endTime,
() => [absoluteSegmentUri],
startByte,
endByte,
initSegmentReference,
timestampOffset,
/* appendWindowStart= */ 0,
/* appendWindowEnd= */ Infinity,
partialSegmentRefs,
);
}

const extinfTag = this.getRequiredTag_(tags, 'EXTINF');
// The EXTINF tag format is '#EXTINF:<duration>,[<title>]'.
// We're interested in the duration part.
const extinfValues = extinfTag.value.split(',');
const duration = Number(extinfValues[0]);
const endTime = startTime + duration;

/**
* Parse the startByte and endByte.
* @param {shaka.media.SegmentReference} previousReference
* @param {?string} byterange
* @return {!Array.<number>} An array with the start byte and end byte.
* @private
*/
parseByteRange_(previousReference, byterange) {
let startByte = 0;
let endByte = null;
const byterange =
shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-BYTERANGE');

// If BYTERANGE is not specified, the segment consists of the entire
// resource.
if (byterange) {
const blocks = byterange.value.split('@');
const blocks = byterange.split('@');
const byteLength = Number(blocks[0]);
if (blocks[1]) {
startByte = Number(blocks[1]);
Expand All @@ -1492,17 +1570,7 @@ shaka.hls.HlsParser = class {
}
endByte = startByte + byteLength - 1;
}

return new shaka.media.SegmentReference(
startTime,
endTime,
() => [absoluteSegmentUri],
startByte,
endByte,
initSegmentReference,
timestampOffset,
/* appendWindowStart= */ 0,
/* appendWindowEnd= */ Infinity);
return [startByte, endByte];
}

/** @private */
Expand Down Expand Up @@ -1608,20 +1676,20 @@ shaka.hls.HlsParser = class {
mimeType, mediaSequenceNumber, item, variables, startTime);
}

const extinfTag = shaka.hls.Utils.getFirstTagWithName(
item.tags, 'EXTINF');
// Don't create a SegmentReference for a segment with only Partial
// Segments and no regular segment info (EXTINF tag).
// TODO: Remove it once creating SegmentReferences for Partial
// Segments is added.
const reference = this.createSegmentReference_(
initSegmentRef,
previousReference,
item,
startTime,
timestampOffset,
variables,
playlist.absoluteUri);

// TODO: Skip Partial SegmentReferences temporarily.
// Remove it once streaming Partial SegmentReferences is added.
const extinfTag =
shaka.hls.Utils.getFirstTagWithName(item.tags, 'EXTINF');
if (extinfTag) {
const reference = this.createSegmentReference_(
initSegmentRef,
previousReference,
item,
startTime,
timestampOffset,
variables);
references.push(reference);
}
}
Expand Down Expand Up @@ -1724,8 +1792,8 @@ shaka.hls.HlsParser = class {
// Some servers do not support Range requests, and others do not support
// the OPTIONS request which must be made before any cross-origin Range
// request. Since this fallback is expensive, warn the app developer.
shaka.log.alwaysWarn('Unable to fetch a partial HLS segment! ' +
'Falling back to a full segment request, ' +
shaka.log.alwaysWarn('Unable to fetch the starting part of HLS ' +
'segment! Falling back to a full segment request, ' +
'which is expensive! Your server should ' +
'support Range requests and CORS preflights.',
partialRequest.uris[0]);
Expand Down Expand Up @@ -1759,7 +1827,8 @@ shaka.hls.HlsParser = class {
segment,
/* startTime= */ 0,
/* timestampOffset= */ 0,
variables);
variables,
/* absoluteMediaPlaylistUri= */ '');
// 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,
Expand Down
8 changes: 7 additions & 1 deletion lib/media/segment_reference.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,13 @@ shaka.media.SegmentReference = class {
* The end of the append window for this reference, relative to the
* presentation. Any content from after this time will be removed by
* MediaSource.
* @param {!Array.<!shaka.media.SegmentReference>=} partialReferences
A list of SegmentReferences for the Partial Segments.
*/
constructor(
startTime, endTime, uris, startByte, endByte, initSegmentReference,
timestampOffset, appendWindowStart, appendWindowEnd) {
timestampOffset, appendWindowStart, appendWindowEnd,
partialReferences = []) {
goog.asserts.assert(startTime < endTime,
'startTime must be less than endTime');
goog.asserts.assert((endByte == null) || (startByte < endByte),
Expand Down Expand Up @@ -151,6 +154,9 @@ shaka.media.SegmentReference = class {

/** @type {number} */
this.appendWindowEnd = appendWindowEnd;

/** @type {!Array.<!shaka.media.SegmentReference>} */
this.partialReferences = partialReferences;
}

/**
Expand Down
40 changes: 40 additions & 0 deletions test/hls/hls_live_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,46 @@ describe('HlsParser live', () => {
expect(ref.startTime).not.toBeLessThan(rolloverOffset);
});

it('parses streams with Partial Segments', async () => {
const mediaWithPartialSegments = [
'#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-PART:DURATION=2,URI="partial.mp4"\n',
'#EXT-X-PART:DURATION=2,URI="partial2.mp4"\n',
'#EXTINF:4,\n',
'main.mp4\n',
].join('');

fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video', mediaWithPartialSegments)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData)
.setResponseValue('test:/partial.mp4', segmentData)
.setResponseValue('test:/partial2.mp4', segmentData);

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

const partialRef2 = ManifestParser.makeReference(
'test:/partial2.mp4', segmentDataStartTime + 2,
segmentDataStartTime + 4, /* baseUri= */ '', /* startByte= */ 0,
/* endByte= */ null);

const ref = ManifestParser.makeReference(
'test:/main.mp4', segmentDataStartTime, segmentDataStartTime + 4,
/* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null,
/* timestampOffset= */ 0, [partialRef, partialRef2]);

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

describe('update', () => {
it('adds new segments when they appear', async () => {
const ref1 = ManifestParser.makeReference('test:/main.mp4', 2, 4);
Expand Down
3 changes: 2 additions & 1 deletion test/test/externs/jasmine.js
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@ jasmine.matchersUtil = {};
/**
* @param {*} first
* @param {*} second
* @param {*} customEqualityTesters
* @return {boolean}
*/
jasmine.matchersUtil.equals = function(first, second) {};
jasmine.matchersUtil.equals = function(first, second, customEqualityTesters) {};
8 changes: 6 additions & 2 deletions test/test/util/manifest_parser_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ shaka.test.ManifestParser = class {
* @param {number=} startByte
* @param {?number=} endByte
* @param {number=} timestampOffset
* @param {!Array.<!shaka.media.SegmentReference>=} partialReferences
* @return {!shaka.media.SegmentReference}
*/
static makeReference(uri, start, end, baseUri = '',
startByte = 0, endByte = null, timestampOffset) {
startByte = 0, endByte = null, timestampOffset = 0,
partialReferences = []) {
const getUris = () => [baseUri + uri];

// If a test wants to verify these, they can be set explicitly after
Expand All @@ -77,6 +79,8 @@ shaka.test.ManifestParser = class {
initSegmentReference,
timestampOffset,
appendWindowStart,
appendWindowEnd);
appendWindowEnd,
partialReferences,
);
}
};
6 changes: 5 additions & 1 deletion test/test/util/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,11 @@ shaka.test.Util = class {

// Compare those using Jasmine's utility, which will compare the fields of
// an object and the items of an array.
return jasmine.matchersUtil.equals(trimmedFirst, trimmedSecond);
const customEqualityTesters = [
shaka.test.Util.compareReferences,
];
return jasmine.matchersUtil.equals(
trimmedFirst, trimmedSecond, customEqualityTesters);
}

return undefined;
Expand Down

0 comments on commit 244ebac

Please sign in to comment.