@blockcast/mmt-container 0.1.0-main.149f78a4adae
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 +432 -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 +148 -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 +93 -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/browser-runtime.js +17109 -0
- package/dist/browser-runtime.js.map +7 -0
- package/dist/carp-catalog.d.ts +318 -0
- package/dist/carp-catalog.d.ts.map +1 -0
- package/dist/carp-catalog.js +549 -0
- package/dist/carp-catalog.js.map +7 -0
- package/dist/catalog-fec-params.d.ts +82 -0
- package/dist/catalog-fec-params.d.ts.map +1 -0
- package/dist/catalog-fec-params.js +66 -0
- package/dist/catalog-fec-params.js.map +7 -0
- package/dist/catalog-network-source.d.ts +19 -0
- package/dist/catalog-network-source.d.ts.map +1 -0
- package/dist/catalog-network-source.js +22 -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 +168 -0
- package/dist/cmaf-block-assembler.d.ts.map +1 -0
- package/dist/cmaf-block-assembler.js +1755 -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 +195 -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 +207 -0
- package/dist/cmcd-producer.js.map +7 -0
- package/dist/codec-utils.d.ts +235 -0
- package/dist/codec-utils.d.ts.map +1 -0
- package/dist/codec-utils.js +1225 -0
- package/dist/codec-utils.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 +266 -0
- package/dist/fec-manager.d.ts.map +1 -0
- package/dist/fec-manager.js +1447 -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 +75 -0
- package/dist/fec-starvation-detector.js.map +7 -0
- package/dist/fec-worker-setup.d.ts +76 -0
- package/dist/fec-worker-setup.d.ts.map +1 -0
- package/dist/fec-worker-setup.js +603 -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 +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +330 -0
- package/dist/index.js.map +7 -0
- package/dist/iso-bmff-timing.d.ts +47 -0
- package/dist/iso-bmff-timing.d.ts.map +1 -0
- package/dist/iso-bmff-timing.js +526 -0
- package/dist/iso-bmff-timing.js.map +7 -0
- package/dist/loc.d.ts +68 -0
- package/dist/loc.d.ts.map +1 -0
- package/dist/loc.js +143 -0
- package/dist/loc.js.map +7 -0
- 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 +1267 -0
- package/dist/manifest-auth-plan.js.map +7 -0
- package/dist/media-router.d.ts +338 -0
- package/dist/media-router.d.ts.map +1 -0
- package/dist/media-router.js +802 -0
- package/dist/media-router.js.map +7 -0
- package/dist/mfu-reassembler.d.ts +264 -0
- package/dist/mfu-reassembler.d.ts.map +1 -0
- package/dist/mfu-reassembler.js +2668 -0
- package/dist/mfu-reassembler.js.map +7 -0
- 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 +304 -0
- package/dist/mmtp-container-parser.d.ts.map +1 -0
- package/dist/mmtp-container-parser.js +5378 -0
- package/dist/mmtp-container-parser.js.map +7 -0
- package/dist/mmtp-container-parser.no-modules.js +7853 -0
- package/dist/mmtp-container-parser.no-modules.js.map +7 -0
- package/dist/mmtp-decode-pipeline.d.ts +96 -0
- package/dist/mmtp-decode-pipeline.d.ts.map +1 -0
- package/dist/mmtp-decode-pipeline.js +5589 -0
- package/dist/mmtp-decode-pipeline.js.map +7 -0
- package/dist/mmtp-router.d.ts +383 -0
- package/dist/mmtp-router.d.ts.map +1 -0
- package/dist/mmtp-router.js +8384 -0
- package/dist/mmtp-router.js.map +7 -0
- package/dist/mmtp-to-cmaf.d.ts +166 -0
- package/dist/mmtp-to-cmaf.d.ts.map +1 -0
- package/dist/mmtp-to-cmaf.js +781 -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 +107 -0
- package/dist/multicast-input.d.ts.map +1 -0
- package/dist/multicast-input.js +256 -0
- package/dist/multicast-input.js.map +7 -0
- package/dist/multicast-manager.d.ts +330 -0
- package/dist/multicast-manager.d.ts.map +1 -0
- package/dist/multicast-manager.js +11032 -0
- package/dist/multicast-manager.js.map +7 -0
- package/dist/multicast-transport.d.ts +62 -0
- package/dist/multicast-transport.d.ts.map +1 -0
- package/dist/multicast-transport.js +338 -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 +384 -0
- package/dist/multicast-unwrap.js.map +7 -0
- package/dist/no-modules.d.ts +9 -0
- package/dist/no-modules.d.ts.map +1 -0
- package/dist/no-modules.js +7819 -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 +58 -0
- package/dist/repair-fetch.d.ts.map +1 -0
- package/dist/repair-fetch.js +117 -0
- package/dist/repair-fetch.js.map +7 -0
- package/dist/sample-combiner.d.ts +182 -0
- package/dist/sample-combiner.d.ts.map +1 -0
- package/dist/sample-combiner.js +571 -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 +232 -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 +1220 -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 +85 -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 +456 -0
- package/dist/timestamp-codec.js.map +7 -0
- package/dist/track-pipeline-setup.d.ts +72 -0
- package/dist/track-pipeline-setup.d.ts.map +1 -0
- package/dist/track-pipeline-setup.js +306 -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 +229 -0
- package/dist/track-pipeline.js.map +7 -0
- package/dist/track-session.d.ts +269 -0
- package/dist/track-session.d.ts.map +1 -0
- package/dist/track-session.js +597 -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 +88 -0
- package/dist/transport-pool.js.map +7 -0
- package/dist/wasm-mfu-reassembler.d.ts +81 -0
- package/dist/wasm-mfu-reassembler.d.ts.map +1 -0
- package/dist/wasm-mfu-reassembler.js +1915 -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 +5635 -0
- package/dist/wasm-mmtp-container-parser.js.map +7 -0
- package/node_modules/@blockcast/fec-worker/README.md +213 -0
- package/node_modules/@blockcast/fec-worker/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +80 -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 +270 -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 +252 -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 +565 -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 +349 -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 +179 -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 +47 -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/fec-worker-client.ts +309 -0
- package/node_modules/@blockcast/fec-worker/src/fec-worker-types.ts +268 -0
- package/node_modules/@blockcast/fec-worker/src/fec-worker.ts +1048 -0
- package/node_modules/@blockcast/fec-worker/src/index.ts +32 -0
- package/node_modules/@blockcast/fec-worker/src/shred-fec-worker.ts +558 -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 +194 -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 +120 -0
- package/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-base/dist/index.d.ts +2 -0
- package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/index.js +2 -0
- package/node_modules/@blockcast/mmt-base/dist/index.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +275 -0
- package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/mmtp.js +644 -0
- package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +1 -0
- package/node_modules/@blockcast/mmt-base/package.json +49 -0
- package/node_modules/@blockcast/mmt-base/src/index.ts +1 -0
- package/node_modules/@blockcast/mmt-base/src/mmtp.ts +898 -0
- package/node_modules/@blockcast/mmt-fec/README.md +270 -0
- package/node_modules/@blockcast/mmt-fec/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts +80 -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 +294 -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 +72 -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 +192 -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 +185 -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 +273 -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 +22 -0
- package/node_modules/@blockcast/mmt-fec/dist/index.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/index.js +22 -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 +174 -0
- package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/oti.js +256 -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/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 +120 -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/index.d.ts +2 -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 +2 -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 +275 -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 +644 -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/package.json +49 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/index.ts +1 -0
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/mmtp.ts +898 -0
- package/node_modules/@blockcast/mmt-fec/package.json +79 -0
- package/node_modules/@blockcast/mmt-fec/src/block-manager.ts +369 -0
- package/node_modules/@blockcast/mmt-fec/src/block-processor.ts +277 -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 +332 -0
- package/node_modules/@blockcast/mmt-fec/src/index.ts +77 -0
- package/node_modules/@blockcast/mmt-fec/src/loc-symbols.ts +34 -0
- package/node_modules/@blockcast/mmt-fec/src/oti.ts +319 -0
- package/node_modules/@blockcast/mmt-fec/src/reorder-buffer.ts +101 -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 +90 -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 +462 -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 +636 -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 +203 -0
- package/schemas/catalog.schema.json +283 -0
- package/src/align.ts +235 -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/carp-catalog.ts +776 -0
- package/src/catalog-fec-params.ts +167 -0
- package/src/catalog-network-source.ts +49 -0
- package/src/catalog-validator.ts +34 -0
- package/src/cmaf-block-assembler.ts +1058 -0
- package/src/cmaf-group-batcher.ts +104 -0
- package/src/cmcd-producer.ts +329 -0
- package/src/codec-utils.ts +1539 -0
- package/src/fec-codec.ts +61 -0
- package/src/fec-manager.ts +1939 -0
- package/src/fec-starvation-detector.ts +128 -0
- package/src/fec-worker-setup.ts +177 -0
- package/src/fmp4-completion.ts +47 -0
- package/src/fmp4-reader.ts +112 -0
- package/src/index.ts +443 -0
- package/src/iso-bmff-timing.ts +688 -0
- package/src/loc.ts +181 -0
- package/src/manifest-auth-plan.ts +107 -0
- package/src/media-router.ts +1185 -0
- package/src/mfu-reassembler.ts +2842 -0
- package/src/mfu-reassembly-policy.ts +134 -0
- package/src/mmtp-container-parser.ts +1498 -0
- package/src/mmtp-decode-pipeline.ts +315 -0
- package/src/mmtp-router.ts +3027 -0
- package/src/mmtp-to-cmaf.ts +956 -0
- package/src/multicast-auth-catalog.ts +340 -0
- package/src/multicast-input.ts +439 -0
- package/src/multicast-manager.ts +1503 -0
- package/src/multicast-transport.ts +320 -0
- package/src/multicast-unwrap.ts +132 -0
- package/src/no-modules.ts +20 -0
- package/src/phase14-catalog-override.ts +336 -0
- package/src/repair-fetch.ts +217 -0
- package/src/sample-combiner.ts +846 -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 +189 -0
- package/src/track-pipeline.ts +332 -0
- package/src/track-session.ts +778 -0
- package/src/transport-pool.ts +128 -0
- package/src/wasm-mfu-reassembler.ts +372 -0
- package/src/wasm-mmtp-container-parser.ts +23 -0
- package/test-vectors/align.json +199 -0
- package/test-vectors/catalog/README.md +25 -0
- package/test-vectors/catalog/negative/legacy-selection-params.json +15 -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/network-source-object.json +25 -0
- package/test-vectors/catalog/negative/raptorq-unaligned-symbol.json +26 -0
- package/test-vectors/catalog/negative/repair-track-legacy-shape.json +18 -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/fec-multicast.json +51 -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/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 +148 -0
- package/test-vectors/catalog-validator-public-api.mjs +25 -0
- package/test-vectors/generate-source-auth-fixture.mjs +114 -0
- package/test-vectors/source-auth-v1.json +61 -0
- package/test-vectors/subsample-placement-v1.json +230 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/track-session.ts", "../src/fec-manager.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * TrackSession \u2014 per-track transport state machine.\n * SessionManager \u2014 orchestrates all sessions + TransportPool.\n *\n * Implements Patent Claim 9 (draft-ramadan-moq-fec APPLICATION-1):\n * (a) subscribe unicast\n * (b) simultaneously join multicast\n * (c) render from unicast during join\n * (d) activate multicast on RAP, suppress unicast\n * (e) cross-transport FEC repair during transition\n * (f) hysteresis before switching away from multicast\n * (g) re-subscribe unicast on sustained failure\n *\n * PIM: manages transport state without inspecting packet contents.\n */\n\nimport {\n TransportPool,\n type Subscription,\n type SubscriptionMode,\n type AcquireOptions,\n type DirectSocketHealth,\n type TransportPoolOptions,\n} from './transport-pool.js'\nimport { deriveFecInterleaveDepth } from '@blockcast/mmt-fec'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type TrackState =\n | 'idle'\n | 'unicast'\n | 'unicast-fallback'\n | 'joining'\n | 'multicast'\n | 'amt'\n | 'failing'\n\nexport interface TrackSessionConfig {\n trackName: string\n trackType: 'video' | 'audio'\n packetId: number\n /** FEC interleave depth from catalog (ms). Used for hysteresis timer. */\n fecInterleaveMs: number\n /** FEC K from catalog. Used for audio worklet latency calculation. */\n fecK: number\n /** FEC repair symbols per block (P). NOT symbol size. */\n fecRepairCount: number\n /** FEC symbol size in bytes (T). Required for WASM decoder OTI. */\n fecSymbolSize: number\n /** MoQ repair track name from catalog FEC config. */\n repairTrackName?: string\n /** Frame samples from initData parsing (audio-init-parser.ts). */\n frameSamples?: number\n /** Audio sample rate from catalog. */\n sampleRate?: number\n /** Ordered transport preference: best first. */\n preferredTransports: SubscriptionMode[]\n /** Subscription key for multicast/AMT: \"(S,G,port)\". */\n multicastKey?: string\n /** MoQ track name for unicast subscription. */\n moqTrackName?: string\n /** AMT relay address (if AMT is in preference chain). */\n amtRelay?: string\n /** MoQ relay URL (for WebTransport subscription). */\n moqRelayUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// PIM callbacks (protocol-independent \u2014 transport state only)\n// ---------------------------------------------------------------------------\n\nexport interface SessionManagerCallbacks {\n /** Fired on every transport state transition. */\n onStateChange?(trackName: string, from: TrackState, to: TrackState): void\n /** Fired to suppress/unsuppress unicast for this track (transport arbitration). */\n onSuppress?(trackName: string, suppressed: boolean): void\n /**\n * Fired when a track sat in `joining` for SessionManager.takeoverDeadlineMs\n * without a RAP (BLO-9413/D9). The session has already returned to `unicast`\n * (nothing was suppressed, so nothing to restore) \u2014 the host should abandon\n * the multicast attempt (e.g. MediaRouter.failbackToMoq()) so the router\n * stops waiting for a keyframe that is never coming.\n */\n onTakeoverTimeout?(trackName: string): void\n}\n\n// ---------------------------------------------------------------------------\n// MMTP application-layer extension (FEC lifecycle, media resets)\n// ---------------------------------------------------------------------------\n\n/**\n * MMTP track events (discriminated union).\n * Each protocol layer defines its own event union \u2014 MMTP events cover\n * media resets, latency, and FEC lifecycle.\n */\nexport type MmtpTrackEvent =\n | { type: 'reset' }\n | { type: 'latency'; latencyMs: number }\n | { type: 'fec-setup' }\n | { type: 'fec-teardown' }\n\n/**\n * MMTP application-layer extension of SessionManagerCallbacks.\n * Single onTrackEvent dispatches all MMTP-specific events.\n * PIM consumers use the base SessionManagerCallbacks (no onTrackEvent).\n *\n * IWA can define its own subinterface with its own TrackEvent union\n * (e.g., IwaTrackEvent for socket health, AMT state) using the same pattern.\n */\nexport interface MmtpSessionCallbacks extends SessionManagerCallbacks {\n onTrackEvent?(trackName: string, event: MmtpTrackEvent, config: TrackSessionConfig): void\n}\n\n/**\n * Loss-hysteresis window covering the signaled interleave window plus jitter.\n *\n * This path lacks media cadence, so it intentionally does not estimate a full\n * source-plus-repair production horizon. FECManager uses\n * computeFecDeliveryWindowMs with media cadence when configuring block\n * inactivity deadlines.\n *\n * `fecK` no longer feeds the math; it is still validated as a catalog\n * well-formedness guard and kept in the signature for positional API compat\n * (removal is a follow-up with the next cross-repo pin bump).\n */\nexport function computeFecLossWindowMs(fecInterleaveMs: number, fecK: number, jitterBufferMs?: number): number {\n return computeFecDeliveryWindowMs({\n interleaveDepthMs: fecInterleaveMs,\n sourceSymbols: fecK,\n jitterBufferMs,\n })\n}\n\nexport interface FecDeliveryWindowConfig {\n interleaveDepthMs: number\n sourceSymbols: number\n repairSymbols?: number\n symbolSize?: number\n mediaUnitsPerSecond?: number\n bandwidthBps?: number\n /**\n * Jitter + one-way-delay + reorder budget in ms (draft-ramadan-moq-fec \u00A79),\n * sourced from the catalog `jitterBufferMs`. Added on top of the max-of-spans\n * core; it is NOT a multiplier. See the margin-policy note in the function.\n */\n jitterBufferMs?: number\n}\n\n/**\n * Catalog-derived silence window that covers interleave geometry plus the time\n * required to produce or serialize a complete source+repair block. The catalog\n * field remains D media units; this function does not feed mapper geometry.\n */\nexport function computeFecDeliveryWindowMs(\n config: FecDeliveryWindowConfig,\n): number {\n const fecInterleaveMs = config.interleaveDepthMs\n const fecK = config.sourceSymbols\n if (fecInterleaveMs <= 0) {\n throw new Error(\n `[TrackSession] fecInterleaveMs=${fecInterleaveMs}. ` +\n `Derive a positive FEC interleave window from the stream catalog.`\n )\n }\n if (fecK <= 0) {\n throw new Error(\n `[TrackSession] fecK=${fecK}. ` +\n `Derive a positive FEC source-symbol count from the stream catalog.`\n )\n }\n\n const spansMs = [fecInterleaveMs]\n if (config.mediaUnitsPerSecond && config.mediaUnitsPerSecond > 0) {\n const mediaUnitMs = 1000 / config.mediaUnitsPerSecond\n // interleaveDepthMs is integer catalog signaling while media-unit duration\n // is often fractional (for example 4/30s = 133.33ms). Recover D using the\n // producer's nearest-ms quantization, then cover every source and repair\n // round-robin cycle. A decoder cannot recover a partial MFU until the\n // repair tail arrives, so a K-only deadline expires the source fragments\n // before the final repair symbols that complete them.\n const interleaveUnits = deriveFecInterleaveDepth(fecInterleaveMs, mediaUnitMs)\n const deliverySymbols = fecK + Math.max(0, config.repairSymbols ?? 0)\n spansMs.push(deliverySymbols * interleaveUnits * mediaUnitMs)\n }\n if (\n config.symbolSize &&\n config.symbolSize > 0 &&\n config.bandwidthBps &&\n config.bandwidthBps > 0\n ) {\n const repairSymbols = Math.max(0, config.repairSymbols ?? 0)\n spansMs.push(\n ((fecK + repairSymbols) * config.symbolSize * 8 * 1000) /\n config.bandwidthBps,\n )\n }\n\n // draft-ramadan-moq-fec \u00A79: the recovery timeout is a DERIVED budget \u2014\n // max(interleave span, source+repair production horizon, block serialization) PLUS\n // an ADDITIVE margin for jitter + one-way delay + reorder. It MUST NOT be a\n // fixed multiple of the interleave span (this replaces the retired `* 3` /\n // `* 2` safety factors). The margin is the catalog `jitterBufferMs`.\n //\n // MARGIN-POLICY (operator sign-off \u2014 see PR): no catalog currently emits\n // `jitterBufferMs`. Rather than throw (which would break every existing\n // catalog) or invent a magic constant, the default margin is the un-rounded\n // interleave window \u2014 the spec's natural reorder-budget bound and already a\n // catalog value. Emit `jitterBufferMs` in the catalog to set it explicitly.\n const coreMs = Math.max(...spansMs)\n const marginMs =\n config.jitterBufferMs !== undefined && config.jitterBufferMs >= 0\n ? config.jitterBufferMs\n : fecInterleaveMs\n return Math.ceil(coreMs) + Math.ceil(marginMs)\n}\n\n/** Convert the shared FEC delivery deadline into an integer media timescale. */\nexport function computeFecDeliveryWindowMediaUnits(\n config: FecDeliveryWindowConfig,\n timestampTimescale: number,\n): number {\n if (!Number.isFinite(timestampTimescale) || timestampTimescale <= 0) {\n throw new Error(\n `[TrackSession] timestampTimescale=${timestampTimescale}. ` +\n 'Derive a positive media timescale from the container.',\n )\n }\n const deliveryWindow = Math.ceil(\n computeFecDeliveryWindowMs(config) * timestampTimescale / 1000,\n )\n if (!Number.isSafeInteger(deliveryWindow)) {\n throw new Error('[TrackSession] FEC delivery window exceeds the media timestamp range.')\n }\n return deliveryWindow\n}\n\n// ---------------------------------------------------------------------------\n// TrackSession\n// ---------------------------------------------------------------------------\n\nexport class TrackSession {\n readonly config: TrackSessionConfig\n #state: TrackState = 'idle'\n #subscription: Subscription | null = null\n #hysteresisTimer: ReturnType<typeof setTimeout> | null = null\n #joinDeadlineTimer: ReturnType<typeof setTimeout> | null = null\n #previousPrimary: SubscriptionMode | null = null\n #manager: SessionManager\n // Single suppression authority flag (BLO-9413 failback coherence): every\n // suppress this session fires is recorded here, and every path that leaves\n // a suppressed state unwinds through #unsuppress(). Before this flag,\n // unsubscribe() only un-suppressed from multicast/amt \u2014 a session torn down\n // from `failing` (exactly where a starving track sits) was re-subscribed to\n // unicast with the host's media still muted: MoQ playing into a dead sink.\n #suppressFired = false\n\n constructor(config: TrackSessionConfig, manager: SessionManager) {\n this.config = config\n this.#manager = manager\n }\n\n // -----------------------------------------------------------------------\n // State\n // -----------------------------------------------------------------------\n\n get state(): TrackState { return this.#state }\n get subscription(): Subscription | null { return this.#subscription }\n\n /** True while this session's onSuppress(true) is outstanding (no matching\n * onSuppress(false) yet). Test/assertion surface for failback coherence. */\n get isSuppressed(): boolean { return this.#suppressFired }\n\n /** Current active transport mode, or null if idle. */\n get transport(): SubscriptionMode | null {\n switch (this.#state) {\n case 'multicast': return 'multicast'\n case 'amt': return 'amt'\n case 'unicast': return 'moq'\n case 'unicast-fallback': return 'moq' // SSM bridge absent \u2014 MoQ unicast is the only source\n case 'joining': return 'moq' // MoQ still primary during join\n case 'failing': return this.#previousPrimary\n default: return null\n }\n }\n\n // -----------------------------------------------------------------------\n // Computed (no magic numbers)\n // -----------------------------------------------------------------------\n\n /** Audio worklet latency: max(fecInterleaveMs, frameDurationMs * fecK).\n * Returns undefined if frame duration unknown (no initData). */\n get audioLatencyMs(): number | undefined {\n const fd = this.frameDurationMs\n if (fd === undefined) {\n return this.config.fecInterleaveMs > 0 ? this.config.fecInterleaveMs : undefined\n }\n return Math.max(this.config.fecInterleaveMs, fd * this.config.fecK)\n }\n\n /** Frame duration in ms, derived from frameSamples + sampleRate. */\n get frameDurationMs(): number | undefined {\n const { frameSamples, sampleRate } = this.config\n if (!frameSamples || !sampleRate || sampleRate <= 0) return undefined\n return (frameSamples / sampleRate) * 1000\n }\n\n // -----------------------------------------------------------------------\n // Transitions (Patent Claim 9)\n // -----------------------------------------------------------------------\n\n /** idle \u2192 unicast: player subscribes to this track. */\n subscribe(): void {\n if (this.#state !== 'idle') return\n this.#transition('unicast')\n\n // Emit audio latency on first subscribe\n if (this.config.trackType === 'audio') {\n const latency = this.audioLatencyMs\n if (latency !== undefined) {\n this.#manager.mmtp?.onTrackEvent?.(this.config.trackName, { type: 'latency', latencyMs: latency }, this.config)\n }\n }\n }\n\n /** unicast \u2192 joining: multicast/AMT capability detected, join in progress. */\n joinStarted(): void {\n if (this.#state !== 'unicast') return\n this.#transition('joining')\n // FEC decoders must be ready before multicast data arrives (Claim 9e: cross-transport repair)\n this.#manager.mmtp?.onTrackEvent?.(this.config.trackName, { type: 'fec-setup' }, this.config)\n }\n\n /**\n * any non-fallback state \u2192 unicast-fallback. Used when the SSM bridge is\n * absent (vanilla Chromium / no extension) and the host falls back to a\n * MoQ unicast subscription per track. Terminal-ish: activateMulticast,\n * failbackAll, and transportFailed are no-ops while in this state.\n *\n * Fires fec-setup so the per-track repair worker spins up \u2014 repair packets\n * still flow over the MoQ repair-track subscription, so FEC source+repair\n * can converge as in the multicast path.\n *\n * Audio latency is NOT re-emitted here: subscribe() already fires latency\n * once on the idle \u2192 unicast edge, and enterUnicastFallback is only ever\n * reached from a non-idle state (SessionManager skips idle sessions).\n */\n enterUnicastFallback(): void {\n if (this.#state === 'unicast-fallback') return\n this.#clearHysteresis()\n this.#transition('unicast-fallback')\n // Suppress the redundant unicast media decoder. On the no-bridge path the\n // host feeds MoQ-unicast source packets into MulticastManager (the render\n // consumer); a second base-media subscribe (the @moq/watch decoder) would\n // split the single connection's QUIC flow-control budget and stall delivery\n // (BLO-8969). Fire suppress here, on the fallback edge, so the decoder is\n // gated off before it subscribes rather than after the relay forward task\n // has already wedged.\n this.#suppress()\n this.#manager.mmtp?.onTrackEvent?.(this.config.trackName, { type: 'fec-setup' }, this.config)\n }\n\n /**\n * unicast-fallback \u2192 unicast. Called when the SSM bridge appears mid-session\n * (extension installed after fallback engaged). Host first aborts the fallback\n * drainers, then transitions sessions back to unicast so the regular\n * multicast/AMT preference chain can take over via joinStarted().\n */\n exitUnicastFallback(): void {\n if (this.#state !== 'unicast-fallback') return\n this.#transition('unicast')\n // Un-suppress the unicast decoder: the bridge appeared mid-session, so the\n // host tears down the fallback drainers and the regular preference chain\n // (joinStarted -> joining -> rapReceived) will re-suppress on multicast RAP.\n this.#unsuppress()\n }\n\n /**\n * joining \u2192 multicast or amt: RAP received on the joined transport (Claim 9d).\n * Resets audio worklet, then suppresses MoQ.\n */\n rapReceived(mode: 'multicast' | 'amt'): void {\n if (this.#state !== 'joining') return\n this.#previousPrimary = mode\n this.#transition(mode)\n if (this.config.trackType === 'audio') {\n this.#manager.mmtp?.onTrackEvent?.(this.config.trackName, { type: 'reset' }, this.config)\n }\n this.#suppress()\n }\n\n /** multicast/amt \u2192 failing: loss detected, start hysteresis timer (Claim 9f). */\n lossDetected(): void {\n if (this.#state !== 'multicast' && this.#state !== 'amt') return\n this.#previousPrimary = this.#state as SubscriptionMode\n this.#transition('failing')\n\n // Hysteresis timer: sustained loss before failback (Claim 9f). The window\n // covers a complete FEC block plus safety margin: the catalog interleave\n // value IS the block span (interleaveDepthMs, \u00A75.1), scaled by the same\n // safety factor FECManager uses for its block timeout.\n const hysteresisMs = computeFecLossWindowMs(this.config.fecInterleaveMs, this.config.fecK)\n this.#hysteresisTimer = setTimeout(() => {\n this.hysteresisExpired()\n }, hysteresisMs)\n }\n\n /** failing \u2192 previous primary: loss recovered within hysteresis window. */\n lossRecovered(): void {\n if (this.#state !== 'failing') return\n this.#clearHysteresis()\n if (this.#previousPrimary) {\n this.#transition(this.#previousPrimary as TrackState)\n }\n }\n\n /**\n * Transport failed (AMT relay down, multicast interface gone).\n * Try next transport in preference chain.\n */\n transportFailed(): void {\n this.#clearHysteresis()\n // Unicast-fallback is terminal \u2014 the host owns the subscription and\n // SessionManager transport machinery does not arbitrate it.\n if (this.#state === 'unicast-fallback') return\n const current = this.#previousPrimary ?? this.transport\n const prefs = this.config.preferredTransports\n const currentIdx = current ? prefs.indexOf(current) : -1\n const nextMode = prefs[currentIdx + 1]\n\n if (nextMode) {\n // Failover to next in chain\n if (nextMode === 'moq') {\n this.#transition('unicast')\n this.#unsuppress()\n if (this.config.trackType === 'audio') {\n this.#manager.mmtp?.onTrackEvent?.(this.config.trackName, { type: 'reset' }, this.config)\n }\n } else {\n // Next is multicast or AMT \u2014 start joining. `joining` means MoQ is\n // primary again (see transport getter) \u2014 if the failed transport had\n // suppressed unicast, restore it for the join window or the viewer\n // watches black video until the next RAP (which may never come).\n this.#transition('joining')\n this.#unsuppress()\n }\n } else {\n // End of chain \u2014 fall back to unicast (Claim 9g)\n this.#transition('unicast')\n this.#unsuppress()\n if (this.config.trackType === 'audio') {\n this.#manager.mmtp?.onTrackEvent?.(this.config.trackName, { type: 'reset' }, this.config)\n }\n }\n }\n\n /** Hysteresis expired \u2192 failback to next transport (Claim 9g). */\n hysteresisExpired(): void {\n if (this.#state !== 'failing') return\n this.#clearHysteresis()\n this.transportFailed()\n }\n\n /** \u2192 idle: player unsubscribes / tab close / cleanup. */\n unsubscribe(): void {\n this.#clearHysteresis()\n if (this.#state !== 'idle') {\n this.#transition('idle')\n // Unwind any outstanding suppression regardless of which state we left.\n // The old `wasActive` check (multicast/amt only) leaked suppression when\n // tearing down from `failing` or `unicast-fallback` \u2014 failbackAll()'s\n // unsubscribe\u2192subscribe cycle then re-entered unicast with the host's\n // media still muted (BLO-9413 freeze: \"failing back\" logged, video dead).\n this.#unsuppress()\n }\n }\n\n /** True when SessionManager.enterUnicastFallback put this track into the\n * host-driven unicast subscription mode. Host can iterate sessions to\n * decide whether to keep its fallback drainer alive across MediaRouter\n * arbitration events. */\n get isUnicastFallback(): boolean {\n return this.#state === 'unicast-fallback'\n }\n\n dispose(): void {\n this.#clearHysteresis()\n this.#clearJoinDeadline()\n this.#state = 'idle'\n this.#subscription = null\n }\n\n // -----------------------------------------------------------------------\n // Internal\n // -----------------------------------------------------------------------\n\n #transition(to: TrackState): void {\n const from = this.#state\n if (from === to) return\n this.#state = to\n // Takeover deadline (BLO-9413/D9): every entry into `joining` arms it\n // (joinStarted AND transportFailed-failover both route through here);\n // every exit cancels it. Without this, a transport that accepts the\n // subscribe but never delivers a RAP parks the session in `joining`\n // forever (observed live in the Phase-0 headless repro).\n if (from === 'joining') this.#clearJoinDeadline()\n if (to === 'joining') this.#armJoinDeadline()\n // FEC teardown: leaving FEC-active states \u2192 non-FEC state\n const fecActive = (s: TrackState) =>\n s === 'joining' ||\n s === 'multicast' ||\n s === 'amt' ||\n s === 'failing' ||\n s === 'unicast-fallback'\n if (fecActive(from) && !fecActive(to)) {\n this.#manager.mmtp?.onTrackEvent?.(this.config.trackName, { type: 'fec-teardown' }, this.config)\n }\n this.#manager.callbacks?.onStateChange?.(this.config.trackName, from, to)\n }\n\n #clearHysteresis(): void {\n if (this.#hysteresisTimer !== null) {\n clearTimeout(this.#hysteresisTimer)\n this.#hysteresisTimer = null\n }\n }\n\n #armJoinDeadline(): void {\n this.#clearJoinDeadline()\n const deadlineMs = this.#manager.takeoverDeadlineMs\n if (deadlineMs <= 0) return // disabled\n this.#joinDeadlineTimer = setTimeout(() => {\n this.#joinDeadlineTimer = null\n if (this.#state !== 'joining') return\n // No RAP within the deadline: return to unicast. MoQ was never\n // suppressed in `joining`, so there is nothing to restore \u2014 this edge\n // must stay silent on onSuppress (asserted in tests).\n this.#transition('unicast')\n this.#manager.callbacks?.onTakeoverTimeout?.(this.config.trackName)\n }, deadlineMs)\n }\n\n #clearJoinDeadline(): void {\n if (this.#joinDeadlineTimer !== null) {\n clearTimeout(this.#joinDeadlineTimer)\n this.#joinDeadlineTimer = null\n }\n }\n\n /** Fire onSuppress(true) exactly once per suppression epoch. */\n #suppress(): void {\n if (this.#suppressFired) return\n this.#suppressFired = true\n this.#manager.callbacks?.onSuppress?.(this.config.trackName, true)\n }\n\n /** Unwind an outstanding suppression; no-op when none is outstanding. */\n #unsuppress(): void {\n if (!this.#suppressFired) return\n this.#suppressFired = false\n this.#manager.callbacks?.onSuppress?.(this.config.trackName, false)\n }\n}\n\n// ---------------------------------------------------------------------------\n// SessionManager\n// ---------------------------------------------------------------------------\n\nexport class SessionManager {\n readonly pool: TransportPool\n readonly sessions = new Map<string, TrackSession>()\n callbacks: SessionManagerCallbacks | null = null\n /**\n * Takeover deadline (BLO-9413/D9): max time a session may sit in `joining`\n * without a RAP before returning to `unicast` and firing onTakeoverTimeout.\n * MoQ is never suppressed during `joining`, so expiry is a silent abandon \u2014\n * playback never glitches. Hosts tune per stream (e.g. GOP length); 0\n * disables.\n */\n takeoverDeadlineMs = 5000\n\n /** Narrow to MMTP callbacks (onTrackEvent). Safe \u2014 optional chaining handles PIM-only consumers. */\n get mmtp(): MmtpSessionCallbacks | null {\n return this.callbacks as MmtpSessionCallbacks | null\n }\n\n constructor(poolOptions?: TransportPoolOptions) {\n this.pool = new TransportPool(poolOptions)\n\n // Wire pool failure \u2192 per-track failover\n this.pool.onSubscriptionFailed(sub => {\n for (const trackName of sub.consumers) {\n const session = this.sessions.get(trackName)\n if (session) session.transportFailed()\n }\n })\n }\n\n // -----------------------------------------------------------------------\n // Track management\n // -----------------------------------------------------------------------\n\n /** Add a track session from catalog config. */\n addTrack(config: TrackSessionConfig): TrackSession {\n if (this.sessions.has(config.trackName)) {\n throw new Error(`[SessionManager] Track \"${config.trackName}\" already exists`)\n }\n const session = new TrackSession(config, this)\n this.sessions.set(config.trackName, session)\n return session\n }\n\n /** Remove a track session. Releases subscription with grace period. */\n removeTrack(trackName: string): void {\n const session = this.sessions.get(trackName)\n if (!session) return\n session.unsubscribe()\n\n // Release pool subscription\n const subKey = session.config.multicastKey ?? session.config.moqTrackName\n if (subKey) this.pool.releaseSubscription(subKey, trackName)\n\n session.dispose()\n this.sessions.delete(trackName)\n }\n\n // -----------------------------------------------------------------------\n // Atomic multi-track operations\n // -----------------------------------------------------------------------\n\n /**\n * All tracks with multicast RAP \u2192 multicast state (atomic).\n * Called when MediaRouter fires onSourceSwitch('multicast').\n * Prevents split-brain between video and audio.\n *\n * A takeover deadline can return a track from `joining` to `unicast` while\n * the multicast socket keeps delivering packets. If a valid RAP arrives\n * later, allow that late-RAP edge to suppress unicast and promote multicast.\n */\n activateMulticast(): void {\n for (const [, session] of this.sessions) {\n if (session.state === 'unicast') {\n session.joinStarted()\n }\n if (session.state === 'joining') {\n session.rapReceived('multicast')\n }\n }\n }\n\n /**\n * All tracks with AMT RAP \u2192 amt state (atomic).\n */\n activateAmt(): void {\n for (const [, session] of this.sessions) {\n if (session.state === 'unicast') {\n session.joinStarted()\n }\n if (session.state === 'joining') {\n session.rapReceived('amt')\n }\n }\n }\n\n /**\n * All tracks \u2192 unicast (failback, atomic).\n * Called when MulticastManager fires onFailback().\n * unicast-fallback sessions are skipped \u2014 the host owns those subscriptions\n * and they must drain uninterrupted across transport-state events.\n *\n * Coherence invariant (BLO-9413): after this returns, every non-fallback\n * session is `unicast` (or `idle`) AND unsuppressed \u2014 unsubscribe() unwinds\n * any outstanding suppression regardless of the state it tears down from\n * (`failing` included; see TrackSession.#suppressFired).\n */\n failbackAll(): void {\n for (const [, session] of this.sessions) {\n if (\n session.state !== 'idle' &&\n session.state !== 'unicast' &&\n session.state !== 'unicast-fallback'\n ) {\n session.unsubscribe()\n session.subscribe() // re-enter unicast\n }\n }\n }\n\n /**\n * All tracks \u2192 unicast-fallback (atomic).\n * Called by the host when the multicast gateway is absent and\n * the player will subscribe to source tracks over MoQ unicast and feed\n * packets via MulticastManager.feedUnicastSource() (or manager.router\n * directly). Sessions in this state stay terminal until the host calls\n * unsubscribe() / disposes the SessionManager.\n */\n enterUnicastFallback(): void {\n for (const [, session] of this.sessions) {\n if (session.state === 'idle') continue\n session.enterUnicastFallback()\n }\n }\n\n /**\n * True if any session is currently in unicast-fallback. Hosts use this to\n * gate `multicastTrack.close()` on failback events \u2014 closing the shared\n * broadcast would kill the host's fallback drainers.\n */\n hasActiveFallback(): boolean {\n for (const [, session] of this.sessions) {\n if (session.isUnicastFallback) return true\n }\n return false\n }\n\n /**\n * All fallback tracks \u2192 unicast (atomic).\n * Called when the SSM bridge appears mid-session and the host has aborted\n * the fallback drainers. The session re-enters the regular preference chain\n * via joinStarted() (issued by the host immediately after).\n */\n exitUnicastFallback(): void {\n for (const [, session] of this.sessions) {\n if (session.isUnicastFallback) session.exitUnicastFallback()\n }\n }\n\n // -----------------------------------------------------------------------\n // Cleanup lifecycle\n // -----------------------------------------------------------------------\n\n /** Tab closed \u2014 release all subscriptions for this tab (grace period). */\n tabClosed(tabId: string): void {\n // All tracks owned by this tab \u2192 idle\n for (const [trackName, session] of this.sessions) {\n session.unsubscribe()\n this.pool.releaseAllForConsumer(trackName)\n }\n }\n\n /** Bridge WebTransport connection dropped \u2014 same as tab close. */\n bridgeDisconnected(): void {\n this.tabClosed('bridge')\n }\n\n /**\n * Track selection changed (user or ABR).\n * Old track released (grace period), new track subscribed.\n */\n trackSelectionChanged(oldTrackName: string, newConfig: TrackSessionConfig): void {\n this.removeTrack(oldTrackName)\n const session = this.addTrack(newConfig)\n session.subscribe()\n }\n\n // -----------------------------------------------------------------------\n // Query\n // -----------------------------------------------------------------------\n\n /** Get a track session by name. */\n getTrack(trackName: string): TrackSession | undefined {\n return this.sessions.get(trackName)\n }\n\n // -----------------------------------------------------------------------\n // Cleanup\n // -----------------------------------------------------------------------\n\n dispose(): void {\n for (const [, session] of this.sessions) {\n session.dispose()\n }\n this.sessions.clear()\n this.pool.dispose()\n this.callbacks = null\n }\n}\n", "/**\n * FEC Manager \u2014 codec-agnostic FEC decode service.\n *\n * Receives (packetId, sbn, esi, symbolData) tuples from any transport\n * (MMTP, LOC, etc.) and manages FEC block lifecycle: source counting,\n * WASM RaptorQ decode, block timeout, and recovery/failure notification.\n *\n * Extracted from MulticastManager to decouple FEC block logic from\n * MMTP packet parsing and frame routing.\n */\n\nimport { computeFecDeliveryWindowMs } from './track-session.js'\nimport { deriveFecInterleaveDepth } from '@blockcast/mmt-fec'\n\nimport type { MmtpDeliveryProvenance } from './mfu-reassembler.js'\nimport type {\n FecWorkerClient,\n FecTrackStats,\n FecBlockSnapshot,\n FrameMeta,\n FecWorkerErrorCode,\n} from '@blockcast/fec-worker'\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Minimum valid FEC interleave depth in milliseconds, per spec \u00A75.1\n * (the interactive/gaming floor; broadcast/ATSC 3.0 ranges up to 10000ms).\n *\n * A protocol constant, NOT a tuning knob. Values below this floor are almost\n * certainly a raw frame count (e.g. `2`) that a caller passed when no framerate\n * was available to convert to ms (common on audio tracks). Feeding such a value\n * into computeFecTimeout treats it as `2ms`, yielding a near-zero FEC block\n * timeout that abandons startup FEC blocks before their repair symbols arrive\n * (measured: moqtail startupFailed=4, shaka dropped=5.8% on startup). Timing\n * derivations MUST reject sub-floor values and fall back to the established\n * timeout instead of collapsing.\n */\nexport const MIN_INTERLEAVE_DEPTH_MS = 33\nconst WORKER_ERROR_LOG_CAP = 5\nconst WORKER_ERROR_KIND_CAP = 32\nconst WORKER_ERROR_OVERFLOW_KIND = 'other'\nconst STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Algorithm-agnostic FEC decoder backend interface.\n *\n * Implementations:\n * - RaptorQ WASM (mmt_wasm.js) \u2014 RFC 6330\n * - Reed-Solomon WASM \u2014 future\n * - XOR \u2014 future\n *\n * Source/repair symbol IDs follow ISO 23008-1 \u00A7C.5.2/C.5.3:\n * source: SS_ID (flat 32-bit symbol identifier)\n * repair: SS_Start + SSB_length + RS_ID (block-scoped repair coords)\n */\nexport type WasmFecDecoder = {\n add_source(ss_id: number, data: Uint8Array, ts: number): Uint8Array | null\n add_repair(ss_start: number, ssb_length: number, rs_id: number, data: Uint8Array, ts: number): Uint8Array | null\n configure(oti: Uint8Array): void\n cleanup(current_time: number): Uint8Array\n cleanup_blocks_detailed(sbns: Uint32Array): Uint8Array\n flush?(): void\n reset_stats?(): void\n}\n\n/** Algorithm-agnostic FEC stats snapshot. */\nexport interface FecStats {\n sourcePackets: number\n repairPackets: number\n duplicateSymbols: number\n blocksClean: number\n blocksRecovered: number\n blocksFailed: number\n greenFillFrames: number\n blocksComplete: number\n mixedPathBlocks: number\n recentBlockPathCounts: FecBlockPathCount[]\n k: number\n /** Of blocks with loss, what percentage were recovered (0-100). */\n recoveryRate: number\n /** What percentage of blocks had any loss (0-100). */\n packetLossRate: number\n /** Per-MMTP-packet-ID counters for the synchronous WASM decoder path. */\n tracks: FecPacketStats[]\n /** Latest Worker/WASM snapshot for each packet ID. */\n workers: Array<FecTrackStats & { packetId: number }>\n /** Main-thread repair symbols rejected before dispatch because their size did not match T. */\n repairSymbolSizeMismatches: number\n /** Bounded Worker error counts keyed by stable code, or message text for legacy errors. */\n workerErrors: Record<string, number>\n}\n\nexport interface FecPacketStats {\n packetId: number\n sourcePackets: number\n repairPackets: number\n duplicateSymbols: number\n blocksClean: number\n blocksRecovered: number\n blocksFailed: number\n greenFillFrames: number\n blocksComplete: number\n k: number\n recoveryRate: number\n packetLossRate: number\n}\n\nexport type FecSymbolSource = 'multicast' | 'moq'\n\nexport interface FecBlockPathCount {\n packetId: number\n sbn: number\n catalogTrackName?: string\n multicastSymbols: number\n moqSymbols: number\n complete: boolean\n}\n\ntype FecLifecycleTrace = {\n atMs: number\n event: 'repair-requested' | 'repair-arrived' | 'timeout-queued' | 'cleanup-ack' | 'recovered' | 'failed'\n packetId: number\n sbn: number\n catalogTrackName?: string\n sourceCount: number\n sourceEsis: number[]\n repairRsIds: number[]\n decoderPath: 'worker' | 'sync' | 'none'\n deadlineRemainingMs?: number\n pendingCleanup: boolean\n rsId?: number\n}\n\nexport interface FECManagerConfig {\n k?: number\n /** Symbol size in bytes (T). NOT repair count. */\n symbolSize?: number\n interleaveDepthMs?: number\n fps?: number\n /** Guarded block inactivity deadline derived from the full delivery geometry. */\n timeoutMs?: number\n}\n\ninterface ResolvedFECTrackConfig {\n k: number\n symbolSize: number\n interleaveDepthMs: number\n timeoutMs: number\n fps?: number\n}\n\ninterface MutableFecCounters {\n sourcePackets: number\n repairPackets: number\n duplicateSymbols: number\n blocksClean: number\n blocksRecovered: number\n blocksFailed: number\n greenFillFrames: number\n}\n\nfunction newFecCounters(): MutableFecCounters {\n return {\n sourcePackets: 0,\n repairPackets: 0,\n duplicateSymbols: 0,\n blocksClean: 0,\n blocksRecovered: 0,\n blocksFailed: 0,\n greenFillFrames: 0,\n }\n}\n\n// ---------------------------------------------------------------------------\n// FECManager\n// ---------------------------------------------------------------------------\n\nexport class FECManager {\n // Per-track WASM RaptorQ decoders, keyed by physical stream identity.\n #fecDecoders = new Map<string, { client: WasmFecDecoder; k: number }>()\n /** @deprecated Use setFecDecoder(packetId, ...). Backwards-compat shim. */\n\n // Per-track Worker-backed FEC decoders, keyed by physical stream identity.\n // Worker-backed tracks use async decode \u2014 recovered frames arrive via onFrame callback,\n // not synchronous return from feedSourceSymbol/feedRepairSymbol.\n #workerClients = new Map<string, FecWorkerClient>()\n // Worker setup can finish while an interleaved block is already collecting\n // on the sync decoder. Keep that block on its original decoder so later\n // source/repair symbols do not split across two RaptorQ states.\n #syncDecoderBlockOwners = new Map<string, WasmFecDecoder>()\n #decoderBlockOwners = new WeakMap<object, Map<number, {\n catalogTrackName?: string\n conflicted: boolean\n }>>()\n\n #fecK = 4\n #fecSymbolSize = 0\n #fecTimeoutMs = 500 // default 500ms; refined by setFramerate() when fps known\n #trackConfigs = new Map<number, ResolvedFECTrackConfig>()\n #fecStats = newFecCounters()\n #fecTrackStats = new Map<number, MutableFecCounters>()\n #fecWorkerStats = new Map<number, FecTrackStats>()\n #fecStatsObservers = new Set<(packetId: number, stats: FecTrackStats) => void>()\n\n // Block inactivity tracking: (packetId, SBN) -> timer. The deadline is\n // derived from catalog geometry and refreshed on progress; only an idle\n // block fails.\n #activeBlocks = new Map<string, ReturnType<typeof setTimeout>>()\n #activeBlockDeadlinesMs = new Map<string, number>()\n #activeBlockTimeoutOwners = new Map<string, object>()\n #lifecycleTrace: FecLifecycleTrace[] = []\n // Worker-backed timeouts enqueue cleanup behind all prior symbol messages.\n // Failure remains pending until cleanupComplete acknowledges that queue drain.\n #pendingWorkerCleanups = new Set<string>()\n #pendingWorkerCleanupClients = new Map<string, FecWorkerClient>()\n // Source symbol count per track-scoped block \u2014 marks complete when K symbols arrive.\n #blockSourceCounts = new Map<string, number>()\n #blockSourceEsis = new Map<string, Set<number>>()\n #blockRepairRsIds = new Map<string, Set<number>>()\n // Blocks already recovered \u2014 prevents late source symbols from restarting timeouts.\n // WASM removes blocks after recovery; late symbols create empty blocks returning null,\n // which would falsely start new timeouts without this guard.\n #completedBlocks = new Set<string>()\n #seenSymbols = new Set<string>()\n #blockPathCounts = new Map<string, FecBlockPathCount>()\n // Exact catalog identity observed on accepted symbols for each FEC block.\n // Conflicting identities collapse to null so recovery fails closed.\n #blockCatalogTrackNames = new Map<string, string | null>()\n // A receiver joins an already-running live stream in the middle of D\n // interleaved source blocks. This applies to both multicast and reliable MoQ:\n // a MoQ group boundary is not an FEC ESI=0 boundary. Those blocks are not\n // packet loss because their earlier ESIs predate the subscription. Ignore\n // each such block until ESI=0 establishes a complete boundary for the track.\n // After that boundary, a first observed ESI>0 is real delivery loss and must\n // continue through normal FEC recovery/failure accounting.\n #startupAlignedPackets = new Set<string>()\n #startupPartialBlocks = new Set<string>()\n // A live multicast join can observe an ESI=0 source boundary before the\n // repair lane catches up. Returning false here also drops that media packet\n // at MmtpRouter, so the readiness probe is multicast-only and bounded: an\n // actual multicast repair unlocks the next boundary, while a second source\n // boundary fails open. Reliable MoQ media continues through normal startup\n // alignment and retained MoQ repair cannot prove multicast readiness.\n #startupRequireRepairReady = false\n #startupRepairReadyPackets = new Set<string>()\n #startupRepairBoundaryCounts = new Map<string, number>()\n #startupRepairFailOpenPackets = new Set<string>()\n // SBNs that reached K source-symbol arrivals via trackBlock. Used by the\n // worker recovery suppression check (see onFrame handler below) so we only\n // suppress `recovered=true` emits for blocks that ALSO completed cleanly\n // via source symbols. Separating this from #completedBlocks (which is also\n // populated by every worker frame emit, by failed timeouts, and by external\n // callers) was the BLO-3323 L15 fix: the prior predicate caused blocksRecovered\n // to stay at 0 because the SBN was always in #completedBlocks by the time the\n // worker fired recovered=true.\n #cleanlyCompletedBlocks = new Set<string>()\n // D-28: blocks whose repair deadline already fired onRepairNeeded. At most\n // one repair request per block; entries are removed when the block\n // completes, recovers, or fails.\n #repairRequestedBlocks = new Set<string>()\n // D-28: repair-deadline timers refreshed by accepted source progress until\n // the first repair request. A `null` value marks a deadline that already\n // fired (never re-armed). At most K accepted source symbols can refresh the\n // horizon, so browser scheduling delay cannot create an early loss signal\n // and repair traffic cannot keep a block alive indefinitely.\n #repairDeadlines = new Map<string, ReturnType<typeof setTimeout> | null>()\n // Worker recovery emits all K source symbols. Track recovery once per block\n // while routing only ESI slots that were absent from the direct source path.\n #workerRecoveredBlocks = new Set<string>()\n\n // Framerate detection state\n #fpsDetected = false\n #fecFps?: number\n #fecInterleaveDepthMs = 0\n\n // Worker error log rate-limiter \u2014 bounds console.warn output and exported\n // error cardinality when the FEC Worker emits the same failure repeatedly\n // with volatile packet details (e.g. ssStart) in the message text.\n // Stable Worker codes aggregate those messages; uncoded/legacy messages are\n // capped into an `other` bucket. This also covers wasm-bindgen\n // \"recursive use of an object\" bubbled from add_source/add_repair during\n // overlapping message processing). Without this, Point 5 of the 7-point\n // validation harness \u2014 which caps headless warnings at 500 \u2014 fails even\n // though the errors are duplicates of a single underlying event.\n #workerErrorCounts = new Map<string, number>()\n #repairSymbolSizeMismatchCount = 0\n\n // Event callbacks \u2014 block lifecycle notifications for MmtpRouter wiring.\n // NOTE: For sync WASM path, recovery routing is handled by MmtpRouter from\n // feedSourceSymbol/feedRepairSymbol return values. For Worker path, recovery\n // arrives via onBlockRecovered callback.\n onBlockStarted:\n | ((sbn: number, packetId: number, catalogTrackName?: string) => void)\n | null = null\n onBlockComplete:\n | ((sbn: number, packetId: number, catalogTrackName?: string) => void)\n | null = null\n onBlockFailed:\n | ((sbn: number, packetId?: number, catalogTrackName?: string) => void)\n | null = null\n\n // D-28 on-demand repair callbacks. When onRepairNeeded is registered, each\n // block arms a one-shot repair deadline after the derived K-symbol source\n // horizon; a block still short of K when it fires requests a per-SBN MoQ\n // repair fetch upstream. The inactivity fail bound is unchanged either way \u2014\n // repair and failure are orthogonal timers, so wiring D-28 never delays\n // failure accounting. onRepairResolved fires when a repair-requested block\n // leaves the active set, with the outcome (\"completed\" after clean completion\n // or recovery, \"failed\" at the fail bound) so the upstream in-flight slot\n // (MulticastManager.clearRepairForBlock) can be released and logged.\n onRepairNeeded:\n | ((sbn: number, packetId: number, catalogTrackName?: string) => void)\n | null = null\n onRepairResolved:\n | ((\n sbn: number,\n packetId: number,\n outcome: 'completed' | 'failed',\n catalogTrackName?: string,\n ) => void)\n | null = null\n\n // Worker-path callbacks \u2014 async recovery and stats from FecWorkerClient.\n onBlockRecovered: ((sbn: number, packetId: number, data: ArrayBuffer, meta: FrameMeta, catalogTrackName?: string) => void) | null = null\n onFecStatsUpdate: ((packetId: number, stats: FecTrackStats) => void) | null = null\n onGreenFill: ((sbn: number, packetId: number, data: ArrayBuffer) => void) | null = null\n\n #streamKey(packetId: number, catalogTrackName?: string): string {\n return catalogTrackName === undefined\n ? `${packetId}`\n : `${packetId}:${encodeURIComponent(catalogTrackName)}`\n }\n\n #belongsToPacket(key: string, packetId: number): boolean {\n return Number(key.split(':', 1)[0]) === packetId\n }\n\n #belongsToStream(key: string, packetId: number, catalogTrackName: string): boolean {\n if (!this.#belongsToPacket(key, packetId)) return false\n const parts = key.split(':')\n const encodedTrackName = encodeURIComponent(catalogTrackName)\n if (parts.length === 2) return parts[1] === encodedTrackName\n if (parts.length >= 3) return parts[2] === encodedTrackName\n return false\n }\n\n #packetIdsForStreamKeys(keys: Set<string>): number[] {\n return [...new Set([...keys].map((key) => Number(key.split(':', 1)[0])))]\n }\n\n #blockKey(sbn: number, packetId: number, catalogTrackName?: string): string {\n const base = `${packetId}:${sbn}`\n return catalogTrackName === undefined\n ? base\n : `${base}:${encodeURIComponent(catalogTrackName)}`\n }\n\n #parseBlockKey(key: string): { packetId: number; sbn: number; catalogTrackName?: string } {\n const [packetIdText, sbnText, ...catalogParts] = key.split(':')\n const catalogTrackName = catalogParts.length > 0\n ? decodeURIComponent(catalogParts.join(':'))\n : undefined\n return {\n packetId: Number(packetIdText) || 0,\n sbn: Number(sbnText) || 0,\n ...(catalogTrackName === undefined ? {} : { catalogTrackName }),\n }\n }\n\n #decoderForPacket(packetId: number, catalogTrackName?: string) {\n return this.#fecDecoders.get(this.#streamKey(packetId, catalogTrackName))\n ?? this.#fecDecoders.get(this.#streamKey(packetId))\n }\n\n #workerForPacket(packetId: number, catalogTrackName?: string) {\n return this.#workerClients.get(this.#streamKey(packetId, catalogTrackName))\n ?? this.#workerClients.get(this.#streamKey(packetId))\n }\n\n #claimDecoderBlock(\n client: object,\n sbn: number,\n catalogTrackName?: string,\n ): boolean {\n let blocks = this.#decoderBlockOwners.get(client)\n if (!blocks) {\n blocks = new Map()\n this.#decoderBlockOwners.set(client, blocks)\n }\n const owner = blocks.get(sbn)\n if (!owner) {\n blocks.set(sbn, { catalogTrackName, conflicted: false })\n if (blocks.size > 512) {\n const stale = [...blocks.keys()].slice(0, 256)\n for (const key of stale) blocks.delete(key)\n }\n return true\n }\n if (owner.conflicted) return false\n if (owner.catalogTrackName === catalogTrackName) return true\n owner.conflicted = true\n return false\n }\n\n #decoderBlockCatalogTrackName(client: object, sbn: number): string | undefined {\n const owner = this.#decoderBlockOwners.get(client)?.get(sbn)\n return owner?.conflicted ? undefined : owner?.catalogTrackName\n }\n\n #resetDecoderBlockOwners(client: object, catalogTrackName: string): void {\n const blocks = this.#decoderBlockOwners.get(client)\n if (!blocks) return\n for (const [sbn, owner] of blocks) {\n if (owner.catalogTrackName === catalogTrackName || owner.conflicted) {\n blocks.delete(sbn)\n }\n }\n if (blocks.size === 0) this.#decoderBlockOwners.delete(client)\n }\n\n #discardFecDecoderClient(client: WasmFecDecoder): void {\n for (const [key, entry] of this.#fecDecoders) {\n if (entry.client === client) this.#fecDecoders.delete(key)\n }\n this.#decoderBlockOwners.delete(client)\n }\n\n #assertExclusiveDecoderClient(\n streamKey: string,\n client: object,\n registrations: Iterable<readonly [string, object]>,\n kind: 'decoder' | 'worker',\n ): void {\n for (const [registeredKey, registeredClient] of registrations) {\n if (registeredClient !== client || registeredKey === streamKey) continue\n throw new Error(\n `[FECManager] ${kind} client is already registered for stream ${registeredKey}; ` +\n `create one client per physical stream`,\n )\n }\n }\n\n #statsForPacket(packetId: number): MutableFecCounters {\n let stats = this.#fecTrackStats.get(packetId)\n if (!stats) {\n stats = newFecCounters()\n this.#fecTrackStats.set(packetId, stats)\n }\n return stats\n }\n\n #markPathBlockComplete(sbn: number, packetId: number, catalogTrackName?: string): void {\n const block = this.#blockPathCounts.get(this.#blockKey(sbn, packetId, catalogTrackName))\n if (block) block.complete = true\n }\n\n /** Cancel and forget a block's D-28 repair-deadline timer. */\n #clearRepairDeadline(key: string): void {\n const timer = this.#repairDeadlines.get(key)\n if (timer != null) clearTimeout(timer)\n this.#repairDeadlines.delete(key)\n }\n\n #requestRepair(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): void {\n const key = this.#blockKey(sbn, packetId, catalogTrackName)\n if (\n this.#completedBlocks.has(key) ||\n !this.onRepairNeeded ||\n this.#repairRequestedBlocks.has(key)\n ) return\n\n const deadline = this.#repairDeadlines.get(key)\n if (deadline) clearTimeout(deadline)\n this.#repairDeadlines.set(key, null)\n this.#repairRequestedBlocks.add(key)\n\n // Preserve the source-progress inactivity deadline and append exactly one\n // bounded repair-fetch grace. Once requested, noteBlockActivity() no\n // longer moves this terminal cleanup boundary.\n const config = this.#resolveTrackConfig(packetId)\n const graceMs = this.#repairGraceMs(config)\n const now = performance.now()\n const currentDeadline = this.#activeBlockDeadlinesMs.get(key) ?? now\n this.#armBlockTimeout(\n sbn,\n packetId,\n catalogTrackName,\n Math.max(1, Math.ceil(Math.max(now, currentDeadline) + graceMs - now)),\n )\n this.#traceLifecycle('repair-requested', sbn, packetId, catalogTrackName)\n if (catalogTrackName === undefined) {\n this.onRepairNeeded(sbn, packetId)\n } else {\n this.onRepairNeeded(sbn, packetId, catalogTrackName)\n }\n }\n\n #traceLifecycle(\n event: FecLifecycleTrace['event'],\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n rsId?: number,\n ): void {\n const key = this.#blockKey(sbn, packetId, catalogTrackName)\n const deadline = this.#activeBlockDeadlinesMs.get(key)\n const decoderPath = this.#syncDecoderBlockOwners.has(key)\n ? 'sync'\n : this.#workerForPacket(packetId, catalogTrackName)\n ? 'worker'\n : this.#decoderForPacket(packetId, catalogTrackName)\n ? 'sync'\n : 'none'\n this.#lifecycleTrace.push({\n atMs: Date.now(),\n event,\n packetId,\n sbn,\n ...(catalogTrackName === undefined ? {} : { catalogTrackName }),\n sourceCount: this.#blockSourceCounts.get(key) ?? 0,\n sourceEsis: [...(this.#blockSourceEsis.get(key) ?? [])].sort((a, b) => a - b),\n repairRsIds: [...(this.#blockRepairRsIds.get(key) ?? [])].sort((a, b) => a - b),\n decoderPath,\n ...(deadline === undefined\n ? {}\n : { deadlineRemainingMs: Math.max(0, deadline - performance.now()) }),\n pendingCleanup: this.#pendingWorkerCleanups.has(key),\n ...(rsId === undefined ? {} : { rsId }),\n })\n if (this.#lifecycleTrace.length > 64) this.#lifecycleTrace.splice(0, 32)\n }\n\n #defaultTrackConfig(): ResolvedFECTrackConfig {\n return {\n k: this.#fecK,\n symbolSize: this.#fecSymbolSize,\n interleaveDepthMs: this.#fecInterleaveDepthMs,\n fps: this.#fecFps,\n timeoutMs: this.#fecTimeoutMs,\n }\n }\n\n #resolveTrackConfig(packetId: number): ResolvedFECTrackConfig {\n return this.#trackConfigs.get(packetId) ?? this.#defaultTrackConfig()\n }\n\n #computeTimeout(config: {\n k: number\n interleaveDepthMs: number\n fps?: number\n timeoutMs?: number\n }): number {\n if (config.timeoutMs && config.timeoutMs > 0) return config.timeoutMs\n // Only a valid \u00A75.1 ms interleave (>= MIN_INTERLEAVE_DEPTH_MS) drives the\n // timeout. A sub-floor value is a raw frame count (no fps to convert with);\n // fall back to the established #fecTimeoutMs rather than collapse to ~0.\n if (config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) {\n return Math.max(\n this.#fecTimeoutMs,\n computeFecDeliveryWindowMs({\n interleaveDepthMs: config.interleaveDepthMs,\n sourceSymbols: config.k,\n mediaUnitsPerSecond: config.fps,\n }),\n )\n }\n return this.#fecTimeoutMs\n }\n\n #repairDeadlineMs(config: ResolvedFECTrackConfig): number {\n const halfFailBoundMs = Math.ceil(config.timeoutMs / 2)\n if (\n config.k > 0 &&\n config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS &&\n config.fps !== undefined &&\n Number.isFinite(config.fps) &&\n config.fps > 0\n ) {\n const mediaUnitMs = 1000 / config.fps\n const interleaveUnits = deriveFecInterleaveDepth(\n config.interleaveDepthMs,\n mediaUnitMs,\n )\n const sourceHorizonMs = Math.ceil(config.k * interleaveUnits * mediaUnitMs)\n if (sourceHorizonMs < config.timeoutMs) return sourceHorizonMs\n }\n return halfFailBoundMs\n }\n\n #repairGraceMs(config: ResolvedFECTrackConfig): number {\n return Math.max(1, config.timeoutMs - this.#repairDeadlineMs(config))\n }\n\n #armBlockTimeout(\n sbn: number,\n packetId: number,\n catalogTrackName: string | undefined,\n delayMs: number,\n ): void {\n const key = this.#blockKey(sbn, packetId, catalogTrackName)\n const current = this.#activeBlocks.get(key)\n if (current) clearTimeout(current)\n const boundedDelayMs = Math.max(1, Math.ceil(delayMs))\n this.#activeBlockDeadlinesMs.set(key, performance.now() + boundedDelayMs)\n const owner = {}\n this.#activeBlockTimeoutOwners.set(key, owner)\n const timer = setTimeout(() => {\n // clearTimeout() cannot retract a callback that the browser already\n // moved onto its task queue. Only the timer that still owns this\n // block's deadline may fail it or enqueue Worker cleanup.\n if (this.#activeBlockTimeoutOwners.get(key) !== owner) return\n this.#activeBlocks.delete(key)\n this.#activeBlockDeadlinesMs.delete(key)\n this.#activeBlockTimeoutOwners.delete(key)\n this.#traceLifecycle('timeout-queued', sbn, packetId, catalogTrackName)\n\n // A block that began on the synchronous decoder remains owned by that\n // decoder even if Worker setup completed later. Cleanup must follow the\n // same ownership rule as source and repair symbol dispatch.\n const syncOwner = this.#syncDecoderBlockOwners.get(key)\n if (syncOwner) {\n this.#routeSyncCleanup(syncOwner, sbn, packetId)\n this.#failBlock(sbn, packetId, false, catalogTrackName)\n return\n }\n\n const workerClient = this.#workerForPacket(packetId, catalogTrackName)\n if (workerClient) {\n this.#pendingWorkerCleanups.add(key)\n this.#pendingWorkerCleanupClients.set(key, workerClient)\n workerClient.cleanup(sbn)\n } else {\n const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client\n if (decoder) {\n this.#routeSyncCleanup(decoder, sbn, packetId)\n this.#failBlock(sbn, packetId, false, catalogTrackName)\n } else {\n this.#failBlock(sbn, packetId, false, catalogTrackName)\n }\n }\n }, boundedDelayMs)\n this.#activeBlocks.set(key, timer)\n }\n\n #upsertTrackConfig(packetId: number, config: FECManagerConfig): ResolvedFECTrackConfig {\n const previous = this.#trackConfigs.get(packetId) ?? this.#defaultTrackConfig()\n const timingChanged =\n config.k !== undefined ||\n config.interleaveDepthMs !== undefined ||\n config.fps !== undefined\n const next: ResolvedFECTrackConfig = {\n k: config.k ?? previous.k,\n symbolSize:\n config.symbolSize != null && config.symbolSize > 0\n ? config.symbolSize\n : previous.symbolSize,\n interleaveDepthMs: config.interleaveDepthMs ?? previous.interleaveDepthMs,\n fps: config.fps ?? previous.fps,\n timeoutMs: config.timeoutMs ?? (timingChanged ? 0 : previous.timeoutMs),\n }\n next.timeoutMs = this.#computeTimeout(next)\n this.#trackConfigs.set(packetId, next)\n return next\n }\n\n #normalizeSymbolData(packetId: number, data: Uint8Array): Uint8Array {\n const symbolSize = this.symbolSizeForPacket(packetId)\n if (symbolSize <= 0 || data.byteLength === symbolSize) return data\n const normalized = new Uint8Array(symbolSize)\n normalized.set(data.subarray(0, symbolSize))\n return normalized\n }\n\n #blockKeysFor(sbn: number, packetId?: number, catalogTrackName?: string): string[] {\n if (packetId != null && catalogTrackName !== undefined) {\n return [this.#blockKey(sbn, packetId, catalogTrackName)]\n }\n\n const keys = new Set<string>()\n for (const source of [\n this.#activeBlocks,\n this.#blockSourceCounts,\n this.#blockCatalogTrackNames,\n ]) {\n for (const key of source.keys()) {\n const block = this.#parseBlockKey(key)\n if (\n block.sbn === sbn &&\n (packetId === undefined || block.packetId === packetId)\n ) keys.add(key)\n }\n }\n for (const source of [this.#completedBlocks, this.#cleanlyCompletedBlocks]) {\n for (const key of source) {\n const block = this.#parseBlockKey(key)\n if (\n block.sbn === sbn &&\n (packetId === undefined || block.packetId === packetId)\n ) keys.add(key)\n }\n }\n\n if (packetId !== undefined) {\n if (keys.size === 0) return [this.#blockKey(sbn, packetId)]\n return keys.size === 1 ? [...keys] : []\n }\n return keys.size > 0 ? [...keys] : [this.#blockKey(sbn, 0, catalogTrackName)]\n }\n\n // -----------------------------------------------------------------------\n // Accessors\n // -----------------------------------------------------------------------\n\n /** Internal state for diagnostics \u2014 not part of the public stats API. */\n get diagnostics() {\n return {\n timeoutMs: this.#fecTimeoutMs,\n activeBlocks: this.#activeBlocks.size,\n completedBlocks: this.#completedBlocks.size,\n pendingCounts: Object.fromEntries(this.#blockSourceCounts),\n fpsDetected: this.#fpsDetected,\n interleaveDepthMs: this.#fecInterleaveDepthMs,\n startupAlignedPackets: this.#packetIdsForStreamKeys(this.#startupAlignedPackets),\n startupPartialBlocks: this.#startupPartialBlocks.size,\n startupRepairReadyPackets: this.#packetIdsForStreamKeys(this.#startupRepairReadyPackets),\n startupRepairFailOpenPackets: this.#packetIdsForStreamKeys(this.#startupRepairFailOpenPackets),\n pendingWorkerCleanups: this.#pendingWorkerCleanups.size,\n repairTrackedBlocks: this.#blockRepairRsIds.size,\n lifecycleTrace: this.#lifecycleTrace.map((entry) => ({ ...entry })),\n }\n }\n\n get fecStats(): FecStats {\n const s = this.#fecStats\n const affected = s.blocksRecovered + s.blocksFailed\n const total = s.blocksClean + affected\n const tracks = [...this.#fecTrackStats.entries()]\n .sort(([left], [right]) => left - right)\n .map(([packetId, stats]) => {\n const trackAffected = stats.blocksRecovered + stats.blocksFailed\n const trackTotal = stats.blocksClean + trackAffected\n return {\n ...stats,\n packetId,\n blocksComplete: trackTotal,\n k: this.#resolveTrackConfig(packetId).k,\n recoveryRate: trackAffected > 0\n ? (stats.blocksRecovered / trackAffected) * 100\n : 100,\n packetLossRate: trackTotal > 0 ? (trackAffected / trackTotal) * 100 : 0,\n }\n })\n return {\n ...s,\n blocksComplete: total,\n mixedPathBlocks: Array.from(this.#blockPathCounts.values()).filter(\n (block) => block.multicastSymbols > 0 && block.moqSymbols > 0,\n ).length,\n recentBlockPathCounts: Array.from(this.#blockPathCounts.values()).slice(-24),\n k: this.#fecK,\n // True recovery rate: of blocks that HAD loss, how many were recovered?\n recoveryRate: affected > 0 ? (s.blocksRecovered / affected) * 100 : 100,\n packetLossRate: total > 0 ? (affected / total) * 100 : 0,\n tracks,\n workers: [...this.#fecWorkerStats.entries()]\n .sort(([left], [right]) => left - right)\n .map(([packetId, stats]) => ({\n ...stats,\n config: {\n ...stats.config,\n resolution: stats.config.resolution ? { ...stats.config.resolution } : undefined,\n },\n blocks: stats.blocks.map((block) => ({ ...block })),\n decoder: stats.decoder ? { ...stats.decoder } : undefined,\n altaFailedDetails: stats.altaFailedDetails?.map((detail) => ({ ...detail })),\n packetId,\n })),\n repairSymbolSizeMismatches: this.#repairSymbolSizeMismatchCount,\n workerErrors: Object.fromEntries(this.#workerErrorCounts),\n }\n }\n\n /**\n * Observe Worker-backed per-track snapshots without taking ownership of the\n * legacy single-callback slot. Infrastructure diagnostics and a player may\n * therefore consume the same source event without wrapping or replacing one\n * another's callback.\n */\n addFecStatsObserver(\n observer: (packetId: number, stats: FecTrackStats) => void,\n ): () => void {\n this.#fecStatsObservers.add(observer)\n return () => this.#fecStatsObservers.delete(observer)\n }\n\n /** Start a new multicast transport join without discarding track/decoder state. */\n beginMulticastJoin(): void {\n this.#startupAlignedPackets.clear()\n this.#startupPartialBlocks.clear()\n this.#startupRequireRepairReady = true\n this.#startupRepairReadyPackets.clear()\n this.#startupRepairBoundaryCounts.clear()\n this.#startupRepairFailOpenPackets.clear()\n }\n\n /** Begin a fresh decode epoch for one physical stream. */\n resetTrack(packetId: number, catalogTrackName: string): void {\n if (!Number.isInteger(packetId) || packetId < 0) {\n throw new Error(`[FECManager] invalid packetId for reset: ${packetId}`)\n }\n if (!catalogTrackName) {\n throw new Error('[FECManager] catalogTrackName is required for track reset')\n }\n const resetMap = <T>(map: Map<string, T>): void => {\n for (const key of map.keys()) {\n if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue\n map.delete(key)\n }\n }\n const resetTimerMap = (\n map: Map<string, ReturnType<typeof setTimeout> | null>,\n ): void => {\n for (const [key, timer] of map) {\n if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue\n if (timer != null) clearTimeout(timer)\n map.delete(key)\n }\n }\n const resetSet = (set: Set<string>): void => {\n for (const key of set) {\n if (this.#belongsToStream(key, packetId, catalogTrackName)) set.delete(key)\n }\n }\n\n resetTimerMap(this.#activeBlocks)\n resetTimerMap(this.#repairDeadlines)\n resetMap(this.#activeBlockDeadlinesMs)\n resetMap(this.#activeBlockTimeoutOwners)\n resetMap(this.#pendingWorkerCleanupClients)\n resetMap(this.#blockSourceCounts)\n resetMap(this.#blockSourceEsis)\n resetMap(this.#blockRepairRsIds)\n resetMap(this.#blockPathCounts)\n resetMap(this.#blockCatalogTrackNames)\n resetMap(this.#syncDecoderBlockOwners)\n resetMap(this.#startupRepairBoundaryCounts)\n resetSet(this.#pendingWorkerCleanups)\n resetSet(this.#completedBlocks)\n resetSet(this.#cleanlyCompletedBlocks)\n resetSet(this.#repairRequestedBlocks)\n resetSet(this.#workerRecoveredBlocks)\n resetSet(this.#startupAlignedPackets)\n resetSet(this.#startupPartialBlocks)\n resetSet(this.#startupRepairReadyPackets)\n resetSet(this.#startupRepairFailOpenPackets)\n resetSet(this.#seenSymbols)\n\n const decoderClients = new Set<WasmFecDecoder>()\n for (const [key, entry] of this.#fecDecoders) {\n if (this.#belongsToStream(key, packetId, catalogTrackName)) {\n decoderClients.add(entry.client)\n }\n }\n const clients = new Set<object>()\n for (const client of decoderClients) {\n if (\n typeof client.flush === 'function' &&\n typeof client.reset_stats === 'function'\n ) {\n try {\n client.flush()\n client.reset_stats()\n clients.add(client)\n } catch {\n this.#discardFecDecoderClient(client)\n }\n } else {\n this.#discardFecDecoderClient(client)\n }\n }\n for (const [key, client] of this.#workerClients) {\n if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue\n client.reset()\n clients.add(client)\n }\n for (const client of clients) this.#resetDecoderBlockOwners(client, catalogTrackName)\n }\n\n /** Record a transport symbol and reject later duplicates of the same packetId/SBN/ESI. */\n feedSymbolFromSource(\n packetId: number,\n sbn: number,\n esi: number,\n _data: Uint8Array,\n source: FecSymbolSource,\n catalogTrackName?: string,\n ): boolean {\n const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)\n const streamKey = this.#streamKey(packetId, catalogTrackName)\n const k = this.kForPacket(packetId)\n const isSourceSymbol = esi >= 0 && esi < k\n const isStartupPartial = this.#startupPartialBlocks.has(blockKey)\n const awaitingMulticastRepair =\n this.#startupRequireRepairReady &&\n source === 'multicast' &&\n !this.#startupRepairReadyPackets.has(streamKey) &&\n !this.#startupRepairFailOpenPackets.has(streamKey)\n if (awaitingMulticastRepair) {\n if (!isSourceSymbol) {\n this.#startupRepairReadyPackets.add(streamKey)\n this.#startupRepairBoundaryCounts.delete(streamKey)\n this.#markStartupPartial(blockKey)\n return false\n }\n\n if (esi === 0) {\n const boundaries = (this.#startupRepairBoundaryCounts.get(streamKey) ?? 0) + 1\n this.#startupRepairBoundaryCounts.set(streamKey, boundaries)\n if (boundaries >= STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES) {\n this.#startupRepairBoundaryCounts.delete(streamKey)\n this.#startupRepairFailOpenPackets.add(streamKey)\n } else {\n this.#markStartupPartial(blockKey)\n return false\n }\n } else {\n this.#markStartupPartial(blockKey)\n return false\n }\n }\n if (isSourceSymbol && esi === 0) {\n // ESI=0 establishes a complete source boundary for this SBN. Remove\n // only this block: other interleaved SBNs may still be join partials.\n this.#startupPartialBlocks.delete(blockKey)\n this.#startupAlignedPackets.add(streamKey)\n } else if (\n isStartupPartial ||\n !this.#startupAlignedPackets.has(streamKey)\n ) {\n this.#markStartupPartial(blockKey)\n return false\n }\n\n // A completed block remains authoritative after its individual symbol\n // identities age out of the bounded #seenSymbols cache. Without this\n // guard, a delayed copy from the other transport can recreate the block\n // inside a Worker decoder; that orphan never receives the full source and\n // repair set, so cleanup later reports a false failure/green fill.\n if (this.#completedBlocks.has(blockKey)) {\n this.#fecStats.duplicateSymbols++\n this.#statsForPacket(packetId).duplicateSymbols++\n return false\n }\n\n const symbolKey = `${blockKey}:${esi}`\n if (this.#seenSymbols.has(symbolKey)) {\n this.#fecStats.duplicateSymbols++\n this.#statsForPacket(packetId).duplicateSymbols++\n return false\n }\n this.#seenSymbols.add(symbolKey)\n\n const counts = this.#blockPathCounts.get(blockKey) ?? {\n packetId,\n sbn,\n ...(catalogTrackName === undefined ? {} : { catalogTrackName }),\n multicastSymbols: 0,\n moqSymbols: 0,\n complete: false,\n }\n if (source === 'multicast') counts.multicastSymbols++\n else counts.moqSymbols++\n this.#blockPathCounts.set(blockKey, counts)\n this.#observeBlockCatalogTrackName(blockKey, catalogTrackName)\n\n if (this.#seenSymbols.size > 4096) {\n this.#seenSymbols = new Set(Array.from(this.#seenSymbols).slice(-2048))\n }\n if (this.#blockPathCounts.size > 512) {\n this.#blockPathCounts = new Map(Array.from(this.#blockPathCounts).slice(-256))\n }\n if (this.#blockCatalogTrackNames.size > 512) {\n this.#blockCatalogTrackNames = new Map(\n Array.from(this.#blockCatalogTrackNames).slice(-256),\n )\n }\n\n return true\n }\n\n #observeBlockCatalogTrackName(blockKey: string, catalogTrackName?: string): void {\n if (catalogTrackName === undefined) return\n if (!this.#blockCatalogTrackNames.has(blockKey)) {\n this.#blockCatalogTrackNames.set(blockKey, catalogTrackName)\n return\n }\n if (this.#blockCatalogTrackNames.get(blockKey) !== catalogTrackName) {\n this.#blockCatalogTrackNames.set(blockKey, null)\n }\n }\n\n /** Exact catalog identity retained for async recovery, if unambiguous. */\n catalogTrackNameForBlock(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): string | undefined {\n if (catalogTrackName !== undefined) {\n return this.#blockCatalogTrackNames.get(\n this.#blockKey(sbn, packetId, catalogTrackName),\n ) ?? undefined\n }\n const names = new Set<string>()\n for (const [key, name] of this.#blockCatalogTrackNames) {\n const block = this.#parseBlockKey(key)\n if (block.packetId === packetId && block.sbn === sbn && name !== null) names.add(name)\n }\n return names.size === 1 ? [...names][0] : undefined\n }\n\n #markStartupPartial(blockKey: string): void {\n this.#startupPartialBlocks.add(blockKey)\n if (this.#startupPartialBlocks.size > 128) {\n this.#startupPartialBlocks = new Set(\n Array.from(this.#startupPartialBlocks).slice(-64),\n )\n }\n }\n\n /** Per-block state snapshot for FecBlockGrid visualization.\n * Returns the most recent blocks (active + recently completed). */\n getBlockStates(): Array<{\n sbn: number;\n packetId?: number;\n state: 'collecting' | 'complete' | 'failed';\n sourceCount: number;\n k: number;\n progress: number;\n }> {\n const blocks: Array<{ sbn: number; packetId?: number; state: 'collecting' | 'complete' | 'failed'; sourceCount: number; k: number; progress: number }> = [];\n // Active blocks (still collecting symbols)\n for (const [key] of this.#activeBlocks) {\n const { packetId, sbn } = this.#parseBlockKey(key);\n const count = this.#blockSourceCounts.get(key) ?? 0;\n const k = this.kForPacket(packetId);\n blocks.push({ sbn, packetId, state: 'collecting', sourceCount: count, k, progress: k > 0 ? Math.min(count / k, 1) : 0 });\n }\n // Recently completed blocks (last 16)\n const completed = [...this.#completedBlocks].slice(-16);\n for (const key of completed) {\n const { packetId, sbn } = this.#parseBlockKey(key);\n if (!this.#activeBlocks.has(key)) {\n const k = this.kForPacket(packetId);\n blocks.push({ sbn, packetId, state: 'complete', sourceCount: k, k, progress: 1 });\n }\n }\n blocks.sort((a, b) => a.sbn - b.sbn || (a.packetId ?? 0) - (b.packetId ?? 0));\n return blocks.slice(-24); // cap at 24 most recent\n }\n\n /** The raw WASM decoder for stats access (first registered decoder) */\n get wasmDecoder(): WasmFecDecoder | null {\n return this.#fecDecoders.values().next().value?.client ?? null\n }\n\n get k(): number { return this.#fecK }\n kForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).k }\n /** FEC symbol size in bytes. */\n get symbolSize(): number { return this.#fecSymbolSize }\n symbolSizeForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).symbolSize }\n /** FEC interleave window in milliseconds (0 = no interleaving). */\n get interleaveDepthMs(): number { return this.#fecInterleaveDepthMs }\n interleaveDepthMsForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).interleaveDepthMs }\n /** Resolved per-track block fail bound in milliseconds. */\n timeoutMsForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).timeoutMs }\n /** Resolved per-track D-28 repair request deadline in milliseconds. */\n repairDeadlineMsForPacket(packetId: number): number {\n return this.#repairDeadlineMs(this.#resolveTrackConfig(packetId))\n }\n /** Remaining fetch budget for a specific repair-requested block. */\n repairFetchTimeoutMsForBlock(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): number {\n const deadlineMs = this.#activeBlockDeadlinesMs.get(\n this.#blockKey(sbn, packetId, catalogTrackName),\n )\n if (deadlineMs !== undefined) {\n return Math.max(1, Math.ceil(deadlineMs - performance.now()))\n }\n const trackConfig = this.#resolveTrackConfig(packetId)\n return Math.max(1, trackConfig.timeoutMs - this.#repairDeadlineMs(trackConfig))\n }\n\n // -----------------------------------------------------------------------\n // Configuration\n // -----------------------------------------------------------------------\n\n /** Configure FEC parameters. */\n configure(config: FECManagerConfig): void {\n if (config.k != null) this.#fecK = config.k\n if (config.symbolSize != null && config.symbolSize > 0) this.#fecSymbolSize = config.symbolSize\n if (config.interleaveDepthMs != null) this.#fecInterleaveDepthMs = config.interleaveDepthMs\n if (config.fps != null && Number.isFinite(config.fps) && config.fps > 0) {\n this.#fecFps = config.fps\n }\n // interleaveDepthMs is in milliseconds per spec \u00A75.1.\n // Valid range: 33ms (interactive/gaming) to 10000ms (broadcast ATSC 3.0).\n // Warn if value looks like frame count instead of ms (common misconfiguration).\n if (config.interleaveDepthMs != null && config.interleaveDepthMs > 0 && config.interleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS) {\n console.warn(`[FECManager] interleaveDepthMs=${config.interleaveDepthMs} looks like frame count, not milliseconds (\u00A75.1 requires ms). Expected range: ${MIN_INTERLEAVE_DEPTH_MS}-10000ms.`)\n }\n if (config.timeoutMs != null && config.timeoutMs > 0) {\n this.#fecTimeoutMs = config.timeoutMs\n } else if (\n config.interleaveDepthMs != null &&\n config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS\n ) {\n this.#fecTimeoutMs = computeFecDeliveryWindowMs({\n interleaveDepthMs: config.interleaveDepthMs,\n sourceSymbols: this.#fecK,\n symbolSize: this.#fecSymbolSize,\n mediaUnitsPerSecond: config.fps,\n })\n }\n // A shared multicast parser receives every packetId. Keep existing track\n // deadlines at least as wide as the selected video delivery envelope so a\n // short audio interleave cannot expire during browser/AMT scheduling gaps.\n for (const [packetId, track] of this.#trackConfigs) {\n if (track.timeoutMs < this.#fecTimeoutMs) {\n this.#trackConfigs.set(packetId, { ...track, timeoutMs: this.#fecTimeoutMs })\n }\n }\n }\n\n /** Configure FEC parameters for one packetId without changing legacy defaults. */\n configureTrack(packetId: number, config: FECManagerConfig): void {\n this.#upsertTrackConfig(packetId, config)\n }\n\n /** Register a FEC decoder for a specific packetId (track). */\n setFecDecoder(\n packetId: number,\n client: WasmFecDecoder,\n k: number,\n config?: FECManagerConfig,\n catalogTrackName?: string,\n ): void {\n const streamKey = this.#streamKey(packetId, catalogTrackName)\n this.#assertExclusiveDecoderClient(\n streamKey,\n client,\n [...this.#fecDecoders].map(([key, entry]) => [key, entry.client] as const),\n 'decoder',\n )\n const trackConfig = this.#upsertTrackConfig(packetId, { ...config, k })\n this.#fecDecoders.set(streamKey, { client, k: trackConfig.k })\n }\n\n /**\n * Register a Worker-backed FEC decoder for a specific packetId (track).\n *\n * Worker-backed tracks decode asynchronously \u2014 feedSourceSymbol/feedRepairSymbol\n * return null immediately, and recovered frames arrive via the onBlockRecovered callback.\n *\n * Takes precedence over sync WASM decoders for the same packetId.\n */\n setFecWorker(\n packetId: number,\n client: FecWorkerClient,\n config: FECManagerConfig,\n catalogTrackName?: string,\n ): void {\n const k = config?.k\n if (k === undefined || !Number.isFinite(k) || k <= 0) {\n throw new Error(`[FECManager] packetId=${packetId} Worker requires an explicit positive k`)\n }\n const symbolSize = config.symbolSize\n if (symbolSize === undefined || !Number.isFinite(symbolSize) || symbolSize <= 0) {\n throw new Error(\n `[FECManager] packetId=${packetId} Worker requires an explicit positive symbolSize`,\n )\n }\n const streamKey = this.#streamKey(packetId, catalogTrackName)\n this.#assertExclusiveDecoderClient(\n streamKey,\n client,\n this.#workerClients,\n 'worker',\n )\n if (config) this.#upsertTrackConfig(packetId, config)\n const workerK = this.#resolveTrackConfig(packetId).k\n this.#workerClients.set(streamKey, client)\n\n client.onFrame((data: ArrayBuffer, meta: FrameMeta) => {\n if (meta.recovered) {\n // Recovered Worker events represent one exact source-symbol position.\n // Block-completion accounting needs that exact ESI, and counting an\n // ESI-less event as recovered would report healthy FEC while dropping\n // the media. Enforce the Worker contract before any recovery metric.\n if (!Number.isInteger(meta.esi) || meta.esi! < 0 || meta.esi! >= workerK) {\n const kind = 'recovered-symbol-missing-esi'\n const seen = this.#workerErrorCounts.get(kind) ?? 0\n this.#workerErrorCounts.set(kind, seen + 1)\n if (seen < WORKER_ERROR_LOG_CAP) {\n console.warn(\n `[FECManager] Dropping recovered symbol without valid ESI: pid=${packetId} sbn=${meta.sbn} esi=${String(meta.esi)}`,\n )\n }\n return\n }\n const esi = meta.esi!\n\n // Suppress duplicate routing when the worker fires recovered=true\n // for a block ALREADY CLEANLY COMPLETED by K source symbols.\n //\n // BLO-3323 L15: predicate was previously `#completedBlocks.has(sbn)`,\n // but `#completedBlocks` is populated by 5 paths (clean K-arrival,\n // failed timeout, every worker frame emit via markBlockCompleted below,\n // external markBlockCompleted, and cancelBlockTimeout). The first\n // worker frame for a block adds the SBN, so subsequent recovered=true\n // emits for the same block were always suppressed \u2192 blocksRecovered\n // stayed at 0 even when recovery actually fired. Switching to the\n // narrower #cleanlyCompletedBlocks set (populated only by trackBlock\n // when count >= K) preserves the original dedup intent (BLO-3035\n // layer 4 \u2014 don't double-route a block already received via source)\n // while letting genuinely-recovered blocks bump the counter.\n const recoveredCatalogTrackName = catalogTrackName\n ?? this.#decoderBlockCatalogTrackName(client, meta.sbn)\n if (!this.#claimDecoderBlock(client, meta.sbn, recoveredCatalogTrackName)) return\n const blockKey = this.#blockKey(meta.sbn, packetId, recoveredCatalogTrackName)\n const cleanlyCompleted = this.#cleanlyCompletedBlocks.has(blockKey)\n if (!cleanlyCompleted) {\n if (!this.#workerRecoveredBlocks.has(blockKey)) {\n this.#traceLifecycle(\n 'recovered',\n meta.sbn,\n packetId,\n recoveredCatalogTrackName,\n )\n this.#workerRecoveredBlocks.add(blockKey)\n this.#fecStats.blocksRecovered++\n this.#statsForPacket(packetId).blocksRecovered++\n if (this.#workerRecoveredBlocks.size > 1000) {\n const stale = Array.from(this.#workerRecoveredBlocks).slice(0, 500)\n for (const key of stale) this.#workerRecoveredBlocks.delete(key)\n }\n }\n\n // RaptorQ returns the full K-symbol block, not only erased symbols.\n // The direct path already parsed every source ESI in #seenSymbols;\n // routing those symbols again injects duplicate MFU fragments and can\n // leave a real FI=3 tail followed by duplicate FI=2 fragments.\n const symbolKey = `${blockKey}:${esi}`\n if (!this.#seenSymbols.has(symbolKey)) {\n this.#seenSymbols.add(symbolKey)\n this.onBlockRecovered?.(\n meta.sbn,\n packetId,\n data,\n meta,\n recoveredCatalogTrackName,\n )\n }\n }\n\n // The worker emits recovered source symbols in ESI order. Complete the\n // block only after the final slot so assemblers receive every missing\n // symbol before their completion callback flushes buffered media.\n if (\n esi >= workerK - 1 &&\n !this.#completedBlocks.has(blockKey)\n ) {\n this.cancelBlockTimeout(meta.sbn, packetId, recoveredCatalogTrackName)\n if (recoveredCatalogTrackName === undefined) {\n this.onBlockComplete?.(meta.sbn, packetId)\n } else {\n this.onBlockComplete?.(\n meta.sbn,\n packetId,\n recoveredCatalogTrackName,\n )\n }\n }\n } else {\n const completedCatalogTrackName = catalogTrackName\n ?? this.#decoderBlockCatalogTrackName(client, meta.sbn)\n this.markBlockCompleted(meta.sbn, packetId, completedCatalogTrackName)\n }\n })\n\n client.onSnapshot((stats: FecTrackStats) => {\n this.#fecWorkerStats.set(packetId, stats)\n try {\n this.onFecStatsUpdate?.(packetId, stats)\n } catch (error) {\n console.warn('[FECManager] FEC stats callback failed:', error)\n }\n for (const observer of this.#fecStatsObservers) {\n try {\n observer(packetId, stats)\n } catch (error) {\n console.warn('[FECManager] FEC stats observer failed:', error)\n }\n }\n })\n\n client.onGreenFill((sbn: number, data: ArrayBuffer) => {\n this.#fecStats.greenFillFrames++\n this.#statsForPacket(packetId).greenFillFrames++\n this.onGreenFill?.(sbn, packetId, data)\n })\n\n client.onCleanupComplete((sbn: number) => {\n const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {\n const block = this.#parseBlockKey(key)\n return block.packetId === packetId &&\n block.sbn === sbn &&\n this.#pendingWorkerCleanupClients.get(key) === client\n })\n for (const key of cleanupKeys) {\n this.#pendingWorkerCleanups.delete(key)\n this.#pendingWorkerCleanupClients.delete(key)\n if (this.#completedBlocks.has(key)) continue\n const block = this.#parseBlockKey(key)\n this.#traceLifecycle(\n 'cleanup-ack',\n sbn,\n packetId,\n block.catalogTrackName,\n )\n this.#failBlock(sbn, packetId, false, block.catalogTrackName)\n }\n })\n\n client.onError((msg: string, code?: FecWorkerErrorCode) => {\n const requestedKind = code ?? msg\n const kind =\n this.#workerErrorCounts.has(requestedKind) ||\n this.#workerErrorCounts.size < WORKER_ERROR_KIND_CAP - 1\n ? requestedKind\n : WORKER_ERROR_OVERFLOW_KIND\n const seen = this.#workerErrorCounts.get(kind) ?? 0\n this.#workerErrorCounts.set(kind, seen + 1)\n if (seen < WORKER_ERROR_LOG_CAP) {\n console.warn('[FECManager] Worker error:', msg)\n } else if (seen === WORKER_ERROR_LOG_CAP) {\n console.warn(\n `[FECManager] Worker error (suppressing further copies of ${kind}): ${msg}`,\n )\n }\n })\n }\n\n /** Called by player when framerate is known (from decoder output). Refines FEC block timeout. */\n setFramerate(fps: number): void {\n if (this.#fpsDetected || fps <= 0) return\n this.#fpsDetected = true\n this.#fecFps = fps\n // Only a valid \u00A75.1 ms interleave refines the timeout; a sub-floor value is\n // a raw frame count and would collapse it (see MIN_INTERLEAVE_DEPTH_MS).\n if (this.#fecInterleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) {\n this.#fecTimeoutMs = computeFecDeliveryWindowMs({\n interleaveDepthMs: this.#fecInterleaveDepthMs,\n sourceSymbols: this.#fecK,\n symbolSize: this.#fecSymbolSize,\n mediaUnitsPerSecond: fps,\n })\n }\n for (const [packetId, config] of this.#trackConfigs) {\n if (config.fps !== undefined || config.interleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS) continue\n this.#trackConfigs.set(packetId, {\n ...config,\n fps,\n timeoutMs: Math.max(\n this.#fecTimeoutMs,\n computeFecDeliveryWindowMs({\n interleaveDepthMs: config.interleaveDepthMs,\n sourceSymbols: config.k,\n symbolSize: config.symbolSize,\n mediaUnitsPerSecond: fps,\n }),\n ),\n })\n }\n }\n\n // -----------------------------------------------------------------------\n // Symbol feeding (codec-agnostic)\n // -----------------------------------------------------------------------\n\n /**\n * Feed a source symbol to the WASM FEC decoder.\n *\n * Does NOT track block source counts or fire lifecycle events \u2014 call\n * `trackBlock()` separately BEFORE this method (MmtpRouter does this).\n *\n * @returns Recovered block data if WASM completed the block, else null.\n * Caller is responsible for routing recovered data and updating\n * block completion state via `markBlockCompleted()`.\n */\n feedSourceSymbol(\n packetId: number,\n ssId: number,\n symbolData: Uint8Array,\n alta?: { auth: Uint8Array; payloadEnd: number },\n catalogTrackName?: string,\n ): Uint8Array | null {\n this.#fecStats.sourcePackets++\n this.#statsForPacket(packetId).sourcePackets++\n const fecData = this.#normalizeSymbolData(packetId, symbolData)\n const sbn = Math.floor(ssId / this.kForPacket(packetId))\n const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)\n const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey)\n\n // Worker-backed track: dispatch async, return null (recovery via onFrame callback)\n const workerClient = this.#workerForPacket(packetId, catalogTrackName)\n if (workerClient && !syncBlockOwner) {\n if (!this.#claimDecoderBlock(workerClient, sbn, catalogTrackName)) return null\n // Create clean ArrayBuffer copy \u2014 prevents transferring a shared underlying buffer\n // when symbolData is a view into a larger packet (T-03-06)\n const buf = fecData.buffer.slice(\n fecData.byteOffset,\n fecData.byteOffset + fecData.byteLength,\n ) as ArrayBuffer\n // Pass the flat SS_ID through to the worker. The WASM decoder derives\n // SBN/ESI internally per ISO 23008-1 \u00A7C.5.2; the WASM add_source\n // signature is (ss_id, data, timestamp) \u2014 pre-computing sbn/esi on this\n // side produces wrong WASM argument alignment and silently breaks\n // block assembly.\n if (alta) {\n // Copy auth into a fresh ArrayBuffer to transfer zero-copy. The\n // underlying buffer of the parsed trailer view belongs to the wire\n // packet, which may be transferred separately or reused.\n const authBuf = alta.auth.buffer.slice(\n alta.auth.byteOffset,\n alta.auth.byteOffset + alta.auth.byteLength,\n ) as ArrayBuffer\n workerClient.feedSource(ssId, buf, performance.now(), {\n auth: authBuf,\n payloadEnd: alta.payloadEnd,\n })\n } else {\n workerClient.feedSource(ssId, buf, performance.now())\n }\n return null\n }\n\n // Synchronous WASM path.\n const entry = this.#decoderForPacket(packetId, catalogTrackName)\n const client = syncBlockOwner ?? entry?.client\n if (!client) return null\n if (!this.#claimDecoderBlock(client, sbn, catalogTrackName)) return null\n this.#syncDecoderBlockOwners.set(blockKey, client)\n\n let recovered: Uint8Array | null = null\n try {\n recovered = client.add_source(ssId, fecData, performance.now())\n } catch {\n // WASM RefCell panic \u2014 decoder instance shared with multicast repair path\n }\n if (recovered || this.#completedBlocks.has(blockKey)) {\n this.#syncDecoderBlockOwners.delete(blockKey)\n }\n\n return recovered\n }\n\n /**\n * Feed a repair symbol to the WASM FEC decoder.\n *\n * @returns Recovered block data if repair completed the block, else null.\n * Caller is responsible for routing recovered data and updating\n * block completion state via `markBlockCompleted()`.\n */\n feedRepairSymbol(\n packetId: number,\n ssStart: number,\n ssbLength: number,\n rsId: number,\n repairData: Uint8Array,\n catalogTrackName?: string,\n ): Uint8Array | null {\n this.#fecStats.repairPackets++\n this.#statsForPacket(packetId).repairPackets++\n const symbolSize = this.symbolSizeForPacket(packetId)\n if (symbolSize > 0 && repairData.byteLength !== symbolSize) {\n this.#repairSymbolSizeMismatchCount++\n if (this.#repairSymbolSizeMismatchCount <= 5 || this.#repairSymbolSizeMismatchCount % 100 === 0) {\n console.warn(\n `[FECManager] Dropping repair symbol with invalid size: got ${repairData.byteLength}B, expected ${symbolSize}B (packetId=${packetId}, ssStart=${ssStart}, ssbLength=${ssbLength}, rsId=${rsId})`,\n )\n }\n return null\n }\n const fecData = repairData\n\n // Repair symbols are block activity: refresh the inactivity fail bound so\n // a block being actively repaired over MoQ is not failed mid-recovery.\n // The multicast in-band path already does this at the router; this covers\n // the MoQ backfill path, which bypasses the router's packet accounting.\n // ssbLength is SSB_length = K (ISO 23008-1 \u00A7C.5.3), so ssStart/ssbLength\n // is the block number. The legacy feedSymbol shape passes ssbLength=1\n // with flat symbol ids \u2014 its ratio is not a block number, so skip it.\n if (ssbLength > 1) {\n this.noteBlockActivity(\n Math.floor(ssStart / ssbLength),\n packetId,\n catalogTrackName,\n )\n }\n\n const sbn = ssbLength > 0 ? Math.floor(ssStart / ssbLength) : 0\n const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)\n if (this.#completedBlocks.has(blockKey)) return null\n const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey)\n let repairIds = this.#blockRepairRsIds.get(blockKey)\n if (!repairIds) {\n repairIds = new Set()\n this.#blockRepairRsIds.set(blockKey, repairIds)\n }\n repairIds.add(rsId)\n if (this.#repairRequestedBlocks.has(blockKey)) {\n this.#traceLifecycle('repair-arrived', sbn, packetId, catalogTrackName, rsId)\n }\n\n // Worker-backed track: dispatch async, return null (recovery via onFrame callback)\n const workerClient = this.#workerForPacket(packetId, catalogTrackName)\n if (workerClient && !syncBlockOwner) {\n if (!this.#claimDecoderBlock(workerClient, sbn, catalogTrackName)) return null\n // Create clean ArrayBuffer copy (T-03-06)\n const buf = fecData.buffer.slice(\n fecData.byteOffset,\n fecData.byteOffset + fecData.byteLength,\n ) as ArrayBuffer\n // Forward the wire-level Repair FEC Payload ID (ISO 23008-1 \u00A7C.5.3)\n // directly. WASM add_repair signature is\n // (ss_start, ssb_length, rs_id, data, timestamp) \u2014 pre-computing\n // (sbn, esi) on this side misaligns WASM args and produces 0% recovery.\n workerClient.feedRepair(ssStart, ssbLength, rsId, buf, performance.now())\n return null\n }\n\n // Synchronous WASM path.\n const entry = this.#decoderForPacket(packetId, catalogTrackName)\n const client = syncBlockOwner ?? entry?.client\n if (!client) return null\n if (!this.#claimDecoderBlock(client, sbn, catalogTrackName)) return null\n this.#syncDecoderBlockOwners.set(blockKey, client)\n\n let recovered: Uint8Array | null = null\n try {\n recovered = client.add_repair(ssStart, ssbLength, rsId, fecData, performance.now())\n } catch {\n // WASM add_repair failure \u2014 block will timeout and be marked failed\n }\n if (recovered) this.#syncDecoderBlockOwners.delete(blockKey)\n\n return recovered\n }\n\n #routeSyncCleanup(\n decoder: WasmFecDecoder,\n sbn: number,\n packetId: number,\n ): void {\n let framed: Uint8Array\n try {\n framed = decoder.cleanup_blocks_detailed(Uint32Array.of(sbn))\n } catch (error) {\n console.warn(`[FECManager] Scoped cleanup failed for SBN ${sbn}:`, error)\n return\n }\n\n const view = new DataView(framed.buffer, framed.byteOffset, framed.byteLength)\n let offset = 0\n while (offset < framed.byteLength) {\n if (framed.byteLength - offset < 8) {\n console.warn(`[FECManager] Scoped cleanup returned a truncated record for SBN ${sbn}`)\n return\n }\n const fillSbn = view.getUint32(offset, false)\n const length = view.getUint32(offset + 4, false)\n offset += 8\n if (length > framed.byteLength - offset) {\n console.warn(`[FECManager] Scoped cleanup returned a truncated payload for SBN ${sbn}`)\n return\n }\n const data = framed.slice(offset, offset + length).buffer as ArrayBuffer\n offset += length\n this.#fecStats.greenFillFrames++\n this.#statsForPacket(packetId).greenFillFrames++\n this.onGreenFill?.(fillSbn, packetId, data)\n }\n }\n\n /**\n * Unified symbol feeder. Dispatches to feedSourceSymbol or feedRepairSymbol.\n */\n feedSymbol(packetId: number, symbolId: number, data: Uint8Array, isRepair: boolean): Uint8Array | null {\n if (isRepair) {\n return this.feedRepairSymbol(packetId, symbolId, 1, symbolId, data)\n }\n return this.feedSourceSymbol(packetId, symbolId, data)\n }\n\n // -----------------------------------------------------------------------\n // Block lifecycle\n // -----------------------------------------------------------------------\n\n /**\n * Track a source symbol for an FEC block. Refreshes the inactivity deadline\n * on each symbol and marks the block clean when K source symbols arrive.\n */\n trackBlock(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n esi?: number,\n ): void {\n const key = this.#blockKey(sbn, packetId, catalogTrackName)\n const trackConfig = this.#resolveTrackConfig(packetId)\n const k = trackConfig.k\n\n // Skip blocks already recovered \u2014 late source symbols must not restart timeouts\n if (this.#completedBlocks.has(key)) return\n\n // Count source symbols per block\n const count = (this.#blockSourceCounts.get(key) ?? 0) + 1\n this.#blockSourceCounts.set(key, count)\n if (Number.isInteger(esi) && esi! >= 0 && esi! < k) {\n let sourceEsis = this.#blockSourceEsis.get(key)\n if (!sourceEsis) {\n sourceEsis = new Set()\n this.#blockSourceEsis.set(key, sourceEsis)\n }\n sourceEsis.add(esi!)\n }\n if (count === 1) {\n if (catalogTrackName === undefined) {\n this.onBlockStarted?.(sbn, packetId)\n } else {\n this.onBlockStarted?.(sbn, packetId, catalogTrackName)\n }\n }\n\n // When K source symbols received, block is clean (no loss, no FEC needed).\n if (k > 0 && count >= k) {\n this.#fecStats.blocksClean++\n this.#statsForPacket(packetId).blocksClean++\n this.cancelBlockTimeout(sbn, packetId, catalogTrackName)\n this.#blockSourceCounts.delete(key)\n this.#blockSourceEsis.delete(key)\n this.#blockRepairRsIds.delete(key)\n this.#completedBlocks.add(key)\n // BLO-3323 L15: track which SBNs reached K source-symbol arrivals so the\n // worker recovery suppression can distinguish \"block completed by source\"\n // (legitimately suppress duplicate recovery) from \"block touched by any\n // prior emit / timeout / external mark\" (do NOT suppress recovery).\n this.#cleanlyCompletedBlocks.add(key)\n this.#markPathBlockComplete(sbn, packetId, catalogTrackName)\n if (catalogTrackName === undefined) {\n this.onBlockComplete?.(sbn, packetId)\n } else {\n this.onBlockComplete?.(sbn, packetId, catalogTrackName)\n }\n // MmtpRouter feeds the K-th source immediately after this callback.\n // Defer cleanup so that synchronous feed selects the same decoder; this\n // also clears blocks whose K-th packet was too large for decoder input.\n queueMicrotask(() => this.#syncDecoderBlockOwners.delete(key))\n return\n }\n\n this.noteBlockActivity(sbn, packetId, catalogTrackName, true)\n\n // Source ESI is intentionally not a repair trigger. Multicast delivery is\n // unordered, so K-1 may precede an interior symbol in a clean block. The\n // progress-refreshed source horizon above is the earliest loss signal.\n }\n\n /** Refresh deadlines after accepted block activity. */\n noteBlockActivity(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n sourceProgress = false,\n ): void {\n const key = this.#blockKey(sbn, packetId, catalogTrackName)\n\n // Late symbols must not resurrect a block that already completed or failed.\n if (this.#completedBlocks.has(key)) return\n // The worker cleanup command is already queued after all symbols that made\n // the inactivity deadline. Symbols arriving after that boundary are late\n // and must not arm a second timer before the acknowledgement resolves it.\n if (this.#pendingWorkerCleanups.has(key)) return\n\n // #requestRepair appends one bounded grace to the active deadline. Source\n // or repair symbols may still complete the block, but cannot extend it.\n if (this.#repairRequestedBlocks.has(key)) return\n\n const trackConfig = this.#resolveTrackConfig(packetId)\n const timeoutMs = trackConfig.timeoutMs\n if (timeoutMs <= 0) return\n\n // D-28: accepted source progress refreshes the pre-request horizon. There\n // are at most K such arrivals; repair traffic never refreshes this timer.\n const repairDeadline = this.#repairDeadlines.get(key)\n if (\n this.onRepairNeeded &&\n (repairDeadline === undefined || sourceProgress)\n ) {\n if (repairDeadline != null) clearTimeout(repairDeadline)\n this.#repairDeadlines.set(\n key,\n setTimeout(() => {\n this.#requestRepair(sbn, packetId, catalogTrackName)\n }, this.#repairDeadlineMs(trackConfig)),\n )\n }\n\n this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs)\n }\n\n #failBlock(\n sbn: number,\n packetId: number,\n countGreenFill: boolean,\n catalogTrackName?: string,\n ): void {\n const key = this.#blockKey(sbn, packetId, catalogTrackName)\n this.#traceLifecycle('failed', sbn, packetId, catalogTrackName)\n this.#clearRepairDeadline(key)\n const activeTimer = this.#activeBlocks.get(key)\n if (activeTimer) {\n clearTimeout(activeTimer)\n this.#activeBlocks.delete(key)\n }\n this.#activeBlockDeadlinesMs.delete(key)\n if (this.#repairRequestedBlocks.delete(key)) {\n if (catalogTrackName === undefined) {\n this.onRepairResolved?.(sbn, packetId, 'failed')\n } else {\n this.onRepairResolved?.(sbn, packetId, 'failed', catalogTrackName)\n }\n }\n this.#blockSourceCounts.delete(key)\n this.#blockSourceEsis.delete(key)\n this.#blockRepairRsIds.delete(key)\n this.#activeBlockTimeoutOwners.delete(key)\n this.#syncDecoderBlockOwners.delete(key)\n this.#completedBlocks.add(key)\n this.#markPathBlockComplete(sbn, packetId, catalogTrackName)\n this.#fecStats.blocksFailed++\n this.#statsForPacket(packetId).blocksFailed++\n if (countGreenFill) {\n this.#fecStats.greenFillFrames++\n this.#statsForPacket(packetId).greenFillFrames++\n }\n if (catalogTrackName === undefined) {\n this.onBlockFailed?.(sbn, packetId)\n } else {\n this.onBlockFailed?.(sbn, packetId, catalogTrackName)\n }\n }\n\n /** Check whether a block has already been completed (clean or recovered). */\n isBlockCompleted(sbn: number, packetId?: number, catalogTrackName?: string): boolean {\n return this.#blockKeysFor(sbn, packetId, catalogTrackName).some((key) => this.#completedBlocks.has(key))\n }\n\n /** Mark a block as completed externally (e.g., after routing recovered data). */\n markBlockCompleted(sbn: number, packetId?: number, catalogTrackName?: string): void {\n for (const key of this.#blockKeysFor(sbn, packetId, catalogTrackName)) {\n this.#completedBlocks.add(key)\n this.#pendingWorkerCleanups.delete(key)\n this.#pendingWorkerCleanupClients.delete(key)\n const block = this.#parseBlockKey(key)\n this.#markPathBlockComplete(block.sbn, block.packetId, block.catalogTrackName)\n this.#clearRepairDeadline(key)\n if (this.#repairRequestedBlocks.delete(key)) {\n if (block.catalogTrackName === undefined) {\n this.onRepairResolved?.(block.sbn, block.packetId, 'completed')\n } else {\n this.onRepairResolved?.(\n block.sbn,\n block.packetId,\n 'completed',\n block.catalogTrackName,\n )\n }\n }\n this.#blockSourceCounts.delete(key)\n this.#blockSourceEsis.delete(key)\n this.#blockRepairRsIds.delete(key)\n const timer = this.#activeBlocks.get(key)\n if (timer) {\n clearTimeout(timer)\n this.#activeBlocks.delete(key)\n }\n this.#activeBlockDeadlinesMs.delete(key)\n this.#activeBlockTimeoutOwners.delete(key)\n }\n }\n\n /** Increment blocksRecovered stat (called by MmtpRouter after routing recovery). */\n countRecovery(packetId?: number): void {\n this.#fecStats.blocksRecovered++\n if (packetId != null) this.#statsForPacket(packetId).blocksRecovered++\n }\n\n /**\n * Strict delivery provenance for one FEC block.\n *\n * A recovered symbol is a function of every symbol that entered its block,\n * so it can only be attributed to multicast when NO symbol in that block\n * arrived over MoQ. Repair symbols routinely arrive over reliable QUIC\n * (`MmtpRouter.feedRepairPacket`), so a block that recovered with MoQ help\n * produced bytes that never crossed a multicast socket.\n *\n * An untracked block returns 'unknown' rather than a guess: absence of\n * evidence is not evidence of multicast.\n */\n blockDeliveryProvenance(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): MmtpDeliveryProvenance {\n const counts = this.blockPathCount(sbn, packetId, catalogTrackName)\n if (!counts) return 'unknown'\n if (counts.moqSymbols > 0) return 'mixed'\n if (counts.multicastSymbols > 0) return 'multicast'\n return 'unknown'\n }\n\n /** Per-path symbol counts for one block (D-28 gate logging), if tracked. */\n blockPathCount(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): FecBlockPathCount | null {\n return this.#blockPathCounts.get(\n this.#blockKey(sbn, packetId, catalogTrackName),\n ) ?? null\n }\n\n /** Cancel block timeout (block completed via clean reception or FEC recovery). */\n cancelBlockTimeout(sbn: number, packetId?: number, catalogTrackName?: string): void {\n for (const key of this.#blockKeysFor(sbn, packetId, catalogTrackName)) {\n this.#completedBlocks.add(key)\n this.#pendingWorkerCleanups.delete(key)\n this.#pendingWorkerCleanupClients.delete(key)\n const block = this.#parseBlockKey(key)\n this.#markPathBlockComplete(block.sbn, block.packetId, block.catalogTrackName)\n this.#clearRepairDeadline(key)\n if (this.#repairRequestedBlocks.delete(key)) {\n if (block.catalogTrackName === undefined) {\n this.onRepairResolved?.(block.sbn, block.packetId, 'completed')\n } else {\n this.onRepairResolved?.(\n block.sbn,\n block.packetId,\n 'completed',\n block.catalogTrackName,\n )\n }\n }\n this.#blockSourceCounts.delete(key)\n this.#blockSourceEsis.delete(key)\n this.#blockRepairRsIds.delete(key)\n const timer = this.#activeBlocks.get(key)\n if (timer) {\n clearTimeout(timer)\n this.#activeBlocks.delete(key)\n }\n this.#activeBlockDeadlinesMs.delete(key)\n this.#activeBlockTimeoutOwners.delete(key)\n }\n // Trim old entries to prevent unbounded growth (keep last ~500 blocks)\n if (this.#completedBlocks.size > 1000) {\n const iter = this.#completedBlocks.values()\n for (let i = 0; i < 500; i++) iter.next()\n // Sets iterate in insertion order \u2014 first 500 are oldest\n const keep = new Set<string>()\n for (const v of iter) keep.add(v)\n this.#completedBlocks = keep\n }\n }\n\n // -----------------------------------------------------------------------\n // Cleanup\n // -----------------------------------------------------------------------\n\n dispose(): void {\n for (const timer of this.#activeBlocks.values()) clearTimeout(timer)\n this.#activeBlocks.clear()\n this.#activeBlockDeadlinesMs.clear()\n this.#activeBlockTimeoutOwners.clear()\n this.#pendingWorkerCleanups.clear()\n this.#pendingWorkerCleanupClients.clear()\n for (const timer of this.#repairDeadlines.values()) {\n if (timer != null) clearTimeout(timer)\n }\n this.#repairDeadlines.clear()\n this.#blockSourceCounts.clear()\n this.#blockSourceEsis.clear()\n this.#blockRepairRsIds.clear()\n this.#completedBlocks.clear()\n this.#cleanlyCompletedBlocks.clear()\n this.#repairRequestedBlocks.clear()\n this.#workerRecoveredBlocks.clear()\n this.#syncDecoderBlockOwners.clear()\n this.#startupAlignedPackets.clear()\n this.#startupPartialBlocks.clear()\n this.#startupRequireRepairReady = false\n this.#startupRepairReadyPackets.clear()\n this.#startupRepairBoundaryCounts.clear()\n this.#startupRepairFailOpenPackets.clear()\n this.#seenSymbols.clear()\n this.#blockPathCounts.clear()\n this.#blockCatalogTrackNames.clear()\n this.#trackConfigs.clear()\n this.#fecDecoders.clear()\n // Dispose Worker clients (T-03-07: prevent Worker leak)\n for (const client of new Set(this.#workerClients.values())) client.dispose()\n this.#workerClients.clear()\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["deriveFecInterleaveDepth", "deriveFecInterleaveDepth"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FecStarvationDetector — post-cutover source-starvation detection (BLO-9413/D4).
|
|
3
|
+
*
|
|
4
|
+
* The packet-presence watchdog (MulticastTransport, 5s) only catches TOTAL
|
|
5
|
+
* silence: any packet — including FEC repair — resets it. The observed
|
|
6
|
+
* BLO-9413 failure delivered repair symbols continuously while ZERO source
|
|
7
|
+
* symbols arrived (`sourceSymbols:0, repairSymbols:551, blocksComplete:0`):
|
|
8
|
+
* the watchdog stayed fed forever while nothing reconstructable reached the
|
|
9
|
+
* decoder. This detector closes that hole by watching delivery progress and
|
|
10
|
+
* driving the designed-but-previously-uncalled TrackSession loss arcs:
|
|
11
|
+
*
|
|
12
|
+
* starvation sustained ≥ windowMs → onStarvation() → session.lossDetected()
|
|
13
|
+
* block progress resumes → onRecovery() → session.lossRecovered()
|
|
14
|
+
*
|
|
15
|
+
* TrackSession's own FEC block-span hysteresis then decides whether a recovery
|
|
16
|
+
* cancels the failback — this detector deliberately does NOT add a second
|
|
17
|
+
* hysteresis layer.
|
|
18
|
+
*
|
|
19
|
+
* Push-fed: call update() with each FecTrackStats-shaped snapshot (e.g. from
|
|
20
|
+
* MulticastManager.setOnFecStatsUpdate or a host sampler). Hosts that can also
|
|
21
|
+
* observe decoded/appended media should include a monotonic mediaFrames count:
|
|
22
|
+
* media progress is delivery progress even if sparse FEC block counters do not
|
|
23
|
+
* advance in the same sample window. Evaluation happens only on update — a
|
|
24
|
+
* fully dead transport stops producing updates and stays the transport
|
|
25
|
+
* watchdog's responsibility (total-silence backstop).
|
|
26
|
+
*/
|
|
27
|
+
/** Monotonic counters sampled from the FEC layer (FecTrackStats subset). */
|
|
28
|
+
export interface FecStarvationSnapshot {
|
|
29
|
+
sourceSymbols: number;
|
|
30
|
+
repairSymbols: number;
|
|
31
|
+
blocksComplete: number;
|
|
32
|
+
blocksRecovered: number;
|
|
33
|
+
/** Optional host-observed decoded/appended media frames for this packetId. */
|
|
34
|
+
mediaFrames?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface FecStarvationDetectorConfig {
|
|
37
|
+
/**
|
|
38
|
+
* How long starvation must persist before onStarvation fires. Derive from
|
|
39
|
+
* the stream's FEC block-span config — no magic defaults, matching the
|
|
40
|
+
* TrackSession hysteresis convention.
|
|
41
|
+
*/
|
|
42
|
+
windowMs: number;
|
|
43
|
+
/** Starvation sustained ≥ windowMs. Fires once per starvation episode. */
|
|
44
|
+
onStarvation(): void;
|
|
45
|
+
/** Healthy delivery resumed after onStarvation fired. Fires once per episode. */
|
|
46
|
+
onRecovery(): void;
|
|
47
|
+
/** Injectable clock for tests. Defaults to Date.now. */
|
|
48
|
+
now?: () => number;
|
|
49
|
+
}
|
|
50
|
+
export declare class FecStarvationDetector {
|
|
51
|
+
#private;
|
|
52
|
+
constructor(config: FecStarvationDetectorConfig);
|
|
53
|
+
/** True while a starvation episode is active (onStarvation fired, no recovery yet). */
|
|
54
|
+
get isStarved(): boolean;
|
|
55
|
+
update(snapshot: FecStarvationSnapshot): void;
|
|
56
|
+
/** Reset episode state (e.g. on transport switch / re-join). Keeps no baseline. */
|
|
57
|
+
reset(): void;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=fec-starvation-detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fec-starvation-detector.d.ts","sourceRoot":"","sources":["../src/fec-starvation-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,4EAA4E;AAC5E,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,eAAe,EAAE,MAAM,CAAA;IACvB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,0EAA0E;IAC1E,YAAY,IAAI,IAAI,CAAA;IACpB,iFAAiF;IACjF,UAAU,IAAI,IAAI,CAAA;IAClB,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,qBAAa,qBAAqB;;gBAQpB,MAAM,EAAE,2BAA2B;IAY/C,uFAAuF;IACvF,IAAI,SAAS,IAAI,OAAO,CAAyB;IAEjD,MAAM,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI;IA8C7C,mFAAmF;IACnF,KAAK,IAAI,IAAI;CAKd"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
var __typeError = (msg) => {
|
|
2
|
+
throw TypeError(msg);
|
|
3
|
+
};
|
|
4
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
5
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
6
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
7
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
|
+
|
|
9
|
+
// src/fec-starvation-detector.ts
|
|
10
|
+
var _config, _now, _last, _lastSampleAt, _starvedSince, _latched;
|
|
11
|
+
var FecStarvationDetector = class {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
__privateAdd(this, _config);
|
|
14
|
+
__privateAdd(this, _now);
|
|
15
|
+
__privateAdd(this, _last, null);
|
|
16
|
+
__privateAdd(this, _lastSampleAt, 0);
|
|
17
|
+
__privateAdd(this, _starvedSince, null);
|
|
18
|
+
__privateAdd(this, _latched, false);
|
|
19
|
+
if (config.windowMs <= 0) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`[FecStarvationDetector] windowMs=${config.windowMs}. Derive a positive window from the stream's FEC block-span config \u2014 a non-positive window would fire on the first repair packet of a healthy stream.`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
__privateSet(this, _config, config);
|
|
25
|
+
__privateSet(this, _now, config.now ?? Date.now);
|
|
26
|
+
}
|
|
27
|
+
/** True while a starvation episode is active (onStarvation fired, no recovery yet). */
|
|
28
|
+
get isStarved() {
|
|
29
|
+
return __privateGet(this, _latched);
|
|
30
|
+
}
|
|
31
|
+
update(snapshot) {
|
|
32
|
+
const last = __privateGet(this, _last);
|
|
33
|
+
const prevSampleAt = __privateGet(this, _lastSampleAt);
|
|
34
|
+
const now = __privateGet(this, _now).call(this);
|
|
35
|
+
__privateSet(this, _last, { ...snapshot });
|
|
36
|
+
__privateSet(this, _lastSampleAt, now);
|
|
37
|
+
if (last === null) return;
|
|
38
|
+
const srcDelta = snapshot.sourceSymbols - last.sourceSymbols;
|
|
39
|
+
const repDelta = snapshot.repairSymbols - last.repairSymbols;
|
|
40
|
+
const doneDelta = snapshot.blocksComplete - last.blocksComplete + (snapshot.blocksRecovered - last.blocksRecovered);
|
|
41
|
+
const mediaDelta = (snapshot.mediaFrames ?? 0) - (last.mediaFrames ?? 0);
|
|
42
|
+
const packetsFlowing = srcDelta > 0 || repDelta > 0;
|
|
43
|
+
if (!packetsFlowing && mediaDelta <= 0) return;
|
|
44
|
+
const starving = doneDelta === 0 && mediaDelta <= 0;
|
|
45
|
+
if (starving) {
|
|
46
|
+
if (__privateGet(this, _starvedSince) === null) __privateSet(this, _starvedSince, prevSampleAt);
|
|
47
|
+
if (!__privateGet(this, _latched) && now - __privateGet(this, _starvedSince) >= __privateGet(this, _config).windowMs) {
|
|
48
|
+
__privateSet(this, _latched, true);
|
|
49
|
+
__privateGet(this, _config).onStarvation();
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
__privateSet(this, _starvedSince, null);
|
|
54
|
+
if (__privateGet(this, _latched)) {
|
|
55
|
+
__privateSet(this, _latched, false);
|
|
56
|
+
__privateGet(this, _config).onRecovery();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Reset episode state (e.g. on transport switch / re-join). Keeps no baseline. */
|
|
60
|
+
reset() {
|
|
61
|
+
__privateSet(this, _last, null);
|
|
62
|
+
__privateSet(this, _starvedSince, null);
|
|
63
|
+
__privateSet(this, _latched, false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
_config = new WeakMap();
|
|
67
|
+
_now = new WeakMap();
|
|
68
|
+
_last = new WeakMap();
|
|
69
|
+
_lastSampleAt = new WeakMap();
|
|
70
|
+
_starvedSince = new WeakMap();
|
|
71
|
+
_latched = new WeakMap();
|
|
72
|
+
export {
|
|
73
|
+
FecStarvationDetector
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=fec-starvation-detector.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/fec-starvation-detector.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * FecStarvationDetector \u2014 post-cutover source-starvation detection (BLO-9413/D4).\n *\n * The packet-presence watchdog (MulticastTransport, 5s) only catches TOTAL\n * silence: any packet \u2014 including FEC repair \u2014 resets it. The observed\n * BLO-9413 failure delivered repair symbols continuously while ZERO source\n * symbols arrived (`sourceSymbols:0, repairSymbols:551, blocksComplete:0`):\n * the watchdog stayed fed forever while nothing reconstructable reached the\n * decoder. This detector closes that hole by watching delivery progress and\n * driving the designed-but-previously-uncalled TrackSession loss arcs:\n *\n * starvation sustained \u2265 windowMs \u2192 onStarvation() \u2192 session.lossDetected()\n * block progress resumes \u2192 onRecovery() \u2192 session.lossRecovered()\n *\n * TrackSession's own FEC block-span hysteresis then decides whether a recovery\n * cancels the failback \u2014 this detector deliberately does NOT add a second\n * hysteresis layer.\n *\n * Push-fed: call update() with each FecTrackStats-shaped snapshot (e.g. from\n * MulticastManager.setOnFecStatsUpdate or a host sampler). Hosts that can also\n * observe decoded/appended media should include a monotonic mediaFrames count:\n * media progress is delivery progress even if sparse FEC block counters do not\n * advance in the same sample window. Evaluation happens only on update \u2014 a\n * fully dead transport stops producing updates and stays the transport\n * watchdog's responsibility (total-silence backstop).\n */\n\n/** Monotonic counters sampled from the FEC layer (FecTrackStats subset). */\nexport interface FecStarvationSnapshot {\n sourceSymbols: number\n repairSymbols: number\n blocksComplete: number\n blocksRecovered: number\n /** Optional host-observed decoded/appended media frames for this packetId. */\n mediaFrames?: number\n}\n\nexport interface FecStarvationDetectorConfig {\n /**\n * How long starvation must persist before onStarvation fires. Derive from\n * the stream's FEC block-span config \u2014 no magic defaults, matching the\n * TrackSession hysteresis convention.\n */\n windowMs: number\n /** Starvation sustained \u2265 windowMs. Fires once per starvation episode. */\n onStarvation(): void\n /** Healthy delivery resumed after onStarvation fired. Fires once per episode. */\n onRecovery(): void\n /** Injectable clock for tests. Defaults to Date.now. */\n now?: () => number\n}\n\nexport class FecStarvationDetector {\n #config: FecStarvationDetectorConfig\n #now: () => number\n #last: FecStarvationSnapshot | null = null\n #lastSampleAt = 0\n #starvedSince: number | null = null\n #latched = false\n\n constructor(config: FecStarvationDetectorConfig) {\n if (config.windowMs <= 0) {\n throw new Error(\n `[FecStarvationDetector] windowMs=${config.windowMs}. Derive a positive window ` +\n `from the stream's FEC block-span config \u2014 a non-positive ` +\n `window would fire on the first repair packet of a healthy stream.`\n )\n }\n this.#config = config\n this.#now = config.now ?? Date.now\n }\n\n /** True while a starvation episode is active (onStarvation fired, no recovery yet). */\n get isStarved(): boolean { return this.#latched }\n\n update(snapshot: FecStarvationSnapshot): void {\n const last = this.#last\n const prevSampleAt = this.#lastSampleAt\n const now = this.#now()\n this.#last = { ...snapshot }\n this.#lastSampleAt = now\n if (last === null) return // first sample = baseline only\n\n const srcDelta = snapshot.sourceSymbols - last.sourceSymbols\n const repDelta = snapshot.repairSymbols - last.repairSymbols\n const doneDelta =\n (snapshot.blocksComplete - last.blocksComplete) +\n (snapshot.blocksRecovered - last.blocksRecovered)\n const mediaDelta = (snapshot.mediaFrames ?? 0) - (last.mediaFrames ?? 0)\n\n const packetsFlowing = srcDelta > 0 || repDelta > 0\n if (!packetsFlowing && mediaDelta <= 0) return // total silence \u2014 transport watchdog's domain\n\n // Starving: traffic arrives but nothing reconstructable comes out. Source\n // and repair symbols arrive at symbol cadence while complete/recovered\n // blocks arrive at block cadence; requiring source and block deltas in the\n // same snapshot false-fails healthy live streams. A repaired block is still\n // decodable media, and host-observed frame progress is even stronger\n // evidence, so either resets the starvation clock.\n const starving = doneDelta === 0 && mediaDelta <= 0\n\n if (starving) {\n // The episode began when delivery was last known healthy \u2014 the previous\n // sample's timestamp, not this sample's arrival (the deltas in THIS\n // sample describe the interval since then).\n if (this.#starvedSince === null) this.#starvedSince = prevSampleAt\n if (!this.#latched && now - this.#starvedSince >= this.#config.windowMs) {\n this.#latched = true\n this.#config.onStarvation()\n }\n return\n }\n\n // Healthy sample: completed or recovered blocks are reaching the decoder.\n this.#starvedSince = null\n if (this.#latched) {\n this.#latched = false\n this.#config.onRecovery()\n }\n }\n\n /** Reset episode state (e.g. on transport switch / re-join). Keeps no baseline. */\n reset(): void {\n this.#last = null\n this.#starvedSince = null\n this.#latched = false\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;AAAA;AAoDO,IAAM,wBAAN,MAA4B;AAAA,EAQjC,YAAY,QAAqC;AAPjD;AACA;AACA,8BAAsC;AACtC,sCAAgB;AAChB,sCAA+B;AAC/B,iCAAW;AAGT,QAAI,OAAO,YAAY,GAAG;AACxB,YAAM,IAAI;AAAA,QACR,oCAAoC,OAAO,QAAQ;AAAA,MAGrD;AAAA,IACF;AACA,uBAAK,SAAU;AACf,uBAAK,MAAO,OAAO,OAAO,KAAK;AAAA,EACjC;AAAA;AAAA,EAGA,IAAI,YAAqB;AAAE,WAAO,mBAAK;AAAA,EAAS;AAAA,EAEhD,OAAO,UAAuC;AAC5C,UAAM,OAAO,mBAAK;AAClB,UAAM,eAAe,mBAAK;AAC1B,UAAM,MAAM,mBAAK,MAAL;AACZ,uBAAK,OAAQ,EAAE,GAAG,SAAS;AAC3B,uBAAK,eAAgB;AACrB,QAAI,SAAS,KAAM;AAEnB,UAAM,WAAW,SAAS,gBAAgB,KAAK;AAC/C,UAAM,WAAW,SAAS,gBAAgB,KAAK;AAC/C,UAAM,YACH,SAAS,iBAAiB,KAAK,kBAC/B,SAAS,kBAAkB,KAAK;AACnC,UAAM,cAAc,SAAS,eAAe,MAAM,KAAK,eAAe;AAEtE,UAAM,iBAAiB,WAAW,KAAK,WAAW;AAClD,QAAI,CAAC,kBAAkB,cAAc,EAAG;AAQxC,UAAM,WAAW,cAAc,KAAK,cAAc;AAElD,QAAI,UAAU;AAIZ,UAAI,mBAAK,mBAAkB,KAAM,oBAAK,eAAgB;AACtD,UAAI,CAAC,mBAAK,aAAY,MAAM,mBAAK,kBAAiB,mBAAK,SAAQ,UAAU;AACvE,2BAAK,UAAW;AAChB,2BAAK,SAAQ,aAAa;AAAA,MAC5B;AACA;AAAA,IACF;AAGA,uBAAK,eAAgB;AACrB,QAAI,mBAAK,WAAU;AACjB,yBAAK,UAAW;AAChB,yBAAK,SAAQ,WAAW;AAAA,IAC1B;AAAA,EACF;AAAA;AAAA,EAGA,QAAc;AACZ,uBAAK,OAAQ;AACb,uBAAK,eAAgB;AACrB,uBAAK,UAAW;AAAA,EAClB;AACF;AA1EE;AACA;AACA;AACA;AACA;AACA;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared fec-setup helper.
|
|
3
|
+
*
|
|
4
|
+
* Wires a FecWorkerClient into a MulticastManager for a specific track.
|
|
5
|
+
* Both moq-lib (`hang-mmt-fec/js/watch/src/element.ts`) and moqtail
|
|
6
|
+
* (`moqtail-private/apps/client-js/src/lib/player.ts`) call this from their
|
|
7
|
+
* SessionManager `fec-setup` handler so the Worker+ALTA setup lives in one
|
|
8
|
+
* place instead of being duplicated across ~80 lines per player.
|
|
9
|
+
*
|
|
10
|
+
* Callers are responsible for holding the returned `dispose()` and calling it
|
|
11
|
+
* from their `fec-teardown` path (and/or effect cleanup).
|
|
12
|
+
*/
|
|
13
|
+
import type { FecWorkerClient } from '@blockcast/fec-worker';
|
|
14
|
+
import type { MulticastManager } from './multicast-manager.js';
|
|
15
|
+
/** Asset URLs the helper needs. Callers pass the already-resolved set. */
|
|
16
|
+
export interface FecWorkerAssetUrls {
|
|
17
|
+
/** URL to the fec-worker entry bundle (new Worker() target). */
|
|
18
|
+
fecWorker: string;
|
|
19
|
+
/** URL to the RaptorQ WASM module (compiled via compileStreaming). */
|
|
20
|
+
rqWasm: string;
|
|
21
|
+
/** URL to the ALTA WASM module. Fetched only when an ALTA key is provided. */
|
|
22
|
+
altaWasm: string;
|
|
23
|
+
}
|
|
24
|
+
export interface FecWorkerSetupOptions {
|
|
25
|
+
/** MulticastManager that should receive the configured Worker. */
|
|
26
|
+
multicastManager: MulticastManager;
|
|
27
|
+
/** MMTP packetId this track occupies (from catalog). */
|
|
28
|
+
packetId: number;
|
|
29
|
+
/** Human-readable track name (catalog key). */
|
|
30
|
+
trackName: string;
|
|
31
|
+
/** Track type from catalog. Never inferred from name. */
|
|
32
|
+
trackType: 'video' | 'audio';
|
|
33
|
+
/** FEC K from catalog (source symbols per block). */
|
|
34
|
+
fecK: number;
|
|
35
|
+
/** Symbol size in bytes (T) — the ALTA-adjusted value if ALTA is signed. */
|
|
36
|
+
fecSymbolSize: number;
|
|
37
|
+
/** Repair symbols per block (P). */
|
|
38
|
+
fecRepairCount: number;
|
|
39
|
+
/** Interleave depth in ms. */
|
|
40
|
+
fecInterleaveMs: number;
|
|
41
|
+
/** Duration of one FEC media unit in ms (video frame or audio access unit). */
|
|
42
|
+
mediaUnitDurationMs: number;
|
|
43
|
+
/** Optional repair track name (MoQ). Forwarded to FecTrackConfig. */
|
|
44
|
+
repairTrackName?: string;
|
|
45
|
+
/** Audio sample rate (optional — audio tracks only). */
|
|
46
|
+
sampleRate?: number;
|
|
47
|
+
/** Codec string (optional; "unknown" is acceptable for first setup). */
|
|
48
|
+
codec?: string;
|
|
49
|
+
/** Video framerate (optional metadata forwarded to the worker). */
|
|
50
|
+
framerate?: number;
|
|
51
|
+
/** Base64 SPKI public key (optional). Accepts padded/unpadded, std or url-safe alphabet. */
|
|
52
|
+
altaPublicKeySpki?: string;
|
|
53
|
+
/** Absolute or origin-relative asset URLs. */
|
|
54
|
+
assetUrls: FecWorkerAssetUrls;
|
|
55
|
+
/** Optional FecWorkerClient factory — main use is test injection. */
|
|
56
|
+
clientFactory?: (workerUrl: URL) => FecWorkerClient;
|
|
57
|
+
}
|
|
58
|
+
export interface FecWorkerSetupResult {
|
|
59
|
+
/** The configured client (already registered with MulticastManager). */
|
|
60
|
+
client: FecWorkerClient;
|
|
61
|
+
/** True when the ALTA public key decoded and the ALTA WASM compiled. */
|
|
62
|
+
altaEnabled: boolean;
|
|
63
|
+
/** Releases the Worker and removes it from MulticastManager routing. */
|
|
64
|
+
dispose: () => void;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Configure a FecWorkerClient and attach it to `multicastManager` via
|
|
68
|
+
* `setFecWorker(packetId, client, trackType, config, trackName)`. Worker-path takes precedence
|
|
69
|
+
* over any sync WASM decoder already registered for the same packetId —
|
|
70
|
+
* sync decoders remain as a fallback.
|
|
71
|
+
*
|
|
72
|
+
* Any loader failure is re-thrown so the caller can log and leave the sync
|
|
73
|
+
* WASM decoder active as fallback (same pattern both players used before).
|
|
74
|
+
*/
|
|
75
|
+
export declare function setupFecWorkerForTrack(opts: FecWorkerSetupOptions): Promise<FecWorkerSetupResult>;
|
|
76
|
+
//# sourceMappingURL=fec-worker-setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fec-worker-setup.d.ts","sourceRoot":"","sources":["../src/fec-worker-setup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,uBAAuB,CAAA;AAE5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAG9D,0EAA0E;AAC1E,MAAM,WAAW,kBAAkB;IACjC,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAA;IACjB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAA;IACd,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,qBAAqB;IACpC,kEAAkE;IAClE,gBAAgB,EAAE,gBAAgB,CAAA;IAClC,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAA;IAChB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB,yDAAyD;IACzD,SAAS,EAAE,OAAO,GAAG,OAAO,CAAA;IAC5B,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAA;IACrB,oCAAoC;IACpC,cAAc,EAAE,MAAM,CAAA;IACtB,8BAA8B;IAC9B,eAAe,EAAE,MAAM,CAAA;IACvB,+EAA+E;IAC/E,mBAAmB,EAAE,MAAM,CAAA;IAC3B,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4FAA4F;IAC5F,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,8CAA8C;IAC9C,SAAS,EAAE,kBAAkB,CAAA;IAC7B,qEAAqE;IACrE,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,KAAK,eAAe,CAAA;CACpD;AAED,MAAM,WAAW,oBAAoB;IACnC,wEAAwE;IACxE,MAAM,EAAE,eAAe,CAAA;IACvB,wEAAwE;IACxE,WAAW,EAAE,OAAO,CAAA;IACpB,wEAAwE;IACxE,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,oBAAoB,CAAC,CA6F/B"}
|