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