From 2ece86fde61e5f7f942c3bbdeb6eafe5fc50d8d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Wed, 7 Jun 2023 11:37:48 +0200 Subject: [PATCH] feat(HLS): Add support to _HLS_msn query param in LL streams (#5262) --- lib/hls/hls_parser.js | 34 ++++++++++++++++++++++++++++------ test/hls/hls_live_unit.js | 36 +++++++++++++++++++++++++----------- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 0f47adb510..9a03bcde80 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -342,10 +342,21 @@ shaka.hls.HlsParser = class { async updateStream_(streamInfo) { const manifestUri = streamInfo.absoluteMediaPlaylistUri; const uriObj = new goog.Uri(manifestUri); - if (this.lowLatencyMode_ && streamInfo.canSkipSegments) { - // Enable delta updates. This will replace older segments with - // 'EXT-X-SKIP' tag in the media playlist. - uriObj.setQueryData(new goog.Uri.QueryData('_HLS_skip=YES')); + if (this.lowLatencyMode_) { + const queryData = new goog.Uri.QueryData(); + if (streamInfo.canSkipSegments) { + // Enable delta updates. This will replace older segments with + // 'EXT-X-SKIP' tag in the media playlist. + queryData.add('_HLS_skip', 'YES'); + } + if (streamInfo.lastMediaSequence >= 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)); + } + if (queryData.getCount()) { + uriObj.setQueryData(queryData); + } } const response = await this.requestManifest_(uriObj.toString(), /* isPlaylist= */ true); @@ -388,6 +399,10 @@ 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); @@ -395,8 +410,6 @@ 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 playlistStartTime = mediaSequenceToStartTime.get( mediaSequenceNumber); stream.segmentIndex.evict(playlistStartTime); @@ -1941,6 +1954,7 @@ shaka.hls.HlsParser = class { canSkipSegments: false, hasEndList: false, firstSequenceNumber: -1, + lastMediaSequence: -1, loadedOnce: false, }; @@ -1987,6 +2001,7 @@ shaka.hls.HlsParser = class { streamInfo.hasEndList = realStreamInfo.hasEndList; streamInfo.mediaSequenceToStartTime = realStreamInfo.mediaSequenceToStartTime; + streamInfo.lastMediaSequence = realStreamInfo.lastMediaSequence; streamInfo.loadedOnce = true; stream.segmentIndex = realStream.segmentIndex; stream.encrypted = realStream.encrypted; @@ -2272,6 +2287,9 @@ shaka.hls.HlsParser = class { const canSkipSegments = serverControlTag ? serverControlTag.getAttribute('CAN-SKIP-UNTIL') != null : false; + const lastMediaSequence = shaka.hls.Utils.getFirstTagWithNameAsNumber( + playlist.tags, 'EXT-X-MEDIA-SEQUENCE', 0); + const stream = this.makeStreamObject_(codecs, type, language, primary, name, channelsCount, closedCaptions, characteristics, forced, spatialAudio); stream.segmentIndex = segmentIndex; @@ -2290,6 +2308,7 @@ shaka.hls.HlsParser = class { canSkipSegments, hasEndList: false, firstSequenceNumber: -1, + lastMediaSequence, mediaSequenceToStartTime, loadedOnce: false, }; @@ -3603,6 +3622,7 @@ shaka.hls.HlsParser = class { * canSkipSegments: boolean, * hasEndList: boolean, * firstSequenceNumber: number, + * lastMediaSequence: number, * loadedOnce: boolean * }} * @@ -3635,6 +3655,8 @@ 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 {boolean} loadedOnce * True if the stream has been loaded at least once. */ diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index 1f793144f5..0c518dab1d 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -163,14 +163,28 @@ describe('HlsParser live', () => { * @param {shaka.extern.Manifest} manifest * @param {string} updatedMedia * @param {Array=} updatedReferences + * @param {?number=} sequenceNumber */ - async function testUpdate(manifest, updatedMedia, updatedReferences=null) { + async function testUpdate(manifest, updatedMedia, updatedReferences=null, + sequenceNumber=null) { // Replace the entries with the updated values. - fakeNetEngine - .setResponseText('test:/video', updatedMedia) - .setResponseText('test:/redirected/video', updatedMedia) - .setResponseText('test:/video2', updatedMedia) - .setResponseText('test:/audio', updatedMedia); + if (sequenceNumber == null) { + fakeNetEngine + .setResponseText('test:/video', updatedMedia) + .setResponseText('test:/redirected/video', updatedMedia) + .setResponseText('test:/video2', updatedMedia) + .setResponseText('test:/audio', updatedMedia); + } else { + fakeNetEngine + .setResponseText('test:/video?_HLS_msn=' + sequenceNumber, + updatedMedia) + .setResponseText('test:/redirected/video?_HLS_msn=' + sequenceNumber, + updatedMedia) + .setResponseText('test:/video2?_HLS_msn=' + sequenceNumber, + updatedMedia) + .setResponseText('test:/audio?_HLS_msn=' + sequenceNumber, + updatedMedia); + } await delayForUpdatePeriod(); @@ -906,7 +920,7 @@ describe('HlsParser live', () => { '#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-MEDIA-SEQUENCE:1\n', '#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=60.0\n', '#EXT-X-SKIP:SKIPPED-SEGMENTS=1\n', '#EXTINF:2,\n', @@ -916,7 +930,7 @@ describe('HlsParser live', () => { ].join(''); fakeNetEngine.setResponseText( - 'test:/video?_HLS_skip=YES', mediaWithSkippedSegments); + 'test:/video?_HLS_skip=YES&_HLS_msn=1', mediaWithSkippedSegments); playerInterface.isLowLatencyMode = () => true; @@ -926,7 +940,7 @@ describe('HlsParser live', () => { await delayForUpdatePeriod(); fakeNetEngine.expectRequest( - 'test:/video?_HLS_skip=YES', + 'test:/video?_HLS_skip=YES&_HLS_msn=1', shaka.net.NetworkingEngine.RequestType.MANIFEST, {type: shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_PLAYLIST}); @@ -960,7 +974,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]); + manifest, mediaWithSkippedSegments, [ref1, ref2, ref3], 1); }); it('skips older segments with discontinuity', async () => { @@ -1025,7 +1039,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]); + manifest, mediaWithSkippedSegments2, [ref1, ref2, ref3, ref4], 1); }); it('updates encryption keys', async () => {