From 5611274c32ffa686c100820e41f2559871f8c2b0 Mon Sep 17 00:00:00 2001 From: Pavel Fomin Date: Wed, 6 Mar 2024 19:21:45 +0300 Subject: [PATCH 1/5] API.md update removeLevel fix after rebase --- docs/API.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/API.md b/docs/API.md index 2868102f12c..3d359942437 100644 --- a/docs/API.md +++ b/docs/API.md @@ -142,7 +142,7 @@ See [API Reference](https://hlsjs-dev.video-dev.org/api-docs/) for a complete li - [`hls.maxHdcpLevel`](#hlsmaxhdcplevel) - [`hls.capLevelToPlayerSize`](#hlscapleveltoplayersize) - [`hls.bandwidthEstimate`](#hlsbandwidthestimate) - - [`hls.removeLevel(levelIndex, urlId)`](#hlsremovelevellevelindex-urlid) + - [`hls.removeLevel(levelIndex)`](#hlsremovelevellevelindex) - [Version Control](#version-control) - [`Hls.version`](#hlsversion) - [Network Loading Control API](#network-loading-control-api) @@ -1729,9 +1729,9 @@ get: Returns the current bandwidth estimate in bits/s, if available. Otherwise, set: Reset `EwmaBandWidthEstimator` using the value set as the new default estimate. This will update the value of `config.abrEwmaDefaultEstimate`. -### `hls.removeLevel(levelIndex, urlId)` +### `hls.removeLevel(levelIndex)` -Remove a loaded level from the list of levels, or a url from a level's list of redundant urls. +Remove a level from the list of loaded levels. This can be used to remove a rendition or playlist url that errors frequently from the list of levels that a user or hls.js can choose from. From 1c77fde12bf186da62cc4865ce330e0b5f9e887b Mon Sep 17 00:00:00 2001 From: Henry McIntyre Date: Mon, 8 Apr 2024 13:55:39 -0400 Subject: [PATCH 2/5] Determine canSkip based on age of last playlist request (#6300) (cherry picked from commit d802d6013c7044cdfc7db6963643abbcf1b2d351) --- api-extractor/report/hls.js.api.md | 2 +- src/controller/audio-track-controller.ts | 6 +++++- src/controller/base-playlist-controller.ts | 10 ++++----- src/controller/level-controller.ts | 6 +++++- src/controller/subtitle-track-controller.ts | 6 +++++- src/types/level.ts | 11 ++++++---- tests/unit/controller/level-controller.ts | 23 ++++++++++++++------- 7 files changed, 42 insertions(+), 22 deletions(-) diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 24aa184abd6..4a1d1129b69 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -279,7 +279,7 @@ export class BasePlaylistController implements NetworkComponentAPI { // (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; // (undocumented) diff --git a/src/controller/audio-track-controller.ts b/src/controller/audio-track-controller.ts index 2cb4b65f416..42325642e5c 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 dde6a5f7fe7..afb3a2b2850 100644 --- a/src/controller/base-playlist-controller.ts +++ b/src/controller/base-playlist-controller.ts @@ -55,6 +55,7 @@ export default class BasePlaylistController implements NetworkComponentAPI { 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 implements NetworkComponentAPI { 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); } } } @@ -298,7 +296,7 @@ export default class BasePlaylistController implements NetworkComponentAPI { msn?: number, part?: number, ): HlsUrlParameters { - let skip = getSkipValue(details, msn); + 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 4596d6ac49c..972a85f11c8 100644 --- a/src/controller/level-controller.ts +++ b/src/controller/level-controller.ts @@ -473,7 +473,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 75e28294a9b..865535822bd 100644 --- a/src/controller/subtitle-track-controller.ts +++ b/src/controller/subtitle-track-controller.ts @@ -535,7 +535,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); } 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; } 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); From 54d62c7577e228658962041f183ee529535a0417 Mon Sep 17 00:00:00 2001 From: Asen-O-Nikolov <98342935+Asen-O-Nikolov@users.noreply.github.com> Date: Wed, 10 Apr 2024 20:16:43 +0300 Subject: [PATCH 3/5] Bugfix: preferManagedMediaSource:false when MSE is not present (#6338) (cherry picked from commit 0d00e7e4e72193970d3596037882a2159c2e5f6b) --- src/controller/buffer-controller.ts | 13 ++++++++----- src/utils/mediasource-helper.ts | 6 ++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/controller/buffer-controller.ts b/src/controller/buffer-controller.ts index 729dee65fa7..ffb7f06665e 100755 --- a/src/controller/buffer-controller.ts +++ b/src/controller/buffer-controller.ts @@ -6,7 +6,10 @@ import { getCodecCompatibleName, pickMostCompleteCodecName, } from '../utils/codecs'; -import { getMediaSource } from '../utils/mediasource-helper'; +import { + getMediaSource, + isManagedMediaSource, +} from '../utils/mediasource-helper'; import { ElementaryStreamTypes } from '../loader/fragment'; import type { TrackSet } from '../types/track'; import BufferOperationQueue from './buffer-operation-queue'; @@ -89,10 +92,9 @@ export default class BufferController implements ComponentAPI { constructor(hls: Hls) { this.hls = hls; const logPrefix = '[buffer-controller]'; - this.appendSource = - hls.config.preferManagedMediaSource && - typeof self !== 'undefined' && - (self as any).ManagedMediaSource; + this.appendSource = isManagedMediaSource( + getMediaSource(hls.config.preferManagedMediaSource), + ); this.log = logger.log.bind(logger, logPrefix); this.warn = logger.warn.bind(logger, logPrefix); this.error = logger.error.bind(logger, logPrefix); @@ -190,6 +192,7 @@ export default class BufferController implements ComponentAPI { ) { const media = (this.media = data.media); const MediaSource = getMediaSource(this.appendSource); + if (media && MediaSource) { const ms = (this.mediaSource = new MediaSource()); this.log(`created media source: ${ms.constructor?.name}`); diff --git a/src/utils/mediasource-helper.ts b/src/utils/mediasource-helper.ts index f9ed92ddb65..77e799dd16b 100644 --- a/src/utils/mediasource-helper.ts +++ b/src/utils/mediasource-helper.ts @@ -15,3 +15,9 @@ export function getMediaSource( ((self as any).WebKitMediaSource as typeof MediaSource) ); } + +export function isManagedMediaSource(source: typeof MediaSource | undefined) { + return ( + typeof self !== 'undefined' && source === (self as any).ManagedMediaSource + ); +} From 380076a81d5be4e159d427c5446d7780b79e0c7d Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Wed, 10 Apr 2024 14:21:58 -0700 Subject: [PATCH 4/5] Do not use "levelCodec" to instantiate SourceBuffer when multiple audio codec strings are present in the Multivariant Playlist variant CODECS attribute with muxed content (#6341) Fixes #6337 --- src/controller/buffer-controller.ts | 7 +++++-- src/utils/codecs.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/controller/buffer-controller.ts b/src/controller/buffer-controller.ts index ffb7f06665e..b9cfa7cb9c8 100755 --- a/src/controller/buffer-controller.ts +++ b/src/controller/buffer-controller.ts @@ -929,8 +929,11 @@ export default class BufferController implements ComponentAPI { `source buffer exists for track ${trackName}, however track does not`, ); } - // use levelCodec as first priority - let codec = track.levelCodec || track.codec; + // use levelCodec as first priority unless it contains multiple comma-separated codec values + let codec = + track.levelCodec?.indexOf(',') === -1 + ? track.levelCodec + : track.codec; if (codec) { if (trackName.slice(0, 5) === 'audio') { codec = getCodecCompatibleName(codec, this.appendSource); diff --git a/src/utils/codecs.ts b/src/utils/codecs.ts index 293adaab3de..eb42ff9f975 100644 --- a/src/utils/codecs.ts +++ b/src/utils/codecs.ts @@ -193,7 +193,7 @@ export function pickMostCompleteCodecName( if (parsedCodec && parsedCodec !== 'mp4a') { return parsedCodec; } - return levelCodec; + return levelCodec ? levelCodec.split(',')[0] : levelCodec; } export function convertAVC1ToAVCOTI(codec: string) { From 23dd8c92129c16f035ffa19bf79a77263375197c Mon Sep 17 00:00:00 2001 From: Evan Burton <2407836+iamboorrito@users.noreply.github.com> Date: Wed, 10 Apr 2024 15:11:31 -0700 Subject: [PATCH 5/5] Bugfix: Patch for light player exception with audio groups (#6342) * Don't look through audio groups if environment __USE_ALT_AUDIO__ is false * Update entries 10 -> 13 to add tests to light build --- src/utils/rendition-helper.ts | 5 ++++- tests/functional/auto/setup.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/utils/rendition-helper.ts b/src/utils/rendition-helper.ts index 1e733489bb6..63dee30a3de 100644 --- a/src/utils/rendition-helper.ts +++ b/src/utils/rendition-helper.ts @@ -269,12 +269,15 @@ export function getCodecTiers( tier.fragmentError += level.fragmentError; tier.videoRanges[level.videoRange] = (tier.videoRanges[level.videoRange] || 0) + 1; - if (audioGroups) { + if (__USE_ALT_AUDIO__ && audioGroups) { audioGroups.forEach((audioGroupId) => { if (!audioGroupId) { return; } const audioGroup = audioTracksByGroup.groups[audioGroupId]; + if (!audioGroup) { + return; + } // Default audio is any group with DEFAULT=YES, or if missing then any group with AUTOSELECT=YES, or all variants tier.hasDefaultAudio = tier.hasDefaultAudio || audioTracksByGroup.hasDefaultAudio diff --git a/tests/functional/auto/setup.js b/tests/functional/auto/setup.js index 44b76df8f36..45b44167e42 100644 --- a/tests/functional/auto/setup.js +++ b/tests/functional/auto/setup.js @@ -613,7 +613,7 @@ describe(`testing hls.js playback in the browser on "${browserDescription}"`, fu const entries = Object.entries(streams); if (HlsjsLightBuild) { - entries.length = 10; + entries.length = 13; } entries