From ec8804d0be2c2cd26a86bbd5737544261c29cc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Wed, 7 Jun 2023 18:53:04 +0200 Subject: [PATCH] feat(HLS): Add support to _HLS_part query param in LL streams (#5265) --- lib/hls/hls_parser.js | 90 ++++++++++++++++++++++++++++------ lib/media/segment_reference.js | 22 +++++++++ test/hls/hls_live_unit.js | 9 ++-- 3 files changed, 103 insertions(+), 18 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 9a03bcde80..2c07ae7316 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -349,10 +349,16 @@ shaka.hls.HlsParser = class { // 'EXT-X-SKIP' tag in the media playlist. queryData.add('_HLS_skip', 'YES'); } - if (streamInfo.lastMediaSequence >= 0) { + if (streamInfo.nextMediaSequence >= 0) { // Indicates that the server must hold the request until a Playlist // contains a Media Segment with Media Sequence - queryData.add('_HLS_msn', String(streamInfo.lastMediaSequence + 1)); + queryData.add('_HLS_msn', String(streamInfo.nextMediaSequence)); + } + if (streamInfo.nextPart >= 0) { + // Indicates, in combination with _HLS_msn, that the server must hold + // the request until a Playlist contains Partial Segment N of Media + // Sequence Number M or later. + queryData.add('_HLS_part', String(streamInfo.nextPart)); } if (queryData.getCount()) { uriObj.setQueryData(queryData); @@ -399,10 +405,6 @@ shaka.hls.HlsParser = class { this.playerInterface_.newDrmInfo(stream); } - const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber( - playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0); - streamInfo.lastMediaSequence = mediaSequenceNumber; - const segments = this.createSegments_( streamInfo.verbatimMediaPlaylistUri, playlist, stream.type, stream.mimeType, mediaSequenceToStartTime, mediaVariables); @@ -410,6 +412,12 @@ shaka.hls.HlsParser = class { stream.segmentIndex.mergeAndEvict( segments, this.presentationTimeline_.getSegmentAvailabilityStart()); if (segments.length) { + const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber( + playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0); + const {nextMediaSequence, nextPart} = + this.getNextMediaSequenceAndPart_(mediaSequenceNumber, segments); + streamInfo.nextMediaSequence = nextMediaSequence; + streamInfo.nextPart = nextPart; const playlistStartTime = mediaSequenceToStartTime.get( mediaSequenceNumber); stream.segmentIndex.evict(playlistStartTime); @@ -1954,7 +1962,8 @@ shaka.hls.HlsParser = class { canSkipSegments: false, hasEndList: false, firstSequenceNumber: -1, - lastMediaSequence: -1, + nextMediaSequence: -1, + nextPart: -1, loadedOnce: false, }; @@ -2001,7 +2010,8 @@ shaka.hls.HlsParser = class { streamInfo.hasEndList = realStreamInfo.hasEndList; streamInfo.mediaSequenceToStartTime = realStreamInfo.mediaSequenceToStartTime; - streamInfo.lastMediaSequence = realStreamInfo.lastMediaSequence; + streamInfo.nextMediaSequence = realStreamInfo.nextMediaSequence; + streamInfo.nextPart = realStreamInfo.nextPart; streamInfo.loadedOnce = true; stream.segmentIndex = realStream.segmentIndex; stream.encrypted = realStream.encrypted; @@ -2278,7 +2288,8 @@ shaka.hls.HlsParser = class { } const firstStartTime = segments[0].startTime; - const lastEndTime = segments[segments.length - 1].endTime; + const lastSegment = segments[segments.length - 1]; + const lastEndTime = lastSegment.endTime; /** @type {!shaka.media.SegmentIndex} */ const segmentIndex = new shaka.media.SegmentIndex(segments); @@ -2287,9 +2298,12 @@ shaka.hls.HlsParser = class { const canSkipSegments = serverControlTag ? serverControlTag.getAttribute('CAN-SKIP-UNTIL') != null : false; - const lastMediaSequence = shaka.hls.Utils.getFirstTagWithNameAsNumber( + const mediaSequenceNumber = shaka.hls.Utils.getFirstTagWithNameAsNumber( playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0); + const {nextMediaSequence, nextPart} = + this.getNextMediaSequenceAndPart_(mediaSequenceNumber, segments); + const stream = this.makeStreamObject_(codecs, type, language, primary, name, channelsCount, closedCaptions, characteristics, forced, spatialAudio); stream.segmentIndex = segmentIndex; @@ -2308,12 +2322,54 @@ shaka.hls.HlsParser = class { canSkipSegments, hasEndList: false, firstSequenceNumber: -1, - lastMediaSequence, + nextMediaSequence, + nextPart, mediaSequenceToStartTime, loadedOnce: false, }; } + /** + * Get the next msn and part + * + * @param {number} mediaSequenceNumber + * @param {!Array.} segments + * @return {{nextMediaSequence: number, nextPart:number}}} + * @private + */ + getNextMediaSequenceAndPart_(mediaSequenceNumber, segments) { + const currentMediaSequence = mediaSequenceNumber + segments.length - 1; + let nextMediaSequence = currentMediaSequence; + let nextPart = -1; + if (!segments.length) { + nextMediaSequence++; + return { + nextMediaSequence, + nextPart, + }; + } + const lastSegment = segments[segments.length - 1]; + const partialReferences = lastSegment.partialReferences; + if (!lastSegment.partialReferences.length) { + nextMediaSequence++; + return { + nextMediaSequence, + nextPart, + }; + } + nextPart = partialReferences.length - 1; + const lastPartialReference = + partialReferences[partialReferences.length - 1]; + if (!lastPartialReference.isPreload()) { + nextMediaSequence++; + nextPart = 0; + } + return { + nextMediaSequence, + nextPart, + }; + } + /** * Creates a stream object with the given parameters. @@ -2831,6 +2887,9 @@ shaka.hls.HlsParser = class { /* syncTime= */ null, partialStatus, hlsAes128Key); + if (item.name == 'EXT-X-PRELOAD-HINT') { + partial.markAsPreload(); + } partialSegmentRefs.push(partial); } // for-loop of hlsSegment.partialSegments } @@ -3622,7 +3681,8 @@ shaka.hls.HlsParser = class { * canSkipSegments: boolean, * hasEndList: boolean, * firstSequenceNumber: number, - * lastMediaSequence: number, + * nextMediaSequence: number, + * nextPart: number, * loadedOnce: boolean * }} * @@ -3655,8 +3715,10 @@ shaka.hls.HlsParser = class { * True if the stream has an EXT-X-ENDLIST tag. * @property {number} firstSequenceNumber * The sequence number of the first reference. Only calculated if needed. - * @property {number} lastMediaSequence - * The last media sequence seen. + * @property {number} nextMediaSequence + * The next media sequence. + * @property {number} nextPart + * The next part. * @property {boolean} loadedOnce * True if the stream has been loaded at least once. */ diff --git a/lib/media/segment_reference.js b/lib/media/segment_reference.js index 6ab23981aa..b75ebc39c7 100644 --- a/lib/media/segment_reference.js +++ b/lib/media/segment_reference.js @@ -262,6 +262,9 @@ shaka.media.SegmentReference = class { /** @type {shaka.media.SegmentReference.Status} */ this.status = status; + /** @type {boolean} */ + this.preload = false; + /** @type {?shaka.extern.HlsAes128Key} */ this.hlsAes128Key = hlsAes128Key; @@ -385,6 +388,25 @@ shaka.media.SegmentReference = class { this.status = shaka.media.SegmentReference.Status.UNAVAILABLE; } + /** + * Mark the reference as preload. + * + * @export + */ + markAsPreload() { + this.preload = true; + } + + /** + * Returns true if the segment is preloaded. + * + * @return {boolean} + * @export + */ + isPreload() { + return this.preload; + } + /** * Set the segment's thumbnail sprite. * diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index 0c518dab1d..ecd249766b 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -666,6 +666,7 @@ describe('HlsParser live', () => { const preloadRef = makeReference( 'test:/partial.mp4', 6, 7.5, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 210, /* endByte= */ null); + preloadRef.markAsPreload(); // ref2 is not fully published yet, so it doesn't have a segment uri. const ref2 = makeReference( @@ -930,7 +931,7 @@ describe('HlsParser live', () => { ].join(''); fakeNetEngine.setResponseText( - 'test:/video?_HLS_skip=YES&_HLS_msn=1', mediaWithSkippedSegments); + 'test:/video?_HLS_skip=YES&_HLS_msn=2', mediaWithSkippedSegments); playerInterface.isLowLatencyMode = () => true; @@ -940,7 +941,7 @@ describe('HlsParser live', () => { await delayForUpdatePeriod(); fakeNetEngine.expectRequest( - 'test:/video?_HLS_skip=YES&_HLS_msn=1', + 'test:/video?_HLS_skip=YES&_HLS_msn=2', shaka.net.NetworkingEngine.RequestType.MANIFEST, {type: shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_PLAYLIST}); @@ -974,7 +975,7 @@ describe('HlsParser live', () => { // and ref1 should be in the SegmentReferences list. // ref3 should be appended to the SegmentReferences list. await testUpdate( - manifest, mediaWithSkippedSegments, [ref1, ref2, ref3], 1); + manifest, mediaWithSkippedSegments, [ref1, ref2, ref3], 2); }); it('skips older segments with discontinuity', async () => { @@ -1039,7 +1040,7 @@ describe('HlsParser live', () => { // and ref1,ref2 should be in the SegmentReferences list. // ref3,ref4 should be appended to the SegmentReferences list. await testUpdate( - manifest, mediaWithSkippedSegments2, [ref1, ref2, ref3, ref4], 1); + manifest, mediaWithSkippedSegments2, [ref1, ref2, ref3, ref4], 3); }); it('updates encryption keys', async () => {