@blockcast/mmt-container 0.1.0 → 0.3.0
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,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../mmt-alta-parse/src/index.ts", "../../mmt-base/src/mmtp.ts", "../../mmt-base/src/ntp-short.ts", "../../mmt-base/src/fec-block-profile.ts", "../src/catalog-fec-params.ts", "../src/multicast-auth-catalog.ts", "../src/repair-layers.ts", "../src/carp-catalog.ts", "../src/fec-state.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Zero-dependency helper for extracting the detached ALTA authenticator trailer\n * from an MMTP source packet.\n *\n * Lives in its own leaf package so both `@blockcast/mmt-container` (wire-path\n * parsing) and `@blockcast/fec-worker` (FEC-recovered-source parsing) can share\n * one implementation without creating a package cycle between them.\n *\n * Spec: ISO/IEC 23008-1:2023 + draft-ramadan-moq-mmt \u00A712 (detached trailer, Path 2).\n */\n\n// --- Private MMTP/MPU/MFU constants (ISO/IEC 23008-1 \u00A79.2.2\u20139.2.3) ---\n// Duplicated here as immutable spec values so this package stays a leaf with\n// zero `@blockcast/*` dependencies. Re-deriving from `mmt-container` would\n// reintroduce the package cycle this split exists to avoid.\n\nconst MMTP_HEADER_SIZE = 12\nconst MPU_HEADER_SIZE = 8\nconst MPU_LENGTH_FIELD_SIZE = 2\nconst MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE\nconst TIMED_MFU_DU_HEADER_SIZE = 14\nconst NONTIMED_MFU_DU_HEADER_SIZE = 4\nconst PACKET_TYPE_MPU = 0x00\n\n// --- Public ALTA trailer constants ---\n\n/**\n * MMTP header-extension ext_type assigned to the detached ALTA authenticator\n * trailer (Path 2 / 08-07). Carried in an `[ext_type][ext_length][auth]`\n * block appended after the MPU's declared payload bytes, before the 4-byte\n * Source FEC Payload ID. MMTP X-bit stays 0 \u2014 non-ALTA MMTP receivers treat\n * the trailer as bytes past `payload_length` and skip it (per ISO 23008-1\n * \u00A79.2.2 \"may be discarded without impacting correct processing\").\n */\nexport const MMTP_EXT_TYPE_ALTA_AUTH = 0xf0a1\n\n/** Fixed size of the trailer's `[ext_type][ext_length]` prefix (2 + 2 BE). */\nexport const MMTP_EXT_TRAILER_HEADER_SIZE = 4\n\n/** Result of extracting the detached ALTA authenticator from an MMTP packet. */\nexport interface AltaTrailer {\n /** Authenticator bytes (no ext_type/ext_length prefix, no trailing padding). */\n auth: Uint8Array\n /**\n * Wire-packet byte offset where the signed payload ends, i.e. where the\n * trailer begins. Caller uses this to produce\n * `payload = wirePacket.subarray(0, signedEnd)` for verify_detached.\n */\n signedEnd: number\n}\n\n/**\n * Extract the detached ALTA authenticator trailer from an MMTP source packet.\n *\n * Wire layout:\n * `[MMTP hdr][length(2)][remaining MPU hdr(6)][DU?][media][TRAILER][SS_ID?]`\n * `payload_length = 6 + DU bytes + media bytes`\n *\n * Returns `null` when:\n * - the packet is too small / malformed,\n * - `payload_length` cannot contain the remaining six MPU-header bytes,\n * - the declared payload overruns the packet,\n * - no bytes remain between `payload_length` and the SS_ID,\n * - the trailer's `ext_type` isn't {@link MMTP_EXT_TYPE_ALTA_AUTH},\n * - the declared `ext_length` would extend past the SS_ID boundary.\n *\n * Non-MPU packets (repair, signaling) always return `null`.\n *\n * @param options.skipFecId When true, do NOT reserve 4 trailing bytes for the\n * Source FEC Payload ID. Use on FEC-recovered symbols where the PID has\n * already been stripped upstream.\n */\nexport function extractAltaTrailer(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\n): AltaTrailer | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n // ISO/IEC 23008-1:2023 Figure 8 places V=0 packet_type in the low six bits.\n const packetType = mmtpPacket[1]! & 0x3f\n if (packetType !== PACKET_TYPE_MPU) return null\n\n const fecType = (mmtpPacket[0]! >> 3) & 0x03\n let headerLength = MMTP_HEADER_SIZE\n if (fecType < 2 && (mmtpPacket[0]! & 0x20) !== 0) headerLength += 4\n if (fecType < 2 && (mmtpPacket[0]! & 0x02) !== 0) {\n if (headerLength + 4 > mmtpPacket.length) return null\n const extensionLength = new DataView(\n mmtpPacket.buffer,\n mmtpPacket.byteOffset + headerLength + 2,\n 2,\n ).getUint16(0, false)\n headerLength += 4 + extensionLength\n }\n if (headerLength + MPU_HEADER_SIZE > mmtpPacket.length) return null\n\n const payload = mmtpPacket.subarray(headerLength)\n\n // Inline a minimal MPU-header read \u2014 only the fields extractAltaTrailer needs.\n // payloadLength: 16 BE at bytes 0-1.\n // Byte 2 packs FT(4) | T(1) | FI(2) | A(1).\n const payloadLength = (payload[0]! << 8) | payload[1]!\n const flagsByte = payload[2]!\n const fragmentType = (flagsByte >> 4) & 0x0f\n const timed = ((flagsByte >> 3) & 0x01) === 1\n const fragmentationIndicator = (flagsByte >> 1) & 0x03\n\n // Only MFU packets (FT=2) carry the detached trailer.\n if (fragmentType !== 2) return null\n\n // FI=0/1 structurally carries a DU header; FI=2/3 does not. Require at\n // least one media byte after that deterministic layout so the bytes at the\n // declared boundary cannot impersonate ALTA for a header-only fragment.\n const dataUnitHeaderSize = fragmentationIndicator <= 1\n ? (timed ? TIMED_MFU_DU_HEADER_SIZE : NONTIMED_MFU_DU_HEADER_SIZE)\n : 0\n if (payloadLength <= MPU_HEADER_REMAINDER_SIZE + dataUnitHeaderSize) return null\n\n if (fecType > 1) return null\n const hasFecId = !options?.skipFecId && fecType === 1\n const payloadEnd = hasFecId ? payload.length - 4 : payload.length\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength\n if (declaredEnd > payloadEnd) return null\n\n if (declaredEnd + MMTP_EXT_TRAILER_HEADER_SIZE > payloadEnd) return null\n const extType = (payload[declaredEnd]! << 8) | payload[declaredEnd + 1]!\n if (extType !== MMTP_EXT_TYPE_ALTA_AUTH) return null\n const extLen = (payload[declaredEnd + 2]! << 8) | payload[declaredEnd + 3]!\n\n const authStart = declaredEnd + MMTP_EXT_TRAILER_HEADER_SIZE\n if (authStart + extLen !== payloadEnd) return null\n\n return {\n auth: payload.subarray(authStart, payloadEnd),\n signedEnd: headerLength + declaredEnd,\n }\n}\n", "import {\n MMTP_EXT_TYPE_ALTA_AUTH as _LEAF_MMTP_EXT_TYPE_ALTA_AUTH,\n MMTP_EXT_TRAILER_HEADER_SIZE as _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE,\n extractAltaTrailer as _leafExtractAltaTrailer,\n type AltaTrailer as _LeafAltaTrailer,\n} from '@blockcast/mmt-alta-parse'\n\n/**\n * @mmt/container - MMTP Parser\n *\n * MMTP (MMT Protocol) packet parser based on ISO/IEC 23008-1:2023 Section 9.2\n *\n * MMTP header format (12 bytes):\n * - Byte 0: V(2) | C(1) | FEC_type(2) | r(1) | X(1) | R(1)\n * - Byte 1: reserved(2) | packet_type(6)\n * - Bytes 2-3: packet_id (big-endian)\n * - Bytes 4-7: timestamp (big-endian, 32-bit)\n * - Bytes 8-11: sequence_number (big-endian)\n *\n * FEC_type values:\n * - 0: No FEC / source packet\n * - 1: FEC mode 0 (source with FEC payload ID)\n * - 2: FEC repair packet mode 0\n * - 3: FEC repair packet mode 1\n *\n * @packageDocumentation\n */\n\nexport interface MMTPHeader {\n version: number\n /** Packet type: 0x00=MPU, 0x02=Signaling, 0x03=Repair */\n packetType: number\n packetId: number\n timestamp: bigint\n sequenceNumber: number\n payloadLength: number\n fecType: number\n /** C flag: header carries a 32-bit packet_counter. */\n packetCounterFlag: boolean\n packetCounter?: number\n /** X flag: header carries an extension header and value. */\n extensionFlag: boolean\n extension?: { type: number; data: Uint8Array }\n /** Byte offset at which the MMTP payload begins. */\n headerLength: number\n /** Random Access Point flag - indicates keyframe (ISO 23008-1, byte 0 bit 0) */\n rapFlag: boolean\n}\n\nexport interface MPUHeader {\n mpuSequenceNumber: number\n fragmentationIndicator: number // FI: 0=complete, 1=first, 2=middle, 3=last\n /** 8-bit MPU fragment_counter from byte 3. */\n fragmentCounter: number\n /** T flag: 1=timed media (14-byte MFU DU header), 0=non-timed (4-byte header) */\n timedFlag: number\n /** A flag: aggregated MPU payload. This strict profile requires A=0. */\n aggregationFlag: boolean\n /** MPU payload_length (bytes 0-1): bytes after the length field, including\n * the remaining six MPU-header bytes and all data-unit bytes. */\n payloadLength: number\n}\n\nexport interface MFUDuHeader {\n movieFragmentSequenceNumber: number\n sampleNumber: number\n offset: number\n priority: number\n dependencyCounter: number\n}\n\nexport interface MMTPPacket {\n header: MMTPHeader\n payload: Uint8Array\n}\n\nexport const MMTP_HEADER_SIZE = 12\nexport const MPU_HEADER_SIZE = 8\nexport const MFU_DU_HEADER_SIZE_TIMED = 14 // T=1: timed media (video/audio samples)\nexport const MFU_DU_HEADER_SIZE_NONTIMED = 4 // T=0: non-timed media (item_id only)\n\nconst MPU_LENGTH_FIELD_SIZE = 2\nconst MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE\nconst SOURCE_FEC_PAYLOAD_ID_SIZE = 4\n\n// MMTP Packet Types (ISO/IEC 23008-1:2023 Section 9.2.2)\nexport const PACKET_TYPE_MPU = 0x00\nexport const PACKET_TYPE_SIGNALING = 0x02\nexport const PACKET_TYPE_REPAIR = 0x03\n\n// AL-FEC Signaling Message ID (ISO/IEC 23008-1:2023 Amendment 1:2025)\nexport const ALFEC_MESSAGE_ID = 0x0203\n\n/**\n * MMTP `packet_id` of the signaling flow, which carries PACKET_TYPE_SIGNALING\n * packets such as the AL-FEC message (ALFEC_MESSAGE_ID). The FFmpeg `moq_mmt`\n * muxer sends that message on this packet_id into the same multicast group as\n * the media, and moqcast-draft (PR #44) makes advertising it for a track a\n * MUST NOT. A track's packet_id is therefore 1..MMTP_PACKET_ID_MAX.\n */\nexport const MMTP_SIGNALING_PACKET_ID = 0\n\n/** Largest value of the 16-bit MMTP `packet_id` header field. */\nexport const MMTP_PACKET_ID_MAX = 0xffff\n\n// FEC Code IDs\nexport const FEC_CODE_XOR = 1\nexport const FEC_CODE_RAPTORQ = 2\n\n/**\n * Parse MMTP header from buffer\n */\nexport function parseMMTPHeader(buffer: Uint8Array): MMTPHeader {\n if (buffer.length < MMTP_HEADER_SIZE) {\n throw new Error(`MMTP packet too small: ${buffer.length} bytes`)\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)\n\n const version = (buffer[0]! >> 6) & 0x03\n if (version !== 0) {\n throw new Error(`Unsupported MMTP version: ${version}`)\n }\n\n // Extract FEC type from byte 0, bits 3-4\n const fecType = (buffer[0]! >> 3) & 0x03\n const packetCounterFlag = (buffer[0]! & 0x20) !== 0\n const extensionFlag = (buffer[0]! & 0x02) !== 0\n\n // Extract RAP flag from byte 0, bit 0 (R bit - Random Access Point)\n const rapFlag = (buffer[0]! & 0x01) === 1\n\n // ISO/IEC 23008-1:2023 Figure 8 (V=0) places reserved(2) before\n // packet_type(6), so packet_type occupies the low six bits of byte 1.\n const packetType = buffer[1]! & 0x3F\n\n let offset = 2\n\n // Packet ID (2 bytes, big-endian)\n const packetId = view.getUint16(offset, false)\n offset += 2\n\n // Timestamp (4 bytes, big-endian)\n const timestamp = BigInt(view.getUint32(offset, false))\n offset += 4\n\n // Sequence number (4 bytes, big-endian)\n const sequenceNumber = view.getUint32(offset, false)\n offset += 4\n\n let packetCounter: number | undefined\n if (packetCounterFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in packet_counter')\n packetCounter = view.getUint32(offset, false)\n offset += 4\n }\n\n let extension: { type: number; data: Uint8Array } | undefined\n if (extensionFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in header extension')\n const type = view.getUint16(offset, false)\n const length = view.getUint16(offset + 2, false)\n offset += 4\n if (offset + length > buffer.length) throw new Error('MMTP packet truncated in header extension value')\n extension = { type, data: buffer.subarray(offset, offset + length) }\n offset += length\n }\n\n return {\n version,\n packetType,\n packetId,\n timestamp,\n sequenceNumber,\n payloadLength: buffer.length - offset,\n fecType,\n packetCounterFlag,\n packetCounter,\n extensionFlag,\n extension,\n headerLength: offset,\n rapFlag,\n }\n}\n\n/**\n * The MMTP `packet_id` of `buffer`, or null exactly when parseMMTPHeader would\n * throw for it (short buffer, V != 0, truncated packet_counter or header\n * extension).\n *\n * This is the per-packet filter a receiver runs before any other work, so it\n * builds no header object, DataView or BigInt timestamp (BLO-36599). Use\n * parseMMTPHeader when any other header field is needed.\n */\nexport function readMMTPPacketId(buffer: Uint8Array): number | null {\n if (buffer.length < MMTP_HEADER_SIZE) return null\n const flags = buffer[0]!\n if (((flags >> 6) & 0x03) !== 0) return null\n let offset = MMTP_HEADER_SIZE\n if ((flags & 0x20) !== 0) {\n if (offset + 4 > buffer.length) return null\n offset += 4\n }\n if ((flags & 0x02) !== 0) {\n if (offset + 4 > buffer.length) return null\n const length = (buffer[offset + 2]! << 8) | buffer[offset + 3]!\n if (offset + 4 + length > buffer.length) return null\n }\n return (buffer[2]! << 8) | buffer[3]!\n}\n\n/**\n * Unwrap MMTP packet to extract payload\n */\nexport function unwrapMMTP(buffer: Uint8Array): MMTPPacket {\n const header = parseMMTPHeader(buffer)\n const payload = buffer.subarray(header.headerLength)\n\n return { header, payload }\n}\n\n/**\n * Parse MPU header from payload\n * ISO/IEC 23008-1:2023 Section 9.2.3.3\n *\n * MPU Header format (8 bytes) as written by FFmpeg/libmmt:\n * - Bytes 0-1: payload_length (16 bits, big-endian)\n * - Byte 2: FT(4) | T(1) | FI(2) | A(1)\n * - Byte 3: fragment_counter (8 bits)\n * - Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n */\nexport function parseMPUHeader(payload: Uint8Array): MPUHeader {\n if (payload.length < MPU_HEADER_SIZE) {\n throw new Error(`Payload too small for MPU header: ${payload.length} bytes`)\n }\n\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)\n\n // Bytes 0-1: payload_length (16 bits, big-endian)\n const payloadLength = view.getUint16(0, false)\n\n // Byte 2: FT(4) | T(1) | FI(2) | A(1)\n const flagsByte = payload[2]!\n const timedFlag = (flagsByte >> 3) & 0x01\n const fragmentationIndicator = (flagsByte >> 1) & 0x03\n const aggregationFlag = (flagsByte & 0x01) !== 0\n\n // Byte 3: fragment_counter (8 bits). The high bit is part of the counter in\n // the current libmmt/FFmpeg writer ABI.\n const fragmentCounter = payload[3]!\n\n // Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n const mpuSequenceNumber = view.getUint32(4, false)\n\n return {\n mpuSequenceNumber,\n fragmentationIndicator,\n fragmentCounter,\n timedFlag,\n aggregationFlag,\n payloadLength,\n }\n}\n\nexport function parseTimedMfuDuHeader(payload: Uint8Array, offset: number): MFUDuHeader | null {\n if (offset < 0 || offset + MFU_DU_HEADER_SIZE_TIMED > payload.length) return null\n const view = new DataView(payload.buffer, payload.byteOffset + offset, MFU_DU_HEADER_SIZE_TIMED)\n return {\n movieFragmentSequenceNumber: view.getUint32(0, false),\n sampleNumber: view.getUint32(4, false),\n offset: view.getUint32(8, false),\n priority: payload[offset + 12]!,\n dependencyCounter: payload[offset + 13]!,\n }\n}\n\n/**\n * Why `extractMFUPayload` refused a packet.\n *\n * These identifiers are part of the public contract: callers surface them in\n * diagnostics, so renaming one is a breaking change.\n */\nexport type MFURejectionGate =\n | 'packet-shorter-than-mmtp-plus-mpu-header'\n | 'mmtp-header-unparseable'\n | 'not-an-mpu-packet'\n | 'payload-shorter-than-mpu-header'\n | 'aggregation-flag-set'\n | 'mpu-payload-length-below-header-remainder'\n | 'mpu-declared-end-exceeds-payload'\n | 'mfu-offset-at-or-past-declared-end'\n | 'source-fec-payload-id-width-mismatch'\n | 'declared-trailers-unresolved'\n\nexport interface MFURejection {\n gate: MFURejectionGate\n /** Human-readable detail, including the byte arithmetic that decided it. */\n detail: string\n}\n\ninterface MfuPayloadLayout {\n hasMfuDuHeader: boolean\n mfuOffset: number\n dataEnd: number\n}\n\nfunction resolveMfuPayloadLayout(\n mpu: MPUHeader,\n fragmentType: number,\n payload: Uint8Array,\n): { layout: MfuPayloadLayout } | { rejection: MFURejection } {\n if (mpu.payloadLength < MPU_HEADER_REMAINDER_SIZE) {\n return {\n rejection: {\n gate: 'mpu-payload-length-below-header-remainder',\n detail: `payload_length=${mpu.payloadLength} < ${MPU_HEADER_REMAINDER_SIZE}`,\n },\n }\n }\n\n // ISO/IEC 23008-1 defines payload_length from immediately after its own\n // two-byte field. It therefore includes the remaining six MPU-header bytes.\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + mpu.payloadLength\n if (declaredEnd > payload.length) {\n return {\n rejection: {\n gate: 'mpu-declared-end-exceeds-payload',\n detail:\n `payload_length=${mpu.payloadLength} implies end=${declaredEnd}, ` +\n `payload is ${payload.length} bytes`,\n },\n }\n }\n\n const hasHeader = fragmentType === 2 && mpu.fragmentationIndicator <= 1\n const mfuHeaderSize = mpu.timedFlag ? MFU_DU_HEADER_SIZE_TIMED : MFU_DU_HEADER_SIZE_NONTIMED\n const mfuOffset = MPU_HEADER_SIZE + (hasHeader ? mfuHeaderSize : 0)\n if (mfuOffset >= declaredEnd) {\n return {\n rejection: {\n gate: 'mfu-offset-at-or-past-declared-end',\n detail: `mfuOffset=${mfuOffset} >= declaredEnd=${declaredEnd}`,\n },\n }\n }\n\n return {\n layout: {\n hasMfuDuHeader: hasHeader,\n mfuOffset,\n dataEnd: declaredEnd,\n },\n }\n}\n\nfunction isEmptyOrExactAltaTrailer(payload: Uint8Array, start: number, end: number): boolean {\n if (start === end) return true\n if (start < 0 || end > payload.length || start + _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE > end) {\n return false\n }\n\n const view = new DataView(\n payload.buffer,\n payload.byteOffset + start,\n end - start,\n )\n if (view.getUint16(0, false) !== _LEAF_MMTP_EXT_TYPE_ALTA_AUTH) return false\n\n const authLength = view.getUint16(2, false)\n return start + _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE + authLength === end\n}\n\ninterface DeclaredTrailerLayout {\n hasFecId: boolean\n}\n\nfunction resolveDeclaredTrailers(\n payload: Uint8Array,\n declaredEnd: number,\n fecType: number,\n options?: { skipFecId?: boolean },\n): DeclaredTrailerLayout | null {\n if (declaredEnd > payload.length) return null\n\n if (options?.skipFecId) {\n return isEmptyOrExactAltaTrailer(payload, declaredEnd, payload.length)\n ? { hasFecId: false }\n : null\n }\n\n if (fecType === 1) {\n const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n if (fecStart < declaredEnd || !isEmptyOrExactAltaTrailer(payload, declaredEnd, fecStart)) {\n return null\n }\n return { hasFecId: true }\n }\n if (fecType !== 0) return null\n\n if (isEmptyOrExactAltaTrailer(payload, declaredEnd, payload.length)) {\n return { hasFecId: false }\n }\n\n return null\n}\n\n/**\n * Extract NAL unit payload from MMTP packet\n */\nexport function extractNALUnit(mmtpPacket: Uint8Array): Uint8Array {\n const extraction = extractMFUPayload(mmtpPacket)\n if (!extraction) throw new Error('Malformed MMTP MPU payload')\n return extraction.data\n}\n\n/** Result of extracting MFU payload from an MMTP packet */\nexport interface MFUExtraction {\n /** MMTP header fields */\n header: MMTPHeader\n /** MPU header fields */\n mpu: MPUHeader\n /** Fragment type (FT field, 4 bits): 0=metadata, 2=MFU data */\n fragmentType: number\n /** MFU payload data (video/audio bytes, headers stripped, FEC ID excluded) */\n data: Uint8Array\n /** Parsed timed MFU DU header when present, otherwise null */\n mfuDuHeader: MFUDuHeader | null\n /** Non-timed MFU Item_ID when the 4-byte DU header is present. */\n itemId: number | null\n /** Source FEC Payload ID if present (`FEC_type == 1`), or null */\n fecPayloadId: { ssId: number } | null\n}\n\n/**\n * Extract MFU payload from a raw MMTP packet.\n *\n * Handles all the byte arithmetic for:\n * - MMTP header (12 bytes)\n * - MPU sub-header (8 bytes, payloadLength field)\n * - MFU DU header (14 bytes timed / 4 bytes non-timed, only for FI \u2264 1)\n * - Source FEC Payload ID (4 bytes at end, only when `FEC_type == 1`)\n *\n * `payloadLength` is the ISO length-after-field value: six remaining MPU-header\n * bytes, then the MFU DU header when present, then media. ALTA and Source FEC\n * trailers are outside that declared span and must consume the exact suffix.\n *\n * @param mmtpPacket Raw MMTP packet (from multicast or MoQ)\n * @param options.skipFecId When true, do NOT subtract the 4-byte Source FEC Payload ID\n * from the payload boundary, even if fecType >= 1 in the MMTP header. Use this for\n * FEC-recovered symbols where the FEC PID was already stripped before WASM decoding\n * but the fecType bits in byte 0 still reflect the original packet (FEC Type=1 per\n * draft-ramadan-moq-mmt-00 \u00A73.1).\n * @returns Extracted MFU data + metadata, or null if packet is too small\n */\ninterface MfuPlan {\n header: MMTPHeader\n payload: Uint8Array\n mpu: MPUHeader\n fragmentType: number\n layout: MfuPayloadLayout\n trailerLayout: DeclaredTrailerLayout\n}\n\n/**\n * Single source of truth for whether a packet yields an MFU payload.\n *\n * `extractMFUPayload` and `explainMFURejection` both delegate here, so the\n * reason reported to operators can never drift from the decision the parser\n * actually made.\n */\nfunction planMfuExtraction(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\n parsedHeader?: MMTPHeader,\n): { plan: MfuPlan } | { rejection: MFURejection } {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'packet-shorter-than-mmtp-plus-mpu-header',\n detail: `${mmtpPacket.length} bytes < ${MMTP_HEADER_SIZE + MPU_HEADER_SIZE}`,\n },\n }\n }\n\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch (error) {\n return {\n rejection: {\n gate: 'mmtp-header-unparseable',\n detail: error instanceof Error ? error.message : String(error),\n },\n }\n }\n }\n if (header.packetType !== PACKET_TYPE_MPU) {\n return {\n rejection: {\n gate: 'not-an-mpu-packet',\n detail: `packetType=0x${header.packetType.toString(16).padStart(2, '0')}`,\n },\n }\n }\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'payload-shorter-than-mpu-header',\n detail: `${payload.length} bytes < ${MPU_HEADER_SIZE}`,\n },\n }\n }\n\n const mpu = parseMPUHeader(payload)\n if (mpu.aggregationFlag) {\n return {\n rejection: { gate: 'aggregation-flag-set', detail: 'strict profile requires A=0' },\n }\n }\n\n // Fragment type from byte 2 upper nibble (not returned by parseMPUHeader)\n const fragmentType = (payload[2]! >> 4) & 0x0f\n\n const layoutResult = resolveMfuPayloadLayout(mpu, fragmentType, payload)\n if ('rejection' in layoutResult) return layoutResult\n const { layout } = layoutResult\n\n const trailerLayout = resolveDeclaredTrailers(\n payload,\n layout.dataEnd,\n header.fecType,\n options,\n )\n if (!trailerLayout) {\n // The most common cause is a producer whose Source FEC Payload ID width\n // disagrees with ISO/IEC 23008-1 \u00A7C.5.2. Name that explicitly rather than\n // leaving operators to infer it from a generic trailer error.\n if (!options?.skipFecId && header.fecType === 1) {\n const observed = payload.length - layout.dataEnd\n if (observed !== SOURCE_FEC_PAYLOAD_ID_SIZE) {\n return {\n rejection: {\n gate: 'source-fec-payload-id-width-mismatch',\n detail:\n `producer wrote a ${observed}-byte Source FEC Payload ID, ` +\n `ISO/IEC 23008-1 \u00A7C.5.2 requires ${SOURCE_FEC_PAYLOAD_ID_SIZE} ` +\n `(payload=${payload.length}, declaredEnd=${layout.dataEnd})`,\n },\n }\n }\n }\n return {\n rejection: {\n gate: 'declared-trailers-unresolved',\n detail:\n `trailer bytes [${layout.dataEnd}, ${payload.length}) are neither empty ` +\n `nor an exact ALTA trailer (fecType=${header.fecType})`,\n },\n }\n }\n\n return { plan: { header, payload, mpu, fragmentType, layout, trailerLayout } }\n}\n\n/**\n * Report why `extractMFUPayload` would refuse this packet, or null if it would\n * accept it.\n *\n * `extractMFUPayload` returns bare `null` on ten distinct conditions, which\n * makes a producer/consumer framing disagreement indistinguishable from a\n * publisher that never started \u2014 both render as silence. Use this wherever\n * that distinction matters: diagnostics, contract tests, and operator-facing\n * logs.\n */\nexport function explainMFURejection(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\n): MFURejection | null {\n const result = planMfuExtraction(mmtpPacket, options)\n return 'rejection' in result ? result.rejection : null\n}\n\nexport function extractMFUPayload(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options))\n}\n\n/**\n * extractMFUPayload for a caller that already holds this packet's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet; the\n * result is then identical to `extractMFUPayload(mmtpPacket, options)`.\n */\nexport function extractMFUPayloadWithHeader(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n options?: { skipFecId?: boolean },\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options, header))\n}\n\nfunction extractPlannedMfu(\n mmtpPacket: Uint8Array,\n result: { plan: MfuPlan } | { rejection: MFURejection },\n): MFUExtraction | null {\n if ('rejection' in result) return null\n const { header, payload, mpu, fragmentType, layout, trailerLayout } = result.plan\n\n const mfuDuHeaderPresent = layout.hasMfuDuHeader\n const mfuOffset = layout.mfuOffset\n const dataEnd = layout.dataEnd\n\n const mfuDuHeader =\n mfuDuHeaderPresent && mpu.timedFlag === 1\n ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE)\n : null\n const itemId =\n mfuDuHeaderPresent && mpu.timedFlag === 0\n ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false)\n : null\n\n const data = payload.subarray(mfuOffset, dataEnd)\n\n const fecPayloadId = trailerLayout.hasFecId\n ? {\n ssId: new DataView(\n mmtpPacket.buffer,\n mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE,\n SOURCE_FEC_PAYLOAD_ID_SIZE,\n ).getUint32(0, false),\n }\n : null\n\n return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId }\n}\n\n/**\n * Extract timestamp from MMTP packet\n */\nexport function extractTimestamp(mmtpPacket: Uint8Array): bigint {\n const { header } = unwrapMMTP(mmtpPacket)\n return header.timestamp\n}\n\n/**\n * Check if MMTP packet is an FEC repair packet\n * FEC_type 2 and 3 are repair packets\n */\nexport function isFecRepairPacket(mmtpPacket: Uint8Array): boolean {\n if (mmtpPacket.length < 1) return false\n const fecType = (mmtpPacket[0]! >> 3) & 0x03\n return fecType >= 2\n}\n\n/**\n * Extract Source FEC Payload ID (SS_ID) from the last 4 bytes of an MMTP\n * source packet per ISO/IEC 23008-1:2023 Section C.5.2 Figure C.10.\n *\n * SS_ID is a flat 32-bit monotonic counter (incremented by 1 per packet\n * for ssbg_mode0 and ssbg_mode1). Block membership and ESI use the\n * configured K; repair SS_Start identifies the first SS_ID of the block and\n * SSB_length records the authored source-symbol count:\n * SBN = floor(SS_ID / K), ESI = SS_ID % K\n * SS_Start = SBN * K (first SS_ID of the block)\n *\n * This matches FFmpeg moqenc_mmt.c, the drafts (draft-ramadan-moq-fec \u00A76.2),\n * and ATSC A/331 \u00A78.1.2.5 which defers to ISO 23008-1 Annex C.\n */\nexport function extractFecPayloadId(mmtpPacket: Uint8Array): { ssId: number } | null {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header ? sourceFecPayloadIdOf(mmtpPacket, header) : null\n}\n\n/**\n * Return true when FEC_type=1 and the exact suffix after the ISO-declared MPU\n * payload contains a four-byte Source FEC Payload ID.\n */\nexport function hasSourceFecPayloadId(mmtpPacket: Uint8Array): boolean {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header !== null && sourceFecPayloadIdOf(mmtpPacket, header) !== null\n}\n\nfunction parseHeaderForSourceFecId(mmtpPacket: Uint8Array): MMTPHeader | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n try {\n return parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n}\n\n/**\n * The Source FEC Payload ID of a packet whose MMTP header the caller already\n * parsed, or null when the packet carries none: present only when FEC_type=1\n * and the exact suffix after the ISO-declared MPU payload holds the four-byte\n * ID (ISO/IEC 23008-1 \u00A7C.5.2). Same answer as extractFecPayloadId, without a\n * second header parse (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet.\n */\nexport function sourceFecPayloadIdOf(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n): { ssId: number } | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n if (header.packetType !== PACKET_TYPE_MPU) return null\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) return null\n const payloadLength = (payload[0]! << 8) | payload[1]!\n if (payloadLength < MPU_HEADER_REMAINDER_SIZE) return null\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength\n if (!resolveDeclaredTrailers(payload, declaredEnd, header.fecType)?.hasFecId) return null\n\n // Flat 4-byte SS_ID: encoder writes block_id * K + symbol_id\n const off = mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n return {\n ssId:\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0,\n }\n}\n\n/**\n * MMTP header-extension ext_type assigned to the detached ALTA authenticator\n * trailer (Path 2 / 08-07). Carried in an `[ext_type][ext_length][auth]`\n * block appended after the MPU's declared payload bytes, before the 4-byte\n * Source FEC Payload ID. MMTP X-bit stays 0 \u2014 non-ALTA MMTP receivers treat\n * the trailer as bytes past `payload_length` and skip it (per ISO 23008-1\n * \u00A79.2.2 \"may be discarded without impacting correct processing\").\n */\n// Re-export the ALTA trailer helper from the zero-dep leaf package. The leaf\n// owns the implementation so container and FEC consumers share one parser\n// without creating a package cycle.\nexport const MMTP_EXT_TYPE_ALTA_AUTH = _LEAF_MMTP_EXT_TYPE_ALTA_AUTH\nexport const MMTP_EXT_TRAILER_HEADER_SIZE = _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE\nexport type AltaTrailer = _LeafAltaTrailer\nexport const extractAltaTrailer = _leafExtractAltaTrailer\n\n/** Size of Repair FEC Payload ID per ISO/IEC 23008-1 Section C.5.3 */\nexport const REPAIR_FEC_PID_SIZE = 13\n\n/**\n * Parse the 13-byte Repair FEC Payload ID after the complete dynamic MMTP header.\n * `parsedHeader`, when given, must be `parseMMTPHeader(mmtpPacket)` of this\n * same packet; it spares the second header parse (BLO-36599).\n */\nfunction readRepairFecPayloadIdFields(mmtpPacket: Uint8Array, parsedHeader?: MMTPHeader): {\n packetId: number; ssStart: number; rsbLength: number; rsId: number; ssbLength: number; dataOffset: number\n} | null {\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n }\n if (header.packetType !== PACKET_TYPE_REPAIR || header.fecType !== 2) return null\n\n const off = header.headerLength\n if (mmtpPacket.length < off + REPAIR_FEC_PID_SIZE) return null\n const ssStart =\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0 // 32 bits\n const rsbLength = (mmtpPacket[off + 4]! << 16) | (mmtpPacket[off + 5]! << 8) | mmtpPacket[off + 6]! // 24 bits\n const rsId = (mmtpPacket[off + 7]! << 16) | (mmtpPacket[off + 8]! << 8) | mmtpPacket[off + 9]! // 24 bits\n const ssbLength = (mmtpPacket[off + 10]! << 16) | (mmtpPacket[off + 11]! << 8) | mmtpPacket[off + 12]! // 24 bits\n return {\n packetId: header.packetId,\n ssStart,\n rsbLength,\n rsId,\n ssbLength,\n dataOffset: off + REPAIR_FEC_PID_SIZE,\n }\n}\n\n/**\n * Extract Repair FEC Payload ID from a raw-multicast MMTP repair packet.\n * Per ISO/IEC 23008-1:2023 Section C.5.3 (ssbg_mode0, one-stage, no FFSRP_TS):\n * [MMTP header][SS_Start:4][RSB_length:3][RS_ID:3][SSB_length:3][repair data]\n * OTI is delivered via AL-FEC signaling message (Table C.3), not per-packet.\n *\n * Byte 1 is reserved(2)|packet_type(6) for V=0, so repair type 0x03 is 0x03.\n * Pre-migration high-bits bytes (0x0c) are rejected \u2014 no compatibility decode.\n */\nexport function extractRepairFecPayloadId(mmtpPacket: Uint8Array): {\n ssStart: number; rsbLength: number; rsId: number; ssbLength: number\n} | null {\n const fields = readRepairFecPayloadIdFields(mmtpPacket)\n if (!fields) return null\n return {\n ssStart: fields.ssStart,\n rsbLength: fields.rsbLength,\n rsId: fields.rsId,\n ssbLength: fields.ssbLength,\n }\n}\n\n/** Parsed MoQ repair frame per ISO/IEC 23008-1 Section C.4.3 + C.5.3 */\nexport interface MoqRepairFrame {\n /** MMTP packetId (1=video, 2=audio) \u2014 for routing to correct FEC decoder */\n packetId: number\n /** SS_Start (32 bits): SS_ID of first source symbol in block */\n ssStart: number\n /** RSB_length (24 bits): number of repair symbols (P) */\n rsbLength: number\n /** RS_ID (24 bits): repair symbol index (0-based) */\n rsId: number\n /** SSB_length (24 bits): authored source-symbol count (1..=configured K) */\n ssbLength: number\n /** Repair symbol data after the dynamic MMTP header and 13-byte FEC payload ID. */\n symbolData: Uint8Array\n}\n\n/**\n * Parse a MoQ repair frame (ISO/IEC 23008-1 Annex C format).\n *\n * Wire format per ISO 23008-1 \u00A7C.4.3 + \u00A7C.5.3:\n * MMTP Header: fixed 12 bytes plus C/X optional fields\n * then SS_Start (4 bytes) \u2192 block identifier\n * then RSB_length (3 bytes) \u2192 P\n * then RS_ID (3 bytes) \u2192 ESI = K + RS_ID\n * then SSB_length (3 bytes) \u2192 authored source-symbol count (1..=K)\n * then Repair Symbol Data (T bytes)\n *\n * @param frame - Raw MoQ repair frame bytes\n * @returns Parsed frame or null if invalid\n */\nexport function parseMoqRepairFrame(frame: Uint8Array): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame))\n}\n\n/**\n * parseMoqRepairFrame for a caller that already holds this frame's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599). The router's\n * multicast repair branch uses it: one repair packet in every K + P otherwise\n * paid two header parses.\n *\n * `header` must be `parseMMTPHeader(frame)` of this same frame; the result is\n * then identical to `parseMoqRepairFrame(frame)`.\n */\nexport function parseMoqRepairFrameWithHeader(\n frame: Uint8Array,\n header: MMTPHeader,\n): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame, header))\n}\n\nfunction moqRepairFrameFrom(\n frame: Uint8Array,\n repairId: ReturnType<typeof readRepairFecPayloadIdFields>,\n): MoqRepairFrame | null {\n if (!repairId) return null\n\n const symbolData = frame.subarray(repairId.dataOffset)\n if (\n repairId.rsbLength === 0 ||\n repairId.ssbLength === 0 ||\n repairId.rsId >= repairId.rsbLength ||\n symbolData.length === 0\n ) return null\n\n return {\n packetId: repairId.packetId,\n ssStart: repairId.ssStart,\n rsbLength: repairId.rsbLength,\n rsId: repairId.rsId,\n ssbLength: repairId.ssbLength,\n symbolData,\n }\n}\n\n// The signalling-message parsers (9.3.4.2 payload header, AL-FEC message,\n// PA/MPT) live in ./signalling.ts.\n", "/**\n * NTP short format (RFC 5905 \u00A76): an unsigned 32-bit, 16.16 fixed-point count\n * of seconds. The MMTP header `timestamp` field (ISO/IEC 23008-1 \u00A79.2.2)\n * carries each packet's UTC sending instant in this format, so the value wraps\n * to 0 every 2^16 s (~18.2 h).\n *\n * Plain numeric order on these values breaks at every wrap: a retained\n * `Math.max` stays at the pre-wrap value and every later instant compares as\n * older. Order send times only with {@link compareNtpShort}, which applies\n * RFC 1982 serial-number arithmetic over the field's width.\n */\n\n/** Width in bits of the MMTP `timestamp` field (NTP short). */\nexport const NTP_SHORT_BITS = 32\n\n/** Number of distinct NTP-short values; the field wraps to 0 here. */\nexport const NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS\n\n/**\n * RFC 1982 \u00A73.2's `2^(SERIAL_BITS - 1)`. Two values whose forward distance is\n * below this are ordered; exactly this far apart they are not ordered at all.\n * Two instants must be less than this far apart (2^15 s, ~9.1 h of send time)\n * for the comparison to give their true order.\n */\nexport const NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1)\n\n/**\n * Serial order of one NTP-short value relative to another.\n *\n * `'unordered'` is RFC 1982's undefined case: the values are exactly\n * {@link NTP_SHORT_HALF_RANGE} apart, so neither precedes the other. It is\n * never folded into `'before'` or `'after'`; each caller decides what an\n * unorderable pair means for it.\n */\nexport type NtpShortOrder = 'before' | 'equal' | 'after' | 'unordered'\n\n/** True when `value` is an NTP-short value: an integer in [0, NTP_SHORT_MODULUS). */\nexport function isNtpShort(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value < NTP_SHORT_MODULUS\n}\n\n/**\n * RFC 1982 serial comparison of NTP-short `a` against `b`: `'after'` when `a`\n * is the later instant, `'before'` when it is the earlier one.\n *\n * The answer is the true order only while the two instants are less than\n * {@link NTP_SHORT_HALF_RANGE} apart. Past that a 32-bit field cannot tell\n * later from earlier, and the result names whichever direction is nearer.\n * This is not a total order (it is not transitive across the whole space), so\n * it must never be used to sort.\n *\n * @throws {RangeError} if either operand is not an NTP-short value\n */\nexport function compareNtpShort(a: number, b: number): NtpShortOrder {\n if (!isNtpShort(a) || !isNtpShort(b)) {\n throw new RangeError(\n `NTP-short values must be integers in [0, 2^${NTP_SHORT_BITS}), got ${String(a)} and ${String(b)}`,\n )\n }\n if (a === b) return 'equal'\n // Forward distance from b to a, reduced into [1, NTP_SHORT_MODULUS). Both\n // operands are below 2^32, so every step is exact in a double.\n const forward = (a - b + NTP_SHORT_MODULUS) % NTP_SHORT_MODULUS\n if (forward < NTP_SHORT_HALF_RANGE) return 'after'\n if (forward > NTP_SHORT_HALF_RANGE) return 'before'\n return 'unordered'\n}\n", "/**\n * FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C).\n *\n * Every catalog `fec` object that declares an FEC instance names exactly one\n * block profile in `fec.blockProfile`. A receiver applies all of a profile's\n * rules or none of them, so the token is the one explicit per-stream\n * discriminator: it is never defaulted and never inferred from the wire.\n *\n * - `k-padded-v1` (legacy, Blockcast-private): a source symbol is the\n * protected MMTP packet zero-padded to T with no length field; every block\n * is a K-symbol RaptorQ block whose ESIs `SSB_length..K-1` are implicit\n * zero padding; `SS_ID = SBN*K + ESI`, so `SS_Start = SBN*K`; repair\n * ESI = K + RS_ID; the MoQ repair group id is the SBN.\n * - `iso-ssbg1-v1`: ssbg_mode1 (C.3.3): a source symbol is\n * `u16 length || protected packet || zero padding` to T; a block is a\n * K' = SSB_length RaptorQ block (C.5.3) and repair ESI = SSB_length + RS_ID;\n * SS_ID increments by one per FEC source packet and wraps at 2^32 (C.5.2),\n * so a block is located by membership in `[SS_Start, SS_Start + SSB_length)`\n * under RFC 1982 serial arithmetic; the MoQ repair group id is the 62-bit\n * wrap-epoch-extended SS_Start. The AL-FEC coding byte for this profile is\n * 0x14 (one-stage, ssbg_mode1, fec_payload_id_mode 0, Amd 1:2025 Table C.3).\n */\nimport { serial32Add, serial32Distance, serial32Precedes } from './serial-number.js'\n\n/** The two block profiles a catalog `fec.blockProfile` may name. */\nexport type FecBlockProfile = 'k-padded-v1' | 'iso-ssbg1-v1'\n\n/** Every accepted `fec.blockProfile` token, legacy first. */\nexport const FEC_BLOCK_PROFILES: readonly FecBlockProfile[] = Object.freeze([\n 'k-padded-v1',\n 'iso-ssbg1-v1',\n])\n\n/** True for a known block-profile token. */\nexport function isFecBlockProfile(value: unknown): value is FecBlockProfile {\n return value === 'k-padded-v1' || value === 'iso-ssbg1-v1'\n}\n\n/**\n * Read a required `fec.blockProfile`. Throws on an absent or unknown token:\n * there is no default and nothing is inferred, because the two profiles carry\n * identical wire tuples whose repair math differs.\n *\n * @param where the catalog path of the field, for the error message.\n */\nexport function parseFecBlockProfile(value: unknown, where: string): FecBlockProfile {\n if (isFecBlockProfile(value)) return value\n if (value === undefined) {\n throw new Error(\n `${where} is required: name the FEC block profile (\"k-padded-v1\" or \"iso-ssbg1-v1\")`,\n )\n }\n throw new Error(\n `${where} must be \"k-padded-v1\" or \"iso-ssbg1-v1\" (got ${JSON.stringify(value)})`,\n )\n}\n\n/** ISO/IEC 23008-1 C.3.3 ssbg_mode1: the two-octet packet length field. */\nexport const SSBG1_LENGTH_FIELD_BYTES = 2\n\n/** The largest protected packet an ssbg_mode1 symbol of size T can carry. */\nexport function ssbg1MaxPacketLength(symbolSize: number): number {\n return symbolSize - SSBG1_LENGTH_FIELD_BYTES\n}\n\n/** Whether a protected packet of `packetLength` bytes fits one T-byte ssbg_mode1 symbol. */\nexport function ssbg1FitsSymbol(packetLength: number, symbolSize: number): boolean {\n return (\n Number.isSafeInteger(packetLength) &&\n packetLength >= 0 &&\n packetLength <= ssbg1MaxPacketLength(symbolSize) &&\n packetLength <= 0xffff\n )\n}\n\n/**\n * Build one ssbg_mode1 source symbol (C.3.3): the protected packet's length\n * in two octets, network byte order, then the packet, then zero octets up to\n * T. The protected packet is the MMTP packet before its Source FEC Payload ID\n * is appended (C.4.2).\n *\n * @throws when `length + 2 > T`: the packet cannot be protected under this\n * profile, and silently truncating it would corrupt every repair symbol.\n */\nexport function buildSsbg1SourceSymbol(packet: Uint8Array, symbolSize: number): Uint8Array {\n if (!Number.isSafeInteger(symbolSize) || symbolSize <= SSBG1_LENGTH_FIELD_BYTES) {\n throw new Error(`ssbg_mode1 symbol size must exceed ${SSBG1_LENGTH_FIELD_BYTES} (got ${symbolSize})`)\n }\n if (!ssbg1FitsSymbol(packet.byteLength, symbolSize)) {\n throw new Error(\n `ssbg_mode1 source symbol needs ${packet.byteLength} + ${SSBG1_LENGTH_FIELD_BYTES} bytes but T is ${symbolSize}`,\n )\n }\n const symbol = new Uint8Array(symbolSize)\n symbol[0] = (packet.byteLength >>> 8) & 0xff\n symbol[1] = packet.byteLength & 0xff\n symbol.set(packet, SSBG1_LENGTH_FIELD_BYTES)\n return symbol\n}\n\n/**\n * Recover the protected packet from one ssbg_mode1 source symbol: the bytes\n * the length field names. Returns null, never a guess, when the length field\n * points past the symbol or any padding octet is non-zero (C.3.3 padding is\n * all 00h), since either means the recovered symbol is not what was sent.\n */\nexport function trimSsbg1SourceSymbol(symbol: Uint8Array): Uint8Array | null {\n if (symbol.byteLength < SSBG1_LENGTH_FIELD_BYTES) return null\n const length = (symbol[0]! << 8) | symbol[1]!\n const end = SSBG1_LENGTH_FIELD_BYTES + length\n if (end > symbol.byteLength) return null\n for (let i = end; i < symbol.byteLength; i++) {\n if (symbol[i] !== 0) return null\n }\n return symbol.subarray(SSBG1_LENGTH_FIELD_BYTES, end)\n}\n\n/** The SS_ID after the last source symbol of `[ssStart, ssStart + ssbLength)`. */\nexport function isoBlockEnd(ssStart: number, ssbLength: number): number {\n return serial32Add(ssStart, ssbLength)\n}\n\n/**\n * Range membership (C.5.2, C.5.3): whether `ssId` is one of the\n * `ssbLength` consecutive SS_IDs starting at `ssStart`, modulo 2^32.\n */\nexport function isoBlockContains(ssStart: number, ssbLength: number, ssId: number): boolean {\n return serial32Distance(ssStart, ssId) < ssbLength\n}\n\n/**\n * A lower bound on how many blocks the block starting at `a` lies behind the\n * one starting at `b` (0 when `a` does not precede `b`). An iso-ssbg1-v1\n * block holds at most `kMax` source symbols (the catalog `sourceSymbols`, the\n * AL-FEC maximum_k_for_repair_flow), so `n * kMax` SS_IDs always span at least\n * `n` blocks. Every interleave horizon in the receiver (decoder retention,\n * worker retirement, ordered-relay settlement) uses this one bound, so the\n * manager's horizon can never fall inside the decoder's.\n */\nexport function isoBlocksBehindLowerBound(a: number, b: number, kMax: number): number {\n if (!Number.isSafeInteger(kMax) || kMax <= 0) {\n throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`)\n }\n return serial32Precedes(a, b) ? Math.floor(serial32Distance(a, b) / kMax) : 0\n}\n", "// Catalog \u2192 per-track FEC geometry resolution.\n//\n// Moved from hang-mmt-fec's js/watch/src/track-fec-params.ts so every player\n// (moq-lib/hang, moqtail, shaka MSF) resolves catalog FEC semantics through\n// one shared implementation \u2014 players keep only wiring, not MMT/catalog\n// interpretation. Structural track shape ({ name, type }) keeps this module\n// player-agnostic.\n\nimport {\n\tFEC_BLOCK_PROFILES,\n\tisFecBlockProfile,\n\tparseFecBlockProfile,\n\ttype FecBlockProfile,\n} from \"@blockcast/mmt-base\";\nimport { catalogFecInterleaveMs, type FecInterleaveCatalog } from \"@blockcast/mmt-fec\";\nimport { resolveGroupDurationMs } from \"./carp-catalog.js\";\nimport { MIN_INTERLEAVE_DEPTH_MS, catalogDeclaresFec, isoBlockSpanUsable } from \"./fec-state.js\";\nimport {\n\tassembleRepairLayers,\n\tassertRepairTrackReachable,\n\tdescribeRepairTrack,\n\trejectRepairFieldsOnMediaTrack,\n\trejectRepairTrackGeometry,\n\trepairReachability,\n\tdependsTarget,\n\treadFecBlockProfile,\n\tRepairContractError,\n\ttype RepairLayerParams,\n\ttype RepairLayersResolution,\n\ttype RepairOverlayParams,\n\ttype RepairSourceGeometry,\n\ttype RepairTrackDescriptor,\n\tthrowRepairContractError,\n\tvalidateRepairTrackFields,\n} from \"./repair-layers.js\";\nimport type { RepairTarget } from \"./repair-fetch.js\";\n\nexport { catalogFecInterleaveMs, type FecInterleaveCatalog };\nexport { FEC_BLOCK_PROFILES, isFecBlockProfile, parseFecBlockProfile, type FecBlockProfile };\nexport {\n\tassembleRepairLayers,\n\tassertRepairTrackReachable,\n\trepairReachability,\n\ttype AssembleRepairLayersOptions,\n\ttype RepairReachability,\n\tdependsTarget,\n\tdescribeRepairTrack,\n\treadFecBlockProfile,\n\trejectRepairFieldsOnMediaTrack,\n\trejectRepairTrackGeometry,\n\tREPAIR_TRACK_GEOMETRY_KEYS,\n\tREPAIR_TRACK_ONLY_KEYS,\n\tvalidateRepairTrackFields,\n\tDEFAULT_REPAIR_PRIORITY,\n\tMAX_ENCODING_SYMBOL_ID,\n\tMAX_RAPTORQ_SOURCE_SYMBOLS,\n\tMAX_REPAIR_SYMBOLS,\n\tREPAIR_PRIORITY_MAX,\n\tREPAIR_PRIORITY_MIN,\n\tRepairContractError,\n\ttype RepairContractCode,\n\ttype RepairContractFail,\n\ttype RepairLayerParams,\n\ttype RepairLayersResolution,\n\ttype RepairOverlayParams,\n\ttype RepairSourceGeometry,\n\ttype RepairTrackDescriptor,\n} from \"./repair-layers.js\";\n\n/**\n * Minimal structural shape of a resolved endpoint track. Callers may carry\n * extra fields (e.g. hang's ResolvedEndpointTrack adds `pid`) \u2014 the resolver\n * only reads `name` and `type`.\n */\nexport type CatalogTrackRef = {\n\tname: string;\n\ttype: \"video\" | \"audio\" | (string & {});\n};\n\ntype FecCatalog = FecInterleaveCatalog & {\n\talgorithm?: unknown;\n\t/** FEC block profile token (BLO-35597): required on a declared FEC\n\t * instance, read through `readFecBlockProfile`. */\n\tblockProfile?: unknown;\n\tsourceSymbols?: number;\n\trepairSymbols?: number;\n\tsymbolSize?: number;\n\t/** Signaled jitter/reorder allowance in ms (added to the block span). */\n\treorderToleranceMs?: number;\n\t/** Layer 0 repair track. Further layers and overlays describe themselves\n\t * (draft-ramadan-moq-fec \u00A76.3): MSF `tracks[]` entries with\n\t * `packaging: \"fec-repair\"`, or hang `repair.renditions` entries, that\n\t * `depend` on this rendition. */\n\trepairTrack?: string;\n};\n\n/** One self-describing repair rendition of the hang catalog's `repair` block\n * (draft-ramadan-moq-fec \u00A76.3). Mirrors the MSF fec-repair track fields. */\nexport type RepairRenditionCatalog = {\n\tdepends?: unknown;\n\trepairLayer?: unknown;\n\trepairSymbols?: unknown;\n\tpriority?: unknown;\n\tscope?: unknown;\n\tsourceSymbols?: unknown;\n};\n\n/** The container fields the FEC and group-duration readers consult. */\ntype ContainerCatalog = { kind?: unknown; timescale?: unknown };\n\ntype RenditionCatalog = KeyframeCadenceCatalog & {\n\tcontainer?: ContainerCatalog;\n\tfec?: FecCatalog;\n\tframerate?: unknown;\n\tsampleRate?: unknown;\n\tsamplerate?: unknown;\n};\n\ntype CatalogWithRenditions = {\n\tvideo?: { renditions?: Record<string, RenditionCatalog> };\n\taudio?: { renditions?: Record<string, RenditionCatalog> };\n\t/** hang-native repair renditions: every repair track beyond layer 0. */\n\trepair?: { renditions?: Record<string, RepairRenditionCatalog> };\n};\n\n/** An MSF/CMSF catalog: `tracks[]` entries, each self-describing. */\ntype CatalogWithTracks = {\n\ttracks?: unknown;\n};\n\n/** Either catalog dialect `resolveRepairLayers` reads. */\nexport type RepairLayerCatalog = CatalogWithRenditions | CatalogWithTracks;\n\nexport type GroupDurationCatalog = {\n\tgroupDurationTicks?: number;\n\tgroupDurationMs?: number;\n\ttimescale?: number;\n};\n\n/** Rendition fields the group-duration reader consumes. */\nexport type GroupDurationRendition = GroupDurationCatalog & {\n\tcontainer?: ContainerCatalog;\n};\n\n/**\n * Largest tick count per Hz of timescale whose millisecond duration\n * `resolveGroupDurationMs` still accepts: `(ticks / timescale) * 1000` must not\n * exceed `Number.MAX_SAFE_INTEGER`, so `ticks <= timescale * (MAX_SAFE / 1000)`\n * (Hz times seconds is ticks).\n */\nconst MAX_SAFE_TICKS_PER_HZ = Number.MAX_SAFE_INTEGER / 1000;\n\n/** A positive safe-integer Hz timescale, or undefined. */\nfunction timescaleHz(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isSafeInteger(value) && value > 0 ? value : undefined;\n}\n\n/**\n * A rendition's publisher group duration in ms, or undefined when the catalog\n * carries none.\n *\n * `groupDurationTicks` over a timescale wins when both are present, through\n * `resolveGroupDurationMs` (draft-ramadan-moq-mmt-00 \u00A74.4.4). The timescale is\n * the MMTP container's when the rendition declares one, else the rendition's\n * own `timescale`. Only the ticks are forwarded: a catalog `groupDurationMs`\n * is often a rounded fraction (21.33 for 1024/48000), which the ticks path\n * never reads but `resolveGroupDurationMs` would reject as a non-integral ms.\n * Ticks with no usable timescale, and ticks that `resolveGroupDurationMs`\n * would reject, fall back to `groupDurationMs` rather than guessing a clock.\n * That rejection is on the product, not the operand: the count must be a safe\n * integer and `(ticks / timescale) * 1000` must stay within\n * `Number.MAX_SAFE_INTEGER`, which a safe-integer count can still exceed when\n * `timescale < 1000`, so the guard below bounds the count by\n * `timescale * (Number.MAX_SAFE_INTEGER / 1000)` and the reader never throws.\n * The production catalog signals both fields on every rendition and no\n * rendition-level timescale, so the `groupDurationMs` fallback is the path it\n * takes.\n */\nexport function catalogGroupDurationMs(rendition: GroupDurationRendition | undefined): number | undefined {\n\tif (!rendition) return undefined;\n\tconst ticks = numericField(rendition.groupDurationTicks);\n\tconst ms = numericField(rendition.groupDurationMs);\n\tconst timescale =\n\t\ttimescaleHz(rendition.container?.kind === \"mmtp\" ? rendition.container.timescale : undefined) ??\n\t\ttimescaleHz(rendition.timescale);\n\tif (\n\t\tticks !== undefined &&\n\t\ttimescale !== undefined &&\n\t\tNumber.isSafeInteger(ticks) &&\n\t\tticks <= timescale * MAX_SAFE_TICKS_PER_HZ\n\t) {\n\t\treturn resolveGroupDurationMs({ groupDurationTicks: ticks }, timescale);\n\t}\n\treturn ms;\n}\n\n/**\n * The group duration of the catalog rendition an endpoint track was resolved\n * from, by type and name (the same lookup `resolveCatalogTrackFec` performs\n * for FEC geometry). Undefined when the rendition is missing or carries no\n * group duration, so a caller that needs one can fail closed by name.\n */\nexport function resolveCatalogTrackGroupDurationMs(\n\tcatalog: CatalogWithRenditions | undefined,\n\ttrack: CatalogTrackRef,\n): number | undefined {\n\tconst renditions = track.type === \"audio\" ? catalog?.audio?.renditions : catalog?.video?.renditions;\n\treturn catalogGroupDurationMs(renditions?.[track.name]);\n}\n\nexport type KeyframeCadenceCatalog = GroupDurationCatalog & {\n\tkeyframeDurationTicks?: number;\n\tkeyframeDurationMs?: number;\n\tkeyframeIntervalTicks?: number;\n\tkeyframeIntervalMs?: number;\n\tgopDurationTicks?: number;\n\tgopDurationMs?: number;\n};\n\n/**\n * Why a track's catalog `fec` block resolved to no FEC although it is not a\n * plain no-FEC declaration (BLO-35597): it declares an FEC instance but names\n * no `blockProfile`, or it names a token that is not a known profile\n * (`track_fec_block_profile`), or it names `iso-ssbg1-v1` without the block\n * span that bounds that profile's block location (`track_fec`, on\n * `interleaveDepthMs`). The profile selects the block math and is never\n * defaulted or inferred, and the span is never borrowed from another track, so\n * such a track carries no FEC geometry and no repair track, and it is the only\n * track affected: every other track of the catalog resolves as usual.\n */\nexport type TrackFecUnavailable = {\n\t/** The repair-contract code a catalog validator reports for the same field. */\n\treadonly code: \"track_fec_block_profile\" | \"track_fec\";\n\t/** JSON path of the offending field: `blockProfile`, or `interleaveDepthMs`. */\n\treadonly path: string;\n\t/** What is wrong, worded as the `RepairContractError` a validator throws. */\n\treadonly message: string;\n};\n\nexport type TrackFecParams = {\n\tfecK: number;\n\t/** The catalog `fec.blockProfile` (BLO-35597, ISO/IEC 23008-1 Annex C).\n\t * Present exactly when the track has FEC (`fecK > 0`) and absent on no\n\t * FEC: it selects the block math (`k-padded-v1`: SS_Start = SBN*K over\n\t * zero-padded K-symbol blocks; `iso-ssbg1-v1`: ssbg_mode1, K' =\n\t * SSB_length, consecutive SS_IDs), so it is never defaulted. A declared\n\t * instance that names no known profile has no FEC; `fecUnavailable` says\n\t * why. */\n\tfecBlockProfile?: FecBlockProfile;\n\t/** Present exactly when the track's `fec` block names no usable\n\t * `blockProfile`, or names `iso-ssbg1-v1` without a usable block span\n\t * (see `TrackFecUnavailable`). The track then resolves to\n\t * no FEC (`fecK` 0, no `fecBlockProfile`, no repair track) and this is\n\t * the reason a caller surfaces. Absent on every other track, with FEC or\n\t * without. */\n\tfecUnavailable?: TrackFecUnavailable;\n\t/** P_0: repair symbols per block on layer 0 (the track-level repairSymbols). */\n\tfecRepairCount: number;\n\t/** Total repair symbols per block across the BASE layers: the wire\n\t * RSB_length on every base layer packet. Equals fecRepairCount on a\n\t * single-layer track. A keyframe overlay is a separate FEC instance with\n\t * its own RSB_length and is not included. */\n\tfecTotalRepairCount: number;\n\tfecSymbolSize: number;\n\tfecInterleaveMs: number;\n\t/** Catalog fec.reorderToleranceMs \u2014 signaled jitter/reorder allowance, ADDED\n\t * to the block span to form a deadline. Deliberately `number | undefined`\n\t * and NOT floored to 0: a declared 0 (\"this path needs no slack\") and an\n\t * absent value (\"the encoder never said\") must stay distinguishable, or a\n\t * consumer that wants to fail loud on absence silently reads a default\n\t * instead \u2014 which is exactly how the magic 3.0 survived. */\n\tfecReorderToleranceMs?: number;\n\trepairTrackName?: string;\n\t/** Every base repair layer in order, layer 0 first, with derived RS_ID\n\t * offsets. Present whenever repairTrackName is. */\n\trepairLayers?: readonly RepairLayerParams[];\n\t/** Keyframe overlay instances declared for this track. Present (possibly\n\t * empty) whenever repairLayers is. */\n\trepairOverlays?: readonly RepairOverlayParams[];\n\tfps?: number;\n\tsampleRate?: number;\n};\n\nconst NO_FEC: TrackFecParams = {\n\tfecK: 0,\n\tfecRepairCount: 0,\n\tfecTotalRepairCount: 0,\n\tfecSymbolSize: 0,\n\tfecInterleaveMs: 0,\n};\n\n/**\n * Repair symbols per block a receiver has actually joined: the sum of\n * `repairSymbols` over the BASE layers whose repair track it joined\n * (draft-ramadan-moq-fec 6.3). This is a symbol count, not a count of layers.\n *\n * A keyframe overlay is a separate FEC instance with its own K, its own\n * RSB_length and its own RS_ID space, so it is never added here however it was\n * joined. Counting it would overstate the base instance's P and misreport\n * CMCD `x-bc-fec-p`.\n *\n * Falls back to P_0 when the track declares no layers, which is the\n * single-layer identity rather than a default: with one layer, P_0 is the\n * total.\n */\nexport function joinedRepairSymbolCount(\n\tparams: Pick<TrackFecParams, \"fecRepairCount\" | \"repairLayers\">,\n\tjoinedRepairTrackNames: Iterable<string>,\n): number {\n\tconst layers = params.repairLayers;\n\tif (layers === undefined || layers.length === 0) return params.fecRepairCount;\n\tconst joined = new Set(joinedRepairTrackNames);\n\tlet total = 0;\n\tfor (const layer of layers) {\n\t\tif (joined.has(layer.repairTrackName)) total += layer.repairSymbols;\n\t}\n\treturn total;\n}\n\nfunction recordOf(value: unknown): Record<string, unknown> | undefined {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;\n}\n\n/** The MSF `tracks[]` view of a catalog, or undefined for the hang dialect. */\nfunction msfTracks(catalog: RepairLayerCatalog): { tracks: Record<string, unknown>[] } | undefined {\n\tconst raw = (catalog as CatalogWithTracks).tracks;\n\tif (!Array.isArray(raw)) return undefined;\n\treturn { tracks: raw.map((t) => recordOf(t) ?? {}) };\n}\n\n/**\n * Resolve every repair track of one source track: base layers with derived\n * RS_ID offsets and keyframe overlays (draft-ramadan-moq-fec \u00A76.3), from either\n * catalog dialect.\n *\n * - MSF `tracks[]`: the source is the entry named `source`; repair tracks are\n * `packaging: \"fec-repair\"` entries. Layer 0 is the source's\n * `fec.repairTrack`; layers 1..n and overlays `depend` on the source.\n * - hang: the source is a `video`/`audio` rendition; repair tracks beyond\n * layer 0 are `repair.renditions` entries that `depend` on it.\n *\n * Returns undefined when the source carries no FEC repair track and nothing\n * depends on it. Throws `RepairContractError` (code + JSON path) on any\n * violation, including a repair rendition whose `depends` names a rendition\n * without FEC, so a player fails closed before joining any repair track.\n */\nexport function resolveRepairLayers(\n\tcatalog: RepairLayerCatalog | undefined,\n\tsource: string | CatalogTrackRef,\n): RepairLayersResolution | undefined {\n\tif (!catalog) return undefined;\n\tconst sourceName = typeof source === \"string\" ? source : source.name;\n\n\t// hang dialect first. When a catalog carries hang-native rendition blocks\n\t// beside a tracks[] switching-set index (the unified ABR ladder catalog),\n\t// the rendition blocks are authoritative, exactly as they are for hang's own\n\t// catalog decode. A typed ref resolves only within its own role.\n\tconst hang = catalog as CatalogWithRenditions;\n\tconst roles: Array<\"video\" | \"audio\"> =\n\t\ttypeof source === \"string\"\n\t\t\t? [\"video\", \"audio\"]\n\t\t\t: source.type === \"audio\"\n\t\t\t\t? [\"audio\"]\n\t\t\t\t: source.type === \"video\"\n\t\t\t\t\t? [\"video\"]\n\t\t\t\t\t: [\"video\", \"audio\"];\n\tconst role = roles.find((r) => recordOf(hang[r]?.renditions?.[sourceName]) !== undefined);\n\tif (role !== undefined) {\n\t\tconst renditionPath = `$.${role}.renditions[${JSON.stringify(sourceName)}]`;\n\t\tconst rendition = recordOf(hang[role]!.renditions![sourceName])!;\n\t\trejectRepairFieldsOnMediaTrack(renditionPath, rendition, throwRepairContractError);\n\t\tconst repairRenditions = recordOf(hang.repair?.renditions) ?? {};\n\t\tconst descriptors: RepairTrackDescriptor[] = Object.entries(repairRenditions).map(([name, entry]) => {\n\t\t\tconst path = `$.repair.renditions[${JSON.stringify(name)}]`;\n\t\t\tconst fields = recordOf(entry);\n\t\t\tif (fields === undefined) {\n\t\t\t\tthrowRepairContractError(\"track_repair_layer\", path, \"a repair rendition must be an object\");\n\t\t\t}\n\t\t\trejectRepairTrackGeometry(path, fields, throwRepairContractError);\n\t\t\treturn describeRepairTrack(name, path, fields);\n\t\t});\n\t\tconst fecOfRendition = (entry: unknown): Record<string, unknown> | undefined => {\n\t\t\tconst fec = recordOf(recordOf(entry)?.[\"fec\"]);\n\t\t\treturn fec !== undefined && fec[\"algorithm\"] !== \"none\" ? fec : undefined;\n\t\t};\n\t\tconst mediaRenditions = [\n\t\t\t...Object.values(recordOf(hang.video?.renditions) ?? {}),\n\t\t\t...Object.values(recordOf(hang.audio?.renditions) ?? {}),\n\t\t];\n\t\tconst reachability = repairReachability(mediaRenditions.map((entry) => recordOf(entry)?.[\"fec\"]));\n\t\tfor (const d of descriptors) {\n\t\t\tvalidateRepairTrackFields(d, throwRepairContractError);\n\t\t\tassertRepairTrackReachable(d, reachability, throwRepairContractError);\n\t\t\tconst target = dependsTarget(d);\n\t\t\tif (target === undefined) continue;\n\t\t\tconst targetFec =\n\t\t\t\tfecOfRendition(hang.video?.renditions?.[target]) ?? fecOfRendition(hang.audio?.renditions?.[target]);\n\t\t\tif (targetFec === undefined) {\n\t\t\t\tthrowRepairContractError(\n\t\t\t\t\t\"track_repair_depends\",\n\t\t\t\t\t`${d.path}.depends`,\n\t\t\t\t\t`${JSON.stringify(target)} is not a video or audio rendition with FEC`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\treturn assembleRepairLayers(\n\t\t\tgeometryOf(sourceName, `${renditionPath}.fec`, recordOf(rendition[\"fec\"])),\n\t\t\tdescriptors,\n\t\t\tthrowRepairContractError,\n\t\t);\n\t}\n\n\tconst msf = msfTracks(catalog);\n\tconst msfIndex = msf ? msf.tracks.findIndex((t) => t[\"name\"] === sourceName) : -1;\n\tif (!msf || msfIndex < 0) return undefined;\n\tconst track = msf.tracks[msfIndex]!;\n\tconst fec = recordOf(track[\"fec\"]);\n\tconst packagingOf = (t: Record<string, unknown>): unknown => t[\"packaging\"];\n\tconst fecOfTrack = (t: Record<string, unknown> | undefined): Record<string, unknown> | undefined => {\n\t\tconst trackFec = recordOf(t?.[\"fec\"]);\n\t\treturn trackFec !== undefined && trackFec[\"algorithm\"] !== \"none\" ? trackFec : undefined;\n\t};\n\t// Rule 8 on the resolver path too: a repair track carries no block geometry\n\t// and a media track carries no repair-track fields. Without this a repair\n\t// track with its own `fec` would count as a FEC source below and name\n\t// itself as layer 0, so rule 9 would never see it.\n\tconst descriptors: RepairTrackDescriptor[] = [];\n\tconst mediaTracks: Record<string, unknown>[] = [];\n\tmsf.tracks.forEach((t, i) => {\n\t\tconst path = `$.tracks[${i}]`;\n\t\tif (packagingOf(t) === \"fec-repair\") {\n\t\t\trejectRepairTrackGeometry(path, t, throwRepairContractError);\n\t\t\tif (typeof t[\"name\"] === \"string\") descriptors.push(describeRepairTrack(t[\"name\"], path, t));\n\t\t\treturn;\n\t\t}\n\t\trejectRepairFieldsOnMediaTrack(path, t, throwRepairContractError);\n\t\tmediaTracks.push(t);\n\t});\n\tconst reachability = repairReachability(mediaTracks.map((t) => t[\"fec\"]));\n\tfor (const d of descriptors) {\n\t\tvalidateRepairTrackFields(d, throwRepairContractError);\n\t\tassertRepairTrackReachable(d, reachability, throwRepairContractError);\n\t\tconst target = dependsTarget(d);\n\t\tif (target === undefined) continue;\n\t\tconst targetTrack = msf.tracks.find((t) => t[\"name\"] === target);\n\t\tif (targetTrack === undefined || packagingOf(targetTrack) !== \"mmtp\" || fecOfTrack(targetTrack) === undefined) {\n\t\t\tthrowRepairContractError(\n\t\t\t\t\"track_repair_depends\",\n\t\t\t\t`${d.path}.depends`,\n\t\t\t\t`${JSON.stringify(target)} is not an mmtp track with FEC`,\n\t\t\t);\n\t\t}\n\t}\n\treturn assembleRepairLayers(geometryOf(sourceName, `$.tracks[${msfIndex}].fec`, fec), descriptors, throwRepairContractError, {\n\t\tlayerZeroMustBeDeclared: true,\n\t});\n}\n\n/** A MoQ Transport priority (0..255, lower first), or null. */\nfunction moqtPriority(value: unknown): number | null {\n\treturn Number.isSafeInteger(value) && (value as number) >= 0 && (value as number) <= 255 ? (value as number) : null;\n}\n\n/**\n * The on-demand repair target of one source track, read from the catalog:\n * its base layer 0 repair track and the two catalog priorities\n * draft-ramadan-moq-fec \u00A711.4.5 derives the FETCH's SUBSCRIBER_PRIORITY from\n * (see `repairFetchSubscriberPriority`). This is what a player's\n * `ResolveRepairTarget` returns once it knows which track owns the block.\n *\n * - `repairPriority`: layer 0's catalog `priority`, or 240 when it declares\n * none; that value is defined by draft-ramadan-moq-fec \u00A710, not chosen\n * here (`resolveRepairLayers` applies it).\n * - `sourcePriority`: the source track's MSF `tracks[]` entry `priority`\n * (MoQ Transport scale). A catalog with no MSF entry for the track (a hang-only catalog,\n * whose renditions carry no MoQ Transport priority) yields null, and the\n * keyframe repair FETCH is then sent without the parameter.\n *\n * Returns null when the track has no repair track.\n *\n * @throws RepairContractError exactly as `resolveRepairLayers` does.\n */\nexport function catalogRepairTarget(\n\tcatalog: RepairLayerCatalog | undefined,\n\tsource: string | CatalogTrackRef,\n): RepairTarget | null {\n\tconst repair = resolveRepairLayers(catalog, source);\n\tconst layerZero = repair?.baseLayers[0];\n\tif (!catalog || !layerZero) return null;\n\tconst sourceName = typeof source === \"string\" ? source : source.name;\n\tconst msf = msfTracks(catalog);\n\tconst entry = msf?.tracks.find((t) => t[\"name\"] === sourceName);\n\tconst sourcePriority =\n\t\tentry === undefined ? null : moqtPriority(entry[\"priority\"]);\n\treturn { repairTrack: layerZero.repairTrackName, sourcePriority, repairPriority: layerZero.priority };\n}\n\nfunction geometryOf(name: string, path: string, fec: Record<string, unknown> | undefined): RepairSourceGeometry {\n\treturn {\n\t\tname,\n\t\tpath,\n\t\talgorithm: fec?.[\"algorithm\"],\n\t\tsourceSymbols: fec?.[\"sourceSymbols\"],\n\t\trepairSymbols: fec?.[\"repairSymbols\"],\n\t\trepairTrack: fec?.[\"repairTrack\"],\n\t};\n}\n\nfunction numericField(value: unknown): number | undefined {\n\tconst n = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : Number.NaN;\n\treturn Number.isFinite(n) && n > 0 ? n : undefined;\n}\n\n/** Like `numericField` but admits an explicit 0, and still rejects absent,\n * negative and non-finite input. Absence returns undefined so callers can\n * tell \"declared none\" from \"never declared\". */\nfunction nonNegativeField(value: unknown): number | undefined {\n\tconst n = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : Number.NaN;\n\treturn Number.isFinite(n) && n >= 0 ? n : undefined;\n}\n\n/**\n * The track-scoped reading of `readFecBlockProfile` (BLO-35597): the same one\n * rule every catalog reader applies (a declared FEC instance names a known\n * profile; a block declaring none may omit it, but a token it carries must be\n * known), with its one failure, `track_fec_block_profile`, returned as the\n * track's reason instead of thrown. A profile problem costs only its own\n * track: it never aborts resolving the catalog's other tracks, and the token\n * is still never defaulted or inferred.\n */\nfunction blockProfileUnavailable(\n\tvalue: unknown,\n\tpath: string,\n\tdeclaresFecInstance: boolean,\n): TrackFecUnavailable | undefined {\n\ttry {\n\t\treadFecBlockProfile(value, path, declaresFecInstance, throwRepairContractError);\n\t\treturn undefined;\n\t} catch (error) {\n\t\tif (!(error instanceof RepairContractError) || error.code !== \"track_fec_block_profile\") throw error;\n\t\treturn Object.freeze({ code: \"track_fec_block_profile\", path: error.path, message: error.message });\n\t}\n}\n\n/**\n * An `iso-ssbg1-v1` instance without a usable block span (`isoBlockSpanUsable`)\n * has no FEC for its track alone, instead of resolving to a configuration the\n * FEC manager refuses for every track it holds.\n */\nfunction isoSpanUnavailable(fec: unknown, path: string, declaresFecInstance: boolean): TrackFecUnavailable | undefined {\n\tif (!declaresFecInstance || isoBlockSpanUsable(fec)) return undefined;\n\treturn Object.freeze({\n\t\tcode: \"track_fec\",\n\t\tpath,\n\t\tmessage:\n\t\t\t`[repair-layers:track_fec] ${path}: iso-ssbg1-v1 requires a block span of at least ` +\n\t\t\t`${MIN_INTERLEAVE_DEPTH_MS} ms, which bounds its block location`,\n\t});\n}\n\n/**\n * Per-track FEC geometry, or `NO_FEC` when the rendition declares none (no\n * `fec`, or `algorithm: \"none\"`) or names no usable `blockProfile` or, under\n * `iso-ssbg1-v1`, no usable block span; the latter two carry `fecUnavailable`.\n *\n * @throws RepairContractError from `resolveRepairLayers` when the rendition\n * declares a `fec` block (any algorithm, `none` included) and the catalog's\n * repair contract is malformed, and `track_fec` when its `fec` declares an FEC\n * instance whose `sourceSymbols` or `symbolSize` is not a positive integer;\n * see `resolveCatalogTrackFec`. An absent or unknown `blockProfile`, or an\n * `iso-ssbg1-v1` span that is absent or below `MIN_INTERLEAVE_DEPTH_MS`, never\n * throws: it is this track's `fecUnavailable`.\n */\nfunction fecParams(\n\tcatalog: CatalogWithRenditions | undefined,\n\ttrack: CatalogTrackRef | undefined,\n\trendition: RenditionCatalog | undefined,\n\tfps?: number,\n\tsampleRate?: number,\n): TrackFecParams {\n\tconst mediaRate = sampleRate === undefined ? { fps } : { fps, sampleRate };\n\tconst fec = rendition?.fec;\n\t// Every caller passes a track with its rendition.\n\tif (!fec || !track) return { ...NO_FEC, ...mediaRate };\n\t// The repair contract is enforced for every declared `fec` block, including\n\t// one that declares no FEC instance (a repair layer may not depend on it).\n\tconst repair = resolveRepairLayers(catalog, track);\n\tconst fecPath = `$.${track.type}.renditions[${JSON.stringify(track.name)}].fec`;\n\t// One predicate for \"this track has FEC\", shared with resolveFecState: K\n\t// and T are positive integers, the algorithm is not `none`, and the block\n\t// profile is known.\n\tif (!catalogDeclaresFec(fec)) {\n\t\tconst declaresFecInstance = fec.algorithm !== \"none\";\n\t\tconst kValid = Number.isSafeInteger(fec.sourceSymbols) && (fec.sourceSymbols ?? 0) > 0;\n\t\tconst tValid = Number.isSafeInteger(fec.symbolSize) && (fec.symbolSize ?? 0) > 0;\n\t\tif (declaresFecInstance && !(kValid && tValid)) {\n\t\t\t// Any algorithm but `none` declares an FEC instance, so geometry a\n\t\t\t// decoder cannot configure is a malformed catalog, not \"no FEC\".\n\t\t\t// Returning NO_FEC would silently drop the track's repair\n\t\t\t// subscription (repairTrackName, repairLayers, repairOverlays).\n\t\t\tthrowRepairContractError(\n\t\t\t\t\"track_fec\",\n\t\t\t\t`${fecPath}.${kValid ? \"symbolSize\" : \"sourceSymbols\"}`,\n\t\t\t\t\"must be a positive integer\",\n\t\t\t);\n\t\t}\n\t\t// What remains is `algorithm: \"none\"` (which carries no profile; a token\n\t\t// it does carry must still be known) or a declared instance naming no\n\t\t// known profile. Neither has FEC; an unusable profile says why, for this\n\t\t// track alone.\n\t\tconst fecUnavailable =\n\t\t\tblockProfileUnavailable(fec.blockProfile, `${fecPath}.blockProfile`, declaresFecInstance) ??\n\t\t\tisoSpanUnavailable(fec, `${fecPath}.interleaveDepthMs`, declaresFecInstance);\n\t\treturn fecUnavailable === undefined ? { ...NO_FEC, ...mediaRate } : { ...NO_FEC, ...mediaRate, fecUnavailable };\n\t}\n\t// The predicate held, so the profile is known: this read cannot fail.\n\tconst fecBlockProfile = readFecBlockProfile(fec.blockProfile, `${fecPath}.blockProfile`, true, throwRepairContractError);\n\treturn {\n\t\tfecK: fec.sourceSymbols ?? 0,\n\t\tfecBlockProfile,\n\t\tfecRepairCount: fec.repairSymbols ?? 0,\n\t\t// The wire RSB_length: every base layer's packets carry the base total (\u00A77.1).\n\t\tfecTotalRepairCount: repair?.totalBaseRepairSymbols ?? fec.repairSymbols ?? 0,\n\t\tfecSymbolSize: fec.symbolSize ?? 0,\n\t\t// interleaveDepthMs is the FEC block span in MILLISECONDS (\u00A75.1) \u2014 the\n\t\t// only interleave key the catalog carries post-BLO-14526. Receivers\n\t\t// derive D by positive round-half-up of span / GOP duration where needed.\n\t\tfecInterleaveMs: catalogFecInterleaveMs(fec) ?? 0,\n\t\t// Additive, never a multiplier on the span: jitter belongs to the network\n\t\t// path and the span belongs to the encoder, so scaling one by the other\n\t\t// hands one network different slack per track. `numericField` rejects 0,\n\t\t// so it cannot be reused here \u2014 a declared 0 is a legitimate value.\n\t\tfecReorderToleranceMs: nonNegativeField(fec.reorderToleranceMs),\n\t\trepairTrackName: fec.repairTrack,\n\t\trepairLayers: repair?.baseLayers,\n\t\trepairOverlays: repair?.overlays,\n\t\t...mediaRate,\n\t};\n}\n\n/**\n * Resolves per-track FEC geometry from the catalog for the active endpoint tracks.\n *\n * `video`/`audio` (and the top-level names and `catalogFps`) describe the\n * FIRST video and audio track of `tracks`. `forTrack` resolves any track from\n * its OWN rendition, scoped by its type: a second video track (an ABR rung or\n * simulcast variant, possibly sharing a packetId) gets its own framerate,\n * interleave and repair layers, never the first rendition's. A track with no\n * rendition of its type resolves to no FEC (`fecK` 0) and no media rate.\n *\n * The block profile is track-scoped (BLO-35597). A rendition whose `fec`\n * declares an FEC instance but names no `blockProfile`, or whose `fec` (a\n * `none` block included) names a token that is not a known profile, resolves\n * to no FEC: `fecK` 0, no `fecBlockProfile`, no repair track, and\n * `fecUnavailable` carrying the `track_fec_block_profile` reason at the\n * field's JSON path for the caller to surface. Every other track resolves as\n * usual. The profile is never defaulted or inferred; `algorithm: \"none\"` may\n * omit it.\n *\n * @throws RepairContractError `track_fec` when a rendition's `fec` declares an\n * FEC instance (any `algorithm` but `none`) whose `sourceSymbols` or\n * `symbolSize` is not a positive integer, and when a rendition that declares a\n * `fec` block (any algorithm, `none` included) carries a repair contract that\n * violates draft-ramadan-moq-fec 6.3 (a gap or\n * duplicate in the layer chain, a bad `depends` target, an unreachable repair\n * track, a bound violation). Because the hang dialect validates every `repair.renditions`\n * entry of the catalog, resolving the video track fails on a malformed audio\n * repair rendition too. This is deliberate: a player must fail closed before it\n * joins any repair track. Callers that used to treat this function as total\n * must catch it and surface the catalog error.\n */\nexport function resolveCatalogTrackFec(\n\tcatalog: CatalogWithRenditions | undefined,\n\ttracks: readonly CatalogTrackRef[],\n): {\n\tvideoTrackName?: string;\n\taudioTrackName?: string;\n\tmissingVideoRendition?: string;\n\tmissingAudioRendition?: string;\n\tvideoForKeyframeRepair?: KeyframeCadenceCatalog;\n\tcatalogFps?: number;\n\tvideo: TrackFecParams;\n\taudio: TrackFecParams;\n\tforTrack(track: CatalogTrackRef): TrackFecParams;\n} {\n\tconst videoTrackName = tracks.find((t) => t.type === \"video\")?.name;\n\tconst audioTrackName = tracks.find((t) => t.type === \"audio\")?.name;\n\tconst videoRenditions = catalog?.video?.renditions;\n\tconst audioRenditions = catalog?.audio?.renditions;\n\tconst video = videoTrackName ? videoRenditions?.[videoTrackName] : undefined;\n\tconst audio = audioTrackName ? audioRenditions?.[audioTrackName] : undefined;\n\tconst catalogFps = numericField(video?.framerate);\n\tconst sampleRate = numericField(audio?.sampleRate ?? audio?.samplerate);\n\tconst audioFps = sampleRate ? sampleRate / 1024 : undefined;\n\tconst videoParams = fecParams(catalog, videoTrackName ? { name: videoTrackName, type: \"video\" } : undefined, video, catalogFps);\n\tconst audioParams = fecParams(\n\t\tcatalog,\n\t\taudioTrackName ? { name: audioTrackName, type: \"audio\" } : undefined,\n\t\taudio,\n\t\taudioFps,\n\t\tsampleRate,\n\t);\n\n\tconst forTrack = (track: CatalogTrackRef): TrackFecParams => {\n\t\tif (track.type === \"audio\") {\n\t\t\tif (track.name === audioTrackName) return audioParams;\n\t\t\tconst rendition = audioRenditions?.[track.name];\n\t\t\tif (!rendition) return { ...NO_FEC };\n\t\t\tconst rate = numericField(rendition.sampleRate ?? rendition.samplerate);\n\t\t\treturn fecParams(catalog, { name: track.name, type: \"audio\" }, rendition, rate ? rate / 1024 : undefined, rate);\n\t\t}\n\t\tif (track.name === videoTrackName) return videoParams;\n\t\tconst rendition = videoRenditions?.[track.name];\n\t\tif (!rendition) return { ...NO_FEC };\n\t\treturn fecParams(catalog, { name: track.name, type: \"video\" }, rendition, numericField(rendition.framerate));\n\t};\n\n\treturn {\n\t\tvideoTrackName,\n\t\taudioTrackName,\n\t\tmissingVideoRendition: videoTrackName && videoRenditions && !video ? videoTrackName : undefined,\n\t\tmissingAudioRendition: audioTrackName && audioRenditions && !audio ? audioTrackName : undefined,\n\t\tvideoForKeyframeRepair: video,\n\t\tcatalogFps,\n\t\tvideo: videoParams,\n\t\taudio: audioParams,\n\t\tforTrack,\n\t};\n}\n", "/**\n * `multicast.auth` v2 \u2014 catalog representation for per-track source-auth\n * scopes and manifest tracks (BLO-16700 decision).\n *\n * Reconciles two previously-accepted, mutually incompatible contracts:\n * - BLO-16481 (S0): every publisher session/track/source-symbol-format\n * needs its own fresh, unpredictable 32-byte `auth_scope`, bound into\n * the signed per-group Merkle root (see `sourceAuthSignaturePreimage`\n * in ./source-auth.ts).\n * - BLO-16485 (S3 draft): a singular `multicast.auth` block with one\n * `manifestTrack` string and no `authScope` field at all \u2014 unable to\n * describe two protected tracks (e.g. audio + video) in one session.\n *\n * `multicast.auth` v2 keeps one shared signing identity (scheme, hash,\n * sig, publicKey, keyId, previousKey, compaction, enforcement) but moves\n * `manifestTrack` into a `tracks[]` array, one entry per protected media\n * track, each carrying its own `manifestTrack` name, `authScope`, and\n * `sourceSymbolFormat`. This is additive over the S3 draft shape (every\n * top-level field the draft specified still exists at the top level) \u2014\n * only `manifestTrack` moves from top-level into each track entry.\n *\n * `manifestTrack` is carried as an opaque string field, deliberately NOT\n * run through `parseTrackRole`/`VALID_ROLE_SUFFIXES` in ./carp-catalog.ts.\n * `carp-catalog.ts` only recognises `base`/`delta`/`repair` role suffixes\n * and throws on anything else (including a hypothetical `manifest` role);\n * resolving BLO-16485's \"manifest-track name collision\" MANDATORY item by\n * adding a role would force every catalog consumer to learn a fourth role\n * and would break `validateSwitchingSet`'s base/delta/repair completeness\n * check. Carrying it as a typed field here avoids both.\n */\n\nexport type MulticastAuthScheme = \"bc-provenance\";\nexport type MulticastAuthHash = \"blake3\";\nexport type MulticastAuthSig = \"ed25519\";\nexport type MulticastAuthCompaction = \"merkle\" | \"none\";\nexport type MulticastAuthEnforcement = \"shadow\" | \"enforce\";\nexport type MulticastAuthMode = \"off\" | MulticastAuthEnforcement;\n\nconst RAW_ED25519_KEY_LENGTH = 32;\nconst SOURCE_AUTH_SCOPE_LENGTH = 32;\n\nconst BASE64URL_ALPHABET =\n\t\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\n\n/** Encode raw bytes as unpadded base64url \u2014 the one accepted encoding for\n * `authScope`/`publicKey`/`previousKey` (BLO-16483's operator quickstart). */\nexport function encodeBase64Url(bytes: Uint8Array): string {\n\tlet output = \"\";\n\tfor (let i = 0; i < bytes.length; i += 3) {\n\t\tconst b0 = bytes[i]!;\n\t\tconst b1 = bytes[i + 1];\n\t\tconst b2 = bytes[i + 2];\n\t\toutput += BASE64URL_ALPHABET[b0 >> 2];\n\t\toutput += BASE64URL_ALPHABET[((b0 & 0x03) << 4) | (b1 === undefined ? 0 : b1 >> 4)];\n\t\tif (b1 === undefined) break;\n\t\toutput += BASE64URL_ALPHABET[((b1 & 0x0f) << 2) | (b2 === undefined ? 0 : b2 >> 6)];\n\t\tif (b2 === undefined) break;\n\t\toutput += BASE64URL_ALPHABET[b2 & 0x3f];\n\t}\n\treturn output;\n}\n\nconst BASE64URL_DECODE_TABLE = (() => {\n\tconst table = new Int8Array(128).fill(-1);\n\tfor (let i = 0; i < BASE64URL_ALPHABET.length; i++) {\n\t\ttable[BASE64URL_ALPHABET.charCodeAt(i)] = i;\n\t}\n\treturn table;\n})();\n\n/** Decode unpadded base64url text to raw bytes. Throws on any padding\n * (`=`), standard-alphabet (`+`/`/`), or otherwise invalid character. */\nexport function decodeBase64Url(text: string): Uint8Array {\n\tif (text.length % 4 === 1) {\n\t\tthrow new Error(`[multicast-auth-catalog] invalid unpadded base64url length for \"${text}\"`);\n\t}\n\tconst bytes: number[] = [];\n\tlet buffer = 0;\n\tlet bits = 0;\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst code = text.charCodeAt(i);\n\t\tconst value = code < 128 ? BASE64URL_DECODE_TABLE[code] : -1;\n\t\tif (value === undefined || value < 0) {\n\t\t\tthrow new Error(`[multicast-auth-catalog] invalid base64url character in \"${text}\"`);\n\t\t}\n\t\tbuffer = (buffer << 6) | value;\n\t\tbits += 6;\n\t\tif (bits >= 8) {\n\t\t\tbits -= 8;\n\t\t\tbytes.push((buffer >> bits) & 0xff);\n\t\t}\n\t}\n\tconst decoded = Uint8Array.from(bytes);\n\tif (encodeBase64Url(decoded) !== text) {\n\t\tthrow new Error(`[multicast-auth-catalog] non-canonical base64url encoding \"${text}\"`);\n\t}\n\treturn decoded;\n}\n\n/** Decode + length-validate an `authScope` field. Throws unless it decodes\n * to exactly `SOURCE_AUTH_SCOPE_LENGTH` (32) raw bytes. */\nexport function decodeAuthScope(authScope: string): Uint8Array {\n\tconst bytes = decodeBase64Url(authScope);\n\tif (bytes.byteLength !== SOURCE_AUTH_SCOPE_LENGTH) {\n\t\tthrow new Error(\n\t\t\t`[multicast-auth-catalog] authScope must decode to ${SOURCE_AUTH_SCOPE_LENGTH} raw bytes, got ${bytes.byteLength}`,\n\t\t);\n\t}\n\treturn bytes;\n}\n\n/** Decode a catalog key in the only accepted representation: exactly 32 raw\n * ed25519 bytes encoded as canonical, unpadded base64url. */\nexport function decodeEd25519PublicKey(publicKey: string): Uint8Array {\n\tconst bytes = decodeBase64Url(publicKey);\n\tif (bytes.byteLength !== RAW_ED25519_KEY_LENGTH) {\n\t\tthrow new Error(\n\t\t\t`[multicast-auth-catalog] public key must decode to ${RAW_ED25519_KEY_LENGTH} raw bytes, got ${bytes.byteLength}`,\n\t\t);\n\t}\n\treturn bytes;\n}\n\n/**\n * One protected media track's manifest + scope binding.\n *\n * - `mediaTrack`: the actual media track name (e.g. `\"video/base\"`).\n * - `manifestTrack`: the sibling track carrying this track's signed\n * manifest objects (see ./source-auth.ts `SourceAuthObjectEntry`/\n * `SourceAuthGroupClose`). Opaque string \u2014 not role-parsed.\n * - `authScope`: this track's unique 32-byte scope, raw bytes as unpadded\n * base64url. BLO-16481 requires a scope fresh per (publisher session,\n * track, source-symbol format) \u2014 a track publishing under two source-\n * symbol formats in the same session MUST appear as two entries here,\n * each with its own scope.\n * - `sourceSymbolFormat`: names the deployed source-symbol profile this\n * track's authenticated bytes are derived from (e.g.\n * `\"full-mmtp-packet\"` or `\"fec-padded-recovery\"`, the two formats\n * BLO-16481's S0 fixture covers).\n */\nexport interface MulticastAuthTrackConfig {\n\tmediaTrack: string;\n\tmanifestTrack: string;\n\tauthScope: string;\n\tsourceSymbolFormat: string;\n}\n\n/**\n * `multicast.auth` v2 \u2014 supersedes the BLO-16485 draft's singular\n * `manifestTrack`/no-`authScope` shape (BLO-16700 decision).\n */\nexport interface MulticastAuthConfig {\n\tscheme: MulticastAuthScheme;\n\thash: MulticastAuthHash;\n\tsig: MulticastAuthSig;\n\t/** Raw 32-byte ed25519 public key, unpadded base64url. */\n\tpublicKey: string;\n\tkeyId: string;\n\t/** Raw 32-byte ed25519 public key, unpadded base64url. Accepted\n\t * alongside `publicKey` during a key-rotation overlap window. */\n\tpreviousKey?: string;\n\tcompaction: MulticastAuthCompaction;\n\t/** Player-config-wins default; catalog value only selects the default\n\t * when the player hasn't pinned one (S3 enforcement precedence). */\n\tenforcement?: MulticastAuthEnforcement;\n\ttracks: MulticastAuthTrackConfig[];\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n\treturn typeof value === \"string\" && value.length > 0;\n}\n\nfunction isValidAuthScopeField(value: unknown): value is string {\n\tif (!isNonEmptyString(value)) return false;\n\ttry {\n\t\tdecodeAuthScope(value);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction isValidPublicKeyField(value: unknown): value is string {\n\tif (!isNonEmptyString(value)) return false;\n\ttry {\n\t\tdecodeEd25519PublicKey(value);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction hasOnlyKeys(object: Record<string, unknown>, allowed: readonly string[]): boolean {\n\tconst allowedKeys = new Set(allowed);\n\treturn Object.keys(object).every((key) => allowedKeys.has(key));\n}\n\nexport function isMulticastAuthTrackConfig(obj: unknown): obj is MulticastAuthTrackConfig {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\treturn (\n\t\thasOnlyKeys(o, [\"mediaTrack\", \"manifestTrack\", \"authScope\", \"sourceSymbolFormat\"]) &&\n\t\tisNonEmptyString(o[\"mediaTrack\"]) &&\n\t\tisNonEmptyString(o[\"manifestTrack\"]) &&\n\t\to[\"mediaTrack\"] !== o[\"manifestTrack\"] &&\n\t\tisValidAuthScopeField(o[\"authScope\"]) &&\n\t\tisNonEmptyString(o[\"sourceSymbolFormat\"])\n\t);\n}\n\nconst VALID_COMPACTION = new Set<string>([\"merkle\", \"none\"]);\nconst VALID_ENFORCEMENT = new Set<string>([\"shadow\", \"enforce\"]);\n\n/** Semantic uniqueness rule shared by runtime parsing and the published\n * catalog schema's `uniqueMulticastAuthTrackBindings` keyword. */\nexport function hasUniqueMulticastAuthTrackBindings(tracks: readonly unknown[]): boolean {\n\tconst bindings = tracks.flatMap((track) => {\n\t\tif (typeof track !== \"object\" || track === null) return [];\n\t\tconst value = track as Record<string, unknown>;\n\t\tif (typeof value[\"mediaTrack\"] !== \"string\" || typeof value[\"sourceSymbolFormat\"] !== \"string\") {\n\t\t\treturn [];\n\t\t}\n\t\treturn [JSON.stringify([value[\"mediaTrack\"], value[\"sourceSymbolFormat\"]])];\n\t});\n\treturn new Set(bindings).size === bindings.length;\n}\n\nexport function isMulticastAuthConfig(obj: unknown): obj is MulticastAuthConfig {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\n\tif (o[\"scheme\"] !== \"bc-provenance\") return false;\n\tif (o[\"hash\"] !== \"blake3\") return false;\n\tif (o[\"sig\"] !== \"ed25519\") return false;\n\tif (!hasOnlyKeys(o, [\"scheme\", \"hash\", \"sig\", \"publicKey\", \"keyId\", \"previousKey\", \"compaction\", \"enforcement\", \"tracks\"])) return false;\n\tif (!isValidPublicKeyField(o[\"publicKey\"])) return false;\n\tif (!isNonEmptyString(o[\"keyId\"])) return false;\n\tif (o[\"previousKey\"] !== undefined && !isValidPublicKeyField(o[\"previousKey\"])) return false;\n\tif (o[\"previousKey\"] === o[\"publicKey\"]) return false;\n\tif (typeof o[\"compaction\"] !== \"string\" || !VALID_COMPACTION.has(o[\"compaction\"])) return false;\n\tif (o[\"enforcement\"] !== undefined) {\n\t\tif (typeof o[\"enforcement\"] !== \"string\" || !VALID_ENFORCEMENT.has(o[\"enforcement\"])) return false;\n\t}\n\n\tif (!Array.isArray(o[\"tracks\"]) || o[\"tracks\"].length === 0) return false;\n\tconst tracks = o[\"tracks\"] as unknown[];\n\tif (!tracks.every((t) => isMulticastAuthTrackConfig(t))) return false;\n\tconst typedTracks = tracks as MulticastAuthTrackConfig[];\n\n\t// BLO-16481: a fresh scope per track \u2014 catalog-declared scopes must be\n\t// pairwise distinct within one auth block.\n\tconst scopes = typedTracks.map((t) => t.authScope);\n\tif (new Set(scopes).size !== scopes.length) return false;\n\tif (!hasUniqueMulticastAuthTrackBindings(typedTracks)) return false;\n\n\t// Each entry must point at its own manifest track.\n\tconst manifestTracks = typedTracks.map((t) => t.manifestTrack);\n\tif (new Set(manifestTracks).size !== manifestTracks.length) return false;\n\n\treturn true;\n}\n\nexport interface ResolvedMulticastAuthConfig {\n\tkeyId: string;\n\t/** Current key first, followed by the overlap key when advertised. */\n\tacceptedPublicKeys: Uint8Array[];\n\tmode: MulticastAuthMode;\n\tcompaction: MulticastAuthCompaction;\n\ttracks: Array<Omit<MulticastAuthTrackConfig, \"authScope\"> & { authScope: Uint8Array }>;\n}\n\n/** Player configuration is authoritative, including an explicit `off`.\n * Catalog enforcement is only a default; an auth block without it defaults\n * to shadow, while a catalog without auth defaults to off. */\nexport function resolveMulticastAuthMode(\n\tplayerMode: MulticastAuthMode | undefined,\n\tauth: MulticastAuthConfig | undefined,\n): MulticastAuthMode {\n\tif (playerMode !== undefined) return playerMode;\n\tif (auth === undefined) return \"off\";\n\treturn auth.enforcement ?? \"shadow\";\n}\n\n/** Parse and resolve the catalog auth block into verifier-ready raw keys and\n * scopes. Invalid blocks and dangling media/manifest track references fail\n * closed instead of silently falling back to unauthenticated playback. */\nexport function resolveMulticastAuthCatalog(\n\tcatalog: unknown,\n\tplayerMode?: MulticastAuthMode,\n): ResolvedMulticastAuthConfig | undefined {\n\tif (typeof catalog !== \"object\" || catalog === null || Array.isArray(catalog)) {\n\t\tthrow new Error(\"[multicast-auth-catalog] catalog must be an object\");\n\t}\n\tconst root = catalog as Record<string, unknown>;\n\tconst rawAuth = root[\"multicast.auth\"];\n\tif (rawAuth === undefined) {\n\t\tif (playerMode === \"enforce\") {\n\t\t\tthrow new Error(\"[multicast-auth-catalog] player enforce mode requires multicast.auth\");\n\t\t}\n\t\treturn undefined;\n\t}\n\tif (!isMulticastAuthConfig(rawAuth)) {\n\t\tthrow new Error(\"[multicast-auth-catalog] invalid multicast.auth v2 block\");\n\t}\n\n\tconst rawTracks = root[\"tracks\"];\n\tif (!Array.isArray(rawTracks)) {\n\t\tthrow new Error(\"[multicast-auth-catalog] catalog tracks must be an array\");\n\t}\n\tconst catalogTrackNames = new Set(\n\t\trawTracks.flatMap((track) =>\n\t\t\ttypeof track === \"object\" && track !== null &&\n\t\t\ttypeof (track as Record<string, unknown>)[\"name\"] === \"string\"\n\t\t\t\t? [(track as Record<string, unknown>)[\"name\"] as string]\n\t\t\t\t: []\n\t\t),\n\t);\n\tfor (const track of rawAuth.tracks) {\n\t\tif (!catalogTrackNames.has(track.mediaTrack)) {\n\t\t\tthrow new Error(`[multicast-auth-catalog] unknown mediaTrack \"${track.mediaTrack}\"`);\n\t\t}\n\t\tif (!catalogTrackNames.has(track.manifestTrack)) {\n\t\t\tthrow new Error(`[multicast-auth-catalog] unknown manifestTrack \"${track.manifestTrack}\"`);\n\t\t}\n\t}\n\n\treturn {\n\t\tkeyId: rawAuth.keyId,\n\t\tacceptedPublicKeys: [\n\t\t\tdecodeEd25519PublicKey(rawAuth.publicKey),\n\t\t\t...(rawAuth.previousKey === undefined ? [] : [decodeEd25519PublicKey(rawAuth.previousKey)]),\n\t\t],\n\t\tmode: resolveMulticastAuthMode(playerMode, rawAuth),\n\t\tcompaction: rawAuth.compaction,\n\t\ttracks: rawAuth.tracks.map((track) => ({\n\t\t\t...track,\n\t\t\tauthScope: decodeAuthScope(track.authScope),\n\t\t})),\n\t};\n}\n", "// Layered and overlay FEC repair contract (draft-ramadan-moq-fec \u00A76.3).\n//\n// One source track may carry several repair tracks. Every base layer is\n// generated from the same source partition (RaptorQ, K, T, interleaveDepthMs,\n// reorderToleranceMs) and differs from its siblings only in its repair symbol\n// count P_i. Layer i owns the Repair Symbol IDs [O_i, O_i + P_i) with\n// O_i = sum(P_j, j < i), and ESI = K + RS_ID, so any subset of layers combines\n// into one decode. A keyframe overlay is a second, independent FEC instance\n// covering only random access point fragments, with its own K and P and its\n// own RS_ID space starting at 0.\n//\n// Repair tracks describe themselves (`depends`, `repairLayer`, `scope`); names\n// carry no meaning. This module holds the one rule set both catalog dialects\n// (MSF `tracks[]` and hang `repair.renditions`) resolve through, so the CMSF\n// validator and the player resolver cannot drift apart.\n//\n// The one dialect difference is where layer 0 lives. In MSF, `fec.repairTrack`\n// must name a `fec-repair` track in `tracks[]`, so a dangling pointer fails\n// closed (`layerZeroMustBeDeclared`). In hang, layer 0 is named only by the\n// rendition's `fec.repairTrack` and needs no `repair.renditions` entry; an\n// entry there is optional and, when present, must agree with the source.\n\nimport { parseFecBlockProfile, type FecBlockProfile } from \"@blockcast/mmt-base\";\n\n/** Lowest MoQT-scale priority a repair track may declare. */\nexport const REPAIR_PRIORITY_MIN = 192;\n/** Highest MoQT-scale priority a repair track may declare. */\nexport const REPAIR_PRIORITY_MAX = 255;\n/** Priority of a base layer 0 track that declares none. */\nexport const DEFAULT_REPAIR_PRIORITY = 240;\n/** RFC 6330 \u00A74.4.1.2: the largest number of source symbols in one RaptorQ source block. */\nexport const MAX_RAPTORQ_SOURCE_SYMBOLS = 56403;\n/** The 24-bit RS_ID field (\u00A77.1) bounds the repair total of one FEC instance. */\nexport const MAX_REPAIR_SYMBOLS = 0xffffff;\n/** The 24-bit ESI bounds K + totalP - 1 for one FEC instance. */\nexport const MAX_ENCODING_SYMBOL_ID = 0xffffff;\n\nexport type RepairContractCode =\n\t| \"track_fec\"\n\t| \"track_repair_track\"\n\t| \"track_repair_depends\"\n\t| \"track_repair_layer\"\n\t| \"track_repair_priority\"\n\t| \"track_repair_overlay\"\n\t| \"track_repair_geometry\"\n\t| \"track_fec_bounds\"\n\t| \"track_fec_block_profile\";\n\n/** Reports one contract violation and never returns. */\nexport type RepairContractFail = (code: RepairContractCode, path: string, message: string) => never;\n\nexport class RepairContractError extends Error {\n\tconstructor(\n\t\tpublic readonly code: RepairContractCode,\n\t\tpublic readonly path: string,\n\t\tmessage: string,\n\t) {\n\t\tsuper(`[repair-layers:${code}] ${path}: ${message}`);\n\t\tthis.name = \"RepairContractError\";\n\t}\n}\n\nexport const throwRepairContractError: RepairContractFail = (code, path, message) => {\n\tthrow new RepairContractError(code, path, message);\n};\n\n/**\n * Read one catalog `fec.blockProfile` (BLO-35597, ISO/IEC 23008-1 Annex C):\n * the one rule both catalog dialects, the CMSF validator and the per-track\n * resolver apply.\n *\n * A `fec` block that declares an FEC instance (any `algorithm` other than\n * `\"none\"`) MUST name its block profile, so an absent or unknown token fails\n * with `track_fec_block_profile` at `path`. A block declaring no FEC instance\n * may omit it (returns undefined), but a token it does carry must be known.\n * The token is never defaulted and never inferred: `k-padded-v1` and\n * `iso-ssbg1-v1` carry identical wire tuples whose repair math differs.\n *\n * @param path JSON path of the `blockProfile` field, for the error.\n */\nexport function readFecBlockProfile(\n\tvalue: unknown,\n\tpath: string,\n\tdeclaresFecInstance: true,\n\tfail: RepairContractFail,\n): FecBlockProfile;\nexport function readFecBlockProfile(\n\tvalue: unknown,\n\tpath: string,\n\tdeclaresFecInstance: boolean,\n\tfail: RepairContractFail,\n): FecBlockProfile | undefined;\nexport function readFecBlockProfile(\n\tvalue: unknown,\n\tpath: string,\n\tdeclaresFecInstance: boolean,\n\tfail: RepairContractFail,\n): FecBlockProfile | undefined {\n\tif (value === undefined && !declaresFecInstance) return undefined;\n\ttry {\n\t\treturn parseFecBlockProfile(value, \"blockProfile\");\n\t} catch (error) {\n\t\treturn fail(\"track_fec_block_profile\", path, (error as Error).message);\n\t}\n}\n\n/** The self-describing fields of one repair track entry, in either dialect. */\nexport type RepairTrackDescriptor = {\n\t/** Track (MSF) or rendition (hang) name. */\n\tname: string;\n\t/** JSON path of the entry, for error reporting. */\n\tpath: string;\n\tdepends?: unknown;\n\trepairLayer?: unknown;\n\trepairSymbols?: unknown;\n\tpriority?: unknown;\n\tscope?: unknown;\n\tsourceSymbols?: unknown;\n};\n\n/** The source track's `fec` object as the layer contract reads it. */\nexport type RepairSourceGeometry = {\n\t/** Source track or rendition name. */\n\tname: string;\n\t/** JSON path of the `fec` object, for error reporting. */\n\tpath: string;\n\talgorithm?: unknown;\n\t/** K. */\n\tsourceSymbols?: unknown;\n\t/** P_0. */\n\trepairSymbols?: unknown;\n\t/** Layer 0 track name. */\n\trepairTrack?: unknown;\n};\n\n/** A resolved base repair layer with its derived RS_ID range. */\nexport type RepairLayerParams = {\n\t/** Layer index; 0 is the track named by the source's `fec.repairTrack`. */\n\tlayer: number;\n\trepairTrackName: string;\n\t/** P_i: repair symbols per block carried by this layer. */\n\trepairSymbols: number;\n\t/** O_i = sum(P_j for j < i): the first RS_ID this layer carries. ESI = K + RS_ID. */\n\trsIdOffset: number;\n\t/** MoQT-scale priority (lower first); 240 when layer 0 declares none. */\n\tpriority: number;\n};\n\n/** A resolved keyframe overlay: a separate FEC instance over RAP fragments only. */\nexport type RepairOverlayParams = {\n\trepairTrackName: string;\n\tscope: \"keyframe\";\n\t/** K_overlay: source symbols of the overlay instance. */\n\tsourceSymbols: number;\n\t/** P_overlay: repair symbols per overlay block; also the overlay's own RSB_length. */\n\trepairSymbols: number;\n\t/** MoQT-scale priority, independent of the base layer chain. */\n\tpriority: number;\n};\n\nexport type RepairLayersResolution = {\n\tsourceTrackName: string;\n\t/** Base layers in layer order, layer 0 first. Never empty. */\n\tbaseLayers: readonly RepairLayerParams[];\n\t/** Keyframe overlay instances, in catalog order. Empty when none is declared. */\n\t/** At most one entry: the wire carries one overlay trailer per RAP packet. */\n\toverlays: readonly RepairOverlayParams[];\n\t/** Sum of P over base layers: the wire RSB_length on every base layer packet. */\n\ttotalBaseRepairSymbols: number;\n};\n\nconst REPAIR_FIELDS = [\"depends\", \"repairLayer\", \"repairSymbols\", \"priority\", \"scope\", \"sourceSymbols\"] as const;\n\n/** Track-level block geometry keys a repair track must not carry (\u00A76.3 rule 8): geometry is the source's. */\nexport const REPAIR_TRACK_GEOMETRY_KEYS = [\n\t\"mmtpMode\",\n\t\"timescale\",\n\t\"groupDurationMs\",\n\t\"groupDurationTicks\",\n\t\"keyframeIntervalMs\",\n\t\"keyframeIntervalTicks\",\n\t\"fec\",\n\t\"symbolSize\",\n\t\"interleaveDepthMs\",\n] as const;\n\n/** Self-describing repair fields that only a repair track or repair rendition may carry. */\nexport const REPAIR_TRACK_ONLY_KEYS = [\"repairLayer\", \"repairSymbols\", \"sourceSymbols\", \"scope\"] as const;\n\n/** Rule 8: a repair track (MSF) or repair rendition (hang) carries no block geometry. */\nexport function rejectRepairTrackGeometry(path: string, fields: Record<string, unknown>, fail: RepairContractFail): void {\n\tfor (const key of REPAIR_TRACK_GEOMETRY_KEYS) {\n\t\tif (fields[key] !== undefined) {\n\t\t\tfail(\n\t\t\t\t\"track_repair_geometry\",\n\t\t\t\t`${path}.${key}`,\n\t\t\t\t\"a repair track carries no block geometry; it is shared with the source track's fec object\",\n\t\t\t);\n\t\t}\n\t}\n}\n\n/** Which repair tracks a catalog can reach, for rule 9 (no orphan repair track). */\nexport type RepairReachability = {\n\t/** True once any track or rendition carries `fec` with an algorithm other than `none`. */\n\tdeclaresFecSource: boolean;\n\t/** Every non-empty `fec.repairTrack` a FEC source names: the layer-0 tracks. */\n\tlayerZeroNames: ReadonlySet<string>;\n};\n\n/**\n * Derive rule 9's inputs from the raw `fec` objects of every media track (MSF,\n * packaging other than `fec-repair`) or media rendition (hang) in a catalog.\n * Callers must not pass repair tracks: rule 8 forbids `fec` on them, and a\n * repair track that carried one would otherwise name itself as its own\n * layer 0 and slip past rule 9. One derivation for the validator and both\n * resolver dialects, so an empty-string pointer or an `algorithm: \"none\"`\n * source is read the same way everywhere.\n */\nexport function repairReachability(fecObjects: Iterable<unknown>): RepairReachability {\n\tlet declaresFecSource = false;\n\tconst layerZeroNames = new Set<string>();\n\tfor (const candidate of fecObjects) {\n\t\tif (typeof candidate !== \"object\" || candidate === null || Array.isArray(candidate)) continue;\n\t\tconst fec = candidate as Record<string, unknown>;\n\t\tif (fec[\"algorithm\"] === \"none\") continue;\n\t\tdeclaresFecSource = true;\n\t\tconst repairTrack = fec[\"repairTrack\"];\n\t\tif (typeof repairTrack === \"string\" && repairTrack.length > 0) layerZeroNames.add(repairTrack);\n\t}\n\treturn { declaresFecSource, layerZeroNames };\n}\n\n/**\n * Rule 9: once a catalog declares a FEC source, a repair track that declares no\n * `depends` must be some source's layer 0. A catalog with no FEC source may\n * still carry a naming-era repair track that nothing joins.\n */\nexport function assertRepairTrackReachable(\n\tdescriptor: RepairTrackDescriptor,\n\treachability: RepairReachability,\n\tfail: RepairContractFail,\n): void {\n\tif (dependsTarget(descriptor) !== undefined) return;\n\tif (reachability.declaresFecSource && !reachability.layerZeroNames.has(descriptor.name)) {\n\t\tfail(\n\t\t\t\"track_repair_depends\",\n\t\t\t`${descriptor.path}.depends`,\n\t\t\t\"no source names this repair track in fec.repairTrack and it declares no depends\",\n\t\t);\n\t}\n}\n\n/** A media track (MSF) or media rendition (hang) carries no repair-track fields. */\nexport function rejectRepairFieldsOnMediaTrack(path: string, fields: Record<string, unknown>, fail: RepairContractFail): void {\n\tfor (const key of REPAIR_TRACK_ONLY_KEYS) {\n\t\tif (fields[key] !== undefined) {\n\t\t\tfail(\"track_repair_layer\", `${path}.${key}`, `${key} is a repair track field`);\n\t\t}\n\t}\n}\n\n/** Pick the self-describing repair fields out of a raw track or rendition record. */\nexport function describeRepairTrack(name: string, path: string, fields: Record<string, unknown>): RepairTrackDescriptor {\n\tconst descriptor: RepairTrackDescriptor = { name, path };\n\tfor (const field of REPAIR_FIELDS) {\n\t\tif (field in fields) descriptor[field] = fields[field];\n\t}\n\treturn descriptor;\n}\n\nfunction isSafeInteger(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isSafeInteger(value);\n}\n\nfunction positiveInteger(value: unknown): value is number {\n\treturn isSafeInteger(value) && value > 0;\n}\n\n/** The single source a repair track names in `depends`, once its shape has been validated. */\nexport function dependsTarget(descriptor: RepairTrackDescriptor): string | undefined {\n\tconst depends = descriptor.depends;\n\treturn Array.isArray(depends) && depends.length === 1 && typeof depends[0] === \"string\" && depends[0].length > 0\n\t\t? depends[0]\n\t\t: undefined;\n}\n\n/**\n * Validate the shape of one repair track's self-describing fields, independent\n * of any source. Cross-track rules (layer 0 agreement, contiguity, priority\n * order, bounds) live in `assembleRepairLayers`.\n */\nexport function validateRepairTrackFields(descriptor: RepairTrackDescriptor, fail: RepairContractFail): void {\n\tconst { path } = descriptor;\n\tif (descriptor.depends !== undefined && dependsTarget(descriptor) === undefined) {\n\t\tfail(\"track_repair_depends\", `${path}.depends`, \"must name exactly one source track\");\n\t}\n\tif (descriptor.repairLayer !== undefined && (!isSafeInteger(descriptor.repairLayer) || descriptor.repairLayer < 0)) {\n\t\tfail(\"track_repair_layer\", `${path}.repairLayer`, \"must be a non-negative integer\");\n\t}\n\tif (\n\t\tdescriptor.repairSymbols !== undefined &&\n\t\t(!positiveInteger(descriptor.repairSymbols) || descriptor.repairSymbols > MAX_REPAIR_SYMBOLS)\n\t) {\n\t\tfail(\"track_repair_layer\", `${path}.repairSymbols`, `must be a positive integer at most ${MAX_REPAIR_SYMBOLS}`);\n\t}\n\tif (\n\t\tdescriptor.priority !== undefined &&\n\t\t(!isSafeInteger(descriptor.priority) ||\n\t\t\tdescriptor.priority < REPAIR_PRIORITY_MIN ||\n\t\t\tdescriptor.priority > REPAIR_PRIORITY_MAX)\n\t) {\n\t\tfail(\n\t\t\t\"track_repair_priority\",\n\t\t\t`${path}.priority`,\n\t\t\t`must be an integer from ${REPAIR_PRIORITY_MIN} to ${REPAIR_PRIORITY_MAX} (MoQT scale, lower first)`,\n\t\t);\n\t}\n\tif (descriptor.scope !== undefined) {\n\t\tif (descriptor.scope !== \"keyframe\") {\n\t\t\tfail(\"track_repair_overlay\", `${path}.scope`, 'must be \"keyframe\"');\n\t\t}\n\t\tif (descriptor.repairLayer !== undefined) {\n\t\t\tfail(\"track_repair_overlay\", `${path}.repairLayer`, \"an overlay is a separate FEC instance and carries no repairLayer\");\n\t\t}\n\t\tfor (const field of [\"depends\", \"sourceSymbols\", \"repairSymbols\", \"priority\"] as const) {\n\t\t\tif (descriptor[field] === undefined) {\n\t\t\t\tfail(\"track_repair_overlay\", `${path}.${field}`, `an overlay must declare ${field}`);\n\t\t\t}\n\t\t}\n\t}\n\tif (descriptor.sourceSymbols !== undefined) {\n\t\tif (descriptor.scope === undefined) {\n\t\t\tfail(\"track_repair_overlay\", `${path}.sourceSymbols`, \"only an overlay (scope: keyframe) declares its own sourceSymbols\");\n\t\t}\n\t\tif (!positiveInteger(descriptor.sourceSymbols) || descriptor.sourceSymbols > MAX_RAPTORQ_SOURCE_SYMBOLS) {\n\t\t\tfail(\n\t\t\t\t\"track_fec_bounds\",\n\t\t\t\t`${path}.sourceSymbols`,\n\t\t\t\t`must be a positive integer at most ${MAX_RAPTORQ_SOURCE_SYMBOLS} (RFC 6330 source block limit)`,\n\t\t\t);\n\t\t}\n\t}\n\tif (isSafeInteger(descriptor.repairLayer) && descriptor.repairLayer >= 1) {\n\t\tfor (const field of [\"depends\", \"repairSymbols\", \"priority\"] as const) {\n\t\t\tif (descriptor[field] === undefined) {\n\t\t\t\tfail(\"track_repair_layer\", `${path}.${field}`, `repair layer ${descriptor.repairLayer} must declare ${field}`);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Resolve the base layers and overlays of one source track and derive each\n * base layer's RS_ID offset. Fails closed on every \u00A76.3 rule violation through\n * `fail`, which receives the code and the JSON path of the offending field.\n *\n * `descriptors` are every repair track (or repair rendition) in the catalog,\n * already shape-checked by `validateRepairTrackFields`. Returns undefined when\n * the source names no layer 0 and nothing depends on it.\n */\nexport type AssembleRepairLayersOptions = {\n\t/**\n\t * MSF: `fec.repairTrack` must name one of `descriptors` (a `fec-repair`\n\t * track), so a dangling pointer fails closed with `track_repair_track`.\n\t * hang: layer 0 lives outside `repair.renditions`, so leave this unset.\n\t */\n\tlayerZeroMustBeDeclared?: boolean;\n};\n\nexport function assembleRepairLayers(\n\tsource: RepairSourceGeometry,\n\tdescriptors: readonly RepairTrackDescriptor[],\n\tfail: RepairContractFail = throwRepairContractError,\n\toptions: AssembleRepairLayersOptions = {},\n): RepairLayersResolution | undefined {\n\tconst layerZeroName =\n\t\ttypeof source.repairTrack === \"string\" && source.repairTrack.length > 0 ? source.repairTrack : undefined;\n\tconst layerZero = layerZeroName === undefined ? undefined : descriptors.find((d) => d.name === layerZeroName);\n\tif (layerZeroName !== undefined && layerZero === undefined && options.layerZeroMustBeDeclared) {\n\t\tfail(\"track_repair_track\", `${source.path}.repairTrack`, `unknown FEC repair track ${JSON.stringify(layerZeroName)}`);\n\t}\n\tconst dependents = descriptors.filter((d) => d.name !== layerZeroName && dependsTarget(d) === source.name);\n\tconst k = positiveInteger(source.sourceSymbols) ? source.sourceSymbols : 0;\n\t// RFC 6330 bounds every RaptorQ source block, with or without a repair track.\n\tif (source.algorithm === \"raptorq\" && k > MAX_RAPTORQ_SOURCE_SYMBOLS) {\n\t\tfail(\n\t\t\t\"track_fec_bounds\",\n\t\t\t`${source.path}.sourceSymbols`,\n\t\t\t`must be at most ${MAX_RAPTORQ_SOURCE_SYMBOLS} (RFC 6330 source block limit)`,\n\t\t);\n\t}\n\tif (layerZeroName === undefined) {\n\t\tif (dependents.length === 0) return undefined;\n\t\tfail(\n\t\t\t\"track_repair_layer\",\n\t\t\t`${dependents[0]!.path}.depends`,\n\t\t\t`repair layers of ${JSON.stringify(source.name)} require a layer 0 (fec.repairTrack) on the source`,\n\t\t);\n\t}\n\n\tconst layered = dependents.length > 0;\n\tlet layerZeroSymbols = 0;\n\tif (positiveInteger(source.repairSymbols)) {\n\t\tlayerZeroSymbols = source.repairSymbols;\n\t} else if (layered) {\n\t\tfail(\"track_fec\", `${source.path}.repairSymbols`, \"layered or overlay repair needs a positive integer repairSymbols on layer 0\");\n\t}\n\tif (layered && !positiveInteger(source.sourceSymbols)) {\n\t\tfail(\"track_fec\", `${source.path}.sourceSymbols`, \"layered or overlay repair needs a positive integer sourceSymbols on the source\");\n\t}\n\tif (layered && source.algorithm !== \"raptorq\") {\n\t\tfail(\n\t\t\t\"track_fec\",\n\t\t\t`${source.path}.algorithm`,\n\t\t\t\"repair layers and overlays are defined only for raptorq: a block code fixes its codeword length, so symbols of a larger P are not symbols of the code a receiver holding fewer layers decodes\",\n\t\t);\n\t}\n\n\tlet layerZeroPriority = DEFAULT_REPAIR_PRIORITY;\n\tif (layerZero !== undefined) {\n\t\tif (layerZero.depends !== undefined && dependsTarget(layerZero) !== source.name) {\n\t\t\tfail(\n\t\t\t\t\"track_repair_depends\",\n\t\t\t\t`${layerZero.path}.depends`,\n\t\t\t\t`layer 0 of ${JSON.stringify(source.name)} must depend on that source`,\n\t\t\t);\n\t\t}\n\t\tif (layerZero.repairLayer !== undefined && layerZero.repairLayer !== 0) {\n\t\t\tfail(\"track_repair_layer\", `${layerZero.path}.repairLayer`, \"the track named by fec.repairTrack is layer 0\");\n\t\t}\n\t\tif (layerZero.scope !== undefined) {\n\t\t\tfail(\"track_repair_overlay\", `${layerZero.path}.scope`, \"the track named by fec.repairTrack is base layer 0, not an overlay\");\n\t\t}\n\t\tif (layerZero.repairSymbols !== undefined && layerZero.repairSymbols !== layerZeroSymbols) {\n\t\t\tfail(\n\t\t\t\t\"track_repair_layer\",\n\t\t\t\t`${layerZero.path}.repairSymbols`,\n\t\t\t\t`must agree with the source's fec.repairSymbols (${layerZeroSymbols})`,\n\t\t\t);\n\t\t}\n\t\tif (typeof layerZero.priority === \"number\") layerZeroPriority = layerZero.priority;\n\t}\n\n\tconst baseLayers: RepairLayerParams[] = [\n\t\t{ layer: 0, repairTrackName: layerZeroName!, repairSymbols: layerZeroSymbols, rsIdOffset: 0, priority: layerZeroPriority },\n\t];\n\t// Clarification 7 fixes the wire: a RAP packet carries exactly two Source\n\t// FEC Payload IDs, base first and overlay last, and the receiver strips a\n\t// fixed trailer length. A second overlay would need a third trailer that\n\t// the draft does not define, so one overlay per source is the contract.\n\tconst overlays: RepairOverlayParams[] = [];\n\tfor (const d of dependents) {\n\t\tif (d.scope === undefined && d.repairLayer === undefined) {\n\t\t\tfail(\n\t\t\t\t\"track_repair_layer\",\n\t\t\t\t`${d.path}.repairLayer`,\n\t\t\t\t`a repair track that depends on ${JSON.stringify(source.name)} must declare repairLayer (1..n) or scope: \"keyframe\"`,\n\t\t\t);\n\t\t}\n\t}\n\tconst layerTracks = dependents\n\t\t.filter((d) => d.scope === undefined)\n\t\t.sort((a, b) => (a.repairLayer as number) - (b.repairLayer as number));\n\tfor (const d of dependents) {\n\t\tif (d.scope !== \"keyframe\") continue;\n\t\tif (overlays.length === 1) {\n\t\t\tfail(\n\t\t\t\t\"track_repair_overlay\",\n\t\t\t\t`${d.path}.scope`,\n\t\t\t\t`${JSON.stringify(source.name)} already has keyframe overlay ${JSON.stringify(overlays[0]!.repairTrackName)}: a RAP packet carries exactly two Source FEC Payload IDs (base, then overlay), so one overlay per source`,\n\t\t\t);\n\t\t}\n\t\tconst sourceSymbols = d.sourceSymbols as number;\n\t\tconst repairSymbols = d.repairSymbols as number;\n\t\tif (sourceSymbols + repairSymbols - 1 > MAX_ENCODING_SYMBOL_ID) {\n\t\t\tfail(\n\t\t\t\t\"track_fec_bounds\",\n\t\t\t\t`${d.path}.repairSymbols`,\n\t\t\t\t`overlay sourceSymbols + repairSymbols - 1 exceeds the 24-bit ESI space (${MAX_ENCODING_SYMBOL_ID})`,\n\t\t\t);\n\t\t}\n\t\toverlays.push({ repairTrackName: d.name, scope: \"keyframe\", sourceSymbols, repairSymbols, priority: d.priority as number });\n\t}\n\n\tlet offset = layerZeroSymbols;\n\tlet previousPriority = layerZeroPriority;\n\tlayerTracks.forEach((d, i) => {\n\t\tconst expected = i + 1;\n\t\tif (d.repairLayer === 0) {\n\t\t\tfail(\"track_repair_layer\", `${d.path}.repairLayer`, `layer 0 of ${JSON.stringify(source.name)} is ${JSON.stringify(layerZeroName)}, named by fec.repairTrack`);\n\t\t}\n\t\tif (d.repairLayer !== expected) {\n\t\t\tfail(\n\t\t\t\t\"track_repair_layer\",\n\t\t\t\t`${d.path}.repairLayer`,\n\t\t\t\ti > 0 && d.repairLayer === layerTracks[i - 1]!.repairLayer\n\t\t\t\t\t? `repair layer ${d.repairLayer} is declared twice for ${JSON.stringify(source.name)}`\n\t\t\t\t\t: `repair layers must be contiguous from 1: expected ${expected}, found ${d.repairLayer}`,\n\t\t\t);\n\t\t}\n\t\tconst priority = d.priority as number;\n\t\tif (priority < previousPriority) {\n\t\t\tfail(\n\t\t\t\t\"track_repair_priority\",\n\t\t\t\t`${d.path}.priority`,\n\t\t\t\t`must be at least ${previousPriority}, the priority of layer ${expected - 1}: priority is monotonic non-decreasing with layer index`,\n\t\t\t);\n\t\t}\n\t\tpreviousPriority = priority;\n\t\tconst repairSymbols = d.repairSymbols as number;\n\t\tbaseLayers.push({ layer: expected, repairTrackName: d.name, repairSymbols, rsIdOffset: offset, priority });\n\t\toffset += repairSymbols;\n\t});\n\n\tconst totalBaseRepairSymbols = offset;\n\tif (totalBaseRepairSymbols > MAX_REPAIR_SYMBOLS) {\n\t\tfail(\n\t\t\t\"track_fec_bounds\",\n\t\t\t`${layerTracks.length === 0 ? source.path : layerTracks[layerTracks.length - 1]!.path}.repairSymbols`,\n\t\t\t`total repair symbols across base layers exceeds the 24-bit RS_ID space (${MAX_REPAIR_SYMBOLS})`,\n\t\t);\n\t}\n\tif (k + totalBaseRepairSymbols - 1 > MAX_ENCODING_SYMBOL_ID) {\n\t\tfail(\n\t\t\t\"track_fec_bounds\",\n\t\t\t`${source.path}.sourceSymbols`,\n\t\t\t`sourceSymbols + total base repair symbols - 1 exceeds the 24-bit ESI space (${MAX_ENCODING_SYMBOL_ID})`,\n\t\t);\n\t}\n\n\treturn { sourceTrackName: source.name, baseLayers, overlays, totalBaseRepairSymbols };\n}\n", "/**\n * CARP Catalog Types \u2014 FEC and Multicast catalog extensions.\n *\n * Per draft-law-ramadan-moq-carp-reliability \u00A73 (FEC) and \u00A74 (Multicast).\n * Plain TypeScript interfaces + type guards \u2014 no Zod dependency.\n *\n * These are the canonical type definitions. Upstream Zod schemas\n * should derive from these via z.infer<> to avoid duplication.\n */\n\nimport {\n\tMMTP_PACKET_ID_MAX,\n\tMMTP_SIGNALING_PACKET_ID,\n\tisFecBlockProfile,\n\ttype FecBlockProfile,\n} from \"@blockcast/mmt-base\";\nimport { resolveMulticastAuthCatalog, type MulticastAuthConfig } from \"./multicast-auth-catalog.js\";\nimport {\n\tassembleRepairLayers,\n\tassertRepairTrackReachable,\n\tdescribeRepairTrack,\n\trejectRepairFieldsOnMediaTrack,\n\trejectRepairTrackGeometry,\n\trepairReachability,\n\tdependsTarget,\n\treadFecBlockProfile,\n\tvalidateRepairTrackFields,\n} from \"./repair-layers.js\";\n\n// \u2500\u2500 Track Role (Phase 14, D-02..D-05) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Track role derived from the last slash-segment of the track name.\n *\n * Convention: `{content}/base`, `{content}/delta`, `{content}/repair`.\n * Single-track (no slash) legacy streams use `\"single\"`.\n *\n * D-01: No `trackRole` / `role` / `kind` field in the catalog.\n * D-02: Role is expressed via track name suffix only.\n * D-03: Unknown suffixes throw at parse time (error on bad init).\n * D-04: Legacy single-track streams (no slash suffix) return `\"single\"`.\n * D-05: Single role per track \u2014 no multi-role arrays.\n */\nexport type TrackRole = \"base\" | \"delta\" | \"repair\" | \"single\";\n\nconst VALID_ROLE_SUFFIXES = new Set<string>([\"base\", \"delta\", \"repair\"]);\n\n/**\n * Derive the role of a track from its name.\n *\n * - Names with no `/` \u2192 `\"single\"` (legacy single-track streams, D-04).\n * - Names ending with `/base`, `/delta`, or `/repair` \u2192 that role (D-02).\n * - Empty name \u2192 throws (error on bad init).\n * - Trailing slash (empty last segment) \u2192 throws.\n * - Non-ASCII or control characters anywhere in the name \u2192 throws (T-14-01 fuzz hardening).\n * - Unknown last segment \u2192 throws.\n *\n * @throws {Error} On empty name, non-ASCII/control chars, empty suffix, or unknown suffix.\n */\nexport function parseTrackRole(trackName: string): TrackRole {\n\tif (trackName === \"\") {\n\t\tthrow new Error(\"[carp-catalog] empty track name\");\n\t}\n\n\t// T-14-01 fuzz hardening: reject non-ASCII and control characters.\n\t// This prevents catalog injection attacks where a publisher sends a crafted\n\t// track name that bypasses role checks by embedding unusual code points.\n\tfor (let i = 0; i < trackName.length; i++) {\n\t\tconst cp = trackName.charCodeAt(i);\n\t\t// Reject control characters (0x00-0x1F, 0x7F) and non-ASCII (>0x7E)\n\t\tif (cp <= 0x1f || cp === 0x7f || cp > 0x7e) {\n\t\t\tthrow new Error(\n\t\t\t\t`[carp-catalog] track name contains non-ASCII or control character (code point 0x${cp.toString(16)}) in \"${trackName}\"`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst lastSlash = trackName.lastIndexOf(\"/\");\n\tif (lastSlash === -1) {\n\t\t// No slash \u2014 legacy single-track stream (D-04)\n\t\treturn \"single\";\n\t}\n\n\tconst suffix = trackName.slice(lastSlash + 1);\n\tif (suffix === \"\") {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] empty role suffix in track name \"${trackName}\"; track names must not end with \"/\"`,\n\t\t);\n\t}\n\n\tif (!VALID_ROLE_SUFFIXES.has(suffix)) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] unknown role suffix \"${suffix}\" in track name \"${trackName}\"; expected one of: base, delta, repair`,\n\t\t);\n\t}\n\n\treturn suffix as TrackRole;\n}\n\n/**\n * Non-throwing switching-set detector (BLO-8158).\n *\n * Returns `true` iff at least one track name carries a media role suffix\n * (`base` or `delta`) \u2014 i.e. the set looks like a base/delta/repair switching\n * set. A `/repair` track alone is ordinary FEC topology and is not sufficient\n * evidence of a switching set. Unlike `parseTrackRole`, this NEVER throws:\n * names without a `/`, and names with an unrecognised suffix (e.g. an ABR\n * resolution ladder like `video/1080p`, `video/720p`, `video/8k`), simply do\n * not count as a role.\n *\n * Use this for DETECTION (\"is this a switching set?\"). Once a switching set is\n * confirmed, `parseTrackRole` / `validateSwitchingSet` still strictly gate the\n * actual wiring. ABR ladders yield `false` here and fall through to the\n * ABR/single path instead of throwing.\n */\nexport function hasSwitchingSetRole(names: string[]): boolean {\n\treturn names.some((name) => {\n\t\tconst lastSlash = name.lastIndexOf(\"/\");\n\t\tif (lastSlash === -1) return false;\n\t\tconst suffix = name.slice(lastSlash + 1);\n\t\treturn suffix === \"base\" || suffix === \"delta\";\n\t});\n}\n\n/**\n * Validate that a set of track names forms a complete switching set.\n *\n * - Empty or single-track sets (all parsing to `\"single\"`) \u2192 `{ ok: true }`.\n * - Multi-track sets (any name has a role suffix) MUST include all three\n * roles (`base`, `delta`, `repair`). Missing roles are listed.\n *\n * Propagates throws from `parseTrackRole` (non-ASCII, unknown suffix, etc.).\n *\n * D-04: Legacy single-track streams pass without enforcement.\n * D-04: Once any suffix appears, the set is multi-track and must be complete.\n */\nexport function validateSwitchingSet(\n\ttrackNames: string[],\n): { ok: true } | { ok: false; missing: TrackRole[] } {\n\tif (trackNames.length === 0) return { ok: true };\n\n\tconst roles = trackNames.map(parseTrackRole);\n\n\t// If every role is \"single\", this is a legacy single-track stream \u2014 pass.\n\tif (roles.every((r) => r === \"single\")) {\n\t\treturn { ok: true };\n\t}\n\n\t// Multi-track: at least one name has a role suffix.\n\t// The set MUST contain all three required roles.\n\t// Explicit Set<TrackRole> so the TrackRole-typed `required.filter`\n\t// callback can call .has(r) without a narrow-mismatch complaint.\n\tconst presentRoles: Set<TrackRole> = new Set(roles.filter((r) => r !== \"single\"));\n\tconst required: TrackRole[] = [\"base\", \"delta\", \"repair\"];\n\tconst missing = required.filter((r) => !presentRoles.has(r));\n\n\tif (missing.length > 0) {\n\t\treturn { ok: false, missing };\n\t}\n\treturn { ok: true };\n}\n\n// \u2500\u2500 FEC Catalog Extension (CARP \u00A73.2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type FecAlgorithm = \"none\" | \"raptorq\" | \"reed-solomon\";\n\nexport interface FecCatalogConfig {\n\t/** FEC algorithm identifier (REQUIRED per \u00A74.1, values per \u00A73.1). */\n\talgorithm: FecAlgorithm;\n\t/** FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C). REQUIRED when\n\t * `algorithm` declares an FEC instance (anything but `\"none\"`); optional\n\t * on `\"none\"`, where a present token must still be known. Never defaulted\n\t * and never inferred from the wire: `k-padded-v1` (zero-padded K-symbol\n\t * blocks, SS_Start = SBN*K) and `iso-ssbg1-v1` (ssbg_mode1, K' =\n\t * SSB_length, consecutive SS_IDs) carry identical wire tuples whose repair\n\t * math differs. */\n\tblockProfile?: FecBlockProfile;\n\t/** K \u2014 source symbols per block (REQUIRED). */\n\tsourceSymbols: number;\n\t/** P \u2014 repair symbols per block (REQUIRED). */\n\trepairSymbols: number;\n\t/** T \u2014 bytes per symbol, 8-byte aligned (REQUIRED). */\n\tsymbolSize: number;\n\t/** FEC block span in MILLISECONDS (OPTIONAL, default 0 = single-group blocks).\n\t * D is the positive round-half-up value of interleaveDepthMs / GOP_duration_ms.\n\t * BLO-14526: renamed from the legacy `interleaveDepth` count-era key \u2014 no fallback read. */\n\tinterleaveDepthMs?: number;\n\t/** Declared jitter/reorder allowance in MILLISECONDS.\n\t * Added to interleaveDepthMs to form a block's deadline; never multiplied\n\t * by it. Jitter is a property of the network path and the block span is a\n\t * property of the encoder, so a multiplicative \"safety factor\" couples two\n\t * independent quantities and grants different slack per track for the same\n\t * network. Consumed identically by FECManager, FEC workers and player\n\t * policies so one signaled value governs end to end. */\n\treorderToleranceMs?: number;\n\t/** Track name for repair symbols (REQUIRED per \u00A76.1). Base layer 0 when the\n\t * source carries further repair layers or a keyframe overlay: those are\n\t * self-describing `fec-repair` tracks (`depends`, `repairLayer`, `scope`)\n\t * per draft-ramadan-moq-fec \u00A76.3, never listed here. */\n\trepairTrack: string;\n\t/** FEC block mode: \"interleaved\" (default) or \"object\" (per-frame). */\n\t// extensions feature \u2014 see moqcast-draft docs/fec-extensions-material.md, not in public -00\n\tmode?: \"interleaved\" | \"object\";\n\t/** UEP (Unequal Error Protection) \u2014 extensions feature, see moqcast-draft docs/fec-extensions-material.md, not in public -00. When present, repair count\n\t * varies per frame type. repairSymbols becomes the base (P-frame) count.\n\t * MUST be omitted on multi-track base/delta streams (D-08). */\n\tuep?: {\n\t\t/** Keyframe repair ratio: P = ceil(K * keyRatio). Default 0.5. */\n\t\tkeyRatio: number;\n\t\t/** Delta frame repair ratio: P = ceil(K * deltaRatio). Default 0.25. */\n\t\tdeltaRatio: number;\n\t};\n\t/**\n\t * Base-only fallback window in FEC blocks (D-11, Phase 14).\n\t *\n\t * When the delta track's FEC block failure rate exceeds the derived threshold\n\t * over this many consecutive blocks, MediaRouter falls back to base-only mode.\n\t * Derivation: framerate * fallback_window_seconds. If absent, construction\n\t * of the multi-track subscriber MUST throw (error on bad init).\n\t *\n\t * Present only on the base track's fec config (not the delta or repair track).\n\t */\n\tdeltaFailWindowBlocks?: number;\n\t/**\n\t * Delta re-subscribe probe interval in milliseconds (D-13, Phase 14).\n\t *\n\t * After a base-only fallback, wait this long before re-subscribing to the\n\t * delta track to check if conditions have improved.\n\t * Derivation: computeFecDeliveryWindowMs(...) \u2014 the single FEC recovery-window\n\t * source of truth (draft-ramadan-moq-fec \u00A79). Never a fixed interleave multiple.\n\t *\n\t * Present only on the base track's fec config (not the delta or repair track).\n\t */\n\tdeltaProbeIntervalMs?: number;\n\t/**\n\t * Jitter + one-way-delay + reorder budget in MILLISECONDS (draft \u00A79).\n\t *\n\t * An explicit ADDITIVE margin over the complete signaled FEC block span.\n\t * OPTIONAL: absence does not authorize a local default or a span multiplier.\n\t */\n\tjitterBufferMs?: number;\n}\n\nexport function isFecCatalogConfig(obj: unknown): obj is FecCatalogConfig {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\tconst algorithm = o[\"algorithm\"];\n\tconst symbolSize = o[\"symbolSize\"];\n\treturn (\n\t\ttypeof algorithm === \"string\" &&\n\t\t(algorithm === \"none\" || algorithm === \"raptorq\" || algorithm === \"reed-solomon\") &&\n\t\ttypeof o[\"sourceSymbols\"] === \"number\" && o[\"sourceSymbols\"] >= 1 &&\n\t\ttypeof o[\"repairSymbols\"] === \"number\" && o[\"repairSymbols\"] >= 1 &&\n\t\ttypeof symbolSize === \"number\" && symbolSize >= 1 &&\n\t\t(algorithm !== \"raptorq\" || symbolSize % 8 === 0) &&\n\t\ttypeof o[\"repairTrack\"] === \"string\" && (o[\"repairTrack\"] as string).length > 0 &&\n\t\t(isFecBlockProfile(o[\"blockProfile\"]) || (algorithm === \"none\" && o[\"blockProfile\"] === undefined))\n\t);\n}\n\n// \u2500\u2500 Multicast Catalog Extension (CARP \u00A74.1-4.4) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type MulticastProtocol = \"ssm\" | \"asm\" | \"amt\" | \"atsc3\";\n\n/**\n * Per-track endpoint membership, per draft-ramadan-moq-multicast-00 \u00A74.1.\n *\n * `packetId` is the MMTP packet_id used for packet-level demux on the\n * multicast wire, in 1..65535: packet_id 0 (`MMTP_SIGNALING_PACKET_ID`) is\n * the MMTP signaling flow and is never a track's. New emitters MUST use this\n * object form; the legacy\n * `string` form is accepted by parsers for one release for backward\n * compatibility (BLO-3323 migration).\n */\nexport interface MulticastTrackObject {\n\tname: string;\n\tpacketId: number;\n}\n\n/** Track entry in a multicast endpoint \u2014 accepts both legacy and v2 shape. */\nexport type MulticastTrack = string | MulticastTrackObject;\n\nexport interface MulticastEndpoint {\n\t/** Transport protocol. Defaults to SSM when sourceAddress is present. */\n\tprotocol?: MulticastProtocol;\n\t/** SSM source IP address (REQUIRED for SSM). */\n\tsourceAddress?: string;\n\t/** Multicast group address (REQUIRED). */\n\tgroupAddress: string;\n\t/** UDP port number (REQUIRED). */\n\tport: number;\n\t/**\n\t * Tracks available on this endpoint (REQUIRED).\n\t *\n\t * Accepts both legacy `string` (track name only) and v2 object form\n\t * (`{ name, packetId }`) for backward compatibility. New emitters\n\t * MUST use the object form (BLO-3323 / draft \u00A74.1). Consumers can\n\t * type-narrow with `typeof t === \"string\"` to handle both.\n\t */\n\ttracks: MulticastTrack[];\n\t/**\n\t * AMT relay hostname.\n\t *\n\t * @deprecated Superseded by catalog-level `multicast.networkSource`\n\t * (`{type: \"amt\", discovery, relay}`). Retained so a catalog published\n\t * before the migration still parses; emitters MUST NOT write it, and\n\t * `catalog.schema.json` already rejects it (`additionalProperties: false`\n\t * on the endpoint), as does moq-rs `MulticastEndpoint`\n\t * (`deny_unknown_fields`, and it carries no relay field at all).\n\t */\n\trelayAddress?: string;\n\t/**\n\t * Aggregate bandwidth of this endpoint in bits per second.\n\t * RECOMMENDED per draft \u00A74.1 \u2014 enables capacity-aware joins.\n\t */\n\tbandwidth?: number;\n}\n\nexport type AmtDiscovery = \"driad\" | \"manual\";\n\n/**\n * One `multicast.networkSource[]` entry, per draft-ramadan-moq-multicast \u00A74.1.\n *\n * Mirrors moq-rs `NetworkSource` (the normative validator) and the\n * `networkSource` `$def` in `catalog.schema.json` 1:1 \u2014 a change here needs the\n * same change in both. `discovery: \"manual\"` requires `relay`; that relay is\n * the one the player joins. Under `discovery: \"driad\"` the relay is resolved at\n * runtime, so `relay` is OPTIONAL and, when present, only a discovery hint\n * (e.g. the `amtFallback.relayDns` name the derived path carries over): readers\n * do not join it (`findAmtRelay` in packages/transport\n * multicast-catalog-policy.ts and `catalogAmtNetworkSource` in\n * catalog-network-source.ts both skip it), so a DRIAD entry never pins a relay.\n * `port` is OPTIONAL on the wire: the join boundary itself has no default, but\n * every reader resolves one before that boundary, so a manual relay without a\n * port joins on RFC 7450's 2268 rather than failing (BLO-38176).\n *\n * Not to be confused with `CatalogNetworkSource` in `catalog-network-source.ts`,\n * which is also exported from the package root and also describes\n * `$defs/networkSource`. The split is deliberate and the two must NOT be\n * unified: this type is the strict EMITTER/wire shape, so tightening it is what\n * keeps published catalogs normative. That one is the tolerant READER \u2014 every\n * field optional, and it also accepts the bare-object form that hang's zod\n * RootSchema and older hand-authored catalogs emit. Tightening the reader would\n * reject catalogs already in the wild, so when this type changes, widen the\n * reader only if the new shape must be consumed; never narrow it to match.\n *\n * This is the one place a catalog declares an AMT relay. It is catalog-level,\n * not per-endpoint: the normative endpoint model carries no relay field.\n */\nexport type NetworkSource =\n\t| { type: \"amt\"; discovery: AmtDiscovery; relay?: string; port?: number }\n\t| { type: \"atsc3\"; frequency: number; plpId: number; serviceId: number; slsUri: string };\n\nexport interface MulticastCatalogConfig {\n\t/** Enable/disable multicast (OPTIONAL, default true). */\n\tenabled?: boolean;\n\t/** Multicast endpoints (REQUIRED). */\n\tendpoints: MulticastEndpoint[];\n\t/**\n\t * Relay/tuning discovery for these endpoints. Required whenever an endpoint\n\t * declares a bearer protocol (`amt`, `atsc3`), which must name its own entry\n\t * type here; omitted for native joins (`ssm`, `asm`), whose endpoint tuple is\n\t * the whole join.\n\t */\n\tnetworkSource?: NetworkSource[];\n\t/**\n\t * AMT fallback configuration.\n\t *\n\t * @deprecated Superseded by `networkSource` \u2014 `{discovery, relayDns}` maps\n\t * onto `{type: \"amt\", discovery, relay}`. `multicastFromConfig` lifts it and\n\t * no longer emits it; the schema rejects it.\n\t */\n\tamtFallback?: {\n\t\tdiscovery: AmtDiscovery;\n\t\trelayDns?: string;\n\t};\n\t/**\n\t * Monotonic catalog version. Publishers MUST bump on any\n\t * `endpoints[]` mutation (BLO-3323 catalog freshness model).\n\t */\n\tversion?: number;\n\t/**\n\t * Unix-ms timestamp after which subscribers SHOULD refetch the\n\t * catalog. Refetch-on-multicast-receive-timeout short-circuits this.\n\t */\n\tvalidUntil?: number;\n}\n\nexport function isMulticastTrackObject(obj: unknown): obj is MulticastTrackObject {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\treturn typeof o[\"name\"] === \"string\" && typeof o[\"packetId\"] === \"number\";\n}\n\n/**\n * Wire-shape predicate: returns `true` when `obj` matches the\n * MulticastEndpoint JSON shape strictly enough to be safely cast.\n *\n * Intentionally less strict than `multicastFromConfig`'s\n * `validateEndpoint` (in `multicast-input.ts`):\n *\n * - `tracks: []` is accepted here (Array.isArray check only) but\n * rejected by `validateEndpoint` as `empty_tracks`.\n * - SSM-specific group-range and source-required checks are not\n * enforced here.\n * - Bandwidth/AmtFallback/version are not type-checked.\n *\n * That asymmetry is deliberate: this predicate guards JSON parsed off\n * the wire (already-published catalogs that downstream may have\n * trimmed), while `validateEndpoint` guards publisher input *before*\n * a catalog is composed. Subscribers are tolerant; publishers are\n * strict \u2014 the catalog format never re-validates upstream invariants.\n */\nexport function isMulticastEndpoint(obj: unknown): obj is MulticastEndpoint {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\tconst protocol = o[\"protocol\"] ??\n\t\t(typeof o[\"sourceAddress\"] === \"string\" && o[\"sourceAddress\"].length > 0 ? \"ssm\" : undefined);\n\treturn (\n\t\ttypeof protocol === \"string\" &&\n\t\t(protocol === \"ssm\" || protocol === \"asm\" || protocol === \"amt\" || protocol === \"atsc3\") &&\n\t\ttypeof o[\"groupAddress\"] === \"string\" && (o[\"groupAddress\"] as string).length > 0 &&\n\t\ttypeof o[\"port\"] === \"number\" && (o[\"port\"] as number) > 0 && (o[\"port\"] as number) <= 65535 &&\n\t\tArray.isArray(o[\"tracks\"]) &&\n\t\t(o[\"tracks\"] as unknown[]).every((t: unknown) => typeof t === \"string\" || isMulticastTrackObject(t))\n\t);\n}\n\nexport function isMulticastCatalogConfig(obj: unknown): obj is MulticastCatalogConfig {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\treturn (\n\t\tArray.isArray(o[\"endpoints\"]) &&\n\t\t(o[\"endpoints\"] as unknown[]).every((ep: unknown) => isMulticastEndpoint(ep)) &&\n\t\t// Shape only, not contents \u2014 entry validation is `multicastFromConfig`'s.\n\t\t// `networkSource` now carries the relay, so a non-array here must not\n\t\t// narrow clean: every reader of it indexes it.\n\t\t//\n\t\t// `=== undefined`, not `!(\"networkSource\" in o)`: `multicastFromConfig`\n\t\t// tests definedness, and the two must agree on what \"set\" means. A key\n\t\t// present with value `undefined` is a legal `MulticastCatalogConfig`\n\t\t// (`strict` without `exactOptionalPropertyTypes`) and is what a spread\n\t\t// of an optional field produces \u2014 `in` would narrow that valid config\n\t\t// to `false` and a consumer gating on this guard would discard a\n\t\t// working multicast block.\n\t\t(o[\"networkSource\"] === undefined || Array.isArray(o[\"networkSource\"]))\n\t);\n}\n\n// \u2500\u2500 Track Timing (mmt-00 \u00A74.4.4) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Per-track group-duration signaling.\n *\n * The catalog publishes either `groupDurationMs` (integer ms) or the exact\n * `groupDurationTicks` value used by `groupIdForTicks` in align.ts. The tick\n * form is required for cases where milliseconds cannot represent the cadence\n * exactly:\n *\n * - Per-frame group durations at non-integer-ms cadences\n * (1/30 s = 33.333\u2026 ms; 1/60 s = 16.666\u2026 ms).\n * - Audio sample-rate timescales not divisible by 1000 Hz\n * (e.g. 44 100 Hz \u00D7 333 ms = 14 685.3 ticks).\n *\n * If both fields are present, `groupDurationTicks` wins (matches the\n * spec, mmt-00 \u00A74.4.4).\n *\n * @see draft-ramadan-moq-mmt-00 \u00A74.4.4\n * @see ./align.ts groupIdForTicks\n */\nexport interface TrackTimingConfig {\n\t/** Group duration in milliseconds (positive integer). Required unless an\n\t * exact groupDurationTicks value is supplied. */\n\tgroupDurationMs?: number;\n\t/** Integer-tick override (OPTIONAL, positive integer). Wins over the\n\t * ms field when both are present. Required when\n\t * `(groupDurationMs * timescale) % 1000 != 0`. */\n\tgroupDurationTicks?: number;\n}\n\nexport function isTrackTimingConfig(obj: unknown): obj is TrackTimingConfig {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\tconst ms = o[\"groupDurationMs\"];\n\tconst ticks = o[\"groupDurationTicks\"];\n\tif (ms === undefined && ticks === undefined) return false;\n\tif (ms !== undefined && (typeof ms !== \"number\" || !Number.isInteger(ms) || ms <= 0)) return false;\n\tif (\"groupDurationTicks\" in o) {\n\t\tif (ticks === undefined) return ms !== undefined;\n\t\tif (typeof ticks !== \"number\" || !Number.isInteger(ticks) || ticks <= 0) return false;\n\t}\n\treturn true;\n}\n\n/** Resolve catalog group timing to an operational wall-clock duration.\n *\n * `groupDurationTicks` wins when both wire fields are present, matching\n * mmt-00 \u00A74.4.4. The result may be fractional because exact frame cadences\n * such as 33_333 ticks at 1 MHz cannot be represented by the wire catalog's\n * integer `groupDurationMs` field.\n */\nexport function resolveGroupDurationMs(\n\ttiming: TrackTimingConfig,\n\ttimescale: number,\n): number {\n\tif (!Number.isSafeInteger(timescale) || timescale <= 0) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationMs: timescale must be a positive safe integer Hz, got ${timescale}`,\n\t\t);\n\t}\n\tif (!isTrackTimingConfig(timing)) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationMs: invalid TrackTimingConfig: ${JSON.stringify(timing)}`,\n\t\t);\n\t}\n\tif (timing.groupDurationTicks === undefined) {\n\t\treturn timing.groupDurationMs!;\n\t}\n\n\tconst durationMs = (timing.groupDurationTicks / timescale) * 1000;\n\tif (!Number.isFinite(durationMs) || durationMs <= 0 || durationMs > Number.MAX_SAFE_INTEGER) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationMs: groupDurationTicks=${timing.groupDurationTicks} ` +\n\t\t\t\t`at timescale=${timescale} cannot be represented as a positive safe millisecond duration`,\n\t\t);\n\t}\n\treturn durationMs;\n}\n\n/**\n * Optional unicast archive tier (BLO-41290).\n *\n * `origin` is scheme-and-authority only. Object paths are never advertised:\n * a consumer derives them via the v1 ContentId profile \u2014\n * `/v1/{ns}/{track}/{group}[/{object}]` under this origin\n * (`mmt-archive/src/content_id.rs`). Advertising the pair a second time would\n * be a second source of truth that can disagree with `tracks[]`.\n *\n * `{track}` is the track's `name`. `{ns}` is the track's own `namespace` when\n * it declares one, and otherwise the MoQ subscription's Track Namespace tuple,\n * of which `ContentId::from_moq` admits exactly one element. The catalog does\n * not carry that tuple, so for a track omitting `namespace` the derivation\n * needs the subscription context \u2014 the catalog alone is not sufficient. That\n * inheritance is the load-bearing half of the contract: `namespace` is\n * optional in the schema, so a catalog can advertise an archive whose object\n * paths only resolve in a subscribing consumer.\n *\n * One origin at the top level, not one per track: the `(namespace, track)` key\n * lives in the derived path rather than in the origin, so a single origin\n * already disambiguates every track however many namespaces the catalog spans.\n * A per-track origin would only buy splitting one catalog across several\n * archives, which nothing asks for.\n *\n * Absence means \"no archive advertised\". It is never a default URL.\n */\nexport interface ArchiveConfig {\n\torigin: string;\n}\n\n/**\n * The advertised archive origin, or `undefined` when none is advertised.\n *\n * Takes a catalog already through `validateCmsfMmtpCatalog`, so the returned\n * string is canonical and safe to use as a base \u2014 no string surgery on a\n * config blob, and no second place that decides what an origin may look like.\n */\nexport function archiveOrigin(catalog: CmsfCatalog): string | undefined {\n\treturn catalog.archive?.origin;\n}\n\n/**\n * Pin the archive origin to exactly one spelling per archive.\n *\n * The ContentId codec rejects percent-encoding so every identity has exactly\n * one URL path; an advertised base must not reintroduce aliasing above it.\n * `value === new URL(value).origin` is most of the rule \u2014 `URL.origin` drops\n * path, query, fragment and userinfo, lowercases scheme and host, strips the\n * default port and any trailing slash, and normalizes percent-escapes. So a\n * separate percent-encoding check would be redundant, not missing: `%6d` in a\n * host decodes and fails the comparison. `https:` is checked separately\n * because `http://host` is already its own origin and passes the comparison.\n *\n * The comparison alone admits one alias, so the trailing dot is rejected\n * explicitly: `https://host.` is its own `URL.origin` verbatim, while `host.`\n * and `host` are the same DNS name. That aliases the advertised *base*, not\n * the codec input \u2014 the origin contributes no path segments, so a trailing\n * dot never reaches `ContentId::parse_path` \u2014 but it still gives one archive\n * two cache keys, and some TLS stacks treat `host.` as a distinct SNI/CORS\n * origin.\n *\n * `additionalProperties: false` on `archive` is enforced here as well as in\n * the schema. The runtime validator is the gate `archiveOrigin` documents\n * itself against, so a second spelling of the origin smuggled in under\n * another key must be rejected by both, not just by AJV.\n */\nfunction validateArchive(catalog: Record<string, unknown>): void {\n\tconst archive = catalog[\"archive\"];\n\tif (archive === undefined) return;\n\tif (typeof archive !== \"object\" || archive === null || Array.isArray(archive)) {\n\t\tcatalogError(\"archive_object\", \"$.archive\", \"archive must be an object\");\n\t}\n\tconst unknown = Object.keys(archive as Record<string, unknown>).filter(key => key !== \"origin\");\n\tif (unknown.length > 0) {\n\t\tcatalogError(\"archive_object\", \"$.archive\", `archive has unknown field(s) ${unknown.map(key => JSON.stringify(key)).join(\", \")}; only \"origin\" is allowed`);\n\t}\n\tconst origin = (archive as Record<string, unknown>)[\"origin\"];\n\t// Absent and present-but-wrong-type are different mistakes and the schema\n\t// already distinguishes them (`required` vs `type`). Collapsing both into\n\t// \"must be a string\" tells someone hand-writing a catalog to fix a field\n\t// they never wrote. Same wording convention as `codec is required`.\n\t//\n\t// This is the one archive rule where the two gates report different fields,\n\t// deliberately: AJV raises `required` at the parent, `/archive` (its\n\t// placement, not a choice \u2014 same shape as `fec-block-profile-missing`),\n\t// while this raises at `$.archive.origin`. Pointing at the field someone has\n\t// to add is the more useful of the two, so the \"same field\" convention\n\t// documented above bends here rather than the message getting vaguer.\n\tif (origin === undefined) {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", \"origin is required when an archive block is present\");\n\t}\n\tif (typeof origin !== \"string\") {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", \"origin must be a string\");\n\t}\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(origin);\n\t} catch {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", `origin must be an absolute URL, got ${JSON.stringify(origin)}`);\n\t}\n\tif (parsed.protocol !== \"https:\") {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", `origin must use https, got ${JSON.stringify(parsed.protocol)}`);\n\t}\n\tif (origin !== parsed.origin) {\n\t\tcatalogError(\n\t\t\t\"archive_origin\",\n\t\t\t\"$.archive.origin\",\n\t\t\t`origin must be scheme and authority in canonical form with no path, query, fragment, userinfo or trailing slash; expected ${JSON.stringify(parsed.origin)}, got ${JSON.stringify(origin)}`,\n\t\t);\n\t}\n\tif (parsed.hostname.endsWith(\".\")) {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", `origin host must not end in a trailing dot; ${JSON.stringify(parsed.hostname)} and ${JSON.stringify(parsed.hostname.slice(0, -1))} are the same host`);\n\t}\n}\n\nexport type CmsfCatalog = {\n\tversion: number;\n\ttracks: Record<string, unknown>[];\n\tmulticast?: Record<string, unknown>;\n\t\"multicast.auth\"?: MulticastAuthConfig;\n\tarchive?: ArchiveConfig;\n\t[key: string]: unknown;\n};\n\nexport type CmsfCatalogErrorCode =\n\t| \"catalog_object\"\n\t| \"catalog_version\"\n\t| \"catalog_tracks\"\n\t| \"catalog_common_track_fields\"\n\t| \"track_object\"\n\t| \"track_name\"\n\t| \"track_packaging\"\n\t| \"track_selection_params\"\n\t| \"track_is_live\"\n\t| \"track_codec\"\n\t| \"track_mmtp_mode\"\n\t| \"track_timescale\"\n\t| \"track_group_duration_ms\"\n\t| \"track_group_duration_ticks\"\n\t| \"track_group_duration_exact\"\n\t| \"track_keyframe_interval\"\n\t| \"track_fec\"\n\t| \"track_fec_bounds\"\n\t| \"track_fec_block_profile\"\n\t| \"track_repair_track\"\n\t| \"track_repair_depends\"\n\t| \"track_repair_layer\"\n\t| \"track_repair_priority\"\n\t| \"track_repair_overlay\"\n\t| \"track_repair_geometry\"\n\t| \"track_switching_set\"\n\t| \"multicast_auth\"\n\t| \"multicast_track\"\n\t| \"multicast_packet_id\"\n\t| \"archive_object\"\n\t| \"archive_origin\";\n\nexport class CmsfCatalogValidationError extends Error {\n\tconstructor(\n\t\tpublic readonly code: CmsfCatalogErrorCode,\n\t\tpublic readonly path: string,\n\t\tmessage: string,\n\t) {\n\t\tsuper(`[cmsf:${code}] ${path}: ${message}`);\n\t\tthis.name = \"CmsfCatalogValidationError\";\n\t}\n}\n\nfunction catalogError(code: CmsfCatalogErrorCode, path: string, message: string): never {\n\tthrow new CmsfCatalogValidationError(code, path, message);\n}\n\n/** Validate the common CMSF shape and the MMTP required-field profile. */\nexport function validateCmsfMmtpCatalog(value: unknown): CmsfCatalog {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tcatalogError(\"catalog_object\", \"$\", \"catalog must be an object\");\n\t}\n\tconst catalog = value as Record<string, unknown>;\n\t// IETF-draft catalogs (cast `ladder_catalog.rs`, moq-server\n\t// `catalog-synthesizer.ts`, consumed by the hang player) carry a string\n\t// version tag such as \"draft-ietf-moq-catalogformat-01\" instead of the CMSF\n\t// `version: 1` integer. Nothing keys behavior off the catalog version, so\n\t// normalize the tag to a number rather than rejecting the whole catalog \u2014\n\t// mirrors the relay (dual-stack-relay `bridge/catalog.rs`\n\t// `deserialize_optional_u32_lenient`, #825) and moqtail-ts\n\t// `normalizeCatalogVersion`.\n\tconst rawVersion = catalog[\"version\"];\n\tconst normalizedVersion =\n\t\ttypeof rawVersion === \"string\"\n\t\t\t? Number.isFinite(Number(rawVersion))\n\t\t\t\t? Number(rawVersion)\n\t\t\t\t: rawVersion.startsWith(\"draft-ietf-moq-catalogformat\")\n\t\t\t\t\t? 1\n\t\t\t\t\t: NaN\n\t\t\t: rawVersion;\n\tif (!Number.isInteger(normalizedVersion) || (normalizedVersion as number) <= 0) {\n\t\tcatalogError(\"catalog_version\", \"$.version\", \"version must be a positive integer or a recognized format tag\");\n\t}\n\tcatalog[\"version\"] = normalizedVersion;\n\tif (!Array.isArray(catalog[\"tracks\"])) {\n\t\tcatalogError(\"catalog_tracks\", \"$.tracks\", \"tracks must be an array\");\n\t}\n\n\tif (\"commonTrackFields\" in catalog) {\n\t\tcatalogError(\n\t\t\t\"catalog_common_track_fields\",\n\t\t\t\"$.commonTrackFields\",\n\t\t\t\"commonTrackFields is retired; every track carries its own fields\",\n\t\t);\n\t}\n\tconst tracks = catalog[\"tracks\"] as unknown[];\n\tconst trackRecords: Record<string, unknown>[] = [];\n\tconst positiveInteger = (input: unknown): input is number => Number.isSafeInteger(input) && (input as number) > 0;\n\tconst positiveNumber = (input: unknown): input is number =>\n\t\ttypeof input === \"number\" && Number.isFinite(input) && input > 0;\n\n\tfor (let index = 0; index < tracks.length; index++) {\n\t\tconst raw = tracks[index];\n\t\tconst path = `$.tracks[${index}]`;\n\t\tif (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n\t\t\tcatalogError(\"track_object\", path, \"track must be an object\");\n\t\t}\n\t\tconst track = raw as Record<string, unknown>;\n\t\tconst name = track[\"name\"];\n\t\tif (typeof name !== \"string\" || name.length === 0) {\n\t\t\tcatalogError(\"track_name\", `${path}.name`, \"name must be a non-empty string\");\n\t\t}\n\t\ttrackRecords.push(track);\n\n\t\tconst packaging = track[\"packaging\"];\n\t\tif (typeof packaging !== \"string\" || packaging.length === 0) {\n\t\t\tcatalogError(\"track_packaging\", `${path}.packaging`, \"packaging must be a non-empty string\");\n\t\t}\n\t\tconst hasIsLive = Object.prototype.hasOwnProperty.call(track, \"isLive\");\n\t\tconst isLive = track[\"isLive\"];\n\t\tif (hasIsLive && typeof isLive !== \"boolean\") {\n\t\t\tcatalogError(\"track_is_live\", `${path}.isLive`, \"isLive must be a boolean\");\n\t\t}\n\t\ttrack[\"isLive\"] = isLive ?? true;\n\t\tif (\"selectionParams\" in track) {\n\t\t\tcatalogError(\n\t\t\t\t\"track_selection_params\",\n\t\t\t\t`${path}.selectionParams`,\n\t\t\t\t\"selectionParams is retired; codec and the other selection fields are flat on the track\",\n\t\t\t);\n\t\t}\n\t\tconst codec = track[\"codec\"];\n\t\tif (packaging !== \"timeline\" && packaging !== \"mediatimeline\" && packaging !== \"eventtimeline\" && packaging !== \"fec-repair\" &&\n\t\t\t(typeof codec !== \"string\" || codec.length === 0)) {\n\t\t\tcatalogError(\"track_codec\", `${path}.codec`, `codec is required for packaging=${packaging}`);\n\t\t}\n\n\t\tconst timescale = track[\"timescale\"];\n\t\tconst groupDurationMs = track[\"groupDurationMs\"];\n\t\tconst groupDurationTicks = track[\"groupDurationTicks\"];\n\t\tconst keyframeIntervalMs = track[\"keyframeIntervalMs\"];\n\t\tconst keyframeIntervalTicks = track[\"keyframeIntervalTicks\"];\n\t\tif (groupDurationMs !== undefined && !positiveInteger(groupDurationMs)) {\n\t\t\tcatalogError(\"track_group_duration_ms\", `${path}.groupDurationMs`, \"must be a positive integer\");\n\t\t}\n\t\tif (groupDurationTicks !== undefined && !positiveInteger(groupDurationTicks)) {\n\t\t\tcatalogError(\"track_group_duration_ticks\", `${path}.groupDurationTicks`, \"must be a positive integer\");\n\t\t}\n\t\tif ((keyframeIntervalMs !== undefined && !positiveNumber(keyframeIntervalMs)) ||\n\t\t\t(keyframeIntervalTicks !== undefined && !positiveInteger(keyframeIntervalTicks))) {\n\t\t\tcatalogError(\"track_keyframe_interval\", path, \"keyframe interval fields must be positive numbers\");\n\t\t}\n\t\t// draft-ramadan-moq-fec \u00A76.3: repair tracks describe themselves and carry\n\t\t// no block geometry; media tracks never carry repair-track fields.\n\t\tif (packaging === \"fec-repair\") {\n\t\t\trejectRepairTrackGeometry(path, track, catalogError);\n\t\t\tvalidateRepairTrackFields(describeRepairTrack(name, path, track), catalogError);\n\t\t} else {\n\t\t\trejectRepairFieldsOnMediaTrack(path, track, catalogError);\n\t\t}\n\t\tif (packaging === \"mmtp\") {\n\t\t\tconst mmtpMode = track[\"mmtpMode\"];\n\t\t\tif (mmtpMode !== \"mpu\" && mmtpMode !== \"mfu\") {\n\t\t\t\tcatalogError(\"track_mmtp_mode\", `${path}.mmtpMode`, \"must be mpu or mfu\");\n\t\t\t}\n\t\t\tif (!positiveInteger(timescale)) {\n\t\t\t\tcatalogError(\"track_timescale\", `${path}.timescale`, \"must be a positive integer\");\n\t\t\t}\n\t\t\tif (groupDurationMs === undefined && groupDurationTicks === undefined) {\n\t\t\t\tcatalogError(\n\t\t\t\t\t\"track_group_duration_ms\",\n\t\t\t\t\tpath,\n\t\t\t\t\t\"groupDurationMs or groupDurationTicks must be a positive integer\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (groupDurationTicks === undefined &&\n\t\t\t\t(BigInt(groupDurationMs as number) * BigInt(timescale)) % 1000n !== 0n) {\n\t\t\t\tcatalogError(\"track_group_duration_exact\", `${path}.groupDurationMs`, \"does not resolve to an exact integer tick count\");\n\t\t\t}\n\t\t}\n\n\t\tif (track[\"fec\"] !== undefined) {\n\t\t\tconst fec = track[\"fec\"];\n\t\t\tif (typeof fec !== \"object\" || fec === null || Array.isArray(fec)) {\n\t\t\t\tcatalogError(\"track_fec\", `${path}.fec`, \"FEC must be an object\");\n\t\t\t}\n\t\t\tconst config = fec as Record<string, unknown>;\n\t\t\tconst algorithm = config[\"algorithm\"];\n\t\t\tif (algorithm !== \"none\" && algorithm !== \"raptorq\" && algorithm !== \"reed-solomon\") {\n\t\t\t\tcatalogError(\"track_fec\", `${path}.fec.algorithm`, \"algorithm is not supported\");\n\t\t\t}\n\t\t\tif (\"interleaveDepth\" in config || \"interleaveMs\" in config) {\n\t\t\t\tcatalogError(\"track_fec\", `${path}.fec`, \"interleaveDepthMs is the only supported interleave field\");\n\t\t\t}\n\t\t\tif (\"enhancementRepair\" in config) {\n\t\t\t\tcatalogError(\n\t\t\t\t\t\"track_fec\",\n\t\t\t\t\t`${path}.fec.enhancementRepair`,\n\t\t\t\t\t\"repair layers are self-describing fec-repair tracks (depends, repairLayer); fec.enhancementRepair is not supported\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (config[\"interleaveDepthMs\"] !== undefined && !positiveInteger(config[\"interleaveDepthMs\"])) {\n\t\t\t\tcatalogError(\"track_fec\", `${path}.fec.interleaveDepthMs`, \"must be a positive integer\");\n\t\t\t}\n\t\t\tif (algorithm !== \"none\") {\n\t\t\t\tfor (const field of [\"sourceSymbols\", \"repairSymbols\", \"symbolSize\"] as const) {\n\t\t\t\t\tif (!positiveInteger(config[field])) {\n\t\t\t\t\t\tcatalogError(\"track_fec\", `${path}.fec.${field}`, \"must be a positive integer\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (typeof config[\"repairTrack\"] !== \"string\" || config[\"repairTrack\"].length === 0) {\n\t\t\t\t\tcatalogError(\"track_fec\", `${path}.fec.repairTrack`, \"must be a non-empty string\");\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Required on a declared FEC instance, optional but known on \"none\".\n\t\t\treadFecBlockProfile(config[\"blockProfile\"], `${path}.fec.blockProfile`, algorithm !== \"none\", catalogError);\n\t\t}\n\t}\n\n\t// Repair tracks (draft-ramadan-moq-fec \u00A76.3). Every fec-repair track is\n\t// described once; each FEC source then assembles its base layer chain and\n\t// overlays through the shared contract, which fails closed with a code and\n\t// the JSON path of the offending field.\n\tconst repairDescriptors = trackRecords.flatMap((track, index) =>\n\t\ttrack[\"packaging\"] === \"fec-repair\"\n\t\t\t? [describeRepairTrack(track[\"name\"] as string, `$.tracks[${index}]`, track)]\n\t\t\t: [],\n\t);\n\t// Rule 9 (no orphan repair track) reads the same derivation the resolver\n\t// uses; see `repairReachability` in repair-layers.ts for its scope.\n\tconst reachability = repairReachability(\n\t\ttrackRecords.filter((track) => track[\"packaging\"] !== \"fec-repair\").map((track) => track[\"fec\"]),\n\t);\n\tfor (const descriptor of repairDescriptors) {\n\t\tassertRepairTrackReachable(descriptor, reachability, catalogError);\n\t\tconst target = dependsTarget(descriptor);\n\t\tif (target === undefined) continue;\n\t\tconst source = trackRecords.find((candidate) => candidate[\"name\"] === target);\n\t\tconst sourceFec = source?.[\"fec\"] as Record<string, unknown> | undefined;\n\t\tif (!source || source[\"packaging\"] !== \"mmtp\" || !sourceFec || sourceFec[\"algorithm\"] === \"none\") {\n\t\t\tcatalogError(\n\t\t\t\t\"track_repair_depends\",\n\t\t\t\t`${descriptor.path}.depends`,\n\t\t\t\t`${JSON.stringify(target)} is not an mmtp track with FEC`,\n\t\t\t);\n\t\t}\n\t}\n\tfor (let index = 0; index < trackRecords.length; index++) {\n\t\tconst track = trackRecords[index]!;\n\t\tconst fec = track[\"fec\"] as Record<string, unknown> | undefined;\n\t\tif (!fec || fec[\"algorithm\"] === \"none\") continue;\n\t\t// A dangling fec.repairTrack (missing, or not a fec-repair track) fails\n\t\t// inside the shared contract, so the resolver and this validator agree.\n\t\tassembleRepairLayers(\n\t\t\t{\n\t\t\t\tname: track[\"name\"] as string,\n\t\t\t\tpath: `$.tracks[${index}].fec`,\n\t\t\t\talgorithm: fec[\"algorithm\"],\n\t\t\t\tsourceSymbols: fec[\"sourceSymbols\"],\n\t\t\t\trepairSymbols: fec[\"repairSymbols\"],\n\t\t\t\trepairTrack: fec[\"repairTrack\"],\n\t\t\t},\n\t\t\trepairDescriptors,\n\t\t\tcatalogError,\n\t\t\t{ layerZeroMustBeDeclared: true },\n\t\t);\n\t}\n\n\tconst switchingSets = new Map<number, { name: string; ticks: bigint; timescale: bigint }>();\n\tfor (let index = 0; index < trackRecords.length; index++) {\n\t\tconst track = trackRecords[index]!;\n\t\tif (track[\"packaging\"] !== \"mmtp\") continue;\n\t\tconst altGroup = track[\"altGroup\"];\n\t\tif (!Number.isSafeInteger(altGroup)) continue;\n\t\tconst timescale = BigInt(track[\"timescale\"] as number);\n\t\tconst durationTicks = track[\"groupDurationTicks\"];\n\t\tconst ticks = durationTicks !== undefined\n\t\t\t? BigInt(durationTicks as number)\n\t\t\t: BigInt(track[\"groupDurationMs\"] as number) * timescale / 1000n;\n\t\tconst anchor = switchingSets.get(altGroup as number);\n\t\tif (anchor && ticks * anchor.timescale !== anchor.ticks * timescale) {\n\t\t\tcatalogError(\"track_switching_set\", `$.tracks[${index}]`, `group duration does not match ${JSON.stringify(anchor.name)} in altGroup ${altGroup}`);\n\t\t}\n\t\tif (!anchor) switchingSets.set(altGroup as number, { name: track[\"name\"] as string, ticks, timescale });\n\t}\n\n\tconst multicast = catalog[\"multicast\"];\n\tif (typeof multicast === \"object\" && multicast !== null && !Array.isArray(multicast)) {\n\t\tconst endpoints = (multicast as Record<string, unknown>)[\"endpoints\"];\n\t\tif (Array.isArray(endpoints)) {\n\t\t\tfor (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex++) {\n\t\t\t\tconst endpoint = endpoints[endpointIndex];\n\t\t\t\tif (typeof endpoint !== \"object\" || endpoint === null || Array.isArray(endpoint)) continue;\n\t\t\t\tconst refs = (endpoint as Record<string, unknown>)[\"tracks\"];\n\t\t\t\tif (!Array.isArray(refs)) continue;\n\t\t\t\tconst packetIds = new Map<number, string>();\n\t\t\t\tfor (let refIndex = 0; refIndex < refs.length; refIndex++) {\n\t\t\t\t\tconst ref = refs[refIndex];\n\t\t\t\t\tif (typeof ref !== \"object\" || ref === null || Array.isArray(ref)) continue;\n\t\t\t\t\tconst trackRef = ref as Record<string, unknown>;\n\t\t\t\t\tconst refPath = `$.multicast.endpoints[${endpointIndex}].tracks[${refIndex}]`;\n\t\t\t\t\tconst name = trackRef[\"name\"];\n\t\t\t\t\tif (typeof name !== \"string\" || !trackRecords.some((track) => track[\"name\"] === name)) {\n\t\t\t\t\t\tcatalogError(\"multicast_track\", `${refPath}.name`, `unknown catalog track ${JSON.stringify(name)}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst packetId = trackRef[\"packetId\"];\n\t\t\t\t\tif (packetId === MMTP_SIGNALING_PACKET_ID) {\n\t\t\t\t\t\tcatalogError(\n\t\t\t\t\t\t\t\"multicast_packet_id\",\n\t\t\t\t\t\t\t`${refPath}.packetId`,\n\t\t\t\t\t\t\t`packetId ${packetId} is the MMTP signaling flow and must not be advertised for a track; packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (!Number.isSafeInteger(packetId) || (packetId as number) < 1 || (packetId as number) > MMTP_PACKET_ID_MAX) {\n\t\t\t\t\t\tcatalogError(\"multicast_packet_id\", `${refPath}.packetId`, `packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst previous = packetIds.get(packetId as number);\n\t\t\t\t\tif (previous !== undefined) {\n\t\t\t\t\t\tcatalogError(\"multicast_packet_id\", `${refPath}.packetId`, `packetId is already assigned to ${JSON.stringify(previous)}`);\n\t\t\t\t\t}\n\t\t\t\t\tpacketIds.set(packetId as number, name as string);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\ttry {\n\t\tresolveMulticastAuthCatalog(catalog);\n\t} catch (error) {\n\t\tcatalogError(\n\t\t\t\"multicast_auth\",\n\t\t\t\"$.multicast.auth\",\n\t\t\terror instanceof Error ? error.message : \"invalid multicast.auth v2 block\",\n\t\t);\n\t}\n\n\tvalidateArchive(catalog);\n\n\treturn catalog as CmsfCatalog;\n}\n\n/**\n * Resolve a track's group duration to integer ticks using the supplied\n * timescale (Hz). Implements mmt-00 \u00A74.4.4:\n *\n * - If `groupDurationTicks` is present, return it (override wins).\n * - Else compute `groupDurationMs * timescale / 1000`. The result MUST\n * be an exact integer; throws if `(ms * timescale) % 1000 != 0`.\n *\n * Throws on invalid timing config or non-positive timescale (CLAUDE.md:\n * error on bad init).\n */\nexport function resolveGroupDurationTicks(\n\ttiming: TrackTimingConfig,\n\ttimescale: number,\n): number {\n\tif (!Number.isFinite(timescale) || !Number.isInteger(timescale) || timescale <= 0) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationTicks: timescale must be a positive integer Hz, got ${timescale}`,\n\t\t);\n\t}\n\tif (!isTrackTimingConfig(timing)) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationTicks: invalid TrackTimingConfig: ${JSON.stringify(timing)}`,\n\t\t);\n\t}\n\tif (timing.groupDurationTicks !== undefined) {\n\t\treturn timing.groupDurationTicks;\n\t}\n\tif (timing.groupDurationMs === undefined) {\n\t\tthrow new Error(\n\t\t\t\"[carp-catalog] resolveGroupDurationTicks: groupDurationMs is required when groupDurationTicks is absent\",\n\t\t);\n\t}\n\tconst product = timing.groupDurationMs * timescale;\n\tif (product % 1000 !== 0) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationTicks: groupDurationMs=${timing.groupDurationMs} at timescale=${timescale} ` +\n\t\t\t\t`produces non-integer ticks (${product / 1000}); publisher must use groupDurationTicks override (mmt-00 \u00A74.4.4)`,\n\t\t);\n\t}\n\treturn product / 1000;\n}\n\n/**\n * Validate that every track in a switching set agrees on the same\n * wall-clock group duration (mmt-00 \u00A74.4.4). Per-track timescales may\n * differ, so raw tick counts cannot be compared directly.\n *\n * Returns `{ ok: true }` when all tracks agree (or the set is empty);\n * `{ ok: false, mismatches }` listing each track whose effective ticks\n * differ from the first track's value. Subscribers per spec SHOULD\n * reject the catalog on mismatch.\n *\n * Throws if any track's timing config is invalid (per\n * `resolveGroupDurationTicks`).\n */\nexport function validateSwitchingSetTiming(\n\ttracks: Array<{ name: string; timing: TrackTimingConfig; timescale: number }>,\n):\n\t| { ok: true }\n\t| { ok: false; expectedTicks: number; mismatches: Array<{ name: string; ticks: number }> } {\n\tif (tracks.length === 0) return { ok: true };\n\tconst ticksByTrack = tracks.map((t) => ({\n\t\tname: t.name,\n\t\tticks: resolveGroupDurationTicks(t.timing, t.timescale),\n\t\ttimescale: t.timescale,\n\t}));\n\tconst expectedTicks = ticksByTrack[0].ticks;\n\tconst expectedTimescale = ticksByTrack[0].timescale;\n\tconst mismatches = ticksByTrack\n\t\t.slice(1)\n\t\t.filter(\n\t\t\t(t) =>\n\t\t\t\tBigInt(t.ticks) * BigInt(expectedTimescale) !==\n\t\t\t\tBigInt(expectedTicks) * BigInt(t.timescale),\n\t\t)\n\t\t.map(({ name, ticks }) => ({ name, ticks }));\n\tif (mismatches.length === 0) return { ok: true };\n\treturn { ok: false, expectedTicks, mismatches };\n}\n", "// Typed FEC state shared by every player (hang/moq-lib, moqtail, Shaka MSF).\n//\n// A player's FEC counters are `null` in three unrelated situations: the\n// session is on reliable MoQ, where FEC has nothing to protect (FEC protects\n// only lossy multicast/AMT datagrams); no multicast bridge exists, so no lossy\n// leg can be joined; or the catalog declares no FEC for the tracks the player\n// selected. `null` also meant \"nothing decoded yet\" and \"the pipeline broke\",\n// so an idle session and a broken one looked identical.\n//\n// `resolveFecState` names which case holds. Each reason is read from a fact the\n// player already owns -- its catalog, its transport policy, its bridge probe\n// and its physical subscription -- and never from counters, console text,\n// timers or elapsed time. The rules live here so the three players cannot\n// disagree about precedence.\n\nimport { isFecBlockProfile } from '@blockcast/mmt-base'\n\n/**\n * Minimum valid FEC interleave depth in milliseconds, per spec \u00A75.1\n * (the interactive/gaming floor; broadcast/ATSC 3.0 ranges up to 10000ms).\n *\n * A protocol constant, NOT a tuning knob. Values below this floor are almost\n * certainly a raw frame count (e.g. `2`) that a caller passed when no framerate\n * was available to convert to ms (common on audio tracks). Feeding such a value\n * into computeFecTimeout treats it as `2ms`, yielding a near-zero FEC block\n * timeout that abandons startup FEC blocks before their repair symbols arrive\n * (measured: moqtail startupFailed=4, shaka dropped=5.8% on startup). Timing\n * derivations MUST reject sub-floor values and fall back to the established\n * timeout instead of collapsing.\n */\nexport const MIN_INTERLEAVE_DEPTH_MS = 33\n\n/** Why FEC is not protecting this session. */\nexport type FecInactiveReason = 'no-catalog-fec' | 'reliable-moq' | 'no-multicast-bridge'\n\n/** Every inactive reason, in resolution order. */\nexport const FEC_INACTIVE_REASONS: readonly FecInactiveReason[] = Object.freeze([\n 'no-catalog-fec',\n 'reliable-moq',\n 'no-multicast-bridge',\n] as const)\n\n/** The transport policy vocabulary every player accepts. */\nexport type FecTransportPolicy = 'auto' | 'moq-only' | 'multicast-only'\n\nconst TRANSPORT_POLICIES: ReadonlySet<string> = new Set<FecTransportPolicy>(['auto', 'moq-only', 'multicast-only'])\n\n/** FEC is not protecting this session; `reason` says why. Carries no counters. */\nexport interface FecInactiveState {\n readonly state: 'inactive'\n readonly reason: FecInactiveReason\n}\n\n/**\n * FEC is the protection in force for this session's delivery path.\n *\n * Flat on purpose: the player's existing counters stay at the paths every\n * consumer already reads (`fec.blocksRecovered`, `fec.tracks`, `fec.blocks`),\n * with `state` added beside them. A consumer that ignores `state` reads an\n * active report exactly as before and an inactive one as \"no counters\", which\n * is what `null` meant to it.\n */\nexport type FecActiveState<S extends object> = S & { readonly state: 'active' }\n\nexport type FecState<S extends object> = FecActiveState<S> | FecInactiveState\n\n/** An active state whose decoder has not reported any counter yet. */\nexport type NoFecCounters = Record<never, never>\n\n/** The authoritative facts `resolveFecState` reads. */\nexport interface FecStateEvidence<S extends object> {\n /**\n * The counters the player reports today, or null/undefined when it has none.\n * Payload only: it never decides the state, because moqtail and Shaka report\n * a zeroed snapshot as soon as a MulticastManager exists, which is before any\n * bridge or subscription does.\n */\n readonly stats: S | null | undefined\n /**\n * Whether the catalog declares FEC (see `catalogDeclaresFec`) for at least\n * one media track the player selected.\n */\n readonly catalogFec: boolean\n /** The player's configured transport policy. */\n readonly transportPolicy: FecTransportPolicy\n /** Whether a multicast bridge (`window.multicast`) is available to the page. */\n readonly bridgeAvailable: boolean\n /**\n * Whether a physical multicast/AMT subscription is live: the bridge accepted\n * it and it has not been closed. A requested leg is not a joined one.\n */\n readonly multicastLegJoined: boolean\n}\n\nconst INACTIVE: Readonly<Record<FecInactiveReason, FecInactiveState>> = Object.freeze({\n 'no-catalog-fec': Object.freeze({ state: 'inactive', reason: 'no-catalog-fec' }),\n 'reliable-moq': Object.freeze({ state: 'inactive', reason: 'reliable-moq' }),\n 'no-multicast-bridge': Object.freeze({ state: 'inactive', reason: 'no-multicast-bridge' }),\n})\n\nfunction recordOf(value: unknown): Record<string, unknown> | undefined {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined\n}\n\nfunction isPositiveSafeInteger(value: unknown): boolean {\n return typeof value === 'number' && Number.isSafeInteger(value) && value > 0\n}\n\n/**\n * Whether one catalog `fec` block (a hang rendition's `fec` or an MSF/CMSF\n * track's `fec`) declares an FEC instance a receiver can configure: source\n * symbols per block (K) and a symbol size (T) are both positive integers, the\n * algorithm is not `none`, and `blockProfile` names a known block profile\n * (BLO-35597). Total: it never throws, and answers false for any other value.\n *\n * `resolveCatalogTrackFec` reads this predicate, so the players' parameters and\n * this state never disagree. Of the blocks this rejects, it returns no FEC\n * geometry only for `algorithm: \"none\"`. One with any other algorithm is a\n * malformed catalog (`FecCatalogConfig` marks both fields REQUIRED; the MSF\n * schema and `validateCmsfMmtpCatalog` enforce them on `tracks[]`, but the\n * hang dialect's `renditions` schema is permissive, so this is where a hang\n * block fails closed), and `resolveCatalogTrackFec` throws a `RepairContractError` for\n * it (`track_fec` unless another repair-contract violation fires first),\n * whether or not the source has repair layers, rather than silently dropping\n * the track's repair subscription.\n *\n * The block profile is part of this predicate because the profile selects the\n * block math and is never defaulted: without a known one no receiver can\n * configure the instance. `resolveCatalogTrackFec` resolves such a track to no\n * FEC with a typed `fecUnavailable` reason (`track_fec_block_profile`), scoped\n * to that track, so its parameters and this state still agree. The CMSF\n * conformance validator (`validateCmsfMmtpCatalog`) still rejects the catalog\n * with `track_fec_block_profile`. An `iso-ssbg1-v1` instance without a usable\n * block span (`isoBlockSpanUsable`) is likewise no FEC for its track alone,\n * with `fecUnavailable` code `track_fec` on its `interleaveDepthMs`.\n *\n * The repair-contract resolvers (`resolveRepairLayers`) gate a `depends` target\n * on `algorithm` alone and never read a source's `symbolSize`. They check its\n * `sourceSymbols` for the RaptorQ K bound, and require a positive integer\n * `sourceSymbols` and `repairSymbols` only when the source has repair layers or\n * an overlay beyond layer 0.\n */\nexport function catalogDeclaresFec(fec: unknown): boolean {\n const block = recordOf(fec)\n if (block === undefined || block['algorithm'] === 'none') return false\n return (\n isPositiveSafeInteger(block['sourceSymbols']) &&\n isPositiveSafeInteger(block['symbolSize']) &&\n isFecBlockProfile(block['blockProfile']) &&\n isoBlockSpanUsable(block)\n )\n}\n\n/**\n * Whether a `fec` block that names `iso-ssbg1-v1` carries the block span its\n * block location is bounded by: `interleaveDepthMs`, a finite number of\n * milliseconds no smaller than `MIN_INTERLEAVE_DEPTH_MS`. Without it a receiver\n * cannot bound the sources it holds before a repair states their block, so the\n * instance cannot be configured (BLO-35597). Any other block needs no span here.\n */\nexport function isoBlockSpanUsable(fec: unknown): boolean {\n const block = recordOf(fec)\n if (block === undefined || block['blockProfile'] !== 'iso-ssbg1-v1') return true\n const ms = block['interleaveDepthMs']\n return typeof ms === 'number' && Number.isFinite(ms) && ms >= MIN_INTERLEAVE_DEPTH_MS\n}\n\nfunction requireBoolean(value: unknown, field: string): boolean {\n if (typeof value !== 'boolean') {\n throw new TypeError(`resolveFecState: ${field} must be a boolean, got ${typeof value}`)\n }\n return value\n}\n\n/**\n * Resolve a player's FEC state. First match wins:\n *\n * 1. The catalog declares no FEC for a selected track: `no-catalog-fec`. FEC\n * geometry comes only from the catalog, so there is nothing to configure on\n * any path.\n * 2. The policy is `moq-only`: `reliable-moq`. Media travels only over QUIC,\n * which is reliable, so FEC has no loss to repair, bridge or not.\n * 3. No multicast bridge: `no-multicast-bridge`. No lossy leg can exist, and on\n * `auto` this is the reason the session is on reliable MoQ.\n * 4. The policy is `multicast-only`, or a lossy subscription is live: active.\n * `multicast-only` commits the session to the lossy leg, so FEC is in force\n * before the join completes; whether the join succeeded is the physical\n * attestation's question, not this one. The counters may be absent until\n * the decoder reports.\n * 5. Otherwise (`auto`, a bridge, no live subscription): `reliable-moq`.\n *\n * Throws on malformed evidence rather than picking a reason for it.\n */\nexport function resolveFecState<S extends object>(evidence: FecStateEvidence<S> & { readonly stats: S }): FecState<S>\nexport function resolveFecState<S extends object>(evidence: FecStateEvidence<S>): FecState<S | NoFecCounters>\nexport function resolveFecState<S extends object>(evidence: FecStateEvidence<S>): FecState<S | NoFecCounters> {\n const catalogFec = requireBoolean(evidence.catalogFec, 'catalogFec')\n const bridgeAvailable = requireBoolean(evidence.bridgeAvailable, 'bridgeAvailable')\n const multicastLegJoined = requireBoolean(evidence.multicastLegJoined, 'multicastLegJoined')\n const { transportPolicy, stats } = evidence\n if (typeof transportPolicy !== 'string' || !TRANSPORT_POLICIES.has(transportPolicy)) {\n throw new TypeError(`resolveFecState: unsupported transportPolicy ${JSON.stringify(transportPolicy)}`)\n }\n if (stats !== null && stats !== undefined && recordOf(stats) === undefined) {\n throw new TypeError('resolveFecState: stats must be a plain object, null or undefined')\n }\n\n if (!catalogFec) return INACTIVE['no-catalog-fec']\n if (transportPolicy === 'moq-only') return INACTIVE['reliable-moq']\n if (!bridgeAvailable) return INACTIVE['no-multicast-bridge']\n if (transportPolicy === 'multicast-only' || multicastLegJoined) {\n // The discriminant is written last so a stats field of the same name can\n // never masquerade as a state.\n return { ...(stats ?? {}), state: 'active' } as FecActiveState<S | NoFecCounters>\n }\n return INACTIVE['reliable-moq']\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["MPU_HEADER_SIZE", "MPU_LENGTH_FIELD_SIZE", "MPU_HEADER_REMAINDER_SIZE", "MPU_HEADER_SIZE", "recordOf", "descriptors", "fec", "reachability", "rendition"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { DEFAULT_AMT_RELAY_PORT } from "@blockcast/mmt-base";
|
|
2
|
+
/** One `networkSource[]` entry as published (`{type, discovery?, relay, port?}`). */
|
|
3
|
+
export type CatalogNetworkSource = {
|
|
4
|
+
type?: string;
|
|
5
|
+
/** `"manual"` (or absent) names a dialable relay; `"driad"` resolves at runtime. */
|
|
6
|
+
discovery?: string;
|
|
7
|
+
relay?: string;
|
|
8
|
+
port?: number;
|
|
9
|
+
};
|
|
10
|
+
/** Resolved AMT relay a player can hand to its multicast join/subscribe. */
|
|
11
|
+
export type AmtNetworkSource = {
|
|
12
|
+
relay: string;
|
|
13
|
+
/** Always resolved: `DEFAULT_AMT_RELAY_PORT` when the catalog names none. */
|
|
14
|
+
port: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* AMT well-known UDP port (RFC 7450 §4).
|
|
18
|
+
*
|
|
19
|
+
* Re-exported so `networkSource` consumers get it from the module that owns
|
|
20
|
+
* those semantics; the definition lives in `@blockcast/mmt-base` so transport
|
|
21
|
+
* and container cannot drift.
|
|
22
|
+
*/
|
|
23
|
+
export { DEFAULT_AMT_RELAY_PORT };
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the AMT relay from a catalog `multicast` block's `networkSource`
|
|
26
|
+
* (array or bare-object form). Returns the first entry with `type: "amt"`
|
|
27
|
+
* and a non-blank relay, or `undefined` when the catalog advertises none —
|
|
28
|
+
* callers fall back to native multicast (or their own explicit relay config).
|
|
29
|
+
*
|
|
30
|
+
* A `discovery: "driad"` entry is skipped even when it names a `relay`: that
|
|
31
|
+
* string is the `_amt._udp` DNS name a runtime RFC 8777 lookup resolves, not
|
|
32
|
+
* an address to dial, and the emitter derives exactly that shape from
|
|
33
|
+
* `amtFallback: {discovery: "driad", relayDns}`. Returning it would turn a
|
|
34
|
+
* catalog that asked for DRIAD into a manual join against an unroutable name.
|
|
35
|
+
* `findAmtRelay` in `@blockcast/mmt-transport` applies the same rule — the two
|
|
36
|
+
* readers of one field must agree on which entries are dialable.
|
|
37
|
+
*
|
|
38
|
+
* `port` is always resolved: an entry that names none, or names an unusable
|
|
39
|
+
* one, gets `DEFAULT_AMT_RELAY_PORT`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function catalogAmtNetworkSource(networkSource: unknown): AmtNetworkSource | undefined;
|
|
42
|
+
//# sourceMappingURL=catalog-network-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-network-source.d.ts","sourceRoot":"","sources":["../src/catalog-network-source.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,qFAAqF;AACrF,MAAM,MAAM,oBAAoB,GAAG;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,4EAA4E;AAC5E,MAAM,MAAM,gBAAgB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,CAAC;AAOlC;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CAAC,aAAa,EAAE,OAAO,GAAG,gBAAgB,GAAG,SAAS,CAY5F"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ../mmt-alta-parse/dist/index.js
|
|
2
|
+
var MPU_HEADER_SIZE = 8;
|
|
3
|
+
var MPU_LENGTH_FIELD_SIZE = 2;
|
|
4
|
+
var MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE;
|
|
5
|
+
|
|
6
|
+
// ../mmt-base/dist/mmtp.js
|
|
7
|
+
var MPU_HEADER_SIZE2 = 8;
|
|
8
|
+
var MPU_LENGTH_FIELD_SIZE2 = 2;
|
|
9
|
+
var MPU_HEADER_REMAINDER_SIZE2 = MPU_HEADER_SIZE2 - MPU_LENGTH_FIELD_SIZE2;
|
|
10
|
+
|
|
11
|
+
// ../mmt-base/dist/net-constants.js
|
|
12
|
+
var DEFAULT_AMT_RELAY_PORT = 2268;
|
|
13
|
+
|
|
14
|
+
// ../mmt-base/dist/ntp-short.js
|
|
15
|
+
var NTP_SHORT_BITS = 32;
|
|
16
|
+
var NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS;
|
|
17
|
+
var NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1);
|
|
18
|
+
|
|
19
|
+
// ../mmt-base/dist/fec-block-profile.js
|
|
20
|
+
var FEC_BLOCK_PROFILES = Object.freeze([
|
|
21
|
+
"k-padded-v1",
|
|
22
|
+
"iso-ssbg1-v1"
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
// src/catalog-network-source.ts
|
|
26
|
+
function normalizePort(value) {
|
|
27
|
+
const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
28
|
+
return Number.isInteger(n) && n > 0 && n <= 65535 ? n : void 0;
|
|
29
|
+
}
|
|
30
|
+
function catalogAmtNetworkSource(networkSource) {
|
|
31
|
+
const entries = Array.isArray(networkSource) ? networkSource : networkSource ? [networkSource] : [];
|
|
32
|
+
for (const raw of entries) {
|
|
33
|
+
if (!raw || typeof raw !== "object") continue;
|
|
34
|
+
const ns = raw;
|
|
35
|
+
if (ns.type !== "amt") continue;
|
|
36
|
+
if (ns.discovery === "driad") continue;
|
|
37
|
+
const relay = typeof ns.relay === "string" ? ns.relay.trim() : "";
|
|
38
|
+
if (!relay) continue;
|
|
39
|
+
return { relay, port: normalizePort(ns.port) ?? DEFAULT_AMT_RELAY_PORT };
|
|
40
|
+
}
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
DEFAULT_AMT_RELAY_PORT,
|
|
45
|
+
catalogAmtNetworkSource
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=catalog-network-source.js.map
|