@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":"index.js","names":[],"sources":["../../../../src/media/types/index.ts"],"sourcesContent":["/**\n * Core SPF Types\n *\n * Based on CMAF-HAM (Common Media Application Format - Hypothetical Application Model)\n * Protocol-agnostic representation of streaming media content.\n *\n * @see https://github.com/AcademySoftwareFoundation/common-media-library\n */\n\n// =============================================================================\n// Base Types\n// =============================================================================\n\n/**\n * Base identifier type for all HAM objects.\n */\nexport interface Ham {\n id: string;\n}\n\n/**\n * Addressable resource with optional byte range.\n */\nexport interface AddressableObject {\n url: string;\n byteRange?: {\n start: number;\n end: number;\n };\n}\n\n// =============================================================================\n// Platform-agnostic Media Element\n// =============================================================================\n\n/**\n * Platform-agnostic media element interface.\n * Captures minimal shape needed for orchestration without DOM dependencies.\n * HTMLMediaElement satisfies this interface.\n */\nexport interface MediaElementLike {\n preload: string;\n}\n\n// =============================================================================\n// Time and Duration\n// =============================================================================\n\n/**\n * Time span with start time and duration.\n * Used for segments and other timed ranges.\n */\nexport interface TimeSpan {\n startTime: number;\n duration: number;\n}\n\n// =============================================================================\n// Enums\n// =============================================================================\n\n/**\n * Track content type.\n */\nexport type TrackType = 'video' | 'audio' | 'text';\n\n// =============================================================================\n// Frame Rate\n// =============================================================================\n\n/**\n * Video frame rate expressed as numerator/denominator.\n *\n * Examples:\n * - 30 fps: { frameRateNumerator: 30 }\n * - 29.97 fps: { frameRateNumerator: 30000, frameRateDenominator: 1001 }\n */\nexport interface FrameRate {\n frameRateNumerator: number;\n frameRateDenominator?: number;\n}\n\n// =============================================================================\n// Partially Resolved Tracks (before media playlist is fetched)\n// =============================================================================\n\n/**\n * Generic type for partially resolved tracks.\n * Removes fields that come from media playlist parsing.\n *\n * @param T - Track type to make partially resolved (must extend Track)\n */\nexport type PartiallyResolved<T extends Track = Track> = Omit<T, 'segments' | 'initialization' | keyof TimeSpan> & {\n segments?: never;\n duration?: never;\n startTime?: never;\n initialization?: never;\n};\n\n/**\n * Partially resolved video track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedVideoTrack = PartiallyResolved<VideoTrack>;\n\n/**\n * Partially resolved audio track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedAudioTrack = PartiallyResolved<AudioTrack>;\n\n// =============================================================================\n// Resolved Track Types (with segments from media playlist)\n// =============================================================================\n\n/**\n * Base track type containing common properties for all resolved tracks.\n * A resolved track has segments, duration, and initialization data.\n * All URLs are fully qualified (parsers resolve relative URLs).\n */\n/**\n * Track startTime is always 0 (for future multi-period support).\n */\nexport type Track = Ham &\n AddressableObject &\n TimeSpan & {\n type: TrackType;\n codecs?: string[]; // Optional per HLS spec\n mimeType: string;\n language?: string | undefined;\n bandwidth: number;\n initialization?: AddressableObject;\n segments: Segment[];\n };\n\n/**\n * Resolved video track with segments.\n */\nexport type VideoTrack = Track &\n Required<Pick<Track, 'initialization' | 'codecs'>> & {\n type: 'video';\n\n // Optional metadata from multivariant (per HLS spec)\n width?: number;\n height?: number;\n frameRate?: FrameRate;\n audioGroupId?: string;\n };\n\n/**\n * Resolved audio track with segments.\n */\nexport type AudioTrack = Track &\n Required<Pick<Track, 'initialization' | 'codecs'>> & {\n type: 'audio';\n groupId: string;\n name: string;\n sampleRate: number;\n channels: number;\n default?: boolean;\n autoselect?: boolean;\n };\n\n/**\n * Resolved text track with segments.\n */\nexport type TextTrack = Track & {\n type: 'text';\n groupId: string;\n label: string;\n kind: 'subtitles' | 'captions';\n default?: boolean;\n autoselect?: boolean;\n forced?: boolean;\n};\n\n/**\n * Minimal text-track cue shape — start time, end time, and display text.\n *\n * Host-agnostic representation. `VTTCue` structurally satisfies this\n * interface, so DOM consumers pass `VTTCue` values directly. Non-DOM\n * hosts (workers, test fakes, non-browser engines) can satisfy the same\n * shape without pulling in DOM types.\n */\nexport interface Cue {\n startTime: number;\n endTime: number;\n text: string;\n}\n\n/**\n * Media element with an iterable text-track list, host-agnostic.\n *\n * Extends `MediaElementLike` with the minimum surface needed to observe\n * which text tracks are currently mounted on the media. `HTMLMediaElement`\n * structurally satisfies this (its `textTracks` is a `TextTrackList`,\n * which is iterable with `{ id }` items).\n */\nexport interface MediaElementWithTextTracks extends MediaElementLike {\n readonly textTracks: Iterable<{ readonly id: string }>;\n}\n\n/**\n * Partially resolved text track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedTextTrack = PartiallyResolved<TextTrack>;\n\n/**\n * Union of all resolved track types.\n */\nexport type ResolvedTrack = VideoTrack | AudioTrack | TextTrack;\n\n/**\n * Union of all partially resolved track types.\n */\nexport type PartiallyResolvedTrack =\n | PartiallyResolvedVideoTrack\n | PartiallyResolvedAudioTrack\n | PartiallyResolvedTextTrack;\n\n// =============================================================================\n// Switching and Selection Sets\n// =============================================================================\n\n/**\n * Generic switching set type.\n * A group of tracks that can be switched between seamlessly.\n *\n * @param T - Track type (VideoTrack, AudioTrack, or TextTrack)\n */\nexport type SwitchingSetOf<T extends Track = Track> = Ham & {\n type: T['type'];\n tracks: (PartiallyResolved<T> | T)[];\n};\n\n/**\n * Video switching set - contains only video tracks (partially resolved or fully resolved).\n */\nexport type VideoSwitchingSet = SwitchingSetOf<VideoTrack>;\n\n/**\n * Audio switching set - contains only audio tracks (partially resolved or fully resolved).\n */\nexport type AudioSwitchingSet = SwitchingSetOf<AudioTrack>;\n\n/**\n * Text switching set - contains only text tracks (partially resolved or fully resolved).\n */\nexport type TextSwitchingSet = SwitchingSetOf<TextTrack>;\n\n/**\n * Switching set - a group of tracks that can be switched between seamlessly.\n * Discriminated by track type.\n */\nexport type SwitchingSet = VideoSwitchingSet | AudioSwitchingSet | TextSwitchingSet;\n\n/**\n * Generic selection set type.\n * Groups switching sets by track type.\n *\n * @param T - Track type (VideoTrack, AudioTrack, or TextTrack)\n */\nexport type SelectionSetOf<T extends Track = Track> = Ham & {\n type: T['type'];\n switchingSets: SwitchingSetOf<T>[];\n};\n\n/**\n * Video selection set - contains only video switching sets.\n */\nexport type VideoSelectionSet = SelectionSetOf<VideoTrack>;\n\n/**\n * Audio selection set - contains only audio switching sets.\n */\nexport type AudioSelectionSet = SelectionSetOf<AudioTrack>;\n\n/**\n * Text selection set - contains only text switching sets.\n */\nexport type TextSelectionSet = SelectionSetOf<TextTrack>;\n\n/**\n * Selection set - groups switching sets by track type.\n * Discriminated union ensures type-safe track access.\n */\nexport type SelectionSet = VideoSelectionSet | AudioSelectionSet | TextSelectionSet;\n\n// =============================================================================\n// Segment\n// =============================================================================\n\n/**\n * Media segment with timing information.\n * Follows CMAF-HAM composition pattern.\n */\nexport type Segment = Ham & AddressableObject & TimeSpan;\n\n/**\n * Floating-point tolerance for matching segments by `startTime`. Two\n * segments are considered the same position when\n * `Math.abs(a.startTime - b.startTime) < SEGMENT_TIME_EPSILON`. Used by\n * the source-buffer dedup and segment-loader quality-aware filter to\n * tolerate sub-millisecond drift in segment timestamps across multiple\n * playlists / quality levels.\n */\nexport const SEGMENT_TIME_EPSILON = 0.0001;\n\n// =============================================================================\n// Media Playlist Info\n// =============================================================================\n\n/**\n * Intermediate representation of a parsed media playlist.\n * Used internally before assembling into full Track structure.\n */\nexport interface MediaPlaylistInfo {\n version: number;\n targetDuration: number;\n playlistType: 'VOD' | 'EVENT' | undefined;\n initSegment: AddressableObject | null;\n segments: Segment[];\n duration: number;\n endList: boolean;\n}\n\n// =============================================================================\n// Presentation\n// =============================================================================\n\n/**\n * Presentation - a single playable period of content.\n * Uses TimeSpan fields (startTime always 0, duration optional until track resolved).\n *\n * Extends AddressableObject so `url` contains the original manifest URL.\n * All URLs are fully qualified (parsers resolve relative URLs).\n */\nexport type Presentation = Ham &\n AddressableObject &\n Partial<TimeSpan> & {\n selectionSets: SelectionSet[];\n };\n\n/**\n * State-shaped presentation that may or may not be resolved yet.\n *\n * The lifecycle is a single value: a caller writes `{ url }`, and the\n * resolver populates the rest in place. `url` is always present; resolved\n * fields (`id`, `selectionSets`, duration) appear once parsing succeeds.\n *\n * Use `isResolvedPresentation` to narrow to `Presentation`.\n */\nexport type MaybeResolvedPresentation = AddressableObject & Partial<Omit<Presentation, keyof AddressableObject>>;\n\n// =============================================================================\n// Type Guards\n// =============================================================================\n\n/**\n * Check if a track is resolved (has segments).\n * Works for all track types with overloaded signatures for type narrowing.\n */\nexport function isResolvedTrack(track: PartiallyResolvedVideoTrack | VideoTrack): track is VideoTrack;\nexport function isResolvedTrack(track: PartiallyResolvedAudioTrack | AudioTrack): track is AudioTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTextTrack | TextTrack): track is TextTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTrack | ResolvedTrack): track is ResolvedTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTrack | ResolvedTrack): track is ResolvedTrack {\n return 'segments' in track;\n}\n\n/**\n * Check if a presentation has duration (at least one track resolved).\n * Narrows type to include required duration.\n */\nexport function hasPresentationDuration(\n presentation: MaybeResolvedPresentation\n): presentation is MaybeResolvedPresentation & { duration: number } {\n return presentation.duration !== undefined;\n}\n\n/**\n * Narrows a `MaybeResolvedPresentation` to a fully resolved `Presentation`.\n *\n * A presentation is resolved once `resolvePresentation` has parsed the\n * manifest and populated both `id` and `selectionSets`. Both must be\n * present — a partial value with only one of them isn't usable, and\n * letting it through would have downstream behaviors crash when they\n * access `selectionSets`.\n */\nexport function isResolvedPresentation(\n presentation: MaybeResolvedPresentation | undefined\n): presentation is Presentation {\n return presentation !== undefined && presentation.id !== undefined && presentation.selectionSets !== undefined;\n}\n"],"mappings":";;;;;;;;;AAmTA,MAAa,uBAAuB;AA4DpC,SAAgB,gBAAgB,OAAuE;AACrG,QAAO,cAAc;;;;;;AAOvB,SAAgB,wBACd,cACkE;AAClE,QAAO,aAAa,aAAa,KAAA;;;;;;;;;;;AAYnC,SAAgB,uBACd,cAC8B;AAC9B,QAAO,iBAAiB,KAAA,KAAa,aAAa,OAAO,KAAA,KAAa,aAAa,kBAAkB,KAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/media/types/index.ts"],"sourcesContent":["/**\n * Core SPF Types\n *\n * Based on CMAF-HAM (Common Media Application Format - Hypothetical Application Model)\n * Protocol-agnostic representation of streaming media content.\n *\n * @see https://github.com/AcademySoftwareFoundation/common-media-library\n */\n\n// =============================================================================\n// Base Types\n// =============================================================================\n\n/**\n * Base identifier type for all HAM objects.\n */\nexport interface Ham {\n id: string;\n}\n\n/**\n * Addressable resource with optional byte range.\n */\nexport interface AddressableObject {\n url: string;\n byteRange?: {\n start: number;\n end: number;\n };\n}\n\n// =============================================================================\n// Platform-agnostic Media Element\n// =============================================================================\n\n/**\n * Platform-agnostic media element interface.\n * Captures minimal shape needed for orchestration without DOM dependencies.\n * HTMLMediaElement satisfies this interface.\n */\nexport interface MediaElementLike {\n preload: string;\n}\n\n// =============================================================================\n// Time and Duration\n// =============================================================================\n\n/**\n * Time span with start time and duration.\n * Used for segments and other timed ranges.\n */\nexport interface TimeSpan {\n startTime: number;\n duration: number;\n}\n\n// =============================================================================\n// Enums\n// =============================================================================\n\n/**\n * Track content type.\n */\nexport type TrackType = 'video' | 'audio' | 'text';\n\n// =============================================================================\n// Frame Rate\n// =============================================================================\n\n/**\n * Video frame rate expressed as numerator/denominator.\n *\n * Examples:\n * - 30 fps: { frameRateNumerator: 30 }\n * - 29.97 fps: { frameRateNumerator: 30000, frameRateDenominator: 1001 }\n */\nexport interface FrameRate {\n frameRateNumerator: number;\n frameRateDenominator?: number;\n}\n\n// =============================================================================\n// Partially Resolved Tracks (before media playlist is fetched)\n// =============================================================================\n\n/**\n * Generic type for partially resolved tracks.\n * Removes fields that come from media playlist parsing.\n *\n * @param T - Track type to make partially resolved (must extend Track)\n */\nexport type PartiallyResolved<T extends Track = Track> = Omit<T, 'segments' | 'initialization' | keyof TimeSpan> & {\n segments?: never;\n duration?: never;\n startTime?: never;\n initialization?: never;\n};\n\n/**\n * Partially resolved video track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedVideoTrack = PartiallyResolved<VideoTrack>;\n\n/**\n * Partially resolved audio track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedAudioTrack = PartiallyResolved<AudioTrack>;\n\n// =============================================================================\n// Resolved Track Types (with segments from media playlist)\n// =============================================================================\n\n/**\n * Base track type containing common properties for all resolved tracks.\n * A resolved track has segments, duration, and initialization data.\n * All URLs are fully qualified (parsers resolve relative URLs).\n */\n/**\n * Track startTime is always 0 (for future multi-period support).\n */\nexport type Track = Ham &\n AddressableObject &\n TimeSpan & {\n type: TrackType;\n codecs?: string[]; // Optional per HLS spec\n mimeType: string;\n language?: string | undefined;\n bandwidth: number;\n initialization?: AddressableObject;\n segments: Segment[];\n };\n\n/**\n * Resolved video track with segments.\n */\nexport type VideoTrack = Track &\n Required<Pick<Track, 'initialization' | 'codecs'>> & {\n type: 'video';\n\n // Optional metadata from multivariant (per HLS spec)\n width?: number;\n height?: number;\n frameRate?: FrameRate;\n /**\n * Audio groups (`EXT-X-STREAM-INF:AUDIO`) this video rendition can pair\n * with. A list because one rendition is typically listed across multiple\n * `EXT-X-STREAM-INF` entries — one per audio group (the HLS cross-product) —\n * which the parser collapses into a single track carrying every group it\n * advertised.\n */\n audioGroupIds?: string[];\n };\n\n/**\n * Resolved audio track with segments.\n */\nexport type AudioTrack = Track &\n Required<Pick<Track, 'initialization' | 'codecs'>> & {\n type: 'audio';\n groupId: string;\n name: string;\n sampleRate: number;\n channels: number;\n default?: boolean;\n autoselect?: boolean;\n };\n\n/**\n * Resolved text track with segments.\n */\nexport type TextTrack = Track & {\n type: 'text';\n groupId: string;\n label: string;\n kind: 'subtitles' | 'captions';\n default?: boolean;\n autoselect?: boolean;\n forced?: boolean;\n};\n\n/**\n * Predicate that answers \"can this environment decode this track?\" — the\n * capability-probing surface, read by the track-switching hard-constraint\n * pre-pass (`excludeUnplayableTracks`) to drop undecodable renditions before\n * selection. Kept DOM-free here (a plain function type over a minimal track\n * shape) so DOM-free behaviors can consume it; the DOM implementation\n * (`canPlayTrack` in `media/dom/capabilities.ts`) wraps\n * `MediaSource.isTypeSupported`.\n *\n * Takes the minimal codec-bearing shape both video and audio candidates\n * carry. `mimeType` is optional so unprobeable candidates (no MIME) can be\n * passed straight through as playable rather than dropped.\n */\nexport type CanPlayTrack = (track: { mimeType?: string; codecs?: string[] }) => boolean;\n\n/**\n * Minimal text-track cue shape — start time, end time, and display text.\n *\n * Host-agnostic representation. `VTTCue` structurally satisfies this\n * interface, so DOM consumers pass `VTTCue` values directly. Non-DOM\n * hosts (workers, test fakes, non-browser engines) can satisfy the same\n * shape without pulling in DOM types.\n */\nexport interface Cue {\n startTime: number;\n endTime: number;\n text: string;\n}\n\n/**\n * Media element with an iterable text-track list, host-agnostic.\n *\n * Extends `MediaElementLike` with the minimum surface needed to observe\n * which text tracks are currently mounted on the media. `HTMLMediaElement`\n * structurally satisfies this (its `textTracks` is a `TextTrackList`,\n * which is iterable with `{ id }` items).\n */\nexport interface MediaElementWithTextTracks extends MediaElementLike {\n readonly textTracks: Iterable<{ readonly id: string }>;\n}\n\n/**\n * Partially resolved text track from multivariant playlist.\n * Has metadata but no segments or initialization yet (media playlist not fetched).\n */\nexport type PartiallyResolvedTextTrack = PartiallyResolved<TextTrack>;\n\n/**\n * Union of all resolved track types.\n */\nexport type ResolvedTrack = VideoTrack | AudioTrack | TextTrack;\n\n/**\n * Union of all partially resolved track types.\n */\nexport type PartiallyResolvedTrack =\n | PartiallyResolvedVideoTrack\n | PartiallyResolvedAudioTrack\n | PartiallyResolvedTextTrack;\n\n// =============================================================================\n// Switching and Selection Sets\n// =============================================================================\n\n/**\n * Generic switching set type.\n * A group of tracks that can be switched between seamlessly.\n *\n * @param T - Track type (VideoTrack, AudioTrack, or TextTrack)\n */\nexport type SwitchingSetOf<T extends Track = Track> = Ham & {\n type: T['type'];\n tracks: (PartiallyResolved<T> | T)[];\n};\n\n/**\n * Video switching set - contains only video tracks (partially resolved or fully resolved).\n */\nexport type VideoSwitchingSet = SwitchingSetOf<VideoTrack>;\n\n/**\n * Audio switching set - contains only audio tracks (partially resolved or fully resolved).\n */\nexport type AudioSwitchingSet = SwitchingSetOf<AudioTrack>;\n\n/**\n * Text switching set - contains only text tracks (partially resolved or fully resolved).\n */\nexport type TextSwitchingSet = SwitchingSetOf<TextTrack>;\n\n/**\n * Switching set - a group of tracks that can be switched between seamlessly.\n * Discriminated by track type.\n */\nexport type SwitchingSet = VideoSwitchingSet | AudioSwitchingSet | TextSwitchingSet;\n\n/**\n * Generic selection set type.\n * Groups switching sets by track type.\n *\n * @param T - Track type (VideoTrack, AudioTrack, or TextTrack)\n */\nexport type SelectionSetOf<T extends Track = Track> = Ham & {\n type: T['type'];\n switchingSets: SwitchingSetOf<T>[];\n};\n\n/**\n * Video selection set - contains only video switching sets.\n */\nexport type VideoSelectionSet = SelectionSetOf<VideoTrack>;\n\n/**\n * Audio selection set - contains only audio switching sets.\n */\nexport type AudioSelectionSet = SelectionSetOf<AudioTrack>;\n\n/**\n * Text selection set - contains only text switching sets.\n */\nexport type TextSelectionSet = SelectionSetOf<TextTrack>;\n\n/**\n * Selection set - groups switching sets by track type.\n * Discriminated union ensures type-safe track access.\n */\nexport type SelectionSet = VideoSelectionSet | AudioSelectionSet | TextSelectionSet;\n\n// =============================================================================\n// Segment\n// =============================================================================\n\n/**\n * Media segment with timing information.\n * Follows CMAF-HAM composition pattern.\n */\nexport type Segment = Ham & AddressableObject & TimeSpan;\n\n/**\n * Floating-point tolerance for matching segments by `startTime`. Two\n * segments are considered the same position when\n * `Math.abs(a.startTime - b.startTime) < SEGMENT_TIME_EPSILON`. Used by\n * the source-buffer dedup and segment-loader quality-aware filter to\n * tolerate sub-millisecond drift in segment timestamps across multiple\n * playlists / quality levels.\n */\nexport const SEGMENT_TIME_EPSILON = 0.0001;\n\n// =============================================================================\n// Media Playlist Info\n// =============================================================================\n\n/**\n * Intermediate representation of a parsed media playlist.\n * Used internally before assembling into full Track structure.\n */\nexport interface MediaPlaylistInfo {\n version: number;\n targetDuration: number;\n playlistType: 'VOD' | 'EVENT' | undefined;\n initSegment: AddressableObject | null;\n segments: Segment[];\n duration: number;\n endList: boolean;\n}\n\n// =============================================================================\n// Presentation\n// =============================================================================\n\n/**\n * Presentation - a single playable period of content.\n * Uses TimeSpan fields (startTime always 0, duration optional until track resolved).\n *\n * Extends AddressableObject so `url` contains the original manifest URL.\n * All URLs are fully qualified (parsers resolve relative URLs).\n */\nexport type Presentation = Ham &\n AddressableObject &\n Partial<TimeSpan> & {\n selectionSets: SelectionSet[];\n };\n\n/**\n * State-shaped presentation that may or may not be resolved yet.\n *\n * The lifecycle is a single value: a caller writes `{ url }`, and the\n * resolver populates the rest in place. `url` is always present; resolved\n * fields (`id`, `selectionSets`, duration) appear once parsing succeeds.\n *\n * Use `isResolvedPresentation` to narrow to `Presentation`.\n */\nexport type MaybeResolvedPresentation = AddressableObject & Partial<Omit<Presentation, keyof AddressableObject>>;\n\n// =============================================================================\n// Type Guards\n// =============================================================================\n\n/**\n * Check if a track is resolved (has segments).\n * Works for all track types with overloaded signatures for type narrowing.\n */\nexport function isResolvedTrack(track: PartiallyResolvedVideoTrack | VideoTrack): track is VideoTrack;\nexport function isResolvedTrack(track: PartiallyResolvedAudioTrack | AudioTrack): track is AudioTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTextTrack | TextTrack): track is TextTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTrack | ResolvedTrack): track is ResolvedTrack;\nexport function isResolvedTrack(track: PartiallyResolvedTrack | ResolvedTrack): track is ResolvedTrack {\n return 'segments' in track;\n}\n\n/**\n * Check if a presentation has duration (at least one track resolved).\n * Narrows type to include required duration.\n */\nexport function hasPresentationDuration(\n presentation: MaybeResolvedPresentation\n): presentation is MaybeResolvedPresentation & { duration: number } {\n return presentation.duration !== undefined;\n}\n\n/**\n * Narrows a `MaybeResolvedPresentation` to a fully resolved `Presentation`.\n *\n * A presentation is resolved once `resolvePresentation` has parsed the\n * manifest and populated both `id` and `selectionSets`. Both must be\n * present — a partial value with only one of them isn't usable, and\n * letting it through would have downstream behaviors crash when they\n * access `selectionSets`.\n */\nexport function isResolvedPresentation(\n presentation: MaybeResolvedPresentation | undefined\n): presentation is Presentation {\n return presentation !== undefined && presentation.id !== undefined && presentation.selectionSets !== undefined;\n}\n"],"mappings":";;;;;;;;;AAyUA,MAAa,uBAAuB;AA4DpC,SAAgB,gBAAgB,OAAuE;AACrG,QAAO,cAAc;;;;;;AAOvB,SAAgB,wBACd,cACkE;AAClE,QAAO,aAAa,aAAa,KAAA;;;;;;;;;;;AAYnC,SAAgB,uBACd,cAC8B;AAC9B,QAAO,iBAAiB,KAAA,KAAa,aAAa,OAAO,KAAA,KAAa,aAAa,kBAAkB,KAAA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/media/utils/cdn.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Derive a stable grouping key for the CDN a URL is served from. Synchronous and
|
|
4
|
+
* pure (deliberately not a `resolve*` — no fetch). Consumers override the
|
|
5
|
+
* default via the engine's `getCdnId` config (e.g. to key on Mux's `cdn=` query
|
|
6
|
+
* param instead of the host); every CDN-identity site reads that same function
|
|
7
|
+
* so keys stay comparable across `cdnPriority`, `failedCdns`, and the
|
|
8
|
+
* track-switching constraint + scope.
|
|
9
|
+
*/
|
|
10
|
+
type GetCdnId = (url: string) => string;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { GetCdnId };
|
|
13
|
+
//# sourceMappingURL=cdn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdn.d.ts","names":[],"sources":["../../../../src/media/utils/cdn.ts"],"mappings":";AAUA;;;;;;;;AAAA,KAAY,QAAA,IAAY,GAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region src/media/utils/cdn.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default {@link GetCdnId}: the URL's origin (scheme + host + port); falls back
|
|
4
|
+
* to the raw string when the URL can't be parsed, so the return value is always
|
|
5
|
+
* a stable grouping key.
|
|
6
|
+
*/
|
|
7
|
+
function getCdnId(url) {
|
|
8
|
+
try {
|
|
9
|
+
return new URL(url).origin;
|
|
10
|
+
} catch {
|
|
11
|
+
return url;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const CDN_TYPE_PRIORITY = {
|
|
15
|
+
video: 0,
|
|
16
|
+
audio: 1,
|
|
17
|
+
text: 2
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The distinct CDNs a presentation's tracks are served from, ordered video CDNs
|
|
21
|
+
* first, then audio, then text (manifest order within a type). The head is the
|
|
22
|
+
* primary CDN — the one a sticky pick defaults to — and is always video-derived
|
|
23
|
+
* when the source has video. Returns `[]` for an unresolved presentation with
|
|
24
|
+
* no tracks.
|
|
25
|
+
*
|
|
26
|
+
* Redundant-stream sources list the same content on multiple hosts (e.g. Mux's
|
|
27
|
+
* `?redundant_streams=true`), so each host contributes its own candidate tracks;
|
|
28
|
+
* this collapses them to the set of CDNs across every track type. The CDN-id
|
|
29
|
+
* derivation defaults to {@link getCdnId}; pass a consumer-configured `getId` to
|
|
30
|
+
* key on something other than origin.
|
|
31
|
+
*/
|
|
32
|
+
function getOrderedCdnIds(presentation, getId = getCdnId) {
|
|
33
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34
|
+
const ids = [];
|
|
35
|
+
const selectionSets = [...presentation.selectionSets ?? []].sort((a, b) => CDN_TYPE_PRIORITY[a.type] - CDN_TYPE_PRIORITY[b.type]);
|
|
36
|
+
for (const selectionSet of selectionSets) for (const switchingSet of selectionSet.switchingSets) for (const track of switchingSet.tracks) {
|
|
37
|
+
const id = getId(track.url);
|
|
38
|
+
if (seen.has(id)) continue;
|
|
39
|
+
seen.add(id);
|
|
40
|
+
ids.push(id);
|
|
41
|
+
}
|
|
42
|
+
return ids;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Add a CDN id to a failed-CDN list, preserving order and ignoring duplicates.
|
|
46
|
+
* Idempotent: re-adding an already-present id returns the same array reference
|
|
47
|
+
* (so a no-op trip doesn't churn the `failedCdns` signal). The failover trip in
|
|
48
|
+
* `resolve-track` and the segment loaders feed this into `failedCdns` via `update`.
|
|
49
|
+
*/
|
|
50
|
+
function addFailedCdn(failed, cdn) {
|
|
51
|
+
return failed?.includes(cdn) ? failed : [...failed ?? [], cdn];
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { addFailedCdn, getCdnId, getOrderedCdnIds };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=cdn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdn.js","names":[],"sources":["../../../../src/media/utils/cdn.ts"],"sourcesContent":["import type { MaybeResolvedPresentation, TrackType } from '../types';\n\n/**\n * Derive a stable grouping key for the CDN a URL is served from. Synchronous and\n * pure (deliberately not a `resolve*` — no fetch). Consumers override the\n * default via the engine's `getCdnId` config (e.g. to key on Mux's `cdn=` query\n * param instead of the host); every CDN-identity site reads that same function\n * so keys stay comparable across `cdnPriority`, `failedCdns`, and the\n * track-switching constraint + scope.\n */\nexport type GetCdnId = (url: string) => string;\n\n/**\n * Default {@link GetCdnId}: the URL's origin (scheme + host + port); falls back\n * to the raw string when the URL can't be parsed, so the return value is always\n * a stable grouping key.\n */\nexport function getCdnId(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return url;\n }\n}\n\n// Track-type priority for CDN ordering: video first, then audio, then text.\n// Selection sets are visited in this order so the head of the returned list is\n// always video-derived. `preferActiveCdn` anchors every track type to the\n// first CDN with surviving tracks (the head), so this makes \"the primary CDN\n// is the video CDN\" a guarantee of `getOrderedCdnIds` rather than a side effect\n// of the order tracks happen to be parsed in.\nconst CDN_TYPE_PRIORITY: Record<TrackType, number> = { video: 0, audio: 1, text: 2 };\n\n/**\n * The distinct CDNs a presentation's tracks are served from, ordered video CDNs\n * first, then audio, then text (manifest order within a type). The head is the\n * primary CDN — the one a sticky pick defaults to — and is always video-derived\n * when the source has video. Returns `[]` for an unresolved presentation with\n * no tracks.\n *\n * Redundant-stream sources list the same content on multiple hosts (e.g. Mux's\n * `?redundant_streams=true`), so each host contributes its own candidate tracks;\n * this collapses them to the set of CDNs across every track type. The CDN-id\n * derivation defaults to {@link getCdnId}; pass a consumer-configured `getId` to\n * key on something other than origin.\n */\nexport function getOrderedCdnIds(presentation: MaybeResolvedPresentation, getId: GetCdnId = getCdnId): string[] {\n const seen = new Set<string>();\n const ids: string[] = [];\n // Stable sort keeps manifest order among same-type selection sets.\n const selectionSets = [...(presentation.selectionSets ?? [])].sort(\n (a, b) => CDN_TYPE_PRIORITY[a.type] - CDN_TYPE_PRIORITY[b.type]\n );\n for (const selectionSet of selectionSets) {\n for (const switchingSet of selectionSet.switchingSets) {\n for (const track of switchingSet.tracks) {\n const id = getId(track.url);\n if (seen.has(id)) continue;\n seen.add(id);\n ids.push(id);\n }\n }\n }\n return ids;\n}\n\n/**\n * Add a CDN id to a failed-CDN list, preserving order and ignoring duplicates.\n * Idempotent: re-adding an already-present id returns the same array reference\n * (so a no-op trip doesn't churn the `failedCdns` signal). The failover trip in\n * `resolve-track` and the segment loaders feed this into `failedCdns` via `update`.\n */\nexport function addFailedCdn(failed: string[] | undefined, cdn: string): string[] {\n return failed?.includes(cdn) ? failed : [...(failed ?? []), cdn];\n}\n"],"mappings":";;;;;;AAiBA,SAAgB,SAAS,KAAqB;AAC5C,KAAI;AACF,SAAO,IAAI,IAAI,IAAI,CAAC;SACd;AACN,SAAO;;;AAUX,MAAM,oBAA+C;CAAE,OAAO;CAAG,OAAO;CAAG,MAAM;CAAG;;;;;;;;;;;;;;AAepF,SAAgB,iBAAiB,cAAyC,QAAkB,UAAoB;CAC9G,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,MAAgB,EAAE;CAExB,MAAM,gBAAgB,CAAC,GAAI,aAAa,iBAAiB,EAAE,CAAE,CAAC,MAC3D,GAAG,MAAM,kBAAkB,EAAE,QAAQ,kBAAkB,EAAE,MAC3D;AACD,MAAK,MAAM,gBAAgB,cACzB,MAAK,MAAM,gBAAgB,aAAa,cACtC,MAAK,MAAM,SAAS,aAAa,QAAQ;EACvC,MAAM,KAAK,MAAM,MAAM,IAAI;AAC3B,MAAI,KAAK,IAAI,GAAG,CAAE;AAClB,OAAK,IAAI,GAAG;AACZ,MAAI,KAAK,GAAG;;AAIlB,QAAO;;;;;;;;AAST,SAAgB,aAAa,QAA8B,KAAuB;AAChF,QAAO,QAAQ,SAAS,IAAI,GAAG,SAAS,CAAC,GAAI,UAAU,EAAE,EAAG,IAAI"}
|
|
@@ -81,6 +81,33 @@ function hasCodecs(track) {
|
|
|
81
81
|
return !!track && "codecs" in track && !!track.codecs?.length;
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
+
* Set `mimeType` on every track of one `type` (immutably). Used to propagate a
|
|
85
|
+
* detected container across a type's renditions: an ABR ladder is the same
|
|
86
|
+
* content at different bitrates, so one rendition's container holds for all of
|
|
87
|
+
* them — capability probing + SourceBuffer setup then get the right MIME for the
|
|
88
|
+
* whole type from a single resolved media playlist, without fetching the rest.
|
|
89
|
+
*
|
|
90
|
+
* Scoped to one type on purpose: propagating *across* audio/video would be wrong
|
|
91
|
+
* for mixed-container sources (e.g. muxed-TS video + raw-`.aac` audio) and races
|
|
92
|
+
* concurrent per-type resolution. Same-type writes are disjoint and safe.
|
|
93
|
+
* Idempotent — tracks already at `mimeType` are left as-is.
|
|
94
|
+
*/
|
|
95
|
+
function applyContainerMimeType(presentation, type, mimeType) {
|
|
96
|
+
return {
|
|
97
|
+
...presentation,
|
|
98
|
+
selectionSets: presentation.selectionSets.map((selectionSet) => selectionSet.type === type ? {
|
|
99
|
+
...selectionSet,
|
|
100
|
+
switchingSets: selectionSet.switchingSets.map((switchingSet) => ({
|
|
101
|
+
...switchingSet,
|
|
102
|
+
tracks: switchingSet.tracks.map((track) => track.mimeType === mimeType ? track : {
|
|
103
|
+
...track,
|
|
104
|
+
mimeType
|
|
105
|
+
})
|
|
106
|
+
}))
|
|
107
|
+
} : selectionSet)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
84
111
|
* Updates a track within a presentation (immutably). Generic — works for
|
|
85
112
|
* video, audio, or text tracks.
|
|
86
113
|
*/
|
|
@@ -98,6 +125,6 @@ function updateTrackInPresentation(presentation, resolvedTrack) {
|
|
|
98
125
|
};
|
|
99
126
|
}
|
|
100
127
|
//#endregion
|
|
101
|
-
export { findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack, findTrack, findTrackById, getTracksByType, hasCodecs, updateTrackInPresentation };
|
|
128
|
+
export { applyContainerMimeType, findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack, findTrack, findTrackById, getTracksByType, hasCodecs, updateTrackInPresentation };
|
|
102
129
|
|
|
103
130
|
//# sourceMappingURL=tracks.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracks.js","names":[],"sources":["../../../../src/media/utils/tracks.ts"],"sourcesContent":["import type {\n AudioTrack,\n MaybeResolvedPresentation,\n PartiallyResolvedTrack,\n Presentation,\n ResolvedTrack,\n TextTrack,\n TrackType,\n VideoTrack,\n} from '../types';\nimport { isResolvedTrack } from '../types';\n\n/**\n * Get the tracks of the given type from a presentation's first switching set.\n *\n * Returns `[]` when the presentation is unresolved, when no selection set of\n * `type` exists, or when its first switching set is empty. Returned tracks may\n * be partially resolved (URL only) or fully resolved (with segments) — callers\n * narrow as needed.\n *\n * The \"first switching set\" assumption matches the rest of the codebase\n * (HLS typically has one switching set per type); multi-group / multi-period\n * support would generalize this.\n */\nexport function getTracksByType(\n presentation: MaybeResolvedPresentation,\n type: TrackType\n): readonly (PartiallyResolvedTrack | ResolvedTrack)[] {\n return presentation.selectionSets?.find(({ type: t }) => t === type)?.switchingSets[0]?.tracks ?? [];\n}\n\n/**\n * Find a track of the given type and id within a presentation.\n *\n * Returns the matching track from the first switching set of the matching\n * selection set, or `undefined` if either is missing. The returned track may\n * be partially resolved (URL only) or fully resolved (with segments) — callers\n * narrow as needed.\n */\nexport function findTrack(\n presentation: MaybeResolvedPresentation,\n type: TrackType,\n trackId: string\n): PartiallyResolvedTrack | ResolvedTrack | undefined {\n return getTracksByType(presentation, type).find(({ id }) => id === trackId);\n}\n\n/**\n * Find a track by id across all selection sets in a presentation, without\n * knowing its type up front. Used when the caller has a track id obtained\n * from a downstream consumer (e.g. `SourceBufferActor.initTrackId`) and\n * needs to locate the corresponding track in the presentation.\n *\n * Track ids are unique within a presentation per the HLS spec; the first\n * match wins.\n */\nexport function findTrackById(\n presentation: MaybeResolvedPresentation,\n trackId: string\n): PartiallyResolvedTrack | ResolvedTrack | undefined {\n for (const selectionSet of presentation.selectionSets ?? []) {\n const track = selectionSet.switchingSets[0]?.tracks.find(({ id }) => id === trackId);\n if (track) return track;\n }\n return undefined;\n}\n\n/**\n * Find a text track of the given id within a presentation and narrow it to\n * the fully-resolved `TextTrack` shape (segments populated). Returns\n * `undefined` if no track matches the id, the matching track isn't a text\n * track, or it hasn't been resolved yet.\n *\n * The segments-non-empty check stays at the call site — a resolved track\n * with zero segments is a valid state, distinct from \"ready to load.\"\n */\nexport function findResolvedTextTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): TextTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'text', trackId);\n // `findTrack` returns the wide union; narrow via discriminant before\n // applying `isResolvedTrack`'s text-specific overload.\n if (!track || track.type !== 'text' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\nexport function findResolvedVideoTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): VideoTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'video', trackId);\n if (!track || track.type !== 'video' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\nexport function findResolvedAudioTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): AudioTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'audio', trackId);\n if (!track || track.type !== 'audio' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\n/**\n * Whether a track carries a non-empty `codecs` array. Both partially-\n * resolved and fully-resolved tracks may carry codecs — they come from\n * the multivariant playlist's `EXT-X-STREAM-INF` line, not from the\n * per-type media playlist — so this works at either resolution stage.\n *\n * `TextTrack` doesn't declare a `codecs` field; the `'codecs' in track`\n * check narrows it out for the false branch.\n */\nexport function hasCodecs(track: PartiallyResolvedTrack | ResolvedTrack | undefined): boolean {\n return !!track && 'codecs' in track && !!track.codecs?.length;\n}\n\n/**\n * Updates a track within a presentation (immutably). Generic — works for\n * video, audio, or text tracks.\n */\nexport function updateTrackInPresentation<T extends ResolvedTrack>(\n presentation: Presentation,\n resolvedTrack: T\n): Presentation {\n const trackId = resolvedTrack.id;\n return {\n ...presentation,\n selectionSets: presentation.selectionSets.map((selectionSet) => ({\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) => (track.id === trackId ? resolvedTrack : track)),\n })),\n })),\n } as Presentation;\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,SAAgB,gBACd,cACA,MACqD;AACrD,QAAO,aAAa,eAAe,MAAM,EAAE,MAAM,QAAQ,MAAM,KAAK,EAAE,cAAc,IAAI,UAAU,EAAE;;;;;;;;;;AAWtG,SAAgB,UACd,cACA,MACA,SACoD;AACpD,QAAO,gBAAgB,cAAc,KAAK,CAAC,MAAM,EAAE,SAAS,OAAO,QAAQ;;;;;;;;;;;AAY7E,SAAgB,cACd,cACA,SACoD;AACpD,MAAK,MAAM,gBAAgB,aAAa,iBAAiB,EAAE,EAAE;EAC3D,MAAM,QAAQ,aAAa,cAAc,IAAI,OAAO,MAAM,EAAE,SAAS,OAAO,QAAQ;AACpF,MAAI,MAAO,QAAO;;;;;;;;;;;;AActB,SAAgB,sBACd,cACA,SACuB;AACvB,KAAI,CAAC,gBAAgB,CAAC,QAAS,QAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,QAAQ,QAAQ;AAGtD,KAAI,CAAC,SAAS,MAAM,SAAS,UAAU,CAAC,gBAAgB,MAAM,CAAE,QAAO,KAAA;AACvE,QAAO;;AAGT,SAAgB,uBACd,cACA,SACwB;AACxB,KAAI,CAAC,gBAAgB,CAAC,QAAS,QAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,SAAS,QAAQ;AACvD,KAAI,CAAC,SAAS,MAAM,SAAS,WAAW,CAAC,gBAAgB,MAAM,CAAE,QAAO,KAAA;AACxE,QAAO;;AAGT,SAAgB,uBACd,cACA,SACwB;AACxB,KAAI,CAAC,gBAAgB,CAAC,QAAS,QAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,SAAS,QAAQ;AACvD,KAAI,CAAC,SAAS,MAAM,SAAS,WAAW,CAAC,gBAAgB,MAAM,CAAE,QAAO,KAAA;AACxE,QAAO;;;;;;;;;;;AAYT,SAAgB,UAAU,OAAoE;AAC5F,QAAO,CAAC,CAAC,SAAS,YAAY,SAAS,CAAC,CAAC,MAAM,QAAQ;;;;;;
|
|
1
|
+
{"version":3,"file":"tracks.js","names":[],"sources":["../../../../src/media/utils/tracks.ts"],"sourcesContent":["import type {\n AudioTrack,\n MaybeResolvedPresentation,\n PartiallyResolvedTrack,\n Presentation,\n ResolvedTrack,\n TextTrack,\n TrackType,\n VideoTrack,\n} from '../types';\nimport { isResolvedTrack } from '../types';\n\n/**\n * Get the tracks of the given type from a presentation's first switching set.\n *\n * Returns `[]` when the presentation is unresolved, when no selection set of\n * `type` exists, or when its first switching set is empty. Returned tracks may\n * be partially resolved (URL only) or fully resolved (with segments) — callers\n * narrow as needed.\n *\n * The \"first switching set\" assumption matches the rest of the codebase\n * (HLS typically has one switching set per type); multi-group / multi-period\n * support would generalize this.\n */\nexport function getTracksByType(\n presentation: MaybeResolvedPresentation,\n type: TrackType\n): readonly (PartiallyResolvedTrack | ResolvedTrack)[] {\n return presentation.selectionSets?.find(({ type: t }) => t === type)?.switchingSets[0]?.tracks ?? [];\n}\n\n/**\n * Find a track of the given type and id within a presentation.\n *\n * Returns the matching track from the first switching set of the matching\n * selection set, or `undefined` if either is missing. The returned track may\n * be partially resolved (URL only) or fully resolved (with segments) — callers\n * narrow as needed.\n */\nexport function findTrack(\n presentation: MaybeResolvedPresentation,\n type: TrackType,\n trackId: string\n): PartiallyResolvedTrack | ResolvedTrack | undefined {\n return getTracksByType(presentation, type).find(({ id }) => id === trackId);\n}\n\n/**\n * Find a track by id across all selection sets in a presentation, without\n * knowing its type up front. Used when the caller has a track id obtained\n * from a downstream consumer (e.g. `SourceBufferActor.initTrackId`) and\n * needs to locate the corresponding track in the presentation.\n *\n * Track ids are unique within a presentation per the HLS spec; the first\n * match wins.\n */\nexport function findTrackById(\n presentation: MaybeResolvedPresentation,\n trackId: string\n): PartiallyResolvedTrack | ResolvedTrack | undefined {\n for (const selectionSet of presentation.selectionSets ?? []) {\n const track = selectionSet.switchingSets[0]?.tracks.find(({ id }) => id === trackId);\n if (track) return track;\n }\n return undefined;\n}\n\n/**\n * Find a text track of the given id within a presentation and narrow it to\n * the fully-resolved `TextTrack` shape (segments populated). Returns\n * `undefined` if no track matches the id, the matching track isn't a text\n * track, or it hasn't been resolved yet.\n *\n * The segments-non-empty check stays at the call site — a resolved track\n * with zero segments is a valid state, distinct from \"ready to load.\"\n */\nexport function findResolvedTextTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): TextTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'text', trackId);\n // `findTrack` returns the wide union; narrow via discriminant before\n // applying `isResolvedTrack`'s text-specific overload.\n if (!track || track.type !== 'text' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\nexport function findResolvedVideoTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): VideoTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'video', trackId);\n if (!track || track.type !== 'video' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\nexport function findResolvedAudioTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): AudioTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'audio', trackId);\n if (!track || track.type !== 'audio' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\n/**\n * Whether a track carries a non-empty `codecs` array. Both partially-\n * resolved and fully-resolved tracks may carry codecs — they come from\n * the multivariant playlist's `EXT-X-STREAM-INF` line, not from the\n * per-type media playlist — so this works at either resolution stage.\n *\n * `TextTrack` doesn't declare a `codecs` field; the `'codecs' in track`\n * check narrows it out for the false branch.\n */\nexport function hasCodecs(track: PartiallyResolvedTrack | ResolvedTrack | undefined): boolean {\n return !!track && 'codecs' in track && !!track.codecs?.length;\n}\n\n/**\n * Set `mimeType` on every track of one `type` (immutably). Used to propagate a\n * detected container across a type's renditions: an ABR ladder is the same\n * content at different bitrates, so one rendition's container holds for all of\n * them — capability probing + SourceBuffer setup then get the right MIME for the\n * whole type from a single resolved media playlist, without fetching the rest.\n *\n * Scoped to one type on purpose: propagating *across* audio/video would be wrong\n * for mixed-container sources (e.g. muxed-TS video + raw-`.aac` audio) and races\n * concurrent per-type resolution. Same-type writes are disjoint and safe.\n * Idempotent — tracks already at `mimeType` are left as-is.\n */\nexport function applyContainerMimeType(presentation: Presentation, type: TrackType, mimeType: string): Presentation {\n return {\n ...presentation,\n selectionSets: presentation.selectionSets.map((selectionSet) =>\n selectionSet.type === type\n ? {\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) =>\n track.mimeType === mimeType ? track : { ...track, mimeType }\n ),\n })),\n }\n : selectionSet\n ),\n } as Presentation;\n}\n\n/**\n * Updates a track within a presentation (immutably). Generic — works for\n * video, audio, or text tracks.\n */\nexport function updateTrackInPresentation<T extends ResolvedTrack>(\n presentation: Presentation,\n resolvedTrack: T\n): Presentation {\n const trackId = resolvedTrack.id;\n return {\n ...presentation,\n selectionSets: presentation.selectionSets.map((selectionSet) => ({\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) => (track.id === trackId ? resolvedTrack : track)),\n })),\n })),\n } as Presentation;\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,SAAgB,gBACd,cACA,MACqD;AACrD,QAAO,aAAa,eAAe,MAAM,EAAE,MAAM,QAAQ,MAAM,KAAK,EAAE,cAAc,IAAI,UAAU,EAAE;;;;;;;;;;AAWtG,SAAgB,UACd,cACA,MACA,SACoD;AACpD,QAAO,gBAAgB,cAAc,KAAK,CAAC,MAAM,EAAE,SAAS,OAAO,QAAQ;;;;;;;;;;;AAY7E,SAAgB,cACd,cACA,SACoD;AACpD,MAAK,MAAM,gBAAgB,aAAa,iBAAiB,EAAE,EAAE;EAC3D,MAAM,QAAQ,aAAa,cAAc,IAAI,OAAO,MAAM,EAAE,SAAS,OAAO,QAAQ;AACpF,MAAI,MAAO,QAAO;;;;;;;;;;;;AActB,SAAgB,sBACd,cACA,SACuB;AACvB,KAAI,CAAC,gBAAgB,CAAC,QAAS,QAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,QAAQ,QAAQ;AAGtD,KAAI,CAAC,SAAS,MAAM,SAAS,UAAU,CAAC,gBAAgB,MAAM,CAAE,QAAO,KAAA;AACvE,QAAO;;AAGT,SAAgB,uBACd,cACA,SACwB;AACxB,KAAI,CAAC,gBAAgB,CAAC,QAAS,QAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,SAAS,QAAQ;AACvD,KAAI,CAAC,SAAS,MAAM,SAAS,WAAW,CAAC,gBAAgB,MAAM,CAAE,QAAO,KAAA;AACxE,QAAO;;AAGT,SAAgB,uBACd,cACA,SACwB;AACxB,KAAI,CAAC,gBAAgB,CAAC,QAAS,QAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,SAAS,QAAQ;AACvD,KAAI,CAAC,SAAS,MAAM,SAAS,WAAW,CAAC,gBAAgB,MAAM,CAAE,QAAO,KAAA;AACxE,QAAO;;;;;;;;;;;AAYT,SAAgB,UAAU,OAAoE;AAC5F,QAAO,CAAC,CAAC,SAAS,YAAY,SAAS,CAAC,CAAC,MAAM,QAAQ;;;;;;;;;;;;;;AAezD,SAAgB,uBAAuB,cAA4B,MAAiB,UAAgC;AAClH,QAAO;EACL,GAAG;EACH,eAAe,aAAa,cAAc,KAAK,iBAC7C,aAAa,SAAS,OAClB;GACE,GAAG;GACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;IAC/D,GAAG;IACH,QAAQ,aAAa,OAAO,KAAK,UAC/B,MAAM,aAAa,WAAW,QAAQ;KAAE,GAAG;KAAO;KAAU,CAC7D;IACF,EAAE;GACJ,GACD,aACL;EACF;;;;;;AAOH,SAAgB,0BACd,cACA,eACc;CACd,MAAM,UAAU,cAAc;AAC9B,QAAO;EACL,GAAG;EACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;GAC/D,GAAG;GACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;IAC/D,GAAG;IACH,QAAQ,aAAa,OAAO,KAAK,UAAW,MAAM,OAAO,UAAU,gBAAgB,MAAO;IAC3F,EAAE;GACJ,EAAE;EACJ"}
|
|
@@ -63,6 +63,12 @@ async function fetchResolvable(addressable, options) {
|
|
|
63
63
|
function getResponseText(response) {
|
|
64
64
|
return response.text();
|
|
65
65
|
}
|
|
66
|
+
/** Default {@link FetchText}: fetch the resource, reject on non-OK, return text. */
|
|
67
|
+
const fetchResolvableText = async (addressable, options) => {
|
|
68
|
+
const response = await fetchResolvable(addressable, options);
|
|
69
|
+
if (!response.ok) throw new Error(`fetchResolvableText: ${response.status} ${response.statusText} for ${addressable.url}`);
|
|
70
|
+
return getResponseText(response);
|
|
71
|
+
};
|
|
66
72
|
async function fetchStream(addressable, options) {
|
|
67
73
|
const { minChunkSize, ...fetchOptions } = options ?? {};
|
|
68
74
|
const response = await fetchResolvable(addressable, fetchOptions);
|
|
@@ -104,6 +110,6 @@ function createTrackedFetch(initial, onSample) {
|
|
|
104
110
|
};
|
|
105
111
|
}
|
|
106
112
|
//#endregion
|
|
107
|
-
export { createTrackedFetch, fetchResolvable, fetchStream, getResponseText };
|
|
113
|
+
export { createTrackedFetch, fetchResolvable, fetchResolvableText, fetchStream, getResponseText };
|
|
108
114
|
|
|
109
115
|
//# sourceMappingURL=fetch.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.js","names":[],"sources":["../../../src/network/fetch.ts"],"sourcesContent":["/**\n * HTTP Fetch Wrapper\n *\n * Composable building blocks:\n * - fetchResolvable() — fetch a Resource (handles byte ranges); returns Response\n * - getResponseText() — extract text from Response\n * - fetchResolvableStream() — single-stage async generator over body chunks\n * - fetchStream() — two-stage: await connection establishment, then lazily\n * iterate body chunks. Use when timing the connection start independently\n * of body consumption matters (e.g., observable fetch timing for ABR).\n * - createTrackedFetch() — factory for a fetchStream-shape function that\n * samples bandwidth (via EWMA) per chunk and notifies via callback.\n */\n\nimport { type BandwidthState, sampleBandwidth } from './bandwidth-estimator';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from './chunked-stream-iterable';\n\n/**\n * Minimal Response-like interface for text extraction.\n * Allows testing without full Response object.\n */\nexport interface ResponseLike {\n text(): Promise<string>;\n}\n\n/**\n * An HTTP-addressable resource — URL plus optional byte range.\n * Media's `AddressableObject` (and anything else with the same shape)\n * is structurally compatible; kept local so this module stays\n * domain-agnostic.\n */\nexport interface Resource {\n url: string;\n byteRange?: {\n start: number;\n end: number;\n };\n}\n\n/**\n * Fetch resolvable from a Resource.\n *\n * Handles byte range requests if byteRange is present.\n * Returns native fetch Response for composability (can extract text, stream, etc.).\n *\n * @param addressable - Resource to fetch (url + optional byteRange)\n * @returns Promise resolving to Response\n *\n * @example\n * const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });\n * const text = await getResponseText(response);\n *\n * @example\n * // With byte range\n * const response = await fetchResolvable({\n * url: 'https://example.com/file.mp4',\n * byteRange: { start: 1000, end: 1999 }\n * });\n */\nexport async function fetchResolvable(addressable: Resource, options?: RequestInit): Promise<Response> {\n const headers = new Headers(options?.headers);\n\n // Add Range header for byte range requests\n if (addressable.byteRange) {\n const { start, end } = addressable.byteRange;\n headers.set('Range', `bytes=${start}-${end}`);\n }\n\n const request = new Request(addressable.url, {\n method: 'GET',\n headers,\n ...options,\n });\n\n return fetch(request);\n}\n\n/**\n * Fetch resolvable as bytes.\n *\n * Convenience wrapper around fetchResolvable that resolves the body as an\n * ArrayBuffer. Use this when you need the raw bytes (e.g. segment appends).\n * For text or streaming consumption, use fetchResolvable directly.\n */\nexport async function fetchResolvableBytes(addressable: Resource, options?: RequestInit): Promise<ArrayBuffer> {\n const response = await fetchResolvable(addressable, options);\n return response.arrayBuffer();\n}\n\n/**\n * Fetch resolvable as a stream of Uint8Array chunks.\n *\n * Convenience wrapper around fetchResolvable that yields the body as chunks\n * via ChunkedStreamIterable. Headers are awaited before the first chunk is\n * yielded (TTFB is accounted for before iteration begins).\n *\n * Throws if the response body is null (e.g. non-body HTTP status).\n * Errors from the underlying stream propagate naturally as thrown errors.\n */\nexport async function* fetchResolvableStream(\n addressable: Resource,\n options?: RequestInit & ChunkedStreamIterableOptions\n): AsyncGenerator<Uint8Array> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n yield* new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Extract text from Response.\n *\n * Accepts minimal Response-like object (just needs text() method).\n * Returns promise from response.text().\n *\n * @param response - Response-like object with text() method\n * @returns Promise resolving to text content\n *\n * @example\n * const response = await fetchResolvable(addressable);\n * const text = await getResponseText(response);\n */\nexport function getResponseText(response: ResponseLike): Promise<string> {\n return response.text();\n}\n\n/**\n * Two-stage fetch helper: eagerly starts the HTTP request (TTFB is awaited),\n * then returns a lazy iterable over the response body. Separating connection\n * start from body iteration makes fetch timing predictable and observable\n * regardless of when downstream consumers begin pulling chunks.\n *\n * Sibling to {@link fetchResolvableStream}, which is single-stage (calls\n * `fetch` only when iteration starts). Pick `fetchStream` when \"when did the\n * fetch start\" needs to be observable separately from \"when did the body\n * begin arriving.\"\n */\nexport type FetchOptions = RequestInit & ChunkedStreamIterableOptions;\n\nexport type FetchBytes = (addressable: Resource, options?: FetchOptions) => Promise<AsyncIterable<Uint8Array>>;\n\nexport async function fetchStream(addressable: Resource, options?: FetchOptions): Promise<AsyncIterable<Uint8Array>> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n return new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Returns a {@link FetchBytes} function that samples bandwidth via EWMA\n * per body chunk. The factory captures the running bandwidth state\n * internally; per chunk it computes the next state and notifies the\n * supplied `onSample` callback.\n *\n * The factory's internal accumulator is seeded from `initial` and updated\n * on every chunk; callers don't need to thread it back in. `onSample`\n * receives the *new* state after each chunk — typical use is to bridge\n * samples back into engine state for ABR consumers.\n *\n * @param initial - Starting `BandwidthState` (commonly zeros or the\n * engine's current accumulator).\n * @param onSample - Called with the new `BandwidthState` after each chunk.\n */\nexport function createTrackedFetch(initial: BandwidthState, onSample: (next: BandwidthState) => void): FetchBytes {\n let state = initial;\n return async (addressable, options) => {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n const body = response.body;\n return {\n [Symbol.asyncIterator]: async function* () {\n let chunkStart = performance.now();\n for await (const chunk of new ChunkedStreamIterable(\n body,\n ...(minChunkSize !== undefined ? [{ minChunkSize }] : [])\n )) {\n const elapsed = performance.now() - chunkStart;\n state = sampleBandwidth(state, elapsed, chunk.byteLength);\n onSample(state);\n yield chunk;\n chunkStart = performance.now();\n }\n },\n };\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,eAAsB,gBAAgB,aAAuB,SAA0C;CACrG,MAAM,UAAU,IAAI,QAAQ,SAAS,QAAQ;AAG7C,KAAI,YAAY,WAAW;EACzB,MAAM,EAAE,OAAO,QAAQ,YAAY;AACnC,UAAQ,IAAI,SAAS,SAAS,MAAM,GAAG,MAAM;;CAG/C,MAAM,UAAU,IAAI,QAAQ,YAAY,KAAK;EAC3C,QAAQ;EACR;EACA,GAAG;EACJ,CAAC;AAEF,QAAO,MAAM,QAAQ;;;;;;;;;;;;;;;AAgDvB,SAAgB,gBAAgB,UAAyC;AACvE,QAAO,SAAS,MAAM;;
|
|
1
|
+
{"version":3,"file":"fetch.js","names":[],"sources":["../../../src/network/fetch.ts"],"sourcesContent":["/**\n * HTTP Fetch Wrapper\n *\n * Composable building blocks:\n * - fetchResolvable() — fetch a Resource (handles byte ranges); returns Response\n * - getResponseText() — extract text from Response\n * - fetchResolvableStream() — single-stage async generator over body chunks\n * - fetchStream() — two-stage: await connection establishment, then lazily\n * iterate body chunks. Use when timing the connection start independently\n * of body consumption matters (e.g., observable fetch timing for ABR).\n * - createTrackedFetch() — factory for a fetchStream-shape function that\n * samples bandwidth (via EWMA) per chunk and notifies via callback.\n */\n\nimport { type BandwidthState, sampleBandwidth } from './bandwidth-estimator';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from './chunked-stream-iterable';\n\n/**\n * Minimal Response-like interface for text extraction.\n * Allows testing without full Response object.\n */\nexport interface ResponseLike {\n text(): Promise<string>;\n}\n\n/**\n * An HTTP-addressable resource — URL plus optional byte range.\n * Media's `AddressableObject` (and anything else with the same shape)\n * is structurally compatible; kept local so this module stays\n * domain-agnostic.\n */\nexport interface Resource {\n url: string;\n byteRange?: {\n start: number;\n end: number;\n };\n}\n\n/**\n * Fetch resolvable from a Resource.\n *\n * Handles byte range requests if byteRange is present.\n * Returns native fetch Response for composability (can extract text, stream, etc.).\n *\n * @param addressable - Resource to fetch (url + optional byteRange)\n * @returns Promise resolving to Response\n *\n * @example\n * const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });\n * const text = await getResponseText(response);\n *\n * @example\n * // With byte range\n * const response = await fetchResolvable({\n * url: 'https://example.com/file.mp4',\n * byteRange: { start: 1000, end: 1999 }\n * });\n */\nexport async function fetchResolvable(addressable: Resource, options?: RequestInit): Promise<Response> {\n const headers = new Headers(options?.headers);\n\n // Add Range header for byte range requests\n if (addressable.byteRange) {\n const { start, end } = addressable.byteRange;\n headers.set('Range', `bytes=${start}-${end}`);\n }\n\n const request = new Request(addressable.url, {\n method: 'GET',\n headers,\n ...options,\n });\n\n return fetch(request);\n}\n\n/**\n * Fetch resolvable as bytes.\n *\n * Convenience wrapper around fetchResolvable that resolves the body as an\n * ArrayBuffer. Use this when you need the raw bytes (e.g. segment appends).\n * For text or streaming consumption, use fetchResolvable directly.\n */\nexport async function fetchResolvableBytes(addressable: Resource, options?: RequestInit): Promise<ArrayBuffer> {\n const response = await fetchResolvable(addressable, options);\n return response.arrayBuffer();\n}\n\n/**\n * Fetch resolvable as a stream of Uint8Array chunks.\n *\n * Convenience wrapper around fetchResolvable that yields the body as chunks\n * via ChunkedStreamIterable. Headers are awaited before the first chunk is\n * yielded (TTFB is accounted for before iteration begins).\n *\n * Throws if the response body is null (e.g. non-body HTTP status).\n * Errors from the underlying stream propagate naturally as thrown errors.\n */\nexport async function* fetchResolvableStream(\n addressable: Resource,\n options?: RequestInit & ChunkedStreamIterableOptions\n): AsyncGenerator<Uint8Array> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n yield* new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Extract text from Response.\n *\n * Accepts minimal Response-like object (just needs text() method).\n * Returns promise from response.text().\n *\n * @param response - Response-like object with text() method\n * @returns Promise resolving to text content\n *\n * @example\n * const response = await fetchResolvable(addressable);\n * const text = await getResponseText(response);\n */\nexport function getResponseText(response: ResponseLike): Promise<string> {\n return response.text();\n}\n\n/**\n * Fetch a resource and resolve its text body — the text analog of\n * {@link FetchBytes}. A non-OK status rejects, so HTTP failures surface as\n * rejections that callers (and decorators like the failover tracker) handle\n * uniformly with network errors.\n */\nexport type FetchText = (addressable: Resource, options?: RequestInit) => Promise<string>;\n\n/** Default {@link FetchText}: fetch the resource, reject on non-OK, return text. */\nexport const fetchResolvableText: FetchText = async (addressable, options) => {\n const response = await fetchResolvable(addressable, options);\n if (!response.ok) {\n throw new Error(`fetchResolvableText: ${response.status} ${response.statusText} for ${addressable.url}`);\n }\n return getResponseText(response);\n};\n\n/**\n * Two-stage fetch helper: eagerly starts the HTTP request (TTFB is awaited),\n * then returns a lazy iterable over the response body. Separating connection\n * start from body iteration makes fetch timing predictable and observable\n * regardless of when downstream consumers begin pulling chunks.\n *\n * Sibling to {@link fetchResolvableStream}, which is single-stage (calls\n * `fetch` only when iteration starts). Pick `fetchStream` when \"when did the\n * fetch start\" needs to be observable separately from \"when did the body\n * begin arriving.\"\n */\nexport type FetchOptions = RequestInit & ChunkedStreamIterableOptions;\n\nexport type FetchBytes = (addressable: Resource, options?: FetchOptions) => Promise<AsyncIterable<Uint8Array>>;\n\nexport async function fetchStream(addressable: Resource, options?: FetchOptions): Promise<AsyncIterable<Uint8Array>> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n return new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Returns a {@link FetchBytes} function that samples bandwidth via EWMA\n * per body chunk. The factory captures the running bandwidth state\n * internally; per chunk it computes the next state and notifies the\n * supplied `onSample` callback.\n *\n * The factory's internal accumulator is seeded from `initial` and updated\n * on every chunk; callers don't need to thread it back in. `onSample`\n * receives the *new* state after each chunk — typical use is to bridge\n * samples back into engine state for ABR consumers.\n *\n * @param initial - Starting `BandwidthState` (commonly zeros or the\n * engine's current accumulator).\n * @param onSample - Called with the new `BandwidthState` after each chunk.\n */\nexport function createTrackedFetch(initial: BandwidthState, onSample: (next: BandwidthState) => void): FetchBytes {\n let state = initial;\n return async (addressable, options) => {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n const body = response.body;\n return {\n [Symbol.asyncIterator]: async function* () {\n let chunkStart = performance.now();\n for await (const chunk of new ChunkedStreamIterable(\n body,\n ...(minChunkSize !== undefined ? [{ minChunkSize }] : [])\n )) {\n const elapsed = performance.now() - chunkStart;\n state = sampleBandwidth(state, elapsed, chunk.byteLength);\n onSample(state);\n yield chunk;\n chunkStart = performance.now();\n }\n },\n };\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,eAAsB,gBAAgB,aAAuB,SAA0C;CACrG,MAAM,UAAU,IAAI,QAAQ,SAAS,QAAQ;AAG7C,KAAI,YAAY,WAAW;EACzB,MAAM,EAAE,OAAO,QAAQ,YAAY;AACnC,UAAQ,IAAI,SAAS,SAAS,MAAM,GAAG,MAAM;;CAG/C,MAAM,UAAU,IAAI,QAAQ,YAAY,KAAK;EAC3C,QAAQ;EACR;EACA,GAAG;EACJ,CAAC;AAEF,QAAO,MAAM,QAAQ;;;;;;;;;;;;;;;AAgDvB,SAAgB,gBAAgB,UAAyC;AACvE,QAAO,SAAS,MAAM;;;AAYxB,MAAa,sBAAiC,OAAO,aAAa,YAAY;CAC5E,MAAM,WAAW,MAAM,gBAAgB,aAAa,QAAQ;AAC5D,KAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MAAM,wBAAwB,SAAS,OAAO,GAAG,SAAS,WAAW,OAAO,YAAY,MAAM;AAE1G,QAAO,gBAAgB,SAAS;;AAkBlC,eAAsB,YAAY,aAAuB,SAA4D;CACnH,MAAM,EAAE,cAAc,GAAG,iBAAiB,WAAW,EAAE;CACvD,MAAM,WAAW,MAAM,gBAAgB,aAAa,aAAa;AACjE,KAAI,CAAC,SAAS,KAAM,OAAM,IAAI,MAAM,uBAAuB;AAC3D,QAAO,IAAI,sBAAsB,SAAS,MAAM,GAAI,iBAAiB,KAAA,IAAY,CAAC,EAAE,cAAc,CAAC,GAAG,EAAE,CAAE;;;;;;;;;;;;;;;;;AAkB5G,SAAgB,mBAAmB,SAAyB,UAAsD;CAChH,IAAI,QAAQ;AACZ,QAAO,OAAO,aAAa,YAAY;EACrC,MAAM,EAAE,cAAc,GAAG,iBAAiB,WAAW,EAAE;EACvD,MAAM,WAAW,MAAM,gBAAgB,aAAa,aAAa;AACjE,MAAI,CAAC,SAAS,KAAM,OAAM,IAAI,MAAM,uBAAuB;EAC3D,MAAM,OAAO,SAAS;AACtB,SAAO,EACL,CAAC,OAAO,gBAAgB,mBAAmB;GACzC,IAAI,aAAa,YAAY,KAAK;AAClC,cAAW,MAAM,SAAS,IAAI,sBAC5B,MACA,GAAI,iBAAiB,KAAA,IAAY,CAAC,EAAE,cAAc,CAAC,GAAG,EAAE,CACzD,EAAE;IACD,MAAM,UAAU,YAAY,KAAK,GAAG;AACpC,YAAQ,gBAAgB,OAAO,SAAS,MAAM,WAAW;AACzD,aAAS,MAAM;AACf,UAAM;AACN,iBAAa,YAAY,KAAK;;KAGnC"}
|
|
@@ -3,7 +3,7 @@ import { effect } from "../../../core/signals/effect.js";
|
|
|
3
3
|
import { SerialRunner, Task } from "../../../core/tasks/task.js";
|
|
4
4
|
import { createMachineActor } from "../../../core/actors/create-machine-actor.js";
|
|
5
5
|
import { DEFAULT_FORWARD_BUFFER_CONFIG, calculateForwardFlushPoint, getSegmentsToLoad } from "../../../media/buffer/forward-buffer.js";
|
|
6
|
-
import
|
|
6
|
+
import "../../../media/types/index.js";
|
|
7
7
|
import { DEFAULT_BACK_BUFFER_CONFIG, calculateBackBufferFlushPoint } from "../../../media/buffer/back-buffer.js";
|
|
8
8
|
//#region src/playback/actors/dom/segment-loader.ts
|
|
9
9
|
/**
|
|
@@ -144,9 +144,19 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes, config = {}) {
|
|
|
144
144
|
* @todo Rename alongside LoadTask (e.g. planOps).
|
|
145
145
|
*
|
|
146
146
|
* Case 1 — Removes: forward and back buffer flush points, segment-aligned.
|
|
147
|
-
*
|
|
148
|
-
* ranges, and the actor's
|
|
149
|
-
* accurate as new
|
|
147
|
+
* ABR-style track switches (same content, different bitrate) do not flush:
|
|
148
|
+
* appending new content overwrites existing buffer ranges, and the actor's
|
|
149
|
+
* time-aligned deduplication keeps the segment model accurate as new
|
|
150
|
+
* segments arrive.
|
|
151
|
+
*
|
|
152
|
+
* Cross-rendition track switches (audio language change, text language
|
|
153
|
+
* change) do flush: the buffered content is semantically incompatible with
|
|
154
|
+
* the newly-selected track, so overwrite-on-append would leave stale
|
|
155
|
+
* content playing until each replacement segment lands. Today's predicate:
|
|
156
|
+
* `actorCtx.initTrackLanguage !== track.language` — fires for language
|
|
157
|
+
* changes, no-ops for video / same-language audio bitrate switches.
|
|
158
|
+
* Future stage: pluggable predicate / strategy at actor construction time
|
|
159
|
+
* for codec-change (5.1 surround) and other cross-rendition shapes.
|
|
150
160
|
*
|
|
151
161
|
* Case 2 — Init: schedule if not yet committed for this track.
|
|
152
162
|
*
|
|
@@ -158,29 +168,50 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes, config = {}) {
|
|
|
158
168
|
const bufferedSegments = getBufferedSegments(track.segments);
|
|
159
169
|
const currentTime = range?.start ?? 0;
|
|
160
170
|
const tasks = [];
|
|
171
|
+
const isCrossRenditionSwitch = actorCtx.initTrackId !== void 0 && actorCtx.initTrackId !== track.id && actorCtx.initTrackLanguage !== track.language;
|
|
172
|
+
const removes = [];
|
|
173
|
+
const staleRanges = [];
|
|
161
174
|
if (range) {
|
|
175
|
+
if (isCrossRenditionSwitch) {
|
|
176
|
+
const staleStart = actorCtx.segments.find((s) => s.startTime <= currentTime && s.startTime + s.duration > currentTime)?.startTime ?? actorCtx.segments.find((s) => s.startTime > currentTime)?.startTime;
|
|
177
|
+
if (staleStart !== void 0) staleRanges.push({
|
|
178
|
+
start: staleStart,
|
|
179
|
+
end: Infinity
|
|
180
|
+
});
|
|
181
|
+
}
|
|
162
182
|
const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime, forwardBufferConfig);
|
|
163
|
-
if (forwardFlushStart < Infinity)
|
|
164
|
-
type: "remove",
|
|
183
|
+
if (forwardFlushStart < Infinity) removes.push({
|
|
165
184
|
start: forwardFlushStart,
|
|
166
185
|
end: Infinity
|
|
167
186
|
});
|
|
168
187
|
const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime, backBufferConfig);
|
|
169
|
-
if (backFlushEnd > 0)
|
|
170
|
-
type: "remove",
|
|
188
|
+
if (backFlushEnd > 0) removes.push({
|
|
171
189
|
start: 0,
|
|
172
190
|
end: backFlushEnd
|
|
173
191
|
});
|
|
192
|
+
for (const r of removes) tasks.push({
|
|
193
|
+
type: "remove",
|
|
194
|
+
start: r.start,
|
|
195
|
+
end: r.end
|
|
196
|
+
});
|
|
174
197
|
}
|
|
198
|
+
const overlapsStale = (seg) => {
|
|
199
|
+
const segEnd = seg.startTime + seg.duration;
|
|
200
|
+
return removes.some((r) => seg.startTime < r.end && segEnd > r.start) || staleRanges.some((r) => seg.startTime < r.end && segEnd > r.start);
|
|
201
|
+
};
|
|
202
|
+
const effectiveBuffered = removes.length + staleRanges.length > 0 ? bufferedSegments.filter((s) => !overlapsStale(s)) : bufferedSegments;
|
|
175
203
|
if (actorCtx.initTrackId !== track.id) tasks.push({
|
|
176
204
|
type: "append-init",
|
|
177
|
-
meta: {
|
|
205
|
+
meta: {
|
|
206
|
+
trackId: track.id,
|
|
207
|
+
language: track.language
|
|
208
|
+
},
|
|
178
209
|
url: track.initialization.url,
|
|
179
210
|
...track.initialization.byteRange !== void 0 && { byteRange: track.initialization.byteRange }
|
|
180
211
|
});
|
|
181
212
|
if (range) {
|
|
182
|
-
const segmentsToLoad = getSegmentsToLoad(track.segments,
|
|
183
|
-
const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) <
|
|
213
|
+
const segmentsToLoad = getSegmentsToLoad(track.segments, effectiveBuffered, currentTime, forwardBufferConfig).filter((seg) => {
|
|
214
|
+
const existing = actorCtx.segments.find((s) => !overlapsStale(s) && Math.abs(s.startTime - seg.startTime) < 1e-4);
|
|
184
215
|
if (existing?.partial) return true;
|
|
185
216
|
if (!existing?.trackBandwidth || !track.bandwidth) return true;
|
|
186
217
|
return track.bandwidth > existing.trackBandwidth;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segment-loader.js","names":[],"sources":["../../../../../src/playback/actors/dom/segment-loader.ts"],"sourcesContent":["import { createMachineActor, type HandlerContext, type MessageActor } from '../../../core/actors/create-machine-actor';\nimport { effect } from '../../../core/signals/effect';\nimport { peek } from '../../../core/signals/primitives';\nimport { SerialRunner, Task } from '../../../core/tasks/task';\nimport {\n type BackBufferConfig,\n calculateBackBufferFlushPoint,\n DEFAULT_BACK_BUFFER_CONFIG,\n} from '../../../media/buffer/back-buffer';\nimport {\n calculateForwardFlushPoint,\n DEFAULT_FORWARD_BUFFER_CONFIG,\n type ForwardBufferConfig,\n getSegmentsToLoad,\n} from '../../../media/buffer/forward-buffer';\nimport {\n type AddressableObject,\n type AudioTrack,\n SEGMENT_TIME_EPSILON,\n type Segment,\n type VideoTrack,\n} from '../../../media/types';\nimport type { AppendInitMessage, AppendSegmentMessage, RemoveMessage, SourceBufferActor } from './source-buffer';\n\n// ============================================================================\n// BUFFER STATE TYPES\n// ============================================================================\n\n/**\n * Buffer state for a single SourceBuffer.\n * Tracks which init segment and media segments are loaded.\n */\nexport interface SourceBufferState {\n /** Track ID of the loaded init segment */\n initTrackId?: string;\n /** Loaded media segments (unordered - selectors derive ordering) */\n segments: Array<{ id: string; trackId: string }>;\n}\n\n/**\n * Buffer state for all SourceBuffers.\n */\nexport interface BufferState {\n video?: SourceBufferState;\n audio?: SourceBufferState;\n}\n\n// ============================================================================\n// MESSAGE PROTOCOL\n// ============================================================================\n\n/** Track types that have SourceBuffers (video and audio only). */\nexport type SegmentLoaderTrack = VideoTrack | AudioTrack;\n\n/**\n * Message sent to a SegmentLoaderActor.\n *\n * `range` is optional to distinguish loading modes:\n * - No range: load init segment only (metadata preload mode)\n * - With range: load init + all segments overlapping [start, end]\n *\n * `start` and `end` are raw time values — no segment snapping.\n * The actor maps them onto segment boundaries internally.\n */\nexport type SegmentLoaderMessage = {\n type: 'load';\n track: SegmentLoaderTrack;\n range?: { start: number; end: number };\n};\n\n// ============================================================================\n// LOAD TASK\n// ============================================================================\n\n/**\n * A LoadTask is the intent to perform one unit of SegmentLoader work.\n * Unlike SourceBufferMessage, fetch-based tasks carry a URL rather than\n * pre-fetched data — the runner fetches and appends them in sequence.\n *\n * Derived from SourceBufferMessage types by removing `data` and adding\n * a fetch URL via AddressableObject.\n *\n * @todo Rename — \"LoadTask\" risks confusion with the `Task` class used for\n * SourceBufferActor scheduling. These are closer to operation descriptors or\n * messages than tasks in that sense.\n */\nexport type LoadTask =\n | (Omit<AppendInitMessage, 'data'> & AddressableObject)\n | (Omit<AppendSegmentMessage, 'data'> & AddressableObject)\n | RemoveMessage;\n\n// ============================================================================\n// ACTOR INTERFACE\n// ============================================================================\n\n/** Finite states of the actor. */\nexport type SegmentLoaderActorState = 'idle' | 'loading' | 'destroyed';\n\n/** Non-finite (extended) data managed by the actor. */\nexport interface SegmentLoaderActorContext {\n /** Track ID of the init segment currently being fetched/appended, or null. */\n inFlightInitTrackId: string | null;\n /** Segment ID currently being fetched/appended, or null. */\n inFlightSegmentId: string | null;\n}\n\nexport type SegmentLoaderActor = MessageActor<SegmentLoaderActorState, SegmentLoaderActorContext, SegmentLoaderMessage>;\n\n/**\n * Configuration for `createSegmentLoaderActor`. Each sub-config is\n * spread over the corresponding `DEFAULT_*_CONFIG` so callers can\n * override individual fields.\n */\nexport interface SegmentLoaderActorConfig {\n forwardBuffer?: Partial<ForwardBufferConfig>;\n backBuffer?: Partial<BackBufferConfig>;\n}\n\n// ============================================================================\n// HELPERS\n// ============================================================================\n\ntype FetchBytes = (\n addressable: AddressableObject,\n options?: RequestInit & { minChunkSize?: number }\n) => Promise<AsyncIterable<Uint8Array>>;\n\n/**\n * Resolves when the SourceBufferActor snapshot reaches 'idle'.\n * Rejects if the signal is aborted or the actor is destroyed.\n *\n * Used to sequence SourceBufferActor operations without awaiting send()\n * directly — send() is fire-and-forget; callers observe completion via\n * state transition.\n */\nfunction waitForIdle(snapshot: SourceBufferActor['snapshot'], signal: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (snapshot.get().value === 'idle') {\n resolve();\n return;\n }\n if (snapshot.get().value === 'destroyed') {\n reject(new DOMException('Aborted', 'AbortError'));\n return;\n }\n if (signal.aborted) {\n reject(signal.reason);\n return;\n }\n\n let stop: (() => void) | undefined;\n\n const cleanup = (fn: () => void) => {\n stop?.();\n signal.removeEventListener('abort', onAbort);\n fn();\n };\n\n const onAbort = () => cleanup(() => reject(signal.reason));\n\n stop = effect(() => {\n const value = snapshot.get().value;\n if (value === 'idle') cleanup(resolve);\n else if (value === 'destroyed') cleanup(() => reject(new DOMException('Aborted', 'AbortError')));\n });\n\n signal.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n// ============================================================================\n// LOAD TASK FACTORY\n// ============================================================================\n\ninterface LoadTaskOptions {\n getContext: () => SegmentLoaderActorContext;\n setContext: (ctx: SegmentLoaderActorContext) => void;\n fetchBytes: FetchBytes;\n sourceBufferActor: SourceBufferActor;\n}\n\n/**\n * Wraps a LoadTask descriptor into a Task that fetches (if needed) and\n * forwards to SourceBufferActor. Updates in-flight context around async\n * operations so the loading handler can make accurate continue/preempt\n * decisions at any point.\n */\nfunction makeLoadTask(\n op: LoadTask,\n { getContext, setContext, fetchBytes, sourceBufferActor }: LoadTaskOptions\n): Task<void> {\n return new Task(async (taskSignal) => {\n if (taskSignal.aborted) return;\n\n if (op.type === 'remove') {\n sourceBufferActor.send(op);\n await waitForIdle(sourceBufferActor.snapshot, taskSignal);\n return;\n }\n\n if (op.type === 'append-init') {\n setContext({ ...getContext(), inFlightInitTrackId: op.meta.trackId });\n try {\n // Init segments are small and need the full body before appending.\n // minChunkSize: Infinity accumulates all chunks into one before yielding.\n const data = await fetchBytes(op, { signal: taskSignal, minChunkSize: Infinity });\n if (!taskSignal.aborted) {\n sourceBufferActor.send({ type: 'append-init', data, meta: op.meta });\n await waitForIdle(sourceBufferActor.snapshot, taskSignal);\n }\n } finally {\n setContext({ ...getContext(), inFlightInitTrackId: null });\n }\n return;\n }\n\n // append-segment: await headers eagerly (starts the HTTP connection and\n // records the fetch in observers like tests), then pass the body stream\n // directly to the actor so chunks are appended as they arrive.\n setContext({ ...getContext(), inFlightSegmentId: op.meta.id });\n try {\n const stream = await fetchBytes(op, { signal: taskSignal });\n if (!taskSignal.aborted) {\n sourceBufferActor.send({ type: 'append-segment', data: stream, meta: op.meta });\n await waitForIdle(sourceBufferActor.snapshot, taskSignal);\n }\n } finally {\n setContext({ ...getContext(), inFlightSegmentId: null });\n }\n });\n}\n\n// ============================================================================\n// IMPLEMENTATION\n// ============================================================================\n\n/**\n * Creates a SegmentLoaderActor for one track type (video or audio).\n *\n * Receives load assignments via `send()` and owns all execution: planning,\n * removes, fetches, and appends. Coordinates with the SourceBufferActor for\n * all physical SourceBuffer operations.\n *\n * Planning (Cases 1–3) happens in the `load` handler on every incoming\n * message, producing an ordered LoadTask list. The runner drains that list\n * sequentially via SerialRunner. When a new message arrives mid-run, the\n * handler replans and either continues the in-flight operation (abortPending\n * + schedule new remainder) or preempts it (abortAll + cancel SourceBuffer\n * if needed + schedule new plan).\n *\n * @param sourceBufferActor - Shared SourceBufferActor reference (not owned)\n * @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).\n * Accepts an optional `minChunkSize` in options; init segments pass `Infinity`\n * so the entire body accumulates as one chunk before appending.\n */\nexport function createSegmentLoaderActor(\n sourceBufferActor: SourceBufferActor,\n fetchBytes: FetchBytes,\n config: SegmentLoaderActorConfig = {}\n): SegmentLoaderActor {\n type UserState = Exclude<SegmentLoaderActorState, 'destroyed'>;\n type Ctx = HandlerContext<UserState, SegmentLoaderActorContext, () => SerialRunner>;\n\n const forwardBufferConfig: ForwardBufferConfig = { ...DEFAULT_FORWARD_BUFFER_CONFIG, ...config.forwardBuffer };\n const backBufferConfig: BackBufferConfig = { ...DEFAULT_BACK_BUFFER_CONFIG, ...config.backBuffer };\n\n const getBufferedSegments = (allSegments: readonly Segment[]): Segment[] => {\n // Exclude partial segments — they are still being streamed and must not be\n // treated as fully buffered for load planning or buffer window calculations.\n //\n // `peek` defensively: `load` handlers run synchronously inside `send()`,\n // which is called from inside the dispatcher reactor's `effects:` body.\n // A tracked `.snapshot.get()` here would leak the source-buffer-actor's\n // snapshot into the dispatcher's dep set, causing the dispatcher to re-\n // fire on every SourceBufferActor state change. Mirrors the fix applied\n // to the text-track loader in `b3f44efe`.\n const bufferedIds = new Set(\n peek(sourceBufferActor.snapshot)\n .context.segments.filter((s) => !s.partial)\n .map((s) => s.id)\n );\n return allSegments.filter((s) => bufferedIds.has(s.id));\n };\n\n /**\n * Translate a load message into an ordered LoadTask list based on committed\n * actor state. In-flight awareness is handled separately in the load handler.\n *\n * @todo Rename alongside LoadTask (e.g. planOps).\n *\n * Case 1 — Removes: forward and back buffer flush points, segment-aligned.\n * No flush on track switch: appending new content overwrites existing buffer\n * ranges, and the actor's time-aligned deduplication keeps the segment model\n * accurate as new segments arrive.\n *\n * Case 2 — Init: schedule if not yet committed for this track.\n *\n * Case 3 — Segments: all segments in the load window not yet committed.\n */\n const planTasks = (message: SegmentLoaderMessage): LoadTask[] => {\n const { track, range } = message;\n // `peek` for the same reason as `getBufferedSegments` above — avoid\n // leaking the SourceBufferActor snapshot into the calling dispatcher's\n // tracking scope.\n const actorCtx = peek(sourceBufferActor.snapshot).context;\n const bufferedSegments = getBufferedSegments(track.segments);\n const currentTime = range?.start ?? 0;\n const tasks: LoadTask[] = [];\n\n // Case 1: Removes\n if (range) {\n const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime, forwardBufferConfig);\n if (forwardFlushStart < Infinity) {\n tasks.push({ type: 'remove', start: forwardFlushStart, end: Infinity });\n }\n const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime, backBufferConfig);\n if (backFlushEnd > 0) {\n tasks.push({ type: 'remove', start: 0, end: backFlushEnd });\n }\n }\n\n // Case 2: Init\n if (actorCtx.initTrackId !== track.id) {\n tasks.push({\n type: 'append-init',\n meta: { trackId: track.id },\n url: track.initialization.url,\n ...(track.initialization.byteRange !== undefined && { byteRange: track.initialization.byteRange }),\n });\n }\n\n // Case 3: Segments\n if (range) {\n const segmentsToLoad = getSegmentsToLoad(\n track.segments,\n bufferedSegments,\n currentTime,\n forwardBufferConfig\n ).filter((seg) => {\n // Quality-aware filter: skip segments already covered by equal-or-higher-quality\n // content in the actor context. Preserves buffered high-quality content during\n // ABR downgrades; loads during upgrades and for uncovered positions.\n const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) < SEGMENT_TIME_EPSILON);\n // Partial segments are still streaming — treat as not buffered so they\n // are always re-planned (avoids relying on incomplete data).\n if (existing?.partial) return true;\n if (!existing?.trackBandwidth || !track.bandwidth) return true;\n return track.bandwidth > existing.trackBandwidth;\n });\n for (const segment of segmentsToLoad) {\n tasks.push({\n type: 'append-segment',\n meta: {\n id: segment.id,\n startTime: segment.startTime,\n duration: segment.duration,\n trackId: track.id,\n trackBandwidth: track.bandwidth,\n },\n url: segment.url,\n ...(segment.byteRange !== undefined && { byteRange: segment.byteRange }),\n });\n }\n }\n\n return tasks;\n };\n\n const scheduleAll = (tasks: LoadTask[], { getContext, setContext, runner }: Ctx): void => {\n tasks.forEach((op) => {\n runner\n .schedule(makeLoadTask(op, { getContext, setContext, fetchBytes, sourceBufferActor }))\n .then(undefined, (e: unknown) => {\n if (e instanceof Error && e.name === 'AbortError') return;\n // On unexpected fetch/append errors, abort remaining tasks so a failed\n // init doesn't cause segment fetches to proceed with no init segment.\n console.error('Unexpected error in segment loader:', e);\n runner.abortPending();\n });\n });\n };\n\n return createMachineActor<UserState, SegmentLoaderActorContext, SegmentLoaderMessage, () => SerialRunner>({\n runner: () => new SerialRunner(),\n initial: 'idle',\n context: { inFlightInitTrackId: null, inFlightSegmentId: null },\n states: {\n idle: {\n on: {\n load: (msg, ctx) => {\n const allTasks = planTasks(msg);\n if (allTasks.length === 0) return;\n ctx.transition('loading');\n scheduleAll(allTasks, ctx);\n },\n },\n },\n loading: {\n onSettled: 'idle',\n on: {\n load: (msg, ctx) => {\n const { context, runner } = ctx;\n const allTasks = planTasks(msg);\n\n // Determine whether the in-flight operation is still needed.\n const inFlightStillNeeded =\n (context.inFlightSegmentId !== null &&\n allTasks.some((t) => t.type === 'append-segment' && t.meta.id === context.inFlightSegmentId)) ||\n (context.inFlightInitTrackId !== null &&\n allTasks.some((t) => t.type === 'append-init' && t.meta.trackId === context.inFlightInitTrackId));\n\n if (inFlightStillNeeded) {\n // Continue: abort only the pending queue, let the in-flight task finish.\n // Schedule everything except the in-flight item — it covers that slot.\n runner.abortPending();\n scheduleAll(\n allTasks.filter(\n (t) =>\n !(t.type === 'append-segment' && t.meta.id === context.inFlightSegmentId) &&\n !(t.type === 'append-init' && t.meta.trackId === context.inFlightInitTrackId)\n ),\n ctx\n );\n } else {\n // Preempt: abort everything and replan.\n runner.abortAll();\n // Cancel SourceBufferActor tasks when a segment is in-flight (always\n // discard) or when a track switch is happening (new track's init follows).\n // For a same-track seek with an in-flight init, skip cancel — the task's\n // signal is not aborted (abortAll was called on the runner, but the init\n // task already completed or will complete via its own signal path).\n const cancelSourceBuffer =\n context.inFlightSegmentId !== null ||\n (context.inFlightInitTrackId !== null &&\n allTasks.some((t) => t.type === 'append-init' && t.meta.trackId !== context.inFlightInitTrackId));\n if (cancelSourceBuffer) {\n sourceBufferActor.send({ type: 'cancel' });\n }\n scheduleAll(allTasks, ctx);\n }\n },\n },\n },\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"segment-loader.js","names":[],"sources":["../../../../../src/playback/actors/dom/segment-loader.ts"],"sourcesContent":["import { createMachineActor, type HandlerContext, type MessageActor } from '../../../core/actors/create-machine-actor';\nimport { effect } from '../../../core/signals/effect';\nimport { peek } from '../../../core/signals/primitives';\nimport { SerialRunner, Task } from '../../../core/tasks/task';\nimport {\n type BackBufferConfig,\n calculateBackBufferFlushPoint,\n DEFAULT_BACK_BUFFER_CONFIG,\n} from '../../../media/buffer/back-buffer';\nimport {\n calculateForwardFlushPoint,\n DEFAULT_FORWARD_BUFFER_CONFIG,\n type ForwardBufferConfig,\n getSegmentsToLoad,\n} from '../../../media/buffer/forward-buffer';\nimport {\n type AddressableObject,\n type AudioTrack,\n SEGMENT_TIME_EPSILON,\n type Segment,\n type VideoTrack,\n} from '../../../media/types';\nimport type { AppendInitMessage, AppendSegmentMessage, RemoveMessage, SourceBufferActor } from './source-buffer';\n\n// ============================================================================\n// BUFFER STATE TYPES\n// ============================================================================\n\n/**\n * Buffer state for a single SourceBuffer.\n * Tracks which init segment and media segments are loaded.\n */\nexport interface SourceBufferState {\n /** Track ID of the loaded init segment */\n initTrackId?: string;\n /** Loaded media segments (unordered - selectors derive ordering) */\n segments: Array<{ id: string; trackId: string }>;\n}\n\n/**\n * Buffer state for all SourceBuffers.\n */\nexport interface BufferState {\n video?: SourceBufferState;\n audio?: SourceBufferState;\n}\n\n// ============================================================================\n// MESSAGE PROTOCOL\n// ============================================================================\n\n/** Track types that have SourceBuffers (video and audio only). */\nexport type SegmentLoaderTrack = VideoTrack | AudioTrack;\n\n/**\n * Message sent to a SegmentLoaderActor.\n *\n * `range` is optional to distinguish loading modes:\n * - No range: load init segment only (metadata preload mode)\n * - With range: load init + all segments overlapping [start, end]\n *\n * `start` and `end` are raw time values — no segment snapping.\n * The actor maps them onto segment boundaries internally.\n */\nexport type SegmentLoaderMessage = {\n type: 'load';\n track: SegmentLoaderTrack;\n range?: { start: number; end: number };\n};\n\n// ============================================================================\n// LOAD TASK\n// ============================================================================\n\n/**\n * A LoadTask is the intent to perform one unit of SegmentLoader work.\n * Unlike SourceBufferMessage, fetch-based tasks carry a URL rather than\n * pre-fetched data — the runner fetches and appends them in sequence.\n *\n * Derived from SourceBufferMessage types by removing `data` and adding\n * a fetch URL via AddressableObject.\n *\n * @todo Rename — \"LoadTask\" risks confusion with the `Task` class used for\n * SourceBufferActor scheduling. These are closer to operation descriptors or\n * messages than tasks in that sense.\n */\nexport type LoadTask =\n | (Omit<AppendInitMessage, 'data'> & AddressableObject)\n | (Omit<AppendSegmentMessage, 'data'> & AddressableObject)\n | RemoveMessage;\n\n// ============================================================================\n// ACTOR INTERFACE\n// ============================================================================\n\n/** Finite states of the actor. */\nexport type SegmentLoaderActorState = 'idle' | 'loading' | 'destroyed';\n\n/** Non-finite (extended) data managed by the actor. */\nexport interface SegmentLoaderActorContext {\n /** Track ID of the init segment currently being fetched/appended, or null. */\n inFlightInitTrackId: string | null;\n /** Segment ID currently being fetched/appended, or null. */\n inFlightSegmentId: string | null;\n}\n\nexport type SegmentLoaderActor = MessageActor<SegmentLoaderActorState, SegmentLoaderActorContext, SegmentLoaderMessage>;\n\n/**\n * Configuration for `createSegmentLoaderActor`. Each sub-config is\n * spread over the corresponding `DEFAULT_*_CONFIG` so callers can\n * override individual fields.\n */\nexport interface SegmentLoaderActorConfig {\n forwardBuffer?: Partial<ForwardBufferConfig>;\n backBuffer?: Partial<BackBufferConfig>;\n}\n\n// ============================================================================\n// HELPERS\n// ============================================================================\n\ntype FetchBytes = (\n addressable: AddressableObject,\n options?: RequestInit & { minChunkSize?: number }\n) => Promise<AsyncIterable<Uint8Array>>;\n\n/**\n * Resolves when the SourceBufferActor snapshot reaches 'idle'.\n * Rejects if the signal is aborted or the actor is destroyed.\n *\n * Used to sequence SourceBufferActor operations without awaiting send()\n * directly — send() is fire-and-forget; callers observe completion via\n * state transition.\n */\nfunction waitForIdle(snapshot: SourceBufferActor['snapshot'], signal: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (snapshot.get().value === 'idle') {\n resolve();\n return;\n }\n if (snapshot.get().value === 'destroyed') {\n reject(new DOMException('Aborted', 'AbortError'));\n return;\n }\n if (signal.aborted) {\n reject(signal.reason);\n return;\n }\n\n let stop: (() => void) | undefined;\n\n const cleanup = (fn: () => void) => {\n stop?.();\n signal.removeEventListener('abort', onAbort);\n fn();\n };\n\n const onAbort = () => cleanup(() => reject(signal.reason));\n\n stop = effect(() => {\n const value = snapshot.get().value;\n if (value === 'idle') cleanup(resolve);\n else if (value === 'destroyed') cleanup(() => reject(new DOMException('Aborted', 'AbortError')));\n });\n\n signal.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n// ============================================================================\n// LOAD TASK FACTORY\n// ============================================================================\n\ninterface LoadTaskOptions {\n getContext: () => SegmentLoaderActorContext;\n setContext: (ctx: SegmentLoaderActorContext) => void;\n fetchBytes: FetchBytes;\n sourceBufferActor: SourceBufferActor;\n}\n\n/**\n * Wraps a LoadTask descriptor into a Task that fetches (if needed) and\n * forwards to SourceBufferActor. Updates in-flight context around async\n * operations so the loading handler can make accurate continue/preempt\n * decisions at any point.\n */\nfunction makeLoadTask(\n op: LoadTask,\n { getContext, setContext, fetchBytes, sourceBufferActor }: LoadTaskOptions\n): Task<void> {\n return new Task(async (taskSignal) => {\n if (taskSignal.aborted) return;\n\n if (op.type === 'remove') {\n sourceBufferActor.send(op);\n await waitForIdle(sourceBufferActor.snapshot, taskSignal);\n return;\n }\n\n if (op.type === 'append-init') {\n setContext({ ...getContext(), inFlightInitTrackId: op.meta.trackId });\n try {\n // Init segments are small and need the full body before appending.\n // minChunkSize: Infinity accumulates all chunks into one before yielding.\n const data = await fetchBytes(op, { signal: taskSignal, minChunkSize: Infinity });\n if (!taskSignal.aborted) {\n sourceBufferActor.send({ type: 'append-init', data, meta: op.meta });\n await waitForIdle(sourceBufferActor.snapshot, taskSignal);\n }\n } finally {\n setContext({ ...getContext(), inFlightInitTrackId: null });\n }\n return;\n }\n\n // append-segment: await headers eagerly (starts the HTTP connection and\n // records the fetch in observers like tests), then pass the body stream\n // directly to the actor so chunks are appended as they arrive.\n setContext({ ...getContext(), inFlightSegmentId: op.meta.id });\n try {\n const stream = await fetchBytes(op, { signal: taskSignal });\n if (!taskSignal.aborted) {\n sourceBufferActor.send({ type: 'append-segment', data: stream, meta: op.meta });\n await waitForIdle(sourceBufferActor.snapshot, taskSignal);\n }\n } finally {\n setContext({ ...getContext(), inFlightSegmentId: null });\n }\n });\n}\n\n// ============================================================================\n// IMPLEMENTATION\n// ============================================================================\n\n/**\n * Creates a SegmentLoaderActor for one track type (video or audio).\n *\n * Receives load assignments via `send()` and owns all execution: planning,\n * removes, fetches, and appends. Coordinates with the SourceBufferActor for\n * all physical SourceBuffer operations.\n *\n * Planning (Cases 1–3) happens in the `load` handler on every incoming\n * message, producing an ordered LoadTask list. The runner drains that list\n * sequentially via SerialRunner. When a new message arrives mid-run, the\n * handler replans and either continues the in-flight operation (abortPending\n * + schedule new remainder) or preempts it (abortAll + cancel SourceBuffer\n * if needed + schedule new plan).\n *\n * @param sourceBufferActor - Shared SourceBufferActor reference (not owned)\n * @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).\n * Accepts an optional `minChunkSize` in options; init segments pass `Infinity`\n * so the entire body accumulates as one chunk before appending.\n */\nexport function createSegmentLoaderActor(\n sourceBufferActor: SourceBufferActor,\n fetchBytes: FetchBytes,\n config: SegmentLoaderActorConfig = {}\n): SegmentLoaderActor {\n type UserState = Exclude<SegmentLoaderActorState, 'destroyed'>;\n type Ctx = HandlerContext<UserState, SegmentLoaderActorContext, () => SerialRunner>;\n\n const forwardBufferConfig: ForwardBufferConfig = { ...DEFAULT_FORWARD_BUFFER_CONFIG, ...config.forwardBuffer };\n const backBufferConfig: BackBufferConfig = { ...DEFAULT_BACK_BUFFER_CONFIG, ...config.backBuffer };\n\n const getBufferedSegments = (allSegments: readonly Segment[]): Segment[] => {\n // Exclude partial segments — they are still being streamed and must not be\n // treated as fully buffered for load planning or buffer window calculations.\n //\n // `peek` defensively: `load` handlers run synchronously inside `send()`,\n // which is called from inside the dispatcher reactor's `effects:` body.\n // A tracked `.snapshot.get()` here would leak the source-buffer-actor's\n // snapshot into the dispatcher's dep set, causing the dispatcher to re-\n // fire on every SourceBufferActor state change. Mirrors the fix applied\n // to the text-track loader in `b3f44efe`.\n const bufferedIds = new Set(\n peek(sourceBufferActor.snapshot)\n .context.segments.filter((s) => !s.partial)\n .map((s) => s.id)\n );\n return allSegments.filter((s) => bufferedIds.has(s.id));\n };\n\n /**\n * Translate a load message into an ordered LoadTask list based on committed\n * actor state. In-flight awareness is handled separately in the load handler.\n *\n * @todo Rename alongside LoadTask (e.g. planOps).\n *\n * Case 1 — Removes: forward and back buffer flush points, segment-aligned.\n * ABR-style track switches (same content, different bitrate) do not flush:\n * appending new content overwrites existing buffer ranges, and the actor's\n * time-aligned deduplication keeps the segment model accurate as new\n * segments arrive.\n *\n * Cross-rendition track switches (audio language change, text language\n * change) do flush: the buffered content is semantically incompatible with\n * the newly-selected track, so overwrite-on-append would leave stale\n * content playing until each replacement segment lands. Today's predicate:\n * `actorCtx.initTrackLanguage !== track.language` — fires for language\n * changes, no-ops for video / same-language audio bitrate switches.\n * Future stage: pluggable predicate / strategy at actor construction time\n * for codec-change (5.1 surround) and other cross-rendition shapes.\n *\n * Case 2 — Init: schedule if not yet committed for this track.\n *\n * Case 3 — Segments: all segments in the load window not yet committed.\n */\n const planTasks = (message: SegmentLoaderMessage): LoadTask[] => {\n const { track, range } = message;\n // `peek` for the same reason as `getBufferedSegments` above — avoid\n // leaking the SourceBufferActor snapshot into the calling dispatcher's\n // tracking scope.\n const actorCtx = peek(sourceBufferActor.snapshot).context;\n const bufferedSegments = getBufferedSegments(track.segments);\n const currentTime = range?.start ?? 0;\n const tasks: LoadTask[] = [];\n\n // Cross-rendition switch check (mid-stream language change). Fires when\n // (a) an init segment has already been committed for some track,\n // (b) the newly-selected track is a different track, and\n // (c) the languages differ. The buffered range from the current segment\n // boundary forward is treated as stale (new track's same-timestamp\n // segments will overwrite it via MSE append-at-same-timestamp); no\n // explicit `remove` task is emitted for the cross-rendition range.\n // Computed against the currently-buffered segments (stable reference;\n // new track's playlist may not be resolved yet at first load).\n const isCrossRenditionSwitch =\n actorCtx.initTrackId !== undefined &&\n actorCtx.initTrackId !== track.id &&\n actorCtx.initTrackLanguage !== track.language;\n\n // Two categories of \"buffered content that should not gate planning\":\n //\n // - `removes` — content that needs an explicit `remove` task (out-of-window\n // forward content, back-buffer content beyond the keep window). Emitted\n // as `{ type: 'remove' }` tasks.\n // - `staleRanges` — content that the new appends will overwrite at the\n // same timestamps (cross-rendition switch). No explicit `remove` —\n // MSE's overwrite-on-append handles it, which gives a much smaller\n // perceived audio gap than `remove`-then-fetch-then-append.\n //\n // Both categories affect `effectiveBuffered` so `getSegmentsToLoad`\n // re-plans new-track segments inside them.\n const removes: Array<{ start: number; end: number }> = [];\n const staleRanges: Array<{ start: number; end: number }> = [];\n if (range) {\n if (isCrossRenditionSwitch) {\n // Mark current-segment-start onward as stale. Falls back to the\n // first buffered segment after currentTime when the playhead sits\n // in a buffer gap.\n const currentSeg = actorCtx.segments.find(\n (s) => s.startTime <= currentTime && s.startTime + s.duration > currentTime\n );\n const staleStart = currentSeg?.startTime ?? actorCtx.segments.find((s) => s.startTime > currentTime)?.startTime;\n if (staleStart !== undefined) {\n staleRanges.push({ start: staleStart, end: Infinity });\n }\n }\n const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime, forwardBufferConfig);\n if (forwardFlushStart < Infinity) {\n removes.push({ start: forwardFlushStart, end: Infinity });\n }\n const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime, backBufferConfig);\n if (backFlushEnd > 0) {\n removes.push({ start: 0, end: backFlushEnd });\n }\n for (const r of removes) tasks.push({ type: 'remove', start: r.start, end: r.end });\n }\n\n // Treat any segment overlapping a planned remove OR a stale range as\n // not-buffered. Without this, sibling renditions that share segment IDs\n // and startTimes (audio language variants) would see the new track's\n // same-startTime segments marked \"buffered\" by the pre-flush snapshot,\n // skip them in `getSegmentsToLoad`, and leave a permanent gap from\n // `currentSegmentStart` to the end of the old buffer window — stalling\n // playback.\n const overlapsStale = (seg: { startTime: number; duration: number }): boolean => {\n const segEnd = seg.startTime + seg.duration;\n return (\n removes.some((r) => seg.startTime < r.end && segEnd > r.start) ||\n staleRanges.some((r) => seg.startTime < r.end && segEnd > r.start)\n );\n };\n const effectiveBuffered =\n removes.length + staleRanges.length > 0 ? bufferedSegments.filter((s) => !overlapsStale(s)) : bufferedSegments;\n\n // Case 2: Init\n if (actorCtx.initTrackId !== track.id) {\n tasks.push({\n type: 'append-init',\n meta: { trackId: track.id, language: track.language },\n url: track.initialization.url,\n ...(track.initialization.byteRange !== undefined && { byteRange: track.initialization.byteRange }),\n });\n }\n\n // Case 3: Segments\n if (range) {\n const segmentsToLoad = getSegmentsToLoad(\n track.segments,\n effectiveBuffered,\n currentTime,\n forwardBufferConfig\n ).filter((seg) => {\n // Quality-aware filter: skip segments already covered by equal-or-higher-quality\n // content in the actor context. Preserves buffered high-quality content during\n // ABR downgrades; loads during upgrades and for uncovered positions.\n // Actor entries that overlap a planned remove OR a stale range are treated\n // as nonexistent here — they're about to be flushed or overwritten, so the\n // new-track segment must load regardless of the existing entry's bandwidth.\n const existing = actorCtx.segments.find(\n (s) => !overlapsStale(s) && Math.abs(s.startTime - seg.startTime) < SEGMENT_TIME_EPSILON\n );\n // Partial segments are still streaming — treat as not buffered so they\n // are always re-planned (avoids relying on incomplete data).\n if (existing?.partial) return true;\n if (!existing?.trackBandwidth || !track.bandwidth) return true;\n return track.bandwidth > existing.trackBandwidth;\n });\n for (const segment of segmentsToLoad) {\n tasks.push({\n type: 'append-segment',\n meta: {\n id: segment.id,\n startTime: segment.startTime,\n duration: segment.duration,\n trackId: track.id,\n trackBandwidth: track.bandwidth,\n },\n url: segment.url,\n ...(segment.byteRange !== undefined && { byteRange: segment.byteRange }),\n });\n }\n }\n\n return tasks;\n };\n\n const scheduleAll = (tasks: LoadTask[], { getContext, setContext, runner }: Ctx): void => {\n tasks.forEach((op) => {\n runner\n .schedule(makeLoadTask(op, { getContext, setContext, fetchBytes, sourceBufferActor }))\n .then(undefined, (e: unknown) => {\n if (e instanceof Error && e.name === 'AbortError') return;\n // On unexpected fetch/append errors, abort remaining tasks so a failed\n // init doesn't cause segment fetches to proceed with no init segment.\n console.error('Unexpected error in segment loader:', e);\n runner.abortPending();\n });\n });\n };\n\n return createMachineActor<UserState, SegmentLoaderActorContext, SegmentLoaderMessage, () => SerialRunner>({\n runner: () => new SerialRunner(),\n initial: 'idle',\n context: { inFlightInitTrackId: null, inFlightSegmentId: null },\n states: {\n idle: {\n on: {\n load: (msg, ctx) => {\n const allTasks = planTasks(msg);\n if (allTasks.length === 0) return;\n ctx.transition('loading');\n scheduleAll(allTasks, ctx);\n },\n },\n },\n loading: {\n onSettled: 'idle',\n on: {\n load: (msg, ctx) => {\n const { context, runner } = ctx;\n const allTasks = planTasks(msg);\n\n // Determine whether the in-flight operation is still needed.\n const inFlightStillNeeded =\n (context.inFlightSegmentId !== null &&\n allTasks.some((t) => t.type === 'append-segment' && t.meta.id === context.inFlightSegmentId)) ||\n (context.inFlightInitTrackId !== null &&\n allTasks.some((t) => t.type === 'append-init' && t.meta.trackId === context.inFlightInitTrackId));\n\n if (inFlightStillNeeded) {\n // Continue: abort only the pending queue, let the in-flight task finish.\n // Schedule everything except the in-flight item — it covers that slot.\n runner.abortPending();\n scheduleAll(\n allTasks.filter(\n (t) =>\n !(t.type === 'append-segment' && t.meta.id === context.inFlightSegmentId) &&\n !(t.type === 'append-init' && t.meta.trackId === context.inFlightInitTrackId)\n ),\n ctx\n );\n } else {\n // Preempt: abort everything and replan.\n runner.abortAll();\n // Cancel SourceBufferActor tasks when a segment is in-flight (always\n // discard) or when a track switch is happening (new track's init follows).\n // For a same-track seek with an in-flight init, skip cancel — the task's\n // signal is not aborted (abortAll was called on the runner, but the init\n // task already completed or will complete via its own signal path).\n const cancelSourceBuffer =\n context.inFlightSegmentId !== null ||\n (context.inFlightInitTrackId !== null &&\n allTasks.some((t) => t.type === 'append-init' && t.meta.trackId !== context.inFlightInitTrackId));\n if (cancelSourceBuffer) {\n sourceBufferActor.send({ type: 'cancel' });\n }\n scheduleAll(allTasks, ctx);\n }\n },\n },\n },\n },\n });\n}\n"],"mappings":"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"}
|
|
@@ -14,8 +14,16 @@ type AppendSegmentMeta = Pick<Segment, 'id' | 'startTime' | 'duration'> & {
|
|
|
14
14
|
type AppendInitMessage = {
|
|
15
15
|
type: 'append-init';
|
|
16
16
|
data: AppendData;
|
|
17
|
+
/**
|
|
18
|
+
* `language` is captured alongside `trackId` so downstream loaders can
|
|
19
|
+
* compare the buffered track's language to the newly-selected track's
|
|
20
|
+
* language and decide whether ahead-buffer flush is warranted on track
|
|
21
|
+
* switch (see `segment-loader`'s `planTasks`). Undefined for video and
|
|
22
|
+
* for audio without explicit `LANGUAGE` attribute.
|
|
23
|
+
*/
|
|
17
24
|
meta: {
|
|
18
25
|
trackId: Track['id'];
|
|
26
|
+
language?: string;
|
|
19
27
|
};
|
|
20
28
|
};
|
|
21
29
|
type AppendSegmentMessage = {
|
|
@@ -43,6 +51,13 @@ type SourceBufferActorState = 'idle' | 'updating' | 'destroyed';
|
|
|
43
51
|
/** Non-finite (extended) data managed by the actor — the XState "context". */
|
|
44
52
|
interface SourceBufferActorContext {
|
|
45
53
|
initTrackId?: string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Language of the most recently appended init segment's track (when
|
|
56
|
+
* present on the playlist). Used by the segment-loader's `planTasks`
|
|
57
|
+
* to detect cross-language switches and schedule ahead-buffer flush.
|
|
58
|
+
* Undefined for video and for language-less audio.
|
|
59
|
+
*/
|
|
60
|
+
initTrackLanguage?: string | undefined;
|
|
46
61
|
segments: Array<Pick<Segment, 'id' | 'startTime' | 'duration'> & {
|
|
47
62
|
trackId: Track['id'];
|
|
48
63
|
trackBandwidth?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-buffer.d.ts","names":[],"sources":["../../../../../src/playback/actors/dom/source-buffer.ts"],"mappings":";;;;;UAUiB,aAAA;EACf,KAAA;EACA,GAAA;AAAA;AAAA,KAGU,iBAAA,GAAoB,IAAA,CAAK,OAAA;EACnC,OAAA,EAAS,KAAA,QAJN;EAMH,cAAA;AAAA;AAAA,KAKU,iBAAA;
|
|
1
|
+
{"version":3,"file":"source-buffer.d.ts","names":[],"sources":["../../../../../src/playback/actors/dom/source-buffer.ts"],"mappings":";;;;;UAUiB,aAAA;EACf,KAAA;EACA,GAAA;AAAA;AAAA,KAGU,iBAAA,GAAoB,IAAA,CAAK,OAAA;EACnC,OAAA,EAAS,KAAA,QAJN;EAMH,cAAA;AAAA;AAAA,KAKU,iBAAA;EACV,IAAA;EACA,IAAA,EAAM,UAAA;EATQ;;;;;;;EAiBd,IAAA;IAAQ,OAAA,EAAS,KAAA;IAAa,QAAA;EAAA;AAAA;AAAA,KAEpB,oBAAA;EAAyB,IAAA;EAAwB,IAAA,EAAM,UAAA;EAAY,IAAA,EAAM,iBAAA;AAAA;AAAA,KACzE,aAAA;EAAkB,IAAA;EAAgB,KAAA;EAAe,GAAA;AAAA;AAAA,KACjD,6BAAA,GAAgC,iBAAA,GAAoB,oBAAA,GAAuB,aAAA;AAAA,KAC3E,YAAA;EAAiB,IAAA;EAAe,QAAA,EAAU,6BAAA;AAAA;AAAA,KAC1C,aAAA;EAAkB,IAAA;AAAA;;KAGlB,mBAAA,GAAsB,6BAAA,GAAgC,YAAA,GAAe,aAAA;;KAGrE,sBAAA;;UAGK,wBAAA;EACf,WAAA;EAb4B;;;;;AAC9B;EAmBE,iBAAA;EACA,QAAA,EAAU,KAAA,CACR,IAAA,CAAK,OAAA;IACH,OAAA,EAAS,KAAA;IACT,cAAA;IAvBiF;;;;;IA6BjF,OAAA;EAAA;EAGJ,cAAA,EAAgB,aAAA;AAAA;;KAIN,iBAAA,GAAoB,YAAA,CAAa,sBAAA,EAAwB,wBAAA,EAA0B,mBAAA"}
|
|
@@ -19,7 +19,8 @@ function appendInitTask(message, { getContext, sourceBuffer }) {
|
|
|
19
19
|
await appendSegment(sourceBuffer, message.data);
|
|
20
20
|
return {
|
|
21
21
|
...ctx,
|
|
22
|
-
initTrackId: message.meta.trackId
|
|
22
|
+
initTrackId: message.meta.trackId,
|
|
23
|
+
initTrackLanguage: message.meta.language
|
|
23
24
|
};
|
|
24
25
|
});
|
|
25
26
|
}
|