@blockcast/mmt-container 0.1.0-main.0399efef1f07
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 +152 -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 +20087 -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 +873 -0
- package/dist/carp-catalog.js.map +7 -0
- package/dist/catalog-fec-params.d.ts +202 -0
- package/dist/catalog-fec-params.d.ts.map +1 -0
- package/dist/catalog-fec-params.js +568 -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 +217 -0
- package/dist/cmaf-block-assembler.d.ts.map +1 -0
- package/dist/cmaf-block-assembler.js +1769 -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 +248 -0
- package/dist/codec-utils.d.ts.map +1 -0
- package/dist/codec-utils.js +1268 -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 +307 -0
- package/dist/fec-manager.d.ts.map +1 -0
- package/dist/fec-manager.js +1530 -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 +88 -0
- package/dist/fec-worker-setup.d.ts.map +1 -0
- package/dist/fec-worker-setup.js +614 -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 +380 -0
- package/dist/index.js.map +7 -0
- package/dist/iso-bmff-timing.d.ts +78 -0
- package/dist/iso-bmff-timing.d.ts.map +1 -0
- package/dist/iso-bmff-timing.js +618 -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 +1285 -0
- package/dist/manifest-auth-plan.js.map +7 -0
- package/dist/media-router.d.ts +251 -0
- package/dist/media-router.d.ts.map +1 -0
- package/dist/media-router.js +624 -0
- package/dist/media-router.js.map +7 -0
- package/dist/mfu-reassembler.d.ts +396 -0
- package/dist/mfu-reassembler.d.ts.map +1 -0
- package/dist/mfu-reassembler.js +3085 -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 +434 -0
- package/dist/mmtp-container-parser.d.ts.map +1 -0
- package/dist/mmtp-container-parser.js +6164 -0
- package/dist/mmtp-container-parser.js.map +7 -0
- package/dist/mmtp-container-parser.no-modules.js +8999 -0
- package/dist/mmtp-container-parser.no-modules.js.map +7 -0
- package/dist/mmtp-decode-pipeline.d.ts +132 -0
- package/dist/mmtp-decode-pipeline.d.ts.map +1 -0
- package/dist/mmtp-decode-pipeline.js +6413 -0
- package/dist/mmtp-decode-pipeline.js.map +7 -0
- package/dist/mmtp-router.d.ts +390 -0
- package/dist/mmtp-router.d.ts.map +1 -0
- package/dist/mmtp-router.js +9242 -0
- package/dist/mmtp-router.js.map +7 -0
- package/dist/mmtp-to-cmaf.d.ts +181 -0
- package/dist/mmtp-to-cmaf.d.ts.map +1 -0
- package/dist/mmtp-to-cmaf.js +802 -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 +369 -0
- package/dist/multicast-manager.d.ts.map +1 -0
- package/dist/multicast-manager.js +13400 -0
- package/dist/multicast-manager.js.map +7 -0
- package/dist/multicast-transport.d.ts +61 -0
- package/dist/multicast-transport.d.ts.map +1 -0
- package/dist/multicast-transport.js +1665 -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 +8965 -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 +127 -0
- package/dist/repair-fetch.js.map +7 -0
- package/dist/repair-layers.d.ts +141 -0
- package/dist/repair-layers.d.ts.map +1 -0
- package/dist/repair-layers.js +303 -0
- package/dist/repair-layers.js.map +7 -0
- package/dist/sample-combiner.d.ts +212 -0
- package/dist/sample-combiner.d.ts.map +1 -0
- package/dist/sample-combiner.js +616 -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 +1238 -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 +398 -0
- package/dist/track-session.d.ts.map +1 -0
- package/dist/track-session.js +767 -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 +92 -0
- package/dist/wasm-mfu-reassembler.d.ts.map +1 -0
- package/dist/wasm-mfu-reassembler.js +2139 -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 +6418 -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 +87 -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 +279 -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 +319 -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 +276 -0
- package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-base/dist/mmtp.js +645 -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 +899 -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 +82 -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 +296 -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 +172 -0
- package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts.map +1 -0
- package/node_modules/@blockcast/mmt-fec/dist/oti.js +254 -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 +276 -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 +645 -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 +899 -0
- package/node_modules/@blockcast/mmt-fec/package.json +79 -0
- package/node_modules/@blockcast/mmt-fec/src/block-manager.ts +371 -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 +317 -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 +99 -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 +492 -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 +669 -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 +360 -0
- package/src/align.ts +239 -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 +845 -0
- package/src/catalog-fec-params.ts +535 -0
- package/src/catalog-network-source.ts +49 -0
- package/src/catalog-validator.ts +34 -0
- package/src/cmaf-block-assembler.ts +1167 -0
- package/src/cmaf-group-batcher.ts +104 -0
- package/src/cmcd-producer.ts +334 -0
- package/src/codec-utils.ts +1611 -0
- package/src/fec-codec.ts +61 -0
- package/src/fec-manager.ts +2081 -0
- package/src/fec-starvation-detector.ts +128 -0
- package/src/fec-worker-setup.ts +213 -0
- package/src/fmp4-completion.ts +47 -0
- package/src/fmp4-reader.ts +112 -0
- package/src/index.ts +486 -0
- package/src/iso-bmff-timing.ts +813 -0
- package/src/loc.ts +181 -0
- package/src/manifest-auth-plan.ts +107 -0
- package/src/media-router.ts +795 -0
- package/src/mfu-reassembler.ts +3741 -0
- package/src/mfu-reassembly-policy.ts +134 -0
- package/src/mmtp-container-parser.ts +1836 -0
- package/src/mmtp-decode-pipeline.ts +389 -0
- package/src/mmtp-router.ts +3136 -0
- package/src/mmtp-to-cmaf.ts +1024 -0
- package/src/multicast-auth-catalog.ts +340 -0
- package/src/multicast-input.ts +439 -0
- package/src/multicast-manager.ts +1720 -0
- package/src/multicast-transport.ts +321 -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 +238 -0
- package/src/repair-layers.ts +490 -0
- package/src/sample-combiner.ts +938 -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 +994 -0
- package/src/transport-pool.ts +128 -0
- package/src/wasm-mfu-reassembler.ts +389 -0
- package/src/wasm-mmtp-container-parser.ts +23 -0
- package/test-vectors/align.json +199 -0
- package/test-vectors/catalog/README.md +40 -0
- package/test-vectors/catalog/negative/fec-enhancement-repair-removed.json +27 -0
- package/test-vectors/catalog/negative/fec-repair-layer-geometry.json +32 -0
- package/test-vectors/catalog/negative/fec-repair-layer-missing-symbols.json +30 -0
- package/test-vectors/catalog/negative/fec-repair-multiple-depends.json +31 -0
- package/test-vectors/catalog/negative/fec-repair-overlay-with-layer.json +33 -0
- package/test-vectors/catalog/negative/fec-repair-priority-out-of-band.json +23 -0
- package/test-vectors/catalog/negative/fec-repair-source-symbols-without-scope.json +32 -0
- package/test-vectors/catalog/negative/legacy-selection-params.json +15 -0
- package/test-vectors/catalog/negative/media-track-repair-layer.json +16 -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-source-symbols-over-max.json +23 -0
- package/test-vectors/catalog/negative/raptorq-unaligned-symbol.json +26 -0
- package/test-vectors/catalog/negative/repair-track-legacy-shape.json +19 -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-layered-repair.json +104 -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/source-contiguity-v1.json +131 -0
- package/test-vectors/subsample-placement-v1.json +230 -0
|
@@ -0,0 +1,1836 @@
|
|
|
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
|
+
isMfuReassemblyError,
|
|
14
|
+
type MfuReassemblerBackend,
|
|
15
|
+
type MmtpSourcePath,
|
|
16
|
+
type MmtpDeliveryProvenance,
|
|
17
|
+
type ReassembledMFU,
|
|
18
|
+
} from './mfu-reassembler.js'
|
|
19
|
+
import {
|
|
20
|
+
SampleCombiner,
|
|
21
|
+
SampleCombineError,
|
|
22
|
+
type AbandonedSample,
|
|
23
|
+
type CombinedSample,
|
|
24
|
+
type SampleCombinerOptions,
|
|
25
|
+
type SampleCombinerStats,
|
|
26
|
+
} from './sample-combiner.js'
|
|
27
|
+
import {
|
|
28
|
+
classifyH264Keyframe,
|
|
29
|
+
mmtpTimestampToMicros,
|
|
30
|
+
normalizeMmtpInitSegment,
|
|
31
|
+
} from './codec-utils.js'
|
|
32
|
+
import {
|
|
33
|
+
mmtpFrameReorderWindowFrames,
|
|
34
|
+
mfuReassemblyTimeoutMs,
|
|
35
|
+
type MfuReassemblyPolicy,
|
|
36
|
+
} from './mfu-reassembly-policy.js'
|
|
37
|
+
import {
|
|
38
|
+
IsoBmffTimingResolver,
|
|
39
|
+
mediaTicksToMicroseconds,
|
|
40
|
+
type IsoBmffSampleTiming,
|
|
41
|
+
} from './iso-bmff-timing.js'
|
|
42
|
+
import type { AuthoredTiming } from './authored-timing.js'
|
|
43
|
+
|
|
44
|
+
// ── Types ───────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export interface TrackInfo {
|
|
47
|
+
type: 'video' | 'audio'
|
|
48
|
+
codec: string
|
|
49
|
+
/** Catalog/in-band frame cadence available to downstream media stages. */
|
|
50
|
+
framerate?: number
|
|
51
|
+
/** Catalog MMTP group duration available to downstream media stages. */
|
|
52
|
+
groupDurationMs?: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface MmtpParserConfig {
|
|
56
|
+
tracks?: Map<number, TrackInfo>
|
|
57
|
+
reorderWindow?: number
|
|
58
|
+
/** MFU reassembly timeout (ms). Derive from FEC interleaveDepthMs or frame duration.
|
|
59
|
+
* Pass null for live clock-free operation; memory remains bounded by maxBuffered.
|
|
60
|
+
* Defaults to 1000ms if not provided. */
|
|
61
|
+
mfuTimeoutMs?: number | null
|
|
62
|
+
/** Explicit transport policy. Reliable MoQ is clock-free; datagrams require
|
|
63
|
+
* a catalog/in-band-derived deadline. Mutually exclusive with mfuTimeoutMs. */
|
|
64
|
+
reassemblyPolicy?: MfuReassemblyPolicy
|
|
65
|
+
/** Max concurrent active MFUs. A new first fragment is rejected at the bound. */
|
|
66
|
+
maxBufferedMfus?: number
|
|
67
|
+
/**
|
|
68
|
+
* Max subsample data units held for one partitioned sample (moqcast #37).
|
|
69
|
+
* A sample needs `ceil(sampleSize / (256 * maxPayload))` data units; at the
|
|
70
|
+
* ~1300-byte payloads this producer emits, one data unit spans ~327 KB, so
|
|
71
|
+
* the default admits a ~5 MB sample — beyond any 8K intra frame — while
|
|
72
|
+
* still bounding the per-insert overlap scan. Raise it only alongside a
|
|
73
|
+
* larger authored frame size, never to paper over a lost data unit.
|
|
74
|
+
*/
|
|
75
|
+
maxSubsampleSegments?: number
|
|
76
|
+
/** Alternate backend. The TypeScript and Rust/WASM implementations enforce
|
|
77
|
+
* the same authored active-DU contract. */
|
|
78
|
+
reassembler?: MfuReassemblerBackend
|
|
79
|
+
/** Default track info for auto-discovered packetIds.
|
|
80
|
+
* When the parser receives an MMTP packet with an unregistered packetId,
|
|
81
|
+
* it creates a new track entry using this info. Required for correct
|
|
82
|
+
* keyframe detection with HEVC/AV1/VP9 (without it, defaults to H.264). */
|
|
83
|
+
defaultTrack?: TrackInfo
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Structured result from parseHeader(). Used by MmtpRouter for two-phase parsing:
|
|
87
|
+
* 1. parseHeader() → get packetId + fecPayloadId for FEC feeding
|
|
88
|
+
* 2. feed() → MFU extraction, reassembly, keyframe detection, emit */
|
|
89
|
+
export interface PacketInfo {
|
|
90
|
+
packetId: number
|
|
91
|
+
packetType: number
|
|
92
|
+
timestamp: bigint
|
|
93
|
+
/** MMTP timestamp converted to monotonic microseconds for pacing/sync users. */
|
|
94
|
+
timestampUs: number
|
|
95
|
+
sequenceNumber: number
|
|
96
|
+
rapFlag: boolean
|
|
97
|
+
fecType: number
|
|
98
|
+
/** FEC source symbol ID (from 4-byte FEC Payload ID at packet end). Null if no FEC. */
|
|
99
|
+
fecPayloadId: { ssId: number } | null
|
|
100
|
+
/** True if this is a repair packet (packetType=0x03). */
|
|
101
|
+
isRepair: boolean
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface FrameMeta {
|
|
105
|
+
/** Authored ISO-BMFF presentation time converted to microseconds. */
|
|
106
|
+
timestamp: number
|
|
107
|
+
/** Authored ISO-BMFF decode time converted to microseconds. */
|
|
108
|
+
decodeTimestamp?: number
|
|
109
|
+
/** Authored ISO-BMFF sample duration converted to microseconds. */
|
|
110
|
+
duration?: number
|
|
111
|
+
/** Exact authored decode time in mediaTimescale units. */
|
|
112
|
+
decodeTime?: bigint
|
|
113
|
+
/** Exact authored presentation time in mediaTimescale units. */
|
|
114
|
+
presentationTime?: bigint
|
|
115
|
+
/** Exact authored duration in mediaTimescale units. */
|
|
116
|
+
durationTicks?: number
|
|
117
|
+
/** mdhd media timescale for the exact timing fields. */
|
|
118
|
+
mediaTimescale?: number
|
|
119
|
+
/** Exact authored ISO-BMFF timing carried without narrowing to microseconds. */
|
|
120
|
+
authoredTiming?: AuthoredTiming
|
|
121
|
+
keyframe: boolean
|
|
122
|
+
packetId: number
|
|
123
|
+
trackType: 'video' | 'audio' | null
|
|
124
|
+
packetSequenceNumber?: number
|
|
125
|
+
/** Every authoritative MMTP source-packet sequence that formed this frame. */
|
|
126
|
+
packetSequenceNumbers?: number[]
|
|
127
|
+
/** Subset of packetSequenceNumbers reconstructed by FEC. */
|
|
128
|
+
repairedPacketSequenceNumbers?: number[]
|
|
129
|
+
/** Immutable catalog identity supplied by the owning ingress subscription. */
|
|
130
|
+
catalogTrackName?: string
|
|
131
|
+
/** Raw MMTP short-NTP timestamp retained for adapters that own the timeline. */
|
|
132
|
+
mmtpTimestamp?: bigint
|
|
133
|
+
mpuSequenceNumber?: number
|
|
134
|
+
movieFragmentSequenceNumber?: number
|
|
135
|
+
sampleNumber?: number
|
|
136
|
+
fragmentCount?: number
|
|
137
|
+
rapFlag?: boolean
|
|
138
|
+
priority?: number
|
|
139
|
+
recovered?: boolean
|
|
140
|
+
/** Starts a new publisher/source epoch for ordering and timestamp state. */
|
|
141
|
+
sourceDiscontinuity?: boolean
|
|
142
|
+
/** Source of the accepted FI=1/FI=0 frame path. Absent for recovery. */
|
|
143
|
+
source?: MmtpSourcePath
|
|
144
|
+
/**
|
|
145
|
+
* Strict whole-frame delivery provenance. Unlike `source`, which names the
|
|
146
|
+
* head fragment's transport for routing, this is folded over every
|
|
147
|
+
* contributing fragment (and, for FEC-recovered frames, over the transports
|
|
148
|
+
* that supplied the block's symbols). Only 'multicast' is evidence of
|
|
149
|
+
* multicast-exclusive delivery. Absent means no producer on this path
|
|
150
|
+
* recorded provenance — consumers MUST treat that as unproven, never as
|
|
151
|
+
* multicast.
|
|
152
|
+
*/
|
|
153
|
+
deliveryProvenance?: MmtpDeliveryProvenance
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Typed ISO-BMFF timing-sample budget counters. See `timingBudgetStats`. */
|
|
157
|
+
export interface MmtpTimingBudgetStats {
|
|
158
|
+
/** Slots currently charged to declared-but-undelivered samples. */
|
|
159
|
+
pending: number
|
|
160
|
+
/** Total slots available; `pending === capacity` rejects every new moof. */
|
|
161
|
+
capacity: number
|
|
162
|
+
/** Slots returned by samples the combiner abandoned (evicted or timed out). */
|
|
163
|
+
reclaimed: number
|
|
164
|
+
/**
|
|
165
|
+
* Slots reclaimed by window displacement — samples no component ever saw,
|
|
166
|
+
* so no abandonment event could be raised for them. Separate from
|
|
167
|
+
* `reclaimed` because the owners differ: `reclaimed` is the combiner giving
|
|
168
|
+
* up, `evicted` is loss that was never observable at all.
|
|
169
|
+
*/
|
|
170
|
+
evicted: number
|
|
171
|
+
/** Movie fragments refused because the budget had no room for their samples. */
|
|
172
|
+
rejectedMovieFragments: number
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface MmtpReorderStats {
|
|
176
|
+
droppedBeforeStartupKeyframe: number
|
|
177
|
+
droppedIncompleteStartupInterval: number
|
|
178
|
+
droppedStale: number
|
|
179
|
+
forcedWindowFlushes: number
|
|
180
|
+
droppedOnLiveDispose: number
|
|
181
|
+
publisherResets: number
|
|
182
|
+
droppedOnPublisherReset: number
|
|
183
|
+
bufferedFrames: number
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface MmtpReorderFrameDiagnostics {
|
|
187
|
+
timestampUs: number
|
|
188
|
+
keyframe: boolean
|
|
189
|
+
sequenceNumber: number
|
|
190
|
+
packetSequenceNumber?: number
|
|
191
|
+
movieFragmentSequenceNumber?: number
|
|
192
|
+
sampleNumber?: number
|
|
193
|
+
fragmentCount?: number
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface MmtpReorderStreamDiagnostics {
|
|
197
|
+
packetId: number
|
|
198
|
+
bufferedFrames: number
|
|
199
|
+
lastEmitted: MmtpReorderFrameDiagnostics | null
|
|
200
|
+
startupKeyframe: MmtpReorderFrameDiagnostics | null
|
|
201
|
+
head: MmtpReorderFrameDiagnostics[]
|
|
202
|
+
tail: MmtpReorderFrameDiagnostics[]
|
|
203
|
+
keyframes: MmtpReorderFrameDiagnostics[]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export type MmtpParserRecord =
|
|
207
|
+
| {
|
|
208
|
+
kind: 'repair' | 'non-media'
|
|
209
|
+
packet: PacketInfo
|
|
210
|
+
byteLength: number
|
|
211
|
+
}
|
|
212
|
+
| {
|
|
213
|
+
kind: 'init' | 'fragment'
|
|
214
|
+
packet: PacketInfo
|
|
215
|
+
data: Uint8Array
|
|
216
|
+
mpuSequenceNumber: number
|
|
217
|
+
fragmentationIndicator: number
|
|
218
|
+
fragmentCounter: number
|
|
219
|
+
}
|
|
220
|
+
| {
|
|
221
|
+
kind: 'pending'
|
|
222
|
+
packet: PacketInfo
|
|
223
|
+
mpuSequenceNumber: number
|
|
224
|
+
fragmentationIndicator: number
|
|
225
|
+
fragmentCounter: number
|
|
226
|
+
}
|
|
227
|
+
| {
|
|
228
|
+
kind: 'rejected'
|
|
229
|
+
packet: PacketInfo
|
|
230
|
+
mpuSequenceNumber: number
|
|
231
|
+
fragmentationIndicator: number
|
|
232
|
+
fragmentCounter: number
|
|
233
|
+
error: {
|
|
234
|
+
/**
|
|
235
|
+
* Either a single data unit's own fragment chain was rejected, or
|
|
236
|
+
* combining reassembled data units into a sample was (moqcast #37).
|
|
237
|
+
* Both are fail-closed drops of the same packet, so they share this
|
|
238
|
+
* record; the code says which class it came from.
|
|
239
|
+
*/
|
|
240
|
+
code: MfuReassemblyError['code'] | SampleCombineError['code']
|
|
241
|
+
message: string
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
| {
|
|
245
|
+
kind: 'frame'
|
|
246
|
+
packet: PacketInfo
|
|
247
|
+
data: Uint8Array
|
|
248
|
+
meta: FrameMeta
|
|
249
|
+
mpuSequenceNumber: number
|
|
250
|
+
fragmentCount: number
|
|
251
|
+
rapFlag: boolean
|
|
252
|
+
priority?: number
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Why feed() returned no record. Every `return null` inside feed() names one
|
|
257
|
+
* of these, so a packet that produced nothing is attributable to the stage
|
|
258
|
+
* that rejected it rather than to one collapsed failure bucket.
|
|
259
|
+
*
|
|
260
|
+
* The distinction is what makes a wedged receive path readable: a media track
|
|
261
|
+
* that stops producing records looks identical from outside whether the header
|
|
262
|
+
* failed to parse, MFU extraction failed, or the timing resolver refused the
|
|
263
|
+
* sample — and those name three different components.
|
|
264
|
+
*/
|
|
265
|
+
export type MmtpFeedDropReason =
|
|
266
|
+
/** feed() after dispose(); the host is still pushing at a torn-down parser. */
|
|
267
|
+
| 'disposed'
|
|
268
|
+
| 'header-parse-failed'
|
|
269
|
+
| 'mfu-extract-failed'
|
|
270
|
+
/** MPU present but not a timed MFU, so no media payload to reassemble. */
|
|
271
|
+
| 'untimed-mpu'
|
|
272
|
+
| 'init-unexpected-fragmentation'
|
|
273
|
+
| 'init-normalize-failed'
|
|
274
|
+
/** No timing resolver, or it refused the initialization segment. */
|
|
275
|
+
| 'init-timing-refused'
|
|
276
|
+
| 'fragment-unexpected-fragmentation'
|
|
277
|
+
| 'fragment-no-resolver'
|
|
278
|
+
/** addMovieFragment refused — includes the pending-timing-sample budget. */
|
|
279
|
+
| 'fragment-timing-refused'
|
|
280
|
+
| 'unknown-fragment-type'
|
|
281
|
+
| 'mfu-no-resolver'
|
|
282
|
+
| 'mfu-empty-payload'
|
|
283
|
+
| 'mfu-du-header-missing'
|
|
284
|
+
| 'mfu-sample-timing-missing'
|
|
285
|
+
| 'mfu-offset-out-of-range'
|
|
286
|
+
| 'mfu-size-mismatch'
|
|
287
|
+
| 'assembled-identity-missing'
|
|
288
|
+
| 'assembled-timing-missing'
|
|
289
|
+
| 'assembled-mpu-mismatch'
|
|
290
|
+
/**
|
|
291
|
+
* Refusals inside the combined-sample delivery step, which runs after the
|
|
292
|
+
* combiner has already produced a whole sample. These are distinct from
|
|
293
|
+
* their `assembled-*` counterparts: those reject a data unit on the way in,
|
|
294
|
+
* these reject a sample that was fully reassembled and then found
|
|
295
|
+
* undeliverable, so collapsing the two would hide a combiner that completes
|
|
296
|
+
* samples nothing can present.
|
|
297
|
+
*/
|
|
298
|
+
| 'combined-timing-missing'
|
|
299
|
+
| 'combined-size-mismatch'
|
|
300
|
+
/** Frame metadata could not be built — an fMP4 box arrived as a timed MFU
|
|
301
|
+
* payload, or the authored timing converts to a non-positive duration. */
|
|
302
|
+
| 'combined-meta-build-failed'
|
|
303
|
+
| 'combined-timing-consume-refused'
|
|
304
|
+
/** feed() threw. Counted so the tally still sums to fedObjects; the error
|
|
305
|
+
* is rethrown, never swallowed. */
|
|
306
|
+
| 'threw'
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Every key `outcomes` can carry: one per record kind, one per drop reason.
|
|
310
|
+
*
|
|
311
|
+
* Derived from the two unions rather than written out, so a new record kind or
|
|
312
|
+
* drop reason cannot be counted under a key the type does not admit, and a
|
|
313
|
+
* consumer can enumerate the vocabulary from the type instead of from prose.
|
|
314
|
+
*/
|
|
315
|
+
export type MmtpFeedOutcomeKey =
|
|
316
|
+
| MmtpParserRecord['kind']
|
|
317
|
+
| `drop:${MmtpFeedDropReason}`
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Feed tallies owned by the parser.
|
|
321
|
+
*
|
|
322
|
+
* These live here rather than on MmtpDecodePipeline because the two players
|
|
323
|
+
* sit at different layers of this library: hang-mmt-fec drives the pipeline,
|
|
324
|
+
* while moqtail constructs MmtpContainerParser directly and never builds the
|
|
325
|
+
* wrapper. feed() is the one chokepoint both cross, so a tally maintained here
|
|
326
|
+
* instruments both by construction.
|
|
327
|
+
*
|
|
328
|
+
* Logs are diagnostic output, not an acceptance API — this is the typed
|
|
329
|
+
* counter a harness should read instead of scraping console histograms.
|
|
330
|
+
*/
|
|
331
|
+
export interface MmtpParserFeedStats {
|
|
332
|
+
/** Every packet handed to feed(), including post-dispose calls. */
|
|
333
|
+
fedObjects: number
|
|
334
|
+
/**
|
|
335
|
+
* Mutually exclusive outcome tally: record kinds under their own `kind`,
|
|
336
|
+
* drops under `drop:<reason>`. The values sum to exactly `fedObjects`, which
|
|
337
|
+
* is the property that makes a frozen media bucket beside a still-climbing
|
|
338
|
+
* non-media bucket a readable signal rather than two unrelated numbers.
|
|
339
|
+
*
|
|
340
|
+
* Partial because a key appears only once it has been counted; absent and
|
|
341
|
+
* zero mean the same thing to a reader.
|
|
342
|
+
*/
|
|
343
|
+
outcomes: Partial<Record<MmtpFeedOutcomeKey, number>>
|
|
344
|
+
/**
|
|
345
|
+
* Refusals from a completed older sample delivered while registering a
|
|
346
|
+
* later packet's authored size. Kept separate because that side effect is
|
|
347
|
+
* not the packet's own feed outcome.
|
|
348
|
+
*/
|
|
349
|
+
deferredOutcomes: Partial<Record<MmtpFeedDropReason, number>>
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
type FrameCallback = (data: Uint8Array, meta: FrameMeta) => void
|
|
353
|
+
type InitCallback = (data: Uint8Array, packetId: number, catalogTrackName?: string) => void
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Result of one combined-sample delivery attempt: either the record to return
|
|
357
|
+
* or the reason the sample could not be delivered.
|
|
358
|
+
*
|
|
359
|
+
* Internal to the parser. A bare `null` would be indistinguishable from the
|
|
360
|
+
* other refusals `feed()` can take, and the whole point of the drop-reason
|
|
361
|
+
* vocabulary is that a stage which stops producing frames says which stage it
|
|
362
|
+
* was.
|
|
363
|
+
*/
|
|
364
|
+
type CombinedSampleOutcome =
|
|
365
|
+
| { readonly record: MmtpParserRecord; readonly reason?: undefined }
|
|
366
|
+
| { readonly record?: undefined; readonly reason: MmtpFeedDropReason }
|
|
367
|
+
|
|
368
|
+
export function isMfuKeyframeMetadata(rapFlag: boolean, priority?: number): boolean {
|
|
369
|
+
return rapFlag || (priority ?? 0) > 0x80
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Per-packet detached ALTA authenticator trailer (08-07 Path 2).
|
|
373
|
+
* - `auth`: the authenticator bytes (length-prefixed at packet end).
|
|
374
|
+
* - `payloadEnd`: byte offset within the source packet where signed
|
|
375
|
+
* payload ends — consumers reconstruct `payload = packet[0..payloadEnd]`
|
|
376
|
+
* for verify_detached. */
|
|
377
|
+
export interface ParsedAltaTrailer {
|
|
378
|
+
auth: Uint8Array
|
|
379
|
+
payloadEnd: number
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Source-symbol exposure for FEC consumers.
|
|
383
|
+
*
|
|
384
|
+
* Fires from {@link MmtpContainerParser.feed} once per FEC-protected source
|
|
385
|
+
* packet — after parseHeader confirms fec_payload_id is present, before MFU
|
|
386
|
+
* extraction.
|
|
387
|
+
*
|
|
388
|
+
* Arguments:
|
|
389
|
+
* - `packetId` – MMTP header `packet_id` (track identifier)
|
|
390
|
+
* - `ssId` – flat Source Symbol ID from the 4-byte fec_payload_id trailer
|
|
391
|
+
* - `rawSymbol` – packet bytes minus the 4-byte fec_payload_id trailer.
|
|
392
|
+
* NOT zero-padded to FEC symbol size — consumer pads if needed
|
|
393
|
+
* (FEC symbol size is FEC-config, not parser-config).
|
|
394
|
+
* - `alta` – detached ALTA authenticator if present, else undefined
|
|
395
|
+
*
|
|
396
|
+
* Designed so consumers (player, Shaka shim, upstream moqtail) can wire
|
|
397
|
+
* source-symbol delivery to whatever FEC pipeline they own, without depending
|
|
398
|
+
* on libmmt's MulticastManager/MmtpRouter/FECManager stack. */
|
|
399
|
+
type SourceSymbolCallback = (
|
|
400
|
+
packetId: number,
|
|
401
|
+
ssId: number,
|
|
402
|
+
rawSymbol: Uint8Array,
|
|
403
|
+
alta: ParsedAltaTrailer | undefined,
|
|
404
|
+
) => void
|
|
405
|
+
|
|
406
|
+
export interface ParseHeaderOptions {
|
|
407
|
+
/** Recovered source symbols already had the trailing Source FEC Payload ID stripped. */
|
|
408
|
+
skipFecPayloadId?: boolean
|
|
409
|
+
/** Mark frames emitted from this packet as FEC-recovered. */
|
|
410
|
+
recovered?: boolean
|
|
411
|
+
/** Exact catalog track bound to this packet by its ingress subscription. */
|
|
412
|
+
catalogTrackName?: string
|
|
413
|
+
/** Source transport for parser-owned frame attribution. */
|
|
414
|
+
source?: MmtpSourcePath
|
|
415
|
+
/**
|
|
416
|
+
* Explicit strict provenance for the fragments this packet produces.
|
|
417
|
+
* The router sets it for FEC-recovered packets, which carry no `source`
|
|
418
|
+
* but whose FEC block's symbol provenance is known.
|
|
419
|
+
*/
|
|
420
|
+
deliveryProvenance?: MmtpDeliveryProvenance
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── FrameSequencer ──────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
interface PendingFrame {
|
|
426
|
+
data: Uint8Array
|
|
427
|
+
meta: FrameMeta
|
|
428
|
+
seq: number
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
interface SequencerStream {
|
|
432
|
+
buf: PendingFrame[]
|
|
433
|
+
lastEmitted: PendingFrame | null
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function reorderFrameDiagnostics(frame: PendingFrame): MmtpReorderFrameDiagnostics {
|
|
437
|
+
return {
|
|
438
|
+
timestampUs: frame.meta.timestamp,
|
|
439
|
+
keyframe: frame.meta.keyframe,
|
|
440
|
+
sequenceNumber: frame.seq,
|
|
441
|
+
...(frame.meta.packetSequenceNumber === undefined
|
|
442
|
+
? {}
|
|
443
|
+
: { packetSequenceNumber: frame.meta.packetSequenceNumber }),
|
|
444
|
+
...(frame.meta.movieFragmentSequenceNumber === undefined
|
|
445
|
+
? {}
|
|
446
|
+
: { movieFragmentSequenceNumber: frame.meta.movieFragmentSequenceNumber }),
|
|
447
|
+
...(frame.meta.sampleNumber === undefined ? {} : { sampleNumber: frame.meta.sampleNumber }),
|
|
448
|
+
...(frame.meta.fragmentCount === undefined
|
|
449
|
+
? {}
|
|
450
|
+
: { fragmentCount: frame.meta.fragmentCount }),
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
type SequencerStat = Exclude<keyof MmtpReorderStats, 'bufferedFrames'>
|
|
455
|
+
|
|
456
|
+
function serialSequenceDelta(a: number, b: number): number {
|
|
457
|
+
return (a - b) | 0
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function compareFrameOrder(
|
|
461
|
+
a: PendingFrame,
|
|
462
|
+
b: PendingFrame,
|
|
463
|
+
): number {
|
|
464
|
+
if (a.meta.decodeTime !== undefined && b.meta.decodeTime !== undefined &&
|
|
465
|
+
a.meta.mediaTimescale === b.meta.mediaTimescale) {
|
|
466
|
+
if (a.meta.decodeTime < b.meta.decodeTime) return -1
|
|
467
|
+
if (a.meta.decodeTime > b.meta.decodeTime) return 1
|
|
468
|
+
} else {
|
|
469
|
+
const decodeDelta = (a.meta.decodeTimestamp ?? a.meta.timestamp) -
|
|
470
|
+
(b.meta.decodeTimestamp ?? b.meta.timestamp)
|
|
471
|
+
if (decodeDelta !== 0) return decodeDelta
|
|
472
|
+
}
|
|
473
|
+
const movieDelta = serialSequenceDelta(
|
|
474
|
+
a.meta.movieFragmentSequenceNumber ?? 0,
|
|
475
|
+
b.meta.movieFragmentSequenceNumber ?? 0,
|
|
476
|
+
)
|
|
477
|
+
if (movieDelta !== 0) return movieDelta
|
|
478
|
+
const sampleDelta = (a.meta.sampleNumber ?? 0) - (b.meta.sampleNumber ?? 0)
|
|
479
|
+
return sampleDelta !== 0 ? sampleDelta : serialSequenceDelta(a.seq, b.seq)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
class FrameSequencer {
|
|
483
|
+
#streams = new Map<number, SequencerStream>()
|
|
484
|
+
#windowSize: number
|
|
485
|
+
#emit: (data: Uint8Array, meta: FrameMeta) => void
|
|
486
|
+
#recordStat: (stat: SequencerStat, count?: number) => void
|
|
487
|
+
|
|
488
|
+
constructor(
|
|
489
|
+
windowSize: number,
|
|
490
|
+
emit: (data: Uint8Array, meta: FrameMeta) => void,
|
|
491
|
+
recordStat: (stat: SequencerStat, count?: number) => void = () => {},
|
|
492
|
+
) {
|
|
493
|
+
this.#windowSize = windowSize
|
|
494
|
+
this.#emit = emit
|
|
495
|
+
this.#recordStat = recordStat
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
push(data: Uint8Array, meta: FrameMeta, seq: number): void {
|
|
499
|
+
if (this.#windowSize <= 0) {
|
|
500
|
+
this.#emit(data, meta)
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
const frame: PendingFrame = { data, meta: { ...meta }, seq }
|
|
504
|
+
const stream = this.#streamFor(meta.packetId)
|
|
505
|
+
if (meta.sourceDiscontinuity) {
|
|
506
|
+
this.#recordStat('publisherResets')
|
|
507
|
+
this.#recordStat('droppedOnPublisherReset', stream.buf.length)
|
|
508
|
+
stream.buf = []
|
|
509
|
+
stream.lastEmitted = null
|
|
510
|
+
frame.meta.sourceDiscontinuity = true
|
|
511
|
+
}
|
|
512
|
+
if (this.#isStale(stream, frame)) {
|
|
513
|
+
this.#recordStat('droppedStale')
|
|
514
|
+
return
|
|
515
|
+
}
|
|
516
|
+
if (meta.keyframe && stream.lastEmitted === null) {
|
|
517
|
+
// The first RAP establishes decoder sync. Discard any pre-RAP media and
|
|
518
|
+
// emit it immediately so startup does not pay the reorder-window delay.
|
|
519
|
+
this.#recordStat('droppedBeforeStartupKeyframe', stream.buf.length)
|
|
520
|
+
stream.buf = []
|
|
521
|
+
this.#emitFrame(stream, frame)
|
|
522
|
+
return
|
|
523
|
+
}
|
|
524
|
+
let lo = 0, hi = stream.buf.length
|
|
525
|
+
while (lo < hi) {
|
|
526
|
+
const mid = (lo + hi) >>> 1
|
|
527
|
+
if (compareFrameOrder(stream.buf[mid]!, frame) < 0) lo = mid + 1
|
|
528
|
+
else hi = mid
|
|
529
|
+
}
|
|
530
|
+
stream.buf.splice(lo, 0, frame)
|
|
531
|
+
while (stream.buf.length > this.#windowSize) this.#flushHead(stream)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
flush(): void {
|
|
535
|
+
for (const packetId of [...this.#streams.keys()].sort((a, b) => a - b)) {
|
|
536
|
+
const stream = this.#streams.get(packetId)
|
|
537
|
+
while (stream && stream.buf.length > 0) this.#flushHead(stream)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
get buffered(): number {
|
|
542
|
+
let total = 0
|
|
543
|
+
for (const stream of this.#streams.values()) total += stream.buf.length
|
|
544
|
+
return total
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
get diagnostics(): Omit<MmtpReorderStreamDiagnostics, 'packetId'>[] {
|
|
548
|
+
return [...this.#streams.values()].map(stream => ({
|
|
549
|
+
bufferedFrames: stream.buf.length,
|
|
550
|
+
lastEmitted: stream.lastEmitted ? reorderFrameDiagnostics(stream.lastEmitted) : null,
|
|
551
|
+
startupKeyframe: null,
|
|
552
|
+
head: stream.buf.slice(0, 4).map(reorderFrameDiagnostics),
|
|
553
|
+
tail: stream.buf.slice(-4).map(reorderFrameDiagnostics),
|
|
554
|
+
keyframes: stream.buf.filter(frame => frame.meta.keyframe).slice(0, 8)
|
|
555
|
+
.map(reorderFrameDiagnostics),
|
|
556
|
+
}))
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
discard(): void {
|
|
560
|
+
for (const stream of this.#streams.values()) {
|
|
561
|
+
this.#recordStat('droppedOnLiveDispose', stream.buf.length)
|
|
562
|
+
stream.buf = []
|
|
563
|
+
}
|
|
564
|
+
this.#streams.clear()
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
#streamFor(packetId: number): SequencerStream {
|
|
568
|
+
let stream = this.#streams.get(packetId)
|
|
569
|
+
if (!stream) {
|
|
570
|
+
stream = { buf: [], lastEmitted: null }
|
|
571
|
+
this.#streams.set(packetId, stream)
|
|
572
|
+
}
|
|
573
|
+
return stream
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
#flushHead(stream: SequencerStream): boolean {
|
|
577
|
+
const f = stream.buf.shift()
|
|
578
|
+
if (!f) return false
|
|
579
|
+
return this.#emitFrame(stream, f)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
#isStale(stream: SequencerStream, frame: PendingFrame): boolean {
|
|
583
|
+
return stream.lastEmitted !== null &&
|
|
584
|
+
compareFrameOrder(frame, stream.lastEmitted) <= 0
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
#emitFrame(stream: SequencerStream, frame: PendingFrame): boolean {
|
|
588
|
+
if (this.#isStale(stream, frame)) {
|
|
589
|
+
this.#recordStat('droppedStale')
|
|
590
|
+
return false
|
|
591
|
+
}
|
|
592
|
+
stream.lastEmitted = frame
|
|
593
|
+
this.#emit(frame.data, frame.meta)
|
|
594
|
+
return true
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ── fMP4 box detection ──────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
const FMPU_BOX_TYPES = new Set(['ftyp', 'moov', 'moof'])
|
|
601
|
+
|
|
602
|
+
function isFmp4Box(data: Uint8Array): boolean {
|
|
603
|
+
if (data.length < 8) return false
|
|
604
|
+
const type = String.fromCharCode(data[4]!, data[5]!, data[6]!, data[7]!)
|
|
605
|
+
return FMPU_BOX_TYPES.has(type)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
interface TimingStreamState {
|
|
609
|
+
packetId: number
|
|
610
|
+
resolver: IsoBmffTimingResolver
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ── MmtpContainerParser ─────────────────────────────────────────────
|
|
614
|
+
|
|
615
|
+
export class MmtpContainerParser {
|
|
616
|
+
#tracks = new Map<number, TrackInfo>()
|
|
617
|
+
#defaultTrack: TrackInfo | null = null
|
|
618
|
+
#reassembler: MfuReassemblerBackend
|
|
619
|
+
/**
|
|
620
|
+
* Combines subsample data units back into whole samples (moqcast #37), one
|
|
621
|
+
* combiner per parser stream. Per-stream is required, not an optimization:
|
|
622
|
+
* {@link SampleCombiner} identifies a sample by
|
|
623
|
+
* `(catalogTrackName, mpu, movieFragment, sampleNumber)`, so two streams
|
|
624
|
+
* that carry no catalog identity would collide on every sample number and
|
|
625
|
+
* each would see the other's data units as duplicates of its own. The
|
|
626
|
+
* parser already isolates timing and reordering by this same stream key.
|
|
627
|
+
*
|
|
628
|
+
* Inert until a producer partitions — which it does the moment a frame
|
|
629
|
+
* exceeds one data unit's 256-payload capacity (~327 KB here, routinely
|
|
630
|
+
* exceeded by 4K/8K intra frames).
|
|
631
|
+
*/
|
|
632
|
+
#sampleCombiners = new Map<string, SampleCombiner>()
|
|
633
|
+
#sampleCombinerOptions: SampleCombinerOptions
|
|
634
|
+
/** Counters folded in from combiners dropped by a track reset, so the
|
|
635
|
+
* totals below stay monotonic across a publisher change. */
|
|
636
|
+
#retiredSampleCombinerStats: SampleCombinerStats = {
|
|
637
|
+
combinedSamples: 0,
|
|
638
|
+
passthroughSamples: 0,
|
|
639
|
+
timedOutSamples: 0,
|
|
640
|
+
evictedSamples: 0,
|
|
641
|
+
}
|
|
642
|
+
#timingStreams = new Map<string, TimingStreamState>()
|
|
643
|
+
#maxPendingTimingSamples: number
|
|
644
|
+
#pendingTimingSamples = 0
|
|
645
|
+
#reclaimedTimingSamples = 0
|
|
646
|
+
#evictedTimingSamples = 0
|
|
647
|
+
#rejectedMovieFragments = 0
|
|
648
|
+
#sequencers = new Map<string, { packetId: number; sequencer: FrameSequencer }>()
|
|
649
|
+
#reorderWindow = 0
|
|
650
|
+
#reassemblyPolicy: MfuReassemblyPolicy | null = null
|
|
651
|
+
#frameCb: FrameCallback | null = null
|
|
652
|
+
#initCb: InitCallback | null = null
|
|
653
|
+
#sourceSymbolCb: SourceSymbolCallback | null = null
|
|
654
|
+
#trackFrameCbs = new Map<number, FrameCallback>()
|
|
655
|
+
#reorderStats: Omit<MmtpReorderStats, 'bufferedFrames'> = {
|
|
656
|
+
droppedBeforeStartupKeyframe: 0,
|
|
657
|
+
droppedIncompleteStartupInterval: 0,
|
|
658
|
+
droppedStale: 0,
|
|
659
|
+
forcedWindowFlushes: 0,
|
|
660
|
+
droppedOnLiveDispose: 0,
|
|
661
|
+
publisherResets: 0,
|
|
662
|
+
droppedOnPublisherReset: 0,
|
|
663
|
+
}
|
|
664
|
+
/** Streams (keyed by physical parser stream, value is packetId) that have
|
|
665
|
+
* observed their first RAP under a reliable-moq policy. Reliable MoQ
|
|
666
|
+
* resolves to a zero parser reorder window (transport ordering is
|
|
667
|
+
* protocol-owned), so #sequencerFor returns null and its own startup gate
|
|
668
|
+
* never runs; this map preserves an order-independent "discard delta
|
|
669
|
+
* frames before the first RAP" gate for that path without reintroducing
|
|
670
|
+
* reordering into the parser. */
|
|
671
|
+
#reliableMoqStartupGate = new Map<string, number>()
|
|
672
|
+
#disposed = false
|
|
673
|
+
#fedObjects = 0
|
|
674
|
+
#feedOutcomes = new Map<MmtpFeedOutcomeKey, number>()
|
|
675
|
+
#deferredFeedOutcomes = new Map<MmtpFeedDropReason, number>()
|
|
676
|
+
#lastOutcome: MmtpFeedOutcomeKey | null = null
|
|
677
|
+
#lastOutcomeCount = 0
|
|
678
|
+
|
|
679
|
+
#streamKey(packetId: number, catalogTrackName?: string): string {
|
|
680
|
+
return JSON.stringify([packetId, catalogTrackName ?? null])
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
#sampleCombinerFor(streamKey: string): SampleCombiner {
|
|
684
|
+
let combiner = this.#sampleCombiners.get(streamKey)
|
|
685
|
+
if (combiner === undefined) {
|
|
686
|
+
combiner = new SampleCombiner({
|
|
687
|
+
...this.#sampleCombinerOptions,
|
|
688
|
+
onAbandon: sample => this.#releaseTimingSlot(streamKey, sample),
|
|
689
|
+
})
|
|
690
|
+
this.#sampleCombiners.set(streamKey, combiner)
|
|
691
|
+
}
|
|
692
|
+
return combiner
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Return the timing budget slot of a sample the combiner abandoned.
|
|
697
|
+
*
|
|
698
|
+
* Without this, `#pendingTimingSamples` is charged for every sample a moof
|
|
699
|
+
* declares but credited only when that sample is delivered, so each sample
|
|
700
|
+
* lost to eviction or timeout burns a slot until the window displaces it.
|
|
701
|
+
* Window displacement bounds that leak but only reclaims the OLDEST entries;
|
|
702
|
+
* this path returns a slot the moment the combiner knows the sample is gone,
|
|
703
|
+
* which keeps the live window as wide as the stream actually needs.
|
|
704
|
+
*
|
|
705
|
+
* The parser total is decremented only when the resolver reports it
|
|
706
|
+
* actually freed a slot, so a resolver already dropped by a track reset, or
|
|
707
|
+
* a sample released twice, cannot drive the accounting negative.
|
|
708
|
+
*/
|
|
709
|
+
#releaseTimingSlot(streamKey: string, sample: AbandonedSample): void {
|
|
710
|
+
const resolver = this.#timingStreams.get(streamKey)?.resolver
|
|
711
|
+
if (!resolver) return
|
|
712
|
+
if (!resolver.releaseSample(
|
|
713
|
+
sample.mpuSequenceNumber,
|
|
714
|
+
sample.movieFragmentSequenceNumber,
|
|
715
|
+
sample.sampleNumber,
|
|
716
|
+
)) return
|
|
717
|
+
this.#pendingTimingSamples--
|
|
718
|
+
this.#reclaimedTimingSamples++
|
|
719
|
+
if (this.#pendingTimingSamples < 0) {
|
|
720
|
+
throw new Error('[MmtpContainerParser] pending timing sample accounting underflow')
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** Drop one stream's combiner, folding its counters into the retained
|
|
725
|
+
* totals so a reset cannot make the exposed stats run backwards. */
|
|
726
|
+
#dropSampleCombiner(streamKey: string): void {
|
|
727
|
+
const combiner = this.#sampleCombiners.get(streamKey)
|
|
728
|
+
if (combiner === undefined) return
|
|
729
|
+
const stats = combiner.stats
|
|
730
|
+
this.#retiredSampleCombinerStats.combinedSamples += stats.combinedSamples
|
|
731
|
+
this.#retiredSampleCombinerStats.passthroughSamples += stats.passthroughSamples
|
|
732
|
+
this.#retiredSampleCombinerStats.timedOutSamples += stats.timedOutSamples
|
|
733
|
+
this.#retiredSampleCombinerStats.evictedSamples += stats.evictedSamples
|
|
734
|
+
this.#sampleCombiners.delete(streamKey)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
constructor(config?: MmtpParserConfig) {
|
|
738
|
+
if (config?.reassemblyPolicy && config.mfuTimeoutMs !== undefined) {
|
|
739
|
+
throw new Error('[MmtpContainerParser] use reassemblyPolicy or mfuTimeoutMs, not both')
|
|
740
|
+
}
|
|
741
|
+
const reassemblyPolicy = config?.reassemblyPolicy ?? null
|
|
742
|
+
const requestedReorderWindow = config?.reorderWindow ?? 0
|
|
743
|
+
if (!Number.isFinite(requestedReorderWindow) || requestedReorderWindow < 0) {
|
|
744
|
+
throw new Error('[MmtpContainerParser] reorderWindow must be a finite number >= 0')
|
|
745
|
+
}
|
|
746
|
+
const normalizedReorderWindow = Math.ceil(requestedReorderWindow)
|
|
747
|
+
const requestedMfuTimeout = reassemblyPolicy
|
|
748
|
+
? mfuReassemblyTimeoutMs(reassemblyPolicy)
|
|
749
|
+
: config?.mfuTimeoutMs
|
|
750
|
+
if (reassemblyPolicy) {
|
|
751
|
+
mmtpFrameReorderWindowFrames(reassemblyPolicy, normalizedReorderWindow)
|
|
752
|
+
}
|
|
753
|
+
if (
|
|
754
|
+
requestedMfuTimeout !== undefined &&
|
|
755
|
+
requestedMfuTimeout !== null &&
|
|
756
|
+
requestedMfuTimeout <= 0
|
|
757
|
+
) {
|
|
758
|
+
throw new Error('[MmtpContainerParser] mfuTimeoutMs must be > 0')
|
|
759
|
+
}
|
|
760
|
+
const maxPendingTimingSamples = config?.maxBufferedMfus ?? 100
|
|
761
|
+
if (!Number.isInteger(maxPendingTimingSamples) || maxPendingTimingSamples <= 0) {
|
|
762
|
+
throw new Error('[MmtpContainerParser] maxBufferedMfus must be a positive integer')
|
|
763
|
+
}
|
|
764
|
+
this.#maxPendingTimingSamples = maxPendingTimingSamples
|
|
765
|
+
|
|
766
|
+
const maxSubsampleSegments = config?.maxSubsampleSegments ?? 16
|
|
767
|
+
if (!Number.isInteger(maxSubsampleSegments) || maxSubsampleSegments <= 0) {
|
|
768
|
+
throw new Error('[MmtpContainerParser] maxSubsampleSegments must be a positive integer')
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Everything above only reads `config` — the full policy/window
|
|
772
|
+
// combination is validated before this point, so it is now safe to
|
|
773
|
+
// mutate this instance or an injected, caller-owned reassembler.
|
|
774
|
+
this.#reassemblyPolicy = reassemblyPolicy
|
|
775
|
+
if (config?.reassembler) {
|
|
776
|
+
this.#reassembler = config.reassembler
|
|
777
|
+
// An injected backend owns its existing transport policy unless this
|
|
778
|
+
// parser was given an explicit policy/legacy timeout override.
|
|
779
|
+
if (requestedMfuTimeout !== undefined) {
|
|
780
|
+
this.#reassembler.timeoutMs = requestedMfuTimeout
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
this.#reassembler = new MFUReassembler({
|
|
784
|
+
timeoutMs: requestedMfuTimeout === undefined ? 1000 : requestedMfuTimeout,
|
|
785
|
+
...(config?.maxBufferedMfus === undefined ? {} : { maxBuffered: config.maxBufferedMfus }),
|
|
786
|
+
})
|
|
787
|
+
}
|
|
788
|
+
this.#reorderWindow = normalizedReorderWindow
|
|
789
|
+
this.#sampleCombinerOptions = {
|
|
790
|
+
maxBuffered: maxPendingTimingSamples,
|
|
791
|
+
// The parser resolves every timed sample's authored size from FT=1
|
|
792
|
+
// before its data units reach the combiner, so "no size yet" always
|
|
793
|
+
// means the size has not arrived -- never that the host declines to
|
|
794
|
+
// supply one. Waiting is therefore correct: treating an offset-zero
|
|
795
|
+
// data unit as a whole sample in that window would truncate a
|
|
796
|
+
// partitioned sample instead of completing it.
|
|
797
|
+
requireAuthoredSize: true,
|
|
798
|
+
maxSegmentsPerSample: maxSubsampleSegments,
|
|
799
|
+
}
|
|
800
|
+
this.#defaultTrack = config?.defaultTrack ?? null
|
|
801
|
+
if (this.#defaultTrack) this.#validateReassemblyPolicy()
|
|
802
|
+
if (config?.tracks) {
|
|
803
|
+
for (const [id, info] of config.tracks) {
|
|
804
|
+
this.#validateReassemblyPolicy()
|
|
805
|
+
this.#tracks.set(id, info)
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** The MFU reassembler that buffers fragmented MFUs fed via feed().
|
|
811
|
+
* Exposed so a co-operating FEC-recovery layer (MmtpRouter) feeds recovered
|
|
812
|
+
* fragments into the SAME reassembler: a frame split between source-received
|
|
813
|
+
* and FEC-recovered fragments must share one buffer to complete (BLO-8151).
|
|
814
|
+
* Before this was shared, recovered video symbols reported "0 emitted,
|
|
815
|
+
* pending-reassembly=N" because they landed in a separate instance. */
|
|
816
|
+
get reassembler(): MfuReassemblerBackend {
|
|
817
|
+
return this.#reassembler
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Subsample-combining counters (moqcast #37). `combinedSamples` counts
|
|
822
|
+
* samples reassembled from two or more data units and `passthroughSamples`
|
|
823
|
+
* those that arrived whole; the two are deliberately separate, so a
|
|
824
|
+
* producer that silently stopped partitioning cannot hide behind a single
|
|
825
|
+
* "samples delivered" total. This is the typed acceptance signal for
|
|
826
|
+
* large-frame playback — never parse the console for it.
|
|
827
|
+
*/
|
|
828
|
+
get sampleCombinerStats(): Readonly<SampleCombinerStats> {
|
|
829
|
+
const total = { ...this.#retiredSampleCombinerStats }
|
|
830
|
+
for (const combiner of this.#sampleCombiners.values()) {
|
|
831
|
+
const stats = combiner.stats
|
|
832
|
+
total.combinedSamples += stats.combinedSamples
|
|
833
|
+
total.passthroughSamples += stats.passthroughSamples
|
|
834
|
+
total.timedOutSamples += stats.timedOutSamples
|
|
835
|
+
total.evictedSamples += stats.evictedSamples
|
|
836
|
+
}
|
|
837
|
+
return total
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Typed view of the ISO-BMFF timing-sample budget.
|
|
842
|
+
*
|
|
843
|
+
* This is the acceptance signal for the budget ratchet: `pending` walking
|
|
844
|
+
* to `capacity` while `rejectedMovieFragments` climbs is the stream wedging,
|
|
845
|
+
* and it is observable here rather than only as media silently stopping.
|
|
846
|
+
* Per repo policy this is a producer-owned counter -- never infer budget
|
|
847
|
+
* state by parsing logs.
|
|
848
|
+
*/
|
|
849
|
+
get timingBudgetStats(): Readonly<MmtpTimingBudgetStats> {
|
|
850
|
+
return {
|
|
851
|
+
pending: this.#pendingTimingSamples,
|
|
852
|
+
capacity: this.#maxPendingTimingSamples,
|
|
853
|
+
reclaimed: this.#reclaimedTimingSamples,
|
|
854
|
+
evicted: this.#evictedTimingSamples,
|
|
855
|
+
rejectedMovieFragments: this.#rejectedMovieFragments,
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
get reorderStats(): Readonly<MmtpReorderStats> {
|
|
860
|
+
let bufferedFrames = 0
|
|
861
|
+
for (const { sequencer } of this.#sequencers.values()) bufferedFrames += sequencer.buffered
|
|
862
|
+
return { ...this.#reorderStats, bufferedFrames }
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Feed tallies for this parser. A fresh snapshot per call, not a live view,
|
|
867
|
+
* so a caller can hold one and compare it against a later read.
|
|
868
|
+
*
|
|
869
|
+
* Survives dispose() — the counters are the parser's own state and are not
|
|
870
|
+
* cleared by teardown, because a wedged receive path is usually read after
|
|
871
|
+
* the fact.
|
|
872
|
+
*/
|
|
873
|
+
get feedStats(): Readonly<MmtpParserFeedStats> {
|
|
874
|
+
return {
|
|
875
|
+
fedObjects: this.#fedObjects,
|
|
876
|
+
outcomes: Object.fromEntries(this.#feedOutcomes),
|
|
877
|
+
deferredOutcomes: Object.fromEntries(this.#deferredFeedOutcomes),
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Outcome token of the most recent feed(), or null before the first call.
|
|
883
|
+
*
|
|
884
|
+
* Exposed alongside `lastFeedOutcomeCount` as two scalars rather than one
|
|
885
|
+
* object because a wrapper reads them on every packet, and feedStats()
|
|
886
|
+
* allocates a fresh snapshot per call.
|
|
887
|
+
*/
|
|
888
|
+
get lastFeedOutcome(): MmtpFeedOutcomeKey | null {
|
|
889
|
+
return this.#lastOutcome
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/** How many times `lastFeedOutcome` has now been seen. 1 on its first
|
|
893
|
+
* occurrence, which is what a caller logging once per novel outcome needs. */
|
|
894
|
+
get lastFeedOutcomeCount(): number {
|
|
895
|
+
return this.#lastOutcomeCount
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
#countOutcome(token: MmtpFeedOutcomeKey): void {
|
|
899
|
+
const count = (this.#feedOutcomes.get(token) ?? 0) + 1
|
|
900
|
+
this.#feedOutcomes.set(token, count)
|
|
901
|
+
this.#lastOutcome = token
|
|
902
|
+
this.#lastOutcomeCount = count
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
#countDeferredOutcome(reason: MmtpFeedDropReason): void {
|
|
906
|
+
this.#deferredFeedOutcomes.set(
|
|
907
|
+
reason,
|
|
908
|
+
(this.#deferredFeedOutcomes.get(reason) ?? 0) + 1,
|
|
909
|
+
)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Count a feed() drop and return null in one expression, so every early
|
|
914
|
+
* return inside feed() states the reason it took instead of collapsing into
|
|
915
|
+
* a single failure bucket. Returns null so call sites stay `return this.#drop(...)`.
|
|
916
|
+
*/
|
|
917
|
+
#drop(reason: MmtpFeedDropReason): null {
|
|
918
|
+
this.#countOutcome(`drop:${reason}`)
|
|
919
|
+
return null
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
get reorderDiagnostics(): ReadonlyArray<MmtpReorderStreamDiagnostics> {
|
|
923
|
+
const streams: MmtpReorderStreamDiagnostics[] = []
|
|
924
|
+
for (const { packetId, sequencer } of this.#sequencers.values()) {
|
|
925
|
+
for (const diagnostics of sequencer.diagnostics) streams.push({ packetId, ...diagnostics })
|
|
926
|
+
}
|
|
927
|
+
return streams
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Reclaim samples abandoned by the sample combiner's protocol deadline.
|
|
932
|
+
*
|
|
933
|
+
* This is deliberately driven by the parser's existing packet/lifecycle
|
|
934
|
+
* cleanup path rather than by another timer. The reassembler timeout is the
|
|
935
|
+
* same catalog/FEC-derived deadline that bounds each constituent MFU.
|
|
936
|
+
*/
|
|
937
|
+
cleanupTimeouts(): void {
|
|
938
|
+
const timeoutMs = this.#reassembler.timeoutMs
|
|
939
|
+
if (timeoutMs === null) return
|
|
940
|
+
for (const combiner of this.#sampleCombiners.values()) {
|
|
941
|
+
combiner.cleanupTimeouts(timeoutMs)
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Phase 1 of two-phase parsing. Returns structured header info for FEC feeding.
|
|
947
|
+
* Call this before feed() when you need to extract FEC payload ID and feed
|
|
948
|
+
* symbols to FECManager before MFU extraction.
|
|
949
|
+
* Returns null for malformed packets.
|
|
950
|
+
*/
|
|
951
|
+
parseHeader(packet: Uint8Array, options?: ParseHeaderOptions): PacketInfo | null {
|
|
952
|
+
try {
|
|
953
|
+
const header = parseMMTPHeader(packet)
|
|
954
|
+
const isRepair = header.packetType === PACKET_TYPE_REPAIR
|
|
955
|
+
const fecPayloadId =
|
|
956
|
+
!options?.skipFecPayloadId &&
|
|
957
|
+
!isRepair && hasSourceFecPayloadId(packet)
|
|
958
|
+
? extractFecPayloadId(packet)
|
|
959
|
+
: null
|
|
960
|
+
return {
|
|
961
|
+
packetId: header.packetId,
|
|
962
|
+
packetType: header.packetType,
|
|
963
|
+
timestamp: header.timestamp,
|
|
964
|
+
timestampUs: mmtpTimestampToMicros(header.timestamp),
|
|
965
|
+
sequenceNumber: header.sequenceNumber,
|
|
966
|
+
rapFlag: header.rapFlag,
|
|
967
|
+
fecType: header.fecType,
|
|
968
|
+
fecPayloadId,
|
|
969
|
+
isRepair,
|
|
970
|
+
}
|
|
971
|
+
} catch {
|
|
972
|
+
return null
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/** Feed a complete MMTP packet. For two-phase use, call parseHeader() first. */
|
|
977
|
+
feed(packet: Uint8Array, options?: ParseHeaderOptions): MmtpParserRecord | null {
|
|
978
|
+
this.#fedObjects += 1
|
|
979
|
+
let record: MmtpParserRecord | null
|
|
980
|
+
try {
|
|
981
|
+
record = this.#feed(packet, options)
|
|
982
|
+
} catch (error) {
|
|
983
|
+
// Count before rethrowing: a throw is an outcome, and leaving it
|
|
984
|
+
// uncounted would silently break the sum-to-fedObjects invariant that
|
|
985
|
+
// makes this tally readable.
|
|
986
|
+
this.#countOutcome('drop:threw')
|
|
987
|
+
throw error
|
|
988
|
+
}
|
|
989
|
+
// Drops are counted at their own return sites via #drop(), which is what
|
|
990
|
+
// preserves the reason. Only successful records are counted here.
|
|
991
|
+
if (record) this.#countOutcome(record.kind)
|
|
992
|
+
return record
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
#feed(packet: Uint8Array, options?: ParseHeaderOptions): MmtpParserRecord | null {
|
|
996
|
+
if (this.#disposed) return this.#drop('disposed')
|
|
997
|
+
|
|
998
|
+
this.cleanupTimeouts()
|
|
999
|
+
|
|
1000
|
+
const info = this.parseHeader(packet, options)
|
|
1001
|
+
if (!info) return this.#drop('header-parse-failed')
|
|
1002
|
+
|
|
1003
|
+
if (info.packetType !== PACKET_TYPE_MPU) {
|
|
1004
|
+
return {
|
|
1005
|
+
kind: info.isRepair ? 'repair' : 'non-media',
|
|
1006
|
+
packet: info,
|
|
1007
|
+
byteLength: packet.byteLength,
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// ── FEC source-symbol exposure ──
|
|
1012
|
+
// Fire onSourceSymbol BEFORE MFU extraction so the consumer-side FEC
|
|
1013
|
+
// pipeline (e.g. FecWorkerClient.feedSource) starts as early as possible.
|
|
1014
|
+
// Source-only: parseHeader already gates fecPayloadId on !isRepair.
|
|
1015
|
+
if (info.fecPayloadId !== null && this.#sourceSymbolCb) {
|
|
1016
|
+
// Strip the 4-byte fec_payload_id trailer per ISO 23008-1 §C.5.2.
|
|
1017
|
+
// Symbol-size zero-padding is the consumer's responsibility — FEC
|
|
1018
|
+
// symbol size is a FEC-config concern, not a parser concern.
|
|
1019
|
+
const rawSymbol = packet.subarray(0, packet.length - 4)
|
|
1020
|
+
const altaTrailer = extractAltaTrailer(packet)
|
|
1021
|
+
const alta = altaTrailer
|
|
1022
|
+
? { auth: altaTrailer.auth, payloadEnd: altaTrailer.signedEnd }
|
|
1023
|
+
: undefined
|
|
1024
|
+
try {
|
|
1025
|
+
this.#sourceSymbolCb(info.packetId, info.fecPayloadId.ssId, rawSymbol, alta)
|
|
1026
|
+
} catch {
|
|
1027
|
+
// Source-symbol consumer threw — never block MFU extraction on
|
|
1028
|
+
// FEC-pipeline errors. The frame path stays viable; the consumer
|
|
1029
|
+
// is responsible for its own error surfacing.
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const extraction = extractMFUPayload(packet, {
|
|
1034
|
+
skipFecId: options?.skipFecPayloadId,
|
|
1035
|
+
})
|
|
1036
|
+
if (!extraction) return this.#drop('mfu-extract-failed')
|
|
1037
|
+
|
|
1038
|
+
const { mpu, fragmentType, data, mfuDuHeader } = extraction
|
|
1039
|
+
if (mpu.timedFlag !== 1) return this.#drop('untimed-mpu')
|
|
1040
|
+
const streamKey = this.#streamKey(info.packetId, options?.catalogTrackName)
|
|
1041
|
+
|
|
1042
|
+
if (fragmentType === 0) {
|
|
1043
|
+
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
1044
|
+
return this.#drop('init-unexpected-fragmentation')
|
|
1045
|
+
// ISO MMTP MPU metadata may carry an `mmpu` box between the ftyp and
|
|
1046
|
+
// moov boxes. That box is valid MMTP metadata but is not a legal
|
|
1047
|
+
// top-level MSE initialization box. Normalize at the parser boundary so
|
|
1048
|
+
// every container consumer (including MSE and rendition handoff paths)
|
|
1049
|
+
// receives append-safe ftyp+moov bytes rather than having to remember
|
|
1050
|
+
// this wire-format detail independently.
|
|
1051
|
+
const normalizedInit = normalizeMmtpInitSegment(data)
|
|
1052
|
+
if (!normalizedInit) return this.#drop('init-normalize-failed')
|
|
1053
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, true)
|
|
1054
|
+
if (!resolver?.addInitialization(normalizedInit)) return this.#drop('init-timing-refused')
|
|
1055
|
+
this.#initCb?.(normalizedInit, info.packetId, options?.catalogTrackName)
|
|
1056
|
+
return {
|
|
1057
|
+
kind: 'init',
|
|
1058
|
+
packet: info,
|
|
1059
|
+
data: normalizedInit,
|
|
1060
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
1061
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
1062
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (fragmentType === 1) {
|
|
1067
|
+
if (mpu.fragmentationIndicator !== 0 || mpu.fragmentCounter !== 0)
|
|
1068
|
+
return this.#drop('fragment-unexpected-fragmentation')
|
|
1069
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, false)
|
|
1070
|
+
if (!resolver) return this.#drop('fragment-no-resolver')
|
|
1071
|
+
const pendingBefore = resolver.pendingSampleCount
|
|
1072
|
+
const evictedBefore = resolver.evictedSampleCount
|
|
1073
|
+
const remaining = this.#maxPendingTimingSamples - this.#pendingTimingSamples
|
|
1074
|
+
const admitted = resolver.addMovieFragment(mpu.mpuSequenceNumber, data, remaining)
|
|
1075
|
+
// Sync on BOTH paths: the resolver can displace stale timing and still
|
|
1076
|
+
// refuse the fragment in hand, and a global count that missed those
|
|
1077
|
+
// freed slots would keep refusing on a budget that is no longer full.
|
|
1078
|
+
this.#pendingTimingSamples += resolver.pendingSampleCount - pendingBefore
|
|
1079
|
+
this.#evictedTimingSamples += resolver.evictedSampleCount - evictedBefore
|
|
1080
|
+
if (!admitted) {
|
|
1081
|
+
if (resolver.lastMovieFragmentRejection === 'budget') {
|
|
1082
|
+
this.#rejectedMovieFragments++
|
|
1083
|
+
}
|
|
1084
|
+
return this.#drop('fragment-timing-refused')
|
|
1085
|
+
}
|
|
1086
|
+
return {
|
|
1087
|
+
kind: 'fragment',
|
|
1088
|
+
packet: info,
|
|
1089
|
+
data,
|
|
1090
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
1091
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
1092
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (fragmentType !== 2) return this.#drop('unknown-fragment-type')
|
|
1096
|
+
|
|
1097
|
+
const resolver = this.#timingResolver(streamKey, info.packetId, false)
|
|
1098
|
+
if (!resolver) return this.#drop('mfu-no-resolver')
|
|
1099
|
+
if (!Number.isSafeInteger(data.byteLength) || data.byteLength <= 0)
|
|
1100
|
+
return this.#drop('mfu-empty-payload')
|
|
1101
|
+
if (mpu.fragmentationIndicator === 0 || mpu.fragmentationIndicator === 1) {
|
|
1102
|
+
if (!mfuDuHeader || mfuDuHeader.movieFragmentSequenceNumber === 0 ||
|
|
1103
|
+
mfuDuHeader.sampleNumber === 0) {
|
|
1104
|
+
return this.#drop('mfu-du-header-missing')
|
|
1105
|
+
}
|
|
1106
|
+
const headTiming = resolver.sampleTiming(
|
|
1107
|
+
mpu.mpuSequenceNumber,
|
|
1108
|
+
mfuDuHeader.movieFragmentSequenceNumber,
|
|
1109
|
+
mfuDuHeader.sampleNumber,
|
|
1110
|
+
)
|
|
1111
|
+
if (!headTiming) return this.#drop('mfu-sample-timing-missing')
|
|
1112
|
+
// A data unit's authored base offset is its position within the sample
|
|
1113
|
+
// (moqcast #37): 0 for a whole-sample DU, nonzero for one subsample DU
|
|
1114
|
+
// among siblings. Sizes below are therefore relative to that base, not
|
|
1115
|
+
// to the sample's start -- comparing a subsample DU against the whole
|
|
1116
|
+
// sample's size is what dropped every partitioned 4K/8K frame.
|
|
1117
|
+
const offset = mfuDuHeader.offset
|
|
1118
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset >= headTiming.size)
|
|
1119
|
+
return this.#drop('mfu-offset-out-of-range')
|
|
1120
|
+
const remaining = headTiming.size - offset
|
|
1121
|
+
// FI=0 carries its data unit whole, in a single payload. Only a
|
|
1122
|
+
// partitioned sample's FINAL data unit can be shorter than a full data
|
|
1123
|
+
// unit -- every earlier one spans exactly 256 payloads and so is always
|
|
1124
|
+
// fragmented -- which makes a whole-carried DU exactly the bytes left
|
|
1125
|
+
// from its base. At offset 0 this is the pre-amendment check unchanged.
|
|
1126
|
+
if (mpu.fragmentationIndicator === 0 && data.byteLength !== remaining)
|
|
1127
|
+
return this.#drop('mfu-size-mismatch')
|
|
1128
|
+
if (mpu.fragmentationIndicator === 1 && data.byteLength >= remaining)
|
|
1129
|
+
return this.#drop('mfu-size-mismatch')
|
|
1130
|
+
|
|
1131
|
+
// Publish the authored size before this data unit can complete, so the
|
|
1132
|
+
// combiner never has to guess whether an offset-zero DU is a whole
|
|
1133
|
+
// sample. Registration can complete a previously buffered sample; its
|
|
1134
|
+
// refusal is a side effect of this feed, not this packet's own outcome,
|
|
1135
|
+
// so expose it in deferredOutcomes without double-counting outcomes.
|
|
1136
|
+
const readyOnRegister = this.#sampleCombinerFor(streamKey).registerAuthoredSize(
|
|
1137
|
+
options?.catalogTrackName,
|
|
1138
|
+
mpu.mpuSequenceNumber,
|
|
1139
|
+
mfuDuHeader.movieFragmentSequenceNumber,
|
|
1140
|
+
mfuDuHeader.sampleNumber,
|
|
1141
|
+
headTiming.size,
|
|
1142
|
+
)
|
|
1143
|
+
if (readyOnRegister) {
|
|
1144
|
+
const deferred = this.#emitCombinedSample(
|
|
1145
|
+
readyOnRegister,
|
|
1146
|
+
info,
|
|
1147
|
+
resolver,
|
|
1148
|
+
options?.recovered === true,
|
|
1149
|
+
)
|
|
1150
|
+
if (deferred.record === undefined) this.#countDeferredOutcome(deferred.reason)
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
let assembled: ReassembledMFU | null
|
|
1155
|
+
try {
|
|
1156
|
+
assembled = this.#reassembler.addFragment({
|
|
1157
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
1158
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
1159
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
1160
|
+
data,
|
|
1161
|
+
timestamp: info.timestamp,
|
|
1162
|
+
rapFlag: info.rapFlag,
|
|
1163
|
+
priority: mfuDuHeader?.priority,
|
|
1164
|
+
sampleNumber: mfuDuHeader?.sampleNumber,
|
|
1165
|
+
movieFragmentSequenceNumber: mfuDuHeader?.movieFragmentSequenceNumber,
|
|
1166
|
+
sampleOffset: mfuDuHeader?.offset,
|
|
1167
|
+
packetSequenceNumber: info.sequenceNumber,
|
|
1168
|
+
recovered: options?.recovered === true,
|
|
1169
|
+
source: options?.source,
|
|
1170
|
+
deliveryProvenance: options?.deliveryProvenance,
|
|
1171
|
+
catalogTrackName: options?.catalogTrackName,
|
|
1172
|
+
packetId: info.packetId,
|
|
1173
|
+
})
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
if (!isMfuReassemblyError(error)) throw error
|
|
1176
|
+
console.warn(
|
|
1177
|
+
`[MmtpContainerParser] rejected MFU fragment: code=${error.code} ` +
|
|
1178
|
+
`mpu=${mpu.mpuSequenceNumber} fi=${mpu.fragmentationIndicator} ` +
|
|
1179
|
+
`counter=${mpu.fragmentCounter}`,
|
|
1180
|
+
)
|
|
1181
|
+
return {
|
|
1182
|
+
kind: 'rejected',
|
|
1183
|
+
packet: info,
|
|
1184
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
1185
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
1186
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
1187
|
+
error: { code: error.code, message: error.message },
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (!assembled) {
|
|
1192
|
+
return {
|
|
1193
|
+
kind: 'pending',
|
|
1194
|
+
packet: info,
|
|
1195
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
1196
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
1197
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const movieFragmentSequenceNumber = assembled.movieFragmentSequenceNumber
|
|
1202
|
+
const sampleNumber = assembled.sampleNumber
|
|
1203
|
+
if (movieFragmentSequenceNumber === undefined || sampleNumber === undefined)
|
|
1204
|
+
return this.#drop('assembled-identity-missing')
|
|
1205
|
+
// Resolve authored timing BEFORE the combiner sees this data unit. An
|
|
1206
|
+
// assembled data unit whose sample identity has no authored timing is not
|
|
1207
|
+
// a sample this stream declared, so it must be dropped outright -- letting
|
|
1208
|
+
// it buffer would occupy a combiner slot and could evict a legitimate
|
|
1209
|
+
// partitioned sample waiting on a sibling.
|
|
1210
|
+
if (!resolver.sampleTiming(
|
|
1211
|
+
assembled.mpuSequenceNumber,
|
|
1212
|
+
movieFragmentSequenceNumber,
|
|
1213
|
+
sampleNumber,
|
|
1214
|
+
)) return this.#drop('assembled-timing-missing')
|
|
1215
|
+
if (assembled.mpuSequenceNumber !== mpu.mpuSequenceNumber)
|
|
1216
|
+
return this.#drop('assembled-mpu-mismatch')
|
|
1217
|
+
|
|
1218
|
+
// One reassembled data unit is not necessarily one whole sample. A
|
|
1219
|
+
// partitioned sample completes only once every sibling data unit has
|
|
1220
|
+
// arrived and they are byte-adjacent from zero to the authored size; an
|
|
1221
|
+
// unpartitioned sample passes straight through with no buffering.
|
|
1222
|
+
let combined: CombinedSample | null
|
|
1223
|
+
try {
|
|
1224
|
+
combined = this.#sampleCombinerFor(streamKey).addMfu(assembled)
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
if (!(error instanceof SampleCombineError)) throw error
|
|
1227
|
+
console.warn(
|
|
1228
|
+
`[MmtpContainerParser] rejected subsample data unit: code=${error.code} ` +
|
|
1229
|
+
`mpu=${assembled.mpuSequenceNumber} sample=${sampleNumber} ` +
|
|
1230
|
+
`offset=${assembled.sampleOffset ?? 0}`,
|
|
1231
|
+
)
|
|
1232
|
+
return {
|
|
1233
|
+
kind: 'rejected',
|
|
1234
|
+
packet: info,
|
|
1235
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
1236
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
1237
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
1238
|
+
error: { code: error.code, message: error.message },
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (!combined) {
|
|
1242
|
+
return {
|
|
1243
|
+
kind: 'pending',
|
|
1244
|
+
packet: info,
|
|
1245
|
+
mpuSequenceNumber: mpu.mpuSequenceNumber,
|
|
1246
|
+
fragmentationIndicator: mpu.fragmentationIndicator,
|
|
1247
|
+
fragmentCounter: mpu.fragmentCounter,
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const outcome = this.#emitCombinedSample(
|
|
1252
|
+
combined,
|
|
1253
|
+
info,
|
|
1254
|
+
resolver,
|
|
1255
|
+
options?.recovered === true,
|
|
1256
|
+
)
|
|
1257
|
+
if (outcome.record !== undefined) return outcome.record
|
|
1258
|
+
return this.#drop(outcome.reason)
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Deliver one combined sample: resolve its authored timing, build frame
|
|
1263
|
+
* metadata, retire the timing entry, and route it. Shared by the ordinary
|
|
1264
|
+
* completion path and by the rarer completion that falls out of registering
|
|
1265
|
+
* an authored size for data units that arrived before it.
|
|
1266
|
+
*
|
|
1267
|
+
* Reports a refusal as a reason rather than counting it, because the two
|
|
1268
|
+
* call sites owe different accounting. On `#feed()`'s tail return the
|
|
1269
|
+
* refusal *is* that feed()'s outcome and must be attributed; on the
|
|
1270
|
+
* register-completion path it belongs to an earlier sample while this
|
|
1271
|
+
* packet's own fragment continues to its own outcome, so counting it there
|
|
1272
|
+
* would push `outcomes` past `fedObjects` — the very invariant the
|
|
1273
|
+
* attribution exists to keep readable. Leaving the choice at the call site
|
|
1274
|
+
* is what keeps both true.
|
|
1275
|
+
*/
|
|
1276
|
+
#emitCombinedSample(
|
|
1277
|
+
combined: CombinedSample,
|
|
1278
|
+
info: PacketInfo,
|
|
1279
|
+
resolver: IsoBmffTimingResolver,
|
|
1280
|
+
recovered: boolean,
|
|
1281
|
+
): CombinedSampleOutcome {
|
|
1282
|
+
const timing = resolver.sampleTiming(
|
|
1283
|
+
combined.mpuSequenceNumber,
|
|
1284
|
+
combined.movieFragmentSequenceNumber,
|
|
1285
|
+
combined.sampleNumber,
|
|
1286
|
+
)
|
|
1287
|
+
if (!timing) return { reason: 'combined-timing-missing' }
|
|
1288
|
+
// The combiner already completes against the registered authored size;
|
|
1289
|
+
// re-checking here keeps the unpartitioned passthrough path fail-closed
|
|
1290
|
+
// on the same invariant rather than trusting it by construction.
|
|
1291
|
+
if (combined.data.byteLength !== timing.size) return { reason: 'combined-size-mismatch' }
|
|
1292
|
+
|
|
1293
|
+
const meta = this.#buildFrameMeta(
|
|
1294
|
+
combined,
|
|
1295
|
+
info.packetId,
|
|
1296
|
+
info.sequenceNumber,
|
|
1297
|
+
timing,
|
|
1298
|
+
recovered,
|
|
1299
|
+
)
|
|
1300
|
+
if (!meta) return { reason: 'combined-meta-build-failed' }
|
|
1301
|
+
if (!resolver.consumeSample(
|
|
1302
|
+
combined.mpuSequenceNumber,
|
|
1303
|
+
combined.movieFragmentSequenceNumber,
|
|
1304
|
+
combined.sampleNumber,
|
|
1305
|
+
timing,
|
|
1306
|
+
)) return { reason: 'combined-timing-consume-refused' }
|
|
1307
|
+
this.#pendingTimingSamples--
|
|
1308
|
+
this.routeFrame(combined.data, meta)
|
|
1309
|
+
return {
|
|
1310
|
+
record: {
|
|
1311
|
+
kind: 'frame',
|
|
1312
|
+
packet: info,
|
|
1313
|
+
data: combined.data,
|
|
1314
|
+
meta,
|
|
1315
|
+
mpuSequenceNumber: combined.mpuSequenceNumber,
|
|
1316
|
+
fragmentCount: combined.fragmentCount,
|
|
1317
|
+
rapFlag: combined.rapFlag,
|
|
1318
|
+
...(combined.priority === undefined ? {} : { priority: combined.priority }),
|
|
1319
|
+
},
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
onInit(cb: InitCallback): void {
|
|
1324
|
+
this.#initCb = cb
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
onFrame(cb: FrameCallback): void {
|
|
1328
|
+
this.#frameCb = cb
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/** Register a per-packet FEC source-symbol callback. Fires for each MMTP
|
|
1332
|
+
* source MPU packet that carries a Source FEC Payload ID (ISO 23008-1 §C.5.2).
|
|
1333
|
+
*
|
|
1334
|
+
* Consumer responsibilities:
|
|
1335
|
+
* - Zero-pad `rawSymbol` to the FEC `symbolSize` if shorter (FEC config is
|
|
1336
|
+
* out-of-band — typically from the CMSF catalog's `fec.symbolSize`).
|
|
1337
|
+
* - Decide oversized handling (rawSymbol.length > symbolSize is not FEC-
|
|
1338
|
+
* decodable; consumer typically skips that block's source-feed).
|
|
1339
|
+
* - Look up the right FEC consumer (e.g. FecWorkerClient) by packetId.
|
|
1340
|
+
*
|
|
1341
|
+
* Designed for spec-clean reuse: Shaka, upstream moqtail, or any consumer
|
|
1342
|
+
* that uses MmtpContainerParser can wire FEC source delivery without
|
|
1343
|
+
* pulling in MulticastManager/MmtpRouter/FECManager. */
|
|
1344
|
+
onSourceSymbol(cb: SourceSymbolCallback): void {
|
|
1345
|
+
this.#sourceSymbolCb = cb
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
onTrackFrame(packetId: number, cb: FrameCallback): void {
|
|
1349
|
+
this.#trackFrameCbs.set(packetId, cb)
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
setTrack(packetId: number, info: TrackInfo): void {
|
|
1353
|
+
this.#validateReassemblyPolicy()
|
|
1354
|
+
for (const [key, entry] of this.#sequencers) {
|
|
1355
|
+
if (entry.packetId !== packetId) continue
|
|
1356
|
+
entry.sequencer.flush()
|
|
1357
|
+
this.#sequencers.delete(key)
|
|
1358
|
+
}
|
|
1359
|
+
for (const [key, state] of this.#timingStreams) {
|
|
1360
|
+
if (state.packetId === packetId) this.#dropTimingStream(key)
|
|
1361
|
+
}
|
|
1362
|
+
for (const key of [...this.#sampleCombiners.keys()]) {
|
|
1363
|
+
if ((JSON.parse(key) as [number, string | null])[0] === packetId) {
|
|
1364
|
+
this.#dropSampleCombiner(key)
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
for (const [key, gatedPacketId] of this.#reliableMoqStartupGate) {
|
|
1368
|
+
if (gatedPacketId === packetId) this.#reliableMoqStartupGate.delete(key)
|
|
1369
|
+
}
|
|
1370
|
+
this.#tracks.set(packetId, info)
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/** Drop one physical stream's ordering and MFU state for retained replay. */
|
|
1374
|
+
resetTrack(packetId: number, catalogTrackName: string): void {
|
|
1375
|
+
if (!catalogTrackName) {
|
|
1376
|
+
throw new Error('[MmtpContainerParser] catalogTrackName is required for track reset')
|
|
1377
|
+
}
|
|
1378
|
+
for (const [key, entry] of this.#sequencers) {
|
|
1379
|
+
if (key !== this.#streamKey(packetId, catalogTrackName)) continue
|
|
1380
|
+
entry.sequencer.discard()
|
|
1381
|
+
this.#sequencers.delete(key)
|
|
1382
|
+
}
|
|
1383
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
1384
|
+
this.#dropTimingStream(streamKey)
|
|
1385
|
+
this.#reliableMoqStartupGate.delete(streamKey)
|
|
1386
|
+
this.#reassembler.resetStream(catalogTrackName)
|
|
1387
|
+
// Drop half-combined samples too: a partitioned sample whose siblings
|
|
1388
|
+
// straddle the reset would otherwise complete against a stale authored
|
|
1389
|
+
// size and hand the decoder bytes from two different publisher epochs.
|
|
1390
|
+
this.#dropSampleCombiner(streamKey)
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
setMfuTimeoutMs(ms: number | null): void {
|
|
1394
|
+
if (ms !== null && ms <= 0) throw new Error('[MmtpContainerParser] mfuTimeoutMs must be > 0')
|
|
1395
|
+
this.#reassembler.timeoutMs = ms
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
setReassemblyPolicy(policy: MfuReassemblyPolicy): void {
|
|
1399
|
+
const timeoutMs = mfuReassemblyTimeoutMs(policy)
|
|
1400
|
+
this.#validateReassemblyPolicy(policy)
|
|
1401
|
+
this.#flushSequencers()
|
|
1402
|
+
this.#reassemblyPolicy = policy
|
|
1403
|
+
this.#reliableMoqStartupGate.clear()
|
|
1404
|
+
this.setMfuTimeoutMs(timeoutMs)
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
setReorderWindow(windowSize: number): void {
|
|
1408
|
+
if (!Number.isFinite(windowSize) || windowSize < 0) {
|
|
1409
|
+
throw new Error('[MmtpContainerParser] reorderWindow must be a finite number >= 0')
|
|
1410
|
+
}
|
|
1411
|
+
const nextWindow = Math.ceil(windowSize)
|
|
1412
|
+
this.#validateReassemblyPolicy(this.#reassemblyPolicy, nextWindow)
|
|
1413
|
+
this.#flushSequencers()
|
|
1414
|
+
this.#reorderWindow = nextWindow
|
|
1415
|
+
if (this.#reorderWindow <= 0) this.#sequencers.clear()
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/** Route an already-assembled frame through the same reorder/callback path
|
|
1419
|
+
* used by source packets. FEC recovery uses this to merge recovered frames
|
|
1420
|
+
* with direct source frames before the decoder sees them. */
|
|
1421
|
+
routeFrame(data: Uint8Array, meta: FrameMeta): void {
|
|
1422
|
+
const seq = meta.packetSequenceNumber
|
|
1423
|
+
const sequencer = this.#sequencerFor(meta.packetId, meta.trackType, meta.catalogTrackName)
|
|
1424
|
+
if (sequencer && seq !== undefined && Number.isFinite(seq)) {
|
|
1425
|
+
sequencer.push(data, meta, seq)
|
|
1426
|
+
return
|
|
1427
|
+
}
|
|
1428
|
+
if (this.#reassemblyPolicy?.transport === 'reliable-moq' &&
|
|
1429
|
+
meta.trackType === 'video' &&
|
|
1430
|
+
!this.#passReliableMoqStartupGate(meta)) {
|
|
1431
|
+
return
|
|
1432
|
+
}
|
|
1433
|
+
this.#emitFrame(data, meta)
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Claim and return the authored timing for a recovered sample. Recovery does
|
|
1438
|
+
* not pass through the ordinary `feed()` completion path, so it must retire
|
|
1439
|
+
* the same bounded resolver entry once the router accepts the recovered
|
|
1440
|
+
* sample. A missing entry is a fail-closed signal: callers must not replace
|
|
1441
|
+
* it with an MMTP send timestamp or cadence guess.
|
|
1442
|
+
*/
|
|
1443
|
+
consumeAuthoredTimingFor(
|
|
1444
|
+
packetId: number,
|
|
1445
|
+
mpuSequenceNumber: number,
|
|
1446
|
+
movieFragmentSequenceNumber: number | undefined,
|
|
1447
|
+
sampleNumber: number | undefined,
|
|
1448
|
+
catalogTrackName?: string,
|
|
1449
|
+
): AuthoredTiming | null {
|
|
1450
|
+
if (movieFragmentSequenceNumber === undefined || sampleNumber === undefined) return null
|
|
1451
|
+
const resolver = this.#timingResolver(
|
|
1452
|
+
this.#streamKey(packetId, catalogTrackName),
|
|
1453
|
+
packetId,
|
|
1454
|
+
false,
|
|
1455
|
+
)
|
|
1456
|
+
const timing = resolver?.sampleTiming(
|
|
1457
|
+
mpuSequenceNumber,
|
|
1458
|
+
movieFragmentSequenceNumber,
|
|
1459
|
+
sampleNumber,
|
|
1460
|
+
)
|
|
1461
|
+
if (!timing || !resolver || !resolver.consumeSample(
|
|
1462
|
+
mpuSequenceNumber,
|
|
1463
|
+
movieFragmentSequenceNumber,
|
|
1464
|
+
sampleNumber,
|
|
1465
|
+
timing,
|
|
1466
|
+
)) return null
|
|
1467
|
+
this.#pendingTimingSamples--
|
|
1468
|
+
if (this.#pendingTimingSamples < 0) {
|
|
1469
|
+
throw new Error('[MmtpContainerParser] pending timing sample accounting underflow')
|
|
1470
|
+
}
|
|
1471
|
+
return {
|
|
1472
|
+
decodeTime: timing.decodeTime,
|
|
1473
|
+
presentationTime: timing.presentationTime,
|
|
1474
|
+
durationTicks: timing.duration,
|
|
1475
|
+
mediaTimescale: timing.timescale,
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/** Order-independent reliable-moq startup gate: discard delta frames for a
|
|
1480
|
+
* stream until its first RAP is observed, without buffering/reordering. */
|
|
1481
|
+
#passReliableMoqStartupGate(meta: FrameMeta): boolean {
|
|
1482
|
+
const streamKey = this.#streamKey(meta.packetId, meta.catalogTrackName)
|
|
1483
|
+
if (meta.sourceDiscontinuity) this.#reliableMoqStartupGate.delete(streamKey)
|
|
1484
|
+
if (this.#reliableMoqStartupGate.has(streamKey)) return true
|
|
1485
|
+
if (!meta.keyframe) {
|
|
1486
|
+
this.#reorderStats.droppedBeforeStartupKeyframe++
|
|
1487
|
+
return false
|
|
1488
|
+
}
|
|
1489
|
+
this.#reliableMoqStartupGate.set(streamKey, meta.packetId)
|
|
1490
|
+
return true
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/** Emit all ordered frames currently buffered by the parser. Finite-stream
|
|
1494
|
+
* consumers may call this before dispose(); live cancellation should not. */
|
|
1495
|
+
flush(): void {
|
|
1496
|
+
if (this.#disposed) return
|
|
1497
|
+
this.#flushSequencers()
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
dispose(): void {
|
|
1501
|
+
this.#disposed = true
|
|
1502
|
+
for (const { sequencer } of this.#sequencers.values()) sequencer.discard()
|
|
1503
|
+
this.#sequencers.clear()
|
|
1504
|
+
this.#timingStreams.clear()
|
|
1505
|
+
this.#pendingTimingSamples = 0
|
|
1506
|
+
this.#reliableMoqStartupGate.clear()
|
|
1507
|
+
this.#reassembler.reset()
|
|
1508
|
+
this.#frameCb = null
|
|
1509
|
+
this.#initCb = null
|
|
1510
|
+
this.#trackFrameCbs.clear()
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// ── Private ─────────────────────────────────────────────────────
|
|
1514
|
+
|
|
1515
|
+
#timingResolver(
|
|
1516
|
+
streamKey: string,
|
|
1517
|
+
packetId: number,
|
|
1518
|
+
create: boolean,
|
|
1519
|
+
): IsoBmffTimingResolver | null {
|
|
1520
|
+
const existing = this.#timingStreams.get(streamKey)
|
|
1521
|
+
if (existing) return existing.resolver
|
|
1522
|
+
if (!create) return null
|
|
1523
|
+
const resolver = new IsoBmffTimingResolver(this.#maxPendingTimingSamples)
|
|
1524
|
+
this.#timingStreams.set(streamKey, { packetId, resolver })
|
|
1525
|
+
return resolver
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
#dropTimingStream(streamKey: string): void {
|
|
1529
|
+
const state = this.#timingStreams.get(streamKey)
|
|
1530
|
+
if (!state) return
|
|
1531
|
+
this.#timingStreams.delete(streamKey)
|
|
1532
|
+
this.#pendingTimingSamples -= state.resolver.pendingSampleCount
|
|
1533
|
+
if (this.#pendingTimingSamples < 0) {
|
|
1534
|
+
throw new Error('[MmtpContainerParser] pending timing sample accounting underflow')
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
#validateReassemblyPolicy(
|
|
1539
|
+
policy: MfuReassemblyPolicy | null = this.#reassemblyPolicy,
|
|
1540
|
+
reorderWindow = this.#reorderWindow,
|
|
1541
|
+
): void {
|
|
1542
|
+
if (!policy) return
|
|
1543
|
+
mmtpFrameReorderWindowFrames(policy, reorderWindow)
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
#buildFrameMeta(
|
|
1547
|
+
assembled: CombinedSample,
|
|
1548
|
+
packetId: number,
|
|
1549
|
+
seq: number,
|
|
1550
|
+
timing: IsoBmffSampleTiming,
|
|
1551
|
+
recovered = false,
|
|
1552
|
+
): FrameMeta | null {
|
|
1553
|
+
const { data, rapFlag, priority } = assembled
|
|
1554
|
+
|
|
1555
|
+
if (isFmp4Box(data)) return null
|
|
1556
|
+
const presentationTimestamp = mediaTicksToMicroseconds(
|
|
1557
|
+
timing.presentationTime,
|
|
1558
|
+
timing.timescale,
|
|
1559
|
+
)
|
|
1560
|
+
const decodeTimestamp = mediaTicksToMicroseconds(timing.decodeTime, timing.timescale)
|
|
1561
|
+
const duration = mediaTicksToMicroseconds(BigInt(timing.duration), timing.timescale)
|
|
1562
|
+
if (presentationTimestamp === null || decodeTimestamp === null || duration === null ||
|
|
1563
|
+
duration <= 0) {
|
|
1564
|
+
return null
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
let track = this.#tracks.get(packetId)
|
|
1568
|
+
if (!track && this.#defaultTrack) {
|
|
1569
|
+
track = this.#defaultTrack
|
|
1570
|
+
this.#tracks.set(packetId, track)
|
|
1571
|
+
}
|
|
1572
|
+
const codec = track?.codec ?? ''
|
|
1573
|
+
|
|
1574
|
+
const keyframe = MmtpContainerParser.isKeyframe(data, isMfuKeyframeMetadata(rapFlag, priority), codec)
|
|
1575
|
+
|
|
1576
|
+
const meta: FrameMeta = {
|
|
1577
|
+
timestamp: presentationTimestamp,
|
|
1578
|
+
decodeTimestamp,
|
|
1579
|
+
duration,
|
|
1580
|
+
decodeTime: timing.decodeTime,
|
|
1581
|
+
presentationTime: timing.presentationTime,
|
|
1582
|
+
durationTicks: timing.duration,
|
|
1583
|
+
mediaTimescale: timing.timescale,
|
|
1584
|
+
authoredTiming: {
|
|
1585
|
+
decodeTime: timing.decodeTime,
|
|
1586
|
+
presentationTime: timing.presentationTime,
|
|
1587
|
+
durationTicks: timing.duration,
|
|
1588
|
+
mediaTimescale: timing.timescale,
|
|
1589
|
+
},
|
|
1590
|
+
keyframe,
|
|
1591
|
+
packetId,
|
|
1592
|
+
trackType: track?.type ?? null,
|
|
1593
|
+
packetSequenceNumber: seq,
|
|
1594
|
+
...(assembled.packetSequenceNumbers === undefined
|
|
1595
|
+
? {}
|
|
1596
|
+
: { packetSequenceNumbers: assembled.packetSequenceNumbers }),
|
|
1597
|
+
...(assembled.repairedPacketSequenceNumbers === undefined
|
|
1598
|
+
? {}
|
|
1599
|
+
: { repairedPacketSequenceNumbers: assembled.repairedPacketSequenceNumbers }),
|
|
1600
|
+
...(assembled.catalogTrackName === undefined
|
|
1601
|
+
? {}
|
|
1602
|
+
: { catalogTrackName: assembled.catalogTrackName }),
|
|
1603
|
+
...(assembled.mmtpTimestamp === undefined
|
|
1604
|
+
? {}
|
|
1605
|
+
: { mmtpTimestamp: assembled.mmtpTimestamp }),
|
|
1606
|
+
mpuSequenceNumber: assembled.mpuSequenceNumber,
|
|
1607
|
+
...(assembled.movieFragmentSequenceNumber === undefined
|
|
1608
|
+
? {}
|
|
1609
|
+
: { movieFragmentSequenceNumber: assembled.movieFragmentSequenceNumber }),
|
|
1610
|
+
...(assembled.sampleNumber === undefined ? {} : { sampleNumber: assembled.sampleNumber }),
|
|
1611
|
+
fragmentCount: assembled.fragmentCount,
|
|
1612
|
+
rapFlag,
|
|
1613
|
+
...(priority === undefined ? {} : { priority }),
|
|
1614
|
+
...(assembled.source === undefined ? {} : { source: assembled.source }),
|
|
1615
|
+
deliveryProvenance: assembled.deliveryProvenance,
|
|
1616
|
+
}
|
|
1617
|
+
if (recovered || (assembled.repairedPacketSequenceNumbers?.length ?? 0) > 0) {
|
|
1618
|
+
meta.recovered = true
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
return meta
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
#sequencerFor(
|
|
1625
|
+
packetId: number,
|
|
1626
|
+
_trackType: FrameMeta['trackType'],
|
|
1627
|
+
catalogTrackName?: string,
|
|
1628
|
+
): FrameSequencer | null {
|
|
1629
|
+
const windowSize = this.#reassemblyPolicy
|
|
1630
|
+
? mmtpFrameReorderWindowFrames(this.#reassemblyPolicy, this.#reorderWindow)
|
|
1631
|
+
: this.#reorderWindow
|
|
1632
|
+
if (windowSize <= 0) return null
|
|
1633
|
+
const streamKey = this.#streamKey(packetId, catalogTrackName)
|
|
1634
|
+
let sequencer = this.#sequencers.get(streamKey)?.sequencer
|
|
1635
|
+
if (!sequencer) {
|
|
1636
|
+
sequencer = new FrameSequencer(windowSize, (data, meta) => {
|
|
1637
|
+
this.#emitFrame(data, meta)
|
|
1638
|
+
}, (stat, count = 1) => {
|
|
1639
|
+
this.#reorderStats[stat] += count
|
|
1640
|
+
})
|
|
1641
|
+
this.#sequencers.set(streamKey, { packetId, sequencer })
|
|
1642
|
+
}
|
|
1643
|
+
return sequencer
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
#flushSequencers(): void {
|
|
1647
|
+
for (const { sequencer } of this.#sequencers.values()) sequencer.flush()
|
|
1648
|
+
this.#sequencers.clear()
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
/** Authored presentation timestamp of the last emitted frame, in microseconds. */
|
|
1652
|
+
lastFrameTimestamp: number | undefined = undefined
|
|
1653
|
+
|
|
1654
|
+
#emitFrame(data: Uint8Array, meta: FrameMeta): void {
|
|
1655
|
+
this.lastFrameTimestamp = meta.timestamp
|
|
1656
|
+
const trackCb = this.#trackFrameCbs.get(meta.packetId)
|
|
1657
|
+
trackCb?.(data, meta)
|
|
1658
|
+
this.#frameCb?.(data, meta)
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// ── Static utilities ──────────────────────────────────────────
|
|
1662
|
+
|
|
1663
|
+
static isKeyframe(data: Uint8Array, rapFlag: boolean, codec = ''): boolean {
|
|
1664
|
+
const payloadKeyframe = MmtpContainerParser.#scanForKeyframe(data, codec)
|
|
1665
|
+
if (payloadKeyframe !== undefined) return payloadKeyframe
|
|
1666
|
+
return rapFlag
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
static #scanForKeyframe(data: Uint8Array, codec: string): boolean | undefined {
|
|
1670
|
+
const c = codec.toLowerCase()
|
|
1671
|
+
if (data.length === 0) return undefined
|
|
1672
|
+
|
|
1673
|
+
if (c.startsWith('av01')) {
|
|
1674
|
+
return scanAv1ObusForKeyframe(data)
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
if (c.startsWith('vp09') || c.startsWith('vp8')) {
|
|
1678
|
+
return (data[0]! & 0x04) === 0
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
const isHevc = c.startsWith('hev1') || c.startsWith('hvc1')
|
|
1682
|
+
if (isHevc) {
|
|
1683
|
+
if (data.length < 5) return undefined
|
|
1684
|
+
if (startsWithAnnexB(data)) {
|
|
1685
|
+
return MmtpContainerParser.#scanAnnexBForKeyframe(data, true)
|
|
1686
|
+
}
|
|
1687
|
+
return MmtpContainerParser.#scanLengthPrefixedHevcForKeyframe(data)
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
if (c === '' || c.startsWith('avc')) {
|
|
1691
|
+
return classifyH264Keyframe(data, { allowPartialAvcc: true })
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
return undefined
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
static #scanLengthPrefixedHevcForKeyframe(data: Uint8Array): boolean | undefined {
|
|
1698
|
+
let sawNal = false
|
|
1699
|
+
let offset = 0
|
|
1700
|
+
while (offset + 4 < data.length) {
|
|
1701
|
+
const nalLen =
|
|
1702
|
+
(data[offset]! << 24) |
|
|
1703
|
+
(data[offset + 1]! << 16) |
|
|
1704
|
+
(data[offset + 2]! << 8) |
|
|
1705
|
+
data[offset + 3]!
|
|
1706
|
+
offset += 4
|
|
1707
|
+
if (nalLen <= 0) return sawNal ? false : undefined
|
|
1708
|
+
// The router's sync probe may see only the first fragment of a large
|
|
1709
|
+
// length-prefixed NAL. The length can exceed this packet, but the NAL
|
|
1710
|
+
// header is still enough to classify IRAP/IDR and open the sync gate.
|
|
1711
|
+
if (offset >= data.length) return sawNal ? false : undefined
|
|
1712
|
+
sawNal = true
|
|
1713
|
+
const hevcType = (data[offset]! >> 1) & 0x3f
|
|
1714
|
+
if (hevcType >= 16 && hevcType <= 21) return true
|
|
1715
|
+
if (offset + nalLen > data.length) break
|
|
1716
|
+
offset += nalLen
|
|
1717
|
+
}
|
|
1718
|
+
return sawNal ? false : undefined
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
static #scanAnnexBForKeyframe(data: Uint8Array, isHevc: boolean): boolean | undefined {
|
|
1722
|
+
let offset = 0
|
|
1723
|
+
let sawNal = false
|
|
1724
|
+
while (offset < data.length) {
|
|
1725
|
+
const start = findAnnexBStart(data, offset)
|
|
1726
|
+
if (!start) return sawNal ? false : undefined
|
|
1727
|
+
const headerOffset = start.offset + start.length
|
|
1728
|
+
if (headerOffset >= data.length) return sawNal ? false : undefined
|
|
1729
|
+
sawNal = true
|
|
1730
|
+
if (isHevc) {
|
|
1731
|
+
if (headerOffset + 1 >= data.length) return sawNal ? false : undefined
|
|
1732
|
+
const hevcType = (data[headerOffset]! >> 1) & 0x3f
|
|
1733
|
+
if (hevcType >= 16 && hevcType <= 21) return true
|
|
1734
|
+
} else {
|
|
1735
|
+
const nalType = data[headerOffset]! & 0x1f
|
|
1736
|
+
if (nalType === 5) return true
|
|
1737
|
+
}
|
|
1738
|
+
offset = headerOffset + 1
|
|
1739
|
+
}
|
|
1740
|
+
return sawNal ? false : undefined
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
function scanAv1ObusForKeyframe(data: Uint8Array): boolean | undefined {
|
|
1745
|
+
let offset = 0
|
|
1746
|
+
let sawSequenceHeader = false
|
|
1747
|
+
let sawFrame = false
|
|
1748
|
+
|
|
1749
|
+
while (offset < data.length) {
|
|
1750
|
+
const obu = readAv1Obu(data, offset)
|
|
1751
|
+
if (!obu) return sawFrame ? false : undefined
|
|
1752
|
+
offset = obu.nextOffset
|
|
1753
|
+
|
|
1754
|
+
if (obu.type === 1) {
|
|
1755
|
+
sawSequenceHeader = true
|
|
1756
|
+
continue
|
|
1757
|
+
}
|
|
1758
|
+
if (obu.type !== 3 && obu.type !== 6) continue
|
|
1759
|
+
|
|
1760
|
+
sawFrame = true
|
|
1761
|
+
if (!sawSequenceHeader) {
|
|
1762
|
+
continue
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
const keyframe = readAv1FrameHeaderKeyframe(data.subarray(obu.payloadOffset, obu.payloadEnd))
|
|
1766
|
+
if (keyframe !== undefined) return keyframe
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
return sawFrame ? false : undefined
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function readAv1Obu(
|
|
1773
|
+
data: Uint8Array,
|
|
1774
|
+
offset: number,
|
|
1775
|
+
): { type: number; payloadOffset: number; payloadEnd: number; nextOffset: number } | null {
|
|
1776
|
+
if (offset >= data.length) return null
|
|
1777
|
+
const header = data[offset]!
|
|
1778
|
+
if ((header & 0x80) !== 0 || (header & 0x01) !== 0) return null
|
|
1779
|
+
|
|
1780
|
+
const type = (header >> 3) & 0x0f
|
|
1781
|
+
const extensionFlag = (header & 0x04) !== 0
|
|
1782
|
+
const hasSizeField = (header & 0x02) !== 0
|
|
1783
|
+
let cursor = offset + 1
|
|
1784
|
+
if (extensionFlag) cursor += 1
|
|
1785
|
+
if (cursor > data.length) return null
|
|
1786
|
+
|
|
1787
|
+
let payloadSize = data.length - cursor
|
|
1788
|
+
if (hasSizeField) {
|
|
1789
|
+
const leb = readLeb128(data, cursor)
|
|
1790
|
+
if (!leb) return null
|
|
1791
|
+
payloadSize = leb.value
|
|
1792
|
+
cursor = leb.nextOffset
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
const payloadEnd = cursor + payloadSize
|
|
1796
|
+
if (payloadEnd > data.length) return null
|
|
1797
|
+
return { type, payloadOffset: cursor, payloadEnd, nextOffset: payloadEnd }
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
function readLeb128(data: Uint8Array, offset: number): { value: number; nextOffset: number } | null {
|
|
1801
|
+
let value = 0
|
|
1802
|
+
for (let i = 0; i < 8; i++) {
|
|
1803
|
+
if (offset + i >= data.length) return null
|
|
1804
|
+
const byte = data[offset + i]!
|
|
1805
|
+
value += (byte & 0x7f) * (2 ** (i * 7))
|
|
1806
|
+
if ((byte & 0x80) === 0) return { value, nextOffset: offset + i + 1 }
|
|
1807
|
+
}
|
|
1808
|
+
return null
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function readAv1FrameHeaderKeyframe(payload: Uint8Array): boolean | undefined {
|
|
1812
|
+
if (payload.length === 0) return undefined
|
|
1813
|
+
const first = payload[0]!
|
|
1814
|
+
const showExistingFrame = (first & 0x80) !== 0
|
|
1815
|
+
if (showExistingFrame) return false
|
|
1816
|
+
const frameType = (first >> 5) & 0x03
|
|
1817
|
+
return frameType === 0
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function startsWithAnnexB(data: Uint8Array): boolean {
|
|
1821
|
+
return (
|
|
1822
|
+
(data.length >= 4 && data[0] === 0 && data[1] === 0 && data[2] === 1) ||
|
|
1823
|
+
(data.length >= 5 && data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 1)
|
|
1824
|
+
)
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function findAnnexBStart(data: Uint8Array, offset: number): { offset: number; length: 3 | 4 } | null {
|
|
1828
|
+
for (let i = offset; i + 3 <= data.length; i++) {
|
|
1829
|
+
if (data[i] !== 0 || data[i + 1] !== 0) continue
|
|
1830
|
+
if (data[i + 2] === 1) return { offset: i, length: 3 }
|
|
1831
|
+
if (i + 3 < data.length && data[i + 2] === 0 && data[i + 3] === 1) {
|
|
1832
|
+
return { offset: i, length: 4 }
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return null
|
|
1836
|
+
}
|