From 5f48cf62a85f605f5e0f85dd09bb1acd68a871ac Mon Sep 17 00:00:00 2001 From: henry-mcintyre Date: Fri, 22 Mar 2024 22:42:00 -0400 Subject: [PATCH 1/6] Determine canSkip based on age of last playlist request --- src/controller/base-playlist-controller.ts | 4 ++-- src/controller/level-controller.ts | 11 ++++++++++- src/types/level.ts | 11 +++++++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/controller/base-playlist-controller.ts b/src/controller/base-playlist-controller.ts index a3af6988101..976ff6caee1 100644 --- a/src/controller/base-playlist-controller.ts +++ b/src/controller/base-playlist-controller.ts @@ -1,6 +1,6 @@ import type Hls from '../hls'; import type { NetworkComponentAPI } from '../types/component-api'; -import { getSkipValue, HlsSkip, HlsUrlParameters, Level } from '../types/level'; +import { HlsSkip, HlsUrlParameters, Level } from '../types/level'; import { computeReloadInterval, mergeDetails } from '../utils/level-helper'; import { ErrorData } from '../types/events'; import { getRetryDelay, isTimeoutError } from '../utils/error-helper'; @@ -310,7 +310,7 @@ export default class BasePlaylistController msn?: number, part?: number, ): HlsUrlParameters { - let skip = getSkipValue(details, msn); + let skip: HlsSkip | undefined; if (previousDeliveryDirectives?.skip && details.deltaUpdateFailed) { msn = previousDeliveryDirectives.msn; part = previousDeliveryDirectives.part; diff --git a/src/controller/level-controller.ts b/src/controller/level-controller.ts index 05abe274fc8..443f35ab4cd 100644 --- a/src/controller/level-controller.ts +++ b/src/controller/level-controller.ts @@ -8,7 +8,12 @@ import { ManifestLoadingData, FragBufferedData, } from '../types/events'; -import { Level, VideoRangeValues, isVideoRange } from '../types/level'; +import { + Level, + VideoRangeValues, + isVideoRange, + getSkipValue, +} from '../types/level'; import { Events } from '../events'; import { ErrorTypes, ErrorDetails } from '../errors'; import { @@ -575,9 +580,13 @@ export default class LevelController extends BasePlaylistController { const currentLevel = this.currentLevel; if (currentLevel && this.shouldLoadPlaylist(currentLevel)) { + const skipValue = currentLevel.details + ? getSkipValue(currentLevel.details) + : undefined; let url = currentLevel.uri; if (hlsUrlParameters) { try { + hlsUrlParameters.skip = skipValue; url = hlsUrlParameters.addDirectives(url); } catch (error) { this.warn( diff --git a/src/types/level.ts b/src/types/level.ts index 5fddb95d0d0..c267802c0ac 100755 --- a/src/types/level.ts +++ b/src/types/level.ts @@ -59,10 +59,13 @@ export const enum HlsSkip { v2 = 'v2', } -export function getSkipValue(details: LevelDetails, msn?: number): HlsSkip { - const { canSkipUntil, canSkipDateRanges, endSN } = details; - const snChangeGoal = msn !== undefined ? msn - endSN : 0; - if (canSkipUntil && snChangeGoal < canSkipUntil) { +export function getSkipValue(details: LevelDetails): HlsSkip { + const { canSkipUntil, canSkipDateRanges, age } = details; + // A Client SHOULD NOT request a Playlist Delta Update unless it already + // has a version of the Playlist that is no older than one-half of the Skip Boundary. + // @see: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-6.3.7 + const playlistRecentEnough = age < canSkipUntil / 2; + if (canSkipUntil && playlistRecentEnough) { if (canSkipDateRanges) { return HlsSkip.v2; } From b21f0f871ef4705b9ac5eddac1aef9b1df4f4c7e Mon Sep 17 00:00:00 2001 From: Henry McIntyre Date: Sat, 30 Mar 2024 23:21:17 -0400 Subject: [PATCH 2/6] Determine Skip delivery directive for all playlist in base loadPlaylist --- src/controller/audio-track-controller.ts | 2 +- src/controller/base-playlist-controller.ts | 11 +++++++++-- src/controller/level-controller.ts | 13 ++----------- src/controller/subtitle-track-controller.ts | 2 +- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/controller/audio-track-controller.ts b/src/controller/audio-track-controller.ts index bf90a409e75..dcca1605a92 100644 --- a/src/controller/audio-track-controller.ts +++ b/src/controller/audio-track-controller.ts @@ -401,7 +401,7 @@ class AudioTrackController extends BasePlaylistController { protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void { const audioTrack = this.currentTrack; if (this.shouldLoadPlaylist(audioTrack) && audioTrack) { - super.loadPlaylist(); + super.loadPlaylist(hlsUrlParameters, audioTrack.details); const id = audioTrack.id; const groupId = audioTrack.groupId as string; let url = audioTrack.url; diff --git a/src/controller/base-playlist-controller.ts b/src/controller/base-playlist-controller.ts index 976ff6caee1..2ed7199ada2 100644 --- a/src/controller/base-playlist-controller.ts +++ b/src/controller/base-playlist-controller.ts @@ -1,6 +1,6 @@ import type Hls from '../hls'; import type { NetworkComponentAPI } from '../types/component-api'; -import { HlsSkip, HlsUrlParameters, Level } from '../types/level'; +import { HlsSkip, HlsUrlParameters, Level, getSkipValue } from '../types/level'; import { computeReloadInterval, mergeDetails } from '../utils/level-helper'; import { ErrorData } from '../types/events'; import { getRetryDelay, isTimeoutError } from '../utils/error-helper'; @@ -101,7 +101,14 @@ export default class BasePlaylistController } } - protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void { + protected loadPlaylist( + hlsUrlParameters?: HlsUrlParameters, + levelDetails?: LevelDetails, + ): void { + const skipValue = levelDetails && getSkipValue(levelDetails); + if (hlsUrlParameters) { + hlsUrlParameters.skip = skipValue; + } if (this.requestScheduled === -1) { this.requestScheduled = self.performance.now(); } diff --git a/src/controller/level-controller.ts b/src/controller/level-controller.ts index 443f35ab4cd..63c7341f0d1 100644 --- a/src/controller/level-controller.ts +++ b/src/controller/level-controller.ts @@ -8,12 +8,7 @@ import { ManifestLoadingData, FragBufferedData, } from '../types/events'; -import { - Level, - VideoRangeValues, - isVideoRange, - getSkipValue, -} from '../types/level'; +import { Level, VideoRangeValues, isVideoRange } from '../types/level'; import { Events } from '../events'; import { ErrorTypes, ErrorDetails } from '../errors'; import { @@ -575,18 +570,14 @@ export default class LevelController extends BasePlaylistController { } protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters) { - super.loadPlaylist(); const currentLevelIndex = this.currentLevelIndex; const currentLevel = this.currentLevel; + super.loadPlaylist(hlsUrlParameters, currentLevel?.details); if (currentLevel && this.shouldLoadPlaylist(currentLevel)) { - const skipValue = currentLevel.details - ? getSkipValue(currentLevel.details) - : undefined; let url = currentLevel.uri; if (hlsUrlParameters) { try { - hlsUrlParameters.skip = skipValue; url = hlsUrlParameters.addDirectives(url); } catch (error) { this.warn( diff --git a/src/controller/subtitle-track-controller.ts b/src/controller/subtitle-track-controller.ts index 982dc718f06..c39220e7e03 100644 --- a/src/controller/subtitle-track-controller.ts +++ b/src/controller/subtitle-track-controller.ts @@ -421,8 +421,8 @@ class SubtitleTrackController extends BasePlaylistController { } protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void { - super.loadPlaylist(); const currentTrack = this.currentTrack; + super.loadPlaylist(hlsUrlParameters, currentTrack?.details); if (this.shouldLoadPlaylist(currentTrack) && currentTrack) { const id = currentTrack.id; const groupId = currentTrack.groupId as string; From 5b490a4e120ea35df0c4f94b87fca56768fcd0e3 Mon Sep 17 00:00:00 2001 From: Henry McIntyre Date: Wed, 3 Apr 2024 00:25:55 -0400 Subject: [PATCH 3/6] Determine Skip delivery directive from base-playlist-controller --- src/controller/audio-track-controller.ts | 2 +- src/controller/base-playlist-controller.ts | 11 ++--------- src/controller/level-controller.ts | 2 +- src/controller/subtitle-track-controller.ts | 2 +- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/controller/audio-track-controller.ts b/src/controller/audio-track-controller.ts index dcca1605a92..bf90a409e75 100644 --- a/src/controller/audio-track-controller.ts +++ b/src/controller/audio-track-controller.ts @@ -401,7 +401,7 @@ class AudioTrackController extends BasePlaylistController { protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void { const audioTrack = this.currentTrack; if (this.shouldLoadPlaylist(audioTrack) && audioTrack) { - super.loadPlaylist(hlsUrlParameters, audioTrack.details); + super.loadPlaylist(); const id = audioTrack.id; const groupId = audioTrack.groupId as string; let url = audioTrack.url; diff --git a/src/controller/base-playlist-controller.ts b/src/controller/base-playlist-controller.ts index 2ed7199ada2..52fbfcbc294 100644 --- a/src/controller/base-playlist-controller.ts +++ b/src/controller/base-playlist-controller.ts @@ -101,14 +101,7 @@ export default class BasePlaylistController } } - protected loadPlaylist( - hlsUrlParameters?: HlsUrlParameters, - levelDetails?: LevelDetails, - ): void { - const skipValue = levelDetails && getSkipValue(levelDetails); - if (hlsUrlParameters) { - hlsUrlParameters.skip = skipValue; - } + protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void { if (this.requestScheduled === -1) { this.requestScheduled = self.performance.now(); } @@ -317,7 +310,7 @@ export default class BasePlaylistController msn?: number, part?: number, ): HlsUrlParameters { - let skip: HlsSkip | undefined; + let skip = getSkipValue(details); if (previousDeliveryDirectives?.skip && details.deltaUpdateFailed) { msn = previousDeliveryDirectives.msn; part = previousDeliveryDirectives.part; diff --git a/src/controller/level-controller.ts b/src/controller/level-controller.ts index 63c7341f0d1..05abe274fc8 100644 --- a/src/controller/level-controller.ts +++ b/src/controller/level-controller.ts @@ -570,9 +570,9 @@ export default class LevelController extends BasePlaylistController { } protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters) { + super.loadPlaylist(); const currentLevelIndex = this.currentLevelIndex; const currentLevel = this.currentLevel; - super.loadPlaylist(hlsUrlParameters, currentLevel?.details); if (currentLevel && this.shouldLoadPlaylist(currentLevel)) { let url = currentLevel.uri; diff --git a/src/controller/subtitle-track-controller.ts b/src/controller/subtitle-track-controller.ts index c39220e7e03..982dc718f06 100644 --- a/src/controller/subtitle-track-controller.ts +++ b/src/controller/subtitle-track-controller.ts @@ -421,8 +421,8 @@ class SubtitleTrackController extends BasePlaylistController { } protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void { + super.loadPlaylist(); const currentTrack = this.currentTrack; - super.loadPlaylist(hlsUrlParameters, currentTrack?.details); if (this.shouldLoadPlaylist(currentTrack) && currentTrack) { const id = currentTrack.id; const groupId = currentTrack.groupId as string; From 4ed9b875a77a07f6e789440a811308c2436f1641 Mon Sep 17 00:00:00 2001 From: Henry McIntyre Date: Wed, 3 Apr 2024 00:32:47 -0400 Subject: [PATCH 4/6] Preserve Skip delivery directive for recently loaded playlists --- src/controller/audio-track-controller.ts | 6 +++++- src/controller/base-playlist-controller.ts | 8 +++----- src/controller/level-controller.ts | 6 +++++- src/controller/subtitle-track-controller.ts | 6 +++++- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/controller/audio-track-controller.ts b/src/controller/audio-track-controller.ts index bf90a409e75..15ceeef2993 100644 --- a/src/controller/audio-track-controller.ts +++ b/src/controller/audio-track-controller.ts @@ -339,7 +339,11 @@ class AudioTrackController extends BasePlaylistController { if (trackLoaded) { return; } - const hlsUrlParameters = this.switchParams(track.url, lastTrack?.details); + const hlsUrlParameters = this.switchParams( + track.url, + lastTrack?.details, + track.details, + ); this.loadPlaylist(hlsUrlParameters); } diff --git a/src/controller/base-playlist-controller.ts b/src/controller/base-playlist-controller.ts index 52fbfcbc294..bd2aa04b019 100644 --- a/src/controller/base-playlist-controller.ts +++ b/src/controller/base-playlist-controller.ts @@ -55,6 +55,7 @@ export default class BasePlaylistController protected switchParams( playlistUri: string, previous: LevelDetails | undefined, + current: LevelDetails | undefined, ): HlsUrlParameters | undefined { const renditionReports = previous?.renditionReports; if (renditionReports) { @@ -92,11 +93,8 @@ export default class BasePlaylistController part += 1; } } - return new HlsUrlParameters( - msn, - part >= 0 ? part : undefined, - HlsSkip.No, - ); + const skip = current && getSkipValue(current); + return new HlsUrlParameters(msn, part >= 0 ? part : undefined, skip); } } } diff --git a/src/controller/level-controller.ts b/src/controller/level-controller.ts index 05abe274fc8..222c6884cd4 100644 --- a/src/controller/level-controller.ts +++ b/src/controller/level-controller.ts @@ -461,7 +461,11 @@ export default class LevelController extends BasePlaylistController { const levelDetails = level.details; if (!levelDetails || levelDetails.live) { // level not retrieved yet, or live playlist we need to (re)load it - const hlsUrlParameters = this.switchParams(level.uri, lastLevel?.details); + const hlsUrlParameters = this.switchParams( + level.uri, + lastLevel?.details, + levelDetails, + ); this.loadPlaylist(hlsUrlParameters); } } diff --git a/src/controller/subtitle-track-controller.ts b/src/controller/subtitle-track-controller.ts index 982dc718f06..5f994e573e1 100644 --- a/src/controller/subtitle-track-controller.ts +++ b/src/controller/subtitle-track-controller.ts @@ -537,7 +537,11 @@ class SubtitleTrackController extends BasePlaylistController { type, url, }); - const hlsUrlParameters = this.switchParams(track.url, lastTrack?.details); + const hlsUrlParameters = this.switchParams( + track.url, + lastTrack?.details, + track.details, + ); this.loadPlaylist(hlsUrlParameters); } From 849037edc514b299f3c4b78f0e50dc6b3353ab2a Mon Sep 17 00:00:00 2001 From: henry-mcintyre Date: Wed, 3 Apr 2024 12:28:16 -0400 Subject: [PATCH 5/6] Update API signature --- api-extractor/report/hls.js.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index fba702304a9..b71a600c503 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -267,7 +267,7 @@ export class BasePlaylistController extends Logger implements NetworkComponentAP // (undocumented) stopLoad(): void; // (undocumented) - protected switchParams(playlistUri: string, previous: LevelDetails | undefined): HlsUrlParameters | undefined; + protected switchParams(playlistUri: string, previous: LevelDetails | undefined, current: LevelDetails | undefined): HlsUrlParameters | undefined; // (undocumented) protected timer: number; } From 04ed229a4e348475e0e57e3d90f23553af023bec Mon Sep 17 00:00:00 2001 From: henry-mcintyre Date: Thu, 4 Apr 2024 11:51:20 -0400 Subject: [PATCH 6/6] Update tests with new switchParams argument --- tests/unit/controller/level-controller.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/tests/unit/controller/level-controller.ts b/tests/unit/controller/level-controller.ts index c971e8c235c..15907a8a659 100755 --- a/tests/unit/controller/level-controller.ts +++ b/tests/unit/controller/level-controller.ts @@ -48,6 +48,7 @@ type LevelControllerTestable = Omit & { switchParams: ( playlistUri: string, previous: LevelDetails | undefined, + current: LevelDetails | undefined, ) => void; redundantFailover: (levelIndex: number) => void; }; @@ -582,7 +583,7 @@ vfrag3.m4v #EXT-X-RENDITION-REPORT:URI="chunklist_vfrag100.m3u8",LAST-MSN=4,LAST-PART=1`; it('returns RENDITION-REPORT query values for the selected playlist URI', function () { - const levelDetails = M3U8Parser.parseLevelPlaylist( + const previousLevelDetails = M3U8Parser.parseLevelPlaylist( mediaPlaylist, 'http://example.com/playlist.m3u8?abc=deg', 0, @@ -590,18 +591,20 @@ vfrag3.m4v 0, {}, ); + const mockCurrentDetails = undefined; const selectedUri = 'http://example.com/chunklist_vfrag1500.m3u8'; const hlsUrlParameters = levelController.switchParams( selectedUri, - levelDetails, + previousLevelDetails, + mockCurrentDetails, ); expect(hlsUrlParameters).to.have.property('msn').which.equals(4); expect(hlsUrlParameters).to.have.property('part').which.equals(1); - expect(hlsUrlParameters).to.have.property('skip').which.equals(''); + expect(hlsUrlParameters).to.have.property('skip').to.be.undefined; }); it('returns RENDITION-REPORT query values for the selected playlist URI with additional query params', function () { - const levelDetails = M3U8Parser.parseLevelPlaylist( + const previousDetails = M3U8Parser.parseLevelPlaylist( mediaPlaylist, 'http://example.com/playlist.m3u8?abc=deg', 0, @@ -609,20 +612,22 @@ vfrag3.m4v 0, {}, ); + const mockCurrentDetails = undefined; const selectedUriWithQuery = 'http://example.com/chunklist_vfrag1500.m3u8?abc=123'; const hlsUrlParameters = levelController.switchParams( selectedUriWithQuery, - levelDetails, + previousDetails, + mockCurrentDetails, ); expect(hlsUrlParameters).to.not.be.undefined; expect(hlsUrlParameters).to.have.property('msn').which.equals(4); expect(hlsUrlParameters).to.have.property('part').which.equals(1); - expect(hlsUrlParameters).to.have.property('skip').which.equals(''); + expect(hlsUrlParameters).to.have.property('skip').to.be.undefined; }); it('returns RENDITION-REPORT exact URI match over partial match for playlist URIs with additional query params', function () { - const levelDetails = M3U8Parser.parseLevelPlaylist( + const previousLevelDetails = M3U8Parser.parseLevelPlaylist( `#EXTM3U #EXT-X-VERSION:7 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES @@ -644,11 +649,13 @@ vfrag3.m4v 0, {}, ); + const mockCurrentDetails = undefined; const selectedUriWithQuery = 'http://example.com/chunklist.m3u8?token=123'; const hlsUrlParameters = levelController.switchParams( selectedUriWithQuery, - levelDetails, + previousLevelDetails, + mockCurrentDetails, ); expect(hlsUrlParameters).to.not.be.undefined; expect(hlsUrlParameters).to.have.property('msn').which.equals(6);