From 7361d0288c7024afb5993a82a33578bd208f6fb7 Mon Sep 17 00:00:00 2001 From: Thomas Jammet Date: Wed, 6 May 2026 18:55:21 +0200 Subject: [PATCH 1/8] feat(Util): add an helper to convert string or array to a normalized array --- src/Util.spec.ts | 22 ++++++++++++++++++++++ src/Util.ts | 17 +++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/Util.spec.ts b/src/Util.spec.ts index 0365fcc..0b1a6b1 100644 --- a/src/Util.spec.ts +++ b/src/Util.spec.ts @@ -60,6 +60,28 @@ describe('Util', () => { }); }); + describe('normalizeStringArray', () => { + it('should normalize a string into an array', () => { + expect(Util.normalizeStringArray('value')).toEqual(['value']); + }); + + it('should keep non-empty values from an array', () => { + expect(Util.normalizeStringArray(['value1', '', 'value2'])).toEqual(['value1', 'value2']); + }); + + it('should return an empty string fallback by default when no values are provided', () => { + expect(Util.normalizeStringArray()).toEqual(['']); + expect(Util.normalizeStringArray('')).toEqual(['']); + expect(Util.normalizeStringArray([''])).toEqual(['']); + }); + + it('should omit the empty string fallback when includeEmpty is false', () => { + expect(Util.normalizeStringArray(undefined, false)).toEqual([]); + expect(Util.normalizeStringArray('', false)).toEqual([]); + expect(Util.normalizeStringArray([''], false)).toEqual([]); + }); + }); + describe('toBin', () => { it('should convert string to UTF-8 representation in Uint8Array', () => { const str = 'Hello 😭'; diff --git a/src/Util.ts b/src/Util.ts index 2b925c1..67c22c1 100644 --- a/src/Util.ts +++ b/src/Util.ts @@ -22,6 +22,23 @@ const _perf = performance; // to increase x10 now performance! */ export const EMPTY_FUNCTION = () => {}; +/** + * Converts a string or an array of strings into a normalized string array. + * Empty string values are filtered out. When no value remains, an optional empty-string + * fallback can be returned to simplify cartesian-product style expansions. + * + * @param value The string or array of strings to normalize. + * @param includeEmpty If true, returns `['']` when no non-empty values are provided. + * @returns A normalized array of strings. + */ +export function normalizeStringArray(value?: string | string[], includeEmpty: boolean = true): string[] { + if (!value) { + return includeEmpty ? [''] : []; + } + const values = Array.isArray(value) ? value.filter(Boolean) : [value]; + return values.length ? values : includeEmpty ? [''] : []; +} + /** * Returns an efficient timestamp in milliseconds elapsed since performance.timeOrigin, * representing the start of the current JavaScript execution context. From e4d649656a5289e03d53ab210a26f16d56f7d97f Mon Sep 17 00:00:00 2001 From: Thomas Jammet Date: Wed, 6 May 2026 19:00:48 +0200 Subject: [PATCH 2/8] feat(Connect): support DRM template configurations - add structured DRM request and certificate config types - extend `KeySystem` with `templateConfigurations` - add `createTemplateConfigurations()` helper for DRM capability templates - support robustness-only templates intended to be completed from metadata - add utility and tests for template generation --- src/Connect.spec.ts | 107 ++++++++++++++++++++++++++++++++++- src/Connect.ts | 134 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 222 insertions(+), 19 deletions(-) diff --git a/src/Connect.spec.ts b/src/Connect.spec.ts index fc78203..3fc2f64 100644 --- a/src/Connect.spec.ts +++ b/src/Connect.spec.ts @@ -5,7 +5,7 @@ */ import { describe, it, expect } from 'vitest'; -import { Type, Params, buildURL, defineMediaExt } from './Connect'; +import { Type, Params, buildURL, createTemplateConfigurations, defineMediaExt } from './Connect'; describe('Connect', () => { describe('defineMediaExt', () => { @@ -197,4 +197,109 @@ describe('Connect', () => { expect(() => buildURL(Type.HESP, params)).toThrow(Error); }); }); + + describe('createTemplateConfigurations', () => { + it('should preserve the KeySystem string shorthand in Params', () => { + const params: Params = { + endPoint: 'example.com', + contentProtection: { + 'com.microsoft.playready': 'https://license.example.com' + } + }; + + expect(params.contentProtection?.['com.microsoft.playready']).toBe('https://license.example.com'); + }); + + it('should build the cartesian product of content types and robustness values', () => { + const configurations = createTemplateConfigurations({ + audioContentTypes: ['audio/mp4; codecs="mp4a.40.2"'], + videoContentTypes: ['video/mp4; codecs="avc1.640028"', 'video/mp4; codecs="hvc1.1.6.L93.B0"'], + audioRobustness: ['SW_SECURE_CRYPTO', 'HW_SECURE_CRYPTO'], + videoRobustness: ['SW_SECURE_DECODE', 'HW_SECURE_DECODE'] + }); + + expect(configurations).toHaveLength(8); + expect(configurations[0]).toEqual({ + audioCapabilities: [{ contentType: 'audio/mp4; codecs="mp4a.40.2"', robustness: 'SW_SECURE_CRYPTO' }], + videoCapabilities: [{ contentType: 'video/mp4; codecs="avc1.640028"', robustness: 'SW_SECURE_DECODE' }] + }); + expect(configurations[7]).toEqual({ + audioCapabilities: [{ contentType: 'audio/mp4; codecs="mp4a.40.2"', robustness: 'HW_SECURE_CRYPTO' }], + videoCapabilities: [ + { contentType: 'video/mp4; codecs="hvc1.1.6.L93.B0"', robustness: 'HW_SECURE_DECODE' } + ] + }); + }); + + it('should omit empty robustness values', () => { + const configurations = createTemplateConfigurations({ + audioContentTypes: 'audio/mp4; codecs="mp4a.40.2"', + videoContentTypes: 'video/mp4; codecs="avc1.640028"' + }); + + expect(configurations).toEqual([ + { + audioCapabilities: [{ contentType: 'audio/mp4; codecs="mp4a.40.2"' }], + videoCapabilities: [{ contentType: 'video/mp4; codecs="avc1.640028"' }] + } + ]); + }); + + it('should not duplicate video-only configurations when audio robustness is provided', () => { + const configurations = createTemplateConfigurations({ + videoContentTypes: 'video/mp4; codecs="avc1.640028"', + audioRobustness: ['SW_SECURE_CRYPTO', 'HW_SECURE_CRYPTO'], + videoRobustness: ['SW_SECURE_DECODE', 'HW_SECURE_DECODE'] + }); + + expect(configurations).toEqual([ + { + videoCapabilities: [ + { contentType: 'video/mp4; codecs="avc1.640028"', robustness: 'SW_SECURE_DECODE' } + ] + }, + { + videoCapabilities: [ + { contentType: 'video/mp4; codecs="avc1.640028"', robustness: 'HW_SECURE_DECODE' } + ] + } + ]); + }); + + it('should create unresolved capability templates from robustness-only inputs', () => { + const configurations = createTemplateConfigurations({ + audioRobustness: ['A1', 'A2'], + videoRobustness: ['V1', 'V2'] + }); + + expect(configurations).toEqual([ + { + audioCapabilities: [{ robustness: 'A1' }, { robustness: 'A2' }], + videoCapabilities: [{ robustness: 'V1' }, { robustness: 'V2' }] + } + ]); + }); + + it('should keep the base configuration on each generated entry', () => { + const configurations = createTemplateConfigurations({ + audioContentTypes: 'audio/mp4; codecs="mp4a.40.2"', + baseConfiguration: { + initDataTypes: ['cenc'], + distinctiveIdentifier: 'optional', + persistentState: 'optional', + sessionTypes: ['temporary'] + } + }); + + expect(configurations).toEqual([ + { + initDataTypes: ['cenc'], + distinctiveIdentifier: 'optional', + persistentState: 'optional', + sessionTypes: ['temporary'], + audioCapabilities: [{ contentType: 'audio/mp4; codecs="mp4a.40.2"' }] + } + ]); + }); + }); }); diff --git a/src/Connect.ts b/src/Connect.ts index 2b76ec4..dccc48f 100644 --- a/src/Connect.ts +++ b/src/Connect.ts @@ -7,6 +7,112 @@ import * as Util from './Util'; import { NetAddress } from './NetAddress'; import { log } from './Log'; +export type TemplateConfigurationsParams = { + audioContentTypes?: string | string[]; + videoContentTypes?: string | string[]; + audioRobustness?: string | string[]; + videoRobustness?: string | string[]; + baseConfiguration?: MediaKeySystemConfiguration; +}; + +/** + * Helper to create DRM configuration templates from common audio/video content types and robustness values. + * It generates all combinations of audio and video content types and robustness values, and merges them with + * the base configuration if provided. + * + * The returned configurations can be assigned to {@link KeySystem.templateConfigurations}. When the caller + * omits capability content types, a DRM implementation may enrich those templates with stream metadata before + * passing the final configurations to `requestMediaKeySystemAccess()`. + * + * @returns An array of MediaKeySystemConfiguration templates. + */ +export function createTemplateConfigurations(params: TemplateConfigurationsParams): MediaKeySystemConfiguration[] { + const audioContentTypes = Util.normalizeStringArray(params.audioContentTypes, false); + const videoContentTypes = Util.normalizeStringArray(params.videoContentTypes, false); + const requestedAudioRobustness = Util.normalizeStringArray(params.audioRobustness, false); + const requestedVideoRobustness = Util.normalizeStringArray(params.videoRobustness, false); + + // If no content types are provided, we create a single configuration with robustness values only + if ( + !audioContentTypes.length && + !videoContentTypes.length && + (requestedAudioRobustness.length > 0 || requestedVideoRobustness.length > 0) + ) { + return [ + { + ...params.baseConfiguration, + ...(requestedAudioRobustness.length > 0 && { + audioCapabilities: (requestedAudioRobustness.length ? requestedAudioRobustness : ['']).map( + robustness => ({ + ...(robustness && { robustness }) + }) + ) + }), + ...(requestedVideoRobustness.length > 0 && { + videoCapabilities: (requestedVideoRobustness.length ? requestedVideoRobustness : ['']).map( + robustness => ({ + ...(robustness && { robustness }) + }) + ) + }) + } + ]; + } + + const configurations: MediaKeySystemConfiguration[] = []; + const audioRobustness = audioContentTypes.length ? Util.normalizeStringArray(params.audioRobustness) : ['']; + const videoRobustness = videoContentTypes.length ? Util.normalizeStringArray(params.videoRobustness) : ['']; + const audioInputs = audioContentTypes.length ? audioContentTypes : ['']; + const videoInputs = videoContentTypes.length ? videoContentTypes : ['']; + + // Create a complete configuration for each combination of audio/video content types and robustness values + for (const audioContentType of audioInputs) { + for (const videoContentType of videoInputs) { + for (const audioR of audioRobustness) { + for (const videoR of videoRobustness) { + const audioCapabilities = audioContentType + ? [{ contentType: audioContentType, ...(audioR && { robustness: audioR }) }] + : undefined; + const videoCapabilities = videoContentType + ? [{ contentType: videoContentType, ...(videoR && { robustness: videoR }) }] + : undefined; + + const config: MediaKeySystemConfiguration = { + ...params.baseConfiguration, + ...(audioCapabilities && { audioCapabilities }), + ...(videoCapabilities && { videoCapabilities }) + }; + configurations.push(config); + } + } + } + } + + return configurations.length ? configurations : [{ ...params.baseConfiguration }]; +} + +export type DRMRequestConfig = + | string + | { + url: string; + /** + * The additional HTTP headers to send to the license server + */ + headers?: Record; + }; + +export type DRMCertificateConfig = + | string + | Uint8Array + | { + url?: string; + data?: Uint8Array; + /** + * The additional HTTP headers to send to the certificate server + */ + headers?: Record; + }; + /** * Parameters of a key system for encrypted streams (DRM) * @@ -18,31 +124,23 @@ export type KeySystem = | string | { /** - * The license server URL + * The license URL or configuration for the key system. If it's a string, it's the URL of the license server. */ - licenseUrl: string; + license?: DRMRequestConfig; /** - * The certificate URL if needed (for FairPlay) - * - * Or directly the certificate + * The certificate URL if needed (for FairPlay) or the certificate data as Uint8Array. */ - certificate?: string | Uint8Array; + certificate?: DRMCertificateConfig; /** - * The additional HTTP headers to send to the license server - */ - headers?: Record; - /** - * Audio robustness level + * Optional MediaKeySystemConfiguration templates. * - * A list of robustness levels, prioritized by the order of the array. - */ - audioRobustness?: string[]; - /** - * Video robustness level + * If metadata is available, a DRM implementation may enrich capabilities that do not define `contentType` + * before calling `requestMediaKeySystemAccess()`. Explicit `contentType` values provided by the user should + * take precedence over metadata-derived values. * - * A list of robustness levels, prioritized by the order of the array. + * If metadata is not available, these configurations are expected to be complete enough to be used as-is. */ - videoRobustness?: string[]; + templateConfigurations?: MediaKeySystemConfiguration[]; }; /** From 48b668ee308ff4b75fac4e3cad3041672166f8b1 Mon Sep 17 00:00:00 2001 From: Thomas Jammet Date: Wed, 6 May 2026 22:15:00 +0200 Subject: [PATCH 3/8] ci(vitest): add text rendering to coverage command So we can watch the coverage in the terminal without having to open the lcov report in a browser. --- vitest.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vitest.config.ts b/vitest.config.ts index 7eaacb7..686abfe 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,8 +10,9 @@ export default defineConfig({ globals: true, environment: 'jsdom', coverage: { + include: ['src/**/*.ts'], provider: 'istanbul', - reporter: ['lcov'], + reporter: ['text', 'lcov'], reportsDirectory: './coverage', reportOnFailure: true }, From 116cbb18efcbcbaaefc8db56ee09a87897bd9ad9 Mon Sep 17 00:00:00 2001 From: Thomas Jammet Date: Wed, 6 May 2026 22:16:49 +0200 Subject: [PATCH 4/8] chore(Connect): clean up createTemplateConfigurations() And also add a missing test case --- src/Connect.spec.ts | 10 ++++++++++ src/Connect.ts | 29 ++++++++++++----------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/Connect.spec.ts b/src/Connect.spec.ts index 3fc2f64..3aea943 100644 --- a/src/Connect.spec.ts +++ b/src/Connect.spec.ts @@ -301,5 +301,15 @@ describe('Connect', () => { } ]); }); + + it('should return the base configuration when no template inputs are provided', () => { + const configurations = createTemplateConfigurations({ + baseConfiguration: { + persistentState: 'optional' + } + }); + + expect(configurations).toEqual([{ persistentState: 'optional' }]); + }); }); }); diff --git a/src/Connect.ts b/src/Connect.ts index dccc48f..68fcb52 100644 --- a/src/Connect.ts +++ b/src/Connect.ts @@ -32,28 +32,23 @@ export function createTemplateConfigurations(params: TemplateConfigurationsParam const requestedAudioRobustness = Util.normalizeStringArray(params.audioRobustness, false); const requestedVideoRobustness = Util.normalizeStringArray(params.videoRobustness, false); - // If no content types are provided, we create a single configuration with robustness values only - if ( - !audioContentTypes.length && - !videoContentTypes.length && - (requestedAudioRobustness.length > 0 || requestedVideoRobustness.length > 0) - ) { + if (!audioContentTypes.length && !videoContentTypes.length) { + if (!requestedAudioRobustness.length && !requestedVideoRobustness.length) { + return params.baseConfiguration ? [{ ...params.baseConfiguration }] : [{}]; + } + // If no content types are provided, we create a single configuration with robustness values only return [ { ...params.baseConfiguration, ...(requestedAudioRobustness.length > 0 && { - audioCapabilities: (requestedAudioRobustness.length ? requestedAudioRobustness : ['']).map( - robustness => ({ - ...(robustness && { robustness }) - }) - ) + audioCapabilities: requestedAudioRobustness.map(robustness => ({ + ...(robustness && { robustness }) + })) }), ...(requestedVideoRobustness.length > 0 && { - videoCapabilities: (requestedVideoRobustness.length ? requestedVideoRobustness : ['']).map( - robustness => ({ - ...(robustness && { robustness }) - }) - ) + videoCapabilities: requestedVideoRobustness.map(robustness => ({ + ...(robustness && { robustness }) + })) }) } ]; @@ -88,7 +83,7 @@ export function createTemplateConfigurations(params: TemplateConfigurationsParam } } - return configurations.length ? configurations : [{ ...params.baseConfiguration }]; + return configurations; } export type DRMRequestConfig = From d49e1aedce6ef42d639eac14f9b7f224a3a188b1 Mon Sep 17 00:00:00 2001 From: Thomas Jammet Date: Thu, 7 May 2026 22:02:02 +0200 Subject: [PATCH 5/8] chore(Connect): improve naming of types and functions --- src/Connect.spec.ts | 27 ++++++------------- src/Connect.ts | 64 +++++++++++++++++++++++++++++---------------- 2 files changed, 49 insertions(+), 42 deletions(-) diff --git a/src/Connect.spec.ts b/src/Connect.spec.ts index 3aea943..1294c34 100644 --- a/src/Connect.spec.ts +++ b/src/Connect.spec.ts @@ -5,7 +5,7 @@ */ import { describe, it, expect } from 'vitest'; -import { Type, Params, buildURL, createTemplateConfigurations, defineMediaExt } from './Connect'; +import { Type, Params, buildURL, createMediaKeySystemConfigurations, defineMediaExt } from './Connect'; describe('Connect', () => { describe('defineMediaExt', () => { @@ -198,20 +198,9 @@ describe('Connect', () => { }); }); - describe('createTemplateConfigurations', () => { - it('should preserve the KeySystem string shorthand in Params', () => { - const params: Params = { - endPoint: 'example.com', - contentProtection: { - 'com.microsoft.playready': 'https://license.example.com' - } - }; - - expect(params.contentProtection?.['com.microsoft.playready']).toBe('https://license.example.com'); - }); - + describe('createMediaKeySystemConfigurations', () => { it('should build the cartesian product of content types and robustness values', () => { - const configurations = createTemplateConfigurations({ + const configurations = createMediaKeySystemConfigurations({ audioContentTypes: ['audio/mp4; codecs="mp4a.40.2"'], videoContentTypes: ['video/mp4; codecs="avc1.640028"', 'video/mp4; codecs="hvc1.1.6.L93.B0"'], audioRobustness: ['SW_SECURE_CRYPTO', 'HW_SECURE_CRYPTO'], @@ -232,7 +221,7 @@ describe('Connect', () => { }); it('should omit empty robustness values', () => { - const configurations = createTemplateConfigurations({ + const configurations = createMediaKeySystemConfigurations({ audioContentTypes: 'audio/mp4; codecs="mp4a.40.2"', videoContentTypes: 'video/mp4; codecs="avc1.640028"' }); @@ -246,7 +235,7 @@ describe('Connect', () => { }); it('should not duplicate video-only configurations when audio robustness is provided', () => { - const configurations = createTemplateConfigurations({ + const configurations = createMediaKeySystemConfigurations({ videoContentTypes: 'video/mp4; codecs="avc1.640028"', audioRobustness: ['SW_SECURE_CRYPTO', 'HW_SECURE_CRYPTO'], videoRobustness: ['SW_SECURE_DECODE', 'HW_SECURE_DECODE'] @@ -267,7 +256,7 @@ describe('Connect', () => { }); it('should create unresolved capability templates from robustness-only inputs', () => { - const configurations = createTemplateConfigurations({ + const configurations = createMediaKeySystemConfigurations({ audioRobustness: ['A1', 'A2'], videoRobustness: ['V1', 'V2'] }); @@ -281,7 +270,7 @@ describe('Connect', () => { }); it('should keep the base configuration on each generated entry', () => { - const configurations = createTemplateConfigurations({ + const configurations = createMediaKeySystemConfigurations({ audioContentTypes: 'audio/mp4; codecs="mp4a.40.2"', baseConfiguration: { initDataTypes: ['cenc'], @@ -303,7 +292,7 @@ describe('Connect', () => { }); it('should return the base configuration when no template inputs are provided', () => { - const configurations = createTemplateConfigurations({ + const configurations = createMediaKeySystemConfigurations({ baseConfiguration: { persistentState: 'optional' } diff --git a/src/Connect.ts b/src/Connect.ts index 68fcb52..52623d0 100644 --- a/src/Connect.ts +++ b/src/Connect.ts @@ -7,7 +7,13 @@ import * as Util from './Util'; import { NetAddress } from './NetAddress'; import { log } from './Log'; -export type TemplateConfigurationsParams = { +/** + * Parameters of the createMediaKeySystemConfigurations helper function + * + * These parameters allow to write concise configurations for encrypted streams by specifying + * common audio/video content types, robustness values, and a base configuration. + */ +export type MediaKeySystemConfigurationParams = { audioContentTypes?: string | string[]; videoContentTypes?: string | string[]; audioRobustness?: string | string[]; @@ -16,17 +22,36 @@ export type TemplateConfigurationsParams = { }; /** - * Helper to create DRM configuration templates from common audio/video content types and robustness values. + * Helper to create the MediaKeySystem.configuration from : + * - common audio/video content types + * - base configuration (with other parameters) + * - robustness values * It generates all combinations of audio and video content types and robustness values, and merges them with * the base configuration if provided. * - * The returned configurations can be assigned to {@link KeySystem.templateConfigurations}. When the caller + * The returned configurations can be assigned to {@link MediaKeySystem.configurations}. When the caller * omits capability content types, a DRM implementation may enrich those templates with stream metadata before * passing the final configurations to `requestMediaKeySystemAccess()`. * + * Example of usage : + * + * ```ts + * const keySystem: MediaKeySystem = { + * license: 'https://license-server.com/getLicense', + * configurations: createMediaKeySystemConfigurations({ + * audioContentTypes: ['audio/mp4; codecs="mp4a.40.2"'], + * videoContentTypes: ['video/mp4; codecs="avc1.640028"', 'video/mp4; codecs="hvc1.1.6.L93.B0"'], + * audioRobustness: ['SW_SECURE_CRYPTO', 'HW_SECURE_CRYPTO'], + * videoRobustness: ['SW_SECURE_DECODE', 'HW_SECURE_DECODE'] + * }) + * }; + * ``` + * * @returns An array of MediaKeySystemConfiguration templates. */ -export function createTemplateConfigurations(params: TemplateConfigurationsParams): MediaKeySystemConfiguration[] { +export function createMediaKeySystemConfigurations( + params: MediaKeySystemConfigurationParams +): MediaKeySystemConfiguration[] { const audioContentTypes = Util.normalizeStringArray(params.audioContentTypes, false); const videoContentTypes = Util.normalizeStringArray(params.videoContentTypes, false); const requestedAudioRobustness = Util.normalizeStringArray(params.audioRobustness, false); @@ -86,26 +111,19 @@ export function createTemplateConfigurations(params: TemplateConfigurationsParam return configurations; } -export type DRMRequestConfig = +export type MediaKeyLicense = | string | { url: string; - /** - * The additional HTTP headers to send to the license server - */ - headers?: Record; + headers?: Record; }; -export type DRMCertificateConfig = +export type MediaKeyCertificate = | string | Uint8Array | { - url?: string; - data?: Uint8Array; - /** - * The additional HTTP headers to send to the certificate server - */ - headers?: Record; + url: string; + headers?: Record; }; /** @@ -115,27 +133,27 @@ export type DRMCertificateConfig = * * If the key system is an object, it's a key system configuration with more parameters. */ -export type KeySystem = +export type MediaKeySystem = | string | { /** * The license URL or configuration for the key system. If it's a string, it's the URL of the license server. */ - license?: DRMRequestConfig; + license?: MediaKeyLicense; /** * The certificate URL if needed (for FairPlay) or the certificate data as Uint8Array. */ - certificate?: DRMCertificateConfig; + certificate?: MediaKeyCertificate; /** - * Optional MediaKeySystemConfiguration templates. + * Optional MediaKeySystemConfiguration[]. * - * If metadata is available, a DRM implementation may enrich capabilities that do not define `contentType` + * If metadata is available, configuration may enrich capabilities that do not define `contentType` * before calling `requestMediaKeySystemAccess()`. Explicit `contentType` values provided by the user should * take precedence over metadata-derived values. * * If metadata is not available, these configurations are expected to be complete enough to be used as-is. */ - templateConfigurations?: MediaKeySystemConfiguration[]; + configurations?: MediaKeySystemConfiguration[]; }; /** @@ -165,7 +183,7 @@ export type Params = { * Map of keys to content protection settings for encrypted streams * The key can be "com.apple.fps" for example for FairPlay */ - contentProtection?: Record; + contentProtection?: Record; /** * Optional media extension (mp4, flv, ts, rts), usefull for protocol like WebRTS which supports different container type. * When not set, it's also an output parameter for {@link defineMediaExt} to indicate what is the media type selected From 9dbf3552bfa6099bcd7c49c6d10663868a71cd5b Mon Sep 17 00:00:00 2001 From: Thomas Jammet Date: Thu, 7 May 2026 22:04:53 +0200 Subject: [PATCH 6/8] chore(Util): normalizeStringArray is now toStringArray It also now support a default value instead of just an empty string fallback --- src/Connect.ts | 13 +++++++------ src/Util.spec.ts | 22 +++++++++++----------- src/Util.ts | 16 ++++++++-------- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/Connect.ts b/src/Connect.ts index 52623d0..007cd56 100644 --- a/src/Connect.ts +++ b/src/Connect.ts @@ -52,10 +52,10 @@ export type MediaKeySystemConfigurationParams = { export function createMediaKeySystemConfigurations( params: MediaKeySystemConfigurationParams ): MediaKeySystemConfiguration[] { - const audioContentTypes = Util.normalizeStringArray(params.audioContentTypes, false); - const videoContentTypes = Util.normalizeStringArray(params.videoContentTypes, false); - const requestedAudioRobustness = Util.normalizeStringArray(params.audioRobustness, false); - const requestedVideoRobustness = Util.normalizeStringArray(params.videoRobustness, false); + const audioContentTypes = Util.toStringArray(params.audioContentTypes); + const videoContentTypes = Util.toStringArray(params.videoContentTypes); + const requestedAudioRobustness = Util.toStringArray(params.audioRobustness); + const requestedVideoRobustness = Util.toStringArray(params.videoRobustness); if (!audioContentTypes.length && !videoContentTypes.length) { if (!requestedAudioRobustness.length && !requestedVideoRobustness.length) { @@ -80,8 +80,9 @@ export function createMediaKeySystemConfigurations( } const configurations: MediaKeySystemConfiguration[] = []; - const audioRobustness = audioContentTypes.length ? Util.normalizeStringArray(params.audioRobustness) : ['']; - const videoRobustness = videoContentTypes.length ? Util.normalizeStringArray(params.videoRobustness) : ['']; + + const audioRobustness = audioContentTypes.length ? Util.toStringArray(params.audioRobustness, ['']) : ['']; + const videoRobustness = videoContentTypes.length ? Util.toStringArray(params.videoRobustness, ['']) : ['']; const audioInputs = audioContentTypes.length ? audioContentTypes : ['']; const videoInputs = videoContentTypes.length ? videoContentTypes : ['']; diff --git a/src/Util.spec.ts b/src/Util.spec.ts index 0b1a6b1..70850d3 100644 --- a/src/Util.spec.ts +++ b/src/Util.spec.ts @@ -60,25 +60,25 @@ describe('Util', () => { }); }); - describe('normalizeStringArray', () => { + describe('toStringArray', () => { it('should normalize a string into an array', () => { - expect(Util.normalizeStringArray('value')).toEqual(['value']); + expect(Util.toStringArray('value')).toEqual(['value']); }); it('should keep non-empty values from an array', () => { - expect(Util.normalizeStringArray(['value1', '', 'value2'])).toEqual(['value1', 'value2']); + expect(Util.toStringArray(['value1', '', 'value2'])).toEqual(['value1', 'value2']); }); - it('should return an empty string fallback by default when no values are provided', () => { - expect(Util.normalizeStringArray()).toEqual(['']); - expect(Util.normalizeStringArray('')).toEqual(['']); - expect(Util.normalizeStringArray([''])).toEqual(['']); + it('should return an empty array by default when no values are provided', () => { + expect(Util.toStringArray(undefined)).toEqual([]); + expect(Util.toStringArray('')).toEqual([]); + expect(Util.toStringArray([''])).toEqual([]); }); - it('should omit the empty string fallback when includeEmpty is false', () => { - expect(Util.normalizeStringArray(undefined, false)).toEqual([]); - expect(Util.normalizeStringArray('', false)).toEqual([]); - expect(Util.normalizeStringArray([''], false)).toEqual([]); + it('should use the fallback array when provided', () => { + expect(Util.toStringArray(undefined, ['fallback'])).toEqual(['fallback']); + expect(Util.toStringArray('', ['fallback'])).toEqual(['fallback']); + expect(Util.toStringArray([''], ['fall', 'back'])).toEqual(['fall', 'back']); }); }); diff --git a/src/Util.ts b/src/Util.ts index 67c22c1..29baf49 100644 --- a/src/Util.ts +++ b/src/Util.ts @@ -23,20 +23,20 @@ const _perf = performance; // to increase x10 now performance! export const EMPTY_FUNCTION = () => {}; /** - * Converts a string or an array of strings into a normalized string array. - * Empty string values are filtered out. When no value remains, an optional empty-string - * fallback can be returned to simplify cartesian-product style expansions. + * Converts a string or array of strings into a normalized string array. + * Empty string values are filtered out. When no non-empty value remains, + * `defaultValue` is returned (defaults to `[]`). * * @param value The string or array of strings to normalize. - * @param includeEmpty If true, returns `['']` when no non-empty values are provided. + * @param defaultValue Returned when `value` is empty or contains only empty strings. * @returns A normalized array of strings. */ -export function normalizeStringArray(value?: string | string[], includeEmpty: boolean = true): string[] { +export function toStringArray(value: string | string[] | undefined, defaultValue: string[] = []): string[] { if (!value) { - return includeEmpty ? [''] : []; + return defaultValue; } - const values = Array.isArray(value) ? value.filter(Boolean) : [value]; - return values.length ? values : includeEmpty ? [''] : []; + const values = (Array.isArray(value) ? value : [value]).filter(Boolean); + return values.length ? values : defaultValue; } /** From 17c8cd30dcca49eddda170a6d399f250bfa186c3 Mon Sep 17 00:00:00 2001 From: Thomas Jammet Date: Thu, 7 May 2026 22:10:42 +0200 Subject: [PATCH 7/8] chore(Connect): createMediaKeySystemConfigurations now preserves unresolved media capabilities --- src/Connect.spec.ts | 27 ++++++++++++++++++++++++++- src/Connect.ts | 16 ++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/Connect.spec.ts b/src/Connect.spec.ts index 1294c34..b30159a 100644 --- a/src/Connect.spec.ts +++ b/src/Connect.spec.ts @@ -234,7 +234,7 @@ describe('Connect', () => { ]); }); - it('should not duplicate video-only configurations when audio robustness is provided', () => { + it('should preserve unresolved audio capabilities without duplicating video-only configurations', () => { const configurations = createMediaKeySystemConfigurations({ videoContentTypes: 'video/mp4; codecs="avc1.640028"', audioRobustness: ['SW_SECURE_CRYPTO', 'HW_SECURE_CRYPTO'], @@ -243,11 +243,13 @@ describe('Connect', () => { expect(configurations).toEqual([ { + audioCapabilities: [{ robustness: 'SW_SECURE_CRYPTO' }, { robustness: 'HW_SECURE_CRYPTO' }], videoCapabilities: [ { contentType: 'video/mp4; codecs="avc1.640028"', robustness: 'SW_SECURE_DECODE' } ] }, { + audioCapabilities: [{ robustness: 'SW_SECURE_CRYPTO' }, { robustness: 'HW_SECURE_CRYPTO' }], videoCapabilities: [ { contentType: 'video/mp4; codecs="avc1.640028"', robustness: 'HW_SECURE_DECODE' } ] @@ -255,6 +257,29 @@ describe('Connect', () => { ]); }); + it('should preserve unresolved video capabilities without duplicating audio-only configurations', () => { + const configurations = createMediaKeySystemConfigurations({ + audioContentTypes: 'audio/mp4; codecs="mp4a.40.2"', + audioRobustness: ['SW_SECURE_CRYPTO', 'HW_SECURE_CRYPTO'], + videoRobustness: ['SW_SECURE_DECODE', 'HW_SECURE_DECODE'] + }); + + expect(configurations).toEqual([ + { + audioCapabilities: [ + { contentType: 'audio/mp4; codecs="mp4a.40.2"', robustness: 'SW_SECURE_CRYPTO' } + ], + videoCapabilities: [{ robustness: 'SW_SECURE_DECODE' }, { robustness: 'HW_SECURE_DECODE' }] + }, + { + audioCapabilities: [ + { contentType: 'audio/mp4; codecs="mp4a.40.2"', robustness: 'HW_SECURE_CRYPTO' } + ], + videoCapabilities: [{ robustness: 'SW_SECURE_DECODE' }, { robustness: 'HW_SECURE_DECODE' }] + } + ]); + }); + it('should create unresolved capability templates from robustness-only inputs', () => { const configurations = createMediaKeySystemConfigurations({ audioRobustness: ['A1', 'A2'], diff --git a/src/Connect.ts b/src/Connect.ts index 007cd56..e2e4c30 100644 --- a/src/Connect.ts +++ b/src/Connect.ts @@ -83,6 +83,18 @@ export function createMediaKeySystemConfigurations( const audioRobustness = audioContentTypes.length ? Util.toStringArray(params.audioRobustness, ['']) : ['']; const videoRobustness = videoContentTypes.length ? Util.toStringArray(params.videoRobustness, ['']) : ['']; + const unresolvedAudioCapabilities = + requestedAudioRobustness.length > 0 + ? requestedAudioRobustness.map(robustness => ({ + ...(robustness && { robustness }) + })) + : undefined; + const unresolvedVideoCapabilities = + requestedVideoRobustness.length > 0 + ? requestedVideoRobustness.map(robustness => ({ + ...(robustness && { robustness }) + })) + : undefined; const audioInputs = audioContentTypes.length ? audioContentTypes : ['']; const videoInputs = videoContentTypes.length ? videoContentTypes : ['']; @@ -93,10 +105,10 @@ export function createMediaKeySystemConfigurations( for (const videoR of videoRobustness) { const audioCapabilities = audioContentType ? [{ contentType: audioContentType, ...(audioR && { robustness: audioR }) }] - : undefined; + : unresolvedAudioCapabilities; const videoCapabilities = videoContentType ? [{ contentType: videoContentType, ...(videoR && { robustness: videoR }) }] - : undefined; + : unresolvedVideoCapabilities; const config: MediaKeySystemConfiguration = { ...params.baseConfiguration, From 30fbd0451c6c9a54cbc765a43ebdf905fd8af7d6 Mon Sep 17 00:00:00 2001 From: Thomas Jammet Date: Mon, 11 May 2026 08:32:30 +0200 Subject: [PATCH 8/8] chore(Connect): fix typo and remove useless check --- src/Connect.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Connect.ts b/src/Connect.ts index e2e4c30..92f6c19 100644 --- a/src/Connect.ts +++ b/src/Connect.ts @@ -22,7 +22,7 @@ export type MediaKeySystemConfigurationParams = { }; /** - * Helper to create the MediaKeySystem.configuration from : + * Helper to create the MediaKeySystem.configurations from : * - common audio/video content types * - base configuration (with other parameters) * - robustness values @@ -66,14 +66,10 @@ export function createMediaKeySystemConfigurations( { ...params.baseConfiguration, ...(requestedAudioRobustness.length > 0 && { - audioCapabilities: requestedAudioRobustness.map(robustness => ({ - ...(robustness && { robustness }) - })) + audioCapabilities: requestedAudioRobustness.map(robustness => ({ ...{ robustness } })) }), ...(requestedVideoRobustness.length > 0 && { - videoCapabilities: requestedVideoRobustness.map(robustness => ({ - ...(robustness && { robustness }) - })) + videoCapabilities: requestedVideoRobustness.map(robustness => ({ ...{ robustness } })) }) } ];