@videojs/spf 10.0.0-beta.30 → 10.0.0-beta.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/default/core/actors/create-machine-actor.js +23 -25
- package/dist/default/core/actors/create-machine-actor.js.map +1 -1
- package/dist/default/core/actors/create-transition-actor.js +7 -9
- package/dist/default/core/actors/create-transition-actor.js.map +1 -1
- package/dist/default/core/composition/create-composition.js +26 -29
- package/dist/default/core/composition/create-composition.js.map +1 -1
- package/dist/default/core/composition/share-signals.js +12 -17
- package/dist/default/core/composition/share-signals.js.map +1 -1
- package/dist/default/core/machine.js +3 -4
- package/dist/default/core/machine.js.map +1 -1
- package/dist/default/core/reactors/create-machine-reactor.js +20 -19
- package/dist/default/core/reactors/create-machine-reactor.js.map +1 -1
- package/dist/default/core/signals/effect.js +24 -4
- package/dist/default/core/signals/effect.js.map +1 -1
- package/dist/default/core/signals/primitives.js +10 -14
- package/dist/default/core/signals/primitives.js.map +1 -1
- package/dist/default/core/signals/when.js +6 -8
- package/dist/default/core/signals/when.js.map +1 -1
- package/dist/default/core/tasks/delayed-reschedule.js +7 -11
- package/dist/default/core/tasks/delayed-reschedule.js.map +1 -1
- package/dist/default/core/tasks/task.js +59 -82
- package/dist/default/core/tasks/task.js.map +1 -1
- package/dist/default/hls.js +3 -1
- package/dist/default/index.js +2 -3
- package/dist/default/index.js.map +1 -1
- package/dist/default/media/abr/quality-selection.js +3 -7
- package/dist/default/media/abr/quality-selection.js.map +1 -1
- package/dist/default/media/buffer/back-buffer.js +15 -19
- package/dist/default/media/buffer/back-buffer.js.map +1 -1
- package/dist/default/media/buffer/forward-buffer.js +32 -41
- package/dist/default/media/buffer/forward-buffer.js.map +1 -1
- package/dist/default/media/dom/capabilities.js +26 -37
- package/dist/default/media/dom/capabilities.js.map +1 -1
- package/dist/default/media/dom/mse/append-segment.js +3 -5
- package/dist/default/media/dom/mse/append-segment.js.map +1 -1
- package/dist/default/media/dom/mse/buffer-flusher.js +5 -5
- package/dist/default/media/dom/mse/buffer-flusher.js.map +1 -1
- package/dist/default/media/dom/mse/duration.js +23 -31
- package/dist/default/media/dom/mse/duration.js.map +1 -1
- package/dist/default/media/dom/mse/end-of-stream.js +3 -6
- package/dist/default/media/dom/mse/end-of-stream.js.map +1 -1
- package/dist/default/media/dom/mse/mediasource-setup.js +65 -81
- package/dist/default/media/dom/mse/mediasource-setup.js.map +1 -1
- package/dist/default/media/dom/screen.js +37 -54
- package/dist/default/media/dom/screen.js.map +1 -1
- package/dist/default/media/dom/text/resolve-vtt-segment.js.map +1 -1
- package/dist/default/media/dom/text/text-track-slots.js +15 -23
- package/dist/default/media/dom/text/text-track-slots.js.map +1 -1
- package/dist/default/media/errors.js +25 -34
- package/dist/default/media/errors.js.map +1 -1
- package/dist/default/media/hls/parse-attributes.js +9 -26
- package/dist/default/media/hls/parse-attributes.js.map +1 -1
- package/dist/default/media/hls/parse-media-playlist.js +25 -33
- package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
- package/dist/default/media/hls/parse-multivariant.js +3 -3
- package/dist/default/media/hls/parse-multivariant.js.map +1 -1
- package/dist/default/media/hls/reload-policy.js +19 -27
- package/dist/default/media/hls/reload-policy.js.map +1 -1
- package/dist/default/media/hls/resolve-url.js +1 -3
- package/dist/default/media/hls/resolve-url.js.map +1 -1
- package/dist/default/media/live-window.js +9 -13
- package/dist/default/media/live-window.js.map +1 -1
- package/dist/default/media/media-tracks/media-tracks.js +12 -17
- package/dist/default/media/media-tracks/media-tracks.js.map +1 -1
- package/dist/default/media/mp4/box.js +4 -5
- package/dist/default/media/mp4/box.js.map +1 -1
- package/dist/default/media/mp4/timestamp-origin.js +30 -42
- package/dist/default/media/mp4/timestamp-origin.js.map +1 -1
- package/dist/default/media/primitives/resolution.js +5 -8
- package/dist/default/media/primitives/resolution.js.map +1 -1
- package/dist/default/media/primitives/select-tracks.js +24 -34
- package/dist/default/media/primitives/select-tracks.js.map +1 -1
- package/dist/default/media/text/parse-vtt-timestamp-map.js +6 -8
- package/dist/default/media/text/parse-vtt-timestamp-map.js.map +1 -1
- package/dist/default/media/text/resolve-vtt-metadata.js +8 -9
- package/dist/default/media/text/resolve-vtt-metadata.js.map +1 -1
- package/dist/default/media/types/index.js +10 -18
- package/dist/default/media/types/index.js.map +1 -1
- package/dist/default/media/utils/cdn.js +10 -16
- package/dist/default/media/utils/cdn.js.map +1 -1
- package/dist/default/media/utils/preload.js +4 -6
- package/dist/default/media/utils/preload.js.map +1 -1
- package/dist/default/media/utils/track-selection.js +10 -14
- package/dist/default/media/utils/track-selection.js.map +1 -1
- package/dist/default/media/utils/tracks.js +56 -54
- package/dist/default/media/utils/tracks.js.map +1 -1
- package/dist/default/network/bandwidth-estimator.js +18 -19
- package/dist/default/network/bandwidth-estimator.js.map +1 -1
- package/dist/default/network/chunked-stream-iterable.js +4 -6
- package/dist/default/network/chunked-stream-iterable.js.map +1 -1
- package/dist/default/network/ewma.js +22 -25
- package/dist/default/network/ewma.js.map +1 -1
- package/dist/default/network/fetch.js +30 -37
- package/dist/default/network/fetch.js.map +1 -1
- package/dist/default/playback/actors/dom/segment-loader.js +28 -44
- package/dist/default/playback/actors/dom/segment-loader.js.map +1 -1
- package/dist/default/playback/actors/dom/source-buffer.js.map +1 -1
- package/dist/default/playback/actors/dom/text-tracks.js.map +1 -1
- package/dist/default/playback/actors/text-track-segment-loader.js +24 -35
- package/dist/default/playback/actors/text-track-segment-loader.js.map +1 -1
- package/dist/default/playback/adapters/hls-audio/adapter.js +23 -31
- package/dist/default/playback/adapters/hls-audio/adapter.js.map +1 -1
- package/dist/default/playback/adapters/hls-audio/media.js.map +1 -1
- package/dist/default/playback/adapters/hls-background-video/adapter.js +50 -70
- package/dist/default/playback/adapters/hls-background-video/adapter.js.map +1 -1
- package/dist/default/playback/adapters/hls-background-video/host.js +12 -16
- package/dist/default/playback/adapters/hls-background-video/host.js.map +1 -1
- package/dist/default/playback/adapters/hls-background-video/media.js +5 -8
- package/dist/default/playback/adapters/hls-background-video/media.js.map +1 -1
- package/dist/default/playback/adapters/hls-video/adapter.js +43 -60
- package/dist/default/playback/adapters/hls-video/adapter.js.map +1 -1
- package/dist/default/playback/adapters/hls-video/error-surface.js +13 -18
- package/dist/default/playback/adapters/hls-video/error-surface.js.map +1 -1
- package/dist/default/playback/adapters/hls-video/media-tracks.js +4 -5
- package/dist/default/playback/adapters/hls-video/media-tracks.js.map +1 -1
- package/dist/default/playback/adapters/hls-video/media.js.map +1 -1
- package/dist/default/playback/adapters/mux-audio/media.js +8 -12
- package/dist/default/playback/adapters/mux-audio/media.js.map +1 -1
- package/dist/default/playback/adapters/mux-video/adapter.js +25 -33
- package/dist/default/playback/adapters/mux-video/adapter.js.map +1 -1
- package/dist/default/playback/adapters/mux-video/media.js +7 -9
- package/dist/default/playback/adapters/mux-video/media.js.map +1 -1
- package/dist/default/playback/behaviors/calculate-presentation-duration.js +5 -8
- package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -1
- package/dist/default/playback/behaviors/collect-errors.js +39 -52
- package/dist/default/playback/behaviors/collect-errors.js.map +1 -1
- package/dist/default/playback/behaviors/derive-cdn-priority.js +16 -23
- package/dist/default/playback/behaviors/derive-cdn-priority.js.map +1 -1
- package/dist/default/playback/behaviors/dom/airplay.js +38 -61
- package/dist/default/playback/behaviors/dom/airplay.js.map +1 -1
- package/dist/default/playback/behaviors/dom/apply-start-position.js +23 -35
- package/dist/default/playback/behaviors/dom/apply-start-position.js.map +1 -1
- package/dist/default/playback/behaviors/dom/end-of-stream.js +11 -16
- package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
- package/dist/default/playback/behaviors/dom/load-segments.js +27 -39
- package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -1
- package/dist/default/playback/behaviors/dom/recover-end-stall.js +21 -26
- package/dist/default/playback/behaviors/dom/recover-end-stall.js.map +1 -1
- package/dist/default/playback/behaviors/dom/seek-to-live-edge.js +41 -60
- package/dist/default/playback/behaviors/dom/seek-to-live-edge.js.map +1 -1
- package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +50 -74
- package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
- package/dist/default/playback/behaviors/dom/setup-mediasource.js +29 -43
- package/dist/default/playback/behaviors/dom/setup-mediasource.js.map +1 -1
- package/dist/default/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
- package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js +3 -4
- package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -1
- package/dist/default/playback/behaviors/dom/sync-text-tracks.js +30 -45
- package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-current-time.js +9 -11
- package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-load-triggers.js +24 -32
- package/dist/default/playback/behaviors/dom/track-load-triggers.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-playback-rate.js +6 -8
- package/dist/default/playback/behaviors/dom/track-playback-rate.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-player-resolution.js +10 -14
- package/dist/default/playback/behaviors/dom/track-player-resolution.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-screen-resolution.js +8 -9
- package/dist/default/playback/behaviors/dom/track-screen-resolution.js.map +1 -1
- package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +28 -43
- package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
- package/dist/default/playback/behaviors/establish-start-media-time.js +32 -42
- package/dist/default/playback/behaviors/establish-start-media-time.js.map +1 -1
- package/dist/default/playback/behaviors/resolve-presentation.js +13 -22
- package/dist/default/playback/behaviors/resolve-presentation.js.map +1 -1
- package/dist/default/playback/behaviors/resolve-track.js +5 -12
- package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
- package/dist/default/playback/behaviors/select-tracks.js +77 -104
- package/dist/default/playback/behaviors/select-tracks.js.map +1 -1
- package/dist/default/playback/behaviors/setup-failover-monitor.js +11 -16
- package/dist/default/playback/behaviors/setup-failover-monitor.js.map +1 -1
- package/dist/default/playback/behaviors/sync-preload.js +9 -13
- package/dist/default/playback/behaviors/sync-preload.js.map +1 -1
- package/dist/default/playback/behaviors/track-switching.js +189 -173
- package/dist/default/playback/behaviors/track-switching.js.map +1 -1
- package/dist/default/playback/engines/hls/engine-audio-only.js +20 -24
- package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -1
- package/dist/default/playback/engines/hls/engine-background-video.js +22 -27
- package/dist/default/playback/engines/hls/engine-background-video.js.map +1 -1
- package/dist/default/playback/engines/hls/engine.js +22 -25
- package/dist/default/playback/engines/hls/engine.js.map +1 -1
- package/dist/default/playback/primitives/error-messages.js +16 -25
- package/dist/default/playback/primitives/error-messages.js.map +1 -1
- package/dist/default/playback/primitives/failover-fetch.js +9 -14
- package/dist/default/playback/primitives/failover-fetch.js.map +1 -1
- package/dist/default/playback/primitives/head-peek.js +7 -10
- package/dist/default/playback/primitives/head-peek.js.map +1 -1
- package/dist/default/playback/primitives/live-window.js +12 -15
- package/dist/default/playback/primitives/live-window.js.map +1 -1
- package/dist/default/playback/primitives/relocation-pipelines.js +34 -46
- package/dist/default/playback/primitives/relocation-pipelines.js.map +1 -1
- package/dist/default/playback/primitives/report-track-conditions.js +26 -38
- package/dist/default/playback/primitives/report-track-conditions.js.map +1 -1
- package/dist/default/playback/primitives/segment-load-pipeline.js +15 -15
- package/dist/default/playback/primitives/segment-load-pipeline.js.map +1 -1
- package/dist/default/playback/primitives/selection-rules.js +65 -36
- package/dist/default/playback/primitives/selection-rules.js.map +1 -1
- package/dist/default/playback/primitives/text-segment-load-pipeline.js +4 -5
- package/dist/default/playback/primitives/text-segment-load-pipeline.js.map +1 -1
- package/dist/default/playback/primitives/track-types.js +10 -18
- package/dist/default/playback/primitives/track-types.js.map +1 -1
- package/dist/dev/core/actors/actor.d.ts +4 -4
- package/dist/dev/core/actors/actor.d.ts.map +1 -1
- package/dist/dev/core/actors/create-machine-actor.d.ts +38 -47
- package/dist/dev/core/actors/create-machine-actor.d.ts.map +1 -1
- package/dist/dev/core/actors/create-machine-actor.js +23 -25
- package/dist/dev/core/actors/create-machine-actor.js.map +1 -1
- package/dist/dev/core/actors/create-transition-actor.d.ts +11 -15
- package/dist/dev/core/actors/create-transition-actor.d.ts.map +1 -1
- package/dist/dev/core/actors/create-transition-actor.js +7 -9
- package/dist/dev/core/actors/create-transition-actor.js.map +1 -1
- package/dist/dev/core/composition/create-composition.d.ts +70 -102
- package/dist/dev/core/composition/create-composition.d.ts.map +1 -1
- package/dist/dev/core/composition/create-composition.js +26 -29
- package/dist/dev/core/composition/create-composition.js.map +1 -1
- package/dist/dev/core/composition/share-signals.d.ts +17 -24
- package/dist/dev/core/composition/share-signals.d.ts.map +1 -1
- package/dist/dev/core/composition/share-signals.js +12 -17
- package/dist/dev/core/composition/share-signals.js.map +1 -1
- package/dist/dev/core/machine.d.ts +4 -4
- package/dist/dev/core/machine.js +3 -4
- package/dist/dev/core/machine.js.map +1 -1
- package/dist/dev/core/reactors/create-machine-reactor.d.ts +33 -39
- package/dist/dev/core/reactors/create-machine-reactor.d.ts.map +1 -1
- package/dist/dev/core/reactors/create-machine-reactor.js +20 -19
- package/dist/dev/core/reactors/create-machine-reactor.js.map +1 -1
- package/dist/dev/core/signals/effect.d.ts +3 -4
- package/dist/dev/core/signals/effect.d.ts.map +1 -1
- package/dist/dev/core/signals/effect.js +24 -4
- package/dist/dev/core/signals/effect.js.map +1 -1
- package/dist/dev/core/signals/primitives.d.ts +10 -12
- package/dist/dev/core/signals/primitives.d.ts.map +1 -1
- package/dist/dev/core/signals/primitives.js +10 -14
- package/dist/dev/core/signals/primitives.js.map +1 -1
- package/dist/dev/core/signals/when.js +6 -8
- package/dist/dev/core/signals/when.js.map +1 -1
- package/dist/dev/core/tasks/delayed-reschedule.js +7 -11
- package/dist/dev/core/tasks/delayed-reschedule.js.map +1 -1
- package/dist/dev/core/tasks/task.d.ts +51 -73
- package/dist/dev/core/tasks/task.d.ts.map +1 -1
- package/dist/dev/core/tasks/task.js +59 -82
- package/dist/dev/core/tasks/task.js.map +1 -1
- package/dist/dev/hls.d.ts +3 -1
- package/dist/dev/hls.js +3 -1
- package/dist/dev/index.d.ts +2 -3
- package/dist/dev/index.d.ts.map +1 -1
- package/dist/dev/index.js +2 -3
- package/dist/dev/index.js.map +1 -1
- package/dist/dev/media/abr/quality-selection.d.ts +7 -10
- package/dist/dev/media/abr/quality-selection.d.ts.map +1 -1
- package/dist/dev/media/abr/quality-selection.js +3 -7
- package/dist/dev/media/abr/quality-selection.js.map +1 -1
- package/dist/dev/media/buffer/back-buffer.d.ts +2 -7
- package/dist/dev/media/buffer/back-buffer.d.ts.map +1 -1
- package/dist/dev/media/buffer/back-buffer.js +15 -19
- package/dist/dev/media/buffer/back-buffer.js.map +1 -1
- package/dist/dev/media/buffer/forward-buffer.d.ts +2 -7
- package/dist/dev/media/buffer/forward-buffer.d.ts.map +1 -1
- package/dist/dev/media/buffer/forward-buffer.js +32 -41
- package/dist/dev/media/buffer/forward-buffer.js.map +1 -1
- package/dist/dev/media/dom/capabilities.js +26 -37
- package/dist/dev/media/dom/capabilities.js.map +1 -1
- package/dist/dev/media/dom/mse/append-segment.d.ts +3 -5
- package/dist/dev/media/dom/mse/append-segment.d.ts.map +1 -1
- package/dist/dev/media/dom/mse/append-segment.js +3 -5
- package/dist/dev/media/dom/mse/append-segment.js.map +1 -1
- package/dist/dev/media/dom/mse/buffer-flusher.d.ts +5 -5
- package/dist/dev/media/dom/mse/buffer-flusher.js +5 -5
- package/dist/dev/media/dom/mse/buffer-flusher.js.map +1 -1
- package/dist/dev/media/dom/mse/duration.js +23 -31
- package/dist/dev/media/dom/mse/duration.js.map +1 -1
- package/dist/dev/media/dom/mse/end-of-stream.js +3 -6
- package/dist/dev/media/dom/mse/end-of-stream.js.map +1 -1
- package/dist/dev/media/dom/mse/mediasource-setup.js +65 -81
- package/dist/dev/media/dom/mse/mediasource-setup.js.map +1 -1
- package/dist/dev/media/dom/screen.d.ts.map +1 -1
- package/dist/dev/media/dom/screen.js +37 -54
- package/dist/dev/media/dom/screen.js.map +1 -1
- package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts.map +1 -1
- package/dist/dev/media/dom/text/resolve-vtt-segment.js.map +1 -1
- package/dist/dev/media/dom/text/text-track-slots.d.ts +10 -16
- package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -1
- package/dist/dev/media/dom/text/text-track-slots.js +15 -23
- package/dist/dev/media/dom/text/text-track-slots.js.map +1 -1
- package/dist/dev/media/errors.d.ts +32 -46
- package/dist/dev/media/errors.d.ts.map +1 -1
- package/dist/dev/media/errors.js +25 -34
- package/dist/dev/media/errors.js.map +1 -1
- package/dist/dev/media/hls/parse-attributes.js +9 -26
- package/dist/dev/media/hls/parse-attributes.js.map +1 -1
- package/dist/dev/media/hls/parse-media-playlist.js +25 -33
- package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
- package/dist/dev/media/hls/parse-multivariant.js +3 -3
- package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
- package/dist/dev/media/hls/reload-policy.js +19 -27
- package/dist/dev/media/hls/reload-policy.js.map +1 -1
- package/dist/dev/media/hls/resolve-url.js +1 -3
- package/dist/dev/media/hls/resolve-url.js.map +1 -1
- package/dist/dev/media/live-window.js +9 -13
- package/dist/dev/media/live-window.js.map +1 -1
- package/dist/dev/media/media-tracks/media-tracks.d.ts +8 -13
- package/dist/dev/media/media-tracks/media-tracks.d.ts.map +1 -1
- package/dist/dev/media/media-tracks/media-tracks.js +12 -17
- package/dist/dev/media/media-tracks/media-tracks.js.map +1 -1
- package/dist/dev/media/mp4/box.js +4 -5
- package/dist/dev/media/mp4/box.js.map +1 -1
- package/dist/dev/media/mp4/timestamp-origin.js +30 -42
- package/dist/dev/media/mp4/timestamp-origin.js.map +1 -1
- package/dist/dev/media/primitives/resolution.d.ts +6 -8
- package/dist/dev/media/primitives/resolution.d.ts.map +1 -1
- package/dist/dev/media/primitives/resolution.js +5 -8
- package/dist/dev/media/primitives/resolution.js.map +1 -1
- package/dist/dev/media/primitives/select-tracks.js +24 -34
- package/dist/dev/media/primitives/select-tracks.js.map +1 -1
- package/dist/dev/media/text/parse-vtt-timestamp-map.js +6 -8
- package/dist/dev/media/text/parse-vtt-timestamp-map.js.map +1 -1
- package/dist/dev/media/text/resolve-vtt-metadata.js +8 -9
- package/dist/dev/media/text/resolve-vtt-metadata.js.map +1 -1
- package/dist/dev/media/types/index.d.ts +106 -177
- package/dist/dev/media/types/index.d.ts.map +1 -1
- package/dist/dev/media/types/index.js +10 -18
- package/dist/dev/media/types/index.js.map +1 -1
- package/dist/dev/media/utils/cdn.d.ts +4 -6
- package/dist/dev/media/utils/cdn.d.ts.map +1 -1
- package/dist/dev/media/utils/cdn.js +10 -16
- package/dist/dev/media/utils/cdn.js.map +1 -1
- package/dist/dev/media/utils/preload.js +4 -6
- package/dist/dev/media/utils/preload.js.map +1 -1
- package/dist/dev/media/utils/track-selection.js +10 -14
- package/dist/dev/media/utils/track-selection.js.map +1 -1
- package/dist/dev/media/utils/tracks.js +56 -54
- package/dist/dev/media/utils/tracks.js.map +1 -1
- package/dist/dev/network/bandwidth-estimator.d.ts +7 -9
- package/dist/dev/network/bandwidth-estimator.d.ts.map +1 -1
- package/dist/dev/network/bandwidth-estimator.js +18 -19
- package/dist/dev/network/bandwidth-estimator.js.map +1 -1
- package/dist/dev/network/chunked-stream-iterable.js +4 -6
- package/dist/dev/network/chunked-stream-iterable.js.map +1 -1
- package/dist/dev/network/ewma.js +22 -25
- package/dist/dev/network/ewma.js.map +1 -1
- package/dist/dev/network/fetch.js +30 -37
- package/dist/dev/network/fetch.js.map +1 -1
- package/dist/dev/playback/actors/dom/segment-loader.d.ts +7 -11
- package/dist/dev/playback/actors/dom/segment-loader.d.ts.map +1 -1
- package/dist/dev/playback/actors/dom/segment-loader.js +28 -44
- package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -1
- package/dist/dev/playback/actors/dom/source-buffer.d.ts +5 -7
- package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -1
- package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -1
- package/dist/dev/playback/actors/dom/text-tracks.js.map +1 -1
- package/dist/dev/playback/actors/text-track-segment-loader.d.ts +10 -16
- package/dist/dev/playback/actors/text-track-segment-loader.d.ts.map +1 -1
- package/dist/dev/playback/actors/text-track-segment-loader.js +24 -35
- package/dist/dev/playback/actors/text-track-segment-loader.js.map +1 -1
- package/dist/dev/playback/actors/text-tracks.d.ts +5 -8
- package/dist/dev/playback/actors/text-tracks.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-audio/adapter.d.ts +11 -14
- package/dist/dev/playback/adapters/hls-audio/adapter.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-audio/adapter.js +23 -31
- package/dist/dev/playback/adapters/hls-audio/adapter.js.map +1 -1
- package/dist/dev/playback/adapters/hls-audio/media.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-audio/media.js.map +1 -1
- package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts +28 -39
- package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-background-video/adapter.js +50 -70
- package/dist/dev/playback/adapters/hls-background-video/adapter.js.map +1 -1
- package/dist/dev/playback/adapters/hls-background-video/host.d.ts +12 -16
- package/dist/dev/playback/adapters/hls-background-video/host.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-background-video/host.js +12 -16
- package/dist/dev/playback/adapters/hls-background-video/host.js.map +1 -1
- package/dist/dev/playback/adapters/hls-background-video/media.d.ts +5 -8
- package/dist/dev/playback/adapters/hls-background-video/media.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-background-video/media.js +5 -8
- package/dist/dev/playback/adapters/hls-background-video/media.js.map +1 -1
- package/dist/dev/playback/adapters/hls-video/adapter.d.ts +12 -14
- package/dist/dev/playback/adapters/hls-video/adapter.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-video/adapter.js +43 -60
- package/dist/dev/playback/adapters/hls-video/adapter.js.map +1 -1
- package/dist/dev/playback/adapters/hls-video/error-surface.d.ts +4 -5
- package/dist/dev/playback/adapters/hls-video/error-surface.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-video/error-surface.js +13 -18
- package/dist/dev/playback/adapters/hls-video/error-surface.js.map +1 -1
- package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts +4 -5
- package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-video/media-tracks.js +4 -5
- package/dist/dev/playback/adapters/hls-video/media-tracks.js.map +1 -1
- package/dist/dev/playback/adapters/hls-video/media.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-video/media.js.map +1 -1
- package/dist/dev/playback/adapters/mux-audio/media.d.ts +8 -12
- package/dist/dev/playback/adapters/mux-audio/media.d.ts.map +1 -1
- package/dist/dev/playback/adapters/mux-audio/media.js +8 -12
- package/dist/dev/playback/adapters/mux-audio/media.js.map +1 -1
- package/dist/dev/playback/adapters/mux-video/adapter.d.ts +9 -11
- package/dist/dev/playback/adapters/mux-video/adapter.d.ts.map +1 -1
- package/dist/dev/playback/adapters/mux-video/adapter.js +25 -33
- package/dist/dev/playback/adapters/mux-video/adapter.js.map +1 -1
- package/dist/dev/playback/adapters/mux-video/media.d.ts +7 -9
- package/dist/dev/playback/adapters/mux-video/media.d.ts.map +1 -1
- package/dist/dev/playback/adapters/mux-video/media.js +7 -9
- package/dist/dev/playback/adapters/mux-video/media.js.map +1 -1
- package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +5 -8
- package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/calculate-presentation-duration.js +5 -8
- package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -1
- package/dist/dev/playback/behaviors/collect-errors.d.ts +2 -0
- package/dist/dev/playback/behaviors/collect-errors.js +39 -52
- package/dist/dev/playback/behaviors/collect-errors.js.map +1 -1
- package/dist/dev/playback/behaviors/derive-cdn-priority.js +16 -23
- package/dist/dev/playback/behaviors/derive-cdn-priority.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/airplay.js +38 -61
- package/dist/dev/playback/behaviors/dom/airplay.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/apply-start-position.js +23 -35
- package/dist/dev/playback/behaviors/dom/apply-start-position.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/end-of-stream.js +11 -16
- package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/load-segments.d.ts +5 -8
- package/dist/dev/playback/behaviors/dom/load-segments.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/load-segments.js +27 -39
- package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/recover-end-stall.js +21 -26
- package/dist/dev/playback/behaviors/dom/recover-end-stall.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js +41 -60
- package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +50 -74
- package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/setup-mediasource.js +29 -43
- package/dist/dev/playback/behaviors/dom/setup-mediasource.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts +3 -3
- package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js +3 -4
- package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/sync-text-tracks.js +30 -45
- package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-current-time.d.ts +2 -2
- package/dist/dev/playback/behaviors/dom/track-current-time.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-current-time.js +9 -11
- package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts +6 -12
- package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-load-triggers.js +24 -32
- package/dist/dev/playback/behaviors/dom/track-load-triggers.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts +2 -2
- package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-playback-rate.js +6 -8
- package/dist/dev/playback/behaviors/dom/track-playback-rate.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-player-resolution.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-player-resolution.js +10 -14
- package/dist/dev/playback/behaviors/dom/track-player-resolution.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-screen-resolution.js +8 -9
- package/dist/dev/playback/behaviors/dom/track-screen-resolution.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +28 -43
- package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
- package/dist/dev/playback/behaviors/establish-start-media-time.d.ts +15 -19
- package/dist/dev/playback/behaviors/establish-start-media-time.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/establish-start-media-time.js +32 -42
- package/dist/dev/playback/behaviors/establish-start-media-time.js.map +1 -1
- package/dist/dev/playback/behaviors/resolve-presentation.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/resolve-presentation.js +13 -22
- package/dist/dev/playback/behaviors/resolve-presentation.js.map +1 -1
- package/dist/dev/playback/behaviors/resolve-track.js +5 -12
- package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
- package/dist/dev/playback/behaviors/select-tracks.d.ts +28 -39
- package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/select-tracks.js +77 -104
- package/dist/dev/playback/behaviors/select-tracks.js.map +1 -1
- package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts +1 -4
- package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/setup-failover-monitor.js +11 -16
- package/dist/dev/playback/behaviors/setup-failover-monitor.js.map +1 -1
- package/dist/dev/playback/behaviors/sync-preload.js +9 -13
- package/dist/dev/playback/behaviors/sync-preload.js.map +1 -1
- package/dist/dev/playback/behaviors/track-switching.d.ts +110 -0
- package/dist/dev/playback/behaviors/track-switching.d.ts.map +1 -0
- package/dist/dev/playback/behaviors/track-switching.js +189 -173
- package/dist/dev/playback/behaviors/track-switching.js.map +1 -1
- package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +59 -72
- package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/engine-audio-only.js +20 -24
- package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -1
- package/dist/dev/playback/engines/hls/engine-background-video.d.ts +51 -78
- package/dist/dev/playback/engines/hls/engine-background-video.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/engine-background-video.js +22 -27
- package/dist/dev/playback/engines/hls/engine-background-video.js.map +1 -1
- package/dist/dev/playback/engines/hls/engine.d.ts +115 -155
- package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/engine.js +22 -25
- package/dist/dev/playback/engines/hls/engine.js.map +1 -1
- package/dist/dev/playback/primitives/derive-start-media-time.d.ts +3 -3
- package/dist/dev/playback/primitives/derive-start-media-time.d.ts.map +1 -1
- package/dist/dev/playback/primitives/error-messages.js +16 -25
- package/dist/dev/playback/primitives/error-messages.js.map +1 -1
- package/dist/dev/playback/primitives/failover-fetch.js +9 -14
- package/dist/dev/playback/primitives/failover-fetch.js.map +1 -1
- package/dist/dev/playback/primitives/head-peek.js +7 -10
- package/dist/dev/playback/primitives/head-peek.js.map +1 -1
- package/dist/dev/playback/primitives/live-window.js +12 -15
- package/dist/dev/playback/primitives/live-window.js.map +1 -1
- package/dist/dev/playback/primitives/relocation-pipelines.js +34 -46
- package/dist/dev/playback/primitives/relocation-pipelines.js.map +1 -1
- package/dist/dev/playback/primitives/report-track-conditions.d.ts +1 -4
- package/dist/dev/playback/primitives/report-track-conditions.d.ts.map +1 -1
- package/dist/dev/playback/primitives/report-track-conditions.js +26 -38
- package/dist/dev/playback/primitives/report-track-conditions.js.map +1 -1
- package/dist/dev/playback/primitives/segment-load-pipeline.js +15 -15
- package/dist/dev/playback/primitives/segment-load-pipeline.js.map +1 -1
- package/dist/dev/playback/primitives/selection-rules.d.ts +45 -17
- package/dist/dev/playback/primitives/selection-rules.d.ts.map +1 -1
- package/dist/dev/playback/primitives/selection-rules.js +65 -36
- package/dist/dev/playback/primitives/selection-rules.js.map +1 -1
- package/dist/dev/playback/primitives/source-buffer-messages.d.ts +6 -9
- package/dist/dev/playback/primitives/source-buffer-messages.d.ts.map +1 -1
- package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts +20 -29
- package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts.map +1 -1
- package/dist/dev/playback/primitives/text-segment-load-pipeline.js +4 -5
- package/dist/dev/playback/primitives/text-segment-load-pipeline.js.map +1 -1
- package/dist/dev/playback/primitives/text-track-messages.d.ts.map +1 -1
- package/dist/dev/playback/primitives/track-types.js +10 -18
- package/dist/dev/playback/primitives/track-types.js.map +1 -1
- package/package.json +25 -28
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply-start-position.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/apply-start-position.ts"],"sourcesContent":["/**\n * **Start playback of a source at a requested position.** `state.startPosition
|
|
1
|
+
{"version":3,"file":"apply-start-position.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/apply-start-position.ts"],"sourcesContent":["/**\n * **Start playback of a source at a requested position.** `state.startPosition` is a one-shot command — \"when the\n * current source can seek, start there\" — the SPF analogue of hls.js's `startPosition` (and the primitive\n * `EXT-X-START`, resume-where-you-left-off, and MediaSource-recovery restore build on). Consumers (adapters,\n * `setupAirPlay`'s session-end snapshot) write it; this behavior is its sole consumer and clears it after applying, so\n * a stale position can never replay against a later source or rebuild.\n *\n * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'position-pending'`): gated on `mediaElement + resolved\n * presentation + startPosition` defined. The entry applies the command in two steps:\n *\n * 1. **Seed `state.currentTime` immediately.** The segment loaders anchor their load window on `state.currentTime`;\n * seeding points the _first_ fetches at the requested position instead of 0. Multi-writer with `trackCurrentTime`\n * (ongoing DOM mirror) — legitimate: different decision domains (element-derived mirror vs one-shot command), and\n * before HAVE_METADATA no `timeupdate`/`seeking` fires to overwrite the seed. Compose this behavior _after_\n * `trackCurrentTime` so the seed lands after the mirror's attach-time sync.\n * 2. **Seek the element at metadata.** `element.currentTime = position` once `readyState >= HAVE_METADATA` (immediately if\n * already there, else on `loadedmetadata`), then clear `startPosition` (consume). The element clamps the seek to its\n * seekable range per spec, and the resulting `seeking` event flows back through `trackCurrentTime` — from here the\n * ordinary seek path owns the position.\n *\n * Position only — playing/paused is deliberately out of scope. The media element load algorithm forces `paused = true`,\n * so a source that was playing before a rebuild comes back paused at the restored position; resume intent belongs to\n * whoever commands the start (e.g. `setupAirPlay` restores its session-end playing state itself).\n *\n * Deliberately NOT relying on the pre-metadata \"default playback start position\" write (setting `currentTime` at\n * HAVE_NOTHING): cross-browser MSE behavior there is inconsistent; the explicit `loadedmetadata` sequencing is\n * deterministic everywhere.\n *\n * State-exit cleanup (source reset, element detach, destroy) drops the pending `loadedmetadata` listener. An\n * _unapplied_ command survives a source reset — \"start the source I'm loading at P\" holds while the presentation routes\n * through unresolved — but is consumed the moment it applies.\n */\n\nimport { listen } from '@videojs/utils/dom';\nimport { isUndefined } from '@videojs/utils/predicate';\n\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport { isResolvedPresentation, type MaybeResolvedPresentation } from '../../../media/types';\n\nexport interface StartPositionState {\n presentation?: MaybeResolvedPresentation;\n /**\n * One-shot start-position command in presentation-timeline seconds. Written by consumers (adapter, recovery\n * snapshot); consumed (cleared) by `applyStartPosition` once the element seeks.\n */\n startPosition?: number;\n currentTime?: number;\n}\n\nexport interface StartPositionContext {\n mediaElement?: HTMLMediaElement | undefined;\n}\n\ntype StartPositionFsmState = 'preconditions-unmet' | 'position-pending';\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaElement: HTMLMediaElement | undefined,\n startPosition: number | undefined\n): StartPositionFsmState {\n if (!mediaElement || !isResolvedPresentation(presentation)) return 'preconditions-unmet';\n\n if (isUndefined(startPosition)) return 'preconditions-unmet';\n\n return 'position-pending';\n}\n\nfunction applyStartPositionSetup({\n state,\n context,\n}: {\n state: {\n presentation: ReadonlySignal<StartPositionState['presentation']>;\n startPosition: Signal<StartPositionState['startPosition']>;\n currentTime: Signal<StartPositionState['currentTime']>;\n };\n context: {\n mediaElement: ReadonlySignal<StartPositionContext['mediaElement']>;\n };\n}): Reactor<StartPositionFsmState | 'destroying' | 'destroyed'> {\n const derivedStateSignal = computed(() =>\n deriveState(state.presentation.get(), context.mediaElement.get(), state.startPosition.get())\n );\n\n return createMachineReactor<StartPositionFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'position-pending': {\n // entry body is auto-untracked. Consuming the command flips the\n // monitor back to 'preconditions-unmet'; the state-exit cleanup then\n // drops the (already-fired) listener.\n entry: () => {\n const mediaElement = context.mediaElement.get()!;\n const position = state.startPosition.get()!;\n\n // Step 1 — point the loaders' first load window at the position.\n state.currentTime.set(position);\n\n // Step 2 — seek once the element can honor it, then consume.\n const apply = () => {\n mediaElement.currentTime = position;\n state.startPosition.set(undefined);\n };\n\n if (mediaElement.readyState >= HTMLMediaElement.HAVE_METADATA) {\n apply();\n return;\n }\n\n return listen(mediaElement, 'loadedmetadata', apply, { once: true });\n },\n },\n },\n });\n}\n\nexport const applyStartPosition = defineBehavior({\n stateKeys: ['presentation', 'startPosition', 'currentTime'],\n contextKeys: ['mediaElement'],\n setup: applyStartPositionSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,SAAS,YACP,cACA,cACA,eACuB;CACvB,IAAI,CAAC,gBAAgB,CAAC,uBAAuB,YAAY,GAAG,OAAO;CAEnE,IAAI,YAAY,aAAa,GAAG,OAAO;CAEvC,OAAO;AACT;AAEA,SAAS,wBAAwB,EAC/B,OACA,WAU8D;CAC9D,MAAM,qBAAqB,eACzB,YAAY,MAAM,aAAa,IAAI,GAAG,QAAQ,aAAa,IAAI,GAAG,MAAM,cAAc,IAAI,CAAC,CAC7F;CAEA,OAAO,qBAA4C;EACjD,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GAExB,oBAAoB,EAIlB,aAAa;IACX,MAAM,eAAe,QAAQ,aAAa,IAAI;IAC9C,MAAM,WAAW,MAAM,cAAc,IAAI;IAGzC,MAAM,YAAY,IAAI,QAAQ;IAG9B,MAAM,cAAc;KAClB,aAAa,cAAc;KAC3B,MAAM,cAAc,IAAI,KAAA,CAAS;IACnC;IAEA,IAAI,aAAa,cAAc,iBAAiB,eAAe;KAC7D,MAAM;KACN;IACF;IAEA,OAAO,OAAO,cAAc,kBAAkB,OAAO,EAAE,MAAM,KAAK,CAAC;GACrE,EACF;EACF;CACF,CAAC;AACH;AAEA,MAAa,qBAAqB,eAAe;CAC/C,WAAW;EAAC;EAAgB;EAAiB;CAAa;CAC1D,aAAa,CAAC,cAAc;CAC5B,OAAO;AACT,CAAC"}
|
|
@@ -8,14 +8,12 @@ import { getMaxBufferedEnd, waitForSourceBuffersReady } from "../../../media/dom
|
|
|
8
8
|
import { isLastSegmentAppended } from "../../../media/dom/mse/end-of-stream.js";
|
|
9
9
|
//#region src/playback/behaviors/dom/end-of-stream.ts
|
|
10
10
|
/**
|
|
11
|
-
* Slack (seconds) on the "playhead has reached the last segment" gate. A tiny final
|
|
12
|
-
* segment
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* segment still finalize. Firing slightly early is harmless: the last segment is already
|
|
18
|
-
* appended (the gate above), so no more data is expected.
|
|
11
|
+
* Slack (seconds) on the "playhead has reached the last segment" gate. A tiny final segment (e.g. Apple's ~44ms last
|
|
12
|
+
* segment) starts right at the buffered end, and the browser freezes the playhead ~50–70ms short of that end (its
|
|
13
|
+
* render horizon), so a strict `currentTime >= lastSegStart` would never open — deadlocking `endOfStream` (the
|
|
14
|
+
* MediaSource stays `'open'`, so the browser keeps the playhead frozen waiting for data/EOS that never comes). This
|
|
15
|
+
* slack lets a playhead stalled just short of the final segment still finalize. Firing slightly early is harmless: the
|
|
16
|
+
* last segment is already appended (the gate above), so no more data is expected.
|
|
19
17
|
*/
|
|
20
18
|
const LAST_SEGMENT_REACHED_SLACK = .5;
|
|
21
19
|
function deriveState(presentation, mediaSource, msIsOpen, videoBufferActor, audioBufferActor, currentTime) {
|
|
@@ -82,14 +80,11 @@ function endOfStreamSetup({ state, context }) {
|
|
|
82
80
|
};
|
|
83
81
|
}
|
|
84
82
|
/**
|
|
85
|
-
* `endOfStream` uses a manual `Behavior<>` literal (rather than
|
|
86
|
-
* `
|
|
87
|
-
* `
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* opts out of the exhaustiveness check so the typed context shape can
|
|
91
|
-
* include the optional fields used at runtime. See the comment on
|
|
92
|
-
* `endOfStreamSetup`'s context param for the discipline.
|
|
83
|
+
* `endOfStream` uses a manual `Behavior<>` literal (rather than `defineBehavior`) because it reads `videoBufferActor` /
|
|
84
|
+
* `audioBufferActor` defensively without declaring them in its contextKeys — those slots are contributed by other
|
|
85
|
+
* behaviors and compose conditionally per engine variant. The `Behavior<>` literal opts out of the exhaustiveness check
|
|
86
|
+
* so the typed context shape can include the optional fields used at runtime. See the comment on `endOfStreamSetup`'s
|
|
87
|
+
* context param for the discipline.
|
|
93
88
|
*/
|
|
94
89
|
const endOfStream = {
|
|
95
90
|
stateKeys: ["presentation", "currentTime"],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"end-of-stream.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/end-of-stream.ts"],"sourcesContent":["/**\n * **Drive each `open → ended` transition of the MediaSource.** Calls\n * `MediaSource.endOfStream()` once every active buffer actor's\n * currently-loading track is *complete* (finite `Track.duration` — the parser's\n * completeness signal), its temporally last segments are fully appended, and\n * the user has reached them — letting the browser finalize duration and\n * fire `ended` on the media element. The completeness gate keeps it inert for\n * ongoing live (`Track.duration === Infinity`), whose \"last segment\" is only\n * the rolling edge; a live stream opts in when its duration turns finite.\n *\n * Re-fires on every subsequent `open → ended → open` cycle. Per the MSE\n * spec, `appendBuffer()` after `endOfStream()` transitions the MediaSource\n * back to `'open'`; seek-back replays and back-buffer refills that re-load\n * earlier segments take this path, so the behavior must call\n * `endOfStream()` again once the last segments are reappended. The reactor's\n * `'preconditions-unmet'` ↔ `'eos-ready'` cycle *is* the re-arm mechanism:\n * a successful `endOfStream()` flips `mediaSource.readyState` to `'ended'`,\n * which (via the local `msIsOpen` signal) exits `'eos-ready'`; the next\n * `'open'` re-evaluates preconditions and may re-enter.\n *\n * # Tracking what's in the buffer\n *\n * Each `SourceBufferActor` knows which track it's currently loading via\n * `initTrackId` (set on the most recent `append-init` message) and which\n * segments it has appended. `deriveState` iterates over the available\n * buffer actors (`[videoBufferActor, audioBufferActor].filter(Boolean)`),\n * resolves each to its track via `findTrackById`, and checks that track's\n * last segment is appended. This means audio-only / video-only / mixed\n * configurations compose uniformly — the body iterates whatever's in\n * scope. No reliance on `selectedTrackId` slots: the actor's view IS the\n * source of truth for \"what's being loaded into this buffer.\"\n *\n * **Two-fire on mid-end ABR switches**: when a quality switch occurs near\n * end-of-stream, the actor's `initTrackId` still reflects the *old* track\n * until the new init segment is appended. The reactor may fire\n * `endOfStream()` against the old track's last-segment-appended state,\n * then the new init's `appendBuffer()` re-opens the MS, which re-arms us\n * to fire again once the new track's last segment lands. Functionally\n * correct (the browser re-fires `ended` after the re-arm), at the cost of\n * one extra call. Accepted as the price of dropping `selectedTrackId`\n * dependence.\n *\n * # Buffer-actor idle gate\n *\n * Each actor must be `'idle'` (no queued or in-flight tasks) before we\n * fire. `buffer.updating === false` alone is insufficient: for chunked\n * fMP4 streaming, `updateend` fires after each chunk while the actor's\n * for-await loop synchronously enqueues the next `appendBuffer()` — so\n * the buffer flips `!updating → updating` across a microtask boundary.\n * The actor's `'updating'` state spans the entire multi-chunk append, so\n * `actor.snapshot.value === 'idle'` is the canonical \"no more pending or\n * in-flight work\" oracle.\n *\n * This bet (actor models the pipeline; `mediaSource.sourceBuffers` models\n * DOM state) is durable as long as the segment loader is the sole writer\n * of `appendBuffer()` calls. A future loop-mode that auto-fetches earlier\n * segments mid-`ended` would require a broader coordination story\n * between SourceBuffers and the MediaSource — possibly via a\n * `MediaSourceActor` — and that's where this gate would want\n * re-evaluating.\n *\n * # currentTime gate\n *\n * `currentTime` must have reached (within {@link LAST_SEGMENT_REACHED_SLACK})\n * at least one active track's last segment startTime. Prevents `'eos-ready'`\n * entry when a back-buffer `remove()` / `appendBuffer()` briefly re-opens the\n * MediaSource while the user is mid-stream. HLS rendition time-alignment means\n * any active track works as the reference. The slack absorbs the near-end\n * playhead freeze (see the constant) so a tiny final segment doesn't deadlock.\n *\n * # MS readyState — local subscription\n *\n * The behavior subscribes to `mediaSource`'s readyState changes inside\n * its setup (via `onMediaSourceReadyStateChange`) and mirrors `'open'` to\n * a behavior-local `msIsOpen` signal that `deriveState` reads. No shared\n * `mediaSourceReadyState` slot dependency. Anticipates a future\n * `MediaSourceActor` whose snapshot would expose the same signal — the\n * consumer code wouldn't change.\n *\n * # `endOfStream` entry sequence\n *\n * 1. Wait for all SourceBuffers to be idle (DOM-level — defensive,\n * should already hold given the actor-idle gate in `deriveState`).\n * 2. Set `mediaSource.duration` from `getMaxBufferedEnd` to match actual\n * container timestamps (`endOfStream()` only clamps up implicitly;\n * setting it explicitly here keeps the final value deterministic).\n * 3. Call `mediaSource.endOfStream()`.\n *\n * State-exit cleanup (`controller.abort()`) cancels any in-flight wait\n * on source unload, presentation replace, or behavior destroy.\n *\n * # Coordination with `updateMediaSourceDuration`\n *\n * `updateMediaSourceDuration` writes the initial `mediaSource.duration`\n * from `presentation.duration` once per source; this behavior writes the\n * final value from the buffered end. Decision domains don't overlap.\n */\nimport type { Behavior } from '../../../core/composition/create-composition';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { effect } from '../../../core/signals/effect';\nimport { computed, type ReadonlySignal, signal } from '../../../core/signals/primitives';\nimport { getMaxBufferedEnd, waitForSourceBuffersReady } from '../../../media/dom/mse/duration';\nimport { isLastSegmentAppended } from '../../../media/dom/mse/end-of-stream';\nimport { onMediaSourceReadyStateChange } from '../../../media/dom/mse/mediasource-setup';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\nimport { isResolvedTrack } from '../../../media/types';\nimport { findTrackById } from '../../../media/utils/tracks';\nimport type { SourceBufferActor } from '../../actors/dom/source-buffer';\n\nexport interface EndOfStreamState {\n presentation?: MaybeResolvedPresentation;\n currentTime?: number;\n}\n\nexport interface EndOfStreamContext {\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\ntype EndOfStreamFsmState = 'preconditions-unmet' | 'eos-ready';\n\n/**\n * Slack (seconds) on the \"playhead has reached the last segment\" gate. A tiny final\n * segment (e.g. Apple's ~44ms last segment) starts right at the buffered end, and the\n * browser freezes the playhead ~50–70ms short of that end (its render horizon), so a\n * strict `currentTime >= lastSegStart` would never open — deadlocking `endOfStream`\n * (the MediaSource stays `'open'`, so the browser keeps the playhead frozen waiting for\n * data/EOS that never comes). This slack lets a playhead stalled just short of the final\n * segment still finalize. Firing slightly early is harmless: the last segment is already\n * appended (the gate above), so no more data is expected.\n */\nconst LAST_SEGMENT_REACHED_SLACK = 0.5;\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaSource: MediaSource | undefined,\n msIsOpen: boolean,\n videoBufferActor: SourceBufferActor | undefined,\n audioBufferActor: SourceBufferActor | undefined,\n currentTime: number | undefined\n): EndOfStreamFsmState {\n if (!mediaSource || !presentation || !msIsOpen) return 'preconditions-unmet';\n\n const actors = [videoBufferActor, audioBufferActor].filter((a): a is SourceBufferActor => a !== undefined);\n // No active buffer actors means setup hasn't completed wiring yet.\n if (actors.length === 0) return 'preconditions-unmet';\n\n // Track the latest last-segment startTime across actors as the\n // currentTime reference. HLS rendition time-alignment makes any active\n // track usable; using the max is safest when timings drift slightly.\n let lastSegStart: number | undefined;\n\n for (const actor of actors) {\n const snapshot = actor.snapshot.get();\n if (snapshot.value !== 'idle') return 'preconditions-unmet';\n\n const { initTrackId, segments: appended } = snapshot.context;\n if (!initTrackId) return 'preconditions-unmet';\n\n const track = findTrackById(presentation, initTrackId);\n if (!track || !isResolvedTrack(track)) return 'preconditions-unmet';\n // Only a complete playlist has a true last segment: firing on an ongoing\n // live window would pin a finite (live-edge) duration and end the stream,\n // only for the next reload's appends to reopen it and re-fire on a loop.\n // Independent of the currentTime slack below — this asks \"is there an end at\n // all,\" the slack asks \"has the playhead effectively reached it.\"\n if (!Number.isFinite(track.duration)) return 'preconditions-unmet';\n if (!isLastSegmentAppended(track.segments, appended)) return 'preconditions-unmet';\n\n if (track.segments.length > 0) {\n const start = track.segments[track.segments.length - 1]!.startTime;\n if (lastSegStart === undefined || start > lastSegStart) lastSegStart = start;\n }\n }\n\n // Slack absorbs the near-end playhead freeze so a tiny final segment doesn't deadlock.\n if (lastSegStart !== undefined && (currentTime ?? 0) < lastSegStart - LAST_SEGMENT_REACHED_SLACK) {\n return 'preconditions-unmet';\n }\n\n return 'eos-ready';\n}\n\nfunction endOfStreamSetup({\n state,\n context,\n}: {\n state: {\n presentation: ReadonlySignal<EndOfStreamState['presentation']>;\n currentTime: ReadonlySignal<EndOfStreamState['currentTime']>;\n };\n context: {\n mediaSource: ReadonlySignal<EndOfStreamContext['mediaSource']>;\n // See behavior definition for details on these optional context signals.\n videoBufferActor?: ReadonlySignal<SourceBufferActor | undefined>;\n audioBufferActor?: ReadonlySignal<SourceBufferActor | undefined>;\n };\n}): () => void {\n // Behavior-local mirror of `mediaSource.readyState === 'open'`. Subscribes\n // to MS events whenever a MediaSource is in scope; tears down when the\n // MS is swapped or the behavior destroys. Anticipates a future\n // `MediaSourceActor` whose snapshot would expose the same value — the\n // consumer code (deriveState) wouldn't change.\n const msIsOpen = signal(false);\n const cleanupMsListener = effect(() => {\n const mediaSource = context.mediaSource.get();\n if (!mediaSource) {\n msIsOpen.set(false);\n return;\n }\n msIsOpen.set(mediaSource.readyState === 'open');\n const controller = new AbortController();\n onMediaSourceReadyStateChange(mediaSource, controller.signal, (rs) => {\n msIsOpen.set(rs === 'open');\n });\n return () => controller.abort();\n });\n\n const derivedStateSignal = computed(() =>\n deriveState(\n state.presentation.get(),\n context.mediaSource.get(),\n msIsOpen.get(),\n context.videoBufferActor?.get(),\n context.audioBufferActor?.get(),\n state.currentTime.get()\n )\n );\n\n const reactor = createMachineReactor<EndOfStreamFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'eos-ready': {\n // entry body is auto-untracked. deriveState handles source resets\n // and the MS readyState flip from 'open' to 'ended' that\n // endOfStream() produces; this entry resolves the MSE-spec\n // preconditions and binds its cleanup (abort) to state exit +\n // destroy.\n entry: () => {\n const mediaSource = context.mediaSource.get()!;\n const controller = new AbortController();\n\n const endStreamWhenReady = async () => {\n // Defensive: the actor-idle gate in deriveState should mean\n // buffers are already settled, but a microtask boundary\n // between deriveState's read and entry firing leaves a thin\n // gap. The DOM-level wait closes it.\n await waitForSourceBuffersReady(mediaSource.sourceBuffers, controller.signal);\n if (controller.signal.aborted) return;\n\n // MSE spec: duration cannot be less than any buffered range, and\n // endOfStream() will only clamp up implicitly. Setting it\n // explicitly here from actual container timestamps keeps the\n // final value deterministic for assets whose declared duration\n // disagrees with the buffered end (common with CMAF).\n const bufferedEnd = getMaxBufferedEnd(mediaSource.sourceBuffers);\n if (bufferedEnd > 0) mediaSource.duration = bufferedEnd;\n\n mediaSource.endOfStream();\n };\n\n endStreamWhenReady().catch((err) => console.error('Failed to call endOfStream:', err));\n\n return () => controller.abort();\n },\n },\n },\n });\n\n return () => {\n cleanupMsListener();\n reactor.destroy();\n };\n}\n\n/**\n * `endOfStream` uses a manual `Behavior<>` literal (rather than\n * `defineBehavior`) because it reads `videoBufferActor` /\n * `audioBufferActor` defensively without declaring them in its\n * contextKeys — those slots are contributed by other behaviors and\n * compose conditionally per engine variant. The `Behavior<>` literal\n * opts out of the exhaustiveness check so the typed context shape can\n * include the optional fields used at runtime. See the comment on\n * `endOfStreamSetup`'s context param for the discipline.\n */\nexport const endOfStream: Behavior<\n {\n presentation: ReadonlySignal<EndOfStreamState['presentation']>;\n currentTime: ReadonlySignal<EndOfStreamState['currentTime']>;\n },\n { mediaSource: ReadonlySignal<EndOfStreamContext['mediaSource']> }\n> = {\n stateKeys: ['presentation', 'currentTime'],\n contextKeys: ['mediaSource'],\n setup: endOfStreamSetup,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAoIA,MAAM,6BAA6B;AAEnC,SAAS,YACP,cACA,aACA,UACA,kBACA,kBACA,aACqB;CACrB,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,UAAU,OAAO;CAEvD,MAAM,SAAS,CAAC,kBAAkB,gBAAgB,CAAC,CAAC,QAAQ,MAA8B,MAAM,KAAA,CAAS;CAEzG,IAAI,OAAO,WAAW,GAAG,OAAO;CAKhC,IAAI;CAEJ,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,MAAM,SAAS,IAAI;EACpC,IAAI,SAAS,UAAU,QAAQ,OAAO;EAEtC,MAAM,EAAE,aAAa,UAAU,aAAa,SAAS;EACrD,IAAI,CAAC,aAAa,OAAO;EAEzB,MAAM,QAAQ,cAAc,cAAc,WAAW;EACrD,IAAI,CAAC,SAAS,CAAC,gBAAgB,KAAK,GAAG,OAAO;EAM9C,IAAI,CAAC,OAAO,SAAS,MAAM,QAAQ,GAAG,OAAO;EAC7C,IAAI,CAAC,sBAAsB,MAAM,UAAU,QAAQ,GAAG,OAAO;EAE7D,IAAI,MAAM,SAAS,SAAS,GAAG;GAC7B,MAAM,QAAQ,MAAM,SAAS,MAAM,SAAS,SAAS,EAAE,CAAE;GACzD,IAAI,iBAAiB,KAAA,KAAa,QAAQ,cAAc,eAAe;EACzE;CACF;CAGA,IAAI,iBAAiB,KAAA,MAAc,eAAe,KAAK,eAAe,4BACpE,OAAO;CAGT,OAAO;AACT;AAEA,SAAS,iBAAiB,EACxB,OACA,WAYa;CAMb,MAAM,WAAW,OAAO,KAAK;CAC7B,MAAM,oBAAoB,aAAa;EACrC,MAAM,cAAc,QAAQ,YAAY,IAAI;EAC5C,IAAI,CAAC,aAAa;GAChB,SAAS,IAAI,KAAK;GAClB;EACF;EACA,SAAS,IAAI,YAAY,eAAe,MAAM;EAC9C,MAAM,aAAa,IAAI,gBAAgB;EACvC,8BAA8B,aAAa,WAAW,SAAS,OAAO;GACpE,SAAS,IAAI,OAAO,MAAM;EAC5B,CAAC;EACD,aAAa,WAAW,MAAM;CAChC,CAAC;CAED,MAAM,qBAAqB,eACzB,YACE,MAAM,aAAa,IAAI,GACvB,QAAQ,YAAY,IAAI,GACxB,SAAS,IAAI,GACb,QAAQ,kBAAkB,IAAI,GAC9B,QAAQ,kBAAkB,IAAI,GAC9B,MAAM,YAAY,IAAI,CACxB,CACF;CAEA,MAAM,UAAU,qBAA0C;EACxD,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GAExB,aAAa,EAMX,aAAa;IACX,MAAM,cAAc,QAAQ,YAAY,IAAI;IAC5C,MAAM,aAAa,IAAI,gBAAgB;IAEvC,MAAM,qBAAqB,YAAY;KAKrC,MAAM,0BAA0B,YAAY,eAAe,WAAW,MAAM;KAC5E,IAAI,WAAW,OAAO,SAAS;KAO/B,MAAM,cAAc,kBAAkB,YAAY,aAAa;KAC/D,IAAI,cAAc,GAAG,YAAY,WAAW;KAE5C,YAAY,YAAY;IAC1B;IAEA,mBAAmB,CAAC,CAAC,OAAO,QAAQ,QAAQ,MAAM,+BAA+B,GAAG,CAAC;IAErF,aAAa,WAAW,MAAM;GAChC,EACF;EACF;CACF,CAAC;CAED,aAAa;EACX,kBAAkB;EAClB,QAAQ,QAAQ;CAClB;AACF;;;;;;;;;;;AAYA,MAAa,cAMT;CACF,WAAW,CAAC,gBAAgB,aAAa;CACzC,aAAa,CAAC,aAAa;CAC3B,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"end-of-stream.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/end-of-stream.ts"],"sourcesContent":["/**\n * **Drive each `open → ended` transition of the MediaSource.** Calls `MediaSource.endOfStream()` once every active\n * buffer actor's currently-loading track is _complete_ (finite `Track.duration` — the parser's completeness signal),\n * its temporally last segments are fully appended, and the user has reached them — letting the browser finalize\n * duration and fire `ended` on the media element. The completeness gate keeps it inert for ongoing live\n * (`Track.duration === Infinity`), whose \"last segment\" is only the rolling edge; a live stream opts in when its\n * duration turns finite.\n *\n * Re-fires on every subsequent `open → ended → open` cycle. Per the MSE spec, `appendBuffer()` after `endOfStream()`\n * transitions the MediaSource back to `'open'`; seek-back replays and back-buffer refills that re-load earlier segments\n * take this path, so the behavior must call `endOfStream()` again once the last segments are reappended. The reactor's\n * `'preconditions-unmet'` ↔ `'eos-ready'` cycle _is_ the re-arm mechanism: a successful `endOfStream()` flips\n * `mediaSource.readyState` to `'ended'`, which (via the local `msIsOpen` signal) exits `'eos-ready'`; the next `'open'`\n * re-evaluates preconditions and may re-enter.\n *\n * # Tracking what's in the buffer\n *\n * Each `SourceBufferActor` knows which track it's currently loading via `initTrackId` (set on the most recent\n * `append-init` message) and which segments it has appended. `deriveState` iterates over the available buffer actors\n * (`[videoBufferActor, audioBufferActor].filter(Boolean)`), resolves each to its track via `findTrackById`, and checks\n * that track's last segment is appended. This means audio-only / video-only / mixed configurations compose uniformly —\n * the body iterates whatever's in scope. No reliance on `selectedTrackId` slots: the actor's view IS the source of\n * truth for \"what's being loaded into this buffer.\"\n *\n * **Two-fire on mid-end ABR switches**: when a quality switch occurs near end-of-stream, the actor's `initTrackId`\n * still reflects the _old_ track until the new init segment is appended. The reactor may fire `endOfStream()` against\n * the old track's last-segment-appended state, then the new init's `appendBuffer()` re-opens the MS, which re-arms us\n * to fire again once the new track's last segment lands. Functionally correct (the browser re-fires `ended` after the\n * re-arm), at the cost of one extra call. Accepted as the price of dropping `selectedTrackId` dependence.\n *\n * # Buffer-actor idle gate\n *\n * Each actor must be `'idle'` (no queued or in-flight tasks) before we fire. `buffer.updating === false` alone is\n * insufficient: for chunked fMP4 streaming, `updateend` fires after each chunk while the actor's for-await loop\n * synchronously enqueues the next `appendBuffer()` — so the buffer flips `!updating → updating` across a microtask\n * boundary. The actor's `'updating'` state spans the entire multi-chunk append, so `actor.snapshot.value === 'idle'` is\n * the canonical \"no more pending or in-flight work\" oracle.\n *\n * This bet (actor models the pipeline; `mediaSource.sourceBuffers` models DOM state) is durable as long as the segment\n * loader is the sole writer of `appendBuffer()` calls. A future loop-mode that auto-fetches earlier segments\n * mid-`ended` would require a broader coordination story between SourceBuffers and the MediaSource — possibly via a\n * `MediaSourceActor` — and that's where this gate would want re-evaluating.\n *\n * # currentTime gate\n *\n * `currentTime` must have reached (within {@link LAST_SEGMENT_REACHED_SLACK}) at least one active track's last segment\n * startTime. Prevents `'eos-ready'` entry when a back-buffer `remove()` / `appendBuffer()` briefly re-opens the\n * MediaSource while the user is mid-stream. HLS rendition time-alignment means any active track works as the reference.\n * The slack absorbs the near-end playhead freeze (see the constant) so a tiny final segment doesn't deadlock.\n *\n * # MS readyState — local subscription\n *\n * The behavior subscribes to `mediaSource`'s readyState changes inside its setup (via `onMediaSourceReadyStateChange`)\n * and mirrors `'open'` to a behavior-local `msIsOpen` signal that `deriveState` reads. No shared\n * `mediaSourceReadyState` slot dependency. Anticipates a future `MediaSourceActor` whose snapshot would expose the same\n * signal — the consumer code wouldn't change.\n *\n * # `endOfStream` entry sequence\n *\n * 1. Wait for all SourceBuffers to be idle (DOM-level — defensive, should already hold given the actor-idle gate in\n * `deriveState`).\n * 2. Set `mediaSource.duration` from `getMaxBufferedEnd` to match actual container timestamps (`endOfStream()` only clamps\n * up implicitly; setting it explicitly here keeps the final value deterministic).\n * 3. Call `mediaSource.endOfStream()`.\n *\n * State-exit cleanup (`controller.abort()`) cancels any in-flight wait on source unload, presentation replace, or\n * behavior destroy.\n *\n * # Coordination with `updateMediaSourceDuration`\n *\n * `updateMediaSourceDuration` writes the initial `mediaSource.duration` from `presentation.duration` once per source;\n * this behavior writes the final value from the buffered end. Decision domains don't overlap.\n */\nimport type { Behavior } from '../../../core/composition/create-composition';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { effect } from '../../../core/signals/effect';\nimport { computed, type ReadonlySignal, signal } from '../../../core/signals/primitives';\nimport { getMaxBufferedEnd, waitForSourceBuffersReady } from '../../../media/dom/mse/duration';\nimport { isLastSegmentAppended } from '../../../media/dom/mse/end-of-stream';\nimport { onMediaSourceReadyStateChange } from '../../../media/dom/mse/mediasource-setup';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\nimport { isResolvedTrack } from '../../../media/types';\nimport { findTrackById } from '../../../media/utils/tracks';\nimport type { SourceBufferActor } from '../../actors/dom/source-buffer';\n\nexport interface EndOfStreamState {\n presentation?: MaybeResolvedPresentation;\n currentTime?: number;\n}\n\nexport interface EndOfStreamContext {\n mediaSource?: MediaSource;\n videoBufferActor?: SourceBufferActor;\n audioBufferActor?: SourceBufferActor;\n}\n\ntype EndOfStreamFsmState = 'preconditions-unmet' | 'eos-ready';\n\n/**\n * Slack (seconds) on the \"playhead has reached the last segment\" gate. A tiny final segment (e.g. Apple's ~44ms last\n * segment) starts right at the buffered end, and the browser freezes the playhead ~50–70ms short of that end (its\n * render horizon), so a strict `currentTime >= lastSegStart` would never open — deadlocking `endOfStream` (the\n * MediaSource stays `'open'`, so the browser keeps the playhead frozen waiting for data/EOS that never comes). This\n * slack lets a playhead stalled just short of the final segment still finalize. Firing slightly early is harmless: the\n * last segment is already appended (the gate above), so no more data is expected.\n */\nconst LAST_SEGMENT_REACHED_SLACK = 0.5;\n\nfunction deriveState(\n presentation: MaybeResolvedPresentation | undefined,\n mediaSource: MediaSource | undefined,\n msIsOpen: boolean,\n videoBufferActor: SourceBufferActor | undefined,\n audioBufferActor: SourceBufferActor | undefined,\n currentTime: number | undefined\n): EndOfStreamFsmState {\n if (!mediaSource || !presentation || !msIsOpen) return 'preconditions-unmet';\n\n const actors = [videoBufferActor, audioBufferActor].filter((a): a is SourceBufferActor => a !== undefined);\n // No active buffer actors means setup hasn't completed wiring yet.\n if (actors.length === 0) return 'preconditions-unmet';\n\n // Track the latest last-segment startTime across actors as the\n // currentTime reference. HLS rendition time-alignment makes any active\n // track usable; using the max is safest when timings drift slightly.\n let lastSegStart: number | undefined;\n\n for (const actor of actors) {\n const snapshot = actor.snapshot.get();\n if (snapshot.value !== 'idle') return 'preconditions-unmet';\n\n const { initTrackId, segments: appended } = snapshot.context;\n if (!initTrackId) return 'preconditions-unmet';\n\n const track = findTrackById(presentation, initTrackId);\n if (!track || !isResolvedTrack(track)) return 'preconditions-unmet';\n\n // Only a complete playlist has a true last segment: firing on an ongoing\n // live window would pin a finite (live-edge) duration and end the stream,\n // only for the next reload's appends to reopen it and re-fire on a loop.\n // Independent of the currentTime slack below — this asks \"is there an end at\n // all,\" the slack asks \"has the playhead effectively reached it.\"\n if (!Number.isFinite(track.duration)) return 'preconditions-unmet';\n\n if (!isLastSegmentAppended(track.segments, appended)) return 'preconditions-unmet';\n\n if (track.segments.length > 0) {\n const start = track.segments[track.segments.length - 1]!.startTime;\n\n if (lastSegStart === undefined || start > lastSegStart) lastSegStart = start;\n }\n }\n\n // Slack absorbs the near-end playhead freeze so a tiny final segment doesn't deadlock.\n if (lastSegStart !== undefined && (currentTime ?? 0) < lastSegStart - LAST_SEGMENT_REACHED_SLACK) {\n return 'preconditions-unmet';\n }\n\n return 'eos-ready';\n}\n\nfunction endOfStreamSetup({\n state,\n context,\n}: {\n state: {\n presentation: ReadonlySignal<EndOfStreamState['presentation']>;\n currentTime: ReadonlySignal<EndOfStreamState['currentTime']>;\n };\n context: {\n mediaSource: ReadonlySignal<EndOfStreamContext['mediaSource']>;\n // See behavior definition for details on these optional context signals.\n videoBufferActor?: ReadonlySignal<SourceBufferActor | undefined>;\n audioBufferActor?: ReadonlySignal<SourceBufferActor | undefined>;\n };\n}): () => void {\n // Behavior-local mirror of `mediaSource.readyState === 'open'`. Subscribes\n // to MS events whenever a MediaSource is in scope; tears down when the\n // MS is swapped or the behavior destroys. Anticipates a future\n // `MediaSourceActor` whose snapshot would expose the same value — the\n // consumer code (deriveState) wouldn't change.\n const msIsOpen = signal(false);\n const cleanupMsListener = effect(() => {\n const mediaSource = context.mediaSource.get();\n\n if (!mediaSource) {\n msIsOpen.set(false);\n return;\n }\n\n msIsOpen.set(mediaSource.readyState === 'open');\n const controller = new AbortController();\n\n onMediaSourceReadyStateChange(mediaSource, controller.signal, (rs) => {\n msIsOpen.set(rs === 'open');\n });\n return () => controller.abort();\n });\n\n const derivedStateSignal = computed(() =>\n deriveState(\n state.presentation.get(),\n context.mediaSource.get(),\n msIsOpen.get(),\n context.videoBufferActor?.get(),\n context.audioBufferActor?.get(),\n state.currentTime.get()\n )\n );\n\n const reactor = createMachineReactor<EndOfStreamFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'eos-ready': {\n // entry body is auto-untracked. deriveState handles source resets\n // and the MS readyState flip from 'open' to 'ended' that\n // endOfStream() produces; this entry resolves the MSE-spec\n // preconditions and binds its cleanup (abort) to state exit +\n // destroy.\n entry: () => {\n const mediaSource = context.mediaSource.get()!;\n const controller = new AbortController();\n\n const endStreamWhenReady = async () => {\n // Defensive: the actor-idle gate in deriveState should mean\n // buffers are already settled, but a microtask boundary\n // between deriveState's read and entry firing leaves a thin\n // gap. The DOM-level wait closes it.\n await waitForSourceBuffersReady(mediaSource.sourceBuffers, controller.signal);\n\n if (controller.signal.aborted) return;\n\n // MSE spec: duration cannot be less than any buffered range, and\n // endOfStream() will only clamp up implicitly. Setting it\n // explicitly here from actual container timestamps keeps the\n // final value deterministic for assets whose declared duration\n // disagrees with the buffered end (common with CMAF).\n const bufferedEnd = getMaxBufferedEnd(mediaSource.sourceBuffers);\n\n if (bufferedEnd > 0) mediaSource.duration = bufferedEnd;\n\n mediaSource.endOfStream();\n };\n\n endStreamWhenReady().catch((err) => console.error('Failed to call endOfStream:', err));\n\n return () => controller.abort();\n },\n },\n },\n });\n\n return () => {\n cleanupMsListener();\n reactor.destroy();\n };\n}\n\n/**\n * `endOfStream` uses a manual `Behavior<>` literal (rather than `defineBehavior`) because it reads `videoBufferActor` /\n * `audioBufferActor` defensively without declaring them in its contextKeys — those slots are contributed by other\n * behaviors and compose conditionally per engine variant. The `Behavior<>` literal opts out of the exhaustiveness check\n * so the typed context shape can include the optional fields used at runtime. See the comment on `endOfStreamSetup`'s\n * context param for the discipline.\n */\nexport const endOfStream: Behavior<\n {\n presentation: ReadonlySignal<EndOfStreamState['presentation']>;\n currentTime: ReadonlySignal<EndOfStreamState['currentTime']>;\n },\n { mediaSource: ReadonlySignal<EndOfStreamContext['mediaSource']> }\n> = {\n stateKeys: ['presentation', 'currentTime'],\n contextKeys: ['mediaSource'],\n setup: endOfStreamSetup,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AA0GA,MAAM,6BAA6B;AAEnC,SAAS,YACP,cACA,aACA,UACA,kBACA,kBACA,aACqB;CACrB,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,UAAU,OAAO;CAEvD,MAAM,SAAS,CAAC,kBAAkB,gBAAgB,CAAC,CAAC,QAAQ,MAA8B,MAAM,KAAA,CAAS;CAEzG,IAAI,OAAO,WAAW,GAAG,OAAO;CAKhC,IAAI;CAEJ,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,MAAM,SAAS,IAAI;EACpC,IAAI,SAAS,UAAU,QAAQ,OAAO;EAEtC,MAAM,EAAE,aAAa,UAAU,aAAa,SAAS;EACrD,IAAI,CAAC,aAAa,OAAO;EAEzB,MAAM,QAAQ,cAAc,cAAc,WAAW;EACrD,IAAI,CAAC,SAAS,CAAC,gBAAgB,KAAK,GAAG,OAAO;EAO9C,IAAI,CAAC,OAAO,SAAS,MAAM,QAAQ,GAAG,OAAO;EAE7C,IAAI,CAAC,sBAAsB,MAAM,UAAU,QAAQ,GAAG,OAAO;EAE7D,IAAI,MAAM,SAAS,SAAS,GAAG;GAC7B,MAAM,QAAQ,MAAM,SAAS,MAAM,SAAS,SAAS,EAAE,CAAE;GAEzD,IAAI,iBAAiB,KAAA,KAAa,QAAQ,cAAc,eAAe;EACzE;CACF;CAGA,IAAI,iBAAiB,KAAA,MAAc,eAAe,KAAK,eAAe,4BACpE,OAAO;CAGT,OAAO;AACT;AAEA,SAAS,iBAAiB,EACxB,OACA,WAYa;CAMb,MAAM,WAAW,OAAO,KAAK;CAC7B,MAAM,oBAAoB,aAAa;EACrC,MAAM,cAAc,QAAQ,YAAY,IAAI;EAE5C,IAAI,CAAC,aAAa;GAChB,SAAS,IAAI,KAAK;GAClB;EACF;EAEA,SAAS,IAAI,YAAY,eAAe,MAAM;EAC9C,MAAM,aAAa,IAAI,gBAAgB;EAEvC,8BAA8B,aAAa,WAAW,SAAS,OAAO;GACpE,SAAS,IAAI,OAAO,MAAM;EAC5B,CAAC;EACD,aAAa,WAAW,MAAM;CAChC,CAAC;CAED,MAAM,qBAAqB,eACzB,YACE,MAAM,aAAa,IAAI,GACvB,QAAQ,YAAY,IAAI,GACxB,SAAS,IAAI,GACb,QAAQ,kBAAkB,IAAI,GAC9B,QAAQ,kBAAkB,IAAI,GAC9B,MAAM,YAAY,IAAI,CACxB,CACF;CAEA,MAAM,UAAU,qBAA0C;EACxD,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GAExB,aAAa,EAMX,aAAa;IACX,MAAM,cAAc,QAAQ,YAAY,IAAI;IAC5C,MAAM,aAAa,IAAI,gBAAgB;IAEvC,MAAM,qBAAqB,YAAY;KAKrC,MAAM,0BAA0B,YAAY,eAAe,WAAW,MAAM;KAE5E,IAAI,WAAW,OAAO,SAAS;KAO/B,MAAM,cAAc,kBAAkB,YAAY,aAAa;KAE/D,IAAI,cAAc,GAAG,YAAY,WAAW;KAE5C,YAAY,YAAY;IAC1B;IAEA,mBAAmB,CAAC,CAAC,OAAO,QAAQ,QAAQ,MAAM,+BAA+B,GAAG,CAAC;IAErF,aAAa,WAAW,MAAM;GAChC,EACF;EACF;CACF,CAAC;CAED,aAAa;EACX,kBAAkB;EAClB,QAAQ,QAAQ;CAClB;AACF;;;;;;;;AASA,MAAa,cAMT;CACF,WAAW,CAAC,gBAAgB,aAAa;CACzC,aAAa,CAAC,aAAa;CAC3B,OAAO;AACT"}
|
|
@@ -13,14 +13,11 @@ interface SegmentLoadingState {
|
|
|
13
13
|
/** True once a preload-overriding event has fired for the current source. */
|
|
14
14
|
loadActivated?: boolean;
|
|
15
15
|
/**
|
|
16
|
-
* Intent-level policy input: initiate no new loading work while `true`.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* exists only in compositions where a feature behavior declares and
|
|
22
|
-
* writes it (e.g. `setupAirPlay`, while a remote-playback session owns
|
|
23
|
-
* presentation). An absent slot means never suspended.
|
|
16
|
+
* Intent-level policy input: initiate no new loading work while `true`. Read here by the `loadXSegments` dispatchers
|
|
17
|
+
* (park in `'dormant'`, highest precedence) and by `setupMediaSource` (a pending MediaSource rebuild waits — attach
|
|
18
|
+
* runs the element's load algorithm). **Observed, never declared**: no reader lists this key in `stateKeys`, so the
|
|
19
|
+
* slot exists only in compositions where a feature behavior declares and writes it (e.g. `setupAirPlay`, while a
|
|
20
|
+
* remote-playback session owns presentation). An absent slot means never suspended.
|
|
24
21
|
*/
|
|
25
22
|
loadingSuspended?: boolean;
|
|
26
23
|
selectedVideoTrackId?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-segments.d.ts","names":[],"sources":["../../../../../src/playback/behaviors/dom/load-segments.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"load-segments.d.ts","names":[],"sources":["../../../../../src/playback/behaviors/dom/load-segments.ts"],"mappings":";;;;;;;UA8DiB;EACf,eAAe;EACf;;EAEA;;EAEA;;;;;;;;EAQA;EACA;EACA;EACA;;;UAIe;EACf,0BAA0B;EAC1B,0BAA0B;EAC1B,8BAA8B;;KAO3B;KAEA;KAGA,uBAAuB,UAAU;EACpC,cAAc,eAAe;EAC7B,SAAS,eAAe;EACxB,aAAa,eAAe;EAC5B,eAAe,eAAe;OACzB,KAAK,IAAI,eAAe,oBAAoB;cA4ItC;;;;;;;MAUkB,yBAAA,eAAe;;;;;;cAWjC;;;;;;;MAUkB,yBAAA,eAAe"}
|
|
@@ -6,63 +6,51 @@ import { findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack }
|
|
|
6
6
|
import { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "../../primitives/track-types.js";
|
|
7
7
|
//#region src/playback/behaviors/dom/load-segments.ts
|
|
8
8
|
/**
|
|
9
|
-
* **Per-type segment loading dispatch.** Per available track type (video /
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* changes (selected track, current time crossing a segment boundary).
|
|
9
|
+
* **Per-type segment loading dispatch.** Per available track type (video / audio / text), reads the per-type
|
|
10
|
+
* segment-loader actor from context and sends typed `'load'` messages whenever a meaningful loading condition changes
|
|
11
|
+
* (selected track, current time crossing a segment boundary).
|
|
13
12
|
*
|
|
14
13
|
* Loader-actor lifecycle is owned upstream:
|
|
14
|
+
*
|
|
15
15
|
* - Video: `setupVideoBufferActors`
|
|
16
16
|
* - Audio: `setupAudioBufferActors`
|
|
17
17
|
* - Text: `setupTextTrackActors`
|
|
18
18
|
*
|
|
19
|
-
* This behavior is pure-consumer: it reads `context[loaderKey]` and
|
|
20
|
-
*
|
|
21
|
-
* resolver.
|
|
19
|
+
* This behavior is pure-consumer: it reads `context[loaderKey]` and dispatches typed messages via the variant's
|
|
20
|
+
* per-type `findResolvedTrack` resolver.
|
|
22
21
|
*
|
|
23
22
|
* # Load modes as reactor states
|
|
24
23
|
*
|
|
25
24
|
* Four states encode the load-gating policy directly:
|
|
26
25
|
*
|
|
27
|
-
* - `'preconditions-unmet'` — no loader actor in context, or the selected
|
|
28
|
-
*
|
|
29
|
-
* -
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* init segment; text's actor no-ops (no init concept).
|
|
37
|
-
* - `'full-range'` — `loadActivated || preload === 'auto'`. Effect re-fires
|
|
38
|
-
* on selected-track change and on segment-boundary crossing.
|
|
26
|
+
* - `'preconditions-unmet'` — no loader actor in context, or the selected track hasn't resolved.
|
|
27
|
+
* - `'dormant'` — loading disabled by policy: an observed `loadingSuspended` (highest precedence) or `preload === 'none'
|
|
28
|
+
* && !loadActivated`. Nothing fires; already-queued loader work drains. Auto-resumes into the derived state when the
|
|
29
|
+
* policy lifts.
|
|
30
|
+
* - `'metadata-only'` — `!loadActivated && preload !== 'auto' && preload !== 'none'`. Fires an init-segment-only `load`
|
|
31
|
+
* message **once on entry**. The variant's loader actor decides what to do — v/a's actor fetches the init segment;
|
|
32
|
+
* text's actor no-ops (no init concept).
|
|
33
|
+
* - `'full-range'` — `loadActivated || preload === 'auto'`. Effect re-fires on selected-track change and on
|
|
34
|
+
* segment-boundary crossing.
|
|
39
35
|
*
|
|
40
36
|
* # Per-type parameterization (inference-driven)
|
|
41
37
|
*
|
|
42
|
-
* The helper is generic over `Track` — the resolved-track type. Each
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* actor types (`SegmentLoaderActor`, `TextTrackSegmentLoaderActor`)
|
|
48
|
-
* satisfy this via function-parameter contravariance: an actor whose
|
|
49
|
-
* `.send` accepts a wider track type is assignable to a slot expecting a
|
|
50
|
-
* narrower track type.
|
|
38
|
+
* The helper is generic over `Track` — the resolved-track type. Each variant supplies its own `findResolvedTrack`
|
|
39
|
+
* resolver via config; TS infers `Track` from the resolver's return type. The loader signal's value type is constrained
|
|
40
|
+
* to `SegmentLoaderLike<Track>` — anything with a `send` method accepting the `Track`-parameterized message. Concrete
|
|
41
|
+
* actor types (`SegmentLoaderActor`, `TextTrackSegmentLoaderActor`) satisfy this via function-parameter contravariance:
|
|
42
|
+
* an actor whose `.send` accepts a wider track type is assignable to a slot expecting a narrower track type.
|
|
51
43
|
*
|
|
52
|
-
* No widening of actor message types is needed; no casts inside the
|
|
53
|
-
*
|
|
54
|
-
* is enforced at the variant call site.
|
|
44
|
+
* No widening of actor message types is needed; no casts inside the helper. Per-variant wiring (right loader paired
|
|
45
|
+
* with right resolver) is enforced at the variant call site.
|
|
55
46
|
*/
|
|
56
47
|
/**
|
|
57
|
-
* Specialization helper. Generic over `Track` (inferred from
|
|
58
|
-
* `
|
|
59
|
-
*
|
|
60
|
-
* (whose `.send` accepts a wider track union) satisfy this via
|
|
61
|
-
* function-parameter contravariance.
|
|
48
|
+
* Specialization helper. Generic over `Track` (inferred from `findResolvedTrack`'s return type). The loader value type
|
|
49
|
+
* is constrained to `SegmentLoaderLike<Track>` — concrete actor types (whose `.send` accepts a wider track union)
|
|
50
|
+
* satisfy this via function-parameter contravariance.
|
|
62
51
|
*
|
|
63
|
-
* Tracks that the helper handles must have a `segments` field — used by
|
|
64
|
-
*
|
|
65
|
-
* variant's resolver narrows to the right resolved-track shape.
|
|
52
|
+
* Tracks that the helper handles must have a `segments` field — used by `segmentBoundarySignal` to compute the
|
|
53
|
+
* load-anchor boundary. Each variant's resolver narrows to the right resolved-track shape.
|
|
66
54
|
*/
|
|
67
55
|
function setupSegmentLoading({ state, context, config }) {
|
|
68
56
|
const { selectedKey, loaderKey, findResolvedTrack } = config;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-segments.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/load-segments.ts"],"sourcesContent":["/**\n * **Per-type segment loading dispatch.** Per available track type (video /\n * audio / text), reads the per-type segment-loader actor from context and\n * sends typed `'load'` messages whenever a meaningful loading condition\n * changes (selected track, current time crossing a segment boundary).\n *\n * Loader-actor lifecycle is owned upstream:\n * - Video: `setupVideoBufferActors`\n * - Audio: `setupAudioBufferActors`\n * - Text: `setupTextTrackActors`\n *\n * This behavior is pure-consumer: it reads `context[loaderKey]` and\n * dispatches typed messages via the variant's per-type `findResolvedTrack`\n * resolver.\n *\n * # Load modes as reactor states\n *\n * Four states encode the load-gating policy directly:\n *\n * - `'preconditions-unmet'` — no loader actor in context, or the selected\n * track hasn't resolved.\n * - `'dormant'` — loading disabled by policy: an observed `loadingSuspended`\n * (highest precedence) or `preload === 'none' && !loadActivated`. Nothing\n * fires; already-queued loader work drains. Auto-resumes into the derived\n * state when the policy lifts.\n * - `'metadata-only'` — `!loadActivated && preload !== 'auto' && preload !== 'none'`.\n * Fires an init-segment-only `load` message **once on entry**. The\n * variant's loader actor decides what to do — v/a's actor fetches the\n * init segment; text's actor no-ops (no init concept).\n * - `'full-range'` — `loadActivated || preload === 'auto'`. Effect re-fires\n * on selected-track change and on segment-boundary crossing.\n *\n * # Per-type parameterization (inference-driven)\n *\n * The helper is generic over `Track` — the resolved-track type. Each\n * variant supplies its own `findResolvedTrack` resolver via config; TS\n * infers `Track` from the resolver's return type. The loader signal's\n * value type is constrained to `SegmentLoaderLike<Track>` — anything with\n * a `send` method accepting the `Track`-parameterized message. Concrete\n * actor types (`SegmentLoaderActor`, `TextTrackSegmentLoaderActor`)\n * satisfy this via function-parameter contravariance: an actor whose\n * `.send` accepts a wider track type is assignable to a slot expecting a\n * narrower track type.\n *\n * No widening of actor message types is needed; no casts inside the\n * helper. Per-variant wiring (right loader paired with right resolver)\n * is enforced at the variant call site.\n */\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal } from '../../../core/signals/primitives';\nimport {\n DEFAULT_FORWARD_BUFFER_CONFIG,\n type ForwardBufferConfig,\n segmentStartForTime,\n} from '../../../media/buffer/forward-buffer';\nimport type { MaybeResolvedPresentation, Segment } from '../../../media/types';\nimport { findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack } from '../../../media/utils/tracks';\nimport type { BufferState, SegmentLoaderActor, SourceBufferState } from '../../actors/dom/segment-loader';\nimport type { TextTrackSegmentLoaderActor } from '../../actors/text-track-segment-loader';\nimport { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from '../../primitives/track-types';\n\n// Re-export buffer state types for consumers that import them from this module.\nexport type { BufferState, SourceBufferState };\n\n// ============================================================================\n// STATE & CONTEXT\n// ============================================================================\n\n/** State shape for segment loading. */\nexport interface SegmentLoadingState {\n presentation?: MaybeResolvedPresentation;\n preload?: string;\n /** Current playback position in seconds. Defaults to 0 when undefined. */\n currentTime?: number;\n /** True once a preload-overriding event has fired for the current source. */\n loadActivated?: boolean;\n /**\n * Intent-level policy input: initiate no new loading work while `true`.\n * Read here by the `loadXSegments` dispatchers (park in `'dormant'`,\n * highest precedence) and by `setupMediaSource` (a pending MediaSource\n * rebuild waits — attach runs the element's load algorithm). **Observed,\n * never declared**: no reader lists this key in `stateKeys`, so the slot\n * exists only in compositions where a feature behavior declares and\n * writes it (e.g. `setupAirPlay`, while a remote-playback session owns\n * presentation). An absent slot means never suspended.\n */\n loadingSuspended?: boolean;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/** Context shape for segment loading. Each variant only consumes its own type's loader actor. */\nexport interface SegmentLoadingContext {\n videoSegmentLoaderActor?: SegmentLoaderActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n textTrackSegmentLoaderActor?: TextTrackSegmentLoaderActor;\n}\n\n// ============================================================================\n// REACTOR\n// ============================================================================\n\ntype SegmentLoadingFsmState = 'preconditions-unmet' | 'dormant' | 'metadata-only' | 'full-range';\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId' | 'selectedTextTrackId';\ntype SegmentLoaderActorKey = 'videoSegmentLoaderActor' | 'audioSegmentLoaderActor' | 'textTrackSegmentLoaderActor';\n\ntype SegmentLoadingStateMap<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<SegmentLoadingState['presentation']>;\n preload: ReadonlySignal<SegmentLoadingState['preload']>;\n currentTime: ReadonlySignal<SegmentLoadingState['currentTime']>;\n loadActivated: ReadonlySignal<SegmentLoadingState['loadActivated']>;\n} & { [P in K]: ReadonlySignal<SegmentLoadingState[P]> };\n\n/** Shared `'load'` message shape parameterized over the resolved track type. */\ninterface LoadMessage<Track> {\n type: 'load';\n track: Track;\n range?: { start: number; end: number };\n}\n\n/** Structural constraint for any segment-loader-style actor. */\ninterface SegmentLoaderLike<Track> {\n send: (message: LoadMessage<Track>) => void;\n}\n\n/**\n * Specialization helper. Generic over `Track` (inferred from\n * `findResolvedTrack`'s return type). The loader value type is\n * constrained to `SegmentLoaderLike<Track>` — concrete actor types\n * (whose `.send` accepts a wider track union) satisfy this via\n * function-parameter contravariance.\n *\n * Tracks that the helper handles must have a `segments` field — used by\n * `segmentBoundarySignal` to compute the load-anchor boundary. Each\n * variant's resolver narrows to the right resolved-track shape.\n */\nfunction setupSegmentLoading<\n K extends SelectedTrackKey,\n L extends SegmentLoaderActorKey,\n Track extends { segments: readonly Segment[] },\n>({\n state,\n context,\n config,\n}: {\n // `loadingSuspended` is observed, never declared (see its state-shape doc):\n // optional here so variant maps — which omit it from their contracts — are\n // assignable as-is, while compositions with a writer expose the live slot.\n state: SegmentLoadingStateMap<K> & { loadingSuspended?: ReadonlySignal<SegmentLoadingState['loadingSuspended']> };\n context: { [P in L]: ReadonlySignal<SegmentLoaderLike<Track> | undefined> };\n config: {\n selectedKey: K;\n loaderKey: L;\n findResolvedTrack: (\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n ) => Track | undefined;\n forwardBuffer?: Partial<ForwardBufferConfig>;\n };\n}): Reactor<SegmentLoadingFsmState | 'destroying' | 'destroyed'> {\n const { selectedKey, loaderKey, findResolvedTrack } = config;\n const bufferDuration = config.forwardBuffer?.bufferDuration ?? DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration;\n\n const selectedTrack = computed<Track | undefined>(() =>\n findResolvedTrack(state.presentation.get(), state[selectedKey].get())\n );\n\n // Segment-boundary signal — `segmentStartForTime` returns the same number\n // while `currentTime` stays inside one segment, so signal-polyfill's\n // `Object.is` equality on this computed dedups within-segment ticks.\n const segmentBoundarySignal = computed(() => {\n const track = selectedTrack.get();\n if (!track) return undefined;\n return segmentStartForTime(state.currentTime.get() ?? 0, track.segments);\n });\n\n const derivedStateSignal = computed<SegmentLoadingFsmState>(() => {\n // Policy-off wins even over preconditions: a loader arriving while\n // suspended must not dispatch.\n if (state.loadingSuspended?.get()) return 'dormant';\n if (!context[loaderKey].get() || !selectedTrack.get()) return 'preconditions-unmet';\n if (state.loadActivated.get() || state.preload.get() === 'auto') return 'full-range';\n if (state.preload.get() === 'none') return 'dormant';\n return 'metadata-only';\n });\n\n return createMachineReactor<SegmentLoadingFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n dormant: {},\n 'metadata-only': {\n // Fires once on entry. Matches HTMLMediaElement's preload='metadata'\n // semantics — load enough to surface metadata for the entry-time\n // selection. Each variant's loader decides what to do: v/a's actor\n // fetches the init segment; text's actor treats no-range as a\n // no-op. Selection changes while still in `'metadata-only'` are\n // intentionally not followed.\n entry: () => {\n const track = selectedTrack.get()!;\n context[loaderKey].get()!.send({ type: 'load', track });\n },\n },\n 'full-range': {\n // Re-fires on selected-track changes and on segment-boundary\n // crossings. The loader and `currentTime` are peeked — the\n // boundary-dedup signal already handles re-firing policy.\n effects: () => {\n const track = selectedTrack.get()!;\n segmentBoundarySignal.get();\n const currentTime = peek(state.currentTime) ?? 0;\n peek(context[loaderKey])!.send({\n type: 'load',\n track,\n range: { start: currentTime, end: currentTime + bufferDuration },\n });\n },\n },\n },\n });\n}\n\n// ============================================================================\n// Per-helper-per-type configs — defaults that variants spread engine config over\n// ============================================================================\n\nconst VIDEO_SEGMENT_LOADING_CONFIG = {\n ...VIDEO_TYPE_CONFIG,\n findResolvedTrack: findResolvedVideoTrack,\n} as const;\n\nconst AUDIO_SEGMENT_LOADING_CONFIG = {\n ...AUDIO_TYPE_CONFIG,\n findResolvedTrack: findResolvedAudioTrack,\n} as const;\n\nconst TEXT_SEGMENT_LOADING_CONFIG = {\n ...TEXT_TYPE_CONFIG,\n findResolvedTrack: findResolvedTextTrack,\n} as const;\n\n// ============================================================================\n// Specialized exports — one per media type\n// ============================================================================\n\nexport const loadVideoSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedVideoTrackId'],\n contextKeys: ['videoSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedVideoTrackId'>;\n context: {\n videoSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['videoSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...VIDEO_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n\nexport const loadAudioSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedAudioTrackId'],\n contextKeys: ['audioSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedAudioTrackId'>;\n context: {\n audioSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['audioSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...AUDIO_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n\nexport const loadTextTrackSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedTextTrackId'],\n contextKeys: ['textTrackSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedTextTrackId'>;\n context: {\n textTrackSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['textTrackSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...TEXT_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4IA,SAAS,oBAIP,EACA,OACA,SACA,UAgB+D;CAC/D,MAAM,EAAE,aAAa,WAAW,sBAAsB;CACtD,MAAM,iBAAiB,OAAO,eAAe,kBAAkB,8BAA8B;CAE7F,MAAM,gBAAgB,eACpB,kBAAkB,MAAM,aAAa,IAAI,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CACtE;CAKA,MAAM,wBAAwB,eAAe;EAC3C,MAAM,QAAQ,cAAc,IAAI;EAChC,IAAI,CAAC,OAAO,OAAO,KAAA;EACnB,OAAO,oBAAoB,MAAM,YAAY,IAAI,KAAK,GAAG,MAAM,QAAQ;CACzE,CAAC;CAED,MAAM,qBAAqB,eAAuC;EAGhE,IAAI,MAAM,kBAAkB,IAAI,GAAG,OAAO;EAC1C,IAAI,CAAC,QAAQ,UAAU,CAAC,IAAI,KAAK,CAAC,cAAc,IAAI,GAAG,OAAO;EAC9D,IAAI,MAAM,cAAc,IAAI,KAAK,MAAM,QAAQ,IAAI,MAAM,QAAQ,OAAO;EACxE,IAAI,MAAM,QAAQ,IAAI,MAAM,QAAQ,OAAO;EAC3C,OAAO;CACT,CAAC;CAED,OAAO,qBAA6C;EAClD,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GACxB,SAAS,CAAC;GACV,iBAAiB,EAOf,aAAa;IACX,MAAM,QAAQ,cAAc,IAAI;IAChC,QAAQ,UAAU,CAAC,IAAI,CAAC,CAAE,KAAK;KAAE,MAAM;KAAQ;IAAM,CAAC;GACxD,EACF;GACA,cAAc,EAIZ,eAAe;IACb,MAAM,QAAQ,cAAc,IAAI;IAChC,sBAAsB,IAAI;IAC1B,MAAM,cAAc,KAAK,MAAM,WAAW,KAAK;IAC/C,KAAK,QAAQ,UAAU,CAAC,CAAE,KAAK;KAC7B,MAAM;KACN;KACA,OAAO;MAAE,OAAO;MAAa,KAAK,cAAc;KAAe;IACjE,CAAC;GACH,EACF;EACF;CACF,CAAC;AACH;AAMA,MAAM,+BAA+B;CACnC,GAAG;CACH,mBAAmB;AACrB;AAEA,MAAM,+BAA+B;CACnC,GAAG;CACH,mBAAmB;AACrB;AAEA,MAAM,8BAA8B;CAClC,GAAG;CACH,mBAAmB;AACrB;AAMA,MAAa,oBAAoB,eAAe;CAC9C,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;CAAsB;CAC7F,aAAa,CAAC,yBAAyB;CACvC,QAAQ,EACN,OACA,SACA,SAAS,CAAC,QAQV,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA8B,GAAG;EAAO;CACvD,CAAC;AACL,CAAC;AAED,MAAa,oBAAoB,eAAe;CAC9C,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;CAAsB;CAC7F,aAAa,CAAC,yBAAyB;CACvC,QAAQ,EACN,OACA,SACA,SAAS,CAAC,QAQV,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA8B,GAAG;EAAO;CACvD,CAAC;AACL,CAAC;AAED,MAAa,wBAAwB,eAAe;CAClD,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;CAAqB;CAC5F,aAAa,CAAC,6BAA6B;CAC3C,QAAQ,EACN,OACA,SACA,SAAS,CAAC,QAQV,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA6B,GAAG;EAAO;CACtD,CAAC;AACL,CAAC"}
|
|
1
|
+
{"version":3,"file":"load-segments.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/load-segments.ts"],"sourcesContent":["/**\n * **Per-type segment loading dispatch.** Per available track type (video / audio / text), reads the per-type\n * segment-loader actor from context and sends typed `'load'` messages whenever a meaningful loading condition changes\n * (selected track, current time crossing a segment boundary).\n *\n * Loader-actor lifecycle is owned upstream:\n *\n * - Video: `setupVideoBufferActors`\n * - Audio: `setupAudioBufferActors`\n * - Text: `setupTextTrackActors`\n *\n * This behavior is pure-consumer: it reads `context[loaderKey]` and dispatches typed messages via the variant's\n * per-type `findResolvedTrack` resolver.\n *\n * # Load modes as reactor states\n *\n * Four states encode the load-gating policy directly:\n *\n * - `'preconditions-unmet'` — no loader actor in context, or the selected track hasn't resolved.\n * - `'dormant'` — loading disabled by policy: an observed `loadingSuspended` (highest precedence) or `preload === 'none'\n * && !loadActivated`. Nothing fires; already-queued loader work drains. Auto-resumes into the derived state when the\n * policy lifts.\n * - `'metadata-only'` — `!loadActivated && preload !== 'auto' && preload !== 'none'`. Fires an init-segment-only `load`\n * message **once on entry**. The variant's loader actor decides what to do — v/a's actor fetches the init segment;\n * text's actor no-ops (no init concept).\n * - `'full-range'` — `loadActivated || preload === 'auto'`. Effect re-fires on selected-track change and on\n * segment-boundary crossing.\n *\n * # Per-type parameterization (inference-driven)\n *\n * The helper is generic over `Track` — the resolved-track type. Each variant supplies its own `findResolvedTrack`\n * resolver via config; TS infers `Track` from the resolver's return type. The loader signal's value type is constrained\n * to `SegmentLoaderLike<Track>` — anything with a `send` method accepting the `Track`-parameterized message. Concrete\n * actor types (`SegmentLoaderActor`, `TextTrackSegmentLoaderActor`) satisfy this via function-parameter contravariance:\n * an actor whose `.send` accepts a wider track type is assignable to a slot expecting a narrower track type.\n *\n * No widening of actor message types is needed; no casts inside the helper. Per-variant wiring (right loader paired\n * with right resolver) is enforced at the variant call site.\n */\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Reactor } from '../../../core/reactors/create-machine-reactor';\nimport { createMachineReactor } from '../../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal } from '../../../core/signals/primitives';\nimport {\n DEFAULT_FORWARD_BUFFER_CONFIG,\n type ForwardBufferConfig,\n segmentStartForTime,\n} from '../../../media/buffer/forward-buffer';\nimport type { MaybeResolvedPresentation, Segment } from '../../../media/types';\nimport { findResolvedAudioTrack, findResolvedTextTrack, findResolvedVideoTrack } from '../../../media/utils/tracks';\nimport type { BufferState, SegmentLoaderActor, SourceBufferState } from '../../actors/dom/segment-loader';\nimport type { TextTrackSegmentLoaderActor } from '../../actors/text-track-segment-loader';\nimport { AUDIO_TYPE_CONFIG, TEXT_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from '../../primitives/track-types';\n\n// Re-export buffer state types for consumers that import them from this module.\nexport type { BufferState, SourceBufferState };\n\n// ============================================================================\n// STATE & CONTEXT\n// ============================================================================\n\n/** State shape for segment loading. */\nexport interface SegmentLoadingState {\n presentation?: MaybeResolvedPresentation;\n preload?: string;\n /** Current playback position in seconds. Defaults to 0 when undefined. */\n currentTime?: number;\n /** True once a preload-overriding event has fired for the current source. */\n loadActivated?: boolean;\n /**\n * Intent-level policy input: initiate no new loading work while `true`. Read here by the `loadXSegments` dispatchers\n * (park in `'dormant'`, highest precedence) and by `setupMediaSource` (a pending MediaSource rebuild waits — attach\n * runs the element's load algorithm). **Observed, never declared**: no reader lists this key in `stateKeys`, so the\n * slot exists only in compositions where a feature behavior declares and writes it (e.g. `setupAirPlay`, while a\n * remote-playback session owns presentation). An absent slot means never suspended.\n */\n loadingSuspended?: boolean;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/** Context shape for segment loading. Each variant only consumes its own type's loader actor. */\nexport interface SegmentLoadingContext {\n videoSegmentLoaderActor?: SegmentLoaderActor;\n audioSegmentLoaderActor?: SegmentLoaderActor;\n textTrackSegmentLoaderActor?: TextTrackSegmentLoaderActor;\n}\n\n// ============================================================================\n// REACTOR\n// ============================================================================\n\ntype SegmentLoadingFsmState = 'preconditions-unmet' | 'dormant' | 'metadata-only' | 'full-range';\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId' | 'selectedTextTrackId';\ntype SegmentLoaderActorKey = 'videoSegmentLoaderActor' | 'audioSegmentLoaderActor' | 'textTrackSegmentLoaderActor';\n\ntype SegmentLoadingStateMap<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<SegmentLoadingState['presentation']>;\n preload: ReadonlySignal<SegmentLoadingState['preload']>;\n currentTime: ReadonlySignal<SegmentLoadingState['currentTime']>;\n loadActivated: ReadonlySignal<SegmentLoadingState['loadActivated']>;\n} & { [P in K]: ReadonlySignal<SegmentLoadingState[P]> };\n\n/** Shared `'load'` message shape parameterized over the resolved track type. */\ninterface LoadMessage<Track> {\n type: 'load';\n track: Track;\n range?: { start: number; end: number };\n}\n\n/** Structural constraint for any segment-loader-style actor. */\ninterface SegmentLoaderLike<Track> {\n send: (message: LoadMessage<Track>) => void;\n}\n\n/**\n * Specialization helper. Generic over `Track` (inferred from `findResolvedTrack`'s return type). The loader value type\n * is constrained to `SegmentLoaderLike<Track>` — concrete actor types (whose `.send` accepts a wider track union)\n * satisfy this via function-parameter contravariance.\n *\n * Tracks that the helper handles must have a `segments` field — used by `segmentBoundarySignal` to compute the\n * load-anchor boundary. Each variant's resolver narrows to the right resolved-track shape.\n */\nfunction setupSegmentLoading<\n K extends SelectedTrackKey,\n L extends SegmentLoaderActorKey,\n Track extends { segments: readonly Segment[] },\n>({\n state,\n context,\n config,\n}: {\n // `loadingSuspended` is observed, never declared (see its state-shape doc):\n // optional here so variant maps — which omit it from their contracts — are\n // assignable as-is, while compositions with a writer expose the live slot.\n state: SegmentLoadingStateMap<K> & { loadingSuspended?: ReadonlySignal<SegmentLoadingState['loadingSuspended']> };\n context: { [P in L]: ReadonlySignal<SegmentLoaderLike<Track> | undefined> };\n config: {\n selectedKey: K;\n loaderKey: L;\n findResolvedTrack: (\n presentation: MaybeResolvedPresentation | undefined,\n trackId: string | undefined\n ) => Track | undefined;\n forwardBuffer?: Partial<ForwardBufferConfig>;\n };\n}): Reactor<SegmentLoadingFsmState | 'destroying' | 'destroyed'> {\n const { selectedKey, loaderKey, findResolvedTrack } = config;\n const bufferDuration = config.forwardBuffer?.bufferDuration ?? DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration;\n\n const selectedTrack = computed<Track | undefined>(() =>\n findResolvedTrack(state.presentation.get(), state[selectedKey].get())\n );\n\n // Segment-boundary signal — `segmentStartForTime` returns the same number\n // while `currentTime` stays inside one segment, so signal-polyfill's\n // `Object.is` equality on this computed dedups within-segment ticks.\n const segmentBoundarySignal = computed(() => {\n const track = selectedTrack.get();\n if (!track) return undefined;\n\n return segmentStartForTime(state.currentTime.get() ?? 0, track.segments);\n });\n\n const derivedStateSignal = computed<SegmentLoadingFsmState>(() => {\n // Policy-off wins even over preconditions: a loader arriving while\n // suspended must not dispatch.\n if (state.loadingSuspended?.get()) return 'dormant';\n\n if (!context[loaderKey].get() || !selectedTrack.get()) return 'preconditions-unmet';\n\n if (state.loadActivated.get() || state.preload.get() === 'auto') return 'full-range';\n\n if (state.preload.get() === 'none') return 'dormant';\n\n return 'metadata-only';\n });\n\n return createMachineReactor<SegmentLoadingFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n dormant: {},\n 'metadata-only': {\n // Fires once on entry. Matches HTMLMediaElement's preload='metadata'\n // semantics — load enough to surface metadata for the entry-time\n // selection. Each variant's loader decides what to do: v/a's actor\n // fetches the init segment; text's actor treats no-range as a\n // no-op. Selection changes while still in `'metadata-only'` are\n // intentionally not followed.\n entry: () => {\n const track = selectedTrack.get()!;\n\n context[loaderKey].get()!.send({ type: 'load', track });\n },\n },\n 'full-range': {\n // Re-fires on selected-track changes and on segment-boundary\n // crossings. The loader and `currentTime` are peeked — the\n // boundary-dedup signal already handles re-firing policy.\n effects: () => {\n const track = selectedTrack.get()!;\n\n segmentBoundarySignal.get();\n const currentTime = peek(state.currentTime) ?? 0;\n\n peek(context[loaderKey])!.send({\n type: 'load',\n track,\n range: { start: currentTime, end: currentTime + bufferDuration },\n });\n },\n },\n },\n });\n}\n\n// ============================================================================\n// Per-helper-per-type configs — defaults that variants spread engine config over\n// ============================================================================\n\nconst VIDEO_SEGMENT_LOADING_CONFIG = {\n ...VIDEO_TYPE_CONFIG,\n findResolvedTrack: findResolvedVideoTrack,\n} as const;\n\nconst AUDIO_SEGMENT_LOADING_CONFIG = {\n ...AUDIO_TYPE_CONFIG,\n findResolvedTrack: findResolvedAudioTrack,\n} as const;\n\nconst TEXT_SEGMENT_LOADING_CONFIG = {\n ...TEXT_TYPE_CONFIG,\n findResolvedTrack: findResolvedTextTrack,\n} as const;\n\n// ============================================================================\n// Specialized exports — one per media type\n// ============================================================================\n\nexport const loadVideoSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedVideoTrackId'],\n contextKeys: ['videoSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedVideoTrackId'>;\n context: {\n videoSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['videoSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...VIDEO_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n\nexport const loadAudioSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedAudioTrackId'],\n contextKeys: ['audioSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedAudioTrackId'>;\n context: {\n audioSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['audioSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...AUDIO_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n\nexport const loadTextTrackSegments = defineBehavior({\n stateKeys: ['presentation', 'preload', 'currentTime', 'loadActivated', 'selectedTextTrackId'],\n contextKeys: ['textTrackSegmentLoaderActor'],\n setup: ({\n state,\n context,\n config = {},\n }: {\n state: SegmentLoadingStateMap<'selectedTextTrackId'>;\n context: {\n textTrackSegmentLoaderActor: ReadonlySignal<SegmentLoadingContext['textTrackSegmentLoaderActor']>;\n };\n config?: object;\n }) =>\n setupSegmentLoading({\n state,\n context,\n config: { ...TEXT_SEGMENT_LOADING_CONFIG, ...config },\n }),\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6HA,SAAS,oBAIP,EACA,OACA,SACA,UAgB+D;CAC/D,MAAM,EAAE,aAAa,WAAW,sBAAsB;CACtD,MAAM,iBAAiB,OAAO,eAAe,kBAAkB,8BAA8B;CAE7F,MAAM,gBAAgB,eACpB,kBAAkB,MAAM,aAAa,IAAI,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CACtE;CAKA,MAAM,wBAAwB,eAAe;EAC3C,MAAM,QAAQ,cAAc,IAAI;EAChC,IAAI,CAAC,OAAO,OAAO,KAAA;EAEnB,OAAO,oBAAoB,MAAM,YAAY,IAAI,KAAK,GAAG,MAAM,QAAQ;CACzE,CAAC;CAED,MAAM,qBAAqB,eAAuC;EAGhE,IAAI,MAAM,kBAAkB,IAAI,GAAG,OAAO;EAE1C,IAAI,CAAC,QAAQ,UAAU,CAAC,IAAI,KAAK,CAAC,cAAc,IAAI,GAAG,OAAO;EAE9D,IAAI,MAAM,cAAc,IAAI,KAAK,MAAM,QAAQ,IAAI,MAAM,QAAQ,OAAO;EAExE,IAAI,MAAM,QAAQ,IAAI,MAAM,QAAQ,OAAO;EAE3C,OAAO;CACT,CAAC;CAED,OAAO,qBAA6C;EAClD,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,uBAAuB,CAAC;GACxB,SAAS,CAAC;GACV,iBAAiB,EAOf,aAAa;IACX,MAAM,QAAQ,cAAc,IAAI;IAEhC,QAAQ,UAAU,CAAC,IAAI,CAAC,CAAE,KAAK;KAAE,MAAM;KAAQ;IAAM,CAAC;GACxD,EACF;GACA,cAAc,EAIZ,eAAe;IACb,MAAM,QAAQ,cAAc,IAAI;IAEhC,sBAAsB,IAAI;IAC1B,MAAM,cAAc,KAAK,MAAM,WAAW,KAAK;IAE/C,KAAK,QAAQ,UAAU,CAAC,CAAE,KAAK;KAC7B,MAAM;KACN;KACA,OAAO;MAAE,OAAO;MAAa,KAAK,cAAc;KAAe;IACjE,CAAC;GACH,EACF;EACF;CACF,CAAC;AACH;AAMA,MAAM,+BAA+B;CACnC,GAAG;CACH,mBAAmB;AACrB;AAEA,MAAM,+BAA+B;CACnC,GAAG;CACH,mBAAmB;AACrB;AAEA,MAAM,8BAA8B;CAClC,GAAG;CACH,mBAAmB;AACrB;AAMA,MAAa,oBAAoB,eAAe;CAC9C,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;CAAsB;CAC7F,aAAa,CAAC,yBAAyB;CACvC,QAAQ,EACN,OACA,SACA,SAAS,CAAC,QAQV,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA8B,GAAG;EAAO;CACvD,CAAC;AACL,CAAC;AAED,MAAa,oBAAoB,eAAe;CAC9C,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;CAAsB;CAC7F,aAAa,CAAC,yBAAyB;CACvC,QAAQ,EACN,OACA,SACA,SAAS,CAAC,QAQV,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA8B,GAAG;EAAO;CACvD,CAAC;AACL,CAAC;AAED,MAAa,wBAAwB,eAAe;CAClD,WAAW;EAAC;EAAgB;EAAW;EAAe;EAAiB;CAAqB;CAC5F,aAAa,CAAC,6BAA6B;CAC3C,QAAQ,EACN,OACA,SACA,SAAS,CAAC,QAQV,oBAAoB;EAClB;EACA;EACA,QAAQ;GAAE,GAAG;GAA6B,GAAG;EAAO;CACtD,CAAC;AACL,CAAC"}
|
|
@@ -4,41 +4,36 @@ import { getMinBufferedEnd } from "../../../media/dom/mse/duration.js";
|
|
|
4
4
|
import { listen } from "@videojs/utils/dom";
|
|
5
5
|
//#region src/playback/behaviors/dom/recover-end-stall.ts
|
|
6
6
|
/**
|
|
7
|
-
* Recover the end-of-stream stall that Chrome exhibits on skewed A/V. After
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* the playhead sits at the reachable buffered end, nudges `currentTime` to `duration`
|
|
14
|
-
* to force the native `ended`.
|
|
7
|
+
* Recover the end-of-stream stall that Chrome exhibits on skewed A/V. After `endOfStream`, when the audio and video
|
|
8
|
+
* tracks end a few ms apart (e.g. a source with an A/V PTS skew), Chrome's audio-clock-paced playback freezes the
|
|
9
|
+
* playhead ~50–70ms short of the reachable buffered end and never fires `ended` — so playback hangs at the very end and
|
|
10
|
+
* loop never re-triggers. This behavior watches for the `waiting` event that fires at that freeze and, when the
|
|
11
|
+
* MediaSource is `ended` and the playhead sits at the reachable buffered end, nudges `currentTime` to `duration` to
|
|
12
|
+
* force the native `ended`.
|
|
15
13
|
*
|
|
16
|
-
* **Event-driven, no poll.** `waiting` fires at the instant the playhead stalls
|
|
17
|
-
*
|
|
18
|
-
* add its interval + a stall threshold before reacting.
|
|
14
|
+
* **Event-driven, no poll.** `waiting` fires at the instant the playhead stalls (measured ~0ms latency), so there's
|
|
15
|
+
* nothing to gain from polling — and polling would add its interval + a stall threshold before reacting.
|
|
19
16
|
*
|
|
20
|
-
* **Proximity to the
|
|
21
|
-
* `getMinBufferedEnd(mediaSource.sourceBuffers)` — the `min` of the per-track (video/audio)
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* measured gap and is config-tunable for empirical tuning.
|
|
17
|
+
* **Proximity to the _reachable_ buffered end** is the discriminator. That end is
|
|
18
|
+
* `getMinBufferedEnd(mediaSource.sourceBuffers)` — the `min` of the per-track (video/audio) SourceBuffer ends, i.e. the
|
|
19
|
+
* furthest point playback can reach — read from the SourceBuffers directly rather than the `mediaElement.buffered`
|
|
20
|
+
* aggregate. Once `endOfStream` is signalled that's the true content end. Requiring the playhead within
|
|
21
|
+
* `endStallNudgeWindow` of it distinguishes the real end-of-stream freeze from a mid-stream buffer-hole stall (which
|
|
22
|
+
* sits far from the buffered end), so we never skip content. The window must exceed the freeze gap; too small would
|
|
23
|
+
* miss the stall (a permanent hang), so the default is generous relative to the measured gap and is config-tunable for
|
|
24
|
+
* empirical tuning.
|
|
29
25
|
*
|
|
30
|
-
* Inert where it shouldn't act: live (the MediaSource never reaches `ended` while the
|
|
31
|
-
*
|
|
26
|
+
* Inert where it shouldn't act: live (the MediaSource never reaches `ended` while the window grows; `duration` is
|
|
27
|
+
* `Infinity`) and streams that end cleanly (no `waiting`).
|
|
32
28
|
*
|
|
33
29
|
* See `internal/decisions/spf/end-of-stream-av-skew-recovery.md`.
|
|
34
30
|
*/
|
|
35
31
|
/** ~2.8× the measured max freeze gap (71ms) — tight, but with headroom against a miss. */
|
|
36
32
|
const DEFAULT_END_STALL_NUDGE_WINDOW = .2;
|
|
37
33
|
/**
|
|
38
|
-
* Whether a `waiting` should be forced to `ended`: the MediaSource is `ended`, the
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* true end, not a mid-stream buffer hole). Pure — the behavior supplies the live values.
|
|
34
|
+
* Whether a `waiting` should be forced to `ended`: the MediaSource is `ended`, the stream is finite (not live),
|
|
35
|
+
* playback is active (not paused/seeking/already-ended), and the playhead sits within `nudgeWindow` of the reachable
|
|
36
|
+
* buffered end (so it's the true end, not a mid-stream buffer hole). Pure — the behavior supplies the live values.
|
|
42
37
|
*/
|
|
43
38
|
function shouldForceEnded(input, nudgeWindow) {
|
|
44
39
|
const { msEnded, durationFinite, paused, seeking, ended, currentTime, bufferedEnd } = input;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recover-end-stall.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/recover-end-stall.ts"],"sourcesContent":["/**\n * Recover the end-of-stream stall that Chrome exhibits on skewed A/V. After
|
|
1
|
+
{"version":3,"file":"recover-end-stall.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/recover-end-stall.ts"],"sourcesContent":["/**\n * Recover the end-of-stream stall that Chrome exhibits on skewed A/V. After `endOfStream`, when the audio and video\n * tracks end a few ms apart (e.g. a source with an A/V PTS skew), Chrome's audio-clock-paced playback freezes the\n * playhead ~50–70ms short of the reachable buffered end and never fires `ended` — so playback hangs at the very end and\n * loop never re-triggers. This behavior watches for the `waiting` event that fires at that freeze and, when the\n * MediaSource is `ended` and the playhead sits at the reachable buffered end, nudges `currentTime` to `duration` to\n * force the native `ended`.\n *\n * **Event-driven, no poll.** `waiting` fires at the instant the playhead stalls (measured ~0ms latency), so there's\n * nothing to gain from polling — and polling would add its interval + a stall threshold before reacting.\n *\n * **Proximity to the _reachable_ buffered end** is the discriminator. That end is\n * `getMinBufferedEnd(mediaSource.sourceBuffers)` — the `min` of the per-track (video/audio) SourceBuffer ends, i.e. the\n * furthest point playback can reach — read from the SourceBuffers directly rather than the `mediaElement.buffered`\n * aggregate. Once `endOfStream` is signalled that's the true content end. Requiring the playhead within\n * `endStallNudgeWindow` of it distinguishes the real end-of-stream freeze from a mid-stream buffer-hole stall (which\n * sits far from the buffered end), so we never skip content. The window must exceed the freeze gap; too small would\n * miss the stall (a permanent hang), so the default is generous relative to the measured gap and is config-tunable for\n * empirical tuning.\n *\n * Inert where it shouldn't act: live (the MediaSource never reaches `ended` while the window grows; `duration` is\n * `Infinity`) and streams that end cleanly (no `waiting`).\n *\n * See `internal/decisions/spf/end-of-stream-av-skew-recovery.md`.\n */\nimport { listen } from '@videojs/utils/dom';\n\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport type { ReadonlySignal } from '../../../core/signals/primitives';\nimport { getMinBufferedEnd } from '../../../media/dom/mse/duration';\n\nexport interface RecoverEndStallContext {\n mediaElement?: HTMLMediaElement | undefined;\n mediaSource?: MediaSource | undefined;\n}\n\nexport interface RecoverEndStallConfig {\n /**\n * How close (seconds) the playhead must be to the reachable buffered end for a `waiting` to count as the\n * end-of-stream freeze. Must exceed the audio-clock freeze gap (~50–70ms measured on Chrome); too small risks missing\n * the stall (a permanent hang), so keep a margin. Default {@link DEFAULT_END_STALL_NUDGE_WINDOW}.\n */\n endStallNudgeWindow?: number;\n}\n\n/** ~2.8× the measured max freeze gap (71ms) — tight, but with headroom against a miss. */\nexport const DEFAULT_END_STALL_NUDGE_WINDOW = 0.2;\n\n/**\n * Whether a `waiting` should be forced to `ended`: the MediaSource is `ended`, the stream is finite (not live),\n * playback is active (not paused/seeking/already-ended), and the playhead sits within `nudgeWindow` of the reachable\n * buffered end (so it's the true end, not a mid-stream buffer hole). Pure — the behavior supplies the live values.\n */\nexport function shouldForceEnded(\n input: {\n msEnded: boolean;\n durationFinite: boolean;\n paused: boolean;\n seeking: boolean;\n ended: boolean;\n currentTime: number;\n bufferedEnd: number | undefined;\n },\n nudgeWindow: number\n): boolean {\n const { msEnded, durationFinite, paused, seeking, ended, currentTime, bufferedEnd } = input;\n if (!msEnded || !durationFinite || paused || seeking || ended || bufferedEnd === undefined) return false;\n\n const gap = bufferedEnd - currentTime;\n\n return gap >= 0 && gap < nudgeWindow;\n}\n\nfunction recoverEndStallSetup({\n context,\n config,\n}: {\n context: {\n mediaElement: ReadonlySignal<RecoverEndStallContext['mediaElement']>;\n mediaSource: ReadonlySignal<RecoverEndStallContext['mediaSource']>;\n };\n config?: RecoverEndStallConfig;\n}): () => void {\n const nudgeWindow = config?.endStallNudgeWindow ?? DEFAULT_END_STALL_NUDGE_WINDOW;\n\n return effect(() => {\n const mediaElement = context.mediaElement.get();\n if (!mediaElement) return;\n\n const onWaiting = () => {\n const mediaSource = context.mediaSource.get();\n const forceEnded = shouldForceEnded(\n {\n msEnded: mediaSource?.readyState === 'ended',\n durationFinite: Number.isFinite(mediaElement.duration),\n paused: mediaElement.paused,\n seeking: mediaElement.seeking,\n ended: mediaElement.ended,\n currentTime: mediaElement.currentTime,\n bufferedEnd: mediaSource ? getMinBufferedEnd(mediaSource.sourceBuffers) : undefined,\n },\n nudgeWindow\n );\n\n // Nudge to `duration` → native `ended` (the seeking/ended guards above prevent a\n // re-fire while the nudge-seek is in flight, so no latch is needed).\n if (forceEnded) mediaElement.currentTime = mediaElement.duration;\n };\n\n return listen(mediaElement, 'waiting', onWaiting);\n });\n}\n\nexport const recoverEndStall = defineBehavior({\n stateKeys: [] as const,\n contextKeys: ['mediaElement', 'mediaSource'] as const,\n setup: recoverEndStallSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,MAAa,iCAAiC;;;;;;AAO9C,SAAgB,iBACd,OASA,aACS;CACT,MAAM,EAAE,SAAS,gBAAgB,QAAQ,SAAS,OAAO,aAAa,gBAAgB;CACtF,IAAI,CAAC,WAAW,CAAC,kBAAkB,UAAU,WAAW,SAAS,gBAAgB,KAAA,GAAW,OAAO;CAEnG,MAAM,MAAM,cAAc;CAE1B,OAAO,OAAO,KAAK,MAAM;AAC3B;AAEA,SAAS,qBAAqB,EAC5B,SACA,UAOa;CACb,MAAM,cAAc,QAAQ,uBAAA;CAE5B,OAAO,aAAa;EAClB,MAAM,eAAe,QAAQ,aAAa,IAAI;EAC9C,IAAI,CAAC,cAAc;EAEnB,MAAM,kBAAkB;GACtB,MAAM,cAAc,QAAQ,YAAY,IAAI;GAgB5C,IAfmB,iBACjB;IACE,SAAS,aAAa,eAAe;IACrC,gBAAgB,OAAO,SAAS,aAAa,QAAQ;IACrD,QAAQ,aAAa;IACrB,SAAS,aAAa;IACtB,OAAO,aAAa;IACpB,aAAa,aAAa;IAC1B,aAAa,cAAc,kBAAkB,YAAY,aAAa,IAAI,KAAA;GAC5E,GACA,WAKW,GAAG,aAAa,cAAc,aAAa;EAC1D;EAEA,OAAO,OAAO,cAAc,WAAW,SAAS;CAClD,CAAC;AACH;AAEA,MAAa,kBAAkB,eAAe;CAC5C,WAAW,CAAC;CACZ,aAAa,CAAC,gBAAgB,aAAa;CAC3C,OAAO;AACT,CAAC"}
|