@videojs/spf 10.0.0-beta.30 → 10.0.0-beta.32
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/core/actors/create-machine-actor.js +23 -25
- package/dist/default/core/actors/create-machine-actor.js.map +1 -1
- package/dist/default/core/actors/create-transition-actor.js +7 -9
- package/dist/default/core/actors/create-transition-actor.js.map +1 -1
- package/dist/default/core/composition/create-composition.js +26 -29
- package/dist/default/core/composition/create-composition.js.map +1 -1
- package/dist/default/core/composition/share-signals.js +12 -17
- package/dist/default/core/composition/share-signals.js.map +1 -1
- package/dist/default/core/machine.js +3 -4
- package/dist/default/core/machine.js.map +1 -1
- package/dist/default/core/reactors/create-machine-reactor.js +20 -19
- package/dist/default/core/reactors/create-machine-reactor.js.map +1 -1
- package/dist/default/core/signals/effect.js +24 -4
- package/dist/default/core/signals/effect.js.map +1 -1
- package/dist/default/core/signals/primitives.js +10 -14
- package/dist/default/core/signals/primitives.js.map +1 -1
- package/dist/default/core/signals/when.js +6 -8
- package/dist/default/core/signals/when.js.map +1 -1
- package/dist/default/core/tasks/delayed-reschedule.js +7 -11
- package/dist/default/core/tasks/delayed-reschedule.js.map +1 -1
- package/dist/default/core/tasks/task.js +59 -82
- package/dist/default/core/tasks/task.js.map +1 -1
- package/dist/default/hls.js +3 -1
- package/dist/default/index.js +2 -3
- package/dist/default/index.js.map +1 -1
- package/dist/default/media/abr/quality-selection.js +3 -7
- package/dist/default/media/abr/quality-selection.js.map +1 -1
- package/dist/default/media/buffer/back-buffer.js +15 -19
- package/dist/default/media/buffer/back-buffer.js.map +1 -1
- package/dist/default/media/buffer/forward-buffer.js +32 -41
- package/dist/default/media/buffer/forward-buffer.js.map +1 -1
- package/dist/default/media/dom/capabilities.js +26 -37
- package/dist/default/media/dom/capabilities.js.map +1 -1
- package/dist/default/media/dom/mse/append-segment.js +3 -5
- package/dist/default/media/dom/mse/append-segment.js.map +1 -1
- package/dist/default/media/dom/mse/buffer-flusher.js +5 -5
- package/dist/default/media/dom/mse/buffer-flusher.js.map +1 -1
- package/dist/default/media/dom/mse/duration.js +23 -31
- package/dist/default/media/dom/mse/duration.js.map +1 -1
- package/dist/default/media/dom/mse/end-of-stream.js +3 -6
- package/dist/default/media/dom/mse/end-of-stream.js.map +1 -1
- package/dist/default/media/dom/mse/mediasource-setup.js +65 -81
- package/dist/default/media/dom/mse/mediasource-setup.js.map +1 -1
- package/dist/default/media/dom/screen.js +37 -54
- package/dist/default/media/dom/screen.js.map +1 -1
- package/dist/default/media/dom/text/resolve-vtt-segment.js.map +1 -1
- package/dist/default/media/dom/text/text-track-slots.js +15 -23
- package/dist/default/media/dom/text/text-track-slots.js.map +1 -1
- package/dist/default/media/errors.js +25 -34
- package/dist/default/media/errors.js.map +1 -1
- package/dist/default/media/hls/parse-attributes.js +9 -26
- package/dist/default/media/hls/parse-attributes.js.map +1 -1
- package/dist/default/media/hls/parse-media-playlist.js +25 -33
- package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
- package/dist/default/media/hls/parse-multivariant.js +3 -3
- package/dist/default/media/hls/parse-multivariant.js.map +1 -1
- package/dist/default/media/hls/reload-policy.js +19 -27
- package/dist/default/media/hls/reload-policy.js.map +1 -1
- package/dist/default/media/hls/resolve-url.js +1 -3
- package/dist/default/media/hls/resolve-url.js.map +1 -1
- package/dist/default/media/live-window.js +9 -13
- package/dist/default/media/live-window.js.map +1 -1
- package/dist/default/media/media-tracks/media-tracks.js +12 -17
- package/dist/default/media/media-tracks/media-tracks.js.map +1 -1
- package/dist/default/media/mp4/box.js +4 -5
- package/dist/default/media/mp4/box.js.map +1 -1
- package/dist/default/media/mp4/timestamp-origin.js +30 -42
- package/dist/default/media/mp4/timestamp-origin.js.map +1 -1
- package/dist/default/media/primitives/resolution.js +5 -8
- package/dist/default/media/primitives/resolution.js.map +1 -1
- package/dist/default/media/primitives/select-tracks.js +24 -34
- package/dist/default/media/primitives/select-tracks.js.map +1 -1
- package/dist/default/media/text/parse-vtt-timestamp-map.js +6 -8
- package/dist/default/media/text/parse-vtt-timestamp-map.js.map +1 -1
- package/dist/default/media/text/resolve-vtt-metadata.js +8 -9
- package/dist/default/media/text/resolve-vtt-metadata.js.map +1 -1
- package/dist/default/media/types/index.js +10 -18
- package/dist/default/media/types/index.js.map +1 -1
- package/dist/default/media/utils/cdn.js +10 -16
- package/dist/default/media/utils/cdn.js.map +1 -1
- package/dist/default/media/utils/preload.js +4 -6
- package/dist/default/media/utils/preload.js.map +1 -1
- package/dist/default/media/utils/track-selection.js +10 -14
- package/dist/default/media/utils/track-selection.js.map +1 -1
- package/dist/default/media/utils/tracks.js +56 -54
- package/dist/default/media/utils/tracks.js.map +1 -1
- package/dist/default/network/bandwidth-estimator.js +18 -19
- package/dist/default/network/bandwidth-estimator.js.map +1 -1
- package/dist/default/network/chunked-stream-iterable.js +4 -6
- package/dist/default/network/chunked-stream-iterable.js.map +1 -1
- package/dist/default/network/ewma.js +22 -25
- package/dist/default/network/ewma.js.map +1 -1
- package/dist/default/network/fetch.js +30 -37
- package/dist/default/network/fetch.js.map +1 -1
- package/dist/default/playback/actors/dom/segment-loader.js +28 -44
- package/dist/default/playback/actors/dom/segment-loader.js.map +1 -1
- package/dist/default/playback/actors/dom/source-buffer.js.map +1 -1
- package/dist/default/playback/actors/dom/text-tracks.js.map +1 -1
- package/dist/default/playback/actors/text-track-segment-loader.js +24 -35
- package/dist/default/playback/actors/text-track-segment-loader.js.map +1 -1
- package/dist/default/playback/adapters/hls-audio/adapter.js +23 -31
- package/dist/default/playback/adapters/hls-audio/adapter.js.map +1 -1
- package/dist/default/playback/adapters/hls-audio/media.js.map +1 -1
- package/dist/default/playback/adapters/hls-background-video/adapter.js +50 -70
- package/dist/default/playback/adapters/hls-background-video/adapter.js.map +1 -1
- package/dist/default/playback/adapters/hls-background-video/host.js +12 -16
- package/dist/default/playback/adapters/hls-background-video/host.js.map +1 -1
- package/dist/default/playback/adapters/hls-background-video/media.js +5 -8
- package/dist/default/playback/adapters/hls-background-video/media.js.map +1 -1
- package/dist/default/playback/adapters/hls-video/adapter.js +43 -60
- package/dist/default/playback/adapters/hls-video/adapter.js.map +1 -1
- package/dist/default/playback/adapters/hls-video/error-surface.js +13 -18
- package/dist/default/playback/adapters/hls-video/error-surface.js.map +1 -1
- package/dist/default/playback/adapters/hls-video/media-tracks.js +4 -5
- package/dist/default/playback/adapters/hls-video/media-tracks.js.map +1 -1
- package/dist/default/playback/adapters/hls-video/media.js.map +1 -1
- package/dist/default/playback/adapters/mux-audio/media.js +8 -12
- package/dist/default/playback/adapters/mux-audio/media.js.map +1 -1
- package/dist/default/playback/adapters/mux-video/adapter.js +25 -33
- package/dist/default/playback/adapters/mux-video/adapter.js.map +1 -1
- package/dist/default/playback/adapters/mux-video/media.js +7 -9
- package/dist/default/playback/adapters/mux-video/media.js.map +1 -1
- package/dist/default/playback/behaviors/calculate-presentation-duration.js +5 -8
- package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -1
- package/dist/default/playback/behaviors/collect-errors.js +39 -52
- package/dist/default/playback/behaviors/collect-errors.js.map +1 -1
- package/dist/default/playback/behaviors/derive-cdn-priority.js +16 -23
- package/dist/default/playback/behaviors/derive-cdn-priority.js.map +1 -1
- package/dist/default/playback/behaviors/dom/airplay.js +38 -61
- package/dist/default/playback/behaviors/dom/airplay.js.map +1 -1
- package/dist/default/playback/behaviors/dom/apply-start-position.js +23 -35
- package/dist/default/playback/behaviors/dom/apply-start-position.js.map +1 -1
- package/dist/default/playback/behaviors/dom/end-of-stream.js +11 -16
- package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
- package/dist/default/playback/behaviors/dom/load-segments.js +27 -39
- package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -1
- package/dist/default/playback/behaviors/dom/recover-end-stall.js +21 -26
- package/dist/default/playback/behaviors/dom/recover-end-stall.js.map +1 -1
- package/dist/default/playback/behaviors/dom/seek-to-live-edge.js +41 -60
- package/dist/default/playback/behaviors/dom/seek-to-live-edge.js.map +1 -1
- package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +50 -74
- package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
- package/dist/default/playback/behaviors/dom/setup-mediasource.js +29 -43
- package/dist/default/playback/behaviors/dom/setup-mediasource.js.map +1 -1
- package/dist/default/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
- package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js +3 -4
- package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -1
- package/dist/default/playback/behaviors/dom/sync-text-tracks.js +30 -45
- package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-current-time.js +9 -11
- package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-load-triggers.js +24 -32
- package/dist/default/playback/behaviors/dom/track-load-triggers.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-playback-rate.js +6 -8
- package/dist/default/playback/behaviors/dom/track-playback-rate.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-player-resolution.js +10 -14
- package/dist/default/playback/behaviors/dom/track-player-resolution.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-screen-resolution.js +8 -9
- package/dist/default/playback/behaviors/dom/track-screen-resolution.js.map +1 -1
- package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +28 -43
- package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
- package/dist/default/playback/behaviors/establish-start-media-time.js +32 -42
- package/dist/default/playback/behaviors/establish-start-media-time.js.map +1 -1
- package/dist/default/playback/behaviors/resolve-presentation.js +13 -22
- package/dist/default/playback/behaviors/resolve-presentation.js.map +1 -1
- package/dist/default/playback/behaviors/resolve-track.js +5 -12
- package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
- package/dist/default/playback/behaviors/select-tracks.js +77 -104
- package/dist/default/playback/behaviors/select-tracks.js.map +1 -1
- package/dist/default/playback/behaviors/setup-failover-monitor.js +11 -16
- package/dist/default/playback/behaviors/setup-failover-monitor.js.map +1 -1
- package/dist/default/playback/behaviors/sync-preload.js +9 -13
- package/dist/default/playback/behaviors/sync-preload.js.map +1 -1
- package/dist/default/playback/behaviors/track-switching.js +189 -173
- package/dist/default/playback/behaviors/track-switching.js.map +1 -1
- package/dist/default/playback/engines/hls/engine-audio-only.js +20 -24
- package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -1
- package/dist/default/playback/engines/hls/engine-background-video.js +22 -27
- package/dist/default/playback/engines/hls/engine-background-video.js.map +1 -1
- package/dist/default/playback/engines/hls/engine.js +22 -25
- package/dist/default/playback/engines/hls/engine.js.map +1 -1
- package/dist/default/playback/primitives/error-messages.js +16 -25
- package/dist/default/playback/primitives/error-messages.js.map +1 -1
- package/dist/default/playback/primitives/failover-fetch.js +9 -14
- package/dist/default/playback/primitives/failover-fetch.js.map +1 -1
- package/dist/default/playback/primitives/head-peek.js +7 -10
- package/dist/default/playback/primitives/head-peek.js.map +1 -1
- package/dist/default/playback/primitives/live-window.js +12 -15
- package/dist/default/playback/primitives/live-window.js.map +1 -1
- package/dist/default/playback/primitives/relocation-pipelines.js +34 -46
- package/dist/default/playback/primitives/relocation-pipelines.js.map +1 -1
- package/dist/default/playback/primitives/report-track-conditions.js +26 -38
- package/dist/default/playback/primitives/report-track-conditions.js.map +1 -1
- package/dist/default/playback/primitives/segment-load-pipeline.js +15 -15
- package/dist/default/playback/primitives/segment-load-pipeline.js.map +1 -1
- package/dist/default/playback/primitives/selection-rules.js +65 -36
- package/dist/default/playback/primitives/selection-rules.js.map +1 -1
- package/dist/default/playback/primitives/text-segment-load-pipeline.js +4 -5
- package/dist/default/playback/primitives/text-segment-load-pipeline.js.map +1 -1
- package/dist/default/playback/primitives/track-types.js +10 -18
- package/dist/default/playback/primitives/track-types.js.map +1 -1
- package/dist/dev/core/actors/actor.d.ts +4 -4
- package/dist/dev/core/actors/actor.d.ts.map +1 -1
- package/dist/dev/core/actors/create-machine-actor.d.ts +38 -47
- package/dist/dev/core/actors/create-machine-actor.d.ts.map +1 -1
- package/dist/dev/core/actors/create-machine-actor.js +23 -25
- package/dist/dev/core/actors/create-machine-actor.js.map +1 -1
- package/dist/dev/core/actors/create-transition-actor.d.ts +11 -15
- package/dist/dev/core/actors/create-transition-actor.d.ts.map +1 -1
- package/dist/dev/core/actors/create-transition-actor.js +7 -9
- package/dist/dev/core/actors/create-transition-actor.js.map +1 -1
- package/dist/dev/core/composition/create-composition.d.ts +70 -102
- package/dist/dev/core/composition/create-composition.d.ts.map +1 -1
- package/dist/dev/core/composition/create-composition.js +26 -29
- package/dist/dev/core/composition/create-composition.js.map +1 -1
- package/dist/dev/core/composition/share-signals.d.ts +17 -24
- package/dist/dev/core/composition/share-signals.d.ts.map +1 -1
- package/dist/dev/core/composition/share-signals.js +12 -17
- package/dist/dev/core/composition/share-signals.js.map +1 -1
- package/dist/dev/core/machine.d.ts +4 -4
- package/dist/dev/core/machine.js +3 -4
- package/dist/dev/core/machine.js.map +1 -1
- package/dist/dev/core/reactors/create-machine-reactor.d.ts +33 -39
- package/dist/dev/core/reactors/create-machine-reactor.d.ts.map +1 -1
- package/dist/dev/core/reactors/create-machine-reactor.js +20 -19
- package/dist/dev/core/reactors/create-machine-reactor.js.map +1 -1
- package/dist/dev/core/signals/effect.d.ts +3 -4
- package/dist/dev/core/signals/effect.d.ts.map +1 -1
- package/dist/dev/core/signals/effect.js +24 -4
- package/dist/dev/core/signals/effect.js.map +1 -1
- package/dist/dev/core/signals/primitives.d.ts +10 -12
- package/dist/dev/core/signals/primitives.d.ts.map +1 -1
- package/dist/dev/core/signals/primitives.js +10 -14
- package/dist/dev/core/signals/primitives.js.map +1 -1
- package/dist/dev/core/signals/when.js +6 -8
- package/dist/dev/core/signals/when.js.map +1 -1
- package/dist/dev/core/tasks/delayed-reschedule.js +7 -11
- package/dist/dev/core/tasks/delayed-reschedule.js.map +1 -1
- package/dist/dev/core/tasks/task.d.ts +51 -73
- package/dist/dev/core/tasks/task.d.ts.map +1 -1
- package/dist/dev/core/tasks/task.js +59 -82
- package/dist/dev/core/tasks/task.js.map +1 -1
- package/dist/dev/hls.d.ts +3 -1
- package/dist/dev/hls.js +3 -1
- package/dist/dev/index.d.ts +2 -3
- package/dist/dev/index.d.ts.map +1 -1
- package/dist/dev/index.js +2 -3
- package/dist/dev/index.js.map +1 -1
- package/dist/dev/media/abr/quality-selection.d.ts +7 -10
- package/dist/dev/media/abr/quality-selection.d.ts.map +1 -1
- package/dist/dev/media/abr/quality-selection.js +3 -7
- package/dist/dev/media/abr/quality-selection.js.map +1 -1
- package/dist/dev/media/buffer/back-buffer.d.ts +2 -7
- package/dist/dev/media/buffer/back-buffer.d.ts.map +1 -1
- package/dist/dev/media/buffer/back-buffer.js +15 -19
- package/dist/dev/media/buffer/back-buffer.js.map +1 -1
- package/dist/dev/media/buffer/forward-buffer.d.ts +2 -7
- package/dist/dev/media/buffer/forward-buffer.d.ts.map +1 -1
- package/dist/dev/media/buffer/forward-buffer.js +32 -41
- package/dist/dev/media/buffer/forward-buffer.js.map +1 -1
- package/dist/dev/media/dom/capabilities.js +26 -37
- package/dist/dev/media/dom/capabilities.js.map +1 -1
- package/dist/dev/media/dom/mse/append-segment.d.ts +3 -5
- package/dist/dev/media/dom/mse/append-segment.d.ts.map +1 -1
- package/dist/dev/media/dom/mse/append-segment.js +3 -5
- package/dist/dev/media/dom/mse/append-segment.js.map +1 -1
- package/dist/dev/media/dom/mse/buffer-flusher.d.ts +5 -5
- package/dist/dev/media/dom/mse/buffer-flusher.js +5 -5
- package/dist/dev/media/dom/mse/buffer-flusher.js.map +1 -1
- package/dist/dev/media/dom/mse/duration.js +23 -31
- package/dist/dev/media/dom/mse/duration.js.map +1 -1
- package/dist/dev/media/dom/mse/end-of-stream.js +3 -6
- package/dist/dev/media/dom/mse/end-of-stream.js.map +1 -1
- package/dist/dev/media/dom/mse/mediasource-setup.js +65 -81
- package/dist/dev/media/dom/mse/mediasource-setup.js.map +1 -1
- package/dist/dev/media/dom/screen.d.ts.map +1 -1
- package/dist/dev/media/dom/screen.js +37 -54
- package/dist/dev/media/dom/screen.js.map +1 -1
- package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts.map +1 -1
- package/dist/dev/media/dom/text/resolve-vtt-segment.js.map +1 -1
- package/dist/dev/media/dom/text/text-track-slots.d.ts +10 -16
- package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -1
- package/dist/dev/media/dom/text/text-track-slots.js +15 -23
- package/dist/dev/media/dom/text/text-track-slots.js.map +1 -1
- package/dist/dev/media/errors.d.ts +32 -46
- package/dist/dev/media/errors.d.ts.map +1 -1
- package/dist/dev/media/errors.js +25 -34
- package/dist/dev/media/errors.js.map +1 -1
- package/dist/dev/media/hls/parse-attributes.js +9 -26
- package/dist/dev/media/hls/parse-attributes.js.map +1 -1
- package/dist/dev/media/hls/parse-media-playlist.js +25 -33
- package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
- package/dist/dev/media/hls/parse-multivariant.js +3 -3
- package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
- package/dist/dev/media/hls/reload-policy.js +19 -27
- package/dist/dev/media/hls/reload-policy.js.map +1 -1
- package/dist/dev/media/hls/resolve-url.js +1 -3
- package/dist/dev/media/hls/resolve-url.js.map +1 -1
- package/dist/dev/media/live-window.js +9 -13
- package/dist/dev/media/live-window.js.map +1 -1
- package/dist/dev/media/media-tracks/media-tracks.d.ts +8 -13
- package/dist/dev/media/media-tracks/media-tracks.d.ts.map +1 -1
- package/dist/dev/media/media-tracks/media-tracks.js +12 -17
- package/dist/dev/media/media-tracks/media-tracks.js.map +1 -1
- package/dist/dev/media/mp4/box.js +4 -5
- package/dist/dev/media/mp4/box.js.map +1 -1
- package/dist/dev/media/mp4/timestamp-origin.js +30 -42
- package/dist/dev/media/mp4/timestamp-origin.js.map +1 -1
- package/dist/dev/media/primitives/resolution.d.ts +6 -8
- package/dist/dev/media/primitives/resolution.d.ts.map +1 -1
- package/dist/dev/media/primitives/resolution.js +5 -8
- package/dist/dev/media/primitives/resolution.js.map +1 -1
- package/dist/dev/media/primitives/select-tracks.js +24 -34
- package/dist/dev/media/primitives/select-tracks.js.map +1 -1
- package/dist/dev/media/text/parse-vtt-timestamp-map.js +6 -8
- package/dist/dev/media/text/parse-vtt-timestamp-map.js.map +1 -1
- package/dist/dev/media/text/resolve-vtt-metadata.js +8 -9
- package/dist/dev/media/text/resolve-vtt-metadata.js.map +1 -1
- package/dist/dev/media/types/index.d.ts +106 -177
- package/dist/dev/media/types/index.d.ts.map +1 -1
- package/dist/dev/media/types/index.js +10 -18
- package/dist/dev/media/types/index.js.map +1 -1
- package/dist/dev/media/utils/cdn.d.ts +4 -6
- package/dist/dev/media/utils/cdn.d.ts.map +1 -1
- package/dist/dev/media/utils/cdn.js +10 -16
- package/dist/dev/media/utils/cdn.js.map +1 -1
- package/dist/dev/media/utils/preload.js +4 -6
- package/dist/dev/media/utils/preload.js.map +1 -1
- package/dist/dev/media/utils/track-selection.js +10 -14
- package/dist/dev/media/utils/track-selection.js.map +1 -1
- package/dist/dev/media/utils/tracks.js +56 -54
- package/dist/dev/media/utils/tracks.js.map +1 -1
- package/dist/dev/network/bandwidth-estimator.d.ts +7 -9
- package/dist/dev/network/bandwidth-estimator.d.ts.map +1 -1
- package/dist/dev/network/bandwidth-estimator.js +18 -19
- package/dist/dev/network/bandwidth-estimator.js.map +1 -1
- package/dist/dev/network/chunked-stream-iterable.js +4 -6
- package/dist/dev/network/chunked-stream-iterable.js.map +1 -1
- package/dist/dev/network/ewma.js +22 -25
- package/dist/dev/network/ewma.js.map +1 -1
- package/dist/dev/network/fetch.js +30 -37
- package/dist/dev/network/fetch.js.map +1 -1
- package/dist/dev/playback/actors/dom/segment-loader.d.ts +7 -11
- package/dist/dev/playback/actors/dom/segment-loader.d.ts.map +1 -1
- package/dist/dev/playback/actors/dom/segment-loader.js +28 -44
- package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -1
- package/dist/dev/playback/actors/dom/source-buffer.d.ts +5 -7
- package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -1
- package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -1
- package/dist/dev/playback/actors/dom/text-tracks.js.map +1 -1
- package/dist/dev/playback/actors/text-track-segment-loader.d.ts +10 -16
- package/dist/dev/playback/actors/text-track-segment-loader.d.ts.map +1 -1
- package/dist/dev/playback/actors/text-track-segment-loader.js +24 -35
- package/dist/dev/playback/actors/text-track-segment-loader.js.map +1 -1
- package/dist/dev/playback/actors/text-tracks.d.ts +5 -8
- package/dist/dev/playback/actors/text-tracks.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-audio/adapter.d.ts +11 -14
- package/dist/dev/playback/adapters/hls-audio/adapter.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-audio/adapter.js +23 -31
- package/dist/dev/playback/adapters/hls-audio/adapter.js.map +1 -1
- package/dist/dev/playback/adapters/hls-audio/media.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-audio/media.js.map +1 -1
- package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts +28 -39
- package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-background-video/adapter.js +50 -70
- package/dist/dev/playback/adapters/hls-background-video/adapter.js.map +1 -1
- package/dist/dev/playback/adapters/hls-background-video/host.d.ts +12 -16
- package/dist/dev/playback/adapters/hls-background-video/host.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-background-video/host.js +12 -16
- package/dist/dev/playback/adapters/hls-background-video/host.js.map +1 -1
- package/dist/dev/playback/adapters/hls-background-video/media.d.ts +5 -8
- package/dist/dev/playback/adapters/hls-background-video/media.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-background-video/media.js +5 -8
- package/dist/dev/playback/adapters/hls-background-video/media.js.map +1 -1
- package/dist/dev/playback/adapters/hls-video/adapter.d.ts +12 -14
- package/dist/dev/playback/adapters/hls-video/adapter.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-video/adapter.js +43 -60
- package/dist/dev/playback/adapters/hls-video/adapter.js.map +1 -1
- package/dist/dev/playback/adapters/hls-video/error-surface.d.ts +4 -5
- package/dist/dev/playback/adapters/hls-video/error-surface.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-video/error-surface.js +13 -18
- package/dist/dev/playback/adapters/hls-video/error-surface.js.map +1 -1
- package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts +4 -5
- package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-video/media-tracks.js +4 -5
- package/dist/dev/playback/adapters/hls-video/media-tracks.js.map +1 -1
- package/dist/dev/playback/adapters/hls-video/media.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-video/media.js.map +1 -1
- package/dist/dev/playback/adapters/mux-audio/media.d.ts +8 -12
- package/dist/dev/playback/adapters/mux-audio/media.d.ts.map +1 -1
- package/dist/dev/playback/adapters/mux-audio/media.js +8 -12
- package/dist/dev/playback/adapters/mux-audio/media.js.map +1 -1
- package/dist/dev/playback/adapters/mux-video/adapter.d.ts +9 -11
- package/dist/dev/playback/adapters/mux-video/adapter.d.ts.map +1 -1
- package/dist/dev/playback/adapters/mux-video/adapter.js +25 -33
- package/dist/dev/playback/adapters/mux-video/adapter.js.map +1 -1
- package/dist/dev/playback/adapters/mux-video/media.d.ts +7 -9
- package/dist/dev/playback/adapters/mux-video/media.d.ts.map +1 -1
- package/dist/dev/playback/adapters/mux-video/media.js +7 -9
- package/dist/dev/playback/adapters/mux-video/media.js.map +1 -1
- package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +5 -8
- package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/calculate-presentation-duration.js +5 -8
- package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -1
- package/dist/dev/playback/behaviors/collect-errors.d.ts +2 -0
- package/dist/dev/playback/behaviors/collect-errors.js +39 -52
- package/dist/dev/playback/behaviors/collect-errors.js.map +1 -1
- package/dist/dev/playback/behaviors/derive-cdn-priority.js +16 -23
- package/dist/dev/playback/behaviors/derive-cdn-priority.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/airplay.js +38 -61
- package/dist/dev/playback/behaviors/dom/airplay.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/apply-start-position.js +23 -35
- package/dist/dev/playback/behaviors/dom/apply-start-position.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/end-of-stream.js +11 -16
- package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/load-segments.d.ts +5 -8
- package/dist/dev/playback/behaviors/dom/load-segments.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/load-segments.js +27 -39
- package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/recover-end-stall.js +21 -26
- package/dist/dev/playback/behaviors/dom/recover-end-stall.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js +41 -60
- package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +50 -74
- package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/setup-mediasource.js +29 -43
- package/dist/dev/playback/behaviors/dom/setup-mediasource.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts +3 -3
- package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js +3 -4
- package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/sync-text-tracks.js +30 -45
- package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-current-time.d.ts +2 -2
- package/dist/dev/playback/behaviors/dom/track-current-time.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-current-time.js +9 -11
- package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts +6 -12
- package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-load-triggers.js +24 -32
- package/dist/dev/playback/behaviors/dom/track-load-triggers.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts +2 -2
- package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-playback-rate.js +6 -8
- package/dist/dev/playback/behaviors/dom/track-playback-rate.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-player-resolution.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-player-resolution.js +10 -14
- package/dist/dev/playback/behaviors/dom/track-player-resolution.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-screen-resolution.js +8 -9
- package/dist/dev/playback/behaviors/dom/track-screen-resolution.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +28 -43
- package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
- package/dist/dev/playback/behaviors/establish-start-media-time.d.ts +15 -19
- package/dist/dev/playback/behaviors/establish-start-media-time.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/establish-start-media-time.js +32 -42
- package/dist/dev/playback/behaviors/establish-start-media-time.js.map +1 -1
- package/dist/dev/playback/behaviors/resolve-presentation.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/resolve-presentation.js +13 -22
- package/dist/dev/playback/behaviors/resolve-presentation.js.map +1 -1
- package/dist/dev/playback/behaviors/resolve-track.js +5 -12
- package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
- package/dist/dev/playback/behaviors/select-tracks.d.ts +28 -39
- package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/select-tracks.js +77 -104
- package/dist/dev/playback/behaviors/select-tracks.js.map +1 -1
- package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts +1 -4
- package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/setup-failover-monitor.js +11 -16
- package/dist/dev/playback/behaviors/setup-failover-monitor.js.map +1 -1
- package/dist/dev/playback/behaviors/sync-preload.js +9 -13
- package/dist/dev/playback/behaviors/sync-preload.js.map +1 -1
- package/dist/dev/playback/behaviors/track-switching.d.ts +110 -0
- package/dist/dev/playback/behaviors/track-switching.d.ts.map +1 -0
- package/dist/dev/playback/behaviors/track-switching.js +189 -173
- package/dist/dev/playback/behaviors/track-switching.js.map +1 -1
- package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +59 -72
- package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/engine-audio-only.js +20 -24
- package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -1
- package/dist/dev/playback/engines/hls/engine-background-video.d.ts +51 -78
- package/dist/dev/playback/engines/hls/engine-background-video.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/engine-background-video.js +22 -27
- package/dist/dev/playback/engines/hls/engine-background-video.js.map +1 -1
- package/dist/dev/playback/engines/hls/engine.d.ts +115 -155
- package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/engine.js +22 -25
- package/dist/dev/playback/engines/hls/engine.js.map +1 -1
- package/dist/dev/playback/primitives/derive-start-media-time.d.ts +3 -3
- package/dist/dev/playback/primitives/derive-start-media-time.d.ts.map +1 -1
- package/dist/dev/playback/primitives/error-messages.js +16 -25
- package/dist/dev/playback/primitives/error-messages.js.map +1 -1
- package/dist/dev/playback/primitives/failover-fetch.js +9 -14
- package/dist/dev/playback/primitives/failover-fetch.js.map +1 -1
- package/dist/dev/playback/primitives/head-peek.js +7 -10
- package/dist/dev/playback/primitives/head-peek.js.map +1 -1
- package/dist/dev/playback/primitives/live-window.js +12 -15
- package/dist/dev/playback/primitives/live-window.js.map +1 -1
- package/dist/dev/playback/primitives/relocation-pipelines.js +34 -46
- package/dist/dev/playback/primitives/relocation-pipelines.js.map +1 -1
- package/dist/dev/playback/primitives/report-track-conditions.d.ts +1 -4
- package/dist/dev/playback/primitives/report-track-conditions.d.ts.map +1 -1
- package/dist/dev/playback/primitives/report-track-conditions.js +26 -38
- package/dist/dev/playback/primitives/report-track-conditions.js.map +1 -1
- package/dist/dev/playback/primitives/segment-load-pipeline.js +15 -15
- package/dist/dev/playback/primitives/segment-load-pipeline.js.map +1 -1
- package/dist/dev/playback/primitives/selection-rules.d.ts +45 -17
- package/dist/dev/playback/primitives/selection-rules.d.ts.map +1 -1
- package/dist/dev/playback/primitives/selection-rules.js +65 -36
- package/dist/dev/playback/primitives/selection-rules.js.map +1 -1
- package/dist/dev/playback/primitives/source-buffer-messages.d.ts +6 -9
- package/dist/dev/playback/primitives/source-buffer-messages.d.ts.map +1 -1
- package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts +20 -29
- package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts.map +1 -1
- package/dist/dev/playback/primitives/text-segment-load-pipeline.js +4 -5
- package/dist/dev/playback/primitives/text-segment-load-pipeline.js.map +1 -1
- package/dist/dev/playback/primitives/text-track-messages.d.ts.map +1 -1
- package/dist/dev/playback/primitives/track-types.js +10 -18
- package/dist/dev/playback/primitives/track-types.js.map +1 -1
- package/package.json +25 -28
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"track-switching.js","names":["getCdnId","defaultGetCdnId"],"sources":["../../../../src/playback/behaviors/track-switching.ts"],"sourcesContent":["/**\n * **Per-type track selection as a rule chain.** While a presentation is\n * resolved, owns that type's `selected{Video,Audio,Text}TrackId` signal: pick a\n * default, react to user intent and algorithmic ranking, and clear it on src\n * unload.\n *\n * Selection runs in two stages. First a **hard-constraints pre-pass**\n * (`applyConstraints`) prunes the unplayable from the candidate set — the\n * failed-CDN constraint (`excludeFailedCdns`, failover cooldown) and the\n * capability constraint (`excludeUnplayableTracks`, codec support). Then a small\n * ordered chain of rules (`applyRules`) picks among the survivors. Each constraint/rule reads the signals it needs at apply\n * time, so the effect subscribes to exactly what was consulted. The chain runs\n * most authoritative first:\n *\n * 1. **user intent** — a soft filter on `user*TrackSelection`: narrow to the\n * partial-track match; an empty match falls through to the full set.\n * 2. **active CDN** — a soft filter on `cdnPriority` (`preferActiveCdn`):\n * narrow to the highest-priority CDN that still has tracks; an empty match\n * falls through. Shared by video and audio, so every type stays on one CDN\n * (`deriveCdnPriority` owns the list). No-op for non-redundant sources.\n * 3. **player resolution** — a soft filter on `playerResolution`\n * (`playerResolutionCap`, video only): narrow to the smallest rendition\n * tier covering the player element, plus everything below it. No-op\n * without a measurement. Ahead of the ranker but behind the CDN scope, so\n * the cap chooses *within* a host rather than between hosts.\n * 4. **ranking** — the terminal sort: `rankByBandwidth`, shared by video and\n * audio. Fitting tracks (within the throughput threshold) first, highest\n * bitrate first; over-throughput tracks after, least-over first. Hysteresis\n * via boosting the current track's sort weight by `upgradeMargin`.\n *\n * The composer's early-bail (one survivor → stop) is load-bearing: a user\n * selection that narrows to a single track is the pick without the ranker\n * running, so the bandwidth estimate is never read and the effect doesn't\n * re-fire on bandwidth while that choice holds.\n *\n * Lifecycle: `'presentation-unresolved'` ↔ `'presentation-resolved'`. The\n * resolved state owns the signal; its entry-returned cleanup clears it on exit\n * (canonical cleanup-binds-to-setup per `reactors.md`).\n *\n * The pick is the chain's result mapped to a slot value by `resolveSelection`\n * (default: the head, `applyRules(...)[0]`). Each variant supplies its\n * **constraints + rule chain (+ optional resolveSelection)** via config;\n * `setupTrackSwitching` owns only the lifecycle and runs what it's given. Video\n * and audio run constraints `[excludeFailedCdns, excludeUnplayableTracks]` then\n * rules `[filterByUserSelection, preferActiveCdn, rankByBandwidth]` and take the\n * head; video inserts `playerResolutionCap` after the active-CDN scope, and\n * `switchVideoTrack` also accepts ABR tuning config, `switchAudioTrack` takes\n * none. `switchTextTrack` differs — selection is *optional* (captions are\n * opt-in / off-able), so it runs `[excludeFailedCdns]` + `[preferActiveCdn]` and\n * supplies a text terminal (`pickResolvedTextTrack`) that resolves standing user\n * intent (`userTextTrackSelection`, incl. `'off'`) and may yield no selection.\n * (The active-CDN *scope* is the sticky-pick half of multi-CDN; the failed-CDN\n * *constraint* is the failover half — prune the cooled-down CDN, the scope falls\n * to the next.)\n *\n * When the pre-pass prunes a type that *has* tracks to empty, the behavior\n * clears the selection (so a now-unplayable pick can't linger and stall) and\n * reports the type's `noSupportedTrackCode`. Which constraint emptied the set is\n * deliberately not consulted — the behavior reads no constraint's state, so the\n * chain stays composable. A type with no tracks at all is left alone; that's a\n * legitimate source shape, not a failure. The late `createSourceBuffer` check\n * stays as the structural backstop.\n *\n * Deferred: audio's preferred-language / default-track selection as standing\n * soft-filter rules (previously the empty-slot picker, dropped in the move to\n * the rule chain).\n */\n\nimport { type AnySlotMap, defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../core/signals/primitives';\nimport { DEFAULT_QUALITY_CONFIG, type QualityConfig, resolutionArea } from '../../media/abr/quality-selection';\nimport { SVTA_NO_SUPPORTED_AUDIO_TRACK, SVTA_NO_SUPPORTED_VIDEO_TRACK } from '../../media/errors';\nimport type { Resolution } from '../../media/primitives/resolution';\nimport {\n matchesPartialTrack,\n pickTextTrackFromTracks,\n smallestCoveringPixelArea,\n type TextSelectionConfig,\n tracksUnderPixelArea,\n} from '../../media/primitives/select-tracks';\nimport {\n type AudioTrack,\n type CanPlayTrack,\n isResolvedPresentation,\n type MaybeResolvedPresentation,\n type PartiallyResolvedAudioTrack,\n type PartiallyResolvedTextTrack,\n type PartiallyResolvedVideoTrack,\n type TextTrack,\n type VideoTrack,\n} from '../../media/types';\nimport { getCdnId as defaultGetCdnId, type GetCdnId } from '../../media/utils/cdn';\nimport { getTracksByType } from '../../media/utils/tracks';\nimport type { BandwidthConfig, BandwidthState } from '../../network/bandwidth-estimator';\nimport { DEFAULT_BANDWIDTH_CONFIG, getBandwidthEstimate } from '../../network/bandwidth-estimator';\nimport type { SelectionRule, SelectionRuleDeps } from '../primitives/selection-rules';\nimport { applyConstraints, applyRules, excludeUnplayableTracks, sameCandidateSet } from '../primitives/selection-rules';\nimport { type ErrorEmitterState, emitError } from './collect-errors';\n\n// ============================================================================\n// State + Config\n// ============================================================================\n\n/**\n * The slots `setupTrackSwitching` itself owns: the `presentation` gate it reads\n * and the per-type `selected*TrackId` it writes. Rule-only inputs are\n * deliberately absent — `user*TrackSelection` and `bandwidthState` belong to\n * whoever materializes them (the embedder via `shareSignals`, the buffer-actor\n * sampler), and each rule declares the signal it consults as an optional slot\n * on its own deps map, so the behavior never assumes a rule's signal exists.\n */\nexport interface TrackSwitchingState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/**\n * Config for `switchVideoTrack` — the ABR tuning read by its ranker rule\n * (`rankByBandwidth`). `quality.safetyMargin` is the bandwidth-headroom\n * multiplier; `quality.upgradeMargin` the hysteresis ratio gating upgrades;\n * `bandwidth` tunes the estimator; `initialBandwidth` is the pre-sample\n * fallback. Defaults: `DEFAULT_QUALITY_CONFIG` (0.85 / 1.15),\n * `DEFAULT_BANDWIDTH_CONFIG`, `DEFAULT_INITIAL_BANDWIDTH` (5 Mbps).\n */\nexport interface SwitchVideoTrackConfig {\n quality?: Partial<QualityConfig>;\n bandwidth?: Partial<BandwidthConfig>;\n initialBandwidth?: number;\n /** Override CDN-id derivation (shared by the CDN scope + failover constraint). */\n getCdnId?: GetCdnId;\n /**\n * Codec capability probe read by the `excludeUnplayableTracks` hard\n * constraint — drops renditions this environment can't decode before\n * selection runs. Injected (rather than imported) so the DOM-free behavior\n * never reaches a DOM API directly; the engine defaults it to the\n * `MediaSource.isTypeSupported`-backed `canPlayTrack`. Absent → no codec\n * filtering (the constraint passes everything through).\n */\n canPlayTrack?: CanPlayTrack;\n}\n\n/** Default initial-bandwidth value before bandwidth measurements arrive. */\nexport const DEFAULT_INITIAL_BANDWIDTH = 5_000_000;\n\n// ============================================================================\n// Rule chain\n// ============================================================================\n\n// Re-exported so a consumer typing against this module's rules doesn't need a\n// second import; the definitions live in `../primitives/selection-rules` so the\n// simple `selectVideoTrack` variant can share them without pulling the ABR path\n// in with them. See that module's note.\nexport type { SelectionRule, SelectionRuleDeps } from '../primitives/selection-rules';\nexport { applyConstraints, applyRules, excludeUnplayableTracks } from '../primitives/selection-rules';\n\n// ============================================================================\n// Specialization helper\n//\n// `setupTrackSwitching` has the same shape as a Behavior `setup` function:\n// `({ state, config }) => Reactor`. Each `switchXTrack` export below calls\n// it from inside its own `defineBehavior` setup. Its generics — `S` (selection\n// slot key), `T` (candidate track type), `C` (the concrete config the variant\n// builds) — all infer from the passed `state` + `config`, so the variants need\n// no explicit type arguments. `C extends TrackSwitchingConfig<S, T>` lets the\n// variant's richer config (rule-specific fields included) flow through the\n// helper untouched; the rules read those fields off their own config views.\n//\n// -- Design note: why narrow `SelectionKey` / `UserSelectionKey` unions ----\n// Goal we did not reach: have callers \"fully pass in\" the slot keys, with\n// the helper enforcing zero internal knowledge of which literals are valid.\n// What blocks it: indexing a mapped-type intersection by a generic key.\n// When `S extends keyof TrackSwitchingState` (or `string`), TS conservatively\n// treats `state[selectionKey]` as the union of every possible match across\n// the intersected mapped portions — including the fixed-key signal\n// (`presentation`) — and widens to their value-type union.\n// The sibling pattern hits the same constraint and answers it the same way:\n// `SelectedTrackKey` in `select-tracks.ts` is a hardcoded narrow union for\n// the same reason.\n//\n// Current pick is the narrow-union route because it matches siblings and\n// the unions read as documentation (\"these are the slots this helper\n// manages\") rather than restriction. Extending to a new track-switching\n// axis is one literal per union.\n// --------------------------------------------------------------------------\n// ============================================================================\n\n/**\n * Minimum candidate-track shape consumed by the helper and its rules: an `id`\n * (the pick), a `url` (the active-CDN scope derives the CDN from it), an\n * optional `bandwidth` (the ranker's throughput sort), and optional\n * `width`/`height` (the ranker's equal-bitrate tie-break — absent on audio, so\n * audio candidates area-compare equal). Every resolved/partially-resolved video\n * track carries them all; audio tracks omit the dimensions.\n */\ntype SwitchableTrack = {\n id: string;\n url: string;\n bandwidth?: number;\n width?: number;\n height?: number;\n // Read by the capability constraint to probe codec support. Optional on the\n // minimal shape; every resolved/partially-resolved video & audio candidate\n // carries them, and an absent `mimeType` makes a track unprobeable (kept).\n mimeType?: string;\n codecs?: string[];\n};\n\n/**\n * Map the rule chain's surviving candidates to the final selection id, or\n * `undefined` for a deliberate no-selection. Defaults to the chain head\n * (`selectChainHead`) — the always-pick contract video and audio rely on\n * (`applyRules` guarantees a non-empty result, so the head is always there). A\n * variant whose selection is legitimately optional (text: opt-in captions,\n * explicit off) supplies its own picker that may return `undefined`; the helper\n * writes that straight through to the slot, clearing it.\n */\nexport type ResolveSelection<T extends SwitchableTrack, State = unknown, Context = unknown, Config = unknown> = (\n candidates: readonly T[],\n deps: SelectionRuleDeps<State, Context, Config>\n) => string | undefined;\n\ntype SelectionKey = 'selectedVideoTrackId' | 'selectedAudioTrackId' | 'selectedTextTrackId';\ntype UserSelectionKey = 'userVideoTrackSelection' | 'userAudioTrackSelection';\n\n// Each mapped value references `P` so TS keeps the per-key dependency and\n// resolves `state[selectionKey]` to the right arm. `T` (track type) stays out\n// of the state map (it flows through `TrackSwitchingConfig` instead).\n//\n// Only the behavior's own lifecycle signals are required here: the presentation\n// gate and the selection slot it writes. Signals a *rule* reads but the\n// behavior doesn't — `user*TrackSelection` (the user-selection filter) and\n// `bandwidthState` (the bandwidth ranker) — are NOT here; each rule declares\n// the signal it needs as *optional* on its own deps and reads it defensively,\n// so the behavior never assumes a rule-only signal exists. Those slots are\n// materialized by whoever owns them: `shareSignals` for the consumer-input\n// `user*TrackSelection`, the buffer-actor sampler for `bandwidthState`.\nexport type TrackSwitchingStateMap<S extends SelectionKey> = {\n presentation: ReadonlySignal<TrackSwitchingState['presentation']>;\n} & { [P in S]: Signal<TrackSwitchingState[P]> };\n\n/**\n * The lifecycle map plus the *optional* `errors` slot reporters append to. Owned\n * by `collectErrors`; optional so a composition without it still type-checks and\n * emission no-ops. The behavior reads no constraint's state — see\n * `noSupportedTrackCode`.\n */\ntype TrackSwitchingReporterStateMap<S extends SelectionKey> = TrackSwitchingStateMap<S> & ErrorEmitterState;\n\n/**\n * Config `setupTrackSwitching` itself reads — its own wiring: which selection\n * slot to write and clear (`selectionKey`), how to enumerate candidate tracks\n * (`getTracks`), the optional **hard-constraints pre-pass** (`constraints`,\n * applied before the chain to prune the unplayable), and the **rule chain** to\n * run (`rules`), and how to map the chain's survivors to the final pick\n * (`resolveSelection`, defaulting to the chain head). Rule-/constraint-specific\n * config is deliberately absent — each declares the fields it reads as *optional*\n * on its own config view (`UserSelectionConfig`, `BandwidthRankerConfig`), so the\n * behavior never enumerates them. The variant builds the concrete config as this\n * base plus whatever its chain consults; it flows through untouched as the `C`\n * type param on `setupTrackSwitching`.\n */\ninterface TrackSwitchingConfig<S extends SelectionKey, T extends SwitchableTrack> {\n selectionKey: S;\n getTracks: (presentation: MaybeResolvedPresentation) => readonly T[];\n constraints?: readonly SelectionRule<T, TrackSwitchingStateMap<S>, AnySlotMap, TrackSwitchingConfig<S, T>>[];\n rules: readonly SelectionRule<T, TrackSwitchingStateMap<S>, AnySlotMap, TrackSwitchingConfig<S, T>>[];\n /**\n * Map the chain's surviving candidates to the final selection id. Optional —\n * absent means the chain head (`selectChainHead`), the always-pick path video\n * and audio use. A variant with optional selection (text) supplies one that\n * may return `undefined`.\n */\n resolveSelection?: ResolveSelection<T, TrackSwitchingStateMap<S>, AnySlotMap, TrackSwitchingConfig<S, T>>;\n /**\n * SVTA code to report when this type *has* tracks but the constraints pruned\n * every one. Per-variant because the condition isn't universally an error:\n * video and audio supply {@link SVTA_NO_SUPPORTED_VIDEO_TRACK} /\n * {@link SVTA_NO_SUPPORTED_AUDIO_TRACK}, while text supplies none — an\n * unavailable subtitle track is a legitimate outcome, not a playback failure.\n * Absent → the selection still clears, nothing is reported.\n */\n noSupportedTrackCode?: number;\n}\n\n/**\n * State the user-selection filter reads: the lifecycle map plus an *optional*\n * user-selection slot (keyed by `U`), holding a partial-track description to\n * match against the candidates (`Partial<T>` — `{ id }`, `{ language }`,\n * `{ height }`, …). The slot exists only when the composition provides it\n * (materialized by `shareSignals`); the filter reads it defensively and no-ops\n * when it's absent (no user override).\n */\ntype UserSelectionStateMap<\n S extends SelectionKey,\n U extends UserSelectionKey,\n T extends SwitchableTrack,\n> = TrackSwitchingStateMap<S> & {\n [P in U]?: ReadonlySignal<Partial<T> | undefined>;\n};\n\n/**\n * Config the user-selection filter reads: `userSelectionKey` names the state\n * slot holding the user's selection. *Optional* on the rule's view — the base\n * config doesn't carry it, so an unwired key means \"no user selection\" and the\n * filter passes through. The variants always supply it.\n */\ntype UserSelectionConfig<\n S extends SelectionKey,\n U extends UserSelectionKey,\n T extends SwitchableTrack,\n> = TrackSwitchingConfig<S, T> & { userSelectionKey?: U };\n\n/**\n * State the bandwidth ranker reads: the lifecycle map plus an *optional*\n * `bandwidthState`. The signal exists only when the composition includes a\n * bandwidth sampler; the ranker reads it defensively and falls back to\n * `initialBandwidth` (with a debug note) when it's absent.\n */\ntype BandwidthRankerStateMap<S extends SelectionKey> = TrackSwitchingStateMap<S> & {\n bandwidthState?: ReadonlySignal<BandwidthState | undefined>;\n};\n\n/**\n * Config the bandwidth ranker reads: the ABR tuning in `SwitchVideoTrackConfig`\n * (`quality` / `bandwidth` / `initialBandwidth`), all optional with defaults.\n */\ntype BandwidthRankerConfig<S extends SelectionKey, T extends SwitchableTrack> = TrackSwitchingConfig<S, T> &\n SwitchVideoTrackConfig;\n\n/**\n * State the player-resolution cap reads: TrackSwitchingStateMap plus the\n * *optional* player measurement, manifested by `trackPlayerResolution`.\n */\ntype PlayerResolutionCapStateMap<S extends SelectionKey> = TrackSwitchingStateMap<S> & {\n playerResolution?: ReadonlySignal<Resolution | undefined>;\n};\n\n/**\n * State the active-CDN scope reads: the lifecycle map plus an *optional*\n * `cdnPriority` — the manifest-ordered CDN list (most-preferred first). The\n * signal exists only when the composition includes `deriveCdnPriority` (which\n * materializes + owns it); the scope reads it defensively and passes through\n * when it's absent (no CDN preference).\n */\ntype CdnScopeStateMap<S extends SelectionKey> = TrackSwitchingStateMap<S> & {\n cdnPriority?: ReadonlySignal<string[] | undefined>;\n};\n\n/**\n * State the failed-CDN constraint reads: the lifecycle map plus an *optional*\n * `failedCdns` — the CDN ids currently in failover cooldown. The signal exists\n * only when the composition includes a failover monitor (or an external driver); the\n * constraint reads it defensively and excludes nothing when it's absent.\n */\ntype CdnConstraintStateMap<S extends SelectionKey> = TrackSwitchingStateMap<S> & {\n failedCdns?: ReadonlySignal<string[] | undefined>;\n};\n\n/**\n * Config the CDN rules read: the base config plus an *optional* `getCdnId`\n * override. Both `excludeFailedCdns` and `preferActiveCdn` derive a track's CDN\n * from its URL; the override must be the *same* one `deriveCdnPriority` and the\n * failover trip use, or the keys stop matching. Optional → defaults to the\n * origin-based `getCdnId`, so the base config (without it) stays assignable.\n */\ntype CdnRuleConfig<S extends SelectionKey, T extends SwitchableTrack> = TrackSwitchingConfig<S, T> & {\n getCdnId?: GetCdnId;\n};\n\ntype VideoTrackCandidate = PartiallyResolvedVideoTrack | VideoTrack;\ntype AudioTrackCandidate = PartiallyResolvedAudioTrack | AudioTrack;\ntype TextTrackCandidate = PartiallyResolvedTextTrack | TextTrack;\n\n// ----------------------------------------------------------------------------\n// Rules — defined outside the behavior closure, parameterized only by their\n// deps. Each is generic over the slot keys + track type; the variant's\n// concrete keys instantiate it where the chain is assembled.\n// ----------------------------------------------------------------------------\n\n/**\n * User intent — a soft filter. Narrows to tracks matching the partial-track\n * selection in `user*TrackSelection`; an empty match falls through (the\n * composer skips it) to the unfiltered set — e.g. a stale id from a previous\n * source.\n */\nfunction filterByUserSelection<S extends SelectionKey, U extends UserSelectionKey, T extends SwitchableTrack>(\n tracks: readonly T[],\n { state, config }: SelectionRuleDeps<UserSelectionStateMap<S, U, T>, AnySlotMap, UserSelectionConfig<S, U, T>>\n): readonly T[] {\n const key = config.userSelectionKey;\n if (!key) return tracks;\n const filter = state[key]?.get();\n return filter ? tracks.filter((track) => matchesPartialTrack(track, filter)) : tracks;\n}\n\n/**\n * Player-resolution cap — a soft filter, video only. Narrows to the renditions\n * worth delivering at the player element's rendered size, so a small embed\n * doesn't pull segments nobody can perceive. The tighter sibling of\n * `screenResolutionCap`: the element's box, not the screen behind it.\n *\n * The cap is the *smallest tier that still covers the player*, and everything at\n * or below it survives — not \"everything at or below the player's area,\" which\n * under-serves a player falling between two tiers. Take an 800×450 player\n * against a 360p/720p/1080p ladder: only 360p is below it, so capping at the\n * player's area would hold an 800-px-wide box to a 640-px-wide picture. The\n * honest answer is the tier above, 720p, with 360p left in for the ranker.\n * `smallestCoveringPixelArea` picks that cap; `tracksUnderPixelArea` — the same\n * filter `screenResolutionCap` narrows with — applies it.\n *\n * Renditions declaring no width or height compare as area `0` and are never capped\n * out — they can't be judged against the player, and dropping them could strand\n * a source whose renditions all omit it.\n *\n * Runs *after* `preferActiveCdn`, so it narrows within the host already chosen.\n * Ahead of it, a cap that pruned every rendition of the preferred CDN would leave\n * the scope to fall to the next one with survivors — a size preference silently\n * moving playback to another host. Redundant streams normally mirror the same\n * ladder, which makes that a nonstandard-but-legal mismatch across CDNs rather\n * than an everyday case; the ordering costs nothing either way.\n *\n * Reading `state.playerResolution` through its signal is what subscribes the\n * chain to resizes; `undefined` — no signal composed, or nothing to measure —\n * means \"don't cap\" rather than a cap of zero, so the chain proceeds unnarrowed.\n */\nfunction playerResolutionCap<S extends SelectionKey, T extends SwitchableTrack>(\n tracks: readonly T[],\n { state }: SelectionRuleDeps<PlayerResolutionCapStateMap<S>, AnySlotMap, TrackSwitchingConfig<S, T>>\n): readonly T[] {\n const playerResolution = state.playerResolution?.get();\n if (!playerResolution) return tracks;\n\n // A player larger than every rendition has no covering tier, so the cap is\n // `undefined` and the filter's unbounded default narrows nothing.\n const cap = smallestCoveringPixelArea(tracks, playerResolution.width * playerResolution.height);\n\n return tracksUnderPixelArea(tracks, cap);\n}\n\n/**\n * Failed-CDN constraint — a *hard* filter (constraints pre-pass), shared by\n * video and audio. Removes tracks served from a CDN currently in failover\n * cooldown (`failedCdns`, written by the failover monitor). Removed tracks are never\n * attempted; the scope then narrows to the next surviving CDN in `cdnPriority`,\n * and snaps back to the primary once it leaves cooldown.\n *\n * Passes everything through when there's no `failedCdns` signal/value. When it\n * prunes *every* track (all CDNs cooled down), the empty result is preserved\n * (per `applyConstraints`) — \"nothing playable,\" which clears the selection (no\n * pick); a later CDN recovery refills the candidate set and re-picks.\n */\nfunction excludeFailedCdns<S extends SelectionKey, T extends SwitchableTrack>(\n tracks: readonly T[],\n { state, config }: SelectionRuleDeps<CdnConstraintStateMap<S>, AnySlotMap, CdnRuleConfig<S, T>>\n): readonly T[] {\n const failed = state.failedCdns?.get();\n if (!failed?.length) return tracks;\n const getCdnId = config.getCdnId ?? defaultGetCdnId;\n const failedSet = new Set(failed);\n return tracks.filter((track) => !failedSet.has(getCdnId(track.url)));\n}\n\n/**\n * Active-CDN scope — a soft filter, shared by video and audio. Narrows to the\n * highest-priority CDN in `cdnPriority` (owned by `deriveCdnPriority`) that\n * still has tracks, so every track type stays on one CDN. A redundant-streams\n * source lists the same renditions on multiple hosts; this keeps the pick on one\n * host rather than letting the ranker drift across them.\n *\n * \"Active\" is derived, not stored: constraints run before the rule chain, so a\n * failed CDN's tracks are already pruned by the time this runs — \"first CDN with\n * survivors\" *is* the active CDN, and it falls through to the next on failover\n * (and snaps back to the primary when it recovers). Content steering reorders\n * `cdnPriority`; this rule just honors the order.\n *\n * Soft-filter semantics: passes through when there's no `cdnPriority` signal/value\n * (no preference) or when nothing matches (`applyRules` skips an empty result).\n * Non-redundant sources have one CDN, so the narrow is a no-op.\n *\n * The CDN-id derivation defaults to origin-based `getCdnId`, overridable via the\n * `getCdnId` config — it must match the one `deriveCdnPriority` used to build\n * `cdnPriority`, or no track's CDN would ever equal an entry.\n */\nfunction preferActiveCdn<S extends SelectionKey, T extends SwitchableTrack>(\n tracks: readonly T[],\n { state, config }: SelectionRuleDeps<CdnScopeStateMap<S>, AnySlotMap, CdnRuleConfig<S, T>>\n): readonly T[] {\n const cdnPriority = state.cdnPriority?.get();\n if (!cdnPriority?.length) return tracks;\n const getCdnId = config.getCdnId ?? defaultGetCdnId;\n for (const cdn of cdnPriority) {\n const tracksUsingCdn = tracks.filter((track) => getCdnId(track.url) === cdn);\n if (tracksUsingCdn.length) return tracksUsingCdn;\n }\n return tracks;\n}\n\n/**\n * Bandwidth ranking — the terminal sort, shared by video and audio. Orders by\n * the throughput estimate: tracks within the bandwidth threshold first\n * (fitting), highest bitrate first; then over-threshold tracks, least-over\n * first. The head is the best-quality track that fits, falling back to the\n * smallest over-throughput track when nothing fits.\n *\n * Hysteresis without temporal state: the current track's effective bitrate is\n * boosted by `upgradeMargin` in the fitting sort, so a higher track only\n * outranks it once it clears `current.bitrate * upgradeMargin` (no flapping on\n * marginal bandwidth gains). Downgrades fall out for free — a current track\n * over the threshold isn't in the fitting set to be boosted, so the best fit (a\n * downgrade) wins immediately. Equal-bitrate tracks break by resolution (higher\n * `width × height` first), so an equal-bitrate ladder never picks a lower-\n * quality rendition by manifest order; audio tracks carry no dimensions, so\n * they area-compare equal and a stable sort keeps their candidate order (e.g.\n * same-bitrate language variants). Early-bail skips this rule when a prior one\n * narrowed to a single track, so the estimate is neither read nor subscribed\n * while that holds.\n */\nfunction rankByBandwidth<S extends SelectionKey, T extends SwitchableTrack>(\n tracks: readonly T[],\n { state, config }: SelectionRuleDeps<BandwidthRankerStateMap<S>, AnySlotMap, BandwidthRankerConfig<S, T>>\n): readonly T[] {\n const safetyMargin = config.quality?.safetyMargin ?? DEFAULT_QUALITY_CONFIG.safetyMargin;\n const upgradeMargin = config.quality?.upgradeMargin ?? DEFAULT_QUALITY_CONFIG.upgradeMargin;\n const initialBandwidth = config.initialBandwidth ?? DEFAULT_INITIAL_BANDWIDTH;\n const bandwidthConfig: BandwidthConfig = { ...DEFAULT_BANDWIDTH_CONFIG, ...config.bandwidth };\n if (!state.bandwidthState) {\n console.debug(\n '[track-switching] rankByBandwidth: no bandwidthState signal in composition; ranking on initialBandwidth'\n );\n }\n const threshold = getBandwidthEstimate(state.bandwidthState?.get(), initialBandwidth, bandwidthConfig) * safetyMargin;\n const currentId = state[config.selectionKey].get();\n const bitrate = (track: T) => track.bandwidth ?? 0;\n // Boost the current track's sort weight by upgradeMargin (fitting set only) so\n // an upgrade must clear current.bitrate * upgradeMargin to outrank it.\n const rank = (track: T) => (track.id === currentId ? bitrate(track) * upgradeMargin : bitrate(track));\n // Equal bitrate → prefer higher resolution (width × height), so an\n // equal-bitrate ladder doesn't pick a lower-quality rendition by manifest\n // order. Audio tracks carry no dimensions, so they area-compare equal and\n // keep candidate order (stable sort).\n const fitting = tracks\n .filter((track) => bitrate(track) <= threshold)\n .sort((a, b) => rank(b) - rank(a) || resolutionArea(b) - resolutionArea(a));\n const over = tracks\n .filter((track) => bitrate(track) > threshold)\n .sort((a, b) => bitrate(a) - bitrate(b) || resolutionArea(b) - resolutionArea(a));\n return [...fitting, ...over];\n}\n\n/**\n * Default final pick: the chain head. `applyRules` never narrows to nothing and\n * early-bails to a single survivor, so video and audio always converge to a\n * track and the head is the pick.\n */\nfunction selectChainHead<T extends SwitchableTrack>(candidates: readonly T[]): string {\n return candidates[0]!.id;\n}\n\n/**\n * State the text terminal reads: the lifecycle map plus an *optional*\n * `userTextTrackSelection` — the standing user intent. `Partial<TextTrack>` is an\n * explicit pick (language-based), `'off'` is explicit no-captions, `undefined` is\n * auto (no preference). The slot exists only when the composition materializes it\n * (`shareSignals`); the terminal reads it defensively and treats absence as auto.\n *\n * Unlike `user*TrackSelection` for video/audio, this carries the `'off'` sentinel\n * and feeds the terminal pick (not the shared `filterByUserSelection`) — text is\n * the only type whose selection is legitimately optional, so the off/auto logic\n * lives in one text-specific place rather than widening the shared filter.\n */\ntype TextSelectionStateMap = TrackSwitchingStateMap<'selectedTextTrackId'> & {\n userTextTrackSelection?: ReadonlySignal<Partial<TextTrack> | 'off' | undefined>;\n};\n\n/** Config the text terminal reads: the base config plus the opt-in default policy. */\ntype TextTerminalConfig = TrackSwitchingConfig<'selectedTextTrackId', TextTrackCandidate> & TextSelectionConfig;\n\n/**\n * Terminal pick for text — the `resolveSelection` the text variant supplies.\n * Resolves the standing `userTextTrackSelection` intent against the chain's\n * survivors (already CDN-failover-pruned and active-CDN-scoped):\n *\n * - `'off'` → no selection (clear the slot). Sticky through re-evaluation, so a\n * live refresh or failover re-run can't re-assert a default.\n * - explicit `Partial<TextTrack>` → narrow to the match (language-based). A\n * stale pick whose match is gone (e.g. the language dropped on a source\n * change) falls through to the default policy.\n * - auto (`undefined`) → the opt-in default policy (`preferredSubtitleLanguage`\n * → `DEFAULT=YES + AUTOSELECT=YES` → none), via `pickTextTrackFromTracks`.\n *\n * Returning `undefined` is a real outcome (captions are opt-in), which is why the\n * text variant relies on `setupTrackSwitching`'s no-selection seam.\n */\nfunction pickResolvedTextTrack<T extends TextTrackCandidate>(\n candidates: readonly T[],\n { state, config }: SelectionRuleDeps<TextSelectionStateMap, AnySlotMap, TextTerminalConfig>\n): string | undefined {\n const intent = state.userTextTrackSelection?.get();\n if (intent === 'off') return undefined;\n if (intent) {\n // The stored intent is a `Partial<TextTrack>`; cast to the candidate's own\n // partial shape so the generic `matchesPartialTrack` accepts it (every field\n // it carries — language, forced — exists on the candidate too).\n const matched = candidates.filter((track) => matchesPartialTrack(track, intent as Partial<T>));\n if (matched.length) return matched[0]!.id;\n }\n return pickTextTrackFromTracks(candidates, config);\n}\n\n// `context` is the composition's context map, threaded in by each variant's\n// rest-spread and typed as the generic slot-map shape (`AnySlotMap`). It can't\n// be a typed param on the `defineBehavior` setup without widening `ContextMap`\n// to its constraint and forcing the slot required, so the variants forward it\n// untyped via the rest and it lands here — absent on direct setup calls, and\n// passed straight through to the rules (which don't read it yet).\nexport function setupTrackSwitching<\n S extends SelectionKey,\n T extends SwitchableTrack,\n C extends TrackSwitchingConfig<S, T>,\n>(deps: { state: TrackSwitchingReporterStateMap<S>; context?: AnySlotMap; config: C }) {\n const { state, config } = deps;\n const { selectionKey, getTracks, rules, resolveSelection = selectChainHead, noSupportedTrackCode } = config;\n\n const derivedStateSignal = computed(() =>\n isResolvedPresentation(state.presentation.get())\n ? ('presentation-resolved' as const)\n : ('presentation-unresolved' as const)\n );\n\n // The playable candidate set — the tracks the rule chain gets to pick from,\n // derived *outside* the reaction. The hard-constraints pre-pass (capability\n // probing, CDN-failover cooldown) narrows the type's tracks before the chain\n // runs. Because this is a `computed`, a constraint's own signal reads (e.g.\n // `cdnHealth`) are tracked here, so when the playable set changes — a new\n // source, or a *dynamic* constraint like a CDN entering cooldown — the effect\n // re-picks. With no constraints configured this is just the type's tracks\n // while a presentation is resolved.\n //\n // The `equals` gates notification on the *set of track ids*, not array\n // identity: a live playlist refresh swaps in a new presentation object with\n // the same variant tracks, and a constraint's inputs can churn without\n // changing which tracks survive. In both cases the playable set is unchanged,\n // so the reaction must not re-fire (the rule chain still re-runs on its own\n // inputs — bandwidth, user selection). Same intent as `equalsById`, for the\n // track list.\n const candidateSet = computed<readonly T[]>(\n () => {\n const presentation = state.presentation.get();\n if (!isResolvedPresentation(presentation)) return [];\n return applyConstraints(config.constraints ?? [], getTracks(presentation), deps);\n },\n { equals: sameCandidateSet }\n );\n\n return createMachineReactor({\n initial: 'presentation-unresolved',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'presentation-unresolved': {},\n 'presentation-resolved': {\n // Canonical cleanup-binds-to-setup: the selection signal's valid\n // lifespan is exactly 'presentation-resolved'. Clear fires on exit,\n // covering both src unload and behavior destroy.\n entry: () => () => state[selectionKey].set(undefined),\n effects: [\n () => {\n // Reactive read: subscribes the reaction to the candidate set, so a\n // new presentation — or a constraint pruning it — re-fires this and\n // re-picks.\n const tracks = candidateSet.get();\n\n // Empty candidate set — two shapes, told apart by whether the type\n // has any tracks at all:\n // - The type has no tracks (e.g. a video-only source's absent\n // audio): legitimate, nothing to pick or clear.\n // - The type HAS tracks but the hard-constraints pre-pass pruned\n // every one (every rendition undecodable, or every CDN in\n // failover cooldown): no playable rendition. Clear the selection\n // so a pick made earlier — e.g. under the initial mp4 label,\n // before resolve-track relabeled the type to a non-fMP4\n // container — can't linger as a now-unplayable selection and\n // silently stall the pipeline.\n if (!tracks.length) {\n const presentation = peek(state.presentation);\n const hasTracksOfType = isResolvedPresentation(presentation) && getTracks(presentation).length > 0;\n if (hasTracksOfType) {\n // Reported generically: *why* the set emptied is the constraints'\n // business, not this behavior's, so no constraint's state is read\n // here and no cause is distinguished. Whatever the reason, a type\n // that has renditions but none selectable can't play — a codec\n // this environment can't decode, or every CDN failed, which is\n // itself fatal-or-nearly so. Finer causes (container vs codec)\n // would need `CanPlayTrack` to report a reason; see\n // `internal/design/spf/features/errors.md`.\n if (noSupportedTrackCode !== undefined) {\n emitError(state, { code: noSupportedTrackCode, data: { selectionKey } });\n }\n state[selectionKey].set(undefined);\n }\n return;\n }\n\n // The whole deps object passes straight through to every rule in the\n // variant-supplied chain (state + config from the behavior; context\n // threaded in by the variant's rest-spread). Typed against the base\n // config — each rule re-declares the extra fields it reads as\n // optional, and the concrete `C` is assignable to the base.\n const candidates = applyRules<T, TrackSwitchingStateMap<S>, AnySlotMap, TrackSwitchingConfig<S, T>>(\n rules,\n tracks,\n deps\n );\n\n // applyRules early-bails to a single survivor and never narrows to\n // nothing (a soft filter that would empty the set falls through), so\n // the pick is just the head. An empty result means a rule misbehaved\n // — applyRules is supposed to account for those cases, so surface it.\n if (!candidates.length) {\n console.error('[track-switching] applyRules returned no candidates');\n return;\n }\n // Map survivors to the final id. Defaults to the chain head; a\n // variant with optional selection (text) may resolve to `undefined`,\n // which clears the slot (e.g. explicit off, opt-in decline).\n // No change-guard needed: the slot uses default (Object.is) equality,\n // so re-setting the same value is a no-op (no notify, no re-fire).\n state[selectionKey].set(resolveSelection(candidates, deps));\n },\n ],\n },\n },\n });\n}\n\n// ============================================================================\n// Variant: switchVideoTrack — bandwidth-driven ABR\n// ============================================================================\n\n/**\n * Manage `selectedVideoTrackId`: pick a default on src load, dynamically\n * adjust based on bandwidth, clear on src unload. Honors\n * `userVideoTrackSelection` as a partial-track constraint on candidates;\n * short-circuits ABR when the constraint narrows to a single track.\n *\n * @example\n * const reactor = switchVideoTrack.setup({ state });\n */\nexport const switchVideoTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedVideoTrackId'],\n contextKeys: [],\n setup: ({\n state,\n config,\n ...otherProps\n }: {\n state: TrackSwitchingStateMap<'selectedVideoTrackId'>;\n config?: SwitchVideoTrackConfig;\n }) =>\n setupTrackSwitching({\n ...otherProps,\n state,\n config: {\n ...config,\n selectionKey: 'selectedVideoTrackId',\n userSelectionKey: 'userVideoTrackSelection',\n getTracks: (presentation) => getTracksByType(presentation, 'video') as readonly VideoTrackCandidate[],\n constraints: [excludeFailedCdns, excludeUnplayableTracks],\n rules: [filterByUserSelection, preferActiveCdn, playerResolutionCap, rankByBandwidth],\n noSupportedTrackCode: SVTA_NO_SUPPORTED_VIDEO_TRACK,\n },\n }),\n});\n\n// ============================================================================\n// Variant: switchAudioTrack — bandwidth-ranked (shared ranker)\n// ============================================================================\n\n/**\n * Manage `selectedAudioTrackId`: pick a default on src load, narrow by\n * `userAudioTrackSelection` filter, re-pick on filter change, clear on\n * src unload.\n *\n * Mid-stream flush on language switch is handled by the segment-loader's\n * `planTasks` (see `playback/actors/dom/segment-loader.ts`) — not this\n * behavior. Same split as the video pipeline: slot owner writes; loader\n * orchestrates segment + flush plans.\n *\n * @example\n * const reactor = switchAudioTrack.setup({ state });\n */\nexport const switchAudioTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedAudioTrackId'],\n contextKeys: [],\n setup: ({\n state,\n config,\n ...otherProps\n }: {\n state: TrackSwitchingStateMap<'selectedAudioTrackId'>;\n // Shares the video config shape so the engine config spreads through (CDN\n // derivation + any future cross-cutting fields).\n config?: SwitchVideoTrackConfig;\n }) =>\n setupTrackSwitching({\n ...otherProps,\n state,\n config: {\n // Spread engine config so cross-cutting fields (`getCdnId`, future shared\n // tuning) flow through like they do for video, then override the per-type\n // wiring. Video-only ABR tuning (`quality`/`bandwidth`/`initialBandwidth`)\n // rides along into the shared `rankByBandwidth` too; harmless since audio\n // has no `bandwidthState` to act on it and the ranker always yields a pick.\n // FOLLOW-UP: a shared config type for the genuinely cross-cutting fields\n // would keep video-only tuning out of audio entirely (CJP).\n ...config,\n selectionKey: 'selectedAudioTrackId',\n userSelectionKey: 'userAudioTrackSelection',\n getTracks: (presentation) => getTracksByType(presentation, 'audio') as readonly AudioTrackCandidate[],\n constraints: [excludeFailedCdns, excludeUnplayableTracks],\n rules: [filterByUserSelection, preferActiveCdn, rankByBandwidth],\n noSupportedTrackCode: SVTA_NO_SUPPORTED_AUDIO_TRACK,\n },\n }),\n});\n\n// ============================================================================\n// Variant: switchTextTrack — intent-resolved, optional selection\n// ============================================================================\n\n/**\n * Config for `switchTextTrack` — the opt-in default policy\n * (`preferredSubtitleLanguage` / `includeForcedTracks` / `enableDefaultTrack`,\n * via `TextSelectionConfig`) read by the terminal when the user has no standing\n * intent, plus the `getCdnId` override shared with the CDN constraint + scope.\n */\nexport interface SwitchTextTrackConfig extends TextSelectionConfig {\n /** Override CDN-id derivation (shared by the failed-CDN constraint + active-CDN scope). */\n getCdnId?: GetCdnId;\n}\n\n/**\n * Manage `selectedTextTrackId` as the single-writer **output** of standing user\n * intent (`userTextTrackSelection`) resolved against the playable, CDN-scoped\n * text renditions: clear on src unload; re-resolve when a CDN fails or recovers.\n *\n * Unlike video/audio, the selection is *optional* — captions are opt-in and the\n * user can turn them off — so the chain skips the bandwidth ranker and the shared\n * user-selection filter, and supplies a text-specific terminal\n * (`pickResolvedTextTrack`) that may resolve to no-selection via\n * `setupTrackSwitching`'s `resolveSelection` seam. Constraints are failed-CDN only\n * (`excludeUnplayableTracks`/`canPlayTrack` is MSE-based — the wrong probe for\n * text, whose playability is SPF-parser support); the active-CDN scope co-locates\n * captions with the surviving CDN on failover.\n *\n * @example\n * const reactor = switchTextTrack.setup({ state, config: { preferredSubtitleLanguage: 'en' } });\n */\nexport const switchTextTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedTextTrackId'],\n contextKeys: [],\n setup: ({\n state,\n config,\n ...otherProps\n }: {\n state: TrackSwitchingStateMap<'selectedTextTrackId'>;\n config?: SwitchTextTrackConfig;\n }) =>\n // Explicit type args pin the candidate type to `TextTrackCandidate`. Video and\n // audio let it infer to the `SwitchableTrack` constraint (harmless — every\n // rule is assignable up to it), but the text terminal needs the narrower type\n // (it reads text-only fields), so it's named here rather than inferred.\n setupTrackSwitching<'selectedTextTrackId', TextTrackCandidate, TextTerminalConfig & SwitchTextTrackConfig>({\n ...otherProps,\n state,\n config: {\n ...config,\n selectionKey: 'selectedTextTrackId',\n getTracks: (presentation) => getTracksByType(presentation, 'text') as readonly TextTrackCandidate[],\n constraints: [excludeFailedCdns],\n rules: [preferActiveCdn],\n resolveSelection: pickResolvedTextTrack,\n },\n }),\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"track-switching.js","names":["getCdnId","defaultGetCdnId"],"sources":["../../../../src/playback/behaviors/track-switching.ts"],"sourcesContent":["/**\n * **Per-type track selection as a rule chain.** While a presentation is resolved, owns that type's\n * `selected{Video,Audio,Text}TrackId` signal: pick a default, react to user intent and algorithmic ranking, and clear\n * it on src unload.\n *\n * Selection runs in two stages. First a **hard-constraints pre-pass** (`applyConstraints`) prunes the unplayable from\n * the candidate set — the failed-CDN constraint (`excludeFailedCdns`, failover cooldown), the capability constraint\n * (`excludeUnplayableTracks`, codec support), and the codec-family sticky constraint (`stickToSelectedCodecs`, no\n * `SourceBuffer.changeType()` — see its note). Then a small ordered chain of rules (`applyRules`) picks among the\n * survivors. Each constraint/rule reads the signals it needs at apply time, so the effect subscribes to exactly what\n * was consulted. The chain runs most authoritative first:\n *\n * 1. **user intent** — a soft filter on `user*TrackSelection`: narrow to the partial-track match; an empty match falls\n * through to the full set.\n * 2. **codec-family preference** — a soft filter on `preferredCodecs` (`preferCodecFamilies`, default AVC/AAC): on a\n * mixed-codec source, narrow which family the _initial_ pick — the one the sticky constraint then holds — lands in.\n * Behind user intent so an explicit initial pick may land anywhere; inert once the constraint narrows to a\n * non-preferred family.\n * 3. **active CDN** — a soft filter on `cdnPriority` (`preferActiveCdn`): narrow to the highest-priority CDN that still\n * has tracks; an empty match falls through. Shared by video and audio, so every type stays on one CDN\n * (`deriveCdnPriority` owns the list). No-op for non-redundant sources.\n * 4. **player resolution** — a soft filter on `playerResolution` (`playerResolutionCap`, video only): narrow to the\n * smallest rendition tier covering the player element, plus everything below it. No-op without a measurement. Ahead\n * of the ranker but behind the CDN scope, so the cap chooses _within_ a host rather than between hosts.\n * 5. **ranking** — the terminal sort: `rankByBandwidth`, shared by video and audio. Fitting tracks (within the throughput\n * threshold) first, highest bitrate first; over-throughput tracks after, least-over first. Hysteresis via boosting\n * the current track's sort weight by `upgradeMargin`.\n *\n * The composer's early-bail (one survivor → stop) is load-bearing: a user selection that narrows to a single track is\n * the pick without the ranker running, so the bandwidth estimate is never read and the effect doesn't re-fire on\n * bandwidth while that choice holds.\n *\n * Lifecycle: `'presentation-unresolved'` ↔ `'presentation-resolved'`. The resolved state owns the signal; its\n * entry-returned cleanup clears it on exit (canonical cleanup-binds-to-setup per `reactors.md`).\n *\n * The pick is the chain's result mapped to a slot value by `resolveSelection` (default: the head,\n * `applyRules(...)[0]`). Each variant supplies its **constraints + rule chain (+ optional resolveSelection)** via\n * config; `setupTrackSwitching` owns only the lifecycle and runs what it's given. Video and audio run constraints\n * `[excludeFailedCdns, excludeUnplayableTracks, stickToSelectedCodecs]` then rules `[filterByUserSelection,\n * preferCodecFamilies, preferActiveCdn, rankByBandwidth]` and take the head; video inserts `playerResolutionCap` after\n * the active-CDN scope, and `switchVideoTrack` also accepts ABR tuning config, `switchAudioTrack` takes none.\n * `switchTextTrack` differs — selection is _optional_ (captions are opt-in / off-able), so it runs\n * `[excludeFailedCdns]` + `[preferActiveCdn]` and supplies a text terminal (`pickResolvedTextTrack`) that resolves\n * standing user intent (`userTextTrackSelection`, incl. `'off'`) and may yield no selection. (The active-CDN _scope_ is\n * the sticky-pick half of multi-CDN; the failed-CDN _constraint_ is the failover half — prune the cooled-down CDN, the\n * scope falls to the next.)\n *\n * When the pre-pass prunes a type that _has_ tracks to empty, the behavior clears the selection (so a now-unplayable\n * pick can't linger and stall) and reports the type's `noSupportedTrackCode`. Which constraint emptied the set is\n * deliberately not consulted — the behavior reads no constraint's state, so the chain stays composable. A type with no\n * tracks at all is left alone; that's a legitimate source shape, not a failure. The late `createSourceBuffer` check\n * stays as the structural backstop.\n *\n * Deferred: audio's preferred-language / default-track selection as standing soft-filter rules (previously the\n * empty-slot picker, dropped in the move to the rule chain).\n */\n\nimport { type AnySlotMap, defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../core/signals/primitives';\nimport { DEFAULT_QUALITY_CONFIG, type QualityConfig, resolutionArea } from '../../media/abr/quality-selection';\nimport { SVTA_NO_SUPPORTED_AUDIO_TRACK, SVTA_NO_SUPPORTED_VIDEO_TRACK } from '../../media/errors';\nimport type { Resolution } from '../../media/primitives/resolution';\nimport {\n matchesPartialTrack,\n pickTextTrackFromTracks,\n smallestCoveringPixelArea,\n type TextSelectionConfig,\n tracksUnderPixelArea,\n} from '../../media/primitives/select-tracks';\nimport {\n type AudioTrack,\n type CanPlayTrack,\n isResolvedPresentation,\n type MaybeResolvedPresentation,\n type PartiallyResolvedAudioTrack,\n type PartiallyResolvedTextTrack,\n type PartiallyResolvedVideoTrack,\n type TextTrack,\n type VideoTrack,\n} from '../../media/types';\nimport { getCdnId as defaultGetCdnId, type GetCdnId } from '../../media/utils/cdn';\nimport { getCodecFamilies, getTracksByType } from '../../media/utils/tracks';\nimport type { BandwidthConfig, BandwidthState } from '../../network/bandwidth-estimator';\nimport { DEFAULT_BANDWIDTH_CONFIG, getBandwidthEstimate } from '../../network/bandwidth-estimator';\nimport type { SelectionRule, SelectionRuleDeps } from '../primitives/selection-rules';\nimport {\n applyConstraints,\n applyRules,\n excludeUnplayableTracks,\n preferCodecFamilies,\n sameCandidateSet,\n} from '../primitives/selection-rules';\nimport { type ErrorEmitterState, emitError } from './collect-errors';\n\n// ============================================================================\n// State + Config\n// ============================================================================\n\n/**\n * The slots `setupTrackSwitching` itself owns: the `presentation` gate it reads and the per-type `selected*TrackId` it\n * writes. Rule-only inputs are deliberately absent — `user*TrackSelection` and `bandwidthState` belong to whoever\n * materializes them (the embedder via `shareSignals`, the buffer-actor sampler), and each rule declares the signal it\n * consults as an optional slot on its own deps map, so the behavior never assumes a rule's signal exists.\n */\nexport interface TrackSwitchingState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/**\n * Config for `switchVideoTrack` — the ABR tuning read by its ranker rule (`rankByBandwidth`). `quality.safetyMargin` is\n * the bandwidth-headroom multiplier; `quality.upgradeMargin` the hysteresis ratio gating upgrades; `bandwidth` tunes\n * the estimator; `initialBandwidth` is the pre-sample fallback. Defaults: `DEFAULT_QUALITY_CONFIG` (0.85 / 1.15),\n * `DEFAULT_BANDWIDTH_CONFIG`, `DEFAULT_INITIAL_BANDWIDTH` (5 Mbps).\n */\nexport interface SwitchVideoTrackConfig {\n quality?: Partial<QualityConfig>;\n bandwidth?: Partial<BandwidthConfig>;\n initialBandwidth?: number;\n /** Override CDN-id derivation (shared by the CDN scope + failover constraint). */\n getCdnId?: GetCdnId;\n /**\n * Codec capability probe read by the `excludeUnplayableTracks` hard constraint — drops renditions this environment\n * can't decode before selection runs. Injected (rather than imported) so the DOM-free behavior never reaches a DOM\n * API directly; the engine defaults it to the `MediaSource.isTypeSupported`-backed `canPlayTrack`. Absent → no codec\n * filtering (the constraint passes everything through).\n */\n canPlayTrack?: CanPlayTrack;\n /**\n * Codec families (RFC 6381 4CCs, e.g. `'avc1'` / `'hvc1'` / `'mp4a'`) the initial pick prefers on a mixed-codec\n * source, read by the `preferCodecFamilies` scope. With no `SourceBuffer.changeType()`, the initial family is the one\n * `stickToSelectedCodecs` holds for the source's lifetime, so this decides which family that is. Cross-cutting like\n * `getCdnId` — one list serves video and audio (each type's rule matches only the families its tracks carry).\n * Defaults to `DEFAULT_PREFERRED_CODECS` (AVC + AAC); pass `[]` to disable.\n */\n preferredCodecs?: string[];\n}\n\n/** Default initial-bandwidth value before bandwidth measurements arrive. */\nexport const DEFAULT_INITIAL_BANDWIDTH = 5_000_000;\n\n// ============================================================================\n// Rule chain\n// ============================================================================\n\n// Re-exported so a consumer typing against this module's rules doesn't need a\n// second import; the definitions live in `../primitives/selection-rules` so the\n// simple `selectVideoTrack` variant can share them without pulling the ABR path\n// in with them. See that module's note.\nexport type { CodecPreferenceConfig, SelectionRule, SelectionRuleDeps } from '../primitives/selection-rules';\nexport {\n applyConstraints,\n applyRules,\n DEFAULT_PREFERRED_CODECS,\n excludeUnplayableTracks,\n preferCodecFamilies,\n} from '../primitives/selection-rules';\n\n// ============================================================================\n// Specialization helper\n//\n// `setupTrackSwitching` has the same shape as a Behavior `setup` function:\n// `({ state, config }) => Reactor`. Each `switchXTrack` export below calls\n// it from inside its own `defineBehavior` setup. Its generics — `S` (selection\n// slot key), `T` (candidate track type), `C` (the concrete config the variant\n// builds) — all infer from the passed `state` + `config`, so the variants need\n// no explicit type arguments. `C extends TrackSwitchingConfig<S, T>` lets the\n// variant's richer config (rule-specific fields included) flow through the\n// helper untouched; the rules read those fields off their own config views.\n//\n// -- Design note: why narrow `SelectionKey` / `UserSelectionKey` unions ----\n// Goal we did not reach: have callers \"fully pass in\" the slot keys, with\n// the helper enforcing zero internal knowledge of which literals are valid.\n// What blocks it: indexing a mapped-type intersection by a generic key.\n// When `S extends keyof TrackSwitchingState` (or `string`), TS conservatively\n// treats `state[selectionKey]` as the union of every possible match across\n// the intersected mapped portions — including the fixed-key signal\n// (`presentation`) — and widens to their value-type union.\n// The sibling pattern hits the same constraint and answers it the same way:\n// `SelectedTrackKey` in `select-tracks.ts` is a hardcoded narrow union for\n// the same reason.\n//\n// Current pick is the narrow-union route because it matches siblings and\n// the unions read as documentation (\"these are the slots this helper\n// manages\") rather than restriction. Extending to a new track-switching\n// axis is one literal per union.\n// --------------------------------------------------------------------------\n// ============================================================================\n\n/**\n * Minimum candidate-track shape consumed by the helper and its rules: an `id` (the pick), a `url` (the active-CDN scope\n * derives the CDN from it), an optional `bandwidth` (the ranker's throughput sort), and optional `width`/`height` (the\n * ranker's equal-bitrate tie-break — absent on audio, so audio candidates area-compare equal). Every\n * resolved/partially-resolved video track carries them all; audio tracks omit the dimensions.\n */\ntype SwitchableTrack = {\n id: string;\n url: string;\n bandwidth?: number;\n width?: number;\n height?: number;\n // Read by the capability constraint to probe codec support. Optional on the\n // minimal shape; every resolved/partially-resolved video & audio candidate\n // carries them, and an absent `mimeType` makes a track unprobeable (kept).\n mimeType?: string;\n codecs?: string[];\n};\n\n/**\n * Map the rule chain's surviving candidates to the final selection id, or `undefined` for a deliberate no-selection.\n * Defaults to the chain head (`selectChainHead`) — the always-pick contract video and audio rely on (`applyRules`\n * guarantees a non-empty result, so the head is always there). A variant whose selection is legitimately optional\n * (text: opt-in captions, explicit off) supplies its own picker that may return `undefined`; the helper writes that\n * straight through to the slot, clearing it.\n */\nexport type ResolveSelection<T extends SwitchableTrack, State = unknown, Context = unknown, Config = unknown> = (\n candidates: readonly T[],\n deps: SelectionRuleDeps<State, Context, Config>\n) => string | undefined;\n\ntype SelectionKey = 'selectedVideoTrackId' | 'selectedAudioTrackId' | 'selectedTextTrackId';\ntype UserSelectionKey = 'userVideoTrackSelection' | 'userAudioTrackSelection';\n\n// Each mapped value references `P` so TS keeps the per-key dependency and\n// resolves `state[selectionKey]` to the right arm. `T` (track type) stays out\n// of the state map (it flows through `TrackSwitchingConfig` instead).\n//\n// Only the behavior's own lifecycle signals are required here: the presentation\n// gate and the selection slot it writes. Signals a *rule* reads but the\n// behavior doesn't — `user*TrackSelection` (the user-selection filter) and\n// `bandwidthState` (the bandwidth ranker) — are NOT here; each rule declares\n// the signal it needs as *optional* on its own deps and reads it defensively,\n// so the behavior never assumes a rule-only signal exists. Those slots are\n// materialized by whoever owns them: `shareSignals` for the consumer-input\n// `user*TrackSelection`, the buffer-actor sampler for `bandwidthState`.\nexport type TrackSwitchingStateMap<S extends SelectionKey> = {\n presentation: ReadonlySignal<TrackSwitchingState['presentation']>;\n} & { [P in S]: Signal<TrackSwitchingState[P]> };\n\n/**\n * The lifecycle map plus the _optional_ `errors` slot reporters append to. Owned by `collectErrors`; optional so a\n * composition without it still type-checks and emission no-ops. The behavior reads no constraint's state — see\n * `noSupportedTrackCode`.\n */\ntype TrackSwitchingReporterStateMap<S extends SelectionKey> = TrackSwitchingStateMap<S> & ErrorEmitterState;\n\n/**\n * Config `setupTrackSwitching` itself reads — its own wiring: which selection slot to write and clear (`selectionKey`),\n * how to enumerate candidate tracks (`getTracks`), the optional **hard-constraints pre-pass** (`constraints`, applied\n * before the chain to prune the unplayable), and the **rule chain** to run (`rules`), and how to map the chain's\n * survivors to the final pick (`resolveSelection`, defaulting to the chain head). Rule-/constraint-specific config is\n * deliberately absent — each declares the fields it reads as _optional_ on its own config view (`UserSelectionConfig`,\n * `BandwidthRankerConfig`), so the behavior never enumerates them. The variant builds the concrete config as this base\n * plus whatever its chain consults; it flows through untouched as the `C` type param on `setupTrackSwitching`.\n */\ninterface TrackSwitchingConfig<S extends SelectionKey, T extends SwitchableTrack> {\n selectionKey: S;\n getTracks: (presentation: MaybeResolvedPresentation) => readonly T[];\n constraints?: readonly SelectionRule<T, TrackSwitchingStateMap<S>, AnySlotMap, TrackSwitchingConfig<S, T>>[];\n rules: readonly SelectionRule<T, TrackSwitchingStateMap<S>, AnySlotMap, TrackSwitchingConfig<S, T>>[];\n /**\n * Map the chain's surviving candidates to the final selection id. Optional — absent means the chain head\n * (`selectChainHead`), the always-pick path video and audio use. A variant with optional selection (text) supplies\n * one that may return `undefined`.\n */\n resolveSelection?: ResolveSelection<T, TrackSwitchingStateMap<S>, AnySlotMap, TrackSwitchingConfig<S, T>>;\n /**\n * SVTA code to report when this type _has_ tracks but the constraints pruned every one. Per-variant because the\n * condition isn't universally an error: video and audio supply {@link SVTA_NO_SUPPORTED_VIDEO_TRACK} /\n * {@link SVTA_NO_SUPPORTED_AUDIO_TRACK}, while text supplies none — an unavailable subtitle track is a legitimate\n * outcome, not a playback failure. Absent → the selection still clears, nothing is reported.\n */\n noSupportedTrackCode?: number;\n}\n\n/**\n * State the user-selection filter reads: the lifecycle map plus an _optional_ user-selection slot (keyed by `U`),\n * holding a partial-track description to match against the candidates (`Partial<T>` — `{ id }`, `{ language }`, `{\n * height }`, …). The slot exists only when the composition provides it (materialized by `shareSignals`); the filter\n * reads it defensively and no-ops when it's absent (no user override).\n */\ntype UserSelectionStateMap<\n S extends SelectionKey,\n U extends UserSelectionKey,\n T extends SwitchableTrack,\n> = TrackSwitchingStateMap<S> & {\n [P in U]?: ReadonlySignal<Partial<T> | undefined>;\n};\n\n/**\n * Config the user-selection filter reads: `userSelectionKey` names the state slot holding the user's selection.\n * _Optional_ on the rule's view — the base config doesn't carry it, so an unwired key means \"no user selection\" and the\n * filter passes through. The variants always supply it.\n */\ntype UserSelectionConfig<\n S extends SelectionKey,\n U extends UserSelectionKey,\n T extends SwitchableTrack,\n> = TrackSwitchingConfig<S, T> & { userSelectionKey?: U };\n\n/**\n * State the bandwidth ranker reads: the lifecycle map plus an _optional_ `bandwidthState`. The signal exists only when\n * the composition includes a bandwidth sampler; the ranker reads it defensively and falls back to `initialBandwidth`\n * (with a debug note) when it's absent.\n */\ntype BandwidthRankerStateMap<S extends SelectionKey> = TrackSwitchingStateMap<S> & {\n bandwidthState?: ReadonlySignal<BandwidthState | undefined>;\n};\n\n/**\n * Config the bandwidth ranker reads: the ABR tuning in `SwitchVideoTrackConfig` (`quality` / `bandwidth` /\n * `initialBandwidth`), all optional with defaults.\n */\ntype BandwidthRankerConfig<S extends SelectionKey, T extends SwitchableTrack> = TrackSwitchingConfig<S, T> &\n SwitchVideoTrackConfig;\n\n/**\n * State the player-resolution cap reads: TrackSwitchingStateMap plus the _optional_ player measurement, manifested by\n * `trackPlayerResolution`.\n */\ntype PlayerResolutionCapStateMap<S extends SelectionKey> = TrackSwitchingStateMap<S> & {\n playerResolution?: ReadonlySignal<Resolution | undefined>;\n};\n\n/**\n * State the active-CDN scope reads: the lifecycle map plus an _optional_ `cdnPriority` — the manifest-ordered CDN list\n * (most-preferred first). The signal exists only when the composition includes `deriveCdnPriority` (which materializes\n * + owns it); the scope reads it defensively and passes through when it's absent (no CDN preference).\n */\ntype CdnScopeStateMap<S extends SelectionKey> = TrackSwitchingStateMap<S> & {\n cdnPriority?: ReadonlySignal<string[] | undefined>;\n};\n\n/**\n * State the failed-CDN constraint reads: the lifecycle map plus an _optional_ `failedCdns` — the CDN ids currently in\n * failover cooldown. The signal exists only when the composition includes a failover monitor (or an external driver);\n * the constraint reads it defensively and excludes nothing when it's absent.\n */\ntype CdnConstraintStateMap<S extends SelectionKey> = TrackSwitchingStateMap<S> & {\n failedCdns?: ReadonlySignal<string[] | undefined>;\n};\n\n/**\n * Config the CDN rules read: the base config plus an _optional_ `getCdnId` override. Both `excludeFailedCdns` and\n * `preferActiveCdn` derive a track's CDN from its URL; the override must be the _same_ one `deriveCdnPriority` and the\n * failover trip use, or the keys stop matching. Optional → defaults to the origin-based `getCdnId`, so the base config\n * (without it) stays assignable.\n */\ntype CdnRuleConfig<S extends SelectionKey, T extends SwitchableTrack> = TrackSwitchingConfig<S, T> & {\n getCdnId?: GetCdnId;\n};\n\ntype VideoTrackCandidate = PartiallyResolvedVideoTrack | VideoTrack;\ntype AudioTrackCandidate = PartiallyResolvedAudioTrack | AudioTrack;\ntype TextTrackCandidate = PartiallyResolvedTextTrack | TextTrack;\n\n// ----------------------------------------------------------------------------\n// Rules — defined outside the behavior closure, parameterized only by their\n// deps. Each is generic over the slot keys + track type; the variant's\n// concrete keys instantiate it where the chain is assembled.\n// ----------------------------------------------------------------------------\n\n/**\n * User intent — a soft filter. Narrows to tracks matching the partial-track selection in `user*TrackSelection`; an\n * empty match falls through (the composer skips it) to the unfiltered set — e.g. a stale id from a previous source.\n */\nfunction filterByUserSelection<S extends SelectionKey, U extends UserSelectionKey, T extends SwitchableTrack>(\n tracks: readonly T[],\n { state, config }: SelectionRuleDeps<UserSelectionStateMap<S, U, T>, AnySlotMap, UserSelectionConfig<S, U, T>>\n): readonly T[] {\n const key = config.userSelectionKey;\n if (!key) return tracks;\n\n const filter = state[key]?.get();\n\n return filter ? tracks.filter((track) => matchesPartialTrack(track, filter)) : tracks;\n}\n\n/**\n * Player-resolution cap — a soft filter, video only. Narrows to the renditions worth delivering at the player element's\n * rendered size, so a small embed doesn't pull segments nobody can perceive. The tighter sibling of\n * `screenResolutionCap`: the element's box, not the screen behind it.\n *\n * The cap is the _smallest tier that still covers the player_, and everything at or below it survives — not \"everything\n * at or below the player's area,\" which under-serves a player falling between two tiers. Take an 800×450 player against\n * a 360p/720p/1080p ladder: only 360p is below it, so capping at the player's area would hold an 800-px-wide box to a\n * 640-px-wide picture. The honest answer is the tier above, 720p, with 360p left in for the ranker.\n * `smallestCoveringPixelArea` picks that cap; `tracksUnderPixelArea` — the same filter `screenResolutionCap` narrows\n * with — applies it.\n *\n * Renditions declaring no width or height compare as area `0` and are never capped out — they can't be judged against\n * the player, and dropping them could strand a source whose renditions all omit it.\n *\n * Runs _after_ `preferActiveCdn`, so it narrows within the host already chosen. Ahead of it, a cap that pruned every\n * rendition of the preferred CDN would leave the scope to fall to the next one with survivors — a size preference\n * silently moving playback to another host. Redundant streams normally mirror the same ladder, which makes that a\n * nonstandard-but-legal mismatch across CDNs rather than an everyday case; the ordering costs nothing either way.\n *\n * Reading `state.playerResolution` through its signal is what subscribes the chain to resizes; `undefined` — no signal\n * composed, or nothing to measure — means \"don't cap\" rather than a cap of zero, so the chain proceeds unnarrowed.\n */\nfunction playerResolutionCap<S extends SelectionKey, T extends SwitchableTrack>(\n tracks: readonly T[],\n { state }: SelectionRuleDeps<PlayerResolutionCapStateMap<S>, AnySlotMap, TrackSwitchingConfig<S, T>>\n): readonly T[] {\n const playerResolution = state.playerResolution?.get();\n if (!playerResolution) return tracks;\n\n // A player larger than every rendition has no covering tier, so the cap is\n // `undefined` and the filter's unbounded default narrows nothing.\n const cap = smallestCoveringPixelArea(tracks, playerResolution.width * playerResolution.height);\n\n return tracksUnderPixelArea(tracks, cap);\n}\n\n/**\n * Failed-CDN constraint — a _hard_ filter (constraints pre-pass), shared by video and audio. Removes tracks served from\n * a CDN currently in failover cooldown (`failedCdns`, written by the failover monitor). Removed tracks are never\n * attempted; the scope then narrows to the next surviving CDN in `cdnPriority`, and snaps back to the primary once it\n * leaves cooldown.\n *\n * Passes everything through when there's no `failedCdns` signal/value. When it prunes _every_ track (all CDNs cooled\n * down), the empty result is preserved (per `applyConstraints`) — \"nothing playable,\" which clears the selection (no\n * pick); a later CDN recovery refills the candidate set and re-picks.\n */\nfunction excludeFailedCdns<S extends SelectionKey, T extends SwitchableTrack>(\n tracks: readonly T[],\n { state, config }: SelectionRuleDeps<CdnConstraintStateMap<S>, AnySlotMap, CdnRuleConfig<S, T>>\n): readonly T[] {\n const failed = state.failedCdns?.get();\n if (!failed?.length) return tracks;\n\n const getCdnId = config.getCdnId ?? defaultGetCdnId;\n const failedSet = new Set(failed);\n\n return tracks.filter((track) => !failedSet.has(getCdnId(track.url)));\n}\n\n/**\n * Active-CDN scope — a soft filter, shared by video and audio. Narrows to the highest-priority CDN in `cdnPriority`\n * (owned by `deriveCdnPriority`) that still has tracks, so every track type stays on one CDN. A redundant-streams\n * source lists the same renditions on multiple hosts; this keeps the pick on one host rather than letting the ranker\n * drift across them.\n *\n * \"Active\" is derived, not stored: constraints run before the rule chain, so a failed CDN's tracks are already pruned\n * by the time this runs — \"first CDN with survivors\" _is_ the active CDN, and it falls through to the next on failover\n * (and snaps back to the primary when it recovers). Content steering reorders `cdnPriority`; this rule just honors the\n * order.\n *\n * Soft-filter semantics: passes through when there's no `cdnPriority` signal/value (no preference) or when nothing\n * matches (`applyRules` skips an empty result). Non-redundant sources have one CDN, so the narrow is a no-op.\n *\n * The CDN-id derivation defaults to origin-based `getCdnId`, overridable via the `getCdnId` config — it must match the\n * one `deriveCdnPriority` used to build `cdnPriority`, or no track's CDN would ever equal an entry.\n */\nfunction preferActiveCdn<S extends SelectionKey, T extends SwitchableTrack>(\n tracks: readonly T[],\n { state, config }: SelectionRuleDeps<CdnScopeStateMap<S>, AnySlotMap, CdnRuleConfig<S, T>>\n): readonly T[] {\n const cdnPriority = state.cdnPriority?.get();\n if (!cdnPriority?.length) return tracks;\n\n const getCdnId = config.getCdnId ?? defaultGetCdnId;\n\n for (const cdn of cdnPriority) {\n const tracksUsingCdn = tracks.filter((track) => getCdnId(track.url) === cdn);\n if (tracksUsingCdn.length) return tracksUsingCdn;\n }\n\n return tracks;\n}\n\n/**\n * Codec-family sticky constraint — a _hard_ filter for the constraints pre-pass, shared by video and audio, and the\n * reason the re-evaluating variants can run ABR over a mixed-codec source at all: SPF implements no\n * `SourceBuffer.changeType()`, so once segments of the selected track's codec families are what a buffer was created\n * for, a pick outside them could never play — constraint semantics (\"can't play here\"), not preference. Removes the\n * candidates whose codec-family set doesn't _equal_ the selected track's (set-equality so a muxed `hvc1,mp4a` rendition\n * can't pass as a match for `avc1,mp4a` on its audio half). Purely relational: the lock _is_ the current selection's\n * families, no state of its own. Pruned pre-pass, a cross-family user selection mid-stream never reaches the user\n * filter, which falls through unhonored instead of killing playback. Before any selection exists (the initial pick) it\n * removes nothing, which is what leaves the initial family choice to the rule chain (user intent, then\n * `preferCodecFamilies`).\n *\n * The selected track's families come from the _presentation's_ track list, not the already-pruned candidates: a CDN\n * entering cooldown may prune the current track itself while same-family renditions survive on another host, and the\n * lock must carry over to them. Removes nothing when: no selection yet, a selection the presentation no longer carries,\n * or a selected track without codecs (then no candidate's compatibility is decidable — same inertness as\n * `preferCodecFamilies` on a codec-less ladder).\n *\n * When the selected family genuinely vanishes from the playable set, the pre-pass empties: the behavior reports the\n * type's no-supported-track code and clears the selection — an explicable stop instead of a cross-family append\n * surfacing as an opaque decode error. Clearing also releases the lock (it keys on the live selection), so a following\n * pick may land in the surviving family on freshly-created buffer actors; whether the append layer survives that\n * rebuild is the changeType gap, out of this constraint's hands. If `changeType` support ever lands, this constraint is\n * what relaxes.\n *\n * Reading the selection here — a slot the picking effect itself writes, from inside the candidate-set computed — is\n * safe only because the effect scheduler revalidates an effect's sources after each run; see `core/signals/effect.ts`\n * (`revalidateSources`) for the lost-wakeup it prevents.\n *\n * Composed only into the re-evaluating `switch*` variants: the pinned `selectVideoTrack` (background compositions)\n * evaluates once and never re-picks, so it's immune by construction.\n */\nexport function stickToSelectedCodecs<S extends SelectionKey, T extends SwitchableTrack>(\n tracks: readonly T[],\n { state, config }: SelectionRuleDeps<TrackSwitchingStateMap<S>, AnySlotMap, TrackSwitchingConfig<S, T>>\n): readonly T[] {\n const currentId = state[config.selectionKey].get();\n if (!currentId) return tracks;\n\n const presentation = state.presentation.get();\n if (!isResolvedPresentation(presentation)) return tracks;\n\n const current = config.getTracks(presentation).find((track) => track.id === currentId);\n const families = current && getCodecFamilies(current);\n if (!families) return tracks;\n\n return tracks.filter((track) => {\n const candidateFamilies = getCodecFamilies(track);\n\n return (\n !!candidateFamilies &&\n candidateFamilies.length === families.length &&\n candidateFamilies.every((family) => families.includes(family))\n );\n });\n}\n\n/**\n * Bandwidth ranking — the terminal sort, shared by video and audio. Orders by the throughput estimate: tracks within\n * the bandwidth threshold first (fitting), highest bitrate first; then over-threshold tracks, least-over first. The\n * head is the best-quality track that fits, falling back to the smallest over-throughput track when nothing fits.\n *\n * Hysteresis without temporal state: the current track's effective bitrate is boosted by `upgradeMargin` in the fitting\n * sort, so a higher track only outranks it once it clears `current.bitrate * upgradeMargin` (no flapping on marginal\n * bandwidth gains). Downgrades fall out for free — a current track over the threshold isn't in the fitting set to be\n * boosted, so the best fit (a downgrade) wins immediately. Equal-bitrate tracks break by resolution (higher `width ×\n * height` first), so an equal-bitrate ladder never picks a lower- quality rendition by manifest order; audio tracks\n * carry no dimensions, so they area-compare equal and a stable sort keeps their candidate order (e.g. same-bitrate\n * language variants). Early-bail skips this rule when a prior one narrowed to a single track, so the estimate is\n * neither read nor subscribed while that holds.\n */\nfunction rankByBandwidth<S extends SelectionKey, T extends SwitchableTrack>(\n tracks: readonly T[],\n { state, config }: SelectionRuleDeps<BandwidthRankerStateMap<S>, AnySlotMap, BandwidthRankerConfig<S, T>>\n): readonly T[] {\n const safetyMargin = config.quality?.safetyMargin ?? DEFAULT_QUALITY_CONFIG.safetyMargin;\n const upgradeMargin = config.quality?.upgradeMargin ?? DEFAULT_QUALITY_CONFIG.upgradeMargin;\n const initialBandwidth = config.initialBandwidth ?? DEFAULT_INITIAL_BANDWIDTH;\n const bandwidthConfig: BandwidthConfig = { ...DEFAULT_BANDWIDTH_CONFIG, ...config.bandwidth };\n\n if (!state.bandwidthState) {\n console.debug(\n '[track-switching] rankByBandwidth: no bandwidthState signal in composition; ranking on initialBandwidth'\n );\n }\n\n const threshold = getBandwidthEstimate(state.bandwidthState?.get(), initialBandwidth, bandwidthConfig) * safetyMargin;\n const currentId = state[config.selectionKey].get();\n const bitrate = (track: T) => track.bandwidth ?? 0;\n // Boost the current track's sort weight by upgradeMargin (fitting set only) so\n // an upgrade must clear current.bitrate * upgradeMargin to outrank it.\n const rank = (track: T) => (track.id === currentId ? bitrate(track) * upgradeMargin : bitrate(track));\n // Equal bitrate → prefer higher resolution (width × height), so an\n // equal-bitrate ladder doesn't pick a lower-quality rendition by manifest\n // order. Audio tracks carry no dimensions, so they area-compare equal and\n // keep candidate order (stable sort).\n const fitting = tracks\n .filter((track) => bitrate(track) <= threshold)\n .sort((a, b) => rank(b) - rank(a) || resolutionArea(b) - resolutionArea(a));\n const over = tracks\n .filter((track) => bitrate(track) > threshold)\n .sort((a, b) => bitrate(a) - bitrate(b) || resolutionArea(b) - resolutionArea(a));\n\n return [...fitting, ...over];\n}\n\n/**\n * Default final pick: the chain head. `applyRules` never narrows to nothing and early-bails to a single survivor, so\n * video and audio always converge to a track and the head is the pick.\n */\nfunction selectChainHead<T extends SwitchableTrack>(candidates: readonly T[]): string {\n return candidates[0]!.id;\n}\n\n/**\n * State the text terminal reads: the lifecycle map plus an _optional_ `userTextTrackSelection` — the standing user\n * intent. `Partial<TextTrack>` is an explicit pick (language-based), `'off'` is explicit no-captions, `undefined` is\n * auto (no preference). The slot exists only when the composition materializes it (`shareSignals`); the terminal reads\n * it defensively and treats absence as auto.\n *\n * Unlike `user*TrackSelection` for video/audio, this carries the `'off'` sentinel and feeds the terminal pick (not the\n * shared `filterByUserSelection`) — text is the only type whose selection is legitimately optional, so the off/auto\n * logic lives in one text-specific place rather than widening the shared filter.\n */\ntype TextSelectionStateMap = TrackSwitchingStateMap<'selectedTextTrackId'> & {\n userTextTrackSelection?: ReadonlySignal<Partial<TextTrack> | 'off' | undefined>;\n};\n\n/** Config the text terminal reads: the base config plus the opt-in default policy. */\ntype TextTerminalConfig = TrackSwitchingConfig<'selectedTextTrackId', TextTrackCandidate> & TextSelectionConfig;\n\n/**\n * Terminal pick for text — the `resolveSelection` the text variant supplies. Resolves the standing\n * `userTextTrackSelection` intent against the chain's survivors (already CDN-failover-pruned and active-CDN-scoped):\n *\n * - `'off'` → no selection (clear the slot). Sticky through re-evaluation, so a live refresh or failover re-run can't\n * re-assert a default.\n * - Explicit `Partial<TextTrack>` → narrow to the match (language-based). A stale pick whose match is gone (e.g. the\n * language dropped on a source change) falls through to the default policy.\n * - Auto (`undefined`) → the opt-in default policy (`preferredSubtitleLanguage` → `DEFAULT=YES + AUTOSELECT=YES` → none),\n * via `pickTextTrackFromTracks`.\n *\n * Returning `undefined` is a real outcome (captions are opt-in), which is why the text variant relies on\n * `setupTrackSwitching`'s no-selection seam.\n */\nfunction pickResolvedTextTrack<T extends TextTrackCandidate>(\n candidates: readonly T[],\n { state, config }: SelectionRuleDeps<TextSelectionStateMap, AnySlotMap, TextTerminalConfig>\n): string | undefined {\n const intent = state.userTextTrackSelection?.get();\n if (intent === 'off') return undefined;\n\n if (intent) {\n // The stored intent is a `Partial<TextTrack>`; cast to the candidate's own\n // partial shape so the generic `matchesPartialTrack` accepts it (every field\n // it carries — language, forced — exists on the candidate too).\n const matched = candidates.filter((track) => matchesPartialTrack(track, intent as Partial<T>));\n if (matched.length) return matched[0]!.id;\n }\n\n return pickTextTrackFromTracks(candidates, config);\n}\n\n// `context` is the composition's context map, threaded in by each variant's\n// rest-spread and typed as the generic slot-map shape (`AnySlotMap`). It can't\n// be a typed param on the `defineBehavior` setup without widening `ContextMap`\n// to its constraint and forcing the slot required, so the variants forward it\n// untyped via the rest and it lands here — absent on direct setup calls, and\n// passed straight through to the rules (which don't read it yet).\nexport function setupTrackSwitching<\n S extends SelectionKey,\n T extends SwitchableTrack,\n C extends TrackSwitchingConfig<S, T>,\n>(deps: { state: TrackSwitchingReporterStateMap<S>; context?: AnySlotMap; config: C }) {\n const { state, config } = deps;\n const { selectionKey, getTracks, rules, resolveSelection = selectChainHead, noSupportedTrackCode } = config;\n\n const derivedStateSignal = computed(() =>\n isResolvedPresentation(state.presentation.get())\n ? ('presentation-resolved' as const)\n : ('presentation-unresolved' as const)\n );\n\n // The playable candidate set — the tracks the rule chain gets to pick from,\n // derived *outside* the reaction. The hard-constraints pre-pass (capability\n // probing, CDN-failover cooldown) narrows the type's tracks before the chain\n // runs. Because this is a `computed`, a constraint's own signal reads (e.g.\n // `cdnHealth`) are tracked here, so when the playable set changes — a new\n // source, or a *dynamic* constraint like a CDN entering cooldown — the effect\n // re-picks. With no constraints configured this is just the type's tracks\n // while a presentation is resolved.\n //\n // The `equals` gates notification on the *set of track ids*, not array\n // identity: a live playlist refresh swaps in a new presentation object with\n // the same variant tracks, and a constraint's inputs can churn without\n // changing which tracks survive. In both cases the playable set is unchanged,\n // so the reaction must not re-fire (the rule chain still re-runs on its own\n // inputs — bandwidth, user selection). Same intent as `equalsById`, for the\n // track list.\n const candidateSet = computed<readonly T[]>(\n () => {\n const presentation = state.presentation.get();\n if (!isResolvedPresentation(presentation)) return [];\n\n return applyConstraints(config.constraints ?? [], getTracks(presentation), deps);\n },\n { equals: sameCandidateSet }\n );\n\n return createMachineReactor({\n initial: 'presentation-unresolved',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'presentation-unresolved': {},\n 'presentation-resolved': {\n // Canonical cleanup-binds-to-setup: the selection signal's valid\n // lifespan is exactly 'presentation-resolved'. Clear fires on exit,\n // covering both src unload and behavior destroy.\n entry: () => () => state[selectionKey].set(undefined),\n effects: [\n () => {\n // Reactive read: subscribes the reaction to the candidate set, so a\n // new presentation — or a constraint pruning it — re-fires this and\n // re-picks.\n const tracks = candidateSet.get();\n\n // Empty candidate set — two shapes, told apart by whether the type\n // has any tracks at all:\n // - The type has no tracks (e.g. a video-only source's absent\n // audio): legitimate, nothing to pick or clear.\n // - The type HAS tracks but the hard-constraints pre-pass pruned\n // every one (every rendition undecodable, or every CDN in\n // failover cooldown): no playable rendition. Clear the selection\n // so a pick made earlier — e.g. under the initial mp4 label,\n // before resolve-track relabeled the type to a non-fMP4\n // container — can't linger as a now-unplayable selection and\n // silently stall the pipeline.\n if (!tracks.length) {\n const presentation = peek(state.presentation);\n const hasTracksOfType = isResolvedPresentation(presentation) && getTracks(presentation).length > 0;\n\n if (hasTracksOfType) {\n // Reported generically: *why* the set emptied is the constraints'\n // business, not this behavior's, so no constraint's state is read\n // here and no cause is distinguished. Whatever the reason, a type\n // that has renditions but none selectable can't play — a codec\n // this environment can't decode, or every CDN failed, which is\n // itself fatal-or-nearly so. Finer causes (container vs codec)\n // would need `CanPlayTrack` to report a reason; see\n // `internal/design/spf/features/errors.md`.\n if (noSupportedTrackCode !== undefined) {\n emitError(state, { code: noSupportedTrackCode, data: { selectionKey } });\n }\n\n state[selectionKey].set(undefined);\n }\n\n return;\n }\n\n // The whole deps object passes straight through to every rule in the\n // variant-supplied chain (state + config from the behavior; context\n // threaded in by the variant's rest-spread). Typed against the base\n // config — each rule re-declares the extra fields it reads as\n // optional, and the concrete `C` is assignable to the base.\n const candidates = applyRules<T, TrackSwitchingStateMap<S>, AnySlotMap, TrackSwitchingConfig<S, T>>(\n rules,\n tracks,\n deps\n );\n\n // applyRules early-bails to a single survivor and never narrows to\n // nothing (a soft filter that would empty the set falls through), so\n // the pick is just the head. An empty result means a rule misbehaved\n // — applyRules is supposed to account for those cases, so surface it.\n if (!candidates.length) {\n console.error('[track-switching] applyRules returned no candidates');\n return;\n }\n\n // Map survivors to the final id. Defaults to the chain head; a\n // variant with optional selection (text) may resolve to `undefined`,\n // which clears the slot (e.g. explicit off, opt-in decline).\n // No change-guard needed: the slot uses default (Object.is) equality,\n // so re-setting the same value is a no-op (no notify, no re-fire).\n state[selectionKey].set(resolveSelection(candidates, deps));\n },\n ],\n },\n },\n });\n}\n\n// ============================================================================\n// Variant: switchVideoTrack — bandwidth-driven ABR\n// ============================================================================\n\n/**\n * Manage `selectedVideoTrackId`: pick a default on src load, dynamically adjust based on bandwidth, clear on src\n * unload. Honors `userVideoTrackSelection` as a partial-track constraint on candidates; short-circuits ABR when the\n * constraint narrows to a single track.\n *\n * @example\n * const reactor = switchVideoTrack.setup({ state });\n */\nexport const switchVideoTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedVideoTrackId'],\n contextKeys: [],\n setup: ({\n state,\n config,\n ...otherProps\n }: {\n state: TrackSwitchingStateMap<'selectedVideoTrackId'>;\n config?: SwitchVideoTrackConfig;\n }) =>\n setupTrackSwitching({\n ...otherProps,\n state,\n config: {\n ...config,\n selectionKey: 'selectedVideoTrackId',\n userSelectionKey: 'userVideoTrackSelection',\n getTracks: (presentation) => getTracksByType(presentation, 'video') as readonly VideoTrackCandidate[],\n constraints: [excludeFailedCdns, excludeUnplayableTracks, stickToSelectedCodecs],\n rules: [filterByUserSelection, preferCodecFamilies, preferActiveCdn, playerResolutionCap, rankByBandwidth],\n noSupportedTrackCode: SVTA_NO_SUPPORTED_VIDEO_TRACK,\n },\n }),\n});\n\n// ============================================================================\n// Variant: switchAudioTrack — bandwidth-ranked (shared ranker)\n// ============================================================================\n\n/**\n * Manage `selectedAudioTrackId`: pick a default on src load, narrow by `userAudioTrackSelection` filter, re-pick on\n * filter change, clear on src unload.\n *\n * Mid-stream flush on language switch is handled by the segment-loader's `planTasks` (see\n * `playback/actors/dom/segment-loader.ts`) — not this behavior. Same split as the video pipeline: slot owner writes;\n * loader orchestrates segment + flush plans.\n *\n * @example\n * const reactor = switchAudioTrack.setup({ state });\n */\nexport const switchAudioTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedAudioTrackId'],\n contextKeys: [],\n setup: ({\n state,\n config,\n ...otherProps\n }: {\n state: TrackSwitchingStateMap<'selectedAudioTrackId'>;\n // Shares the video config shape so the engine config spreads through (CDN\n // derivation + any future cross-cutting fields).\n config?: SwitchVideoTrackConfig;\n }) =>\n setupTrackSwitching({\n ...otherProps,\n state,\n config: {\n // Spread engine config so cross-cutting fields (`getCdnId`, future shared\n // tuning) flow through like they do for video, then override the per-type\n // wiring. Video-only ABR tuning (`quality`/`bandwidth`/`initialBandwidth`)\n // rides along into the shared `rankByBandwidth` too; harmless since audio\n // has no `bandwidthState` to act on it and the ranker always yields a pick.\n // FOLLOW-UP: a shared config type for the genuinely cross-cutting fields\n // would keep video-only tuning out of audio entirely (CJP).\n ...config,\n selectionKey: 'selectedAudioTrackId',\n userSelectionKey: 'userAudioTrackSelection',\n getTracks: (presentation) => getTracksByType(presentation, 'audio') as readonly AudioTrackCandidate[],\n constraints: [excludeFailedCdns, excludeUnplayableTracks, stickToSelectedCodecs],\n rules: [filterByUserSelection, preferCodecFamilies, preferActiveCdn, rankByBandwidth],\n noSupportedTrackCode: SVTA_NO_SUPPORTED_AUDIO_TRACK,\n },\n }),\n});\n\n// ============================================================================\n// Variant: switchTextTrack — intent-resolved, optional selection\n// ============================================================================\n\n/**\n * Config for `switchTextTrack` — the opt-in default policy (`preferredSubtitleLanguage` / `includeForcedTracks` /\n * `enableDefaultTrack`, via `TextSelectionConfig`) read by the terminal when the user has no standing intent, plus the\n * `getCdnId` override shared with the CDN constraint + scope.\n */\nexport interface SwitchTextTrackConfig extends TextSelectionConfig {\n /** Override CDN-id derivation (shared by the failed-CDN constraint + active-CDN scope). */\n getCdnId?: GetCdnId;\n}\n\n/**\n * Manage `selectedTextTrackId` as the single-writer **output** of standing user intent (`userTextTrackSelection`)\n * resolved against the playable, CDN-scoped text renditions: clear on src unload; re-resolve when a CDN fails or\n * recovers.\n *\n * Unlike video/audio, the selection is _optional_ — captions are opt-in and the user can turn them off — so the chain\n * skips the bandwidth ranker and the shared user-selection filter, and supplies a text-specific terminal\n * (`pickResolvedTextTrack`) that may resolve to no-selection via `setupTrackSwitching`'s `resolveSelection` seam.\n * Constraints are failed-CDN only (`excludeUnplayableTracks`/`canPlayTrack` is MSE-based — the wrong probe for text,\n * whose playability is SPF-parser support); the active-CDN scope co-locates captions with the surviving CDN on\n * failover.\n *\n * @example\n * const reactor = switchTextTrack.setup({ state, config: { preferredSubtitleLanguage: 'en' } });\n */\nexport const switchTextTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedTextTrackId'],\n contextKeys: [],\n setup: ({\n state,\n config,\n ...otherProps\n }: {\n state: TrackSwitchingStateMap<'selectedTextTrackId'>;\n config?: SwitchTextTrackConfig;\n }) =>\n // Explicit type args pin the candidate type to `TextTrackCandidate`. Video and\n // audio let it infer to the `SwitchableTrack` constraint (harmless — every\n // rule is assignable up to it), but the text terminal needs the narrower type\n // (it reads text-only fields), so it's named here rather than inferred.\n setupTrackSwitching<'selectedTextTrackId', TextTrackCandidate, TextTerminalConfig & SwitchTextTrackConfig>({\n ...otherProps,\n state,\n config: {\n ...config,\n selectionKey: 'selectedTextTrackId',\n getTracks: (presentation) => getTracksByType(presentation, 'text') as readonly TextTrackCandidate[],\n constraints: [excludeFailedCdns],\n rules: [preferActiveCdn],\n resolveSelection: pickResolvedTextTrack,\n },\n }),\n});\n"],"mappings":"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"}
|
|
@@ -4,12 +4,13 @@ import { loadAudioSegments } from "../../behaviors/dom/load-segments.js";
|
|
|
4
4
|
import { trackCurrentTime } from "../../behaviors/dom/track-current-time.js";
|
|
5
5
|
import { trackLoadTriggers } from "../../behaviors/dom/track-load-triggers.js";
|
|
6
6
|
import { deriveSharedMinStartMediaTime, establishStartMediaTime } from "../../behaviors/establish-start-media-time.js";
|
|
7
|
+
import { collectErrors } from "../../behaviors/collect-errors.js";
|
|
8
|
+
import { switchAudioTrack } from "../../behaviors/track-switching.js";
|
|
7
9
|
import { attachMediaSourceAsSourceElement } from "../../../media/dom/mse/mediasource-setup.js";
|
|
8
10
|
import { canPlayTrack } from "../../../media/dom/capabilities.js";
|
|
9
11
|
import { parseMultivariantPlaylist } from "../../../media/hls/parse-multivariant.js";
|
|
10
12
|
import { getResolvedSelectedTrackDuration } from "../../../media/utils/track-selection.js";
|
|
11
13
|
import { calculatePresentationDuration } from "../../behaviors/calculate-presentation-duration.js";
|
|
12
|
-
import { collectErrors } from "../../behaviors/collect-errors.js";
|
|
13
14
|
import { deriveCdnPriority } from "../../behaviors/derive-cdn-priority.js";
|
|
14
15
|
import { setupAirPlay } from "../../behaviors/dom/airplay.js";
|
|
15
16
|
import { applyStartPosition } from "../../behaviors/dom/apply-start-position.js";
|
|
@@ -22,7 +23,6 @@ import { resolvePresentation } from "../../behaviors/resolve-presentation.js";
|
|
|
22
23
|
import { resolveAudioTrack } from "../../behaviors/resolve-track.js";
|
|
23
24
|
import { setupFailoverMonitor } from "../../behaviors/setup-failover-monitor.js";
|
|
24
25
|
import { syncPreload } from "../../behaviors/sync-preload.js";
|
|
25
|
-
import { switchAudioTrack } from "../../behaviors/track-switching.js";
|
|
26
26
|
import { relocationPipelinesFor } from "../../primitives/relocation-pipelines.js";
|
|
27
27
|
import { reportUnsupportedTrackConditions } from "../../primitives/report-track-conditions.js";
|
|
28
28
|
//#region src/playback/engines/hls/engine-audio-only.ts
|
|
@@ -30,32 +30,28 @@ const shareSignals = makeShareSignals(["userAudioTrackSelection", "disableRemote
|
|
|
30
30
|
/**
|
|
31
31
|
* Create an audio-only HLS playback engine.
|
|
32
32
|
*
|
|
33
|
-
* Subtractive composition variant of `createHlsVideoEngine`: omits
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
36
|
-
*
|
|
37
|
-
* `setupTextTrackActors`, `loadTextTrackSegments`). The remaining audio
|
|
38
|
-
* pipeline composes unchanged.
|
|
33
|
+
* Subtractive composition variant of `createHlsVideoEngine`: omits video-side behaviors (`resolveVideoTrack`,
|
|
34
|
+
* `switchVideoTrack`, `setupVideoBufferActors`, `loadVideoSegments`) and text-track behaviors (`switchTextTrack`,
|
|
35
|
+
* `resolveTextTrack`, `syncTextTracks`, `setupTextTrackActors`, `loadTextTrackSegments`). The remaining audio pipeline
|
|
36
|
+
* composes unchanged.
|
|
39
37
|
*
|
|
40
|
-
* Handles both truly audio-only HLS sources (no video stream-inf) and
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* is encoded by adapter choice; this engine does not branch on source
|
|
44
|
-
* shape.
|
|
38
|
+
* Handles both truly audio-only HLS sources (no video stream-inf) and mixed-AV HLS sources where the audio rendition is
|
|
39
|
+
* selected and video / subtitle renditions are ignored at composition time. The variant decision is encoded by adapter
|
|
40
|
+
* choice; this engine does not branch on source shape.
|
|
45
41
|
*
|
|
46
42
|
* @example
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* let signals: HlsAudioEngineSignals;
|
|
45
|
+
* const engine = createHlsAudioEngine({
|
|
46
|
+
* preferredAudioLanguage: 'en',
|
|
47
|
+
* onSignalsReady: (refs) => {
|
|
48
|
+
* signals = refs;
|
|
49
|
+
* },
|
|
50
|
+
* });
|
|
55
51
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
52
|
+
* signals.context.mediaElement.set(audioEl);
|
|
53
|
+
* signals.state.presentation.set({ url: 'https://example.com/stream.m3u8' });
|
|
54
|
+
* ```;
|
|
59
55
|
*/
|
|
60
56
|
function createHlsAudioEngine(config = {}) {
|
|
61
57
|
const deriveStartMediaTime = config.deriveStartMediaTime ?? deriveSharedMinStartMediaTime;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine-audio-only.js","names":[],"sources":["../../../../../src/playback/engines/hls/engine-audio-only.ts"],"sourcesContent":["import {\n type Composition,\n type ContextSignals,\n createComposition,\n type StateSignals,\n} from '../../../core/composition/create-composition';\nimport { makeShareSignals, type ShareSignalsConfig } from '../../../core/composition/share-signals';\nimport type { BackBufferConfig } from '../../../media/buffer/back-buffer';\nimport type { ForwardBufferConfig } from '../../../media/buffer/forward-buffer';\nimport { canPlayTrack } from '../../../media/dom/capabilities';\nimport { attachMediaSourceAsSourceElement } from '../../../media/dom/mse/mediasource-setup';\nimport type { SvtaError } from '../../../media/errors';\nimport { parseMultivariantPlaylist } from '../../../media/hls/parse-multivariant';\nimport type { AudioTrack, CanPlayTrack, MaybeResolvedPresentation, MediaContainerData } from '../../../media/types';\nimport type { GetCdnId } from '../../../media/utils/cdn';\nimport { getResolvedSelectedTrackDuration } from '../../../media/utils/track-selection';\nimport type { SegmentLoaderActor } from '../../actors/dom/segment-loader';\nimport type { SourceBufferActor } from '../../actors/dom/source-buffer';\nimport {\n calculatePresentationDuration,\n type PresentationDurationResolver,\n} from '../../behaviors/calculate-presentation-duration';\nimport { collectErrors } from '../../behaviors/collect-errors';\nimport { deriveCdnPriority } from '../../behaviors/derive-cdn-priority';\nimport { setupAirPlay } from '../../behaviors/dom/airplay';\nimport { applyStartPosition } from '../../behaviors/dom/apply-start-position';\nimport { endOfStream } from '../../behaviors/dom/end-of-stream';\nimport { loadAudioSegments } from '../../behaviors/dom/load-segments';\nimport { recoverEndStall } from '../../behaviors/dom/recover-end-stall';\nimport { setupAudioBufferActors } from '../../behaviors/dom/setup-buffer-actors';\nimport { setupMediaSource } from '../../behaviors/dom/setup-mediasource';\nimport { trackCurrentTime } from '../../behaviors/dom/track-current-time';\nimport { trackLoadTriggers } from '../../behaviors/dom/track-load-triggers';\nimport { updateMediaSourceDuration } from '../../behaviors/dom/update-mediasource-duration';\n// Non-zero-PTS relocation (spike): remove this import, the composed reactor, the\n// `audioMessagePipelines` finalConfig entry, the `mediaContainerData` state slot,\n// and the `deriveStartMediaTime` config field to drop relocation from audio-only.\nimport {\n type DeriveStartMediaTime,\n deriveSharedMinStartMediaTime,\n establishStartMediaTime,\n} from '../../behaviors/establish-start-media-time';\nimport { type ParsePresentation, resolvePresentation } from '../../behaviors/resolve-presentation';\nimport { resolveAudioTrack } from '../../behaviors/resolve-track';\nimport { type FailoverMonitorConfig, setupFailoverMonitor } from '../../behaviors/setup-failover-monitor';\nimport { syncPreload } from '../../behaviors/sync-preload';\nimport { switchAudioTrack } from '../../behaviors/track-switching';\nimport { relocationPipelinesFor } from '../../primitives/relocation-pipelines';\nimport {\n type ReportUnsupportedTrackConditions,\n reportUnsupportedTrackConditions,\n} from '../../primitives/report-track-conditions';\n\n// ============================================================================\n// Audio-Only HLS Engine State & Context\n// ============================================================================\n\n/**\n * State shape for the audio-only HLS playback engine.\n *\n * Subset of `HlsVideoEngineState` covering only the slots written and read\n * by audio-side behaviors. Video and text-track slots are absent —\n * subtractive composition removes the behaviors that declare them.\n */\nexport interface HlsAudioEngineState {\n presentation?: MaybeResolvedPresentation;\n preload?: 'auto' | 'metadata' | 'none';\n selectedAudioTrackId?: string;\n // Non-zero-PTS relocation (spike): transient per-track container data owned by\n // `establishStartMediaTime`. Remove with the composed reactor.\n mediaContainerData?: Record<string, MediaContainerData>;\n /**\n * Consumer-driven constraint narrowing the audio candidate set. Sibling\n * of `userVideoTrackSelection` in the default engine. Partial-track\n * shape — `{ language: 'es' }`, `{ id: 'audio-en' }`, etc.\n * `selectAudioTrack` reads this and re-picks when it changes.\n * Multi-language-audio Tier 2 programmatic-write path.\n */\n userAudioTrackSelection?: Partial<AudioTrack>;\n /**\n * The CDNs the source is served from, in manifest priority order (mirrors\n * HLS content steering's `PATHWAY-PRIORITY`). Owned by `deriveCdnPriority`,\n * read by `track-switching`'s `preferActiveCdn` scope. Only meaningful for\n * redundant-stream sources; a single-CDN source has one entry.\n */\n cdnPriority?: string[];\n /**\n * CDN ids currently in failover cooldown — read by `track-switching`'s\n * `excludeFailedCdns` constraint, which prunes their tracks so the active-CDN\n * scope falls to the next CDN. Empty / absent means all CDNs are eligible.\n */\n failedCdns?: string[];\n /**\n * Conditions reported during playback, in order — appended by whichever\n * behavior detects one, owned and cleared per source by `collectErrors`.\n * Severity is decided above the engine. Audio-only makes an all-audio-pruned\n * source unrecoverable: there's no video fallback to fall back to.\n */\n errors?: SvtaError[];\n currentTime?: number;\n loadActivated?: boolean;\n /**\n * One-shot command: start the current source at this position\n * (presentation-timeline seconds). Written by consumers or by\n * `setupAirPlay`'s session-end snapshot; consumed (cleared) by\n * `applyStartPosition` once the element seeks. See\n * `behaviors/dom/apply-start-position.ts`.\n */\n startPosition?: number;\n /**\n * Intent-level loading policy: initiate no new loading work while `true`.\n * Written by `setupAirPlay` (the only behavior declaring the key) while a\n * remote-playback session owns presentation; observed by `loadAudioSegments`\n * (parks in `'dormant'`) and by `setupMediaSource` (a pending rebuild\n * waits). See `SegmentLoadingState['loadingSuspended']`.\n */\n loadingSuspended?: boolean;\n /**\n * Author intent for the AirPlay/remote-playback picker, written by the media\n * adapter's `disableRemotePlayback` IDL property. `true` is an explicit\n * opt-out: `setupAirPlay` reads it at attach and sets nothing up, leaving the\n * element's remote playback disabled. Distinct from the underlying media\n * element's own `disableRemotePlayback`, which stays programmatically managed\n * (ManagedMediaSource / AirPlay).\n */\n disableRemotePlayback?: boolean;\n}\n\n/**\n * Context shape for the audio-only HLS playback engine.\n *\n * Subset of `HlsVideoEngineContext` covering only the platform objects and\n * actor refs managed by audio-side behaviors.\n */\nexport interface HlsAudioEngineContext {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n audioBufferActor?: SourceBufferActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n}\n\nexport type HlsAudioEngineSignals = {\n state: StateSignals<HlsAudioEngineState>;\n context: ContextSignals<HlsAudioEngineContext>;\n};\n\n/**\n * Configuration for the audio-only HLS playback engine.\n *\n * Subset of `HlsVideoEngineConfig` — video-quality, bandwidth-estimator,\n * and text-track config fields are omitted (no behavior consumes them).\n */\nexport interface HlsAudioEngineConfig extends ShareSignalsConfig<HlsAudioEngineState, HlsAudioEngineContext> {\n preferredAudioLanguage?: string;\n /**\n * Codec capability probe read by `track-switching`'s `excludeUnplayableTracks`\n * constraint. Defaults to the `MediaSource.isTypeSupported`-backed\n * `canPlayTrack`; override to force-exclude a codec. Mirrors the default\n * engine — without it, capability probing (and TS / raw-AAC detection) would\n * be inert for audio-only playback.\n */\n canPlayTrack?: CanPlayTrack;\n /**\n * Conditions reported about each rendition as it resolves — the *causes* behind\n * a later verdict, and the copy a verdict reuses when they agree. Defaults to\n * {@link reportUnsupportedTrackConditions}, which reports non-fMP4 containers\n * and encryption; supply your own to report a different set (a provider that\n * never ships MPEG-TS can drop that check) or `() => []` to report nothing.\n */\n reportUnsupportedTrackConditions?: ReportUnsupportedTrackConditions;\n resolveDuration?: PresentationDurationResolver;\n parsePresentation?: ParsePresentation;\n forwardBuffer?: Partial<ForwardBufferConfig>;\n backBuffer?: Partial<BackBufferConfig>;\n /** Multi-CDN failover monitor tuning. Defaults: `DEFAULT_FAILOVER_MONITOR_CONFIG`. */\n failover?: Partial<FailoverMonitorConfig>;\n /**\n * Derive a CDN grouping key from a track URL (used by `cdnPriority`, the\n * failover trip, and the track-switching CDN rules — one function read by all).\n * Defaults to the URL origin; override to key on e.g. Mux's `cdn=` param.\n */\n getCdnId?: GetCdnId;\n /** Non-zero-PTS relocation (spike): the reduce seam (tier knob); defaults to per-track own. */\n deriveStartMediaTime?: DeriveStartMediaTime;\n}\n\n// ============================================================================\n// Audio-Only HLS Playback Engine\n// ============================================================================\n\n// Materializes input slots no composed behavior produces — `userAudioTrackSelection`\n// (switchAudioTrack only reads it) and `disableRemotePlayback` (setupAirPlay only\n// reads it) — in addition to forwarding refs. `failedCdns` is owned by\n// `setupFailoverMonitor`, so it's already materialized and reachable on the\n// `onSignalsReady` refs without being listed here.\nconst shareSignals = makeShareSignals<HlsAudioEngineState, HlsAudioEngineContext>([\n 'userAudioTrackSelection',\n 'disableRemotePlayback',\n]);\n\n/**\n * Create an audio-only HLS playback engine.\n *\n * Subtractive composition variant of `createHlsVideoEngine`: omits\n * video-side behaviors (`resolveVideoTrack`, `switchVideoTrack`,\n * `setupVideoBufferActors`, `loadVideoSegments`) and text-track behaviors\n * (`switchTextTrack`, `resolveTextTrack`, `syncTextTracks`,\n * `setupTextTrackActors`, `loadTextTrackSegments`). The remaining audio\n * pipeline composes unchanged.\n *\n * Handles both truly audio-only HLS sources (no video stream-inf) and\n * mixed-AV HLS sources where the audio rendition is selected and video /\n * subtitle renditions are ignored at composition time. The variant decision\n * is encoded by adapter choice; this engine does not branch on source\n * shape.\n *\n * @example\n * ```ts\n * let signals: HlsAudioEngineSignals;\n * const engine = createHlsAudioEngine({\n * preferredAudioLanguage: 'en',\n * onSignalsReady: (refs) => {\n * signals = refs;\n * },\n * });\n *\n * signals.context.mediaElement.set(audioEl);\n * signals.state.presentation.set({ url: 'https://example.com/stream.m3u8' });\n * ```\n */\nexport function createHlsAudioEngine(\n config: HlsAudioEngineConfig = {}\n): Composition<HlsAudioEngineState, HlsAudioEngineContext> {\n const deriveStartMediaTime = config.deriveStartMediaTime ?? deriveSharedMinStartMediaTime;\n const finalConfig = {\n ...config,\n deriveStartMediaTime,\n // Baked (not user-overridable): this engine composes `setupAirPlay`,\n // whose native fallback `<source>` requires the MSE attachment to keep\n // sibling source alternatives part of resource selection. The helper's\n // `video/mp4` source type is inert here — resource selection probes it\n // with `canPlayType`, which answers `'maybe'` on an audio element too.\n attachMediaSource: attachMediaSourceAsSourceElement,\n canPlayTrack: config.canPlayTrack ?? canPlayTrack,\n reportUnsupportedTrackConditions: config.reportUnsupportedTrackConditions ?? reportUnsupportedTrackConditions,\n resolveDuration: config.resolveDuration ?? getResolvedSelectedTrackDuration,\n parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,\n // Non-zero-PTS relocation (spike): pair the audio loader with the relocation steps\n // `establishStartMediaTime` derives from; same `deriveStartMediaTime` seam. Remove\n // with the reactor.\n audioMessagePipelines: relocationPipelinesFor('audio', deriveStartMediaTime),\n };\n\n return createComposition(\n [\n syncPreload,\n trackLoadTriggers,\n resolvePresentation,\n\n // Session-level CDN priority for redundant-stream sources. Owns\n // `cdnPriority`; switchAudioTrack's preferActiveCdn scope reads it. No-op\n // for single-CDN sources.\n //\n // With a single track type there's no cross-type coherence to enforce and\n // the first pick is the primary CDN regardless, so composition order is\n // not load-bearing here today. It earns its place for forward-consistency\n // with the default engine and for future failover / steering, where the\n // active CDN changes dynamically (and selection stays reactive either way).\n deriveCdnPriority,\n\n // CDN failover cooldown: watches `failedCdns` (tripped directly by audio\n // track resolution on a failed media-playlist fetch) and removes each CDN\n // once its cooldown lapses.\n setupFailoverMonitor,\n\n // Owns `errors` and its per-source lifecycle; reporters append into it.\n collectErrors,\n\n // Audio track selection — slot owner with filter reactivity.\n // Mid-stream flush on language switch is handled in segment-loader's\n // planTasks, not here.\n switchAudioTrack,\n\n // Resolve selected tracks — audio only.\n resolveAudioTrack,\n\n // Presentation duration\n calculatePresentationDuration,\n\n // MSE setup. Single audio buffer; no video buffer to coordinate with,\n // so the Firefox `mozHasAudio` registration ordering is moot here.\n setupMediaSource,\n updateMediaSourceDuration,\n\n // Non-zero-PTS relocation (spike): establishes per-track startMediaTime;\n // MUST precede setupAudioBufferActors. Remove this line + the import + the\n // finalConfig/state entries to drop relocation. (Selection is optional in the\n // reactor, so it works with only audio in scope.)\n establishStartMediaTime,\n\n setupAudioBufferActors,\n\n // AirPlay/MSE bridge (WebKit only; no-op elsewhere). Audio-only sources\n // AirPlay to audio receivers (HomePod, AirPlay speakers) through the same\n // native-HLS fallback `<source>`; `webkitCurrentPlaybackTargetIsWireless`\n // and the picker are HTMLMediaElement-level, so an `<audio>` host is\n // AirPlay-capable on the same terms as a `<video>` one.\n setupAirPlay,\n\n // Playback tracking\n trackCurrentTime,\n // After trackCurrentTime: the one-shot currentTime seed must land after\n // the mirror's attach-time sync (see apply-start-position.ts).\n applyStartPosition,\n\n // Segment loading — audio only.\n loadAudioSegments,\n\n // End of stream coordination. `endOfStream` iterates buffer actors via\n // `[videoBufferActor, audioBufferActor].filter(Boolean)` and reads\n // `mediaSource.sourceBuffers` aggregately — composes unchanged with\n // only audio in scope.\n endOfStream,\n // Force native `ended` if Chrome freezes the playhead short of the buffered end\n // after `endOfStream`. Inert for a clean-ending single-track source.\n recoverEndStall,\n\n // Adapter signal callback.\n shareSignals,\n ],\n {\n config: finalConfig,\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmMA,MAAM,eAAe,iBAA6D,CAChF,2BACA,uBACF,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCD,SAAgB,qBACd,SAA+B,CAAC,GACyB;CACzD,MAAM,uBAAuB,OAAO,wBAAwB;CAC5D,MAAM,cAAc;EAClB,GAAG;EACH;EAMA,mBAAmB;EACnB,cAAc,OAAO,gBAAgB;EACrC,kCAAkC,OAAO,oCAAoC;EAC7E,iBAAiB,OAAO,mBAAmB;EAC3C,mBAAmB,OAAO,qBAAqB;EAI/C,uBAAuB,uBAAuB,SAAS,oBAAoB;CAC7E;CAEA,OAAO,kBACL;EACE;EACA;EACA;EAWA;EAKA;EAGA;EAKA;EAGA;EAGA;EAIA;EACA;EAMA;EAEA;EAOA;EAGA;EAGA;EAGA;EAMA;EAGA;EAGA;CACF,GACA,EACE,QAAQ,YACV,CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"engine-audio-only.js","names":[],"sources":["../../../../../src/playback/engines/hls/engine-audio-only.ts"],"sourcesContent":["import {\n type Composition,\n type ContextSignals,\n createComposition,\n type StateSignals,\n} from '../../../core/composition/create-composition';\nimport { makeShareSignals, type ShareSignalsConfig } from '../../../core/composition/share-signals';\nimport type { BackBufferConfig } from '../../../media/buffer/back-buffer';\nimport type { ForwardBufferConfig } from '../../../media/buffer/forward-buffer';\nimport { canPlayTrack } from '../../../media/dom/capabilities';\nimport { attachMediaSourceAsSourceElement } from '../../../media/dom/mse/mediasource-setup';\nimport type { SvtaError } from '../../../media/errors';\nimport { parseMultivariantPlaylist } from '../../../media/hls/parse-multivariant';\nimport type { AudioTrack, CanPlayTrack, MaybeResolvedPresentation, MediaContainerData } from '../../../media/types';\nimport type { GetCdnId } from '../../../media/utils/cdn';\nimport { getResolvedSelectedTrackDuration } from '../../../media/utils/track-selection';\nimport type { SegmentLoaderActor } from '../../actors/dom/segment-loader';\nimport type { SourceBufferActor } from '../../actors/dom/source-buffer';\nimport {\n calculatePresentationDuration,\n type PresentationDurationResolver,\n} from '../../behaviors/calculate-presentation-duration';\nimport { collectErrors } from '../../behaviors/collect-errors';\nimport { deriveCdnPriority } from '../../behaviors/derive-cdn-priority';\nimport { setupAirPlay } from '../../behaviors/dom/airplay';\nimport { applyStartPosition } from '../../behaviors/dom/apply-start-position';\nimport { endOfStream } from '../../behaviors/dom/end-of-stream';\nimport { loadAudioSegments } from '../../behaviors/dom/load-segments';\nimport { recoverEndStall } from '../../behaviors/dom/recover-end-stall';\nimport { setupAudioBufferActors } from '../../behaviors/dom/setup-buffer-actors';\nimport { setupMediaSource } from '../../behaviors/dom/setup-mediasource';\nimport { trackCurrentTime } from '../../behaviors/dom/track-current-time';\nimport { trackLoadTriggers } from '../../behaviors/dom/track-load-triggers';\nimport { updateMediaSourceDuration } from '../../behaviors/dom/update-mediasource-duration';\n// Non-zero-PTS relocation (spike): remove this import, the composed reactor, the\n// `audioMessagePipelines` finalConfig entry, the `mediaContainerData` state slot,\n// and the `deriveStartMediaTime` config field to drop relocation from audio-only.\nimport {\n type DeriveStartMediaTime,\n deriveSharedMinStartMediaTime,\n establishStartMediaTime,\n} from '../../behaviors/establish-start-media-time';\nimport { type ParsePresentation, resolvePresentation } from '../../behaviors/resolve-presentation';\nimport { resolveAudioTrack } from '../../behaviors/resolve-track';\nimport { type FailoverMonitorConfig, setupFailoverMonitor } from '../../behaviors/setup-failover-monitor';\nimport { syncPreload } from '../../behaviors/sync-preload';\nimport { switchAudioTrack } from '../../behaviors/track-switching';\nimport { relocationPipelinesFor } from '../../primitives/relocation-pipelines';\nimport {\n type ReportUnsupportedTrackConditions,\n reportUnsupportedTrackConditions,\n} from '../../primitives/report-track-conditions';\n\n// ============================================================================\n// Audio-Only HLS Engine State & Context\n// ============================================================================\n\n/**\n * State shape for the audio-only HLS playback engine.\n *\n * Subset of `HlsVideoEngineState` covering only the slots written and read by audio-side behaviors. Video and\n * text-track slots are absent — subtractive composition removes the behaviors that declare them.\n */\nexport interface HlsAudioEngineState {\n presentation?: MaybeResolvedPresentation;\n preload?: 'auto' | 'metadata' | 'none';\n selectedAudioTrackId?: string;\n // Non-zero-PTS relocation (spike): transient per-track container data owned by\n // `establishStartMediaTime`. Remove with the composed reactor.\n mediaContainerData?: Record<string, MediaContainerData>;\n /**\n * Consumer-driven constraint narrowing the audio candidate set. Sibling of `userVideoTrackSelection` in the default\n * engine. Partial-track shape — `{ language: 'es' }`, `{ id: 'audio-en' }`, etc. `selectAudioTrack` reads this and\n * re-picks when it changes. Multi-language-audio Tier 2 programmatic-write path.\n */\n userAudioTrackSelection?: Partial<AudioTrack>;\n /**\n * The CDNs the source is served from, in manifest priority order (mirrors HLS content steering's `PATHWAY-PRIORITY`).\n * Owned by `deriveCdnPriority`, read by `track-switching`'s `preferActiveCdn` scope. Only meaningful for\n * redundant-stream sources; a single-CDN source has one entry.\n */\n cdnPriority?: string[];\n /**\n * CDN ids currently in failover cooldown — read by `track-switching`'s `excludeFailedCdns` constraint, which prunes\n * their tracks so the active-CDN scope falls to the next CDN. Empty / absent means all CDNs are eligible.\n */\n failedCdns?: string[];\n /**\n * Conditions reported during playback, in order — appended by whichever behavior detects one, owned and cleared per\n * source by `collectErrors`. Severity is decided above the engine. Audio-only makes an all-audio-pruned source\n * unrecoverable: there's no video fallback to fall back to.\n */\n errors?: SvtaError[];\n currentTime?: number;\n loadActivated?: boolean;\n /**\n * One-shot command: start the current source at this position (presentation-timeline seconds). Written by consumers\n * or by `setupAirPlay`'s session-end snapshot; consumed (cleared) by `applyStartPosition` once the element seeks. See\n * `behaviors/dom/apply-start-position.ts`.\n */\n startPosition?: number;\n /**\n * Intent-level loading policy: initiate no new loading work while `true`. Written by `setupAirPlay` (the only\n * behavior declaring the key) while a remote-playback session owns presentation; observed by `loadAudioSegments`\n * (parks in `'dormant'`) and by `setupMediaSource` (a pending rebuild waits). See\n * `SegmentLoadingState['loadingSuspended']`.\n */\n loadingSuspended?: boolean;\n /**\n * Author intent for the AirPlay/remote-playback picker, written by the media adapter's `disableRemotePlayback` IDL\n * property. `true` is an explicit opt-out: `setupAirPlay` reads it at attach and sets nothing up, leaving the\n * element's remote playback disabled. Distinct from the underlying media element's own `disableRemotePlayback`, which\n * stays programmatically managed (ManagedMediaSource / AirPlay).\n */\n disableRemotePlayback?: boolean;\n}\n\n/**\n * Context shape for the audio-only HLS playback engine.\n *\n * Subset of `HlsVideoEngineContext` covering only the platform objects and actor refs managed by audio-side behaviors.\n */\nexport interface HlsAudioEngineContext {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n audioBufferActor?: SourceBufferActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n}\n\nexport type HlsAudioEngineSignals = {\n state: StateSignals<HlsAudioEngineState>;\n context: ContextSignals<HlsAudioEngineContext>;\n};\n\n/**\n * Configuration for the audio-only HLS playback engine.\n *\n * Subset of `HlsVideoEngineConfig` — video-quality, bandwidth-estimator, and text-track config fields are omitted (no\n * behavior consumes them).\n */\nexport interface HlsAudioEngineConfig extends ShareSignalsConfig<HlsAudioEngineState, HlsAudioEngineContext> {\n preferredAudioLanguage?: string;\n /**\n * Codec capability probe read by `track-switching`'s `excludeUnplayableTracks` constraint. Defaults to the\n * `MediaSource.isTypeSupported`-backed `canPlayTrack`; override to force-exclude a codec. Mirrors the default engine\n * — without it, capability probing (and TS / raw-AAC detection) would be inert for audio-only playback.\n */\n canPlayTrack?: CanPlayTrack;\n /**\n * Codec families the initial audio pick prefers on a mixed-codec source (`preferCodecFamilies` scope) — the family it\n * lands in is then sticky for the source's lifetime (`stickToSelectedCodecs`; SPF implements no\n * `SourceBuffer.changeType()`). Defaults to `DEFAULT_PREFERRED_CODECS` (AAC, plus video 4CCs inert here); pass `[]`\n * to disable.\n */\n preferredCodecs?: string[];\n /**\n * Conditions reported about each rendition as it resolves — the _causes_ behind a later verdict, and the copy a\n * verdict reuses when they agree. Defaults to {@link reportUnsupportedTrackConditions}, which reports non-fMP4\n * containers and encryption; supply your own to report a different set (a provider that never ships MPEG-TS can drop\n * that check) or `() => []` to report nothing.\n */\n reportUnsupportedTrackConditions?: ReportUnsupportedTrackConditions;\n resolveDuration?: PresentationDurationResolver;\n parsePresentation?: ParsePresentation;\n forwardBuffer?: Partial<ForwardBufferConfig>;\n backBuffer?: Partial<BackBufferConfig>;\n /** Multi-CDN failover monitor tuning. Defaults: `DEFAULT_FAILOVER_MONITOR_CONFIG`. */\n failover?: Partial<FailoverMonitorConfig>;\n /**\n * Derive a CDN grouping key from a track URL (used by `cdnPriority`, the failover trip, and the track-switching CDN\n * rules — one function read by all). Defaults to the URL origin; override to key on e.g. Mux's `cdn=` param.\n */\n getCdnId?: GetCdnId;\n /** Non-zero-PTS relocation (spike): the reduce seam (tier knob); defaults to per-track own. */\n deriveStartMediaTime?: DeriveStartMediaTime;\n}\n\n// ============================================================================\n// Audio-Only HLS Playback Engine\n// ============================================================================\n\n// Materializes input slots no composed behavior produces — `userAudioTrackSelection`\n// (switchAudioTrack only reads it) and `disableRemotePlayback` (setupAirPlay only\n// reads it) — in addition to forwarding refs. `failedCdns` is owned by\n// `setupFailoverMonitor`, so it's already materialized and reachable on the\n// `onSignalsReady` refs without being listed here.\nconst shareSignals = makeShareSignals<HlsAudioEngineState, HlsAudioEngineContext>([\n 'userAudioTrackSelection',\n 'disableRemotePlayback',\n]);\n\n/**\n * Create an audio-only HLS playback engine.\n *\n * Subtractive composition variant of `createHlsVideoEngine`: omits video-side behaviors (`resolveVideoTrack`,\n * `switchVideoTrack`, `setupVideoBufferActors`, `loadVideoSegments`) and text-track behaviors (`switchTextTrack`,\n * `resolveTextTrack`, `syncTextTracks`, `setupTextTrackActors`, `loadTextTrackSegments`). The remaining audio pipeline\n * composes unchanged.\n *\n * Handles both truly audio-only HLS sources (no video stream-inf) and mixed-AV HLS sources where the audio rendition is\n * selected and video / subtitle renditions are ignored at composition time. The variant decision is encoded by adapter\n * choice; this engine does not branch on source shape.\n *\n * @example\n * ```ts\n * let signals: HlsAudioEngineSignals;\n * const engine = createHlsAudioEngine({\n * preferredAudioLanguage: 'en',\n * onSignalsReady: (refs) => {\n * signals = refs;\n * },\n * });\n *\n * signals.context.mediaElement.set(audioEl);\n * signals.state.presentation.set({ url: 'https://example.com/stream.m3u8' });\n * ```;\n */\nexport function createHlsAudioEngine(\n config: HlsAudioEngineConfig = {}\n): Composition<HlsAudioEngineState, HlsAudioEngineContext> {\n const deriveStartMediaTime = config.deriveStartMediaTime ?? deriveSharedMinStartMediaTime;\n const finalConfig = {\n ...config,\n deriveStartMediaTime,\n // Baked (not user-overridable): this engine composes `setupAirPlay`,\n // whose native fallback `<source>` requires the MSE attachment to keep\n // sibling source alternatives part of resource selection. The helper's\n // `video/mp4` source type is inert here — resource selection probes it\n // with `canPlayType`, which answers `'maybe'` on an audio element too.\n attachMediaSource: attachMediaSourceAsSourceElement,\n canPlayTrack: config.canPlayTrack ?? canPlayTrack,\n reportUnsupportedTrackConditions: config.reportUnsupportedTrackConditions ?? reportUnsupportedTrackConditions,\n resolveDuration: config.resolveDuration ?? getResolvedSelectedTrackDuration,\n parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,\n // Non-zero-PTS relocation (spike): pair the audio loader with the relocation steps\n // `establishStartMediaTime` derives from; same `deriveStartMediaTime` seam. Remove\n // with the reactor.\n audioMessagePipelines: relocationPipelinesFor('audio', deriveStartMediaTime),\n };\n\n return createComposition(\n [\n syncPreload,\n trackLoadTriggers,\n resolvePresentation,\n\n // Session-level CDN priority for redundant-stream sources. Owns\n // `cdnPriority`; switchAudioTrack's preferActiveCdn scope reads it. No-op\n // for single-CDN sources.\n //\n // With a single track type there's no cross-type coherence to enforce and\n // the first pick is the primary CDN regardless, so composition order is\n // not load-bearing here today. It earns its place for forward-consistency\n // with the default engine and for future failover / steering, where the\n // active CDN changes dynamically (and selection stays reactive either way).\n deriveCdnPriority,\n\n // CDN failover cooldown: watches `failedCdns` (tripped directly by audio\n // track resolution on a failed media-playlist fetch) and removes each CDN\n // once its cooldown lapses.\n setupFailoverMonitor,\n\n // Owns `errors` and its per-source lifecycle; reporters append into it.\n collectErrors,\n\n // Audio track selection — slot owner with filter reactivity.\n // Mid-stream flush on language switch is handled in segment-loader's\n // planTasks, not here.\n switchAudioTrack,\n\n // Resolve selected tracks — audio only.\n resolveAudioTrack,\n\n // Presentation duration\n calculatePresentationDuration,\n\n // MSE setup. Single audio buffer; no video buffer to coordinate with,\n // so the Firefox `mozHasAudio` registration ordering is moot here.\n setupMediaSource,\n updateMediaSourceDuration,\n\n // Non-zero-PTS relocation (spike): establishes per-track startMediaTime;\n // MUST precede setupAudioBufferActors. Remove this line + the import + the\n // finalConfig/state entries to drop relocation. (Selection is optional in the\n // reactor, so it works with only audio in scope.)\n establishStartMediaTime,\n\n setupAudioBufferActors,\n\n // AirPlay/MSE bridge (WebKit only; no-op elsewhere). Audio-only sources\n // AirPlay to audio receivers (HomePod, AirPlay speakers) through the same\n // native-HLS fallback `<source>`; `webkitCurrentPlaybackTargetIsWireless`\n // and the picker are HTMLMediaElement-level, so an `<audio>` host is\n // AirPlay-capable on the same terms as a `<video>` one.\n setupAirPlay,\n\n // Playback tracking\n trackCurrentTime,\n // After trackCurrentTime: the one-shot currentTime seed must land after\n // the mirror's attach-time sync (see apply-start-position.ts).\n applyStartPosition,\n\n // Segment loading — audio only.\n loadAudioSegments,\n\n // End of stream coordination. `endOfStream` iterates buffer actors via\n // `[videoBufferActor, audioBufferActor].filter(Boolean)` and reads\n // `mediaSource.sourceBuffers` aggregately — composes unchanged with\n // only audio in scope.\n endOfStream,\n // Force native `ended` if Chrome freezes the playhead short of the buffered end\n // after `endOfStream`. Inert for a clean-ending single-track source.\n recoverEndStall,\n\n // Adapter signal callback.\n shareSignals,\n ],\n {\n config: finalConfig,\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0LA,MAAM,eAAe,iBAA6D,CAChF,2BACA,uBACF,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BD,SAAgB,qBACd,SAA+B,CAAC,GACyB;CACzD,MAAM,uBAAuB,OAAO,wBAAwB;CAC5D,MAAM,cAAc;EAClB,GAAG;EACH;EAMA,mBAAmB;EACnB,cAAc,OAAO,gBAAgB;EACrC,kCAAkC,OAAO,oCAAoC;EAC7E,iBAAiB,OAAO,mBAAmB;EAC3C,mBAAmB,OAAO,qBAAqB;EAI/C,uBAAuB,uBAAuB,SAAS,oBAAoB;CAC7E;CAEA,OAAO,kBACL;EACE;EACA;EACA;EAWA;EAKA;EAGA;EAKA;EAGA;EAGA;EAIA;EACA;EAMA;EAEA;EAOA;EAGA;EAGA;EAGA;EAMA;EAGA;EAGA;CACF,GACA,EACE,QAAQ,YACV,CACF;AACF"}
|
|
@@ -5,11 +5,11 @@ import { trackCurrentTime } from "../../behaviors/dom/track-current-time.js";
|
|
|
5
5
|
import "../../../media/errors.js";
|
|
6
6
|
import { excludeUnplayableTracks } from "../../primitives/selection-rules.js";
|
|
7
7
|
import { preferHighestResolution, screenResolutionCap, selectVideoTrack } from "../../behaviors/select-tracks.js";
|
|
8
|
+
import { collectErrors, reportAbsentTrackType } from "../../behaviors/collect-errors.js";
|
|
8
9
|
import { canPlayTrack } from "../../../media/dom/capabilities.js";
|
|
9
10
|
import { parseMultivariantPlaylist } from "../../../media/hls/parse-multivariant.js";
|
|
10
11
|
import { getResolvedSelectedTrackDuration } from "../../../media/utils/track-selection.js";
|
|
11
12
|
import { calculatePresentationDuration } from "../../behaviors/calculate-presentation-duration.js";
|
|
12
|
-
import { collectErrors, reportAbsentTrackType } from "../../behaviors/collect-errors.js";
|
|
13
13
|
import { endOfStream } from "../../behaviors/dom/end-of-stream.js";
|
|
14
14
|
import { setupVideoBufferActors } from "../../behaviors/dom/setup-buffer-actors.js";
|
|
15
15
|
import { setupMediaSource } from "../../behaviors/dom/setup-mediasource.js";
|
|
@@ -23,39 +23,34 @@ const shareSignals = makeShareSignals();
|
|
|
23
23
|
/**
|
|
24
24
|
* Create a background-video playback engine.
|
|
25
25
|
*
|
|
26
|
-
* Subtractive composition over the HLS engine baseline:
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* a single rendition for the session. The initial state seeds
|
|
31
|
-
* `loadActivated: true` so the composition behaves as if preload has
|
|
32
|
-
* already been activated — appropriate for ambient / hero / GIF-replacement
|
|
26
|
+
* Subtractive composition over the HLS engine baseline: audio-side, text-side, ABR-driven, preload-monitoring, and
|
|
27
|
+
* play/seek load-trigger behaviors are removed. `selectVideoTrack` (with a highest-resolution rule by default) replaces
|
|
28
|
+
* `switchVideoQuality`, pinning a single rendition for the session. The initial state seeds `loadActivated: true` so
|
|
29
|
+
* the composition behaves as if preload has already been activated — appropriate for ambient / hero / GIF-replacement
|
|
33
30
|
* surfaces that should start loading the moment a src is set.
|
|
34
31
|
*
|
|
35
|
-
* Error reporting is
|
|
36
|
-
* `
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* encryption this engine can't decrypt, and an undecodable codec all leave
|
|
40
|
-
* `HTMLMediaElement.error` null on both Chromium and WebKit.
|
|
32
|
+
* Error reporting is _not_ subtracted: `collectErrors` owns the sequence, `resolveVideoTrack` reports per-rendition
|
|
33
|
+
* causes, and `selectVideoTrack` reports the video verdict when nothing survives its constraints. Without them every
|
|
34
|
+
* unplayable source here is a silent stall — an unsupported container, encryption this engine can't decrypt, and an
|
|
35
|
+
* undecodable codec all leave `HTMLMediaElement.error` null on both Chromium and WebKit.
|
|
41
36
|
*
|
|
42
|
-
* Native `loop` / `muted` / `autoplay` are adapter concerns and live on
|
|
43
|
-
*
|
|
37
|
+
* Native `loop` / `muted` / `autoplay` are adapter concerns and live on `HlsBackgroundVideoMediaElement` rather than
|
|
38
|
+
* the engine.
|
|
44
39
|
*
|
|
45
40
|
* @example
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* let signals: BackgroundVideoEngineSignals;
|
|
43
|
+
* const engine = createBackgroundVideoEngine({
|
|
44
|
+
* onSignalsReady: (refs) => {
|
|
45
|
+
* signals = refs;
|
|
46
|
+
* },
|
|
47
|
+
* });
|
|
53
48
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
49
|
+
* signals.context.mediaElement.set(videoEl);
|
|
50
|
+
* signals.state.presentation.set({ url: 'https://example.com/stream.m3u8' });
|
|
56
51
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
52
|
+
* await engine.destroy();
|
|
53
|
+
* ```;
|
|
59
54
|
*/
|
|
60
55
|
function createBackgroundVideoEngine(config = {}) {
|
|
61
56
|
const finalConfig = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine-background-video.js","names":[],"sources":["../../../../../src/playback/engines/hls/engine-background-video.ts"],"sourcesContent":["import {\n type Composition,\n type ContextSignals,\n createComposition,\n type StateSignals,\n} from '../../../core/composition/create-composition';\nimport { makeShareSignals, type ShareSignalsConfig } from '../../../core/composition/share-signals';\nimport { canPlayTrack } from '../../../media/dom/capabilities';\nimport type { ScreenResolution } from '../../../media/dom/screen';\nimport { SVTA_NO_SUPPORTED_VIDEO_TRACK, type SvtaError } from '../../../media/errors';\nimport { parseMultivariantPlaylist } from '../../../media/hls/parse-multivariant';\nimport type { CanPlayTrack, MaybeResolvedPresentation } from '../../../media/types';\nimport { getResolvedSelectedTrackDuration } from '../../../media/utils/track-selection';\nimport type { SegmentLoaderActor } from '../../actors/dom/segment-loader';\nimport type { SourceBufferActor } from '../../actors/dom/source-buffer';\nimport { calculatePresentationDuration } from '../../behaviors/calculate-presentation-duration';\nimport { collectErrors, reportAbsentTrackType } from '../../behaviors/collect-errors';\nimport { endOfStream } from '../../behaviors/dom/end-of-stream';\nimport { loadVideoSegments } from '../../behaviors/dom/load-segments';\nimport { setupVideoBufferActors } from '../../behaviors/dom/setup-buffer-actors';\nimport { setupMediaSource } from '../../behaviors/dom/setup-mediasource';\nimport { trackCurrentTime } from '../../behaviors/dom/track-current-time';\nimport { trackScreenResolution } from '../../behaviors/dom/track-screen-resolution';\nimport { updateMediaSourceDuration } from '../../behaviors/dom/update-mediasource-duration';\nimport { type ParsePresentation, resolvePresentation } from '../../behaviors/resolve-presentation';\nimport { resolveVideoTrack } from '../../behaviors/resolve-track';\nimport {\n preferHighestResolution,\n type SelectVideoTrackConfig,\n screenResolutionCap,\n selectVideoTrack,\n} from '../../behaviors/select-tracks';\nimport {\n type ReportUnsupportedTrackConditions,\n reportUnsupportedTrackConditions,\n} from '../../primitives/report-track-conditions';\nimport { excludeUnplayableTracks } from '../../primitives/selection-rules';\n\n// ============================================================================\n// Background-video engine state & context\n// ============================================================================\n\n/**\n * State shape for the background-video playback engine.\n *\n * Mostly narrower than `HlsVideoEngineState`: audio/text track slots are absent\n * because their selection/resolution behaviors are subtracted. `bandwidthState`\n * is present because `setupVideoBufferActors` declares it and `loadVideoSegments`\n * samples into it (wasted work in this variant — a Phase 3 alt-impl will skip\n * sampling).\n *\n * `screenResolution` is the one slot this variant has and the HLS video engine\n * doesn't, because the screen-size cap is being built here first. It generalizes\n * — the cap is a selection rule both engines can compose — so expect the slot to\n * appear there too rather than staying variant-specific.\n */\nexport interface BackgroundVideoEngineState {\n /**\n * The presentation being played. A caller writes `{ url }`;\n * `resolvePresentation` parses the manifest and populates the rest.\n */\n presentation?: MaybeResolvedPresentation;\n preload?: 'auto' | 'metadata' | 'none';\n selectedVideoTrackId?: string;\n loadActivated?: boolean;\n /**\n * The screen's pixel dimensions, or `undefined` where there is none to read.\n * Written by `trackScreenResolution`, read by the `screenResolutionCap`\n * selection rule — which treats `undefined` as \"don't cap\".\n */\n screenResolution?: ScreenResolution;\n /**\n * Conditions reported while this source is loaded — the per-rendition causes\n * `resolveVideoTrack` reports and the verdict `selectVideoTrack` reports when\n * the constraints prune every rendition. Owned and cleared per source by\n * `collectErrors`; the adapter derives which are fatal.\n */\n errors?: SvtaError[];\n}\n\n/**\n * Context shape for the background-video engine.\n */\nexport interface BackgroundVideoEngineContext {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n videoSegmentLoaderActor?: SegmentLoaderActor;\n}\n\n/**\n * The composition signal refs handed to `onSignalsReady` callers — the\n * canonical way to drive the engine externally (writes) or observe its\n * state (reads) without touching `composition.state` / `composition.context`\n * directly.\n */\nexport type BackgroundVideoEngineSignals = {\n state: StateSignals<BackgroundVideoEngineState>;\n context: ContextSignals<BackgroundVideoEngineContext>;\n};\n\n/**\n * Configuration for the background-video engine.\n *\n * Each option is consumed by the appropriate behavior — the engine itself\n * has no config beyond what its behaviors read. Compared to\n * `HlsVideoEngineConfig`, audio/text/ABR/bandwidth/quality knobs are\n * dropped: the variant subtracts the behaviors that read them.\n */\nexport interface BackgroundVideoEngineConfig\n extends ShareSignalsConfig<BackgroundVideoEngineState, BackgroundVideoEngineContext> {\n /**\n * Hard-constraint pre-pass handed to `selectVideoTrack`. Defaults to\n * `[excludeUnplayableTracks, reportAbsentTrackType(2011)]` — prune the renditions\n * this environment can't decode, then report 2011 if nothing is left (this engine\n * composes only video, so a source with none playable can never play).\n */\n constraints?: SelectVideoTrackConfig['constraints'];\n /**\n * Selection-rule chain handed to `selectVideoTrack`. Defaults to\n * `[screenResolutionCap, preferHighestResolution]` — narrows to the renditions\n * that fit the screen, takes the largest of those, and pins it for the session.\n *\n * The cap sits ahead of the ranker because a scope that narrows first wins over\n * one applied later; pass `[preferHighestResolution]` alone to opt out and always\n * pin the largest rendition on offer.\n */\n rules?: readonly NonNullable<SelectVideoTrackConfig['rules']>[number][];\n /**\n * Manifest parser handed to `resolvePresentation`. Defaults to the HLS\n * multivariant-playlist parser.\n */\n parsePresentation?: ParsePresentation;\n /**\n * Whether `state.screenResolution` is reported in device pixels. Read by\n * `trackScreenResolution`; defaults to `true`.\n */\n useDevicePixelRatio?: boolean;\n /**\n * Codec/container capability probe read by `selectVideoTrack`'s constraint\n * pre-pass. Defaults to the DOM `canPlayTrack`; override to force-exclude a\n * codec.\n */\n canPlayTrack?: CanPlayTrack;\n /**\n * Per-rendition condition reporting, called by `resolveVideoTrack` once a\n * media playlist parses. Defaults to\n * {@link reportUnsupportedTrackConditions}, which reports non-fMP4 containers\n * (1004) and encryption (4008).\n */\n reportUnsupportedTrackConditions?: ReportUnsupportedTrackConditions;\n}\n\n// ============================================================================\n// Background-video playback engine\n// ============================================================================\n\nconst shareSignals = makeShareSignals<BackgroundVideoEngineState, BackgroundVideoEngineContext>();\n\n/**\n * Create a background-video playback engine.\n *\n * Subtractive composition over the HLS engine baseline:\n * audio-side, text-side, ABR-driven, preload-monitoring, and play/seek\n * load-trigger behaviors are removed. `selectVideoTrack` (with a\n * highest-resolution rule by default) replaces `switchVideoQuality`, pinning\n * a single rendition for the session. The initial state seeds\n * `loadActivated: true` so the composition behaves as if preload has\n * already been activated — appropriate for ambient / hero / GIF-replacement\n * surfaces that should start loading the moment a src is set.\n *\n * Error reporting is *not* subtracted: `collectErrors` owns the sequence,\n * `resolveVideoTrack` reports per-rendition causes, and `selectVideoTrack`\n * reports the video verdict when nothing survives its constraints. Without them\n * every unplayable source here is a silent stall — an unsupported container,\n * encryption this engine can't decrypt, and an undecodable codec all leave\n * `HTMLMediaElement.error` null on both Chromium and WebKit.\n *\n * Native `loop` / `muted` / `autoplay` are adapter concerns and live on\n * `HlsBackgroundVideoMediaElement` rather than the engine.\n *\n * @example\n * ```ts\n * let signals: BackgroundVideoEngineSignals;\n * const engine = createBackgroundVideoEngine({\n * onSignalsReady: (refs) => {\n * signals = refs;\n * },\n * });\n *\n * signals.context.mediaElement.set(videoEl);\n * signals.state.presentation.set({ url: 'https://example.com/stream.m3u8' });\n *\n * await engine.destroy();\n * ```\n */\nexport function createBackgroundVideoEngine(\n config: BackgroundVideoEngineConfig = {}\n): Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext> {\n const finalConfig = {\n ...config,\n constraints: config.constraints ?? [excludeUnplayableTracks, reportAbsentTrackType(SVTA_NO_SUPPORTED_VIDEO_TRACK)],\n rules: config.rules ?? [screenResolutionCap, preferHighestResolution],\n parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,\n resolveDuration: getResolvedSelectedTrackDuration,\n canPlayTrack: config.canPlayTrack ?? canPlayTrack,\n reportUnsupportedTrackConditions: config.reportUnsupportedTrackConditions ?? reportUnsupportedTrackConditions,\n };\n\n return createComposition(\n [\n resolvePresentation,\n // Presentation duration\n calculatePresentationDuration,\n\n // Owns `errors` and its per-source lifecycle; reporters append into it.\n collectErrors,\n\n // Track selection - pinned single-rendition pick on presentation resolve,\n // unpinned again if the constraint pre-pass later prunes every rendition\n // (which is how a container relabel reaches a pick already made).\n selectVideoTrack,\n // Resolve selected video track (fetch its media playlist)\n resolveVideoTrack,\n // Segment loading — video-only.\n loadVideoSegments,\n\n // MSE setup — video-only.\n setupMediaSource,\n updateMediaSourceDuration,\n setupVideoBufferActors,\n\n // Playback tracking\n trackCurrentTime,\n\n // Environment tracking — the signal source for a screen-size rendition\n // cap. Independent of the presentation, so it sits outside the\n // resolve/select/load sequence above.\n trackScreenResolution,\n\n // End of stream coordination\n endOfStream,\n\n // Behavior whose sole purpose is to expose signal refs via a callback\n // (e.g. to an adapter). Listed last so initial signal setup has run\n // before the callback fires.\n shareSignals,\n ],\n {\n config: finalConfig,\n initialState: {\n // Note: Set to true until we add preload configuration\n loadActivated: true,\n },\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA6JA,MAAM,eAAe,iBAA2E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuChG,SAAgB,4BACd,SAAsC,CAAC,GACgC;CACvE,MAAM,cAAc;EAClB,GAAG;EACH,aAAa,OAAO,eAAe,CAAC,yBAAyB,sBAAA,IAAmD,CAAC;EACjH,OAAO,OAAO,SAAS,CAAC,qBAAqB,uBAAuB;EACpE,mBAAmB,OAAO,qBAAqB;EAC/C,iBAAiB;EACjB,cAAc,OAAO,gBAAgB;EACrC,kCAAkC,OAAO,oCAAoC;CAC/E;CAEA,OAAO,kBACL;EACE;EAEA;EAGA;EAKA;EAEA;EAEA;EAGA;EACA;EACA;EAGA;EAKA;EAGA;EAKA;CACF,GACA;EACE,QAAQ;EACR,cAAc,EAEZ,eAAe,KACjB;CACF,CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"engine-background-video.js","names":[],"sources":["../../../../../src/playback/engines/hls/engine-background-video.ts"],"sourcesContent":["import {\n type Composition,\n type ContextSignals,\n createComposition,\n type StateSignals,\n} from '../../../core/composition/create-composition';\nimport { makeShareSignals, type ShareSignalsConfig } from '../../../core/composition/share-signals';\nimport { canPlayTrack } from '../../../media/dom/capabilities';\nimport type { ScreenResolution } from '../../../media/dom/screen';\nimport { SVTA_NO_SUPPORTED_VIDEO_TRACK, type SvtaError } from '../../../media/errors';\nimport { parseMultivariantPlaylist } from '../../../media/hls/parse-multivariant';\nimport type { CanPlayTrack, MaybeResolvedPresentation } from '../../../media/types';\nimport { getResolvedSelectedTrackDuration } from '../../../media/utils/track-selection';\nimport type { SegmentLoaderActor } from '../../actors/dom/segment-loader';\nimport type { SourceBufferActor } from '../../actors/dom/source-buffer';\nimport { calculatePresentationDuration } from '../../behaviors/calculate-presentation-duration';\nimport { collectErrors, reportAbsentTrackType } from '../../behaviors/collect-errors';\nimport { endOfStream } from '../../behaviors/dom/end-of-stream';\nimport { loadVideoSegments } from '../../behaviors/dom/load-segments';\nimport { setupVideoBufferActors } from '../../behaviors/dom/setup-buffer-actors';\nimport { setupMediaSource } from '../../behaviors/dom/setup-mediasource';\nimport { trackCurrentTime } from '../../behaviors/dom/track-current-time';\nimport { trackScreenResolution } from '../../behaviors/dom/track-screen-resolution';\nimport { updateMediaSourceDuration } from '../../behaviors/dom/update-mediasource-duration';\nimport { type ParsePresentation, resolvePresentation } from '../../behaviors/resolve-presentation';\nimport { resolveVideoTrack } from '../../behaviors/resolve-track';\nimport {\n preferHighestResolution,\n type SelectVideoTrackConfig,\n screenResolutionCap,\n selectVideoTrack,\n} from '../../behaviors/select-tracks';\nimport {\n type ReportUnsupportedTrackConditions,\n reportUnsupportedTrackConditions,\n} from '../../primitives/report-track-conditions';\nimport { excludeUnplayableTracks } from '../../primitives/selection-rules';\n\n// ============================================================================\n// Background-video engine state & context\n// ============================================================================\n\n/**\n * State shape for the background-video playback engine.\n *\n * Mostly narrower than `HlsVideoEngineState`: audio/text track slots are absent because their selection/resolution\n * behaviors are subtracted. `bandwidthState` is present because `setupVideoBufferActors` declares it and\n * `loadVideoSegments` samples into it (wasted work in this variant — a Phase 3 alt-impl will skip sampling).\n *\n * `screenResolution` is the one slot this variant has and the HLS video engine doesn't, because the screen-size cap is\n * being built here first. It generalizes — the cap is a selection rule both engines can compose — so expect the slot to\n * appear there too rather than staying variant-specific.\n */\nexport interface BackgroundVideoEngineState {\n /**\n * The presentation being played. A caller writes `{ url }`; `resolvePresentation` parses the manifest and populates\n * the rest.\n */\n presentation?: MaybeResolvedPresentation;\n preload?: 'auto' | 'metadata' | 'none';\n selectedVideoTrackId?: string;\n loadActivated?: boolean;\n /**\n * The screen's pixel dimensions, or `undefined` where there is none to read. Written by `trackScreenResolution`, read\n * by the `screenResolutionCap` selection rule — which treats `undefined` as \"don't cap\".\n */\n screenResolution?: ScreenResolution;\n /**\n * Conditions reported while this source is loaded — the per-rendition causes `resolveVideoTrack` reports and the\n * verdict `selectVideoTrack` reports when the constraints prune every rendition. Owned and cleared per source by\n * `collectErrors`; the adapter derives which are fatal.\n */\n errors?: SvtaError[];\n}\n\n/** Context shape for the background-video engine. */\nexport interface BackgroundVideoEngineContext {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n videoSegmentLoaderActor?: SegmentLoaderActor;\n}\n\n/**\n * The composition signal refs handed to `onSignalsReady` callers — the canonical way to drive the engine externally\n * (writes) or observe its state (reads) without touching `composition.state` / `composition.context` directly.\n */\nexport type BackgroundVideoEngineSignals = {\n state: StateSignals<BackgroundVideoEngineState>;\n context: ContextSignals<BackgroundVideoEngineContext>;\n};\n\n/**\n * Configuration for the background-video engine.\n *\n * Each option is consumed by the appropriate behavior — the engine itself has no config beyond what its behaviors read.\n * Compared to `HlsVideoEngineConfig`, audio/text/ABR/bandwidth/quality knobs are dropped: the variant subtracts the\n * behaviors that read them.\n */\nexport interface BackgroundVideoEngineConfig extends ShareSignalsConfig<\n BackgroundVideoEngineState,\n BackgroundVideoEngineContext\n> {\n /**\n * Hard-constraint pre-pass handed to `selectVideoTrack`. Defaults to `[excludeUnplayableTracks,\n * reportAbsentTrackType(2011)]` — prune the renditions this environment can't decode, then report 2011 if nothing is\n * left (this engine composes only video, so a source with none playable can never play).\n */\n constraints?: SelectVideoTrackConfig['constraints'];\n /**\n * Selection-rule chain handed to `selectVideoTrack`. Defaults to `[screenResolutionCap, preferHighestResolution]` —\n * narrows to the renditions that fit the screen, takes the largest of those, and pins it for the session.\n *\n * The cap sits ahead of the ranker because a scope that narrows first wins over one applied later; pass\n * `[preferHighestResolution]` alone to opt out and always pin the largest rendition on offer.\n */\n rules?: readonly NonNullable<SelectVideoTrackConfig['rules']>[number][];\n /** Manifest parser handed to `resolvePresentation`. Defaults to the HLS multivariant-playlist parser. */\n parsePresentation?: ParsePresentation;\n /** Whether `state.screenResolution` is reported in device pixels. Read by `trackScreenResolution`; defaults to `true`. */\n useDevicePixelRatio?: boolean;\n /**\n * Codec/container capability probe read by `selectVideoTrack`'s constraint pre-pass. Defaults to the DOM\n * `canPlayTrack`; override to force-exclude a codec.\n */\n canPlayTrack?: CanPlayTrack;\n /**\n * Per-rendition condition reporting, called by `resolveVideoTrack` once a media playlist parses. Defaults to\n * {@link reportUnsupportedTrackConditions}, which reports non-fMP4 containers (1004) and encryption (4008).\n */\n reportUnsupportedTrackConditions?: ReportUnsupportedTrackConditions;\n}\n\n// ============================================================================\n// Background-video playback engine\n// ============================================================================\n\nconst shareSignals = makeShareSignals<BackgroundVideoEngineState, BackgroundVideoEngineContext>();\n\n/**\n * Create a background-video playback engine.\n *\n * Subtractive composition over the HLS engine baseline: audio-side, text-side, ABR-driven, preload-monitoring, and\n * play/seek load-trigger behaviors are removed. `selectVideoTrack` (with a highest-resolution rule by default) replaces\n * `switchVideoQuality`, pinning a single rendition for the session. The initial state seeds `loadActivated: true` so\n * the composition behaves as if preload has already been activated — appropriate for ambient / hero / GIF-replacement\n * surfaces that should start loading the moment a src is set.\n *\n * Error reporting is _not_ subtracted: `collectErrors` owns the sequence, `resolveVideoTrack` reports per-rendition\n * causes, and `selectVideoTrack` reports the video verdict when nothing survives its constraints. Without them every\n * unplayable source here is a silent stall — an unsupported container, encryption this engine can't decrypt, and an\n * undecodable codec all leave `HTMLMediaElement.error` null on both Chromium and WebKit.\n *\n * Native `loop` / `muted` / `autoplay` are adapter concerns and live on `HlsBackgroundVideoMediaElement` rather than\n * the engine.\n *\n * @example\n * ```ts\n * let signals: BackgroundVideoEngineSignals;\n * const engine = createBackgroundVideoEngine({\n * onSignalsReady: (refs) => {\n * signals = refs;\n * },\n * });\n *\n * signals.context.mediaElement.set(videoEl);\n * signals.state.presentation.set({ url: 'https://example.com/stream.m3u8' });\n *\n * await engine.destroy();\n * ```;\n */\nexport function createBackgroundVideoEngine(\n config: BackgroundVideoEngineConfig = {}\n): Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext> {\n const finalConfig = {\n ...config,\n constraints: config.constraints ?? [excludeUnplayableTracks, reportAbsentTrackType(SVTA_NO_SUPPORTED_VIDEO_TRACK)],\n rules: config.rules ?? [screenResolutionCap, preferHighestResolution],\n parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,\n resolveDuration: getResolvedSelectedTrackDuration,\n canPlayTrack: config.canPlayTrack ?? canPlayTrack,\n reportUnsupportedTrackConditions: config.reportUnsupportedTrackConditions ?? reportUnsupportedTrackConditions,\n };\n\n return createComposition(\n [\n resolvePresentation,\n // Presentation duration\n calculatePresentationDuration,\n\n // Owns `errors` and its per-source lifecycle; reporters append into it.\n collectErrors,\n\n // Track selection - pinned single-rendition pick on presentation resolve,\n // unpinned again if the constraint pre-pass later prunes every rendition\n // (which is how a container relabel reaches a pick already made).\n selectVideoTrack,\n // Resolve selected video track (fetch its media playlist)\n resolveVideoTrack,\n // Segment loading — video-only.\n loadVideoSegments,\n\n // MSE setup — video-only.\n setupMediaSource,\n updateMediaSourceDuration,\n setupVideoBufferActors,\n\n // Playback tracking\n trackCurrentTime,\n\n // Environment tracking — the signal source for a screen-size rendition\n // cap. Independent of the presentation, so it sits outside the\n // resolve/select/load sequence above.\n trackScreenResolution,\n\n // End of stream coordination\n endOfStream,\n\n // Behavior whose sole purpose is to expose signal refs via a callback\n // (e.g. to an adapter). Listed last so initial signal setup has run\n // before the callback fires.\n shareSignals,\n ],\n {\n config: finalConfig,\n initialState: {\n // Note: Set to true until we add preload configuration\n loadActivated: true,\n },\n }\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAyIA,MAAM,eAAe,iBAA2E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkChG,SAAgB,4BACd,SAAsC,CAAC,GACgC;CACvE,MAAM,cAAc;EAClB,GAAG;EACH,aAAa,OAAO,eAAe,CAAC,yBAAyB,sBAAA,IAAmD,CAAC;EACjH,OAAO,OAAO,SAAS,CAAC,qBAAqB,uBAAuB;EACpE,mBAAmB,OAAO,qBAAqB;EAC/C,iBAAiB;EACjB,cAAc,OAAO,gBAAgB;EACrC,kCAAkC,OAAO,oCAAoC;CAC/E;CAEA,OAAO,kBACL;EACE;EAEA;EAGA;EAKA;EAEA;EAEA;EAGA;EACA;EACA;EAGA;EAKA;EAGA;EAKA;CACF,GACA;EACE,QAAQ;EACR,cAAc,EAEZ,eAAe,KACjB;CACF,CACF;AACF"}
|