diff --git a/.node-version b/.node-version index 5b540673a82..7377d130eda 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -22.16.0 +22.17.1 diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 2d780afb20b..d1eca8cb450 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -197,7 +197,7 @@ export class AudioStreamController extends BaseStreamController implements Netwo // (undocumented) protected onHandlerDestroying(): void; // (undocumented) - onInitPtsFound(event: Events.INIT_PTS_FOUND, { frag, id, initPTS, timescale }: InitPTSFoundData): void; + onInitPtsFound(event: Events.INIT_PTS_FOUND, { frag, id, initPTS, timescale, trackId }: InitPTSFoundData): void; // (undocumented) protected onManifestLoading(): void; // (undocumented) @@ -458,7 +458,7 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP // (undocumented) get inFlightFrag(): InFlightData; // (undocumented) - protected initPTS: RationalTimestamp[]; + protected initPTS: TimestampOffset[]; // (undocumented) protected isLoopLoading(frag: Fragment, targetBufferTime: number): boolean; // (undocumented) @@ -631,7 +631,7 @@ export interface BufferAppendingData { // (undocumented) chunkMeta: ChunkMetadata; // (undocumented) - data: Uint8Array; + data: Uint8Array; // (undocumented) frag: Fragment; // (undocumented) @@ -1807,7 +1807,7 @@ export class Fragment extends BaseSegment { level: number; // (undocumented) levelkeys?: { - [key: string]: LevelKey; + [key: string]: LevelKey | undefined; }; // (undocumented) loader: Loader | null; @@ -1925,7 +1925,7 @@ export class FragmentTracker implements ComponentAPI { detectPartialFragments(data: FragBufferedData): void; // (undocumented) fragBuffered(frag: MediaFragment, force?: true): void; - getAppendedFrag(position: number, levelType: PlaylistLevelType): Fragment | Part | null; + getAppendedFrag(position: number, levelType: PlaylistLevelType): MediaFragment | Part | null; getBufferedFrag(position: number, levelType: PlaylistLevelType): MediaFragment | null; // (undocumented) getFragAtPos(position: number, levelType: PlaylistLevelType, buffered?: boolean): MediaFragment | null; @@ -2188,12 +2188,14 @@ export class HlsAssetPlayer { // (undocumented) get duration(): number; // (undocumented) - readonly hls: Hls; + hls: Hls | null; // (undocumented) - readonly interstitial: InterstitialEvent; + interstitial: InterstitialEvent; // (undocumented) get interstitialId(): InterstitialId; // (undocumented) + loadSource(): void; + // (undocumented) get media(): HTMLMediaElement | null; // (undocumented) off(event: E, listener: HlsListeners[E], context?: Context): void; @@ -2584,6 +2586,8 @@ export interface InitPTSFoundData { initPTS: number; // (undocumented) timescale: number; + // (undocumented) + trackId: number; } // Warning: (ae-missing-release-tag) "InitSegmentData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -2798,6 +2802,8 @@ export interface InterstitialPlayer { // (undocumented) assetPlayers: (HlsAssetPlayer | null)[]; // (undocumented) + bufferedEnd: number; + // (undocumented) currentTime: number; // (undocumented) duration: number; @@ -4796,6 +4802,13 @@ export enum TimelineOccupancy { Range = 1 } +// Warning: (ae-missing-release-tag) "TimestampOffset" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type TimestampOffset = RationalTimestamp & { + trackId: number; +}; + // Warning: (ae-missing-release-tag) "Track" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -4866,7 +4879,7 @@ export class TransmuxerInterface { // (undocumented) flush(chunkMeta: ChunkMetadata): void; // (undocumented) - push(data: ArrayBuffer, initSegmentData: Uint8Array | undefined, audioCodec: string | undefined, videoCodec: string | undefined, frag: MediaFragment, part: Part | null, duration: number, accurateTimeOffset: boolean, chunkMeta: ChunkMetadata, defaultInitPTS?: RationalTimestamp): void; + push(data: ArrayBuffer, initSegmentData: Uint8Array | undefined, audioCodec: string | undefined, videoCodec: string | undefined, frag: MediaFragment, part: Part | null, duration: number, accurateTimeOffset: boolean, chunkMeta: ChunkMetadata, defaultInitPTS?: TimestampOffset): void; // (undocumented) reset(): void; } diff --git a/build-config.js b/build-config.js index 78dbc2464e5..755e6baea58 100644 --- a/build-config.js +++ b/build-config.js @@ -136,12 +136,12 @@ const babelTsWithPresetEnvTargets = ({ targets, stripConsole }) => ], plugins: [ [ - '@babel/plugin-proposal-class-properties', + '@babel/plugin-transform-class-properties', { loose: true, }, ], - '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-transform-object-rest-spread', { visitor: { CallExpression: function (espath) { @@ -172,7 +172,7 @@ const babelTsWithPresetEnvTargets = ({ targets, stripConsole }) => }, }, ['@babel/plugin-transform-object-assign'], - ['@babel/plugin-proposal-optional-chaining'], + ['@babel/plugin-transform-optional-chaining'], ...(stripConsole ? [ diff --git a/docs/API.md b/docs/API.md index 88938b4077d..9bb2807e443 100644 --- a/docs/API.md +++ b/docs/API.md @@ -350,22 +350,44 @@ hls.on(Hls.Events.ERROR, function (event, data) { #### Fatal Error Recovery -HLS.js provides several methods for attempting playback recover in the event of a decoding error in the HTMLMediaElement: +HLS.js provides methods for attempting playback recover in the event of a decoding error in the HTMLMediaElement: ##### `hls.recoverMediaError()` -Resets the MediaSource and restarts streaming from the last known playhead position. +Resets the MediaSource and restarts streaming from the last known playhead position. This should only be used when the media element is in an error state. +It should not be used in response to non-fatal hls.js error events. ###### Error recovery sample code ```js -hls.on(Hls.Events.ERROR, function (event, data) { +let attemptedErrorRecovery = null; + +video.addEventListener('error', (event) { + const mediaError = event.currentTarget.error; + if (mediaError.code === mediaError.MEDIA_ERR_DECODE) { + const now = Date.now(); + if (!attemptedErrorRecovery || now - attemptedErrorRecovery > 5000) { + attemptedErrorRecovery = now; + hls.recoverMediaError(); + } + } +}); + +hls.on(Hls.Events.ERROR, function (name, data) { + // Special handling is only needed to errors flagged as `fatal`. if (data.fatal) { switch (data.type) { - case Hls.ErrorTypes.MEDIA_ERROR: - console.log('fatal media error encountered, try to recover'); - hls.recoverMediaError(); + case Hls.ErrorTypes.MEDIA_ERROR: { + const now = Date.now(); + if (!attemptedErrorRecovery || now - attemptedErrorRecovery > 5000) { + console.log('Fatal media error encountered (' + video.error + + '), attempting to recover'); + attemptedErrorRecovery = now; + hls.recoverMediaError(); + } else { + console.log('Skipping media error recovery (only ' + (now - attemptedErrorRecovery) + 'ms since last error)'); + } break; + } case Hls.ErrorTypes.NETWORK_ERROR: console.error('fatal network error encountered', data); // All retries and media options have been exhausted. diff --git a/docs/design.md b/docs/design.md index 5e2c8244cad..be372055927 100644 --- a/docs/design.md +++ b/docs/design.md @@ -64,7 +64,6 @@ design idea is pretty simple : - [src/controller/id3-track-controller.ts][] - in charge of creating the id3 metadata text track and adding cues to that track in response to the FRAG_PARSING_METADATA event. the raw id3 data is base64 encoded and stored in the cue's text property. - [src/controller/level-controller.ts][] - - handling quality level set/get ((re)loading stream manifest/switching levels) - in charge of scheduling playlist (re)loading - monitors fragment and key loading errors. Performs fragment hunt by switching between primary and backup streams and down-shifting a level till `fragLoadingMaxRetry` limit is reached. @@ -83,7 +82,6 @@ design idea is pretty simple : **Retry Recommendations** By not having multiple renditions, recovery logic will not be able to add extra value to your platform. In order to have good results for dual constraint media hunt, specify big enough limits for fragments and levels retries. - - Level: don't use total retry less than `3 - 4` - Fragment: don't use total retry less than `4 - 6` - Implement short burst retries (i.e. small retry delay `0.5 - 4` seconds), and when library returns fatal error switch to a different CDN diff --git a/package-lock.json b/package-lock.json index a538965a2ee..caa67a406e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,10 @@ "devDependencies": { "@babel/core": "7.28.0", "@babel/helper-module-imports": "7.27.1", - "@babel/plugin-proposal-class-properties": "7.18.6", - "@babel/plugin-proposal-object-rest-spread": "7.20.7", - "@babel/plugin-proposal-optional-chaining": "7.21.0", + "@babel/plugin-transform-class-properties": "7.27.1", "@babel/plugin-transform-object-assign": "7.27.1", + "@babel/plugin-transform-object-rest-spread": "7.28.0", + "@babel/plugin-transform-optional-chaining": "7.27.1", "@babel/preset-env": "7.28.0", "@babel/preset-typescript": "7.27.1", "@babel/register": "7.27.1", @@ -25,25 +25,25 @@ "@rollup/plugin-replace": "6.0.2", "@rollup/plugin-terser": "0.4.4", "@rollup/plugin-typescript": "12.1.4", - "@svta/common-media-library": "0.15.1", + "@svta/common-media-library": "0.17.1", "@types/chai": "4.3.20", "@types/chart.js": "2.9.41", "@types/mocha": "10.0.10", "@types/sinon-chai": "3.2.12", - "@typescript-eslint/eslint-plugin": "8.35.1", - "@typescript-eslint/parser": "8.35.1", + "@typescript-eslint/eslint-plugin": "8.38.0", + "@typescript-eslint/parser": "8.38.0", "babel-loader": "10.0.0", "babel-plugin-transform-remove-console": "6.9.4", "chai": "4.5.0", "chart.js": "2.9.4", - "chromedriver": "138.0.0", + "chromedriver": "138.0.3", "doctoc": "2.2.1", "es-check": "9.1.4", "eslint": "8.57.1", - "eslint-config-prettier": "10.1.5", + "eslint-config-prettier": "10.1.8", "eslint-plugin-import": "2.32.0", "eslint-plugin-mocha": "10.5.0", - "eslint-plugin-n": "17.20.0", + "eslint-plugin-n": "17.21.0", "eslint-plugin-no-for-of-loops": "1.0.1", "eslint-plugin-promise": "7.2.1", "eventemitter3": "5.0.1", @@ -64,9 +64,9 @@ "mocha": "11.7.1", "node-fetch": "3.3.2", "npm-run-all2": "8.0.4", - "prettier": "3.5.3", + "prettier": "3.6.2", "promise-polyfill": "8.3.0", - "rollup": "4.44.1", + "rollup": "4.45.1", "rollup-plugin-istanbul": "5.0.0", "sauce-connect-launcher": "1.3.2", "selenium-webdriver": "4.34.0", @@ -75,7 +75,7 @@ "sinon-chai": "3.7.0", "typescript": "5.8.3", "url-toolkit": "2.2.5", - "wrangler": "4.22.0" + "wrangler": "4.26.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -603,58 +603,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -712,30 +660,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-typescript": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", @@ -1828,25 +1752,10 @@ "node": ">=10.0.0" } }, - "node_modules/@cloudflare/unenv-preset": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.3.3.tgz", - "integrity": "sha512-/M3MEcj3V2WHIRSW1eAQBPRJ6JnGQHc6JKMAPLkDb7pLs3m6X9ES/+K3ceGqxI6TKeF32AWAi7ls0AYzVxCP0A==", - "dev": true, - "peerDependencies": { - "unenv": "2.0.0-rc.17", - "workerd": "^1.20250508.0" - }, - "peerDependenciesMeta": { - "workerd": { - "optional": true - } - } - }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20250617.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250617.0.tgz", - "integrity": "sha512-toG8JUKVLIks4oOJLe9FeuixE84pDpMZ32ip7mCpE7JaFc5BqGFvevk0YC/db3T71AQlialjRwioH3jS/dzItA==", + "version": "1.20250726.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250726.0.tgz", + "integrity": "sha512-SOpQqQ2blLY0io/vErve44vJC1M5i7RHuMBdrdEPIEtxiLBTdOOVp4nqZ3KchocxZjskgTc2N4N3b5hNYuKDGw==", "cpu": [ "x64" ], @@ -1860,9 +1769,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20250617.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250617.0.tgz", - "integrity": "sha512-JTX0exbC9/ZtMmQQA8tDZEZFMXZrxOpTUj2hHnsUkErWYkr5SSZH04RBhPg6dU4VL8bXuB5/eJAh7+P9cZAp7g==", + "version": "1.20250726.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250726.0.tgz", + "integrity": "sha512-I+TOQ+YQahxL/K7eS2GJzv5CZzSVaZoyqfB15Q71MT/+wyzPCaFDTt+fg3uXdwpaIQEMUfqFNpTQSqbKHAYNgA==", "cpu": [ "arm64" ], @@ -1876,9 +1785,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20250617.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250617.0.tgz", - "integrity": "sha512-8jkSoVRJ+1bOx3tuWlZCGaGCV2ew7/jFMl6V3CPXOoEtERUHsZBQLVkQIGKcmC/LKSj7f/mpyBUeu2EPTo2HEg==", + "version": "1.20250726.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250726.0.tgz", + "integrity": "sha512-WSCv4o2uOW6b++ROVazrEW+jjZdBqCmXmmt7uVVfvjVxlzoYVwK9IvV2IXe4gsJ99HG9I0YCa7AT743cZ7TNNg==", "cpu": [ "x64" ], @@ -1892,9 +1801,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20250617.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250617.0.tgz", - "integrity": "sha512-YAzcOyu897z5dQKFzme1oujGWMGEJCR7/Wrrm1nSP6dqutxFPTubRADM8BHn2CV3ij//vaPnAeLmZE3jVwOwig==", + "version": "1.20250726.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250726.0.tgz", + "integrity": "sha512-jNokAGL3EQqH+31b0dX8+tlbKdjt/0UtTLvgD1e+7bOD92lzjYMa/CixHyMIY/FVvhsN4TNqfiz4cqroABTlhg==", "cpu": [ "arm64" ], @@ -1908,9 +1817,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20250617.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250617.0.tgz", - "integrity": "sha512-XWM/6sagDrO0CYDKhXhPjM23qusvIN1ju9ZEml6gOQs8tNOFnq6Cn6X9FAmnyapRFCGUSEC3HZYJAm7zwVKaMA==", + "version": "1.20250726.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250726.0.tgz", + "integrity": "sha512-DiPTY63TNh6/ylvfutNQzYZi688x6NJDjQoqf5uiCp7xHweWx+GpVs42sZPeeXqCNvhm4dYjHjuigXJNh7t8Uw==", "cpu": [ "x64" ], @@ -2477,16 +2386,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -3272,6 +3171,44 @@ "node": ">=14" } }, + "node_modules/@poppinss/colors": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.5.tgz", + "integrity": "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==", + "dev": true, + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.4.tgz", + "integrity": "sha512-iG0TIdqv8xJ3Lt9O8DrPRxw1MRLjNpoqiSGU03P/wNLP/s0ra0udPJ1J2Tx5M0J3H/cVyEgpbn8xUKRY9j59kQ==", + "dev": true, + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/dumper/node_modules/supports-color": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.0.0.tgz", + "integrity": "sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.2.tgz", + "integrity": "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==", + "dev": true + }, "node_modules/@rollup/plugin-alias": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz", @@ -3489,9 +3426,9 @@ "dev": true }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz", - "integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", + "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", "cpu": [ "arm" ], @@ -3502,9 +3439,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz", - "integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", + "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", "cpu": [ "arm64" ], @@ -3515,9 +3452,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz", - "integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", + "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", "cpu": [ "arm64" ], @@ -3528,9 +3465,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz", - "integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", + "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", "cpu": [ "x64" ], @@ -3541,9 +3478,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz", - "integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", + "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", "cpu": [ "arm64" ], @@ -3554,9 +3491,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz", - "integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", + "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", "cpu": [ "x64" ], @@ -3567,9 +3504,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz", - "integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", + "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", "cpu": [ "arm" ], @@ -3580,9 +3517,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz", - "integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", + "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", "cpu": [ "arm" ], @@ -3593,9 +3530,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz", - "integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", + "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", "cpu": [ "arm64" ], @@ -3606,9 +3543,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz", - "integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", + "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", "cpu": [ "arm64" ], @@ -3619,9 +3556,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz", - "integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", + "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", "cpu": [ "loong64" ], @@ -3632,9 +3569,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz", - "integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", + "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", "cpu": [ "ppc64" ], @@ -3645,9 +3582,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz", - "integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", + "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", "cpu": [ "riscv64" ], @@ -3658,9 +3595,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz", - "integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", + "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", "cpu": [ "riscv64" ], @@ -3671,9 +3608,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz", - "integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", + "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", "cpu": [ "s390x" ], @@ -3684,9 +3621,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz", - "integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", + "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", "cpu": [ "x64" ], @@ -3697,9 +3634,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz", - "integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", + "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", "cpu": [ "x64" ], @@ -3710,9 +3647,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz", - "integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", + "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", "cpu": [ "arm64" ], @@ -3723,9 +3660,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz", - "integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", + "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", "cpu": [ "ia32" ], @@ -3736,9 +3673,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz", - "integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", + "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", "cpu": [ "x64" ], @@ -3893,6 +3830,18 @@ "string-argv": "~0.3.1" } }, + "node_modules/@sindresorhus/is": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz", + "integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -3943,10 +3892,16 @@ "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", "dev": true }, + "node_modules/@speed-highlight/core": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.7.tgz", + "integrity": "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==", + "dev": true + }, "node_modules/@svta/common-media-library": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.15.1.tgz", - "integrity": "sha512-Ig26anQU/MVRxrikBv1cMmeJqhXG683jxA7q9iscoMygcYd4+XmLjM0kcshZ574izow4X+j/NcU5vTrvVXF9sQ==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.17.1.tgz", + "integrity": "sha512-UcmqRe1cJ/OloNEeXqKJjbw6MAKPaGZUk4yLsy1QOwtzUmo7hDVvZeIoqXlmoXZNQRQ/P1MsNuboKirsTw9e7w==", "dev": true, "engines": { "node": ">=20" @@ -4144,16 +4099,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", - "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/type-utils": "8.35.1", - "@typescript-eslint/utils": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -4167,7 +4122,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.35.1", + "@typescript-eslint/parser": "^8.38.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -4182,15 +4137,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz", - "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" }, "engines": { @@ -4206,13 +4161,13 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz", - "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", "dev": true, "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.35.1", - "@typescript-eslint/types": "^8.35.1", + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" }, "engines": { @@ -4227,13 +4182,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz", - "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1" + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4244,9 +4199,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz", - "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4260,13 +4215,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz", - "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/utils": "8.35.1", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -4283,9 +4239,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz", - "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4296,15 +4252,15 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz", - "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", "dev": true, "dependencies": { - "@typescript-eslint/project-service": "8.35.1", - "@typescript-eslint/tsconfig-utils": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -4348,15 +4304,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz", - "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1" + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4371,12 +4327,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz", - "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -4912,16 +4868,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/as-table": { - "version": "1.0.55", - "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", - "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "printable-characters": "^1.0.42" - } - }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -5509,9 +5455,9 @@ } }, "node_modules/chromedriver": { - "version": "138.0.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-138.0.0.tgz", - "integrity": "sha512-bJ/DNm5Y0TbqM71ARaAohTWVwcQ2SsWciYC5Q9Ul7DC/oTxm6B1vI2h6WscFCOOi49ul4tXZVjA/LOruljjmjA==", + "version": "138.0.3", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-138.0.3.tgz", + "integrity": "sha512-RKcfzbUthmQzFmy91F9StQQwNZ72khp3febF/RntpkDKhhCkwor0cgop00diwzAVSUq1s2e8B54Iema9FQnynw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -6342,6 +6288,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -6737,9 +6692,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", - "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -6914,13 +6869,12 @@ } }, "node_modules/eslint-plugin-n": { - "version": "17.20.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.20.0.tgz", - "integrity": "sha512-IRSoatgB/NQJZG5EeTbv/iAx1byOGdbbyhQrNvWdCfTnmPxUT0ao9/eGOeG7ljD8wJBsxwE8f6tES5Db0FRKEw==", + "version": "17.21.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.21.0.tgz", + "integrity": "sha512-1+iZ8We4ZlwVMtb/DcHG3y5/bZOdazIpa/4TySo22MLKdwrLcfrX0hbadnCvykSQCCmkAnWmIP8jZVb2AAq29A==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.5.0", - "@typescript-eslint/utils": "^8.26.1", "enhanced-resolve": "^5.17.1", "eslint-plugin-es-x": "^7.8.0", "get-tsconfig": "^4.8.1", @@ -6949,18 +6903,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/eslint-plugin-n/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint-plugin-n/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -7299,11 +7241,10 @@ } }, "node_modules/exsolve": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz", - "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", - "dev": true, - "license": "MIT" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "dev": true }, "node_modules/extend": { "version": "3.0.2", @@ -7817,24 +7758,6 @@ "node": ">= 0.4" } }, - "node_modules/get-source": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", - "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", - "dev": true, - "license": "Unlicense", - "dependencies": { - "data-uri-to-buffer": "^2.0.0", - "source-map": "^0.6.1" - } - }, - "node_modules/get-source/node_modules/data-uri-to-buffer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", - "dev": true, - "license": "MIT" - }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -7947,6 +7870,18 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -9480,6 +9415,15 @@ "node": ">=0.10.0" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -10387,9 +10331,9 @@ } }, "node_modules/miniflare": { - "version": "4.20250617.4", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250617.4.tgz", - "integrity": "sha512-IAoApFKxOJlaaFkym5ETstVX3qWzVt3xyqCDj6vSSTgEH3zxZJ5417jZGg8iQfMHosKCcQH1doPPqqnOZm/yrw==", + "version": "4.20250726.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250726.0.tgz", + "integrity": "sha512-7+/RQQ9dNsyGfR2XN2RDLultf7HHrJ5YltSXSeyQGUpzGU3iYlFhh9Smg+ygkkOJ3+trf0bgwixOnqnnWpc9ZQ==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "0.8.1", @@ -10399,10 +10343,10 @@ "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", - "undici": "^5.28.5", - "workerd": "1.20250617.0", + "undici": "^7.10.0", + "workerd": "1.20250726.0", "ws": "8.18.0", - "youch": "3.3.4", + "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { @@ -10772,16 +10716,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "dev": true, - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - } - }, "node_modules/nano-spawn": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", @@ -11655,9 +11589,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -11669,13 +11603,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/printable-characters": { - "version": "1.0.42", - "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", - "dev": true, - "license": "Unlicense" - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -12168,9 +12095,9 @@ } }, "node_modules/rollup": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", - "integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", + "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", "dev": true, "dependencies": { "@types/estree": "1.0.8" @@ -12183,26 +12110,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.44.1", - "@rollup/rollup-android-arm64": "4.44.1", - "@rollup/rollup-darwin-arm64": "4.44.1", - "@rollup/rollup-darwin-x64": "4.44.1", - "@rollup/rollup-freebsd-arm64": "4.44.1", - "@rollup/rollup-freebsd-x64": "4.44.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", - "@rollup/rollup-linux-arm-musleabihf": "4.44.1", - "@rollup/rollup-linux-arm64-gnu": "4.44.1", - "@rollup/rollup-linux-arm64-musl": "4.44.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", - "@rollup/rollup-linux-riscv64-gnu": "4.44.1", - "@rollup/rollup-linux-riscv64-musl": "4.44.1", - "@rollup/rollup-linux-s390x-gnu": "4.44.1", - "@rollup/rollup-linux-x64-gnu": "4.44.1", - "@rollup/rollup-linux-x64-musl": "4.44.1", - "@rollup/rollup-win32-arm64-msvc": "4.44.1", - "@rollup/rollup-win32-ia32-msvc": "4.44.1", - "@rollup/rollup-win32-x64-msvc": "4.44.1", + "@rollup/rollup-android-arm-eabi": "4.45.1", + "@rollup/rollup-android-arm64": "4.45.1", + "@rollup/rollup-darwin-arm64": "4.45.1", + "@rollup/rollup-darwin-x64": "4.45.1", + "@rollup/rollup-freebsd-arm64": "4.45.1", + "@rollup/rollup-freebsd-x64": "4.45.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", + "@rollup/rollup-linux-arm-musleabihf": "4.45.1", + "@rollup/rollup-linux-arm64-gnu": "4.45.1", + "@rollup/rollup-linux-arm64-musl": "4.45.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-musl": "4.45.1", + "@rollup/rollup-linux-s390x-gnu": "4.45.1", + "@rollup/rollup-linux-x64-gnu": "4.45.1", + "@rollup/rollup-linux-x64-musl": "4.45.1", + "@rollup/rollup-win32-arm64-msvc": "4.45.1", + "@rollup/rollup-win32-ia32-msvc": "4.45.1", + "@rollup/rollup-win32-x64-msvc": "4.45.1", "fsevents": "~2.3.2" } }, @@ -12951,17 +12878,6 @@ "node": "*" } }, - "node_modules/stacktracey": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", - "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", - "dev": true, - "license": "Unlicense", - "dependencies": { - "as-table": "^1.0.36", - "get-source": "^2.0.12" - } - }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -13721,16 +13637,12 @@ "dev": true }, "node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.11.0.tgz", + "integrity": "sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==", "dev": true, - "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, "engines": { - "node": ">=14.0" + "node": ">=20.18.1" } }, "node_modules/undici-types": { @@ -13740,20 +13652,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unenv": { - "version": "2.0.0-rc.17", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.17.tgz", - "integrity": "sha512-B06u0wXkEd+o5gOCMl/ZHl5cfpYbDZKAT+HWTL+Hws6jWu7dCiqBBXXXzMFcFVJb8D4ytAnYmxJA83uwOQRSsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defu": "^6.1.4", - "exsolve": "^1.0.4", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "ufo": "^1.6.1" - } - }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -14384,9 +14282,9 @@ "dev": true }, "node_modules/workerd": { - "version": "1.20250617.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250617.0.tgz", - "integrity": "sha512-Uv6p0PYUHp/W/aWfUPLkZVAoAjapisM27JJlwcX9wCPTfCfnuegGOxFMvvlYpmNaX4YCwEdLCwuNn3xkpSkuZw==", + "version": "1.20250726.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250726.0.tgz", + "integrity": "sha512-wDZqSKfIfQ2eVTUL6UawXdXEKPPyzRTnVdbhoKGq3NFrMxd+7v1cNH92u8775Qo1zO5S+GyWonQmZPFakXLvGw==", "dev": true, "hasInstallScript": true, "bin": { @@ -14396,11 +14294,11 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20250617.0", - "@cloudflare/workerd-darwin-arm64": "1.20250617.0", - "@cloudflare/workerd-linux-64": "1.20250617.0", - "@cloudflare/workerd-linux-arm64": "1.20250617.0", - "@cloudflare/workerd-windows-64": "1.20250617.0" + "@cloudflare/workerd-darwin-64": "1.20250726.0", + "@cloudflare/workerd-darwin-arm64": "1.20250726.0", + "@cloudflare/workerd-linux-64": "1.20250726.0", + "@cloudflare/workerd-linux-arm64": "1.20250726.0", + "@cloudflare/workerd-windows-64": "1.20250726.0" } }, "node_modules/workerpool": { @@ -14410,19 +14308,19 @@ "dev": true }, "node_modules/wrangler": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.22.0.tgz", - "integrity": "sha512-m8qVO3YxhUTII+4U889G/f5UuLSvMkUkCNatupV2f/SJ+iqaWtP1QbuQII8bs2J/O4rqxsz46Wu2S50u7tKB5Q==", + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.26.1.tgz", + "integrity": "sha512-zGFEtHrjTAWOngm+zwEvYCxFwMSIBrzHa3Yu6rAxYMEzsT8PPvo2rdswyUJiUkpE9s2Depr37opceaY7JxEYFw==", "dev": true, "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", - "@cloudflare/unenv-preset": "2.3.3", + "@cloudflare/unenv-preset": "2.5.0", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", - "miniflare": "4.20250617.4", + "miniflare": "4.20250726.0", "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.17", - "workerd": "1.20250617.0" + "unenv": "2.0.0-rc.19", + "workerd": "1.20250726.0" }, "bin": { "wrangler": "bin/wrangler.js", @@ -14435,7 +14333,7 @@ "fsevents": "~2.3.2" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20250617.0" + "@cloudflare/workers-types": "^4.20250726.0" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -14443,6 +14341,34 @@ } } }, + "node_modules/wrangler/node_modules/@cloudflare/unenv-preset": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.5.0.tgz", + "integrity": "sha512-CZe9B2VbjIQjBTyc+KoZcN1oUcm4T6GgCXoel9O7647djHuSRAa6sM6G+NdxWArATZgeMMbsvn9C50GCcnIatA==", + "dev": true, + "peerDependencies": { + "unenv": "2.0.0-rc.19", + "workerd": "^1.20250722.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/unenv": { + "version": "2.0.0-rc.19", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.19.tgz", + "integrity": "sha512-t/OMHBNAkknVCI7bVB9OWjUUAwhVv9vsPIAGnNUxnu3FxPQN11rjh0sksLMzc3g7IlTgvHmOTl4JM7JHpcv5wA==", + "dev": true, + "dependencies": { + "defu": "^6.1.4", + "exsolve": "^1.0.7", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "ufo": "^1.6.1" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -14780,25 +14706,35 @@ } }, "node_modules/youch": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", - "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", "dev": true, - "license": "MIT", "dependencies": { - "cookie": "^0.7.1", - "mustache": "^4.2.0", - "stacktracey": "^2.1.8" + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" } }, "node_modules/youch/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" } }, "node_modules/zod": { @@ -15210,40 +15146,6 @@ "@babel/traverse": "^7.27.1" } }, - "@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - } - }, - "@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, "@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -15278,24 +15180,6 @@ "@babel/helper-plugin-utils": "^7.27.1" } }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, "@babel/plugin-syntax-typescript": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", @@ -16018,45 +15902,38 @@ } } }, - "@cloudflare/unenv-preset": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.3.3.tgz", - "integrity": "sha512-/M3MEcj3V2WHIRSW1eAQBPRJ6JnGQHc6JKMAPLkDb7pLs3m6X9ES/+K3ceGqxI6TKeF32AWAi7ls0AYzVxCP0A==", - "dev": true, - "requires": {} - }, "@cloudflare/workerd-darwin-64": { - "version": "1.20250617.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250617.0.tgz", - "integrity": "sha512-toG8JUKVLIks4oOJLe9FeuixE84pDpMZ32ip7mCpE7JaFc5BqGFvevk0YC/db3T71AQlialjRwioH3jS/dzItA==", + "version": "1.20250726.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250726.0.tgz", + "integrity": "sha512-SOpQqQ2blLY0io/vErve44vJC1M5i7RHuMBdrdEPIEtxiLBTdOOVp4nqZ3KchocxZjskgTc2N4N3b5hNYuKDGw==", "dev": true, "optional": true }, "@cloudflare/workerd-darwin-arm64": { - "version": "1.20250617.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250617.0.tgz", - "integrity": "sha512-JTX0exbC9/ZtMmQQA8tDZEZFMXZrxOpTUj2hHnsUkErWYkr5SSZH04RBhPg6dU4VL8bXuB5/eJAh7+P9cZAp7g==", + "version": "1.20250726.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250726.0.tgz", + "integrity": "sha512-I+TOQ+YQahxL/K7eS2GJzv5CZzSVaZoyqfB15Q71MT/+wyzPCaFDTt+fg3uXdwpaIQEMUfqFNpTQSqbKHAYNgA==", "dev": true, "optional": true }, "@cloudflare/workerd-linux-64": { - "version": "1.20250617.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250617.0.tgz", - "integrity": "sha512-8jkSoVRJ+1bOx3tuWlZCGaGCV2ew7/jFMl6V3CPXOoEtERUHsZBQLVkQIGKcmC/LKSj7f/mpyBUeu2EPTo2HEg==", + "version": "1.20250726.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250726.0.tgz", + "integrity": "sha512-WSCv4o2uOW6b++ROVazrEW+jjZdBqCmXmmt7uVVfvjVxlzoYVwK9IvV2IXe4gsJ99HG9I0YCa7AT743cZ7TNNg==", "dev": true, "optional": true }, "@cloudflare/workerd-linux-arm64": { - "version": "1.20250617.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250617.0.tgz", - "integrity": "sha512-YAzcOyu897z5dQKFzme1oujGWMGEJCR7/Wrrm1nSP6dqutxFPTubRADM8BHn2CV3ij//vaPnAeLmZE3jVwOwig==", + "version": "1.20250726.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250726.0.tgz", + "integrity": "sha512-jNokAGL3EQqH+31b0dX8+tlbKdjt/0UtTLvgD1e+7bOD92lzjYMa/CixHyMIY/FVvhsN4TNqfiz4cqroABTlhg==", "dev": true, "optional": true }, "@cloudflare/workerd-windows-64": { - "version": "1.20250617.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250617.0.tgz", - "integrity": "sha512-XWM/6sagDrO0CYDKhXhPjM23qusvIN1ju9ZEml6gOQs8tNOFnq6Cn6X9FAmnyapRFCGUSEC3HZYJAm7zwVKaMA==", + "version": "1.20250726.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250726.0.tgz", + "integrity": "sha512-DiPTY63TNh6/ylvfutNQzYZi688x6NJDjQoqf5uiCp7xHweWx+GpVs42sZPeeXqCNvhm4dYjHjuigXJNh7t8Uw==", "dev": true, "optional": true }, @@ -16332,12 +16209,6 @@ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true }, - "@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "dev": true - }, "@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -16809,6 +16680,40 @@ "dev": true, "optional": true }, + "@poppinss/colors": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.5.tgz", + "integrity": "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==", + "dev": true, + "requires": { + "kleur": "^4.1.5" + } + }, + "@poppinss/dumper": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.4.tgz", + "integrity": "sha512-iG0TIdqv8xJ3Lt9O8DrPRxw1MRLjNpoqiSGU03P/wNLP/s0ra0udPJ1J2Tx5M0J3H/cVyEgpbn8xUKRY9j59kQ==", + "dev": true, + "requires": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + }, + "dependencies": { + "supports-color": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.0.0.tgz", + "integrity": "sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==", + "dev": true + } + } + }, + "@poppinss/exception": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.2.tgz", + "integrity": "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==", + "dev": true + }, "@rollup/plugin-alias": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz", @@ -16920,142 +16825,142 @@ } }, "@rollup/rollup-android-arm-eabi": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz", - "integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", + "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", "dev": true, "optional": true }, "@rollup/rollup-android-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz", - "integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", + "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", "dev": true, "optional": true }, "@rollup/rollup-darwin-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz", - "integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", + "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", "dev": true, "optional": true }, "@rollup/rollup-darwin-x64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz", - "integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", + "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", "dev": true, "optional": true }, "@rollup/rollup-freebsd-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz", - "integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", + "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", "dev": true, "optional": true }, "@rollup/rollup-freebsd-x64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz", - "integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", + "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz", - "integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", + "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm-musleabihf": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz", - "integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", + "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz", - "integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", + "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz", - "integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", + "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", "dev": true, "optional": true }, "@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz", - "integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", + "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", "dev": true, "optional": true }, "@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz", - "integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", + "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", "dev": true, "optional": true }, "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz", - "integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", + "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", "dev": true, "optional": true }, "@rollup/rollup-linux-riscv64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz", - "integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", + "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", "dev": true, "optional": true }, "@rollup/rollup-linux-s390x-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz", - "integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", + "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", "dev": true, "optional": true }, "@rollup/rollup-linux-x64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz", - "integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", + "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", "dev": true, "optional": true }, "@rollup/rollup-linux-x64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz", - "integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", + "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", "dev": true, "optional": true }, "@rollup/rollup-win32-arm64-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz", - "integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", + "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", "dev": true, "optional": true }, "@rollup/rollup-win32-ia32-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz", - "integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", + "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", "dev": true, "optional": true }, "@rollup/rollup-win32-x64-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz", - "integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", + "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", "dev": true, "optional": true }, @@ -17166,6 +17071,12 @@ "string-argv": "~0.3.1" } }, + "@sindresorhus/is": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz", + "integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==", + "dev": true + }, "@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -17215,10 +17126,16 @@ "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", "dev": true }, + "@speed-highlight/core": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.7.tgz", + "integrity": "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==", + "dev": true + }, "@svta/common-media-library": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.15.1.tgz", - "integrity": "sha512-Ig26anQU/MVRxrikBv1cMmeJqhXG683jxA7q9iscoMygcYd4+XmLjM0kcshZ574izow4X+j/NcU5vTrvVXF9sQ==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.17.1.tgz", + "integrity": "sha512-UcmqRe1cJ/OloNEeXqKJjbw6MAKPaGZUk4yLsy1QOwtzUmo7hDVvZeIoqXlmoXZNQRQ/P1MsNuboKirsTw9e7w==", "dev": true }, "@testim/chrome-version": { @@ -17411,16 +17328,16 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", - "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/type-utils": "8.35.1", - "@typescript-eslint/utils": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -17436,74 +17353,75 @@ } }, "@typescript-eslint/parser": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz", - "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" } }, "@typescript-eslint/project-service": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz", - "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", "dev": true, "requires": { - "@typescript-eslint/tsconfig-utils": "^8.35.1", - "@typescript-eslint/types": "^8.35.1", + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz", - "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", "dev": true, "requires": { - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1" + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" } }, "@typescript-eslint/tsconfig-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz", - "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", "dev": true, "requires": {} }, "@typescript-eslint/type-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz", - "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/utils": "8.35.1", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" } }, "@typescript-eslint/types": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz", - "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz", - "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", "dev": true, "requires": { - "@typescript-eslint/project-service": "8.35.1", - "@typescript-eslint/tsconfig-utils": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -17533,24 +17451,24 @@ } }, "@typescript-eslint/utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz", - "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1" + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" } }, "@typescript-eslint/visitor-keys": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz", - "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", "dev": true, "requires": { - "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" }, "dependencies": { @@ -17984,15 +17902,6 @@ "is-array-buffer": "^3.0.4" } }, - "as-table": { - "version": "1.0.55", - "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", - "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", - "dev": true, - "requires": { - "printable-characters": "^1.0.42" - } - }, "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -18427,9 +18336,9 @@ "peer": true }, "chromedriver": { - "version": "138.0.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-138.0.0.tgz", - "integrity": "sha512-bJ/DNm5Y0TbqM71ARaAohTWVwcQ2SsWciYC5Q9Ul7DC/oTxm6B1vI2h6WscFCOOi49ul4tXZVjA/LOruljjmjA==", + "version": "138.0.3", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-138.0.3.tgz", + "integrity": "sha512-RKcfzbUthmQzFmy91F9StQQwNZ72khp3febF/RntpkDKhhCkwor0cgop00diwzAVSUq1s2e8B54Iema9FQnynw==", "dev": true, "requires": { "@testim/chrome-version": "^1.1.4", @@ -19065,6 +18974,12 @@ "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true }, + "error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true + }, "es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -19445,9 +19360,9 @@ } }, "eslint-config-prettier": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", - "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "requires": {} }, @@ -19580,13 +19495,12 @@ } }, "eslint-plugin-n": { - "version": "17.20.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.20.0.tgz", - "integrity": "sha512-IRSoatgB/NQJZG5EeTbv/iAx1byOGdbbyhQrNvWdCfTnmPxUT0ao9/eGOeG7ljD8wJBsxwE8f6tES5Db0FRKEw==", + "version": "17.21.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.21.0.tgz", + "integrity": "sha512-1+iZ8We4ZlwVMtb/DcHG3y5/bZOdazIpa/4TySo22MLKdwrLcfrX0hbadnCvykSQCCmkAnWmIP8jZVb2AAq29A==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.5.0", - "@typescript-eslint/utils": "^8.26.1", "enhanced-resolve": "^5.17.1", "eslint-plugin-es-x": "^7.8.0", "get-tsconfig": "^4.8.1", @@ -19606,12 +19520,6 @@ "balanced-match": "^1.0.0" } }, - "globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", - "dev": true - }, "minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -19763,9 +19671,9 @@ "dev": true }, "exsolve": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz", - "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", "dev": true }, "extend": { @@ -20135,24 +20043,6 @@ "es-object-atoms": "^1.0.0" } }, - "get-source": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", - "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", - "dev": true, - "requires": { - "data-uri-to-buffer": "^2.0.0", - "source-map": "^0.6.1" - }, - "dependencies": { - "data-uri-to-buffer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", - "dev": true - } - } - }, "get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -20237,6 +20127,12 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, + "globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true + }, "globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -21328,6 +21224,12 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, + "kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true + }, "kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -21974,9 +21876,9 @@ "dev": true }, "miniflare": { - "version": "4.20250617.4", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250617.4.tgz", - "integrity": "sha512-IAoApFKxOJlaaFkym5ETstVX3qWzVt3xyqCDj6vSSTgEH3zxZJ5417jZGg8iQfMHosKCcQH1doPPqqnOZm/yrw==", + "version": "4.20250726.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250726.0.tgz", + "integrity": "sha512-7+/RQQ9dNsyGfR2XN2RDLultf7HHrJ5YltSXSeyQGUpzGU3iYlFhh9Smg+ygkkOJ3+trf0bgwixOnqnnWpc9ZQ==", "dev": true, "requires": { "@cspotcode/source-map-support": "0.8.1", @@ -21986,10 +21888,10 @@ "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", - "undici": "^5.28.5", - "workerd": "1.20250617.0", + "undici": "^7.10.0", + "workerd": "1.20250726.0", "ws": "8.18.0", - "youch": "3.3.4", + "youch": "4.1.0-beta.10", "zod": "3.22.3" } }, @@ -22259,12 +22161,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "dev": true - }, "nano-spawn": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", @@ -22926,15 +22822,9 @@ "dev": true }, "prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", - "dev": true - }, - "printable-characters": { - "version": "1.0.42", - "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true }, "process-nextick-args": { @@ -23302,31 +23192,31 @@ } }, "rollup": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", - "integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==", - "dev": true, - "requires": { - "@rollup/rollup-android-arm-eabi": "4.44.1", - "@rollup/rollup-android-arm64": "4.44.1", - "@rollup/rollup-darwin-arm64": "4.44.1", - "@rollup/rollup-darwin-x64": "4.44.1", - "@rollup/rollup-freebsd-arm64": "4.44.1", - "@rollup/rollup-freebsd-x64": "4.44.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", - "@rollup/rollup-linux-arm-musleabihf": "4.44.1", - "@rollup/rollup-linux-arm64-gnu": "4.44.1", - "@rollup/rollup-linux-arm64-musl": "4.44.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", - "@rollup/rollup-linux-riscv64-gnu": "4.44.1", - "@rollup/rollup-linux-riscv64-musl": "4.44.1", - "@rollup/rollup-linux-s390x-gnu": "4.44.1", - "@rollup/rollup-linux-x64-gnu": "4.44.1", - "@rollup/rollup-linux-x64-musl": "4.44.1", - "@rollup/rollup-win32-arm64-msvc": "4.44.1", - "@rollup/rollup-win32-ia32-msvc": "4.44.1", - "@rollup/rollup-win32-x64-msvc": "4.44.1", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", + "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.45.1", + "@rollup/rollup-android-arm64": "4.45.1", + "@rollup/rollup-darwin-arm64": "4.45.1", + "@rollup/rollup-darwin-x64": "4.45.1", + "@rollup/rollup-freebsd-arm64": "4.45.1", + "@rollup/rollup-freebsd-x64": "4.45.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", + "@rollup/rollup-linux-arm-musleabihf": "4.45.1", + "@rollup/rollup-linux-arm64-gnu": "4.45.1", + "@rollup/rollup-linux-arm64-musl": "4.45.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-gnu": "4.45.1", + "@rollup/rollup-linux-riscv64-musl": "4.45.1", + "@rollup/rollup-linux-s390x-gnu": "4.45.1", + "@rollup/rollup-linux-x64-gnu": "4.45.1", + "@rollup/rollup-linux-x64-musl": "4.45.1", + "@rollup/rollup-win32-arm64-msvc": "4.45.1", + "@rollup/rollup-win32-ia32-msvc": "4.45.1", + "@rollup/rollup-win32-x64-msvc": "4.45.1", "@types/estree": "1.0.8", "fsevents": "~2.3.2" }, @@ -23880,16 +23770,6 @@ "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", "dev": true }, - "stacktracey": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", - "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", - "dev": true, - "requires": { - "as-table": "^1.0.36", - "get-source": "^2.0.12" - } - }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -24416,13 +24296,10 @@ "dev": true }, "undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", - "dev": true, - "requires": { - "@fastify/busboy": "^2.0.0" - } + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.11.0.tgz", + "integrity": "sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==", + "dev": true }, "undici-types": { "version": "6.20.0", @@ -24430,19 +24307,6 @@ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true }, - "unenv": { - "version": "2.0.0-rc.17", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.17.tgz", - "integrity": "sha512-B06u0wXkEd+o5gOCMl/ZHl5cfpYbDZKAT+HWTL+Hws6jWu7dCiqBBXXXzMFcFVJb8D4ytAnYmxJA83uwOQRSsg==", - "dev": true, - "requires": { - "defu": "^6.1.4", - "exsolve": "^1.0.4", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "ufo": "^1.6.1" - } - }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -24925,16 +24789,16 @@ "dev": true }, "workerd": { - "version": "1.20250617.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250617.0.tgz", - "integrity": "sha512-Uv6p0PYUHp/W/aWfUPLkZVAoAjapisM27JJlwcX9wCPTfCfnuegGOxFMvvlYpmNaX4YCwEdLCwuNn3xkpSkuZw==", + "version": "1.20250726.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250726.0.tgz", + "integrity": "sha512-wDZqSKfIfQ2eVTUL6UawXdXEKPPyzRTnVdbhoKGq3NFrMxd+7v1cNH92u8775Qo1zO5S+GyWonQmZPFakXLvGw==", "dev": true, "requires": { - "@cloudflare/workerd-darwin-64": "1.20250617.0", - "@cloudflare/workerd-darwin-arm64": "1.20250617.0", - "@cloudflare/workerd-linux-64": "1.20250617.0", - "@cloudflare/workerd-linux-arm64": "1.20250617.0", - "@cloudflare/workerd-windows-64": "1.20250617.0" + "@cloudflare/workerd-darwin-64": "1.20250726.0", + "@cloudflare/workerd-darwin-arm64": "1.20250726.0", + "@cloudflare/workerd-linux-64": "1.20250726.0", + "@cloudflare/workerd-linux-arm64": "1.20250726.0", + "@cloudflare/workerd-windows-64": "1.20250726.0" } }, "workerpool": { @@ -24944,20 +24808,42 @@ "dev": true }, "wrangler": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.22.0.tgz", - "integrity": "sha512-m8qVO3YxhUTII+4U889G/f5UuLSvMkUkCNatupV2f/SJ+iqaWtP1QbuQII8bs2J/O4rqxsz46Wu2S50u7tKB5Q==", + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.26.1.tgz", + "integrity": "sha512-zGFEtHrjTAWOngm+zwEvYCxFwMSIBrzHa3Yu6rAxYMEzsT8PPvo2rdswyUJiUkpE9s2Depr37opceaY7JxEYFw==", "dev": true, "requires": { "@cloudflare/kv-asset-handler": "0.4.0", - "@cloudflare/unenv-preset": "2.3.3", + "@cloudflare/unenv-preset": "2.5.0", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "fsevents": "~2.3.2", - "miniflare": "4.20250617.4", + "miniflare": "4.20250726.0", "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.17", - "workerd": "1.20250617.0" + "unenv": "2.0.0-rc.19", + "workerd": "1.20250726.0" + }, + "dependencies": { + "@cloudflare/unenv-preset": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.5.0.tgz", + "integrity": "sha512-CZe9B2VbjIQjBTyc+KoZcN1oUcm4T6GgCXoel9O7647djHuSRAa6sM6G+NdxWArATZgeMMbsvn9C50GCcnIatA==", + "dev": true, + "requires": {} + }, + "unenv": { + "version": "2.0.0-rc.19", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.19.tgz", + "integrity": "sha512-t/OMHBNAkknVCI7bVB9OWjUUAwhVv9vsPIAGnNUxnu3FxPQN11rjh0sksLMzc3g7IlTgvHmOTl4JM7JHpcv5wA==", + "dev": true, + "requires": { + "defu": "^6.1.4", + "exsolve": "^1.0.7", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "ufo": "^1.6.1" + } + } } }, "wrap-ansi": { @@ -25200,24 +25086,36 @@ "dev": true }, "youch": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", - "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", "dev": true, "requires": { - "cookie": "^0.7.1", - "mustache": "^4.2.0", - "stacktracey": "^2.1.8" + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" }, "dependencies": { "cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "dev": true } } }, + "youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "requires": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, "zod": { "version": "3.22.3", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", diff --git a/package.json b/package.json index 4b1cb1ff7db..1643f6014da 100644 --- a/package.json +++ b/package.json @@ -67,10 +67,10 @@ "devDependencies": { "@babel/core": "7.28.0", "@babel/helper-module-imports": "7.27.1", - "@babel/plugin-proposal-class-properties": "7.18.6", - "@babel/plugin-proposal-object-rest-spread": "7.20.7", - "@babel/plugin-proposal-optional-chaining": "7.21.0", + "@babel/plugin-transform-class-properties": "7.27.1", "@babel/plugin-transform-object-assign": "7.27.1", + "@babel/plugin-transform-object-rest-spread": "7.28.0", + "@babel/plugin-transform-optional-chaining": "7.27.1", "@babel/preset-env": "7.28.0", "@babel/preset-typescript": "7.27.1", "@babel/register": "7.27.1", @@ -83,25 +83,25 @@ "@rollup/plugin-replace": "6.0.2", "@rollup/plugin-terser": "0.4.4", "@rollup/plugin-typescript": "12.1.4", - "@svta/common-media-library": "0.15.1", + "@svta/common-media-library": "0.17.1", "@types/chai": "4.3.20", "@types/chart.js": "2.9.41", "@types/mocha": "10.0.10", "@types/sinon-chai": "3.2.12", - "@typescript-eslint/eslint-plugin": "8.35.1", - "@typescript-eslint/parser": "8.35.1", + "@typescript-eslint/eslint-plugin": "8.38.0", + "@typescript-eslint/parser": "8.38.0", "babel-loader": "10.0.0", "babel-plugin-transform-remove-console": "6.9.4", "chai": "4.5.0", "chart.js": "2.9.4", - "chromedriver": "138.0.0", + "chromedriver": "138.0.3", "doctoc": "2.2.1", "es-check": "9.1.4", "eslint": "8.57.1", - "eslint-config-prettier": "10.1.5", + "eslint-config-prettier": "10.1.8", "eslint-plugin-import": "2.32.0", "eslint-plugin-mocha": "10.5.0", - "eslint-plugin-n": "17.20.0", + "eslint-plugin-n": "17.21.0", "eslint-plugin-no-for-of-loops": "1.0.1", "eslint-plugin-promise": "7.2.1", "eventemitter3": "5.0.1", @@ -122,9 +122,9 @@ "mocha": "11.7.1", "node-fetch": "3.3.2", "npm-run-all2": "8.0.4", - "prettier": "3.5.3", + "prettier": "3.6.2", "promise-polyfill": "8.3.0", - "rollup": "4.44.1", + "rollup": "4.45.1", "rollup-plugin-istanbul": "5.0.0", "sauce-connect-launcher": "1.3.2", "selenium-webdriver": "4.34.0", @@ -133,6 +133,6 @@ "sinon-chai": "3.7.0", "typescript": "5.8.3", "url-toolkit": "2.2.5", - "wrangler": "4.22.0" + "wrangler": "4.26.1" } } diff --git a/renovate.json b/renovate.json index 21919c957eb..69f0454f5f3 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,7 @@ { - "extends": ["config:base"], + "extends": ["config:recommended"], "labels": ["dependencies", "skip-change-log"], + "schedule": ["* 0 28 * *"], "prHourlyLimit": 0, "prConcurrentLimit": 0, "prCreation": "immediate", @@ -12,10 +13,15 @@ "major": { "addLabels": ["semver-major"] }, + "ignoreDeps": ["FileSaver.js", "@types/chart.js"], "packageRules": [ { - "matchPackagePatterns": ["*"], - "rangeStrategy": "bump" + "matchDatasources": ["html"], + "enabled": false + }, + { + "rangeStrategy": "bump", + "matchPackageNames": ["*"] }, { "matchDepTypes": ["devDependencies"], diff --git a/src/controller/audio-stream-controller.ts b/src/controller/audio-stream-controller.ts index f3517c041c9..1e4f60d64d5 100644 --- a/src/controller/audio-stream-controller.ts +++ b/src/controller/audio-stream-controller.ts @@ -142,16 +142,16 @@ class AudioStreamController // INIT_PTS_FOUND is triggered when the video track parsed in the stream-controller has a new PTS value onInitPtsFound( event: Events.INIT_PTS_FOUND, - { frag, id, initPTS, timescale }: InitPTSFoundData, + { frag, id, initPTS, timescale, trackId }: InitPTSFoundData, ) { // Always update the new INIT PTS // Can change due level switch if (id === PlaylistLevelType.MAIN) { const cc = frag.cc; const inFlightFrag = this.fragCurrent; - this.initPTS[cc] = { baseTime: initPTS, timescale }; + this.initPTS[cc] = { baseTime: initPTS, timescale, trackId }; this.log( - `InitPTS for cc: ${cc} found from main: ${initPTS}/${timescale}`, + `InitPTS for cc: ${cc} found from main: ${initPTS / timescale} (${initPTS}/${timescale}) trackId: ${trackId}`, ); this.mainAnchor = frag; // If we are waiting, tick immediately to unblock audio fragment transmuxing diff --git a/src/controller/base-playlist-controller.ts b/src/controller/base-playlist-controller.ts index 6ff6a58cd63..20d41b30ce6 100644 --- a/src/controller/base-playlist-controller.ts +++ b/src/controller/base-playlist-controller.ts @@ -87,8 +87,8 @@ export default class BasePlaylistController } if (foundIndex !== -1) { const attr = renditionReports[foundIndex]; - const msn = parseInt(attr['LAST-MSN']) || previous?.lastPartSn; - let part = parseInt(attr['LAST-PART']) || previous?.lastPartIndex; + const msn = parseInt(attr['LAST-MSN']) || previous.lastPartSn; + let part = parseInt(attr['LAST-PART']) || previous.lastPartIndex; if (this.hls.config.lowLatencyMode) { const currentGoal = Math.min( previous.age - previous.partTarget, @@ -164,7 +164,7 @@ export default class BasePlaylistController const offset = Math.max(timelineOffset || 0, 0); details.appliedTimelineOffset = offset; details.fragments.forEach((frag) => { - frag.start = frag.playlistOffset + offset; + frag.setStart(frag.playlistOffset + offset); }); } diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 67a43d88ece..1a761b030ea 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -59,7 +59,7 @@ import type { import type { Level } from '../types/level'; import type { RemuxedTrack } from '../types/remuxer'; import type { Bufferable, BufferInfo } from '../utils/buffer-helper'; -import type { RationalTimestamp } from '../utils/timescale-conversion'; +import type { TimestampOffset } from '../utils/timescale-conversion'; type ResolveFragLoaded = (FragLoadedEndData) => void; type RejectFragLoaded = (LoadError) => void; @@ -111,7 +111,7 @@ export default class BaseStreamController protected levelLastLoaded: Level | null = null; protected startFragRequested: boolean = false; protected decrypter: Decrypter; - protected initPTS: RationalTimestamp[] = []; + protected initPTS: TimestampOffset[] = []; protected buffering: boolean = true; protected loadingParts: boolean = false; private loopSn?: string | number; @@ -912,7 +912,7 @@ export default class BaseStreamController this.log( `LL-Part loading OFF after next part miss @${targetBufferTime.toFixed( 2, - )}`, + )} Check buffer at sn: ${frag.sn} loaded parts: ${details.partList?.filter((p) => p.loaded).map((p) => `[${p.start}-${p.end}]`)}`, ); this.loadingParts = false; } else if (!frag.url) { @@ -1397,7 +1397,7 @@ export default class BaseStreamController } protected get primaryPrefetch(): boolean { - if (interstitialsEnabled(this.hls.config)) { + if (interstitialsEnabled(this.config)) { const playingInterstitial = this.hls.interstitialsManager?.playingItem?.event; if (playingInterstitial) { @@ -1415,7 +1415,7 @@ export default class BaseStreamController return frag; } if ( - interstitialsEnabled(this.hls.config) && + interstitialsEnabled(this.config) && frag.type !== PlaylistLevelType.SUBTITLE ) { // Do not load fragments outside the buffering schedule segment @@ -1496,9 +1496,14 @@ export default class BaseStreamController if (loaded) { nextPart = -1; } else if ( - (contiguous || part.independent || independentAttrOmitted) && - part.fragment === frag + contiguous || + ((part.independent || independentAttrOmitted) && part.fragment === frag) ) { + if (part.fragment !== frag) { + this.warn( + `Need buffer at ${targetBufferTime} but next unloaded part starts at ${part.start}`, + ); + } nextPart = i; } contiguous = loaded; @@ -1510,8 +1515,17 @@ export default class BaseStreamController partList: Part[], targetBufferTime: number, ): boolean { - const lastPart = partList[partList.length - 1]; - return lastPart && targetBufferTime > lastPart.start && lastPart.loaded; + let part: Part; + for (let i = partList.length; i--; ) { + part = partList[i]; + if (!part.loaded) { + return false; + } + if (targetBufferTime > part.start) { + return true; + } + } + return false; } /* @@ -1898,7 +1912,7 @@ export default class BaseStreamController // this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708 // in that case flush the whole audio buffer to recover this.warn( - `Buffer full error while media.currentTime is not buffered, flush ${playlistType} buffer`, + `Buffer full error while media.currentTime (${this.getLoadPosition()}) is not buffered, flush ${playlistType} buffer`, ); } if (frag) { diff --git a/src/controller/buffer-controller.ts b/src/controller/buffer-controller.ts index 9011a9032f4..920e9220534 100755 --- a/src/controller/buffer-controller.ts +++ b/src/controller/buffer-controller.ts @@ -278,9 +278,9 @@ export default class BufferController extends Logger implements ComponentAPI { data: MediaAttachingData, ) { const media = (this.media = data.media); - const MediaSource = getMediaSource(this.appendSource); this.transferData = this.overrides = undefined; - if (media && MediaSource) { + const MediaSource = getMediaSource(this.appendSource); + if (MediaSource) { const transferringMedia = !!data.mediaSource; if (transferringMedia || data.overrides) { this.transferData = data; @@ -318,7 +318,7 @@ export default class BufferController extends Logger implements ComponentAPI { private assignMediaSource(ms: MediaSource) { this.log( - `${this.transferData?.mediaSource === ms ? 'transferred' : 'created'} media source: ${ms.constructor?.name}`, + `${this.transferData?.mediaSource === ms ? 'transferred' : 'created'} media source: ${(ms.constructor as any)?.name}`, ); // MediaSource listeners are arrow functions with a lexical scope, and do not need to be bound ms.addEventListener('sourceopen', this._onMediaSourceOpen); @@ -343,9 +343,11 @@ export default class BufferController extends Logger implements ComponentAPI { : null; const trackCount = trackNames ? trackNames.length : 0; const mediaSourceOpenCallback = () => { - if (this.media && this.mediaSourceOpenOrEnded) { - this._onMediaSourceOpen(); - } + Promise.resolve().then(() => { + if (this.media && this.mediaSourceOpenOrEnded) { + this._onMediaSourceOpen(); + } + }); }; if (transferredTracks && trackNames && trackCount) { if (!this.tracksReady) { @@ -435,7 +437,7 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe } private _onEndStreaming = (event) => { - if (!this.hls) { + if (!this.hls as any) { return; } if (this.mediaSource?.readyState !== 'open') { @@ -445,7 +447,7 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe }; private _onStartStreaming = (event) => { - if (!this.hls) { + if (!this.hls as any) { return; } this.hls.resumeBuffering(); @@ -790,7 +792,12 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe if (videoSb && sn !== 'initSegment') { const partOrFrag = part || (frag as MediaFragment); const blockedAudioAppend = this.blockedAudioAppend; - if (type === 'audio' && parent !== 'main' && !this.blockedAudioAppend) { + if ( + type === 'audio' && + parent !== 'main' && + !this.blockedAudioAppend && + !(videoTrack.ending || videoTrack.ended) + ) { const pStart = partOrFrag.start; const pTime = pStart + partOrFrag.duration * 0.05; const vbuffered = videoSb.buffered; @@ -1025,10 +1032,15 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe public get bufferedToEnd(): boolean { return ( this.sourceBufferCount > 0 && - !this.sourceBuffers.some( - ([type]) => - type && (!this.tracks[type]?.ended || this.tracks[type]?.ending), - ) + !this.sourceBuffers.some(([type]) => { + if (type) { + const track = this.tracks[type]; + if (track) { + return !track.ended || track.ending; + } + } + return false; + }) ); } @@ -1077,6 +1089,9 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe this.tracksEnded(); this.hls.trigger(Events.BUFFERED_TO_END, undefined); } + } else if (data.type === 'video') { + // Make sure pending audio appends are unblocked when video reaches end + this.unblockAudio(); } } @@ -1162,13 +1177,14 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe ); } + const frontBufferFlushThreshold = config.frontBufferFlushThreshold; if ( - Number.isFinite(config.frontBufferFlushThreshold) && - config.frontBufferFlushThreshold > 0 + Number.isFinite(frontBufferFlushThreshold) && + frontBufferFlushThreshold > 0 ) { const frontBufferLength = Math.max( config.maxBufferLength, - config.frontBufferFlushThreshold, + frontBufferFlushThreshold, ); const maxFrontBufferLength = Math.max(frontBufferLength, targetDuration); @@ -1273,7 +1289,7 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe const playlistEnd = details.edge; if (details.live && this.hls.config.liveDurationInfinity) { const len = details.fragments.length; - if (len && details.live && !!mediaSource.setLiveSeekableRange) { + if (len && !!(mediaSource as any).setLiveSeekableRange) { const start = Math.max(0, details.fragmentStart); const end = Math.max(start, playlistEnd); @@ -1547,7 +1563,7 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe }; private get mediaSrc(): string | undefined { - const media = this.media?.querySelector?.('source') || this.media; + const media = (this.media?.querySelector as any)?.('source') || this.media; return media?.src; } @@ -1647,7 +1663,10 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe } // This method must result in an updateend event; if append is not called, onSBUpdateEnd must be called manually - private appendExecutor(data: Uint8Array, type: SourceBufferName) { + private appendExecutor( + data: Uint8Array, + type: SourceBufferName, + ) { const track = this.tracks[type]; const sb = track?.buffer; if (!sb) { diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index 5ce41a32e2e..c641f3e367b 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -22,6 +22,8 @@ import { KeySystems, requestMediaKeySystemAccess, } from '../utils/mediakeys-helper'; +import { bin2str, parseSinf } from '../utils/mp4-tools'; +import { base64Decode } from '../utils/numeric-encoding-utils'; import { stringify } from '../utils/safe-json-stringify'; import { strToUtf8array } from '../utils/utf8-utils'; import type { EMEControllerConfig, HlsConfig, LoadPolicy } from '../config'; @@ -552,6 +554,105 @@ class EMEController extends Logger implements ComponentAPI { return this.attemptKeySystemAccess(keySystemsToAttempt); } + private onMediaEncrypted = (event: MediaEncryptedEvent) => { + const { initDataType, initData } = event; + const logMessage = `"${event.type}" event: init data type: "${initDataType}"`; + this.debug(logMessage); + + // Ignore event when initData is null + if (initData === null) { + return; + } + + if (!this.keyFormatPromise) { + let keySystems = Object.keys( + this.keySystemAccessPromises, + ) as KeySystems[]; + if (!keySystems.length) { + keySystems = getKeySystemsForConfig(this.config); + } + const keyFormats = keySystems + .map(keySystemDomainToKeySystemFormat) + .filter((k) => !!k) as KeySystemFormats[]; + this.keyFormatPromise = this.getKeyFormatPromise(keyFormats); + } + + this.keyFormatPromise.then((keySystemFormat) => { + const keySystem = keySystemFormatToKeySystemDomain(keySystemFormat); + if (initDataType !== 'sinf' || keySystem !== KeySystems.FAIRPLAY) { + this.log( + `Ignoring "${event.type}" event with init data type: "${initDataType}" for selected key-system ${keySystem}`, + ); + return; + } + + // Match sinf keyId to playlist skd://keyId= + let keyId: Uint8Array | undefined; + try { + const json = bin2str(new Uint8Array(initData)); + const sinf = base64Decode(JSON.parse(json).sinf); + const tenc = parseSinf(sinf); + if (!tenc) { + throw new Error( + `'schm' box missing or not cbcs/cenc with schi > tenc`, + ); + } + keyId = new Uint8Array(tenc.subarray(8, 24)); + } catch (error) { + this.warn(`${logMessage} Failed to parse sinf: ${error}`); + return; + } + + const keyIdHex = Hex.hexDump(keyId); + const { keyIdToKeySessionPromise, mediaKeySessions } = this; + let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex]; + + for (let i = 0; i < mediaKeySessions.length; i++) { + // Match playlist key + const keyContext = mediaKeySessions[i]; + const decryptdata = keyContext.decryptdata; + if (!decryptdata.keyId) { + continue; + } + const oldKeyIdHex = Hex.hexDump(decryptdata.keyId); + if ( + keyIdHex === oldKeyIdHex || + decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1 + ) { + keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex]; + if (!keySessionContextPromise) { + continue; + } + if (decryptdata.pssh) { + break; + } + delete keyIdToKeySessionPromise[oldKeyIdHex]; + decryptdata.pssh = new Uint8Array(initData); + decryptdata.keyId = keyId; + keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] = + keySessionContextPromise.then(() => { + return this.generateRequestWithPreferredKeySession( + keyContext, + initDataType, + initData, + 'encrypted-event-key-match', + ); + }); + keySessionContextPromise.catch((error) => this.handleError(error)); + break; + } + } + + if (!keySessionContextPromise) { + this.handleError( + new Error( + `Key ID ${keyIdHex} not encountered in playlist. Key-system sessions ${mediaKeySessions.length}.`, + ), + ); + } + }); + }; + private onWaitingForKey = (event: Event) => { this.log(`"${event.type}" event`); }; @@ -1130,6 +1231,7 @@ class EMEController extends Logger implements ComponentAPI { // keep reference of media this.media = media; + addEventListener(media, 'encrypted', this.onMediaEncrypted); addEventListener(media, 'waitingforkey', this.onWaitingForKey); } @@ -1137,6 +1239,7 @@ class EMEController extends Logger implements ComponentAPI { const media = this.media; if (media) { + removeEventListener(media, 'encrypted', this.onMediaEncrypted); removeEventListener(media, 'waitingforkey', this.onWaitingForKey); this.media = null; this.mediaKeys = null; diff --git a/src/controller/fragment-tracker.ts b/src/controller/fragment-tracker.ts index 6ffb3b7f87b..af3b0583751 100644 --- a/src/controller/fragment-tracker.ts +++ b/src/controller/fragment-tracker.ts @@ -36,7 +36,7 @@ export class FragmentTracker implements ComponentAPI { | null = Object.create(null); private bufferPadding: number = 0.2; - private hls: Hls; + private hls: Hls | null; private hasGaps: boolean = false; constructor(hls: Hls) { @@ -47,24 +47,30 @@ export class FragmentTracker implements ComponentAPI { private _registerListeners() { const { hls } = this; - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this); - hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); - hls.on(Events.FRAG_LOADED, this.onFragLoaded, this); + if (hls) { + hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); + hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this); + hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); + hls.on(Events.FRAG_LOADED, this.onFragLoaded, this); + } } private _unregisterListeners() { const { hls } = this; - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this); - hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); - hls.off(Events.FRAG_LOADED, this.onFragLoaded, this); + if (hls) { + hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); + hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this); + hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); + hls.off(Events.FRAG_LOADED, this.onFragLoaded, this); + } } public destroy() { this._unregisterListeners(); // @ts-ignore - this.fragments = + this.hls = + // @ts-ignore + this.fragments = // @ts-ignore this.activePartLists = // @ts-ignore @@ -80,19 +86,18 @@ export class FragmentTracker implements ComponentAPI { public getAppendedFrag( position: number, levelType: PlaylistLevelType, - ): Fragment | Part | null { + ): MediaFragment | Part | null { const activeParts = this.activePartLists[levelType]; if (activeParts) { for (let i = activeParts.length; i--; ) { const activePart = activeParts[i]; - if (!activePart) { + if (!activePart as any) { break; } - const appendedPTS = activePart.end; if ( activePart.start <= position && - appendedPTS !== null && - position <= appendedPTS + position <= activePart.end && + activePart.loaded ) { return activePart; } @@ -534,10 +539,12 @@ export class FragmentTracker implements ComponentAPI { function isPartial(fragmentEntity: FragmentEntity): boolean { return ( fragmentEntity.buffered && - (fragmentEntity.body.gap || + !!( + fragmentEntity.body.gap || fragmentEntity.range.video?.partial || fragmentEntity.range.audio?.partial || - fragmentEntity.range.audiovideo?.partial) + fragmentEntity.range.audiovideo?.partial + ) ); } diff --git a/src/controller/gap-controller.ts b/src/controller/gap-controller.ts index 76b4d8dd527..5ef6869ce37 100644 --- a/src/controller/gap-controller.ts +++ b/src/controller/gap-controller.ts @@ -13,13 +13,14 @@ import type { InFlightData } from './base-stream-controller'; import type { InFlightFragments } from '../hls'; import type Hls from '../hls'; import type { FragmentTracker } from './fragment-tracker'; -import type { Fragment, MediaFragment } from '../loader/fragment'; +import type { Fragment, MediaFragment, Part } from '../loader/fragment'; import type { SourceBufferName } from '../types/buffer'; import type { BufferAppendedData, MediaAttachedData, MediaDetachingData, } from '../types/events'; +import type { ErrorData } from '../types/events'; import type { BufferInfo } from '../utils/buffer-helper'; export const MAX_START_GAP_JUMP = 2.0; @@ -28,8 +29,8 @@ export const SKIP_BUFFER_RANGE_START = 0.05; const TICK_INTERVAL = 100; export default class GapController extends TaskLoop { - private hls: Hls | null = null; - private fragmentTracker: FragmentTracker | null = null; + private hls: Hls | null; + private fragmentTracker: FragmentTracker | null; private media: HTMLMediaElement | null = null; private mediaSource?: MediaSource; @@ -267,10 +268,10 @@ export default class GapController extends TaskLoop { const maxStartGapJump = isLive ? levelDetails!.targetduration * 2 : MAX_START_GAP_JUMP; - const partialOrGap = fragmentTracker.getPartialFragment(currentTime); - if (startJump > 0 && (startJump <= maxStartGapJump || partialOrGap)) { + const appended = appendedFragAtPosition(currentTime, fragmentTracker); + if (startJump > 0 && (startJump <= maxStartGapJump || appended)) { if (!media.paused) { - this._trySkipBufferHole(partialOrGap); + this._trySkipBufferHole(appended); } return; } @@ -314,7 +315,7 @@ export default class GapController extends TaskLoop { } // Report stalling after trying to fix this._reportStall(bufferInfo); - if (!this.media || !this.hls) { + if (!this.media || (!this.hls as any)) { return; } } @@ -398,8 +399,13 @@ export default class GapController extends TaskLoop { this.warn(error.message); // Magic number to flush the pipeline without interuption to audio playback: this.media.currentTime += 0.000001; - const frag = - this.fragmentTracker.getPartialFragment(currentTime) || undefined; + let frag: MediaFragment | Part | null | undefined = + appendedFragAtPosition(currentTime, this.fragmentTracker); + if (frag && 'fragment' in frag) { + frag = frag.fragment; + } else if (!frag) { + frag = undefined; + } const bufferInfo = BufferHelper.bufferInfo( this.media, currentTime, @@ -439,14 +445,14 @@ export default class GapController extends TaskLoop { } const levelDetails = this.hls?.latestLevelDetails; - const partial = fragmentTracker.getPartialFragment(currentTime); + const appended = appendedFragAtPosition(currentTime, fragmentTracker); if ( - partial || + appended || (levelDetails?.live && currentTime < levelDetails.fragmentStart) ) { // Try to skip over the buffer hole caused by a partial fragment // This method isn't limited by the size of the gap between buffered ranges - const targetTime = this._trySkipBufferHole(partial); + const targetTime = this._trySkipBufferHole(appended); // we return here in this case, meaning // the branch below only executes when we haven't seeked to a new position if (targetTime || !this.media) { @@ -526,10 +532,10 @@ export default class GapController extends TaskLoop { /** * Attempts to fix buffer stalls by jumping over known gaps caused by partial fragments - * @param partial - The partial fragment found at the current time (where playback is stalling). + * @param appended - The fragment or part found at the current time (where playback is stalling). * @private */ - private _trySkipBufferHole(partial: MediaFragment | null): number { + private _trySkipBufferHole(appended: MediaFragment | Part | null): number { const { fragmentTracker, media } = this; const config = this.hls?.config; if (!media || !fragmentTracker || !config) { @@ -559,46 +565,34 @@ export default class GapController extends TaskLoop { startGap = true; } } - if (!startGap) { - const startProvisioned = - partial || - fragmentTracker.getAppendedFrag( - currentTime, - PlaylistLevelType.MAIN, - ); - if (startProvisioned) { - // Do not seek when selected variant playlist is unloaded - if (!this.hls.loadLevelObj?.details) { - return 0; - } - // Do not seek when required fragments are inflight or appending - const inFlightDependency = getInFlightDependency( - this.hls.inFlightFragments, - startTime, - ); - if (inFlightDependency) { - return 0; - } - // Do not seek if we can't walk tracked fragments to end of gap - let moreToLoad = false; - let pos = startProvisioned.end; - while (pos < startTime) { - const provisioned = - fragmentTracker.getAppendedFrag( - pos, - PlaylistLevelType.MAIN, - ) || fragmentTracker.getPartialFragment(pos); - if (provisioned) { - pos += provisioned.duration; - } else { - moreToLoad = true; - break; - } - } - if (moreToLoad) { - return 0; + if (!startGap && appended) { + // Do not seek when selected variant playlist is unloaded + if (!this.hls.loadLevelObj?.details) { + return 0; + } + // Do not seek when required fragments are inflight or appending + const inFlightDependency = getInFlightDependency( + this.hls.inFlightFragments, + startTime, + ); + if (inFlightDependency) { + return 0; + } + // Do not seek if we can't walk tracked fragments to end of gap + let moreToLoad = false; + let pos = appended.end; + while (pos < startTime) { + const provisioned = appendedFragAtPosition(pos, fragmentTracker); + if (provisioned) { + pos += provisioned.duration; + } else { + moreToLoad = true; + break; } } + if (moreToLoad) { + return 0; + } } } const targetTime = Math.max( @@ -610,20 +604,27 @@ export default class GapController extends TaskLoop { ); this.moved = true; media.currentTime = targetTime; - if (!partial?.gap) { + if (!appended?.gap) { const error = new Error( `fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`, ); - this.hls.trigger(Events.ERROR, { + const errorData: ErrorData = { type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.BUFFER_SEEK_OVER_HOLE, fatal: false, error, reason: error.message, - frag: partial || undefined, buffer: bufferInfo.len, bufferInfo, - }); + }; + if (appended) { + if ('fragment' in appended) { + errorData.part = appended; + } else { + errorData.frag = appended; + } + } + this.hls.trigger(Events.ERROR, errorData); } return targetTime; } @@ -705,3 +706,10 @@ function inFlight(inFlightData: InFlightData | undefined): Fragment | null { } return inFlightData.frag; } + +function appendedFragAtPosition(pos: number, fragmentTracker: FragmentTracker) { + return ( + fragmentTracker.getAppendedFrag(pos, PlaylistLevelType.MAIN) || + fragmentTracker.getPartialFragment(pos) + ); +} diff --git a/src/controller/id3-track-controller.ts b/src/controller/id3-track-controller.ts index e3c022b239f..24a8d0d1e43 100644 --- a/src/controller/id3-track-controller.ts +++ b/src/controller/id3-track-controller.ts @@ -13,6 +13,7 @@ import { removeCuesInRange, sendAddTrackEvent, } from '../utils/texttrack-utils'; +import type { MediaFragment } from '../hls'; import type Hls from '../hls'; import type { DateRange } from '../loader/date-range'; import type { LevelDetails } from '../loader/level-details'; @@ -36,7 +37,7 @@ const MIN_CUE_DURATION = 0.25; function getCueClass(): typeof VTTCue | typeof TextTrackCue | undefined { if (typeof self === 'undefined') return undefined; - return self.VTTCue || self.TextTrackCue; + return (self.VTTCue as typeof VTTCue | undefined) || self.TextTrackCue; } function createCueWithDataFields( @@ -75,18 +76,20 @@ const MAX_CUE_ENDTIME = (() => { })(); class ID3TrackController implements ComponentAPI { - private hls: Hls; + private hls: Hls | null; private id3Track: TextTrack | null = null; private media: HTMLMediaElement | null = null; private dateRangeCuesAppended: Record< string, - { - cues: Record; - dateRange: DateRange; - durationKnown: boolean; - } + | { + cues: Record; + dateRange: DateRange; + durationKnown: boolean; + } + | undefined > = {}; private removeCues: boolean = true; + private assetCue?: VTTCue | TextTrackCue; constructor(hls) { this.hls = hls; @@ -104,26 +107,30 @@ class ID3TrackController implements ComponentAPI { private _registerListeners() { const { hls } = this; - hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this); - hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); - hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); - hls.on(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this); + if (hls) { + hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); + hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); + hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); + hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); + hls.on(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this); + hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); + hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); + hls.on(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this); + } } private _unregisterListeners() { const { hls } = this; - hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this); - hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); - hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); - hls.off(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this); + if (hls) { + hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); + hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); + hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); + hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); + hls.off(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this); + hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); + hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); + hls.off(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this); + } } private onEventCueEnter = () => { @@ -145,7 +152,7 @@ class ID3TrackController implements ComponentAPI { } private onMediaAttached() { - const details = this.hls.latestLevelDetails; + const details = this.hls?.latestLevelDetails; if (details) { this.updateDateRangeCues(details); } @@ -200,15 +207,11 @@ class ID3TrackController implements ComponentAPI { event: Events.FRAG_PARSING_METADATA, data: FragParsingMetadataData, ) { - if (!this.media) { + if (!this.media || !this.hls) { return; } - const { - hls: { - config: { enableEmsgMetadataCues, enableID3MetadataCues }, - }, - } = this; + const { enableEmsgMetadataCues, enableID3MetadataCues } = this.hls.config; if (!enableEmsgMetadataCues && !enableID3MetadataCues) { return; } @@ -235,35 +238,33 @@ class ID3TrackController implements ComponentAPI { } const frames = getId3Frames(samples[i].data); - if (frames) { - const startTime = samples[i].pts; - let endTime: number = startTime + samples[i].duration; + const startTime = samples[i].pts; + let endTime: number = startTime + samples[i].duration; - if (endTime > MAX_CUE_ENDTIME) { - endTime = MAX_CUE_ENDTIME; - } + if (endTime > MAX_CUE_ENDTIME) { + endTime = MAX_CUE_ENDTIME; + } - const timeDiff = endTime - startTime; - if (timeDiff <= 0) { - endTime = startTime + MIN_CUE_DURATION; - } + const timeDiff = endTime - startTime; + if (timeDiff <= 0) { + endTime = startTime + MIN_CUE_DURATION; + } - for (let j = 0; j < frames.length; j++) { - const frame = frames[j]; - // Safari doesn't put the timestamp frame in the TextTrack - if (!isId3TimestampFrame(frame)) { - // add a bounds to any unbounded cues - this.updateId3CueEnds(startTime, type); - const cue = createCueWithDataFields( - Cue, - startTime, - endTime, - frame, - type, - ); - if (cue) { - this.id3Track.addCue(cue); - } + for (let j = 0; j < frames.length; j++) { + const frame = frames[j]; + // Safari doesn't put the timestamp frame in the TextTrack + if (!isId3TimestampFrame(frame)) { + // add a bounds to any unbounded cues + this.updateId3CueEnds(startTime, type); + const cue = createCueWithDataFields( + Cue, + startTime, + endTime, + frame, + type, + ); + if (cue) { + this.id3Track.addCue(cue); } } } @@ -335,11 +336,49 @@ class ID3TrackController implements ComponentAPI { } private updateDateRangeCues(details: LevelDetails, removeOldCues?: true) { + if (!this.hls || !this.media) { + return; + } + const { + assetPlayerId, + timelineOffset, + enableDateRangeMetadataCues, + interstitialsController, + } = this.hls.config; + if (!enableDateRangeMetadataCues) { + return; + } + + const Cue = getCueClass(); if ( - !this.media || - !details.hasProgramDateTime || - !this.hls.config.enableDateRangeMetadataCues + __USE_INTERSTITIALS__ && + assetPlayerId && + timelineOffset && + !interstitialsController ) { + const { fragmentStart, fragmentEnd } = details; + let cue = this.assetCue; + if (cue) { + cue.startTime = fragmentStart; + cue.endTime = fragmentEnd; + } else if (Cue) { + cue = this.assetCue = createCueWithDataFields( + Cue, + fragmentStart, + fragmentEnd, + { assetPlayerId: this.hls.config.assetPlayerId }, + 'hlsjs.interstitial.asset', + ); + if (cue) { + cue.id = assetPlayerId; + this.id3Track ||= this.createTrack(this.media); + this.id3Track.addCue(cue); + cue.addEventListener('enter', this.onEventCueEnter); + } + } + } + + if (!details.hasProgramDateTime) { return; } const { id3Track } = this; @@ -354,36 +393,39 @@ class ID3TrackController implements ComponentAPI { ); for (let i = idsToRemove.length; i--; ) { const id = idsToRemove[i]; - const cues = dateRangeCuesAppended[id].cues; + const cues = dateRangeCuesAppended[id]?.cues; delete dateRangeCuesAppended[id]; - Object.keys(cues).forEach((key) => { - try { + if (cues) { + Object.keys(cues).forEach((key) => { const cue = cues[key]; - cue.removeEventListener('enter', this.onEventCueEnter); - id3Track.removeCue(cue); - } catch (e) { - /* no-op */ - } - }); + if (cue) { + cue.removeEventListener('enter', this.onEventCueEnter); + try { + id3Track.removeCue(cue); + } catch (e) { + /* no-op */ + } + } + }); + } } } else { dateRangeCuesAppended = this.dateRangeCuesAppended = {}; } } // Exit if the playlist does not have Date Ranges or does not have Program Date Time - const lastFragment = details.fragments[details.fragments.length - 1]; + const lastFragment = details.fragments[details.fragments.length - 1] as + | MediaFragment + | undefined; if (ids.length === 0 || !Number.isFinite(lastFragment?.programDateTime)) { return; } - if (!this.id3Track) { - this.id3Track = this.createTrack(this.media); - } + this.id3Track ||= this.createTrack(this.media); - const Cue = getCueClass(); for (let i = 0; i < ids.length; i++) { const id = ids[i]; - const dateRange = dateRanges[id]; + const dateRange = dateRanges[id]!; const startTime = dateRange.startTime; // Process DateRanges to determine end-time (known DURATION, END-DATE, or END-ON-NEXT) @@ -399,7 +441,7 @@ class ID3TrackController implements ComponentAPI { const nextDateRangeWithSameClass = ids.reduce( (candidateDateRange: DateRange | null, id) => { if (id !== dateRange.id) { - const otherDateRange = dateRanges[id]; + const otherDateRange = dateRanges[id]!; if ( otherDateRange.class === dateRange.class && otherDateRange.startDate > dateRange.startDate && @@ -429,7 +471,7 @@ class ID3TrackController implements ComponentAPI { } const cue = cues[key]; if (cue) { - if (durationKnown && !appendedDateRangeCues.durationKnown) { + if (durationKnown && !appendedDateRangeCues?.durationKnown) { cue.endTime = endTime; } else if (Math.abs(cue.startTime - startTime) > 0.01) { cue.startTime = startTime; @@ -452,10 +494,7 @@ class ID3TrackController implements ComponentAPI { cue.id = id; this.id3Track.addCue(cue); cues[key] = cue; - if ( - __USE_INTERSTITIALS__ && - this.hls.config.interstitialsController - ) { + if (__USE_INTERSTITIALS__ && interstitialsController) { if (key === 'X-ASSET-LIST' || key === 'X-ASSET-URL') { cue.addEventListener('enter', this.onEventCueEnter); } diff --git a/src/controller/interstitial-player.ts b/src/controller/interstitial-player.ts index 4f5af35bcc3..82fb92d1b27 100644 --- a/src/controller/interstitial-player.ts +++ b/src/controller/interstitial-player.ts @@ -14,6 +14,7 @@ import type Hls from '../hls'; import type { BufferCodecsData, MediaAttachingData } from '../types/events'; export interface InterstitialPlayer { + bufferedEnd: number; currentTime: number; duration: number; assetPlayers: (HlsAssetPlayer | null)[]; @@ -25,8 +26,8 @@ export type HlsAssetPlayerConfig = Partial & Required>; export class HlsAssetPlayer { - public readonly hls: Hls; - public readonly interstitial: InterstitialEvent; + public hls: Hls | null; + public interstitial: InterstitialEvent; public readonly assetItem: InterstitialAssetItem; public tracks: Partial | null = null; private hasDetails: boolean = false; @@ -43,14 +44,6 @@ export class HlsAssetPlayer { const hls = (this.hls = new HlsPlayerClass(userConfig)); this.interstitial = interstitial; this.assetItem = assetItem; - let uri: string = assetItem.uri; - try { - uri = getInterstitialUrl(uri, userConfig.primarySessionId).href; - } catch (error) { - // Ignore error parsing ASSET_URI or adding _HLS_primary_id to it. The - // issue should surface as an INTERSTITIAL_ASSET_ERROR loading the asset. - } - hls.loadSource(uri); const detailsLoaded = () => { this.hasDetails = true; }; @@ -77,7 +70,24 @@ export class HlsAssetPlayer { } get appendInPlace(): boolean { - return this.interstitial?.appendInPlace || false; + return this.interstitial.appendInPlace; + } + + loadSource() { + const hls = this.hls; + if (!hls) { + return; + } + if (!hls.url) { + let uri: string = this.assetItem.uri; + try { + uri = getInterstitialUrl(uri, hls.config.primarySessionId || '').href; + } catch (error) { + // Ignore error parsing ASSET_URI or adding _HLS_primary_id to it. The + // issue should surface as an INTERSTITIAL_ASSET_ERROR loading the asset. + } + hls.loadSource(uri); + } } bufferedInPlaceToEnd(media?: HTMLMediaElement | null) { @@ -87,17 +97,18 @@ export class HlsAssetPlayer { if (this.hls?.bufferedToEnd) { return true; } - if (!media || !this._bufferedEosTime) { + if (!media) { return false; } + const duration = this._bufferedEosTime || this.duration; const start = this.timelineOffset; const bufferInfo = BufferHelper.bufferInfo(media, start, 0); const bufferedEnd = this.getAssetTime(bufferInfo.end); - return bufferedEnd >= this._bufferedEosTime - 0.02; + return bufferedEnd >= duration - 0.02; } private checkPlayout = () => { - if (this.reachedPlayout(this.currentTime)) { + if (this.reachedPlayout(this.currentTime) && this.hls) { this.hls.trigger(Events.PLAYOUT_LIMIT_REACHED, {}); } }; @@ -172,7 +183,7 @@ export class HlsAssetPlayer { const timelineOffset = this.timelineOffset; if (value !== timelineOffset) { const diff = value - timelineOffset; - if (Math.abs(diff) > 1 / 90000) { + if (Math.abs(diff) > 1 / 90000 && this.hls) { if (this.hasDetails) { throw new Error( `Cannot set timelineOffset after playlists are loaded`, @@ -208,39 +219,41 @@ export class HlsAssetPlayer { destroy() { this.removeMediaListeners(); - this.hls.destroy(); - // @ts-ignore - this.hls = this.interstitial = null; + if (this.hls) { + this.hls.destroy(); + } + this.hls = null; // @ts-ignore this.tracks = this.mediaAttached = this.checkPlayout = null; } attachMedia(data: HTMLMediaElement | MediaAttachingData) { - this.hls.attachMedia(data); + this.loadSource(); + this.hls?.attachMedia(data); } detachMedia() { this.removeMediaListeners(); this.mediaAttached = null; - this.hls.detachMedia(); + this.hls?.detachMedia(); } resumeBuffering() { - this.hls.resumeBuffering(); + this.hls?.resumeBuffering(); } pauseBuffering() { - this.hls.pauseBuffering(); + this.hls?.pauseBuffering(); } transferMedia() { this.bufferSnapShot(); - return this.hls.transferMedia(); + return this.hls?.transferMedia() || null; } resetDetails() { const hls = this.hls; - if (this.hasDetails) { + if (hls && this.hasDetails) { hls.stopLoad(); const deleteDetails = (obj) => delete obj.details; hls.levels.forEach(deleteDetails); @@ -255,7 +268,7 @@ export class HlsAssetPlayer { listener: HlsListeners[E], context?: Context, ) { - this.hls.on(event, listener); + this.hls?.on(event, listener); } once( @@ -263,7 +276,7 @@ export class HlsAssetPlayer { listener: HlsListeners[E], context?: Context, ) { - this.hls.once(event, listener); + this.hls?.once(event, listener); } off( @@ -271,7 +284,7 @@ export class HlsAssetPlayer { listener: HlsListeners[E], context?: Context, ) { - this.hls.off(event, listener); + this.hls?.off(event, listener); } toString(): string { diff --git a/src/controller/interstitials-controller.ts b/src/controller/interstitials-controller.ts index 23df59f7858..78a5dbc09ca 100644 --- a/src/controller/interstitials-controller.ts +++ b/src/controller/interstitials-controller.ts @@ -117,7 +117,7 @@ export default class InterstitialsController private timelinePos: number = -1; // Schedule - private schedule: InterstitialsSchedule; + private schedule: InterstitialsSchedule | null; // Schedule playback and buffering state private playingItem: InterstitialScheduleItem | null = null; @@ -143,48 +143,51 @@ export default class InterstitialsController private registerListeners() { const hls = this.hls; - hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); - hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); - hls.on(Events.AUDIO_TRACK_UPDATED, this.onAudioTrackUpdated, this); - hls.on(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this); - hls.on(Events.SUBTITLE_TRACK_UPDATED, this.onSubtitleTrackUpdated, this); - hls.on(Events.EVENT_CUE_ENTER, this.onInterstitialCueEnter, this); - hls.on(Events.ASSET_LIST_LOADED, this.onAssetListLoaded, this); - hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this); - hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); - hls.on(Events.BUFFERED_TO_END, this.onBufferedToEnd, this); - hls.on(Events.MEDIA_ENDED, this.onMediaEnded, this); - hls.on(Events.ERROR, this.onError, this); - hls.on(Events.DESTROYING, this.onDestroying, this); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (hls) { + hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); + hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); + hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); + hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); + hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); + hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); + hls.on(Events.AUDIO_TRACK_UPDATED, this.onAudioTrackUpdated, this); + hls.on(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this); + hls.on(Events.SUBTITLE_TRACK_UPDATED, this.onSubtitleTrackUpdated, this); + hls.on(Events.EVENT_CUE_ENTER, this.onInterstitialCueEnter, this); + hls.on(Events.ASSET_LIST_LOADED, this.onAssetListLoaded, this); + hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this); + hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); + hls.on(Events.BUFFERED_TO_END, this.onBufferedToEnd, this); + hls.on(Events.MEDIA_ENDED, this.onMediaEnded, this); + hls.on(Events.ERROR, this.onError, this); + hls.on(Events.DESTROYING, this.onDestroying, this); + } } private unregisterListeners() { const hls = this.hls; - if (!hls) { - return; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (hls) { + hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); + hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); + hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); + hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); + hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); + hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); + hls.off(Events.AUDIO_TRACK_UPDATED, this.onAudioTrackUpdated, this); + hls.off(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this); + hls.off(Events.SUBTITLE_TRACK_UPDATED, this.onSubtitleTrackUpdated, this); + hls.off(Events.EVENT_CUE_ENTER, this.onInterstitialCueEnter, this); + hls.off(Events.ASSET_LIST_LOADED, this.onAssetListLoaded, this); + hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this); + hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this); + hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); + hls.off(Events.BUFFERED_TO_END, this.onBufferedToEnd, this); + hls.off(Events.MEDIA_ENDED, this.onMediaEnded, this); + hls.off(Events.ERROR, this.onError, this); + hls.off(Events.DESTROYING, this.onDestroying, this); } - hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); - hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); - hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); - hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); - hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); - hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); - hls.off(Events.AUDIO_TRACK_UPDATED, this.onAudioTrackUpdated, this); - hls.off(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this); - hls.off(Events.SUBTITLE_TRACK_UPDATED, this.onSubtitleTrackUpdated, this); - hls.off(Events.EVENT_CUE_ENTER, this.onInterstitialCueEnter, this); - hls.off(Events.ASSET_LIST_LOADED, this.onAssetListLoaded, this); - hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this); - hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this); - hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); - hls.off(Events.BUFFERED_TO_END, this.onBufferedToEnd, this); - hls.off(Events.MEDIA_ENDED, this.onMediaEnded, this); - hls.off(Events.ERROR, this.onError, this); - hls.off(Events.DESTROYING, this.onDestroying, this); } startLoad() { @@ -208,6 +211,7 @@ export default class InterstitialsController destroy() { this.unregisterListeners(); this.stopLoad(); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this.assetListLoader) { this.assetListLoader.destroy(); } @@ -221,10 +225,11 @@ export default class InterstitialsController this.mediaSelection = this.requiredTracks = this.altSelection = + this.schedule = this.manager = null; // @ts-ignore - this.hls = this.HlsPlayerClass = this.schedule = this.log = null; + this.hls = this.HlsPlayerClass = this.log = null; // @ts-ignore this.assetListLoader = null; // @ts-ignore @@ -317,345 +322,366 @@ export default class InterstitialsController } public get interstitialsManager(): InterstitialsManager | null { - if (!this.manager) { - if (!this.hls) { - return null; - } - const c = this; - const effectiveBufferingItem = () => c.bufferingItem || c.waitingItem; - const getAssetPlayer = (asset: InterstitialAssetItem | null) => - asset ? c.getAssetPlayer(asset.identifier) : asset; - const getMappedTime = ( - item: InterstitialScheduleItem | null, - timelineType: TimelineType, - asset: InterstitialAssetItem | null, - controllerField: 'bufferedPos' | 'timelinePos', - assetPlayerField: 'bufferedEnd' | 'currentTime', - ) => { - if (item) { - let time = item[timelineType].start; - const interstitial = item.event; - if (interstitial) { - if ( - timelineType === 'playout' || - interstitial.timelineOccupancy !== TimelineOccupancy.Point - ) { - const assetPlayer = getAssetPlayer(asset); - if (assetPlayer?.interstitial === interstitial) { - time += - assetPlayer.assetItem.startOffset + - assetPlayer[assetPlayerField]; - } - } - } else { - const value = - controllerField === 'bufferedPos' - ? getBufferedEnd() - : c[controllerField]; - time += value - item.start; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!this.hls) { + return null; + } + if (this.manager) { + return this.manager; + } + const c = this; + const effectiveBufferingItem = () => c.bufferingItem || c.waitingItem; + const getAssetPlayer = (asset: InterstitialAssetItem | null) => + asset ? c.getAssetPlayer(asset.identifier) : asset; + const getMappedTime = ( + item: InterstitialScheduleItem | null, + timelineType: TimelineType, + asset: InterstitialAssetItem | null, + controllerField: 'bufferedPos' | 'timelinePos', + assetPlayerField: 'bufferedEnd' | 'currentTime', + ): number => { + if (item) { + let time = ( + item[timelineType] as { + start: number; + end: number; } - return time; - } - return 0; - }; - const findMappedTime = ( - primaryTime: number, - timelineType: TimelineType, - ): number => { - if ( - primaryTime !== 0 && - timelineType !== 'primary' && - c.schedule.length - ) { - const index = c.schedule.findItemIndexAtTime(primaryTime); - const item = c.schedule.items?.[index]; - if (item) { - const diff = item[timelineType].start - item.start; - return primaryTime + diff; + ).start; + const interstitial = item.event; + if (interstitial) { + if ( + timelineType === 'playout' || + interstitial.timelineOccupancy !== TimelineOccupancy.Point + ) { + const assetPlayer = getAssetPlayer(asset); + if (assetPlayer?.interstitial === interstitial) { + time += + assetPlayer.assetItem.startOffset + + assetPlayer[assetPlayerField]; + } } + } else { + const value = + controllerField === 'bufferedPos' + ? getBufferedEnd() + : c[controllerField]; + time += value - item.start; } - return primaryTime; - }; - const getBufferedEnd = (): number => { - const value = c.bufferedPos; - if (value === Number.MAX_VALUE) { - return getMappedDuration('primary'); + return time; + } + return 0; + }; + const findMappedTime = ( + primaryTime: number, + timelineType: TimelineType, + ): number => { + if ( + primaryTime !== 0 && + timelineType !== 'primary' && + c.schedule?.length + ) { + const index = c.schedule.findItemIndexAtTime(primaryTime); + const item = c.schedule.items?.[index]; + if (item) { + const diff = item[timelineType].start - item.start; + return primaryTime + diff; } - return Math.max(value, 0); - }; - const getMappedDuration = (timelineType: TimelineType): number => { - if (c.primaryDetails?.live) { - // return end of last event item or playlist - return c.primaryDetails.edge; + } + return primaryTime; + }; + const getBufferedEnd = (): number => { + const value = c.bufferedPos; + if (value === Number.MAX_VALUE) { + return getMappedDuration('primary'); + } + return Math.max(value, 0); + }; + const getMappedDuration = (timelineType: TimelineType): number => { + if (c.primaryDetails?.live) { + // return end of last event item or playlist + return c.primaryDetails.edge; + } + return c.schedule?.durations[timelineType] || 0; + }; + const seekTo = (time: number, timelineType: TimelineType) => { + const item = c.effectivePlayingItem; + if (item?.event?.restrictions.skip || !c.schedule) { + return; + } + c.log(`seek to ${time} "${timelineType}"`); + const playingItem = c.effectivePlayingItem; + const targetIndex = c.schedule.findItemIndexAtTime(time, timelineType); + const targetItem = c.schedule.items?.[targetIndex]; + const bufferingPlayer = c.getBufferingPlayer(); + const bufferingInterstitial = bufferingPlayer?.interstitial; + const appendInPlace = bufferingInterstitial?.appendInPlace; + const seekInItem = playingItem && c.itemsMatch(playingItem, targetItem); + if (playingItem && (appendInPlace || seekInItem)) { + // seek in asset player or primary media (appendInPlace) + const assetPlayer = getAssetPlayer(c.playingAsset); + const media = assetPlayer?.media || c.primaryMedia; + if (media) { + const currentTime = + timelineType === 'primary' + ? media.currentTime + : getMappedTime( + playingItem, + timelineType, + c.playingAsset, + 'timelinePos', + 'currentTime', + ); + + const diff = time - currentTime; + const seekToTime = + (appendInPlace ? currentTime : media.currentTime) + diff; + if ( + seekToTime >= 0 && + (!assetPlayer || + appendInPlace || + seekToTime <= assetPlayer.duration) + ) { + media.currentTime = seekToTime; + return; + } } - return c.schedule.durations[timelineType]; - }; - const seekTo = (time: number, timelineType: TimelineType) => { - const item = c.effectivePlayingItem; - if (item?.event?.restrictions.skip) { - return; + } + // seek out of item or asset + if (targetItem) { + let seekToTime = time; + if (timelineType !== 'primary') { + const primarySegmentStart = targetItem[timelineType].start; + const diff = time - primarySegmentStart; + seekToTime = targetItem.start + diff; } - c.log(`seek to ${time} "${timelineType}"`); - const playingItem = c.effectivePlayingItem; - const targetIndex = c.schedule.findItemIndexAtTime(time, timelineType); - const targetItem = c.schedule.items?.[targetIndex]; - const bufferingPlayer = c.getBufferingPlayer(); - const bufferingInterstitial = bufferingPlayer?.interstitial; - const appendInPlace = bufferingInterstitial?.appendInPlace; - const seekInItem = playingItem && c.itemsMatch(playingItem, targetItem); - if (playingItem && (appendInPlace || seekInItem)) { - // seek in asset player or primary media (appendInPlace) - const assetPlayer = getAssetPlayer(c.playingAsset); - const media = assetPlayer?.media || c.primaryMedia; + const targetIsPrimary = !c.isInterstitial(targetItem); + if ( + (!c.isInterstitial(playingItem) || playingItem.event.appendInPlace) && + (targetIsPrimary || targetItem.event.appendInPlace) + ) { + const media = + c.media || (appendInPlace ? bufferingPlayer?.media : null); if (media) { - const currentTime = - timelineType === 'primary' - ? media.currentTime - : getMappedTime( - playingItem, - timelineType, - c.playingAsset, - 'timelinePos', - 'currentTime', - ); - - const diff = time - currentTime; - const seekToTime = - (appendInPlace ? currentTime : media.currentTime) + diff; - if ( - seekToTime >= 0 && - (!assetPlayer || - appendInPlace || - seekToTime <= assetPlayer.duration) - ) { - media.currentTime = seekToTime; + media.currentTime = seekToTime; + } + } else if (playingItem) { + // check if an Interstitial between the current item and target item has an X-RESTRICT JUMP restriction + const playingIndex = c.findItemIndex(playingItem); + if (targetIndex > playingIndex) { + const jumpIndex = c.schedule.findJumpRestrictedIndex( + playingIndex + 1, + targetIndex, + ); + if (jumpIndex > playingIndex) { + c.setSchedulePosition(jumpIndex); return; } } - } - // seek out of item or asset - if (targetItem) { - let seekToTime = time; - if (timelineType !== 'primary') { - const primarySegmentStart = targetItem[timelineType].start; - const diff = time - primarySegmentStart; - seekToTime = targetItem.start + diff; - } - const targetIsPrimary = !c.isInterstitial(targetItem); - if ( - (!c.isInterstitial(playingItem) || - playingItem.event.appendInPlace) && - (targetIsPrimary || targetItem.event.appendInPlace) - ) { - const media = - c.media || (appendInPlace ? bufferingPlayer?.media : null); - if (media) { - media.currentTime = seekToTime; - } - } else if (playingItem) { - // check if an Interstitial between the current item and target item has an X-RESTRICT JUMP restriction - const playingIndex = c.findItemIndex(playingItem); - if (targetIndex > playingIndex) { - const jumpIndex = c.schedule.findJumpRestrictedIndex( - playingIndex + 1, - targetIndex, - ); - if (jumpIndex > playingIndex) { - c.setSchedulePosition(jumpIndex); - return; - } - } - let assetIndex = 0; - if (targetIsPrimary) { - c.timelinePos = seekToTime; - c.checkBuffer(); - } else { - const assetList = targetItem?.event?.assetList; - if (assetList) { - const eventTime = - time - (targetItem[timelineType] || targetItem).start; - for (let i = assetList.length; i--; ) { - const asset = assetList[i]; - if ( - asset.duration && - eventTime >= asset.startOffset && - eventTime < asset.startOffset + asset.duration - ) { - assetIndex = i; - break; - } - } + let assetIndex = 0; + if (targetIsPrimary) { + c.timelinePos = seekToTime; + c.checkBuffer(); + } else { + const assetList = targetItem.event.assetList; + const eventTime = + time - (targetItem[timelineType] || targetItem).start; + for (let i = assetList.length; i--; ) { + const asset = assetList[i]; + if ( + asset.duration && + eventTime >= asset.startOffset && + eventTime < asset.startOffset + asset.duration + ) { + assetIndex = i; + break; } } - c.setSchedulePosition(targetIndex, assetIndex); } + c.setSchedulePosition(targetIndex, assetIndex); } - }; - const getActiveInterstitial = () => { + } + }; + const getActiveInterstitial = () => { + const playingItem = c.effectivePlayingItem; + if (c.isInterstitial(playingItem)) { + return playingItem; + } + const bufferingItem = effectiveBufferingItem(); + if (c.isInterstitial(bufferingItem)) { + return bufferingItem; + } + return null; + }; + const interstitialPlayer: InterstitialPlayer = { + get bufferedEnd() { + const interstitialItem = effectiveBufferingItem(); + const bufferingItem = c.bufferingItem; + if (bufferingItem && bufferingItem === interstitialItem) { + return ( + getMappedTime( + bufferingItem, + 'playout', + c.bufferingAsset, + 'bufferedPos', + 'bufferedEnd', + ) - bufferingItem.playout.start || + c.bufferingAsset?.startOffset || + 0 + ); + } + return 0; + }, + get currentTime() { + const interstitialItem = getActiveInterstitial(); + const playingItem = c.effectivePlayingItem; + if (playingItem && playingItem === interstitialItem) { + return ( + getMappedTime( + playingItem, + 'playout', + c.effectivePlayingAsset, + 'timelinePos', + 'currentTime', + ) - playingItem.playout.start + ); + } + return 0; + }, + set currentTime(time: number) { + const interstitialItem = getActiveInterstitial(); const playingItem = c.effectivePlayingItem; - if (c.isInterstitial(playingItem)) { - return playingItem; + if (playingItem && playingItem === interstitialItem) { + seekTo(time + playingItem.playout.start, 'playout'); } - const bufferingItem = effectiveBufferingItem(); - if (c.isInterstitial(bufferingItem)) { - return bufferingItem; + }, + get duration() { + const interstitialItem = getActiveInterstitial(); + if (interstitialItem) { + return interstitialItem.playout.end - interstitialItem.playout.start; + } + return 0; + }, + get assetPlayers() { + const assetList = getActiveInterstitial()?.event.assetList; + if (assetList) { + return assetList.map((asset) => c.getAssetPlayer(asset.identifier)); + } + return []; + }, + get playingIndex() { + const interstitial = getActiveInterstitial()?.event; + if (interstitial && c.effectivePlayingAsset) { + return interstitial.findAssetIndex(c.effectivePlayingAsset); + } + return -1; + }, + get scheduleItem() { + return getActiveInterstitial(); + }, + }; + return (this.manager = { + get events() { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return c.schedule?.events?.slice(0) || []; + }, + get schedule() { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return c.schedule?.items?.slice(0) || []; + }, + get interstitialPlayer() { + if (getActiveInterstitial()) { + return interstitialPlayer; } return null; - }; - const interstitialPlayer: InterstitialPlayer = { + }, + get playerQueue() { + return c.playerQueue.slice(0); + }, + get bufferingAsset() { + return c.bufferingAsset; + }, + get bufferingItem() { + return effectiveBufferingItem(); + }, + get bufferingIndex() { + const item = effectiveBufferingItem(); + return c.findItemIndex(item); + }, + get playingAsset() { + return c.effectivePlayingAsset; + }, + get playingItem() { + return c.effectivePlayingItem; + }, + get playingIndex() { + const item = c.effectivePlayingItem; + return c.findItemIndex(item); + }, + primary: { + get bufferedEnd() { + return getBufferedEnd(); + }, get currentTime() { - const interstitialItem = getActiveInterstitial(); - const playingItem = c.effectivePlayingItem; - if (playingItem && playingItem === interstitialItem) { - return ( - getMappedTime( - playingItem, - 'playout', - c.effectivePlayingAsset, - 'timelinePos', - 'currentTime', - ) - playingItem.playout.start - ); - } - return 0; + const timelinePos = c.timelinePos; + return timelinePos > 0 ? timelinePos : 0; }, set currentTime(time: number) { - const interstitialItem = getActiveInterstitial(); - const playingItem = c.effectivePlayingItem; - if (playingItem && playingItem === interstitialItem) { - seekTo(time + playingItem.playout.start, 'playout'); - } + seekTo(time, 'primary'); }, get duration() { - const interstitialItem = getActiveInterstitial(); - if (interstitialItem) { - return ( - interstitialItem.playout.end - interstitialItem.playout.start - ); - } - return 0; - }, - get assetPlayers() { - const assetList = getActiveInterstitial()?.event.assetList; - if (assetList) { - return assetList.map((asset) => c.getAssetPlayer(asset.identifier)); - } - return []; - }, - get playingIndex() { - const interstitial = getActiveInterstitial()?.event; - if (interstitial && c.effectivePlayingAsset) { - return interstitial.findAssetIndex(c.effectivePlayingAsset); - } - return -1; - }, - get scheduleItem() { - return getActiveInterstitial(); - }, - }; - this.manager = { - get events() { - return c.schedule?.events?.slice(0) || []; - }, - get schedule() { - return c.schedule?.items?.slice(0) || []; - }, - get interstitialPlayer() { - if (getActiveInterstitial()) { - return interstitialPlayer; - } - return null; - }, - get playerQueue() { - return c.playerQueue.slice(0); - }, - get bufferingAsset() { - return c.bufferingAsset; - }, - get bufferingItem() { - return effectiveBufferingItem(); + return getMappedDuration('primary'); }, - get bufferingIndex() { - const item = effectiveBufferingItem(); - return c.findItemIndex(item); + get seekableStart() { + return c.primaryDetails?.fragmentStart || 0; }, - get playingAsset() { - return c.effectivePlayingAsset; + }, + integrated: { + get bufferedEnd() { + return getMappedTime( + effectiveBufferingItem(), + 'integrated', + c.bufferingAsset, + 'bufferedPos', + 'bufferedEnd', + ); }, - get playingItem() { - return c.effectivePlayingItem; + get currentTime() { + return getMappedTime( + c.effectivePlayingItem, + 'integrated', + c.effectivePlayingAsset, + 'timelinePos', + 'currentTime', + ); }, - get playingIndex() { - const item = c.effectivePlayingItem; - return c.findItemIndex(item); + set currentTime(time: number) { + seekTo(time, 'integrated'); }, - primary: { - get bufferedEnd() { - return getBufferedEnd(); - }, - get currentTime() { - const timelinePos = c.timelinePos; - return timelinePos > 0 ? timelinePos : 0; - }, - set currentTime(time: number) { - seekTo(time, 'primary'); - }, - get duration() { - return getMappedDuration('primary'); - }, - get seekableStart() { - return c.primaryDetails?.fragmentStart || 0; - }, + get duration() { + return getMappedDuration('integrated'); }, - integrated: { - get bufferedEnd() { - return getMappedTime( - effectiveBufferingItem(), - 'integrated', - c.bufferingAsset, - 'bufferedPos', - 'bufferedEnd', - ); - }, - get currentTime() { - return getMappedTime( - c.effectivePlayingItem, - 'integrated', - c.effectivePlayingAsset, - 'timelinePos', - 'currentTime', - ); - }, - set currentTime(time: number) { - seekTo(time, 'integrated'); - }, - get duration() { - return getMappedDuration('integrated'); - }, - get seekableStart() { - return findMappedTime( - c.primaryDetails?.fragmentStart || 0, - 'integrated', - ); - }, + get seekableStart() { + return findMappedTime( + c.primaryDetails?.fragmentStart || 0, + 'integrated', + ); }, - skip: () => { - const item = c.effectivePlayingItem; - const event = item?.event; - if (event && !event.restrictions.skip) { - const index = c.findItemIndex(item); - if (event.appendInPlace) { - const time = item.playout.start + item.event.duration; - seekTo(time + 0.001, 'playout'); - } else { - c.advanceAfterAssetEnded(event, index, Infinity); - } + }, + skip: () => { + const item = c.effectivePlayingItem; + const event = item?.event; + if (event && !event.restrictions.skip) { + const index = c.findItemIndex(item); + if (event.appendInPlace) { + const time = item.playout.start + item.event.duration; + seekTo(time + 0.001, 'playout'); + } else { + c.advanceAfterAssetEnded(event, index, Infinity); } - }, - }; - } - return this.manager; + } + }, + }); } // Schedule getters @@ -805,8 +831,9 @@ export default class InterstitialsController ) { const interstitial = queuedPlayer.interstitial; this.clearInterstitial(queuedPlayer.interstitial, null); - interstitial.appendInPlace = false; - if (interstitial.appendInPlace) { + interstitial.appendInPlace = false; // setter may be a no-op; + // `appendInPlace` getter may still return `true` after insterstitial streaming has begun in that mode. + if (interstitial.appendInPlace as boolean) { this.warn( `Could not change append strategy for queued assets ${interstitial}`, ); @@ -827,15 +854,16 @@ export default class InterstitialsController this.log( `${transferring ? 'transfering MediaSource' : 'attaching media'} to ${ isAssetPlayer ? player : 'Primary' - } from ${logFromSource}`, + } from ${logFromSource} (media.currentTime: ${media.currentTime})`, ); - if (dataToAttach === attachMediaSourceData) { + const schedule = this.schedule; + if (dataToAttach === attachMediaSourceData && schedule) { const isAssetAtEndOfSchedule = isAssetPlayer && - (player as HlsAssetPlayer).assetId === this.schedule.assetIdAtEnd; + (player as HlsAssetPlayer).assetId === schedule.assetIdAtEnd; // Prevent asset players from marking EoS on transferred MediaSource dataToAttach.overrides = { - duration: this.schedule.duration, + duration: schedule.duration, endOfStream: !isAssetPlayer || isAssetAtEndOfSchedule, cueRemoval: !isAssetPlayer, }; @@ -853,7 +881,7 @@ export default class InterstitialsController private onSeeking = () => { const currentTime = this.currentTime; - if (currentTime === undefined || this.playbackDisabled) { + if (currentTime === undefined || this.playbackDisabled || !this.schedule) { return; } const diff = currentTime - this.timelinePos; @@ -885,13 +913,19 @@ export default class InterstitialsController (backwardSeek && currentTime < playingItem.start) || currentTime >= playingItem.end ) { - const scheduleIndex = this.schedule.findItemIndexAtTime(this.timelinePos); + const playingIndex = this.findItemIndex(playingItem); + let scheduleIndex = this.schedule.findItemIndexAtTime(currentTime); + if (scheduleIndex === -1) { + scheduleIndex = playingIndex + (backwardSeek ? -1 : 1); + this.log( + `seeked ${backwardSeek ? 'back ' : ''}to position not covered by schedule ${currentTime} (resolving from ${playingIndex} to ${scheduleIndex})`, + ); + } if (!this.isInterstitial(playingItem) && this.media?.paused) { this.shouldPlay = false; } if (!backwardSeek) { // check if an Interstitial between the current item and target item has an X-RESTRICT JUMP restriction - const playingIndex = this.findItemIndex(playingItem); if (scheduleIndex > playingIndex) { const jumpIndex = this.schedule.findJumpRestrictedIndex( playingIndex + 1, @@ -912,6 +946,7 @@ export default class InterstitialsController // restart Interstitial at end if (this.playingLastItem && this.isInterstitial(playingItem)) { const restartAsset = playingItem.event.assetList[0]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (restartAsset) { this.endedItem = this.playingItem; this.playingItem = null; @@ -975,7 +1010,7 @@ export default class InterstitialsController // Scheduling methods private checkStart() { const schedule = this.schedule; - const interstitialEvents = schedule.events; + const interstitialEvents = schedule?.events; if (!interstitialEvents || this.playbackDisabled || !this.media) { return; } @@ -1004,6 +1039,23 @@ export default class InterstitialsController } } + private advanceAssetBuffering( + item: InterstitialScheduleEventItem, + assetItem: InterstitialAssetItem, + ) { + const interstitial = item.event; + const assetListIndex = interstitial.findAssetIndex(assetItem); + const nextAssetIndex = getNextAssetIndex(interstitial, assetListIndex); + if (!interstitial.isAssetPastPlayoutLimit(nextAssetIndex)) { + this.bufferedToEvent(item, nextAssetIndex); + } else if (this.schedule) { + const nextItem = this.schedule.items?.[this.findItemIndex(item) + 1]; + if (nextItem) { + this.bufferedToItem(nextItem); + } + } + } + private advanceAfterAssetEnded( interstitial: InterstitialEvent, index: number, @@ -1012,8 +1064,16 @@ export default class InterstitialsController const nextAssetIndex = getNextAssetIndex(interstitial, assetListIndex); if (!interstitial.isAssetPastPlayoutLimit(nextAssetIndex)) { // Advance to next asset list item + if (interstitial.appendInPlace) { + const assetItem = interstitial.assetList[nextAssetIndex] as + | InterstitialAssetItem + | undefined; + if (assetItem) { + this.advanceInPlace(assetItem.timelineStart); + } + } this.setSchedulePosition(index, nextAssetIndex); - } else { + } else if (this.schedule) { // Advance to next schedule segment // check if we've reached the end of the program const scheduleItems = this.schedule.items; @@ -1027,7 +1087,10 @@ export default class InterstitialsController const resumptionTime = interstitial.resumeTime; if (this.timelinePos < resumptionTime) { this.timelinePos = resumptionTime; - this.checkBuffer(); + if (interstitial.appendInPlace) { + this.advanceInPlace(resumptionTime); + } + this.checkBuffer(this.bufferedPos < resumptionTime); } this.setSchedulePosition(nextIndex); } @@ -1039,6 +1102,9 @@ export default class InterstitialsController playingAsset: InterstitialAssetItem, ) { const schedule = this.schedule; + if (!schedule) { + return; + } const parentIdentifier = playingAsset.parentIdentifier; const interstitial = schedule.getEvent(parentIdentifier); if (interstitial) { @@ -1049,14 +1115,14 @@ export default class InterstitialsController } private setSchedulePosition(index: number, assetListIndex?: number) { - const scheduleItems = this.schedule.items; + const scheduleItems = this.schedule?.items; if (!scheduleItems || this.playbackDisabled) { return; } this.log(`setSchedulePosition ${index}, ${assetListIndex}`); const scheduledItem = index >= 0 ? scheduleItems[index] : null; // Cleanup current item / asset - const currentItem = this.playingItem; + const currentItem = this.waitingItem || this.playingItem; const playingLastItem = this.playingLastItem; if (this.isInterstitial(currentItem)) { const interstitial = currentItem.event; @@ -1068,7 +1134,7 @@ export default class InterstitialsController assetId && (!this.eventItemsMatch(currentItem, scheduledItem) || (assetListIndex !== undefined && - assetId !== interstitial.assetList?.[assetListIndex].identifier)) + assetId !== interstitial.assetList[assetListIndex].identifier)) ) { const playingAssetListIndex = interstitial.findAssetIndex(playingAsset); this.log( @@ -1088,7 +1154,8 @@ export default class InterstitialsController // Schedule change occured on INTERSTITIAL_ASSET_ENDED if ( this.itemsMatch(currentItem, this.playingItem) && - !this.playingAsset + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + !this.playingAsset // INTERSTITIAL_ASSET_ENDED side-effect ) { this.advanceAfterAssetEnded( interstitial, @@ -1120,12 +1187,12 @@ export default class InterstitialsController if (interstitial.cue.once) { // Remove interstitial with CUE attribute value of ONCE after it has played this.updateSchedule(); - const items = this.schedule.items; - if (scheduledItem && items) { + const updatedScheduleItems = this.schedule?.items; + if (scheduledItem && updatedScheduleItems) { const updatedIndex = this.findItemIndex(scheduledItem); this.advanceSchedule( updatedIndex, - items, + updatedScheduleItems, assetListIndex, currentItem, playingLastItem, @@ -1150,6 +1217,10 @@ export default class InterstitialsController currentItem: InterstitialScheduleItem | null, playedLastItem: boolean, ) { + const schedule = this.schedule; + if (!schedule) { + return; + } const scheduledItem = index >= 0 ? scheduleItems[index] : null; const media = this.primaryMedia; // Cleanup out of range Interstitials @@ -1157,9 +1228,7 @@ export default class InterstitialsController if (playerQueue.length) { playerQueue.forEach((player) => { const interstitial = player.interstitial; - const queuedIndex = this.schedule.findEventIndex( - interstitial.identifier, - ); + const queuedIndex = schedule.findEventIndex(interstitial.identifier); if (queuedIndex < index || queuedIndex > index + 1) { this.clearInterstitial(interstitial, scheduledItem); } @@ -1175,7 +1244,7 @@ export default class InterstitialsController const interstitial = scheduledItem.event; // find asset index if (assetListIndex === undefined) { - assetListIndex = this.schedule.findAssetIndex( + assetListIndex = schedule.findAssetIndex( interstitial, this.timelinePos, ); @@ -1183,7 +1252,10 @@ export default class InterstitialsController interstitial, assetListIndex - 1, ); - if (interstitial.isAssetPastPlayoutLimit(assetIndexCandidate)) { + if ( + interstitial.isAssetPastPlayoutLimit(assetIndexCandidate) || + (interstitial.appendInPlace && this.timelinePos === scheduledItem.end) + ) { this.advanceAfterAssetEnded(interstitial, index, assetListIndex); return; } @@ -1227,18 +1299,10 @@ export default class InterstitialsController this.playingItem = scheduledItem; // If asset-list is empty or missing asset index, advance to next item - const assetItem = interstitial.assetList[assetListIndex]; + const assetItem = interstitial.assetList[assetListIndex] as + | InterstitialAssetItem + | undefined; if (!assetItem) { - const nextItem = scheduleItems[index + 1]; - const media = this.media; - if ( - nextItem && - media && - !this.isInterstitial(nextItem) && - media.currentTime < nextItem.start - ) { - media.currentTime = this.timelinePos = nextItem.start; - } this.advanceAfterAssetEnded(interstitial, index, assetListIndex || 0); return; } @@ -1259,6 +1323,7 @@ export default class InterstitialsController assetItem, assetListIndex, ); + player.loadSource(); } if (!this.eventItemsMatch(scheduledItem, this.bufferingItem)) { if (interstitial.appendInPlace && this.isAssetBuffered(assetItem)) { @@ -1287,7 +1352,7 @@ export default class InterstitialsController this.playingItem = currentItem; if (!currentItem.event.appendInPlace) { // Media must be re-attached to resume primary schedule if not sharing source - this.attachPrimary(this.schedule.durations.primary, null); + this.attachPrimary(schedule.durations.primary, null); } } } @@ -1297,7 +1362,7 @@ export default class InterstitialsController } private get primaryDetails(): LevelDetails | undefined { - return this.mediaSelection?.main?.details; + return this.mediaSelection?.main.details; } private get primaryLive(): boolean { @@ -1333,7 +1398,7 @@ export default class InterstitialsController return; } - const scheduleItems = this.schedule.items; + const scheduleItems = this.schedule?.items; if (!scheduleItems) { return; } @@ -1431,7 +1496,7 @@ export default class InterstitialsController // HLS.js event callbacks private onManifestLoading() { this.stopLoad(); - this.schedule.reset(); + this.schedule?.reset(); this.emptyPlayerQueue(); this.clearScheduleState(); this.shouldPlay = false; @@ -1447,7 +1512,7 @@ export default class InterstitialsController } private onLevelUpdated(event: Events.LEVEL_UPDATED, data: LevelUpdatedData) { - if (data.level === -1) { + if (data.level === -1 || !this.schedule) { // level was removed return; } @@ -1501,9 +1566,8 @@ export default class InterstitialsController ) { const audioOption = getBasicSelectionOption(data); this.playerQueue.forEach( - (player) => - player.hls.setAudioOption(data) || - player.hls.setAudioOption(audioOption), + ({ hls }) => + hls && (hls.setAudioOption(data) || hls.setAudioOption(audioOption)), ); } @@ -1513,9 +1577,10 @@ export default class InterstitialsController ) { const subtitleOption = getBasicSelectionOption(data); this.playerQueue.forEach( - (player) => - player.hls.setSubtitleOption(data) || - (data.id !== -1 && player.hls.setSubtitleOption(subtitleOption)), + ({ hls }) => + hls && + (hls.setSubtitleOption(data) || + (data.id !== -1 && hls.setSubtitleOption(subtitleOption))), ); } @@ -1550,6 +1615,9 @@ export default class InterstitialsController } private onBufferedToEnd(event: Events.BUFFERED_TO_END) { + if (!this.schedule) { + return; + } // Buffered to post-roll const interstitialEvents = this.schedule.events; if (this.bufferedPos < Number.MAX_VALUE && interstitialEvents) { @@ -1589,6 +1657,9 @@ export default class InterstitialsController previousItems: InterstitialScheduleItem[] | null, ) => { const schedule = this.schedule; + if (!schedule) { + return; + } const playingItem = this.playingItem; const interstitialEvents = schedule.events || []; const scheduleItems = schedule.items || []; @@ -1611,51 +1682,27 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli this.log(`Removed events ${removedIds}`); } - this.playerQueue.forEach((player) => { - if (player.interstitial.appendInPlace) { - const timelineStart = player.assetItem.timelineStart; - const diff = player.timelineOffset - timelineStart; - if (diff) { - try { - player.timelineOffset = timelineStart; - } catch (e) { - if (Math.abs(diff) > ALIGNED_END_THRESHOLD_SECONDS) { - this.warn( - `${e} ("${player.assetId}" ${player.timelineOffset}->${timelineStart})`, - ); - } - } - } - } - }); - // Update schedule item references // Do not replace Interstitial playingItem without a match - used for INTERSTITIAL_ASSET_ENDED and INTERSTITIAL_ENDED - let trimInPlaceForPlayout: null | (() => void) = null; + let updatedPlayingItem: InterstitialScheduleItem | null = null; + let updatedBufferingItem: InterstitialScheduleItem | null = null; if (playingItem) { - const updatedPlayingItem = this.updateItem(playingItem, this.timelinePos); + updatedPlayingItem = this.updateItem(playingItem, this.timelinePos); if (this.itemsMatch(playingItem, updatedPlayingItem)) { this.playingItem = updatedPlayingItem; + } else { this.waitingItem = this.endedItem = null; - trimInPlaceForPlayout = () => - this.trimInPlace(updatedPlayingItem, playingItem); } - } else { - // Clear waitingItem if it has been removed from the schedule - this.waitingItem = this.updateItem(this.waitingItem); - this.endedItem = this.updateItem(this.endedItem); } + // Clear waitingItem if it has been removed from the schedule + this.waitingItem = this.updateItem(this.waitingItem); + this.endedItem = this.updateItem(this.endedItem); // Do not replace Interstitial bufferingItem without a match - used for transfering media element or source const bufferingItem = this.bufferingItem; if (bufferingItem) { - const updatedBufferingItem = this.updateItem( - bufferingItem, - this.bufferedPos, - ); + updatedBufferingItem = this.updateItem(bufferingItem, this.bufferedPos); if (this.itemsMatch(bufferingItem, updatedBufferingItem)) { this.bufferingItem = updatedBufferingItem; - trimInPlaceForPlayout ||= () => - this.trimInPlace(updatedBufferingItem, bufferingItem); } else if (bufferingItem.event) { // Interstitial removed from schedule (Live -> VOD or other scenario where Start Date is outside the range of VOD Playlist) this.bufferingItem = this.playingItem; @@ -1669,6 +1716,24 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli }); }); + this.playerQueue.forEach((player) => { + if (player.interstitial.appendInPlace) { + const timelineStart = player.assetItem.timelineStart; + const diff = player.timelineOffset - timelineStart; + if (diff) { + try { + player.timelineOffset = timelineStart; + } catch (e) { + if (Math.abs(diff) > ALIGNED_END_THRESHOLD_SECONDS) { + this.warn( + `${e} ("${player.assetId}" ${player.timelineOffset}->${timelineStart})`, + ); + } + } + } + } + }); + if (interstitialsUpdated || previousItems) { this.hls.trigger(Events.INTERSTITIALS_UPDATED, { events: interstitialEvents.slice(0), @@ -1688,8 +1753,11 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli return; } - if (trimInPlaceForPlayout) { - trimInPlaceForPlayout(); + if (playingItem) { + this.trimInPlace(updatedPlayingItem, playingItem); + } + if (bufferingItem) { + this.trimInPlace(updatedBufferingItem, bufferingItem); } // Check is buffered to new Interstitial event boundary @@ -1703,10 +1771,10 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli time?: number, ): T | null { // find item in this.schedule.items; - const items = this.schedule.items; + const items = this.schedule?.items; if (previousItem && items) { const index = this.findItemIndex(previousItem, time); - return (items[index] as T) || null; + return (items[index] as T | undefined) || null; } return null; } @@ -1766,7 +1834,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli item: InterstitialScheduleItem | null, time?: number, ): number { - return item ? this.schedule.findItemIndex(item, time) : -1; + return item && this.schedule ? this.schedule.findItemIndex(item, time) : -1; } private updateSchedule() { @@ -1774,12 +1842,12 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli if (!mediaSelection) { return; } - this.schedule.updateSchedule(mediaSelection, []); + this.schedule?.updateSchedule(mediaSelection, []); } // Schedule buffer control private checkBuffer(starved?: boolean) { - const items = this.schedule.items; + const items = this.schedule?.items; if (!items) { return; } @@ -1803,7 +1871,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli ) { const schedule = this.schedule; const bufferingItem = this.bufferingItem; - if (this.bufferedPos > bufferEnd) { + if (this.bufferedPos > bufferEnd || !schedule) { return; } if (items.length === 1 && this.itemsMatch(items[0], bufferingItem)) { @@ -1827,12 +1895,22 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli ) { bufferEndIndex = nextToBufferIndex; } - if ( - nextToBufferIndex - playingIndex > 1 && - bufferingItem?.event?.appendInPlace === false - ) { - // do not advance buffering item past Interstitial that requires source reset - return; + if (this.isInterstitial(bufferingItem)) { + const interstitial = bufferingItem.event; + if ( + nextToBufferIndex - playingIndex > 1 && + interstitial.appendInPlace === false + ) { + // do not advance buffering item past Interstitial that requires source reset + return; + } + if ( + interstitial.assetList.length === 0 && + interstitial.assetListLoader + ) { + // do not advance buffering item past Interstitial loading asset-list + return; + } } this.bufferedPos = bufferEnd; if (bufferEndIndex > bufferingIndex && bufferEndIndex > playingIndex) { @@ -1884,7 +1962,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli const bufferingLast = this.bufferingItem; const schedule = this.schedule; - if (!this.itemsMatch(item, bufferingLast)) { + if (!this.itemsMatch(item, bufferingLast) && schedule) { const { items, events } = schedule; if (!items || !events) { return bufferingLast; @@ -1907,10 +1985,17 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli ); if (!this.playbackDisabled) { if (isInterstitial) { + const bufferIndex = schedule.findAssetIndex( + item.event, + this.bufferedPos, + ); // primary fragment loading will exit early in base-stream-controller while `bufferingItem` is set to an Interstitial block - item.event.assetList.forEach((asset) => { + item.event.assetList.forEach((asset, i) => { const player = this.getAssetPlayer(asset.identifier); if (player) { + if (i === bufferIndex) { + player.loadSource(); + } player.resumeBuffering(); } }); @@ -1978,10 +2063,8 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli // Buffered to Interstitial boundary const player = this.preloadAssets(interstitial, assetListIndex); if (player?.interstitial.appendInPlace) { - // If we have a player and asset list info, start buffering - const assetItem = interstitial.assetList[assetListIndex]; const media = this.primaryMedia; - if (assetItem && media) { + if (media) { this.bufferAssetPlayer(player, media); } } @@ -2060,9 +2143,15 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli this.createAssetPlayer(interstitial, asset, i); } } - return this.getAssetPlayer( - interstitial.assetList[assetListIndex].identifier, - ); + const asset = interstitial.assetList[assetListIndex]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (asset) { + const player = this.getAssetPlayer(asset.identifier); + if (player) { + player.loadSource(); + } + return player; + } } return null; } @@ -2141,11 +2230,13 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli let videoPreference = userConfig.videoPreference; const currentLevel = primary.loadLevelObj || primary.levels[primary.currentLevel]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (videoPreference || currentLevel) { videoPreference = Object.assign({}, videoPreference); if (currentLevel.videoCodec) { videoPreference.videoCodec = currentLevel.videoCodec; } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (currentLevel.videoRange) { videoPreference.allowedVideoRanges = [currentLevel.videoRange]; } @@ -2165,6 +2256,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli const assetId = assetItem.identifier; const playerConfig: HlsAssetPlayerConfig = { ...userConfig, + maxMaxBufferLength: Math.min(180, primary.config.maxMaxBufferLength), autoStartLoad: true, startFragPrefetch: true, primarySessionId: primary.sessionId, @@ -2175,9 +2267,14 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli liveDurationInfinity: false, testBandwidth: false, videoPreference, - audioPreference: selectedAudio || userConfig.audioPreference, - subtitlePreference: selectedSubtitle || userConfig.subtitlePreference, + audioPreference: + (selectedAudio as MediaPlaylist | undefined) || + userConfig.audioPreference, + subtitlePreference: + (selectedSubtitle as MediaPlaylist | undefined) || + userConfig.subtitlePreference, }; + // TODO: limit maxMaxBufferLength in asset players to prevent QEE if (interstitial.appendInPlace) { interstitial.appendInPlaceStarted = true; if (assetItem.timelineStart) { @@ -2204,6 +2301,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli this.playerQueue.push(player); interstitial.assetList[assetListIndex] = assetItem; // Listen for LevelDetails and PTS change to update duration + let initialDuration = true; const updateAssetPlayerDetails = (details: LevelDetails) => { if (details.live) { const error = new Error( @@ -2215,10 +2313,12 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR, error, }; + const scheduleIndex = + this.schedule?.findEventIndex(interstitial.identifier) || -1; this.handleAssetItemError( errorData, interstitial, - this.schedule.findEventIndex(interstitial.identifier), + scheduleIndex, assetListIndex, error.message, ); @@ -2227,7 +2327,12 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli // Get time at end of last fragment const duration = details.edge - details.fragmentStart; const currentAssetDuration = assetItem.duration; - if (currentAssetDuration === null || duration > currentAssetDuration) { + if ( + initialDuration || + currentAssetDuration === null || + duration > currentAssetDuration + ) { + initialDuration = false; this.log( `Interstitial asset "${assetId}" duration change ${currentAssetDuration} > ${duration}`, ); @@ -2242,6 +2347,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli player.on(Events.LEVEL_PTS_UPDATED, (event, { details }) => updateAssetPlayerDetails(details), ); + player.on(Events.EVENT_CUE_ENTER, () => this.onInterstitialCueEnter()); const onBufferCodecs = ( event: Events.BUFFER_CODECS, data: BufferCodecsData, @@ -2264,7 +2370,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli const bufferedToEnd = () => { const inQueuPlayer = this.getAssetPlayer(assetId); this.log(`buffered to end of asset ${inQueuPlayer}`); - if (!inQueuPlayer) { + if (!inQueuPlayer || !this.schedule) { return; } // Preload at end of asset @@ -2273,23 +2379,14 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli ); const item = this.schedule.items?.[scheduleIndex]; if (this.isInterstitial(item)) { - const assetListIndex = interstitial.findAssetIndex(assetItem); - const nextAssetIndex = getNextAssetIndex(interstitial, assetListIndex); - if (!interstitial.isAssetPastPlayoutLimit(nextAssetIndex)) { - this.bufferedToItem(item, nextAssetIndex); - } else { - const nextItem = this.schedule.items?.[scheduleIndex + 1]; - if (nextItem) { - this.bufferedToItem(nextItem); - } - } + this.advanceAssetBuffering(item, assetItem); } }; player.on(Events.BUFFERED_TO_END, bufferedToEnd); const endedWithAssetIndex = (assetIndex) => { return () => { const inQueuPlayer = this.getAssetPlayer(assetId); - if (!inQueuPlayer) { + if (!inQueuPlayer || !this.schedule) { return; } this.shouldPlay = true; @@ -2302,28 +2399,17 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli player.once(Events.MEDIA_ENDED, endedWithAssetIndex(assetListIndex)); player.once(Events.PLAYOUT_LIMIT_REACHED, endedWithAssetIndex(Infinity)); player.on(Events.ERROR, (event: Events.ERROR, data: ErrorData) => { + if (!this.schedule) { + return; + } const inQueuPlayer = this.getAssetPlayer(assetId); if (data.details === ErrorDetails.BUFFER_STALLED_ERROR) { - if (inQueuPlayer?.media) { - const assetCurrentTime = inQueuPlayer.currentTime; - const distanceFromEnd = inQueuPlayer.duration - assetCurrentTime; - if ( - assetCurrentTime && - interstitial.appendInPlace && - distanceFromEnd / inQueuPlayer.media.playbackRate < 0.5 - ) { - this.log( - `Advancing buffer past end of asset ${assetId} ${interstitial} at ${inQueuPlayer.media.currentTime}`, - ); - bufferedToEnd(); - } else { - this.warn( - `Stalled at ${assetCurrentTime} of ${assetCurrentTime + distanceFromEnd} in asset ${assetId} ${interstitial}`, - ); - this.onTimeupdate(); - this.checkBuffer(true); - } + if (inQueuPlayer?.appendInPlace) { + this.handleInPlaceStall(interstitial); + return; } + this.onTimeupdate(); + this.checkBuffer(true); return; } this.handleAssetItemError( @@ -2336,7 +2422,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli }); player.on(Events.DESTROYING, () => { const inQueuPlayer = this.getAssetPlayer(assetId); - if (!inQueuPlayer) { + if (!inQueuPlayer || !this.schedule) { return; } const error = new Error(`Asset player destroyed unexpectedly ${assetId}`); @@ -2452,12 +2538,16 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli } private bufferAssetPlayer(player: HlsAssetPlayer, media: HTMLMediaElement) { + if (!this.schedule) { + return; + } const { interstitial, assetItem } = player; const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier); const item = this.schedule.items?.[scheduleIndex]; if (!item) { return; } + player.loadSource(); this.setBufferingItem(item); this.bufferingAsset = assetItem; const bufferingPlayer = this.getBufferingPlayer(); @@ -2479,6 +2569,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli if (appendInPlaceNext && assetItem !== this.playingAsset) { // Do not buffer another item if tracks are unknown or incompatible if (!player.tracks) { + this.log(`Waiting for track info before buffering ${player}`); return; } if ( @@ -2509,6 +2600,52 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli this.transferMediaTo(player, media); } + private handleInPlaceStall(interstitial: InterstitialEvent) { + const schedule = this.schedule; + const media = this.primaryMedia; + if (!schedule || !media) { + return; + } + const currentTime = media.currentTime; + const foundAssetIndex = schedule.findAssetIndex(interstitial, currentTime); + const stallingAsset = interstitial.assetList[foundAssetIndex] as + | InterstitialAssetItem + | undefined; + if (stallingAsset) { + const player = this.getAssetPlayer(stallingAsset.identifier); + if (player) { + const assetCurrentTime = + player.currentTime || currentTime - stallingAsset.timelineStart; + const distanceFromEnd = player.duration - assetCurrentTime; + this.warn( + `Stalled at ${assetCurrentTime} of ${assetCurrentTime + distanceFromEnd} in ${player} ${interstitial} (media.currentTime: ${currentTime})`, + ); + if ( + assetCurrentTime && + (distanceFromEnd / media.playbackRate < 0.5 || + player.bufferedInPlaceToEnd(media)) && + player.hls + ) { + const scheduleIndex = schedule.findEventIndex( + interstitial.identifier, + ); + this.advanceAfterAssetEnded( + interstitial, + scheduleIndex, + foundAssetIndex, + ); + } + } + } + } + + private advanceInPlace(time: number) { + const media = this.primaryMedia; + if (media && media.currentTime < time) { + media.currentTime = time; + } + } + private handleAssetItemError( data: ErrorData, interstitial: InterstitialEvent, @@ -2519,11 +2656,15 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli if (data.details === ErrorDetails.BUFFER_STALLED_ERROR) { return; } - const assetItem = interstitial.assetList[assetListIndex]; + const assetItem = (interstitial.assetList[assetListIndex] || + null) as InterstitialAssetItem | null; this.warn( `INTERSTITIAL_ASSET_ERROR ${assetItem ? eventAssetToString(assetItem) : assetItem} ${data.error}`, ); - const assetId = assetItem?.identifier; + if (!this.schedule) { + return; + } + const assetId = assetItem?.identifier || ''; const playerIndex = this.getAssetPlayerQueueIndex(assetId); const player = this.playerQueue[playerIndex] || null; const items = this.schedule.items; @@ -2543,6 +2684,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli } const playingAsset = this.playingAsset; + const bufferingAsset = this.bufferingAsset; const error = new Error(errorMessage); if (assetItem) { this.clearAssetPlayer(assetId, null); @@ -2563,6 +2705,12 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli this.primaryFallback(interstitial); } else if (playingAsset && playingAsset.identifier === assetId) { this.advanceAfterAssetEnded(interstitial, scheduleIndex, assetListIndex); + } else if ( + bufferingAsset && + bufferingAsset.identifier === assetId && + this.isInterstitial(this.bufferingItem) + ) { + this.advanceAssetBuffering(this.bufferingItem, bufferingAsset); } } @@ -2576,9 +2724,9 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli this.log( `Fallback to primary from event "${interstitial.identifier}" start: ${ flushStart - } pos: ${this.timelinePos} playing: ${ - playingItem ? segmentToString(playingItem) : '' - } error: ${interstitial.error}`, + } pos: ${this.timelinePos} playing: ${segmentToString( + playingItem, + )} error: ${interstitial.error}`, ); let timelinePos = this.timelinePos; if (timelinePos === -1) { @@ -2592,6 +2740,9 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli this.attachPrimary(flushStart, null); this.flushFrontBuffer(flushStart); } + if (!this.schedule) { + return; + } const scheduleIndex = this.schedule.findItemIndexAtTime(timelinePos); this.setSchedulePosition(scheduleIndex); } else { @@ -2607,7 +2758,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli const interstitial = data.event; const interstitialId = interstitial.identifier; const assets = data.assetListResponse.ASSETS; - if (!this.schedule.hasEvent(interstitialId)) { + if (!this.schedule?.hasEvent(interstitialId)) { // Interstitial with id was removed return; } @@ -2658,21 +2809,28 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli this.setBufferingItem(item); } this.setSchedulePosition(scheduleIndex); - } else if ( - bufferingEvent?.identifier === interstitialId && - bufferingEvent.appendInPlace - ) { - // If buffering (but not playback) has reached this item transfer media-source + } else if (bufferingEvent?.identifier === interstitialId) { const assetItem = interstitial.assetList[0]; - const player = this.getAssetPlayer(assetItem.identifier); - const media = this.primaryMedia; - if (assetItem && player && media) { - this.bufferAssetPlayer(player, media); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (assetItem) { + const player = this.getAssetPlayer(assetItem.identifier); + if (bufferingEvent.appendInPlace) { + // If buffering (but not playback) has reached this item transfer media-source + const media = this.primaryMedia; + if (player && media) { + this.bufferAssetPlayer(player, media); + } + } else if (player) { + player.loadSource(); + } } } } private onError(event: Events.ERROR, data: ErrorData) { + if (!this.schedule) { + return; + } switch (data.details) { case ErrorDetails.ASSET_LIST_PARSING_ERROR: case ErrorDetails.ASSET_LIST_LOAD_ERROR: @@ -2684,6 +2842,18 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli break; } case ErrorDetails.BUFFER_STALLED_ERROR: { + const stallingItem = + this.endedItem || this.waitingItem || this.playingItem; + if ( + this.isInterstitial(stallingItem) && + stallingItem.event.appendInPlace + ) { + this.handleInPlaceStall(stallingItem.event); + return; + } + this.log( + `Primary player stall @${this.timelinePos} bufferedPos: ${this.bufferedPos}`, + ); this.onTimeupdate(); this.checkBuffer(true); break; diff --git a/src/controller/interstitials-schedule.ts b/src/controller/interstitials-schedule.ts index 4193355327d..942bdfb2266 100644 --- a/src/controller/interstitials-schedule.ts +++ b/src/controller/interstitials-schedule.ts @@ -217,7 +217,8 @@ export class InterstitialsSchedule extends Logger { if ( timelinePos === timelineStart || (timelinePos > timelineStart && - timelinePos < timelineStart + (asset.duration || 0)) + (timelinePos < timelineStart + (asset.duration || 0) || + i === length - 1)) ) { return i; } diff --git a/src/controller/stream-controller.ts b/src/controller/stream-controller.ts index 93e692be61b..8865b301df0 100644 --- a/src/controller/stream-controller.ts +++ b/src/controller/stream-controller.ts @@ -1062,7 +1062,14 @@ export default class StreamController return; } if (this.reduceLengthAndFlushBuffer(data)) { - this.flushMainBuffer(0, Number.POSITIVE_INFINITY); + const isAssetPlayer = + !this.config.interstitialsController && this.config.assetPlayerId; + if (isAssetPlayer) { + // Use currentTime in buffer estimate to prevent loading more until playback advances + this._hasEnoughToStart = true; + } else { + this.flushMainBuffer(0, Number.POSITIVE_INFINITY); + } } break; case ErrorDetails.INTERNAL_EXCEPTION: @@ -1248,7 +1255,6 @@ export default class StreamController }); } - // This would be nice if Number.isFinite acted as a typeguard, but it doesn't. See: https://github.com/Microsoft/TypeScript/issues/10038 const baseTime = initSegment.initPTS as number; const timescale = initSegment.timescale as number; const initPTS = this.initPTS[frag.cc]; @@ -1258,12 +1264,18 @@ export default class StreamController initPTS.baseTime !== baseTime || initPTS.timescale !== timescale) ) { - this.initPTS[frag.cc] = { baseTime, timescale }; + const trackId = initSegment.trackId as number; + this.initPTS[frag.cc] = { + baseTime, + timescale, + trackId, + }; hls.trigger(Events.INIT_PTS_FOUND, { frag, id, initPTS: baseTime, timescale, + trackId, }); } } diff --git a/src/controller/timeline-controller.ts b/src/controller/timeline-controller.ts index 00942d277e4..7f8e38240fd 100644 --- a/src/controller/timeline-controller.ts +++ b/src/controller/timeline-controller.ts @@ -36,7 +36,7 @@ import type { MediaPlaylist } from '../types/media-playlist'; import type { VTTCCs } from '../types/vtt'; import type { CaptionScreen } from '../utils/cea-608-parser'; import type { CuesInterface } from '../utils/cues'; -import type { RationalTimestamp } from '../utils/timescale-conversion'; +import type { TimestampOffset } from '../utils/timescale-conversion'; type TrackProperties = { label: string; @@ -61,7 +61,7 @@ export class TimelineController implements ComponentAPI { private Cues: CuesInterface; private textTracks: Array = []; private tracks: Array = []; - private initPTS: RationalTimestamp[] = []; + private initPTS: TimestampOffset[] = []; private unparsedVttFrags: Array = []; private captionsTracks: Record = {}; private nonNativeCaptionsTracks: Record = {}; @@ -191,11 +191,11 @@ export class TimelineController implements ComponentAPI { // Triggered when an initial PTS is found; used for synchronisation of WebVTT. private onInitPtsFound( event: Events.INIT_PTS_FOUND, - { frag, id, initPTS, timescale }: InitPTSFoundData, + { frag, id, initPTS, timescale, trackId }: InitPTSFoundData, ) { const { unparsedVttFrags } = this; if (id === PlaylistLevelType.MAIN) { - this.initPTS[frag.cc] = { baseTime: initPTS, timescale }; + this.initPTS[frag.cc] = { baseTime: initPTS, timescale, trackId }; } // Due to asynchronous processing, initial PTS may arrive later than the first VTT fragments are loaded. diff --git a/src/demux/audio/base-audio-demuxer.ts b/src/demux/audio/base-audio-demuxer.ts index 5d9a382d281..4a980e11023 100644 --- a/src/demux/audio/base-audio-demuxer.ts +++ b/src/demux/audio/base-audio-demuxer.ts @@ -14,7 +14,10 @@ import { } from '../../types/demuxer'; import { appendUint8Array } from '../../utils/mp4-tools'; import { dummyTrack } from '../dummy-demuxed-track'; -import type { RationalTimestamp } from '../../utils/timescale-conversion'; +import type { + RationalTimestamp, + TimestampOffset, +} from '../../utils/timescale-conversion'; class BaseAudioDemuxer implements Demuxer { protected _audioTrack?: DemuxedAudioTrack; @@ -22,7 +25,7 @@ class BaseAudioDemuxer implements Demuxer { protected frameIndex: number = 0; protected cachedData: Uint8Array | null = null; protected basePTS: number | null = null; - protected initPTS: RationalTimestamp | null = null; + protected initPTS: TimestampOffset | null = null; protected lastPTS: number | null = null; resetInitSegment( @@ -42,7 +45,7 @@ class BaseAudioDemuxer implements Demuxer { }; } - resetTimeStamp(deaultTimestamp: RationalTimestamp | null) { + resetTimeStamp(deaultTimestamp: TimestampOffset | null) { this.initPTS = deaultTimestamp; this.resetContiguity(); } diff --git a/src/demux/transmuxer-interface.ts b/src/demux/transmuxer-interface.ts index 3ae429dacce..50f79aa527b 100644 --- a/src/demux/transmuxer-interface.ts +++ b/src/demux/transmuxer-interface.ts @@ -21,7 +21,7 @@ import type Hls from '../hls'; import type { MediaFragment, Part } from '../loader/fragment'; import type { ErrorData, FragDecryptedData } from '../types/events'; import type { ChunkMetadata, TransmuxerResult } from '../types/transmuxer'; -import type { RationalTimestamp } from '../utils/timescale-conversion'; +import type { TimestampOffset } from '../utils/timescale-conversion'; let transmuxerInstanceCount: number = 0; @@ -193,7 +193,7 @@ export default class TransmuxerInterface { duration: number, accurateTimeOffset: boolean, chunkMeta: ChunkMetadata, - defaultInitPTS?: RationalTimestamp, + defaultInitPTS?: TimestampOffset, ) { chunkMeta.transmuxing.start = self.performance.now(); const { instanceNo, transmuxer } = this; diff --git a/src/demux/transmuxer.ts b/src/demux/transmuxer.ts index 45d8d8db863..136a5443482 100644 --- a/src/demux/transmuxer.ts +++ b/src/demux/transmuxer.ts @@ -21,7 +21,7 @@ import type { Remuxer } from '../types/remuxer'; import type { ChunkMetadata, TransmuxerResult } from '../types/transmuxer'; import type { TypeSupported } from '../utils/codecs'; import type { ILogger } from '../utils/logger'; -import type { RationalTimestamp } from '../utils/timescale-conversion'; +import type { TimestampOffset } from '../utils/timescale-conversion'; let now: () => number; // performance.now() not available on WebWorker, at least on Safari Desktop @@ -312,7 +312,7 @@ export default class Transmuxer { chunkMeta.transmuxing.executeEnd = now(); } - resetInitialTimestamp(defaultInitPts: RationalTimestamp | null) { + resetInitialTimestamp(defaultInitPts: TimestampOffset | null) { const { demuxer, remuxer } = this; if (!demuxer || !remuxer) { return; @@ -517,14 +517,14 @@ export class TransmuxConfig { public videoCodec?: string; public initSegmentData?: Uint8Array; public duration: number; - public defaultInitPts: RationalTimestamp | null; + public defaultInitPts: TimestampOffset | null; constructor( audioCodec: string | undefined, videoCodec: string | undefined, initSegmentData: Uint8Array | undefined, duration: number, - defaultInitPts?: RationalTimestamp, + defaultInitPts?: TimestampOffset, ) { this.audioCodec = audioCodec; this.videoCodec = videoCodec; diff --git a/src/demux/tsdemuxer.ts b/src/demux/tsdemuxer.ts index aa52b4881a9..863e0baadc1 100644 --- a/src/demux/tsdemuxer.ts +++ b/src/demux/tsdemuxer.ts @@ -191,6 +191,7 @@ class TSDemuxer implements Demuxer { this._audioTrack.segmentCodec = 'aac'; // flush any partial content + this.videoParser = null; this.aacOverFlow = null; this.remainderData = null; this.audioCodec = audioCodec; @@ -291,18 +292,7 @@ class TSDemuxer implements Demuxer { case videoPid: if (stt) { if (videoData && (pes = parsePES(videoData, this.logger))) { - if (this.videoParser === null) { - switch (videoTrack.segmentCodec) { - case 'avc': - this.videoParser = new AvcVideoParser(); - break; - case 'hevc': - if (__USE_M2TS_ADVANCED_CODECS__) { - this.videoParser = new HevcVideoParser(); - } - break; - } - } + this.readyVideoParser(videoTrack.segmentCodec); if (this.videoParser !== null) { this.videoParser.parsePES(videoTrack, textTrack, pes, false); } @@ -477,18 +467,7 @@ class TSDemuxer implements Demuxer { // try to parse last PES packets let pes: PES | null; if (videoData && (pes = parsePES(videoData, this.logger))) { - if (this.videoParser === null) { - switch (videoTrack.segmentCodec) { - case 'avc': - this.videoParser = new AvcVideoParser(); - break; - case 'hevc': - if (__USE_M2TS_ADVANCED_CODECS__) { - this.videoParser = new HevcVideoParser(); - } - break; - } - } + this.readyVideoParser(videoTrack.segmentCodec); if (this.videoParser !== null) { this.videoParser.parsePES( videoTrack as DemuxedVideoTrack, @@ -557,6 +536,16 @@ class TSDemuxer implements Demuxer { return this.decrypt(demuxResult, sampleAes); } + private readyVideoParser(codec: string | undefined) { + if (this.videoParser === null) { + if (codec === 'avc') { + this.videoParser = new AvcVideoParser(); + } else if (__USE_M2TS_ADVANCED_CODECS__ && codec === 'hevc') { + this.videoParser = new HevcVideoParser(); + } + } + } + private decrypt( demuxResult: DemuxerResult, sampleAes: SampleAesDecrypter, diff --git a/src/demux/video/hevc-video-parser.ts b/src/demux/video/hevc-video-parser.ts index 485ee85c6bd..61d00640a4c 100644 --- a/src/demux/video/hevc-video-parser.ts +++ b/src/demux/video/hevc-video-parser.ts @@ -516,10 +516,10 @@ class HevcVideoParser extends BaseVideoParser { eg.readBoolean(); // frame_field_info_present_flag default_display_window_flag = eg.readBoolean(); if (default_display_window_flag) { - pic_left_offset += eg.readUEG(); - pic_right_offset += eg.readUEG(); - pic_top_offset += eg.readUEG(); - pic_bottom_offset += eg.readUEG(); + eg.skipUEG(); + eg.skipUEG(); + eg.skipUEG(); + eg.skipUEG(); } const vui_timing_info_present_flag = eg.readBoolean(); if (vui_timing_info_present_flag) { @@ -604,7 +604,7 @@ class HevcVideoParser extends BaseVideoParser { let width = pic_width_in_luma_samples, height = pic_height_in_luma_samples; - if (conformance_window_flag || default_display_window_flag) { + if (conformance_window_flag) { let chroma_scale_w = 1, chroma_scale_h = 1; if (chroma_format_idc === 1) { diff --git a/src/hls.ts b/src/hls.ts index 663cb277885..718005282f7 100644 --- a/src/hls.ts +++ b/src/hls.ts @@ -1533,4 +1533,7 @@ export type { KeySystems, KeySystemFormats, } from './utils/mediakeys-helper'; -export type { RationalTimestamp } from './utils/timescale-conversion'; +export type { + RationalTimestamp, + TimestampOffset, +} from './utils/timescale-conversion'; diff --git a/src/loader/fragment.ts b/src/loader/fragment.ts index 9431b039a7a..97e5e08cd95 100644 --- a/src/loader/fragment.ts +++ b/src/loader/fragment.ts @@ -177,7 +177,7 @@ export class Fragment extends BaseSegment { // levelkeys are the EXT-X-KEY tags that apply to this segment for decryption // core difference from the private field _decryptdata is the lack of the initialized IV // _decryptdata will set the IV for this segment based on the segment number in the fragment - public levelkeys?: { [key: string]: LevelKey }; + public levelkeys?: { [key: string]: LevelKey | undefined }; // A string representing the fragment type public readonly type: PlaylistLevelType; // A reference to the loader. Set while the fragment is loading, and removed afterwards. Used to abort fragment loading @@ -233,7 +233,7 @@ export class Fragment extends BaseSegment { return total; } } - if (this.byteRange) { + if (this.byteRange.length) { const start = this.byteRange[0]; const end = this.byteRange[1]; if (Number.isFinite(start) && Number.isFinite(end)) { @@ -270,9 +270,11 @@ export class Fragment extends BaseSegment { } else { const keyFormats = Object.keys(this.levelkeys); if (keyFormats.length === 1) { - return (this._decryptdata = this.levelkeys[ - keyFormats[0] - ].getDecryptData(this.sn)); + const levelKey = (this._decryptdata = + this.levelkeys[keyFormats[0]] || null); + if (levelKey) { + return levelKey.getDecryptData(this.sn); + } } else { // Multiple keys. key-loader to call Fragment.setKeyFormat based on selected key-system. } @@ -305,7 +307,7 @@ export class Fragment extends BaseSegment { } else if (this.levelkeys) { const keyFormats = Object.keys(this.levelkeys); const len = keyFormats.length; - if (len > 1 || (len === 1 && this.levelkeys[keyFormats[0]].encrypted)) { + if (len > 1 || (len === 1 && this.levelkeys[keyFormats[0]]?.encrypted)) { return true; } } diff --git a/src/loader/m3u8-parser.ts b/src/loader/m3u8-parser.ts index 36d42a5246c..12eb6e92c99 100644 --- a/src/loader/m3u8-parser.ts +++ b/src/loader/m3u8-parser.ts @@ -43,6 +43,8 @@ type ParsedMultivariantMediaOptions = { 'CLOSED-CAPTIONS'?: MediaPlaylist[]; }; +type LevelKeys = { [key: string]: LevelKey | undefined }; + const MASTER_PLAYLIST_REGEX = /#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-(SESSION-DATA|SESSION-KEY|DEFINE|CONTENT-STEERING|START):([^\r\n]*)[\r\n]+/g; const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g; @@ -254,8 +256,9 @@ export default class M3U8Parser { const attrs = new AttrList(result[1], parsed) as MediaAttributes; const type = attrs.TYPE; if (type) { - const groups: (typeof groupsByType)[keyof typeof groupsByType] = - groupsByType[type]; + const groups: + | (typeof groupsByType)[keyof typeof groupsByType] + | undefined = groupsByType[type]; const medias: MediaPlaylist[] = results[type] || []; results[type] = medias; const lang = attrs.LANGUAGE; @@ -328,7 +331,7 @@ export default class M3U8Parser { let frag: Fragment = new Fragment(type, base); let result: RegExpExecArray | RegExpMatchArray | null; let i: number; - let levelkeys: { [key: string]: LevelKey } | undefined; + let levelkeys: LevelKeys | undefined; let firstPdtIndex = -1; let createNextFrag = false; let nextByteRange: string | null = null; @@ -351,7 +354,7 @@ export default class M3U8Parser { frag = new Fragment(type, base); // setup the next fragment for part loading frag.playlistOffset = totalduration; - frag.start = totalduration; + frag.setStart(totalduration); frag.sn = currentSN; frag.cc = discontinuityCounter; if (currentBitrate) { @@ -383,7 +386,7 @@ export default class M3U8Parser { // url if (Number.isFinite(frag.duration)) { frag.playlistOffset = totalduration; - frag.start = totalduration; + frag.setStart(totalduration); if (levelkeys) { setFragLevelKeys(frag, levelkeys, level); } @@ -414,7 +417,7 @@ export default class M3U8Parser { continue; } for (i = 1; i < result.length; i++) { - if (result[i] !== undefined) { + if ((result[i] as any) !== undefined) { break; } } @@ -720,9 +723,6 @@ export default class M3U8Parser { if (!level.live) { lastFragment.endList = true; } - if (firstFragment && level.startCC === undefined) { - level.startCC = firstFragment.cc; - } /** * Backfill any missing PDT values * "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after @@ -738,9 +738,6 @@ export default class M3U8Parser { programDateTimes.unshift(firstFragment as MediaFragment); } } - } else { - level.endSN = 0; - level.startCC = 0; } if (level.fragmentHint) { totalduration += level.fragmentHint.duration; @@ -769,7 +766,7 @@ export function mapDateRanges( const playlistEnd = details.live ? Infinity : details.totalduration; const dateRangeIds = Object.keys(details.dateRanges); for (let i = dateRangeIds.length; i--; ) { - const dateRange = details.dateRanges[dateRangeIds[i]]; + const dateRange = details.dateRanges[dateRangeIds[i]]!; const startDateTime = dateRange.startDate.getTime(); dateRange.tagAnchor = lastProgramDateTime.ref; for (let j = programDateTimeCount; j--; ) { @@ -795,7 +792,7 @@ function findFragmentWithStartDate( index: number, endTime: number, ): number { - const pdtFragment = programDateTimes[index]; + const pdtFragment = programDateTimes[index] as MediaFragment | undefined; if (pdtFragment) { // find matching range between PDT tags const pdtStart = pdtFragment.programDateTime as number; @@ -934,7 +931,7 @@ function setInitSegment( frag: Fragment, mapAttrs: AttrList, id: number, - levelkeys: { [key: string]: LevelKey } | undefined, + levelkeys: LevelKeys | undefined, ) { frag.relurl = mapAttrs.URI; if (mapAttrs.BYTERANGE) { @@ -950,7 +947,7 @@ function setInitSegment( function setFragLevelKeys( frag: Fragment, - levelkeys: { [key: string]: LevelKey }, + levelkeys: LevelKeys, level: LevelDetails, ) { frag.levelkeys = levelkeys; @@ -960,7 +957,7 @@ function setFragLevelKeys( encryptedFragments[encryptedFragments.length - 1].levelkeys !== levelkeys) && Object.keys(levelkeys).some( - (format) => levelkeys![format].isCommonEncryption, + (format) => levelkeys[format]!.isCommonEncryption, ) ) { encryptedFragments.push(frag); diff --git a/src/loader/playlist-loader.ts b/src/loader/playlist-loader.ts index 168db972e9e..b589ad1f8fc 100644 --- a/src/loader/playlist-loader.ts +++ b/src/loader/playlist-loader.ts @@ -711,7 +711,7 @@ class PlaylistLoader implements NetworkComponentAPI { } const error = levelDetails.playlistParsingError; if (error) { - this.hls.logger.warn(error); + this.hls.logger.warn(`${error} ${levelDetails.url}`); if (!hls.config.ignorePlaylistParsingErrors) { hls.trigger(Events.ERROR, { type: ErrorTypes.NETWORK_ERROR, diff --git a/src/remux/mp4-remuxer.ts b/src/remux/mp4-remuxer.ts index f97bfabd6af..9bd59545f98 100644 --- a/src/remux/mp4-remuxer.ts +++ b/src/remux/mp4-remuxer.ts @@ -27,7 +27,10 @@ import type { } from '../types/remuxer'; import type { TrackSet } from '../types/track'; import type { TypeSupported } from '../utils/codecs'; -import type { RationalTimestamp } from '../utils/timescale-conversion'; +import type { + RationalTimestamp, + TimestampOffset, +} from '../utils/timescale-conversion'; const MAX_SILENT_FRAME_DURATION = 10 * 1000; // 10 seconds const AAC_SAMPLES_PER_FRAME = 1024; @@ -62,8 +65,8 @@ export default class MP4Remuxer extends Logger implements Remuxer { private readonly config: HlsConfig; private readonly typeSupported: TypeSupported; private ISGenerated: boolean = false; - private _initPTS: RationalTimestamp | null = null; - private _initDTS: RationalTimestamp | null = null; + private _initPTS: TimestampOffset | null = null; + private _initDTS: TimestampOffset | null = null; private nextVideoTs: number | null = null; private nextAudioTs: number | null = null; private videoSampleDuration: number | null = null; @@ -103,7 +106,7 @@ export default class MP4Remuxer extends Logger implements Remuxer { this.config = this.videoTrackConfig = this._initPTS = this._initDTS = null; } - resetTimeStamp(defaultTimeStamp: RationalTimestamp | null) { + resetTimeStamp(defaultTimeStamp: TimestampOffset | null) { this.log('initPTS & initDTS reset'); this._initPTS = this._initDTS = defaultTimeStamp; } @@ -347,7 +350,7 @@ export default class MP4Remuxer extends Logger implements Remuxer { let initPTS: number | undefined; let initDTS: number | undefined; let timescale: number | undefined; - let trackId: number | undefined; + let trackId: number = -1; if (computePTSDTS) { initPTS = initDTS = Infinity; @@ -439,13 +442,23 @@ export default class MP4Remuxer extends Logger implements Remuxer { if (Object.keys(tracks).length) { this.ISGenerated = true; if (computePTSDTS) { + if (_initPTS) { + this.warn( + `Timestamps at playlist time: ${accurateTimeOffset ? '' : '~'}${timeOffset} ${initPTS! / timescale!} != initPTS: ${_initPTS.baseTime / _initPTS.timescale} (${_initPTS.baseTime}/${_initPTS.timescale}) trackId: ${_initPTS.trackId}`, + ); + } + this.log( + `Found initPTS at playlist time: ${timeOffset} offset: ${initPTS! / timescale!} (${initPTS}/${timescale}) trackId: ${trackId}`, + ); this._initPTS = { baseTime: initPTS as number, timescale: timescale as number, + trackId: trackId as number, }; this._initDTS = { baseTime: initDTS as number, timescale: timescale as number, + trackId: trackId as number, }; } else { initPTS = timescale = undefined; @@ -1134,8 +1147,8 @@ function findKeyframeIndex(samples: Array): number { export function flushTextTrackMetadataCueSamples( track: DemuxedMetadataTrack, timeOffset: number, - initPTS: RationalTimestamp, - initDTS: RationalTimestamp, + initPTS: TimestampOffset, + initDTS: TimestampOffset, ): RemuxedMetadata | undefined { const length = track.samples.length; if (!length) { diff --git a/src/remux/passthrough-remuxer.ts b/src/remux/passthrough-remuxer.ts index 22faa8a4503..de243ed10fe 100644 --- a/src/remux/passthrough-remuxer.ts +++ b/src/remux/passthrough-remuxer.ts @@ -25,14 +25,14 @@ import type { import type { TrackSet } from '../types/track'; import type { TypeSupported } from '../utils/codecs'; import type { InitData, InitDataTrack, TrackTimes } from '../utils/mp4-tools'; -import type { RationalTimestamp } from '../utils/timescale-conversion'; +import type { TimestampOffset } from '../utils/timescale-conversion'; class PassThroughRemuxer extends Logger implements Remuxer { private emitInitSegment: boolean = false; private audioCodec?: string; private videoCodec?: string; private initData?: InitData; - private initPTS: (RationalTimestamp & { trackId?: number }) | null = null; + private initPTS: TimestampOffset | null = null; private initTracks?: TrackSet; private lastEndTime: number | null = null; private isVideoContiguous: boolean = false; @@ -48,7 +48,7 @@ class PassThroughRemuxer extends Logger implements Remuxer { public destroy() {} - public resetTimeStamp(defaultInitPTS: RationalTimestamp | null) { + public resetTimeStamp(defaultInitPTS: TimestampOffset | null) { this.lastEndTime = null; const initPTS = this.initPTS; if (initPTS && defaultInitPTS) { @@ -182,7 +182,7 @@ class PassThroughRemuxer extends Logger implements Remuxer { return result; } if (this.emitInitSegment) { - initSegment.tracks = this.initTracks as TrackSet; + initSegment.tracks = this.initTracks; this.emitInitSegment = false; } @@ -199,53 +199,71 @@ class PassThroughRemuxer extends Logger implements Remuxer { const videoEndTime = toStartEndOrDefault(videoSampleTimestamps, 0, true); const audioEndTime = toStartEndOrDefault(audioSampleTimestamps, 0, true); - let baseOffsetSamples: TrackTimes | undefined; let decodeTime = timeOffset; - let duration: number = 0; - if ( + let duration = 0; + + const syncOnAudio = audioSampleTimestamps && (!videoSampleTimestamps || (!initPTS && audioStartTime < videoStartTime) || - (initPTS && initPTS.trackId === initData.audio!.id)) - ) { - initSegment.trackId = initData.audio!.id; - baseOffsetSamples = audioSampleTimestamps; - duration = audioEndTime - audioStartTime; - } else if (videoSampleTimestamps) { - initSegment.trackId = initData.video!.id; - baseOffsetSamples = videoSampleTimestamps; - duration = videoEndTime - videoStartTime; - } + (initPTS && initPTS.trackId === initData.audio!.id)); + const baseOffsetSamples = syncOnAudio + ? audioSampleTimestamps + : videoSampleTimestamps; + if (baseOffsetSamples) { const timescale = baseOffsetSamples.timescale; + const baseTime = baseOffsetSamples.start - timeOffset * timescale; + const trackId = syncOnAudio ? initData.audio!.id : initData.video!.id; + decodeTime = baseOffsetSamples.start / timescale; - initSegment.initPTS = baseOffsetSamples.start - timeOffset * timescale; - initSegment.timescale = timescale; - if (!initPTS) { - this.initPTS = initPTS = { - baseTime: initSegment.initPTS, - timescale, - trackId: initSegment.trackId, - }; - } - } + duration = syncOnAudio + ? audioEndTime - audioStartTime + : videoEndTime - videoStartTime; - if ( - (accurateTimeOffset || !initPTS) && - (isInvalidInitPts(initPTS, decodeTime, timeOffset, duration) || - initSegment.timescale !== initPTS.timescale) - ) { - initSegment.initPTS = decodeTime - timeOffset; - initSegment.timescale = 1; - if (initPTS && initPTS.timescale === 1) { - this.warn( - `Adjusting initPTS @${timeOffset} from ${initPTS.baseTime / initPTS.timescale} to ${initSegment.initPTS}`, + if ( + (accurateTimeOffset || !initPTS) && + (isInvalidInitPts(initPTS, decodeTime, timeOffset, duration) || + timescale !== initPTS.timescale) + ) { + if (initPTS) { + this.warn( + `Timestamps at playlist time: ${accurateTimeOffset ? '' : '~'}${timeOffset} ${baseTime / timescale} != initPTS: ${initPTS.baseTime / initPTS.timescale} (${initPTS.baseTime}/${initPTS.timescale}) trackId: ${initPTS.trackId}`, + ); + } + this.log( + `Found initPTS at playlist time: ${timeOffset} offset: ${decodeTime - timeOffset} (${baseTime}/${timescale}) trackId: ${trackId}`, ); + initPTS = null; + initSegment.initPTS = baseTime; + initSegment.timescale = timescale; + initSegment.trackId = trackId; + } + } else { + this.warn( + `No audio or video samples found for initPTS at playlist time: ${timeOffset}`, + ); + } + if (!initPTS) { + if ( + !initSegment.timescale || + initSegment.trackId === undefined || + initSegment.initPTS === undefined + ) { + this.warn('Could not set initPTS'); + initSegment.initPTS = decodeTime; + initSegment.timescale = 1; + initSegment.trackId = -1; } this.initPTS = initPTS = { baseTime: initSegment.initPTS, - timescale: 1, + timescale: initSegment.timescale, + trackId: initSegment.trackId, }; + } else { + initSegment.initPTS = initPTS.baseTime; + initSegment.timescale = initPTS.timescale; + initSegment.trackId = initPTS.trackId; } const startTime = audioTrack @@ -348,7 +366,7 @@ function toStartEndOrDefault( } function isInvalidInitPts( - initPTS: RationalTimestamp | null, + initPTS: TimestampOffset | null, startDTS: number, timeOffset: number, duration: number, diff --git a/src/types/events.ts b/src/types/events.ts index 2d88349ec5c..975cf49656f 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -91,7 +91,7 @@ export interface BufferAppendingData { chunkMeta: ChunkMetadata; offset?: number | undefined; parent: PlaylistLevelType; - data: Uint8Array; + data: Uint8Array; } export interface BufferAppendedData { @@ -374,6 +374,7 @@ export interface InitPTSFoundData { frag: MediaFragment; initPTS: number; timescale: number; + trackId: number; } export interface FragLoadingData { diff --git a/src/types/remuxer.ts b/src/types/remuxer.ts index b6f2b6019b6..9981456ba53 100644 --- a/src/types/remuxer.ts +++ b/src/types/remuxer.ts @@ -11,7 +11,7 @@ import type { import type { PlaylistLevelType } from './loader'; import type { TrackSet } from './track'; import type { DecryptData } from '../loader/level-key'; -import type { RationalTimestamp } from '../utils/timescale-conversion'; +import type { TimestampOffset } from '../utils/timescale-conversion'; export interface Remuxer { remux( @@ -30,7 +30,7 @@ export interface Remuxer { videoCodec: string | undefined, decryptdata: DecryptData | null, ): void; - resetTimeStamp(defaultInitPTS: RationalTimestamp | null): void; + resetTimeStamp(defaultInitPTS: TimestampOffset | null): void; resetNextTimestamp(): void; destroy(): void; } diff --git a/src/utils/discontinuities.ts b/src/utils/discontinuities.ts index 795365e05b9..a6abea9ef71 100644 --- a/src/utils/discontinuities.ts +++ b/src/utils/discontinuities.ts @@ -31,11 +31,10 @@ export function shouldAlignOnDiscontinuities( } function adjustFragmentStart(frag: Fragment, sliding: number) { - if (frag) { - const start = frag.start + sliding; - frag.start = frag.startPTS = start; - frag.endPTS = start + frag.duration; - } + const start = frag.start + sliding; + frag.startPTS = start; + frag.setStart(start); + frag.endPTS = start + frag.duration; } export function adjustSlidingStart(sliding: number, details: LevelDetails) { @@ -68,13 +67,13 @@ export function alignStream( return; } alignDiscontinuities(details, switchDetails); - if (!details.alignedSliding && switchDetails) { + if (!details.alignedSliding) { // If the PTS wasn't figured out via discontinuity sequence that means there was no CC increase within the level. // Aligning via Program Date Time should therefore be reliable, since PDT should be the same within the same // discontinuity sequence. alignMediaPlaylistByPDT(details, switchDetails); } - if (!details.alignedSliding && switchDetails && !details.skippedSegments) { + if (!details.alignedSliding && !details.skippedSegments) { // Try to align on sn so that we pick a better start fragment. // Do not perform this on playlists with delta updates as this is only to align levels on switch // and adjustSliding only adjusts fragments after skippedSegments. diff --git a/src/utils/imsc1-ttml-parser.ts b/src/utils/imsc1-ttml-parser.ts index 764176fc10e..2a6dc5b6d9c 100644 --- a/src/utils/imsc1-ttml-parser.ts +++ b/src/utils/imsc1-ttml-parser.ts @@ -4,7 +4,7 @@ import { toTimescaleFromScale } from './timescale-conversion'; import VTTCue from './vttcue'; import { parseTimeStamp } from './vttparser'; import { generateCueId } from './webvtt-parser'; -import type { RationalTimestamp } from './timescale-conversion'; +import type { TimestampOffset } from './timescale-conversion'; export const IMSC1_CODEC = 'stpp.ttml.im1t'; @@ -24,7 +24,7 @@ const textAlignToLineAlign: Partial> = { export function parseIMSC1( payload: ArrayBuffer, - initPTS: RationalTimestamp, + initPTS: TimestampOffset, callBack: (cues: Array) => any, errorCallBack: (error: Error) => any, ) { diff --git a/src/utils/texttrack-utils.ts b/src/utils/texttrack-utils.ts index a39ef3071dc..6ae14eeece6 100644 --- a/src/utils/texttrack-utils.ts +++ b/src/utils/texttrack-utils.ts @@ -49,7 +49,10 @@ export function addCueToTrack(track: TextTrack, cue: VTTCue) { } } -export function clearCurrentCues(track: TextTrack, enterHandler?: () => void) { +export function clearCurrentCues( + track: TextTrack, + enterHandler?: (e?: Event) => void, +) { // When track.mode is disabled, track.cues will be null. // To guarantee the removal of cues, we need to temporarily // change the mode to hidden diff --git a/src/utils/timescale-conversion.ts b/src/utils/timescale-conversion.ts index f46aa531871..c2762a6ba32 100644 --- a/src/utils/timescale-conversion.ts +++ b/src/utils/timescale-conversion.ts @@ -5,6 +5,8 @@ export type RationalTimestamp = { timescale: number; // ticks per second }; +export type TimestampOffset = RationalTimestamp & { trackId: number }; + export function toTimescaleFromBase( baseTime: number, destScale: number, diff --git a/src/utils/webvtt-parser.ts b/src/utils/webvtt-parser.ts index 7e44e3da932..57a683ee518 100644 --- a/src/utils/webvtt-parser.ts +++ b/src/utils/webvtt-parser.ts @@ -3,7 +3,7 @@ import { hash } from './hash'; import { toMpegTsClockFromTimescale } from './timescale-conversion'; import { VTTParser } from './vttparser'; import { normalizePts } from '../remux/mp4-remuxer'; -import type { RationalTimestamp } from './timescale-conversion'; +import type { TimestampOffset } from './timescale-conversion'; import type { VTTCCs } from '../types/vtt'; const LINEBREAKS = /\r\n|\n\r|\n|\r/g; @@ -80,7 +80,7 @@ const calculateOffset = function (vttCCs: VTTCCs, cc, presentationTime) { export function parseWebVTT( vttByteArray: ArrayBuffer, - initPTS: RationalTimestamp | undefined, + initPTS: TimestampOffset | undefined, vttCCs: VTTCCs, cc: number, timeOffset: number, diff --git a/tests/unit/controller/audio-stream-controller.ts b/tests/unit/controller/audio-stream-controller.ts index 7dd91954a18..2076463f9a3 100644 --- a/tests/unit/controller/audio-stream-controller.ts +++ b/tests/unit/controller/audio-stream-controller.ts @@ -7,12 +7,13 @@ import { State } from '../../../src/controller/base-stream-controller'; import { FragmentTracker } from '../../../src/controller/fragment-tracker'; import { Events } from '../../../src/events'; import Hls from '../../../src/hls'; +import { Fragment } from '../../../src/loader/fragment'; import KeyLoader from '../../../src/loader/key-loader'; import { LoadStats } from '../../../src/loader/load-stats'; import { Level } from '../../../src/types/level'; +import { PlaylistLevelType } from '../../../src/types/loader'; import { AttrList } from '../../../src/utils/attr-list'; import { adjustSlidingStart } from '../../../src/utils/discontinuities'; -import type { Fragment } from '../../../src/loader/fragment'; import type { LevelDetails } from '../../../src/loader/level-details'; import type { AudioTrackLoadedData, @@ -180,19 +181,19 @@ describe('AudioStreamController', function () { const getPlaylistData = function ( startSN: number, endSN: number, - type: 'audio' | 'main', + type: PlaylistLevelType, live: boolean, ) { const targetduration = 10; const fragments: Fragment[] = Array.from(new Array(endSN - startSN)).map( - (u, i) => - ({ - sn: i + startSN, - cc: Math.floor((i + startSN) / 10), - start: i * targetduration, - duration: targetduration, - type, - }) as unknown as Fragment, + (u, i) => { + const frag = new Fragment(type, ''); + frag.sn = i + startSN; + frag.cc = Math.floor((i + startSN) / 10); + frag.setStart(i * targetduration); + frag.duration = targetduration; + return frag; + }, ); return { details: { @@ -221,7 +222,12 @@ describe('AudioStreamController', function () { endSN: number, live: boolean = false, ): LevelLoadedData { - const data = getPlaylistData(startSN, endSN, 'main', live); + const data = getPlaylistData( + startSN, + endSN, + PlaylistLevelType.MAIN, + live, + ); const levelData: LevelLoadedData = { ...data, level: 0, @@ -234,7 +240,12 @@ describe('AudioStreamController', function () { endSN: number, live: boolean = false, ): AudioTrackLoadedData { - const data = getPlaylistData(startSN, endSN, 'audio', live); + const data = getPlaylistData( + startSN, + endSN, + PlaylistLevelType.AUDIO, + live, + ); const audioTrackData: AudioTrackLoadedData = { ...data, groupId: 'audio', diff --git a/tests/unit/controller/base-stream-controller.ts b/tests/unit/controller/base-stream-controller.ts index a7ca4754086..40a3651795c 100644 --- a/tests/unit/controller/base-stream-controller.ts +++ b/tests/unit/controller/base-stream-controller.ts @@ -68,7 +68,7 @@ describe('BaseStreamController', function () { const frag = new Fragment(PlaylistLevelType.MAIN, '') as MediaFragment; frag.duration = 5; frag.sn = i; - frag.start = i * 5; + frag.setStart(i * 5); details.fragments.push(frag); } details.live = live; diff --git a/tests/unit/controller/buffer-controller.ts b/tests/unit/controller/buffer-controller.ts index 3d89bd8524e..86994802693 100644 --- a/tests/unit/controller/buffer-controller.ts +++ b/tests/unit/controller/buffer-controller.ts @@ -137,7 +137,6 @@ describe('BufferController', function () { .stub(bufferController, 'createSourceBuffers') .callsFake(() => { Object.keys(bufferController.tracks).forEach((type) => { - bufferController.tracks ||= {}; bufferController.tracks[type] = { appendBuffer: () => {}, remove: () => {}, diff --git a/tests/unit/controller/cap-level-controller.js b/tests/unit/controller/cap-level-controller.ts similarity index 90% rename from tests/unit/controller/cap-level-controller.js rename to tests/unit/controller/cap-level-controller.ts index b478d41ee05..401a51eee42 100644 --- a/tests/unit/controller/cap-level-controller.js +++ b/tests/unit/controller/cap-level-controller.ts @@ -1,30 +1,38 @@ +import chai from 'chai'; import sinon from 'sinon'; -import Hls from '../../../src/hls'; +import sinonChai from 'sinon-chai'; import CapLevelController from '../../../src/controller/cap-level-controller'; import { Events } from '../../../src/events'; +import Hls from '../../../src/hls'; +import { Level } from '../../../src/types/level'; +import { parsedLevel } from '../utils/mock-level'; + +chai.use(sinonChai); +const expect = chai.expect; -const levels = [ - { +const parsedLevels = [ + parsedLevel({ width: 360, height: 360, - bandwidth: 1000, - }, - { + bitrate: 1000, + }), + parsedLevel({ width: 540, height: 540, - bandwidth: 2000, - }, - { + bitrate: 2000, + }), + parsedLevel({ width: 540, height: 540, - bandwidth: 3000, - }, - { + bitrate: 3000, + }), + parsedLevel({ width: 720, height: 720, - bandwidth: 4000, - }, + bitrate: 4000, + }), ]; +const levels = parsedLevels.map((parsedLevel) => new Level(parsedLevel)); describe('CapLevelController', function () { describe('getMaxLevelByMediaSize', function () { @@ -63,16 +71,6 @@ describe('CapLevelController', function () { const actual = CapLevelController.getMaxLevelByMediaSize([], 5000, 5000); expect(expected).to.equal(actual); }); - - it('Should return -1 if there levels is undefined', function () { - const expected = -1; - const actual = CapLevelController.getMaxLevelByMediaSize( - undefined, - 5000, - 5000, - ); - expect(expected).to.equal(actual); - }); }); describe('getDimensions', function () { @@ -100,7 +98,10 @@ describe('CapLevelController', function () { if (media.parentNode) { media.parentNode.removeChild(media); } - document.body.removeChild(document.querySelector('#test-fixture')); + const fixture = document.querySelector('#test-fixture'); + if (fixture) { + document.body.removeChild(fixture); + } hls.destroy(); }); @@ -126,7 +127,14 @@ describe('CapLevelController', function () { it('gets client bounds width and height when media element is in the DOM', function () { media.style.width = '1280px'; media.style.height = '720px'; - document.querySelector('#test-fixture').appendChild(media); + + const fixture = document.querySelector('#test-fixture'); + if (!fixture) { + expect(fixture).is.not.null; + return; + } + fixture.appendChild(media); + const pixelRatio = capLevelController.contentScaleFactor; const bounds = capLevelController.getDimensions(); expect(bounds.width).to.equal(1280); @@ -150,11 +158,17 @@ describe('CapLevelController', function () { media.style.width = '1280px'; media.style.height = '720px'; - document.querySelector('#test-fixture').appendChild(media); + + const fixture = document.querySelector('#test-fixture'); + if (!fixture) { + expect(fixture).is.not.null; + return; + } + fixture.appendChild(media); + capLevelController.onMediaAttaching(Events.MEDIA_ATTACHING, { media, }); - const pixelRatio = capLevelController.contentScaleFactor; bounds = capLevelController.getDimensions(); expect(bounds.width).to.equal(1280); diff --git a/tests/unit/controller/eme-controller.ts b/tests/unit/controller/eme-controller.ts index 2564181b2db..b94509a3005 100644 --- a/tests/unit/controller/eme-controller.ts +++ b/tests/unit/controller/eme-controller.ts @@ -158,9 +158,6 @@ describe('EMEController', function () { } as any); expect(emePromise).to.be.a('Promise'); - if (!emePromise) { - return; - } return emePromise.finally(() => { expect(media.setMediaKeys).callCount(1); expect(reqMediaKsAccessSpy).callCount(1); @@ -226,9 +223,6 @@ describe('EMEController', function () { } as any); expect(emePromise).to.be.a('Promise'); - if (!emePromise) { - return; - } return emePromise.finally(() => { expect(reqMediaKsAccessSpy).callCount(1); const args = reqMediaKsAccessSpy.getCall(0) @@ -416,13 +410,6 @@ describe('EMEController', function () { '00000000000000000000000000000000' ], ).to.be.a('Promise'); - if ( - !emeController.keyIdToKeySessionPromise[ - '00000000000000000000000000000000' - ] - ) { - return; - } return emeController.keyIdToKeySessionPromise[ '00000000000000000000000000000000' ].finally(() => { @@ -499,13 +486,6 @@ describe('EMEController', function () { '00000000000000000000000000000000' ], ).to.be.a('Promise'); - if ( - !emeController.keyIdToKeySessionPromise[ - '00000000000000000000000000000000' - ] - ) { - return; - } return emeController.keyIdToKeySessionPromise[ '00000000000000000000000000000000' ] @@ -582,13 +562,6 @@ describe('EMEController', function () { '00000000000000000000000000000000' ], ).to.be.a('Promise'); - if ( - !emeController.keyIdToKeySessionPromise[ - '00000000000000000000000000000000' - ] - ) { - return; - } return emeController.keyIdToKeySessionPromise[ '00000000000000000000000000000000' ] diff --git a/tests/unit/controller/fragment-tracker.ts b/tests/unit/controller/fragment-tracker.ts index d245ef6fb6c..8c0fdf28e7c 100644 --- a/tests/unit/controller/fragment-tracker.ts +++ b/tests/unit/controller/fragment-tracker.ts @@ -608,7 +608,7 @@ function createBufferAppendedData( frag: new Fragment(PlaylistLevelType.MAIN, ''), part: null, parent: PlaylistLevelType.MAIN, - type: audio && video ? 'audiovideo' : video ? 'video' : 'audio', + type: audio ? 'audiovideo' : 'video', timeRanges: { video: createMockBuffer(video), audio: createMockBuffer(audio || video), @@ -655,7 +655,7 @@ function createMockFragment( ): Fragment { const frag = new Fragment(data.type, ''); Object.assign(frag, data); - frag.start = data.startPTS; + frag.setStart(data.startPTS); frag.duration = data.endPTS - data.startPTS; types.forEach((t) => { frag.setElementaryStreamInfo( diff --git a/tests/unit/controller/interstitials-controller.ts b/tests/unit/controller/interstitials-controller.ts index d0ee1b86bbd..2db36842684 100644 --- a/tests/unit/controller/interstitials-controller.ts +++ b/tests/unit/controller/interstitials-controller.ts @@ -36,10 +36,7 @@ class HLSTestPlayer extends Hls { hlsTestable.coreComponents.forEach((component) => component.destroy()); hlsTestable.coreComponents.length = 0; hlsTestable.on(Events.MEDIA_ATTACHING, (t, data) => { - const media = data.media; - if (media) { - media.src = ''; - } + data.media.src = ''; }); hlsTestable.on(Events.MEDIA_DETACHING, () => { const media = hlsTestable.media; @@ -140,7 +137,7 @@ describe('InterstitialsController', function () { 0, null, ); - expect(details?.playlistParsingError).to.equal(null); + expect(details.playlistParsingError).to.equal(null); const attrs = new AttrList({}); const level = new Level({ name: '', @@ -210,9 +207,6 @@ fileSequence4.ts expect(insterstitials.playingIndex).to.equal(0, 'playingIndex'); expect(insterstitials.events).is.an('array').which.has.lengthOf(1); expect(schedule).is.an('array').which.has.lengthOf(2); - if (!insterstitials.events || !schedule) { - return; - } const interstitialEvent = insterstitials.events[0]; expect(interstitialEvent.identifier).to.equal('0'); expect(interstitialEvent.restrictions.jump).to.equal(true); @@ -296,11 +290,8 @@ fileSequence4.ts .is.an('array') .which.has.lengthOf( 7, - `Schedule items: ${schedule?.map((item) => `${item.start}-${item.end}`).join(', ')}`, + `Schedule items: ${schedule.map((item) => `${item.start}-${item.end}`).join(', ')}`, ); - if (!events || !schedule) { - return; - } const eventAssertions = [ { startTime: 0, @@ -478,13 +469,10 @@ fileSequence3.ts const events = insterstitials.events; const schedule = insterstitials.schedule; expect(events).is.an('array').which.has.lengthOf(5); - const scheduleDebugString = `Schedule items: ${schedule?.map((item) => `[${item.event ? 'I' : 'P'}:${item.start}-${item.end}]`).join(', ')}`; + const scheduleDebugString = `Schedule items: ${schedule.map((item) => `[${item.event ? 'I' : 'P'}:${item.start}-${item.end}]`).join(', ')}`; expect(schedule) .is.an('array') .which.has.lengthOf(9, scheduleDebugString); - if (!events || !schedule) { - return; - } [ 'primary', '1', @@ -498,12 +486,12 @@ fileSequence3.ts ].forEach((typeOrIdentifier, i) => { if (typeOrIdentifier === 'primary') { expect( - schedule?.[i], + schedule[i], `Expected to find a primary segment at index ${i}: ${scheduleDebugString}`, ).to.have.property('nextEvent'); } else { expect( - schedule?.[i], + schedule[i], `Expected to find an Interstitial at index ${i}: ${scheduleDebugString}`, ) .to.have.property('event') @@ -725,9 +713,6 @@ fileSequence3.mp4 expect(insterstitials.playingIndex).to.equal(0, 'playingIndex'); expect(insterstitials.events).is.an('array').which.has.lengthOf(2); expect(schedule).is.an('array').which.has.lengthOf(4); - if (!insterstitials.events || !schedule) { - return; - } expect(insterstitials.events[0].identifier).to.equal('ad1'); expect(insterstitials.events[1].identifier).to.equal('ad2'); expect(insterstitials.events[0]).to.equal(schedule[0].event); @@ -827,9 +812,6 @@ fileSequence3.mp4 const schedule = insterstitials.schedule; expect(insterstitials.events).is.an('array').which.has.lengthOf(2); expect(schedule).is.an('array').which.has.lengthOf(2); - if (!insterstitials.events || !schedule) { - return; - } expect(insterstitials.events[0].identifier).to.equal('ad1'); expect(insterstitials.events[1].identifier).to.equal('ad2'); expect(insterstitials.events[0]).to.equal(schedule[0].event); @@ -891,9 +873,6 @@ fileSequence4.ts const events = insterstitials.events; expect(events).is.an('array').which.has.lengthOf(1); expect(schedule).is.an('array').which.has.lengthOf(2); - if (!events || !schedule) { - return; - } expect(events[0]).to.deep.include({ identifier: '0', timelineOccupancy: TimelineOccupancy.Point, @@ -986,9 +965,6 @@ fileSequence4.ts const events = insterstitials.events; expect(events).is.an('array').which.has.lengthOf(4); expect(schedule).is.an('array').which.has.lengthOf(5); - if (!events || !schedule) { - return; - } eventAssertions.forEach((assertions, i) => { expect(events[i].identifier).to.equal('' + i); expect(events[i], `Interstitial Event "${i}"`).to.deep.include( @@ -1118,9 +1094,6 @@ fileSequence5.mp4`; } expect(insterstitials.events).is.an('array').which.has.lengthOf(2); expect(insterstitials.schedule).is.an('array').which.has.lengthOf(4); - if (!insterstitials.events || !insterstitials.schedule) { - return; - } const callsWithPrerollBeforeAttach = getTriggerCalls(); expect(callsWithPrerollBeforeAttach).to.deep.equal( [Events.LEVEL_UPDATED, Events.INTERSTITIALS_UPDATED], @@ -1185,9 +1158,6 @@ fileSequence3.mp4 } expect(insterstitials.events).is.an('array').which.has.lengthOf(1); expect(insterstitials.schedule).is.an('array').which.has.lengthOf(2); - if (!insterstitials.events || !insterstitials.schedule) { - return; - } const callsBeforeAttach = getTriggerCalls(); expect(callsBeforeAttach).to.deep.equal( [ @@ -1255,9 +1225,6 @@ fileSequence3.mp4 } expect(insterstitials.events).is.an('array').which.has.lengthOf(1); expect(insterstitials.schedule).is.an('array').which.has.lengthOf(2); - if (!insterstitials.events || !insterstitials.schedule) { - return; - } const callsBeforeAttach = getTriggerCalls(); expect(callsBeforeAttach).to.deep.equal( [ @@ -1333,9 +1300,6 @@ fileSequence6.mp4`; } expect(insterstitials.events).is.an('array').which.has.lengthOf(1); expect(insterstitials.schedule).is.an('array').which.has.lengthOf(3); - if (!insterstitials.events || !insterstitials.schedule) { - return; - } const eventsBeforeAttach = getTriggerCalls(); expect(eventsBeforeAttach).to.deep.equal( [Events.LEVEL_UPDATED, Events.INTERSTITIALS_UPDATED], @@ -1463,9 +1427,6 @@ fileSequence6.mp4`; } expect(insterstitials.events).is.an('array').which.has.lengthOf(1); expect(insterstitials.schedule).is.an('array').which.has.lengthOf(3); - if (!insterstitials.events || !insterstitials.schedule) { - return; - } const eventsAfterPlaylist = getTriggerCalls(); expect(eventsAfterPlaylist).to.deep.equal( [ @@ -1508,10 +1469,10 @@ fileSequence6.mp4`; insterstitials.skip(); const eventsAfterSkip = getTriggerCalls(); const expectedSkipEvents = [ - Events.INTERSTITIALS_BUFFERED_TO_BOUNDARY, Events.INTERSTITIAL_ASSET_ENDED, Events.INTERSTITIAL_ENDED, Events.INTERSTITIALS_UPDATED, // removed Interstitial with CUE="ONCE" + Events.INTERSTITIALS_BUFFERED_TO_BOUNDARY, Events.MEDIA_ATTACHING, Events.INTERSTITIALS_PRIMARY_RESUMED, ]; @@ -1607,17 +1568,13 @@ fileSequence6.mp4 duration: 15, }); } else if (bufferingIndex === 2) { - // buffered to primary (end of interstitial) + // buffered to primary (interstitial ended) expectIm(primary, e).to.include({ currentTime: 40, bufferedEnd: 40 }); expectIm(integrated, e).to.include({ currentTime: 45, bufferedEnd: 45, }); - expectIm(interstitialPlayer, e).to.include({ - playingIndex: 2, - currentTime: 15, - duration: 15, - }); + expectIm(interstitialPlayer, e).to.be.null; } }); hls.on(Events.INTERSTITIAL_STARTED, (t) => { @@ -1799,9 +1756,6 @@ fileSequence6.mp4 expect(im.events).is.an('array').which.has.lengthOf(1); expect(im.schedule).is.an('array').which.has.lengthOf(3); - if (!im.events || !im.schedule) { - return; - } const eventsBeforeAttach = getTriggerCalls(); expect(eventsBeforeAttach).to.deep.equal( [Events.LEVEL_UPDATED, Events.INTERSTITIALS_UPDATED], @@ -1956,9 +1910,9 @@ fileSequence6.mp4 }); const eventsAfterLastAsset = getTriggerCalls(); const expectedEndLastAssetEvents = [ - Events.INTERSTITIALS_BUFFERED_TO_BOUNDARY, Events.INTERSTITIAL_ASSET_ENDED, Events.INTERSTITIAL_ENDED, + Events.INTERSTITIALS_BUFFERED_TO_BOUNDARY, Events.MEDIA_ATTACHING, Events.INTERSTITIALS_PRIMARY_RESUMED, ]; @@ -2008,9 +1962,6 @@ fileSequence6.mp4`; } expect(insterstitials.events).is.an('array').which.has.lengthOf(1); expect(insterstitials.schedule).is.an('array').which.has.lengthOf(3); - if (!insterstitials.events || !insterstitials.schedule) { - return; - } // Capture asset-list request const loadSpy = sandbox.spy(hls.config.loader.prototype, 'load'); diff --git a/tests/unit/controller/level-controller.ts b/tests/unit/controller/level-controller.ts index 232f1928bc6..0a2b1e660b2 100755 --- a/tests/unit/controller/level-controller.ts +++ b/tests/unit/controller/level-controller.ts @@ -10,6 +10,7 @@ import { PlaylistLevelType } from '../../../src/types/loader'; import { AttrList } from '../../../src/utils/attr-list'; import { getMediaSource } from '../../../src/utils/mediasource-helper'; import HlsMock from '../../mocks/hls.mock'; +import { parsedLevel } from '../utils/mock-level'; import type { Fragment } from '../../../src/loader/fragment'; import type { LevelDetails } from '../../../src/loader/level-details'; import type { ParsedMultivariantPlaylist } from '../../../src/loader/m3u8-parser'; @@ -17,7 +18,6 @@ import type { ManifestLoadedData, ManifestParsedData, } from '../../../src/types/events'; -import type { LevelParsed } from '../../../src/types/level'; import type { PlaylistLoaderContext } from '../../../src/types/loader'; import type { MediaAttributes, @@ -49,18 +49,6 @@ type LevelControllerTestable = Omit & { redundantFailover: (levelIndex: number) => void; }; -function parsedLevel( - options: Partial & { bitrate: number }, -): LevelParsed { - const level: LevelParsed = { - attrs: new AttrList({ BANDWIDTH: options.bitrate }), - bitrate: options.bitrate, - name: '', - url: `${options.bitrate}.m3u8`, - }; - return Object.assign(level, options); -} - function mediaPlaylist(options: Partial): MediaPlaylist { const track: MediaPlaylist = { attrs: new AttrList({}) as MediaAttributes, diff --git a/tests/unit/controller/level-helper.ts b/tests/unit/controller/level-helper.ts index da2973f48ad..e788e4b0571 100644 --- a/tests/unit/controller/level-helper.ts +++ b/tests/unit/controller/level-helper.ts @@ -48,7 +48,7 @@ const generatePlaylist = (sequenceNumbers, offset = 0, duration = 5) => { playlist.fragments = sequenceNumbers.map((n, i) => { const frag = new Fragment(PlaylistLevelType.MAIN, ''); frag.sn = n; - frag.start = i * 5 + offset; + frag.setStart(i * 5 + offset); frag.duration = duration; return frag; }); @@ -67,7 +67,9 @@ const getIteratedSequence = (oldPlaylist, newPlaylist) => { }; const getFragmentSequenceNumbers = (details: LevelDetails) => - details.fragments.map((f) => `${f?.sn}-${f?.cc}`).join(','); + details.fragments + .map((f: MediaFragment | null) => `${f?.sn}-${f?.cc}`) + .join(','); describe('LevelHelper Tests', function () { let sandbox; @@ -204,7 +206,7 @@ describe('LevelHelper Tests', function () { it('matches start when the new playlist starts before the old', function () { const oldPlaylist = generatePlaylist([3, 4, 5]); oldPlaylist.fragments.forEach((f) => { - f.start += 10; + f.addStart(10); }); const newPlaylist = generatePlaylist([1, 2, 3]); mergeDetails(oldPlaylist, newPlaylist); @@ -374,12 +376,12 @@ fileSequence11.ts .which.equals(details.fragments[2].ref) .which.has.property('sn') .which.equals(5); - expect(details.dateRanges.one.startTime).to.equal(4); - expect(details.dateRanges.two.startTime).to.equal(10); - expect(details.dateRanges.three.startTime).to.equal(16); - expect(details.dateRanges.one.tagOrder, 'one.tagOrder').to.equal(0); - expect(details.dateRanges.two.tagOrder, 'two.tagOrder').to.equal(1); - expect(details.dateRanges.three.tagOrder, 'three.tagOrder').to.equal(2); + expect(details.dateRanges.one!.startTime).to.equal(4); + expect(details.dateRanges.two!.startTime).to.equal(10); + expect(details.dateRanges.three!.startTime).to.equal(16); + expect(details.dateRanges.one!.tagOrder, 'one.tagOrder').to.equal(0); + expect(details.dateRanges.two!.tagOrder, 'two.tagOrder').to.equal(1); + expect(details.dateRanges.three!.tagOrder, 'three.tagOrder').to.equal(2); expect( detailsUpdated.hasProgramDateTime, 'detailsUpdated.hasProgramDateTime', @@ -407,25 +409,26 @@ fileSequence11.ts .which.has.property('tagAnchor') .which.has.property('sn') .which.equals(5); - expect(detailsUpdated.dateRanges.one.startTime).to.equal(4); - expect(detailsUpdated.dateRanges.two.startTime).to.equal(10); - expect(detailsUpdated.dateRanges.three.startTime).to.equal(16); - expect(detailsUpdated.dateRanges.four.startTime).to.equal(76); + expect(detailsUpdated.dateRanges.one!.startTime).to.equal(4); + expect(detailsUpdated.dateRanges.two!.startTime).to.equal(10); + expect(detailsUpdated.dateRanges.three!.startTime).to.equal(16); + expect(detailsUpdated.dateRanges.four!.startTime).to.equal(76); expect( - detailsUpdated.dateRanges.one.tagOrder, + detailsUpdated.dateRanges.one!.tagOrder, 'one.tagOrder updated', ).to.equal(0); expect( - detailsUpdated.dateRanges.two.tagOrder, + detailsUpdated.dateRanges.two!.tagOrder, 'two.tagOrder updated', ).to.equal(1); expect( - detailsUpdated.dateRanges.three.tagOrder, + detailsUpdated.dateRanges.three!.tagOrder, 'three.tagOrder updated', ).to.equal(2); - expect(detailsUpdated.dateRanges.four.tagOrder, 'four.tagOrder').to.equal( - 3, - ); + expect( + detailsUpdated.dateRanges.four!.tagOrder, + 'four.tagOrder', + ).to.equal(3); expect(detailsUpdated.playlistParsingError).to.be.null; }); @@ -1096,8 +1099,8 @@ fileSequence18.ts`; Object.keys(details.dateRanges), 'first playlist daterange ids', ).to.have.deep.equal(['d0', 'd1', 'd2', 'd3', 'd4']); - expect(details.dateRanges.d2.startTime).to.equal(2.94); - expect(details.dateRanges.d3.startTime).to.equal(3.94); + expect(details.dateRanges.d2!.startTime).to.equal(2.94); + expect(details.dateRanges.d3!.startTime).to.equal(3.94); expect( detailsUpdated.hasProgramDateTime, 'detailsUpdated.hasProgramDateTime', @@ -1116,8 +1119,8 @@ fileSequence18.ts`; .which.equals(detailsUpdated.fragments[0].ref) .which.has.property('sn') .which.equals(3); - expect(detailsUpdated.dateRanges.d2.startTime).to.equal(2.94); - expect(detailsUpdated.dateRanges.d3.startTime).to.equal(3.94); + expect(detailsUpdated.dateRanges.d2!.startTime).to.equal(2.94); + expect(detailsUpdated.dateRanges.d3!.startTime).to.equal(3.94); // Multiple #EXT-X-SKIP tags are not allowed expect(detailsUpdated.playlistParsingError).to.include({ message: `#EXT-X-SKIP must not appear more than once (#EXT-X-SKIP:SKIPPED-SEGMENTS=2,RECENTLY-REMOVED-DATERANGES="d0")`, diff --git a/tests/unit/controller/stream-controller.ts b/tests/unit/controller/stream-controller.ts index d645a0648aa..8bee6813a4c 100644 --- a/tests/unit/controller/stream-controller.ts +++ b/tests/unit/controller/stream-controller.ts @@ -210,7 +210,7 @@ describe('StreamController', function () { fragPrevious.programDateTime = 1505502671523; fragPrevious.duration = 5.0; fragPrevious.level = 1; - fragPrevious.start = 10.0; + fragPrevious.setStart(10.0); fragPrevious.sn = 2; // Fragment with PDT 1505502671523 in level 1 does not have the same sn as in level 2 where cc is 1 fragPrevious.cc = 0; @@ -274,7 +274,7 @@ describe('StreamController', function () { fragPrevious.programDateTime = 1505502681523; fragPrevious.duration = 5.0; fragPrevious.level = 1; - fragPrevious.start = 15.0; + fragPrevious.setStart(15.0); fragPrevious.sn = 3; streamController['fragPrevious'] = fragPrevious; @@ -475,7 +475,7 @@ describe('StreamController', function () { ) as MediaFragment; firstFrag.duration = 5.0; firstFrag.level = 1; - firstFrag.start = 0; + firstFrag.setStart(0); firstFrag.sn = 1; firstFrag.cc = 0; firstFrag.elementaryStreams.video = { diff --git a/tests/unit/hls.ts b/tests/unit/hls.ts index ae64e5af31b..cf9779e5091 100644 --- a/tests/unit/hls.ts +++ b/tests/unit/hls.ts @@ -57,7 +57,7 @@ describe('Hls', function () { const hls = new Hls({ capLevelOnFPSDrop: true }); expect(hls.media).to.equal(null); const media = document.createElement('video'); - expect(media || null).to.not.equal(null); + expect(media).to.be.an('HTMLVideoElement'); hls.attachMedia(media); expect(hls.media).to.equal(media); detachTest(hls, media, 6); @@ -72,7 +72,7 @@ describe('Hls', function () { }); expect(hls.media).to.equal(null); const media = document.createElement('video'); - expect(media || null).to.not.equal(null); + expect(media).to.be.an('HTMLVideoElement'); hls.attachMedia(media); expect(hls.media).to.equal(media); hls.trigger(Events.MEDIA_ATTACHED, { media }); diff --git a/tests/unit/loader/m3u8-parser.ts b/tests/unit/loader/m3u8-parser.ts index e41bfc7d0ee..1549f308ce7 100644 --- a/tests/unit/loader/m3u8-parser.ts +++ b/tests/unit/loader/m3u8-parser.ts @@ -1996,10 +1996,10 @@ segment.m4s ); expect(details.playlistParsingError).to.be.null; expect(details.dateRangeTagCount).to.equal(4); - expect(details.dateRanges.pre.isInterstitial).to.be.true; - expect(details.dateRanges.mid1.isInterstitial).to.be.true; - expect(details.dateRanges.mid2.isInterstitial).to.be.true; - expect(details.dateRanges.post.isInterstitial).to.be.true; + expect(details.dateRanges.pre!.isInterstitial).to.be.true; + expect(details.dateRanges.mid1!.isInterstitial).to.be.true; + expect(details.dateRanges.mid2!.isInterstitial).to.be.true; + expect(details.dateRanges.post!.isInterstitial).to.be.true; expect(details.dateRanges).to.have.property('pre').which.deep.includes({ tagOrder: 0, }); @@ -2012,15 +2012,15 @@ segment.m4s expect(details.dateRanges).to.have.property('post').which.deep.includes({ tagOrder: 3, }); - expect(details.dateRanges.pre.cue.pre).to.be.true; - expect(details.dateRanges.mid1.cue.once).to.be.true; - expect(details.dateRanges.post.cue.post).to.be.true; - expect(details.dateRanges.post.cue.once).to.be.true; + expect(details.dateRanges.pre!.cue.pre).to.be.true; + expect(details.dateRanges.mid1!.cue.once).to.be.true; + expect(details.dateRanges.post!.cue.post).to.be.true; + expect(details.dateRanges.post!.cue.once).to.be.true; // DateRange start times are mapped to the primary timeline and not changed by CUE Interstitial DURATION - expect(details.dateRanges.pre.startTime).to.equal(-7200); - expect(details.dateRanges.mid1.startTime).to.equal(10); - expect(details.dateRanges.mid2.startTime).to.equal(25); - expect(details.dateRanges.post.startTime).to.equal(0); + expect(details.dateRanges.pre!.startTime).to.equal(-7200); + expect(details.dateRanges.mid1!.startTime).to.equal(10); + expect(details.dateRanges.mid2!.startTime).to.equal(25); + expect(details.dateRanges.post!.startTime).to.equal(0); }); it('ensures DateRanges are mapped to a segment whose TimeRange covers the start date of the DATERANGE tag', function () { @@ -2047,14 +2047,14 @@ segment.m4s null, ); expect(details.playlistParsingError).to.be.null; - expect(details.dateRanges.sooner.isValid).to.equal( + expect(details.dateRanges.sooner!.isValid).to.equal( true, 'is valid DateRange', ); - expect(details.dateRanges.sooner.tagAnchor) + expect(details.dateRanges.sooner!.tagAnchor) .to.have.property('sn') .which.equals(2); - expect(details.dateRanges.sooner.startTime).to.equal(10); + expect(details.dateRanges.sooner!.startTime).to.equal(10); }); it('ensures DateRanges that start before the program are mapped to the first PDT tag', function () { @@ -2081,14 +2081,14 @@ segment.m4s null, ); expect(details.playlistParsingError).to.be.null; - expect(details.dateRanges.earlier.isValid).to.equal( + expect(details.dateRanges.earlier!.isValid).to.equal( true, 'is valid DateRange', ); - expect(details.dateRanges.earlier.tagAnchor) + expect(details.dateRanges.earlier!.tagAnchor) .to.have.property('sn') .which.equals(1); - expect(details.dateRanges.earlier.startTime).to.equal(-10); + expect(details.dateRanges.earlier!.startTime).to.equal(-10); }); it('adds PROGRAM-DATE-TIME and DATERANGE tag text to fragment[].tagList for backwards compatibility', function () { @@ -2758,10 +2758,6 @@ a{$mvpVariable}.mp4 TYPE: 'PART', URI: 'part-5.1.mp4', }); - if (details.partList === null) { - expect(details.partList, 'partList').to.not.equal(null); - return; - } if (!details.renditionReports) { expect(details.renditionReports, 'renditionReports').to.not.be .undefined; @@ -2913,7 +2909,7 @@ a{$bar}.mp4 details, 'Missing preceding EXT-X-DEFINE tag for Variable Reference: "bar"', ); - expect(details.fragments?.[0].relurl).to.equal('a{$bar}.mp4'); + expect(details.fragments[0].relurl).to.equal('a{$bar}.mp4'); }); it('fails to parse Media Playlist when variable reference precedes definition', function () { diff --git a/tests/unit/utils/mediacapabilities-helper.ts b/tests/unit/utils/mediacapabilities-helper.ts index 445c554a59e..c7518b24201 100644 --- a/tests/unit/utils/mediacapabilities-helper.ts +++ b/tests/unit/utils/mediacapabilities-helper.ts @@ -7,6 +7,11 @@ import type { MediaPlaylist, } from '../../../src/types/media-playlist'; +declare const navigator: { + prototype: Navigator; + mediaCapabilities?: MediaCapabilities; +}; + describe('getMediaDecodingInfoPromise', function () { it('adds queries to cache', function () { if (!navigator.mediaCapabilities) { diff --git a/tests/unit/utils/mock-level.ts b/tests/unit/utils/mock-level.ts new file mode 100644 index 00000000000..2b2fd421ef7 --- /dev/null +++ b/tests/unit/utils/mock-level.ts @@ -0,0 +1,15 @@ +import { AttrList } from '../../../src/utils/attr-list'; +import type { LevelParsed } from '../../../src/types/level'; + +export function parsedLevel( + options: Partial & { bitrate: number }, +): LevelParsed { + const { bitrate, height } = options; + const level: LevelParsed = { + attrs: new AttrList({ BANDWIDTH: bitrate }), + bitrate, + name: `${height}-${bitrate}`, + url: `${bitrate}.m3u8`, + }; + return Object.assign(level, options); +} diff --git a/tools/mp4-inspect.js b/tools/mp4-inspect.js index 4dad9167e6f..9672cc8654f 100644 --- a/tools/mp4-inspect.js +++ b/tools/mp4-inspect.js @@ -663,7 +663,8 @@ var mp4toJSON = function (data) { while (i < data.byteLength) { // parse box data - (size = view.getUint32(i)), (type = parseType(data.subarray(i + 4, i + 8))); + ((size = view.getUint32(i)), + (type = parseType(data.subarray(i + 4, i + 8)))); end = size > 1 ? i + size : data.byteLength; // parse type-specific data