@videojs/html 10.0.0-beta.15 → 10.0.0-beta.16
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/audio-minimal-ui.dev.js +4 -5
- package/cdn/audio-minimal-ui.js +1 -1
- package/cdn/audio-minimal.dev.js +7 -8
- 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-ui.dev.js +5 -5
- package/cdn/audio-ui.js +1 -1
- package/cdn/audio.dev.js +23 -7
- 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 -3
- package/cdn/background.js +1 -1
- package/cdn/background.js.map +1 -1
- package/cdn/context-C-IpVHEt.js +14 -0
- package/cdn/context-C-IpVHEt.js.map +1 -0
- package/cdn/{context-BjrbhiSk.js → context-CvJ83xGk.js} +2 -83
- package/cdn/context-CvJ83xGk.js.map +1 -0
- package/cdn/create-player-B_oNyyb5.js +7 -0
- package/cdn/create-player-B_oNyyb5.js.map +1 -0
- package/cdn/{create-player-p9xCO0yC.js → create-player-DckZq-sE.js} +56 -1416
- package/cdn/create-player-DckZq-sE.js.map +1 -0
- package/cdn/{hls-BriQI4q1.js → hls-CJCXZSGG.js} +8 -11
- package/cdn/{hls-BriQI4q1.js.map → hls-CJCXZSGG.js.map} +1 -1
- package/cdn/{hls-26YJVjvD.js → hls-lPH0rWmB.js} +3 -3
- package/cdn/{hls-26YJVjvD.js.map → hls-lPH0rWmB.js.map} +1 -1
- package/cdn/hotkey-B7OaweHa.js +154 -0
- package/cdn/hotkey-B7OaweHa.js.map +1 -0
- package/cdn/hotkey-BLEt77Z0.js +2 -0
- package/cdn/hotkey-BLEt77Z0.js.map +1 -0
- package/cdn/media/dash-video.dev.js +3 -4
- package/cdn/media/dash-video.dev.js.map +1 -1
- package/cdn/media/dash-video.js +2 -2
- package/cdn/media/hls-video.dev.js +4 -5
- package/cdn/media/hls-video.dev.js.map +1 -1
- package/cdn/media/hls-video.js +1 -1
- package/cdn/media/hls-video.js.map +1 -1
- package/cdn/media/mux-audio.dev.d.ts +1 -0
- package/cdn/media/mux-audio.dev.js +29 -0
- package/cdn/media/mux-audio.dev.js.map +1 -0
- package/cdn/media/mux-audio.js +2 -0
- package/cdn/media/mux-audio.js.map +1 -0
- package/cdn/media/mux-video.dev.js +5 -3099
- package/cdn/media/mux-video.dev.js.map +1 -1
- package/cdn/media/mux-video.js +1 -24
- package/cdn/media/mux-video.js.map +1 -1
- package/cdn/media/native-hls-video.dev.js +4 -4
- package/cdn/media/native-hls-video.js +1 -1
- package/cdn/media/simple-hls-video.dev.js +965 -680
- package/cdn/media/simple-hls-video.dev.js.map +1 -1
- package/cdn/media/simple-hls-video.js +6 -6
- package/cdn/media/simple-hls-video.js.map +1 -1
- package/cdn/{media-attach-mixin-DfKkofgF.js → media-attach-mixin-CMH1ziXP.js} +2 -2
- package/cdn/{media-attach-mixin-DfKkofgF.js.map → media-attach-mixin-CMH1ziXP.js.map} +1 -1
- package/cdn/{media-attach-mixin-Dv2D43tb.js → media-attach-mixin-DUzmXEJ3.js} +2 -2
- package/cdn/{media-attach-mixin-Dv2D43tb.js.map → media-attach-mixin-DUzmXEJ3.js.map} +1 -1
- package/cdn/{media-props-mixin-Dc5dpCV4.js → media-props-mixin-CZLZIhGT.js} +92 -8
- package/cdn/media-props-mixin-CZLZIhGT.js.map +1 -0
- package/cdn/media-props-mixin-DrjlZybt.js +47 -0
- package/cdn/media-props-mixin-DrjlZybt.js.map +1 -0
- package/cdn/{minimal-BUUGNZK7.js → minimal-D9Dw-L2P.js} +1 -1
- package/cdn/{minimal-BUUGNZK7.js.map → minimal-D9Dw-L2P.js.map} +1 -1
- package/cdn/{minimal-CJGOJ6pE.js → minimal-Dc5nNl_-.js} +1 -1
- package/cdn/{minimal-CJGOJ6pE.js.map → minimal-Dc5nNl_-.js.map} +1 -1
- package/cdn/mux-CBXn2z5k.js +3102 -0
- package/cdn/mux-CBXn2z5k.js.map +1 -0
- package/cdn/mux-DmGzBNIq.js +25 -0
- package/cdn/mux-DmGzBNIq.js.map +1 -0
- package/cdn/{native-hls-D2rfyyyW.js → native-hls-BCOLtRUl.js} +2 -3
- package/cdn/{native-hls-D2rfyyyW.js.map → native-hls-BCOLtRUl.js.map} +1 -1
- package/cdn/native-hls-BU8--1Kg.js +2 -0
- package/cdn/{native-hls-CjRvSXAJ.js.map → native-hls-BU8--1Kg.js.map} +1 -1
- package/cdn/player-DAmuLGE1.js +2 -0
- package/cdn/{player-Cz9-_j7J.js.map → player-DAmuLGE1.js.map} +1 -1
- package/cdn/{player-D0bhjT8X.js → player-Dbl1MUJ2.js} +3 -3
- package/cdn/{player-D0bhjT8X.js.map → player-Dbl1MUJ2.js.map} +1 -1
- package/cdn/poster-B9COb7AR.js +2 -0
- package/cdn/poster-B9COb7AR.js.map +1 -0
- package/cdn/poster-qAhnKWea.js +480 -0
- package/cdn/poster-qAhnKWea.js.map +1 -0
- package/cdn/{skin-element-DB-cKAAx.js → skin-element-DbmY-ske.js} +2 -2
- package/cdn/{skin-element-DB-cKAAx.js.map → skin-element-DbmY-ske.js.map} +1 -1
- package/cdn/{skin-element-zakTHELS.js → skin-element-ih3VrnYx.js} +2 -2
- package/cdn/{skin-element-zakTHELS.js.map → skin-element-ih3VrnYx.js.map} +1 -1
- package/cdn/video-minimal-ui.dev.js +4 -5
- package/cdn/video-minimal-ui.js +1 -1
- package/cdn/video-minimal.dev.js +7 -8
- 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-ui.dev.js +5 -5
- package/cdn/video-ui.js +1 -1
- package/cdn/video.dev.js +26 -7
- 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-BbIc1N4U.js +8 -0
- package/cdn/volume-slider-BbIc1N4U.js.map +1 -0
- package/cdn/{volume-slider-Co5X4Z1t.js → volume-slider-C6ek7oC9.js} +1855 -386
- package/cdn/volume-slider-C6ek7oC9.js.map +1 -0
- package/dist/default/_virtual/inline-css_src/define/audio/minimal-skin.js +1 -1
- 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 +1 -1
- package/dist/default/_virtual/inline-css_src/define/audio/skin.js.map +1 -1
- package/dist/default/_virtual/inline-css_src/define/video/minimal-skin.js +1 -1
- 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 +1 -1
- package/dist/default/_virtual/inline-css_src/define/video/skin.js.map +1 -1
- package/dist/default/define/audio/minimal-skin.css +23 -0
- package/dist/default/define/audio/skin.css +23 -0
- package/dist/default/define/audio/skin.js +1 -1
- package/dist/default/define/audio/skin.js.map +1 -1
- package/dist/default/define/audio/ui.js +1 -0
- package/dist/default/define/background/skin.js +1 -1
- package/dist/default/define/media/mux-audio.js +13 -0
- package/dist/default/define/media/mux-audio.js.map +1 -0
- package/dist/default/define/skin-element.js +1 -1
- package/dist/default/define/ui/hotkey.js +7 -0
- package/dist/default/define/ui/hotkey.js.map +1 -0
- package/dist/default/define/ui/slider.js +1 -1
- package/dist/default/define/ui/time-slider.js +1 -1
- package/dist/default/define/ui/volume-slider.js +1 -1
- package/dist/default/define/video/minimal-skin.css +23 -0
- package/dist/default/define/video/skin.css +23 -0
- package/dist/default/define/video/skin.js +1 -1
- package/dist/default/define/video/skin.js.map +1 -1
- package/dist/default/define/video/ui.js +1 -0
- package/dist/default/index.js +5 -1
- package/dist/default/media/container-element.js +16 -0
- package/dist/default/media/container-element.js.map +1 -1
- package/dist/default/media/mux-audio/index.js +18 -0
- package/dist/default/media/mux-audio/index.js.map +1 -0
- package/dist/default/player/context.js.map +1 -1
- package/dist/default/ui/alert-dialog/alert-dialog-element.js +1 -0
- package/dist/default/ui/alert-dialog/alert-dialog-element.js.map +1 -1
- package/dist/default/ui/buffering-indicator/buffering-indicator-element.js +1 -0
- package/dist/default/ui/buffering-indicator/buffering-indicator-element.js.map +1 -1
- package/dist/default/ui/captions-button/captions-button-element.js +1 -0
- package/dist/default/ui/captions-button/captions-button-element.js.map +1 -1
- package/dist/default/ui/error-dialog/error-dialog-element.js +1 -0
- package/dist/default/ui/error-dialog/error-dialog-element.js.map +1 -1
- package/dist/default/ui/fullscreen-button/fullscreen-button-element.js +1 -0
- package/dist/default/ui/fullscreen-button/fullscreen-button-element.js.map +1 -1
- package/dist/default/ui/hotkey/aria-key-shortcuts-controller.js +28 -0
- package/dist/default/ui/hotkey/aria-key-shortcuts-controller.js.map +1 -0
- package/dist/default/ui/hotkey/hotkey-element.js +81 -0
- package/dist/default/ui/hotkey/hotkey-element.js.map +1 -0
- package/dist/default/ui/media-button-element.js +9 -1
- package/dist/default/ui/media-button-element.js.map +1 -1
- package/dist/default/ui/mute-button/mute-button-element.js +1 -0
- package/dist/default/ui/mute-button/mute-button-element.js.map +1 -1
- package/dist/default/ui/pip-button/pip-button-element.js +1 -0
- package/dist/default/ui/pip-button/pip-button-element.js.map +1 -1
- package/dist/default/ui/play-button/play-button-element.js +1 -0
- package/dist/default/ui/play-button/play-button-element.js.map +1 -1
- package/dist/default/ui/popover/popover-element.js +7 -55
- package/dist/default/ui/popover/popover-element.js.map +1 -1
- package/dist/default/ui/position-controller.js +76 -0
- package/dist/default/ui/position-controller.js.map +1 -0
- package/dist/default/ui/slider/slider-element.js +1 -1
- package/dist/default/ui/slider/slider-preview-element.js +1 -1
- package/dist/default/ui/time-slider/time-slider-element.js +5 -5
- package/dist/default/ui/time-slider/time-slider-element.js.map +1 -1
- package/dist/default/ui/tooltip/tooltip-element.js +8 -54
- package/dist/default/ui/tooltip/tooltip-element.js.map +1 -1
- package/dist/default/ui/volume-slider/volume-slider-element.js +1 -1
- package/dist/dev/_virtual/inline-css_src/define/audio/minimal-skin.js +1 -1
- 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 +1 -1
- package/dist/dev/_virtual/inline-css_src/define/audio/skin.js.map +1 -1
- package/dist/dev/_virtual/inline-css_src/define/video/minimal-skin.js +1 -1
- 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 +1 -1
- package/dist/dev/_virtual/inline-css_src/define/video/skin.js.map +1 -1
- package/dist/dev/define/audio/minimal-skin.css +23 -0
- package/dist/dev/define/audio/skin.css +23 -0
- package/dist/dev/define/audio/skin.d.ts.map +1 -1
- package/dist/dev/define/audio/skin.js +16 -0
- package/dist/dev/define/audio/skin.js.map +1 -1
- package/dist/dev/define/audio/ui.js +1 -0
- package/dist/dev/define/background/skin.js +1 -1
- package/dist/dev/define/media/mux-audio.d.ts +14 -0
- package/dist/dev/define/media/mux-audio.d.ts.map +1 -0
- package/dist/dev/define/media/mux-audio.js +13 -0
- package/dist/dev/define/media/mux-audio.js.map +1 -0
- package/dist/dev/define/skin-element.d.ts +1 -1
- package/dist/dev/define/skin-element.js +1 -1
- package/dist/dev/define/ui/hotkey.d.ts +9 -0
- package/dist/dev/define/ui/hotkey.d.ts.map +1 -0
- package/dist/dev/define/ui/hotkey.js +7 -0
- package/dist/dev/define/ui/hotkey.js.map +1 -0
- package/dist/dev/define/ui/slider.js +1 -1
- package/dist/dev/define/ui/time-slider.js +1 -1
- package/dist/dev/define/ui/volume-slider.js +1 -1
- package/dist/dev/define/video/minimal-skin.css +23 -0
- package/dist/dev/define/video/skin.css +23 -0
- package/dist/dev/define/video/skin.d.ts.map +1 -1
- package/dist/dev/define/video/skin.js +19 -0
- package/dist/dev/define/video/skin.js.map +1 -1
- package/dist/dev/define/video/ui.js +1 -0
- package/dist/dev/index.d.ts +6 -2
- package/dist/dev/index.js +5 -1
- package/dist/dev/media/container-element.d.ts +3 -0
- package/dist/dev/media/container-element.d.ts.map +1 -1
- package/dist/dev/media/container-element.js +16 -0
- package/dist/dev/media/container-element.js.map +1 -1
- package/dist/dev/media/mux-audio/index.d.ts +11 -0
- package/dist/dev/media/mux-audio/index.d.ts.map +1 -0
- package/dist/dev/media/mux-audio/index.js +18 -0
- package/dist/dev/media/mux-audio/index.js.map +1 -0
- package/dist/dev/player/context.d.ts +4 -2
- package/dist/dev/player/context.d.ts.map +1 -1
- package/dist/dev/player/context.js.map +1 -1
- package/dist/dev/ui/alert-dialog/alert-dialog-element.d.ts.map +1 -1
- package/dist/dev/ui/alert-dialog/alert-dialog-element.js +1 -0
- package/dist/dev/ui/alert-dialog/alert-dialog-element.js.map +1 -1
- package/dist/dev/ui/buffering-indicator/buffering-indicator-element.d.ts.map +1 -1
- package/dist/dev/ui/buffering-indicator/buffering-indicator-element.js +1 -0
- package/dist/dev/ui/buffering-indicator/buffering-indicator-element.js.map +1 -1
- package/dist/dev/ui/captions-button/captions-button-element.d.ts +1 -0
- package/dist/dev/ui/captions-button/captions-button-element.d.ts.map +1 -1
- package/dist/dev/ui/captions-button/captions-button-element.js +1 -0
- package/dist/dev/ui/captions-button/captions-button-element.js.map +1 -1
- package/dist/dev/ui/context-part-element.d.ts +1 -1
- package/dist/dev/ui/error-dialog/error-dialog-element.d.ts.map +1 -1
- package/dist/dev/ui/error-dialog/error-dialog-element.js +1 -0
- package/dist/dev/ui/error-dialog/error-dialog-element.js.map +1 -1
- package/dist/dev/ui/fullscreen-button/fullscreen-button-element.d.ts +1 -0
- package/dist/dev/ui/fullscreen-button/fullscreen-button-element.d.ts.map +1 -1
- package/dist/dev/ui/fullscreen-button/fullscreen-button-element.js +1 -0
- package/dist/dev/ui/fullscreen-button/fullscreen-button-element.js.map +1 -1
- package/dist/dev/ui/hotkey/aria-key-shortcuts-controller.d.ts +15 -0
- package/dist/dev/ui/hotkey/aria-key-shortcuts-controller.d.ts.map +1 -0
- package/dist/dev/ui/hotkey/aria-key-shortcuts-controller.js +28 -0
- package/dist/dev/ui/hotkey/aria-key-shortcuts-controller.js.map +1 -0
- package/dist/dev/ui/hotkey/hotkey-element.d.ts +21 -0
- package/dist/dev/ui/hotkey/hotkey-element.d.ts.map +1 -0
- package/dist/dev/ui/hotkey/hotkey-element.js +81 -0
- package/dist/dev/ui/hotkey/hotkey-element.js.map +1 -0
- package/dist/dev/ui/media-button-element.d.ts +2 -0
- package/dist/dev/ui/media-button-element.d.ts.map +1 -1
- package/dist/dev/ui/media-button-element.js +9 -1
- package/dist/dev/ui/media-button-element.js.map +1 -1
- package/dist/dev/ui/mute-button/mute-button-element.d.ts +1 -0
- package/dist/dev/ui/mute-button/mute-button-element.d.ts.map +1 -1
- package/dist/dev/ui/mute-button/mute-button-element.js +1 -0
- package/dist/dev/ui/mute-button/mute-button-element.js.map +1 -1
- package/dist/dev/ui/pip-button/pip-button-element.d.ts +1 -0
- package/dist/dev/ui/pip-button/pip-button-element.d.ts.map +1 -1
- package/dist/dev/ui/pip-button/pip-button-element.js +1 -0
- package/dist/dev/ui/pip-button/pip-button-element.js.map +1 -1
- package/dist/dev/ui/play-button/play-button-element.d.ts +1 -0
- package/dist/dev/ui/play-button/play-button-element.d.ts.map +1 -1
- package/dist/dev/ui/play-button/play-button-element.js +1 -0
- package/dist/dev/ui/play-button/play-button-element.js.map +1 -1
- package/dist/dev/ui/popover/popover-element.d.ts.map +1 -1
- package/dist/dev/ui/popover/popover-element.js +7 -55
- package/dist/dev/ui/popover/popover-element.js.map +1 -1
- package/dist/dev/ui/position-controller.js +76 -0
- package/dist/dev/ui/position-controller.js.map +1 -0
- package/dist/dev/ui/slider/slider-element.js +1 -1
- package/dist/dev/ui/slider/slider-preview-element.d.ts +21 -0
- package/dist/dev/ui/slider/slider-preview-element.d.ts.map +1 -0
- package/dist/dev/ui/slider/slider-preview-element.js +1 -1
- package/dist/dev/ui/time-slider/time-slider-element.d.ts +2 -2
- package/dist/dev/ui/time-slider/time-slider-element.js +5 -5
- package/dist/dev/ui/time-slider/time-slider-element.js.map +1 -1
- package/dist/dev/ui/tooltip/tooltip-element.d.ts.map +1 -1
- package/dist/dev/ui/tooltip/tooltip-element.js +8 -54
- package/dist/dev/ui/tooltip/tooltip-element.js.map +1 -1
- package/dist/dev/ui/volume-slider/volume-slider-element.js +1 -1
- package/package.json +8 -8
- package/cdn/context-BjrbhiSk.js.map +0 -1
- package/cdn/context-CdkhXQEw.js +0 -14
- package/cdn/context-CdkhXQEw.js.map +0 -1
- package/cdn/create-player-BqU8r8H4.js +0 -7
- package/cdn/create-player-BqU8r8H4.js.map +0 -1
- package/cdn/create-player-p9xCO0yC.js.map +0 -1
- package/cdn/delegate-B626t_Va.js +0 -2
- package/cdn/delegate-B626t_Va.js.map +0 -1
- package/cdn/delegate-BCw-LjP_.js +0 -56
- package/cdn/delegate-BCw-LjP_.js.map +0 -1
- package/cdn/media-props-mixin-Bd_N2HWy.js +0 -47
- package/cdn/media-props-mixin-Bd_N2HWy.js.map +0 -1
- package/cdn/media-props-mixin-Dc5dpCV4.js.map +0 -1
- package/cdn/native-hls-CjRvSXAJ.js +0 -2
- package/cdn/player-Cz9-_j7J.js +0 -2
- package/cdn/poster-2FPYTjyv.js +0 -2
- package/cdn/poster-2FPYTjyv.js.map +0 -1
- package/cdn/poster-gVT-J1-z.js +0 -215
- package/cdn/poster-gVT-J1-z.js.map +0 -1
- package/cdn/volume-slider-Co5X4Z1t.js.map +0 -1
- package/cdn/volume-slider-D18sXnIA.js +0 -8
- package/cdn/volume-slider-D18sXnIA.js.map +0 -1
- /package/cdn/{minimal-ui-BL9eBXck.js → minimal-ui-DXFwjywa.js} +0 -0
- /package/cdn/{minimal-ui-a1UOKF79.js → minimal-ui-jrmrxIOn.js} +0 -0
- /package/cdn/{ui-CFIgmrlA.js → ui-18UYtw8D.js} +0 -0
- /package/cdn/{ui-Ccp_LyCf.js → ui-8HVY3XSt.js} +0 -0
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { t as listen } from "../listen-w-Aun0yN.js";
|
|
2
|
-
import {
|
|
2
|
+
import { o as safeDefine } from "../context-CvJ83xGk.js";
|
|
3
3
|
import { t as anyAbortSignal } from "../abort-CzqmAxxH.js";
|
|
4
|
-
import {
|
|
5
|
-
import { t as MediaAttachMixin } from "../media-attach-mixin-
|
|
6
|
-
import { n as CustomVideoElement, t as MediaPropsMixin } from "../media-props-mixin-Dc5dpCV4.js";
|
|
4
|
+
import { i as DelegateMixin, r as CustomVideoElement, t as MediaPropsMixin } from "../media-props-mixin-CZLZIhGT.js";
|
|
5
|
+
import { t as MediaAttachMixin } from "../media-attach-mixin-CMH1ziXP.js";
|
|
7
6
|
|
|
8
7
|
//#region ../spf/dist/dev/core/abr/ewma.js
|
|
9
8
|
/**
|
|
@@ -981,19 +980,436 @@ function calculateBackBufferFlushPoint(segments, currentTime, config = DEFAULT_B
|
|
|
981
980
|
return segmentsBefore[segmentsToFlush].startTime;
|
|
982
981
|
}
|
|
983
982
|
|
|
983
|
+
//#endregion
|
|
984
|
+
//#region ../spf/dist/dev/core/machine.js
|
|
985
|
+
/**
|
|
986
|
+
* Provisions the shared mechanics for all machine-like primitives: a snapshot
|
|
987
|
+
* signal, an untracked state reader, and a transition function.
|
|
988
|
+
*
|
|
989
|
+
* Internal — consumed by `createMachineActor` and `createMachineReactor`. Not part of the
|
|
990
|
+
* public API.
|
|
991
|
+
*/
|
|
992
|
+
function createMachineCore(initialSnapshot) {
|
|
993
|
+
const snapshotSignal = signal(initialSnapshot);
|
|
994
|
+
const getState = () => untrack(() => snapshotSignal.get().value);
|
|
995
|
+
const transition = (to) => update(snapshotSignal, (current) => ({
|
|
996
|
+
...current,
|
|
997
|
+
value: to
|
|
998
|
+
}));
|
|
999
|
+
return {
|
|
1000
|
+
snapshotSignal,
|
|
1001
|
+
getState,
|
|
1002
|
+
transition
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
//#endregion
|
|
1007
|
+
//#region ../spf/dist/dev/core/create-machine-actor.js
|
|
1008
|
+
/**
|
|
1009
|
+
* Creates a message-driven actor from a declarative definition.
|
|
1010
|
+
*
|
|
1011
|
+
* The actor owns a reactive snapshot signal (state + context), an optional
|
|
1012
|
+
* runner, and dispatches incoming messages to per-state handlers. `'destroyed'`
|
|
1013
|
+
* is always the implicit terminal state — `destroy()` transitions there
|
|
1014
|
+
* unconditionally and all subsequent `send()` calls are no-ops.
|
|
1015
|
+
*
|
|
1016
|
+
* When a state declares `onSettled`, the framework calls `runner.whenSettled()`
|
|
1017
|
+
* after the handler returns. The runner owns the generation-token logic — if
|
|
1018
|
+
* new tasks are scheduled before the current batch settles, the callback is
|
|
1019
|
+
* automatically superseded.
|
|
1020
|
+
*
|
|
1021
|
+
* @example
|
|
1022
|
+
* const actor = createMachineActor({
|
|
1023
|
+
* runner: () => new SerialRunner(),
|
|
1024
|
+
* initial: 'idle',
|
|
1025
|
+
* context: {},
|
|
1026
|
+
* states: {
|
|
1027
|
+
* idle: {
|
|
1028
|
+
* on: {
|
|
1029
|
+
* load: (msg, { transition, runner }) => {
|
|
1030
|
+
* segments.forEach(s => runner.schedule(new Task(...)));
|
|
1031
|
+
* transition('loading');
|
|
1032
|
+
* }
|
|
1033
|
+
* }
|
|
1034
|
+
* },
|
|
1035
|
+
* loading: {
|
|
1036
|
+
* onSettled: 'idle',
|
|
1037
|
+
* on: {
|
|
1038
|
+
* load: (msg, { runner }) => {
|
|
1039
|
+
* runner.abortAll();
|
|
1040
|
+
* segments.forEach(s => runner.schedule(new Task(...)));
|
|
1041
|
+
* }
|
|
1042
|
+
* }
|
|
1043
|
+
* }
|
|
1044
|
+
* }
|
|
1045
|
+
* });
|
|
1046
|
+
*/
|
|
1047
|
+
function createMachineActor(def) {
|
|
1048
|
+
const runner = def.runner?.();
|
|
1049
|
+
const { snapshotSignal, getState, transition } = createMachineCore({
|
|
1050
|
+
value: def.initial,
|
|
1051
|
+
context: def.context
|
|
1052
|
+
});
|
|
1053
|
+
const getContext = () => untrack(() => snapshotSignal.get().context);
|
|
1054
|
+
const setContext = (context) => {
|
|
1055
|
+
update(snapshotSignal, { context });
|
|
1056
|
+
};
|
|
1057
|
+
return {
|
|
1058
|
+
get snapshot() {
|
|
1059
|
+
return snapshotSignal;
|
|
1060
|
+
},
|
|
1061
|
+
send(message) {
|
|
1062
|
+
const state = getState();
|
|
1063
|
+
if (state === "destroyed") return;
|
|
1064
|
+
const handler = def.states[state]?.on?.[message.type];
|
|
1065
|
+
if (!handler) return;
|
|
1066
|
+
handler(message, {
|
|
1067
|
+
context: getContext(),
|
|
1068
|
+
getContext,
|
|
1069
|
+
transition: (to) => transition(to),
|
|
1070
|
+
setContext,
|
|
1071
|
+
...runner ? { runner } : {}
|
|
1072
|
+
});
|
|
1073
|
+
const newState = getState();
|
|
1074
|
+
if (newState !== "destroyed") {
|
|
1075
|
+
const newStateDef = def.states[newState];
|
|
1076
|
+
if (newStateDef?.onSettled && runner) {
|
|
1077
|
+
const targetState = newStateDef.onSettled;
|
|
1078
|
+
runner.whenSettled(() => {
|
|
1079
|
+
if (getState() !== newState) return;
|
|
1080
|
+
transition(targetState);
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
destroy() {
|
|
1086
|
+
if (getState() === "destroyed") return;
|
|
1087
|
+
runner?.destroy();
|
|
1088
|
+
transition("destroyed");
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
//#endregion
|
|
1094
|
+
//#region ../spf/dist/dev/core/utils/generate-id.js
|
|
1095
|
+
/**
|
|
1096
|
+
* Generate unique ID for HAM objects.
|
|
1097
|
+
*
|
|
1098
|
+
* Uses timestamp + random number for sufficient uniqueness.
|
|
1099
|
+
* IDs are strings without decimals.
|
|
1100
|
+
*
|
|
1101
|
+
* @returns Unique string ID in format: timestamp-random
|
|
1102
|
+
*
|
|
1103
|
+
* @example
|
|
1104
|
+
* ```ts
|
|
1105
|
+
* const id = generateId(); // "1738423156789-542891"
|
|
1106
|
+
* ```
|
|
1107
|
+
*/
|
|
1108
|
+
function generateId() {
|
|
1109
|
+
return `${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
//#endregion
|
|
1113
|
+
//#region ../spf/dist/dev/core/task.js
|
|
1114
|
+
/**
|
|
1115
|
+
* Generic reusable task that wraps an async run function.
|
|
1116
|
+
*
|
|
1117
|
+
* Owns its own AbortController so it can always be aborted independently.
|
|
1118
|
+
* Optionally composes an external AbortSignal so that a parent's cancellation
|
|
1119
|
+
* propagates into the task's work without requiring the caller to track the
|
|
1120
|
+
* task separately.
|
|
1121
|
+
*
|
|
1122
|
+
* Ordering guarantee: `value` is written before `status` transitions to `'done'`;
|
|
1123
|
+
* `error` is written before `status` transitions to `'error'`. Any reader
|
|
1124
|
+
* observing `status === 'done'` is guaranteed `value` is already present.
|
|
1125
|
+
*/
|
|
1126
|
+
var Task = class {
|
|
1127
|
+
id;
|
|
1128
|
+
#runFn;
|
|
1129
|
+
#abortController = new AbortController();
|
|
1130
|
+
#signal;
|
|
1131
|
+
#status = "pending";
|
|
1132
|
+
#value = void 0;
|
|
1133
|
+
#error = void 0;
|
|
1134
|
+
constructor(runFn, config) {
|
|
1135
|
+
this.#runFn = runFn;
|
|
1136
|
+
const rawId = config?.id;
|
|
1137
|
+
this.id = typeof rawId === "function" ? rawId() : rawId ?? generateId();
|
|
1138
|
+
this.#signal = config?.signal ? anyAbortSignal([this.#abortController.signal, config.signal]) : this.#abortController.signal;
|
|
1139
|
+
}
|
|
1140
|
+
get status() {
|
|
1141
|
+
return this.#status;
|
|
1142
|
+
}
|
|
1143
|
+
get value() {
|
|
1144
|
+
return this.#value;
|
|
1145
|
+
}
|
|
1146
|
+
get error() {
|
|
1147
|
+
return this.#error;
|
|
1148
|
+
}
|
|
1149
|
+
async run() {
|
|
1150
|
+
this.#status = "running";
|
|
1151
|
+
try {
|
|
1152
|
+
const result = await this.#runFn(this.#signal);
|
|
1153
|
+
this.#value = result;
|
|
1154
|
+
this.#status = "done";
|
|
1155
|
+
return result;
|
|
1156
|
+
} catch (e) {
|
|
1157
|
+
this.#error = e;
|
|
1158
|
+
this.#status = "error";
|
|
1159
|
+
throw e;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
abort() {
|
|
1163
|
+
this.#abortController.abort();
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
/**
|
|
1167
|
+
* Runs tasks concurrently, deduplicated by task id.
|
|
1168
|
+
*
|
|
1169
|
+
* If a task with a given id is already in flight, subsequent schedule() calls
|
|
1170
|
+
* for that id are silently ignored until the first completes. Tasks are stored
|
|
1171
|
+
* so abortAll() can cancel any in-flight work (e.g. on engine cleanup).
|
|
1172
|
+
*/
|
|
1173
|
+
var ConcurrentRunner = class {
|
|
1174
|
+
#pending = /* @__PURE__ */ new Map();
|
|
1175
|
+
#settled = Promise.resolve();
|
|
1176
|
+
#resolveSettled = null;
|
|
1177
|
+
#destroyed = false;
|
|
1178
|
+
schedule(task) {
|
|
1179
|
+
if (this.#destroyed) return Promise.resolve();
|
|
1180
|
+
const existing = this.#pending.get(task.id);
|
|
1181
|
+
if (existing) return existing.promise;
|
|
1182
|
+
if (this.#pending.size === 0) this.#settled = new Promise((resolve) => {
|
|
1183
|
+
this.#resolveSettled = resolve;
|
|
1184
|
+
});
|
|
1185
|
+
const promise = task.run();
|
|
1186
|
+
promise.catch(() => {});
|
|
1187
|
+
const cleanup = () => {
|
|
1188
|
+
this.#pending.delete(task.id);
|
|
1189
|
+
if (this.#pending.size === 0) {
|
|
1190
|
+
this.#resolveSettled?.();
|
|
1191
|
+
this.#resolveSettled = null;
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
promise.then(cleanup, cleanup);
|
|
1195
|
+
this.#pending.set(task.id, {
|
|
1196
|
+
task,
|
|
1197
|
+
promise
|
|
1198
|
+
});
|
|
1199
|
+
return promise;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Registers a callback to fire when all currently in-flight tasks settle.
|
|
1203
|
+
* If the runner is already idle, the callback is never called. If abortAll()
|
|
1204
|
+
* is called before the batch settles, the callback is superseded and silently
|
|
1205
|
+
* dropped — no stale callbacks, no generation token required by the caller.
|
|
1206
|
+
*/
|
|
1207
|
+
whenSettled(callback) {
|
|
1208
|
+
if (this.#pending.size === 0) return;
|
|
1209
|
+
const captured = this.#settled;
|
|
1210
|
+
captured.then(() => {
|
|
1211
|
+
if (this.#settled !== captured) return;
|
|
1212
|
+
callback();
|
|
1213
|
+
}, () => {});
|
|
1214
|
+
}
|
|
1215
|
+
abortAll() {
|
|
1216
|
+
for (const { task } of this.#pending.values()) task.abort();
|
|
1217
|
+
this.#pending.clear();
|
|
1218
|
+
this.#resolveSettled?.();
|
|
1219
|
+
this.#resolveSettled = null;
|
|
1220
|
+
this.#settled = Promise.resolve();
|
|
1221
|
+
}
|
|
1222
|
+
destroy() {
|
|
1223
|
+
this.#destroyed = true;
|
|
1224
|
+
this.abortAll();
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
/**
|
|
1228
|
+
* Runs tasks one at a time in submission order.
|
|
1229
|
+
*
|
|
1230
|
+
* Each schedule() call returns a Promise that resolves or rejects with the
|
|
1231
|
+
* task's result when it is eventually executed. Tasks wait in queue until the
|
|
1232
|
+
* prior task completes.
|
|
1233
|
+
*
|
|
1234
|
+
* Serialization is achieved by chaining each task's run() onto the tail of a
|
|
1235
|
+
* shared promise chain — no explicit queue or drain loop needed.
|
|
1236
|
+
*
|
|
1237
|
+
* abortAll() aborts all pending (not yet started) tasks and the currently
|
|
1238
|
+
* in-flight task. Pending tasks still run briefly but receive an aborted
|
|
1239
|
+
* signal and are expected to exit early.
|
|
1240
|
+
*/
|
|
1241
|
+
var SerialRunner = class {
|
|
1242
|
+
#chain = Promise.resolve();
|
|
1243
|
+
#pending = /* @__PURE__ */ new Set();
|
|
1244
|
+
#current = null;
|
|
1245
|
+
#destroyed = false;
|
|
1246
|
+
schedule(task) {
|
|
1247
|
+
if (this.#destroyed) return Promise.resolve();
|
|
1248
|
+
const t = task;
|
|
1249
|
+
this.#pending.add(t);
|
|
1250
|
+
const result = this.#chain.then(() => {
|
|
1251
|
+
this.#pending.delete(t);
|
|
1252
|
+
this.#current = t;
|
|
1253
|
+
return task.run();
|
|
1254
|
+
}).finally(() => {
|
|
1255
|
+
this.#current = null;
|
|
1256
|
+
});
|
|
1257
|
+
this.#chain = result.then(() => {}, () => {});
|
|
1258
|
+
return result;
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* A promise that resolves when all currently-scheduled tasks have settled.
|
|
1262
|
+
* Use the reference as a generation token: capture it after scheduling a
|
|
1263
|
+
* batch, then check identity in the resolution callback to detect whether
|
|
1264
|
+
* a subsequent abortAll() + new batch has superseded this one.
|
|
1265
|
+
*/
|
|
1266
|
+
get settled() {
|
|
1267
|
+
return this.#chain;
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Registers a callback to fire when all currently-pending tasks settle.
|
|
1271
|
+
* If the runner is already idle (no pending or running tasks), the callback
|
|
1272
|
+
* is never called. If new tasks are scheduled before the current batch
|
|
1273
|
+
* settles, the callback is superseded and silently dropped — no stale
|
|
1274
|
+
* callbacks, no generation token required by the caller.
|
|
1275
|
+
*/
|
|
1276
|
+
whenSettled(callback) {
|
|
1277
|
+
if (this.#pending.size === 0 && this.#current === null) return;
|
|
1278
|
+
const currentChain = this.#chain;
|
|
1279
|
+
currentChain.then(() => {
|
|
1280
|
+
if (this.#chain !== currentChain) return;
|
|
1281
|
+
callback();
|
|
1282
|
+
}, () => {});
|
|
1283
|
+
}
|
|
1284
|
+
/** Aborts and clears queued tasks without touching the in-flight task. */
|
|
1285
|
+
abortPending() {
|
|
1286
|
+
for (const task of this.#pending) task.abort();
|
|
1287
|
+
this.#pending.clear();
|
|
1288
|
+
}
|
|
1289
|
+
abortAll() {
|
|
1290
|
+
this.abortPending();
|
|
1291
|
+
this.#current?.abort();
|
|
1292
|
+
}
|
|
1293
|
+
destroy() {
|
|
1294
|
+
this.#destroyed = true;
|
|
1295
|
+
this.abortAll();
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
|
|
984
1299
|
//#endregion
|
|
985
1300
|
//#region ../spf/dist/dev/dom/features/segment-loader-actor.js
|
|
986
1301
|
/**
|
|
1302
|
+
* Resolves when the SourceBufferActor snapshot reaches 'idle'.
|
|
1303
|
+
* Rejects if the signal is aborted or the actor is destroyed.
|
|
1304
|
+
*
|
|
1305
|
+
* Used to sequence SourceBufferActor operations without awaiting send()
|
|
1306
|
+
* directly — send() is fire-and-forget; callers observe completion via
|
|
1307
|
+
* state transition.
|
|
1308
|
+
*/
|
|
1309
|
+
function waitForIdle(snapshot, signal) {
|
|
1310
|
+
return new Promise((resolve, reject) => {
|
|
1311
|
+
if (snapshot.get().value === "idle") {
|
|
1312
|
+
resolve();
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
if (snapshot.get().value === "destroyed") {
|
|
1316
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (signal.aborted) {
|
|
1320
|
+
reject(signal.reason);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
let stop;
|
|
1324
|
+
const cleanup = (fn) => {
|
|
1325
|
+
stop?.();
|
|
1326
|
+
signal.removeEventListener("abort", onAbort);
|
|
1327
|
+
fn();
|
|
1328
|
+
};
|
|
1329
|
+
const onAbort = () => cleanup(() => reject(signal.reason));
|
|
1330
|
+
stop = effect(() => {
|
|
1331
|
+
const value = snapshot.get().value;
|
|
1332
|
+
if (value === "idle") cleanup(resolve);
|
|
1333
|
+
else if (value === "destroyed") cleanup(() => reject(new DOMException("Aborted", "AbortError")));
|
|
1334
|
+
});
|
|
1335
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Wraps a LoadTask descriptor into a Task that fetches (if needed) and
|
|
1340
|
+
* forwards to SourceBufferActor. Updates in-flight context around async
|
|
1341
|
+
* operations so the loading handler can make accurate continue/preempt
|
|
1342
|
+
* decisions at any point.
|
|
1343
|
+
*/
|
|
1344
|
+
function makeLoadTask(op, { getContext, setContext, fetchBytes, sourceBufferActor }) {
|
|
1345
|
+
return new Task(async (taskSignal) => {
|
|
1346
|
+
if (taskSignal.aborted) return;
|
|
1347
|
+
if (op.type === "remove") {
|
|
1348
|
+
sourceBufferActor.send(op);
|
|
1349
|
+
await waitForIdle(sourceBufferActor.snapshot, taskSignal);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (op.type === "append-init") {
|
|
1353
|
+
setContext({
|
|
1354
|
+
...getContext(),
|
|
1355
|
+
inFlightInitTrackId: op.meta.trackId
|
|
1356
|
+
});
|
|
1357
|
+
try {
|
|
1358
|
+
const data = await fetchBytes(op, {
|
|
1359
|
+
signal: taskSignal,
|
|
1360
|
+
minChunkSize: Infinity
|
|
1361
|
+
});
|
|
1362
|
+
if (!taskSignal.aborted) {
|
|
1363
|
+
sourceBufferActor.send({
|
|
1364
|
+
type: "append-init",
|
|
1365
|
+
data,
|
|
1366
|
+
meta: op.meta
|
|
1367
|
+
});
|
|
1368
|
+
await waitForIdle(sourceBufferActor.snapshot, taskSignal);
|
|
1369
|
+
}
|
|
1370
|
+
} finally {
|
|
1371
|
+
setContext({
|
|
1372
|
+
...getContext(),
|
|
1373
|
+
inFlightInitTrackId: null
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
setContext({
|
|
1379
|
+
...getContext(),
|
|
1380
|
+
inFlightSegmentId: op.meta.id
|
|
1381
|
+
});
|
|
1382
|
+
try {
|
|
1383
|
+
const stream = await fetchBytes(op, { signal: taskSignal });
|
|
1384
|
+
if (!taskSignal.aborted) {
|
|
1385
|
+
sourceBufferActor.send({
|
|
1386
|
+
type: "append-segment",
|
|
1387
|
+
data: stream,
|
|
1388
|
+
meta: op.meta
|
|
1389
|
+
});
|
|
1390
|
+
await waitForIdle(sourceBufferActor.snapshot, taskSignal);
|
|
1391
|
+
}
|
|
1392
|
+
} finally {
|
|
1393
|
+
setContext({
|
|
1394
|
+
...getContext(),
|
|
1395
|
+
inFlightSegmentId: null
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
987
1401
|
* Creates a SegmentLoaderActor for one track type (video or audio).
|
|
988
1402
|
*
|
|
989
1403
|
* Receives load assignments via `send()` and owns all execution: planning,
|
|
990
1404
|
* removes, fetches, and appends. Coordinates with the SourceBufferActor for
|
|
991
1405
|
* all physical SourceBuffer operations.
|
|
992
1406
|
*
|
|
993
|
-
* Planning (Cases 1–3) happens in `
|
|
994
|
-
* an ordered LoadTask list. The runner drains that list
|
|
995
|
-
* message arrives mid-run,
|
|
996
|
-
*
|
|
1407
|
+
* Planning (Cases 1–3) happens in the `load` handler on every incoming
|
|
1408
|
+
* message, producing an ordered LoadTask list. The runner drains that list
|
|
1409
|
+
* sequentially via SerialRunner. When a new message arrives mid-run, the
|
|
1410
|
+
* handler replans and either continues the in-flight operation (abortPending
|
|
1411
|
+
* + schedule new remainder) or preempts it (abortAll + cancel SourceBuffer
|
|
1412
|
+
* if needed + schedule new plan).
|
|
997
1413
|
*
|
|
998
1414
|
* @param sourceBufferActor - Shared SourceBufferActor reference (not owned)
|
|
999
1415
|
* @param fetchBytes - Tracked fetch closure (owns throughput sampling for segments).
|
|
@@ -1001,19 +1417,13 @@ function calculateBackBufferFlushPoint(segments, currentTime, config = DEFAULT_B
|
|
|
1001
1417
|
* so the entire body accumulates as one chunk before appending.
|
|
1002
1418
|
*/
|
|
1003
1419
|
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
1420
|
const getBufferedSegments = (allSegments) => {
|
|
1011
1421
|
const bufferedIds = new Set(sourceBufferActor.snapshot.get().context.segments.filter((s) => !s.partial).map((s) => s.id));
|
|
1012
1422
|
return allSegments.filter((s) => bufferedIds.has(s.id));
|
|
1013
1423
|
};
|
|
1014
1424
|
/**
|
|
1015
1425
|
* Translate a load message into an ordered LoadTask list based on committed
|
|
1016
|
-
* actor state. In-flight awareness is handled separately in
|
|
1426
|
+
* actor state. In-flight awareness is handled separately in the load handler.
|
|
1017
1427
|
*
|
|
1018
1428
|
* @todo Rename alongside LoadTask (e.g. planOps).
|
|
1019
1429
|
*
|
|
@@ -1075,102 +1485,51 @@ function createSegmentLoaderActor(sourceBufferActor, fetchBytes) {
|
|
|
1075
1485
|
}
|
|
1076
1486
|
return tasks;
|
|
1077
1487
|
};
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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
|
-
}
|
|
1124
|
-
};
|
|
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;
|
|
1150
|
-
}
|
|
1151
|
-
abortController = null;
|
|
1152
|
-
running = false;
|
|
1488
|
+
const scheduleAll = (tasks, { getContext, setContext, runner }) => {
|
|
1489
|
+
tasks.forEach((op) => {
|
|
1490
|
+
runner.schedule(makeLoadTask(op, {
|
|
1491
|
+
getContext,
|
|
1492
|
+
setContext,
|
|
1493
|
+
fetchBytes,
|
|
1494
|
+
sourceBufferActor
|
|
1495
|
+
})).then(void 0, (e) => {
|
|
1496
|
+
if (e instanceof Error && e.name === "AbortError") return;
|
|
1497
|
+
console.error("Unexpected error in segment loader:", e);
|
|
1498
|
+
runner.abortPending();
|
|
1499
|
+
});
|
|
1500
|
+
});
|
|
1153
1501
|
};
|
|
1154
|
-
return {
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1502
|
+
return createMachineActor({
|
|
1503
|
+
runner: () => new SerialRunner(),
|
|
1504
|
+
initial: "idle",
|
|
1505
|
+
context: {
|
|
1506
|
+
inFlightInitTrackId: null,
|
|
1507
|
+
inFlightSegmentId: null
|
|
1508
|
+
},
|
|
1509
|
+
states: {
|
|
1510
|
+
idle: { on: { load: (msg, ctx) => {
|
|
1511
|
+
const allTasks = planTasks(msg);
|
|
1159
1512
|
if (allTasks.length === 0) return;
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1513
|
+
ctx.transition("loading");
|
|
1514
|
+
scheduleAll(allTasks, ctx);
|
|
1515
|
+
} } },
|
|
1516
|
+
loading: {
|
|
1517
|
+
onSettled: "idle",
|
|
1518
|
+
on: { load: (msg, ctx) => {
|
|
1519
|
+
const { context, runner } = ctx;
|
|
1520
|
+
const allTasks = planTasks(msg);
|
|
1521
|
+
if (context.inFlightSegmentId !== null && allTasks.some((t) => t.type === "append-segment" && t.meta.id === context.inFlightSegmentId) || context.inFlightInitTrackId !== null && allTasks.some((t) => t.type === "append-init" && t.meta.trackId === context.inFlightInitTrackId)) {
|
|
1522
|
+
runner.abortPending();
|
|
1523
|
+
scheduleAll(allTasks.filter((t) => !(t.type === "append-segment" && t.meta.id === context.inFlightSegmentId) && !(t.type === "append-init" && t.meta.trackId === context.inFlightInitTrackId)), ctx);
|
|
1524
|
+
} else {
|
|
1525
|
+
runner.abortAll();
|
|
1526
|
+
if (context.inFlightSegmentId !== null || context.inFlightInitTrackId !== null && allTasks.some((t) => t.type === "append-init" && t.meta.trackId !== context.inFlightInitTrackId)) sourceBufferActor.send({ type: "cancel" });
|
|
1527
|
+
scheduleAll(allTasks, ctx);
|
|
1528
|
+
}
|
|
1529
|
+
} }
|
|
1167
1530
|
}
|
|
1168
|
-
},
|
|
1169
|
-
destroy() {
|
|
1170
|
-
destroyed = true;
|
|
1171
|
-
abortController?.abort();
|
|
1172
1531
|
}
|
|
1173
|
-
};
|
|
1532
|
+
});
|
|
1174
1533
|
}
|
|
1175
1534
|
|
|
1176
1535
|
//#endregion
|
|
@@ -1409,157 +1768,308 @@ function destroyVttParser() {
|
|
|
1409
1768
|
}
|
|
1410
1769
|
|
|
1411
1770
|
//#endregion
|
|
1412
|
-
//#region ../spf/dist/dev/
|
|
1413
|
-
const
|
|
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
|
-
};
|
|
1429
|
-
/**
|
|
1430
|
-
* Load text track cues task (composite - orchestrates VTT segment subtasks).
|
|
1431
|
-
*/
|
|
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);
|
|
1458
|
-
}
|
|
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
|
-
};
|
|
1771
|
+
//#region ../spf/dist/dev/core/create-machine-reactor.js
|
|
1772
|
+
const toArray = (x) => x === void 0 ? [] : Array.isArray(x) ? x : [x];
|
|
1465
1773
|
/**
|
|
1466
|
-
*
|
|
1467
|
-
*/
|
|
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);
|
|
1473
|
-
}
|
|
1474
|
-
/**
|
|
1475
|
-
* Get the browser's TextTrack object for the selected text track.
|
|
1774
|
+
* Creates a reactive Reactor from a declarative definition.
|
|
1476
1775
|
*
|
|
1477
|
-
*
|
|
1478
|
-
*
|
|
1776
|
+
* A Reactor is driven by subscriptions to external signals rather than
|
|
1777
|
+
* imperative messages. Each state holds an array of effect functions —
|
|
1778
|
+
* every element becomes one independent `effect()` call gated on that state,
|
|
1779
|
+
* with its own dependency tracking and cleanup lifecycle.
|
|
1479
1780
|
*
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1781
|
+
* `'destroying'` and `'destroyed'` are always implicit terminal states.
|
|
1782
|
+
* `destroy()` transitions through both in sequence: `'destroying'` first (for
|
|
1783
|
+
* potential async teardown in a future extension), then immediately `'destroyed'`
|
|
1784
|
+
* for the synchronous base case. Active effect cleanups fire via disposal.
|
|
1482
1785
|
*
|
|
1483
|
-
* @
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
1786
|
+
* @example
|
|
1787
|
+
* const reactor = createMachineReactor({
|
|
1788
|
+
* initial: 'waiting',
|
|
1789
|
+
* monitor: () => srcSignal.get() ? 'active' : 'waiting',
|
|
1790
|
+
* states: {
|
|
1791
|
+
* active: {
|
|
1792
|
+
* // entry: runs once on state entry; fn body is automatically untracked.
|
|
1793
|
+
* entry: () => listen(el, 'play', handler),
|
|
1794
|
+
* // effects: re-runs whenever tracked signals change.
|
|
1795
|
+
* effects: () => { currentTimeSignal.get(); return cleanup; },
|
|
1796
|
+
* },
|
|
1797
|
+
* waiting: {},
|
|
1798
|
+
* }
|
|
1799
|
+
* });
|
|
1486
1800
|
*/
|
|
1487
|
-
function
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
|
|
1801
|
+
function createMachineReactor(def) {
|
|
1802
|
+
const { snapshotSignal, getState, transition } = createMachineCore({ value: def.initial });
|
|
1803
|
+
const effectDisposals = [];
|
|
1804
|
+
const wrapResult = (result) => {
|
|
1805
|
+
if (!result) return void 0;
|
|
1806
|
+
if (typeof result === "function") return result;
|
|
1807
|
+
return () => result.abort();
|
|
1808
|
+
};
|
|
1809
|
+
const untracked = (baseCall) => () => untrack(baseCall);
|
|
1810
|
+
const isTerminal = (snapshot) => snapshot.value === "destroying" || snapshot.value === "destroyed";
|
|
1811
|
+
const descriptors = [...toArray(def.monitor).map((fn) => ({
|
|
1812
|
+
fn: () => {
|
|
1813
|
+
const target = fn();
|
|
1814
|
+
if (target !== getState()) transition(target);
|
|
1815
|
+
},
|
|
1816
|
+
shouldSkip: isTerminal
|
|
1817
|
+
})), ...Object.entries(def.states).flatMap(([state, stateDef]) => {
|
|
1818
|
+
const isNotState = (snapshot) => snapshot.value !== state;
|
|
1819
|
+
return [...toArray(stateDef.entry).map((fn) => ({
|
|
1820
|
+
fn,
|
|
1821
|
+
shouldSkip: isNotState,
|
|
1822
|
+
toFnCall: untracked
|
|
1823
|
+
})), ...toArray(stateDef.effects).map((fn) => ({
|
|
1824
|
+
fn,
|
|
1825
|
+
shouldSkip: isNotState
|
|
1826
|
+
}))];
|
|
1827
|
+
})];
|
|
1828
|
+
const toEffect = ({ fn, shouldSkip, toFnCall = (baseCall) => baseCall }) => effect(() => {
|
|
1829
|
+
if (shouldSkip(snapshotSignal.get())) return;
|
|
1830
|
+
const baseCall = () => fn();
|
|
1831
|
+
return wrapResult(toFnCall(baseCall)());
|
|
1832
|
+
});
|
|
1833
|
+
effectDisposals.push(...descriptors.map(toEffect));
|
|
1834
|
+
return {
|
|
1835
|
+
get snapshot() {
|
|
1836
|
+
return snapshotSignal;
|
|
1837
|
+
},
|
|
1838
|
+
destroy() {
|
|
1839
|
+
const state = getState();
|
|
1840
|
+
if (state === "destroying" || state === "destroyed") return;
|
|
1841
|
+
transition("destroying");
|
|
1842
|
+
transition("destroyed");
|
|
1843
|
+
for (const dispose of effectDisposals) dispose();
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1491
1846
|
}
|
|
1847
|
+
|
|
1848
|
+
//#endregion
|
|
1849
|
+
//#region ../spf/dist/dev/dom/features/text-track-segment-loader-actor.js
|
|
1492
1850
|
/**
|
|
1493
|
-
*
|
|
1851
|
+
* Loads VTT segments for a text track and delegates cue management to a
|
|
1852
|
+
* TextTracksActor. Mirrors the SegmentLoaderActor/SourceBufferActor pattern
|
|
1853
|
+
* for the text track equivalent.
|
|
1494
1854
|
*
|
|
1495
|
-
*
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
* - Track element exists for selected track
|
|
1855
|
+
* Planning is done in the load handler: segments already recorded in
|
|
1856
|
+
* TextTracksActor's context are skipped. Each load preempts in-flight work
|
|
1857
|
+
* via abortAll() before scheduling fresh tasks.
|
|
1499
1858
|
*/
|
|
1500
|
-
function
|
|
1501
|
-
|
|
1859
|
+
function createTextTrackSegmentLoaderActor(textTracksActor) {
|
|
1860
|
+
const runner = new SerialRunner();
|
|
1861
|
+
let destroyed = false;
|
|
1862
|
+
return {
|
|
1863
|
+
send({ track, currentTime }) {
|
|
1864
|
+
if (destroyed) return;
|
|
1865
|
+
const trackId = track.id;
|
|
1866
|
+
const bufferedSegments = untrack(() => textTracksActor.snapshot.get().context.segments[trackId] ?? []);
|
|
1867
|
+
const segmentsToLoad = getSegmentsToLoad(track.segments, bufferedSegments, currentTime);
|
|
1868
|
+
runner.abortAll();
|
|
1869
|
+
for (const segment of segmentsToLoad) runner.schedule(new Task(async (signal) => {
|
|
1870
|
+
if (signal.aborted) return;
|
|
1871
|
+
try {
|
|
1872
|
+
const cues = await parseVttSegment(segment.url);
|
|
1873
|
+
if (signal.aborted) return;
|
|
1874
|
+
textTracksActor.send({
|
|
1875
|
+
type: "add-cues",
|
|
1876
|
+
meta: {
|
|
1877
|
+
trackId,
|
|
1878
|
+
id: segment.id,
|
|
1879
|
+
startTime: segment.startTime,
|
|
1880
|
+
duration: segment.duration
|
|
1881
|
+
},
|
|
1882
|
+
cues
|
|
1883
|
+
});
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
console.error("Failed to load VTT segment:", error);
|
|
1886
|
+
}
|
|
1887
|
+
}));
|
|
1888
|
+
},
|
|
1889
|
+
destroy() {
|
|
1890
|
+
if (destroyed) return;
|
|
1891
|
+
destroyed = true;
|
|
1892
|
+
runner.destroy();
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1502
1895
|
}
|
|
1896
|
+
|
|
1897
|
+
//#endregion
|
|
1898
|
+
//#region ../spf/dist/dev/core/create-transition-actor.js
|
|
1503
1899
|
/**
|
|
1504
|
-
*
|
|
1900
|
+
* Creates a reducer-shaped actor from an initial context and a reducer function.
|
|
1505
1901
|
*
|
|
1506
|
-
*
|
|
1507
|
-
*
|
|
1508
|
-
* -
|
|
1509
|
-
*
|
|
1902
|
+
* The reducer receives the current context and a message and returns the next
|
|
1903
|
+
* context. Returning the same reference (by identity) skips the signal update —
|
|
1904
|
+
* so early-returning `context` unchanged is both the no-op and the optimization.
|
|
1905
|
+
*
|
|
1906
|
+
* Side effects (e.g. DOM mutations) may be performed inside the reducer.
|
|
1907
|
+
* They run synchronously before the signal is updated.
|
|
1908
|
+
*
|
|
1909
|
+
* @example
|
|
1910
|
+
* const actor = createTransitionActor(
|
|
1911
|
+
* { count: 0 },
|
|
1912
|
+
* (context, message: { type: 'increment' }) => ({ count: context.count + 1 })
|
|
1913
|
+
* );
|
|
1510
1914
|
*/
|
|
1511
|
-
function
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1915
|
+
function createTransitionActor(initialContext, reducer) {
|
|
1916
|
+
const { snapshotSignal, getState, transition } = createMachineCore({
|
|
1917
|
+
value: "active",
|
|
1918
|
+
context: initialContext
|
|
1919
|
+
});
|
|
1920
|
+
const getContext = () => untrack(() => snapshotSignal.get().context);
|
|
1921
|
+
const setContext = (context) => update(snapshotSignal, { context });
|
|
1922
|
+
return {
|
|
1923
|
+
get snapshot() {
|
|
1924
|
+
return snapshotSignal;
|
|
1925
|
+
},
|
|
1926
|
+
send(message) {
|
|
1927
|
+
if (getState() === "destroyed") return;
|
|
1928
|
+
const context = getContext();
|
|
1929
|
+
const newContext = reducer(context, message);
|
|
1930
|
+
if (newContext !== context) setContext(newContext);
|
|
1931
|
+
},
|
|
1932
|
+
destroy() {
|
|
1933
|
+
if (getState() === "destroyed") return;
|
|
1934
|
+
transition("destroyed");
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
//#endregion
|
|
1940
|
+
//#region ../spf/dist/dev/dom/features/text-tracks-actor.js
|
|
1941
|
+
function isDuplicateCue(cue, existing) {
|
|
1942
|
+
return existing.some((r) => r.startTime === cue.startTime && r.endTime === cue.endTime && r.text === cue.text);
|
|
1943
|
+
}
|
|
1944
|
+
/** TextTrack actor: wraps all text tracks on a media element, owns cue operations. */
|
|
1945
|
+
function createTextTracksActor(mediaElement) {
|
|
1946
|
+
return createTransitionActor({
|
|
1947
|
+
loaded: {},
|
|
1948
|
+
segments: {}
|
|
1949
|
+
}, (context, message) => {
|
|
1950
|
+
const { meta, cues } = message;
|
|
1951
|
+
const { trackId, id: segmentId, startTime, duration } = meta;
|
|
1952
|
+
const textTrack = Array.from(mediaElement.textTracks).find((t) => t.id === trackId);
|
|
1953
|
+
if (!textTrack) return context;
|
|
1954
|
+
const existingCues = context.loaded[trackId] ?? [];
|
|
1955
|
+
const existingSegments = context.segments[trackId] ?? [];
|
|
1956
|
+
const prunedCues = cues.filter((cue) => !isDuplicateCue(cue, existingCues));
|
|
1957
|
+
const segmentAlreadyLoaded = existingSegments.some((s) => s.id === segmentId);
|
|
1958
|
+
if (prunedCues.length === 0 && segmentAlreadyLoaded) return context;
|
|
1959
|
+
for (const cue of prunedCues) textTrack.addCue(cue);
|
|
1960
|
+
return {
|
|
1961
|
+
...context,
|
|
1962
|
+
loaded: {
|
|
1963
|
+
...context.loaded,
|
|
1964
|
+
[trackId]: [...existingCues, ...prunedCues]
|
|
1965
|
+
},
|
|
1966
|
+
segments: segmentAlreadyLoaded ? context.segments : {
|
|
1967
|
+
...context.segments,
|
|
1968
|
+
[trackId]: [...existingSegments, {
|
|
1969
|
+
id: segmentId,
|
|
1970
|
+
startTime,
|
|
1971
|
+
duration
|
|
1972
|
+
}]
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
//#endregion
|
|
1979
|
+
//#region ../spf/dist/dev/dom/features/load-text-track-cues.js
|
|
1980
|
+
function getTextTracks(presentation) {
|
|
1981
|
+
return presentation?.selectionSets?.find((s) => s.type === "text")?.switchingSets[0]?.tracks;
|
|
1982
|
+
}
|
|
1983
|
+
function findSelectedTrack(state) {
|
|
1984
|
+
const track = getTextTracks(state.presentation)?.find((t) => t.id === state.selectedTextTrackId);
|
|
1985
|
+
return track && isResolvedTrack(track) ? track : void 0;
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Derives the correct state from current state and owners.
|
|
1989
|
+
*
|
|
1990
|
+
* States are mutually exclusive and exhaustive:
|
|
1991
|
+
* - `'preconditions-unmet'`: no mediaElement, or no resolved presentation with text tracks
|
|
1992
|
+
* - `'setting-up'`: preconditions met; actors not yet in owners
|
|
1993
|
+
* - `'pending'`: actors alive; no selection, or selected track not yet resolved/in DOM
|
|
1994
|
+
* - `'monitoring-for-loads'`: selected track resolved, in DOM — ready to dispatch load messages
|
|
1995
|
+
*/
|
|
1996
|
+
function deriveState$2(state, owners) {
|
|
1997
|
+
if (!owners.mediaElement || !getTextTracks(state.presentation)?.length) return "preconditions-unmet";
|
|
1998
|
+
if (!owners.textTracksActor || !owners.segmentLoaderActor) return "setting-up";
|
|
1999
|
+
const track = findSelectedTrack(state);
|
|
2000
|
+
if (!track || track.segments.length === 0) return "pending";
|
|
2001
|
+
if (!Array.from(owners.mediaElement.textTracks).some((t) => t.id === state.selectedTextTrackId)) return "pending";
|
|
2002
|
+
return "monitoring-for-loads";
|
|
2003
|
+
}
|
|
2004
|
+
function teardownActors(owners) {
|
|
2005
|
+
const { textTracksActor, segmentLoaderActor } = untrack(() => owners.get());
|
|
2006
|
+
if (!textTracksActor && !segmentLoaderActor) return;
|
|
2007
|
+
textTracksActor?.destroy();
|
|
2008
|
+
segmentLoaderActor?.destroy();
|
|
2009
|
+
update(owners, {
|
|
2010
|
+
textTracksActor: void 0,
|
|
2011
|
+
segmentLoaderActor: void 0
|
|
2012
|
+
});
|
|
1517
2013
|
}
|
|
1518
2014
|
/**
|
|
1519
|
-
*
|
|
2015
|
+
* Text track cue loading orchestration.
|
|
2016
|
+
*
|
|
2017
|
+
* A single `always` monitor keeps the reactor in sync with conditions.
|
|
2018
|
+
* Actor lifecycle is managed across two states:
|
|
1520
2019
|
*
|
|
1521
|
-
*
|
|
1522
|
-
*
|
|
1523
|
-
* -
|
|
1524
|
-
*
|
|
2020
|
+
* - **`'setting-up'`** — creates `TextTracksActor` and `TextTrackSegmentLoaderActor`
|
|
2021
|
+
* and writes them to owners. Entry resets any stale actors first.
|
|
2022
|
+
* - **`'preconditions-unmet'`** — destroys any actors in owners and resets them to
|
|
2023
|
+
* `undefined`. Handles all paths back from active states.
|
|
2024
|
+
* - **`'monitoring-for-loads'`** — reactive dispatch: re-runs whenever `state`
|
|
2025
|
+
* changes (selection, currentTime, presentation) and sends a `load` message to
|
|
2026
|
+
* the segment loader.
|
|
1525
2027
|
*
|
|
1526
|
-
*
|
|
1527
|
-
*
|
|
1528
|
-
*
|
|
2028
|
+
* **Destroy note:** actors written to owners are NOT auto-destroyed when
|
|
2029
|
+
* `reactor.destroy()` is called. Callers must read them from owners and destroy
|
|
2030
|
+
* them explicitly alongside the reactor.
|
|
1529
2031
|
*
|
|
1530
2032
|
* @example
|
|
1531
|
-
* const
|
|
2033
|
+
* const reactor = loadTextTrackCues({ state, owners });
|
|
2034
|
+
* // later:
|
|
2035
|
+
* const { textTracksActor, segmentLoaderActor } = owners.get();
|
|
2036
|
+
* textTracksActor?.destroy();
|
|
2037
|
+
* segmentLoaderActor?.destroy();
|
|
2038
|
+
* reactor.destroy();
|
|
1532
2039
|
*/
|
|
1533
2040
|
function loadTextTrackCues({ state, owners }) {
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
2041
|
+
const derivedStateSignal = computed(() => deriveState$2(state.get(), owners.get()));
|
|
2042
|
+
const currentTimeSignal = computed(() => state.get().currentTime ?? 0);
|
|
2043
|
+
const selectedTrackSignal = computed(() => findSelectedTrack(state.get()));
|
|
2044
|
+
return createMachineReactor({
|
|
2045
|
+
initial: "preconditions-unmet",
|
|
2046
|
+
monitor: () => derivedStateSignal.get(),
|
|
2047
|
+
states: {
|
|
2048
|
+
"preconditions-unmet": { entry: () => {
|
|
2049
|
+
teardownActors(owners);
|
|
2050
|
+
} },
|
|
2051
|
+
"setting-up": { entry: () => {
|
|
2052
|
+
teardownActors(owners);
|
|
2053
|
+
const mediaElement = owners.get().mediaElement;
|
|
2054
|
+
const textTracksActor = createTextTracksActor(mediaElement);
|
|
2055
|
+
update(owners, {
|
|
2056
|
+
textTracksActor,
|
|
2057
|
+
segmentLoaderActor: createTextTrackSegmentLoaderActor(textTracksActor)
|
|
2058
|
+
});
|
|
2059
|
+
} },
|
|
2060
|
+
pending: {},
|
|
2061
|
+
"monitoring-for-loads": { effects: () => {
|
|
2062
|
+
const currentTime = currentTimeSignal.get();
|
|
2063
|
+
const track = selectedTrackSignal.get();
|
|
2064
|
+
const { segmentLoaderActor } = untrack(() => owners.get());
|
|
2065
|
+
segmentLoaderActor.send({
|
|
2066
|
+
type: "load",
|
|
2067
|
+
track,
|
|
2068
|
+
currentTime
|
|
2069
|
+
});
|
|
2070
|
+
} }
|
|
1545
2071
|
}
|
|
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
|
-
});
|
|
1558
2072
|
});
|
|
1559
|
-
return () => {
|
|
1560
|
-
abortController?.abort();
|
|
1561
|
-
cleanupEffect();
|
|
1562
|
-
};
|
|
1563
2073
|
}
|
|
1564
2074
|
|
|
1565
2075
|
//#endregion
|
|
@@ -1609,49 +2119,62 @@ function trackCurrentTime({ state, owners }) {
|
|
|
1609
2119
|
//#endregion
|
|
1610
2120
|
//#region ../spf/dist/dev/dom/features/track-playback-initiated.js
|
|
1611
2121
|
/**
|
|
2122
|
+
* FSM states for playback initiation tracking.
|
|
2123
|
+
*
|
|
2124
|
+
* ```
|
|
2125
|
+
* 'preconditions-unmet' ──── element + URL ────→ 'monitoring'
|
|
2126
|
+
* ↑ ↑ │
|
|
2127
|
+
* │ preconditions lost │ play / !paused
|
|
2128
|
+
* │ │ ↓
|
|
2129
|
+
* └────────────── 'playback-initiated' ←──────┘
|
|
2130
|
+
* (exit cleanup resets state.playbackInitiated → false)
|
|
2131
|
+
*
|
|
2132
|
+
* any state ──── destroy() ────→ 'destroying' ────→ 'destroyed'
|
|
2133
|
+
* ```
|
|
2134
|
+
*/
|
|
2135
|
+
function deriveState$1(state, owners) {
|
|
2136
|
+
if (!owners.mediaElement || !state.presentation?.url) return "preconditions-unmet";
|
|
2137
|
+
if (state.playbackInitiated) return "playback-initiated";
|
|
2138
|
+
return "monitoring";
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
1612
2141
|
* Track whether playback has been initiated for the current presentation URL.
|
|
1613
2142
|
*
|
|
1614
|
-
*
|
|
1615
|
-
*
|
|
1616
|
-
* -
|
|
1617
|
-
*
|
|
1618
|
-
*
|
|
1619
|
-
*
|
|
1620
|
-
* the async forward bridge has run.
|
|
2143
|
+
* A three-state Reactor FSM driven by `state.playbackInitiated` and the
|
|
2144
|
+
* `deriveState` pattern:
|
|
2145
|
+
* - `'preconditions-unmet'` — no element or URL yet; no effects.
|
|
2146
|
+
* - `'monitoring'` — checks `!el.paused` on entry; listens for `play`.
|
|
2147
|
+
* - `'playback-initiated'` — tracks element and URL; exit cleanup resets
|
|
2148
|
+
* `state.playbackInitiated` to `false` on any change or lost preconditions.
|
|
1621
2149
|
*
|
|
1622
2150
|
* @example
|
|
1623
|
-
* const
|
|
2151
|
+
* const reactor = trackPlaybackInitiated({ state, owners });
|
|
2152
|
+
* // later:
|
|
2153
|
+
* reactor.destroy();
|
|
1624
2154
|
*/
|
|
1625
2155
|
function trackPlaybackInitiated({ state, owners }) {
|
|
1626
|
-
const
|
|
1627
|
-
const
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
if (pi === void 0) return;
|
|
1648
|
-
if (state.get().playbackInitiated !== pi) update(state, { playbackInitiated: pi });
|
|
2156
|
+
const derivedStateSignal = computed(() => deriveState$1(state.get(), owners.get()));
|
|
2157
|
+
const mediaElementSignal = computed(() => owners.get().mediaElement);
|
|
2158
|
+
const urlSignal = computed(() => state.get().presentation?.url);
|
|
2159
|
+
return createMachineReactor({
|
|
2160
|
+
initial: "preconditions-unmet",
|
|
2161
|
+
monitor: () => derivedStateSignal.get(),
|
|
2162
|
+
states: {
|
|
2163
|
+
"preconditions-unmet": {},
|
|
2164
|
+
monitoring: { entry: () => {
|
|
2165
|
+
const el = mediaElementSignal.get();
|
|
2166
|
+
update(state, { playbackInitiated: !el.paused });
|
|
2167
|
+
return listen(el, "play", () => {
|
|
2168
|
+
update(state, { playbackInitiated: !el.paused });
|
|
2169
|
+
});
|
|
2170
|
+
} },
|
|
2171
|
+
"playback-initiated": { effects: () => {
|
|
2172
|
+
mediaElementSignal.get();
|
|
2173
|
+
urlSignal.get();
|
|
2174
|
+
return () => update(state, { playbackInitiated: false });
|
|
2175
|
+
} }
|
|
2176
|
+
}
|
|
1649
2177
|
});
|
|
1650
|
-
return () => {
|
|
1651
|
-
cleanupResetEffect();
|
|
1652
|
-
cleanupPlayEffect();
|
|
1653
|
-
cleanupMergeEffect();
|
|
1654
|
-
};
|
|
1655
2178
|
}
|
|
1656
2179
|
|
|
1657
2180
|
//#endregion
|
|
@@ -1927,25 +2450,6 @@ function switchQuality({ state }, config = {}) {
|
|
|
1927
2450
|
});
|
|
1928
2451
|
}
|
|
1929
2452
|
|
|
1930
|
-
//#endregion
|
|
1931
|
-
//#region ../spf/dist/dev/core/utils/generate-id.js
|
|
1932
|
-
/**
|
|
1933
|
-
* Generate unique ID for HAM objects.
|
|
1934
|
-
*
|
|
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
|
-
* ```
|
|
1944
|
-
*/
|
|
1945
|
-
function generateId() {
|
|
1946
|
-
return `${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
2453
|
//#endregion
|
|
1950
2454
|
//#region ../spf/dist/dev/core/hls/parse-attributes.js
|
|
1951
2455
|
/**
|
|
@@ -2299,15 +2803,6 @@ function parseMultivariantPlaylist(text, unresolved) {
|
|
|
2299
2803
|
//#endregion
|
|
2300
2804
|
//#region ../spf/dist/dev/core/features/resolve-presentation.js
|
|
2301
2805
|
/**
|
|
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
2806
|
* Determines if resolution conditions are met based on preload policy and playback state.
|
|
2312
2807
|
*
|
|
2313
2808
|
* Resolution conditions:
|
|
@@ -2322,49 +2817,54 @@ function shouldResolve(state) {
|
|
|
2322
2817
|
return ["auto", "metadata"].includes(preload) || !!playbackInitiated;
|
|
2323
2818
|
}
|
|
2324
2819
|
/**
|
|
2820
|
+
* Derives the correct state from current state conditions.
|
|
2821
|
+
*
|
|
2822
|
+
* States are mutually exclusive and exhaustive:
|
|
2823
|
+
* - `'preconditions-unmet'`: no presentation, or presentation has no URL
|
|
2824
|
+
* - `'idle'`: URL present, unresolved (no id), shouldResolve not met
|
|
2825
|
+
* - `'resolving'`: URL present, unresolved (no id), shouldResolve met
|
|
2826
|
+
* - `'resolved'`: URL present, resolved (has id)
|
|
2827
|
+
*/
|
|
2828
|
+
function deriveState(state) {
|
|
2829
|
+
const { presentation } = state;
|
|
2830
|
+
if (!presentation || !("url" in presentation)) return "preconditions-unmet";
|
|
2831
|
+
if ("id" in presentation) return "resolved";
|
|
2832
|
+
return shouldResolve(state) ? "resolving" : "idle";
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2325
2835
|
* Resolves unresolved presentations using reactive composition.
|
|
2326
2836
|
*
|
|
2327
|
-
*
|
|
2328
|
-
*
|
|
2329
|
-
*
|
|
2837
|
+
* FSM driven by `deriveState` — a single `always` monitor keeps the state in
|
|
2838
|
+
* sync with conditions at all times. `'resolving'` additionally runs the fetch
|
|
2839
|
+
* task and returns an AbortController so the framework aborts it on state exit.
|
|
2330
2840
|
*
|
|
2331
2841
|
* @example
|
|
2332
|
-
*
|
|
2333
|
-
*
|
|
2334
|
-
*
|
|
2335
|
-
* const cleanup = resolvePresentation({ state });
|
|
2336
|
-
*
|
|
2337
|
-
* // State-driven: resolves immediately when preload allows
|
|
2338
|
-
* state.set({ ...state.get(), presentation: { url: 'http://example.com/playlist.m3u8' } });
|
|
2339
|
-
*
|
|
2340
|
-
* // Playback-driven: resolves when playbackInitiated is set
|
|
2341
|
-
* state.set({ ...state.get(), preload: 'none', presentation: { url: '...' }, playbackInitiated: true });
|
|
2342
|
-
* ```
|
|
2842
|
+
* const reactor = resolvePresentation({ state });
|
|
2843
|
+
* // later:
|
|
2844
|
+
* reactor.destroy();
|
|
2343
2845
|
*/
|
|
2344
2846
|
function resolvePresentation({ state }) {
|
|
2345
|
-
const
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2847
|
+
const derivedStateSignal = computed(() => deriveState(state.get()));
|
|
2848
|
+
return createMachineReactor({
|
|
2849
|
+
initial: "preconditions-unmet",
|
|
2850
|
+
monitor: () => derivedStateSignal.get(),
|
|
2851
|
+
states: {
|
|
2852
|
+
"preconditions-unmet": {},
|
|
2853
|
+
idle: {},
|
|
2854
|
+
resolving: { entry: () => {
|
|
2855
|
+
const presentation = state.get().presentation;
|
|
2856
|
+
const ac = new AbortController();
|
|
2857
|
+
fetchResolvable(presentation, { signal: ac.signal }).then((response) => getResponseText(response)).then((text) => {
|
|
2858
|
+
update(state, { presentation: parseMultivariantPlaylist(text, presentation) });
|
|
2859
|
+
}).catch((error) => {
|
|
2860
|
+
if (error instanceof Error && error.name === "AbortError") return;
|
|
2861
|
+
throw error;
|
|
2862
|
+
});
|
|
2863
|
+
return ac;
|
|
2864
|
+
} },
|
|
2865
|
+
resolved: {}
|
|
2866
|
+
}
|
|
2363
2867
|
});
|
|
2364
|
-
return () => {
|
|
2365
|
-
abortController?.abort();
|
|
2366
|
-
cleanupEffect();
|
|
2367
|
-
};
|
|
2368
2868
|
}
|
|
2369
2869
|
|
|
2370
2870
|
//#endregion
|
|
@@ -2445,124 +2945,6 @@ function parseMediaPlaylist(text, unresolved) {
|
|
|
2445
2945
|
};
|
|
2446
2946
|
}
|
|
2447
2947
|
|
|
2448
|
-
//#endregion
|
|
2449
|
-
//#region ../spf/dist/dev/core/task.js
|
|
2450
|
-
/**
|
|
2451
|
-
* Generic reusable task that wraps an async run function.
|
|
2452
|
-
*
|
|
2453
|
-
* Owns its own AbortController so it can always be aborted independently.
|
|
2454
|
-
* Optionally composes an external AbortSignal so that a parent's cancellation
|
|
2455
|
-
* propagates into the task's work without requiring the caller to track the
|
|
2456
|
-
* task separately.
|
|
2457
|
-
*
|
|
2458
|
-
* Ordering guarantee: `value` is written before `status` transitions to `'done'`;
|
|
2459
|
-
* `error` is written before `status` transitions to `'error'`. Any reader
|
|
2460
|
-
* observing `status === 'done'` is guaranteed `value` is already present.
|
|
2461
|
-
*/
|
|
2462
|
-
var Task = class {
|
|
2463
|
-
id;
|
|
2464
|
-
#runFn;
|
|
2465
|
-
#abortController = new AbortController();
|
|
2466
|
-
#signal;
|
|
2467
|
-
#status = "pending";
|
|
2468
|
-
#value = void 0;
|
|
2469
|
-
#error = void 0;
|
|
2470
|
-
constructor(runFn, config) {
|
|
2471
|
-
this.#runFn = runFn;
|
|
2472
|
-
const rawId = config?.id;
|
|
2473
|
-
this.id = typeof rawId === "function" ? rawId() : rawId ?? generateId();
|
|
2474
|
-
this.#signal = config?.signal ? anyAbortSignal([this.#abortController.signal, config.signal]) : this.#abortController.signal;
|
|
2475
|
-
}
|
|
2476
|
-
get status() {
|
|
2477
|
-
return this.#status;
|
|
2478
|
-
}
|
|
2479
|
-
get value() {
|
|
2480
|
-
return this.#value;
|
|
2481
|
-
}
|
|
2482
|
-
get error() {
|
|
2483
|
-
return this.#error;
|
|
2484
|
-
}
|
|
2485
|
-
async run() {
|
|
2486
|
-
this.#status = "running";
|
|
2487
|
-
try {
|
|
2488
|
-
const result = await this.#runFn(this.#signal);
|
|
2489
|
-
this.#value = result;
|
|
2490
|
-
this.#status = "done";
|
|
2491
|
-
return result;
|
|
2492
|
-
} catch (e) {
|
|
2493
|
-
this.#error = e;
|
|
2494
|
-
this.#status = "error";
|
|
2495
|
-
throw e;
|
|
2496
|
-
}
|
|
2497
|
-
}
|
|
2498
|
-
abort() {
|
|
2499
|
-
this.#abortController.abort();
|
|
2500
|
-
}
|
|
2501
|
-
};
|
|
2502
|
-
/**
|
|
2503
|
-
* Runs tasks concurrently, deduplicated by task id.
|
|
2504
|
-
*
|
|
2505
|
-
* If a task with a given id is already in flight, subsequent schedule() calls
|
|
2506
|
-
* for that id are silently ignored until the first completes. Tasks are stored
|
|
2507
|
-
* so abortAll() can cancel any in-flight work (e.g. on engine cleanup).
|
|
2508
|
-
*/
|
|
2509
|
-
var ConcurrentRunner = class {
|
|
2510
|
-
#pending = /* @__PURE__ */ new Map();
|
|
2511
|
-
schedule(task) {
|
|
2512
|
-
if (this.#pending.has(task.id)) return;
|
|
2513
|
-
this.#pending.set(task.id, task);
|
|
2514
|
-
task.run().catch((error) => {
|
|
2515
|
-
if (!(error instanceof Error && error.name === "AbortError")) throw error;
|
|
2516
|
-
}).finally(() => {
|
|
2517
|
-
this.#pending.delete(task.id);
|
|
2518
|
-
});
|
|
2519
|
-
}
|
|
2520
|
-
abortAll() {
|
|
2521
|
-
for (const task of this.#pending.values()) task.abort();
|
|
2522
|
-
this.#pending.clear();
|
|
2523
|
-
}
|
|
2524
|
-
};
|
|
2525
|
-
/**
|
|
2526
|
-
* Runs tasks one at a time in submission order.
|
|
2527
|
-
*
|
|
2528
|
-
* Each schedule() call returns a Promise that resolves or rejects with the
|
|
2529
|
-
* task's result when it is eventually executed. Tasks wait in queue until the
|
|
2530
|
-
* prior task completes.
|
|
2531
|
-
*
|
|
2532
|
-
* Serialization is achieved by chaining each task's run() onto the tail of a
|
|
2533
|
-
* shared promise chain — no explicit queue or drain loop needed.
|
|
2534
|
-
*
|
|
2535
|
-
* abortAll() aborts all pending (not yet started) tasks and the currently
|
|
2536
|
-
* in-flight task. Pending tasks still run briefly but receive an aborted
|
|
2537
|
-
* signal and are expected to exit early.
|
|
2538
|
-
*/
|
|
2539
|
-
var SerialRunner = class {
|
|
2540
|
-
#chain = Promise.resolve();
|
|
2541
|
-
#pending = /* @__PURE__ */ new Set();
|
|
2542
|
-
#current = null;
|
|
2543
|
-
schedule(task) {
|
|
2544
|
-
const t = task;
|
|
2545
|
-
this.#pending.add(t);
|
|
2546
|
-
const result = this.#chain.then(() => {
|
|
2547
|
-
this.#pending.delete(t);
|
|
2548
|
-
this.#current = t;
|
|
2549
|
-
return task.run();
|
|
2550
|
-
}).finally(() => {
|
|
2551
|
-
this.#current = null;
|
|
2552
|
-
});
|
|
2553
|
-
this.#chain = result.then(() => {}, () => {});
|
|
2554
|
-
return result;
|
|
2555
|
-
}
|
|
2556
|
-
abortAll() {
|
|
2557
|
-
for (const task of this.#pending) task.abort();
|
|
2558
|
-
this.#pending.clear();
|
|
2559
|
-
this.#current?.abort();
|
|
2560
|
-
}
|
|
2561
|
-
destroy() {
|
|
2562
|
-
this.abortAll();
|
|
2563
|
-
}
|
|
2564
|
-
};
|
|
2565
|
-
|
|
2566
2948
|
//#endregion
|
|
2567
2949
|
//#region ../spf/dist/dev/core/features/resolve-track.js
|
|
2568
2950
|
function canResolve(state, config) {
|
|
@@ -2820,8 +3202,8 @@ function shouldEndStream(state, owners) {
|
|
|
2820
3202
|
const hasAudioTrack = !!state.selectedAudioTrackId;
|
|
2821
3203
|
if (hasVideoTrack && !owners.videoBuffer) return false;
|
|
2822
3204
|
if (hasAudioTrack && !owners.audioBuffer) return false;
|
|
2823
|
-
if (owners.videoBufferActor?.snapshot.get().
|
|
2824
|
-
if (owners.audioBufferActor?.snapshot.get().
|
|
3205
|
+
if (owners.videoBufferActor?.snapshot.get().value === "updating") return false;
|
|
3206
|
+
if (owners.audioBufferActor?.snapshot.get().value === "updating") return false;
|
|
2825
3207
|
if (!hasLastSegmentLoaded(state, owners)) return false;
|
|
2826
3208
|
if (mediaElement) {
|
|
2827
3209
|
const videoTrack = hasVideoTrack ? getSelectedTrack(state, "video") : void 0;
|
|
@@ -2836,17 +3218,17 @@ function shouldEndStream(state, owners) {
|
|
|
2836
3218
|
}
|
|
2837
3219
|
/**
|
|
2838
3220
|
* Wait for all currently-updating SourceBufferActors to finish.
|
|
2839
|
-
* Uses actor
|
|
3221
|
+
* Uses actor state rather than raw SourceBuffer.updating so the wait is
|
|
2840
3222
|
* aligned with the same abstraction that owns all buffer operations.
|
|
2841
3223
|
*/
|
|
2842
3224
|
function waitForSourceBuffersReady$1(owners) {
|
|
2843
|
-
const updatingActors = [owners.videoBufferActor, owners.audioBufferActor].filter((actor) => actor !== void 0 && actor.snapshot.get().
|
|
3225
|
+
const updatingActors = [owners.videoBufferActor, owners.audioBufferActor].filter((actor) => actor !== void 0 && actor.snapshot.get().value === "updating");
|
|
2844
3226
|
if (updatingActors.length === 0) return Promise.resolve();
|
|
2845
3227
|
return Promise.all(updatingActors.map((actor) => new Promise((resolve) => {
|
|
2846
3228
|
let cleanup;
|
|
2847
3229
|
let resolved = false;
|
|
2848
3230
|
cleanup = effect(() => {
|
|
2849
|
-
if (actor.snapshot.get().
|
|
3231
|
+
if (actor.snapshot.get().value !== "updating") {
|
|
2850
3232
|
if (!resolved) {
|
|
2851
3233
|
resolved = true;
|
|
2852
3234
|
resolve();
|
|
@@ -3095,16 +3477,6 @@ function setupMediaSource({ state, owners }) {
|
|
|
3095
3477
|
|
|
3096
3478
|
//#endregion
|
|
3097
3479
|
//#region ../spf/dist/dev/dom/media/source-buffer-actor.js
|
|
3098
|
-
/**
|
|
3099
|
-
* Thrown when a message is sent to the actor in a state that does not
|
|
3100
|
-
* accept messages (currently: 'updating').
|
|
3101
|
-
*/
|
|
3102
|
-
var SourceBufferActorError = class extends Error {
|
|
3103
|
-
constructor(message) {
|
|
3104
|
-
super(message);
|
|
3105
|
-
this.name = "SourceBufferActorError";
|
|
3106
|
-
}
|
|
3107
|
-
};
|
|
3108
3480
|
function snapshotBuffered(buffered) {
|
|
3109
3481
|
const ranges = [];
|
|
3110
3482
|
for (let i = 0; i < buffered.length; i++) ranges.push({
|
|
@@ -3113,25 +3485,25 @@ function snapshotBuffered(buffered) {
|
|
|
3113
3485
|
});
|
|
3114
3486
|
return ranges;
|
|
3115
3487
|
}
|
|
3116
|
-
function appendInitTask(message, {
|
|
3488
|
+
function appendInitTask(message, { getContext, sourceBuffer }) {
|
|
3117
3489
|
return new Task(async (taskSignal) => {
|
|
3118
|
-
const ctx =
|
|
3490
|
+
const ctx = getContext();
|
|
3119
3491
|
if (taskSignal.aborted) return ctx;
|
|
3120
3492
|
await appendSegment(sourceBuffer, message.data);
|
|
3121
3493
|
return {
|
|
3122
3494
|
...ctx,
|
|
3123
3495
|
initTrackId: message.meta.trackId
|
|
3124
3496
|
};
|
|
3125
|
-
}
|
|
3497
|
+
});
|
|
3126
3498
|
}
|
|
3127
|
-
function appendSegmentTask(message, {
|
|
3499
|
+
function appendSegmentTask(message, { getContext, sourceBuffer, setContext }) {
|
|
3128
3500
|
return new Task(async (taskSignal) => {
|
|
3129
|
-
const ctx =
|
|
3501
|
+
const ctx = getContext();
|
|
3130
3502
|
if (taskSignal.aborted) return ctx;
|
|
3131
3503
|
const { meta } = message;
|
|
3132
3504
|
const EPSILON = 1e-4;
|
|
3133
3505
|
const filtered = ctx.segments.filter((s) => Math.abs(s.startTime - meta.startTime) >= EPSILON);
|
|
3134
|
-
if (!(message.data instanceof ArrayBuffer))
|
|
3506
|
+
if (!(message.data instanceof ArrayBuffer)) setContext({
|
|
3135
3507
|
...ctx,
|
|
3136
3508
|
segments: [...filtered, {
|
|
3137
3509
|
id: meta.id,
|
|
@@ -3155,11 +3527,11 @@ function appendSegmentTask(message, { signal, getCtx, sourceBuffer, onPartialCon
|
|
|
3155
3527
|
}],
|
|
3156
3528
|
bufferedRanges: snapshotBuffered(sourceBuffer.buffered)
|
|
3157
3529
|
};
|
|
3158
|
-
}
|
|
3530
|
+
});
|
|
3159
3531
|
}
|
|
3160
|
-
function removeTask(message, {
|
|
3532
|
+
function removeTask(message, { getContext, sourceBuffer }) {
|
|
3161
3533
|
return new Task(async (taskSignal) => {
|
|
3162
|
-
const ctx =
|
|
3534
|
+
const ctx = getContext();
|
|
3163
3535
|
if (taskSignal.aborted) return ctx;
|
|
3164
3536
|
await flushBuffer(sourceBuffer, message.start, message.end);
|
|
3165
3537
|
const bufferedRanges = snapshotBuffered(sourceBuffer.buffered);
|
|
@@ -3172,7 +3544,7 @@ function removeTask(message, { signal, getCtx, sourceBuffer }) {
|
|
|
3172
3544
|
segments: filtered,
|
|
3173
3545
|
bufferedRanges
|
|
3174
3546
|
};
|
|
3175
|
-
}
|
|
3547
|
+
});
|
|
3176
3548
|
}
|
|
3177
3549
|
const messageTaskFactories = {
|
|
3178
3550
|
"append-init": appendInitTask,
|
|
@@ -3184,83 +3556,54 @@ function messageToTask(message, options) {
|
|
|
3184
3556
|
return factory(message, options);
|
|
3185
3557
|
}
|
|
3186
3558
|
function createSourceBufferActor(sourceBuffer, initialContext) {
|
|
3187
|
-
const
|
|
3188
|
-
|
|
3559
|
+
const handleError = (e) => {
|
|
3560
|
+
if (!(e instanceof Error && e.name === "AbortError")) console.error("SourceBuffer operation failed:", e);
|
|
3561
|
+
};
|
|
3562
|
+
const onMessage = (msg, { transition, setContext, getContext, runner }) => {
|
|
3563
|
+
transition("updating");
|
|
3564
|
+
const task = messageToTask(msg, {
|
|
3565
|
+
getContext,
|
|
3566
|
+
sourceBuffer,
|
|
3567
|
+
setContext
|
|
3568
|
+
});
|
|
3569
|
+
runner.schedule(task).then(setContext, handleError);
|
|
3570
|
+
};
|
|
3571
|
+
return createMachineActor({
|
|
3572
|
+
runner: () => new SerialRunner(),
|
|
3573
|
+
initial: "idle",
|
|
3189
3574
|
context: {
|
|
3190
3575
|
segments: [],
|
|
3191
3576
|
bufferedRanges: [],
|
|
3192
3577
|
initTrackId: void 0,
|
|
3193
3578
|
...initialContext
|
|
3194
|
-
}
|
|
3195
|
-
});
|
|
3196
|
-
const runner = new SerialRunner();
|
|
3197
|
-
function applyResult(newContext) {
|
|
3198
|
-
const status = snapshotSignal.get().status === "destroyed" ? "destroyed" : "idle";
|
|
3199
|
-
snapshotSignal.set({
|
|
3200
|
-
status,
|
|
3201
|
-
context: newContext
|
|
3202
|
-
});
|
|
3203
|
-
}
|
|
3204
|
-
function handleError(e) {
|
|
3205
|
-
update(snapshotSignal, { status: snapshotSignal.get().status === "destroyed" ? "destroyed" : "idle" });
|
|
3206
|
-
throw e;
|
|
3207
|
-
}
|
|
3208
|
-
return {
|
|
3209
|
-
get snapshot() {
|
|
3210
|
-
return snapshotSignal;
|
|
3211
|
-
},
|
|
3212
|
-
send(message, signal) {
|
|
3213
|
-
if (snapshotSignal.get().status !== "idle") return Promise.reject(new SourceBufferActorError(`send() called while actor is ${snapshotSignal.get().status}`));
|
|
3214
|
-
update(snapshotSignal, { status: "updating" });
|
|
3215
|
-
const onPartialContext = (ctx) => {
|
|
3216
|
-
snapshotSignal.set({
|
|
3217
|
-
status: "updating",
|
|
3218
|
-
context: ctx
|
|
3219
|
-
});
|
|
3220
|
-
};
|
|
3221
|
-
const task = messageToTask(message, {
|
|
3222
|
-
signal,
|
|
3223
|
-
getCtx: () => snapshotSignal.get().context,
|
|
3224
|
-
sourceBuffer,
|
|
3225
|
-
onPartialContext
|
|
3226
|
-
});
|
|
3227
|
-
return runner.schedule(task).then(applyResult).catch(handleError);
|
|
3228
3579
|
},
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
}
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3580
|
+
states: {
|
|
3581
|
+
idle: { on: {
|
|
3582
|
+
"append-init": onMessage,
|
|
3583
|
+
"append-segment": onMessage,
|
|
3584
|
+
remove: onMessage,
|
|
3585
|
+
batch: (msg, { transition, setContext, getContext, runner }) => {
|
|
3586
|
+
const { messages } = msg;
|
|
3587
|
+
if (messages.length === 0) return;
|
|
3588
|
+
transition("updating");
|
|
3589
|
+
messages.forEach((msg) => {
|
|
3590
|
+
const task = messageToTask(msg, {
|
|
3591
|
+
getContext,
|
|
3592
|
+
sourceBuffer,
|
|
3593
|
+
setContext
|
|
3594
|
+
});
|
|
3595
|
+
runner.schedule(task).then(setContext, handleError);
|
|
3596
|
+
});
|
|
3597
|
+
}
|
|
3598
|
+
} },
|
|
3599
|
+
updating: {
|
|
3600
|
+
onSettled: "idle",
|
|
3601
|
+
on: { cancel: (_, { runner }) => {
|
|
3602
|
+
runner.abortAll();
|
|
3603
|
+
} }
|
|
3250
3604
|
}
|
|
3251
|
-
const lastTask = messageToTask(messages[messages.length - 1], {
|
|
3252
|
-
signal,
|
|
3253
|
-
getCtx: () => workingCtx,
|
|
3254
|
-
sourceBuffer,
|
|
3255
|
-
onPartialContext
|
|
3256
|
-
});
|
|
3257
|
-
return runner.schedule(lastTask).then(applyResult).catch(handleError);
|
|
3258
|
-
},
|
|
3259
|
-
destroy() {
|
|
3260
|
-
update(snapshotSignal, { status: "destroyed" });
|
|
3261
|
-
runner.destroy();
|
|
3262
3605
|
}
|
|
3263
|
-
};
|
|
3606
|
+
});
|
|
3264
3607
|
}
|
|
3265
3608
|
|
|
3266
3609
|
//#endregion
|
|
@@ -3334,143 +3677,92 @@ function setupSourceBuffers({ state, owners }) {
|
|
|
3334
3677
|
}
|
|
3335
3678
|
|
|
3336
3679
|
//#endregion
|
|
3337
|
-
//#region ../spf/dist/dev/dom/features/
|
|
3338
|
-
/**
|
|
3339
|
-
* Create a track element for a text track.
|
|
3340
|
-
*
|
|
3341
|
-
* Note: We use DOM <track> elements instead of the TextTrack JS API
|
|
3342
|
-
* because there's no way to remove TextTracks added via addTextTrack().
|
|
3343
|
-
*/
|
|
3680
|
+
//#region ../spf/dist/dev/dom/features/sync-text-tracks.js
|
|
3344
3681
|
function createTrackElement(track) {
|
|
3345
|
-
const
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
if (track.
|
|
3351
|
-
|
|
3352
|
-
return
|
|
3682
|
+
const el = document.createElement("track");
|
|
3683
|
+
el.id = track.id;
|
|
3684
|
+
el.kind = track.kind;
|
|
3685
|
+
el.label = track.label;
|
|
3686
|
+
el.toggleAttribute("data-src-track", true);
|
|
3687
|
+
if (track.language) el.srclang = track.language;
|
|
3688
|
+
if (track.default) el.default = true;
|
|
3689
|
+
return el;
|
|
3690
|
+
}
|
|
3691
|
+
function getModelTextTracks(presentation) {
|
|
3692
|
+
return presentation?.selectionSets?.find((s) => s.type === "text")?.switchingSets[0]?.tracks;
|
|
3693
|
+
}
|
|
3694
|
+
function syncModes(textTracks, selectedId) {
|
|
3695
|
+
for (let i = 0; i < textTracks.length; i++) {
|
|
3696
|
+
const track = textTracks[i];
|
|
3697
|
+
if (track.kind !== "subtitles" && track.kind !== "captions") continue;
|
|
3698
|
+
track.mode = track.id === selectedId ? "showing" : "disabled";
|
|
3699
|
+
}
|
|
3353
3700
|
}
|
|
3354
3701
|
/**
|
|
3355
|
-
*
|
|
3702
|
+
* Text track sync orchestration.
|
|
3356
3703
|
*
|
|
3357
|
-
*
|
|
3358
|
-
* -
|
|
3359
|
-
* - presentation is resolved (has text tracks)
|
|
3704
|
+
* A single `always` monitor keeps the reactor in sync with preconditions.
|
|
3705
|
+
* `'set-up'` owns the full lifecycle of `<track>` elements:
|
|
3360
3706
|
*
|
|
3361
|
-
*
|
|
3362
|
-
*
|
|
3363
|
-
*
|
|
3364
|
-
*
|
|
3365
|
-
*
|
|
3707
|
+
* - **Effect 1** — creates `<track>` elements on entry; exit cleanup removes
|
|
3708
|
+
* them and clears `selectedTextTrackId` on any outbound transition.
|
|
3709
|
+
* - **Effect 2** — owns mode sync, the Chromium settling-window guard, and
|
|
3710
|
+
* the `'change'` listener that bridges DOM state back to
|
|
3711
|
+
* `selectedTextTrackId`.
|
|
3366
3712
|
*
|
|
3367
3713
|
* @example
|
|
3368
|
-
* const
|
|
3714
|
+
* const reactor = syncTextTracks({ state, owners });
|
|
3715
|
+
* // later:
|
|
3716
|
+
* reactor.destroy();
|
|
3369
3717
|
*/
|
|
3370
|
-
function
|
|
3371
|
-
const
|
|
3718
|
+
function syncTextTracks({ state, owners }) {
|
|
3719
|
+
const mediaElementSignal = computed(() => owners.get().mediaElement);
|
|
3720
|
+
const modelTextTracksSignal = computed(() => getModelTextTracks(state.get().presentation), { equals(prevTextTracks, nextTextTracks) {
|
|
3372
3721
|
if (prevTextTracks === nextTextTracks) return true;
|
|
3373
3722
|
if (typeof prevTextTracks !== typeof nextTextTracks) return false;
|
|
3374
3723
|
if (prevTextTracks?.length !== nextTextTracks?.length) return false;
|
|
3375
3724
|
return !!nextTextTracks && nextTextTracks.every((nextTextTrack) => prevTextTracks?.some((prevTextTrack) => prevTextTrack.id === nextTextTrack.id));
|
|
3376
3725
|
} });
|
|
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();
|
|
3385
|
-
const trackMap = /* @__PURE__ */ new Map();
|
|
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 });
|
|
3392
|
-
});
|
|
3393
|
-
return () => {
|
|
3394
|
-
owners.get().textTracks?.forEach((trackElement) => trackElement.remove());
|
|
3395
|
-
cleanupEffect();
|
|
3396
|
-
};
|
|
3397
|
-
}
|
|
3398
|
-
|
|
3399
|
-
//#endregion
|
|
3400
|
-
//#region ../spf/dist/dev/dom/features/sync-selected-text-track-from-dom.js
|
|
3401
|
-
/**
|
|
3402
|
-
* Sync selectedTextTrackId from DOM text track mode changes.
|
|
3403
|
-
*
|
|
3404
|
-
* Listens to the `change` event on `media.textTracks` and updates
|
|
3405
|
-
* `selectedTextTrackId` when external code (e.g. the captions button via
|
|
3406
|
-
* `toggleSubtitles()`) changes a subtitle/caption track mode to 'showing'.
|
|
3407
|
-
*
|
|
3408
|
-
* This bridges the core store's `toggleSubtitles()` with SPF's reactive text
|
|
3409
|
-
* track pipeline (`syncTextTrackModes`, `loadTextTrackCues`). Without this
|
|
3410
|
-
* bridge, direct DOM mode changes would be immediately overridden by
|
|
3411
|
-
* `syncTextTrackModes` on the next SPF state update.
|
|
3412
|
-
*
|
|
3413
|
-
* When a subtitle/caption track's mode is 'showing', its DOM `id` — which
|
|
3414
|
-
* matches the SPF track ID set by `setupTextTracks` — is written to
|
|
3415
|
-
* `selectedTextTrackId`. When no subtitle/caption track is 'showing',
|
|
3416
|
-
* `selectedTextTrackId` is cleared along with the deselected track's
|
|
3417
|
-
* `textBufferState` entry — setting mode to 'disabled' clears native cues from
|
|
3418
|
-
* the track element, so the buffer must be reset to re-fetch cues on re-enable.
|
|
3419
|
-
*
|
|
3420
|
-
* @example
|
|
3421
|
-
* const cleanup = syncSelectedTextTrackFromDom({ state, owners });
|
|
3422
|
-
*/
|
|
3423
|
-
function syncSelectedTextTrackFromDom({ state, owners }) {
|
|
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();
|
|
3431
|
-
if (current.selectedTextTrackId === newId) return;
|
|
3432
|
-
if (newId) update(state, { selectedTextTrackId: newId });
|
|
3433
|
-
else {
|
|
3434
|
-
const prevId = current.selectedTextTrackId;
|
|
3435
|
-
if (prevId && current.textBufferState?.[prevId]) {
|
|
3436
|
-
const next = { ...current.textBufferState };
|
|
3437
|
-
delete next[prevId];
|
|
3438
|
-
update(state, {
|
|
3439
|
-
selectedTextTrackId: void 0,
|
|
3440
|
-
textBufferState: next
|
|
3441
|
-
});
|
|
3442
|
-
} else update(state, { selectedTextTrackId: void 0 });
|
|
3443
|
-
}
|
|
3444
|
-
});
|
|
3445
|
-
});
|
|
3446
|
-
}
|
|
3447
|
-
|
|
3448
|
-
//#endregion
|
|
3449
|
-
//#region ../spf/dist/dev/dom/features/sync-text-track-modes.js
|
|
3450
|
-
/**
|
|
3451
|
-
* Sync text track modes orchestration.
|
|
3452
|
-
*
|
|
3453
|
-
* Manages track element modes based on selectedTextTrackId:
|
|
3454
|
-
* - Selected track: mode = "showing"
|
|
3455
|
-
* - Other tracks: mode = "hidden"
|
|
3456
|
-
* - No selection: all tracks mode = "hidden"
|
|
3457
|
-
*
|
|
3458
|
-
* Note: Uses "hidden" instead of "disabled" for non-selected tracks
|
|
3459
|
-
* so they remain available in the browser's track menu.
|
|
3460
|
-
*
|
|
3461
|
-
* @example
|
|
3462
|
-
* const cleanup = syncTextTrackModes({ state, owners });
|
|
3463
|
-
*/
|
|
3464
|
-
function syncTextTrackModes({ state, owners }) {
|
|
3465
|
-
const textTracksSignal = computed(() => owners.get().textTracks);
|
|
3466
3726
|
const selectedTextTrackIdSignal = computed(() => state.get().selectedTextTrackId);
|
|
3467
|
-
const
|
|
3468
|
-
return
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3727
|
+
const preconditionsMetSignal = computed(() => !!mediaElementSignal.get() && !!modelTextTracksSignal.get()?.length);
|
|
3728
|
+
return createMachineReactor({
|
|
3729
|
+
initial: "preconditions-unmet",
|
|
3730
|
+
monitor: () => preconditionsMetSignal.get() ? "set-up" : "preconditions-unmet",
|
|
3731
|
+
states: {
|
|
3732
|
+
"preconditions-unmet": {},
|
|
3733
|
+
"set-up": {
|
|
3734
|
+
entry: () => {
|
|
3735
|
+
const mediaElement = mediaElementSignal.get();
|
|
3736
|
+
modelTextTracksSignal.get().forEach((track) => mediaElement.appendChild(createTrackElement(track)));
|
|
3737
|
+
return () => {
|
|
3738
|
+
mediaElement.querySelectorAll("track[data-src-track]:is([kind=\"subtitles\"],[kind=\"captions\"]").forEach((trackEl) => trackEl.remove());
|
|
3739
|
+
update(state, { selectedTextTrackId: void 0 });
|
|
3740
|
+
};
|
|
3741
|
+
},
|
|
3742
|
+
effects: () => {
|
|
3743
|
+
const mediaElement = untrack(() => mediaElementSignal.get());
|
|
3744
|
+
const selectedId = selectedTextTrackIdSignal.get();
|
|
3745
|
+
syncModes(mediaElement.textTracks, selectedId);
|
|
3746
|
+
let syncTimeout = setTimeout(() => {
|
|
3747
|
+
syncTimeout = void 0;
|
|
3748
|
+
}, 0);
|
|
3749
|
+
const onChange = () => {
|
|
3750
|
+
if (syncTimeout) {
|
|
3751
|
+
syncModes(mediaElement.textTracks, untrack(() => selectedTextTrackIdSignal.get()));
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
const newId = Array.from(mediaElement.textTracks).find((t) => t.mode === "showing" && (t.kind === "subtitles" || t.kind === "captions"))?.id;
|
|
3755
|
+
if (newId === untrack(() => selectedTextTrackIdSignal.get())) return;
|
|
3756
|
+
update(state, { selectedTextTrackId: newId });
|
|
3757
|
+
};
|
|
3758
|
+
const unlisten = listen(mediaElement.textTracks, "change", onChange);
|
|
3759
|
+
return () => {
|
|
3760
|
+
clearTimeout(syncTimeout ?? void 0);
|
|
3761
|
+
unlisten();
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3474
3766
|
});
|
|
3475
3767
|
}
|
|
3476
3768
|
|
|
@@ -3651,15 +3943,7 @@ function createPlaybackEngine(config = {}) {
|
|
|
3651
3943
|
state,
|
|
3652
3944
|
owners
|
|
3653
3945
|
}),
|
|
3654
|
-
|
|
3655
|
-
state,
|
|
3656
|
-
owners
|
|
3657
|
-
}),
|
|
3658
|
-
syncTextTrackModes({
|
|
3659
|
-
state,
|
|
3660
|
-
owners
|
|
3661
|
-
}),
|
|
3662
|
-
syncSelectedTextTrackFromDom({
|
|
3946
|
+
syncTextTracks({
|
|
3663
3947
|
state,
|
|
3664
3948
|
owners
|
|
3665
3949
|
}),
|
|
@@ -3672,7 +3956,8 @@ function createPlaybackEngine(config = {}) {
|
|
|
3672
3956
|
state,
|
|
3673
3957
|
owners,
|
|
3674
3958
|
destroy: () => {
|
|
3675
|
-
cleanups.forEach((cleanup) => cleanup());
|
|
3959
|
+
cleanups.forEach((cleanup) => typeof cleanup === "function" ? cleanup() : cleanup.destroy());
|
|
3960
|
+
for (const value of Object.values(owners.get())) if (value !== null && typeof value === "object" && typeof value.destroy === "function") value.destroy();
|
|
3676
3961
|
destroyVttParser();
|
|
3677
3962
|
}
|
|
3678
3963
|
};
|