mattebox 0.1.0
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/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/cdn/mattebox.dash-drm.min.js +2 -0
- package/dist/cdn/mattebox.dash.min.js +2 -0
- package/dist/cdn/mattebox.dual-drm.min.js +2 -0
- package/dist/cdn/mattebox.dual-ts-drm.min.js +3 -0
- package/dist/cdn/mattebox.dual-ts.min.js +3 -0
- package/dist/cdn/mattebox.dual.min.js +2 -0
- package/dist/cdn/mattebox.hls-drm.min.js +2 -0
- package/dist/cdn/mattebox.hls-ts-drm.min.js +3 -0
- package/dist/cdn/mattebox.hls-ts.min.js +3 -0
- package/dist/cdn/mattebox.hls.min.js +2 -0
- package/dist/cdn/mattebox.kernel.min.js +1 -0
- package/dist/cdn/mattebox.min.js +6 -0
- package/dist/cdn/transmux.worker.js +1 -0
- package/dist/containers/adts.d.ts +33 -0
- package/dist/containers/adts.d.ts.map +1 -0
- package/dist/containers/adts.js +66 -0
- package/dist/containers/adts.js.map +1 -0
- package/dist/containers/captions.d.ts +27 -0
- package/dist/containers/captions.d.ts.map +1 -0
- package/dist/containers/captions.js +36 -0
- package/dist/containers/captions.js.map +1 -0
- package/dist/containers/codec-probe/index.d.ts +35 -0
- package/dist/containers/codec-probe/index.d.ts.map +1 -0
- package/dist/containers/codec-probe/index.js +222 -0
- package/dist/containers/codec-probe/index.js.map +1 -0
- package/dist/containers/fmp4/writer.d.ts +62 -0
- package/dist/containers/fmp4/writer.d.ts.map +1 -0
- package/dist/containers/fmp4/writer.js +201 -0
- package/dist/containers/fmp4/writer.js.map +1 -0
- package/dist/containers/id3.d.ts +25 -0
- package/dist/containers/id3.d.ts.map +1 -0
- package/dist/containers/id3.js +75 -0
- package/dist/containers/id3.js.map +1 -0
- package/dist/containers/mp4-box/index.d.ts +79 -0
- package/dist/containers/mp4-box/index.d.ts.map +1 -0
- package/dist/containers/mp4-box/index.js +231 -0
- package/dist/containers/mp4-box/index.js.map +1 -0
- package/dist/containers/packed-audio/index.d.ts +7 -0
- package/dist/containers/packed-audio/index.d.ts.map +1 -0
- package/dist/containers/packed-audio/index.js +63 -0
- package/dist/containers/packed-audio/index.js.map +1 -0
- package/dist/containers/sei.d.ts +26 -0
- package/dist/containers/sei.d.ts.map +1 -0
- package/dist/containers/sei.js +100 -0
- package/dist/containers/sei.js.map +1 -0
- package/dist/containers/ts-transmux/demux.d.ts +49 -0
- package/dist/containers/ts-transmux/demux.d.ts.map +1 -0
- package/dist/containers/ts-transmux/demux.js +222 -0
- package/dist/containers/ts-transmux/demux.js.map +1 -0
- package/dist/containers/ts-transmux/h264.d.ts +48 -0
- package/dist/containers/ts-transmux/h264.d.ts.map +1 -0
- package/dist/containers/ts-transmux/h264.js +211 -0
- package/dist/containers/ts-transmux/h264.js.map +1 -0
- package/dist/containers/ts-transmux/index.d.ts +17 -0
- package/dist/containers/ts-transmux/index.d.ts.map +1 -0
- package/dist/containers/ts-transmux/index.js +34 -0
- package/dist/containers/ts-transmux/index.js.map +1 -0
- package/dist/containers/ts-transmux/runner.d.ts +23 -0
- package/dist/containers/ts-transmux/runner.d.ts.map +1 -0
- package/dist/containers/ts-transmux/runner.js +99 -0
- package/dist/containers/ts-transmux/runner.js.map +1 -0
- package/dist/containers/ts-transmux/transmux.d.ts +24 -0
- package/dist/containers/ts-transmux/transmux.d.ts.map +1 -0
- package/dist/containers/ts-transmux/transmux.js +212 -0
- package/dist/containers/ts-transmux/transmux.js.map +1 -0
- package/dist/containers/ts-transmux/transmux.worker.d.ts +2 -0
- package/dist/containers/ts-transmux/transmux.worker.d.ts.map +1 -0
- package/dist/containers/ts-transmux/transmux.worker.js +19 -0
- package/dist/containers/ts-transmux/transmux.worker.js.map +1 -0
- package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/asyncToGenerator.js +27 -0
- package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/defineProperty.js +12 -0
- package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/objectSpread2.js +25 -0
- package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/objectWithoutProperties.js +13 -0
- package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/objectWithoutPropertiesLoose.js +12 -0
- package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/toPrimitive.js +14 -0
- package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/toPropertyKey.js +9 -0
- package/dist/es2015/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/typeof.js +11 -0
- package/dist/es2015/containers/adts.js +77 -0
- package/dist/es2015/containers/captions.js +36 -0
- package/dist/es2015/containers/codec-probe/index.js +220 -0
- package/dist/es2015/containers/fmp4/writer.js +165 -0
- package/dist/es2015/containers/id3.js +72 -0
- package/dist/es2015/containers/mp4-box/index.js +209 -0
- package/dist/es2015/containers/packed-audio/index.js +67 -0
- package/dist/es2015/containers/sei.js +79 -0
- package/dist/es2015/containers/ts-transmux/demux.js +211 -0
- package/dist/es2015/containers/ts-transmux/h264.js +204 -0
- package/dist/es2015/containers/ts-transmux/index.js +37 -0
- package/dist/es2015/containers/ts-transmux/runner.js +95 -0
- package/dist/es2015/containers/ts-transmux/transmux.js +210 -0
- package/dist/es2015/containers/ts-transmux/transmux.worker.js +26 -0
- package/dist/es2015/index.js +356 -0
- package/dist/es2015/kernel/append-queue.js +148 -0
- package/dist/es2015/kernel/bus.js +134 -0
- package/dist/es2015/kernel/context.js +92 -0
- package/dist/es2015/kernel/effects.js +62 -0
- package/dist/es2015/kernel/evictor.js +67 -0
- package/dist/es2015/kernel/lifecycle.js +122 -0
- package/dist/es2015/kernel/loader.js +108 -0
- package/dist/es2015/kernel/mime.js +20 -0
- package/dist/es2015/kernel/mse.js +426 -0
- package/dist/es2015/kernel/reducer.js +1051 -0
- package/dist/es2015/kernel/rendition-select.js +263 -0
- package/dist/es2015/kernel/scheduler.js +82 -0
- package/dist/es2015/kernel/sinks/metadata-sink.js +7 -0
- package/dist/es2015/kernel/sinks/mse-sink.js +31 -0
- package/dist/es2015/kernel/sinks/text-track-sink.js +167 -0
- package/dist/es2015/kernel/timeline.js +127 -0
- package/dist/es2015/kernel/trace.js +134 -0
- package/dist/es2015/kernel/track-registry.js +47 -0
- package/dist/es2015/kernel/transport.js +242 -0
- package/dist/es2015/kernel/watchdog.js +38 -0
- package/dist/es2015/presets/dash/index.js +9 -0
- package/dist/es2015/presets/dash-drm/index.js +13 -0
- package/dist/es2015/presets/define.js +45 -0
- package/dist/es2015/presets/dual/index.js +14 -0
- package/dist/es2015/presets/dual-drm/index.js +14 -0
- package/dist/es2015/presets/dual-ts/index.js +14 -0
- package/dist/es2015/presets/dual-ts-drm/index.js +15 -0
- package/dist/es2015/presets/full/index.js +28 -0
- package/dist/es2015/presets/hls/index.js +9 -0
- package/dist/es2015/presets/hls-drm/index.js +13 -0
- package/dist/es2015/presets/hls-ts/index.js +13 -0
- package/dist/es2015/presets/hls-ts-drm/index.js +14 -0
- package/dist/es2015/presets/kernel/index.js +10 -0
- package/dist/es2015/presets/storage.js +24 -0
- package/dist/es2015/presets/tiers.js +93 -0
- package/dist/es2015/protocols/dash-cmaf/index.js +161 -0
- package/dist/es2015/protocols/dash-cmaf/parse.js +491 -0
- package/dist/es2015/protocols/dash-live/index.js +192 -0
- package/dist/es2015/protocols/hls-cmaf/index.js +168 -0
- package/dist/es2015/protocols/hls-cmaf/lexer.js +72 -0
- package/dist/es2015/protocols/hls-cmaf/parse.js +457 -0
- package/dist/es2015/protocols/hls-live/index.js +296 -0
- package/dist/es2015/protocols/live-shared.js +29 -0
- package/dist/es2015/stages/abr/index.js +126 -0
- package/dist/es2015/stages/abr-cap-size/index.js +22 -0
- package/dist/es2015/stages/abr-persist/index.js +52 -0
- package/dist/es2015/stages/aes-128/index.js +76 -0
- package/dist/es2015/stages/alt-audio/index.js +80 -0
- package/dist/es2015/stages/cmaf-timing/index.js +55 -0
- package/dist/es2015/stages/cmcd/index.js +70 -0
- package/dist/es2015/stages/codec-probe/index.js +65 -0
- package/dist/es2015/stages/codec-switch/index.js +48 -0
- package/dist/es2015/stages/content-steering/index.js +179 -0
- package/dist/es2015/stages/drm-shared.js +18 -0
- package/dist/es2015/stages/eme-cenc/index.js +46 -0
- package/dist/es2015/stages/eme-core/index.js +237 -0
- package/dist/es2015/stages/eme-fairplay/index.js +52 -0
- package/dist/es2015/stages/meta-id3/index.js +37 -0
- package/dist/es2015/stages/mp4-box/index.js +27 -0
- package/dist/es2015/stages/nal-scan/index.js +121 -0
- package/dist/es2015/stages/pdt/index.js +34 -0
- package/dist/es2015/stages/recovery/index.js +283 -0
- package/dist/es2015/stages/text-cea608/decode.js +201 -0
- package/dist/es2015/stages/text-cea608/index.js +71 -0
- package/dist/es2015/stages/text-webvtt/index.js +82 -0
- package/dist/es2015/stages/text-webvtt/parse.js +60 -0
- package/dist/es2015/stages/text-webvtt-segmented/index.js +57 -0
- package/dist/es2015/stages/thumbnails/index.js +73 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +423 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/append-queue.d.ts +47 -0
- package/dist/kernel/append-queue.d.ts.map +1 -0
- package/dist/kernel/append-queue.js +180 -0
- package/dist/kernel/append-queue.js.map +1 -0
- package/dist/kernel/bus.d.ts +50 -0
- package/dist/kernel/bus.d.ts.map +1 -0
- package/dist/kernel/bus.js +147 -0
- package/dist/kernel/bus.js.map +1 -0
- package/dist/kernel/context.d.ts +45 -0
- package/dist/kernel/context.d.ts.map +1 -0
- package/dist/kernel/context.js +116 -0
- package/dist/kernel/context.js.map +1 -0
- package/dist/kernel/effects.d.ts +33 -0
- package/dist/kernel/effects.d.ts.map +1 -0
- package/dist/kernel/effects.js +65 -0
- package/dist/kernel/effects.js.map +1 -0
- package/dist/kernel/evictor.d.ts +46 -0
- package/dist/kernel/evictor.d.ts.map +1 -0
- package/dist/kernel/evictor.js +92 -0
- package/dist/kernel/evictor.js.map +1 -0
- package/dist/kernel/lifecycle.d.ts +41 -0
- package/dist/kernel/lifecycle.d.ts.map +1 -0
- package/dist/kernel/lifecycle.js +129 -0
- package/dist/kernel/lifecycle.js.map +1 -0
- package/dist/kernel/loader.d.ts +28 -0
- package/dist/kernel/loader.d.ts.map +1 -0
- package/dist/kernel/loader.js +126 -0
- package/dist/kernel/loader.js.map +1 -0
- package/dist/kernel/mime.d.ts +13 -0
- package/dist/kernel/mime.d.ts.map +1 -0
- package/dist/kernel/mime.js +18 -0
- package/dist/kernel/mime.js.map +1 -0
- package/dist/kernel/mse.d.ts +62 -0
- package/dist/kernel/mse.d.ts.map +1 -0
- package/dist/kernel/mse.js +490 -0
- package/dist/kernel/mse.js.map +1 -0
- package/dist/kernel/reducer.d.ts +39 -0
- package/dist/kernel/reducer.d.ts.map +1 -0
- package/dist/kernel/reducer.js +1316 -0
- package/dist/kernel/reducer.js.map +1 -0
- package/dist/kernel/rendition-select.d.ts +102 -0
- package/dist/kernel/rendition-select.d.ts.map +1 -0
- package/dist/kernel/rendition-select.js +295 -0
- package/dist/kernel/rendition-select.js.map +1 -0
- package/dist/kernel/scheduler.d.ts +57 -0
- package/dist/kernel/scheduler.d.ts.map +1 -0
- package/dist/kernel/scheduler.js +98 -0
- package/dist/kernel/scheduler.js.map +1 -0
- package/dist/kernel/sinks/metadata-sink.d.ts +7 -0
- package/dist/kernel/sinks/metadata-sink.d.ts.map +1 -0
- package/dist/kernel/sinks/metadata-sink.js +5 -0
- package/dist/kernel/sinks/metadata-sink.js.map +1 -0
- package/dist/kernel/sinks/mse-sink.d.ts +17 -0
- package/dist/kernel/sinks/mse-sink.d.ts.map +1 -0
- package/dist/kernel/sinks/mse-sink.js +20 -0
- package/dist/kernel/sinks/mse-sink.js.map +1 -0
- package/dist/kernel/sinks/text-track-sink.d.ts +45 -0
- package/dist/kernel/sinks/text-track-sink.d.ts.map +1 -0
- package/dist/kernel/sinks/text-track-sink.js +184 -0
- package/dist/kernel/sinks/text-track-sink.js.map +1 -0
- package/dist/kernel/timeline.d.ts +57 -0
- package/dist/kernel/timeline.d.ts.map +1 -0
- package/dist/kernel/timeline.js +203 -0
- package/dist/kernel/timeline.js.map +1 -0
- package/dist/kernel/trace.d.ts +42 -0
- package/dist/kernel/trace.d.ts.map +1 -0
- package/dist/kernel/trace.js +165 -0
- package/dist/kernel/trace.js.map +1 -0
- package/dist/kernel/track-registry.d.ts +22 -0
- package/dist/kernel/track-registry.d.ts.map +1 -0
- package/dist/kernel/track-registry.js +42 -0
- package/dist/kernel/track-registry.js.map +1 -0
- package/dist/kernel/transport.d.ts +90 -0
- package/dist/kernel/transport.d.ts.map +1 -0
- package/dist/kernel/transport.js +256 -0
- package/dist/kernel/transport.js.map +1 -0
- package/dist/kernel/watchdog.d.ts +35 -0
- package/dist/kernel/watchdog.d.ts.map +1 -0
- package/dist/kernel/watchdog.js +43 -0
- package/dist/kernel/watchdog.js.map +1 -0
- package/dist/presets/dash/index.d.ts +4 -0
- package/dist/presets/dash/index.d.ts.map +1 -0
- package/dist/presets/dash/index.js +8 -0
- package/dist/presets/dash/index.js.map +1 -0
- package/dist/presets/dash-drm/index.d.ts +4 -0
- package/dist/presets/dash-drm/index.d.ts.map +1 -0
- package/dist/presets/dash-drm/index.js +8 -0
- package/dist/presets/dash-drm/index.js.map +1 -0
- package/dist/presets/define.d.ts +36 -0
- package/dist/presets/define.d.ts.map +1 -0
- package/dist/presets/define.js +39 -0
- package/dist/presets/define.js.map +1 -0
- package/dist/presets/dual/index.d.ts +4 -0
- package/dist/presets/dual/index.d.ts.map +1 -0
- package/dist/presets/dual/index.js +9 -0
- package/dist/presets/dual/index.js.map +1 -0
- package/dist/presets/dual-drm/index.d.ts +4 -0
- package/dist/presets/dual-drm/index.d.ts.map +1 -0
- package/dist/presets/dual-drm/index.js +13 -0
- package/dist/presets/dual-drm/index.js.map +1 -0
- package/dist/presets/dual-ts/index.d.ts +4 -0
- package/dist/presets/dual-ts/index.d.ts.map +1 -0
- package/dist/presets/dual-ts/index.js +8 -0
- package/dist/presets/dual-ts/index.js.map +1 -0
- package/dist/presets/dual-ts-drm/index.d.ts +4 -0
- package/dist/presets/dual-ts-drm/index.d.ts.map +1 -0
- package/dist/presets/dual-ts-drm/index.js +14 -0
- package/dist/presets/dual-ts-drm/index.js.map +1 -0
- package/dist/presets/full/index.d.ts +4 -0
- package/dist/presets/full/index.d.ts.map +1 -0
- package/dist/presets/full/index.js +27 -0
- package/dist/presets/full/index.js.map +1 -0
- package/dist/presets/hls/index.d.ts +4 -0
- package/dist/presets/hls/index.d.ts.map +1 -0
- package/dist/presets/hls/index.js +8 -0
- package/dist/presets/hls/index.js.map +1 -0
- package/dist/presets/hls-drm/index.d.ts +4 -0
- package/dist/presets/hls-drm/index.d.ts.map +1 -0
- package/dist/presets/hls-drm/index.js +8 -0
- package/dist/presets/hls-drm/index.js.map +1 -0
- package/dist/presets/hls-ts/index.d.ts +4 -0
- package/dist/presets/hls-ts/index.d.ts.map +1 -0
- package/dist/presets/hls-ts/index.js +8 -0
- package/dist/presets/hls-ts/index.js.map +1 -0
- package/dist/presets/hls-ts-drm/index.d.ts +4 -0
- package/dist/presets/hls-ts-drm/index.d.ts.map +1 -0
- package/dist/presets/hls-ts-drm/index.js +13 -0
- package/dist/presets/hls-ts-drm/index.js.map +1 -0
- package/dist/presets/kernel/index.d.ts +4 -0
- package/dist/presets/kernel/index.d.ts.map +1 -0
- package/dist/presets/kernel/index.js +9 -0
- package/dist/presets/kernel/index.js.map +1 -0
- package/dist/presets/storage.d.ts +9 -0
- package/dist/presets/storage.d.ts.map +1 -0
- package/dist/presets/storage.js +24 -0
- package/dist/presets/storage.js.map +1 -0
- package/dist/presets/tiers.d.ts +22 -0
- package/dist/presets/tiers.d.ts.map +1 -0
- package/dist/presets/tiers.js +78 -0
- package/dist/presets/tiers.js.map +1 -0
- package/dist/protocols/dash-cmaf/index.d.ts +20 -0
- package/dist/protocols/dash-cmaf/index.d.ts.map +1 -0
- package/dist/protocols/dash-cmaf/index.js +176 -0
- package/dist/protocols/dash-cmaf/index.js.map +1 -0
- package/dist/protocols/dash-cmaf/parse.d.ts +24 -0
- package/dist/protocols/dash-cmaf/parse.d.ts.map +1 -0
- package/dist/protocols/dash-cmaf/parse.js +586 -0
- package/dist/protocols/dash-cmaf/parse.js.map +1 -0
- package/dist/protocols/dash-live/index.d.ts +4 -0
- package/dist/protocols/dash-live/index.d.ts.map +1 -0
- package/dist/protocols/dash-live/index.js +188 -0
- package/dist/protocols/dash-live/index.js.map +1 -0
- package/dist/protocols/hls-cmaf/index.d.ts +17 -0
- package/dist/protocols/hls-cmaf/index.d.ts.map +1 -0
- package/dist/protocols/hls-cmaf/index.js +189 -0
- package/dist/protocols/hls-cmaf/index.js.map +1 -0
- package/dist/protocols/hls-cmaf/lexer.d.ts +25 -0
- package/dist/protocols/hls-cmaf/lexer.d.ts.map +1 -0
- package/dist/protocols/hls-cmaf/lexer.js +81 -0
- package/dist/protocols/hls-cmaf/lexer.js.map +1 -0
- package/dist/protocols/hls-cmaf/parse.d.ts +57 -0
- package/dist/protocols/hls-cmaf/parse.d.ts.map +1 -0
- package/dist/protocols/hls-cmaf/parse.js +572 -0
- package/dist/protocols/hls-cmaf/parse.js.map +1 -0
- package/dist/protocols/hls-live/index.d.ts +4 -0
- package/dist/protocols/hls-live/index.d.ts.map +1 -0
- package/dist/protocols/hls-live/index.js +309 -0
- package/dist/protocols/hls-live/index.js.map +1 -0
- package/dist/protocols/live-shared.d.ts +21 -0
- package/dist/protocols/live-shared.d.ts.map +1 -0
- package/dist/protocols/live-shared.js +32 -0
- package/dist/protocols/live-shared.js.map +1 -0
- package/dist/stages/abr/index.d.ts +16 -0
- package/dist/stages/abr/index.d.ts.map +1 -0
- package/dist/stages/abr/index.js +176 -0
- package/dist/stages/abr/index.js.map +1 -0
- package/dist/stages/abr-cap-size/index.d.ts +8 -0
- package/dist/stages/abr-cap-size/index.d.ts.map +1 -0
- package/dist/stages/abr-cap-size/index.js +21 -0
- package/dist/stages/abr-cap-size/index.js.map +1 -0
- package/dist/stages/abr-persist/index.d.ts +8 -0
- package/dist/stages/abr-persist/index.d.ts.map +1 -0
- package/dist/stages/abr-persist/index.js +44 -0
- package/dist/stages/abr-persist/index.js.map +1 -0
- package/dist/stages/aes-128/index.d.ts +15 -0
- package/dist/stages/aes-128/index.d.ts.map +1 -0
- package/dist/stages/aes-128/index.js +78 -0
- package/dist/stages/aes-128/index.js.map +1 -0
- package/dist/stages/alt-audio/index.d.ts +4 -0
- package/dist/stages/alt-audio/index.d.ts.map +1 -0
- package/dist/stages/alt-audio/index.js +90 -0
- package/dist/stages/alt-audio/index.js.map +1 -0
- package/dist/stages/cmaf-timing/index.d.ts +3 -0
- package/dist/stages/cmaf-timing/index.d.ts.map +1 -0
- package/dist/stages/cmaf-timing/index.js +56 -0
- package/dist/stages/cmaf-timing/index.js.map +1 -0
- package/dist/stages/cmcd/index.d.ts +22 -0
- package/dist/stages/cmcd/index.d.ts.map +1 -0
- package/dist/stages/cmcd/index.js +92 -0
- package/dist/stages/cmcd/index.js.map +1 -0
- package/dist/stages/codec-probe/index.d.ts +9 -0
- package/dist/stages/codec-probe/index.d.ts.map +1 -0
- package/dist/stages/codec-probe/index.js +68 -0
- package/dist/stages/codec-probe/index.js.map +1 -0
- package/dist/stages/codec-switch/index.d.ts +6 -0
- package/dist/stages/codec-switch/index.d.ts.map +1 -0
- package/dist/stages/codec-switch/index.js +59 -0
- package/dist/stages/codec-switch/index.js.map +1 -0
- package/dist/stages/content-steering/index.d.ts +4 -0
- package/dist/stages/content-steering/index.d.ts.map +1 -0
- package/dist/stages/content-steering/index.js +174 -0
- package/dist/stages/content-steering/index.js.map +1 -0
- package/dist/stages/drm-shared.d.ts +46 -0
- package/dist/stages/drm-shared.d.ts.map +1 -0
- package/dist/stages/drm-shared.js +28 -0
- package/dist/stages/drm-shared.js.map +1 -0
- package/dist/stages/eme-cenc/index.d.ts +12 -0
- package/dist/stages/eme-cenc/index.d.ts.map +1 -0
- package/dist/stages/eme-cenc/index.js +53 -0
- package/dist/stages/eme-cenc/index.js.map +1 -0
- package/dist/stages/eme-core/index.d.ts +29 -0
- package/dist/stages/eme-core/index.d.ts.map +1 -0
- package/dist/stages/eme-core/index.js +244 -0
- package/dist/stages/eme-core/index.js.map +1 -0
- package/dist/stages/eme-fairplay/index.d.ts +20 -0
- package/dist/stages/eme-fairplay/index.d.ts.map +1 -0
- package/dist/stages/eme-fairplay/index.js +59 -0
- package/dist/stages/eme-fairplay/index.js.map +1 -0
- package/dist/stages/meta-id3/index.d.ts +14 -0
- package/dist/stages/meta-id3/index.d.ts.map +1 -0
- package/dist/stages/meta-id3/index.js +30 -0
- package/dist/stages/meta-id3/index.js.map +1 -0
- package/dist/stages/mp4-box/index.d.ts +3 -0
- package/dist/stages/mp4-box/index.d.ts.map +1 -0
- package/dist/stages/mp4-box/index.js +19 -0
- package/dist/stages/mp4-box/index.js.map +1 -0
- package/dist/stages/nal-scan/index.d.ts +3 -0
- package/dist/stages/nal-scan/index.d.ts.map +1 -0
- package/dist/stages/nal-scan/index.js +123 -0
- package/dist/stages/nal-scan/index.js.map +1 -0
- package/dist/stages/pdt/index.d.ts +14 -0
- package/dist/stages/pdt/index.d.ts.map +1 -0
- package/dist/stages/pdt/index.js +34 -0
- package/dist/stages/pdt/index.js.map +1 -0
- package/dist/stages/recovery/index.d.ts +16 -0
- package/dist/stages/recovery/index.d.ts.map +1 -0
- package/dist/stages/recovery/index.js +270 -0
- package/dist/stages/recovery/index.js.map +1 -0
- package/dist/stages/text-cea608/decode.d.ts +44 -0
- package/dist/stages/text-cea608/decode.d.ts.map +1 -0
- package/dist/stages/text-cea608/decode.js +225 -0
- package/dist/stages/text-cea608/decode.js.map +1 -0
- package/dist/stages/text-cea608/index.d.ts +3 -0
- package/dist/stages/text-cea608/index.d.ts.map +1 -0
- package/dist/stages/text-cea608/index.js +84 -0
- package/dist/stages/text-cea608/index.js.map +1 -0
- package/dist/stages/text-webvtt/index.d.ts +3 -0
- package/dist/stages/text-webvtt/index.d.ts.map +1 -0
- package/dist/stages/text-webvtt/index.js +82 -0
- package/dist/stages/text-webvtt/index.js.map +1 -0
- package/dist/stages/text-webvtt/parse.d.ts +19 -0
- package/dist/stages/text-webvtt/parse.d.ts.map +1 -0
- package/dist/stages/text-webvtt/parse.js +62 -0
- package/dist/stages/text-webvtt/parse.js.map +1 -0
- package/dist/stages/text-webvtt-segmented/index.d.ts +5 -0
- package/dist/stages/text-webvtt-segmented/index.d.ts.map +1 -0
- package/dist/stages/text-webvtt-segmented/index.js +63 -0
- package/dist/stages/text-webvtt-segmented/index.js.map +1 -0
- package/dist/stages/thumbnails/index.d.ts +34 -0
- package/dist/stages/thumbnails/index.d.ts.map +1 -0
- package/dist/stages/thumbnails/index.js +58 -0
- package/dist/stages/thumbnails/index.js.map +1 -0
- package/dist/types/error.d.ts +33 -0
- package/dist/types/error.d.ts.map +1 -0
- package/dist/types/error.js +7 -0
- package/dist/types/error.js.map +1 -0
- package/dist/types/facade.d.ts +104 -0
- package/dist/types/facade.d.ts.map +1 -0
- package/dist/types/facade.js +2 -0
- package/dist/types/facade.js.map +1 -0
- package/dist/types/ir.d.ts +238 -0
- package/dist/types/ir.d.ts.map +1 -0
- package/dist/types/ir.js +7 -0
- package/dist/types/ir.js.map +1 -0
- package/dist/types/kernel.d.ts +217 -0
- package/dist/types/kernel.d.ts.map +1 -0
- package/dist/types/kernel.js +2 -0
- package/dist/types/kernel.js.map +1 -0
- package/dist/types/messages.d.ts +288 -0
- package/dist/types/messages.d.ts.map +1 -0
- package/dist/types/messages.js +2 -0
- package/dist/types/messages.js.map +1 -0
- package/dist/types/quality.d.ts +108 -0
- package/dist/types/quality.d.ts.map +1 -0
- package/dist/types/quality.js +2 -0
- package/dist/types/quality.js.map +1 -0
- package/dist/types/sink.d.ts +27 -0
- package/dist/types/sink.d.ts.map +1 -0
- package/dist/types/sink.js +2 -0
- package/dist/types/sink.js.map +1 -0
- package/dist/types/stage.d.ts +139 -0
- package/dist/types/stage.d.ts.map +1 -0
- package/dist/types/stage.js +2 -0
- package/dist/types/stage.js.map +1 -0
- package/package.json +132 -0
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
import { normalizeMimeType } from './mime.js';
|
|
2
|
+
import { canSwitchTo, codecFamily, createArbiter, planPinApply } from './rendition-select.js';
|
|
3
|
+
import { bufferedEndFrom, schedule } from './scheduler.js';
|
|
4
|
+
import { sbIdFor } from './sinks/mse-sink.js';
|
|
5
|
+
import { DEFAULT_TRACE_CAPACITY } from './trace.js';
|
|
6
|
+
/**
|
|
7
|
+
* The default tuning. Every value is overridable through the `config`
|
|
8
|
+
* parameters of `initialState` and `createReducer`; these are starting
|
|
9
|
+
* points, not policy.
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_KERNEL_CONFIG = {
|
|
12
|
+
// Small enough to ride out one odd sample.
|
|
13
|
+
ewmaAlpha: 0.2,
|
|
14
|
+
// Large enough that a collapse shows within two or three samples.
|
|
15
|
+
ewmaFastAlpha: 0.6,
|
|
16
|
+
// An init segment or playlist measures connection jitter, not bandwidth.
|
|
17
|
+
ewmaMinSampleBytes: 10_000,
|
|
18
|
+
// Three strikes: enough to ride out one transient, few enough that a
|
|
19
|
+
// structurally unplayable track halts before the loop burns the network.
|
|
20
|
+
bufferErrorLimit: 3,
|
|
21
|
+
// Looser than the error breaker: flushes and seeks legitimately repeat a
|
|
22
|
+
// decision once or twice; six identical ones in a row never happen.
|
|
23
|
+
repeatFetchLimit: 5,
|
|
24
|
+
backBufferSeconds: 30,
|
|
25
|
+
bufferGoalSeconds: 30,
|
|
26
|
+
manifestTimeoutMs: 10_000,
|
|
27
|
+
traceCapacity: DEFAULT_TRACE_CAPACITY,
|
|
28
|
+
// Long enough that a recovery stage's zero-delay commands apply first,
|
|
29
|
+
// short enough that a retry without recovery still feels prompt.
|
|
30
|
+
baseRetryDelayMs: 400,
|
|
31
|
+
mediaTimeNormalized: false,
|
|
32
|
+
};
|
|
33
|
+
export function resolveConfig(config) {
|
|
34
|
+
return { ...DEFAULT_KERNEL_CONFIG, ...config };
|
|
35
|
+
}
|
|
36
|
+
export function initialState(config) {
|
|
37
|
+
const cfg = resolveConfig(config);
|
|
38
|
+
return {
|
|
39
|
+
lifecycle: { phase: 'idle' },
|
|
40
|
+
presentation: null,
|
|
41
|
+
timeline: { periodOffsets: new Map(), discontinuitySeq: 0 },
|
|
42
|
+
buffers: new Map(),
|
|
43
|
+
bufferErrors: new Map(),
|
|
44
|
+
cues: new Map(),
|
|
45
|
+
live: null,
|
|
46
|
+
scheduling: { inflight: new Map(), bufferGoal: cfg.bufferGoalSeconds, tokenSeq: 0 },
|
|
47
|
+
tracks: { active: new Map(), available: [] },
|
|
48
|
+
quality: { version: 0, constraints: new Map(), pinned: null, active: null, appendLog: [] },
|
|
49
|
+
stats: { throughputEwma: 0, throughputFastEwma: 0 },
|
|
50
|
+
playback: { currentTime: 0, buffered: [], seeking: false },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Gap width treated as continuous when measuring buffered spans. */
|
|
54
|
+
const GAP_TOLERANCE = 0.25;
|
|
55
|
+
/** The codecs part of an MSE type string ('video/mp4; codecs="avc1.42c00d"' -> 'avc1.42c00d'). */
|
|
56
|
+
function bufferCodecString(type) {
|
|
57
|
+
const match = /codecs="?([^"]+)"?/.exec(type);
|
|
58
|
+
return match?.[1] ?? null;
|
|
59
|
+
}
|
|
60
|
+
function reject(state, command, reason) {
|
|
61
|
+
return [state, [{ kind: 'emit', event: 'command:rejected', payload: { command, reason } }]];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A manifest that will never yield a presentation. Every path lands here:
|
|
65
|
+
* an adapter's parse failure (MANIFEST_FAILED), a fetch failure on the
|
|
66
|
+
* manifest request, a `mimeType` no adapter accepts, and bytes no adapter
|
|
67
|
+
* claims. One phase transition and one error shape, so an integrator
|
|
68
|
+
* handles a single code per cause.
|
|
69
|
+
*/
|
|
70
|
+
function failManifest(state, error, extra = {}) {
|
|
71
|
+
const phase = error.fatal ? 'error' : state.lifecycle.phase;
|
|
72
|
+
return [
|
|
73
|
+
{ ...state, lifecycle: { phase } },
|
|
74
|
+
[
|
|
75
|
+
{
|
|
76
|
+
kind: 'emit',
|
|
77
|
+
event: 'error',
|
|
78
|
+
payload: {
|
|
79
|
+
category: error.category,
|
|
80
|
+
code: error.code,
|
|
81
|
+
fatal: error.fatal,
|
|
82
|
+
recoverable: error.recoverable,
|
|
83
|
+
...extra,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
/** The in-flight manifest request a SEGMENT_LOADED fact answers, or null. At most one is ever in flight. */
|
|
90
|
+
function manifestRequestFor(state, msg) {
|
|
91
|
+
if (msg.trackId !== 'manifest')
|
|
92
|
+
return null;
|
|
93
|
+
for (const request of state.scheduling.inflight.values()) {
|
|
94
|
+
if (request.trackId === 'manifest' &&
|
|
95
|
+
(msg.token === undefined || msg.token === request.token)) {
|
|
96
|
+
return request;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
function bumped(quality) {
|
|
102
|
+
return { ...quality, version: quality.version + 1 };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Forgets the last fetch decision. A pin or a seek legitimately makes the
|
|
106
|
+
* scheduler repeat itself; only unprompted repetition means a stuck loop.
|
|
107
|
+
*/
|
|
108
|
+
function clearRepeat(scheduling) {
|
|
109
|
+
if (scheduling.repeat === undefined)
|
|
110
|
+
return scheduling;
|
|
111
|
+
const { repeat: _repeat, ...rest } = scheduling;
|
|
112
|
+
return rest;
|
|
113
|
+
}
|
|
114
|
+
function ewma(previous, sample, alpha) {
|
|
115
|
+
return previous === 0 ? sample : alpha * sample + (1 - alpha) * previous;
|
|
116
|
+
}
|
|
117
|
+
/** Companion groups present, as `contentType:groupId`, for the coupling filter. */
|
|
118
|
+
function availableGroups(state) {
|
|
119
|
+
const groups = new Set();
|
|
120
|
+
for (const period of state.presentation?.periods ?? []) {
|
|
121
|
+
for (const track of period.tracks) {
|
|
122
|
+
if (track.contentType !== 'audio' && track.contentType !== 'text')
|
|
123
|
+
continue;
|
|
124
|
+
const colon = track.id.indexOf(':');
|
|
125
|
+
const group = colon === -1 ? track.id : track.id.slice(0, colon);
|
|
126
|
+
groups.add(`${track.contentType}:${group}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return groups;
|
|
130
|
+
}
|
|
131
|
+
/** Merges one span into a sorted coverage list, coalescing overlaps. */
|
|
132
|
+
function mergeCoverage(coverage, added) {
|
|
133
|
+
const merged = [];
|
|
134
|
+
let pending = added;
|
|
135
|
+
for (const range of coverage) {
|
|
136
|
+
if (range.end < pending.start || range.start > pending.end) {
|
|
137
|
+
merged.push(range);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
pending = {
|
|
141
|
+
start: Math.min(range.start, pending.start),
|
|
142
|
+
end: Math.max(range.end, pending.end),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
merged.push(pending);
|
|
147
|
+
return merged.sort((a, b) => a.start - b.start);
|
|
148
|
+
}
|
|
149
|
+
function findTrack(presentation, trackId) {
|
|
150
|
+
if (presentation === null)
|
|
151
|
+
return null;
|
|
152
|
+
for (const period of presentation.periods) {
|
|
153
|
+
for (const track of period.tracks) {
|
|
154
|
+
if (track.id === trackId)
|
|
155
|
+
return track;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function findTrackSite(presentation, trackId) {
|
|
161
|
+
for (const period of presentation.periods) {
|
|
162
|
+
for (const track of period.tracks) {
|
|
163
|
+
if (track.id === trackId)
|
|
164
|
+
return { track, period };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
function findRendition(presentation, renditionId) {
|
|
170
|
+
if (presentation === null)
|
|
171
|
+
return null;
|
|
172
|
+
for (const period of presentation.periods) {
|
|
173
|
+
for (const track of period.tracks) {
|
|
174
|
+
for (const rendition of track.renditions) {
|
|
175
|
+
if (rendition.id === renditionId)
|
|
176
|
+
return { rendition, track, period };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Whether a completed request feeds the throughput estimate: only media of
|
|
184
|
+
* the lead track (video where there is one, else audio). An audio segment
|
|
185
|
+
* beside video, a text cue, or a playlist is a small transfer whose time
|
|
186
|
+
* is latency, not bandwidth.
|
|
187
|
+
*/
|
|
188
|
+
function samplesThroughput(state, request) {
|
|
189
|
+
const lead = state.tracks.active.get('video') ?? state.tracks.active.get('audio');
|
|
190
|
+
return lead === undefined ? request.renditionId === undefined : request.trackId === lead;
|
|
191
|
+
}
|
|
192
|
+
/** Abort effects for every matching in-flight request, plus the state with them removed. */
|
|
193
|
+
function abortInflight(state, trackId) {
|
|
194
|
+
const effects = [];
|
|
195
|
+
const inflight = new Map(state.scheduling.inflight);
|
|
196
|
+
for (const [token, request] of state.scheduling.inflight) {
|
|
197
|
+
if (trackId !== undefined && request.trackId !== trackId)
|
|
198
|
+
continue;
|
|
199
|
+
effects.push({ kind: 'abort', token });
|
|
200
|
+
inflight.delete(token);
|
|
201
|
+
}
|
|
202
|
+
if (effects.length === 0)
|
|
203
|
+
return [state, []];
|
|
204
|
+
return [{ ...state, scheduling: { ...state.scheduling, inflight } }, effects];
|
|
205
|
+
}
|
|
206
|
+
function reduceCommand(state, msg, cfg, hooks) {
|
|
207
|
+
switch (msg.type) {
|
|
208
|
+
case 'ATTACH': {
|
|
209
|
+
if (state.lifecycle.phase !== 'idle') {
|
|
210
|
+
return reject(state, msg.type, 'already attached');
|
|
211
|
+
}
|
|
212
|
+
return [{ ...state, lifecycle: { phase: 'attaching' } }, []];
|
|
213
|
+
}
|
|
214
|
+
case 'DETACH': {
|
|
215
|
+
// Idempotent and safe from any phase, including error.
|
|
216
|
+
const [, aborts] = abortInflight(state);
|
|
217
|
+
return [initialState(cfg), aborts];
|
|
218
|
+
}
|
|
219
|
+
case 'LOAD': {
|
|
220
|
+
if (state.lifecycle.phase === 'idle') {
|
|
221
|
+
return reject(state, msg.type, 'not attached');
|
|
222
|
+
}
|
|
223
|
+
if (state.lifecycle.phase !== 'attaching') {
|
|
224
|
+
return reject(state, msg.type, 'already loaded');
|
|
225
|
+
}
|
|
226
|
+
// An explicit mimeType is authoritative: when no composed adapter
|
|
227
|
+
// declares it, the load fails here, before any bytes move. An
|
|
228
|
+
// integrator's source resolver relies on this being cheap.
|
|
229
|
+
if (msg.mimeType !== undefined && hooks.manifestTypes !== undefined) {
|
|
230
|
+
const mimeType = normalizeMimeType(msg.mimeType);
|
|
231
|
+
if (!hooks.manifestTypes.has(mimeType)) {
|
|
232
|
+
return failManifest(state, { category: 'manifest', code: 'MANIFEST_UNSUPPORTED', fatal: true, recoverable: false }, { mimeType });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const tokenSeq = state.scheduling.tokenSeq + 1;
|
|
236
|
+
const token = `t${tokenSeq}:manifest`;
|
|
237
|
+
const inflight = new Map(state.scheduling.inflight);
|
|
238
|
+
inflight.set(token, { token, trackId: 'manifest', seq: 0, url: msg.url });
|
|
239
|
+
return [
|
|
240
|
+
{
|
|
241
|
+
...state,
|
|
242
|
+
lifecycle: { phase: 'loading' },
|
|
243
|
+
scheduling: { ...state.scheduling, inflight, tokenSeq },
|
|
244
|
+
},
|
|
245
|
+
[{ kind: 'fetch', token, url: msg.url, timeout: cfg.manifestTimeoutMs }],
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
case 'UNLOAD': {
|
|
249
|
+
const [, aborts] = abortInflight(state);
|
|
250
|
+
const fresh = initialState(cfg);
|
|
251
|
+
const phase = state.lifecycle.phase === 'idle' ? 'idle' : 'attaching';
|
|
252
|
+
// The source goes, so do its cues: a native track keeps rendering
|
|
253
|
+
// what it holds, and the next source must not start under the last
|
|
254
|
+
// subtitle of the previous one.
|
|
255
|
+
const clears = [...state.cues.keys()].map((trackId) => ({
|
|
256
|
+
kind: 'clearCues',
|
|
257
|
+
trackId,
|
|
258
|
+
start: 0,
|
|
259
|
+
end: Number.POSITIVE_INFINITY,
|
|
260
|
+
}));
|
|
261
|
+
return [
|
|
262
|
+
{
|
|
263
|
+
...fresh,
|
|
264
|
+
lifecycle: { phase },
|
|
265
|
+
scheduling: { ...fresh.scheduling, tokenSeq: state.scheduling.tokenSeq },
|
|
266
|
+
},
|
|
267
|
+
[...aborts, ...clears],
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
case 'SEEK': {
|
|
271
|
+
if (state.presentation === null) {
|
|
272
|
+
return reject(state, msg.type, 'no source');
|
|
273
|
+
}
|
|
274
|
+
const [next, aborts] = abortInflight(state);
|
|
275
|
+
// A live span bounds the seek: behind it nothing is available any
|
|
276
|
+
// more, ahead of it nothing exists yet. The edge, not the span end,
|
|
277
|
+
// is the far bound, so a seek to "now" lands where playback can start.
|
|
278
|
+
const to = state.live === null
|
|
279
|
+
? msg.to
|
|
280
|
+
: Math.min(Math.max(msg.to, state.live.span.start), state.live.edge);
|
|
281
|
+
return [
|
|
282
|
+
{ ...next, scheduling: clearRepeat(next.scheduling) },
|
|
283
|
+
[...aborts, { kind: 'seekElement', to }],
|
|
284
|
+
];
|
|
285
|
+
}
|
|
286
|
+
case 'SEEK_TO_LIVE_EDGE': {
|
|
287
|
+
if (state.presentation === null || !state.presentation.isLive) {
|
|
288
|
+
return reject(state, msg.type, 'not live');
|
|
289
|
+
}
|
|
290
|
+
if (state.live === null) {
|
|
291
|
+
return reject(state, msg.type, 'no live support loaded');
|
|
292
|
+
}
|
|
293
|
+
const [next, aborts] = abortInflight(state);
|
|
294
|
+
return [
|
|
295
|
+
{ ...next, scheduling: clearRepeat(next.scheduling) },
|
|
296
|
+
[...aborts, { kind: 'seekElement', to: state.live.edge }],
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
case 'SELECT_TRACK': {
|
|
300
|
+
const track = findTrack(state.presentation, msg.trackId);
|
|
301
|
+
if (track === null) {
|
|
302
|
+
return reject(state, msg.type, `unknown track: ${msg.trackId}`);
|
|
303
|
+
}
|
|
304
|
+
const previous = state.tracks.active.get(track.contentType);
|
|
305
|
+
const active = new Map(state.tracks.active);
|
|
306
|
+
active.set(track.contentType, track.id);
|
|
307
|
+
const effects = [];
|
|
308
|
+
let buffers = state.buffers;
|
|
309
|
+
let inflight = state.scheduling.inflight;
|
|
310
|
+
// A media track change mid-stream must clear the old track's buffer,
|
|
311
|
+
// or the new track's segment (which re-covers time the old track
|
|
312
|
+
// already buffered) overlaps stale content and strict decoders
|
|
313
|
+
// (WebKit) reject the append. The whole buffer goes; the new track
|
|
314
|
+
// refills from the playhead's segment with a brief gap.
|
|
315
|
+
if (previous !== undefined &&
|
|
316
|
+
previous !== track.id &&
|
|
317
|
+
(track.contentType === 'audio' || track.contentType === 'video')) {
|
|
318
|
+
const sbId = sbIdFor(track.contentType);
|
|
319
|
+
if (state.buffers.has(sbId)) {
|
|
320
|
+
effects.push({
|
|
321
|
+
kind: 'remove',
|
|
322
|
+
sbId,
|
|
323
|
+
start: 0,
|
|
324
|
+
end: Number.POSITIVE_INFINITY,
|
|
325
|
+
});
|
|
326
|
+
const buffer = state.buffers.get(sbId);
|
|
327
|
+
if (buffer !== undefined) {
|
|
328
|
+
// Force an init re-fetch for the new track: its initFor no
|
|
329
|
+
// longer matches, so scheduling fetches init before media.
|
|
330
|
+
const next = new Map(state.buffers);
|
|
331
|
+
const { initFor: _initFor, ...rest } = buffer;
|
|
332
|
+
next.set(sbId, rest);
|
|
333
|
+
buffers = next;
|
|
334
|
+
}
|
|
335
|
+
// Drop the old track's in-flight fetches.
|
|
336
|
+
const pruned = new Map(state.scheduling.inflight);
|
|
337
|
+
for (const [token, request] of state.scheduling.inflight) {
|
|
338
|
+
if (request.trackId === previous) {
|
|
339
|
+
effects.push({ kind: 'abort', token });
|
|
340
|
+
pruned.delete(token);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
inflight = pruned;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (previous !== track.id) {
|
|
347
|
+
effects.push({
|
|
348
|
+
kind: 'emit',
|
|
349
|
+
event: 'tracks:selected',
|
|
350
|
+
payload: { contentType: track.contentType, trackId: track.id },
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return [
|
|
354
|
+
{
|
|
355
|
+
...state,
|
|
356
|
+
tracks: { ...state.tracks, active },
|
|
357
|
+
buffers,
|
|
358
|
+
scheduling: clearRepeat({ ...state.scheduling, inflight }),
|
|
359
|
+
quality: bumped(state.quality),
|
|
360
|
+
},
|
|
361
|
+
effects,
|
|
362
|
+
];
|
|
363
|
+
}
|
|
364
|
+
case 'DESELECT_TRACK': {
|
|
365
|
+
if (msg.contentType !== 'text' && msg.contentType !== 'metadata') {
|
|
366
|
+
return reject(state, msg.type, 'video and audio always keep a selection');
|
|
367
|
+
}
|
|
368
|
+
const trackId = state.tracks.active.get(msg.contentType);
|
|
369
|
+
if (trackId === undefined)
|
|
370
|
+
return [state, []];
|
|
371
|
+
const active = new Map(state.tracks.active);
|
|
372
|
+
active.delete(msg.contentType);
|
|
373
|
+
const cues = new Map(state.cues);
|
|
374
|
+
cues.delete(trackId);
|
|
375
|
+
const [aborted, abortEffects] = abortInflight(state, trackId);
|
|
376
|
+
return [
|
|
377
|
+
{
|
|
378
|
+
...aborted,
|
|
379
|
+
tracks: { ...state.tracks, active },
|
|
380
|
+
cues,
|
|
381
|
+
quality: bumped(state.quality),
|
|
382
|
+
},
|
|
383
|
+
[
|
|
384
|
+
...abortEffects,
|
|
385
|
+
{ kind: 'clearCues', trackId, start: 0, end: Number.POSITIVE_INFINITY },
|
|
386
|
+
{
|
|
387
|
+
kind: 'emit',
|
|
388
|
+
event: 'tracks:selected',
|
|
389
|
+
payload: { contentType: msg.contentType, trackId: null },
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
];
|
|
393
|
+
}
|
|
394
|
+
case 'PIN_RENDITION': {
|
|
395
|
+
const site = findRendition(state.presentation, msg.renditionId);
|
|
396
|
+
if (site === null) {
|
|
397
|
+
return reject(state, msg.type, `unknown rendition: ${msg.renditionId}`);
|
|
398
|
+
}
|
|
399
|
+
const pinnedState = {
|
|
400
|
+
...state,
|
|
401
|
+
quality: bumped({ ...state.quality, pinned: site.rendition.id, active: site.rendition.id }),
|
|
402
|
+
};
|
|
403
|
+
// Apply planning needs a buffer to flush; text and metadata pins
|
|
404
|
+
// change future fetches only.
|
|
405
|
+
if (site.track.contentType !== 'video' && site.track.contentType !== 'audio') {
|
|
406
|
+
return [pinnedState, []];
|
|
407
|
+
}
|
|
408
|
+
const sbId = sbIdFor(site.track.contentType);
|
|
409
|
+
const inflightTokens = [];
|
|
410
|
+
for (const request of state.scheduling.inflight.values()) {
|
|
411
|
+
if (request.trackId === site.track.id)
|
|
412
|
+
inflightTokens.push(request.token);
|
|
413
|
+
}
|
|
414
|
+
const plan = planPinApply({
|
|
415
|
+
strategy: msg.apply,
|
|
416
|
+
currentTime: state.playback.currentTime,
|
|
417
|
+
ranges: state.buffers.get(sbId)?.ranges ?? [],
|
|
418
|
+
sbId,
|
|
419
|
+
trackId: site.track.id,
|
|
420
|
+
inflightTokens,
|
|
421
|
+
period: site.period,
|
|
422
|
+
rendition: site.rendition,
|
|
423
|
+
tokenSeq: state.scheduling.tokenSeq,
|
|
424
|
+
});
|
|
425
|
+
const inflight = new Map(state.scheduling.inflight);
|
|
426
|
+
for (const token of inflightTokens)
|
|
427
|
+
inflight.delete(token);
|
|
428
|
+
for (const request of plan.requests)
|
|
429
|
+
inflight.set(request.token, request);
|
|
430
|
+
return [
|
|
431
|
+
{
|
|
432
|
+
...pinnedState,
|
|
433
|
+
scheduling: clearRepeat({ ...state.scheduling, inflight, tokenSeq: plan.tokenSeq }),
|
|
434
|
+
},
|
|
435
|
+
plan.effects,
|
|
436
|
+
];
|
|
437
|
+
}
|
|
438
|
+
case 'RELEASE_PIN': {
|
|
439
|
+
return [{ ...state, quality: bumped({ ...state.quality, pinned: null }) }, []];
|
|
440
|
+
}
|
|
441
|
+
case 'CONSTRAIN': {
|
|
442
|
+
const constraints = new Map(state.quality.constraints);
|
|
443
|
+
constraints.set(msg.source, msg.constraint);
|
|
444
|
+
// A changed allowed set means the last fetch decision is no longer
|
|
445
|
+
// the same decision; reset the repeat breaker so recovery's exclusion
|
|
446
|
+
// gets a chance to switch renditions before failures re-trip it.
|
|
447
|
+
return [
|
|
448
|
+
{
|
|
449
|
+
...state,
|
|
450
|
+
quality: bumped({ ...state.quality, constraints }),
|
|
451
|
+
scheduling: clearRepeat(state.scheduling),
|
|
452
|
+
},
|
|
453
|
+
[],
|
|
454
|
+
];
|
|
455
|
+
}
|
|
456
|
+
case 'RELEASE_CONSTRAINT': {
|
|
457
|
+
if (!state.quality.constraints.has(msg.source))
|
|
458
|
+
return [state, []];
|
|
459
|
+
const constraints = new Map(state.quality.constraints);
|
|
460
|
+
constraints.delete(msg.source);
|
|
461
|
+
return [
|
|
462
|
+
{
|
|
463
|
+
...state,
|
|
464
|
+
quality: bumped({ ...state.quality, constraints }),
|
|
465
|
+
scheduling: clearRepeat(state.scheduling),
|
|
466
|
+
},
|
|
467
|
+
[],
|
|
468
|
+
];
|
|
469
|
+
}
|
|
470
|
+
case 'SET_BUFFER_GOAL': {
|
|
471
|
+
if (!Number.isFinite(msg.seconds) || msg.seconds <= 0) {
|
|
472
|
+
return reject(state, msg.type, 'invalid buffer goal');
|
|
473
|
+
}
|
|
474
|
+
return [{ ...state, scheduling: { ...state.scheduling, bufferGoal: msg.seconds } }, []];
|
|
475
|
+
}
|
|
476
|
+
case 'ABORT_INFLIGHT': {
|
|
477
|
+
return abortInflight(state, msg.trackId);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function reduceFact(state, msg, cfg, hooks) {
|
|
482
|
+
switch (msg.type) {
|
|
483
|
+
case 'ELEMENT_ATTACHED':
|
|
484
|
+
case 'MEDIASOURCE_OPEN':
|
|
485
|
+
case 'MEDIASOURCE_CLOSED':
|
|
486
|
+
// Lifecycle detail owned by the mse module. Absorbed for the trace.
|
|
487
|
+
return [state, []];
|
|
488
|
+
case 'MANIFEST_LOADED': {
|
|
489
|
+
const available = [];
|
|
490
|
+
for (const period of msg.presentation.periods) {
|
|
491
|
+
for (const track of period.tracks)
|
|
492
|
+
available.push(track.id);
|
|
493
|
+
}
|
|
494
|
+
const inflight = new Map(state.scheduling.inflight);
|
|
495
|
+
for (const [token, request] of inflight) {
|
|
496
|
+
if (request.trackId === 'manifest')
|
|
497
|
+
inflight.delete(token);
|
|
498
|
+
}
|
|
499
|
+
const phase = state.lifecycle.phase === 'loading' ? 'ready' : state.lifecycle.phase;
|
|
500
|
+
// Default activation: the first video and audio track, so a manifest
|
|
501
|
+
// alone yields a playable composition without a SELECT_TRACK.
|
|
502
|
+
const active = new Map(state.tracks.active);
|
|
503
|
+
for (const period of msg.presentation.periods) {
|
|
504
|
+
for (const track of period.tracks) {
|
|
505
|
+
if ((track.contentType === 'video' || track.contentType === 'audio') &&
|
|
506
|
+
!active.has(track.contentType)) {
|
|
507
|
+
active.set(track.contentType, track.id);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const loaded = {
|
|
512
|
+
...state,
|
|
513
|
+
lifecycle: { phase },
|
|
514
|
+
presentation: msg.presentation,
|
|
515
|
+
scheduling: { ...state.scheduling, inflight },
|
|
516
|
+
tracks: { active, available },
|
|
517
|
+
quality: bumped(state.quality),
|
|
518
|
+
};
|
|
519
|
+
// A live reload feeds a MANIFEST_LOADED per playlist; the track list
|
|
520
|
+
// only changed if the ids did.
|
|
521
|
+
const sameTracks = available.length === state.tracks.available.length &&
|
|
522
|
+
available.every((id, index) => id === state.tracks.available[index]);
|
|
523
|
+
const manifestEffects = sameTracks
|
|
524
|
+
? []
|
|
525
|
+
: [{ kind: 'emit', event: 'tracks:changed', payload: { available } }];
|
|
526
|
+
// The manifest DRM route: emit every track's protection schemes so
|
|
527
|
+
// eme-core (if loaded) can open sessions. Serializable init data
|
|
528
|
+
// rides the event; nothing DRM-specific enters the reducer.
|
|
529
|
+
const schemes = msg.presentation.periods.flatMap((period) => period.tracks.flatMap((track) => track.protection?.schemes ?? []));
|
|
530
|
+
if (schemes.length > 0) {
|
|
531
|
+
manifestEffects.push({
|
|
532
|
+
kind: 'emit',
|
|
533
|
+
event: 'presentation:protection',
|
|
534
|
+
payload: schemes,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
if (!msg.presentation.isLive && msg.presentation.duration !== undefined) {
|
|
538
|
+
manifestEffects.push({ kind: 'setDuration', seconds: msg.presentation.duration });
|
|
539
|
+
}
|
|
540
|
+
return [loaded, manifestEffects];
|
|
541
|
+
}
|
|
542
|
+
case 'MANIFEST_FAILED':
|
|
543
|
+
return failManifest(state, msg.error);
|
|
544
|
+
case 'PLAYLIST_REFRESHED':
|
|
545
|
+
// Merging a refreshed live playlist into the presentation belongs to
|
|
546
|
+
// the protocol stages. Absorbed for the trace until they exist.
|
|
547
|
+
return [state, []];
|
|
548
|
+
case 'SEGMENT_LOADED': {
|
|
549
|
+
let matched = null;
|
|
550
|
+
if (msg.token !== undefined) {
|
|
551
|
+
// The token names one request. Track and sequence alone would also
|
|
552
|
+
// match the refetch that replaced an aborted request.
|
|
553
|
+
matched = state.scheduling.inflight.get(msg.token) ?? null;
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
for (const request of state.scheduling.inflight.values()) {
|
|
557
|
+
if (request.trackId === msg.trackId && request.seq === msg.seq) {
|
|
558
|
+
matched = request;
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (matched === null) {
|
|
564
|
+
// Late arrival for an aborted or superseded request. The bytes
|
|
565
|
+
// already crossed the network; absorb and drop them.
|
|
566
|
+
return [state, []];
|
|
567
|
+
}
|
|
568
|
+
const inflight = new Map(state.scheduling.inflight);
|
|
569
|
+
inflight.delete(matched.token);
|
|
570
|
+
// The throughput estimate follows the lead track: a 96 KB audio
|
|
571
|
+
// segment beside a 2 MB video segment measures latency, not the link,
|
|
572
|
+
// and would drag the estimate down to a fraction of what video sees.
|
|
573
|
+
const sampleBps = msg.rtt > 0 && msg.size >= cfg.ewmaMinSampleBytes && samplesThroughput(state, matched)
|
|
574
|
+
? (msg.size * 8000) / msg.rtt
|
|
575
|
+
: null;
|
|
576
|
+
const stats = sampleBps === null
|
|
577
|
+
? state.stats
|
|
578
|
+
: {
|
|
579
|
+
throughputEwma: ewma(state.stats.throughputEwma, sampleBps, cfg.ewmaAlpha),
|
|
580
|
+
throughputFastEwma: ewma(state.stats.throughputFastEwma, sampleBps, cfg.ewmaFastAlpha),
|
|
581
|
+
};
|
|
582
|
+
let buffers = state.buffers;
|
|
583
|
+
let timeline = state.timeline;
|
|
584
|
+
let quality = state.quality;
|
|
585
|
+
let cues = state.cues;
|
|
586
|
+
const effects = [];
|
|
587
|
+
// No SourceBuffer destination: a cue track's segment routes to its
|
|
588
|
+
// sink through the deliver effect. Coverage merges here, from the
|
|
589
|
+
// request's own timing, so a parse failure still counts as covered
|
|
590
|
+
// and the scheduler never refetch-loops a bad segment.
|
|
591
|
+
const cueTrack = matched.sbId === undefined ? findTrack(state.presentation, matched.trackId) : null;
|
|
592
|
+
if (cueTrack !== null &&
|
|
593
|
+
(cueTrack.contentType === 'text' || cueTrack.contentType === 'metadata') &&
|
|
594
|
+
matched.segmentStart !== undefined) {
|
|
595
|
+
const span = {
|
|
596
|
+
start: matched.segmentStart,
|
|
597
|
+
end: matched.segmentStart + (matched.segmentDuration ?? 0),
|
|
598
|
+
};
|
|
599
|
+
const next = new Map(cues);
|
|
600
|
+
next.set(matched.trackId, mergeCoverage(cues.get(matched.trackId) ?? [], span));
|
|
601
|
+
cues = next;
|
|
602
|
+
effects.push({
|
|
603
|
+
kind: 'deliver',
|
|
604
|
+
trackId: matched.trackId,
|
|
605
|
+
contentType: cueTrack.contentType,
|
|
606
|
+
data: msg.bytes,
|
|
607
|
+
meta: {
|
|
608
|
+
trackId: matched.trackId,
|
|
609
|
+
renditionId: matched.renditionId ?? '',
|
|
610
|
+
contentType: cueTrack.contentType,
|
|
611
|
+
seq: matched.seq,
|
|
612
|
+
start: matched.segmentStart,
|
|
613
|
+
duration: matched.segmentDuration ?? 0,
|
|
614
|
+
isInit: matched.seq < 0,
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
if (matched.sbId !== undefined) {
|
|
619
|
+
// seq below zero is the init-segment convention: mark the buffer
|
|
620
|
+
// initialized for this rendition so media scheduling can proceed.
|
|
621
|
+
if (matched.seq < 0 && matched.renditionId !== undefined) {
|
|
622
|
+
const buffer = state.buffers.get(matched.sbId);
|
|
623
|
+
if (buffer !== undefined) {
|
|
624
|
+
// A switch that changes the codec family needs a changeType
|
|
625
|
+
// before the new init, or the append fails. In-family profile
|
|
626
|
+
// changes append the new init bare: browsers accept those in
|
|
627
|
+
// practice, and forcing changeType there breaks WebKit. The
|
|
628
|
+
// reducer emits the mechanism; codec-switch's policy governed
|
|
629
|
+
// whether abr proposed the switch at all.
|
|
630
|
+
const targetSite = findRendition(state.presentation, matched.renditionId);
|
|
631
|
+
const targetCodecs = targetSite === null
|
|
632
|
+
? buffer.codecs
|
|
633
|
+
: targetSite.rendition.codecs === null
|
|
634
|
+
? targetSite.rendition.mimeType
|
|
635
|
+
: `${targetSite.rendition.mimeType}; codecs="${targetSite.rendition.codecs}"`;
|
|
636
|
+
const familyChanged = codecFamily(targetSite?.rendition.codecs ?? null) !==
|
|
637
|
+
codecFamily(bufferCodecString(buffer.codecs));
|
|
638
|
+
const nextBuffers = new Map(state.buffers);
|
|
639
|
+
if (targetCodecs !== buffer.codecs && familyChanged) {
|
|
640
|
+
effects.push({ kind: 'changeType', sbId: matched.sbId, codecs: targetCodecs });
|
|
641
|
+
}
|
|
642
|
+
nextBuffers.set(matched.sbId, {
|
|
643
|
+
...buffer,
|
|
644
|
+
initFor: matched.renditionId,
|
|
645
|
+
codecs: targetCodecs,
|
|
646
|
+
});
|
|
647
|
+
buffers = nextBuffers;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// A media segment decodes only under its own rendition's init
|
|
651
|
+
// segment. A constraint or pin can move the buffer to another
|
|
652
|
+
// rendition while this request was in flight, and its bytes would
|
|
653
|
+
// then meet the wrong parameter sets: a hardware decoder rejects
|
|
654
|
+
// them as malformed. Drop them; scheduling below refetches the
|
|
655
|
+
// span from the rendition the buffer is set up for.
|
|
656
|
+
const initFor = buffers.get(matched.sbId)?.initFor;
|
|
657
|
+
if (matched.seq >= 0 &&
|
|
658
|
+
matched.renditionId !== undefined &&
|
|
659
|
+
initFor !== undefined &&
|
|
660
|
+
initFor !== matched.renditionId) {
|
|
661
|
+
const next = {
|
|
662
|
+
...state,
|
|
663
|
+
scheduling: { ...state.scheduling, inflight },
|
|
664
|
+
stats,
|
|
665
|
+
buffers,
|
|
666
|
+
cues,
|
|
667
|
+
};
|
|
668
|
+
// SEGMENT_LOADED drives scheduling on the way out, so the span is
|
|
669
|
+
// refetched from the rendition the buffer holds the init for.
|
|
670
|
+
return [
|
|
671
|
+
next,
|
|
672
|
+
[
|
|
673
|
+
...effects,
|
|
674
|
+
{
|
|
675
|
+
kind: 'emit',
|
|
676
|
+
event: 'quality:stale-segment',
|
|
677
|
+
payload: { renditionId: matched.renditionId, seq: matched.seq, initFor },
|
|
678
|
+
},
|
|
679
|
+
],
|
|
680
|
+
];
|
|
681
|
+
}
|
|
682
|
+
// The scheduler computed the epoch's offset at request time; apply
|
|
683
|
+
// it ahead of the append when it differs from what the buffer has.
|
|
684
|
+
// Playlist discontinuities and period boundaries both land here.
|
|
685
|
+
if (matched.timestampOffset !== undefined &&
|
|
686
|
+
state.timeline.periodOffsets.get(matched.sbId) !== matched.timestampOffset) {
|
|
687
|
+
effects.push({
|
|
688
|
+
kind: 'setTimestampOffset',
|
|
689
|
+
sbId: matched.sbId,
|
|
690
|
+
offset: matched.timestampOffset,
|
|
691
|
+
});
|
|
692
|
+
const periodOffsets = new Map(state.timeline.periodOffsets);
|
|
693
|
+
periodOffsets.set(matched.sbId, matched.timestampOffset);
|
|
694
|
+
timeline = { ...state.timeline, periodOffsets };
|
|
695
|
+
}
|
|
696
|
+
effects.push({
|
|
697
|
+
kind: 'append',
|
|
698
|
+
sbId: matched.sbId,
|
|
699
|
+
data: msg.bytes,
|
|
700
|
+
...(matched.segmentStart !== undefined ? { start: matched.segmentStart } : {}),
|
|
701
|
+
...(matched.renditionId !== undefined ? { renditionId: matched.renditionId } : {}),
|
|
702
|
+
seq: matched.seq,
|
|
703
|
+
});
|
|
704
|
+
const buffer = buffers.get(matched.sbId);
|
|
705
|
+
if (buffer !== undefined) {
|
|
706
|
+
const nextBuffers = new Map(buffers);
|
|
707
|
+
nextBuffers.set(matched.sbId, { ...buffer, pendingAppends: buffer.pendingAppends + 1 });
|
|
708
|
+
buffers = nextBuffers;
|
|
709
|
+
}
|
|
710
|
+
if (matched.sbId === sbIdFor('video') &&
|
|
711
|
+
matched.renditionId !== undefined &&
|
|
712
|
+
matched.segmentStart !== undefined) {
|
|
713
|
+
// The append log behind quality.playing: which video rendition
|
|
714
|
+
// occupies which span of the buffer. Video only: an audio append
|
|
715
|
+
// carries its track id here, and logging it would answer
|
|
716
|
+
// `playing` with an id no video rendition has. Pruned behind the
|
|
717
|
+
// eviction watermark, so its size is bounded by the buffered span
|
|
718
|
+
// over the segment duration.
|
|
719
|
+
const watermark = state.playback.currentTime - cfg.backBufferSeconds;
|
|
720
|
+
const appendLog = [
|
|
721
|
+
...state.quality.appendLog.filter(([range]) => range.end > watermark),
|
|
722
|
+
[
|
|
723
|
+
{
|
|
724
|
+
start: matched.segmentStart,
|
|
725
|
+
end: matched.segmentStart + (matched.segmentDuration ?? 0),
|
|
726
|
+
},
|
|
727
|
+
matched.renditionId,
|
|
728
|
+
],
|
|
729
|
+
];
|
|
730
|
+
quality = { ...quality, appendLog };
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return [
|
|
734
|
+
{
|
|
735
|
+
...state,
|
|
736
|
+
buffers,
|
|
737
|
+
timeline,
|
|
738
|
+
quality,
|
|
739
|
+
cues,
|
|
740
|
+
scheduling: { ...state.scheduling, inflight },
|
|
741
|
+
stats,
|
|
742
|
+
},
|
|
743
|
+
effects,
|
|
744
|
+
];
|
|
745
|
+
}
|
|
746
|
+
case 'SEGMENT_FAILED': {
|
|
747
|
+
const inflight = new Map(state.scheduling.inflight);
|
|
748
|
+
for (const [token, request] of inflight) {
|
|
749
|
+
if (request.trackId === msg.trackId && request.seq === msg.seq)
|
|
750
|
+
inflight.delete(token);
|
|
751
|
+
}
|
|
752
|
+
// The manifest request has no rendition to exclude and no segment to
|
|
753
|
+
// skip: the transport already retried per policy, so its failure is
|
|
754
|
+
// the load's failure. Without this the engine sat in `loading` with
|
|
755
|
+
// a non-fatal error and a retry tick that had nothing to schedule.
|
|
756
|
+
if (msg.trackId === 'manifest') {
|
|
757
|
+
const contentType = msg.error.context?.contentType;
|
|
758
|
+
return failManifest({ ...state, scheduling: { ...state.scheduling, inflight } }, { ...msg.error, fatal: true, recoverable: false }, {
|
|
759
|
+
status: msg.status ?? null,
|
|
760
|
+
...(typeof contentType === 'string' ? { contentType } : {}),
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
// A cue pipeline degrades, never kills playback: subtitles stop,
|
|
764
|
+
// video continues. The span counts as covered so the scheduler moves
|
|
765
|
+
// on instead of hammering the same missing segment.
|
|
766
|
+
const failedTrack = findTrack(state.presentation, msg.trackId);
|
|
767
|
+
const isCueTrack = failedTrack !== null &&
|
|
768
|
+
(failedTrack.contentType === 'text' || failedTrack.contentType === 'metadata');
|
|
769
|
+
let cues = state.cues;
|
|
770
|
+
if (isCueTrack) {
|
|
771
|
+
const request = [...state.scheduling.inflight.values()].find((r) => r.trackId === msg.trackId && r.seq === msg.seq);
|
|
772
|
+
if (request?.segmentStart !== undefined) {
|
|
773
|
+
const next = new Map(cues);
|
|
774
|
+
next.set(msg.trackId, mergeCoverage(cues.get(msg.trackId) ?? [], {
|
|
775
|
+
start: request.segmentStart,
|
|
776
|
+
end: request.segmentStart + (request.segmentDuration ?? 0),
|
|
777
|
+
}));
|
|
778
|
+
cues = next;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const failEffects = [
|
|
782
|
+
{
|
|
783
|
+
kind: 'emit',
|
|
784
|
+
event: 'error',
|
|
785
|
+
payload: {
|
|
786
|
+
category: msg.error.category,
|
|
787
|
+
code: msg.error.code,
|
|
788
|
+
fatal: isCueTrack ? false : msg.error.fatal,
|
|
789
|
+
recoverable: msg.error.recoverable,
|
|
790
|
+
trackId: msg.trackId,
|
|
791
|
+
seq: msg.seq,
|
|
792
|
+
status: msg.status ?? null,
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
];
|
|
796
|
+
// A failed media fetch must re-drive, but not synchronously: a 404
|
|
797
|
+
// resolves as a microtask, and an immediate re-fetch of the same dead
|
|
798
|
+
// segment would flood the loop before a recovery stage's macrotask
|
|
799
|
+
// commands could intervene. A short backoff schedules the re-drive,
|
|
800
|
+
// giving exclusion and skip time to change the decision. Without
|
|
801
|
+
// recovery, the same segment retries until the breaker ends it.
|
|
802
|
+
if (!isCueTrack) {
|
|
803
|
+
failEffects.push({
|
|
804
|
+
kind: 'schedule',
|
|
805
|
+
token: 'kernel:retry',
|
|
806
|
+
delayMs: cfg.baseRetryDelayMs,
|
|
807
|
+
// biome-ignore lint/suspicious/noThenProperty: `then` is the schedule effect's field name from the message taxonomy
|
|
808
|
+
then: { type: 'TICK', token: 'kernel:retry' },
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
return [{ ...state, cues, scheduling: { ...state.scheduling, inflight } }, failEffects];
|
|
812
|
+
}
|
|
813
|
+
case 'SOURCEBUFFER_CREATED': {
|
|
814
|
+
const buffers = new Map(state.buffers);
|
|
815
|
+
buffers.set(msg.sbId, { codecs: msg.codecs, ranges: [], pendingAppends: 0 });
|
|
816
|
+
return [{ ...state, buffers }, []];
|
|
817
|
+
}
|
|
818
|
+
case 'SOURCEBUFFER_UPDATEEND': {
|
|
819
|
+
const buffer = state.buffers.get(msg.sbId);
|
|
820
|
+
if (buffer === undefined) {
|
|
821
|
+
// The buffer was removed by a concurrent detach. Absorb and ignore.
|
|
822
|
+
return [state, []];
|
|
823
|
+
}
|
|
824
|
+
const buffers = new Map(state.buffers);
|
|
825
|
+
buffers.set(msg.sbId, {
|
|
826
|
+
...buffer,
|
|
827
|
+
...(msg.ranges !== undefined ? { ranges: msg.ranges } : {}),
|
|
828
|
+
pendingAppends: Math.max(0, buffer.pendingAppends - 1),
|
|
829
|
+
});
|
|
830
|
+
// A successful append proves the buffer works: the breaker resets.
|
|
831
|
+
let bufferErrors = state.bufferErrors;
|
|
832
|
+
if (bufferErrors.has(msg.sbId)) {
|
|
833
|
+
bufferErrors = new Map(bufferErrors);
|
|
834
|
+
bufferErrors.delete(msg.sbId);
|
|
835
|
+
}
|
|
836
|
+
// An append completing is the natural moment to decide the next fetch,
|
|
837
|
+
// so the buffer keeps filling toward the goal without waiting for the
|
|
838
|
+
// next time update.
|
|
839
|
+
return driveScheduling({ ...state, buffers, bufferErrors }, hooks, cfg);
|
|
840
|
+
}
|
|
841
|
+
case 'SOURCEBUFFER_ERROR': {
|
|
842
|
+
// The circuit breaker: repeated failures on one buffer can never make
|
|
843
|
+
// progress, and every failure loops back into a refetch of the same
|
|
844
|
+
// segment. At the limit the failure turns fatal and the loop dies.
|
|
845
|
+
const count = (state.bufferErrors.get(msg.sbId) ?? 0) + 1;
|
|
846
|
+
const bufferErrors = new Map(state.bufferErrors);
|
|
847
|
+
bufferErrors.set(msg.sbId, count);
|
|
848
|
+
const fatal = msg.error.fatal || count >= cfg.bufferErrorLimit;
|
|
849
|
+
const phase = fatal ? 'error' : state.lifecycle.phase;
|
|
850
|
+
const effects = [
|
|
851
|
+
{
|
|
852
|
+
kind: 'emit',
|
|
853
|
+
event: 'error',
|
|
854
|
+
payload: {
|
|
855
|
+
category: msg.error.category,
|
|
856
|
+
code: msg.error.code,
|
|
857
|
+
fatal,
|
|
858
|
+
recoverable: msg.error.recoverable,
|
|
859
|
+
sbId: msg.sbId,
|
|
860
|
+
...(fatal && !msg.error.fatal ? { consecutiveFailures: count } : {}),
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
];
|
|
864
|
+
let inflight = state.scheduling.inflight;
|
|
865
|
+
if (fatal) {
|
|
866
|
+
// Stop the world for this buffer: whatever is in flight will only
|
|
867
|
+
// feed the same failure again.
|
|
868
|
+
const pruned = new Map(state.scheduling.inflight);
|
|
869
|
+
for (const [token, request] of state.scheduling.inflight) {
|
|
870
|
+
if (request.sbId === msg.sbId) {
|
|
871
|
+
effects.push({ kind: 'abort', token });
|
|
872
|
+
pruned.delete(token);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
inflight = pruned;
|
|
876
|
+
}
|
|
877
|
+
return [
|
|
878
|
+
{
|
|
879
|
+
...state,
|
|
880
|
+
bufferErrors,
|
|
881
|
+
lifecycle: { phase },
|
|
882
|
+
scheduling: { ...state.scheduling, inflight },
|
|
883
|
+
},
|
|
884
|
+
effects,
|
|
885
|
+
];
|
|
886
|
+
}
|
|
887
|
+
case 'QUOTA_EXCEEDED': {
|
|
888
|
+
const boundary = Math.max(0, state.playback.currentTime - cfg.backBufferSeconds);
|
|
889
|
+
if (boundary <= 0) {
|
|
890
|
+
// Nothing behind the playhead to evict. Report; recovery is a stage.
|
|
891
|
+
return [state, [{ kind: 'emit', event: 'quota:exhausted', payload: { sbId: msg.sbId } }]];
|
|
892
|
+
}
|
|
893
|
+
return [state, [{ kind: 'remove', sbId: msg.sbId, start: 0, end: boundary }]];
|
|
894
|
+
}
|
|
895
|
+
case 'TIME_UPDATE': {
|
|
896
|
+
const moved = {
|
|
897
|
+
...state,
|
|
898
|
+
playback: { ...state.playback, currentTime: msg.currentTime, buffered: msg.buffered },
|
|
899
|
+
};
|
|
900
|
+
return driveScheduling(moved, hooks, cfg);
|
|
901
|
+
}
|
|
902
|
+
case 'SEEKING': {
|
|
903
|
+
// Seeking out of the ended phase resumes an ordinary ready state.
|
|
904
|
+
const phase = state.lifecycle.phase === 'ended' ? 'ready' : state.lifecycle.phase;
|
|
905
|
+
return [
|
|
906
|
+
{
|
|
907
|
+
...state,
|
|
908
|
+
lifecycle: { phase },
|
|
909
|
+
playback: { ...state.playback, currentTime: msg.to, seeking: true },
|
|
910
|
+
},
|
|
911
|
+
[],
|
|
912
|
+
];
|
|
913
|
+
}
|
|
914
|
+
case 'SEEKED': {
|
|
915
|
+
return [
|
|
916
|
+
{ ...state, playback: { ...state.playback, currentTime: msg.at, seeking: false } },
|
|
917
|
+
[],
|
|
918
|
+
];
|
|
919
|
+
}
|
|
920
|
+
case 'STALLED': {
|
|
921
|
+
return [state, [{ kind: 'emit', event: 'playback:stalled', payload: { at: msg.at } }]];
|
|
922
|
+
}
|
|
923
|
+
case 'ENCRYPTED': {
|
|
924
|
+
return [
|
|
925
|
+
state,
|
|
926
|
+
[
|
|
927
|
+
{
|
|
928
|
+
kind: 'emit',
|
|
929
|
+
event: 'drm:encrypted',
|
|
930
|
+
payload: { initDataType: msg.initDataType, initData: msg.initData },
|
|
931
|
+
},
|
|
932
|
+
],
|
|
933
|
+
];
|
|
934
|
+
}
|
|
935
|
+
case 'LIVE_WINDOW_CHANGED': {
|
|
936
|
+
const firstWindow = state.live === null;
|
|
937
|
+
const withLive = {
|
|
938
|
+
...state,
|
|
939
|
+
live: { span: { start: msg.start, end: msg.end }, edge: msg.edge },
|
|
940
|
+
};
|
|
941
|
+
// The availability span is what the element may seek within. MSE
|
|
942
|
+
// derives `seekable` from the duration unless told otherwise, so a
|
|
943
|
+
// live presentation without a declared duration gets an infinite one
|
|
944
|
+
// on its first span and the span itself as the seekable range on
|
|
945
|
+
// every update. Native controls then scrub the DVR span and clamp
|
|
946
|
+
// seeks into it, instead of offering 0 to the buffered end.
|
|
947
|
+
const spanEffects = [];
|
|
948
|
+
if (firstWindow && state.presentation?.duration === undefined) {
|
|
949
|
+
spanEffects.push({ kind: 'setDuration', seconds: Number.POSITIVE_INFINITY });
|
|
950
|
+
}
|
|
951
|
+
spanEffects.push({ kind: 'setLiveSeekableRange', start: msg.start, end: msg.end });
|
|
952
|
+
// On the first availability update of a live stream, start at the live
|
|
953
|
+
// edge instead of the beginning of what can be a long DVR span (hours
|
|
954
|
+
// behind). The playhead moves to the edge, the element seeks there, and
|
|
955
|
+
// scheduling then fills the buffer at the edge, not at the span start.
|
|
956
|
+
if (firstWindow && msg.edge > state.playback.currentTime + 1) {
|
|
957
|
+
const seeked = {
|
|
958
|
+
...withLive,
|
|
959
|
+
playback: { ...withLive.playback, currentTime: msg.edge },
|
|
960
|
+
};
|
|
961
|
+
const [scheduled, effects] = driveScheduling(seeked, hooks, cfg);
|
|
962
|
+
return [scheduled, [...spanEffects, { kind: 'seekElement', to: msg.edge }, ...effects]];
|
|
963
|
+
}
|
|
964
|
+
const [scheduled, effects] = driveScheduling(withLive, hooks, cfg);
|
|
965
|
+
return [scheduled, [...spanEffects, ...effects]];
|
|
966
|
+
}
|
|
967
|
+
case 'TICK': {
|
|
968
|
+
// Slices own their tokens; the kernel has nothing to do.
|
|
969
|
+
return [state, []];
|
|
970
|
+
}
|
|
971
|
+
case 'ENDED': {
|
|
972
|
+
if (state.lifecycle.phase !== 'ready')
|
|
973
|
+
return [state, []];
|
|
974
|
+
return [
|
|
975
|
+
{ ...state, lifecycle: { phase: 'ended' } },
|
|
976
|
+
[{ kind: 'emit', event: 'playback:ended', payload: { at: msg.at } }],
|
|
977
|
+
];
|
|
978
|
+
}
|
|
979
|
+
case 'THROUGHPUT_SAMPLE': {
|
|
980
|
+
return [
|
|
981
|
+
{
|
|
982
|
+
...state,
|
|
983
|
+
stats: {
|
|
984
|
+
throughputEwma: ewma(state.stats.throughputEwma, msg.bps, cfg.ewmaAlpha),
|
|
985
|
+
throughputFastEwma: ewma(state.stats.throughputFastEwma, msg.bps, cfg.ewmaFastAlpha),
|
|
986
|
+
},
|
|
987
|
+
},
|
|
988
|
+
[],
|
|
989
|
+
];
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* The buffer-goal loop, run on TIME_UPDATE. Pure: rendition choice comes
|
|
995
|
+
* from arbitration over state, buffer knowledge from the ranges snapshots
|
|
996
|
+
* the updateend facts carried into state. Live presentations wait for
|
|
997
|
+
* the live stages to supply availability bounds; until then nothing is
|
|
998
|
+
* scheduled.
|
|
999
|
+
*/
|
|
1000
|
+
function driveScheduling(state, hooks, cfg) {
|
|
1001
|
+
if (state.lifecycle.phase !== 'ready' || state.presentation === null)
|
|
1002
|
+
return [state, []];
|
|
1003
|
+
// Live schedules only once a live stage has reported availability
|
|
1004
|
+
// bounds; the kernel never computes an edge itself.
|
|
1005
|
+
if (state.presentation.isLive && state.live === null)
|
|
1006
|
+
return [state, []];
|
|
1007
|
+
const effects = [];
|
|
1008
|
+
const tracks = [];
|
|
1009
|
+
const initFetches = [];
|
|
1010
|
+
let quality = state.quality;
|
|
1011
|
+
for (const contentType of ['video', 'audio']) {
|
|
1012
|
+
const trackId = state.tracks.active.get(contentType);
|
|
1013
|
+
if (trackId === undefined)
|
|
1014
|
+
continue;
|
|
1015
|
+
const found = findTrackSite(state.presentation, trackId);
|
|
1016
|
+
if (found === null || found.track.renditions.length === 0)
|
|
1017
|
+
continue;
|
|
1018
|
+
const sbId = sbIdFor(contentType);
|
|
1019
|
+
// An append in flight means the ranges snapshot is stale: deciding on
|
|
1020
|
+
// it refetches the very segment being appended. Its updateend is a
|
|
1021
|
+
// driving fact, so waiting costs nothing.
|
|
1022
|
+
if ((state.buffers.get(sbId)?.pendingAppends ?? 0) > 0)
|
|
1023
|
+
continue;
|
|
1024
|
+
const bufferAhead = bufferedEndFrom(state.buffers.get(sbId)?.ranges ?? [], state.playback.currentTime, GAP_TOLERANCE) - state.playback.currentTime;
|
|
1025
|
+
// Arbitrate the rendition for this track. Memoized on quality.version:
|
|
1026
|
+
// TIME_UPDATE at 60 Hz reuses the last outcome until something changes.
|
|
1027
|
+
// With an abr chooser loaded the key also carries coarse telemetry
|
|
1028
|
+
// buckets, so a real throughput or buffer change re-arbitrates without
|
|
1029
|
+
// recomputing at frame rate.
|
|
1030
|
+
const memoKey = hooks.abr == null
|
|
1031
|
+
? state.quality.version
|
|
1032
|
+
: `${state.quality.version}:${Math.round(state.stats.throughputEwma / 25_000)}:${Math.round(state.stats.throughputFastEwma / 25_000)}:${Math.round(bufferAhead)}:${state.quality.active}`;
|
|
1033
|
+
const outcome = arbiterFor(hooks, contentType).run({
|
|
1034
|
+
renditions: found.track.renditions,
|
|
1035
|
+
constraints: state.quality.constraints,
|
|
1036
|
+
pinned: state.quality.pinned,
|
|
1037
|
+
current: state.quality.active,
|
|
1038
|
+
couplings: state.presentation.couplings,
|
|
1039
|
+
activeTracks: state.tracks.active,
|
|
1040
|
+
availableGroups: availableGroups(state),
|
|
1041
|
+
abr: hooks.abr ?? null,
|
|
1042
|
+
telemetry: {
|
|
1043
|
+
throughputEwma: state.stats.throughputEwma,
|
|
1044
|
+
throughputFastEwma: state.stats.throughputFastEwma,
|
|
1045
|
+
bufferAhead,
|
|
1046
|
+
current: state.quality.active,
|
|
1047
|
+
currentTime: state.playback.currentTime,
|
|
1048
|
+
canSwitchTo: hooks.switchPolicy ?? canSwitchTo,
|
|
1049
|
+
},
|
|
1050
|
+
}, memoKey);
|
|
1051
|
+
for (const event of outcome.events)
|
|
1052
|
+
effects.push(event);
|
|
1053
|
+
const rendition = found.track.renditions.find((r) => r.id === outcome.result.selected);
|
|
1054
|
+
if (rendition === undefined)
|
|
1055
|
+
continue;
|
|
1056
|
+
if (contentType === 'video' && quality.active !== rendition.id) {
|
|
1057
|
+
quality = { ...quality, active: rendition.id };
|
|
1058
|
+
}
|
|
1059
|
+
const inflight = [];
|
|
1060
|
+
for (const request of state.scheduling.inflight.values()) {
|
|
1061
|
+
if (request.trackId === trackId)
|
|
1062
|
+
inflight.push(request);
|
|
1063
|
+
}
|
|
1064
|
+
// The buffer request rides with the first fetch decision; while that
|
|
1065
|
+
// fetch is in flight the SOURCEBUFFER_CREATED fact is on its way, so
|
|
1066
|
+
// re-requesting every tick would spam the trace.
|
|
1067
|
+
if (!state.buffers.has(sbId) && inflight.length === 0) {
|
|
1068
|
+
const codecs = rendition.codecs === null
|
|
1069
|
+
? rendition.mimeType
|
|
1070
|
+
: `${rendition.mimeType}; codecs="${rendition.codecs}"`;
|
|
1071
|
+
effects.push({ kind: 'createSourceBuffer', sbId, codecs });
|
|
1072
|
+
}
|
|
1073
|
+
// Init before media, always: while the buffer's init is not this
|
|
1074
|
+
// rendition's, the only fetch this track may make is the init segment.
|
|
1075
|
+
// One request per track makes the ordering free.
|
|
1076
|
+
if (rendition.init !== undefined &&
|
|
1077
|
+
state.buffers.get(sbId)?.initFor !== rendition.id &&
|
|
1078
|
+
inflight.length === 0) {
|
|
1079
|
+
initFetches.push({ trackId, sbId, rendition: rendition.id, init: rendition.init });
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
tracks.push({
|
|
1083
|
+
trackId,
|
|
1084
|
+
period: found.period,
|
|
1085
|
+
rendition,
|
|
1086
|
+
ranges: state.buffers.get(sbId)?.ranges ?? [],
|
|
1087
|
+
sbId,
|
|
1088
|
+
inflight,
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
// Cue pipelines schedule like media, minus everything SourceBuffer:
|
|
1092
|
+
// ranges come from delivered coverage, there is no init and no
|
|
1093
|
+
// destination id, and delivery routes through the sink instead.
|
|
1094
|
+
for (const contentType of ['text', 'metadata']) {
|
|
1095
|
+
const trackId = state.tracks.active.get(contentType);
|
|
1096
|
+
if (trackId === undefined)
|
|
1097
|
+
continue;
|
|
1098
|
+
const found = findTrackSite(state.presentation, trackId);
|
|
1099
|
+
const rendition = found?.track.renditions[0];
|
|
1100
|
+
if (found === null || rendition === undefined)
|
|
1101
|
+
continue;
|
|
1102
|
+
const inflight = [];
|
|
1103
|
+
for (const request of state.scheduling.inflight.values()) {
|
|
1104
|
+
if (request.trackId === trackId)
|
|
1105
|
+
inflight.push(request);
|
|
1106
|
+
}
|
|
1107
|
+
tracks.push({
|
|
1108
|
+
trackId,
|
|
1109
|
+
period: found.period,
|
|
1110
|
+
rendition,
|
|
1111
|
+
ranges: state.cues.get(trackId) ?? [],
|
|
1112
|
+
inflight,
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
const result = schedule({
|
|
1116
|
+
currentTime: state.playback.currentTime,
|
|
1117
|
+
bufferGoal: state.scheduling.bufferGoal,
|
|
1118
|
+
tokenSeq: state.scheduling.tokenSeq + initFetches.length,
|
|
1119
|
+
tracks,
|
|
1120
|
+
liveWindow: state.presentation.isLive ? (state.live?.span ?? null) : null,
|
|
1121
|
+
mediaTimeNormalized: cfg.mediaTimeNormalized,
|
|
1122
|
+
});
|
|
1123
|
+
// The scheduling breaker: an identical decision repeated past the limit
|
|
1124
|
+
// means appends report success but nothing ever progresses, and the
|
|
1125
|
+
// engine would fetch the same bytes forever. Halt fatally instead.
|
|
1126
|
+
const decisionKey = [
|
|
1127
|
+
...initFetches.map((p) => `${p.trackId}:init:${p.rendition}`),
|
|
1128
|
+
...result.requests.map((r) => `${r.trackId}:${r.seq}:${r.renditionId ?? ''}`),
|
|
1129
|
+
].join('|');
|
|
1130
|
+
let repeat = state.scheduling.repeat;
|
|
1131
|
+
if (decisionKey !== '') {
|
|
1132
|
+
repeat =
|
|
1133
|
+
repeat !== undefined && repeat.key === decisionKey
|
|
1134
|
+
? { key: decisionKey, count: repeat.count + 1 }
|
|
1135
|
+
: { key: decisionKey, count: 1 };
|
|
1136
|
+
if (repeat.count > cfg.repeatFetchLimit) {
|
|
1137
|
+
effects.push({
|
|
1138
|
+
kind: 'emit',
|
|
1139
|
+
event: 'error',
|
|
1140
|
+
payload: {
|
|
1141
|
+
category: 'internal',
|
|
1142
|
+
code: 'INTERNAL_ASSERTION',
|
|
1143
|
+
fatal: true,
|
|
1144
|
+
recoverable: false,
|
|
1145
|
+
context: { reason: 'the same fetch decision repeated without progress', decisionKey },
|
|
1146
|
+
},
|
|
1147
|
+
});
|
|
1148
|
+
return [
|
|
1149
|
+
{
|
|
1150
|
+
...state,
|
|
1151
|
+
quality,
|
|
1152
|
+
lifecycle: { phase: 'error' },
|
|
1153
|
+
scheduling: { ...state.scheduling, repeat },
|
|
1154
|
+
},
|
|
1155
|
+
effects,
|
|
1156
|
+
];
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
let tokenSeq = state.scheduling.tokenSeq;
|
|
1160
|
+
const inflight = new Map(state.scheduling.inflight);
|
|
1161
|
+
for (const pending of initFetches) {
|
|
1162
|
+
tokenSeq += 1;
|
|
1163
|
+
const token = `t${tokenSeq}:${pending.trackId}:init`;
|
|
1164
|
+
effects.push({
|
|
1165
|
+
kind: 'fetch',
|
|
1166
|
+
token,
|
|
1167
|
+
url: pending.init.url,
|
|
1168
|
+
...(pending.init.byteRange !== undefined ? { range: pending.init.byteRange } : {}),
|
|
1169
|
+
});
|
|
1170
|
+
inflight.set(token, {
|
|
1171
|
+
token,
|
|
1172
|
+
trackId: pending.trackId,
|
|
1173
|
+
seq: -1,
|
|
1174
|
+
url: pending.init.url,
|
|
1175
|
+
sbId: pending.sbId,
|
|
1176
|
+
renditionId: pending.rendition,
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
for (const effect of result.effects)
|
|
1180
|
+
effects.push(effect);
|
|
1181
|
+
for (const request of result.requests) {
|
|
1182
|
+
inflight.set(request.token, request);
|
|
1183
|
+
}
|
|
1184
|
+
// VOD end: every track has nothing left to fetch and the buffers reach
|
|
1185
|
+
// the announced duration. endOfStream is safe to re-emit; the handler
|
|
1186
|
+
// no-ops once the source has left 'open'.
|
|
1187
|
+
// Only SourceBuffer tracks gate the end: a subtitle playlist shorter
|
|
1188
|
+
// than the video must not hold the stream open.
|
|
1189
|
+
const mediaTracks = tracks.filter((track) => track.sbId !== undefined);
|
|
1190
|
+
if (state.presentation.duration !== undefined &&
|
|
1191
|
+
initFetches.length === 0 &&
|
|
1192
|
+
result.effects.length === 0 &&
|
|
1193
|
+
mediaTracks.length > 0 &&
|
|
1194
|
+
mediaTracks.every((track) => {
|
|
1195
|
+
const end = bufferedEndFrom(track.ranges, state.playback.currentTime, 0.25);
|
|
1196
|
+
return end >= (state.presentation?.duration ?? 0) - 0.5;
|
|
1197
|
+
})) {
|
|
1198
|
+
effects.push({ kind: 'endOfStream' });
|
|
1199
|
+
}
|
|
1200
|
+
if (effects.length === 0 && quality === state.quality)
|
|
1201
|
+
return [state, []];
|
|
1202
|
+
return [
|
|
1203
|
+
{
|
|
1204
|
+
...state,
|
|
1205
|
+
quality,
|
|
1206
|
+
scheduling: {
|
|
1207
|
+
...state.scheduling,
|
|
1208
|
+
inflight,
|
|
1209
|
+
tokenSeq: result.tokenSeq,
|
|
1210
|
+
...(repeat !== undefined ? { repeat } : {}),
|
|
1211
|
+
},
|
|
1212
|
+
},
|
|
1213
|
+
effects,
|
|
1214
|
+
];
|
|
1215
|
+
}
|
|
1216
|
+
const arbiters = new WeakMap();
|
|
1217
|
+
function arbiterFor(hooks, contentType) {
|
|
1218
|
+
let pair = arbiters.get(hooks);
|
|
1219
|
+
if (pair === undefined) {
|
|
1220
|
+
pair = { video: createArbiter(), audio: createArbiter() };
|
|
1221
|
+
arbiters.set(hooks, pair);
|
|
1222
|
+
}
|
|
1223
|
+
return pair[contentType];
|
|
1224
|
+
}
|
|
1225
|
+
const COMMAND_TYPES = {
|
|
1226
|
+
ATTACH: true,
|
|
1227
|
+
DETACH: true,
|
|
1228
|
+
LOAD: true,
|
|
1229
|
+
UNLOAD: true,
|
|
1230
|
+
SEEK: true,
|
|
1231
|
+
SEEK_TO_LIVE_EDGE: true,
|
|
1232
|
+
DESELECT_TRACK: true,
|
|
1233
|
+
SELECT_TRACK: true,
|
|
1234
|
+
PIN_RENDITION: true,
|
|
1235
|
+
RELEASE_PIN: true,
|
|
1236
|
+
CONSTRAIN: true,
|
|
1237
|
+
RELEASE_CONSTRAINT: true,
|
|
1238
|
+
SET_BUFFER_GOAL: true,
|
|
1239
|
+
ABORT_INFLIGHT: true,
|
|
1240
|
+
};
|
|
1241
|
+
export function isCommand(msg) {
|
|
1242
|
+
return msg.type in COMMAND_TYPES;
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Builds the root reducer from the built-in kernel logic plus stage slices.
|
|
1246
|
+
* A slice reducer receives its own slice (undefined on first run) and a
|
|
1247
|
+
* read-only view of kernel state. A throwing slice reducer is contained: its
|
|
1248
|
+
* slice keeps the previous value and an error event effect is emitted, so
|
|
1249
|
+
* one broken stage cannot corrupt kernel state.
|
|
1250
|
+
*/
|
|
1251
|
+
export function createReducer(slices, config, hooks = {}) {
|
|
1252
|
+
const sliceList = slices ? [...slices] : [];
|
|
1253
|
+
const cfg = resolveConfig(config);
|
|
1254
|
+
const DRIVING_FACTS = new Set([
|
|
1255
|
+
'MANIFEST_LOADED',
|
|
1256
|
+
'SEGMENT_LOADED',
|
|
1257
|
+
'SOURCEBUFFER_UPDATEEND',
|
|
1258
|
+
'SEEKING',
|
|
1259
|
+
'SEEKED',
|
|
1260
|
+
'LIVE_WINDOW_CHANGED',
|
|
1261
|
+
// A failed fetch re-drives on a scheduled backoff TICK, not synchronously,
|
|
1262
|
+
// so recovery's commands change the decision before the retry fires.
|
|
1263
|
+
'TICK',
|
|
1264
|
+
]);
|
|
1265
|
+
return (state, msg) => {
|
|
1266
|
+
// Manifest bytes answer exactly one request; the kernel notes which so
|
|
1267
|
+
// it can tell, after the slices ran, whether any adapter claimed them.
|
|
1268
|
+
const manifestRequest = !isCommand(msg) && msg.type === 'SEGMENT_LOADED' ? manifestRequestFor(state, msg) : null;
|
|
1269
|
+
let [next, effects] = isCommand(msg)
|
|
1270
|
+
? reduceCommand(state, msg, cfg, hooks)
|
|
1271
|
+
: reduceFact(state, msg, cfg, hooks);
|
|
1272
|
+
// The buffer-goal loop runs on every fact that can change what to
|
|
1273
|
+
// fetch next. TIME_UPDATE drives inside its own reduction; the others
|
|
1274
|
+
// drive here, which is what makes startup work on a paused element
|
|
1275
|
+
// that fires no timeupdate.
|
|
1276
|
+
if (!isCommand(msg) && DRIVING_FACTS.has(msg.type)) {
|
|
1277
|
+
const [driven, driveEffects] = driveScheduling(next, hooks, cfg);
|
|
1278
|
+
next = driven;
|
|
1279
|
+
if (driveEffects.length > 0)
|
|
1280
|
+
effects = [...effects, ...driveEffects];
|
|
1281
|
+
}
|
|
1282
|
+
// An adapter claims manifest bytes by acting on them: a loop-back feed
|
|
1283
|
+
// of MANIFEST_LOADED or MANIFEST_FAILED, or a further fetch. Events are
|
|
1284
|
+
// observation, not a claim. Bytes nobody acts on would otherwise leave
|
|
1285
|
+
// the engine in `loading` forever, or misreport as a parse failure.
|
|
1286
|
+
let claimed = manifestRequest === null;
|
|
1287
|
+
for (const [name, slice] of sliceList) {
|
|
1288
|
+
try {
|
|
1289
|
+
const [sliceState, sliceEffects] = slice(next[name], msg, next);
|
|
1290
|
+
next = { ...next, [name]: sliceState };
|
|
1291
|
+
if (sliceEffects.length > 0) {
|
|
1292
|
+
effects = [...effects, ...sliceEffects];
|
|
1293
|
+
if (sliceEffects.some((effect) => effect.kind !== 'emit'))
|
|
1294
|
+
claimed = true;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
catch (err) {
|
|
1298
|
+
effects = [
|
|
1299
|
+
...effects,
|
|
1300
|
+
{
|
|
1301
|
+
kind: 'emit',
|
|
1302
|
+
event: 'kernel:slice-error',
|
|
1303
|
+
payload: { slice: name, message: String(err) },
|
|
1304
|
+
},
|
|
1305
|
+
];
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (!claimed && manifestRequest !== null && next.lifecycle.phase === 'loading') {
|
|
1309
|
+
const [failed, failEffects] = failManifest(next, { category: 'manifest', code: 'MANIFEST_UNSUPPORTED', fatal: true, recoverable: false }, { url: manifestRequest.url });
|
|
1310
|
+
next = failed;
|
|
1311
|
+
effects = [...effects, ...failEffects];
|
|
1312
|
+
}
|
|
1313
|
+
return [next, effects];
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
//# sourceMappingURL=reducer.js.map
|