@videojs/spf 10.0.0-beta.25 → 10.0.0-beta.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/default/core/actors/create-machine-actor.js.map +1 -1
- package/dist/default/core/actors/create-transition-actor.js.map +1 -1
- package/dist/default/core/composition/create-composition.js +1 -1
- package/dist/default/core/composition/create-composition.js.map +1 -1
- package/dist/default/core/composition/share-signals.js.map +1 -1
- package/dist/default/core/machine.js.map +1 -1
- package/dist/default/core/reactors/create-machine-reactor.js.map +1 -1
- package/dist/default/core/signals/effect.js.map +1 -1
- package/dist/default/core/signals/primitives.js.map +1 -1
- package/dist/default/core/signals/when.js +38 -0
- package/dist/default/core/signals/when.js.map +1 -0
- package/dist/default/core/tasks/delayed-reschedule.js +31 -0
- package/dist/default/core/tasks/delayed-reschedule.js.map +1 -0
- package/dist/default/core/tasks/task.js +133 -3
- package/dist/default/core/tasks/task.js.map +1 -1
- package/dist/default/hls-audio.js +3 -0
- package/dist/default/hls-background-video.js +3 -0
- package/dist/default/hls-video.js +4 -0
- package/dist/default/hls.js +7 -5
- package/dist/default/media/abr/quality-selection.js.map +1 -1
- package/dist/default/media/buffer/back-buffer.js.map +1 -1
- package/dist/default/media/buffer/forward-buffer.js +37 -3
- package/dist/default/media/buffer/forward-buffer.js.map +1 -1
- package/dist/default/media/dom/capabilities.js +2 -0
- package/dist/default/media/dom/capabilities.js.map +1 -1
- package/dist/default/media/dom/mse/append-segment.js.map +1 -1
- package/dist/default/media/dom/mse/buffer-flusher.js.map +1 -1
- package/dist/default/media/dom/mse/duration.js +27 -10
- package/dist/default/media/dom/mse/duration.js.map +1 -1
- package/dist/default/media/dom/mse/end-of-stream.js.map +1 -1
- package/dist/default/media/dom/mse/mediasource-setup.js +68 -17
- package/dist/default/media/dom/mse/mediasource-setup.js.map +1 -1
- package/dist/default/media/dom/text/resolve-vtt-segment.js +1 -7
- package/dist/default/media/dom/text/resolve-vtt-segment.js.map +1 -1
- package/dist/default/media/dom/text/text-track-slots.js.map +1 -1
- package/dist/default/media/errors.js +62 -0
- package/dist/default/media/errors.js.map +1 -0
- package/dist/default/media/hls/parse-attributes.js +1 -1
- package/dist/default/media/hls/parse-attributes.js.map +1 -1
- package/dist/default/media/hls/parse-media-playlist.js +175 -19
- package/dist/default/media/hls/parse-media-playlist.js.map +1 -1
- package/dist/default/media/hls/parse-multivariant.js +14 -3
- package/dist/default/media/hls/parse-multivariant.js.map +1 -1
- package/dist/default/media/hls/reload-policy.js +71 -0
- package/dist/default/media/hls/reload-policy.js.map +1 -0
- package/dist/default/media/hls/resolve-url.js.map +1 -1
- package/dist/default/media/live-window.js +36 -0
- package/dist/default/media/live-window.js.map +1 -0
- package/dist/default/media/media-tracks/media-tracks.js +101 -0
- package/dist/default/media/media-tracks/media-tracks.js.map +1 -0
- package/dist/default/media/mp4/box.js +58 -0
- package/dist/default/media/mp4/box.js.map +1 -0
- package/dist/default/media/mp4/timestamp-origin.js +107 -0
- package/dist/default/media/mp4/timestamp-origin.js.map +1 -0
- package/dist/default/media/primitives/select-tracks.js +1 -24
- package/dist/default/media/primitives/select-tracks.js.map +1 -1
- package/dist/default/media/text/parse-vtt-timestamp-map.js +42 -0
- package/dist/default/media/text/parse-vtt-timestamp-map.js.map +1 -0
- package/dist/default/media/text/resolve-vtt-metadata.js +23 -0
- package/dist/default/media/text/resolve-vtt-metadata.js.map +1 -0
- package/dist/default/media/types/index.js +15 -1
- package/dist/default/media/types/index.js.map +1 -1
- package/dist/default/media/utils/cdn.js.map +1 -1
- package/dist/default/media/utils/preload.js.map +1 -1
- package/dist/default/media/utils/track-selection.js +1 -1
- package/dist/default/media/utils/track-selection.js.map +1 -1
- package/dist/default/media/utils/tracks.js +17 -3
- package/dist/default/media/utils/tracks.js.map +1 -1
- package/dist/default/media-tracks.js +2 -0
- package/dist/default/mux-audio.js +3 -0
- package/dist/default/mux-background-video.js +3 -0
- package/dist/default/mux-video.js +3 -0
- package/dist/default/network/bandwidth-estimator.js.map +1 -1
- package/dist/default/network/chunked-stream-iterable.js.map +1 -1
- package/dist/default/network/ewma.js +1 -1
- package/dist/default/network/ewma.js.map +1 -1
- package/dist/default/network/fetch.js.map +1 -1
- package/dist/default/playback/actors/dom/segment-loader.js +61 -95
- package/dist/default/playback/actors/dom/segment-loader.js.map +1 -1
- package/dist/default/playback/actors/dom/source-buffer.js +1 -0
- package/dist/default/playback/actors/dom/source-buffer.js.map +1 -1
- package/dist/default/playback/actors/dom/text-tracks.js.map +1 -1
- package/dist/default/playback/actors/text-track-segment-loader.js +30 -16
- package/dist/default/playback/actors/text-track-segment-loader.js.map +1 -1
- package/dist/default/playback/adapters/hls-audio/adapter.js +186 -0
- package/dist/default/playback/adapters/hls-audio/adapter.js.map +1 -0
- package/dist/default/playback/adapters/hls-audio/media.js +9 -0
- package/dist/default/playback/adapters/hls-audio/media.js.map +1 -0
- package/dist/default/playback/adapters/hls-background-video/adapter.js +123 -0
- package/dist/default/playback/adapters/hls-background-video/adapter.js.map +1 -0
- package/dist/default/playback/adapters/hls-background-video/host.js +60 -0
- package/dist/default/playback/adapters/hls-background-video/host.js.map +1 -0
- package/dist/default/playback/adapters/hls-background-video/media.js +21 -0
- package/dist/default/playback/adapters/hls-background-video/media.js.map +1 -0
- package/dist/default/playback/adapters/hls-video/adapter.js +316 -0
- package/dist/default/playback/adapters/hls-video/adapter.js.map +1 -0
- package/dist/default/playback/adapters/hls-video/error-surface.js +51 -0
- package/dist/default/playback/adapters/hls-video/error-surface.js.map +1 -0
- package/dist/default/playback/adapters/hls-video/media-tracks.js +126 -0
- package/dist/default/playback/adapters/hls-video/media-tracks.js.map +1 -0
- package/dist/default/playback/adapters/hls-video/media.js +11 -0
- package/dist/default/playback/adapters/hls-video/media.js.map +1 -0
- package/dist/default/playback/adapters/mux-audio/media.js +26 -0
- package/dist/default/playback/adapters/mux-audio/media.js.map +1 -0
- package/dist/default/playback/adapters/mux-video/adapter.js +92 -0
- package/dist/default/playback/adapters/mux-video/adapter.js.map +1 -0
- package/dist/default/playback/adapters/mux-video/media.js +23 -0
- package/dist/default/playback/adapters/mux-video/media.js.map +1 -0
- package/dist/default/playback/behaviors/calculate-presentation-duration.js.map +1 -1
- package/dist/default/playback/behaviors/collect-errors.js +77 -0
- package/dist/default/playback/behaviors/collect-errors.js.map +1 -0
- package/dist/default/playback/behaviors/derive-cdn-priority.js.map +1 -1
- package/dist/default/playback/behaviors/dom/airplay.js +211 -0
- package/dist/default/playback/behaviors/dom/airplay.js.map +1 -0
- package/dist/default/playback/behaviors/dom/apply-start-position.js +93 -0
- package/dist/default/playback/behaviors/dom/apply-start-position.js.map +1 -0
- package/dist/default/playback/behaviors/dom/end-of-stream.js +13 -1
- package/dist/default/playback/behaviors/dom/end-of-stream.js.map +1 -1
- package/dist/default/playback/behaviors/dom/load-segments.js +5 -1
- package/dist/default/playback/behaviors/dom/load-segments.js.map +1 -1
- package/dist/default/playback/behaviors/dom/recover-end-stall.js +77 -0
- package/dist/default/playback/behaviors/dom/recover-end-stall.js.map +1 -0
- package/dist/default/playback/behaviors/dom/seek-to-live-edge.js +132 -0
- package/dist/default/playback/behaviors/dom/seek-to-live-edge.js.map +1 -0
- package/dist/default/playback/behaviors/dom/setup-buffer-actors.js +17 -4
- package/dist/default/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
- package/dist/default/playback/behaviors/dom/setup-mediasource.js +36 -11
- package/dist/default/playback/behaviors/dom/setup-mediasource.js.map +1 -1
- package/dist/default/playback/behaviors/dom/setup-text-track-actors.js +11 -25
- package/dist/default/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
- package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js +26 -0
- package/dist/default/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -0
- package/dist/default/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-current-time.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-load-triggers.js +4 -4
- package/dist/default/playback/behaviors/dom/track-load-triggers.js.map +1 -1
- package/dist/default/playback/behaviors/dom/track-playback-rate.js.map +1 -1
- package/dist/default/playback/behaviors/dom/update-mediasource-duration.js +31 -8
- package/dist/default/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
- package/dist/default/playback/behaviors/establish-start-media-time.js +197 -0
- package/dist/default/playback/behaviors/establish-start-media-time.js.map +1 -0
- package/dist/default/playback/behaviors/resolve-presentation.js.map +1 -1
- package/dist/default/playback/behaviors/resolve-track.js +29 -8
- package/dist/default/playback/behaviors/resolve-track.js.map +1 -1
- package/dist/default/playback/behaviors/select-tracks.js.map +1 -1
- package/dist/default/playback/behaviors/setup-failover-monitor.js +1 -1
- package/dist/default/playback/behaviors/setup-failover-monitor.js.map +1 -1
- package/dist/default/playback/behaviors/sync-preload.js.map +1 -1
- package/dist/default/playback/behaviors/track-switching.js +76 -5
- package/dist/default/playback/behaviors/track-switching.js.map +1 -1
- package/dist/default/playback/engines/hls/engine-audio-only.js +25 -7
- package/dist/default/playback/engines/hls/engine-audio-only.js.map +1 -1
- package/dist/{dev/playback/engines/background-video/engine.js → default/playback/engines/hls/engine-background-video.js} +3 -3
- package/dist/default/playback/engines/hls/engine-background-video.js.map +1 -0
- package/dist/default/playback/engines/hls/engine.js +36 -6
- package/dist/default/playback/engines/hls/engine.js.map +1 -1
- package/dist/default/playback/primitives/error-messages.js +39 -0
- package/dist/default/playback/primitives/error-messages.js.map +1 -0
- package/dist/default/playback/primitives/failover-fetch.js.map +1 -1
- package/dist/default/playback/primitives/head-peek.js +44 -0
- package/dist/default/playback/primitives/head-peek.js.map +1 -0
- package/dist/default/playback/primitives/live-window.js +66 -0
- package/dist/default/playback/primitives/live-window.js.map +1 -0
- package/dist/default/playback/primitives/relocation-pipelines.js +203 -0
- package/dist/default/playback/primitives/relocation-pipelines.js.map +1 -0
- package/dist/default/playback/primitives/report-track-conditions.js +86 -0
- package/dist/default/playback/primitives/report-track-conditions.js.map +1 -0
- package/dist/default/playback/primitives/segment-load-pipeline.js +96 -0
- package/dist/default/playback/primitives/segment-load-pipeline.js.map +1 -0
- package/dist/default/playback/primitives/text-segment-load-pipeline.js +37 -0
- package/dist/default/playback/primitives/text-segment-load-pipeline.js.map +1 -0
- package/dist/default/playback/primitives/track-types.js.map +1 -1
- package/dist/dev/core/actors/actor.d.ts +0 -1
- package/dist/dev/core/actors/actor.d.ts.map +1 -1
- package/dist/dev/core/actors/create-machine-actor.d.ts +9 -6
- package/dist/dev/core/actors/create-machine-actor.d.ts.map +1 -1
- package/dist/dev/core/actors/create-machine-actor.js.map +1 -1
- package/dist/dev/core/actors/create-transition-actor.d.ts +0 -1
- package/dist/dev/core/actors/create-transition-actor.d.ts.map +1 -1
- package/dist/dev/core/actors/create-transition-actor.js.map +1 -1
- package/dist/dev/core/composition/create-composition.d.ts +10 -11
- package/dist/dev/core/composition/create-composition.d.ts.map +1 -1
- package/dist/dev/core/composition/create-composition.js +1 -1
- package/dist/dev/core/composition/create-composition.js.map +1 -1
- package/dist/dev/core/composition/share-signals.d.ts +0 -1
- package/dist/dev/core/composition/share-signals.d.ts.map +1 -1
- package/dist/dev/core/composition/share-signals.js.map +1 -1
- package/dist/dev/core/machine.d.ts +1 -0
- package/dist/dev/core/machine.d.ts.map +1 -1
- package/dist/dev/core/machine.js.map +1 -1
- package/dist/dev/core/reactors/create-machine-reactor.d.ts +2 -2
- package/dist/dev/core/reactors/create-machine-reactor.d.ts.map +1 -1
- package/dist/dev/core/reactors/create-machine-reactor.js.map +1 -1
- package/dist/dev/core/signals/effect.d.ts.map +1 -1
- package/dist/dev/core/signals/effect.js.map +1 -1
- package/dist/dev/core/signals/primitives.d.ts +1 -2
- package/dist/dev/core/signals/primitives.d.ts.map +1 -1
- package/dist/dev/core/signals/primitives.js.map +1 -1
- package/dist/dev/core/signals/when.js +38 -0
- package/dist/dev/core/signals/when.js.map +1 -0
- package/dist/dev/core/tasks/delayed-reschedule.js +31 -0
- package/dist/dev/core/tasks/delayed-reschedule.js.map +1 -0
- package/dist/dev/core/tasks/task.d.ts +53 -2
- package/dist/dev/core/tasks/task.d.ts.map +1 -1
- package/dist/dev/core/tasks/task.js +133 -3
- package/dist/dev/core/tasks/task.js.map +1 -1
- package/dist/dev/hls-audio.d.ts +3 -0
- package/dist/dev/hls-audio.js +3 -0
- package/dist/dev/hls-background-video.d.ts +3 -0
- package/dist/dev/hls-background-video.js +3 -0
- package/dist/dev/hls-video.d.ts +5 -0
- package/dist/dev/hls-video.js +4 -0
- package/dist/dev/hls.d.ts +8 -5
- package/dist/dev/hls.js +7 -5
- package/dist/dev/index.d.ts +0 -1
- package/dist/dev/index.d.ts.map +1 -1
- package/dist/dev/media/abr/quality-selection.d.ts.map +1 -1
- package/dist/dev/media/abr/quality-selection.js.map +1 -1
- package/dist/dev/media/buffer/back-buffer.d.ts.map +1 -1
- package/dist/dev/media/buffer/back-buffer.js.map +1 -1
- package/dist/dev/media/buffer/forward-buffer.d.ts.map +1 -1
- package/dist/dev/media/buffer/forward-buffer.js +37 -3
- package/dist/dev/media/buffer/forward-buffer.js.map +1 -1
- package/dist/dev/media/dom/capabilities.js +2 -0
- package/dist/dev/media/dom/capabilities.js.map +1 -1
- package/dist/dev/media/dom/mse/append-segment.d.ts +3 -9
- package/dist/dev/media/dom/mse/append-segment.d.ts.map +1 -1
- package/dist/dev/media/dom/mse/append-segment.js.map +1 -1
- package/dist/dev/media/dom/mse/buffer-flusher.d.ts.map +1 -1
- package/dist/dev/media/dom/mse/buffer-flusher.js.map +1 -1
- package/dist/dev/media/dom/mse/duration.js +27 -10
- package/dist/dev/media/dom/mse/duration.js.map +1 -1
- package/dist/dev/media/dom/mse/end-of-stream.js.map +1 -1
- package/dist/dev/media/dom/mse/mediasource-setup.js +68 -17
- package/dist/dev/media/dom/mse/mediasource-setup.js.map +1 -1
- package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts +0 -6
- package/dist/dev/media/dom/text/resolve-vtt-segment.d.ts.map +1 -1
- package/dist/dev/media/dom/text/resolve-vtt-segment.js +1 -7
- package/dist/dev/media/dom/text/resolve-vtt-segment.js.map +1 -1
- package/dist/dev/media/dom/text/text-track-slots.d.ts +0 -1
- package/dist/dev/media/dom/text/text-track-slots.d.ts.map +1 -1
- package/dist/dev/media/dom/text/text-track-slots.js.map +1 -1
- package/dist/dev/media/errors.d.ts +82 -0
- package/dist/dev/media/errors.d.ts.map +1 -0
- package/dist/dev/media/errors.js +62 -0
- package/dist/dev/media/errors.js.map +1 -0
- package/dist/dev/media/hls/parse-attributes.js +1 -1
- package/dist/dev/media/hls/parse-attributes.js.map +1 -1
- package/dist/dev/media/hls/parse-media-playlist.js +175 -19
- package/dist/dev/media/hls/parse-media-playlist.js.map +1 -1
- package/dist/dev/media/hls/parse-multivariant.js +14 -3
- package/dist/dev/media/hls/parse-multivariant.js.map +1 -1
- package/dist/dev/media/hls/reload-policy.js +71 -0
- package/dist/dev/media/hls/reload-policy.js.map +1 -0
- package/dist/dev/media/hls/resolve-url.js.map +1 -1
- package/dist/dev/media/live-window.js +36 -0
- package/dist/dev/media/live-window.js.map +1 -0
- package/dist/dev/media/media-tracks/media-tracks.d.ts +51 -0
- package/dist/dev/media/media-tracks/media-tracks.d.ts.map +1 -0
- package/dist/dev/media/media-tracks/media-tracks.js +101 -0
- package/dist/dev/media/media-tracks/media-tracks.js.map +1 -0
- package/dist/dev/media/mp4/box.js +58 -0
- package/dist/dev/media/mp4/box.js.map +1 -0
- package/dist/dev/media/mp4/timestamp-origin.js +107 -0
- package/dist/dev/media/mp4/timestamp-origin.js.map +1 -0
- package/dist/dev/media/primitives/select-tracks.d.ts +0 -1
- package/dist/dev/media/primitives/select-tracks.d.ts.map +1 -1
- package/dist/dev/media/primitives/select-tracks.js +1 -24
- package/dist/dev/media/primitives/select-tracks.js.map +1 -1
- package/dist/dev/media/text/parse-vtt-timestamp-map.js +42 -0
- package/dist/dev/media/text/parse-vtt-timestamp-map.js.map +1 -0
- package/dist/dev/media/text/resolve-vtt-metadata.js +23 -0
- package/dist/dev/media/text/resolve-vtt-metadata.js.map +1 -0
- package/dist/dev/media/types/index.d.ts +163 -4
- package/dist/dev/media/types/index.d.ts.map +1 -1
- package/dist/dev/media/types/index.js +15 -1
- package/dist/dev/media/types/index.js.map +1 -1
- package/dist/dev/media/utils/cdn.d.ts.map +1 -1
- package/dist/dev/media/utils/cdn.js.map +1 -1
- package/dist/dev/media/utils/preload.js.map +1 -1
- package/dist/dev/media/utils/track-selection.js +1 -1
- package/dist/dev/media/utils/track-selection.js.map +1 -1
- package/dist/dev/media/utils/tracks.js +17 -3
- package/dist/dev/media/utils/tracks.js.map +1 -1
- package/dist/dev/media-tracks.d.ts +3 -0
- package/dist/dev/media-tracks.js +2 -0
- package/dist/dev/mux-audio.d.ts +4 -0
- package/dist/dev/mux-audio.js +3 -0
- package/dist/dev/mux-background-video.d.ts +3 -0
- package/dist/dev/mux-background-video.js +3 -0
- package/dist/dev/mux-video.d.ts +4 -0
- package/dist/dev/mux-video.js +3 -0
- package/dist/dev/network/bandwidth-estimator.d.ts.map +1 -1
- package/dist/dev/network/bandwidth-estimator.js.map +1 -1
- package/dist/dev/network/chunked-stream-iterable.js.map +1 -1
- package/dist/dev/network/ewma.js +1 -1
- package/dist/dev/network/ewma.js.map +1 -1
- package/dist/dev/network/fetch.js.map +1 -1
- package/dist/dev/playback/actors/dom/segment-loader.d.ts +16 -4
- package/dist/dev/playback/actors/dom/segment-loader.d.ts.map +1 -1
- package/dist/dev/playback/actors/dom/segment-loader.js +61 -95
- package/dist/dev/playback/actors/dom/segment-loader.js.map +1 -1
- package/dist/dev/playback/actors/dom/source-buffer.d.ts +3 -33
- package/dist/dev/playback/actors/dom/source-buffer.d.ts.map +1 -1
- package/dist/dev/playback/actors/dom/source-buffer.js +1 -0
- package/dist/dev/playback/actors/dom/source-buffer.js.map +1 -1
- package/dist/dev/playback/actors/dom/text-tracks.d.ts +3 -1
- package/dist/dev/playback/actors/dom/text-tracks.js.map +1 -1
- package/dist/dev/playback/actors/text-track-segment-loader.d.ts +7 -12
- package/dist/dev/playback/actors/text-track-segment-loader.d.ts.map +1 -1
- package/dist/dev/playback/actors/text-track-segment-loader.js +30 -16
- package/dist/dev/playback/actors/text-track-segment-loader.js.map +1 -1
- package/dist/dev/playback/actors/text-tracks.d.ts +3 -12
- package/dist/dev/playback/actors/text-tracks.d.ts.map +1 -1
- package/dist/dev/playback/adapters/hls-audio/adapter.d.ts +54 -0
- package/dist/dev/playback/adapters/hls-audio/adapter.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-audio/adapter.js +186 -0
- package/dist/dev/playback/adapters/hls-audio/adapter.js.map +1 -0
- package/dist/dev/playback/adapters/hls-audio/media.d.ts +10 -0
- package/dist/dev/playback/adapters/hls-audio/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-audio/media.js +9 -0
- package/dist/dev/playback/adapters/hls-audio/media.js.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts +59 -0
- package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/adapter.js +123 -0
- package/dist/dev/playback/adapters/hls-background-video/adapter.js.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/host.d.ts +38 -0
- package/dist/dev/playback/adapters/hls-background-video/host.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/host.js +60 -0
- package/dist/dev/playback/adapters/hls-background-video/host.js.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/media.d.ts +20 -0
- package/dist/dev/playback/adapters/hls-background-video/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-background-video/media.js +21 -0
- package/dist/dev/playback/adapters/hls-background-video/media.js.map +1 -0
- package/dist/dev/playback/adapters/hls-video/adapter.d.ts +63 -0
- package/dist/dev/playback/adapters/hls-video/adapter.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-video/adapter.js +316 -0
- package/dist/dev/playback/adapters/hls-video/adapter.js.map +1 -0
- package/dist/dev/playback/adapters/hls-video/error-surface.d.ts +18 -0
- package/dist/dev/playback/adapters/hls-video/error-surface.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-video/error-surface.js +51 -0
- package/dist/dev/playback/adapters/hls-video/error-surface.js.map +1 -0
- package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts +22 -0
- package/dist/dev/playback/adapters/hls-video/media-tracks.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-video/media-tracks.js +126 -0
- package/dist/dev/playback/adapters/hls-video/media-tracks.js.map +1 -0
- package/dist/dev/playback/adapters/hls-video/media.d.ts +10 -0
- package/dist/dev/playback/adapters/hls-video/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/hls-video/media.js +11 -0
- package/dist/dev/playback/adapters/hls-video/media.js.map +1 -0
- package/dist/dev/playback/adapters/mux-audio/media.d.ts +28 -0
- package/dist/dev/playback/adapters/mux-audio/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/mux-audio/media.js +26 -0
- package/dist/dev/playback/adapters/mux-audio/media.js.map +1 -0
- package/dist/dev/playback/adapters/mux-video/adapter.d.ts +33 -0
- package/dist/dev/playback/adapters/mux-video/adapter.d.ts.map +1 -0
- package/dist/dev/playback/adapters/mux-video/adapter.js +92 -0
- package/dist/dev/playback/adapters/mux-video/adapter.js.map +1 -0
- package/dist/dev/playback/adapters/mux-video/media.d.ts +24 -0
- package/dist/dev/playback/adapters/mux-video/media.d.ts.map +1 -0
- package/dist/dev/playback/adapters/mux-video/media.js +23 -0
- package/dist/dev/playback/adapters/mux-video/media.js.map +1 -0
- package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts +3 -2
- package/dist/dev/playback/behaviors/calculate-presentation-duration.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/calculate-presentation-duration.js.map +1 -1
- package/dist/dev/playback/behaviors/collect-errors.js +77 -0
- package/dist/dev/playback/behaviors/collect-errors.js.map +1 -0
- package/dist/dev/playback/behaviors/derive-cdn-priority.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/airplay.js +211 -0
- package/dist/dev/playback/behaviors/dom/airplay.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/apply-start-position.js +93 -0
- package/dist/dev/playback/behaviors/dom/apply-start-position.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/end-of-stream.js +13 -1
- package/dist/dev/playback/behaviors/dom/end-of-stream.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/load-segments.d.ts +14 -4
- package/dist/dev/playback/behaviors/dom/load-segments.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/load-segments.js +5 -1
- package/dist/dev/playback/behaviors/dom/load-segments.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/recover-end-stall.js +77 -0
- package/dist/dev/playback/behaviors/dom/recover-end-stall.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js +132 -0
- package/dist/dev/playback/behaviors/dom/seek-to-live-edge.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js +17 -4
- package/dist/dev/playback/behaviors/dom/setup-buffer-actors.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/setup-mediasource.js +36 -11
- package/dist/dev/playback/behaviors/dom/setup-mediasource.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts +16 -19
- package/dist/dev/playback/behaviors/dom/setup-text-track-actors.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js +11 -25
- package/dist/dev/playback/behaviors/dom/setup-text-track-actors.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js +26 -0
- package/dist/dev/playback/behaviors/dom/sync-live-seekable-range.js.map +1 -0
- package/dist/dev/playback/behaviors/dom/sync-text-tracks.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-current-time.d.ts +1 -2
- package/dist/dev/playback/behaviors/dom/track-current-time.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-current-time.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts +1 -2
- package/dist/dev/playback/behaviors/dom/track-load-triggers.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-load-triggers.js +4 -4
- package/dist/dev/playback/behaviors/dom/track-load-triggers.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts +1 -2
- package/dist/dev/playback/behaviors/dom/track-playback-rate.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/dom/track-playback-rate.js.map +1 -1
- package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js +31 -8
- package/dist/dev/playback/behaviors/dom/update-mediasource-duration.js.map +1 -1
- package/dist/dev/playback/behaviors/establish-start-media-time.d.ts +36 -0
- package/dist/dev/playback/behaviors/establish-start-media-time.d.ts.map +1 -0
- package/dist/dev/playback/behaviors/establish-start-media-time.js +197 -0
- package/dist/dev/playback/behaviors/establish-start-media-time.js.map +1 -0
- package/dist/dev/playback/behaviors/resolve-presentation.d.ts +2 -1
- package/dist/dev/playback/behaviors/resolve-presentation.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/resolve-presentation.js.map +1 -1
- package/dist/dev/playback/behaviors/resolve-track.js +29 -8
- package/dist/dev/playback/behaviors/resolve-track.js.map +1 -1
- package/dist/dev/playback/behaviors/select-tracks.d.ts +2 -1
- package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/select-tracks.js.map +1 -1
- package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts +2 -0
- package/dist/dev/playback/behaviors/setup-failover-monitor.d.ts.map +1 -1
- package/dist/dev/playback/behaviors/setup-failover-monitor.js +1 -1
- package/dist/dev/playback/behaviors/setup-failover-monitor.js.map +1 -1
- package/dist/dev/playback/behaviors/sync-preload.js.map +1 -1
- package/dist/dev/playback/behaviors/track-switching.js +76 -5
- package/dist/dev/playback/behaviors/track-switching.js.map +1 -1
- package/dist/dev/playback/engines/hls/engine-audio-only.d.ts +63 -17
- package/dist/dev/playback/engines/hls/engine-audio-only.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/engine-audio-only.js +25 -7
- package/dist/dev/playback/engines/hls/engine-audio-only.js.map +1 -1
- package/dist/dev/playback/engines/{background-video/engine.d.ts → hls/engine-background-video.d.ts} +8 -9
- package/dist/dev/playback/engines/hls/engine-background-video.d.ts.map +1 -0
- package/dist/{default/playback/engines/background-video/engine.js → dev/playback/engines/hls/engine-background-video.js} +3 -3
- package/dist/dev/playback/engines/hls/engine-background-video.js.map +1 -0
- package/dist/dev/playback/engines/hls/engine.d.ts +90 -16
- package/dist/dev/playback/engines/hls/engine.d.ts.map +1 -1
- package/dist/dev/playback/engines/hls/engine.js +36 -6
- package/dist/dev/playback/engines/hls/engine.js.map +1 -1
- package/dist/dev/playback/primitives/derive-start-media-time.d.ts +16 -0
- package/dist/dev/playback/primitives/derive-start-media-time.d.ts.map +1 -0
- package/dist/dev/playback/primitives/error-messages.js +39 -0
- package/dist/dev/playback/primitives/error-messages.js.map +1 -0
- package/dist/dev/playback/primitives/failover-fetch.js.map +1 -1
- package/dist/dev/playback/primitives/head-peek.js +44 -0
- package/dist/dev/playback/primitives/head-peek.js.map +1 -0
- package/dist/dev/playback/primitives/live-window.js +66 -0
- package/dist/dev/playback/primitives/live-window.js.map +1 -0
- package/dist/dev/playback/primitives/relocation-pipelines.js +203 -0
- package/dist/dev/playback/primitives/relocation-pipelines.js.map +1 -0
- package/dist/dev/playback/primitives/report-track-conditions.d.ts +11 -0
- package/dist/dev/playback/primitives/report-track-conditions.d.ts.map +1 -0
- package/dist/dev/playback/primitives/report-track-conditions.js +86 -0
- package/dist/dev/playback/primitives/report-track-conditions.js.map +1 -0
- package/dist/dev/playback/primitives/segment-load-pipeline.d.ts +2 -0
- package/dist/dev/playback/primitives/segment-load-pipeline.js +96 -0
- package/dist/dev/playback/primitives/segment-load-pipeline.js.map +1 -0
- package/dist/dev/playback/primitives/source-buffer-messages.d.ts +43 -0
- package/dist/dev/playback/primitives/source-buffer-messages.d.ts.map +1 -0
- package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts +65 -0
- package/dist/dev/playback/primitives/text-segment-load-pipeline.d.ts.map +1 -0
- package/dist/dev/playback/primitives/text-segment-load-pipeline.js +37 -0
- package/dist/dev/playback/primitives/text-segment-load-pipeline.js.map +1 -0
- package/dist/dev/playback/primitives/text-track-messages.d.ts +14 -0
- package/dist/dev/playback/primitives/text-track-messages.d.ts.map +1 -0
- package/dist/dev/playback/primitives/track-types.js.map +1 -1
- package/package.json +42 -11
- package/dist/default/background-video.js +0 -3
- package/dist/default/playback/engines/background-video/adapter.js +0 -158
- package/dist/default/playback/engines/background-video/adapter.js.map +0 -1
- package/dist/default/playback/engines/background-video/engine.js.map +0 -1
- package/dist/default/playback/engines/hls/adapter-audio-only.js +0 -119
- package/dist/default/playback/engines/hls/adapter-audio-only.js.map +0 -1
- package/dist/default/playback/engines/hls/adapter.js +0 -118
- package/dist/default/playback/engines/hls/adapter.js.map +0 -1
- package/dist/dev/background-video.d.ts +0 -3
- package/dist/dev/background-video.js +0 -3
- package/dist/dev/playback/engines/background-video/adapter.d.ts +0 -60
- package/dist/dev/playback/engines/background-video/adapter.d.ts.map +0 -1
- package/dist/dev/playback/engines/background-video/adapter.js +0 -158
- package/dist/dev/playback/engines/background-video/adapter.js.map +0 -1
- package/dist/dev/playback/engines/background-video/engine.d.ts.map +0 -1
- package/dist/dev/playback/engines/background-video/engine.js.map +0 -1
- package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts +0 -45
- package/dist/dev/playback/engines/hls/adapter-audio-only.d.ts.map +0 -1
- package/dist/dev/playback/engines/hls/adapter-audio-only.js +0 -119
- package/dist/dev/playback/engines/hls/adapter-audio-only.js.map +0 -1
- package/dist/dev/playback/engines/hls/adapter.d.ts +0 -44
- package/dist/dev/playback/engines/hls/adapter.d.ts.map +0 -1
- package/dist/dev/playback/engines/hls/adapter.js +0 -118
- package/dist/dev/playback/engines/hls/adapter.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mediasource-setup.js","names":[],"sources":["../../../../../src/media/dom/mse/mediasource-setup.ts"],"sourcesContent":["/**\n * MediaSource Setup\n *\n * Utilities for creating and configuring MediaSource/ManagedMediaSource\n * for MSE (Media Source Extensions) playback.\n *\n * Global ManagedMediaSource types are defined in ./mediasource.d.ts\n */\n\n/**\n * Check if MediaSource API is supported.\n */\nexport function supportsMediaSource(): boolean {\n return typeof MediaSource !== 'undefined';\n}\n\n/**\n * Check if ManagedMediaSource API is supported.\n * ManagedMediaSource is a newer Safari API with better lifecycle management.\n */\nexport function supportsManagedMediaSource(): boolean {\n return typeof ManagedMediaSource !== 'undefined';\n}\n\n/**\n * Options for creating a MediaSource.\n */\nexport interface CreateMediaSourceOptions {\n /** Prefer ManagedMediaSource when available (default: false for broader compatibility). */\n preferManaged?: boolean;\n}\n\n/**\n * Create a MediaSource or ManagedMediaSource instance.\n *\n * @param options - Creation options\n * @returns A MediaSource or ManagedMediaSource instance\n * @throws Error if no MediaSource API is available\n *\n * @example\n * const mediaSource = createMediaSource();\n * const mediaElement = document.querySelector('video');\n * attachMediaSource(mediaSource, mediaElement);\n */\nexport function createMediaSource(options: CreateMediaSourceOptions = {}): MediaSource {\n const { preferManaged = false } = options;\n\n if (preferManaged && supportsManagedMediaSource()) {\n return new ManagedMediaSource!();\n }\n\n if (supportsMediaSource()) {\n return new MediaSource();\n }\n\n throw new Error('MediaSource API is not supported');\n}\n\n/**\n * Result of attaching a MediaSource to a media element.\n */\nexport interface AttachMediaSourceResult {\n /** The object URL created for the MediaSource (empty string for ManagedMediaSource). */\n url: string;\n /** Detach the MediaSource and clean up resources. */\n detach: () => void;\n}\n\n/**\n * Attach a MediaSource to an HTMLMediaElement.\n *\n * Uses srcObject for ManagedMediaSource (Safari), or createObjectURL for regular MediaSource.\n *\n * @param mediaSource - The MediaSource to attach\n * @param mediaElement - The media element to attach to\n * @returns Object with URL and detach function\n *\n * @example\n * const mediaSource = createMediaSource();\n * const { detach } = attachMediaSource(mediaSource, videoElement);\n * // Use mediaSource...\n * // Later, to clean up:\n * detach();\n */\nexport function attachMediaSource(mediaSource: MediaSource, mediaElement: HTMLMediaElement): AttachMediaSourceResult {\n // ManagedMediaSource requires srcObject instead of createObjectURL\n const isManagedMediaSource = supportsManagedMediaSource() && mediaSource instanceof ManagedMediaSource!;\n\n if (isManagedMediaSource) {\n // ManagedMediaSource requires disableRemotePlayback — without it Safari\n // will not fire sourceopen.\n (mediaElement as HTMLMediaElement & { disableRemotePlayback: boolean }).disableRemotePlayback = true;\n\n // Use srcObject for ManagedMediaSource\n (mediaElement as HTMLMediaElement & { srcObject: MediaSource | null }).srcObject = mediaSource;\n\n const detach = (): void => {\n (mediaElement as HTMLMediaElement & { srcObject: MediaSource | null }).srcObject = null;\n mediaElement.load(); // Reset the element\n };\n\n return { url: '', detach };\n }\n\n // Use createObjectURL for regular MediaSource\n const url = URL.createObjectURL(mediaSource);\n mediaElement.src = url;\n\n const detach = (): void => {\n mediaElement.removeAttribute('src');\n mediaElement.load(); // Reset the element\n URL.revokeObjectURL(url);\n };\n\n return { url, detach };\n}\n\n/**\n * Create a SourceBuffer on a MediaSource.\n *\n * @param mediaSource - The MediaSource (must be in 'open' state)\n * @param mimeCodec - MIME type with codecs (e.g., 'video/mp4; codecs=\"avc1.42E01E\"')\n * @returns The created SourceBuffer\n * @throws Error if MediaSource is not open or codec is unsupported\n *\n * @example\n * const buffer = createSourceBuffer(mediaSource, 'video/mp4; codecs=\"avc1.42E01E\"');\n */\nexport function createSourceBuffer(mediaSource: MediaSource, mimeCodec: string): SourceBuffer {\n if (mediaSource.readyState !== 'open') {\n throw new Error('MediaSource is not open');\n }\n\n if (!isCodecSupported(mimeCodec)) {\n throw new Error(`Codec not supported: ${mimeCodec}`);\n }\n\n return mediaSource.addSourceBuffer(mimeCodec);\n}\n\n/**\n * Build a MIME codec string from a track's `mimeType` + `codecs`. Works\n * on partially-resolved tracks — both fields come from the multivariant\n * playlist and are available before media-playlist resolution.\n *\n * @param track - Track carrying `mimeType` and `codecs`\n * @returns MIME codec string suitable for `MediaSource.addSourceBuffer`\n *\n * @example\n * buildMimeCodec({ mimeType: 'video/mp4', codecs: ['avc1.42E01E'] })\n * // => 'video/mp4; codecs=\"avc1.42E01E\"'\n */\nexport function buildMimeCodec(track: { mimeType: string; codecs?: string[] }): string {\n const codecString = track.codecs?.join(',') ?? '';\n return `${track.mimeType}; codecs=\"${codecString}\"`;\n}\n\n/**\n * Check if a codec is supported.\n *\n * @param mimeCodec - MIME type with codecs string\n * @returns True if the codec is supported\n *\n * @example\n * if (isCodecSupported('video/mp4; codecs=\"avc1.42E01E\"')) {\n * // Create source buffer\n * }\n */\nexport function isCodecSupported(mimeCodec: string): boolean {\n if (!supportsMediaSource()) {\n return false;\n }\n\n return MediaSource.isTypeSupported(mimeCodec);\n}\n\n/**\n * Observe `mediaSource.readyState` changes via DOM events.\n *\n * Listens to `sourceopen`, `sourceended`, and `sourceclose` and invokes\n * `onChange` with the current `readyState` after each event. Listeners\n * are automatically removed when `abortSignal` is aborted.\n *\n * @param mediaSource - The MediaSource to observe\n * @param abortSignal - AbortSignal that controls listener lifetime\n * @param onChange - Called with the current readyState after each change\n *\n * @example\n * const controller = new AbortController();\n * onMediaSourceReadyStateChange(mediaSource, controller.signal, (state) => {\n * if (state === 'open') { ... }\n * });\n * // Later: controller.abort();\n */\nexport function onMediaSourceReadyStateChange(\n mediaSource: MediaSource,\n abortSignal: AbortSignal,\n onChange: (readyState: MediaSource['readyState']) => void\n): void {\n const update = () => onChange(mediaSource.readyState);\n const options = { signal: abortSignal };\n mediaSource.addEventListener('sourceopen', update, options);\n mediaSource.addEventListener('sourceended', update, options);\n mediaSource.addEventListener('sourceclose', update, options);\n}\n\n/**\n * Wait until `mediaSource.readyState` transitions away from `'closed'`\n * (the next `sourceopen`/`sourceended`/`sourceclose` event), or until\n * `signal` aborts — whichever fires first.\n *\n * Resolves immediately when `readyState` is already `'open'` or `'ended'`\n * (no further transition is coming, so there's nothing to wait for). The\n * caller is expected to re-check `readyState` after the await to\n * distinguish `'open'` from terminal states.\n *\n * Companion to `onMediaSourceReadyStateChange` for one-shot use in async\n * sequences (e.g., a behavior's reactor entry that needs to wait for the\n * MediaSource to attach before performing a spec-conforming mutation).\n *\n * @example\n * await waitForMediaSourceOpen(mediaSource, signal);\n * if (signal.aborted || mediaSource.readyState !== 'open') return;\n * // safe to perform 'open'-state-only work\n */\nexport function waitForMediaSourceOpen(mediaSource: MediaSource, signal: AbortSignal): Promise<void> {\n if (signal.aborted) return Promise.resolve();\n if (mediaSource.readyState !== 'closed') return Promise.resolve();\n\n return new Promise<void>((resolve) => {\n const done = () => resolve();\n const options = { once: true, signal };\n mediaSource.addEventListener('sourceopen', done, options);\n mediaSource.addEventListener('sourceended', done, options);\n mediaSource.addEventListener('sourceclose', done, options);\n signal.addEventListener('abort', done, { once: true });\n });\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,sBAA+B;AAC7C,QAAO,OAAO,gBAAgB;;;;;;AAOhC,SAAgB,6BAAsC;AACpD,QAAO,OAAO,uBAAuB;;;;;;;;;;;;;;AAuBvC,SAAgB,kBAAkB,UAAoC,EAAE,EAAe;CACrF,MAAM,EAAE,gBAAgB,UAAU;AAElC,KAAI,iBAAiB,4BAA4B,CAC/C,QAAO,IAAI,oBAAqB;AAGlC,KAAI,qBAAqB,CACvB,QAAO,IAAI,aAAa;AAG1B,OAAM,IAAI,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;AA6BrD,SAAgB,kBAAkB,aAA0B,cAAyD;AAInH,KAF6B,4BAA4B,IAAI,uBAAuB,oBAE1D;AAGvB,eAAuE,wBAAwB;AAG/F,eAAsE,YAAY;EAEnF,MAAM,eAAqB;AACxB,gBAAsE,YAAY;AACnF,gBAAa,MAAM;;AAGrB,SAAO;GAAE,KAAK;GAAI;GAAQ;;CAI5B,MAAM,MAAM,IAAI,gBAAgB,YAAY;AAC5C,cAAa,MAAM;CAEnB,MAAM,eAAqB;AACzB,eAAa,gBAAgB,MAAM;AACnC,eAAa,MAAM;AACnB,MAAI,gBAAgB,IAAI;;AAG1B,QAAO;EAAE;EAAK;EAAQ;;;;;;;;;;;;;AAcxB,SAAgB,mBAAmB,aAA0B,WAAiC;AAC5F,KAAI,YAAY,eAAe,OAC7B,OAAM,IAAI,MAAM,0BAA0B;AAG5C,KAAI,CAAC,iBAAiB,UAAU,CAC9B,OAAM,IAAI,MAAM,wBAAwB,YAAY;AAGtD,QAAO,YAAY,gBAAgB,UAAU;;;;;;;;;;;;;;AAe/C,SAAgB,eAAe,OAAwD;CACrF,MAAM,cAAc,MAAM,QAAQ,KAAK,IAAI,IAAI;AAC/C,QAAO,GAAG,MAAM,SAAS,YAAY,YAAY;;;;;;;;;;;;;AAcnD,SAAgB,iBAAiB,WAA4B;AAC3D,KAAI,CAAC,qBAAqB,CACxB,QAAO;AAGT,QAAO,YAAY,gBAAgB,UAAU;;;;;;;;;;;;;;;;;;;;AAqB/C,SAAgB,8BACd,aACA,aACA,UACM;CACN,MAAM,eAAe,SAAS,YAAY,WAAW;CACrD,MAAM,UAAU,EAAE,QAAQ,aAAa;AACvC,aAAY,iBAAiB,cAAc,QAAQ,QAAQ;AAC3D,aAAY,iBAAiB,eAAe,QAAQ,QAAQ;AAC5D,aAAY,iBAAiB,eAAe,QAAQ,QAAQ;;;;;;;;;;;;;;;;;;;;;AAsB9D,SAAgB,uBAAuB,aAA0B,QAAoC;AACnG,KAAI,OAAO,QAAS,QAAO,QAAQ,SAAS;AAC5C,KAAI,YAAY,eAAe,SAAU,QAAO,QAAQ,SAAS;AAEjE,QAAO,IAAI,SAAe,YAAY;EACpC,MAAM,aAAa,SAAS;EAC5B,MAAM,UAAU;GAAE,MAAM;GAAM;GAAQ;AACtC,cAAY,iBAAiB,cAAc,MAAM,QAAQ;AACzD,cAAY,iBAAiB,eAAe,MAAM,QAAQ;AAC1D,cAAY,iBAAiB,eAAe,MAAM,QAAQ;AAC1D,SAAO,iBAAiB,SAAS,MAAM,EAAE,MAAM,MAAM,CAAC;GACtD"}
|
|
1
|
+
{"version":3,"file":"mediasource-setup.js","names":[],"sources":["../../../../../src/media/dom/mse/mediasource-setup.ts"],"sourcesContent":["/**\n * MediaSource Setup\n *\n * Utilities for creating and configuring MediaSource/ManagedMediaSource\n * for MSE (Media Source Extensions) playback.\n *\n * Global ManagedMediaSource types are defined in ./mediasource.d.ts\n */\n\n/**\n * Check if MediaSource API is supported.\n */\nexport function supportsMediaSource(): boolean {\n return typeof MediaSource !== 'undefined';\n}\n\n/**\n * Check if ManagedMediaSource API is supported.\n * ManagedMediaSource is a newer Safari API with better lifecycle management.\n */\nexport function supportsManagedMediaSource(): boolean {\n return typeof ManagedMediaSource !== 'undefined';\n}\n\n/**\n * Options for creating a MediaSource.\n */\nexport interface CreateMediaSourceOptions {\n /** Prefer ManagedMediaSource when available (default: false for broader compatibility). */\n preferManaged?: boolean;\n}\n\n/**\n * Create a MediaSource or ManagedMediaSource instance.\n *\n * @param options - Creation options\n * @returns A MediaSource or ManagedMediaSource instance\n * @throws Error if no MediaSource API is available\n *\n * @example\n * const mediaSource = createMediaSource();\n * const mediaElement = document.querySelector('video');\n * attachMediaSource(mediaSource, mediaElement);\n */\nexport function createMediaSource(options: CreateMediaSourceOptions = {}): MediaSource {\n const { preferManaged = false } = options;\n\n if (preferManaged && supportsManagedMediaSource()) {\n return new ManagedMediaSource!();\n }\n\n if (supportsMediaSource()) {\n return new MediaSource();\n }\n\n throw new Error('MediaSource API is not supported');\n}\n\n/**\n * Options for `detach`.\n */\nexport interface DetachOptions {\n /**\n * Run the `load()` reset on the next microtask instead of synchronously.\n *\n * Required whenever another owner contributes sibling `<source>` children to\n * the same element and drops them from a signal effect: effects re-run on a\n * microtask, so a synchronous reset would run resource selection while those\n * siblings are still in the DOM and commit the element to one of them.\n * Canonical caller: `setupMediaSource`, whose compositions may include\n * `setupAirPlay`'s native-HLS fallback source.\n *\n * The ownership guard is evaluated when the reset actually fires, so a\n * re-attach landing in the interim correctly suppresses it.\n */\n deferReset?: boolean;\n}\n\n/**\n * Result of attaching a MediaSource to a media element.\n */\nexport interface AttachMediaSourceResult {\n /** The object URL created for the MediaSource. */\n url: string;\n /** Detach the MediaSource and clean up resources. */\n detach: (options?: DetachOptions) => void;\n}\n\n/**\n * Attach a MediaSource to an HTMLMediaElement via the `src` attribute.\n *\n * The object URL on the `src` attribute is the industry-hardened MSE attach\n * across the browser matrix, and works for ManagedMediaSource too (`srcObject`\n * buys nothing over it and forfeits the uniform URL lifecycle).\n *\n * @param mediaSource - The MediaSource to attach\n * @param mediaElement - The media element to attach to\n * @returns Object with URL and detach function\n *\n * @example\n * const mediaSource = createMediaSource();\n * const { detach } = attachMediaSource(mediaSource, videoElement);\n * // Use mediaSource...\n * // Later, to clean up:\n * detach();\n */\nexport function attachMediaSource(mediaSource: MediaSource, mediaElement: HTMLMediaElement): AttachMediaSourceResult {\n // ManagedMediaSource requires disableRemotePlayback — without it Safari\n // will not fire sourceopen. (MMS-only: on other platforms the flag governs\n // the standard Remote Playback API and must be left alone.)\n if (supportsManagedMediaSource() && mediaSource instanceof ManagedMediaSource!) {\n mediaElement.disableRemotePlayback = true;\n }\n\n const url = URL.createObjectURL(mediaSource);\n mediaElement.src = url;\n\n const detach = ({ deferReset }: DetachOptions = {}): void => {\n mediaElement.removeAttribute('src');\n scheduleReset(mediaSource, mediaElement, url, deferReset);\n URL.revokeObjectURL(url);\n };\n\n return { url, detach };\n}\n\n/**\n * Attach a MediaSource as a `<source>` child element.\n *\n * The object URL rides a `<source type=\"video/mp4\">` inserted as the\n * element's FIRST child (any bare `src` attribute is dropped; `load()`\n * re-runs resource selection). Unlike `srcObject`/`src` — which commit the\n * element to the MSE resource and ignore every `<source>` child — this\n * keeps sibling `<source>` alternatives part of resource selection, so a\n * composition can offer the element a natively-playable alternative next to\n * MSE. Canonical consumer: `setupAirPlay`'s native-HLS fallback source,\n * wired through `setupMediaSource`'s `attachMediaSource` config\n * (https://webkit.org/blog/15036/how-to-use-media-source-extensions-with-airplay/).\n */\nexport function attachMediaSourceAsSourceElement(\n mediaSource: MediaSource,\n mediaElement: HTMLMediaElement\n): AttachMediaSourceResult {\n // ManagedMediaSource requires disableRemotePlayback — without it Safari\n // will not fire sourceopen. (MMS-only: on other platforms the flag governs\n // the standard Remote Playback API and must be left alone.) Features that\n // need it false flip it once the source is open.\n if (supportsManagedMediaSource() && mediaSource instanceof ManagedMediaSource!) {\n mediaElement.disableRemotePlayback = true;\n }\n\n const url = URL.createObjectURL(mediaSource);\n const sourceEl = document.createElement('source');\n sourceEl.type = 'video/mp4';\n sourceEl.src = url;\n\n mediaElement.removeAttribute('src');\n mediaElement.prepend(sourceEl);\n mediaElement.load();\n\n const detach = ({ deferReset }: DetachOptions = {}): void => {\n sourceEl.remove();\n scheduleReset(mediaSource, mediaElement, url, deferReset);\n URL.revokeObjectURL(url);\n };\n\n return { url, detach };\n}\n\n/**\n * Detach's `load()` reset, applied only when tearing down an **unclosed**\n * attachment the element is still committed to:\n *\n * - **Closed MediaSource**: skip. The attachment is already dead, and the\n * element deliberately keeps whatever playback state it carries for\n * whatever attaches next — that attach's own `load()` performs the reset.\n * - **Element moved to another resource**: skip — resetting would rip that\n * resource out from under its owner.\n */\nfunction resetIfOwnedAndNotClosed(mediaSource: MediaSource, mediaElement: HTMLMediaElement, url: string): void {\n if (mediaSource.readyState !== 'closed' && mediaElement.currentSrc === url) {\n mediaElement.load();\n }\n}\n\n/**\n * Run the reset now, or on the next microtask when the caller has sibling\n * `<source>` owners that clear on an effect (see {@link DetachOptions.deferReset}).\n *\n * Deferring is safe because removing this attachment's own source does not by\n * itself re-run resource selection: the element stays committed to the object\n * URL until something calls `load()`, so nothing starts playing in the gap.\n */\nfunction scheduleReset(\n mediaSource: MediaSource,\n mediaElement: HTMLMediaElement,\n url: string,\n deferReset: boolean | undefined\n): void {\n if (deferReset) {\n queueMicrotask(() => resetIfOwnedAndNotClosed(mediaSource, mediaElement, url));\n return;\n }\n resetIfOwnedAndNotClosed(mediaSource, mediaElement, url);\n}\n\n/**\n * Create a SourceBuffer on a MediaSource.\n *\n * @param mediaSource - The MediaSource (must be in 'open' state)\n * @param mimeCodec - MIME type with codecs (e.g., 'video/mp4; codecs=\"avc1.42E01E\"')\n * @returns The created SourceBuffer\n * @throws Error if MediaSource is not open or codec is unsupported\n *\n * @example\n * const buffer = createSourceBuffer(mediaSource, 'video/mp4; codecs=\"avc1.42E01E\"');\n */\nexport function createSourceBuffer(mediaSource: MediaSource, mimeCodec: string): SourceBuffer {\n if (mediaSource.readyState !== 'open') {\n throw new Error('MediaSource is not open');\n }\n\n if (!isCodecSupported(mimeCodec)) {\n throw new Error(`Codec not supported: ${mimeCodec}`);\n }\n\n return mediaSource.addSourceBuffer(mimeCodec);\n}\n\n/**\n * Build a MIME codec string from a track's `mimeType` + `codecs`. Works\n * on partially-resolved tracks — both fields come from the multivariant\n * playlist and are available before media-playlist resolution.\n *\n * @param track - Track carrying `mimeType` and `codecs`\n * @returns MIME codec string suitable for `MediaSource.addSourceBuffer`\n *\n * @example\n * buildMimeCodec({ mimeType: 'video/mp4', codecs: ['avc1.42E01E'] })\n * // => 'video/mp4; codecs=\"avc1.42E01E\"'\n */\nexport function buildMimeCodec(track: { mimeType: string; codecs?: string[] }): string {\n const codecString = track.codecs?.join(',') ?? '';\n return `${track.mimeType}; codecs=\"${codecString}\"`;\n}\n\n/**\n * Check if a codec is supported.\n *\n * @param mimeCodec - MIME type with codecs string\n * @returns True if the codec is supported\n *\n * @example\n * if (isCodecSupported('video/mp4; codecs=\"avc1.42E01E\"')) {\n * // Create source buffer\n * }\n */\nexport function isCodecSupported(mimeCodec: string): boolean {\n if (!supportsMediaSource()) {\n return false;\n }\n\n return MediaSource.isTypeSupported(mimeCodec);\n}\n\n/**\n * Observe `mediaSource.readyState` changes via DOM events.\n *\n * Listens to `sourceopen`, `sourceended`, and `sourceclose` and invokes\n * `onChange` with the current `readyState` after each event. Listeners\n * are automatically removed when `abortSignal` is aborted.\n *\n * @param mediaSource - The MediaSource to observe\n * @param abortSignal - AbortSignal that controls listener lifetime\n * @param onChange - Called with the current readyState after each change\n *\n * @example\n * const controller = new AbortController();\n * onMediaSourceReadyStateChange(mediaSource, controller.signal, (state) => {\n * if (state === 'open') { ... }\n * });\n * // Later: controller.abort();\n */\nexport function onMediaSourceReadyStateChange(\n mediaSource: MediaSource,\n abortSignal: AbortSignal,\n onChange: (readyState: MediaSource['readyState']) => void\n): void {\n const update = () => onChange(mediaSource.readyState);\n const options = { signal: abortSignal };\n mediaSource.addEventListener('sourceopen', update, options);\n mediaSource.addEventListener('sourceended', update, options);\n mediaSource.addEventListener('sourceclose', update, options);\n}\n\n/**\n * Wait until `mediaSource.readyState` transitions away from `'closed'`\n * (the next `sourceopen`/`sourceended`/`sourceclose` event), or until\n * `signal` aborts — whichever fires first.\n *\n * Resolves immediately when `readyState` is already `'open'` or `'ended'`\n * (no further transition is coming, so there's nothing to wait for). The\n * caller is expected to re-check `readyState` after the await to\n * distinguish `'open'` from terminal states.\n *\n * Companion to `onMediaSourceReadyStateChange` for one-shot use in async\n * sequences (e.g., a behavior's reactor entry that needs to wait for the\n * MediaSource to attach before performing a spec-conforming mutation).\n *\n * @example\n * await waitForMediaSourceOpen(mediaSource, signal);\n * if (signal.aborted || mediaSource.readyState !== 'open') return;\n * // safe to perform 'open'-state-only work\n */\nexport function waitForMediaSourceOpen(mediaSource: MediaSource, signal: AbortSignal): Promise<void> {\n if (signal.aborted) return Promise.resolve();\n if (mediaSource.readyState !== 'closed') return Promise.resolve();\n\n return new Promise<void>((resolve) => {\n const done = () => resolve();\n const options = { once: true, signal };\n mediaSource.addEventListener('sourceopen', done, options);\n mediaSource.addEventListener('sourceended', done, options);\n mediaSource.addEventListener('sourceclose', done, options);\n signal.addEventListener('abort', done, { once: true });\n });\n}\n"],"mappings":"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"}
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
//#region src/media/dom/text/resolve-vtt-segment.ts
|
|
2
|
-
/**
|
|
3
|
-
* Parse a VTT segment using browser's native parser.
|
|
4
|
-
*
|
|
5
|
-
* Creates a dummy video element with a track element to leverage
|
|
6
|
-
* the browser's optimized VTT parsing. Returns parsed VTTCue objects.
|
|
7
|
-
*/
|
|
8
2
|
let dummyVideo = null;
|
|
9
3
|
function ensureDummyVideo() {
|
|
10
4
|
if (!dummyVideo) {
|
|
@@ -20,7 +14,6 @@ function resolveVttSegment(url) {
|
|
|
20
14
|
const video = ensureDummyVideo();
|
|
21
15
|
const track = document.createElement("track");
|
|
22
16
|
track.kind = "subtitles";
|
|
23
|
-
track.default = true;
|
|
24
17
|
return new Promise((resolve, reject) => {
|
|
25
18
|
const onLoad = () => {
|
|
26
19
|
const cues = [];
|
|
@@ -44,6 +37,7 @@ function resolveVttSegment(url) {
|
|
|
44
37
|
track.addEventListener("load", onLoad);
|
|
45
38
|
track.addEventListener("error", onError);
|
|
46
39
|
video.appendChild(track);
|
|
40
|
+
track.track.mode = "hidden";
|
|
47
41
|
track.src = url;
|
|
48
42
|
});
|
|
49
43
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-vtt-segment.js","names":[],"sources":["../../../../../src/media/dom/text/resolve-vtt-segment.ts"],"sourcesContent":["/**\n * Parse a VTT segment using browser's native parser.\n *\n * Creates a dummy video element with a track element to leverage\n * the browser's optimized VTT parsing. Returns parsed VTTCue objects.\n */\n\n// Singleton dummy video (reused across all parsing)\nlet dummyVideo: HTMLVideoElement | null = null;\n\nfunction ensureDummyVideo(): HTMLVideoElement {\n if (!dummyVideo) {\n dummyVideo = document.createElement('video');\n dummyVideo.muted = true;\n dummyVideo.preload = 'none';\n dummyVideo.style.display = 'none';\n dummyVideo.crossOrigin = 'anonymous';\n }\n return dummyVideo;\n}\n\nexport function resolveVttSegment(url: string): Promise<VTTCue[]> {\n const video = ensureDummyVideo();\n const track = document.createElement('track');\n track.kind = 'subtitles';\n
|
|
1
|
+
{"version":3,"file":"resolve-vtt-segment.js","names":[],"sources":["../../../../../src/media/dom/text/resolve-vtt-segment.ts"],"sourcesContent":["/**\n * Parse a VTT segment using browser's native parser.\n *\n * Creates a dummy video element with a track element to leverage\n * the browser's optimized VTT parsing. Returns parsed VTTCue objects.\n */\n\nimport { resolveVttSegmentMetadata, type TextSegmentMetadata } from '../../text/resolve-vtt-metadata';\n\n// Singleton dummy video (reused across all parsing)\nlet dummyVideo: HTMLVideoElement | null = null;\n\nfunction ensureDummyVideo(): HTMLVideoElement {\n if (!dummyVideo) {\n dummyVideo = document.createElement('video');\n dummyVideo.muted = true;\n dummyVideo.preload = 'none';\n dummyVideo.style.display = 'none';\n dummyVideo.crossOrigin = 'anonymous';\n }\n return dummyVideo;\n}\n\nexport function resolveVttSegment(url: string): Promise<VTTCue[]> {\n const video = ensureDummyVideo();\n const track = document.createElement('track');\n track.kind = 'subtitles';\n\n return new Promise((resolve, reject) => {\n const onLoad = (): void => {\n const cues: VTTCue[] = [];\n const textTrack = track.track;\n\n if (textTrack.cues) {\n for (let i = 0; i < textTrack.cues.length; i++) {\n const cue = textTrack.cues[i];\n if (cue) {\n cues.push(cue as VTTCue);\n }\n }\n }\n\n cleanup();\n resolve(cues);\n };\n\n const onError = (): void => {\n cleanup();\n reject(new Error(`Failed to load VTT segment: ${url}`));\n };\n\n const cleanup = (): void => {\n track.removeEventListener('load', onLoad);\n track.removeEventListener('error', onError);\n video.removeChild(track);\n };\n\n track.addEventListener('load', onLoad);\n track.addEventListener('error', onError);\n video.appendChild(track);\n // Force the browser to load and parse THIS track's resource by activating\n // it explicitly. Relying on `default = true` only works for the first track\n // appended to the reused dummy video: the media element's automatic\n // text-track selection runs once, so on Firefox every subsequent `<track>`\n // stays inactive, its resource is never fetched, and `load` never fires —\n // stranding all cues past the first segment. Setting `mode = 'hidden'`\n // (active but not rendered — this video is never shown) loads each segment\n // on every browser.\n track.track.mode = 'hidden';\n track.src = url;\n });\n}\n\nexport function destroyVttResolver(): void {\n dummyVideo = null;\n}\n\n/**\n * A resolved VTT segment paired with its header metadata — the shape used when a\n * caller needs the `X-TIMESTAMP-MAP` correlation (e.g. non-zero-PTS sources),\n * not just the cues.\n */\nexport interface ResolvedVttSegment {\n cues: VTTCue[];\n metadata: TextSegmentMetadata;\n}\n\n/**\n * Resolve a VTT segment's cues and header metadata together. Cues still come\n * from the browser's native parser ({@link resolveVttSegment}); the header is\n * scraped in parallel ({@link resolveVttSegmentMetadata}).\n */\nexport function resolveVttSegmentWithMetadata(url: string): Promise<ResolvedVttSegment> {\n return Promise.all([resolveVttSegment(url), resolveVttSegmentMetadata(url)]).then(([cues, metadata]) => ({\n cues,\n metadata,\n }));\n}\n"],"mappings":";AAUA,IAAI,aAAsC;AAE1C,SAAS,mBAAqC;CAC5C,IAAI,CAAC,YAAY;EACf,aAAa,SAAS,cAAc,OAAO;EAC3C,WAAW,QAAQ;EACnB,WAAW,UAAU;EACrB,WAAW,MAAM,UAAU;EAC3B,WAAW,cAAc;CAC3B;CACA,OAAO;AACT;AAEA,SAAgB,kBAAkB,KAAgC;CAChE,MAAM,QAAQ,iBAAiB;CAC/B,MAAM,QAAQ,SAAS,cAAc,OAAO;CAC5C,MAAM,OAAO;CAEb,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,eAAqB;GACzB,MAAM,OAAiB,CAAC;GACxB,MAAM,YAAY,MAAM;GAExB,IAAI,UAAU,MACZ,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,KAAK,QAAQ,KAAK;IAC9C,MAAM,MAAM,UAAU,KAAK;IAC3B,IAAI,KACF,KAAK,KAAK,GAAa;GAE3B;GAGF,QAAQ;GACR,QAAQ,IAAI;EACd;EAEA,MAAM,gBAAsB;GAC1B,QAAQ;GACR,uBAAO,IAAI,MAAM,+BAA+B,KAAK,CAAC;EACxD;EAEA,MAAM,gBAAsB;GAC1B,MAAM,oBAAoB,QAAQ,MAAM;GACxC,MAAM,oBAAoB,SAAS,OAAO;GAC1C,MAAM,YAAY,KAAK;EACzB;EAEA,MAAM,iBAAiB,QAAQ,MAAM;EACrC,MAAM,iBAAiB,SAAS,OAAO;EACvC,MAAM,YAAY,KAAK;EASvB,MAAM,MAAM,OAAO;EACnB,MAAM,MAAM;CACd,CAAC;AACH;AAEA,SAAgB,qBAA2B;CACzC,aAAa;AACf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-track-slots.js","names":[],"sources":["../../../../../src/media/dom/text/text-track-slots.ts"],"sourcesContent":["import { isCaptionOrSubtitleTrack } from '@videojs/utils/dom';\n\nimport type { PartiallyResolvedTextTrack, TextTrack } from '../../types';\n\n/**\n * SPF-owned `<track>` selector. Each slot created by\n * `addSubtitlesTracksToMedia` carries this attribute so reads and removals can\n * filter SPF-owned tracks from host-page-owned ones.\n */\nconst SPF_TRACK_SELECTOR = 'track[data-src-track]';\n\n/**\n * Allocate text-track slots on `mediaElement` for each model track by creating\n * and appending `<track>` children. Marks each element with `data-src-track`\n * so it can be distinguished from `<track>` children the host page added\n * directly — used by `getShowingSubtitlesTrackFromMedia` and\n * `removeAllSubtitlesTracksFromMedia` to scope their reads/removals to\n * SPF-owned slots. The spec has no `removeTextTrack` API, so creating\n * `<track>` elements is the only mechanism for adding *and* removing entries\n * to `mediaElement.textTracks`.\n */\nexport function addSubtitlesTracksToMedia(\n mediaElement: HTMLMediaElement,\n modelTextTracks: readonly (PartiallyResolvedTextTrack | TextTrack)[]\n): void {\n for (const modelTrack of modelTextTracks) {\n const el = document.createElement('track');\n el.id = modelTrack.id;\n el.kind = modelTrack.kind;\n el.label = modelTrack.label;\n el.toggleAttribute('data-src-track', true);\n if (modelTrack.language) el.srclang = modelTrack.language;\n // Deliberately NOT propagating `modelTrack.default` to the `default`\n // attribute: that makes the browser auto-activate the slot on insertion,\n // which fires a `change` that `syncTextTracks` records as user intent —\n // auto-enabling captions past SPF's opt-in policy (`enableDefaultTrack`\n // governs DEFAULT=YES handling in `switchTextTrack`, not the browser). SPF\n // owns selection; these slots are containers, so they carry no selection hint.\n mediaElement.appendChild(el);\n }\n}\n\n/**\n * Return the SPF-owned subtitle/caption `TextTrack` currently in `'showing'`\n * mode, or `undefined` if none. Restricts the search to slots created by\n * `addSubtitlesTracksToMedia` (via the `data-src-track` selector) so a showing\n * track that the host page added directly is ignored — SPF selection only\n * mirrors tracks it owns.\n */\nexport function getShowingSubtitlesTrackFromMedia(mediaElement: HTMLMediaElement): globalThis.TextTrack | undefined {\n const elements = mediaElement.querySelectorAll<HTMLTrackElement>(SPF_TRACK_SELECTOR);\n for (const el of elements) {\n const track = el.track;\n if (track.mode === 'showing' && isCaptionOrSubtitleTrack(track)) {\n return track;\n }\n }\n return undefined;\n}\n\n/**\n * Remove every SPF-owned `<track>` child from `mediaElement` (those tagged\n * with `data-src-track` by `addSubtitlesTracksToMedia`). `<track>` elements\n * the host page added directly are left in place.\n */\nexport function removeAllSubtitlesTracksFromMedia(mediaElement: HTMLMediaElement): void {\n const elements = mediaElement.querySelectorAll<HTMLTrackElement>(SPF_TRACK_SELECTOR);\n for (const el of elements) {\n el.remove();\n }\n}\n\n/**\n * Apply a selection to a `TextTrackList` by setting each subtitle/caption\n * track's `mode` to `'showing'` if its `id` matches `selectedId` and\n * `'disabled'` otherwise. Tracks of other kinds (chapters, metadata,\n * descriptions) are left untouched — they may be owned by the host page.\n */\nexport function syncTextTrackModes(textTracks: TextTrackList, selectedId: string | undefined): void {\n for (let i = 0; i < textTracks.length; i++) {\n const track = textTracks[i]!;\n if (!isCaptionOrSubtitleTrack(track)) continue;\n track.mode = track.id === selectedId ? 'showing' : 'disabled';\n }\n}\n"],"mappings":";;;;;;;AASA,MAAM,qBAAqB;;;;;;;;;;;AAY3B,SAAgB,0BACd,cACA,iBACM;
|
|
1
|
+
{"version":3,"file":"text-track-slots.js","names":[],"sources":["../../../../../src/media/dom/text/text-track-slots.ts"],"sourcesContent":["import { isCaptionOrSubtitleTrack } from '@videojs/utils/dom';\n\nimport type { PartiallyResolvedTextTrack, TextTrack } from '../../types';\n\n/**\n * SPF-owned `<track>` selector. Each slot created by\n * `addSubtitlesTracksToMedia` carries this attribute so reads and removals can\n * filter SPF-owned tracks from host-page-owned ones.\n */\nconst SPF_TRACK_SELECTOR = 'track[data-src-track]';\n\n/**\n * Allocate text-track slots on `mediaElement` for each model track by creating\n * and appending `<track>` children. Marks each element with `data-src-track`\n * so it can be distinguished from `<track>` children the host page added\n * directly — used by `getShowingSubtitlesTrackFromMedia` and\n * `removeAllSubtitlesTracksFromMedia` to scope their reads/removals to\n * SPF-owned slots. The spec has no `removeTextTrack` API, so creating\n * `<track>` elements is the only mechanism for adding *and* removing entries\n * to `mediaElement.textTracks`.\n */\nexport function addSubtitlesTracksToMedia(\n mediaElement: HTMLMediaElement,\n modelTextTracks: readonly (PartiallyResolvedTextTrack | TextTrack)[]\n): void {\n for (const modelTrack of modelTextTracks) {\n const el = document.createElement('track');\n el.id = modelTrack.id;\n el.kind = modelTrack.kind;\n el.label = modelTrack.label;\n el.toggleAttribute('data-src-track', true);\n if (modelTrack.language) el.srclang = modelTrack.language;\n // Deliberately NOT propagating `modelTrack.default` to the `default`\n // attribute: that makes the browser auto-activate the slot on insertion,\n // which fires a `change` that `syncTextTracks` records as user intent —\n // auto-enabling captions past SPF's opt-in policy (`enableDefaultTrack`\n // governs DEFAULT=YES handling in `switchTextTrack`, not the browser). SPF\n // owns selection; these slots are containers, so they carry no selection hint.\n mediaElement.appendChild(el);\n }\n}\n\n/**\n * Return the SPF-owned subtitle/caption `TextTrack` currently in `'showing'`\n * mode, or `undefined` if none. Restricts the search to slots created by\n * `addSubtitlesTracksToMedia` (via the `data-src-track` selector) so a showing\n * track that the host page added directly is ignored — SPF selection only\n * mirrors tracks it owns.\n */\nexport function getShowingSubtitlesTrackFromMedia(mediaElement: HTMLMediaElement): globalThis.TextTrack | undefined {\n const elements = mediaElement.querySelectorAll<HTMLTrackElement>(SPF_TRACK_SELECTOR);\n for (const el of elements) {\n const track = el.track;\n if (track.mode === 'showing' && isCaptionOrSubtitleTrack(track)) {\n return track;\n }\n }\n return undefined;\n}\n\n/**\n * Remove every SPF-owned `<track>` child from `mediaElement` (those tagged\n * with `data-src-track` by `addSubtitlesTracksToMedia`). `<track>` elements\n * the host page added directly are left in place.\n */\nexport function removeAllSubtitlesTracksFromMedia(mediaElement: HTMLMediaElement): void {\n const elements = mediaElement.querySelectorAll<HTMLTrackElement>(SPF_TRACK_SELECTOR);\n for (const el of elements) {\n el.remove();\n }\n}\n\n/**\n * Apply a selection to a `TextTrackList` by setting each subtitle/caption\n * track's `mode` to `'showing'` if its `id` matches `selectedId` and\n * `'disabled'` otherwise. Tracks of other kinds (chapters, metadata,\n * descriptions) are left untouched — they may be owned by the host page.\n */\nexport function syncTextTrackModes(textTracks: TextTrackList, selectedId: string | undefined): void {\n for (let i = 0; i < textTracks.length; i++) {\n const track = textTracks[i]!;\n if (!isCaptionOrSubtitleTrack(track)) continue;\n track.mode = track.id === selectedId ? 'showing' : 'disabled';\n }\n}\n"],"mappings":";;;;;;;AASA,MAAM,qBAAqB;;;;;;;;;;;AAY3B,SAAgB,0BACd,cACA,iBACM;CACN,KAAK,MAAM,cAAc,iBAAiB;EACxC,MAAM,KAAK,SAAS,cAAc,OAAO;EACzC,GAAG,KAAK,WAAW;EACnB,GAAG,OAAO,WAAW;EACrB,GAAG,QAAQ,WAAW;EACtB,GAAG,gBAAgB,kBAAkB,IAAI;EACzC,IAAI,WAAW,UAAU,GAAG,UAAU,WAAW;EAOjD,aAAa,YAAY,EAAE;CAC7B;AACF;;;;;;;;AASA,SAAgB,kCAAkC,cAAkE;CAClH,MAAM,WAAW,aAAa,iBAAmC,kBAAkB;CACnF,KAAK,MAAM,MAAM,UAAU;EACzB,MAAM,QAAQ,GAAG;EACjB,IAAI,MAAM,SAAS,aAAa,yBAAyB,KAAK,GAC5D,OAAO;CAEX;AAEF;;;;;;AAOA,SAAgB,kCAAkC,cAAsC;CACtF,MAAM,WAAW,aAAa,iBAAmC,kBAAkB;CACnF,KAAK,MAAM,MAAM,UACf,GAAG,OAAO;AAEd;;;;;;;AAQA,SAAgB,mBAAmB,YAA2B,YAAsC;CAClG,KAAK,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;EAC1C,MAAM,QAAQ,WAAW;EACzB,IAAI,CAAC,yBAAyB,KAAK,GAAG;EACtC,MAAM,OAAO,MAAM,OAAO,aAAa,YAAY;CACrD;AACF"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/media/errors.ts
|
|
2
|
+
/** SVTA 1 [Media Content] 004 — the video is in a format we can't play (e.g. an MPEG-TS container). */
|
|
3
|
+
const SVTA_UNSUPPORTED_VIDEO_FORMAT = 1004;
|
|
4
|
+
/** SVTA 1 [Media Content] 005 — the audio counterpart of {@link SVTA_UNSUPPORTED_VIDEO_FORMAT}. */
|
|
5
|
+
const SVTA_UNSUPPORTED_AUDIO_FORMAT = 1005;
|
|
6
|
+
/**
|
|
7
|
+
* SVTA 4 [Content Protection] 008 — unsupported or unavailable DRM system. Used
|
|
8
|
+
* for "this source is encrypted and we have no decryption pipeline," which is
|
|
9
|
+
* detection, not a license failure.
|
|
10
|
+
*/
|
|
11
|
+
const SVTA_UNSUPPORTED_DRM_SYSTEM = 4008;
|
|
12
|
+
/**
|
|
13
|
+
* SVTA 2 [Playback] 011 — no video track the environment can play. For a source
|
|
14
|
+
* that *has* video renditions where every one was excluded as unplayable.
|
|
15
|
+
*/
|
|
16
|
+
const SVTA_NO_SUPPORTED_VIDEO_TRACK = 2011;
|
|
17
|
+
/** SVTA 2 [Playback] 012 — the audio counterpart of {@link SVTA_NO_SUPPORTED_VIDEO_TRACK}. */
|
|
18
|
+
const SVTA_NO_SUPPORTED_AUDIO_TRACK = 2012;
|
|
19
|
+
/**
|
|
20
|
+
* SVTA 99 [Custom] 001 — this engine has no pipeline for something the source
|
|
21
|
+
* requires, so the source is unplayable *here* rather than broken.
|
|
22
|
+
*
|
|
23
|
+
* Custom rather than standard because the standard codes available describe
|
|
24
|
+
* either narrower or wider things. The causes (1004/1005 unsupported format,
|
|
25
|
+
* 4008 unsupported DRM) say what one rendition hit; the verdicts (2011/2012 no
|
|
26
|
+
* supported track) say a type emptied without saying why it's unfixable. And
|
|
27
|
+
* 2039 "Manifest feature unsupported" covers features that are unsupported but
|
|
28
|
+
* still *playable* — LL-HLS degrading to standard live is a 2039 — so
|
|
29
|
+
* overloading it for a fatal condition would make it useless for the notices it
|
|
30
|
+
* belongs on.
|
|
31
|
+
*
|
|
32
|
+
* Index `001`: the spec defines only `99000` (Unknown) for the custom category
|
|
33
|
+
* and leaves the rest to the publisher, so this is the first code we define.
|
|
34
|
+
*
|
|
35
|
+
* Five digits, and deliberately not special-cased anywhere: {@link svtaCategory}
|
|
36
|
+
* and {@link svtaIndex} decompose it correctly by arithmetic alone, because
|
|
37
|
+
* every standard category is below `8000` and custom starts at `99000`.
|
|
38
|
+
*/
|
|
39
|
+
const SVTA_UNSUPPORTED_PLAYBACK_FEATURE = 99001;
|
|
40
|
+
/**
|
|
41
|
+
* The error's domain — `code / 1000`, per the spec's "divide by one thousand to
|
|
42
|
+
* obtain the error category". Works uniformly across the four-digit native form
|
|
43
|
+
* and the five-digit form embedding an external standard: `"03404"` is
|
|
44
|
+
* numerically 3404, which decomposes identically. That also makes a numeric code
|
|
45
|
+
* immune to the spec's inconsistent zero-padding (§Approach writes a
|
|
46
|
+
* category-unknown network error as `"0300"` where the error index implies
|
|
47
|
+
* category 3 / index 000).
|
|
48
|
+
*/
|
|
49
|
+
function svtaCategory(code) {
|
|
50
|
+
return Math.floor(code / 1e3);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The specific error within its category — `code % 1000`. For a five-digit code
|
|
54
|
+
* this is the embedded external value (an HTTP status, a VAST code).
|
|
55
|
+
*/
|
|
56
|
+
function svtaIndex(code) {
|
|
57
|
+
return code % 1e3;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { SVTA_NO_SUPPORTED_AUDIO_TRACK, SVTA_NO_SUPPORTED_VIDEO_TRACK, SVTA_UNSUPPORTED_AUDIO_FORMAT, SVTA_UNSUPPORTED_DRM_SYSTEM, SVTA_UNSUPPORTED_PLAYBACK_FEATURE, SVTA_UNSUPPORTED_VIDEO_FORMAT, svtaCategory, svtaIndex };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../../../src/media/errors.ts"],"sourcesContent":["/**\n * SVTA 2070 (Standardized Error Codes) — the vocabulary for identifying a\n * playback failure or notice, independent of how it's transported or who\n * decides what to do about it.\n *\n * A code is a single integer: the leading digit(s) are the **category** (the\n * error's domain) and the trailing three are the **index** (the specific error\n * within it). Four digits for natively-defined errors, five when an external\n * standard is embedded — `\"03404\"` is an HTTP 404 under the network category.\n * Categories are `0` unknown, `1` media content, `2` playback, `3` network,\n * `4` content protection, `5` accessibility, `6` remote play, `7` advertising,\n * `99` custom.\n *\n * Two properties of the spec shape the types here:\n *\n * - **Severity is deliberately not part of a code.** Per §Approach, \"impact\n * varies with player implementation, breaking the consistency of a specific\n * error mapping to single code.\" So an error carries no fatal flag — whether a\n * condition is fatal depends on the composition observing it, and is decided\n * downstream.\n * - **Reporting is partial and stacked** (Principles 5–6). Errors are reported\n * as encountered, most of them non-fatal, and the *sequence* carries causation\n * a single value can't.\n *\n * See `internal/design/spf/features/errors.md`.\n */\n\n/**\n * A reported condition, identified by its SVTA code.\n *\n * Named for the spec rather than the engine because the vocabulary is\n * format- and player-neutral. Not `MediaError` — that name belongs to the\n * `@videojs/media` DOM-facing class this eventually maps *onto*, and the mapping\n * is the point at which severity and user-facing text get decided.\n */\nexport interface SvtaError {\n /** The SVTA code — see {@link svtaCategory} / {@link svtaIndex}. */\n code: number;\n /** Engineer-facing detail. Optional; the code is the identity. */\n message?: string;\n /** Reporter-specific context (track type, url, the constraint that fired). */\n data?: unknown;\n}\n\n/** SVTA 1 [Media Content] 004 — the video is in a format we can't play (e.g. an MPEG-TS container). */\nexport const SVTA_UNSUPPORTED_VIDEO_FORMAT = 1004;\n\n/** SVTA 1 [Media Content] 005 — the audio counterpart of {@link SVTA_UNSUPPORTED_VIDEO_FORMAT}. */\nexport const SVTA_UNSUPPORTED_AUDIO_FORMAT = 1005;\n\n/**\n * SVTA 4 [Content Protection] 008 — unsupported or unavailable DRM system. Used\n * for \"this source is encrypted and we have no decryption pipeline,\" which is\n * detection, not a license failure.\n */\nexport const SVTA_UNSUPPORTED_DRM_SYSTEM = 4008;\n\n/**\n * SVTA 2 [Playback] 011 — no video track the environment can play. For a source\n * that *has* video renditions where every one was excluded as unplayable.\n */\nexport const SVTA_NO_SUPPORTED_VIDEO_TRACK = 2011;\n\n/** SVTA 2 [Playback] 012 — the audio counterpart of {@link SVTA_NO_SUPPORTED_VIDEO_TRACK}. */\nexport const SVTA_NO_SUPPORTED_AUDIO_TRACK = 2012;\n\n/**\n * SVTA 99 [Custom] 001 — this engine has no pipeline for something the source\n * requires, so the source is unplayable *here* rather than broken.\n *\n * Custom rather than standard because the standard codes available describe\n * either narrower or wider things. The causes (1004/1005 unsupported format,\n * 4008 unsupported DRM) say what one rendition hit; the verdicts (2011/2012 no\n * supported track) say a type emptied without saying why it's unfixable. And\n * 2039 \"Manifest feature unsupported\" covers features that are unsupported but\n * still *playable* — LL-HLS degrading to standard live is a 2039 — so\n * overloading it for a fatal condition would make it useless for the notices it\n * belongs on.\n *\n * Index `001`: the spec defines only `99000` (Unknown) for the custom category\n * and leaves the rest to the publisher, so this is the first code we define.\n *\n * Five digits, and deliberately not special-cased anywhere: {@link svtaCategory}\n * and {@link svtaIndex} decompose it correctly by arithmetic alone, because\n * every standard category is below `8000` and custom starts at `99000`.\n */\nexport const SVTA_UNSUPPORTED_PLAYBACK_FEATURE = 99001;\n\n/**\n * The error's domain — `code / 1000`, per the spec's \"divide by one thousand to\n * obtain the error category\". Works uniformly across the four-digit native form\n * and the five-digit form embedding an external standard: `\"03404\"` is\n * numerically 3404, which decomposes identically. That also makes a numeric code\n * immune to the spec's inconsistent zero-padding (§Approach writes a\n * category-unknown network error as `\"0300\"` where the error index implies\n * category 3 / index 000).\n */\nexport function svtaCategory(code: number): number {\n return Math.floor(code / 1000);\n}\n\n/**\n * The specific error within its category — `code % 1000`. For a five-digit code\n * this is the embedded external value (an HTTP status, a VAST code).\n */\nexport function svtaIndex(code: number): number {\n return code % 1000;\n}\n"],"mappings":";;AA6CA,MAAa,gCAAgC;;AAG7C,MAAa,gCAAgC;;;;;;AAO7C,MAAa,8BAA8B;;;;;AAM3C,MAAa,gCAAgC;;AAG7C,MAAa,gCAAgC;;;;;;;;;;;;;;;;;;;;;AAsB7C,MAAa,oCAAoC;;;;;;;;;;AAWjD,SAAgB,aAAa,MAAsB;CACjD,OAAO,KAAK,MAAM,OAAO,GAAI;AAC/B;;;;;AAMA,SAAgB,UAAU,MAAsB;CAC9C,OAAO,OAAO;AAChB"}
|
|
@@ -143,6 +143,6 @@ function matchTag(line, tag) {
|
|
|
143
143
|
return createAttributeList(line.slice(prefix.length));
|
|
144
144
|
}
|
|
145
145
|
//#endregion
|
|
146
|
-
export { matchTag, parseByteRange, parseCodecs, parseExtInfDuration };
|
|
146
|
+
export { createAttributeList, matchTag, parseAttributeList, parseByteRange, parseCodecs, parseExtInfDuration, parseFrameRate, parseResolution };
|
|
147
147
|
|
|
148
148
|
//# sourceMappingURL=parse-attributes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-attributes.js","names":[],"sources":["../../../../src/media/hls/parse-attributes.ts"],"sourcesContent":["import type { FrameRate } from '../types';\n\n/**\n * Parse HLS attribute list from a tag line.\n * Handles both quoted and unquoted values.\n */\nexport function parseAttributeList(line: string): Map<string, string> {\n const attributes = new Map<string, string>();\n const regex = /([A-Z0-9-]+)=(?:\"([^\"]*)\"|([^,]*))/g;\n\n for (const match of line.matchAll(regex)) {\n const key = match[1];\n const value = match[2] ?? match[3] ?? '';\n if (key) {\n attributes.set(key, value);\n }\n }\n\n return attributes;\n}\n\n/**\n * Parse RESOLUTION attribute value (WIDTHxHEIGHT).\n */\nexport function parseResolution(value: string): { width: number; height: number } | null {\n const match = /^(\\d+)x(\\d+)$/.exec(value);\n if (!match) return null;\n\n const width = Number.parseInt(match[1]!, 10);\n const height = Number.parseInt(match[2]!, 10);\n\n return { width, height };\n}\n\n/**\n * Parse FRAME-RATE attribute to rational frame rate.\n */\nexport function parseFrameRate(value: string): FrameRate | undefined {\n const fps = Number.parseFloat(value);\n if (Number.isNaN(fps) || fps <= 0) return undefined;\n\n // Common frame rates with tolerance for floating point precision\n if (Math.abs(fps - 23.976) < 0.01) {\n return { frameRateNumerator: 24000, frameRateDenominator: 1001 };\n }\n if (Math.abs(fps - 29.97) < 0.01) {\n return { frameRateNumerator: 30000, frameRateDenominator: 1001 };\n }\n if (Math.abs(fps - 59.94) < 0.01) {\n return { frameRateNumerator: 60000, frameRateDenominator: 1001 };\n }\n\n // Integer frame rates\n if (fps % 1 === 0) {\n return { frameRateNumerator: Math.round(fps) };\n }\n\n // Default: use rounded value\n return { frameRateNumerator: Math.round(fps) };\n}\n\n// Audio codec identifiers, matched case-insensitively against each CODECS\n// entry's prefix. Beyond AAC (`mp4a.*`): Dolby (`ac-3`, `ec-3`, `ac-4`), Opus,\n// FLAC (`fLaC`), DTS (`dts*`), ALAC, and Vorbis. Recognizing these is what lets\n// capability probing filter undecodable audio renditions (e.g. an AC-3 5.1\n// track on a browser without AC-3) — an unrecognized codec parses empty and is\n// treated as unprobeable, so it would never be pruned.\nconst AUDIO_CODEC_PREFIXES = ['mp4a.', 'ac-3', 'ec-3', 'ac-4', 'opus', 'flac', 'dts', 'alac', 'vorbis'];\n\n/**\n * Parse CODECS attribute into separate video and audio codecs.\n */\nexport function parseCodecs(codecs: string): { video?: string; audio?: string } {\n const parts = codecs.split(',').map((s) => s.trim());\n const result: { video?: string; audio?: string } = {};\n\n for (const codec of parts) {\n const lower = codec.toLowerCase();\n if (codec.startsWith('avc1.') || codec.startsWith('hvc1.') || codec.startsWith('hev1.')) {\n result.video = codec;\n } else if (AUDIO_CODEC_PREFIXES.some((prefix) => lower.startsWith(prefix))) {\n result.audio = codec;\n }\n }\n\n return result;\n}\n\n/**\n * Parse #EXTINF duration value.\n */\nexport function parseExtInfDuration(value: string): number {\n const durationPart = value.split(',')[0] ?? value;\n const duration = Number.parseFloat(durationPart);\n return Number.isNaN(duration) ? 0 : duration;\n}\n\n/**\n * Parse BYTERANGE attribute value.\n * Format: \"length[@offset]\"\n * If offset is omitted, it continues from the previous byte range end.\n */\nexport function parseByteRange(value: string, previousEnd?: number): { start: number; end: number } | null {\n const match = /^(\\d+)(?:@(\\d+))?$/.exec(value);\n if (!match) return null;\n\n const length = Number.parseInt(match[1]!, 10);\n if (Number.isNaN(length)) return null;\n\n let start: number;\n if (match[2] !== undefined) {\n start = Number.parseInt(match[2], 10);\n if (Number.isNaN(start)) return null;\n } else if (previousEnd !== undefined) {\n start = previousEnd;\n } else {\n return null;\n }\n\n return { start, end: start + length - 1 };\n}\n\n/**\n * AttributeList - Typed attribute access wrapper.\n */\nexport interface AttributeList {\n get: (key: string) => string | undefined;\n getInt: (key: string, defaultValue?: number) => number | undefined;\n getFloat: (key: string, defaultValue?: number) => number | undefined;\n getBool: (key: string) => boolean;\n getResolution: (key: string) => { width: number; height: number } | undefined;\n getFrameRate: (key: string) => FrameRate | undefined;\n}\n\n/**\n * Create AttributeList from raw attribute string.\n */\nexport function createAttributeList(line: string): AttributeList {\n const map = parseAttributeList(line);\n\n return {\n get(key: string): string | undefined {\n return map.get(key);\n },\n\n getInt(key: string, defaultValue?: number): number | undefined {\n const value = map.get(key);\n if (value === undefined) return defaultValue;\n const parsed = Number.parseInt(value, 10);\n return Number.isNaN(parsed) ? defaultValue : parsed;\n },\n\n getFloat(key: string, defaultValue?: number): number | undefined {\n const value = map.get(key);\n if (value === undefined) return defaultValue;\n const parsed = Number.parseFloat(value);\n return Number.isNaN(parsed) ? defaultValue : parsed;\n },\n\n getBool(key: string): boolean {\n return map.get(key) === 'YES';\n },\n\n getResolution(key: string): { width: number; height: number } | undefined {\n const value = map.get(key);\n if (!value) return undefined;\n return parseResolution(value) ?? undefined;\n },\n\n getFrameRate(key: string): FrameRate | undefined {\n const value = map.get(key);\n if (!value) return undefined;\n return parseFrameRate(value);\n },\n };\n}\n\n/**\n * Match a tag and extract its attributes.\n * Returns null if the line doesn't match the tag.\n */\nexport function matchTag(line: string, tag: string): AttributeList | null {\n const prefix = `#${tag}:`;\n if (!line.startsWith(prefix)) return null;\n return createAttributeList(line.slice(prefix.length));\n}\n"],"mappings":";;;;;AAMA,SAAgB,mBAAmB,MAAmC;CACpE,MAAM,6BAAa,IAAI,
|
|
1
|
+
{"version":3,"file":"parse-attributes.js","names":[],"sources":["../../../../src/media/hls/parse-attributes.ts"],"sourcesContent":["import type { FrameRate } from '../types';\n\n/**\n * Parse HLS attribute list from a tag line.\n * Handles both quoted and unquoted values.\n */\nexport function parseAttributeList(line: string): Map<string, string> {\n const attributes = new Map<string, string>();\n const regex = /([A-Z0-9-]+)=(?:\"([^\"]*)\"|([^,]*))/g;\n\n for (const match of line.matchAll(regex)) {\n const key = match[1];\n const value = match[2] ?? match[3] ?? '';\n if (key) {\n attributes.set(key, value);\n }\n }\n\n return attributes;\n}\n\n/**\n * Parse RESOLUTION attribute value (WIDTHxHEIGHT).\n */\nexport function parseResolution(value: string): { width: number; height: number } | null {\n const match = /^(\\d+)x(\\d+)$/.exec(value);\n if (!match) return null;\n\n const width = Number.parseInt(match[1]!, 10);\n const height = Number.parseInt(match[2]!, 10);\n\n return { width, height };\n}\n\n/**\n * Parse FRAME-RATE attribute to rational frame rate.\n */\nexport function parseFrameRate(value: string): FrameRate | undefined {\n const fps = Number.parseFloat(value);\n if (Number.isNaN(fps) || fps <= 0) return undefined;\n\n // Common frame rates with tolerance for floating point precision\n if (Math.abs(fps - 23.976) < 0.01) {\n return { frameRateNumerator: 24000, frameRateDenominator: 1001 };\n }\n if (Math.abs(fps - 29.97) < 0.01) {\n return { frameRateNumerator: 30000, frameRateDenominator: 1001 };\n }\n if (Math.abs(fps - 59.94) < 0.01) {\n return { frameRateNumerator: 60000, frameRateDenominator: 1001 };\n }\n\n // Integer frame rates\n if (fps % 1 === 0) {\n return { frameRateNumerator: Math.round(fps) };\n }\n\n // Default: use rounded value\n return { frameRateNumerator: Math.round(fps) };\n}\n\n// Audio codec identifiers, matched case-insensitively against each CODECS\n// entry's prefix. Beyond AAC (`mp4a.*`): Dolby (`ac-3`, `ec-3`, `ac-4`), Opus,\n// FLAC (`fLaC`), DTS (`dts*`), ALAC, and Vorbis. Recognizing these is what lets\n// capability probing filter undecodable audio renditions (e.g. an AC-3 5.1\n// track on a browser without AC-3) — an unrecognized codec parses empty and is\n// treated as unprobeable, so it would never be pruned.\nconst AUDIO_CODEC_PREFIXES = ['mp4a.', 'ac-3', 'ec-3', 'ac-4', 'opus', 'flac', 'dts', 'alac', 'vorbis'];\n\n/**\n * Parse CODECS attribute into separate video and audio codecs.\n */\nexport function parseCodecs(codecs: string): { video?: string; audio?: string } {\n const parts = codecs.split(',').map((s) => s.trim());\n const result: { video?: string; audio?: string } = {};\n\n for (const codec of parts) {\n const lower = codec.toLowerCase();\n if (codec.startsWith('avc1.') || codec.startsWith('hvc1.') || codec.startsWith('hev1.')) {\n result.video = codec;\n } else if (AUDIO_CODEC_PREFIXES.some((prefix) => lower.startsWith(prefix))) {\n result.audio = codec;\n }\n }\n\n return result;\n}\n\n/**\n * Parse #EXTINF duration value.\n */\nexport function parseExtInfDuration(value: string): number {\n const durationPart = value.split(',')[0] ?? value;\n const duration = Number.parseFloat(durationPart);\n return Number.isNaN(duration) ? 0 : duration;\n}\n\n/**\n * Parse BYTERANGE attribute value.\n * Format: \"length[@offset]\"\n * If offset is omitted, it continues from the previous byte range end.\n */\nexport function parseByteRange(value: string, previousEnd?: number): { start: number; end: number } | null {\n const match = /^(\\d+)(?:@(\\d+))?$/.exec(value);\n if (!match) return null;\n\n const length = Number.parseInt(match[1]!, 10);\n if (Number.isNaN(length)) return null;\n\n let start: number;\n if (match[2] !== undefined) {\n start = Number.parseInt(match[2], 10);\n if (Number.isNaN(start)) return null;\n } else if (previousEnd !== undefined) {\n start = previousEnd;\n } else {\n return null;\n }\n\n return { start, end: start + length - 1 };\n}\n\n/**\n * AttributeList - Typed attribute access wrapper.\n */\nexport interface AttributeList {\n get: (key: string) => string | undefined;\n getInt: (key: string, defaultValue?: number) => number | undefined;\n getFloat: (key: string, defaultValue?: number) => number | undefined;\n getBool: (key: string) => boolean;\n getResolution: (key: string) => { width: number; height: number } | undefined;\n getFrameRate: (key: string) => FrameRate | undefined;\n}\n\n/**\n * Create AttributeList from raw attribute string.\n */\nexport function createAttributeList(line: string): AttributeList {\n const map = parseAttributeList(line);\n\n return {\n get(key: string): string | undefined {\n return map.get(key);\n },\n\n getInt(key: string, defaultValue?: number): number | undefined {\n const value = map.get(key);\n if (value === undefined) return defaultValue;\n const parsed = Number.parseInt(value, 10);\n return Number.isNaN(parsed) ? defaultValue : parsed;\n },\n\n getFloat(key: string, defaultValue?: number): number | undefined {\n const value = map.get(key);\n if (value === undefined) return defaultValue;\n const parsed = Number.parseFloat(value);\n return Number.isNaN(parsed) ? defaultValue : parsed;\n },\n\n getBool(key: string): boolean {\n return map.get(key) === 'YES';\n },\n\n getResolution(key: string): { width: number; height: number } | undefined {\n const value = map.get(key);\n if (!value) return undefined;\n return parseResolution(value) ?? undefined;\n },\n\n getFrameRate(key: string): FrameRate | undefined {\n const value = map.get(key);\n if (!value) return undefined;\n return parseFrameRate(value);\n },\n };\n}\n\n/**\n * Match a tag and extract its attributes.\n * Returns null if the line doesn't match the tag.\n */\nexport function matchTag(line: string, tag: string): AttributeList | null {\n const prefix = `#${tag}:`;\n if (!line.startsWith(prefix)) return null;\n return createAttributeList(line.slice(prefix.length));\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { MEDIA_PLAYLIST_METADATA_KEY, getMediaPlaylistMetadata, isResolvedTrack } from "../types/index.js";
|
|
1
2
|
import { matchTag, parseByteRange, parseExtInfDuration } from "./parse-attributes.js";
|
|
2
3
|
import { resolveUrl } from "./resolve-url.js";
|
|
4
|
+
import { isUndefined } from "@videojs/utils/predicate";
|
|
5
|
+
//#region src/media/hls/parse-media-playlist.ts
|
|
6
|
+
/** MPEG-2 Transport Stream (IANA `video/MP2T`, lowercased for `isTypeSupported`). Video + audio TS — there is no `audio/mp2t`. */
|
|
7
|
+
const MPEG_TS_MIME = "video/mp2t";
|
|
8
|
+
/** Raw ADTS AAC packed-audio (HLS `.aac` segments; IANA `audio/aac`). */
|
|
9
|
+
const RAW_AAC_MIME = "audio/aac";
|
|
3
10
|
const CONTAINER_MIME_BY_EXTENSION = {
|
|
4
|
-
".ts":
|
|
5
|
-
".aac":
|
|
11
|
+
".ts": MPEG_TS_MIME,
|
|
12
|
+
".aac": RAW_AAC_MIME
|
|
6
13
|
};
|
|
7
14
|
/** The non-fMP4 container MIMEs the parser detects — all currently treated as unplayable. */
|
|
8
15
|
const NON_FMP4_CONTAINER_MIMES = new Set(Object.values(CONTAINER_MIME_BY_EXTENSION));
|
|
@@ -22,18 +29,93 @@ function containerMimeFromSegment(url) {
|
|
|
22
29
|
return dot === -1 ? void 0 : CONTAINER_MIME_BY_EXTENSION[path.slice(dot)];
|
|
23
30
|
}
|
|
24
31
|
/**
|
|
32
|
+
* Position a freshly-parsed window (whose segment `startTime`s are snapshot-
|
|
33
|
+
* local, i.e. from 0) onto the timeline established by the previous resolved
|
|
34
|
+
* snapshot. Carries the timeline forward using the media-sequence overlap and
|
|
35
|
+
* the previous window's *actual* segment durations — see
|
|
36
|
+
* `internal/design/spf/live-presentation-timeline-model.md`.
|
|
37
|
+
*
|
|
38
|
+
* - **Overlap** (`0 <= offset < previous.segments.length`): the new window's
|
|
39
|
+
* first segment is the same segment as `previous.segments[offset]`; anchor to
|
|
40
|
+
* its start (URLs checked — a mismatch warns).
|
|
41
|
+
* - **Sequence went backwards** (non-conformant): reset to the local base.
|
|
42
|
+
* - **Full turnover** (no overlap): estimate forward from the previous window's
|
|
43
|
+
* end across the unseen gap. This is the *only* place EXT-X-TARGETDURATION is
|
|
44
|
+
* used for timing — an upper-bound estimate, since the actual rolled-off
|
|
45
|
+
* durations are gone (exact recovery is the deferred PDT decision).
|
|
46
|
+
*
|
|
47
|
+
* Returns the rebased segments (the window edge is `segments[0].startTime`,
|
|
48
|
+
* derived — never stored on the track).
|
|
49
|
+
*/
|
|
50
|
+
function placeOnPreviousTimeline(previous, segments, mediaSequence, targetDuration) {
|
|
51
|
+
const prevSegments = previous.segments;
|
|
52
|
+
const localBase = segments[0]?.startTime ?? 0;
|
|
53
|
+
if (prevSegments.length === 0 || segments.length === 0) return segments;
|
|
54
|
+
const offset = mediaSequence - (getMediaPlaylistMetadata(previous)?.mediaSequence ?? 0);
|
|
55
|
+
let anchor;
|
|
56
|
+
if (offset >= 0 && offset < prevSegments.length) {
|
|
57
|
+
const overlap = prevSegments[offset];
|
|
58
|
+
if (overlap.url !== segments[0].url) console.warn(`[parseMediaPlaylist] media-sequence aligns previous[${offset}] with the new window's first segment, but URLs differ (${overlap.url} vs ${segments[0].url}); sequence numbers may be unreliable.`);
|
|
59
|
+
anchor = overlap.startTime;
|
|
60
|
+
} else if (offset < 0) {
|
|
61
|
+
console.warn(`[parseMediaPlaylist] media-sequence went backwards (offset ${offset}); resetting timeline.`);
|
|
62
|
+
anchor = localBase;
|
|
63
|
+
} else {
|
|
64
|
+
const last = prevSegments[prevSegments.length - 1];
|
|
65
|
+
const newFirst = segments[0];
|
|
66
|
+
if (last.startDate !== void 0 && newFirst.startDate !== void 0) anchor = last.startTime + (newFirst.startDate - last.startDate);
|
|
67
|
+
else {
|
|
68
|
+
anchor = last.startTime + last.duration + (offset - prevSegments.length) * targetDuration;
|
|
69
|
+
console.warn(`[parseMediaPlaylist] full window turnover (offset ${offset} >= ${prevSegments.length}); no PDT to bridge — estimating from previous end.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const shift = anchor - localBase;
|
|
73
|
+
return shift === 0 ? segments : segments.map((segment) => ({
|
|
74
|
+
...segment,
|
|
75
|
+
startTime: segment.startTime + shift
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Place a window on the frozen wall-clock anchor — the track's `startDate`
|
|
80
|
+
* (wall clock at media-time 0). The anchoring PDT-bearing segment lands at
|
|
81
|
+
* `segment.startDate − anchor`, so the window sits on the shared presentation
|
|
82
|
+
* timeline rather than a local-from-zero one (the `startDate` recomputed
|
|
83
|
+
* afterwards then reads back as the anchor — idempotent). This is the **main
|
|
84
|
+
* live path on every parse**: first resolves place against the pre-stamped
|
|
85
|
+
* shell anchor, and reloads re-place from PDT so drift can't accumulate and a
|
|
86
|
+
* long-stall turnover re-places with no overlap bridging (the HLS authoring
|
|
87
|
+
* spec's §8.1 EXTINF-accuracy rule bounds the per-reload correction to ~one
|
|
88
|
+
* video frame). Falls back to the local base when no segment carries PDT —
|
|
89
|
+
* there's nothing to anchor against (callers guard this for reloads, where the
|
|
90
|
+
* local base would reset the timeline).
|
|
91
|
+
*/
|
|
92
|
+
function placeOnAnchor(segments, anchor) {
|
|
93
|
+
const anchorSegment = segments.find((segment) => !isUndefined(segment.startDate));
|
|
94
|
+
if (!anchorSegment || isUndefined(anchorSegment.startDate)) return segments;
|
|
95
|
+
const shift = anchorSegment.startDate - anchor - anchorSegment.startTime;
|
|
96
|
+
if (shift === 0) return segments;
|
|
97
|
+
return segments.map((segment) => ({
|
|
98
|
+
...segment,
|
|
99
|
+
startTime: segment.startTime + shift
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
25
103
|
* Parse HLS media playlist and resolve track with segments.
|
|
26
104
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
105
|
+
* `previous` is what was known about this track before this parse: the
|
|
106
|
+
* partially-resolved track from the multivariant playlist on the first resolve,
|
|
107
|
+
* or the previously-resolved snapshot on a live reload. Its metadata is carried
|
|
108
|
+
* onto the result either way; when it's already resolved (has segments), its
|
|
109
|
+
* timeline is carried forward so the new window lands on a stable, advancing
|
|
110
|
+
* timeline (see {@link placeOnPreviousTimeline}).
|
|
29
111
|
*
|
|
30
112
|
* @param text - Media playlist text content
|
|
31
|
-
* @param
|
|
113
|
+
* @param previous - Prior track state (unresolved shell, or previous resolved snapshot)
|
|
32
114
|
* @returns Resolved track with segments (type inferred from input)
|
|
33
115
|
*/
|
|
34
|
-
function parseMediaPlaylist(text,
|
|
116
|
+
function parseMediaPlaylist(text, previous) {
|
|
35
117
|
const lines = text.split(/\r?\n/);
|
|
36
|
-
const baseUrl =
|
|
118
|
+
const baseUrl = previous.url;
|
|
37
119
|
const segments = [];
|
|
38
120
|
let initSegmentUrl;
|
|
39
121
|
let initSegmentByteRange;
|
|
@@ -42,10 +124,56 @@ function parseMediaPlaylist(text, unresolved) {
|
|
|
42
124
|
let currentTime = 0;
|
|
43
125
|
let segmentIndex = 0;
|
|
44
126
|
let previousByteRangeEnd;
|
|
127
|
+
let currentStartDate;
|
|
128
|
+
let targetDuration = 0;
|
|
129
|
+
let mediaSequence = 0;
|
|
130
|
+
let playlistType;
|
|
131
|
+
let endList = false;
|
|
132
|
+
let holdBack;
|
|
133
|
+
let lowLatency = false;
|
|
134
|
+
let encrypted = false;
|
|
45
135
|
for (const line of lines) {
|
|
46
136
|
const trimmed = line.trim();
|
|
47
137
|
if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
|
|
48
|
-
if (trimmed
|
|
138
|
+
if (trimmed.startsWith("#EXT-X-TARGETDURATION:")) {
|
|
139
|
+
targetDuration = Number.parseInt(trimmed.slice(22), 10) || 0;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (trimmed.startsWith("#EXT-X-MEDIA-SEQUENCE:")) {
|
|
143
|
+
mediaSequence = Number.parseInt(trimmed.slice(22), 10) || 0;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (trimmed.startsWith("#EXT-X-PROGRAM-DATE-TIME:")) {
|
|
147
|
+
const parsed = Date.parse(trimmed.slice(25).trim());
|
|
148
|
+
currentStartDate = Number.isNaN(parsed) ? currentStartDate : parsed / 1e3;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (trimmed.startsWith("#EXT-X-PLAYLIST-TYPE:")) {
|
|
152
|
+
const value = trimmed.slice(21).trim();
|
|
153
|
+
playlistType = value === "VOD" || value === "EVENT" ? value : void 0;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const key = matchTag(trimmed, "EXT-X-KEY");
|
|
157
|
+
if (key) {
|
|
158
|
+
const method = key.get("METHOD");
|
|
159
|
+
if (method !== void 0 && method !== "NONE") encrypted = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const serverControl = matchTag(trimmed, "EXT-X-SERVER-CONTROL");
|
|
163
|
+
if (serverControl) {
|
|
164
|
+
const value = serverControl.get("HOLD-BACK");
|
|
165
|
+
if (value !== void 0) {
|
|
166
|
+
const parsed = Number.parseFloat(value);
|
|
167
|
+
if (Number.isFinite(parsed) && parsed > 0) holdBack = parsed;
|
|
168
|
+
}
|
|
169
|
+
if (serverControl.get("PART-HOLD-BACK") !== void 0) lowLatency = true;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (trimmed.startsWith("#EXT-X-PART-INF") || trimmed.startsWith("#EXT-X-PART:")) {
|
|
173
|
+
lowLatency = true;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
|
|
49
177
|
const mapAttrs = matchTag(trimmed, "EXT-X-MAP");
|
|
50
178
|
if (mapAttrs) {
|
|
51
179
|
const uri = mapAttrs.get("URI");
|
|
@@ -64,14 +192,21 @@ function parseMediaPlaylist(text, unresolved) {
|
|
|
64
192
|
currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? void 0;
|
|
65
193
|
continue;
|
|
66
194
|
}
|
|
67
|
-
if (trimmed === "#EXT-X-ENDLIST")
|
|
195
|
+
if (trimmed === "#EXT-X-ENDLIST") {
|
|
196
|
+
endList = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
68
199
|
if (!trimmed.startsWith("#") && currentDuration > 0) {
|
|
69
200
|
const segment = {
|
|
70
|
-
id: `segment-${segmentIndex}`,
|
|
201
|
+
id: `segment-${mediaSequence + segmentIndex}`,
|
|
71
202
|
url: resolveUrl(trimmed, baseUrl),
|
|
72
203
|
duration: currentDuration,
|
|
73
204
|
startTime: currentTime
|
|
74
205
|
};
|
|
206
|
+
if (!isUndefined(currentStartDate)) {
|
|
207
|
+
segment.startDate = currentStartDate;
|
|
208
|
+
currentStartDate += currentDuration;
|
|
209
|
+
}
|
|
75
210
|
if (currentByteRange) {
|
|
76
211
|
segment.byteRange = currentByteRange;
|
|
77
212
|
previousByteRangeEnd = currentByteRange.end + 1;
|
|
@@ -83,23 +218,44 @@ function parseMediaPlaylist(text, unresolved) {
|
|
|
83
218
|
currentByteRange = void 0;
|
|
84
219
|
}
|
|
85
220
|
}
|
|
86
|
-
const
|
|
87
|
-
const
|
|
221
|
+
const complete = endList || playlistType === "VOD";
|
|
222
|
+
const anchor = previous.startDate;
|
|
223
|
+
const hasPdt = segments.some((segment) => !isUndefined(segment.startDate));
|
|
224
|
+
const placed = !isUndefined(anchor) && hasPdt ? placeOnAnchor(segments, anchor) : isResolvedTrack(previous) ? placeOnPreviousTimeline(previous, segments, mediaSequence, targetDuration) : segments;
|
|
225
|
+
const lastPlaced = placed[placed.length - 1];
|
|
226
|
+
const placedEnd = lastPlaced ? lastPlaced.startTime + lastPlaced.duration : 0;
|
|
227
|
+
const trackDuration = complete ? placedEnd : Number.POSITIVE_INFINITY;
|
|
228
|
+
const anchorSegment = placed.find((segment) => !isUndefined(segment.startDate));
|
|
229
|
+
const startDate = anchorSegment && !isUndefined(anchorSegment.startDate) ? anchorSegment.startDate - anchorSegment.startTime : void 0;
|
|
230
|
+
const initialization = previous.type === "text" && !initSegmentUrl ? void 0 : initSegmentUrl ? {
|
|
88
231
|
url: initSegmentUrl,
|
|
89
232
|
...initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}
|
|
90
233
|
} : { url: "" };
|
|
91
|
-
const detectedContainer = initSegmentUrl ? void 0 : containerMimeFromSegment(
|
|
92
|
-
const mimeType =
|
|
234
|
+
const detectedContainer = initSegmentUrl ? void 0 : containerMimeFromSegment(placed[0]?.url);
|
|
235
|
+
const mimeType = previous.type !== "text" && detectedContainer ? detectedContainer : previous.mimeType;
|
|
93
236
|
return {
|
|
94
|
-
...
|
|
237
|
+
...previous,
|
|
95
238
|
mimeType,
|
|
96
239
|
startTime: 0,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
240
|
+
startDate,
|
|
241
|
+
duration: trackDuration,
|
|
242
|
+
segments: placed,
|
|
243
|
+
initialization,
|
|
244
|
+
metadata: {
|
|
245
|
+
...previous.metadata,
|
|
246
|
+
[MEDIA_PLAYLIST_METADATA_KEY]: {
|
|
247
|
+
targetDuration,
|
|
248
|
+
mediaSequence,
|
|
249
|
+
playlistType,
|
|
250
|
+
lowLatency,
|
|
251
|
+
endList,
|
|
252
|
+
holdBack,
|
|
253
|
+
encrypted
|
|
254
|
+
}
|
|
255
|
+
}
|
|
100
256
|
};
|
|
101
257
|
}
|
|
102
258
|
//#endregion
|
|
103
|
-
export { NON_FMP4_CONTAINER_MIMES, parseMediaPlaylist };
|
|
259
|
+
export { MPEG_TS_MIME, NON_FMP4_CONTAINER_MIMES, RAW_AAC_MIME, parseMediaPlaylist };
|
|
104
260
|
|
|
105
261
|
//# sourceMappingURL=parse-media-playlist.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-media-playlist.js","names":[],"sources":["../../../../src/media/hls/parse-media-playlist.ts"],"sourcesContent":["import type {\n AudioTrack,\n PartiallyResolvedAudioTrack,\n PartiallyResolvedTextTrack,\n PartiallyResolvedTrack,\n PartiallyResolvedVideoTrack,\n Segment,\n TextTrack,\n VideoTrack,\n} from '../types';\nimport { matchTag, parseByteRange, parseExtInfDuration } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/** MPEG-2 Transport Stream (IANA `video/MP2T`, lowercased for `isTypeSupported`). Video + audio TS — there is no `audio/mp2t`. */\nexport const MPEG_TS_MIME = 'video/mp2t';\n/** Raw ADTS AAC packed-audio (HLS `.aac` segments; IANA `audio/aac`). */\nexport const RAW_AAC_MIME = 'audio/aac';\n\n// Non-fMP4 container MIMEs keyed by segment file extension. fMP4 (the MSE\n// default) always carries an EXT-X-MAP init segment, so a media playlist with\n// no init segment and one of these extensions is a non-fMP4 rendition,\n// relabeled from the fMP4 default. Extend with `.mp3` → 'audio/mpeg' etc.\nconst CONTAINER_MIME_BY_EXTENSION: Record<string, string> = {\n '.ts': MPEG_TS_MIME,\n '.aac': RAW_AAC_MIME,\n};\n\n/** The non-fMP4 container MIMEs the parser detects — all currently treated as unplayable. */\nexport const NON_FMP4_CONTAINER_MIMES = new Set(Object.values(CONTAINER_MIME_BY_EXTENSION));\n\n/**\n * Non-fMP4 container MIME for a (resolved, absolute) segment URL, by file\n * extension, ignoring the query string. `undefined` for fMP4 / unrecognized.\n */\nfunction containerMimeFromSegment(url: string | undefined): string | undefined {\n if (!url) return undefined;\n let path: string;\n try {\n path = new URL(url).pathname.toLowerCase();\n } catch {\n path = url.toLowerCase().split('?')[0] ?? '';\n }\n const dot = path.lastIndexOf('.');\n return dot === -1 ? undefined : CONTAINER_MIME_BY_EXTENSION[path.slice(dot)];\n}\n\n/**\n * Resolve unresolved track type to its resolved equivalent.\n */\ntype ResolveTrack<T> = T extends PartiallyResolvedVideoTrack\n ? VideoTrack\n : T extends PartiallyResolvedAudioTrack\n ? AudioTrack\n : T extends PartiallyResolvedTextTrack\n ? TextTrack\n : never;\n\n/**\n * Parse HLS media playlist and resolve track with segments.\n *\n * Takes an unresolved track (from multivariant playlist) and media playlist text,\n * returns a HAM-compliant resolved track with segments.\n *\n * @param text - Media playlist text content\n * @param unresolved - Unresolved track from parseMultivariantPlaylist\n * @returns Resolved track with segments (type inferred from input)\n */\nexport function parseMediaPlaylist<T extends PartiallyResolvedTrack>(\n text: string,\n unresolved: T | ResolveTrack<T>\n): ResolveTrack<T> {\n const lines = text.split(/\\r?\\n/);\n\n // Segments and resources resolve relative to media playlist URL (per HLS spec)\n const baseUrl = unresolved.url;\n\n // Parse playlist\n const segments: Segment[] = [];\n let initSegmentUrl: string | undefined;\n let initSegmentByteRange: { start: number; end: number } | undefined;\n\n let currentDuration = 0;\n let currentByteRange: { start: number; end: number } | undefined;\n let currentTime = 0;\n let segmentIndex = 0;\n let previousByteRangeEnd: number | undefined;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#EXT'))) {\n continue;\n }\n\n if (\n trimmed === '#EXTM3U' ||\n trimmed.startsWith('#EXT-X-VERSION:') ||\n trimmed.startsWith('#EXT-X-TARGETDURATION:') ||\n trimmed.startsWith('#EXT-X-PLAYLIST-TYPE:') ||\n trimmed.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')\n ) {\n continue;\n }\n\n // #EXT-X-MAP - Init segment\n const mapAttrs = matchTag(trimmed, 'EXT-X-MAP');\n if (mapAttrs) {\n const uri = mapAttrs.get('URI');\n if (uri) {\n initSegmentUrl = resolveUrl(uri, baseUrl);\n const byteRangeStr = mapAttrs.get('BYTERANGE');\n if (byteRangeStr) {\n initSegmentByteRange = parseByteRange(byteRangeStr, 0) ?? undefined;\n }\n }\n continue;\n }\n\n // #EXTINF - Segment duration\n if (trimmed.startsWith('#EXTINF:')) {\n currentDuration = parseExtInfDuration(trimmed.slice(8));\n continue;\n }\n\n // #EXT-X-BYTERANGE - Segment byte range\n if (trimmed.startsWith('#EXT-X-BYTERANGE:')) {\n currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? undefined;\n continue;\n }\n\n if (trimmed === '#EXT-X-ENDLIST') {\n continue;\n }\n\n // Segment URI\n if (!trimmed.startsWith('#') && currentDuration > 0) {\n const segment: Segment = {\n id: `segment-${segmentIndex}`,\n url: resolveUrl(trimmed, baseUrl),\n duration: currentDuration,\n startTime: currentTime,\n };\n\n if (currentByteRange) {\n segment.byteRange = currentByteRange;\n previousByteRangeEnd = currentByteRange.end + 1;\n } else {\n previousByteRangeEnd = undefined;\n }\n\n segments.push(segment);\n currentTime += currentDuration;\n segmentIndex++;\n\n currentDuration = 0;\n currentByteRange = undefined;\n }\n }\n\n const totalDuration = currentTime;\n\n // Build initialization (VTT may not have init segment)\n const initialization =\n unresolved.type === 'text' && !initSegmentUrl\n ? undefined\n : initSegmentUrl\n ? { url: initSegmentUrl, ...(initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}) }\n : { url: '' };\n\n // Container detection: fMP4 always carries an EXT-X-MAP init segment, so its\n // absence plus a recognized non-fMP4 segment extension (`.ts` → MPEG-TS,\n // `.aac` → raw ADTS AAC) marks a non-fMP4 rendition (high-precision — never\n // trips on fMP4, which mandates the map). Relabel from the fMP4 default\n // `video/mp4` / `audio/mp4` to the container MIME so capability probing prunes\n // it (these containers are currently treated as unplayable; see `canPlayTrack`).\n const detectedContainer = initSegmentUrl ? undefined : containerMimeFromSegment(segments[0]?.url);\n const mimeType = unresolved.type !== 'text' && detectedContainer ? detectedContainer : unresolved.mimeType;\n\n // Generic resolution: All type-specific fields already on unresolved track from P1\n // Just add parsed properties (startTime, duration, segments, initialization)\n return {\n ...unresolved,\n mimeType,\n startTime: 0,\n duration: totalDuration,\n segments,\n initialization,\n } as unknown as ResolveTrack<T>;\n}\n"],"mappings":";;AAsBA,MAAM,8BAAsD;CAC1D,OAT0B;CAU1B,QAR0B;CAS3B;;AAGD,MAAa,2BAA2B,IAAI,IAAI,OAAO,OAAO,4BAA4B,CAAC;;;;;AAM3F,SAAS,yBAAyB,KAA6C;AAC7E,KAAI,CAAC,IAAK,QAAO,KAAA;CACjB,IAAI;AACJ,KAAI;AACF,SAAO,IAAI,IAAI,IAAI,CAAC,SAAS,aAAa;SACpC;AACN,SAAO,IAAI,aAAa,CAAC,MAAM,IAAI,CAAC,MAAM;;CAE5C,MAAM,MAAM,KAAK,YAAY,IAAI;AACjC,QAAO,QAAQ,KAAK,KAAA,IAAY,4BAA4B,KAAK,MAAM,IAAI;;;;;;;;;;;;AAwB7E,SAAgB,mBACd,MACA,YACiB;CACjB,MAAM,QAAQ,KAAK,MAAM,QAAQ;CAGjC,MAAM,UAAU,WAAW;CAG3B,MAAM,WAAsB,EAAE;CAC9B,IAAI;CACJ,IAAI;CAEJ,IAAI,kBAAkB;CACtB,IAAI;CACJ,IAAI,cAAc;CAClB,IAAI,eAAe;CACnB,IAAI;AAEJ,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,KAAK,MAAM;AAE3B,MAAI,CAAC,WAAY,QAAQ,WAAW,IAAI,IAAI,CAAC,QAAQ,WAAW,OAAO,CACrE;AAGF,MACE,YAAY,aACZ,QAAQ,WAAW,kBAAkB,IACrC,QAAQ,WAAW,yBAAyB,IAC5C,QAAQ,WAAW,wBAAwB,IAC3C,QAAQ,WAAW,8BAA8B,CAEjD;EAIF,MAAM,WAAW,SAAS,SAAS,YAAY;AAC/C,MAAI,UAAU;GACZ,MAAM,MAAM,SAAS,IAAI,MAAM;AAC/B,OAAI,KAAK;AACP,qBAAiB,WAAW,KAAK,QAAQ;IACzC,MAAM,eAAe,SAAS,IAAI,YAAY;AAC9C,QAAI,aACF,wBAAuB,eAAe,cAAc,EAAE,IAAI,KAAA;;AAG9D;;AAIF,MAAI,QAAQ,WAAW,WAAW,EAAE;AAClC,qBAAkB,oBAAoB,QAAQ,MAAM,EAAE,CAAC;AACvD;;AAIF,MAAI,QAAQ,WAAW,oBAAoB,EAAE;AAC3C,sBAAmB,eAAe,QAAQ,MAAM,GAAG,EAAE,qBAAqB,IAAI,KAAA;AAC9E;;AAGF,MAAI,YAAY,iBACd;AAIF,MAAI,CAAC,QAAQ,WAAW,IAAI,IAAI,kBAAkB,GAAG;GACnD,MAAM,UAAmB;IACvB,IAAI,WAAW;IACf,KAAK,WAAW,SAAS,QAAQ;IACjC,UAAU;IACV,WAAW;IACZ;AAED,OAAI,kBAAkB;AACpB,YAAQ,YAAY;AACpB,2BAAuB,iBAAiB,MAAM;SAE9C,wBAAuB,KAAA;AAGzB,YAAS,KAAK,QAAQ;AACtB,kBAAe;AACf;AAEA,qBAAkB;AAClB,sBAAmB,KAAA;;;CAIvB,MAAM,gBAAgB;CAGtB,MAAM,iBACJ,WAAW,SAAS,UAAU,CAAC,iBAC3B,KAAA,IACA,iBACE;EAAE,KAAK;EAAgB,GAAI,uBAAuB,EAAE,WAAW,sBAAsB,GAAG,EAAE;EAAG,GAC7F,EAAE,KAAK,IAAI;CAQnB,MAAM,oBAAoB,iBAAiB,KAAA,IAAY,yBAAyB,SAAS,IAAI,IAAI;CACjG,MAAM,WAAW,WAAW,SAAS,UAAU,oBAAoB,oBAAoB,WAAW;AAIlG,QAAO;EACL,GAAG;EACH;EACA,WAAW;EACX,UAAU;EACV;EACA;EACD"}
|
|
1
|
+
{"version":3,"file":"parse-media-playlist.js","names":[],"sources":["../../../../src/media/hls/parse-media-playlist.ts"],"sourcesContent":["import { isUndefined } from '@videojs/utils/predicate';\nimport {\n type AudioTrack,\n getMediaPlaylistMetadata,\n isResolvedTrack,\n MEDIA_PLAYLIST_METADATA_KEY,\n type MediaPlaylistMetadata,\n type PartiallyResolvedAudioTrack,\n type PartiallyResolvedTextTrack,\n type PartiallyResolvedTrack,\n type PartiallyResolvedVideoTrack,\n type ResolvedTrack,\n type Segment,\n type TextTrack,\n type VideoTrack,\n} from '../types';\nimport { matchTag, parseByteRange, parseExtInfDuration } from './parse-attributes';\nimport { resolveUrl } from './resolve-url';\n\n/** MPEG-2 Transport Stream (IANA `video/MP2T`, lowercased for `isTypeSupported`). Video + audio TS — there is no `audio/mp2t`. */\nexport const MPEG_TS_MIME = 'video/mp2t';\n/** Raw ADTS AAC packed-audio (HLS `.aac` segments; IANA `audio/aac`). */\nexport const RAW_AAC_MIME = 'audio/aac';\n\n// Non-fMP4 container MIMEs keyed by segment file extension. fMP4 (the MSE\n// default) always carries an EXT-X-MAP init segment, so a media playlist with\n// no init segment and one of these extensions is a non-fMP4 rendition,\n// relabeled from the fMP4 default. Extend with `.mp3` → 'audio/mpeg' etc.\nconst CONTAINER_MIME_BY_EXTENSION: Record<string, string> = {\n '.ts': MPEG_TS_MIME,\n '.aac': RAW_AAC_MIME,\n};\n\n/** The non-fMP4 container MIMEs the parser detects — all currently treated as unplayable. */\nexport const NON_FMP4_CONTAINER_MIMES = new Set(Object.values(CONTAINER_MIME_BY_EXTENSION));\n\n/**\n * Non-fMP4 container MIME for a (resolved, absolute) segment URL, by file\n * extension, ignoring the query string. `undefined` for fMP4 / unrecognized.\n */\nfunction containerMimeFromSegment(url: string | undefined): string | undefined {\n if (!url) return undefined;\n let path: string;\n try {\n path = new URL(url).pathname.toLowerCase();\n } catch {\n path = url.toLowerCase().split('?')[0] ?? '';\n }\n const dot = path.lastIndexOf('.');\n return dot === -1 ? undefined : CONTAINER_MIME_BY_EXTENSION[path.slice(dot)];\n}\n\n/**\n * Resolve unresolved track type to its resolved equivalent.\n */\ntype ResolveTrack<T> = T extends PartiallyResolvedVideoTrack\n ? VideoTrack\n : T extends PartiallyResolvedAudioTrack\n ? AudioTrack\n : T extends PartiallyResolvedTextTrack\n ? TextTrack\n : never;\n\n/**\n * Position a freshly-parsed window (whose segment `startTime`s are snapshot-\n * local, i.e. from 0) onto the timeline established by the previous resolved\n * snapshot. Carries the timeline forward using the media-sequence overlap and\n * the previous window's *actual* segment durations — see\n * `internal/design/spf/live-presentation-timeline-model.md`.\n *\n * - **Overlap** (`0 <= offset < previous.segments.length`): the new window's\n * first segment is the same segment as `previous.segments[offset]`; anchor to\n * its start (URLs checked — a mismatch warns).\n * - **Sequence went backwards** (non-conformant): reset to the local base.\n * - **Full turnover** (no overlap): estimate forward from the previous window's\n * end across the unseen gap. This is the *only* place EXT-X-TARGETDURATION is\n * used for timing — an upper-bound estimate, since the actual rolled-off\n * durations are gone (exact recovery is the deferred PDT decision).\n *\n * Returns the rebased segments (the window edge is `segments[0].startTime`,\n * derived — never stored on the track).\n */\nfunction placeOnPreviousTimeline(\n previous: ResolvedTrack,\n segments: Segment[],\n mediaSequence: number,\n targetDuration: number\n): Segment[] {\n const prevSegments = previous.segments;\n const localBase = segments[0]?.startTime ?? 0;\n\n if (prevSegments.length === 0 || segments.length === 0) {\n return segments;\n }\n\n const prevMediaSequence = getMediaPlaylistMetadata(previous)?.mediaSequence ?? 0;\n const offset = mediaSequence - prevMediaSequence;\n\n let anchor: number;\n if (offset >= 0 && offset < prevSegments.length) {\n const overlap = prevSegments[offset]!;\n if (overlap.url !== segments[0]!.url) {\n console.warn(\n `[parseMediaPlaylist] media-sequence aligns previous[${offset}] with the new window's first segment, ` +\n `but URLs differ (${overlap.url} vs ${segments[0]!.url}); sequence numbers may be unreliable.`\n );\n }\n anchor = overlap.startTime;\n } else if (offset < 0) {\n console.warn(`[parseMediaPlaylist] media-sequence went backwards (offset ${offset}); resetting timeline.`);\n anchor = localBase;\n } else {\n const last = prevSegments[prevSegments.length - 1]!;\n const newFirst = segments[0]!;\n if (last.startDate !== undefined && newFirst.startDate !== undefined) {\n // PDT bridges the gap exactly — the spec-consistent cross-reload reference,\n // immune to the target-duration over-estimate when actual segment durations\n // differ from the declared ceiling. (`startDate` is intrinsic PDT, unaffected\n // by the new window's local positioning.)\n anchor = last.startTime + (newFirst.startDate - last.startDate);\n } else {\n anchor = last.startTime + last.duration + (offset - prevSegments.length) * targetDuration;\n console.warn(\n `[parseMediaPlaylist] full window turnover (offset ${offset} >= ${prevSegments.length}); ` +\n 'no PDT to bridge — estimating from previous end.'\n );\n }\n }\n\n const shift = anchor - localBase;\n return shift === 0 ? segments : segments.map((segment) => ({ ...segment, startTime: segment.startTime + shift }));\n}\n\n/**\n * Place a window on the frozen wall-clock anchor — the track's `startDate`\n * (wall clock at media-time 0). The anchoring PDT-bearing segment lands at\n * `segment.startDate − anchor`, so the window sits on the shared presentation\n * timeline rather than a local-from-zero one (the `startDate` recomputed\n * afterwards then reads back as the anchor — idempotent). This is the **main\n * live path on every parse**: first resolves place against the pre-stamped\n * shell anchor, and reloads re-place from PDT so drift can't accumulate and a\n * long-stall turnover re-places with no overlap bridging (the HLS authoring\n * spec's §8.1 EXTINF-accuracy rule bounds the per-reload correction to ~one\n * video frame). Falls back to the local base when no segment carries PDT —\n * there's nothing to anchor against (callers guard this for reloads, where the\n * local base would reset the timeline).\n */\nfunction placeOnAnchor(segments: Segment[], anchor: number): Segment[] {\n const anchorSegment = segments.find((segment) => !isUndefined(segment.startDate));\n if (!anchorSegment || isUndefined(anchorSegment.startDate)) {\n return segments;\n }\n\n const shift = anchorSegment.startDate - anchor - anchorSegment.startTime;\n if (shift === 0) {\n return segments;\n }\n\n return segments.map((segment) => ({ ...segment, startTime: segment.startTime + shift }));\n}\n\n/**\n * Parse HLS media playlist and resolve track with segments.\n *\n * `previous` is what was known about this track before this parse: the\n * partially-resolved track from the multivariant playlist on the first resolve,\n * or the previously-resolved snapshot on a live reload. Its metadata is carried\n * onto the result either way; when it's already resolved (has segments), its\n * timeline is carried forward so the new window lands on a stable, advancing\n * timeline (see {@link placeOnPreviousTimeline}).\n *\n * @param text - Media playlist text content\n * @param previous - Prior track state (unresolved shell, or previous resolved snapshot)\n * @returns Resolved track with segments (type inferred from input)\n */\nexport function parseMediaPlaylist<T extends PartiallyResolvedTrack>(\n text: string,\n previous: T | ResolveTrack<T>\n): ResolveTrack<T> {\n const lines = text.split(/\\r?\\n/);\n\n // Segments and resources resolve relative to media playlist URL (per HLS spec)\n const baseUrl = previous.url;\n\n // Parse playlist\n const segments: Segment[] = [];\n let initSegmentUrl: string | undefined;\n let initSegmentByteRange: { start: number; end: number } | undefined;\n\n let currentDuration = 0;\n let currentByteRange: { start: number; end: number } | undefined;\n let currentTime = 0;\n let segmentIndex = 0;\n let previousByteRangeEnd: number | undefined;\n // Absolute wall-clock of the next segment's first sample, in epoch seconds.\n // Seeded by an explicit `#EXT-X-PROGRAM-DATE-TIME` (which re-anchors, e.g.\n // across a discontinuity) and advanced by each segment's duration so segments\n // without their own tag are interpolated forward (per RFC 8216).\n let currentStartDate: number | undefined;\n\n // Playlist-level metadata (surfaced for live reload pacing / merge / termination).\n let targetDuration = 0;\n let mediaSequence = 0;\n let playlistType: 'VOD' | 'EVENT' | undefined;\n let endList = false;\n let holdBack: number | undefined;\n // Low-Latency HLS delivery. Any of the partial-segment tags is enough: the\n // parser ignores parts and the loader fetches whole segments, so this records\n // that the publisher configured LL-HLS, not that we honour it.\n let lowLatency = false;\n // Any EXT-X-KEY with a real METHOD makes the rendition encrypted. Sticky: a\n // clear lead (METHOD=NONE first, a real key later) still counts, since we can\n // only report whether decryption is needed at all.\n let encrypted = false;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#EXT'))) {\n continue;\n }\n\n if (trimmed.startsWith('#EXT-X-TARGETDURATION:')) {\n targetDuration = Number.parseInt(trimmed.slice('#EXT-X-TARGETDURATION:'.length), 10) || 0;\n continue;\n }\n\n if (trimmed.startsWith('#EXT-X-MEDIA-SEQUENCE:')) {\n mediaSequence = Number.parseInt(trimmed.slice('#EXT-X-MEDIA-SEQUENCE:'.length), 10) || 0;\n continue;\n }\n\n if (trimmed.startsWith('#EXT-X-PROGRAM-DATE-TIME:')) {\n const parsed = Date.parse(trimmed.slice('#EXT-X-PROGRAM-DATE-TIME:'.length).trim());\n currentStartDate = Number.isNaN(parsed) ? currentStartDate : parsed / 1000;\n continue;\n }\n\n if (trimmed.startsWith('#EXT-X-PLAYLIST-TYPE:')) {\n const value = trimmed.slice('#EXT-X-PLAYLIST-TYPE:'.length).trim();\n playlistType = value === 'VOD' || value === 'EVENT' ? value : undefined;\n continue;\n }\n\n const key = matchTag(trimmed, 'EXT-X-KEY');\n if (key) {\n const method = key.get('METHOD');\n if (method !== undefined && method !== 'NONE') encrypted = true;\n continue;\n }\n\n // #EXT-X-SERVER-CONTROL — HOLD-BACK supplies the latency target. PART-HOLD-BACK\n // is read for its presence only, as an LL-HLS signal; see MediaPlaylistMetadata\n // on why its value stays out of `holdBack` until LL-HLS support lands.\n const serverControl = matchTag(trimmed, 'EXT-X-SERVER-CONTROL');\n if (serverControl) {\n const value = serverControl.get('HOLD-BACK');\n if (value !== undefined) {\n const parsed = Number.parseFloat(value);\n if (Number.isFinite(parsed) && parsed > 0) holdBack = parsed;\n }\n // Advertised for clients playing partial segments — its presence is an\n // LL-HLS signal even though the value stays deliberately unread.\n if (serverControl.get('PART-HOLD-BACK') !== undefined) lowLatency = true;\n continue;\n }\n\n // Partial segments: noted, then skipped. `#EXT-X-PART` lines would otherwise\n // fall through as unrecognized `#EXT` tags, so LL-HLS delivery went unnoticed.\n if (trimmed.startsWith('#EXT-X-PART-INF') || trimmed.startsWith('#EXT-X-PART:')) {\n lowLatency = true;\n continue;\n }\n\n if (\n trimmed === '#EXTM3U' ||\n trimmed.startsWith('#EXT-X-VERSION:') ||\n trimmed.startsWith('#EXT-X-INDEPENDENT-SEGMENTS')\n ) {\n continue;\n }\n\n // #EXT-X-MAP - Init segment\n const mapAttrs = matchTag(trimmed, 'EXT-X-MAP');\n if (mapAttrs) {\n const uri = mapAttrs.get('URI');\n if (uri) {\n initSegmentUrl = resolveUrl(uri, baseUrl);\n const byteRangeStr = mapAttrs.get('BYTERANGE');\n if (byteRangeStr) {\n initSegmentByteRange = parseByteRange(byteRangeStr, 0) ?? undefined;\n }\n }\n continue;\n }\n\n // #EXTINF - Segment duration\n if (trimmed.startsWith('#EXTINF:')) {\n currentDuration = parseExtInfDuration(trimmed.slice(8));\n continue;\n }\n\n // #EXT-X-BYTERANGE - Segment byte range\n if (trimmed.startsWith('#EXT-X-BYTERANGE:')) {\n currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? undefined;\n continue;\n }\n\n if (trimmed === '#EXT-X-ENDLIST') {\n endList = true;\n continue;\n }\n\n // Segment URI\n if (!trimmed.startsWith('#') && currentDuration > 0) {\n const segment: Segment = {\n id: `segment-${mediaSequence + segmentIndex}`,\n url: resolveUrl(trimmed, baseUrl),\n duration: currentDuration,\n startTime: currentTime,\n };\n\n if (!isUndefined(currentStartDate)) {\n segment.startDate = currentStartDate;\n // Interpolate forward: the next segment without an explicit tag inherits\n // this anchor plus this segment's duration.\n currentStartDate += currentDuration;\n }\n\n if (currentByteRange) {\n segment.byteRange = currentByteRange;\n previousByteRangeEnd = currentByteRange.end + 1;\n } else {\n previousByteRangeEnd = undefined;\n }\n\n segments.push(segment);\n currentTime += currentDuration;\n segmentIndex++;\n\n currentDuration = 0;\n currentByteRange = undefined;\n }\n }\n\n const complete = endList || playlistType === 'VOD';\n\n // Position this window on the timeline. PDT is primary: whenever the track\n // carries a wall-clock anchor (`startDate` — pre-stamped on the shell for a\n // first resolve, recomputed-and-stable for a reload) and the new window has\n // PDT to place with, place from PDT against that frozen anchor\n // (`placeOnAnchor`) — self-correcting across reloads, history-free across\n // turnovers. Media-sequence/EXTINF carry-forward is the fallback for\n // PDT-less windows; a first resolve with neither anchors at the local base 0.\n const anchor = previous.startDate;\n const hasPdt = segments.some((segment) => !isUndefined(segment.startDate));\n const placed =\n !isUndefined(anchor) && hasPdt\n ? placeOnAnchor(segments, anchor)\n : isResolvedTrack(previous)\n ? placeOnPreviousTimeline(previous, segments, mediaSequence, targetDuration)\n : segments;\n\n // `duration` is the track's duration: Infinity while the playlist can still\n // grow (unended live), finite once complete (VOD / ENDLIST). Paired with\n // `startTime: 0` below, it spans the presentation origin to the last placed\n // segment's end — so it's measured off `placed`, not the local EXTINF sum:\n // for a window that has slid, the sum is the window's length, not its span.\n // Never the target duration.\n const lastPlaced = placed[placed.length - 1];\n const placedEnd = lastPlaced ? lastPlaced.startTime + lastPlaced.duration : 0;\n const trackDuration = complete ? placedEnd : Number.POSITIVE_INFINITY;\n\n // Wall-clock anchor: `startDate − startTime` for the first PDT-bearing\n // segment (constant along a linear timeline). Maps this track's origin to\n // wall clock; recomputed each parse, so it stays stable as the window slides\n // and is comparable across tracks for A/V alignment.\n const anchorSegment = placed.find((segment) => !isUndefined(segment.startDate));\n const startDate =\n anchorSegment && !isUndefined(anchorSegment.startDate)\n ? anchorSegment.startDate - anchorSegment.startTime\n : undefined;\n\n // Build initialization (VTT may not have init segment)\n const initialization =\n previous.type === 'text' && !initSegmentUrl\n ? undefined\n : initSegmentUrl\n ? { url: initSegmentUrl, ...(initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}) }\n : { url: '' };\n\n // Container detection: fMP4 always carries an EXT-X-MAP init segment, so its\n // absence plus a recognized non-fMP4 segment extension (`.ts` → MPEG-TS,\n // `.aac` → raw ADTS AAC) marks a non-fMP4 rendition (high-precision — never\n // trips on fMP4, which mandates the map). Relabel from the fMP4 default\n // `video/mp4` / `audio/mp4` to the container MIME so capability probing prunes\n // it (these containers are currently treated as unplayable; see `canPlayTrack`).\n const detectedContainer = initSegmentUrl ? undefined : containerMimeFromSegment(placed[0]?.url);\n const mimeType = previous.type !== 'text' && detectedContainer ? detectedContainer : previous.mimeType;\n\n // Generic resolution: All type-specific fields already on `previous` track\n // Just add parsed properties (startTime, duration, segments, initialization, metadata)\n return {\n ...previous,\n mimeType,\n startTime: 0,\n startDate,\n duration: trackDuration,\n segments: placed,\n initialization,\n metadata: {\n ...previous.metadata,\n [MEDIA_PLAYLIST_METADATA_KEY]: {\n targetDuration,\n mediaSequence,\n playlistType,\n lowLatency,\n endList,\n holdBack,\n encrypted,\n } satisfies MediaPlaylistMetadata,\n },\n } as unknown as ResolveTrack<T>;\n}\n"],"mappings":"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"}
|