@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,1498 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseMMTPHeader,
|
|
3
|
+
extractMFUPayload,
|
|
4
|
+
extractFecPayloadId,
|
|
5
|
+
hasSourceFecPayloadId,
|
|
6
|
+
extractAltaTrailer,
|
|
7
|
+
PACKET_TYPE_MPU,
|
|
8
|
+
PACKET_TYPE_REPAIR,
|
|
9
|
+
} from '@blockcast/mmt-base'
|
|
10
|
+
import {
|
|
11
|
+
MFUReassembler,
|
|
12
|
+
MfuReassemblyError,
|
|
13
|
+
type MfuReassemblerBackend,
|
|
14
|
+
type MmtpSourcePath,
|
|
15
|
+
type MmtpDeliveryProvenance,
|
|
16
|
+
type ReassembledMFU,
|
|
17
|
+
} from './mfu-reassembler.js'
|
|
18
|
+
import {
|
|
19
|
+
SampleCombiner,
|
|
20
|
+
SampleCombineError,
|
|
21
|
+
type CombinedSample,
|
|
22
|
+
type SampleCombinerOptions,
|
|
23
|
+
type SampleCombinerStats,
|
|
24
|
+
} from './sample-combiner.js'
|
|
25
|
+
import { classifyH264Keyframe, mmtpTimestampToMicros } from './codec-utils.js'
|
|
26
|
+
import {
|
|
27
|
+
mmtpFrameReorderWindowFrames,
|
|
28
|
+
mfuReassemblyTimeoutMs,
|
|
29
|
+
type MfuReassemblyPolicy,
|
|
30
|
+
} from './mfu-reassembly-policy.js'
|
|
31
|
+
import {
|
|
32
|
+
IsoBmffTimingResolver,
|
|
33
|
+
mediaTicksToMicroseconds,
|
|
34
|
+
type IsoBmffSampleTiming,
|
|
35
|
+
} from './iso-bmff-timing.js'
|
|
36
|
+
import type { AuthoredTiming } from './authored-timing.js'
|
|
37
|
+
|
|
38
|
+
// ── Types ───────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export interface TrackInfo {
|
|
41
|
+
type: 'video' | 'audio'
|
|
42
|
+
codec: string
|
|
43
|
+
/** Catalog/in-band frame cadence available to downstream media stages. */
|
|
44
|
+
framerate?: number
|
|
45
|
+
/** Catalog MMTP group duration available to downstream media stages. */
|
|
46
|
+
groupDurationMs?: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface MmtpParserConfig {
|
|
50
|
+
tracks?: Map<number, TrackInfo>
|
|
51
|
+
reorderWindow?: number
|
|
52
|
+
/** MFU reassembly timeout (ms). Derive from FEC interleaveDepthMs or frame duration.
|
|
53
|
+
* Pass null for live clock-free operation; memory remains bounded by maxBuffered.
|
|
54
|
+
* Defaults to 1000ms if not provided. */
|
|
55
|
+
mfuTimeoutMs?: number | null
|
|
56
|
+
/** Explicit transport policy. Reliable MoQ is clock-free; datagrams require
|
|
57
|
+
* a catalog/in-band-derived deadline. Mutually exclusive with mfuTimeoutMs. */
|
|
58
|
+
reassemblyPolicy?: MfuReassemblyPolicy
|
|
59
|
+
/** Max concurrent active MFUs. A new first fragment is rejected at the bound. */
|
|
60
|
+
maxBufferedMfus?: number
|
|
61
|
+
/**
|
|
62
|
+
* Max subsample data units held for one partitioned sample (moqcast #37).
|
|
63
|
+
* A sample needs `ceil(sampleSize / (256 * maxPayload))` data units; at the
|
|
64
|
+
* ~1300-byte payloads this producer emits, one data unit spans ~327 KB, so
|
|
65
|
+
* the default admits a ~5 MB sample — beyond any 8K intra frame — while
|
|
66
|
+
* still bounding the per-insert overlap scan. Raise it only alongside a
|
|
67
|
+
* larger authored frame size, never to paper over a lost data unit.
|
|
68
|
+
*/
|
|
69
|
+
maxSubsampleSegments?: number
|
|
70
|
+
/** Alternate backend. The TypeScript and Rust/WASM implementations enforce
|
|
71
|
+
* the same authored active-DU contract. */
|
|
72
|
+
reassembler?: MfuReassemblerBackend
|
|
73
|
+
/** Default track info for auto-discovered packetIds.
|
|
74
|
+
* When the parser receives an MMTP packet with an unregistered packetId,
|
|
75
|
+
* it creates a new track entry using this info. Required for correct
|
|
76
|
+
* keyframe detection with HEVC/AV1/VP9 (without it, defaults to H.264). */
|
|
77
|
+
defaultTrack?: TrackInfo
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Structured result from parseHeader(). Used by MmtpRouter for two-phase parsing:
|
|
81
|
+
* 1. parseHeader() → get packetId + fecPayloadId for FEC feeding
|
|
82
|
+
* 2. feed() → MFU extraction, reassembly, keyframe detection, emit */
|
|
83
|
+
export interface PacketInfo {
|
|
84
|
+
packetId: number
|
|
85
|
+
packetType: number
|
|
86
|
+
timestamp: bigint
|
|
87
|
+
/** MMTP timestamp converted to monotonic microseconds for pacing/sync users. */
|
|
88
|
+
timestampUs: number
|
|
89
|
+
sequenceNumber: number
|
|
90
|
+
rapFlag: boolean
|
|
91
|
+
fecType: number
|
|
92
|
+
/** FEC source symbol ID (from 4-byte FEC Payload ID at packet end). Null if no FEC. */
|
|
93
|
+
fecPayloadId: { ssId: number } | null
|
|
94
|
+
/** True if this is a repair packet (packetType=0x03). */
|
|
95
|
+
isRepair: boolean
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface FrameMeta {
|
|
99
|
+
/** Authored ISO-BMFF presentation time converted to microseconds. */
|
|
100
|
+
timestamp: number
|
|
101
|
+
/** Authored ISO-BMFF decode time converted to microseconds. */
|
|
102
|
+
decodeTimestamp?: number
|
|
103
|
+
/** Authored ISO-BMFF sample duration converted to microseconds. */
|
|
104
|
+
duration?: number
|
|
105
|
+
/** Exact authored decode time in mediaTimescale units. */
|
|
106
|
+
decodeTime?: bigint
|
|
107
|
+
/** Exact authored presentation time in mediaTimescale units. */
|
|
108
|
+
presentationTime?: bigint
|
|
109
|
+
/** Exact authored duration in mediaTimescale units. */
|
|
110
|
+
durationTicks?: number
|
|
111
|
+
/** mdhd media timescale for the exact timing fields. */
|
|
112
|
+
mediaTimescale?: number
|
|
113
|
+
/** Exact authored ISO-BMFF timing carried without narrowing to microseconds. */
|
|
114
|
+
authoredTiming?: AuthoredTiming
|
|
115
|
+
keyframe: boolean
|
|
116
|
+
packetId: number
|
|
117
|
+
trackType: 'video' | 'audio' | null
|
|
118
|
+
packetSequenceNumber?: number
|
|
119
|
+
/** Every authoritative MMTP source-packet sequence that formed this frame. */
|
|
120
|
+
packetSequenceNumbers?: number[]
|
|
121
|
+
/** Subset of packetSequenceNumbers reconstructed by FEC. */
|
|
122
|
+
repairedPacketSequenceNumbers?: number[]
|
|
123
|
+
/** Immutable catalog identity supplied by the owning ingress subscription. */
|
|
124
|
+
catalogTrackName?: string
|
|
125
|
+
/** Raw MMTP short-NTP timestamp retained for adapters that own the timeline. */
|
|
126
|
+
mmtpTimestamp?: bigint
|
|
127
|
+
mpuSequenceNumber?: number
|
|
128
|
+
movieFragmentSequenceNumber?: number
|
|
129
|
+
sampleNumber?: number
|
|
130
|
+
fragmentCount?: number
|
|
131
|
+
rapFlag?: boolean
|
|
132
|
+
priority?: number
|
|
133
|
+
recovered?: boolean
|
|
134
|
+
/** Starts a new publisher/source epoch for ordering and timestamp state. */
|
|
135
|
+
sourceDiscontinuity?: boolean
|
|
136
|
+
/** Source of the accepted FI=1/FI=0 frame path. Absent for recovery. */
|
|
137
|
+
source?: MmtpSourcePath
|
|
138
|
+
/**
|
|
139
|
+
* Strict whole-frame delivery provenance. Unlike `source`, which names the
|
|
140
|
+
* head fragment's transport for routing, this is folded over every
|
|
141
|
+
* contributing fragment (and, for FEC-recovered frames, over the transports
|
|
142
|
+
* that supplied the block's symbols). Only 'multicast' is evidence of
|
|
143
|
+
* multicast-exclusive delivery. Absent means no producer on this path
|
|
144
|
+
* recorded provenance — consumers MUST treat that as unproven, never as
|
|
145
|
+
* multicast.
|
|
146
|
+
*/
|
|
147
|
+
deliveryProvenance?: MmtpDeliveryProvenance
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface MmtpReorderStats {
|
|
151
|
+
droppedBeforeStartupKeyframe: number
|
|
152
|
+
droppedIncompleteStartupInterval: number
|
|
153
|
+
droppedStale: number
|
|
154
|
+
forcedWindowFlushes: number
|
|
155
|
+
droppedOnLiveDispose: number
|
|
156
|
+
publisherResets: number
|
|
157
|
+
droppedOnPublisherReset: number
|
|
158
|
+
bufferedFrames: number
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface MmtpReorderFrameDiagnostics {
|
|
162
|
+
timestampUs: number
|
|
163
|
+
keyframe: boolean
|
|
164
|
+
sequenceNumber: number
|
|
165
|
+
packetSequenceNumber?: number
|
|
166
|
+
movieFragmentSequenceNumber?: number
|
|
167
|
+
sampleNumber?: number
|
|
168
|
+
fragmentCount?: number
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface MmtpReorderStreamDiagnostics {
|
|
172
|
+
packetId: number
|
|
173
|
+
bufferedFrames: number
|
|
174
|
+
lastEmitted: MmtpReorderFrameDiagnostics | null
|
|
175
|
+
startupKeyframe: MmtpReorderFrameDiagnostics | null
|
|
176
|
+
head: MmtpReorderFrameDiagnostics[]
|
|
177
|
+
tail: MmtpReorderFrameDiagnostics[]
|
|
178
|
+
keyframes: MmtpReorderFrameDiagnostics[]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type MmtpParserRecord =
|
|
182
|
+
| {
|
|
183
|
+
kind: 'repair' | 'non-media'
|
|
184
|
+
packet: PacketInfo
|
|
185
|
+
byteLength: number
|
|
186
|
+
}
|
|
187
|
+
| {
|
|
188
|
+
kind: 'init' | 'fragment'
|
|
189
|
+
packet: PacketInfo
|
|
190
|
+
data: Uint8Array
|
|
191
|
+
mpuSequenceNumber: number
|
|
192
|
+
fragmentationIndicator: number
|
|
193
|
+
fragmentCounter: number
|
|
194
|
+
}
|
|
195
|
+
| {
|
|
196
|
+
kind: 'pending'
|
|
197
|
+
packet: PacketInfo
|
|
198
|
+
mpuSequenceNumber: number
|
|
199
|
+
fragmentationIndicator: number
|
|
200
|
+
fragmentCounter: number
|
|
201
|
+
}
|
|
202
|
+
| {
|
|
203
|
+
kind: 'rejected'
|
|
204
|
+
packet: PacketInfo
|
|
205
|
+
mpuSequenceNumber: number
|
|
206
|
+
fragmentationIndicator: number
|
|
207
|
+
fragmentCounter: number
|
|
208
|
+
error: {
|
|
209
|
+
/**
|
|
210
|
+
* Either a single data unit's own fragment chain was rejected, or
|
|
211
|
+
* combining reassembled data units into a sample was (moqcast #37).
|
|
212
|
+
* Both are fail-closed drops of the same packet, so they share this
|
|
213
|
+
* record; the code says which class it came from.
|
|
214
|
+
*/
|
|
215
|
+
code: MfuReassemblyError['code'] | SampleCombineError['code']
|
|
216
|
+
message: string
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
| {
|
|
220
|
+
kind: 'frame'
|
|
221
|
+
packet: PacketInfo
|
|
222
|
+
data: Uint8Array
|
|
223
|
+
meta: FrameMeta
|
|
224
|
+
mpuSequenceNumber: number
|
|
225
|
+
fragmentCount: number
|
|
226
|
+
rapFlag: boolean
|
|
227
|
+
priority?: number
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
type FrameCallback = (data: Uint8Array, meta: FrameMeta) => void
|
|
231
|
+
type InitCallback = (data: Uint8Array, packetId: number, catalogTrackName?: string) => void
|
|
232
|
+
|
|
233
|
+
export function isMfuKeyframeMetadata(rapFlag: boolean, priority?: number): boolean {
|
|
234
|
+
return rapFlag || (priority ?? 0) > 0x80
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Per-packet detached ALTA authenticator trailer (08-07 Path 2).
|
|
238
|
+
* - `auth`: the authenticator bytes (length-prefixed at packet end).
|
|
239
|
+
* - `payloadEnd`: byte offset within the source packet where signed
|
|
240
|
+
* payload ends — consumers reconstruct `payload = packet[0..payloadEnd]`
|
|
241
|
+
* for verify_detached. */
|
|
242
|
+
export interface ParsedAltaTrailer {
|
|
243
|
+
auth: Uint8Array
|
|
244
|
+
payloadEnd: number
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Source-symbol exposure for FEC consumers.
|
|
248
|
+
*
|
|
249
|
+
* Fires from {@link MmtpContainerParser.feed} once per FEC-protected source
|
|
250
|
+
* packet — after parseHeader confirms fec_payload_id is present, before MFU
|
|
251
|
+
* extraction.
|
|
252
|
+
*
|
|
253
|
+
* Arguments:
|
|
254
|
+
* - `packetId` – MMTP header `packet_id` (track identifier)
|
|
255
|
+
* - `ssId` – flat Source Symbol ID from the 4-byte fec_payload_id trailer
|
|
256
|
+
* - `rawSymbol` – packet bytes minus the 4-byte fec_payload_id trailer.
|
|
257
|
+
* NOT zero-padded to FEC symbol size — consumer pads if needed
|
|
258
|
+
* (FEC symbol size is FEC-config, not parser-config).
|
|
259
|
+
* - `alta` – detached ALTA authenticator if present, else undefined
|
|
260
|
+
*
|
|
261
|
+
* Designed so consumers (player, Shaka shim, upstream moqtail) can wire
|
|
262
|
+
* source-symbol delivery to whatever FEC pipeline they own, without depending
|
|
263
|
+
* on libmmt's MulticastManager/MmtpRouter/FECManager stack. */
|
|
264
|
+
type SourceSymbolCallback = (
|
|
265
|
+
packetId: number,
|
|
266
|
+
ssId: number,
|
|
267
|
+
rawSymbol: Uint8Array,
|
|
268
|
+
alta: ParsedAltaTrailer | undefined,
|
|
269
|
+
) => void
|
|
270
|
+
|
|
271
|
+
export interface ParseHeaderOptions {
|
|
272
|
+
/** Recovered source symbols already had the trailing Source FEC Payload ID stripped. */
|
|
273
|
+
skipFecPayloadId?: boolean
|
|
274
|
+
/** Mark frames emitted from this packet as FEC-recovered. */
|
|
275
|
+
recovered?: boolean
|
|
276
|
+
/** Exact catalog track bound to this packet by its ingress subscription. */
|
|
277
|
+
catalogTrackName?: string
|
|
278
|
+
/** Source transport for parser-owned frame attribution. */
|
|
279
|
+
source?: MmtpSourcePath
|
|
280
|
+
/**
|
|
281
|
+
* Explicit strict provenance for the fragments this packet produces.
|
|
282
|
+
* The router sets it for FEC-recovered packets, which carry no `source`
|
|
283
|
+
* but whose FEC block's symbol provenance is known.
|
|
284
|
+
*/
|
|
285
|
+
deliveryProvenance?: MmtpDeliveryProvenance
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── FrameSequencer ──────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
interface PendingFrame {
|
|
291
|
+
data: Uint8Array
|
|
292
|
+
meta: FrameMeta
|
|
293
|
+
seq: number
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
interface SequencerStream {
|
|
297
|
+
buf: PendingFrame[]
|
|
298
|
+
lastEmitted: PendingFrame | null
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function reorderFrameDiagnostics(frame: PendingFrame): MmtpReorderFrameDiagnostics {
|
|
302
|
+
return {
|
|
303
|
+
timestampUs: frame.meta.timestamp,
|
|
304
|
+
keyframe: frame.meta.keyframe,
|
|
305
|
+
sequenceNumber: frame.seq,
|
|
306
|
+
...(frame.meta.packetSequenceNumber === undefined
|
|
307
|
+
? {}
|
|
308
|
+
: { packetSequenceNumber: frame.meta.packetSequenceNumber }),
|
|
309
|
+
...(frame.meta.movieFragmentSequenceNumber === undefined
|
|
310
|
+
? {}
|
|
311
|
+
: { movieFragmentSequenceNumber: frame.meta.movieFragmentSequenceNumber }),
|
|
312
|
+
...(frame.meta.sampleNumber === undefined ? {} : { sampleNumber: frame.meta.sampleNumber }),
|
|
313
|
+
...(frame.meta.fragmentCount === undefined
|
|
314
|
+
? {}
|
|
315
|
+
: { fragmentCount: frame.meta.fragmentCount }),
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
type SequencerStat = Exclude<keyof MmtpReorderStats, 'bufferedFrames'>
|
|
320
|
+
|
|
321
|
+
function serialSequenceDelta(a: number, b: number): number {
|
|
322
|
+
return (a - b) | 0
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function compareFrameOrder(
|
|
326
|
+
a: PendingFrame,
|
|
327
|
+
b: PendingFrame,
|
|
328
|
+
): number {
|
|
329
|
+
if (a.meta.decodeTime !== undefined && b.meta.decodeTime !== undefined &&
|
|
330
|
+
a.meta.mediaTimescale === b.meta.mediaTimescale) {
|
|
331
|
+
if (a.meta.decodeTime < b.meta.decodeTime) return -1
|
|
332
|
+
if (a.meta.decodeTime > b.meta.decodeTime) return 1
|
|
333
|
+
} else {
|
|
334
|
+
const decodeDelta = (a.meta.decodeTimestamp ?? a.meta.timestamp) -
|
|
335
|
+
(b.meta.decodeTimestamp ?? b.meta.timestamp)
|
|
336
|
+
if (decodeDelta !== 0) return decodeDelta
|
|
337
|
+
}
|
|
338
|
+
const movieDelta = serialSequenceDelta(
|
|
339
|
+
a.meta.movieFragmentSequenceNumber ?? 0,
|
|
340
|
+
b.meta.movieFragmentSequenceNumber ?? 0,
|
|
341
|
+
)
|
|
342
|
+
if (movieDelta !== 0) return movieDelta
|
|
343
|
+
const sampleDelta = (a.meta.sampleNumber ?? 0) - (b.meta.sampleNumber ?? 0)
|
|
344
|
+
return sampleDelta !== 0 ? sampleDelta : serialSequenceDelta(a.seq, b.seq)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
class FrameSequencer {
|
|
348
|
+
#streams = new Map<number, SequencerStream>()
|
|
349
|
+
#windowSize: number
|
|
350
|
+
#emit: (data: Uint8Array, meta: FrameMeta) => void
|
|
351
|
+
#recordStat: (stat: SequencerStat, count?: number) => void
|
|
352
|
+
|
|
353
|
+
constructor(
|
|
354
|
+
windowSize: number,
|
|
355
|
+
emit: (data: Uint8Array, meta: FrameMeta) => void,
|
|
356
|
+
recordStat: (stat: SequencerStat, count?: number) => void = () => {},
|
|
357
|
+
) {
|
|
358
|
+
this.#windowSize = windowSize
|
|
359
|
+
this.#emit = emit
|
|
360
|
+
this.#recordStat = recordStat
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
push(data: Uint8Array, meta: FrameMeta, seq: number): void {
|
|
364
|
+
if (this.#windowSize <= 0) {
|
|
365
|
+
this.#emit(data, meta)
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
const frame: PendingFrame = { data, meta: { ...meta }, seq }
|
|
369
|
+
const stream = this.#streamFor(meta.packetId)
|
|
370
|
+
if (meta.sourceDiscontinuity) {
|
|
371
|
+
this.#recordStat('publisherResets')
|
|
372
|
+
this.#recordStat('droppedOnPublisherReset', stream.buf.length)
|
|
373
|
+
stream.buf = []
|
|
374
|
+
stream.lastEmitted = null
|
|
375
|
+
frame.meta.sourceDiscontinuity = true
|
|
376
|
+
}
|
|
377
|
+
if (this.#isStale(stream, frame)) {
|
|
378
|
+
this.#recordStat('droppedStale')
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
if (meta.keyframe && stream.lastEmitted === null) {
|
|
382
|
+
// The first RAP establishes decoder sync. Discard any pre-RAP media and
|
|
383
|
+
// emit it immediately so startup does not pay the reorder-window delay.
|
|
384
|
+
this.#recordStat('droppedBeforeStartupKeyframe', stream.buf.length)
|
|
385
|
+
stream.buf = []
|
|
386
|
+
this.#emitFrame(stream, frame)
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
let lo = 0, hi = stream.buf.length
|
|
390
|
+
while (lo < hi) {
|
|
391
|
+
const mid = (lo + hi) >>> 1
|
|
392
|
+
if (compareFrameOrder(stream.buf[mid]!, frame) < 0) lo = mid + 1
|
|
393
|
+
else hi = mid
|
|
394
|
+
}
|
|
395
|
+
stream.buf.splice(lo, 0, frame)
|
|
396
|
+
while (stream.buf.length > this.#windowSize) this.#flushHead(stream)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
flush(): void {
|
|
400
|
+
for (const packetId of [...this.#streams.keys()].sort((a, b) => a - b)) {
|
|
401
|
+
const stream = this.#streams.get(packetId)
|
|
402
|
+
while (stream && stream.buf.length > 0) this.#flushHead(stream)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
get buffered(): number {
|
|
407
|
+
let total = 0
|
|
408
|
+
for (const stream of this.#streams.values()) total += stream.buf.length
|
|
409
|
+
return total
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
get diagnostics(): Omit<MmtpReorderStreamDiagnostics, 'packetId'>[] {
|
|
413
|
+
return [...this.#streams.values()].map(stream => ({
|
|
414
|
+
bufferedFrames: stream.buf.length,
|
|
415
|
+
lastEmitted: stream.lastEmitted ? reorderFrameDiagnostics(stream.lastEmitted) : null,
|
|
416
|
+
startupKeyframe: null,
|
|
417
|
+
head: stream.buf.slice(0, 4).map(reorderFrameDiagnostics),
|
|
418
|
+
tail: stream.buf.slice(-4).map(reorderFrameDiagnostics),
|
|
419
|
+
keyframes: stream.buf.filter(frame => frame.meta.keyframe).slice(0, 8)
|
|
420
|
+
.map(reorderFrameDiagnostics),
|
|
421
|
+
}))
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
discard(): void {
|
|
425
|
+
for (const stream of this.#streams.values()) {
|
|
426
|
+
this.#recordStat('droppedOnLiveDispose', stream.buf.length)
|
|
427
|
+
stream.buf = []
|
|
428
|
+
}
|
|
429
|
+
this.#streams.clear()
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
#streamFor(packetId: number): SequencerStream {
|
|
433
|
+
let stream = this.#streams.get(packetId)
|
|
434
|
+
if (!stream) {
|
|
435
|
+
stream = { buf: [], lastEmitted: null }
|
|
436
|
+
this.#streams.set(packetId, stream)
|
|
437
|
+
}
|
|
438
|
+
return stream
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#flushHead(stream: SequencerStream): boolean {
|
|
442
|
+
const f = stream.buf.shift()
|
|
443
|
+
if (!f) return false
|
|
444
|
+
return this.#emitFrame(stream, f)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
#isStale(stream: SequencerStream, frame: PendingFrame): boolean {
|
|
448
|
+
return stream.lastEmitted !== null &&
|
|
449
|
+
compareFrameOrder(frame, stream.lastEmitted) <= 0
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
#emitFrame(stream: SequencerStream, frame: PendingFrame): boolean {
|
|
453
|
+
if (this.#isStale(stream, frame)) {
|
|
454
|
+
this.#recordStat('droppedStale')
|
|
455
|
+
return false
|
|
456
|
+
}
|
|
457
|
+
stream.lastEmitted = frame
|
|
458
|
+
this.#emit(frame.data, frame.meta)
|
|
459
|
+
return true
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ── fMP4 box detection ──────────────────────────────────────────────
|
|
464
|
+
|
|
465
|
+
const FMPU_BOX_TYPES = new Set(['ftyp', 'moov', 'moof'])
|
|
466
|
+
|
|
467
|
+
function isFmp4Box(data: Uint8Array): boolean {
|
|
468
|
+
if (data.length < 8) return false
|
|
469
|
+
const type = String.fromCharCode(data[4]!, data[5]!, data[6]!, data[7]!)
|
|
470
|
+
return FMPU_BOX_TYPES.has(type)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
interface TimingStreamState {
|
|
474
|
+
packetId: number
|
|
475
|
+
resolver: IsoBmffTimingResolver
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ── MmtpContainerParser ─────────────────────────────────────────────
|
|
479
|
+
|
|
480
|
+
export class MmtpContainerParser {
|
|
481
|
+
#tracks = new Map<number, TrackInfo>()
|
|
482
|
+
#defaultTrack: TrackInfo | null = null
|
|
483
|
+
#reassembler: MfuReassemblerBackend
|
|
484
|
+
/**
|
|
485
|
+
* Combines subsample data units back into whole samples (moqcast #37), one
|
|
486
|
+
* combiner per parser stream. Per-stream is required, not an optimization:
|
|
487
|
+
* {@link SampleCombiner} identifies a sample by
|
|
488
|
+
* `(catalogTrackName, mpu, movieFragment, sampleNumber)`, so two streams
|
|
489
|
+
* that carry no catalog identity would collide on every sample number and
|
|
490
|
+
* each would see the other's data units as duplicates of its own. The
|
|
491
|
+
* parser already isolates timing and reordering by this same stream key.
|
|
492
|
+
*
|
|
493
|
+
* Inert until a producer partitions — which it does the moment a frame
|
|
494
|
+
* exceeds one data unit's 256-payload capacity (~327 KB here, routinely
|
|
495
|
+
* exceeded by 4K/8K intra frames).
|
|
496
|
+
*/
|
|
497
|
+
#sampleCombiners = new Map<string, SampleCombiner>()
|
|
498
|
+
#sampleCombinerOptions: SampleCombinerOptions
|
|
499
|
+
/** Counters folded in from combiners dropped by a track reset, so the
|
|
500
|
+
* totals below stay monotonic across a publisher change. */
|
|
501
|
+
#retiredSampleCombinerStats: SampleCombinerStats = {
|
|
502
|
+
combinedSamples: 0,
|
|
503
|
+
passthroughSamples: 0,
|
|
504
|
+
timedOutSamples: 0,
|
|
505
|
+
evictedSamples: 0,
|
|
506
|
+
}
|
|
507
|
+
#timingStreams = new Map<string, TimingStreamState>()
|
|
508
|
+
#maxPendingTimingSamples: number
|
|
509
|
+
#pendingTimingSamples = 0
|
|
510
|
+
#sequencers = new Map<string, { packetId: number; sequencer: FrameSequencer }>()
|
|
511
|
+
#reorderWindow = 0
|
|
512
|
+
#reassemblyPolicy: MfuReassemblyPolicy | null = null
|
|
513
|
+
#frameCb: FrameCallback | null = null
|
|
514
|
+
#initCb: InitCallback | null = null
|
|
515
|
+
#sourceSymbolCb: SourceSymbolCallback | null = null
|
|
516
|
+
#trackFrameCbs = new Map<number, FrameCallback>()
|
|
517
|
+
#reorderStats: Omit<MmtpReorderStats, 'bufferedFrames'> = {
|
|
518
|
+
droppedBeforeStartupKeyframe: 0,
|
|
519
|
+
droppedIncompleteStartupInterval: 0,
|
|
520
|
+
droppedStale: 0,
|
|
521
|
+
forcedWindowFlushes: 0,
|
|
522
|
+
droppedOnLiveDispose: 0,
|
|
523
|
+
publisherResets: 0,
|
|
524
|
+
droppedOnPublisherReset: 0,
|
|
525
|
+
}
|
|
526
|
+
/** Streams (keyed by physical parser stream, value is packetId) that have
|
|
527
|
+
* observed their first RAP under a reliable-moq policy. Reliable MoQ
|
|
528
|
+
* resolves to a zero parser reorder window (transport ordering is
|
|
529
|
+
* protocol-owned), so #sequencerFor returns null and its own startup gate
|
|
530
|
+
* never runs; this map preserves an order-independent "discard delta
|
|
531
|
+
* frames before the first RAP" gate for that path without reintroducing
|
|
532
|
+
* reordering into the parser. */
|
|
533
|
+
#reliableMoqStartupGate = new Map<string, number>()
|
|
534
|
+
#disposed = false
|
|
535
|
+
|
|
536
|
+
#streamKey(packetId: number, catalogTrackName?: string): string {
|
|
537
|
+
return JSON.stringify([packetId, catalogTrackName ?? null])
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
#sampleCombinerFor(streamKey: string): SampleCombiner {
|
|
541
|
+
let combiner = this.#sampleCombiners.get(streamKey)
|
|
542
|
+
if (combiner === undefined) {
|
|
543
|
+
combiner = new SampleCombiner(this.#sampleCombinerOptions)
|
|
544
|
+
this.#sampleCombiners.set(streamKey, combiner)
|
|
545
|
+
}
|
|
546
|
+
return combiner
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Drop one stream's combiner, folding its counters into the retained
|
|
550
|
+
* totals so a reset cannot make the exposed stats run backwards. */
|
|
551
|
+
#dropSampleCombiner(streamKey: string): void {
|
|
552
|
+
const combiner = this.#sampleCombiners.get(streamKey)
|
|
553
|
+
if (combiner === undefined) return
|
|
554
|
+
const stats = combiner.stats
|
|
555
|
+
this.#retiredSampleCombinerStats.combinedSamples += stats.combinedSamples
|
|
556
|
+
this.#retiredSampleCombinerStats.passthroughSamples += stats.passthroughSamples
|
|
557
|
+
this.#retiredSampleCombinerStats.timedOutSamples += stats.timedOutSamples
|
|
558
|
+
this.#retiredSampleCombinerStats.evictedSamples += stats.evictedSamples
|
|
559
|
+
this.#sampleCombiners.delete(streamKey)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
constructor(config?: MmtpParserConfig) {
|
|
563
|
+
if (config?.reassemblyPolicy && config.mfuTimeoutMs !== undefined) {
|
|
564
|
+
throw new Error('[MmtpContainerParser] use reassemblyPolicy or mfuTimeoutMs, not both')
|
|
565
|
+
}
|
|
566
|
+
const reassemblyPolicy = config?.reassemblyPolicy ?? null
|
|
567
|
+
const requestedReorderWindow = config?.reorderWindow ?? 0
|
|
568
|
+
if (!Number.isFinite(requestedReorderWindow) || requestedReorderWindow < 0) {
|
|
569
|
+
throw new Error('[MmtpContainerParser] reorderWindow must be a finite number >= 0')
|
|
570
|
+
}
|
|
571
|
+
const normalizedReorderWindow = Math.ceil(requestedReorderWindow)
|
|
572
|
+
const requestedMfuTimeout = reassemblyPolicy
|
|
573
|
+
? mfuReassemblyTimeoutMs(reassemblyPolicy)
|
|
574
|
+
: config?.mfuTimeoutMs
|
|
575
|
+
if (reassemblyPolicy) {
|
|
576
|
+
mmtpFrameReorderWindowFrames(reassemblyPolicy, normalizedReorderWindow)
|
|
577
|
+
}
|
|
578
|
+
if (
|
|
579
|
+
requestedMfuTimeout !== undefined &&
|
|
580
|
+
requestedMfuTimeout !== null &&
|
|
581
|
+
requestedMfuTimeout <= 0
|
|
582
|
+
) {
|
|
583
|
+
throw new Error('[MmtpContainerParser] mfuTimeoutMs must be > 0')
|
|
584
|
+
}
|
|
585
|
+
const maxPendingTimingSamples = config?.maxBufferedMfus ?? 100
|
|
586
|
+
if (!Number.isInteger(maxPendingTimingSamples) || maxPendingTimingSamples <= 0) {
|
|
587
|
+
throw new Error('[MmtpContainerParser] maxBufferedMfus must be a positive integer')
|
|
588
|
+
}
|
|
589
|
+
this.#maxPendingTimingSamples = maxPendingTimingSamples
|
|
590
|
+
|
|
591
|
+
const maxSubsampleSegments = config?.maxSubsampleSegments ?? 16
|
|
592
|
+
if (!Number.isInteger(maxSubsampleSegments) || maxSubsampleSegments <= 0) {
|
|
593
|
+
throw new Error('[MmtpContainerParser] maxSubsampleSegments must be a positive integer')
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Everything above only reads `config` — the full policy/window
|
|
597
|
+
// combination is validated before this point, so it is now safe to
|
|
598
|
+
// mutate this instance or an injected, caller-owned reassembler.
|
|
599
|
+
this.#reassemblyPolicy = reassemblyPolicy
|
|
600
|
+
if (config?.reassembler) {
|
|
601
|
+
this.#reassembler = config.reassembler
|
|
602
|
+
// An injected backend owns its existing transport policy unless this
|
|
603
|
+
// parser was given an explicit policy/legacy timeout override.
|
|
604
|
+
if (requestedMfuTimeout !== undefined) {
|
|
605
|
+
this.#reassembler.timeoutMs = requestedMfuTimeout
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
this.#reassembler = new MFUReassembler({
|
|
609
|
+
timeoutMs: requestedMfuTimeout === undefined ? 1000 : requestedMfuTimeout,
|
|
610
|
+
...(config?.maxBufferedMfus === undefined ? {} : { maxBuffered: config.maxBufferedMfus }),
|
|
611
|
+
})
|
|
612
|
+
}
|
|
613
|
+
this.#reorderWindow = normalizedReorderWindow
|
|
614
|
+
this.#sampleCombinerOptions = {
|
|
615
|
+
maxBuffered: maxPendingTimingSamples,
|
|
616
|
+
// The parser resolves every timed sample's authored size from FT=1
|
|
617
|
+
// before its data units reach the combiner, so "no size yet" always
|
|
618
|
+
// means the size has not arrived -- never that the host declines to
|
|
619
|
+
// supply one. Waiting is therefore correct: treating an offset-zero
|
|
620
|
+
// data unit as a whole sample in that window would truncate a
|
|
621
|
+
// partitioned sample instead of completing it.
|
|
622
|
+
requireAuthoredSize: true,
|
|
623
|
+
maxSegmentsPerSample: maxSubsampleSegments,
|
|
624
|
+
}
|
|
625
|
+
this.#defaultTrack = config?.defaultTrack ?? null
|
|
626
|
+
if (this.#defaultTrack) this.#validateReassemblyPolicy()
|
|
627
|
+
if (config?.tracks) {
|
|
628
|
+
for (const [id, info] of config.tracks) {
|
|
629
|
+
this.#validateReassemblyPolicy()
|
|
630
|
+
this.#tracks.set(id, info)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** The MFU reassembler that buffers fragmented MFUs fed via feed().
|
|
636
|
+
* Exposed so a co-operating FEC-recovery layer (MmtpRouter) feeds recovered
|
|
637
|
+
* fragments into the SAME reassembler: a frame split between source-received
|
|
638
|
+
* and FEC-recovered fragments must share one buffer to complete (BLO-8151).
|
|
639
|
+
* Before this was shared, recovered video symbols reported "0 emitted,
|
|
640
|
+
* pending-reassembly=N" because they landed in a separate instance. */
|
|
641
|
+
get reassembler(): MfuReassemblerBackend {
|
|
642
|
+
return this.#reassembler
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Subsample-combining counters (moqcast #37). `combinedSamples` counts
|
|
647
|
+
* samples reassembled from two or more data units and `passthroughSamples`
|
|
648
|
+
* those that arrived whole; the two are deliberately separate, so a
|
|
649
|
+
* producer that silently stopped partitioning cannot hide behind a single
|
|
650
|
+
* "samples delivered" total. This is the typed acceptance signal for
|
|
651
|
+
* large-frame playback — never parse the console for it.
|
|
652
|
+
*/
|
|
653
|
+
get sampleCombinerStats(): Readonly<SampleCombinerStats> {
|
|
654
|
+
const total = { ...this.#retiredSampleCombinerStats }
|
|
655
|
+
for (const combiner of this.#sampleCombiners.values()) {
|
|
656
|
+
const stats = combiner.stats
|
|
657
|
+
total.combinedSamples += stats.combinedSamples
|
|
658
|
+
total.passthroughSamples += stats.passthroughSamples
|
|
659
|
+
total.timedOutSamples += stats.timedOutSamples
|
|
660
|
+
total.evictedSamples += stats.evictedSamples
|
|
661
|
+
}
|
|
662
|
+
return total
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
get reorderStats(): Readonly<MmtpReorderStats> {
|
|
666
|
+
let bufferedFrames = 0
|
|
667
|
+
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered
|
|
668
|
+
return { ...this.#reorderStats, bufferedFrames }
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
get reorderDiagnostics(): ReadonlyArray<MmtpReorderStreamDiagnostics> {
|
|
672
|
+
const streams: MmtpReorderStreamDiagnostics[] = []
|
|
673
|
+
for (const { packetId, sequencer } of this.#sequencers.values()) {
|
|
674
|
+
for (const diagnostics of sequencer.diagnostics) streams.push({ packetId, ...diagnostics })
|
|
675
|
+
}
|
|
676
|
+
return streams
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Phase 1 of two-phase parsing. Returns structured header info for FEC feeding.
|
|
681
|
+
* Call this before feed() when you need to extract FEC payload ID and feed
|
|
682
|
+
* symbols to FECManager before MFU extraction.
|
|
683
|
+
* Returns null for malformed packets.
|
|
684
|
+
*/
|
|
685
|
+
parseHeader(packet: Uint8Array, options?: ParseHeaderOptions): PacketInfo | null {
|
|
686
|
+
try {
|
|
687
|
+
const header = parseMMTPHeader(packet)
|
|
688
|
+
const isRepair = header.packetType === PACKET_TYPE_REPAIR
|
|
689
|
+
const fecPayloadId =
|
|
690
|
+
!options?.skipFecPayloadId &&
|
|
691
|
+
!isRepair && hasSourceFecPayloadId(packet)
|
|
692
|
+
? extractFecPayloadId(packet)
|
|
693
|
+
: null
|
|
694
|
+
return {
|
|
695
|
+
packetId: header.packetId,
|
|
696
|
+
packetType: header.packetType,
|
|
697
|
+
timestamp: header.timestamp,
|
|
698
|
+
timestampUs: mmtpTimestampToMicros(header.timestamp),
|
|
699
|
+
sequenceNumber: header.sequenceNumber,
|
|
700
|
+
rapFlag: header.rapFlag,
|
|
701
|
+
fecType: header.fecType,
|
|
702
|
+
fecPayloadId,
|
|
703
|
+
isRepair,
|
|
704
|
+
}
|
|
705
|
+
} catch {
|
|
706
|
+
return null
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Feed a complete MMTP packet. For two-phase use, call parseHeader() first. */
|
|
711
|
+
feed(packet: Uint8Array, options?: ParseHeaderOptions): MmtpParserRecord | null {
|
|
712
|
+
if (this.#disposed) return null
|
|
713
|
+
|
|
714
|
+
const info = this.parseHeader(packet, options)
|
|
715
|
+
if (!info) return null
|
|
716
|
+
|
|
717
|
+
if (info.packetType !== PACKET_TYPE_MPU) {
|
|
718
|
+
return {
|
|
719
|
+
kind: info.isRepair ? 'repair' : 'non-media',
|
|
720
|
+
packet: info,
|
|
721
|
+
byteLength: packet.byteLength,
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// ── FEC source-symbol exposure ──
|
|
726
|
+
// Fire onSourceSymbol BEFORE MFU extraction so the consumer-side FEC
|
|
727
|
+
// pipeline (e.g. FecWorkerClient.feedSource) starts as early as possible.
|
|
728
|
+
// Source-only: parseHeader already gates fecPayloadId on !isRepair.
|
|
729
|
+
if (info.fecPayloadId !== null && this.#sourceSymbolCb) {
|
|
730
|
+
// Strip the 4-byte fec_payload_id trailer per ISO 23008-1 §C.5.2.
|
|
731
|
+
// Symbol-size zero-padding is the consumer's responsibility — FEC
|
|
732
|
+
// symbol size is a FEC-config concern, not a parser concern.
|
|
733
|
+
const rawSymbol = packet.subarray(0, packet.length - 4)
|
|
734
|
+
const altaTrailer = extractAltaTrailer(packet)
|
|
735
|
+
const alta = altaTrailer
|
|
736
|
+
? { auth: altaTrailer.auth, payloadEnd: altaTrailer.signedEnd }
|
|
737
|
+
: undefined
|
|
738
|
+
try {
|
|
739
|
+
this.#sourceSymbolCb(info.packetId, info.fecPayloadId.ssId, rawSymbol, alta)
|
|
740
|
+
} catch {
|
|
741
|
+
// Source-symbol consumer threw — never block MFU extraction on
|
|
742
|
+
// FEC-pipeline errors. The frame path stays viable; the consumer
|
|
743
|
+
// is responsible for its own error surfacing.
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const extraction = extractMFUPayload(packet, {
|
|
748
|
+
skipFecId: options?.skipFecPayloadId,
|
|
749
|
+
})
|
|
750
|
+
if (!extraction) return null
|
|
751
|
+
|
|
752
|
+
const { mpu, fragmentType, data, mfuDuHeader } = extraction
|
|
753
|
+
if (mpu.timedFlag !== 1) return null
|
|
754
|
+
const streamKey = this.#streamKey(info.packetId, options?.catalogTrackName)
|
|
755
|
+
|
|
756
|
+
if (fragmentType === 0) {
|
|
757
|
+
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0) return null
|
|
758
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true)
|
|
759
|
+
if (!resolver?.addInitialization(data)) return null
|
|
760
|
+
this.#initCb?.(data, info.packetId, options?.catalogTrackName)
|
|
761
|
+
return {
|
|
762
|
+
kind: 'init',
|
|
763
|
+
packet: info,
|
|
764
|
+
data,
|
|
765
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
766
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
767
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (fragmentType === 1) {
|
|
772
|
+
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0) return null
|
|
773
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, false)
|
|
774
|
+
if (!resolver) return null
|
|
775
|
+
const pendingBefore = resolver.pendingSampleCount
|
|
776
|
+
const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples
|
|
777
|
+
if (!resolver.addMovieFragment(mpu.mpuSequenceNumber, data, remaining)) return null
|
|
778
|
+
this.#pendingTimingSamples += resolver.pendingSampleCount - pendingBefore
|
|
779
|
+
return {
|
|
780
|
+
kind: 'fragment',
|
|
781
|
+
packet: info,
|
|
782
|
+
data,
|
|
783
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
784
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
785
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (fragmentType !== 2) return null
|
|
789
|
+
|
|
790
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, false)
|
|
791
|
+
if (!resolver) return null
|
|
792
|
+
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0) return null
|
|
793
|
+
if (mpu.fragmentationIndicator === 0 || mpu.fragmentationIndicator === 1) {
|
|
794
|
+
if (!mfuDuHeader || mfuDuHeader.movieFragmentSequenceNumber === 0 ||
|
|
795
|
+
mfuDuHeader.sampleNumber === 0) {
|
|
796
|
+
return null
|
|
797
|
+
}
|
|
798
|
+
const headTiming = resolver.sampleTiming(
|
|
799
|
+
mpu.mpuSequenceNumber,
|
|
800
|
+
mfuDuHeader.movieFragmentSequenceNumber,
|
|
801
|
+
mfuDuHeader.sampleNumber,
|
|
802
|
+
)
|
|
803
|
+
if (!headTiming) return null
|
|
804
|
+
// A data unit's authored base offset is its position within the sample
|
|
805
|
+
// (moqcast #37): 0 for a whole-sample DU, nonzero for one subsample DU
|
|
806
|
+
// among siblings. Sizes below are therefore relative to that base, not
|
|
807
|
+
// to the sample's start -- comparing a subsample DU against the whole
|
|
808
|
+
// sample's size is what dropped every partitioned 4K/8K frame.
|
|
809
|
+
const offset = mfuDuHeader.offset
|
|
810
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset >= headTiming.size) return null
|
|
811
|
+
const remaining = headTiming.size - offset
|
|
812
|
+
// FI=0 carries its data unit whole, in a single payload. Only a
|
|
813
|
+
// partitioned sample's FINAL data unit can be shorter than a full data
|
|
814
|
+
// unit -- every earlier one spans exactly 256 payloads and so is always
|
|
815
|
+
// fragmented -- which makes a whole-carried DU exactly the bytes left
|
|
816
|
+
// from its base. At offset 0 this is the pre-amendment check unchanged.
|
|
817
|
+
if (mpu.fragmentationIndicator === 0 && data.byteLength !== remaining) return null
|
|
818
|
+
if (mpu.fragmentationIndicator === 1 && data.byteLength >= remaining) return null
|
|
819
|
+
|
|
820
|
+
// Publish the authored size before this data unit can complete, so the
|
|
821
|
+
// combiner never has to guess whether an offset-zero DU is a whole
|
|
822
|
+
// sample. Registering can itself complete a sample whose data units all
|
|
823
|
+
// arrived while the size was still unknown; that sample is delivered
|
|
824
|
+
// through routeFrame like any other, and this packet's own fragment
|
|
825
|
+
// continues below.
|
|
826
|
+
const readyOnRegister = this.#sampleCombinerFor(streamKey).registerAuthoredSize(
|
|
827
|
+
options?.catalogTrackName,
|
|
828
|
+
mpu.mpuSequenceNumber,
|
|
829
|
+
mfuDuHeader.movieFragmentSequenceNumber,
|
|
830
|
+
mfuDuHeader.sampleNumber,
|
|
831
|
+
headTiming.size,
|
|
832
|
+
)
|
|
833
|
+
if (readyOnRegister) {
|
|
834
|
+
this.#emitCombinedSample(readyOnRegister, info, resolver, options?.recovered === true)
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
let assembled: ReassembledMFU | null
|
|
839
|
+
try {
|
|
840
|
+
assembled = this.#reassembler.addFragment({
|
|
841
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
842
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
843
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
844
|
+
data,
|
|
845
|
+
timestamp: info.timestamp,
|
|
846
|
+
rapFlag: info.rapFlag,
|
|
847
|
+
priority: mfuDuHeader?.priority,
|
|
848
|
+
sampleNumber: mfuDuHeader?.sampleNumber,
|
|
849
|
+
movieFragmentSequenceNumber: mfuDuHeader?.movieFragmentSequenceNumber,
|
|
850
|
+
sampleOffset: mfuDuHeader?.offset,
|
|
851
|
+
packetSequenceNumber: info.sequenceNumber,
|
|
852
|
+
recovered: options?.recovered === true,
|
|
853
|
+
source: options?.source,
|
|
854
|
+
deliveryProvenance: options?.deliveryProvenance,
|
|
855
|
+
catalogTrackName: options?.catalogTrackName,
|
|
856
|
+
packetId: info.packetId,
|
|
857
|
+
})
|
|
858
|
+
} catch (error) {
|
|
859
|
+
if (!(error instanceof MfuReassemblyError)) throw error
|
|
860
|
+
console.warn(
|
|
861
|
+
`[MmtpContainerParser] rejected MFU fragment: code=${error.code} ` +
|
|
862
|
+
`mpu=${mpu.mpuSequenceNumber} fi=${mpu.fragmentationIndicator} ` +
|
|
863
|
+
`counter=${mpu.fragmentCounter}`,
|
|
864
|
+
)
|
|
865
|
+
return {
|
|
866
|
+
kind: 'rejected',
|
|
867
|
+
packet: info,
|
|
868
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
869
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
870
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
871
|
+
error: { code: error.code, message: error.message },
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (!assembled) {
|
|
876
|
+
return {
|
|
877
|
+
kind: 'pending',
|
|
878
|
+
packet: info,
|
|
879
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
880
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
881
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const movieFragmentSequenceNumber = assembled.movieFragmentSequenceNumber
|
|
886
|
+
const sampleNumber = assembled.sampleNumber
|
|
887
|
+
if (movieFragmentSequenceNumber === undefined || sampleNumber === undefined) return null
|
|
888
|
+
// Resolve authored timing BEFORE the combiner sees this data unit. An
|
|
889
|
+
// assembled data unit whose sample identity has no authored timing is not
|
|
890
|
+
// a sample this stream declared, so it must be dropped outright -- letting
|
|
891
|
+
// it buffer would occupy a combiner slot and could evict a legitimate
|
|
892
|
+
// partitioned sample waiting on a sibling.
|
|
893
|
+
if (!resolver.sampleTiming(
|
|
894
|
+
assembled.mpuSequenceNumber,
|
|
895
|
+
movieFragmentSequenceNumber,
|
|
896
|
+
sampleNumber,
|
|
897
|
+
)) return null
|
|
898
|
+
if (assembled.mpuSequenceNumber !== mpu.mpuSequenceNumber) return null
|
|
899
|
+
|
|
900
|
+
// One reassembled data unit is not necessarily one whole sample. A
|
|
901
|
+
// partitioned sample completes only once every sibling data unit has
|
|
902
|
+
// arrived and they are byte-adjacent from zero to the authored size; an
|
|
903
|
+
// unpartitioned sample passes straight through with no buffering.
|
|
904
|
+
let combined: CombinedSample | null
|
|
905
|
+
try {
|
|
906
|
+
combined = this.#sampleCombinerFor(streamKey).addMfu(assembled)
|
|
907
|
+
} catch (error) {
|
|
908
|
+
if (!(error instanceof SampleCombineError)) throw error
|
|
909
|
+
console.warn(
|
|
910
|
+
`[MmtpContainerParser] rejected subsample data unit: code=${error.code} ` +
|
|
911
|
+
`mpu=${assembled.mpuSequenceNumber} sample=${sampleNumber} ` +
|
|
912
|
+
`offset=${assembled.sampleOffset ?? 0}`,
|
|
913
|
+
)
|
|
914
|
+
return {
|
|
915
|
+
kind: 'rejected',
|
|
916
|
+
packet: info,
|
|
917
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
918
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
919
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
920
|
+
error: { code: error.code, message: error.message },
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (!combined) {
|
|
924
|
+
return {
|
|
925
|
+
kind: 'pending',
|
|
926
|
+
packet: info,
|
|
927
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
928
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
929
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return this.#emitCombinedSample(combined, info, resolver, options?.recovered === true)
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Deliver one combined sample: resolve its authored timing, build frame
|
|
938
|
+
* metadata, retire the timing entry, and route it. Shared by the ordinary
|
|
939
|
+
* completion path and by the rarer completion that falls out of registering
|
|
940
|
+
* an authored size for data units that arrived before it.
|
|
941
|
+
*/
|
|
942
|
+
#emitCombinedSample(
|
|
943
|
+
combined: CombinedSample,
|
|
944
|
+
info: PacketInfo,
|
|
945
|
+
resolver: IsoBmffTimingResolver,
|
|
946
|
+
recovered: boolean,
|
|
947
|
+
): MmtpParserRecord | null {
|
|
948
|
+
const timing = resolver.sampleTiming(
|
|
949
|
+
combined.mpuSequenceNumber,
|
|
950
|
+
combined.movieFragmentSequenceNumber,
|
|
951
|
+
combined.sampleNumber,
|
|
952
|
+
)
|
|
953
|
+
if (!timing) return null
|
|
954
|
+
// The combiner already completes against the registered authored size;
|
|
955
|
+
// re-checking here keeps the unpartitioned passthrough path fail-closed
|
|
956
|
+
// on the same invariant rather than trusting it by construction.
|
|
957
|
+
if (combined.data.byteLength !== timing.size) return null
|
|
958
|
+
|
|
959
|
+
const meta = this.#buildFrameMeta(
|
|
960
|
+
combined,
|
|
961
|
+
info.packetId,
|
|
962
|
+
info.sequenceNumber,
|
|
963
|
+
timing,
|
|
964
|
+
recovered,
|
|
965
|
+
)
|
|
966
|
+
if (!meta) return null
|
|
967
|
+
if (!resolver.consumeSample(
|
|
968
|
+
combined.mpuSequenceNumber,
|
|
969
|
+
combined.movieFragmentSequenceNumber,
|
|
970
|
+
combined.sampleNumber,
|
|
971
|
+
timing,
|
|
972
|
+
)) return null
|
|
973
|
+
this.#pendingTimingSamples--
|
|
974
|
+
this.routeFrame(combined.data, meta)
|
|
975
|
+
return {
|
|
976
|
+
kind: 'frame',
|
|
977
|
+
packet: info,
|
|
978
|
+
data: combined.data,
|
|
979
|
+
meta,
|
|
980
|
+
mpuSequenceNumber: combined.mpuSequenceNumber,
|
|
981
|
+
fragmentCount: combined.fragmentCount,
|
|
982
|
+
rapFlag: combined.rapFlag,
|
|
983
|
+
...(combined.priority === undefined ? {} : { priority: combined.priority }),
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
onInit(cb: InitCallback): void {
|
|
988
|
+
this.#initCb = cb
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
onFrame(cb: FrameCallback): void {
|
|
992
|
+
this.#frameCb = cb
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** Register a per-packet FEC source-symbol callback. Fires for each MMTP
|
|
996
|
+
* source MPU packet that carries a Source FEC Payload ID (ISO 23008-1 §C.5.2).
|
|
997
|
+
*
|
|
998
|
+
* Consumer responsibilities:
|
|
999
|
+
* - Zero-pad `rawSymbol` to the FEC `symbolSize` if shorter (FEC config is
|
|
1000
|
+
* out-of-band — typically from the CMSF catalog's `fec.symbolSize`).
|
|
1001
|
+
* - Decide oversized handling (rawSymbol.length > symbolSize is not FEC-
|
|
1002
|
+
* decodable; consumer typically skips that block's source-feed).
|
|
1003
|
+
* - Look up the right FEC consumer (e.g. FecWorkerClient) by packetId.
|
|
1004
|
+
*
|
|
1005
|
+
* Designed for spec-clean reuse: Shaka, upstream moqtail, or any consumer
|
|
1006
|
+
* that uses MmtpContainerParser can wire FEC source delivery without
|
|
1007
|
+
* pulling in MulticastManager/MmtpRouter/FECManager. */
|
|
1008
|
+
onSourceSymbol(cb: SourceSymbolCallback): void {
|
|
1009
|
+
this.#sourceSymbolCb = cb
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
onTrackFrame(packetId: number, cb: FrameCallback): void {
|
|
1013
|
+
this.#trackFrameCbs.set(packetId, cb)
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
setTrack(packetId: number, info: TrackInfo): void {
|
|
1017
|
+
this.#validateReassemblyPolicy()
|
|
1018
|
+
for (const [key, entry] of this.#sequencers) {
|
|
1019
|
+
if (entry.packetId !== packetId) continue
|
|
1020
|
+
entry.sequencer.flush()
|
|
1021
|
+
this.#sequencers.delete(key)
|
|
1022
|
+
}
|
|
1023
|
+
for (const [key, state] of this.#timingStreams) {
|
|
1024
|
+
if (state.packetId === packetId) this.#dropTimingStream(key)
|
|
1025
|
+
}
|
|
1026
|
+
for (const key of [...this.#sampleCombiners.keys()]) {
|
|
1027
|
+
if ((JSON.parse(key) as [number, string | null])[0] === packetId) {
|
|
1028
|
+
this.#dropSampleCombiner(key)
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
for (const [key, gatedPacketId] of this.#reliableMoqStartupGate) {
|
|
1032
|
+
if (gatedPacketId === packetId) this.#reliableMoqStartupGate.delete(key)
|
|
1033
|
+
}
|
|
1034
|
+
this.#tracks.set(packetId, info)
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/** Drop one physical stream's ordering and MFU state for retained replay. */
|
|
1038
|
+
resetTrack(packetId: number, catalogTrackName: string): void {
|
|
1039
|
+
if (!catalogTrackName) {
|
|
1040
|
+
throw new Error('[MmtpContainerParser] catalogTrackName is required for track reset')
|
|
1041
|
+
}
|
|
1042
|
+
for (const [key, entry] of this.#sequencers) {
|
|
1043
|
+
if (key !== this.#streamKey(packetId, catalogTrackName)) continue
|
|
1044
|
+
entry.sequencer.discard()
|
|
1045
|
+
this.#sequencers.delete(key)
|
|
1046
|
+
}
|
|
1047
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
1048
|
+
this.#dropTimingStream(streamKey)
|
|
1049
|
+
this.#reliableMoqStartupGate.delete(streamKey)
|
|
1050
|
+
this.#reassembler.resetStream(catalogTrackName)
|
|
1051
|
+
// Drop half-combined samples too: a partitioned sample whose siblings
|
|
1052
|
+
// straddle the reset would otherwise complete against a stale authored
|
|
1053
|
+
// size and hand the decoder bytes from two different publisher epochs.
|
|
1054
|
+
this.#dropSampleCombiner(streamKey)
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
setMfuTimeoutMs(ms: number | null): void {
|
|
1058
|
+
if (ms !== null && ms <= 0) throw new Error('[MmtpContainerParser] mfuTimeoutMs must be > 0')
|
|
1059
|
+
this.#reassembler.timeoutMs = ms
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
setReassemblyPolicy(policy: MfuReassemblyPolicy): void {
|
|
1063
|
+
const timeoutMs = mfuReassemblyTimeoutMs(policy)
|
|
1064
|
+
this.#validateReassemblyPolicy(policy)
|
|
1065
|
+
this.#flushSequencers()
|
|
1066
|
+
this.#reassemblyPolicy = policy
|
|
1067
|
+
this.#reliableMoqStartupGate.clear()
|
|
1068
|
+
this.setMfuTimeoutMs(timeoutMs)
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
setReorderWindow(windowSize: number): void {
|
|
1072
|
+
if (!Number.isFinite(windowSize) || windowSize < 0) {
|
|
1073
|
+
throw new Error('[MmtpContainerParser] reorderWindow must be a finite number >= 0')
|
|
1074
|
+
}
|
|
1075
|
+
const nextWindow = Math.ceil(windowSize)
|
|
1076
|
+
this.#validateReassemblyPolicy(this.#reassemblyPolicy, nextWindow)
|
|
1077
|
+
this.#flushSequencers()
|
|
1078
|
+
this.#reorderWindow = nextWindow
|
|
1079
|
+
if (this.#reorderWindow <= 0) this.#sequencers.clear()
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/** Route an already-assembled frame through the same reorder/callback path
|
|
1083
|
+
* used by source packets. FEC recovery uses this to merge recovered frames
|
|
1084
|
+
* with direct source frames before the decoder sees them. */
|
|
1085
|
+
routeFrame(data: Uint8Array, meta: FrameMeta): void {
|
|
1086
|
+
const seq = meta.packetSequenceNumber
|
|
1087
|
+
const sequencer = this.#sequencerFor(meta.packetId, meta.trackType, meta.catalogTrackName)
|
|
1088
|
+
if (sequencer && seq !== undefined && Number.isFinite(seq)) {
|
|
1089
|
+
sequencer.push(data, meta, seq)
|
|
1090
|
+
return
|
|
1091
|
+
}
|
|
1092
|
+
if (this.#reassemblyPolicy?.transport === 'reliable-moq' &&
|
|
1093
|
+
meta.trackType === 'video' &&
|
|
1094
|
+
!this.#passReliableMoqStartupGate(meta)) {
|
|
1095
|
+
return
|
|
1096
|
+
}
|
|
1097
|
+
this.#emitFrame(data, meta)
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
1102
|
+
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
1103
|
+
* the same bounded resolver entry once the router accepts the recovered
|
|
1104
|
+
* sample. A missing entry is a fail-closed signal: callers must not replace
|
|
1105
|
+
* it with an MMTP send timestamp or cadence guess.
|
|
1106
|
+
*/
|
|
1107
|
+
consumeAuthoredTimingFor(
|
|
1108
|
+
packetId: number,
|
|
1109
|
+
mpuSequenceNumber: number,
|
|
1110
|
+
movieFragmentSequenceNumber: number | undefined,
|
|
1111
|
+
sampleNumber: number | undefined,
|
|
1112
|
+
catalogTrackName?: string,
|
|
1113
|
+
): AuthoredTiming | null {
|
|
1114
|
+
if (movieFragmentSequenceNumber === undefined || sampleNumber === undefined) return null
|
|
1115
|
+
const resolver = this.#timingResolver(
|
|
1116
|
+
this.#streamKey(packetId, catalogTrackName),
|
|
1117
|
+
packetId,
|
|
1118
|
+
false,
|
|
1119
|
+
)
|
|
1120
|
+
const timing = resolver?.sampleTiming(
|
|
1121
|
+
mpuSequenceNumber,
|
|
1122
|
+
movieFragmentSequenceNumber,
|
|
1123
|
+
sampleNumber,
|
|
1124
|
+
)
|
|
1125
|
+
if (!timing || !resolver || !resolver.consumeSample(
|
|
1126
|
+
mpuSequenceNumber,
|
|
1127
|
+
movieFragmentSequenceNumber,
|
|
1128
|
+
sampleNumber,
|
|
1129
|
+
timing,
|
|
1130
|
+
)) return null
|
|
1131
|
+
this.#pendingTimingSamples--
|
|
1132
|
+
if (this.#pendingTimingSamples < 0) {
|
|
1133
|
+
throw new Error('[MmtpContainerParser] pending timing sample accounting underflow')
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
decodeTime: timing.decodeTime,
|
|
1137
|
+
presentationTime: timing.presentationTime,
|
|
1138
|
+
durationTicks: timing.duration,
|
|
1139
|
+
mediaTimescale: timing.timescale,
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/** Order-independent reliable-moq startup gate: discard delta frames for a
|
|
1144
|
+
* stream until its first RAP is observed, without buffering/reordering. */
|
|
1145
|
+
#passReliableMoqStartupGate(meta: FrameMeta): boolean {
|
|
1146
|
+
const streamKey = this.#streamKey(meta.packetId, meta.catalogTrackName)
|
|
1147
|
+
if (meta.sourceDiscontinuity) this.#reliableMoqStartupGate.delete(streamKey)
|
|
1148
|
+
if (this.#reliableMoqStartupGate.has(streamKey)) return true
|
|
1149
|
+
if (!meta.keyframe) {
|
|
1150
|
+
this.#reorderStats.droppedBeforeStartupKeyframe++
|
|
1151
|
+
return false
|
|
1152
|
+
}
|
|
1153
|
+
this.#reliableMoqStartupGate.set(streamKey, meta.packetId)
|
|
1154
|
+
return true
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/** Emit all ordered frames currently buffered by the parser. Finite-stream
|
|
1158
|
+
* consumers may call this before dispose(); live cancellation should not. */
|
|
1159
|
+
flush(): void {
|
|
1160
|
+
if (this.#disposed) return
|
|
1161
|
+
this.#flushSequencers()
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
dispose(): void {
|
|
1165
|
+
this.#disposed = true
|
|
1166
|
+
for (const { sequencer } of this.#sequencers.values()) sequencer.discard()
|
|
1167
|
+
this.#sequencers.clear()
|
|
1168
|
+
this.#timingStreams.clear()
|
|
1169
|
+
this.#pendingTimingSamples = 0
|
|
1170
|
+
this.#reliableMoqStartupGate.clear()
|
|
1171
|
+
this.#reassembler.reset()
|
|
1172
|
+
this.#frameCb = null
|
|
1173
|
+
this.#initCb = null
|
|
1174
|
+
this.#trackFrameCbs.clear()
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// ── Private ─────────────────────────────────────────────────────
|
|
1178
|
+
|
|
1179
|
+
#timingResolver(
|
|
1180
|
+
streamKey: string,
|
|
1181
|
+
packetId: number,
|
|
1182
|
+
create: boolean,
|
|
1183
|
+
): IsoBmffTimingResolver | null {
|
|
1184
|
+
const existing = this.#timingStreams.get(streamKey)
|
|
1185
|
+
if (existing) return existing.resolver
|
|
1186
|
+
if (!create) return null
|
|
1187
|
+
const resolver = new IsoBmffTimingResolver(this.#maxPendingTimingSamples)
|
|
1188
|
+
this.#timingStreams.set(streamKey, { packetId, resolver })
|
|
1189
|
+
return resolver
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
#dropTimingStream(streamKey: string): void {
|
|
1193
|
+
const state = this.#timingStreams.get(streamKey)
|
|
1194
|
+
if (!state) return
|
|
1195
|
+
this.#timingStreams.delete(streamKey)
|
|
1196
|
+
this.#pendingTimingSamples -= state.resolver.pendingSampleCount
|
|
1197
|
+
if (this.#pendingTimingSamples < 0) {
|
|
1198
|
+
throw new Error('[MmtpContainerParser] pending timing sample accounting underflow')
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
#validateReassemblyPolicy(
|
|
1203
|
+
policy: MfuReassemblyPolicy | null = this.#reassemblyPolicy,
|
|
1204
|
+
reorderWindow = this.#reorderWindow,
|
|
1205
|
+
): void {
|
|
1206
|
+
if (!policy) return
|
|
1207
|
+
mmtpFrameReorderWindowFrames(policy, reorderWindow)
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
#buildFrameMeta(
|
|
1211
|
+
assembled: CombinedSample,
|
|
1212
|
+
packetId: number,
|
|
1213
|
+
seq: number,
|
|
1214
|
+
timing: IsoBmffSampleTiming,
|
|
1215
|
+
recovered = false,
|
|
1216
|
+
): FrameMeta | null {
|
|
1217
|
+
const { data, timestamp: mmtpTimestamp, rapFlag, priority } = assembled
|
|
1218
|
+
|
|
1219
|
+
if (isFmp4Box(data)) return null
|
|
1220
|
+
const presentationTimestamp = mediaTicksToMicroseconds(
|
|
1221
|
+
timing.presentationTime,
|
|
1222
|
+
timing.timescale,
|
|
1223
|
+
)
|
|
1224
|
+
const decodeTimestamp = mediaTicksToMicroseconds(timing.decodeTime, timing.timescale)
|
|
1225
|
+
const duration = mediaTicksToMicroseconds(BigInt(timing.duration), timing.timescale)
|
|
1226
|
+
if (presentationTimestamp === null || decodeTimestamp === null || duration === null ||
|
|
1227
|
+
duration <= 0) {
|
|
1228
|
+
return null
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
let track = this.#tracks.get(packetId)
|
|
1232
|
+
if (!track && this.#defaultTrack) {
|
|
1233
|
+
track = this.#defaultTrack
|
|
1234
|
+
this.#tracks.set(packetId, track)
|
|
1235
|
+
}
|
|
1236
|
+
const codec = track?.codec ?? ''
|
|
1237
|
+
|
|
1238
|
+
const keyframe = MmtpContainerParser.isKeyframe(data, isMfuKeyframeMetadata(rapFlag, priority), codec)
|
|
1239
|
+
|
|
1240
|
+
const meta: FrameMeta = {
|
|
1241
|
+
timestamp: presentationTimestamp,
|
|
1242
|
+
decodeTimestamp,
|
|
1243
|
+
duration,
|
|
1244
|
+
decodeTime: timing.decodeTime,
|
|
1245
|
+
presentationTime: timing.presentationTime,
|
|
1246
|
+
durationTicks: timing.duration,
|
|
1247
|
+
mediaTimescale: timing.timescale,
|
|
1248
|
+
authoredTiming: {
|
|
1249
|
+
decodeTime: timing.decodeTime,
|
|
1250
|
+
presentationTime: timing.presentationTime,
|
|
1251
|
+
durationTicks: timing.duration,
|
|
1252
|
+
mediaTimescale: timing.timescale,
|
|
1253
|
+
},
|
|
1254
|
+
keyframe,
|
|
1255
|
+
packetId,
|
|
1256
|
+
trackType: track?.type ?? null,
|
|
1257
|
+
packetSequenceNumber: seq,
|
|
1258
|
+
...(assembled.packetSequenceNumbers === undefined
|
|
1259
|
+
? {}
|
|
1260
|
+
: { packetSequenceNumbers: assembled.packetSequenceNumbers }),
|
|
1261
|
+
...(assembled.repairedPacketSequenceNumbers === undefined
|
|
1262
|
+
? {}
|
|
1263
|
+
: { repairedPacketSequenceNumbers: assembled.repairedPacketSequenceNumbers }),
|
|
1264
|
+
...(assembled.catalogTrackName === undefined
|
|
1265
|
+
? {}
|
|
1266
|
+
: { catalogTrackName: assembled.catalogTrackName }),
|
|
1267
|
+
mmtpTimestamp,
|
|
1268
|
+
mpuSequenceNumber: assembled.mpuSequenceNumber,
|
|
1269
|
+
...(assembled.movieFragmentSequenceNumber === undefined
|
|
1270
|
+
? {}
|
|
1271
|
+
: { movieFragmentSequenceNumber: assembled.movieFragmentSequenceNumber }),
|
|
1272
|
+
...(assembled.sampleNumber === undefined ? {} : { sampleNumber: assembled.sampleNumber }),
|
|
1273
|
+
fragmentCount: assembled.fragmentCount,
|
|
1274
|
+
rapFlag,
|
|
1275
|
+
...(priority === undefined ? {} : { priority }),
|
|
1276
|
+
...(assembled.source === undefined ? {} : { source: assembled.source }),
|
|
1277
|
+
deliveryProvenance: assembled.deliveryProvenance,
|
|
1278
|
+
}
|
|
1279
|
+
if (recovered || (assembled.repairedPacketSequenceNumbers?.length ?? 0) > 0) {
|
|
1280
|
+
meta.recovered = true
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
return meta
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
#sequencerFor(
|
|
1287
|
+
packetId: number,
|
|
1288
|
+
_trackType: FrameMeta['trackType'],
|
|
1289
|
+
catalogTrackName?: string,
|
|
1290
|
+
): FrameSequencer | null {
|
|
1291
|
+
const windowSize = this.#reassemblyPolicy
|
|
1292
|
+
? mmtpFrameReorderWindowFrames(this.#reassemblyPolicy, this.#reorderWindow)
|
|
1293
|
+
: this.#reorderWindow
|
|
1294
|
+
if (windowSize <= 0) return null
|
|
1295
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
1296
|
+
let sequencer = this.#sequencers.get(streamKey)?.sequencer
|
|
1297
|
+
if (!sequencer) {
|
|
1298
|
+
sequencer = new FrameSequencer(windowSize, (data, meta) => {
|
|
1299
|
+
this.#emitFrame(data, meta)
|
|
1300
|
+
}, (stat, count = 1) => {
|
|
1301
|
+
this.#reorderStats[stat] += count
|
|
1302
|
+
})
|
|
1303
|
+
this.#sequencers.set(streamKey, { packetId, sequencer })
|
|
1304
|
+
}
|
|
1305
|
+
return sequencer
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
#flushSequencers(): void {
|
|
1309
|
+
for (const { sequencer } of this.#sequencers.values()) sequencer.flush()
|
|
1310
|
+
this.#sequencers.clear()
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/** Authored presentation timestamp of the last emitted frame, in microseconds. */
|
|
1314
|
+
lastFrameTimestamp: number | undefined = undefined
|
|
1315
|
+
|
|
1316
|
+
#emitFrame(data: Uint8Array, meta: FrameMeta): void {
|
|
1317
|
+
this.lastFrameTimestamp = meta.timestamp
|
|
1318
|
+
const trackCb = this.#trackFrameCbs.get(meta.packetId)
|
|
1319
|
+
trackCb?.(data, meta)
|
|
1320
|
+
this.#frameCb?.(data, meta)
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// ── Static utilities ──────────────────────────────────────────
|
|
1324
|
+
|
|
1325
|
+
static isKeyframe(data: Uint8Array, rapFlag: boolean, codec = ''): boolean {
|
|
1326
|
+
const payloadKeyframe = MmtpContainerParser.#scanForKeyframe(data, codec)
|
|
1327
|
+
if (payloadKeyframe !== undefined) return payloadKeyframe
|
|
1328
|
+
return rapFlag
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
static #scanForKeyframe(data: Uint8Array, codec: string): boolean | undefined {
|
|
1332
|
+
const c = codec.toLowerCase()
|
|
1333
|
+
if (data.length === 0) return undefined
|
|
1334
|
+
|
|
1335
|
+
if (c.startsWith('av01')) {
|
|
1336
|
+
return scanAv1ObusForKeyframe(data)
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (c.startsWith('vp09') || c.startsWith('vp8')) {
|
|
1340
|
+
return (data[0]! & 0x04) === 0
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const isHevc = c.startsWith('hev1') || c.startsWith('hvc1')
|
|
1344
|
+
if (isHevc) {
|
|
1345
|
+
if (data.length < 5) return undefined
|
|
1346
|
+
if (startsWithAnnexB(data)) {
|
|
1347
|
+
return MmtpContainerParser.#scanAnnexBForKeyframe(data, true)
|
|
1348
|
+
}
|
|
1349
|
+
return MmtpContainerParser.#scanLengthPrefixedHevcForKeyframe(data)
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
if (c === '' || c.startsWith('avc')) {
|
|
1353
|
+
return classifyH264Keyframe(data, { allowPartialAvcc: true })
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
return undefined
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
static #scanLengthPrefixedHevcForKeyframe(data: Uint8Array): boolean | undefined {
|
|
1360
|
+
let sawNal = false
|
|
1361
|
+
let offset = 0
|
|
1362
|
+
while (offset + 4 < data.length) {
|
|
1363
|
+
const nalLen =
|
|
1364
|
+
(data[offset]! << 24) |
|
|
1365
|
+
(data[offset + 1]! << 16) |
|
|
1366
|
+
(data[offset + 2]! << 8) |
|
|
1367
|
+
data[offset + 3]!
|
|
1368
|
+
offset += 4
|
|
1369
|
+
if (nalLen <= 0) return sawNal ? false : undefined
|
|
1370
|
+
// The router's sync probe may see only the first fragment of a large
|
|
1371
|
+
// length-prefixed NAL. The length can exceed this packet, but the NAL
|
|
1372
|
+
// header is still enough to classify IRAP/IDR and open the sync gate.
|
|
1373
|
+
if (offset >= data.length) return sawNal ? false : undefined
|
|
1374
|
+
sawNal = true
|
|
1375
|
+
const hevcType = (data[offset]! >> 1) & 0x3f
|
|
1376
|
+
if (hevcType >= 16 && hevcType <= 21) return true
|
|
1377
|
+
if (offset + nalLen > data.length) break
|
|
1378
|
+
offset += nalLen
|
|
1379
|
+
}
|
|
1380
|
+
return sawNal ? false : undefined
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
static #scanAnnexBForKeyframe(data: Uint8Array, isHevc: boolean): boolean | undefined {
|
|
1384
|
+
let offset = 0
|
|
1385
|
+
let sawNal = false
|
|
1386
|
+
while (offset < data.length) {
|
|
1387
|
+
const start = findAnnexBStart(data, offset)
|
|
1388
|
+
if (!start) return sawNal ? false : undefined
|
|
1389
|
+
const headerOffset = start.offset + start.length
|
|
1390
|
+
if (headerOffset >= data.length) return sawNal ? false : undefined
|
|
1391
|
+
sawNal = true
|
|
1392
|
+
if (isHevc) {
|
|
1393
|
+
if (headerOffset + 1 >= data.length) return sawNal ? false : undefined
|
|
1394
|
+
const hevcType = (data[headerOffset]! >> 1) & 0x3f
|
|
1395
|
+
if (hevcType >= 16 && hevcType <= 21) return true
|
|
1396
|
+
} else {
|
|
1397
|
+
const nalType = data[headerOffset]! & 0x1f
|
|
1398
|
+
if (nalType === 5) return true
|
|
1399
|
+
}
|
|
1400
|
+
offset = headerOffset + 1
|
|
1401
|
+
}
|
|
1402
|
+
return sawNal ? false : undefined
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function scanAv1ObusForKeyframe(data: Uint8Array): boolean | undefined {
|
|
1407
|
+
let offset = 0
|
|
1408
|
+
let sawSequenceHeader = false
|
|
1409
|
+
let sawFrame = false
|
|
1410
|
+
|
|
1411
|
+
while (offset < data.length) {
|
|
1412
|
+
const obu = readAv1Obu(data, offset)
|
|
1413
|
+
if (!obu) return sawFrame ? false : undefined
|
|
1414
|
+
offset = obu.nextOffset
|
|
1415
|
+
|
|
1416
|
+
if (obu.type === 1) {
|
|
1417
|
+
sawSequenceHeader = true
|
|
1418
|
+
continue
|
|
1419
|
+
}
|
|
1420
|
+
if (obu.type !== 3 && obu.type !== 6) continue
|
|
1421
|
+
|
|
1422
|
+
sawFrame = true
|
|
1423
|
+
if (!sawSequenceHeader) {
|
|
1424
|
+
continue
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const keyframe = readAv1FrameHeaderKeyframe(data.subarray(obu.payloadOffset, obu.payloadEnd))
|
|
1428
|
+
if (keyframe !== undefined) return keyframe
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
return sawFrame ? false : undefined
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function readAv1Obu(
|
|
1435
|
+
data: Uint8Array,
|
|
1436
|
+
offset: number,
|
|
1437
|
+
): { type: number; payloadOffset: number; payloadEnd: number; nextOffset: number } | null {
|
|
1438
|
+
if (offset >= data.length) return null
|
|
1439
|
+
const header = data[offset]!
|
|
1440
|
+
if ((header & 0x80) !== 0 || (header & 0x01) !== 0) return null
|
|
1441
|
+
|
|
1442
|
+
const type = (header >> 3) & 0x0f
|
|
1443
|
+
const extensionFlag = (header & 0x04) !== 0
|
|
1444
|
+
const hasSizeField = (header & 0x02) !== 0
|
|
1445
|
+
let cursor = offset + 1
|
|
1446
|
+
if (extensionFlag) cursor += 1
|
|
1447
|
+
if (cursor > data.length) return null
|
|
1448
|
+
|
|
1449
|
+
let payloadSize = data.length - cursor
|
|
1450
|
+
if (hasSizeField) {
|
|
1451
|
+
const leb = readLeb128(data, cursor)
|
|
1452
|
+
if (!leb) return null
|
|
1453
|
+
payloadSize = leb.value
|
|
1454
|
+
cursor = leb.nextOffset
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
const payloadEnd = cursor + payloadSize
|
|
1458
|
+
if (payloadEnd > data.length) return null
|
|
1459
|
+
return { type, payloadOffset: cursor, payloadEnd, nextOffset: payloadEnd }
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function readLeb128(data: Uint8Array, offset: number): { value: number; nextOffset: number } | null {
|
|
1463
|
+
let value = 0
|
|
1464
|
+
for (let i = 0; i < 8; i++) {
|
|
1465
|
+
if (offset + i >= data.length) return null
|
|
1466
|
+
const byte = data[offset + i]!
|
|
1467
|
+
value += (byte & 0x7f) * (2 ** (i * 7))
|
|
1468
|
+
if ((byte & 0x80) === 0) return { value, nextOffset: offset + i + 1 }
|
|
1469
|
+
}
|
|
1470
|
+
return null
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function readAv1FrameHeaderKeyframe(payload: Uint8Array): boolean | undefined {
|
|
1474
|
+
if (payload.length === 0) return undefined
|
|
1475
|
+
const first = payload[0]!
|
|
1476
|
+
const showExistingFrame = (first & 0x80) !== 0
|
|
1477
|
+
if (showExistingFrame) return false
|
|
1478
|
+
const frameType = (first >> 5) & 0x03
|
|
1479
|
+
return frameType === 0
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function startsWithAnnexB(data: Uint8Array): boolean {
|
|
1483
|
+
return (
|
|
1484
|
+
(data.length >= 4 && data[0] === 0 && data[1] === 0 && data[2] === 1) ||
|
|
1485
|
+
(data.length >= 5 && data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 1)
|
|
1486
|
+
)
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function findAnnexBStart(data: Uint8Array, offset: number): { offset: number; length: 3 | 4 } | null {
|
|
1490
|
+
for (let i = offset; i + 3 <= data.length; i++) {
|
|
1491
|
+
if (data[i] !== 0 || data[i + 1] !== 0) continue
|
|
1492
|
+
if (data[i + 2] === 1) return { offset: i, length: 3 }
|
|
1493
|
+
if (i + 3 < data.length && data[i + 2] === 0 && data[i + 3] === 1) {
|
|
1494
|
+
return { offset: i, length: 4 }
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return null
|
|
1498
|
+
}
|