From 244ebacac3653d65c6befa1ddbc22be643d66893 Mon Sep 17 00:00:00 2001 From: michellezhuo Date: Thu, 28 May 2020 08:22:50 -0700 Subject: [PATCH] feat(LL-HLS): create SegmentReferences for Partial Segments 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 #1525 Change-Id: I946cc007aad2ff911b69bf1c6a46df145452bfaa --- lib/hls/hls_parser.js | 145 ++++++++++++++++++------- lib/media/segment_reference.js | 8 +- test/hls/hls_live_unit.js | 40 +++++++ test/test/externs/jasmine.js | 3 +- test/test/util/manifest_parser_util.js | 8 +- test/test/util/util.js | 6 +- 6 files changed, 167 insertions(+), 43 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 4e2d339fde..001b167057 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -1456,32 +1456,110 @@ shaka.hls.HlsParser = class { * @param {number} startTime * @param {number} timestampOffset * @param {!Map.} 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:,[]'. + // 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]); @@ -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 */ @@ -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); } } @@ -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]); @@ -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, diff --git a/lib/media/segment_reference.js b/lib/media/segment_reference.js index a9f8744dca..7352595753 100644 --- a/lib/media/segment_reference.js +++ b/lib/media/segment_reference.js @@ -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), @@ -151,6 +154,9 @@ shaka.media.SegmentReference = class { /** @type {number} */ this.appendWindowEnd = appendWindowEnd; + + /** @type {!Array.<!shaka.media.SegmentReference>} */ + this.partialReferences = partialReferences; } /** diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index aaa15e4bf9..3730d40db8 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -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); diff --git a/test/test/externs/jasmine.js b/test/test/externs/jasmine.js index f9f7bbb47d..bb28c8b85e 100644 --- a/test/test/externs/jasmine.js +++ b/test/test/externs/jasmine.js @@ -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) {}; diff --git a/test/test/util/manifest_parser_util.js b/test/test/util/manifest_parser_util.js index 102be2282f..8ce6979933 100644 --- a/test/test/util/manifest_parser_util.js +++ b/test/test/util/manifest_parser_util.js @@ -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 @@ -77,6 +79,8 @@ shaka.test.ManifestParser = class { initSegmentReference, timestampOffset, appendWindowStart, - appendWindowEnd); + appendWindowEnd, + partialReferences, + ); } }; diff --git a/test/test/util/util.js b/test/test/util/util.js index 934dc2c7b8..8aedcd2706 100644 --- a/test/test/util/util.js +++ b/test/test/util/util.js @@ -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;