@blockcast/mmt-container 0.1.0 → 0.3.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/README.md +524 -0
- package/dist/.build-stamp +0 -0
- package/dist/__tests__/test-utils.d.ts +71 -0
- package/dist/__tests__/test-utils.d.ts.map +1 -0
- package/dist/align.d.ts +152 -0
- package/dist/align.d.ts.map +1 -0
- package/dist/align.js +88 -0
- package/dist/align.js.map +7 -0
- package/dist/audio-init-parser.d.ts +66 -0
- package/dist/audio-init-parser.d.ts.map +1 -0
- package/dist/audio-init-parser.js +118 -0
- package/dist/audio-init-parser.js.map +7 -0
- package/dist/audio-ring-buffer.d.ts +50 -0
- package/dist/audio-ring-buffer.d.ts.map +1 -0
- package/dist/audio-ring-buffer.js +80 -0
- package/dist/audio-ring-buffer.js.map +7 -0
- package/dist/authored-timing.d.ts +32 -0
- package/dist/authored-timing.d.ts.map +1 -0
- package/dist/authored-timing.js +32 -0
- package/dist/authored-timing.js.map +7 -0
- package/dist/block-policy.d.ts +314 -0
- package/dist/block-policy.d.ts.map +1 -0
- package/dist/block-policy.js +466 -0
- package/dist/block-policy.js.map +7 -0
- package/dist/browser-runtime.js +26549 -0
- package/dist/browser-runtime.js.map +7 -0
- package/dist/carp-catalog.d.ts +429 -0
- package/dist/carp-catalog.d.ts.map +1 -0
- package/dist/carp-catalog.js +985 -0
- package/dist/carp-catalog.js.map +7 -0
- package/dist/catalog-fec-params.d.ts +275 -0
- package/dist/catalog-fec-params.d.ts.map +1 -0
- package/dist/catalog-fec-params.js +702 -0
- package/dist/catalog-fec-params.js.map +7 -0
- package/dist/catalog-network-source.d.ts +42 -0
- package/dist/catalog-network-source.d.ts.map +1 -0
- package/dist/catalog-network-source.js +47 -0
- package/dist/catalog-network-source.js.map +7 -0
- package/dist/catalog-validator.d.ts +18 -0
- package/dist/catalog-validator.d.ts.map +1 -0
- package/dist/catalog-validator.js +7747 -0
- package/dist/catalog-validator.js.map +7 -0
- package/dist/cmaf-block-assembler.d.ts +285 -0
- package/dist/cmaf-block-assembler.d.ts.map +1 -0
- package/dist/cmaf-block-assembler.js +1966 -0
- package/dist/cmaf-block-assembler.js.map +7 -0
- package/dist/cmaf-group-batcher.d.ts +38 -0
- package/dist/cmaf-group-batcher.d.ts.map +1 -0
- package/dist/cmaf-group-batcher.js +176 -0
- package/dist/cmaf-group-batcher.js.map +7 -0
- package/dist/cmcd-producer.d.ts +104 -0
- package/dist/cmcd-producer.d.ts.map +1 -0
- package/dist/cmcd-producer.js +183 -0
- package/dist/cmcd-producer.js.map +7 -0
- package/dist/codec-utils.d.ts +384 -0
- package/dist/codec-utils.d.ts.map +1 -0
- package/dist/codec-utils.js +1546 -0
- package/dist/codec-utils.js.map +7 -0
- package/dist/conceal-timeline.d.ts +146 -0
- package/dist/conceal-timeline.d.ts.map +1 -0
- package/dist/conceal-timeline.js +137 -0
- package/dist/conceal-timeline.js.map +7 -0
- package/dist/delivery-gap.d.ts +75 -0
- package/dist/delivery-gap.d.ts.map +1 -0
- package/dist/delivery-gap.js +88 -0
- package/dist/delivery-gap.js.map +7 -0
- package/dist/duplicate-ledger.d.ts +197 -0
- package/dist/duplicate-ledger.d.ts.map +1 -0
- package/dist/duplicate-ledger.js +226 -0
- package/dist/duplicate-ledger.js.map +7 -0
- package/dist/fec-codec.d.ts +32 -0
- package/dist/fec-codec.d.ts.map +1 -0
- package/dist/fec-codec.js +47 -0
- package/dist/fec-codec.js.map +7 -0
- package/dist/fec-manager.d.ts +583 -0
- package/dist/fec-manager.d.ts.map +1 -0
- package/dist/fec-manager.js +3899 -0
- package/dist/fec-manager.js.map +7 -0
- package/dist/fec-starvation-detector.d.ts +59 -0
- package/dist/fec-starvation-detector.d.ts.map +1 -0
- package/dist/fec-starvation-detector.js +60 -0
- package/dist/fec-starvation-detector.js.map +7 -0
- package/dist/fec-state.d.ts +131 -0
- package/dist/fec-state.d.ts.map +1 -0
- package/dist/fec-state.js +87 -0
- package/dist/fec-state.js.map +7 -0
- package/dist/fec-worker-setup.d.ts +94 -0
- package/dist/fec-worker-setup.d.ts.map +1 -0
- package/dist/fec-worker-setup.js +673 -0
- package/dist/fec-worker-setup.js.map +7 -0
- package/dist/fmp4-completion.d.ts +30 -0
- package/dist/fmp4-completion.d.ts.map +1 -0
- package/dist/fmp4-completion.js +21 -0
- package/dist/fmp4-completion.js.map +7 -0
- package/dist/fmp4-reader.d.ts +37 -0
- package/dist/fmp4-reader.d.ts.map +1 -0
- package/dist/fmp4-reader.js +100 -0
- package/dist/fmp4-reader.js.map +7 -0
- package/dist/index.d.ts +44 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +460 -14
- package/dist/index.js.map +7 -1
- package/dist/iso-block-locator.d.ts +118 -0
- package/dist/iso-block-locator.d.ts.map +1 -0
- package/dist/iso-block-locator.js +289 -0
- package/dist/iso-block-locator.js.map +7 -0
- package/dist/iso-bmff-timing.d.ts +78 -0
- package/dist/iso-bmff-timing.d.ts.map +1 -0
- package/dist/iso-bmff-timing.js +593 -0
- package/dist/iso-bmff-timing.js.map +7 -0
- package/dist/loc.d.ts +70 -75
- package/dist/loc.d.ts.map +1 -1
- package/dist/loc.js +106 -192
- package/dist/loc.js.map +7 -1
- package/dist/manifest-auth-plan.d.ts +30 -0
- package/dist/manifest-auth-plan.d.ts.map +1 -0
- package/dist/manifest-auth-plan.js +1266 -0
- package/dist/manifest-auth-plan.js.map +7 -0
- package/dist/media-router.d.ts +262 -0
- package/dist/media-router.d.ts.map +1 -0
- package/dist/media-router.js +584 -0
- package/dist/media-router.js.map +7 -0
- package/dist/mfu-reassembler.d.ts +313 -9
- package/dist/mfu-reassembler.d.ts.map +1 -1
- package/dist/mfu-reassembler.js +3051 -285
- package/dist/mfu-reassembler.js.map +7 -1
- package/dist/mfu-reassembly-policy.d.ts +24 -0
- package/dist/mfu-reassembly-policy.d.ts.map +1 -0
- package/dist/mfu-reassembly-policy.js +99 -0
- package/dist/mfu-reassembly-policy.js.map +7 -0
- package/dist/mmtp-container-parser.d.ts +568 -0
- package/dist/mmtp-container-parser.d.ts.map +1 -0
- package/dist/mmtp-container-parser.js +6580 -0
- package/dist/mmtp-container-parser.js.map +7 -0
- package/dist/mmtp-container-parser.no-modules.js +9616 -0
- package/dist/mmtp-container-parser.no-modules.js.map +7 -0
- package/dist/mmtp-decode-pipeline.d.ts +138 -0
- package/dist/mmtp-decode-pipeline.d.ts.map +1 -0
- package/dist/mmtp-decode-pipeline.js +6814 -0
- package/dist/mmtp-decode-pipeline.js.map +7 -0
- package/dist/mmtp-router.d.ts +679 -0
- package/dist/mmtp-router.d.ts.map +1 -0
- package/dist/mmtp-router.js +11278 -0
- package/dist/mmtp-router.js.map +7 -0
- package/dist/mmtp-to-cmaf.d.ts +187 -0
- package/dist/mmtp-to-cmaf.d.ts.map +1 -0
- package/dist/mmtp-to-cmaf.js +716 -0
- package/dist/mmtp-to-cmaf.js.map +7 -0
- package/dist/multicast-auth-catalog.d.ts +115 -0
- package/dist/multicast-auth-catalog.d.ts.map +1 -0
- package/dist/multicast-auth-catalog.js +202 -0
- package/dist/multicast-auth-catalog.js.map +7 -0
- package/dist/multicast-input.d.ts +141 -0
- package/dist/multicast-input.d.ts.map +1 -0
- package/dist/multicast-input.js +390 -0
- package/dist/multicast-input.js.map +7 -0
- package/dist/multicast-manager.d.ts +669 -0
- package/dist/multicast-manager.d.ts.map +1 -0
- package/dist/multicast-manager.js +19101 -0
- package/dist/multicast-manager.js.map +7 -0
- package/dist/multicast-transport.d.ts +271 -0
- package/dist/multicast-transport.d.ts.map +1 -0
- package/dist/multicast-transport.js +2264 -0
- package/dist/multicast-transport.js.map +7 -0
- package/dist/multicast-unwrap.d.ts +57 -0
- package/dist/multicast-unwrap.d.ts.map +1 -0
- package/dist/multicast-unwrap.js +418 -0
- package/dist/multicast-unwrap.js.map +7 -0
- package/dist/no-modules.d.ts +10 -0
- package/dist/no-modules.d.ts.map +1 -0
- package/dist/no-modules.js +9491 -0
- package/dist/no-modules.js.map +7 -0
- package/dist/phase14-catalog-override.d.ts +126 -0
- package/dist/phase14-catalog-override.d.ts.map +1 -0
- package/dist/phase14-catalog-override.js +155 -0
- package/dist/phase14-catalog-override.js.map +7 -0
- package/dist/repair-fetch.d.ts +270 -0
- package/dist/repair-fetch.d.ts.map +1 -0
- package/dist/repair-fetch.js +381 -0
- package/dist/repair-fetch.js.map +7 -0
- package/dist/repair-layers.d.ts +158 -0
- package/dist/repair-layers.d.ts.map +1 -0
- package/dist/repair-layers.js +344 -0
- package/dist/repair-layers.js.map +7 -0
- package/dist/sample-combiner.d.ts +212 -0
- package/dist/sample-combiner.d.ts.map +1 -0
- package/dist/sample-combiner.js +595 -0
- package/dist/sample-combiner.js.map +7 -0
- package/dist/signaling.d.ts +29 -0
- package/dist/signaling.d.ts.map +1 -0
- package/dist/signaling.js +572 -0
- package/dist/signaling.js.map +7 -0
- package/dist/source-auth.d.ts +2 -0
- package/dist/source-auth.d.ts.map +1 -0
- package/dist/source-auth.js +1209 -0
- package/dist/source-auth.js.map +7 -0
- package/dist/switching-set-alignment.d.ts +61 -0
- package/dist/switching-set-alignment.d.ts.map +1 -0
- package/dist/switching-set-alignment.js +70 -0
- package/dist/switching-set-alignment.js.map +7 -0
- package/dist/timestamp-codec.d.ts +34 -0
- package/dist/timestamp-codec.d.ts.map +1 -0
- package/dist/timestamp-codec.js +473 -0
- package/dist/timestamp-codec.js.map +7 -0
- package/dist/track-pipeline-setup.d.ts +83 -0
- package/dist/track-pipeline-setup.d.ts.map +1 -0
- package/dist/track-pipeline-setup.js +291 -0
- package/dist/track-pipeline-setup.js.map +7 -0
- package/dist/track-pipeline.d.ts +121 -0
- package/dist/track-pipeline.d.ts.map +1 -0
- package/dist/track-pipeline.js +209 -0
- package/dist/track-pipeline.js.map +7 -0
- package/dist/track-session.d.ts +414 -0
- package/dist/track-session.d.ts.map +1 -0
- package/dist/track-session.js +754 -0
- package/dist/track-session.js.map +7 -0
- package/dist/transport-pool.d.ts +69 -0
- package/dist/transport-pool.d.ts.map +1 -0
- package/dist/transport-pool.js +74 -0
- package/dist/transport-pool.js.map +7 -0
- package/dist/wasm-mfu-reassembler.d.ts +97 -0
- package/dist/wasm-mfu-reassembler.d.ts.map +1 -0
- package/dist/wasm-mfu-reassembler.js +2133 -0
- package/dist/wasm-mfu-reassembler.js.map +7 -0
- package/dist/wasm-mmtp-container-parser.d.ts +10 -0
- package/dist/wasm-mmtp-container-parser.d.ts.map +1 -0
- package/dist/wasm-mmtp-container-parser.js +6834 -0
- package/dist/wasm-mmtp-container-parser.js.map +7 -0
- package/node_modules/@blockcast/fec-worker/README.md +223 -0
- package/node_modules/@blockcast/fec-worker/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/fec-worker/dist/block-order.d.ts +38 -0
- package/node_modules/@blockcast/fec-worker/dist/block-order.d.ts.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/block-order.js +78 -0
- package/node_modules/@blockcast/fec-worker/dist/block-order.js.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +93 -0
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +346 -0
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts +380 -0
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js +11 -0
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts +14 -0
- package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/fec-worker.js +859 -0
- package/node_modules/@blockcast/fec-worker/dist/fec-worker.js.map +7 -0
- package/node_modules/@blockcast/fec-worker/dist/index.d.ts +10 -0
- package/node_modules/@blockcast/fec-worker/dist/index.d.ts.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/index.js +8 -0
- package/node_modules/@blockcast/fec-worker/dist/index.js.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts +20 -0
- package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js +370 -0
- package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js.map +7 -0
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts +104 -0
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js +181 -0
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts +194 -0
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js +62 -0
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts +9 -0
- package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts.map +1 -0
- package/node_modules/@blockcast/fec-worker/dist/transfer.js +13 -0
- package/node_modules/@blockcast/fec-worker/dist/transfer.js.map +1 -0
- package/node_modules/@blockcast/fec-worker/package.json +59 -0
- package/node_modules/@blockcast/fec-worker/src/block-order.ts +93 -0
- package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +392 -0
- package/node_modules/@blockcast/fec-worker/src/fec-worker-types.ts +399 -0
- package/node_modules/@blockcast/fec-worker/src/fec-worker.ts +1502 -0
- package/node_modules/@blockcast/fec-worker/src/index.ts +35 -0
- package/node_modules/@blockcast/fec-worker/src/shred-fec-worker.ts +589 -0
- package/node_modules/@blockcast/fec-worker/src/shred-worker-client.ts +222 -0
- package/node_modules/@blockcast/fec-worker/src/shred-worker-types.ts +209 -0
- package/node_modules/@blockcast/fec-worker/src/transfer.ts +12 -0
- package/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
- package/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
- package/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
- package/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
- package/node_modules/@blockcast/mmt-base/README.md +134 -0
- package/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts +55 -0
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js +128 -0
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/index.d.ts +7 -0
- package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/index.js +7 -0
- package/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +292 -0
- package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/mmtp.js +612 -0
- package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +18 -0
- package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/net-constants.js +18 -0
- package/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +47 -0
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.js +54 -0
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts +57 -0
- package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/serial-number.js +98 -0
- package/node_modules/@blockcast/mmt-base/dist/serial-number.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts +312 -0
- package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/signalling.js +637 -0
- package/node_modules/@blockcast/mmt-base/dist/signalling.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/package.json +49 -0
- package/node_modules/@blockcast/mmt-base/src/fec-block-profile.ts +145 -0
- package/node_modules/@blockcast/mmt-base/src/index.ts +6 -0
- package/node_modules/@blockcast/mmt-base/src/mmtp.ts +892 -0
- package/node_modules/@blockcast/mmt-base/src/net-constants.ts +17 -0
- package/node_modules/@blockcast/mmt-base/src/ntp-short.ts +67 -0
- package/node_modules/@blockcast/mmt-base/src/serial-number.ts +107 -0
- package/node_modules/@blockcast/mmt-base/src/signalling.ts +862 -0
- package/node_modules/@blockcast/mmt-fec/README.md +271 -0
- package/node_modules/@blockcast/mmt-fec/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts +82 -0
- package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/block-manager.js +296 -0
- package/node_modules/@blockcast/mmt-fec/dist/block-manager.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts +106 -0
- package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/block-processor.js +288 -0
- package/node_modules/@blockcast/mmt-fec/dist/block-processor.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts +46 -0
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js +96 -0
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts +33 -0
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js +57 -0
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts +205 -0
- package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js +240 -0
- package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/index.d.ts +28 -0
- package/node_modules/@blockcast/mmt-fec/dist/index.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/index.js +28 -0
- package/node_modules/@blockcast/mmt-fec/dist/index.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts +20 -0
- package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js +28 -0
- package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts +258 -0
- package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/oti.js +382 -0
- package/node_modules/@blockcast/mmt-fec/dist/oti.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts +41 -0
- package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js +81 -0
- package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/repair-group.d.ts +82 -0
- package/node_modules/@blockcast/mmt-fec/dist/repair-group.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/repair-group.js +170 -0
- package/node_modules/@blockcast/mmt-fec/dist/repair-group.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/README.md +181 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +57 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js +115 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/package.json +50 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/src/index.ts +136 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/README.md +134 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts +55 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js +128 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts +7 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js +7 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +292 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js +612 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +18 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.js +18 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +47 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.js +54 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts +57 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.js +98 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.d.ts +312 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.js +637 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.js.map +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/package.json +49 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/fec-block-profile.ts +145 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/index.ts +6 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/mmtp.ts +892 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/net-constants.ts +17 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/ntp-short.ts +67 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/serial-number.ts +107 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/signalling.ts +862 -0
- package/node_modules/@blockcast/mmt-fec/package.json +79 -0
- package/node_modules/@blockcast/mmt-fec/src/block-manager.ts +371 -0
- package/node_modules/@blockcast/mmt-fec/src/block-processor.ts +439 -0
- package/node_modules/@blockcast/mmt-fec/src/cmaf-recovery.ts +127 -0
- package/node_modules/@blockcast/mmt-fec/src/cmaf-symbols.ts +68 -0
- package/node_modules/@blockcast/mmt-fec/src/fec-block-mapper.ts +310 -0
- package/node_modules/@blockcast/mmt-fec/src/index.ts +94 -0
- package/node_modules/@blockcast/mmt-fec/src/loc-symbols.ts +34 -0
- package/node_modules/@blockcast/mmt-fec/src/oti.ts +463 -0
- package/node_modules/@blockcast/mmt-fec/src/reorder-buffer.ts +101 -0
- package/node_modules/@blockcast/mmt-fec/src/repair-group.ts +195 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts +28 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js +128 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js.map +1 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts +4 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js +4 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js.map +1 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts +37 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js +179 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js.map +1 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts +99 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js +492 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js.map +1 -0
- package/node_modules/@blockcast/mmt-manifest-verify/package.json +45 -0
- package/node_modules/@blockcast/mmt-manifest-verify/src/admission.ts +171 -0
- package/node_modules/@blockcast/mmt-manifest-verify/src/index.ts +3 -0
- package/node_modules/@blockcast/mmt-manifest-verify/src/source-auth.ts +230 -0
- package/node_modules/@blockcast/mmt-manifest-verify/src/verifier.ts +669 -0
- package/node_modules/@noble/hashes/LICENSE +21 -0
- package/node_modules/@noble/hashes/README.md +521 -0
- package/node_modules/@noble/hashes/_assert.d.ts +17 -0
- package/node_modules/@noble/hashes/_assert.d.ts.map +1 -0
- package/node_modules/@noble/hashes/_assert.js +18 -0
- package/node_modules/@noble/hashes/_assert.js.map +1 -0
- package/node_modules/@noble/hashes/_blake.d.ts +14 -0
- package/node_modules/@noble/hashes/_blake.d.ts.map +1 -0
- package/node_modules/@noble/hashes/_blake.js +50 -0
- package/node_modules/@noble/hashes/_blake.js.map +1 -0
- package/node_modules/@noble/hashes/_md.d.ts +51 -0
- package/node_modules/@noble/hashes/_md.d.ts.map +1 -0
- package/node_modules/@noble/hashes/_md.js +162 -0
- package/node_modules/@noble/hashes/_md.js.map +1 -0
- package/node_modules/@noble/hashes/_u64.d.ts +55 -0
- package/node_modules/@noble/hashes/_u64.d.ts.map +1 -0
- package/node_modules/@noble/hashes/_u64.js +90 -0
- package/node_modules/@noble/hashes/_u64.js.map +1 -0
- package/node_modules/@noble/hashes/argon2.d.ts +32 -0
- package/node_modules/@noble/hashes/argon2.d.ts.map +1 -0
- package/node_modules/@noble/hashes/argon2.js +401 -0
- package/node_modules/@noble/hashes/argon2.js.map +1 -0
- package/node_modules/@noble/hashes/blake1.d.ts +106 -0
- package/node_modules/@noble/hashes/blake1.d.ts.map +1 -0
- package/node_modules/@noble/hashes/blake1.js +459 -0
- package/node_modules/@noble/hashes/blake1.js.map +1 -0
- package/node_modules/@noble/hashes/blake2.d.ts +116 -0
- package/node_modules/@noble/hashes/blake2.d.ts.map +1 -0
- package/node_modules/@noble/hashes/blake2.js +420 -0
- package/node_modules/@noble/hashes/blake2.js.map +1 -0
- package/node_modules/@noble/hashes/blake2b.d.ts +11 -0
- package/node_modules/@noble/hashes/blake2b.d.ts.map +1 -0
- package/node_modules/@noble/hashes/blake2b.js +14 -0
- package/node_modules/@noble/hashes/blake2b.js.map +1 -0
- package/node_modules/@noble/hashes/blake2s.d.ts +20 -0
- package/node_modules/@noble/hashes/blake2s.d.ts.map +1 -0
- package/node_modules/@noble/hashes/blake2s.js +24 -0
- package/node_modules/@noble/hashes/blake2s.js.map +1 -0
- package/node_modules/@noble/hashes/blake3.d.ts +54 -0
- package/node_modules/@noble/hashes/blake3.d.ts.map +1 -0
- package/node_modules/@noble/hashes/blake3.js +255 -0
- package/node_modules/@noble/hashes/blake3.js.map +1 -0
- package/node_modules/@noble/hashes/crypto.d.ts +2 -0
- package/node_modules/@noble/hashes/crypto.d.ts.map +1 -0
- package/node_modules/@noble/hashes/crypto.js +5 -0
- package/node_modules/@noble/hashes/crypto.js.map +1 -0
- package/node_modules/@noble/hashes/cryptoNode.d.ts +2 -0
- package/node_modules/@noble/hashes/cryptoNode.d.ts.map +1 -0
- package/node_modules/@noble/hashes/cryptoNode.js +18 -0
- package/node_modules/@noble/hashes/cryptoNode.js.map +1 -0
- package/node_modules/@noble/hashes/eskdf.d.ts +47 -0
- package/node_modules/@noble/hashes/eskdf.d.ts.map +1 -0
- package/node_modules/@noble/hashes/eskdf.js +166 -0
- package/node_modules/@noble/hashes/eskdf.js.map +1 -0
- package/node_modules/@noble/hashes/esm/_assert.d.ts +17 -0
- package/node_modules/@noble/hashes/esm/_assert.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/_assert.js +15 -0
- package/node_modules/@noble/hashes/esm/_assert.js.map +1 -0
- package/node_modules/@noble/hashes/esm/_blake.d.ts +14 -0
- package/node_modules/@noble/hashes/esm/_blake.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/_blake.js +45 -0
- package/node_modules/@noble/hashes/esm/_blake.js.map +1 -0
- package/node_modules/@noble/hashes/esm/_md.d.ts +51 -0
- package/node_modules/@noble/hashes/esm/_md.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/_md.js +155 -0
- package/node_modules/@noble/hashes/esm/_md.js.map +1 -0
- package/node_modules/@noble/hashes/esm/_u64.d.ts +55 -0
- package/node_modules/@noble/hashes/esm/_u64.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/_u64.js +67 -0
- package/node_modules/@noble/hashes/esm/_u64.js.map +1 -0
- package/node_modules/@noble/hashes/esm/argon2.d.ts +32 -0
- package/node_modules/@noble/hashes/esm/argon2.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/argon2.js +392 -0
- package/node_modules/@noble/hashes/esm/argon2.js.map +1 -0
- package/node_modules/@noble/hashes/esm/blake1.d.ts +106 -0
- package/node_modules/@noble/hashes/esm/blake1.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/blake1.js +452 -0
- package/node_modules/@noble/hashes/esm/blake1.js.map +1 -0
- package/node_modules/@noble/hashes/esm/blake2.d.ts +116 -0
- package/node_modules/@noble/hashes/esm/blake2.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/blake2.js +413 -0
- package/node_modules/@noble/hashes/esm/blake2.js.map +1 -0
- package/node_modules/@noble/hashes/esm/blake2b.d.ts +11 -0
- package/node_modules/@noble/hashes/esm/blake2b.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/blake2b.js +11 -0
- package/node_modules/@noble/hashes/esm/blake2b.js.map +1 -0
- package/node_modules/@noble/hashes/esm/blake2s.d.ts +20 -0
- package/node_modules/@noble/hashes/esm/blake2s.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/blake2s.js +21 -0
- package/node_modules/@noble/hashes/esm/blake2s.js.map +1 -0
- package/node_modules/@noble/hashes/esm/blake3.d.ts +54 -0
- package/node_modules/@noble/hashes/esm/blake3.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/blake3.js +251 -0
- package/node_modules/@noble/hashes/esm/blake3.js.map +1 -0
- package/node_modules/@noble/hashes/esm/crypto.d.ts +2 -0
- package/node_modules/@noble/hashes/esm/crypto.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/crypto.js +2 -0
- package/node_modules/@noble/hashes/esm/crypto.js.map +1 -0
- package/node_modules/@noble/hashes/esm/cryptoNode.d.ts +2 -0
- package/node_modules/@noble/hashes/esm/cryptoNode.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/cryptoNode.js +15 -0
- package/node_modules/@noble/hashes/esm/cryptoNode.js.map +1 -0
- package/node_modules/@noble/hashes/esm/eskdf.d.ts +47 -0
- package/node_modules/@noble/hashes/esm/eskdf.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/eskdf.js +160 -0
- package/node_modules/@noble/hashes/esm/eskdf.js.map +1 -0
- package/node_modules/@noble/hashes/esm/hkdf.d.ts +36 -0
- package/node_modules/@noble/hashes/esm/hkdf.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/hkdf.js +82 -0
- package/node_modules/@noble/hashes/esm/hkdf.js.map +1 -0
- package/node_modules/@noble/hashes/esm/hmac.d.ts +35 -0
- package/node_modules/@noble/hashes/esm/hmac.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/hmac.js +86 -0
- package/node_modules/@noble/hashes/esm/hmac.js.map +1 -0
- package/node_modules/@noble/hashes/esm/index.d.ts +2 -0
- package/node_modules/@noble/hashes/esm/index.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/index.js +33 -0
- package/node_modules/@noble/hashes/esm/index.js.map +1 -0
- package/node_modules/@noble/hashes/esm/legacy.d.ts +71 -0
- package/node_modules/@noble/hashes/esm/legacy.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/legacy.js +281 -0
- package/node_modules/@noble/hashes/esm/legacy.js.map +1 -0
- package/node_modules/@noble/hashes/esm/package.json +10 -0
- package/node_modules/@noble/hashes/esm/pbkdf2.d.ts +23 -0
- package/node_modules/@noble/hashes/esm/pbkdf2.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/pbkdf2.js +97 -0
- package/node_modules/@noble/hashes/esm/pbkdf2.js.map +1 -0
- package/node_modules/@noble/hashes/esm/ripemd160.d.ts +13 -0
- package/node_modules/@noble/hashes/esm/ripemd160.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/ripemd160.js +13 -0
- package/node_modules/@noble/hashes/esm/ripemd160.js.map +1 -0
- package/node_modules/@noble/hashes/esm/scrypt.d.ts +34 -0
- package/node_modules/@noble/hashes/esm/scrypt.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/scrypt.js +228 -0
- package/node_modules/@noble/hashes/esm/scrypt.js.map +1 -0
- package/node_modules/@noble/hashes/esm/sha1.d.ts +11 -0
- package/node_modules/@noble/hashes/esm/sha1.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/sha1.js +11 -0
- package/node_modules/@noble/hashes/esm/sha1.js.map +1 -0
- package/node_modules/@noble/hashes/esm/sha2.d.ts +159 -0
- package/node_modules/@noble/hashes/esm/sha2.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/sha2.js +375 -0
- package/node_modules/@noble/hashes/esm/sha2.js.map +1 -0
- package/node_modules/@noble/hashes/esm/sha256.d.ts +20 -0
- package/node_modules/@noble/hashes/esm/sha256.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/sha256.js +20 -0
- package/node_modules/@noble/hashes/esm/sha256.js.map +1 -0
- package/node_modules/@noble/hashes/esm/sha3-addons.d.ts +142 -0
- package/node_modules/@noble/hashes/esm/sha3-addons.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/sha3-addons.js +393 -0
- package/node_modules/@noble/hashes/esm/sha3-addons.js.map +1 -0
- package/node_modules/@noble/hashes/esm/sha3.d.ts +53 -0
- package/node_modules/@noble/hashes/esm/sha3.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/sha3.js +234 -0
- package/node_modules/@noble/hashes/esm/sha3.js.map +1 -0
- package/node_modules/@noble/hashes/esm/sha512.d.ts +26 -0
- package/node_modules/@noble/hashes/esm/sha512.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/sha512.js +26 -0
- package/node_modules/@noble/hashes/esm/sha512.js.map +1 -0
- package/node_modules/@noble/hashes/esm/utils.d.ts +161 -0
- package/node_modules/@noble/hashes/esm/utils.d.ts.map +1 -0
- package/node_modules/@noble/hashes/esm/utils.js +281 -0
- package/node_modules/@noble/hashes/esm/utils.js.map +1 -0
- package/node_modules/@noble/hashes/hkdf.d.ts +36 -0
- package/node_modules/@noble/hashes/hkdf.d.ts.map +1 -0
- package/node_modules/@noble/hashes/hkdf.js +88 -0
- package/node_modules/@noble/hashes/hkdf.js.map +1 -0
- package/node_modules/@noble/hashes/hmac.d.ts +35 -0
- package/node_modules/@noble/hashes/hmac.d.ts.map +1 -0
- package/node_modules/@noble/hashes/hmac.js +91 -0
- package/node_modules/@noble/hashes/hmac.js.map +1 -0
- package/node_modules/@noble/hashes/index.d.ts +1 -0
- package/node_modules/@noble/hashes/index.d.ts.map +1 -0
- package/node_modules/@noble/hashes/index.js +33 -0
- package/node_modules/@noble/hashes/index.js.map +1 -0
- package/node_modules/@noble/hashes/legacy.d.ts +71 -0
- package/node_modules/@noble/hashes/legacy.d.ts.map +1 -0
- package/node_modules/@noble/hashes/legacy.js +287 -0
- package/node_modules/@noble/hashes/legacy.js.map +1 -0
- package/node_modules/@noble/hashes/package.json +266 -0
- package/node_modules/@noble/hashes/pbkdf2.d.ts +23 -0
- package/node_modules/@noble/hashes/pbkdf2.d.ts.map +1 -0
- package/node_modules/@noble/hashes/pbkdf2.js +101 -0
- package/node_modules/@noble/hashes/pbkdf2.js.map +1 -0
- package/node_modules/@noble/hashes/ripemd160.d.ts +13 -0
- package/node_modules/@noble/hashes/ripemd160.d.ts.map +1 -0
- package/node_modules/@noble/hashes/ripemd160.js +16 -0
- package/node_modules/@noble/hashes/ripemd160.js.map +1 -0
- package/node_modules/@noble/hashes/scrypt.d.ts +34 -0
- package/node_modules/@noble/hashes/scrypt.d.ts.map +1 -0
- package/node_modules/@noble/hashes/scrypt.js +232 -0
- package/node_modules/@noble/hashes/scrypt.js.map +1 -0
- package/node_modules/@noble/hashes/sha1.d.ts +11 -0
- package/node_modules/@noble/hashes/sha1.d.ts.map +1 -0
- package/node_modules/@noble/hashes/sha1.js +14 -0
- package/node_modules/@noble/hashes/sha1.js.map +1 -0
- package/node_modules/@noble/hashes/sha2.d.ts +159 -0
- package/node_modules/@noble/hashes/sha2.d.ts.map +1 -0
- package/node_modules/@noble/hashes/sha2.js +384 -0
- package/node_modules/@noble/hashes/sha2.js.map +1 -0
- package/node_modules/@noble/hashes/sha256.d.ts +20 -0
- package/node_modules/@noble/hashes/sha256.d.ts.map +1 -0
- package/node_modules/@noble/hashes/sha256.js +23 -0
- package/node_modules/@noble/hashes/sha256.js.map +1 -0
- package/node_modules/@noble/hashes/sha3-addons.d.ts +142 -0
- package/node_modules/@noble/hashes/sha3-addons.d.ts.map +1 -0
- package/node_modules/@noble/hashes/sha3-addons.js +402 -0
- package/node_modules/@noble/hashes/sha3-addons.js.map +1 -0
- package/node_modules/@noble/hashes/sha3.d.ts +53 -0
- package/node_modules/@noble/hashes/sha3.d.ts.map +1 -0
- package/node_modules/@noble/hashes/sha3.js +239 -0
- package/node_modules/@noble/hashes/sha3.js.map +1 -0
- package/node_modules/@noble/hashes/sha512.d.ts +26 -0
- package/node_modules/@noble/hashes/sha512.d.ts.map +1 -0
- package/node_modules/@noble/hashes/sha512.js +29 -0
- package/node_modules/@noble/hashes/sha512.js.map +1 -0
- package/node_modules/@noble/hashes/src/_assert.ts +22 -0
- package/node_modules/@noble/hashes/src/_blake.ts +50 -0
- package/node_modules/@noble/hashes/src/_md.ts +176 -0
- package/node_modules/@noble/hashes/src/_u64.ts +91 -0
- package/node_modules/@noble/hashes/src/argon2.ts +497 -0
- package/node_modules/@noble/hashes/src/blake1.ts +534 -0
- package/node_modules/@noble/hashes/src/blake2.ts +486 -0
- package/node_modules/@noble/hashes/src/blake2b.ts +10 -0
- package/node_modules/@noble/hashes/src/blake2s.ts +20 -0
- package/node_modules/@noble/hashes/src/blake3.ts +272 -0
- package/node_modules/@noble/hashes/src/crypto.ts +9 -0
- package/node_modules/@noble/hashes/src/cryptoNode.ts +15 -0
- package/node_modules/@noble/hashes/src/eskdf.ts +187 -0
- package/node_modules/@noble/hashes/src/hkdf.ts +88 -0
- package/node_modules/@noble/hashes/src/hmac.ts +94 -0
- package/node_modules/@noble/hashes/src/index.ts +31 -0
- package/node_modules/@noble/hashes/src/legacy.ts +293 -0
- package/node_modules/@noble/hashes/src/pbkdf2.ts +122 -0
- package/node_modules/@noble/hashes/src/ripemd160.ts +12 -0
- package/node_modules/@noble/hashes/src/scrypt.ts +257 -0
- package/node_modules/@noble/hashes/src/sha1.ts +10 -0
- package/node_modules/@noble/hashes/src/sha2.ts +402 -0
- package/node_modules/@noble/hashes/src/sha256.ts +24 -0
- package/node_modules/@noble/hashes/src/sha3-addons.ts +499 -0
- package/node_modules/@noble/hashes/src/sha3.ts +258 -0
- package/node_modules/@noble/hashes/src/sha512.ts +34 -0
- package/node_modules/@noble/hashes/src/utils.ts +395 -0
- package/node_modules/@noble/hashes/utils.d.ts +161 -0
- package/node_modules/@noble/hashes/utils.d.ts.map +1 -0
- package/node_modules/@noble/hashes/utils.js +313 -0
- package/node_modules/@noble/hashes/utils.js.map +1 -0
- package/package.json +179 -8
- package/schemas/catalog.schema.json +357 -0
- package/src/align.ts +239 -0
- package/src/audio-init-parser.ts +189 -0
- package/src/audio-ring-buffer.ts +120 -0
- package/src/authored-timing.ts +63 -0
- package/src/block-policy.ts +903 -0
- package/src/carp-catalog.ts +1065 -0
- package/src/catalog-fec-params.ts +729 -0
- package/src/catalog-network-source.ts +74 -0
- package/src/catalog-validator.ts +34 -0
- package/src/cmaf-block-assembler.ts +1255 -0
- package/src/cmaf-group-batcher.ts +104 -0
- package/src/cmcd-producer.ts +329 -0
- package/src/codec-utils.ts +1984 -0
- package/src/conceal-timeline.ts +293 -0
- package/src/delivery-gap.ts +147 -0
- package/src/duplicate-ledger.ts +372 -0
- package/src/fec-codec.ts +61 -0
- package/src/fec-manager.ts +4298 -0
- package/src/fec-starvation-detector.ts +128 -0
- package/src/fec-state.ts +219 -0
- package/src/fec-worker-setup.ts +283 -0
- package/src/fmp4-completion.ts +47 -0
- package/src/fmp4-reader.ts +112 -0
- package/src/index.ts +590 -21
- package/src/iso-block-locator.ts +329 -0
- package/src/iso-bmff-timing.ts +813 -0
- package/src/loc.ts +128 -154
- package/src/manifest-auth-plan.ts +107 -0
- package/src/media-router.ts +841 -0
- package/src/mfu-reassembler.ts +3480 -155
- package/src/mfu-reassembly-policy.ts +134 -0
- package/src/mmtp-container-parser.ts +2380 -0
- package/src/mmtp-decode-pipeline.ts +406 -0
- package/src/mmtp-router.ts +4860 -0
- package/src/mmtp-to-cmaf.ts +977 -0
- package/src/multicast-auth-catalog.ts +340 -0
- package/src/multicast-input.ts +793 -0
- package/src/multicast-manager.ts +2783 -0
- package/src/multicast-transport.ts +1058 -0
- package/src/multicast-unwrap.ts +132 -0
- package/src/no-modules.ts +21 -0
- package/src/phase14-catalog-override.ts +336 -0
- package/src/repair-fetch.ts +772 -0
- package/src/repair-layers.ts +533 -0
- package/src/sample-combiner.ts +938 -0
- package/src/signaling.ts +78 -0
- package/src/source-auth.ts +1 -0
- package/src/switching-set-alignment.ts +109 -0
- package/src/timestamp-codec.ts +265 -0
- package/src/track-pipeline-setup.ts +205 -0
- package/src/track-pipeline.ts +332 -0
- package/src/track-session.ts +1010 -0
- package/src/transport-pool.ts +128 -0
- package/src/wasm-mfu-reassembler.ts +405 -0
- package/src/wasm-mmtp-container-parser.ts +23 -0
- package/test-vectors/align.json +199 -0
- package/test-vectors/catalog/README.md +106 -0
- package/test-vectors/catalog/negative/archive-origin-with-path.json +33 -0
- package/test-vectors/catalog/negative/archive-unknown-field.json +34 -0
- package/test-vectors/catalog/negative/fec-block-profile-missing.json +51 -0
- package/test-vectors/catalog/negative/fec-block-profile-unknown.json +52 -0
- package/test-vectors/catalog/negative/fec-enhancement-repair-removed.json +28 -0
- package/test-vectors/catalog/negative/fec-repair-layer-geometry.json +33 -0
- package/test-vectors/catalog/negative/fec-repair-layer-missing-symbols.json +31 -0
- package/test-vectors/catalog/negative/fec-repair-multiple-depends.json +32 -0
- package/test-vectors/catalog/negative/fec-repair-overlay-with-layer.json +34 -0
- package/test-vectors/catalog/negative/fec-repair-priority-out-of-band.json +24 -0
- package/test-vectors/catalog/negative/fec-repair-source-symbols-without-scope.json +33 -0
- package/test-vectors/catalog/negative/legacy-selection-params.json +15 -0
- package/test-vectors/catalog/negative/media-track-repair-layer.json +16 -0
- package/test-vectors/catalog/negative/missing-mmtp-fields.json +12 -0
- package/test-vectors/catalog/negative/multicast-auth-padded-key.json +37 -0
- package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +53 -0
- package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +37 -0
- package/test-vectors/catalog/negative/multicast-endpoint-missing-protocol-and-source.json +24 -0
- package/test-vectors/catalog/negative/multicast-network-source-empty.json +44 -0
- package/test-vectors/catalog/negative/multicast-packet-id-signaling.json +26 -0
- package/test-vectors/catalog/negative/network-source-object.json +25 -0
- package/test-vectors/catalog/negative/raptorq-source-symbols-over-max.json +24 -0
- package/test-vectors/catalog/negative/raptorq-unaligned-symbol.json +27 -0
- package/test-vectors/catalog/negative/repair-track-legacy-shape.json +19 -0
- package/test-vectors/catalog/negative/track-role-empty.json +16 -0
- package/test-vectors/catalog/negative/track-role-non-string.json +16 -0
- package/test-vectors/catalog/positive/archive-origin.json +34 -0
- package/test-vectors/catalog/positive/fec-iso-ssbg1.json +52 -0
- package/test-vectors/catalog/positive/fec-layered-repair.json +106 -0
- package/test-vectors/catalog/positive/fec-multicast.json +52 -0
- package/test-vectors/catalog/positive/flat-av.json +30 -0
- package/test-vectors/catalog/positive/multicast-auth-rotation.json +55 -0
- package/test-vectors/catalog/positive/multicast-native-ssm-no-network-source.json +43 -0
- package/test-vectors/catalog/positive/ticks-and-mixed-timescales.json +39 -0
- package/test-vectors/catalog/positive/track-role.json +49 -0
- package/test-vectors/catalog/releases/moqcast-native-v16-v1/catalog.json +65 -0
- package/test-vectors/catalog/releases/moqcast-native-v16-v1.archive.sha256 +1 -0
- package/test-vectors/catalog-multicast-auth-v1.json +26 -0
- package/test-vectors/catalog-validation.json +732 -0
- package/test-vectors/catalog-validator-public-api.mjs +25 -0
- package/test-vectors/generate-source-auth-fixture.mjs +114 -0
- package/test-vectors/hevc-x265-repeat-headers-v1.json +40 -0
- package/test-vectors/source-auth-v1.json +61 -0
- package/test-vectors/source-contiguity-v1.json +131 -0
- package/test-vectors/subsample-placement-v1.json +230 -0
- package/dist/mmtp.d.ts +0 -73
- package/dist/mmtp.d.ts.map +0 -1
- package/dist/mmtp.js +0 -210
- package/dist/mmtp.js.map +0 -1
- package/src/mmtp.ts +0 -277
|
@@ -0,0 +1,3899 @@
|
|
|
1
|
+
// src/track-session.ts
|
|
2
|
+
import { fecBlockBudgetMs } from "@blockcast/mmt-fec";
|
|
3
|
+
function computeFecDeliveryWindowMs(config) {
|
|
4
|
+
const fecInterleaveMs = config.interleaveDepthMs;
|
|
5
|
+
const fecK = config.sourceSymbols;
|
|
6
|
+
if (!Number.isFinite(fecInterleaveMs) || fecInterleaveMs <= 0) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
`[TrackSession] fecInterleaveMs=${fecInterleaveMs}. Derive a positive FEC interleave window from the stream catalog.`
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
if (!Number.isFinite(fecK) || fecK <= 0) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`[TrackSession] fecK=${fecK}. Derive a positive FEC source-symbol count from the stream catalog.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
if (config.jitterBufferMs === void 0) return Math.ceil(fecInterleaveMs);
|
|
17
|
+
return fecBlockBudgetMs(fecInterleaveMs, config.jitterBufferMs);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/delivery-gap.ts
|
|
21
|
+
var DeliveryGapEstimator = class {
|
|
22
|
+
/** Most recent arrival, or undefined before the first one since reset. */
|
|
23
|
+
#lastArrivalMs;
|
|
24
|
+
/** Last MMTP packet_sequence_number seen per packet_id. */
|
|
25
|
+
#lastSequence = /* @__PURE__ */ new Map();
|
|
26
|
+
/**
|
|
27
|
+
* Monotonic deque of (arrivalMs, gapMs): gaps strictly decrease from head to
|
|
28
|
+
* tail, so the head is the window maximum. Amortized O(1) per arrival.
|
|
29
|
+
*/
|
|
30
|
+
#times = [];
|
|
31
|
+
#gaps = [];
|
|
32
|
+
#head = 0;
|
|
33
|
+
#windowMs;
|
|
34
|
+
/**
|
|
35
|
+
* @param windowMs trailing window in ms, which also caps the reported gap,
|
|
36
|
+
* or undefined when the catalog gives none (no gap is reported).
|
|
37
|
+
*/
|
|
38
|
+
setWindowMs(windowMs) {
|
|
39
|
+
if (windowMs !== void 0 && !(Number.isFinite(windowMs) && windowMs > 0)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`[DeliveryGap] window must be a finite positive ms value or undefined (got ${windowMs})`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
this.#windowMs = windowMs;
|
|
45
|
+
}
|
|
46
|
+
get windowMs() {
|
|
47
|
+
return this.#windowMs;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Record one multicast datagram arrival at receiver time `nowMs`, carrying
|
|
51
|
+
* MMTP `packetSequenceNumber` (32-bit, per packet_id) on `packetId`.
|
|
52
|
+
*/
|
|
53
|
+
noteArrival(nowMs, packetId, packetSequenceNumber) {
|
|
54
|
+
const last = this.#lastArrivalMs;
|
|
55
|
+
const previousSequence = this.#lastSequence.get(packetId);
|
|
56
|
+
this.#lastArrivalMs = nowMs;
|
|
57
|
+
this.#lastSequence.set(packetId, packetSequenceNumber);
|
|
58
|
+
if (last === void 0 || previousSequence === void 0) return;
|
|
59
|
+
if (this.#windowMs === void 0) return;
|
|
60
|
+
if (previousSequence + 1 >>> 0 !== packetSequenceNumber >>> 0) return;
|
|
61
|
+
const gap = nowMs - last;
|
|
62
|
+
this.#evict(nowMs);
|
|
63
|
+
while (this.#gaps.length > this.#head && this.#gaps[this.#gaps.length - 1] <= gap) {
|
|
64
|
+
this.#gaps.pop();
|
|
65
|
+
this.#times.pop();
|
|
66
|
+
}
|
|
67
|
+
this.#gaps.push(gap);
|
|
68
|
+
this.#times.push(nowMs);
|
|
69
|
+
this.#compact();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Largest gap observed within the window ending at `nowMs`, at most the
|
|
73
|
+
* window itself; 0 if none or when no window is set.
|
|
74
|
+
*/
|
|
75
|
+
peakGapMs(nowMs) {
|
|
76
|
+
const windowMs = this.#windowMs;
|
|
77
|
+
if (windowMs === void 0) return 0;
|
|
78
|
+
this.#evict(nowMs);
|
|
79
|
+
return this.#gaps.length > this.#head ? Math.min(this.#gaps[this.#head], windowMs) : 0;
|
|
80
|
+
}
|
|
81
|
+
/** Forget everything: a new delivery path starts with no measurement. */
|
|
82
|
+
reset() {
|
|
83
|
+
this.#lastArrivalMs = void 0;
|
|
84
|
+
this.#lastSequence.clear();
|
|
85
|
+
this.#times = [];
|
|
86
|
+
this.#gaps = [];
|
|
87
|
+
this.#head = 0;
|
|
88
|
+
}
|
|
89
|
+
#evict(nowMs) {
|
|
90
|
+
const windowMs = this.#windowMs;
|
|
91
|
+
if (windowMs === void 0) return;
|
|
92
|
+
while (this.#times.length > this.#head && this.#times[this.#head] < nowMs - windowMs) {
|
|
93
|
+
this.#head++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
#compact() {
|
|
97
|
+
if (this.#head > 0 && this.#head * 2 >= this.#times.length) {
|
|
98
|
+
this.#times = this.#times.slice(this.#head);
|
|
99
|
+
this.#gaps = this.#gaps.slice(this.#head);
|
|
100
|
+
this.#head = 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/fec-manager.ts
|
|
106
|
+
import {
|
|
107
|
+
deriveFecInterleaveDepth as deriveFecInterleaveDepth2,
|
|
108
|
+
deriveFecRetentionBlocks,
|
|
109
|
+
tryDeriveFecRetentionBlocks
|
|
110
|
+
} from "@blockcast/mmt-fec";
|
|
111
|
+
|
|
112
|
+
// ../mmt-alta-parse/dist/index.js
|
|
113
|
+
var MPU_HEADER_SIZE = 8;
|
|
114
|
+
var MPU_LENGTH_FIELD_SIZE = 2;
|
|
115
|
+
var MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE;
|
|
116
|
+
|
|
117
|
+
// ../mmt-base/dist/mmtp.js
|
|
118
|
+
var MPU_HEADER_SIZE2 = 8;
|
|
119
|
+
var MPU_LENGTH_FIELD_SIZE2 = 2;
|
|
120
|
+
var MPU_HEADER_REMAINDER_SIZE2 = MPU_HEADER_SIZE2 - MPU_LENGTH_FIELD_SIZE2;
|
|
121
|
+
|
|
122
|
+
// ../mmt-base/dist/ntp-short.js
|
|
123
|
+
var NTP_SHORT_BITS = 32;
|
|
124
|
+
var NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS;
|
|
125
|
+
var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
|
|
126
|
+
|
|
127
|
+
// ../mmt-base/dist/serial-number.js
|
|
128
|
+
var SERIAL32_SPAN = 4294967296;
|
|
129
|
+
var SERIAL32_HALF = 2147483648;
|
|
130
|
+
function assertUint32(value, name) {
|
|
131
|
+
if (!Number.isInteger(value) || value < 0 || value >= SERIAL32_SPAN) {
|
|
132
|
+
throw new RangeError(`${name} must be a uint32 (got ${value})`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function serial32Add(s, n) {
|
|
136
|
+
assertUint32(s, "s");
|
|
137
|
+
if (!Number.isInteger(n) || n <= -SERIAL32_HALF || n >= SERIAL32_HALF) {
|
|
138
|
+
throw new RangeError(`serial addend must lie in (-2^31, 2^31) (got ${n})`);
|
|
139
|
+
}
|
|
140
|
+
return (s + n + SERIAL32_SPAN) % SERIAL32_SPAN;
|
|
141
|
+
}
|
|
142
|
+
function serial32Distance(from, to) {
|
|
143
|
+
return to - from >>> 0;
|
|
144
|
+
}
|
|
145
|
+
function serial32Precedes(a, b) {
|
|
146
|
+
const distance = serial32Distance(a, b);
|
|
147
|
+
return distance !== 0 && distance < SERIAL32_HALF;
|
|
148
|
+
}
|
|
149
|
+
function kStrideBlockStart(sbn, k) {
|
|
150
|
+
assertUint32(sbn, "sbn");
|
|
151
|
+
if (!Number.isInteger(k) || k < 1 || k >= SERIAL32_HALF) {
|
|
152
|
+
throw new RangeError(`k must be a positive integer below 2^31 (got ${k})`);
|
|
153
|
+
}
|
|
154
|
+
const high = Math.floor(sbn / 65536);
|
|
155
|
+
const low = sbn % 65536;
|
|
156
|
+
return (high * k % SERIAL32_SPAN * 65536 + low * k) % SERIAL32_SPAN;
|
|
157
|
+
}
|
|
158
|
+
function kStrideBlockPrecedes(a, b, k) {
|
|
159
|
+
return serial32Precedes(kStrideBlockStart(a, k), kStrideBlockStart(b, k));
|
|
160
|
+
}
|
|
161
|
+
function kStrideBlocksBehind(a, b, k) {
|
|
162
|
+
const from = kStrideBlockStart(a, k);
|
|
163
|
+
const to = kStrideBlockStart(b, k);
|
|
164
|
+
if (!serial32Precedes(from, to))
|
|
165
|
+
return 0;
|
|
166
|
+
return Math.ceil(serial32Distance(from, to) / k);
|
|
167
|
+
}
|
|
168
|
+
function isKStrideBlock(sbn, k) {
|
|
169
|
+
return Number.isInteger(sbn) && sbn >= 0 && sbn < SERIAL32_SPAN && Number.isInteger(k) && k >= 1 && k < SERIAL32_HALF;
|
|
170
|
+
}
|
|
171
|
+
function kStrideNextBlock(sbn, k) {
|
|
172
|
+
assertUint32(sbn, "sbn");
|
|
173
|
+
kStrideBlockStart(sbn, k);
|
|
174
|
+
return (sbn + 1) * k >= SERIAL32_SPAN ? 0 : sbn + 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ../mmt-base/dist/fec-block-profile.js
|
|
178
|
+
var FEC_BLOCK_PROFILES = Object.freeze([
|
|
179
|
+
"k-padded-v1",
|
|
180
|
+
"iso-ssbg1-v1"
|
|
181
|
+
]);
|
|
182
|
+
function isFecBlockProfile(value) {
|
|
183
|
+
return value === "k-padded-v1" || value === "iso-ssbg1-v1";
|
|
184
|
+
}
|
|
185
|
+
function parseFecBlockProfile(value, where) {
|
|
186
|
+
if (isFecBlockProfile(value))
|
|
187
|
+
return value;
|
|
188
|
+
if (value === void 0) {
|
|
189
|
+
throw new Error(`${where} is required: name the FEC block profile ("k-padded-v1" or "iso-ssbg1-v1")`);
|
|
190
|
+
}
|
|
191
|
+
throw new Error(`${where} must be "k-padded-v1" or "iso-ssbg1-v1" (got ${JSON.stringify(value)})`);
|
|
192
|
+
}
|
|
193
|
+
var SSBG1_LENGTH_FIELD_BYTES = 2;
|
|
194
|
+
function ssbg1MaxPacketLength(symbolSize) {
|
|
195
|
+
return symbolSize - SSBG1_LENGTH_FIELD_BYTES;
|
|
196
|
+
}
|
|
197
|
+
function ssbg1FitsSymbol(packetLength, symbolSize) {
|
|
198
|
+
return Number.isSafeInteger(packetLength) && packetLength >= 0 && packetLength <= ssbg1MaxPacketLength(symbolSize) && packetLength <= 65535;
|
|
199
|
+
}
|
|
200
|
+
function isoBlockEnd(ssStart, ssbLength) {
|
|
201
|
+
return serial32Add(ssStart, ssbLength);
|
|
202
|
+
}
|
|
203
|
+
function isoBlockContains(ssStart, ssbLength, ssId) {
|
|
204
|
+
return serial32Distance(ssStart, ssId) < ssbLength;
|
|
205
|
+
}
|
|
206
|
+
function isoBlocksBehindLowerBound(a, b, kMax) {
|
|
207
|
+
if (!Number.isSafeInteger(kMax) || kMax <= 0) {
|
|
208
|
+
throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`);
|
|
209
|
+
}
|
|
210
|
+
return serial32Precedes(a, b) ? Math.floor(serial32Distance(a, b) / kMax) : 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/iso-block-locator.ts
|
|
214
|
+
var IsoBlockLocator = class _IsoBlockLocator {
|
|
215
|
+
/** Known blocks and spans, in serial order of their start. */
|
|
216
|
+
#blocks = [];
|
|
217
|
+
/** Held first-arrival sources whose block no repair has stated yet. */
|
|
218
|
+
#held = /* @__PURE__ */ new Map();
|
|
219
|
+
/** End of contiguous coverage: the SS_ID the next unknown block starts at. */
|
|
220
|
+
#coverageEnd;
|
|
221
|
+
#kMax;
|
|
222
|
+
#retentionBlocks;
|
|
223
|
+
#heldEvicted = 0;
|
|
224
|
+
#newest;
|
|
225
|
+
/**
|
|
226
|
+
* @param kMax the catalog maximum K (`sourceSymbols`), the bound on SSB_length.
|
|
227
|
+
* @param retentionBlocks how many blocks behind the newest position the
|
|
228
|
+
* locator keeps, derived by its owner from catalog timing; required, so
|
|
229
|
+
* no locator exists without retention.
|
|
230
|
+
*/
|
|
231
|
+
constructor(kMax, retentionBlocks) {
|
|
232
|
+
if (!Number.isSafeInteger(kMax) || kMax <= 0) {
|
|
233
|
+
throw new Error(`[IsoBlockLocator] maximum K must be a positive integer (got ${kMax})`);
|
|
234
|
+
}
|
|
235
|
+
this.#kMax = kMax;
|
|
236
|
+
this.#retentionBlocks = _IsoBlockLocator.#checkedRetention(retentionBlocks);
|
|
237
|
+
}
|
|
238
|
+
static #checkedRetention(retentionBlocks) {
|
|
239
|
+
if (!Number.isSafeInteger(retentionBlocks) || retentionBlocks <= 0) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`[IsoBlockLocator] retention must be a positive integer number of blocks (got ${retentionBlocks})`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return retentionBlocks;
|
|
245
|
+
}
|
|
246
|
+
get kMax() {
|
|
247
|
+
return this.#kMax;
|
|
248
|
+
}
|
|
249
|
+
/** Blocks behind the newest position the locator keeps. */
|
|
250
|
+
get retentionBlocks() {
|
|
251
|
+
return this.#retentionBlocks;
|
|
252
|
+
}
|
|
253
|
+
/** Update the retention when the catalog timing it derives from changes. */
|
|
254
|
+
set retentionBlocks(retentionBlocks) {
|
|
255
|
+
this.#retentionBlocks = _IsoBlockLocator.#checkedRetention(retentionBlocks);
|
|
256
|
+
}
|
|
257
|
+
/** The most sources held at once: the SS_IDs the retention horizon spans. */
|
|
258
|
+
get heldCapacity() {
|
|
259
|
+
return (this.#retentionBlocks + 1) * this.#kMax;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The newest position the stream has reached (RFC 1982 order): the newest
|
|
263
|
+
* SS_ID of a source held, or fed once located (`observe`), or SS_Start of a
|
|
264
|
+
* placed repair, so no source the stream has fed follows it. Undefined
|
|
265
|
+
* before the first one.
|
|
266
|
+
*/
|
|
267
|
+
get newest() {
|
|
268
|
+
return this.#newest;
|
|
269
|
+
}
|
|
270
|
+
/** Advance `newest` to `position` if it is newer; retires nothing. */
|
|
271
|
+
observe(position) {
|
|
272
|
+
if (this.#newest === void 0 || serial32Precedes(this.#newest, position)) this.#newest = position;
|
|
273
|
+
}
|
|
274
|
+
/** Held sources evicted by `heldCapacity` (never by the horizon itself). */
|
|
275
|
+
get heldEvicted() {
|
|
276
|
+
return this.#heldEvicted;
|
|
277
|
+
}
|
|
278
|
+
/** Number of known blocks and spans. */
|
|
279
|
+
get blockCount() {
|
|
280
|
+
return this.#blocks.length;
|
|
281
|
+
}
|
|
282
|
+
/** End of contiguous coverage, or undefined before the first repair. */
|
|
283
|
+
get coverageEnd() {
|
|
284
|
+
return this.#coverageEnd;
|
|
285
|
+
}
|
|
286
|
+
/** Number of sources held for a block no repair has stated yet. */
|
|
287
|
+
get heldCount() {
|
|
288
|
+
return this.#held.size;
|
|
289
|
+
}
|
|
290
|
+
/** The known block or span containing `ssId`, with its ESI. */
|
|
291
|
+
locate(ssId) {
|
|
292
|
+
for (let i = this.#blocks.length - 1; i >= 0; i--) {
|
|
293
|
+
const block = this.#blocks[i];
|
|
294
|
+
if (isoBlockContains(block.start, block.length, ssId)) {
|
|
295
|
+
return { block, esi: serial32Distance(block.start, ssId) };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
/** The known block or span starting exactly at `start`. */
|
|
301
|
+
blockAt(start) {
|
|
302
|
+
return this.#blocks.find((block) => block.start === start);
|
|
303
|
+
}
|
|
304
|
+
/** Known blocks and spans overlapping the SS_IDs strictly between `a` and `b`. */
|
|
305
|
+
blocksBetween(a, b) {
|
|
306
|
+
const first = a + 1 >>> 0;
|
|
307
|
+
const count = serial32Distance(a, b) - 1;
|
|
308
|
+
if (!serial32Precedes(a, b) || count <= 0) return [];
|
|
309
|
+
return this.#blocks.filter((block) => {
|
|
310
|
+
const toBlock = serial32Distance(first, block.start);
|
|
311
|
+
const fromBlock = serial32Distance(block.start, first);
|
|
312
|
+
return toBlock < count || fromBlock < block.length;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Whether every SS_ID strictly between `a` and `b` lies in known coverage,
|
|
317
|
+
* so no block that could hold them is still to be stated by a repair.
|
|
318
|
+
*/
|
|
319
|
+
coversBetween(a, b) {
|
|
320
|
+
if (this.#coverageEnd === void 0) return false;
|
|
321
|
+
if (!serial32Precedes(a, b) || serial32Distance(a, b) <= 1) return true;
|
|
322
|
+
return serial32Precedes(b - 1 >>> 0, this.#coverageEnd);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Hold a first-arrival source symbol whose block is not yet known. Returns
|
|
326
|
+
* false for a second copy of an SS_ID already held, so the first copy wins.
|
|
327
|
+
* Retires behind `ssId` (see the class note).
|
|
328
|
+
*/
|
|
329
|
+
hold(ssId, payload) {
|
|
330
|
+
if (this.#held.has(ssId)) return false;
|
|
331
|
+
this.#held.set(ssId, payload);
|
|
332
|
+
this.retireBehind(ssId);
|
|
333
|
+
for (const oldest of this.#held.keys()) {
|
|
334
|
+
if (this.#held.size <= this.heldCapacity) break;
|
|
335
|
+
this.#held.delete(oldest);
|
|
336
|
+
this.#heldEvicted++;
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
/** Whether `ssId` is held unplaced. */
|
|
341
|
+
isHeld(ssId) {
|
|
342
|
+
return this.#held.has(ssId);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Apply one repair symbol's `(SS_Start, SSB_length)`. The first repair of a
|
|
346
|
+
* block states its range and places every held source in it; later repair
|
|
347
|
+
* symbols of the same block change nothing. A range that contradicts a
|
|
348
|
+
* known block (a different length, or an overlap with another block or a
|
|
349
|
+
* span) fails closed: the repair symbol is dropped and no block changes.
|
|
350
|
+
* A newly stated block retires behind its SS_Start (see the class note).
|
|
351
|
+
*/
|
|
352
|
+
placeRepair(start, length) {
|
|
353
|
+
if (!Number.isSafeInteger(length) || length <= 0 || length > this.#kMax) {
|
|
354
|
+
return { status: "conflict", reason: `SSB_length ${length} is outside 1..${this.#kMax}` };
|
|
355
|
+
}
|
|
356
|
+
const known = this.blockAt(start);
|
|
357
|
+
if (known !== void 0) {
|
|
358
|
+
if (known.span) {
|
|
359
|
+
return { status: "overlap", reason: `repair block ${start}+${length} falls inside a repair-less span` };
|
|
360
|
+
}
|
|
361
|
+
if (known.length !== length) {
|
|
362
|
+
return {
|
|
363
|
+
status: "conflict",
|
|
364
|
+
reason: `repair SSB_length ${length} conflicts with ${known.length} for SS_Start ${start}`
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
return { status: "placed", block: known, placed: [] };
|
|
368
|
+
}
|
|
369
|
+
for (const block2 of this.#blocks) {
|
|
370
|
+
if (isoBlockContains(block2.start, block2.length, start) || isoBlockContains(start, length, block2.start)) {
|
|
371
|
+
return {
|
|
372
|
+
status: "overlap",
|
|
373
|
+
reason: `repair block ${start}+${length} overlaps known block ${block2.start}+${block2.length}`
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
let span;
|
|
378
|
+
if (this.#coverageEnd === void 0) {
|
|
379
|
+
for (const ssId of this.#held.keys()) {
|
|
380
|
+
if (serial32Precedes(ssId, start)) this.#held.delete(ssId);
|
|
381
|
+
}
|
|
382
|
+
this.#coverageEnd = start;
|
|
383
|
+
} else if (serial32Precedes(start, this.#coverageEnd)) {
|
|
384
|
+
return { status: "stale", reason: `repair block ${start}+${length} precedes known coverage` };
|
|
385
|
+
} else if (start !== this.#coverageEnd) {
|
|
386
|
+
const spanBlock = {
|
|
387
|
+
start: this.#coverageEnd,
|
|
388
|
+
length: serial32Distance(this.#coverageEnd, start),
|
|
389
|
+
span: true
|
|
390
|
+
};
|
|
391
|
+
span = { block: spanBlock, placed: this.#claim(spanBlock) };
|
|
392
|
+
this.#blocks.push(spanBlock);
|
|
393
|
+
}
|
|
394
|
+
const block = { start, length, span: false };
|
|
395
|
+
const placed = this.#claim(block);
|
|
396
|
+
this.#blocks.push(block);
|
|
397
|
+
this.#coverageEnd = isoBlockEnd(start, length);
|
|
398
|
+
this.retireBehind(start);
|
|
399
|
+
return span === void 0 ? { status: "placed", block, placed } : { status: "placed", block, placed, span };
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Retire every block and span whose last SS_ID, and every held source,
|
|
403
|
+
* lies more than `retentionBlocks` blocks behind `position`, with the shared
|
|
404
|
+
* lower-bound metric (`isoBlocksBehindLowerBound`) every ISO interleave
|
|
405
|
+
* horizon uses. Returns the retired block starts.
|
|
406
|
+
*/
|
|
407
|
+
retireBehind(position) {
|
|
408
|
+
this.observe(position);
|
|
409
|
+
const horizon = this.#retentionBlocks;
|
|
410
|
+
const retired = [];
|
|
411
|
+
for (let i = this.#blocks.length - 1; i >= 0; i--) {
|
|
412
|
+
const block = this.#blocks[i];
|
|
413
|
+
const last = serial32Add(block.start, block.length - 1);
|
|
414
|
+
if (isoBlocksBehindLowerBound(last, position, this.#kMax) > horizon) {
|
|
415
|
+
retired.push(block.start);
|
|
416
|
+
this.#blocks.splice(i, 1);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
for (const ssId of [...this.#held.keys()]) {
|
|
420
|
+
if (isoBlocksBehindLowerBound(ssId, position, this.#kMax) > horizon) this.#held.delete(ssId);
|
|
421
|
+
}
|
|
422
|
+
return retired;
|
|
423
|
+
}
|
|
424
|
+
/** Forget everything (publisher restart, track reset). */
|
|
425
|
+
reset() {
|
|
426
|
+
this.#blocks.length = 0;
|
|
427
|
+
this.#held.clear();
|
|
428
|
+
this.#coverageEnd = void 0;
|
|
429
|
+
this.#newest = void 0;
|
|
430
|
+
}
|
|
431
|
+
#claim(block) {
|
|
432
|
+
const placed = [];
|
|
433
|
+
for (const [ssId, payload] of this.#held) {
|
|
434
|
+
if (!isoBlockContains(block.start, block.length, ssId)) continue;
|
|
435
|
+
placed.push({ ssId, esi: serial32Distance(block.start, ssId), payload });
|
|
436
|
+
this.#held.delete(ssId);
|
|
437
|
+
}
|
|
438
|
+
placed.sort((x, y) => x.esi - y.esi);
|
|
439
|
+
return placed;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// src/fec-state.ts
|
|
444
|
+
var MIN_INTERLEAVE_DEPTH_MS = 33;
|
|
445
|
+
var FEC_INACTIVE_REASONS = Object.freeze([
|
|
446
|
+
"no-catalog-fec",
|
|
447
|
+
"reliable-moq",
|
|
448
|
+
"no-multicast-bridge"
|
|
449
|
+
]);
|
|
450
|
+
var INACTIVE = Object.freeze({
|
|
451
|
+
"no-catalog-fec": Object.freeze({ state: "inactive", reason: "no-catalog-fec" }),
|
|
452
|
+
"reliable-moq": Object.freeze({ state: "inactive", reason: "reliable-moq" }),
|
|
453
|
+
"no-multicast-bridge": Object.freeze({ state: "inactive", reason: "no-multicast-bridge" })
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// src/block-policy.ts
|
|
457
|
+
import { deriveFecInterleaveDepth } from "@blockcast/mmt-fec";
|
|
458
|
+
function keyframeIntervalMediaTicks(geometry, mediaTimescale) {
|
|
459
|
+
if (geometry.trackKind !== "video" || geometry.mode !== "classifying") return null;
|
|
460
|
+
if (!Number.isSafeInteger(mediaTimescale) || mediaTimescale <= 0) return null;
|
|
461
|
+
const { ticks, timescale } = geometry.keyframeInterval;
|
|
462
|
+
if (!Number.isSafeInteger(ticks) || !Number.isSafeInteger(timescale)) return null;
|
|
463
|
+
const scaled = BigInt(ticks) * BigInt(mediaTimescale);
|
|
464
|
+
const divisor = BigInt(timescale);
|
|
465
|
+
if (scaled % divisor !== 0n) return null;
|
|
466
|
+
return scaled / divisor;
|
|
467
|
+
}
|
|
468
|
+
function compareDataUnitStarts(a, b) {
|
|
469
|
+
return a.movieFragment - b.movieFragment || a.idx - b.idx || a.offset - b.offset;
|
|
470
|
+
}
|
|
471
|
+
var BlockClassifier = class {
|
|
472
|
+
#geometry;
|
|
473
|
+
#mode;
|
|
474
|
+
#reason;
|
|
475
|
+
/**
|
|
476
|
+
* False once the stream showed that an MPU boundary does not bound its
|
|
477
|
+
* RAPs (a RAP inside an MPU, or an MPU longer than a GOP). Recorded in
|
|
478
|
+
* every mode: the stall/conceal decision relies on it even when the
|
|
479
|
+
* catalog geometry already runs the classifier `all-protected`.
|
|
480
|
+
*/
|
|
481
|
+
#mpuBoundsRaps = true;
|
|
482
|
+
#blocks = /* @__PURE__ */ new Map();
|
|
483
|
+
/**
|
|
484
|
+
* P1 ledger, kept for the MPUs within the classification horizon (the
|
|
485
|
+
* highest MPU seen and the one before it): the SBN span of every source
|
|
486
|
+
* packet of each MPU, and each MPU's data-unit starts in authored order.
|
|
487
|
+
* Valid only within one continuous SBN sequence; a source discontinuity
|
|
488
|
+
* clears it (`noteSourceDiscontinuity`).
|
|
489
|
+
*/
|
|
490
|
+
#mpuSbnSpans = /* @__PURE__ */ new Map();
|
|
491
|
+
#dataUnitStarts = /* @__PURE__ */ new Map();
|
|
492
|
+
#highestMpu = null;
|
|
493
|
+
/** MPU → movie_fragment_sequence_number observed in it. */
|
|
494
|
+
#mpuFragments = /* @__PURE__ */ new Map();
|
|
495
|
+
#lastRap = null;
|
|
496
|
+
constructor(geometry) {
|
|
497
|
+
this.#geometry = geometry;
|
|
498
|
+
this.#mode = "classifying";
|
|
499
|
+
this.#reason = null;
|
|
500
|
+
this.reset(geometry);
|
|
501
|
+
}
|
|
502
|
+
get geometry() {
|
|
503
|
+
return this.#geometry;
|
|
504
|
+
}
|
|
505
|
+
get mode() {
|
|
506
|
+
return this.#mode;
|
|
507
|
+
}
|
|
508
|
+
get reason() {
|
|
509
|
+
return this.#reason;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Whether "same MPU on both sides of a gap" still proves the gap held no
|
|
513
|
+
* RAP. It stops holding for the rest of the epoch once a RAP was received
|
|
514
|
+
* inside an MPU or an MPU ran longer than a GOP.
|
|
515
|
+
*/
|
|
516
|
+
get mpuBoundsRaps() {
|
|
517
|
+
return this.#mpuBoundsRaps;
|
|
518
|
+
}
|
|
519
|
+
/** Begin a fresh epoch (catalog change or track reset). */
|
|
520
|
+
reset(geometry = this.#geometry) {
|
|
521
|
+
this.#geometry = geometry;
|
|
522
|
+
this.#blocks.clear();
|
|
523
|
+
this.#clearContiguityLedger();
|
|
524
|
+
this.#mpuFragments.clear();
|
|
525
|
+
this.#lastRap = null;
|
|
526
|
+
this.#mpuBoundsRaps = true;
|
|
527
|
+
if (geometry.trackKind === "video" && geometry.mode === "all-protected") {
|
|
528
|
+
this.#mode = "all-protected";
|
|
529
|
+
this.#reason = geometry.reason;
|
|
530
|
+
} else {
|
|
531
|
+
this.#mode = "classifying";
|
|
532
|
+
this.#reason = null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Retire this classifier (its stream moves to a new catalog epoch). Every
|
|
537
|
+
* block it had classified `delta` loses the evidence behind that class and
|
|
538
|
+
* reads `unknown` from now on; the returned changes say so.
|
|
539
|
+
*/
|
|
540
|
+
retire() {
|
|
541
|
+
const changes = [];
|
|
542
|
+
for (const [sbn, block] of this.#blocks) {
|
|
543
|
+
if (block.cls !== "delta") continue;
|
|
544
|
+
block.cls = "unknown";
|
|
545
|
+
changes.push({
|
|
546
|
+
kind: "reclassified",
|
|
547
|
+
sbn,
|
|
548
|
+
from: "delta",
|
|
549
|
+
to: "unknown",
|
|
550
|
+
evidence: "classifier-disabled"
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
return changes;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* The stream's SBN sequence restarted (FECManager source discontinuity:
|
|
557
|
+
* a publisher restart, or the SS_ID space wrapped). Authored order and SBN
|
|
558
|
+
* order are comparable only within one continuous sequence, so the P1
|
|
559
|
+
* ledger starts again.
|
|
560
|
+
*/
|
|
561
|
+
noteSourceDiscontinuity() {
|
|
562
|
+
this.#clearContiguityLedger();
|
|
563
|
+
}
|
|
564
|
+
/** Register a block that exists without evidence (a vanished block). */
|
|
565
|
+
touch(sbn) {
|
|
566
|
+
this.#blockFor(sbn);
|
|
567
|
+
}
|
|
568
|
+
/** Forget a resolved block. */
|
|
569
|
+
forget(sbn) {
|
|
570
|
+
this.#blocks.delete(sbn);
|
|
571
|
+
}
|
|
572
|
+
classOf(sbn) {
|
|
573
|
+
if (this.#geometry.trackKind === "audio") return "audio";
|
|
574
|
+
return this.#blocks.get(sbn)?.cls ?? "unknown";
|
|
575
|
+
}
|
|
576
|
+
/** Record one first-arrival source symbol's evidence for block `sbn`. */
|
|
577
|
+
observe(sbn, evidence) {
|
|
578
|
+
if (this.#geometry.trackKind === "audio") return [];
|
|
579
|
+
const changes = [];
|
|
580
|
+
const block = this.#blockFor(sbn);
|
|
581
|
+
if (evidence.rap) block.rapSeen = true;
|
|
582
|
+
if (evidence.mpu !== void 0) block.mpus.add(evidence.mpu);
|
|
583
|
+
if (evidence.idx !== void 0) {
|
|
584
|
+
block.idxMin = block.idxMin === void 0 ? evidence.idx : Math.min(block.idxMin, evidence.idx);
|
|
585
|
+
block.idxMax = block.idxMax === void 0 ? evidence.idx : Math.max(block.idxMax, evidence.idx);
|
|
586
|
+
}
|
|
587
|
+
if (evidence.rap && evidence.idx !== void 0 && evidence.idx !== 0) {
|
|
588
|
+
this.#mpuBoundsRaps = false;
|
|
589
|
+
}
|
|
590
|
+
const violation = this.#checkInvariants(sbn, block, evidence);
|
|
591
|
+
if (violation !== null) changes.push(...this.#disable(violation));
|
|
592
|
+
const next = this.#classify(block);
|
|
593
|
+
if (next !== block.cls && this.#allowed(block.cls, next)) {
|
|
594
|
+
changes.push({
|
|
595
|
+
kind: "reclassified",
|
|
596
|
+
sbn,
|
|
597
|
+
from: block.cls,
|
|
598
|
+
to: next,
|
|
599
|
+
evidence: this.#evidenceFor(block, next)
|
|
600
|
+
});
|
|
601
|
+
block.cls = next;
|
|
602
|
+
}
|
|
603
|
+
return changes;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Record a RAP frame released in order. Released MPU-start RAPs of MPUs m
|
|
607
|
+
* and m+n must be exactly n keyframe intervals apart in the media
|
|
608
|
+
* timescale, compared as integers.
|
|
609
|
+
*
|
|
610
|
+
* Comparing only consecutive MPUs would miss exactly the case the window
|
|
611
|
+
* rule cannot survive: a short MPU whose successor's RAP was lost (that
|
|
612
|
+
* RAP-bearing block is the one a wrong G calls delta), because the next
|
|
613
|
+
* released RAP then belongs to m+2. Every MPU is one interval long, so the
|
|
614
|
+
* span to any later released RAP proves or refutes all MPUs in between.
|
|
615
|
+
*/
|
|
616
|
+
noteReleasedRap(mpu, presentationTime, mediaTimescale) {
|
|
617
|
+
if (this.#geometry.trackKind === "audio" || this.#mode === "all-protected") {
|
|
618
|
+
return [];
|
|
619
|
+
}
|
|
620
|
+
const interval = keyframeIntervalMediaTicks(this.#geometry, mediaTimescale);
|
|
621
|
+
if (interval === null) return this.#disable("keyframe-interval-not-media-integral");
|
|
622
|
+
const previous = this.#lastRap;
|
|
623
|
+
if (previous !== null && previous.mpu === mpu && previous.mediaTimescale === mediaTimescale) {
|
|
624
|
+
return [];
|
|
625
|
+
}
|
|
626
|
+
this.#lastRap = { mpu, presentationTime, mediaTimescale };
|
|
627
|
+
if (previous === null || mpu < previous.mpu || previous.mediaTimescale !== mediaTimescale) {
|
|
628
|
+
return [];
|
|
629
|
+
}
|
|
630
|
+
if (presentationTime - previous.presentationTime === interval * BigInt(mpu - previous.mpu)) {
|
|
631
|
+
return [];
|
|
632
|
+
}
|
|
633
|
+
return this.#disable("rap-spacing-mismatch");
|
|
634
|
+
}
|
|
635
|
+
#blockFor(sbn) {
|
|
636
|
+
let block = this.#blocks.get(sbn);
|
|
637
|
+
if (!block) {
|
|
638
|
+
block = { rapSeen: false, mpus: /* @__PURE__ */ new Set(), cls: "unknown" };
|
|
639
|
+
this.#blocks.set(sbn, block);
|
|
640
|
+
}
|
|
641
|
+
return block;
|
|
642
|
+
}
|
|
643
|
+
#checkInvariants(sbn, block, evidence) {
|
|
644
|
+
if (this.#mode === "all-protected" || this.#geometry.trackKind !== "video") return null;
|
|
645
|
+
const geometry = this.#geometry;
|
|
646
|
+
if (geometry.mode !== "classifying") return null;
|
|
647
|
+
if (evidence.rap && evidence.idx !== void 0 && evidence.idx !== 0) {
|
|
648
|
+
return "rap-not-mpu-start";
|
|
649
|
+
}
|
|
650
|
+
if (evidence.idx !== void 0 && evidence.idx >= geometry.gopFrames) {
|
|
651
|
+
return "mpu-longer-than-gop";
|
|
652
|
+
}
|
|
653
|
+
if (block.mpus.size === 1 && block.idxMin !== void 0 && block.idxMax !== void 0 && block.idxMax - block.idxMin >= geometry.interleaveUnits) {
|
|
654
|
+
return "block-wider-than-interleave";
|
|
655
|
+
}
|
|
656
|
+
if (evidence.mpu !== void 0 && evidence.movieFragment !== void 0) {
|
|
657
|
+
const fragment = this.#mpuFragments.get(evidence.mpu);
|
|
658
|
+
if (fragment === void 0) {
|
|
659
|
+
this.#mpuFragments.set(evidence.mpu, evidence.movieFragment);
|
|
660
|
+
for (const mpu of this.#mpuFragments.keys()) {
|
|
661
|
+
if (mpu + 1 < evidence.mpu) this.#mpuFragments.delete(mpu);
|
|
662
|
+
}
|
|
663
|
+
} else if (fragment !== evidence.movieFragment) {
|
|
664
|
+
return "mpu-multiple-fragments";
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (evidence.mpu !== void 0 && this.#contradictsAuthoredOrder(sbn, evidence.mpu, evidence)) {
|
|
668
|
+
return "block-not-contiguous";
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
#clearContiguityLedger() {
|
|
673
|
+
this.#mpuSbnSpans.clear();
|
|
674
|
+
this.#dataUnitStarts.clear();
|
|
675
|
+
this.#highestMpu = null;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* P1 check for one source packet of MPU `mpu` in block `sbn`, then record
|
|
679
|
+
* it. True when some recorded packet's authored order and SBN order
|
|
680
|
+
* disagree with this one's (see the class comment). Only positions the
|
|
681
|
+
* wire states are compared: MPU order for every packet, and data-unit
|
|
682
|
+
* order for packets that carry a DU header. A packet of one data unit
|
|
683
|
+
* that continues into the next block carries no DU header and so only
|
|
684
|
+
* takes part through its MPU, which is exactly what makes an adjacent
|
|
685
|
+
* straddle consistent.
|
|
686
|
+
*/
|
|
687
|
+
#contradictsAuthoredOrder(sbn, mpu, evidence) {
|
|
688
|
+
for (const [other, span2] of this.#mpuSbnSpans) {
|
|
689
|
+
if (other < mpu && span2.max > sbn) return true;
|
|
690
|
+
if (other > mpu && span2.min < sbn) return true;
|
|
691
|
+
}
|
|
692
|
+
const inHorizon = this.#highestMpu === null || mpu + 1 >= this.#highestMpu;
|
|
693
|
+
let insertAt = -1;
|
|
694
|
+
let starts;
|
|
695
|
+
let start;
|
|
696
|
+
if (evidence.idx !== void 0) {
|
|
697
|
+
start = {
|
|
698
|
+
movieFragment: evidence.movieFragment ?? 0,
|
|
699
|
+
idx: evidence.idx,
|
|
700
|
+
offset: evidence.offset ?? 0,
|
|
701
|
+
sbn
|
|
702
|
+
};
|
|
703
|
+
starts = this.#dataUnitStarts.get(mpu);
|
|
704
|
+
if (starts !== void 0) {
|
|
705
|
+
let lo = 0;
|
|
706
|
+
let hi = starts.length;
|
|
707
|
+
while (lo < hi) {
|
|
708
|
+
const mid = lo + hi >>> 1;
|
|
709
|
+
if (compareDataUnitStarts(starts[mid], start) < 0) lo = mid + 1;
|
|
710
|
+
else hi = mid;
|
|
711
|
+
}
|
|
712
|
+
const at = starts[lo];
|
|
713
|
+
if (at !== void 0 && compareDataUnitStarts(at, start) === 0) {
|
|
714
|
+
if (at.sbn !== sbn) return true;
|
|
715
|
+
} else {
|
|
716
|
+
const before = starts[lo - 1];
|
|
717
|
+
if (before !== void 0 && before.sbn > sbn) return true;
|
|
718
|
+
if (at !== void 0 && at.sbn < sbn) return true;
|
|
719
|
+
insertAt = lo;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (!inHorizon) return false;
|
|
724
|
+
const span = this.#mpuSbnSpans.get(mpu);
|
|
725
|
+
if (span === void 0) this.#mpuSbnSpans.set(mpu, { min: sbn, max: sbn });
|
|
726
|
+
else {
|
|
727
|
+
if (sbn < span.min) span.min = sbn;
|
|
728
|
+
if (sbn > span.max) span.max = sbn;
|
|
729
|
+
}
|
|
730
|
+
if (start !== void 0) {
|
|
731
|
+
if (starts === void 0) this.#dataUnitStarts.set(mpu, [start]);
|
|
732
|
+
else if (insertAt >= 0) starts.splice(insertAt, 0, start);
|
|
733
|
+
}
|
|
734
|
+
if (this.#highestMpu === null || mpu > this.#highestMpu) {
|
|
735
|
+
this.#highestMpu = mpu;
|
|
736
|
+
for (const other of [...this.#mpuSbnSpans.keys()]) {
|
|
737
|
+
if (other + 1 < mpu) {
|
|
738
|
+
this.#mpuSbnSpans.delete(other);
|
|
739
|
+
this.#dataUnitStarts.delete(other);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
#disable(reason) {
|
|
746
|
+
if (reason === "rap-not-mpu-start" || reason === "mpu-longer-than-gop") {
|
|
747
|
+
this.#mpuBoundsRaps = false;
|
|
748
|
+
}
|
|
749
|
+
if (this.#mode === "all-protected") return [];
|
|
750
|
+
this.#mode = "all-protected";
|
|
751
|
+
this.#reason = reason;
|
|
752
|
+
const changes = [{ kind: "mode", mode: "all-protected", reason }];
|
|
753
|
+
for (const [sbn, block] of this.#blocks) {
|
|
754
|
+
if (block.cls !== "delta") continue;
|
|
755
|
+
block.cls = "unknown";
|
|
756
|
+
changes.push({
|
|
757
|
+
kind: "reclassified",
|
|
758
|
+
sbn,
|
|
759
|
+
from: "delta",
|
|
760
|
+
to: "unknown",
|
|
761
|
+
evidence: "classifier-disabled"
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
return changes;
|
|
765
|
+
}
|
|
766
|
+
#classify(block) {
|
|
767
|
+
if (block.rapSeen || block.mpus.size >= 2) return "keyframe";
|
|
768
|
+
if (this.#mode === "all-protected") return "unknown";
|
|
769
|
+
const geometry = this.#geometry;
|
|
770
|
+
if (geometry.trackKind !== "video" || geometry.mode !== "classifying") return "unknown";
|
|
771
|
+
if (block.mpus.size === 1 && block.idxMin !== void 0 && block.idxMax !== void 0) {
|
|
772
|
+
const span = geometry.interleaveUnits - 1;
|
|
773
|
+
if (block.idxMax - span >= 1 && block.idxMin + span <= geometry.gopFrames - 1) {
|
|
774
|
+
return "delta";
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return "unknown";
|
|
778
|
+
}
|
|
779
|
+
#allowed(from, to) {
|
|
780
|
+
if (from === "keyframe") return false;
|
|
781
|
+
if (from === "unknown") return to === "delta" || to === "keyframe";
|
|
782
|
+
if (from === "delta") return to === "keyframe";
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
#evidenceFor(block, to) {
|
|
786
|
+
if (to === "delta") return "window";
|
|
787
|
+
return block.rapSeen ? "rap" : "mpu-span";
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
// src/fec-manager.ts
|
|
792
|
+
var WORKER_ERROR_LOG_CAP = 5;
|
|
793
|
+
var WORKER_ERROR_KIND_CAP = 32;
|
|
794
|
+
var WORKER_ERROR_OVERFLOW_KIND = "other";
|
|
795
|
+
var STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2;
|
|
796
|
+
function isMulticastSymbolDelivery(value) {
|
|
797
|
+
return value === "datagram" || value === "ordered-relay";
|
|
798
|
+
}
|
|
799
|
+
function newFecCounters() {
|
|
800
|
+
return {
|
|
801
|
+
sourcePackets: 0,
|
|
802
|
+
repairPackets: 0,
|
|
803
|
+
duplicateSymbols: 0,
|
|
804
|
+
blocksClean: 0,
|
|
805
|
+
blocksRecovered: 0,
|
|
806
|
+
blocksFailed: 0
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function dropOldest(collection, count) {
|
|
810
|
+
const dropped = [];
|
|
811
|
+
for (const key of collection.keys()) {
|
|
812
|
+
if (dropped.length >= count) break;
|
|
813
|
+
dropped.push(key);
|
|
814
|
+
}
|
|
815
|
+
for (const key of dropped) collection.delete(key);
|
|
816
|
+
return dropped;
|
|
817
|
+
}
|
|
818
|
+
var TerminalQueue = class {
|
|
819
|
+
#records = [];
|
|
820
|
+
#head = 0;
|
|
821
|
+
push(record) {
|
|
822
|
+
this.#records.push(record);
|
|
823
|
+
}
|
|
824
|
+
peek() {
|
|
825
|
+
return this.#records[this.#head];
|
|
826
|
+
}
|
|
827
|
+
shift() {
|
|
828
|
+
this.#head++;
|
|
829
|
+
if (this.#head * 2 >= this.#records.length) {
|
|
830
|
+
this.#records = this.#records.slice(this.#head);
|
|
831
|
+
this.#head = 0;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
var FECManager = class _FECManager {
|
|
836
|
+
constructor() {
|
|
837
|
+
// Per-track WASM RaptorQ decoders, keyed by physical stream identity.
|
|
838
|
+
this.#fecDecoders = /* @__PURE__ */ new Map();
|
|
839
|
+
/** @deprecated Use setFecDecoder(packetId, ...). Backwards-compat shim. */
|
|
840
|
+
// Per-track Worker-backed FEC decoders, keyed by physical stream identity.
|
|
841
|
+
// Worker-backed tracks use async decode — recovered frames arrive via onFrame callback,
|
|
842
|
+
// not synchronous return from feedSourceSymbol/feedRepairSymbol.
|
|
843
|
+
this.#workerClients = /* @__PURE__ */ new Map();
|
|
844
|
+
// Worker clients whose streams releasePacket ended. A Worker posts
|
|
845
|
+
// asynchronously, so a message it sent before its reset can still arrive;
|
|
846
|
+
// the stream it describes is gone, and letting it in would rebuild state
|
|
847
|
+
// (block ledgers, statistics, lifecycle events) for a retired packet ID.
|
|
848
|
+
this.#releasedWorkerClients = /* @__PURE__ */ new WeakSet();
|
|
849
|
+
// Worker setup can finish while an interleaved block is already collecting
|
|
850
|
+
// on the sync decoder. Keep that block on its original decoder so later
|
|
851
|
+
// source/repair symbols do not split across two RaptorQ states.
|
|
852
|
+
this.#syncDecoderBlockOwners = /* @__PURE__ */ new Map();
|
|
853
|
+
this.#decoderBlockOwners = /* @__PURE__ */ new WeakMap();
|
|
854
|
+
this.#fecK = 4;
|
|
855
|
+
this.#fecSymbolSize = 0;
|
|
856
|
+
this.#fecTimeoutMs = 500;
|
|
857
|
+
// default 500ms; refined by setFramerate() when fps known
|
|
858
|
+
this.#trackConfigs = /* @__PURE__ */ new Map();
|
|
859
|
+
this.#fecStats = newFecCounters();
|
|
860
|
+
this.#fecTrackStats = /* @__PURE__ */ new Map();
|
|
861
|
+
this.#fecWorkerStats = /* @__PURE__ */ new Map();
|
|
862
|
+
this.#fecStatsObservers = /* @__PURE__ */ new Set();
|
|
863
|
+
// Block inactivity tracking: (packetId, SBN) -> the one armed timer. The
|
|
864
|
+
// deadline is derived from catalog geometry and refreshed on progress; only
|
|
865
|
+
// an idle block fails. #activeBlockDeadlinesMs is the authoritative
|
|
866
|
+
// deadline; the timer re-checks it when it fires (see DeadlineTimer).
|
|
867
|
+
this.#activeBlocks = /* @__PURE__ */ new Map();
|
|
868
|
+
this.#activeBlockDeadlinesMs = /* @__PURE__ */ new Map();
|
|
869
|
+
// The two terms #activeBlockDeadlinesMs is the later of (BLO-35597):
|
|
870
|
+
// - the multicast late-symbol wait, refreshed by every accepted symbol;
|
|
871
|
+
// - a D-28 repair hold, set by the repair scheduler only while a repair
|
|
872
|
+
// FETCH it admitted can still land before the block's presentation
|
|
873
|
+
// deadline. The multicast span elapsing never ends a fetch the scheduler
|
|
874
|
+
// judged able to arrive in time.
|
|
875
|
+
this.#blockWaitDeadlinesMs = /* @__PURE__ */ new Map();
|
|
876
|
+
this.#repairHoldUntilMs = /* @__PURE__ */ new Map();
|
|
877
|
+
// Receiver-measured multicast delivery gap: the late-symbol wait's jitter
|
|
878
|
+
// term (see DeliveryGapEstimator for the statistic and why).
|
|
879
|
+
this.#deliveryGaps = new DeliveryGapEstimator();
|
|
880
|
+
this.#lifecycleTrace = [];
|
|
881
|
+
// Worker-backed timeouts enqueue cleanup behind all prior symbol messages.
|
|
882
|
+
// Failure remains pending until cleanupComplete acknowledges that queue drain.
|
|
883
|
+
this.#pendingWorkerCleanups = /* @__PURE__ */ new Set();
|
|
884
|
+
this.#pendingWorkerCleanupClients = /* @__PURE__ */ new Map();
|
|
885
|
+
// One bound per owed cleanup: the acknowledgement is awaited for at most one
|
|
886
|
+
// catalog-derived block lifetime (fail bound plus repair grace).
|
|
887
|
+
// A Worker that hangs or drops one message must not leave the block
|
|
888
|
+
// 'pending' forever, which would park every later frame of its stream in
|
|
889
|
+
// the in-order release gate. Each entry owns its handle; the callback
|
|
890
|
+
// checks it still owns the key, so a settled or reset key is never touched.
|
|
891
|
+
this.#pendingWorkerCleanupBounds = /* @__PURE__ */ new Map();
|
|
892
|
+
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
893
|
+
this.#blockSourceCounts = /* @__PURE__ */ new Map();
|
|
894
|
+
this.#blockSourceEsis = /* @__PURE__ */ new Map();
|
|
895
|
+
// Authored SSB_length per block (ISO/IEC 23008-1 §C.5.3), learned from the
|
|
896
|
+
// first repair symbol. The FFmpeg moq_mmt publisher closes a block every D
|
|
897
|
+
// media units, usually before it holds K symbols, and fec-raptorq
|
|
898
|
+
// force_complete() encodes it as a K-symbol RaptorQ block whose ESIs
|
|
899
|
+
// SSB_length..K-1 are zero padding that is never sent. Such a block is clean
|
|
900
|
+
// at SSB_length source arrivals and recoverable from SSB_length received
|
|
901
|
+
// symbols; waiting for K fails every one of them.
|
|
902
|
+
this.#blockAuthoredLengths = /* @__PURE__ */ new Map();
|
|
903
|
+
this.#ssbLengthConflictCount = 0;
|
|
904
|
+
this.#blockRepairRsIds = /* @__PURE__ */ new Map();
|
|
905
|
+
// Blocks already recovered — prevents late source symbols from restarting timeouts.
|
|
906
|
+
// WASM removes blocks after recovery; late symbols create empty blocks returning null,
|
|
907
|
+
// which would falsely start new timeouts without this guard.
|
|
908
|
+
//
|
|
909
|
+
// Every insertion goes through #markTerminal, which retires the key once its
|
|
910
|
+
// catalog-derived retention window (terminalRetentionMsForPacket) passes. The
|
|
911
|
+
// previous bound (trim to 500 at 1000) lived in cancelBlockTimeout alone, so
|
|
912
|
+
// a stream whose blocks only failed grew this set without limit (BLO-36599).
|
|
913
|
+
this.#completedBlocks = /* @__PURE__ */ new Set();
|
|
914
|
+
this.#terminalExpiryMs = /* @__PURE__ */ new Map();
|
|
915
|
+
this.#terminalQueues = /* @__PURE__ */ new Map();
|
|
916
|
+
// sbn -> (block key -> packetId) for every key held by #activeBlocks,
|
|
917
|
+
// #blockSourceCounts, #blockCatalogTrackNames or #completedBlocks. A lookup
|
|
918
|
+
// without a catalog name used to string-parse every key of all of those on
|
|
919
|
+
// each source packet, which is O(retained blocks) per packet (BLO-36599).
|
|
920
|
+
this.#blockKeysBySbn = /* @__PURE__ */ new Map();
|
|
921
|
+
// Stable per-name encodings for stream and block keys, bounded by the
|
|
922
|
+
// catalog's track names; #blockKey is built several times per packet.
|
|
923
|
+
this.#encodedTrackNames = /* @__PURE__ */ new Map();
|
|
924
|
+
// The last block and stream key built. One packet asks for the same block's
|
|
925
|
+
// key from every FECManager entry point it passes through (~6 per source
|
|
926
|
+
// symbol), so each key is built once per packet rather than once per call
|
|
927
|
+
// (BLO-36599). Both keys are pure functions of their arguments.
|
|
928
|
+
this.#lastBlockKeySbn = -1;
|
|
929
|
+
this.#lastBlockKeyPacketId = -1;
|
|
930
|
+
this.#lastBlockKeyTrackName = void 0;
|
|
931
|
+
this.#lastBlockKey = "";
|
|
932
|
+
this.#lastStreamKeyPacketId = -1;
|
|
933
|
+
this.#lastStreamKeyTrackName = void 0;
|
|
934
|
+
this.#lastStreamKey = "";
|
|
935
|
+
this.#seenSymbols = /* @__PURE__ */ new Set();
|
|
936
|
+
this.#blockPathCounts = /* @__PURE__ */ new Map();
|
|
937
|
+
// Receiver-local first-accepted-symbol time per block, bounded like
|
|
938
|
+
// #blockPathCounts (see blockAnchorAtMs).
|
|
939
|
+
this.#blockAnchorsMs = /* @__PURE__ */ new Map();
|
|
940
|
+
// Exact catalog identity observed on accepted symbols for each FEC block.
|
|
941
|
+
// Conflicting identities collapse to null so recovery fails closed.
|
|
942
|
+
this.#blockCatalogTrackNames = /* @__PURE__ */ new Map();
|
|
943
|
+
// A receiver joins an already-running live stream in the middle of D
|
|
944
|
+
// interleaved source blocks. This applies to both multicast and reliable MoQ:
|
|
945
|
+
// a MoQ group boundary is not an FEC ESI=0 boundary. Those blocks are not
|
|
946
|
+
// packet loss because their earlier ESIs predate the subscription. Ignore
|
|
947
|
+
// each such block until ESI=0 establishes a complete boundary for the track.
|
|
948
|
+
// After that boundary, a first observed ESI>0 is real delivery loss and must
|
|
949
|
+
// continue through normal FEC recovery/failure accounting.
|
|
950
|
+
this.#startupAlignedPackets = /* @__PURE__ */ new Set();
|
|
951
|
+
this.#startupPartialBlocks = /* @__PURE__ */ new Set();
|
|
952
|
+
// A live multicast join can observe an ESI=0 source boundary before the
|
|
953
|
+
// repair lane catches up. Returning false here also drops that media packet
|
|
954
|
+
// at MmtpRouter, so the readiness probe is multicast-only and bounded: an
|
|
955
|
+
// actual multicast repair unlocks the next boundary, while a second source
|
|
956
|
+
// boundary fails open. Reliable MoQ media continues through normal startup
|
|
957
|
+
// alignment and retained MoQ repair cannot prove multicast readiness.
|
|
958
|
+
this.#startupRequireRepairReady = false;
|
|
959
|
+
this.#startupRepairReadyPackets = /* @__PURE__ */ new Set();
|
|
960
|
+
this.#startupRepairBoundaryCounts = /* @__PURE__ */ new Map();
|
|
961
|
+
this.#startupRepairFailOpenPackets = /* @__PURE__ */ new Set();
|
|
962
|
+
// SBNs that reached K source-symbol arrivals via trackBlock. Used by the
|
|
963
|
+
// worker recovery suppression check (see onFrame handler below) so we only
|
|
964
|
+
// suppress `recovered=true` emits for blocks that ALSO completed cleanly
|
|
965
|
+
// via source symbols. Separating this from #completedBlocks (which is also
|
|
966
|
+
// populated by every worker frame emit, by failed timeouts, and by external
|
|
967
|
+
// callers) was the BLO-3323 L15 fix: the prior predicate caused blocksRecovered
|
|
968
|
+
// to stay at 0 because the SBN was always in #completedBlocks by the time the
|
|
969
|
+
// worker fired recovered=true.
|
|
970
|
+
this.#cleanlyCompletedBlocks = /* @__PURE__ */ new Set();
|
|
971
|
+
// D-28: blocks whose repair deadline already fired onRepairNeeded. At most
|
|
972
|
+
// one repair request per block; entries are removed when the block
|
|
973
|
+
// completes, recovers, or fails.
|
|
974
|
+
this.#repairRequestedBlocks = /* @__PURE__ */ new Set();
|
|
975
|
+
// D-28: repair deadlines refreshed by accepted source progress until the
|
|
976
|
+
// first repair request. A `null` value marks a deadline that already fired
|
|
977
|
+
// and requested repair (never re-armed). At most K accepted source symbols
|
|
978
|
+
// can refresh the horizon, so browser scheduling delay cannot create an
|
|
979
|
+
// early loss signal and repair traffic cannot keep a block alive
|
|
980
|
+
// indefinitely. #repairDeadlineAtMs is the authoritative deadline; the one
|
|
981
|
+
// armed timer re-checks it when it fires (see DeadlineTimer).
|
|
982
|
+
this.#repairDeadlines = /* @__PURE__ */ new Map();
|
|
983
|
+
this.#repairDeadlineAtMs = /* @__PURE__ */ new Map();
|
|
984
|
+
// Worker recovery emits all K source symbols. Track recovery once per block
|
|
985
|
+
// while routing only ESI slots that were absent from the direct source path.
|
|
986
|
+
this.#workerRecoveredBlocks = /* @__PURE__ */ new Set();
|
|
987
|
+
// How the multicast leg delivers symbols (see MulticastSymbolDelivery). The
|
|
988
|
+
// provider declares it per transport join; absence keeps datagram semantics.
|
|
989
|
+
this.#multicastSymbolDelivery = "datagram";
|
|
990
|
+
// ordered-relay: per stream, the highest SBN any multicast symbol carried.
|
|
991
|
+
// The relay preserves the receiving socket's order, so every block more than
|
|
992
|
+
// the decoder's interleave horizon behind this frontier has received every
|
|
993
|
+
// symbol it will ever receive over multicast.
|
|
994
|
+
this.#orderedFrontiers = /* @__PURE__ */ new Map();
|
|
995
|
+
// ordered-relay: blocks with multicast symbols that are neither terminal nor
|
|
996
|
+
// handed to a decoder cleanup, per stream, keyed by SBN. They carry no
|
|
997
|
+
// timer; the frontier settles them. At most one interleave horizon of blocks
|
|
998
|
+
// per stream is ever here.
|
|
999
|
+
this.#orderedPendingBlocks = /* @__PURE__ */ new Map();
|
|
1000
|
+
// ── Keyframe-protected repair policy (block-policy.ts) ──
|
|
1001
|
+
// One classifier per physical stream, built from that stream's geometry.
|
|
1002
|
+
this.#classifiers = /* @__PURE__ */ new Map();
|
|
1003
|
+
// Geometry a stream-scoped configuration gave one stream (streamKey). ABR
|
|
1004
|
+
// rungs can share a packetId with a different framerate or GOP, so the
|
|
1005
|
+
// packet-level geometry is only the geometry of a packet whose streams
|
|
1006
|
+
// were never configured apart (see #geometryFor).
|
|
1007
|
+
this.#streamGeometries = /* @__PURE__ */ new Map();
|
|
1008
|
+
// Terminal outcomes retained for in-order frame release: a failed block is
|
|
1009
|
+
// resolved-with-loss, a completed one resolved-clean.
|
|
1010
|
+
this.#failedBlocks = /* @__PURE__ */ new Set();
|
|
1011
|
+
// A block's class at resolution, so events emitted after the classifier
|
|
1012
|
+
// forgot the block (the router counts a recovery after cancelling the
|
|
1013
|
+
// block) still carry it.
|
|
1014
|
+
this.#resolvedBlockClasses = /* @__PURE__ */ new Map();
|
|
1015
|
+
// Blocks already reported through onBlockRepaired (once per block).
|
|
1016
|
+
this.#repairedBlocks = /* @__PURE__ */ new Set();
|
|
1017
|
+
// Blocks created without any symbol because every symbol vanished.
|
|
1018
|
+
this.#materializedBlocks = /* @__PURE__ */ new Set();
|
|
1019
|
+
// packetIds whose ordered-relay horizon became underivable and fell back to
|
|
1020
|
+
// the datagram deadline. Bounds the warn in #derivableOrderedHorizon to once
|
|
1021
|
+
// per stream. Not only throws: the ordinary out-of-range case is detected by
|
|
1022
|
+
// arithmetic and never throws at all.
|
|
1023
|
+
this.#orderedHorizonWarnedFor = /* @__PURE__ */ new Set();
|
|
1024
|
+
// Highest SBN seen per stream and when, for vanished-block detection.
|
|
1025
|
+
this.#highestSbn = /* @__PURE__ */ new Map();
|
|
1026
|
+
// iso-ssbg1-v1 block location per stream (streamKey). A source belongs to a
|
|
1027
|
+
// block only once a repair symbol stated the block's (SS_Start, SSB_length).
|
|
1028
|
+
this.#isoLocators = /* @__PURE__ */ new Map();
|
|
1029
|
+
this.#isoRepairRefusedCount = 0;
|
|
1030
|
+
this.#isoSourceOversizeCount = 0;
|
|
1031
|
+
// Held sources a locator's capacity cap evicted (IsoBlockLocator.heldCapacity):
|
|
1032
|
+
// only a stream whose SS_IDs are not consecutive reaches it. Cumulative,
|
|
1033
|
+
// unlike the locators, which a reset discards.
|
|
1034
|
+
this.#isoHeldEvictedCount = 0;
|
|
1035
|
+
// Sources a synchronous decoder held unplaced and retired at its horizon
|
|
1036
|
+
// (WasmFecDecoder.retire_unplaced): placed sources whose block's repair
|
|
1037
|
+
// never reached that decoder. Cumulative.
|
|
1038
|
+
this.#isoDecoderUnplacedRetired = 0;
|
|
1039
|
+
// Failed retire_unplaced calls. The sweep runs on every feed, so its warn is
|
|
1040
|
+
// rate-limited like the other per-symbol warns here. Cumulative.
|
|
1041
|
+
this.#isoDecoderUnplacedRetireFailed = 0;
|
|
1042
|
+
// The latest source symbol per stream that carried its MMTP
|
|
1043
|
+
// packet_sequence_number: the anchor that bounds how many blocks the
|
|
1044
|
+
// producer can have started before a later source symbol.
|
|
1045
|
+
this.#sourceAnchors = /* @__PURE__ */ new Map();
|
|
1046
|
+
// Catalog/reset epoch per packetId. Gap decisions carry it for fencing.
|
|
1047
|
+
this.#packetEpochs = /* @__PURE__ */ new Map();
|
|
1048
|
+
// Framerate detection state
|
|
1049
|
+
this.#fpsDetected = false;
|
|
1050
|
+
this.#fecInterleaveDepthMs = 0;
|
|
1051
|
+
// Worker error log rate-limiter — bounds console.warn output and exported
|
|
1052
|
+
// error cardinality when the FEC Worker emits the same failure repeatedly
|
|
1053
|
+
// with volatile packet details (e.g. ssStart) in the message text.
|
|
1054
|
+
// Stable Worker codes aggregate those messages; uncoded/legacy messages are
|
|
1055
|
+
// capped into an `other` bucket. This also covers wasm-bindgen
|
|
1056
|
+
// "recursive use of an object" bubbled from add_source/add_repair during
|
|
1057
|
+
// overlapping message processing). Without this, Point 5 of the 7-point
|
|
1058
|
+
// validation harness — which caps headless warnings at 500 — fails even
|
|
1059
|
+
// though the errors are duplicates of a single underlying event.
|
|
1060
|
+
this.#workerErrorCounts = /* @__PURE__ */ new Map();
|
|
1061
|
+
this.#decoderUnavailableEvents = 0;
|
|
1062
|
+
this.#repairSymbolSizeMismatchCount = 0;
|
|
1063
|
+
// Event callbacks — block lifecycle notifications for MmtpRouter wiring.
|
|
1064
|
+
// NOTE: For sync WASM path, recovery routing is handled by MmtpRouter from
|
|
1065
|
+
// feedSourceSymbol/feedRepairSymbol return values. For Worker path, recovery
|
|
1066
|
+
// arrives via onBlockRecovered callback.
|
|
1067
|
+
this.onBlockStarted = null;
|
|
1068
|
+
this.onBlockComplete = null;
|
|
1069
|
+
/** A block reached its fail bound. `blockClass` is its class at failure. */
|
|
1070
|
+
this.onBlockFailed = null;
|
|
1071
|
+
/**
|
|
1072
|
+
* Fired once per stream when its Worker decoder stops being routable.
|
|
1073
|
+
* `reason` is 'cleared' when clearFecWorker() removed it and 'disposed' when
|
|
1074
|
+
* a symbol or reset found the registered client already disposed and
|
|
1075
|
+
* evicted it. `fallbackDecoder` is 'sync' when a synchronous WASM decoder
|
|
1076
|
+
* registered for the same stream keeps decoding its symbols, and 'none'
|
|
1077
|
+
* when nothing does. Only 'none' leaves the stream unprotected: the host
|
|
1078
|
+
* must then register a decoder again or leave the stream, because counting
|
|
1079
|
+
* its symbols as handled would hide the loss.
|
|
1080
|
+
*/
|
|
1081
|
+
this.onDecoderUnavailable = null;
|
|
1082
|
+
// D-28 on-demand repair callbacks. When onRepairNeeded is registered, each
|
|
1083
|
+
// block arms a one-shot repair deadline after the derived K-symbol source
|
|
1084
|
+
// horizon; a block still short of K when it fires requests a per-SBN MoQ
|
|
1085
|
+
// repair fetch upstream. The inactivity fail bound is unchanged either way —
|
|
1086
|
+
// repair and failure are orthogonal timers, so wiring D-28 never delays
|
|
1087
|
+
// failure accounting. onRepairResolved fires when a repair-requested block
|
|
1088
|
+
// leaves the active set, with the outcome ("completed" after clean completion
|
|
1089
|
+
// or recovery, "failed" at the fail bound) so the upstream in-flight slot
|
|
1090
|
+
// (MulticastManager.clearRepairForBlock) can be released and logged.
|
|
1091
|
+
this.onRepairNeeded = null;
|
|
1092
|
+
this.onRepairResolved = null;
|
|
1093
|
+
/**
|
|
1094
|
+
* A block that could not complete from its first-arrival source symbols
|
|
1095
|
+
* alone was completed anyway: by FEC decode (in-band or QUIC repair
|
|
1096
|
+
* symbols) or, after a D-28 request, by late symbols. Fired once per block.
|
|
1097
|
+
*/
|
|
1098
|
+
this.onBlockRepaired = null;
|
|
1099
|
+
/** A block's class changed on new evidence or when classification was disabled. */
|
|
1100
|
+
this.onBlockReclassified = null;
|
|
1101
|
+
/** A video stream's classifier mode was established or changed. */
|
|
1102
|
+
this.onClassifierMode = null;
|
|
1103
|
+
/** A block every symbol of which vanished was created by SBN evidence. */
|
|
1104
|
+
this.onBlockVanished = null;
|
|
1105
|
+
/** A block reached a terminal state (completed, recovered, or failed). */
|
|
1106
|
+
this.onBlockResolved = null;
|
|
1107
|
+
/** A stream's SBN went backwards (or jumped implausibly): the source restarted. */
|
|
1108
|
+
this.onSourceDiscontinuity = null;
|
|
1109
|
+
// Worker-path callbacks — async recovery and stats from FecWorkerClient.
|
|
1110
|
+
this.onBlockRecovered = null;
|
|
1111
|
+
this.onFecStatsUpdate = null;
|
|
1112
|
+
}
|
|
1113
|
+
#fecDecoders;
|
|
1114
|
+
#workerClients;
|
|
1115
|
+
#releasedWorkerClients;
|
|
1116
|
+
#syncDecoderBlockOwners;
|
|
1117
|
+
#decoderBlockOwners;
|
|
1118
|
+
#fecK;
|
|
1119
|
+
#fecSymbolSize;
|
|
1120
|
+
#fecTimeoutMs;
|
|
1121
|
+
#trackConfigs;
|
|
1122
|
+
#fecStats;
|
|
1123
|
+
#fecTrackStats;
|
|
1124
|
+
#fecWorkerStats;
|
|
1125
|
+
#fecStatsObservers;
|
|
1126
|
+
#activeBlocks;
|
|
1127
|
+
#activeBlockDeadlinesMs;
|
|
1128
|
+
#blockWaitDeadlinesMs;
|
|
1129
|
+
#repairHoldUntilMs;
|
|
1130
|
+
#deliveryGaps;
|
|
1131
|
+
#lifecycleTrace;
|
|
1132
|
+
#pendingWorkerCleanups;
|
|
1133
|
+
#pendingWorkerCleanupClients;
|
|
1134
|
+
#pendingWorkerCleanupBounds;
|
|
1135
|
+
#blockSourceCounts;
|
|
1136
|
+
#blockSourceEsis;
|
|
1137
|
+
#blockAuthoredLengths;
|
|
1138
|
+
#ssbLengthConflictCount;
|
|
1139
|
+
#blockRepairRsIds;
|
|
1140
|
+
#completedBlocks;
|
|
1141
|
+
#terminalExpiryMs;
|
|
1142
|
+
#terminalQueues;
|
|
1143
|
+
#blockKeysBySbn;
|
|
1144
|
+
#encodedTrackNames;
|
|
1145
|
+
#lastBlockKeySbn;
|
|
1146
|
+
#lastBlockKeyPacketId;
|
|
1147
|
+
#lastBlockKeyTrackName;
|
|
1148
|
+
#lastBlockKey;
|
|
1149
|
+
#lastStreamKeyPacketId;
|
|
1150
|
+
#lastStreamKeyTrackName;
|
|
1151
|
+
#lastStreamKey;
|
|
1152
|
+
#seenSymbols;
|
|
1153
|
+
#blockPathCounts;
|
|
1154
|
+
#blockAnchorsMs;
|
|
1155
|
+
#blockCatalogTrackNames;
|
|
1156
|
+
#startupAlignedPackets;
|
|
1157
|
+
#startupPartialBlocks;
|
|
1158
|
+
#startupRequireRepairReady;
|
|
1159
|
+
#startupRepairReadyPackets;
|
|
1160
|
+
#startupRepairBoundaryCounts;
|
|
1161
|
+
#startupRepairFailOpenPackets;
|
|
1162
|
+
#cleanlyCompletedBlocks;
|
|
1163
|
+
#repairRequestedBlocks;
|
|
1164
|
+
#repairDeadlines;
|
|
1165
|
+
#repairDeadlineAtMs;
|
|
1166
|
+
#workerRecoveredBlocks;
|
|
1167
|
+
#multicastSymbolDelivery;
|
|
1168
|
+
#orderedFrontiers;
|
|
1169
|
+
#orderedPendingBlocks;
|
|
1170
|
+
#classifiers;
|
|
1171
|
+
#streamGeometries;
|
|
1172
|
+
#failedBlocks;
|
|
1173
|
+
#resolvedBlockClasses;
|
|
1174
|
+
#repairedBlocks;
|
|
1175
|
+
#materializedBlocks;
|
|
1176
|
+
#orderedHorizonWarnedFor;
|
|
1177
|
+
#highestSbn;
|
|
1178
|
+
#isoLocators;
|
|
1179
|
+
#isoRepairRefusedCount;
|
|
1180
|
+
#isoSourceOversizeCount;
|
|
1181
|
+
#isoHeldEvictedCount;
|
|
1182
|
+
#isoDecoderUnplacedRetired;
|
|
1183
|
+
#isoDecoderUnplacedRetireFailed;
|
|
1184
|
+
#sourceAnchors;
|
|
1185
|
+
#packetEpochs;
|
|
1186
|
+
#fpsDetected;
|
|
1187
|
+
#fecFps;
|
|
1188
|
+
#fecInterleaveDepthMs;
|
|
1189
|
+
#workerErrorCounts;
|
|
1190
|
+
#decoderUnavailableEvents;
|
|
1191
|
+
#repairSymbolSizeMismatchCount;
|
|
1192
|
+
#encodeTrackName(catalogTrackName) {
|
|
1193
|
+
let encoded = this.#encodedTrackNames.get(catalogTrackName);
|
|
1194
|
+
if (encoded === void 0) {
|
|
1195
|
+
encoded = encodeURIComponent(catalogTrackName);
|
|
1196
|
+
this.#encodedTrackNames.set(catalogTrackName, encoded);
|
|
1197
|
+
}
|
|
1198
|
+
return encoded;
|
|
1199
|
+
}
|
|
1200
|
+
#streamKey(packetId, catalogTrackName) {
|
|
1201
|
+
if (packetId === this.#lastStreamKeyPacketId && catalogTrackName === this.#lastStreamKeyTrackName) {
|
|
1202
|
+
return this.#lastStreamKey;
|
|
1203
|
+
}
|
|
1204
|
+
const key = catalogTrackName === void 0 ? `${packetId}` : `${packetId}:${this.#encodeTrackName(catalogTrackName)}`;
|
|
1205
|
+
this.#lastStreamKeyPacketId = packetId;
|
|
1206
|
+
this.#lastStreamKeyTrackName = catalogTrackName;
|
|
1207
|
+
this.#lastStreamKey = key;
|
|
1208
|
+
return key;
|
|
1209
|
+
}
|
|
1210
|
+
#belongsToPacket(key, packetId) {
|
|
1211
|
+
return Number(key.split(":", 1)[0]) === packetId;
|
|
1212
|
+
}
|
|
1213
|
+
#belongsToStream(key, packetId, catalogTrackName) {
|
|
1214
|
+
if (!this.#belongsToPacket(key, packetId)) return false;
|
|
1215
|
+
const parts = key.split(":");
|
|
1216
|
+
const encodedTrackName = this.#encodeTrackName(catalogTrackName);
|
|
1217
|
+
if (parts.length === 2) return parts[1] === encodedTrackName;
|
|
1218
|
+
if (parts.length >= 3) return parts[2] === encodedTrackName;
|
|
1219
|
+
return false;
|
|
1220
|
+
}
|
|
1221
|
+
#packetIdsForStreamKeys(keys) {
|
|
1222
|
+
return [...new Set([...keys].map((key) => Number(key.split(":", 1)[0])))];
|
|
1223
|
+
}
|
|
1224
|
+
#parseStreamKey(key) {
|
|
1225
|
+
const separator = key.indexOf(":");
|
|
1226
|
+
if (separator < 0) return { packetId: Number(key) || 0 };
|
|
1227
|
+
return {
|
|
1228
|
+
packetId: Number(key.slice(0, separator)) || 0,
|
|
1229
|
+
catalogTrackName: decodeURIComponent(key.slice(separator + 1))
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
#blockKey(sbn, packetId, catalogTrackName) {
|
|
1233
|
+
if (sbn === this.#lastBlockKeySbn && packetId === this.#lastBlockKeyPacketId && catalogTrackName === this.#lastBlockKeyTrackName) {
|
|
1234
|
+
return this.#lastBlockKey;
|
|
1235
|
+
}
|
|
1236
|
+
const base = `${packetId}:${sbn}`;
|
|
1237
|
+
const key = catalogTrackName === void 0 ? base : `${base}:${this.#encodeTrackName(catalogTrackName)}`;
|
|
1238
|
+
this.#lastBlockKeySbn = sbn;
|
|
1239
|
+
this.#lastBlockKeyPacketId = packetId;
|
|
1240
|
+
this.#lastBlockKeyTrackName = catalogTrackName;
|
|
1241
|
+
this.#lastBlockKey = key;
|
|
1242
|
+
return key;
|
|
1243
|
+
}
|
|
1244
|
+
/** Record that `key` (block `sbn` on `packetId`) is held by a lookup set. */
|
|
1245
|
+
#indexBlockKey(key, sbn, packetId) {
|
|
1246
|
+
let keys = this.#blockKeysBySbn.get(sbn);
|
|
1247
|
+
if (!keys) {
|
|
1248
|
+
keys = /* @__PURE__ */ new Map();
|
|
1249
|
+
this.#blockKeysBySbn.set(sbn, keys);
|
|
1250
|
+
}
|
|
1251
|
+
keys.set(key, packetId);
|
|
1252
|
+
}
|
|
1253
|
+
/** True while any set a catalog-name-less lookup consults still holds `key`. */
|
|
1254
|
+
#isBlockKeyHeld(key) {
|
|
1255
|
+
return this.#activeBlocks.has(key) || this.#blockSourceCounts.has(key) || this.#blockCatalogTrackNames.has(key) || this.#completedBlocks.has(key);
|
|
1256
|
+
}
|
|
1257
|
+
/** Drop `key` from the SBN index once no lookup set holds it any more. */
|
|
1258
|
+
#unindexBlockKey(key) {
|
|
1259
|
+
if (this.#isBlockKeyHeld(key)) return;
|
|
1260
|
+
const sbn = this.#parseBlockKey(key).sbn;
|
|
1261
|
+
const keys = this.#blockKeysBySbn.get(sbn);
|
|
1262
|
+
if (!keys) return;
|
|
1263
|
+
keys.delete(key);
|
|
1264
|
+
if (keys.size === 0) this.#blockKeysBySbn.delete(sbn);
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Make `key` terminal: late symbols for it are duplicates from now on. The
|
|
1268
|
+
* key is retired terminalRetentionMsForPacket later, when no copy of any of
|
|
1269
|
+
* its symbols can still be in flight on either transport and no frame can
|
|
1270
|
+
* still be held behind it. Retirement is swept lazily here, on the stream's
|
|
1271
|
+
* own terminal events, so it needs no timer.
|
|
1272
|
+
*/
|
|
1273
|
+
#markTerminal(key, sbn, packetId, clean) {
|
|
1274
|
+
const nowMs = performance.now();
|
|
1275
|
+
this.#retireTerminalBlocks(packetId, nowMs);
|
|
1276
|
+
this.#forgetOrderedPending(key);
|
|
1277
|
+
this.#completedBlocks.add(key);
|
|
1278
|
+
if (clean) this.#cleanlyCompletedBlocks.add(key);
|
|
1279
|
+
this.#indexBlockKey(key, sbn, packetId);
|
|
1280
|
+
const expiresAtMs = nowMs + this.terminalRetentionMsForPacket(packetId);
|
|
1281
|
+
this.#terminalExpiryMs.set(key, expiresAtMs);
|
|
1282
|
+
let queue = this.#terminalQueues.get(packetId);
|
|
1283
|
+
if (!queue) {
|
|
1284
|
+
queue = new TerminalQueue();
|
|
1285
|
+
this.#terminalQueues.set(packetId, queue);
|
|
1286
|
+
}
|
|
1287
|
+
queue.push({ key, expiresAtMs });
|
|
1288
|
+
}
|
|
1289
|
+
#retireTerminalBlocks(packetId, nowMs) {
|
|
1290
|
+
const queue = this.#terminalQueues.get(packetId);
|
|
1291
|
+
if (!queue) return;
|
|
1292
|
+
for (let record = queue.peek(); record !== void 0; record = queue.peek()) {
|
|
1293
|
+
if (record.expiresAtMs > nowMs) return;
|
|
1294
|
+
const current = this.#terminalExpiryMs.get(record.key) === record.expiresAtMs;
|
|
1295
|
+
if (current && this.#withinOrderedHorizon(record.key)) return;
|
|
1296
|
+
queue.shift();
|
|
1297
|
+
if (!current) continue;
|
|
1298
|
+
this.#terminalExpiryMs.delete(record.key);
|
|
1299
|
+
this.#completedBlocks.delete(record.key);
|
|
1300
|
+
this.#cleanlyCompletedBlocks.delete(record.key);
|
|
1301
|
+
this.#workerRecoveredBlocks.delete(record.key);
|
|
1302
|
+
this.#failedBlocks.delete(record.key);
|
|
1303
|
+
this.#resolvedBlockClasses.delete(record.key);
|
|
1304
|
+
this.#repairedBlocks.delete(record.key);
|
|
1305
|
+
this.#unindexBlockKey(record.key);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
// -----------------------------------------------------------------------
|
|
1309
|
+
// Ordered-relay settlement (MulticastSymbolDelivery 'ordered-relay')
|
|
1310
|
+
// -----------------------------------------------------------------------
|
|
1311
|
+
/**
|
|
1312
|
+
* The decoder's interleave horizon for one stream, in blocks: the same
|
|
1313
|
+
* retention the Worker and the WASM decoder are configured with, because it
|
|
1314
|
+
* comes from the same function they do (`deriveFecRetentionBlocks`, over the
|
|
1315
|
+
* block's delivery window and its span). The decoder retires every block
|
|
1316
|
+
* more than this many SBNs behind the newest one it saw (`cleanup_by_sbn`),
|
|
1317
|
+
* so settling earlier than this would decide a block the decoder would still
|
|
1318
|
+
* have recovered.
|
|
1319
|
+
*
|
|
1320
|
+
* This used to call `deriveFecInterleaveDepth`, which returns D -- MEDIA
|
|
1321
|
+
* UNITS per block, not a block count -- and a comment here asserted the two
|
|
1322
|
+
* matched. They did, only while the decoder's retention was also (wrongly)
|
|
1323
|
+
* sized in D. Once BLO-35597 sized retention as a real block count this
|
|
1324
|
+
* settled 2-5 blocks early on the live ISS catalog at reorderToleranceMs=100
|
|
1325
|
+
* (720p 1 vs 6, audio 2 vs 5), making the manager the binding constraint and
|
|
1326
|
+
* the decoder fix inert on ordered relays. Both sides now consume one
|
|
1327
|
+
* derivation; do not re-derive a horizon at either call site.
|
|
1328
|
+
*/
|
|
1329
|
+
#orderedHorizonBlocksFor(packetId, config) {
|
|
1330
|
+
if (!_FECManager.#hasOrderedGeometry(config)) {
|
|
1331
|
+
throw new Error(
|
|
1332
|
+
`[FECManager] packetId=${packetId}: ordered-relay settlement derives the decoder's interleave horizon from the catalog interleaveDepthMs (>= ${MIN_INTERLEAVE_DEPTH_MS}ms), the media-unit rate and the block delivery window; got interleaveDepthMs=${config.interleaveDepthMs} fps=${String(config.fps)} timeoutMs=${config.timeoutMs}.`
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
return deriveFecRetentionBlocks(config.timeoutMs, config.interleaveDepthMs);
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Whether a stream's geometry can define an ordered-relay horizon at all.
|
|
1339
|
+
* One definition, shared by the config-time and packet-feed callers, so the
|
|
1340
|
+
* two can never disagree about which streams settle by stream order.
|
|
1341
|
+
*/
|
|
1342
|
+
static #hasOrderedGeometry(config) {
|
|
1343
|
+
return config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS && // `fps` is no longer an input to the horizon, but it still gates WHICH
|
|
1344
|
+
// streams settle by stream order at all: a track with no usable
|
|
1345
|
+
// media-unit rate fell back to datagram semantics before this and must
|
|
1346
|
+
// keep doing so. Removing this widens ordered-relay settlement to streams
|
|
1347
|
+
// that never had it. Load-bearing — do not drop it as unused.
|
|
1348
|
+
config.fps !== void 0 && Number.isFinite(config.fps) && config.fps > 0 && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* The horizon for one stream, or undefined when its geometry cannot define
|
|
1352
|
+
* one.
|
|
1353
|
+
*
|
|
1354
|
+
* `config.timeoutMs` is the block's delivery window. On the live path
|
|
1355
|
+
* multicast-manager computes it with `jitterBufferMs: fec.reorderToleranceMs`
|
|
1356
|
+
* and passes it to configure(), which is the same `deliveryWindowMs`
|
|
1357
|
+
* fec-worker-setup sizes decoder retention from, so the two horizons are
|
|
1358
|
+
* equal there.
|
|
1359
|
+
*
|
|
1360
|
+
* They are NOT equal on the fallback path, and the skew runs in the
|
|
1361
|
+
* direction this file's invariant calls unsafe. `jitterBufferMs` reaches
|
|
1362
|
+
* fec-manager only inside an explicit configure({timeoutMs}); all four of
|
|
1363
|
+
* its own computeFecDeliveryWindowMs calls (one in #computeTimeout, one in
|
|
1364
|
+
* configure(), two in setFramerate) omit it, so a manager-derived
|
|
1365
|
+
* timeoutMs is tolerance-free and yields a
|
|
1366
|
+
* horizon NARROWER than the decoder's retention — the direction that settles
|
|
1367
|
+
* a block the decoder would still have recovered. At 720p/33ms the decoder
|
|
1368
|
+
* retains ceil(133/33)+1 = 6 while a tolerance-free 33ms window gives
|
|
1369
|
+
* ceil(33/33)+1 = 2. Two writes can also raise a validated timeoutMs
|
|
1370
|
+
* afterwards without re-validating (configure()'s widening loop and
|
|
1371
|
+
* setFramerate).
|
|
1372
|
+
*
|
|
1373
|
+
* Not fixed here: the real fix is to thread the tolerance into those four
|
|
1374
|
+
* calls so both sides derive from the same quantity, which is a change to
|
|
1375
|
+
* inputs fec-manager does not currently receive. Tracked in BLO-38250. The
|
|
1376
|
+
* live ISS path does not hit it — multicast-manager always supplies both
|
|
1377
|
+
* `timeoutMs` and `fps`, and setFramerate skips tracks that already carry
|
|
1378
|
+
* `fps` — so this is a latent skew on the fallback, not a live defect. Do
|
|
1379
|
+
* not read the old "can only skew narrower, never wider" note as a safety
|
|
1380
|
+
* argument: narrower IS the failing direction.
|
|
1381
|
+
*/
|
|
1382
|
+
#derivableOrderedHorizon(config, packetId) {
|
|
1383
|
+
if (!_FECManager.#hasOrderedGeometry(config)) return void 0;
|
|
1384
|
+
try {
|
|
1385
|
+
const blocks = tryDeriveFecRetentionBlocks(config.timeoutMs, config.interleaveDepthMs);
|
|
1386
|
+
if (blocks === void 0) {
|
|
1387
|
+
this.#noteLeftOrderedSettlement(
|
|
1388
|
+
packetId,
|
|
1389
|
+
`geometry cannot define a retention horizon within the 255-block ceiling (timeoutMs=${config.timeoutMs}ms over interleaveDepthMs=${config.interleaveDepthMs}ms)`
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
return blocks;
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
this.#noteLeftOrderedSettlement(packetId, String(error));
|
|
1395
|
+
return void 0;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Warn once per stream that it dropped from ordered-relay settlement to the
|
|
1400
|
+
* datagram deadline. Swallowing is right for the packet-feed contract, but a
|
|
1401
|
+
* configured stream changing settlement mid-flight is a behavioural change,
|
|
1402
|
+
* and it is otherwise indistinguishable from a stream that never had
|
|
1403
|
+
* geometry.
|
|
1404
|
+
*/
|
|
1405
|
+
#noteLeftOrderedSettlement(packetId, reason) {
|
|
1406
|
+
if (this.#orderedHorizonWarnedFor.has(packetId)) return;
|
|
1407
|
+
this.#orderedHorizonWarnedFor.add(packetId);
|
|
1408
|
+
console.warn(
|
|
1409
|
+
`[FECManager] packetId=${packetId} left ordered-relay settlement for the datagram deadline: ${reason}`
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* The stream's interleave horizon, or undefined when its geometry cannot
|
|
1414
|
+
* define one. Two different streams reach that case, with different causes:
|
|
1415
|
+
* a packetId that was never configured, and so resolves to the manager
|
|
1416
|
+
* defaults (explicitly configured tracks are refused an underivable geometry
|
|
1417
|
+
* at config time — #upsertTrackConfig, setMulticastSymbolDelivery); or a
|
|
1418
|
+
* configured track whose timeoutMs was later widened past the 255-block
|
|
1419
|
+
* ceiling without re-validating, which #derivableOrderedHorizon catches
|
|
1420
|
+
* rather than throwing from the packet-feed path. Either way the stream
|
|
1421
|
+
* keeps datagram semantics instead of throwing. The manager defaults can
|
|
1422
|
+
* also change under a stream that already holds blocks, so a caller cannot
|
|
1423
|
+
* assume a horizon that was defined when a block was held is still defined
|
|
1424
|
+
* (see #advanceOrderedFrontier).
|
|
1425
|
+
*/
|
|
1426
|
+
#orderedHorizonBlocks(packetId) {
|
|
1427
|
+
return this.#derivableOrderedHorizon(this.#resolveTrackConfig(packetId), packetId);
|
|
1428
|
+
}
|
|
1429
|
+
/** True while a block of an ordered-relay stream can still receive symbols. */
|
|
1430
|
+
#withinOrderedHorizon(key) {
|
|
1431
|
+
if (this.#multicastSymbolDelivery !== "ordered-relay") return false;
|
|
1432
|
+
const block = this.#parseBlockKey(key);
|
|
1433
|
+
const frontier = this.#orderedFrontiers.get(
|
|
1434
|
+
this.#streamKey(block.packetId, block.catalogTrackName)
|
|
1435
|
+
);
|
|
1436
|
+
if (frontier === void 0) return false;
|
|
1437
|
+
const horizon = this.#orderedHorizonBlocks(block.packetId);
|
|
1438
|
+
if (horizon === void 0) return false;
|
|
1439
|
+
return this.#blocksBehind(block.sbn, frontier, block.packetId) <= horizon;
|
|
1440
|
+
}
|
|
1441
|
+
/** Whether a block's fate is decided by stream order rather than a timer. */
|
|
1442
|
+
#settlesByOrder(key) {
|
|
1443
|
+
return this.#multicastSymbolDelivery === "ordered-relay" && (this.#blockPathCounts.get(key)?.multicastSymbols ?? 0) > 0 && this.#orderedHorizonBlocks(this.#parseBlockKey(key).packetId) !== void 0;
|
|
1444
|
+
}
|
|
1445
|
+
#forgetOrderedPending(key) {
|
|
1446
|
+
if (this.#orderedPendingBlocks.size === 0) return;
|
|
1447
|
+
const block = this.#parseBlockKey(key);
|
|
1448
|
+
const streamKey = this.#streamKey(block.packetId, block.catalogTrackName);
|
|
1449
|
+
const pending = this.#orderedPendingBlocks.get(streamKey);
|
|
1450
|
+
if (pending?.get(block.sbn) !== key) return;
|
|
1451
|
+
pending.delete(block.sbn);
|
|
1452
|
+
if (pending.size === 0) this.#orderedPendingBlocks.delete(streamKey);
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Record the stream position of a multicast symbol and settle every pending
|
|
1456
|
+
* block it proves final: a block more than the interleave horizon behind the
|
|
1457
|
+
* frontier has already been handed every symbol the relay will deliver.
|
|
1458
|
+
*/
|
|
1459
|
+
#advanceOrderedFrontier(streamKey, sbn, packetId, catalogTrackName) {
|
|
1460
|
+
const previous = this.#orderedFrontiers.get(streamKey);
|
|
1461
|
+
const frontier = previous === void 0 || this.#blockPrecedes(previous, sbn, packetId) ? sbn : previous;
|
|
1462
|
+
if (frontier !== previous) this.#orderedFrontiers.set(streamKey, frontier);
|
|
1463
|
+
const pending = this.#orderedPendingBlocks.get(streamKey);
|
|
1464
|
+
if (!pending) return;
|
|
1465
|
+
const blocks = this.#orderedHorizonBlocks(packetId);
|
|
1466
|
+
if (blocks === void 0) {
|
|
1467
|
+
this.#orderedPendingBlocks.delete(streamKey);
|
|
1468
|
+
for (const pendingSbn of pending.keys()) {
|
|
1469
|
+
this.noteBlockActivity(pendingSbn, packetId, catalogTrackName);
|
|
1470
|
+
}
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
for (const [pendingSbn, key] of pending) {
|
|
1474
|
+
if (this.#blocksBehind(pendingSbn, frontier, packetId) <= blocks) continue;
|
|
1475
|
+
pending.delete(pendingSbn);
|
|
1476
|
+
this.#settleOrderedBlock(key, pendingSbn, packetId, catalogTrackName);
|
|
1477
|
+
}
|
|
1478
|
+
if (pending.size === 0 && this.#orderedPendingBlocks.get(streamKey) === pending) {
|
|
1479
|
+
this.#orderedPendingBlocks.delete(streamKey);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Decide a block the ordered relay has finished delivering. Its accounting
|
|
1484
|
+
* is final: SSB_length (from any repair symbol) against the source and
|
|
1485
|
+
* repair symbols it holds says whether the decoder can still recover it.
|
|
1486
|
+
* A block short of that asks for repair when D-28 is wired, under the same
|
|
1487
|
+
* post-request fetch budget the datagram path grants; otherwise, and for a
|
|
1488
|
+
* block the decoder holds enough symbols for, the decoder finalizes it
|
|
1489
|
+
* exactly as a datagram deadline would (a Worker acknowledges the cleanup
|
|
1490
|
+
* only after every earlier symbol, so an in-flight recovery still wins).
|
|
1491
|
+
*/
|
|
1492
|
+
#settleOrderedBlock(key, sbn, packetId, catalogTrackName) {
|
|
1493
|
+
if (this.#completedBlocks.has(key) || this.#pendingWorkerCleanups.has(key) || this.#repairRequestedBlocks.has(key)) return;
|
|
1494
|
+
const authoredLength = this.#blockAuthoredLengths.get(key);
|
|
1495
|
+
const received = (this.#blockSourceCounts.get(key) ?? 0) + (this.#blockRepairRsIds.get(key)?.size ?? 0);
|
|
1496
|
+
if (this.onRepairNeeded && (authoredLength === void 0 || received < authoredLength)) {
|
|
1497
|
+
this.#requestRepair(sbn, packetId, catalogTrackName, true);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
this.#traceLifecycle("horizon-settled", sbn, packetId, catalogTrackName);
|
|
1501
|
+
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
|
|
1502
|
+
}
|
|
1503
|
+
/** Register a block whose multicast symbols arrive over an ordered relay. */
|
|
1504
|
+
#holdForOrderedSettlement(key, sbn, packetId, catalogTrackName) {
|
|
1505
|
+
this.#clearRepairDeadline(key);
|
|
1506
|
+
if (this.#activeBlocks.has(key)) {
|
|
1507
|
+
this.#clearBlockTimer(key);
|
|
1508
|
+
this.#unindexBlockKey(key);
|
|
1509
|
+
}
|
|
1510
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
1511
|
+
let pending = this.#orderedPendingBlocks.get(streamKey);
|
|
1512
|
+
if (!pending) {
|
|
1513
|
+
pending = /* @__PURE__ */ new Map();
|
|
1514
|
+
this.#orderedPendingBlocks.set(streamKey, pending);
|
|
1515
|
+
}
|
|
1516
|
+
pending.set(sbn, key);
|
|
1517
|
+
}
|
|
1518
|
+
/** Cancel and forget a block's inactivity timer and deadline. */
|
|
1519
|
+
#clearBlockTimer(key) {
|
|
1520
|
+
const timer = this.#activeBlocks.get(key);
|
|
1521
|
+
if (timer?.handle != null) clearTimeout(timer.handle);
|
|
1522
|
+
this.#activeBlocks.delete(key);
|
|
1523
|
+
this.#activeBlockDeadlinesMs.delete(key);
|
|
1524
|
+
this.#blockWaitDeadlinesMs.delete(key);
|
|
1525
|
+
this.#repairHoldUntilMs.delete(key);
|
|
1526
|
+
}
|
|
1527
|
+
#parseBlockKey(key) {
|
|
1528
|
+
const [packetIdText, sbnText, ...catalogParts] = key.split(":");
|
|
1529
|
+
const catalogTrackName = catalogParts.length > 0 ? decodeURIComponent(catalogParts.join(":")) : void 0;
|
|
1530
|
+
return {
|
|
1531
|
+
packetId: Number(packetIdText) || 0,
|
|
1532
|
+
sbn: Number(sbnText) || 0,
|
|
1533
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName }
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
#decoderForPacket(packetId, catalogTrackName) {
|
|
1537
|
+
return this.#fecDecoders.get(this.#streamKey(packetId, catalogTrackName)) ?? this.#fecDecoders.get(this.#streamKey(packetId));
|
|
1538
|
+
}
|
|
1539
|
+
#workerForPacket(packetId, catalogTrackName) {
|
|
1540
|
+
return this.#workerClients.get(this.#streamKey(packetId, catalogTrackName)) ?? this.#workerClients.get(this.#streamKey(packetId));
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* The stream's Worker client for routing, or undefined after evicting a
|
|
1544
|
+
* registered client that was already disposed. A disposed client throws on
|
|
1545
|
+
* every command, so it leaves routing here before a symbol reaches it and
|
|
1546
|
+
* the stream reports its decoder unavailable instead.
|
|
1547
|
+
*/
|
|
1548
|
+
#routableWorkerForPacket(packetId, catalogTrackName) {
|
|
1549
|
+
const client = this.#workerForPacket(packetId, catalogTrackName);
|
|
1550
|
+
if (client === void 0 || client.disposed !== true) return client;
|
|
1551
|
+
this.#evictDisposedWorker(client);
|
|
1552
|
+
return void 0;
|
|
1553
|
+
}
|
|
1554
|
+
#claimDecoderBlock(client, sbn, catalogTrackName) {
|
|
1555
|
+
let blocks = this.#decoderBlockOwners.get(client);
|
|
1556
|
+
if (!blocks) {
|
|
1557
|
+
blocks = /* @__PURE__ */ new Map();
|
|
1558
|
+
this.#decoderBlockOwners.set(client, blocks);
|
|
1559
|
+
}
|
|
1560
|
+
const owner = blocks.get(sbn);
|
|
1561
|
+
if (!owner) {
|
|
1562
|
+
blocks.set(sbn, { catalogTrackName, conflicted: false });
|
|
1563
|
+
if (blocks.size > 512) {
|
|
1564
|
+
const stale = [...blocks.keys()].slice(0, 256);
|
|
1565
|
+
for (const key of stale) blocks.delete(key);
|
|
1566
|
+
}
|
|
1567
|
+
return true;
|
|
1568
|
+
}
|
|
1569
|
+
if (owner.conflicted) return false;
|
|
1570
|
+
if (owner.catalogTrackName === catalogTrackName) return true;
|
|
1571
|
+
owner.conflicted = true;
|
|
1572
|
+
return false;
|
|
1573
|
+
}
|
|
1574
|
+
#decoderBlockCatalogTrackName(client, sbn) {
|
|
1575
|
+
const owner = this.#decoderBlockOwners.get(client)?.get(sbn);
|
|
1576
|
+
return owner?.conflicted ? void 0 : owner?.catalogTrackName;
|
|
1577
|
+
}
|
|
1578
|
+
#resetDecoderBlockOwners(client, catalogTrackName) {
|
|
1579
|
+
const blocks = this.#decoderBlockOwners.get(client);
|
|
1580
|
+
if (!blocks) return;
|
|
1581
|
+
for (const [sbn, owner] of blocks) {
|
|
1582
|
+
if (owner.catalogTrackName === catalogTrackName || owner.conflicted) {
|
|
1583
|
+
blocks.delete(sbn);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
if (blocks.size === 0) this.#decoderBlockOwners.delete(client);
|
|
1587
|
+
}
|
|
1588
|
+
#discardFecDecoderClient(client) {
|
|
1589
|
+
for (const [key, entry] of this.#fecDecoders) {
|
|
1590
|
+
if (entry.client === client) this.#fecDecoders.delete(key);
|
|
1591
|
+
}
|
|
1592
|
+
this.#decoderBlockOwners.delete(client);
|
|
1593
|
+
}
|
|
1594
|
+
#assertExclusiveDecoderClient(streamKey, client, registrations, kind) {
|
|
1595
|
+
for (const [registeredKey, registeredClient] of registrations) {
|
|
1596
|
+
if (registeredClient !== client || registeredKey === streamKey) continue;
|
|
1597
|
+
throw new Error(
|
|
1598
|
+
`[FECManager] ${kind} client is already registered for stream ${registeredKey}; create one client per physical stream`
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
#statsForPacket(packetId) {
|
|
1603
|
+
let stats = this.#fecTrackStats.get(packetId);
|
|
1604
|
+
if (!stats) {
|
|
1605
|
+
stats = newFecCounters();
|
|
1606
|
+
this.#fecTrackStats.set(packetId, stats);
|
|
1607
|
+
}
|
|
1608
|
+
return stats;
|
|
1609
|
+
}
|
|
1610
|
+
#markPathBlockComplete(sbn, packetId, catalogTrackName) {
|
|
1611
|
+
const block = this.#blockPathCounts.get(this.#blockKey(sbn, packetId, catalogTrackName));
|
|
1612
|
+
if (block) block.complete = true;
|
|
1613
|
+
}
|
|
1614
|
+
/** Cancel and forget a block's D-28 repair-deadline timer. */
|
|
1615
|
+
#clearRepairDeadline(key) {
|
|
1616
|
+
const timer = this.#repairDeadlines.get(key);
|
|
1617
|
+
if (timer?.handle != null) clearTimeout(timer.handle);
|
|
1618
|
+
this.#repairDeadlines.delete(key);
|
|
1619
|
+
this.#repairDeadlineAtMs.delete(key);
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Move a block's D-28 repair deadline to `delayMs` from now. The timer
|
|
1623
|
+
* already armed for an earlier moment fires first and re-arms for the
|
|
1624
|
+
* remainder, so repeated source progress costs no timer operation.
|
|
1625
|
+
*/
|
|
1626
|
+
#armRepairDeadline(key, sbn, packetId, catalogTrackName, delayMs) {
|
|
1627
|
+
const deadlineMs = performance.now() + delayMs;
|
|
1628
|
+
this.#repairDeadlineAtMs.set(key, deadlineMs);
|
|
1629
|
+
const armed = this.#repairDeadlines.get(key);
|
|
1630
|
+
if (armed?.handle != null) {
|
|
1631
|
+
if (armed.fireAtMs <= deadlineMs) return;
|
|
1632
|
+
clearTimeout(armed.handle);
|
|
1633
|
+
}
|
|
1634
|
+
const timer = { handle: null, fireAtMs: deadlineMs };
|
|
1635
|
+
const onFire = () => {
|
|
1636
|
+
if (this.#repairDeadlines.get(key) !== timer) return;
|
|
1637
|
+
const currentDeadlineMs = this.#repairDeadlineAtMs.get(key);
|
|
1638
|
+
const nowMs = performance.now();
|
|
1639
|
+
if (currentDeadlineMs !== void 0 && currentDeadlineMs > nowMs) {
|
|
1640
|
+
timer.fireAtMs = currentDeadlineMs;
|
|
1641
|
+
timer.handle = setTimeout(onFire, Math.ceil(currentDeadlineMs - nowMs));
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
timer.handle = null;
|
|
1645
|
+
this.#requestRepair(sbn, packetId, catalogTrackName);
|
|
1646
|
+
};
|
|
1647
|
+
timer.handle = setTimeout(onFire, delayMs);
|
|
1648
|
+
this.#repairDeadlines.set(key, timer);
|
|
1649
|
+
}
|
|
1650
|
+
#requestRepair(sbn, packetId, catalogTrackName, settledByOrder = false) {
|
|
1651
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
1652
|
+
if (this.#completedBlocks.has(key) || !this.onRepairNeeded || this.#repairRequestedBlocks.has(key)) return;
|
|
1653
|
+
const deadline = this.#repairDeadlines.get(key);
|
|
1654
|
+
if (deadline?.handle != null) clearTimeout(deadline.handle);
|
|
1655
|
+
this.#repairDeadlines.set(key, null);
|
|
1656
|
+
this.#repairDeadlineAtMs.delete(key);
|
|
1657
|
+
this.#repairRequestedBlocks.add(key);
|
|
1658
|
+
if (settledByOrder) this.#armBlockTimeout(sbn, packetId, catalogTrackName, 0);
|
|
1659
|
+
this.#traceLifecycle("repair-requested", sbn, packetId, catalogTrackName);
|
|
1660
|
+
this.onRepairNeeded(
|
|
1661
|
+
sbn,
|
|
1662
|
+
packetId,
|
|
1663
|
+
catalogTrackName,
|
|
1664
|
+
this.blockClassFor(sbn, packetId, catalogTrackName)
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
// ── Keyframe-protected repair policy ──
|
|
1668
|
+
/**
|
|
1669
|
+
* The classifier geometry of one stream. A stream-scoped configuration
|
|
1670
|
+
* wins. Once any stream of a packetId was configured with its own geometry,
|
|
1671
|
+
* a sibling stream without one has none that provably belongs to it: the
|
|
1672
|
+
* packet-level geometry may be another rung's (a different framerate or
|
|
1673
|
+
* GOP), so it runs all-protected (`stream-geometry-absent`).
|
|
1674
|
+
*/
|
|
1675
|
+
#geometryFor(packetId, catalogTrackName) {
|
|
1676
|
+
const own = this.#streamGeometries.get(this.#streamKey(packetId, catalogTrackName));
|
|
1677
|
+
if (own) return own;
|
|
1678
|
+
const config = this.#trackConfigs.get(packetId);
|
|
1679
|
+
if (config?.trackKind === "audio") return { trackKind: "audio" };
|
|
1680
|
+
for (const key of this.#streamGeometries.keys()) {
|
|
1681
|
+
if (this.#belongsToPacket(key, packetId)) {
|
|
1682
|
+
return { trackKind: "video", mode: "all-protected", reason: "stream-geometry-absent" };
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
if (config?.geometry) return config.geometry;
|
|
1686
|
+
return { trackKind: "video", mode: "all-protected", reason: "keyframe-cadence-absent" };
|
|
1687
|
+
}
|
|
1688
|
+
#classifierFor(packetId, catalogTrackName) {
|
|
1689
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
1690
|
+
let classifier = this.#classifiers.get(streamKey);
|
|
1691
|
+
if (!classifier) {
|
|
1692
|
+
classifier = new BlockClassifier(this.#geometryFor(packetId, catalogTrackName));
|
|
1693
|
+
this.#classifiers.set(streamKey, classifier);
|
|
1694
|
+
if (classifier.geometry.trackKind === "video") {
|
|
1695
|
+
this.onClassifierMode?.(packetId, catalogTrackName, classifier.mode, classifier.reason);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
return classifier;
|
|
1699
|
+
}
|
|
1700
|
+
#applyClassifierChanges(packetId, catalogTrackName, changes) {
|
|
1701
|
+
for (const change of changes) {
|
|
1702
|
+
if (change.kind === "mode") {
|
|
1703
|
+
this.onClassifierMode?.(packetId, catalogTrackName, change.mode, change.reason);
|
|
1704
|
+
} else {
|
|
1705
|
+
this.onBlockReclassified?.(
|
|
1706
|
+
change.sbn,
|
|
1707
|
+
packetId,
|
|
1708
|
+
catalogTrackName,
|
|
1709
|
+
change.from,
|
|
1710
|
+
change.to,
|
|
1711
|
+
change.evidence
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Freeze a block's class at resolution and drop its classifier state. The
|
|
1718
|
+
* frozen class answers every later query (a recovery counted after the
|
|
1719
|
+
* block was cancelled, a failure event) exactly.
|
|
1720
|
+
*/
|
|
1721
|
+
#retireBlockClass(key, sbn, packetId, catalogTrackName) {
|
|
1722
|
+
const frozen = this.#resolvedBlockClasses.get(key);
|
|
1723
|
+
if (frozen !== void 0) return frozen;
|
|
1724
|
+
const blockClass = this.#peekClass(sbn, packetId, catalogTrackName);
|
|
1725
|
+
this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.forget(sbn);
|
|
1726
|
+
this.#resolvedBlockClasses.set(key, blockClass);
|
|
1727
|
+
this.#materializedBlocks.delete(key);
|
|
1728
|
+
return blockClass;
|
|
1729
|
+
}
|
|
1730
|
+
/** A block's live class without creating classifier state for the stream. */
|
|
1731
|
+
#peekClass(sbn, packetId, catalogTrackName) {
|
|
1732
|
+
const classifier = this.#classifiers.get(this.#streamKey(packetId, catalogTrackName));
|
|
1733
|
+
if (classifier) return classifier.classOf(sbn);
|
|
1734
|
+
return this.#geometryFor(packetId, catalogTrackName).trackKind === "audio" ? "audio" : "unknown";
|
|
1735
|
+
}
|
|
1736
|
+
#viaFor(key) {
|
|
1737
|
+
return (this.#blockPathCounts.get(key)?.moqSymbols ?? 0) > 0 ? "quic" : "inband";
|
|
1738
|
+
}
|
|
1739
|
+
#noteBlockRepaired(key, sbn, packetId, catalogTrackName) {
|
|
1740
|
+
if (this.#repairedBlocks.has(key)) return;
|
|
1741
|
+
this.#repairedBlocks.add(key);
|
|
1742
|
+
this.onBlockRepaired?.(
|
|
1743
|
+
sbn,
|
|
1744
|
+
packetId,
|
|
1745
|
+
catalogTrackName,
|
|
1746
|
+
this.blockClassFor(sbn, packetId, catalogTrackName),
|
|
1747
|
+
this.#viaFor(key)
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
// Deliberately one block tighter than deriveFecRetentionBlocks over the same
|
|
1751
|
+
// inputs (that function's `+ 1`). Different question: the retention horizon
|
|
1752
|
+
// asks how long the decoder still ACCEPTS symbols for a block, this asks how
|
|
1753
|
+
// far back a block that vanished entirely may still be repairable (see
|
|
1754
|
+
// #noteSbnSeen).
|
|
1755
|
+
// The extra block of retention is slack for a block still being fed; a block
|
|
1756
|
+
// nothing has been heard from does not get it. Not the units defect — but it
|
|
1757
|
+
// is the next place the two can drift, so change both or neither.
|
|
1758
|
+
#liveWindowBlocks(config) {
|
|
1759
|
+
if (config.interleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS || config.timeoutMs <= 0) return null;
|
|
1760
|
+
return Math.ceil(config.timeoutMs / config.interleaveDepthMs);
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Record that a symbol for block `sbn` arrived and materialise any blocks
|
|
1764
|
+
* that vanished entirely between the stream's highest SBN and this one.
|
|
1765
|
+
*
|
|
1766
|
+
* The newest ceil(timeoutMs / interleaveDepthMs) vanished blocks may still
|
|
1767
|
+
* be repairable, so they get the normal repair and fail deadlines, armed
|
|
1768
|
+
* from now. Older ones are already past their fail bound and fail now, as
|
|
1769
|
+
* class `unknown`. A SOURCE symbol whose SBN goes back further than one
|
|
1770
|
+
* live window, or jumps further than the stream could have produced since
|
|
1771
|
+
* its last symbol, is a source discontinuity, not loss. A repair symbol can
|
|
1772
|
+
* only advance the frontier: a D-28 fetch legitimately answers for an old
|
|
1773
|
+
* block, so it never signals a discontinuity.
|
|
1774
|
+
*
|
|
1775
|
+
* The producer closes a block at its Dth media-unit boundary OR when K
|
|
1776
|
+
* source symbols filled it, so one interleave span can start several
|
|
1777
|
+
* blocks (every large keyframe does). What bounds a forward jump is
|
|
1778
|
+
* therefore packet evidence, not a block rate: with the MMTP
|
|
1779
|
+
* packet_sequence_number (contiguous per packet_id, ISO/IEC 23008-1) of
|
|
1780
|
+
* both source symbols, every block strictly between them holds at least one
|
|
1781
|
+
* of the packets sent strictly between them, and K-closes can add at most
|
|
1782
|
+
* one block per K of those packets on top of the D-closes elapsed time
|
|
1783
|
+
* allows. Without it, only the D-close rate is known.
|
|
1784
|
+
*/
|
|
1785
|
+
#noteSbnSeen(sbn, packetId, catalogTrackName, symbol, packetSequenceNumber) {
|
|
1786
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
1787
|
+
const now = performance.now();
|
|
1788
|
+
const previous = this.#highestSbn.get(streamKey);
|
|
1789
|
+
const anchor = this.#sourceAnchors.get(streamKey);
|
|
1790
|
+
const psn = symbol === "source" && packetSequenceNumber !== void 0 && Number.isSafeInteger(packetSequenceNumber) && packetSequenceNumber >= 0 && packetSequenceNumber <= 4294967295 ? packetSequenceNumber : void 0;
|
|
1791
|
+
const anchorHere = () => {
|
|
1792
|
+
if (psn !== void 0 && (anchor === void 0 || !this.#blockPrecedes(sbn, anchor.sbn, packetId))) {
|
|
1793
|
+
this.#sourceAnchors.set(streamKey, { sbn, psn, atMs: now });
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
const discontinuity = () => {
|
|
1797
|
+
this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
|
|
1798
|
+
this.#highestSbn.set(streamKey, { sbn, atMs: now });
|
|
1799
|
+
if (psn === void 0) this.#sourceAnchors.delete(streamKey);
|
|
1800
|
+
else this.#sourceAnchors.set(streamKey, { sbn, psn, atMs: now });
|
|
1801
|
+
this.onSourceDiscontinuity?.(packetId, catalogTrackName);
|
|
1802
|
+
};
|
|
1803
|
+
if (previous === void 0) {
|
|
1804
|
+
this.#highestSbn.set(streamKey, { sbn, atMs: now });
|
|
1805
|
+
anchorHere();
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
const config = this.#resolveTrackConfig(packetId);
|
|
1809
|
+
const window = this.#liveWindowBlocks(config);
|
|
1810
|
+
if (!this.#blockPrecedes(previous.sbn, sbn, packetId)) {
|
|
1811
|
+
if (symbol === "source" && window !== null && this.#blocksBehind(sbn, previous.sbn, packetId) > window) {
|
|
1812
|
+
discontinuity();
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
anchorHere();
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const missing = this.#blocksBehind(previous.sbn, sbn, packetId) - 1;
|
|
1819
|
+
let plausible = true;
|
|
1820
|
+
if (window !== null) {
|
|
1821
|
+
if (this.#isIso(packetId) && psn !== void 0 && anchor !== void 0 && this.#blockPrecedes(anchor.sbn, sbn, packetId) && psn !== anchor.psn) {
|
|
1822
|
+
const sent = psn - anchor.psn >>> 0;
|
|
1823
|
+
plausible = sent <= 2147483647 && serial32Distance(anchor.sbn, sbn) <= sent + config.k;
|
|
1824
|
+
} else if (psn !== void 0 && anchor !== void 0 && this.#blockPrecedes(anchor.sbn, sbn, packetId) && psn !== anchor.psn) {
|
|
1825
|
+
const sent = psn - anchor.psn >>> 0;
|
|
1826
|
+
const between = this.#blocksBehind(anchor.sbn, sbn, packetId) - 1;
|
|
1827
|
+
const dCloses = window + Math.ceil((now - anchor.atMs) / config.interleaveDepthMs);
|
|
1828
|
+
const kCloses = config.k > 0 ? Math.ceil(sent / config.k) : sent;
|
|
1829
|
+
plausible = sent <= 2147483647 && between <= sent - 1 && between <= dCloses + kCloses;
|
|
1830
|
+
} else {
|
|
1831
|
+
plausible = missing <= window + Math.ceil((now - previous.atMs) / config.interleaveDepthMs);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
if (!plausible) {
|
|
1835
|
+
if (symbol === "repair") return;
|
|
1836
|
+
discontinuity();
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
this.#highestSbn.set(streamKey, { sbn, atMs: now });
|
|
1840
|
+
anchorHere();
|
|
1841
|
+
if (this.#isIso(packetId)) return;
|
|
1842
|
+
if (missing <= 0 || window === null) return;
|
|
1843
|
+
let vanished = previous.sbn;
|
|
1844
|
+
for (let i = 0; i < missing; i++) {
|
|
1845
|
+
vanished = this.#nextBlock(vanished, packetId);
|
|
1846
|
+
this.#materializeBlock(
|
|
1847
|
+
vanished,
|
|
1848
|
+
packetId,
|
|
1849
|
+
catalogTrackName,
|
|
1850
|
+
this.#blocksBehind(vanished, sbn, packetId) <= window
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
/*
|
|
1855
|
+
* Block order for one stream (BLO-35597 iso:5). SS_ID is a 32-bit serial
|
|
1856
|
+
* number (ISO/IEC 23008-1 C.5.2), so blocks are ordered by RFC 1982
|
|
1857
|
+
* comparison of their first SS_ID (sbn * K mod 2^32). That is identical to
|
|
1858
|
+
* comparing SBNs with < for every stream that has not wrapped SS_ID; only
|
|
1859
|
+
* the order across the wrap changes. A block outside K-stride geometry has
|
|
1860
|
+
* no SS_ID position, so it keeps the SBN order it always had.
|
|
1861
|
+
*/
|
|
1862
|
+
#blockPrecedes(a, b, packetId) {
|
|
1863
|
+
if (this.#isIso(packetId)) return serial32Precedes(a, b);
|
|
1864
|
+
const k = this.kForPacket(packetId);
|
|
1865
|
+
return isKStrideBlock(a, k) && isKStrideBlock(b, k) ? kStrideBlockPrecedes(a, b, k) : a < b;
|
|
1866
|
+
}
|
|
1867
|
+
/** Blocks `a` lies behind `b` (0 when it does not precede it). */
|
|
1868
|
+
#blocksBehind(a, b, packetId) {
|
|
1869
|
+
if (this.#isIso(packetId)) return isoBlocksBehindLowerBound(a, b, this.kForPacket(packetId));
|
|
1870
|
+
const k = this.kForPacket(packetId);
|
|
1871
|
+
return isKStrideBlock(a, k) && isKStrideBlock(b, k) ? kStrideBlocksBehind(a, b, k) : Math.max(0, b - a);
|
|
1872
|
+
}
|
|
1873
|
+
/** The block after `sbn`, stepping to block 0 where SS_ID wraps. */
|
|
1874
|
+
#nextBlock(sbn, packetId) {
|
|
1875
|
+
const k = this.kForPacket(packetId);
|
|
1876
|
+
return isKStrideBlock(sbn, k) ? kStrideNextBlock(sbn, k) : sbn + 1;
|
|
1877
|
+
}
|
|
1878
|
+
#materializeBlock(sbn, packetId, catalogTrackName, repairable) {
|
|
1879
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
1880
|
+
if (this.#blockResolutionFor(key) !== "untracked") return;
|
|
1881
|
+
this.#materializedBlocks.add(key);
|
|
1882
|
+
this.#classifierFor(packetId, catalogTrackName).touch(sbn);
|
|
1883
|
+
this.onBlockVanished?.(sbn, packetId, catalogTrackName);
|
|
1884
|
+
if (repairable) {
|
|
1885
|
+
this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
|
|
1886
|
+
} else {
|
|
1887
|
+
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
#blockResolutionFor(key) {
|
|
1891
|
+
if (this.#failedBlocks.has(key)) return "failed";
|
|
1892
|
+
if (this.#completedBlocks.has(key)) return "complete";
|
|
1893
|
+
if (this.#activeBlocks.has(key) || this.#pendingWorkerCleanups.has(key) || this.#repairDeadlines.has(key) || this.#blockSourceCounts.has(key) || this.#blockRepairRsIds.has(key) || this.#materializedBlocks.has(key)) {
|
|
1894
|
+
return "pending";
|
|
1895
|
+
}
|
|
1896
|
+
return "untracked";
|
|
1897
|
+
}
|
|
1898
|
+
#bumpEpoch(packetId) {
|
|
1899
|
+
this.#packetEpochs.set(packetId, (this.#packetEpochs.get(packetId) ?? 0) + 1);
|
|
1900
|
+
}
|
|
1901
|
+
#traceLifecycle(event, sbn, packetId, catalogTrackName, rsId) {
|
|
1902
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
1903
|
+
const deadline = this.#activeBlockDeadlinesMs.get(key);
|
|
1904
|
+
const decoderPath = this.#syncDecoderBlockOwners.has(key) ? "sync" : this.#workerForPacket(packetId, catalogTrackName) ? "worker" : this.#decoderForPacket(packetId, catalogTrackName) ? "sync" : "none";
|
|
1905
|
+
this.#lifecycleTrace.push({
|
|
1906
|
+
atMs: Date.now(),
|
|
1907
|
+
event,
|
|
1908
|
+
packetId,
|
|
1909
|
+
sbn,
|
|
1910
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName },
|
|
1911
|
+
sourceCount: this.#blockSourceCounts.get(key) ?? 0,
|
|
1912
|
+
sourceEsis: [...this.#blockSourceEsis.get(key) ?? []].sort((a, b) => a - b),
|
|
1913
|
+
repairRsIds: [...this.#blockRepairRsIds.get(key) ?? []].sort((a, b) => a - b),
|
|
1914
|
+
decoderPath,
|
|
1915
|
+
...deadline === void 0 ? {} : { deadlineRemainingMs: Math.max(0, deadline - performance.now()) },
|
|
1916
|
+
pendingCleanup: this.#pendingWorkerCleanups.has(key),
|
|
1917
|
+
...rsId === void 0 ? {} : { rsId }
|
|
1918
|
+
});
|
|
1919
|
+
if (this.#lifecycleTrace.length > 64) this.#lifecycleTrace.splice(0, 32);
|
|
1920
|
+
}
|
|
1921
|
+
#defaultTrackConfig() {
|
|
1922
|
+
return {
|
|
1923
|
+
k: this.#fecK,
|
|
1924
|
+
symbolSize: this.#fecSymbolSize,
|
|
1925
|
+
interleaveDepthMs: this.#fecInterleaveDepthMs,
|
|
1926
|
+
fps: this.#fecFps,
|
|
1927
|
+
timeoutMs: this.#fecTimeoutMs
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
#resolveTrackConfig(packetId) {
|
|
1931
|
+
return this.#trackConfigs.get(packetId) ?? this.#defaultTrackConfig();
|
|
1932
|
+
}
|
|
1933
|
+
#computeTimeout(config) {
|
|
1934
|
+
if (config.timeoutMs && config.timeoutMs > 0) return config.timeoutMs;
|
|
1935
|
+
if (config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) {
|
|
1936
|
+
return Math.max(
|
|
1937
|
+
this.#fecTimeoutMs,
|
|
1938
|
+
computeFecDeliveryWindowMs({
|
|
1939
|
+
interleaveDepthMs: config.interleaveDepthMs,
|
|
1940
|
+
sourceSymbols: config.k,
|
|
1941
|
+
mediaUnitsPerSecond: config.fps
|
|
1942
|
+
})
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
return this.#fecTimeoutMs;
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* The delivery-gap window is the longest catalog keyframe interval among
|
|
1949
|
+
* the configured video streams. A failed video block costs every frame up
|
|
1950
|
+
* to the next keyframe, so a receiver must remember a stall for at least
|
|
1951
|
+
* that long: forgetting it sooner would let the next recurrence fail a
|
|
1952
|
+
* block whose damage outlives the memory of the stall that caused it. It
|
|
1953
|
+
* comes from the catalog's `keyframeIntervalTicks`/`keyframeIntervalMs`
|
|
1954
|
+
* (via the classifier geometry), never from a constant. It also caps the
|
|
1955
|
+
* gap the wait may absorb; with no catalog cadence no gap is reported and
|
|
1956
|
+
* the catalog tolerance alone applies (see DeliveryGapEstimator).
|
|
1957
|
+
*/
|
|
1958
|
+
#refreshDeliveryGapWindow() {
|
|
1959
|
+
let windowMs;
|
|
1960
|
+
const consider = (geometry) => {
|
|
1961
|
+
if (geometry?.trackKind !== "video" || geometry.mode !== "classifying") return;
|
|
1962
|
+
const { ticks, timescale } = geometry.keyframeInterval;
|
|
1963
|
+
const ms = ticks / timescale * 1e3;
|
|
1964
|
+
if (Number.isFinite(ms) && ms > 0 && (windowMs === void 0 || ms > windowMs)) windowMs = ms;
|
|
1965
|
+
};
|
|
1966
|
+
for (const geometry of this.#streamGeometries.values()) consider(geometry);
|
|
1967
|
+
for (const config of this.#trackConfigs.values()) consider(config.geometry);
|
|
1968
|
+
this.#deliveryGaps.setWindowMs(windowMs);
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* The multicast late-symbol wait: the block span plus the larger of the
|
|
1972
|
+
* catalog's signaled tolerance and the receiver-measured delivery gap.
|
|
1973
|
+
*
|
|
1974
|
+
* `config.timeoutMs` already is span + `fec.reorderToleranceMs` (or the
|
|
1975
|
+
* wider envelope configure() applies), so it stays the floor; the measured
|
|
1976
|
+
* gap widens it only when the path stalls for longer than the catalog
|
|
1977
|
+
* allowed for. Datagram delivery only: an ordered relay settles blocks by
|
|
1978
|
+
* stream order and never feeds the estimator.
|
|
1979
|
+
*/
|
|
1980
|
+
#multicastWaitMs(config) {
|
|
1981
|
+
return this.#waitForGapMs(config, this.#deliveryGaps.peakGapMs(performance.now()));
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* The widest #multicastWaitMs can become: the estimator reports at most its
|
|
1985
|
+
* catalog window, so this is a catalog quantity that no measurement moves.
|
|
1986
|
+
*/
|
|
1987
|
+
#maxMulticastWaitMs(config) {
|
|
1988
|
+
return this.#waitForGapMs(config, this.#deliveryGaps.windowMs ?? 0);
|
|
1989
|
+
}
|
|
1990
|
+
#waitForGapMs(config, gapMs) {
|
|
1991
|
+
if (!(gapMs > 0) || !(config.interleaveDepthMs > 0)) return config.timeoutMs;
|
|
1992
|
+
return Math.max(config.timeoutMs, Math.ceil(config.interleaveDepthMs + gapMs));
|
|
1993
|
+
}
|
|
1994
|
+
#repairDeadlineMs(config, waitMs = this.#multicastWaitMs(config)) {
|
|
1995
|
+
const halfFailBoundMs = Math.ceil(waitMs / 2);
|
|
1996
|
+
if (config.k > 0 && config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS && config.fps !== void 0 && Number.isFinite(config.fps) && config.fps > 0) {
|
|
1997
|
+
const mediaUnitMs = 1e3 / config.fps;
|
|
1998
|
+
const interleaveUnits = deriveFecInterleaveDepth2(
|
|
1999
|
+
config.interleaveDepthMs,
|
|
2000
|
+
mediaUnitMs
|
|
2001
|
+
);
|
|
2002
|
+
const sourceHorizonMs = Math.ceil(config.k * interleaveUnits * mediaUnitMs);
|
|
2003
|
+
if (sourceHorizonMs < waitMs) return sourceHorizonMs;
|
|
2004
|
+
}
|
|
2005
|
+
return halfFailBoundMs;
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* One block lifetime: the inactivity fail bound plus the D-28 repair grace,
|
|
2009
|
+
* at the widest wait the catalog allows (#maxMulticastWaitMs). Everything
|
|
2010
|
+
* derived from it (terminal retention, the release gate's hold ceiling, the
|
|
2011
|
+
* duplicate ledgers, the owed-cleanup bound) is compared across time, so it
|
|
2012
|
+
* must not follow the live measurement.
|
|
2013
|
+
*/
|
|
2014
|
+
#blockLifetimeMs(config) {
|
|
2015
|
+
const waitMs = this.#maxMulticastWaitMs(config);
|
|
2016
|
+
return waitMs + this.#repairGraceMs(config, waitMs);
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* The lifetime at the live wait, capped at #blockLifetimeMs: lifetime is not
|
|
2020
|
+
* monotone in the wait (the repair deadline switches from half the wait to
|
|
2021
|
+
* the source horizon), so the cap is what keeps it inside the retention.
|
|
2022
|
+
*/
|
|
2023
|
+
#liveBlockLifetimeMs(config) {
|
|
2024
|
+
const waitMs = this.#multicastWaitMs(config);
|
|
2025
|
+
return Math.min(waitMs + this.#repairGraceMs(config, waitMs), this.#blockLifetimeMs(config));
|
|
2026
|
+
}
|
|
2027
|
+
#repairGraceMs(config, waitMs) {
|
|
2028
|
+
return Math.max(1, waitMs - this.#repairDeadlineMs(config, waitMs));
|
|
2029
|
+
}
|
|
2030
|
+
#armBlockTimeout(sbn, packetId, catalogTrackName, delayMs) {
|
|
2031
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
2032
|
+
const waitAtMs = performance.now() + Math.max(1, Math.ceil(delayMs));
|
|
2033
|
+
const holdAtMs = this.#repairHoldUntilMs.get(key);
|
|
2034
|
+
this.#blockWaitDeadlinesMs.set(key, holdAtMs === void 0 ? waitAtMs : Math.min(waitAtMs, holdAtMs));
|
|
2035
|
+
this.#applyBlockDeadline(key, sbn, packetId, catalogTrackName);
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Arm (or move) a block's one fail timer at the later of its multicast
|
|
2039
|
+
* wait and its repair hold. Moving it later costs no timer operation: the
|
|
2040
|
+
* armed timer wakes no later than the new deadline and re-arms for the
|
|
2041
|
+
* remainder.
|
|
2042
|
+
*/
|
|
2043
|
+
#applyBlockDeadline(key, sbn, packetId, catalogTrackName) {
|
|
2044
|
+
const nowMs = performance.now();
|
|
2045
|
+
const deadlineMs = Math.max(
|
|
2046
|
+
this.#blockWaitDeadlinesMs.get(key) ?? nowMs + 1,
|
|
2047
|
+
this.#repairHoldUntilMs.get(key) ?? -Infinity
|
|
2048
|
+
);
|
|
2049
|
+
this.#activeBlockDeadlinesMs.set(key, deadlineMs);
|
|
2050
|
+
const armed = this.#activeBlocks.get(key);
|
|
2051
|
+
if (armed?.handle != null) {
|
|
2052
|
+
if (armed.fireAtMs <= deadlineMs) return;
|
|
2053
|
+
clearTimeout(armed.handle);
|
|
2054
|
+
}
|
|
2055
|
+
const timer = { handle: null, fireAtMs: deadlineMs };
|
|
2056
|
+
const onFire = () => {
|
|
2057
|
+
if (this.#activeBlocks.get(key) !== timer) return;
|
|
2058
|
+
const currentDeadlineMs = this.#activeBlockDeadlinesMs.get(key);
|
|
2059
|
+
const firedAtMs = performance.now();
|
|
2060
|
+
if (currentDeadlineMs !== void 0 && currentDeadlineMs > firedAtMs) {
|
|
2061
|
+
timer.fireAtMs = currentDeadlineMs;
|
|
2062
|
+
timer.handle = setTimeout(onFire, Math.ceil(currentDeadlineMs - firedAtMs));
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
this.#activeBlocks.delete(key);
|
|
2066
|
+
this.#activeBlockDeadlinesMs.delete(key);
|
|
2067
|
+
this.#blockWaitDeadlinesMs.delete(key);
|
|
2068
|
+
this.#repairHoldUntilMs.delete(key);
|
|
2069
|
+
this.#traceLifecycle("timeout-queued", sbn, packetId, catalogTrackName);
|
|
2070
|
+
this.#finalizeExpiredBlock(sbn, packetId, catalogTrackName);
|
|
2071
|
+
};
|
|
2072
|
+
timer.handle = setTimeout(onFire, Math.max(1, Math.ceil(deadlineMs - nowMs)));
|
|
2073
|
+
this.#activeBlocks.set(key, timer);
|
|
2074
|
+
this.#indexBlockKey(key, sbn, packetId);
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Hand a block whose delivery is over to its decoder for finalization: the
|
|
2078
|
+
* datagram deadline fired, or the ordered relay moved past it.
|
|
2079
|
+
*/
|
|
2080
|
+
#finalizeExpiredBlock(sbn, packetId, catalogTrackName) {
|
|
2081
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
2082
|
+
if (this.#materializedBlocks.has(key) && !this.#blockPathCounts.has(key)) {
|
|
2083
|
+
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
const syncOwner = this.#syncDecoderBlockOwners.get(key);
|
|
2087
|
+
if (syncOwner) {
|
|
2088
|
+
this.#routeSyncCleanup(syncOwner, sbn, packetId, catalogTrackName);
|
|
2089
|
+
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
2093
|
+
if (workerClient) {
|
|
2094
|
+
this.#pendingWorkerCleanups.add(key);
|
|
2095
|
+
this.#pendingWorkerCleanupClients.set(key, workerClient);
|
|
2096
|
+
this.#boundOwedCleanup(key, packetId, workerClient);
|
|
2097
|
+
workerClient.cleanup(sbn);
|
|
2098
|
+
} else {
|
|
2099
|
+
const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client;
|
|
2100
|
+
if (decoder) {
|
|
2101
|
+
this.#routeSyncCleanup(decoder, sbn, packetId, catalogTrackName);
|
|
2102
|
+
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
2103
|
+
} else {
|
|
2104
|
+
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Upsert one packetId's track configuration. With `catalogTrackName`, the
|
|
2110
|
+
* classifier geometry in `config` is that stream's own and is kept per
|
|
2111
|
+
* stream; every other field stays per packetId.
|
|
2112
|
+
*/
|
|
2113
|
+
#upsertTrackConfig(packetId, config, catalogTrackName) {
|
|
2114
|
+
const previous = this.#trackConfigs.get(packetId) ?? this.#defaultTrackConfig();
|
|
2115
|
+
const streamScopedGeometry = catalogTrackName !== void 0 && config.geometry !== void 0 ? config.geometry : void 0;
|
|
2116
|
+
if (streamScopedGeometry !== void 0) {
|
|
2117
|
+
const { geometry: _streamGeometry, ...packetLevel } = config;
|
|
2118
|
+
config = packetLevel;
|
|
2119
|
+
}
|
|
2120
|
+
const timingChanged = config.k !== void 0 || config.interleaveDepthMs !== void 0 || config.fps !== void 0;
|
|
2121
|
+
if (config.blockProfile !== void 0) {
|
|
2122
|
+
parseFecBlockProfile(config.blockProfile, `packetId=${packetId} fec.blockProfile`);
|
|
2123
|
+
}
|
|
2124
|
+
const blockProfile = config.blockProfile ?? previous.blockProfile;
|
|
2125
|
+
if (blockProfile === void 0) {
|
|
2126
|
+
throw new Error(
|
|
2127
|
+
`[FECManager] packetId=${packetId}: fec.blockProfile is required ("k-padded-v1" or "iso-ssbg1-v1"); configure the track from its catalog`
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
if (blockProfile === "iso-ssbg1-v1") {
|
|
2131
|
+
const ownInterleaveDepthMs = config.interleaveDepthMs ?? this.#trackConfigs.get(packetId)?.interleaveDepthMs;
|
|
2132
|
+
if (ownInterleaveDepthMs === void 0 || !Number.isFinite(ownInterleaveDepthMs) || ownInterleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS) {
|
|
2133
|
+
throw new Error(
|
|
2134
|
+
`[FECManager] packetId=${packetId}: iso-ssbg1-v1 requires the catalog fec.interleaveDepthMs (>= ${MIN_INTERLEAVE_DEPTH_MS} ms; got ${String(ownInterleaveDepthMs)}): it bounds how long block location keeps stated blocks and held sources`
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
const next = {
|
|
2139
|
+
blockProfile,
|
|
2140
|
+
k: config.k ?? previous.k,
|
|
2141
|
+
symbolSize: config.symbolSize != null && config.symbolSize > 0 ? config.symbolSize : previous.symbolSize,
|
|
2142
|
+
interleaveDepthMs: config.interleaveDepthMs ?? previous.interleaveDepthMs,
|
|
2143
|
+
fps: config.fps ?? previous.fps,
|
|
2144
|
+
timeoutMs: config.timeoutMs ?? (timingChanged ? 0 : previous.timeoutMs),
|
|
2145
|
+
...(config.trackKind ?? previous.trackKind) === void 0 ? {} : { trackKind: config.trackKind ?? previous.trackKind },
|
|
2146
|
+
...(config.codec ?? previous.codec) === void 0 ? {} : { codec: config.codec ?? previous.codec },
|
|
2147
|
+
...(config.geometry ?? previous.geometry) === void 0 ? {} : { geometry: config.geometry ?? previous.geometry }
|
|
2148
|
+
};
|
|
2149
|
+
next.timeoutMs = this.#computeTimeout(next);
|
|
2150
|
+
if (this.#multicastSymbolDelivery === "ordered-relay") {
|
|
2151
|
+
this.#orderedHorizonBlocksFor(packetId, next);
|
|
2152
|
+
}
|
|
2153
|
+
if (previous.blockProfile !== void 0 && (previous.blockProfile !== blockProfile || blockProfile === "iso-ssbg1-v1" && previous.k !== next.k)) {
|
|
2154
|
+
this.#dropIsoLocators(packetId);
|
|
2155
|
+
}
|
|
2156
|
+
this.#trackConfigs.set(packetId, next);
|
|
2157
|
+
this.#refreshDeliveryGapWindow();
|
|
2158
|
+
let policyChanged = config.geometry !== void 0 && JSON.stringify(config.geometry) !== JSON.stringify(previous.geometry) || config.trackKind !== void 0 && config.trackKind !== previous.trackKind;
|
|
2159
|
+
if (streamScopedGeometry !== void 0) {
|
|
2160
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
2161
|
+
if (JSON.stringify(streamScopedGeometry) !== JSON.stringify(this.#streamGeometries.get(streamKey))) {
|
|
2162
|
+
this.#streamGeometries.set(streamKey, streamScopedGeometry);
|
|
2163
|
+
this.#refreshDeliveryGapWindow();
|
|
2164
|
+
policyChanged = true;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
if (policyChanged) {
|
|
2168
|
+
this.#bumpEpoch(packetId);
|
|
2169
|
+
for (const [key, classifier] of [...this.#classifiers]) {
|
|
2170
|
+
if (!this.#belongsToPacket(key, packetId)) continue;
|
|
2171
|
+
const stream = this.#parseStreamKey(key);
|
|
2172
|
+
const geometry = this.#geometryFor(stream.packetId, stream.catalogTrackName);
|
|
2173
|
+
if (JSON.stringify(geometry) === JSON.stringify(classifier.geometry)) continue;
|
|
2174
|
+
this.#classifiers.delete(key);
|
|
2175
|
+
this.#applyClassifierChanges(stream.packetId, stream.catalogTrackName, classifier.retire());
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
return next;
|
|
2179
|
+
}
|
|
2180
|
+
#normalizeSymbolData(packetId, data) {
|
|
2181
|
+
const symbolSize = this.symbolSizeForPacket(packetId);
|
|
2182
|
+
if (symbolSize <= 0 || data.byteLength === symbolSize) return data;
|
|
2183
|
+
const normalized = new Uint8Array(symbolSize);
|
|
2184
|
+
normalized.set(data.subarray(0, symbolSize));
|
|
2185
|
+
return normalized;
|
|
2186
|
+
}
|
|
2187
|
+
#blockKeysFor(sbn, packetId, catalogTrackName) {
|
|
2188
|
+
if (packetId != null && catalogTrackName !== void 0) {
|
|
2189
|
+
return [this.#blockKey(sbn, packetId, catalogTrackName)];
|
|
2190
|
+
}
|
|
2191
|
+
const keys = [];
|
|
2192
|
+
const candidates = this.#blockKeysBySbn.get(sbn);
|
|
2193
|
+
if (candidates) {
|
|
2194
|
+
for (const [key, keyPacketId] of candidates) {
|
|
2195
|
+
if (packetId !== void 0 && keyPacketId !== packetId) continue;
|
|
2196
|
+
if (!this.#isBlockKeyHeld(key)) {
|
|
2197
|
+
candidates.delete(key);
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
keys.push(key);
|
|
2201
|
+
}
|
|
2202
|
+
if (candidates.size === 0) this.#blockKeysBySbn.delete(sbn);
|
|
2203
|
+
}
|
|
2204
|
+
if (packetId !== void 0) {
|
|
2205
|
+
if (keys.length === 0) return [this.#blockKey(sbn, packetId)];
|
|
2206
|
+
return keys.length === 1 ? keys : [];
|
|
2207
|
+
}
|
|
2208
|
+
return keys.length > 0 ? keys : [this.#blockKey(sbn, 0, catalogTrackName)];
|
|
2209
|
+
}
|
|
2210
|
+
// -----------------------------------------------------------------------
|
|
2211
|
+
// Accessors
|
|
2212
|
+
// -----------------------------------------------------------------------
|
|
2213
|
+
/** Internal state for diagnostics — not part of the public stats API. */
|
|
2214
|
+
get diagnostics() {
|
|
2215
|
+
return {
|
|
2216
|
+
timeoutMs: this.#fecTimeoutMs,
|
|
2217
|
+
activeBlocks: this.#activeBlocks.size,
|
|
2218
|
+
completedBlocks: this.#completedBlocks.size,
|
|
2219
|
+
cleanlyCompletedBlocks: this.#cleanlyCompletedBlocks.size,
|
|
2220
|
+
pendingCounts: Object.fromEntries(this.#blockSourceCounts),
|
|
2221
|
+
fpsDetected: this.#fpsDetected,
|
|
2222
|
+
interleaveDepthMs: this.#fecInterleaveDepthMs,
|
|
2223
|
+
startupAlignedPackets: this.#packetIdsForStreamKeys(this.#startupAlignedPackets),
|
|
2224
|
+
startupPartialBlocks: this.#startupPartialBlocks.size,
|
|
2225
|
+
startupRepairReadyPackets: this.#packetIdsForStreamKeys(this.#startupRepairReadyPackets),
|
|
2226
|
+
startupRepairFailOpenPackets: this.#packetIdsForStreamKeys(this.#startupRepairFailOpenPackets),
|
|
2227
|
+
pendingWorkerCleanups: this.#pendingWorkerCleanups.size,
|
|
2228
|
+
decoderUnavailableEvents: this.#decoderUnavailableEvents,
|
|
2229
|
+
repairTrackedBlocks: this.#blockRepairRsIds.size,
|
|
2230
|
+
multicastSymbolDelivery: this.#multicastSymbolDelivery,
|
|
2231
|
+
orderedFrontiers: Object.fromEntries(this.#orderedFrontiers),
|
|
2232
|
+
orderedPendingBlocks: [...this.#orderedPendingBlocks.values()].reduce((total, pending) => total + pending.size, 0),
|
|
2233
|
+
lifecycleTrace: this.#lifecycleTrace.map((entry) => ({ ...entry }))
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
get fecStats() {
|
|
2237
|
+
const s = this.#fecStats;
|
|
2238
|
+
const affected = s.blocksRecovered + s.blocksFailed;
|
|
2239
|
+
const total = s.blocksClean + affected;
|
|
2240
|
+
const tracks = [...this.#fecTrackStats.entries()].sort(([left], [right]) => left - right).map(([packetId, stats]) => {
|
|
2241
|
+
const trackAffected = stats.blocksRecovered + stats.blocksFailed;
|
|
2242
|
+
const trackTotal = stats.blocksClean + trackAffected;
|
|
2243
|
+
return {
|
|
2244
|
+
...stats,
|
|
2245
|
+
packetId,
|
|
2246
|
+
blocksComplete: trackTotal,
|
|
2247
|
+
k: this.#resolveTrackConfig(packetId).k,
|
|
2248
|
+
recoveryRate: trackAffected > 0 ? stats.blocksRecovered / trackAffected * 100 : 100,
|
|
2249
|
+
packetLossRate: trackTotal > 0 ? trackAffected / trackTotal * 100 : 0
|
|
2250
|
+
};
|
|
2251
|
+
});
|
|
2252
|
+
return {
|
|
2253
|
+
...s,
|
|
2254
|
+
blocksComplete: total,
|
|
2255
|
+
mixedPathBlocks: Array.from(this.#blockPathCounts.values()).filter(
|
|
2256
|
+
(block) => block.multicastSymbols > 0 && block.moqSymbols > 0
|
|
2257
|
+
).length,
|
|
2258
|
+
recentBlockPathCounts: Array.from(this.#blockPathCounts.values()).slice(-24),
|
|
2259
|
+
k: this.#fecK,
|
|
2260
|
+
// True recovery rate: of blocks that HAD loss, how many were recovered?
|
|
2261
|
+
recoveryRate: affected > 0 ? s.blocksRecovered / affected * 100 : 100,
|
|
2262
|
+
packetLossRate: total > 0 ? affected / total * 100 : 0,
|
|
2263
|
+
tracks,
|
|
2264
|
+
workers: [...this.#fecWorkerStats.entries()].sort(([left], [right]) => left - right).map(([packetId, stats]) => ({
|
|
2265
|
+
...stats,
|
|
2266
|
+
config: {
|
|
2267
|
+
...stats.config,
|
|
2268
|
+
resolution: stats.config.resolution ? { ...stats.config.resolution } : void 0
|
|
2269
|
+
},
|
|
2270
|
+
blocks: stats.blocks.map((block) => ({ ...block })),
|
|
2271
|
+
decoder: stats.decoder ? { ...stats.decoder } : void 0,
|
|
2272
|
+
altaFailedDetails: stats.altaFailedDetails?.map((detail) => ({ ...detail })),
|
|
2273
|
+
packetId
|
|
2274
|
+
})),
|
|
2275
|
+
repairSymbolSizeMismatches: this.#repairSymbolSizeMismatchCount,
|
|
2276
|
+
workerErrors: Object.fromEntries(this.#workerErrorCounts)
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Observe Worker-backed per-track snapshots without taking ownership of the
|
|
2281
|
+
* legacy single-callback slot. Infrastructure diagnostics and a player may
|
|
2282
|
+
* therefore consume the same source event without wrapping or replacing one
|
|
2283
|
+
* another's callback.
|
|
2284
|
+
*/
|
|
2285
|
+
addFecStatsObserver(observer) {
|
|
2286
|
+
this.#fecStatsObservers.add(observer);
|
|
2287
|
+
return () => this.#fecStatsObservers.delete(observer);
|
|
2288
|
+
}
|
|
2289
|
+
/** Start a new multicast transport join without discarding track/decoder state. */
|
|
2290
|
+
beginMulticastJoin() {
|
|
2291
|
+
this.#orderedFrontiers.clear();
|
|
2292
|
+
this.#startupAlignedPackets.clear();
|
|
2293
|
+
this.#startupPartialBlocks.clear();
|
|
2294
|
+
this.#startupRequireRepairReady = true;
|
|
2295
|
+
this.#startupRepairReadyPackets.clear();
|
|
2296
|
+
this.#startupRepairBoundaryCounts.clear();
|
|
2297
|
+
this.#startupRepairFailOpenPackets.clear();
|
|
2298
|
+
}
|
|
2299
|
+
/** How the multicast leg currently delivers symbols. */
|
|
2300
|
+
get multicastSymbolDelivery() {
|
|
2301
|
+
return this.#multicastSymbolDelivery;
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Declare how the multicast leg delivers symbols (see
|
|
2305
|
+
* MulticastSymbolDelivery). The multicast provider owns this fact:
|
|
2306
|
+
* MulticastManager applies what the window.multicast subscription declares
|
|
2307
|
+
* on every join, and a consumer feeding packets itself declares its own leg.
|
|
2308
|
+
*
|
|
2309
|
+
* Switching to 'ordered-relay' requires every configured track to define
|
|
2310
|
+
* its interleave horizon and throws otherwise. Switching back to 'datagram'
|
|
2311
|
+
* gives every block awaiting ordered settlement the datagram inactivity
|
|
2312
|
+
* deadline from now.
|
|
2313
|
+
*/
|
|
2314
|
+
setMulticastSymbolDelivery(delivery) {
|
|
2315
|
+
if (!isMulticastSymbolDelivery(delivery)) {
|
|
2316
|
+
throw new Error(
|
|
2317
|
+
`[FECManager] multicast symbol delivery must be 'datagram' or 'ordered-relay', got ${String(delivery)}`
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
if (delivery === this.#multicastSymbolDelivery) return;
|
|
2321
|
+
this.#deliveryGaps.reset();
|
|
2322
|
+
if (delivery === "ordered-relay") {
|
|
2323
|
+
for (const [packetId, config] of this.#trackConfigs) {
|
|
2324
|
+
this.#orderedHorizonBlocksFor(packetId, config);
|
|
2325
|
+
}
|
|
2326
|
+
this.#multicastSymbolDelivery = delivery;
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
this.#multicastSymbolDelivery = delivery;
|
|
2330
|
+
const pending = [...this.#orderedPendingBlocks.values()].flatMap((blocks) => [...blocks.values()]);
|
|
2331
|
+
this.#orderedPendingBlocks.clear();
|
|
2332
|
+
this.#orderedFrontiers.clear();
|
|
2333
|
+
for (const key of pending) {
|
|
2334
|
+
const block = this.#parseBlockKey(key);
|
|
2335
|
+
this.noteBlockActivity(block.sbn, block.packetId, block.catalogTrackName);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
/** Begin a fresh decode epoch for one physical stream. */
|
|
2339
|
+
resetTrack(packetId, catalogTrackName) {
|
|
2340
|
+
if (!Number.isInteger(packetId) || packetId < 0) {
|
|
2341
|
+
throw new Error(`[FECManager] invalid packetId for reset: ${packetId}`);
|
|
2342
|
+
}
|
|
2343
|
+
if (!catalogTrackName) {
|
|
2344
|
+
throw new Error("[FECManager] catalogTrackName is required for track reset");
|
|
2345
|
+
}
|
|
2346
|
+
const resetMap = (map) => {
|
|
2347
|
+
for (const key of map.keys()) {
|
|
2348
|
+
if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue;
|
|
2349
|
+
map.delete(key);
|
|
2350
|
+
}
|
|
2351
|
+
};
|
|
2352
|
+
const resetTimerMap = (map) => {
|
|
2353
|
+
for (const [key, timer] of map) {
|
|
2354
|
+
if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue;
|
|
2355
|
+
if (timer?.handle != null) clearTimeout(timer.handle);
|
|
2356
|
+
map.delete(key);
|
|
2357
|
+
}
|
|
2358
|
+
};
|
|
2359
|
+
const resetSet = (set) => {
|
|
2360
|
+
for (const key of set) {
|
|
2361
|
+
if (this.#belongsToStream(key, packetId, catalogTrackName)) set.delete(key);
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
resetTimerMap(this.#activeBlocks);
|
|
2365
|
+
resetTimerMap(this.#repairDeadlines);
|
|
2366
|
+
resetMap(this.#activeBlockDeadlinesMs);
|
|
2367
|
+
resetMap(this.#blockWaitDeadlinesMs);
|
|
2368
|
+
resetMap(this.#repairHoldUntilMs);
|
|
2369
|
+
resetMap(this.#repairDeadlineAtMs);
|
|
2370
|
+
resetMap(this.#terminalExpiryMs);
|
|
2371
|
+
resetMap(this.#pendingWorkerCleanupClients);
|
|
2372
|
+
for (const key of [...this.#pendingWorkerCleanupBounds.keys()]) {
|
|
2373
|
+
if (this.#belongsToStream(key, packetId, catalogTrackName)) this.#clearOwedCleanupBound(key);
|
|
2374
|
+
}
|
|
2375
|
+
resetMap(this.#blockSourceCounts);
|
|
2376
|
+
resetMap(this.#blockSourceEsis);
|
|
2377
|
+
resetMap(this.#blockAuthoredLengths);
|
|
2378
|
+
resetMap(this.#blockRepairRsIds);
|
|
2379
|
+
resetMap(this.#blockPathCounts);
|
|
2380
|
+
resetMap(this.#blockAnchorsMs);
|
|
2381
|
+
resetMap(this.#blockCatalogTrackNames);
|
|
2382
|
+
resetMap(this.#syncDecoderBlockOwners);
|
|
2383
|
+
resetMap(this.#startupRepairBoundaryCounts);
|
|
2384
|
+
resetMap(this.#orderedFrontiers);
|
|
2385
|
+
resetMap(this.#orderedPendingBlocks);
|
|
2386
|
+
resetSet(this.#pendingWorkerCleanups);
|
|
2387
|
+
resetSet(this.#completedBlocks);
|
|
2388
|
+
resetSet(this.#cleanlyCompletedBlocks);
|
|
2389
|
+
resetSet(this.#repairRequestedBlocks);
|
|
2390
|
+
resetSet(this.#workerRecoveredBlocks);
|
|
2391
|
+
resetSet(this.#startupAlignedPackets);
|
|
2392
|
+
resetSet(this.#startupPartialBlocks);
|
|
2393
|
+
resetSet(this.#startupRepairReadyPackets);
|
|
2394
|
+
resetSet(this.#startupRepairFailOpenPackets);
|
|
2395
|
+
resetSet(this.#seenSymbols);
|
|
2396
|
+
resetSet(this.#failedBlocks);
|
|
2397
|
+
resetSet(this.#repairedBlocks);
|
|
2398
|
+
resetSet(this.#materializedBlocks);
|
|
2399
|
+
resetMap(this.#resolvedBlockClasses);
|
|
2400
|
+
resetMap(this.#highestSbn);
|
|
2401
|
+
resetMap(this.#isoLocators);
|
|
2402
|
+
resetMap(this.#sourceAnchors);
|
|
2403
|
+
this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
|
|
2404
|
+
this.#bumpEpoch(packetId);
|
|
2405
|
+
for (const [sbn, keys] of this.#blockKeysBySbn) {
|
|
2406
|
+
for (const key of keys.keys()) {
|
|
2407
|
+
if (this.#belongsToStream(key, packetId, catalogTrackName)) keys.delete(key);
|
|
2408
|
+
}
|
|
2409
|
+
if (keys.size === 0) this.#blockKeysBySbn.delete(sbn);
|
|
2410
|
+
}
|
|
2411
|
+
const decoderClients = /* @__PURE__ */ new Set();
|
|
2412
|
+
for (const [key, entry] of this.#fecDecoders) {
|
|
2413
|
+
if (this.#belongsToStream(key, packetId, catalogTrackName)) {
|
|
2414
|
+
decoderClients.add(entry.client);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
const clients = /* @__PURE__ */ new Set();
|
|
2418
|
+
for (const client of decoderClients) {
|
|
2419
|
+
if (typeof client.flush === "function" && typeof client.reset_stats === "function") {
|
|
2420
|
+
try {
|
|
2421
|
+
client.flush();
|
|
2422
|
+
client.reset_stats();
|
|
2423
|
+
clients.add(client);
|
|
2424
|
+
} catch {
|
|
2425
|
+
this.#discardFecDecoderClient(client);
|
|
2426
|
+
}
|
|
2427
|
+
} else {
|
|
2428
|
+
this.#discardFecDecoderClient(client);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
for (const [key, client] of [...this.#workerClients]) {
|
|
2432
|
+
if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue;
|
|
2433
|
+
if (client.disposed === true) {
|
|
2434
|
+
this.#evictDisposedWorker(client);
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
client.reset();
|
|
2438
|
+
clients.add(client);
|
|
2439
|
+
}
|
|
2440
|
+
for (const client of clients) this.#resetDecoderBlockOwners(client, catalogTrackName);
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Retire one packet ID, drop-only: forget every stream on it (block
|
|
2444
|
+
* ledgers, deadlines and their timers, repair holds, terminal records,
|
|
2445
|
+
* classifiers, stream geometry), its per-packet configuration and
|
|
2446
|
+
* statistics, and take its decoders out of routing.
|
|
2447
|
+
*
|
|
2448
|
+
* This is the end of the packet ID's streams, not a new epoch of them (that
|
|
2449
|
+
* is resetTrack): afterwards isTrackConfigured() is false and nothing about
|
|
2450
|
+
* it remains until it is configured again. Nothing is reported, because
|
|
2451
|
+
* nothing happened to the media: no block fails, recovers or resolves, no
|
|
2452
|
+
* repair resolves, and no decoder becomes "unavailable" (a retired stream
|
|
2453
|
+
* has no symbols left to decode, so a decoder-unavailable event would
|
|
2454
|
+
* describe lost protection that no stream suffers). The epoch still
|
|
2455
|
+
* advances, so a gap decision made before the retirement never matches a
|
|
2456
|
+
* stream later configured on the same packet ID.
|
|
2457
|
+
*
|
|
2458
|
+
* Decoder and Worker clients stay caller-owned: each is flushed or reset so
|
|
2459
|
+
* it holds no block of the retired streams, then released from routing;
|
|
2460
|
+
* dispose it after this returns. Cumulative totals in fecStats keep what
|
|
2461
|
+
* the retired streams contributed.
|
|
2462
|
+
*/
|
|
2463
|
+
releasePacket(packetId) {
|
|
2464
|
+
if (!Number.isInteger(packetId) || packetId < 0) {
|
|
2465
|
+
throw new Error(`[FECManager] invalid packetId for release: ${packetId}`);
|
|
2466
|
+
}
|
|
2467
|
+
const onPacket = (key) => this.#belongsToPacket(key, packetId);
|
|
2468
|
+
const dropMap = (map) => {
|
|
2469
|
+
for (const key of [...map.keys()]) if (onPacket(key)) map.delete(key);
|
|
2470
|
+
};
|
|
2471
|
+
const dropTimerMap = (map) => {
|
|
2472
|
+
for (const [key, timer] of [...map]) {
|
|
2473
|
+
if (!onPacket(key)) continue;
|
|
2474
|
+
if (timer?.handle != null) clearTimeout(timer.handle);
|
|
2475
|
+
map.delete(key);
|
|
2476
|
+
}
|
|
2477
|
+
};
|
|
2478
|
+
const dropSet = (set) => {
|
|
2479
|
+
for (const key of [...set]) if (onPacket(key)) set.delete(key);
|
|
2480
|
+
};
|
|
2481
|
+
dropTimerMap(this.#activeBlocks);
|
|
2482
|
+
dropTimerMap(this.#repairDeadlines);
|
|
2483
|
+
for (const key of [...this.#pendingWorkerCleanupBounds.keys()]) {
|
|
2484
|
+
if (onPacket(key)) this.#clearOwedCleanupBound(key);
|
|
2485
|
+
}
|
|
2486
|
+
dropMap(this.#activeBlockDeadlinesMs);
|
|
2487
|
+
dropMap(this.#blockWaitDeadlinesMs);
|
|
2488
|
+
dropMap(this.#repairHoldUntilMs);
|
|
2489
|
+
dropMap(this.#repairDeadlineAtMs);
|
|
2490
|
+
dropMap(this.#terminalExpiryMs);
|
|
2491
|
+
dropMap(this.#pendingWorkerCleanupClients);
|
|
2492
|
+
dropMap(this.#blockSourceCounts);
|
|
2493
|
+
dropMap(this.#blockSourceEsis);
|
|
2494
|
+
dropMap(this.#blockAuthoredLengths);
|
|
2495
|
+
dropMap(this.#blockRepairRsIds);
|
|
2496
|
+
dropMap(this.#blockPathCounts);
|
|
2497
|
+
dropMap(this.#blockAnchorsMs);
|
|
2498
|
+
dropMap(this.#blockCatalogTrackNames);
|
|
2499
|
+
dropMap(this.#syncDecoderBlockOwners);
|
|
2500
|
+
dropMap(this.#startupRepairBoundaryCounts);
|
|
2501
|
+
dropMap(this.#orderedFrontiers);
|
|
2502
|
+
dropMap(this.#orderedPendingBlocks);
|
|
2503
|
+
dropMap(this.#resolvedBlockClasses);
|
|
2504
|
+
dropMap(this.#highestSbn);
|
|
2505
|
+
dropMap(this.#isoLocators);
|
|
2506
|
+
dropMap(this.#sourceAnchors);
|
|
2507
|
+
dropMap(this.#classifiers);
|
|
2508
|
+
dropMap(this.#streamGeometries);
|
|
2509
|
+
dropSet(this.#pendingWorkerCleanups);
|
|
2510
|
+
dropSet(this.#completedBlocks);
|
|
2511
|
+
dropSet(this.#cleanlyCompletedBlocks);
|
|
2512
|
+
dropSet(this.#repairRequestedBlocks);
|
|
2513
|
+
dropSet(this.#workerRecoveredBlocks);
|
|
2514
|
+
dropSet(this.#startupAlignedPackets);
|
|
2515
|
+
dropSet(this.#startupPartialBlocks);
|
|
2516
|
+
dropSet(this.#startupRepairReadyPackets);
|
|
2517
|
+
dropSet(this.#startupRepairFailOpenPackets);
|
|
2518
|
+
dropSet(this.#seenSymbols);
|
|
2519
|
+
dropSet(this.#failedBlocks);
|
|
2520
|
+
dropSet(this.#repairedBlocks);
|
|
2521
|
+
dropSet(this.#materializedBlocks);
|
|
2522
|
+
for (const [sbn, keys] of [...this.#blockKeysBySbn]) {
|
|
2523
|
+
for (const [key, keyPacketId] of [...keys]) {
|
|
2524
|
+
if (keyPacketId === packetId) keys.delete(key);
|
|
2525
|
+
}
|
|
2526
|
+
if (keys.size === 0) this.#blockKeysBySbn.delete(sbn);
|
|
2527
|
+
}
|
|
2528
|
+
this.#terminalQueues.delete(packetId);
|
|
2529
|
+
this.#trackConfigs.delete(packetId);
|
|
2530
|
+
this.#fecTrackStats.delete(packetId);
|
|
2531
|
+
this.#fecWorkerStats.delete(packetId);
|
|
2532
|
+
this.#bumpEpoch(packetId);
|
|
2533
|
+
this.#refreshDeliveryGapWindow();
|
|
2534
|
+
for (const [key, entry] of [...this.#fecDecoders]) {
|
|
2535
|
+
if (!onPacket(key)) continue;
|
|
2536
|
+
this.#fecDecoders.delete(key);
|
|
2537
|
+
this.#decoderBlockOwners.delete(entry.client);
|
|
2538
|
+
try {
|
|
2539
|
+
entry.client.flush?.();
|
|
2540
|
+
} catch {
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
for (const [key, client] of [...this.#workerClients]) {
|
|
2544
|
+
if (!onPacket(key)) continue;
|
|
2545
|
+
this.#workerClients.delete(key);
|
|
2546
|
+
this.#decoderBlockOwners.delete(client);
|
|
2547
|
+
this.#releasedWorkerClients.add(client);
|
|
2548
|
+
try {
|
|
2549
|
+
if (client.disposed !== true) client.reset();
|
|
2550
|
+
} catch {
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
/** Record a transport symbol and reject later duplicates of the same packetId/SBN/ESI. */
|
|
2555
|
+
feedSymbolFromSource(packetId, sbn, esi, _data, source, catalogTrackName) {
|
|
2556
|
+
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
2557
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
2558
|
+
if (source === "multicast" && this.#multicastSymbolDelivery === "ordered-relay") {
|
|
2559
|
+
this.#advanceOrderedFrontier(streamKey, sbn, packetId, catalogTrackName);
|
|
2560
|
+
}
|
|
2561
|
+
const k = this.kForPacket(packetId);
|
|
2562
|
+
const sourceSymbols = this.#isIso(packetId) ? this.#isoLocatorFor(packetId, catalogTrackName).blockAt(sbn)?.length ?? k : k;
|
|
2563
|
+
const isSourceSymbol = esi >= 0 && esi < sourceSymbols;
|
|
2564
|
+
const isStartupPartial = this.#startupPartialBlocks.has(blockKey);
|
|
2565
|
+
const isReadyMulticastRepair = this.#startupRequireRepairReady && source === "multicast" && !isSourceSymbol && this.#startupRepairReadyPackets.has(streamKey);
|
|
2566
|
+
const awaitingMulticastRepair = this.#startupRequireRepairReady && source === "multicast" && !this.#startupRepairReadyPackets.has(streamKey) && !this.#startupRepairFailOpenPackets.has(streamKey);
|
|
2567
|
+
if (awaitingMulticastRepair) {
|
|
2568
|
+
if (!isSourceSymbol) {
|
|
2569
|
+
this.#startupRepairReadyPackets.add(streamKey);
|
|
2570
|
+
this.#startupRepairBoundaryCounts.delete(streamKey);
|
|
2571
|
+
this.#markStartupPartial(blockKey);
|
|
2572
|
+
return false;
|
|
2573
|
+
}
|
|
2574
|
+
if (esi === 0) {
|
|
2575
|
+
const boundaries = (this.#startupRepairBoundaryCounts.get(streamKey) ?? 0) + 1;
|
|
2576
|
+
this.#startupRepairBoundaryCounts.set(streamKey, boundaries);
|
|
2577
|
+
if (boundaries >= STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES) {
|
|
2578
|
+
this.#startupRepairBoundaryCounts.delete(streamKey);
|
|
2579
|
+
this.#startupRepairFailOpenPackets.add(streamKey);
|
|
2580
|
+
} else {
|
|
2581
|
+
this.#markStartupPartial(blockKey);
|
|
2582
|
+
return false;
|
|
2583
|
+
}
|
|
2584
|
+
} else {
|
|
2585
|
+
this.#markStartupPartial(blockKey);
|
|
2586
|
+
return false;
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
if (isSourceSymbol && esi === 0) {
|
|
2590
|
+
this.#startupPartialBlocks.delete(blockKey);
|
|
2591
|
+
this.#startupAlignedPackets.add(streamKey);
|
|
2592
|
+
} else if (isStartupPartial && !isReadyMulticastRepair || !this.#startupAlignedPackets.has(streamKey) && !isReadyMulticastRepair) {
|
|
2593
|
+
this.#markStartupPartial(blockKey);
|
|
2594
|
+
return false;
|
|
2595
|
+
}
|
|
2596
|
+
if (this.#completedBlocks.has(blockKey)) {
|
|
2597
|
+
this.#fecStats.duplicateSymbols++;
|
|
2598
|
+
this.#statsForPacket(packetId).duplicateSymbols++;
|
|
2599
|
+
return false;
|
|
2600
|
+
}
|
|
2601
|
+
const symbolKey = `${blockKey}:${esi}`;
|
|
2602
|
+
if (this.#seenSymbols.has(symbolKey)) {
|
|
2603
|
+
this.#fecStats.duplicateSymbols++;
|
|
2604
|
+
this.#statsForPacket(packetId).duplicateSymbols++;
|
|
2605
|
+
return false;
|
|
2606
|
+
}
|
|
2607
|
+
this.#seenSymbols.add(symbolKey);
|
|
2608
|
+
const counts = this.#blockPathCounts.get(blockKey) ?? {
|
|
2609
|
+
packetId,
|
|
2610
|
+
sbn,
|
|
2611
|
+
...catalogTrackName === void 0 ? {} : { catalogTrackName },
|
|
2612
|
+
multicastSymbols: 0,
|
|
2613
|
+
moqSymbols: 0,
|
|
2614
|
+
complete: false
|
|
2615
|
+
};
|
|
2616
|
+
if (!this.#blockAnchorsMs.has(blockKey)) this.#blockAnchorsMs.set(blockKey, performance.now());
|
|
2617
|
+
if (source === "multicast") counts.multicastSymbols++;
|
|
2618
|
+
else counts.moqSymbols++;
|
|
2619
|
+
this.#blockPathCounts.set(blockKey, counts);
|
|
2620
|
+
this.#observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName);
|
|
2621
|
+
if (this.#seenSymbols.size > 4096) {
|
|
2622
|
+
dropOldest(this.#seenSymbols, this.#seenSymbols.size - 2048);
|
|
2623
|
+
}
|
|
2624
|
+
if (this.#blockPathCounts.size > 512) {
|
|
2625
|
+
dropOldest(this.#blockPathCounts, this.#blockPathCounts.size - 256);
|
|
2626
|
+
}
|
|
2627
|
+
if (this.#blockAnchorsMs.size > 512) {
|
|
2628
|
+
dropOldest(this.#blockAnchorsMs, this.#blockAnchorsMs.size - 256);
|
|
2629
|
+
}
|
|
2630
|
+
if (this.#blockCatalogTrackNames.size > 512) {
|
|
2631
|
+
for (const key of dropOldest(
|
|
2632
|
+
this.#blockCatalogTrackNames,
|
|
2633
|
+
this.#blockCatalogTrackNames.size - 256
|
|
2634
|
+
)) {
|
|
2635
|
+
this.#unindexBlockKey(key);
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
return true;
|
|
2639
|
+
}
|
|
2640
|
+
#observeBlockCatalogTrackName(blockKey, sbn, packetId, catalogTrackName) {
|
|
2641
|
+
if (catalogTrackName === void 0) return;
|
|
2642
|
+
if (!this.#blockCatalogTrackNames.has(blockKey)) {
|
|
2643
|
+
this.#blockCatalogTrackNames.set(blockKey, catalogTrackName);
|
|
2644
|
+
this.#indexBlockKey(blockKey, sbn, packetId);
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
if (this.#blockCatalogTrackNames.get(blockKey) !== catalogTrackName) {
|
|
2648
|
+
this.#blockCatalogTrackNames.set(blockKey, null);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
/** Exact catalog identity retained for async recovery, if unambiguous. */
|
|
2652
|
+
catalogTrackNameForBlock(sbn, packetId, catalogTrackName) {
|
|
2653
|
+
if (catalogTrackName !== void 0) {
|
|
2654
|
+
return this.#blockCatalogTrackNames.get(
|
|
2655
|
+
this.#blockKey(sbn, packetId, catalogTrackName)
|
|
2656
|
+
) ?? void 0;
|
|
2657
|
+
}
|
|
2658
|
+
const names = /* @__PURE__ */ new Set();
|
|
2659
|
+
for (const [key, name] of this.#blockCatalogTrackNames) {
|
|
2660
|
+
const block = this.#parseBlockKey(key);
|
|
2661
|
+
if (block.packetId === packetId && block.sbn === sbn && name !== null) names.add(name);
|
|
2662
|
+
}
|
|
2663
|
+
return names.size === 1 ? [...names][0] : void 0;
|
|
2664
|
+
}
|
|
2665
|
+
#markStartupPartial(blockKey) {
|
|
2666
|
+
this.#startupPartialBlocks.add(blockKey);
|
|
2667
|
+
if (this.#startupPartialBlocks.size > 128) {
|
|
2668
|
+
this.#startupPartialBlocks = new Set(
|
|
2669
|
+
Array.from(this.#startupPartialBlocks).slice(-64)
|
|
2670
|
+
);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
/** Per-block state snapshot for FecBlockGrid visualization.
|
|
2674
|
+
* Returns the most recent blocks (active + recently completed). */
|
|
2675
|
+
getBlockStates() {
|
|
2676
|
+
const blocks = [];
|
|
2677
|
+
for (const [key] of this.#activeBlocks) {
|
|
2678
|
+
const { packetId, sbn } = this.#parseBlockKey(key);
|
|
2679
|
+
const count = this.#blockSourceCounts.get(key) ?? 0;
|
|
2680
|
+
const k = this.#blockAuthoredLengths.get(key) ?? this.kForPacket(packetId);
|
|
2681
|
+
blocks.push({ sbn, packetId, state: "collecting", sourceCount: count, k, progress: k > 0 ? Math.min(count / k, 1) : 0 });
|
|
2682
|
+
}
|
|
2683
|
+
const completed = [...this.#completedBlocks].slice(-16);
|
|
2684
|
+
for (const key of completed) {
|
|
2685
|
+
const { packetId, sbn } = this.#parseBlockKey(key);
|
|
2686
|
+
if (!this.#activeBlocks.has(key)) {
|
|
2687
|
+
const k = this.kForPacket(packetId);
|
|
2688
|
+
blocks.push({ sbn, packetId, state: "complete", sourceCount: k, k, progress: 1 });
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
blocks.sort((a, b) => a.sbn - b.sbn || (a.packetId ?? 0) - (b.packetId ?? 0));
|
|
2692
|
+
return blocks.slice(-24);
|
|
2693
|
+
}
|
|
2694
|
+
/** The raw WASM decoder for stats access (first registered decoder) */
|
|
2695
|
+
get wasmDecoder() {
|
|
2696
|
+
return this.#fecDecoders.values().next().value?.client ?? null;
|
|
2697
|
+
}
|
|
2698
|
+
get k() {
|
|
2699
|
+
return this.#fecK;
|
|
2700
|
+
}
|
|
2701
|
+
kForPacket(packetId) {
|
|
2702
|
+
return this.#resolveTrackConfig(packetId).k;
|
|
2703
|
+
}
|
|
2704
|
+
/** FEC symbol size in bytes. */
|
|
2705
|
+
get symbolSize() {
|
|
2706
|
+
return this.#fecSymbolSize;
|
|
2707
|
+
}
|
|
2708
|
+
symbolSizeForPacket(packetId) {
|
|
2709
|
+
return this.#resolveTrackConfig(packetId).symbolSize;
|
|
2710
|
+
}
|
|
2711
|
+
/** FEC block span in milliseconds (0 = no interleaving). */
|
|
2712
|
+
get interleaveDepthMs() {
|
|
2713
|
+
return this.#fecInterleaveDepthMs;
|
|
2714
|
+
}
|
|
2715
|
+
interleaveDepthMsForPacket(packetId) {
|
|
2716
|
+
return this.#resolveTrackConfig(packetId).interleaveDepthMs;
|
|
2717
|
+
}
|
|
2718
|
+
/**
|
|
2719
|
+
* Record one multicast MMTP packet arrival (every packet of the leg, FEC
|
|
2720
|
+
* symbol or not) for the delivery-gap measurement. `packetSequenceNumber`
|
|
2721
|
+
* is the MMTP packet_sequence_number, which increments by one per packet of
|
|
2722
|
+
* a packet_id: it is what separates a stall from a loss (see
|
|
2723
|
+
* DeliveryGapEstimator). Datagram delivery only.
|
|
2724
|
+
*/
|
|
2725
|
+
noteMulticastArrival(packetId, packetSequenceNumber) {
|
|
2726
|
+
if (this.#multicastSymbolDelivery !== "datagram") return;
|
|
2727
|
+
this.#deliveryGaps.noteArrival(performance.now(), packetId, packetSequenceNumber);
|
|
2728
|
+
}
|
|
2729
|
+
/**
|
|
2730
|
+
* Resolved per-track multicast late-symbol wait in milliseconds: the span
|
|
2731
|
+
* plus the larger of the catalog tolerance and the measured delivery gap.
|
|
2732
|
+
*/
|
|
2733
|
+
timeoutMsForPacket(packetId) {
|
|
2734
|
+
return this.#multicastWaitMs(this.#resolveTrackConfig(packetId));
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* Largest multicast delivery gap measured over the current window (ms), and
|
|
2738
|
+
* that window (undefined: held since the last reset). Diagnostics.
|
|
2739
|
+
*/
|
|
2740
|
+
multicastDeliveryGap() {
|
|
2741
|
+
return {
|
|
2742
|
+
peakGapMs: this.#deliveryGaps.peakGapMs(performance.now()),
|
|
2743
|
+
windowMs: this.#deliveryGaps.windowMs
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Receiver-local time of a block's first accepted symbol, or undefined for
|
|
2748
|
+
* a block no symbol reached (one materialized because a later SBN arrived).
|
|
2749
|
+
*/
|
|
2750
|
+
blockAnchorAtMs(sbn, packetId, catalogTrackName) {
|
|
2751
|
+
return this.#blockAnchorsMs.get(this.#blockKey(sbn, packetId, catalogTrackName));
|
|
2752
|
+
}
|
|
2753
|
+
/** Absolute fail deadline of a still-open block, or undefined. */
|
|
2754
|
+
blockDeadlineAtMs(sbn, packetId, catalogTrackName) {
|
|
2755
|
+
return this.#activeBlockDeadlinesMs.get(this.#blockKey(sbn, packetId, catalogTrackName));
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Keep a block open until `untilAtMs` while a repair FETCH the scheduler
|
|
2759
|
+
* admitted is in flight. Never shortens the multicast wait already armed;
|
|
2760
|
+
* while set, the hold is the block's whole deadline, so no later symbol
|
|
2761
|
+
* extends it. Returns false when the block is no longer open (resolved, or
|
|
2762
|
+
* not yet tracked).
|
|
2763
|
+
*/
|
|
2764
|
+
holdBlockForRepair(sbn, packetId, catalogTrackName, untilAtMs) {
|
|
2765
|
+
if (!Number.isFinite(untilAtMs)) {
|
|
2766
|
+
throw new Error(`[FECManager] repair hold must be a finite time (got ${untilAtMs})`);
|
|
2767
|
+
}
|
|
2768
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
2769
|
+
if (!this.#activeBlocks.has(key)) return false;
|
|
2770
|
+
const previous = this.#repairHoldUntilMs.get(key);
|
|
2771
|
+
if (previous !== void 0 && previous >= untilAtMs) return true;
|
|
2772
|
+
this.#repairHoldUntilMs.set(
|
|
2773
|
+
key,
|
|
2774
|
+
Math.max(untilAtMs, this.#blockWaitDeadlinesMs.get(key) ?? -Infinity)
|
|
2775
|
+
);
|
|
2776
|
+
this.#applyBlockDeadline(key, sbn, packetId, catalogTrackName);
|
|
2777
|
+
return true;
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Drop a block's repair hold: its FETCH ended without delivering. The block
|
|
2781
|
+
* falls back to its multicast wait, which may already have passed.
|
|
2782
|
+
*/
|
|
2783
|
+
releaseRepairHold(sbn, packetId, catalogTrackName) {
|
|
2784
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
2785
|
+
if (!this.#repairHoldUntilMs.delete(key) || !this.#activeBlocks.has(key)) return;
|
|
2786
|
+
this.#applyBlockDeadline(key, sbn, packetId, catalogTrackName);
|
|
2787
|
+
}
|
|
2788
|
+
/** Resolved per-track D-28 repair request deadline in milliseconds. */
|
|
2789
|
+
repairDeadlineMsForPacket(packetId) {
|
|
2790
|
+
return this.#repairDeadlineMs(this.#resolveTrackConfig(packetId));
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* One block lifetime: the longest a block stays open after its last symbol
|
|
2794
|
+
* (inactivity fail bound plus D-28 repair grace).
|
|
2795
|
+
*/
|
|
2796
|
+
blockLifetimeMsForPacket(packetId) {
|
|
2797
|
+
return this.#blockLifetimeMs(this.#resolveTrackConfig(packetId));
|
|
2798
|
+
}
|
|
2799
|
+
/**
|
|
2800
|
+
* How long a terminal (clean, recovered or failed) block stays authoritative,
|
|
2801
|
+
* for late symbols and for the in-order release gate. It is also the gate's
|
|
2802
|
+
* hold ceiling (MmtpRouter's holdLimitMs): a retired block reads back as
|
|
2803
|
+
* untracked, which the gate counts as lost, so no frame may still be waiting
|
|
2804
|
+
* on a block that can retire. The window is one interleave depth for a gap's
|
|
2805
|
+
* last block to finish arriving, one block lifetime to fail it, and one more
|
|
2806
|
+
* for an owed Worker cleanup that can keep a failing block pending. Every
|
|
2807
|
+
* term comes from the track's catalog geometry; there is no separate cap.
|
|
2808
|
+
*/
|
|
2809
|
+
terminalRetentionMsForPacket(packetId) {
|
|
2810
|
+
const config = this.#resolveTrackConfig(packetId);
|
|
2811
|
+
return config.interleaveDepthMs + 2 * this.#blockLifetimeMs(config);
|
|
2812
|
+
}
|
|
2813
|
+
/** Remaining fetch budget for a specific repair-requested block. */
|
|
2814
|
+
repairFetchTimeoutMsForBlock(sbn, packetId, catalogTrackName) {
|
|
2815
|
+
const deadlineMs = this.#activeBlockDeadlinesMs.get(
|
|
2816
|
+
this.#blockKey(sbn, packetId, catalogTrackName)
|
|
2817
|
+
);
|
|
2818
|
+
if (deadlineMs !== void 0) {
|
|
2819
|
+
return Math.max(1, Math.ceil(deadlineMs - performance.now()));
|
|
2820
|
+
}
|
|
2821
|
+
const trackConfig = this.#resolveTrackConfig(packetId);
|
|
2822
|
+
return Math.max(1, trackConfig.timeoutMs - this.#repairDeadlineMs(trackConfig));
|
|
2823
|
+
}
|
|
2824
|
+
/** True once configureTrack/setFecDecoder/setFecWorker configured this packetId. */
|
|
2825
|
+
// -----------------------------------------------------------------------
|
|
2826
|
+
// iso-ssbg1-v1 block location (BLO-35597 iso:2/iso:3)
|
|
2827
|
+
// -----------------------------------------------------------------------
|
|
2828
|
+
/** The configured `fec.blockProfile` of a packetId (undefined until configured). */
|
|
2829
|
+
blockProfileForPacket(packetId) {
|
|
2830
|
+
return this.#trackConfigs.get(packetId)?.blockProfile;
|
|
2831
|
+
}
|
|
2832
|
+
#isIso(packetId) {
|
|
2833
|
+
return this.#trackConfigs.get(packetId)?.blockProfile === "iso-ssbg1-v1";
|
|
2834
|
+
}
|
|
2835
|
+
#isoLocatorFor(packetId, catalogTrackName) {
|
|
2836
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
2837
|
+
let locator = this.#isoLocators.get(streamKey);
|
|
2838
|
+
if (!locator) {
|
|
2839
|
+
locator = new IsoBlockLocator(this.kForPacket(packetId), this.#isoRetentionBlocks(packetId));
|
|
2840
|
+
this.#isoLocators.set(streamKey, locator);
|
|
2841
|
+
}
|
|
2842
|
+
return locator;
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* The locator a call is about to grow, with its retention brought up to
|
|
2846
|
+
* date: the horizon follows catalog timing, which a later configuration
|
|
2847
|
+
* (this track's or another video track's keyframe cadence) can change.
|
|
2848
|
+
*/
|
|
2849
|
+
#growingIsoLocatorFor(packetId, catalogTrackName) {
|
|
2850
|
+
const locator = this.#isoLocatorFor(packetId, catalogTrackName);
|
|
2851
|
+
locator.retentionBlocks = this.#isoRetentionBlocks(packetId);
|
|
2852
|
+
return locator;
|
|
2853
|
+
}
|
|
2854
|
+
#dropIsoLocators(packetId) {
|
|
2855
|
+
for (const key of [...this.#isoLocators.keys()]) {
|
|
2856
|
+
if (this.#belongsToPacket(key, packetId)) this.#isoLocators.delete(key);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* The iso-ssbg1-v1 locator horizon in blocks: every block the manager can
|
|
2861
|
+
* still hold terminal state for (`terminalRetentionMsForPacket`) over the
|
|
2862
|
+
* block span, plus the block being filled, measured with the shared
|
|
2863
|
+
* lower-bound metric so a retained block is never forgotten early. Every
|
|
2864
|
+
* term is the packet's catalog geometry, which configuration validated
|
|
2865
|
+
* (#upsertTrackConfig), so this never falls back: an underivable horizon
|
|
2866
|
+
* throws rather than leave the locator without retention.
|
|
2867
|
+
*/
|
|
2868
|
+
#isoRetentionBlocks(packetId) {
|
|
2869
|
+
const config = this.#trackConfigs.get(packetId);
|
|
2870
|
+
if (config?.blockProfile !== "iso-ssbg1-v1" || !(config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS)) {
|
|
2871
|
+
throw new Error(
|
|
2872
|
+
`[FECManager] packetId=${packetId} has no iso-ssbg1-v1 block span to derive block-location retention from`
|
|
2873
|
+
);
|
|
2874
|
+
}
|
|
2875
|
+
const blocks = Math.ceil(this.terminalRetentionMsForPacket(packetId) / config.interleaveDepthMs) + 1;
|
|
2876
|
+
if (!Number.isSafeInteger(blocks) || blocks <= 0) {
|
|
2877
|
+
throw new Error(`[FECManager] packetId=${packetId} iso-ssbg1-v1 retention of ${blocks} blocks is not derivable`);
|
|
2878
|
+
}
|
|
2879
|
+
return blocks;
|
|
2880
|
+
}
|
|
2881
|
+
/** Whether an iso-ssbg1-v1 protected packet fits one ssbg_mode1 symbol (len + 2 <= T). */
|
|
2882
|
+
isoSourceFits(packetId, packetLength) {
|
|
2883
|
+
return ssbg1FitsSymbol(packetLength, this.symbolSizeForPacket(packetId));
|
|
2884
|
+
}
|
|
2885
|
+
/**
|
|
2886
|
+
* Locate an iso-ssbg1-v1 source by range membership in a block a repair
|
|
2887
|
+
* already stated. Null while its block is unknown: hold it (holdIsoSource).
|
|
2888
|
+
*/
|
|
2889
|
+
locateIsoSource(packetId, catalogTrackName, ssId) {
|
|
2890
|
+
const located = this.#isoLocatorFor(packetId, catalogTrackName).locate(ssId);
|
|
2891
|
+
return located === null ? null : { sbn: located.block.start, esi: located.esi };
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* Hold a first-arrival iso-ssbg1-v1 source whose block no repair has stated
|
|
2895
|
+
* yet. Returns false for a later copy of an SS_ID already held.
|
|
2896
|
+
*/
|
|
2897
|
+
holdIsoSource(packetId, catalogTrackName, ssId, source) {
|
|
2898
|
+
const locator = this.#growingIsoLocatorFor(packetId, catalogTrackName);
|
|
2899
|
+
const evictedBefore = locator.heldEvicted;
|
|
2900
|
+
const held = locator.hold(ssId, source);
|
|
2901
|
+
const evicted = locator.heldEvicted - evictedBefore;
|
|
2902
|
+
if (evicted > 0) {
|
|
2903
|
+
const before = this.#isoHeldEvictedCount;
|
|
2904
|
+
this.#isoHeldEvictedCount += evicted;
|
|
2905
|
+
if (before < 5 || Math.floor(before / 100) !== Math.floor(this.#isoHeldEvictedCount / 100)) {
|
|
2906
|
+
console.warn(
|
|
2907
|
+
`[FECManager] packetId=${packetId} iso-ssbg1-v1 held-source cap (${locator.heldCapacity}) evicted ${evicted} unplaced source(s); SS_IDs are not consecutive (C.5.2)`
|
|
2908
|
+
);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
return held;
|
|
2912
|
+
}
|
|
2913
|
+
/**
|
|
2914
|
+
* Apply an iso-ssbg1-v1 repair symbol's `(SS_Start, SSB_length)`: states the
|
|
2915
|
+
* block, and hands back the held sources it (and any repair-less span in
|
|
2916
|
+
* front of it) now places. The caller feeds them as first-arrival sources,
|
|
2917
|
+
* then the repair symbol, the order they had on the wire. A repair that
|
|
2918
|
+
* contradicts a known block is refused (counted) and must be dropped.
|
|
2919
|
+
*/
|
|
2920
|
+
placeIsoRepair(packetId, catalogTrackName, ssStart, ssbLength) {
|
|
2921
|
+
const placement = this.#growingIsoLocatorFor(packetId, catalogTrackName).placeRepair(ssStart, ssbLength);
|
|
2922
|
+
if (placement.status !== "placed") {
|
|
2923
|
+
this.#isoRepairRefusedCount++;
|
|
2924
|
+
if (this.#isoRepairRefusedCount <= 5 || this.#isoRepairRefusedCount % 100 === 0) {
|
|
2925
|
+
console.warn(`[FECManager] packetId=${packetId} iso-ssbg1-v1 repair refused (${placement.status}): ${placement.reason}`);
|
|
2926
|
+
}
|
|
2927
|
+
return placement;
|
|
2928
|
+
}
|
|
2929
|
+
if (placement.span !== void 0) {
|
|
2930
|
+
const key = this.#blockKey(placement.span.block.start, packetId, catalogTrackName);
|
|
2931
|
+
if (!this.#blockAuthoredLengths.has(key)) this.#blockAuthoredLengths.set(key, placement.span.block.length);
|
|
2932
|
+
}
|
|
2933
|
+
if (placement.span !== void 0 && placement.span.placed.length === 0) {
|
|
2934
|
+
this.#materializeBlock(placement.span.block.start, packetId, catalogTrackName, true);
|
|
2935
|
+
}
|
|
2936
|
+
return placement;
|
|
2937
|
+
}
|
|
2938
|
+
/**
|
|
2939
|
+
* Resolution of the SS_IDs strictly between two released frames' SS_IDs on
|
|
2940
|
+
* an iso-ssbg1-v1 stream, for in-order frame release: null while a block
|
|
2941
|
+
* that could hold them is pending or not yet stated by a repair, else the
|
|
2942
|
+
* starts of the blocks that failed or vanished.
|
|
2943
|
+
*
|
|
2944
|
+
* `untracked` reads exactly as in the k-padded-v1 release gate
|
|
2945
|
+
* (MmtpRouter #createFecReleaseGate): a block holding either released
|
|
2946
|
+
* frame's SS_ID (`fromSsId` or `toSsId`) delivered that frame, so untracked
|
|
2947
|
+
* there means it resolved and retired; a block starting after the stream's frontier (`highestSbn`,
|
|
2948
|
+
* an SS_Start here) may still be heard from; a block at or behind the
|
|
2949
|
+
* frontier that no symbol of this manager ever reached vanished, and its
|
|
2950
|
+
* media is lost.
|
|
2951
|
+
*/
|
|
2952
|
+
isoSpanResolution(packetId, catalogTrackName, fromSsId, toSsId) {
|
|
2953
|
+
if (!serial32Precedes(fromSsId, toSsId) || serial32Distance(fromSsId, toSsId) <= 1) return [];
|
|
2954
|
+
const locator = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName));
|
|
2955
|
+
if (!locator || !locator.coversBetween(fromSsId, toSsId)) return null;
|
|
2956
|
+
const highest = this.highestSbn(packetId, catalogTrackName);
|
|
2957
|
+
const lost = [];
|
|
2958
|
+
for (const block of locator.blocksBetween(fromSsId, toSsId)) {
|
|
2959
|
+
const resolution = this.blockResolution(block.start, packetId, catalogTrackName);
|
|
2960
|
+
if (resolution === "pending") return null;
|
|
2961
|
+
if (resolution === "failed") {
|
|
2962
|
+
lost.push(block.start);
|
|
2963
|
+
} else if (resolution === "untracked") {
|
|
2964
|
+
if (isoBlockContains(block.start, block.length, fromSsId) || isoBlockContains(block.start, block.length, toSsId)) {
|
|
2965
|
+
continue;
|
|
2966
|
+
}
|
|
2967
|
+
if (highest === void 0 || serial32Precedes(highest, block.start)) return null;
|
|
2968
|
+
lost.push(block.start);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
return lost;
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* iso-ssbg1-v1 block location: repair symbols it refused, oversize sources,
|
|
2975
|
+
* the sources held now for a block no repair has stated yet, and the held
|
|
2976
|
+
* sources the capacity cap evicted (cumulative), and the sources a
|
|
2977
|
+
* synchronous decoder held unplaced and retired at its horizon (cumulative).
|
|
2978
|
+
*/
|
|
2979
|
+
get isoDiagnostics() {
|
|
2980
|
+
let heldSources = 0;
|
|
2981
|
+
for (const locator of this.#isoLocators.values()) heldSources += locator.heldCount;
|
|
2982
|
+
return {
|
|
2983
|
+
repairRefused: this.#isoRepairRefusedCount,
|
|
2984
|
+
sourceOversize: this.#isoSourceOversizeCount,
|
|
2985
|
+
heldSources,
|
|
2986
|
+
heldEvicted: this.#isoHeldEvictedCount,
|
|
2987
|
+
decoderUnplacedRetired: this.#isoDecoderUnplacedRetired,
|
|
2988
|
+
decoderUnplacedRetireFailed: this.#isoDecoderUnplacedRetireFailed
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
isTrackConfigured(packetId) {
|
|
2992
|
+
return this.#trackConfigs.has(packetId);
|
|
2993
|
+
}
|
|
2994
|
+
/** Catalog/reset epoch of a packet's streams; gap decisions carry it. */
|
|
2995
|
+
epochFor(packetId) {
|
|
2996
|
+
return this.#packetEpochs.get(packetId) ?? 0;
|
|
2997
|
+
}
|
|
2998
|
+
/** Configured media kind of a packetId, when known. */
|
|
2999
|
+
trackKindFor(packetId) {
|
|
3000
|
+
return this.#trackConfigs.get(packetId)?.trackKind;
|
|
3001
|
+
}
|
|
3002
|
+
/** Configured codec of a packetId, when known. */
|
|
3003
|
+
codecFor(packetId) {
|
|
3004
|
+
return this.#trackConfigs.get(packetId)?.codec;
|
|
3005
|
+
}
|
|
3006
|
+
/**
|
|
3007
|
+
* Block classifier geometry of one stream: its own stream-scoped geometry,
|
|
3008
|
+
* else the packet's (fail-closed when absent, or when a sibling stream of
|
|
3009
|
+
* the packet has its own geometry and this one has none).
|
|
3010
|
+
*/
|
|
3011
|
+
geometryFor(packetId, catalogTrackName) {
|
|
3012
|
+
return this.#geometryFor(packetId, catalogTrackName);
|
|
3013
|
+
}
|
|
3014
|
+
/** Current classifier mode of a stream. */
|
|
3015
|
+
classifierModeFor(packetId, catalogTrackName) {
|
|
3016
|
+
const classifier = this.#classifierFor(packetId, catalogTrackName);
|
|
3017
|
+
return { mode: classifier.mode, reason: classifier.reason };
|
|
3018
|
+
}
|
|
3019
|
+
/**
|
|
3020
|
+
* Whether an MPU boundary still bounds this stream's RAPs (see
|
|
3021
|
+
* `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
|
|
3022
|
+
* it does not. A stream with no classifier yet has shown nothing against it.
|
|
3023
|
+
*/
|
|
3024
|
+
mpuBoundsRapsFor(packetId, catalogTrackName) {
|
|
3025
|
+
return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
|
|
3026
|
+
}
|
|
3027
|
+
/** A block's class now, or frozen at its resolution. */
|
|
3028
|
+
blockClassFor(sbn, packetId, catalogTrackName) {
|
|
3029
|
+
const frozen = this.#resolvedBlockClasses.get(this.#blockKey(sbn, packetId, catalogTrackName));
|
|
3030
|
+
if (frozen !== void 0) return frozen;
|
|
3031
|
+
return this.#peekClass(sbn, packetId, catalogTrackName);
|
|
3032
|
+
}
|
|
3033
|
+
/**
|
|
3034
|
+
* Lifecycle resolution of one block for in-order frame release: `pending`
|
|
3035
|
+
* while it can still complete, `complete`/`failed` once terminal, and
|
|
3036
|
+
* `untracked` when no symbol of it was ever accepted.
|
|
3037
|
+
*/
|
|
3038
|
+
blockResolution(sbn, packetId, catalogTrackName) {
|
|
3039
|
+
return this.#blockResolutionFor(this.#blockKey(sbn, packetId, catalogTrackName));
|
|
3040
|
+
}
|
|
3041
|
+
/** Highest SBN any symbol of this stream carried in the current epoch. */
|
|
3042
|
+
highestSbn(packetId, catalogTrackName) {
|
|
3043
|
+
return this.#highestSbn.get(this.#streamKey(packetId, catalogTrackName))?.sbn;
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Record a RAP frame released in order, for the classifier's RAP-spacing
|
|
3047
|
+
* invariant (consecutive MPUs exactly one keyframe interval apart).
|
|
3048
|
+
*/
|
|
3049
|
+
noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
|
|
3050
|
+
if (!this.#trackConfigs.has(packetId)) return;
|
|
3051
|
+
this.#applyClassifierChanges(
|
|
3052
|
+
packetId,
|
|
3053
|
+
catalogTrackName,
|
|
3054
|
+
this.#classifierFor(packetId, catalogTrackName).noteReleasedRap(
|
|
3055
|
+
mpu,
|
|
3056
|
+
presentationTime,
|
|
3057
|
+
mediaTimescale
|
|
3058
|
+
)
|
|
3059
|
+
);
|
|
3060
|
+
}
|
|
3061
|
+
// -----------------------------------------------------------------------
|
|
3062
|
+
// Configuration
|
|
3063
|
+
// -----------------------------------------------------------------------
|
|
3064
|
+
/** Configure FEC parameters. */
|
|
3065
|
+
configure(config) {
|
|
3066
|
+
if (config.k != null) this.#fecK = config.k;
|
|
3067
|
+
if (config.symbolSize != null && config.symbolSize > 0) this.#fecSymbolSize = config.symbolSize;
|
|
3068
|
+
if (config.interleaveDepthMs != null) this.#fecInterleaveDepthMs = config.interleaveDepthMs;
|
|
3069
|
+
if (config.fps != null && Number.isFinite(config.fps) && config.fps > 0) {
|
|
3070
|
+
this.#fecFps = config.fps;
|
|
3071
|
+
}
|
|
3072
|
+
if (config.interleaveDepthMs != null && config.interleaveDepthMs > 0 && config.interleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS) {
|
|
3073
|
+
console.warn(`[FECManager] interleaveDepthMs=${config.interleaveDepthMs} looks like frame count, not milliseconds (\xA75.1 requires ms). Expected range: ${MIN_INTERLEAVE_DEPTH_MS}-10000ms.`);
|
|
3074
|
+
}
|
|
3075
|
+
if (config.timeoutMs != null && config.timeoutMs > 0) {
|
|
3076
|
+
this.#fecTimeoutMs = config.timeoutMs;
|
|
3077
|
+
} else if (config.interleaveDepthMs != null && config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) {
|
|
3078
|
+
this.#fecTimeoutMs = computeFecDeliveryWindowMs({
|
|
3079
|
+
interleaveDepthMs: config.interleaveDepthMs,
|
|
3080
|
+
sourceSymbols: this.#fecK,
|
|
3081
|
+
symbolSize: this.#fecSymbolSize,
|
|
3082
|
+
mediaUnitsPerSecond: config.fps
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
3085
|
+
for (const [packetId, track] of this.#trackConfigs) {
|
|
3086
|
+
if (track.timeoutMs < this.#fecTimeoutMs) {
|
|
3087
|
+
this.#trackConfigs.set(packetId, { ...track, timeoutMs: this.#fecTimeoutMs });
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
/**
|
|
3092
|
+
* Configure FEC parameters for one packetId without changing legacy
|
|
3093
|
+
* defaults. With `catalogTrackName`, `config.geometry` is that stream's
|
|
3094
|
+
* own classifier geometry (ABR rungs that share a packetId each get
|
|
3095
|
+
* theirs); K, T, timing, kind and codec stay per packetId.
|
|
3096
|
+
*/
|
|
3097
|
+
configureTrack(packetId, config, catalogTrackName) {
|
|
3098
|
+
this.#upsertTrackConfig(packetId, config, catalogTrackName);
|
|
3099
|
+
}
|
|
3100
|
+
/** Register a FEC decoder for a specific packetId (track). */
|
|
3101
|
+
setFecDecoder(packetId, client, k, config, catalogTrackName) {
|
|
3102
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
3103
|
+
this.#assertExclusiveDecoderClient(
|
|
3104
|
+
streamKey,
|
|
3105
|
+
client,
|
|
3106
|
+
[...this.#fecDecoders].map(([key, entry]) => [key, entry.client]),
|
|
3107
|
+
"decoder"
|
|
3108
|
+
);
|
|
3109
|
+
const trackConfig = this.#upsertTrackConfig(packetId, { ...config, k }, catalogTrackName);
|
|
3110
|
+
this.#fecDecoders.set(streamKey, { client, k: trackConfig.k });
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* Register a Worker-backed FEC decoder for a specific packetId (track).
|
|
3114
|
+
*
|
|
3115
|
+
* Worker-backed tracks decode asynchronously — feedSourceSymbol/feedRepairSymbol
|
|
3116
|
+
* return null immediately, and recovered frames arrive via the onBlockRecovered callback.
|
|
3117
|
+
*
|
|
3118
|
+
* Takes precedence over sync WASM decoders for the same packetId.
|
|
3119
|
+
*/
|
|
3120
|
+
setFecWorker(packetId, client, config, catalogTrackName) {
|
|
3121
|
+
const k = config?.k;
|
|
3122
|
+
if (k === void 0 || !Number.isFinite(k) || k <= 0) {
|
|
3123
|
+
throw new Error(`[FECManager] packetId=${packetId} Worker requires an explicit positive k`);
|
|
3124
|
+
}
|
|
3125
|
+
const symbolSize = config.symbolSize;
|
|
3126
|
+
if (symbolSize === void 0 || !Number.isFinite(symbolSize) || symbolSize <= 0) {
|
|
3127
|
+
throw new Error(
|
|
3128
|
+
`[FECManager] packetId=${packetId} Worker requires an explicit positive symbolSize`
|
|
3129
|
+
);
|
|
3130
|
+
}
|
|
3131
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
3132
|
+
this.#assertExclusiveDecoderClient(
|
|
3133
|
+
streamKey,
|
|
3134
|
+
client,
|
|
3135
|
+
this.#workerClients,
|
|
3136
|
+
"worker"
|
|
3137
|
+
);
|
|
3138
|
+
const incumbent = this.#workerClients.get(streamKey);
|
|
3139
|
+
if (incumbent !== void 0 && incumbent !== client) {
|
|
3140
|
+
throw new Error(
|
|
3141
|
+
`[FECManager] stream ${streamKey} already routes to a Worker; call clearFecWorker before registering another`
|
|
3142
|
+
);
|
|
3143
|
+
}
|
|
3144
|
+
if (config) this.#upsertTrackConfig(packetId, config, catalogTrackName);
|
|
3145
|
+
const workerK = this.#resolveTrackConfig(packetId).k;
|
|
3146
|
+
this.#workerClients.set(streamKey, client);
|
|
3147
|
+
this.#releasedWorkerClients.delete(client);
|
|
3148
|
+
client.onFrame((data, meta) => {
|
|
3149
|
+
if (this.#releasedWorkerClients.has(client)) return;
|
|
3150
|
+
if (meta.recovered) {
|
|
3151
|
+
if (!Number.isInteger(meta.ssbLength) || meta.ssbLength <= 0 || meta.ssbLength > workerK) {
|
|
3152
|
+
const kind = "recovered-symbol-ssb-length-invalid";
|
|
3153
|
+
const seen = this.#workerErrorCounts.get(kind) ?? 0;
|
|
3154
|
+
this.#workerErrorCounts.set(kind, seen + 1);
|
|
3155
|
+
if (seen < WORKER_ERROR_LOG_CAP) {
|
|
3156
|
+
console.warn(
|
|
3157
|
+
`[FECManager] Dropping recovered symbol with invalid SSB_length: pid=${packetId} sbn=${meta.sbn} ssbLength=${String(meta.ssbLength)}`
|
|
3158
|
+
);
|
|
3159
|
+
}
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
if (!Number.isInteger(meta.esi) || meta.esi < 0 || meta.esi >= meta.ssbLength) {
|
|
3163
|
+
const kind = "recovered-symbol-missing-esi";
|
|
3164
|
+
const seen = this.#workerErrorCounts.get(kind) ?? 0;
|
|
3165
|
+
this.#workerErrorCounts.set(kind, seen + 1);
|
|
3166
|
+
if (seen < WORKER_ERROR_LOG_CAP) {
|
|
3167
|
+
console.warn(
|
|
3168
|
+
`[FECManager] Dropping recovered symbol without valid ESI: pid=${packetId} sbn=${meta.sbn} esi=${String(meta.esi)} ssbLength=${String(meta.ssbLength)}`
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
const esi = meta.esi;
|
|
3174
|
+
const recoveredCatalogTrackName = catalogTrackName ?? this.#decoderBlockCatalogTrackName(client, meta.sbn);
|
|
3175
|
+
if (!this.#claimDecoderBlock(client, meta.sbn, recoveredCatalogTrackName)) return;
|
|
3176
|
+
const blockKey = this.#blockKey(meta.sbn, packetId, recoveredCatalogTrackName);
|
|
3177
|
+
const cleanlyCompleted = this.#cleanlyCompletedBlocks.has(blockKey);
|
|
3178
|
+
if (!cleanlyCompleted) {
|
|
3179
|
+
if (!this.#workerRecoveredBlocks.has(blockKey)) {
|
|
3180
|
+
this.#traceLifecycle(
|
|
3181
|
+
"recovered",
|
|
3182
|
+
meta.sbn,
|
|
3183
|
+
packetId,
|
|
3184
|
+
recoveredCatalogTrackName
|
|
3185
|
+
);
|
|
3186
|
+
this.#workerRecoveredBlocks.add(blockKey);
|
|
3187
|
+
this.#fecStats.blocksRecovered++;
|
|
3188
|
+
this.#statsForPacket(packetId).blocksRecovered++;
|
|
3189
|
+
if (this.#workerRecoveredBlocks.size > 1e3) {
|
|
3190
|
+
dropOldest(this.#workerRecoveredBlocks, 500);
|
|
3191
|
+
}
|
|
3192
|
+
this.#noteBlockRepaired(blockKey, meta.sbn, packetId, recoveredCatalogTrackName);
|
|
3193
|
+
}
|
|
3194
|
+
const symbolKey = `${blockKey}:${esi}`;
|
|
3195
|
+
if (!this.#seenSymbols.has(symbolKey)) {
|
|
3196
|
+
this.#seenSymbols.add(symbolKey);
|
|
3197
|
+
this.onBlockRecovered?.(
|
|
3198
|
+
meta.sbn,
|
|
3199
|
+
packetId,
|
|
3200
|
+
data,
|
|
3201
|
+
meta,
|
|
3202
|
+
recoveredCatalogTrackName
|
|
3203
|
+
);
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
if (esi >= meta.ssbLength - 1 && !this.#completedBlocks.has(blockKey)) {
|
|
3207
|
+
this.cancelBlockTimeout(meta.sbn, packetId, recoveredCatalogTrackName);
|
|
3208
|
+
if (recoveredCatalogTrackName === void 0) {
|
|
3209
|
+
this.onBlockComplete?.(meta.sbn, packetId);
|
|
3210
|
+
} else {
|
|
3211
|
+
this.onBlockComplete?.(
|
|
3212
|
+
meta.sbn,
|
|
3213
|
+
packetId,
|
|
3214
|
+
recoveredCatalogTrackName
|
|
3215
|
+
);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
} else {
|
|
3219
|
+
const completedCatalogTrackName = catalogTrackName ?? this.#decoderBlockCatalogTrackName(client, meta.sbn);
|
|
3220
|
+
this.markBlockCompleted(meta.sbn, packetId, completedCatalogTrackName);
|
|
3221
|
+
}
|
|
3222
|
+
});
|
|
3223
|
+
client.onSnapshot((stats) => {
|
|
3224
|
+
if (this.#releasedWorkerClients.has(client)) return;
|
|
3225
|
+
this.#fecWorkerStats.set(packetId, stats);
|
|
3226
|
+
try {
|
|
3227
|
+
this.onFecStatsUpdate?.(packetId, stats);
|
|
3228
|
+
} catch (error) {
|
|
3229
|
+
console.warn("[FECManager] FEC stats callback failed:", error);
|
|
3230
|
+
}
|
|
3231
|
+
for (const observer of this.#fecStatsObservers) {
|
|
3232
|
+
try {
|
|
3233
|
+
observer(packetId, stats);
|
|
3234
|
+
} catch (error) {
|
|
3235
|
+
console.warn("[FECManager] FEC stats observer failed:", error);
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
});
|
|
3239
|
+
client.onCleanupComplete((sbn) => {
|
|
3240
|
+
if (this.#releasedWorkerClients.has(client)) return;
|
|
3241
|
+
const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {
|
|
3242
|
+
const block = this.#parseBlockKey(key);
|
|
3243
|
+
return block.packetId === packetId && block.sbn === sbn && this.#pendingWorkerCleanupClients.get(key) === client;
|
|
3244
|
+
});
|
|
3245
|
+
for (const key of cleanupKeys) this.#settleOwedCleanup(key, "cleanup-ack");
|
|
3246
|
+
});
|
|
3247
|
+
client.onError((msg, code) => {
|
|
3248
|
+
const requestedKind = code ?? msg;
|
|
3249
|
+
const kind = this.#workerErrorCounts.has(requestedKind) || this.#workerErrorCounts.size < WORKER_ERROR_KIND_CAP - 1 ? requestedKind : WORKER_ERROR_OVERFLOW_KIND;
|
|
3250
|
+
const seen = this.#workerErrorCounts.get(kind) ?? 0;
|
|
3251
|
+
this.#workerErrorCounts.set(kind, seen + 1);
|
|
3252
|
+
if (seen < WORKER_ERROR_LOG_CAP) {
|
|
3253
|
+
console.warn("[FECManager] Worker error:", msg);
|
|
3254
|
+
} else if (seen === WORKER_ERROR_LOG_CAP) {
|
|
3255
|
+
console.warn(
|
|
3256
|
+
`[FECManager] Worker error (suppressing further copies of ${kind}): ${msg}`
|
|
3257
|
+
);
|
|
3258
|
+
}
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
/**
|
|
3262
|
+
* Remove a stream's Worker decoder from routing.
|
|
3263
|
+
*
|
|
3264
|
+
* Returns false when no Worker is registered under that exact stream key.
|
|
3265
|
+
* On success every cleanup acknowledgement still owed by the client is
|
|
3266
|
+
* resolved by failing its block now (a Worker that is going away cannot
|
|
3267
|
+
* post it), the client's block ownership is released, and
|
|
3268
|
+
* onDecoderUnavailable fires once with reason 'cleared' and the decoder
|
|
3269
|
+
* that remains for the stream ('sync' or 'none'). Call this before
|
|
3270
|
+
* disposing the client; a client disposed while still registered is
|
|
3271
|
+
* evicted on its next symbol or reset with reason 'disposed'.
|
|
3272
|
+
*/
|
|
3273
|
+
clearFecWorker(packetId, catalogTrackName) {
|
|
3274
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName);
|
|
3275
|
+
const client = this.#workerClients.get(streamKey);
|
|
3276
|
+
if (!client) return false;
|
|
3277
|
+
this.#retireWorker(streamKey, client, "cleared");
|
|
3278
|
+
return true;
|
|
3279
|
+
}
|
|
3280
|
+
/**
|
|
3281
|
+
* Take a Worker client out of routing under one stream key, fail the blocks
|
|
3282
|
+
* whose completion it still owed, and report the stream's decoder
|
|
3283
|
+
* unavailable together with whatever still decodes the stream.
|
|
3284
|
+
*/
|
|
3285
|
+
#retireWorker(streamKey, client, reason) {
|
|
3286
|
+
this.#workerClients.delete(streamKey);
|
|
3287
|
+
this.#releaseWorkerClient(client);
|
|
3288
|
+
this.#decoderUnavailableEvents++;
|
|
3289
|
+
const stream = this.#parseStreamKey(streamKey);
|
|
3290
|
+
const fallbackDecoder = this.#decoderForPacket(stream.packetId, stream.catalogTrackName) ? "sync" : "none";
|
|
3291
|
+
this.onDecoderUnavailable?.({
|
|
3292
|
+
packetId: stream.packetId,
|
|
3293
|
+
...stream.catalogTrackName === void 0 ? {} : { catalogTrackName: stream.catalogTrackName },
|
|
3294
|
+
reason,
|
|
3295
|
+
fallbackDecoder
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
/**
|
|
3299
|
+
* Fail every block whose Worker cleanup acknowledgement is owed by `client`
|
|
3300
|
+
* and drop its block ownership, so no block waits on a message the Worker
|
|
3301
|
+
* will never post.
|
|
3302
|
+
*/
|
|
3303
|
+
#releaseWorkerClient(client) {
|
|
3304
|
+
const owed = [...this.#pendingWorkerCleanupClients].filter(([, owner]) => owner === client).map(([key]) => key);
|
|
3305
|
+
for (const key of owed) this.#settleOwedCleanup(key, "decoder-unavailable");
|
|
3306
|
+
this.#decoderBlockOwners.delete(client);
|
|
3307
|
+
}
|
|
3308
|
+
/**
|
|
3309
|
+
* Bound the wait for one Worker cleanup acknowledgement by the block's
|
|
3310
|
+
* catalog-derived terminal lifetime. When it elapses with the cleanup still
|
|
3311
|
+
* owed to the same client, the block fails as if the ack had arrived: its
|
|
3312
|
+
* fail deadline already passed, so failing it is the decision that deadline
|
|
3313
|
+
* made. A late ack then finds nothing owed and is a no-op.
|
|
3314
|
+
*/
|
|
3315
|
+
#boundOwedCleanup(key, packetId, client) {
|
|
3316
|
+
this.#clearOwedCleanupBound(key);
|
|
3317
|
+
const bound = {
|
|
3318
|
+
handle: setTimeout(() => {
|
|
3319
|
+
if (this.#pendingWorkerCleanupBounds.get(key) !== bound) return;
|
|
3320
|
+
this.#pendingWorkerCleanupBounds.delete(key);
|
|
3321
|
+
if (!this.#pendingWorkerCleanups.has(key) || this.#pendingWorkerCleanupClients.get(key) !== client) {
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
this.#settleOwedCleanup(key, "cleanup-ack-timeout");
|
|
3325
|
+
}, Math.ceil(this.#liveBlockLifetimeMs(this.#resolveTrackConfig(packetId))))
|
|
3326
|
+
};
|
|
3327
|
+
this.#pendingWorkerCleanupBounds.set(key, bound);
|
|
3328
|
+
}
|
|
3329
|
+
#clearOwedCleanupBound(key) {
|
|
3330
|
+
const bound = this.#pendingWorkerCleanupBounds.get(key);
|
|
3331
|
+
if (!bound) return;
|
|
3332
|
+
clearTimeout(bound.handle);
|
|
3333
|
+
this.#pendingWorkerCleanupBounds.delete(key);
|
|
3334
|
+
}
|
|
3335
|
+
/**
|
|
3336
|
+
* Resolve one owed Worker cleanup: drop the pending entry and fail the block
|
|
3337
|
+
* unless it already completed. Shared by the cleanup acknowledgement and by
|
|
3338
|
+
* Worker retirement, so the two paths cannot drift in their bookkeeping.
|
|
3339
|
+
*/
|
|
3340
|
+
#settleOwedCleanup(key, event) {
|
|
3341
|
+
this.#clearOwedCleanupBound(key);
|
|
3342
|
+
this.#pendingWorkerCleanups.delete(key);
|
|
3343
|
+
this.#pendingWorkerCleanupClients.delete(key);
|
|
3344
|
+
if (this.#completedBlocks.has(key)) return;
|
|
3345
|
+
const block = this.#parseBlockKey(key);
|
|
3346
|
+
this.#traceLifecycle(event, block.sbn, block.packetId, block.catalogTrackName);
|
|
3347
|
+
this.#failBlock(block.sbn, block.packetId, block.catalogTrackName);
|
|
3348
|
+
}
|
|
3349
|
+
/** Evict a disposed client under every stream key that still maps to it. */
|
|
3350
|
+
#evictDisposedWorker(client) {
|
|
3351
|
+
for (const [key, registered] of [...this.#workerClients]) {
|
|
3352
|
+
if (registered !== client) continue;
|
|
3353
|
+
const stream = this.#parseStreamKey(key);
|
|
3354
|
+
const track = stream.catalogTrackName === void 0 ? "" : ` track=${stream.catalogTrackName}`;
|
|
3355
|
+
console.warn(
|
|
3356
|
+
`[FECManager] packetId=${stream.packetId}${track} Worker decoder was disposed while still registered; evicting it`
|
|
3357
|
+
);
|
|
3358
|
+
this.#retireWorker(key, client, "disposed");
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
/** Called by player when framerate is known (from decoder output). Refines FEC block timeout. */
|
|
3362
|
+
setFramerate(fps) {
|
|
3363
|
+
if (this.#fpsDetected || fps <= 0) return;
|
|
3364
|
+
this.#fpsDetected = true;
|
|
3365
|
+
this.#fecFps = fps;
|
|
3366
|
+
if (this.#fecInterleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) {
|
|
3367
|
+
this.#fecTimeoutMs = computeFecDeliveryWindowMs({
|
|
3368
|
+
interleaveDepthMs: this.#fecInterleaveDepthMs,
|
|
3369
|
+
sourceSymbols: this.#fecK,
|
|
3370
|
+
symbolSize: this.#fecSymbolSize,
|
|
3371
|
+
mediaUnitsPerSecond: fps
|
|
3372
|
+
});
|
|
3373
|
+
}
|
|
3374
|
+
for (const [packetId, config] of this.#trackConfigs) {
|
|
3375
|
+
if (config.fps !== void 0 || config.interleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS) continue;
|
|
3376
|
+
this.#trackConfigs.set(packetId, {
|
|
3377
|
+
...config,
|
|
3378
|
+
fps,
|
|
3379
|
+
timeoutMs: Math.max(
|
|
3380
|
+
this.#fecTimeoutMs,
|
|
3381
|
+
computeFecDeliveryWindowMs({
|
|
3382
|
+
interleaveDepthMs: config.interleaveDepthMs,
|
|
3383
|
+
sourceSymbols: config.k,
|
|
3384
|
+
symbolSize: config.symbolSize,
|
|
3385
|
+
mediaUnitsPerSecond: fps
|
|
3386
|
+
})
|
|
3387
|
+
)
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
// -----------------------------------------------------------------------
|
|
3392
|
+
// Symbol feeding (codec-agnostic)
|
|
3393
|
+
// -----------------------------------------------------------------------
|
|
3394
|
+
/**
|
|
3395
|
+
* Feed a source symbol to the WASM FEC decoder.
|
|
3396
|
+
*
|
|
3397
|
+
* Does NOT track block source counts or fire lifecycle events — call
|
|
3398
|
+
* `trackBlock()` separately BEFORE this method (MmtpRouter does this).
|
|
3399
|
+
*
|
|
3400
|
+
* @returns Recovered block data if WASM completed the block, else null.
|
|
3401
|
+
* Caller is responsible for routing recovered data and updating
|
|
3402
|
+
* block completion state via `markBlockCompleted()`.
|
|
3403
|
+
*/
|
|
3404
|
+
feedSourceSymbol(packetId, ssId, symbolData, alta, catalogTrackName) {
|
|
3405
|
+
this.#fecStats.sourcePackets++;
|
|
3406
|
+
this.#statsForPacket(packetId).sourcePackets++;
|
|
3407
|
+
let sbn;
|
|
3408
|
+
let fecData;
|
|
3409
|
+
if (this.#isIso(packetId)) {
|
|
3410
|
+
const locator = this.#isoLocatorFor(packetId, catalogTrackName);
|
|
3411
|
+
const located = locator.locate(ssId);
|
|
3412
|
+
if (located === null) {
|
|
3413
|
+
throw new Error(
|
|
3414
|
+
`[FECManager] iso-ssbg1-v1 source SS_ID=${ssId} (packetId=${packetId}) has no stated block; hold it until a repair places it (placeIsoRepair)`
|
|
3415
|
+
);
|
|
3416
|
+
}
|
|
3417
|
+
locator.observe(ssId);
|
|
3418
|
+
if (!ssbg1FitsSymbol(symbolData.byteLength, this.symbolSizeForPacket(packetId))) {
|
|
3419
|
+
this.#isoSourceOversizeCount++;
|
|
3420
|
+
return null;
|
|
3421
|
+
}
|
|
3422
|
+
sbn = located.block.start;
|
|
3423
|
+
fecData = symbolData;
|
|
3424
|
+
} else {
|
|
3425
|
+
fecData = this.#normalizeSymbolData(packetId, symbolData);
|
|
3426
|
+
sbn = Math.floor(ssId / this.kForPacket(packetId));
|
|
3427
|
+
}
|
|
3428
|
+
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
3429
|
+
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
|
|
3430
|
+
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
3431
|
+
if (workerClient && !syncBlockOwner) {
|
|
3432
|
+
if (!this.#claimDecoderBlock(workerClient, sbn, catalogTrackName)) return null;
|
|
3433
|
+
const buf = fecData !== symbolData ? fecData.buffer : fecData.buffer.slice(
|
|
3434
|
+
fecData.byteOffset,
|
|
3435
|
+
fecData.byteOffset + fecData.byteLength
|
|
3436
|
+
);
|
|
3437
|
+
if (alta) {
|
|
3438
|
+
const authBuf = alta.auth.buffer.slice(
|
|
3439
|
+
alta.auth.byteOffset,
|
|
3440
|
+
alta.auth.byteOffset + alta.auth.byteLength
|
|
3441
|
+
);
|
|
3442
|
+
workerClient.feedSource(ssId, buf, performance.now(), {
|
|
3443
|
+
auth: authBuf,
|
|
3444
|
+
payloadEnd: alta.payloadEnd
|
|
3445
|
+
});
|
|
3446
|
+
} else {
|
|
3447
|
+
workerClient.feedSource(ssId, buf, performance.now());
|
|
3448
|
+
}
|
|
3449
|
+
return null;
|
|
3450
|
+
}
|
|
3451
|
+
const entry = this.#decoderForPacket(packetId, catalogTrackName);
|
|
3452
|
+
const client = syncBlockOwner ?? entry?.client;
|
|
3453
|
+
if (!client) return null;
|
|
3454
|
+
if (!this.#claimDecoderBlock(client, sbn, catalogTrackName)) return null;
|
|
3455
|
+
this.#syncDecoderBlockOwners.set(blockKey, client);
|
|
3456
|
+
let recovered = null;
|
|
3457
|
+
try {
|
|
3458
|
+
recovered = client.add_source(ssId, fecData, performance.now());
|
|
3459
|
+
} catch {
|
|
3460
|
+
}
|
|
3461
|
+
this.#retireIsoUnplaced(client, packetId, catalogTrackName);
|
|
3462
|
+
if (recovered || this.#completedBlocks.has(blockKey)) {
|
|
3463
|
+
this.#syncDecoderBlockOwners.delete(blockKey);
|
|
3464
|
+
}
|
|
3465
|
+
return recovered;
|
|
3466
|
+
}
|
|
3467
|
+
/**
|
|
3468
|
+
* Feed a repair symbol to the WASM FEC decoder.
|
|
3469
|
+
*
|
|
3470
|
+
* @returns Recovered block data if repair completed the block, else null.
|
|
3471
|
+
* Caller is responsible for routing recovered data and updating
|
|
3472
|
+
* block completion state via `markBlockCompleted()`.
|
|
3473
|
+
*/
|
|
3474
|
+
feedRepairSymbol(packetId, ssStart, ssbLength, rsId, repairData, catalogTrackName) {
|
|
3475
|
+
this.#fecStats.repairPackets++;
|
|
3476
|
+
this.#statsForPacket(packetId).repairPackets++;
|
|
3477
|
+
const symbolSize = this.symbolSizeForPacket(packetId);
|
|
3478
|
+
if (symbolSize > 0 && repairData.byteLength !== symbolSize) {
|
|
3479
|
+
this.#repairSymbolSizeMismatchCount++;
|
|
3480
|
+
if (this.#repairSymbolSizeMismatchCount <= 5 || this.#repairSymbolSizeMismatchCount % 100 === 0) {
|
|
3481
|
+
console.warn(
|
|
3482
|
+
`[FECManager] Dropping repair symbol with invalid size: got ${repairData.byteLength}B, expected ${symbolSize}B (packetId=${packetId}, ssStart=${ssStart}, ssbLength=${ssbLength}, rsId=${rsId})`
|
|
3483
|
+
);
|
|
3484
|
+
}
|
|
3485
|
+
return null;
|
|
3486
|
+
}
|
|
3487
|
+
const fecData = repairData;
|
|
3488
|
+
const k = this.kForPacket(packetId);
|
|
3489
|
+
let sbn;
|
|
3490
|
+
if (this.#isIso(packetId)) {
|
|
3491
|
+
const block = this.#isoLocatorFor(packetId, catalogTrackName).blockAt(ssStart);
|
|
3492
|
+
if (ssbLength <= 0 || ssbLength > k || block === void 0 || block.span || block.length !== ssbLength) {
|
|
3493
|
+
return null;
|
|
3494
|
+
}
|
|
3495
|
+
sbn = ssStart;
|
|
3496
|
+
} else {
|
|
3497
|
+
if (ssbLength <= 0 || ssbLength > k || ssStart % k !== 0) return null;
|
|
3498
|
+
sbn = Math.floor(ssStart / k);
|
|
3499
|
+
}
|
|
3500
|
+
this.#noteSbnSeen(sbn, packetId, catalogTrackName, "repair");
|
|
3501
|
+
this.noteBlockActivity(sbn, packetId, catalogTrackName);
|
|
3502
|
+
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
3503
|
+
if (this.#completedBlocks.has(blockKey)) return null;
|
|
3504
|
+
if (!this.#noteAuthoredLength(blockKey, sbn, packetId, ssbLength, catalogTrackName)) {
|
|
3505
|
+
return null;
|
|
3506
|
+
}
|
|
3507
|
+
let repairIds = this.#blockRepairRsIds.get(blockKey);
|
|
3508
|
+
if (!repairIds) {
|
|
3509
|
+
repairIds = /* @__PURE__ */ new Set();
|
|
3510
|
+
this.#blockRepairRsIds.set(blockKey, repairIds);
|
|
3511
|
+
}
|
|
3512
|
+
repairIds.add(rsId);
|
|
3513
|
+
if (this.#repairRequestedBlocks.has(blockKey)) {
|
|
3514
|
+
this.#traceLifecycle("repair-arrived", sbn, packetId, catalogTrackName, rsId);
|
|
3515
|
+
}
|
|
3516
|
+
const cleanOnArrival = (this.#blockSourceCounts.get(blockKey) ?? 0) >= ssbLength;
|
|
3517
|
+
const recovered = this.#dispatchRepairSymbol(
|
|
3518
|
+
blockKey,
|
|
3519
|
+
sbn,
|
|
3520
|
+
packetId,
|
|
3521
|
+
ssStart,
|
|
3522
|
+
ssbLength,
|
|
3523
|
+
rsId,
|
|
3524
|
+
fecData,
|
|
3525
|
+
catalogTrackName
|
|
3526
|
+
);
|
|
3527
|
+
if (cleanOnArrival) {
|
|
3528
|
+
this.#completeCleanBlock(blockKey, sbn, packetId, catalogTrackName);
|
|
3529
|
+
return null;
|
|
3530
|
+
}
|
|
3531
|
+
return recovered;
|
|
3532
|
+
}
|
|
3533
|
+
/**
|
|
3534
|
+
* Record a block's authored SSB_length from a repair symbol. Returns false,
|
|
3535
|
+
* leaving the block unchanged, when the symbol contradicts the block: a
|
|
3536
|
+
* different SSB_length than an earlier repair symbol declared is dropped,
|
|
3537
|
+
* and an SSB_length that places an already-received source ESI inside the
|
|
3538
|
+
* zero padding fails the block, since its geometry can no longer be trusted.
|
|
3539
|
+
*/
|
|
3540
|
+
#noteAuthoredLength(blockKey, sbn, packetId, ssbLength, catalogTrackName) {
|
|
3541
|
+
const known = this.#blockAuthoredLengths.get(blockKey);
|
|
3542
|
+
if (known === ssbLength) return true;
|
|
3543
|
+
if (known !== void 0) {
|
|
3544
|
+
this.#ssbLengthConflictCount++;
|
|
3545
|
+
if (this.#ssbLengthConflictCount <= 5 || this.#ssbLengthConflictCount % 100 === 0) {
|
|
3546
|
+
console.warn(
|
|
3547
|
+
`[FECManager] Dropping repair symbol whose SSB_length ${ssbLength} conflicts with ${known} for packetId=${packetId} sbn=${sbn}`
|
|
3548
|
+
);
|
|
3549
|
+
}
|
|
3550
|
+
return false;
|
|
3551
|
+
}
|
|
3552
|
+
const sourceEsis = this.#blockSourceEsis.get(blockKey);
|
|
3553
|
+
if (sourceEsis && [...sourceEsis].some((esi) => esi >= ssbLength)) {
|
|
3554
|
+
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
3555
|
+
return false;
|
|
3556
|
+
}
|
|
3557
|
+
this.#blockAuthoredLengths.set(blockKey, ssbLength);
|
|
3558
|
+
return true;
|
|
3559
|
+
}
|
|
3560
|
+
#dispatchRepairSymbol(blockKey, sbn, packetId, ssStart, ssbLength, rsId, fecData, catalogTrackName) {
|
|
3561
|
+
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey);
|
|
3562
|
+
const workerClient = this.#routableWorkerForPacket(packetId, catalogTrackName);
|
|
3563
|
+
if (workerClient && !syncBlockOwner) {
|
|
3564
|
+
if (!this.#claimDecoderBlock(workerClient, sbn, catalogTrackName)) return null;
|
|
3565
|
+
const buf = fecData.buffer.slice(
|
|
3566
|
+
fecData.byteOffset,
|
|
3567
|
+
fecData.byteOffset + fecData.byteLength
|
|
3568
|
+
);
|
|
3569
|
+
workerClient.feedRepair(ssStart, ssbLength, rsId, buf, performance.now());
|
|
3570
|
+
return null;
|
|
3571
|
+
}
|
|
3572
|
+
const entry = this.#decoderForPacket(packetId, catalogTrackName);
|
|
3573
|
+
const client = syncBlockOwner ?? entry?.client;
|
|
3574
|
+
if (!client) return null;
|
|
3575
|
+
if (!this.#claimDecoderBlock(client, sbn, catalogTrackName)) return null;
|
|
3576
|
+
this.#syncDecoderBlockOwners.set(blockKey, client);
|
|
3577
|
+
let recovered = null;
|
|
3578
|
+
try {
|
|
3579
|
+
recovered = client.add_repair(ssStart, ssbLength, rsId, fecData, performance.now());
|
|
3580
|
+
} catch {
|
|
3581
|
+
}
|
|
3582
|
+
this.#retireIsoUnplaced(client, packetId, catalogTrackName);
|
|
3583
|
+
if (recovered) this.#syncDecoderBlockOwners.delete(blockKey);
|
|
3584
|
+
return recovered;
|
|
3585
|
+
}
|
|
3586
|
+
#routeSyncCleanup(decoder, sbn, packetId, catalogTrackName) {
|
|
3587
|
+
try {
|
|
3588
|
+
decoder.cleanup_blocks_detailed(Uint32Array.of(sbn));
|
|
3589
|
+
} catch (error) {
|
|
3590
|
+
console.warn(`[FECManager] Scoped cleanup failed for SBN ${sbn}:`, error);
|
|
3591
|
+
}
|
|
3592
|
+
this.#retireIsoUnplaced(decoder, packetId, catalogTrackName);
|
|
3593
|
+
}
|
|
3594
|
+
/**
|
|
3595
|
+
* iso-ssbg1-v1: a placed source the decoder took before its block's repair
|
|
3596
|
+
* (fed in wire order, sources first) waits unplaced in the decoder; one
|
|
3597
|
+
* whose repair never reaches it, such as a source of a repair-less span,
|
|
3598
|
+
* would stay there. Retire those by the decoder's horizon behind the newest
|
|
3599
|
+
* SS_ID the stream's locator has seen. Run after every feed that grows the
|
|
3600
|
+
* decoder and when a block is retired, as the fec-worker sweeps its own
|
|
3601
|
+
* pool: a stream that loses only repair symbols never expires a block, so
|
|
3602
|
+
* a sweep tied to expiry alone would never run there.
|
|
3603
|
+
*/
|
|
3604
|
+
#retireIsoUnplaced(decoder, packetId, catalogTrackName) {
|
|
3605
|
+
if (!this.#isIso(packetId)) return;
|
|
3606
|
+
const newest = this.#isoLocators.get(this.#streamKey(packetId, catalogTrackName))?.newest;
|
|
3607
|
+
if (newest === void 0) return;
|
|
3608
|
+
try {
|
|
3609
|
+
this.#isoDecoderUnplacedRetired += decoder.retire_unplaced(newest);
|
|
3610
|
+
} catch (error) {
|
|
3611
|
+
this.#isoDecoderUnplacedRetireFailed++;
|
|
3612
|
+
const failed = this.#isoDecoderUnplacedRetireFailed;
|
|
3613
|
+
if (failed <= 5 || failed % 100 === 0) {
|
|
3614
|
+
console.warn(
|
|
3615
|
+
`[FECManager] iso-ssbg1-v1 unplaced retirement failed for packetId=${packetId} (${failed} total):`,
|
|
3616
|
+
error
|
|
3617
|
+
);
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
// -----------------------------------------------------------------------
|
|
3622
|
+
// Block lifecycle
|
|
3623
|
+
// -----------------------------------------------------------------------
|
|
3624
|
+
/**
|
|
3625
|
+
* Track a first-arrival network source symbol for an FEC block. Refreshes
|
|
3626
|
+
* the inactivity deadline on each symbol, records the symbol's in-band
|
|
3627
|
+
* classification evidence, and marks the block clean when all of its
|
|
3628
|
+
* authored source symbols arrived: the SSB_length a repair symbol declared,
|
|
3629
|
+
* otherwise K.
|
|
3630
|
+
*
|
|
3631
|
+
* Throws for a packetId that was never configured, so a classified block
|
|
3632
|
+
* never reads the manager-global K or timeout.
|
|
3633
|
+
*/
|
|
3634
|
+
trackBlock(sbn, packetId, catalogTrackName, esi, evidence) {
|
|
3635
|
+
const trackConfig = this.#trackConfigs.get(packetId);
|
|
3636
|
+
if (trackConfig === void 0) {
|
|
3637
|
+
throw new Error(
|
|
3638
|
+
`[FECManager] trackBlock for packetId=${packetId}, which was never configured; configure the track from its catalog (configureTrack/setFecWorker/setFecDecoder) first`
|
|
3639
|
+
);
|
|
3640
|
+
}
|
|
3641
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
3642
|
+
const k = trackConfig.k;
|
|
3643
|
+
if (this.#completedBlocks.has(key)) return;
|
|
3644
|
+
this.#noteSbnSeen(sbn, packetId, catalogTrackName, "source", evidence.packetSequenceNumber);
|
|
3645
|
+
const authoredLength = this.#blockAuthoredLengths.get(key) ?? k;
|
|
3646
|
+
if (Number.isInteger(esi) && esi >= authoredLength) {
|
|
3647
|
+
this.#failBlock(sbn, packetId, catalogTrackName);
|
|
3648
|
+
return;
|
|
3649
|
+
}
|
|
3650
|
+
this.#applyClassifierChanges(
|
|
3651
|
+
packetId,
|
|
3652
|
+
catalogTrackName,
|
|
3653
|
+
this.#classifierFor(packetId, catalogTrackName).observe(sbn, evidence)
|
|
3654
|
+
);
|
|
3655
|
+
const count = (this.#blockSourceCounts.get(key) ?? 0) + 1;
|
|
3656
|
+
this.#blockSourceCounts.set(key, count);
|
|
3657
|
+
if (count === 1) this.#indexBlockKey(key, sbn, packetId);
|
|
3658
|
+
if (Number.isInteger(esi) && esi >= 0 && esi < k) {
|
|
3659
|
+
let sourceEsis = this.#blockSourceEsis.get(key);
|
|
3660
|
+
if (!sourceEsis) {
|
|
3661
|
+
sourceEsis = /* @__PURE__ */ new Set();
|
|
3662
|
+
this.#blockSourceEsis.set(key, sourceEsis);
|
|
3663
|
+
}
|
|
3664
|
+
sourceEsis.add(esi);
|
|
3665
|
+
}
|
|
3666
|
+
if (count === 1) {
|
|
3667
|
+
if (catalogTrackName === void 0) {
|
|
3668
|
+
this.onBlockStarted?.(sbn, packetId);
|
|
3669
|
+
} else {
|
|
3670
|
+
this.onBlockStarted?.(sbn, packetId, catalogTrackName);
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
if (k > 0 && count >= authoredLength) {
|
|
3674
|
+
this.#completeCleanBlock(key, sbn, packetId, catalogTrackName);
|
|
3675
|
+
return;
|
|
3676
|
+
}
|
|
3677
|
+
this.noteBlockActivity(sbn, packetId, catalogTrackName, true);
|
|
3678
|
+
}
|
|
3679
|
+
#completeCleanBlock(key, sbn, packetId, catalogTrackName) {
|
|
3680
|
+
this.#fecStats.blocksClean++;
|
|
3681
|
+
this.#statsForPacket(packetId).blocksClean++;
|
|
3682
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName);
|
|
3683
|
+
this.#blockSourceCounts.delete(key);
|
|
3684
|
+
this.#blockSourceEsis.delete(key);
|
|
3685
|
+
this.#blockAuthoredLengths.delete(key);
|
|
3686
|
+
this.#blockRepairRsIds.delete(key);
|
|
3687
|
+
this.#markTerminal(key, sbn, packetId, true);
|
|
3688
|
+
this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
|
|
3689
|
+
if (catalogTrackName === void 0) {
|
|
3690
|
+
this.onBlockComplete?.(sbn, packetId);
|
|
3691
|
+
} else {
|
|
3692
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName);
|
|
3693
|
+
}
|
|
3694
|
+
queueMicrotask(() => this.#syncDecoderBlockOwners.delete(key));
|
|
3695
|
+
}
|
|
3696
|
+
/** Refresh deadlines after accepted block activity. */
|
|
3697
|
+
noteBlockActivity(sbn, packetId, catalogTrackName, sourceProgress = false) {
|
|
3698
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
3699
|
+
if (this.#completedBlocks.has(key)) return;
|
|
3700
|
+
if (this.#pendingWorkerCleanups.has(key)) return;
|
|
3701
|
+
const repairRequested = this.#repairRequestedBlocks.has(key);
|
|
3702
|
+
if (repairRequested && this.#settlesByOrder(key)) return;
|
|
3703
|
+
if (this.#settlesByOrder(key)) {
|
|
3704
|
+
this.#holdForOrderedSettlement(key, sbn, packetId, catalogTrackName);
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
const trackConfig = this.#resolveTrackConfig(packetId);
|
|
3708
|
+
const timeoutMs = this.#multicastWaitMs(trackConfig);
|
|
3709
|
+
if (!(timeoutMs > 0)) {
|
|
3710
|
+
throw new Error(`[FECManager] packetId=${packetId} has no positive fail bound (timeoutMs=${timeoutMs})`);
|
|
3711
|
+
}
|
|
3712
|
+
const repairDeadline = this.#repairDeadlines.get(key);
|
|
3713
|
+
if (this.onRepairNeeded && !repairRequested && (repairDeadline === void 0 || sourceProgress)) {
|
|
3714
|
+
this.#armRepairDeadline(
|
|
3715
|
+
key,
|
|
3716
|
+
sbn,
|
|
3717
|
+
packetId,
|
|
3718
|
+
catalogTrackName,
|
|
3719
|
+
this.#repairDeadlineMs(trackConfig)
|
|
3720
|
+
);
|
|
3721
|
+
}
|
|
3722
|
+
this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs);
|
|
3723
|
+
}
|
|
3724
|
+
#failBlock(sbn, packetId, catalogTrackName) {
|
|
3725
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName);
|
|
3726
|
+
this.#traceLifecycle("failed", sbn, packetId, catalogTrackName);
|
|
3727
|
+
this.#clearRepairDeadline(key);
|
|
3728
|
+
this.#clearBlockTimer(key);
|
|
3729
|
+
const blockClass = this.#retireBlockClass(key, sbn, packetId, catalogTrackName);
|
|
3730
|
+
if (this.#repairRequestedBlocks.delete(key)) {
|
|
3731
|
+
this.onRepairResolved?.(sbn, packetId, "failed", catalogTrackName, null);
|
|
3732
|
+
}
|
|
3733
|
+
this.#blockSourceCounts.delete(key);
|
|
3734
|
+
this.#blockSourceEsis.delete(key);
|
|
3735
|
+
this.#blockAuthoredLengths.delete(key);
|
|
3736
|
+
this.#blockRepairRsIds.delete(key);
|
|
3737
|
+
this.#syncDecoderBlockOwners.delete(key);
|
|
3738
|
+
this.#markTerminal(key, sbn, packetId, false);
|
|
3739
|
+
this.#failedBlocks.add(key);
|
|
3740
|
+
this.#markPathBlockComplete(sbn, packetId, catalogTrackName);
|
|
3741
|
+
this.#fecStats.blocksFailed++;
|
|
3742
|
+
this.#statsForPacket(packetId).blocksFailed++;
|
|
3743
|
+
this.onBlockFailed?.(sbn, packetId, catalogTrackName, blockClass);
|
|
3744
|
+
this.onBlockResolved?.(sbn, packetId, catalogTrackName);
|
|
3745
|
+
}
|
|
3746
|
+
/**
|
|
3747
|
+
* Resolve a block as completed (clean, recovered, or externally marked).
|
|
3748
|
+
* A block that had a D-28 repair outstanding counts as repaired: it was
|
|
3749
|
+
* short of its source at the repair deadline and completed anyway.
|
|
3750
|
+
*/
|
|
3751
|
+
#resolveCompleted(key) {
|
|
3752
|
+
const block = this.#parseBlockKey(key);
|
|
3753
|
+
this.#markTerminal(key, block.sbn, block.packetId, false);
|
|
3754
|
+
this.#clearOwedCleanupBound(key);
|
|
3755
|
+
this.#pendingWorkerCleanups.delete(key);
|
|
3756
|
+
this.#pendingWorkerCleanupClients.delete(key);
|
|
3757
|
+
this.#markPathBlockComplete(block.sbn, block.packetId, block.catalogTrackName);
|
|
3758
|
+
this.#clearRepairDeadline(key);
|
|
3759
|
+
if (this.#repairRequestedBlocks.delete(key)) {
|
|
3760
|
+
this.#noteBlockRepaired(key, block.sbn, block.packetId, block.catalogTrackName);
|
|
3761
|
+
this.onRepairResolved?.(
|
|
3762
|
+
block.sbn,
|
|
3763
|
+
block.packetId,
|
|
3764
|
+
"completed",
|
|
3765
|
+
block.catalogTrackName,
|
|
3766
|
+
this.#viaFor(key)
|
|
3767
|
+
);
|
|
3768
|
+
}
|
|
3769
|
+
this.#retireBlockClass(key, block.sbn, block.packetId, block.catalogTrackName);
|
|
3770
|
+
this.#blockSourceCounts.delete(key);
|
|
3771
|
+
this.#blockSourceEsis.delete(key);
|
|
3772
|
+
this.#blockAuthoredLengths.delete(key);
|
|
3773
|
+
this.#blockRepairRsIds.delete(key);
|
|
3774
|
+
this.#clearBlockTimer(key);
|
|
3775
|
+
this.onBlockResolved?.(block.sbn, block.packetId, block.catalogTrackName);
|
|
3776
|
+
}
|
|
3777
|
+
/** Check whether a block has already been completed (clean or recovered). */
|
|
3778
|
+
isBlockCompleted(sbn, packetId, catalogTrackName) {
|
|
3779
|
+
return this.#blockKeysFor(sbn, packetId, catalogTrackName).some((key) => this.#completedBlocks.has(key));
|
|
3780
|
+
}
|
|
3781
|
+
/** Mark a block as completed externally (e.g., after routing recovered data). */
|
|
3782
|
+
markBlockCompleted(sbn, packetId, catalogTrackName) {
|
|
3783
|
+
for (const key of this.#blockKeysFor(sbn, packetId, catalogTrackName)) {
|
|
3784
|
+
this.#resolveCompleted(key);
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
/**
|
|
3788
|
+
* Increment blocksRecovered (called by MmtpRouter after routing a
|
|
3789
|
+
* synchronous recovery). With the block's coordinates the recovery is also
|
|
3790
|
+
* reported through onBlockRepaired, once per block.
|
|
3791
|
+
*/
|
|
3792
|
+
countRecovery(packetId, sbn, catalogTrackName) {
|
|
3793
|
+
this.#fecStats.blocksRecovered++;
|
|
3794
|
+
if (packetId != null) this.#statsForPacket(packetId).blocksRecovered++;
|
|
3795
|
+
if (packetId != null && sbn != null) {
|
|
3796
|
+
this.#noteBlockRepaired(
|
|
3797
|
+
this.#blockKey(sbn, packetId, catalogTrackName),
|
|
3798
|
+
sbn,
|
|
3799
|
+
packetId,
|
|
3800
|
+
catalogTrackName
|
|
3801
|
+
);
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
/**
|
|
3805
|
+
* Strict delivery provenance for one FEC block.
|
|
3806
|
+
*
|
|
3807
|
+
* A recovered symbol is a function of every symbol that entered its block,
|
|
3808
|
+
* so it can only be attributed to multicast when NO symbol in that block
|
|
3809
|
+
* arrived over MoQ. Repair symbols routinely arrive over reliable QUIC
|
|
3810
|
+
* (`MmtpRouter.feedRepairPacket`), so a block that recovered with MoQ help
|
|
3811
|
+
* produced bytes that never crossed a multicast socket.
|
|
3812
|
+
*
|
|
3813
|
+
* An untracked block returns 'unknown' rather than a guess: absence of
|
|
3814
|
+
* evidence is not evidence of multicast.
|
|
3815
|
+
*/
|
|
3816
|
+
blockDeliveryProvenance(sbn, packetId, catalogTrackName) {
|
|
3817
|
+
const counts = this.blockPathCount(sbn, packetId, catalogTrackName);
|
|
3818
|
+
if (!counts) return "unknown";
|
|
3819
|
+
if (counts.moqSymbols > 0) return "mixed";
|
|
3820
|
+
if (counts.multicastSymbols > 0) return "multicast";
|
|
3821
|
+
return "unknown";
|
|
3822
|
+
}
|
|
3823
|
+
/** Per-path symbol counts for one block (D-28 gate logging), if tracked. */
|
|
3824
|
+
blockPathCount(sbn, packetId, catalogTrackName) {
|
|
3825
|
+
return this.#blockPathCounts.get(
|
|
3826
|
+
this.#blockKey(sbn, packetId, catalogTrackName)
|
|
3827
|
+
) ?? null;
|
|
3828
|
+
}
|
|
3829
|
+
/** Cancel block timeout (block completed via clean reception or FEC recovery). */
|
|
3830
|
+
cancelBlockTimeout(sbn, packetId, catalogTrackName) {
|
|
3831
|
+
for (const key of this.#blockKeysFor(sbn, packetId, catalogTrackName)) {
|
|
3832
|
+
this.#resolveCompleted(key);
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
// -----------------------------------------------------------------------
|
|
3836
|
+
// Cleanup
|
|
3837
|
+
// -----------------------------------------------------------------------
|
|
3838
|
+
dispose() {
|
|
3839
|
+
for (const timer of this.#activeBlocks.values()) {
|
|
3840
|
+
if (timer.handle != null) clearTimeout(timer.handle);
|
|
3841
|
+
}
|
|
3842
|
+
this.#activeBlocks.clear();
|
|
3843
|
+
this.#activeBlockDeadlinesMs.clear();
|
|
3844
|
+
this.#blockWaitDeadlinesMs.clear();
|
|
3845
|
+
this.#repairHoldUntilMs.clear();
|
|
3846
|
+
this.#deliveryGaps.reset();
|
|
3847
|
+
this.#pendingWorkerCleanups.clear();
|
|
3848
|
+
this.#pendingWorkerCleanupClients.clear();
|
|
3849
|
+
for (const bound of this.#pendingWorkerCleanupBounds.values()) clearTimeout(bound.handle);
|
|
3850
|
+
this.#pendingWorkerCleanupBounds.clear();
|
|
3851
|
+
for (const timer of this.#repairDeadlines.values()) {
|
|
3852
|
+
if (timer?.handle != null) clearTimeout(timer.handle);
|
|
3853
|
+
}
|
|
3854
|
+
this.#repairDeadlines.clear();
|
|
3855
|
+
this.#repairDeadlineAtMs.clear();
|
|
3856
|
+
this.#terminalExpiryMs.clear();
|
|
3857
|
+
this.#terminalQueues.clear();
|
|
3858
|
+
this.#blockKeysBySbn.clear();
|
|
3859
|
+
this.#blockSourceCounts.clear();
|
|
3860
|
+
this.#blockSourceEsis.clear();
|
|
3861
|
+
this.#blockAuthoredLengths.clear();
|
|
3862
|
+
this.#blockRepairRsIds.clear();
|
|
3863
|
+
this.#completedBlocks.clear();
|
|
3864
|
+
this.#cleanlyCompletedBlocks.clear();
|
|
3865
|
+
this.#repairRequestedBlocks.clear();
|
|
3866
|
+
this.#workerRecoveredBlocks.clear();
|
|
3867
|
+
this.#syncDecoderBlockOwners.clear();
|
|
3868
|
+
this.#startupAlignedPackets.clear();
|
|
3869
|
+
this.#startupPartialBlocks.clear();
|
|
3870
|
+
this.#startupRequireRepairReady = false;
|
|
3871
|
+
this.#startupRepairReadyPackets.clear();
|
|
3872
|
+
this.#startupRepairBoundaryCounts.clear();
|
|
3873
|
+
this.#startupRepairFailOpenPackets.clear();
|
|
3874
|
+
this.#orderedFrontiers.clear();
|
|
3875
|
+
this.#orderedPendingBlocks.clear();
|
|
3876
|
+
this.#seenSymbols.clear();
|
|
3877
|
+
this.#blockPathCounts.clear();
|
|
3878
|
+
this.#blockAnchorsMs.clear();
|
|
3879
|
+
this.#blockCatalogTrackNames.clear();
|
|
3880
|
+
this.#classifiers.clear();
|
|
3881
|
+
this.#streamGeometries.clear();
|
|
3882
|
+
this.#failedBlocks.clear();
|
|
3883
|
+
this.#resolvedBlockClasses.clear();
|
|
3884
|
+
this.#repairedBlocks.clear();
|
|
3885
|
+
this.#materializedBlocks.clear();
|
|
3886
|
+
this.#highestSbn.clear();
|
|
3887
|
+
this.#isoLocators.clear();
|
|
3888
|
+
this.#sourceAnchors.clear();
|
|
3889
|
+
this.#trackConfigs.clear();
|
|
3890
|
+
this.#fecDecoders.clear();
|
|
3891
|
+
for (const client of new Set(this.#workerClients.values())) client.dispose();
|
|
3892
|
+
this.#workerClients.clear();
|
|
3893
|
+
}
|
|
3894
|
+
};
|
|
3895
|
+
export {
|
|
3896
|
+
FECManager,
|
|
3897
|
+
MIN_INTERLEAVE_DEPTH_MS
|
|
3898
|
+
};
|
|
3899
|
+
//# sourceMappingURL=fec-manager.js.map
|