@videojs/spf 10.0.0-beta.24 → 10.0.0-beta.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/default/background-video.js +3 -0
- package/dist/default/core/actors/create-machine-actor.js.map +1 -1
- package/dist/default/core/actors/create-transition-actor.js.map +1 -1
- package/dist/default/core/composition/create-composition.js +2 -2
- package/dist/default/core/composition/create-composition.js.map +1 -1
- package/dist/default/core/composition/share-signals.js +13 -11
- package/dist/default/core/composition/share-signals.js.map +1 -1
- package/dist/default/core/machine.js.map +1 -1
- package/dist/default/core/reactors/create-machine-reactor.js.map +1 -1
- package/dist/default/core/signals/effect.js.map +1 -1
- package/dist/default/core/signals/primitives.js.map +1 -1
- package/dist/default/core/tasks/task.js.map +1 -1
- package/dist/default/hls.js +4 -1
- package/dist/default/media/abr/quality-selection.js +6 -68
- package/dist/default/media/abr/quality-selection.js.map +1 -1
- package/dist/default/media/buffer/back-buffer.js.map +1 -1
- package/dist/default/media/buffer/forward-buffer.js +37 -3
- package/dist/default/media/buffer/forward-buffer.js.map +1 -1
- package/dist/default/media/dom/capabilities.js +70 -0
- package/dist/default/media/dom/capabilities.js.map +1 -0
- package/dist/default/media/dom/mse/append-segment.js.map +1 -1
- package/dist/default/media/dom/mse/buffer-flusher.js.map +1 -1
- package/dist/default/media/dom/mse/duration.js +27 -10
- package/dist/default/media/dom/mse/duration.js.map +1 -1
- package/dist/default/media/dom/mse/end-of-stream.js.map +1 -1
- package/dist/default/media/dom/mse/mediasource-setup.js +68 -17
- package/dist/default/media/dom/mse/mediasource-setup.js.map +1 -1
- package/dist/default/media/dom/text/resolve-vtt-segment.js +1 -7
- package/dist/default/media/dom/text/resolve-vtt-segment.js.map +1 -1
- package/dist/default/media/dom/text/text-track-slots.js +3 -3
- package/dist/default/media/dom/text/text-track-slots.js.map +1 -1
- package/dist/default/media/hls/parse-attributes.js +17 -3
- package/dist/default/media/hls/parse-attributes.js.map +1 -1
- package/dist/default/media/hls/parse-media-playlist.js +29 -1
- package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
- package/dist/default/media/hls/parse-multivariant.js +15 -6
- package/dist/default/media/hls/parse-multivariant.js.map +1 -1
- package/dist/default/media/hls/resolve-url.js.map +1 -1
- package/dist/default/media/media-tracks/media-tracks.js +101 -0
- package/dist/default/media/media-tracks/media-tracks.js.map +1 -0
- package/dist/default/media/mp4/box.js +58 -0
- package/dist/default/media/mp4/box.js.map +1 -0
- package/dist/default/media/mp4/timestamp-origin.js +107 -0
- package/dist/default/media/mp4/timestamp-origin.js.map +1 -0
- package/dist/default/media/primitives/select-tracks.js +98 -13
- package/dist/default/media/primitives/select-tracks.js.map +1 -1
- package/dist/default/media/text/parse-vtt-timestamp-map.js +42 -0
- package/dist/default/media/text/parse-vtt-timestamp-map.js.map +1 -0
- package/dist/default/media/text/resolve-vtt-metadata.js +23 -0
- package/dist/default/media/text/resolve-vtt-metadata.js.map +1 -0
- package/dist/default/media/types/index.js.map +1 -1
- package/dist/default/media/utils/cdn.js +56 -0
- package/dist/default/media/utils/cdn.js.map +1 -0
- package/dist/default/media/utils/preload.js.map +1 -1
- package/dist/default/media/utils/track-selection.js +1 -1
- package/dist/default/media/utils/track-selection.js.map +1 -1
- package/dist/default/media/utils/tracks.js +31 -4
- package/dist/default/media/utils/tracks.js.map +1 -1
- package/dist/default/media-tracks.js +2 -0
- package/dist/default/network/bandwidth-estimator.js.map +1 -1
- package/dist/default/network/chunked-stream-iterable.js.map +1 -1
- package/dist/default/network/ewma.js +1 -1
- package/dist/default/network/ewma.js.map +1 -1
- package/dist/default/network/fetch.js +7 -1
- package/dist/default/network/fetch.js.map +1 -1
- package/dist/default/playback/actors/dom/segment-loader.js +103 -106
- package/dist/default/playback/actors/dom/segment-loader.js.map +1 -1
- package/dist/default/playback/actors/dom/source-buffer.js +3 -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 +30 -16
- package/dist/default/playback/actors/text-track-segment-loader.js.map +1 -1
- package/dist/default/playback/behaviors/calculate-presentation-duration.js +21 -43
- package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -1
- package/dist/default/playback/behaviors/derive-cdn-priority.js +68 -0
- package/dist/default/playback/behaviors/derive-cdn-priority.js.map +1 -0
- package/dist/default/playback/behaviors/dom/airplay.js +211 -0
- package/dist/default/playback/behaviors/dom/airplay.js.map +1 -0
- package/dist/default/playback/behaviors/dom/apply-start-position.js +93 -0
- package/dist/default/playback/behaviors/dom/apply-start-position.js.map +1 -0
- package/dist/default/playback/behaviors/dom/end-of-stream.js +25 -102
- package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
- package/dist/default/playback/behaviors/dom/load-segments.js +6 -2
- package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -1
- package/dist/default/playback/behaviors/dom/recover-end-stall.js +77 -0
- package/dist/default/playback/behaviors/dom/recover-end-stall.js.map +1 -0
- package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +50 -18
- package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
- package/dist/default/playback/behaviors/dom/setup-mediasource.js +36 -11
- package/dist/default/playback/behaviors/dom/setup-mediasource.js.map +1 -1
- package/dist/default/playback/behaviors/dom/setup-text-track-actors.js +11 -25
- package/dist/default/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
- package/dist/default/playback/behaviors/dom/sync-text-tracks.js +43 -18
- package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-current-time.js +6 -0
- package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-load-triggers.js +4 -4
- package/dist/default/playback/behaviors/dom/track-load-triggers.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-playback-rate.js.map +1 -1
- package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +3 -2
- package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
- package/dist/default/playback/behaviors/establish-start-media-time.js +123 -0
- package/dist/default/playback/behaviors/establish-start-media-time.js.map +1 -0
- package/dist/default/playback/behaviors/resolve-presentation.js.map +1 -1
- package/dist/default/playback/behaviors/resolve-track.js +45 -22
- package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
- package/dist/default/playback/behaviors/select-tracks.js +46 -77
- package/dist/default/playback/behaviors/select-tracks.js.map +1 -1
- package/dist/default/playback/behaviors/setup-failover-monitor.js +66 -0
- package/dist/default/playback/behaviors/setup-failover-monitor.js.map +1 -0
- package/dist/default/playback/behaviors/sync-preload.js.map +1 -1
- package/dist/default/playback/behaviors/track-switching.js +403 -0
- package/dist/default/playback/behaviors/track-switching.js.map +1 -0
- package/dist/default/playback/engines/background-video/adapter.js +158 -0
- package/dist/default/playback/engines/background-video/adapter.js.map +1 -0
- package/dist/default/playback/engines/background-video/engine.js +75 -0
- package/dist/default/playback/engines/background-video/engine.js.map +1 -0
- package/dist/default/playback/engines/hls/adapter-audio-only.js +124 -0
- package/dist/default/playback/engines/hls/adapter-audio-only.js.map +1 -0
- package/dist/default/playback/engines/hls/adapter.js +20 -11
- package/dist/default/playback/engines/hls/adapter.js.map +1 -1
- package/dist/default/playback/engines/hls/engine-audio-only.js +94 -0
- package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -0
- package/dist/default/playback/engines/hls/engine.js +38 -8
- package/dist/default/playback/engines/hls/engine.js.map +1 -1
- package/dist/default/playback/primitives/failover-fetch.js +42 -0
- package/dist/default/playback/primitives/failover-fetch.js.map +1 -0
- package/dist/default/playback/primitives/head-peek.js +44 -0
- package/dist/default/playback/primitives/head-peek.js.map +1 -0
- package/dist/default/playback/primitives/relocation-pipelines.js +203 -0
- package/dist/default/playback/primitives/relocation-pipelines.js.map +1 -0
- package/dist/default/playback/primitives/segment-load-pipeline.js +96 -0
- package/dist/default/playback/primitives/segment-load-pipeline.js.map +1 -0
- package/dist/default/playback/primitives/text-segment-load-pipeline.js +37 -0
- package/dist/default/playback/primitives/text-segment-load-pipeline.js.map +1 -0
- package/dist/{dev/playback/behaviors → default/playback/primitives}/track-types.js +1 -1
- package/dist/default/playback/primitives/track-types.js.map +1 -0
- package/dist/dev/background-video.d.ts +3 -0
- package/dist/dev/background-video.js +3 -0
- package/dist/dev/core/actors/actor.d.ts +0 -1
- package/dist/dev/core/actors/actor.d.ts.map +1 -1
- package/dist/dev/core/actors/create-machine-actor.d.ts +9 -6
- package/dist/dev/core/actors/create-machine-actor.d.ts.map +1 -1
- package/dist/dev/core/actors/create-machine-actor.js.map +1 -1
- package/dist/dev/core/actors/create-transition-actor.d.ts +0 -1
- package/dist/dev/core/actors/create-transition-actor.d.ts.map +1 -1
- package/dist/dev/core/actors/create-transition-actor.js.map +1 -1
- package/dist/dev/core/composition/create-composition.d.ts +10 -11
- package/dist/dev/core/composition/create-composition.d.ts.map +1 -1
- package/dist/dev/core/composition/create-composition.js +2 -2
- package/dist/dev/core/composition/create-composition.js.map +1 -1
- package/dist/dev/core/composition/share-signals.d.ts +11 -10
- package/dist/dev/core/composition/share-signals.d.ts.map +1 -1
- package/dist/dev/core/composition/share-signals.js +13 -11
- package/dist/dev/core/composition/share-signals.js.map +1 -1
- package/dist/dev/core/machine.d.ts +1 -0
- package/dist/dev/core/machine.d.ts.map +1 -1
- package/dist/dev/core/machine.js.map +1 -1
- package/dist/dev/core/reactors/create-machine-reactor.d.ts +2 -2
- package/dist/dev/core/reactors/create-machine-reactor.d.ts.map +1 -1
- package/dist/dev/core/reactors/create-machine-reactor.js.map +1 -1
- package/dist/dev/core/signals/effect.d.ts.map +1 -1
- package/dist/dev/core/signals/effect.js.map +1 -1
- package/dist/dev/core/signals/primitives.d.ts +1 -2
- package/dist/dev/core/signals/primitives.d.ts.map +1 -1
- package/dist/dev/core/signals/primitives.js.map +1 -1
- package/dist/dev/core/tasks/task.d.ts +2 -2
- package/dist/dev/core/tasks/task.d.ts.map +1 -1
- package/dist/dev/core/tasks/task.js.map +1 -1
- package/dist/dev/hls.d.ts +5 -1
- package/dist/dev/hls.js +4 -1
- package/dist/dev/index.d.ts +0 -1
- package/dist/dev/index.d.ts.map +1 -1
- package/dist/dev/media/abr/quality-selection.d.ts.map +1 -1
- package/dist/dev/media/abr/quality-selection.js +6 -68
- package/dist/dev/media/abr/quality-selection.js.map +1 -1
- package/dist/dev/media/buffer/back-buffer.d.ts.map +1 -1
- package/dist/dev/media/buffer/back-buffer.js.map +1 -1
- package/dist/dev/media/buffer/forward-buffer.d.ts.map +1 -1
- package/dist/dev/media/buffer/forward-buffer.js +37 -3
- package/dist/dev/media/buffer/forward-buffer.js.map +1 -1
- package/dist/dev/media/dom/capabilities.js +70 -0
- package/dist/dev/media/dom/capabilities.js.map +1 -0
- package/dist/dev/media/dom/mse/append-segment.d.ts +3 -9
- package/dist/dev/media/dom/mse/append-segment.d.ts.map +1 -1
- package/dist/dev/media/dom/mse/append-segment.js.map +1 -1
- package/dist/dev/media/dom/mse/buffer-flusher.d.ts.map +1 -1
- package/dist/dev/media/dom/mse/buffer-flusher.js.map +1 -1
- package/dist/dev/media/dom/mse/duration.js +27 -10
- package/dist/dev/media/dom/mse/duration.js.map +1 -1
- package/dist/dev/media/dom/mse/end-of-stream.js.map +1 -1
- package/dist/dev/media/dom/mse/mediasource-setup.js +68 -17
- package/dist/dev/media/dom/mse/mediasource-setup.js.map +1 -1
- package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts +0 -6
- package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts.map +1 -1
- package/dist/dev/media/dom/text/resolve-vtt-segment.js +1 -7
- package/dist/dev/media/dom/text/resolve-vtt-segment.js.map +1 -1
- package/dist/dev/media/dom/text/text-track-slots.d.ts +0 -1
- package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -1
- package/dist/dev/media/dom/text/text-track-slots.js +3 -3
- package/dist/dev/media/dom/text/text-track-slots.js.map +1 -1
- package/dist/dev/media/hls/parse-attributes.js +17 -3
- package/dist/dev/media/hls/parse-attributes.js.map +1 -1
- package/dist/dev/media/hls/parse-media-playlist.js +29 -1
- package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
- package/dist/dev/media/hls/parse-multivariant.js +15 -6
- package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
- package/dist/dev/media/hls/resolve-url.js.map +1 -1
- package/dist/dev/media/media-tracks/media-tracks.d.ts +51 -0
- package/dist/dev/media/media-tracks/media-tracks.d.ts.map +1 -0
- package/dist/dev/media/media-tracks/media-tracks.js +101 -0
- package/dist/dev/media/media-tracks/media-tracks.js.map +1 -0
- package/dist/dev/media/mp4/box.js +58 -0
- package/dist/dev/media/mp4/box.js.map +1 -0
- package/dist/dev/media/mp4/timestamp-origin.js +107 -0
- package/dist/dev/media/mp4/timestamp-origin.js.map +1 -0
- package/dist/dev/media/primitives/select-tracks.d.ts +35 -0
- package/dist/dev/media/primitives/select-tracks.d.ts.map +1 -0
- package/dist/dev/media/primitives/select-tracks.js +98 -13
- package/dist/dev/media/primitives/select-tracks.js.map +1 -1
- package/dist/dev/media/text/parse-vtt-timestamp-map.js +42 -0
- package/dist/dev/media/text/parse-vtt-timestamp-map.js.map +1 -0
- package/dist/dev/media/text/resolve-vtt-metadata.js +23 -0
- package/dist/dev/media/text/resolve-vtt-metadata.js.map +1 -0
- package/dist/dev/media/types/index.d.ts +69 -2
- package/dist/dev/media/types/index.d.ts.map +1 -1
- package/dist/dev/media/types/index.js.map +1 -1
- package/dist/dev/media/utils/cdn.d.ts +13 -0
- package/dist/dev/media/utils/cdn.d.ts.map +1 -0
- package/dist/dev/media/utils/cdn.js +56 -0
- package/dist/dev/media/utils/cdn.js.map +1 -0
- package/dist/dev/media/utils/preload.js.map +1 -1
- package/dist/dev/media/utils/track-selection.js +1 -1
- package/dist/dev/media/utils/track-selection.js.map +1 -1
- package/dist/dev/media/utils/tracks.js +31 -4
- package/dist/dev/media/utils/tracks.js.map +1 -1
- package/dist/dev/media-tracks.d.ts +3 -0
- package/dist/dev/media-tracks.js +2 -0
- package/dist/dev/network/bandwidth-estimator.d.ts.map +1 -1
- package/dist/dev/network/bandwidth-estimator.js.map +1 -1
- package/dist/dev/network/chunked-stream-iterable.js.map +1 -1
- package/dist/dev/network/ewma.js +1 -1
- package/dist/dev/network/ewma.js.map +1 -1
- package/dist/dev/network/fetch.js +7 -1
- package/dist/dev/network/fetch.js.map +1 -1
- package/dist/dev/playback/actors/dom/segment-loader.d.ts +16 -4
- package/dist/dev/playback/actors/dom/segment-loader.d.ts.map +1 -1
- package/dist/dev/playback/actors/dom/segment-loader.js +103 -106
- package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -1
- package/dist/dev/playback/actors/dom/source-buffer.d.ts +10 -25
- package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -1
- package/dist/dev/playback/actors/dom/source-buffer.js +3 -1
- package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -1
- package/dist/dev/playback/actors/dom/text-tracks.d.ts +3 -1
- package/dist/dev/playback/actors/dom/text-tracks.js.map +1 -1
- package/dist/dev/playback/actors/text-track-segment-loader.d.ts +7 -12
- package/dist/dev/playback/actors/text-track-segment-loader.d.ts.map +1 -1
- package/dist/dev/playback/actors/text-track-segment-loader.js +30 -16
- package/dist/dev/playback/actors/text-track-segment-loader.js.map +1 -1
- package/dist/dev/playback/actors/text-tracks.d.ts +3 -12
- package/dist/dev/playback/actors/text-tracks.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +11 -2
- package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/calculate-presentation-duration.js +21 -43
- package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -1
- package/dist/dev/playback/behaviors/derive-cdn-priority.js +68 -0
- package/dist/dev/playback/behaviors/derive-cdn-priority.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/airplay.js +211 -0
- package/dist/dev/playback/behaviors/dom/airplay.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/apply-start-position.js +93 -0
- package/dist/dev/playback/behaviors/dom/apply-start-position.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/end-of-stream.js +25 -102
- package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/load-segments.d.ts +14 -4
- package/dist/dev/playback/behaviors/dom/load-segments.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/load-segments.js +6 -2
- package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/recover-end-stall.js +77 -0
- package/dist/dev/playback/behaviors/dom/recover-end-stall.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +50 -18
- package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/setup-mediasource.js +36 -11
- package/dist/dev/playback/behaviors/dom/setup-mediasource.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts +16 -19
- 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 +11 -25
- package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/sync-text-tracks.js +43 -18
- package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-current-time.d.ts +1 -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 +6 -0
- package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts +1 -2
- package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-load-triggers.js +4 -4
- package/dist/dev/playback/behaviors/dom/track-load-triggers.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts +1 -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.map +1 -1
- package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +3 -2
- package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
- package/dist/dev/playback/behaviors/establish-start-media-time.d.ts +31 -0
- package/dist/dev/playback/behaviors/establish-start-media-time.d.ts.map +1 -0
- package/dist/dev/playback/behaviors/establish-start-media-time.js +123 -0
- package/dist/dev/playback/behaviors/establish-start-media-time.js.map +1 -0
- package/dist/dev/playback/behaviors/resolve-presentation.d.ts +2 -1
- package/dist/dev/playback/behaviors/resolve-presentation.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/resolve-presentation.js.map +1 -1
- package/dist/dev/playback/behaviors/resolve-track.js +45 -22
- package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
- package/dist/dev/playback/behaviors/select-tracks.d.ts +14 -0
- package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -0
- package/dist/dev/playback/behaviors/select-tracks.js +46 -77
- package/dist/dev/playback/behaviors/select-tracks.js.map +1 -1
- package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts +14 -0
- package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts.map +1 -0
- package/dist/dev/playback/behaviors/setup-failover-monitor.js +66 -0
- package/dist/dev/playback/behaviors/setup-failover-monitor.js.map +1 -0
- package/dist/dev/playback/behaviors/sync-preload.js.map +1 -1
- package/dist/dev/playback/behaviors/track-switching.js +403 -0
- package/dist/dev/playback/behaviors/track-switching.js.map +1 -0
- package/dist/dev/playback/engines/background-video/adapter.d.ts +59 -0
- package/dist/dev/playback/engines/background-video/adapter.d.ts.map +1 -0
- package/dist/dev/playback/engines/background-video/adapter.js +158 -0
- package/dist/dev/playback/engines/background-video/adapter.js.map +1 -0
- package/dist/dev/playback/engines/background-video/engine.d.ts +106 -0
- package/dist/dev/playback/engines/background-video/engine.d.ts.map +1 -0
- package/dist/dev/playback/engines/background-video/engine.js +75 -0
- package/dist/dev/playback/engines/background-video/engine.js.map +1 -0
- package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +46 -0
- package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +1 -0
- package/dist/dev/playback/engines/hls/adapter-audio-only.js +124 -0
- package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +1 -0
- package/dist/dev/playback/engines/hls/adapter.d.ts +3 -5
- package/dist/dev/playback/engines/hls/adapter.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/adapter.js +20 -11
- package/dist/dev/playback/engines/hls/adapter.js.map +1 -1
- package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +156 -0
- package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -0
- package/dist/dev/playback/engines/hls/engine-audio-only.js +94 -0
- package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -0
- package/dist/dev/playback/engines/hls/engine.d.ts +109 -4
- package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/engine.js +38 -8
- package/dist/dev/playback/engines/hls/engine.js.map +1 -1
- package/dist/dev/playback/primitives/derive-start-media-time.d.ts +16 -0
- package/dist/dev/playback/primitives/derive-start-media-time.d.ts.map +1 -0
- package/dist/dev/playback/primitives/failover-fetch.js +42 -0
- package/dist/dev/playback/primitives/failover-fetch.js.map +1 -0
- package/dist/dev/playback/primitives/head-peek.js +44 -0
- package/dist/dev/playback/primitives/head-peek.js.map +1 -0
- package/dist/dev/playback/primitives/relocation-pipelines.js +203 -0
- package/dist/dev/playback/primitives/relocation-pipelines.js.map +1 -0
- package/dist/dev/playback/primitives/segment-load-pipeline.d.ts +2 -0
- package/dist/dev/playback/primitives/segment-load-pipeline.js +96 -0
- package/dist/dev/playback/primitives/segment-load-pipeline.js.map +1 -0
- package/dist/dev/playback/primitives/source-buffer-messages.d.ts +43 -0
- package/dist/dev/playback/primitives/source-buffer-messages.d.ts.map +1 -0
- package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts +65 -0
- package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts.map +1 -0
- package/dist/dev/playback/primitives/text-segment-load-pipeline.js +37 -0
- package/dist/dev/playback/primitives/text-segment-load-pipeline.js.map +1 -0
- package/dist/dev/playback/primitives/text-track-messages.d.ts +14 -0
- package/dist/dev/playback/primitives/text-track-messages.d.ts.map +1 -0
- package/dist/{default/playback/behaviors → dev/playback/primitives}/track-types.js +1 -1
- package/dist/dev/playback/primitives/track-types.js.map +1 -0
- package/package.json +17 -7
- package/dist/default/playback/behaviors/quality-switching.js +0 -96
- package/dist/default/playback/behaviors/quality-switching.js.map +0 -1
- package/dist/default/playback/behaviors/track-types.js.map +0 -1
- package/dist/dev/playback/behaviors/quality-switching.js +0 -96
- package/dist/dev/playback/behaviors/quality-switching.js.map +0 -1
- package/dist/dev/playback/behaviors/track-types.js.map +0 -1
|
@@ -53,19 +53,19 @@ function findTrackById(presentation, trackId) {
|
|
|
53
53
|
function findResolvedTextTrack(presentation, trackId) {
|
|
54
54
|
if (!presentation || !trackId) return void 0;
|
|
55
55
|
const track = findTrack(presentation, "text", trackId);
|
|
56
|
-
if (
|
|
56
|
+
if (track?.type !== "text" || !isResolvedTrack(track)) return void 0;
|
|
57
57
|
return track;
|
|
58
58
|
}
|
|
59
59
|
function findResolvedVideoTrack(presentation, trackId) {
|
|
60
60
|
if (!presentation || !trackId) return void 0;
|
|
61
61
|
const track = findTrack(presentation, "video", trackId);
|
|
62
|
-
if (
|
|
62
|
+
if (track?.type !== "video" || !isResolvedTrack(track)) return void 0;
|
|
63
63
|
return track;
|
|
64
64
|
}
|
|
65
65
|
function findResolvedAudioTrack(presentation, trackId) {
|
|
66
66
|
if (!presentation || !trackId) return void 0;
|
|
67
67
|
const track = findTrack(presentation, "audio", trackId);
|
|
68
|
-
if (
|
|
68
|
+
if (track?.type !== "audio" || !isResolvedTrack(track)) return void 0;
|
|
69
69
|
return track;
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
@@ -81,6 +81,33 @@ function hasCodecs(track) {
|
|
|
81
81
|
return !!track && "codecs" in track && !!track.codecs?.length;
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
+
* Set `mimeType` on every track of one `type` (immutably). Used to propagate a
|
|
85
|
+
* detected container across a type's renditions: an ABR ladder is the same
|
|
86
|
+
* content at different bitrates, so one rendition's container holds for all of
|
|
87
|
+
* them — capability probing + SourceBuffer setup then get the right MIME for the
|
|
88
|
+
* whole type from a single resolved media playlist, without fetching the rest.
|
|
89
|
+
*
|
|
90
|
+
* Scoped to one type on purpose: propagating *across* audio/video would be wrong
|
|
91
|
+
* for mixed-container sources (e.g. muxed-TS video + raw-`.aac` audio) and races
|
|
92
|
+
* concurrent per-type resolution. Same-type writes are disjoint and safe.
|
|
93
|
+
* Idempotent — tracks already at `mimeType` are left as-is.
|
|
94
|
+
*/
|
|
95
|
+
function applyContainerMimeType(presentation, type, mimeType) {
|
|
96
|
+
return {
|
|
97
|
+
...presentation,
|
|
98
|
+
selectionSets: presentation.selectionSets.map((selectionSet) => selectionSet.type === type ? {
|
|
99
|
+
...selectionSet,
|
|
100
|
+
switchingSets: selectionSet.switchingSets.map((switchingSet) => ({
|
|
101
|
+
...switchingSet,
|
|
102
|
+
tracks: switchingSet.tracks.map((track) => track.mimeType === mimeType ? track : {
|
|
103
|
+
...track,
|
|
104
|
+
mimeType
|
|
105
|
+
})
|
|
106
|
+
}))
|
|
107
|
+
} : selectionSet)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
84
111
|
* Updates a track within a presentation (immutably). Generic — works for
|
|
85
112
|
* video, audio, or text tracks.
|
|
86
113
|
*/
|
|
@@ -98,6 +125,6 @@ function updateTrackInPresentation(presentation, resolvedTrack) {
|
|
|
98
125
|
};
|
|
99
126
|
}
|
|
100
127
|
//#endregion
|
|
101
|
-
export { findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack, findTrack, findTrackById, getTracksByType, hasCodecs, updateTrackInPresentation };
|
|
128
|
+
export { applyContainerMimeType, findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack, findTrack, findTrackById, getTracksByType, hasCodecs, updateTrackInPresentation };
|
|
102
129
|
|
|
103
130
|
//# sourceMappingURL=tracks.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracks.js","names":[],"sources":["../../../../src/media/utils/tracks.ts"],"sourcesContent":["import type {\n AudioTrack,\n MaybeResolvedPresentation,\n PartiallyResolvedTrack,\n Presentation,\n ResolvedTrack,\n TextTrack,\n TrackType,\n VideoTrack,\n} from '../types';\nimport { isResolvedTrack } from '../types';\n\n/**\n * Get the tracks of the given type from a presentation's first switching set.\n *\n * Returns `[]` when the presentation is unresolved, when no selection set of\n * `type` exists, or when its first switching set is empty. Returned tracks may\n * be partially resolved (URL only) or fully resolved (with segments) — callers\n * narrow as needed.\n *\n * The \"first switching set\" assumption matches the rest of the codebase\n * (HLS typically has one switching set per type); multi-group / multi-period\n * support would generalize this.\n */\nexport function getTracksByType(\n presentation: MaybeResolvedPresentation,\n type: TrackType\n): readonly (PartiallyResolvedTrack | ResolvedTrack)[] {\n return presentation.selectionSets?.find(({ type: t }) => t === type)?.switchingSets[0]?.tracks ?? [];\n}\n\n/**\n * Find a track of the given type and id within a presentation.\n *\n * Returns the matching track from the first switching set of the matching\n * selection set, or `undefined` if either is missing. The returned track may\n * be partially resolved (URL only) or fully resolved (with segments) — callers\n * narrow as needed.\n */\nexport function findTrack(\n presentation: MaybeResolvedPresentation,\n type: TrackType,\n trackId: string\n): PartiallyResolvedTrack | ResolvedTrack | undefined {\n return getTracksByType(presentation, type).find(({ id }) => id === trackId);\n}\n\n/**\n * Find a track by id across all selection sets in a presentation, without\n * knowing its type up front. Used when the caller has a track id obtained\n * from a downstream consumer (e.g. `SourceBufferActor.initTrackId`) and\n * needs to locate the corresponding track in the presentation.\n *\n * Track ids are unique within a presentation per the HLS spec; the first\n * match wins.\n */\nexport function findTrackById(\n presentation: MaybeResolvedPresentation,\n trackId: string\n): PartiallyResolvedTrack | ResolvedTrack | undefined {\n for (const selectionSet of presentation.selectionSets ?? []) {\n const track = selectionSet.switchingSets[0]?.tracks.find(({ id }) => id === trackId);\n if (track) return track;\n }\n return undefined;\n}\n\n/**\n * Find a text track of the given id within a presentation and narrow it to\n * the fully-resolved `TextTrack` shape (segments populated). Returns\n * `undefined` if no track matches the id, the matching track isn't a text\n * track, or it hasn't been resolved yet.\n *\n * The segments-non-empty check stays at the call site — a resolved track\n * with zero segments is a valid state, distinct from \"ready to load.\"\n */\nexport function findResolvedTextTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): TextTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'text', trackId);\n // `findTrack` returns the wide union; narrow via discriminant before\n // applying `isResolvedTrack`'s text-specific overload.\n if (
|
|
1
|
+
{"version":3,"file":"tracks.js","names":[],"sources":["../../../../src/media/utils/tracks.ts"],"sourcesContent":["import type {\n AudioTrack,\n MaybeResolvedPresentation,\n PartiallyResolvedTrack,\n Presentation,\n ResolvedTrack,\n TextTrack,\n TrackType,\n VideoTrack,\n} from '../types';\nimport { isResolvedTrack } from '../types';\n\n/**\n * Get the tracks of the given type from a presentation's first switching set.\n *\n * Returns `[]` when the presentation is unresolved, when no selection set of\n * `type` exists, or when its first switching set is empty. Returned tracks may\n * be partially resolved (URL only) or fully resolved (with segments) — callers\n * narrow as needed.\n *\n * The \"first switching set\" assumption matches the rest of the codebase\n * (HLS typically has one switching set per type); multi-group / multi-period\n * support would generalize this.\n */\nexport function getTracksByType(\n presentation: MaybeResolvedPresentation,\n type: TrackType\n): readonly (PartiallyResolvedTrack | ResolvedTrack)[] {\n return presentation.selectionSets?.find(({ type: t }) => t === type)?.switchingSets[0]?.tracks ?? [];\n}\n\n/**\n * Find a track of the given type and id within a presentation.\n *\n * Returns the matching track from the first switching set of the matching\n * selection set, or `undefined` if either is missing. The returned track may\n * be partially resolved (URL only) or fully resolved (with segments) — callers\n * narrow as needed.\n */\nexport function findTrack(\n presentation: MaybeResolvedPresentation,\n type: TrackType,\n trackId: string\n): PartiallyResolvedTrack | ResolvedTrack | undefined {\n return getTracksByType(presentation, type).find(({ id }) => id === trackId);\n}\n\n/**\n * Find a track by id across all selection sets in a presentation, without\n * knowing its type up front. Used when the caller has a track id obtained\n * from a downstream consumer (e.g. `SourceBufferActor.initTrackId`) and\n * needs to locate the corresponding track in the presentation.\n *\n * Track ids are unique within a presentation per the HLS spec; the first\n * match wins.\n */\nexport function findTrackById(\n presentation: MaybeResolvedPresentation,\n trackId: string\n): PartiallyResolvedTrack | ResolvedTrack | undefined {\n for (const selectionSet of presentation.selectionSets ?? []) {\n const track = selectionSet.switchingSets[0]?.tracks.find(({ id }) => id === trackId);\n if (track) return track;\n }\n return undefined;\n}\n\n/**\n * Find a text track of the given id within a presentation and narrow it to\n * the fully-resolved `TextTrack` shape (segments populated). Returns\n * `undefined` if no track matches the id, the matching track isn't a text\n * track, or it hasn't been resolved yet.\n *\n * The segments-non-empty check stays at the call site — a resolved track\n * with zero segments is a valid state, distinct from \"ready to load.\"\n */\nexport function findResolvedTextTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): TextTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'text', trackId);\n // `findTrack` returns the wide union; narrow via discriminant before\n // applying `isResolvedTrack`'s text-specific overload.\n if (track?.type !== 'text' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\nexport function findResolvedVideoTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): VideoTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'video', trackId);\n if (track?.type !== 'video' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\nexport function findResolvedAudioTrack(\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n): AudioTrack | undefined {\n if (!presentation || !trackId) return undefined;\n const track = findTrack(presentation, 'audio', trackId);\n if (track?.type !== 'audio' || !isResolvedTrack(track)) return undefined;\n return track;\n}\n\n/**\n * Whether a track carries a non-empty `codecs` array. Both partially-\n * resolved and fully-resolved tracks may carry codecs — they come from\n * the multivariant playlist's `EXT-X-STREAM-INF` line, not from the\n * per-type media playlist — so this works at either resolution stage.\n *\n * `TextTrack` doesn't declare a `codecs` field; the `'codecs' in track`\n * check narrows it out for the false branch.\n */\nexport function hasCodecs(track: PartiallyResolvedTrack | ResolvedTrack | undefined): boolean {\n return !!track && 'codecs' in track && !!track.codecs?.length;\n}\n\n/**\n * Set `mimeType` on every track of one `type` (immutably). Used to propagate a\n * detected container across a type's renditions: an ABR ladder is the same\n * content at different bitrates, so one rendition's container holds for all of\n * them — capability probing + SourceBuffer setup then get the right MIME for the\n * whole type from a single resolved media playlist, without fetching the rest.\n *\n * Scoped to one type on purpose: propagating *across* audio/video would be wrong\n * for mixed-container sources (e.g. muxed-TS video + raw-`.aac` audio) and races\n * concurrent per-type resolution. Same-type writes are disjoint and safe.\n * Idempotent — tracks already at `mimeType` are left as-is.\n */\nexport function applyContainerMimeType(presentation: Presentation, type: TrackType, mimeType: string): Presentation {\n return {\n ...presentation,\n selectionSets: presentation.selectionSets.map((selectionSet) =>\n selectionSet.type === type\n ? {\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) =>\n track.mimeType === mimeType ? track : { ...track, mimeType }\n ),\n })),\n }\n : selectionSet\n ),\n } as Presentation;\n}\n\n/**\n * Updates a track within a presentation (immutably). Generic — works for\n * video, audio, or text tracks.\n */\nexport function updateTrackInPresentation<T extends ResolvedTrack>(\n presentation: Presentation,\n resolvedTrack: T\n): Presentation {\n const trackId = resolvedTrack.id;\n return {\n ...presentation,\n selectionSets: presentation.selectionSets.map((selectionSet) => ({\n ...selectionSet,\n switchingSets: selectionSet.switchingSets.map((switchingSet) => ({\n ...switchingSet,\n tracks: switchingSet.tracks.map((track) => (track.id === trackId ? resolvedTrack : track)),\n })),\n })),\n } as Presentation;\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,SAAgB,gBACd,cACA,MACqD;CACrD,OAAO,aAAa,eAAe,MAAM,EAAE,MAAM,QAAQ,MAAM,IAAI,CAAC,EAAE,cAAc,EAAE,EAAE,UAAU,CAAC;AACrG;;;;;;;;;AAUA,SAAgB,UACd,cACA,MACA,SACoD;CACpD,OAAO,gBAAgB,cAAc,IAAI,CAAC,CAAC,MAAM,EAAE,SAAS,OAAO,OAAO;AAC5E;;;;;;;;;;AAWA,SAAgB,cACd,cACA,SACoD;CACpD,KAAK,MAAM,gBAAgB,aAAa,iBAAiB,CAAC,GAAG;EAC3D,MAAM,QAAQ,aAAa,cAAc,EAAE,EAAE,OAAO,MAAM,EAAE,SAAS,OAAO,OAAO;EACnF,IAAI,OAAO,OAAO;CACpB;AAEF;;;;;;;;;;AAWA,SAAgB,sBACd,cACA,SACuB;CACvB,IAAI,CAAC,gBAAgB,CAAC,SAAS,OAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,QAAQ,OAAO;CAGrD,IAAI,OAAO,SAAS,UAAU,CAAC,gBAAgB,KAAK,GAAG,OAAO,KAAA;CAC9D,OAAO;AACT;AAEA,SAAgB,uBACd,cACA,SACwB;CACxB,IAAI,CAAC,gBAAgB,CAAC,SAAS,OAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,SAAS,OAAO;CACtD,IAAI,OAAO,SAAS,WAAW,CAAC,gBAAgB,KAAK,GAAG,OAAO,KAAA;CAC/D,OAAO;AACT;AAEA,SAAgB,uBACd,cACA,SACwB;CACxB,IAAI,CAAC,gBAAgB,CAAC,SAAS,OAAO,KAAA;CACtC,MAAM,QAAQ,UAAU,cAAc,SAAS,OAAO;CACtD,IAAI,OAAO,SAAS,WAAW,CAAC,gBAAgB,KAAK,GAAG,OAAO,KAAA;CAC/D,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,UAAU,OAAoE;CAC5F,OAAO,CAAC,CAAC,SAAS,YAAY,SAAS,CAAC,CAAC,MAAM,QAAQ;AACzD;;;;;;;;;;;;;AAcA,SAAgB,uBAAuB,cAA4B,MAAiB,UAAgC;CAClH,OAAO;EACL,GAAG;EACH,eAAe,aAAa,cAAc,KAAK,iBAC7C,aAAa,SAAS,OAClB;GACE,GAAG;GACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;IAC/D,GAAG;IACH,QAAQ,aAAa,OAAO,KAAK,UAC/B,MAAM,aAAa,WAAW,QAAQ;KAAE,GAAG;KAAO;IAAS,CAC7D;GACF,EAAE;EACJ,IACA,YACN;CACF;AACF;;;;;AAMA,SAAgB,0BACd,cACA,eACc;CACd,MAAM,UAAU,cAAc;CAC9B,OAAO;EACL,GAAG;EACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;GAC/D,GAAG;GACH,eAAe,aAAa,cAAc,KAAK,kBAAkB;IAC/D,GAAG;IACH,QAAQ,aAAa,OAAO,KAAK,UAAW,MAAM,OAAO,UAAU,gBAAgB,KAAM;GAC3F,EAAE;EACJ,EAAE;CACJ;AACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { dedupedAudioTracks, dedupedVideoTracks, findAudioTrackById, findVideoTrackById, frameRateToNumber, isSameAudioTrack, isSameVideoTrack, toUserAudioTrackSelection, toUserVideoTrackSelection } from "./media/media-tracks/media-tracks.js";
|
|
2
|
+
export { dedupedAudioTracks, dedupedVideoTracks, findAudioTrackById, findVideoTrackById, frameRateToNumber, isSameAudioTrack, isSameVideoTrack, toUserAudioTrackSelection, toUserVideoTrackSelection };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bandwidth-estimator.js","names":[],"sources":["../../../src/network/bandwidth-estimator.ts"],"sourcesContent":["/**\n * Dual EWMA Bandwidth Estimator\n *\n * Estimates available bandwidth using two EWMA calculations with different\n * half-lives, taking the minimum of both. This approach (from Shaka Player):\n *\n * - **Fast EWMA** (2s half-life): Reacts quickly to bandwidth drops\n * - **Slow EWMA** (5s half-life): Provides stability during fluctuations\n * - **min(fast, slow)**: Adapts down quickly, up slowly\n *\n * This naturally provides asymmetric behavior needed for good QoE:\n * avoiding stalls (quick downgrade) while preventing oscillation (slow upgrade).\n */\n\nimport { applyZeroFactor, calculateEwma } from './ewma';\n\n/**\n * Bandwidth estimator state.\n *\n * This state structure will be managed by O1 (State Container).\n * Functions in this module operate on this state immutably.\n */\nexport interface BandwidthState {\n /** Fast-moving EWMA estimate (raw, uncorrected). */\n fastEstimate: number;\n /** Total weight accumulated in fast EWMA. */\n fastTotalWeight: number;\n /** Slow-moving EWMA estimate (raw, uncorrected). */\n slowEstimate: number;\n /** Total weight accumulated in slow EWMA. */\n slowTotalWeight: number;\n /** Total bytes sampled across all valid samples. */\n bytesSampled: number;\n}\n\n/**\n * Configuration for bandwidth estimation.\n */\nexport interface BandwidthConfig {\n /** Half-life for fast EWMA in seconds. */\n fastHalfLife: number;\n /** Half-life for slow EWMA in seconds. */\n slowHalfLife: number;\n /** Minimum total bytes before trusting the estimate. */\n minTotalBytes: number;\n /** Minimum bytes per sample to count (filters TTFB-dominated samples). */\n minBytes: number;\n /** Minimum sample duration in ms (filters cached responses). */\n minDuration: number;\n}\n\n/**\n * Default bandwidth estimator configuration.\n *\n * Values match Shaka Player defaults based on experimentation.\n */\nexport const DEFAULT_BANDWIDTH_CONFIG: BandwidthConfig = {\n fastHalfLife: 2, // 2 seconds\n slowHalfLife: 5, // 5 seconds\n minTotalBytes: 128_000, // 128 KB\n minBytes: 16_000, // 16 KB\n minDuration: 5, // 5 ms\n};\n\n/**\n * Add a bandwidth sample from a segment download.\n *\n * Samples are filtered based on:\n * - Minimum bytes (filters TTFB-dominated small segments)\n * - Minimum duration (filters cached responses)\n *\n * Valid samples update both fast and slow EWMA estimates.\n *\n * @param state - Current estimator state\n * @param durationMs - Download duration in milliseconds\n * @param numBytes - Number of bytes downloaded\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns New estimator state with sample incorporated (or unchanged if filtered)\n *\n * @example\n * let state = { fastEstimate: 0, fastTotalWeight: 0, ... };\n * // Sample: 1MB in 1 second\n * state = sampleBandwidth(state, 1000, 1_000_000);\n */\nexport function sampleBandwidth(\n state: BandwidthState,\n durationMs: number,\n numBytes: number,\n config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG\n): BandwidthState {\n // Always track bytes for startup phase calculation\n const updatedBytesSampled = state.bytesSampled + numBytes;\n\n // Filter: Ignore samples below minimum bytes (TTFB-dominated)\n // Small segments' download time is mostly connection setup latency,\n // which would artificially lower our bandwidth estimate\n if (numBytes < config.minBytes) {\n return { ...state, bytesSampled: updatedBytesSampled };\n }\n\n // Filter: Ignore samples faster than minimum duration (cached responses)\n // Cached responses load nearly instantly and would artificially inflate\n // our bandwidth estimate\n if (durationMs < config.minDuration) {\n return { ...state, bytesSampled: updatedBytesSampled };\n }\n\n // Calculate bandwidth in bits per second\n // Formula: (bytes * 8 bits/byte * 1000 ms/s) / ms = bits/s\n const bandwidth = (8000 * numBytes) / durationMs;\n\n // Weight by duration in seconds\n // Longer downloads are more reliable indicators of true bandwidth\n const weight = durationMs / 1000;\n\n return {\n fastEstimate: calculateEwma(state.fastEstimate, bandwidth, weight, config.fastHalfLife),\n fastTotalWeight: state.fastTotalWeight + weight,\n slowEstimate: calculateEwma(state.slowEstimate, bandwidth, weight, config.slowHalfLife),\n slowTotalWeight: state.slowTotalWeight + weight,\n bytesSampled: updatedBytesSampled,\n };\n}\n\n/**\n * Get the current bandwidth estimate.\n *\n * Returns the **minimum** of the fast and slow EWMA estimates.\n * This provides the key asymmetric behavior:\n * - When bandwidth drops, fast EWMA reacts first and dominates (quick adaptation)\n * - When bandwidth rises, slow EWMA lags behind and dominates (slow adaptation)\n *\n * Uses default estimate until enough data has been sampled — and when no\n * estimator state exists at all (`state === undefined`).\n *\n * @param state - Current estimator state, or `undefined` before any samples have been collected\n * @param defaultEstimate - Fallback estimate before sufficient samples (bps)\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns Bandwidth estimate in bits per second\n *\n * @example\n * const estimate = getBandwidthEstimate(state, 5_000_000); // 5 Mbps default\n */\nexport function getBandwidthEstimate(\n state: BandwidthState | undefined,\n defaultEstimate: number,\n config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG\n): number {\n // Use default until we have enough samples to trust our estimate\n if (!state || state.bytesSampled < config.minTotalBytes) {\n return defaultEstimate;\n }\n\n // Apply zero-factor correction to both estimates\n const fastEstimate = applyZeroFactor(state.fastEstimate, state.fastTotalWeight, config.fastHalfLife);\n\n const slowEstimate = applyZeroFactor(state.slowEstimate, state.slowTotalWeight, config.slowHalfLife);\n\n // Take the minimum - this is the key insight from Shaka Player\n // It naturally provides \"down quickly, up slowly\" behavior\n return Math.min(fastEstimate, slowEstimate);\n}\n\n/**\n * Check if the estimator has enough data to provide a reliable estimate.\n *\n * Requires both:\n * - Enough total bytes sampled (minTotalBytes threshold)\n * - At least one valid EWMA sample (totalWeight > 0)\n *\n * @param state - Current estimator state\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns True if we've sampled enough bytes to trust the estimate\n *\n * @example\n * if (hasGoodEstimate(state)) {\n * const estimate = getBandwidthEstimate(state, 5_000_000);\n * }\n */\nexport function hasGoodEstimate(state: BandwidthState, config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG): boolean {\n // Need enough total bytes AND at least one valid EWMA sample\n return state.bytesSampled >= config.minTotalBytes && state.fastTotalWeight > 0 && state.slowTotalWeight > 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAwDA,MAAa,2BAA4C;CACvD,cAAc;CACd,cAAc;CACd,eAAe;CACf,UAAU;CACV,aAAa;
|
|
1
|
+
{"version":3,"file":"bandwidth-estimator.js","names":[],"sources":["../../../src/network/bandwidth-estimator.ts"],"sourcesContent":["/**\n * Dual EWMA Bandwidth Estimator\n *\n * Estimates available bandwidth using two EWMA calculations with different\n * half-lives, taking the minimum of both. This approach (from Shaka Player):\n *\n * - **Fast EWMA** (2s half-life): Reacts quickly to bandwidth drops\n * - **Slow EWMA** (5s half-life): Provides stability during fluctuations\n * - **min(fast, slow)**: Adapts down quickly, up slowly\n *\n * This naturally provides asymmetric behavior needed for good QoE:\n * avoiding stalls (quick downgrade) while preventing oscillation (slow upgrade).\n */\n\nimport { applyZeroFactor, calculateEwma } from './ewma';\n\n/**\n * Bandwidth estimator state.\n *\n * This state structure will be managed by O1 (State Container).\n * Functions in this module operate on this state immutably.\n */\nexport interface BandwidthState {\n /** Fast-moving EWMA estimate (raw, uncorrected). */\n fastEstimate: number;\n /** Total weight accumulated in fast EWMA. */\n fastTotalWeight: number;\n /** Slow-moving EWMA estimate (raw, uncorrected). */\n slowEstimate: number;\n /** Total weight accumulated in slow EWMA. */\n slowTotalWeight: number;\n /** Total bytes sampled across all valid samples. */\n bytesSampled: number;\n}\n\n/**\n * Configuration for bandwidth estimation.\n */\nexport interface BandwidthConfig {\n /** Half-life for fast EWMA in seconds. */\n fastHalfLife: number;\n /** Half-life for slow EWMA in seconds. */\n slowHalfLife: number;\n /** Minimum total bytes before trusting the estimate. */\n minTotalBytes: number;\n /** Minimum bytes per sample to count (filters TTFB-dominated samples). */\n minBytes: number;\n /** Minimum sample duration in ms (filters cached responses). */\n minDuration: number;\n}\n\n/**\n * Default bandwidth estimator configuration.\n *\n * Values match Shaka Player defaults based on experimentation.\n */\nexport const DEFAULT_BANDWIDTH_CONFIG: BandwidthConfig = {\n fastHalfLife: 2, // 2 seconds\n slowHalfLife: 5, // 5 seconds\n minTotalBytes: 128_000, // 128 KB\n minBytes: 16_000, // 16 KB\n minDuration: 5, // 5 ms\n};\n\n/**\n * Add a bandwidth sample from a segment download.\n *\n * Samples are filtered based on:\n * - Minimum bytes (filters TTFB-dominated small segments)\n * - Minimum duration (filters cached responses)\n *\n * Valid samples update both fast and slow EWMA estimates.\n *\n * @param state - Current estimator state\n * @param durationMs - Download duration in milliseconds\n * @param numBytes - Number of bytes downloaded\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns New estimator state with sample incorporated (or unchanged if filtered)\n *\n * @example\n * let state = { fastEstimate: 0, fastTotalWeight: 0, ... };\n * // Sample: 1MB in 1 second\n * state = sampleBandwidth(state, 1000, 1_000_000);\n */\nexport function sampleBandwidth(\n state: BandwidthState,\n durationMs: number,\n numBytes: number,\n config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG\n): BandwidthState {\n // Always track bytes for startup phase calculation\n const updatedBytesSampled = state.bytesSampled + numBytes;\n\n // Filter: Ignore samples below minimum bytes (TTFB-dominated)\n // Small segments' download time is mostly connection setup latency,\n // which would artificially lower our bandwidth estimate\n if (numBytes < config.minBytes) {\n return { ...state, bytesSampled: updatedBytesSampled };\n }\n\n // Filter: Ignore samples faster than minimum duration (cached responses)\n // Cached responses load nearly instantly and would artificially inflate\n // our bandwidth estimate\n if (durationMs < config.minDuration) {\n return { ...state, bytesSampled: updatedBytesSampled };\n }\n\n // Calculate bandwidth in bits per second\n // Formula: (bytes * 8 bits/byte * 1000 ms/s) / ms = bits/s\n const bandwidth = (8000 * numBytes) / durationMs;\n\n // Weight by duration in seconds\n // Longer downloads are more reliable indicators of true bandwidth\n const weight = durationMs / 1000;\n\n return {\n fastEstimate: calculateEwma(state.fastEstimate, bandwidth, weight, config.fastHalfLife),\n fastTotalWeight: state.fastTotalWeight + weight,\n slowEstimate: calculateEwma(state.slowEstimate, bandwidth, weight, config.slowHalfLife),\n slowTotalWeight: state.slowTotalWeight + weight,\n bytesSampled: updatedBytesSampled,\n };\n}\n\n/**\n * Get the current bandwidth estimate.\n *\n * Returns the **minimum** of the fast and slow EWMA estimates.\n * This provides the key asymmetric behavior:\n * - When bandwidth drops, fast EWMA reacts first and dominates (quick adaptation)\n * - When bandwidth rises, slow EWMA lags behind and dominates (slow adaptation)\n *\n * Uses default estimate until enough data has been sampled — and when no\n * estimator state exists at all (`state === undefined`).\n *\n * @param state - Current estimator state, or `undefined` before any samples have been collected\n * @param defaultEstimate - Fallback estimate before sufficient samples (bps)\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns Bandwidth estimate in bits per second\n *\n * @example\n * const estimate = getBandwidthEstimate(state, 5_000_000); // 5 Mbps default\n */\nexport function getBandwidthEstimate(\n state: BandwidthState | undefined,\n defaultEstimate: number,\n config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG\n): number {\n // Use default until we have enough samples to trust our estimate\n if (!state || state.bytesSampled < config.minTotalBytes) {\n return defaultEstimate;\n }\n\n // Apply zero-factor correction to both estimates\n const fastEstimate = applyZeroFactor(state.fastEstimate, state.fastTotalWeight, config.fastHalfLife);\n\n const slowEstimate = applyZeroFactor(state.slowEstimate, state.slowTotalWeight, config.slowHalfLife);\n\n // Take the minimum - this is the key insight from Shaka Player\n // It naturally provides \"down quickly, up slowly\" behavior\n return Math.min(fastEstimate, slowEstimate);\n}\n\n/**\n * Check if the estimator has enough data to provide a reliable estimate.\n *\n * Requires both:\n * - Enough total bytes sampled (minTotalBytes threshold)\n * - At least one valid EWMA sample (totalWeight > 0)\n *\n * @param state - Current estimator state\n * @param config - Optional estimator configuration (uses defaults if not provided)\n * @returns True if we've sampled enough bytes to trust the estimate\n *\n * @example\n * if (hasGoodEstimate(state)) {\n * const estimate = getBandwidthEstimate(state, 5_000_000);\n * }\n */\nexport function hasGoodEstimate(state: BandwidthState, config: BandwidthConfig = DEFAULT_BANDWIDTH_CONFIG): boolean {\n // Need enough total bytes AND at least one valid EWMA sample\n return state.bytesSampled >= config.minTotalBytes && state.fastTotalWeight > 0 && state.slowTotalWeight > 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAwDA,MAAa,2BAA4C;CACvD,cAAc;CACd,cAAc;CACd,eAAe;CACf,UAAU;CACV,aAAa;AACf;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,gBACd,OACA,YACA,UACA,SAA0B,0BACV;CAEhB,MAAM,sBAAsB,MAAM,eAAe;CAKjD,IAAI,WAAW,OAAO,UACpB,OAAO;EAAE,GAAG;EAAO,cAAc;CAAoB;CAMvD,IAAI,aAAa,OAAO,aACtB,OAAO;EAAE,GAAG;EAAO,cAAc;CAAoB;CAKvD,MAAM,YAAa,MAAO,WAAY;CAItC,MAAM,SAAS,aAAa;CAE5B,OAAO;EACL,cAAc,cAAc,MAAM,cAAc,WAAW,QAAQ,OAAO,YAAY;EACtF,iBAAiB,MAAM,kBAAkB;EACzC,cAAc,cAAc,MAAM,cAAc,WAAW,QAAQ,OAAO,YAAY;EACtF,iBAAiB,MAAM,kBAAkB;EACzC,cAAc;CAChB;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,qBACd,OACA,iBACA,SAA0B,0BAClB;CAER,IAAI,CAAC,SAAS,MAAM,eAAe,OAAO,eACxC,OAAO;CAIT,MAAM,eAAe,gBAAgB,MAAM,cAAc,MAAM,iBAAiB,OAAO,YAAY;CAEnG,MAAM,eAAe,gBAAgB,MAAM,cAAc,MAAM,iBAAiB,OAAO,YAAY;CAInG,OAAO,KAAK,IAAI,cAAc,YAAY;AAC5C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunked-stream-iterable.js","names":["#readableStream"],"sources":["../../../src/network/chunked-stream-iterable.ts"],"sourcesContent":["const DEFAULT_MIN_CHUNK_SIZE = 2 ** 17; // 128 KB\n\nexport interface ChunkedStreamIterableOptions {\n minChunkSize?: number;\n}\n\n/**\n * Adapts a `ReadableStream<Uint8Array>` (e.g. `response.body`) into an\n * `AsyncIterable<Uint8Array>` that yields chunks no smaller than\n * `minChunkSize` bytes. Smaller network chunks are accumulated and yielded\n * together once the threshold is met. Any remainder is flushed on stream end.\n *\n * Errors from the underlying stream propagate naturally — the reader lock is\n * always released via `finally`.\n */\nexport class ChunkedStreamIterable implements AsyncIterable<Uint8Array> {\n readonly minChunkSize: number;\n #readableStream: ReadableStream<Uint8Array>;\n\n constructor(\n readableStream: ReadableStream<Uint8Array>,\n { minChunkSize = DEFAULT_MIN_CHUNK_SIZE }: ChunkedStreamIterableOptions = {}\n ) {\n this.#readableStream = readableStream;\n this.minChunkSize = minChunkSize;\n }\n\n async *[Symbol.asyncIterator](): AsyncGenerator<Uint8Array> {\n let pending: Uint8Array | undefined;\n const reader = this.#readableStream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n if (pending) yield pending;\n break;\n }\n\n pending = pending ? concat(pending, value) : value;\n\n if (pending.length >= this.minChunkSize) {\n yield pending;\n pending = undefined;\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n}\n\nfunction concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n const result = new Uint8Array(a.length + b.length);\n result.set(a);\n result.set(b, a.length);\n return result;\n}\n"],"mappings":";AAAA,MAAM,yBAAyB,KAAK;;;;;;;;;;AAepC,IAAa,wBAAb,MAAwE;CACtE;CACA;CAEA,YACE,gBACA,EAAE,eAAe,2BAAyD,
|
|
1
|
+
{"version":3,"file":"chunked-stream-iterable.js","names":["#readableStream"],"sources":["../../../src/network/chunked-stream-iterable.ts"],"sourcesContent":["const DEFAULT_MIN_CHUNK_SIZE = 2 ** 17; // 128 KB\n\nexport interface ChunkedStreamIterableOptions {\n minChunkSize?: number;\n}\n\n/**\n * Adapts a `ReadableStream<Uint8Array>` (e.g. `response.body`) into an\n * `AsyncIterable<Uint8Array>` that yields chunks no smaller than\n * `minChunkSize` bytes. Smaller network chunks are accumulated and yielded\n * together once the threshold is met. Any remainder is flushed on stream end.\n *\n * Errors from the underlying stream propagate naturally — the reader lock is\n * always released via `finally`.\n */\nexport class ChunkedStreamIterable implements AsyncIterable<Uint8Array> {\n readonly minChunkSize: number;\n #readableStream: ReadableStream<Uint8Array>;\n\n constructor(\n readableStream: ReadableStream<Uint8Array>,\n { minChunkSize = DEFAULT_MIN_CHUNK_SIZE }: ChunkedStreamIterableOptions = {}\n ) {\n this.#readableStream = readableStream;\n this.minChunkSize = minChunkSize;\n }\n\n async *[Symbol.asyncIterator](): AsyncGenerator<Uint8Array> {\n let pending: Uint8Array | undefined;\n const reader = this.#readableStream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n if (pending) yield pending;\n break;\n }\n\n pending = pending ? concat(pending, value) : value;\n\n if (pending.length >= this.minChunkSize) {\n yield pending;\n pending = undefined;\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n}\n\nfunction concat(a: Uint8Array, b: Uint8Array): Uint8Array {\n const result = new Uint8Array(a.length + b.length);\n result.set(a);\n result.set(b, a.length);\n return result;\n}\n"],"mappings":";AAAA,MAAM,yBAAyB,KAAK;;;;;;;;;;AAepC,IAAa,wBAAb,MAAwE;CACtE;CACA;CAEA,YACE,gBACA,EAAE,eAAe,2BAAyD,CAAC,GAC3E;EACA,KAAKA,kBAAkB;EACvB,KAAK,eAAe;CACtB;CAEA,QAAQ,OAAO,iBAA6C;EAC1D,IAAI;EACJ,MAAM,SAAS,KAAKA,gBAAgB,UAAU;EAC9C,IAAI;GACF,OAAO,MAAM;IACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;IAC1C,IAAI,MAAM;KACR,IAAI,SAAS,MAAM;KACnB;IACF;IAEA,UAAU,UAAU,OAAO,SAAS,KAAK,IAAI;IAE7C,IAAI,QAAQ,UAAU,KAAK,cAAc;KACvC,MAAM;KACN,UAAU,KAAA;IACZ;GACF;EACF,UAAU;GACR,OAAO,YAAY;EACrB;CACF;AACF;AAEA,SAAS,OAAO,GAAe,GAA2B;CACxD,MAAM,SAAS,IAAI,WAAW,EAAE,SAAS,EAAE,MAAM;CACjD,OAAO,IAAI,CAAC;CACZ,OAAO,IAAI,GAAG,EAAE,MAAM;CACtB,OAAO;AACT"}
|
|
@@ -66,6 +66,6 @@ function applyZeroFactor(estimate, totalWeight, halfLife) {
|
|
|
66
66
|
return estimate / (1 - calculateAlpha(halfLife) ** totalWeight);
|
|
67
67
|
}
|
|
68
68
|
//#endregion
|
|
69
|
-
export { applyZeroFactor, calculateEwma };
|
|
69
|
+
export { applyZeroFactor, calculateAlpha, calculateEwma };
|
|
70
70
|
|
|
71
71
|
//# sourceMappingURL=ewma.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ewma.js","names":[],"sources":["../../../src/network/ewma.ts"],"sourcesContent":["/**\n * Exponentially Weighted Moving Average (EWMA)\n *\n * Pure functional implementation of EWMA calculations.\n * Based on Shaka Player's EWMA algorithm.\n */\n\n/**\n * Calculate alpha (decay factor) from half-life.\n *\n * Alpha determines how quickly old data \"expires\":\n * - alpha close to 1 = slow decay (long memory)\n * - alpha close to 0 = fast decay (short memory)\n *\n * @param halfLife - The quantity of prior samples (by weight) that make up\n * half of the new estimate. Must be positive.\n * @returns Alpha value between 0 and 1\n *\n * @example\n * const alpha = calculateAlpha(2); // ≈ 0.7071 for 2-second half-life\n */\nexport function calculateAlpha(halfLife: number): number {\n return Math.exp(Math.log(0.5) / halfLife);\n}\n\n/**\n * Calculate exponentially weighted moving average.\n *\n * Updates an estimate by blending a new value with the previous estimate,\n * weighted by the sample duration. Longer samples have more influence.\n *\n * @param prevEstimate - Previous EWMA estimate\n * @param value - New sample value to incorporate\n * @param weight - Sample weight (typically duration in seconds)\n * @param halfLife - Half-life for decay (typically 2-5 seconds)\n * @returns Updated EWMA estimate\n *\n * @example\n * let estimate = 0;\n * estimate = calculateEwma(estimate, 1_000_000, 1, 2); // First sample\n * estimate = calculateEwma(estimate, 2_000_000, 1, 2); // Second sample\n */\nexport function calculateEwma(prevEstimate: number, value: number, weight: number, halfLife: number): number {\n const alpha = calculateAlpha(halfLife);\n const adjAlpha = alpha ** weight;\n return value * (1 - adjAlpha) + adjAlpha * prevEstimate;\n}\n\n/**\n * Apply zero-factor correction to EWMA estimate.\n *\n * The zero-factor correction compensates for bias when starting from zero.\n * Without this correction, early estimates would be artificially low.\n *\n * As totalWeight increases, the correction factor approaches 1, meaning\n * the estimate becomes more reliable and needs less correction.\n *\n * @param estimate - Raw EWMA estimate (uncorrected)\n * @param totalWeight - Accumulated weight from all samples\n * @param halfLife - Half-life used in EWMA calculation\n * @returns Corrected estimate, or 0 if totalWeight is 0\n *\n * @example\n * const raw = calculateEwma(0, 1_000_000, 1, 2);\n * const corrected = applyZeroFactor(raw, 1, 2); // ≈ 1_000_000\n */\nexport function applyZeroFactor(estimate: number, totalWeight: number, halfLife: number): number {\n if (totalWeight === 0) {\n return 0;\n }\n\n const alpha = calculateAlpha(halfLife);\n const zeroFactor = 1 - alpha ** totalWeight;\n return estimate / zeroFactor;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,eAAe,UAA0B;
|
|
1
|
+
{"version":3,"file":"ewma.js","names":[],"sources":["../../../src/network/ewma.ts"],"sourcesContent":["/**\n * Exponentially Weighted Moving Average (EWMA)\n *\n * Pure functional implementation of EWMA calculations.\n * Based on Shaka Player's EWMA algorithm.\n */\n\n/**\n * Calculate alpha (decay factor) from half-life.\n *\n * Alpha determines how quickly old data \"expires\":\n * - alpha close to 1 = slow decay (long memory)\n * - alpha close to 0 = fast decay (short memory)\n *\n * @param halfLife - The quantity of prior samples (by weight) that make up\n * half of the new estimate. Must be positive.\n * @returns Alpha value between 0 and 1\n *\n * @example\n * const alpha = calculateAlpha(2); // ≈ 0.7071 for 2-second half-life\n */\nexport function calculateAlpha(halfLife: number): number {\n return Math.exp(Math.log(0.5) / halfLife);\n}\n\n/**\n * Calculate exponentially weighted moving average.\n *\n * Updates an estimate by blending a new value with the previous estimate,\n * weighted by the sample duration. Longer samples have more influence.\n *\n * @param prevEstimate - Previous EWMA estimate\n * @param value - New sample value to incorporate\n * @param weight - Sample weight (typically duration in seconds)\n * @param halfLife - Half-life for decay (typically 2-5 seconds)\n * @returns Updated EWMA estimate\n *\n * @example\n * let estimate = 0;\n * estimate = calculateEwma(estimate, 1_000_000, 1, 2); // First sample\n * estimate = calculateEwma(estimate, 2_000_000, 1, 2); // Second sample\n */\nexport function calculateEwma(prevEstimate: number, value: number, weight: number, halfLife: number): number {\n const alpha = calculateAlpha(halfLife);\n const adjAlpha = alpha ** weight;\n return value * (1 - adjAlpha) + adjAlpha * prevEstimate;\n}\n\n/**\n * Apply zero-factor correction to EWMA estimate.\n *\n * The zero-factor correction compensates for bias when starting from zero.\n * Without this correction, early estimates would be artificially low.\n *\n * As totalWeight increases, the correction factor approaches 1, meaning\n * the estimate becomes more reliable and needs less correction.\n *\n * @param estimate - Raw EWMA estimate (uncorrected)\n * @param totalWeight - Accumulated weight from all samples\n * @param halfLife - Half-life used in EWMA calculation\n * @returns Corrected estimate, or 0 if totalWeight is 0\n *\n * @example\n * const raw = calculateEwma(0, 1_000_000, 1, 2);\n * const corrected = applyZeroFactor(raw, 1, 2); // ≈ 1_000_000\n */\nexport function applyZeroFactor(estimate: number, totalWeight: number, halfLife: number): number {\n if (totalWeight === 0) {\n return 0;\n }\n\n const alpha = calculateAlpha(halfLife);\n const zeroFactor = 1 - alpha ** totalWeight;\n return estimate / zeroFactor;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,eAAe,UAA0B;CACvD,OAAO,KAAK,IAAI,KAAK,IAAI,EAAG,IAAI,QAAQ;AAC1C;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,cAAc,cAAsB,OAAe,QAAgB,UAA0B;CAE3G,MAAM,WADQ,eAAe,QACR,KAAK;CAC1B,OAAO,SAAS,IAAI,YAAY,WAAW;AAC7C;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,gBAAgB,UAAkB,aAAqB,UAA0B;CAC/F,IAAI,gBAAgB,GAClB,OAAO;CAKT,OAAO,YADY,IADL,eAAe,QACF,KAAK;AAElC"}
|
|
@@ -63,6 +63,12 @@ async function fetchResolvable(addressable, options) {
|
|
|
63
63
|
function getResponseText(response) {
|
|
64
64
|
return response.text();
|
|
65
65
|
}
|
|
66
|
+
/** Default {@link FetchText}: fetch the resource, reject on non-OK, return text. */
|
|
67
|
+
const fetchResolvableText = async (addressable, options) => {
|
|
68
|
+
const response = await fetchResolvable(addressable, options);
|
|
69
|
+
if (!response.ok) throw new Error(`fetchResolvableText: ${response.status} ${response.statusText} for ${addressable.url}`);
|
|
70
|
+
return getResponseText(response);
|
|
71
|
+
};
|
|
66
72
|
async function fetchStream(addressable, options) {
|
|
67
73
|
const { minChunkSize, ...fetchOptions } = options ?? {};
|
|
68
74
|
const response = await fetchResolvable(addressable, fetchOptions);
|
|
@@ -104,6 +110,6 @@ function createTrackedFetch(initial, onSample) {
|
|
|
104
110
|
};
|
|
105
111
|
}
|
|
106
112
|
//#endregion
|
|
107
|
-
export { createTrackedFetch, fetchResolvable, fetchStream, getResponseText };
|
|
113
|
+
export { createTrackedFetch, fetchResolvable, fetchResolvableText, fetchStream, getResponseText };
|
|
108
114
|
|
|
109
115
|
//# sourceMappingURL=fetch.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.js","names":[],"sources":["../../../src/network/fetch.ts"],"sourcesContent":["/**\n * HTTP Fetch Wrapper\n *\n * Composable building blocks:\n * - fetchResolvable() — fetch a Resource (handles byte ranges); returns Response\n * - getResponseText() — extract text from Response\n * - fetchResolvableStream() — single-stage async generator over body chunks\n * - fetchStream() — two-stage: await connection establishment, then lazily\n * iterate body chunks. Use when timing the connection start independently\n * of body consumption matters (e.g., observable fetch timing for ABR).\n * - createTrackedFetch() — factory for a fetchStream-shape function that\n * samples bandwidth (via EWMA) per chunk and notifies via callback.\n */\n\nimport { type BandwidthState, sampleBandwidth } from './bandwidth-estimator';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from './chunked-stream-iterable';\n\n/**\n * Minimal Response-like interface for text extraction.\n * Allows testing without full Response object.\n */\nexport interface ResponseLike {\n text(): Promise<string>;\n}\n\n/**\n * An HTTP-addressable resource — URL plus optional byte range.\n * Media's `AddressableObject` (and anything else with the same shape)\n * is structurally compatible; kept local so this module stays\n * domain-agnostic.\n */\nexport interface Resource {\n url: string;\n byteRange?: {\n start: number;\n end: number;\n };\n}\n\n/**\n * Fetch resolvable from a Resource.\n *\n * Handles byte range requests if byteRange is present.\n * Returns native fetch Response for composability (can extract text, stream, etc.).\n *\n * @param addressable - Resource to fetch (url + optional byteRange)\n * @returns Promise resolving to Response\n *\n * @example\n * const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });\n * const text = await getResponseText(response);\n *\n * @example\n * // With byte range\n * const response = await fetchResolvable({\n * url: 'https://example.com/file.mp4',\n * byteRange: { start: 1000, end: 1999 }\n * });\n */\nexport async function fetchResolvable(addressable: Resource, options?: RequestInit): Promise<Response> {\n const headers = new Headers(options?.headers);\n\n // Add Range header for byte range requests\n if (addressable.byteRange) {\n const { start, end } = addressable.byteRange;\n headers.set('Range', `bytes=${start}-${end}`);\n }\n\n const request = new Request(addressable.url, {\n method: 'GET',\n headers,\n ...options,\n });\n\n return fetch(request);\n}\n\n/**\n * Fetch resolvable as bytes.\n *\n * Convenience wrapper around fetchResolvable that resolves the body as an\n * ArrayBuffer. Use this when you need the raw bytes (e.g. segment appends).\n * For text or streaming consumption, use fetchResolvable directly.\n */\nexport async function fetchResolvableBytes(addressable: Resource, options?: RequestInit): Promise<ArrayBuffer> {\n const response = await fetchResolvable(addressable, options);\n return response.arrayBuffer();\n}\n\n/**\n * Fetch resolvable as a stream of Uint8Array chunks.\n *\n * Convenience wrapper around fetchResolvable that yields the body as chunks\n * via ChunkedStreamIterable. Headers are awaited before the first chunk is\n * yielded (TTFB is accounted for before iteration begins).\n *\n * Throws if the response body is null (e.g. non-body HTTP status).\n * Errors from the underlying stream propagate naturally as thrown errors.\n */\nexport async function* fetchResolvableStream(\n addressable: Resource,\n options?: RequestInit & ChunkedStreamIterableOptions\n): AsyncGenerator<Uint8Array> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n yield* new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Extract text from Response.\n *\n * Accepts minimal Response-like object (just needs text() method).\n * Returns promise from response.text().\n *\n * @param response - Response-like object with text() method\n * @returns Promise resolving to text content\n *\n * @example\n * const response = await fetchResolvable(addressable);\n * const text = await getResponseText(response);\n */\nexport function getResponseText(response: ResponseLike): Promise<string> {\n return response.text();\n}\n\n/**\n * Two-stage fetch helper: eagerly starts the HTTP request (TTFB is awaited),\n * then returns a lazy iterable over the response body. Separating connection\n * start from body iteration makes fetch timing predictable and observable\n * regardless of when downstream consumers begin pulling chunks.\n *\n * Sibling to {@link fetchResolvableStream}, which is single-stage (calls\n * `fetch` only when iteration starts). Pick `fetchStream` when \"when did the\n * fetch start\" needs to be observable separately from \"when did the body\n * begin arriving.\"\n */\nexport type FetchOptions = RequestInit & ChunkedStreamIterableOptions;\n\nexport type FetchBytes = (addressable: Resource, options?: FetchOptions) => Promise<AsyncIterable<Uint8Array>>;\n\nexport async function fetchStream(addressable: Resource, options?: FetchOptions): Promise<AsyncIterable<Uint8Array>> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n return new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Returns a {@link FetchBytes} function that samples bandwidth via EWMA\n * per body chunk. The factory captures the running bandwidth state\n * internally; per chunk it computes the next state and notifies the\n * supplied `onSample` callback.\n *\n * The factory's internal accumulator is seeded from `initial` and updated\n * on every chunk; callers don't need to thread it back in. `onSample`\n * receives the *new* state after each chunk — typical use is to bridge\n * samples back into engine state for ABR consumers.\n *\n * @param initial - Starting `BandwidthState` (commonly zeros or the\n * engine's current accumulator).\n * @param onSample - Called with the new `BandwidthState` after each chunk.\n */\nexport function createTrackedFetch(initial: BandwidthState, onSample: (next: BandwidthState) => void): FetchBytes {\n let state = initial;\n return async (addressable, options) => {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n const body = response.body;\n return {\n [Symbol.asyncIterator]: async function* () {\n let chunkStart = performance.now();\n for await (const chunk of new ChunkedStreamIterable(\n body,\n ...(minChunkSize !== undefined ? [{ minChunkSize }] : [])\n )) {\n const elapsed = performance.now() - chunkStart;\n state = sampleBandwidth(state, elapsed, chunk.byteLength);\n onSample(state);\n yield chunk;\n chunkStart = performance.now();\n }\n },\n };\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,eAAsB,gBAAgB,aAAuB,SAA0C;CACrG,MAAM,UAAU,IAAI,QAAQ,SAAS,
|
|
1
|
+
{"version":3,"file":"fetch.js","names":[],"sources":["../../../src/network/fetch.ts"],"sourcesContent":["/**\n * HTTP Fetch Wrapper\n *\n * Composable building blocks:\n * - fetchResolvable() — fetch a Resource (handles byte ranges); returns Response\n * - getResponseText() — extract text from Response\n * - fetchResolvableStream() — single-stage async generator over body chunks\n * - fetchStream() — two-stage: await connection establishment, then lazily\n * iterate body chunks. Use when timing the connection start independently\n * of body consumption matters (e.g., observable fetch timing for ABR).\n * - createTrackedFetch() — factory for a fetchStream-shape function that\n * samples bandwidth (via EWMA) per chunk and notifies via callback.\n */\n\nimport { type BandwidthState, sampleBandwidth } from './bandwidth-estimator';\nimport { ChunkedStreamIterable, type ChunkedStreamIterableOptions } from './chunked-stream-iterable';\n\n/**\n * Minimal Response-like interface for text extraction.\n * Allows testing without full Response object.\n */\nexport interface ResponseLike {\n text(): Promise<string>;\n}\n\n/**\n * An HTTP-addressable resource — URL plus optional byte range.\n * Media's `AddressableObject` (and anything else with the same shape)\n * is structurally compatible; kept local so this module stays\n * domain-agnostic.\n */\nexport interface Resource {\n url: string;\n byteRange?: {\n start: number;\n end: number;\n };\n}\n\n/**\n * Fetch resolvable from a Resource.\n *\n * Handles byte range requests if byteRange is present.\n * Returns native fetch Response for composability (can extract text, stream, etc.).\n *\n * @param addressable - Resource to fetch (url + optional byteRange)\n * @returns Promise resolving to Response\n *\n * @example\n * const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });\n * const text = await getResponseText(response);\n *\n * @example\n * // With byte range\n * const response = await fetchResolvable({\n * url: 'https://example.com/file.mp4',\n * byteRange: { start: 1000, end: 1999 }\n * });\n */\nexport async function fetchResolvable(addressable: Resource, options?: RequestInit): Promise<Response> {\n const headers = new Headers(options?.headers);\n\n // Add Range header for byte range requests\n if (addressable.byteRange) {\n const { start, end } = addressable.byteRange;\n headers.set('Range', `bytes=${start}-${end}`);\n }\n\n const request = new Request(addressable.url, {\n method: 'GET',\n headers,\n ...options,\n });\n\n return fetch(request);\n}\n\n/**\n * Fetch resolvable as bytes.\n *\n * Convenience wrapper around fetchResolvable that resolves the body as an\n * ArrayBuffer. Use this when you need the raw bytes (e.g. segment appends).\n * For text or streaming consumption, use fetchResolvable directly.\n */\nexport async function fetchResolvableBytes(addressable: Resource, options?: RequestInit): Promise<ArrayBuffer> {\n const response = await fetchResolvable(addressable, options);\n return response.arrayBuffer();\n}\n\n/**\n * Fetch resolvable as a stream of Uint8Array chunks.\n *\n * Convenience wrapper around fetchResolvable that yields the body as chunks\n * via ChunkedStreamIterable. Headers are awaited before the first chunk is\n * yielded (TTFB is accounted for before iteration begins).\n *\n * Throws if the response body is null (e.g. non-body HTTP status).\n * Errors from the underlying stream propagate naturally as thrown errors.\n */\nexport async function* fetchResolvableStream(\n addressable: Resource,\n options?: RequestInit & ChunkedStreamIterableOptions\n): AsyncGenerator<Uint8Array> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n yield* new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Extract text from Response.\n *\n * Accepts minimal Response-like object (just needs text() method).\n * Returns promise from response.text().\n *\n * @param response - Response-like object with text() method\n * @returns Promise resolving to text content\n *\n * @example\n * const response = await fetchResolvable(addressable);\n * const text = await getResponseText(response);\n */\nexport function getResponseText(response: ResponseLike): Promise<string> {\n return response.text();\n}\n\n/**\n * Fetch a resource and resolve its text body — the text analog of\n * {@link FetchBytes}. A non-OK status rejects, so HTTP failures surface as\n * rejections that callers (and decorators like the failover tracker) handle\n * uniformly with network errors.\n */\nexport type FetchText = (addressable: Resource, options?: RequestInit) => Promise<string>;\n\n/** Default {@link FetchText}: fetch the resource, reject on non-OK, return text. */\nexport const fetchResolvableText: FetchText = async (addressable, options) => {\n const response = await fetchResolvable(addressable, options);\n if (!response.ok) {\n throw new Error(`fetchResolvableText: ${response.status} ${response.statusText} for ${addressable.url}`);\n }\n return getResponseText(response);\n};\n\n/**\n * Two-stage fetch helper: eagerly starts the HTTP request (TTFB is awaited),\n * then returns a lazy iterable over the response body. Separating connection\n * start from body iteration makes fetch timing predictable and observable\n * regardless of when downstream consumers begin pulling chunks.\n *\n * Sibling to {@link fetchResolvableStream}, which is single-stage (calls\n * `fetch` only when iteration starts). Pick `fetchStream` when \"when did the\n * fetch start\" needs to be observable separately from \"when did the body\n * begin arriving.\"\n */\nexport type FetchOptions = RequestInit & ChunkedStreamIterableOptions;\n\nexport type FetchBytes = (addressable: Resource, options?: FetchOptions) => Promise<AsyncIterable<Uint8Array>>;\n\nexport async function fetchStream(addressable: Resource, options?: FetchOptions): Promise<AsyncIterable<Uint8Array>> {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n return new ChunkedStreamIterable(response.body, ...(minChunkSize !== undefined ? [{ minChunkSize }] : []));\n}\n\n/**\n * Returns a {@link FetchBytes} function that samples bandwidth via EWMA\n * per body chunk. The factory captures the running bandwidth state\n * internally; per chunk it computes the next state and notifies the\n * supplied `onSample` callback.\n *\n * The factory's internal accumulator is seeded from `initial` and updated\n * on every chunk; callers don't need to thread it back in. `onSample`\n * receives the *new* state after each chunk — typical use is to bridge\n * samples back into engine state for ABR consumers.\n *\n * @param initial - Starting `BandwidthState` (commonly zeros or the\n * engine's current accumulator).\n * @param onSample - Called with the new `BandwidthState` after each chunk.\n */\nexport function createTrackedFetch(initial: BandwidthState, onSample: (next: BandwidthState) => void): FetchBytes {\n let state = initial;\n return async (addressable, options) => {\n const { minChunkSize, ...fetchOptions } = options ?? {};\n const response = await fetchResolvable(addressable, fetchOptions);\n if (!response.body) throw new Error('Response has no body');\n const body = response.body;\n return {\n [Symbol.asyncIterator]: async function* () {\n let chunkStart = performance.now();\n for await (const chunk of new ChunkedStreamIterable(\n body,\n ...(minChunkSize !== undefined ? [{ minChunkSize }] : [])\n )) {\n const elapsed = performance.now() - chunkStart;\n state = sampleBandwidth(state, elapsed, chunk.byteLength);\n onSample(state);\n yield chunk;\n chunkStart = performance.now();\n }\n },\n };\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,eAAsB,gBAAgB,aAAuB,SAA0C;CACrG,MAAM,UAAU,IAAI,QAAQ,SAAS,OAAO;CAG5C,IAAI,YAAY,WAAW;EACzB,MAAM,EAAE,OAAO,QAAQ,YAAY;EACnC,QAAQ,IAAI,SAAS,SAAS,MAAM,GAAG,KAAK;CAC9C;CAEA,MAAM,UAAU,IAAI,QAAQ,YAAY,KAAK;EAC3C,QAAQ;EACR;EACA,GAAG;CACL,CAAC;CAED,OAAO,MAAM,OAAO;AACtB;;;;;;;;;;;;;;AA+CA,SAAgB,gBAAgB,UAAyC;CACvE,OAAO,SAAS,KAAK;AACvB;;AAWA,MAAa,sBAAiC,OAAO,aAAa,YAAY;CAC5E,MAAM,WAAW,MAAM,gBAAgB,aAAa,OAAO;CAC3D,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,wBAAwB,SAAS,OAAO,GAAG,SAAS,WAAW,OAAO,YAAY,KAAK;CAEzG,OAAO,gBAAgB,QAAQ;AACjC;AAiBA,eAAsB,YAAY,aAAuB,SAA4D;CACnH,MAAM,EAAE,cAAc,GAAG,iBAAiB,WAAW,CAAC;CACtD,MAAM,WAAW,MAAM,gBAAgB,aAAa,YAAY;CAChE,IAAI,CAAC,SAAS,MAAM,MAAM,IAAI,MAAM,sBAAsB;CAC1D,OAAO,IAAI,sBAAsB,SAAS,MAAM,GAAI,iBAAiB,KAAA,IAAY,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAE;AAC3G;;;;;;;;;;;;;;;;AAiBA,SAAgB,mBAAmB,SAAyB,UAAsD;CAChH,IAAI,QAAQ;CACZ,OAAO,OAAO,aAAa,YAAY;EACrC,MAAM,EAAE,cAAc,GAAG,iBAAiB,WAAW,CAAC;EACtD,MAAM,WAAW,MAAM,gBAAgB,aAAa,YAAY;EAChE,IAAI,CAAC,SAAS,MAAM,MAAM,IAAI,MAAM,sBAAsB;EAC1D,MAAM,OAAO,SAAS;EACtB,OAAO,EACL,CAAC,OAAO,gBAAgB,mBAAmB;GACzC,IAAI,aAAa,YAAY,IAAI;GACjC,WAAW,MAAM,SAAS,IAAI,sBAC5B,MACA,GAAI,iBAAiB,KAAA,IAAY,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CACzD,GAAG;IACD,MAAM,UAAU,YAAY,IAAI,IAAI;IACpC,QAAQ,gBAAgB,OAAO,SAAS,MAAM,UAAU;IACxD,SAAS,KAAK;IACd,MAAM;IACN,aAAa,YAAY,IAAI;GAC/B;EACF,EACF;CACF;AACF"}
|
|
@@ -1,106 +1,49 @@
|
|
|
1
1
|
import { peek } from "../../../core/signals/primitives.js";
|
|
2
|
-
import { effect } from "../../../core/signals/effect.js";
|
|
3
2
|
import { SerialRunner, Task } from "../../../core/tasks/task.js";
|
|
4
3
|
import { createMachineActor } from "../../../core/actors/create-machine-actor.js";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
4
|
+
import "../../../media/types/index.js";
|
|
5
|
+
import { DEFAULT_FORWARD_BUFFER_CONFIG, calculateForwardFlushPoint, getSegmentsToLoad, isTimeRangeCovered, mergeTimeRanges } from "../../../media/buffer/forward-buffer.js";
|
|
7
6
|
import { DEFAULT_BACK_BUFFER_CONFIG, calculateBackBufferFlushPoint } from "../../../media/buffer/back-buffer.js";
|
|
7
|
+
import { DEFAULT_MESSAGE_PIPELINES } from "../../primitives/segment-load-pipeline.js";
|
|
8
8
|
//#region src/playback/actors/dom/segment-loader.ts
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* state transition.
|
|
10
|
+
* Wraps a LoadTask descriptor into a Task that runs the op's message pipeline
|
|
11
|
+
* (fetch/discover/stamp/dispatch, per the composition's `messagePipelines`).
|
|
12
|
+
* Updates in-flight context around the async region so the loading handler can
|
|
13
|
+
* make accurate continue/preempt decisions at any point, and checks the abort
|
|
14
|
+
* signal before each step.
|
|
16
15
|
*/
|
|
17
|
-
function
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
if (snapshot.get().value === "idle") {
|
|
20
|
-
resolve();
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (snapshot.get().value === "destroyed") {
|
|
24
|
-
reject(new DOMException("Aborted", "AbortError"));
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
if (signal.aborted) {
|
|
28
|
-
reject(signal.reason);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
let stop;
|
|
32
|
-
const cleanup = (fn) => {
|
|
33
|
-
stop?.();
|
|
34
|
-
signal.removeEventListener("abort", onAbort);
|
|
35
|
-
fn();
|
|
36
|
-
};
|
|
37
|
-
const onAbort = () => cleanup(() => reject(signal.reason));
|
|
38
|
-
stop = effect(() => {
|
|
39
|
-
const value = snapshot.get().value;
|
|
40
|
-
if (value === "idle") cleanup(resolve);
|
|
41
|
-
else if (value === "destroyed") cleanup(() => reject(new DOMException("Aborted", "AbortError")));
|
|
42
|
-
});
|
|
43
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Wraps a LoadTask descriptor into a Task that fetches (if needed) and
|
|
48
|
-
* forwards to SourceBufferActor. Updates in-flight context around async
|
|
49
|
-
* operations so the loading handler can make accurate continue/preempt
|
|
50
|
-
* decisions at any point.
|
|
51
|
-
*/
|
|
52
|
-
function makeLoadTask(op, { getContext, setContext, fetchBytes, sourceBufferActor }) {
|
|
16
|
+
function makeLoadTask(op, { getContext, setContext, pipelines, deps }) {
|
|
53
17
|
return new Task(async (taskSignal) => {
|
|
54
18
|
if (taskSignal.aborted) return;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
setContext({
|
|
19
|
+
const frame = op.type === "append-segment" ? {
|
|
20
|
+
op,
|
|
21
|
+
meta: op.meta
|
|
22
|
+
} : { op };
|
|
23
|
+
try {
|
|
24
|
+
if (op.type === "append-init") setContext({
|
|
62
25
|
...getContext(),
|
|
63
26
|
inFlightInitTrackId: op.meta.trackId
|
|
64
27
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!taskSignal.aborted) {
|
|
71
|
-
sourceBufferActor.send({
|
|
72
|
-
type: "append-init",
|
|
73
|
-
data,
|
|
74
|
-
meta: op.meta
|
|
75
|
-
});
|
|
76
|
-
await waitForIdle(sourceBufferActor.snapshot, taskSignal);
|
|
28
|
+
else if (op.type === "append-segment") setContext({
|
|
29
|
+
...getContext(),
|
|
30
|
+
inFlightSegment: {
|
|
31
|
+
id: op.meta.id,
|
|
32
|
+
trackId: op.meta.trackId
|
|
77
33
|
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
setContext({
|
|
87
|
-
...getContext(),
|
|
88
|
-
inFlightSegmentId: op.meta.id
|
|
89
|
-
});
|
|
90
|
-
try {
|
|
91
|
-
const stream = await fetchBytes(op, { signal: taskSignal });
|
|
92
|
-
if (!taskSignal.aborted) {
|
|
93
|
-
sourceBufferActor.send({
|
|
94
|
-
type: "append-segment",
|
|
95
|
-
data: stream,
|
|
96
|
-
meta: op.meta
|
|
97
|
-
});
|
|
98
|
-
await waitForIdle(sourceBufferActor.snapshot, taskSignal);
|
|
34
|
+
});
|
|
35
|
+
for (const step of pipelines[op.type]) {
|
|
36
|
+
if (taskSignal.aborted) return;
|
|
37
|
+
await step(frame, taskSignal, deps);
|
|
99
38
|
}
|
|
100
39
|
} finally {
|
|
101
|
-
setContext({
|
|
40
|
+
if (op.type === "append-init") setContext({
|
|
102
41
|
...getContext(),
|
|
103
|
-
|
|
42
|
+
inFlightInitTrackId: null
|
|
43
|
+
});
|
|
44
|
+
else if (op.type === "append-segment") setContext({
|
|
45
|
+
...getContext(),
|
|
46
|
+
inFlightSegment: null
|
|
104
47
|
});
|
|
105
48
|
}
|
|
106
49
|
});
|
|
@@ -123,8 +66,16 @@ function makeLoadTask(op, { getContext, setContext, fetchBytes, sourceBufferActo
|
|
|
123
66
|
* @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).
|
|
124
67
|
* Accepts an optional `minChunkSize` in options; init segments pass `Infinity`
|
|
125
68
|
* so the entire body accumulates as one chunk before appending.
|
|
69
|
+
* @param compositionDeps - The composition's `state`/`context`/`config`, threaded
|
|
70
|
+
* opaquely into each step's {@link StepDeps} (the loader never reads them). Lets
|
|
71
|
+
* injected steps (relocation) read composition signals at call time. Defaults to
|
|
72
|
+
* empty for standalone / base-pipeline use.
|
|
126
73
|
*/
|
|
127
|
-
function createSegmentLoaderActor(sourceBufferActor, fetchBytes, config = {}
|
|
74
|
+
function createSegmentLoaderActor(sourceBufferActor, fetchBytes, config = {}, compositionDeps = {
|
|
75
|
+
state: {},
|
|
76
|
+
context: {},
|
|
77
|
+
config: {}
|
|
78
|
+
}) {
|
|
128
79
|
const forwardBufferConfig = {
|
|
129
80
|
...DEFAULT_FORWARD_BUFFER_CONFIG,
|
|
130
81
|
...config.forwardBuffer
|
|
@@ -133,9 +84,22 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes, config = {}) {
|
|
|
133
84
|
...DEFAULT_BACK_BUFFER_CONFIG,
|
|
134
85
|
...config.backBuffer
|
|
135
86
|
};
|
|
87
|
+
const deps = {
|
|
88
|
+
state: compositionDeps.state,
|
|
89
|
+
context: compositionDeps.context,
|
|
90
|
+
config: {
|
|
91
|
+
...compositionDeps.config,
|
|
92
|
+
sourceBufferActor,
|
|
93
|
+
fetch: fetchBytes
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const pipelines = (config.messagePipelines ?? DEFAULT_MESSAGE_PIPELINES)();
|
|
136
97
|
const getBufferedSegments = (allSegments) => {
|
|
137
|
-
const
|
|
138
|
-
|
|
98
|
+
const merged = mergeTimeRanges(peek(sourceBufferActor.snapshot).context.segments.filter((s) => !s.partial).map((s) => ({
|
|
99
|
+
start: s.startTime,
|
|
100
|
+
end: s.startTime + s.duration
|
|
101
|
+
})));
|
|
102
|
+
return allSegments.filter((s) => isTimeRangeCovered(s.startTime, s.startTime + s.duration, merged));
|
|
139
103
|
};
|
|
140
104
|
/**
|
|
141
105
|
* Translate a load message into an ordered LoadTask list based on committed
|
|
@@ -144,9 +108,19 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes, config = {}) {
|
|
|
144
108
|
* @todo Rename alongside LoadTask (e.g. planOps).
|
|
145
109
|
*
|
|
146
110
|
* Case 1 — Removes: forward and back buffer flush points, segment-aligned.
|
|
147
|
-
*
|
|
148
|
-
* ranges, and the actor's
|
|
149
|
-
* accurate as new
|
|
111
|
+
* ABR-style track switches (same content, different bitrate) do not flush:
|
|
112
|
+
* appending new content overwrites existing buffer ranges, and the actor's
|
|
113
|
+
* time-aligned deduplication keeps the segment model accurate as new
|
|
114
|
+
* segments arrive.
|
|
115
|
+
*
|
|
116
|
+
* Cross-rendition track switches (audio language change, text language
|
|
117
|
+
* change) do flush: the buffered content is semantically incompatible with
|
|
118
|
+
* the newly-selected track, so overwrite-on-append would leave stale
|
|
119
|
+
* content playing until each replacement segment lands. Today's predicate:
|
|
120
|
+
* `actorCtx.initTrackLanguage !== track.language` — fires for language
|
|
121
|
+
* changes, no-ops for video / same-language audio bitrate switches.
|
|
122
|
+
* Future stage: pluggable predicate / strategy at actor construction time
|
|
123
|
+
* for codec-change (5.1 surround) and other cross-rendition shapes.
|
|
150
124
|
*
|
|
151
125
|
* Case 2 — Init: schedule if not yet committed for this track.
|
|
152
126
|
*
|
|
@@ -158,29 +132,50 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes, config = {}) {
|
|
|
158
132
|
const bufferedSegments = getBufferedSegments(track.segments);
|
|
159
133
|
const currentTime = range?.start ?? 0;
|
|
160
134
|
const tasks = [];
|
|
135
|
+
const isCrossRenditionSwitch = actorCtx.initTrackId !== void 0 && actorCtx.initTrackId !== track.id && actorCtx.initTrackLanguage !== track.language;
|
|
136
|
+
const removes = [];
|
|
137
|
+
const staleRanges = [];
|
|
161
138
|
if (range) {
|
|
139
|
+
if (isCrossRenditionSwitch) {
|
|
140
|
+
const staleStart = actorCtx.segments.find((s) => s.startTime <= currentTime && s.startTime + s.duration > currentTime)?.startTime ?? actorCtx.segments.find((s) => s.startTime > currentTime)?.startTime;
|
|
141
|
+
if (staleStart !== void 0) staleRanges.push({
|
|
142
|
+
start: staleStart,
|
|
143
|
+
end: Infinity
|
|
144
|
+
});
|
|
145
|
+
}
|
|
162
146
|
const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime, forwardBufferConfig);
|
|
163
|
-
if (forwardFlushStart < Infinity)
|
|
164
|
-
type: "remove",
|
|
147
|
+
if (forwardFlushStart < Infinity) removes.push({
|
|
165
148
|
start: forwardFlushStart,
|
|
166
149
|
end: Infinity
|
|
167
150
|
});
|
|
168
151
|
const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime, backBufferConfig);
|
|
169
|
-
if (backFlushEnd > 0)
|
|
170
|
-
type: "remove",
|
|
152
|
+
if (backFlushEnd > 0) removes.push({
|
|
171
153
|
start: 0,
|
|
172
154
|
end: backFlushEnd
|
|
173
155
|
});
|
|
156
|
+
for (const r of removes) tasks.push({
|
|
157
|
+
type: "remove",
|
|
158
|
+
start: r.start,
|
|
159
|
+
end: r.end
|
|
160
|
+
});
|
|
174
161
|
}
|
|
162
|
+
const overlapsStale = (seg) => {
|
|
163
|
+
const segEnd = seg.startTime + seg.duration;
|
|
164
|
+
return removes.some((r) => seg.startTime < r.end && segEnd > r.start) || staleRanges.some((r) => seg.startTime < r.end && segEnd > r.start);
|
|
165
|
+
};
|
|
166
|
+
const effectiveBuffered = removes.length + staleRanges.length > 0 ? bufferedSegments.filter((s) => !overlapsStale(s)) : bufferedSegments;
|
|
175
167
|
if (actorCtx.initTrackId !== track.id) tasks.push({
|
|
176
168
|
type: "append-init",
|
|
177
|
-
meta: {
|
|
169
|
+
meta: {
|
|
170
|
+
trackId: track.id,
|
|
171
|
+
language: track.language
|
|
172
|
+
},
|
|
178
173
|
url: track.initialization.url,
|
|
179
174
|
...track.initialization.byteRange !== void 0 && { byteRange: track.initialization.byteRange }
|
|
180
175
|
});
|
|
181
176
|
if (range) {
|
|
182
|
-
const segmentsToLoad = getSegmentsToLoad(track.segments,
|
|
183
|
-
const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) <
|
|
177
|
+
const segmentsToLoad = getSegmentsToLoad(track.segments, effectiveBuffered, currentTime, forwardBufferConfig).filter((seg) => {
|
|
178
|
+
const existing = actorCtx.segments.find((s) => !overlapsStale(s) && Math.abs(s.startTime - seg.startTime) < 1e-4);
|
|
184
179
|
if (existing?.partial) return true;
|
|
185
180
|
if (!existing?.trackBandwidth || !track.bandwidth) return true;
|
|
186
181
|
return track.bandwidth > existing.trackBandwidth;
|
|
@@ -205,8 +200,8 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes, config = {}) {
|
|
|
205
200
|
runner.schedule(makeLoadTask(op, {
|
|
206
201
|
getContext,
|
|
207
202
|
setContext,
|
|
208
|
-
|
|
209
|
-
|
|
203
|
+
pipelines,
|
|
204
|
+
deps
|
|
210
205
|
})).then(void 0, (e) => {
|
|
211
206
|
if (e instanceof Error && e.name === "AbortError") return;
|
|
212
207
|
console.error("Unexpected error in segment loader:", e);
|
|
@@ -219,7 +214,7 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes, config = {}) {
|
|
|
219
214
|
initial: "idle",
|
|
220
215
|
context: {
|
|
221
216
|
inFlightInitTrackId: null,
|
|
222
|
-
|
|
217
|
+
inFlightSegment: null
|
|
223
218
|
},
|
|
224
219
|
states: {
|
|
225
220
|
idle: { on: { load: (msg, ctx) => {
|
|
@@ -233,12 +228,14 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes, config = {}) {
|
|
|
233
228
|
on: { load: (msg, ctx) => {
|
|
234
229
|
const { context, runner } = ctx;
|
|
235
230
|
const allTasks = planTasks(msg);
|
|
236
|
-
|
|
231
|
+
const inFlight = context.inFlightSegment;
|
|
232
|
+
const segmentInFlightStillNeeded = (t) => t.type === "append-segment" && inFlight !== null && t.meta.id === inFlight.id && t.meta.trackId === inFlight.trackId;
|
|
233
|
+
if (inFlight !== null && allTasks.some(segmentInFlightStillNeeded) || context.inFlightInitTrackId !== null && allTasks.some((t) => t.type === "append-init" && t.meta.trackId === context.inFlightInitTrackId)) {
|
|
237
234
|
runner.abortPending();
|
|
238
|
-
scheduleAll(allTasks.filter((t) => !(t
|
|
235
|
+
scheduleAll(allTasks.filter((t) => !segmentInFlightStillNeeded(t) && !(t.type === "append-init" && t.meta.trackId === context.inFlightInitTrackId)), ctx);
|
|
239
236
|
} else {
|
|
240
237
|
runner.abortAll();
|
|
241
|
-
if (context.
|
|
238
|
+
if (context.inFlightSegment !== null || context.inFlightInitTrackId !== null && allTasks.some((t) => t.type === "append-init" && t.meta.trackId !== context.inFlightInitTrackId)) sourceBufferActor.send({ type: "cancel" });
|
|
242
239
|
scheduleAll(allTasks, ctx);
|
|
243
240
|
}
|
|
244
241
|
} }
|