@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,1939 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FEC Manager — codec-agnostic FEC decode service.
|
|
3
|
+
*
|
|
4
|
+
* Receives (packetId, sbn, esi, symbolData) tuples from any transport
|
|
5
|
+
* (MMTP, LOC, etc.) and manages FEC block lifecycle: source counting,
|
|
6
|
+
* WASM RaptorQ decode, block timeout, and recovery/failure notification.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from MulticastManager to decouple FEC block logic from
|
|
9
|
+
* MMTP packet parsing and frame routing.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { computeFecDeliveryWindowMs } from './track-session.js'
|
|
13
|
+
import { deriveFecInterleaveDepth } from '@blockcast/mmt-fec'
|
|
14
|
+
|
|
15
|
+
import type { MmtpDeliveryProvenance } from './mfu-reassembler.js'
|
|
16
|
+
import type {
|
|
17
|
+
FecWorkerClient,
|
|
18
|
+
FecTrackStats,
|
|
19
|
+
FecBlockSnapshot,
|
|
20
|
+
FrameMeta,
|
|
21
|
+
FecWorkerErrorCode,
|
|
22
|
+
} from '@blockcast/fec-worker'
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Constants
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Minimum valid FEC interleave depth in milliseconds, per spec §5.1
|
|
30
|
+
* (the interactive/gaming floor; broadcast/ATSC 3.0 ranges up to 10000ms).
|
|
31
|
+
*
|
|
32
|
+
* A protocol constant, NOT a tuning knob. Values below this floor are almost
|
|
33
|
+
* certainly a raw frame count (e.g. `2`) that a caller passed when no framerate
|
|
34
|
+
* was available to convert to ms (common on audio tracks). Feeding such a value
|
|
35
|
+
* into computeFecTimeout treats it as `2ms`, yielding a near-zero FEC block
|
|
36
|
+
* timeout that abandons startup FEC blocks before their repair symbols arrive
|
|
37
|
+
* (measured: moqtail startupFailed=4, shaka dropped=5.8% on startup). Timing
|
|
38
|
+
* derivations MUST reject sub-floor values and fall back to the established
|
|
39
|
+
* timeout instead of collapsing.
|
|
40
|
+
*/
|
|
41
|
+
export const MIN_INTERLEAVE_DEPTH_MS = 33
|
|
42
|
+
const WORKER_ERROR_LOG_CAP = 5
|
|
43
|
+
const WORKER_ERROR_KIND_CAP = 32
|
|
44
|
+
const WORKER_ERROR_OVERFLOW_KIND = 'other'
|
|
45
|
+
const STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES = 2
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Types
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Algorithm-agnostic FEC decoder backend interface.
|
|
53
|
+
*
|
|
54
|
+
* Implementations:
|
|
55
|
+
* - RaptorQ WASM (mmt_wasm.js) — RFC 6330
|
|
56
|
+
* - Reed-Solomon WASM — future
|
|
57
|
+
* - XOR — future
|
|
58
|
+
*
|
|
59
|
+
* Source/repair symbol IDs follow ISO 23008-1 §C.5.2/C.5.3:
|
|
60
|
+
* source: SS_ID (flat 32-bit symbol identifier)
|
|
61
|
+
* repair: SS_Start + SSB_length + RS_ID (block-scoped repair coords)
|
|
62
|
+
*/
|
|
63
|
+
export type WasmFecDecoder = {
|
|
64
|
+
add_source(ss_id: number, data: Uint8Array, ts: number): Uint8Array | null
|
|
65
|
+
add_repair(ss_start: number, ssb_length: number, rs_id: number, data: Uint8Array, ts: number): Uint8Array | null
|
|
66
|
+
configure(oti: Uint8Array): void
|
|
67
|
+
cleanup(current_time: number): Uint8Array
|
|
68
|
+
cleanup_blocks_detailed(sbns: Uint32Array): Uint8Array
|
|
69
|
+
flush?(): void
|
|
70
|
+
reset_stats?(): void
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Algorithm-agnostic FEC stats snapshot. */
|
|
74
|
+
export interface FecStats {
|
|
75
|
+
sourcePackets: number
|
|
76
|
+
repairPackets: number
|
|
77
|
+
duplicateSymbols: number
|
|
78
|
+
blocksClean: number
|
|
79
|
+
blocksRecovered: number
|
|
80
|
+
blocksFailed: number
|
|
81
|
+
greenFillFrames: number
|
|
82
|
+
blocksComplete: number
|
|
83
|
+
mixedPathBlocks: number
|
|
84
|
+
recentBlockPathCounts: FecBlockPathCount[]
|
|
85
|
+
k: number
|
|
86
|
+
/** Of blocks with loss, what percentage were recovered (0-100). */
|
|
87
|
+
recoveryRate: number
|
|
88
|
+
/** What percentage of blocks had any loss (0-100). */
|
|
89
|
+
packetLossRate: number
|
|
90
|
+
/** Per-MMTP-packet-ID counters for the synchronous WASM decoder path. */
|
|
91
|
+
tracks: FecPacketStats[]
|
|
92
|
+
/** Latest Worker/WASM snapshot for each packet ID. */
|
|
93
|
+
workers: Array<FecTrackStats & { packetId: number }>
|
|
94
|
+
/** Main-thread repair symbols rejected before dispatch because their size did not match T. */
|
|
95
|
+
repairSymbolSizeMismatches: number
|
|
96
|
+
/** Bounded Worker error counts keyed by stable code, or message text for legacy errors. */
|
|
97
|
+
workerErrors: Record<string, number>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface FecPacketStats {
|
|
101
|
+
packetId: number
|
|
102
|
+
sourcePackets: number
|
|
103
|
+
repairPackets: number
|
|
104
|
+
duplicateSymbols: number
|
|
105
|
+
blocksClean: number
|
|
106
|
+
blocksRecovered: number
|
|
107
|
+
blocksFailed: number
|
|
108
|
+
greenFillFrames: number
|
|
109
|
+
blocksComplete: number
|
|
110
|
+
k: number
|
|
111
|
+
recoveryRate: number
|
|
112
|
+
packetLossRate: number
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type FecSymbolSource = 'multicast' | 'moq'
|
|
116
|
+
|
|
117
|
+
export interface FecBlockPathCount {
|
|
118
|
+
packetId: number
|
|
119
|
+
sbn: number
|
|
120
|
+
catalogTrackName?: string
|
|
121
|
+
multicastSymbols: number
|
|
122
|
+
moqSymbols: number
|
|
123
|
+
complete: boolean
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
type FecLifecycleTrace = {
|
|
127
|
+
atMs: number
|
|
128
|
+
event: 'repair-requested' | 'repair-arrived' | 'timeout-queued' | 'cleanup-ack' | 'recovered' | 'failed'
|
|
129
|
+
packetId: number
|
|
130
|
+
sbn: number
|
|
131
|
+
catalogTrackName?: string
|
|
132
|
+
sourceCount: number
|
|
133
|
+
sourceEsis: number[]
|
|
134
|
+
repairRsIds: number[]
|
|
135
|
+
decoderPath: 'worker' | 'sync' | 'none'
|
|
136
|
+
deadlineRemainingMs?: number
|
|
137
|
+
pendingCleanup: boolean
|
|
138
|
+
rsId?: number
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface FECManagerConfig {
|
|
142
|
+
k?: number
|
|
143
|
+
/** Symbol size in bytes (T). NOT repair count. */
|
|
144
|
+
symbolSize?: number
|
|
145
|
+
interleaveDepthMs?: number
|
|
146
|
+
fps?: number
|
|
147
|
+
/** Guarded block inactivity deadline derived from the full delivery geometry. */
|
|
148
|
+
timeoutMs?: number
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface ResolvedFECTrackConfig {
|
|
152
|
+
k: number
|
|
153
|
+
symbolSize: number
|
|
154
|
+
interleaveDepthMs: number
|
|
155
|
+
timeoutMs: number
|
|
156
|
+
fps?: number
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface MutableFecCounters {
|
|
160
|
+
sourcePackets: number
|
|
161
|
+
repairPackets: number
|
|
162
|
+
duplicateSymbols: number
|
|
163
|
+
blocksClean: number
|
|
164
|
+
blocksRecovered: number
|
|
165
|
+
blocksFailed: number
|
|
166
|
+
greenFillFrames: number
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function newFecCounters(): MutableFecCounters {
|
|
170
|
+
return {
|
|
171
|
+
sourcePackets: 0,
|
|
172
|
+
repairPackets: 0,
|
|
173
|
+
duplicateSymbols: 0,
|
|
174
|
+
blocksClean: 0,
|
|
175
|
+
blocksRecovered: 0,
|
|
176
|
+
blocksFailed: 0,
|
|
177
|
+
greenFillFrames: 0,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// FECManager
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
export class FECManager {
|
|
186
|
+
// Per-track WASM RaptorQ decoders, keyed by physical stream identity.
|
|
187
|
+
#fecDecoders = new Map<string, { client: WasmFecDecoder; k: number }>()
|
|
188
|
+
/** @deprecated Use setFecDecoder(packetId, ...). Backwards-compat shim. */
|
|
189
|
+
|
|
190
|
+
// Per-track Worker-backed FEC decoders, keyed by physical stream identity.
|
|
191
|
+
// Worker-backed tracks use async decode — recovered frames arrive via onFrame callback,
|
|
192
|
+
// not synchronous return from feedSourceSymbol/feedRepairSymbol.
|
|
193
|
+
#workerClients = new Map<string, FecWorkerClient>()
|
|
194
|
+
// Worker setup can finish while an interleaved block is already collecting
|
|
195
|
+
// on the sync decoder. Keep that block on its original decoder so later
|
|
196
|
+
// source/repair symbols do not split across two RaptorQ states.
|
|
197
|
+
#syncDecoderBlockOwners = new Map<string, WasmFecDecoder>()
|
|
198
|
+
#decoderBlockOwners = new WeakMap<object, Map<number, {
|
|
199
|
+
catalogTrackName?: string
|
|
200
|
+
conflicted: boolean
|
|
201
|
+
}>>()
|
|
202
|
+
|
|
203
|
+
#fecK = 4
|
|
204
|
+
#fecSymbolSize = 0
|
|
205
|
+
#fecTimeoutMs = 500 // default 500ms; refined by setFramerate() when fps known
|
|
206
|
+
#trackConfigs = new Map<number, ResolvedFECTrackConfig>()
|
|
207
|
+
#fecStats = newFecCounters()
|
|
208
|
+
#fecTrackStats = new Map<number, MutableFecCounters>()
|
|
209
|
+
#fecWorkerStats = new Map<number, FecTrackStats>()
|
|
210
|
+
#fecStatsObservers = new Set<(packetId: number, stats: FecTrackStats) => void>()
|
|
211
|
+
|
|
212
|
+
// Block inactivity tracking: (packetId, SBN) -> timer. The deadline is
|
|
213
|
+
// derived from catalog geometry and refreshed on progress; only an idle
|
|
214
|
+
// block fails.
|
|
215
|
+
#activeBlocks = new Map<string, ReturnType<typeof setTimeout>>()
|
|
216
|
+
#activeBlockDeadlinesMs = new Map<string, number>()
|
|
217
|
+
#activeBlockTimeoutOwners = new Map<string, object>()
|
|
218
|
+
#lifecycleTrace: FecLifecycleTrace[] = []
|
|
219
|
+
// Worker-backed timeouts enqueue cleanup behind all prior symbol messages.
|
|
220
|
+
// Failure remains pending until cleanupComplete acknowledges that queue drain.
|
|
221
|
+
#pendingWorkerCleanups = new Set<string>()
|
|
222
|
+
#pendingWorkerCleanupClients = new Map<string, FecWorkerClient>()
|
|
223
|
+
// Source symbol count per track-scoped block — marks complete when K symbols arrive.
|
|
224
|
+
#blockSourceCounts = new Map<string, number>()
|
|
225
|
+
#blockSourceEsis = new Map<string, Set<number>>()
|
|
226
|
+
#blockRepairRsIds = new Map<string, Set<number>>()
|
|
227
|
+
// Blocks already recovered — prevents late source symbols from restarting timeouts.
|
|
228
|
+
// WASM removes blocks after recovery; late symbols create empty blocks returning null,
|
|
229
|
+
// which would falsely start new timeouts without this guard.
|
|
230
|
+
#completedBlocks = new Set<string>()
|
|
231
|
+
#seenSymbols = new Set<string>()
|
|
232
|
+
#blockPathCounts = new Map<string, FecBlockPathCount>()
|
|
233
|
+
// Exact catalog identity observed on accepted symbols for each FEC block.
|
|
234
|
+
// Conflicting identities collapse to null so recovery fails closed.
|
|
235
|
+
#blockCatalogTrackNames = new Map<string, string | null>()
|
|
236
|
+
// A receiver joins an already-running live stream in the middle of D
|
|
237
|
+
// interleaved source blocks. This applies to both multicast and reliable MoQ:
|
|
238
|
+
// a MoQ group boundary is not an FEC ESI=0 boundary. Those blocks are not
|
|
239
|
+
// packet loss because their earlier ESIs predate the subscription. Ignore
|
|
240
|
+
// each such block until ESI=0 establishes a complete boundary for the track.
|
|
241
|
+
// After that boundary, a first observed ESI>0 is real delivery loss and must
|
|
242
|
+
// continue through normal FEC recovery/failure accounting.
|
|
243
|
+
#startupAlignedPackets = new Set<string>()
|
|
244
|
+
#startupPartialBlocks = new Set<string>()
|
|
245
|
+
// A live multicast join can observe an ESI=0 source boundary before the
|
|
246
|
+
// repair lane catches up. Returning false here also drops that media packet
|
|
247
|
+
// at MmtpRouter, so the readiness probe is multicast-only and bounded: an
|
|
248
|
+
// actual multicast repair unlocks the next boundary, while a second source
|
|
249
|
+
// boundary fails open. Reliable MoQ media continues through normal startup
|
|
250
|
+
// alignment and retained MoQ repair cannot prove multicast readiness.
|
|
251
|
+
#startupRequireRepairReady = false
|
|
252
|
+
#startupRepairReadyPackets = new Set<string>()
|
|
253
|
+
#startupRepairBoundaryCounts = new Map<string, number>()
|
|
254
|
+
#startupRepairFailOpenPackets = new Set<string>()
|
|
255
|
+
// SBNs that reached K source-symbol arrivals via trackBlock. Used by the
|
|
256
|
+
// worker recovery suppression check (see onFrame handler below) so we only
|
|
257
|
+
// suppress `recovered=true` emits for blocks that ALSO completed cleanly
|
|
258
|
+
// via source symbols. Separating this from #completedBlocks (which is also
|
|
259
|
+
// populated by every worker frame emit, by failed timeouts, and by external
|
|
260
|
+
// callers) was the BLO-3323 L15 fix: the prior predicate caused blocksRecovered
|
|
261
|
+
// to stay at 0 because the SBN was always in #completedBlocks by the time the
|
|
262
|
+
// worker fired recovered=true.
|
|
263
|
+
#cleanlyCompletedBlocks = new Set<string>()
|
|
264
|
+
// D-28: blocks whose repair deadline already fired onRepairNeeded. At most
|
|
265
|
+
// one repair request per block; entries are removed when the block
|
|
266
|
+
// completes, recovers, or fails.
|
|
267
|
+
#repairRequestedBlocks = new Set<string>()
|
|
268
|
+
// D-28: repair-deadline timers refreshed by accepted source progress until
|
|
269
|
+
// the first repair request. A `null` value marks a deadline that already
|
|
270
|
+
// fired (never re-armed). At most K accepted source symbols can refresh the
|
|
271
|
+
// horizon, so browser scheduling delay cannot create an early loss signal
|
|
272
|
+
// and repair traffic cannot keep a block alive indefinitely.
|
|
273
|
+
#repairDeadlines = new Map<string, ReturnType<typeof setTimeout> | null>()
|
|
274
|
+
// Worker recovery emits all K source symbols. Track recovery once per block
|
|
275
|
+
// while routing only ESI slots that were absent from the direct source path.
|
|
276
|
+
#workerRecoveredBlocks = new Set<string>()
|
|
277
|
+
|
|
278
|
+
// Framerate detection state
|
|
279
|
+
#fpsDetected = false
|
|
280
|
+
#fecFps?: number
|
|
281
|
+
#fecInterleaveDepthMs = 0
|
|
282
|
+
|
|
283
|
+
// Worker error log rate-limiter — bounds console.warn output and exported
|
|
284
|
+
// error cardinality when the FEC Worker emits the same failure repeatedly
|
|
285
|
+
// with volatile packet details (e.g. ssStart) in the message text.
|
|
286
|
+
// Stable Worker codes aggregate those messages; uncoded/legacy messages are
|
|
287
|
+
// capped into an `other` bucket. This also covers wasm-bindgen
|
|
288
|
+
// "recursive use of an object" bubbled from add_source/add_repair during
|
|
289
|
+
// overlapping message processing). Without this, Point 5 of the 7-point
|
|
290
|
+
// validation harness — which caps headless warnings at 500 — fails even
|
|
291
|
+
// though the errors are duplicates of a single underlying event.
|
|
292
|
+
#workerErrorCounts = new Map<string, number>()
|
|
293
|
+
#repairSymbolSizeMismatchCount = 0
|
|
294
|
+
|
|
295
|
+
// Event callbacks — block lifecycle notifications for MmtpRouter wiring.
|
|
296
|
+
// NOTE: For sync WASM path, recovery routing is handled by MmtpRouter from
|
|
297
|
+
// feedSourceSymbol/feedRepairSymbol return values. For Worker path, recovery
|
|
298
|
+
// arrives via onBlockRecovered callback.
|
|
299
|
+
onBlockStarted:
|
|
300
|
+
| ((sbn: number, packetId: number, catalogTrackName?: string) => void)
|
|
301
|
+
| null = null
|
|
302
|
+
onBlockComplete:
|
|
303
|
+
| ((sbn: number, packetId: number, catalogTrackName?: string) => void)
|
|
304
|
+
| null = null
|
|
305
|
+
onBlockFailed:
|
|
306
|
+
| ((sbn: number, packetId?: number, catalogTrackName?: string) => void)
|
|
307
|
+
| null = null
|
|
308
|
+
|
|
309
|
+
// D-28 on-demand repair callbacks. When onRepairNeeded is registered, each
|
|
310
|
+
// block arms a one-shot repair deadline after the derived K-symbol source
|
|
311
|
+
// horizon; a block still short of K when it fires requests a per-SBN MoQ
|
|
312
|
+
// repair fetch upstream. The inactivity fail bound is unchanged either way —
|
|
313
|
+
// repair and failure are orthogonal timers, so wiring D-28 never delays
|
|
314
|
+
// failure accounting. onRepairResolved fires when a repair-requested block
|
|
315
|
+
// leaves the active set, with the outcome ("completed" after clean completion
|
|
316
|
+
// or recovery, "failed" at the fail bound) so the upstream in-flight slot
|
|
317
|
+
// (MulticastManager.clearRepairForBlock) can be released and logged.
|
|
318
|
+
onRepairNeeded:
|
|
319
|
+
| ((sbn: number, packetId: number, catalogTrackName?: string) => void)
|
|
320
|
+
| null = null
|
|
321
|
+
onRepairResolved:
|
|
322
|
+
| ((
|
|
323
|
+
sbn: number,
|
|
324
|
+
packetId: number,
|
|
325
|
+
outcome: 'completed' | 'failed',
|
|
326
|
+
catalogTrackName?: string,
|
|
327
|
+
) => void)
|
|
328
|
+
| null = null
|
|
329
|
+
|
|
330
|
+
// Worker-path callbacks — async recovery and stats from FecWorkerClient.
|
|
331
|
+
onBlockRecovered: ((sbn: number, packetId: number, data: ArrayBuffer, meta: FrameMeta, catalogTrackName?: string) => void) | null = null
|
|
332
|
+
onFecStatsUpdate: ((packetId: number, stats: FecTrackStats) => void) | null = null
|
|
333
|
+
onGreenFill: ((sbn: number, packetId: number, data: ArrayBuffer) => void) | null = null
|
|
334
|
+
|
|
335
|
+
#streamKey(packetId: number, catalogTrackName?: string): string {
|
|
336
|
+
return catalogTrackName === undefined
|
|
337
|
+
? `${packetId}`
|
|
338
|
+
: `${packetId}:${encodeURIComponent(catalogTrackName)}`
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
#belongsToPacket(key: string, packetId: number): boolean {
|
|
342
|
+
return Number(key.split(':', 1)[0]) === packetId
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#belongsToStream(key: string, packetId: number, catalogTrackName: string): boolean {
|
|
346
|
+
if (!this.#belongsToPacket(key, packetId)) return false
|
|
347
|
+
const parts = key.split(':')
|
|
348
|
+
const encodedTrackName = encodeURIComponent(catalogTrackName)
|
|
349
|
+
if (parts.length === 2) return parts[1] === encodedTrackName
|
|
350
|
+
if (parts.length >= 3) return parts[2] === encodedTrackName
|
|
351
|
+
return false
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#packetIdsForStreamKeys(keys: Set<string>): number[] {
|
|
355
|
+
return [...new Set([...keys].map((key) => Number(key.split(':', 1)[0])))]
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
#blockKey(sbn: number, packetId: number, catalogTrackName?: string): string {
|
|
359
|
+
const base = `${packetId}:${sbn}`
|
|
360
|
+
return catalogTrackName === undefined
|
|
361
|
+
? base
|
|
362
|
+
: `${base}:${encodeURIComponent(catalogTrackName)}`
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
#parseBlockKey(key: string): { packetId: number; sbn: number; catalogTrackName?: string } {
|
|
366
|
+
const [packetIdText, sbnText, ...catalogParts] = key.split(':')
|
|
367
|
+
const catalogTrackName = catalogParts.length > 0
|
|
368
|
+
? decodeURIComponent(catalogParts.join(':'))
|
|
369
|
+
: undefined
|
|
370
|
+
return {
|
|
371
|
+
packetId: Number(packetIdText) || 0,
|
|
372
|
+
sbn: Number(sbnText) || 0,
|
|
373
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#decoderForPacket(packetId: number, catalogTrackName?: string) {
|
|
378
|
+
return this.#fecDecoders.get(this.#streamKey(packetId, catalogTrackName))
|
|
379
|
+
?? this.#fecDecoders.get(this.#streamKey(packetId))
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#workerForPacket(packetId: number, catalogTrackName?: string) {
|
|
383
|
+
return this.#workerClients.get(this.#streamKey(packetId, catalogTrackName))
|
|
384
|
+
?? this.#workerClients.get(this.#streamKey(packetId))
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
#claimDecoderBlock(
|
|
388
|
+
client: object,
|
|
389
|
+
sbn: number,
|
|
390
|
+
catalogTrackName?: string,
|
|
391
|
+
): boolean {
|
|
392
|
+
let blocks = this.#decoderBlockOwners.get(client)
|
|
393
|
+
if (!blocks) {
|
|
394
|
+
blocks = new Map()
|
|
395
|
+
this.#decoderBlockOwners.set(client, blocks)
|
|
396
|
+
}
|
|
397
|
+
const owner = blocks.get(sbn)
|
|
398
|
+
if (!owner) {
|
|
399
|
+
blocks.set(sbn, { catalogTrackName, conflicted: false })
|
|
400
|
+
if (blocks.size > 512) {
|
|
401
|
+
const stale = [...blocks.keys()].slice(0, 256)
|
|
402
|
+
for (const key of stale) blocks.delete(key)
|
|
403
|
+
}
|
|
404
|
+
return true
|
|
405
|
+
}
|
|
406
|
+
if (owner.conflicted) return false
|
|
407
|
+
if (owner.catalogTrackName === catalogTrackName) return true
|
|
408
|
+
owner.conflicted = true
|
|
409
|
+
return false
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
#decoderBlockCatalogTrackName(client: object, sbn: number): string | undefined {
|
|
413
|
+
const owner = this.#decoderBlockOwners.get(client)?.get(sbn)
|
|
414
|
+
return owner?.conflicted ? undefined : owner?.catalogTrackName
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
#resetDecoderBlockOwners(client: object, catalogTrackName: string): void {
|
|
418
|
+
const blocks = this.#decoderBlockOwners.get(client)
|
|
419
|
+
if (!blocks) return
|
|
420
|
+
for (const [sbn, owner] of blocks) {
|
|
421
|
+
if (owner.catalogTrackName === catalogTrackName || owner.conflicted) {
|
|
422
|
+
blocks.delete(sbn)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (blocks.size === 0) this.#decoderBlockOwners.delete(client)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#discardFecDecoderClient(client: WasmFecDecoder): void {
|
|
429
|
+
for (const [key, entry] of this.#fecDecoders) {
|
|
430
|
+
if (entry.client === client) this.#fecDecoders.delete(key)
|
|
431
|
+
}
|
|
432
|
+
this.#decoderBlockOwners.delete(client)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
#assertExclusiveDecoderClient(
|
|
436
|
+
streamKey: string,
|
|
437
|
+
client: object,
|
|
438
|
+
registrations: Iterable<readonly [string, object]>,
|
|
439
|
+
kind: 'decoder' | 'worker',
|
|
440
|
+
): void {
|
|
441
|
+
for (const [registeredKey, registeredClient] of registrations) {
|
|
442
|
+
if (registeredClient !== client || registeredKey === streamKey) continue
|
|
443
|
+
throw new Error(
|
|
444
|
+
`[FECManager] ${kind} client is already registered for stream ${registeredKey}; ` +
|
|
445
|
+
`create one client per physical stream`,
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
#statsForPacket(packetId: number): MutableFecCounters {
|
|
451
|
+
let stats = this.#fecTrackStats.get(packetId)
|
|
452
|
+
if (!stats) {
|
|
453
|
+
stats = newFecCounters()
|
|
454
|
+
this.#fecTrackStats.set(packetId, stats)
|
|
455
|
+
}
|
|
456
|
+
return stats
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
#markPathBlockComplete(sbn: number, packetId: number, catalogTrackName?: string): void {
|
|
460
|
+
const block = this.#blockPathCounts.get(this.#blockKey(sbn, packetId, catalogTrackName))
|
|
461
|
+
if (block) block.complete = true
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Cancel and forget a block's D-28 repair-deadline timer. */
|
|
465
|
+
#clearRepairDeadline(key: string): void {
|
|
466
|
+
const timer = this.#repairDeadlines.get(key)
|
|
467
|
+
if (timer != null) clearTimeout(timer)
|
|
468
|
+
this.#repairDeadlines.delete(key)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
#requestRepair(
|
|
472
|
+
sbn: number,
|
|
473
|
+
packetId: number,
|
|
474
|
+
catalogTrackName?: string,
|
|
475
|
+
): void {
|
|
476
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
477
|
+
if (
|
|
478
|
+
this.#completedBlocks.has(key) ||
|
|
479
|
+
!this.onRepairNeeded ||
|
|
480
|
+
this.#repairRequestedBlocks.has(key)
|
|
481
|
+
) return
|
|
482
|
+
|
|
483
|
+
const deadline = this.#repairDeadlines.get(key)
|
|
484
|
+
if (deadline) clearTimeout(deadline)
|
|
485
|
+
this.#repairDeadlines.set(key, null)
|
|
486
|
+
this.#repairRequestedBlocks.add(key)
|
|
487
|
+
|
|
488
|
+
// Preserve the source-progress inactivity deadline and append exactly one
|
|
489
|
+
// bounded repair-fetch grace. Once requested, noteBlockActivity() no
|
|
490
|
+
// longer moves this terminal cleanup boundary.
|
|
491
|
+
const config = this.#resolveTrackConfig(packetId)
|
|
492
|
+
const graceMs = this.#repairGraceMs(config)
|
|
493
|
+
const now = performance.now()
|
|
494
|
+
const currentDeadline = this.#activeBlockDeadlinesMs.get(key) ?? now
|
|
495
|
+
this.#armBlockTimeout(
|
|
496
|
+
sbn,
|
|
497
|
+
packetId,
|
|
498
|
+
catalogTrackName,
|
|
499
|
+
Math.max(1, Math.ceil(Math.max(now, currentDeadline) + graceMs - now)),
|
|
500
|
+
)
|
|
501
|
+
this.#traceLifecycle('repair-requested', sbn, packetId, catalogTrackName)
|
|
502
|
+
if (catalogTrackName === undefined) {
|
|
503
|
+
this.onRepairNeeded(sbn, packetId)
|
|
504
|
+
} else {
|
|
505
|
+
this.onRepairNeeded(sbn, packetId, catalogTrackName)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
#traceLifecycle(
|
|
510
|
+
event: FecLifecycleTrace['event'],
|
|
511
|
+
sbn: number,
|
|
512
|
+
packetId: number,
|
|
513
|
+
catalogTrackName?: string,
|
|
514
|
+
rsId?: number,
|
|
515
|
+
): void {
|
|
516
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
517
|
+
const deadline = this.#activeBlockDeadlinesMs.get(key)
|
|
518
|
+
const decoderPath = this.#syncDecoderBlockOwners.has(key)
|
|
519
|
+
? 'sync'
|
|
520
|
+
: this.#workerForPacket(packetId, catalogTrackName)
|
|
521
|
+
? 'worker'
|
|
522
|
+
: this.#decoderForPacket(packetId, catalogTrackName)
|
|
523
|
+
? 'sync'
|
|
524
|
+
: 'none'
|
|
525
|
+
this.#lifecycleTrace.push({
|
|
526
|
+
atMs: Date.now(),
|
|
527
|
+
event,
|
|
528
|
+
packetId,
|
|
529
|
+
sbn,
|
|
530
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
531
|
+
sourceCount: this.#blockSourceCounts.get(key) ?? 0,
|
|
532
|
+
sourceEsis: [...(this.#blockSourceEsis.get(key) ?? [])].sort((a, b) => a - b),
|
|
533
|
+
repairRsIds: [...(this.#blockRepairRsIds.get(key) ?? [])].sort((a, b) => a - b),
|
|
534
|
+
decoderPath,
|
|
535
|
+
...(deadline === undefined
|
|
536
|
+
? {}
|
|
537
|
+
: { deadlineRemainingMs: Math.max(0, deadline - performance.now()) }),
|
|
538
|
+
pendingCleanup: this.#pendingWorkerCleanups.has(key),
|
|
539
|
+
...(rsId === undefined ? {} : { rsId }),
|
|
540
|
+
})
|
|
541
|
+
if (this.#lifecycleTrace.length > 64) this.#lifecycleTrace.splice(0, 32)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#defaultTrackConfig(): ResolvedFECTrackConfig {
|
|
545
|
+
return {
|
|
546
|
+
k: this.#fecK,
|
|
547
|
+
symbolSize: this.#fecSymbolSize,
|
|
548
|
+
interleaveDepthMs: this.#fecInterleaveDepthMs,
|
|
549
|
+
fps: this.#fecFps,
|
|
550
|
+
timeoutMs: this.#fecTimeoutMs,
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
#resolveTrackConfig(packetId: number): ResolvedFECTrackConfig {
|
|
555
|
+
return this.#trackConfigs.get(packetId) ?? this.#defaultTrackConfig()
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
#computeTimeout(config: {
|
|
559
|
+
k: number
|
|
560
|
+
interleaveDepthMs: number
|
|
561
|
+
fps?: number
|
|
562
|
+
timeoutMs?: number
|
|
563
|
+
}): number {
|
|
564
|
+
if (config.timeoutMs && config.timeoutMs > 0) return config.timeoutMs
|
|
565
|
+
// Only a valid §5.1 ms interleave (>= MIN_INTERLEAVE_DEPTH_MS) drives the
|
|
566
|
+
// timeout. A sub-floor value is a raw frame count (no fps to convert with);
|
|
567
|
+
// fall back to the established #fecTimeoutMs rather than collapse to ~0.
|
|
568
|
+
if (config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) {
|
|
569
|
+
return Math.max(
|
|
570
|
+
this.#fecTimeoutMs,
|
|
571
|
+
computeFecDeliveryWindowMs({
|
|
572
|
+
interleaveDepthMs: config.interleaveDepthMs,
|
|
573
|
+
sourceSymbols: config.k,
|
|
574
|
+
mediaUnitsPerSecond: config.fps,
|
|
575
|
+
}),
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
return this.#fecTimeoutMs
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
#repairDeadlineMs(config: ResolvedFECTrackConfig): number {
|
|
582
|
+
const halfFailBoundMs = Math.ceil(config.timeoutMs / 2)
|
|
583
|
+
if (
|
|
584
|
+
config.k > 0 &&
|
|
585
|
+
config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS &&
|
|
586
|
+
config.fps !== undefined &&
|
|
587
|
+
Number.isFinite(config.fps) &&
|
|
588
|
+
config.fps > 0
|
|
589
|
+
) {
|
|
590
|
+
const mediaUnitMs = 1000 / config.fps
|
|
591
|
+
const interleaveUnits = deriveFecInterleaveDepth(
|
|
592
|
+
config.interleaveDepthMs,
|
|
593
|
+
mediaUnitMs,
|
|
594
|
+
)
|
|
595
|
+
const sourceHorizonMs = Math.ceil(config.k * interleaveUnits * mediaUnitMs)
|
|
596
|
+
if (sourceHorizonMs < config.timeoutMs) return sourceHorizonMs
|
|
597
|
+
}
|
|
598
|
+
return halfFailBoundMs
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
#repairGraceMs(config: ResolvedFECTrackConfig): number {
|
|
602
|
+
return Math.max(1, config.timeoutMs - this.#repairDeadlineMs(config))
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
#armBlockTimeout(
|
|
606
|
+
sbn: number,
|
|
607
|
+
packetId: number,
|
|
608
|
+
catalogTrackName: string | undefined,
|
|
609
|
+
delayMs: number,
|
|
610
|
+
): void {
|
|
611
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
612
|
+
const current = this.#activeBlocks.get(key)
|
|
613
|
+
if (current) clearTimeout(current)
|
|
614
|
+
const boundedDelayMs = Math.max(1, Math.ceil(delayMs))
|
|
615
|
+
this.#activeBlockDeadlinesMs.set(key, performance.now() + boundedDelayMs)
|
|
616
|
+
const owner = {}
|
|
617
|
+
this.#activeBlockTimeoutOwners.set(key, owner)
|
|
618
|
+
const timer = setTimeout(() => {
|
|
619
|
+
// clearTimeout() cannot retract a callback that the browser already
|
|
620
|
+
// moved onto its task queue. Only the timer that still owns this
|
|
621
|
+
// block's deadline may fail it or enqueue Worker cleanup.
|
|
622
|
+
if (this.#activeBlockTimeoutOwners.get(key) !== owner) return
|
|
623
|
+
this.#activeBlocks.delete(key)
|
|
624
|
+
this.#activeBlockDeadlinesMs.delete(key)
|
|
625
|
+
this.#activeBlockTimeoutOwners.delete(key)
|
|
626
|
+
this.#traceLifecycle('timeout-queued', sbn, packetId, catalogTrackName)
|
|
627
|
+
|
|
628
|
+
// A block that began on the synchronous decoder remains owned by that
|
|
629
|
+
// decoder even if Worker setup completed later. Cleanup must follow the
|
|
630
|
+
// same ownership rule as source and repair symbol dispatch.
|
|
631
|
+
const syncOwner = this.#syncDecoderBlockOwners.get(key)
|
|
632
|
+
if (syncOwner) {
|
|
633
|
+
this.#routeSyncCleanup(syncOwner, sbn, packetId)
|
|
634
|
+
this.#failBlock(sbn, packetId, false, catalogTrackName)
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const workerClient = this.#workerForPacket(packetId, catalogTrackName)
|
|
639
|
+
if (workerClient) {
|
|
640
|
+
this.#pendingWorkerCleanups.add(key)
|
|
641
|
+
this.#pendingWorkerCleanupClients.set(key, workerClient)
|
|
642
|
+
workerClient.cleanup(sbn)
|
|
643
|
+
} else {
|
|
644
|
+
const decoder = this.#decoderForPacket(packetId, catalogTrackName)?.client
|
|
645
|
+
if (decoder) {
|
|
646
|
+
this.#routeSyncCleanup(decoder, sbn, packetId)
|
|
647
|
+
this.#failBlock(sbn, packetId, false, catalogTrackName)
|
|
648
|
+
} else {
|
|
649
|
+
this.#failBlock(sbn, packetId, false, catalogTrackName)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}, boundedDelayMs)
|
|
653
|
+
this.#activeBlocks.set(key, timer)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
#upsertTrackConfig(packetId: number, config: FECManagerConfig): ResolvedFECTrackConfig {
|
|
657
|
+
const previous = this.#trackConfigs.get(packetId) ?? this.#defaultTrackConfig()
|
|
658
|
+
const timingChanged =
|
|
659
|
+
config.k !== undefined ||
|
|
660
|
+
config.interleaveDepthMs !== undefined ||
|
|
661
|
+
config.fps !== undefined
|
|
662
|
+
const next: ResolvedFECTrackConfig = {
|
|
663
|
+
k: config.k ?? previous.k,
|
|
664
|
+
symbolSize:
|
|
665
|
+
config.symbolSize != null && config.symbolSize > 0
|
|
666
|
+
? config.symbolSize
|
|
667
|
+
: previous.symbolSize,
|
|
668
|
+
interleaveDepthMs: config.interleaveDepthMs ?? previous.interleaveDepthMs,
|
|
669
|
+
fps: config.fps ?? previous.fps,
|
|
670
|
+
timeoutMs: config.timeoutMs ?? (timingChanged ? 0 : previous.timeoutMs),
|
|
671
|
+
}
|
|
672
|
+
next.timeoutMs = this.#computeTimeout(next)
|
|
673
|
+
this.#trackConfigs.set(packetId, next)
|
|
674
|
+
return next
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
#normalizeSymbolData(packetId: number, data: Uint8Array): Uint8Array {
|
|
678
|
+
const symbolSize = this.symbolSizeForPacket(packetId)
|
|
679
|
+
if (symbolSize <= 0 || data.byteLength === symbolSize) return data
|
|
680
|
+
const normalized = new Uint8Array(symbolSize)
|
|
681
|
+
normalized.set(data.subarray(0, symbolSize))
|
|
682
|
+
return normalized
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
#blockKeysFor(sbn: number, packetId?: number, catalogTrackName?: string): string[] {
|
|
686
|
+
if (packetId != null && catalogTrackName !== undefined) {
|
|
687
|
+
return [this.#blockKey(sbn, packetId, catalogTrackName)]
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const keys = new Set<string>()
|
|
691
|
+
for (const source of [
|
|
692
|
+
this.#activeBlocks,
|
|
693
|
+
this.#blockSourceCounts,
|
|
694
|
+
this.#blockCatalogTrackNames,
|
|
695
|
+
]) {
|
|
696
|
+
for (const key of source.keys()) {
|
|
697
|
+
const block = this.#parseBlockKey(key)
|
|
698
|
+
if (
|
|
699
|
+
block.sbn === sbn &&
|
|
700
|
+
(packetId === undefined || block.packetId === packetId)
|
|
701
|
+
) keys.add(key)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
for (const source of [this.#completedBlocks, this.#cleanlyCompletedBlocks]) {
|
|
705
|
+
for (const key of source) {
|
|
706
|
+
const block = this.#parseBlockKey(key)
|
|
707
|
+
if (
|
|
708
|
+
block.sbn === sbn &&
|
|
709
|
+
(packetId === undefined || block.packetId === packetId)
|
|
710
|
+
) keys.add(key)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (packetId !== undefined) {
|
|
715
|
+
if (keys.size === 0) return [this.#blockKey(sbn, packetId)]
|
|
716
|
+
return keys.size === 1 ? [...keys] : []
|
|
717
|
+
}
|
|
718
|
+
return keys.size > 0 ? [...keys] : [this.#blockKey(sbn, 0, catalogTrackName)]
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// -----------------------------------------------------------------------
|
|
722
|
+
// Accessors
|
|
723
|
+
// -----------------------------------------------------------------------
|
|
724
|
+
|
|
725
|
+
/** Internal state for diagnostics — not part of the public stats API. */
|
|
726
|
+
get diagnostics() {
|
|
727
|
+
return {
|
|
728
|
+
timeoutMs: this.#fecTimeoutMs,
|
|
729
|
+
activeBlocks: this.#activeBlocks.size,
|
|
730
|
+
completedBlocks: this.#completedBlocks.size,
|
|
731
|
+
pendingCounts: Object.fromEntries(this.#blockSourceCounts),
|
|
732
|
+
fpsDetected: this.#fpsDetected,
|
|
733
|
+
interleaveDepthMs: this.#fecInterleaveDepthMs,
|
|
734
|
+
startupAlignedPackets: this.#packetIdsForStreamKeys(this.#startupAlignedPackets),
|
|
735
|
+
startupPartialBlocks: this.#startupPartialBlocks.size,
|
|
736
|
+
startupRepairReadyPackets: this.#packetIdsForStreamKeys(this.#startupRepairReadyPackets),
|
|
737
|
+
startupRepairFailOpenPackets: this.#packetIdsForStreamKeys(this.#startupRepairFailOpenPackets),
|
|
738
|
+
pendingWorkerCleanups: this.#pendingWorkerCleanups.size,
|
|
739
|
+
repairTrackedBlocks: this.#blockRepairRsIds.size,
|
|
740
|
+
lifecycleTrace: this.#lifecycleTrace.map((entry) => ({ ...entry })),
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
get fecStats(): FecStats {
|
|
745
|
+
const s = this.#fecStats
|
|
746
|
+
const affected = s.blocksRecovered + s.blocksFailed
|
|
747
|
+
const total = s.blocksClean + affected
|
|
748
|
+
const tracks = [...this.#fecTrackStats.entries()]
|
|
749
|
+
.sort(([left], [right]) => left - right)
|
|
750
|
+
.map(([packetId, stats]) => {
|
|
751
|
+
const trackAffected = stats.blocksRecovered + stats.blocksFailed
|
|
752
|
+
const trackTotal = stats.blocksClean + trackAffected
|
|
753
|
+
return {
|
|
754
|
+
...stats,
|
|
755
|
+
packetId,
|
|
756
|
+
blocksComplete: trackTotal,
|
|
757
|
+
k: this.#resolveTrackConfig(packetId).k,
|
|
758
|
+
recoveryRate: trackAffected > 0
|
|
759
|
+
? (stats.blocksRecovered / trackAffected) * 100
|
|
760
|
+
: 100,
|
|
761
|
+
packetLossRate: trackTotal > 0 ? (trackAffected / trackTotal) * 100 : 0,
|
|
762
|
+
}
|
|
763
|
+
})
|
|
764
|
+
return {
|
|
765
|
+
...s,
|
|
766
|
+
blocksComplete: total,
|
|
767
|
+
mixedPathBlocks: Array.from(this.#blockPathCounts.values()).filter(
|
|
768
|
+
(block) => block.multicastSymbols > 0 && block.moqSymbols > 0,
|
|
769
|
+
).length,
|
|
770
|
+
recentBlockPathCounts: Array.from(this.#blockPathCounts.values()).slice(-24),
|
|
771
|
+
k: this.#fecK,
|
|
772
|
+
// True recovery rate: of blocks that HAD loss, how many were recovered?
|
|
773
|
+
recoveryRate: affected > 0 ? (s.blocksRecovered / affected) * 100 : 100,
|
|
774
|
+
packetLossRate: total > 0 ? (affected / total) * 100 : 0,
|
|
775
|
+
tracks,
|
|
776
|
+
workers: [...this.#fecWorkerStats.entries()]
|
|
777
|
+
.sort(([left], [right]) => left - right)
|
|
778
|
+
.map(([packetId, stats]) => ({
|
|
779
|
+
...stats,
|
|
780
|
+
config: {
|
|
781
|
+
...stats.config,
|
|
782
|
+
resolution: stats.config.resolution ? { ...stats.config.resolution } : undefined,
|
|
783
|
+
},
|
|
784
|
+
blocks: stats.blocks.map((block) => ({ ...block })),
|
|
785
|
+
decoder: stats.decoder ? { ...stats.decoder } : undefined,
|
|
786
|
+
altaFailedDetails: stats.altaFailedDetails?.map((detail) => ({ ...detail })),
|
|
787
|
+
packetId,
|
|
788
|
+
})),
|
|
789
|
+
repairSymbolSizeMismatches: this.#repairSymbolSizeMismatchCount,
|
|
790
|
+
workerErrors: Object.fromEntries(this.#workerErrorCounts),
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Observe Worker-backed per-track snapshots without taking ownership of the
|
|
796
|
+
* legacy single-callback slot. Infrastructure diagnostics and a player may
|
|
797
|
+
* therefore consume the same source event without wrapping or replacing one
|
|
798
|
+
* another's callback.
|
|
799
|
+
*/
|
|
800
|
+
addFecStatsObserver(
|
|
801
|
+
observer: (packetId: number, stats: FecTrackStats) => void,
|
|
802
|
+
): () => void {
|
|
803
|
+
this.#fecStatsObservers.add(observer)
|
|
804
|
+
return () => this.#fecStatsObservers.delete(observer)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** Start a new multicast transport join without discarding track/decoder state. */
|
|
808
|
+
beginMulticastJoin(): void {
|
|
809
|
+
this.#startupAlignedPackets.clear()
|
|
810
|
+
this.#startupPartialBlocks.clear()
|
|
811
|
+
this.#startupRequireRepairReady = true
|
|
812
|
+
this.#startupRepairReadyPackets.clear()
|
|
813
|
+
this.#startupRepairBoundaryCounts.clear()
|
|
814
|
+
this.#startupRepairFailOpenPackets.clear()
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** Begin a fresh decode epoch for one physical stream. */
|
|
818
|
+
resetTrack(packetId: number, catalogTrackName: string): void {
|
|
819
|
+
if (!Number.isInteger(packetId) || packetId < 0) {
|
|
820
|
+
throw new Error(`[FECManager] invalid packetId for reset: ${packetId}`)
|
|
821
|
+
}
|
|
822
|
+
if (!catalogTrackName) {
|
|
823
|
+
throw new Error('[FECManager] catalogTrackName is required for track reset')
|
|
824
|
+
}
|
|
825
|
+
const resetMap = <T>(map: Map<string, T>): void => {
|
|
826
|
+
for (const key of map.keys()) {
|
|
827
|
+
if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue
|
|
828
|
+
map.delete(key)
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const resetTimerMap = (
|
|
832
|
+
map: Map<string, ReturnType<typeof setTimeout> | null>,
|
|
833
|
+
): void => {
|
|
834
|
+
for (const [key, timer] of map) {
|
|
835
|
+
if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue
|
|
836
|
+
if (timer != null) clearTimeout(timer)
|
|
837
|
+
map.delete(key)
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const resetSet = (set: Set<string>): void => {
|
|
841
|
+
for (const key of set) {
|
|
842
|
+
if (this.#belongsToStream(key, packetId, catalogTrackName)) set.delete(key)
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
resetTimerMap(this.#activeBlocks)
|
|
847
|
+
resetTimerMap(this.#repairDeadlines)
|
|
848
|
+
resetMap(this.#activeBlockDeadlinesMs)
|
|
849
|
+
resetMap(this.#activeBlockTimeoutOwners)
|
|
850
|
+
resetMap(this.#pendingWorkerCleanupClients)
|
|
851
|
+
resetMap(this.#blockSourceCounts)
|
|
852
|
+
resetMap(this.#blockSourceEsis)
|
|
853
|
+
resetMap(this.#blockRepairRsIds)
|
|
854
|
+
resetMap(this.#blockPathCounts)
|
|
855
|
+
resetMap(this.#blockCatalogTrackNames)
|
|
856
|
+
resetMap(this.#syncDecoderBlockOwners)
|
|
857
|
+
resetMap(this.#startupRepairBoundaryCounts)
|
|
858
|
+
resetSet(this.#pendingWorkerCleanups)
|
|
859
|
+
resetSet(this.#completedBlocks)
|
|
860
|
+
resetSet(this.#cleanlyCompletedBlocks)
|
|
861
|
+
resetSet(this.#repairRequestedBlocks)
|
|
862
|
+
resetSet(this.#workerRecoveredBlocks)
|
|
863
|
+
resetSet(this.#startupAlignedPackets)
|
|
864
|
+
resetSet(this.#startupPartialBlocks)
|
|
865
|
+
resetSet(this.#startupRepairReadyPackets)
|
|
866
|
+
resetSet(this.#startupRepairFailOpenPackets)
|
|
867
|
+
resetSet(this.#seenSymbols)
|
|
868
|
+
|
|
869
|
+
const decoderClients = new Set<WasmFecDecoder>()
|
|
870
|
+
for (const [key, entry] of this.#fecDecoders) {
|
|
871
|
+
if (this.#belongsToStream(key, packetId, catalogTrackName)) {
|
|
872
|
+
decoderClients.add(entry.client)
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
const clients = new Set<object>()
|
|
876
|
+
for (const client of decoderClients) {
|
|
877
|
+
if (
|
|
878
|
+
typeof client.flush === 'function' &&
|
|
879
|
+
typeof client.reset_stats === 'function'
|
|
880
|
+
) {
|
|
881
|
+
try {
|
|
882
|
+
client.flush()
|
|
883
|
+
client.reset_stats()
|
|
884
|
+
clients.add(client)
|
|
885
|
+
} catch {
|
|
886
|
+
this.#discardFecDecoderClient(client)
|
|
887
|
+
}
|
|
888
|
+
} else {
|
|
889
|
+
this.#discardFecDecoderClient(client)
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
for (const [key, client] of this.#workerClients) {
|
|
893
|
+
if (!this.#belongsToStream(key, packetId, catalogTrackName)) continue
|
|
894
|
+
client.reset()
|
|
895
|
+
clients.add(client)
|
|
896
|
+
}
|
|
897
|
+
for (const client of clients) this.#resetDecoderBlockOwners(client, catalogTrackName)
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/** Record a transport symbol and reject later duplicates of the same packetId/SBN/ESI. */
|
|
901
|
+
feedSymbolFromSource(
|
|
902
|
+
packetId: number,
|
|
903
|
+
sbn: number,
|
|
904
|
+
esi: number,
|
|
905
|
+
_data: Uint8Array,
|
|
906
|
+
source: FecSymbolSource,
|
|
907
|
+
catalogTrackName?: string,
|
|
908
|
+
): boolean {
|
|
909
|
+
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
910
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
911
|
+
const k = this.kForPacket(packetId)
|
|
912
|
+
const isSourceSymbol = esi >= 0 && esi < k
|
|
913
|
+
const isStartupPartial = this.#startupPartialBlocks.has(blockKey)
|
|
914
|
+
const awaitingMulticastRepair =
|
|
915
|
+
this.#startupRequireRepairReady &&
|
|
916
|
+
source === 'multicast' &&
|
|
917
|
+
!this.#startupRepairReadyPackets.has(streamKey) &&
|
|
918
|
+
!this.#startupRepairFailOpenPackets.has(streamKey)
|
|
919
|
+
if (awaitingMulticastRepair) {
|
|
920
|
+
if (!isSourceSymbol) {
|
|
921
|
+
this.#startupRepairReadyPackets.add(streamKey)
|
|
922
|
+
this.#startupRepairBoundaryCounts.delete(streamKey)
|
|
923
|
+
this.#markStartupPartial(blockKey)
|
|
924
|
+
return false
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (esi === 0) {
|
|
928
|
+
const boundaries = (this.#startupRepairBoundaryCounts.get(streamKey) ?? 0) + 1
|
|
929
|
+
this.#startupRepairBoundaryCounts.set(streamKey, boundaries)
|
|
930
|
+
if (boundaries >= STARTUP_REPAIR_WAIT_SOURCE_BOUNDARIES) {
|
|
931
|
+
this.#startupRepairBoundaryCounts.delete(streamKey)
|
|
932
|
+
this.#startupRepairFailOpenPackets.add(streamKey)
|
|
933
|
+
} else {
|
|
934
|
+
this.#markStartupPartial(blockKey)
|
|
935
|
+
return false
|
|
936
|
+
}
|
|
937
|
+
} else {
|
|
938
|
+
this.#markStartupPartial(blockKey)
|
|
939
|
+
return false
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (isSourceSymbol && esi === 0) {
|
|
943
|
+
// ESI=0 establishes a complete source boundary for this SBN. Remove
|
|
944
|
+
// only this block: other interleaved SBNs may still be join partials.
|
|
945
|
+
this.#startupPartialBlocks.delete(blockKey)
|
|
946
|
+
this.#startupAlignedPackets.add(streamKey)
|
|
947
|
+
} else if (
|
|
948
|
+
isStartupPartial ||
|
|
949
|
+
!this.#startupAlignedPackets.has(streamKey)
|
|
950
|
+
) {
|
|
951
|
+
this.#markStartupPartial(blockKey)
|
|
952
|
+
return false
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// A completed block remains authoritative after its individual symbol
|
|
956
|
+
// identities age out of the bounded #seenSymbols cache. Without this
|
|
957
|
+
// guard, a delayed copy from the other transport can recreate the block
|
|
958
|
+
// inside a Worker decoder; that orphan never receives the full source and
|
|
959
|
+
// repair set, so cleanup later reports a false failure/green fill.
|
|
960
|
+
if (this.#completedBlocks.has(blockKey)) {
|
|
961
|
+
this.#fecStats.duplicateSymbols++
|
|
962
|
+
this.#statsForPacket(packetId).duplicateSymbols++
|
|
963
|
+
return false
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const symbolKey = `${blockKey}:${esi}`
|
|
967
|
+
if (this.#seenSymbols.has(symbolKey)) {
|
|
968
|
+
this.#fecStats.duplicateSymbols++
|
|
969
|
+
this.#statsForPacket(packetId).duplicateSymbols++
|
|
970
|
+
return false
|
|
971
|
+
}
|
|
972
|
+
this.#seenSymbols.add(symbolKey)
|
|
973
|
+
|
|
974
|
+
const counts = this.#blockPathCounts.get(blockKey) ?? {
|
|
975
|
+
packetId,
|
|
976
|
+
sbn,
|
|
977
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
978
|
+
multicastSymbols: 0,
|
|
979
|
+
moqSymbols: 0,
|
|
980
|
+
complete: false,
|
|
981
|
+
}
|
|
982
|
+
if (source === 'multicast') counts.multicastSymbols++
|
|
983
|
+
else counts.moqSymbols++
|
|
984
|
+
this.#blockPathCounts.set(blockKey, counts)
|
|
985
|
+
this.#observeBlockCatalogTrackName(blockKey, catalogTrackName)
|
|
986
|
+
|
|
987
|
+
if (this.#seenSymbols.size > 4096) {
|
|
988
|
+
this.#seenSymbols = new Set(Array.from(this.#seenSymbols).slice(-2048))
|
|
989
|
+
}
|
|
990
|
+
if (this.#blockPathCounts.size > 512) {
|
|
991
|
+
this.#blockPathCounts = new Map(Array.from(this.#blockPathCounts).slice(-256))
|
|
992
|
+
}
|
|
993
|
+
if (this.#blockCatalogTrackNames.size > 512) {
|
|
994
|
+
this.#blockCatalogTrackNames = new Map(
|
|
995
|
+
Array.from(this.#blockCatalogTrackNames).slice(-256),
|
|
996
|
+
)
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
return true
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
#observeBlockCatalogTrackName(blockKey: string, catalogTrackName?: string): void {
|
|
1003
|
+
if (catalogTrackName === undefined) return
|
|
1004
|
+
if (!this.#blockCatalogTrackNames.has(blockKey)) {
|
|
1005
|
+
this.#blockCatalogTrackNames.set(blockKey, catalogTrackName)
|
|
1006
|
+
return
|
|
1007
|
+
}
|
|
1008
|
+
if (this.#blockCatalogTrackNames.get(blockKey) !== catalogTrackName) {
|
|
1009
|
+
this.#blockCatalogTrackNames.set(blockKey, null)
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/** Exact catalog identity retained for async recovery, if unambiguous. */
|
|
1014
|
+
catalogTrackNameForBlock(
|
|
1015
|
+
sbn: number,
|
|
1016
|
+
packetId: number,
|
|
1017
|
+
catalogTrackName?: string,
|
|
1018
|
+
): string | undefined {
|
|
1019
|
+
if (catalogTrackName !== undefined) {
|
|
1020
|
+
return this.#blockCatalogTrackNames.get(
|
|
1021
|
+
this.#blockKey(sbn, packetId, catalogTrackName),
|
|
1022
|
+
) ?? undefined
|
|
1023
|
+
}
|
|
1024
|
+
const names = new Set<string>()
|
|
1025
|
+
for (const [key, name] of this.#blockCatalogTrackNames) {
|
|
1026
|
+
const block = this.#parseBlockKey(key)
|
|
1027
|
+
if (block.packetId === packetId && block.sbn === sbn && name !== null) names.add(name)
|
|
1028
|
+
}
|
|
1029
|
+
return names.size === 1 ? [...names][0] : undefined
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
#markStartupPartial(blockKey: string): void {
|
|
1033
|
+
this.#startupPartialBlocks.add(blockKey)
|
|
1034
|
+
if (this.#startupPartialBlocks.size > 128) {
|
|
1035
|
+
this.#startupPartialBlocks = new Set(
|
|
1036
|
+
Array.from(this.#startupPartialBlocks).slice(-64),
|
|
1037
|
+
)
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/** Per-block state snapshot for FecBlockGrid visualization.
|
|
1042
|
+
* Returns the most recent blocks (active + recently completed). */
|
|
1043
|
+
getBlockStates(): Array<{
|
|
1044
|
+
sbn: number;
|
|
1045
|
+
packetId?: number;
|
|
1046
|
+
state: 'collecting' | 'complete' | 'failed';
|
|
1047
|
+
sourceCount: number;
|
|
1048
|
+
k: number;
|
|
1049
|
+
progress: number;
|
|
1050
|
+
}> {
|
|
1051
|
+
const blocks: Array<{ sbn: number; packetId?: number; state: 'collecting' | 'complete' | 'failed'; sourceCount: number; k: number; progress: number }> = [];
|
|
1052
|
+
// Active blocks (still collecting symbols)
|
|
1053
|
+
for (const [key] of this.#activeBlocks) {
|
|
1054
|
+
const { packetId, sbn } = this.#parseBlockKey(key);
|
|
1055
|
+
const count = this.#blockSourceCounts.get(key) ?? 0;
|
|
1056
|
+
const k = this.kForPacket(packetId);
|
|
1057
|
+
blocks.push({ sbn, packetId, state: 'collecting', sourceCount: count, k, progress: k > 0 ? Math.min(count / k, 1) : 0 });
|
|
1058
|
+
}
|
|
1059
|
+
// Recently completed blocks (last 16)
|
|
1060
|
+
const completed = [...this.#completedBlocks].slice(-16);
|
|
1061
|
+
for (const key of completed) {
|
|
1062
|
+
const { packetId, sbn } = this.#parseBlockKey(key);
|
|
1063
|
+
if (!this.#activeBlocks.has(key)) {
|
|
1064
|
+
const k = this.kForPacket(packetId);
|
|
1065
|
+
blocks.push({ sbn, packetId, state: 'complete', sourceCount: k, k, progress: 1 });
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
blocks.sort((a, b) => a.sbn - b.sbn || (a.packetId ?? 0) - (b.packetId ?? 0));
|
|
1069
|
+
return blocks.slice(-24); // cap at 24 most recent
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/** The raw WASM decoder for stats access (first registered decoder) */
|
|
1073
|
+
get wasmDecoder(): WasmFecDecoder | null {
|
|
1074
|
+
return this.#fecDecoders.values().next().value?.client ?? null
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
get k(): number { return this.#fecK }
|
|
1078
|
+
kForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).k }
|
|
1079
|
+
/** FEC symbol size in bytes. */
|
|
1080
|
+
get symbolSize(): number { return this.#fecSymbolSize }
|
|
1081
|
+
symbolSizeForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).symbolSize }
|
|
1082
|
+
/** FEC interleave window in milliseconds (0 = no interleaving). */
|
|
1083
|
+
get interleaveDepthMs(): number { return this.#fecInterleaveDepthMs }
|
|
1084
|
+
interleaveDepthMsForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).interleaveDepthMs }
|
|
1085
|
+
/** Resolved per-track block fail bound in milliseconds. */
|
|
1086
|
+
timeoutMsForPacket(packetId: number): number { return this.#resolveTrackConfig(packetId).timeoutMs }
|
|
1087
|
+
/** Resolved per-track D-28 repair request deadline in milliseconds. */
|
|
1088
|
+
repairDeadlineMsForPacket(packetId: number): number {
|
|
1089
|
+
return this.#repairDeadlineMs(this.#resolveTrackConfig(packetId))
|
|
1090
|
+
}
|
|
1091
|
+
/** Remaining fetch budget for a specific repair-requested block. */
|
|
1092
|
+
repairFetchTimeoutMsForBlock(
|
|
1093
|
+
sbn: number,
|
|
1094
|
+
packetId: number,
|
|
1095
|
+
catalogTrackName?: string,
|
|
1096
|
+
): number {
|
|
1097
|
+
const deadlineMs = this.#activeBlockDeadlinesMs.get(
|
|
1098
|
+
this.#blockKey(sbn, packetId, catalogTrackName),
|
|
1099
|
+
)
|
|
1100
|
+
if (deadlineMs !== undefined) {
|
|
1101
|
+
return Math.max(1, Math.ceil(deadlineMs - performance.now()))
|
|
1102
|
+
}
|
|
1103
|
+
const trackConfig = this.#resolveTrackConfig(packetId)
|
|
1104
|
+
return Math.max(1, trackConfig.timeoutMs - this.#repairDeadlineMs(trackConfig))
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// -----------------------------------------------------------------------
|
|
1108
|
+
// Configuration
|
|
1109
|
+
// -----------------------------------------------------------------------
|
|
1110
|
+
|
|
1111
|
+
/** Configure FEC parameters. */
|
|
1112
|
+
configure(config: FECManagerConfig): void {
|
|
1113
|
+
if (config.k != null) this.#fecK = config.k
|
|
1114
|
+
if (config.symbolSize != null && config.symbolSize > 0) this.#fecSymbolSize = config.symbolSize
|
|
1115
|
+
if (config.interleaveDepthMs != null) this.#fecInterleaveDepthMs = config.interleaveDepthMs
|
|
1116
|
+
if (config.fps != null && Number.isFinite(config.fps) && config.fps > 0) {
|
|
1117
|
+
this.#fecFps = config.fps
|
|
1118
|
+
}
|
|
1119
|
+
// interleaveDepthMs is in milliseconds per spec §5.1.
|
|
1120
|
+
// Valid range: 33ms (interactive/gaming) to 10000ms (broadcast ATSC 3.0).
|
|
1121
|
+
// Warn if value looks like frame count instead of ms (common misconfiguration).
|
|
1122
|
+
if (config.interleaveDepthMs != null && config.interleaveDepthMs > 0 && config.interleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS) {
|
|
1123
|
+
console.warn(`[FECManager] interleaveDepthMs=${config.interleaveDepthMs} looks like frame count, not milliseconds (§5.1 requires ms). Expected range: ${MIN_INTERLEAVE_DEPTH_MS}-10000ms.`)
|
|
1124
|
+
}
|
|
1125
|
+
if (config.timeoutMs != null && config.timeoutMs > 0) {
|
|
1126
|
+
this.#fecTimeoutMs = config.timeoutMs
|
|
1127
|
+
} else if (
|
|
1128
|
+
config.interleaveDepthMs != null &&
|
|
1129
|
+
config.interleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS
|
|
1130
|
+
) {
|
|
1131
|
+
this.#fecTimeoutMs = computeFecDeliveryWindowMs({
|
|
1132
|
+
interleaveDepthMs: config.interleaveDepthMs,
|
|
1133
|
+
sourceSymbols: this.#fecK,
|
|
1134
|
+
symbolSize: this.#fecSymbolSize,
|
|
1135
|
+
mediaUnitsPerSecond: config.fps,
|
|
1136
|
+
})
|
|
1137
|
+
}
|
|
1138
|
+
// A shared multicast parser receives every packetId. Keep existing track
|
|
1139
|
+
// deadlines at least as wide as the selected video delivery envelope so a
|
|
1140
|
+
// short audio interleave cannot expire during browser/AMT scheduling gaps.
|
|
1141
|
+
for (const [packetId, track] of this.#trackConfigs) {
|
|
1142
|
+
if (track.timeoutMs < this.#fecTimeoutMs) {
|
|
1143
|
+
this.#trackConfigs.set(packetId, { ...track, timeoutMs: this.#fecTimeoutMs })
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/** Configure FEC parameters for one packetId without changing legacy defaults. */
|
|
1149
|
+
configureTrack(packetId: number, config: FECManagerConfig): void {
|
|
1150
|
+
this.#upsertTrackConfig(packetId, config)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/** Register a FEC decoder for a specific packetId (track). */
|
|
1154
|
+
setFecDecoder(
|
|
1155
|
+
packetId: number,
|
|
1156
|
+
client: WasmFecDecoder,
|
|
1157
|
+
k: number,
|
|
1158
|
+
config?: FECManagerConfig,
|
|
1159
|
+
catalogTrackName?: string,
|
|
1160
|
+
): void {
|
|
1161
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
1162
|
+
this.#assertExclusiveDecoderClient(
|
|
1163
|
+
streamKey,
|
|
1164
|
+
client,
|
|
1165
|
+
[...this.#fecDecoders].map(([key, entry]) => [key, entry.client] as const),
|
|
1166
|
+
'decoder',
|
|
1167
|
+
)
|
|
1168
|
+
const trackConfig = this.#upsertTrackConfig(packetId, { ...config, k })
|
|
1169
|
+
this.#fecDecoders.set(streamKey, { client, k: trackConfig.k })
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Register a Worker-backed FEC decoder for a specific packetId (track).
|
|
1174
|
+
*
|
|
1175
|
+
* Worker-backed tracks decode asynchronously — feedSourceSymbol/feedRepairSymbol
|
|
1176
|
+
* return null immediately, and recovered frames arrive via the onBlockRecovered callback.
|
|
1177
|
+
*
|
|
1178
|
+
* Takes precedence over sync WASM decoders for the same packetId.
|
|
1179
|
+
*/
|
|
1180
|
+
setFecWorker(
|
|
1181
|
+
packetId: number,
|
|
1182
|
+
client: FecWorkerClient,
|
|
1183
|
+
config: FECManagerConfig,
|
|
1184
|
+
catalogTrackName?: string,
|
|
1185
|
+
): void {
|
|
1186
|
+
const k = config?.k
|
|
1187
|
+
if (k === undefined || !Number.isFinite(k) || k <= 0) {
|
|
1188
|
+
throw new Error(`[FECManager] packetId=${packetId} Worker requires an explicit positive k`)
|
|
1189
|
+
}
|
|
1190
|
+
const symbolSize = config.symbolSize
|
|
1191
|
+
if (symbolSize === undefined || !Number.isFinite(symbolSize) || symbolSize <= 0) {
|
|
1192
|
+
throw new Error(
|
|
1193
|
+
`[FECManager] packetId=${packetId} Worker requires an explicit positive symbolSize`,
|
|
1194
|
+
)
|
|
1195
|
+
}
|
|
1196
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
1197
|
+
this.#assertExclusiveDecoderClient(
|
|
1198
|
+
streamKey,
|
|
1199
|
+
client,
|
|
1200
|
+
this.#workerClients,
|
|
1201
|
+
'worker',
|
|
1202
|
+
)
|
|
1203
|
+
if (config) this.#upsertTrackConfig(packetId, config)
|
|
1204
|
+
const workerK = this.#resolveTrackConfig(packetId).k
|
|
1205
|
+
this.#workerClients.set(streamKey, client)
|
|
1206
|
+
|
|
1207
|
+
client.onFrame((data: ArrayBuffer, meta: FrameMeta) => {
|
|
1208
|
+
if (meta.recovered) {
|
|
1209
|
+
// Recovered Worker events represent one exact source-symbol position.
|
|
1210
|
+
// Block-completion accounting needs that exact ESI, and counting an
|
|
1211
|
+
// ESI-less event as recovered would report healthy FEC while dropping
|
|
1212
|
+
// the media. Enforce the Worker contract before any recovery metric.
|
|
1213
|
+
if (!Number.isInteger(meta.esi) || meta.esi! < 0 || meta.esi! >= workerK) {
|
|
1214
|
+
const kind = 'recovered-symbol-missing-esi'
|
|
1215
|
+
const seen = this.#workerErrorCounts.get(kind) ?? 0
|
|
1216
|
+
this.#workerErrorCounts.set(kind, seen + 1)
|
|
1217
|
+
if (seen < WORKER_ERROR_LOG_CAP) {
|
|
1218
|
+
console.warn(
|
|
1219
|
+
`[FECManager] Dropping recovered symbol without valid ESI: pid=${packetId} sbn=${meta.sbn} esi=${String(meta.esi)}`,
|
|
1220
|
+
)
|
|
1221
|
+
}
|
|
1222
|
+
return
|
|
1223
|
+
}
|
|
1224
|
+
const esi = meta.esi!
|
|
1225
|
+
|
|
1226
|
+
// Suppress duplicate routing when the worker fires recovered=true
|
|
1227
|
+
// for a block ALREADY CLEANLY COMPLETED by K source symbols.
|
|
1228
|
+
//
|
|
1229
|
+
// BLO-3323 L15: predicate was previously `#completedBlocks.has(sbn)`,
|
|
1230
|
+
// but `#completedBlocks` is populated by 5 paths (clean K-arrival,
|
|
1231
|
+
// failed timeout, every worker frame emit via markBlockCompleted below,
|
|
1232
|
+
// external markBlockCompleted, and cancelBlockTimeout). The first
|
|
1233
|
+
// worker frame for a block adds the SBN, so subsequent recovered=true
|
|
1234
|
+
// emits for the same block were always suppressed → blocksRecovered
|
|
1235
|
+
// stayed at 0 even when recovery actually fired. Switching to the
|
|
1236
|
+
// narrower #cleanlyCompletedBlocks set (populated only by trackBlock
|
|
1237
|
+
// when count >= K) preserves the original dedup intent (BLO-3035
|
|
1238
|
+
// layer 4 — don't double-route a block already received via source)
|
|
1239
|
+
// while letting genuinely-recovered blocks bump the counter.
|
|
1240
|
+
const recoveredCatalogTrackName = catalogTrackName
|
|
1241
|
+
?? this.#decoderBlockCatalogTrackName(client, meta.sbn)
|
|
1242
|
+
if (!this.#claimDecoderBlock(client, meta.sbn, recoveredCatalogTrackName)) return
|
|
1243
|
+
const blockKey = this.#blockKey(meta.sbn, packetId, recoveredCatalogTrackName)
|
|
1244
|
+
const cleanlyCompleted = this.#cleanlyCompletedBlocks.has(blockKey)
|
|
1245
|
+
if (!cleanlyCompleted) {
|
|
1246
|
+
if (!this.#workerRecoveredBlocks.has(blockKey)) {
|
|
1247
|
+
this.#traceLifecycle(
|
|
1248
|
+
'recovered',
|
|
1249
|
+
meta.sbn,
|
|
1250
|
+
packetId,
|
|
1251
|
+
recoveredCatalogTrackName,
|
|
1252
|
+
)
|
|
1253
|
+
this.#workerRecoveredBlocks.add(blockKey)
|
|
1254
|
+
this.#fecStats.blocksRecovered++
|
|
1255
|
+
this.#statsForPacket(packetId).blocksRecovered++
|
|
1256
|
+
if (this.#workerRecoveredBlocks.size > 1000) {
|
|
1257
|
+
const stale = Array.from(this.#workerRecoveredBlocks).slice(0, 500)
|
|
1258
|
+
for (const key of stale) this.#workerRecoveredBlocks.delete(key)
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// RaptorQ returns the full K-symbol block, not only erased symbols.
|
|
1263
|
+
// The direct path already parsed every source ESI in #seenSymbols;
|
|
1264
|
+
// routing those symbols again injects duplicate MFU fragments and can
|
|
1265
|
+
// leave a real FI=3 tail followed by duplicate FI=2 fragments.
|
|
1266
|
+
const symbolKey = `${blockKey}:${esi}`
|
|
1267
|
+
if (!this.#seenSymbols.has(symbolKey)) {
|
|
1268
|
+
this.#seenSymbols.add(symbolKey)
|
|
1269
|
+
this.onBlockRecovered?.(
|
|
1270
|
+
meta.sbn,
|
|
1271
|
+
packetId,
|
|
1272
|
+
data,
|
|
1273
|
+
meta,
|
|
1274
|
+
recoveredCatalogTrackName,
|
|
1275
|
+
)
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// The worker emits recovered source symbols in ESI order. Complete the
|
|
1280
|
+
// block only after the final slot so assemblers receive every missing
|
|
1281
|
+
// symbol before their completion callback flushes buffered media.
|
|
1282
|
+
if (
|
|
1283
|
+
esi >= workerK - 1 &&
|
|
1284
|
+
!this.#completedBlocks.has(blockKey)
|
|
1285
|
+
) {
|
|
1286
|
+
this.cancelBlockTimeout(meta.sbn, packetId, recoveredCatalogTrackName)
|
|
1287
|
+
if (recoveredCatalogTrackName === undefined) {
|
|
1288
|
+
this.onBlockComplete?.(meta.sbn, packetId)
|
|
1289
|
+
} else {
|
|
1290
|
+
this.onBlockComplete?.(
|
|
1291
|
+
meta.sbn,
|
|
1292
|
+
packetId,
|
|
1293
|
+
recoveredCatalogTrackName,
|
|
1294
|
+
)
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
} else {
|
|
1298
|
+
const completedCatalogTrackName = catalogTrackName
|
|
1299
|
+
?? this.#decoderBlockCatalogTrackName(client, meta.sbn)
|
|
1300
|
+
this.markBlockCompleted(meta.sbn, packetId, completedCatalogTrackName)
|
|
1301
|
+
}
|
|
1302
|
+
})
|
|
1303
|
+
|
|
1304
|
+
client.onSnapshot((stats: FecTrackStats) => {
|
|
1305
|
+
this.#fecWorkerStats.set(packetId, stats)
|
|
1306
|
+
try {
|
|
1307
|
+
this.onFecStatsUpdate?.(packetId, stats)
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
console.warn('[FECManager] FEC stats callback failed:', error)
|
|
1310
|
+
}
|
|
1311
|
+
for (const observer of this.#fecStatsObservers) {
|
|
1312
|
+
try {
|
|
1313
|
+
observer(packetId, stats)
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
console.warn('[FECManager] FEC stats observer failed:', error)
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
})
|
|
1319
|
+
|
|
1320
|
+
client.onGreenFill((sbn: number, data: ArrayBuffer) => {
|
|
1321
|
+
this.#fecStats.greenFillFrames++
|
|
1322
|
+
this.#statsForPacket(packetId).greenFillFrames++
|
|
1323
|
+
this.onGreenFill?.(sbn, packetId, data)
|
|
1324
|
+
})
|
|
1325
|
+
|
|
1326
|
+
client.onCleanupComplete((sbn: number) => {
|
|
1327
|
+
const cleanupKeys = [...this.#pendingWorkerCleanups].filter((key) => {
|
|
1328
|
+
const block = this.#parseBlockKey(key)
|
|
1329
|
+
return block.packetId === packetId &&
|
|
1330
|
+
block.sbn === sbn &&
|
|
1331
|
+
this.#pendingWorkerCleanupClients.get(key) === client
|
|
1332
|
+
})
|
|
1333
|
+
for (const key of cleanupKeys) {
|
|
1334
|
+
this.#pendingWorkerCleanups.delete(key)
|
|
1335
|
+
this.#pendingWorkerCleanupClients.delete(key)
|
|
1336
|
+
if (this.#completedBlocks.has(key)) continue
|
|
1337
|
+
const block = this.#parseBlockKey(key)
|
|
1338
|
+
this.#traceLifecycle(
|
|
1339
|
+
'cleanup-ack',
|
|
1340
|
+
sbn,
|
|
1341
|
+
packetId,
|
|
1342
|
+
block.catalogTrackName,
|
|
1343
|
+
)
|
|
1344
|
+
this.#failBlock(sbn, packetId, false, block.catalogTrackName)
|
|
1345
|
+
}
|
|
1346
|
+
})
|
|
1347
|
+
|
|
1348
|
+
client.onError((msg: string, code?: FecWorkerErrorCode) => {
|
|
1349
|
+
const requestedKind = code ?? msg
|
|
1350
|
+
const kind =
|
|
1351
|
+
this.#workerErrorCounts.has(requestedKind) ||
|
|
1352
|
+
this.#workerErrorCounts.size < WORKER_ERROR_KIND_CAP - 1
|
|
1353
|
+
? requestedKind
|
|
1354
|
+
: WORKER_ERROR_OVERFLOW_KIND
|
|
1355
|
+
const seen = this.#workerErrorCounts.get(kind) ?? 0
|
|
1356
|
+
this.#workerErrorCounts.set(kind, seen + 1)
|
|
1357
|
+
if (seen < WORKER_ERROR_LOG_CAP) {
|
|
1358
|
+
console.warn('[FECManager] Worker error:', msg)
|
|
1359
|
+
} else if (seen === WORKER_ERROR_LOG_CAP) {
|
|
1360
|
+
console.warn(
|
|
1361
|
+
`[FECManager] Worker error (suppressing further copies of ${kind}): ${msg}`,
|
|
1362
|
+
)
|
|
1363
|
+
}
|
|
1364
|
+
})
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/** Called by player when framerate is known (from decoder output). Refines FEC block timeout. */
|
|
1368
|
+
setFramerate(fps: number): void {
|
|
1369
|
+
if (this.#fpsDetected || fps <= 0) return
|
|
1370
|
+
this.#fpsDetected = true
|
|
1371
|
+
this.#fecFps = fps
|
|
1372
|
+
// Only a valid §5.1 ms interleave refines the timeout; a sub-floor value is
|
|
1373
|
+
// a raw frame count and would collapse it (see MIN_INTERLEAVE_DEPTH_MS).
|
|
1374
|
+
if (this.#fecInterleaveDepthMs >= MIN_INTERLEAVE_DEPTH_MS) {
|
|
1375
|
+
this.#fecTimeoutMs = computeFecDeliveryWindowMs({
|
|
1376
|
+
interleaveDepthMs: this.#fecInterleaveDepthMs,
|
|
1377
|
+
sourceSymbols: this.#fecK,
|
|
1378
|
+
symbolSize: this.#fecSymbolSize,
|
|
1379
|
+
mediaUnitsPerSecond: fps,
|
|
1380
|
+
})
|
|
1381
|
+
}
|
|
1382
|
+
for (const [packetId, config] of this.#trackConfigs) {
|
|
1383
|
+
if (config.fps !== undefined || config.interleaveDepthMs < MIN_INTERLEAVE_DEPTH_MS) continue
|
|
1384
|
+
this.#trackConfigs.set(packetId, {
|
|
1385
|
+
...config,
|
|
1386
|
+
fps,
|
|
1387
|
+
timeoutMs: Math.max(
|
|
1388
|
+
this.#fecTimeoutMs,
|
|
1389
|
+
computeFecDeliveryWindowMs({
|
|
1390
|
+
interleaveDepthMs: config.interleaveDepthMs,
|
|
1391
|
+
sourceSymbols: config.k,
|
|
1392
|
+
symbolSize: config.symbolSize,
|
|
1393
|
+
mediaUnitsPerSecond: fps,
|
|
1394
|
+
}),
|
|
1395
|
+
),
|
|
1396
|
+
})
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// -----------------------------------------------------------------------
|
|
1401
|
+
// Symbol feeding (codec-agnostic)
|
|
1402
|
+
// -----------------------------------------------------------------------
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* Feed a source symbol to the WASM FEC decoder.
|
|
1406
|
+
*
|
|
1407
|
+
* Does NOT track block source counts or fire lifecycle events — call
|
|
1408
|
+
* `trackBlock()` separately BEFORE this method (MmtpRouter does this).
|
|
1409
|
+
*
|
|
1410
|
+
* @returns Recovered block data if WASM completed the block, else null.
|
|
1411
|
+
* Caller is responsible for routing recovered data and updating
|
|
1412
|
+
* block completion state via `markBlockCompleted()`.
|
|
1413
|
+
*/
|
|
1414
|
+
feedSourceSymbol(
|
|
1415
|
+
packetId: number,
|
|
1416
|
+
ssId: number,
|
|
1417
|
+
symbolData: Uint8Array,
|
|
1418
|
+
alta?: { auth: Uint8Array; payloadEnd: number },
|
|
1419
|
+
catalogTrackName?: string,
|
|
1420
|
+
): Uint8Array | null {
|
|
1421
|
+
this.#fecStats.sourcePackets++
|
|
1422
|
+
this.#statsForPacket(packetId).sourcePackets++
|
|
1423
|
+
const fecData = this.#normalizeSymbolData(packetId, symbolData)
|
|
1424
|
+
const sbn = Math.floor(ssId / this.kForPacket(packetId))
|
|
1425
|
+
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
1426
|
+
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey)
|
|
1427
|
+
|
|
1428
|
+
// Worker-backed track: dispatch async, return null (recovery via onFrame callback)
|
|
1429
|
+
const workerClient = this.#workerForPacket(packetId, catalogTrackName)
|
|
1430
|
+
if (workerClient && !syncBlockOwner) {
|
|
1431
|
+
if (!this.#claimDecoderBlock(workerClient, sbn, catalogTrackName)) return null
|
|
1432
|
+
// Create clean ArrayBuffer copy — prevents transferring a shared underlying buffer
|
|
1433
|
+
// when symbolData is a view into a larger packet (T-03-06)
|
|
1434
|
+
const buf = fecData.buffer.slice(
|
|
1435
|
+
fecData.byteOffset,
|
|
1436
|
+
fecData.byteOffset + fecData.byteLength,
|
|
1437
|
+
) as ArrayBuffer
|
|
1438
|
+
// Pass the flat SS_ID through to the worker. The WASM decoder derives
|
|
1439
|
+
// SBN/ESI internally per ISO 23008-1 §C.5.2; the WASM add_source
|
|
1440
|
+
// signature is (ss_id, data, timestamp) — pre-computing sbn/esi on this
|
|
1441
|
+
// side produces wrong WASM argument alignment and silently breaks
|
|
1442
|
+
// block assembly.
|
|
1443
|
+
if (alta) {
|
|
1444
|
+
// Copy auth into a fresh ArrayBuffer to transfer zero-copy. The
|
|
1445
|
+
// underlying buffer of the parsed trailer view belongs to the wire
|
|
1446
|
+
// packet, which may be transferred separately or reused.
|
|
1447
|
+
const authBuf = alta.auth.buffer.slice(
|
|
1448
|
+
alta.auth.byteOffset,
|
|
1449
|
+
alta.auth.byteOffset + alta.auth.byteLength,
|
|
1450
|
+
) as ArrayBuffer
|
|
1451
|
+
workerClient.feedSource(ssId, buf, performance.now(), {
|
|
1452
|
+
auth: authBuf,
|
|
1453
|
+
payloadEnd: alta.payloadEnd,
|
|
1454
|
+
})
|
|
1455
|
+
} else {
|
|
1456
|
+
workerClient.feedSource(ssId, buf, performance.now())
|
|
1457
|
+
}
|
|
1458
|
+
return null
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Synchronous WASM path.
|
|
1462
|
+
const entry = this.#decoderForPacket(packetId, catalogTrackName)
|
|
1463
|
+
const client = syncBlockOwner ?? entry?.client
|
|
1464
|
+
if (!client) return null
|
|
1465
|
+
if (!this.#claimDecoderBlock(client, sbn, catalogTrackName)) return null
|
|
1466
|
+
this.#syncDecoderBlockOwners.set(blockKey, client)
|
|
1467
|
+
|
|
1468
|
+
let recovered: Uint8Array | null = null
|
|
1469
|
+
try {
|
|
1470
|
+
recovered = client.add_source(ssId, fecData, performance.now())
|
|
1471
|
+
} catch {
|
|
1472
|
+
// WASM RefCell panic — decoder instance shared with multicast repair path
|
|
1473
|
+
}
|
|
1474
|
+
if (recovered || this.#completedBlocks.has(blockKey)) {
|
|
1475
|
+
this.#syncDecoderBlockOwners.delete(blockKey)
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
return recovered
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Feed a repair symbol to the WASM FEC decoder.
|
|
1483
|
+
*
|
|
1484
|
+
* @returns Recovered block data if repair completed the block, else null.
|
|
1485
|
+
* Caller is responsible for routing recovered data and updating
|
|
1486
|
+
* block completion state via `markBlockCompleted()`.
|
|
1487
|
+
*/
|
|
1488
|
+
feedRepairSymbol(
|
|
1489
|
+
packetId: number,
|
|
1490
|
+
ssStart: number,
|
|
1491
|
+
ssbLength: number,
|
|
1492
|
+
rsId: number,
|
|
1493
|
+
repairData: Uint8Array,
|
|
1494
|
+
catalogTrackName?: string,
|
|
1495
|
+
): Uint8Array | null {
|
|
1496
|
+
this.#fecStats.repairPackets++
|
|
1497
|
+
this.#statsForPacket(packetId).repairPackets++
|
|
1498
|
+
const symbolSize = this.symbolSizeForPacket(packetId)
|
|
1499
|
+
if (symbolSize > 0 && repairData.byteLength !== symbolSize) {
|
|
1500
|
+
this.#repairSymbolSizeMismatchCount++
|
|
1501
|
+
if (this.#repairSymbolSizeMismatchCount <= 5 || this.#repairSymbolSizeMismatchCount % 100 === 0) {
|
|
1502
|
+
console.warn(
|
|
1503
|
+
`[FECManager] Dropping repair symbol with invalid size: got ${repairData.byteLength}B, expected ${symbolSize}B (packetId=${packetId}, ssStart=${ssStart}, ssbLength=${ssbLength}, rsId=${rsId})`,
|
|
1504
|
+
)
|
|
1505
|
+
}
|
|
1506
|
+
return null
|
|
1507
|
+
}
|
|
1508
|
+
const fecData = repairData
|
|
1509
|
+
|
|
1510
|
+
// Repair symbols are block activity: refresh the inactivity fail bound so
|
|
1511
|
+
// a block being actively repaired over MoQ is not failed mid-recovery.
|
|
1512
|
+
// The multicast in-band path already does this at the router; this covers
|
|
1513
|
+
// the MoQ backfill path, which bypasses the router's packet accounting.
|
|
1514
|
+
// ssbLength is SSB_length = K (ISO 23008-1 §C.5.3), so ssStart/ssbLength
|
|
1515
|
+
// is the block number. The legacy feedSymbol shape passes ssbLength=1
|
|
1516
|
+
// with flat symbol ids — its ratio is not a block number, so skip it.
|
|
1517
|
+
if (ssbLength > 1) {
|
|
1518
|
+
this.noteBlockActivity(
|
|
1519
|
+
Math.floor(ssStart / ssbLength),
|
|
1520
|
+
packetId,
|
|
1521
|
+
catalogTrackName,
|
|
1522
|
+
)
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
const sbn = ssbLength > 0 ? Math.floor(ssStart / ssbLength) : 0
|
|
1526
|
+
const blockKey = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
1527
|
+
if (this.#completedBlocks.has(blockKey)) return null
|
|
1528
|
+
const syncBlockOwner = this.#syncDecoderBlockOwners.get(blockKey)
|
|
1529
|
+
let repairIds = this.#blockRepairRsIds.get(blockKey)
|
|
1530
|
+
if (!repairIds) {
|
|
1531
|
+
repairIds = new Set()
|
|
1532
|
+
this.#blockRepairRsIds.set(blockKey, repairIds)
|
|
1533
|
+
}
|
|
1534
|
+
repairIds.add(rsId)
|
|
1535
|
+
if (this.#repairRequestedBlocks.has(blockKey)) {
|
|
1536
|
+
this.#traceLifecycle('repair-arrived', sbn, packetId, catalogTrackName, rsId)
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// Worker-backed track: dispatch async, return null (recovery via onFrame callback)
|
|
1540
|
+
const workerClient = this.#workerForPacket(packetId, catalogTrackName)
|
|
1541
|
+
if (workerClient && !syncBlockOwner) {
|
|
1542
|
+
if (!this.#claimDecoderBlock(workerClient, sbn, catalogTrackName)) return null
|
|
1543
|
+
// Create clean ArrayBuffer copy (T-03-06)
|
|
1544
|
+
const buf = fecData.buffer.slice(
|
|
1545
|
+
fecData.byteOffset,
|
|
1546
|
+
fecData.byteOffset + fecData.byteLength,
|
|
1547
|
+
) as ArrayBuffer
|
|
1548
|
+
// Forward the wire-level Repair FEC Payload ID (ISO 23008-1 §C.5.3)
|
|
1549
|
+
// directly. WASM add_repair signature is
|
|
1550
|
+
// (ss_start, ssb_length, rs_id, data, timestamp) — pre-computing
|
|
1551
|
+
// (sbn, esi) on this side misaligns WASM args and produces 0% recovery.
|
|
1552
|
+
workerClient.feedRepair(ssStart, ssbLength, rsId, buf, performance.now())
|
|
1553
|
+
return null
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// Synchronous WASM path.
|
|
1557
|
+
const entry = this.#decoderForPacket(packetId, catalogTrackName)
|
|
1558
|
+
const client = syncBlockOwner ?? entry?.client
|
|
1559
|
+
if (!client) return null
|
|
1560
|
+
if (!this.#claimDecoderBlock(client, sbn, catalogTrackName)) return null
|
|
1561
|
+
this.#syncDecoderBlockOwners.set(blockKey, client)
|
|
1562
|
+
|
|
1563
|
+
let recovered: Uint8Array | null = null
|
|
1564
|
+
try {
|
|
1565
|
+
recovered = client.add_repair(ssStart, ssbLength, rsId, fecData, performance.now())
|
|
1566
|
+
} catch {
|
|
1567
|
+
// WASM add_repair failure — block will timeout and be marked failed
|
|
1568
|
+
}
|
|
1569
|
+
if (recovered) this.#syncDecoderBlockOwners.delete(blockKey)
|
|
1570
|
+
|
|
1571
|
+
return recovered
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
#routeSyncCleanup(
|
|
1575
|
+
decoder: WasmFecDecoder,
|
|
1576
|
+
sbn: number,
|
|
1577
|
+
packetId: number,
|
|
1578
|
+
): void {
|
|
1579
|
+
let framed: Uint8Array
|
|
1580
|
+
try {
|
|
1581
|
+
framed = decoder.cleanup_blocks_detailed(Uint32Array.of(sbn))
|
|
1582
|
+
} catch (error) {
|
|
1583
|
+
console.warn(`[FECManager] Scoped cleanup failed for SBN ${sbn}:`, error)
|
|
1584
|
+
return
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
const view = new DataView(framed.buffer, framed.byteOffset, framed.byteLength)
|
|
1588
|
+
let offset = 0
|
|
1589
|
+
while (offset < framed.byteLength) {
|
|
1590
|
+
if (framed.byteLength - offset < 8) {
|
|
1591
|
+
console.warn(`[FECManager] Scoped cleanup returned a truncated record for SBN ${sbn}`)
|
|
1592
|
+
return
|
|
1593
|
+
}
|
|
1594
|
+
const fillSbn = view.getUint32(offset, false)
|
|
1595
|
+
const length = view.getUint32(offset + 4, false)
|
|
1596
|
+
offset += 8
|
|
1597
|
+
if (length > framed.byteLength - offset) {
|
|
1598
|
+
console.warn(`[FECManager] Scoped cleanup returned a truncated payload for SBN ${sbn}`)
|
|
1599
|
+
return
|
|
1600
|
+
}
|
|
1601
|
+
const data = framed.slice(offset, offset + length).buffer as ArrayBuffer
|
|
1602
|
+
offset += length
|
|
1603
|
+
this.#fecStats.greenFillFrames++
|
|
1604
|
+
this.#statsForPacket(packetId).greenFillFrames++
|
|
1605
|
+
this.onGreenFill?.(fillSbn, packetId, data)
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* Unified symbol feeder. Dispatches to feedSourceSymbol or feedRepairSymbol.
|
|
1611
|
+
*/
|
|
1612
|
+
feedSymbol(packetId: number, symbolId: number, data: Uint8Array, isRepair: boolean): Uint8Array | null {
|
|
1613
|
+
if (isRepair) {
|
|
1614
|
+
return this.feedRepairSymbol(packetId, symbolId, 1, symbolId, data)
|
|
1615
|
+
}
|
|
1616
|
+
return this.feedSourceSymbol(packetId, symbolId, data)
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// -----------------------------------------------------------------------
|
|
1620
|
+
// Block lifecycle
|
|
1621
|
+
// -----------------------------------------------------------------------
|
|
1622
|
+
|
|
1623
|
+
/**
|
|
1624
|
+
* Track a source symbol for an FEC block. Refreshes the inactivity deadline
|
|
1625
|
+
* on each symbol and marks the block clean when K source symbols arrive.
|
|
1626
|
+
*/
|
|
1627
|
+
trackBlock(
|
|
1628
|
+
sbn: number,
|
|
1629
|
+
packetId: number,
|
|
1630
|
+
catalogTrackName?: string,
|
|
1631
|
+
esi?: number,
|
|
1632
|
+
): void {
|
|
1633
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
1634
|
+
const trackConfig = this.#resolveTrackConfig(packetId)
|
|
1635
|
+
const k = trackConfig.k
|
|
1636
|
+
|
|
1637
|
+
// Skip blocks already recovered — late source symbols must not restart timeouts
|
|
1638
|
+
if (this.#completedBlocks.has(key)) return
|
|
1639
|
+
|
|
1640
|
+
// Count source symbols per block
|
|
1641
|
+
const count = (this.#blockSourceCounts.get(key) ?? 0) + 1
|
|
1642
|
+
this.#blockSourceCounts.set(key, count)
|
|
1643
|
+
if (Number.isInteger(esi) && esi! >= 0 && esi! < k) {
|
|
1644
|
+
let sourceEsis = this.#blockSourceEsis.get(key)
|
|
1645
|
+
if (!sourceEsis) {
|
|
1646
|
+
sourceEsis = new Set()
|
|
1647
|
+
this.#blockSourceEsis.set(key, sourceEsis)
|
|
1648
|
+
}
|
|
1649
|
+
sourceEsis.add(esi!)
|
|
1650
|
+
}
|
|
1651
|
+
if (count === 1) {
|
|
1652
|
+
if (catalogTrackName === undefined) {
|
|
1653
|
+
this.onBlockStarted?.(sbn, packetId)
|
|
1654
|
+
} else {
|
|
1655
|
+
this.onBlockStarted?.(sbn, packetId, catalogTrackName)
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// When K source symbols received, block is clean (no loss, no FEC needed).
|
|
1660
|
+
if (k > 0 && count >= k) {
|
|
1661
|
+
this.#fecStats.blocksClean++
|
|
1662
|
+
this.#statsForPacket(packetId).blocksClean++
|
|
1663
|
+
this.cancelBlockTimeout(sbn, packetId, catalogTrackName)
|
|
1664
|
+
this.#blockSourceCounts.delete(key)
|
|
1665
|
+
this.#blockSourceEsis.delete(key)
|
|
1666
|
+
this.#blockRepairRsIds.delete(key)
|
|
1667
|
+
this.#completedBlocks.add(key)
|
|
1668
|
+
// BLO-3323 L15: track which SBNs reached K source-symbol arrivals so the
|
|
1669
|
+
// worker recovery suppression can distinguish "block completed by source"
|
|
1670
|
+
// (legitimately suppress duplicate recovery) from "block touched by any
|
|
1671
|
+
// prior emit / timeout / external mark" (do NOT suppress recovery).
|
|
1672
|
+
this.#cleanlyCompletedBlocks.add(key)
|
|
1673
|
+
this.#markPathBlockComplete(sbn, packetId, catalogTrackName)
|
|
1674
|
+
if (catalogTrackName === undefined) {
|
|
1675
|
+
this.onBlockComplete?.(sbn, packetId)
|
|
1676
|
+
} else {
|
|
1677
|
+
this.onBlockComplete?.(sbn, packetId, catalogTrackName)
|
|
1678
|
+
}
|
|
1679
|
+
// MmtpRouter feeds the K-th source immediately after this callback.
|
|
1680
|
+
// Defer cleanup so that synchronous feed selects the same decoder; this
|
|
1681
|
+
// also clears blocks whose K-th packet was too large for decoder input.
|
|
1682
|
+
queueMicrotask(() => this.#syncDecoderBlockOwners.delete(key))
|
|
1683
|
+
return
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
this.noteBlockActivity(sbn, packetId, catalogTrackName, true)
|
|
1687
|
+
|
|
1688
|
+
// Source ESI is intentionally not a repair trigger. Multicast delivery is
|
|
1689
|
+
// unordered, so K-1 may precede an interior symbol in a clean block. The
|
|
1690
|
+
// progress-refreshed source horizon above is the earliest loss signal.
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/** Refresh deadlines after accepted block activity. */
|
|
1694
|
+
noteBlockActivity(
|
|
1695
|
+
sbn: number,
|
|
1696
|
+
packetId: number,
|
|
1697
|
+
catalogTrackName?: string,
|
|
1698
|
+
sourceProgress = false,
|
|
1699
|
+
): void {
|
|
1700
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
1701
|
+
|
|
1702
|
+
// Late symbols must not resurrect a block that already completed or failed.
|
|
1703
|
+
if (this.#completedBlocks.has(key)) return
|
|
1704
|
+
// The worker cleanup command is already queued after all symbols that made
|
|
1705
|
+
// the inactivity deadline. Symbols arriving after that boundary are late
|
|
1706
|
+
// and must not arm a second timer before the acknowledgement resolves it.
|
|
1707
|
+
if (this.#pendingWorkerCleanups.has(key)) return
|
|
1708
|
+
|
|
1709
|
+
// #requestRepair appends one bounded grace to the active deadline. Source
|
|
1710
|
+
// or repair symbols may still complete the block, but cannot extend it.
|
|
1711
|
+
if (this.#repairRequestedBlocks.has(key)) return
|
|
1712
|
+
|
|
1713
|
+
const trackConfig = this.#resolveTrackConfig(packetId)
|
|
1714
|
+
const timeoutMs = trackConfig.timeoutMs
|
|
1715
|
+
if (timeoutMs <= 0) return
|
|
1716
|
+
|
|
1717
|
+
// D-28: accepted source progress refreshes the pre-request horizon. There
|
|
1718
|
+
// are at most K such arrivals; repair traffic never refreshes this timer.
|
|
1719
|
+
const repairDeadline = this.#repairDeadlines.get(key)
|
|
1720
|
+
if (
|
|
1721
|
+
this.onRepairNeeded &&
|
|
1722
|
+
(repairDeadline === undefined || sourceProgress)
|
|
1723
|
+
) {
|
|
1724
|
+
if (repairDeadline != null) clearTimeout(repairDeadline)
|
|
1725
|
+
this.#repairDeadlines.set(
|
|
1726
|
+
key,
|
|
1727
|
+
setTimeout(() => {
|
|
1728
|
+
this.#requestRepair(sbn, packetId, catalogTrackName)
|
|
1729
|
+
}, this.#repairDeadlineMs(trackConfig)),
|
|
1730
|
+
)
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
this.#armBlockTimeout(sbn, packetId, catalogTrackName, timeoutMs)
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
#failBlock(
|
|
1737
|
+
sbn: number,
|
|
1738
|
+
packetId: number,
|
|
1739
|
+
countGreenFill: boolean,
|
|
1740
|
+
catalogTrackName?: string,
|
|
1741
|
+
): void {
|
|
1742
|
+
const key = this.#blockKey(sbn, packetId, catalogTrackName)
|
|
1743
|
+
this.#traceLifecycle('failed', sbn, packetId, catalogTrackName)
|
|
1744
|
+
this.#clearRepairDeadline(key)
|
|
1745
|
+
const activeTimer = this.#activeBlocks.get(key)
|
|
1746
|
+
if (activeTimer) {
|
|
1747
|
+
clearTimeout(activeTimer)
|
|
1748
|
+
this.#activeBlocks.delete(key)
|
|
1749
|
+
}
|
|
1750
|
+
this.#activeBlockDeadlinesMs.delete(key)
|
|
1751
|
+
if (this.#repairRequestedBlocks.delete(key)) {
|
|
1752
|
+
if (catalogTrackName === undefined) {
|
|
1753
|
+
this.onRepairResolved?.(sbn, packetId, 'failed')
|
|
1754
|
+
} else {
|
|
1755
|
+
this.onRepairResolved?.(sbn, packetId, 'failed', catalogTrackName)
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
this.#blockSourceCounts.delete(key)
|
|
1759
|
+
this.#blockSourceEsis.delete(key)
|
|
1760
|
+
this.#blockRepairRsIds.delete(key)
|
|
1761
|
+
this.#activeBlockTimeoutOwners.delete(key)
|
|
1762
|
+
this.#syncDecoderBlockOwners.delete(key)
|
|
1763
|
+
this.#completedBlocks.add(key)
|
|
1764
|
+
this.#markPathBlockComplete(sbn, packetId, catalogTrackName)
|
|
1765
|
+
this.#fecStats.blocksFailed++
|
|
1766
|
+
this.#statsForPacket(packetId).blocksFailed++
|
|
1767
|
+
if (countGreenFill) {
|
|
1768
|
+
this.#fecStats.greenFillFrames++
|
|
1769
|
+
this.#statsForPacket(packetId).greenFillFrames++
|
|
1770
|
+
}
|
|
1771
|
+
if (catalogTrackName === undefined) {
|
|
1772
|
+
this.onBlockFailed?.(sbn, packetId)
|
|
1773
|
+
} else {
|
|
1774
|
+
this.onBlockFailed?.(sbn, packetId, catalogTrackName)
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
/** Check whether a block has already been completed (clean or recovered). */
|
|
1779
|
+
isBlockCompleted(sbn: number, packetId?: number, catalogTrackName?: string): boolean {
|
|
1780
|
+
return this.#blockKeysFor(sbn, packetId, catalogTrackName).some((key) => this.#completedBlocks.has(key))
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/** Mark a block as completed externally (e.g., after routing recovered data). */
|
|
1784
|
+
markBlockCompleted(sbn: number, packetId?: number, catalogTrackName?: string): void {
|
|
1785
|
+
for (const key of this.#blockKeysFor(sbn, packetId, catalogTrackName)) {
|
|
1786
|
+
this.#completedBlocks.add(key)
|
|
1787
|
+
this.#pendingWorkerCleanups.delete(key)
|
|
1788
|
+
this.#pendingWorkerCleanupClients.delete(key)
|
|
1789
|
+
const block = this.#parseBlockKey(key)
|
|
1790
|
+
this.#markPathBlockComplete(block.sbn, block.packetId, block.catalogTrackName)
|
|
1791
|
+
this.#clearRepairDeadline(key)
|
|
1792
|
+
if (this.#repairRequestedBlocks.delete(key)) {
|
|
1793
|
+
if (block.catalogTrackName === undefined) {
|
|
1794
|
+
this.onRepairResolved?.(block.sbn, block.packetId, 'completed')
|
|
1795
|
+
} else {
|
|
1796
|
+
this.onRepairResolved?.(
|
|
1797
|
+
block.sbn,
|
|
1798
|
+
block.packetId,
|
|
1799
|
+
'completed',
|
|
1800
|
+
block.catalogTrackName,
|
|
1801
|
+
)
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
this.#blockSourceCounts.delete(key)
|
|
1805
|
+
this.#blockSourceEsis.delete(key)
|
|
1806
|
+
this.#blockRepairRsIds.delete(key)
|
|
1807
|
+
const timer = this.#activeBlocks.get(key)
|
|
1808
|
+
if (timer) {
|
|
1809
|
+
clearTimeout(timer)
|
|
1810
|
+
this.#activeBlocks.delete(key)
|
|
1811
|
+
}
|
|
1812
|
+
this.#activeBlockDeadlinesMs.delete(key)
|
|
1813
|
+
this.#activeBlockTimeoutOwners.delete(key)
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/** Increment blocksRecovered stat (called by MmtpRouter after routing recovery). */
|
|
1818
|
+
countRecovery(packetId?: number): void {
|
|
1819
|
+
this.#fecStats.blocksRecovered++
|
|
1820
|
+
if (packetId != null) this.#statsForPacket(packetId).blocksRecovered++
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/**
|
|
1824
|
+
* Strict delivery provenance for one FEC block.
|
|
1825
|
+
*
|
|
1826
|
+
* A recovered symbol is a function of every symbol that entered its block,
|
|
1827
|
+
* so it can only be attributed to multicast when NO symbol in that block
|
|
1828
|
+
* arrived over MoQ. Repair symbols routinely arrive over reliable QUIC
|
|
1829
|
+
* (`MmtpRouter.feedRepairPacket`), so a block that recovered with MoQ help
|
|
1830
|
+
* produced bytes that never crossed a multicast socket.
|
|
1831
|
+
*
|
|
1832
|
+
* An untracked block returns 'unknown' rather than a guess: absence of
|
|
1833
|
+
* evidence is not evidence of multicast.
|
|
1834
|
+
*/
|
|
1835
|
+
blockDeliveryProvenance(
|
|
1836
|
+
sbn: number,
|
|
1837
|
+
packetId: number,
|
|
1838
|
+
catalogTrackName?: string,
|
|
1839
|
+
): MmtpDeliveryProvenance {
|
|
1840
|
+
const counts = this.blockPathCount(sbn, packetId, catalogTrackName)
|
|
1841
|
+
if (!counts) return 'unknown'
|
|
1842
|
+
if (counts.moqSymbols > 0) return 'mixed'
|
|
1843
|
+
if (counts.multicastSymbols > 0) return 'multicast'
|
|
1844
|
+
return 'unknown'
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/** Per-path symbol counts for one block (D-28 gate logging), if tracked. */
|
|
1848
|
+
blockPathCount(
|
|
1849
|
+
sbn: number,
|
|
1850
|
+
packetId: number,
|
|
1851
|
+
catalogTrackName?: string,
|
|
1852
|
+
): FecBlockPathCount | null {
|
|
1853
|
+
return this.#blockPathCounts.get(
|
|
1854
|
+
this.#blockKey(sbn, packetId, catalogTrackName),
|
|
1855
|
+
) ?? null
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/** Cancel block timeout (block completed via clean reception or FEC recovery). */
|
|
1859
|
+
cancelBlockTimeout(sbn: number, packetId?: number, catalogTrackName?: string): void {
|
|
1860
|
+
for (const key of this.#blockKeysFor(sbn, packetId, catalogTrackName)) {
|
|
1861
|
+
this.#completedBlocks.add(key)
|
|
1862
|
+
this.#pendingWorkerCleanups.delete(key)
|
|
1863
|
+
this.#pendingWorkerCleanupClients.delete(key)
|
|
1864
|
+
const block = this.#parseBlockKey(key)
|
|
1865
|
+
this.#markPathBlockComplete(block.sbn, block.packetId, block.catalogTrackName)
|
|
1866
|
+
this.#clearRepairDeadline(key)
|
|
1867
|
+
if (this.#repairRequestedBlocks.delete(key)) {
|
|
1868
|
+
if (block.catalogTrackName === undefined) {
|
|
1869
|
+
this.onRepairResolved?.(block.sbn, block.packetId, 'completed')
|
|
1870
|
+
} else {
|
|
1871
|
+
this.onRepairResolved?.(
|
|
1872
|
+
block.sbn,
|
|
1873
|
+
block.packetId,
|
|
1874
|
+
'completed',
|
|
1875
|
+
block.catalogTrackName,
|
|
1876
|
+
)
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
this.#blockSourceCounts.delete(key)
|
|
1880
|
+
this.#blockSourceEsis.delete(key)
|
|
1881
|
+
this.#blockRepairRsIds.delete(key)
|
|
1882
|
+
const timer = this.#activeBlocks.get(key)
|
|
1883
|
+
if (timer) {
|
|
1884
|
+
clearTimeout(timer)
|
|
1885
|
+
this.#activeBlocks.delete(key)
|
|
1886
|
+
}
|
|
1887
|
+
this.#activeBlockDeadlinesMs.delete(key)
|
|
1888
|
+
this.#activeBlockTimeoutOwners.delete(key)
|
|
1889
|
+
}
|
|
1890
|
+
// Trim old entries to prevent unbounded growth (keep last ~500 blocks)
|
|
1891
|
+
if (this.#completedBlocks.size > 1000) {
|
|
1892
|
+
const iter = this.#completedBlocks.values()
|
|
1893
|
+
for (let i = 0; i < 500; i++) iter.next()
|
|
1894
|
+
// Sets iterate in insertion order — first 500 are oldest
|
|
1895
|
+
const keep = new Set<string>()
|
|
1896
|
+
for (const v of iter) keep.add(v)
|
|
1897
|
+
this.#completedBlocks = keep
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// -----------------------------------------------------------------------
|
|
1902
|
+
// Cleanup
|
|
1903
|
+
// -----------------------------------------------------------------------
|
|
1904
|
+
|
|
1905
|
+
dispose(): void {
|
|
1906
|
+
for (const timer of this.#activeBlocks.values()) clearTimeout(timer)
|
|
1907
|
+
this.#activeBlocks.clear()
|
|
1908
|
+
this.#activeBlockDeadlinesMs.clear()
|
|
1909
|
+
this.#activeBlockTimeoutOwners.clear()
|
|
1910
|
+
this.#pendingWorkerCleanups.clear()
|
|
1911
|
+
this.#pendingWorkerCleanupClients.clear()
|
|
1912
|
+
for (const timer of this.#repairDeadlines.values()) {
|
|
1913
|
+
if (timer != null) clearTimeout(timer)
|
|
1914
|
+
}
|
|
1915
|
+
this.#repairDeadlines.clear()
|
|
1916
|
+
this.#blockSourceCounts.clear()
|
|
1917
|
+
this.#blockSourceEsis.clear()
|
|
1918
|
+
this.#blockRepairRsIds.clear()
|
|
1919
|
+
this.#completedBlocks.clear()
|
|
1920
|
+
this.#cleanlyCompletedBlocks.clear()
|
|
1921
|
+
this.#repairRequestedBlocks.clear()
|
|
1922
|
+
this.#workerRecoveredBlocks.clear()
|
|
1923
|
+
this.#syncDecoderBlockOwners.clear()
|
|
1924
|
+
this.#startupAlignedPackets.clear()
|
|
1925
|
+
this.#startupPartialBlocks.clear()
|
|
1926
|
+
this.#startupRequireRepairReady = false
|
|
1927
|
+
this.#startupRepairReadyPackets.clear()
|
|
1928
|
+
this.#startupRepairBoundaryCounts.clear()
|
|
1929
|
+
this.#startupRepairFailOpenPackets.clear()
|
|
1930
|
+
this.#seenSymbols.clear()
|
|
1931
|
+
this.#blockPathCounts.clear()
|
|
1932
|
+
this.#blockCatalogTrackNames.clear()
|
|
1933
|
+
this.#trackConfigs.clear()
|
|
1934
|
+
this.#fecDecoders.clear()
|
|
1935
|
+
// Dispose Worker clients (T-03-07: prevent Worker leak)
|
|
1936
|
+
for (const client of new Set(this.#workerClients.values())) client.dispose()
|
|
1937
|
+
this.#workerClients.clear()
|
|
1938
|
+
}
|
|
1939
|
+
}
|