@videojs/spf 10.0.0-beta.2 → 10.0.0-beta.21
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/README.md +16 -40
- package/dist/default/core/abr/bandwidth-estimator.js +95 -0
- package/dist/default/core/abr/bandwidth-estimator.js.map +1 -0
- package/dist/default/core/abr/ewma.js +71 -0
- package/dist/default/core/abr/ewma.js.map +1 -0
- package/dist/default/core/abr/quality-selection.js +53 -0
- package/dist/default/core/abr/quality-selection.js.map +1 -0
- package/dist/default/core/buffer/back-buffer.js +46 -0
- package/dist/default/core/buffer/back-buffer.js.map +1 -0
- package/dist/default/core/buffer/forward-buffer.js +73 -0
- package/dist/default/core/buffer/forward-buffer.js.map +1 -0
- package/dist/default/core/create-machine-actor.js +91 -0
- package/dist/default/core/create-machine-actor.js.map +1 -0
- package/dist/default/core/create-machine-reactor.js +83 -0
- package/dist/default/core/create-machine-reactor.js.map +1 -0
- package/dist/default/core/create-transition-actor.js +46 -0
- package/dist/default/core/create-transition-actor.js.map +1 -0
- package/dist/default/core/features/calculate-presentation-duration.js +53 -0
- package/dist/default/core/features/calculate-presentation-duration.js.map +1 -0
- package/dist/default/core/features/quality-switching.js +69 -0
- package/dist/default/core/features/quality-switching.js.map +1 -0
- package/dist/default/core/features/resolve-presentation.js +73 -0
- package/dist/default/core/features/resolve-presentation.js.map +1 -0
- package/dist/default/core/features/resolve-track.js +66 -0
- package/dist/default/core/features/resolve-track.js.map +1 -0
- package/dist/default/core/features/select-tracks.js +130 -0
- package/dist/default/core/features/select-tracks.js.map +1 -0
- package/dist/default/core/features/sync-preload-attribute.js +26 -0
- package/dist/default/core/features/sync-preload-attribute.js.map +1 -0
- package/dist/default/core/hls/parse-attributes.js +134 -0
- package/dist/default/core/hls/parse-attributes.js.map +1 -0
- package/dist/default/core/hls/parse-media-playlist.js +82 -0
- package/dist/default/core/hls/parse-media-playlist.js.map +1 -0
- package/dist/default/core/hls/parse-multivariant.js +214 -0
- package/dist/default/core/hls/parse-multivariant.js.map +1 -0
- package/dist/default/core/hls/resolve-url.js +11 -0
- package/dist/default/core/hls/resolve-url.js.map +1 -0
- package/dist/default/core/machine.js +26 -0
- package/dist/default/core/machine.js.map +1 -0
- package/dist/default/core/signals/effect.js +41 -0
- package/dist/default/core/signals/effect.js.map +1 -0
- package/dist/default/core/signals/primitives.js +32 -0
- package/dist/default/core/signals/primitives.js.map +1 -0
- package/dist/default/core/task.js +191 -0
- package/dist/default/core/task.js.map +1 -0
- package/dist/default/core/types/index.js +15 -0
- package/dist/default/core/types/index.js.map +1 -0
- package/dist/default/core/utils/generate-id.js +21 -0
- package/dist/default/core/utils/generate-id.js.map +1 -0
- package/dist/default/core/utils/track-selection.js +39 -0
- package/dist/default/core/utils/track-selection.js.map +1 -0
- package/dist/default/dom/features/end-of-stream.js +145 -0
- package/dist/default/dom/features/end-of-stream.js.map +1 -0
- package/dist/default/dom/features/load-segments.js +192 -0
- package/dist/default/dom/features/load-segments.js.map +1 -0
- package/dist/default/dom/features/load-text-track-cues.js +104 -0
- package/dist/default/dom/features/load-text-track-cues.js.map +1 -0
- package/dist/default/dom/features/segment-loader-actor.js +243 -0
- package/dist/default/dom/features/segment-loader-actor.js.map +1 -0
- package/dist/default/dom/features/setup-mediasource.js +48 -0
- package/dist/default/dom/features/setup-mediasource.js.map +1 -0
- package/dist/default/dom/features/setup-sourcebuffer.js +78 -0
- package/dist/default/dom/features/setup-sourcebuffer.js.map +1 -0
- package/dist/default/dom/features/sync-text-tracks.js +95 -0
- package/dist/default/dom/features/sync-text-tracks.js.map +1 -0
- package/dist/default/dom/features/text-track-segment-loader-actor.js +55 -0
- package/dist/default/dom/features/text-track-segment-loader-actor.js.map +1 -0
- package/dist/default/dom/features/text-tracks-actor.js +42 -0
- package/dist/default/dom/features/text-tracks-actor.js.map +1 -0
- package/dist/default/dom/features/track-current-time.js +49 -0
- package/dist/default/dom/features/track-current-time.js.map +1 -0
- package/dist/default/dom/features/track-playback-initiated.js +66 -0
- package/dist/default/dom/features/track-playback-initiated.js.map +1 -0
- package/dist/default/dom/features/track-playback-rate.js +32 -0
- package/dist/default/dom/features/track-playback-rate.js.map +1 -0
- package/dist/default/dom/features/update-duration.js +78 -0
- package/dist/default/dom/features/update-duration.js.map +1 -0
- package/dist/default/dom/media/append-segment.js +60 -0
- package/dist/default/dom/media/append-segment.js.map +1 -0
- package/dist/default/dom/media/buffer-flusher.js +55 -0
- package/dist/default/dom/media/buffer-flusher.js.map +1 -0
- package/dist/default/dom/media/mediasource-setup.js +146 -0
- package/dist/default/dom/media/mediasource-setup.js.map +1 -0
- package/dist/default/dom/media/source-buffer-actor.js +137 -0
- package/dist/default/dom/media/source-buffer-actor.js.map +1 -0
- package/dist/default/dom/network/chunked-stream-iterable.js +49 -0
- package/dist/default/dom/network/chunked-stream-iterable.js.map +1 -0
- package/dist/default/dom/network/fetch.js +54 -0
- package/dist/default/dom/network/fetch.js.map +1 -0
- package/dist/default/dom/playback-engine/adapter.js +100 -0
- package/dist/default/dom/playback-engine/adapter.js.map +1 -0
- package/dist/default/dom/playback-engine/engine.js +142 -0
- package/dist/default/dom/playback-engine/engine.js.map +1 -0
- package/dist/default/dom/playback-engine.js +4 -0
- package/dist/default/dom/text/parse-vtt-segment.js +56 -0
- package/dist/default/dom/text/parse-vtt-segment.js.map +1 -0
- package/dist/default/dom.js +10 -0
- package/dist/{src-DRsS_H80.js → default/index.js} +3 -3
- package/dist/default/index.js.map +1 -0
- package/dist/dev/core/abr/bandwidth-estimator.d.ts +35 -0
- package/dist/dev/core/abr/bandwidth-estimator.d.ts.map +1 -0
- package/dist/dev/core/abr/bandwidth-estimator.js +95 -0
- package/dist/dev/core/abr/bandwidth-estimator.js.map +1 -0
- package/dist/dev/core/abr/ewma.js +71 -0
- package/dist/dev/core/abr/ewma.js.map +1 -0
- package/dist/dev/core/abr/quality-selection.js +53 -0
- package/dist/dev/core/abr/quality-selection.js.map +1 -0
- package/dist/dev/core/actor.d.ts +27 -0
- package/dist/dev/core/actor.d.ts.map +1 -0
- package/dist/dev/core/buffer/back-buffer.js +46 -0
- package/dist/dev/core/buffer/back-buffer.js.map +1 -0
- package/dist/dev/core/buffer/forward-buffer.js +73 -0
- package/dist/dev/core/buffer/forward-buffer.js.map +1 -0
- package/dist/dev/core/create-machine-actor.d.ts +12 -0
- package/dist/dev/core/create-machine-actor.d.ts.map +1 -0
- package/dist/dev/core/create-machine-actor.js +91 -0
- package/dist/dev/core/create-machine-actor.js.map +1 -0
- package/dist/dev/core/create-machine-reactor.d.ts +8 -0
- package/dist/dev/core/create-machine-reactor.d.ts.map +1 -0
- package/dist/dev/core/create-machine-reactor.js +83 -0
- package/dist/dev/core/create-machine-reactor.js.map +1 -0
- package/dist/dev/core/create-transition-actor.d.ts +23 -0
- package/dist/dev/core/create-transition-actor.d.ts.map +1 -0
- package/dist/dev/core/create-transition-actor.js +46 -0
- package/dist/dev/core/create-transition-actor.js.map +1 -0
- package/dist/dev/core/features/calculate-presentation-duration.js +53 -0
- package/dist/dev/core/features/calculate-presentation-duration.js.map +1 -0
- package/dist/dev/core/features/quality-switching.js +69 -0
- package/dist/dev/core/features/quality-switching.js.map +1 -0
- package/dist/dev/core/features/resolve-presentation.js +73 -0
- package/dist/dev/core/features/resolve-presentation.js.map +1 -0
- package/dist/dev/core/features/resolve-track.js +66 -0
- package/dist/dev/core/features/resolve-track.js.map +1 -0
- package/dist/dev/core/features/select-tracks.js +130 -0
- package/dist/dev/core/features/select-tracks.js.map +1 -0
- package/dist/dev/core/features/sync-preload-attribute.js +26 -0
- package/dist/dev/core/features/sync-preload-attribute.js.map +1 -0
- package/dist/dev/core/hls/parse-attributes.js +134 -0
- package/dist/dev/core/hls/parse-attributes.js.map +1 -0
- package/dist/dev/core/hls/parse-media-playlist.js +82 -0
- package/dist/dev/core/hls/parse-media-playlist.js.map +1 -0
- package/dist/dev/core/hls/parse-multivariant.js +214 -0
- package/dist/dev/core/hls/parse-multivariant.js.map +1 -0
- package/dist/dev/core/hls/resolve-url.js +11 -0
- package/dist/dev/core/hls/resolve-url.js.map +1 -0
- package/dist/dev/core/machine.d.ts +21 -0
- package/dist/dev/core/machine.d.ts.map +1 -0
- package/dist/dev/core/machine.js +26 -0
- package/dist/dev/core/machine.js.map +1 -0
- package/dist/dev/core/signals/effect.d.ts +15 -0
- package/dist/dev/core/signals/effect.d.ts.map +1 -0
- package/dist/dev/core/signals/effect.js +41 -0
- package/dist/dev/core/signals/effect.js.map +1 -0
- package/dist/dev/core/signals/primitives.d.ts +10 -0
- package/dist/dev/core/signals/primitives.d.ts.map +1 -0
- package/dist/dev/core/signals/primitives.js +32 -0
- package/dist/dev/core/signals/primitives.js.map +1 -0
- package/dist/dev/core/task.js +191 -0
- package/dist/dev/core/task.js.map +1 -0
- package/dist/dev/core/types/index.d.ts +166 -0
- package/dist/dev/core/types/index.d.ts.map +1 -0
- package/dist/dev/core/types/index.js +15 -0
- package/dist/dev/core/types/index.js.map +1 -0
- package/dist/dev/core/utils/generate-id.js +21 -0
- package/dist/dev/core/utils/generate-id.js.map +1 -0
- package/dist/dev/core/utils/track-selection.d.ts +16 -0
- package/dist/dev/core/utils/track-selection.d.ts.map +1 -0
- package/dist/dev/core/utils/track-selection.js +39 -0
- package/dist/dev/core/utils/track-selection.js.map +1 -0
- package/dist/dev/dom/features/end-of-stream.js +145 -0
- package/dist/dev/dom/features/end-of-stream.js.map +1 -0
- package/dist/dev/dom/features/load-segments.d.ts +72 -0
- package/dist/dev/dom/features/load-segments.d.ts.map +1 -0
- package/dist/dev/dom/features/load-segments.js +192 -0
- package/dist/dev/dom/features/load-segments.js.map +1 -0
- package/dist/dev/dom/features/load-text-track-cues.d.ts +85 -0
- package/dist/dev/dom/features/load-text-track-cues.d.ts.map +1 -0
- package/dist/dev/dom/features/load-text-track-cues.js +104 -0
- package/dist/dev/dom/features/load-text-track-cues.js.map +1 -0
- package/dist/dev/dom/features/segment-loader-actor.d.ts +1 -0
- package/dist/dev/dom/features/segment-loader-actor.js +243 -0
- package/dist/dev/dom/features/segment-loader-actor.js.map +1 -0
- package/dist/dev/dom/features/setup-mediasource.js +48 -0
- package/dist/dev/dom/features/setup-mediasource.js.map +1 -0
- package/dist/dev/dom/features/setup-sourcebuffer.d.ts +9 -0
- package/dist/dev/dom/features/setup-sourcebuffer.d.ts.map +1 -0
- package/dist/dev/dom/features/setup-sourcebuffer.js +78 -0
- package/dist/dev/dom/features/setup-sourcebuffer.js.map +1 -0
- package/dist/dev/dom/features/sync-text-tracks.js +95 -0
- package/dist/dev/dom/features/sync-text-tracks.js.map +1 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts +12 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.d.ts.map +1 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.js +55 -0
- package/dist/dev/dom/features/text-track-segment-loader-actor.js.map +1 -0
- package/dist/dev/dom/features/text-tracks-actor.d.ts +31 -0
- package/dist/dev/dom/features/text-tracks-actor.d.ts.map +1 -0
- package/dist/dev/dom/features/text-tracks-actor.js +42 -0
- package/dist/dev/dom/features/text-tracks-actor.js.map +1 -0
- package/dist/dev/dom/features/track-current-time.d.ts +40 -0
- package/dist/dev/dom/features/track-current-time.d.ts.map +1 -0
- package/dist/dev/dom/features/track-current-time.js +49 -0
- package/dist/dev/dom/features/track-current-time.js.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts +46 -0
- package/dist/dev/dom/features/track-playback-initiated.d.ts.map +1 -0
- package/dist/dev/dom/features/track-playback-initiated.js +66 -0
- package/dist/dev/dom/features/track-playback-initiated.js.map +1 -0
- package/dist/dev/dom/features/track-playback-rate.d.ts +34 -0
- package/dist/dev/dom/features/track-playback-rate.d.ts.map +1 -0
- package/dist/dev/dom/features/track-playback-rate.js +32 -0
- package/dist/dev/dom/features/track-playback-rate.js.map +1 -0
- package/dist/dev/dom/features/update-duration.js +78 -0
- package/dist/dev/dom/features/update-duration.js.map +1 -0
- package/dist/dev/dom/media/append-segment.d.ts +24 -0
- package/dist/dev/dom/media/append-segment.d.ts.map +1 -0
- package/dist/dev/dom/media/append-segment.js +60 -0
- package/dist/dev/dom/media/append-segment.js.map +1 -0
- package/dist/dev/dom/media/buffer-flusher.d.ts +24 -0
- package/dist/dev/dom/media/buffer-flusher.d.ts.map +1 -0
- package/dist/dev/dom/media/buffer-flusher.js +55 -0
- package/dist/dev/dom/media/buffer-flusher.js.map +1 -0
- package/dist/dev/dom/media/mediasource-setup.js +146 -0
- package/dist/dev/dom/media/mediasource-setup.js.map +1 -0
- package/dist/dev/dom/media/source-buffer-actor.d.ts +62 -0
- package/dist/dev/dom/media/source-buffer-actor.d.ts.map +1 -0
- package/dist/dev/dom/media/source-buffer-actor.js +137 -0
- package/dist/dev/dom/media/source-buffer-actor.js.map +1 -0
- package/dist/dev/dom/network/chunked-stream-iterable.js +49 -0
- package/dist/dev/dom/network/chunked-stream-iterable.js.map +1 -0
- package/dist/dev/dom/network/fetch.js +54 -0
- package/dist/dev/dom/network/fetch.js.map +1 -0
- package/dist/dev/dom/playback-engine/adapter.d.ts +40 -0
- package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -0
- package/dist/dev/dom/playback-engine/adapter.js +100 -0
- package/dist/dev/dom/playback-engine/adapter.js.map +1 -0
- package/dist/dev/dom/playback-engine/engine.d.ts +124 -0
- package/dist/dev/dom/playback-engine/engine.d.ts.map +1 -0
- package/dist/dev/dom/playback-engine/engine.js +142 -0
- package/dist/dev/dom/playback-engine/engine.js.map +1 -0
- package/dist/dev/dom/playback-engine.d.ts +4 -0
- package/dist/dev/dom/playback-engine.js +4 -0
- package/dist/dev/dom/text/parse-vtt-segment.d.ts +12 -0
- package/dist/dev/dom/text/parse-vtt-segment.d.ts.map +1 -0
- package/dist/dev/dom/text/parse-vtt-segment.js +56 -0
- package/dist/dev/dom/text/parse-vtt-segment.js.map +1 -0
- package/dist/dev/dom.d.ts +10 -0
- package/dist/dev/dom.js +10 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +14 -0
- package/dist/dev/index.js.map +1 -0
- package/package.json +26 -24
- package/dist/adapter-CMw-rvbk.js +0 -3512
- package/dist/adapter-CMw-rvbk.js.map +0 -1
- package/dist/adapter-l8iJN_GI.d.ts +0 -689
- package/dist/adapter-l8iJN_GI.d.ts.map +0 -1
- package/dist/dom/index.d.ts +0 -243
- package/dist/dom/index.d.ts.map +0 -1
- package/dist/dom/index.js +0 -38
- package/dist/dom/index.js.map +0 -1
- package/dist/dom/playback-engine/index.d.ts +0 -2
- package/dist/dom/playback-engine/index.js +0 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3
- package/dist/src-DRsS_H80.js.map +0 -1
- /package/dist/{index.d.ts → dev/index.d.ts} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select-tracks.js","names":[],"sources":["../../../../src/core/features/select-tracks.ts"],"sourcesContent":["import { DEFAULT_QUALITY_CONFIG, selectQuality } from '../abr/quality-selection';\nimport { effect } from '../signals/effect';\nimport { type Signal, update } from '../signals/primitives';\nimport type { AudioSelectionSet, Presentation, TrackType, VideoSelectionSet } 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?: Presentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/**\n * Owners shape for track selection.\n * Currently empty - reserved for future use (e.g., bandwidth estimator).\n */\nexport type TrackSelectionOwners = 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 * Generic with default to 'text' for convenience.\n */\nexport interface TextSelectionConfig<T extends TrackType = 'text'> extends TrackSelectionConfig<T> {\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 * 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.\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 *\n * @param presentation - Presentation with text tracks\n * @param config - Selection configuration\n * @returns Track ID or undefined (no auto-selection)\n */\nexport function pickTextTrack(presentation: Presentation, config: TextSelectionConfig): 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\n// =============================================================================\n// Orchestrations\n// =============================================================================\n\n/**\n * Select video track orchestration.\n *\n * Selects video track when:\n * - Presentation exists\n * - No video track is selected yet\n *\n * Uses bandwidth-based quality selection algorithm.\n *\n * @example\n * const cleanup = selectVideoTrack(\n * { state, owners, events },\n * { initialBandwidth: 2_000_000 }\n * );\n */\nexport function selectVideoTrack<S extends TrackSelectionState>(\n { state }: { state: Signal<S> },\n config: VideoSelectionConfig = { type: 'video' }\n): () => void {\n return effect(() => {\n const currentState = state.get();\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;\n\n // Just to have basic functionality/POC, simply selecting the first track\n const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === config.type)\n ?.switchingSets[0]?.tracks[0]?.id;\n\n if (selectedTrackId) {\n const selectedTrackKey = SelectedTrackIdKeyByType[config.type];\n const patch: Partial<TrackSelectionState> = { [selectedTrackKey]: selectedTrackId };\n update(state, patch);\n }\n });\n}\n\n/**\n * Select audio track orchestration.\n *\n * Selects audio track when:\n * - Presentation exists\n * - No audio track is selected yet\n *\n * Uses language and preference-based selection.\n *\n * @example\n * const cleanup = selectAudioTrack(\n * { state, owners, events },\n * { preferredAudioLanguage: 'en' }\n * );\n */\nexport function selectAudioTrack<S extends TrackSelectionState>(\n { state }: { state: Signal<S> },\n config: AudioSelectionConfig = { type: 'audio' }\n): () => void {\n return effect(() => {\n const currentState = state.get();\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;\n\n // Just to have basic functionality/POC, simply selecting the first track\n const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === 'audio')\n ?.switchingSets[0]?.tracks[0]?.id;\n\n if (selectedTrackId) {\n const patch: Partial<TrackSelectionState> = { selectedAudioTrackId: selectedTrackId };\n update(state, patch);\n }\n });\n}\n\n/**\n * Select text track orchestration.\n *\n * Selects text track when:\n * - Presentation exists\n * - No text track is selected yet\n *\n * Note: Currently does not auto-select (user opt-in).\n *\n * @example\n * const cleanup = selectTextTrack({ state, owners, events }, {});\n */\nexport function selectTextTrack<S extends TrackSelectionState>(\n { state }: { state: Signal<S> },\n config: TextSelectionConfig = { type: 'text' }\n): () => void {\n return effect(() => {\n const currentState = state.get();\n if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;\n\n // Text tracks are user opt-in - don't auto-select\n const selectedTextTrackId = pickTextTrack(currentState.presentation!, config);\n\n if (selectedTextTrackId) {\n const patch: Partial<TrackSelectionState> = { selectedTextTrackId };\n update(state, patch);\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAwMA,SAAgB,cAAc,cAA4B,QAAiD;CACzG,MAAM,UAAU,aAAa,cAAc,MAAM,QAAQ,IAAI,SAAS,OAAO;AAC7E,KAAI,CAAC,SAAS,gBAAgB,IAAI,OAAO,OAAQ,QAAO,KAAA;CAExD,MAAM,SAAS,QAAQ,cAAc,GAAG;CAIxC,MAAM,kBAAkB,OAAO,sBAAsB,SAAS,OAAO,QAAQ,UAAU,CAAC,MAAM,OAAO;AAErG,KAAI,gBAAgB,WAAW,EAAG,QAAO,KAAA;CAEzC,MAAM,EAAE,2BAA2B,qBAAqB,UAAU;AAGlE,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;;;;;;;;;;;;AAgB1C,SAAgB,eACd,OACA,QACS;AACT,QAAO,CAAC,CAAC,OAAO,cAAc,eAAe,MAAM,EAAE,WAAW,SAAS,OAAO,KAAK,EAAE,gBAAgB,IAAI,OACxG;;;;;;;;;;;;;AAcL,SAAgB,kBACd,OACA,QACS;AACT,QAAO,CAAC,MAAM,yBAAyB,OAAO;;;;;;;;;;;;;;;;;AAsBhD,SAAgB,iBACd,EAAE,SACF,SAA+B,EAAE,MAAM,SAAS,EACpC;AACZ,QAAO,aAAa;EAClB,MAAM,eAAe,MAAM,KAAK;AAChC,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,CAAE;EAGvF,MAAM,kBAAkB,aAAa,cAAc,cAAc,MAAM,EAAE,WAAW,SAAS,OAAO,KAAK,EACrG,cAAc,IAAI,OAAO,IAAI;AAEjC,MAAI,gBAGF,QAAO,OADqC,GADnB,yBAAyB,OAAO,QACS,iBAAiB,CAC/D;GAEtB;;;;;;;;;;;;;;;;;AAkBJ,SAAgB,iBACd,EAAE,SACF,SAA+B,EAAE,MAAM,SAAS,EACpC;AACZ,QAAO,aAAa;EAClB,MAAM,eAAe,MAAM,KAAK;AAChC,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,CAAE;EAGvF,MAAM,kBAAkB,aAAa,cAAc,cAAc,MAAM,EAAE,WAAW,SAAS,QAAQ,EACjG,cAAc,IAAI,OAAO,IAAI;AAEjC,MAAI,gBAEF,QAAO,OADqC,EAAE,sBAAsB,iBAAiB,CACjE;GAEtB;;;;;;;;;;;;;;AAeJ,SAAgB,gBACd,EAAE,SACF,SAA8B,EAAE,MAAM,QAAQ,EAClC;AACZ,QAAO,aAAa;EAClB,MAAM,eAAe,MAAM,KAAK;AAChC,MAAI,CAAC,eAAe,cAAc,OAAO,IAAI,CAAC,kBAAkB,cAAc,OAAO,CAAE;EAGvF,MAAM,sBAAsB,cAAc,aAAa,cAAe,OAAO;AAE7E,MAAI,oBAEF,QAAO,OADqC,EAAE,qBAAqB,CAC/C;GAEtB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { effect } from "../signals/effect.js";
|
|
2
|
+
import { computed, update } from "../signals/primitives.js";
|
|
3
|
+
//#region src/core/features/sync-preload-attribute.ts
|
|
4
|
+
/**
|
|
5
|
+
* Syncs preload attribute from mediaElement to state.
|
|
6
|
+
*
|
|
7
|
+
* Watches the owners signal for mediaElement changes and copies the
|
|
8
|
+
* preload attribute to state when no explicit value has been set.
|
|
9
|
+
* An explicit value (set via SpfMedia.preload) always wins.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const cleanup = syncPreloadAttribute({ state, owners });
|
|
13
|
+
*/
|
|
14
|
+
function syncPreloadAttribute({ state, owners }) {
|
|
15
|
+
const mediaElement = computed(() => owners.get().mediaElement);
|
|
16
|
+
return effect(() => {
|
|
17
|
+
if (state.get().preload !== void 0) return;
|
|
18
|
+
const preload = mediaElement.get()?.preload || void 0;
|
|
19
|
+
if (preload === void 0) return;
|
|
20
|
+
update(state, { preload });
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { syncPreloadAttribute };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=sync-preload-attribute.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-preload-attribute.js","names":[],"sources":["../../../../src/core/features/sync-preload-attribute.ts"],"sourcesContent":["import { effect } from '../signals/effect';\nimport { computed, type Signal, update } from '../signals/primitives';\nimport type { MediaElementLike } from '../types';\nimport type { PresentationState } from './resolve-presentation';\n\n/**\n * Owners shape for preload attribute sync.\n */\nexport interface PlatformOwners {\n mediaElement?: MediaElementLike | undefined;\n}\n\n/**\n * Syncs preload attribute from mediaElement to state.\n *\n * Watches the owners signal for mediaElement changes and copies the\n * preload attribute to state when no explicit value has been set.\n * An explicit value (set via SpfMedia.preload) always wins.\n *\n * @example\n * const cleanup = syncPreloadAttribute({ state, owners });\n */\nexport function syncPreloadAttribute<S extends PresentationState, O extends PlatformOwners>({\n state,\n owners,\n}: {\n state: Signal<S>;\n owners: Signal<O>;\n}): () => void {\n const mediaElement = computed(() => owners.get().mediaElement);\n return effect(() => {\n if (state.get().preload !== undefined) return;\n const preload = mediaElement.get()?.preload || undefined;\n if (preload === undefined) return;\n const patch: Partial<PresentationState> = { preload: preload as 'auto' | 'metadata' | 'none' };\n update(state, patch);\n });\n}\n"],"mappings":";;;;;;;;;;;;;AAsBA,SAAgB,qBAA4E,EAC1F,OACA,UAIa;CACb,MAAM,eAAe,eAAe,OAAO,KAAK,CAAC,aAAa;AAC9D,QAAO,aAAa;AAClB,MAAI,MAAM,KAAK,CAAC,YAAY,KAAA,EAAW;EACvC,MAAM,UAAU,aAAa,KAAK,EAAE,WAAW,KAAA;AAC/C,MAAI,YAAY,KAAA,EAAW;AAE3B,SAAO,OADmC,EAAW,SAAyC,CAC1E;GACpB"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
//#region src/core/hls/parse-attributes.ts
|
|
2
|
+
/**
|
|
3
|
+
* Parse HLS attribute list from a tag line.
|
|
4
|
+
* Handles both quoted and unquoted values.
|
|
5
|
+
*/
|
|
6
|
+
function parseAttributeList(line) {
|
|
7
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
8
|
+
for (const match of line.matchAll(/([A-Z0-9-]+)=(?:"([^"]*)"|([^,]*))/g)) {
|
|
9
|
+
const key = match[1];
|
|
10
|
+
const value = match[2] ?? match[3] ?? "";
|
|
11
|
+
if (key) attributes.set(key, value);
|
|
12
|
+
}
|
|
13
|
+
return attributes;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parse RESOLUTION attribute value (WIDTHxHEIGHT).
|
|
17
|
+
*/
|
|
18
|
+
function parseResolution(value) {
|
|
19
|
+
const match = /^(\d+)x(\d+)$/.exec(value);
|
|
20
|
+
if (!match) return null;
|
|
21
|
+
return {
|
|
22
|
+
width: Number.parseInt(match[1], 10),
|
|
23
|
+
height: Number.parseInt(match[2], 10)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parse FRAME-RATE attribute to rational frame rate.
|
|
28
|
+
*/
|
|
29
|
+
function parseFrameRate(value) {
|
|
30
|
+
const fps = Number.parseFloat(value);
|
|
31
|
+
if (Number.isNaN(fps) || fps <= 0) return void 0;
|
|
32
|
+
if (Math.abs(fps - 23.976) < .01) return {
|
|
33
|
+
frameRateNumerator: 24e3,
|
|
34
|
+
frameRateDenominator: 1001
|
|
35
|
+
};
|
|
36
|
+
if (Math.abs(fps - 29.97) < .01) return {
|
|
37
|
+
frameRateNumerator: 3e4,
|
|
38
|
+
frameRateDenominator: 1001
|
|
39
|
+
};
|
|
40
|
+
if (Math.abs(fps - 59.94) < .01) return {
|
|
41
|
+
frameRateNumerator: 6e4,
|
|
42
|
+
frameRateDenominator: 1001
|
|
43
|
+
};
|
|
44
|
+
if (fps % 1 === 0) return { frameRateNumerator: Math.round(fps) };
|
|
45
|
+
return { frameRateNumerator: Math.round(fps) };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parse CODECS attribute into separate video and audio codecs.
|
|
49
|
+
*/
|
|
50
|
+
function parseCodecs(codecs) {
|
|
51
|
+
const parts = codecs.split(",").map((s) => s.trim());
|
|
52
|
+
const result = {};
|
|
53
|
+
for (const codec of parts) if (codec.startsWith("avc1.") || codec.startsWith("hvc1.") || codec.startsWith("hev1.")) result.video = codec;
|
|
54
|
+
else if (codec.startsWith("mp4a.")) result.audio = codec;
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parse #EXTINF duration value.
|
|
59
|
+
*/
|
|
60
|
+
function parseExtInfDuration(value) {
|
|
61
|
+
const durationPart = value.split(",")[0] ?? value;
|
|
62
|
+
const duration = Number.parseFloat(durationPart);
|
|
63
|
+
return Number.isNaN(duration) ? 0 : duration;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Parse BYTERANGE attribute value.
|
|
67
|
+
* Format: "length[@offset]"
|
|
68
|
+
* If offset is omitted, it continues from the previous byte range end.
|
|
69
|
+
*/
|
|
70
|
+
function parseByteRange(value, previousEnd) {
|
|
71
|
+
const match = /^(\d+)(?:@(\d+))?$/.exec(value);
|
|
72
|
+
if (!match) return null;
|
|
73
|
+
const length = Number.parseInt(match[1], 10);
|
|
74
|
+
if (Number.isNaN(length)) return null;
|
|
75
|
+
let start;
|
|
76
|
+
if (match[2] !== void 0) {
|
|
77
|
+
start = Number.parseInt(match[2], 10);
|
|
78
|
+
if (Number.isNaN(start)) return null;
|
|
79
|
+
} else if (previousEnd !== void 0) start = previousEnd;
|
|
80
|
+
else return null;
|
|
81
|
+
return {
|
|
82
|
+
start,
|
|
83
|
+
end: start + length - 1
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Create AttributeList from raw attribute string.
|
|
88
|
+
*/
|
|
89
|
+
function createAttributeList(line) {
|
|
90
|
+
const map = parseAttributeList(line);
|
|
91
|
+
return {
|
|
92
|
+
get(key) {
|
|
93
|
+
return map.get(key);
|
|
94
|
+
},
|
|
95
|
+
getInt(key, defaultValue) {
|
|
96
|
+
const value = map.get(key);
|
|
97
|
+
if (value === void 0) return defaultValue;
|
|
98
|
+
const parsed = Number.parseInt(value, 10);
|
|
99
|
+
return Number.isNaN(parsed) ? defaultValue : parsed;
|
|
100
|
+
},
|
|
101
|
+
getFloat(key, defaultValue) {
|
|
102
|
+
const value = map.get(key);
|
|
103
|
+
if (value === void 0) return defaultValue;
|
|
104
|
+
const parsed = Number.parseFloat(value);
|
|
105
|
+
return Number.isNaN(parsed) ? defaultValue : parsed;
|
|
106
|
+
},
|
|
107
|
+
getBool(key) {
|
|
108
|
+
return map.get(key) === "YES";
|
|
109
|
+
},
|
|
110
|
+
getResolution(key) {
|
|
111
|
+
const value = map.get(key);
|
|
112
|
+
if (!value) return void 0;
|
|
113
|
+
return parseResolution(value) ?? void 0;
|
|
114
|
+
},
|
|
115
|
+
getFrameRate(key) {
|
|
116
|
+
const value = map.get(key);
|
|
117
|
+
if (!value) return void 0;
|
|
118
|
+
return parseFrameRate(value);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Match a tag and extract its attributes.
|
|
124
|
+
* Returns null if the line doesn't match the tag.
|
|
125
|
+
*/
|
|
126
|
+
function matchTag(line, tag) {
|
|
127
|
+
const prefix = `#${tag}:`;
|
|
128
|
+
if (!line.startsWith(prefix)) return null;
|
|
129
|
+
return createAttributeList(line.slice(prefix.length));
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
export { matchTag, parseByteRange, parseCodecs, parseExtInfDuration };
|
|
133
|
+
|
|
134
|
+
//# sourceMappingURL=parse-attributes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-attributes.js","names":[],"sources":["../../../../src/core/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 (codec.startsWith('mp4a.')) {\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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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { matchTag, parseByteRange, parseExtInfDuration } from "./parse-attributes.js";
|
|
2
|
+
import { resolveUrl } from "./resolve-url.js";
|
|
3
|
+
//#region src/core/hls/parse-media-playlist.ts
|
|
4
|
+
/**
|
|
5
|
+
* Parse HLS media playlist and resolve track with segments.
|
|
6
|
+
*
|
|
7
|
+
* Takes an unresolved track (from multivariant playlist) and media playlist text,
|
|
8
|
+
* returns a HAM-compliant resolved track with segments.
|
|
9
|
+
*
|
|
10
|
+
* @param text - Media playlist text content
|
|
11
|
+
* @param unresolved - Unresolved track from parseMultivariantPlaylist
|
|
12
|
+
* @returns Resolved track with segments (type inferred from input)
|
|
13
|
+
*/
|
|
14
|
+
function parseMediaPlaylist(text, unresolved) {
|
|
15
|
+
const lines = text.split(/\r?\n/);
|
|
16
|
+
const baseUrl = unresolved.url;
|
|
17
|
+
const segments = [];
|
|
18
|
+
let initSegmentUrl;
|
|
19
|
+
let initSegmentByteRange;
|
|
20
|
+
let currentDuration = 0;
|
|
21
|
+
let currentByteRange;
|
|
22
|
+
let currentTime = 0;
|
|
23
|
+
let segmentIndex = 0;
|
|
24
|
+
let previousByteRangeEnd;
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
|
|
28
|
+
if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-TARGETDURATION:") || trimmed.startsWith("#EXT-X-PLAYLIST-TYPE:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
|
|
29
|
+
const mapAttrs = matchTag(trimmed, "EXT-X-MAP");
|
|
30
|
+
if (mapAttrs) {
|
|
31
|
+
const uri = mapAttrs.get("URI");
|
|
32
|
+
if (uri) {
|
|
33
|
+
initSegmentUrl = resolveUrl(uri, baseUrl);
|
|
34
|
+
const byteRangeStr = mapAttrs.get("BYTERANGE");
|
|
35
|
+
if (byteRangeStr) initSegmentByteRange = parseByteRange(byteRangeStr, 0) ?? void 0;
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (trimmed.startsWith("#EXTINF:")) {
|
|
40
|
+
currentDuration = parseExtInfDuration(trimmed.slice(8));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (trimmed.startsWith("#EXT-X-BYTERANGE:")) {
|
|
44
|
+
currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? void 0;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (trimmed === "#EXT-X-ENDLIST") continue;
|
|
48
|
+
if (!trimmed.startsWith("#") && currentDuration > 0) {
|
|
49
|
+
const segment = {
|
|
50
|
+
id: `segment-${segmentIndex}`,
|
|
51
|
+
url: resolveUrl(trimmed, baseUrl),
|
|
52
|
+
duration: currentDuration,
|
|
53
|
+
startTime: currentTime
|
|
54
|
+
};
|
|
55
|
+
if (currentByteRange) {
|
|
56
|
+
segment.byteRange = currentByteRange;
|
|
57
|
+
previousByteRangeEnd = currentByteRange.end + 1;
|
|
58
|
+
} else previousByteRangeEnd = void 0;
|
|
59
|
+
segments.push(segment);
|
|
60
|
+
currentTime += currentDuration;
|
|
61
|
+
segmentIndex++;
|
|
62
|
+
currentDuration = 0;
|
|
63
|
+
currentByteRange = void 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const totalDuration = currentTime;
|
|
67
|
+
const initialization = unresolved.type === "text" && !initSegmentUrl ? void 0 : initSegmentUrl ? {
|
|
68
|
+
url: initSegmentUrl,
|
|
69
|
+
...initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}
|
|
70
|
+
} : { url: "" };
|
|
71
|
+
return {
|
|
72
|
+
...unresolved,
|
|
73
|
+
startTime: 0,
|
|
74
|
+
duration: totalDuration,
|
|
75
|
+
segments,
|
|
76
|
+
initialization
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { parseMediaPlaylist };
|
|
81
|
+
|
|
82
|
+
//# sourceMappingURL=parse-media-playlist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-media-playlist.js","names":[],"sources":["../../../../src/core/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":";;;;;;;;;;;;;AAkCA,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;AAInB,QAAO;EACL,GAAG;EACH,WAAW;EACX,UAAU;EACV;EACA;EACD"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { generateId } from "../utils/generate-id.js";
|
|
2
|
+
import { matchTag, parseCodecs } from "./parse-attributes.js";
|
|
3
|
+
import { resolveUrl } from "./resolve-url.js";
|
|
4
|
+
//#region src/core/hls/parse-multivariant.ts
|
|
5
|
+
/**
|
|
6
|
+
* Parse HLS multivariant playlist into a Presentation.
|
|
7
|
+
*
|
|
8
|
+
* Returns Presentation with partially resolved tracks (no segment information).
|
|
9
|
+
* Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs)
|
|
10
|
+
* but segment information is added when media playlists are fetched.
|
|
11
|
+
*
|
|
12
|
+
* @param text - Raw playlist text content
|
|
13
|
+
* @param unresolved - Unresolved presentation (contains URL for base URL resolution)
|
|
14
|
+
* @returns Presentation with partially resolved tracks (duration is undefined)
|
|
15
|
+
*/
|
|
16
|
+
function parseMultivariantPlaylist(text, unresolved) {
|
|
17
|
+
const baseUrl = unresolved.url;
|
|
18
|
+
const lines = text.split(/\r?\n/);
|
|
19
|
+
const streams = [];
|
|
20
|
+
const audioRenditions = [];
|
|
21
|
+
const subtitleRenditions = [];
|
|
22
|
+
let pendingStreamInfo = null;
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
|
|
26
|
+
if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
|
|
27
|
+
const mediaAttrs = matchTag(trimmed, "EXT-X-MEDIA");
|
|
28
|
+
if (mediaAttrs) {
|
|
29
|
+
const type = mediaAttrs.get("TYPE");
|
|
30
|
+
const groupId = mediaAttrs.get("GROUP-ID");
|
|
31
|
+
const name = mediaAttrs.get("NAME");
|
|
32
|
+
if (type === "AUDIO" && groupId && name) {
|
|
33
|
+
const uri = mediaAttrs.get("URI");
|
|
34
|
+
audioRenditions.push({
|
|
35
|
+
groupId,
|
|
36
|
+
name,
|
|
37
|
+
language: mediaAttrs.get("LANGUAGE"),
|
|
38
|
+
uri: uri ? resolveUrl(uri, baseUrl) : void 0,
|
|
39
|
+
default: mediaAttrs.getBool("DEFAULT"),
|
|
40
|
+
autoselect: mediaAttrs.getBool("AUTOSELECT")
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (type === "SUBTITLES" && groupId && name) {
|
|
44
|
+
const uri = mediaAttrs.get("URI");
|
|
45
|
+
if (uri) subtitleRenditions.push({
|
|
46
|
+
groupId,
|
|
47
|
+
name,
|
|
48
|
+
language: mediaAttrs.get("LANGUAGE"),
|
|
49
|
+
uri: resolveUrl(uri, baseUrl),
|
|
50
|
+
default: mediaAttrs.getBool("DEFAULT"),
|
|
51
|
+
autoselect: mediaAttrs.getBool("AUTOSELECT"),
|
|
52
|
+
forced: mediaAttrs.getBool("FORCED")
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const streamInfAttrs = matchTag(trimmed, "EXT-X-STREAM-INF");
|
|
58
|
+
if (streamInfAttrs) {
|
|
59
|
+
pendingStreamInfo = {
|
|
60
|
+
bandwidth: streamInfAttrs.getInt("BANDWIDTH", 0),
|
|
61
|
+
resolution: streamInfAttrs.getResolution("RESOLUTION"),
|
|
62
|
+
codecs: streamInfAttrs.get("CODECS"),
|
|
63
|
+
frameRate: streamInfAttrs.getFrameRate("FRAME-RATE"),
|
|
64
|
+
audioGroupId: streamInfAttrs.get("AUDIO")
|
|
65
|
+
};
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!trimmed.startsWith("#") && pendingStreamInfo) {
|
|
69
|
+
streams.push({
|
|
70
|
+
...pendingStreamInfo,
|
|
71
|
+
uri: resolveUrl(trimmed, baseUrl)
|
|
72
|
+
});
|
|
73
|
+
pendingStreamInfo = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const videoStreams = [];
|
|
77
|
+
const audioOnlyStreams = [];
|
|
78
|
+
for (const stream of streams) {
|
|
79
|
+
if (!stream.codecs) {
|
|
80
|
+
videoStreams.push(stream);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const parsedCodecs = parseCodecs(stream.codecs);
|
|
84
|
+
if (stream.codecs.split(",").length === 1) if (parsedCodecs.audio && !parsedCodecs.video) audioOnlyStreams.push(stream);
|
|
85
|
+
else videoStreams.push(stream);
|
|
86
|
+
else videoStreams.push(stream);
|
|
87
|
+
}
|
|
88
|
+
const videoTracks = videoStreams.map((stream) => {
|
|
89
|
+
const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
|
|
90
|
+
const track = {
|
|
91
|
+
type: "video",
|
|
92
|
+
id: generateId(),
|
|
93
|
+
url: stream.uri,
|
|
94
|
+
bandwidth: stream.bandwidth,
|
|
95
|
+
mimeType: "video/mp4",
|
|
96
|
+
codecs: []
|
|
97
|
+
};
|
|
98
|
+
if (stream.resolution?.width !== void 0) track.width = stream.resolution.width;
|
|
99
|
+
if (stream.resolution?.height !== void 0) track.height = stream.resolution.height;
|
|
100
|
+
if (codecs?.video) track.codecs = [codecs.video];
|
|
101
|
+
if (stream.frameRate) track.frameRate = stream.frameRate;
|
|
102
|
+
if (stream.audioGroupId) track.audioGroupId = stream.audioGroupId;
|
|
103
|
+
return track;
|
|
104
|
+
});
|
|
105
|
+
const audioOnlyTracks = audioOnlyStreams.map((stream) => {
|
|
106
|
+
const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
|
|
107
|
+
return {
|
|
108
|
+
type: "audio",
|
|
109
|
+
id: generateId(),
|
|
110
|
+
url: stream.uri,
|
|
111
|
+
bandwidth: stream.bandwidth,
|
|
112
|
+
mimeType: "audio/mp4",
|
|
113
|
+
codecs: codecs?.audio ? [codecs.audio] : [],
|
|
114
|
+
groupId: stream.audioGroupId || "default",
|
|
115
|
+
name: "Default",
|
|
116
|
+
sampleRate: 48e3,
|
|
117
|
+
channels: 2
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
const audioTracks = [...audioRenditions.map((rendition) => {
|
|
121
|
+
let audioCodecs;
|
|
122
|
+
for (const stream of streams) if (stream.audioGroupId === rendition.groupId && stream.codecs) {
|
|
123
|
+
const codecs = parseCodecs(stream.codecs);
|
|
124
|
+
if (codecs.audio) {
|
|
125
|
+
audioCodecs = [codecs.audio];
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const track = {
|
|
130
|
+
type: "audio",
|
|
131
|
+
id: generateId(),
|
|
132
|
+
url: rendition.uri ?? "",
|
|
133
|
+
groupId: rendition.groupId,
|
|
134
|
+
name: rendition.name,
|
|
135
|
+
mimeType: "audio/mp4",
|
|
136
|
+
bandwidth: 0,
|
|
137
|
+
sampleRate: 48e3,
|
|
138
|
+
channels: 2,
|
|
139
|
+
codecs: []
|
|
140
|
+
};
|
|
141
|
+
if (rendition.language) track.language = rendition.language;
|
|
142
|
+
if (audioCodecs) track.codecs = audioCodecs;
|
|
143
|
+
if (rendition.default) track.default = rendition.default;
|
|
144
|
+
if (rendition.autoselect) track.autoselect = rendition.autoselect;
|
|
145
|
+
return track;
|
|
146
|
+
}), ...audioOnlyTracks];
|
|
147
|
+
const textTracks = subtitleRenditions.map((rendition) => {
|
|
148
|
+
const track = {
|
|
149
|
+
type: "text",
|
|
150
|
+
id: generateId(),
|
|
151
|
+
url: rendition.uri,
|
|
152
|
+
groupId: rendition.groupId,
|
|
153
|
+
label: rendition.name,
|
|
154
|
+
kind: "subtitles",
|
|
155
|
+
mimeType: "text/vtt",
|
|
156
|
+
bandwidth: 0
|
|
157
|
+
};
|
|
158
|
+
if (rendition.language) track.language = rendition.language;
|
|
159
|
+
if (rendition.default && rendition.autoselect) track.default = true;
|
|
160
|
+
if (rendition.autoselect) track.autoselect = rendition.autoselect;
|
|
161
|
+
if (rendition.forced) track.forced = rendition.forced;
|
|
162
|
+
return track;
|
|
163
|
+
});
|
|
164
|
+
const selectionSets = [];
|
|
165
|
+
if (videoTracks.length > 0) {
|
|
166
|
+
const videoSwitchingSet = {
|
|
167
|
+
id: generateId(),
|
|
168
|
+
type: "video",
|
|
169
|
+
tracks: videoTracks
|
|
170
|
+
};
|
|
171
|
+
const videoSelectionSet = {
|
|
172
|
+
id: generateId(),
|
|
173
|
+
type: "video",
|
|
174
|
+
switchingSets: [videoSwitchingSet]
|
|
175
|
+
};
|
|
176
|
+
selectionSets.push(videoSelectionSet);
|
|
177
|
+
}
|
|
178
|
+
if (audioTracks.length > 0) {
|
|
179
|
+
const audioSwitchingSet = {
|
|
180
|
+
id: generateId(),
|
|
181
|
+
type: "audio",
|
|
182
|
+
tracks: audioTracks
|
|
183
|
+
};
|
|
184
|
+
const audioSelectionSet = {
|
|
185
|
+
id: generateId(),
|
|
186
|
+
type: "audio",
|
|
187
|
+
switchingSets: [audioSwitchingSet]
|
|
188
|
+
};
|
|
189
|
+
selectionSets.push(audioSelectionSet);
|
|
190
|
+
}
|
|
191
|
+
if (textTracks.length > 0) {
|
|
192
|
+
const textSwitchingSet = {
|
|
193
|
+
id: generateId(),
|
|
194
|
+
type: "text",
|
|
195
|
+
tracks: textTracks
|
|
196
|
+
};
|
|
197
|
+
const textSelectionSet = {
|
|
198
|
+
id: generateId(),
|
|
199
|
+
type: "text",
|
|
200
|
+
switchingSets: [textSwitchingSet]
|
|
201
|
+
};
|
|
202
|
+
selectionSets.push(textSelectionSet);
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
id: generateId(),
|
|
206
|
+
url: unresolved.url,
|
|
207
|
+
startTime: 0,
|
|
208
|
+
selectionSets
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
export { parseMultivariantPlaylist };
|
|
213
|
+
|
|
214
|
+
//# sourceMappingURL=parse-multivariant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-multivariant.js","names":[],"sources":["../../../../src/core/hls/parse-multivariant.ts"],"sourcesContent":["import 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 { generateId } from '../utils/generate-id';\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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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/core/hls/resolve-url.ts
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a potentially relative URL against a base URL using native URL API.
|
|
4
|
+
*/
|
|
5
|
+
function resolveUrl(url, baseUrl) {
|
|
6
|
+
return new URL(url, baseUrl).href;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { resolveUrl };
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=resolve-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-url.js","names":[],"sources":["../../../../src/core/hls/resolve-url.ts"],"sourcesContent":["/**\n * Resolve a potentially relative URL against a base URL using native URL API.\n */\nexport function resolveUrl(url: string, baseUrl: string): string {\n return new URL(url, baseUrl).href;\n}\n"],"mappings":";;;;AAGA,SAAgB,WAAW,KAAa,SAAyB;AAC/D,QAAO,IAAI,IAAI,KAAK,QAAQ,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { signal, untrack, update } from "./signals/primitives.js";
|
|
2
|
+
//#region src/core/machine.ts
|
|
3
|
+
/**
|
|
4
|
+
* Provisions the shared mechanics for all machine-like primitives: a snapshot
|
|
5
|
+
* signal, an untracked state reader, and a transition function.
|
|
6
|
+
*
|
|
7
|
+
* Internal — consumed by `createMachineActor` and `createMachineReactor`. Not part of the
|
|
8
|
+
* public API.
|
|
9
|
+
*/
|
|
10
|
+
function createMachineCore(initialSnapshot) {
|
|
11
|
+
const snapshotSignal = signal(initialSnapshot);
|
|
12
|
+
const getState = () => untrack(() => snapshotSignal.get().value);
|
|
13
|
+
const transition = (to) => update(snapshotSignal, (current) => ({
|
|
14
|
+
...current,
|
|
15
|
+
value: to
|
|
16
|
+
}));
|
|
17
|
+
return {
|
|
18
|
+
snapshotSignal,
|
|
19
|
+
getState,
|
|
20
|
+
transition
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { createMachineCore };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=machine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine.js","names":[],"sources":["../../../src/core/machine.ts"],"sourcesContent":["import type { ReadonlySignal } from './signals/primitives';\nimport { signal, untrack, update } from './signals/primitives';\n\n// =============================================================================\n// Shared snapshot type\n// =============================================================================\n\n/**\n * Base snapshot for all machine-like primitives (Actors and Reactors).\n * Carries only the finite state value. Actors extend this with `context`.\n */\nexport interface MachineSnapshot<State extends string> {\n value: State;\n}\n\n// =============================================================================\n// Shared interface\n// =============================================================================\n\n/**\n * Shared interface for all machine-like primitives.\n * Both Actors (message-driven) and Reactors (signal-driven) implement this.\n */\nexport interface Machine<Snapshot extends MachineSnapshot<string>> {\n readonly snapshot: ReadonlySignal<Snapshot>;\n destroy(): void;\n}\n\n// =============================================================================\n// Shared core factory\n// =============================================================================\n\n/**\n * Provisions the shared mechanics for all machine-like primitives: a snapshot\n * signal, an untracked state reader, and a transition function.\n *\n * Internal — consumed by `createMachineActor` and `createMachineReactor`. Not part of the\n * public API.\n */\nexport function createMachineCore<FullState extends string, Snapshot extends MachineSnapshot<FullState>>(\n initialSnapshot: Snapshot\n) {\n const snapshotSignal = signal(initialSnapshot);\n const getState = (): FullState => untrack(() => snapshotSignal.get().value);\n const transition = (to: FullState): void => update(snapshotSignal, (current) => ({ ...current, value: to }));\n return { snapshotSignal, getState, transition };\n}\n"],"mappings":";;;;;;;;;AAuCA,SAAgB,kBACd,iBACA;CACA,MAAM,iBAAiB,OAAO,gBAAgB;CAC9C,MAAM,iBAA4B,cAAc,eAAe,KAAK,CAAC,MAAM;CAC3E,MAAM,cAAc,OAAwB,OAAO,iBAAiB,aAAa;EAAE,GAAG;EAAS,OAAO;EAAI,EAAE;AAC5G,QAAO;EAAE;EAAgB;EAAU;EAAY"}
|