@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,1185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MediaRouter — source arbitration for MoQ ↔ multicast switching.
|
|
3
|
+
*
|
|
4
|
+
* Both moq-lib and moqtail wire their MoQ and multicast frame outputs
|
|
5
|
+
* through this router. The router decides which source reaches the decoder:
|
|
6
|
+
*
|
|
7
|
+
* - MoQ frames pass through while multicast is inactive.
|
|
8
|
+
* - Multicast video frames are suppressed until the first keyframe.
|
|
9
|
+
* - Once multicast has both a video keyframe and audio, takeover is requested.
|
|
10
|
+
* - MoQ is suppressed only after the source-switch callback completes.
|
|
11
|
+
* - failbackToMoq() reverses the switch immediately.
|
|
12
|
+
*
|
|
13
|
+
* Per-track setTrackSource() allows individual tracks to be wired to a
|
|
14
|
+
* specific transport source (video=multicast, audio=moq, overlay=amt) without
|
|
15
|
+
* changing the binary MoQ↔multicast switching for other tracks.
|
|
16
|
+
*
|
|
17
|
+
* notifyGroupBoundary() is an observer hook: callers report the
|
|
18
|
+
* packet-stamped groupId + media time for each group, and the router
|
|
19
|
+
* cross-checks against the shared `align.ts` formula. Drift is emitted via
|
|
20
|
+
* onGroupDrift so the player can bump the CMCD `x-bc-switch-drift-us` /
|
|
21
|
+
* `x-bc-switch-miscount` counters. Observation only — does NOT alter routing.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { groupIdForTicks, driftCheck } from './align.js'
|
|
25
|
+
import { computeFecDeliveryWindowMs } from './track-session.js'
|
|
26
|
+
|
|
27
|
+
export type FrameMeta = {
|
|
28
|
+
timestamp: number
|
|
29
|
+
keyframe: boolean
|
|
30
|
+
packetId?: number
|
|
31
|
+
packetSequenceNumber?: number
|
|
32
|
+
source?: 'moq' | 'multicast' | 'repair'
|
|
33
|
+
recovered?: boolean
|
|
34
|
+
sourceDiscontinuity?: boolean
|
|
35
|
+
}
|
|
36
|
+
export type FrameKind = 'video' | 'audio'
|
|
37
|
+
export type SourceKind = 'moq' | 'multicast'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Tick-space timing for one group boundary — the exact, authoritative form.
|
|
41
|
+
*
|
|
42
|
+
* These are the integers the catalog already carries (`groupDurationTicks`,
|
|
43
|
+
* `timescale`) plus the packet's raw timestamp in the same units. Passing
|
|
44
|
+
* them straight through keeps the whole drift check in integer math.
|
|
45
|
+
*
|
|
46
|
+
* Prefer this over the seconds overload: `groupDurationTicks / timescale`
|
|
47
|
+
* is not exactly representable for the common non-integer-second cadences
|
|
48
|
+
* (33333@90k, 1001@30k, 1024@44.1k), and the seconds path has to quantize
|
|
49
|
+
* to whole nanoseconds — which shifts `expected` by a whole group from the
|
|
50
|
+
* second boundary onward and reports drift on a perfectly aligned stream.
|
|
51
|
+
*/
|
|
52
|
+
export interface GroupBoundaryTicks {
|
|
53
|
+
/** Packet media timestamp in `timescale` units. Integer; negatives clamp to 0. */
|
|
54
|
+
readonly mediaTimeTicks: number
|
|
55
|
+
/** Group duration in `timescale` units. Positive integer. */
|
|
56
|
+
readonly groupDurationTicks: number
|
|
57
|
+
/**
|
|
58
|
+
* Catalog timescale in Hz. Positive integer — **validated, and a hard gate**:
|
|
59
|
+
* a non-integer or non-positive value skips the drift check entirely rather
|
|
60
|
+
* than reporting against it. It does not feed the groupId computation (that
|
|
61
|
+
* is `mediaTimeTicks / groupDurationTicks` alone), but it does derive the
|
|
62
|
+
* `mediaTimeSec` / `groupDurationSec` on every GroupDriftEvent — so letting a
|
|
63
|
+
* bad value through would push NaN/Infinity into CMCD `x-bc-switch-drift-us`,
|
|
64
|
+
* which is worse than skipping.
|
|
65
|
+
*/
|
|
66
|
+
readonly timescale: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Payload emitted by MediaRouter.onGroupDrift when a packet-stamped groupId
|
|
70
|
+
* disagrees with the shared `align.ts` formula. */
|
|
71
|
+
export interface GroupDriftEvent {
|
|
72
|
+
trackName: string
|
|
73
|
+
expectedGroupId: number
|
|
74
|
+
actualGroupId: number
|
|
75
|
+
/** `actual - expected` (positive = ahead, negative = behind). */
|
|
76
|
+
deltaGroups: number
|
|
77
|
+
/** Seconds view, kept for CMCD consumers. Derived when the caller used ticks. */
|
|
78
|
+
mediaTimeSec: number
|
|
79
|
+
groupDurationSec: number
|
|
80
|
+
/** Exact tick-space inputs. Present only when the caller used the ticks form. */
|
|
81
|
+
timing?: GroupBoundaryTicks
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Normalized output of the two notifyGroupBoundary input forms. */
|
|
85
|
+
type ResolvedBoundary = {
|
|
86
|
+
readonly expected: number
|
|
87
|
+
readonly mediaTimeSec: number
|
|
88
|
+
readonly groupDurationSec: number
|
|
89
|
+
readonly timing?: GroupBoundaryTicks
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Distinct failure modes on the notifyGroupBoundary drift path. Each gets its
|
|
94
|
+
* own bounded warn budget (see `#skipWithWarning`), so a stream that trips one
|
|
95
|
+
* cannot permanently silence the diagnostic for a genuinely different one.
|
|
96
|
+
*
|
|
97
|
+
* Deliberately a closed union rather than `string`: the tag is the dedup key,
|
|
98
|
+
* and a typo would silently mint a second budget for an existing reason.
|
|
99
|
+
*/
|
|
100
|
+
type GroupAlignSkipReason =
|
|
101
|
+
| 'invalid-tick-inputs'
|
|
102
|
+
// Currently UNREACHABLE, kept as defence in depth — see the catch in
|
|
103
|
+
// #resolveTicks. Do not write a test chasing it.
|
|
104
|
+
| 'ticks-formula-threw'
|
|
105
|
+
| 'invalid-seconds-inputs'
|
|
106
|
+
| 'sub-nanosecond-duration'
|
|
107
|
+
| 'media-time-overflow'
|
|
108
|
+
| 'seconds-formula-threw'
|
|
109
|
+
| 'callback-threw'
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Warns allowed per `GroupAlignSkipReason` for the lifetime of a router.
|
|
113
|
+
*
|
|
114
|
+
* Not a single flat number (PCL-2211, #329 review). The six resolver reasons
|
|
115
|
+
* are closed, deterministic conditions: once you have seen
|
|
116
|
+
* `sub-nanosecond-duration` on this router, the second occurrence carries no
|
|
117
|
+
* new information, so one slot is the right budget and repeated malformed
|
|
118
|
+
* catalog input stays silent.
|
|
119
|
+
*
|
|
120
|
+
* `callback-threw` is the opposite. It wraps arbitrary downstream consumer
|
|
121
|
+
* code and interpolates `err.message` *because the error varies*. Under one
|
|
122
|
+
* slot, a consumer that throws at startup permanently hides an unrelated
|
|
123
|
+
* exception later — including one from a *replacement* consumer registered
|
|
124
|
+
* through `onGroupDrift()`, which is a public setter with no budget reset.
|
|
125
|
+
* That is the same argument `#skipWithWarning` makes against a single shared
|
|
126
|
+
* boolean, one level down. A small cap bounds the flood, at the cost of
|
|
127
|
+
* silencing distinct errors past the cap: three throws at startup exhaust
|
|
128
|
+
* `callback-threw`, and a replacement consumer's unrelated exception is then
|
|
129
|
+
* silenced permanently — the same failure as the one-slot budget, moved from
|
|
130
|
+
* the 1st throw to the 4th. That residual is the inherent price of a bounded
|
|
131
|
+
* budget and is accepted deliberately; the alternative is an unbounded warn.
|
|
132
|
+
*
|
|
133
|
+
* Deliberately NOT a reset on `onGroupDrift()` re-registration: that setter is
|
|
134
|
+
* public and unthrottled, so a caller re-registering per boundary would
|
|
135
|
+
* refresh the budget every time and restore the exact flood this prevents.
|
|
136
|
+
*
|
|
137
|
+
* A total `Record` rather than a lookup with a default: the union is closed, so
|
|
138
|
+
* a new reason that forgets to declare its budget is a compile error — the
|
|
139
|
+
* same typo-safety rationale that makes the union closed in the first place.
|
|
140
|
+
*/
|
|
141
|
+
const GROUP_ALIGN_WARN_BUDGET: Record<GroupAlignSkipReason, number> = {
|
|
142
|
+
'invalid-tick-inputs': 1,
|
|
143
|
+
'ticks-formula-threw': 1,
|
|
144
|
+
'invalid-seconds-inputs': 1,
|
|
145
|
+
'sub-nanosecond-duration': 1,
|
|
146
|
+
'media-time-overflow': 1,
|
|
147
|
+
'seconds-formula-threw': 1,
|
|
148
|
+
'callback-threw': 3,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type FrameCb = (data: Uint8Array, meta: FrameMeta) => void
|
|
152
|
+
type SwitchCb = (source: SourceKind) => void | Promise<void>
|
|
153
|
+
|
|
154
|
+
export interface MediaRouterOptions {
|
|
155
|
+
/** Set false for video-only streams. Default: true. */
|
|
156
|
+
requireAudio?: boolean
|
|
157
|
+
/**
|
|
158
|
+
* Audio fallback timeout (ms): force multicast activation if video keyframe
|
|
159
|
+
* arrived but audio hasn't within this window. Catalog-derived via
|
|
160
|
+
* computeFecDeliveryWindowMs (the single FEC recovery-window source of truth).
|
|
161
|
+
* Omit to disable the independent audio-fallback timer (no magic default).
|
|
162
|
+
*/
|
|
163
|
+
audioFallbackMs?: number
|
|
164
|
+
/**
|
|
165
|
+
* Number of binary-path frames to allow through during a crossfade transition.
|
|
166
|
+
* Derive from FEC block size (e.g., ceil(K / groupsPerBlock)). Default: 10.
|
|
167
|
+
*/
|
|
168
|
+
crossfadeFrames?: number
|
|
169
|
+
/**
|
|
170
|
+
* Max pre-boundary overlay frames to buffer before dropping.
|
|
171
|
+
* Derive from FEC config (e.g., ceil(K * 1.5)). Default: 128.
|
|
172
|
+
*/
|
|
173
|
+
maxOverlayBuffer?: number
|
|
174
|
+
/**
|
|
175
|
+
* Max concurrent in-flight repairs. Prevents relay SUBSCRIBE flood (T-13-01 mitigation).
|
|
176
|
+
* Default: 2.
|
|
177
|
+
*/
|
|
178
|
+
maxRepairInFlight?: number
|
|
179
|
+
/**
|
|
180
|
+
* Allow multicast takeover from a delta frame after the player has confirmed
|
|
181
|
+
* that MoQ already seeded the same decoder timeline with a video keyframe.
|
|
182
|
+
* Default false: decoder-reset paths still need multicast to provide a RAP.
|
|
183
|
+
*/
|
|
184
|
+
allowDeltaMulticastTakeoverAfterMoqKeyframe?: boolean
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A per-track source that emits frames independently of the binary MoQ/multicast path.
|
|
189
|
+
* Used by setTrackSource() to wire specific tracks to specific transport sources.
|
|
190
|
+
*/
|
|
191
|
+
export interface TrackSource {
|
|
192
|
+
name: string
|
|
193
|
+
/**
|
|
194
|
+
* Register a frame callback. Returns an unsubscribe function.
|
|
195
|
+
* MediaRouter calls unsubscribe() when clearTrackSource() is called.
|
|
196
|
+
*/
|
|
197
|
+
onFrame(cb: (data: Uint8Array, meta: FrameMeta) => void): () => void
|
|
198
|
+
/**
|
|
199
|
+
* Optional error callback for when the source fails.
|
|
200
|
+
* MediaRouter uses this to trigger failback to binary switching for this track.
|
|
201
|
+
*/
|
|
202
|
+
onError?(cb: () => void): () => void
|
|
203
|
+
/** Optional dispose hook called when clearTrackSource() is called. */
|
|
204
|
+
dispose?(): void
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Thrown when setTrackSource() is called for a track that already has a source
|
|
209
|
+
* without an intervening clearTrackSource(). Prevents silent concurrent-assignment
|
|
210
|
+
* races (T-02-16 mitigation).
|
|
211
|
+
*/
|
|
212
|
+
export class TrackSourceConflictError extends Error {
|
|
213
|
+
constructor(public readonly trackName: string) {
|
|
214
|
+
super(`Track source already assigned for '${trackName}'; call clearTrackSource first`)
|
|
215
|
+
this.name = 'TrackSourceConflictError'
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Migration policy for setTrackSource() transport switching.
|
|
221
|
+
*
|
|
222
|
+
* - 'crossfade': Overlap old (binary) and new source briefly (CROSSFADE_WINDOW_FRAMES),
|
|
223
|
+
* then suppress the binary path. Default for audio.
|
|
224
|
+
* - 'drop': Cut to new source immediately. Binary path suppressed at once.
|
|
225
|
+
* Suitable for latency-critical tracks.
|
|
226
|
+
* - 'wait': Defer migration until first FEC block boundary (same as overlay=true).
|
|
227
|
+
* Default for video — avoids decoder reset mid-block.
|
|
228
|
+
*/
|
|
229
|
+
export type MigrationPolicy = 'crossfade' | 'drop' | 'wait'
|
|
230
|
+
|
|
231
|
+
/** Default crossfade window — override via MediaRouterOptions.crossfadeFrames. */
|
|
232
|
+
const DEFAULT_CROSSFADE_FRAMES = 10
|
|
233
|
+
|
|
234
|
+
/** Options passed to setTrackSource(). */
|
|
235
|
+
export interface SetTrackSourceOptions {
|
|
236
|
+
/**
|
|
237
|
+
* If true, this source is an overlay track that must wait for the first FEC
|
|
238
|
+
* block boundary signal (via notifyFecBlockBoundary()) before emitting frames.
|
|
239
|
+
* Frames arriving before the first boundary are buffered up to MAX_OVERLAY_BUFFER.
|
|
240
|
+
*
|
|
241
|
+
* Backwards compat: equivalent to policy: 'wait'.
|
|
242
|
+
*/
|
|
243
|
+
overlay?: boolean
|
|
244
|
+
/**
|
|
245
|
+
* Migration policy for the transport switch. Overrides `overlay` when provided.
|
|
246
|
+
* See MigrationPolicy for semantics.
|
|
247
|
+
*/
|
|
248
|
+
policy?: MigrationPolicy
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Default overlay buffer cap — override via MediaRouterOptions.maxOverlayBuffer. */
|
|
252
|
+
const DEFAULT_MAX_OVERLAY_BUFFER = 128
|
|
253
|
+
|
|
254
|
+
/** Internal per-track source record. */
|
|
255
|
+
interface TrackSourceRecord {
|
|
256
|
+
source: TrackSource
|
|
257
|
+
unsubscribeFrame: () => void
|
|
258
|
+
unsubscribeError?: () => void
|
|
259
|
+
emitCount: number
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export class MediaRouter {
|
|
263
|
+
#active: SourceKind = 'moq'
|
|
264
|
+
#multicastVideoKeyed = false
|
|
265
|
+
#multicastAudioReceived = false
|
|
266
|
+
#moqVideoKeyframeEmitted = false
|
|
267
|
+
#requireAudio: boolean
|
|
268
|
+
#suppressed = false // true after multicast confirmed — suppresses MoQ
|
|
269
|
+
#audioFallbackTimer: ReturnType<typeof setTimeout> | null = null
|
|
270
|
+
|
|
271
|
+
// Configurable constants (from catalog/FEC config, not magic numbers)
|
|
272
|
+
readonly #audioFallbackMs: number
|
|
273
|
+
readonly #crossfadeFrames: number
|
|
274
|
+
readonly #maxOverlayBuffer: number
|
|
275
|
+
readonly #allowDeltaMulticastTakeoverAfterMoqKeyframe: boolean
|
|
276
|
+
|
|
277
|
+
// ── Keyframe repair state ────────────────────────────────────────
|
|
278
|
+
/** Groups currently being repaired — idempotent guard per Pitfall 2 (T-13-01 mitigation) */
|
|
279
|
+
#repairInFlight = new Set<number>()
|
|
280
|
+
/** Max concurrent repairs — prevents relay SUBSCRIBE flood (T-13-01 mitigation) */
|
|
281
|
+
readonly #maxRepairInFlight: number
|
|
282
|
+
/** Callback to request MoQ subscribe for a specific group (wired by player) */
|
|
283
|
+
#onRepairRequest: ((expectedGroupId: number, trackName: string) => void) | null = null
|
|
284
|
+
|
|
285
|
+
#onVideoFrame: FrameCb | null = null
|
|
286
|
+
#onAudioFrame: FrameCb | null = null
|
|
287
|
+
#onOverlayFrame: FrameCb | null = null
|
|
288
|
+
#onSourceSwitch: SwitchCb | null = null
|
|
289
|
+
#sourceSwitchReady: Promise<void> | null = null
|
|
290
|
+
#sourceSwitchToken = 0
|
|
291
|
+
#pendingMulticastToken: number | null = null
|
|
292
|
+
#pendingMulticastVideo: { data: Uint8Array; meta: FrameMeta } | null = null
|
|
293
|
+
#pendingMulticastAudio: { data: Uint8Array; meta: FrameMeta } | null = null
|
|
294
|
+
|
|
295
|
+
// Per-track source state
|
|
296
|
+
#trackSources = new Map<string, TrackSourceRecord>()
|
|
297
|
+
|
|
298
|
+
// Track type registry: trackName → { type, source }.
|
|
299
|
+
// Init data / inline signaling is authoritative until catalog updates.
|
|
300
|
+
// Priority: 'init' > 'catalog' > 'name' (backward compat fallback).
|
|
301
|
+
#trackTypes = new Map<string, { type: FrameKind; source: 'catalog' | 'init' }>()
|
|
302
|
+
|
|
303
|
+
// Crossfade state: maps trackName → remaining binary-path frames allowed during window
|
|
304
|
+
#crossfadeCounters = new Map<string, number>()
|
|
305
|
+
|
|
306
|
+
// Overlay alignment state
|
|
307
|
+
#overlayBoundaryReached = false
|
|
308
|
+
#overlayBuffer: Array<{ data: Uint8Array; meta: FrameMeta }> = []
|
|
309
|
+
|
|
310
|
+
// ── Phase 14: deltaActive state machine (D-11..D-14) ─────────────
|
|
311
|
+
// No timers — state transitions driven entirely by notifyDeltaBlock() callbacks.
|
|
312
|
+
// All thresholds derived from catalog fields or throw at configureDelta() time.
|
|
313
|
+
#deltaConfigured = false
|
|
314
|
+
#deltaActive: boolean = true
|
|
315
|
+
#deltaWindow: boolean[] = [] // ring buffer: true = block failed
|
|
316
|
+
#deltaWindowSize: number = 0
|
|
317
|
+
#deltaFailThresholdRatio: number = 0 // P/K
|
|
318
|
+
#deltaRecoverThresholdRatio: number = 0 // (P/K)/2 — hysteresis (D-13)
|
|
319
|
+
#deltaProbeIntervalMs: number = 0
|
|
320
|
+
#lastProbeAt: number = 0
|
|
321
|
+
#onDeltaFallbackCb: (() => void) | null = null
|
|
322
|
+
#onDeltaRecoverCb: (() => void) | null = null
|
|
323
|
+
|
|
324
|
+
constructor(options?: MediaRouterOptions) {
|
|
325
|
+
this.#requireAudio = options?.requireAudio ?? true
|
|
326
|
+
// Catalog-derived — the caller passes an audioFallbackMs computed from the
|
|
327
|
+
// FEC recovery window (computeFecDeliveryWindowMs). No magic default: when
|
|
328
|
+
// absent, the independent audio-fallback timer is disabled (0) and
|
|
329
|
+
// activation waits for real audio arrival.
|
|
330
|
+
this.#audioFallbackMs = options?.audioFallbackMs ?? 0
|
|
331
|
+
this.#crossfadeFrames = options?.crossfadeFrames ?? DEFAULT_CROSSFADE_FRAMES
|
|
332
|
+
this.#maxOverlayBuffer = options?.maxOverlayBuffer ?? DEFAULT_MAX_OVERLAY_BUFFER
|
|
333
|
+
this.#maxRepairInFlight = options?.maxRepairInFlight ?? 2
|
|
334
|
+
this.#allowDeltaMulticastTakeoverAfterMoqKeyframe =
|
|
335
|
+
options?.allowDeltaMulticastTakeoverAfterMoqKeyframe ?? false
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
onVideoFrame(cb: FrameCb) { this.#onVideoFrame = cb }
|
|
339
|
+
onAudioFrame(cb: FrameCb) { this.#onAudioFrame = cb }
|
|
340
|
+
onOverlayFrame(cb: FrameCb) { this.#onOverlayFrame = cb }
|
|
341
|
+
onSourceSwitch(cb: SwitchCb) { this.#onSourceSwitch = cb }
|
|
342
|
+
|
|
343
|
+
// ── Group-alignment observer (align.ts formula) ──────────────────
|
|
344
|
+
// Observation only — does NOT alter routing. Producers are wired in
|
|
345
|
+
// follow-up PRs; when they land they will call
|
|
346
|
+
// CmcdProducer.incrementSwitchMiscount() + emitSwitchDrift() from the
|
|
347
|
+
// onGroupDrift callback so divergence between packet-stamped groupIds
|
|
348
|
+
// and the shared formula surfaces on `x-bc-switch-drift-us` /
|
|
349
|
+
// `x-bc-switch-miscount`. Canonical Go impl: Blockcast/multicast
|
|
350
|
+
// cmd/caddy/sender/group_align.go (commit 104eeff). Canonical Rust impl:
|
|
351
|
+
// moqtail/moqtail apps/relay/src/server/abr.rs (branch
|
|
352
|
+
// experimental/server-side-abr, commit 39accbf).
|
|
353
|
+
#onGroupDrift: ((evt: GroupDriftEvent) => void) | null = null
|
|
354
|
+
#groupAlignWarnCounts = new Map<GroupAlignSkipReason, number>()
|
|
355
|
+
onGroupDrift(cb: (evt: GroupDriftEvent) => void): void {
|
|
356
|
+
this.#onGroupDrift = cb
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Report a group boundary observed on an incoming track. Cross-checks the
|
|
361
|
+
* packet-stamped groupId against the shared `align.ts` formula and fires
|
|
362
|
+
* the onGroupDrift callback when they disagree.
|
|
363
|
+
*
|
|
364
|
+
* Two input forms:
|
|
365
|
+
* - **Ticks (preferred).** `notifyGroupBoundary(track, id, { mediaTimeTicks,
|
|
366
|
+
* groupDurationTicks, timescale })` — pure integer math via
|
|
367
|
+
* `align.groupIdForTicks`, no rounding anywhere.
|
|
368
|
+
* - **Seconds (deprecated).** The 4-argument form. Quantizes both operands
|
|
369
|
+
* to whole nanoseconds, which is exact only when `groupDurationSec` is a
|
|
370
|
+
* whole number of nanoseconds. It is not for 33333@90k, 1001@30k or
|
|
371
|
+
* 1024@44.1k, and there `expected` diverges by a whole group from the
|
|
372
|
+
* second boundary onward — a false drift event on every subsequent
|
|
373
|
+
* group, and a systematically wrong CMCD `x-bc-switch-drift-us`.
|
|
374
|
+
*
|
|
375
|
+
* No-op when no onGroupDrift callback is registered (avoid unnecessary work
|
|
376
|
+
* on hot paths).
|
|
377
|
+
*
|
|
378
|
+
* Hot-path robustness (does NOT throw on bad inputs — the ingest stream
|
|
379
|
+
* must not be torn down by a malformed catalog or a disposed producer):
|
|
380
|
+
* - Invalid duration (NaN / <=0) or non-finite media time → warn + early
|
|
381
|
+
* return (same convention as audioFallbackMs).
|
|
382
|
+
* - Any throw out of the align formula (e.g. the safe-integer overflow
|
|
383
|
+
* guard at ~104 days of media time on the seconds path) is caught +
|
|
384
|
+
* logged so an unusually long-running stream cannot tear down ingest.
|
|
385
|
+
* - Callback throws are caught + logged; ingest continues.
|
|
386
|
+
* - Skip warnings are coalesced **per failure mode** for the lifetime
|
|
387
|
+
* of the router (never reset, including across `dispose()`), so a hot
|
|
388
|
+
* path cannot flood the log while a genuinely new failure mode still
|
|
389
|
+
* gets heard. Deterministic modes get one line; a throwing consumer
|
|
390
|
+
* callback gets a small budget because its error varies. See
|
|
391
|
+
* `#skipWithWarning` and `GROUP_ALIGN_WARN_BUDGET`.
|
|
392
|
+
*
|
|
393
|
+
* Init-track filter: media time === 0 with a non-zero stamped groupId is
|
|
394
|
+
* treated as an init/ambient track emitting pre-PTS-zero packets and is
|
|
395
|
+
* skipped (matches Go's base-clamp intent without producing spurious
|
|
396
|
+
* drift).
|
|
397
|
+
*
|
|
398
|
+
* NOTE: do NOT call from the repair path (feedRepair). Repair frames carry
|
|
399
|
+
* the groupId they are recovering for, not a freshly-stamped one.
|
|
400
|
+
*/
|
|
401
|
+
notifyGroupBoundary(
|
|
402
|
+
trackName: string,
|
|
403
|
+
packetGroupId: number,
|
|
404
|
+
timing: GroupBoundaryTicks,
|
|
405
|
+
): void
|
|
406
|
+
/**
|
|
407
|
+
* @deprecated Pass a {@link GroupBoundaryTicks} instead. The seconds form
|
|
408
|
+
* rounds to whole nanoseconds and reports phantom drift on catalogs whose
|
|
409
|
+
* `groupDurationTicks / timescale` is not ns-exact.
|
|
410
|
+
*/
|
|
411
|
+
notifyGroupBoundary(
|
|
412
|
+
trackName: string,
|
|
413
|
+
packetGroupId: number,
|
|
414
|
+
mediaTimeSec: number,
|
|
415
|
+
groupDurationSec: number,
|
|
416
|
+
): void
|
|
417
|
+
notifyGroupBoundary(
|
|
418
|
+
trackName: string,
|
|
419
|
+
packetGroupId: number,
|
|
420
|
+
timingOrMediaTimeSec: GroupBoundaryTicks | number,
|
|
421
|
+
groupDurationSec?: number,
|
|
422
|
+
): void {
|
|
423
|
+
if (!this.#onGroupDrift) return
|
|
424
|
+
|
|
425
|
+
const resolved =
|
|
426
|
+
typeof timingOrMediaTimeSec === 'object' && timingOrMediaTimeSec !== null
|
|
427
|
+
? this.#resolveTicks(trackName, timingOrMediaTimeSec)
|
|
428
|
+
: this.#resolveSeconds(
|
|
429
|
+
trackName,
|
|
430
|
+
timingOrMediaTimeSec as number,
|
|
431
|
+
groupDurationSec as number,
|
|
432
|
+
)
|
|
433
|
+
if (!resolved) return
|
|
434
|
+
|
|
435
|
+
// Init/ambient track heuristic: mediaTime=0 with stamped groupId !== 0
|
|
436
|
+
// is a signaling packet pre-PTS-zero, not a real drift. Skipping here
|
|
437
|
+
// avoids every stream opening with a false drift event.
|
|
438
|
+
if (resolved.mediaTimeSec === 0 && packetGroupId !== 0) return
|
|
439
|
+
|
|
440
|
+
const expected = resolved.expected
|
|
441
|
+
const result = driftCheck(expected, packetGroupId)
|
|
442
|
+
if (result.kind === 'aligned') return
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
this.#onGroupDrift({
|
|
446
|
+
trackName,
|
|
447
|
+
expectedGroupId: expected,
|
|
448
|
+
actualGroupId: packetGroupId,
|
|
449
|
+
deltaGroups: result.deltaGroups,
|
|
450
|
+
mediaTimeSec: resolved.mediaTimeSec,
|
|
451
|
+
groupDurationSec: resolved.groupDurationSec,
|
|
452
|
+
...(resolved.timing ? { timing: resolved.timing } : {}),
|
|
453
|
+
})
|
|
454
|
+
} catch (err) {
|
|
455
|
+
// A disposed CmcdProducer (or any downstream consumer) throwing here
|
|
456
|
+
// must NOT tear down the MoQ ingest stream. Log and continue.
|
|
457
|
+
//
|
|
458
|
+
// Routed through the same per-reason budget as the resolver skips: this
|
|
459
|
+
// fires once per DRIFT EVENT, so a consumer that throws on every
|
|
460
|
+
// boundary would otherwise flood at boundary rate — exactly the failure
|
|
461
|
+
// mode #skipWithWarning exists to prevent, and exactly the disposed-
|
|
462
|
+
// producer scenario this comment already cites (#329 review).
|
|
463
|
+
this.#skipWithWarning(
|
|
464
|
+
'callback-threw',
|
|
465
|
+
`[MediaRouter] onGroupDrift callback threw (track=${trackName} expected=${expected} actual=${packetGroupId}); continuing: ${(err as Error)?.message ?? err}`,
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Warn at most `GROUP_ALIGN_WARN_BUDGET[reason]` times **per reason**, across
|
|
472
|
+
* every skip and post-emit failure on the drift path.
|
|
473
|
+
*
|
|
474
|
+
* Note the two call shapes: the resolvers call this *instead of* computing a
|
|
475
|
+
* groupId, and return its `null` to skip the drift check; the `onGroupDrift`
|
|
476
|
+
* catch calls it after the check already ran and the event was already
|
|
477
|
+
* emitted, so there only the warn is coalesced and the `null` is discarded.
|
|
478
|
+
*
|
|
479
|
+
* Keyed on a short stable `reason` tag, NOT on the formatted message: the
|
|
480
|
+
* messages interpolate track name and operand values, so message-keying
|
|
481
|
+
* would hand a malformed stream a fresh warn slot on every boundary — the
|
|
482
|
+
* log flood this coalescing exists to prevent.
|
|
483
|
+
*
|
|
484
|
+
* Per-reason rather than one shared boolean (PCL-2211, #329 review): the
|
|
485
|
+
* budget is never reset — not by `dispose()`, not on source switch — so a
|
|
486
|
+
* single flag means the FIRST malformed boundary of a long-running stream
|
|
487
|
+
* permanently silences every later, genuinely different failure mode. The
|
|
488
|
+
* throw/overflow diagnostics carry the operand values (`ticks=`, `durTicks=`,
|
|
489
|
+
* `err.message`) an incident responder actually needs, and under one shared
|
|
490
|
+
* flag those are precisely the ones most likely to be already spent by an
|
|
491
|
+
* unrelated NaN at catalog load.
|
|
492
|
+
*
|
|
493
|
+
* A closed, bounded set of reasons times a small per-reason budget is still
|
|
494
|
+
* bounded, so this cannot flood. See `GROUP_ALIGN_WARN_BUDGET` for why the
|
|
495
|
+
* budget is not uniform.
|
|
496
|
+
*/
|
|
497
|
+
#skipWithWarning(reason: GroupAlignSkipReason, message: string): null {
|
|
498
|
+
const seen = this.#groupAlignWarnCounts.get(reason) ?? 0
|
|
499
|
+
if (seen < GROUP_ALIGN_WARN_BUDGET[reason]) {
|
|
500
|
+
this.#groupAlignWarnCounts.set(reason, seen + 1)
|
|
501
|
+
console.warn(message)
|
|
502
|
+
}
|
|
503
|
+
return null
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Exact path: integers straight from the catalog, no rounding. `mediaTimeSec`
|
|
508
|
+
* / `groupDurationSec` are derived for reporting only and never feed the
|
|
509
|
+
* groupId computation.
|
|
510
|
+
*/
|
|
511
|
+
#resolveTicks(
|
|
512
|
+
trackName: string,
|
|
513
|
+
timing: GroupBoundaryTicks,
|
|
514
|
+
): ResolvedBoundary | null {
|
|
515
|
+
const { mediaTimeTicks, groupDurationTicks, timescale } = timing
|
|
516
|
+
if (
|
|
517
|
+
!Number.isInteger(mediaTimeTicks) ||
|
|
518
|
+
!Number.isInteger(groupDurationTicks) ||
|
|
519
|
+
groupDurationTicks <= 0 ||
|
|
520
|
+
!Number.isInteger(timescale) ||
|
|
521
|
+
timescale <= 0
|
|
522
|
+
) {
|
|
523
|
+
return this.#skipWithWarning(
|
|
524
|
+
'invalid-tick-inputs',
|
|
525
|
+
`[MediaRouter] notifyGroupBoundary invalid tick inputs (track=${trackName} mediaTimeTicks=${mediaTimeTicks} groupDurationTicks=${groupDurationTicks} timescale=${timescale}); skipping drift check. Pre-validate at catalog load time.`,
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
return {
|
|
530
|
+
expected: groupIdForTicks(mediaTimeTicks, groupDurationTicks),
|
|
531
|
+
mediaTimeSec: mediaTimeTicks / timescale,
|
|
532
|
+
groupDurationSec: groupDurationTicks / timescale,
|
|
533
|
+
// Snapshot, not the caller's reference: a producer that reuses one
|
|
534
|
+
// scratch object per boundary would otherwise retroactively mutate
|
|
535
|
+
// every GroupDriftEvent it has already emitted. Three integers.
|
|
536
|
+
timing: { mediaTimeTicks, groupDurationTicks, timescale },
|
|
537
|
+
}
|
|
538
|
+
} catch (err) {
|
|
539
|
+
// UNREACHABLE as of today, and deliberately kept. groupIdForTicks throws
|
|
540
|
+
// on exactly two conditions — !Number.isInteger(ticks) and
|
|
541
|
+
// !Number.isInteger(groupDurationTicks) || groupDurationTicks <= 0 — and
|
|
542
|
+
// the guard above pre-rejects both; Math.floor(a / b) does not throw.
|
|
543
|
+
//
|
|
544
|
+
// Kept because align.ts is a separate module that can grow a new
|
|
545
|
+
// precondition (a safe-integer check, say) without this file changing,
|
|
546
|
+
// and the no-throw contract must hold on that day too. Nobody should
|
|
547
|
+
// write a test chasing this branch — see BLO-21176 note.
|
|
548
|
+
//
|
|
549
|
+
// NOTE the asymmetry this implies: the seconds path is the only one that
|
|
550
|
+
// can actually reach its catch, because durTicks <= 0 cannot catch
|
|
551
|
+
// Infinity and Number.isSafeInteger guards only `ticks`. That is the real
|
|
552
|
+
// shape of the AC#5a bug, not an oversight in this resolver.
|
|
553
|
+
return this.#skipWithWarning(
|
|
554
|
+
'ticks-formula-threw',
|
|
555
|
+
`[MediaRouter] notifyGroupBoundary: groupIdForTicks threw (track=${trackName} mediaTimeTicks=${mediaTimeTicks} groupDurationTicks=${groupDurationTicks}); skipping drift check: ${(err as Error)?.message ?? err}`,
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Legacy seconds path. Quantizes to a synthetic 1e9 Hz timescale — the exact
|
|
562
|
+
* behaviour the deprecated `align.groupIdFor` wrapper had, reproduced here so
|
|
563
|
+
* that wrapper has no non-test callers left and can be deleted (BLO-21176).
|
|
564
|
+
*
|
|
565
|
+
* Inherits that wrapper's ns-rounding error; see the `@deprecated` overload.
|
|
566
|
+
* Delete this branch once every caller passes ticks.
|
|
567
|
+
*/
|
|
568
|
+
#resolveSeconds(
|
|
569
|
+
trackName: string,
|
|
570
|
+
mediaTimeSec: number,
|
|
571
|
+
groupDurationSec: number,
|
|
572
|
+
): ResolvedBoundary | null {
|
|
573
|
+
if (
|
|
574
|
+
!Number.isFinite(groupDurationSec) ||
|
|
575
|
+
groupDurationSec <= 0 ||
|
|
576
|
+
!Number.isFinite(mediaTimeSec)
|
|
577
|
+
) {
|
|
578
|
+
return this.#skipWithWarning(
|
|
579
|
+
'invalid-seconds-inputs',
|
|
580
|
+
`[MediaRouter] notifyGroupBoundary invalid inputs (track=${trackName} mediaTimeSec=${mediaTimeSec} groupDurationSec=${groupDurationSec}); skipping drift check. Pre-validate at catalog load time.`,
|
|
581
|
+
)
|
|
582
|
+
}
|
|
583
|
+
// 1e9 Hz keeps both operands inside MAX_SAFE_INTEGER for ~104 days of
|
|
584
|
+
// media time. Past that Math.round yields a non-safe integer that
|
|
585
|
+
// Number.isInteger still accepts, so bail explicitly rather than
|
|
586
|
+
// silently quantizing — ingest must survive either way.
|
|
587
|
+
const TS = 1_000_000_000
|
|
588
|
+
const ticks = mediaTimeSec < 0 ? 0 : Math.round(mediaTimeSec * TS)
|
|
589
|
+
const durTicks = Math.round(groupDurationSec * TS)
|
|
590
|
+
if (durTicks <= 0) {
|
|
591
|
+
return this.#skipWithWarning(
|
|
592
|
+
'sub-nanosecond-duration',
|
|
593
|
+
`[MediaRouter] notifyGroupBoundary: groupDurationSec=${groupDurationSec} rounds to <1 tick at 1e9 Hz (track=${trackName}); skipping drift check. Pass GroupBoundaryTicks for sub-nanosecond durations.`,
|
|
594
|
+
)
|
|
595
|
+
}
|
|
596
|
+
if (!Number.isSafeInteger(ticks)) {
|
|
597
|
+
return this.#skipWithWarning(
|
|
598
|
+
'media-time-overflow',
|
|
599
|
+
`[MediaRouter] notifyGroupBoundary: mediaTimeSec=${mediaTimeSec} overflows safe-integer ticks at 1e9 Hz (>~104 days, track=${trackName}); skipping drift check. Pass GroupBoundaryTicks with the catalog timescale.`,
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
// Same try as #resolveTicks: a finite groupDurationSec can still overflow
|
|
603
|
+
// the * 1e9 multiply to Infinity, which clears the durTicks <= 0 bail above
|
|
604
|
+
// but fails Number.isInteger inside groupIdForTicks. Without this catch the
|
|
605
|
+
// throw escapes notifyGroupBoundary and breaks its documented no-throw
|
|
606
|
+
// contract — the one path #resolveTicks already guards and this did not.
|
|
607
|
+
try {
|
|
608
|
+
return {
|
|
609
|
+
expected: groupIdForTicks(ticks, durTicks),
|
|
610
|
+
mediaTimeSec,
|
|
611
|
+
groupDurationSec,
|
|
612
|
+
}
|
|
613
|
+
} catch (err) {
|
|
614
|
+
return this.#skipWithWarning(
|
|
615
|
+
'seconds-formula-threw',
|
|
616
|
+
`[MediaRouter] notifyGroupBoundary: groupIdForTicks threw (track=${trackName} mediaTimeSec=${mediaTimeSec} groupDurationSec=${groupDurationSec} ticks=${ticks} durTicks=${durTicks}); skipping drift check: ${(err as Error)?.message ?? err}`,
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
get activeSource(): SourceKind { return this.#active }
|
|
622
|
+
|
|
623
|
+
// ── Keyframe repair API ──────────────────────────────────────────
|
|
624
|
+
|
|
625
|
+
/** Register callback for keyframe repair requests. Player wires this to issue MoQ SUBSCRIBE. */
|
|
626
|
+
onRepairRequest(cb: (expectedGroupId: number, trackName: string) => void): void {
|
|
627
|
+
this.#onRepairRequest = cb
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Request repair for a lost keyframe group. Only fires when activeSource is 'multicast'.
|
|
632
|
+
* Idempotent per groupId — duplicate calls for the same group are suppressed.
|
|
633
|
+
* @param expectedGroupId - The group ID expected but not received
|
|
634
|
+
* @param trackName - Track name for the SUBSCRIBE (e.g. 'video'). Default: 'video'.
|
|
635
|
+
*/
|
|
636
|
+
repairLostGroup(expectedGroupId: number, trackName = 'video'): void {
|
|
637
|
+
if (this.#active !== 'multicast') return // only repair on multicast path
|
|
638
|
+
if (this.#repairInFlight.has(expectedGroupId)) return // idempotent
|
|
639
|
+
if (this.#repairInFlight.size >= this.#maxRepairInFlight) {
|
|
640
|
+
console.warn(
|
|
641
|
+
`[MediaRouter] max repair in flight (${this.#maxRepairInFlight}), dropping repair for group ${expectedGroupId}`,
|
|
642
|
+
)
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
this.#repairInFlight.add(expectedGroupId)
|
|
646
|
+
this.#onRepairRequest?.(expectedGroupId, trackName)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Feed a recovered keyframe frame from the repair path.
|
|
651
|
+
* Bypasses source arbitration entirely — goes directly to decoder callbacks.
|
|
652
|
+
* Does NOT change activeSource. Does NOT trigger failbackToMoq.
|
|
653
|
+
*/
|
|
654
|
+
feedRepair(data: Uint8Array, meta: FrameMeta, kind: FrameKind): void {
|
|
655
|
+
// Repair only makes sense when multicast is active.
|
|
656
|
+
// Silently drop if MoQ became active (race with failbackToMoq) to avoid
|
|
657
|
+
// injecting stale-timestamp frames into the MoQ decoder path (WR-02).
|
|
658
|
+
if (this.#active !== 'multicast') return
|
|
659
|
+
const routedMeta = { ...meta, source: 'repair' as const, recovered: true }
|
|
660
|
+
if (kind === 'video') this.#onVideoFrame?.(data, routedMeta)
|
|
661
|
+
else this.#onAudioFrame?.(data, routedMeta)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Clear repair-in-flight state for a group (called after repair completes or times out).
|
|
666
|
+
*/
|
|
667
|
+
clearRepairInFlight(groupId: number): void {
|
|
668
|
+
this.#repairInFlight.delete(groupId)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ── Phase 14: deltaActive state machine API ──────────────────────
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Configure the delta-track fallback state machine from catalog parameters.
|
|
675
|
+
*
|
|
676
|
+
* D-11: Thresholds derive from fec.sourceSymbols, fec.repairSymbols, framerate.
|
|
677
|
+
* D-13: Probe interval derives from deltaProbeIntervalMs or interleaveDepthMs*2.
|
|
678
|
+
* CLAUDE.md: error on bad init — throws if required inputs are absent.
|
|
679
|
+
*
|
|
680
|
+
* Calling again resets deltaActive=true and clears the window buffer (stream restart).
|
|
681
|
+
*/
|
|
682
|
+
configureDelta(cfg: {
|
|
683
|
+
framerate: number
|
|
684
|
+
sourceSymbols: number
|
|
685
|
+
repairSymbols: number
|
|
686
|
+
deltaFailWindowBlocks?: number
|
|
687
|
+
deltaProbeIntervalMs?: number
|
|
688
|
+
interleaveDepthMs?: number
|
|
689
|
+
}): void {
|
|
690
|
+
if (typeof cfg.framerate !== 'number' || !Number.isFinite(cfg.framerate) || cfg.framerate <= 0) {
|
|
691
|
+
throw new Error('[MediaRouter] configureDelta: framerate required and must be > 0')
|
|
692
|
+
}
|
|
693
|
+
if (typeof cfg.sourceSymbols !== 'number' || cfg.sourceSymbols <= 0) {
|
|
694
|
+
throw new Error('[MediaRouter] configureDelta: sourceSymbols (K_delta) required and must be > 0')
|
|
695
|
+
}
|
|
696
|
+
if (typeof cfg.repairSymbols !== 'number' || cfg.repairSymbols <= 0) {
|
|
697
|
+
throw new Error('[MediaRouter] configureDelta: repairSymbols (P_delta) required and must be > 0')
|
|
698
|
+
}
|
|
699
|
+
if (cfg.deltaProbeIntervalMs === undefined && cfg.interleaveDepthMs === undefined) {
|
|
700
|
+
throw new Error(
|
|
701
|
+
'[MediaRouter] configureDelta: either deltaProbeIntervalMs or interleaveDepthMs required for probe interval derivation'
|
|
702
|
+
)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Derivations — no magic floors, no hardcoded defaults (CLAUDE.md).
|
|
706
|
+
this.#deltaWindowSize = cfg.deltaFailWindowBlocks ?? Math.ceil(cfg.framerate)
|
|
707
|
+
this.#deltaFailThresholdRatio = cfg.repairSymbols / cfg.sourceSymbols
|
|
708
|
+
this.#deltaRecoverThresholdRatio = this.#deltaFailThresholdRatio / 2
|
|
709
|
+
// Probe interval routes through the ONE FEC recovery-window derivation
|
|
710
|
+
// (computeFecDeliveryWindowMs). Interleave-span + jitter margin only — the
|
|
711
|
+
// K-cycle production horizon (mediaUnitsPerSecond) is deliberately omitted:
|
|
712
|
+
// this is a re-subscribe cadence, not a block-recovery deadline. With no
|
|
713
|
+
// catalog jitterBufferMs the margin defaults to one interleave window, so
|
|
714
|
+
// the value equals the retired interleaveDepthMs*2 but is now catalog-tunable.
|
|
715
|
+
this.#deltaProbeIntervalMs = cfg.deltaProbeIntervalMs ?? computeFecDeliveryWindowMs({
|
|
716
|
+
interleaveDepthMs: cfg.interleaveDepthMs!,
|
|
717
|
+
sourceSymbols: cfg.sourceSymbols,
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
// Reset state — useful for stream restart.
|
|
721
|
+
this.#deltaActive = true
|
|
722
|
+
this.#deltaWindow = []
|
|
723
|
+
this.#lastProbeAt = 0
|
|
724
|
+
this.#deltaConfigured = true
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Notify the state machine of a completed FEC block for the delta track.
|
|
729
|
+
*
|
|
730
|
+
* @param failed - true if the block was unrecoverable (FEC failure)
|
|
731
|
+
* @param nowMs - caller-injected monotonic ms (no internal timer — D-13, CLAUDE.md)
|
|
732
|
+
*
|
|
733
|
+
* If configureDelta() has not been called, this is a silent no-op (pre-catalog state).
|
|
734
|
+
*/
|
|
735
|
+
notifyDeltaBlock(failed: boolean, nowMs: number): void {
|
|
736
|
+
if (!this.#deltaConfigured) return
|
|
737
|
+
|
|
738
|
+
// Maintain sliding window — ring buffer semantics.
|
|
739
|
+
this.#deltaWindow.push(failed)
|
|
740
|
+
if (this.#deltaWindow.length > this.#deltaWindowSize) {
|
|
741
|
+
this.#deltaWindow.shift()
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Window not yet full — insufficient data for decision.
|
|
745
|
+
if (this.#deltaWindow.length < this.#deltaWindowSize) return
|
|
746
|
+
|
|
747
|
+
const failedCount = this.#deltaWindow.filter(x => x).length
|
|
748
|
+
const ratio = failedCount / this.#deltaWindowSize
|
|
749
|
+
|
|
750
|
+
if (this.#deltaActive) {
|
|
751
|
+
// Fallback transition: active → inactive (T-14-03: fires exactly once)
|
|
752
|
+
if (ratio > this.#deltaFailThresholdRatio) {
|
|
753
|
+
this.#deltaActive = false
|
|
754
|
+
this.#lastProbeAt = nowMs
|
|
755
|
+
this.#onDeltaFallbackCb?.()
|
|
756
|
+
}
|
|
757
|
+
} else {
|
|
758
|
+
// Recovery transition: inactive → active (after probe interval + low ratio)
|
|
759
|
+
if (
|
|
760
|
+
(nowMs - this.#lastProbeAt) >= this.#deltaProbeIntervalMs &&
|
|
761
|
+
ratio < this.#deltaRecoverThresholdRatio
|
|
762
|
+
) {
|
|
763
|
+
this.#deltaActive = true
|
|
764
|
+
this.#lastProbeAt = nowMs
|
|
765
|
+
this.#onDeltaRecoverCb?.()
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** Register callback fired when delta track degrades past threshold (D-12). */
|
|
771
|
+
onDeltaFallback(cb: () => void): void {
|
|
772
|
+
this.#onDeltaFallbackCb = cb
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** Register callback fired when delta track recovers below recovery threshold (D-13). */
|
|
776
|
+
onDeltaRecover(cb: () => void): void {
|
|
777
|
+
this.#onDeltaRecoverCb = cb
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/** Whether the delta track is currently considered healthy (true = active). */
|
|
781
|
+
get deltaActive(): boolean {
|
|
782
|
+
return this.#deltaActive
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ── Test-visible accessors for threshold verification ────────────
|
|
786
|
+
// These expose internal derived values for test assertions (Tests 3,4,5,6,7).
|
|
787
|
+
// Prefixed with 'delta' to namespace clearly within the public API surface.
|
|
788
|
+
|
|
789
|
+
/** Derived window size — exposed for test assertions (Test 3, 4). */
|
|
790
|
+
get deltaWindowSize(): number { return this.#deltaWindowSize }
|
|
791
|
+
/** Derived fail threshold ratio (P/K) — exposed for test assertions (Test 6). */
|
|
792
|
+
get deltaFailThresholdRatio(): number { return this.#deltaFailThresholdRatio }
|
|
793
|
+
/** Derived recovery threshold ratio ((P/K)/2) — exposed for test assertions (Test 7). */
|
|
794
|
+
get deltaRecoverThresholdRatio(): number { return this.#deltaRecoverThresholdRatio }
|
|
795
|
+
/** Derived probe interval ms — exposed for test assertions (Test 5a). */
|
|
796
|
+
get deltaProbeIntervalMs(): number { return this.#deltaProbeIntervalMs }
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Register a track name → type mapping from catalog.
|
|
800
|
+
* Catalog is the default authority. Init data / inline signaling
|
|
801
|
+
* takes precedence until the next catalog update.
|
|
802
|
+
*/
|
|
803
|
+
registerTrackType(trackName: string, type: FrameKind): void {
|
|
804
|
+
// Catalog overwrites any previous registration (including init).
|
|
805
|
+
this.#trackTypes.set(trackName, { type, source: 'catalog' })
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Register a track name → type from init data / inline signaling (MMTP FT=0).
|
|
810
|
+
* Authoritative until the next catalog update per §5.3.
|
|
811
|
+
* Does NOT overwrite an existing catalog registration.
|
|
812
|
+
*/
|
|
813
|
+
registerTrackTypeFromInit(trackName: string, type: FrameKind): void {
|
|
814
|
+
const existing = this.#trackTypes.get(trackName)
|
|
815
|
+
if (existing?.source === 'catalog') return // catalog already set — don't downgrade
|
|
816
|
+
this.#trackTypes.set(trackName, { type, source: 'init' })
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Assign a per-track source. Frames from this source bypass the binary
|
|
821
|
+
* MoQ↔multicast switching for this track only.
|
|
822
|
+
*
|
|
823
|
+
* Throws TrackSourceConflictError if a source is already assigned for this
|
|
824
|
+
* track without a clearTrackSource() in between.
|
|
825
|
+
*/
|
|
826
|
+
setTrackSource(trackName: string, source: TrackSource, opts?: SetTrackSourceOptions): void {
|
|
827
|
+
if (this.#trackSources.has(trackName)) {
|
|
828
|
+
throw new TrackSourceConflictError(trackName)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Resolve effective policy. opts.policy takes precedence; overlay=true is legacy 'wait'.
|
|
832
|
+
const effectivePolicy: MigrationPolicy | undefined =
|
|
833
|
+
opts?.policy ?? (opts?.overlay ? 'wait' : undefined)
|
|
834
|
+
|
|
835
|
+
// For crossfade: open the binary-path window immediately so the first per-track frame
|
|
836
|
+
// triggers the window (counter starts before the frame callback is wired).
|
|
837
|
+
if (effectivePolicy === 'crossfade') {
|
|
838
|
+
this.#crossfadeCounters.set(trackName, this.#crossfadeFrames)
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const record: TrackSourceRecord = {
|
|
842
|
+
source,
|
|
843
|
+
unsubscribeFrame: () => {},
|
|
844
|
+
emitCount: 0,
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Wire the frame callback
|
|
848
|
+
record.unsubscribeFrame = source.onFrame((data, meta) => {
|
|
849
|
+
record.emitCount++
|
|
850
|
+
if (effectivePolicy === 'wait') {
|
|
851
|
+
this.#handleOverlayFrame(data, meta)
|
|
852
|
+
} else {
|
|
853
|
+
// 'drop', 'crossfade', or undefined: emit immediately
|
|
854
|
+
this.#emitForTrack(trackName, data, meta)
|
|
855
|
+
}
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
// Wire the optional error callback — failback to binary switching
|
|
859
|
+
if (source.onError) {
|
|
860
|
+
record.unsubscribeError = source.onError(() => {
|
|
861
|
+
this.clearTrackSource(trackName)
|
|
862
|
+
})
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
this.#trackSources.set(trackName, record)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Remove the per-track source for the given track.
|
|
870
|
+
* Binary switching resumes for this track after this call.
|
|
871
|
+
*/
|
|
872
|
+
clearTrackSource(trackName: string): void {
|
|
873
|
+
const record = this.#trackSources.get(trackName)
|
|
874
|
+
if (!record) return
|
|
875
|
+
|
|
876
|
+
record.unsubscribeFrame()
|
|
877
|
+
record.unsubscribeError?.()
|
|
878
|
+
record.source.dispose?.()
|
|
879
|
+
this.#trackSources.delete(trackName)
|
|
880
|
+
|
|
881
|
+
// Clean up crossfade window if one was active for this track
|
|
882
|
+
this.#crossfadeCounters.delete(trackName)
|
|
883
|
+
|
|
884
|
+
// Reset overlay buffer if removing a non-media track (overlay/unknown)
|
|
885
|
+
const reg = this.#trackTypes.get(trackName)
|
|
886
|
+
const isMedia = reg?.type === 'video' || reg?.type === 'audio' ||
|
|
887
|
+
(reg === undefined && (trackName === 'video' || trackName === 'audio'))
|
|
888
|
+
if (!isMedia) {
|
|
889
|
+
this.#overlayBoundaryReached = false
|
|
890
|
+
this.#overlayBuffer = []
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Signal that a FEC block boundary has been reached on the base track.
|
|
896
|
+
* Overlay tracks waiting for alignment flush their buffered frames now.
|
|
897
|
+
*/
|
|
898
|
+
notifyFecBlockBoundary(): void {
|
|
899
|
+
if (this.#overlayBoundaryReached) return
|
|
900
|
+
this.#overlayBoundaryReached = true
|
|
901
|
+
|
|
902
|
+
// Flush buffered overlay frames
|
|
903
|
+
const buffered = this.#overlayBuffer.splice(0)
|
|
904
|
+
for (const { data, meta } of buffered) {
|
|
905
|
+
this.#onOverlayFrame?.(data, meta)
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Return the number of frames emitted from the per-track source for a given
|
|
911
|
+
* track. Used in test-mode to verify per-track routing histogram.
|
|
912
|
+
* Returns 0 if no per-track source is active for the track.
|
|
913
|
+
*/
|
|
914
|
+
trackSourceEmitCount(trackName: string): number {
|
|
915
|
+
return this.#trackSources.get(trackName)?.emitCount ?? 0
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/** Feed a frame from the MoQ path. Suppressed when multicast is active OR a per-track source owns this track. */
|
|
919
|
+
feedMoq(data: Uint8Array, meta: FrameMeta, kind: FrameKind) {
|
|
920
|
+
// Per-track source takes precedence; allow through during crossfade window
|
|
921
|
+
if (this.#trackSources.has(kind)) {
|
|
922
|
+
const remaining = this.#crossfadeCounters.get(kind)
|
|
923
|
+
if (remaining && remaining > 0) {
|
|
924
|
+
// Decrement crossfade counter and allow this frame through
|
|
925
|
+
const next = remaining - 1
|
|
926
|
+
if (next === 0) {
|
|
927
|
+
this.#crossfadeCounters.delete(kind)
|
|
928
|
+
} else {
|
|
929
|
+
this.#crossfadeCounters.set(kind, next)
|
|
930
|
+
}
|
|
931
|
+
// Fall through to emit below (no return)
|
|
932
|
+
} else {
|
|
933
|
+
return // suppressed — no crossfade window active
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (this.#suppressed) return
|
|
937
|
+
// Audio suppression is handled by SessionManager onSuppress callback —
|
|
938
|
+
// do NOT independently suppress based on multicastAudioReceived.
|
|
939
|
+
// The state machine decides when to switch; the router just routes.
|
|
940
|
+
const routedMeta = { ...meta, source: 'moq' as const }
|
|
941
|
+
if (kind === 'video') this.#onVideoFrame?.(data, routedMeta)
|
|
942
|
+
else this.#onAudioFrame?.(data, routedMeta)
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Feed a frame from the multicast path. Video waits for keyframe before activating. */
|
|
946
|
+
feedMulticast(data: Uint8Array, meta: FrameMeta, kind: FrameKind) {
|
|
947
|
+
// Per-track source takes precedence; allow through during crossfade window
|
|
948
|
+
if (this.#trackSources.has(kind)) {
|
|
949
|
+
const remaining = this.#crossfadeCounters.get(kind)
|
|
950
|
+
if (remaining && remaining > 0) {
|
|
951
|
+
const next = remaining - 1
|
|
952
|
+
if (next === 0) {
|
|
953
|
+
this.#crossfadeCounters.delete(kind)
|
|
954
|
+
} else {
|
|
955
|
+
this.#crossfadeCounters.set(kind, next)
|
|
956
|
+
}
|
|
957
|
+
// Fall through to emit below (no return)
|
|
958
|
+
} else {
|
|
959
|
+
return // suppressed
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (kind === 'video') {
|
|
964
|
+
if (!this.#multicastVideoKeyed) {
|
|
965
|
+
const moqSeededDelta =
|
|
966
|
+
!meta.keyframe &&
|
|
967
|
+
this.#allowDeltaMulticastTakeoverAfterMoqKeyframe &&
|
|
968
|
+
this.#moqVideoKeyframeEmitted
|
|
969
|
+
if (!meta.keyframe && !moqSeededDelta) return // drop until first safe decode point
|
|
970
|
+
this.#multicastVideoKeyed = true
|
|
971
|
+
const pendingMeta: FrameMeta = { ...meta, source: 'multicast' }
|
|
972
|
+
if (!moqSeededDelta) pendingMeta.sourceDiscontinuity = true
|
|
973
|
+
this.#pendingMulticastVideo = {
|
|
974
|
+
data: data.slice(),
|
|
975
|
+
meta: pendingMeta,
|
|
976
|
+
}
|
|
977
|
+
// If audio is required but hasn't arrived yet, start a fallback timer.
|
|
978
|
+
// Audio FEC blocks may recover after video — don't block activation forever.
|
|
979
|
+
// Only arms when a catalog-derived audioFallbackMs was supplied (>0).
|
|
980
|
+
if (this.#requireAudio && this.#audioFallbackMs > 0 && !this.#multicastAudioReceived && !this.#audioFallbackTimer) {
|
|
981
|
+
this.#audioFallbackTimer = setTimeout(() => {
|
|
982
|
+
if (!this.#multicastAudioReceived && this.#active !== 'multicast') {
|
|
983
|
+
this.#multicastAudioReceived = true // force
|
|
984
|
+
// Also release the MoQ-audio-warmup gate so the fallback actually
|
|
985
|
+
// activates — otherwise #tryActivateMulticast would still bail.
|
|
986
|
+
this.#moqAudioWarmed = true
|
|
987
|
+
this.#tryActivateMulticast()
|
|
988
|
+
}
|
|
989
|
+
}, this.#audioFallbackMs)
|
|
990
|
+
}
|
|
991
|
+
} else if (this.#active !== 'multicast') {
|
|
992
|
+
// Do not feed video into the multicast decoder while the source switch
|
|
993
|
+
// is still pending. Keep the newest keyframe as the post-activation
|
|
994
|
+
// decoder opener and drop deltas until takeover commits.
|
|
995
|
+
if (meta.keyframe) {
|
|
996
|
+
this.#pendingMulticastVideo = {
|
|
997
|
+
data: data.slice(),
|
|
998
|
+
meta: { ...meta, source: 'multicast', sourceDiscontinuity: true },
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
this.#tryActivateMulticast()
|
|
1002
|
+
return
|
|
1003
|
+
}
|
|
1004
|
+
if (this.#active === 'multicast') this.#onVideoFrame?.(data, { ...meta, source: 'multicast' })
|
|
1005
|
+
this.#tryActivateMulticast()
|
|
1006
|
+
} else {
|
|
1007
|
+
this.#multicastAudioReceived = true
|
|
1008
|
+
if (this.#audioFallbackTimer) {
|
|
1009
|
+
clearTimeout(this.#audioFallbackTimer)
|
|
1010
|
+
this.#audioFallbackTimer = null
|
|
1011
|
+
}
|
|
1012
|
+
// BLO-3035 layer 5: in a multicast-first scenario (no MoQ unicast audio
|
|
1013
|
+
// source) `notifyMoqAudioEmit()` is never called and the audio-fallback
|
|
1014
|
+
// timer's force-set is bypassed (the timer's inner `!multicastAudioReceived`
|
|
1015
|
+
// check returns early because audio already arrived). Without this gate
|
|
1016
|
+
// `#moqAudioWarmed` stays false forever and `#tryActivateMulticast` never
|
|
1017
|
+
// fires `onSourceSwitch('multicast')` — the player can't know to emit
|
|
1018
|
+
// `__MULTICAST_TAKEOVER_TIME`. Treat the arrival of multicast audio as
|
|
1019
|
+
// sufficient evidence to release the warmup gate. For the MoQ→multicast
|
|
1020
|
+
// handoff case `notifyMoqAudioEmit()` will already have set this flag, so
|
|
1021
|
+
// the assignment is idempotent.
|
|
1022
|
+
if (!this.#moqAudioWarmed) this.#moqAudioWarmed = true
|
|
1023
|
+
// Activate BEFORE delivering the frame — the source switch callback
|
|
1024
|
+
// (e.g. ring buffer reset) must fire before the first multicast audio
|
|
1025
|
+
// frame reaches the backend. Otherwise the NTP timestamp hits the
|
|
1026
|
+
// MoQ-era ring buffer base → overflow → silence gap.
|
|
1027
|
+
this.#tryActivateMulticast()
|
|
1028
|
+
// Only emit multicast audio AFTER takeover succeeds. Before takeover,
|
|
1029
|
+
// MoQ audio is still flowing through feedMoq → #onAudioFrame; emitting
|
|
1030
|
+
// multicast audio too would double-feed the backend (2x audio frames,
|
|
1031
|
+
// pitch wobble and distortion). Post-takeover, feedMoq is suppressed
|
|
1032
|
+
// (#suppressed=true) so only multicast audio reaches the backend.
|
|
1033
|
+
if (this.#active === 'multicast') {
|
|
1034
|
+
this.#onAudioFrame?.(data, { ...meta, source: 'multicast' })
|
|
1035
|
+
} else if (this.#pendingMulticastToken !== null) {
|
|
1036
|
+
this.#pendingMulticastAudio = { data: data.slice(), meta: { ...meta, source: 'multicast' } }
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* MoQ unicast audio warmup gate. When audio is required, multicast takeover
|
|
1043
|
+
* waits until the MoQ unicast audio decoder has emitted at least one frame
|
|
1044
|
+
* (so downstream samples-before-takeover counters observe nonzero) or until
|
|
1045
|
+
* the existing audio-fallback timer forces the flag true.
|
|
1046
|
+
*/
|
|
1047
|
+
#moqAudioWarmed = false
|
|
1048
|
+
|
|
1049
|
+
/** Called by the player when the MoQ unicast audio decoder emits first frame. */
|
|
1050
|
+
public notifyMoqAudioEmit(): void {
|
|
1051
|
+
if (this.#moqAudioWarmed) return
|
|
1052
|
+
this.#moqAudioWarmed = true
|
|
1053
|
+
this.#tryActivateMulticast()
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Called by the player after a MoQ video keyframe has reached the active
|
|
1058
|
+
* decoder path. Used only by MSE/MMTP continuity switching; decoder-reset
|
|
1059
|
+
* paths leave allowDeltaMulticastTakeoverAfterMoqKeyframe disabled.
|
|
1060
|
+
*/
|
|
1061
|
+
public notifyMoqVideoKeyframeEmit(): void {
|
|
1062
|
+
this.#moqVideoKeyframeEmitted = true
|
|
1063
|
+
this.#tryActivateMulticast()
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
#tryActivateMulticast() {
|
|
1067
|
+
if (this.#active === 'multicast') return
|
|
1068
|
+
if (this.#pendingMulticastToken !== null) return
|
|
1069
|
+
if (!this.#multicastVideoKeyed) return
|
|
1070
|
+
if (this.#requireAudio && !this.#multicastAudioReceived) return
|
|
1071
|
+
if (this.#requireAudio && !this.#moqAudioWarmed) return
|
|
1072
|
+
const token = ++this.#sourceSwitchToken
|
|
1073
|
+
const maybeReady = this.#onSourceSwitch?.('multicast')
|
|
1074
|
+
if (!maybeReady || typeof (maybeReady as Promise<void>).then !== 'function') {
|
|
1075
|
+
this.#commitMulticastActivation(token)
|
|
1076
|
+
return
|
|
1077
|
+
}
|
|
1078
|
+
const ready = Promise.resolve(maybeReady)
|
|
1079
|
+
this.#pendingMulticastToken = token
|
|
1080
|
+
this.#sourceSwitchReady = ready
|
|
1081
|
+
void ready.then(
|
|
1082
|
+
() => this.#commitMulticastActivation(token),
|
|
1083
|
+
err => this.#rejectMulticastActivation(token, err),
|
|
1084
|
+
).finally(() => {
|
|
1085
|
+
if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null
|
|
1086
|
+
})
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
#commitMulticastActivation(token: number) {
|
|
1090
|
+
if (token !== this.#sourceSwitchToken) return
|
|
1091
|
+
this.#active = 'multicast'
|
|
1092
|
+
this.#suppressed = true
|
|
1093
|
+
this.#pendingMulticastToken = null
|
|
1094
|
+
const video = this.#pendingMulticastVideo
|
|
1095
|
+
this.#pendingMulticastVideo = null
|
|
1096
|
+
const audio = this.#pendingMulticastAudio
|
|
1097
|
+
this.#pendingMulticastAudio = null
|
|
1098
|
+
if (video) this.#onVideoFrame?.(video.data, video.meta)
|
|
1099
|
+
if (audio) this.#onAudioFrame?.(audio.data, audio.meta)
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
#rejectMulticastActivation(token: number, err: unknown) {
|
|
1103
|
+
if (token !== this.#sourceSwitchToken) return
|
|
1104
|
+
if (this.#pendingMulticastToken === token) this.#pendingMulticastToken = null
|
|
1105
|
+
this.#pendingMulticastVideo = null
|
|
1106
|
+
this.#pendingMulticastAudio = null
|
|
1107
|
+
console.warn('[MediaRouter] onSourceSwitch(multicast) failed', err)
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/** Switch back to MoQ immediately (called on multicast failback). */
|
|
1111
|
+
failbackToMoq() {
|
|
1112
|
+
const hadPendingMulticast = this.#pendingMulticastToken !== null
|
|
1113
|
+
if (this.#active === 'moq' && !hadPendingMulticast) return // already on MoQ, no-op
|
|
1114
|
+
++this.#sourceSwitchToken
|
|
1115
|
+
this.#active = 'moq'
|
|
1116
|
+
this.#suppressed = false
|
|
1117
|
+
this.#pendingMulticastToken = null
|
|
1118
|
+
this.#pendingMulticastVideo = null
|
|
1119
|
+
this.#pendingMulticastAudio = null
|
|
1120
|
+
this.#sourceSwitchReady = null
|
|
1121
|
+
this.#multicastVideoKeyed = false
|
|
1122
|
+
this.#multicastAudioReceived = false
|
|
1123
|
+
this.#moqVideoKeyframeEmitted = false
|
|
1124
|
+
// A pending audio-fallback timer belongs to the abandoned takeover attempt.
|
|
1125
|
+
// Left running it would force-set the audio flags against the next attempt's
|
|
1126
|
+
// fresh state (BLO-9413 failback coherence — every takeover-era flag/timer
|
|
1127
|
+
// unwinds on this edge).
|
|
1128
|
+
if (this.#audioFallbackTimer) {
|
|
1129
|
+
clearTimeout(this.#audioFallbackTimer)
|
|
1130
|
+
this.#audioFallbackTimer = null
|
|
1131
|
+
}
|
|
1132
|
+
this.#onSourceSwitch?.('moq')
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/** Dispose timers and per-track sources. Call on teardown to prevent
|
|
1136
|
+
* stale timer callbacks firing into garbage-collected closures (WR-03). */
|
|
1137
|
+
dispose(): void {
|
|
1138
|
+
if (this.#audioFallbackTimer) {
|
|
1139
|
+
clearTimeout(this.#audioFallbackTimer)
|
|
1140
|
+
this.#audioFallbackTimer = null
|
|
1141
|
+
}
|
|
1142
|
+
for (const [name] of this.#trackSources) {
|
|
1143
|
+
this.clearTrackSource(name)
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// ── Private helpers ──────────────────────────────────────────────────────
|
|
1148
|
+
|
|
1149
|
+
/** Emit a frame on the correct per-track callback using the type registry.
|
|
1150
|
+
* Falls back to name match if no type registered (backward compat). */
|
|
1151
|
+
#emitForTrack(trackName: string, data: Uint8Array, meta: FrameMeta): void {
|
|
1152
|
+
// Prefer explicit type registry (catalog or init data)
|
|
1153
|
+
const reg = this.#trackTypes.get(trackName)
|
|
1154
|
+
if (reg?.type === 'video') {
|
|
1155
|
+
this.#onVideoFrame?.(data, meta)
|
|
1156
|
+
} else if (reg?.type === 'audio') {
|
|
1157
|
+
this.#onAudioFrame?.(data, meta)
|
|
1158
|
+
} else if (reg !== undefined) {
|
|
1159
|
+
// Registered but unknown type — overlay
|
|
1160
|
+
this.#onOverlayFrame?.(data, meta)
|
|
1161
|
+
} else {
|
|
1162
|
+
// Not registered — fall back to name match (backward compat for tests/legacy)
|
|
1163
|
+
if (trackName === 'video') this.#onVideoFrame?.(data, meta)
|
|
1164
|
+
else if (trackName === 'audio') this.#onAudioFrame?.(data, meta)
|
|
1165
|
+
else this.#onOverlayFrame?.(data, meta)
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/** Handle an overlay frame: buffer until first FEC block boundary, then pass through. */
|
|
1170
|
+
#handleOverlayFrame(data: Uint8Array, meta: FrameMeta): void {
|
|
1171
|
+
if (this.#overlayBoundaryReached) {
|
|
1172
|
+
this.#onOverlayFrame?.(data, meta)
|
|
1173
|
+
return
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Buffer until boundary — enforcing cap for T-02-17 DoS mitigation
|
|
1177
|
+
if (this.#overlayBuffer.length >= this.#maxOverlayBuffer) {
|
|
1178
|
+
console.warn(
|
|
1179
|
+
`[MediaRouter] overlay buffer full (${this.#maxOverlayBuffer} frames); dropping frame ts=${meta.timestamp}`
|
|
1180
|
+
)
|
|
1181
|
+
return
|
|
1182
|
+
}
|
|
1183
|
+
this.#overlayBuffer.push({ data, meta })
|
|
1184
|
+
}
|
|
1185
|
+
}
|