@videojs/html 10.0.0-beta.11 → 10.0.0-beta.12
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/cdn/abort-C7q_G_dT.js +2 -0
- package/cdn/abort-C7q_G_dT.js.map +1 -0
- package/cdn/abort-JT-ewLFq.js +22 -0
- package/cdn/abort-JT-ewLFq.js.map +1 -0
- package/cdn/audio-minimal.dev.js +23 -12
- package/cdn/audio-minimal.dev.js.map +1 -1
- package/cdn/audio-minimal.js +1 -1
- package/cdn/audio-minimal.js.map +1 -1
- package/cdn/audio.dev.js +72 -57
- package/cdn/audio.dev.js.map +1 -1
- package/cdn/audio.js +1 -1
- package/cdn/audio.js.map +1 -1
- package/cdn/background.dev.js +3 -4
- package/cdn/background.dev.js.map +1 -1
- package/cdn/background.js +1 -1
- package/cdn/background.js.map +1 -1
- package/cdn/{create-player-AcfnN3li.js → create-player-BoPlCSNw.js} +103 -19
- package/cdn/create-player-BoPlCSNw.js.map +1 -0
- package/cdn/create-player-CA3KLZMe.js +7 -0
- package/cdn/create-player-CA3KLZMe.js.map +1 -0
- package/cdn/{default-cLso8BHO.js → default-CKnlVEjQ.js} +1 -1
- package/cdn/{default-cLso8BHO.js.map → default-CKnlVEjQ.js.map} +1 -1
- package/cdn/{default-GgKND7a8.js → default-CnBlD9BM.js} +1 -1
- package/cdn/{default-GgKND7a8.js.map → default-CnBlD9BM.js.map} +1 -1
- package/cdn/{delegate-CzAcT1xm.js → delegate-CSc5c0ZR.js} +16 -3
- package/cdn/delegate-CSc5c0ZR.js.map +1 -0
- package/cdn/delegate-jczJeizF.js +2 -0
- package/cdn/delegate-jczJeizF.js.map +1 -0
- package/cdn/hls-C6htsSW4.js +28661 -0
- package/cdn/hls-C6htsSW4.js.map +1 -0
- package/cdn/hls-DQ4glyHe.js +41 -0
- package/cdn/hls-DQ4glyHe.js.map +1 -0
- package/cdn/{listen-YSH3Jfyk.js → listen-BkAEGXCe.js} +1 -1
- package/cdn/{listen-YSH3Jfyk.js.map → listen-BkAEGXCe.js.map} +1 -1
- package/cdn/{listen-4jqsRSKo.js → listen-UqQNdlqV.js} +1 -1
- package/cdn/{listen-4jqsRSKo.js.map → listen-UqQNdlqV.js.map} +1 -1
- package/cdn/media/dash-video.dev.js +10 -17
- package/cdn/media/dash-video.dev.js.map +1 -1
- package/cdn/media/dash-video.js +3 -3
- package/cdn/media/dash-video.js.map +1 -1
- package/cdn/media/hls-video.dev.js +5 -28336
- package/cdn/media/hls-video.dev.js.map +1 -1
- package/cdn/media/hls-video.js +1 -40
- package/cdn/media/hls-video.js.map +1 -1
- package/cdn/media/mux-video.dev.d.ts +1 -0
- package/cdn/media/mux-video.dev.js +3122 -0
- package/cdn/media/mux-video.dev.js.map +1 -0
- package/cdn/media/mux-video.js +25 -0
- package/cdn/media/mux-video.js.map +1 -0
- package/cdn/media/simple-hls-video.dev.js +2286 -2094
- package/cdn/media/simple-hls-video.dev.js.map +1 -1
- package/cdn/media/simple-hls-video.js +58 -1
- package/cdn/media/simple-hls-video.js.map +1 -1
- package/cdn/media-attach-mixin-BIrlT_tz.js +2 -0
- package/cdn/{media-attach-mixin-D5_nfJpa.js.map → media-attach-mixin-BIrlT_tz.js.map} +1 -1
- package/cdn/{media-attach-mixin-U_KQB_9O.js → media-attach-mixin-Dsn4gxJA.js} +2 -2
- package/cdn/{media-attach-mixin-U_KQB_9O.js.map → media-attach-mixin-Dsn4gxJA.js.map} +1 -1
- package/cdn/{custom-media-element-DqevSVgS.js → media-props-mixin-BuVUebRp.js} +2 -2
- package/cdn/media-props-mixin-BuVUebRp.js.map +1 -0
- package/cdn/{custom-media-element-moFa3UZp.js → media-props-mixin-DxsM38Bx.js} +44 -2
- package/cdn/media-props-mixin-DxsM38Bx.js.map +1 -0
- package/cdn/{minimal-BJfleQcQ.js → minimal-CKMdOXWm.js} +1 -1
- package/cdn/{minimal-BJfleQcQ.js.map → minimal-CKMdOXWm.js.map} +1 -1
- package/cdn/{minimal-DBMdC_0I.js → minimal-fA2p2Jrn.js} +1 -1
- package/cdn/{minimal-DBMdC_0I.js.map → minimal-fA2p2Jrn.js.map} +1 -1
- package/cdn/player-Dzvu8Tzs.js +2 -0
- package/cdn/{player-C46h14iP.js.map → player-Dzvu8Tzs.js.map} +1 -1
- package/cdn/{player-CvrOeLpy.js → player-rkxd0mpV.js} +3 -3
- package/cdn/{player-CvrOeLpy.js.map → player-rkxd0mpV.js.map} +1 -1
- package/cdn/{poster-Olv5zDI_.js → poster-BPMPXyn3.js} +4 -5
- package/cdn/{poster-Olv5zDI_.js.map → poster-BPMPXyn3.js.map} +1 -1
- package/cdn/poster-DqjXzMK_.js +2 -0
- package/cdn/{poster-odJ4iwIv.js.map → poster-DqjXzMK_.js.map} +1 -1
- package/cdn/{context-Be8C5kVd.js → safe-define-D26LrTu4.js} +14 -5
- package/cdn/safe-define-D26LrTu4.js.map +1 -0
- package/cdn/safe-define-EEn8NTOG.js +14 -0
- package/cdn/safe-define-EEn8NTOG.js.map +1 -0
- package/cdn/shallow-equal-CaIo44Co.js +15 -0
- package/cdn/shallow-equal-CaIo44Co.js.map +1 -0
- package/cdn/shallow-equal-zo2IZwso.js +2 -0
- package/cdn/shallow-equal-zo2IZwso.js.map +1 -0
- package/cdn/video-minimal.dev.js +24 -13
- package/cdn/video-minimal.dev.js.map +1 -1
- package/cdn/video-minimal.js +1 -1
- package/cdn/video-minimal.js.map +1 -1
- package/cdn/video.dev.js +107 -92
- package/cdn/video.dev.js.map +1 -1
- package/cdn/video.js +1 -1
- package/cdn/video.js.map +1 -1
- package/cdn/{volume-slider-D7BOdSDF.js → volume-slider-BEXiB6_j.js} +245 -14
- package/cdn/volume-slider-BEXiB6_j.js.map +1 -0
- package/cdn/volume-slider-CQ0Yq947.js +9 -0
- package/cdn/volume-slider-CQ0Yq947.js.map +1 -0
- package/dist/default/_virtual/inline-css_src/define/audio/minimal-skin.js +2 -2
- package/dist/default/_virtual/inline-css_src/define/audio/minimal-skin.js.map +1 -1
- package/dist/default/_virtual/inline-css_src/define/audio/skin.js +2 -2
- package/dist/default/_virtual/inline-css_src/define/audio/skin.js.map +1 -1
- package/dist/default/_virtual/inline-css_src/define/background/skin.js +1 -1
- package/dist/default/_virtual/inline-css_src/define/background/skin.js.map +1 -1
- package/dist/default/_virtual/inline-css_src/define/base.js +1 -1
- package/dist/default/_virtual/inline-css_src/define/base.js.map +1 -1
- package/dist/default/_virtual/inline-css_src/define/shared.js +1 -1
- package/dist/default/_virtual/inline-css_src/define/shared.js.map +1 -1
- package/dist/default/_virtual/inline-css_src/define/video/minimal-skin.js +2 -2
- package/dist/default/_virtual/inline-css_src/define/video/minimal-skin.js.map +1 -1
- package/dist/default/_virtual/inline-css_src/define/video/skin.js +2 -2
- package/dist/default/_virtual/inline-css_src/define/video/skin.js.map +1 -1
- package/dist/default/define/audio/minimal-skin.css +81 -59
- package/dist/default/define/audio/minimal-skin.js +5 -4
- package/dist/default/define/audio/minimal-skin.js.map +1 -1
- package/dist/default/define/audio/minimal-skin.tailwind.js +6 -5
- package/dist/default/define/audio/minimal-skin.tailwind.js.map +1 -1
- package/dist/default/define/audio/player.js +1 -2
- package/dist/default/define/audio/player.js.map +1 -1
- package/dist/default/define/audio/skin.css +54 -42
- package/dist/default/define/audio/skin.js +5 -4
- package/dist/default/define/audio/skin.js.map +1 -1
- package/dist/default/define/audio/skin.tailwind.js +7 -5
- package/dist/default/define/audio/skin.tailwind.js.map +1 -1
- package/dist/default/define/background/player.js +1 -2
- package/dist/default/define/background/player.js.map +1 -1
- package/dist/default/define/background/skin.js +1 -2
- package/dist/default/define/background/skin.js.map +1 -1
- package/dist/default/define/background/video.js +1 -2
- package/dist/default/define/media/background-video.js +1 -2
- package/dist/default/define/media/background-video.js.map +1 -1
- package/dist/default/define/media/container.js +1 -2
- package/dist/default/define/media/container.js.map +1 -1
- package/dist/default/define/media/dash-video.js +1 -2
- package/dist/default/define/media/dash-video.js.map +1 -1
- package/dist/default/define/media/hls-video.js +1 -2
- package/dist/default/define/media/hls-video.js.map +1 -1
- package/dist/default/define/media/mux-video.js +13 -0
- package/dist/default/define/media/mux-video.js.map +1 -0
- package/dist/default/define/media/native-hls-video.js +13 -0
- package/dist/default/define/media/native-hls-video.js.map +1 -0
- package/dist/default/define/media/simple-hls-video.js +3 -3
- package/dist/default/define/media/simple-hls-video.js.map +1 -1
- package/dist/default/define/safe-define.js +4 -2
- package/dist/default/define/safe-define.js.map +1 -1
- package/dist/default/define/skin-mixin.js +30 -11
- package/dist/default/define/skin-mixin.js.map +1 -1
- package/dist/default/define/ui/alert-dialog-close.js +1 -2
- package/dist/default/define/ui/alert-dialog-close.js.map +1 -1
- package/dist/default/define/ui/alert-dialog-description.js +1 -2
- package/dist/default/define/ui/alert-dialog-description.js.map +1 -1
- package/dist/default/define/ui/alert-dialog-title.js +1 -2
- package/dist/default/define/ui/alert-dialog-title.js.map +1 -1
- package/dist/default/define/ui/alert-dialog.js +1 -2
- package/dist/default/define/ui/alert-dialog.js.map +1 -1
- package/dist/default/define/ui/buffering-indicator.js +1 -2
- package/dist/default/define/ui/buffering-indicator.js.map +1 -1
- package/dist/default/define/ui/captions-button.js +3 -3
- package/dist/default/define/ui/captions-button.js.map +1 -1
- package/dist/default/define/ui/controls-group.js +1 -2
- package/dist/default/define/ui/controls-group.js.map +1 -1
- package/dist/default/define/ui/controls.js +1 -2
- package/dist/default/define/ui/controls.js.map +1 -1
- package/dist/default/define/ui/error-dialog.js +13 -0
- package/dist/default/define/ui/error-dialog.js.map +1 -0
- package/dist/default/define/ui/fullscreen-button.js +1 -2
- package/dist/default/define/ui/fullscreen-button.js.map +1 -1
- package/dist/default/define/ui/mute-button.js +1 -2
- package/dist/default/define/ui/mute-button.js.map +1 -1
- package/dist/default/define/ui/pip-button.js +1 -2
- package/dist/default/define/ui/pip-button.js.map +1 -1
- package/dist/default/define/ui/play-button.js +1 -2
- package/dist/default/define/ui/play-button.js.map +1 -1
- package/dist/default/define/ui/playback-rate-button.js +1 -2
- package/dist/default/define/ui/playback-rate-button.js.map +1 -1
- package/dist/default/define/ui/popover.js +1 -2
- package/dist/default/define/ui/popover.js.map +1 -1
- package/dist/default/define/ui/poster.js +1 -2
- package/dist/default/define/ui/poster.js.map +1 -1
- package/dist/default/define/ui/seek-button.js +1 -2
- package/dist/default/define/ui/seek-button.js.map +1 -1
- package/dist/default/define/ui/slider-buffer.js +1 -2
- package/dist/default/define/ui/slider-buffer.js.map +1 -1
- package/dist/default/define/ui/slider-fill.js +1 -2
- package/dist/default/define/ui/slider-fill.js.map +1 -1
- package/dist/default/define/ui/slider-thumb.js +1 -2
- package/dist/default/define/ui/slider-thumb.js.map +1 -1
- package/dist/default/define/ui/slider-thumbnail.js +1 -2
- package/dist/default/define/ui/slider-thumbnail.js.map +1 -1
- package/dist/default/define/ui/slider-track.js +1 -2
- package/dist/default/define/ui/slider-track.js.map +1 -1
- package/dist/default/define/ui/slider-value.js +1 -2
- package/dist/default/define/ui/slider-value.js.map +1 -1
- package/dist/default/define/ui/slider.js +1 -2
- package/dist/default/define/ui/slider.js.map +1 -1
- package/dist/default/define/ui/thumbnail.js +1 -2
- package/dist/default/define/ui/thumbnail.js.map +1 -1
- package/dist/default/define/ui/time-group.js +1 -2
- package/dist/default/define/ui/time-group.js.map +1 -1
- package/dist/default/define/ui/time-separator.js +1 -2
- package/dist/default/define/ui/time-separator.js.map +1 -1
- package/dist/default/define/ui/time-slider.js +1 -2
- package/dist/default/define/ui/time-slider.js.map +1 -1
- package/dist/default/define/ui/time.js +1 -2
- package/dist/default/define/ui/time.js.map +1 -1
- package/dist/default/define/ui/tooltip-group.js +1 -2
- package/dist/default/define/ui/tooltip-group.js.map +1 -1
- package/dist/default/define/ui/tooltip.js +1 -2
- package/dist/default/define/ui/tooltip.js.map +1 -1
- package/dist/default/define/ui/volume-slider.js +1 -2
- package/dist/default/define/ui/volume-slider.js.map +1 -1
- package/dist/default/define/video/minimal-skin.css +156 -75
- package/dist/default/define/video/minimal-skin.js +5 -4
- package/dist/default/define/video/minimal-skin.js.map +1 -1
- package/dist/default/define/video/minimal-skin.tailwind.js +6 -7
- package/dist/default/define/video/minimal-skin.tailwind.js.map +1 -1
- package/dist/default/define/video/player.js +1 -2
- package/dist/default/define/video/player.js.map +1 -1
- package/dist/default/define/video/skin.css +114 -58
- package/dist/default/define/video/skin.js +5 -4
- package/dist/default/define/video/skin.js.map +1 -1
- package/dist/default/define/video/skin.tailwind.js +6 -6
- package/dist/default/define/video/skin.tailwind.js.map +1 -1
- package/dist/default/icons/dist/render/default/index.js +1 -1
- package/dist/default/icons/dist/render/minimal/index.js +1 -1
- package/dist/default/index.js +3 -4
- package/dist/default/media/background-video/index.js +1 -2
- package/dist/default/media/background-video/index.js.map +1 -1
- package/dist/default/media/container-element.js +1 -2
- package/dist/default/media/container-element.js.map +1 -1
- package/dist/default/media/dash-video/index.js +4 -12
- package/dist/default/media/dash-video/index.js.map +1 -1
- package/dist/default/media/hls-video/index.js +4 -12
- package/dist/default/media/hls-video/index.js.map +1 -1
- package/dist/default/media/mux-video/index.js +18 -0
- package/dist/default/media/mux-video/index.js.map +1 -0
- package/dist/default/media/native-hls-video/index.js +18 -0
- package/dist/default/media/native-hls-video/index.js.map +1 -0
- package/dist/default/media/simple-hls-video/index.js +4 -12
- package/dist/default/media/simple-hls-video/index.js.map +1 -1
- package/dist/default/player/context.js +1 -2
- package/dist/default/player/context.js.map +1 -1
- package/dist/default/player/create-player.js +1 -2
- package/dist/default/player/create-player.js.map +1 -1
- package/dist/default/player/player-controller.js +1 -2
- package/dist/default/player/player-controller.js.map +1 -1
- package/dist/default/presets/audio.js +1 -2
- package/dist/default/presets/background.js +1 -2
- package/dist/default/presets/video.js +1 -2
- package/dist/default/skins/dist/default/default/tailwind/audio.tailwind.js +14 -18
- package/dist/default/skins/dist/default/default/tailwind/audio.tailwind.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/buffering.js +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/buffering.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/button-group.js +7 -0
- package/dist/default/skins/dist/default/default/tailwind/components/button-group.js.map +1 -0
- package/dist/default/skins/dist/default/default/tailwind/components/button.js +3 -4
- package/dist/default/skins/dist/default/default/tailwind/components/button.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/controls.js +2 -3
- package/dist/default/skins/dist/default/default/tailwind/components/controls.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/error.js +2 -3
- package/dist/default/skins/dist/default/default/tailwind/components/error.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/icon.js +1 -2
- package/dist/default/skins/dist/default/default/tailwind/components/icon.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/overlay.js +2 -3
- package/dist/default/skins/dist/default/default/tailwind/components/overlay.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/playback-rate.js +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/playback-rate.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/popup.js +4 -5
- package/dist/default/skins/dist/default/default/tailwind/components/popup.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/poster.js +1 -2
- package/dist/default/skins/dist/default/default/tailwind/components/poster.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/preview.js +2 -3
- package/dist/default/skins/dist/default/default/tailwind/components/preview.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/root.js +2 -3
- package/dist/default/skins/dist/default/default/tailwind/components/root.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/seek.js +1 -2
- package/dist/default/skins/dist/default/default/tailwind/components/seek.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/slider.js +2 -3
- package/dist/default/skins/dist/default/default/tailwind/components/slider.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/surface.js +2 -3
- package/dist/default/skins/dist/default/default/tailwind/components/surface.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/time.js +1 -1
- package/dist/default/skins/dist/default/default/tailwind/components/time.js.map +1 -1
- package/dist/default/skins/dist/default/default/tailwind/video.tailwind.js +25 -20
- package/dist/default/skins/dist/default/default/tailwind/video.tailwind.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/audio.tailwind.js +15 -16
- package/dist/default/skins/dist/default/minimal/tailwind/audio.tailwind.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/buffering.js +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/buffering.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/button-group.js +1 -2
- package/dist/default/skins/dist/default/minimal/tailwind/components/button-group.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/button.js +2 -3
- package/dist/default/skins/dist/default/minimal/tailwind/components/button.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/controls.js +2 -3
- package/dist/default/skins/dist/default/minimal/tailwind/components/controls.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/error.js +2 -3
- package/dist/default/skins/dist/default/minimal/tailwind/components/error.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/icon.js +1 -2
- package/dist/default/skins/dist/default/minimal/tailwind/components/icon.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/overlay.js +2 -3
- package/dist/default/skins/dist/default/minimal/tailwind/components/overlay.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/playback-rate.js +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/playback-rate.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/popup.js +2 -3
- package/dist/default/skins/dist/default/minimal/tailwind/components/popup.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/poster.js +1 -2
- package/dist/default/skins/dist/default/minimal/tailwind/components/poster.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/preview.js +2 -3
- package/dist/default/skins/dist/default/minimal/tailwind/components/preview.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/root.js +2 -3
- package/dist/default/skins/dist/default/minimal/tailwind/components/root.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/seek.js +1 -2
- package/dist/default/skins/dist/default/minimal/tailwind/components/seek.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/slider.js +2 -3
- package/dist/default/skins/dist/default/minimal/tailwind/components/slider.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/components/time.js +5 -6
- package/dist/default/skins/dist/default/minimal/tailwind/components/time.js.map +1 -1
- package/dist/default/skins/dist/default/minimal/tailwind/video.tailwind.js +23 -18
- package/dist/default/skins/dist/default/minimal/tailwind/video.tailwind.js.map +1 -1
- package/dist/default/skins/dist/default/shared/tailwind/icon-state.js +1 -1
- package/dist/default/skins/dist/default/shared/tailwind/icon-state.js.map +1 -1
- package/dist/default/skins/dist/default/shared/tailwind/tooltip-state.js +1 -1
- package/dist/default/skins/dist/default/shared/tailwind/tooltip-state.js.map +1 -1
- package/dist/default/store/container-mixin.js +1 -2
- package/dist/default/store/container-mixin.js.map +1 -1
- package/dist/default/store/media-attach-mixin.js +1 -2
- package/dist/default/store/media-attach-mixin.js.map +1 -1
- package/dist/default/store/provider-mixin.js +1 -2
- package/dist/default/store/provider-mixin.js.map +1 -1
- package/dist/default/ui/alert-dialog/alert-dialog-close-element.js +1 -2
- package/dist/default/ui/alert-dialog/alert-dialog-close-element.js.map +1 -1
- package/dist/default/ui/alert-dialog/alert-dialog-description-element.js +1 -2
- package/dist/default/ui/alert-dialog/alert-dialog-description-element.js.map +1 -1
- package/dist/default/ui/alert-dialog/alert-dialog-element.js +1 -2
- package/dist/default/ui/alert-dialog/alert-dialog-element.js.map +1 -1
- package/dist/default/ui/alert-dialog/alert-dialog-title-element.js +1 -2
- package/dist/default/ui/alert-dialog/alert-dialog-title-element.js.map +1 -1
- package/dist/default/ui/alert-dialog/context.js +2 -5
- package/dist/default/ui/alert-dialog/context.js.map +1 -1
- package/dist/default/ui/buffering-indicator/buffering-indicator-element.js +1 -2
- package/dist/default/ui/buffering-indicator/buffering-indicator-element.js.map +1 -1
- package/dist/default/ui/captions-button/captions-button-element.js +1 -2
- package/dist/default/ui/captions-button/captions-button-element.js.map +1 -1
- package/dist/default/ui/context-part-element.js +1 -2
- package/dist/default/ui/context-part-element.js.map +1 -1
- package/dist/default/ui/controls/context.js +2 -5
- package/dist/default/ui/controls/context.js.map +1 -1
- package/dist/default/ui/controls/controls-element.js +1 -2
- package/dist/default/ui/controls/controls-element.js.map +1 -1
- package/dist/default/ui/controls/controls-group-element.js +1 -2
- package/dist/default/ui/controls/controls-group-element.js.map +1 -1
- package/dist/default/ui/error-dialog/error-dialog-element.js +76 -0
- package/dist/default/ui/error-dialog/error-dialog-element.js.map +1 -0
- package/dist/default/ui/fullscreen-button/fullscreen-button-element.js +1 -2
- package/dist/default/ui/fullscreen-button/fullscreen-button-element.js.map +1 -1
- package/dist/default/ui/media-button-element.js +1 -2
- package/dist/default/ui/media-button-element.js.map +1 -1
- package/dist/default/ui/media-element.js +1 -2
- package/dist/default/ui/media-element.js.map +1 -1
- package/dist/default/ui/media-ui-element.js +1 -2
- package/dist/default/ui/media-ui-element.js.map +1 -1
- package/dist/default/ui/mute-button/mute-button-element.js +1 -2
- package/dist/default/ui/mute-button/mute-button-element.js.map +1 -1
- package/dist/default/ui/pip-button/pip-button-element.js +1 -2
- package/dist/default/ui/pip-button/pip-button-element.js.map +1 -1
- package/dist/default/ui/play-button/play-button-element.js +1 -2
- package/dist/default/ui/play-button/play-button-element.js.map +1 -1
- package/dist/default/ui/playback-rate-button/playback-rate-button-element.js +1 -2
- package/dist/default/ui/playback-rate-button/playback-rate-button-element.js.map +1 -1
- package/dist/default/ui/popover/popover-element.js +1 -2
- package/dist/default/ui/popover/popover-element.js.map +1 -1
- package/dist/default/ui/poster/poster-element.js +1 -2
- package/dist/default/ui/poster/poster-element.js.map +1 -1
- package/dist/default/ui/seek-button/seek-button-element.js +1 -2
- package/dist/default/ui/seek-button/seek-button-element.js.map +1 -1
- package/dist/default/ui/slider/context.js +2 -5
- package/dist/default/ui/slider/context.js.map +1 -1
- package/dist/default/ui/slider/slider-buffer-element.js +1 -2
- package/dist/default/ui/slider/slider-buffer-element.js.map +1 -1
- package/dist/default/ui/slider/slider-element.js +1 -2
- package/dist/default/ui/slider/slider-element.js.map +1 -1
- package/dist/default/ui/slider/slider-fill-element.js +1 -2
- package/dist/default/ui/slider/slider-fill-element.js.map +1 -1
- package/dist/default/ui/slider/slider-preview-element.js +1 -2
- package/dist/default/ui/slider/slider-preview-element.js.map +1 -1
- package/dist/default/ui/slider/slider-thumb-element.js +1 -2
- package/dist/default/ui/slider/slider-thumb-element.js.map +1 -1
- package/dist/default/ui/slider/slider-thumbnail-element.js +1 -2
- package/dist/default/ui/slider/slider-thumbnail-element.js.map +1 -1
- package/dist/default/ui/slider/slider-track-element.js +1 -2
- package/dist/default/ui/slider/slider-track-element.js.map +1 -1
- package/dist/default/ui/slider/slider-value-element.js +1 -2
- package/dist/default/ui/slider/slider-value-element.js.map +1 -1
- package/dist/default/ui/thumbnail/thumbnail-element.js +1 -2
- package/dist/default/ui/thumbnail/thumbnail-element.js.map +1 -1
- package/dist/default/ui/time/time-element.js +1 -2
- package/dist/default/ui/time/time-element.js.map +1 -1
- package/dist/default/ui/time/time-group-element.js +1 -2
- package/dist/default/ui/time/time-group-element.js.map +1 -1
- package/dist/default/ui/time/time-separator-element.js +1 -2
- package/dist/default/ui/time/time-separator-element.js.map +1 -1
- package/dist/default/ui/time-slider/time-slider-element.js +1 -2
- package/dist/default/ui/time-slider/time-slider-element.js.map +1 -1
- package/dist/default/ui/tooltip/context.js +2 -5
- package/dist/default/ui/tooltip/context.js.map +1 -1
- package/dist/default/ui/tooltip/tooltip-element.js +1 -2
- package/dist/default/ui/tooltip/tooltip-element.js.map +1 -1
- package/dist/default/ui/tooltip/tooltip-group-element.js +1 -2
- package/dist/default/ui/tooltip/tooltip-group-element.js.map +1 -1
- package/dist/default/ui/volume-slider/volume-slider-element.js +1 -2
- package/dist/default/ui/volume-slider/volume-slider-element.js.map +1 -1
- package/dist/default/utils/media-props-mixin.js +44 -0
- package/dist/default/utils/media-props-mixin.js.map +1 -0
- package/dist/dev/_virtual/inline-css_src/define/audio/minimal-skin.js +2 -2
- package/dist/dev/_virtual/inline-css_src/define/audio/minimal-skin.js.map +1 -1
- package/dist/dev/_virtual/inline-css_src/define/audio/skin.js +2 -2
- package/dist/dev/_virtual/inline-css_src/define/audio/skin.js.map +1 -1
- package/dist/dev/_virtual/inline-css_src/define/background/skin.js +1 -1
- package/dist/dev/_virtual/inline-css_src/define/background/skin.js.map +1 -1
- package/dist/dev/_virtual/inline-css_src/define/base.js +1 -1
- package/dist/dev/_virtual/inline-css_src/define/base.js.map +1 -1
- package/dist/dev/_virtual/inline-css_src/define/shared.js +1 -1
- package/dist/dev/_virtual/inline-css_src/define/shared.js.map +1 -1
- package/dist/dev/_virtual/inline-css_src/define/video/minimal-skin.js +2 -2
- package/dist/dev/_virtual/inline-css_src/define/video/minimal-skin.js.map +1 -1
- package/dist/dev/_virtual/inline-css_src/define/video/skin.js +2 -2
- package/dist/dev/_virtual/inline-css_src/define/video/skin.js.map +1 -1
- package/dist/dev/define/audio/minimal-skin.css +81 -59
- package/dist/dev/define/audio/minimal-skin.d.ts +2 -2
- package/dist/dev/define/audio/minimal-skin.d.ts.map +1 -1
- package/dist/dev/define/audio/minimal-skin.js +20 -7
- package/dist/dev/define/audio/minimal-skin.js.map +1 -1
- package/dist/dev/define/audio/minimal-skin.tailwind.d.ts +1 -1
- package/dist/dev/define/audio/minimal-skin.tailwind.d.ts.map +1 -1
- package/dist/dev/define/audio/minimal-skin.tailwind.js +19 -6
- package/dist/dev/define/audio/minimal-skin.tailwind.js.map +1 -1
- package/dist/dev/define/audio/player.js +1 -2
- package/dist/dev/define/audio/player.js.map +1 -1
- package/dist/dev/define/audio/skin.css +54 -42
- package/dist/dev/define/audio/skin.d.ts +2 -2
- package/dist/dev/define/audio/skin.d.ts.map +1 -1
- package/dist/dev/define/audio/skin.js +69 -52
- package/dist/dev/define/audio/skin.js.map +1 -1
- package/dist/dev/define/audio/skin.tailwind.d.ts +1 -1
- package/dist/dev/define/audio/skin.tailwind.d.ts.map +1 -1
- package/dist/dev/define/audio/skin.tailwind.js +71 -53
- package/dist/dev/define/audio/skin.tailwind.js.map +1 -1
- package/dist/dev/define/background/player.js +1 -2
- package/dist/dev/define/background/player.js.map +1 -1
- package/dist/dev/define/background/skin.js +1 -2
- package/dist/dev/define/background/skin.js.map +1 -1
- package/dist/dev/define/background/video.js +1 -2
- package/dist/dev/define/media/background-video.js +1 -2
- package/dist/dev/define/media/background-video.js.map +1 -1
- package/dist/dev/define/media/container.js +1 -2
- package/dist/dev/define/media/container.js.map +1 -1
- package/dist/dev/define/media/dash-video.js +1 -2
- package/dist/dev/define/media/dash-video.js.map +1 -1
- package/dist/dev/define/media/hls-video.js +1 -2
- package/dist/dev/define/media/hls-video.js.map +1 -1
- package/dist/dev/define/media/mux-video.d.ts +14 -0
- package/dist/dev/define/media/mux-video.d.ts.map +1 -0
- package/dist/dev/define/media/mux-video.js +13 -0
- package/dist/dev/define/media/mux-video.js.map +1 -0
- package/dist/dev/define/media/native-hls-video.d.ts +14 -0
- package/dist/dev/define/media/native-hls-video.d.ts.map +1 -0
- package/dist/dev/define/media/native-hls-video.js +13 -0
- package/dist/dev/define/media/native-hls-video.js.map +1 -0
- package/dist/dev/define/media/simple-hls-video.d.ts.map +1 -1
- package/dist/dev/define/media/simple-hls-video.js +3 -3
- package/dist/dev/define/media/simple-hls-video.js.map +1 -1
- package/dist/dev/define/safe-define.js +4 -2
- package/dist/dev/define/safe-define.js.map +1 -1
- package/dist/dev/define/skin-mixin.d.ts +4 -3
- package/dist/dev/define/skin-mixin.d.ts.map +1 -1
- package/dist/dev/define/skin-mixin.js +30 -11
- package/dist/dev/define/skin-mixin.js.map +1 -1
- package/dist/dev/define/ui/alert-dialog-close.js +1 -2
- package/dist/dev/define/ui/alert-dialog-close.js.map +1 -1
- package/dist/dev/define/ui/alert-dialog-description.js +1 -2
- package/dist/dev/define/ui/alert-dialog-description.js.map +1 -1
- package/dist/dev/define/ui/alert-dialog-title.js +1 -2
- package/dist/dev/define/ui/alert-dialog-title.js.map +1 -1
- package/dist/dev/define/ui/alert-dialog.js +1 -2
- package/dist/dev/define/ui/alert-dialog.js.map +1 -1
- package/dist/dev/define/ui/buffering-indicator.js +1 -2
- package/dist/dev/define/ui/buffering-indicator.js.map +1 -1
- package/dist/dev/define/ui/captions-button.d.ts.map +1 -1
- package/dist/dev/define/ui/captions-button.js +3 -3
- package/dist/dev/define/ui/captions-button.js.map +1 -1
- package/dist/dev/define/ui/controls-group.js +1 -2
- package/dist/dev/define/ui/controls-group.js.map +1 -1
- package/dist/dev/define/ui/controls.js +1 -2
- package/dist/dev/define/ui/controls.js.map +1 -1
- package/dist/dev/define/ui/error-dialog.d.ts +9 -0
- package/dist/dev/define/ui/error-dialog.d.ts.map +1 -0
- package/dist/dev/define/ui/error-dialog.js +13 -0
- package/dist/dev/define/ui/error-dialog.js.map +1 -0
- package/dist/dev/define/ui/fullscreen-button.js +1 -2
- package/dist/dev/define/ui/fullscreen-button.js.map +1 -1
- package/dist/dev/define/ui/mute-button.js +1 -2
- package/dist/dev/define/ui/mute-button.js.map +1 -1
- package/dist/dev/define/ui/pip-button.js +1 -2
- package/dist/dev/define/ui/pip-button.js.map +1 -1
- package/dist/dev/define/ui/play-button.js +1 -2
- package/dist/dev/define/ui/play-button.js.map +1 -1
- package/dist/dev/define/ui/playback-rate-button.js +1 -2
- package/dist/dev/define/ui/playback-rate-button.js.map +1 -1
- package/dist/dev/define/ui/popover.js +1 -2
- package/dist/dev/define/ui/popover.js.map +1 -1
- package/dist/dev/define/ui/poster.js +1 -2
- package/dist/dev/define/ui/poster.js.map +1 -1
- package/dist/dev/define/ui/seek-button.js +1 -2
- package/dist/dev/define/ui/seek-button.js.map +1 -1
- package/dist/dev/define/ui/slider-buffer.js +1 -2
- package/dist/dev/define/ui/slider-buffer.js.map +1 -1
- package/dist/dev/define/ui/slider-fill.js +1 -2
- package/dist/dev/define/ui/slider-fill.js.map +1 -1
- package/dist/dev/define/ui/slider-thumb.js +1 -2
- package/dist/dev/define/ui/slider-thumb.js.map +1 -1
- package/dist/dev/define/ui/slider-thumbnail.js +1 -2
- package/dist/dev/define/ui/slider-thumbnail.js.map +1 -1
- package/dist/dev/define/ui/slider-track.js +1 -2
- package/dist/dev/define/ui/slider-track.js.map +1 -1
- package/dist/dev/define/ui/slider-value.js +1 -2
- package/dist/dev/define/ui/slider-value.js.map +1 -1
- package/dist/dev/define/ui/slider.js +1 -2
- package/dist/dev/define/ui/slider.js.map +1 -1
- package/dist/dev/define/ui/thumbnail.js +1 -2
- package/dist/dev/define/ui/thumbnail.js.map +1 -1
- package/dist/dev/define/ui/time-group.js +1 -2
- package/dist/dev/define/ui/time-group.js.map +1 -1
- package/dist/dev/define/ui/time-separator.js +1 -2
- package/dist/dev/define/ui/time-separator.js.map +1 -1
- package/dist/dev/define/ui/time-slider.js +1 -2
- package/dist/dev/define/ui/time-slider.js.map +1 -1
- package/dist/dev/define/ui/time.js +1 -2
- package/dist/dev/define/ui/time.js.map +1 -1
- package/dist/dev/define/ui/tooltip-group.js +1 -2
- package/dist/dev/define/ui/tooltip-group.js.map +1 -1
- package/dist/dev/define/ui/tooltip.js +1 -2
- package/dist/dev/define/ui/tooltip.js.map +1 -1
- package/dist/dev/define/ui/volume-slider.js +1 -2
- package/dist/dev/define/ui/volume-slider.js.map +1 -1
- package/dist/dev/define/video/minimal-skin.css +156 -75
- package/dist/dev/define/video/minimal-skin.d.ts +2 -2
- package/dist/dev/define/video/minimal-skin.d.ts.map +1 -1
- package/dist/dev/define/video/minimal-skin.js +21 -8
- package/dist/dev/define/video/minimal-skin.js.map +1 -1
- package/dist/dev/define/video/minimal-skin.tailwind.d.ts +1 -1
- package/dist/dev/define/video/minimal-skin.tailwind.d.ts.map +1 -1
- package/dist/dev/define/video/minimal-skin.tailwind.js +22 -11
- package/dist/dev/define/video/minimal-skin.tailwind.js.map +1 -1
- package/dist/dev/define/video/player.js +1 -2
- package/dist/dev/define/video/player.js.map +1 -1
- package/dist/dev/define/video/skin.css +114 -58
- package/dist/dev/define/video/skin.d.ts +2 -2
- package/dist/dev/define/video/skin.d.ts.map +1 -1
- package/dist/dev/define/video/skin.js +103 -86
- package/dist/dev/define/video/skin.js.map +1 -1
- package/dist/dev/define/video/skin.tailwind.d.ts +1 -1
- package/dist/dev/define/video/skin.tailwind.d.ts.map +1 -1
- package/dist/dev/define/video/skin.tailwind.js +104 -88
- package/dist/dev/define/video/skin.tailwind.js.map +1 -1
- package/dist/dev/icons/dist/render/default/index.js +1 -1
- package/dist/dev/icons/dist/render/minimal/index.js +1 -1
- package/dist/dev/index.d.ts +3 -2
- package/dist/dev/index.js +3 -4
- package/dist/dev/media/background-video/index.js +1 -2
- package/dist/dev/media/background-video/index.js.map +1 -1
- package/dist/dev/media/container-element.js +1 -2
- package/dist/dev/media/container-element.js.map +1 -1
- package/dist/dev/media/dash-video/index.d.ts +0 -2
- package/dist/dev/media/dash-video/index.d.ts.map +1 -1
- package/dist/dev/media/dash-video/index.js +4 -12
- package/dist/dev/media/dash-video/index.js.map +1 -1
- package/dist/dev/media/hls-video/index.d.ts +0 -2
- package/dist/dev/media/hls-video/index.d.ts.map +1 -1
- package/dist/dev/media/hls-video/index.js +4 -12
- package/dist/dev/media/hls-video/index.js.map +1 -1
- package/dist/dev/media/mux-video/index.d.ts +11 -0
- package/dist/dev/media/mux-video/index.d.ts.map +1 -0
- package/dist/dev/media/mux-video/index.js +18 -0
- package/dist/dev/media/mux-video/index.js.map +1 -0
- package/dist/dev/media/native-hls-video/index.d.ts +11 -0
- package/dist/dev/media/native-hls-video/index.d.ts.map +1 -0
- package/dist/dev/media/native-hls-video/index.js +18 -0
- package/dist/dev/media/native-hls-video/index.js.map +1 -0
- package/dist/dev/media/simple-hls-video/index.d.ts +0 -2
- package/dist/dev/media/simple-hls-video/index.d.ts.map +1 -1
- package/dist/dev/media/simple-hls-video/index.js +4 -12
- package/dist/dev/media/simple-hls-video/index.js.map +1 -1
- package/dist/dev/player/context.js +1 -2
- package/dist/dev/player/context.js.map +1 -1
- package/dist/dev/player/create-player.js +1 -2
- package/dist/dev/player/create-player.js.map +1 -1
- package/dist/dev/player/player-controller.js +1 -2
- package/dist/dev/player/player-controller.js.map +1 -1
- package/dist/dev/presets/audio.js +1 -2
- package/dist/dev/presets/background.js +1 -2
- package/dist/dev/presets/video.js +1 -2
- package/dist/dev/skins/dist/default/default/tailwind/audio.tailwind.js +14 -18
- package/dist/dev/skins/dist/default/default/tailwind/audio.tailwind.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/buffering.js +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/buffering.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/button-group.js +7 -0
- package/dist/dev/skins/dist/default/default/tailwind/components/button-group.js.map +1 -0
- package/dist/dev/skins/dist/default/default/tailwind/components/button.js +3 -4
- package/dist/dev/skins/dist/default/default/tailwind/components/button.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/controls.js +2 -3
- package/dist/dev/skins/dist/default/default/tailwind/components/controls.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/error.js +2 -3
- package/dist/dev/skins/dist/default/default/tailwind/components/error.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/icon.js +1 -2
- package/dist/dev/skins/dist/default/default/tailwind/components/icon.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/overlay.js +2 -3
- package/dist/dev/skins/dist/default/default/tailwind/components/overlay.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/playback-rate.js +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/playback-rate.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/popup.js +4 -5
- package/dist/dev/skins/dist/default/default/tailwind/components/popup.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/poster.js +1 -2
- package/dist/dev/skins/dist/default/default/tailwind/components/poster.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/preview.js +2 -3
- package/dist/dev/skins/dist/default/default/tailwind/components/preview.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/root.js +2 -3
- package/dist/dev/skins/dist/default/default/tailwind/components/root.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/seek.js +1 -2
- package/dist/dev/skins/dist/default/default/tailwind/components/seek.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/slider.js +2 -3
- package/dist/dev/skins/dist/default/default/tailwind/components/slider.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/surface.js +2 -3
- package/dist/dev/skins/dist/default/default/tailwind/components/surface.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/time.js +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/components/time.js.map +1 -1
- package/dist/dev/skins/dist/default/default/tailwind/video.tailwind.js +25 -20
- package/dist/dev/skins/dist/default/default/tailwind/video.tailwind.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/audio.tailwind.js +15 -16
- package/dist/dev/skins/dist/default/minimal/tailwind/audio.tailwind.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/buffering.js +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/buffering.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/button-group.js +1 -2
- package/dist/dev/skins/dist/default/minimal/tailwind/components/button-group.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/button.js +2 -3
- package/dist/dev/skins/dist/default/minimal/tailwind/components/button.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/controls.js +2 -3
- package/dist/dev/skins/dist/default/minimal/tailwind/components/controls.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/error.js +2 -3
- package/dist/dev/skins/dist/default/minimal/tailwind/components/error.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/icon.js +1 -2
- package/dist/dev/skins/dist/default/minimal/tailwind/components/icon.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/overlay.js +2 -3
- package/dist/dev/skins/dist/default/minimal/tailwind/components/overlay.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/playback-rate.js +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/playback-rate.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/popup.js +2 -3
- package/dist/dev/skins/dist/default/minimal/tailwind/components/popup.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/poster.js +1 -2
- package/dist/dev/skins/dist/default/minimal/tailwind/components/poster.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/preview.js +2 -3
- package/dist/dev/skins/dist/default/minimal/tailwind/components/preview.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/root.js +2 -3
- package/dist/dev/skins/dist/default/minimal/tailwind/components/root.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/seek.js +1 -2
- package/dist/dev/skins/dist/default/minimal/tailwind/components/seek.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/slider.js +2 -3
- package/dist/dev/skins/dist/default/minimal/tailwind/components/slider.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/components/time.js +5 -6
- package/dist/dev/skins/dist/default/minimal/tailwind/components/time.js.map +1 -1
- package/dist/dev/skins/dist/default/minimal/tailwind/video.tailwind.js +23 -18
- package/dist/dev/skins/dist/default/minimal/tailwind/video.tailwind.js.map +1 -1
- package/dist/dev/skins/dist/default/shared/tailwind/icon-state.js +1 -1
- package/dist/dev/skins/dist/default/shared/tailwind/icon-state.js.map +1 -1
- package/dist/dev/skins/dist/default/shared/tailwind/tooltip-state.js +1 -1
- package/dist/dev/skins/dist/default/shared/tailwind/tooltip-state.js.map +1 -1
- package/dist/dev/store/container-mixin.js +1 -2
- package/dist/dev/store/container-mixin.js.map +1 -1
- package/dist/dev/store/media-attach-mixin.js +1 -2
- package/dist/dev/store/media-attach-mixin.js.map +1 -1
- package/dist/dev/store/provider-mixin.js +1 -2
- package/dist/dev/store/provider-mixin.js.map +1 -1
- package/dist/dev/ui/alert-dialog/alert-dialog-close-element.js +1 -2
- package/dist/dev/ui/alert-dialog/alert-dialog-close-element.js.map +1 -1
- package/dist/dev/ui/alert-dialog/alert-dialog-description-element.js +1 -2
- package/dist/dev/ui/alert-dialog/alert-dialog-description-element.js.map +1 -1
- package/dist/dev/ui/alert-dialog/alert-dialog-element.js +1 -2
- package/dist/dev/ui/alert-dialog/alert-dialog-element.js.map +1 -1
- package/dist/dev/ui/alert-dialog/alert-dialog-title-element.js +1 -2
- package/dist/dev/ui/alert-dialog/alert-dialog-title-element.js.map +1 -1
- package/dist/dev/ui/alert-dialog/context.js +2 -5
- package/dist/dev/ui/alert-dialog/context.js.map +1 -1
- package/dist/dev/ui/buffering-indicator/buffering-indicator-element.js +1 -2
- package/dist/dev/ui/buffering-indicator/buffering-indicator-element.js.map +1 -1
- package/dist/dev/ui/captions-button/captions-button-element.js +1 -2
- package/dist/dev/ui/captions-button/captions-button-element.js.map +1 -1
- package/dist/dev/ui/context-part-element.js +1 -2
- package/dist/dev/ui/context-part-element.js.map +1 -1
- package/dist/dev/ui/controls/context.js +2 -5
- package/dist/dev/ui/controls/context.js.map +1 -1
- package/dist/dev/ui/controls/controls-element.js +1 -2
- package/dist/dev/ui/controls/controls-element.js.map +1 -1
- package/dist/dev/ui/controls/controls-group-element.js +1 -2
- package/dist/dev/ui/controls/controls-group-element.js.map +1 -1
- package/dist/dev/ui/error-dialog/error-dialog-element.d.ts +16 -0
- package/dist/dev/ui/error-dialog/error-dialog-element.d.ts.map +1 -0
- package/dist/dev/ui/error-dialog/error-dialog-element.js +76 -0
- package/dist/dev/ui/error-dialog/error-dialog-element.js.map +1 -0
- package/dist/dev/ui/fullscreen-button/fullscreen-button-element.js +1 -2
- package/dist/dev/ui/fullscreen-button/fullscreen-button-element.js.map +1 -1
- package/dist/dev/ui/media-button-element.js +1 -2
- package/dist/dev/ui/media-button-element.js.map +1 -1
- package/dist/dev/ui/media-element.js +1 -2
- package/dist/dev/ui/media-element.js.map +1 -1
- package/dist/dev/ui/media-ui-element.js +1 -2
- package/dist/dev/ui/media-ui-element.js.map +1 -1
- package/dist/dev/ui/mute-button/mute-button-element.js +1 -2
- package/dist/dev/ui/mute-button/mute-button-element.js.map +1 -1
- package/dist/dev/ui/pip-button/pip-button-element.js +1 -2
- package/dist/dev/ui/pip-button/pip-button-element.js.map +1 -1
- package/dist/dev/ui/play-button/play-button-element.js +1 -2
- package/dist/dev/ui/play-button/play-button-element.js.map +1 -1
- package/dist/dev/ui/playback-rate-button/playback-rate-button-element.js +1 -2
- package/dist/dev/ui/playback-rate-button/playback-rate-button-element.js.map +1 -1
- package/dist/dev/ui/popover/popover-element.js +1 -2
- package/dist/dev/ui/popover/popover-element.js.map +1 -1
- package/dist/dev/ui/poster/poster-element.js +1 -2
- package/dist/dev/ui/poster/poster-element.js.map +1 -1
- package/dist/dev/ui/seek-button/seek-button-element.js +1 -2
- package/dist/dev/ui/seek-button/seek-button-element.js.map +1 -1
- package/dist/dev/ui/slider/context.js +2 -5
- package/dist/dev/ui/slider/context.js.map +1 -1
- package/dist/dev/ui/slider/slider-buffer-element.js +1 -2
- package/dist/dev/ui/slider/slider-buffer-element.js.map +1 -1
- package/dist/dev/ui/slider/slider-element.js +1 -2
- package/dist/dev/ui/slider/slider-element.js.map +1 -1
- package/dist/dev/ui/slider/slider-fill-element.js +1 -2
- package/dist/dev/ui/slider/slider-fill-element.js.map +1 -1
- package/dist/dev/ui/slider/slider-preview-element.js +1 -2
- package/dist/dev/ui/slider/slider-preview-element.js.map +1 -1
- package/dist/dev/ui/slider/slider-thumb-element.js +1 -2
- package/dist/dev/ui/slider/slider-thumb-element.js.map +1 -1
- package/dist/dev/ui/slider/slider-thumbnail-element.js +1 -2
- package/dist/dev/ui/slider/slider-thumbnail-element.js.map +1 -1
- package/dist/dev/ui/slider/slider-track-element.js +1 -2
- package/dist/dev/ui/slider/slider-track-element.js.map +1 -1
- package/dist/dev/ui/slider/slider-value-element.js +1 -2
- package/dist/dev/ui/slider/slider-value-element.js.map +1 -1
- package/dist/dev/ui/thumbnail/thumbnail-element.js +1 -2
- package/dist/dev/ui/thumbnail/thumbnail-element.js.map +1 -1
- package/dist/dev/ui/time/time-element.js +1 -2
- package/dist/dev/ui/time/time-element.js.map +1 -1
- package/dist/dev/ui/time/time-group-element.js +1 -2
- package/dist/dev/ui/time/time-group-element.js.map +1 -1
- package/dist/dev/ui/time/time-separator-element.js +1 -2
- package/dist/dev/ui/time/time-separator-element.js.map +1 -1
- package/dist/dev/ui/time-slider/time-slider-element.js +1 -2
- package/dist/dev/ui/time-slider/time-slider-element.js.map +1 -1
- package/dist/dev/ui/tooltip/context.js +2 -5
- package/dist/dev/ui/tooltip/context.js.map +1 -1
- package/dist/dev/ui/tooltip/tooltip-element.js +1 -2
- package/dist/dev/ui/tooltip/tooltip-element.js.map +1 -1
- package/dist/dev/ui/tooltip/tooltip-group-element.js +1 -2
- package/dist/dev/ui/tooltip/tooltip-group-element.js.map +1 -1
- package/dist/dev/ui/volume-slider/volume-slider-element.js +1 -2
- package/dist/dev/ui/volume-slider/volume-slider-element.js.map +1 -1
- package/dist/dev/utils/media-props-mixin.js +44 -0
- package/dist/dev/utils/media-props-mixin.js.map +1 -0
- package/package.json +10 -10
- package/cdn/context-Be8C5kVd.js.map +0 -1
- package/cdn/context-CUBywtsB.js +0 -14
- package/cdn/context-CUBywtsB.js.map +0 -1
- package/cdn/create-player-AcfnN3li.js.map +0 -1
- package/cdn/create-player-s_qISCpw.js +0 -7
- package/cdn/create-player-s_qISCpw.js.map +0 -1
- package/cdn/custom-media-element-DqevSVgS.js.map +0 -1
- package/cdn/custom-media-element-moFa3UZp.js.map +0 -1
- package/cdn/delegate-CzAcT1xm.js.map +0 -1
- package/cdn/delegate-Uc-6tQDR.js +0 -2
- package/cdn/delegate-Uc-6tQDR.js.map +0 -1
- package/cdn/media-attach-mixin-D5_nfJpa.js +0 -2
- package/cdn/player-C46h14iP.js +0 -2
- package/cdn/poster-odJ4iwIv.js +0 -2
- package/cdn/predicate-BG-dj_kF.js +0 -26
- package/cdn/predicate-BG-dj_kF.js.map +0 -1
- package/cdn/predicate-Y9jDHLpX.js +0 -2
- package/cdn/predicate-Y9jDHLpX.js.map +0 -1
- package/cdn/safe-define-B8lHgj_K.js +0 -9
- package/cdn/safe-define-B8lHgj_K.js.map +0 -1
- package/cdn/safe-define-GrHW3P9e.js +0 -2
- package/cdn/safe-define-GrHW3P9e.js.map +0 -1
- package/cdn/volume-slider-D7BOdSDF.js.map +0 -1
- package/cdn/volume-slider-DPeFF5tt.js +0 -8
- package/cdn/volume-slider-DPeFF5tt.js.map +0 -1
|
@@ -1,151 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import "../
|
|
3
|
-
import { t as listen } from "../listen-
|
|
4
|
-
import { t as DelegateMixin } from "../delegate-
|
|
5
|
-
import { t as MediaAttachMixin } from "../media-attach-mixin-
|
|
6
|
-
import { t as
|
|
1
|
+
import { t as anyAbortSignal } from "../abort-JT-ewLFq.js";
|
|
2
|
+
import { t as safeDefine } from "../safe-define-D26LrTu4.js";
|
|
3
|
+
import { t as listen } from "../listen-BkAEGXCe.js";
|
|
4
|
+
import { t as DelegateMixin } from "../delegate-CSc5c0ZR.js";
|
|
5
|
+
import { t as MediaAttachMixin } from "../media-attach-mixin-Dsn4gxJA.js";
|
|
6
|
+
import { n as CustomVideoElement, t as MediaPropsMixin } from "../media-props-mixin-DxsM38Bx.js";
|
|
7
7
|
|
|
8
|
-
//#region ../spf/dist/dev/core/state/create-state.js
|
|
9
|
-
/**
|
|
10
|
-
* Reactive state container with selectors, custom equality, and batched updates.
|
|
11
|
-
*
|
|
12
|
-
* Manages both immutable state values and mutable object references (e.g., HTMLMediaElement).
|
|
13
|
-
*/
|
|
14
|
-
const STATE_SYMBOL = Symbol("@videojs/spf/state");
|
|
15
|
-
/**
|
|
16
|
-
* Default equality function using Object.is.
|
|
17
|
-
*/
|
|
18
|
-
function defaultEquality(a, b) {
|
|
19
|
-
return Object.is(a, b);
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* State container implementation.
|
|
23
|
-
*/
|
|
24
|
-
var StateContainer = class {
|
|
25
|
-
[STATE_SYMBOL] = true;
|
|
26
|
-
#current;
|
|
27
|
-
#pending = null;
|
|
28
|
-
#pendingFlush = false;
|
|
29
|
-
#equalityFn;
|
|
30
|
-
#listeners = /* @__PURE__ */ new Set();
|
|
31
|
-
#selectorListeners = /* @__PURE__ */ new Set();
|
|
32
|
-
constructor(initial, config) {
|
|
33
|
-
this.#current = typeof initial === "object" && initial !== null ? { ...initial } : initial;
|
|
34
|
-
this.#equalityFn = config?.equalityFn ?? defaultEquality;
|
|
35
|
-
}
|
|
36
|
-
get current() {
|
|
37
|
-
return this.#pending ?? this.#current;
|
|
38
|
-
}
|
|
39
|
-
patch(partial) {
|
|
40
|
-
const base = this.#pending ?? this.#current;
|
|
41
|
-
if (typeof base !== "object" || base === null) {
|
|
42
|
-
const value = partial;
|
|
43
|
-
if (!Object.is(base, value)) {
|
|
44
|
-
this.#pending = value;
|
|
45
|
-
this.#scheduleFlush();
|
|
46
|
-
}
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const next = { ...base };
|
|
50
|
-
let changed = false;
|
|
51
|
-
for (const key in partial) {
|
|
52
|
-
if (!Object.hasOwn(partial, key)) continue;
|
|
53
|
-
const value = partial[key];
|
|
54
|
-
if (!Object.is(base[key], value)) {
|
|
55
|
-
next[key] = value;
|
|
56
|
-
changed = true;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (changed) {
|
|
60
|
-
this.#pending = next;
|
|
61
|
-
this.#scheduleFlush();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
subscribe(selectorOrListener, maybeListener, options) {
|
|
65
|
-
if (maybeListener === void 0) {
|
|
66
|
-
const listener = selectorOrListener;
|
|
67
|
-
this.#listeners.add(listener);
|
|
68
|
-
listener(this.current);
|
|
69
|
-
return () => {
|
|
70
|
-
this.#listeners.delete(listener);
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
const selector = selectorOrListener;
|
|
74
|
-
const listener = maybeListener;
|
|
75
|
-
const entry = {
|
|
76
|
-
selector,
|
|
77
|
-
listener,
|
|
78
|
-
options: options ?? {}
|
|
79
|
-
};
|
|
80
|
-
this.#selectorListeners.add(entry);
|
|
81
|
-
listener(selector(this.current));
|
|
82
|
-
return () => {
|
|
83
|
-
this.#selectorListeners.delete(entry);
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
flush() {
|
|
87
|
-
if (this.#pending === null) return;
|
|
88
|
-
const prev = this.#current;
|
|
89
|
-
const next = this.#pending;
|
|
90
|
-
this.#pending = null;
|
|
91
|
-
this.#pendingFlush = false;
|
|
92
|
-
if (this.#equalityFn(prev, next)) return;
|
|
93
|
-
this.#current = next;
|
|
94
|
-
for (const listener of this.#listeners) listener(this.#current);
|
|
95
|
-
for (const entry of this.#selectorListeners) {
|
|
96
|
-
const prevSelected = entry.selector(prev);
|
|
97
|
-
const nextSelected = entry.selector(this.#current);
|
|
98
|
-
if (!(entry.options.equalityFn ?? Object.is)(prevSelected, nextSelected)) entry.listener(nextSelected);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
#scheduleFlush() {
|
|
102
|
-
if (this.#pendingFlush) return;
|
|
103
|
-
this.#pendingFlush = true;
|
|
104
|
-
queueMicrotask(() => this.flush());
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
/**
|
|
108
|
-
* Create a reactive state container.
|
|
109
|
-
*
|
|
110
|
-
* @example
|
|
111
|
-
* ```typescript
|
|
112
|
-
* const state = createState({ count: 0 });
|
|
113
|
-
*
|
|
114
|
-
* // Subscribe to changes
|
|
115
|
-
* state.subscribe((current, prev) => {
|
|
116
|
-
* console.log('Changed:', prev, '->', current);
|
|
117
|
-
* });
|
|
118
|
-
*
|
|
119
|
-
* // Updates are batched
|
|
120
|
-
* state.patch({ count: 1 });
|
|
121
|
-
* state.patch({ count: 2 });
|
|
122
|
-
* // Only one notification fires (with count: 2)
|
|
123
|
-
* ```
|
|
124
|
-
*
|
|
125
|
-
* @example Selector subscriptions
|
|
126
|
-
* ```typescript
|
|
127
|
-
* const state = createState({ count: 0, name: 'test' });
|
|
128
|
-
*
|
|
129
|
-
* // Only notified when count changes
|
|
130
|
-
* state.subscribe(
|
|
131
|
-
* s => s.count,
|
|
132
|
-
* (current, prev) => console.log(current, prev)
|
|
133
|
-
* );
|
|
134
|
-
* ```
|
|
135
|
-
*
|
|
136
|
-
* @example Custom equality
|
|
137
|
-
* ```typescript
|
|
138
|
-
* const state = createState(
|
|
139
|
-
* { count: 0, name: 'test' },
|
|
140
|
-
* { equalityFn: (a, b) => a.count === b.count }
|
|
141
|
-
* );
|
|
142
|
-
* ```
|
|
143
|
-
*/
|
|
144
|
-
function createState(initial, config) {
|
|
145
|
-
return new StateContainer(initial, config);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
//#endregion
|
|
149
8
|
//#region ../spf/dist/dev/core/abr/ewma.js
|
|
150
9
|
/**
|
|
151
10
|
* Exponentially Weighted Moving Average (EWMA)
|
|
@@ -377,6 +236,558 @@ function getSegmentsToLoad(segments, bufferedSegments, currentTime, config = DEF
|
|
|
377
236
|
});
|
|
378
237
|
}
|
|
379
238
|
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region ../../node_modules/.pnpm/signal-polyfill@0.2.2/node_modules/signal-polyfill/dist/index.js
|
|
241
|
+
var __defProp = Object.defineProperty;
|
|
242
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
|
|
243
|
+
enumerable: true,
|
|
244
|
+
configurable: true,
|
|
245
|
+
writable: true,
|
|
246
|
+
value
|
|
247
|
+
}) : obj[key] = value;
|
|
248
|
+
var __publicField = (obj, key, value) => {
|
|
249
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
250
|
+
return value;
|
|
251
|
+
};
|
|
252
|
+
var __accessCheck = (obj, member, msg) => {
|
|
253
|
+
if (!member.has(obj)) throw TypeError("Cannot " + msg);
|
|
254
|
+
};
|
|
255
|
+
var __privateIn = (member, obj) => {
|
|
256
|
+
if (Object(obj) !== obj) throw TypeError("Cannot use the \"in\" operator on this value");
|
|
257
|
+
return member.has(obj);
|
|
258
|
+
};
|
|
259
|
+
var __privateAdd = (obj, member, value) => {
|
|
260
|
+
if (member.has(obj)) throw TypeError("Cannot add the same private member more than once");
|
|
261
|
+
member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
262
|
+
};
|
|
263
|
+
var __privateMethod = (obj, member, method) => {
|
|
264
|
+
__accessCheck(obj, member, "access private method");
|
|
265
|
+
return method;
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* @license
|
|
269
|
+
* Copyright Google LLC All Rights Reserved.
|
|
270
|
+
*
|
|
271
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
272
|
+
* found in the LICENSE file at https://angular.io/license
|
|
273
|
+
*/
|
|
274
|
+
function defaultEquals(a, b) {
|
|
275
|
+
return Object.is(a, b);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* @license
|
|
279
|
+
* Copyright Google LLC All Rights Reserved.
|
|
280
|
+
*
|
|
281
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
282
|
+
* found in the LICENSE file at https://angular.io/license
|
|
283
|
+
*/
|
|
284
|
+
let activeConsumer = null;
|
|
285
|
+
let inNotificationPhase = false;
|
|
286
|
+
let epoch = 1;
|
|
287
|
+
const SIGNAL = /* @__PURE__ */ Symbol("SIGNAL");
|
|
288
|
+
function setActiveConsumer(consumer) {
|
|
289
|
+
const prev = activeConsumer;
|
|
290
|
+
activeConsumer = consumer;
|
|
291
|
+
return prev;
|
|
292
|
+
}
|
|
293
|
+
function getActiveConsumer() {
|
|
294
|
+
return activeConsumer;
|
|
295
|
+
}
|
|
296
|
+
function isInNotificationPhase() {
|
|
297
|
+
return inNotificationPhase;
|
|
298
|
+
}
|
|
299
|
+
const REACTIVE_NODE = {
|
|
300
|
+
version: 0,
|
|
301
|
+
lastCleanEpoch: 0,
|
|
302
|
+
dirty: false,
|
|
303
|
+
producerNode: void 0,
|
|
304
|
+
producerLastReadVersion: void 0,
|
|
305
|
+
producerIndexOfThis: void 0,
|
|
306
|
+
nextProducerIndex: 0,
|
|
307
|
+
liveConsumerNode: void 0,
|
|
308
|
+
liveConsumerIndexOfThis: void 0,
|
|
309
|
+
consumerAllowSignalWrites: false,
|
|
310
|
+
consumerIsAlwaysLive: false,
|
|
311
|
+
producerMustRecompute: () => false,
|
|
312
|
+
producerRecomputeValue: () => {},
|
|
313
|
+
consumerMarkedDirty: () => {},
|
|
314
|
+
consumerOnSignalRead: () => {}
|
|
315
|
+
};
|
|
316
|
+
function producerAccessed(node) {
|
|
317
|
+
if (inNotificationPhase) throw new Error(typeof ngDevMode !== "undefined" && ngDevMode ? `Assertion error: signal read during notification phase` : "");
|
|
318
|
+
if (activeConsumer === null) return;
|
|
319
|
+
activeConsumer.consumerOnSignalRead(node);
|
|
320
|
+
const idx = activeConsumer.nextProducerIndex++;
|
|
321
|
+
assertConsumerNode(activeConsumer);
|
|
322
|
+
if (idx < activeConsumer.producerNode.length && activeConsumer.producerNode[idx] !== node) {
|
|
323
|
+
if (consumerIsLive(activeConsumer)) {
|
|
324
|
+
const staleProducer = activeConsumer.producerNode[idx];
|
|
325
|
+
producerRemoveLiveConsumerAtIndex(staleProducer, activeConsumer.producerIndexOfThis[idx]);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (activeConsumer.producerNode[idx] !== node) {
|
|
329
|
+
activeConsumer.producerNode[idx] = node;
|
|
330
|
+
activeConsumer.producerIndexOfThis[idx] = consumerIsLive(activeConsumer) ? producerAddLiveConsumer(node, activeConsumer, idx) : 0;
|
|
331
|
+
}
|
|
332
|
+
activeConsumer.producerLastReadVersion[idx] = node.version;
|
|
333
|
+
}
|
|
334
|
+
function producerIncrementEpoch() {
|
|
335
|
+
epoch++;
|
|
336
|
+
}
|
|
337
|
+
function producerUpdateValueVersion(node) {
|
|
338
|
+
if (!node.dirty && node.lastCleanEpoch === epoch) return;
|
|
339
|
+
if (!node.producerMustRecompute(node) && !consumerPollProducersForChange(node)) {
|
|
340
|
+
node.dirty = false;
|
|
341
|
+
node.lastCleanEpoch = epoch;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
node.producerRecomputeValue(node);
|
|
345
|
+
node.dirty = false;
|
|
346
|
+
node.lastCleanEpoch = epoch;
|
|
347
|
+
}
|
|
348
|
+
function producerNotifyConsumers(node) {
|
|
349
|
+
if (node.liveConsumerNode === void 0) return;
|
|
350
|
+
const prev = inNotificationPhase;
|
|
351
|
+
inNotificationPhase = true;
|
|
352
|
+
try {
|
|
353
|
+
for (const consumer of node.liveConsumerNode) if (!consumer.dirty) consumerMarkDirty(consumer);
|
|
354
|
+
} finally {
|
|
355
|
+
inNotificationPhase = prev;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function producerUpdatesAllowed() {
|
|
359
|
+
return (activeConsumer == null ? void 0 : activeConsumer.consumerAllowSignalWrites) !== false;
|
|
360
|
+
}
|
|
361
|
+
function consumerMarkDirty(node) {
|
|
362
|
+
var _a;
|
|
363
|
+
node.dirty = true;
|
|
364
|
+
producerNotifyConsumers(node);
|
|
365
|
+
(_a = node.consumerMarkedDirty) == null || _a.call(node.wrapper ?? node);
|
|
366
|
+
}
|
|
367
|
+
function consumerBeforeComputation(node) {
|
|
368
|
+
node && (node.nextProducerIndex = 0);
|
|
369
|
+
return setActiveConsumer(node);
|
|
370
|
+
}
|
|
371
|
+
function consumerAfterComputation(node, prevConsumer) {
|
|
372
|
+
setActiveConsumer(prevConsumer);
|
|
373
|
+
if (!node || node.producerNode === void 0 || node.producerIndexOfThis === void 0 || node.producerLastReadVersion === void 0) return;
|
|
374
|
+
if (consumerIsLive(node)) for (let i = node.nextProducerIndex; i < node.producerNode.length; i++) producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
|
|
375
|
+
while (node.producerNode.length > node.nextProducerIndex) {
|
|
376
|
+
node.producerNode.pop();
|
|
377
|
+
node.producerLastReadVersion.pop();
|
|
378
|
+
node.producerIndexOfThis.pop();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function consumerPollProducersForChange(node) {
|
|
382
|
+
assertConsumerNode(node);
|
|
383
|
+
for (let i = 0; i < node.producerNode.length; i++) {
|
|
384
|
+
const producer = node.producerNode[i];
|
|
385
|
+
const seenVersion = node.producerLastReadVersion[i];
|
|
386
|
+
if (seenVersion !== producer.version) return true;
|
|
387
|
+
producerUpdateValueVersion(producer);
|
|
388
|
+
if (seenVersion !== producer.version) return true;
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
function producerAddLiveConsumer(node, consumer, indexOfThis) {
|
|
393
|
+
var _a;
|
|
394
|
+
assertProducerNode(node);
|
|
395
|
+
assertConsumerNode(node);
|
|
396
|
+
if (node.liveConsumerNode.length === 0) {
|
|
397
|
+
(_a = node.watched) == null || _a.call(node.wrapper);
|
|
398
|
+
for (let i = 0; i < node.producerNode.length; i++) node.producerIndexOfThis[i] = producerAddLiveConsumer(node.producerNode[i], node, i);
|
|
399
|
+
}
|
|
400
|
+
node.liveConsumerIndexOfThis.push(indexOfThis);
|
|
401
|
+
return node.liveConsumerNode.push(consumer) - 1;
|
|
402
|
+
}
|
|
403
|
+
function producerRemoveLiveConsumerAtIndex(node, idx) {
|
|
404
|
+
var _a;
|
|
405
|
+
assertProducerNode(node);
|
|
406
|
+
assertConsumerNode(node);
|
|
407
|
+
if (typeof ngDevMode !== "undefined" && ngDevMode && idx >= node.liveConsumerNode.length) throw new Error(`Assertion error: active consumer index ${idx} is out of bounds of ${node.liveConsumerNode.length} consumers)`);
|
|
408
|
+
if (node.liveConsumerNode.length === 1) {
|
|
409
|
+
(_a = node.unwatched) == null || _a.call(node.wrapper);
|
|
410
|
+
for (let i = 0; i < node.producerNode.length; i++) producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
|
|
411
|
+
}
|
|
412
|
+
const lastIdx = node.liveConsumerNode.length - 1;
|
|
413
|
+
node.liveConsumerNode[idx] = node.liveConsumerNode[lastIdx];
|
|
414
|
+
node.liveConsumerIndexOfThis[idx] = node.liveConsumerIndexOfThis[lastIdx];
|
|
415
|
+
node.liveConsumerNode.length--;
|
|
416
|
+
node.liveConsumerIndexOfThis.length--;
|
|
417
|
+
if (idx < node.liveConsumerNode.length) {
|
|
418
|
+
const idxProducer = node.liveConsumerIndexOfThis[idx];
|
|
419
|
+
const consumer = node.liveConsumerNode[idx];
|
|
420
|
+
assertConsumerNode(consumer);
|
|
421
|
+
consumer.producerIndexOfThis[idxProducer] = idx;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function consumerIsLive(node) {
|
|
425
|
+
var _a;
|
|
426
|
+
return node.consumerIsAlwaysLive || (((_a = node == null ? void 0 : node.liveConsumerNode) == null ? void 0 : _a.length) ?? 0) > 0;
|
|
427
|
+
}
|
|
428
|
+
function assertConsumerNode(node) {
|
|
429
|
+
node.producerNode ?? (node.producerNode = []);
|
|
430
|
+
node.producerIndexOfThis ?? (node.producerIndexOfThis = []);
|
|
431
|
+
node.producerLastReadVersion ?? (node.producerLastReadVersion = []);
|
|
432
|
+
}
|
|
433
|
+
function assertProducerNode(node) {
|
|
434
|
+
node.liveConsumerNode ?? (node.liveConsumerNode = []);
|
|
435
|
+
node.liveConsumerIndexOfThis ?? (node.liveConsumerIndexOfThis = []);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* @license
|
|
439
|
+
* Copyright Google LLC All Rights Reserved.
|
|
440
|
+
*
|
|
441
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
442
|
+
* found in the LICENSE file at https://angular.io/license
|
|
443
|
+
*/
|
|
444
|
+
function computedGet(node) {
|
|
445
|
+
producerUpdateValueVersion(node);
|
|
446
|
+
producerAccessed(node);
|
|
447
|
+
if (node.value === ERRORED) throw node.error;
|
|
448
|
+
return node.value;
|
|
449
|
+
}
|
|
450
|
+
function createComputed(computation) {
|
|
451
|
+
const node = Object.create(COMPUTED_NODE);
|
|
452
|
+
node.computation = computation;
|
|
453
|
+
const computed = () => computedGet(node);
|
|
454
|
+
computed[SIGNAL] = node;
|
|
455
|
+
return computed;
|
|
456
|
+
}
|
|
457
|
+
const UNSET = /* @__PURE__ */ Symbol("UNSET");
|
|
458
|
+
const COMPUTING = /* @__PURE__ */ Symbol("COMPUTING");
|
|
459
|
+
const ERRORED = /* @__PURE__ */ Symbol("ERRORED");
|
|
460
|
+
const COMPUTED_NODE = {
|
|
461
|
+
...REACTIVE_NODE,
|
|
462
|
+
value: UNSET,
|
|
463
|
+
dirty: true,
|
|
464
|
+
error: null,
|
|
465
|
+
equal: defaultEquals,
|
|
466
|
+
producerMustRecompute(node) {
|
|
467
|
+
return node.value === UNSET || node.value === COMPUTING;
|
|
468
|
+
},
|
|
469
|
+
producerRecomputeValue(node) {
|
|
470
|
+
if (node.value === COMPUTING) throw new Error("Detected cycle in computations.");
|
|
471
|
+
const oldValue = node.value;
|
|
472
|
+
node.value = COMPUTING;
|
|
473
|
+
const prevConsumer = consumerBeforeComputation(node);
|
|
474
|
+
let newValue;
|
|
475
|
+
let wasEqual = false;
|
|
476
|
+
try {
|
|
477
|
+
newValue = node.computation.call(node.wrapper);
|
|
478
|
+
wasEqual = oldValue !== UNSET && oldValue !== ERRORED && node.equal.call(node.wrapper, oldValue, newValue);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
newValue = ERRORED;
|
|
481
|
+
node.error = err;
|
|
482
|
+
} finally {
|
|
483
|
+
consumerAfterComputation(node, prevConsumer);
|
|
484
|
+
}
|
|
485
|
+
if (wasEqual) {
|
|
486
|
+
node.value = oldValue;
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
node.value = newValue;
|
|
490
|
+
node.version++;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
/**
|
|
494
|
+
* @license
|
|
495
|
+
* Copyright Google LLC All Rights Reserved.
|
|
496
|
+
*
|
|
497
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
498
|
+
* found in the LICENSE file at https://angular.io/license
|
|
499
|
+
*/
|
|
500
|
+
function defaultThrowError() {
|
|
501
|
+
throw new Error();
|
|
502
|
+
}
|
|
503
|
+
let throwInvalidWriteToSignalErrorFn = defaultThrowError;
|
|
504
|
+
function throwInvalidWriteToSignalError() {
|
|
505
|
+
throwInvalidWriteToSignalErrorFn();
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* @license
|
|
509
|
+
* Copyright Google LLC All Rights Reserved.
|
|
510
|
+
*
|
|
511
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
512
|
+
* found in the LICENSE file at https://angular.io/license
|
|
513
|
+
*/
|
|
514
|
+
function createSignal(initialValue) {
|
|
515
|
+
const node = Object.create(SIGNAL_NODE);
|
|
516
|
+
node.value = initialValue;
|
|
517
|
+
const getter = () => {
|
|
518
|
+
producerAccessed(node);
|
|
519
|
+
return node.value;
|
|
520
|
+
};
|
|
521
|
+
getter[SIGNAL] = node;
|
|
522
|
+
return getter;
|
|
523
|
+
}
|
|
524
|
+
function signalGetFn() {
|
|
525
|
+
producerAccessed(this);
|
|
526
|
+
return this.value;
|
|
527
|
+
}
|
|
528
|
+
function signalSetFn(node, newValue) {
|
|
529
|
+
if (!producerUpdatesAllowed()) throwInvalidWriteToSignalError();
|
|
530
|
+
if (!node.equal.call(node.wrapper, node.value, newValue)) {
|
|
531
|
+
node.value = newValue;
|
|
532
|
+
signalValueChanged(node);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const SIGNAL_NODE = {
|
|
536
|
+
...REACTIVE_NODE,
|
|
537
|
+
equal: defaultEquals,
|
|
538
|
+
value: void 0
|
|
539
|
+
};
|
|
540
|
+
function signalValueChanged(node) {
|
|
541
|
+
node.version++;
|
|
542
|
+
producerIncrementEpoch();
|
|
543
|
+
producerNotifyConsumers(node);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* @license
|
|
547
|
+
* Copyright 2024 Bloomberg Finance L.P.
|
|
548
|
+
*
|
|
549
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
550
|
+
* you may not use this file except in compliance with the License.
|
|
551
|
+
* You may obtain a copy of the License at
|
|
552
|
+
*
|
|
553
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
554
|
+
*
|
|
555
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
556
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
557
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
558
|
+
* See the License for the specific language governing permissions and
|
|
559
|
+
* limitations under the License.
|
|
560
|
+
*/
|
|
561
|
+
const NODE = Symbol("node");
|
|
562
|
+
var Signal;
|
|
563
|
+
((Signal2) => {
|
|
564
|
+
var _a, _brand, _b, _brand2;
|
|
565
|
+
class State {
|
|
566
|
+
constructor(initialValue, options = {}) {
|
|
567
|
+
__privateAdd(this, _brand);
|
|
568
|
+
__publicField(this, _a);
|
|
569
|
+
const node = createSignal(initialValue)[SIGNAL];
|
|
570
|
+
this[NODE] = node;
|
|
571
|
+
node.wrapper = this;
|
|
572
|
+
if (options) {
|
|
573
|
+
const equals = options.equals;
|
|
574
|
+
if (equals) node.equal = equals;
|
|
575
|
+
node.watched = options[Signal2.subtle.watched];
|
|
576
|
+
node.unwatched = options[Signal2.subtle.unwatched];
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
get() {
|
|
580
|
+
if (!(0, Signal2.isState)(this)) throw new TypeError("Wrong receiver type for Signal.State.prototype.get");
|
|
581
|
+
return signalGetFn.call(this[NODE]);
|
|
582
|
+
}
|
|
583
|
+
set(newValue) {
|
|
584
|
+
if (!(0, Signal2.isState)(this)) throw new TypeError("Wrong receiver type for Signal.State.prototype.set");
|
|
585
|
+
if (isInNotificationPhase()) throw new Error("Writes to signals not permitted during Watcher callback");
|
|
586
|
+
const ref = this[NODE];
|
|
587
|
+
signalSetFn(ref, newValue);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
_a = NODE;
|
|
591
|
+
_brand = /* @__PURE__ */ new WeakSet();
|
|
592
|
+
Signal2.isState = (s) => typeof s === "object" && __privateIn(_brand, s);
|
|
593
|
+
Signal2.State = State;
|
|
594
|
+
class Computed {
|
|
595
|
+
constructor(computation, options) {
|
|
596
|
+
__privateAdd(this, _brand2);
|
|
597
|
+
__publicField(this, _b);
|
|
598
|
+
const node = createComputed(computation)[SIGNAL];
|
|
599
|
+
node.consumerAllowSignalWrites = true;
|
|
600
|
+
this[NODE] = node;
|
|
601
|
+
node.wrapper = this;
|
|
602
|
+
if (options) {
|
|
603
|
+
const equals = options.equals;
|
|
604
|
+
if (equals) node.equal = equals;
|
|
605
|
+
node.watched = options[Signal2.subtle.watched];
|
|
606
|
+
node.unwatched = options[Signal2.subtle.unwatched];
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
get() {
|
|
610
|
+
if (!(0, Signal2.isComputed)(this)) throw new TypeError("Wrong receiver type for Signal.Computed.prototype.get");
|
|
611
|
+
return computedGet(this[NODE]);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
_b = NODE;
|
|
615
|
+
_brand2 = /* @__PURE__ */ new WeakSet();
|
|
616
|
+
Signal2.isComputed = (c) => typeof c === "object" && __privateIn(_brand2, c);
|
|
617
|
+
Signal2.Computed = Computed;
|
|
618
|
+
((subtle2) => {
|
|
619
|
+
var _a2, _brand3, _assertSignals, assertSignals_fn;
|
|
620
|
+
function untrack(cb) {
|
|
621
|
+
let output;
|
|
622
|
+
let prevActiveConsumer = null;
|
|
623
|
+
try {
|
|
624
|
+
prevActiveConsumer = setActiveConsumer(null);
|
|
625
|
+
output = cb();
|
|
626
|
+
} finally {
|
|
627
|
+
setActiveConsumer(prevActiveConsumer);
|
|
628
|
+
}
|
|
629
|
+
return output;
|
|
630
|
+
}
|
|
631
|
+
subtle2.untrack = untrack;
|
|
632
|
+
function introspectSources(sink) {
|
|
633
|
+
var _a3;
|
|
634
|
+
if (!(0, Signal2.isComputed)(sink) && !(0, Signal2.isWatcher)(sink)) throw new TypeError("Called introspectSources without a Computed or Watcher argument");
|
|
635
|
+
return ((_a3 = sink[NODE].producerNode) == null ? void 0 : _a3.map((n) => n.wrapper)) ?? [];
|
|
636
|
+
}
|
|
637
|
+
subtle2.introspectSources = introspectSources;
|
|
638
|
+
function introspectSinks(signal) {
|
|
639
|
+
var _a3;
|
|
640
|
+
if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) throw new TypeError("Called introspectSinks without a Signal argument");
|
|
641
|
+
return ((_a3 = signal[NODE].liveConsumerNode) == null ? void 0 : _a3.map((n) => n.wrapper)) ?? [];
|
|
642
|
+
}
|
|
643
|
+
subtle2.introspectSinks = introspectSinks;
|
|
644
|
+
function hasSinks(signal) {
|
|
645
|
+
if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) throw new TypeError("Called hasSinks without a Signal argument");
|
|
646
|
+
const liveConsumerNode = signal[NODE].liveConsumerNode;
|
|
647
|
+
if (!liveConsumerNode) return false;
|
|
648
|
+
return liveConsumerNode.length > 0;
|
|
649
|
+
}
|
|
650
|
+
subtle2.hasSinks = hasSinks;
|
|
651
|
+
function hasSources(signal) {
|
|
652
|
+
if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isWatcher)(signal)) throw new TypeError("Called hasSources without a Computed or Watcher argument");
|
|
653
|
+
const producerNode = signal[NODE].producerNode;
|
|
654
|
+
if (!producerNode) return false;
|
|
655
|
+
return producerNode.length > 0;
|
|
656
|
+
}
|
|
657
|
+
subtle2.hasSources = hasSources;
|
|
658
|
+
class Watcher {
|
|
659
|
+
constructor(notify) {
|
|
660
|
+
__privateAdd(this, _brand3);
|
|
661
|
+
__privateAdd(this, _assertSignals);
|
|
662
|
+
__publicField(this, _a2);
|
|
663
|
+
let node = Object.create(REACTIVE_NODE);
|
|
664
|
+
node.wrapper = this;
|
|
665
|
+
node.consumerMarkedDirty = notify;
|
|
666
|
+
node.consumerIsAlwaysLive = true;
|
|
667
|
+
node.consumerAllowSignalWrites = false;
|
|
668
|
+
node.producerNode = [];
|
|
669
|
+
this[NODE] = node;
|
|
670
|
+
}
|
|
671
|
+
watch(...signals) {
|
|
672
|
+
if (!(0, Signal2.isWatcher)(this)) throw new TypeError("Called unwatch without Watcher receiver");
|
|
673
|
+
__privateMethod(this, _assertSignals, assertSignals_fn).call(this, signals);
|
|
674
|
+
const node = this[NODE];
|
|
675
|
+
node.dirty = false;
|
|
676
|
+
const prev = setActiveConsumer(node);
|
|
677
|
+
for (const signal of signals) producerAccessed(signal[NODE]);
|
|
678
|
+
setActiveConsumer(prev);
|
|
679
|
+
}
|
|
680
|
+
unwatch(...signals) {
|
|
681
|
+
if (!(0, Signal2.isWatcher)(this)) throw new TypeError("Called unwatch without Watcher receiver");
|
|
682
|
+
__privateMethod(this, _assertSignals, assertSignals_fn).call(this, signals);
|
|
683
|
+
const node = this[NODE];
|
|
684
|
+
assertConsumerNode(node);
|
|
685
|
+
for (let i = node.producerNode.length - 1; i >= 0; i--) if (signals.includes(node.producerNode[i].wrapper)) {
|
|
686
|
+
producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
|
|
687
|
+
const lastIdx = node.producerNode.length - 1;
|
|
688
|
+
node.producerNode[i] = node.producerNode[lastIdx];
|
|
689
|
+
node.producerIndexOfThis[i] = node.producerIndexOfThis[lastIdx];
|
|
690
|
+
node.producerNode.length--;
|
|
691
|
+
node.producerIndexOfThis.length--;
|
|
692
|
+
node.nextProducerIndex--;
|
|
693
|
+
if (i < node.producerNode.length) {
|
|
694
|
+
const idxConsumer = node.producerIndexOfThis[i];
|
|
695
|
+
const producer = node.producerNode[i];
|
|
696
|
+
assertProducerNode(producer);
|
|
697
|
+
producer.liveConsumerIndexOfThis[idxConsumer] = i;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
getPending() {
|
|
702
|
+
if (!(0, Signal2.isWatcher)(this)) throw new TypeError("Called getPending without Watcher receiver");
|
|
703
|
+
return this[NODE].producerNode.filter((n) => n.dirty).map((n) => n.wrapper);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
_a2 = NODE;
|
|
707
|
+
_brand3 = /* @__PURE__ */ new WeakSet();
|
|
708
|
+
_assertSignals = /* @__PURE__ */ new WeakSet();
|
|
709
|
+
assertSignals_fn = function(signals) {
|
|
710
|
+
for (const signal of signals) if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) throw new TypeError("Called watch/unwatch without a Computed or State argument");
|
|
711
|
+
};
|
|
712
|
+
Signal2.isWatcher = (w) => __privateIn(_brand3, w);
|
|
713
|
+
subtle2.Watcher = Watcher;
|
|
714
|
+
function currentComputed() {
|
|
715
|
+
var _a3;
|
|
716
|
+
return (_a3 = getActiveConsumer()) == null ? void 0 : _a3.wrapper;
|
|
717
|
+
}
|
|
718
|
+
subtle2.currentComputed = currentComputed;
|
|
719
|
+
subtle2.watched = Symbol("watched");
|
|
720
|
+
subtle2.unwatched = Symbol("unwatched");
|
|
721
|
+
})(Signal2.subtle || (Signal2.subtle = {}));
|
|
722
|
+
})(Signal || (Signal = {}));
|
|
723
|
+
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region ../spf/dist/dev/core/signals/effect.js
|
|
726
|
+
const pending = /* @__PURE__ */ new Set();
|
|
727
|
+
const watcher = new Signal.subtle.Watcher(() => {
|
|
728
|
+
queueMicrotask(runPending);
|
|
729
|
+
});
|
|
730
|
+
function runPending() {
|
|
731
|
+
for (const c of watcher.getPending()) pending.add(c);
|
|
732
|
+
watcher.watch();
|
|
733
|
+
for (const c of pending) {
|
|
734
|
+
pending.delete(c);
|
|
735
|
+
c.get();
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Run a side effect whenever its signal dependencies change.
|
|
740
|
+
*
|
|
741
|
+
* Executes immediately (synchronous initial run), then re-runs on the next
|
|
742
|
+
* microtask after any dependency changes. If the callback returns a function,
|
|
743
|
+
* it is called before each re-run and when the effect is stopped — the same
|
|
744
|
+
* cleanup contract as Preact Signals, Maverick Signals, and Svelte 5 $effect.
|
|
745
|
+
*
|
|
746
|
+
* Returns a cleanup function that stops the effect.
|
|
747
|
+
*/
|
|
748
|
+
function effect(fn) {
|
|
749
|
+
let cleanup;
|
|
750
|
+
const c = new Signal.Computed(() => {
|
|
751
|
+
if (typeof cleanup === "function") cleanup();
|
|
752
|
+
cleanup = fn();
|
|
753
|
+
});
|
|
754
|
+
watcher.watch(c);
|
|
755
|
+
c.get();
|
|
756
|
+
return () => {
|
|
757
|
+
watcher.unwatch(c);
|
|
758
|
+
if (typeof cleanup === "function") cleanup();
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
//#endregion
|
|
763
|
+
//#region ../spf/dist/dev/core/signals/primitives.js
|
|
764
|
+
/** Read a signal value without tracking it as a dependency. */
|
|
765
|
+
const untrack = Signal.subtle.untrack;
|
|
766
|
+
/** Create a writable reactive value. */
|
|
767
|
+
function signal(initialValue, options) {
|
|
768
|
+
return new Signal.State(initialValue, options);
|
|
769
|
+
}
|
|
770
|
+
/** Create a computed reactive value. */
|
|
771
|
+
function computed(fn, options) {
|
|
772
|
+
return new Signal.Computed(fn, options);
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Update a writable signal. Accepts either a partial object to merge into the
|
|
776
|
+
* current state, or an updater function that receives the current state and
|
|
777
|
+
* returns the next state.
|
|
778
|
+
*
|
|
779
|
+
* @example
|
|
780
|
+
* update(state, { playbackRate: 2 });
|
|
781
|
+
* update(state, (s) => ({ ...s, count: s.count + 1 }));
|
|
782
|
+
*/
|
|
783
|
+
function update(signal, updater) {
|
|
784
|
+
const current = untrack(() => signal.get());
|
|
785
|
+
signal.set(typeof updater === "function" ? updater(current) : {
|
|
786
|
+
...current,
|
|
787
|
+
...updater
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
380
791
|
//#endregion
|
|
381
792
|
//#region ../spf/dist/dev/core/types/index.js
|
|
382
793
|
function isResolvedTrack(track) {
|
|
@@ -390,6 +801,43 @@ function hasPresentationDuration(presentation) {
|
|
|
390
801
|
return presentation.duration !== void 0;
|
|
391
802
|
}
|
|
392
803
|
|
|
804
|
+
//#endregion
|
|
805
|
+
//#region ../spf/dist/dev/core/utils/track-selection.js
|
|
806
|
+
/**
|
|
807
|
+
* Map track type to selected track ID property key in state.
|
|
808
|
+
*/
|
|
809
|
+
const SelectedTrackIdKeyByType = {
|
|
810
|
+
video: "selectedVideoTrackId",
|
|
811
|
+
audio: "selectedAudioTrackId",
|
|
812
|
+
text: "selectedTextTrackId"
|
|
813
|
+
};
|
|
814
|
+
/**
|
|
815
|
+
* Map track type to buffer owner property key.
|
|
816
|
+
* Used for SourceBuffer references in owners.
|
|
817
|
+
*/
|
|
818
|
+
const BufferKeyByType = {
|
|
819
|
+
video: "videoBuffer",
|
|
820
|
+
audio: "audioBuffer"
|
|
821
|
+
};
|
|
822
|
+
/**
|
|
823
|
+
* Get selected track from state by type.
|
|
824
|
+
* Returns properly typed track (partially or fully resolved) or undefined.
|
|
825
|
+
* Type parameter T is inferred from the type argument.
|
|
826
|
+
*
|
|
827
|
+
* @example
|
|
828
|
+
* const videoTrack = getSelectedTrack(state, 'video');
|
|
829
|
+
* if (videoTrack && isResolvedTrack(videoTrack)) {
|
|
830
|
+
* // videoTrack is VideoTrack
|
|
831
|
+
* }
|
|
832
|
+
*/
|
|
833
|
+
function getSelectedTrack(state, type) {
|
|
834
|
+
const { presentation } = state;
|
|
835
|
+
/** @TODO Consider moving and reusing isUnresolved(presentation) (CJP) */
|
|
836
|
+
if (!presentation || !("id" in presentation)) return void 0;
|
|
837
|
+
const trackId = state[SelectedTrackIdKeyByType[type]];
|
|
838
|
+
return presentation.selectionSets.find(({ type: selectionSetType }) => selectionSetType === type)?.switchingSets[0]?.tracks.find(({ id }) => id === trackId);
|
|
839
|
+
}
|
|
840
|
+
|
|
393
841
|
//#endregion
|
|
394
842
|
//#region ../spf/dist/dev/dom/network/chunked-stream-iterable.js
|
|
395
843
|
const DEFAULT_MIN_CHUNK_SIZE = 2 ** 17;
|
|
@@ -490,1812 +938,1511 @@ function getResponseText(response) {
|
|
|
490
938
|
}
|
|
491
939
|
|
|
492
940
|
//#endregion
|
|
493
|
-
//#region ../spf/dist/dev/core/
|
|
941
|
+
//#region ../spf/dist/dev/core/buffer/back-buffer.js
|
|
942
|
+
/**
|
|
943
|
+
* Default back buffer configuration.
|
|
944
|
+
*/
|
|
945
|
+
const DEFAULT_BACK_BUFFER_CONFIG = { keepSegments: 2 };
|
|
494
946
|
/**
|
|
495
|
-
*
|
|
947
|
+
* Calculate back buffer flush point.
|
|
496
948
|
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
949
|
+
* Determines where to flush old segments from the back buffer.
|
|
950
|
+
* Keeps a fixed number of segments behind the current playback position.
|
|
499
951
|
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
952
|
+
* Algorithm:
|
|
953
|
+
* 1. Find segments before currentTime
|
|
954
|
+
* 2. Count back N segments (keepSegments)
|
|
955
|
+
* 3. Return startTime of segment N+1 back (flush everything before this)
|
|
502
956
|
*
|
|
503
|
-
* @param
|
|
504
|
-
* @
|
|
957
|
+
* @param segments - Available segments (should be sorted by startTime)
|
|
958
|
+
* @param currentTime - Current playback position in seconds
|
|
959
|
+
* @param config - Optional back buffer configuration
|
|
960
|
+
* @returns Time in seconds to flush up to (flush range: [0, flushEnd))
|
|
505
961
|
*
|
|
506
962
|
* @example
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
* // React to event + state condition
|
|
514
|
-
* }
|
|
515
|
-
* });
|
|
516
|
-
* ```
|
|
963
|
+
* const segments = [
|
|
964
|
+
* { startTime: 0, duration: 6, ... },
|
|
965
|
+
* { startTime: 6, duration: 6, ... },
|
|
966
|
+
* { startTime: 12, duration: 6, ... },
|
|
967
|
+
* { startTime: 18, duration: 6, ... },
|
|
968
|
+
* ];
|
|
517
969
|
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
* ([s, o]) => deriveKey(s, o),
|
|
522
|
-
* (key) => { ... },
|
|
523
|
-
* { equalityFn: keyEq }
|
|
524
|
-
* );
|
|
525
|
-
* ```
|
|
526
|
-
*/
|
|
527
|
-
function combineLatest(sources) {
|
|
528
|
-
const subscribeToSources = (listener) => {
|
|
529
|
-
const latest = new Array(sources.length);
|
|
530
|
-
const hasValue = new Array(sources.length).fill(false);
|
|
531
|
-
const unsubscribers = [];
|
|
532
|
-
for (let i = 0; i < sources.length; i++) {
|
|
533
|
-
const unsubscribe = sources[i].subscribe((value) => {
|
|
534
|
-
latest[i] = value;
|
|
535
|
-
hasValue[i] = true;
|
|
536
|
-
if (hasValue.every((has) => has)) listener([...latest]);
|
|
537
|
-
});
|
|
538
|
-
unsubscribers.push(unsubscribe);
|
|
539
|
-
}
|
|
540
|
-
return () => {
|
|
541
|
-
for (const unsubscribe of unsubscribers) unsubscribe();
|
|
542
|
-
};
|
|
543
|
-
};
|
|
544
|
-
return { subscribe(listenerOrSelector, maybeListener, options) {
|
|
545
|
-
if (maybeListener === void 0) return subscribeToSources(listenerOrSelector);
|
|
546
|
-
const selector = listenerOrSelector;
|
|
547
|
-
const listener = maybeListener;
|
|
548
|
-
const equalityFn = options?.equalityFn ?? Object.is;
|
|
549
|
-
let prevSelected;
|
|
550
|
-
let initialized = false;
|
|
551
|
-
return subscribeToSources((values) => {
|
|
552
|
-
const nextSelected = selector(values);
|
|
553
|
-
if (!initialized || !equalityFn(prevSelected, nextSelected)) {
|
|
554
|
-
prevSelected = nextSelected;
|
|
555
|
-
initialized = true;
|
|
556
|
-
listener(nextSelected);
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
} };
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
//#endregion
|
|
563
|
-
//#region ../spf/dist/dev/core/hls/resolve-url.js
|
|
564
|
-
/**
|
|
565
|
-
* Resolve a potentially relative URL against a base URL using native URL API.
|
|
970
|
+
* // Playing at 18s, keep 2 segments
|
|
971
|
+
* const flushEnd = calculateBackBufferFlushPoint(segments, 18);
|
|
972
|
+
* // Returns 6 (flush [0, 6), keep [6-18))
|
|
566
973
|
*/
|
|
567
|
-
function
|
|
568
|
-
|
|
974
|
+
function calculateBackBufferFlushPoint(segments, currentTime, config = DEFAULT_BACK_BUFFER_CONFIG) {
|
|
975
|
+
if (segments.length === 0) return 0;
|
|
976
|
+
const segmentsBefore = segments.filter((seg) => seg.startTime < currentTime);
|
|
977
|
+
if (segmentsBefore.length === 0) return 0;
|
|
978
|
+
const segmentsToFlush = segmentsBefore.length - config.keepSegments;
|
|
979
|
+
if (segmentsToFlush <= 0) return 0;
|
|
980
|
+
if (segmentsToFlush >= segmentsBefore.length) return currentTime;
|
|
981
|
+
return segmentsBefore[segmentsToFlush].startTime;
|
|
569
982
|
}
|
|
570
983
|
|
|
571
984
|
//#endregion
|
|
572
|
-
//#region ../spf/dist/dev/
|
|
985
|
+
//#region ../spf/dist/dev/dom/features/segment-loader-actor.js
|
|
573
986
|
/**
|
|
574
|
-
*
|
|
575
|
-
*
|
|
987
|
+
* Creates a SegmentLoaderActor for one track type (video or audio).
|
|
988
|
+
*
|
|
989
|
+
* Receives load assignments via `send()` and owns all execution: planning,
|
|
990
|
+
* removes, fetches, and appends. Coordinates with the SourceBufferActor for
|
|
991
|
+
* all physical SourceBuffer operations.
|
|
992
|
+
*
|
|
993
|
+
* Planning (Cases 1–3) happens in `send()` on every incoming message, producing
|
|
994
|
+
* an ordered LoadTask list. The runner drains that list sequentially. When a new
|
|
995
|
+
* message arrives mid-run, send() replans and either continues the in-flight
|
|
996
|
+
* operation (if still needed) or preempts it.
|
|
997
|
+
*
|
|
998
|
+
* @param sourceBufferActor - Shared SourceBufferActor reference (not owned)
|
|
999
|
+
* @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).
|
|
1000
|
+
* Accepts an optional `minChunkSize` in options; init segments pass `Infinity`
|
|
1001
|
+
* so the entire body accumulates as one chunk before appending.
|
|
576
1002
|
*/
|
|
577
|
-
function
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
* Parse RESOLUTION attribute value (WIDTHxHEIGHT).
|
|
588
|
-
*/
|
|
589
|
-
function parseResolution(value) {
|
|
590
|
-
const match = /^(\d+)x(\d+)$/.exec(value);
|
|
591
|
-
if (!match) return null;
|
|
592
|
-
return {
|
|
593
|
-
width: Number.parseInt(match[1], 10),
|
|
594
|
-
height: Number.parseInt(match[2], 10)
|
|
595
|
-
};
|
|
596
|
-
}
|
|
597
|
-
/**
|
|
598
|
-
* Parse FRAME-RATE attribute to rational frame rate.
|
|
599
|
-
*/
|
|
600
|
-
function parseFrameRate(value) {
|
|
601
|
-
const fps = Number.parseFloat(value);
|
|
602
|
-
if (Number.isNaN(fps) || fps <= 0) return void 0;
|
|
603
|
-
if (Math.abs(fps - 23.976) < .01) return {
|
|
604
|
-
frameRateNumerator: 24e3,
|
|
605
|
-
frameRateDenominator: 1001
|
|
606
|
-
};
|
|
607
|
-
if (Math.abs(fps - 29.97) < .01) return {
|
|
608
|
-
frameRateNumerator: 3e4,
|
|
609
|
-
frameRateDenominator: 1001
|
|
1003
|
+
function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
|
|
1004
|
+
let pendingTasks = null;
|
|
1005
|
+
let inFlightInitTrackId = null;
|
|
1006
|
+
let inFlightSegmentId = null;
|
|
1007
|
+
let abortController = null;
|
|
1008
|
+
let running = false;
|
|
1009
|
+
let destroyed = false;
|
|
1010
|
+
const getBufferedSegments = (allSegments) => {
|
|
1011
|
+
const bufferedIds = new Set(sourceBufferActor.snapshot.get().context.segments.filter((s) => !s.partial).map((s) => s.id));
|
|
1012
|
+
return allSegments.filter((s) => bufferedIds.has(s.id));
|
|
610
1013
|
};
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1014
|
+
/**
|
|
1015
|
+
* Translate a load message into an ordered LoadTask list based on committed
|
|
1016
|
+
* actor state. In-flight awareness is handled separately in send().
|
|
1017
|
+
*
|
|
1018
|
+
* @todo Rename alongside LoadTask (e.g. planOps).
|
|
1019
|
+
*
|
|
1020
|
+
* Case 1 — Removes: forward and back buffer flush points, segment-aligned.
|
|
1021
|
+
* No flush on track switch: appending new content overwrites existing buffer
|
|
1022
|
+
* ranges, and the actor's time-aligned deduplication keeps the segment model
|
|
1023
|
+
* accurate as new segments arrive.
|
|
1024
|
+
*
|
|
1025
|
+
* Case 2 — Init: schedule if not yet committed for this track.
|
|
1026
|
+
*
|
|
1027
|
+
* Case 3 — Segments: all segments in the load window not yet committed.
|
|
1028
|
+
*/
|
|
1029
|
+
const planTasks = (message) => {
|
|
1030
|
+
const { track, range } = message;
|
|
1031
|
+
const actorCtx = sourceBufferActor.snapshot.get().context;
|
|
1032
|
+
const bufferedSegments = getBufferedSegments(track.segments);
|
|
1033
|
+
const currentTime = range?.start ?? 0;
|
|
1034
|
+
const tasks = [];
|
|
1035
|
+
if (range) {
|
|
1036
|
+
const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime);
|
|
1037
|
+
if (forwardFlushStart < Infinity) tasks.push({
|
|
1038
|
+
type: "remove",
|
|
1039
|
+
start: forwardFlushStart,
|
|
1040
|
+
end: Infinity
|
|
1041
|
+
});
|
|
1042
|
+
const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime);
|
|
1043
|
+
if (backFlushEnd > 0) tasks.push({
|
|
1044
|
+
type: "remove",
|
|
1045
|
+
start: 0,
|
|
1046
|
+
end: backFlushEnd
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
if (actorCtx.initTrackId !== track.id) tasks.push({
|
|
1050
|
+
type: "append-init",
|
|
1051
|
+
meta: { trackId: track.id },
|
|
1052
|
+
url: track.initialization.url,
|
|
1053
|
+
...track.initialization.byteRange !== void 0 && { byteRange: track.initialization.byteRange }
|
|
1054
|
+
});
|
|
1055
|
+
if (range) {
|
|
1056
|
+
const EPSILON = 1e-4;
|
|
1057
|
+
const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime).filter((seg) => {
|
|
1058
|
+
const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) < EPSILON);
|
|
1059
|
+
if (existing?.partial) return true;
|
|
1060
|
+
if (!existing?.trackBandwidth || !track.bandwidth) return true;
|
|
1061
|
+
return track.bandwidth > existing.trackBandwidth;
|
|
1062
|
+
});
|
|
1063
|
+
for (const segment of segmentsToLoad) tasks.push({
|
|
1064
|
+
type: "append-segment",
|
|
1065
|
+
meta: {
|
|
1066
|
+
id: segment.id,
|
|
1067
|
+
startTime: segment.startTime,
|
|
1068
|
+
duration: segment.duration,
|
|
1069
|
+
trackId: track.id,
|
|
1070
|
+
trackBandwidth: track.bandwidth
|
|
1071
|
+
},
|
|
1072
|
+
url: segment.url,
|
|
1073
|
+
...segment.byteRange !== void 0 && { byteRange: segment.byteRange }
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
return tasks;
|
|
614
1077
|
};
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
*
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1078
|
+
/**
|
|
1079
|
+
* Execute a single LoadTask: fetch (if needed) then forward to SourceBufferActor.
|
|
1080
|
+
* Sets/clears in-flight tracking around async operations so send() can make
|
|
1081
|
+
* accurate continue/preempt decisions at any point during execution.
|
|
1082
|
+
*
|
|
1083
|
+
* @todo Rename alongside LoadTask (e.g. executeOp).
|
|
1084
|
+
*/
|
|
1085
|
+
const executeLoadTask = async (task) => {
|
|
1086
|
+
const signal = abortController.signal;
|
|
1087
|
+
try {
|
|
1088
|
+
if (task.type === "remove") {
|
|
1089
|
+
await sourceBufferActor.send(task, signal);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (task.type === "append-init") {
|
|
1093
|
+
inFlightInitTrackId = task.meta.trackId;
|
|
1094
|
+
if (!signal.aborted) {
|
|
1095
|
+
const data = await fetchBytes(task, {
|
|
1096
|
+
signal,
|
|
1097
|
+
minChunkSize: Infinity
|
|
1098
|
+
});
|
|
1099
|
+
const isTrackSwitch = pendingTasks?.some((t) => t.type === "append-init" && t.meta.trackId !== task.meta.trackId);
|
|
1100
|
+
if (!signal.aborted || !isTrackSwitch) {
|
|
1101
|
+
const appendSignal = signal.aborted ? new AbortController().signal : signal;
|
|
1102
|
+
await sourceBufferActor.send({
|
|
1103
|
+
type: "append-init",
|
|
1104
|
+
data,
|
|
1105
|
+
meta: task.meta
|
|
1106
|
+
}, appendSignal);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
inFlightSegmentId = task.meta.id;
|
|
1112
|
+
if (!signal.aborted) {
|
|
1113
|
+
const stream = await fetchBytes(task, { signal });
|
|
1114
|
+
if (!signal.aborted) await sourceBufferActor.send({
|
|
1115
|
+
type: "append-segment",
|
|
1116
|
+
data: stream,
|
|
1117
|
+
meta: task.meta
|
|
1118
|
+
}, signal);
|
|
1119
|
+
}
|
|
1120
|
+
} finally {
|
|
1121
|
+
inFlightInitTrackId = null;
|
|
1122
|
+
inFlightSegmentId = null;
|
|
1123
|
+
}
|
|
655
1124
|
};
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
*
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
getResolution(key) {
|
|
682
|
-
const value = map.get(key);
|
|
683
|
-
if (!value) return void 0;
|
|
684
|
-
return parseResolution(value) ?? void 0;
|
|
685
|
-
},
|
|
686
|
-
getFrameRate(key) {
|
|
687
|
-
const value = map.get(key);
|
|
688
|
-
if (!value) return void 0;
|
|
689
|
-
return parseFrameRate(value);
|
|
1125
|
+
/**
|
|
1126
|
+
* Drain the scheduled task list sequentially.
|
|
1127
|
+
* After each task completes, checks for a pending replacement plan from send().
|
|
1128
|
+
* If the signal was aborted and no new plan arrived, stops immediately.
|
|
1129
|
+
*/
|
|
1130
|
+
const runScheduled = async (initialTasks) => {
|
|
1131
|
+
running = true;
|
|
1132
|
+
abortController = new AbortController();
|
|
1133
|
+
let scheduled = initialTasks;
|
|
1134
|
+
while (scheduled.length > 0 && !destroyed) {
|
|
1135
|
+
const task = scheduled[0];
|
|
1136
|
+
scheduled = scheduled.slice(1);
|
|
1137
|
+
try {
|
|
1138
|
+
await executeLoadTask(task);
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
if (error instanceof Error && error.name === "AbortError") {} else {
|
|
1141
|
+
console.error("Unexpected error in segment loader:", error);
|
|
1142
|
+
scheduled = [];
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (pendingTasks !== null) {
|
|
1146
|
+
scheduled = pendingTasks;
|
|
1147
|
+
pendingTasks = null;
|
|
1148
|
+
abortController = new AbortController();
|
|
1149
|
+
} else if (abortController.signal.aborted) break;
|
|
690
1150
|
}
|
|
1151
|
+
abortController = null;
|
|
1152
|
+
running = false;
|
|
691
1153
|
};
|
|
692
|
-
}
|
|
693
|
-
/**
|
|
694
|
-
* Match a tag and extract its attributes.
|
|
695
|
-
* Returns null if the line doesn't match the tag.
|
|
696
|
-
*/
|
|
697
|
-
function matchTag(line, tag) {
|
|
698
|
-
const prefix = `#${tag}:`;
|
|
699
|
-
if (!line.startsWith(prefix)) return null;
|
|
700
|
-
return createAttributeList(line.slice(prefix.length));
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
//#endregion
|
|
704
|
-
//#region ../spf/dist/dev/core/hls/parse-media-playlist.js
|
|
705
|
-
/**
|
|
706
|
-
* Parse HLS media playlist and resolve track with segments.
|
|
707
|
-
*
|
|
708
|
-
* Takes an unresolved track (from multivariant playlist) and media playlist text,
|
|
709
|
-
* returns a HAM-compliant resolved track with segments.
|
|
710
|
-
*
|
|
711
|
-
* @param text - Media playlist text content
|
|
712
|
-
* @param unresolved - Unresolved track from parseMultivariantPlaylist
|
|
713
|
-
* @returns Resolved track with segments (type inferred from input)
|
|
714
|
-
*/
|
|
715
|
-
function parseMediaPlaylist(text, unresolved) {
|
|
716
|
-
const lines = text.split(/\r?\n/);
|
|
717
|
-
const baseUrl = unresolved.url;
|
|
718
|
-
const segments = [];
|
|
719
|
-
let initSegmentUrl;
|
|
720
|
-
let initSegmentByteRange;
|
|
721
|
-
let currentDuration = 0;
|
|
722
|
-
let currentByteRange;
|
|
723
|
-
let currentTime = 0;
|
|
724
|
-
let segmentIndex = 0;
|
|
725
|
-
let previousByteRangeEnd;
|
|
726
|
-
for (const line of lines) {
|
|
727
|
-
const trimmed = line.trim();
|
|
728
|
-
if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
|
|
729
|
-
if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-TARGETDURATION:") || trimmed.startsWith("#EXT-X-PLAYLIST-TYPE:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
|
|
730
|
-
const mapAttrs = matchTag(trimmed, "EXT-X-MAP");
|
|
731
|
-
if (mapAttrs) {
|
|
732
|
-
const uri = mapAttrs.get("URI");
|
|
733
|
-
if (uri) {
|
|
734
|
-
initSegmentUrl = resolveUrl(uri, baseUrl);
|
|
735
|
-
const byteRangeStr = mapAttrs.get("BYTERANGE");
|
|
736
|
-
if (byteRangeStr) initSegmentByteRange = parseByteRange(byteRangeStr, 0) ?? void 0;
|
|
737
|
-
}
|
|
738
|
-
continue;
|
|
739
|
-
}
|
|
740
|
-
if (trimmed.startsWith("#EXTINF:")) {
|
|
741
|
-
currentDuration = parseExtInfDuration(trimmed.slice(8));
|
|
742
|
-
continue;
|
|
743
|
-
}
|
|
744
|
-
if (trimmed.startsWith("#EXT-X-BYTERANGE:")) {
|
|
745
|
-
currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? void 0;
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
if (trimmed === "#EXT-X-ENDLIST") continue;
|
|
749
|
-
if (!trimmed.startsWith("#") && currentDuration > 0) {
|
|
750
|
-
const segment = {
|
|
751
|
-
id: `segment-${segmentIndex}`,
|
|
752
|
-
url: resolveUrl(trimmed, baseUrl),
|
|
753
|
-
duration: currentDuration,
|
|
754
|
-
startTime: currentTime
|
|
755
|
-
};
|
|
756
|
-
if (currentByteRange) {
|
|
757
|
-
segment.byteRange = currentByteRange;
|
|
758
|
-
previousByteRangeEnd = currentByteRange.end + 1;
|
|
759
|
-
} else previousByteRangeEnd = void 0;
|
|
760
|
-
segments.push(segment);
|
|
761
|
-
currentTime += currentDuration;
|
|
762
|
-
segmentIndex++;
|
|
763
|
-
currentDuration = 0;
|
|
764
|
-
currentByteRange = void 0;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
const totalDuration = currentTime;
|
|
768
|
-
const initialization = unresolved.type === "text" && !initSegmentUrl ? void 0 : initSegmentUrl ? {
|
|
769
|
-
url: initSegmentUrl,
|
|
770
|
-
...initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}
|
|
771
|
-
} : { url: "" };
|
|
772
1154
|
return {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
//#endregion
|
|
782
|
-
//#region ../spf/dist/dev/core/utils/generate-id.js
|
|
783
|
-
/**
|
|
784
|
-
* Generate unique ID for HAM objects.
|
|
785
|
-
*
|
|
786
|
-
* Uses timestamp + random number for sufficient uniqueness.
|
|
787
|
-
* IDs are strings without decimals.
|
|
788
|
-
*
|
|
789
|
-
* @returns Unique string ID in format: timestamp-random
|
|
790
|
-
*
|
|
791
|
-
* @example
|
|
792
|
-
* ```ts
|
|
793
|
-
* const id = generateId(); // "1738423156789-542891"
|
|
794
|
-
* ```
|
|
795
|
-
*/
|
|
796
|
-
function generateId() {
|
|
797
|
-
return `${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
//#endregion
|
|
801
|
-
//#region ../spf/dist/dev/core/hls/parse-multivariant.js
|
|
802
|
-
/**
|
|
803
|
-
* Parse HLS multivariant playlist into a Presentation.
|
|
804
|
-
*
|
|
805
|
-
* Returns Presentation with partially resolved tracks (no segment information).
|
|
806
|
-
* Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs)
|
|
807
|
-
* but segment information is added when media playlists are fetched.
|
|
808
|
-
*
|
|
809
|
-
* @param text - Raw playlist text content
|
|
810
|
-
* @param unresolved - Unresolved presentation (contains URL for base URL resolution)
|
|
811
|
-
* @returns Presentation with partially resolved tracks (duration is undefined)
|
|
812
|
-
*/
|
|
813
|
-
function parseMultivariantPlaylist(text, unresolved) {
|
|
814
|
-
const baseUrl = unresolved.url;
|
|
815
|
-
const lines = text.split(/\r?\n/);
|
|
816
|
-
const streams = [];
|
|
817
|
-
const audioRenditions = [];
|
|
818
|
-
const subtitleRenditions = [];
|
|
819
|
-
let pendingStreamInfo = null;
|
|
820
|
-
for (const line of lines) {
|
|
821
|
-
const trimmed = line.trim();
|
|
822
|
-
if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
|
|
823
|
-
if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
|
|
824
|
-
const mediaAttrs = matchTag(trimmed, "EXT-X-MEDIA");
|
|
825
|
-
if (mediaAttrs) {
|
|
826
|
-
const type = mediaAttrs.get("TYPE");
|
|
827
|
-
const groupId = mediaAttrs.get("GROUP-ID");
|
|
828
|
-
const name = mediaAttrs.get("NAME");
|
|
829
|
-
if (type === "AUDIO" && groupId && name) {
|
|
830
|
-
const uri = mediaAttrs.get("URI");
|
|
831
|
-
audioRenditions.push({
|
|
832
|
-
groupId,
|
|
833
|
-
name,
|
|
834
|
-
language: mediaAttrs.get("LANGUAGE"),
|
|
835
|
-
uri: uri ? resolveUrl(uri, baseUrl) : void 0,
|
|
836
|
-
default: mediaAttrs.getBool("DEFAULT"),
|
|
837
|
-
autoselect: mediaAttrs.getBool("AUTOSELECT")
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
if (type === "SUBTITLES" && groupId && name) {
|
|
841
|
-
const uri = mediaAttrs.get("URI");
|
|
842
|
-
if (uri) subtitleRenditions.push({
|
|
843
|
-
groupId,
|
|
844
|
-
name,
|
|
845
|
-
language: mediaAttrs.get("LANGUAGE"),
|
|
846
|
-
uri: resolveUrl(uri, baseUrl),
|
|
847
|
-
default: mediaAttrs.getBool("DEFAULT"),
|
|
848
|
-
autoselect: mediaAttrs.getBool("AUTOSELECT"),
|
|
849
|
-
forced: mediaAttrs.getBool("FORCED")
|
|
850
|
-
});
|
|
1155
|
+
send(message) {
|
|
1156
|
+
if (destroyed) return;
|
|
1157
|
+
const allTasks = planTasks(message);
|
|
1158
|
+
if (!running) {
|
|
1159
|
+
if (allTasks.length === 0) return;
|
|
1160
|
+
runScheduled(allTasks);
|
|
1161
|
+
return;
|
|
851
1162
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
pendingStreamInfo = {
|
|
857
|
-
bandwidth: streamInfAttrs.getInt("BANDWIDTH", 0),
|
|
858
|
-
resolution: streamInfAttrs.getResolution("RESOLUTION"),
|
|
859
|
-
codecs: streamInfAttrs.get("CODECS"),
|
|
860
|
-
frameRate: streamInfAttrs.getFrameRate("FRAME-RATE"),
|
|
861
|
-
audioGroupId: streamInfAttrs.get("AUDIO")
|
|
862
|
-
};
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
if (!trimmed.startsWith("#") && pendingStreamInfo) {
|
|
866
|
-
streams.push({
|
|
867
|
-
...pendingStreamInfo,
|
|
868
|
-
uri: resolveUrl(trimmed, baseUrl)
|
|
869
|
-
});
|
|
870
|
-
pendingStreamInfo = null;
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
const videoStreams = [];
|
|
874
|
-
const audioOnlyStreams = [];
|
|
875
|
-
for (const stream of streams) {
|
|
876
|
-
if (!stream.codecs) {
|
|
877
|
-
videoStreams.push(stream);
|
|
878
|
-
continue;
|
|
879
|
-
}
|
|
880
|
-
const parsedCodecs = parseCodecs(stream.codecs);
|
|
881
|
-
if (stream.codecs.split(",").length === 1) if (parsedCodecs.audio && !parsedCodecs.video) audioOnlyStreams.push(stream);
|
|
882
|
-
else videoStreams.push(stream);
|
|
883
|
-
else videoStreams.push(stream);
|
|
884
|
-
}
|
|
885
|
-
const videoTracks = videoStreams.map((stream) => {
|
|
886
|
-
const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
|
|
887
|
-
const track = {
|
|
888
|
-
type: "video",
|
|
889
|
-
id: generateId(),
|
|
890
|
-
url: stream.uri,
|
|
891
|
-
bandwidth: stream.bandwidth,
|
|
892
|
-
mimeType: "video/mp4",
|
|
893
|
-
codecs: []
|
|
894
|
-
};
|
|
895
|
-
if (stream.resolution?.width !== void 0) track.width = stream.resolution.width;
|
|
896
|
-
if (stream.resolution?.height !== void 0) track.height = stream.resolution.height;
|
|
897
|
-
if (codecs?.video) track.codecs = [codecs.video];
|
|
898
|
-
if (stream.frameRate) track.frameRate = stream.frameRate;
|
|
899
|
-
if (stream.audioGroupId) track.audioGroupId = stream.audioGroupId;
|
|
900
|
-
return track;
|
|
901
|
-
});
|
|
902
|
-
const audioOnlyTracks = audioOnlyStreams.map((stream) => {
|
|
903
|
-
const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
|
|
904
|
-
return {
|
|
905
|
-
type: "audio",
|
|
906
|
-
id: generateId(),
|
|
907
|
-
url: stream.uri,
|
|
908
|
-
bandwidth: stream.bandwidth,
|
|
909
|
-
mimeType: "audio/mp4",
|
|
910
|
-
codecs: codecs?.audio ? [codecs.audio] : [],
|
|
911
|
-
groupId: stream.audioGroupId || "default",
|
|
912
|
-
name: "Default",
|
|
913
|
-
sampleRate: 48e3,
|
|
914
|
-
channels: 2
|
|
915
|
-
};
|
|
916
|
-
});
|
|
917
|
-
const audioTracks = [...audioRenditions.map((rendition) => {
|
|
918
|
-
let audioCodecs;
|
|
919
|
-
for (const stream of streams) if (stream.audioGroupId === rendition.groupId && stream.codecs) {
|
|
920
|
-
const codecs = parseCodecs(stream.codecs);
|
|
921
|
-
if (codecs.audio) {
|
|
922
|
-
audioCodecs = [codecs.audio];
|
|
923
|
-
break;
|
|
1163
|
+
if (inFlightSegmentId !== null && allTasks.some((t) => t.type === "append-segment" && t.meta.id === inFlightSegmentId) || inFlightInitTrackId !== null && allTasks.some((t) => t.type === "append-init" && t.meta.trackId === inFlightInitTrackId)) pendingTasks = allTasks.filter((t) => !(t.type === "append-segment" && t.meta.id === inFlightSegmentId) && !(t.type === "append-init" && t.meta.trackId === inFlightInitTrackId));
|
|
1164
|
+
else {
|
|
1165
|
+
pendingTasks = allTasks;
|
|
1166
|
+
abortController?.abort();
|
|
924
1167
|
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
name: rendition.name,
|
|
932
|
-
mimeType: "audio/mp4",
|
|
933
|
-
bandwidth: 0,
|
|
934
|
-
sampleRate: 48e3,
|
|
935
|
-
channels: 2,
|
|
936
|
-
codecs: []
|
|
937
|
-
};
|
|
938
|
-
if (rendition.language) track.language = rendition.language;
|
|
939
|
-
if (audioCodecs) track.codecs = audioCodecs;
|
|
940
|
-
if (rendition.default) track.default = rendition.default;
|
|
941
|
-
if (rendition.autoselect) track.autoselect = rendition.autoselect;
|
|
942
|
-
return track;
|
|
943
|
-
}), ...audioOnlyTracks];
|
|
944
|
-
const textTracks = subtitleRenditions.map((rendition) => {
|
|
945
|
-
const track = {
|
|
946
|
-
type: "text",
|
|
947
|
-
id: generateId(),
|
|
948
|
-
url: rendition.uri,
|
|
949
|
-
groupId: rendition.groupId,
|
|
950
|
-
label: rendition.name,
|
|
951
|
-
kind: "subtitles",
|
|
952
|
-
mimeType: "text/vtt",
|
|
953
|
-
bandwidth: 0
|
|
954
|
-
};
|
|
955
|
-
if (rendition.language) track.language = rendition.language;
|
|
956
|
-
if (rendition.default && rendition.autoselect) track.default = true;
|
|
957
|
-
if (rendition.autoselect) track.autoselect = rendition.autoselect;
|
|
958
|
-
if (rendition.forced) track.forced = rendition.forced;
|
|
959
|
-
return track;
|
|
960
|
-
});
|
|
961
|
-
const selectionSets = [];
|
|
962
|
-
if (videoTracks.length > 0) {
|
|
963
|
-
const videoSwitchingSet = {
|
|
964
|
-
id: generateId(),
|
|
965
|
-
type: "video",
|
|
966
|
-
tracks: videoTracks
|
|
967
|
-
};
|
|
968
|
-
const videoSelectionSet = {
|
|
969
|
-
id: generateId(),
|
|
970
|
-
type: "video",
|
|
971
|
-
switchingSets: [videoSwitchingSet]
|
|
972
|
-
};
|
|
973
|
-
selectionSets.push(videoSelectionSet);
|
|
974
|
-
}
|
|
975
|
-
if (audioTracks.length > 0) {
|
|
976
|
-
const audioSwitchingSet = {
|
|
977
|
-
id: generateId(),
|
|
978
|
-
type: "audio",
|
|
979
|
-
tracks: audioTracks
|
|
980
|
-
};
|
|
981
|
-
const audioSelectionSet = {
|
|
982
|
-
id: generateId(),
|
|
983
|
-
type: "audio",
|
|
984
|
-
switchingSets: [audioSwitchingSet]
|
|
985
|
-
};
|
|
986
|
-
selectionSets.push(audioSelectionSet);
|
|
987
|
-
}
|
|
988
|
-
if (textTracks.length > 0) {
|
|
989
|
-
const textSwitchingSet = {
|
|
990
|
-
id: generateId(),
|
|
991
|
-
type: "text",
|
|
992
|
-
tracks: textTracks
|
|
993
|
-
};
|
|
994
|
-
const textSelectionSet = {
|
|
995
|
-
id: generateId(),
|
|
996
|
-
type: "text",
|
|
997
|
-
switchingSets: [textSwitchingSet]
|
|
998
|
-
};
|
|
999
|
-
selectionSets.push(textSelectionSet);
|
|
1000
|
-
}
|
|
1001
|
-
return {
|
|
1002
|
-
id: generateId(),
|
|
1003
|
-
url: unresolved.url,
|
|
1004
|
-
startTime: 0,
|
|
1005
|
-
selectionSets
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
//#endregion
|
|
1010
|
-
//#region ../spf/dist/dev/core/abr/quality-selection.js
|
|
1011
|
-
/**
|
|
1012
|
-
* Default quality selection configuration.
|
|
1013
|
-
* Values match Shaka Player upgrade threshold (0.85 = 15% headroom).
|
|
1014
|
-
*/
|
|
1015
|
-
const DEFAULT_QUALITY_CONFIG = { safetyMargin: .85 };
|
|
1016
|
-
/**
|
|
1017
|
-
* Select the best video track based on current bandwidth estimate.
|
|
1018
|
-
*
|
|
1019
|
-
* Selects the highest quality track where bandwidth is sufficient with safety margin:
|
|
1020
|
-
* - currentBandwidth >= track.bandwidth / safetyMargin
|
|
1021
|
-
* - Default safetyMargin 0.85 means track uses ≤85% of bandwidth (15% headroom)
|
|
1022
|
-
* - At same bandwidth, prefers higher resolution
|
|
1023
|
-
*
|
|
1024
|
-
* @param tracks - Available video tracks (can be unsorted)
|
|
1025
|
-
* @param currentBandwidth - Current bandwidth estimate in bits per second
|
|
1026
|
-
* @param config - Optional quality selection configuration
|
|
1027
|
-
* @returns Selected track, or undefined if no tracks available
|
|
1028
|
-
*
|
|
1029
|
-
* @example
|
|
1030
|
-
* const tracks = [
|
|
1031
|
-
* { id: '360p', bandwidth: 500_000, ... },
|
|
1032
|
-
* { id: '720p', bandwidth: 2_000_000, ... },
|
|
1033
|
-
* { id: '1080p', bandwidth: 4_000_000, ... },
|
|
1034
|
-
* ];
|
|
1035
|
-
*
|
|
1036
|
-
* // With 2.5 Mbps, selects 720p (1080p needs 4M/0.85 = 4.7 Mbps)
|
|
1037
|
-
* const selected = selectQuality(tracks, 2_500_000);
|
|
1038
|
-
*/
|
|
1039
|
-
function selectQuality(tracks, currentBandwidth, config = DEFAULT_QUALITY_CONFIG) {
|
|
1040
|
-
if (tracks.length === 0) return;
|
|
1041
|
-
const sortedTracks = tracks.slice().sort((a, b) => a.bandwidth - b.bandwidth);
|
|
1042
|
-
let chosen;
|
|
1043
|
-
for (const track of sortedTracks) if (currentBandwidth >= track.bandwidth / config.safetyMargin) {
|
|
1044
|
-
if (!chosen || track.bandwidth > chosen.bandwidth || track.bandwidth === chosen.bandwidth && hasHigherResolution(track, chosen)) chosen = track;
|
|
1045
|
-
}
|
|
1046
|
-
return chosen ?? sortedTracks[0];
|
|
1047
|
-
}
|
|
1048
|
-
/**
|
|
1049
|
-
* Check if track A has higher resolution than track B.
|
|
1050
|
-
* Compares by total pixel count (width × height).
|
|
1051
|
-
*
|
|
1052
|
-
* @param trackA - First track to compare
|
|
1053
|
-
* @param trackB - Second track to compare
|
|
1054
|
-
* @returns True if trackA has more pixels than trackB
|
|
1055
|
-
*/
|
|
1056
|
-
function hasHigherResolution(trackA, trackB) {
|
|
1057
|
-
return (trackA.width ?? 0) * (trackA.height ?? 0) > (trackB.width ?? 0) * (trackB.height ?? 0);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
//#endregion
|
|
1061
|
-
//#region ../spf/dist/dev/core/buffer/back-buffer.js
|
|
1062
|
-
/**
|
|
1063
|
-
* Default back buffer configuration.
|
|
1064
|
-
*/
|
|
1065
|
-
const DEFAULT_BACK_BUFFER_CONFIG = { keepSegments: 2 };
|
|
1066
|
-
/**
|
|
1067
|
-
* Calculate back buffer flush point.
|
|
1068
|
-
*
|
|
1069
|
-
* Determines where to flush old segments from the back buffer.
|
|
1070
|
-
* Keeps a fixed number of segments behind the current playback position.
|
|
1071
|
-
*
|
|
1072
|
-
* Algorithm:
|
|
1073
|
-
* 1. Find segments before currentTime
|
|
1074
|
-
* 2. Count back N segments (keepSegments)
|
|
1075
|
-
* 3. Return startTime of segment N+1 back (flush everything before this)
|
|
1076
|
-
*
|
|
1077
|
-
* @param segments - Available segments (should be sorted by startTime)
|
|
1078
|
-
* @param currentTime - Current playback position in seconds
|
|
1079
|
-
* @param config - Optional back buffer configuration
|
|
1080
|
-
* @returns Time in seconds to flush up to (flush range: [0, flushEnd))
|
|
1081
|
-
*
|
|
1082
|
-
* @example
|
|
1083
|
-
* const segments = [
|
|
1084
|
-
* { startTime: 0, duration: 6, ... },
|
|
1085
|
-
* { startTime: 6, duration: 6, ... },
|
|
1086
|
-
* { startTime: 12, duration: 6, ... },
|
|
1087
|
-
* { startTime: 18, duration: 6, ... },
|
|
1088
|
-
* ];
|
|
1089
|
-
*
|
|
1090
|
-
* // Playing at 18s, keep 2 segments
|
|
1091
|
-
* const flushEnd = calculateBackBufferFlushPoint(segments, 18);
|
|
1092
|
-
* // Returns 6 (flush [0, 6), keep [6-18))
|
|
1093
|
-
*/
|
|
1094
|
-
function calculateBackBufferFlushPoint(segments, currentTime, config = DEFAULT_BACK_BUFFER_CONFIG) {
|
|
1095
|
-
if (segments.length === 0) return 0;
|
|
1096
|
-
const segmentsBefore = segments.filter((seg) => seg.startTime < currentTime);
|
|
1097
|
-
if (segmentsBefore.length === 0) return 0;
|
|
1098
|
-
const segmentsToFlush = segmentsBefore.length - config.keepSegments;
|
|
1099
|
-
if (segmentsToFlush <= 0) return 0;
|
|
1100
|
-
if (segmentsToFlush >= segmentsBefore.length) return currentTime;
|
|
1101
|
-
return segmentsBefore[segmentsToFlush].startTime;
|
|
1168
|
+
},
|
|
1169
|
+
destroy() {
|
|
1170
|
+
destroyed = true;
|
|
1171
|
+
abortController?.abort();
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1102
1174
|
}
|
|
1103
1175
|
|
|
1104
1176
|
//#endregion
|
|
1105
|
-
//#region ../spf/dist/dev/dom/
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1177
|
+
//#region ../spf/dist/dev/dom/features/load-segments.js
|
|
1178
|
+
const ActorKeyByType$1 = {
|
|
1179
|
+
video: "videoBufferActor",
|
|
1180
|
+
audio: "audioBufferActor"
|
|
1181
|
+
};
|
|
1182
|
+
function createTrackedFetch(throughput, onSample) {
|
|
1183
|
+
return async (addressable, options) => {
|
|
1184
|
+
const { minChunkSize, ...fetchOptions } = options ?? {};
|
|
1185
|
+
const response = await fetchResolvable(addressable, fetchOptions);
|
|
1186
|
+
if (!response.body) throw new Error("Response has no body");
|
|
1187
|
+
const body = response.body;
|
|
1188
|
+
return { [Symbol.asyncIterator]: async function* () {
|
|
1189
|
+
let chunkStart = performance.now();
|
|
1190
|
+
for await (const chunk of new ChunkedStreamIterable(body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : [])) {
|
|
1191
|
+
const elapsed = performance.now() - chunkStart;
|
|
1192
|
+
const next = sampleBandwidth(throughput.get(), elapsed, chunk.byteLength);
|
|
1193
|
+
throughput.set(next);
|
|
1194
|
+
onSample?.(next);
|
|
1195
|
+
yield chunk;
|
|
1196
|
+
chunkStart = performance.now();
|
|
1197
|
+
}
|
|
1198
|
+
} };
|
|
1199
|
+
};
|
|
1119
1200
|
}
|
|
1120
1201
|
/**
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1202
|
+
* Non-tracking fetch: eagerly starts the request and returns the response body
|
|
1203
|
+
* as a lazy chunk iterable. Used for audio tracks which don't sample bandwidth.
|
|
1204
|
+
* Pass `minChunkSize: Infinity` to accumulate the full body as a single chunk
|
|
1205
|
+
* (equivalent to arrayBuffer() but through the same streaming path).
|
|
1123
1206
|
*/
|
|
1124
|
-
function
|
|
1125
|
-
|
|
1207
|
+
async function fetchStream(addressable, options) {
|
|
1208
|
+
const { minChunkSize, ...fetchOptions } = options ?? {};
|
|
1209
|
+
const response = await fetchResolvable(addressable, fetchOptions);
|
|
1210
|
+
if (!response.body) throw new Error("Response has no body");
|
|
1211
|
+
return new ChunkedStreamIterable(response.body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : []);
|
|
1212
|
+
}
|
|
1213
|
+
function selectLoadingInputs([segmentsCanLoad, state], type) {
|
|
1214
|
+
const { playbackInitiated, preload, currentTime } = state;
|
|
1215
|
+
return {
|
|
1216
|
+
playbackInitiated,
|
|
1217
|
+
preload,
|
|
1218
|
+
currentTime,
|
|
1219
|
+
track: getSelectedTrack(state, type),
|
|
1220
|
+
segmentsCanLoad
|
|
1221
|
+
};
|
|
1126
1222
|
}
|
|
1127
1223
|
/**
|
|
1128
|
-
*
|
|
1224
|
+
* Equality function encoding the condition hierarchy for relevant changes.
|
|
1129
1225
|
*
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1226
|
+
* Pre-play (!playbackInitiated):
|
|
1227
|
+
* Only preload changes matter. currentTime and resolvedTrackId are ignored
|
|
1228
|
+
* (track changes not supported pre-play; currentTime value is used at
|
|
1229
|
+
* trigger time but changes don't re-trigger).
|
|
1133
1230
|
*
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1231
|
+
* playbackInitiated transition:
|
|
1232
|
+
* Always fires (handled in the subscriber; preload='auto' suppression
|
|
1233
|
+
* applied there since equality functions have no memory of prior values).
|
|
1234
|
+
*
|
|
1235
|
+
* Post-play (playbackInitiated):
|
|
1236
|
+
* resolvedTrackId changes (track switch or previously-unresolved track
|
|
1237
|
+
* resolving) and currentTime changes both trigger. preload is irrelevant.
|
|
1138
1238
|
*/
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
throw new Error("MediaSource API is not supported");
|
|
1144
|
-
}
|
|
1239
|
+
const segmentStartFor = (currentTime, track) => {
|
|
1240
|
+
if (currentTime == null) return void 0;
|
|
1241
|
+
return track?.segments.find(({ startTime, duration }, i, segments) => currentTime >= startTime && (currentTime < startTime + duration || i === segments.length - 1))?.startTime;
|
|
1242
|
+
};
|
|
1145
1243
|
/**
|
|
1146
|
-
*
|
|
1147
|
-
*
|
|
1148
|
-
* Uses srcObject for ManagedMediaSource (Safari), or createObjectURL for regular MediaSource.
|
|
1149
|
-
*
|
|
1150
|
-
* @param mediaSource - The MediaSource to attach
|
|
1151
|
-
* @param mediaElement - The media element to attach to
|
|
1152
|
-
* @returns Object with URL and detach function
|
|
1244
|
+
* Returns true when the inputs are equal (no meaningful change — don't fire).
|
|
1245
|
+
* Returns false when the inputs differ in a way that requires a new message.
|
|
1153
1246
|
*
|
|
1154
|
-
*
|
|
1155
|
-
* const mediaSource = createMediaSource();
|
|
1156
|
-
* const { detach } = attachMediaSource(mediaSource, videoElement);
|
|
1157
|
-
* await waitForSourceOpen(mediaSource);
|
|
1158
|
-
* // Use mediaSource...
|
|
1159
|
-
* // Later, to clean up:
|
|
1160
|
-
* detach();
|
|
1247
|
+
* This IS the shouldLoadSegments logic, expressed as an equality function.
|
|
1161
1248
|
*/
|
|
1162
|
-
function
|
|
1163
|
-
if (
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
mediaElement.srcObject = null;
|
|
1168
|
-
mediaElement.load();
|
|
1169
|
-
};
|
|
1170
|
-
return {
|
|
1171
|
-
url: "",
|
|
1172
|
-
detach
|
|
1173
|
-
};
|
|
1249
|
+
function loadingInputsEq(prevState, curState) {
|
|
1250
|
+
if (!curState.segmentsCanLoad) return true;
|
|
1251
|
+
if (!curState.playbackInitiated) {
|
|
1252
|
+
if (curState.preload === "none") return true;
|
|
1253
|
+
return curState.preload === prevState.preload;
|
|
1174
1254
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
};
|
|
1182
|
-
return {
|
|
1183
|
-
url,
|
|
1184
|
-
detach
|
|
1185
|
-
};
|
|
1255
|
+
if (!prevState.playbackInitiated && curState.playbackInitiated) {
|
|
1256
|
+
if (prevState.preload !== "auto") return false;
|
|
1257
|
+
}
|
|
1258
|
+
if (!curState.track || !isResolvedTrack(curState.track)) return true;
|
|
1259
|
+
if (prevState.track?.id !== curState.track.id && isResolvedTrack(curState.track)) return false;
|
|
1260
|
+
return segmentStartFor(prevState.currentTime, curState.track) === segmentStartFor(curState.currentTime, curState.track);
|
|
1186
1261
|
}
|
|
1187
1262
|
/**
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1263
|
+
* Load segments orchestration — Reactor layer.
|
|
1264
|
+
*
|
|
1265
|
+
* Sends typed load messages to a SegmentLoaderActor when relevant conditions
|
|
1266
|
+
* change. Uses targeted subscriptions rather than broad combineLatest so only
|
|
1267
|
+
* meaningful state changes trigger evaluation.
|
|
1268
|
+
*
|
|
1269
|
+
* Condition hierarchy (see SegmentLoadingKey for detail):
|
|
1270
|
+
*
|
|
1271
|
+
* !playbackInitiated
|
|
1272
|
+
* preload==='none' (or unset) → dormant; no trigger
|
|
1273
|
+
* preload==='metadata' → trigger on transition to 'metadata'
|
|
1274
|
+
* preload==='auto' → trigger on transition to 'auto'
|
|
1275
|
+
*
|
|
1276
|
+
* !playbackInitiated → playbackInitiated
|
|
1277
|
+
* preload !== 'auto' → trigger (message shape changes)
|
|
1278
|
+
* preload === 'auto' → suppressed (was already full-range mode;
|
|
1279
|
+
* let segmentStart take over post-play)
|
|
1280
|
+
* KNOWN LIMITATION: seek-before-play with
|
|
1281
|
+
* preload='auto' is not supported — if the
|
|
1282
|
+
* user seeks before pressing play, the
|
|
1283
|
+
* first re-send is delayed until the next
|
|
1284
|
+
* segment boundary crossing post-play.
|
|
1190
1285
|
*
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1286
|
+
* playbackInitiated
|
|
1287
|
+
* resolvedTrackId changes → trigger
|
|
1288
|
+
* segmentStart(currentTime) changes → trigger (segment boundary only)
|
|
1194
1289
|
*
|
|
1195
1290
|
* @example
|
|
1196
|
-
* const
|
|
1197
|
-
* attachMediaSource(mediaSource, videoElement);
|
|
1198
|
-
* await waitForSourceOpen(mediaSource);
|
|
1199
|
-
* // MediaSource is now ready for SourceBuffer creation
|
|
1291
|
+
* const cleanup = loadSegments({ state, owners }, { type: 'video' });
|
|
1200
1292
|
*/
|
|
1201
|
-
function
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1293
|
+
function loadSegments({ state, owners }, config) {
|
|
1294
|
+
const { type } = config;
|
|
1295
|
+
const actorKey = ActorKeyByType$1[type];
|
|
1296
|
+
const initialBandwidth = state.get().bandwidthState;
|
|
1297
|
+
const throughput = signal(initialBandwidth ?? {
|
|
1298
|
+
fastEstimate: 0,
|
|
1299
|
+
fastTotalWeight: 0,
|
|
1300
|
+
slowEstimate: 0,
|
|
1301
|
+
slowTotalWeight: 0,
|
|
1302
|
+
bytesSampled: 0
|
|
1303
|
+
});
|
|
1304
|
+
const fetchBytes = type === "video" ? createTrackedFetch(throughput, initialBandwidth !== void 0 ? (next) => {
|
|
1305
|
+
state.set(Object.assign({}, state.get(), { bandwidthState: next }));
|
|
1306
|
+
} : void 0) : fetchStream;
|
|
1307
|
+
const segmentLoader = signal(void 0);
|
|
1308
|
+
const actorSource = computed(() => owners.get()[actorKey]);
|
|
1309
|
+
let currentLoader;
|
|
1310
|
+
const cleanupActorLifecycle = effect(() => {
|
|
1311
|
+
const actor = actorSource.get();
|
|
1312
|
+
if (currentLoader) {
|
|
1313
|
+
currentLoader.destroy();
|
|
1314
|
+
segmentLoader.set(void 0);
|
|
1315
|
+
currentLoader = void 0;
|
|
1206
1316
|
}
|
|
1207
|
-
if (
|
|
1208
|
-
|
|
1209
|
-
|
|
1317
|
+
if (actor) {
|
|
1318
|
+
const loader = createSegmentLoaderActor(actor, fetchBytes);
|
|
1319
|
+
currentLoader = loader;
|
|
1320
|
+
segmentLoader.set(loader);
|
|
1210
1321
|
}
|
|
1211
|
-
const controller = new AbortController();
|
|
1212
|
-
const options = { signal: controller.signal };
|
|
1213
|
-
mediaSource.addEventListener("sourceopen", () => {
|
|
1214
|
-
controller.abort();
|
|
1215
|
-
resolve();
|
|
1216
|
-
}, options);
|
|
1217
|
-
signal?.addEventListener("abort", () => {
|
|
1218
|
-
controller.abort();
|
|
1219
|
-
reject(new DOMException("Aborted", "AbortError"));
|
|
1220
|
-
}, options);
|
|
1221
1322
|
});
|
|
1323
|
+
const segmentsCanLoad = computed(() => {
|
|
1324
|
+
const track = getSelectedTrack(state.get(), type);
|
|
1325
|
+
return !!track && isResolvedTrack(track) && !!segmentLoader.get();
|
|
1326
|
+
});
|
|
1327
|
+
const loadingInputs = computed(() => selectLoadingInputs([segmentsCanLoad.get(), state.get()], type));
|
|
1328
|
+
let prevInputs;
|
|
1329
|
+
const cleanupLoadEffect = effect(() => {
|
|
1330
|
+
const inputs = loadingInputs.get();
|
|
1331
|
+
if (prevInputs !== void 0 && loadingInputsEq(prevInputs, inputs)) return;
|
|
1332
|
+
const { preload, playbackInitiated, currentTime, track, segmentsCanLoad: canLoad } = inputs;
|
|
1333
|
+
if (!canLoad) return;
|
|
1334
|
+
prevInputs = inputs;
|
|
1335
|
+
if (!(preload === "auto" || !!playbackInitiated))
|
|
1336
|
+
/** @ts-expect-error */
|
|
1337
|
+
segmentLoader.get()?.send({
|
|
1338
|
+
type: "load",
|
|
1339
|
+
track
|
|
1340
|
+
});
|
|
1341
|
+
else segmentLoader.get()?.send({
|
|
1342
|
+
type: "load",
|
|
1343
|
+
track,
|
|
1344
|
+
range: {
|
|
1345
|
+
start: currentTime,
|
|
1346
|
+
end: currentTime + DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1350
|
+
return () => {
|
|
1351
|
+
cleanupActorLifecycle();
|
|
1352
|
+
cleanupLoadEffect();
|
|
1353
|
+
currentLoader?.destroy();
|
|
1354
|
+
};
|
|
1222
1355
|
}
|
|
1356
|
+
|
|
1357
|
+
//#endregion
|
|
1358
|
+
//#region ../spf/dist/dev/dom/text/parse-vtt-segment.js
|
|
1223
1359
|
/**
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
* @param mediaSource - The MediaSource (must be in 'open' state)
|
|
1227
|
-
* @param mimeCodec - MIME type with codecs (e.g., 'video/mp4; codecs="avc1.42E01E"')
|
|
1228
|
-
* @returns The created SourceBuffer
|
|
1229
|
-
* @throws Error if MediaSource is not open or codec is unsupported
|
|
1360
|
+
* Parse a VTT segment using browser's native parser.
|
|
1230
1361
|
*
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
* const buffer = createSourceBuffer(mediaSource, 'video/mp4; codecs="avc1.42E01E"');
|
|
1362
|
+
* Creates a dummy video element with a track element to leverage
|
|
1363
|
+
* the browser's optimized VTT parsing. Returns parsed VTTCue objects.
|
|
1234
1364
|
*/
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
if (!
|
|
1238
|
-
|
|
1365
|
+
let dummyVideo = null;
|
|
1366
|
+
function ensureDummyVideo() {
|
|
1367
|
+
if (!dummyVideo) {
|
|
1368
|
+
dummyVideo = document.createElement("video");
|
|
1369
|
+
dummyVideo.muted = true;
|
|
1370
|
+
dummyVideo.preload = "none";
|
|
1371
|
+
dummyVideo.style.display = "none";
|
|
1372
|
+
dummyVideo.crossOrigin = "anonymous";
|
|
1373
|
+
}
|
|
1374
|
+
return dummyVideo;
|
|
1375
|
+
}
|
|
1376
|
+
function parseVttSegment(url) {
|
|
1377
|
+
const video = ensureDummyVideo();
|
|
1378
|
+
const track = document.createElement("track");
|
|
1379
|
+
track.kind = "subtitles";
|
|
1380
|
+
track.default = true;
|
|
1381
|
+
return new Promise((resolve, reject) => {
|
|
1382
|
+
const onLoad = () => {
|
|
1383
|
+
const cues = [];
|
|
1384
|
+
const textTrack = track.track;
|
|
1385
|
+
if (textTrack.cues) for (let i = 0; i < textTrack.cues.length; i++) {
|
|
1386
|
+
const cue = textTrack.cues[i];
|
|
1387
|
+
if (cue) cues.push(cue);
|
|
1388
|
+
}
|
|
1389
|
+
cleanup();
|
|
1390
|
+
resolve(cues);
|
|
1391
|
+
};
|
|
1392
|
+
const onError = () => {
|
|
1393
|
+
cleanup();
|
|
1394
|
+
reject(/* @__PURE__ */ new Error(`Failed to load VTT segment: ${url}`));
|
|
1395
|
+
};
|
|
1396
|
+
const cleanup = () => {
|
|
1397
|
+
track.removeEventListener("load", onLoad);
|
|
1398
|
+
track.removeEventListener("error", onError);
|
|
1399
|
+
video.removeChild(track);
|
|
1400
|
+
};
|
|
1401
|
+
track.addEventListener("load", onLoad);
|
|
1402
|
+
track.addEventListener("error", onError);
|
|
1403
|
+
video.appendChild(track);
|
|
1404
|
+
track.src = url;
|
|
1405
|
+
});
|
|
1239
1406
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
*
|
|
1243
|
-
* @param mimeCodec - MIME type with codecs string
|
|
1244
|
-
* @returns True if the codec is supported
|
|
1245
|
-
*
|
|
1246
|
-
* @example
|
|
1247
|
-
* if (isCodecSupported('video/mp4; codecs="avc1.42E01E"')) {
|
|
1248
|
-
* // Create source buffer
|
|
1249
|
-
* }
|
|
1250
|
-
*/
|
|
1251
|
-
function isCodecSupported(mimeCodec) {
|
|
1252
|
-
if (!supportsMediaSource()) return false;
|
|
1253
|
-
return MediaSource.isTypeSupported(mimeCodec);
|
|
1407
|
+
function destroyVttParser() {
|
|
1408
|
+
dummyVideo = null;
|
|
1254
1409
|
}
|
|
1255
1410
|
|
|
1256
1411
|
//#endregion
|
|
1257
|
-
//#region ../spf/dist/dev/
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1412
|
+
//#region ../spf/dist/dev/dom/features/load-text-track-cues.js
|
|
1413
|
+
const CueKeys = [
|
|
1414
|
+
"startTime",
|
|
1415
|
+
"endTime",
|
|
1416
|
+
"text"
|
|
1417
|
+
];
|
|
1418
|
+
function isDuplicateCue(cue, existingCues) {
|
|
1419
|
+
return Array.prototype.some.call(existingCues ?? [], (existingCue) => {
|
|
1420
|
+
return CueKeys.every((k) => existingCue[k] === cue[k]);
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
const loadVttSegmentTask = async ({ segment }, { textTrack }) => {
|
|
1424
|
+
(await parseVttSegment(segment.url)).forEach((cue) => {
|
|
1425
|
+
if (isDuplicateCue(cue, textTrack.cues)) return;
|
|
1426
|
+
textTrack.addCue(cue);
|
|
1427
|
+
});
|
|
1428
|
+
};
|
|
1265
1429
|
/**
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
* Events are dispatched synchronously to all subscribers.
|
|
1269
|
-
* Conforms to Observable-like shape for future compatibility.
|
|
1270
|
-
*
|
|
1271
|
-
* Events must have a 'type' property for discriminated union type narrowing.
|
|
1272
|
-
*
|
|
1273
|
-
* @example
|
|
1274
|
-
* ```ts
|
|
1275
|
-
* type Action = { type: 'PLAY' } | { type: 'PAUSE' };
|
|
1276
|
-
* const events = createEventStream<Action>();
|
|
1277
|
-
*
|
|
1278
|
-
* events.subscribe((action) => {
|
|
1279
|
-
* if (action.type === 'PLAY') {
|
|
1280
|
-
* // Type narrowed to { type: 'PLAY' }
|
|
1281
|
-
* }
|
|
1282
|
-
* });
|
|
1283
|
-
*
|
|
1284
|
-
* events.dispatch({ type: 'PLAY' });
|
|
1285
|
-
* ```
|
|
1430
|
+
* Load text track cues task (composite - orchestrates VTT segment subtasks).
|
|
1286
1431
|
*/
|
|
1287
|
-
|
|
1288
|
-
const
|
|
1289
|
-
return
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1432
|
+
const loadTextTrackCuesTask = async ({ currentState }, context) => {
|
|
1433
|
+
const track = findSelectedTextTrack(currentState);
|
|
1434
|
+
if (!track || !isResolvedTrack(track)) return;
|
|
1435
|
+
const { segments } = track;
|
|
1436
|
+
if (segments.length === 0) return;
|
|
1437
|
+
const trackId = track.id;
|
|
1438
|
+
const loadedIds = new Set((currentState.textBufferState?.[trackId]?.segments ?? []).map((s) => s.id));
|
|
1439
|
+
const segmentsToLoad = getSegmentsToLoad(segments, segments.filter((s) => loadedIds.has(s.id)), currentState.currentTime ?? 0).filter((s) => !loadedIds.has(s.id));
|
|
1440
|
+
if (segmentsToLoad.length === 0) return;
|
|
1441
|
+
for (const segment of segmentsToLoad) {
|
|
1442
|
+
if (context.signal.aborted) break;
|
|
1443
|
+
try {
|
|
1444
|
+
await loadVttSegmentTask({ segment }, { textTrack: context.textTrack });
|
|
1445
|
+
const latestState = context.state.get();
|
|
1446
|
+
const latest = latestState.textBufferState ?? {};
|
|
1447
|
+
const trackState = latest[trackId] ?? { segments: [] };
|
|
1448
|
+
context.state.set({
|
|
1449
|
+
...latestState,
|
|
1450
|
+
textBufferState: {
|
|
1451
|
+
...latest,
|
|
1452
|
+
[trackId]: { segments: [...trackState.segments, { id: segment.id }] }
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
if (error instanceof Error && error.name === "AbortError") break;
|
|
1457
|
+
console.error("Failed to load VTT segment:", error);
|
|
1298
1458
|
}
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1459
|
+
}
|
|
1460
|
+
if (context.textTrack.mode === "showing" && context.textTrack.cues) Array.from(context.textTrack.cues).forEach((cue) => {
|
|
1461
|
+
context.textTrack.addCue(cue);
|
|
1462
|
+
});
|
|
1463
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
1464
|
+
};
|
|
1304
1465
|
/**
|
|
1305
|
-
*
|
|
1466
|
+
* Find the selected text track in the presentation.
|
|
1306
1467
|
*/
|
|
1307
|
-
function
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
return
|
|
1468
|
+
function findSelectedTextTrack(state) {
|
|
1469
|
+
if (!state.presentation || !state.selectedTextTrackId) return;
|
|
1470
|
+
const textSet = state.presentation.selectionSets.find((set) => set.type === "text");
|
|
1471
|
+
if (!textSet?.switchingSets?.[0]?.tracks) return;
|
|
1472
|
+
return textSet.switchingSets[0].tracks.find((t) => t.id === state.selectedTextTrackId);
|
|
1312
1473
|
}
|
|
1313
1474
|
/**
|
|
1314
|
-
*
|
|
1475
|
+
* Get the browser's TextTrack object for the selected text track.
|
|
1315
1476
|
*
|
|
1316
|
-
*
|
|
1317
|
-
*
|
|
1318
|
-
* - Event-driven: play event
|
|
1477
|
+
* Retrieves the live TextTrack interface from the track element in owners,
|
|
1478
|
+
* which is used for adding cues, checking mode, and managing track state.
|
|
1319
1479
|
*
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1480
|
+
* Note: Returns the DOM TextTrack interface (HTMLTrackElement.track),
|
|
1481
|
+
* not the presentation Track metadata type.
|
|
1482
|
+
*
|
|
1483
|
+
* @param state - Current playback state (track selection)
|
|
1484
|
+
* @param owners - DOM owners containing track elements map
|
|
1485
|
+
* @returns DOM TextTrack interface or undefined if not found
|
|
1323
1486
|
*/
|
|
1324
|
-
function
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1487
|
+
function getSelectedTextTrackFromOwners(state, owners) {
|
|
1488
|
+
const trackId = state.selectedTextTrackId;
|
|
1489
|
+
if (!trackId || !owners.textTracks) return;
|
|
1490
|
+
return owners.textTracks.get(trackId)?.track;
|
|
1327
1491
|
}
|
|
1328
1492
|
/**
|
|
1329
|
-
*
|
|
1493
|
+
* Check if we can load text track cues.
|
|
1330
1494
|
*
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
1495
|
+
* Requires:
|
|
1496
|
+
* - Selected text track ID exists
|
|
1497
|
+
* - Track elements map exists
|
|
1498
|
+
* - Track element exists for selected track
|
|
1499
|
+
*/
|
|
1500
|
+
function canLoadTextTrackCues(state, owners) {
|
|
1501
|
+
return !!state.selectedTextTrackId && !!owners.textTracks && owners.textTracks.has(state.selectedTextTrackId);
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Check if we should load text track cues.
|
|
1333
1505
|
*
|
|
1334
|
-
*
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
1506
|
+
* Only load if:
|
|
1507
|
+
* - Track is resolved (has segments)
|
|
1508
|
+
* - Track has at least one segment
|
|
1509
|
+
* - Track element exists
|
|
1337
1510
|
*/
|
|
1338
|
-
function
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1511
|
+
function shouldLoadTextTrackCues(state, owners) {
|
|
1512
|
+
if (!canLoadTextTrackCues(state, owners)) return false;
|
|
1513
|
+
const track = findSelectedTextTrack(state);
|
|
1514
|
+
if (!track || !isResolvedTrack(track) || track.segments.length === 0) return false;
|
|
1515
|
+
if (!getSelectedTextTrackFromOwners(state, owners)) return false;
|
|
1516
|
+
return true;
|
|
1344
1517
|
}
|
|
1345
1518
|
/**
|
|
1346
|
-
*
|
|
1519
|
+
* Load text track cues orchestration.
|
|
1347
1520
|
*
|
|
1348
|
-
*
|
|
1349
|
-
*
|
|
1521
|
+
* Triggers when:
|
|
1522
|
+
* - Text track is selected
|
|
1523
|
+
* - Track is resolved (has segments)
|
|
1524
|
+
* - Track element exists
|
|
1350
1525
|
*
|
|
1351
|
-
*
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
1526
|
+
* Fetches and parses VTT segments within the forward buffer window, then adds
|
|
1527
|
+
* cues to the track incrementally. Continues on segment errors to provide
|
|
1528
|
+
* partial subtitles.
|
|
1354
1529
|
*
|
|
1355
1530
|
* @example
|
|
1356
|
-
*
|
|
1357
|
-
* const state = createState({ presentation: undefined, preload: 'auto' });
|
|
1358
|
-
* const events = createEventStream<PresentationAction>();
|
|
1359
|
-
*
|
|
1360
|
-
* const cleanup = resolvePresentation({ state, events });
|
|
1361
|
-
*
|
|
1362
|
-
* // State-driven: resolves immediately when preload allows
|
|
1363
|
-
* state.patch({ presentation: { url: 'http://example.com/playlist.m3u8' } });
|
|
1364
|
-
*
|
|
1365
|
-
* // Event-driven: resolves on PLAY when preload="none"
|
|
1366
|
-
* state.patch({ preload: 'none', presentation: { url: '...' } });
|
|
1367
|
-
* events.dispatch({ type: 'PLAY' });
|
|
1368
|
-
* ```
|
|
1531
|
+
* const cleanup = loadTextTrackCues({ state, owners });
|
|
1369
1532
|
*/
|
|
1370
|
-
function
|
|
1371
|
-
let
|
|
1533
|
+
function loadTextTrackCues({ state, owners }) {
|
|
1534
|
+
let currentTask = null;
|
|
1372
1535
|
let abortController = null;
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
if (error instanceof Error && error.name === "AbortError") return;
|
|
1383
|
-
throw error;
|
|
1384
|
-
} finally {
|
|
1385
|
-
resolving = false;
|
|
1386
|
-
abortController = null;
|
|
1536
|
+
let lastTrackId;
|
|
1537
|
+
const selectedTrackId = computed(() => state.get().selectedTextTrackId);
|
|
1538
|
+
const cleanupEffect = effect(() => {
|
|
1539
|
+
const s = state.get();
|
|
1540
|
+
const o = owners.get();
|
|
1541
|
+
if (selectedTrackId.get() !== lastTrackId) {
|
|
1542
|
+
lastTrackId = selectedTrackId.get();
|
|
1543
|
+
abortController?.abort();
|
|
1544
|
+
currentTask = null;
|
|
1387
1545
|
}
|
|
1546
|
+
if (currentTask) return;
|
|
1547
|
+
if (!shouldLoadTextTrackCues(s, o)) return;
|
|
1548
|
+
const textTrack = getSelectedTextTrackFromOwners(s, o);
|
|
1549
|
+
if (!textTrack) return;
|
|
1550
|
+
abortController = new AbortController();
|
|
1551
|
+
currentTask = loadTextTrackCuesTask({ currentState: s }, {
|
|
1552
|
+
signal: abortController.signal,
|
|
1553
|
+
textTrack,
|
|
1554
|
+
state
|
|
1555
|
+
}).finally(() => {
|
|
1556
|
+
currentTask = null;
|
|
1557
|
+
});
|
|
1388
1558
|
});
|
|
1389
1559
|
return () => {
|
|
1390
1560
|
abortController?.abort();
|
|
1391
|
-
|
|
1561
|
+
cleanupEffect();
|
|
1392
1562
|
};
|
|
1393
1563
|
}
|
|
1394
1564
|
|
|
1395
1565
|
//#endregion
|
|
1396
|
-
//#region ../spf/dist/dev/
|
|
1397
|
-
/**
|
|
1398
|
-
* Default quality switching configuration.
|
|
1399
|
-
*/
|
|
1400
|
-
const DEFAULT_SWITCHING_CONFIG = {
|
|
1401
|
-
safetyMargin: .85,
|
|
1402
|
-
minUpgradeInterval: 8e3,
|
|
1403
|
-
defaultBandwidth: 5e6
|
|
1404
|
-
};
|
|
1405
|
-
/**
|
|
1406
|
-
* Get all video tracks from a presentation's first switching set.
|
|
1407
|
-
* Returns [] when the presentation is still unresolved (no selectionSets yet).
|
|
1408
|
-
*/
|
|
1409
|
-
function getVideoTracks(presentation) {
|
|
1410
|
-
return (presentation.selectionSets?.find((s) => s.type === "video"))?.switchingSets[0]?.tracks ?? [];
|
|
1411
|
-
}
|
|
1566
|
+
//#region ../spf/dist/dev/dom/features/track-current-time.js
|
|
1412
1567
|
/**
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1415
|
-
* Reacts to bandwidth estimate changes and updates `selectedVideoTrackId`
|
|
1416
|
-
* when a different quality is optimal:
|
|
1568
|
+
* Track current playback position from the media element.
|
|
1417
1569
|
*
|
|
1418
|
-
*
|
|
1419
|
-
* -
|
|
1420
|
-
* -
|
|
1570
|
+
* Mirrors `mediaElement.currentTime` into reactive state on:
|
|
1571
|
+
* - `timeupdate` — fires during playback (~4 Hz)
|
|
1572
|
+
* - `seeking` — fires when a seek begins; per spec, `currentTime` is
|
|
1573
|
+
* already at the new position when this event dispatches, so buffer
|
|
1574
|
+
* management can react immediately rather than waiting for `timeupdate`,
|
|
1575
|
+
* which does not fire while paused.
|
|
1421
1576
|
*
|
|
1422
|
-
*
|
|
1423
|
-
* `resolveTrack` fetches the new playlist and `loadSegments` reloads the init
|
|
1424
|
-
* segment, then appends media segments from the current position in the new
|
|
1425
|
-
* quality. The browser's SourceBuffer replaces the overlapping buffered range.
|
|
1577
|
+
* Also syncs immediately when a media element becomes available.
|
|
1426
1578
|
*
|
|
1427
1579
|
* @example
|
|
1428
|
-
* const cleanup =
|
|
1429
|
-
* // Later, when done:
|
|
1430
|
-
* cleanup();
|
|
1580
|
+
* const cleanup = trackCurrentTime({ state, owners });
|
|
1431
1581
|
*/
|
|
1432
|
-
function
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
const
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
if (!
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
lastUpgradeTime = now;
|
|
1453
|
-
}
|
|
1454
|
-
state.patch({ selectedVideoTrackId: optimal.id });
|
|
1582
|
+
function trackCurrentTime({ state, owners }) {
|
|
1583
|
+
let lastMediaElement;
|
|
1584
|
+
let removeListeners = null;
|
|
1585
|
+
const cleanupEffect = effect(() => {
|
|
1586
|
+
const { mediaElement } = owners.get();
|
|
1587
|
+
if (mediaElement === lastMediaElement) return;
|
|
1588
|
+
removeListeners?.();
|
|
1589
|
+
removeListeners = null;
|
|
1590
|
+
lastMediaElement = mediaElement;
|
|
1591
|
+
if (!mediaElement) return;
|
|
1592
|
+
const sync = () => {
|
|
1593
|
+
update(state, { currentTime: mediaElement.currentTime });
|
|
1594
|
+
};
|
|
1595
|
+
sync();
|
|
1596
|
+
const removeTimeupdate = listen(mediaElement, "timeupdate", sync);
|
|
1597
|
+
const removeSeeking = listen(mediaElement, "seeking", sync);
|
|
1598
|
+
removeListeners = () => {
|
|
1599
|
+
removeTimeupdate();
|
|
1600
|
+
removeSeeking();
|
|
1601
|
+
};
|
|
1455
1602
|
});
|
|
1603
|
+
return () => {
|
|
1604
|
+
removeListeners?.();
|
|
1605
|
+
cleanupEffect();
|
|
1606
|
+
};
|
|
1456
1607
|
}
|
|
1457
1608
|
|
|
1458
1609
|
//#endregion
|
|
1459
|
-
//#region ../spf/dist/dev/
|
|
1460
|
-
/**
|
|
1461
|
-
* Map track type to selected track ID property key in state.
|
|
1462
|
-
*/
|
|
1463
|
-
const SelectedTrackIdKeyByType = {
|
|
1464
|
-
video: "selectedVideoTrackId",
|
|
1465
|
-
audio: "selectedAudioTrackId",
|
|
1466
|
-
text: "selectedTextTrackId"
|
|
1467
|
-
};
|
|
1610
|
+
//#region ../spf/dist/dev/dom/features/track-playback-initiated.js
|
|
1468
1611
|
/**
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1612
|
+
* Track whether playback has been initiated for the current presentation URL.
|
|
1613
|
+
*
|
|
1614
|
+
* Uses a local intermediate signal written by two effect streams:
|
|
1615
|
+
* - false stream: resets on URL change
|
|
1616
|
+
* - true stream: sets on play event
|
|
1617
|
+
*
|
|
1618
|
+
* A third merge effect reads the local signal and writes to state, reading
|
|
1619
|
+
* `state.get()` at merge time so the spread uses the up-to-date value after
|
|
1620
|
+
* the async forward bridge has run.
|
|
1472
1621
|
*
|
|
1473
1622
|
* @example
|
|
1474
|
-
* const
|
|
1475
|
-
* if (videoTrack && isResolvedTrack(videoTrack)) {
|
|
1476
|
-
* // videoTrack is VideoTrack
|
|
1477
|
-
* }
|
|
1623
|
+
* const cleanup = trackPlaybackInitiated({ state, owners, events });
|
|
1478
1624
|
*/
|
|
1479
|
-
function
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1625
|
+
function trackPlaybackInitiated({ state, owners }) {
|
|
1626
|
+
const presentationUrl = computed(() => state.get().presentation?.url);
|
|
1627
|
+
const mediaElement = computed(() => owners.get().mediaElement);
|
|
1628
|
+
const playbackInitiated = signal(void 0);
|
|
1629
|
+
let lastPresentationUrl;
|
|
1630
|
+
let lastMediaElement;
|
|
1631
|
+
const cleanupResetEffect = effect(() => {
|
|
1632
|
+
const url = presentationUrl.get();
|
|
1633
|
+
const el = mediaElement.get();
|
|
1634
|
+
if (url !== lastPresentationUrl && lastPresentationUrl !== void 0 || el !== lastMediaElement && lastMediaElement !== void 0) playbackInitiated.set(false);
|
|
1635
|
+
lastPresentationUrl = url;
|
|
1636
|
+
lastMediaElement = el;
|
|
1637
|
+
});
|
|
1638
|
+
const cleanupPlayEffect = effect(() => {
|
|
1639
|
+
const el = mediaElement.get();
|
|
1640
|
+
if (!el) return;
|
|
1641
|
+
return listen(el, "play", () => {
|
|
1642
|
+
playbackInitiated.set(true);
|
|
1643
|
+
});
|
|
1644
|
+
});
|
|
1645
|
+
const cleanupMergeEffect = effect(() => {
|
|
1646
|
+
const pi = playbackInitiated.get();
|
|
1647
|
+
if (pi === void 0) return;
|
|
1648
|
+
if (state.get().playbackInitiated !== pi) update(state, { playbackInitiated: pi });
|
|
1649
|
+
});
|
|
1650
|
+
return () => {
|
|
1651
|
+
cleanupResetEffect();
|
|
1652
|
+
cleanupPlayEffect();
|
|
1653
|
+
cleanupMergeEffect();
|
|
1654
|
+
};
|
|
1485
1655
|
}
|
|
1486
1656
|
|
|
1487
1657
|
//#endregion
|
|
1488
|
-
//#region ../spf/dist/dev/dom/
|
|
1658
|
+
//#region ../spf/dist/dev/dom/media/append-segment.js
|
|
1489
1659
|
/**
|
|
1490
|
-
*
|
|
1491
|
-
*
|
|
1492
|
-
* Receives load assignments via `send()` and owns all execution: planning,
|
|
1493
|
-
* removes, fetches, and appends. Coordinates with the SourceBufferActor for
|
|
1494
|
-
* all physical SourceBuffer operations.
|
|
1660
|
+
* Append media data to a SourceBuffer.
|
|
1495
1661
|
*
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
*
|
|
1499
|
-
* operation (if still needed) or preempts it.
|
|
1662
|
+
* Accepts either a full ArrayBuffer (single append) or an AsyncIterable of
|
|
1663
|
+
* Uint8Array chunks (one append per chunk, in order). Waits for `updateend`
|
|
1664
|
+
* between each call so appends are serialized correctly.
|
|
1500
1665
|
*
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1503
|
-
* Accepts an optional `minChunkSize` in options; init segments pass `Infinity`
|
|
1504
|
-
* so the entire body accumulates as one chunk before appending.
|
|
1666
|
+
* Errors from the SourceBuffer (`error` event) or from the iterable are
|
|
1667
|
+
* propagated as rejections.
|
|
1505
1668
|
*/
|
|
1506
|
-
function
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
let destroyed = false;
|
|
1513
|
-
const getBufferedSegments = (allSegments) => {
|
|
1514
|
-
const bufferedIds = new Set(sourceBufferActor.snapshot.context.segments.filter((s) => !s.partial).map((s) => s.id));
|
|
1515
|
-
return allSegments.filter((s) => bufferedIds.has(s.id));
|
|
1516
|
-
};
|
|
1517
|
-
/**
|
|
1518
|
-
* Translate a load message into an ordered LoadTask list based on committed
|
|
1519
|
-
* actor state. In-flight awareness is handled separately in send().
|
|
1520
|
-
*
|
|
1521
|
-
* @todo Rename alongside LoadTask (e.g. planOps).
|
|
1522
|
-
*
|
|
1523
|
-
* Case 1 — Removes: forward and back buffer flush points, segment-aligned.
|
|
1524
|
-
* No flush on track switch: appending new content overwrites existing buffer
|
|
1525
|
-
* ranges, and the actor's time-aligned deduplication keeps the segment model
|
|
1526
|
-
* accurate as new segments arrive.
|
|
1527
|
-
*
|
|
1528
|
-
* Case 2 — Init: schedule if not yet committed for this track.
|
|
1529
|
-
*
|
|
1530
|
-
* Case 3 — Segments: all segments in the load window not yet committed.
|
|
1531
|
-
*/
|
|
1532
|
-
const planTasks = (message) => {
|
|
1533
|
-
const { track, range } = message;
|
|
1534
|
-
const actorCtx = sourceBufferActor.snapshot.context;
|
|
1535
|
-
const bufferedSegments = getBufferedSegments(track.segments);
|
|
1536
|
-
const currentTime = range?.start ?? 0;
|
|
1537
|
-
const tasks = [];
|
|
1538
|
-
if (range) {
|
|
1539
|
-
const forwardFlushStart = calculateForwardFlushPoint(bufferedSegments, currentTime);
|
|
1540
|
-
if (forwardFlushStart < Infinity) tasks.push({
|
|
1541
|
-
type: "remove",
|
|
1542
|
-
start: forwardFlushStart,
|
|
1543
|
-
end: Infinity
|
|
1544
|
-
});
|
|
1545
|
-
const backFlushEnd = calculateBackBufferFlushPoint(bufferedSegments, currentTime);
|
|
1546
|
-
if (backFlushEnd > 0) tasks.push({
|
|
1547
|
-
type: "remove",
|
|
1548
|
-
start: 0,
|
|
1549
|
-
end: backFlushEnd
|
|
1550
|
-
});
|
|
1551
|
-
}
|
|
1552
|
-
if (actorCtx.initTrackId !== track.id) tasks.push({
|
|
1553
|
-
type: "append-init",
|
|
1554
|
-
meta: { trackId: track.id },
|
|
1555
|
-
url: track.initialization.url,
|
|
1556
|
-
...track.initialization.byteRange !== void 0 && { byteRange: track.initialization.byteRange }
|
|
1557
|
-
});
|
|
1558
|
-
if (range) {
|
|
1559
|
-
const EPSILON = 1e-4;
|
|
1560
|
-
const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime).filter((seg) => {
|
|
1561
|
-
const existing = actorCtx.segments.find((s) => Math.abs(s.startTime - seg.startTime) < EPSILON);
|
|
1562
|
-
if (existing?.partial) return true;
|
|
1563
|
-
if (!existing?.trackBandwidth || !track.bandwidth) return true;
|
|
1564
|
-
return track.bandwidth > existing.trackBandwidth;
|
|
1565
|
-
});
|
|
1566
|
-
for (const segment of segmentsToLoad) tasks.push({
|
|
1567
|
-
type: "append-segment",
|
|
1568
|
-
meta: {
|
|
1569
|
-
id: segment.id,
|
|
1570
|
-
startTime: segment.startTime,
|
|
1571
|
-
duration: segment.duration,
|
|
1572
|
-
trackId: track.id,
|
|
1573
|
-
trackBandwidth: track.bandwidth
|
|
1574
|
-
},
|
|
1575
|
-
url: segment.url,
|
|
1576
|
-
...segment.byteRange !== void 0 && { byteRange: segment.byteRange }
|
|
1577
|
-
});
|
|
1669
|
+
async function appendSegment(sourceBuffer, data, signal) {
|
|
1670
|
+
if (data instanceof ArrayBuffer) await appendChunk(sourceBuffer, data);
|
|
1671
|
+
else try {
|
|
1672
|
+
for await (const chunk of data) {
|
|
1673
|
+
if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
1674
|
+
await appendChunk(sourceBuffer, chunk);
|
|
1578
1675
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1676
|
+
} catch (e) {
|
|
1677
|
+
if (e instanceof DOMException && e.name === "AbortError" && !sourceBuffer.updating) try {
|
|
1678
|
+
sourceBuffer.abort();
|
|
1679
|
+
} catch {}
|
|
1680
|
+
throw e;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
async function appendChunk(sourceBuffer, data) {
|
|
1684
|
+
if (sourceBuffer.updating) await new Promise((resolve) => {
|
|
1685
|
+
const onUpdateEnd = () => {
|
|
1686
|
+
sourceBuffer.removeEventListener("updateend", onUpdateEnd);
|
|
1687
|
+
resolve();
|
|
1688
|
+
};
|
|
1689
|
+
sourceBuffer.addEventListener("updateend", onUpdateEnd);
|
|
1690
|
+
});
|
|
1691
|
+
return new Promise((resolve, reject) => {
|
|
1692
|
+
const onUpdateEnd = () => {
|
|
1693
|
+
cleanup();
|
|
1694
|
+
resolve();
|
|
1695
|
+
};
|
|
1696
|
+
const onError = (event) => {
|
|
1697
|
+
cleanup();
|
|
1698
|
+
reject(/* @__PURE__ */ new Error(`SourceBuffer append error: ${event.type}`));
|
|
1699
|
+
};
|
|
1700
|
+
const cleanup = () => {
|
|
1701
|
+
sourceBuffer.removeEventListener("updateend", onUpdateEnd);
|
|
1702
|
+
sourceBuffer.removeEventListener("error", onError);
|
|
1703
|
+
};
|
|
1704
|
+
sourceBuffer.addEventListener("updateend", onUpdateEnd);
|
|
1705
|
+
sourceBuffer.addEventListener("error", onError);
|
|
1590
1706
|
try {
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
if (task.type === "append-init") {
|
|
1596
|
-
inFlightInitTrackId = task.meta.trackId;
|
|
1597
|
-
if (!signal.aborted) {
|
|
1598
|
-
const data = await fetchBytes(task, {
|
|
1599
|
-
signal,
|
|
1600
|
-
minChunkSize: Infinity
|
|
1601
|
-
});
|
|
1602
|
-
const isTrackSwitch = pendingTasks?.some((t) => t.type === "append-init" && t.meta.trackId !== task.meta.trackId);
|
|
1603
|
-
if (!signal.aborted || !isTrackSwitch) {
|
|
1604
|
-
const appendSignal = signal.aborted ? new AbortController().signal : signal;
|
|
1605
|
-
await sourceBufferActor.send({
|
|
1606
|
-
type: "append-init",
|
|
1607
|
-
data,
|
|
1608
|
-
meta: task.meta
|
|
1609
|
-
}, appendSignal);
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
return;
|
|
1613
|
-
}
|
|
1614
|
-
inFlightSegmentId = task.meta.id;
|
|
1615
|
-
if (!signal.aborted) {
|
|
1616
|
-
const stream = await fetchBytes(task, { signal });
|
|
1617
|
-
if (!signal.aborted) await sourceBufferActor.send({
|
|
1618
|
-
type: "append-segment",
|
|
1619
|
-
data: stream,
|
|
1620
|
-
meta: task.meta
|
|
1621
|
-
}, signal);
|
|
1622
|
-
}
|
|
1623
|
-
} finally {
|
|
1624
|
-
inFlightInitTrackId = null;
|
|
1625
|
-
inFlightSegmentId = null;
|
|
1626
|
-
}
|
|
1627
|
-
};
|
|
1628
|
-
/**
|
|
1629
|
-
* Drain the scheduled task list sequentially.
|
|
1630
|
-
* After each task completes, checks for a pending replacement plan from send().
|
|
1631
|
-
* If the signal was aborted and no new plan arrived, stops immediately.
|
|
1632
|
-
*/
|
|
1633
|
-
const runScheduled = async (initialTasks) => {
|
|
1634
|
-
running = true;
|
|
1635
|
-
abortController = new AbortController();
|
|
1636
|
-
let scheduled = initialTasks;
|
|
1637
|
-
while (scheduled.length > 0 && !destroyed) {
|
|
1638
|
-
const task = scheduled[0];
|
|
1639
|
-
scheduled = scheduled.slice(1);
|
|
1640
|
-
try {
|
|
1641
|
-
await executeLoadTask(task);
|
|
1642
|
-
} catch (error) {
|
|
1643
|
-
if (error instanceof Error && error.name === "AbortError") {} else {
|
|
1644
|
-
console.error("Unexpected error in segment loader:", error);
|
|
1645
|
-
scheduled = [];
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
if (pendingTasks !== null) {
|
|
1649
|
-
scheduled = pendingTasks;
|
|
1650
|
-
pendingTasks = null;
|
|
1651
|
-
abortController = new AbortController();
|
|
1652
|
-
} else if (abortController.signal.aborted) break;
|
|
1707
|
+
sourceBuffer.appendBuffer(data);
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
cleanup();
|
|
1710
|
+
reject(error);
|
|
1653
1711
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
//#endregion
|
|
1716
|
+
//#region ../spf/dist/dev/dom/media/buffer-flusher.js
|
|
1717
|
+
/**
|
|
1718
|
+
* Buffer flusher helper (P12)
|
|
1719
|
+
*
|
|
1720
|
+
* Removes a time range from a SourceBuffer to manage memory.
|
|
1721
|
+
*/
|
|
1722
|
+
/**
|
|
1723
|
+
* Remove a time range from a SourceBuffer.
|
|
1724
|
+
*
|
|
1725
|
+
* Waits for the SourceBuffer to be ready (not updating), then removes
|
|
1726
|
+
* the specified range. Returns a promise that resolves when removal completes.
|
|
1727
|
+
*
|
|
1728
|
+
* @param sourceBuffer - The SourceBuffer to remove data from
|
|
1729
|
+
* @param start - Start of the time range to remove (seconds)
|
|
1730
|
+
* @param end - End of the time range to remove (seconds)
|
|
1731
|
+
* @returns Promise that resolves when removal completes
|
|
1732
|
+
*
|
|
1733
|
+
* @example
|
|
1734
|
+
* await flushBuffer(videoSourceBuffer, 0, 30);
|
|
1735
|
+
*/
|
|
1736
|
+
async function flushBuffer(sourceBuffer, start, end) {
|
|
1737
|
+
if (sourceBuffer.updating) await new Promise((resolve) => {
|
|
1738
|
+
const onUpdateEnd = () => {
|
|
1739
|
+
sourceBuffer.removeEventListener("updateend", onUpdateEnd);
|
|
1740
|
+
resolve();
|
|
1741
|
+
};
|
|
1742
|
+
sourceBuffer.addEventListener("updateend", onUpdateEnd);
|
|
1743
|
+
});
|
|
1744
|
+
return new Promise((resolve, reject) => {
|
|
1745
|
+
const onUpdateEnd = () => {
|
|
1746
|
+
cleanup();
|
|
1747
|
+
resolve();
|
|
1748
|
+
};
|
|
1749
|
+
const onError = (event) => {
|
|
1750
|
+
cleanup();
|
|
1751
|
+
reject(/* @__PURE__ */ new Error(`SourceBuffer remove error: ${event.type}`));
|
|
1752
|
+
};
|
|
1753
|
+
const cleanup = () => {
|
|
1754
|
+
sourceBuffer.removeEventListener("updateend", onUpdateEnd);
|
|
1755
|
+
sourceBuffer.removeEventListener("error", onError);
|
|
1756
|
+
};
|
|
1757
|
+
sourceBuffer.addEventListener("updateend", onUpdateEnd);
|
|
1758
|
+
sourceBuffer.addEventListener("error", onError);
|
|
1759
|
+
try {
|
|
1760
|
+
sourceBuffer.remove(start, end);
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
cleanup();
|
|
1763
|
+
reject(error);
|
|
1675
1764
|
}
|
|
1676
|
-
};
|
|
1765
|
+
});
|
|
1677
1766
|
}
|
|
1678
1767
|
|
|
1679
1768
|
//#endregion
|
|
1680
|
-
//#region ../spf/dist/dev/
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
return
|
|
1687
|
-
const { minChunkSize, ...fetchOptions } = options ?? {};
|
|
1688
|
-
const response = await fetchResolvable(addressable, fetchOptions);
|
|
1689
|
-
if (!response.body) throw new Error("Response has no body");
|
|
1690
|
-
const body = response.body;
|
|
1691
|
-
return { [Symbol.asyncIterator]: async function* () {
|
|
1692
|
-
let chunkStart = performance.now();
|
|
1693
|
-
for await (const chunk of new ChunkedStreamIterable(body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : [])) {
|
|
1694
|
-
const elapsed = performance.now() - chunkStart;
|
|
1695
|
-
const next = sampleBandwidth(throughput.current, elapsed, chunk.byteLength);
|
|
1696
|
-
throughput.patch(next);
|
|
1697
|
-
throughput.flush();
|
|
1698
|
-
onSample?.(next);
|
|
1699
|
-
yield chunk;
|
|
1700
|
-
chunkStart = performance.now();
|
|
1701
|
-
}
|
|
1702
|
-
} };
|
|
1703
|
-
};
|
|
1769
|
+
//#region ../spf/dist/dev/core/features/calculate-presentation-duration.js
|
|
1770
|
+
/**
|
|
1771
|
+
* Check if we can calculate presentation duration (have required data).
|
|
1772
|
+
*/
|
|
1773
|
+
function canCalculateDuration(state) {
|
|
1774
|
+
if (!state.presentation) return false;
|
|
1775
|
+
return !!(state.selectedVideoTrackId || state.selectedAudioTrackId);
|
|
1704
1776
|
}
|
|
1705
1777
|
/**
|
|
1706
|
-
*
|
|
1707
|
-
* as a lazy chunk iterable. Used for audio tracks which don't sample bandwidth.
|
|
1708
|
-
* Pass `minChunkSize: Infinity` to accumulate the full body as a single chunk
|
|
1709
|
-
* (equivalent to arrayBuffer() but through the same streaming path).
|
|
1778
|
+
* Check if we should calculate presentation duration (conditions met).
|
|
1710
1779
|
*/
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
const
|
|
1714
|
-
if (
|
|
1715
|
-
|
|
1780
|
+
function shouldCalculateDuration(state) {
|
|
1781
|
+
if (!canCalculateDuration(state)) return false;
|
|
1782
|
+
const { presentation } = state;
|
|
1783
|
+
if (presentation.duration !== void 0) return false;
|
|
1784
|
+
const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, "video") : void 0;
|
|
1785
|
+
const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, "audio") : void 0;
|
|
1786
|
+
return !!(videoTrack && isResolvedTrack(videoTrack) || audioTrack && isResolvedTrack(audioTrack));
|
|
1716
1787
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
};
|
|
1788
|
+
/**
|
|
1789
|
+
* Get duration from the first resolved track (prefer video, fallback to audio).
|
|
1790
|
+
*/
|
|
1791
|
+
function getDurationFromResolvedTracks(state) {
|
|
1792
|
+
const videoTrack = state.selectedVideoTrackId ? getSelectedTrack(state, "video") : void 0;
|
|
1793
|
+
if (videoTrack && isResolvedTrack(videoTrack)) return videoTrack.duration;
|
|
1794
|
+
const audioTrack = state.selectedAudioTrackId ? getSelectedTrack(state, "audio") : void 0;
|
|
1795
|
+
if (audioTrack && isResolvedTrack(audioTrack)) return audioTrack.duration;
|
|
1726
1796
|
}
|
|
1727
1797
|
/**
|
|
1728
|
-
*
|
|
1798
|
+
* Calculate and set presentation duration from resolved tracks.
|
|
1799
|
+
*/
|
|
1800
|
+
function calculatePresentationDuration({ state }) {
|
|
1801
|
+
return effect(() => {
|
|
1802
|
+
const currentState = state.get();
|
|
1803
|
+
if (!shouldCalculateDuration(currentState)) return;
|
|
1804
|
+
const duration = getDurationFromResolvedTracks(currentState);
|
|
1805
|
+
if (duration === void 0 || !Number.isFinite(duration)) return;
|
|
1806
|
+
state.set({
|
|
1807
|
+
...currentState,
|
|
1808
|
+
presentation: {
|
|
1809
|
+
...currentState.presentation,
|
|
1810
|
+
duration
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
//#endregion
|
|
1817
|
+
//#region ../spf/dist/dev/core/abr/quality-selection.js
|
|
1818
|
+
/**
|
|
1819
|
+
* Default quality selection configuration.
|
|
1820
|
+
* Values match Shaka Player upgrade threshold (0.85 = 15% headroom).
|
|
1821
|
+
*/
|
|
1822
|
+
const DEFAULT_QUALITY_CONFIG = { safetyMargin: .85 };
|
|
1823
|
+
/**
|
|
1824
|
+
* Select the best video track based on current bandwidth estimate.
|
|
1729
1825
|
*
|
|
1730
|
-
*
|
|
1731
|
-
*
|
|
1732
|
-
*
|
|
1733
|
-
*
|
|
1826
|
+
* Selects the highest quality track where bandwidth is sufficient with safety margin:
|
|
1827
|
+
* - currentBandwidth >= track.bandwidth / safetyMargin
|
|
1828
|
+
* - Default safetyMargin 0.85 means track uses ≤85% of bandwidth (15% headroom)
|
|
1829
|
+
* - At same bandwidth, prefers higher resolution
|
|
1734
1830
|
*
|
|
1735
|
-
*
|
|
1736
|
-
*
|
|
1737
|
-
*
|
|
1831
|
+
* @param tracks - Available video tracks (can be unsorted)
|
|
1832
|
+
* @param currentBandwidth - Current bandwidth estimate in bits per second
|
|
1833
|
+
* @param config - Optional quality selection configuration
|
|
1834
|
+
* @returns Selected track, or undefined if no tracks available
|
|
1738
1835
|
*
|
|
1739
|
-
*
|
|
1740
|
-
*
|
|
1741
|
-
*
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
return track?.segments.find(({ startTime, duration }, i, segments) => currentTime >= startTime && (currentTime < startTime + duration || i === segments.length - 1))?.startTime;
|
|
1746
|
-
};
|
|
1747
|
-
/**
|
|
1748
|
-
* Returns true when the inputs are equal (no meaningful change — don't fire).
|
|
1749
|
-
* Returns false when the inputs differ in a way that requires a new message.
|
|
1836
|
+
* @example
|
|
1837
|
+
* const tracks = [
|
|
1838
|
+
* { id: '360p', bandwidth: 500_000, ... },
|
|
1839
|
+
* { id: '720p', bandwidth: 2_000_000, ... },
|
|
1840
|
+
* { id: '1080p', bandwidth: 4_000_000, ... },
|
|
1841
|
+
* ];
|
|
1750
1842
|
*
|
|
1751
|
-
*
|
|
1843
|
+
* // With 2.5 Mbps, selects 720p (1080p needs 4M/0.85 = 4.7 Mbps)
|
|
1844
|
+
* const selected = selectQuality(tracks, 2_500_000);
|
|
1752
1845
|
*/
|
|
1753
|
-
function
|
|
1754
|
-
if (
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
if (!prevState.playbackInitiated && curState.playbackInitiated) {
|
|
1760
|
-
if (prevState.preload !== "auto") return false;
|
|
1846
|
+
function selectQuality(tracks, currentBandwidth, config = DEFAULT_QUALITY_CONFIG) {
|
|
1847
|
+
if (tracks.length === 0) return;
|
|
1848
|
+
const sortedTracks = tracks.slice().sort((a, b) => a.bandwidth - b.bandwidth);
|
|
1849
|
+
let chosen;
|
|
1850
|
+
for (const track of sortedTracks) if (currentBandwidth >= track.bandwidth / config.safetyMargin) {
|
|
1851
|
+
if (!chosen || track.bandwidth > chosen.bandwidth || track.bandwidth === chosen.bandwidth && hasHigherResolution(track, chosen)) chosen = track;
|
|
1761
1852
|
}
|
|
1762
|
-
|
|
1763
|
-
if (prevState.track?.id !== curState.track.id && isResolvedTrack(curState.track)) return false;
|
|
1764
|
-
return segmentStartFor(prevState.currentTime, curState.track) === segmentStartFor(curState.currentTime, curState.track);
|
|
1853
|
+
return chosen ?? sortedTracks[0];
|
|
1765
1854
|
}
|
|
1766
1855
|
/**
|
|
1767
|
-
*
|
|
1768
|
-
*
|
|
1769
|
-
* Sends typed load messages to a SegmentLoaderActor when relevant conditions
|
|
1770
|
-
* change. Uses targeted subscriptions rather than broad combineLatest so only
|
|
1771
|
-
* meaningful state changes trigger evaluation.
|
|
1856
|
+
* Check if track A has higher resolution than track B.
|
|
1857
|
+
* Compares by total pixel count (width × height).
|
|
1772
1858
|
*
|
|
1773
|
-
*
|
|
1859
|
+
* @param trackA - First track to compare
|
|
1860
|
+
* @param trackB - Second track to compare
|
|
1861
|
+
* @returns True if trackA has more pixels than trackB
|
|
1862
|
+
*/
|
|
1863
|
+
function hasHigherResolution(trackA, trackB) {
|
|
1864
|
+
return (trackA.width ?? 0) * (trackA.height ?? 0) > (trackB.width ?? 0) * (trackB.height ?? 0);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
//#endregion
|
|
1868
|
+
//#region ../spf/dist/dev/core/features/quality-switching.js
|
|
1869
|
+
/**
|
|
1870
|
+
* Default quality switching configuration.
|
|
1871
|
+
*/
|
|
1872
|
+
const DEFAULT_SWITCHING_CONFIG = {
|
|
1873
|
+
safetyMargin: .85,
|
|
1874
|
+
minUpgradeInterval: 8e3,
|
|
1875
|
+
defaultBandwidth: 5e6
|
|
1876
|
+
};
|
|
1877
|
+
/**
|
|
1878
|
+
* Get all video tracks from a presentation's first switching set.
|
|
1879
|
+
* Returns [] when the presentation is still unresolved (no selectionSets yet).
|
|
1880
|
+
*/
|
|
1881
|
+
function getVideoTracks(presentation) {
|
|
1882
|
+
return (presentation.selectionSets?.find((s) => s.type === "video"))?.switchingSets[0]?.tracks ?? [];
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Quality switching orchestration (F9).
|
|
1774
1886
|
*
|
|
1775
|
-
*
|
|
1776
|
-
*
|
|
1777
|
-
* preload==='metadata' → trigger on transition to 'metadata'
|
|
1778
|
-
* preload==='auto' → trigger on transition to 'auto'
|
|
1887
|
+
* Reacts to bandwidth estimate changes and updates `selectedVideoTrackId`
|
|
1888
|
+
* when a different quality is optimal:
|
|
1779
1889
|
*
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
1782
|
-
*
|
|
1783
|
-
* let segmentStart take over post-play)
|
|
1784
|
-
* KNOWN LIMITATION: seek-before-play with
|
|
1785
|
-
* preload='auto' is not supported — if the
|
|
1786
|
-
* user seeks before pressing play, the
|
|
1787
|
-
* first re-send is delayed until the next
|
|
1788
|
-
* segment boundary crossing post-play.
|
|
1890
|
+
* - **Downgrades** happen immediately to avoid buffering stalls.
|
|
1891
|
+
* - **Upgrades** are gated by `minUpgradeInterval` to prevent oscillation.
|
|
1892
|
+
* - The first switch (from any track, or no track) is always immediate.
|
|
1789
1893
|
*
|
|
1790
|
-
*
|
|
1791
|
-
*
|
|
1792
|
-
*
|
|
1894
|
+
* Smooth switching is handled downstream: when `selectedVideoTrackId` changes,
|
|
1895
|
+
* `resolveTrack` fetches the new playlist and `loadSegments` reloads the init
|
|
1896
|
+
* segment, then appends media segments from the current position in the new
|
|
1897
|
+
* quality. The browser's SourceBuffer replaces the overlapping buffered range.
|
|
1793
1898
|
*
|
|
1794
1899
|
* @example
|
|
1795
|
-
* const cleanup =
|
|
1900
|
+
* const cleanup = switchQuality({ state });
|
|
1901
|
+
* // Later, when done:
|
|
1902
|
+
* cleanup();
|
|
1796
1903
|
*/
|
|
1797
|
-
function
|
|
1798
|
-
const
|
|
1799
|
-
const
|
|
1800
|
-
const
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
if (
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1904
|
+
function switchQuality({ state }, config = {}) {
|
|
1905
|
+
const safetyMargin = config.safetyMargin ?? DEFAULT_SWITCHING_CONFIG.safetyMargin;
|
|
1906
|
+
const minUpgradeInterval = config.minUpgradeInterval ?? DEFAULT_SWITCHING_CONFIG.minUpgradeInterval;
|
|
1907
|
+
const defaultBandwidth = config.defaultBandwidth ?? DEFAULT_SWITCHING_CONFIG.defaultBandwidth;
|
|
1908
|
+
let lastUpgradeTime = Date.now();
|
|
1909
|
+
let firstMeaningfulFire = true;
|
|
1910
|
+
return effect(() => {
|
|
1911
|
+
const { presentation, bandwidthState, selectedVideoTrackId, abrDisabled } = state.get();
|
|
1912
|
+
if (abrDisabled === true) return;
|
|
1913
|
+
if (!presentation || !bandwidthState) return;
|
|
1914
|
+
const videoTracks = getVideoTracks(presentation);
|
|
1915
|
+
if (videoTracks.length === 0) return;
|
|
1916
|
+
const isFirst = firstMeaningfulFire;
|
|
1917
|
+
firstMeaningfulFire = false;
|
|
1918
|
+
const optimal = selectQuality(videoTracks, getBandwidthEstimate(bandwidthState, defaultBandwidth), { safetyMargin });
|
|
1919
|
+
if (!optimal || optimal.id === selectedVideoTrackId) return;
|
|
1920
|
+
const currentTrack = videoTracks.find((t) => t.id === selectedVideoTrackId);
|
|
1921
|
+
if (!currentTrack || optimal.bandwidth > currentTrack.bandwidth) {
|
|
1922
|
+
const now = Date.now();
|
|
1923
|
+
if (!isFirst && now - lastUpgradeTime < minUpgradeInterval) return;
|
|
1924
|
+
lastUpgradeTime = now;
|
|
1818
1925
|
}
|
|
1819
|
-
|
|
1820
|
-
segmentLoader.current?.destroy();
|
|
1821
|
-
segmentLoader.patch(void 0);
|
|
1822
|
-
};
|
|
1926
|
+
update(state, { selectedVideoTrackId: optimal.id });
|
|
1823
1927
|
});
|
|
1824
|
-
const segmentsCanLoad = createState(false);
|
|
1825
|
-
const unsubscribeCanLoadSegments = combineLatest([state, segmentLoader]).subscribe(([currentState, currentSegmentLoader]) => {
|
|
1826
|
-
const track = getSelectedTrack(currentState, type);
|
|
1827
|
-
const trackResolved = !!track && isResolvedTrack(track);
|
|
1828
|
-
const segmentLoaderActorExists = !!currentSegmentLoader;
|
|
1829
|
-
segmentsCanLoad.patch(trackResolved && segmentLoaderActorExists);
|
|
1830
|
-
});
|
|
1831
|
-
const unsubscribeShouldLoadSegments = combineLatest([segmentsCanLoad, state]).subscribe(([segmentsCanLoad, state]) => selectLoadingInputs([segmentsCanLoad, state], type), ({ preload, playbackInitiated, currentTime, track }) => {
|
|
1832
|
-
if (!(preload === "auto" || !!playbackInitiated))
|
|
1833
|
-
/** @ts-expect-error */
|
|
1834
|
-
segmentLoader.current?.send({
|
|
1835
|
-
type: "load",
|
|
1836
|
-
track
|
|
1837
|
-
});
|
|
1838
|
-
else segmentLoader.current?.send({
|
|
1839
|
-
type: "load",
|
|
1840
|
-
track,
|
|
1841
|
-
range: {
|
|
1842
|
-
start: currentTime,
|
|
1843
|
-
end: currentTime + DEFAULT_FORWARD_BUFFER_CONFIG.bufferDuration
|
|
1844
|
-
}
|
|
1845
|
-
});
|
|
1846
|
-
}, { equalityFn: loadingInputsEq });
|
|
1847
|
-
return () => {
|
|
1848
|
-
unsubscribeCanLoadSegments();
|
|
1849
|
-
unsubscribeShouldLoadSegments();
|
|
1850
|
-
unsubActorLifecycle();
|
|
1851
|
-
};
|
|
1852
1928
|
}
|
|
1853
1929
|
|
|
1854
1930
|
//#endregion
|
|
1855
|
-
//#region ../spf/dist/dev/
|
|
1931
|
+
//#region ../spf/dist/dev/core/utils/generate-id.js
|
|
1856
1932
|
/**
|
|
1857
|
-
*
|
|
1933
|
+
* Generate unique ID for HAM objects.
|
|
1858
1934
|
*
|
|
1859
|
-
*
|
|
1860
|
-
*
|
|
1935
|
+
* Uses timestamp + random number for sufficient uniqueness.
|
|
1936
|
+
* IDs are strings without decimals.
|
|
1937
|
+
*
|
|
1938
|
+
* @returns Unique string ID in format: timestamp-random
|
|
1939
|
+
*
|
|
1940
|
+
* @example
|
|
1941
|
+
* ```ts
|
|
1942
|
+
* const id = generateId(); // "1738423156789-542891"
|
|
1943
|
+
* ```
|
|
1861
1944
|
*/
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
if (!dummyVideo) {
|
|
1865
|
-
dummyVideo = document.createElement("video");
|
|
1866
|
-
dummyVideo.muted = true;
|
|
1867
|
-
dummyVideo.preload = "none";
|
|
1868
|
-
dummyVideo.style.display = "none";
|
|
1869
|
-
dummyVideo.crossOrigin = "anonymous";
|
|
1870
|
-
}
|
|
1871
|
-
return dummyVideo;
|
|
1872
|
-
}
|
|
1873
|
-
function parseVttSegment(url) {
|
|
1874
|
-
const video = ensureDummyVideo();
|
|
1875
|
-
const track = document.createElement("track");
|
|
1876
|
-
track.kind = "subtitles";
|
|
1877
|
-
track.default = true;
|
|
1878
|
-
return new Promise((resolve, reject) => {
|
|
1879
|
-
const onLoad = () => {
|
|
1880
|
-
const cues = [];
|
|
1881
|
-
const textTrack = track.track;
|
|
1882
|
-
if (textTrack.cues) for (let i = 0; i < textTrack.cues.length; i++) {
|
|
1883
|
-
const cue = textTrack.cues[i];
|
|
1884
|
-
if (cue) cues.push(cue);
|
|
1885
|
-
}
|
|
1886
|
-
cleanup();
|
|
1887
|
-
resolve(cues);
|
|
1888
|
-
};
|
|
1889
|
-
const onError = () => {
|
|
1890
|
-
cleanup();
|
|
1891
|
-
reject(/* @__PURE__ */ new Error(`Failed to load VTT segment: ${url}`));
|
|
1892
|
-
};
|
|
1893
|
-
const cleanup = () => {
|
|
1894
|
-
track.removeEventListener("load", onLoad);
|
|
1895
|
-
track.removeEventListener("error", onError);
|
|
1896
|
-
video.removeChild(track);
|
|
1897
|
-
};
|
|
1898
|
-
track.addEventListener("load", onLoad);
|
|
1899
|
-
track.addEventListener("error", onError);
|
|
1900
|
-
video.appendChild(track);
|
|
1901
|
-
track.src = url;
|
|
1902
|
-
});
|
|
1903
|
-
}
|
|
1904
|
-
function destroyVttParser() {
|
|
1905
|
-
dummyVideo = null;
|
|
1945
|
+
function generateId() {
|
|
1946
|
+
return `${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
1906
1947
|
}
|
|
1907
1948
|
|
|
1908
1949
|
//#endregion
|
|
1909
|
-
//#region ../spf/dist/dev/
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1950
|
+
//#region ../spf/dist/dev/core/hls/parse-attributes.js
|
|
1951
|
+
/**
|
|
1952
|
+
* Parse HLS attribute list from a tag line.
|
|
1953
|
+
* Handles both quoted and unquoted values.
|
|
1954
|
+
*/
|
|
1955
|
+
function parseAttributeList(line) {
|
|
1956
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
1957
|
+
for (const match of line.matchAll(/([A-Z0-9-]+)=(?:"([^"]*)"|([^,]*))/g)) {
|
|
1958
|
+
const key = match[1];
|
|
1959
|
+
const value = match[2] ?? match[3] ?? "";
|
|
1960
|
+
if (key) attributes.set(key, value);
|
|
1916
1961
|
}
|
|
1917
|
-
return
|
|
1962
|
+
return attributes;
|
|
1918
1963
|
}
|
|
1919
|
-
const loadVttSegmentTask = async ({ segment }, context) => {
|
|
1920
|
-
const cues = await parseVttSegment(segment.url);
|
|
1921
|
-
for (const cue of cues) if (!isDuplicateCue(cue, context.textTrack)) context.textTrack.addCue(cue);
|
|
1922
|
-
};
|
|
1923
1964
|
/**
|
|
1924
|
-
*
|
|
1965
|
+
* Parse RESOLUTION attribute value (WIDTHxHEIGHT).
|
|
1925
1966
|
*/
|
|
1926
|
-
|
|
1927
|
-
const
|
|
1928
|
-
if (!
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
if (segmentsToLoad.length === 0) return;
|
|
1935
|
-
for (const segment of segmentsToLoad) {
|
|
1936
|
-
if (context.signal.aborted) break;
|
|
1937
|
-
try {
|
|
1938
|
-
await loadVttSegmentTask({ segment }, { textTrack: context.textTrack });
|
|
1939
|
-
const latest = context.state.current.textBufferState ?? {};
|
|
1940
|
-
const trackState = latest[trackId] ?? { segments: [] };
|
|
1941
|
-
context.state.patch({ textBufferState: {
|
|
1942
|
-
...latest,
|
|
1943
|
-
[trackId]: { segments: [...trackState.segments, { id: segment.id }] }
|
|
1944
|
-
} });
|
|
1945
|
-
} catch (error) {
|
|
1946
|
-
if (error instanceof Error && error.name === "AbortError") break;
|
|
1947
|
-
console.error("Failed to load VTT segment:", error);
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
if (context.textTrack.mode === "showing" && context.textTrack.cues) Array.from(context.textTrack.cues).forEach((cue) => {
|
|
1951
|
-
context.textTrack.addCue(cue);
|
|
1952
|
-
});
|
|
1953
|
-
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
1954
|
-
};
|
|
1967
|
+
function parseResolution(value) {
|
|
1968
|
+
const match = /^(\d+)x(\d+)$/.exec(value);
|
|
1969
|
+
if (!match) return null;
|
|
1970
|
+
return {
|
|
1971
|
+
width: Number.parseInt(match[1], 10),
|
|
1972
|
+
height: Number.parseInt(match[2], 10)
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1955
1975
|
/**
|
|
1956
|
-
*
|
|
1976
|
+
* Parse FRAME-RATE attribute to rational frame rate.
|
|
1957
1977
|
*/
|
|
1958
|
-
function
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
if (
|
|
1962
|
-
|
|
1978
|
+
function parseFrameRate(value) {
|
|
1979
|
+
const fps = Number.parseFloat(value);
|
|
1980
|
+
if (Number.isNaN(fps) || fps <= 0) return void 0;
|
|
1981
|
+
if (Math.abs(fps - 23.976) < .01) return {
|
|
1982
|
+
frameRateNumerator: 24e3,
|
|
1983
|
+
frameRateDenominator: 1001
|
|
1984
|
+
};
|
|
1985
|
+
if (Math.abs(fps - 29.97) < .01) return {
|
|
1986
|
+
frameRateNumerator: 3e4,
|
|
1987
|
+
frameRateDenominator: 1001
|
|
1988
|
+
};
|
|
1989
|
+
if (Math.abs(fps - 59.94) < .01) return {
|
|
1990
|
+
frameRateNumerator: 6e4,
|
|
1991
|
+
frameRateDenominator: 1001
|
|
1992
|
+
};
|
|
1993
|
+
if (fps % 1 === 0) return { frameRateNumerator: Math.round(fps) };
|
|
1994
|
+
return { frameRateNumerator: Math.round(fps) };
|
|
1963
1995
|
}
|
|
1964
1996
|
/**
|
|
1965
|
-
*
|
|
1966
|
-
*
|
|
1967
|
-
* Retrieves the live TextTrack interface from the track element in owners,
|
|
1968
|
-
* which is used for adding cues, checking mode, and managing track state.
|
|
1969
|
-
*
|
|
1970
|
-
* Note: Returns the DOM TextTrack interface (HTMLTrackElement.track),
|
|
1971
|
-
* not the presentation Track metadata type.
|
|
1972
|
-
*
|
|
1973
|
-
* @param state - Current playback state (track selection)
|
|
1974
|
-
* @param owners - DOM owners containing track elements map
|
|
1975
|
-
* @returns DOM TextTrack interface or undefined if not found
|
|
1997
|
+
* Parse CODECS attribute into separate video and audio codecs.
|
|
1976
1998
|
*/
|
|
1977
|
-
function
|
|
1978
|
-
const
|
|
1979
|
-
|
|
1980
|
-
|
|
1999
|
+
function parseCodecs(codecs) {
|
|
2000
|
+
const parts = codecs.split(",").map((s) => s.trim());
|
|
2001
|
+
const result = {};
|
|
2002
|
+
for (const codec of parts) if (codec.startsWith("avc1.") || codec.startsWith("hvc1.") || codec.startsWith("hev1.")) result.video = codec;
|
|
2003
|
+
else if (codec.startsWith("mp4a.")) result.audio = codec;
|
|
2004
|
+
return result;
|
|
1981
2005
|
}
|
|
1982
2006
|
/**
|
|
1983
|
-
*
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2007
|
+
* Parse #EXTINF duration value.
|
|
2008
|
+
*/
|
|
2009
|
+
function parseExtInfDuration(value) {
|
|
2010
|
+
const durationPart = value.split(",")[0] ?? value;
|
|
2011
|
+
const duration = Number.parseFloat(durationPart);
|
|
2012
|
+
return Number.isNaN(duration) ? 0 : duration;
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Parse BYTERANGE attribute value.
|
|
2016
|
+
* Format: "length[@offset]"
|
|
2017
|
+
* If offset is omitted, it continues from the previous byte range end.
|
|
2018
|
+
*/
|
|
2019
|
+
function parseByteRange(value, previousEnd) {
|
|
2020
|
+
const match = /^(\d+)(?:@(\d+))?$/.exec(value);
|
|
2021
|
+
if (!match) return null;
|
|
2022
|
+
const length = Number.parseInt(match[1], 10);
|
|
2023
|
+
if (Number.isNaN(length)) return null;
|
|
2024
|
+
let start;
|
|
2025
|
+
if (match[2] !== void 0) {
|
|
2026
|
+
start = Number.parseInt(match[2], 10);
|
|
2027
|
+
if (Number.isNaN(start)) return null;
|
|
2028
|
+
} else if (previousEnd !== void 0) start = previousEnd;
|
|
2029
|
+
else return null;
|
|
2030
|
+
return {
|
|
2031
|
+
start,
|
|
2032
|
+
end: start + length - 1
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Create AttributeList from raw attribute string.
|
|
2037
|
+
*/
|
|
2038
|
+
function createAttributeList(line) {
|
|
2039
|
+
const map = parseAttributeList(line);
|
|
2040
|
+
return {
|
|
2041
|
+
get(key) {
|
|
2042
|
+
return map.get(key);
|
|
2043
|
+
},
|
|
2044
|
+
getInt(key, defaultValue) {
|
|
2045
|
+
const value = map.get(key);
|
|
2046
|
+
if (value === void 0) return defaultValue;
|
|
2047
|
+
const parsed = Number.parseInt(value, 10);
|
|
2048
|
+
return Number.isNaN(parsed) ? defaultValue : parsed;
|
|
2049
|
+
},
|
|
2050
|
+
getFloat(key, defaultValue) {
|
|
2051
|
+
const value = map.get(key);
|
|
2052
|
+
if (value === void 0) return defaultValue;
|
|
2053
|
+
const parsed = Number.parseFloat(value);
|
|
2054
|
+
return Number.isNaN(parsed) ? defaultValue : parsed;
|
|
2055
|
+
},
|
|
2056
|
+
getBool(key) {
|
|
2057
|
+
return map.get(key) === "YES";
|
|
2058
|
+
},
|
|
2059
|
+
getResolution(key) {
|
|
2060
|
+
const value = map.get(key);
|
|
2061
|
+
if (!value) return void 0;
|
|
2062
|
+
return parseResolution(value) ?? void 0;
|
|
2063
|
+
},
|
|
2064
|
+
getFrameRate(key) {
|
|
2065
|
+
const value = map.get(key);
|
|
2066
|
+
if (!value) return void 0;
|
|
2067
|
+
return parseFrameRate(value);
|
|
2068
|
+
}
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* Match a tag and extract its attributes.
|
|
2073
|
+
* Returns null if the line doesn't match the tag.
|
|
1989
2074
|
*/
|
|
1990
|
-
function
|
|
1991
|
-
|
|
2075
|
+
function matchTag(line, tag) {
|
|
2076
|
+
const prefix = `#${tag}:`;
|
|
2077
|
+
if (!line.startsWith(prefix)) return null;
|
|
2078
|
+
return createAttributeList(line.slice(prefix.length));
|
|
1992
2079
|
}
|
|
2080
|
+
|
|
2081
|
+
//#endregion
|
|
2082
|
+
//#region ../spf/dist/dev/core/hls/resolve-url.js
|
|
1993
2083
|
/**
|
|
1994
|
-
*
|
|
1995
|
-
*
|
|
1996
|
-
* Only load if:
|
|
1997
|
-
* - Track is resolved (has segments)
|
|
1998
|
-
* - Track has at least one segment
|
|
1999
|
-
* - Track element exists
|
|
2084
|
+
* Resolve a potentially relative URL against a base URL using native URL API.
|
|
2000
2085
|
*/
|
|
2001
|
-
function
|
|
2002
|
-
|
|
2003
|
-
const track = findSelectedTextTrack(state);
|
|
2004
|
-
if (!track || !isResolvedTrack(track) || track.segments.length === 0) return false;
|
|
2005
|
-
if (!getSelectedTextTrackFromOwners(state, owners)) return false;
|
|
2006
|
-
return true;
|
|
2086
|
+
function resolveUrl(url, baseUrl) {
|
|
2087
|
+
return new URL(url, baseUrl).href;
|
|
2007
2088
|
}
|
|
2089
|
+
|
|
2090
|
+
//#endregion
|
|
2091
|
+
//#region ../spf/dist/dev/core/hls/parse-multivariant.js
|
|
2008
2092
|
/**
|
|
2009
|
-
*
|
|
2010
|
-
*
|
|
2011
|
-
* Triggers when:
|
|
2012
|
-
* - Text track is selected
|
|
2013
|
-
* - Track is resolved (has segments)
|
|
2014
|
-
* - Track element exists
|
|
2093
|
+
* Parse HLS multivariant playlist into a Presentation.
|
|
2015
2094
|
*
|
|
2016
|
-
*
|
|
2017
|
-
*
|
|
2018
|
-
*
|
|
2095
|
+
* Returns Presentation with partially resolved tracks (no segment information).
|
|
2096
|
+
* Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs)
|
|
2097
|
+
* but segment information is added when media playlists are fetched.
|
|
2019
2098
|
*
|
|
2020
|
-
* @
|
|
2021
|
-
*
|
|
2099
|
+
* @param text - Raw playlist text content
|
|
2100
|
+
* @param unresolved - Unresolved presentation (contains URL for base URL resolution)
|
|
2101
|
+
* @returns Presentation with partially resolved tracks (duration is undefined)
|
|
2022
2102
|
*/
|
|
2023
|
-
function
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
const
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2103
|
+
function parseMultivariantPlaylist(text, unresolved) {
|
|
2104
|
+
const baseUrl = unresolved.url;
|
|
2105
|
+
const lines = text.split(/\r?\n/);
|
|
2106
|
+
const streams = [];
|
|
2107
|
+
const audioRenditions = [];
|
|
2108
|
+
const subtitleRenditions = [];
|
|
2109
|
+
let pendingStreamInfo = null;
|
|
2110
|
+
for (const line of lines) {
|
|
2111
|
+
const trimmed = line.trim();
|
|
2112
|
+
if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
|
|
2113
|
+
if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
|
|
2114
|
+
const mediaAttrs = matchTag(trimmed, "EXT-X-MEDIA");
|
|
2115
|
+
if (mediaAttrs) {
|
|
2116
|
+
const type = mediaAttrs.get("TYPE");
|
|
2117
|
+
const groupId = mediaAttrs.get("GROUP-ID");
|
|
2118
|
+
const name = mediaAttrs.get("NAME");
|
|
2119
|
+
if (type === "AUDIO" && groupId && name) {
|
|
2120
|
+
const uri = mediaAttrs.get("URI");
|
|
2121
|
+
audioRenditions.push({
|
|
2122
|
+
groupId,
|
|
2123
|
+
name,
|
|
2124
|
+
language: mediaAttrs.get("LANGUAGE"),
|
|
2125
|
+
uri: uri ? resolveUrl(uri, baseUrl) : void 0,
|
|
2126
|
+
default: mediaAttrs.getBool("DEFAULT"),
|
|
2127
|
+
autoselect: mediaAttrs.getBool("AUTOSELECT")
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
if (type === "SUBTITLES" && groupId && name) {
|
|
2131
|
+
const uri = mediaAttrs.get("URI");
|
|
2132
|
+
if (uri) subtitleRenditions.push({
|
|
2133
|
+
groupId,
|
|
2134
|
+
name,
|
|
2135
|
+
language: mediaAttrs.get("LANGUAGE"),
|
|
2136
|
+
uri: resolveUrl(uri, baseUrl),
|
|
2137
|
+
default: mediaAttrs.getBool("DEFAULT"),
|
|
2138
|
+
autoselect: mediaAttrs.getBool("AUTOSELECT"),
|
|
2139
|
+
forced: mediaAttrs.getBool("FORCED")
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
continue;
|
|
2032
2143
|
}
|
|
2033
|
-
|
|
2034
|
-
if (
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2144
|
+
const streamInfAttrs = matchTag(trimmed, "EXT-X-STREAM-INF");
|
|
2145
|
+
if (streamInfAttrs) {
|
|
2146
|
+
pendingStreamInfo = {
|
|
2147
|
+
bandwidth: streamInfAttrs.getInt("BANDWIDTH", 0),
|
|
2148
|
+
resolution: streamInfAttrs.getResolution("RESOLUTION"),
|
|
2149
|
+
codecs: streamInfAttrs.get("CODECS"),
|
|
2150
|
+
frameRate: streamInfAttrs.getFrameRate("FRAME-RATE"),
|
|
2151
|
+
audioGroupId: streamInfAttrs.get("AUDIO")
|
|
2152
|
+
};
|
|
2153
|
+
continue;
|
|
2154
|
+
}
|
|
2155
|
+
if (!trimmed.startsWith("#") && pendingStreamInfo) {
|
|
2156
|
+
streams.push({
|
|
2157
|
+
...pendingStreamInfo,
|
|
2158
|
+
uri: resolveUrl(trimmed, baseUrl)
|
|
2159
|
+
});
|
|
2160
|
+
pendingStreamInfo = null;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
const videoStreams = [];
|
|
2164
|
+
const audioOnlyStreams = [];
|
|
2165
|
+
for (const stream of streams) {
|
|
2166
|
+
if (!stream.codecs) {
|
|
2167
|
+
videoStreams.push(stream);
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
const parsedCodecs = parseCodecs(stream.codecs);
|
|
2171
|
+
if (stream.codecs.split(",").length === 1) if (parsedCodecs.audio && !parsedCodecs.video) audioOnlyStreams.push(stream);
|
|
2172
|
+
else videoStreams.push(stream);
|
|
2173
|
+
else videoStreams.push(stream);
|
|
2174
|
+
}
|
|
2175
|
+
const videoTracks = videoStreams.map((stream) => {
|
|
2176
|
+
const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
|
|
2177
|
+
const track = {
|
|
2178
|
+
type: "video",
|
|
2179
|
+
id: generateId(),
|
|
2180
|
+
url: stream.uri,
|
|
2181
|
+
bandwidth: stream.bandwidth,
|
|
2182
|
+
mimeType: "video/mp4",
|
|
2183
|
+
codecs: []
|
|
2184
|
+
};
|
|
2185
|
+
if (stream.resolution?.width !== void 0) track.width = stream.resolution.width;
|
|
2186
|
+
if (stream.resolution?.height !== void 0) track.height = stream.resolution.height;
|
|
2187
|
+
if (codecs?.video) track.codecs = [codecs.video];
|
|
2188
|
+
if (stream.frameRate) track.frameRate = stream.frameRate;
|
|
2189
|
+
if (stream.audioGroupId) track.audioGroupId = stream.audioGroupId;
|
|
2190
|
+
return track;
|
|
2045
2191
|
});
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2192
|
+
const audioOnlyTracks = audioOnlyStreams.map((stream) => {
|
|
2193
|
+
const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0;
|
|
2194
|
+
return {
|
|
2195
|
+
type: "audio",
|
|
2196
|
+
id: generateId(),
|
|
2197
|
+
url: stream.uri,
|
|
2198
|
+
bandwidth: stream.bandwidth,
|
|
2199
|
+
mimeType: "audio/mp4",
|
|
2200
|
+
codecs: codecs?.audio ? [codecs.audio] : [],
|
|
2201
|
+
groupId: stream.audioGroupId || "default",
|
|
2202
|
+
name: "Default",
|
|
2203
|
+
sampleRate: 48e3,
|
|
2204
|
+
channels: 2
|
|
2205
|
+
};
|
|
2206
|
+
});
|
|
2207
|
+
const audioTracks = [...audioRenditions.map((rendition) => {
|
|
2208
|
+
let audioCodecs;
|
|
2209
|
+
for (const stream of streams) if (stream.audioGroupId === rendition.groupId && stream.codecs) {
|
|
2210
|
+
const codecs = parseCodecs(stream.codecs);
|
|
2211
|
+
if (codecs.audio) {
|
|
2212
|
+
audioCodecs = [codecs.audio];
|
|
2213
|
+
break;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
const track = {
|
|
2217
|
+
type: "audio",
|
|
2218
|
+
id: generateId(),
|
|
2219
|
+
url: rendition.uri ?? "",
|
|
2220
|
+
groupId: rendition.groupId,
|
|
2221
|
+
name: rendition.name,
|
|
2222
|
+
mimeType: "audio/mp4",
|
|
2223
|
+
bandwidth: 0,
|
|
2224
|
+
sampleRate: 48e3,
|
|
2225
|
+
channels: 2,
|
|
2226
|
+
codecs: []
|
|
2227
|
+
};
|
|
2228
|
+
if (rendition.language) track.language = rendition.language;
|
|
2229
|
+
if (audioCodecs) track.codecs = audioCodecs;
|
|
2230
|
+
if (rendition.default) track.default = rendition.default;
|
|
2231
|
+
if (rendition.autoselect) track.autoselect = rendition.autoselect;
|
|
2232
|
+
return track;
|
|
2233
|
+
}), ...audioOnlyTracks];
|
|
2234
|
+
const textTracks = subtitleRenditions.map((rendition) => {
|
|
2235
|
+
const track = {
|
|
2236
|
+
type: "text",
|
|
2237
|
+
id: generateId(),
|
|
2238
|
+
url: rendition.uri,
|
|
2239
|
+
groupId: rendition.groupId,
|
|
2240
|
+
label: rendition.name,
|
|
2241
|
+
kind: "subtitles",
|
|
2242
|
+
mimeType: "text/vtt",
|
|
2243
|
+
bandwidth: 0
|
|
2244
|
+
};
|
|
2245
|
+
if (rendition.language) track.language = rendition.language;
|
|
2246
|
+
if (rendition.default && rendition.autoselect) track.default = true;
|
|
2247
|
+
if (rendition.autoselect) track.autoselect = rendition.autoselect;
|
|
2248
|
+
if (rendition.forced) track.forced = rendition.forced;
|
|
2249
|
+
return track;
|
|
2250
|
+
});
|
|
2251
|
+
const selectionSets = [];
|
|
2252
|
+
if (videoTracks.length > 0) {
|
|
2253
|
+
const videoSwitchingSet = {
|
|
2254
|
+
id: generateId(),
|
|
2255
|
+
type: "video",
|
|
2256
|
+
tracks: videoTracks
|
|
2257
|
+
};
|
|
2258
|
+
const videoSelectionSet = {
|
|
2259
|
+
id: generateId(),
|
|
2260
|
+
type: "video",
|
|
2261
|
+
switchingSets: [videoSwitchingSet]
|
|
2262
|
+
};
|
|
2263
|
+
selectionSets.push(videoSelectionSet);
|
|
2264
|
+
}
|
|
2265
|
+
if (audioTracks.length > 0) {
|
|
2266
|
+
const audioSwitchingSet = {
|
|
2267
|
+
id: generateId(),
|
|
2268
|
+
type: "audio",
|
|
2269
|
+
tracks: audioTracks
|
|
2270
|
+
};
|
|
2271
|
+
const audioSelectionSet = {
|
|
2272
|
+
id: generateId(),
|
|
2273
|
+
type: "audio",
|
|
2274
|
+
switchingSets: [audioSwitchingSet]
|
|
2275
|
+
};
|
|
2276
|
+
selectionSets.push(audioSelectionSet);
|
|
2277
|
+
}
|
|
2278
|
+
if (textTracks.length > 0) {
|
|
2279
|
+
const textSwitchingSet = {
|
|
2280
|
+
id: generateId(),
|
|
2281
|
+
type: "text",
|
|
2282
|
+
tracks: textTracks
|
|
2283
|
+
};
|
|
2284
|
+
const textSelectionSet = {
|
|
2285
|
+
id: generateId(),
|
|
2286
|
+
type: "text",
|
|
2287
|
+
switchingSets: [textSwitchingSet]
|
|
2288
|
+
};
|
|
2289
|
+
selectionSets.push(textSelectionSet);
|
|
2290
|
+
}
|
|
2291
|
+
return {
|
|
2292
|
+
id: generateId(),
|
|
2293
|
+
url: unresolved.url,
|
|
2294
|
+
startTime: 0,
|
|
2295
|
+
selectionSets
|
|
2049
2296
|
};
|
|
2050
2297
|
}
|
|
2051
2298
|
|
|
2052
2299
|
//#endregion
|
|
2053
|
-
//#region ../spf/dist/dev/
|
|
2300
|
+
//#region ../spf/dist/dev/core/features/resolve-presentation.js
|
|
2054
2301
|
/**
|
|
2055
|
-
*
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2302
|
+
* Type guard to check if presentation is unresolved.
|
|
2303
|
+
*/
|
|
2304
|
+
function isUnresolved(presentation) {
|
|
2305
|
+
return presentation !== void 0 && "url" in presentation && !("id" in presentation);
|
|
2306
|
+
}
|
|
2307
|
+
function canResolve$1(state) {
|
|
2308
|
+
return isUnresolved(state.presentation);
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Determines if resolution conditions are met based on preload policy and playback state.
|
|
2063
2312
|
*
|
|
2064
|
-
*
|
|
2313
|
+
* Resolution conditions:
|
|
2314
|
+
* - State-driven: preload is 'auto' or 'metadata'
|
|
2315
|
+
* - Playback-driven: playbackInitiated is true
|
|
2065
2316
|
*
|
|
2066
|
-
* @
|
|
2067
|
-
*
|
|
2317
|
+
* @param state - Current presentation state
|
|
2318
|
+
* @returns true if resolution conditions are met
|
|
2068
2319
|
*/
|
|
2069
|
-
function
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
const unsubscribe = owners.subscribe((currentOwners) => {
|
|
2073
|
-
const { mediaElement } = currentOwners;
|
|
2074
|
-
if (mediaElement === lastMediaElement) return;
|
|
2075
|
-
removeListeners?.();
|
|
2076
|
-
removeListeners = null;
|
|
2077
|
-
lastMediaElement = mediaElement;
|
|
2078
|
-
if (!mediaElement) return;
|
|
2079
|
-
state.patch({ currentTime: mediaElement.currentTime });
|
|
2080
|
-
const sync = () => state.patch({ currentTime: mediaElement.currentTime });
|
|
2081
|
-
const removeTimeupdate = listen(mediaElement, "timeupdate", sync);
|
|
2082
|
-
const removeSeeking = listen(mediaElement, "seeking", sync);
|
|
2083
|
-
removeListeners = () => {
|
|
2084
|
-
removeTimeupdate();
|
|
2085
|
-
removeSeeking();
|
|
2086
|
-
};
|
|
2087
|
-
});
|
|
2088
|
-
return () => {
|
|
2089
|
-
removeListeners?.();
|
|
2090
|
-
unsubscribe();
|
|
2091
|
-
};
|
|
2320
|
+
function shouldResolve(state) {
|
|
2321
|
+
const { preload, playbackInitiated } = state;
|
|
2322
|
+
return ["auto", "metadata"].includes(preload) || !!playbackInitiated;
|
|
2092
2323
|
}
|
|
2093
|
-
|
|
2094
|
-
//#endregion
|
|
2095
|
-
//#region ../spf/dist/dev/dom/features/track-playback-initiated.js
|
|
2096
2324
|
/**
|
|
2097
|
-
*
|
|
2325
|
+
* Resolves unresolved presentations using reactive composition.
|
|
2326
|
+
*
|
|
2327
|
+
* Triggers resolution when:
|
|
2328
|
+
* - State-driven: Unresolved presentation + preload allows (auto/metadata)
|
|
2329
|
+
* - Playback-driven: playbackInitiated is true
|
|
2098
2330
|
*
|
|
2099
|
-
*
|
|
2100
|
-
*
|
|
2101
|
-
*
|
|
2102
|
-
* can react.
|
|
2331
|
+
* @example
|
|
2332
|
+
* ```ts
|
|
2333
|
+
* const state = signal({ presentation: undefined, preload: 'auto', playbackInitiated: false });
|
|
2103
2334
|
*
|
|
2104
|
-
*
|
|
2105
|
-
* so a new source with `preload="none"` won't load segments until play is
|
|
2106
|
-
* triggered again.
|
|
2335
|
+
* const cleanup = resolvePresentation({ state });
|
|
2107
2336
|
*
|
|
2108
|
-
*
|
|
2109
|
-
*
|
|
2110
|
-
* is a startup hint, not a runtime gate.
|
|
2337
|
+
* // State-driven: resolves immediately when preload allows
|
|
2338
|
+
* state.set({ ...state.get(), presentation: { url: 'http://example.com/playlist.m3u8' } });
|
|
2111
2339
|
*
|
|
2112
|
-
*
|
|
2113
|
-
*
|
|
2340
|
+
* // Playback-driven: resolves when playbackInitiated is set
|
|
2341
|
+
* state.set({ ...state.get(), preload: 'none', presentation: { url: '...' }, playbackInitiated: true });
|
|
2342
|
+
* ```
|
|
2114
2343
|
*/
|
|
2115
|
-
function
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
let
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
if (
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
removeListener = listen(mediaElement, "play", () => {
|
|
2134
|
-
state.patch({ playbackInitiated: true });
|
|
2135
|
-
events.dispatch({ type: "play" });
|
|
2344
|
+
function resolvePresentation({ state }) {
|
|
2345
|
+
const canResolveSignal = computed(() => canResolve$1(state.get()));
|
|
2346
|
+
const shouldResolveSignal = computed(() => shouldResolve(state.get()));
|
|
2347
|
+
let resolving = false;
|
|
2348
|
+
let abortController = null;
|
|
2349
|
+
const cleanupEffect = effect(() => {
|
|
2350
|
+
if (!canResolveSignal.get() || !shouldResolveSignal.get() || resolving) return;
|
|
2351
|
+
const presentation = state.get().presentation;
|
|
2352
|
+
resolving = true;
|
|
2353
|
+
abortController = new AbortController();
|
|
2354
|
+
fetchResolvable(presentation, { signal: abortController.signal }).then((response) => getResponseText(response)).then((text) => {
|
|
2355
|
+
update(state, { presentation: parseMultivariantPlaylist(text, presentation) });
|
|
2356
|
+
}).catch((error) => {
|
|
2357
|
+
if (error instanceof Error && error.name === "AbortError") return;
|
|
2358
|
+
throw error;
|
|
2359
|
+
}).finally(() => {
|
|
2360
|
+
resolving = false;
|
|
2361
|
+
abortController = null;
|
|
2136
2362
|
});
|
|
2137
2363
|
});
|
|
2138
2364
|
return () => {
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
unsubscribeOwners();
|
|
2365
|
+
abortController?.abort();
|
|
2366
|
+
cleanupEffect();
|
|
2142
2367
|
};
|
|
2143
2368
|
}
|
|
2144
2369
|
|
|
2145
2370
|
//#endregion
|
|
2146
|
-
//#region ../spf/dist/dev/
|
|
2371
|
+
//#region ../spf/dist/dev/core/hls/parse-media-playlist.js
|
|
2147
2372
|
/**
|
|
2148
|
-
*
|
|
2373
|
+
* Parse HLS media playlist and resolve track with segments.
|
|
2149
2374
|
*
|
|
2150
|
-
*
|
|
2151
|
-
*
|
|
2152
|
-
* between each call so appends are serialized correctly.
|
|
2375
|
+
* Takes an unresolved track (from multivariant playlist) and media playlist text,
|
|
2376
|
+
* returns a HAM-compliant resolved track with segments.
|
|
2153
2377
|
*
|
|
2154
|
-
*
|
|
2155
|
-
*
|
|
2378
|
+
* @param text - Media playlist text content
|
|
2379
|
+
* @param unresolved - Unresolved track from parseMultivariantPlaylist
|
|
2380
|
+
* @returns Resolved track with segments (type inferred from input)
|
|
2156
2381
|
*/
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
cleanup();
|
|
2182
|
-
resolve();
|
|
2183
|
-
};
|
|
2184
|
-
const onError = (event) => {
|
|
2185
|
-
cleanup();
|
|
2186
|
-
reject(/* @__PURE__ */ new Error(`SourceBuffer append error: ${event.type}`));
|
|
2187
|
-
};
|
|
2188
|
-
const cleanup = () => {
|
|
2189
|
-
sourceBuffer.removeEventListener("updateend", onUpdateEnd);
|
|
2190
|
-
sourceBuffer.removeEventListener("error", onError);
|
|
2191
|
-
};
|
|
2192
|
-
sourceBuffer.addEventListener("updateend", onUpdateEnd);
|
|
2193
|
-
sourceBuffer.addEventListener("error", onError);
|
|
2194
|
-
try {
|
|
2195
|
-
sourceBuffer.appendBuffer(data);
|
|
2196
|
-
} catch (error) {
|
|
2197
|
-
cleanup();
|
|
2198
|
-
reject(error);
|
|
2382
|
+
function parseMediaPlaylist(text, unresolved) {
|
|
2383
|
+
const lines = text.split(/\r?\n/);
|
|
2384
|
+
const baseUrl = unresolved.url;
|
|
2385
|
+
const segments = [];
|
|
2386
|
+
let initSegmentUrl;
|
|
2387
|
+
let initSegmentByteRange;
|
|
2388
|
+
let currentDuration = 0;
|
|
2389
|
+
let currentByteRange;
|
|
2390
|
+
let currentTime = 0;
|
|
2391
|
+
let segmentIndex = 0;
|
|
2392
|
+
let previousByteRangeEnd;
|
|
2393
|
+
for (const line of lines) {
|
|
2394
|
+
const trimmed = line.trim();
|
|
2395
|
+
if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue;
|
|
2396
|
+
if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-TARGETDURATION:") || trimmed.startsWith("#EXT-X-PLAYLIST-TYPE:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue;
|
|
2397
|
+
const mapAttrs = matchTag(trimmed, "EXT-X-MAP");
|
|
2398
|
+
if (mapAttrs) {
|
|
2399
|
+
const uri = mapAttrs.get("URI");
|
|
2400
|
+
if (uri) {
|
|
2401
|
+
initSegmentUrl = resolveUrl(uri, baseUrl);
|
|
2402
|
+
const byteRangeStr = mapAttrs.get("BYTERANGE");
|
|
2403
|
+
if (byteRangeStr) initSegmentByteRange = parseByteRange(byteRangeStr, 0) ?? void 0;
|
|
2404
|
+
}
|
|
2405
|
+
continue;
|
|
2199
2406
|
}
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
//#endregion
|
|
2204
|
-
//#region ../spf/dist/dev/dom/media/buffer-flusher.js
|
|
2205
|
-
/**
|
|
2206
|
-
* Buffer flusher helper (P12)
|
|
2207
|
-
*
|
|
2208
|
-
* Removes a time range from a SourceBuffer to manage memory.
|
|
2209
|
-
*/
|
|
2210
|
-
/**
|
|
2211
|
-
* Remove a time range from a SourceBuffer.
|
|
2212
|
-
*
|
|
2213
|
-
* Waits for the SourceBuffer to be ready (not updating), then removes
|
|
2214
|
-
* the specified range. Returns a promise that resolves when removal completes.
|
|
2215
|
-
*
|
|
2216
|
-
* @param sourceBuffer - The SourceBuffer to remove data from
|
|
2217
|
-
* @param start - Start of the time range to remove (seconds)
|
|
2218
|
-
* @param end - End of the time range to remove (seconds)
|
|
2219
|
-
* @returns Promise that resolves when removal completes
|
|
2220
|
-
*
|
|
2221
|
-
* @example
|
|
2222
|
-
* await flushBuffer(videoSourceBuffer, 0, 30);
|
|
2223
|
-
*/
|
|
2224
|
-
async function flushBuffer(sourceBuffer, start, end) {
|
|
2225
|
-
if (sourceBuffer.updating) await new Promise((resolve) => {
|
|
2226
|
-
const onUpdateEnd = () => {
|
|
2227
|
-
sourceBuffer.removeEventListener("updateend", onUpdateEnd);
|
|
2228
|
-
resolve();
|
|
2229
|
-
};
|
|
2230
|
-
sourceBuffer.addEventListener("updateend", onUpdateEnd);
|
|
2231
|
-
});
|
|
2232
|
-
return new Promise((resolve, reject) => {
|
|
2233
|
-
const onUpdateEnd = () => {
|
|
2234
|
-
cleanup();
|
|
2235
|
-
resolve();
|
|
2236
|
-
};
|
|
2237
|
-
const onError = (event) => {
|
|
2238
|
-
cleanup();
|
|
2239
|
-
reject(/* @__PURE__ */ new Error(`SourceBuffer remove error: ${event.type}`));
|
|
2240
|
-
};
|
|
2241
|
-
const cleanup = () => {
|
|
2242
|
-
sourceBuffer.removeEventListener("updateend", onUpdateEnd);
|
|
2243
|
-
sourceBuffer.removeEventListener("error", onError);
|
|
2244
|
-
};
|
|
2245
|
-
sourceBuffer.addEventListener("updateend", onUpdateEnd);
|
|
2246
|
-
sourceBuffer.addEventListener("error", onError);
|
|
2247
|
-
try {
|
|
2248
|
-
sourceBuffer.remove(start, end);
|
|
2249
|
-
} catch (error) {
|
|
2250
|
-
cleanup();
|
|
2251
|
-
reject(error);
|
|
2407
|
+
if (trimmed.startsWith("#EXTINF:")) {
|
|
2408
|
+
currentDuration = parseExtInfDuration(trimmed.slice(8));
|
|
2409
|
+
continue;
|
|
2252
2410
|
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
function calculatePresentationDuration({ state }) {
|
|
2289
|
-
return combineLatest([state]).subscribe(([currentState]) => {
|
|
2290
|
-
if (!shouldCalculateDuration(currentState)) return;
|
|
2291
|
-
const duration = getDurationFromResolvedTracks(currentState);
|
|
2292
|
-
if (duration === void 0 || !Number.isFinite(duration)) return;
|
|
2293
|
-
const { presentation } = currentState;
|
|
2294
|
-
state.patch({ presentation: {
|
|
2295
|
-
...presentation,
|
|
2296
|
-
duration
|
|
2297
|
-
} });
|
|
2298
|
-
});
|
|
2411
|
+
if (trimmed.startsWith("#EXT-X-BYTERANGE:")) {
|
|
2412
|
+
currentByteRange = parseByteRange(trimmed.slice(17), previousByteRangeEnd) ?? void 0;
|
|
2413
|
+
continue;
|
|
2414
|
+
}
|
|
2415
|
+
if (trimmed === "#EXT-X-ENDLIST") continue;
|
|
2416
|
+
if (!trimmed.startsWith("#") && currentDuration > 0) {
|
|
2417
|
+
const segment = {
|
|
2418
|
+
id: `segment-${segmentIndex}`,
|
|
2419
|
+
url: resolveUrl(trimmed, baseUrl),
|
|
2420
|
+
duration: currentDuration,
|
|
2421
|
+
startTime: currentTime
|
|
2422
|
+
};
|
|
2423
|
+
if (currentByteRange) {
|
|
2424
|
+
segment.byteRange = currentByteRange;
|
|
2425
|
+
previousByteRangeEnd = currentByteRange.end + 1;
|
|
2426
|
+
} else previousByteRangeEnd = void 0;
|
|
2427
|
+
segments.push(segment);
|
|
2428
|
+
currentTime += currentDuration;
|
|
2429
|
+
segmentIndex++;
|
|
2430
|
+
currentDuration = 0;
|
|
2431
|
+
currentByteRange = void 0;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
const totalDuration = currentTime;
|
|
2435
|
+
const initialization = unresolved.type === "text" && !initSegmentUrl ? void 0 : initSegmentUrl ? {
|
|
2436
|
+
url: initSegmentUrl,
|
|
2437
|
+
...initSegmentByteRange ? { byteRange: initSegmentByteRange } : {}
|
|
2438
|
+
} : { url: "" };
|
|
2439
|
+
return {
|
|
2440
|
+
...unresolved,
|
|
2441
|
+
startTime: 0,
|
|
2442
|
+
duration: totalDuration,
|
|
2443
|
+
segments,
|
|
2444
|
+
initialization
|
|
2445
|
+
};
|
|
2299
2446
|
}
|
|
2300
2447
|
|
|
2301
2448
|
//#endregion
|
|
@@ -2324,7 +2471,7 @@ var Task = class {
|
|
|
2324
2471
|
this.#runFn = runFn;
|
|
2325
2472
|
const rawId = config?.id;
|
|
2326
2473
|
this.id = typeof rawId === "function" ? rawId() : rawId ?? generateId();
|
|
2327
|
-
this.#signal = config?.signal ?
|
|
2474
|
+
this.#signal = config?.signal ? anyAbortSignal([this.#abortController.signal, config.signal]) : this.#abortController.signal;
|
|
2328
2475
|
}
|
|
2329
2476
|
get status() {
|
|
2330
2477
|
return this.#status;
|
|
@@ -2424,19 +2571,6 @@ function canResolve(state, config) {
|
|
|
2424
2571
|
return !isResolvedTrack(track);
|
|
2425
2572
|
}
|
|
2426
2573
|
/**
|
|
2427
|
-
* Determines if track resolution conditions are met.
|
|
2428
|
-
*
|
|
2429
|
-
* Currently always returns true - conditions are checked by canResolveTrack()
|
|
2430
|
-
* and resolving flag. Kept as placeholder for future conditional logic.
|
|
2431
|
-
*
|
|
2432
|
-
* @param state - Current track resolution state
|
|
2433
|
-
* @param event - Current action/event
|
|
2434
|
-
* @returns true (conditions checked elsewhere)
|
|
2435
|
-
*/
|
|
2436
|
-
function shouldResolve(_state, _event) {
|
|
2437
|
-
return true;
|
|
2438
|
-
}
|
|
2439
|
-
/**
|
|
2440
2574
|
* Updates a track within a presentation (immutably).
|
|
2441
2575
|
* Generic - works for video, audio, or text tracks.
|
|
2442
2576
|
*/
|
|
@@ -2456,26 +2590,29 @@ function updateTrackInPresentation(presentation, resolvedTrack) {
|
|
|
2456
2590
|
/**
|
|
2457
2591
|
* Resolves unresolved tracks using reactive composition.
|
|
2458
2592
|
*
|
|
2459
|
-
*
|
|
2460
|
-
*
|
|
2461
|
-
*
|
|
2462
|
-
* cleanup.
|
|
2593
|
+
* Reacts to state changes and schedules fetch tasks via ConcurrentRunner when
|
|
2594
|
+
* a selected track is unresolved. The ConcurrentRunner handles deduplication,
|
|
2595
|
+
* parallel execution, and cleanup.
|
|
2463
2596
|
*
|
|
2464
2597
|
* Generic version that works for video, audio, or text tracks based on config.
|
|
2465
2598
|
* Type parameter T is inferred from config.type (use 'as const' for inference).
|
|
2466
2599
|
*/
|
|
2467
|
-
function resolveTrack({ state
|
|
2600
|
+
function resolveTrack({ state }, config) {
|
|
2468
2601
|
const runner = new ConcurrentRunner();
|
|
2469
|
-
const cleanup =
|
|
2470
|
-
|
|
2602
|
+
const cleanup = effect(() => {
|
|
2603
|
+
const currentState = state.get();
|
|
2604
|
+
if (!canResolve(currentState, config)) return;
|
|
2471
2605
|
const track = getSelectedTrack(currentState, config.type);
|
|
2472
2606
|
if (!track) return;
|
|
2473
2607
|
const resolvedTrack = track;
|
|
2474
2608
|
runner.schedule(new Task(async (signal) => {
|
|
2475
2609
|
const mediaTrack = parseMediaPlaylist(await getResponseText(await fetchResolvable(resolvedTrack, { signal })), resolvedTrack);
|
|
2476
|
-
const
|
|
2477
|
-
const updatedPresentation = updateTrackInPresentation(
|
|
2478
|
-
state.
|
|
2610
|
+
const latest = state.get();
|
|
2611
|
+
const updatedPresentation = updateTrackInPresentation(latest.presentation, mediaTrack);
|
|
2612
|
+
state.set({
|
|
2613
|
+
...latest,
|
|
2614
|
+
presentation: updatedPresentation
|
|
2615
|
+
});
|
|
2479
2616
|
}, { id: track.id }));
|
|
2480
2617
|
});
|
|
2481
2618
|
return () => {
|
|
@@ -2558,19 +2695,11 @@ function shouldSelectTrack(state, config) {
|
|
|
2558
2695
|
* );
|
|
2559
2696
|
*/
|
|
2560
2697
|
function selectVideoTrack({ state }, config = { type: "video" }) {
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === config.type)?.switchingSets[0]?.tracks[0]?.id;
|
|
2567
|
-
if (selectedTrackId) {
|
|
2568
|
-
const selectedTrackKey = SelectedTrackIdKeyByType[config.type];
|
|
2569
|
-
state.patch({ [selectedTrackKey]: selectedTrackId });
|
|
2570
|
-
}
|
|
2571
|
-
} finally {
|
|
2572
|
-
selecting = false;
|
|
2573
|
-
}
|
|
2698
|
+
return effect(() => {
|
|
2699
|
+
const currentState = state.get();
|
|
2700
|
+
if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
|
|
2701
|
+
const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === config.type)?.switchingSets[0]?.tracks[0]?.id;
|
|
2702
|
+
if (selectedTrackId) update(state, { [SelectedTrackIdKeyByType[config.type]]: selectedTrackId });
|
|
2574
2703
|
});
|
|
2575
2704
|
}
|
|
2576
2705
|
/**
|
|
@@ -2589,16 +2718,11 @@ function selectVideoTrack({ state }, config = { type: "video" }) {
|
|
|
2589
2718
|
* );
|
|
2590
2719
|
*/
|
|
2591
2720
|
function selectAudioTrack({ state }, config = { type: "audio" }) {
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === "audio")?.switchingSets[0]?.tracks[0]?.id;
|
|
2598
|
-
if (selectedTrackId) state.patch({ selectedAudioTrackId: selectedTrackId });
|
|
2599
|
-
} finally {
|
|
2600
|
-
selecting = false;
|
|
2601
|
-
}
|
|
2721
|
+
return effect(() => {
|
|
2722
|
+
const currentState = state.get();
|
|
2723
|
+
if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
|
|
2724
|
+
const selectedTrackId = currentState.presentation?.selectionSets.find(({ type }) => type === "audio")?.switchingSets[0]?.tracks[0]?.id;
|
|
2725
|
+
if (selectedTrackId) update(state, { selectedAudioTrackId: selectedTrackId });
|
|
2602
2726
|
});
|
|
2603
2727
|
}
|
|
2604
2728
|
/**
|
|
@@ -2614,16 +2738,33 @@ function selectAudioTrack({ state }, config = { type: "audio" }) {
|
|
|
2614
2738
|
* const cleanup = selectTextTrack({ state, owners, events }, {});
|
|
2615
2739
|
*/
|
|
2616
2740
|
function selectTextTrack({ state }, config = { type: "text" }) {
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2741
|
+
return effect(() => {
|
|
2742
|
+
const currentState = state.get();
|
|
2743
|
+
if (!canSelectTrack(currentState, config) || !shouldSelectTrack(currentState, config)) return;
|
|
2744
|
+
const selectedTextTrackId = pickTextTrack(currentState.presentation, config);
|
|
2745
|
+
if (selectedTextTrackId) update(state, { selectedTextTrackId });
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
//#endregion
|
|
2750
|
+
//#region ../spf/dist/dev/core/features/sync-preload-attribute.js
|
|
2751
|
+
/**
|
|
2752
|
+
* Syncs preload attribute from mediaElement to state.
|
|
2753
|
+
*
|
|
2754
|
+
* Watches the owners signal for mediaElement changes and copies the
|
|
2755
|
+
* preload attribute to state when no explicit value has been set.
|
|
2756
|
+
* An explicit value (set via SpfMedia.preload) always wins.
|
|
2757
|
+
*
|
|
2758
|
+
* @example
|
|
2759
|
+
* const cleanup = syncPreloadAttribute({ state, owners });
|
|
2760
|
+
*/
|
|
2761
|
+
function syncPreloadAttribute({ state, owners }) {
|
|
2762
|
+
const mediaElement = computed(() => owners.get().mediaElement);
|
|
2763
|
+
return effect(() => {
|
|
2764
|
+
if (state.get().preload !== void 0) return;
|
|
2765
|
+
const preload = mediaElement.get()?.preload || void 0;
|
|
2766
|
+
if (preload === void 0) return;
|
|
2767
|
+
update(state, { preload });
|
|
2627
2768
|
});
|
|
2628
2769
|
}
|
|
2629
2770
|
|
|
@@ -2640,7 +2781,7 @@ function isLastSegmentAppended(segments, actor) {
|
|
|
2640
2781
|
if (segments.length === 0) return true;
|
|
2641
2782
|
const lastSeg = segments[segments.length - 1];
|
|
2642
2783
|
if (!lastSeg) return false;
|
|
2643
|
-
return actor?.snapshot.context.segments.some((s) => s.id === lastSeg.id && !s.partial) ?? false;
|
|
2784
|
+
return actor?.snapshot.get().context.segments.some((s) => s.id === lastSeg.id && !s.partial) ?? false;
|
|
2644
2785
|
}
|
|
2645
2786
|
/**
|
|
2646
2787
|
* Check if the last segment has been appended for each selected track.
|
|
@@ -2672,15 +2813,15 @@ function canEndStream(state, owners) {
|
|
|
2672
2813
|
*/
|
|
2673
2814
|
function shouldEndStream(state, owners) {
|
|
2674
2815
|
if (!canEndStream(state, owners)) return false;
|
|
2675
|
-
const {
|
|
2676
|
-
if (mediaSource
|
|
2816
|
+
const { mediaElement } = owners;
|
|
2817
|
+
if ((owners.mediaSourceReadyState?.get() ?? owners.mediaSource?.readyState) !== "open") return false;
|
|
2677
2818
|
if (mediaElement && mediaElement.readyState < HTMLMediaElement.HAVE_METADATA) return false;
|
|
2678
2819
|
const hasVideoTrack = !!state.selectedVideoTrackId;
|
|
2679
2820
|
const hasAudioTrack = !!state.selectedAudioTrackId;
|
|
2680
2821
|
if (hasVideoTrack && !owners.videoBuffer) return false;
|
|
2681
2822
|
if (hasAudioTrack && !owners.audioBuffer) return false;
|
|
2682
|
-
if (owners.videoBufferActor?.snapshot.status === "updating") return false;
|
|
2683
|
-
if (owners.audioBufferActor?.snapshot.status === "updating") return false;
|
|
2823
|
+
if (owners.videoBufferActor?.snapshot.get().status === "updating") return false;
|
|
2824
|
+
if (owners.audioBufferActor?.snapshot.get().status === "updating") return false;
|
|
2684
2825
|
if (!hasLastSegmentLoaded(state, owners)) return false;
|
|
2685
2826
|
if (mediaElement) {
|
|
2686
2827
|
const videoTrack = hasVideoTrack ? getSelectedTrack(state, "video") : void 0;
|
|
@@ -2699,13 +2840,18 @@ function shouldEndStream(state, owners) {
|
|
|
2699
2840
|
* aligned with the same abstraction that owns all buffer operations.
|
|
2700
2841
|
*/
|
|
2701
2842
|
function waitForSourceBuffersReady$1(owners) {
|
|
2702
|
-
const updatingActors = [owners.videoBufferActor, owners.audioBufferActor].filter((actor) => actor !== void 0 && actor.snapshot.status === "updating");
|
|
2843
|
+
const updatingActors = [owners.videoBufferActor, owners.audioBufferActor].filter((actor) => actor !== void 0 && actor.snapshot.get().status === "updating");
|
|
2703
2844
|
if (updatingActors.length === 0) return Promise.resolve();
|
|
2704
2845
|
return Promise.all(updatingActors.map((actor) => new Promise((resolve) => {
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2846
|
+
let cleanup;
|
|
2847
|
+
let resolved = false;
|
|
2848
|
+
cleanup = effect(() => {
|
|
2849
|
+
if (actor.snapshot.get().status !== "updating") {
|
|
2850
|
+
if (!resolved) {
|
|
2851
|
+
resolved = true;
|
|
2852
|
+
resolve();
|
|
2853
|
+
}
|
|
2854
|
+
queueMicrotask(() => cleanup?.());
|
|
2709
2855
|
}
|
|
2710
2856
|
});
|
|
2711
2857
|
}))).then(() => void 0);
|
|
@@ -2747,64 +2893,165 @@ const endOfStreamTask = async ({ currentOwners }, _context) => {
|
|
|
2747
2893
|
* and MediaSource.duration updates.
|
|
2748
2894
|
*/
|
|
2749
2895
|
function endOfStream({ state, owners }) {
|
|
2896
|
+
const shouldEnd = computed(() => shouldEndStream(state.get(), owners.get()));
|
|
2750
2897
|
let hasEnded = false;
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
if (destroyed) return;
|
|
2755
|
-
const currentState = state.current;
|
|
2756
|
-
const currentOwners = owners.current;
|
|
2898
|
+
return effect(() => {
|
|
2899
|
+
if (!shouldEnd.get()) return;
|
|
2900
|
+
const currentOwners = owners.get();
|
|
2757
2901
|
if (hasEnded) {
|
|
2758
|
-
if (currentOwners.mediaSource?.readyState !== "open") return;
|
|
2902
|
+
if ((currentOwners.mediaSourceReadyState?.get() ?? currentOwners.mediaSource?.readyState) !== "open") return;
|
|
2759
2903
|
hasEnded = false;
|
|
2760
2904
|
}
|
|
2761
|
-
if (!shouldEndStream(currentState, currentOwners)) return;
|
|
2762
2905
|
hasEnded = true;
|
|
2763
|
-
|
|
2764
|
-
await endOfStreamTask({ currentOwners }, {});
|
|
2765
|
-
} catch (error) {
|
|
2766
|
-
console.error("Failed to call endOfStream:", error);
|
|
2767
|
-
}
|
|
2768
|
-
};
|
|
2769
|
-
const cleanupOwners = owners.subscribe((currentOwners) => {
|
|
2770
|
-
activeActorUnsubs.forEach((u) => u());
|
|
2771
|
-
activeActorUnsubs.length = 0;
|
|
2772
|
-
for (const actor of [currentOwners.videoBufferActor, currentOwners.audioBufferActor]) {
|
|
2773
|
-
if (!actor) continue;
|
|
2774
|
-
let isFirst = true;
|
|
2775
|
-
activeActorUnsubs.push(actor.subscribe(() => {
|
|
2776
|
-
if (isFirst) {
|
|
2777
|
-
isFirst = false;
|
|
2778
|
-
return;
|
|
2779
|
-
}
|
|
2780
|
-
runEvaluate();
|
|
2781
|
-
}));
|
|
2782
|
-
}
|
|
2906
|
+
endOfStreamTask({ currentOwners }, {}).catch((error) => console.error("Failed to call endOfStream:", error));
|
|
2783
2907
|
});
|
|
2784
|
-
const cleanupCombineLatest = combineLatest([state, owners]).subscribe(async () => runEvaluate());
|
|
2785
|
-
return () => {
|
|
2786
|
-
destroyed = true;
|
|
2787
|
-
activeActorUnsubs.forEach((u) => u());
|
|
2788
|
-
cleanupOwners();
|
|
2789
|
-
cleanupCombineLatest();
|
|
2790
|
-
};
|
|
2791
2908
|
}
|
|
2792
2909
|
|
|
2793
2910
|
//#endregion
|
|
2794
|
-
//#region ../spf/dist/dev/dom/
|
|
2911
|
+
//#region ../spf/dist/dev/dom/media/mediasource-setup.js
|
|
2912
|
+
/**
|
|
2913
|
+
* MediaSource Setup
|
|
2914
|
+
*
|
|
2915
|
+
* Utilities for creating and configuring MediaSource/ManagedMediaSource
|
|
2916
|
+
* for MSE (Media Source Extensions) playback.
|
|
2917
|
+
*
|
|
2918
|
+
* Global ManagedMediaSource types are defined in ./mediasource.d.ts
|
|
2919
|
+
*/
|
|
2920
|
+
/**
|
|
2921
|
+
* Check if MediaSource API is supported.
|
|
2922
|
+
*/
|
|
2923
|
+
function supportsMediaSource() {
|
|
2924
|
+
return typeof MediaSource !== "undefined";
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* Check if ManagedMediaSource API is supported.
|
|
2928
|
+
* ManagedMediaSource is a newer Safari API with better lifecycle management.
|
|
2929
|
+
*/
|
|
2930
|
+
function supportsManagedMediaSource() {
|
|
2931
|
+
return typeof ManagedMediaSource !== "undefined";
|
|
2932
|
+
}
|
|
2933
|
+
/**
|
|
2934
|
+
* Create a MediaSource or ManagedMediaSource instance.
|
|
2935
|
+
*
|
|
2936
|
+
* @param options - Creation options
|
|
2937
|
+
* @returns A MediaSource or ManagedMediaSource instance
|
|
2938
|
+
* @throws Error if no MediaSource API is available
|
|
2939
|
+
*
|
|
2940
|
+
* @example
|
|
2941
|
+
* const mediaSource = createMediaSource();
|
|
2942
|
+
* const mediaElement = document.querySelector('video');
|
|
2943
|
+
* attachMediaSource(mediaSource, mediaElement);
|
|
2944
|
+
*/
|
|
2945
|
+
function createMediaSource(options = {}) {
|
|
2946
|
+
const { preferManaged = false } = options;
|
|
2947
|
+
if (preferManaged && supportsManagedMediaSource()) return new ManagedMediaSource();
|
|
2948
|
+
if (supportsMediaSource()) return new MediaSource();
|
|
2949
|
+
throw new Error("MediaSource API is not supported");
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* Attach a MediaSource to an HTMLMediaElement.
|
|
2953
|
+
*
|
|
2954
|
+
* Uses srcObject for ManagedMediaSource (Safari), or createObjectURL for regular MediaSource.
|
|
2955
|
+
*
|
|
2956
|
+
* @param mediaSource - The MediaSource to attach
|
|
2957
|
+
* @param mediaElement - The media element to attach to
|
|
2958
|
+
* @returns Object with URL and detach function
|
|
2959
|
+
*
|
|
2960
|
+
* @example
|
|
2961
|
+
* const mediaSource = createMediaSource();
|
|
2962
|
+
* const { detach } = attachMediaSource(mediaSource, videoElement);
|
|
2963
|
+
* // Use mediaSource...
|
|
2964
|
+
* // Later, to clean up:
|
|
2965
|
+
* detach();
|
|
2966
|
+
*/
|
|
2967
|
+
function attachMediaSource(mediaSource, mediaElement) {
|
|
2968
|
+
if (supportsManagedMediaSource() && mediaSource instanceof ManagedMediaSource) {
|
|
2969
|
+
mediaElement.disableRemotePlayback = true;
|
|
2970
|
+
mediaElement.srcObject = mediaSource;
|
|
2971
|
+
const detach = () => {
|
|
2972
|
+
mediaElement.srcObject = null;
|
|
2973
|
+
mediaElement.load();
|
|
2974
|
+
};
|
|
2975
|
+
return {
|
|
2976
|
+
url: "",
|
|
2977
|
+
detach
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
const url = URL.createObjectURL(mediaSource);
|
|
2981
|
+
mediaElement.src = url;
|
|
2982
|
+
const detach = () => {
|
|
2983
|
+
mediaElement.removeAttribute("src");
|
|
2984
|
+
mediaElement.load();
|
|
2985
|
+
URL.revokeObjectURL(url);
|
|
2986
|
+
};
|
|
2987
|
+
return {
|
|
2988
|
+
url,
|
|
2989
|
+
detach
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2795
2992
|
/**
|
|
2796
|
-
*
|
|
2993
|
+
* Create a SourceBuffer on a MediaSource.
|
|
2994
|
+
*
|
|
2995
|
+
* @param mediaSource - The MediaSource (must be in 'open' state)
|
|
2996
|
+
* @param mimeCodec - MIME type with codecs (e.g., 'video/mp4; codecs="avc1.42E01E"')
|
|
2997
|
+
* @returns The created SourceBuffer
|
|
2998
|
+
* @throws Error if MediaSource is not open or codec is unsupported
|
|
2999
|
+
*
|
|
3000
|
+
* @example
|
|
3001
|
+
* const buffer = createSourceBuffer(mediaSource, 'video/mp4; codecs="avc1.42E01E"');
|
|
2797
3002
|
*/
|
|
2798
|
-
function
|
|
2799
|
-
|
|
3003
|
+
function createSourceBuffer(mediaSource, mimeCodec) {
|
|
3004
|
+
if (mediaSource.readyState !== "open") throw new Error("MediaSource is not open");
|
|
3005
|
+
if (!isCodecSupported(mimeCodec)) throw new Error(`Codec not supported: ${mimeCodec}`);
|
|
3006
|
+
return mediaSource.addSourceBuffer(mimeCodec);
|
|
2800
3007
|
}
|
|
2801
3008
|
/**
|
|
2802
|
-
* Check if
|
|
2803
|
-
*
|
|
3009
|
+
* Check if a codec is supported.
|
|
3010
|
+
*
|
|
3011
|
+
* @param mimeCodec - MIME type with codecs string
|
|
3012
|
+
* @returns True if the codec is supported
|
|
3013
|
+
*
|
|
3014
|
+
* @example
|
|
3015
|
+
* if (isCodecSupported('video/mp4; codecs="avc1.42E01E"')) {
|
|
3016
|
+
* // Create source buffer
|
|
3017
|
+
* }
|
|
2804
3018
|
*/
|
|
2805
|
-
function
|
|
2806
|
-
|
|
3019
|
+
function isCodecSupported(mimeCodec) {
|
|
3020
|
+
if (!supportsMediaSource()) return false;
|
|
3021
|
+
return MediaSource.isTypeSupported(mimeCodec);
|
|
3022
|
+
}
|
|
3023
|
+
/**
|
|
3024
|
+
* Create a reactive signal that mirrors `mediaSource.readyState`.
|
|
3025
|
+
*
|
|
3026
|
+
* Listens to `sourceopen`, `sourceended`, and `sourceclose` events and updates
|
|
3027
|
+
* the signal accordingly, making readyState visible to the TC39 signal graph.
|
|
3028
|
+
* Listeners are automatically removed when `signal` is aborted.
|
|
3029
|
+
*
|
|
3030
|
+
* @param mediaSource - The MediaSource to observe
|
|
3031
|
+
* @param signal - AbortSignal that controls listener lifetime
|
|
3032
|
+
* @returns A `Signal.ReadonlyState` that reflects the current `readyState`
|
|
3033
|
+
*
|
|
3034
|
+
* @example
|
|
3035
|
+
* const controller = new AbortController();
|
|
3036
|
+
* const readyState = observeMediaSourceReadyState(mediaSource, controller.signal);
|
|
3037
|
+
* effect(() => {
|
|
3038
|
+
* if (readyState.get() === 'open') { ... }
|
|
3039
|
+
* });
|
|
3040
|
+
* // Later:
|
|
3041
|
+
* controller.abort();
|
|
3042
|
+
*/
|
|
3043
|
+
function observeMediaSourceReadyState(mediaSource, abortSignal) {
|
|
3044
|
+
const readyState = signal(mediaSource.readyState);
|
|
3045
|
+
const update = () => readyState.set(mediaSource.readyState);
|
|
3046
|
+
const options = { signal: abortSignal };
|
|
3047
|
+
mediaSource.addEventListener("sourceopen", update, options);
|
|
3048
|
+
mediaSource.addEventListener("sourceended", update, options);
|
|
3049
|
+
mediaSource.addEventListener("sourceclose", update, options);
|
|
3050
|
+
return readyState;
|
|
2807
3051
|
}
|
|
3052
|
+
|
|
3053
|
+
//#endregion
|
|
3054
|
+
//#region ../spf/dist/dev/dom/features/setup-mediasource.js
|
|
2808
3055
|
/**
|
|
2809
3056
|
* Setup MediaSource orchestration.
|
|
2810
3057
|
*
|
|
@@ -2815,27 +3062,34 @@ function shouldSetup(_state, owners) {
|
|
|
2815
3062
|
* Updates owners.mediaSource after successful setup.
|
|
2816
3063
|
*/
|
|
2817
3064
|
function setupMediaSource({ state, owners }) {
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
const
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
3065
|
+
const abortController = new AbortController();
|
|
3066
|
+
const mediaElementSignal = computed(() => owners.get().mediaElement);
|
|
3067
|
+
const presentationUrlSignal = computed(() => state.get().presentation?.url);
|
|
3068
|
+
const canSetupSignal = computed(() => !!mediaElementSignal.get() && !!presentationUrlSignal.get());
|
|
3069
|
+
const mediaElementSrcSignal = computed(() => mediaElementSignal.get()?.src);
|
|
3070
|
+
const mediaSourceSignal = computed(() => owners.get().mediaSource);
|
|
3071
|
+
const shouldSetupSignal = computed(() => !mediaElementSrcSignal.get());
|
|
3072
|
+
const cleanupEffect = effect(() => {
|
|
3073
|
+
if (!canSetupSignal.get() || !shouldSetupSignal.get()) return;
|
|
3074
|
+
const mediaElement = mediaElementSignal.get();
|
|
3075
|
+
const { signal } = abortController;
|
|
3076
|
+
const mediaSource = createMediaSource({ preferManaged: true });
|
|
3077
|
+
const mediaSourceReadyState = observeMediaSourceReadyState(mediaSource, signal);
|
|
3078
|
+
attachMediaSource(mediaSource, mediaElement);
|
|
3079
|
+
const cleanupOwnersUpdateEffect = effect(() => {
|
|
3080
|
+
if (!!mediaSourceSignal.get() || mediaSourceReadyState.get() !== "open") return;
|
|
3081
|
+
owners.set(Object.assign({}, owners.get(), {
|
|
3082
|
+
mediaSource,
|
|
3083
|
+
mediaSourceReadyState
|
|
3084
|
+
}));
|
|
3085
|
+
});
|
|
3086
|
+
return () => {
|
|
3087
|
+
cleanupOwnersUpdateEffect();
|
|
3088
|
+
};
|
|
2835
3089
|
});
|
|
2836
3090
|
return () => {
|
|
2837
3091
|
abortController?.abort();
|
|
2838
|
-
|
|
3092
|
+
cleanupEffect();
|
|
2839
3093
|
};
|
|
2840
3094
|
}
|
|
2841
3095
|
|
|
@@ -2930,7 +3184,7 @@ function messageToTask(message, options) {
|
|
|
2930
3184
|
return factory(message, options);
|
|
2931
3185
|
}
|
|
2932
3186
|
function createSourceBufferActor(sourceBuffer, initialContext) {
|
|
2933
|
-
const
|
|
3187
|
+
const snapshotSignal = signal({
|
|
2934
3188
|
status: "idle",
|
|
2935
3189
|
context: {
|
|
2936
3190
|
segments: [],
|
|
@@ -2941,55 +3195,47 @@ function createSourceBufferActor(sourceBuffer, initialContext) {
|
|
|
2941
3195
|
});
|
|
2942
3196
|
const runner = new SerialRunner();
|
|
2943
3197
|
function applyResult(newContext) {
|
|
2944
|
-
const status =
|
|
2945
|
-
|
|
3198
|
+
const status = snapshotSignal.get().status === "destroyed" ? "destroyed" : "idle";
|
|
3199
|
+
snapshotSignal.set({
|
|
2946
3200
|
status,
|
|
2947
3201
|
context: newContext
|
|
2948
3202
|
});
|
|
2949
|
-
state.flush();
|
|
2950
3203
|
}
|
|
2951
3204
|
function handleError(e) {
|
|
2952
|
-
|
|
2953
|
-
state.patch({ status });
|
|
2954
|
-
state.flush();
|
|
3205
|
+
update(snapshotSignal, { status: snapshotSignal.get().status === "destroyed" ? "destroyed" : "idle" });
|
|
2955
3206
|
throw e;
|
|
2956
3207
|
}
|
|
2957
3208
|
return {
|
|
2958
3209
|
get snapshot() {
|
|
2959
|
-
return
|
|
2960
|
-
},
|
|
2961
|
-
subscribe(listener) {
|
|
2962
|
-
return state.subscribe(listener);
|
|
3210
|
+
return snapshotSignal;
|
|
2963
3211
|
},
|
|
2964
3212
|
send(message, signal) {
|
|
2965
|
-
if (
|
|
2966
|
-
|
|
3213
|
+
if (snapshotSignal.get().status !== "idle") return Promise.reject(new SourceBufferActorError(`send() called while actor is ${snapshotSignal.get().status}`));
|
|
3214
|
+
update(snapshotSignal, { status: "updating" });
|
|
2967
3215
|
const onPartialContext = (ctx) => {
|
|
2968
|
-
|
|
3216
|
+
snapshotSignal.set({
|
|
2969
3217
|
status: "updating",
|
|
2970
3218
|
context: ctx
|
|
2971
3219
|
});
|
|
2972
|
-
state.flush();
|
|
2973
3220
|
};
|
|
2974
3221
|
const task = messageToTask(message, {
|
|
2975
3222
|
signal,
|
|
2976
|
-
getCtx: () =>
|
|
3223
|
+
getCtx: () => snapshotSignal.get().context,
|
|
2977
3224
|
sourceBuffer,
|
|
2978
3225
|
onPartialContext
|
|
2979
3226
|
});
|
|
2980
3227
|
return runner.schedule(task).then(applyResult).catch(handleError);
|
|
2981
3228
|
},
|
|
2982
3229
|
batch(messages, signal) {
|
|
2983
|
-
if (
|
|
3230
|
+
if (snapshotSignal.get().status !== "idle") return Promise.reject(new SourceBufferActorError(`batch() called while actor is ${snapshotSignal.get().status}`));
|
|
2984
3231
|
if (messages.length === 0) return Promise.resolve();
|
|
2985
|
-
|
|
2986
|
-
let workingCtx =
|
|
3232
|
+
update(snapshotSignal, { status: "updating" });
|
|
3233
|
+
let workingCtx = snapshotSignal.get().context;
|
|
2987
3234
|
const onPartialContext = (ctx) => {
|
|
2988
|
-
|
|
3235
|
+
snapshotSignal.set({
|
|
2989
3236
|
status: "updating",
|
|
2990
3237
|
context: ctx
|
|
2991
3238
|
});
|
|
2992
|
-
state.flush();
|
|
2993
3239
|
};
|
|
2994
3240
|
for (const message of messages.slice(0, -1)) {
|
|
2995
3241
|
const task = messageToTask(message, {
|
|
@@ -3011,8 +3257,7 @@ function createSourceBufferActor(sourceBuffer, initialContext) {
|
|
|
3011
3257
|
return runner.schedule(lastTask).then(applyResult).catch(handleError);
|
|
3012
3258
|
},
|
|
3013
3259
|
destroy() {
|
|
3014
|
-
|
|
3015
|
-
state.flush();
|
|
3260
|
+
update(snapshotSignal, { status: "destroyed" });
|
|
3016
3261
|
runner.destroy();
|
|
3017
3262
|
}
|
|
3018
3263
|
};
|
|
@@ -3020,6 +3265,10 @@ function createSourceBufferActor(sourceBuffer, initialContext) {
|
|
|
3020
3265
|
|
|
3021
3266
|
//#endregion
|
|
3022
3267
|
//#region ../spf/dist/dev/dom/features/setup-sourcebuffer.js
|
|
3268
|
+
const ActorKeyByType = {
|
|
3269
|
+
video: "videoBufferActor",
|
|
3270
|
+
audio: "audioBufferActor"
|
|
3271
|
+
};
|
|
3023
3272
|
/**
|
|
3024
3273
|
* Build MIME codec string from track metadata.
|
|
3025
3274
|
*
|
|
@@ -3037,76 +3286,56 @@ function buildMimeCodec(track) {
|
|
|
3037
3286
|
/**
|
|
3038
3287
|
* Setup all needed SourceBuffers as a single coordinated operation.
|
|
3039
3288
|
*
|
|
3040
|
-
* Waits until ALL
|
|
3041
|
-
*
|
|
3042
|
-
*
|
|
3043
|
-
*
|
|
3044
|
-
*
|
|
3045
|
-
*
|
|
3289
|
+
* Waits until ALL media tracks in the presentation are resolved with codecs,
|
|
3290
|
+
* then creates every SourceBuffer in one synchronous block before setting
|
|
3291
|
+
* owners. This guarantees that downstream consumers (e.g. loadSegments) never
|
|
3292
|
+
* see a partial set of SourceBuffers — preventing the Firefox bug where
|
|
3293
|
+
* appending to a video SourceBuffer before the audio SourceBuffer exists
|
|
3294
|
+
* causes mozHasAudio to be permanently false.
|
|
3046
3295
|
*
|
|
3047
3296
|
* Handles video-only, audio-only, and combined presentations correctly:
|
|
3048
|
-
*
|
|
3297
|
+
* track types are derived from the presentation rather than hardcoded.
|
|
3049
3298
|
*
|
|
3050
3299
|
* @example
|
|
3051
3300
|
* const cleanup = setupSourceBuffers({ state, owners });
|
|
3052
3301
|
*/
|
|
3053
3302
|
function setupSourceBuffers({ state, owners }) {
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
if (
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
const
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3303
|
+
const presentationTypesSignal = computed(() => {
|
|
3304
|
+
const { presentation } = state.get();
|
|
3305
|
+
if (!presentation || !("selectionSets" in presentation)) return [];
|
|
3306
|
+
return presentation.selectionSets.map(({ type }) => type).filter((type) => type === "video" || type === "audio");
|
|
3307
|
+
});
|
|
3308
|
+
const canSetupSignal = computed(() => {
|
|
3309
|
+
const types = presentationTypesSignal.get();
|
|
3310
|
+
if (!owners.get().mediaSource || types.length === 0) return false;
|
|
3311
|
+
const s = state.get();
|
|
3312
|
+
return types.every((type) => {
|
|
3313
|
+
const track = getSelectedTrack(s, type);
|
|
3314
|
+
return track && isResolvedTrack(track) && !!track.codecs?.length;
|
|
3315
|
+
});
|
|
3316
|
+
});
|
|
3317
|
+
const shouldSetupSignal = computed(() => {
|
|
3318
|
+
const o = owners.get();
|
|
3319
|
+
return presentationTypesSignal.get().every((type) => !o[BufferKeyByType[type]]);
|
|
3320
|
+
});
|
|
3321
|
+
return effect(() => {
|
|
3322
|
+
if (!canSetupSignal.get() || !shouldSetupSignal.get()) return;
|
|
3323
|
+
const s = state.get();
|
|
3324
|
+
const o = owners.get();
|
|
3066
3325
|
const patch = {};
|
|
3067
|
-
|
|
3068
|
-
const
|
|
3069
|
-
|
|
3070
|
-
patch
|
|
3071
|
-
|
|
3072
|
-
if (audioSelected && audioTrack && isResolvedTrack(audioTrack)) {
|
|
3073
|
-
const buffer = createSourceBuffer(currentOwners.mediaSource, buildMimeCodec(audioTrack));
|
|
3074
|
-
patch.audioBuffer = buffer;
|
|
3075
|
-
patch.audioBufferActor = createSourceBufferActor(buffer);
|
|
3326
|
+
for (const type of presentationTypesSignal.get()) {
|
|
3327
|
+
const track = getSelectedTrack(s, type);
|
|
3328
|
+
const buffer = createSourceBuffer(o.mediaSource, buildMimeCodec(track));
|
|
3329
|
+
patch[BufferKeyByType[type]] = buffer;
|
|
3330
|
+
patch[ActorKeyByType[type]] = createSourceBufferActor(buffer);
|
|
3076
3331
|
}
|
|
3077
|
-
owners
|
|
3078
|
-
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
3332
|
+
update(owners, patch);
|
|
3079
3333
|
});
|
|
3080
3334
|
}
|
|
3081
3335
|
|
|
3082
3336
|
//#endregion
|
|
3083
3337
|
//#region ../spf/dist/dev/dom/features/setup-text-tracks.js
|
|
3084
3338
|
/**
|
|
3085
|
-
* Get all text tracks from presentation.
|
|
3086
|
-
*/
|
|
3087
|
-
function getTextTracks(presentation) {
|
|
3088
|
-
if (!presentation?.selectionSets) return [];
|
|
3089
|
-
const textSet = presentation.selectionSets.find((set) => set.type === "text");
|
|
3090
|
-
if (!textSet?.switchingSets?.[0]?.tracks) return [];
|
|
3091
|
-
return textSet.switchingSets[0].tracks;
|
|
3092
|
-
}
|
|
3093
|
-
/**
|
|
3094
|
-
* Check if we can setup text tracks.
|
|
3095
|
-
*
|
|
3096
|
-
* Requires:
|
|
3097
|
-
* - mediaElement exists
|
|
3098
|
-
* - presentation has text tracks to setup
|
|
3099
|
-
*/
|
|
3100
|
-
function canSetupTextTracks(state, owners) {
|
|
3101
|
-
return !!owners.mediaElement && !!getTextTracks(state.presentation).length;
|
|
3102
|
-
}
|
|
3103
|
-
/**
|
|
3104
|
-
* Check if we should setup text tracks (not already set up).
|
|
3105
|
-
*/
|
|
3106
|
-
function shouldSetupTextTracks(owners) {
|
|
3107
|
-
return !owners.textTracks;
|
|
3108
|
-
}
|
|
3109
|
-
/**
|
|
3110
3339
|
* Create a track element for a text track.
|
|
3111
3340
|
*
|
|
3112
3341
|
* Note: We use DOM <track> elements instead of the TextTrack JS API
|
|
@@ -3139,27 +3368,31 @@ function createTrackElement(track) {
|
|
|
3139
3368
|
* const cleanup = setupTextTracks({ state, owners });
|
|
3140
3369
|
*/
|
|
3141
3370
|
function setupTextTracks({ state, owners }) {
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
if (
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3371
|
+
const modelTextTracksSignal = computed(() => state.get().presentation?.selectionSets?.find((selectionSet) => selectionSet.type === "text")?.switchingSets[0]?.tracks, { equals(prevTextTracks, nextTextTracks) {
|
|
3372
|
+
if (prevTextTracks === nextTextTracks) return true;
|
|
3373
|
+
if (typeof prevTextTracks !== typeof nextTextTracks) return false;
|
|
3374
|
+
if (prevTextTracks?.length !== nextTextTracks?.length) return false;
|
|
3375
|
+
return !!nextTextTracks && nextTextTracks.every((nextTextTrack) => prevTextTracks?.some((prevTextTrack) => prevTextTrack.id === nextTextTrack.id));
|
|
3376
|
+
} });
|
|
3377
|
+
const ownerTextTracksSignal = computed(() => owners.get().textTracks);
|
|
3378
|
+
const mediaElementSignal = computed(() => owners.get().mediaElement);
|
|
3379
|
+
const canSetupTextTracksSignal = computed(() => !!mediaElementSignal.get() && modelTextTracksSignal.get()?.length);
|
|
3380
|
+
const shouldSetupTextTracksSignal = computed(() => !ownerTextTracksSignal.get());
|
|
3381
|
+
const cleanupEffect = effect(() => {
|
|
3382
|
+
if (!canSetupTextTracksSignal.get() || !shouldSetupTextTracksSignal.get()) return;
|
|
3383
|
+
const mediaElement = mediaElementSignal.get();
|
|
3384
|
+
const modelTextTracks = modelTextTracksSignal.get();
|
|
3150
3385
|
const trackMap = /* @__PURE__ */ new Map();
|
|
3151
|
-
|
|
3152
|
-
const trackElement = createTrackElement(
|
|
3153
|
-
|
|
3154
|
-
trackMap.set(
|
|
3155
|
-
|
|
3156
|
-
}
|
|
3157
|
-
owners.patch({ textTracks: trackMap });
|
|
3386
|
+
modelTextTracks.forEach((modelTextTrack) => {
|
|
3387
|
+
const trackElement = createTrackElement(modelTextTrack);
|
|
3388
|
+
mediaElement.appendChild(trackElement);
|
|
3389
|
+
trackMap.set(modelTextTrack.id, trackElement);
|
|
3390
|
+
});
|
|
3391
|
+
if (trackMap.size) update(owners, { textTracks: trackMap });
|
|
3158
3392
|
});
|
|
3159
3393
|
return () => {
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
unsubscribe();
|
|
3394
|
+
owners.get().textTracks?.forEach((trackElement) => trackElement.remove());
|
|
3395
|
+
cleanupEffect();
|
|
3163
3396
|
};
|
|
3164
3397
|
}
|
|
3165
3398
|
|
|
@@ -3188,52 +3421,33 @@ function setupTextTracks({ state, owners }) {
|
|
|
3188
3421
|
* const cleanup = syncSelectedTextTrackFromDom({ state, owners });
|
|
3189
3422
|
*/
|
|
3190
3423
|
function syncSelectedTextTrackFromDom({ state, owners }) {
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
lastMediaElement = mediaElement;
|
|
3199
|
-
if (!mediaElement) return;
|
|
3200
|
-
const sync = () => {
|
|
3201
|
-
const newId = Array.from(mediaElement.textTracks).find((t) => t.mode === "showing" && (t.kind === "subtitles" || t.kind === "captions"))?.id || void 0;
|
|
3202
|
-
const current = state.current;
|
|
3424
|
+
const mediaElement = computed(() => owners.get().mediaElement);
|
|
3425
|
+
return effect(() => {
|
|
3426
|
+
const el = mediaElement.get();
|
|
3427
|
+
if (!el) return;
|
|
3428
|
+
return listen(el.textTracks, "change", () => {
|
|
3429
|
+
const newId = Array.from(el.textTracks).find((t) => t.mode === "showing" && (t.kind === "subtitles" || t.kind === "captions"))?.id || void 0;
|
|
3430
|
+
const current = state.get();
|
|
3203
3431
|
if (current.selectedTextTrackId === newId) return;
|
|
3204
|
-
if (newId) state
|
|
3432
|
+
if (newId) update(state, { selectedTextTrackId: newId });
|
|
3205
3433
|
else {
|
|
3206
3434
|
const prevId = current.selectedTextTrackId;
|
|
3207
3435
|
if (prevId && current.textBufferState?.[prevId]) {
|
|
3208
3436
|
const next = { ...current.textBufferState };
|
|
3209
3437
|
delete next[prevId];
|
|
3210
|
-
state
|
|
3438
|
+
update(state, {
|
|
3211
3439
|
selectedTextTrackId: void 0,
|
|
3212
3440
|
textBufferState: next
|
|
3213
3441
|
});
|
|
3214
|
-
} else state
|
|
3442
|
+
} else update(state, { selectedTextTrackId: void 0 });
|
|
3215
3443
|
}
|
|
3216
|
-
};
|
|
3217
|
-
removeListener = listen(mediaElement.textTracks, "change", sync);
|
|
3444
|
+
});
|
|
3218
3445
|
});
|
|
3219
|
-
return () => {
|
|
3220
|
-
removeListener?.();
|
|
3221
|
-
unsubscribe();
|
|
3222
|
-
};
|
|
3223
3446
|
}
|
|
3224
3447
|
|
|
3225
3448
|
//#endregion
|
|
3226
3449
|
//#region ../spf/dist/dev/dom/features/sync-text-track-modes.js
|
|
3227
3450
|
/**
|
|
3228
|
-
* Check if we can sync text track modes.
|
|
3229
|
-
*
|
|
3230
|
-
* Requires:
|
|
3231
|
-
* - textTracks map exists (track elements created)
|
|
3232
|
-
*/
|
|
3233
|
-
function canSyncTextTrackModes(owners) {
|
|
3234
|
-
return !!owners.textTracks && owners.textTracks.size > 0;
|
|
3235
|
-
}
|
|
3236
|
-
/**
|
|
3237
3451
|
* Sync text track modes orchestration.
|
|
3238
3452
|
*
|
|
3239
3453
|
* Manages track element modes based on selectedTextTrackId:
|
|
@@ -3248,11 +3462,15 @@ function canSyncTextTrackModes(owners) {
|
|
|
3248
3462
|
* const cleanup = syncTextTrackModes({ state, owners });
|
|
3249
3463
|
*/
|
|
3250
3464
|
function syncTextTrackModes({ state, owners }) {
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3465
|
+
const textTracksSignal = computed(() => owners.get().textTracks);
|
|
3466
|
+
const selectedTextTrackIdSignal = computed(() => state.get().selectedTextTrackId);
|
|
3467
|
+
const canSyncTextTrackModes = computed(() => !!textTracksSignal.get()?.size);
|
|
3468
|
+
return effect(() => {
|
|
3469
|
+
if (!canSyncTextTrackModes.get()) return;
|
|
3470
|
+
/** @TODO refactor TextTracks owners model. Should simply use id. Also should use corresponding TextTrack (JS) element if possible (CJP) */
|
|
3471
|
+
const textTracks = textTracksSignal.get();
|
|
3472
|
+
const selectedTextTrackId = selectedTextTrackIdSignal.get();
|
|
3473
|
+
for (const [trackId, trackElement] of textTracks) trackElement.track.mode = trackId === selectedTextTrackId ? "showing" : "hidden";
|
|
3256
3474
|
});
|
|
3257
3475
|
}
|
|
3258
3476
|
|
|
@@ -3286,7 +3504,7 @@ function shouldUpdateDuration(state, owners) {
|
|
|
3286
3504
|
if (!canUpdateDuration(state, owners)) return false;
|
|
3287
3505
|
const { mediaSource } = owners;
|
|
3288
3506
|
const { presentation } = state;
|
|
3289
|
-
if (mediaSource
|
|
3507
|
+
if ((owners.mediaSourceReadyState?.get() ?? owners.mediaSource?.readyState) !== "open") return false;
|
|
3290
3508
|
const duration = presentation.duration;
|
|
3291
3509
|
if (!Number.isFinite(duration) || Number.isNaN(duration) || duration <= 0) return false;
|
|
3292
3510
|
return Number.isNaN(mediaSource.duration);
|
|
@@ -3307,19 +3525,26 @@ function waitForSourceBuffersReady(owners) {
|
|
|
3307
3525
|
*/
|
|
3308
3526
|
function updateDuration({ state, owners }) {
|
|
3309
3527
|
let destroyed = false;
|
|
3310
|
-
|
|
3311
|
-
|
|
3528
|
+
let running = false;
|
|
3529
|
+
const cleanupEffect = effect(() => {
|
|
3530
|
+
const currentState = state.get();
|
|
3531
|
+
const currentOwners = owners.get();
|
|
3532
|
+
if (!shouldUpdateDuration(currentState, currentOwners) || running) return;
|
|
3312
3533
|
const { mediaSource } = currentOwners;
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3534
|
+
running = true;
|
|
3535
|
+
waitForSourceBuffersReady(currentOwners).then(() => {
|
|
3536
|
+
if (destroyed || mediaSource.readyState !== "open") return;
|
|
3537
|
+
let duration = currentState.presentation.duration;
|
|
3538
|
+
const maxBufferedEnd = getMaxBufferedEnd(currentOwners);
|
|
3539
|
+
if (maxBufferedEnd > duration) duration = maxBufferedEnd;
|
|
3540
|
+
mediaSource.duration = duration;
|
|
3541
|
+
}).finally(() => {
|
|
3542
|
+
running = false;
|
|
3543
|
+
});
|
|
3319
3544
|
});
|
|
3320
3545
|
return () => {
|
|
3321
3546
|
destroyed = true;
|
|
3322
|
-
|
|
3547
|
+
cleanupEffect();
|
|
3323
3548
|
};
|
|
3324
3549
|
}
|
|
3325
3550
|
|
|
@@ -3346,78 +3571,56 @@ function updateDuration({ state, owners }) {
|
|
|
3346
3571
|
* preferredAudioLanguage: 'en',
|
|
3347
3572
|
* });
|
|
3348
3573
|
*
|
|
3349
|
-
* // Initialize by
|
|
3350
|
-
* engine.owners.
|
|
3351
|
-
* engine.state.
|
|
3574
|
+
* // Initialize by setting state and owners
|
|
3575
|
+
* engine.owners.set({ ...engine.owners.get(), mediaElement: document.querySelector('video') });
|
|
3576
|
+
* engine.state.set({
|
|
3577
|
+
* ...engine.state.get(),
|
|
3352
3578
|
* presentation: { url: 'https://example.com/playlist.m3u8' },
|
|
3353
3579
|
* preload: 'auto',
|
|
3354
3580
|
* });
|
|
3355
3581
|
*
|
|
3356
3582
|
* // Inspect state
|
|
3357
|
-
* console.log(engine.state.
|
|
3583
|
+
* console.log(engine.state.get());
|
|
3358
3584
|
*
|
|
3359
3585
|
* // Cleanup
|
|
3360
3586
|
* engine.destroy();
|
|
3361
3587
|
*/
|
|
3362
3588
|
function createPlaybackEngine(config = {}) {
|
|
3363
|
-
const state =
|
|
3589
|
+
const state = signal({ bandwidthState: {
|
|
3364
3590
|
fastEstimate: 0,
|
|
3365
3591
|
fastTotalWeight: 0,
|
|
3366
3592
|
slowEstimate: 0,
|
|
3367
3593
|
slowTotalWeight: 0,
|
|
3368
3594
|
bytesSampled: 0
|
|
3369
3595
|
} });
|
|
3370
|
-
const owners =
|
|
3371
|
-
const events = createEventStream();
|
|
3596
|
+
const owners = signal({});
|
|
3372
3597
|
const cleanups = [
|
|
3373
|
-
syncPreloadAttribute(
|
|
3374
|
-
trackPlaybackInitiated({
|
|
3598
|
+
syncPreloadAttribute({
|
|
3375
3599
|
state,
|
|
3376
|
-
owners
|
|
3377
|
-
events
|
|
3600
|
+
owners
|
|
3378
3601
|
}),
|
|
3379
|
-
|
|
3602
|
+
trackPlaybackInitiated({
|
|
3380
3603
|
state,
|
|
3381
|
-
|
|
3604
|
+
owners
|
|
3382
3605
|
}),
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
owners,
|
|
3386
|
-
events
|
|
3387
|
-
}, {
|
|
3606
|
+
resolvePresentation({ state }),
|
|
3607
|
+
selectVideoTrack({ state }, {
|
|
3388
3608
|
type: "video",
|
|
3389
3609
|
...config.initialBandwidth !== void 0 && { initialBandwidth: config.initialBandwidth }
|
|
3390
3610
|
}),
|
|
3391
|
-
selectAudioTrack({
|
|
3392
|
-
state,
|
|
3393
|
-
owners,
|
|
3394
|
-
events
|
|
3395
|
-
}, {
|
|
3611
|
+
selectAudioTrack({ state }, {
|
|
3396
3612
|
type: "audio",
|
|
3397
3613
|
...config.preferredAudioLanguage !== void 0 && { preferredAudioLanguage: config.preferredAudioLanguage }
|
|
3398
3614
|
}),
|
|
3399
|
-
selectTextTrack({
|
|
3400
|
-
state,
|
|
3401
|
-
owners,
|
|
3402
|
-
events
|
|
3403
|
-
}, {
|
|
3615
|
+
selectTextTrack({ state }, {
|
|
3404
3616
|
type: "text",
|
|
3405
3617
|
...config.preferredSubtitleLanguage !== void 0 && { preferredSubtitleLanguage: config.preferredSubtitleLanguage },
|
|
3406
3618
|
...config.includeForcedTracks !== void 0 && { includeForcedTracks: config.includeForcedTracks },
|
|
3407
3619
|
...config.enableDefaultTrack !== void 0 && { enableDefaultTrack: config.enableDefaultTrack }
|
|
3408
3620
|
}),
|
|
3409
|
-
resolveTrack({
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
}, { type: "video" }),
|
|
3413
|
-
resolveTrack({
|
|
3414
|
-
state,
|
|
3415
|
-
events
|
|
3416
|
-
}, { type: "audio" }),
|
|
3417
|
-
resolveTrack({
|
|
3418
|
-
state,
|
|
3419
|
-
events
|
|
3420
|
-
}, { type: "text" }),
|
|
3621
|
+
resolveTrack({ state }, { type: "video" }),
|
|
3622
|
+
resolveTrack({ state }, { type: "audio" }),
|
|
3623
|
+
resolveTrack({ state }, { type: "text" }),
|
|
3421
3624
|
calculatePresentationDuration({ state }),
|
|
3422
3625
|
setupMediaSource({
|
|
3423
3626
|
state,
|
|
@@ -3435,7 +3638,7 @@ function createPlaybackEngine(config = {}) {
|
|
|
3435
3638
|
state,
|
|
3436
3639
|
owners
|
|
3437
3640
|
}),
|
|
3438
|
-
switchQuality({ state }),
|
|
3641
|
+
switchQuality({ state }, config.initialBandwidth !== void 0 ? { defaultBandwidth: config.initialBandwidth } : {}),
|
|
3439
3642
|
loadSegments({
|
|
3440
3643
|
state,
|
|
3441
3644
|
owners
|
|
@@ -3465,11 +3668,9 @@ function createPlaybackEngine(config = {}) {
|
|
|
3465
3668
|
owners
|
|
3466
3669
|
})
|
|
3467
3670
|
];
|
|
3468
|
-
events.dispatch({ type: "@@INITIALIZE@@" });
|
|
3469
3671
|
return {
|
|
3470
3672
|
state,
|
|
3471
3673
|
owners,
|
|
3472
|
-
events,
|
|
3473
3674
|
destroy: () => {
|
|
3474
3675
|
cleanups.forEach((cleanup) => cleanup());
|
|
3475
3676
|
destroyVttParser();
|
|
@@ -3515,11 +3716,11 @@ var SpfMedia = class {
|
|
|
3515
3716
|
return this.#engine;
|
|
3516
3717
|
}
|
|
3517
3718
|
attach(mediaElement) {
|
|
3518
|
-
this.#engine.owners
|
|
3719
|
+
update(this.#engine.owners, { mediaElement });
|
|
3519
3720
|
}
|
|
3520
3721
|
detach() {
|
|
3521
3722
|
this.#cancelPendingPlay();
|
|
3522
|
-
this.#engine.owners
|
|
3723
|
+
update(this.#engine.owners, { mediaElement: void 0 });
|
|
3523
3724
|
}
|
|
3524
3725
|
destroy() {
|
|
3525
3726
|
this.#cancelPendingPlay();
|
|
@@ -3530,24 +3731,24 @@ var SpfMedia = class {
|
|
|
3530
3731
|
}
|
|
3531
3732
|
set preload(value) {
|
|
3532
3733
|
this.#preload = value;
|
|
3533
|
-
if (value) this.#engine.state
|
|
3734
|
+
if (value) update(this.#engine.state, { preload: value });
|
|
3534
3735
|
}
|
|
3535
3736
|
get src() {
|
|
3536
|
-
return this.#engine.state.
|
|
3737
|
+
return this.#engine.state.get().presentation?.url ?? "";
|
|
3537
3738
|
}
|
|
3538
3739
|
set src(value) {
|
|
3539
|
-
const prevMediaElement = this.#engine.owners.
|
|
3740
|
+
const prevMediaElement = this.#engine.owners.get().mediaElement;
|
|
3540
3741
|
this.#cancelPendingPlay();
|
|
3541
3742
|
this.#engine.destroy();
|
|
3542
3743
|
this.#engine = createPlaybackEngine(this.#config);
|
|
3543
|
-
if (this.#preload) this.#engine.state
|
|
3544
|
-
if (prevMediaElement) this.#engine.owners
|
|
3545
|
-
if (value) this.#engine.state
|
|
3744
|
+
if (this.#preload) update(this.#engine.state, { preload: this.#preload });
|
|
3745
|
+
if (prevMediaElement) update(this.#engine.owners, { mediaElement: prevMediaElement });
|
|
3746
|
+
if (value) update(this.#engine.state, { presentation: { url: value } });
|
|
3546
3747
|
}
|
|
3547
3748
|
play() {
|
|
3548
|
-
const { mediaElement } = this.#engine.owners.
|
|
3749
|
+
const { mediaElement } = this.#engine.owners.get();
|
|
3549
3750
|
if (!mediaElement) return Promise.reject(/* @__PURE__ */ new Error("SpfMedia: no media element attached"));
|
|
3550
|
-
this.#engine.state
|
|
3751
|
+
update(this.#engine.state, { playbackInitiated: true });
|
|
3551
3752
|
return mediaElement.play().catch((err) => {
|
|
3552
3753
|
if (this.src) return new Promise((resolve, reject) => {
|
|
3553
3754
|
const listener = () => {
|
|
@@ -3562,7 +3763,7 @@ var SpfMedia = class {
|
|
|
3562
3763
|
}
|
|
3563
3764
|
#cancelPendingPlay() {
|
|
3564
3765
|
if (!this.#loadstartListener) return;
|
|
3565
|
-
const { mediaElement } = this.#engine.owners.
|
|
3766
|
+
const { mediaElement } = this.#engine.owners.get();
|
|
3566
3767
|
mediaElement?.removeEventListener("loadstart", this.#loadstartListener);
|
|
3567
3768
|
this.#loadstartListener = null;
|
|
3568
3769
|
}
|
|
@@ -3570,24 +3771,15 @@ var SpfMedia = class {
|
|
|
3570
3771
|
|
|
3571
3772
|
//#endregion
|
|
3572
3773
|
//#region ../core/dist/dev/dom/media/simple-hls/index.js
|
|
3573
|
-
var SimpleHlsCustomMedia = class extends DelegateMixin(
|
|
3774
|
+
var SimpleHlsCustomMedia = class extends DelegateMixin(CustomVideoElement, SpfMedia) {};
|
|
3574
3775
|
|
|
3575
3776
|
//#endregion
|
|
3576
3777
|
//#region src/media/simple-hls-video/index.ts
|
|
3577
|
-
var SimpleHlsVideo = class extends MediaAttachMixin(SimpleHlsCustomMedia) {
|
|
3578
|
-
static getTemplateHTML(attrs) {
|
|
3579
|
-
const { src, ...rest } = attrs;
|
|
3580
|
-
return super.getTemplateHTML(rest);
|
|
3581
|
-
}
|
|
3778
|
+
var SimpleHlsVideo = class extends MediaPropsMixin(MediaAttachMixin(SimpleHlsCustomMedia), SpfMedia) {
|
|
3582
3779
|
constructor() {
|
|
3583
3780
|
super();
|
|
3584
3781
|
this.attach(this.target);
|
|
3585
3782
|
}
|
|
3586
|
-
attributeChangedCallback(attrName, oldValue, newValue) {
|
|
3587
|
-
if (attrName !== "src") super.attributeChangedCallback(attrName, oldValue, newValue);
|
|
3588
|
-
if (attrName === "src" && oldValue !== newValue) this.src = newValue ?? "";
|
|
3589
|
-
if (attrName === "preload" && oldValue !== newValue) this.preload = newValue ?? "";
|
|
3590
|
-
}
|
|
3591
3783
|
};
|
|
3592
3784
|
|
|
3593
3785
|
//#endregion
|
|
@@ -3597,7 +3789,7 @@ var SimpleHlsVideoElement = class extends SimpleHlsVideo {
|
|
|
3597
3789
|
this.tagName = "simple-hls-video";
|
|
3598
3790
|
}
|
|
3599
3791
|
};
|
|
3600
|
-
|
|
3792
|
+
safeDefine(SimpleHlsVideoElement);
|
|
3601
3793
|
|
|
3602
3794
|
//#endregion
|
|
3603
3795
|
//# sourceMappingURL=simple-hls-video.dev.js.map
|