@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
|
@@ -4,46 +4,37 @@ import { createMachineReactor } from "../../core/reactors/create-machine-reactor
|
|
|
4
4
|
import { isResolvedPresentation } from "../../media/types/index.js";
|
|
5
5
|
//#region src/playback/behaviors/collect-errors.ts
|
|
6
6
|
/**
|
|
7
|
-
* **Owns the engine's error sequence.** Reporters append through
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* unbounded across a session.
|
|
7
|
+
* **Owns the engine's error sequence.** Reporters append through {@link emitError}; this behavior owns the slot and its
|
|
8
|
+
* per-source lifecycle, clearing it on exit so a new source starts clean and the sequence can't grow unbounded across a
|
|
9
|
+
* session.
|
|
11
10
|
*
|
|
12
|
-
* Same split as `setupFailoverMonitor` and `failedCdns`: writes come from
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* is why this is a lifecycle owner rather than an error *handler*.
|
|
11
|
+
* Same split as `setupFailoverMonitor` and `failedCdns`: writes come from wherever the condition is detected, one
|
|
12
|
+
* behavior owns the slot. Deliberately has no `effects` — it holds no policy and derives nothing. Severity is decided
|
|
13
|
+
* at the adapter, not here (see `internal/design/spf/features/errors.md`), which is why this is a lifecycle owner
|
|
14
|
+
* rather than an error _handler_.
|
|
17
15
|
*
|
|
18
|
-
* Clearing binds to
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* prior source's errors forward; `resolve-track` guards the same transition with
|
|
24
|
-
* a commit-time id check, and doing likewise here is a follow-up.
|
|
16
|
+
* Clearing binds to _exit_ of `presentation-resolved`, mirroring the sibling mixins' clear-on-teardown (`emptied` /
|
|
17
|
+
* `MEDIA_DETACHED`). A live reload swaps the presentation object without leaving the resolved state, so it doesn't
|
|
18
|
+
* clear — only an actual source change or destroy does. Known gap: a resolved→resolved source swap that never passes
|
|
19
|
+
* through unresolved carries the prior source's errors forward; `resolve-track` guards the same transition with a
|
|
20
|
+
* commit-time id check, and doing likewise here is a follow-up.
|
|
25
21
|
*
|
|
26
|
-
* The vocabulary itself ({@link SvtaError} and the codes) is DOM- and
|
|
27
|
-
*
|
|
28
|
-
* it writes.
|
|
22
|
+
* The vocabulary itself ({@link SvtaError} and the codes) is DOM- and signal-free in `media/errors`; only the write
|
|
23
|
+
* seam lives here, with the slot it writes.
|
|
29
24
|
*/
|
|
30
25
|
/**
|
|
31
|
-
* Append `error` to the engine's error sequence. No-op when no owner is
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* Writes go through `update` so concurrent reporters can't lose each other's
|
|
35
|
-
* appends.
|
|
26
|
+
* Append `error` to the engine's error sequence. No-op when no owner is composed. Replaces the array rather than
|
|
27
|
+
* mutating it, so signal consumers notify; duplicates are kept, since a repeated condition is a real observation.
|
|
28
|
+
* Writes go through `update` so concurrent reporters can't lose each other's appends.
|
|
36
29
|
*
|
|
37
|
-
* Every emission is logged, deliberately
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* reaching a person: only *verdicts* are promoted to the media surface, so every
|
|
43
|
-
* cause (and any non-fatal notice) is otherwise invisible outside a debugger.
|
|
30
|
+
* Every emission is logged, deliberately _before_ the owner check. A condition emitted with no `collectErrors` composed
|
|
31
|
+
* is dropped on the floor — that's the case where a log is the only evidence it happened at all, so gating the log on
|
|
32
|
+
* the same check would hide exactly what's worth seeing. Emissions that _are_ collected still get logged, because
|
|
33
|
+
* reaching `state.errors` is no guarantee of reaching a person: only _verdicts_ are promoted to the media surface, so
|
|
34
|
+
* every cause (and any non-fatal notice) is otherwise invisible outside a debugger.
|
|
44
35
|
*
|
|
45
|
-
* Ungated rather than `__DEV__`-only, matching the other reporting paths in this
|
|
46
|
-
*
|
|
36
|
+
* Ungated rather than `__DEV__`-only, matching the other reporting paths in this package (`resolve-presentation`,
|
|
37
|
+
* `track-switching`, the segment actors).
|
|
47
38
|
*/
|
|
48
39
|
function emitError(state, error) {
|
|
49
40
|
console.error("[spf] reported condition", error);
|
|
@@ -51,31 +42,27 @@ function emitError(state, error) {
|
|
|
51
42
|
update(state.errors, (errors) => [...errors ?? [], error]);
|
|
52
43
|
}
|
|
53
44
|
/**
|
|
54
|
-
* A "constraint" that reports a type the source carries **no** renditions of, for
|
|
55
|
-
*
|
|
45
|
+
* A "constraint" that reports a type the source carries **no** renditions of, for a composition that can't play without
|
|
46
|
+
* it.
|
|
56
47
|
*
|
|
57
|
-
* Strange on purpose, and the strangeness is the point: it never constrains
|
|
58
|
-
*
|
|
59
|
-
* composition opts in by adding it to `constraints` — nothing to thread through
|
|
48
|
+
* Strange on purpose, and the strangeness is the point: it never constrains anything, always returning its input
|
|
49
|
+
* untouched. It is shaped as a rule so a composition opts in by adding it to `constraints` — nothing to thread through
|
|
60
50
|
* config, and no cost at all to a composition that leaves it out.
|
|
61
51
|
*
|
|
62
|
-
* **Belongs last in the chain.** A constraint sees the list as it stands at its
|
|
63
|
-
*
|
|
64
|
-
* here" — none of this type offered, or the constraints ahead pruned them all.
|
|
52
|
+
* **Belongs last in the chain.** A constraint sees the list as it stands at its own position, so only at the tail does
|
|
53
|
+
* an empty input mean "nothing playable here" — none of this type offered, or the constraints ahead pruned them all.
|
|
65
54
|
*
|
|
66
|
-
* These are the failures no per-rendition cause can report: causes come from
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* aren't knowable until one does; CODECS is, so an undecodable ladder isn't.
|
|
55
|
+
* These are the failures no per-rendition cause can report: causes come from `reportUnsupportedTrackConditions` as each
|
|
56
|
+
* media playlist resolves, and a rendition pruned before selection never resolves. Container and encryption aren't
|
|
57
|
+
* knowable until one does; CODECS is, so an undecodable ladder isn't.
|
|
70
58
|
*
|
|
71
|
-
* Idempotent because the constraint chain runs inside a `computed` that re-derives
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* from subscribing to the slot this writes.
|
|
59
|
+
* Idempotent because the constraint chain runs inside a `computed` that re-derives on every `presentation` write —
|
|
60
|
+
* segment appends and live reloads included — and the sequence deliberately keeps duplicates. `peek` is what keeps that
|
|
61
|
+
* computed from subscribing to the slot this writes.
|
|
75
62
|
*
|
|
76
63
|
* @example
|
|
77
|
-
*
|
|
78
|
-
*
|
|
64
|
+
* // engine-background-video.ts — video-only, so a source with none can't play
|
|
65
|
+
* constraints: [excludeUnplayableTracks, reportAbsentTrackType(SVTA_NO_SUPPORTED_VIDEO_TRACK)];
|
|
79
66
|
*/
|
|
80
67
|
function reportAbsentTrackType(code) {
|
|
81
68
|
return (tracks, { state }) => {
|
|
@@ -88,7 +75,7 @@ function reportAbsentTrackType(code) {
|
|
|
88
75
|
* Own `errors` for the resolved source's lifetime.
|
|
89
76
|
*
|
|
90
77
|
* @example
|
|
91
|
-
*
|
|
78
|
+
* const reactor = collectErrors.setup({ state });
|
|
92
79
|
*/
|
|
93
80
|
const collectErrors = defineBehavior({
|
|
94
81
|
stateKeys: ["presentation", "errors"],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collect-errors.js","names":[],"sources":["../../../../src/playback/behaviors/collect-errors.ts"],"sourcesContent":["/**\n * **Owns the engine's error sequence.** Reporters append through
|
|
1
|
+
{"version":3,"file":"collect-errors.js","names":[],"sources":["../../../../src/playback/behaviors/collect-errors.ts"],"sourcesContent":["/**\n * **Owns the engine's error sequence.** Reporters append through {@link emitError}; this behavior owns the slot and its\n * per-source lifecycle, clearing it on exit so a new source starts clean and the sequence can't grow unbounded across a\n * session.\n *\n * Same split as `setupFailoverMonitor` and `failedCdns`: writes come from wherever the condition is detected, one\n * behavior owns the slot. Deliberately has no `effects` — it holds no policy and derives nothing. Severity is decided\n * at the adapter, not here (see `internal/design/spf/features/errors.md`), which is why this is a lifecycle owner\n * rather than an error _handler_.\n *\n * Clearing binds to _exit_ of `presentation-resolved`, mirroring the sibling mixins' clear-on-teardown (`emptied` /\n * `MEDIA_DETACHED`). A live reload swaps the presentation object without leaving the resolved state, so it doesn't\n * clear — only an actual source change or destroy does. Known gap: a resolved→resolved source swap that never passes\n * through unresolved carries the prior source's errors forward; `resolve-track` guards the same transition with a\n * commit-time id check, and doing likewise here is a follow-up.\n *\n * The vocabulary itself ({@link SvtaError} and the codes) is DOM- and signal-free in `media/errors`; only the write\n * seam lives here, with the slot it writes.\n */\n\nimport { defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal, update } from '../../core/signals/primitives';\nimport type { SvtaError } from '../../media/errors';\nimport { isResolvedPresentation, type MaybeResolvedPresentation } from '../../media/types';\nimport type { SelectionRule } from '../primitives/selection-rules';\n\nexport interface CollectErrorsState {\n presentation?: MaybeResolvedPresentation;\n errors?: SvtaError[];\n}\n\n/**\n * State an error reporter writes into. The slot is _optional_: a behavior reports through this seam without declaring\n * ownership in its own typed slice, and emission no-ops when `collectErrors` isn't composed. Same contract as\n * `failedCdns` / `failoverFetch`.\n */\nexport interface ErrorEmitterState {\n errors?: Signal<SvtaError[] | undefined>;\n}\n\n/**\n * Append `error` to the engine's error sequence. No-op when no owner is composed. Replaces the array rather than\n * mutating it, so signal consumers notify; duplicates are kept, since a repeated condition is a real observation.\n * Writes go through `update` so concurrent reporters can't lose each other's appends.\n *\n * Every emission is logged, deliberately _before_ the owner check. A condition emitted with no `collectErrors` composed\n * is dropped on the floor — that's the case where a log is the only evidence it happened at all, so gating the log on\n * the same check would hide exactly what's worth seeing. Emissions that _are_ collected still get logged, because\n * reaching `state.errors` is no guarantee of reaching a person: only _verdicts_ are promoted to the media surface, so\n * every cause (and any non-fatal notice) is otherwise invisible outside a debugger.\n *\n * Ungated rather than `__DEV__`-only, matching the other reporting paths in this package (`resolve-presentation`,\n * `track-switching`, the segment actors).\n */\nexport function emitError(state: ErrorEmitterState, error: SvtaError): void {\n console.error('[spf] reported condition', error);\n\n if (!state.errors) return;\n\n update(state.errors, (errors) => [...(errors ?? []), error]);\n}\n\n/**\n * A \"constraint\" that reports a type the source carries **no** renditions of, for a composition that can't play without\n * it.\n *\n * Strange on purpose, and the strangeness is the point: it never constrains anything, always returning its input\n * untouched. It is shaped as a rule so a composition opts in by adding it to `constraints` — nothing to thread through\n * config, and no cost at all to a composition that leaves it out.\n *\n * **Belongs last in the chain.** A constraint sees the list as it stands at its own position, so only at the tail does\n * an empty input mean \"nothing playable here\" — none of this type offered, or the constraints ahead pruned them all.\n *\n * These are the failures no per-rendition cause can report: causes come from `reportUnsupportedTrackConditions` as each\n * media playlist resolves, and a rendition pruned before selection never resolves. Container and encryption aren't\n * knowable until one does; CODECS is, so an undecodable ladder isn't.\n *\n * Idempotent because the constraint chain runs inside a `computed` that re-derives on every `presentation` write —\n * segment appends and live reloads included — and the sequence deliberately keeps duplicates. `peek` is what keeps that\n * computed from subscribing to the slot this writes.\n *\n * @example\n * // engine-background-video.ts — video-only, so a source with none can't play\n * constraints: [excludeUnplayableTracks, reportAbsentTrackType(SVTA_NO_SUPPORTED_VIDEO_TRACK)];\n */\nexport function reportAbsentTrackType<T>(code: number): SelectionRule<T, ErrorEmitterState> {\n return (tracks, { state }) => {\n const reported = state.errors && peek(state.errors);\n\n if (!tracks.length && !reported?.some((error) => error.code === code)) {\n emitError(state, { code });\n }\n\n return tracks;\n };\n}\n\n/**\n * Own `errors` for the resolved source's lifetime.\n *\n * @example\n * const reactor = collectErrors.setup({ state });\n */\nexport const collectErrors = defineBehavior({\n stateKeys: ['presentation', 'errors'],\n contextKeys: [],\n setup: ({\n state,\n }: {\n state: {\n presentation: ReadonlySignal<CollectErrorsState['presentation']>;\n errors: Signal<CollectErrorsState['errors']>;\n };\n }) => {\n const derivedStateSignal = computed(() =>\n isResolvedPresentation(state.presentation.get())\n ? ('presentation-resolved' as const)\n : ('presentation-unresolved' as const)\n );\n\n return createMachineReactor({\n initial: 'presentation-unresolved',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'presentation-unresolved': {},\n 'presentation-resolved': {\n // Cleanup-binds-to-setup: reset for the next source on exit (src\n // unload + destroy).\n entry: () => () => state.errors.set(undefined),\n },\n },\n });\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,SAAgB,UAAU,OAA0B,OAAwB;CAC1E,QAAQ,MAAM,4BAA4B,KAAK;CAE/C,IAAI,CAAC,MAAM,QAAQ;CAEnB,OAAO,MAAM,SAAS,WAAW,CAAC,GAAI,UAAU,CAAC,GAAI,KAAK,CAAC;AAC7D;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,sBAAyB,MAAmD;CAC1F,QAAQ,QAAQ,EAAE,YAAY;EAC5B,MAAM,WAAW,MAAM,UAAU,KAAK,MAAM,MAAM;EAElD,IAAI,CAAC,OAAO,UAAU,CAAC,UAAU,MAAM,UAAU,MAAM,SAAS,IAAI,GAClE,UAAU,OAAO,EAAE,KAAK,CAAC;EAG3B,OAAO;CACT;AACF;;;;;;;AAQA,MAAa,gBAAgB,eAAe;CAC1C,WAAW,CAAC,gBAAgB,QAAQ;CACpC,aAAa,CAAC;CACd,QAAQ,EACN,YAMI;EACJ,MAAM,qBAAqB,eACzB,uBAAuB,MAAM,aAAa,IAAI,CAAC,IAC1C,0BACA,yBACP;EAEA,OAAO,qBAAqB;GAC1B,SAAS;GACT,eAAe,mBAAmB,IAAI;GACtC,QAAQ;IACN,2BAA2B,CAAC;IAC5B,yBAAyB,EAGvB,mBAAmB,MAAM,OAAO,IAAI,KAAA,CAAS,EAC/C;GACF;EACF,CAAC;CACH;AACF,CAAC"}
|
|
@@ -5,38 +5,31 @@ import { isResolvedPresentation } from "../../media/types/index.js";
|
|
|
5
5
|
import { getCdnId, getOrderedCdnIds } from "../../media/utils/cdn.js";
|
|
6
6
|
//#region src/playback/behaviors/derive-cdn-priority.ts
|
|
7
7
|
/**
|
|
8
|
-
* **Session-level CDN priority.** While a presentation is resolved, owns the
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* on src unload. The name mirrors HLS content steering's `PATHWAY-PRIORITY`.
|
|
8
|
+
* **Session-level CDN priority.** While a presentation is resolved, owns the `cdnPriority` signal: the distinct CDNs
|
|
9
|
+
* the source is served from (origin of each track's URL), in manifest priority order — most-preferred first. Cleared on
|
|
10
|
+
* src unload. The name mirrors HLS content steering's `PATHWAY-PRIORITY`.
|
|
12
11
|
*
|
|
13
|
-
* Redundant-stream sources (e.g. Mux's `?redundant_streams=true`) list the same
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* narrows each type's candidates to the first CDN with surviving tracks — so
|
|
18
|
-
* video / audio / text all resolve from one host (the shared list is the
|
|
12
|
+
* Redundant-stream sources (e.g. Mux's `?redundant_streams=true`) list the same content on multiple hosts, so the
|
|
13
|
+
* candidate tracks already include one variant per CDN. This behavior publishes _which_ CDNs exist and their priority;
|
|
14
|
+
* the `preferActiveCdn` scope rule in `track-switching` reads `cdnPriority` and narrows each type's candidates to the
|
|
15
|
+
* first CDN with surviving tracks — so video / audio / text all resolve from one host (the shared list is the
|
|
19
16
|
* per-presentation coherence guarantee).
|
|
20
17
|
*
|
|
21
|
-
* The "active" CDN is not stored — it's derived by the scope as the
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* the scope falls to the next CDN; when the primary recovers, it snaps back.
|
|
26
|
-
* Content steering, when it lands, reorders `cdnPriority` (pathway priority as a
|
|
27
|
-
* sort key).
|
|
18
|
+
* The "active" CDN is not stored — it's derived by the scope as the highest-priority entry in `cdnPriority` that still
|
|
19
|
+
* has tracks after the constraints pre-pass. That makes failover a pure consequence of the (future) failed-CDN
|
|
20
|
+
* constraint: when the primary's tracks are pruned during cooldown, the scope falls to the next CDN; when the primary
|
|
21
|
+
* recovers, it snaps back. Content steering, when it lands, reorders `cdnPriority` (pathway priority as a sort key).
|
|
28
22
|
*
|
|
29
|
-
* Lifecycle: `'presentation-unresolved'` ↔ `'presentation-resolved'`, mirroring
|
|
30
|
-
*
|
|
31
|
-
*
|
|
23
|
+
* Lifecycle: `'presentation-unresolved'` ↔ `'presentation-resolved'`, mirroring `setupTrackSwitching`. The resolved
|
|
24
|
+
* state owns the signal; its entry-returned cleanup clears it on exit (canonical cleanup-binds-to-setup per
|
|
25
|
+
* `reactors.md`).
|
|
32
26
|
*/
|
|
33
27
|
const samePriority = (a, b) => !!a && a.length === b.length && a.every((cdn, i) => cdn === b[i]);
|
|
34
28
|
/**
|
|
35
|
-
* Manage `cdnPriority`: publish the manifest-ordered CDN list on src load, clear
|
|
36
|
-
* on src unload.
|
|
29
|
+
* Manage `cdnPriority`: publish the manifest-ordered CDN list on src load, clear on src unload.
|
|
37
30
|
*
|
|
38
31
|
* @example
|
|
39
|
-
*
|
|
32
|
+
* const reactor = deriveCdnPriority.setup({ state });
|
|
40
33
|
*/
|
|
41
34
|
const deriveCdnPriority = defineBehavior({
|
|
42
35
|
stateKeys: ["presentation", "cdnPriority"],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"derive-cdn-priority.js","names":["getCdnId","defaultGetCdnId"],"sources":["../../../../src/playback/behaviors/derive-cdn-priority.ts"],"sourcesContent":["/**\n * **Session-level CDN priority.** While a presentation is resolved, owns the
|
|
1
|
+
{"version":3,"file":"derive-cdn-priority.js","names":["getCdnId","defaultGetCdnId"],"sources":["../../../../src/playback/behaviors/derive-cdn-priority.ts"],"sourcesContent":["/**\n * **Session-level CDN priority.** While a presentation is resolved, owns the `cdnPriority` signal: the distinct CDNs\n * the source is served from (origin of each track's URL), in manifest priority order — most-preferred first. Cleared on\n * src unload. The name mirrors HLS content steering's `PATHWAY-PRIORITY`.\n *\n * Redundant-stream sources (e.g. Mux's `?redundant_streams=true`) list the same content on multiple hosts, so the\n * candidate tracks already include one variant per CDN. This behavior publishes _which_ CDNs exist and their priority;\n * the `preferActiveCdn` scope rule in `track-switching` reads `cdnPriority` and narrows each type's candidates to the\n * first CDN with surviving tracks — so video / audio / text all resolve from one host (the shared list is the\n * per-presentation coherence guarantee).\n *\n * The \"active\" CDN is not stored — it's derived by the scope as the highest-priority entry in `cdnPriority` that still\n * has tracks after the constraints pre-pass. That makes failover a pure consequence of the (future) failed-CDN\n * constraint: when the primary's tracks are pruned during cooldown, the scope falls to the next CDN; when the primary\n * recovers, it snaps back. Content steering, when it lands, reorders `cdnPriority` (pathway priority as a sort key).\n *\n * Lifecycle: `'presentation-unresolved'` ↔ `'presentation-resolved'`, mirroring `setupTrackSwitching`. The resolved\n * state owns the signal; its entry-returned cleanup clears it on exit (canonical cleanup-binds-to-setup per\n * `reactors.md`).\n */\n\nimport { defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../core/signals/primitives';\nimport { isResolvedPresentation, type MaybeResolvedPresentation } from '../../media/types';\nimport { getCdnId as defaultGetCdnId, type GetCdnId, getOrderedCdnIds } from '../../media/utils/cdn';\n\nexport interface DeriveCdnPriorityState {\n presentation?: MaybeResolvedPresentation;\n cdnPriority?: string[];\n}\n\nconst samePriority = (a: string[] | undefined, b: string[]): boolean =>\n !!a && a.length === b.length && a.every((cdn, i) => cdn === b[i]);\n\n/**\n * Manage `cdnPriority`: publish the manifest-ordered CDN list on src load, clear on src unload.\n *\n * @example\n * const reactor = deriveCdnPriority.setup({ state });\n */\nexport const deriveCdnPriority = defineBehavior({\n stateKeys: ['presentation', 'cdnPriority'],\n contextKeys: [],\n setup: ({\n state,\n config = {},\n }: {\n state: {\n presentation: ReadonlySignal<DeriveCdnPriorityState['presentation']>;\n cdnPriority: Signal<DeriveCdnPriorityState['cdnPriority']>;\n };\n config?: { getCdnId?: GetCdnId };\n }) => {\n const getCdnId = config.getCdnId ?? defaultGetCdnId;\n const derivedStateSignal = computed(() =>\n isResolvedPresentation(state.presentation.get())\n ? ('presentation-resolved' as const)\n : ('presentation-unresolved' as const)\n );\n\n return createMachineReactor({\n initial: 'presentation-unresolved',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'presentation-unresolved': {},\n 'presentation-resolved': {\n // Cleanup-binds-to-setup: the CDN priority list is valid for exactly\n // 'presentation-resolved'. Clear fires on exit (src unload + destroy).\n entry: () => () => state.cdnPriority.set(undefined),\n effects: [\n () => {\n const presentation = state.presentation.get();\n if (!isResolvedPresentation(presentation)) return;\n\n const next = getOrderedCdnIds(presentation, getCdnId);\n\n // Skip the write when the CDN set is unchanged — a live reload swaps\n // in a new presentation object with the same hosts, and re-setting a\n // fresh array would re-fire the scope for an identical result.\n // `peek` reads without subscribing, so this never self-triggers.\n if (!samePriority(peek(state.cdnPriority), next)) state.cdnPriority.set(next);\n },\n ],\n },\n },\n });\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAM,gBAAgB,GAAyB,MAC7C,CAAC,CAAC,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,KAAK,MAAM,QAAQ,EAAE,EAAE;;;;;;;AAQlE,MAAa,oBAAoB,eAAe;CAC9C,WAAW,CAAC,gBAAgB,aAAa;CACzC,aAAa,CAAC;CACd,QAAQ,EACN,OACA,SAAS,CAAC,QAON;EACJ,MAAMA,aAAW,OAAO,YAAYC;EACpC,MAAM,qBAAqB,eACzB,uBAAuB,MAAM,aAAa,IAAI,CAAC,IAC1C,0BACA,yBACP;EAEA,OAAO,qBAAqB;GAC1B,SAAS;GACT,eAAe,mBAAmB,IAAI;GACtC,QAAQ;IACN,2BAA2B,CAAC;IAC5B,yBAAyB;KAGvB,mBAAmB,MAAM,YAAY,IAAI,KAAA,CAAS;KAClD,SAAS,OACD;MACJ,MAAM,eAAe,MAAM,aAAa,IAAI;MAC5C,IAAI,CAAC,uBAAuB,YAAY,GAAG;MAE3C,MAAM,OAAO,iBAAiB,cAAcD,UAAQ;MAMpD,IAAI,CAAC,aAAa,KAAK,MAAM,WAAW,GAAG,IAAI,GAAG,MAAM,YAAY,IAAI,IAAI;KAC9E,CACF;IACF;GACF;EACF,CAAC;CACH;AACF,CAAC"}
|
|
@@ -5,76 +5,53 @@ import { createMachineReactor } from "../../../core/reactors/create-machine-reac
|
|
|
5
5
|
import { isWebKitAirPlayCapable, listen } from "@videojs/utils/dom";
|
|
6
6
|
//#region src/playback/behaviors/dom/airplay.ts
|
|
7
7
|
/**
|
|
8
|
-
* **Bridge MSE playback to AirPlay on WebKit.**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* plays that native-HLS source on the receiver. The session state (WebKit's
|
|
14
|
-
* wireless flag, falling edge debounced — see `REMOTE_INACTIVE_SETTLE_MS`) is
|
|
15
|
-
* written straight to its policy consequences, declared here so the
|
|
16
|
-
* cause→policy mapping stays with the feature:
|
|
8
|
+
* **Bridge MSE playback to AirPlay on WebKit.** MSE streams can't be handed to an AirPlay receiver directly. The
|
|
9
|
+
* WebKit-recommended workaround is to append a fallback `<source type="application/x-mpegURL">` carrying the original
|
|
10
|
+
* manifest URL: Safari exposes the AirPlay picker and, when a wireless target is selected, plays that native-HLS source
|
|
11
|
+
* on the receiver. The session state (WebKit's wireless flag, falling edge debounced — see `REMOTE_INACTIVE_SETTLE_MS`)
|
|
12
|
+
* is written straight to its policy consequences, declared here so the cause→policy mapping stays with the feature:
|
|
17
13
|
*
|
|
18
|
-
* - `state.loadingSuspended` — held while the session is live. Observed by
|
|
19
|
-
* the `
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* pre-rebuild element — and starts it where the receiver left off. The
|
|
29
|
-
* playing state rides the same snapshot but stays behavior-local: this
|
|
30
|
-
* behavior itself calls `play()` once the command has been *consumed* — i.e.
|
|
31
|
-
* after the seek — when the receiver was playing at session end. The whole
|
|
32
|
-
* restore is bound to the presentation the session owned and retracted if
|
|
33
|
-
* that changes.
|
|
14
|
+
* - `state.loadingSuspended` — held while the session is live. Observed by the `loadXSegments` dispatchers (no fetching
|
|
15
|
+
* alongside the receiver) and by `setupMediaSource` (its post-close rebuild waits — attaching runs `element.load()`
|
|
16
|
+
* under the live receiver, which destroys a session still being established). The suspension this behavior holds
|
|
17
|
+
* doubles as its own session fact — same writer, same edges.
|
|
18
|
+
* - `state.startPosition` — one-shot command: the position is captured from the element at the session's settled end
|
|
19
|
+
* (still receiver-mirrored) and written once the rebuild's `load()` resets the element (its `'emptied'`), so
|
|
20
|
+
* `applyStartPosition` applies it to the rebuilt source — never to the pre-rebuild element — and starts it where the
|
|
21
|
+
* receiver left off. The playing state rides the same snapshot but stays behavior-local: this behavior itself calls
|
|
22
|
+
* `play()` once the command has been _consumed_ — i.e. after the seek — when the receiver was playing at session end.
|
|
23
|
+
* The whole restore is bound to the presentation the session owned and retracted if that changes.
|
|
34
24
|
*
|
|
35
|
-
* A source change during a live session releases the hold rather than deferring
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* effect below.
|
|
39
|
-
* https://webkit.org/blog/15036/how-to-use-media-source-extensions-with-airplay/
|
|
25
|
+
* A source change during a live session releases the hold rather than deferring until the session ends, so the rebuild
|
|
26
|
+
* runs and WebKit switches the receiver to the newly-built AirPlay alternate. Measured, not contracted — see the effect
|
|
27
|
+
* below. https://webkit.org/blog/15036/how-to-use-media-source-extensions-with-airplay/
|
|
40
28
|
*
|
|
41
|
-
* Single-positive-state reactor (`'preconditions-unmet'` ↔ `'airplay-capable'`):
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* detach, source reset, behavior destroy) removes the source and restores the
|
|
48
|
-
* element's `disableRemotePlayback` default. No-op on non-WebKit platforms
|
|
49
|
-
* (Chromium, Firefox) — `deriveState` never leaves `'preconditions-unmet'`.
|
|
29
|
+
* Single-positive-state reactor (`'preconditions-unmet'` ↔ `'airplay-capable'`): gated on a WebKit-AirPlay-capable
|
|
30
|
+
* media element being in scope. The entry — gated on `context.mediaSource` — appends the fallback `<source>` (kept
|
|
31
|
+
* current from `state.presentation`) and enables the AirPlay picker once the MediaSource is open, removing the source
|
|
32
|
+
* the moment the MediaSource detaches so it never survives an MSE teardown. State-exit cleanup (author opt-out, detach,
|
|
33
|
+
* source reset, behavior destroy) removes the source and restores the element's `disableRemotePlayback` default. No-op
|
|
34
|
+
* on non-WebKit platforms (Chromium, Firefox) — `deriveState` never leaves `'preconditions-unmet'`.
|
|
50
35
|
*
|
|
51
|
-
* MMS and AirPlay want
|
|
52
|
-
* element, so it is **sequenced**:
|
|
36
|
+
* MMS and AirPlay want _opposite_ values of `disableRemotePlayback` on the same element, so it is **sequenced**:
|
|
53
37
|
*
|
|
54
|
-
* - **MMS needs `true` to open.** `setupMediaSource` sets
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* the source opens would prevent `sourceopen`, so the flip is gated on
|
|
59
|
-
* `context.mediaSource` — which `setupMediaSource` publishes exactly once the
|
|
38
|
+
* - **MMS needs `true` to open.** `setupMediaSource` sets `disableRemotePlayback = true` when it attaches a
|
|
39
|
+
* ManagedMediaSource — Safari won't fire `sourceopen` (and MSE playback never starts) otherwise.
|
|
40
|
+
* - **AirPlay needs `false` to offer the picker.** Flipping to `false` _before_ the source opens would prevent
|
|
41
|
+
* `sourceopen`, so the flip is gated on `context.mediaSource` — which `setupMediaSource` publishes exactly once the
|
|
60
42
|
* MS is open. Re-fires per source (the slot clears + republishes on reset).
|
|
61
|
-
* - **Author opt-out wins.** `state.disableRemotePlayback` is the author's
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* it holds the machine in `'preconditions-unmet'` and nothing is set up.
|
|
43
|
+
* - **Author opt-out wins.** `state.disableRemotePlayback` is the author's intent, written only by the media adapter's
|
|
44
|
+
* IDL property; MMS/programmatic code touch the element's own `disableRemotePlayback` instead. A `true` there is
|
|
45
|
+
* unambiguously the author's choice to disable remote playback, so it holds the machine in `'preconditions-unmet'`
|
|
46
|
+
* and nothing is set up.
|
|
66
47
|
*/
|
|
67
48
|
/**
|
|
68
|
-
* How long WebKit's wireless flag must read
|
|
69
|
-
* session-driven `loadingSuspended` clears.
|
|
49
|
+
* How long WebKit's wireless flag must read _inactive_ before the session-driven `loadingSuspended` clears.
|
|
70
50
|
*
|
|
71
|
-
* Measured on Safari 26.4 (macOS): when an AirPlay session engages, Safari
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* `
|
|
75
|
-
* event. Trusting an instantaneous inactive reading would release
|
|
76
|
-
* `setupMediaSource`'s rebuild hold mid-handoff; its recovery `load()` then
|
|
77
|
-
* destroys the very session being established. Rising edges apply
|
|
51
|
+
* Measured on Safari 26.4 (macOS): when an AirPlay session engages, Safari closes the ManagedMediaSource and — while
|
|
52
|
+
* its pipeline switches to the native-HLS fallback source — transiently reports `webkitCurrentPlaybackTargetIsWireless
|
|
53
|
+
* === false`, firing the changed event. Trusting an instantaneous inactive reading would release `setupMediaSource`'s
|
|
54
|
+
* rebuild hold mid-handoff; its recovery `load()` then destroys the very session being established. Rising edges apply
|
|
78
55
|
* immediately; only the falling edge waits out this settle window.
|
|
79
56
|
*/
|
|
80
57
|
const REMOTE_INACTIVE_SETTLE_MS = 1e3;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"airplay.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/airplay.ts"],"sourcesContent":["/**\n * **Bridge MSE playback to AirPlay on WebKit.**\n * MSE streams can't be handed to an AirPlay receiver directly.\n * The WebKit-recommended workaround is to append a fallback\n * `<source type=\"application/x-mpegURL\">` carrying the original manifest URL:\n * Safari exposes the AirPlay picker and, when a wireless target is selected,\n * plays that native-HLS source on the receiver. The session state (WebKit's\n * wireless flag, falling edge debounced — see `REMOTE_INACTIVE_SETTLE_MS`) is\n * written straight to its policy consequences, declared here so the\n * cause→policy mapping stays with the feature:\n *\n * - `state.loadingSuspended` — held while the session is live. Observed by\n * the `loadXSegments` dispatchers (no fetching alongside the receiver) and\n * by `setupMediaSource` (its post-close rebuild waits — attaching runs\n * `element.load()` under the live receiver, which destroys a session still\n * being established). The suspension this behavior holds doubles as its own\n * session fact — same writer, same edges.\n * - `state.startPosition` — one-shot command: the position is captured from\n * the element at the session's settled end (still receiver-mirrored) and\n * written once the rebuild's `load()` resets the element (its `'emptied'`),\n * so `applyStartPosition` applies it to the rebuilt source — never to the\n * pre-rebuild element — and starts it where the receiver left off. The\n * playing state rides the same snapshot but stays behavior-local: this\n * behavior itself calls `play()` once the command has been *consumed* — i.e.\n * after the seek — when the receiver was playing at session end. The whole\n * restore is bound to the presentation the session owned and retracted if\n * that changes.\n *\n * A source change during a live session releases the hold rather than deferring\n * until the session ends, so the rebuild runs and WebKit switches the receiver\n * to the newly-built AirPlay alternate. Measured, not contracted — see the\n * effect below.\n * https://webkit.org/blog/15036/how-to-use-media-source-extensions-with-airplay/\n *\n * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'airplay-capable'`):\n * gated on a WebKit-AirPlay-capable media element being in scope. The entry —\n * gated on `context.mediaSource` — appends the fallback `<source>` (kept\n * current from `state.presentation`) and enables the AirPlay picker once the\n * MediaSource is open, removing the source the moment the MediaSource detaches\n * so it never survives an MSE teardown. State-exit cleanup (author opt-out,\n * detach, source reset, behavior destroy) removes the source and restores the\n * element's `disableRemotePlayback` default. No-op on non-WebKit platforms\n * (Chromium, Firefox) — `deriveState` never leaves `'preconditions-unmet'`.\n *\n * MMS and AirPlay want *opposite* values of `disableRemotePlayback` on the same\n * element, so it is **sequenced**:\n *\n * - **MMS needs `true` to open.** `setupMediaSource` sets\n * `disableRemotePlayback = true` when it attaches a ManagedMediaSource —\n * Safari won't fire `sourceopen` (and MSE playback never starts) otherwise.\n * - **AirPlay needs `false` to offer the picker.** Flipping to `false` *before*\n * the source opens would prevent `sourceopen`, so the flip is gated on\n * `context.mediaSource` — which `setupMediaSource` publishes exactly once the\n * MS is open. Re-fires per source (the slot clears + republishes on reset).\n * - **Author opt-out wins.** `state.disableRemotePlayback` is the author's\n * intent, written only by the media adapter's IDL property; MMS/programmatic\n * code touch the element's own `disableRemotePlayback` instead. A `true`\n * there is unambiguously the author's choice to disable remote playback, so\n * it holds the machine in `'preconditions-unmet'` and nothing is set up.\n */\n\nimport { isWebKitAirPlayCapable, listen, type WebKitVideoElement } from '@videojs/utils/dom';\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 { effect } from '../../../core/signals/effect';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\nimport type { StartPositionState } from './apply-start-position';\nimport type { SegmentLoadingState } from './load-segments';\n\n/**\n * How long WebKit's wireless flag must read *inactive* before the\n * session-driven `loadingSuspended` clears.\n *\n * Measured on Safari 26.4 (macOS): when an AirPlay session engages, Safari\n * closes the ManagedMediaSource and — while its pipeline switches to the\n * native-HLS fallback source — transiently reports\n * `webkitCurrentPlaybackTargetIsWireless === false`, firing the changed\n * event. Trusting an instantaneous inactive reading would release\n * `setupMediaSource`'s rebuild hold mid-handoff; its recovery `load()` then\n * destroys the very session being established. Rising edges apply\n * immediately; only the falling edge waits out this settle window.\n */\nconst REMOTE_INACTIVE_SETTLE_MS = 1000;\n\ntype AirPlayFsmState = 'preconditions-unmet' | 'airplay-capable';\n\nfunction deriveState(\n mediaElement: HTMLMediaElement | undefined,\n authorDisabledRemotePlayback: boolean | undefined\n): AirPlayFsmState {\n if (!mediaElement || !isWebKitAirPlayCapable(mediaElement)) return 'preconditions-unmet';\n if (authorDisabledRemotePlayback) return 'preconditions-unmet';\n return 'airplay-capable';\n}\n\nfunction setupAirPlaySetup({\n state,\n context,\n}: {\n state: {\n presentation: ReadonlySignal<MaybeResolvedPresentation | undefined>;\n disableRemotePlayback: ReadonlySignal<boolean | undefined>;\n loadingSuspended: Signal<SegmentLoadingState['loadingSuspended']>;\n startPosition: Signal<StartPositionState['startPosition']>;\n };\n context: {\n mediaElement: ReadonlySignal<HTMLMediaElement | undefined>;\n mediaSource: ReadonlySignal<MediaSource | undefined>;\n };\n}): Reactor<AirPlayFsmState | 'destroying' | 'destroyed'> {\n const derivedStateSignal = computed(() => deriveState(context.mediaElement.get(), state.disableRemotePlayback.get()));\n\n return createMachineReactor<AirPlayFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'airplay-capable': {\n entry: () => {\n const mediaElement = context.mediaElement.get() as WebKitVideoElement;\n\n // WebKit's wireless flag is the only session signal. The standard\n // Remote Playback API's `remote.state` is deliberately not consulted:\n // it doesn't track AirPlay sessions reliably on WebKit (same reason\n // `remotePlaybackFeature` in @videojs/core skips it), and a stale\n // `'connected'` would pin `loadingSuspended` on — stranding the\n // rebuild in a way the settle window below can't recover from.\n const isSessionActive = () => !!mediaElement.webkitCurrentPlaybackTargetIsWireless;\n\n let settleTimer: ReturnType<typeof setTimeout> | undefined;\n // Session-end restore. The position/playing snapshot is taken at the\n // settled falling edge (the element still mirrors the receiver) but\n // acted on only at the element's next `'emptied'` — once the\n // rebuild's `load()` has reset the element. Writing it earlier would\n // hand `applyStartPosition` a command against the pre-rebuild element\n // (readyState still >= HAVE_METADATA), which it would apply and\n // consume before the rebuilt source exists.\n //\n // The restore is bound to the presentation the session owned, and\n // that identity is re-checked at *every* async hop — settle timer,\n // `'emptied'`, and the outstanding-command effect below. Checking at\n // only one hop leaves the others open: a source change inside the\n // settle window, or between `'emptied'` and the element becoming\n // seekable, would otherwise strand the old session's position (and an\n // unrequested `play()`) on the new source.\n let sessionPresentationUrl: string | undefined;\n let pendingRestore:\n | { position: number; wasPlaying: boolean; presentationUrl: string | undefined }\n | undefined;\n /** Presentation an already-written `state.startPosition` belongs to. */\n let restoreOwnerUrl: string | undefined;\n let resumeWhenRestored = false;\n\n const sync = () => {\n if (isSessionActive()) {\n clearTimeout(settleTimer);\n settleTimer = undefined;\n // Rising edge — the presentation the receiver is taking over.\n if (!peek(state.loadingSuspended)) sessionPresentationUrl = peek(state.presentation)?.url;\n state.loadingSuspended.set(true);\n } else if (peek(state.loadingSuspended)) {\n // Falling edge: don't trust an instantaneous inactive reading —\n // re-check once the flag has settled.\n settleTimer ??= setTimeout(() => {\n settleTimer = undefined;\n const stillActive = isSessionActive();\n if (!stillActive) {\n const ownerUrl = sessionPresentationUrl;\n sessionPresentationUrl = undefined;\n // Snapshot only while the session's own presentation is still\n // current. `currentTime` here belongs to that presentation —\n // during a session `setupMediaSource` has already torn down,\n // so a source change inside this window resets neither the\n // element nor its position, and pairing that position with the\n // incoming URL would smuggle it onto the new source.\n if (peek(state.presentation)?.url === ownerUrl) {\n pendingRestore = {\n position: mediaElement.currentTime,\n wasPlaying: !mediaElement.paused,\n presentationUrl: ownerUrl,\n };\n }\n }\n state.loadingSuspended.set(stillActive);\n }, REMOTE_INACTIVE_SETTLE_MS);\n } else {\n state.loadingSuspended.set(false);\n }\n };\n const listenerCleanup = new AbortController();\n listen(mediaElement, 'webkitcurrentplaybacktargetiswirelesschanged', sync, {\n signal: listenerCleanup.signal,\n });\n listen(\n mediaElement,\n 'emptied',\n () => {\n if (!pendingRestore) return;\n const { position, wasPlaying, presentationUrl } = pendingRestore;\n pendingRestore = undefined;\n if (peek(state.presentation)?.url !== presentationUrl) return;\n state.startPosition.set(position);\n restoreOwnerUrl = presentationUrl;\n resumeWhenRestored = wasPlaying;\n },\n { signal: listenerCleanup.signal }\n );\n\n // A source change during a live session releases the hold, letting the\n // rebuild run instead of waiting for the session to end.\n //\n // Doing nothing is not a neutral choice — it silently defers the\n // change. `<source>` children are only consulted while resource\n // selection runs, so the effect below rewriting the fallback's `src`\n // does nothing to what the receiver is already playing, and the\n // rebuild is held by `loadingSuspended`, so the receiver stays on the\n // outgoing stream until the user disengages.\n //\n // Releasing the hold lets the rebuild proceed, and the receiver ends\n // up on the new stream — measured on Safari 26.4. Note where the\n // handover actually happens, because it is not the rebuild's `load()`:\n // clearing the hold also makes the source effect below see an inactive\n // session with no `context.mediaSource`, so it drops the fallback, and\n // the rebuild's resource selection runs with the MSE source alone. The\n // switch comes afterwards, when that effect re-creates the fallback\n // against the new presentation once the MediaSource opens and WebKit\n // moves the live session onto the newly-appeared alternate.\n //\n // That leaves a window where an active session has no compatible\n // alternate on the element, and it survives it. Holding the fallback\n // across the rebuild instead would close the window and make the\n // handover selection-driven, but it swaps a measured path for an\n // argued one — don't without a device pass.\n //\n // Measured, not contracted: this is under-specified territory, so\n // nothing here depends on the handover succeeding. If a UA drops the\n // session instead, the falling edge runs its ordinary course; the\n // restore is suppressed either way because the snapshot is bound to\n // the presentation the session owned.\n //\n // Note this is *not* an attempt to end the session. Setting\n // `disableRemotePlayback = true` was tried: the Remote Playback API\n // requires it to disconnect (spec §5.3.2) but WebKit does not honor\n // that for AirPlay — the session survived, measured on Safari 26.4.\n // Deliberately not written here, so that behavior can't silently flip\n // to \"receiver drops\" if WebKit ever implements the disconnect.\n const disposeSourceChangeEnd = effect(() => {\n const url = state.presentation.get()?.url;\n // `loadingSuspended` is this behavior's own session fact; peeked so\n // clearing it below doesn't re-trigger.\n if (!peek(state.loadingSuspended) || url === sessionPresentationUrl) return;\n sessionPresentationUrl = undefined;\n clearTimeout(settleTimer);\n settleTimer = undefined;\n state.loadingSuspended.set(false);\n });\n\n // Watches an outstanding restore command to its end. Two outcomes:\n //\n // - **Consumed** (`startPosition` back to `undefined`): the element is\n // at the restored position, so resuming is safe. Keying the resume\n // off consumption rather than off `'loadedmetadata'` orders it after\n // `applyStartPosition`'s seek — arming a listener for the same event\n // would put `play()` ahead of the seek, since this behavior registers\n // synchronously while `applyStartPosition` only reaches its state a\n // microtask later. The rebuild's load algorithm forced `paused =\n // true`; a rejection here (autoplay policy) degrades to\n // paused-at-position. Requires a `startPosition` consumer in the\n // composition (`applyStartPosition`) — without one the position is\n // never restored either, so resuming would be wrong anyway.\n // - **Superseded** (presentation changed): retract the command and\n // disarm, so neither the seek nor the resume reaches the new source.\n const disposeRestoreWatch = effect(() => {\n const url = state.presentation.get()?.url;\n const position = state.startPosition.get();\n if (!restoreOwnerUrl) return;\n\n if (url !== restoreOwnerUrl) {\n restoreOwnerUrl = undefined;\n resumeWhenRestored = false;\n if (position !== undefined) state.startPosition.set(undefined);\n return;\n }\n\n if (position !== undefined) return;\n restoreOwnerUrl = undefined;\n if (!resumeWhenRestored) return;\n resumeWhenRestored = false;\n mediaElement.play().catch((err) => {\n console.warn('[setupAirPlay] session-end resume play() rejected — staying paused:', err);\n });\n });\n\n // This effect combines:\n // - adding a native HLS fallback source when the mediaSource is\n // attached / removing it when it's detached — UNLESS a session is\n // live: during an AirPlay session the engine detaches its dead\n // MediaSource (see setupMediaSource's sourceclose recovery) while\n // the receiver is playing exactly this fallback source, so it\n // must survive until the session's falling edge (the rebuild's\n // republish then adopts it again).\n // - keeping this sourceEl's src in sync with the current presentation.\n // The created source is also cleaned on state exit.\n // The dependence on context.mediaSource means the source is appended\n // only after the MMS has been attached and opened (therefore we can\n // flip disableRemotePlayback). Being in this state also implies\n // that the author has not disabled remote playback.\n let sourceEl: HTMLSourceElement | null = null;\n const disposeSource = effect(() => {\n const hasMediaSource = !!context.mediaSource.get();\n const sessionActive = !!state.loadingSuspended.get();\n const url = state.presentation.get()?.url ?? '';\n\n if (!hasMediaSource && !sessionActive) {\n sourceEl?.remove();\n sourceEl = null;\n } else if (hasMediaSource && (!sourceEl || sourceEl.parentNode !== mediaElement)) {\n sourceEl = document.createElement('source');\n sourceEl.type = 'application/x-mpegURL';\n mediaElement.append(sourceEl);\n mediaElement.disableRemotePlayback = false;\n }\n if (sourceEl) sourceEl.src = url;\n });\n\n // AirPlay may already be active at (re)attach.\n sync();\n\n return () => {\n disposeSource();\n disposeSourceChangeEnd();\n disposeRestoreWatch();\n listenerCleanup.abort();\n clearTimeout(settleTimer);\n sourceEl?.remove();\n sourceEl = null;\n // Undo the picker enable: hand the element back to its MMS-default\n // `disableRemotePlayback = true`.\n mediaElement.disableRemotePlayback = true;\n // Don't strand the engine held/suspended if we tear down\n // mid-session (author opt-out, detach, destroy).\n state.loadingSuspended.set(false);\n // Nor leave a restore command behind for the next source to apply.\n if (restoreOwnerUrl) {\n restoreOwnerUrl = undefined;\n resumeWhenRestored = false;\n if (peek(state.startPosition) !== undefined) state.startPosition.set(undefined);\n }\n };\n },\n },\n },\n });\n}\n\nexport const setupAirPlay = defineBehavior({\n stateKeys: ['presentation', 'disableRemotePlayback', 'loadingSuspended', 'startPosition'],\n contextKeys: ['mediaElement', 'mediaSource'],\n setup: setupAirPlaySetup,\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"airplay.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/airplay.ts"],"sourcesContent":["/**\n * **Bridge MSE playback to AirPlay on WebKit.** MSE streams can't be handed to an AirPlay receiver directly. The\n * WebKit-recommended workaround is to append a fallback `<source type=\"application/x-mpegURL\">` carrying the original\n * manifest URL: Safari exposes the AirPlay picker and, when a wireless target is selected, plays that native-HLS source\n * on the receiver. The session state (WebKit's wireless flag, falling edge debounced — see `REMOTE_INACTIVE_SETTLE_MS`)\n * is written straight to its policy consequences, declared here so the cause→policy mapping stays with the feature:\n *\n * - `state.loadingSuspended` — held while the session is live. Observed by the `loadXSegments` dispatchers (no fetching\n * alongside the receiver) and by `setupMediaSource` (its post-close rebuild waits — attaching runs `element.load()`\n * under the live receiver, which destroys a session still being established). The suspension this behavior holds\n * doubles as its own session fact — same writer, same edges.\n * - `state.startPosition` — one-shot command: the position is captured from the element at the session's settled end\n * (still receiver-mirrored) and written once the rebuild's `load()` resets the element (its `'emptied'`), so\n * `applyStartPosition` applies it to the rebuilt source — never to the pre-rebuild element — and starts it where the\n * receiver left off. The playing state rides the same snapshot but stays behavior-local: this behavior itself calls\n * `play()` once the command has been _consumed_ — i.e. after the seek — when the receiver was playing at session end.\n * The whole restore is bound to the presentation the session owned and retracted if that changes.\n *\n * A source change during a live session releases the hold rather than deferring until the session ends, so the rebuild\n * runs and WebKit switches the receiver to the newly-built AirPlay alternate. Measured, not contracted — see the effect\n * below. https://webkit.org/blog/15036/how-to-use-media-source-extensions-with-airplay/\n *\n * Single-positive-state reactor (`'preconditions-unmet'` ↔ `'airplay-capable'`): gated on a WebKit-AirPlay-capable\n * media element being in scope. The entry — gated on `context.mediaSource` — appends the fallback `<source>` (kept\n * current from `state.presentation`) and enables the AirPlay picker once the MediaSource is open, removing the source\n * the moment the MediaSource detaches so it never survives an MSE teardown. State-exit cleanup (author opt-out, detach,\n * source reset, behavior destroy) removes the source and restores the element's `disableRemotePlayback` default. No-op\n * on non-WebKit platforms (Chromium, Firefox) — `deriveState` never leaves `'preconditions-unmet'`.\n *\n * MMS and AirPlay want _opposite_ values of `disableRemotePlayback` on the same element, so it is **sequenced**:\n *\n * - **MMS needs `true` to open.** `setupMediaSource` sets `disableRemotePlayback = true` when it attaches a\n * ManagedMediaSource — Safari won't fire `sourceopen` (and MSE playback never starts) otherwise.\n * - **AirPlay needs `false` to offer the picker.** Flipping to `false` _before_ the source opens would prevent\n * `sourceopen`, so the flip is gated on `context.mediaSource` — which `setupMediaSource` publishes exactly once the\n * MS is open. Re-fires per source (the slot clears + republishes on reset).\n * - **Author opt-out wins.** `state.disableRemotePlayback` is the author's intent, written only by the media adapter's\n * IDL property; MMS/programmatic code touch the element's own `disableRemotePlayback` instead. A `true` there is\n * unambiguously the author's choice to disable remote playback, so it holds the machine in `'preconditions-unmet'`\n * and nothing is set up.\n */\n\nimport { isWebKitAirPlayCapable, listen, type WebKitVideoElement } from '@videojs/utils/dom';\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 { effect } from '../../../core/signals/effect';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../../core/signals/primitives';\nimport type { MaybeResolvedPresentation } from '../../../media/types';\nimport type { StartPositionState } from './apply-start-position';\nimport type { SegmentLoadingState } from './load-segments';\n\n/**\n * How long WebKit's wireless flag must read _inactive_ before the session-driven `loadingSuspended` clears.\n *\n * Measured on Safari 26.4 (macOS): when an AirPlay session engages, Safari closes the ManagedMediaSource and — while\n * its pipeline switches to the native-HLS fallback source — transiently reports `webkitCurrentPlaybackTargetIsWireless\n * === false`, firing the changed event. Trusting an instantaneous inactive reading would release `setupMediaSource`'s\n * rebuild hold mid-handoff; its recovery `load()` then destroys the very session being established. Rising edges apply\n * immediately; only the falling edge waits out this settle window.\n */\nconst REMOTE_INACTIVE_SETTLE_MS = 1000;\n\ntype AirPlayFsmState = 'preconditions-unmet' | 'airplay-capable';\n\nfunction deriveState(\n mediaElement: HTMLMediaElement | undefined,\n authorDisabledRemotePlayback: boolean | undefined\n): AirPlayFsmState {\n if (!mediaElement || !isWebKitAirPlayCapable(mediaElement)) return 'preconditions-unmet';\n\n if (authorDisabledRemotePlayback) return 'preconditions-unmet';\n\n return 'airplay-capable';\n}\n\nfunction setupAirPlaySetup({\n state,\n context,\n}: {\n state: {\n presentation: ReadonlySignal<MaybeResolvedPresentation | undefined>;\n disableRemotePlayback: ReadonlySignal<boolean | undefined>;\n loadingSuspended: Signal<SegmentLoadingState['loadingSuspended']>;\n startPosition: Signal<StartPositionState['startPosition']>;\n };\n context: {\n mediaElement: ReadonlySignal<HTMLMediaElement | undefined>;\n mediaSource: ReadonlySignal<MediaSource | undefined>;\n };\n}): Reactor<AirPlayFsmState | 'destroying' | 'destroyed'> {\n const derivedStateSignal = computed(() => deriveState(context.mediaElement.get(), state.disableRemotePlayback.get()));\n\n return createMachineReactor<AirPlayFsmState>({\n initial: 'preconditions-unmet',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'preconditions-unmet': {},\n\n 'airplay-capable': {\n entry: () => {\n const mediaElement = context.mediaElement.get() as WebKitVideoElement;\n\n // WebKit's wireless flag is the only session signal. The standard\n // Remote Playback API's `remote.state` is deliberately not consulted:\n // it doesn't track AirPlay sessions reliably on WebKit (same reason\n // `remotePlaybackFeature` in @videojs/core skips it), and a stale\n // `'connected'` would pin `loadingSuspended` on — stranding the\n // rebuild in a way the settle window below can't recover from.\n const isSessionActive = () => !!mediaElement.webkitCurrentPlaybackTargetIsWireless;\n\n let settleTimer: ReturnType<typeof setTimeout> | undefined;\n // Session-end restore. The position/playing snapshot is taken at the\n // settled falling edge (the element still mirrors the receiver) but\n // acted on only at the element's next `'emptied'` — once the\n // rebuild's `load()` has reset the element. Writing it earlier would\n // hand `applyStartPosition` a command against the pre-rebuild element\n // (readyState still >= HAVE_METADATA), which it would apply and\n // consume before the rebuilt source exists.\n //\n // The restore is bound to the presentation the session owned, and\n // that identity is re-checked at *every* async hop — settle timer,\n // `'emptied'`, and the outstanding-command effect below. Checking at\n // only one hop leaves the others open: a source change inside the\n // settle window, or between `'emptied'` and the element becoming\n // seekable, would otherwise strand the old session's position (and an\n // unrequested `play()`) on the new source.\n let sessionPresentationUrl: string | undefined;\n let pendingRestore:\n | { position: number; wasPlaying: boolean; presentationUrl: string | undefined }\n | undefined;\n /** Presentation an already-written `state.startPosition` belongs to. */\n let restoreOwnerUrl: string | undefined;\n let resumeWhenRestored = false;\n\n const sync = () => {\n if (isSessionActive()) {\n clearTimeout(settleTimer);\n settleTimer = undefined;\n\n // Rising edge — the presentation the receiver is taking over.\n if (!peek(state.loadingSuspended)) sessionPresentationUrl = peek(state.presentation)?.url;\n\n state.loadingSuspended.set(true);\n } else if (peek(state.loadingSuspended)) {\n // Falling edge: don't trust an instantaneous inactive reading —\n // re-check once the flag has settled.\n settleTimer ??= setTimeout(() => {\n settleTimer = undefined;\n const stillActive = isSessionActive();\n\n if (!stillActive) {\n const ownerUrl = sessionPresentationUrl;\n\n sessionPresentationUrl = undefined;\n\n // Snapshot only while the session's own presentation is still\n // current. `currentTime` here belongs to that presentation —\n // during a session `setupMediaSource` has already torn down,\n // so a source change inside this window resets neither the\n // element nor its position, and pairing that position with the\n // incoming URL would smuggle it onto the new source.\n if (peek(state.presentation)?.url === ownerUrl) {\n pendingRestore = {\n position: mediaElement.currentTime,\n wasPlaying: !mediaElement.paused,\n presentationUrl: ownerUrl,\n };\n }\n }\n\n state.loadingSuspended.set(stillActive);\n }, REMOTE_INACTIVE_SETTLE_MS);\n } else {\n state.loadingSuspended.set(false);\n }\n };\n const listenerCleanup = new AbortController();\n\n listen(mediaElement, 'webkitcurrentplaybacktargetiswirelesschanged', sync, {\n signal: listenerCleanup.signal,\n });\n listen(\n mediaElement,\n 'emptied',\n () => {\n if (!pendingRestore) return;\n\n const { position, wasPlaying, presentationUrl } = pendingRestore;\n\n pendingRestore = undefined;\n\n if (peek(state.presentation)?.url !== presentationUrl) return;\n\n state.startPosition.set(position);\n restoreOwnerUrl = presentationUrl;\n resumeWhenRestored = wasPlaying;\n },\n { signal: listenerCleanup.signal }\n );\n\n // A source change during a live session releases the hold, letting the\n // rebuild run instead of waiting for the session to end.\n //\n // Doing nothing is not a neutral choice — it silently defers the\n // change. `<source>` children are only consulted while resource\n // selection runs, so the effect below rewriting the fallback's `src`\n // does nothing to what the receiver is already playing, and the\n // rebuild is held by `loadingSuspended`, so the receiver stays on the\n // outgoing stream until the user disengages.\n //\n // Releasing the hold lets the rebuild proceed, and the receiver ends\n // up on the new stream — measured on Safari 26.4. Note where the\n // handover actually happens, because it is not the rebuild's `load()`:\n // clearing the hold also makes the source effect below see an inactive\n // session with no `context.mediaSource`, so it drops the fallback, and\n // the rebuild's resource selection runs with the MSE source alone. The\n // switch comes afterwards, when that effect re-creates the fallback\n // against the new presentation once the MediaSource opens and WebKit\n // moves the live session onto the newly-appeared alternate.\n //\n // That leaves a window where an active session has no compatible\n // alternate on the element, and it survives it. Holding the fallback\n // across the rebuild instead would close the window and make the\n // handover selection-driven, but it swaps a measured path for an\n // argued one — don't without a device pass.\n //\n // Measured, not contracted: this is under-specified territory, so\n // nothing here depends on the handover succeeding. If a UA drops the\n // session instead, the falling edge runs its ordinary course; the\n // restore is suppressed either way because the snapshot is bound to\n // the presentation the session owned.\n //\n // Note this is *not* an attempt to end the session. Setting\n // `disableRemotePlayback = true` was tried: the Remote Playback API\n // requires it to disconnect (spec §5.3.2) but WebKit does not honor\n // that for AirPlay — the session survived, measured on Safari 26.4.\n // Deliberately not written here, so that behavior can't silently flip\n // to \"receiver drops\" if WebKit ever implements the disconnect.\n const disposeSourceChangeEnd = effect(() => {\n const url = state.presentation.get()?.url;\n // `loadingSuspended` is this behavior's own session fact; peeked so\n // clearing it below doesn't re-trigger.\n if (!peek(state.loadingSuspended) || url === sessionPresentationUrl) return;\n\n sessionPresentationUrl = undefined;\n clearTimeout(settleTimer);\n settleTimer = undefined;\n state.loadingSuspended.set(false);\n });\n\n // Watches an outstanding restore command to its end. Two outcomes:\n //\n // - **Consumed** (`startPosition` back to `undefined`): the element is\n // at the restored position, so resuming is safe. Keying the resume\n // off consumption rather than off `'loadedmetadata'` orders it after\n // `applyStartPosition`'s seek — arming a listener for the same event\n // would put `play()` ahead of the seek, since this behavior registers\n // synchronously while `applyStartPosition` only reaches its state a\n // microtask later. The rebuild's load algorithm forced `paused =\n // true`; a rejection here (autoplay policy) degrades to\n // paused-at-position. Requires a `startPosition` consumer in the\n // composition (`applyStartPosition`) — without one the position is\n // never restored either, so resuming would be wrong anyway.\n // - **Superseded** (presentation changed): retract the command and\n // disarm, so neither the seek nor the resume reaches the new source.\n const disposeRestoreWatch = effect(() => {\n const url = state.presentation.get()?.url;\n const position = state.startPosition.get();\n\n if (!restoreOwnerUrl) return;\n\n if (url !== restoreOwnerUrl) {\n restoreOwnerUrl = undefined;\n resumeWhenRestored = false;\n\n if (position !== undefined) state.startPosition.set(undefined);\n\n return;\n }\n\n if (position !== undefined) return;\n\n restoreOwnerUrl = undefined;\n\n if (!resumeWhenRestored) return;\n\n resumeWhenRestored = false;\n mediaElement.play().catch((err) => {\n console.warn('[setupAirPlay] session-end resume play() rejected — staying paused:', err);\n });\n });\n\n // This effect combines:\n // - adding a native HLS fallback source when the mediaSource is\n // attached / removing it when it's detached — UNLESS a session is\n // live: during an AirPlay session the engine detaches its dead\n // MediaSource (see setupMediaSource's sourceclose recovery) while\n // the receiver is playing exactly this fallback source, so it\n // must survive until the session's falling edge (the rebuild's\n // republish then adopts it again).\n // - keeping this sourceEl's src in sync with the current presentation.\n // The created source is also cleaned on state exit.\n // The dependence on context.mediaSource means the source is appended\n // only after the MMS has been attached and opened (therefore we can\n // flip disableRemotePlayback). Being in this state also implies\n // that the author has not disabled remote playback.\n let sourceEl: HTMLSourceElement | null = null;\n const disposeSource = effect(() => {\n const hasMediaSource = !!context.mediaSource.get();\n const sessionActive = !!state.loadingSuspended.get();\n const url = state.presentation.get()?.url ?? '';\n\n if (!hasMediaSource && !sessionActive) {\n sourceEl?.remove();\n sourceEl = null;\n } else if (hasMediaSource && (!sourceEl || sourceEl.parentNode !== mediaElement)) {\n sourceEl = document.createElement('source');\n sourceEl.type = 'application/x-mpegURL';\n mediaElement.append(sourceEl);\n mediaElement.disableRemotePlayback = false;\n }\n\n if (sourceEl) sourceEl.src = url;\n });\n\n // AirPlay may already be active at (re)attach.\n sync();\n\n return () => {\n disposeSource();\n disposeSourceChangeEnd();\n disposeRestoreWatch();\n listenerCleanup.abort();\n clearTimeout(settleTimer);\n sourceEl?.remove();\n sourceEl = null;\n // Undo the picker enable: hand the element back to its MMS-default\n // `disableRemotePlayback = true`.\n mediaElement.disableRemotePlayback = true;\n // Don't strand the engine held/suspended if we tear down\n // mid-session (author opt-out, detach, destroy).\n state.loadingSuspended.set(false);\n\n // Nor leave a restore command behind for the next source to apply.\n if (restoreOwnerUrl) {\n restoreOwnerUrl = undefined;\n resumeWhenRestored = false;\n\n if (peek(state.startPosition) !== undefined) state.startPosition.set(undefined);\n }\n };\n },\n },\n },\n });\n}\n\nexport const setupAirPlay = defineBehavior({\n stateKeys: ['presentation', 'disableRemotePlayback', 'loadingSuspended', 'startPosition'],\n contextKeys: ['mediaElement', 'mediaSource'],\n setup: setupAirPlaySetup,\n});\n"],"mappings":"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"}
|
|
@@ -6,48 +6,36 @@ import { listen } from "@videojs/utils/dom";
|
|
|
6
6
|
import { isUndefined } from "@videojs/utils/predicate";
|
|
7
7
|
//#region src/playback/behaviors/dom/apply-start-position.ts
|
|
8
8
|
/**
|
|
9
|
-
* **Start playback of a source at a requested position.** `state.startPosition`
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* this behavior is its sole consumer and clears it after applying, so a
|
|
15
|
-
* stale position can never replay against a later source or rebuild.
|
|
9
|
+
* **Start playback of a source at a requested position.** `state.startPosition` is a one-shot command — "when the
|
|
10
|
+
* current source can seek, start there" — the SPF analogue of hls.js's `startPosition` (and the primitive
|
|
11
|
+
* `EXT-X-START`, resume-where-you-left-off, and MediaSource-recovery restore build on). Consumers (adapters,
|
|
12
|
+
* `setupAirPlay`'s session-end snapshot) write it; this behavior is its sole consumer and clears it after applying, so
|
|
13
|
+
* a stale position can never replay against a later source or rebuild.
|
|
16
14
|
*
|
|
17
|
-
* Single-positive-state reactor (`'preconditions-unmet'` ↔ `'position-pending'`):
|
|
18
|
-
*
|
|
19
|
-
* The entry applies the command in two steps:
|
|
15
|
+
* Single-positive-state reactor (`'preconditions-unmet'` ↔ `'position-pending'`): gated on `mediaElement + resolved
|
|
16
|
+
* presentation + startPosition` defined. The entry applies the command in two steps:
|
|
20
17
|
*
|
|
21
|
-
* 1. **Seed `state.currentTime` immediately.** The segment loaders anchor
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* `
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* the
|
|
29
|
-
* 2. **Seek the element at metadata.** `element.currentTime = position` once
|
|
30
|
-
* `readyState >= HAVE_METADATA` (immediately if already there, else on
|
|
31
|
-
* `loadedmetadata`), then clear `startPosition` (consume). The element
|
|
32
|
-
* clamps the seek to its seekable range per spec, and the resulting
|
|
33
|
-
* `seeking` event flows back through `trackCurrentTime` — from here the
|
|
18
|
+
* 1. **Seed `state.currentTime` immediately.** The segment loaders anchor their load window on `state.currentTime`;
|
|
19
|
+
* seeding points the _first_ fetches at the requested position instead of 0. Multi-writer with `trackCurrentTime`
|
|
20
|
+
* (ongoing DOM mirror) — legitimate: different decision domains (element-derived mirror vs one-shot command), and
|
|
21
|
+
* before HAVE_METADATA no `timeupdate`/`seeking` fires to overwrite the seed. Compose this behavior _after_
|
|
22
|
+
* `trackCurrentTime` so the seed lands after the mirror's attach-time sync.
|
|
23
|
+
* 2. **Seek the element at metadata.** `element.currentTime = position` once `readyState >= HAVE_METADATA` (immediately if
|
|
24
|
+
* already there, else on `loadedmetadata`), then clear `startPosition` (consume). The element clamps the seek to its
|
|
25
|
+
* seekable range per spec, and the resulting `seeking` event flows back through `trackCurrentTime` — from here the
|
|
34
26
|
* ordinary seek path owns the position.
|
|
35
27
|
*
|
|
36
|
-
* Position only — playing/paused is deliberately out of scope. The media
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* resume intent belongs to whoever commands the start (e.g. `setupAirPlay`
|
|
40
|
-
* restores its session-end playing state itself).
|
|
28
|
+
* Position only — playing/paused is deliberately out of scope. The media element load algorithm forces `paused = true`,
|
|
29
|
+
* so a source that was playing before a rebuild comes back paused at the restored position; resume intent belongs to
|
|
30
|
+
* whoever commands the start (e.g. `setupAirPlay` restores its session-end playing state itself).
|
|
41
31
|
*
|
|
42
|
-
* Deliberately NOT relying on the pre-metadata "default playback start
|
|
43
|
-
*
|
|
44
|
-
* behavior there is inconsistent; the explicit `loadedmetadata` sequencing is
|
|
32
|
+
* Deliberately NOT relying on the pre-metadata "default playback start position" write (setting `currentTime` at
|
|
33
|
+
* HAVE_NOTHING): cross-browser MSE behavior there is inconsistent; the explicit `loadedmetadata` sequencing is
|
|
45
34
|
* deterministic everywhere.
|
|
46
35
|
*
|
|
47
|
-
* State-exit cleanup (source reset, element detach, destroy) drops the
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* routes through unresolved — but is consumed the moment it applies.
|
|
36
|
+
* State-exit cleanup (source reset, element detach, destroy) drops the pending `loadedmetadata` listener. An
|
|
37
|
+
* _unapplied_ command survives a source reset — "start the source I'm loading at P" holds while the presentation routes
|
|
38
|
+
* through unresolved — but is consumed the moment it applies.
|
|
51
39
|
*/
|
|
52
40
|
function deriveState(presentation, mediaElement, startPosition) {
|
|
53
41
|
if (!mediaElement || !isResolvedPresentation(presentation)) return "preconditions-unmet";
|