@videojs/spf 10.0.0-beta.24 → 10.0.0-beta.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/default/background-video.js +3 -0
- package/dist/default/core/composition/create-composition.js +1 -1
- package/dist/default/core/composition/create-composition.js.map +1 -1
- package/dist/default/core/composition/share-signals.js +13 -11
- package/dist/default/core/composition/share-signals.js.map +1 -1
- package/dist/default/hls.js +3 -1
- package/dist/default/media/abr/quality-selection.js +6 -68
- package/dist/default/media/abr/quality-selection.js.map +1 -1
- package/dist/default/media/dom/capabilities.js +70 -0
- package/dist/default/media/dom/capabilities.js.map +1 -0
- package/dist/default/media/dom/mse/mediasource-setup.js +1 -1
- package/dist/default/media/dom/text/text-track-slots.js +3 -3
- package/dist/default/media/dom/text/text-track-slots.js.map +1 -1
- package/dist/default/media/hls/parse-attributes.js +16 -2
- package/dist/default/media/hls/parse-attributes.js.map +1 -1
- package/dist/default/media/hls/parse-media-playlist.js +25 -2
- package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
- package/dist/default/media/hls/parse-multivariant.js +15 -6
- package/dist/default/media/hls/parse-multivariant.js.map +1 -1
- package/dist/default/media/primitives/select-tracks.js +98 -13
- package/dist/default/media/primitives/select-tracks.js.map +1 -1
- package/dist/default/media/types/index.js.map +1 -1
- package/dist/default/media/utils/cdn.js +56 -0
- package/dist/default/media/utils/cdn.js.map +1 -0
- package/dist/default/media/utils/tracks.js +28 -1
- package/dist/default/media/utils/tracks.js.map +1 -1
- package/dist/default/network/fetch.js +7 -1
- package/dist/default/network/fetch.js.map +1 -1
- package/dist/default/playback/actors/dom/segment-loader.js +42 -11
- package/dist/default/playback/actors/dom/segment-loader.js.map +1 -1
- package/dist/default/playback/actors/dom/source-buffer.js +2 -1
- package/dist/default/playback/actors/dom/source-buffer.js.map +1 -1
- package/dist/default/playback/behaviors/calculate-presentation-duration.js +21 -43
- package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -1
- package/dist/default/playback/behaviors/derive-cdn-priority.js +68 -0
- package/dist/default/playback/behaviors/derive-cdn-priority.js.map +1 -0
- package/dist/default/playback/behaviors/dom/end-of-stream.js +15 -103
- package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
- package/dist/default/playback/behaviors/dom/load-segments.js +1 -1
- package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -1
- package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +33 -14
- package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
- package/dist/default/playback/behaviors/dom/sync-text-tracks.js +43 -18
- package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-current-time.js +6 -0
- package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -1
- package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +1 -1
- package/dist/default/playback/behaviors/resolve-track.js +45 -22
- package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
- package/dist/default/playback/behaviors/select-tracks.js +46 -77
- package/dist/default/playback/behaviors/select-tracks.js.map +1 -1
- package/dist/default/playback/behaviors/setup-failover-monitor.js +66 -0
- package/dist/default/playback/behaviors/setup-failover-monitor.js.map +1 -0
- package/dist/default/playback/behaviors/track-switching.js +341 -0
- package/dist/default/playback/behaviors/track-switching.js.map +1 -0
- package/dist/default/playback/engines/background-video/adapter.js +158 -0
- package/dist/default/playback/engines/background-video/adapter.js.map +1 -0
- package/dist/default/playback/engines/background-video/engine.js +75 -0
- package/dist/default/playback/engines/background-video/engine.js.map +1 -0
- package/dist/default/playback/engines/hls/adapter-audio-only.js +119 -0
- package/dist/default/playback/engines/hls/adapter-audio-only.js.map +1 -0
- package/dist/default/playback/engines/hls/adapter.js +7 -0
- package/dist/default/playback/engines/hls/adapter.js.map +1 -1
- package/dist/default/playback/engines/hls/engine-audio-only.js +80 -0
- package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -0
- package/dist/default/playback/engines/hls/engine.js +20 -7
- package/dist/default/playback/engines/hls/engine.js.map +1 -1
- package/dist/default/playback/primitives/failover-fetch.js +42 -0
- package/dist/default/playback/primitives/failover-fetch.js.map +1 -0
- package/dist/{dev/playback/behaviors → default/playback/primitives}/track-types.js +1 -1
- package/dist/default/playback/primitives/track-types.js.map +1 -0
- package/dist/dev/background-video.d.ts +3 -0
- package/dist/dev/background-video.js +3 -0
- package/dist/dev/core/composition/create-composition.js +1 -1
- package/dist/dev/core/composition/create-composition.js.map +1 -1
- package/dist/dev/core/composition/share-signals.d.ts +11 -9
- package/dist/dev/core/composition/share-signals.d.ts.map +1 -1
- package/dist/dev/core/composition/share-signals.js +13 -11
- package/dist/dev/core/composition/share-signals.js.map +1 -1
- package/dist/dev/hls.d.ts +3 -1
- package/dist/dev/hls.js +3 -1
- package/dist/dev/media/abr/quality-selection.js +6 -68
- package/dist/dev/media/abr/quality-selection.js.map +1 -1
- package/dist/dev/media/dom/capabilities.js +70 -0
- package/dist/dev/media/dom/capabilities.js.map +1 -0
- package/dist/dev/media/dom/mse/mediasource-setup.js +1 -1
- package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -1
- package/dist/dev/media/dom/text/text-track-slots.js +3 -3
- package/dist/dev/media/dom/text/text-track-slots.js.map +1 -1
- package/dist/dev/media/hls/parse-attributes.js +16 -2
- package/dist/dev/media/hls/parse-attributes.js.map +1 -1
- package/dist/dev/media/hls/parse-media-playlist.js +25 -2
- package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
- package/dist/dev/media/hls/parse-multivariant.js +15 -6
- package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
- package/dist/dev/media/primitives/select-tracks.d.ts +36 -0
- package/dist/dev/media/primitives/select-tracks.d.ts.map +1 -0
- package/dist/dev/media/primitives/select-tracks.js +98 -13
- package/dist/dev/media/primitives/select-tracks.js.map +1 -1
- package/dist/dev/media/types/index.d.ts +26 -2
- package/dist/dev/media/types/index.d.ts.map +1 -1
- package/dist/dev/media/types/index.js.map +1 -1
- package/dist/dev/media/utils/cdn.d.ts +13 -0
- package/dist/dev/media/utils/cdn.d.ts.map +1 -0
- package/dist/dev/media/utils/cdn.js +56 -0
- package/dist/dev/media/utils/cdn.js.map +1 -0
- package/dist/dev/media/utils/tracks.js +28 -1
- package/dist/dev/media/utils/tracks.js.map +1 -1
- package/dist/dev/network/fetch.js +7 -1
- package/dist/dev/network/fetch.js.map +1 -1
- package/dist/dev/playback/actors/dom/segment-loader.js +42 -11
- package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -1
- package/dist/dev/playback/actors/dom/source-buffer.d.ts +15 -0
- package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -1
- package/dist/dev/playback/actors/dom/source-buffer.js +2 -1
- package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -1
- package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +8 -0
- package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/calculate-presentation-duration.js +21 -43
- package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -1
- package/dist/dev/playback/behaviors/derive-cdn-priority.js +68 -0
- package/dist/dev/playback/behaviors/derive-cdn-priority.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/end-of-stream.js +15 -103
- package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/load-segments.js +1 -1
- package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +33 -14
- package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/sync-text-tracks.js +43 -18
- package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-current-time.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-current-time.js +6 -0
- package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +1 -1
- package/dist/dev/playback/behaviors/resolve-track.js +45 -22
- package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
- package/dist/dev/playback/behaviors/select-tracks.d.ts +13 -0
- package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -0
- package/dist/dev/playback/behaviors/select-tracks.js +46 -77
- package/dist/dev/playback/behaviors/select-tracks.js.map +1 -1
- package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts +12 -0
- package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts.map +1 -0
- package/dist/dev/playback/behaviors/setup-failover-monitor.js +66 -0
- package/dist/dev/playback/behaviors/setup-failover-monitor.js.map +1 -0
- package/dist/dev/playback/behaviors/track-switching.js +341 -0
- package/dist/dev/playback/behaviors/track-switching.js.map +1 -0
- package/dist/dev/playback/engines/background-video/adapter.d.ts +60 -0
- package/dist/dev/playback/engines/background-video/adapter.d.ts.map +1 -0
- package/dist/dev/playback/engines/background-video/adapter.js +158 -0
- package/dist/dev/playback/engines/background-video/adapter.js.map +1 -0
- package/dist/dev/playback/engines/background-video/engine.d.ts +107 -0
- package/dist/dev/playback/engines/background-video/engine.d.ts.map +1 -0
- package/dist/dev/playback/engines/background-video/engine.js +75 -0
- package/dist/dev/playback/engines/background-video/engine.js.map +1 -0
- package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +45 -0
- package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +1 -0
- package/dist/dev/playback/engines/hls/adapter-audio-only.js +119 -0
- package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +1 -0
- package/dist/dev/playback/engines/hls/adapter.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/adapter.js +7 -0
- package/dist/dev/playback/engines/hls/adapter.js.map +1 -1
- package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +127 -0
- package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -0
- package/dist/dev/playback/engines/hls/engine-audio-only.js +80 -0
- package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -0
- package/dist/dev/playback/engines/hls/engine.d.ts +61 -2
- package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/engine.js +20 -7
- package/dist/dev/playback/engines/hls/engine.js.map +1 -1
- package/dist/dev/playback/primitives/failover-fetch.js +42 -0
- package/dist/dev/playback/primitives/failover-fetch.js.map +1 -0
- package/dist/{default/playback/behaviors → dev/playback/primitives}/track-types.js +1 -1
- package/dist/dev/playback/primitives/track-types.js.map +1 -0
- package/package.json +7 -2
- package/dist/default/playback/behaviors/quality-switching.js +0 -96
- package/dist/default/playback/behaviors/quality-switching.js.map +0 -1
- package/dist/default/playback/behaviors/track-types.js.map +0 -1
- package/dist/dev/playback/behaviors/quality-switching.js +0 -96
- package/dist/dev/playback/behaviors/quality-switching.js.map +0 -1
- package/dist/dev/playback/behaviors/track-types.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-attributes.js","names":[],"sources":["../../../../src/media/hls/parse-attributes.ts"],"sourcesContent":["import type { FrameRate } from '../types';\n\n/**\n * Parse HLS attribute list from a tag line.\n * Handles both quoted and unquoted values.\n */\nexport function parseAttributeList(line: string): Map<string, string> {\n const attributes = new Map<string, string>();\n const regex = /([A-Z0-9-]+)=(?:\"([^\"]*)\"|([^,]*))/g;\n\n for (const match of line.matchAll(regex)) {\n const key = match[1];\n const value = match[2] ?? match[3] ?? '';\n if (key) {\n attributes.set(key, value);\n }\n }\n\n return attributes;\n}\n\n/**\n * Parse RESOLUTION attribute value (WIDTHxHEIGHT).\n */\nexport function parseResolution(value: string): { width: number; height: number } | null {\n const match = /^(\\d+)x(\\d+)$/.exec(value);\n if (!match) return null;\n\n const width = Number.parseInt(match[1]!, 10);\n const height = Number.parseInt(match[2]!, 10);\n\n return { width, height };\n}\n\n/**\n * Parse FRAME-RATE attribute to rational frame rate.\n */\nexport function parseFrameRate(value: string): FrameRate | undefined {\n const fps = Number.parseFloat(value);\n if (Number.isNaN(fps) || fps <= 0) return undefined;\n\n // Common frame rates with tolerance for floating point precision\n if (Math.abs(fps - 23.976) < 0.01) {\n return { frameRateNumerator: 24000, frameRateDenominator: 1001 };\n }\n if (Math.abs(fps - 29.97) < 0.01) {\n return { frameRateNumerator: 30000, frameRateDenominator: 1001 };\n }\n if (Math.abs(fps - 59.94) < 0.01) {\n return { frameRateNumerator: 60000, frameRateDenominator: 1001 };\n }\n\n // Integer frame rates\n if (fps % 1 === 0) {\n return { frameRateNumerator: Math.round(fps) };\n }\n\n // Default: use rounded value\n return { frameRateNumerator: Math.round(fps) };\n}\n\n/**\n * Parse CODECS attribute into separate video and audio codecs.\n */\nexport function parseCodecs(codecs: string): { video?: string; audio?: string } {\n const parts = codecs.split(',').map((s) => s.trim());\n const result: { video?: string; audio?: string } = {};\n\n for (const codec of parts) {\n if (codec.startsWith('avc1.') || codec.startsWith('hvc1.') || codec.startsWith('hev1.')) {\n result.video = codec;\n } else if (
|
|
1
|
+
{"version":3,"file":"parse-attributes.js","names":[],"sources":["../../../../src/media/hls/parse-attributes.ts"],"sourcesContent":["import type { FrameRate } from '../types';\n\n/**\n * Parse HLS attribute list from a tag line.\n * Handles both quoted and unquoted values.\n */\nexport function parseAttributeList(line: string): Map<string, string> {\n const attributes = new Map<string, string>();\n const regex = /([A-Z0-9-]+)=(?:\"([^\"]*)\"|([^,]*))/g;\n\n for (const match of line.matchAll(regex)) {\n const key = match[1];\n const value = match[2] ?? match[3] ?? '';\n if (key) {\n attributes.set(key, value);\n }\n }\n\n return attributes;\n}\n\n/**\n * Parse RESOLUTION attribute value (WIDTHxHEIGHT).\n */\nexport function parseResolution(value: string): { width: number; height: number } | null {\n const match = /^(\\d+)x(\\d+)$/.exec(value);\n if (!match) return null;\n\n const width = Number.parseInt(match[1]!, 10);\n const height = Number.parseInt(match[2]!, 10);\n\n return { width, height };\n}\n\n/**\n * Parse FRAME-RATE attribute to rational frame rate.\n */\nexport function parseFrameRate(value: string): FrameRate | undefined {\n const fps = Number.parseFloat(value);\n if (Number.isNaN(fps) || fps <= 0) return undefined;\n\n // Common frame rates with tolerance for floating point precision\n if (Math.abs(fps - 23.976) < 0.01) {\n return { frameRateNumerator: 24000, frameRateDenominator: 1001 };\n }\n if (Math.abs(fps - 29.97) < 0.01) {\n return { frameRateNumerator: 30000, frameRateDenominator: 1001 };\n }\n if (Math.abs(fps - 59.94) < 0.01) {\n return { frameRateNumerator: 60000, frameRateDenominator: 1001 };\n }\n\n // Integer frame rates\n if (fps % 1 === 0) {\n return { frameRateNumerator: Math.round(fps) };\n }\n\n // Default: use rounded value\n return { frameRateNumerator: Math.round(fps) };\n}\n\n// Audio codec identifiers, matched case-insensitively against each CODECS\n// entry's prefix. Beyond AAC (`mp4a.*`): Dolby (`ac-3`, `ec-3`, `ac-4`), Opus,\n// FLAC (`fLaC`), DTS (`dts*`), ALAC, and Vorbis. Recognizing these is what lets\n// capability probing filter undecodable audio renditions (e.g. an AC-3 5.1\n// track on a browser without AC-3) — an unrecognized codec parses empty and is\n// treated as unprobeable, so it would never be pruned.\nconst AUDIO_CODEC_PREFIXES = ['mp4a.', 'ac-3', 'ec-3', 'ac-4', 'opus', 'flac', 'dts', 'alac', 'vorbis'];\n\n/**\n * Parse CODECS attribute into separate video and audio codecs.\n */\nexport function parseCodecs(codecs: string): { video?: string; audio?: string } {\n const parts = codecs.split(',').map((s) => s.trim());\n const result: { video?: string; audio?: string } = {};\n\n for (const codec of parts) {\n const lower = codec.toLowerCase();\n if (codec.startsWith('avc1.') || codec.startsWith('hvc1.') || codec.startsWith('hev1.')) {\n result.video = codec;\n } else if (AUDIO_CODEC_PREFIXES.some((prefix) => lower.startsWith(prefix))) {\n result.audio = codec;\n }\n }\n\n return result;\n}\n\n/**\n * Parse #EXTINF duration value.\n */\nexport function parseExtInfDuration(value: string): number {\n const durationPart = value.split(',')[0] ?? value;\n const duration = Number.parseFloat(durationPart);\n return Number.isNaN(duration) ? 0 : duration;\n}\n\n/**\n * Parse BYTERANGE attribute value.\n * Format: \"length[@offset]\"\n * If offset is omitted, it continues from the previous byte range end.\n */\nexport function parseByteRange(value: string, previousEnd?: number): { start: number; end: number } | null {\n const match = /^(\\d+)(?:@(\\d+))?$/.exec(value);\n if (!match) return null;\n\n const length = Number.parseInt(match[1]!, 10);\n if (Number.isNaN(length)) return null;\n\n let start: number;\n if (match[2] !== undefined) {\n start = Number.parseInt(match[2], 10);\n if (Number.isNaN(start)) return null;\n } else if (previousEnd !== undefined) {\n start = previousEnd;\n } else {\n return null;\n }\n\n return { start, end: start + length - 1 };\n}\n\n/**\n * AttributeList - Typed attribute access wrapper.\n */\nexport interface AttributeList {\n get: (key: string) => string | undefined;\n getInt: (key: string, defaultValue?: number) => number | undefined;\n getFloat: (key: string, defaultValue?: number) => number | undefined;\n getBool: (key: string) => boolean;\n getResolution: (key: string) => { width: number; height: number } | undefined;\n getFrameRate: (key: string) => FrameRate | undefined;\n}\n\n/**\n * Create AttributeList from raw attribute string.\n */\nexport function createAttributeList(line: string): AttributeList {\n const map = parseAttributeList(line);\n\n return {\n get(key: string): string | undefined {\n return map.get(key);\n },\n\n getInt(key: string, defaultValue?: number): number | undefined {\n const value = map.get(key);\n if (value === undefined) return defaultValue;\n const parsed = Number.parseInt(value, 10);\n return Number.isNaN(parsed) ? defaultValue : parsed;\n },\n\n getFloat(key: string, defaultValue?: number): number | undefined {\n const value = map.get(key);\n if (value === undefined) return defaultValue;\n const parsed = Number.parseFloat(value);\n return Number.isNaN(parsed) ? defaultValue : parsed;\n },\n\n getBool(key: string): boolean {\n return map.get(key) === 'YES';\n },\n\n getResolution(key: string): { width: number; height: number } | undefined {\n const value = map.get(key);\n if (!value) return undefined;\n return parseResolution(value) ?? undefined;\n },\n\n getFrameRate(key: string): FrameRate | undefined {\n const value = map.get(key);\n if (!value) return undefined;\n return parseFrameRate(value);\n },\n };\n}\n\n/**\n * Match a tag and extract its attributes.\n * Returns null if the line doesn't match the tag.\n */\nexport function matchTag(line: string, tag: string): AttributeList | null {\n const prefix = `#${tag}:`;\n if (!line.startsWith(prefix)) return null;\n return createAttributeList(line.slice(prefix.length));\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { matchTag, parseByteRange, parseExtInfDuration } from "./parse-attributes.js";
|
|
2
2
|
import { resolveUrl } from "./resolve-url.js";
|
|
3
|
-
|
|
3
|
+
const CONTAINER_MIME_BY_EXTENSION = {
|
|
4
|
+
".ts": "video/mp2t",
|
|
5
|
+
".aac": "audio/aac"
|
|
6
|
+
};
|
|
7
|
+
/** The non-fMP4 container MIMEs the parser detects — all currently treated as unplayable. */
|
|
8
|
+
const NON_FMP4_CONTAINER_MIMES = new Set(Object.values(CONTAINER_MIME_BY_EXTENSION));
|
|
9
|
+
/**
|
|
10
|
+
* Non-fMP4 container MIME for a (resolved, absolute) segment URL, by file
|
|
11
|
+
* extension, ignoring the query string. `undefined` for fMP4 / unrecognized.
|
|
12
|
+
*/
|
|
13
|
+
function containerMimeFromSegment(url) {
|
|
14
|
+
if (!url) return void 0;
|
|
15
|
+
let path;
|
|
16
|
+
try {
|
|
17
|
+
path = new URL(url).pathname.toLowerCase();
|
|
18
|
+
} catch {
|
|
19
|
+
path = url.toLowerCase().split("?")[0] ?? "";
|
|
20
|
+
}
|
|
21
|
+
const dot = path.lastIndexOf(".");
|
|
22
|
+
return dot === -1 ? void 0 : CONTAINER_MIME_BY_EXTENSION[path.slice(dot)];
|
|
23
|
+
}
|
|
4
24
|
/**
|
|
5
25
|
* Parse HLS media playlist and resolve track with segments.
|
|
6
26
|
*
|
|
@@ -68,8 +88,11 @@ function parseMediaPlaylist(text, unresolved) {
|
|
|
68
88
|
url: initSegmentUrl,
|
|
69
89
|
...initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}
|
|
70
90
|
} : { url: "" };
|
|
91
|
+
const detectedContainer = initSegmentUrl ? void 0 : containerMimeFromSegment(segments[0]?.url);
|
|
92
|
+
const mimeType = unresolved.type !== "text" && detectedContainer ? detectedContainer : unresolved.mimeType;
|
|
71
93
|
return {
|
|
72
94
|
...unresolved,
|
|
95
|
+
mimeType,
|
|
73
96
|
startTime: 0,
|
|
74
97
|
duration: totalDuration,
|
|
75
98
|
segments,
|
|
@@ -77,6 +100,6 @@ function parseMediaPlaylist(text, unresolved) {
|
|
|
77
100
|
};
|
|
78
101
|
}
|
|
79
102
|
//#endregion
|
|
80
|
-
export { parseMediaPlaylist };
|
|
103
|
+
export { NON_FMP4_CONTAINER_MIMES, parseMediaPlaylist };
|
|
81
104
|
|
|
82
105
|
//# sourceMappingURL=parse-media-playlist.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-media-playlist.js","names":[],"sources":["../../../../src/media/hls/parse-media-playlist.ts"],"sourcesContent":["import type {\n AudioTrack,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedTrack,\n PartiallyResolvedVideoTrack,\n Segment,\n TextTrack,\n VideoTrack,\n} from '../types';\nimport { matchTag, parseByteRange, parseExtInfDuration } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/**\n * Resolve unresolved track type to its resolved equivalent.\n */\ntype ResolveTrack<T> = T extends PartiallyResolvedVideoTrack\n ? VideoTrack\n : T extends PartiallyResolvedAudioTrack\n ? AudioTrack\n : T extends PartiallyResolvedTextTrack\n ? TextTrack\n : never;\n\n/**\n * Parse HLS media playlist and resolve track with segments.\n *\n * Takes an unresolved track (from multivariant playlist) and media playlist text,\n * returns a HAM-compliant resolved track with segments.\n *\n * @param text - Media playlist text content\n * @param unresolved - Unresolved track from parseMultivariantPlaylist\n * @returns Resolved track with segments (type inferred from input)\n */\nexport function parseMediaPlaylist<T extends PartiallyResolvedTrack>(\n text: string,\n unresolved: T | ResolveTrack<T>\n): ResolveTrack<T> {\n const lines = text.split(/\\r?\\n/);\n\n // Segments and resources resolve relative to media playlist URL (per HLS spec)\n const baseUrl = unresolved.url;\n\n // Parse playlist\n const segments: Segment[] = [];\n let initSegmentUrl: string | undefined;\n let initSegmentByteRange: { start: number; end: number } | undefined;\n\n let currentDuration = 0;\n let currentByteRange: { start: number; end: number } | undefined;\n let currentTime = 0;\n let segmentIndex = 0;\n let previousByteRangeEnd: number | undefined;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#EXT'))) {\n continue;\n }\n\n if (\n trimmed === '#EXTM3U' ||\n trimmed.startsWith('#EXT-X-VERSION:') ||\n trimmed.startsWith('#EXT-X-TARGETDURATION:') ||\n trimmed.startsWith('#EXT-X-PLAYLIST-TYPE:') ||\n trimmed.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')\n ) {\n continue;\n }\n\n // #EXT-X-MAP - Init segment\n const mapAttrs = matchTag(trimmed, 'EXT-X-MAP');\n if (mapAttrs) {\n const uri = mapAttrs.get('URI');\n if (uri) {\n initSegmentUrl = resolveUrl(uri, baseUrl);\n const byteRangeStr = mapAttrs.get('BYTERANGE');\n if (byteRangeStr) {\n initSegmentByteRange = parseByteRange(byteRangeStr, 0) ?? undefined;\n }\n }\n continue;\n }\n\n // #EXTINF - Segment duration\n if (trimmed.startsWith('#EXTINF:')) {\n currentDuration = parseExtInfDuration(trimmed.slice(8));\n continue;\n }\n\n // #EXT-X-BYTERANGE - Segment byte range\n if (trimmed.startsWith('#EXT-X-BYTERANGE:')) {\n currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? undefined;\n continue;\n }\n\n if (trimmed === '#EXT-X-ENDLIST') {\n continue;\n }\n\n // Segment URI\n if (!trimmed.startsWith('#') && currentDuration > 0) {\n const segment: Segment = {\n id: `segment-${segmentIndex}`,\n url: resolveUrl(trimmed, baseUrl),\n duration: currentDuration,\n startTime: currentTime,\n };\n\n if (currentByteRange) {\n segment.byteRange = currentByteRange;\n previousByteRangeEnd = currentByteRange.end + 1;\n } else {\n previousByteRangeEnd = undefined;\n }\n\n segments.push(segment);\n currentTime += currentDuration;\n segmentIndex++;\n\n currentDuration = 0;\n currentByteRange = undefined;\n }\n }\n\n const totalDuration = currentTime;\n\n // Build initialization (VTT may not have init segment)\n const initialization =\n unresolved.type === 'text' && !initSegmentUrl\n ? undefined\n : initSegmentUrl\n ? { url: initSegmentUrl, ...(initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}) }\n : { url: '' };\n\n // Generic resolution: All type-specific fields already on unresolved track from P1\n // Just add parsed properties (startTime, duration, segments, initialization)\n return {\n ...unresolved,\n startTime: 0,\n duration: totalDuration,\n segments,\n initialization,\n } as unknown as ResolveTrack<T>;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"parse-media-playlist.js","names":[],"sources":["../../../../src/media/hls/parse-media-playlist.ts"],"sourcesContent":["import type {\n AudioTrack,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedTrack,\n PartiallyResolvedVideoTrack,\n Segment,\n TextTrack,\n VideoTrack,\n} from '../types';\nimport { matchTag, parseByteRange, parseExtInfDuration } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/** MPEG-2 Transport Stream (IANA `video/MP2T`, lowercased for `isTypeSupported`). Video + audio TS — there is no `audio/mp2t`. */\nexport const MPEG_TS_MIME = 'video/mp2t';\n/** Raw ADTS AAC packed-audio (HLS `.aac` segments; IANA `audio/aac`). */\nexport const RAW_AAC_MIME = 'audio/aac';\n\n// Non-fMP4 container MIMEs keyed by segment file extension. fMP4 (the MSE\n// default) always carries an EXT-X-MAP init segment, so a media playlist with\n// no init segment and one of these extensions is a non-fMP4 rendition,\n// relabeled from the fMP4 default. Extend with `.mp3` → 'audio/mpeg' etc.\nconst CONTAINER_MIME_BY_EXTENSION: Record<string, string> = {\n '.ts': MPEG_TS_MIME,\n '.aac': RAW_AAC_MIME,\n};\n\n/** The non-fMP4 container MIMEs the parser detects — all currently treated as unplayable. */\nexport const NON_FMP4_CONTAINER_MIMES = new Set(Object.values(CONTAINER_MIME_BY_EXTENSION));\n\n/**\n * Non-fMP4 container MIME for a (resolved, absolute) segment URL, by file\n * extension, ignoring the query string. `undefined` for fMP4 / unrecognized.\n */\nfunction containerMimeFromSegment(url: string | undefined): string | undefined {\n if (!url) return undefined;\n let path: string;\n try {\n path = new URL(url).pathname.toLowerCase();\n } catch {\n path = url.toLowerCase().split('?')[0] ?? '';\n }\n const dot = path.lastIndexOf('.');\n return dot === -1 ? undefined : CONTAINER_MIME_BY_EXTENSION[path.slice(dot)];\n}\n\n/**\n * Resolve unresolved track type to its resolved equivalent.\n */\ntype ResolveTrack<T> = T extends PartiallyResolvedVideoTrack\n ? VideoTrack\n : T extends PartiallyResolvedAudioTrack\n ? AudioTrack\n : T extends PartiallyResolvedTextTrack\n ? TextTrack\n : never;\n\n/**\n * Parse HLS media playlist and resolve track with segments.\n *\n * Takes an unresolved track (from multivariant playlist) and media playlist text,\n * returns a HAM-compliant resolved track with segments.\n *\n * @param text - Media playlist text content\n * @param unresolved - Unresolved track from parseMultivariantPlaylist\n * @returns Resolved track with segments (type inferred from input)\n */\nexport function parseMediaPlaylist<T extends PartiallyResolvedTrack>(\n text: string,\n unresolved: T | ResolveTrack<T>\n): ResolveTrack<T> {\n const lines = text.split(/\\r?\\n/);\n\n // Segments and resources resolve relative to media playlist URL (per HLS spec)\n const baseUrl = unresolved.url;\n\n // Parse playlist\n const segments: Segment[] = [];\n let initSegmentUrl: string | undefined;\n let initSegmentByteRange: { start: number; end: number } | undefined;\n\n let currentDuration = 0;\n let currentByteRange: { start: number; end: number } | undefined;\n let currentTime = 0;\n let segmentIndex = 0;\n let previousByteRangeEnd: number | undefined;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#EXT'))) {\n continue;\n }\n\n if (\n trimmed === '#EXTM3U' ||\n trimmed.startsWith('#EXT-X-VERSION:') ||\n trimmed.startsWith('#EXT-X-TARGETDURATION:') ||\n trimmed.startsWith('#EXT-X-PLAYLIST-TYPE:') ||\n trimmed.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')\n ) {\n continue;\n }\n\n // #EXT-X-MAP - Init segment\n const mapAttrs = matchTag(trimmed, 'EXT-X-MAP');\n if (mapAttrs) {\n const uri = mapAttrs.get('URI');\n if (uri) {\n initSegmentUrl = resolveUrl(uri, baseUrl);\n const byteRangeStr = mapAttrs.get('BYTERANGE');\n if (byteRangeStr) {\n initSegmentByteRange = parseByteRange(byteRangeStr, 0) ?? undefined;\n }\n }\n continue;\n }\n\n // #EXTINF - Segment duration\n if (trimmed.startsWith('#EXTINF:')) {\n currentDuration = parseExtInfDuration(trimmed.slice(8));\n continue;\n }\n\n // #EXT-X-BYTERANGE - Segment byte range\n if (trimmed.startsWith('#EXT-X-BYTERANGE:')) {\n currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? undefined;\n continue;\n }\n\n if (trimmed === '#EXT-X-ENDLIST') {\n continue;\n }\n\n // Segment URI\n if (!trimmed.startsWith('#') && currentDuration > 0) {\n const segment: Segment = {\n id: `segment-${segmentIndex}`,\n url: resolveUrl(trimmed, baseUrl),\n duration: currentDuration,\n startTime: currentTime,\n };\n\n if (currentByteRange) {\n segment.byteRange = currentByteRange;\n previousByteRangeEnd = currentByteRange.end + 1;\n } else {\n previousByteRangeEnd = undefined;\n }\n\n segments.push(segment);\n currentTime += currentDuration;\n segmentIndex++;\n\n currentDuration = 0;\n currentByteRange = undefined;\n }\n }\n\n const totalDuration = currentTime;\n\n // Build initialization (VTT may not have init segment)\n const initialization =\n unresolved.type === 'text' && !initSegmentUrl\n ? undefined\n : initSegmentUrl\n ? { url: initSegmentUrl, ...(initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}) }\n : { url: '' };\n\n // Container detection: fMP4 always carries an EXT-X-MAP init segment, so its\n // absence plus a recognized non-fMP4 segment extension (`.ts` → MPEG-TS,\n // `.aac` → raw ADTS AAC) marks a non-fMP4 rendition (high-precision — never\n // trips on fMP4, which mandates the map). Relabel from the fMP4 default\n // `video/mp4` / `audio/mp4` to the container MIME so capability probing prunes\n // it (these containers are currently treated as unplayable; see `canPlayTrack`).\n const detectedContainer = initSegmentUrl ? undefined : containerMimeFromSegment(segments[0]?.url);\n const mimeType = unresolved.type !== 'text' && detectedContainer ? detectedContainer : unresolved.mimeType;\n\n // Generic resolution: All type-specific fields already on unresolved track from P1\n // Just add parsed properties (startTime, duration, segments, initialization)\n return {\n ...unresolved,\n mimeType,\n startTime: 0,\n duration: totalDuration,\n segments,\n initialization,\n } as unknown as ResolveTrack<T>;\n}\n"],"mappings":";;AAsBA,MAAM,8BAAsD;CAC1D,OAT0B;CAU1B,QAR0B;CAS3B;;AAGD,MAAa,2BAA2B,IAAI,IAAI,OAAO,OAAO,4BAA4B,CAAC;;;;;AAM3F,SAAS,yBAAyB,KAA6C;AAC7E,KAAI,CAAC,IAAK,QAAO,KAAA;CACjB,IAAI;AACJ,KAAI;AACF,SAAO,IAAI,IAAI,IAAI,CAAC,SAAS,aAAa;SACpC;AACN,SAAO,IAAI,aAAa,CAAC,MAAM,IAAI,CAAC,MAAM;;CAE5C,MAAM,MAAM,KAAK,YAAY,IAAI;AACjC,QAAO,QAAQ,KAAK,KAAA,IAAY,4BAA4B,KAAK,MAAM,IAAI;;;;;;;;;;;;AAwB7E,SAAgB,mBACd,MACA,YACiB;CACjB,MAAM,QAAQ,KAAK,MAAM,QAAQ;CAGjC,MAAM,UAAU,WAAW;CAG3B,MAAM,WAAsB,EAAE;CAC9B,IAAI;CACJ,IAAI;CAEJ,IAAI,kBAAkB;CACtB,IAAI;CACJ,IAAI,cAAc;CAClB,IAAI,eAAe;CACnB,IAAI;AAEJ,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,MAAM;AAE3B,MAAI,CAAC,WAAY,QAAQ,WAAW,IAAI,IAAI,CAAC,QAAQ,WAAW,OAAO,CACrE;AAGF,MACE,YAAY,aACZ,QAAQ,WAAW,kBAAkB,IACrC,QAAQ,WAAW,yBAAyB,IAC5C,QAAQ,WAAW,wBAAwB,IAC3C,QAAQ,WAAW,8BAA8B,CAEjD;EAIF,MAAM,WAAW,SAAS,SAAS,YAAY;AAC/C,MAAI,UAAU;GACZ,MAAM,MAAM,SAAS,IAAI,MAAM;AAC/B,OAAI,KAAK;AACP,qBAAiB,WAAW,KAAK,QAAQ;IACzC,MAAM,eAAe,SAAS,IAAI,YAAY;AAC9C,QAAI,aACF,wBAAuB,eAAe,cAAc,EAAE,IAAI,KAAA;;AAG9D;;AAIF,MAAI,QAAQ,WAAW,WAAW,EAAE;AAClC,qBAAkB,oBAAoB,QAAQ,MAAM,EAAE,CAAC;AACvD;;AAIF,MAAI,QAAQ,WAAW,oBAAoB,EAAE;AAC3C,sBAAmB,eAAe,QAAQ,MAAM,GAAG,EAAE,qBAAqB,IAAI,KAAA;AAC9E;;AAGF,MAAI,YAAY,iBACd;AAIF,MAAI,CAAC,QAAQ,WAAW,IAAI,IAAI,kBAAkB,GAAG;GACnD,MAAM,UAAmB;IACvB,IAAI,WAAW;IACf,KAAK,WAAW,SAAS,QAAQ;IACjC,UAAU;IACV,WAAW;IACZ;AAED,OAAI,kBAAkB;AACpB,YAAQ,YAAY;AACpB,2BAAuB,iBAAiB,MAAM;SAE9C,wBAAuB,KAAA;AAGzB,YAAS,KAAK,QAAQ;AACtB,kBAAe;AACf;AAEA,qBAAkB;AAClB,sBAAmB,KAAA;;;CAIvB,MAAM,gBAAgB;CAGtB,MAAM,iBACJ,WAAW,SAAS,UAAU,CAAC,iBAC3B,KAAA,IACA,iBACE;EAAE,KAAK;EAAgB,GAAI,uBAAuB,EAAE,WAAW,sBAAsB,GAAG,EAAE;EAAG,GAC7F,EAAE,KAAK,IAAI;CAQnB,MAAM,oBAAoB,iBAAiB,KAAA,IAAY,yBAAyB,SAAS,IAAI,IAAI;CACjG,MAAM,WAAW,WAAW,SAAS,UAAU,oBAAoB,oBAAoB,WAAW;AAIlG,QAAO;EACL,GAAG;EACH;EACA,WAAW;EACX,UAAU;EACV;EACA;EACD"}
|
|
@@ -37,7 +37,8 @@ function parseMultivariantPlaylist(text, unresolved) {
|
|
|
37
37
|
language: mediaAttrs.get("LANGUAGE"),
|
|
38
38
|
uri: uri ? resolveUrl(uri, baseUrl) : void 0,
|
|
39
39
|
default: mediaAttrs.getBool("DEFAULT"),
|
|
40
|
-
autoselect: mediaAttrs.getBool("AUTOSELECT")
|
|
40
|
+
autoselect: mediaAttrs.getBool("AUTOSELECT"),
|
|
41
|
+
channels: mediaAttrs.getInt("CHANNELS")
|
|
41
42
|
});
|
|
42
43
|
}
|
|
43
44
|
if (type === "SUBTITLES" && groupId && name) {
|
|
@@ -85,7 +86,14 @@ function parseMultivariantPlaylist(text, unresolved) {
|
|
|
85
86
|
else videoStreams.push(stream);
|
|
86
87
|
else videoStreams.push(stream);
|
|
87
88
|
}
|
|
88
|
-
const
|
|
89
|
+
const videoTracksByUrl = /* @__PURE__ */ new Map();
|
|
90
|
+
for (const stream of videoStreams) {
|
|
91
|
+
const existing = videoTracksByUrl.get(stream.uri);
|
|
92
|
+
if (existing) {
|
|
93
|
+
if (stream.audioGroupId && !existing.audioGroupIds?.includes(stream.audioGroupId)) existing.audioGroupIds = [...existing.audioGroupIds ?? [], stream.audioGroupId];
|
|
94
|
+
if (stream.bandwidth < existing.bandwidth) existing.bandwidth = stream.bandwidth;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
89
97
|
const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
|
|
90
98
|
const track = {
|
|
91
99
|
type: "video",
|
|
@@ -99,9 +107,10 @@ function parseMultivariantPlaylist(text, unresolved) {
|
|
|
99
107
|
if (stream.resolution?.height !== void 0) track.height = stream.resolution.height;
|
|
100
108
|
if (codecs?.video) track.codecs = [codecs.video];
|
|
101
109
|
if (stream.frameRate) track.frameRate = stream.frameRate;
|
|
102
|
-
if (stream.audioGroupId) track.
|
|
103
|
-
|
|
104
|
-
}
|
|
110
|
+
if (stream.audioGroupId) track.audioGroupIds = [stream.audioGroupId];
|
|
111
|
+
videoTracksByUrl.set(stream.uri, track);
|
|
112
|
+
}
|
|
113
|
+
const videoTracks = [...videoTracksByUrl.values()];
|
|
105
114
|
const audioOnlyTracks = audioOnlyStreams.map((stream) => {
|
|
106
115
|
const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
|
|
107
116
|
return {
|
|
@@ -135,7 +144,7 @@ function parseMultivariantPlaylist(text, unresolved) {
|
|
|
135
144
|
mimeType: "audio/mp4",
|
|
136
145
|
bandwidth: 0,
|
|
137
146
|
sampleRate: 48e3,
|
|
138
|
-
channels: 2,
|
|
147
|
+
channels: rendition.channels ?? 2,
|
|
139
148
|
codecs: []
|
|
140
149
|
};
|
|
141
150
|
if (rendition.language) track.language = rendition.language;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-multivariant.js","names":[],"sources":["../../../../src/media/hls/parse-multivariant.ts"],"sourcesContent":["import { generateId } from '@videojs/utils/string';\nimport type {\n AddressableObject,\n AudioSelectionSet,\n AudioSwitchingSet,\n FrameRate,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedVideoTrack,\n Presentation,\n SelectionSet,\n TextSelectionSet,\n TextSwitchingSet,\n VideoSelectionSet,\n VideoSwitchingSet,\n} from '../types';\nimport { matchTag, parseCodecs } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/**\n * Parse HLS multivariant playlist into a Presentation.\n *\n * Returns Presentation with partially resolved tracks (no segment information).\n * Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs)\n * but segment information is added when media playlists are fetched.\n *\n * @param text - Raw playlist text content\n * @param unresolved - Unresolved presentation (contains URL for base URL resolution)\n * @returns Presentation with partially resolved tracks (duration is undefined)\n */\nexport function parseMultivariantPlaylist(text: string, unresolved: AddressableObject): Presentation {\n const baseUrl = unresolved.url;\n const lines = text.split(/\\r?\\n/);\n\n // Intermediate parsing structures\n interface StreamInfo {\n uri: string;\n bandwidth: number;\n resolution?: { width: number; height: number } | undefined;\n codecs?: string | undefined;\n frameRate?: FrameRate | undefined;\n audioGroupId?: string | undefined;\n }\n\n interface AudioRenditionInfo {\n groupId: string;\n name: string;\n language?: string | undefined;\n uri?: string | undefined;\n default?: boolean | undefined;\n autoselect?: boolean | undefined;\n }\n\n interface SubtitleRenditionInfo {\n groupId: string;\n name: string;\n language?: string | undefined;\n uri: string;\n default?: boolean | undefined;\n autoselect?: boolean | undefined;\n forced?: boolean | undefined;\n }\n\n const streams: StreamInfo[] = [];\n const audioRenditions: AudioRenditionInfo[] = [];\n const subtitleRenditions: SubtitleRenditionInfo[] = [];\n\n // State for STREAM-INF parsing (URI follows on next line)\n let pendingStreamInfo: Omit<StreamInfo, 'uri'> | null = null;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip empty lines and comments\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#EXT'))) {\n continue;\n }\n\n // Skip tags not used in Presentation model\n if (\n trimmed === '#EXTM3U' ||\n trimmed.startsWith('#EXT-X-VERSION:') ||\n trimmed.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')\n ) {\n continue;\n }\n\n // #EXT-X-MEDIA:TYPE=AUDIO/SUBTITLES\n const mediaAttrs = matchTag(trimmed, 'EXT-X-MEDIA');\n if (mediaAttrs) {\n const type = mediaAttrs.get('TYPE');\n const groupId = mediaAttrs.get('GROUP-ID');\n const name = mediaAttrs.get('NAME');\n\n if (type === 'AUDIO' && groupId && name) {\n const uri = mediaAttrs.get('URI');\n audioRenditions.push({\n groupId,\n name,\n language: mediaAttrs.get('LANGUAGE'),\n uri: uri ? resolveUrl(uri, baseUrl) : undefined,\n default: mediaAttrs.getBool('DEFAULT'),\n autoselect: mediaAttrs.getBool('AUTOSELECT'),\n });\n }\n\n if (type === 'SUBTITLES' && groupId && name) {\n const uri = mediaAttrs.get('URI');\n // URI is required for subtitle tracks\n if (uri) {\n subtitleRenditions.push({\n groupId,\n name,\n language: mediaAttrs.get('LANGUAGE'),\n uri: resolveUrl(uri, baseUrl),\n default: mediaAttrs.getBool('DEFAULT'),\n autoselect: mediaAttrs.getBool('AUTOSELECT'),\n forced: mediaAttrs.getBool('FORCED'),\n });\n }\n }\n continue;\n }\n\n // #EXT-X-STREAM-INF:BANDWIDTH=...\n const streamInfAttrs = matchTag(trimmed, 'EXT-X-STREAM-INF');\n if (streamInfAttrs) {\n pendingStreamInfo = {\n bandwidth: streamInfAttrs.getInt('BANDWIDTH', 0)!,\n resolution: streamInfAttrs.getResolution('RESOLUTION'),\n codecs: streamInfAttrs.get('CODECS'),\n frameRate: streamInfAttrs.getFrameRate('FRAME-RATE'),\n audioGroupId: streamInfAttrs.get('AUDIO'),\n };\n continue;\n }\n\n // URI line following STREAM-INF\n if (!trimmed.startsWith('#') && pendingStreamInfo) {\n streams.push({\n ...pendingStreamInfo,\n uri: resolveUrl(trimmed, baseUrl),\n });\n pendingStreamInfo = null;\n }\n }\n\n // Separate streams into video and audio based on codecs\n // If CODECS has single codec, use parseCodecs to determine type\n const videoStreams: typeof streams = [];\n const audioOnlyStreams: typeof streams = [];\n\n for (const stream of streams) {\n if (!stream.codecs) {\n // No codecs - assume video (default behavior)\n videoStreams.push(stream);\n continue;\n }\n\n const parsedCodecs = parseCodecs(stream.codecs);\n const codecCount = stream.codecs.split(',').length;\n\n // Single codec - determine type from parseCodecs result\n if (codecCount === 1) {\n if (parsedCodecs.audio && !parsedCodecs.video) {\n // Audio-only stream\n audioOnlyStreams.push(stream);\n } else {\n // Video stream (or unknown - default to video)\n videoStreams.push(stream);\n }\n } else {\n // Multiple codecs - video stream with muxed audio\n videoStreams.push(stream);\n }\n }\n\n // Build PartiallyResolvedVideoTracks from video streams\n const videoTracks: PartiallyResolvedVideoTrack[] = videoStreams.map((stream) => {\n const codecs = stream.codecs ? parseCodecs(stream.codecs) : undefined;\n\n const track: PartiallyResolvedVideoTrack = {\n type: 'video' as const,\n id: generateId(),\n url: stream.uri,\n bandwidth: stream.bandwidth,\n // Type-specific defaults (CMAF video)\n mimeType: 'video/mp4',\n codecs: [],\n };\n\n if (stream.resolution?.width !== undefined) {\n track.width = stream.resolution.width;\n }\n if (stream.resolution?.height !== undefined) {\n track.height = stream.resolution.height;\n }\n if (codecs?.video) {\n track.codecs = [codecs.video];\n }\n if (stream.frameRate) {\n track.frameRate = stream.frameRate;\n }\n if (stream.audioGroupId) {\n track.audioGroupId = stream.audioGroupId;\n }\n\n return track;\n });\n\n // Build PartiallyResolvedAudioTracks from audio-only streams\n const audioOnlyTracks: PartiallyResolvedAudioTrack[] = audioOnlyStreams.map((stream) => {\n const codecs = stream.codecs ? parseCodecs(stream.codecs) : undefined;\n\n const track: PartiallyResolvedAudioTrack = {\n type: 'audio' as const,\n id: generateId(),\n url: stream.uri,\n bandwidth: stream.bandwidth,\n mimeType: 'audio/mp4',\n codecs: codecs?.audio ? [codecs.audio] : [],\n groupId: stream.audioGroupId || 'default',\n name: 'Default',\n sampleRate: 48000, // Default - will be in media playlist if available\n channels: 2, // Default - will be in media playlist if available\n };\n\n return track;\n });\n\n // Build PartiallyResolvedAudioTracks from audio renditions (EXT-X-MEDIA)\n // Extract audio codecs from referencing streams\n const audioRenditionTracks: PartiallyResolvedAudioTrack[] = audioRenditions.map((rendition) => {\n let audioCodecs: string[] | undefined;\n for (const stream of streams) {\n if (stream.audioGroupId === rendition.groupId && stream.codecs) {\n const codecs = parseCodecs(stream.codecs);\n if (codecs.audio) {\n audioCodecs = [codecs.audio];\n break;\n }\n }\n }\n\n const track: PartiallyResolvedAudioTrack = {\n type: 'audio' as const,\n id: generateId(),\n url: rendition.uri ?? '',\n groupId: rendition.groupId,\n name: rendition.name,\n // Type-specific defaults (CMAF audio)\n mimeType: 'audio/mp4',\n bandwidth: 0, // Not available in multivariant for demuxed audio\n sampleRate: 48000, // CMAF default\n channels: 2, // Stereo default\n codecs: [],\n };\n\n if (rendition.language) {\n track.language = rendition.language;\n }\n if (audioCodecs) {\n track.codecs = audioCodecs;\n }\n if (rendition.default) {\n track.default = rendition.default;\n }\n if (rendition.autoselect) {\n track.autoselect = rendition.autoselect;\n }\n\n return track;\n });\n\n // Combine audio tracks from both EXT-X-MEDIA renditions and audio-only STREAM-INF\n const audioTracks = [...audioRenditionTracks, ...audioOnlyTracks];\n\n // Build PartiallyResolvedTextTracks from subtitle renditions\n const textTracks: PartiallyResolvedTextTrack[] = subtitleRenditions.map((rendition) => {\n const track: PartiallyResolvedTextTrack = {\n type: 'text' as const,\n id: generateId(),\n url: rendition.uri,\n groupId: rendition.groupId,\n label: rendition.name,\n kind: 'subtitles' as const,\n // Type-specific defaults (VTT)\n mimeType: 'text/vtt',\n bandwidth: 0, // Text tracks don't consume bandwidth\n };\n\n if (rendition.language) {\n track.language = rendition.language;\n }\n // Match hls.js/http-streaming: only set default=true when BOTH DEFAULT=YES AND AUTOSELECT=YES\n if (rendition.default && rendition.autoselect) {\n track.default = true;\n }\n if (rendition.autoselect) {\n track.autoselect = rendition.autoselect;\n }\n if (rendition.forced) {\n track.forced = rendition.forced;\n }\n\n return track;\n });\n\n // Build selection sets\n const selectionSets: SelectionSet[] = [];\n\n if (videoTracks.length > 0) {\n const videoSwitchingSet: VideoSwitchingSet = {\n id: generateId(),\n type: 'video',\n tracks: videoTracks,\n };\n\n const videoSelectionSet: VideoSelectionSet = {\n id: generateId(),\n type: 'video',\n switchingSets: [videoSwitchingSet],\n };\n\n selectionSets.push(videoSelectionSet);\n }\n\n if (audioTracks.length > 0) {\n const audioSwitchingSet: AudioSwitchingSet = {\n id: generateId(),\n type: 'audio',\n tracks: audioTracks,\n };\n\n const audioSelectionSet: AudioSelectionSet = {\n id: generateId(),\n type: 'audio',\n switchingSets: [audioSwitchingSet],\n };\n\n selectionSets.push(audioSelectionSet);\n }\n\n if (textTracks.length > 0) {\n const textSwitchingSet: TextSwitchingSet = {\n id: generateId(),\n type: 'text',\n tracks: textTracks,\n };\n\n const textSelectionSet: TextSelectionSet = {\n id: generateId(),\n type: 'text',\n switchingSets: [textSwitchingSet],\n };\n\n selectionSets.push(textSelectionSet);\n }\n\n // Build presentation (duration is undefined until tracks are resolved)\n return {\n id: generateId(),\n url: unresolved.url,\n startTime: 0,\n // duration: undefined, // Won't be known until after at least one media playlist is fetched + parsed\n selectionSets,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"parse-multivariant.js","names":[],"sources":["../../../../src/media/hls/parse-multivariant.ts"],"sourcesContent":["import { generateId } from '@videojs/utils/string';\nimport type {\n AddressableObject,\n AudioSelectionSet,\n AudioSwitchingSet,\n FrameRate,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedVideoTrack,\n Presentation,\n SelectionSet,\n TextSelectionSet,\n TextSwitchingSet,\n VideoSelectionSet,\n VideoSwitchingSet,\n} from '../types';\nimport { matchTag, parseCodecs } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/**\n * Parse HLS multivariant playlist into a Presentation.\n *\n * Returns Presentation with partially resolved tracks (no segment information).\n * Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs)\n * but segment information is added when media playlists are fetched.\n *\n * @param text - Raw playlist text content\n * @param unresolved - Unresolved presentation (contains URL for base URL resolution)\n * @returns Presentation with partially resolved tracks (duration is undefined)\n */\nexport function parseMultivariantPlaylist(text: string, unresolved: AddressableObject): Presentation {\n const baseUrl = unresolved.url;\n const lines = text.split(/\\r?\\n/);\n\n // Intermediate parsing structures\n interface StreamInfo {\n uri: string;\n bandwidth: number;\n resolution?: { width: number; height: number } | undefined;\n codecs?: string | undefined;\n frameRate?: FrameRate | undefined;\n audioGroupId?: string | undefined;\n }\n\n interface AudioRenditionInfo {\n groupId: string;\n name: string;\n language?: string | undefined;\n uri?: string | undefined;\n default?: boolean | undefined;\n autoselect?: boolean | undefined;\n channels?: number | undefined;\n }\n\n interface SubtitleRenditionInfo {\n groupId: string;\n name: string;\n language?: string | undefined;\n uri: string;\n default?: boolean | undefined;\n autoselect?: boolean | undefined;\n forced?: boolean | undefined;\n }\n\n const streams: StreamInfo[] = [];\n const audioRenditions: AudioRenditionInfo[] = [];\n const subtitleRenditions: SubtitleRenditionInfo[] = [];\n\n // State for STREAM-INF parsing (URI follows on next line)\n let pendingStreamInfo: Omit<StreamInfo, 'uri'> | null = null;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip empty lines and comments\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#EXT'))) {\n continue;\n }\n\n // Skip tags not used in Presentation model\n if (\n trimmed === '#EXTM3U' ||\n trimmed.startsWith('#EXT-X-VERSION:') ||\n trimmed.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')\n ) {\n continue;\n }\n\n // #EXT-X-MEDIA:TYPE=AUDIO/SUBTITLES\n const mediaAttrs = matchTag(trimmed, 'EXT-X-MEDIA');\n if (mediaAttrs) {\n const type = mediaAttrs.get('TYPE');\n const groupId = mediaAttrs.get('GROUP-ID');\n const name = mediaAttrs.get('NAME');\n\n if (type === 'AUDIO' && groupId && name) {\n const uri = mediaAttrs.get('URI');\n audioRenditions.push({\n groupId,\n name,\n language: mediaAttrs.get('LANGUAGE'),\n uri: uri ? resolveUrl(uri, baseUrl) : undefined,\n default: mediaAttrs.getBool('DEFAULT'),\n autoselect: mediaAttrs.getBool('AUTOSELECT'),\n // CHANNELS is a quoted string whose first parameter is the channel\n // count (\"6\", or \"16/JOC\" for spatial audio); getInt reads the\n // leading integer.\n channels: mediaAttrs.getInt('CHANNELS'),\n });\n }\n\n if (type === 'SUBTITLES' && groupId && name) {\n const uri = mediaAttrs.get('URI');\n // URI is required for subtitle tracks\n if (uri) {\n subtitleRenditions.push({\n groupId,\n name,\n language: mediaAttrs.get('LANGUAGE'),\n uri: resolveUrl(uri, baseUrl),\n default: mediaAttrs.getBool('DEFAULT'),\n autoselect: mediaAttrs.getBool('AUTOSELECT'),\n forced: mediaAttrs.getBool('FORCED'),\n });\n }\n }\n continue;\n }\n\n // #EXT-X-STREAM-INF:BANDWIDTH=...\n const streamInfAttrs = matchTag(trimmed, 'EXT-X-STREAM-INF');\n if (streamInfAttrs) {\n pendingStreamInfo = {\n bandwidth: streamInfAttrs.getInt('BANDWIDTH', 0)!,\n resolution: streamInfAttrs.getResolution('RESOLUTION'),\n codecs: streamInfAttrs.get('CODECS'),\n frameRate: streamInfAttrs.getFrameRate('FRAME-RATE'),\n audioGroupId: streamInfAttrs.get('AUDIO'),\n };\n continue;\n }\n\n // URI line following STREAM-INF\n if (!trimmed.startsWith('#') && pendingStreamInfo) {\n streams.push({\n ...pendingStreamInfo,\n uri: resolveUrl(trimmed, baseUrl),\n });\n pendingStreamInfo = null;\n }\n }\n\n // Separate streams into video and audio based on codecs\n // If CODECS has single codec, use parseCodecs to determine type\n const videoStreams: typeof streams = [];\n const audioOnlyStreams: typeof streams = [];\n\n for (const stream of streams) {\n if (!stream.codecs) {\n // No codecs - assume video (default behavior)\n videoStreams.push(stream);\n continue;\n }\n\n const parsedCodecs = parseCodecs(stream.codecs);\n const codecCount = stream.codecs.split(',').length;\n\n // Single codec - determine type from parseCodecs result\n if (codecCount === 1) {\n if (parsedCodecs.audio && !parsedCodecs.video) {\n // Audio-only stream\n audioOnlyStreams.push(stream);\n } else {\n // Video stream (or unknown - default to video)\n videoStreams.push(stream);\n }\n } else {\n // Multiple codecs - video stream with muxed audio\n videoStreams.push(stream);\n }\n }\n\n // Build PartiallyResolvedVideoTracks from video streams, de-duplicating the\n // HLS cross-product: one video rendition is listed across several\n // `EXT-X-STREAM-INF` entries — one per audio group it can pair with, all\n // sharing the same media-playlist URI. Collapse them to one track per URI,\n // accumulating every advertised audio group. (Redundant-stream renditions\n // live at *distinct* per-CDN URIs, so they stay separate — only the same-URI\n // cross-product merges.)\n const videoTracksByUrl = new Map<string, PartiallyResolvedVideoTrack>();\n for (const stream of videoStreams) {\n const existing = videoTracksByUrl.get(stream.uri);\n if (existing) {\n if (stream.audioGroupId && !existing.audioGroupIds?.includes(stream.audioGroupId)) {\n existing.audioGroupIds = [...(existing.audioGroupIds ?? []), stream.audioGroupId];\n }\n // BANDWIDTH is video + audio combined; the duplicates differ only in the\n // paired audio. Keep the lowest as the closest proxy to video-only, which\n // is what ABR should rank on.\n if (stream.bandwidth < existing.bandwidth) {\n existing.bandwidth = stream.bandwidth;\n }\n continue;\n }\n\n const codecs = stream.codecs ? parseCodecs(stream.codecs) : undefined;\n\n const track: PartiallyResolvedVideoTrack = {\n type: 'video' as const,\n id: generateId(),\n url: stream.uri,\n bandwidth: stream.bandwidth,\n // Type-specific defaults (CMAF video)\n mimeType: 'video/mp4',\n codecs: [],\n };\n\n if (stream.resolution?.width !== undefined) {\n track.width = stream.resolution.width;\n }\n if (stream.resolution?.height !== undefined) {\n track.height = stream.resolution.height;\n }\n if (codecs?.video) {\n track.codecs = [codecs.video];\n }\n if (stream.frameRate) {\n track.frameRate = stream.frameRate;\n }\n if (stream.audioGroupId) {\n track.audioGroupIds = [stream.audioGroupId];\n }\n\n videoTracksByUrl.set(stream.uri, track);\n }\n const videoTracks: PartiallyResolvedVideoTrack[] = [...videoTracksByUrl.values()];\n\n // Build PartiallyResolvedAudioTracks from audio-only streams\n const audioOnlyTracks: PartiallyResolvedAudioTrack[] = audioOnlyStreams.map((stream) => {\n const codecs = stream.codecs ? parseCodecs(stream.codecs) : undefined;\n\n const track: PartiallyResolvedAudioTrack = {\n type: 'audio' as const,\n id: generateId(),\n url: stream.uri,\n bandwidth: stream.bandwidth,\n mimeType: 'audio/mp4',\n codecs: codecs?.audio ? [codecs.audio] : [],\n groupId: stream.audioGroupId || 'default',\n name: 'Default',\n sampleRate: 48000, // Default - will be in media playlist if available\n channels: 2, // Default - will be in media playlist if available\n };\n\n return track;\n });\n\n // Build PartiallyResolvedAudioTracks from audio renditions (EXT-X-MEDIA)\n // Extract audio codecs from referencing streams\n const audioRenditionTracks: PartiallyResolvedAudioTrack[] = audioRenditions.map((rendition) => {\n let audioCodecs: string[] | undefined;\n for (const stream of streams) {\n if (stream.audioGroupId === rendition.groupId && stream.codecs) {\n const codecs = parseCodecs(stream.codecs);\n if (codecs.audio) {\n audioCodecs = [codecs.audio];\n break;\n }\n }\n }\n\n const track: PartiallyResolvedAudioTrack = {\n type: 'audio' as const,\n id: generateId(),\n url: rendition.uri ?? '',\n groupId: rendition.groupId,\n name: rendition.name,\n // Type-specific defaults (CMAF audio)\n mimeType: 'audio/mp4',\n bandwidth: 0, // Not available in multivariant for demuxed audio\n sampleRate: 48000, // CMAF default\n channels: rendition.channels ?? 2, // From EXT-X-MEDIA CHANNELS; stereo default\n codecs: [],\n };\n\n if (rendition.language) {\n track.language = rendition.language;\n }\n if (audioCodecs) {\n track.codecs = audioCodecs;\n }\n if (rendition.default) {\n track.default = rendition.default;\n }\n if (rendition.autoselect) {\n track.autoselect = rendition.autoselect;\n }\n\n return track;\n });\n\n // Combine audio tracks from both EXT-X-MEDIA renditions and audio-only STREAM-INF\n const audioTracks = [...audioRenditionTracks, ...audioOnlyTracks];\n\n // Build PartiallyResolvedTextTracks from subtitle renditions\n const textTracks: PartiallyResolvedTextTrack[] = subtitleRenditions.map((rendition) => {\n const track: PartiallyResolvedTextTrack = {\n type: 'text' as const,\n id: generateId(),\n url: rendition.uri,\n groupId: rendition.groupId,\n label: rendition.name,\n kind: 'subtitles' as const,\n // Type-specific defaults (VTT)\n mimeType: 'text/vtt',\n bandwidth: 0, // Text tracks don't consume bandwidth\n };\n\n if (rendition.language) {\n track.language = rendition.language;\n }\n // Match hls.js/http-streaming: only set default=true when BOTH DEFAULT=YES AND AUTOSELECT=YES\n if (rendition.default && rendition.autoselect) {\n track.default = true;\n }\n if (rendition.autoselect) {\n track.autoselect = rendition.autoselect;\n }\n if (rendition.forced) {\n track.forced = rendition.forced;\n }\n\n return track;\n });\n\n // Build selection sets\n const selectionSets: SelectionSet[] = [];\n\n if (videoTracks.length > 0) {\n const videoSwitchingSet: VideoSwitchingSet = {\n id: generateId(),\n type: 'video',\n tracks: videoTracks,\n };\n\n const videoSelectionSet: VideoSelectionSet = {\n id: generateId(),\n type: 'video',\n switchingSets: [videoSwitchingSet],\n };\n\n selectionSets.push(videoSelectionSet);\n }\n\n if (audioTracks.length > 0) {\n const audioSwitchingSet: AudioSwitchingSet = {\n id: generateId(),\n type: 'audio',\n tracks: audioTracks,\n };\n\n const audioSelectionSet: AudioSelectionSet = {\n id: generateId(),\n type: 'audio',\n switchingSets: [audioSwitchingSet],\n };\n\n selectionSets.push(audioSelectionSet);\n }\n\n if (textTracks.length > 0) {\n const textSwitchingSet: TextSwitchingSet = {\n id: generateId(),\n type: 'text',\n tracks: textTracks,\n };\n\n const textSelectionSet: TextSelectionSet = {\n id: generateId(),\n type: 'text',\n switchingSets: [textSwitchingSet],\n };\n\n selectionSets.push(textSelectionSet);\n }\n\n // Build presentation (duration is undefined until tracks are resolved)\n return {\n id: generateId(),\n url: unresolved.url,\n startTime: 0,\n // duration: undefined, // Won't be known until after at least one media playlist is fetched + parsed\n selectionSets,\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { MaybeResolvedPresentation } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
//#region src/media/primitives/select-tracks.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for video track selection.
|
|
6
|
+
*/
|
|
7
|
+
interface VideoSelectionConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Initial bandwidth estimate for cold start (bits per second).
|
|
10
|
+
* Used to select video quality before we have real measurements.
|
|
11
|
+
* Default: 1 Mbps (conservative).
|
|
12
|
+
*/
|
|
13
|
+
initialBandwidth?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Safety margin for quality selection (0-1).
|
|
16
|
+
* Default: 0.85 (15% headroom).
|
|
17
|
+
*/
|
|
18
|
+
safetyMargin?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Contract for a track picker — a pure function that consults a
|
|
22
|
+
* presentation (and optional config) and returns the id of the track to
|
|
23
|
+
* select, or `undefined` to leave the slot unset.
|
|
24
|
+
*
|
|
25
|
+
* Behaviors that own a track-selection slot (`selectAudioTrack`,
|
|
26
|
+
* `selectVideoTrack`, `switchVideoTrack`) accept a
|
|
27
|
+
* `TrackPicker` via config. The behavior passes its own config straight
|
|
28
|
+
* through as the picker's second argument — pickers that need richer
|
|
29
|
+
* options (language preferences, default-track filtering, bandwidth-aware
|
|
30
|
+
* selection) read from `config`; pickers that don't (e.g., first-track)
|
|
31
|
+
* ignore it.
|
|
32
|
+
*/
|
|
33
|
+
type TrackPicker<Config = unknown> = (presentation: MaybeResolvedPresentation, config?: Config) => string | undefined;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { TrackPicker, VideoSelectionConfig };
|
|
36
|
+
//# sourceMappingURL=select-tracks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select-tracks.d.ts","names":[],"sources":["../../../../src/media/primitives/select-tracks.ts"],"mappings":";;;;;;UA0CiB,oBAAA;;;;;;EAMf,gBAAA;;;;;EAMA,YAAA;AAAA;;;;;;;;;;;;;;KA8DU,WAAA,sBACV,YAAA,EAAc,yBAAA,EACd,MAAA,GAAS,MAAA"}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
//#region src/media/primitives/select-tracks.ts
|
|
2
2
|
/**
|
|
3
|
+
* Test whether a track matches a partial-track description: every present,
|
|
4
|
+
* defined field of `filter` equals the track's. Absent or `undefined` filter
|
|
5
|
+
* fields don't constrain. Used to narrow candidates by a user selection
|
|
6
|
+
* (`{ id }`, `{ language }`, `{ height }`, …).
|
|
7
|
+
*
|
|
8
|
+
* @param track - The track to test
|
|
9
|
+
* @param filter - Partial-track description; only present, defined fields constrain
|
|
10
|
+
* @returns `true` when the track matches every constraining field
|
|
11
|
+
*/
|
|
12
|
+
function matchesPartialTrack(track, filter) {
|
|
13
|
+
for (const key in filter) {
|
|
14
|
+
const filterValue = filter[key];
|
|
15
|
+
if (filterValue !== void 0 && track[key] !== filterValue) return false;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
3
20
|
* Pick the first track of the given type from a presentation.
|
|
4
21
|
*
|
|
5
22
|
* Returns the first track in the first switching set of the matching
|
|
@@ -12,21 +29,89 @@ function pickFirstTrackId(presentation, type) {
|
|
|
12
29
|
return presentation.selectionSets?.find((set) => set.type === type)?.switchingSets[0]?.tracks[0]?.id;
|
|
13
30
|
}
|
|
14
31
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
32
|
+
* Translates a "max resolution" into a total total pixel area
|
|
33
|
+
* for comparisons with video track resolutions with an assumed
|
|
34
|
+
* 16:9 ratio.
|
|
35
|
+
*
|
|
36
|
+
* Example: "720p" translates to a 921600 pixel area.
|
|
37
|
+
*
|
|
38
|
+
* Because 720 * 1280 = 720 * (720 * (16/9) ) = 921_600
|
|
39
|
+
*
|
|
40
|
+
* Accepts:
|
|
41
|
+
* - string with the format '{height}p'. ('720p')
|
|
42
|
+
* - bare number, interpreted as pixel area. (921_600)
|
|
43
|
+
* - anything else will translate to `+Infinity`, meaning no cap specified
|
|
44
|
+
*/
|
|
45
|
+
function maxResolutionToPixelArea(value) {
|
|
46
|
+
if (value === void 0 || value === null) return Number.POSITIVE_INFINITY;
|
|
47
|
+
if (typeof value === "number") return Number.isFinite(value) && value > 0 ? value : Number.POSITIVE_INFINITY;
|
|
48
|
+
const match = value.trim().match(/^(\d+)p?$/i);
|
|
49
|
+
if (!match) return Number.POSITIVE_INFINITY;
|
|
50
|
+
const height = Number(match[1]);
|
|
51
|
+
if (!(Number.isFinite(height) && height > 0)) return Number.POSITIVE_INFINITY;
|
|
52
|
+
return height * height * 16 / 9;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Pick the track with the highest pixel area at or below `maxPixelArea`.
|
|
56
|
+
* Falls back to the lowest track when nothing satisfies the cap (the
|
|
57
|
+
* lowest of the above-cap set is the closest to the cap from above).
|
|
58
|
+
* Tiebreak on bandwidth. Missing dimensions are treated as area `0`.
|
|
59
|
+
*/
|
|
60
|
+
function pickTrackUnderPixelArea(tracks, maxPixelArea = Number.POSITIVE_INFINITY) {
|
|
61
|
+
if (tracks.length === 0) return void 0;
|
|
62
|
+
const sorted = [...tracks].sort((a, b) => (b.width ?? 0) * (b.height ?? 0) - (a.width ?? 0) * (a.height ?? 0) || (b.bandwidth ?? 0) - (a.bandwidth ?? 0));
|
|
63
|
+
return sorted.find((t) => (t.width ?? 0) * (t.height ?? 0) <= maxPixelArea) ?? sorted[sorted.length - 1];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Pick the video track with the highest pixel area.
|
|
67
|
+
*
|
|
68
|
+
* Pair with `selectVideoTrack`; compose `switchVideoQuality` instead
|
|
69
|
+
* for runtime-adapted quality.
|
|
70
|
+
*/
|
|
71
|
+
function pickHighestResolutionVideoTrack(presentation) {
|
|
72
|
+
const tracks = (presentation.selectionSets?.find((set) => set.type === "video"))?.switchingSets[0]?.tracks;
|
|
73
|
+
if (!tracks?.length) return void 0;
|
|
74
|
+
return pickTrackUnderPixelArea(tracks)?.id;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Pick audio track.
|
|
18
78
|
*
|
|
19
|
-
* Selection priority
|
|
20
|
-
* 1.
|
|
21
|
-
* 2.
|
|
22
|
-
* 3.
|
|
79
|
+
* Selection priority:
|
|
80
|
+
* 1. First track matching preferred language (if specified)
|
|
81
|
+
* 2. First default track
|
|
82
|
+
* 3. First audio track
|
|
83
|
+
*
|
|
84
|
+
* @param presentation - Presentation with audio tracks
|
|
85
|
+
* @param config - Selection configuration (preferred language)
|
|
86
|
+
* @returns Selected audio track ID, or undefined if no audio tracks
|
|
87
|
+
*/
|
|
88
|
+
function pickAudioTrack(presentation, config) {
|
|
89
|
+
const audioSet = presentation.selectionSets?.find((set) => set.type === "audio");
|
|
90
|
+
if (!audioSet || audioSet.switchingSets.length === 0) return;
|
|
91
|
+
const switchingSet = audioSet.switchingSets[0];
|
|
92
|
+
if (!switchingSet || switchingSet.tracks.length === 0) return;
|
|
93
|
+
const tracks = switchingSet.tracks;
|
|
94
|
+
if (config?.preferredAudioLanguage) {
|
|
95
|
+
const languageMatch = tracks.find((track) => track.language === config.preferredAudioLanguage);
|
|
96
|
+
if (languageMatch) return languageMatch.id;
|
|
97
|
+
}
|
|
98
|
+
const defaultTrack = tracks.find((track) => track.default === true);
|
|
99
|
+
if (defaultTrack) return defaultTrack.id;
|
|
100
|
+
return tracks[0]?.id;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Default text-track policy over an explicit candidate list (rather than a whole
|
|
104
|
+
* presentation): the opt-in three-tier pick `pickTextTrack` delegates to, factored
|
|
105
|
+
* out so a caller that has already narrowed the candidates — a constrained,
|
|
106
|
+
* CDN-scoped track-switching chain — applies the same policy without re-deriving
|
|
107
|
+
* from the presentation.
|
|
23
108
|
*
|
|
24
|
-
*
|
|
109
|
+
* Priority: `preferredSubtitleLanguage` match → `DEFAULT=YES + AUTOSELECT=YES`
|
|
110
|
+
* (only when `enableDefaultTrack`) → `undefined` (opt-in). FORCED tracks are
|
|
111
|
+
* excluded unless `includeForcedTracks` (Apple-spec: a regular track must carry
|
|
112
|
+
* forced content when both exist, so a forced-only track is redundant).
|
|
25
113
|
*/
|
|
26
|
-
function
|
|
27
|
-
const textSet = presentation.selectionSets?.find((set) => set.type === "text");
|
|
28
|
-
if (!textSet?.switchingSets?.[0]?.tracks.length) return void 0;
|
|
29
|
-
const tracks = textSet.switchingSets[0].tracks;
|
|
114
|
+
function pickTextTrackFromTracks(tracks, config) {
|
|
30
115
|
const availableTracks = config?.includeForcedTracks ? tracks : tracks.filter((track) => !track.forced);
|
|
31
116
|
if (availableTracks.length === 0) return void 0;
|
|
32
117
|
const { preferredSubtitleLanguage, enableDefaultTrack = false } = config ?? {};
|
|
@@ -40,6 +125,6 @@ function pickTextTrack(presentation, config) {
|
|
|
40
125
|
}
|
|
41
126
|
}
|
|
42
127
|
//#endregion
|
|
43
|
-
export { pickFirstTrackId,
|
|
128
|
+
export { matchesPartialTrack, maxResolutionToPixelArea, pickAudioTrack, pickFirstTrackId, pickHighestResolutionVideoTrack, pickTextTrackFromTracks, pickTrackUnderPixelArea };
|
|
44
129
|
|
|
45
130
|
//# sourceMappingURL=select-tracks.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select-tracks.js","names":[],"sources":["../../../../src/media/primitives/select-tracks.ts"],"sourcesContent":["import { DEFAULT_QUALITY_CONFIG, selectQuality } from '../abr/quality-selection';\nimport type {\n AudioSelectionSet,\n MaybeResolvedPresentation,\n Presentation,\n TrackType,\n VideoSelectionSet,\n} from '../types';\nimport { SelectedTrackIdKeyByType } from '../utils/track-selection';\n\n/**\n * Default initial bandwidth estimate for cold start (bits per second).\n * Conservative 1 Mbps to avoid over-selecting on slow connections.\n */\nexport const DEFAULT_INITIAL_BANDWIDTH = 1_000_000;\n\n/**\n * State shape for track selection.\n */\nexport interface TrackSelectionState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/**\n * Context shape for track selection.\n * Currently empty - reserved for future use (e.g., bandwidth estimator).\n */\nexport type TrackSelectionContext = Record<string, never>;\n\n/**\n * Action types for track selection.\n * Reserved for future event-driven selection triggers.\n */\nexport type TrackSelectionAction = { type: 'presentation-loaded' };\n\n/**\n * Base configuration for track selection.\n * Generic over track type with discriminant `type` field.\n */\nexport interface TrackSelectionConfig<T extends TrackType = TrackType> {\n type: T;\n}\n\n/**\n * Configuration for video track selection.\n * Generic with default to 'video' for convenience.\n */\nexport interface VideoSelectionConfig<T extends TrackType = 'video'> extends TrackSelectionConfig<T> {\n /**\n * Initial bandwidth estimate for cold start (bits per second).\n * Used to select video quality before we have real measurements.\n * Default: 1 Mbps (conservative).\n */\n initialBandwidth?: number;\n\n /**\n * Safety margin for quality selection (0-1).\n * Default: 0.85 (15% headroom).\n */\n safetyMargin?: number;\n}\n\n/**\n * Configuration for audio track selection.\n * Generic with default to 'audio' for convenience.\n */\nexport interface AudioSelectionConfig<T extends TrackType = 'audio'> extends TrackSelectionConfig<T> {\n /**\n * Preferred audio language (ISO 639 code, e.g., \"en\", \"es\").\n * If not specified, selects first audio track.\n */\n preferredAudioLanguage?: string;\n}\n\n/**\n * Configuration for text track selection.\n *\n * Standalone (not extending `TrackSelectionConfig<'text'>`) because\n * `pickTextTrack` hardcodes the `'text'` type lookup internally — the\n * discriminant field carries no information at runtime, so we don't\n * require callers to pass it.\n */\nexport interface TextSelectionConfig {\n /**\n * Preferred subtitle language (ISO 639 code, e.g., \"en\", \"es\").\n * If specified, selects matching track if available.\n */\n preferredSubtitleLanguage?: string;\n\n /**\n * Include FORCED subtitle tracks in selection.\n * Default: false (follows hls.js/http-streaming pattern)\n *\n * Note: Per Apple's HLS spec, if content has forced and regular subtitles\n * in the same language, the regular track MUST contain both forced and\n * regular content. Therefore, forced-only tracks are redundant and excluded\n * by default.\n */\n includeForcedTracks?: boolean;\n\n /**\n * Auto-select DEFAULT track (requires DEFAULT=YES + AUTOSELECT=YES in HLS).\n * Default: false (user opt-in, matches hls.js/http-streaming)\n *\n * When enabled, tracks marked with both DEFAULT=YES and AUTOSELECT=YES\n * will be automatically selected if no user preference matches.\n */\n enableDefaultTrack?: boolean;\n}\n\n// =============================================================================\n// Helper Functions (Pure Selection Logic)\n// =============================================================================\n\n/**\n * Contract for a track picker — a pure function that consults a\n * presentation (and optional config) and returns the id of the track to\n * select, or `undefined` to leave the slot unset.\n *\n * Behaviors that own a track-selection slot (`selectAudioTrack`,\n * `selectTextTrack`, `selectVideoTrack`, `switchVideoQuality`) accept a\n * `TrackPicker` via config. The behavior passes its own config straight\n * through as the picker's second argument — pickers that need richer\n * options (language preferences, default-track filtering, bandwidth-aware\n * selection) read from `config`; pickers that don't (e.g., first-track)\n * ignore it.\n */\nexport type TrackPicker<Config = unknown> = (\n presentation: MaybeResolvedPresentation,\n config?: Config\n) => string | undefined;\n\n/**\n * Pick the first track of the given type from a presentation.\n *\n * Returns the first track in the first switching set of the matching\n * selection set, or `undefined` if either is missing. POC-shaped\n * default-pick — `pickVideoTrack` / `pickAudioTrack` honor bandwidth +\n * language preferences and will replace this once selection callers are\n * ready.\n */\nexport function pickFirstTrackId(presentation: MaybeResolvedPresentation, type: TrackType): string | undefined {\n return presentation.selectionSets?.find((set) => set.type === type)?.switchingSets[0]?.tracks[0]?.id;\n}\n\n/**\n * Pick video track using quality selection algorithm.\n *\n * Uses bandwidth-based selection with safety margin to pick\n * the highest quality track that fits available bandwidth.\n *\n * @param presentation - Presentation with video tracks\n * @param config - Selection configuration (bandwidth, safety margin)\n * @returns Selected video track ID, or undefined if no video tracks\n */\nexport function pickVideoTrack(presentation: Presentation, config: VideoSelectionConfig): string | undefined {\n const videoSet = presentation.selectionSets.find((set) => set.type === 'video') as VideoSelectionSet | undefined;\n\n if (!videoSet || videoSet.switchingSets.length === 0) {\n return undefined;\n }\n\n // Get first switching set's tracks (HLS typically has one switching set per type)\n const switchingSet = videoSet.switchingSets[0];\n if (!switchingSet || switchingSet.tracks.length === 0) {\n return undefined;\n }\n\n const initialBandwidth = config.initialBandwidth ?? DEFAULT_INITIAL_BANDWIDTH;\n const safetyMargin = config.safetyMargin ?? DEFAULT_QUALITY_CONFIG.safetyMargin;\n\n // selectQuality works with both partially resolved and resolved tracks\n const selected = selectQuality(switchingSet.tracks as any, initialBandwidth, { safetyMargin });\n\n return selected?.id;\n}\n\n/**\n * Pick audio track.\n *\n * Selection priority:\n * 1. First track matching preferred language (if specified)\n * 2. First default track\n * 3. First audio track\n *\n * @param presentation - Presentation with audio tracks\n * @param config - Selection configuration (preferred language)\n * @returns Selected audio track ID, or undefined if no audio tracks\n */\nexport function pickAudioTrack(presentation: Presentation, config: AudioSelectionConfig): string | undefined {\n const audioSet = presentation.selectionSets.find((set) => set.type === 'audio') as AudioSelectionSet | undefined;\n\n if (!audioSet || audioSet.switchingSets.length === 0) {\n return undefined;\n }\n\n // Get first switching set's tracks\n const switchingSet = audioSet.switchingSets[0];\n if (!switchingSet || switchingSet.tracks.length === 0) {\n return undefined;\n }\n\n const tracks = switchingSet.tracks;\n\n // Try preferred language first\n if (config.preferredAudioLanguage) {\n const languageMatch = tracks.find((track) => track.language === config.preferredAudioLanguage);\n if (languageMatch) {\n return languageMatch.id;\n }\n }\n\n // Try default track\n const defaultTrack = tracks.find((track) => track.default === true);\n if (defaultTrack) {\n return defaultTrack.id;\n }\n\n // Fall back to first track\n return tracks[0]?.id;\n}\n\n/**\n * Pick text track to activate. Conforms to the `TrackPicker` contract so it\n * can be used directly as a default picker for `selectTextTrack` without an\n * adapter wrapper.\n *\n * Selection priority (if enabled):\n * 1. User preference (preferredSubtitleLanguage)\n * 2. DEFAULT track (if enableDefaultTrack is true and track has DEFAULT=YES + AUTOSELECT=YES)\n * 3. No auto-selection (user opt-in)\n *\n * By default, FORCED tracks are excluded per Apple's HLS spec.\n */\nexport function pickTextTrack(\n presentation: MaybeResolvedPresentation,\n config?: TextSelectionConfig\n): string | undefined {\n const textSet = presentation.selectionSets?.find((set) => set.type === 'text');\n if (!textSet?.switchingSets?.[0]?.tracks.length) return undefined;\n\n const tracks = textSet.switchingSets[0].tracks;\n\n // Filter out FORCED tracks by default (following hls.js/http-streaming pattern)\n // Per Apple spec: regular tracks MUST contain forced content when both exist\n const availableTracks = config?.includeForcedTracks ? tracks : tracks.filter((track) => !track.forced);\n\n if (availableTracks.length === 0) return undefined;\n\n const { preferredSubtitleLanguage, enableDefaultTrack = false } = config ?? {};\n\n // 1. Preferred language match (if specified)\n if (preferredSubtitleLanguage) {\n const languageMatch = availableTracks.find((track) => track.language === preferredSubtitleLanguage);\n if (languageMatch) return languageMatch.id;\n }\n\n // 2. DEFAULT track (if enabled AND track has both DEFAULT=YES + AUTOSELECT=YES)\n // Note: Parser only sets default=true when BOTH attributes present\n if (enableDefaultTrack) {\n const defaultTrack = availableTracks.find((track) => track.default === true);\n if (defaultTrack) return defaultTrack.id;\n }\n\n // 3. User opt-in (no auto-selection)\n return undefined;\n}\n\n/**\n * Check if we can select a track of the given type.\n *\n * Returns true when:\n * - Presentation exists\n * - Has tracks of the specified type\n *\n * Generic over track type - works for video, audio, or text.\n */\nexport function canSelectTrack<T extends TrackType>(\n state: TrackSelectionState,\n config: TrackSelectionConfig<T>\n): boolean {\n return !!state?.presentation?.selectionSets?.find(({ type }) => type === config.type)?.switchingSets?.[0]?.tracks\n .length;\n}\n\n/**\n * Check if we should select a track of the given type.\n *\n * Returns true when:\n * - Track of this type is not already selected\n *\n * Generic over track type - works for video, audio, or text.\n *\n * @TODO figure out reactive model for ABR cases - right now we're only selecting\n * if we have nothing selected (CJP)\n */\nexport function shouldSelectTrack<T extends TrackType>(\n state: TrackSelectionState,\n config: TrackSelectionConfig<T>\n): boolean {\n return !state[SelectedTrackIdKeyByType[config.type]];\n}\n"],"mappings":";;;;;;;;;;AAgJA,SAAgB,iBAAiB,cAAyC,MAAqC;AAC7G,QAAO,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,KAAK,EAAE,cAAc,IAAI,OAAO,IAAI;;;;;;;;;;;;;;AA4FpG,SAAgB,cACd,cACA,QACoB;CACpB,MAAM,UAAU,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,OAAO;AAC9E,KAAI,CAAC,SAAS,gBAAgB,IAAI,OAAO,OAAQ,QAAO,KAAA;CAExD,MAAM,SAAS,QAAQ,cAAc,GAAG;CAIxC,MAAM,kBAAkB,QAAQ,sBAAsB,SAAS,OAAO,QAAQ,UAAU,CAAC,MAAM,OAAO;AAEtG,KAAI,gBAAgB,WAAW,EAAG,QAAO,KAAA;CAEzC,MAAM,EAAE,2BAA2B,qBAAqB,UAAU,UAAU,EAAE;AAG9E,KAAI,2BAA2B;EAC7B,MAAM,gBAAgB,gBAAgB,MAAM,UAAU,MAAM,aAAa,0BAA0B;AACnG,MAAI,cAAe,QAAO,cAAc;;AAK1C,KAAI,oBAAoB;EACtB,MAAM,eAAe,gBAAgB,MAAM,UAAU,MAAM,YAAY,KAAK;AAC5E,MAAI,aAAc,QAAO,aAAa"}
|
|
1
|
+
{"version":3,"file":"select-tracks.js","names":[],"sources":["../../../../src/media/primitives/select-tracks.ts"],"sourcesContent":["import { DEFAULT_QUALITY_CONFIG, selectQuality } from '../abr/quality-selection';\nimport type {\n AudioSelectionSet,\n MaybeResolvedPresentation,\n PartiallyResolvedTextTrack,\n TextTrack,\n TrackType,\n VideoSelectionSet,\n} from '../types';\nimport { SelectedTrackIdKeyByType } from '../utils/track-selection';\n\n/**\n * Default initial bandwidth estimate for cold start (bits per second).\n * Conservative 1 Mbps to avoid over-selecting on slow connections.\n */\nexport const DEFAULT_INITIAL_BANDWIDTH = 1_000_000;\n\n/**\n * State shape for track selection.\n */\nexport interface TrackSelectionState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/**\n * Context shape for track selection.\n * Currently empty - reserved for future use (e.g., bandwidth estimator).\n */\nexport type TrackSelectionContext = Record<string, never>;\n\n/**\n * Action types for track selection.\n * Reserved for future event-driven selection triggers.\n */\nexport type TrackSelectionAction = { type: 'presentation-loaded' };\n\n/**\n * Configuration for video track selection.\n */\nexport interface VideoSelectionConfig {\n /**\n * Initial bandwidth estimate for cold start (bits per second).\n * Used to select video quality before we have real measurements.\n * Default: 1 Mbps (conservative).\n */\n initialBandwidth?: number;\n\n /**\n * Safety margin for quality selection (0-1).\n * Default: 0.85 (15% headroom).\n */\n safetyMargin?: number;\n}\n\n/**\n * Configuration for audio track selection.\n */\nexport interface AudioSelectionConfig {\n /**\n * Preferred audio language (ISO 639 code, e.g., \"en\", \"es\").\n * If not specified, selects first audio track.\n */\n preferredAudioLanguage?: string;\n}\n\n/**\n * Configuration for text track selection.\n */\nexport interface TextSelectionConfig {\n /**\n * Preferred subtitle language (ISO 639 code, e.g., \"en\", \"es\").\n * If specified, selects matching track if available.\n */\n preferredSubtitleLanguage?: string;\n\n /**\n * Include FORCED subtitle tracks in selection.\n * Default: false (follows hls.js/http-streaming pattern)\n *\n * Note: Per Apple's HLS spec, if content has forced and regular subtitles\n * in the same language, the regular track MUST contain both forced and\n * regular content. Therefore, forced-only tracks are redundant and excluded\n * by default.\n */\n includeForcedTracks?: boolean;\n\n /**\n * Auto-select DEFAULT track (requires DEFAULT=YES + AUTOSELECT=YES in HLS).\n * Default: false (user opt-in, matches hls.js/http-streaming)\n *\n * When enabled, tracks marked with both DEFAULT=YES and AUTOSELECT=YES\n * will be automatically selected if no user preference matches.\n */\n enableDefaultTrack?: boolean;\n}\n\n// =============================================================================\n// Helper Functions (Pure Selection Logic)\n// =============================================================================\n\n/**\n * Contract for a track picker — a pure function that consults a\n * presentation (and optional config) and returns the id of the track to\n * select, or `undefined` to leave the slot unset.\n *\n * Behaviors that own a track-selection slot (`selectAudioTrack`,\n * `selectVideoTrack`, `switchVideoTrack`) accept a\n * `TrackPicker` via config. The behavior passes its own config straight\n * through as the picker's second argument — pickers that need richer\n * options (language preferences, default-track filtering, bandwidth-aware\n * selection) read from `config`; pickers that don't (e.g., first-track)\n * ignore it.\n */\nexport type TrackPicker<Config = unknown> = (\n presentation: MaybeResolvedPresentation,\n config?: Config\n) => string | undefined;\n\n/**\n * Test whether a track matches a partial-track description: every present,\n * defined field of `filter` equals the track's. Absent or `undefined` filter\n * fields don't constrain. Used to narrow candidates by a user selection\n * (`{ id }`, `{ language }`, `{ height }`, …).\n *\n * @param track - The track to test\n * @param filter - Partial-track description; only present, defined fields constrain\n * @returns `true` when the track matches every constraining field\n */\nexport function matchesPartialTrack<T>(track: T, filter: Partial<T>): boolean {\n for (const key in filter) {\n const filterValue = filter[key as keyof T];\n if (filterValue !== undefined && track[key as keyof T] !== filterValue) return false;\n }\n return true;\n}\n\n/**\n * Pick the first track of the given type from a presentation.\n *\n * Returns the first track in the first switching set of the matching\n * selection set, or `undefined` if either is missing. POC-shaped\n * default-pick — `pickVideoTrack` / `pickAudioTrack` honor bandwidth +\n * language preferences and will replace this once selection callers are\n * ready.\n */\nexport function pickFirstTrackId(presentation: MaybeResolvedPresentation, type: TrackType): string | undefined {\n return presentation.selectionSets?.find((set) => set.type === type)?.switchingSets[0]?.tracks[0]?.id;\n}\n\n/**\n * Pick video track using quality selection algorithm.\n *\n * Uses bandwidth-based selection with safety margin to pick\n * the highest quality track that fits available bandwidth.\n *\n * @param presentation - Presentation with video tracks\n * @param config - Selection configuration (bandwidth, safety margin)\n * @returns Selected video track ID, or undefined if no video tracks\n */\nexport function pickVideoTrack(\n presentation: MaybeResolvedPresentation,\n config?: VideoSelectionConfig\n): string | undefined {\n const videoSet = presentation.selectionSets?.find((set) => set.type === 'video') as VideoSelectionSet | undefined;\n\n if (!videoSet || videoSet.switchingSets.length === 0) {\n return undefined;\n }\n\n // Get first switching set's tracks (HLS typically has one switching set per type)\n const switchingSet = videoSet.switchingSets[0];\n if (!switchingSet || switchingSet.tracks.length === 0) {\n return undefined;\n }\n\n const initialBandwidth = config?.initialBandwidth ?? DEFAULT_INITIAL_BANDWIDTH;\n const safetyMargin = config?.safetyMargin ?? DEFAULT_QUALITY_CONFIG.safetyMargin;\n\n // selectQuality works with both partially resolved and resolved tracks\n const selected = selectQuality(switchingSet.tracks as any, { bandwidth: initialBandwidth, safetyMargin });\n\n return selected?.id;\n}\n\n/**\n * Translates a \"max resolution\" into a total total pixel area\n * for comparisons with video track resolutions with an assumed\n * 16:9 ratio.\n *\n * Example: \"720p\" translates to a 921600 pixel area.\n *\n * Because 720 * 1280 = 720 * (720 * (16/9) ) = 921_600\n *\n * Accepts:\n * - string with the format '{height}p'. ('720p')\n * - bare number, interpreted as pixel area. (921_600)\n * - anything else will translate to `+Infinity`, meaning no cap specified\n */\nexport function maxResolutionToPixelArea(value: string | number | undefined): number {\n if (value === undefined || value === null) return Number.POSITIVE_INFINITY;\n if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? value : Number.POSITIVE_INFINITY;\n const match = value.trim().match(/^(\\d+)p?$/i);\n if (!match) return Number.POSITIVE_INFINITY;\n const height = Number(match[1]);\n if (!(Number.isFinite(height) && height > 0)) return Number.POSITIVE_INFINITY;\n return (height * height * 16) / 9;\n}\n\ntype RankableTrack = { id: string; width?: number; height?: number; bandwidth?: number };\n\n/**\n * Pick the track with the highest pixel area at or below `maxPixelArea`.\n * Falls back to the lowest track when nothing satisfies the cap (the\n * lowest of the above-cap set is the closest to the cap from above).\n * Tiebreak on bandwidth. Missing dimensions are treated as area `0`.\n */\nexport function pickTrackUnderPixelArea<T extends RankableTrack>(\n tracks: readonly T[],\n maxPixelArea: number = Number.POSITIVE_INFINITY\n): T | undefined {\n if (tracks.length === 0) return undefined;\n\n // Sort descending by pixel area, bandwidth as tiebreaker. List sizes\n // are small (HLS variant counts) — no need to optimize past a sort.\n const sorted = [...tracks].sort(\n (a, b) =>\n (b.width ?? 0) * (b.height ?? 0) - (a.width ?? 0) * (a.height ?? 0) || (b.bandwidth ?? 0) - (a.bandwidth ?? 0)\n );\n\n return sorted.find((t) => (t.width ?? 0) * (t.height ?? 0) <= maxPixelArea) ?? sorted[sorted.length - 1];\n}\n\n/**\n * Pick the video track with the highest pixel area.\n *\n * Pair with `selectVideoTrack`; compose `switchVideoQuality` instead\n * for runtime-adapted quality.\n */\nexport function pickHighestResolutionVideoTrack(presentation: MaybeResolvedPresentation): string | undefined {\n const videoSet = presentation.selectionSets?.find((set) => set.type === 'video') as VideoSelectionSet | undefined;\n const tracks = videoSet?.switchingSets[0]?.tracks;\n if (!tracks?.length) return undefined;\n return pickTrackUnderPixelArea(tracks)?.id;\n}\n\n/**\n * Pick audio track.\n *\n * Selection priority:\n * 1. First track matching preferred language (if specified)\n * 2. First default track\n * 3. First audio track\n *\n * @param presentation - Presentation with audio tracks\n * @param config - Selection configuration (preferred language)\n * @returns Selected audio track ID, or undefined if no audio tracks\n */\nexport function pickAudioTrack(\n presentation: MaybeResolvedPresentation,\n config?: AudioSelectionConfig\n): string | undefined {\n const audioSet = presentation.selectionSets?.find((set) => set.type === 'audio') as AudioSelectionSet | undefined;\n\n if (!audioSet || audioSet.switchingSets.length === 0) {\n return undefined;\n }\n\n // Get first switching set's tracks\n const switchingSet = audioSet.switchingSets[0];\n if (!switchingSet || switchingSet.tracks.length === 0) {\n return undefined;\n }\n\n const tracks = switchingSet.tracks;\n\n // Try preferred language first\n if (config?.preferredAudioLanguage) {\n const languageMatch = tracks.find((track) => track.language === config.preferredAudioLanguage);\n if (languageMatch) {\n return languageMatch.id;\n }\n }\n\n // Try default track\n const defaultTrack = tracks.find((track) => track.default === true);\n if (defaultTrack) {\n return defaultTrack.id;\n }\n\n // Fall back to first track\n return tracks[0]?.id;\n}\n\n/**\n * Pick text track to activate from a presentation. Conforms to the\n * `TrackPicker` contract. The candidate-list core (`pickTextTrackFromTracks`)\n * is the opt-in default policy `switchTextTrack`'s terminal applies once it has\n * narrowed the renditions.\n *\n * Selection priority (if enabled):\n * 1. User preference (preferredSubtitleLanguage)\n * 2. DEFAULT track (if enableDefaultTrack is true and track has DEFAULT=YES + AUTOSELECT=YES)\n * 3. No auto-selection (user opt-in)\n *\n * By default, FORCED tracks are excluded per Apple's HLS spec.\n */\nexport function pickTextTrack(\n presentation: MaybeResolvedPresentation,\n config?: TextSelectionConfig\n): string | undefined {\n const tracks = presentation.selectionSets?.find((set) => set.type === 'text')?.switchingSets?.[0]?.tracks;\n if (!tracks?.length) return undefined;\n return pickTextTrackFromTracks(tracks, config);\n}\n\n/**\n * Default text-track policy over an explicit candidate list (rather than a whole\n * presentation): the opt-in three-tier pick `pickTextTrack` delegates to, factored\n * out so a caller that has already narrowed the candidates — a constrained,\n * CDN-scoped track-switching chain — applies the same policy without re-deriving\n * from the presentation.\n *\n * Priority: `preferredSubtitleLanguage` match → `DEFAULT=YES + AUTOSELECT=YES`\n * (only when `enableDefaultTrack`) → `undefined` (opt-in). FORCED tracks are\n * excluded unless `includeForcedTracks` (Apple-spec: a regular track must carry\n * forced content when both exist, so a forced-only track is redundant).\n */\nexport function pickTextTrackFromTracks(\n tracks: readonly (PartiallyResolvedTextTrack | TextTrack)[],\n config?: TextSelectionConfig\n): string | undefined {\n const availableTracks = config?.includeForcedTracks ? tracks : tracks.filter((track) => !track.forced);\n if (availableTracks.length === 0) return undefined;\n\n const { preferredSubtitleLanguage, enableDefaultTrack = false } = config ?? {};\n\n if (preferredSubtitleLanguage) {\n const languageMatch = availableTracks.find((track) => track.language === preferredSubtitleLanguage);\n if (languageMatch) return languageMatch.id;\n }\n\n if (enableDefaultTrack) {\n const defaultTrack = availableTracks.find((track) => track.default === true);\n if (defaultTrack) return defaultTrack.id;\n }\n\n return undefined;\n}\n\n/**\n * Check if we can select a track of the given type.\n *\n * Returns true when:\n * - Presentation exists\n * - Has tracks of the specified type\n *\n * Generic over track type - works for video, audio, or text.\n */\nexport function canSelectTrack(state: TrackSelectionState, type: TrackType): boolean {\n return !!state?.presentation?.selectionSets?.find((set) => set.type === type)?.switchingSets?.[0]?.tracks.length;\n}\n\n/**\n * Check if we should select a track of the given type.\n *\n * Returns true when:\n * - Track of this type is not already selected\n *\n * Generic over track type - works for video, audio, or text.\n *\n * @TODO figure out reactive model for ABR cases - right now we're only selecting\n * if we have nothing selected (CJP)\n */\nexport function shouldSelectTrack(state: TrackSelectionState, type: TrackType): boolean {\n return !state[SelectedTrackIdKeyByType[type]];\n}\n"],"mappings":";;;;;;;;;;;AAmIA,SAAgB,oBAAuB,OAAU,QAA6B;AAC5E,MAAK,MAAM,OAAO,QAAQ;EACxB,MAAM,cAAc,OAAO;AAC3B,MAAI,gBAAgB,KAAA,KAAa,MAAM,SAAoB,YAAa,QAAO;;AAEjF,QAAO;;;;;;;;;;;AAYT,SAAgB,iBAAiB,cAAyC,MAAqC;AAC7G,QAAO,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,KAAK,EAAE,cAAc,IAAI,OAAO,IAAI;;;;;;;;;;;;;;;;AAoDpG,SAAgB,yBAAyB,OAA4C;AACnF,KAAI,UAAU,KAAA,KAAa,UAAU,KAAM,QAAO,OAAO;AACzD,KAAI,OAAO,UAAU,SAAU,QAAO,OAAO,SAAS,MAAM,IAAI,QAAQ,IAAI,QAAQ,OAAO;CAC3F,MAAM,QAAQ,MAAM,MAAM,CAAC,MAAM,aAAa;AAC9C,KAAI,CAAC,MAAO,QAAO,OAAO;CAC1B,MAAM,SAAS,OAAO,MAAM,GAAG;AAC/B,KAAI,EAAE,OAAO,SAAS,OAAO,IAAI,SAAS,GAAI,QAAO,OAAO;AAC5D,QAAQ,SAAS,SAAS,KAAM;;;;;;;;AAWlC,SAAgB,wBACd,QACA,eAAuB,OAAO,mBACf;AACf,KAAI,OAAO,WAAW,EAAG,QAAO,KAAA;CAIhC,MAAM,SAAS,CAAC,GAAG,OAAO,CAAC,MACxB,GAAG,OACD,EAAE,SAAS,MAAM,EAAE,UAAU,MAAM,EAAE,SAAS,MAAM,EAAE,UAAU,OAAO,EAAE,aAAa,MAAM,EAAE,aAAa,GAC/G;AAED,QAAO,OAAO,MAAM,OAAO,EAAE,SAAS,MAAM,EAAE,UAAU,MAAM,aAAa,IAAI,OAAO,OAAO,SAAS;;;;;;;;AASxG,SAAgB,gCAAgC,cAA6D;CAE3G,MAAM,UADW,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,QAAQ,GACvD,cAAc,IAAI;AAC3C,KAAI,CAAC,QAAQ,OAAQ,QAAO,KAAA;AAC5B,QAAO,wBAAwB,OAAO,EAAE;;;;;;;;;;;;;;AAe1C,SAAgB,eACd,cACA,QACoB;CACpB,MAAM,WAAW,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,QAAQ;AAEhF,KAAI,CAAC,YAAY,SAAS,cAAc,WAAW,EACjD;CAIF,MAAM,eAAe,SAAS,cAAc;AAC5C,KAAI,CAAC,gBAAgB,aAAa,OAAO,WAAW,EAClD;CAGF,MAAM,SAAS,aAAa;AAG5B,KAAI,QAAQ,wBAAwB;EAClC,MAAM,gBAAgB,OAAO,MAAM,UAAU,MAAM,aAAa,OAAO,uBAAuB;AAC9F,MAAI,cACF,QAAO,cAAc;;CAKzB,MAAM,eAAe,OAAO,MAAM,UAAU,MAAM,YAAY,KAAK;AACnE,KAAI,aACF,QAAO,aAAa;AAItB,QAAO,OAAO,IAAI;;;;;;;;;;;;;;AAqCpB,SAAgB,wBACd,QACA,QACoB;CACpB,MAAM,kBAAkB,QAAQ,sBAAsB,SAAS,OAAO,QAAQ,UAAU,CAAC,MAAM,OAAO;AACtG,KAAI,gBAAgB,WAAW,EAAG,QAAO,KAAA;CAEzC,MAAM,EAAE,2BAA2B,qBAAqB,UAAU,UAAU,EAAE;AAE9E,KAAI,2BAA2B;EAC7B,MAAM,gBAAgB,gBAAgB,MAAM,UAAU,MAAM,aAAa,0BAA0B;AACnG,MAAI,cAAe,QAAO,cAAc;;AAG1C,KAAI,oBAAoB;EACtB,MAAM,eAAe,gBAAgB,MAAM,UAAU,MAAM,YAAY,KAAK;AAC5E,MAAI,aAAc,QAAO,aAAa"}
|
|
@@ -83,7 +83,14 @@ type VideoTrack = Track & Required<Pick<Track, 'initialization' | 'codecs'>> & {
|
|
|
83
83
|
width?: number;
|
|
84
84
|
height?: number;
|
|
85
85
|
frameRate?: FrameRate;
|
|
86
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Audio groups (`EXT-X-STREAM-INF:AUDIO`) this video rendition can pair
|
|
88
|
+
* with. A list because one rendition is typically listed across multiple
|
|
89
|
+
* `EXT-X-STREAM-INF` entries — one per audio group (the HLS cross-product) —
|
|
90
|
+
* which the parser collapses into a single track carrying every group it
|
|
91
|
+
* advertised.
|
|
92
|
+
*/
|
|
93
|
+
audioGroupIds?: string[];
|
|
87
94
|
};
|
|
88
95
|
/**
|
|
89
96
|
* Resolved audio track with segments.
|
|
@@ -109,6 +116,23 @@ type TextTrack = Track & {
|
|
|
109
116
|
autoselect?: boolean;
|
|
110
117
|
forced?: boolean;
|
|
111
118
|
};
|
|
119
|
+
/**
|
|
120
|
+
* Predicate that answers "can this environment decode this track?" — the
|
|
121
|
+
* capability-probing surface, read by the track-switching hard-constraint
|
|
122
|
+
* pre-pass (`excludeUnplayableTracks`) to drop undecodable renditions before
|
|
123
|
+
* selection. Kept DOM-free here (a plain function type over a minimal track
|
|
124
|
+
* shape) so DOM-free behaviors can consume it; the DOM implementation
|
|
125
|
+
* (`canPlayTrack` in `media/dom/capabilities.ts`) wraps
|
|
126
|
+
* `MediaSource.isTypeSupported`.
|
|
127
|
+
*
|
|
128
|
+
* Takes the minimal codec-bearing shape both video and audio candidates
|
|
129
|
+
* carry. `mimeType` is optional so unprobeable candidates (no MIME) can be
|
|
130
|
+
* passed straight through as playable rather than dropped.
|
|
131
|
+
*/
|
|
132
|
+
type CanPlayTrack = (track: {
|
|
133
|
+
mimeType?: string;
|
|
134
|
+
codecs?: string[];
|
|
135
|
+
}) => boolean;
|
|
112
136
|
/**
|
|
113
137
|
* Minimal text-track cue shape — start time, end time, and display text.
|
|
114
138
|
*
|
|
@@ -190,5 +214,5 @@ type Presentation = Ham & AddressableObject & Partial<TimeSpan> & {
|
|
|
190
214
|
*/
|
|
191
215
|
type MaybeResolvedPresentation = AddressableObject & Partial<Omit<Presentation, keyof AddressableObject>>;
|
|
192
216
|
//#endregion
|
|
193
|
-
export { AudioTrack, Cue, MaybeResolvedPresentation, PartiallyResolvedTextTrack, Presentation, Segment, TextTrack, Track, VideoTrack };
|
|
217
|
+
export { AudioTrack, CanPlayTrack, Cue, MaybeResolvedPresentation, PartiallyResolvedTextTrack, Presentation, Segment, TextTrack, Track, VideoTrack };
|
|
194
218
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../../src/media/types/index.ts"],"mappings":";;AAgBA;;;;;AAOA;;;;;UAPiB,GAAA;EACf,EAAA;AAAA;;;AAmCF;UA7BiB,iBAAA;EACf,GAAA;EACA,SAAA;IACE,KAAA;IACA,GAAA;EAAA;AAAA;;;;;UAyBa,QAAA;EACf,SAAA;EACA,QAAA;AAAA;;;;KAUU,SAAA;;;;;;;;UAaK,SAAA;EACf,kBAAA;EACA,oBAAA;AAAA;;;;;;;KAaU,iBAAA,WAA4B,KAAA,GAAQ,KAAA,IAAS,IAAA,CAAK,CAAA,wCAAyC,QAAA;EACrG,QAAA;EACA,QAAA;EACA,SAAA;EACA,cAAA;AAAA;;;AA0CF;;;;;;KAfY,KAAA,GAAQ,GAAA,GAClB,iBAAA,GACA,QAAA;EACE,IAAA,EAAM,SAAA;EACN,MAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;EACA,cAAA,GAAiB,iBAAA;EACjB,QAAA,EAAU,OAAA;AAAA;;;;KAMF,UAAA,GAAa,KAAA,GACvB,QAAA,CAAS,IAAA,CAAK,KAAA;EACZ,IAAA;EAGA,KAAA;EACA,MAAA;EACA,SAAA,GAAY,SAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../../src/media/types/index.ts"],"mappings":";;AAgBA;;;;;AAOA;;;;;UAPiB,GAAA;EACf,EAAA;AAAA;;;AAmCF;UA7BiB,iBAAA;EACf,GAAA;EACA,SAAA;IACE,KAAA;IACA,GAAA;EAAA;AAAA;;;;;UAyBa,QAAA;EACf,SAAA;EACA,QAAA;AAAA;;;;KAUU,SAAA;;;;;;;;UAaK,SAAA;EACf,kBAAA;EACA,oBAAA;AAAA;;;;;;;KAaU,iBAAA,WAA4B,KAAA,GAAQ,KAAA,IAAS,IAAA,CAAK,CAAA,wCAAyC,QAAA;EACrG,QAAA;EACA,QAAA;EACA,SAAA;EACA,cAAA;AAAA;;;AA0CF;;;;;;KAfY,KAAA,GAAQ,GAAA,GAClB,iBAAA,GACA,QAAA;EACE,IAAA,EAAM,SAAA;EACN,MAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;EACA,cAAA,GAAiB,iBAAA;EACjB,QAAA,EAAU,OAAA;AAAA;;;;KAMF,UAAA,GAAa,KAAA,GACvB,QAAA,CAAS,IAAA,CAAK,KAAA;EACZ,IAAA;EAGA,KAAA;EACA,MAAA;EACA,SAAA,GAAY,SAAA;EAcM;;;;;;;EANlB,aAAA;AAAA;;;;KAMQ,UAAA,GAAa,KAAA,GACvB,QAAA,CAAS,IAAA,CAAK,KAAA;EACZ,IAAA;EACA,OAAA;EACA,IAAA;EACA,UAAA;EACA,QAAA;EACA,OAAA;EACA,UAAA;AAAA;AAMJ;;;AAAA,KAAY,SAAA,GAAY,KAAA;EACtB,IAAA;EACA,OAAA;EACA,KAAA;EACA,IAAA;EACA,OAAA;EACA,UAAA;EACA,MAAA;AAAA;;;AAgBF;;;;;;;;;AAUA;;KAVY,YAAA,IAAgB,KAAA;EAAS,QAAA;EAAmB,MAAA;AAAA;;;;AAgCxD;;;;;UAtBiB,GAAA;EACf,SAAA;EACA,OAAA;EACA,IAAA;AAAA;;;;;KAmBU,0BAAA,GAA6B,iBAAA,CAAkB,SAAA;;;;;;;KAyB/C,cAAA,WAAyB,KAAA,GAAQ,KAAA,IAAS,GAAA;EACpD,IAAA,EAAM,CAAA;EACN,MAAA,GAAS,iBAAA,CAAkB,CAAA,IAAK,CAAA;AAAA;;;;AAsDlC;;;KAxBY,cAAA,WAAyB,KAAA,GAAQ,KAAA,IAAS,GAAA;EACpD,IAAA,EAAM,CAAA;EACN,aAAA,EAAe,cAAA,CAAe,CAAA;AAAA;;;;KAMpB,iBAAA,GAAoB,cAAA,CAAe,UAAA;;;AA0B/C;KArBY,iBAAA,GAAoB,cAAA,CAAe,UAAA;;;;KAKnC,gBAAA,GAAmB,cAAA,CAAe,SAAA;;;;;KAMlC,YAAA,GAAe,iBAAA,GAAoB,iBAAA,GAAoB,gBAAA;;;AAmDnE;;KAzCY,OAAA,GAAU,GAAA,GAAM,iBAAA,GAAoB,QAAA;;;;;;;;KAyCpC,YAAA,GAAe,GAAA,GACzB,iBAAA,GACA,OAAA,CAAQ,QAAA;EACN,aAAA,EAAe,YAAA;AAAA;;;;;;;;;;KAYP,yBAAA,GAA4B,iBAAA,GAAoB,OAAA,CAAQ,IAAA,CAAK,YAAA,QAAoB,iBAAA"}
|