@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,2842 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mmt/container - MFU Fragment Reassembler
|
|
3
|
+
*
|
|
4
|
+
* Handles authored fragmented MFUs (Media Fragment Units) according to
|
|
5
|
+
* ISO/IEC 23008-1:2023 Section 9.2.3.3. Fragmented runs are admitted in the
|
|
6
|
+
* exact FI/fragment_counter countdown carried on the wire. Transport-run
|
|
7
|
+
* association additionally requires the repository's contiguous-source
|
|
8
|
+
* producer profile; ISO-permitted interleaving remains incomplete and fails
|
|
9
|
+
* closed.
|
|
10
|
+
*
|
|
11
|
+
* Fragmentation Indicator (FI) values:
|
|
12
|
+
* - 0: Complete MFU (single packet, no fragmentation)
|
|
13
|
+
* - 1: First fragment
|
|
14
|
+
* - 2: Middle fragment
|
|
15
|
+
* - 3: Last fragment
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
extractMFUPayload,
|
|
22
|
+
isFecRepairPacket,
|
|
23
|
+
} from '@blockcast/mmt-base'
|
|
24
|
+
|
|
25
|
+
/** Source transport admitted by the MMTP router. */
|
|
26
|
+
export type MmtpSourcePath = 'multicast' | 'moq'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Strict whole-frame delivery provenance, folded over EVERY contributing
|
|
30
|
+
* fragment rather than the head fragment alone.
|
|
31
|
+
*
|
|
32
|
+
* This is deliberately NOT `source`. `source` answers a routing question —
|
|
33
|
+
* "is this the reliable-MoQ copy that must not confirm multicast takeover?" —
|
|
34
|
+
* and head-fragment-wins is the documented, deterministic answer to it.
|
|
35
|
+
* Provenance answers an evidence question: "did every byte of this frame
|
|
36
|
+
* physically arrive over multicast?" A frame with a multicast head and a
|
|
37
|
+
* QUIC-delivered tail is a legitimate routing win for `source: 'multicast'`
|
|
38
|
+
* and is emphatically NOT multicast-exclusive delivery.
|
|
39
|
+
*
|
|
40
|
+
* - 'multicast' — every contributing fragment was explicitly multicast.
|
|
41
|
+
* The ONLY value that may be counted as physical-delivery evidence.
|
|
42
|
+
* - 'mixed' — at least one contributing fragment arrived over MoQ.
|
|
43
|
+
* - 'unknown' — no fragment arrived over MoQ, but at least one carried no
|
|
44
|
+
* attribution at all. Absence is preserved as its own state rather than
|
|
45
|
+
* defaulted, so a consumer cannot mistake "never recorded" for "proven".
|
|
46
|
+
*/
|
|
47
|
+
export type MmtpDeliveryProvenance = 'multicast' | 'mixed' | 'unknown'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Fold one fragment's `source` into a running provenance verdict.
|
|
51
|
+
*
|
|
52
|
+
* Fail-closed, order-independent and associative: 'mixed' absorbs everything,
|
|
53
|
+
* a missing attribution degrades 'multicast' to 'unknown', and nothing ever
|
|
54
|
+
* upgrades a verdict. Fold over an empty set is 'unknown' — no fragment ever
|
|
55
|
+
* proved anything.
|
|
56
|
+
*/
|
|
57
|
+
export function foldDeliveryProvenance(
|
|
58
|
+
verdict: MmtpDeliveryProvenance,
|
|
59
|
+
source: MmtpSourcePath | undefined,
|
|
60
|
+
): MmtpDeliveryProvenance {
|
|
61
|
+
if (verdict === 'mixed') return 'mixed'
|
|
62
|
+
if (source === 'moq') return 'mixed'
|
|
63
|
+
if (source === undefined) return 'unknown'
|
|
64
|
+
return verdict
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* One fragment's own provenance.
|
|
69
|
+
*
|
|
70
|
+
* An explicit `deliveryProvenance` wins: FEC-recovered fragments carry no
|
|
71
|
+
* `source` (they were never delivered as themselves) but their block's symbol
|
|
72
|
+
* provenance IS known, and the router supplies it. Falling back to `source`
|
|
73
|
+
* keeps directly-delivered fragments working unchanged.
|
|
74
|
+
*/
|
|
75
|
+
function fragmentProvenance(fragment: {
|
|
76
|
+
source?: MmtpSourcePath
|
|
77
|
+
deliveryProvenance?: MmtpDeliveryProvenance
|
|
78
|
+
}): MmtpDeliveryProvenance {
|
|
79
|
+
if (fragment.deliveryProvenance !== undefined) return fragment.deliveryProvenance
|
|
80
|
+
if (fragment.source === 'moq') return 'mixed'
|
|
81
|
+
if (fragment.source === 'multicast') return 'multicast'
|
|
82
|
+
return 'unknown'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Strict provenance for a complete fragment set. */
|
|
86
|
+
export function deliveryProvenanceOf(
|
|
87
|
+
fragments: readonly {
|
|
88
|
+
source?: MmtpSourcePath
|
|
89
|
+
deliveryProvenance?: MmtpDeliveryProvenance
|
|
90
|
+
}[],
|
|
91
|
+
): MmtpDeliveryProvenance {
|
|
92
|
+
if (fragments.length === 0) return 'unknown'
|
|
93
|
+
let verdict: MmtpDeliveryProvenance = 'multicast'
|
|
94
|
+
for (const fragment of fragments) {
|
|
95
|
+
const own = fragmentProvenance(fragment)
|
|
96
|
+
verdict =
|
|
97
|
+
verdict === 'mixed' || own === 'mixed'
|
|
98
|
+
? 'mixed'
|
|
99
|
+
: verdict === 'unknown' || own === 'unknown'
|
|
100
|
+
? 'unknown'
|
|
101
|
+
: 'multicast'
|
|
102
|
+
}
|
|
103
|
+
return verdict
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* MFU Fragment for reassembly
|
|
108
|
+
*/
|
|
109
|
+
export interface MFUFragment {
|
|
110
|
+
fragmentationIndicator: number
|
|
111
|
+
mpuSequenceNumber: number
|
|
112
|
+
fragmentCounter: number
|
|
113
|
+
data: Uint8Array
|
|
114
|
+
timestamp: bigint
|
|
115
|
+
/** Random Access Point flag from MMTP header - indicates keyframe */
|
|
116
|
+
rapFlag: boolean
|
|
117
|
+
/** MFU DU priority. FFmpeg marks random-access samples with high priority. */
|
|
118
|
+
priority?: number
|
|
119
|
+
sampleNumber?: number
|
|
120
|
+
movieFragmentSequenceNumber?: number
|
|
121
|
+
sampleOffset?: number
|
|
122
|
+
/** Non-timed MFU Item_ID from the 4-byte DU header. */
|
|
123
|
+
itemId?: number
|
|
124
|
+
/** Authored u32 MMTP packet_sequence_number, including on recovered packets. */
|
|
125
|
+
packetSequenceNumber?: number
|
|
126
|
+
/** True when this accepted source packet was reconstructed by FEC. */
|
|
127
|
+
recovered?: boolean
|
|
128
|
+
/** Transport that supplied this accepted fragment. Absent for recovered data. */
|
|
129
|
+
source?: MmtpSourcePath
|
|
130
|
+
/**
|
|
131
|
+
* Explicit provenance for this fragment, overriding what `source` implies.
|
|
132
|
+
* Set by the router for FEC-recovered fragments, which have no `source` of
|
|
133
|
+
* their own but whose FEC block's symbol provenance is known.
|
|
134
|
+
*/
|
|
135
|
+
deliveryProvenance?: MmtpDeliveryProvenance
|
|
136
|
+
/** Immutable catalog identity supplied by the owning ingress subscription. */
|
|
137
|
+
catalogTrackName?: string
|
|
138
|
+
/** Physical MMTP stream identity when no catalog identity is available. */
|
|
139
|
+
packetId?: number
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Reassembled MFU result
|
|
144
|
+
*/
|
|
145
|
+
export interface ReassembledMFU {
|
|
146
|
+
mpuSequenceNumber: number
|
|
147
|
+
/** MMTP packet_sequence_number of the first fragment in delivery order. */
|
|
148
|
+
packetSequenceNumber?: number
|
|
149
|
+
/** Every authoritative MMTP source-packet sequence that formed this MFU. */
|
|
150
|
+
packetSequenceNumbers?: number[]
|
|
151
|
+
/** Subset of packetSequenceNumbers reconstructed by FEC. */
|
|
152
|
+
repairedPacketSequenceNumbers?: number[]
|
|
153
|
+
data: Uint8Array
|
|
154
|
+
timestamp: bigint
|
|
155
|
+
fragmentCount: number
|
|
156
|
+
/** Random Access Point flag - indicates keyframe (from first fragment) */
|
|
157
|
+
rapFlag: boolean
|
|
158
|
+
/** MFU DU priority from the first fragment that carried it. */
|
|
159
|
+
priority?: number
|
|
160
|
+
sampleNumber?: number
|
|
161
|
+
movieFragmentSequenceNumber?: number
|
|
162
|
+
/**
|
|
163
|
+
* This DU's authored base byte offset within its sample (moqcast #37). 0
|
|
164
|
+
* for a whole-sample DU; nonzero identifies one subsample DU among
|
|
165
|
+
* siblings partitioning an oversized sample. Constant across the DU's own
|
|
166
|
+
* fragments — feed into a sample combiner to reassemble the full sample.
|
|
167
|
+
*/
|
|
168
|
+
sampleOffset?: number
|
|
169
|
+
/**
|
|
170
|
+
* Transport that supplied the accepted FI=1 head fragment (or the complete
|
|
171
|
+
* FI=0 frame). This is the deterministic winner for mixed-path assembly.
|
|
172
|
+
*
|
|
173
|
+
* ROUTING ONLY. It describes one fragment, so it is not evidence about the
|
|
174
|
+
* whole frame — read {@link ReassembledMFU.deliveryProvenance} for that.
|
|
175
|
+
*/
|
|
176
|
+
source?: MmtpSourcePath
|
|
177
|
+
/**
|
|
178
|
+
* Strict provenance folded over every contributing fragment. Only
|
|
179
|
+
* 'multicast' proves multicast-exclusive delivery of this frame.
|
|
180
|
+
*/
|
|
181
|
+
deliveryProvenance: MmtpDeliveryProvenance
|
|
182
|
+
/** Catalog identity from the accepted FI=1 head fragment, or FI=0 frame. */
|
|
183
|
+
catalogTrackName?: string
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Reassembler statistics
|
|
188
|
+
*/
|
|
189
|
+
export interface ReassemblerStats {
|
|
190
|
+
totalFragments: number
|
|
191
|
+
reassembledMFUs: number
|
|
192
|
+
/** Fragments rejected by strict authored DU-state validation. */
|
|
193
|
+
rejectedFragments: number
|
|
194
|
+
/** Incomplete MFUs discarded after their configured deadline. */
|
|
195
|
+
timedOutMFUs: number
|
|
196
|
+
/** Incomplete MFUs discarded to enforce the configured memory bound. */
|
|
197
|
+
evictedMemoryLimit: number
|
|
198
|
+
outOfOrderFragments: number
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const MFU_REASSEMBLY_ERROR_CODES = [
|
|
202
|
+
'invalid-fragment-state',
|
|
203
|
+
'missing-data-unit-identity',
|
|
204
|
+
'missing-packet-sequence',
|
|
205
|
+
'conflicting-data-unit-identity',
|
|
206
|
+
'overlapping-data-unit',
|
|
207
|
+
'fragment-counter-mismatch',
|
|
208
|
+
'data-unit-identity-mismatch',
|
|
209
|
+
'offset-overflow',
|
|
210
|
+
'active-data-unit-limit',
|
|
211
|
+
'incomplete-terminal-fragment',
|
|
212
|
+
] as const
|
|
213
|
+
|
|
214
|
+
export type MfuReassemblyErrorCode = typeof MFU_REASSEMBLY_ERROR_CODES[number]
|
|
215
|
+
|
|
216
|
+
const mfuReassemblyErrorCodes = new Set<string>(MFU_REASSEMBLY_ERROR_CODES)
|
|
217
|
+
|
|
218
|
+
export function isMfuReassemblyErrorCode(value: unknown): value is MfuReassemblyErrorCode {
|
|
219
|
+
return typeof value === 'string' && mfuReassemblyErrorCodes.has(value)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export class MfuReassemblyError extends Error {
|
|
223
|
+
readonly name = 'MfuReassemblyError'
|
|
224
|
+
|
|
225
|
+
constructor(readonly code: MfuReassemblyErrorCode, message: string) {
|
|
226
|
+
super(message)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function assembledPacketSequences(fragments: readonly MFUFragment[]): {
|
|
231
|
+
packetSequenceNumbers?: number[]
|
|
232
|
+
repairedPacketSequenceNumbers?: number[]
|
|
233
|
+
} {
|
|
234
|
+
const packetSequenceNumbers: number[] = []
|
|
235
|
+
const repairedPacketSequenceNumbers: number[] = []
|
|
236
|
+
const seen = new Set<number>()
|
|
237
|
+
const repaired = new Set<number>()
|
|
238
|
+
for (const fragment of fragments) {
|
|
239
|
+
const sequence = fragment.packetSequenceNumber
|
|
240
|
+
if (typeof sequence !== 'number' || !Number.isSafeInteger(sequence) ||
|
|
241
|
+
sequence < 0 || sequence > 0xffffffff) {
|
|
242
|
+
return {}
|
|
243
|
+
}
|
|
244
|
+
if (!seen.has(sequence)) {
|
|
245
|
+
seen.add(sequence)
|
|
246
|
+
packetSequenceNumbers.push(sequence)
|
|
247
|
+
}
|
|
248
|
+
if (fragment.recovered === true && !repaired.has(sequence)) {
|
|
249
|
+
repaired.add(sequence)
|
|
250
|
+
repairedPacketSequenceNumbers.push(sequence)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return {packetSequenceNumbers, repairedPacketSequenceNumbers}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface PendingMfuDebug {
|
|
257
|
+
key: string
|
|
258
|
+
fragments: number
|
|
259
|
+
fi0: number
|
|
260
|
+
fi1: number
|
|
261
|
+
fi2: number
|
|
262
|
+
fi3: number
|
|
263
|
+
firstFi: number
|
|
264
|
+
lastFi: number
|
|
265
|
+
firstOrder: number
|
|
266
|
+
lastOrder: number
|
|
267
|
+
minCounter: number
|
|
268
|
+
maxCounter: number
|
|
269
|
+
minOffset?: number
|
|
270
|
+
maxOffset?: number
|
|
271
|
+
movieFragmentSequenceNumber?: number
|
|
272
|
+
sampleNumber?: number
|
|
273
|
+
mpuSequenceNumber?: number
|
|
274
|
+
sortedHead: Array<{ fi: number; counter: number; order: number; seq?: number; offset?: number }>
|
|
275
|
+
sortedTail: Array<{ fi: number; counter: number; order: number; seq?: number; offset?: number }>
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface MFUReassemblerOptions {
|
|
279
|
+
/** Timeout (ms) for incomplete MFUs. Derive from frame duration or FEC interleaveDepthMs.
|
|
280
|
+
* Pass null for live clock-free operation; memory remains bounded by maxBuffered. */
|
|
281
|
+
timeoutMs: number | null
|
|
282
|
+
/** Max concurrent incomplete MFUs, including continuations awaiting FI=1. */
|
|
283
|
+
maxBuffered?: number
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Reassembly surface consumed by MmtpContainerParser and MmtpRouter.
|
|
287
|
+
* Implemented by both the TypeScript engine and the shared
|
|
288
|
+
* Rust/WASM IFC adapter. */
|
|
289
|
+
export interface MfuReassemblerBackend {
|
|
290
|
+
timeoutMs: number | null
|
|
291
|
+
readonly stats: ReassemblerStats
|
|
292
|
+
onDeferredAssembly?: (result: ReassembledMFU) => void
|
|
293
|
+
/** Returns null only for a valid incomplete DU; strict rejection throws
|
|
294
|
+
* `MfuReassemblyError`. */
|
|
295
|
+
addFragment(fragment: MFUFragment): ReassembledMFU | null
|
|
296
|
+
flush(): void
|
|
297
|
+
resetStream(catalogTrackName: string): void
|
|
298
|
+
reset(): void
|
|
299
|
+
readonly pendingCount: number
|
|
300
|
+
getStatus(): {
|
|
301
|
+
bufferedMFUs: number
|
|
302
|
+
pendingFragments: number
|
|
303
|
+
pendingSamples: PendingMfuDebug[]
|
|
304
|
+
stats: ReassemblerStats
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function streamScope(fragment: MFUFragment): readonly [kind: string, identity: string | number | null] {
|
|
309
|
+
if (fragment.catalogTrackName !== undefined) return ['catalog', fragment.catalogTrackName]
|
|
310
|
+
if (fragment.packetId !== undefined) return ['packet', fragment.packetId]
|
|
311
|
+
return ['unspecified', null]
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function isU32(value: unknown): value is number {
|
|
315
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 0xffffffff
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Producer-profile transport coordinate shared by every contiguous source
|
|
320
|
+
* fragment in one data-unit run. This is association state only; objectKey
|
|
321
|
+
* remains the intrinsic DU identity. ISO-permitted interleaving creates a
|
|
322
|
+
* different key and therefore cannot cross-associate samples.
|
|
323
|
+
*/
|
|
324
|
+
function fragmentRunKey(fragment: MFUFragment): string {
|
|
325
|
+
if (!isU32(fragment.packetSequenceNumber)) {
|
|
326
|
+
throw new Error('[MFUReassembler] fragmented value has no validated packet sequence')
|
|
327
|
+
}
|
|
328
|
+
const terminalPacketSequence =
|
|
329
|
+
(fragment.packetSequenceNumber + fragment.fragmentCounter) % 0x1_0000_0000
|
|
330
|
+
return JSON.stringify([
|
|
331
|
+
...streamScope(fragment),
|
|
332
|
+
fragment.mpuSequenceNumber,
|
|
333
|
+
terminalPacketSequence,
|
|
334
|
+
])
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function hasAnySampleIdentityField(fragment: MFUFragment): boolean {
|
|
338
|
+
return fragment.movieFragmentSequenceNumber !== undefined ||
|
|
339
|
+
fragment.sampleNumber !== undefined || fragment.sampleOffset !== undefined
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
type IdentifiedSampleFragment = MFUFragment & Required<Pick<
|
|
343
|
+
MFUFragment,
|
|
344
|
+
'movieFragmentSequenceNumber' | 'sampleNumber' | 'sampleOffset'
|
|
345
|
+
>>
|
|
346
|
+
|
|
347
|
+
function hasSampleIdentity(fragment: MFUFragment): fragment is IdentifiedSampleFragment {
|
|
348
|
+
return isU32(fragment.movieFragmentSequenceNumber) && fragment.movieFragmentSequenceNumber > 0 &&
|
|
349
|
+
isU32(fragment.sampleNumber) && fragment.sampleNumber > 0 && isU32(fragment.sampleOffset)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function hasValidFragmentHeader(fragment: MFUFragment): boolean {
|
|
353
|
+
const { fragmentationIndicator: fi, fragmentCounter: counter } = fragment
|
|
354
|
+
if (!Number.isInteger(fi) || fi < 0 || fi > 3) return false
|
|
355
|
+
if (!Number.isInteger(counter) || counter < 0 || counter > 0xff) return false
|
|
356
|
+
return fi === 0 || fi === 3 ? counter === 0 : counter > 0
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function fragmentCounterOrder(fragment: MFUFragment): number {
|
|
360
|
+
return 0xff - fragment.fragmentCounter
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function objectKey(fragment: MFUFragment): string {
|
|
364
|
+
const scope = streamScope(fragment)
|
|
365
|
+
if (fragment.itemId !== undefined) {
|
|
366
|
+
return JSON.stringify([
|
|
367
|
+
...scope,
|
|
368
|
+
fragment.mpuSequenceNumber,
|
|
369
|
+
'item',
|
|
370
|
+
fragment.itemId,
|
|
371
|
+
])
|
|
372
|
+
}
|
|
373
|
+
if (hasSampleIdentity(fragment)) {
|
|
374
|
+
return JSON.stringify([
|
|
375
|
+
...scope,
|
|
376
|
+
fragment.mpuSequenceNumber,
|
|
377
|
+
'sample',
|
|
378
|
+
fragment.movieFragmentSequenceNumber,
|
|
379
|
+
fragment.sampleNumber,
|
|
380
|
+
// The DU's authored base offset (moqcast #37): sibling subsample DUs
|
|
381
|
+
// of one sample get distinct keys instead of colliding. Only ever
|
|
382
|
+
// called with the unmutated FI=1 (or FI=0) fragment, whose
|
|
383
|
+
// sampleOffset is that authored base -- never the accumulating
|
|
384
|
+
// per-continuation value written by #associateDataUnit.
|
|
385
|
+
fragment.sampleOffset,
|
|
386
|
+
])
|
|
387
|
+
}
|
|
388
|
+
return JSON.stringify([
|
|
389
|
+
...scope,
|
|
390
|
+
fragment.mpuSequenceNumber,
|
|
391
|
+
'unidentified',
|
|
392
|
+
])
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function ordersByOffset(fragments: MFUFragment[]): boolean {
|
|
396
|
+
return fragments.every((fragment) => hasSampleIdentity(fragment))
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
type ActiveDataUnit = {
|
|
400
|
+
bufferKey: string
|
|
401
|
+
nextFragmentCounter: number
|
|
402
|
+
catalogTrackName?: string
|
|
403
|
+
} & (
|
|
404
|
+
| {
|
|
405
|
+
kind: 'sample'
|
|
406
|
+
movieFragmentSequenceNumber: number
|
|
407
|
+
sampleNumber: number
|
|
408
|
+
nextOffset: number
|
|
409
|
+
}
|
|
410
|
+
| {
|
|
411
|
+
kind: 'item'
|
|
412
|
+
itemId: number
|
|
413
|
+
}
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
interface DataUnitAssociation {
|
|
417
|
+
bufferKey: string
|
|
418
|
+
first: boolean
|
|
419
|
+
terminal: boolean
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
interface PendingContinuationRun {
|
|
423
|
+
fragments: Map<number, MFUFragment>
|
|
424
|
+
startedAt: number
|
|
425
|
+
timeoutId?: ReturnType<typeof setTimeout>
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
interface BufferTimeoutState {
|
|
429
|
+
startedAt: number
|
|
430
|
+
timeoutId?: ReturnType<typeof setTimeout>
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function checkedSampleEnd(offset: number, byteLength: number): number | null {
|
|
434
|
+
const end = offset + byteLength
|
|
435
|
+
return Number.isSafeInteger(end) && end <= 0xffffffff ? end : null
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* MFU Reassembler with bounded reordered-fragment association
|
|
440
|
+
* Based on ISO/IEC 23008-1:2023 Section 9.2.3.3
|
|
441
|
+
*/
|
|
442
|
+
export class MFUReassembler {
|
|
443
|
+
// Fragment buffers use only the authored MFU DU identity. Continuations are
|
|
444
|
+
// admitted through bounded producer-profile runs keyed by physical stream,
|
|
445
|
+
// MPU, and the wrapping terminal packet sequence. This requires contiguous
|
|
446
|
+
// source-packet sequences within a fragmented DU; interleaving stays
|
|
447
|
+
// separated and incomplete. Continuations may wait in a counter-indexed queue
|
|
448
|
+
// until FI=1 supplies intrinsic identity; MMTP packet send time is never an
|
|
449
|
+
// association key.
|
|
450
|
+
#buffers: Map<string, MFUFragment[]> = new Map()
|
|
451
|
+
#timeouts: Map<string, BufferTimeoutState> = new Map()
|
|
452
|
+
#activeDataUnits: Map<string, ActiveDataUnit> = new Map()
|
|
453
|
+
#pendingContinuations: Map<string, PendingContinuationRun> = new Map()
|
|
454
|
+
#emitted: Set<string> = new Set() // same sample keys — reject FEC-recovered duplicates
|
|
455
|
+
#emittedOrder: string[] = []
|
|
456
|
+
#timeoutMs: number | null
|
|
457
|
+
#maxBuffered: number
|
|
458
|
+
|
|
459
|
+
/** Statistics for debugging */
|
|
460
|
+
readonly stats: ReassemblerStats = {
|
|
461
|
+
totalFragments: 0,
|
|
462
|
+
reassembledMFUs: 0,
|
|
463
|
+
rejectedFragments: 0,
|
|
464
|
+
timedOutMFUs: 0,
|
|
465
|
+
evictedMemoryLimit: 0,
|
|
466
|
+
outOfOrderFragments: 0,
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
constructor(options: MFUReassemblerOptions) {
|
|
470
|
+
if (options.timeoutMs !== null && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) {
|
|
471
|
+
throw new Error('[MFUReassembler] timeoutMs must be > 0 (derive from frame duration or FEC config)')
|
|
472
|
+
}
|
|
473
|
+
const maxBuffered = options.maxBuffered ?? 100
|
|
474
|
+
if (!Number.isInteger(maxBuffered) || maxBuffered <= 0) {
|
|
475
|
+
throw new Error('[MFUReassembler] maxBuffered must be a positive integer')
|
|
476
|
+
}
|
|
477
|
+
this.#timeoutMs = options.timeoutMs
|
|
478
|
+
this.#maxBuffered = maxBuffered
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Update the expiry policy for all retained and future incomplete MFUs. */
|
|
482
|
+
set timeoutMs(ms: number | null) {
|
|
483
|
+
if (ms !== null && (!Number.isFinite(ms) || ms <= 0)) {
|
|
484
|
+
throw new Error('[MFUReassembler] timeoutMs must be > 0 (derive from frame duration or FEC config)')
|
|
485
|
+
}
|
|
486
|
+
this.#timeoutMs = ms
|
|
487
|
+
const now = Date.now()
|
|
488
|
+
for (const [bufferKey, state] of this.#timeouts) {
|
|
489
|
+
if (state.timeoutId !== undefined) clearTimeout(state.timeoutId)
|
|
490
|
+
delete state.timeoutId
|
|
491
|
+
if (ms !== null) {
|
|
492
|
+
state.timeoutId = setTimeout(
|
|
493
|
+
() => this.#onTimeout(bufferKey),
|
|
494
|
+
Math.max(0, state.startedAt + ms - now),
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
for (const [runKey, pending] of this.#pendingContinuations) {
|
|
499
|
+
if (pending.timeoutId !== undefined) clearTimeout(pending.timeoutId)
|
|
500
|
+
delete pending.timeoutId
|
|
501
|
+
if (ms !== null && !this.#activeDataUnits.has(runKey)) {
|
|
502
|
+
pending.timeoutId = setTimeout(
|
|
503
|
+
() => this.#onPendingTimeout(runKey),
|
|
504
|
+
Math.max(0, pending.startedAt + ms - now),
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
get timeoutMs(): number | null { return this.#timeoutMs }
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Add a fragment and return reassembled MFU if complete
|
|
513
|
+
*/
|
|
514
|
+
addFragment(fragment: MFUFragment): ReassembledMFU | null {
|
|
515
|
+
this.stats.totalFragments++
|
|
516
|
+
|
|
517
|
+
if (!hasValidFragmentHeader(fragment)) {
|
|
518
|
+
this.#reject('invalid-fragment-state', 'invalid MFU fragmentation indicator or counter')
|
|
519
|
+
}
|
|
520
|
+
if (fragment.itemId !== undefined && hasAnySampleIdentityField(fragment)) {
|
|
521
|
+
this.#reject(
|
|
522
|
+
'conflicting-data-unit-identity',
|
|
523
|
+
'MFU carries timed and non-timed DU identities',
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const { mpuSequenceNumber, fragmentationIndicator } = fragment
|
|
528
|
+
|
|
529
|
+
// A complete FT=2 data unit still requires its intrinsic DU identity.
|
|
530
|
+
// The transport parser strips the deterministic timed/non-timed DU header
|
|
531
|
+
// before calling this layer; direct callers must provide the same identity.
|
|
532
|
+
// Don't check or update #emitted: in MMTP over MoQ, multiple complete MFUs
|
|
533
|
+
// share the same mpuSequenceNumber (all frames in a GOP have the same mpuSeq).
|
|
534
|
+
// Blocking FI=0 would prevent keyframe fragment assembly at the same mpuSeq.
|
|
535
|
+
if (fragmentationIndicator === 0) {
|
|
536
|
+
const sampleIdentity = hasSampleIdentity(fragment)
|
|
537
|
+
const hasSampleFields = hasAnySampleIdentityField(fragment)
|
|
538
|
+
const hasItemIdentity = fragment.itemId !== undefined
|
|
539
|
+
if (!sampleIdentity && !hasItemIdentity) {
|
|
540
|
+
this.#reject('missing-data-unit-identity', 'complete MFU has no intrinsic DU identity')
|
|
541
|
+
}
|
|
542
|
+
if (hasSampleFields && !sampleIdentity) {
|
|
543
|
+
this.#reject('missing-data-unit-identity', 'complete MFU has an incomplete sample identity')
|
|
544
|
+
}
|
|
545
|
+
if (hasItemIdentity && !isU32(fragment.itemId)) {
|
|
546
|
+
this.#reject('missing-data-unit-identity', 'complete MFU has an invalid Item_ID')
|
|
547
|
+
}
|
|
548
|
+
this.stats.reassembledMFUs++
|
|
549
|
+
return {
|
|
550
|
+
mpuSequenceNumber,
|
|
551
|
+
packetSequenceNumber: fragment.packetSequenceNumber,
|
|
552
|
+
...assembledPacketSequences([fragment]),
|
|
553
|
+
data: fragment.data,
|
|
554
|
+
timestamp: fragment.timestamp,
|
|
555
|
+
fragmentCount: 1,
|
|
556
|
+
rapFlag: fragment.rapFlag,
|
|
557
|
+
priority: fragment.priority,
|
|
558
|
+
sampleNumber: fragment.sampleNumber,
|
|
559
|
+
movieFragmentSequenceNumber: fragment.movieFragmentSequenceNumber,
|
|
560
|
+
sampleOffset: fragment.sampleOffset,
|
|
561
|
+
source: fragment.source,
|
|
562
|
+
deliveryProvenance: deliveryProvenanceOf([fragment]),
|
|
563
|
+
catalogTrackName: fragment.catalogTrackName,
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!isU32(fragment.packetSequenceNumber)) {
|
|
568
|
+
this.#reject(
|
|
569
|
+
'missing-packet-sequence',
|
|
570
|
+
'fragmented MFU has no MMTP packet sequence number',
|
|
571
|
+
)
|
|
572
|
+
}
|
|
573
|
+
const runKey = fragmentRunKey(fragment)
|
|
574
|
+
if (
|
|
575
|
+
fragmentationIndicator === 1 &&
|
|
576
|
+
(hasSampleIdentity(fragment) || isU32(fragment.itemId)) &&
|
|
577
|
+
this.#emitted.has(objectKey(fragment))
|
|
578
|
+
) {
|
|
579
|
+
this.#deletePendingRun(runKey)
|
|
580
|
+
return null
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (fragmentationIndicator !== 1) {
|
|
584
|
+
const active = this.#activeDataUnits.get(runKey)
|
|
585
|
+
if (active !== undefined && fragment.fragmentCounter > active.nextFragmentCounter) {
|
|
586
|
+
this.#reject(
|
|
587
|
+
'fragment-counter-mismatch',
|
|
588
|
+
'MFU continuation is behind the authored fragment-counter countdown',
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
if (active === undefined || fragment.fragmentCounter < active.nextFragmentCounter) {
|
|
592
|
+
this.#parkContinuation(runKey, fragment)
|
|
593
|
+
return null
|
|
594
|
+
}
|
|
595
|
+
} else if (this.#activeDataUnits.has(runKey)) {
|
|
596
|
+
this.#reject('overlapping-data-unit', 'fragmented MFU overlaps an active data unit')
|
|
597
|
+
} else {
|
|
598
|
+
const pending = this.#pendingContinuations.get(runKey)
|
|
599
|
+
if (pending !== undefined && [...pending.fragments.keys()].some(
|
|
600
|
+
counter => counter >= fragment.fragmentCounter,
|
|
601
|
+
)) {
|
|
602
|
+
this.#reject(
|
|
603
|
+
'fragment-counter-mismatch',
|
|
604
|
+
'queued continuation counter is not below the authored first-fragment counter',
|
|
605
|
+
)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const pendingStartedAt = fragmentationIndicator === 1
|
|
610
|
+
? this.#pendingContinuations.get(runKey)?.startedAt
|
|
611
|
+
: undefined
|
|
612
|
+
const result = this.#addAssociatedFragment(fragment, runKey, pendingStartedAt)
|
|
613
|
+
if (fragmentationIndicator === 1) this.#clearPendingTimeout(runKey)
|
|
614
|
+
if (result !== null) return result
|
|
615
|
+
return this.#drainPendingContinuations(runKey)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
#addAssociatedFragment(
|
|
619
|
+
fragment: MFUFragment,
|
|
620
|
+
runKey: string,
|
|
621
|
+
startedAt?: number,
|
|
622
|
+
): ReassembledMFU | null {
|
|
623
|
+
const association = this.#associateDataUnit(fragment, runKey)
|
|
624
|
+
const { bufferKey, first, terminal } = association
|
|
625
|
+
|
|
626
|
+
let fragments = this.#buffers.get(bufferKey)
|
|
627
|
+
if (first) {
|
|
628
|
+
if (fragments !== undefined) {
|
|
629
|
+
this.#reject('overlapping-data-unit', 'intrinsic data-unit buffer already exists')
|
|
630
|
+
}
|
|
631
|
+
fragments = []
|
|
632
|
+
this.#buffers.set(bufferKey, fragments)
|
|
633
|
+
} else if (fragments === undefined) {
|
|
634
|
+
throw new Error('[MFUReassembler] active data unit has no fragment buffer')
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// A lossy-path deadline is measured from the first received fragment,
|
|
638
|
+
// including a continuation that arrived before FI=1. Reliable paths are
|
|
639
|
+
// clock-free and never install this timer.
|
|
640
|
+
if (first) this.#setTimeout(bufferKey, startedAt)
|
|
641
|
+
fragments.push(fragment)
|
|
642
|
+
|
|
643
|
+
if (!terminal) return null
|
|
644
|
+
const result = this.#tryReassemble(bufferKey)
|
|
645
|
+
this.#activeDataUnits.delete(runKey)
|
|
646
|
+
if (result === null) {
|
|
647
|
+
this.#clearTimeout(bufferKey)
|
|
648
|
+
this.#buffers.delete(bufferKey)
|
|
649
|
+
this.#reject(
|
|
650
|
+
'incomplete-terminal-fragment',
|
|
651
|
+
'validated terminal fragment did not complete its data unit',
|
|
652
|
+
)
|
|
653
|
+
}
|
|
654
|
+
return result
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
#parkContinuation(runKey: string, fragment: MFUFragment): void {
|
|
658
|
+
let pending = this.#pendingContinuations.get(runKey)
|
|
659
|
+
if (pending === undefined) {
|
|
660
|
+
const awaitingFirst = !this.#activeDataUnits.has(runKey)
|
|
661
|
+
if (awaitingFirst && this.#incompleteRunCount() >= this.#maxBuffered) {
|
|
662
|
+
this.#reject('active-data-unit-limit', 'active MFU data-unit limit reached')
|
|
663
|
+
}
|
|
664
|
+
const startedAt = Date.now()
|
|
665
|
+
pending = { fragments: new Map(), startedAt }
|
|
666
|
+
if (awaitingFirst && this.#timeoutMs !== null) {
|
|
667
|
+
pending.timeoutId = setTimeout(() => this.#onPendingTimeout(runKey), this.#timeoutMs)
|
|
668
|
+
}
|
|
669
|
+
this.#pendingContinuations.set(runKey, pending)
|
|
670
|
+
}
|
|
671
|
+
if (pending.fragments.has(fragment.fragmentCounter)) {
|
|
672
|
+
this.#reject(
|
|
673
|
+
'fragment-counter-mismatch',
|
|
674
|
+
'duplicate queued continuation counter would replace retained media',
|
|
675
|
+
)
|
|
676
|
+
}
|
|
677
|
+
pending.fragments.set(fragment.fragmentCounter, fragment)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
#drainPendingContinuations(runKey: string): ReassembledMFU | null {
|
|
681
|
+
while (true) {
|
|
682
|
+
const active = this.#activeDataUnits.get(runKey)
|
|
683
|
+
if (active === undefined) {
|
|
684
|
+
this.#deletePendingRun(runKey)
|
|
685
|
+
return null
|
|
686
|
+
}
|
|
687
|
+
const pending = this.#pendingContinuations.get(runKey)
|
|
688
|
+
const fragment = pending?.fragments.get(active.nextFragmentCounter)
|
|
689
|
+
if (fragment === undefined) return null
|
|
690
|
+
pending!.fragments.delete(active.nextFragmentCounter)
|
|
691
|
+
if (pending!.fragments.size === 0) this.#deletePendingRun(runKey)
|
|
692
|
+
const result = this.#addAssociatedFragment(fragment, runKey)
|
|
693
|
+
if (result !== null) {
|
|
694
|
+
this.#deletePendingRun(runKey)
|
|
695
|
+
return result
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
#incompleteRunCount(): number {
|
|
701
|
+
let count = this.#activeDataUnits.size
|
|
702
|
+
for (const runKey of this.#pendingContinuations.keys()) {
|
|
703
|
+
if (!this.#activeDataUnits.has(runKey)) count++
|
|
704
|
+
}
|
|
705
|
+
return count
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
#reject(code: MfuReassemblyErrorCode, message: string): never {
|
|
709
|
+
this.stats.rejectedFragments++
|
|
710
|
+
throw new MfuReassemblyError(code, message)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
#associateDataUnit(fragment: MFUFragment, runKey: string): DataUnitAssociation {
|
|
714
|
+
const fi = fragment.fragmentationIndicator
|
|
715
|
+
if (fi === 1) {
|
|
716
|
+
if (this.#activeDataUnits.has(runKey)) {
|
|
717
|
+
this.#reject('overlapping-data-unit', 'fragmented MFU overlaps an active data unit')
|
|
718
|
+
}
|
|
719
|
+
if (!this.#pendingContinuations.has(runKey) &&
|
|
720
|
+
this.#incompleteRunCount() >= this.#maxBuffered) {
|
|
721
|
+
this.#reject('active-data-unit-limit', 'active MFU data-unit limit reached')
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const sampleIdentity = hasSampleIdentity(fragment)
|
|
725
|
+
const hasSampleFields = hasAnySampleIdentityField(fragment)
|
|
726
|
+
const hasItemIdentity = fragment.itemId !== undefined
|
|
727
|
+
if (sampleIdentity && hasItemIdentity) {
|
|
728
|
+
this.#reject(
|
|
729
|
+
'conflicting-data-unit-identity',
|
|
730
|
+
'fragmented MFU carries timed and non-timed DU identities',
|
|
731
|
+
)
|
|
732
|
+
}
|
|
733
|
+
if (!sampleIdentity && !hasItemIdentity) {
|
|
734
|
+
this.#reject('missing-data-unit-identity', 'first fragment has no intrinsic DU identity')
|
|
735
|
+
}
|
|
736
|
+
if (hasSampleFields && !sampleIdentity) {
|
|
737
|
+
this.#reject('missing-data-unit-identity', 'first fragment has an incomplete sample identity')
|
|
738
|
+
}
|
|
739
|
+
if (hasItemIdentity && !isU32(fragment.itemId)) {
|
|
740
|
+
this.#reject('missing-data-unit-identity', 'first fragment has an invalid Item_ID')
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const bufferKey = objectKey(fragment)
|
|
744
|
+
if (this.#buffers.has(bufferKey)) {
|
|
745
|
+
this.#reject('overlapping-data-unit', 'intrinsic data-unit buffer already exists')
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
let active: ActiveDataUnit
|
|
749
|
+
if (sampleIdentity) {
|
|
750
|
+
const nextOffset = checkedSampleEnd(fragment.sampleOffset, fragment.data.byteLength)
|
|
751
|
+
if (nextOffset === null) this.#reject('offset-overflow', 'sample byte offset overflow')
|
|
752
|
+
active = {
|
|
753
|
+
kind: 'sample',
|
|
754
|
+
bufferKey,
|
|
755
|
+
nextFragmentCounter: fragment.fragmentCounter - 1,
|
|
756
|
+
movieFragmentSequenceNumber: fragment.movieFragmentSequenceNumber,
|
|
757
|
+
sampleNumber: fragment.sampleNumber,
|
|
758
|
+
nextOffset,
|
|
759
|
+
...(fragment.catalogTrackName === undefined
|
|
760
|
+
? {}
|
|
761
|
+
: { catalogTrackName: fragment.catalogTrackName }),
|
|
762
|
+
}
|
|
763
|
+
} else {
|
|
764
|
+
active = {
|
|
765
|
+
kind: 'item',
|
|
766
|
+
bufferKey,
|
|
767
|
+
nextFragmentCounter: fragment.fragmentCounter - 1,
|
|
768
|
+
itemId: fragment.itemId!,
|
|
769
|
+
...(fragment.catalogTrackName === undefined
|
|
770
|
+
? {}
|
|
771
|
+
: { catalogTrackName: fragment.catalogTrackName }),
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
this.#activeDataUnits.set(runKey, active)
|
|
775
|
+
return { bufferKey, first: true, terminal: false }
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const active = this.#activeDataUnits.get(runKey)
|
|
779
|
+
if (active === undefined) {
|
|
780
|
+
throw new Error('[MFUReassembler] continuation was not parked before association')
|
|
781
|
+
}
|
|
782
|
+
if (fragment.fragmentCounter !== active.nextFragmentCounter) {
|
|
783
|
+
this.#reject(
|
|
784
|
+
'fragment-counter-mismatch',
|
|
785
|
+
'MFU continuation does not follow the authored fragment-counter countdown',
|
|
786
|
+
)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (active.kind === 'sample') {
|
|
790
|
+
if (fragment.itemId !== undefined) {
|
|
791
|
+
this.#reject('data-unit-identity-mismatch', 'sample continuation carries an Item_ID')
|
|
792
|
+
}
|
|
793
|
+
if (hasAnySampleIdentityField(fragment) && (
|
|
794
|
+
!hasSampleIdentity(fragment) ||
|
|
795
|
+
fragment.movieFragmentSequenceNumber !== active.movieFragmentSequenceNumber ||
|
|
796
|
+
fragment.sampleNumber !== active.sampleNumber || fragment.sampleOffset !== active.nextOffset
|
|
797
|
+
)) {
|
|
798
|
+
this.#reject('data-unit-identity-mismatch', 'sample continuation identity does not match')
|
|
799
|
+
}
|
|
800
|
+
const nextOffset = checkedSampleEnd(active.nextOffset, fragment.data.byteLength)
|
|
801
|
+
if (nextOffset === null) this.#reject('offset-overflow', 'sample byte offset overflow')
|
|
802
|
+
fragment.movieFragmentSequenceNumber = active.movieFragmentSequenceNumber
|
|
803
|
+
fragment.sampleNumber = active.sampleNumber
|
|
804
|
+
fragment.sampleOffset = active.nextOffset
|
|
805
|
+
if (fi === 2) {
|
|
806
|
+
active.nextOffset = nextOffset
|
|
807
|
+
active.nextFragmentCounter = fragment.fragmentCounter - 1
|
|
808
|
+
}
|
|
809
|
+
} else {
|
|
810
|
+
if (hasAnySampleIdentityField(fragment) ||
|
|
811
|
+
(fragment.itemId !== undefined && fragment.itemId !== active.itemId)) {
|
|
812
|
+
this.#reject('data-unit-identity-mismatch', 'Item_ID continuation identity does not match')
|
|
813
|
+
}
|
|
814
|
+
fragment.itemId = active.itemId
|
|
815
|
+
if (fi === 2) active.nextFragmentCounter = fragment.fragmentCounter - 1
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return {
|
|
819
|
+
bufferKey: active.bufferKey,
|
|
820
|
+
first: false,
|
|
821
|
+
terminal: fi === 3,
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
#tryReassemble(bufferKey: string): ReassembledMFU | null {
|
|
826
|
+
const fragments = this.#buffers.get(bufferKey)
|
|
827
|
+
if (!fragments || fragments.length === 0) {
|
|
828
|
+
return null
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const sorted = [...fragments].sort((a, b) => this.#orderValue(a) - this.#orderValue(b))
|
|
832
|
+
|
|
833
|
+
if (
|
|
834
|
+
sorted[0]!.fragmentationIndicator !== 1 ||
|
|
835
|
+
sorted[sorted.length - 1]!.fragmentationIndicator !== 3
|
|
836
|
+
) {
|
|
837
|
+
return null
|
|
838
|
+
}
|
|
839
|
+
if (sorted.slice(1, -1).some((fragment) => fragment.fragmentationIndicator !== 2)) {
|
|
840
|
+
return null
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// ISO fragment_counter is the number of succeeding payloads. After
|
|
844
|
+
// normalization (`255 - counter`), a complete non-wrapping chain is strictly
|
|
845
|
+
// consecutive. This also caps a data unit at 256 payloads.
|
|
846
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
847
|
+
if (this.#orderValue(sorted[i]!) !== this.#orderValue(sorted[i - 1]!) + 1) {
|
|
848
|
+
return null
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// When every fragment carries a timed-MFU byte offset, independently prove
|
|
853
|
+
// byte adjacency within this data unit. Counter order remains authoritative;
|
|
854
|
+
// conflicting offsets fail closed instead of changing the fragment order.
|
|
855
|
+
// sorted[0]'s offset is this DU's authored base (moqcast #37: zero for a
|
|
856
|
+
// whole-sample DU, nonzero for one subsample DU of a partitioned sample) --
|
|
857
|
+
// never asserted against a literal zero.
|
|
858
|
+
if (ordersByOffset(sorted)) {
|
|
859
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
860
|
+
const previous = sorted[i - 1]!
|
|
861
|
+
if (
|
|
862
|
+
sorted[i]!.sampleOffset !==
|
|
863
|
+
previous.sampleOffset! + previous.data.byteLength
|
|
864
|
+
) {
|
|
865
|
+
return null
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
fragments.splice(0, fragments.length, ...sorted)
|
|
871
|
+
return this.#assemble(bufferKey, fragments)
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
#assemble(bufferKey: string, fragments: MFUFragment[]): ReassembledMFU {
|
|
875
|
+
// Reassemble: concatenate all fragment data
|
|
876
|
+
const totalSize = fragments.reduce((sum, f) => sum + f.data.length, 0)
|
|
877
|
+
const data = new Uint8Array(totalSize)
|
|
878
|
+
|
|
879
|
+
let offset = 0
|
|
880
|
+
for (const fragment of fragments) {
|
|
881
|
+
data.set(fragment.data, offset)
|
|
882
|
+
offset += fragment.data.length
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Cleanup + mark the intrinsic identity as emitted to prevent a recovered
|
|
886
|
+
// duplicate from starting another fragmented run.
|
|
887
|
+
this.#clearTimeout(bufferKey)
|
|
888
|
+
this.#buffers.delete(bufferKey)
|
|
889
|
+
this.#markEmitted(bufferKey)
|
|
890
|
+
this.stats.reassembledMFUs++
|
|
891
|
+
|
|
892
|
+
return {
|
|
893
|
+
mpuSequenceNumber: fragments[0]!.mpuSequenceNumber,
|
|
894
|
+
packetSequenceNumber: fragments[0]!.packetSequenceNumber,
|
|
895
|
+
...assembledPacketSequences(fragments),
|
|
896
|
+
data,
|
|
897
|
+
timestamp: fragments[0]!.timestamp,
|
|
898
|
+
fragmentCount: fragments.length,
|
|
899
|
+
rapFlag: fragments[0]!.rapFlag,
|
|
900
|
+
priority: fragments.find((f) => f.priority !== undefined)?.priority,
|
|
901
|
+
sampleNumber: fragments[0]!.sampleNumber,
|
|
902
|
+
movieFragmentSequenceNumber: fragments[0]!.movieFragmentSequenceNumber,
|
|
903
|
+
sampleOffset: fragments[0]!.sampleOffset,
|
|
904
|
+
source: fragments[0]!.source,
|
|
905
|
+
deliveryProvenance: deliveryProvenanceOf(fragments),
|
|
906
|
+
catalogTrackName: fragments[0]!.catalogTrackName,
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
#setTimeout(bufferKey: string, startedAt = Date.now()): void {
|
|
911
|
+
const state: BufferTimeoutState = { startedAt }
|
|
912
|
+
if (this.#timeoutMs !== null) {
|
|
913
|
+
state.timeoutId = setTimeout(
|
|
914
|
+
() => this.#onTimeout(bufferKey),
|
|
915
|
+
Math.max(0, startedAt + this.#timeoutMs - Date.now()),
|
|
916
|
+
)
|
|
917
|
+
}
|
|
918
|
+
this.#timeouts.set(bufferKey, state)
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
#clearPendingTimeout(runKey: string): void {
|
|
922
|
+
const pending = this.#pendingContinuations.get(runKey)
|
|
923
|
+
if (pending?.timeoutId !== undefined) clearTimeout(pending.timeoutId)
|
|
924
|
+
if (pending !== undefined) {
|
|
925
|
+
delete pending.timeoutId
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
#deletePendingRun(runKey: string): void {
|
|
930
|
+
this.#clearPendingTimeout(runKey)
|
|
931
|
+
this.#pendingContinuations.delete(runKey)
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
#onPendingTimeout(runKey: string): void {
|
|
935
|
+
const pending = this.#pendingContinuations.get(runKey)
|
|
936
|
+
if (pending === undefined) return
|
|
937
|
+
this.stats.timedOutMFUs++
|
|
938
|
+
console.warn(
|
|
939
|
+
`[MfuReassembler] timeout: run=${runKey} fragments=${pending.fragments.size} ` +
|
|
940
|
+
'(continuations awaiting first fragment, discarded)',
|
|
941
|
+
)
|
|
942
|
+
this.#pendingContinuations.delete(runKey)
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
#clearTimeout(bufferKey: string): void {
|
|
946
|
+
const state = this.#timeouts.get(bufferKey)
|
|
947
|
+
if (state !== undefined) {
|
|
948
|
+
if (state.timeoutId !== undefined) clearTimeout(state.timeoutId)
|
|
949
|
+
this.#timeouts.delete(bufferKey)
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** Callback for deferred assembly results (set by caller for async frame delivery) */
|
|
954
|
+
onDeferredAssembly?: (result: ReassembledMFU) => void
|
|
955
|
+
|
|
956
|
+
#markEmitted(bufferKey: string): void {
|
|
957
|
+
if (this.#emitted.has(bufferKey)) return
|
|
958
|
+
this.#emitted.add(bufferKey)
|
|
959
|
+
this.#emittedOrder.push(bufferKey)
|
|
960
|
+
while (this.#emittedOrder.length > this.#maxBuffered) {
|
|
961
|
+
const oldest = this.#emittedOrder.shift()
|
|
962
|
+
if (oldest !== undefined) this.#emitted.delete(oldest)
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
#orderValue(fragment: MFUFragment): number {
|
|
967
|
+
return fragmentCounterOrder(fragment)
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
#removeActiveDataUnitForBuffer(bufferKey: string): void {
|
|
971
|
+
for (const [runKey, active] of this.#activeDataUnits) {
|
|
972
|
+
if (active.bufferKey === bufferKey) {
|
|
973
|
+
this.#activeDataUnits.delete(runKey)
|
|
974
|
+
this.#deletePendingRun(runKey)
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
#onTimeout(bufferKey: string): void {
|
|
980
|
+
const fragments = this.#buffers.get(bufferKey)
|
|
981
|
+
if (!fragments || fragments.length === 0) {
|
|
982
|
+
this.#buffers.delete(bufferKey)
|
|
983
|
+
this.#timeouts.delete(bufferKey)
|
|
984
|
+
this.#removeActiveDataUnitForBuffer(bufferKey)
|
|
985
|
+
this.stats.timedOutMFUs++
|
|
986
|
+
console.warn(`[MfuReassembler] timeout: key=${bufferKey} fragments=0 (no data buffered)`)
|
|
987
|
+
return
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Discard partial MFUs — incomplete AVCC data crashes WebCodecs with
|
|
991
|
+
// "Unable to determine size of bitstream buffer". FEC recovery handles
|
|
992
|
+
// loss; partial assembly only produces garbage frames.
|
|
993
|
+
this.stats.timedOutMFUs++
|
|
994
|
+
console.warn(
|
|
995
|
+
`[MfuReassembler] timeout: key=${bufferKey} fragments=${fragments.length} (partial, discarded)`,
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
this.#buffers.delete(bufferKey)
|
|
999
|
+
this.#timeouts.delete(bufferKey)
|
|
1000
|
+
this.#removeActiveDataUnitForBuffer(bufferKey)
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Flush all buffered fragments
|
|
1005
|
+
*/
|
|
1006
|
+
flush(): void {
|
|
1007
|
+
for (const key of this.#buffers.keys()) {
|
|
1008
|
+
this.#clearTimeout(key)
|
|
1009
|
+
}
|
|
1010
|
+
this.#buffers.clear()
|
|
1011
|
+
this.#timeouts.clear()
|
|
1012
|
+
this.#activeDataUnits.clear()
|
|
1013
|
+
for (const runKey of this.#pendingContinuations.keys()) {
|
|
1014
|
+
this.#clearPendingTimeout(runKey)
|
|
1015
|
+
}
|
|
1016
|
+
this.#pendingContinuations.clear()
|
|
1017
|
+
this.#emitted.clear()
|
|
1018
|
+
this.#emittedOrder.length = 0
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/** Drop one catalog stream's incomplete and dedup state. */
|
|
1022
|
+
resetStream(catalogTrackName: string): void {
|
|
1023
|
+
const belongsToStream = (key: string): boolean => {
|
|
1024
|
+
try {
|
|
1025
|
+
const decoded = JSON.parse(key)
|
|
1026
|
+
return Array.isArray(decoded) && decoded[0] === 'catalog' && decoded[1] === catalogTrackName
|
|
1027
|
+
} catch {
|
|
1028
|
+
return false
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const discarded = new Set<string>()
|
|
1032
|
+
for (const [key, fragments] of this.#buffers) {
|
|
1033
|
+
if (fragments.some((fragment) => fragment.catalogTrackName === catalogTrackName)) {
|
|
1034
|
+
discarded.add(key)
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
for (const [runKey, active] of this.#activeDataUnits) {
|
|
1038
|
+
if (active.catalogTrackName === catalogTrackName) {
|
|
1039
|
+
discarded.add(active.bufferKey)
|
|
1040
|
+
this.#activeDataUnits.delete(runKey)
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
for (const [runKey, pending] of this.#pendingContinuations) {
|
|
1044
|
+
if ([...pending.fragments.values()].some(
|
|
1045
|
+
fragment => fragment.catalogTrackName === catalogTrackName,
|
|
1046
|
+
)) {
|
|
1047
|
+
this.#deletePendingRun(runKey)
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
for (const key of this.#emitted) {
|
|
1051
|
+
if (belongsToStream(key)) discarded.add(key)
|
|
1052
|
+
}
|
|
1053
|
+
for (const key of discarded) {
|
|
1054
|
+
this.#clearTimeout(key)
|
|
1055
|
+
this.#buffers.delete(key)
|
|
1056
|
+
this.#emitted.delete(key)
|
|
1057
|
+
}
|
|
1058
|
+
this.#emittedOrder = this.#emittedOrder.filter((key) => !discarded.has(key))
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/** Reset all state — flush buffers and zero stats */
|
|
1062
|
+
reset(): void {
|
|
1063
|
+
this.flush()
|
|
1064
|
+
this.stats.totalFragments = 0
|
|
1065
|
+
this.stats.reassembledMFUs = 0
|
|
1066
|
+
this.stats.rejectedFragments = 0
|
|
1067
|
+
this.stats.timedOutMFUs = 0
|
|
1068
|
+
this.stats.evictedMemoryLimit = 0
|
|
1069
|
+
this.stats.outOfOrderFragments = 0
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Get number of pending MFUs
|
|
1074
|
+
*/
|
|
1075
|
+
get pendingCount(): number {
|
|
1076
|
+
let count = this.#buffers.size
|
|
1077
|
+
for (const runKey of this.#pendingContinuations.keys()) {
|
|
1078
|
+
if (!this.#activeDataUnits.has(runKey)) count++
|
|
1079
|
+
}
|
|
1080
|
+
return count
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Get current buffer status
|
|
1085
|
+
*/
|
|
1086
|
+
getStatus(): {
|
|
1087
|
+
bufferedMFUs: number
|
|
1088
|
+
pendingFragments: number
|
|
1089
|
+
pendingSamples: PendingMfuDebug[]
|
|
1090
|
+
stats: ReassemblerStats
|
|
1091
|
+
} {
|
|
1092
|
+
let pendingFragments = 0
|
|
1093
|
+
for (const fragments of this.#buffers.values()) {
|
|
1094
|
+
pendingFragments += fragments.length
|
|
1095
|
+
}
|
|
1096
|
+
for (const pending of this.#pendingContinuations.values()) {
|
|
1097
|
+
pendingFragments += pending.fragments.size
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return {
|
|
1101
|
+
bufferedMFUs: this.pendingCount,
|
|
1102
|
+
pendingFragments,
|
|
1103
|
+
pendingSamples: this.#pendingDebugSamples(),
|
|
1104
|
+
stats: { ...this.stats },
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
#pendingDebugSamples(): PendingMfuDebug[] {
|
|
1109
|
+
const out: PendingMfuDebug[] = []
|
|
1110
|
+
for (const [key, fragments] of this.#buffers) {
|
|
1111
|
+
if (fragments.length === 0) continue
|
|
1112
|
+
const sorted = [...fragments].sort((a, b) => this.#orderValue(a) - this.#orderValue(b))
|
|
1113
|
+
const fiCounts = [0, 0, 0, 0]
|
|
1114
|
+
let minCounter = Infinity
|
|
1115
|
+
let maxCounter = -Infinity
|
|
1116
|
+
let minOffset = Infinity
|
|
1117
|
+
let maxOffset = -Infinity
|
|
1118
|
+
for (const fragment of sorted) {
|
|
1119
|
+
fiCounts[fragment.fragmentationIndicator] =
|
|
1120
|
+
(fiCounts[fragment.fragmentationIndicator] ?? 0) + 1
|
|
1121
|
+
minCounter = Math.min(minCounter, fragment.fragmentCounter)
|
|
1122
|
+
maxCounter = Math.max(maxCounter, fragment.fragmentCounter)
|
|
1123
|
+
if (typeof fragment.sampleOffset === 'number') {
|
|
1124
|
+
minOffset = Math.min(minOffset, fragment.sampleOffset)
|
|
1125
|
+
maxOffset = Math.max(maxOffset, fragment.sampleOffset)
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const point = (fragment: MFUFragment) => ({
|
|
1129
|
+
fi: fragment.fragmentationIndicator,
|
|
1130
|
+
counter: fragment.fragmentCounter,
|
|
1131
|
+
order: this.#orderValue(fragment),
|
|
1132
|
+
...(typeof fragment.packetSequenceNumber === 'number' ? { seq: fragment.packetSequenceNumber } : {}),
|
|
1133
|
+
...(typeof fragment.sampleOffset === 'number' ? { offset: fragment.sampleOffset } : {}),
|
|
1134
|
+
})
|
|
1135
|
+
const summary: PendingMfuDebug = {
|
|
1136
|
+
key,
|
|
1137
|
+
fragments: sorted.length,
|
|
1138
|
+
fi0: fiCounts[0] ?? 0,
|
|
1139
|
+
fi1: fiCounts[1] ?? 0,
|
|
1140
|
+
fi2: fiCounts[2] ?? 0,
|
|
1141
|
+
fi3: fiCounts[3] ?? 0,
|
|
1142
|
+
firstFi: sorted[0]!.fragmentationIndicator,
|
|
1143
|
+
lastFi: sorted[sorted.length - 1]!.fragmentationIndicator,
|
|
1144
|
+
firstOrder: this.#orderValue(sorted[0]!),
|
|
1145
|
+
lastOrder: this.#orderValue(sorted[sorted.length - 1]!),
|
|
1146
|
+
minCounter,
|
|
1147
|
+
maxCounter,
|
|
1148
|
+
movieFragmentSequenceNumber: sorted.find((f) => f.movieFragmentSequenceNumber !== undefined)
|
|
1149
|
+
?.movieFragmentSequenceNumber,
|
|
1150
|
+
sampleNumber: sorted.find((f) => f.sampleNumber !== undefined)?.sampleNumber,
|
|
1151
|
+
mpuSequenceNumber: sorted[0]!.mpuSequenceNumber,
|
|
1152
|
+
sortedHead: sorted.slice(0, 6).map(point),
|
|
1153
|
+
sortedTail: sorted.slice(-6).map(point),
|
|
1154
|
+
}
|
|
1155
|
+
if (minOffset !== Infinity) summary.minOffset = minOffset
|
|
1156
|
+
if (maxOffset !== -Infinity) summary.maxOffset = maxOffset
|
|
1157
|
+
out.push(summary)
|
|
1158
|
+
if (out.length >= 12) break
|
|
1159
|
+
}
|
|
1160
|
+
return out
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Create MFU fragment from MMTP packet for reassembly
|
|
1166
|
+
*/
|
|
1167
|
+
export function createFragment(mmtpPacket: Uint8Array): MFUFragment {
|
|
1168
|
+
const extraction = extractMFUPayload(mmtpPacket)
|
|
1169
|
+
if (!extraction) throw new Error('Unable to extract MFU payload from MMTP packet')
|
|
1170
|
+
const { header, mpu, data, mfuDuHeader, itemId } = extraction
|
|
1171
|
+
|
|
1172
|
+
return {
|
|
1173
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
1174
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
1175
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
1176
|
+
data,
|
|
1177
|
+
timestamp: header.timestamp,
|
|
1178
|
+
rapFlag: header.rapFlag,
|
|
1179
|
+
sampleNumber: mfuDuHeader?.sampleNumber,
|
|
1180
|
+
movieFragmentSequenceNumber: mfuDuHeader?.movieFragmentSequenceNumber,
|
|
1181
|
+
sampleOffset: mfuDuHeader?.offset,
|
|
1182
|
+
priority: mfuDuHeader?.priority,
|
|
1183
|
+
itemId: itemId ?? undefined,
|
|
1184
|
+
packetSequenceNumber: header.sequenceNumber,
|
|
1185
|
+
packetId: header.packetId,
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Process MMTP packet with fragment reassembly
|
|
1191
|
+
*/
|
|
1192
|
+
export function processMMTPWithReassembly(
|
|
1193
|
+
mmtpPacket: Uint8Array,
|
|
1194
|
+
reassembler: MFUReassembler,
|
|
1195
|
+
): ReassembledMFU | null {
|
|
1196
|
+
// Skip FEC repair packets
|
|
1197
|
+
if (isFecRepairPacket(mmtpPacket)) {
|
|
1198
|
+
return null
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const fragment = createFragment(mmtpPacket)
|
|
1202
|
+
return reassembler.addFragment(fragment)
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// =============================================================================
|
|
1206
|
+
// Inline Tests (vitest)
|
|
1207
|
+
// =============================================================================
|
|
1208
|
+
|
|
1209
|
+
if (import.meta.vitest) {
|
|
1210
|
+
const { describe, test, expect, vi } = import.meta.vitest
|
|
1211
|
+
|
|
1212
|
+
const expectRejection = (call: () => unknown, code: MfuReassemblyErrorCode): void => {
|
|
1213
|
+
let rejection: unknown
|
|
1214
|
+
try {
|
|
1215
|
+
call()
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
rejection = error
|
|
1218
|
+
}
|
|
1219
|
+
expect(rejection).toBeInstanceOf(MfuReassemblyError)
|
|
1220
|
+
expect((rejection as MfuReassemblyError).code).toBe(code)
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const testPacketSequence = (mpu: number, counter: number, run = 0): number =>
|
|
1224
|
+
(mpu * 1000 + run - counter + 0x1_0000_0000) % 0x1_0000_0000
|
|
1225
|
+
|
|
1226
|
+
describe('MFUReassembler', () => {
|
|
1227
|
+
test('should return complete MFU for FI=0', () => {
|
|
1228
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1229
|
+
const result = reassembler.addFragment({
|
|
1230
|
+
fragmentationIndicator: 0,
|
|
1231
|
+
mpuSequenceNumber: 1,
|
|
1232
|
+
fragmentCounter: 0,
|
|
1233
|
+
itemId: 1,
|
|
1234
|
+
data: new Uint8Array([1, 2, 3, 4]),
|
|
1235
|
+
timestamp: 1000n,
|
|
1236
|
+
|
|
1237
|
+
rapFlag: true,
|
|
1238
|
+
})
|
|
1239
|
+
expect(result).not.toBeNull()
|
|
1240
|
+
expect(result?.data).toEqual(new Uint8Array([1, 2, 3, 4]))
|
|
1241
|
+
expect(result?.fragmentCount).toBe(1)
|
|
1242
|
+
expect(result?.rapFlag).toBe(true)
|
|
1243
|
+
})
|
|
1244
|
+
|
|
1245
|
+
test('rejects a complete MFU without a valid sample identity', () => {
|
|
1246
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1247
|
+
const complete = (identity: Partial<MFUFragment>): MFUFragment => ({
|
|
1248
|
+
fragmentationIndicator: 0,
|
|
1249
|
+
mpuSequenceNumber: 1,
|
|
1250
|
+
fragmentCounter: 0,
|
|
1251
|
+
data: new Uint8Array([1]),
|
|
1252
|
+
timestamp: 1000n,
|
|
1253
|
+
rapFlag: true,
|
|
1254
|
+
...identity,
|
|
1255
|
+
})
|
|
1256
|
+
expectRejection(
|
|
1257
|
+
() => reassembler.addFragment(complete({})),
|
|
1258
|
+
'missing-data-unit-identity',
|
|
1259
|
+
)
|
|
1260
|
+
expectRejection(
|
|
1261
|
+
() => reassembler.addFragment(complete({
|
|
1262
|
+
movieFragmentSequenceNumber: 2,
|
|
1263
|
+
sampleNumber: 1,
|
|
1264
|
+
})),
|
|
1265
|
+
'missing-data-unit-identity',
|
|
1266
|
+
)
|
|
1267
|
+
})
|
|
1268
|
+
|
|
1269
|
+
test('accepts a complete subsample MFU with a nonzero authored offset', () => {
|
|
1270
|
+
// moqcast #37: a subsample DU's offset is its authored base byte
|
|
1271
|
+
// position within the sample -- nonzero identifies one subsample DU
|
|
1272
|
+
// among siblings partitioning an oversized sample.
|
|
1273
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1274
|
+
const result = reassembler.addFragment({
|
|
1275
|
+
fragmentationIndicator: 0,
|
|
1276
|
+
mpuSequenceNumber: 1,
|
|
1277
|
+
fragmentCounter: 0,
|
|
1278
|
+
movieFragmentSequenceNumber: 2,
|
|
1279
|
+
sampleNumber: 1,
|
|
1280
|
+
sampleOffset: 4,
|
|
1281
|
+
data: new Uint8Array([1]),
|
|
1282
|
+
timestamp: 1000n,
|
|
1283
|
+
rapFlag: true,
|
|
1284
|
+
})
|
|
1285
|
+
expect(result).not.toBeNull()
|
|
1286
|
+
expect(result?.sampleOffset).toBe(4)
|
|
1287
|
+
})
|
|
1288
|
+
|
|
1289
|
+
test('accepts a fragmented subsample DU whose first fragment has a nonzero authored offset', () => {
|
|
1290
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1291
|
+
|
|
1292
|
+
expect(reassembler.addFragment({
|
|
1293
|
+
fragmentationIndicator: 1,
|
|
1294
|
+
mpuSequenceNumber: 2,
|
|
1295
|
+
fragmentCounter: 1,
|
|
1296
|
+
movieFragmentSequenceNumber: 2,
|
|
1297
|
+
sampleNumber: 1,
|
|
1298
|
+
sampleOffset: 4,
|
|
1299
|
+
packetSequenceNumber: testPacketSequence(2, 1),
|
|
1300
|
+
data: new Uint8Array([0xa1]),
|
|
1301
|
+
timestamp: 1000n,
|
|
1302
|
+
rapFlag: true,
|
|
1303
|
+
})).toBeNull()
|
|
1304
|
+
expect(reassembler.pendingCount).toBe(1)
|
|
1305
|
+
expect(reassembler.getStatus().pendingFragments).toBe(1)
|
|
1306
|
+
|
|
1307
|
+
const out = reassembler.addFragment({
|
|
1308
|
+
fragmentationIndicator: 3,
|
|
1309
|
+
mpuSequenceNumber: 2,
|
|
1310
|
+
fragmentCounter: 0,
|
|
1311
|
+
data: new Uint8Array([0xa2]),
|
|
1312
|
+
timestamp: 1001n,
|
|
1313
|
+
rapFlag: false,
|
|
1314
|
+
packetSequenceNumber: testPacketSequence(2, 0),
|
|
1315
|
+
})
|
|
1316
|
+
expect(out?.data).toEqual(new Uint8Array([0xa1, 0xa2]))
|
|
1317
|
+
expect(out?.sampleOffset).toBe(4)
|
|
1318
|
+
})
|
|
1319
|
+
|
|
1320
|
+
test('sibling subsample DUs of one sample reassemble independently by offset', () => {
|
|
1321
|
+
// moqcast #37: each subsample DU of a partitioned sample runs its own
|
|
1322
|
+
// FI chain and gets its own buffer, keyed in part by its authored base
|
|
1323
|
+
// offset -- distinct from a sibling DU sharing (movie, sample).
|
|
1324
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1325
|
+
|
|
1326
|
+
expect(reassembler.addFragment({
|
|
1327
|
+
mpuSequenceNumber: 3,
|
|
1328
|
+
movieFragmentSequenceNumber: 5,
|
|
1329
|
+
sampleNumber: 9,
|
|
1330
|
+
fragmentationIndicator: 1,
|
|
1331
|
+
fragmentCounter: 1,
|
|
1332
|
+
sampleOffset: 0,
|
|
1333
|
+
data: new Uint8Array([0xa1]),
|
|
1334
|
+
timestamp: 2000n,
|
|
1335
|
+
rapFlag: true,
|
|
1336
|
+
packetSequenceNumber: testPacketSequence(3, 1, 0),
|
|
1337
|
+
})).toBeNull()
|
|
1338
|
+
expect(reassembler.addFragment({
|
|
1339
|
+
mpuSequenceNumber: 3,
|
|
1340
|
+
movieFragmentSequenceNumber: 5,
|
|
1341
|
+
sampleNumber: 9,
|
|
1342
|
+
fragmentationIndicator: 1,
|
|
1343
|
+
fragmentCounter: 1,
|
|
1344
|
+
sampleOffset: 3,
|
|
1345
|
+
data: new Uint8Array([0xb1]),
|
|
1346
|
+
timestamp: 2001n,
|
|
1347
|
+
rapFlag: false,
|
|
1348
|
+
packetSequenceNumber: testPacketSequence(3, 1, 1),
|
|
1349
|
+
})).toBeNull()
|
|
1350
|
+
expect(reassembler.pendingCount).toBe(2)
|
|
1351
|
+
|
|
1352
|
+
const first = reassembler.addFragment({
|
|
1353
|
+
mpuSequenceNumber: 3,
|
|
1354
|
+
fragmentationIndicator: 3,
|
|
1355
|
+
fragmentCounter: 0,
|
|
1356
|
+
data: new Uint8Array([0xa2]),
|
|
1357
|
+
timestamp: 2000n,
|
|
1358
|
+
rapFlag: false,
|
|
1359
|
+
packetSequenceNumber: testPacketSequence(3, 0, 0),
|
|
1360
|
+
})
|
|
1361
|
+
expect(first?.data).toEqual(new Uint8Array([0xa1, 0xa2]))
|
|
1362
|
+
expect(first?.sampleOffset).toBe(0)
|
|
1363
|
+
|
|
1364
|
+
const second = reassembler.addFragment({
|
|
1365
|
+
mpuSequenceNumber: 3,
|
|
1366
|
+
fragmentationIndicator: 3,
|
|
1367
|
+
fragmentCounter: 0,
|
|
1368
|
+
data: new Uint8Array([0xb2]),
|
|
1369
|
+
timestamp: 2001n,
|
|
1370
|
+
rapFlag: false,
|
|
1371
|
+
packetSequenceNumber: testPacketSequence(3, 0, 1),
|
|
1372
|
+
})
|
|
1373
|
+
expect(second?.data).toEqual(new Uint8Array([0xb1, 0xb2]))
|
|
1374
|
+
expect(second?.sampleOffset).toBe(3)
|
|
1375
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
1376
|
+
})
|
|
1377
|
+
|
|
1378
|
+
test('isolates colliding fragment coordinates by catalog track identity', () => {
|
|
1379
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1380
|
+
const fragment = (
|
|
1381
|
+
catalogTrackName: string,
|
|
1382
|
+
fragmentationIndicator: number,
|
|
1383
|
+
fragmentCounter: number,
|
|
1384
|
+
byte: number,
|
|
1385
|
+
): MFUFragment => ({
|
|
1386
|
+
itemId: 1,
|
|
1387
|
+
fragmentationIndicator,
|
|
1388
|
+
fragmentCounter,
|
|
1389
|
+
mpuSequenceNumber: 1,
|
|
1390
|
+
data: new Uint8Array([byte]),
|
|
1391
|
+
timestamp: 1000n,
|
|
1392
|
+
rapFlag: fragmentationIndicator === 1,
|
|
1393
|
+
catalogTrackName,
|
|
1394
|
+
packetSequenceNumber: testPacketSequence(1, fragmentCounter),
|
|
1395
|
+
})
|
|
1396
|
+
|
|
1397
|
+
expect(reassembler.addFragment(fragment('video/720p', 1, 1, 0x71))).toBeNull()
|
|
1398
|
+
expect(reassembler.addFragment(fragment('video/540p', 1, 1, 0x51))).toBeNull()
|
|
1399
|
+
const high = reassembler.addFragment(fragment('video/720p', 3, 0, 0x72))
|
|
1400
|
+
const low = reassembler.addFragment(fragment('video/540p', 3, 0, 0x52))
|
|
1401
|
+
|
|
1402
|
+
expect(high?.data).toEqual(new Uint8Array([0x71, 0x72]))
|
|
1403
|
+
expect(high?.catalogTrackName).toBe('video/720p')
|
|
1404
|
+
expect(low?.data).toEqual(new Uint8Array([0x51, 0x52]))
|
|
1405
|
+
expect(low?.catalogTrackName).toBe('video/540p')
|
|
1406
|
+
})
|
|
1407
|
+
|
|
1408
|
+
test('rejects an overlapping non-timed item without replacing the active run', () => {
|
|
1409
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1410
|
+
const fragment = (
|
|
1411
|
+
itemId: number,
|
|
1412
|
+
fragmentationIndicator: number,
|
|
1413
|
+
fragmentCounter: number,
|
|
1414
|
+
byte: number,
|
|
1415
|
+
): MFUFragment => ({
|
|
1416
|
+
itemId,
|
|
1417
|
+
fragmentationIndicator,
|
|
1418
|
+
fragmentCounter,
|
|
1419
|
+
mpuSequenceNumber: 1,
|
|
1420
|
+
data: new Uint8Array([byte]),
|
|
1421
|
+
timestamp: 1000n,
|
|
1422
|
+
rapFlag: false,
|
|
1423
|
+
packetSequenceNumber: testPacketSequence(1, fragmentCounter),
|
|
1424
|
+
})
|
|
1425
|
+
|
|
1426
|
+
expect(reassembler.addFragment(fragment(10, 1, 1, 0x10))).toBeNull()
|
|
1427
|
+
expectRejection(
|
|
1428
|
+
() => reassembler.addFragment(fragment(20, 1, 1, 0x20)),
|
|
1429
|
+
'overlapping-data-unit',
|
|
1430
|
+
)
|
|
1431
|
+
expect(reassembler.pendingCount).toBe(1)
|
|
1432
|
+
expect(reassembler.addFragment(fragment(10, 3, 0, 0x11))?.data)
|
|
1433
|
+
.toEqual(new Uint8Array([0x10, 0x11]))
|
|
1434
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
1435
|
+
})
|
|
1436
|
+
|
|
1437
|
+
test('associates continuations by authored state when packet timestamps change', () => {
|
|
1438
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1439
|
+
const fragment = (
|
|
1440
|
+
itemId: number | undefined,
|
|
1441
|
+
fragmentationIndicator: number,
|
|
1442
|
+
fragmentCounter: number,
|
|
1443
|
+
byte: number,
|
|
1444
|
+
timestamp: bigint,
|
|
1445
|
+
): MFUFragment => ({
|
|
1446
|
+
...(itemId === undefined ? {} : { itemId }),
|
|
1447
|
+
fragmentationIndicator,
|
|
1448
|
+
fragmentCounter,
|
|
1449
|
+
mpuSequenceNumber: 1,
|
|
1450
|
+
data: new Uint8Array([byte]),
|
|
1451
|
+
timestamp,
|
|
1452
|
+
rapFlag: false,
|
|
1453
|
+
packetSequenceNumber: testPacketSequence(1, fragmentCounter),
|
|
1454
|
+
})
|
|
1455
|
+
|
|
1456
|
+
expect(reassembler.addFragment(fragment(10, 1, 2, 0x10, 1000n))).toBeNull()
|
|
1457
|
+
expect(reassembler.addFragment(fragment(undefined, 2, 1, 0x11, 1001n))).toBeNull()
|
|
1458
|
+
const out = reassembler.addFragment(fragment(undefined, 3, 0, 0x12, 1002n))
|
|
1459
|
+
expect(out?.data).toEqual(new Uint8Array([0x10, 0x11, 0x12]))
|
|
1460
|
+
expect(out?.timestamp).toBe(1000n)
|
|
1461
|
+
})
|
|
1462
|
+
|
|
1463
|
+
test('should buffer and reassemble 2 fragments', () => {
|
|
1464
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1465
|
+
|
|
1466
|
+
// First fragment (FI=1)
|
|
1467
|
+
const result1 = reassembler.addFragment({
|
|
1468
|
+
fragmentationIndicator: 1,
|
|
1469
|
+
mpuSequenceNumber: 1,
|
|
1470
|
+
fragmentCounter: 1,
|
|
1471
|
+
itemId: 1,
|
|
1472
|
+
data: new Uint8Array([1, 2]),
|
|
1473
|
+
timestamp: 1000n,
|
|
1474
|
+
rapFlag: true,
|
|
1475
|
+
packetSequenceNumber: testPacketSequence(1, 1),
|
|
1476
|
+
})
|
|
1477
|
+
expect(result1).toBeNull()
|
|
1478
|
+
|
|
1479
|
+
// Last fragment (FI=3)
|
|
1480
|
+
const result2 = reassembler.addFragment({
|
|
1481
|
+
fragmentationIndicator: 3,
|
|
1482
|
+
mpuSequenceNumber: 1,
|
|
1483
|
+
fragmentCounter: 0,
|
|
1484
|
+
data: new Uint8Array([3, 4]),
|
|
1485
|
+
timestamp: 1000n,
|
|
1486
|
+
rapFlag: false,
|
|
1487
|
+
packetSequenceNumber: testPacketSequence(1, 0),
|
|
1488
|
+
})
|
|
1489
|
+
expect(result2).not.toBeNull()
|
|
1490
|
+
expect(result2?.data).toEqual(new Uint8Array([1, 2, 3, 4]))
|
|
1491
|
+
expect(result2?.fragmentCount).toBe(2)
|
|
1492
|
+
expect(result2?.rapFlag).toBe(true) // Should use first fragment's rapFlag
|
|
1493
|
+
})
|
|
1494
|
+
|
|
1495
|
+
test('buffers a terminal continuation until its authored first fragment arrives', () => {
|
|
1496
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1497
|
+
|
|
1498
|
+
// Last fragment first (FI=3)
|
|
1499
|
+
expect(reassembler.addFragment({
|
|
1500
|
+
fragmentationIndicator: 3,
|
|
1501
|
+
mpuSequenceNumber: 1,
|
|
1502
|
+
fragmentCounter: 0,
|
|
1503
|
+
data: new Uint8Array([3, 4]),
|
|
1504
|
+
timestamp: 1000n,
|
|
1505
|
+
rapFlag: false,
|
|
1506
|
+
packetSequenceNumber: testPacketSequence(1, 0),
|
|
1507
|
+
})).toBeNull()
|
|
1508
|
+
expect(reassembler.pendingCount).toBe(1)
|
|
1509
|
+
|
|
1510
|
+
// Then first fragment (FI=1)
|
|
1511
|
+
const completed = reassembler.addFragment({
|
|
1512
|
+
fragmentationIndicator: 1,
|
|
1513
|
+
mpuSequenceNumber: 1,
|
|
1514
|
+
fragmentCounter: 1,
|
|
1515
|
+
itemId: 1,
|
|
1516
|
+
data: new Uint8Array([1, 2]),
|
|
1517
|
+
timestamp: 1000n,
|
|
1518
|
+
rapFlag: true,
|
|
1519
|
+
packetSequenceNumber: testPacketSequence(1, 1),
|
|
1520
|
+
})
|
|
1521
|
+
|
|
1522
|
+
expect(completed?.data).toEqual(new Uint8Array([1, 2, 3, 4]))
|
|
1523
|
+
expect(completed?.rapFlag).toBe(true)
|
|
1524
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
1525
|
+
})
|
|
1526
|
+
|
|
1527
|
+
test('clock-free mode keeps an admitted MFU until completion', async () => {
|
|
1528
|
+
const reassembler = new MFUReassembler({ timeoutMs: null })
|
|
1529
|
+
|
|
1530
|
+
reassembler.addFragment({
|
|
1531
|
+
fragmentationIndicator: 1,
|
|
1532
|
+
mpuSequenceNumber: 1,
|
|
1533
|
+
fragmentCounter: 1,
|
|
1534
|
+
itemId: 1,
|
|
1535
|
+
data: new Uint8Array([1, 2]),
|
|
1536
|
+
timestamp: 1000n,
|
|
1537
|
+
rapFlag: true,
|
|
1538
|
+
packetSequenceNumber: testPacketSequence(1, 1),
|
|
1539
|
+
})
|
|
1540
|
+
|
|
1541
|
+
await new Promise(resolve => setTimeout(resolve, 5))
|
|
1542
|
+
|
|
1543
|
+
const result = reassembler.addFragment({
|
|
1544
|
+
fragmentationIndicator: 3,
|
|
1545
|
+
mpuSequenceNumber: 1,
|
|
1546
|
+
fragmentCounter: 0,
|
|
1547
|
+
data: new Uint8Array([3, 4]),
|
|
1548
|
+
timestamp: 1000n,
|
|
1549
|
+
rapFlag: false,
|
|
1550
|
+
packetSequenceNumber: testPacketSequence(1, 0),
|
|
1551
|
+
})
|
|
1552
|
+
|
|
1553
|
+
expect(result).not.toBeNull()
|
|
1554
|
+
expect(result?.data).toEqual(new Uint8Array([1, 2, 3, 4]))
|
|
1555
|
+
expect(reassembler.stats.timedOutMFUs).toBe(0)
|
|
1556
|
+
})
|
|
1557
|
+
|
|
1558
|
+
test('should track pending count', () => {
|
|
1559
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1560
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
1561
|
+
|
|
1562
|
+
reassembler.addFragment({
|
|
1563
|
+
fragmentationIndicator: 1,
|
|
1564
|
+
mpuSequenceNumber: 1,
|
|
1565
|
+
fragmentCounter: 1,
|
|
1566
|
+
itemId: 1,
|
|
1567
|
+
data: new Uint8Array([1]),
|
|
1568
|
+
timestamp: 1000n,
|
|
1569
|
+
rapFlag: false,
|
|
1570
|
+
packetSequenceNumber: testPacketSequence(1, 1),
|
|
1571
|
+
})
|
|
1572
|
+
expect(reassembler.pendingCount).toBe(1)
|
|
1573
|
+
|
|
1574
|
+
reassembler.flush()
|
|
1575
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
1576
|
+
})
|
|
1577
|
+
|
|
1578
|
+
test('should reassemble 3 fragments (FI=1, FI=2, FI=3)', () => {
|
|
1579
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1580
|
+
|
|
1581
|
+
// First fragment
|
|
1582
|
+
expect(reassembler.addFragment({
|
|
1583
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 10,
|
|
1584
|
+
fragmentCounter: 2, itemId: 1, data: new Uint8Array([0xAA, 0xBB]),
|
|
1585
|
+
timestamp: 5000n, rapFlag: true,
|
|
1586
|
+
packetSequenceNumber: testPacketSequence(10, 2),
|
|
1587
|
+
})).toBeNull()
|
|
1588
|
+
|
|
1589
|
+
// Middle fragment
|
|
1590
|
+
expect(reassembler.addFragment({
|
|
1591
|
+
fragmentationIndicator: 2, mpuSequenceNumber: 10,
|
|
1592
|
+
fragmentCounter: 1, data: new Uint8Array([0xCC, 0xDD]),
|
|
1593
|
+
timestamp: 5000n, rapFlag: false,
|
|
1594
|
+
packetSequenceNumber: testPacketSequence(10, 1),
|
|
1595
|
+
})).toBeNull()
|
|
1596
|
+
|
|
1597
|
+
// Last fragment — triggers reassembly
|
|
1598
|
+
const result = reassembler.addFragment({
|
|
1599
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 10,
|
|
1600
|
+
fragmentCounter: 0, data: new Uint8Array([0xEE, 0xFF]),
|
|
1601
|
+
timestamp: 5000n, rapFlag: false,
|
|
1602
|
+
packetSequenceNumber: testPacketSequence(10, 0),
|
|
1603
|
+
})
|
|
1604
|
+
|
|
1605
|
+
expect(result).not.toBeNull()
|
|
1606
|
+
expect(result!.data).toEqual(new Uint8Array([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]))
|
|
1607
|
+
expect(result!.fragmentCount).toBe(3)
|
|
1608
|
+
expect(result!.rapFlag).toBe(true) // from first fragment
|
|
1609
|
+
expect(result!.mpuSequenceNumber).toBe(10)
|
|
1610
|
+
})
|
|
1611
|
+
|
|
1612
|
+
test('should handle interleaved MPU sequences', () => {
|
|
1613
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1614
|
+
|
|
1615
|
+
// MPU 1: first fragment
|
|
1616
|
+
reassembler.addFragment({
|
|
1617
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 1,
|
|
1618
|
+
fragmentCounter: 1, itemId: 1, data: new Uint8Array([0x01]),
|
|
1619
|
+
timestamp: 100n, rapFlag: false,
|
|
1620
|
+
packetSequenceNumber: testPacketSequence(1, 1),
|
|
1621
|
+
})
|
|
1622
|
+
// MPU 2: complete (FI=0)
|
|
1623
|
+
const r2 = reassembler.addFragment({
|
|
1624
|
+
fragmentationIndicator: 0, mpuSequenceNumber: 2,
|
|
1625
|
+
fragmentCounter: 0, itemId: 2, data: new Uint8Array([0x02]),
|
|
1626
|
+
timestamp: 200n, rapFlag: false,
|
|
1627
|
+
})
|
|
1628
|
+
expect(r2).not.toBeNull()
|
|
1629
|
+
expect(r2!.mpuSequenceNumber).toBe(2)
|
|
1630
|
+
|
|
1631
|
+
// MPU 1: last fragment
|
|
1632
|
+
const r1 = reassembler.addFragment({
|
|
1633
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 1,
|
|
1634
|
+
fragmentCounter: 0, data: new Uint8Array([0x03]),
|
|
1635
|
+
timestamp: 100n, rapFlag: false,
|
|
1636
|
+
packetSequenceNumber: testPacketSequence(1, 0),
|
|
1637
|
+
})
|
|
1638
|
+
expect(r1).not.toBeNull()
|
|
1639
|
+
expect(r1!.data).toEqual(new Uint8Array([0x01, 0x03]))
|
|
1640
|
+
})
|
|
1641
|
+
|
|
1642
|
+
test('rejects overlapping fragmented MFUs sharing one stream and MPU', () => {
|
|
1643
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1644
|
+
const mpuSeq = 5
|
|
1645
|
+
|
|
1646
|
+
expect(
|
|
1647
|
+
reassembler.addFragment({
|
|
1648
|
+
fragmentationIndicator: 1, mpuSequenceNumber: mpuSeq,
|
|
1649
|
+
fragmentCounter: 1, itemId: 10,
|
|
1650
|
+
data: new Uint8Array([0xa1]), timestamp: 100n, rapFlag: true,
|
|
1651
|
+
packetSequenceNumber: testPacketSequence(mpuSeq, 1),
|
|
1652
|
+
}),
|
|
1653
|
+
).toBeNull()
|
|
1654
|
+
expectRejection(
|
|
1655
|
+
() => reassembler.addFragment({
|
|
1656
|
+
fragmentationIndicator: 1, mpuSequenceNumber: mpuSeq,
|
|
1657
|
+
fragmentCounter: 1, itemId: 20,
|
|
1658
|
+
data: new Uint8Array([0xb1]), timestamp: 200n, rapFlag: false,
|
|
1659
|
+
packetSequenceNumber: testPacketSequence(mpuSeq, 1),
|
|
1660
|
+
}),
|
|
1661
|
+
'overlapping-data-unit',
|
|
1662
|
+
)
|
|
1663
|
+
expect(reassembler.pendingCount).toBe(1)
|
|
1664
|
+
|
|
1665
|
+
const a = reassembler.addFragment({
|
|
1666
|
+
fragmentationIndicator: 3, mpuSequenceNumber: mpuSeq,
|
|
1667
|
+
fragmentCounter: 0, data: new Uint8Array([0xa2]), timestamp: 101n, rapFlag: false,
|
|
1668
|
+
packetSequenceNumber: testPacketSequence(mpuSeq, 0),
|
|
1669
|
+
})
|
|
1670
|
+
expect(a).not.toBeNull()
|
|
1671
|
+
expect(a!.data).toEqual(new Uint8Array([0xa1, 0xa2]))
|
|
1672
|
+
expect(a!.rapFlag).toBe(true) // from A's first fragment
|
|
1673
|
+
|
|
1674
|
+
expect(reassembler.addFragment({
|
|
1675
|
+
fragmentationIndicator: 1, mpuSequenceNumber: mpuSeq,
|
|
1676
|
+
fragmentCounter: 1, itemId: 20,
|
|
1677
|
+
data: new Uint8Array([0xb1]), timestamp: 200n, rapFlag: false,
|
|
1678
|
+
packetSequenceNumber: testPacketSequence(mpuSeq, 1, 10),
|
|
1679
|
+
})).toBeNull()
|
|
1680
|
+
const b = reassembler.addFragment({
|
|
1681
|
+
fragmentationIndicator: 3, mpuSequenceNumber: mpuSeq,
|
|
1682
|
+
fragmentCounter: 0, data: new Uint8Array([0xb2]), timestamp: 201n, rapFlag: false,
|
|
1683
|
+
packetSequenceNumber: testPacketSequence(mpuSeq, 0, 10),
|
|
1684
|
+
})
|
|
1685
|
+
expect(b).not.toBeNull()
|
|
1686
|
+
expect(b!.data).toEqual(new Uint8Array([0xb1, 0xb2]))
|
|
1687
|
+
})
|
|
1688
|
+
|
|
1689
|
+
test('reassembles FFmpeg timed MFU fragments by sample number and byte offset', () => {
|
|
1690
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1691
|
+
const common = {
|
|
1692
|
+
mpuSequenceNumber: 0,
|
|
1693
|
+
movieFragmentSequenceNumber: 9,
|
|
1694
|
+
sampleNumber: 17,
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
expect(reassembler.addFragment({
|
|
1698
|
+
...common,
|
|
1699
|
+
fragmentationIndicator: 1,
|
|
1700
|
+
fragmentCounter: 2,
|
|
1701
|
+
sampleOffset: 0,
|
|
1702
|
+
data: new Uint8Array([0xa1]),
|
|
1703
|
+
timestamp: 1000n,
|
|
1704
|
+
rapFlag: true,
|
|
1705
|
+
packetSequenceNumber: testPacketSequence(0, 2),
|
|
1706
|
+
})).toBeNull()
|
|
1707
|
+
expect(reassembler.addFragment({
|
|
1708
|
+
...common,
|
|
1709
|
+
fragmentationIndicator: 2,
|
|
1710
|
+
fragmentCounter: 1,
|
|
1711
|
+
sampleOffset: 1,
|
|
1712
|
+
data: new Uint8Array([0xb2]),
|
|
1713
|
+
timestamp: 1001n,
|
|
1714
|
+
rapFlag: false,
|
|
1715
|
+
packetSequenceNumber: testPacketSequence(0, 1),
|
|
1716
|
+
})).toBeNull()
|
|
1717
|
+
const out = reassembler.addFragment({
|
|
1718
|
+
...common,
|
|
1719
|
+
fragmentationIndicator: 3,
|
|
1720
|
+
fragmentCounter: 0,
|
|
1721
|
+
sampleOffset: 2,
|
|
1722
|
+
data: new Uint8Array([0xc3]),
|
|
1723
|
+
timestamp: 1002n,
|
|
1724
|
+
rapFlag: false,
|
|
1725
|
+
packetSequenceNumber: testPacketSequence(0, 0),
|
|
1726
|
+
})
|
|
1727
|
+
|
|
1728
|
+
expect(out).not.toBeNull()
|
|
1729
|
+
expect(out!.data).toEqual(new Uint8Array([0xa1, 0xb2, 0xc3]))
|
|
1730
|
+
expect(out!.timestamp).toBe(1000n)
|
|
1731
|
+
expect(out!.sampleNumber).toBe(17)
|
|
1732
|
+
expect(out!.movieFragmentSequenceNumber).toBe(9)
|
|
1733
|
+
})
|
|
1734
|
+
|
|
1735
|
+
test('buffers a counter gap without advancing the active data unit', () => {
|
|
1736
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1737
|
+
const common = {
|
|
1738
|
+
mpuSequenceNumber: 0,
|
|
1739
|
+
movieFragmentSequenceNumber: 9,
|
|
1740
|
+
sampleNumber: 17,
|
|
1741
|
+
timestamp: 1000n,
|
|
1742
|
+
rapFlag: true,
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
expect(reassembler.addFragment({
|
|
1746
|
+
...common,
|
|
1747
|
+
fragmentationIndicator: 1,
|
|
1748
|
+
fragmentCounter: 2,
|
|
1749
|
+
sampleOffset: 0,
|
|
1750
|
+
data: new Uint8Array([0xa1, 0xa2]),
|
|
1751
|
+
packetSequenceNumber: testPacketSequence(0, 2),
|
|
1752
|
+
})).toBeNull()
|
|
1753
|
+
expect(reassembler.addFragment({
|
|
1754
|
+
...common,
|
|
1755
|
+
fragmentationIndicator: 3,
|
|
1756
|
+
fragmentCounter: 0,
|
|
1757
|
+
sampleOffset: 4,
|
|
1758
|
+
data: new Uint8Array([0xc1, 0xc2]),
|
|
1759
|
+
packetSequenceNumber: testPacketSequence(0, 0),
|
|
1760
|
+
})).toBeNull()
|
|
1761
|
+
expect(reassembler.pendingCount).toBe(1)
|
|
1762
|
+
|
|
1763
|
+
const out = reassembler.addFragment({
|
|
1764
|
+
...common,
|
|
1765
|
+
fragmentationIndicator: 2,
|
|
1766
|
+
fragmentCounter: 1,
|
|
1767
|
+
sampleOffset: 2,
|
|
1768
|
+
data: new Uint8Array([0xb1, 0xb2]),
|
|
1769
|
+
packetSequenceNumber: testPacketSequence(0, 1),
|
|
1770
|
+
})
|
|
1771
|
+
expect(out?.data).toEqual(
|
|
1772
|
+
new Uint8Array([0xa1, 0xa2, 0xb1, 0xb2, 0xc1, 0xc2]),
|
|
1773
|
+
)
|
|
1774
|
+
})
|
|
1775
|
+
|
|
1776
|
+
test('reassembles first-only DU-header fragments across changing packet timestamps', () => {
|
|
1777
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1778
|
+
|
|
1779
|
+
expect(reassembler.addFragment({
|
|
1780
|
+
fragmentationIndicator: 1,
|
|
1781
|
+
mpuSequenceNumber: 44,
|
|
1782
|
+
fragmentCounter: 2,
|
|
1783
|
+
movieFragmentSequenceNumber: 9,
|
|
1784
|
+
sampleNumber: 17,
|
|
1785
|
+
sampleOffset: 0,
|
|
1786
|
+
data: new Uint8Array([0xa1]),
|
|
1787
|
+
timestamp: 12345n,
|
|
1788
|
+
rapFlag: true,
|
|
1789
|
+
packetSequenceNumber: testPacketSequence(44, 2),
|
|
1790
|
+
})).toBeNull()
|
|
1791
|
+
expect(reassembler.addFragment({
|
|
1792
|
+
fragmentationIndicator: 2,
|
|
1793
|
+
mpuSequenceNumber: 44,
|
|
1794
|
+
fragmentCounter: 1,
|
|
1795
|
+
data: new Uint8Array([0xb2]),
|
|
1796
|
+
timestamp: 12346n,
|
|
1797
|
+
rapFlag: false,
|
|
1798
|
+
packetSequenceNumber: testPacketSequence(44, 1),
|
|
1799
|
+
})).toBeNull()
|
|
1800
|
+
const out = reassembler.addFragment({
|
|
1801
|
+
fragmentationIndicator: 3,
|
|
1802
|
+
mpuSequenceNumber: 44,
|
|
1803
|
+
fragmentCounter: 0,
|
|
1804
|
+
data: new Uint8Array([0xc3]),
|
|
1805
|
+
timestamp: 12347n,
|
|
1806
|
+
rapFlag: false,
|
|
1807
|
+
packetSequenceNumber: testPacketSequence(44, 0),
|
|
1808
|
+
})
|
|
1809
|
+
|
|
1810
|
+
expect(out).not.toBeNull()
|
|
1811
|
+
expect(out!.data).toEqual(new Uint8Array([0xa1, 0xb2, 0xc3]))
|
|
1812
|
+
expect(out!.timestamp).toBe(12345n)
|
|
1813
|
+
expect(out!.sampleNumber).toBe(17)
|
|
1814
|
+
expect(out!.movieFragmentSequenceNumber).toBe(9)
|
|
1815
|
+
})
|
|
1816
|
+
|
|
1817
|
+
test('reassembles first-only DU-header fragments with an ISO countdown', () => {
|
|
1818
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1819
|
+
const timestamp = 12345n
|
|
1820
|
+
|
|
1821
|
+
expect(reassembler.addFragment({
|
|
1822
|
+
fragmentationIndicator: 1,
|
|
1823
|
+
mpuSequenceNumber: 44,
|
|
1824
|
+
fragmentCounter: 3,
|
|
1825
|
+
movieFragmentSequenceNumber: 9,
|
|
1826
|
+
sampleNumber: 17,
|
|
1827
|
+
sampleOffset: 0,
|
|
1828
|
+
data: new Uint8Array([0xa1]),
|
|
1829
|
+
timestamp,
|
|
1830
|
+
rapFlag: true,
|
|
1831
|
+
packetSequenceNumber: testPacketSequence(44, 3),
|
|
1832
|
+
})).toBeNull()
|
|
1833
|
+
expect(reassembler.addFragment({
|
|
1834
|
+
fragmentationIndicator: 2,
|
|
1835
|
+
mpuSequenceNumber: 44,
|
|
1836
|
+
fragmentCounter: 2,
|
|
1837
|
+
data: new Uint8Array([0xb2]),
|
|
1838
|
+
timestamp,
|
|
1839
|
+
rapFlag: false,
|
|
1840
|
+
packetSequenceNumber: testPacketSequence(44, 2),
|
|
1841
|
+
})).toBeNull()
|
|
1842
|
+
expect(reassembler.addFragment({
|
|
1843
|
+
fragmentationIndicator: 2,
|
|
1844
|
+
mpuSequenceNumber: 44,
|
|
1845
|
+
fragmentCounter: 1,
|
|
1846
|
+
data: new Uint8Array([0xc3]),
|
|
1847
|
+
timestamp,
|
|
1848
|
+
rapFlag: false,
|
|
1849
|
+
packetSequenceNumber: testPacketSequence(44, 1),
|
|
1850
|
+
})).toBeNull()
|
|
1851
|
+
const out = reassembler.addFragment({
|
|
1852
|
+
fragmentationIndicator: 3,
|
|
1853
|
+
mpuSequenceNumber: 44,
|
|
1854
|
+
fragmentCounter: 0,
|
|
1855
|
+
data: new Uint8Array([0xd4]),
|
|
1856
|
+
timestamp,
|
|
1857
|
+
rapFlag: false,
|
|
1858
|
+
packetSequenceNumber: testPacketSequence(44, 0),
|
|
1859
|
+
})
|
|
1860
|
+
|
|
1861
|
+
expect(out).not.toBeNull()
|
|
1862
|
+
expect(out!.data).toEqual(new Uint8Array([0xa1, 0xb2, 0xc3, 0xd4]))
|
|
1863
|
+
expect(out!.sampleNumber).toBe(17)
|
|
1864
|
+
expect(out!.movieFragmentSequenceNumber).toBe(9)
|
|
1865
|
+
})
|
|
1866
|
+
|
|
1867
|
+
test('rejects fragmented MFUs without an authored DU identity', () => {
|
|
1868
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1869
|
+
const timestamp = 22222n
|
|
1870
|
+
|
|
1871
|
+
expectRejection(() => reassembler.addFragment({
|
|
1872
|
+
fragmentationIndicator: 1,
|
|
1873
|
+
mpuSequenceNumber: 55,
|
|
1874
|
+
fragmentCounter: 2,
|
|
1875
|
+
data: new Uint8Array([0xa1]),
|
|
1876
|
+
timestamp,
|
|
1877
|
+
rapFlag: true,
|
|
1878
|
+
packetSequenceNumber: testPacketSequence(55, 2),
|
|
1879
|
+
}), 'missing-data-unit-identity')
|
|
1880
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
1881
|
+
})
|
|
1882
|
+
|
|
1883
|
+
test('rejects fragmented MFUs without an authored packet sequence', () => {
|
|
1884
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1885
|
+
expectRejection(() => reassembler.addFragment({
|
|
1886
|
+
fragmentationIndicator: 1,
|
|
1887
|
+
mpuSequenceNumber: 55,
|
|
1888
|
+
fragmentCounter: 1,
|
|
1889
|
+
itemId: 1,
|
|
1890
|
+
data: new Uint8Array([0xa1]),
|
|
1891
|
+
timestamp: 22222n,
|
|
1892
|
+
rapFlag: true,
|
|
1893
|
+
}), 'missing-packet-sequence')
|
|
1894
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
1895
|
+
})
|
|
1896
|
+
|
|
1897
|
+
test('rejects fragmented MFUs whose authored packet sequence is not u32', () => {
|
|
1898
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1899
|
+
for (const packetSequenceNumber of [-1, 0x1_0000_0000, 1.5, Number.NaN]) {
|
|
1900
|
+
expectRejection(() => reassembler.addFragment({
|
|
1901
|
+
fragmentationIndicator: 1,
|
|
1902
|
+
mpuSequenceNumber: 55,
|
|
1903
|
+
fragmentCounter: 1,
|
|
1904
|
+
itemId: 1,
|
|
1905
|
+
data: new Uint8Array([0xa1]),
|
|
1906
|
+
timestamp: 22222n,
|
|
1907
|
+
rapFlag: true,
|
|
1908
|
+
packetSequenceNumber,
|
|
1909
|
+
}), 'missing-packet-sequence')
|
|
1910
|
+
}
|
|
1911
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
1912
|
+
})
|
|
1913
|
+
|
|
1914
|
+
test('fails closed when one fragmented DU has interleaved packet sequence gaps', () => {
|
|
1915
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1916
|
+
const timestamp = 66666n
|
|
1917
|
+
|
|
1918
|
+
expect(reassembler.addFragment({
|
|
1919
|
+
fragmentationIndicator: 1,
|
|
1920
|
+
mpuSequenceNumber: 80,
|
|
1921
|
+
fragmentCounter: 17,
|
|
1922
|
+
movieFragmentSequenceNumber: 241,
|
|
1923
|
+
sampleNumber: 1,
|
|
1924
|
+
sampleOffset: 0,
|
|
1925
|
+
packetSequenceNumber: 1000,
|
|
1926
|
+
data: new Uint8Array([0xa0]),
|
|
1927
|
+
timestamp,
|
|
1928
|
+
rapFlag: true,
|
|
1929
|
+
})).toBeNull()
|
|
1930
|
+
|
|
1931
|
+
for (let i = 1; i < 17; i++) {
|
|
1932
|
+
expect(reassembler.addFragment({
|
|
1933
|
+
fragmentationIndicator: 2,
|
|
1934
|
+
mpuSequenceNumber: 80,
|
|
1935
|
+
fragmentCounter: 17 - i,
|
|
1936
|
+
packetSequenceNumber: 1000 + i * 2,
|
|
1937
|
+
data: new Uint8Array([0xa0 + i]),
|
|
1938
|
+
timestamp,
|
|
1939
|
+
rapFlag: false,
|
|
1940
|
+
})).toBeNull()
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
const out = reassembler.addFragment({
|
|
1944
|
+
fragmentationIndicator: 3,
|
|
1945
|
+
mpuSequenceNumber: 80,
|
|
1946
|
+
fragmentCounter: 0,
|
|
1947
|
+
packetSequenceNumber: 1040,
|
|
1948
|
+
data: new Uint8Array([0xb1]),
|
|
1949
|
+
timestamp,
|
|
1950
|
+
rapFlag: false,
|
|
1951
|
+
})
|
|
1952
|
+
|
|
1953
|
+
expect(out).toBeNull()
|
|
1954
|
+
expect(reassembler.pendingCount).toBe(18)
|
|
1955
|
+
expect(reassembler.stats.reassembledMFUs).toBe(0)
|
|
1956
|
+
})
|
|
1957
|
+
|
|
1958
|
+
test('does not combine adjacent reordered MFUs in one MPU', () => {
|
|
1959
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
1960
|
+
const common = { mpuSequenceNumber: 80, packetId: 7, timestamp: 66666n }
|
|
1961
|
+
|
|
1962
|
+
expect(reassembler.addFragment({
|
|
1963
|
+
...common,
|
|
1964
|
+
fragmentationIndicator: 3,
|
|
1965
|
+
fragmentCounter: 0,
|
|
1966
|
+
packetSequenceNumber: 103,
|
|
1967
|
+
data: new Uint8Array([0xb2]),
|
|
1968
|
+
rapFlag: false,
|
|
1969
|
+
})).toBeNull()
|
|
1970
|
+
expect(reassembler.addFragment({
|
|
1971
|
+
...common,
|
|
1972
|
+
fragmentationIndicator: 1,
|
|
1973
|
+
fragmentCounter: 1,
|
|
1974
|
+
packetSequenceNumber: 100,
|
|
1975
|
+
movieFragmentSequenceNumber: 241,
|
|
1976
|
+
sampleNumber: 1,
|
|
1977
|
+
sampleOffset: 0,
|
|
1978
|
+
data: new Uint8Array([0xa1]),
|
|
1979
|
+
rapFlag: true,
|
|
1980
|
+
})).toBeNull()
|
|
1981
|
+
|
|
1982
|
+
const earlier = reassembler.addFragment({
|
|
1983
|
+
...common,
|
|
1984
|
+
fragmentationIndicator: 3,
|
|
1985
|
+
fragmentCounter: 0,
|
|
1986
|
+
packetSequenceNumber: 101,
|
|
1987
|
+
data: new Uint8Array([0xa2]),
|
|
1988
|
+
rapFlag: false,
|
|
1989
|
+
})
|
|
1990
|
+
expect(earlier?.data).toEqual(new Uint8Array([0xa1, 0xa2]))
|
|
1991
|
+
|
|
1992
|
+
const later = reassembler.addFragment({
|
|
1993
|
+
...common,
|
|
1994
|
+
fragmentationIndicator: 1,
|
|
1995
|
+
fragmentCounter: 1,
|
|
1996
|
+
packetSequenceNumber: 102,
|
|
1997
|
+
movieFragmentSequenceNumber: 241,
|
|
1998
|
+
sampleNumber: 2,
|
|
1999
|
+
sampleOffset: 0,
|
|
2000
|
+
data: new Uint8Array([0xb1]),
|
|
2001
|
+
rapFlag: false,
|
|
2002
|
+
})
|
|
2003
|
+
expect(later?.data).toEqual(new Uint8Array([0xb1, 0xb2]))
|
|
2004
|
+
})
|
|
2005
|
+
|
|
2006
|
+
test('wraps the fragmented run coordinate at u32 packet sequence', () => {
|
|
2007
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2008
|
+
expect(reassembler.addFragment({
|
|
2009
|
+
fragmentationIndicator: 1,
|
|
2010
|
+
mpuSequenceNumber: 81,
|
|
2011
|
+
fragmentCounter: 1,
|
|
2012
|
+
packetSequenceNumber: 0xffffffff,
|
|
2013
|
+
itemId: 1,
|
|
2014
|
+
data: new Uint8Array([0xa1]),
|
|
2015
|
+
timestamp: 66666n,
|
|
2016
|
+
rapFlag: true,
|
|
2017
|
+
})).toBeNull()
|
|
2018
|
+
expect(reassembler.addFragment({
|
|
2019
|
+
fragmentationIndicator: 3,
|
|
2020
|
+
mpuSequenceNumber: 81,
|
|
2021
|
+
fragmentCounter: 0,
|
|
2022
|
+
packetSequenceNumber: 0,
|
|
2023
|
+
data: new Uint8Array([0xa2]),
|
|
2024
|
+
timestamp: 66667n,
|
|
2025
|
+
rapFlag: false,
|
|
2026
|
+
})?.data).toEqual(new Uint8Array([0xa1, 0xa2]))
|
|
2027
|
+
})
|
|
2028
|
+
|
|
2029
|
+
test('dedupes recovered copies by ISO fragment-counter position', () => {
|
|
2030
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2031
|
+
const timestamp = 44444n
|
|
2032
|
+
|
|
2033
|
+
expect(reassembler.addFragment({
|
|
2034
|
+
fragmentationIndicator: 1,
|
|
2035
|
+
mpuSequenceNumber: 77,
|
|
2036
|
+
fragmentCounter: 2,
|
|
2037
|
+
movieFragmentSequenceNumber: 12,
|
|
2038
|
+
sampleNumber: 1,
|
|
2039
|
+
sampleOffset: 0,
|
|
2040
|
+
packetSequenceNumber: 2000,
|
|
2041
|
+
data: new Uint8Array([0xa1]),
|
|
2042
|
+
timestamp,
|
|
2043
|
+
rapFlag: true,
|
|
2044
|
+
})).toBeNull()
|
|
2045
|
+
expect(reassembler.addFragment({
|
|
2046
|
+
fragmentationIndicator: 2,
|
|
2047
|
+
mpuSequenceNumber: 77,
|
|
2048
|
+
fragmentCounter: 1,
|
|
2049
|
+
packetSequenceNumber: 2001,
|
|
2050
|
+
data: new Uint8Array([0xb2]),
|
|
2051
|
+
timestamp,
|
|
2052
|
+
rapFlag: false,
|
|
2053
|
+
})).toBeNull()
|
|
2054
|
+
expect(reassembler.addFragment({
|
|
2055
|
+
fragmentationIndicator: 3,
|
|
2056
|
+
mpuSequenceNumber: 77,
|
|
2057
|
+
fragmentCounter: 0,
|
|
2058
|
+
packetSequenceNumber: 2002,
|
|
2059
|
+
data: new Uint8Array([0xc3]),
|
|
2060
|
+
timestamp,
|
|
2061
|
+
rapFlag: false,
|
|
2062
|
+
})).not.toBeNull()
|
|
2063
|
+
|
|
2064
|
+
reassembler.flush()
|
|
2065
|
+
|
|
2066
|
+
expect(reassembler.addFragment({
|
|
2067
|
+
fragmentationIndicator: 1,
|
|
2068
|
+
mpuSequenceNumber: 78,
|
|
2069
|
+
fragmentCounter: 2,
|
|
2070
|
+
movieFragmentSequenceNumber: 13,
|
|
2071
|
+
sampleNumber: 1,
|
|
2072
|
+
sampleOffset: 0,
|
|
2073
|
+
packetSequenceNumber: 3000,
|
|
2074
|
+
data: new Uint8Array([0xd4]),
|
|
2075
|
+
timestamp,
|
|
2076
|
+
rapFlag: true,
|
|
2077
|
+
})).toBeNull()
|
|
2078
|
+
expect(reassembler.addFragment({
|
|
2079
|
+
fragmentationIndicator: 2,
|
|
2080
|
+
mpuSequenceNumber: 78,
|
|
2081
|
+
fragmentCounter: 1,
|
|
2082
|
+
packetSequenceNumber: 3001,
|
|
2083
|
+
data: new Uint8Array([0xe5]),
|
|
2084
|
+
timestamp,
|
|
2085
|
+
rapFlag: false,
|
|
2086
|
+
})).toBeNull()
|
|
2087
|
+
expectRejection(() => reassembler.addFragment({
|
|
2088
|
+
fragmentationIndicator: 2,
|
|
2089
|
+
mpuSequenceNumber: 78,
|
|
2090
|
+
fragmentCounter: 1,
|
|
2091
|
+
packetSequenceNumber: 3001,
|
|
2092
|
+
data: new Uint8Array([0xe5]),
|
|
2093
|
+
timestamp,
|
|
2094
|
+
rapFlag: false,
|
|
2095
|
+
}), 'fragment-counter-mismatch')
|
|
2096
|
+
const out = reassembler.addFragment({
|
|
2097
|
+
fragmentationIndicator: 3,
|
|
2098
|
+
mpuSequenceNumber: 78,
|
|
2099
|
+
fragmentCounter: 0,
|
|
2100
|
+
packetSequenceNumber: 3002,
|
|
2101
|
+
data: new Uint8Array([0xf6]),
|
|
2102
|
+
timestamp,
|
|
2103
|
+
rapFlag: false,
|
|
2104
|
+
})
|
|
2105
|
+
|
|
2106
|
+
expect(out).not.toBeNull()
|
|
2107
|
+
expect(out!.data).toEqual(new Uint8Array([0xd4, 0xe5, 0xf6]))
|
|
2108
|
+
})
|
|
2109
|
+
|
|
2110
|
+
test('reassembles middle and terminal continuations that arrive before first', () => {
|
|
2111
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2112
|
+
const timestamp = 54321n
|
|
2113
|
+
|
|
2114
|
+
expect(reassembler.addFragment({
|
|
2115
|
+
fragmentationIndicator: 2,
|
|
2116
|
+
mpuSequenceNumber: 45,
|
|
2117
|
+
fragmentCounter: 1,
|
|
2118
|
+
data: new Uint8Array([0xb2]),
|
|
2119
|
+
timestamp,
|
|
2120
|
+
rapFlag: false,
|
|
2121
|
+
packetSequenceNumber: testPacketSequence(45, 1),
|
|
2122
|
+
})).toBeNull()
|
|
2123
|
+
expect(reassembler.addFragment({
|
|
2124
|
+
fragmentationIndicator: 3,
|
|
2125
|
+
mpuSequenceNumber: 45,
|
|
2126
|
+
fragmentCounter: 0,
|
|
2127
|
+
data: new Uint8Array([0xc3]),
|
|
2128
|
+
timestamp: timestamp + 1n,
|
|
2129
|
+
rapFlag: false,
|
|
2130
|
+
packetSequenceNumber: testPacketSequence(45, 0),
|
|
2131
|
+
})).toBeNull()
|
|
2132
|
+
expect(reassembler.pendingCount).toBe(1)
|
|
2133
|
+
const out = reassembler.addFragment({
|
|
2134
|
+
fragmentationIndicator: 1,
|
|
2135
|
+
mpuSequenceNumber: 45,
|
|
2136
|
+
fragmentCounter: 2,
|
|
2137
|
+
movieFragmentSequenceNumber: 10,
|
|
2138
|
+
sampleNumber: 18,
|
|
2139
|
+
sampleOffset: 0,
|
|
2140
|
+
data: new Uint8Array([0xa1]),
|
|
2141
|
+
timestamp,
|
|
2142
|
+
rapFlag: true,
|
|
2143
|
+
packetSequenceNumber: testPacketSequence(45, 2),
|
|
2144
|
+
})
|
|
2145
|
+
|
|
2146
|
+
expect(out).not.toBeNull()
|
|
2147
|
+
expect(out!.data).toEqual(new Uint8Array([0xa1, 0xb2, 0xc3]))
|
|
2148
|
+
expect(out!.sampleNumber).toBe(18)
|
|
2149
|
+
expect(out!.movieFragmentSequenceNumber).toBe(10)
|
|
2150
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
2151
|
+
})
|
|
2152
|
+
|
|
2153
|
+
test('rejects a duplicate continuation before first without replacing retained media', () => {
|
|
2154
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2155
|
+
const timestamp = 54322n
|
|
2156
|
+
|
|
2157
|
+
expect(reassembler.addFragment({
|
|
2158
|
+
fragmentationIndicator: 3,
|
|
2159
|
+
mpuSequenceNumber: 46,
|
|
2160
|
+
fragmentCounter: 0,
|
|
2161
|
+
data: new Uint8Array([0xc3]),
|
|
2162
|
+
timestamp,
|
|
2163
|
+
rapFlag: false,
|
|
2164
|
+
packetSequenceNumber: testPacketSequence(46, 0),
|
|
2165
|
+
})).toBeNull()
|
|
2166
|
+
expectRejection(() => reassembler.addFragment({
|
|
2167
|
+
fragmentationIndicator: 3,
|
|
2168
|
+
mpuSequenceNumber: 46,
|
|
2169
|
+
fragmentCounter: 0,
|
|
2170
|
+
data: new Uint8Array([0xff]),
|
|
2171
|
+
timestamp: timestamp + 1n,
|
|
2172
|
+
rapFlag: false,
|
|
2173
|
+
packetSequenceNumber: testPacketSequence(46, 0),
|
|
2174
|
+
}), 'fragment-counter-mismatch')
|
|
2175
|
+
expect(reassembler.addFragment({
|
|
2176
|
+
fragmentationIndicator: 2,
|
|
2177
|
+
mpuSequenceNumber: 46,
|
|
2178
|
+
fragmentCounter: 1,
|
|
2179
|
+
data: new Uint8Array([0xb2]),
|
|
2180
|
+
timestamp: timestamp + 2n,
|
|
2181
|
+
rapFlag: false,
|
|
2182
|
+
packetSequenceNumber: testPacketSequence(46, 1),
|
|
2183
|
+
})).toBeNull()
|
|
2184
|
+
|
|
2185
|
+
const out = reassembler.addFragment({
|
|
2186
|
+
fragmentationIndicator: 1,
|
|
2187
|
+
mpuSequenceNumber: 46,
|
|
2188
|
+
fragmentCounter: 2,
|
|
2189
|
+
movieFragmentSequenceNumber: 10,
|
|
2190
|
+
sampleNumber: 19,
|
|
2191
|
+
sampleOffset: 0,
|
|
2192
|
+
data: new Uint8Array([0xa1]),
|
|
2193
|
+
timestamp: timestamp + 3n,
|
|
2194
|
+
rapFlag: true,
|
|
2195
|
+
packetSequenceNumber: testPacketSequence(46, 2),
|
|
2196
|
+
})
|
|
2197
|
+
|
|
2198
|
+
expect(out).not.toBeNull()
|
|
2199
|
+
expect(out!.data).toEqual(new Uint8Array([0xa1, 0xb2, 0xc3]))
|
|
2200
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
2201
|
+
})
|
|
2202
|
+
|
|
2203
|
+
test('rejects invalid ISO fragmentation-indicator and counter combinations', () => {
|
|
2204
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2205
|
+
const invalidHeaders = [
|
|
2206
|
+
{ fragmentationIndicator: 0, fragmentCounter: 1 },
|
|
2207
|
+
{ fragmentationIndicator: 3, fragmentCounter: 1 },
|
|
2208
|
+
{ fragmentationIndicator: 1, fragmentCounter: 0 },
|
|
2209
|
+
{ fragmentationIndicator: 2, fragmentCounter: 0 },
|
|
2210
|
+
{ fragmentationIndicator: -1, fragmentCounter: 1 },
|
|
2211
|
+
{ fragmentationIndicator: 4, fragmentCounter: 1 },
|
|
2212
|
+
{ fragmentationIndicator: 1.5, fragmentCounter: 1 },
|
|
2213
|
+
{ fragmentationIndicator: 1, fragmentCounter: -1 },
|
|
2214
|
+
{ fragmentationIndicator: 1, fragmentCounter: 256 },
|
|
2215
|
+
{ fragmentationIndicator: 1, fragmentCounter: 1.5 },
|
|
2216
|
+
{ fragmentationIndicator: 1, fragmentCounter: Number.NaN },
|
|
2217
|
+
]
|
|
2218
|
+
|
|
2219
|
+
for (const header of invalidHeaders) {
|
|
2220
|
+
expectRejection(() => reassembler.addFragment({
|
|
2221
|
+
...header,
|
|
2222
|
+
mpuSequenceNumber: 48,
|
|
2223
|
+
data: new Uint8Array([0xa1]),
|
|
2224
|
+
timestamp: 54324n,
|
|
2225
|
+
rapFlag: false,
|
|
2226
|
+
}), 'invalid-fragment-state')
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
2230
|
+
expect(reassembler.stats.reassembledMFUs).toBe(0)
|
|
2231
|
+
})
|
|
2232
|
+
|
|
2233
|
+
test('reassembles the maximum ISO chain of 256 payloads', () => {
|
|
2234
|
+
const reassembler = new MFUReassembler({ timeoutMs: null })
|
|
2235
|
+
const common = {
|
|
2236
|
+
mpuSequenceNumber: 47,
|
|
2237
|
+
timestamp: 54323n,
|
|
2238
|
+
rapFlag: false,
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
expect(reassembler.addFragment({
|
|
2242
|
+
...common,
|
|
2243
|
+
fragmentationIndicator: 1,
|
|
2244
|
+
fragmentCounter: 255,
|
|
2245
|
+
itemId: 1,
|
|
2246
|
+
data: new Uint8Array([0]),
|
|
2247
|
+
rapFlag: true,
|
|
2248
|
+
packetSequenceNumber: testPacketSequence(47, 255),
|
|
2249
|
+
})).toBeNull()
|
|
2250
|
+
for (let counter = 254; counter > 0; counter--) {
|
|
2251
|
+
expect(reassembler.addFragment({
|
|
2252
|
+
...common,
|
|
2253
|
+
fragmentationIndicator: 2,
|
|
2254
|
+
fragmentCounter: counter,
|
|
2255
|
+
data: new Uint8Array([255 - counter]),
|
|
2256
|
+
packetSequenceNumber: testPacketSequence(47, counter),
|
|
2257
|
+
})).toBeNull()
|
|
2258
|
+
}
|
|
2259
|
+
const out = reassembler.addFragment({
|
|
2260
|
+
...common,
|
|
2261
|
+
fragmentationIndicator: 3,
|
|
2262
|
+
fragmentCounter: 0,
|
|
2263
|
+
data: new Uint8Array([255]),
|
|
2264
|
+
packetSequenceNumber: testPacketSequence(47, 0),
|
|
2265
|
+
})
|
|
2266
|
+
|
|
2267
|
+
expect(out?.fragmentCount).toBe(256)
|
|
2268
|
+
expect(out?.data).toEqual(Uint8Array.from({ length: 256 }, (_, index) => index))
|
|
2269
|
+
})
|
|
2270
|
+
|
|
2271
|
+
test('should not reassemble without last fragment (FI=3)', () => {
|
|
2272
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2273
|
+
reassembler.addFragment({
|
|
2274
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 1,
|
|
2275
|
+
fragmentCounter: 2, itemId: 1, data: new Uint8Array([1]),
|
|
2276
|
+
timestamp: 1000n, rapFlag: false,
|
|
2277
|
+
packetSequenceNumber: testPacketSequence(1, 2),
|
|
2278
|
+
})
|
|
2279
|
+
const result = reassembler.addFragment({
|
|
2280
|
+
fragmentationIndicator: 2, mpuSequenceNumber: 1,
|
|
2281
|
+
fragmentCounter: 1, data: new Uint8Array([2]),
|
|
2282
|
+
timestamp: 1000n, rapFlag: false,
|
|
2283
|
+
packetSequenceNumber: testPacketSequence(1, 1),
|
|
2284
|
+
})
|
|
2285
|
+
expect(result).toBeNull()
|
|
2286
|
+
expect(reassembler.pendingCount).toBe(1)
|
|
2287
|
+
})
|
|
2288
|
+
|
|
2289
|
+
test('bounds continuations that arrive before their first fragments', () => {
|
|
2290
|
+
const reassembler = new MFUReassembler({ timeoutMs: null, maxBuffered: 1 })
|
|
2291
|
+
expect(reassembler.addFragment({
|
|
2292
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 1,
|
|
2293
|
+
fragmentCounter: 0, data: new Uint8Array([1]),
|
|
2294
|
+
timestamp: 1000n, rapFlag: false,
|
|
2295
|
+
packetSequenceNumber: testPacketSequence(1, 0),
|
|
2296
|
+
})).toBeNull()
|
|
2297
|
+
expectRejection(() => reassembler.addFragment({
|
|
2298
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 2,
|
|
2299
|
+
fragmentCounter: 0, data: new Uint8Array([2]),
|
|
2300
|
+
timestamp: 1000n, rapFlag: false,
|
|
2301
|
+
packetSequenceNumber: testPacketSequence(2, 0),
|
|
2302
|
+
}), 'active-data-unit-limit')
|
|
2303
|
+
expectRejection(() => reassembler.addFragment({
|
|
2304
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 2,
|
|
2305
|
+
fragmentCounter: 1, itemId: 2, data: new Uint8Array([2]),
|
|
2306
|
+
timestamp: 1000n, rapFlag: true,
|
|
2307
|
+
packetSequenceNumber: testPacketSequence(2, 1),
|
|
2308
|
+
}), 'active-data-unit-limit')
|
|
2309
|
+
expect(reassembler.pendingCount).toBe(1)
|
|
2310
|
+
expect(reassembler.getStatus().pendingFragments).toBe(1)
|
|
2311
|
+
|
|
2312
|
+
const result = reassembler.addFragment({
|
|
2313
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 1,
|
|
2314
|
+
fragmentCounter: 1, itemId: 1, data: new Uint8Array([3]),
|
|
2315
|
+
timestamp: 1000n, rapFlag: true,
|
|
2316
|
+
packetSequenceNumber: testPacketSequence(1, 1),
|
|
2317
|
+
})
|
|
2318
|
+
expect(result?.data).toEqual(new Uint8Array([3, 1]))
|
|
2319
|
+
})
|
|
2320
|
+
|
|
2321
|
+
test('expires a continuation that never receives its first fragment', () => {
|
|
2322
|
+
vi.useFakeTimers()
|
|
2323
|
+
try {
|
|
2324
|
+
const reassembler = new MFUReassembler({ timeoutMs: 50, maxBuffered: 1 })
|
|
2325
|
+
expect(reassembler.addFragment({
|
|
2326
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 1,
|
|
2327
|
+
fragmentCounter: 0, data: new Uint8Array([1]),
|
|
2328
|
+
timestamp: 1000n, rapFlag: false,
|
|
2329
|
+
packetSequenceNumber: testPacketSequence(1, 0),
|
|
2330
|
+
})).toBeNull()
|
|
2331
|
+
vi.advanceTimersByTime(50)
|
|
2332
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
2333
|
+
expect(reassembler.stats.timedOutMFUs).toBe(1)
|
|
2334
|
+
} finally {
|
|
2335
|
+
vi.useRealTimers()
|
|
2336
|
+
}
|
|
2337
|
+
})
|
|
2338
|
+
|
|
2339
|
+
test('retimes retained continuations when the timeout policy changes', () => {
|
|
2340
|
+
vi.useFakeTimers()
|
|
2341
|
+
try {
|
|
2342
|
+
const enabled = new MFUReassembler({ timeoutMs: null, maxBuffered: 1 })
|
|
2343
|
+
expect(enabled.addFragment({
|
|
2344
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 1,
|
|
2345
|
+
fragmentCounter: 0, data: new Uint8Array([1]),
|
|
2346
|
+
timestamp: 1000n, rapFlag: false,
|
|
2347
|
+
packetSequenceNumber: testPacketSequence(1, 0),
|
|
2348
|
+
})).toBeNull()
|
|
2349
|
+
enabled.timeoutMs = 10
|
|
2350
|
+
vi.advanceTimersByTime(10)
|
|
2351
|
+
expect(enabled.pendingCount).toBe(0)
|
|
2352
|
+
|
|
2353
|
+
const disabled = new MFUReassembler({ timeoutMs: 10, maxBuffered: 1 })
|
|
2354
|
+
expect(disabled.addFragment({
|
|
2355
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 2,
|
|
2356
|
+
fragmentCounter: 0, data: new Uint8Array([2]),
|
|
2357
|
+
timestamp: 1000n, rapFlag: false,
|
|
2358
|
+
packetSequenceNumber: testPacketSequence(2, 0),
|
|
2359
|
+
})).toBeNull()
|
|
2360
|
+
disabled.timeoutMs = null
|
|
2361
|
+
vi.advanceTimersByTime(10)
|
|
2362
|
+
expect(disabled.pendingCount).toBe(1)
|
|
2363
|
+
disabled.flush()
|
|
2364
|
+
} finally {
|
|
2365
|
+
vi.useRealTimers()
|
|
2366
|
+
}
|
|
2367
|
+
})
|
|
2368
|
+
|
|
2369
|
+
test('bounds active data units and rejects a new first at maxBuffered', () => {
|
|
2370
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000, maxBuffered: 2 })
|
|
2371
|
+
|
|
2372
|
+
// Two runs remain active; the third first fragment is rejected.
|
|
2373
|
+
for (let i = 0; i < 2; i++) {
|
|
2374
|
+
reassembler.addFragment({
|
|
2375
|
+
fragmentationIndicator: 1, mpuSequenceNumber: i,
|
|
2376
|
+
fragmentCounter: 1, itemId: i, data: new Uint8Array([i]),
|
|
2377
|
+
timestamp: BigInt(i * 100), rapFlag: false,
|
|
2378
|
+
packetSequenceNumber: testPacketSequence(i, 1),
|
|
2379
|
+
})
|
|
2380
|
+
}
|
|
2381
|
+
expectRejection(() => reassembler.addFragment({
|
|
2382
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 2,
|
|
2383
|
+
fragmentCounter: 1, itemId: 2, data: new Uint8Array([2]),
|
|
2384
|
+
timestamp: 200n, rapFlag: false,
|
|
2385
|
+
packetSequenceNumber: testPacketSequence(2, 1),
|
|
2386
|
+
}), 'active-data-unit-limit')
|
|
2387
|
+
expect(reassembler.pendingCount).toBe(2)
|
|
2388
|
+
expect(reassembler.stats.timedOutMFUs).toBe(0)
|
|
2389
|
+
expect(reassembler.stats.evictedMemoryLimit).toBe(0)
|
|
2390
|
+
})
|
|
2391
|
+
|
|
2392
|
+
test('should report stats correctly', () => {
|
|
2393
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2394
|
+
|
|
2395
|
+
// One complete MFU
|
|
2396
|
+
reassembler.addFragment({
|
|
2397
|
+
fragmentationIndicator: 0, mpuSequenceNumber: 1,
|
|
2398
|
+
fragmentCounter: 0, itemId: 1, data: new Uint8Array([1]),
|
|
2399
|
+
timestamp: 1000n, rapFlag: false,
|
|
2400
|
+
})
|
|
2401
|
+
|
|
2402
|
+
const status = reassembler.getStatus()
|
|
2403
|
+
expect(status.bufferedMFUs).toBe(0)
|
|
2404
|
+
expect(status.stats.totalFragments).toBe(1)
|
|
2405
|
+
expect(status.stats.reassembledMFUs).toBe(1)
|
|
2406
|
+
})
|
|
2407
|
+
|
|
2408
|
+
test('rejects an overlapping first without replacing the original fragment', () => {
|
|
2409
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2410
|
+
|
|
2411
|
+
reassembler.addFragment({
|
|
2412
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 1,
|
|
2413
|
+
fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xAA]),
|
|
2414
|
+
timestamp: 1000n, rapFlag: true, source: 'multicast',
|
|
2415
|
+
packetSequenceNumber: testPacketSequence(1, 1),
|
|
2416
|
+
})
|
|
2417
|
+
// Duplicate of fragmentCounter=1
|
|
2418
|
+
expectRejection(() => reassembler.addFragment({
|
|
2419
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 1,
|
|
2420
|
+
fragmentCounter: 1, itemId: 2, data: new Uint8Array([0xBB]),
|
|
2421
|
+
timestamp: 1000n, rapFlag: true, source: 'moq',
|
|
2422
|
+
packetSequenceNumber: testPacketSequence(1, 1),
|
|
2423
|
+
}), 'overlapping-data-unit')
|
|
2424
|
+
|
|
2425
|
+
const result = reassembler.addFragment({
|
|
2426
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 1,
|
|
2427
|
+
fragmentCounter: 0, data: new Uint8Array([0xCC]),
|
|
2428
|
+
timestamp: 1000n, rapFlag: false,
|
|
2429
|
+
packetSequenceNumber: testPacketSequence(1, 0),
|
|
2430
|
+
})
|
|
2431
|
+
expect(result).not.toBeNull()
|
|
2432
|
+
expect(result!.data).toEqual(new Uint8Array([0xAA, 0xCC]))
|
|
2433
|
+
expect(result!.fragmentCount).toBe(2)
|
|
2434
|
+
expect(result!.source).toBe('multicast')
|
|
2435
|
+
})
|
|
2436
|
+
})
|
|
2437
|
+
|
|
2438
|
+
test('should retain an assembly with a gap in fragment counters', () => {
|
|
2439
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2440
|
+
|
|
2441
|
+
// First fragment declares three succeeding payloads.
|
|
2442
|
+
reassembler.addFragment({
|
|
2443
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 1,
|
|
2444
|
+
fragmentCounter: 3, itemId: 1, data: new Uint8Array([0xAA]),
|
|
2445
|
+
timestamp: 1000n, rapFlag: true,
|
|
2446
|
+
packetSequenceNumber: testPacketSequence(1, 3),
|
|
2447
|
+
})
|
|
2448
|
+
// Counter 1 arrives while counter 2 is missing.
|
|
2449
|
+
expect(reassembler.addFragment({
|
|
2450
|
+
fragmentationIndicator: 2, mpuSequenceNumber: 1,
|
|
2451
|
+
fragmentCounter: 1, data: new Uint8Array([0xCC]),
|
|
2452
|
+
timestamp: 1000n, rapFlag: false,
|
|
2453
|
+
packetSequenceNumber: testPacketSequence(1, 1),
|
|
2454
|
+
})).toBeNull()
|
|
2455
|
+
// Last fragment has the required zero counter.
|
|
2456
|
+
expect(reassembler.addFragment({
|
|
2457
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 1,
|
|
2458
|
+
fragmentCounter: 0, data: new Uint8Array([0xDD]),
|
|
2459
|
+
timestamp: 1000n, rapFlag: false,
|
|
2460
|
+
packetSequenceNumber: testPacketSequence(1, 0),
|
|
2461
|
+
})).toBeNull()
|
|
2462
|
+
expect(reassembler.pendingCount).toBe(1)
|
|
2463
|
+
expect(reassembler.getStatus().pendingFragments).toBe(3)
|
|
2464
|
+
})
|
|
2465
|
+
|
|
2466
|
+
test('accepts a reordered countdown and rejects a stale duplicate', () => {
|
|
2467
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2468
|
+
|
|
2469
|
+
// Counters 3, 1, 0 arrive (gap at 2).
|
|
2470
|
+
reassembler.addFragment({
|
|
2471
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 1,
|
|
2472
|
+
fragmentCounter: 3, itemId: 1, data: new Uint8Array([0xAA]),
|
|
2473
|
+
timestamp: 1000n, rapFlag: true,
|
|
2474
|
+
packetSequenceNumber: testPacketSequence(1, 3),
|
|
2475
|
+
})
|
|
2476
|
+
expect(reassembler.addFragment({
|
|
2477
|
+
fragmentationIndicator: 2, mpuSequenceNumber: 1,
|
|
2478
|
+
fragmentCounter: 1, data: new Uint8Array([0xCC]),
|
|
2479
|
+
timestamp: 1000n, rapFlag: false,
|
|
2480
|
+
packetSequenceNumber: testPacketSequence(1, 1),
|
|
2481
|
+
})).toBeNull()
|
|
2482
|
+
expect(reassembler.addFragment({
|
|
2483
|
+
fragmentationIndicator: 2, mpuSequenceNumber: 1,
|
|
2484
|
+
fragmentCounter: 2, data: new Uint8Array([0xBB]),
|
|
2485
|
+
timestamp: 1000n, rapFlag: false,
|
|
2486
|
+
packetSequenceNumber: testPacketSequence(1, 2),
|
|
2487
|
+
})).toBeNull()
|
|
2488
|
+
expectRejection(() => reassembler.addFragment({
|
|
2489
|
+
fragmentationIndicator: 2, mpuSequenceNumber: 1,
|
|
2490
|
+
fragmentCounter: 1, data: new Uint8Array([0xCC]),
|
|
2491
|
+
timestamp: 1001n, rapFlag: false,
|
|
2492
|
+
packetSequenceNumber: testPacketSequence(1, 1),
|
|
2493
|
+
}), 'fragment-counter-mismatch')
|
|
2494
|
+
const result = reassembler.addFragment({
|
|
2495
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 1,
|
|
2496
|
+
fragmentCounter: 0, data: new Uint8Array([0xDD]),
|
|
2497
|
+
timestamp: 1002n, rapFlag: false,
|
|
2498
|
+
packetSequenceNumber: testPacketSequence(1, 0),
|
|
2499
|
+
})
|
|
2500
|
+
expect(result).not.toBeNull()
|
|
2501
|
+
expect(result!.data).toEqual(new Uint8Array([0xAA, 0xBB, 0xCC, 0xDD]))
|
|
2502
|
+
expect(result!.fragmentCount).toBe(4)
|
|
2503
|
+
})
|
|
2504
|
+
|
|
2505
|
+
test('rejects an overlapping first without discarding queued continuations', () => {
|
|
2506
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2507
|
+
const fragment = (
|
|
2508
|
+
fragmentationIndicator: number,
|
|
2509
|
+
fragmentCounter: number,
|
|
2510
|
+
byte: number,
|
|
2511
|
+
): MFUFragment => ({
|
|
2512
|
+
fragmentationIndicator,
|
|
2513
|
+
mpuSequenceNumber: 2,
|
|
2514
|
+
fragmentCounter,
|
|
2515
|
+
data: new Uint8Array([byte]),
|
|
2516
|
+
timestamp: 1000n,
|
|
2517
|
+
rapFlag: fragmentationIndicator === 1,
|
|
2518
|
+
packetSequenceNumber: testPacketSequence(2, fragmentCounter),
|
|
2519
|
+
...(fragmentationIndicator === 1 ? { itemId: 9 } : {}),
|
|
2520
|
+
})
|
|
2521
|
+
|
|
2522
|
+
expect(reassembler.addFragment(fragment(1, 3, 0xAA))).toBeNull()
|
|
2523
|
+
expect(reassembler.addFragment(fragment(2, 1, 0xCC))).toBeNull()
|
|
2524
|
+
expectRejection(
|
|
2525
|
+
() => reassembler.addFragment(fragment(1, 1, 0xEE)),
|
|
2526
|
+
'overlapping-data-unit',
|
|
2527
|
+
)
|
|
2528
|
+
expect(reassembler.addFragment(fragment(2, 2, 0xBB))).toBeNull()
|
|
2529
|
+
expect(reassembler.addFragment(fragment(3, 0, 0xDD))?.data).toEqual(
|
|
2530
|
+
new Uint8Array([0xAA, 0xBB, 0xCC, 0xDD]),
|
|
2531
|
+
)
|
|
2532
|
+
})
|
|
2533
|
+
|
|
2534
|
+
test('rejects a duplicate emitted intrinsic identity', () => {
|
|
2535
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2536
|
+
|
|
2537
|
+
expect(reassembler.addFragment({
|
|
2538
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 42,
|
|
2539
|
+
fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xAA]),
|
|
2540
|
+
timestamp: 1000n, rapFlag: true,
|
|
2541
|
+
packetSequenceNumber: testPacketSequence(42, 1),
|
|
2542
|
+
})).toBeNull()
|
|
2543
|
+
expect(reassembler.addFragment({
|
|
2544
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 42,
|
|
2545
|
+
fragmentCounter: 0, data: new Uint8Array([0xAB]),
|
|
2546
|
+
timestamp: 1001n, rapFlag: false,
|
|
2547
|
+
packetSequenceNumber: testPacketSequence(42, 0),
|
|
2548
|
+
})).not.toBeNull()
|
|
2549
|
+
expect(reassembler.addFragment({
|
|
2550
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 42,
|
|
2551
|
+
fragmentCounter: 0, data: new Uint8Array([0xBC]),
|
|
2552
|
+
timestamp: 1002n, rapFlag: false,
|
|
2553
|
+
packetSequenceNumber: testPacketSequence(42, 0, 10),
|
|
2554
|
+
})).toBeNull()
|
|
2555
|
+
expect(reassembler.pendingCount).toBe(1)
|
|
2556
|
+
expect(reassembler.addFragment({
|
|
2557
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 42,
|
|
2558
|
+
fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xBB]),
|
|
2559
|
+
timestamp: 2000n, rapFlag: true,
|
|
2560
|
+
packetSequenceNumber: testPacketSequence(42, 1, 10),
|
|
2561
|
+
})).toBeNull()
|
|
2562
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
2563
|
+
|
|
2564
|
+
const result = reassembler.addFragment({
|
|
2565
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 43,
|
|
2566
|
+
fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xCC]),
|
|
2567
|
+
timestamp: 2000n, rapFlag: false,
|
|
2568
|
+
packetSequenceNumber: testPacketSequence(43, 1),
|
|
2569
|
+
})
|
|
2570
|
+
expect(result).toBeNull()
|
|
2571
|
+
expect(reassembler.addFragment({
|
|
2572
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 43,
|
|
2573
|
+
fragmentCounter: 0, data: new Uint8Array([0xDD]),
|
|
2574
|
+
timestamp: 2001n, rapFlag: false,
|
|
2575
|
+
packetSequenceNumber: testPacketSequence(43, 0),
|
|
2576
|
+
})?.data).toEqual(new Uint8Array([0xCC, 0xDD]))
|
|
2577
|
+
})
|
|
2578
|
+
|
|
2579
|
+
test('does not create active state for an emitted identity duplicate', () => {
|
|
2580
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2581
|
+
|
|
2582
|
+
expect(reassembler.addFragment({
|
|
2583
|
+
fragmentationIndicator: 1,
|
|
2584
|
+
mpuSequenceNumber: 50,
|
|
2585
|
+
fragmentCounter: 1,
|
|
2586
|
+
movieFragmentSequenceNumber: 10,
|
|
2587
|
+
sampleNumber: 18,
|
|
2588
|
+
sampleOffset: 0,
|
|
2589
|
+
data: new Uint8Array([0xa1]),
|
|
2590
|
+
timestamp: 1000n,
|
|
2591
|
+
rapFlag: true,
|
|
2592
|
+
packetSequenceNumber: testPacketSequence(50, 1),
|
|
2593
|
+
})).toBeNull()
|
|
2594
|
+
expect(reassembler.addFragment({
|
|
2595
|
+
fragmentationIndicator: 3,
|
|
2596
|
+
mpuSequenceNumber: 50,
|
|
2597
|
+
fragmentCounter: 0,
|
|
2598
|
+
data: new Uint8Array([0xb2]),
|
|
2599
|
+
timestamp: 1000n,
|
|
2600
|
+
rapFlag: false,
|
|
2601
|
+
packetSequenceNumber: testPacketSequence(50, 0),
|
|
2602
|
+
})?.data).toEqual(new Uint8Array([0xa1, 0xb2]))
|
|
2603
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
2604
|
+
|
|
2605
|
+
expect(reassembler.addFragment({
|
|
2606
|
+
fragmentationIndicator: 1,
|
|
2607
|
+
mpuSequenceNumber: 50,
|
|
2608
|
+
fragmentCounter: 1,
|
|
2609
|
+
movieFragmentSequenceNumber: 10,
|
|
2610
|
+
sampleNumber: 18,
|
|
2611
|
+
sampleOffset: 0,
|
|
2612
|
+
data: new Uint8Array([0xa1]),
|
|
2613
|
+
timestamp: 2000n,
|
|
2614
|
+
rapFlag: true,
|
|
2615
|
+
packetSequenceNumber: testPacketSequence(50, 1, 10),
|
|
2616
|
+
})).toBeNull()
|
|
2617
|
+
expect(reassembler.pendingCount).toBe(0)
|
|
2618
|
+
})
|
|
2619
|
+
|
|
2620
|
+
test('flush should clear emitted set and allow re-assembly', () => {
|
|
2621
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2622
|
+
|
|
2623
|
+
// Use fragmented MFU (FI=1 start + FI=3 end) — only fragmented MFUs
|
|
2624
|
+
// go through the #emitted dedup set. FI=0 (complete) bypasses it.
|
|
2625
|
+
const first = reassembler.addFragment({
|
|
2626
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 42,
|
|
2627
|
+
fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xAA]),
|
|
2628
|
+
timestamp: 1000n, rapFlag: true,
|
|
2629
|
+
packetSequenceNumber: testPacketSequence(42, 1),
|
|
2630
|
+
})
|
|
2631
|
+
expect(first).toBeNull() // first fragment, not yet complete
|
|
2632
|
+
const assembled = reassembler.addFragment({
|
|
2633
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 42,
|
|
2634
|
+
fragmentCounter: 0, data: new Uint8Array([0xBB]),
|
|
2635
|
+
timestamp: 1000n, rapFlag: true,
|
|
2636
|
+
packetSequenceNumber: testPacketSequence(42, 0),
|
|
2637
|
+
})
|
|
2638
|
+
expect(assembled).not.toBeNull() // complete — mpuSeq 42 now in emitted set
|
|
2639
|
+
|
|
2640
|
+
// Rejected before flush — same mpuSeq already emitted
|
|
2641
|
+
expect(reassembler.addFragment({
|
|
2642
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 42,
|
|
2643
|
+
fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xCC]),
|
|
2644
|
+
timestamp: 1000n, rapFlag: true,
|
|
2645
|
+
packetSequenceNumber: testPacketSequence(42, 1, 10),
|
|
2646
|
+
})).toBeNull()
|
|
2647
|
+
|
|
2648
|
+
// After flush, same mpuSeq should work again
|
|
2649
|
+
reassembler.flush()
|
|
2650
|
+
const r2 = reassembler.addFragment({
|
|
2651
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 42,
|
|
2652
|
+
fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xDD]),
|
|
2653
|
+
timestamp: 1000n, rapFlag: true,
|
|
2654
|
+
packetSequenceNumber: testPacketSequence(42, 1),
|
|
2655
|
+
})
|
|
2656
|
+
expect(r2).toBeNull() // first fragment of new assembly
|
|
2657
|
+
const r3 = reassembler.addFragment({
|
|
2658
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 42,
|
|
2659
|
+
fragmentCounter: 0, data: new Uint8Array([0xEE]),
|
|
2660
|
+
timestamp: 1000n, rapFlag: true,
|
|
2661
|
+
packetSequenceNumber: testPacketSequence(42, 0),
|
|
2662
|
+
})
|
|
2663
|
+
expect(r3).not.toBeNull()
|
|
2664
|
+
expect(r3!.data).toEqual(new Uint8Array([0xDD, 0xEE]))
|
|
2665
|
+
})
|
|
2666
|
+
|
|
2667
|
+
describe('createFragment', () => {
|
|
2668
|
+
test('should create fragment from complete timed MMTP packet', () => {
|
|
2669
|
+
// Build: MMTP(12) + MPU(8, T=1 FI=0 payloadLength=25) + MFU_DU(14) + data(5)
|
|
2670
|
+
const packet = new Uint8Array(12 + 8 + 14 + 5)
|
|
2671
|
+
// MMTP header: byte0 = rap=1, byte1 = MPU type
|
|
2672
|
+
packet[0] = 0x01 // RAP flag set
|
|
2673
|
+
packet[1] = 0x00 // packet_type = MPU
|
|
2674
|
+
// packetId = 1
|
|
2675
|
+
const view = new DataView(packet.buffer)
|
|
2676
|
+
view.setUint16(2, 1, false)
|
|
2677
|
+
view.setUint32(4, 42, false) // timestamp
|
|
2678
|
+
view.setUint32(8, 100, false) // sequence number
|
|
2679
|
+
|
|
2680
|
+
// MPU header at offset 12
|
|
2681
|
+
view.setUint16(12, 25, false) // 6 remaining MPU bytes + 14 DU + 5 media
|
|
2682
|
+
packet[14] = 0x28 // FT=2, T=1, FI=0, A=0
|
|
2683
|
+
packet[15] = 0 // fragment_counter
|
|
2684
|
+
view.setUint32(16, 999, false) // mpuSequenceNumber
|
|
2685
|
+
|
|
2686
|
+
// MFU DU header (14 bytes) at offset 20.
|
|
2687
|
+
view.setUint32(20, 7, false) // movie_fragment_sequence_number
|
|
2688
|
+
view.setUint32(24, 1, false) // sample_number
|
|
2689
|
+
// Video data at offset 34
|
|
2690
|
+
packet[34] = 0xDE; packet[35] = 0xAD; packet[36] = 0xBE
|
|
2691
|
+
packet[37] = 0xEF; packet[38] = 0x01
|
|
2692
|
+
|
|
2693
|
+
const frag = createFragment(packet)
|
|
2694
|
+
expect(frag.mpuSequenceNumber).toBe(999)
|
|
2695
|
+
expect(frag.fragmentationIndicator).toBe(0)
|
|
2696
|
+
expect(frag.rapFlag).toBe(true)
|
|
2697
|
+
expect(frag.movieFragmentSequenceNumber).toBe(7)
|
|
2698
|
+
expect(frag.sampleNumber).toBe(1)
|
|
2699
|
+
expect(frag.sampleOffset).toBe(0)
|
|
2700
|
+
expect(frag.data.length).toBe(5) // 5 bytes of video data
|
|
2701
|
+
})
|
|
2702
|
+
|
|
2703
|
+
test('reads Item_ID from a non-timed MFU DU header', () => {
|
|
2704
|
+
const packet = new Uint8Array(12 + 8 + 4 + 2)
|
|
2705
|
+
const view = new DataView(packet.buffer)
|
|
2706
|
+
view.setUint16(2, 1, false)
|
|
2707
|
+
view.setUint32(4, 42, false)
|
|
2708
|
+
view.setUint32(8, 100, false)
|
|
2709
|
+
view.setUint16(12, 12, false) // 6 remaining MPU bytes + 4 DU + 2 media
|
|
2710
|
+
packet[14] = 0x20 // FT=2, T=0, FI=0
|
|
2711
|
+
view.setUint32(16, 999, false)
|
|
2712
|
+
view.setUint32(20, 0x01020304, false)
|
|
2713
|
+
packet.set([0xde, 0xad], 24)
|
|
2714
|
+
|
|
2715
|
+
const fragment = createFragment(packet)
|
|
2716
|
+
expect(fragment.itemId).toBe(0x01020304)
|
|
2717
|
+
expect(fragment.data).toEqual(new Uint8Array([0xde, 0xad]))
|
|
2718
|
+
})
|
|
2719
|
+
})
|
|
2720
|
+
|
|
2721
|
+
describe('processMMTPWithReassembly', () => {
|
|
2722
|
+
test('should skip FEC repair packets', () => {
|
|
2723
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2724
|
+
const packet = new Uint8Array(12 + 8 + 14 + 4)
|
|
2725
|
+
packet[0] = 0x10 // fecType=2 (repair)
|
|
2726
|
+
const result = processMMTPWithReassembly(packet, reassembler)
|
|
2727
|
+
expect(result).toBeNull()
|
|
2728
|
+
})
|
|
2729
|
+
|
|
2730
|
+
test('should return reassembled MFU for complete packet', () => {
|
|
2731
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2732
|
+
const packet = new Uint8Array(12 + 8 + 14 + 4)
|
|
2733
|
+
packet[0] = 0x01 // RAP
|
|
2734
|
+
packet[1] = 0x00 // MPU
|
|
2735
|
+
const view = new DataView(packet.buffer)
|
|
2736
|
+
view.setUint16(2, 1, false) // packetId
|
|
2737
|
+
view.setUint32(4, 100, false) // timestamp
|
|
2738
|
+
view.setUint32(8, 1, false) // seq
|
|
2739
|
+
// MPU: payloadLength=24 (6 + 14 + 4), FT=2 T=1 FI=0 A=0
|
|
2740
|
+
view.setUint16(12, 24, false)
|
|
2741
|
+
packet[14] = 0x28
|
|
2742
|
+
view.setUint32(16, 42, false) // mpuSeq
|
|
2743
|
+
view.setUint32(20, 7, false) // movie_fragment_sequence_number
|
|
2744
|
+
view.setUint32(24, 1, false) // sample_number
|
|
2745
|
+
|
|
2746
|
+
packet[34] = 0x00; packet[35] = 0x00; packet[36] = 0x00; packet[37] = 0x01
|
|
2747
|
+
|
|
2748
|
+
const result = processMMTPWithReassembly(packet, reassembler)
|
|
2749
|
+
expect(result).not.toBeNull()
|
|
2750
|
+
expect(result!.mpuSequenceNumber).toBe(42)
|
|
2751
|
+
expect(result!.rapFlag).toBe(true)
|
|
2752
|
+
})
|
|
2753
|
+
})
|
|
2754
|
+
|
|
2755
|
+
describe('strict delivery provenance', () => {
|
|
2756
|
+
test('folds to multicast only when every fragment is explicitly multicast', () => {
|
|
2757
|
+
expect(deliveryProvenanceOf([{ source: 'multicast' }, { source: 'multicast' }]))
|
|
2758
|
+
.toBe('multicast')
|
|
2759
|
+
})
|
|
2760
|
+
|
|
2761
|
+
test('a single moq fragment makes the whole frame mixed', () => {
|
|
2762
|
+
expect(deliveryProvenanceOf([{ source: 'multicast' }, { source: 'moq' }]))
|
|
2763
|
+
.toBe('mixed')
|
|
2764
|
+
// ...and it does not matter where in the run it lands.
|
|
2765
|
+
expect(deliveryProvenanceOf([{ source: 'moq' }, { source: 'multicast' }]))
|
|
2766
|
+
.toBe('mixed')
|
|
2767
|
+
})
|
|
2768
|
+
|
|
2769
|
+
test('an unattributed fragment degrades multicast to unknown, never to proven', () => {
|
|
2770
|
+
expect(deliveryProvenanceOf([{ source: 'multicast' }, {}])).toBe('unknown')
|
|
2771
|
+
expect(deliveryProvenanceOf([{}])).toBe('unknown')
|
|
2772
|
+
})
|
|
2773
|
+
|
|
2774
|
+
test('mixed absorbs unknown — a moq byte is worse news than a missing label', () => {
|
|
2775
|
+
expect(deliveryProvenanceOf([{ source: 'moq' }, {}])).toBe('mixed')
|
|
2776
|
+
expect(deliveryProvenanceOf([{}, { source: 'moq' }])).toBe('mixed')
|
|
2777
|
+
})
|
|
2778
|
+
|
|
2779
|
+
test('an empty fragment set proves nothing', () => {
|
|
2780
|
+
expect(deliveryProvenanceOf([])).toBe('unknown')
|
|
2781
|
+
})
|
|
2782
|
+
|
|
2783
|
+
test('the fold never upgrades a verdict', () => {
|
|
2784
|
+
expect(foldDeliveryProvenance('mixed', 'multicast')).toBe('mixed')
|
|
2785
|
+
expect(foldDeliveryProvenance('unknown', 'multicast')).toBe('unknown')
|
|
2786
|
+
expect(foldDeliveryProvenance('unknown', 'moq')).toBe('mixed')
|
|
2787
|
+
})
|
|
2788
|
+
|
|
2789
|
+
test('a multicast head with a non-multicast tail is NOT multicast-exclusive', () => {
|
|
2790
|
+
// This is the leak this field exists to close. `source` legitimately
|
|
2791
|
+
// reports 'multicast' — head-fragment-wins is the routing contract —
|
|
2792
|
+
// but the frame contains bytes that never crossed a multicast socket,
|
|
2793
|
+
// so it must never be counted as physical-delivery evidence.
|
|
2794
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2795
|
+
reassembler.addFragment({
|
|
2796
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 1,
|
|
2797
|
+
fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xAA]),
|
|
2798
|
+
timestamp: 1000n, rapFlag: true, source: 'multicast',
|
|
2799
|
+
packetSequenceNumber: testPacketSequence(1, 1),
|
|
2800
|
+
})
|
|
2801
|
+
const result = reassembler.addFragment({
|
|
2802
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 1,
|
|
2803
|
+
fragmentCounter: 0, data: new Uint8Array([0xCC]),
|
|
2804
|
+
timestamp: 1000n, rapFlag: false, source: 'moq',
|
|
2805
|
+
packetSequenceNumber: testPacketSequence(1, 0),
|
|
2806
|
+
})
|
|
2807
|
+
expect(result).not.toBeNull()
|
|
2808
|
+
expect(result!.fragmentCount).toBe(2)
|
|
2809
|
+
expect(result!.source).toBe('multicast')
|
|
2810
|
+
expect(result!.deliveryProvenance).toBe('mixed')
|
|
2811
|
+
})
|
|
2812
|
+
|
|
2813
|
+
test('an all-multicast fragmented frame is multicast-exclusive', () => {
|
|
2814
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2815
|
+
reassembler.addFragment({
|
|
2816
|
+
fragmentationIndicator: 1, mpuSequenceNumber: 2,
|
|
2817
|
+
fragmentCounter: 1, itemId: 1, data: new Uint8Array([0xAA]),
|
|
2818
|
+
timestamp: 2000n, rapFlag: true, source: 'multicast',
|
|
2819
|
+
packetSequenceNumber: testPacketSequence(2, 1),
|
|
2820
|
+
})
|
|
2821
|
+
const result = reassembler.addFragment({
|
|
2822
|
+
fragmentationIndicator: 3, mpuSequenceNumber: 2,
|
|
2823
|
+
fragmentCounter: 0, data: new Uint8Array([0xCC]),
|
|
2824
|
+
timestamp: 2000n, rapFlag: false, source: 'multicast',
|
|
2825
|
+
packetSequenceNumber: testPacketSequence(2, 0),
|
|
2826
|
+
})
|
|
2827
|
+
expect(result).not.toBeNull()
|
|
2828
|
+
expect(result!.deliveryProvenance).toBe('multicast')
|
|
2829
|
+
})
|
|
2830
|
+
|
|
2831
|
+
test('an unlabelled complete frame proves nothing', () => {
|
|
2832
|
+
const reassembler = new MFUReassembler({ timeoutMs: 1000 })
|
|
2833
|
+
const result = reassembler.addFragment({
|
|
2834
|
+
fragmentationIndicator: 0, mpuSequenceNumber: 3,
|
|
2835
|
+
fragmentCounter: 0, itemId: 1, data: new Uint8Array([1]),
|
|
2836
|
+
timestamp: 3000n, rapFlag: true,
|
|
2837
|
+
})
|
|
2838
|
+
expect(result).not.toBeNull()
|
|
2839
|
+
expect(result!.deliveryProvenance).toBe('unknown')
|
|
2840
|
+
})
|
|
2841
|
+
})
|
|
2842
|
+
}
|