@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,3027 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MMTP packet processing and frame routing layer.
|
|
3
|
+
*
|
|
4
|
+
* Delegates MMTP header parsing, MFU extraction, reassembly, and keyframe
|
|
5
|
+
* detection to MmtpContainerParser while owning FEC orchestration, sync
|
|
6
|
+
* gating, CMAF assembly, and callback routing.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from MulticastManager — this module owns MMTP-layer concerns:
|
|
9
|
+
* FEC symbol feeding, sync gating, MSE segment assembly, and frame routing.
|
|
10
|
+
* It does NOT own network I/O, watchdog timers, or multicast join/leave.
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { parseMMTPHeader } from "@blockcast/mmt-base";
|
|
16
|
+
import { MfuReassemblyError } from "./mfu-reassembler.js";
|
|
17
|
+
import type {
|
|
18
|
+
MfuReassemblerBackend,
|
|
19
|
+
MmtpSourcePath,
|
|
20
|
+
MmtpDeliveryProvenance,
|
|
21
|
+
} from "./mfu-reassembler.js";
|
|
22
|
+
import type { MfuReassemblyPolicy } from "./mfu-reassembly-policy.js";
|
|
23
|
+
import { mmtpTimestampToMicros } from "./codec-utils.js";
|
|
24
|
+
import { scaleAuthoredTiming, type AuthoredTiming } from "./authored-timing.js";
|
|
25
|
+
import {
|
|
26
|
+
processFecBlock,
|
|
27
|
+
processFecSymbol,
|
|
28
|
+
parseRepairPacket,
|
|
29
|
+
FecBlockMapper,
|
|
30
|
+
type ProcessFecBlockResult,
|
|
31
|
+
} from "@blockcast/mmt-fec";
|
|
32
|
+
import {
|
|
33
|
+
CmafBlockAssembler,
|
|
34
|
+
type BlockAssemblerConfig,
|
|
35
|
+
type CmafBlockSample,
|
|
36
|
+
} from "./cmaf-block-assembler.js";
|
|
37
|
+
import { MmtpContainerParser, isMfuKeyframeMetadata } from "./mmtp-container-parser.js";
|
|
38
|
+
import { type TrackRole } from "./carp-catalog.js";
|
|
39
|
+
import type {
|
|
40
|
+
ManifestAuthMode,
|
|
41
|
+
ManifestObjectIdentity,
|
|
42
|
+
ManifestTrackContext,
|
|
43
|
+
ManifestVerifier,
|
|
44
|
+
} from "@blockcast/mmt-manifest-verify";
|
|
45
|
+
|
|
46
|
+
// ─── Constants ─────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Keyframe loss timeout slack factor.
|
|
50
|
+
*
|
|
51
|
+
* Derivation: The keyframe loss timer must fire after one full keyframe group
|
|
52
|
+
* has been missed. Group duration must come from explicit catalog timing; FEC
|
|
53
|
+
* sourceSymbols is a block width, not a GOP length. Multicast jitter means the
|
|
54
|
+
* last packet of a group may arrive up to ~0.5 group durations late (network
|
|
55
|
+
* reordering + FEC recovery latency). The total timeout is therefore:
|
|
56
|
+
*
|
|
57
|
+
* timeout = group_duration * (1.0 + 0.5_jitter) = group_duration * 1.5
|
|
58
|
+
*
|
|
59
|
+
* The 0.5 jitter margin covers: (a) FEC interleave recovery window (up to D
|
|
60
|
+
* frames), (b) network reordering within one interleave depth, and (c) OS
|
|
61
|
+
* socket buffer draining latency. This is tight enough to detect loss within
|
|
62
|
+
* one missing group while avoiding false positives from normal jitter.
|
|
63
|
+
*/
|
|
64
|
+
export const KEYFRAME_LOST_TIMEOUT_SLACK = 1.5;
|
|
65
|
+
|
|
66
|
+
function isRouterDebugEnabled(): boolean {
|
|
67
|
+
const globalDebug = (globalThis as { __MMTP_ROUTER_DEBUG?: unknown }).__MMTP_ROUTER_DEBUG;
|
|
68
|
+
if (globalDebug === true || globalDebug === "true" || globalDebug === "1") return true;
|
|
69
|
+
const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process
|
|
70
|
+
?.env;
|
|
71
|
+
return env?.["MMTP_ROUTER_DEBUG"] === "1" || env?.["MMTP_ROUTER_DEBUG"] === "true";
|
|
72
|
+
}
|
|
73
|
+
function debugRouter(message: string): void {
|
|
74
|
+
if (isRouterDebugEnabled()) console.debug(message);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Types ──────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
export type RoutedFrameMeta = {
|
|
80
|
+
timestamp: number;
|
|
81
|
+
keyframe: boolean;
|
|
82
|
+
/** Exact ISO-BMFF timing in the media track's native timescale. */
|
|
83
|
+
authoredTiming?: AuthoredTiming;
|
|
84
|
+
/** Transport that supplied the accepted FI=1 head (or complete FI=0 frame). */
|
|
85
|
+
source?: MmtpSourcePath;
|
|
86
|
+
/**
|
|
87
|
+
* Whole-frame delivery verdict folded across every contributing fragment.
|
|
88
|
+
*
|
|
89
|
+
* Distinct from `source` on purpose. `source` answers a routing question —
|
|
90
|
+
* which transport's copy is this — and deliberately lets the head fragment
|
|
91
|
+
* decide. This answers an evidence question: did every byte of this frame
|
|
92
|
+
* physically arrive over multicast? A frame can be routed as 'multicast'
|
|
93
|
+
* and still be 'mixed'.
|
|
94
|
+
*/
|
|
95
|
+
deliveryProvenance?: MmtpDeliveryProvenance;
|
|
96
|
+
packetId?: number;
|
|
97
|
+
packetSequenceNumber?: number;
|
|
98
|
+
/** Every authoritative MMTP source-packet sequence that formed this frame. */
|
|
99
|
+
packetSequenceNumbers?: number[];
|
|
100
|
+
/** Subset of packetSequenceNumbers reconstructed by FEC. */
|
|
101
|
+
repairedPacketSequenceNumbers?: number[];
|
|
102
|
+
/** Native MMTP MPU group coordinate. */
|
|
103
|
+
mpuSequenceNumber?: number;
|
|
104
|
+
/** Immutable catalog identity supplied by the owning ingress subscription. */
|
|
105
|
+
catalogTrackName?: string;
|
|
106
|
+
recovered?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* True when this frame opens playback after the router suppressed earlier
|
|
109
|
+
* video deltas while waiting for a sync keyframe. Downstream decoders can
|
|
110
|
+
* use this to treat the following dependency chain as provisional.
|
|
111
|
+
*/
|
|
112
|
+
sourceDiscontinuity?: boolean;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export interface MmtpIngressContext {
|
|
116
|
+
tracks: readonly Readonly<{ name: string; packetId: number }>[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function catalogTrackNameForPacket(
|
|
120
|
+
ingress: MmtpIngressContext | undefined,
|
|
121
|
+
packetId: number,
|
|
122
|
+
): string | undefined {
|
|
123
|
+
if (!ingress) return undefined;
|
|
124
|
+
const names = new Set(
|
|
125
|
+
ingress.tracks
|
|
126
|
+
.filter((track) => track.packetId === packetId && track.name.length > 0)
|
|
127
|
+
.map((track) => track.name),
|
|
128
|
+
);
|
|
129
|
+
return names.size === 1 ? names.values().next().value : undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function hasAmbiguousCatalogTrackName(
|
|
133
|
+
ingress: MmtpIngressContext | undefined,
|
|
134
|
+
packetId: number,
|
|
135
|
+
): boolean {
|
|
136
|
+
if (!ingress) return false;
|
|
137
|
+
const names = new Set(
|
|
138
|
+
ingress.tracks
|
|
139
|
+
.filter((track) => track.packetId === packetId && track.name.length > 0)
|
|
140
|
+
.map((track) => track.name),
|
|
141
|
+
);
|
|
142
|
+
return names.size > 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function ingressHasPacketId(
|
|
146
|
+
ingress: MmtpIngressContext | undefined,
|
|
147
|
+
packetId: number,
|
|
148
|
+
): boolean {
|
|
149
|
+
// Direct/legacy callers have no catalog-backed ingress policy to enforce.
|
|
150
|
+
// MulticastManager supplies a populated context whenever packetIds are selected.
|
|
151
|
+
return ingress === undefined || ingress.tracks.some((track) => track.packetId === packetId);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Callback for decoded video/audio frames. */
|
|
155
|
+
export type FrameCallback = (data: Uint8Array, meta: RoutedFrameMeta) => void;
|
|
156
|
+
|
|
157
|
+
/** Metadata accompanying an accepted raw MMTP source packet. */
|
|
158
|
+
export type RoutedPacketMeta = {
|
|
159
|
+
packetId: number;
|
|
160
|
+
packetSequenceNumber: number;
|
|
161
|
+
/** True when FEC reconstructed this source packet. */
|
|
162
|
+
recovered: boolean;
|
|
163
|
+
/** True when the packet has no trailing Source FEC Payload ID. */
|
|
164
|
+
skipFecPayloadId: boolean;
|
|
165
|
+
/** Immutable catalog identity supplied by the owning ingress subscription. */
|
|
166
|
+
catalogTrackName?: string;
|
|
167
|
+
/**
|
|
168
|
+
* Strict provenance for the fragments this packet produces. Set for
|
|
169
|
+
* FEC-recovered packets, which carry no `source` of their own but inherit
|
|
170
|
+
* their FEC block's symbol provenance.
|
|
171
|
+
*/
|
|
172
|
+
deliveryProvenance?: MmtpDeliveryProvenance;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/** Callback for accepted raw MMTP source packets. */
|
|
176
|
+
export type TrackPacketCallback = (packet: Uint8Array, meta: RoutedPacketMeta) => void;
|
|
177
|
+
|
|
178
|
+
type PendingRecoveredVideoFrame = {
|
|
179
|
+
data: Uint8Array;
|
|
180
|
+
meta: RoutedFrameMeta;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
type PendingRecoveredAudioFrame = {
|
|
184
|
+
data: Uint8Array;
|
|
185
|
+
meta: RoutedFrameMeta;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
type DirectAudioCursor = {
|
|
189
|
+
timestamp: number;
|
|
190
|
+
sequenceNumber: number;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const DIRECT_RECOVERED_VIDEO_QUEUE_LIMIT = 512;
|
|
194
|
+
const DIRECT_RECOVERED_AUDIO_QUEUE_LIMIT = 512;
|
|
195
|
+
|
|
196
|
+
type ParserDiagnostics = {
|
|
197
|
+
packets: number;
|
|
198
|
+
frames: number;
|
|
199
|
+
initSegments: number;
|
|
200
|
+
noFragment: number;
|
|
201
|
+
fragments: number;
|
|
202
|
+
reassembled: number;
|
|
203
|
+
pendingBuffers: number;
|
|
204
|
+
pendingFragments: number;
|
|
205
|
+
pendingSamples?: unknown[];
|
|
206
|
+
lastPacketLength: number;
|
|
207
|
+
lastSequenceNumber: number;
|
|
208
|
+
lastFecSsId?: number;
|
|
209
|
+
lastFrameTimestamp?: number;
|
|
210
|
+
directAudioSourceEmitted: number;
|
|
211
|
+
directAudioSourceRegressions: number;
|
|
212
|
+
directAudioRecoveredQueued: number;
|
|
213
|
+
directAudioRecoveredDrained: number;
|
|
214
|
+
directAudioRecoveredStaleDropped: number;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* FECManager interface — the contract MmtpRouter expects for FEC
|
|
219
|
+
* source/repair symbol processing and block lifecycle events.
|
|
220
|
+
*
|
|
221
|
+
* Concrete implementation: `FECManager` class in `./fec-manager.ts`.
|
|
222
|
+
* MmtpRouter only interacts with FEC through this interface.
|
|
223
|
+
*/
|
|
224
|
+
export interface FECManagerLike {
|
|
225
|
+
/** Feed a source symbol to the FEC decoder for a given track.
|
|
226
|
+
* Returns recovered block bytes if recovery completes, else null.
|
|
227
|
+
*
|
|
228
|
+
* Optional `alta` carries the detached ALTA authenticator extracted from
|
|
229
|
+
* the MMTP header-extension trailer (08-07 Path 2) — present on signed
|
|
230
|
+
* source packets, absent on relay-reassembled / unsigned streams. When
|
|
231
|
+
* provided, the fec-worker runs verify_detached instead of counting the
|
|
232
|
+
* symbol as `altaPending`. */
|
|
233
|
+
feedSourceSymbol(
|
|
234
|
+
packetId: number,
|
|
235
|
+
ssId: number,
|
|
236
|
+
symbolData: Uint8Array,
|
|
237
|
+
alta?: { auth: Uint8Array; payloadEnd: number },
|
|
238
|
+
catalogTrackName?: string,
|
|
239
|
+
): Uint8Array | null;
|
|
240
|
+
|
|
241
|
+
/** Feed a repair symbol to the FEC decoder for a given track.
|
|
242
|
+
* Returns recovered block bytes if recovery completes, else null. */
|
|
243
|
+
feedRepairSymbol(
|
|
244
|
+
packetId: number,
|
|
245
|
+
ssStart: number,
|
|
246
|
+
ssbLength: number,
|
|
247
|
+
rsId: number,
|
|
248
|
+
repairData: Uint8Array,
|
|
249
|
+
catalogTrackName?: string,
|
|
250
|
+
): Uint8Array | null;
|
|
251
|
+
|
|
252
|
+
/** Record a transport symbol for first-arrival-wins dedup and path counters. */
|
|
253
|
+
feedSymbolFromSource(
|
|
254
|
+
packetId: number,
|
|
255
|
+
sbn: number,
|
|
256
|
+
esi: number,
|
|
257
|
+
data: Uint8Array,
|
|
258
|
+
source: "multicast" | "moq",
|
|
259
|
+
catalogTrackName?: string,
|
|
260
|
+
): boolean;
|
|
261
|
+
|
|
262
|
+
/** Strict provenance for a block: 'multicast' only when no symbol arrived
|
|
263
|
+
* over MoQ. Recovered bytes inherit it, since a recovered symbol is a
|
|
264
|
+
* function of every symbol that entered the block. */
|
|
265
|
+
blockDeliveryProvenance?(
|
|
266
|
+
sbn: number,
|
|
267
|
+
packetId: number,
|
|
268
|
+
catalogTrackName?: string,
|
|
269
|
+
): MmtpDeliveryProvenance;
|
|
270
|
+
|
|
271
|
+
/** Track a source symbol arrival for block completion counting.
|
|
272
|
+
* Refreshes the inactivity deadline and marks clean when K arrive. */
|
|
273
|
+
trackBlock(
|
|
274
|
+
sbn: number,
|
|
275
|
+
packetId: number,
|
|
276
|
+
catalogTrackName?: string,
|
|
277
|
+
esi?: number,
|
|
278
|
+
): void;
|
|
279
|
+
|
|
280
|
+
/** Refresh the block inactivity deadline after accepted repair progress. */
|
|
281
|
+
noteBlockActivity?(sbn: number, packetId: number, catalogTrackName?: string): void;
|
|
282
|
+
|
|
283
|
+
/** Cancel block timeout (block completed via clean or FEC recovery). */
|
|
284
|
+
cancelBlockTimeout(sbn: number, packetId?: number, catalogTrackName?: string): void;
|
|
285
|
+
|
|
286
|
+
/** Check if a block is already completed (clean or recovered). */
|
|
287
|
+
isBlockCompleted(sbn: number, packetId?: number, catalogTrackName?: string): boolean;
|
|
288
|
+
|
|
289
|
+
/** Mark a block as completed externally (after routing recovered data). */
|
|
290
|
+
markBlockCompleted(sbn: number, packetId?: number, catalogTrackName?: string): void;
|
|
291
|
+
|
|
292
|
+
/** Increment blocksRecovered stat (called after routing recovery). */
|
|
293
|
+
countRecovery(packetId?: number): void;
|
|
294
|
+
|
|
295
|
+
/** FEC source symbol count K per block. */
|
|
296
|
+
readonly k: number;
|
|
297
|
+
kForPacket?(packetId: number): number;
|
|
298
|
+
|
|
299
|
+
/** FEC symbol size T in bytes (0 = unknown / no FEC). */
|
|
300
|
+
readonly symbolSize: number;
|
|
301
|
+
symbolSizeForPacket?(packetId: number): number;
|
|
302
|
+
|
|
303
|
+
/** Update framerate — used to refine FEC block timeout. */
|
|
304
|
+
setFramerate(fps: number): void;
|
|
305
|
+
|
|
306
|
+
/** Per-block state snapshot for FecBlockGrid visualization. */
|
|
307
|
+
getBlockStates(): Array<{
|
|
308
|
+
sbn: number;
|
|
309
|
+
packetId?: number;
|
|
310
|
+
state: "collecting" | "complete" | "failed";
|
|
311
|
+
sourceCount: number;
|
|
312
|
+
k: number;
|
|
313
|
+
progress: number;
|
|
314
|
+
}>;
|
|
315
|
+
|
|
316
|
+
/** FEC statistics snapshot. */
|
|
317
|
+
readonly fecStats: {
|
|
318
|
+
sourcePackets: number;
|
|
319
|
+
repairPackets: number;
|
|
320
|
+
blocksClean: number;
|
|
321
|
+
blocksRecovered: number;
|
|
322
|
+
blocksFailed: number;
|
|
323
|
+
greenFillFrames: number;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// ── Event hooks (set by MmtpRouter in constructor) ──
|
|
327
|
+
|
|
328
|
+
/** Called when a block receives its first source symbol. */
|
|
329
|
+
onBlockStarted:
|
|
330
|
+
| ((sbn: number, packetId: number, catalogTrackName?: string) => void)
|
|
331
|
+
| null;
|
|
332
|
+
|
|
333
|
+
/** Called when a block completes cleanly (all K sources arrived). */
|
|
334
|
+
onBlockComplete:
|
|
335
|
+
| ((sbn: number, packetId: number, catalogTrackName?: string) => void)
|
|
336
|
+
| null;
|
|
337
|
+
|
|
338
|
+
/** Called when a block times out (unrecoverable). */
|
|
339
|
+
onBlockFailed:
|
|
340
|
+
| ((sbn: number, packetId: number, catalogTrackName?: string) => void)
|
|
341
|
+
| null;
|
|
342
|
+
|
|
343
|
+
/** Dispose timers and state. */
|
|
344
|
+
dispose(): void;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ─── MmtpRouter ─────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
export interface ManifestIdentityResolutionInput {
|
|
350
|
+
packetId: number;
|
|
351
|
+
sbn: number;
|
|
352
|
+
esi: number;
|
|
353
|
+
catalogTrackName?: string;
|
|
354
|
+
recovered: boolean;
|
|
355
|
+
sourceSymbolFormat: "full-mmtp-packet" | "fec-padded-recovery";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export type ManifestIdentityResolver = (
|
|
359
|
+
input: ManifestIdentityResolutionInput,
|
|
360
|
+
) => ManifestObjectIdentity | null;
|
|
361
|
+
|
|
362
|
+
export class MmtpRouter {
|
|
363
|
+
readonly #parser: MmtpContainerParser;
|
|
364
|
+
// Source AND FEC-recovered MFU fragments share ONE reassembler — the parser's
|
|
365
|
+
// (see #parser.reassembler). A keyframe split between source-received and
|
|
366
|
+
// FEC-recovered fragments must reassemble in a single buffer (BLO-8151).
|
|
367
|
+
readonly #fecManager: FECManagerLike | null;
|
|
368
|
+
#defaultFecMapper: FecBlockMapper | null;
|
|
369
|
+
#fecMappersByPacketId = new Map<number, FecBlockMapper>();
|
|
370
|
+
readonly #manifestVerifier: ManifestVerifier | null;
|
|
371
|
+
readonly #ownsManifestVerifier: boolean;
|
|
372
|
+
readonly #resolveManifestIdentity:
|
|
373
|
+
| ((
|
|
374
|
+
packetId: number,
|
|
375
|
+
sbn: number,
|
|
376
|
+
esi: number,
|
|
377
|
+
catalogTrackName?: string,
|
|
378
|
+
) => ManifestObjectIdentity | null)
|
|
379
|
+
| null;
|
|
380
|
+
readonly #resolveManifestIdentityForRoute: ManifestIdentityResolver | null;
|
|
381
|
+
#manifestRecoveryQueues = new Map<
|
|
382
|
+
string,
|
|
383
|
+
Array<{
|
|
384
|
+
blockId: number;
|
|
385
|
+
result: ProcessFecBlockResult;
|
|
386
|
+
ownerPacketId: number;
|
|
387
|
+
catalogTrackName?: string;
|
|
388
|
+
}>
|
|
389
|
+
>();
|
|
390
|
+
|
|
391
|
+
// Temp state set by #processMMTP before calling parser.feed(),
|
|
392
|
+
// read by the parser's onFrame callback for CMAF assembly + dedup.
|
|
393
|
+
#currentFecInfo:
|
|
394
|
+
| { sbn: number; esi: number; oversized?: boolean }
|
|
395
|
+
| undefined;
|
|
396
|
+
#currentSeqNum = 0;
|
|
397
|
+
|
|
398
|
+
// Track type registry: which packetIds are video vs audio (from catalog)
|
|
399
|
+
#videoPacketIds = new Set<number>();
|
|
400
|
+
#audioPacketIds = new Set<number>();
|
|
401
|
+
#catalogTrackNamesByPacketId = new Map<number, Set<string>>();
|
|
402
|
+
|
|
403
|
+
// Per-track codec string for keyframe detection (e.g. 'avc1.42001e', 'av01.0.04M.08')
|
|
404
|
+
#trackCodecs = new Map<number, string>();
|
|
405
|
+
|
|
406
|
+
// Per-track frame callbacks keyed by packetId — used for ABR / multi-track routing
|
|
407
|
+
#trackCallbacks = new Map<
|
|
408
|
+
number,
|
|
409
|
+
(data: Uint8Array, meta: { timestamp: number; keyframe: boolean }) => void
|
|
410
|
+
>();
|
|
411
|
+
#trackPacketCallbacks = new Map<number, TrackPacketCallback>();
|
|
412
|
+
// Cached resolved callbacks: per-track + legacy composite per packetId.
|
|
413
|
+
// Invalidated when any callback is registered (onTrackFrame, onVideoFrame, onAudioFrame).
|
|
414
|
+
#resolvedCallbacks = new Map<number, FrameCallback>();
|
|
415
|
+
|
|
416
|
+
// Per-ingress-stream sync flag: suppress direct frame emission until the first complete
|
|
417
|
+
// FEC block confirms we've joined mid-stream cleanly. Prevents corrupt partial
|
|
418
|
+
// frames from reaching the decoder on multicast mid-stream join. Catalog
|
|
419
|
+
// identity keeps renditions that reuse a packet ID from opening each other's gate.
|
|
420
|
+
#trackSynced = new Set<string>();
|
|
421
|
+
#droppedPreSyncVideo = new Set<string>();
|
|
422
|
+
|
|
423
|
+
// Dedup: packetId-scoped packetSequenceNumbers already received via direct source path.
|
|
424
|
+
// Recovered FEC blocks contain all K source symbols, so skip direct-path
|
|
425
|
+
// duplicates even when the source frame is still buffered by parser reordering.
|
|
426
|
+
#sourcePktSeqs = new Set<string>();
|
|
427
|
+
#routedPacketSeqs = new Set<string>();
|
|
428
|
+
|
|
429
|
+
// Live WebCodecs callbacks cannot accept late recovered inter-frame video.
|
|
430
|
+
// MSE recovery routes through CmafBlockAssembler and can reorder by block; the
|
|
431
|
+
// direct callback path has to preserve decode order before frames leave libmmt.
|
|
432
|
+
#latestVideoSeqByStream = new Map<string, number>();
|
|
433
|
+
#lastDirectSourceVideoTsByStream = new Map<string, number>();
|
|
434
|
+
#lastDirectEmittedVideoTsByStream = new Map<string, number>();
|
|
435
|
+
#pendingDirectRecoveredVideoByStream = new Map<string, Map<string, PendingRecoveredVideoFrame>>();
|
|
436
|
+
// AudioDecoder input must remain monotonic too. A repair that completes after
|
|
437
|
+
// the live source cursor passed its AAC access unit is useful for FEC
|
|
438
|
+
// accounting, but injecting it into the decoder can stall PCM output.
|
|
439
|
+
#lastDirectSourceAudioCursorByStream = new Map<string, DirectAudioCursor>();
|
|
440
|
+
#lastDirectEmittedAudioCursorByStream = new Map<string, DirectAudioCursor>();
|
|
441
|
+
#pendingDirectRecoveredAudioByStream = new Map<string, Map<string, PendingRecoveredAudioFrame>>();
|
|
442
|
+
|
|
443
|
+
// Per-track CMAF block assemblers for MSE mode. When active, frames are buffered per-SBN
|
|
444
|
+
// and emitted as multi-sample CMAF segments on block completion instead of per-frame.
|
|
445
|
+
#mseAssemblers = new Map<string, CmafBlockAssembler>();
|
|
446
|
+
|
|
447
|
+
// Callbacks (legacy — used when per-track callbacks are not registered)
|
|
448
|
+
#onVideoFrame: FrameCallback | null = null;
|
|
449
|
+
#onAudioFrame: FrameCallback | null = null;
|
|
450
|
+
|
|
451
|
+
// Init segment (ftyp+moov) from MMTP MPU metadata — needed for MSE SourceBuffer
|
|
452
|
+
#onInitSegment: ((data: Uint8Array) => void) | null = null;
|
|
453
|
+
|
|
454
|
+
// Per-track init segment callbacks keyed by packetId
|
|
455
|
+
#trackInitCallbacks = new Map<number, (data: Uint8Array) => void>();
|
|
456
|
+
// Track whether init has been delivered per physical stream — prevent duplicates
|
|
457
|
+
#trackInitDelivered = new Set<string>();
|
|
458
|
+
|
|
459
|
+
// FEC block lifecycle — backend decides failed handling: WebCodecs=freeze, MSE=green fill
|
|
460
|
+
#onBlockStarted: ((sbn: number, packetId: number) => void) | null = null;
|
|
461
|
+
/** Subscriber for unrecoverable FEC blocks. Mirrors #onBlockStarted so
|
|
462
|
+
* players can drive decoder recovery when a block times out; the internal
|
|
463
|
+
* MSE/delta-role handling below stays independent of this subscriber. */
|
|
464
|
+
#onBlockFailed:
|
|
465
|
+
| ((sbn: number, packetId: number, catalogTrackName?: string) => void)
|
|
466
|
+
| null = null;
|
|
467
|
+
// ── Phase 14: per-role packetId dispatch (D-26, D-27, D-28) ───────
|
|
468
|
+
/** Maps packetId → TrackRole. Default: 'single' when absent (legacy path). */
|
|
469
|
+
#packetIdRoles: Map<number, TrackRole> = new Map();
|
|
470
|
+
/** Explicit catalog ownership for dedicated repair-track packet IDs. */
|
|
471
|
+
#repairOwnersByPacketId = new Map<number, Set<number>>();
|
|
472
|
+
/** Callback fired when a delta-track FEC block completes or fails. */
|
|
473
|
+
#onDeltaBlockCompleteCb:
|
|
474
|
+
| ((packetId: number, failed: boolean) => void)
|
|
475
|
+
| null = null;
|
|
476
|
+
|
|
477
|
+
// Debug counters
|
|
478
|
+
#pidCounts: Record<number, number> = {};
|
|
479
|
+
#repairPidCounts: Record<number, number> = {};
|
|
480
|
+
#moqRepairPidCounts: Record<number, number> = {};
|
|
481
|
+
#moqRepairOwnerCounts: Record<number, number> = {};
|
|
482
|
+
#parserDiagnostics = new Map<string, ParserDiagnostics>();
|
|
483
|
+
|
|
484
|
+
// ── Keyframe loss detection state ───────────────────────────────
|
|
485
|
+
/** Catalog-derived keyframe loss timeout (ms). null = not configured (throws on video track registration). */
|
|
486
|
+
#keyframeLostTimeoutMs: number | null;
|
|
487
|
+
/** Active keyframe loss timer handle. Single timer per video track — cleared on each rapFlag and on dispose(). */
|
|
488
|
+
#keyframeLostTimer: ReturnType<typeof setTimeout> | null = null;
|
|
489
|
+
/** Monotonic counter incremented on each emitted video keyframe. Tracks current GOP position. */
|
|
490
|
+
#lastKeyframeGroupId = 0;
|
|
491
|
+
/** Callback fired when no complete keyframe is emitted within keyframeLostTimeoutMs. Argument: next expected groupId. */
|
|
492
|
+
#onKeyframeLost: ((expectedGroupId: number) => void) | null = null;
|
|
493
|
+
|
|
494
|
+
#disposed = false;
|
|
495
|
+
|
|
496
|
+
constructor(config?: {
|
|
497
|
+
fecManager?: FECManagerLike;
|
|
498
|
+
fecMapper?: FecBlockMapper;
|
|
499
|
+
/** MFU reassembly timeout (ms) for FEC recovery path. Derive from FEC interleaveDepthMs. */
|
|
500
|
+
mfuTimeoutMs?: number;
|
|
501
|
+
reassemblyPolicy?: MfuReassemblyPolicy;
|
|
502
|
+
reassembler?: MfuReassemblerBackend;
|
|
503
|
+
maxBufferedMfus?: number;
|
|
504
|
+
/** Catalog-derived keyframe loss timeout (ms). Derive from explicit
|
|
505
|
+
* keyframe/group duration; do not use FEC sourceSymbols as GOP length. */
|
|
506
|
+
keyframeLostTimeoutMs?: number;
|
|
507
|
+
manifestVerifier?: ManifestVerifier;
|
|
508
|
+
/** Existing behavior is router ownership; shared browser plans opt into caller ownership. */
|
|
509
|
+
manifestVerifierOwnership?: "caller" | "router";
|
|
510
|
+
resolveManifestIdentity?: (
|
|
511
|
+
packetId: number,
|
|
512
|
+
sbn: number,
|
|
513
|
+
esi: number,
|
|
514
|
+
catalogTrackName?: string,
|
|
515
|
+
) => ManifestObjectIdentity | null;
|
|
516
|
+
resolveManifestIdentityForRoute?: ManifestIdentityResolver;
|
|
517
|
+
/** Internal terminal-failure sink owned by the composing transport. */
|
|
518
|
+
onBlockFailed?: (
|
|
519
|
+
sbn: number,
|
|
520
|
+
packetId: number,
|
|
521
|
+
catalogTrackName?: string,
|
|
522
|
+
) => void;
|
|
523
|
+
}) {
|
|
524
|
+
this.#keyframeLostTimeoutMs = config?.keyframeLostTimeoutMs ?? null;
|
|
525
|
+
this.#fecManager = config?.fecManager ?? null;
|
|
526
|
+
this.#defaultFecMapper = config?.fecMapper ?? null;
|
|
527
|
+
this.#manifestVerifier = config?.manifestVerifier ?? null;
|
|
528
|
+
this.#ownsManifestVerifier = config?.manifestVerifierOwnership !== "caller";
|
|
529
|
+
this.#resolveManifestIdentity = config?.resolveManifestIdentity ?? null;
|
|
530
|
+
this.#resolveManifestIdentityForRoute = config?.resolveManifestIdentityForRoute ?? null;
|
|
531
|
+
|
|
532
|
+
// Create MMTP parser — handles MFU extraction, reassembly, keyframe detection.
|
|
533
|
+
// Its MFUReassembler is the single reassembler for BOTH the source path
|
|
534
|
+
// (parser.feed) and the FEC-recovery path (#routeRecoveredBlock), so a frame
|
|
535
|
+
// split across both completes (BLO-8151). mfuTimeoutMs threads the
|
|
536
|
+
// catalog-derived timeout through.
|
|
537
|
+
this.#parser = new MmtpContainerParser({
|
|
538
|
+
mfuTimeoutMs: config?.mfuTimeoutMs,
|
|
539
|
+
reassemblyPolicy: config?.reassemblyPolicy,
|
|
540
|
+
reassembler: config?.reassembler,
|
|
541
|
+
maxBufferedMfus: config?.maxBufferedMfus,
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Wire parser init callback → router's init delivery (dedup).
|
|
545
|
+
this.#parser.onInit((data, packetId, catalogTrackName) => {
|
|
546
|
+
this.#diagFor(packetId, catalogTrackName).initSegments++;
|
|
547
|
+
this.#deliverInit(packetId, data, catalogTrackName);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Wire parser frame callback → router's callback routing + CMAF assembly.
|
|
551
|
+
this.#parser.onFrame((data, meta) => {
|
|
552
|
+
const packetId = meta.packetId;
|
|
553
|
+
const diag = this.#diagFor(packetId, meta.catalogTrackName);
|
|
554
|
+
diag.frames++;
|
|
555
|
+
diag.lastFrameTimestamp = meta.timestamp;
|
|
556
|
+
const isVideo = this.#classifyPacketId(packetId) === true;
|
|
557
|
+
const frameMeta: RoutedFrameMeta = {
|
|
558
|
+
timestamp: meta.timestamp,
|
|
559
|
+
keyframe: isVideo ? meta.keyframe : true,
|
|
560
|
+
packetId,
|
|
561
|
+
packetSequenceNumber: meta.packetSequenceNumber ?? this.#currentSeqNum,
|
|
562
|
+
...(meta.authoredTiming === undefined
|
|
563
|
+
? {}
|
|
564
|
+
: { authoredTiming: meta.authoredTiming }),
|
|
565
|
+
...(meta.packetSequenceNumbers === undefined
|
|
566
|
+
? {}
|
|
567
|
+
: { packetSequenceNumbers: meta.packetSequenceNumbers }),
|
|
568
|
+
...(meta.repairedPacketSequenceNumbers === undefined
|
|
569
|
+
? {}
|
|
570
|
+
: { repairedPacketSequenceNumbers: meta.repairedPacketSequenceNumbers }),
|
|
571
|
+
...(meta.mpuSequenceNumber === undefined
|
|
572
|
+
? {}
|
|
573
|
+
: { mpuSequenceNumber: meta.mpuSequenceNumber }),
|
|
574
|
+
...(meta.catalogTrackName === undefined
|
|
575
|
+
? {}
|
|
576
|
+
: { catalogTrackName: meta.catalogTrackName }),
|
|
577
|
+
};
|
|
578
|
+
if (meta.source) frameMeta.source = meta.source;
|
|
579
|
+
// Forward whenever present, not when truthy: 'unknown' is a verdict the
|
|
580
|
+
// parser reached, not a value to drop.
|
|
581
|
+
if (meta.deliveryProvenance !== undefined)
|
|
582
|
+
frameMeta.deliveryProvenance = meta.deliveryProvenance;
|
|
583
|
+
if (meta.recovered) frameMeta.recovered = true;
|
|
584
|
+
const sourceDiscontinuity = this.#opensFecSync(
|
|
585
|
+
packetId,
|
|
586
|
+
frameMeta.keyframe,
|
|
587
|
+
frameMeta.catalogTrackName,
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
// Route to MSE assembler if active for this track.
|
|
591
|
+
const assembler = this.#mseAssemblerFor(packetId, frameMeta.catalogTrackName);
|
|
592
|
+
if (assembler) {
|
|
593
|
+
const fecInfo = this.#currentFecInfo;
|
|
594
|
+
if (fecInfo && !fecInfo.oversized) {
|
|
595
|
+
assembler.addFrame(
|
|
596
|
+
fecInfo.sbn,
|
|
597
|
+
fecInfo.esi,
|
|
598
|
+
data,
|
|
599
|
+
frameMeta.timestamp,
|
|
600
|
+
frameMeta.keyframe,
|
|
601
|
+
frameMeta.catalogTrackName,
|
|
602
|
+
frameMeta.mpuSequenceNumber,
|
|
603
|
+
frameMeta.packetSequenceNumber,
|
|
604
|
+
frameMeta.packetSequenceNumbers,
|
|
605
|
+
frameMeta.repairedPacketSequenceNumbers,
|
|
606
|
+
frameMeta.authoredTiming,
|
|
607
|
+
frameMeta.deliveryProvenance,
|
|
608
|
+
);
|
|
609
|
+
} else {
|
|
610
|
+
assembler.addBypassFrame(
|
|
611
|
+
data,
|
|
612
|
+
frameMeta.timestamp,
|
|
613
|
+
frameMeta.keyframe,
|
|
614
|
+
frameMeta.catalogTrackName,
|
|
615
|
+
frameMeta.mpuSequenceNumber,
|
|
616
|
+
frameMeta.packetSequenceNumber,
|
|
617
|
+
frameMeta.packetSequenceNumbers,
|
|
618
|
+
frameMeta.repairedPacketSequenceNumbers,
|
|
619
|
+
frameMeta.authoredTiming,
|
|
620
|
+
frameMeta.deliveryProvenance,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (!this.#shouldEmitFrame(
|
|
626
|
+
packetId,
|
|
627
|
+
frameMeta.keyframe,
|
|
628
|
+
frameMeta.catalogTrackName,
|
|
629
|
+
)) return;
|
|
630
|
+
if (sourceDiscontinuity) frameMeta.sourceDiscontinuity = true;
|
|
631
|
+
|
|
632
|
+
this.#noteEmittedKeyframe(packetId, frameMeta.keyframe);
|
|
633
|
+
|
|
634
|
+
// The emitted-sequence high-water mark (#noteEmittedVideoSeq) is recorded
|
|
635
|
+
// at the actual emit inside #emitDirectVideo, not here — a recovered frame
|
|
636
|
+
// can be queued ahead of the source cursor, so it must track real emits.
|
|
637
|
+
|
|
638
|
+
// Route to per-track or legacy callback. Even when an MSE block-assembler
|
|
639
|
+
// is registered, this callback still fires — downstream consumers
|
|
640
|
+
// (`mc.onVideoFrame` → takeover detection, multicast frame counter,
|
|
641
|
+
// `confirmVideo`/`confirmAudio`) need every reassembled frame. The
|
|
642
|
+
// SourceBuffer-write side of this callback path is suppressed inside
|
|
643
|
+
// `MseBackend.feedVideo`/`feedAudio` when the assembler owns the track,
|
|
644
|
+
// which is configured via `MseBackend.setAssemblerOwns('video' | 'audio')`.
|
|
645
|
+
// Prevents the dual-CMAF-timeline collision (BLO-3035 layer 3).
|
|
646
|
+
const cb = this.#getFrameCallback(packetId);
|
|
647
|
+
if (isVideo && !assembler) {
|
|
648
|
+
if (frameMeta.recovered) {
|
|
649
|
+
this.#routeDirectRecoveredVideo(packetId, data, frameMeta, cb);
|
|
650
|
+
} else {
|
|
651
|
+
this.#routeDirectSourceVideo(packetId, data, frameMeta, cb);
|
|
652
|
+
}
|
|
653
|
+
} else if (!isVideo && !assembler) {
|
|
654
|
+
if (frameMeta.recovered) {
|
|
655
|
+
this.#routeDirectRecoveredAudio(packetId, data, frameMeta, cb);
|
|
656
|
+
} else {
|
|
657
|
+
this.#routeDirectSourceAudio(packetId, data, frameMeta, cb);
|
|
658
|
+
}
|
|
659
|
+
} else {
|
|
660
|
+
cb?.(data, frameMeta);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// Wire up FECManager block lifecycle events.
|
|
665
|
+
// NOTE: Recovery routing is handled from feedSourceSymbol/feedRepairSymbol
|
|
666
|
+
// return values in #processMMTP — no onBlockRecovered event needed.
|
|
667
|
+
if (this.#fecManager) {
|
|
668
|
+
this.#fecManager.onBlockStarted = (
|
|
669
|
+
sbn: number,
|
|
670
|
+
packetId: number,
|
|
671
|
+
_catalogTrackName?: string,
|
|
672
|
+
) => {
|
|
673
|
+
this.#onBlockStarted?.(sbn, packetId);
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
this.#fecManager.onBlockComplete = (
|
|
677
|
+
sbn: number,
|
|
678
|
+
packetId: number,
|
|
679
|
+
catalogTrackName?: string,
|
|
680
|
+
) => {
|
|
681
|
+
this.#mseAssemblerFor(packetId, catalogTrackName)?.completeBlock(sbn);
|
|
682
|
+
// Phase 14 D-26: fan-out to delta callback when delta-role packetId completes.
|
|
683
|
+
if (
|
|
684
|
+
this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) &&
|
|
685
|
+
this.#packetIdRoles.get(packetId) === "delta"
|
|
686
|
+
) {
|
|
687
|
+
this.#onDeltaBlockCompleteCb?.(packetId, false); // failed=false
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
this.#fecManager.onBlockFailed = (
|
|
692
|
+
sbn: number,
|
|
693
|
+
packetId: number,
|
|
694
|
+
catalogTrackName?: string,
|
|
695
|
+
) => {
|
|
696
|
+
this.#mseAssemblerFor(packetId, catalogTrackName)?.failBlock(sbn);
|
|
697
|
+
if (
|
|
698
|
+
this.#isCatalogTrackUnambiguous(packetId, catalogTrackName) &&
|
|
699
|
+
this.#packetIdRoles.get(packetId) === "delta"
|
|
700
|
+
) {
|
|
701
|
+
this.#onDeltaBlockCompleteCb?.(packetId, true); // failed=true
|
|
702
|
+
}
|
|
703
|
+
config?.onBlockFailed?.(sbn, packetId, catalogTrackName);
|
|
704
|
+
this.#onBlockFailed?.(sbn, packetId, catalogTrackName);
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// ── Accessors ───────────────────────────────────────────────────
|
|
710
|
+
|
|
711
|
+
get fecManager(): FECManagerLike | null {
|
|
712
|
+
return this.#fecManager;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
get reassembler(): MfuReassemblerBackend {
|
|
716
|
+
return this.#parser.reassembler;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
getDiagnostics(): Record<number, ParserDiagnostics> {
|
|
720
|
+
const out: Record<number, ParserDiagnostics> = {};
|
|
721
|
+
for (const [key, diag] of this.#parserDiagnostics) {
|
|
722
|
+
const packetId = this.#packetIdFromStreamKey(key);
|
|
723
|
+
const existing = out[packetId];
|
|
724
|
+
if (!existing) {
|
|
725
|
+
out[packetId] = { ...diag };
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const latest =
|
|
729
|
+
diag.lastSequenceNumber >= existing.lastSequenceNumber ? diag : existing;
|
|
730
|
+
out[packetId] = {
|
|
731
|
+
packets: existing.packets + diag.packets,
|
|
732
|
+
frames: existing.frames + diag.frames,
|
|
733
|
+
initSegments: existing.initSegments + diag.initSegments,
|
|
734
|
+
noFragment: existing.noFragment + diag.noFragment,
|
|
735
|
+
fragments: existing.fragments + diag.fragments,
|
|
736
|
+
reassembled: existing.reassembled + diag.reassembled,
|
|
737
|
+
pendingBuffers: existing.pendingBuffers + diag.pendingBuffers,
|
|
738
|
+
pendingFragments: existing.pendingFragments + diag.pendingFragments,
|
|
739
|
+
directAudioSourceEmitted:
|
|
740
|
+
existing.directAudioSourceEmitted + diag.directAudioSourceEmitted,
|
|
741
|
+
directAudioSourceRegressions:
|
|
742
|
+
existing.directAudioSourceRegressions + diag.directAudioSourceRegressions,
|
|
743
|
+
directAudioRecoveredQueued:
|
|
744
|
+
existing.directAudioRecoveredQueued + diag.directAudioRecoveredQueued,
|
|
745
|
+
directAudioRecoveredDrained:
|
|
746
|
+
existing.directAudioRecoveredDrained + diag.directAudioRecoveredDrained,
|
|
747
|
+
directAudioRecoveredStaleDropped:
|
|
748
|
+
existing.directAudioRecoveredStaleDropped + diag.directAudioRecoveredStaleDropped,
|
|
749
|
+
pendingSamples: [
|
|
750
|
+
...((existing.pendingSamples as unknown[]) ?? []),
|
|
751
|
+
...((diag.pendingSamples as unknown[]) ?? []),
|
|
752
|
+
],
|
|
753
|
+
lastPacketLength: latest.lastPacketLength,
|
|
754
|
+
lastSequenceNumber: latest.lastSequenceNumber,
|
|
755
|
+
...(latest.lastFecSsId === undefined ? {} : { lastFecSsId: latest.lastFecSsId }),
|
|
756
|
+
...(latest.lastFrameTimestamp === undefined
|
|
757
|
+
? {}
|
|
758
|
+
: { lastFrameTimestamp: latest.lastFrameTimestamp }),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
return out;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ── Track configuration ─────────────────────────────────────────
|
|
765
|
+
|
|
766
|
+
/** Register callback for keyframe loss detection.
|
|
767
|
+
* Fires when no complete keyframe is emitted within keyframeLostTimeoutMs.
|
|
768
|
+
* Argument: next expected groupId (lastKeyframeGroupId + 1). */
|
|
769
|
+
onKeyframeLost(cb: (expectedGroupId: number) => void): void {
|
|
770
|
+
this.#onKeyframeLost = cb;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Set the keyframe loss timeout after construction.
|
|
775
|
+
* Must be called BEFORE setPacketIdType('video').
|
|
776
|
+
* Derive from explicit keyframe/group duration:
|
|
777
|
+
* Math.ceil(groupDurationMs * KEYFRAME_LOST_TIMEOUT_SLACK).
|
|
778
|
+
*/
|
|
779
|
+
setKeyframeLostTimeoutMs(ms: number): void {
|
|
780
|
+
this.#keyframeLostTimeoutMs = ms;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Update MFU reassembly timeout. Source and FEC-recovery paths share the
|
|
784
|
+
* parser's single reassembler (BLO-8151), so one update covers both. */
|
|
785
|
+
setMfuTimeoutMs(ms: number): void {
|
|
786
|
+
if (ms <= 0) throw new Error("[MmtpRouter] mfuTimeoutMs must be > 0");
|
|
787
|
+
this.#parser.setMfuTimeoutMs(ms);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** Update parser frame reordering for unordered multicast delivery.
|
|
791
|
+
* Callers derive this from the catalog FEC block span (§5.1 — the
|
|
792
|
+
* interleave value IS the span): positive round-half-up of span / frameMs. */
|
|
793
|
+
setReorderWindow(windowSize: number): void {
|
|
794
|
+
this.#parser.setReorderWindow(windowSize);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** Register MMTP packetId → track type mapping.
|
|
798
|
+
* Call BEFORE packets arrive to ensure correct routing from the first packet.
|
|
799
|
+
* If keyframeLostTimeoutMs is not provided, Phase 13 keyframe repair is
|
|
800
|
+
* disabled (video still routes/decodes normally). framerate is OPTIONAL per
|
|
801
|
+
* draft-ietf-moq-catalogformat-01 §3.2.23, so we can't require the timeout. */
|
|
802
|
+
setPacketIdType(packetId: number, trackType: "video" | "audio"): void {
|
|
803
|
+
if (trackType === "video" && this.#keyframeLostTimeoutMs === null) {
|
|
804
|
+
console.warn(
|
|
805
|
+
"[MmtpRouter] keyframeLostTimeoutMs not configured — Phase 13 keyframe " +
|
|
806
|
+
"repair disabled for this track. Set it via setKeyframeLostTimeoutMs() " +
|
|
807
|
+
"or provide in constructor config to enable: " +
|
|
808
|
+
"Math.ceil(groupDurationMs * KEYFRAME_LOST_TIMEOUT_SLACK).",
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
if (trackType === "video") this.#videoPacketIds.add(packetId);
|
|
812
|
+
else this.#audioPacketIds.add(packetId);
|
|
813
|
+
// Sync track info to parser for codec-aware keyframe detection.
|
|
814
|
+
const codec = this.#trackCodecs.get(packetId) ?? "";
|
|
815
|
+
this.#parser.setTrack(packetId, { type: trackType, codec });
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** Register codec string for a packetId — enables codec-aware keyframe detection.
|
|
819
|
+
* Without this, only H.264 IDR (NAL type 5) is detected as keyframe fallback.
|
|
820
|
+
* AV1 (OBU_SEQUENCE_HEADER) and VP9 (frame_type bit) require the codec string. */
|
|
821
|
+
setTrackCodec(packetId: number, codec: string): void {
|
|
822
|
+
this.#trackCodecs.set(packetId, codec);
|
|
823
|
+
// Sync to parser. Determine track type from existing registration.
|
|
824
|
+
const trackType = this.#videoPacketIds.has(packetId)
|
|
825
|
+
? ("video" as const)
|
|
826
|
+
: this.#audioPacketIds.has(packetId)
|
|
827
|
+
? ("audio" as const)
|
|
828
|
+
: null;
|
|
829
|
+
if (trackType) {
|
|
830
|
+
this.#parser.setTrack(packetId, { type: trackType, codec });
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ── Frame callbacks ─────────────────────────────────────────────
|
|
835
|
+
|
|
836
|
+
/** @deprecated Use onTrackFrame(packetId, cb) for per-track routing */
|
|
837
|
+
onVideoFrame(cb: FrameCallback): void {
|
|
838
|
+
this.#onVideoFrame = cb;
|
|
839
|
+
this.#resolvedCallbacks.clear();
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** @deprecated Use onTrackFrame(packetId, cb) for per-track routing */
|
|
843
|
+
onAudioFrame(cb: FrameCallback): void {
|
|
844
|
+
this.#onAudioFrame = cb;
|
|
845
|
+
this.#resolvedCallbacks.clear();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** Register a frame callback for a specific MMTP packetId (track).
|
|
849
|
+
* When a legacy callback (onVideoFrame/onAudioFrame) also exists for this
|
|
850
|
+
* packetId's type, both fire: per-track first, then legacy. This ensures
|
|
851
|
+
* element.ts wiring (confirmAudio, feedMulticast) continues working when
|
|
852
|
+
* TrackPipeline registers per-track sources. */
|
|
853
|
+
onTrackFrame(
|
|
854
|
+
packetId: number,
|
|
855
|
+
cb: (
|
|
856
|
+
data: Uint8Array,
|
|
857
|
+
meta: {
|
|
858
|
+
timestamp: number;
|
|
859
|
+
keyframe: boolean;
|
|
860
|
+
packetId?: number;
|
|
861
|
+
packetSequenceNumber?: number;
|
|
862
|
+
},
|
|
863
|
+
) => void,
|
|
864
|
+
): void {
|
|
865
|
+
this.#trackCallbacks.set(packetId, cb);
|
|
866
|
+
this.#resolvedCallbacks.clear();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/** Register a raw MMTP source-packet callback for one track.
|
|
870
|
+
*
|
|
871
|
+
* Direct packets retain their original bytes, including a Source FEC Payload
|
|
872
|
+
* ID when present. FEC-recovered packets are the recovered source-symbol
|
|
873
|
+
* bytes and set `skipFecPayloadId` because that trailer is not part of the
|
|
874
|
+
* protected symbol. Repair packets are never delivered here.
|
|
875
|
+
*/
|
|
876
|
+
onTrackPacket(packetId: number, cb: TrackPacketCallback): void {
|
|
877
|
+
this.#trackPacketCallbacks.set(packetId, cb);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** Register an init segment callback (ftyp+moov from MMTP MPU metadata). */
|
|
881
|
+
onInitSegment(cb: (data: Uint8Array) => void): void {
|
|
882
|
+
this.#onInitSegment = cb;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** Register an init segment callback for a specific MMTP packetId.
|
|
886
|
+
* Called when FT=0 init arrives via direct source or FEC recovery. */
|
|
887
|
+
onTrackInit(packetId: number, cb: (data: Uint8Array) => void): void {
|
|
888
|
+
this.#trackInitCallbacks.set(packetId, cb);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// ── MSE integration ─────────────────────────────────────────────
|
|
892
|
+
|
|
893
|
+
/** Create a CMAF block assembler for MSE mode on a specific track.
|
|
894
|
+
* When active, source frames are buffered per FEC block and emitted as
|
|
895
|
+
* multi-sample CMAF segments instead of per-frame callbacks. */
|
|
896
|
+
enableMseAssembler(
|
|
897
|
+
packetId: number,
|
|
898
|
+
config: Omit<BlockAssemblerConfig, "k">,
|
|
899
|
+
catalogTrackName?: string,
|
|
900
|
+
): CmafBlockAssembler {
|
|
901
|
+
const k = this.#kForPacket(packetId);
|
|
902
|
+
const assemblerKey = this.#mseAssemblerKey(packetId, catalogTrackName);
|
|
903
|
+
const previous = this.#mseAssemblers.get(assemblerKey);
|
|
904
|
+
if (previous) {
|
|
905
|
+
previous.flush();
|
|
906
|
+
previous.dispose();
|
|
907
|
+
}
|
|
908
|
+
const assembler = new CmafBlockAssembler({ ...config, k });
|
|
909
|
+
this.#mseAssemblers.set(assemblerKey, assembler);
|
|
910
|
+
console.log(
|
|
911
|
+
`[MmtpRouter] MSE assembler enabled for packetId=${packetId}${catalogTrackName === undefined ? "" : ` track=${catalogTrackName}`} (K=${k}, ${config.trackType})`,
|
|
912
|
+
);
|
|
913
|
+
return assembler;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/** Register a segment callback for a track's MSE assembler.
|
|
917
|
+
* Called with complete moof+mdat CMAF segments on block completion. */
|
|
918
|
+
onMseSegment(packetId: number, cb: (segment: Uint8Array) => void, catalogTrackName?: string): void {
|
|
919
|
+
const assembler = this.#mseAssemblers.get(this.#mseAssemblerKey(packetId, catalogTrackName));
|
|
920
|
+
if (assembler) assembler.onSegment = cb;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** Register a raw-sample callback for a track's MSE assembler.
|
|
924
|
+
* Use this when the caller already owns the fMP4/MSE timeline and only wants
|
|
925
|
+
* FEC recovery, keyframe gating, and dedup from the assembler. */
|
|
926
|
+
onMseSample(packetId: number, cb: (sample: CmafBlockSample) => void, catalogTrackName?: string): void {
|
|
927
|
+
const assembler = this.#mseAssemblers.get(this.#mseAssemblerKey(packetId, catalogTrackName));
|
|
928
|
+
if (assembler) assembler.onSample = cb;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** Register an init segment callback for a track's MSE assembler. */
|
|
932
|
+
onMseInitSegment(packetId: number, cb: (init: Uint8Array) => void, catalogTrackName?: string): void {
|
|
933
|
+
const assembler = this.#mseAssemblers.get(this.#mseAssemblerKey(packetId, catalogTrackName));
|
|
934
|
+
if (assembler) assembler.onInitSegment = cb;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/** Build and emit init segment for a track's MSE assembler from catalog config. */
|
|
938
|
+
emitMseInit(packetId: number, catalogTrackName?: string): void {
|
|
939
|
+
this.#mseAssemblers.get(this.#mseAssemblerKey(packetId, catalogTrackName))?.emitInit();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** Mark a video track as synchronized by an external path.
|
|
943
|
+
* This opens the MMTP/FEC pre-sync gates without waiting for a multicast
|
|
944
|
+
* keyframe, for MSE timelines already seeded by MoQ with the same track. */
|
|
945
|
+
markExternalVideoSync(packetId?: number, catalogTrackName?: string): void {
|
|
946
|
+
const packetIds =
|
|
947
|
+
packetId === undefined
|
|
948
|
+
? [...this.#videoPacketIds]
|
|
949
|
+
: this.#videoPacketIds.has(packetId)
|
|
950
|
+
? [packetId]
|
|
951
|
+
: [];
|
|
952
|
+
for (const id of packetIds) {
|
|
953
|
+
const streamKey = this.#mediaStreamKey(id, catalogTrackName);
|
|
954
|
+
this.#trackSynced.add(streamKey);
|
|
955
|
+
this.#droppedPreSyncVideo.delete(streamKey);
|
|
956
|
+
this.#mseAssemblerFor(id, catalogTrackName)?.markVideoSynced();
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ── FEC event handler ───────────────────────────────────────────
|
|
961
|
+
|
|
962
|
+
/** Register callback for FEC blocks when the first source symbol arrives. */
|
|
963
|
+
onFecBlockStarted(cb: (sbn: number, packetId: number) => void): void {
|
|
964
|
+
this.#onBlockStarted = cb;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/** Register callback for FEC blocks that time out unrecoverably. Players use
|
|
968
|
+
* this to recover a decoder that was starved by the lost block. */
|
|
969
|
+
onFecBlockFailed(
|
|
970
|
+
cb: (sbn: number, packetId: number, catalogTrackName?: string) => void,
|
|
971
|
+
): void {
|
|
972
|
+
this.#onBlockFailed = cb;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// ── Phase 14: per-role packetId dispatch (D-26, D-27, D-28) ────────
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Register a TrackRole for a specific packetId.
|
|
979
|
+
*
|
|
980
|
+
* D-26: Enables per-role block-complete fanout — delta-role packetIds fire
|
|
981
|
+
* onDeltaBlockComplete when their FEC block completes or fails.
|
|
982
|
+
* Composite callback preserved: legacy onVideoFrame/onAudioFrame still fire
|
|
983
|
+
* (MEMORY.md "prevents callback stealing").
|
|
984
|
+
*
|
|
985
|
+
* Valid roles: 'base' | 'delta' | 'repair' | 'single'.
|
|
986
|
+
* Throws on unknown role (error on bad init, CLAUDE.md).
|
|
987
|
+
*/
|
|
988
|
+
setPacketIdRole(packetId: number, role: TrackRole): void {
|
|
989
|
+
const VALID_ROLES: readonly TrackRole[] = [
|
|
990
|
+
"base",
|
|
991
|
+
"delta",
|
|
992
|
+
"repair",
|
|
993
|
+
"single",
|
|
994
|
+
];
|
|
995
|
+
if (!VALID_ROLES.includes(role)) {
|
|
996
|
+
throw new Error(
|
|
997
|
+
`[MmtpRouter] setPacketIdRole: unknown role '${role}'. Valid: ${VALID_ROLES.join(", ")}`,
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
this.#packetIdRoles.set(packetId, role);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/** Replace catalog-derived repair ownership before a catalog reconfiguration. */
|
|
1004
|
+
clearRepairPacketIdOwners(): void {
|
|
1005
|
+
this.#repairOwnersByPacketId.clear();
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/** Bind one dedicated repair-track PID to its authoritative media PID. */
|
|
1009
|
+
setRepairPacketIdOwner(repairPacketId: number, ownerPacketId: number): void {
|
|
1010
|
+
const owners = this.#repairOwnersByPacketId.get(repairPacketId) ?? new Set<number>();
|
|
1011
|
+
owners.add(ownerPacketId);
|
|
1012
|
+
this.#repairOwnersByPacketId.set(repairPacketId, owners);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Register callback fired when a delta-track FEC block completes or fails.
|
|
1017
|
+
*
|
|
1018
|
+
* @param cb - (packetId, failed) — packetId identifies the delta track,
|
|
1019
|
+
* failed=false means clean/recovered, failed=true means unrecoverable.
|
|
1020
|
+
*
|
|
1021
|
+
* Called from both onBlockComplete (failed=false) and onBlockFailed (failed=true)
|
|
1022
|
+
* for every packetId registered with role='delta' via setPacketIdRole().
|
|
1023
|
+
*/
|
|
1024
|
+
onDeltaBlockComplete(cb: (packetId: number, failed: boolean) => void): void {
|
|
1025
|
+
this.#onDeltaBlockCompleteCb = cb;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// ── Packet processing ───────────────────────────────────────────
|
|
1029
|
+
|
|
1030
|
+
/** Update framerate — delegates to FECManager for timeout refinement. */
|
|
1031
|
+
setFramerate(fps: number): void {
|
|
1032
|
+
this.#fecManager?.setFramerate(fps);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/** Set FEC block mapper (created when FEC config becomes known via configureFec). */
|
|
1036
|
+
setFecMapper(mapper: FecBlockMapper, packetId?: number): void {
|
|
1037
|
+
if (packetId == null) {
|
|
1038
|
+
this.#defaultFecMapper = mapper;
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
this.#fecMappersByPacketId.set(packetId, mapper);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
registerManifestTrack(context: ManifestTrackContext): void {
|
|
1045
|
+
if (!this.#manifestVerifier) throw new Error("manifest verifier is not configured");
|
|
1046
|
+
this.#manifestVerifier.registerTrack(context);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
feedManifestFrame(trackName: string, frame: Uint8Array): void {
|
|
1050
|
+
this.#manifestVerifier?.ingestManifestFrame(trackName, frame);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
setManifestAuthMode(mode: ManifestAuthMode): void {
|
|
1054
|
+
this.#manifestVerifier?.setMode(mode);
|
|
1055
|
+
if (mode !== "enforce") {
|
|
1056
|
+
for (const queue of this.#manifestRecoveryQueues.values()) {
|
|
1057
|
+
for (const item of queue) {
|
|
1058
|
+
this.#routeProcessedFecSymbols(
|
|
1059
|
+
item.blockId,
|
|
1060
|
+
item.result,
|
|
1061
|
+
item.ownerPacketId,
|
|
1062
|
+
item.catalogTrackName,
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
this.#manifestRecoveryQueues.clear();
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Main entry point: feed a raw MMTP packet for processing.
|
|
1072
|
+
*
|
|
1073
|
+
* Source packets are played immediately AND fed to FECManager for recovery.
|
|
1074
|
+
* Repair packets are fed to FECManager only (no direct playback).
|
|
1075
|
+
*/
|
|
1076
|
+
/**
|
|
1077
|
+
* Feed one MMTP source packet.
|
|
1078
|
+
*
|
|
1079
|
+
* `source` is REQUIRED. It used to default to "multicast", which meant any
|
|
1080
|
+
* caller that forgot it silently attributed its bytes to a multicast socket
|
|
1081
|
+
* they may never have crossed — and that attribution is what the physical-
|
|
1082
|
+
* delivery evidence path is built on. Absence is not multicast; say which.
|
|
1083
|
+
*/
|
|
1084
|
+
feedPacket(
|
|
1085
|
+
packet: Uint8Array,
|
|
1086
|
+
source: "multicast" | "moq",
|
|
1087
|
+
manifestIdentity?: ManifestObjectIdentity,
|
|
1088
|
+
ingress?: MmtpIngressContext,
|
|
1089
|
+
): void {
|
|
1090
|
+
if (this.#disposed) return;
|
|
1091
|
+
this.#processMMTP(packet, source, manifestIdentity, false, ingress);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Feed a repair packet from MoQ (reliable QUIC) — parses MMTP repair
|
|
1096
|
+
* header and delegates to FECManager for FEC recovery.
|
|
1097
|
+
*/
|
|
1098
|
+
feedRepairPacket(
|
|
1099
|
+
packet: Uint8Array,
|
|
1100
|
+
ownerPacketIdHint?: number,
|
|
1101
|
+
catalogTrackName?: string,
|
|
1102
|
+
): void {
|
|
1103
|
+
if (this.#disposed) return;
|
|
1104
|
+
|
|
1105
|
+
const parsed = parseRepairPacket(packet);
|
|
1106
|
+
if (!parsed) return;
|
|
1107
|
+
|
|
1108
|
+
// A dedicated repair track can carry a packet ID that differs from the
|
|
1109
|
+
// media packet ID. The subscribing track supplies the authoritative owner.
|
|
1110
|
+
const header = packet.length >= 4 ? parseMMTPHeader(packet) : null;
|
|
1111
|
+
const repairPacketId = header?.packetId;
|
|
1112
|
+
if (repairPacketId == null) return;
|
|
1113
|
+
|
|
1114
|
+
const hintedOwner =
|
|
1115
|
+
ownerPacketIdHint != null && this.#classifyPacketId(ownerPacketIdHint) !== null
|
|
1116
|
+
? ownerPacketIdHint
|
|
1117
|
+
: null;
|
|
1118
|
+
// A MoQ repair subscription owns a media track explicitly. Dedicated
|
|
1119
|
+
// repair PIDs must carry that hint; only same-PID repair is self-describing.
|
|
1120
|
+
const ownerPacketId = hintedOwner ??
|
|
1121
|
+
(this.#classifyPacketId(repairPacketId) !== null ? repairPacketId : null);
|
|
1122
|
+
if (ownerPacketId === null) {
|
|
1123
|
+
debugRouter(
|
|
1124
|
+
`[MmtpRouter] Dropping MoQ repair packet pid=${repairPacketId}: owner hint is required`,
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (ownerPacketId != null && this.#fecManager) {
|
|
1129
|
+
this.#moqRepairPidCounts[repairPacketId] =
|
|
1130
|
+
(this.#moqRepairPidCounts[repairPacketId] || 0) + 1;
|
|
1131
|
+
this.#moqRepairOwnerCounts[ownerPacketId] =
|
|
1132
|
+
(this.#moqRepairOwnerCounts[ownerPacketId] || 0) + 1;
|
|
1133
|
+
const pidCount = this.#moqRepairPidCounts[repairPacketId]!;
|
|
1134
|
+
const ownerCount = this.#moqRepairOwnerCounts[ownerPacketId]!;
|
|
1135
|
+
if (pidCount <= 2 || pidCount % 500 === 0) {
|
|
1136
|
+
console.log(
|
|
1137
|
+
`[MOQ-REPAIR] pid=${repairPacketId} repairOwner=${ownerPacketId} pidCount=${pidCount} ownerCount=${ownerCount} ssbLength=${parsed.ssbLength} len=${packet.length}`,
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const repairSbn = this.#deriveRepairSbn(
|
|
1142
|
+
parsed.ssStart,
|
|
1143
|
+
parsed.ssbLength,
|
|
1144
|
+
ownerPacketId,
|
|
1145
|
+
);
|
|
1146
|
+
const repairEsi = this.#kForPacket(ownerPacketId) + parsed.rsId;
|
|
1147
|
+
|
|
1148
|
+
if (
|
|
1149
|
+
!this.#feedFecTransportSymbol(
|
|
1150
|
+
ownerPacketId,
|
|
1151
|
+
repairSbn,
|
|
1152
|
+
repairEsi,
|
|
1153
|
+
parsed.repairData,
|
|
1154
|
+
"moq",
|
|
1155
|
+
catalogTrackName,
|
|
1156
|
+
)
|
|
1157
|
+
) return;
|
|
1158
|
+
|
|
1159
|
+
// NOTE: Do NOT call trackBlock for repair packets (same as multicast repair path).
|
|
1160
|
+
const recovered = catalogTrackName === undefined
|
|
1161
|
+
? this.#fecManager.feedRepairSymbol(
|
|
1162
|
+
ownerPacketId,
|
|
1163
|
+
parsed.ssStart,
|
|
1164
|
+
parsed.ssbLength,
|
|
1165
|
+
parsed.rsId,
|
|
1166
|
+
parsed.repairData,
|
|
1167
|
+
)
|
|
1168
|
+
: this.#fecManager.feedRepairSymbol(
|
|
1169
|
+
ownerPacketId,
|
|
1170
|
+
parsed.ssStart,
|
|
1171
|
+
parsed.ssbLength,
|
|
1172
|
+
parsed.rsId,
|
|
1173
|
+
parsed.repairData,
|
|
1174
|
+
catalogTrackName,
|
|
1175
|
+
);
|
|
1176
|
+
if (recovered) {
|
|
1177
|
+
const alreadyCompleted = catalogTrackName === undefined
|
|
1178
|
+
? this.#fecManager.isBlockCompleted(repairSbn, ownerPacketId)
|
|
1179
|
+
: this.#fecManager.isBlockCompleted(repairSbn, ownerPacketId, catalogTrackName);
|
|
1180
|
+
if (catalogTrackName === undefined) {
|
|
1181
|
+
this.#fecManager.cancelBlockTimeout(repairSbn, ownerPacketId);
|
|
1182
|
+
} else {
|
|
1183
|
+
this.#fecManager.cancelBlockTimeout(repairSbn, ownerPacketId, catalogTrackName);
|
|
1184
|
+
}
|
|
1185
|
+
if (!alreadyCompleted) {
|
|
1186
|
+
this.#fecManager.countRecovery(ownerPacketId);
|
|
1187
|
+
if (catalogTrackName === undefined) {
|
|
1188
|
+
this.#fecManager.markBlockCompleted(repairSbn, ownerPacketId);
|
|
1189
|
+
} else {
|
|
1190
|
+
this.#fecManager.markBlockCompleted(repairSbn, ownerPacketId, catalogTrackName);
|
|
1191
|
+
}
|
|
1192
|
+
this.#routeRecoveredBlock(
|
|
1193
|
+
repairSbn,
|
|
1194
|
+
recovered,
|
|
1195
|
+
ownerPacketId,
|
|
1196
|
+
parsed.ssbLength,
|
|
1197
|
+
catalogTrackName,
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// ── SBN/ESI derivation ──────────────────────────────────────────
|
|
1205
|
+
|
|
1206
|
+
#kForPacket(packetId?: number): number {
|
|
1207
|
+
// Same packet-scoped rule as #deriveSbnEsi: once the packet ID is known,
|
|
1208
|
+
// only its explicitly registered mapper may stand in for the FEC
|
|
1209
|
+
// manager's per-packet K — never the default single-track mapper.
|
|
1210
|
+
if (packetId != null)
|
|
1211
|
+
return (
|
|
1212
|
+
this.#fecManager?.kForPacket?.(packetId) ??
|
|
1213
|
+
this.#fecMappersByPacketId.get(packetId)?.k ??
|
|
1214
|
+
this.#fecManager?.k ??
|
|
1215
|
+
4
|
|
1216
|
+
);
|
|
1217
|
+
return this.#defaultFecMapper?.k ?? this.#fecManager?.k ?? 4;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
#symbolSizeForPacket(packetId?: number): number {
|
|
1221
|
+
if (packetId != null) return this.#fecManager?.symbolSizeForPacket?.(packetId) ?? this.#fecManager?.symbolSize ?? 0;
|
|
1222
|
+
return this.#fecManager?.symbolSize ?? 0;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/** Derive SBN/ESI from SS_ID using packet-scoped FEC geometry. */
|
|
1226
|
+
#deriveSbnEsi(ssId: number, packetId?: number): { sbn: number; esi: number } {
|
|
1227
|
+
// A default mapper describes legacy single-track geometry. Once a packet
|
|
1228
|
+
// ID is known, only its explicitly registered mapper may override the
|
|
1229
|
+
// FECManager's packet-scoped K. Falling back to the default video mapper
|
|
1230
|
+
// here maps audio source symbols with video K while repair SBNs correctly
|
|
1231
|
+
// use the audio SSB_length, splitting one block across two coordinates.
|
|
1232
|
+
const mapper = packetId == null
|
|
1233
|
+
? this.#defaultFecMapper
|
|
1234
|
+
: this.#fecMappersByPacketId.get(packetId) ?? null;
|
|
1235
|
+
if (mapper) return mapper.fromSsId(ssId);
|
|
1236
|
+
const k = this.#kForPacket(packetId);
|
|
1237
|
+
return { sbn: Math.floor(ssId / k), esi: ssId % k };
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
#deriveRepairSbn(ssStart: number, ssbLength: number, packetId?: number): number {
|
|
1241
|
+
if (ssbLength > 0) return Math.floor(ssStart / ssbLength);
|
|
1242
|
+
return this.#deriveSbnEsi(ssStart, packetId).sbn;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
#resolveRepairOwnerPacketId(
|
|
1246
|
+
packetId: number,
|
|
1247
|
+
ingress?: MmtpIngressContext,
|
|
1248
|
+
): number | null {
|
|
1249
|
+
if (this.#classifyPacketId(packetId) !== null) return packetId;
|
|
1250
|
+
|
|
1251
|
+
if (this.#packetIdRoles.get(packetId) !== "repair") {
|
|
1252
|
+
debugRouter(
|
|
1253
|
+
`[MmtpRouter] Dropping repair packet pid=${packetId}: physical PID is unregistered`,
|
|
1254
|
+
);
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const registeredOwners = this.#repairOwnersByPacketId.get(packetId) ?? new Set<number>();
|
|
1259
|
+
const owners = ingress === undefined
|
|
1260
|
+
? [...registeredOwners]
|
|
1261
|
+
: [...registeredOwners].filter((ownerPacketId) =>
|
|
1262
|
+
ingressHasPacketId(ingress, ownerPacketId));
|
|
1263
|
+
if (owners.length === 1) return owners[0]!;
|
|
1264
|
+
|
|
1265
|
+
debugRouter(
|
|
1266
|
+
`[MmtpRouter] Dropping repair packet pid=${packetId}: catalog owner is ${owners.length === 0 ? "absent" : "ambiguous"}`,
|
|
1267
|
+
);
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/** Route a recovered FEC block through the parser + dedup pipeline.
|
|
1272
|
+
* Sync WASM decoders return K fixed-width source symbols concatenated. */
|
|
1273
|
+
routeRecoveredBlock(
|
|
1274
|
+
blockId: number,
|
|
1275
|
+
data: Uint8Array,
|
|
1276
|
+
packetId: number,
|
|
1277
|
+
catalogTrackName?: string,
|
|
1278
|
+
): void {
|
|
1279
|
+
this.#routeRecoveredBlock(blockId, data, packetId, undefined, catalogTrackName);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/** Route a single recovered source symbol through the parser + dedup pipeline.
|
|
1283
|
+
* Worker-backed decoders emit one padded source symbol per frame event. */
|
|
1284
|
+
routeRecoveredSymbol(
|
|
1285
|
+
blockId: number,
|
|
1286
|
+
data: Uint8Array,
|
|
1287
|
+
packetId: number,
|
|
1288
|
+
esi = 0,
|
|
1289
|
+
catalogTrackName?: string,
|
|
1290
|
+
): void {
|
|
1291
|
+
const result = processFecSymbol(data, { ownerPacketId: packetId, index: esi });
|
|
1292
|
+
this.#routeVerifiedFecSymbols(blockId, result, packetId, catalogTrackName);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// ── Keyframe timer cancellation ─────────────────────────────────
|
|
1296
|
+
|
|
1297
|
+
/** Cancel the active keyframe loss timer.
|
|
1298
|
+
* Call on failbackToMoq to prevent a stale timer from firing
|
|
1299
|
+
* onKeyframeLost after multicast is no longer the active source. */
|
|
1300
|
+
cancelKeyframeLostTimer(): void {
|
|
1301
|
+
if (this.#keyframeLostTimer) {
|
|
1302
|
+
clearTimeout(this.#keyframeLostTimer);
|
|
1303
|
+
this.#keyframeLostTimer = null;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// ── Dispose ─────────────────────────────────────────────────────
|
|
1308
|
+
|
|
1309
|
+
/** Emit parser frames buffered for a finite input before teardown. */
|
|
1310
|
+
flush(): void {
|
|
1311
|
+
if (this.#disposed) return;
|
|
1312
|
+
this.#parser.flush();
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/** Drop one rendition's parser/router/CMAF epoch without touching peers. */
|
|
1316
|
+
resetTrack(packetId: number, catalogTrackName: string): void {
|
|
1317
|
+
if (this.#disposed) return;
|
|
1318
|
+
this.#parser.resetTrack(packetId, catalogTrackName);
|
|
1319
|
+
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
1320
|
+
this.#trackSynced.delete(streamKey);
|
|
1321
|
+
this.#droppedPreSyncVideo.delete(streamKey);
|
|
1322
|
+
this.#latestVideoSeqByStream.delete(streamKey);
|
|
1323
|
+
this.#lastDirectSourceVideoTsByStream.delete(streamKey);
|
|
1324
|
+
this.#lastDirectEmittedVideoTsByStream.delete(streamKey);
|
|
1325
|
+
this.#pendingDirectRecoveredVideoByStream.delete(streamKey);
|
|
1326
|
+
this.#lastDirectSourceAudioCursorByStream.delete(streamKey);
|
|
1327
|
+
this.#lastDirectEmittedAudioCursorByStream.delete(streamKey);
|
|
1328
|
+
this.#pendingDirectRecoveredAudioByStream.delete(streamKey);
|
|
1329
|
+
this.#manifestRecoveryQueues.delete(streamKey);
|
|
1330
|
+
for (const packetSeqs of [this.#sourcePktSeqs, this.#routedPacketSeqs]) {
|
|
1331
|
+
for (const key of packetSeqs) {
|
|
1332
|
+
try {
|
|
1333
|
+
const [keyPacketId, keyTrackName] = JSON.parse(key);
|
|
1334
|
+
if (keyPacketId === packetId && keyTrackName === catalogTrackName) {
|
|
1335
|
+
packetSeqs.delete(key);
|
|
1336
|
+
}
|
|
1337
|
+
} catch {
|
|
1338
|
+
// Internal keys are JSON tuples; malformed legacy keys do not belong
|
|
1339
|
+
// to a named physical stream and are left untouched.
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const assemblersToReset = new Set<CmafBlockAssembler>();
|
|
1344
|
+
const exactAssembler = this.#mseAssemblers.get(streamKey);
|
|
1345
|
+
if (exactAssembler) assemblersToReset.add(exactAssembler);
|
|
1346
|
+
const selectedAssembler = this.#mseAssemblerFor(packetId, catalogTrackName);
|
|
1347
|
+
if (selectedAssembler) assemblersToReset.add(selectedAssembler);
|
|
1348
|
+
for (const assembler of assemblersToReset) assembler.reset();
|
|
1349
|
+
this.#trackInitDelivered.delete(streamKey);
|
|
1350
|
+
this.#parserDiagnostics.delete(streamKey);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/** Live teardown is drop-only. Finite inputs must opt into a final flush. */
|
|
1354
|
+
dispose(options: { flush?: boolean } = {}): void {
|
|
1355
|
+
if (options.flush) this.flush();
|
|
1356
|
+
this.#disposed = true;
|
|
1357
|
+
this.cancelKeyframeLostTimer();
|
|
1358
|
+
this.#parser.dispose();
|
|
1359
|
+
this.#sourcePktSeqs.clear();
|
|
1360
|
+
this.#routedPacketSeqs.clear();
|
|
1361
|
+
this.#trackPacketCallbacks.clear();
|
|
1362
|
+
this.#latestVideoSeqByStream.clear();
|
|
1363
|
+
this.#lastDirectSourceVideoTsByStream.clear();
|
|
1364
|
+
this.#lastDirectEmittedVideoTsByStream.clear();
|
|
1365
|
+
this.#pendingDirectRecoveredVideoByStream.clear();
|
|
1366
|
+
this.#lastDirectSourceAudioCursorByStream.clear();
|
|
1367
|
+
this.#lastDirectEmittedAudioCursorByStream.clear();
|
|
1368
|
+
this.#pendingDirectRecoveredAudioByStream.clear();
|
|
1369
|
+
this.#trackSynced.clear();
|
|
1370
|
+
this.#droppedPreSyncVideo.clear();
|
|
1371
|
+
this.#trackInitDelivered.clear();
|
|
1372
|
+
this.#repairPidCounts = {};
|
|
1373
|
+
this.#moqRepairPidCounts = {};
|
|
1374
|
+
this.#moqRepairOwnerCounts = {};
|
|
1375
|
+
this.#parserDiagnostics.clear();
|
|
1376
|
+
this.#catalogTrackNamesByPacketId.clear();
|
|
1377
|
+
this.#repairOwnersByPacketId.clear();
|
|
1378
|
+
if (this.#ownsManifestVerifier) this.#manifestVerifier?.dispose();
|
|
1379
|
+
this.#manifestRecoveryQueues.clear();
|
|
1380
|
+
for (const assembler of this.#mseAssemblers.values()) assembler.dispose();
|
|
1381
|
+
this.#mseAssemblers.clear();
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// ── Private: keyframe loss detection ────────────────────────────
|
|
1385
|
+
|
|
1386
|
+
/** Arm (or re-arm) the keyframe loss timer.
|
|
1387
|
+
* Clears any existing timer and sets a new one for keyframeLostTimeoutMs.
|
|
1388
|
+
* On timeout, fires onKeyframeLost with the next expected groupId. */
|
|
1389
|
+
#armKeyframeLostTimer(): void {
|
|
1390
|
+
if (this.#keyframeLostTimer) {
|
|
1391
|
+
clearTimeout(this.#keyframeLostTimer);
|
|
1392
|
+
}
|
|
1393
|
+
const expectedNext = this.#lastKeyframeGroupId + 1;
|
|
1394
|
+
this.#keyframeLostTimer = setTimeout(() => {
|
|
1395
|
+
this.#keyframeLostTimer = null;
|
|
1396
|
+
this.#onKeyframeLost?.(expectedNext);
|
|
1397
|
+
}, this.#keyframeLostTimeoutMs!);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
#noteEmittedKeyframe(packetId: number, keyframe: boolean): void {
|
|
1401
|
+
if (!keyframe) return;
|
|
1402
|
+
if (!this.#videoPacketIds.has(packetId)) return;
|
|
1403
|
+
if (this.#keyframeLostTimeoutMs === null) return;
|
|
1404
|
+
this.#lastKeyframeGroupId++;
|
|
1405
|
+
this.#armKeyframeLostTimer();
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// ── Private: MMTP processing ────────────────────────────────────
|
|
1409
|
+
|
|
1410
|
+
/** Classify a packetId from the catalog. Returns true=video, false=audio, null=unknown (drop). */
|
|
1411
|
+
#classifyPacketId(packetId: number): boolean | null {
|
|
1412
|
+
if (this.#videoPacketIds.has(packetId)) return true;
|
|
1413
|
+
if (this.#audioPacketIds.has(packetId)) return false;
|
|
1414
|
+
return null;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/** Resolve callback for a packetId (cached per-packetId, invalidated on registration).
|
|
1418
|
+
* When a per-track callback is registered AND a legacy callback exists,
|
|
1419
|
+
* fires BOTH: per-track first, then legacy. This ensures element.ts wiring
|
|
1420
|
+
* (confirmAudio, feedMulticast) continues to fire even when TrackPipeline
|
|
1421
|
+
* registers per-track sources.
|
|
1422
|
+
* Returns null for unknown packetIds — callers must drop the packet. */
|
|
1423
|
+
#getFrameCallback(packetId: number): FrameCallback | null {
|
|
1424
|
+
const cached = this.#resolvedCallbacks.get(packetId);
|
|
1425
|
+
if (cached) return cached;
|
|
1426
|
+
|
|
1427
|
+
const perTrack = this.#trackCallbacks.get(packetId);
|
|
1428
|
+
const classification = this.#classifyPacketId(packetId);
|
|
1429
|
+
const legacy =
|
|
1430
|
+
classification === true
|
|
1431
|
+
? this.#onVideoFrame
|
|
1432
|
+
: classification === false
|
|
1433
|
+
? this.#onAudioFrame
|
|
1434
|
+
: null;
|
|
1435
|
+
|
|
1436
|
+
let resolved: FrameCallback | null;
|
|
1437
|
+
if (perTrack && legacy) {
|
|
1438
|
+
// Composite: fire both so per-track routing AND legacy confirm/feedMulticast work.
|
|
1439
|
+
resolved = (data, meta) => {
|
|
1440
|
+
perTrack(data, meta);
|
|
1441
|
+
legacy(data, meta);
|
|
1442
|
+
};
|
|
1443
|
+
} else {
|
|
1444
|
+
resolved = perTrack ?? legacy ?? null;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (resolved) this.#resolvedCallbacks.set(packetId, resolved);
|
|
1448
|
+
return resolved;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/** Deliver init segment for a track (deduped per physical stream). */
|
|
1452
|
+
#deliverInit(packetId: number, data: Uint8Array, catalogTrackName?: string): void {
|
|
1453
|
+
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
1454
|
+
if (this.#trackInitDelivered.has(streamKey)) return;
|
|
1455
|
+
if (data.byteLength <= 8) return;
|
|
1456
|
+
this.#trackInitDelivered.add(streamKey);
|
|
1457
|
+
const cb = this.#trackInitCallbacks.get(packetId);
|
|
1458
|
+
if (cb) {
|
|
1459
|
+
cb(data);
|
|
1460
|
+
} else if (this.#onInitSegment) {
|
|
1461
|
+
this.#onInitSegment(data);
|
|
1462
|
+
}
|
|
1463
|
+
console.log(
|
|
1464
|
+
`[MmtpRouter] Init delivered for packetId=${packetId}${catalogTrackName === undefined ? "" : ` track=${catalogTrackName}`} (${data.byteLength}B)`,
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* Core MMTP packet processing — source packets → play + FEC, repair → FEC only.
|
|
1470
|
+
* Both source and repair feed FECManager so RaptorQ has enough symbols (source + repair >= K).
|
|
1471
|
+
*
|
|
1472
|
+
* Two-phase parsing via MmtpContainerParser:
|
|
1473
|
+
* 1. parseHeader() → extract packetId + fecPayloadId for FEC feeding
|
|
1474
|
+
* 2. feed() → MFU extraction, reassembly, keyframe detection, emit via callbacks
|
|
1475
|
+
*/
|
|
1476
|
+
#processMMTP(
|
|
1477
|
+
packet: Uint8Array,
|
|
1478
|
+
source: "multicast" | "moq",
|
|
1479
|
+
manifestIdentity?: ManifestObjectIdentity,
|
|
1480
|
+
manifestAdmitted = false,
|
|
1481
|
+
ingress?: MmtpIngressContext,
|
|
1482
|
+
): void {
|
|
1483
|
+
const info = this.#parser.parseHeader(packet);
|
|
1484
|
+
if (!info) return;
|
|
1485
|
+
if (source === "multicast" && !ingressHasPacketId(ingress, info.packetId)) {
|
|
1486
|
+
debugRouter(
|
|
1487
|
+
`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: absent from ingress tracks`,
|
|
1488
|
+
);
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const packetRole = this.#packetIdRoles.get(info.packetId);
|
|
1492
|
+
if (
|
|
1493
|
+
source === "multicast" &&
|
|
1494
|
+
this.#classifyPacketId(info.packetId) === null &&
|
|
1495
|
+
(!info.isRepair || packetRole !== "repair")
|
|
1496
|
+
) {
|
|
1497
|
+
debugRouter(
|
|
1498
|
+
`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: physical PID is unregistered`,
|
|
1499
|
+
);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
if (!info.isRepair && hasAmbiguousCatalogTrackName(ingress, info.packetId)) {
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
if (!info.isRepair && this.#manifestVerifier && !manifestAdmitted) {
|
|
1507
|
+
let identity = manifestIdentity;
|
|
1508
|
+
if (!identity && info.fecPayloadId && (this.#resolveManifestIdentityForRoute || this.#resolveManifestIdentity)) {
|
|
1509
|
+
const coordinates = this.#deriveSbnEsi(info.fecPayloadId.ssId, info.packetId);
|
|
1510
|
+
const catalogTrackName = catalogTrackNameForPacket(ingress, info.packetId);
|
|
1511
|
+
identity = (this.#resolveManifestIdentityForRoute
|
|
1512
|
+
? this.#resolveManifestIdentityForRoute({
|
|
1513
|
+
packetId: info.packetId,
|
|
1514
|
+
sbn: coordinates.sbn,
|
|
1515
|
+
esi: coordinates.esi,
|
|
1516
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
1517
|
+
recovered: false,
|
|
1518
|
+
sourceSymbolFormat: "full-mmtp-packet",
|
|
1519
|
+
})
|
|
1520
|
+
: this.#resolveManifestIdentity!(
|
|
1521
|
+
info.packetId,
|
|
1522
|
+
coordinates.sbn,
|
|
1523
|
+
coordinates.esi,
|
|
1524
|
+
catalogTrackName,
|
|
1525
|
+
)) ?? undefined;
|
|
1526
|
+
}
|
|
1527
|
+
if (identity) {
|
|
1528
|
+
identity = {
|
|
1529
|
+
...identity,
|
|
1530
|
+
packetId: info.packetId,
|
|
1531
|
+
...(info.fecPayloadId ? { ssId: info.fecPayloadId.ssId } : {}),
|
|
1532
|
+
};
|
|
1533
|
+
const authenticated = info.fecPayloadId ? packet.subarray(0, packet.length - 4) : packet;
|
|
1534
|
+
this.#manifestVerifier.admitObject(identity, authenticated, () => {
|
|
1535
|
+
if (!this.#disposed) this.#processMMTP(packet, source, identity, true, ingress);
|
|
1536
|
+
});
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if (
|
|
1540
|
+
this.#manifestVerifier.config.mode === "enforce" &&
|
|
1541
|
+
this.#manifestVerifier.config.onMissingManifest === "stall"
|
|
1542
|
+
) {
|
|
1543
|
+
console.warn(`[MmtpRouter] manifest identity absent; stalling packetId=${info.packetId}`);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
if (info.isRepair) {
|
|
1549
|
+
// Repair packet — per ISO 23008-1 §C.5.3, carries (SS_Start, SSB_length, RS_ID).
|
|
1550
|
+
const parsed = parseRepairPacket(packet);
|
|
1551
|
+
if (!parsed) return;
|
|
1552
|
+
|
|
1553
|
+
const ownerPacketId = this.#resolveRepairOwnerPacketId(info.packetId, ingress);
|
|
1554
|
+
if (ownerPacketId != null && this.#fecManager) {
|
|
1555
|
+
if (hasAmbiguousCatalogTrackName(ingress, ownerPacketId)) return;
|
|
1556
|
+
this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
|
|
1557
|
+
const count = this.#repairPidCounts[info.packetId]!;
|
|
1558
|
+
if (count <= 2 || count % 500 === 0) {
|
|
1559
|
+
console.log(
|
|
1560
|
+
`[MC-PKT] pid=${info.packetId} repairOwner=${ownerPacketId} type=${info.packetType} count=${count} ssbLength=${parsed.ssbLength} len=${packet.length}`,
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// NOTE: Do NOT call trackBlock for repair packets. Only source symbols
|
|
1565
|
+
// should count toward block completion — repair inflates blocksClean stats
|
|
1566
|
+
// and masks blocks that actually needed FEC recovery.
|
|
1567
|
+
const repairSbn = this.#deriveRepairSbn(
|
|
1568
|
+
parsed.ssStart,
|
|
1569
|
+
parsed.ssbLength,
|
|
1570
|
+
ownerPacketId,
|
|
1571
|
+
);
|
|
1572
|
+
const repairEsi = this.#kForPacket(ownerPacketId) + parsed.rsId;
|
|
1573
|
+
|
|
1574
|
+
if (
|
|
1575
|
+
!this.#feedFecTransportSymbol(
|
|
1576
|
+
ownerPacketId,
|
|
1577
|
+
repairSbn,
|
|
1578
|
+
repairEsi,
|
|
1579
|
+
parsed.repairData,
|
|
1580
|
+
source,
|
|
1581
|
+
catalogTrackNameForPacket(ingress, ownerPacketId),
|
|
1582
|
+
)
|
|
1583
|
+
) return;
|
|
1584
|
+
|
|
1585
|
+
// Repair symbols are block progress too. The deadline measures
|
|
1586
|
+
// inactivity, not total block lifetime.
|
|
1587
|
+
const repairCatalogTrackName = catalogTrackNameForPacket(ingress, ownerPacketId);
|
|
1588
|
+
if (repairCatalogTrackName === undefined) {
|
|
1589
|
+
this.#fecManager.noteBlockActivity?.(repairSbn, ownerPacketId);
|
|
1590
|
+
} else {
|
|
1591
|
+
this.#fecManager.noteBlockActivity?.(
|
|
1592
|
+
repairSbn,
|
|
1593
|
+
ownerPacketId,
|
|
1594
|
+
repairCatalogTrackName,
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const recovered = repairCatalogTrackName === undefined
|
|
1599
|
+
? this.#fecManager.feedRepairSymbol(
|
|
1600
|
+
ownerPacketId,
|
|
1601
|
+
parsed.ssStart,
|
|
1602
|
+
parsed.ssbLength,
|
|
1603
|
+
parsed.rsId,
|
|
1604
|
+
parsed.repairData,
|
|
1605
|
+
)
|
|
1606
|
+
: this.#fecManager.feedRepairSymbol(
|
|
1607
|
+
ownerPacketId,
|
|
1608
|
+
parsed.ssStart,
|
|
1609
|
+
parsed.ssbLength,
|
|
1610
|
+
parsed.rsId,
|
|
1611
|
+
parsed.repairData,
|
|
1612
|
+
repairCatalogTrackName,
|
|
1613
|
+
);
|
|
1614
|
+
if (recovered) {
|
|
1615
|
+
const alreadyCompleted = repairCatalogTrackName === undefined
|
|
1616
|
+
? this.#fecManager.isBlockCompleted(repairSbn, ownerPacketId)
|
|
1617
|
+
: this.#fecManager.isBlockCompleted(
|
|
1618
|
+
repairSbn,
|
|
1619
|
+
ownerPacketId,
|
|
1620
|
+
repairCatalogTrackName,
|
|
1621
|
+
);
|
|
1622
|
+
if (repairCatalogTrackName === undefined) {
|
|
1623
|
+
this.#fecManager.cancelBlockTimeout(repairSbn, ownerPacketId);
|
|
1624
|
+
} else {
|
|
1625
|
+
this.#fecManager.cancelBlockTimeout(
|
|
1626
|
+
repairSbn,
|
|
1627
|
+
ownerPacketId,
|
|
1628
|
+
repairCatalogTrackName,
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
// Skip routing if block was already completed via source path (all K sources
|
|
1632
|
+
// arrived). The parser's feed() already played every frame.
|
|
1633
|
+
if (!alreadyCompleted) {
|
|
1634
|
+
this.#fecManager.countRecovery(ownerPacketId);
|
|
1635
|
+
if (repairCatalogTrackName === undefined) {
|
|
1636
|
+
this.#fecManager.markBlockCompleted(repairSbn, ownerPacketId);
|
|
1637
|
+
} else {
|
|
1638
|
+
this.#fecManager.markBlockCompleted(
|
|
1639
|
+
repairSbn,
|
|
1640
|
+
ownerPacketId,
|
|
1641
|
+
repairCatalogTrackName,
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
this.#routeRecoveredBlock(
|
|
1645
|
+
repairSbn,
|
|
1646
|
+
recovered,
|
|
1647
|
+
ownerPacketId,
|
|
1648
|
+
parsed.ssbLength,
|
|
1649
|
+
catalogTrackNameForPacket(ingress, ownerPacketId),
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
} else {
|
|
1655
|
+
// Drop source packets with unknown packetId — not in catalog.
|
|
1656
|
+
if (this.#classifyPacketId(info.packetId) === null) return;
|
|
1657
|
+
|
|
1658
|
+
// Debug: count packets per packetId
|
|
1659
|
+
this.#pidCounts[info.packetId] = (this.#pidCounts[info.packetId] || 0) + 1;
|
|
1660
|
+
const count = this.#pidCounts[info.packetId]!;
|
|
1661
|
+
if (count <= 2 || count % 500 === 0) {
|
|
1662
|
+
console.log(
|
|
1663
|
+
`[MC-PKT] pid=${info.packetId} type=${info.packetType} count=${count} len=${packet.length}`,
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// Source packet — parse for playback first, then feed FEC only if the
|
|
1668
|
+
// source block did not complete cleanly. Feeding FEC before parser/feed
|
|
1669
|
+
// lets a source-triggered recovery race ahead of the source timeline.
|
|
1670
|
+
let fecInfo:
|
|
1671
|
+
| { sbn: number; esi: number; oversized?: boolean }
|
|
1672
|
+
| undefined;
|
|
1673
|
+
let sourceFec:
|
|
1674
|
+
| {
|
|
1675
|
+
ssId: number;
|
|
1676
|
+
sbn: number;
|
|
1677
|
+
symbolData: Uint8Array;
|
|
1678
|
+
}
|
|
1679
|
+
| null = null;
|
|
1680
|
+
let sourceSymbolAccepted = false;
|
|
1681
|
+
let sourceSymbolTrackable = false;
|
|
1682
|
+
const catalogTrackName = catalogTrackNameForPacket(ingress, info.packetId);
|
|
1683
|
+
if (catalogTrackName !== undefined) {
|
|
1684
|
+
const names = this.#catalogTrackNamesByPacketId.get(info.packetId) ?? new Set();
|
|
1685
|
+
names.add(catalogTrackName);
|
|
1686
|
+
this.#catalogTrackNamesByPacketId.set(info.packetId, names);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
if (this.#fecManager && info.fecPayloadId) {
|
|
1690
|
+
const fecId = info.fecPayloadId;
|
|
1691
|
+
const symbolSize = this.#symbolSizeForPacket(info.packetId);
|
|
1692
|
+
|
|
1693
|
+
// The mmtp-packet source symbol protects the complete MMTP packet
|
|
1694
|
+
// before SS_ID is appended, self-delimiting and zero-padded to T.
|
|
1695
|
+
const rawSymbol = packet.subarray(0, packet.length - 4);
|
|
1696
|
+
const oversized = symbolSize > 0 && rawSymbol.length > symbolSize;
|
|
1697
|
+
const coords = this.#deriveSbnEsi(fecId.ssId, info.packetId);
|
|
1698
|
+
|
|
1699
|
+
// NOTE: trackBlock() is deferred to AFTER #feedViaParser (below).
|
|
1700
|
+
// Reason: trackBlock synchronously fires onBlockComplete when the
|
|
1701
|
+
// K-th source symbol arrives, which synchronously calls
|
|
1702
|
+
// CmafBlockAssembler.completeBlock → #flushBlock → emit. If
|
|
1703
|
+
// trackBlock fires BEFORE parser.feed, the K-th packet's
|
|
1704
|
+
// parser.onFrame → assembler.addFrame happens AFTER flushBlock
|
|
1705
|
+
// already drained the block — the K-th frame is stranded in a
|
|
1706
|
+
// newly-allocated empty block that never gets a completeBlock
|
|
1707
|
+
// event again, because that SBN is already in fecManager's
|
|
1708
|
+
// #completedBlocks set (BLO-3035 layer 4).
|
|
1709
|
+
//
|
|
1710
|
+
// Complete-packet symbols larger than T cannot be decoded but still
|
|
1711
|
+
// represent a source-symbol arrival, so trackBlock counts them below.
|
|
1712
|
+
|
|
1713
|
+
if (oversized) {
|
|
1714
|
+
// Oversized packet — not FEC-encoded by FFmpeg, skip FEC feed.
|
|
1715
|
+
// Still emitted via parser.feed() below (direct path).
|
|
1716
|
+
sourceSymbolAccepted = this.#feedFecTransportSymbol(
|
|
1717
|
+
info.packetId,
|
|
1718
|
+
coords.sbn,
|
|
1719
|
+
coords.esi,
|
|
1720
|
+
rawSymbol,
|
|
1721
|
+
source,
|
|
1722
|
+
catalogTrackName,
|
|
1723
|
+
);
|
|
1724
|
+
sourceSymbolTrackable = sourceSymbolAccepted;
|
|
1725
|
+
} else {
|
|
1726
|
+
let symbolData: Uint8Array;
|
|
1727
|
+
if (symbolSize > 0 && rawSymbol.length < symbolSize) {
|
|
1728
|
+
symbolData = new Uint8Array(symbolSize);
|
|
1729
|
+
symbolData.set(rawSymbol);
|
|
1730
|
+
} else {
|
|
1731
|
+
symbolData = rawSymbol;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
sourceSymbolAccepted = this.#feedFecTransportSymbol(
|
|
1735
|
+
info.packetId,
|
|
1736
|
+
coords.sbn,
|
|
1737
|
+
coords.esi,
|
|
1738
|
+
symbolData,
|
|
1739
|
+
source,
|
|
1740
|
+
catalogTrackName,
|
|
1741
|
+
);
|
|
1742
|
+
sourceSymbolTrackable = sourceSymbolAccepted;
|
|
1743
|
+
if (sourceSymbolAccepted) {
|
|
1744
|
+
sourceFec = { ssId: fecId.ssId, sbn: coords.sbn, symbolData };
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// Compute FEC block info for the MSE assembler.
|
|
1749
|
+
fecInfo = { sbn: coords.sbn, esi: coords.esi, oversized };
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Parse only the first copy of an FEC source symbol. A late direct copy
|
|
1753
|
+
// of a worker-recovered fragment has the same SBN/ESI and fragment
|
|
1754
|
+
// counter; feeding it again can make the MFU buffer non-contiguous even
|
|
1755
|
+
// when FI=1..3 were already complete. Non-FEC packets still always flow.
|
|
1756
|
+
if (!this.#fecManager || !info.fecPayloadId || sourceSymbolAccepted) {
|
|
1757
|
+
this.#noteSourcePacketSeq(
|
|
1758
|
+
info.packetId,
|
|
1759
|
+
info.sequenceNumber,
|
|
1760
|
+
catalogTrackName,
|
|
1761
|
+
);
|
|
1762
|
+
this.#routeTrackPacket(packet, info.packetId, info.sequenceNumber, {
|
|
1763
|
+
recovered: false,
|
|
1764
|
+
skipFecPayloadId: false,
|
|
1765
|
+
catalogTrackName,
|
|
1766
|
+
});
|
|
1767
|
+
this.#feedViaParser(packet, info, fecInfo, {
|
|
1768
|
+
source,
|
|
1769
|
+
catalogTrackName,
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// Deferred trackBlock (BLO-3035 layer 4 race fix). Fire AFTER
|
|
1774
|
+
// #feedViaParser → parser.feed → addFrame, so the K-th source
|
|
1775
|
+
// packet's frame is buffered in CmafBlockAssembler.#blocks BEFORE
|
|
1776
|
+
// trackBlock synchronously triggers onBlockComplete → completeBlock
|
|
1777
|
+
// → #flushBlock. Without this ordering, the K-th frame was lost on
|
|
1778
|
+
// every block — only K-1 frames per block were ever emitted to MSE.
|
|
1779
|
+
// Only first-arriving symbols count toward completion or playback.
|
|
1780
|
+
if (
|
|
1781
|
+
this.#fecManager &&
|
|
1782
|
+
info.fecPayloadId &&
|
|
1783
|
+
sourceSymbolAccepted &&
|
|
1784
|
+
sourceSymbolTrackable
|
|
1785
|
+
) {
|
|
1786
|
+
const { sbn, esi } = this.#deriveSbnEsi(
|
|
1787
|
+
info.fecPayloadId.ssId,
|
|
1788
|
+
info.packetId,
|
|
1789
|
+
);
|
|
1790
|
+
if (this.#classifyPacketId(info.packetId) !== null) {
|
|
1791
|
+
if (catalogTrackName === undefined) {
|
|
1792
|
+
this.#fecManager.trackBlock(sbn, info.packetId, undefined, esi);
|
|
1793
|
+
} else {
|
|
1794
|
+
this.#fecManager.trackBlock(
|
|
1795
|
+
sbn,
|
|
1796
|
+
info.packetId,
|
|
1797
|
+
catalogTrackName,
|
|
1798
|
+
esi,
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// Only now feed source symbols into FEC. The decoder must still receive
|
|
1805
|
+
// the K-th source symbol after trackBlock() marks a clean block complete:
|
|
1806
|
+
// Worker-backed decoders use that symbol for their own block lifecycle
|
|
1807
|
+
// and stats. Suppress any synchronous recovered-block return when the
|
|
1808
|
+
// manager already completed the block, because every source frame was
|
|
1809
|
+
// parsed and emitted above.
|
|
1810
|
+
if (this.#fecManager && sourceFec) {
|
|
1811
|
+
const alreadyCompleted = catalogTrackName === undefined
|
|
1812
|
+
? this.#fecManager.isBlockCompleted(sourceFec.sbn, info.packetId)
|
|
1813
|
+
: this.#fecManager.isBlockCompleted(
|
|
1814
|
+
sourceFec.sbn,
|
|
1815
|
+
info.packetId,
|
|
1816
|
+
catalogTrackName,
|
|
1817
|
+
);
|
|
1818
|
+
const recovered = catalogTrackName === undefined
|
|
1819
|
+
? this.#fecManager.feedSourceSymbol(
|
|
1820
|
+
info.packetId,
|
|
1821
|
+
sourceFec.ssId,
|
|
1822
|
+
sourceFec.symbolData,
|
|
1823
|
+
undefined,
|
|
1824
|
+
)
|
|
1825
|
+
: this.#fecManager.feedSourceSymbol(
|
|
1826
|
+
info.packetId,
|
|
1827
|
+
sourceFec.ssId,
|
|
1828
|
+
sourceFec.symbolData,
|
|
1829
|
+
undefined,
|
|
1830
|
+
catalogTrackName,
|
|
1831
|
+
);
|
|
1832
|
+
if (recovered) {
|
|
1833
|
+
if (!alreadyCompleted && this.#classifyPacketId(info.packetId) !== null) {
|
|
1834
|
+
if (catalogTrackName === undefined) {
|
|
1835
|
+
this.#fecManager.cancelBlockTimeout(sourceFec.sbn, info.packetId);
|
|
1836
|
+
} else {
|
|
1837
|
+
this.#fecManager.cancelBlockTimeout(
|
|
1838
|
+
sourceFec.sbn,
|
|
1839
|
+
info.packetId,
|
|
1840
|
+
catalogTrackName,
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
// Only route recovered blocks that needed actual repair.
|
|
1845
|
+
if (!alreadyCompleted) {
|
|
1846
|
+
this.#fecManager.countRecovery(info.packetId);
|
|
1847
|
+
if (catalogTrackName === undefined) {
|
|
1848
|
+
this.#fecManager.markBlockCompleted(sourceFec.sbn, info.packetId);
|
|
1849
|
+
} else {
|
|
1850
|
+
this.#fecManager.markBlockCompleted(
|
|
1851
|
+
sourceFec.sbn,
|
|
1852
|
+
info.packetId,
|
|
1853
|
+
catalogTrackName,
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
this.#routeRecoveredBlock(
|
|
1857
|
+
sourceFec.sbn,
|
|
1858
|
+
recovered,
|
|
1859
|
+
info.packetId,
|
|
1860
|
+
undefined,
|
|
1861
|
+
catalogTrackName,
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* Set temp context and delegate to parser.feed() for MFU extraction,
|
|
1871
|
+
* reassembly, keyframe detection, and frame emission via callbacks.
|
|
1872
|
+
*/
|
|
1873
|
+
#feedViaParser(
|
|
1874
|
+
packet: Uint8Array,
|
|
1875
|
+
info: {
|
|
1876
|
+
packetId: number;
|
|
1877
|
+
sequenceNumber: number;
|
|
1878
|
+
fecPayloadId?: { ssId: number } | null;
|
|
1879
|
+
},
|
|
1880
|
+
fecInfo?: { sbn: number; esi: number; oversized?: boolean },
|
|
1881
|
+
parserOptions?: {
|
|
1882
|
+
skipFecPayloadId?: boolean;
|
|
1883
|
+
recovered?: boolean;
|
|
1884
|
+
source?: MmtpSourcePath;
|
|
1885
|
+
deliveryProvenance?: MmtpDeliveryProvenance;
|
|
1886
|
+
catalogTrackName?: string;
|
|
1887
|
+
},
|
|
1888
|
+
): void {
|
|
1889
|
+
const diag = this.#diagFor(info.packetId, parserOptions?.catalogTrackName);
|
|
1890
|
+
const before = this.#parser.reassembler.getStatus();
|
|
1891
|
+
diag.packets++;
|
|
1892
|
+
diag.lastPacketLength = packet.byteLength;
|
|
1893
|
+
diag.lastSequenceNumber = info.sequenceNumber;
|
|
1894
|
+
if (info.fecPayloadId) diag.lastFecSsId = info.fecPayloadId.ssId;
|
|
1895
|
+
this.#currentFecInfo = fecInfo;
|
|
1896
|
+
this.#currentSeqNum = info.sequenceNumber;
|
|
1897
|
+
try {
|
|
1898
|
+
this.#parser.feed(packet, {
|
|
1899
|
+
skipFecPayloadId: parserOptions?.skipFecPayloadId,
|
|
1900
|
+
recovered: parserOptions?.recovered,
|
|
1901
|
+
source: parserOptions?.source,
|
|
1902
|
+
deliveryProvenance: parserOptions?.deliveryProvenance,
|
|
1903
|
+
catalogTrackName: parserOptions?.catalogTrackName,
|
|
1904
|
+
});
|
|
1905
|
+
} finally {
|
|
1906
|
+
this.#currentFecInfo = undefined;
|
|
1907
|
+
}
|
|
1908
|
+
const after = this.#parser.reassembler.getStatus();
|
|
1909
|
+
const fragmentDelta = after.stats.totalFragments - before.stats.totalFragments;
|
|
1910
|
+
const reassembledDelta = after.stats.reassembledMFUs - before.stats.reassembledMFUs;
|
|
1911
|
+
if (fragmentDelta > 0) diag.fragments += fragmentDelta;
|
|
1912
|
+
else diag.noFragment++;
|
|
1913
|
+
if (reassembledDelta > 0) diag.reassembled += reassembledDelta;
|
|
1914
|
+
diag.pendingBuffers = after.bufferedMFUs;
|
|
1915
|
+
diag.pendingFragments = after.pendingFragments;
|
|
1916
|
+
diag.pendingSamples = after.pendingSamples;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
#feedFecTransportSymbol(
|
|
1920
|
+
packetId: number,
|
|
1921
|
+
sbn: number,
|
|
1922
|
+
esi: number,
|
|
1923
|
+
data: Uint8Array,
|
|
1924
|
+
source: MmtpSourcePath,
|
|
1925
|
+
catalogTrackName?: string,
|
|
1926
|
+
): boolean {
|
|
1927
|
+
const fecManager = this.#fecManager;
|
|
1928
|
+
if (!fecManager) return false;
|
|
1929
|
+
return catalogTrackName === undefined
|
|
1930
|
+
? fecManager.feedSymbolFromSource(packetId, sbn, esi, data, source)
|
|
1931
|
+
: fecManager.feedSymbolFromSource(
|
|
1932
|
+
packetId,
|
|
1933
|
+
sbn,
|
|
1934
|
+
esi,
|
|
1935
|
+
data,
|
|
1936
|
+
source,
|
|
1937
|
+
catalogTrackName,
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
#diagFor(packetId: number, catalogTrackName?: string): ParserDiagnostics {
|
|
1942
|
+
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
1943
|
+
let diag = this.#parserDiagnostics.get(streamKey);
|
|
1944
|
+
if (!diag) {
|
|
1945
|
+
diag = {
|
|
1946
|
+
packets: 0,
|
|
1947
|
+
frames: 0,
|
|
1948
|
+
initSegments: 0,
|
|
1949
|
+
noFragment: 0,
|
|
1950
|
+
fragments: 0,
|
|
1951
|
+
reassembled: 0,
|
|
1952
|
+
pendingBuffers: 0,
|
|
1953
|
+
pendingFragments: 0,
|
|
1954
|
+
lastPacketLength: 0,
|
|
1955
|
+
lastSequenceNumber: 0,
|
|
1956
|
+
directAudioSourceEmitted: 0,
|
|
1957
|
+
directAudioSourceRegressions: 0,
|
|
1958
|
+
directAudioRecoveredQueued: 0,
|
|
1959
|
+
directAudioRecoveredDrained: 0,
|
|
1960
|
+
directAudioRecoveredStaleDropped: 0,
|
|
1961
|
+
};
|
|
1962
|
+
this.#parserDiagnostics.set(streamKey, diag);
|
|
1963
|
+
}
|
|
1964
|
+
return diag;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
#shouldEmitFrame(
|
|
1968
|
+
packetId: number,
|
|
1969
|
+
keyframe: boolean,
|
|
1970
|
+
catalogTrackName?: string,
|
|
1971
|
+
): boolean {
|
|
1972
|
+
if (!this.#fecManager) return true;
|
|
1973
|
+
if (!this.#videoPacketIds.has(packetId)) return true;
|
|
1974
|
+
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
1975
|
+
if (this.#trackSynced.has(streamKey)) return true;
|
|
1976
|
+
if (!keyframe) {
|
|
1977
|
+
this.#droppedPreSyncVideo.add(streamKey);
|
|
1978
|
+
return false;
|
|
1979
|
+
}
|
|
1980
|
+
this.#trackSynced.add(streamKey);
|
|
1981
|
+
this.#droppedPreSyncVideo.delete(streamKey);
|
|
1982
|
+
return true;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
#opensFecSync(
|
|
1986
|
+
packetId: number,
|
|
1987
|
+
keyframe: boolean,
|
|
1988
|
+
catalogTrackName?: string,
|
|
1989
|
+
): boolean {
|
|
1990
|
+
const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
|
|
1991
|
+
return !!this.#fecManager &&
|
|
1992
|
+
this.#videoPacketIds.has(packetId) &&
|
|
1993
|
+
!this.#trackSynced.has(streamKey) &&
|
|
1994
|
+
this.#droppedPreSyncVideo.has(streamKey) &&
|
|
1995
|
+
keyframe;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
#noteEmittedVideoSeq(streamKey: string, packetSequenceNumber: number | undefined): void {
|
|
1999
|
+
if (packetSequenceNumber === undefined || !Number.isFinite(packetSequenceNumber)) return;
|
|
2000
|
+
const latest = this.#latestVideoSeqByStream.get(streamKey);
|
|
2001
|
+
if (latest === undefined || packetSequenceNumber > latest) {
|
|
2002
|
+
this.#latestVideoSeqByStream.set(streamKey, packetSequenceNumber);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
#noteSourcePacketSeq(
|
|
2007
|
+
packetId: number,
|
|
2008
|
+
packetSequenceNumber: number | undefined,
|
|
2009
|
+
catalogTrackName?: string,
|
|
2010
|
+
): void {
|
|
2011
|
+
if (packetSequenceNumber === undefined || !Number.isFinite(packetSequenceNumber)) return;
|
|
2012
|
+
this.#sourcePktSeqs.add(
|
|
2013
|
+
this.#packetSeqKey(packetId, packetSequenceNumber, catalogTrackName),
|
|
2014
|
+
);
|
|
2015
|
+
// Eviction: keep the most recent 2000 entries (covers ~62 blocks at K=32/30fps).
|
|
2016
|
+
// FEC recovery completes within the catalog-derived holdback window, so older
|
|
2017
|
+
// sequence numbers cannot be useful for duplicate suppression.
|
|
2018
|
+
if (this.#sourcePktSeqs.size > 4000) {
|
|
2019
|
+
const iter = this.#sourcePktSeqs.values();
|
|
2020
|
+
for (let i = 0; i < 2000; i++) iter.next();
|
|
2021
|
+
const keep = new Set<string>();
|
|
2022
|
+
for (const v of iter) keep.add(v);
|
|
2023
|
+
this.#sourcePktSeqs = keep;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
#routeTrackPacket(
|
|
2028
|
+
packet: Uint8Array,
|
|
2029
|
+
packetId: number,
|
|
2030
|
+
packetSequenceNumber: number,
|
|
2031
|
+
meta: Pick<
|
|
2032
|
+
RoutedPacketMeta,
|
|
2033
|
+
"recovered" | "skipFecPayloadId" | "catalogTrackName" | "deliveryProvenance"
|
|
2034
|
+
>,
|
|
2035
|
+
): void {
|
|
2036
|
+
if (!Number.isFinite(packetSequenceNumber)) return;
|
|
2037
|
+
const callback = this.#trackPacketCallbacks.get(packetId);
|
|
2038
|
+
if (!callback) return;
|
|
2039
|
+
const key = this.#packetSeqKey(packetId, packetSequenceNumber, meta.catalogTrackName);
|
|
2040
|
+
if (this.#routedPacketSeqs.has(key)) return;
|
|
2041
|
+
this.#routedPacketSeqs.add(key);
|
|
2042
|
+
if (this.#routedPacketSeqs.size > 4000) {
|
|
2043
|
+
const iter = this.#routedPacketSeqs.values();
|
|
2044
|
+
for (let i = 0; i < 2000; i++) iter.next();
|
|
2045
|
+
const keep = new Set<string>();
|
|
2046
|
+
for (const value of iter) keep.add(value);
|
|
2047
|
+
this.#routedPacketSeqs = keep;
|
|
2048
|
+
}
|
|
2049
|
+
callback(packet, {
|
|
2050
|
+
packetId,
|
|
2051
|
+
packetSequenceNumber,
|
|
2052
|
+
recovered: meta.recovered,
|
|
2053
|
+
skipFecPayloadId: meta.skipFecPayloadId,
|
|
2054
|
+
...(meta.catalogTrackName === undefined
|
|
2055
|
+
? {}
|
|
2056
|
+
: { catalogTrackName: meta.catalogTrackName }),
|
|
2057
|
+
...(meta.deliveryProvenance === undefined
|
|
2058
|
+
? {}
|
|
2059
|
+
: { deliveryProvenance: meta.deliveryProvenance }),
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
#isStaleRecoveredVideo(
|
|
2064
|
+
packetId: number,
|
|
2065
|
+
packetSequenceNumber: number,
|
|
2066
|
+
catalogTrackName?: string,
|
|
2067
|
+
): boolean {
|
|
2068
|
+
const latest = this.#latestVideoSeqByStream.get(
|
|
2069
|
+
this.#mediaStreamKey(packetId, catalogTrackName),
|
|
2070
|
+
);
|
|
2071
|
+
return latest !== undefined && packetSequenceNumber <= latest;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
#mediaStreamKey(packetId: number, catalogTrackName?: string): string {
|
|
2075
|
+
return JSON.stringify([packetId, catalogTrackName ?? null]);
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
#packetIdFromStreamKey(key: string): number {
|
|
2079
|
+
try {
|
|
2080
|
+
const decoded = JSON.parse(key);
|
|
2081
|
+
if (Array.isArray(decoded) && typeof decoded[0] === "number") return decoded[0];
|
|
2082
|
+
} catch {
|
|
2083
|
+
// Internal keys are JSON tuples; malformed legacy keys should not happen.
|
|
2084
|
+
}
|
|
2085
|
+
return 0;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
#mseAssemblerKey(packetId: number, catalogTrackName?: string): string {
|
|
2089
|
+
return this.#mediaStreamKey(packetId, catalogTrackName);
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
#mseAssemblerFor(
|
|
2093
|
+
packetId: number,
|
|
2094
|
+
catalogTrackName?: string,
|
|
2095
|
+
): CmafBlockAssembler | undefined {
|
|
2096
|
+
const exact = this.#mseAssemblers.get(
|
|
2097
|
+
this.#mseAssemblerKey(packetId, catalogTrackName),
|
|
2098
|
+
);
|
|
2099
|
+
if (exact || catalogTrackName === undefined) return exact;
|
|
2100
|
+
if (!this.#isCatalogTrackUnambiguous(packetId, catalogTrackName)) return undefined;
|
|
2101
|
+
return this.#mseAssemblers.get(this.#mseAssemblerKey(packetId));
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
#isCatalogTrackUnambiguous(
|
|
2105
|
+
packetId: number,
|
|
2106
|
+
catalogTrackName?: string,
|
|
2107
|
+
): boolean {
|
|
2108
|
+
if (catalogTrackName === undefined) return true;
|
|
2109
|
+
const names = this.#catalogTrackNamesByPacketId.get(packetId);
|
|
2110
|
+
return names?.size === 1 && names.has(catalogTrackName);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
#resolveRecoveredTiming(
|
|
2114
|
+
packetId: number,
|
|
2115
|
+
mpuSequenceNumber: number,
|
|
2116
|
+
movieFragmentSequenceNumber: number | undefined,
|
|
2117
|
+
sampleNumber: number | undefined,
|
|
2118
|
+
mmtpTimestamp: bigint,
|
|
2119
|
+
catalogTrackName?: string,
|
|
2120
|
+
): { authoredTiming?: AuthoredTiming; timestamp: number } | null {
|
|
2121
|
+
// Non-timed MFUs do not carry a movie-fragment/sample identity. They are
|
|
2122
|
+
// outside the ISO-BMFF presentation timeline, so retain the raw MMTP
|
|
2123
|
+
// timestamp path used by the untimed router contract.
|
|
2124
|
+
if (movieFragmentSequenceNumber === undefined || sampleNumber === undefined) {
|
|
2125
|
+
return { timestamp: mmtpTimestampToMicros(Number(mmtpTimestamp)) };
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// Claim the resolver entry before scaling it. The claim is the single
|
|
2129
|
+
// consume operation for a recovered sample; doing lookup → scale → claim
|
|
2130
|
+
// would leave an invalid entry live forever when scaling rejects it.
|
|
2131
|
+
const authoredTiming = this.#parser.consumeAuthoredTimingFor(
|
|
2132
|
+
packetId,
|
|
2133
|
+
mpuSequenceNumber,
|
|
2134
|
+
movieFragmentSequenceNumber,
|
|
2135
|
+
sampleNumber,
|
|
2136
|
+
catalogTrackName,
|
|
2137
|
+
);
|
|
2138
|
+
// Recovery is accepted only when the timed stream's FT=1 metadata names
|
|
2139
|
+
// this exact sample. MMTP send timestamps describe transport arrival, not
|
|
2140
|
+
// the authored presentation timeline, so they are never a fallback.
|
|
2141
|
+
if (authoredTiming === null) return null;
|
|
2142
|
+
const scaled = scaleAuthoredTiming(authoredTiming, 1_000_000);
|
|
2143
|
+
if (scaled === null) return null;
|
|
2144
|
+
return { authoredTiming, timestamp: scaled.timestamp };
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
#packetSeqKey(
|
|
2148
|
+
packetId: number,
|
|
2149
|
+
packetSequenceNumber: number,
|
|
2150
|
+
catalogTrackName?: string,
|
|
2151
|
+
): string {
|
|
2152
|
+
return JSON.stringify([packetId, catalogTrackName ?? null, packetSequenceNumber]);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
#pendingRecoveredKey(meta: RoutedFrameMeta): string {
|
|
2156
|
+
return `${meta.timestamp}:${meta.packetSequenceNumber ?? ""}`;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
#pendingRecoveredQueue(streamKey: string): Map<string, PendingRecoveredVideoFrame> {
|
|
2160
|
+
let queue = this.#pendingDirectRecoveredVideoByStream.get(streamKey);
|
|
2161
|
+
if (!queue) {
|
|
2162
|
+
queue = new Map();
|
|
2163
|
+
this.#pendingDirectRecoveredVideoByStream.set(streamKey, queue);
|
|
2164
|
+
}
|
|
2165
|
+
return queue;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
#comparePendingRecoveredVideo(
|
|
2169
|
+
a: PendingRecoveredVideoFrame,
|
|
2170
|
+
b: PendingRecoveredVideoFrame,
|
|
2171
|
+
): number {
|
|
2172
|
+
if (a.meta.timestamp !== b.meta.timestamp) return a.meta.timestamp - b.meta.timestamp;
|
|
2173
|
+
const aSeq = a.meta.packetSequenceNumber ?? Number.MAX_SAFE_INTEGER;
|
|
2174
|
+
const bSeq = b.meta.packetSequenceNumber ?? Number.MAX_SAFE_INTEGER;
|
|
2175
|
+
return aSeq - bSeq;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
#emitDirectVideo(
|
|
2179
|
+
packetId: number,
|
|
2180
|
+
data: Uint8Array,
|
|
2181
|
+
meta: RoutedFrameMeta,
|
|
2182
|
+
cb: FrameCallback | null,
|
|
2183
|
+
): boolean {
|
|
2184
|
+
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
2185
|
+
const lastEmitted = this.#lastDirectEmittedVideoTsByStream.get(streamKey);
|
|
2186
|
+
if (lastEmitted !== undefined && meta.timestamp <= lastEmitted) return false;
|
|
2187
|
+
this.#lastDirectEmittedVideoTsByStream.set(streamKey, meta.timestamp);
|
|
2188
|
+
// Record the emitted sequence number HERE — at the real emit — not when a
|
|
2189
|
+
// recovered frame is queued. #routeDirectRecoveredVideo may enqueue a frame
|
|
2190
|
+
// that is ahead of the source cursor; noting its seq before it drains would
|
|
2191
|
+
// make #isStaleRecoveredVideo drop an earlier gap frame that arrives later
|
|
2192
|
+
// (it would look <= the queued-but-unemitted seq). The high-water mark must
|
|
2193
|
+
// reflect what actually reached the decoder callback.
|
|
2194
|
+
this.#noteEmittedVideoSeq(streamKey, meta.packetSequenceNumber);
|
|
2195
|
+
cb?.(data, meta);
|
|
2196
|
+
return true;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
#drainDirectRecoveredVideoUpTo(
|
|
2200
|
+
streamKey: string,
|
|
2201
|
+
packetId: number,
|
|
2202
|
+
horizonTs: number,
|
|
2203
|
+
cb: FrameCallback | null,
|
|
2204
|
+
): void {
|
|
2205
|
+
const queue = this.#pendingDirectRecoveredVideoByStream.get(streamKey);
|
|
2206
|
+
if (!queue || queue.size === 0) return;
|
|
2207
|
+
|
|
2208
|
+
const ready = [...queue.values()]
|
|
2209
|
+
.filter(frame => frame.meta.timestamp <= horizonTs)
|
|
2210
|
+
.sort((a, b) => this.#comparePendingRecoveredVideo(a, b));
|
|
2211
|
+
|
|
2212
|
+
for (const frame of ready) {
|
|
2213
|
+
queue.delete(this.#pendingRecoveredKey(frame.meta));
|
|
2214
|
+
this.#emitDirectVideo(packetId, frame.data, frame.meta, cb);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
if (queue.size === 0) {
|
|
2218
|
+
this.#pendingDirectRecoveredVideoByStream.delete(streamKey);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
#enqueueDirectRecoveredVideo(
|
|
2223
|
+
packetId: number,
|
|
2224
|
+
data: Uint8Array,
|
|
2225
|
+
meta: RoutedFrameMeta,
|
|
2226
|
+
): void {
|
|
2227
|
+
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
2228
|
+
const lastEmitted = this.#lastDirectEmittedVideoTsByStream.get(streamKey);
|
|
2229
|
+
if (lastEmitted !== undefined && meta.timestamp <= lastEmitted) return;
|
|
2230
|
+
|
|
2231
|
+
const queue = this.#pendingRecoveredQueue(streamKey);
|
|
2232
|
+
queue.set(this.#pendingRecoveredKey(meta), { data, meta: { ...meta } });
|
|
2233
|
+
|
|
2234
|
+
if (queue.size > DIRECT_RECOVERED_VIDEO_QUEUE_LIMIT) {
|
|
2235
|
+
const keep = [...queue.entries()]
|
|
2236
|
+
.sort((a, b) => this.#comparePendingRecoveredVideo(a[1], b[1]))
|
|
2237
|
+
.slice(-Math.floor(DIRECT_RECOVERED_VIDEO_QUEUE_LIMIT / 2));
|
|
2238
|
+
queue.clear();
|
|
2239
|
+
for (const [key, frame] of keep) queue.set(key, frame);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
#routeDirectSourceVideo(
|
|
2244
|
+
packetId: number,
|
|
2245
|
+
data: Uint8Array,
|
|
2246
|
+
meta: RoutedFrameMeta,
|
|
2247
|
+
cb: FrameCallback | null,
|
|
2248
|
+
): void {
|
|
2249
|
+
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
2250
|
+
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
2251
|
+
if (lastSourceTs === undefined || meta.timestamp > lastSourceTs) {
|
|
2252
|
+
this.#lastDirectSourceVideoTsByStream.set(streamKey, meta.timestamp);
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// Recovered FEC may arrive before the source stream reaches the same media
|
|
2256
|
+
// time. Drain those gap fills immediately before the source frame that proves
|
|
2257
|
+
// the live cursor caught up, preserving the decoder dependency chain.
|
|
2258
|
+
this.#drainDirectRecoveredVideoUpTo(streamKey, packetId, meta.timestamp, cb);
|
|
2259
|
+
this.#emitDirectVideo(packetId, data, meta, cb);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
#routeDirectRecoveredVideo(
|
|
2263
|
+
packetId: number,
|
|
2264
|
+
data: Uint8Array,
|
|
2265
|
+
meta: RoutedFrameMeta,
|
|
2266
|
+
cb: FrameCallback | null,
|
|
2267
|
+
): void {
|
|
2268
|
+
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
2269
|
+
this.#enqueueDirectRecoveredVideo(packetId, data, meta);
|
|
2270
|
+
const lastSourceTs = this.#lastDirectSourceVideoTsByStream.get(streamKey);
|
|
2271
|
+
this.#drainDirectRecoveredVideoUpTo(
|
|
2272
|
+
streamKey,
|
|
2273
|
+
packetId,
|
|
2274
|
+
lastSourceTs ?? Number.POSITIVE_INFINITY,
|
|
2275
|
+
cb,
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
#pendingRecoveredAudioQueue(streamKey: string): Map<string, PendingRecoveredAudioFrame> {
|
|
2280
|
+
let queue = this.#pendingDirectRecoveredAudioByStream.get(streamKey);
|
|
2281
|
+
if (!queue) {
|
|
2282
|
+
queue = new Map();
|
|
2283
|
+
this.#pendingDirectRecoveredAudioByStream.set(streamKey, queue);
|
|
2284
|
+
}
|
|
2285
|
+
return queue;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
#directAudioCursor(meta: RoutedFrameMeta): DirectAudioCursor {
|
|
2289
|
+
return {
|
|
2290
|
+
timestamp: meta.timestamp,
|
|
2291
|
+
// A recovered frame without transport sequence metadata must drain before
|
|
2292
|
+
// an equal-timestamp source horizon instead of remaining queued forever.
|
|
2293
|
+
sequenceNumber: meta.packetSequenceNumber ?? Number.MIN_SAFE_INTEGER,
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
#compareDirectAudioCursor(a: DirectAudioCursor, b: DirectAudioCursor): number {
|
|
2298
|
+
if (a.timestamp !== b.timestamp) return a.timestamp - b.timestamp;
|
|
2299
|
+
return a.sequenceNumber - b.sequenceNumber;
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
#emitDirectAudio(
|
|
2303
|
+
packetId: number,
|
|
2304
|
+
data: Uint8Array,
|
|
2305
|
+
meta: RoutedFrameMeta,
|
|
2306
|
+
cb: FrameCallback | null,
|
|
2307
|
+
): boolean {
|
|
2308
|
+
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
2309
|
+
const cursor = this.#directAudioCursor(meta);
|
|
2310
|
+
const lastEmitted = this.#lastDirectEmittedAudioCursorByStream.get(streamKey);
|
|
2311
|
+
if (
|
|
2312
|
+
lastEmitted !== undefined &&
|
|
2313
|
+
this.#compareDirectAudioCursor(cursor, lastEmitted) <= 0
|
|
2314
|
+
) return false;
|
|
2315
|
+
this.#lastDirectEmittedAudioCursorByStream.set(streamKey, cursor);
|
|
2316
|
+
cb?.(data, meta);
|
|
2317
|
+
return true;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
#emitDirectSourceAudio(
|
|
2321
|
+
packetId: number,
|
|
2322
|
+
data: Uint8Array,
|
|
2323
|
+
meta: RoutedFrameMeta,
|
|
2324
|
+
cb: FrameCallback | null,
|
|
2325
|
+
): void {
|
|
2326
|
+
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
2327
|
+
const cursor = this.#directAudioCursor(meta);
|
|
2328
|
+
const lastEmitted = this.#lastDirectEmittedAudioCursorByStream.get(streamKey);
|
|
2329
|
+
const diag = this.#diagFor(packetId, meta.catalogTrackName);
|
|
2330
|
+
diag.directAudioSourceEmitted++;
|
|
2331
|
+
if (
|
|
2332
|
+
lastEmitted !== undefined &&
|
|
2333
|
+
this.#compareDirectAudioCursor(cursor, lastEmitted) <= 0
|
|
2334
|
+
) {
|
|
2335
|
+
// Source packets already passed transport/parser validation. Preserve
|
|
2336
|
+
// them even when unordered multicast completion moves the callback
|
|
2337
|
+
// cursor backward; only recovered frames are optional stale gap fills.
|
|
2338
|
+
diag.directAudioSourceRegressions++;
|
|
2339
|
+
} else {
|
|
2340
|
+
this.#lastDirectEmittedAudioCursorByStream.set(streamKey, cursor);
|
|
2341
|
+
}
|
|
2342
|
+
cb?.(data, meta);
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
#drainDirectRecoveredAudioUpTo(
|
|
2346
|
+
streamKey: string,
|
|
2347
|
+
packetId: number,
|
|
2348
|
+
horizon: DirectAudioCursor,
|
|
2349
|
+
cb: FrameCallback | null,
|
|
2350
|
+
): void {
|
|
2351
|
+
const queue = this.#pendingDirectRecoveredAudioByStream.get(streamKey);
|
|
2352
|
+
if (!queue || queue.size === 0) return;
|
|
2353
|
+
|
|
2354
|
+
const ready = [...queue.values()]
|
|
2355
|
+
.filter(frame => this.#compareDirectAudioCursor(
|
|
2356
|
+
this.#directAudioCursor(frame.meta),
|
|
2357
|
+
horizon,
|
|
2358
|
+
) <= 0)
|
|
2359
|
+
.sort((a, b) => this.#compareDirectAudioCursor(
|
|
2360
|
+
this.#directAudioCursor(a.meta),
|
|
2361
|
+
this.#directAudioCursor(b.meta),
|
|
2362
|
+
));
|
|
2363
|
+
|
|
2364
|
+
for (const frame of ready) {
|
|
2365
|
+
queue.delete(this.#pendingRecoveredKey(frame.meta));
|
|
2366
|
+
if (this.#emitDirectAudio(packetId, frame.data, frame.meta, cb)) {
|
|
2367
|
+
this.#diagFor(packetId, frame.meta.catalogTrackName)
|
|
2368
|
+
.directAudioRecoveredDrained++;
|
|
2369
|
+
} else {
|
|
2370
|
+
this.#diagFor(packetId, frame.meta.catalogTrackName)
|
|
2371
|
+
.directAudioRecoveredStaleDropped++;
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
if (queue.size === 0) {
|
|
2376
|
+
this.#pendingDirectRecoveredAudioByStream.delete(streamKey);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
#routeDirectSourceAudio(
|
|
2381
|
+
packetId: number,
|
|
2382
|
+
data: Uint8Array,
|
|
2383
|
+
meta: RoutedFrameMeta,
|
|
2384
|
+
cb: FrameCallback | null,
|
|
2385
|
+
): void {
|
|
2386
|
+
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
2387
|
+
const cursor = this.#directAudioCursor(meta);
|
|
2388
|
+
const lastSource = this.#lastDirectSourceAudioCursorByStream.get(streamKey);
|
|
2389
|
+
if (
|
|
2390
|
+
lastSource === undefined ||
|
|
2391
|
+
this.#compareDirectAudioCursor(cursor, lastSource) > 0
|
|
2392
|
+
) {
|
|
2393
|
+
this.#lastDirectSourceAudioCursorByStream.set(streamKey, cursor);
|
|
2394
|
+
}
|
|
2395
|
+
this.#drainDirectRecoveredAudioUpTo(streamKey, packetId, cursor, cb);
|
|
2396
|
+
this.#emitDirectSourceAudio(packetId, data, meta, cb);
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
#routeDirectRecoveredAudio(
|
|
2400
|
+
packetId: number,
|
|
2401
|
+
data: Uint8Array,
|
|
2402
|
+
meta: RoutedFrameMeta,
|
|
2403
|
+
cb: FrameCallback | null,
|
|
2404
|
+
): void {
|
|
2405
|
+
const streamKey = this.#mediaStreamKey(packetId, meta.catalogTrackName);
|
|
2406
|
+
const cursor = this.#directAudioCursor(meta);
|
|
2407
|
+
const lastEmitted = this.#lastDirectEmittedAudioCursorByStream.get(streamKey);
|
|
2408
|
+
if (
|
|
2409
|
+
lastEmitted !== undefined &&
|
|
2410
|
+
this.#compareDirectAudioCursor(cursor, lastEmitted) <= 0
|
|
2411
|
+
) {
|
|
2412
|
+
this.#diagFor(packetId, meta.catalogTrackName)
|
|
2413
|
+
.directAudioRecoveredStaleDropped++;
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
const queue = this.#pendingRecoveredAudioQueue(streamKey);
|
|
2418
|
+
queue.set(this.#pendingRecoveredKey(meta), { data, meta: { ...meta } });
|
|
2419
|
+
this.#diagFor(packetId, meta.catalogTrackName).directAudioRecoveredQueued++;
|
|
2420
|
+
if (queue.size > DIRECT_RECOVERED_AUDIO_QUEUE_LIMIT) {
|
|
2421
|
+
const keep = [...queue.entries()]
|
|
2422
|
+
.sort((a, b) => this.#compareDirectAudioCursor(
|
|
2423
|
+
this.#directAudioCursor(a[1].meta),
|
|
2424
|
+
this.#directAudioCursor(b[1].meta),
|
|
2425
|
+
))
|
|
2426
|
+
.slice(-Math.floor(DIRECT_RECOVERED_AUDIO_QUEUE_LIMIT / 2));
|
|
2427
|
+
queue.clear();
|
|
2428
|
+
for (const [key, frame] of keep) queue.set(key, frame);
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
const lastSource = this.#lastDirectSourceAudioCursorByStream.get(streamKey);
|
|
2432
|
+
if (lastSource !== undefined) {
|
|
2433
|
+
this.#drainDirectRecoveredAudioUpTo(streamKey, packetId, lastSource, cb);
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
// ── Private: FEC recovery routing ───────────────────────────────
|
|
2438
|
+
|
|
2439
|
+
/** Route WASM-recovered block through appropriate path based on container kind. */
|
|
2440
|
+
#routeRecoveredBlock(
|
|
2441
|
+
blockId: number,
|
|
2442
|
+
recovered: Uint8Array,
|
|
2443
|
+
ownerPacketId?: number,
|
|
2444
|
+
blockK?: number,
|
|
2445
|
+
catalogTrackName?: string,
|
|
2446
|
+
): void {
|
|
2447
|
+
// MMTP path: use processFecBlock for MMTP-wrapped recovery.
|
|
2448
|
+
// Recovered symbols are individual MMTP packets that may be fragments
|
|
2449
|
+
// (FI=1/2/3) of a larger MFU. Route through the reassembler so
|
|
2450
|
+
// multi-packet MFUs (e.g., keyframes spanning 15+ packets) are
|
|
2451
|
+
// properly assembled before reaching the decoder.
|
|
2452
|
+
// In object mode, blockK comes from repair RSB_length (per-block variable K).
|
|
2453
|
+
const k = blockK ?? this.#kForPacket(ownerPacketId);
|
|
2454
|
+
const packetSymbolSize = this.#symbolSizeForPacket(ownerPacketId);
|
|
2455
|
+
const symbolSize = packetSymbolSize > 0 ? packetSymbolSize : undefined;
|
|
2456
|
+
const result = processFecBlock(recovered, {
|
|
2457
|
+
k,
|
|
2458
|
+
symbolSize,
|
|
2459
|
+
ownerPacketId,
|
|
2460
|
+
});
|
|
2461
|
+
this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
#routeVerifiedFecSymbols(
|
|
2465
|
+
blockId: number,
|
|
2466
|
+
result: ProcessFecBlockResult,
|
|
2467
|
+
ownerPacketId?: number,
|
|
2468
|
+
catalogTrackName?: string,
|
|
2469
|
+
): void {
|
|
2470
|
+
if (!this.#manifestVerifier || ownerPacketId == null) {
|
|
2471
|
+
this.#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
if (!this.#resolveManifestIdentity && !this.#resolveManifestIdentityForRoute) {
|
|
2475
|
+
console.warn(`[MmtpRouter] manifest identity resolver unavailable; recovered media is fail-open for packetId=${ownerPacketId} block=${blockId}`);
|
|
2476
|
+
this.#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
const queueKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
|
|
2480
|
+
const queue = this.#manifestRecoveryQueues.get(queueKey) ?? [];
|
|
2481
|
+
queue.push({ blockId, result, ownerPacketId, catalogTrackName });
|
|
2482
|
+
this.#manifestRecoveryQueues.set(queueKey, queue);
|
|
2483
|
+
if (queue.length > 1) return;
|
|
2484
|
+
const drain = (): void => {
|
|
2485
|
+
const next = queue[0];
|
|
2486
|
+
if (!next) {
|
|
2487
|
+
this.#manifestRecoveryQueues.delete(queueKey);
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
this.#admitRecoveredResult(
|
|
2491
|
+
next.blockId,
|
|
2492
|
+
next.result,
|
|
2493
|
+
next.ownerPacketId,
|
|
2494
|
+
next.catalogTrackName,
|
|
2495
|
+
() => {
|
|
2496
|
+
queue.shift();
|
|
2497
|
+
drain();
|
|
2498
|
+
},
|
|
2499
|
+
);
|
|
2500
|
+
};
|
|
2501
|
+
drain();
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
#admitRecoveredResult(
|
|
2505
|
+
blockId: number,
|
|
2506
|
+
result: ProcessFecBlockResult,
|
|
2507
|
+
ownerPacketId: number,
|
|
2508
|
+
catalogTrackName: string | undefined,
|
|
2509
|
+
done: () => void,
|
|
2510
|
+
): void {
|
|
2511
|
+
const verifier = this.#manifestVerifier;
|
|
2512
|
+
const resolveIdentity = this.#resolveManifestIdentity;
|
|
2513
|
+
const resolveIdentityForRoute = this.#resolveManifestIdentityForRoute;
|
|
2514
|
+
if (!verifier) {
|
|
2515
|
+
this.#routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName);
|
|
2516
|
+
done();
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
if (!resolveIdentity && !resolveIdentityForRoute) throw new Error("manifest identity resolver disappeared during admission");
|
|
2520
|
+
const symbols = result.symbols;
|
|
2521
|
+
const admitNext = (index: number): void => {
|
|
2522
|
+
const symbol = symbols[index];
|
|
2523
|
+
if (!symbol) {
|
|
2524
|
+
done();
|
|
2525
|
+
return;
|
|
2526
|
+
}
|
|
2527
|
+
const identity = resolveIdentityForRoute
|
|
2528
|
+
? resolveIdentityForRoute({
|
|
2529
|
+
packetId: ownerPacketId,
|
|
2530
|
+
sbn: blockId,
|
|
2531
|
+
esi: symbol.index,
|
|
2532
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
2533
|
+
recovered: true,
|
|
2534
|
+
sourceSymbolFormat: "fec-padded-recovery",
|
|
2535
|
+
})
|
|
2536
|
+
: resolveIdentity!(ownerPacketId, blockId, symbol.index, catalogTrackName);
|
|
2537
|
+
if (!identity) {
|
|
2538
|
+
console.warn(`[MmtpRouter] manifest identity unresolved; recovered symbol is fail-open for packetId=${ownerPacketId} block=${blockId} esi=${symbol.index}`);
|
|
2539
|
+
this.#routeProcessedFecSymbols(
|
|
2540
|
+
blockId,
|
|
2541
|
+
{ ...result, symbols: [symbol] },
|
|
2542
|
+
ownerPacketId,
|
|
2543
|
+
catalogTrackName,
|
|
2544
|
+
);
|
|
2545
|
+
admitNext(index + 1);
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
const admission = verifier.admitObject(
|
|
2549
|
+
{
|
|
2550
|
+
...identity,
|
|
2551
|
+
packetId: ownerPacketId,
|
|
2552
|
+
ssId: blockId * this.#kForPacket(ownerPacketId) + symbol.index,
|
|
2553
|
+
},
|
|
2554
|
+
symbol.rawSymbol,
|
|
2555
|
+
() => {
|
|
2556
|
+
if (!this.#disposed) {
|
|
2557
|
+
this.#routeProcessedFecSymbols(
|
|
2558
|
+
blockId,
|
|
2559
|
+
{ ...result, symbols: [symbol] },
|
|
2560
|
+
ownerPacketId,
|
|
2561
|
+
catalogTrackName,
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
admitNext(index + 1);
|
|
2565
|
+
},
|
|
2566
|
+
);
|
|
2567
|
+
if (admission.action === "drop") admitNext(index + 1);
|
|
2568
|
+
};
|
|
2569
|
+
admitNext(0);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
#routeProcessedFecSymbols(
|
|
2573
|
+
blockId: number,
|
|
2574
|
+
result: ProcessFecBlockResult,
|
|
2575
|
+
ownerPacketId?: number,
|
|
2576
|
+
catalogTrackName?: string,
|
|
2577
|
+
): void {
|
|
2578
|
+
// Route recovered symbols to the track's callback. The ownerPacketId tells us
|
|
2579
|
+
// which track this block belongs to — resolves per-track, then legacy fallback.
|
|
2580
|
+
if (ownerPacketId == null) return; // no track association
|
|
2581
|
+
if (this.#classifyPacketId(ownerPacketId) === null) return; // unknown track
|
|
2582
|
+
|
|
2583
|
+
// A recovered symbol is a function of EVERY symbol that entered its block,
|
|
2584
|
+
// so its provenance is the block's, not the media path's. Repair symbols
|
|
2585
|
+
// routinely arrive over reliable QUIC (`feedRepairPacket`), so a block that
|
|
2586
|
+
// recovered with MoQ help produced bytes that never crossed a multicast
|
|
2587
|
+
// socket. Without this, recovered frames reached the decoder carrying no
|
|
2588
|
+
// attribution at all and were counted as multicast by default.
|
|
2589
|
+
// A FEC manager that cannot report block provenance yields 'unknown', not
|
|
2590
|
+
// a guess: these bytes are then never counted as physical-delivery
|
|
2591
|
+
// evidence. `FECManagerLike` is a pluggable structural interface, so an
|
|
2592
|
+
// implementation without this method must degrade, not throw.
|
|
2593
|
+
const deliveryProvenance: MmtpDeliveryProvenance =
|
|
2594
|
+
this.#fecManager?.blockDeliveryProvenance?.(
|
|
2595
|
+
blockId,
|
|
2596
|
+
ownerPacketId,
|
|
2597
|
+
catalogTrackName,
|
|
2598
|
+
) ?? 'unknown';
|
|
2599
|
+
|
|
2600
|
+
for (const sym of result.symbols) {
|
|
2601
|
+
this.#routeTrackPacket(sym.rawSymbol, ownerPacketId, sym.packetSequenceNumber, {
|
|
2602
|
+
recovered: true,
|
|
2603
|
+
skipFecPayloadId: true,
|
|
2604
|
+
catalogTrackName,
|
|
2605
|
+
deliveryProvenance,
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
// Feed recovered metadata before routing data. FT=0 initializes the timing
|
|
2610
|
+
// resolver and FT=1 publishes the sample table that recovered FT=2 data
|
|
2611
|
+
// must resolve against. The parser's callbacks dedupe repeated init
|
|
2612
|
+
// delivery, while keeping this path in lockstep with direct source input.
|
|
2613
|
+
for (const sym of result.symbols) {
|
|
2614
|
+
if (sym.fragmentType !== 0 && sym.fragmentType !== 1) continue;
|
|
2615
|
+
this.#feedViaParser(
|
|
2616
|
+
sym.rawSymbol,
|
|
2617
|
+
{ packetId: ownerPacketId, sequenceNumber: sym.packetSequenceNumber },
|
|
2618
|
+
undefined,
|
|
2619
|
+
{
|
|
2620
|
+
skipFecPayloadId: true,
|
|
2621
|
+
recovered: true,
|
|
2622
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
2623
|
+
},
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
// MSE assembler path: route recovered block as pre-assembled frames (skip FT=0)
|
|
2628
|
+
// BLO-3035 layer 3: recovered symbols are RAW MFU fragments, not complete
|
|
2629
|
+
// frames. A keyframe can span multiple packets (FI=1/2/3); pushing each
|
|
2630
|
+
// fragment as a "frame" produces garbage moof+mdat that fails decode.
|
|
2631
|
+
// Mirror the non-MSE path's logic: FI=0 → emit single-packet MFU directly
|
|
2632
|
+
// (after dedup against source path), FI=1/2/3 → feed reassembler, emit when
|
|
2633
|
+
// the complete frame is available. Take the keyframe bit from the assembled
|
|
2634
|
+
// frame's rapFlag (with `#detectKeyframe` as a bitstream fallback for
|
|
2635
|
+
// publishers that don't set rapFlag) — `sym.isKeyframe` is just rapFlag
|
|
2636
|
+
// from a single fragment's header (see `block-processor.ts` where
|
|
2637
|
+
// `isKeyframe = hdr.rapFlag`) and isn't meaningful for the reassembled-
|
|
2638
|
+
// frame keyframe decision when FI≠0.
|
|
2639
|
+
const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
|
|
2640
|
+
if (assembler) {
|
|
2641
|
+
const isVideoTrack = this.#classifyPacketId(ownerPacketId) === true;
|
|
2642
|
+
const frames: Array<{
|
|
2643
|
+
data: Uint8Array;
|
|
2644
|
+
timestamp: number;
|
|
2645
|
+
keyframe: boolean;
|
|
2646
|
+
authoredTiming?: AuthoredTiming;
|
|
2647
|
+
packetSequenceNumber?: number;
|
|
2648
|
+
packetSequenceNumbers?: number[];
|
|
2649
|
+
repairedPacketSequenceNumbers?: number[];
|
|
2650
|
+
mpuSequenceNumber?: number;
|
|
2651
|
+
catalogTrackName?: string;
|
|
2652
|
+
deliveryProvenance?: MmtpDeliveryProvenance;
|
|
2653
|
+
}> = [];
|
|
2654
|
+
let dedupedAgainstSource = 0;
|
|
2655
|
+
let droppedBeforeSync = 0;
|
|
2656
|
+
for (const sym of result.symbols) {
|
|
2657
|
+
if (sym.fragmentType === 0) continue; // init already delivered above
|
|
2658
|
+
const fi = sym.fragment.fragmentationIndicator;
|
|
2659
|
+
if (fi === 0) {
|
|
2660
|
+
// Single-packet MFU. Dedup against source path so frames aren't
|
|
2661
|
+
// emitted twice. The direct source frame may still be buffered by the
|
|
2662
|
+
// parser reorder window, so use source-received sequence numbers.
|
|
2663
|
+
if (this.#sourcePktSeqs.has(
|
|
2664
|
+
this.#packetSeqKey(ownerPacketId, sym.packetSequenceNumber, catalogTrackName),
|
|
2665
|
+
)) {
|
|
2666
|
+
dedupedAgainstSource++;
|
|
2667
|
+
continue;
|
|
2668
|
+
}
|
|
2669
|
+
const keyframe = isVideoTrack
|
|
2670
|
+
? this.#detectKeyframe(
|
|
2671
|
+
ownerPacketId,
|
|
2672
|
+
sym.fragment.data,
|
|
2673
|
+
isMfuKeyframeMetadata(sym.fragment.rapFlag, sym.fragment.priority),
|
|
2674
|
+
)
|
|
2675
|
+
: sym.fragment.rapFlag;
|
|
2676
|
+
if (!this.#shouldEmitFrame(ownerPacketId, keyframe, catalogTrackName)) {
|
|
2677
|
+
droppedBeforeSync++;
|
|
2678
|
+
continue;
|
|
2679
|
+
}
|
|
2680
|
+
const recoveredTiming = this.#resolveRecoveredTiming(
|
|
2681
|
+
ownerPacketId,
|
|
2682
|
+
sym.fragment.mpuSequenceNumber,
|
|
2683
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
2684
|
+
sym.fragment.sampleNumber,
|
|
2685
|
+
sym.fragment.timestamp,
|
|
2686
|
+
catalogTrackName,
|
|
2687
|
+
);
|
|
2688
|
+
if (!recoveredTiming) continue;
|
|
2689
|
+
frames.push({
|
|
2690
|
+
data: sym.fragment.data,
|
|
2691
|
+
timestamp: recoveredTiming.timestamp,
|
|
2692
|
+
keyframe,
|
|
2693
|
+
authoredTiming: recoveredTiming.authoredTiming,
|
|
2694
|
+
packetSequenceNumber: sym.packetSequenceNumber,
|
|
2695
|
+
packetSequenceNumbers: [sym.packetSequenceNumber],
|
|
2696
|
+
repairedPacketSequenceNumbers: [sym.packetSequenceNumber],
|
|
2697
|
+
mpuSequenceNumber: sym.fragment.mpuSequenceNumber,
|
|
2698
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
2699
|
+
// A single-packet MFU needs the block verdict just as much as a
|
|
2700
|
+
// reassembled one. The multi-fragment branch below gets it via the
|
|
2701
|
+
// reassembler fold (`assembled.deliveryProvenance`); this branch
|
|
2702
|
+
// never touches the reassembler, so it must carry the block-level
|
|
2703
|
+
// verdict directly or the sample reaches MSE unattributed.
|
|
2704
|
+
deliveryProvenance,
|
|
2705
|
+
});
|
|
2706
|
+
this.#noteEmittedKeyframe(ownerPacketId, keyframe);
|
|
2707
|
+
} else {
|
|
2708
|
+
// Multi-packet MFU. Feed to reassembler; emit only when complete.
|
|
2709
|
+
// A decoded block contains every source symbol, including fragments
|
|
2710
|
+
// already accepted on the direct path. Re-feeding those fragments as
|
|
2711
|
+
// recovered would replace their original attribution in the shared
|
|
2712
|
+
// reassembler and overstate repairedPacketSequenceNumbers.
|
|
2713
|
+
if (this.#sourcePktSeqs.has(
|
|
2714
|
+
this.#packetSeqKey(ownerPacketId, sym.packetSequenceNumber, catalogTrackName),
|
|
2715
|
+
)) {
|
|
2716
|
+
dedupedAgainstSource++;
|
|
2717
|
+
continue;
|
|
2718
|
+
}
|
|
2719
|
+
let assembled;
|
|
2720
|
+
try {
|
|
2721
|
+
assembled = this.#parser.reassembler.addFragment({
|
|
2722
|
+
fragmentationIndicator: fi,
|
|
2723
|
+
mpuSequenceNumber: sym.fragment.mpuSequenceNumber,
|
|
2724
|
+
fragmentCounter: sym.fragment.fragmentCounter,
|
|
2725
|
+
data: sym.fragment.data,
|
|
2726
|
+
timestamp: sym.fragment.timestamp,
|
|
2727
|
+
rapFlag: sym.fragment.rapFlag,
|
|
2728
|
+
sampleNumber: sym.fragment.sampleNumber,
|
|
2729
|
+
movieFragmentSequenceNumber: sym.fragment.movieFragmentSequenceNumber,
|
|
2730
|
+
sampleOffset: sym.fragment.sampleOffset,
|
|
2731
|
+
priority: sym.fragment.priority,
|
|
2732
|
+
packetSequenceNumber: sym.packetSequenceNumber,
|
|
2733
|
+
recovered: true,
|
|
2734
|
+
// Without this the fold sees a fragment with neither `source`
|
|
2735
|
+
// nor a verdict, returns 'unknown', and the block-level finding
|
|
2736
|
+
// computed above is lost before the frame reaches any consumer.
|
|
2737
|
+
deliveryProvenance,
|
|
2738
|
+
packetId: ownerPacketId,
|
|
2739
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
2740
|
+
});
|
|
2741
|
+
} catch (error) {
|
|
2742
|
+
if (!(error instanceof MfuReassemblyError)) throw error;
|
|
2743
|
+
console.warn(
|
|
2744
|
+
`[MmtpRouter] dropped rejected recovered MFU fragment: block=${blockId} ` +
|
|
2745
|
+
`packetId=${ownerPacketId} code=${error.code}`,
|
|
2746
|
+
);
|
|
2747
|
+
continue;
|
|
2748
|
+
}
|
|
2749
|
+
if (assembled) {
|
|
2750
|
+
const keyframe = isVideoTrack
|
|
2751
|
+
? this.#detectKeyframe(
|
|
2752
|
+
ownerPacketId,
|
|
2753
|
+
assembled.data,
|
|
2754
|
+
isMfuKeyframeMetadata(assembled.rapFlag, assembled.priority),
|
|
2755
|
+
)
|
|
2756
|
+
: assembled.rapFlag;
|
|
2757
|
+
if (!this.#shouldEmitFrame(
|
|
2758
|
+
ownerPacketId,
|
|
2759
|
+
keyframe,
|
|
2760
|
+
assembled.catalogTrackName,
|
|
2761
|
+
)) {
|
|
2762
|
+
droppedBeforeSync++;
|
|
2763
|
+
continue;
|
|
2764
|
+
}
|
|
2765
|
+
const recoveredCatalogTrackName = assembled.catalogTrackName ?? catalogTrackName;
|
|
2766
|
+
const recoveredTiming = this.#resolveRecoveredTiming(
|
|
2767
|
+
ownerPacketId,
|
|
2768
|
+
assembled.mpuSequenceNumber,
|
|
2769
|
+
assembled.movieFragmentSequenceNumber,
|
|
2770
|
+
assembled.sampleNumber,
|
|
2771
|
+
assembled.timestamp,
|
|
2772
|
+
recoveredCatalogTrackName,
|
|
2773
|
+
);
|
|
2774
|
+
if (!recoveredTiming) continue;
|
|
2775
|
+
frames.push({
|
|
2776
|
+
data: assembled.data,
|
|
2777
|
+
timestamp: recoveredTiming.timestamp,
|
|
2778
|
+
keyframe,
|
|
2779
|
+
authoredTiming: recoveredTiming.authoredTiming,
|
|
2780
|
+
// The singular delivery identity is the fragment that completed
|
|
2781
|
+
// and delivered the MFU, matching the callback/parser path.
|
|
2782
|
+
packetSequenceNumber: sym.packetSequenceNumber,
|
|
2783
|
+
...(assembled.packetSequenceNumbers === undefined
|
|
2784
|
+
? {}
|
|
2785
|
+
: { packetSequenceNumbers: assembled.packetSequenceNumbers }),
|
|
2786
|
+
...(assembled.repairedPacketSequenceNumbers === undefined
|
|
2787
|
+
? {}
|
|
2788
|
+
: {
|
|
2789
|
+
repairedPacketSequenceNumbers:
|
|
2790
|
+
assembled.repairedPacketSequenceNumbers,
|
|
2791
|
+
}),
|
|
2792
|
+
mpuSequenceNumber: assembled.mpuSequenceNumber,
|
|
2793
|
+
...(assembled.catalogTrackName === undefined
|
|
2794
|
+
? {}
|
|
2795
|
+
: { catalogTrackName: assembled.catalogTrackName }),
|
|
2796
|
+
deliveryProvenance: assembled.deliveryProvenance,
|
|
2797
|
+
});
|
|
2798
|
+
this.#noteEmittedKeyframe(ownerPacketId, keyframe);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
if (frames.length > 0) {
|
|
2803
|
+
assembler.addRecoveredBlock(blockId, frames);
|
|
2804
|
+
debugRouter(
|
|
2805
|
+
`[MmtpRouter] Block ${blockId}: ${result.symbols.length} symbols → ${frames.length} reassembled frames → MSE assembler (packetId=${ownerPacketId}, dedupedAgainstSource=${dedupedAgainstSource}, droppedBeforeSync=${droppedBeforeSync})`,
|
|
2806
|
+
);
|
|
2807
|
+
} else if (result.symbols.length > 0) {
|
|
2808
|
+
// Recovery yielded symbols but no complete frames emerged — either all
|
|
2809
|
+
// FI=0 were source-path duplicates, or all FI=1/2/3 are still mid-
|
|
2810
|
+
// reassembly. Surface this so an operator debugging "FEC recovered N
|
|
2811
|
+
// symbols but no new frames rendered" has a signal.
|
|
2812
|
+
debugRouter(
|
|
2813
|
+
`[MmtpRouter] Block ${blockId}: ${result.symbols.length} symbols recovered → 0 emitted (dedupedAgainstSource=${dedupedAgainstSource}, droppedBeforeSync=${droppedBeforeSync}, pending-reassembly=${result.symbols.length - dedupedAgainstSource - droppedBeforeSync}, packetId=${ownerPacketId})`,
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
const cb = this.#getFrameCallback(ownerPacketId);
|
|
2820
|
+
const isVideo = this.#classifyPacketId(ownerPacketId) === true;
|
|
2821
|
+
let droppedBeforeSync = 0;
|
|
2822
|
+
let droppedStale = 0;
|
|
2823
|
+
let dedupedAgainstSource = 0;
|
|
2824
|
+
|
|
2825
|
+
for (const sym of result.symbols) {
|
|
2826
|
+
// FT=0 init already delivered above — skip data routing
|
|
2827
|
+
if (sym.fragmentType === 0) continue;
|
|
2828
|
+
|
|
2829
|
+
if (isVideo) {
|
|
2830
|
+
const fi = sym.fragment.fragmentationIndicator;
|
|
2831
|
+
if (fi === 0) {
|
|
2832
|
+
// Dedup: skip FI=0 frames already received via direct source path.
|
|
2833
|
+
// The reassembler deduplicates FI=1/2/3 fragments via #emitted, but
|
|
2834
|
+
// FI=0 (single-packet MFU) bypasses it. Without this check, recovery
|
|
2835
|
+
// can re-emit a source frame that is only held in the reorder buffer,
|
|
2836
|
+
// producing duplicate timestamps when the source path later flushes.
|
|
2837
|
+
if (this.#sourcePktSeqs.has(
|
|
2838
|
+
this.#packetSeqKey(ownerPacketId, sym.packetSequenceNumber, catalogTrackName),
|
|
2839
|
+
)) {
|
|
2840
|
+
dedupedAgainstSource++;
|
|
2841
|
+
continue;
|
|
2842
|
+
}
|
|
2843
|
+
if (this.#isStaleRecoveredVideo(
|
|
2844
|
+
ownerPacketId,
|
|
2845
|
+
sym.packetSequenceNumber,
|
|
2846
|
+
catalogTrackName,
|
|
2847
|
+
)) {
|
|
2848
|
+
droppedStale++;
|
|
2849
|
+
continue;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
const d = sym.fragment.data;
|
|
2853
|
+
const keyframe = this.#detectKeyframe(
|
|
2854
|
+
ownerPacketId,
|
|
2855
|
+
d,
|
|
2856
|
+
isMfuKeyframeMetadata(sym.fragment.rapFlag, sym.fragment.priority),
|
|
2857
|
+
);
|
|
2858
|
+
const recoveredTiming = this.#resolveRecoveredTiming(
|
|
2859
|
+
ownerPacketId,
|
|
2860
|
+
sym.fragment.mpuSequenceNumber,
|
|
2861
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
2862
|
+
sym.fragment.sampleNumber,
|
|
2863
|
+
sym.fragment.timestamp,
|
|
2864
|
+
catalogTrackName,
|
|
2865
|
+
);
|
|
2866
|
+
if (!recoveredTiming) continue;
|
|
2867
|
+
this.#parser.routeFrame(d, {
|
|
2868
|
+
timestamp: recoveredTiming.timestamp,
|
|
2869
|
+
authoredTiming: recoveredTiming.authoredTiming,
|
|
2870
|
+
keyframe,
|
|
2871
|
+
packetId: ownerPacketId,
|
|
2872
|
+
trackType: "video",
|
|
2873
|
+
packetSequenceNumber: sym.packetSequenceNumber,
|
|
2874
|
+
packetSequenceNumbers: [sym.packetSequenceNumber],
|
|
2875
|
+
repairedPacketSequenceNumbers: [sym.packetSequenceNumber],
|
|
2876
|
+
mpuSequenceNumber: sym.fragment.mpuSequenceNumber,
|
|
2877
|
+
recovered: true,
|
|
2878
|
+
deliveryProvenance,
|
|
2879
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
2880
|
+
});
|
|
2881
|
+
} else {
|
|
2882
|
+
// Fragment of a multi-packet MFU — must go through reassembler
|
|
2883
|
+
if (this.#sourcePktSeqs.has(
|
|
2884
|
+
this.#packetSeqKey(ownerPacketId, sym.packetSequenceNumber, catalogTrackName),
|
|
2885
|
+
)) {
|
|
2886
|
+
dedupedAgainstSource++;
|
|
2887
|
+
continue;
|
|
2888
|
+
}
|
|
2889
|
+
let assembled;
|
|
2890
|
+
try {
|
|
2891
|
+
assembled = this.#parser.reassembler.addFragment({
|
|
2892
|
+
fragmentationIndicator: fi,
|
|
2893
|
+
mpuSequenceNumber: sym.fragment.mpuSequenceNumber,
|
|
2894
|
+
fragmentCounter: sym.fragment.fragmentCounter,
|
|
2895
|
+
data: sym.fragment.data,
|
|
2896
|
+
timestamp: sym.fragment.timestamp,
|
|
2897
|
+
rapFlag: sym.fragment.rapFlag,
|
|
2898
|
+
sampleNumber: sym.fragment.sampleNumber,
|
|
2899
|
+
movieFragmentSequenceNumber: sym.fragment.movieFragmentSequenceNumber,
|
|
2900
|
+
sampleOffset: sym.fragment.sampleOffset,
|
|
2901
|
+
priority: sym.fragment.priority,
|
|
2902
|
+
packetSequenceNumber: sym.packetSequenceNumber,
|
|
2903
|
+
recovered: true,
|
|
2904
|
+
// Without this the fold sees a fragment with neither `source`
|
|
2905
|
+
// nor a verdict, returns 'unknown', and the block-level finding
|
|
2906
|
+
// computed above is lost before the frame reaches any consumer.
|
|
2907
|
+
deliveryProvenance,
|
|
2908
|
+
packetId: ownerPacketId,
|
|
2909
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
2910
|
+
});
|
|
2911
|
+
} catch (error) {
|
|
2912
|
+
if (!(error instanceof MfuReassemblyError)) throw error;
|
|
2913
|
+
console.warn(
|
|
2914
|
+
`[MmtpRouter] dropped rejected recovered MFU fragment: block=${blockId} ` +
|
|
2915
|
+
`packetId=${ownerPacketId} code=${error.code}`,
|
|
2916
|
+
);
|
|
2917
|
+
continue;
|
|
2918
|
+
}
|
|
2919
|
+
if (assembled) {
|
|
2920
|
+
if (this.#isStaleRecoveredVideo(
|
|
2921
|
+
ownerPacketId,
|
|
2922
|
+
sym.packetSequenceNumber,
|
|
2923
|
+
assembled.catalogTrackName,
|
|
2924
|
+
)) {
|
|
2925
|
+
droppedStale++;
|
|
2926
|
+
continue;
|
|
2927
|
+
}
|
|
2928
|
+
const keyframe = this.#detectKeyframe(
|
|
2929
|
+
ownerPacketId,
|
|
2930
|
+
assembled.data,
|
|
2931
|
+
isMfuKeyframeMetadata(assembled.rapFlag, assembled.priority),
|
|
2932
|
+
);
|
|
2933
|
+
const recoveredCatalogTrackName = assembled.catalogTrackName ?? catalogTrackName;
|
|
2934
|
+
const recoveredTiming = this.#resolveRecoveredTiming(
|
|
2935
|
+
ownerPacketId,
|
|
2936
|
+
assembled.mpuSequenceNumber,
|
|
2937
|
+
assembled.movieFragmentSequenceNumber,
|
|
2938
|
+
assembled.sampleNumber,
|
|
2939
|
+
assembled.timestamp,
|
|
2940
|
+
recoveredCatalogTrackName,
|
|
2941
|
+
);
|
|
2942
|
+
if (!recoveredTiming) continue;
|
|
2943
|
+
this.#parser.routeFrame(assembled.data, {
|
|
2944
|
+
timestamp: recoveredTiming.timestamp,
|
|
2945
|
+
authoredTiming: recoveredTiming.authoredTiming,
|
|
2946
|
+
keyframe,
|
|
2947
|
+
packetId: ownerPacketId,
|
|
2948
|
+
trackType: "video",
|
|
2949
|
+
packetSequenceNumber: sym.packetSequenceNumber,
|
|
2950
|
+
...(assembled.packetSequenceNumbers === undefined
|
|
2951
|
+
? {}
|
|
2952
|
+
: { packetSequenceNumbers: assembled.packetSequenceNumbers }),
|
|
2953
|
+
...(assembled.repairedPacketSequenceNumbers === undefined
|
|
2954
|
+
? {}
|
|
2955
|
+
: {
|
|
2956
|
+
repairedPacketSequenceNumbers:
|
|
2957
|
+
assembled.repairedPacketSequenceNumbers,
|
|
2958
|
+
}),
|
|
2959
|
+
mpuSequenceNumber: assembled.mpuSequenceNumber,
|
|
2960
|
+
recovered: true,
|
|
2961
|
+
deliveryProvenance,
|
|
2962
|
+
...(assembled.catalogTrackName === undefined
|
|
2963
|
+
? {}
|
|
2964
|
+
: { catalogTrackName: assembled.catalogTrackName }),
|
|
2965
|
+
});
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
} else {
|
|
2969
|
+
// Audio recovery: dedup single-packet MFUs already emitted via direct source path
|
|
2970
|
+
// (parity with video FI=0 path at line 993). Prevents re-delivering audio frames
|
|
2971
|
+
// with duplicate timestamps — doubled samples in the FIFO ring buffer corrupt the
|
|
2972
|
+
// 1kHz test tone (pitch/energy instability in FFT spectrum).
|
|
2973
|
+
if (this.#sourcePktSeqs.has(
|
|
2974
|
+
this.#packetSeqKey(ownerPacketId, sym.packetSequenceNumber, catalogTrackName),
|
|
2975
|
+
)) {
|
|
2976
|
+
dedupedAgainstSource++;
|
|
2977
|
+
continue;
|
|
2978
|
+
}
|
|
2979
|
+
const recoveredTiming = this.#resolveRecoveredTiming(
|
|
2980
|
+
ownerPacketId,
|
|
2981
|
+
sym.fragment.mpuSequenceNumber,
|
|
2982
|
+
sym.fragment.movieFragmentSequenceNumber,
|
|
2983
|
+
sym.fragment.sampleNumber,
|
|
2984
|
+
sym.fragment.timestamp,
|
|
2985
|
+
catalogTrackName,
|
|
2986
|
+
);
|
|
2987
|
+
if (recoveredTiming === null) continue;
|
|
2988
|
+
const meta = {
|
|
2989
|
+
timestamp: recoveredTiming.timestamp,
|
|
2990
|
+
authoredTiming: recoveredTiming.authoredTiming,
|
|
2991
|
+
keyframe: true,
|
|
2992
|
+
packetId: ownerPacketId,
|
|
2993
|
+
packetSequenceNumber: sym.packetSequenceNumber,
|
|
2994
|
+
packetSequenceNumbers: [sym.packetSequenceNumber],
|
|
2995
|
+
repairedPacketSequenceNumbers: [sym.packetSequenceNumber],
|
|
2996
|
+
mpuSequenceNumber: sym.fragment.mpuSequenceNumber,
|
|
2997
|
+
recovered: true,
|
|
2998
|
+
...(catalogTrackName === undefined ? {} : { catalogTrackName }),
|
|
2999
|
+
};
|
|
3000
|
+
this.#routeDirectRecoveredAudio(
|
|
3001
|
+
ownerPacketId,
|
|
3002
|
+
sym.fragment.data,
|
|
3003
|
+
meta,
|
|
3004
|
+
cb,
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
if (result.symbols.length > 0) {
|
|
3009
|
+
debugRouter(
|
|
3010
|
+
`[MmtpRouter] Block ${blockId}: ${result.symbols.length} symbols recovered (packetId=${ownerPacketId}, dedupedAgainstSource=${dedupedAgainstSource}, droppedBeforeSync=${droppedBeforeSync}, droppedStale=${droppedStale})`,
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
// ── Private: keyframe detection ─────────────────────────────────
|
|
3016
|
+
|
|
3017
|
+
/** Codec-aware keyframe detection from bitstream data.
|
|
3018
|
+
* H.264 IDR (NAL type 5/7), AV1 OBU_SEQUENCE_HEADER (type 1), VP9 frame_type bit. */
|
|
3019
|
+
#detectKeyframe(
|
|
3020
|
+
packetId: number,
|
|
3021
|
+
data: Uint8Array,
|
|
3022
|
+
rapFlag = false,
|
|
3023
|
+
): boolean {
|
|
3024
|
+
const codec = this.#trackCodecs.get(packetId) ?? "";
|
|
3025
|
+
return MmtpContainerParser.isKeyframe(data, rapFlag, codec);
|
|
3026
|
+
}
|
|
3027
|
+
}
|