@blockcast/mmt-container 0.3.0 → 0.4.0-main.1cd3cdea89da
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 +33 -16
- package/dist/browser-runtime.js +14814 -14255
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.d.ts +24 -19
- package/dist/carp-catalog.d.ts.map +1 -1
- package/dist/carp-catalog.js +29 -14
- package/dist/carp-catalog.js.map +4 -4
- package/dist/catalog-fec-params.d.ts +88 -28
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +60 -23
- package/dist/catalog-fec-params.js.map +4 -4
- package/dist/catalog-network-source.js +1 -7
- package/dist/catalog-network-source.js.map +4 -4
- package/dist/cmaf-block-assembler.d.ts +0 -2
- package/dist/cmaf-block-assembler.d.ts.map +1 -1
- package/dist/cmaf-block-assembler.js +0 -1
- package/dist/cmaf-block-assembler.js.map +2 -2
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js +0 -4
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.d.ts +124 -0
- package/dist/delivery-catalog.d.ts.map +1 -0
- package/dist/delivery-catalog.js +1857 -0
- package/dist/delivery-catalog.js.map +7 -0
- package/dist/delta-fallback-config.d.ts +42 -0
- package/dist/delta-fallback-config.d.ts.map +1 -0
- package/dist/delta-fallback-config.js +59 -0
- package/dist/delta-fallback-config.js.map +7 -0
- package/dist/fec-manager.d.ts +52 -30
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +338 -293
- package/dist/fec-manager.js.map +4 -4
- package/dist/fec-state.d.ts +19 -31
- package/dist/fec-state.d.ts.map +1 -1
- package/dist/fec-state.js +4 -10
- package/dist/fec-state.js.map +4 -4
- package/dist/fec-worker-setup.d.ts +18 -14
- package/dist/fec-worker-setup.d.ts.map +1 -1
- package/dist/fec-worker-setup.js +81 -96
- package/dist/fec-worker-setup.js.map +4 -4
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -0
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.js +1 -7
- package/dist/iso-block-locator.js.map +4 -4
- package/dist/media-router.d.ts +20 -8
- package/dist/media-router.d.ts.map +1 -1
- package/dist/media-router.js +46 -21
- package/dist/media-router.js.map +2 -2
- package/dist/mfu-reassembler.js +24 -30
- package/dist/mfu-reassembler.js.map +4 -4
- package/dist/mmtp-container-parser.d.ts +14 -11
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +126 -99
- package/dist/mmtp-container-parser.js.map +4 -4
- package/dist/mmtp-container-parser.no-modules.js +151 -108
- package/dist/mmtp-container-parser.no-modules.js.map +4 -4
- package/dist/mmtp-decode-pipeline.js +126 -99
- package/dist/mmtp-decode-pipeline.js.map +4 -4
- package/dist/mmtp-router.d.ts +51 -29
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +260 -326
- package/dist/mmtp-router.js.map +4 -4
- package/dist/multicast-input.js +1 -7
- package/dist/multicast-input.js.map +4 -4
- package/dist/multicast-manager.d.ts +72 -6
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +882 -673
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +1 -1
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +44 -26
- package/dist/multicast-transport.js.map +4 -4
- package/dist/multicast-unwrap.js +29 -35
- package/dist/multicast-unwrap.js.map +4 -4
- package/dist/no-modules.js +147 -106
- package/dist/no-modules.js.map +4 -4
- package/dist/repair-fetch.d.ts +116 -21
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +114 -34
- package/dist/repair-fetch.js.map +4 -4
- package/dist/repair-layers.d.ts +18 -7
- package/dist/repair-layers.d.ts.map +1 -1
- package/dist/repair-layers.js +12 -10
- package/dist/repair-layers.js.map +4 -4
- package/dist/signaling.js +2 -11
- package/dist/signaling.js.map +4 -4
- package/dist/timestamp-codec.js +24 -30
- package/dist/timestamp-codec.js.map +4 -4
- package/dist/track-session.d.ts +7 -0
- package/dist/track-session.d.ts.map +1 -1
- package/dist/track-session.js +15 -8
- package/dist/track-session.js.map +2 -2
- package/dist/wasm-mfu-reassembler.js +1 -7
- package/dist/wasm-mfu-reassembler.js.map +4 -4
- package/dist/wasm-mmtp-container-parser.js +126 -99
- package/dist/wasm-mmtp-container-parser.js.map +4 -4
- package/package.json +7 -14
- package/schemas/catalog.schema.json +3 -3
- package/src/carp-catalog.ts +46 -24
- package/src/catalog-fec-params.ts +173 -64
- package/src/cmaf-block-assembler.ts +1 -4
- package/src/cmcd-producer.ts +0 -6
- package/src/delivery-catalog.ts +498 -0
- package/src/delta-fallback-config.ts +72 -0
- package/src/fec-manager.ts +457 -334
- package/src/fec-state.ts +20 -32
- package/src/fec-worker-setup.ts +109 -90
- package/src/index.ts +32 -0
- package/src/media-router.ts +59 -21
- package/src/mmtp-container-parser.ts +106 -20
- package/src/mmtp-router.ts +189 -330
- package/src/multicast-manager.ts +265 -86
- package/src/multicast-transport.ts +16 -9
- package/src/repair-fetch.ts +273 -43
- package/src/repair-layers.ts +28 -8
- package/src/track-session.ts +28 -9
- package/test-vectors/catalog/README.md +37 -17
- package/test-vectors/catalog/negative/fec-block-profile-retired.json +52 -0
- package/test-vectors/catalog/negative/fec-enhancement-repair-removed.json +1 -1
- package/test-vectors/catalog/negative/fec-repair-layer-geometry.json +1 -1
- package/test-vectors/catalog/negative/fec-repair-layer-missing-symbols.json +1 -1
- package/test-vectors/catalog/negative/fec-repair-multiple-depends.json +1 -1
- package/test-vectors/catalog/negative/fec-repair-overlay-with-layer.json +1 -1
- package/test-vectors/catalog/negative/fec-repair-priority-out-of-band.json +1 -1
- package/test-vectors/catalog/negative/fec-repair-source-symbols-without-scope.json +1 -1
- package/test-vectors/catalog/negative/multicast-endpoint-relay-address.json +39 -0
- package/test-vectors/catalog/negative/multicast-network-source-empty.json +1 -1
- package/test-vectors/catalog/negative/raptorq-source-symbols-over-max.json +1 -1
- package/test-vectors/catalog/negative/raptorq-unaligned-symbol.json +1 -1
- package/test-vectors/catalog/positive/fec-layered-repair.json +2 -2
- package/test-vectors/catalog/positive/fec-multicast.json +2 -1
- package/test-vectors/catalog/positive/multicast-amt-manual-relay.json +38 -0
- package/test-vectors/catalog/positive/multicast-native-ssm-no-network-source.json +2 -1
- package/test-vectors/catalog-timing.json +356 -0
- package/test-vectors/catalog-validation.json +40 -5
- package/test-vectors/delivery-catalog/README.md +11 -0
- package/test-vectors/delivery-catalog/negative/missing-block-profile.json +58 -0
- package/test-vectors/delivery-catalog/negative/packet-id-signaling.json +59 -0
- package/test-vectors/delivery-catalog/negative/retired-k-padded-block-profile.json +59 -0
- package/test-vectors/delivery-catalog/positive/dash.json +56 -0
- package/test-vectors/delivery-catalog/positive/mmtp-source-only.json +43 -0
- package/node_modules/@blockcast/fec-worker/README.md +0 -223
- package/node_modules/@blockcast/fec-worker/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/fec-worker/dist/block-order.d.ts +0 -38
- package/node_modules/@blockcast/fec-worker/dist/block-order.d.ts.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/block-order.js +0 -78
- package/node_modules/@blockcast/fec-worker/dist/block-order.js.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts +0 -93
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.d.ts.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js +0 -346
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-client.js.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts +0 -380
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.d.ts.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js +0 -11
- package/node_modules/@blockcast/fec-worker/dist/fec-worker-types.js.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts +0 -14
- package/node_modules/@blockcast/fec-worker/dist/fec-worker.d.ts.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/fec-worker.js +0 -859
- package/node_modules/@blockcast/fec-worker/dist/fec-worker.js.map +0 -7
- package/node_modules/@blockcast/fec-worker/dist/index.d.ts +0 -10
- package/node_modules/@blockcast/fec-worker/dist/index.d.ts.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/index.js +0 -8
- package/node_modules/@blockcast/fec-worker/dist/index.js.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts +0 -20
- package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.d.ts.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js +0 -370
- package/node_modules/@blockcast/fec-worker/dist/shred-fec-worker.js.map +0 -7
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts +0 -104
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.d.ts.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js +0 -181
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-client.js.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts +0 -194
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.d.ts.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js +0 -62
- package/node_modules/@blockcast/fec-worker/dist/shred-worker-types.js.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts +0 -9
- package/node_modules/@blockcast/fec-worker/dist/transfer.d.ts.map +0 -1
- package/node_modules/@blockcast/fec-worker/dist/transfer.js +0 -13
- package/node_modules/@blockcast/fec-worker/dist/transfer.js.map +0 -1
- package/node_modules/@blockcast/fec-worker/package.json +0 -59
- package/node_modules/@blockcast/fec-worker/src/block-order.ts +0 -93
- package/node_modules/@blockcast/fec-worker/src/fec-worker-client.ts +0 -392
- package/node_modules/@blockcast/fec-worker/src/fec-worker-types.ts +0 -399
- package/node_modules/@blockcast/fec-worker/src/fec-worker.ts +0 -1502
- package/node_modules/@blockcast/fec-worker/src/index.ts +0 -35
- package/node_modules/@blockcast/fec-worker/src/shred-fec-worker.ts +0 -589
- package/node_modules/@blockcast/fec-worker/src/shred-worker-client.ts +0 -222
- package/node_modules/@blockcast/fec-worker/src/shred-worker-types.ts +0 -209
- package/node_modules/@blockcast/fec-worker/src/transfer.ts +0 -12
- package/node_modules/@blockcast/mmt-alta-parse/README.md +0 -181
- package/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +0 -57
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.js +0 -115
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +0 -1
- package/node_modules/@blockcast/mmt-alta-parse/package.json +0 -50
- package/node_modules/@blockcast/mmt-alta-parse/src/index.ts +0 -136
- package/node_modules/@blockcast/mmt-base/README.md +0 -134
- package/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts +0 -55
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js +0 -128
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/index.d.ts +0 -7
- package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/index.js +0 -7
- package/node_modules/@blockcast/mmt-base/dist/index.js.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +0 -292
- package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/mmtp.js +0 -612
- package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +0 -18
- package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/net-constants.js +0 -18
- package/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +0 -47
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.js +0 -54
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts +0 -57
- package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/serial-number.js +0 -98
- package/node_modules/@blockcast/mmt-base/dist/serial-number.js.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts +0 -312
- package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/signalling.js +0 -637
- package/node_modules/@blockcast/mmt-base/dist/signalling.js.map +0 -1
- package/node_modules/@blockcast/mmt-base/package.json +0 -49
- package/node_modules/@blockcast/mmt-base/src/fec-block-profile.ts +0 -145
- package/node_modules/@blockcast/mmt-base/src/index.ts +0 -6
- package/node_modules/@blockcast/mmt-base/src/mmtp.ts +0 -892
- package/node_modules/@blockcast/mmt-base/src/net-constants.ts +0 -17
- package/node_modules/@blockcast/mmt-base/src/ntp-short.ts +0 -67
- package/node_modules/@blockcast/mmt-base/src/serial-number.ts +0 -107
- package/node_modules/@blockcast/mmt-base/src/signalling.ts +0 -862
- package/node_modules/@blockcast/mmt-fec/README.md +0 -271
- package/node_modules/@blockcast/mmt-fec/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts +0 -82
- package/node_modules/@blockcast/mmt-fec/dist/block-manager.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/block-manager.js +0 -296
- package/node_modules/@blockcast/mmt-fec/dist/block-manager.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts +0 -106
- package/node_modules/@blockcast/mmt-fec/dist/block-processor.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/block-processor.js +0 -288
- package/node_modules/@blockcast/mmt-fec/dist/block-processor.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts +0 -46
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js +0 -96
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-recovery.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts +0 -33
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js +0 -57
- package/node_modules/@blockcast/mmt-fec/dist/cmaf-symbols.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts +0 -205
- package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js +0 -240
- package/node_modules/@blockcast/mmt-fec/dist/fec-block-mapper.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/index.d.ts +0 -28
- package/node_modules/@blockcast/mmt-fec/dist/index.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/index.js +0 -28
- package/node_modules/@blockcast/mmt-fec/dist/index.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts +0 -20
- package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js +0 -28
- package/node_modules/@blockcast/mmt-fec/dist/loc-symbols.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts +0 -258
- package/node_modules/@blockcast/mmt-fec/dist/oti.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/oti.js +0 -382
- package/node_modules/@blockcast/mmt-fec/dist/oti.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts +0 -41
- package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js +0 -81
- package/node_modules/@blockcast/mmt-fec/dist/reorder-buffer.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/repair-group.d.ts +0 -82
- package/node_modules/@blockcast/mmt-fec/dist/repair-group.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/dist/repair-group.js +0 -170
- package/node_modules/@blockcast/mmt-fec/dist/repair-group.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/README.md +0 -181
- 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 +0 -57
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js +0 -115
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/package.json +0 -50
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-alta-parse/src/index.ts +0 -136
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/README.md +0 -134
- 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 +0 -55
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js +0 -128
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts +0 -7
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js +0 -7
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/index.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +0 -292
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js +0 -612
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +0 -18
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.js +0 -18
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +0 -47
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.js +0 -54
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts +0 -57
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.js +0 -98
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/serial-number.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.d.ts +0 -312
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.js +0 -637
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/dist/signalling.js.map +0 -1
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/package.json +0 -49
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/fec-block-profile.ts +0 -145
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/index.ts +0 -6
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/mmtp.ts +0 -892
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/net-constants.ts +0 -17
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/ntp-short.ts +0 -67
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/serial-number.ts +0 -107
- package/node_modules/@blockcast/mmt-fec/node_modules/@blockcast/mmt-base/src/signalling.ts +0 -862
- package/node_modules/@blockcast/mmt-fec/package.json +0 -79
- package/node_modules/@blockcast/mmt-fec/src/block-manager.ts +0 -371
- package/node_modules/@blockcast/mmt-fec/src/block-processor.ts +0 -439
- package/node_modules/@blockcast/mmt-fec/src/cmaf-recovery.ts +0 -127
- package/node_modules/@blockcast/mmt-fec/src/cmaf-symbols.ts +0 -68
- package/node_modules/@blockcast/mmt-fec/src/fec-block-mapper.ts +0 -310
- package/node_modules/@blockcast/mmt-fec/src/index.ts +0 -94
- package/node_modules/@blockcast/mmt-fec/src/loc-symbols.ts +0 -34
- package/node_modules/@blockcast/mmt-fec/src/oti.ts +0 -463
- package/node_modules/@blockcast/mmt-fec/src/reorder-buffer.ts +0 -101
- package/node_modules/@blockcast/mmt-fec/src/repair-group.ts +0 -195
- package/node_modules/@blockcast/mmt-manifest-verify/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts +0 -28
- package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js +0 -128
- package/node_modules/@blockcast/mmt-manifest-verify/dist/admission.js.map +0 -1
- package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts +0 -4
- package/node_modules/@blockcast/mmt-manifest-verify/dist/index.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js +0 -4
- package/node_modules/@blockcast/mmt-manifest-verify/dist/index.js.map +0 -1
- package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts +0 -37
- package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js +0 -179
- package/node_modules/@blockcast/mmt-manifest-verify/dist/source-auth.js.map +0 -1
- package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts +0 -99
- package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js +0 -492
- package/node_modules/@blockcast/mmt-manifest-verify/dist/verifier.js.map +0 -1
- package/node_modules/@blockcast/mmt-manifest-verify/package.json +0 -45
- package/node_modules/@blockcast/mmt-manifest-verify/src/admission.ts +0 -171
- package/node_modules/@blockcast/mmt-manifest-verify/src/index.ts +0 -3
- package/node_modules/@blockcast/mmt-manifest-verify/src/source-auth.ts +0 -230
- package/node_modules/@blockcast/mmt-manifest-verify/src/verifier.ts +0 -669
- package/node_modules/@noble/hashes/LICENSE +0 -21
- package/node_modules/@noble/hashes/README.md +0 -521
- package/node_modules/@noble/hashes/_assert.d.ts +0 -17
- package/node_modules/@noble/hashes/_assert.d.ts.map +0 -1
- package/node_modules/@noble/hashes/_assert.js +0 -18
- package/node_modules/@noble/hashes/_assert.js.map +0 -1
- package/node_modules/@noble/hashes/_blake.d.ts +0 -14
- package/node_modules/@noble/hashes/_blake.d.ts.map +0 -1
- package/node_modules/@noble/hashes/_blake.js +0 -50
- package/node_modules/@noble/hashes/_blake.js.map +0 -1
- package/node_modules/@noble/hashes/_md.d.ts +0 -51
- package/node_modules/@noble/hashes/_md.d.ts.map +0 -1
- package/node_modules/@noble/hashes/_md.js +0 -162
- package/node_modules/@noble/hashes/_md.js.map +0 -1
- package/node_modules/@noble/hashes/_u64.d.ts +0 -55
- package/node_modules/@noble/hashes/_u64.d.ts.map +0 -1
- package/node_modules/@noble/hashes/_u64.js +0 -90
- package/node_modules/@noble/hashes/_u64.js.map +0 -1
- package/node_modules/@noble/hashes/argon2.d.ts +0 -32
- package/node_modules/@noble/hashes/argon2.d.ts.map +0 -1
- package/node_modules/@noble/hashes/argon2.js +0 -401
- package/node_modules/@noble/hashes/argon2.js.map +0 -1
- package/node_modules/@noble/hashes/blake1.d.ts +0 -106
- package/node_modules/@noble/hashes/blake1.d.ts.map +0 -1
- package/node_modules/@noble/hashes/blake1.js +0 -459
- package/node_modules/@noble/hashes/blake1.js.map +0 -1
- package/node_modules/@noble/hashes/blake2.d.ts +0 -116
- package/node_modules/@noble/hashes/blake2.d.ts.map +0 -1
- package/node_modules/@noble/hashes/blake2.js +0 -420
- package/node_modules/@noble/hashes/blake2.js.map +0 -1
- package/node_modules/@noble/hashes/blake2b.d.ts +0 -11
- package/node_modules/@noble/hashes/blake2b.d.ts.map +0 -1
- package/node_modules/@noble/hashes/blake2b.js +0 -14
- package/node_modules/@noble/hashes/blake2b.js.map +0 -1
- package/node_modules/@noble/hashes/blake2s.d.ts +0 -20
- package/node_modules/@noble/hashes/blake2s.d.ts.map +0 -1
- package/node_modules/@noble/hashes/blake2s.js +0 -24
- package/node_modules/@noble/hashes/blake2s.js.map +0 -1
- package/node_modules/@noble/hashes/blake3.d.ts +0 -54
- package/node_modules/@noble/hashes/blake3.d.ts.map +0 -1
- package/node_modules/@noble/hashes/blake3.js +0 -255
- package/node_modules/@noble/hashes/blake3.js.map +0 -1
- package/node_modules/@noble/hashes/crypto.d.ts +0 -2
- package/node_modules/@noble/hashes/crypto.d.ts.map +0 -1
- package/node_modules/@noble/hashes/crypto.js +0 -5
- package/node_modules/@noble/hashes/crypto.js.map +0 -1
- package/node_modules/@noble/hashes/cryptoNode.d.ts +0 -2
- package/node_modules/@noble/hashes/cryptoNode.d.ts.map +0 -1
- package/node_modules/@noble/hashes/cryptoNode.js +0 -18
- package/node_modules/@noble/hashes/cryptoNode.js.map +0 -1
- package/node_modules/@noble/hashes/eskdf.d.ts +0 -47
- package/node_modules/@noble/hashes/eskdf.d.ts.map +0 -1
- package/node_modules/@noble/hashes/eskdf.js +0 -166
- package/node_modules/@noble/hashes/eskdf.js.map +0 -1
- package/node_modules/@noble/hashes/esm/_assert.d.ts +0 -17
- package/node_modules/@noble/hashes/esm/_assert.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/_assert.js +0 -15
- package/node_modules/@noble/hashes/esm/_assert.js.map +0 -1
- package/node_modules/@noble/hashes/esm/_blake.d.ts +0 -14
- package/node_modules/@noble/hashes/esm/_blake.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/_blake.js +0 -45
- package/node_modules/@noble/hashes/esm/_blake.js.map +0 -1
- package/node_modules/@noble/hashes/esm/_md.d.ts +0 -51
- package/node_modules/@noble/hashes/esm/_md.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/_md.js +0 -155
- package/node_modules/@noble/hashes/esm/_md.js.map +0 -1
- package/node_modules/@noble/hashes/esm/_u64.d.ts +0 -55
- package/node_modules/@noble/hashes/esm/_u64.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/_u64.js +0 -67
- package/node_modules/@noble/hashes/esm/_u64.js.map +0 -1
- package/node_modules/@noble/hashes/esm/argon2.d.ts +0 -32
- package/node_modules/@noble/hashes/esm/argon2.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/argon2.js +0 -392
- package/node_modules/@noble/hashes/esm/argon2.js.map +0 -1
- package/node_modules/@noble/hashes/esm/blake1.d.ts +0 -106
- package/node_modules/@noble/hashes/esm/blake1.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/blake1.js +0 -452
- package/node_modules/@noble/hashes/esm/blake1.js.map +0 -1
- package/node_modules/@noble/hashes/esm/blake2.d.ts +0 -116
- package/node_modules/@noble/hashes/esm/blake2.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/blake2.js +0 -413
- package/node_modules/@noble/hashes/esm/blake2.js.map +0 -1
- package/node_modules/@noble/hashes/esm/blake2b.d.ts +0 -11
- package/node_modules/@noble/hashes/esm/blake2b.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/blake2b.js +0 -11
- package/node_modules/@noble/hashes/esm/blake2b.js.map +0 -1
- package/node_modules/@noble/hashes/esm/blake2s.d.ts +0 -20
- package/node_modules/@noble/hashes/esm/blake2s.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/blake2s.js +0 -21
- package/node_modules/@noble/hashes/esm/blake2s.js.map +0 -1
- package/node_modules/@noble/hashes/esm/blake3.d.ts +0 -54
- package/node_modules/@noble/hashes/esm/blake3.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/blake3.js +0 -251
- package/node_modules/@noble/hashes/esm/blake3.js.map +0 -1
- package/node_modules/@noble/hashes/esm/crypto.d.ts +0 -2
- package/node_modules/@noble/hashes/esm/crypto.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/crypto.js +0 -2
- package/node_modules/@noble/hashes/esm/crypto.js.map +0 -1
- package/node_modules/@noble/hashes/esm/cryptoNode.d.ts +0 -2
- package/node_modules/@noble/hashes/esm/cryptoNode.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/cryptoNode.js +0 -15
- package/node_modules/@noble/hashes/esm/cryptoNode.js.map +0 -1
- package/node_modules/@noble/hashes/esm/eskdf.d.ts +0 -47
- package/node_modules/@noble/hashes/esm/eskdf.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/eskdf.js +0 -160
- package/node_modules/@noble/hashes/esm/eskdf.js.map +0 -1
- package/node_modules/@noble/hashes/esm/hkdf.d.ts +0 -36
- package/node_modules/@noble/hashes/esm/hkdf.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/hkdf.js +0 -82
- package/node_modules/@noble/hashes/esm/hkdf.js.map +0 -1
- package/node_modules/@noble/hashes/esm/hmac.d.ts +0 -35
- package/node_modules/@noble/hashes/esm/hmac.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/hmac.js +0 -86
- package/node_modules/@noble/hashes/esm/hmac.js.map +0 -1
- package/node_modules/@noble/hashes/esm/index.d.ts +0 -2
- package/node_modules/@noble/hashes/esm/index.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/index.js +0 -33
- package/node_modules/@noble/hashes/esm/index.js.map +0 -1
- package/node_modules/@noble/hashes/esm/legacy.d.ts +0 -71
- package/node_modules/@noble/hashes/esm/legacy.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/legacy.js +0 -281
- package/node_modules/@noble/hashes/esm/legacy.js.map +0 -1
- package/node_modules/@noble/hashes/esm/package.json +0 -10
- package/node_modules/@noble/hashes/esm/pbkdf2.d.ts +0 -23
- package/node_modules/@noble/hashes/esm/pbkdf2.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/pbkdf2.js +0 -97
- package/node_modules/@noble/hashes/esm/pbkdf2.js.map +0 -1
- package/node_modules/@noble/hashes/esm/ripemd160.d.ts +0 -13
- package/node_modules/@noble/hashes/esm/ripemd160.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/ripemd160.js +0 -13
- package/node_modules/@noble/hashes/esm/ripemd160.js.map +0 -1
- package/node_modules/@noble/hashes/esm/scrypt.d.ts +0 -34
- package/node_modules/@noble/hashes/esm/scrypt.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/scrypt.js +0 -228
- package/node_modules/@noble/hashes/esm/scrypt.js.map +0 -1
- package/node_modules/@noble/hashes/esm/sha1.d.ts +0 -11
- package/node_modules/@noble/hashes/esm/sha1.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/sha1.js +0 -11
- package/node_modules/@noble/hashes/esm/sha1.js.map +0 -1
- package/node_modules/@noble/hashes/esm/sha2.d.ts +0 -159
- package/node_modules/@noble/hashes/esm/sha2.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/sha2.js +0 -375
- package/node_modules/@noble/hashes/esm/sha2.js.map +0 -1
- package/node_modules/@noble/hashes/esm/sha256.d.ts +0 -20
- package/node_modules/@noble/hashes/esm/sha256.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/sha256.js +0 -20
- package/node_modules/@noble/hashes/esm/sha256.js.map +0 -1
- package/node_modules/@noble/hashes/esm/sha3-addons.d.ts +0 -142
- package/node_modules/@noble/hashes/esm/sha3-addons.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/sha3-addons.js +0 -393
- package/node_modules/@noble/hashes/esm/sha3-addons.js.map +0 -1
- package/node_modules/@noble/hashes/esm/sha3.d.ts +0 -53
- package/node_modules/@noble/hashes/esm/sha3.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/sha3.js +0 -234
- package/node_modules/@noble/hashes/esm/sha3.js.map +0 -1
- package/node_modules/@noble/hashes/esm/sha512.d.ts +0 -26
- package/node_modules/@noble/hashes/esm/sha512.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/sha512.js +0 -26
- package/node_modules/@noble/hashes/esm/sha512.js.map +0 -1
- package/node_modules/@noble/hashes/esm/utils.d.ts +0 -161
- package/node_modules/@noble/hashes/esm/utils.d.ts.map +0 -1
- package/node_modules/@noble/hashes/esm/utils.js +0 -281
- package/node_modules/@noble/hashes/esm/utils.js.map +0 -1
- package/node_modules/@noble/hashes/hkdf.d.ts +0 -36
- package/node_modules/@noble/hashes/hkdf.d.ts.map +0 -1
- package/node_modules/@noble/hashes/hkdf.js +0 -88
- package/node_modules/@noble/hashes/hkdf.js.map +0 -1
- package/node_modules/@noble/hashes/hmac.d.ts +0 -35
- package/node_modules/@noble/hashes/hmac.d.ts.map +0 -1
- package/node_modules/@noble/hashes/hmac.js +0 -91
- package/node_modules/@noble/hashes/hmac.js.map +0 -1
- package/node_modules/@noble/hashes/index.d.ts +0 -1
- package/node_modules/@noble/hashes/index.d.ts.map +0 -1
- package/node_modules/@noble/hashes/index.js +0 -33
- package/node_modules/@noble/hashes/index.js.map +0 -1
- package/node_modules/@noble/hashes/legacy.d.ts +0 -71
- package/node_modules/@noble/hashes/legacy.d.ts.map +0 -1
- package/node_modules/@noble/hashes/legacy.js +0 -287
- package/node_modules/@noble/hashes/legacy.js.map +0 -1
- package/node_modules/@noble/hashes/package.json +0 -266
- package/node_modules/@noble/hashes/pbkdf2.d.ts +0 -23
- package/node_modules/@noble/hashes/pbkdf2.d.ts.map +0 -1
- package/node_modules/@noble/hashes/pbkdf2.js +0 -101
- package/node_modules/@noble/hashes/pbkdf2.js.map +0 -1
- package/node_modules/@noble/hashes/ripemd160.d.ts +0 -13
- package/node_modules/@noble/hashes/ripemd160.d.ts.map +0 -1
- package/node_modules/@noble/hashes/ripemd160.js +0 -16
- package/node_modules/@noble/hashes/ripemd160.js.map +0 -1
- package/node_modules/@noble/hashes/scrypt.d.ts +0 -34
- package/node_modules/@noble/hashes/scrypt.d.ts.map +0 -1
- package/node_modules/@noble/hashes/scrypt.js +0 -232
- package/node_modules/@noble/hashes/scrypt.js.map +0 -1
- package/node_modules/@noble/hashes/sha1.d.ts +0 -11
- package/node_modules/@noble/hashes/sha1.d.ts.map +0 -1
- package/node_modules/@noble/hashes/sha1.js +0 -14
- package/node_modules/@noble/hashes/sha1.js.map +0 -1
- package/node_modules/@noble/hashes/sha2.d.ts +0 -159
- package/node_modules/@noble/hashes/sha2.d.ts.map +0 -1
- package/node_modules/@noble/hashes/sha2.js +0 -384
- package/node_modules/@noble/hashes/sha2.js.map +0 -1
- package/node_modules/@noble/hashes/sha256.d.ts +0 -20
- package/node_modules/@noble/hashes/sha256.d.ts.map +0 -1
- package/node_modules/@noble/hashes/sha256.js +0 -23
- package/node_modules/@noble/hashes/sha256.js.map +0 -1
- package/node_modules/@noble/hashes/sha3-addons.d.ts +0 -142
- package/node_modules/@noble/hashes/sha3-addons.d.ts.map +0 -1
- package/node_modules/@noble/hashes/sha3-addons.js +0 -402
- package/node_modules/@noble/hashes/sha3-addons.js.map +0 -1
- package/node_modules/@noble/hashes/sha3.d.ts +0 -53
- package/node_modules/@noble/hashes/sha3.d.ts.map +0 -1
- package/node_modules/@noble/hashes/sha3.js +0 -239
- package/node_modules/@noble/hashes/sha3.js.map +0 -1
- package/node_modules/@noble/hashes/sha512.d.ts +0 -26
- package/node_modules/@noble/hashes/sha512.d.ts.map +0 -1
- package/node_modules/@noble/hashes/sha512.js +0 -29
- package/node_modules/@noble/hashes/sha512.js.map +0 -1
- package/node_modules/@noble/hashes/src/_assert.ts +0 -22
- package/node_modules/@noble/hashes/src/_blake.ts +0 -50
- package/node_modules/@noble/hashes/src/_md.ts +0 -176
- package/node_modules/@noble/hashes/src/_u64.ts +0 -91
- package/node_modules/@noble/hashes/src/argon2.ts +0 -497
- package/node_modules/@noble/hashes/src/blake1.ts +0 -534
- package/node_modules/@noble/hashes/src/blake2.ts +0 -486
- package/node_modules/@noble/hashes/src/blake2b.ts +0 -10
- package/node_modules/@noble/hashes/src/blake2s.ts +0 -20
- package/node_modules/@noble/hashes/src/blake3.ts +0 -272
- package/node_modules/@noble/hashes/src/crypto.ts +0 -9
- package/node_modules/@noble/hashes/src/cryptoNode.ts +0 -15
- package/node_modules/@noble/hashes/src/eskdf.ts +0 -187
- package/node_modules/@noble/hashes/src/hkdf.ts +0 -88
- package/node_modules/@noble/hashes/src/hmac.ts +0 -94
- package/node_modules/@noble/hashes/src/index.ts +0 -31
- package/node_modules/@noble/hashes/src/legacy.ts +0 -293
- package/node_modules/@noble/hashes/src/pbkdf2.ts +0 -122
- package/node_modules/@noble/hashes/src/ripemd160.ts +0 -12
- package/node_modules/@noble/hashes/src/scrypt.ts +0 -257
- package/node_modules/@noble/hashes/src/sha1.ts +0 -10
- package/node_modules/@noble/hashes/src/sha2.ts +0 -402
- package/node_modules/@noble/hashes/src/sha256.ts +0 -24
- package/node_modules/@noble/hashes/src/sha3-addons.ts +0 -499
- package/node_modules/@noble/hashes/src/sha3.ts +0 -258
- package/node_modules/@noble/hashes/src/sha512.ts +0 -34
- package/node_modules/@noble/hashes/src/utils.ts +0 -395
- package/node_modules/@noble/hashes/utils.d.ts +0 -161
- package/node_modules/@noble/hashes/utils.d.ts.map +0 -1
- package/node_modules/@noble/hashes/utils.js +0 -313
- package/node_modules/@noble/hashes/utils.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../node_modules/@blockcast/mmt-transport/src/types.ts", "../node_modules/@blockcast/mmt-
|
|
4
|
-
"sourcesContent": ["/**\n * @blockcast/transport - Type Definitions\n *\n * Shared interfaces for unified transport (WebTransport, SSM, AMT, DRIAD) and FEC.\n */\n\n/** Native IP-multicast request for the window.multicast gateway. */\nexport interface WindowMulticastSubscribeConfigBytes {\n\tgroup: string;\n\tport: number;\n\tsource?: string;\n\tconsume: \"bytes\";\n\torigin?: \"ip-multicast\";\n\t/** MMTP packet IDs to forward before data enters the page transport queue. */\n\tpacketIds?: readonly number[];\n\t/** Catalog track identities corresponding to the selected packet IDs. */\n\ttracks?: ReadonlyArray<{ name: string; packetId: number }>;\n}\n\n/** AMT relay request for the window.multicast gateway. */\nexport interface WindowMulticastSubscribeConfigAmt {\n\tgroup: string;\n\tport: number;\n\tsource?: string;\n\tconsume: \"bytes\";\n\torigin: \"amt\";\n\trelay: string;\n\trelayPort: number;\n\t/** MMTP packet IDs to forward before data enters the page transport queue. */\n\tpacketIds?: readonly number[];\n}\n\n/** Canonical window.multicast config for byte subscriptions. */\nexport type WindowMulticastSubscribeConfig =\n\t| WindowMulticastSubscribeConfigBytes\n\t| WindowMulticastSubscribeConfigAmt;\n\nexport type WindowMulticastSubscriptionState = \"connecting\" | \"connected\" | \"reconnecting\" | \"closed\";\n\nexport interface WindowMulticastSubscriptionEventMap {\n\tbytes: CustomEvent<ArrayBuffer>;\n\t/**\n\t * Provider extension, not part of the frozen v1 contract (BLO-36599): every\n\t * packet the provider decoded in one dispatch batch, in arrival order. Each\n\t * element is exactly what one `bytes` event would have carried. Emitted\n\t * only by a subscription whose `eventTypes` lists `\"packets\"`; see\n\t * {@link subscriptionDeliversPacketBatches}. A consumer listens to exactly\n\t * one of `bytes` and `packets`, because both carry the same packets.\n\t */\n\tpackets: CustomEvent<readonly ArrayBuffer[]>;\n\tstate: CustomEvent<WindowMulticastSubscriptionState>;\n\terror: CustomEvent<unknown>;\n}\n\n/**\n * How a window.multicast subscription hands packets to the page.\n *\n * - `\"datagram\"`: packets are independent datagrams; a pause in arrivals may\n * mean loss.\n * - `\"ordered-relay\"`: the provider relays one receiving socket over a\n * reliable, ordered stream (for example a BCDS bridge WebTransport uni\n * stream). The relay may drop packets (a bounded queue discarding its\n * oldest on overflow) but never reorders them: what arrives is a\n * subsequence of the socket's receive order, so a pause in arrivals\n * carries no loss information.\n *\n * A provider declares `\"ordered-relay\"` only when every packet of the\n * subscription travels that one ordered stream for the life of the\n * subscription. Absent means no ordering promise, which consumers treat as\n * `\"datagram\"`.\n */\nexport type WindowMulticastDelivery = \"datagram\" | \"ordered-relay\";\n\n/** window.multicast subscription returned by the IWA gateway surface. */\nexport interface WindowMulticastSubscription {\n\treadonly id?: string;\n\treadonly state?: WindowMulticastSubscriptionState;\n\t/** How this subscription delivers packets; see WindowMulticastDelivery. */\n\treadonly delivery?: WindowMulticastDelivery;\n\t/**\n\t * Events this subscription emits, when the provider advertises them. A\n\t * provider without the field emits the v1 events only (`bytes`, `state`,\n\t * `error`).\n\t */\n\treadonly eventTypes?: readonly string[];\n\taddEventListener<K extends keyof WindowMulticastSubscriptionEventMap>(\n\t\ttype: K,\n\t\tlistener: (ev: WindowMulticastSubscriptionEventMap[K]) => void,\n\t\toptions?: AddEventListenerOptions | boolean,\n\t): void;\n\tremoveEventListener<K extends keyof WindowMulticastSubscriptionEventMap>(\n\t\ttype: K,\n\t\tlistener: (ev: WindowMulticastSubscriptionEventMap[K]) => void,\n\t\toptions?: EventListenerOptions | boolean,\n\t): void;\n\tunsubscribe: () => Promise<void> | void;\n}\n\nexport interface WindowMulticastCapabilities {\n\treadonly consumeModes?: ReadonlyArray<\"bytes\" | string>;\n}\n\n/** Lowercase window.multicast gateway surface. */\nexport interface WindowMulticastGateway {\n\tsubscribe?: (config: WindowMulticastSubscribeConfig) => Promise<WindowMulticastSubscription>;\n\tcapabilities?: WindowMulticastCapabilities | (() => Promise<WindowMulticastCapabilities>);\n}\n\ndeclare global {\n\tinterface Window {\n\t\tmulticast?: WindowMulticastGateway;\n\t\tUDPSocket?: unknown;\n\t}\n}\n\n/** Transport configuration for multicast subscriptions */\nexport interface TransportConfig {\n\t/** Multicast group address (232.x.x.x for SSM) */\n\tgroup: string;\n\t/** Source address for SSM filtering */\n\tsource: string;\n\t/** UDP port */\n\tport: number;\n\t/** Stream type */\n\tstreamType: \"video\" | \"audio\";\n\t/** Optional AMT relay host for unicast tunneling */\n\tamtRelay?: string;\n\t/** Optional AMT relay port. */\n\tamtRelayPort?: number;\n\t/** Browser gateway transport preference. */\n\tmode?: \"auto\" | \"native\" | \"tunnel\";\n\t/** DRIAD discovery enabled */\n\tdriadEnabled?: boolean;\n}\n\n/** What `TransportManager.connect()` needs beyond the subscription itself. */\nexport interface TransportConnectConfig extends TransportConfig {\n\t/**\n\t * The stream's renditions the ABR controller adapts over, e.g. its catalog\n\t * video tracks. Required: there is no built-in ladder. `connect()` checks\n\t * it (`validateQualityLadder`) and `initialQuality` before tearing anything\n\t * down; a config that fails is refused through the error callback and\n\t * `connect()` resolves `false`, leaving a live subscription untouched.\n\t * `catalogQualityLadder(catalog)` builds it from a catalog.\n\t */\n\tabrLadder: readonly QualityLevel[];\n\t/**\n\t * Ladder rendition to start on, matched by `name` against `abrLadder`; the\n\t * lowest-bitrate rung when omitted. A name `abrLadder` does not carry is\n\t * not a fallback to that rung: `TransportManager.connect()` refuses the\n\t * config (error callback, resolves `false`) before tearing anything down.\n\t */\n\tinitialQuality?: QualityLevel;\n}\n\nexport interface QualityLevel {\n\tname: string;\n\t/** Bits per second: what the ABR controller ranks and costs a rendition by. */\n\tbitrate: number;\n\t/**\n\t * Dimensions and frame rate as the stream's catalog states them; absent\n\t * when it does not. ABR never reads them, so none is defaulted.\n\t */\n\twidth?: number;\n\theight?: number;\n\tfps?: number;\n}\n\nexport interface TransportCapabilities {\n\tnativeSSM: boolean;\n\tnativeIGMP: boolean;\n\tdirectSockets: boolean;\n\tamt: boolean;\n\tdriad: boolean;\n\tfecDecode: boolean;\n}\n\nexport type TransportMode = \"multicast\" | \"amt\" | \"moq\" | \"disconnected\";\n\nexport interface TransportState {\n\tmode: TransportMode;\n\tconnected: boolean;\n\tgroup?: string;\n\tsource?: string;\n\tamtRelay?: string;\n\tquality?: QualityLevel;\n\tpacketsReceived: number;\n\tbytesReceived: number;\n\tlastPacketTime: number;\n}\n\n/** Transport subscription handle */\nexport interface TransportSubscription {\n\tid: string;\n\tconfig: TransportConfig;\n\tonpacket?: (data: ArrayBuffer) => void;\n\tonerror?: (error: Error) => void;\n\tonqualitychange?: (quality: QualityLevel) => void;\n\tclose: () => void;\n}\n\n/** DRIAD Relay information (RFC 8777) */\nexport interface DRIADRelay {\n\thost: string;\n\tport: number;\n\tpriority: number;\n\tweight: number;\n\tasn: number;\n\trtt?: number;\n}\n\n/** AMT Relay information */\nexport interface AmtRelayInfo {\n\taddress: string;\n\tport: number;\n\tdiscoveryMethod: \"driad\" | \"manual\";\n\tdiscoveredAt: number;\n}\n\n/** Group key for multicast subscriptions */\nexport interface GroupKey {\n\tgroup: string;\n\tport: number;\n\tsource?: string;\n}\n\n/** Packet metadata */\nexport interface PacketMetadata {\n\tsourceAddress: string;\n\tsourcePort: number;\n\ttransport: \"native\" | \"tunnel\" | \"websocket\";\n\ttimestamp: number;\n}\n\n/** Time types (microseconds) */\nexport type Micro = number & { readonly __micro: unique symbol };\n\n/** Sync decision for A/V synchronization */\nexport type SyncDecision = \"render\" | \"hold\" | \"discard\";\n\n// ============================================================\n// Multicast Bridge Types (consolidated from @blockcast/multicast)\n// ============================================================\n\n/** Transport mode for multicast subscription (auto-selection) */\nexport type MulticastTransportMode = \"auto\" | \"native\" | \"tunnel\";\n\n/** Multicast subscription configuration */\nexport interface MulticastConfig {\n\tgroup: string;\n\tport: number;\n\tsource?: string;\n\tmode?: MulticastTransportMode;\n\tonPacket: (data: ArrayBuffer, source: PacketSource) => void;\n\tonStateChange?: (state: SubscriptionState) => void;\n\tonError?: (error: Error) => void;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\n\tnativeFallbackTimeout?: number;\n}\n\n/** Packet source information */\nexport interface PacketSource {\n\taddress: string;\n\tport: number;\n\ttransport: \"native\" | \"tunnel\";\n\trecovered?: boolean;\n}\n\n/** Subscription state */\nexport type SubscriptionState = \"initializing\" | \"joining\" | \"active\" | \"suspended\" | \"closed\" | \"error\";\n\n/** Subscription statistics */\nexport interface SubscriptionStats {\n\tpacketsReceived: number;\n\tbytesReceived: number;\n\tpacketsLost: number;\n\tpacketsRecovered: number;\n\tbitrate: number;\n\tuptime: number;\n}\n\n/** Multicast subscription handle */\nexport interface MulticastSubscriptionHandle {\n\treadonly state: SubscriptionState;\n\treadonly group: string;\n\treadonly source?: string;\n\treadonly port: number;\n\treadonly transport: \"native\" | \"tunnel\";\n\treadonly stats: SubscriptionStats;\n\tclose(): Promise<void>;\n}\n\n/** Main Multicast Bridge API */\nexport interface MulticastBridge {\n\tsubscribe(config: MulticastConfig): Promise<MulticastSubscriptionHandle>;\n\tgetSubscriptions(): Promise<MulticastSubscriptionHandle[]>;\n\tisNativeSupported(): Promise<boolean>;\n}\n\n// ============================================================\n// FEC Constants and Utilities per draft-ramadan-moq-fec-00\n// ============================================================\n\n/**\n * Build a conventional repair track path by appending \"repair\" to a source\n * track path.\n *\n * Track names carry no protocol meaning. A repair track's layer, its source\n * and its symbol budget are resolved from the catalog via `resolveRepairLayers`\n * in `@blockcast/mmt-container`, never from a name or a name suffix. This\n * helper is a naming convenience for producers only.\n *\n * @deprecated Naming convenience only. Never classify a track or resolve a\n * repair layer from its path; use `resolveRepairLayers` from\n * `@blockcast/mmt-container`.\n *\n * @param sourceTrack - The source track path (e.g., \"live/video\")\n * @returns The repair track path (e.g., \"live/video/repair\")\n *\n * @example\n * repairTrackPath(\"live/video\") // returns \"live/video/repair\"\n * repairTrackPath(\"broadcast/video/1080p\") // returns \"broadcast/video/1080p/repair\"\n */\nexport function repairTrackPath(sourceTrack: string): string {\n\tif (!sourceTrack || sourceTrack.length === 0) {\n\t\treturn \"repair\";\n\t}\n\treturn `${sourceTrack}/repair`;\n}\n\n/**\n * MoQ FEC Configuration per draft-ramadan-moq-fec-00\n *\n * These parameters are signalled in each track's catalog `fec` block;\n * no FEC_CONFIG control message is sent or parsed.\n */\nexport interface MoqFecConfig {\n\t/** Subscribe ID this config applies to */\n\tsubscribeId: number;\n\t/** Whether FEC is enabled */\n\tenabled: boolean;\n\t/** FEC algorithm (0=none, 1=XOR, 2=RaptorQ) */\n\talgorithm: MoqFecAlgorithm;\n\t/** Number of source symbols per FEC block (K) */\n\tsourceSymbolsPerBlock: number;\n\t/** Number of repair symbols per FEC block (P) */\n\trepairSymbolsPerBlock: number;\n\t/** Interleave depth for burst loss protection */\n\tinterleaveDepth: number;\n\t/** Symbol size in bytes */\n\tsymbolSize: number;\n\t/** RFC 6330 Object Transmission Information (12 bytes) - only for RaptorQ */\n\toti?: Uint8Array;\n}\n\n/**\n * FEC algorithm identifier per draft-ramadan-moq-fec-00\n */\nexport enum MoqFecAlgorithm {\n\t/** No FEC (disabled) */\n\tNone = 0,\n\t/** Simple XOR parity */\n\tXor = 1,\n\t/** RaptorQ (RFC 6330) - recommended for burst loss */\n\tRaptorQ = 2,\n}\n\n/**\n * Repair object header per draft-ramadan-moq-fec-00 Section 6.1\n *\n * Each object on a repair track contains repair symbols for one source block.\n * The object payload begins with this header, followed by concatenated repair symbols.\n */\nexport interface RepairObjectHeader {\n\t/** Source block number being protected (matches group_id of source objects) */\n\tsourceBlockNumber: number;\n\t/** First Repair Symbol ESI (Encoding Symbol ID) - for RaptorQ, repair ESIs >= K */\n\tfirstRepairEsi: number;\n\t/** Number of repair symbols in this object */\n\tnumRepairSymbols: number;\n}\n", "/**\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 * AMT well-known UDP port (RFC 7450 \u00A74).\n *\n * The single definition for the whole repo. It lives here, in the one package\n * both `@blockcast/mmt-transport` and `@blockcast/mmt-container` already\n * depend on, because the two apply it on opposite sides of a boundary that\n * does not otherwise share code:\n *\n * - container resolves it where `networkSource` semantics live, so a catalog\n * relay that names no port is reachable instead of handing a join a\n * portless relay that fails.\n * - transport applies it when reading a catalog, dialling a gateway, and\n * answering DRIAD \u2014 but deliberately not at every boundary. Which call\n * sites do and do not take the default is transport's business; the\n * reasoning lives beside those call sites, not here.\n */\nexport const DEFAULT_AMT_RELAY_PORT = 2268;\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", "/**\n * @blockcast/transport - DRIAD Discovery (RFC 8777)\n *\n * DRIAD (DNS Reverse IP AMT Discovery) discovers AMT relays based on the\n * **source address**, NOT the multicast group. The source network operator\n * configures DNS records for their source IPs to advertise which AMT relay(s)\n * can tunnel their traffic.\n *\n * Example: For source 69.25.95.10 sending to group 232.0.0.1:\n * Query: 10.95.25.69.amt.in-addr.arpa (source-based, NOT group-based)\n */\n\nimport { DEFAULT_AMT_RELAY_PORT } from \"@blockcast/mmt-base\";\nimport type { DRIADRelay } from \"./types.js\";\n\n/**\n * DRIAD (DNS Reverse IP AMT Discovery) - RFC 8777\n * Discovers AMT relays based on source address via DNS lookups\n */\nexport class DRIADDiscovery {\n\tprivate cache = new Map<string, DRIADRelay[]>();\n\tprivate cacheExpiry = 300_000; // 5 minutes\n\n\t/**\n\t * Discover AMT relays for a multicast source address (RFC 8777)\n\t *\n\t * @param source - The multicast source IP address (NOT the group!)\n\t * @returns Array of discovered AMT relays that can tunnel traffic from this source\n\t */\n\tasync discoverRelays(source: string): Promise<DRIADRelay[]> {\n\t\t// Check cache first\n\t\tconst cached = this.cache.get(source);\n\t\tif (cached) {\n\t\t\treturn cached;\n\t\t}\n\n\t\ttry {\n\t\t\t// Convert source to DRIAD format (reversed octets)\n\t\t\tconst reversedSource = source.split(\".\").reverse().join(\".\");\n\t\t\tconst queryDomain = `${reversedSource}.amt.in-addr.arpa`;\n\n\t\t\tconsole.log(`[DRIAD] Discovering relays for source ${source} via ${queryDomain}`);\n\n\t\t\t// In browser, we use a DRIAD proxy service or DNS-over-HTTPS\n\t\t\tconst relays = await this.queryDRIADProxy(source);\n\n\t\t\tif (relays.length > 0) {\n\t\t\t\tthis.cache.set(source, relays);\n\t\t\t\tsetTimeout(() => this.cache.delete(source), this.cacheExpiry);\n\t\t\t}\n\n\t\t\treturn relays;\n\t\t} catch (error) {\n\t\t\tconsole.warn(`[DRIAD] Discovery failed for source ${source}:`, error);\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t/**\n\t * Query DRIAD proxy service (browser-compatible)\n\t */\n\tprivate async queryDRIADProxy(source: string): Promise<DRIADRelay[]> {\n\t\t// List of known DRIAD proxy endpoints\n\t\tconst proxyEndpoints = [\n\t\t\t\"https://driad.amt.net/api/v1/relays\",\n\t\t\t\"https://multicast-gateway.cloudflare.com/driad\",\n\t\t];\n\n\t\tfor (const endpoint of proxyEndpoints) {\n\t\t\ttry {\n\t\t\t\t// Query by source address per RFC 8777\n\t\t\t\tconst response = await fetch(`${endpoint}?source=${encodeURIComponent(source)}`, {\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\theaders: { Accept: \"application/json\" },\n\t\t\t\t\tsignal: AbortSignal.timeout(5000),\n\t\t\t\t});\n\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = (await response.json()) as { relays: DRIADRelay[] };\n\t\t\t\t\treturn data.relays || [];\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Try next proxy endpoint\n\t\t\t}\n\t\t}\n\n\t\t// Fallback: Try DNS-over-HTTPS query\n\t\tconst dohRelay = await this.queryDNSOverHTTPS(source);\n\t\tif (dohRelay) {\n\t\t\treturn [dohRelay];\n\t\t}\n\n\t\treturn [];\n\t}\n\n\t/**\n\t * Query DNS-over-HTTPS for DRIAD relay based on source address (RFC 8777)\n\t */\n\tprivate async queryDNSOverHTTPS(source: string): Promise<DRIADRelay | null> {\n\t\ttry {\n\t\t\t// Build DRIAD query name from source address (reversed octets)\n\t\t\tconst reversedSource = source.split(\".\").reverse().join(\".\");\n\t\t\tconst queryName = `${reversedSource}.amt.in-addr.arpa`;\n\n\t\t\tconst dohUrl = `https://cloudflare-dns.com/dns-query?name=${queryName}&type=A`;\n\t\t\tconst response = await fetch(dohUrl, {\n\t\t\t\theaders: { Accept: \"application/dns-json\" },\n\t\t\t\tsignal: AbortSignal.timeout(3000),\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst result = (await response.json()) as { Answer?: Array<{ data: string }> };\n\n\t\t\t// Extract first A/AAAA record\n\t\t\tif (result.Answer && result.Answer.length > 0) {\n\t\t\t\treturn {\n\t\t\t\t\thost: result.Answer[0].data,\n\t\t\t\t\tport: DEFAULT_AMT_RELAY_PORT,\n\t\t\t\t\tpriority: 10,\n\t\t\t\t\tweight: 100,\n\t\t\t\t\tasn: 0,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn null;\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[DRIAD] DNS-over-HTTPS query failed:\", error);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * Select best relay based on RTT measurements\n\t */\n\tasync selectBestRelay(relays: DRIADRelay[]): Promise<DRIADRelay | null> {\n\t\tif (relays.length === 0) return null;\n\n\t\t// Measure RTT to each relay\n\t\tconst measured = await Promise.all(\n\t\t\trelays.map(async (relay) => {\n\t\t\t\tconst rtt = await this.measureRTT(relay.host, relay.port);\n\t\t\t\treturn { ...relay, rtt };\n\t\t\t}),\n\t\t);\n\n\t\t// Sort by priority, then by RTT\n\t\tmeasured.sort((a, b) => {\n\t\t\tif (a.priority !== b.priority) return a.priority - b.priority;\n\t\t\treturn (a.rtt || Infinity) - (b.rtt || Infinity);\n\t\t});\n\n\t\tconsole.log(`[DRIAD] Selected relay: ${measured[0].host} (RTT: ${measured[0].rtt}ms)`);\n\t\treturn measured[0];\n\t}\n\n\tprivate async measureRTT(host: string, _port: number): Promise<number> {\n\t\tconst start = performance.now();\n\t\ttry {\n\t\t\t// Use HTTP/HTTPS probe since we can't do UDP from browser\n\t\t\tawait fetch(`https://${host}/health`, {\n\t\t\t\tmethod: \"HEAD\",\n\t\t\t\tsignal: AbortSignal.timeout(2000),\n\t\t\t\tmode: \"no-cors\",\n\t\t\t});\n\t\t\treturn performance.now() - start;\n\t\t} catch {\n\t\t\treturn Infinity;\n\t\t}\n\t}\n\n\t/**\n\t * Clear the relay cache\n\t */\n\tclearCache(): void {\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Get cached relays for a source address\n\t */\n\tgetCached(source: string): DRIADRelay[] | undefined {\n\t\treturn this.cache.get(source);\n\t}\n}\n\n// Export singleton instance\nexport const driadDiscovery = new DRIADDiscovery();\n", "export interface MulticastTierEndpoint {\n\tsourceAddress?: string;\n\tgroupAddress: string;\n\tport: number;\n\ttracks: { name: string; packetId?: number }[];\n\tbandwidth?: number;\n\tprotocol?: string;\n\t/** False when every track on this endpoint is a `fec-repair` track, so the\n\t * endpoint carries repair symbols only and no media. A repair-layer\n\t * endpoint is joined by the loss-driven layer policy, never by the\n\t * bandwidth ladder: selecting one as a tier would leave the receiver with\n\t * no media at all. Set by the catalog policy, which resolves repair-track\n\t * membership from `depends`/`fec.repairTrack` rather than from names.\n\t * Undefined means \"not classified\" and is treated as carrying media, so a\n\t * caller that builds endpoints without the policy is unaffected. */\n\tcarriesMedia?: boolean;\n}\n\nexport function endpointKey(endpoint: MulticastTierEndpoint): string {\n\treturn `${endpoint.sourceAddress ?? \"\"}|${endpoint.groupAddress}|${endpoint.port}`;\n}\n\n/**\n * Whether this endpoint may be selected as a media tier.\n *\n * Every path that picks a tier must apply this, not only `selectMulticastTier`:\n * the orchestrator's headroom up-selection ranks endpoints itself, because it\n * advances one adjacent rung rather than fitting a bandwidth budget. Exported\n * so both rankings share one definition of \"carries media\" instead of\n * repeating the comparison and drifting apart.\n */\nexport function carriesMedia(endpoint: MulticastTierEndpoint): boolean {\n\treturn endpoint.carriesMedia !== false;\n}\n\nexport function selectMulticastTier(\n\tendpoints: readonly MulticastTierEndpoint[],\n\testimate: number | undefined,\n): MulticastTierEndpoint | undefined {\n\t// Repair-only endpoints are never a media tier. Filtering once here covers\n\t// every return path below, including the two early returns and the fallback:\n\t// a ladder that has only repair-layer endpoints returns undefined rather\n\t// than joining one and receiving no media.\n\tconst media = endpoints.filter(carriesMedia);\n\tif (media.length <= 1) return media[0];\n\n\tconst ranked = media\n\t\t.filter((endpoint) => typeof endpoint.bandwidth === \"number\" && endpoint.bandwidth > 0)\n\t\t.sort((a, b) => a.bandwidth! - b.bandwidth!);\n\tif (ranked.length < 2) return media[0];\n\tif (estimate === undefined) return ranked[0];\n\n\tconst budget = estimate * 0.8;\n\tlet selected: MulticastTierEndpoint | undefined;\n\tfor (const endpoint of ranked) {\n\t\tif (endpoint.bandwidth! <= budget) selected = endpoint;\n\t}\n\treturn selected ?? ranked[0];\n}\n", "/**\n * @blockcast/transport - Adaptive Bitrate Controller\n *\n * Progressive adaptive bitrate increase based on network conditions.\n * Monitors received bandwidth and packet loss to adjust quality levels.\n *\n * Per draft-ramadan-moq-fec-00, ABR decisions should use the **effective loss rate**\n * after FEC recovery, not the raw transport-level loss rate. This allows FEC to\n * mask burst losses without unnecessarily downgrading quality.\n */\n\nimport type { QualityLevel } from \"./types.js\";\nimport { selectMulticastTier, type MulticastTierEndpoint } from \"./multicast-tier.js\";\n\n/**\n * FEC statistics for ABR decisions\n */\nexport interface FecStatsForAbr {\n\t/** Raw loss rate before FEC recovery (0.0-1.0) */\n\trawLossRate: number;\n\t/** Effective loss rate after FEC recovery (0.0-1.0) */\n\teffectiveLossRate: number;\n\t/** FEC recovery rate (percentage of lost packets recovered) */\n\trecoveryRate: number;\n\t/** Whether FEC is enabled */\n\tfecEnabled: boolean;\n}\n\n/**\n * A generic 240p..4K ladder, kept as public API only.\n *\n * @deprecated Not a stream's ladder and never a default: the controller\n * adapts over the ladder its caller supplies (`AdaptiveBitrateControllerOptions.ladder`,\n * e.g. the catalog's video renditions). Walking this list is how a player on a\n * 360p/480p/720p catalog logged upgrades to 1080p and 1440p it could never\n * switch to (BLO-35597).\n */\nexport const QUALITY_LEVELS: readonly QualityLevel[] = Object.freeze([\n\t{ name: \"240p\", bitrate: 300_000, width: 426, height: 240, fps: 15 },\n\t{ name: \"360p\", bitrate: 500_000, width: 640, height: 360, fps: 24 },\n\t{ name: \"480p\", bitrate: 1_000_000, width: 854, height: 480, fps: 30 },\n\t{ name: \"720p\", bitrate: 2_500_000, width: 1280, height: 720, fps: 30 },\n\t{ name: \"1080p\", bitrate: 5_000_000, width: 1920, height: 1080, fps: 30 },\n\t{ name: \"1440p\", bitrate: 8_000_000, width: 2560, height: 1440, fps: 30 },\n\t{ name: \"4K\", bitrate: 15_000_000, width: 3840, height: 2160, fps: 30 },\n].map((level) => Object.freeze(level)));\n\n/** What the controller adapts over and where it starts; both from the caller. */\nexport interface AdaptiveBitrateControllerOptions {\n\t/**\n\t * The renditions this stream carries (e.g. the catalog's video tracks), in\n\t * any order: a unique name and a bitrate (bps) each, plus dimensions and\n\t * fps when the catalog states them. Renditions may share a bitrate.\n\t */\n\tladder: readonly QualityLevel[];\n\t/** Name of the ladder rendition presented at construction; the lowest\n\t * rung when omitted. A name that matches no rung is refused. */\n\tinitialQuality?: string;\n}\n\nfunction positiveFinite(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n\n/**\n * Validate a caller ladder and return it frozen, ascending by bitrate. Throws\n * on an empty ladder, a rung without a name or a positive finite bitrate, a\n * width, height or fps that is present but not positive finite, or a name that\n * appears twice: a name is how `setQuality` and the initial rendition find a\n * rung. A shared bitrate is legal (e.g. 720p30 and 720p60, or two codecs);\n * `Array.prototype.sort` is stable, so equal bitrates keep the caller's order.\n */\nexport function validateQualityLadder(ladder: readonly QualityLevel[]): readonly QualityLevel[] {\n\tif (!Array.isArray(ladder) || ladder.length === 0) {\n\t\tthrow new TypeError(\"[ABR] ladder must list at least one rendition (e.g. the catalog's video tracks)\");\n\t}\n\tconst names = new Set<string>();\n\tfor (const level of ladder) {\n\t\tif (typeof level?.name !== \"string\" || level.name.length === 0) {\n\t\t\tthrow new TypeError(\"[ABR] every ladder rendition needs a name\");\n\t\t}\n\t\tif (!positiveFinite(level.bitrate)) {\n\t\t\tthrow new RangeError(`[ABR] rendition ${level.name}: bitrate must be a positive finite number, got ${String(level.bitrate)}`);\n\t\t}\n\t\tfor (const field of [\"width\", \"height\", \"fps\"] as const) {\n\t\t\tif (level[field] !== undefined && !positiveFinite(level[field])) {\n\t\t\t\tthrow new RangeError(`[ABR] rendition ${level.name}: ${field} must be a positive finite number when stated, got ${String(level[field])}`);\n\t\t\t}\n\t\t}\n\t\tif (names.has(level.name)) throw new RangeError(`[ABR] rendition ${level.name} appears twice in the ladder`);\n\t\tnames.add(level.name);\n\t}\n\treturn Object.freeze(\n\t\t[...ladder].sort((a, b) => a.bitrate - b.bitrate).map((level) => Object.freeze({ ...level })),\n\t);\n}\n\n/**\n * Progressive Adaptive Bitrate Controller\n *\n * Features:\n * - Monitors bandwidth via received bytes over time window\n * - Tracks packet loss rate (raw and effective after FEC)\n * - Uses effective loss rate for ABR decisions per draft-ramadan-moq-fec-00\n * - Progressive quality upgrades (waits for stability)\n * - Fast quality downgrades on congestion\n */\nexport class AdaptiveBitrateController {\n\t/** The caller's renditions, ascending by bitrate (`validateQualityLadder`). */\n\treadonly ladder: readonly QualityLevel[];\n\tprivate currentQualityIndex: number;\n\tprivate history: { timestamp: number; bytesReceived: number; packetsLost: number }[] = [];\n\tprivate readonly historyWindow = 10_000; // 10 second window\n\tprivate readonly upgradeThreshold = 0.8; // 80% of target bitrate sustained\n\tprivate readonly downgradeThreshold = 0.5; // 50% of target bitrate triggers downgrade\n\tprivate readonly stabilityPeriod = 5_000; // Wait 5s before upgrading\n\n\tprivate lastQualityChange = 0;\n\tprivate onQualityChange?: (quality: QualityLevel) => void;\n\n\t// FEC stats for effective loss rate calculation\n\tprivate fecStats: FecStatsForAbr = {\n\t\trawLossRate: 0,\n\t\teffectiveLossRate: 0,\n\t\trecoveryRate: 1,\n\t\tfecEnabled: false,\n\t};\n\n\tconstructor(options: AdaptiveBitrateControllerOptions) {\n\t\tif (!options || typeof options !== \"object\") {\n\t\t\tthrow new TypeError(\"[ABR] AdaptiveBitrateController needs { ladder, initialQuality }\");\n\t\t}\n\t\tthis.ladder = validateQualityLadder(options.ladder);\n\t\tthis.currentQualityIndex =\n\t\t\toptions.initialQuality === undefined ? 0 : this.ladder.findIndex((q) => q.name === options.initialQuality);\n\t\tif (this.currentQualityIndex < 0) {\n\t\t\tthrow new RangeError(`[ABR] initial rendition ${String(options.initialQuality)} is not in the ladder`);\n\t\t}\n\t}\n\n\tsetCallback(callback: (quality: QualityLevel) => void) {\n\t\tthis.onQualityChange = callback;\n\t}\n\n\tgetCurrentQuality(): QualityLevel {\n\t\treturn this.ladder[this.currentQualityIndex];\n\t}\n\n\t/** Selects the multicast (S,G) tier without replacing a player's unicast ABR manager. */\n\tselectMulticastTier(\n\t\tendpoints: readonly MulticastTierEndpoint[],\n\t\testimate: number | undefined = this.getMetrics().measuredBitrate || undefined,\n\t): MulticastTierEndpoint | undefined {\n\t\treturn selectMulticastTier(endpoints, estimate);\n\t}\n\n\t/**\n\t * Record network sample for bitrate adaptation\n\t *\n\t * @param bytesReceived - Bytes received in this sample\n\t * @param packetsLost - Packets lost (raw, before FEC recovery)\n\t */\n\trecordSample(bytesReceived: number, packetsLost: number) {\n\t\tconst now = Date.now();\n\n\t\tthis.history.push({ timestamp: now, bytesReceived, packetsLost });\n\n\t\t// Prune old samples\n\t\tthis.history = this.history.filter((s) => now - s.timestamp < this.historyWindow);\n\n\t\t// Calculate metrics\n\t\tconst { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();\n\t\tconst currentQuality = this.getCurrentQuality();\n\n\t\t// Per draft-ramadan-moq-fec-00: Use effective loss rate after FEC for ABR decisions\n\t\t// This allows FEC to mask burst losses without triggering unnecessary downgrades\n\t\tconst effectiveLossRate = this.fecStats.fecEnabled\n\t\t\t? this.fecStats.effectiveLossRate\n\t\t\t: rawLossRate;\n\n\t\t// Decision logic\n\t\tif (now - this.lastQualityChange < this.stabilityPeriod) {\n\t\t\treturn; // Wait for stability\n\t\t}\n\n\t\t// Check for downgrade - use effective loss rate (post-FEC)\n\t\t// Only downgrade if FEC cannot recover the losses\n\t\tif (effectiveLossRate > 0.05 || measuredBitrate < currentQuality.bitrate * this.downgradeThreshold) {\n\t\t\tthis.downgrade();\n\t\t\treturn;\n\t\t}\n\n\t\t// Check for upgrade (progressive increase) - use effective loss rate\n\t\tif (\n\t\t\tmeasuredBitrate > currentQuality.bitrate * this.upgradeThreshold &&\n\t\t\teffectiveLossRate < 0.01 &&\n\t\t\tthis.currentQualityIndex < this.ladder.length - 1\n\t\t) {\n\t\t\tthis.upgrade();\n\t\t}\n\t}\n\n\t/**\n\t * Update FEC statistics for ABR decisions\n\t *\n\t * Per draft-ramadan-moq-fec-00, ABR should use the effective loss rate\n\t * (after FEC recovery) rather than raw transport loss rate.\n\t *\n\t * @param stats - FEC statistics from the FEC decoder\n\t */\n\tsetFecStats(stats: FecStatsForAbr) {\n\t\tthis.fecStats = stats;\n\t}\n\n\t/**\n\t * Get current FEC statistics\n\t */\n\tgetFecStats(): FecStatsForAbr {\n\t\treturn { ...this.fecStats };\n\t}\n\n\tprivate calculateMetrics() {\n\t\tif (this.history.length < 2) {\n\t\t\treturn { measuredBitrate: 0, lossRate: 0 };\n\t\t}\n\n\t\tconst totalBytes = this.history.reduce((sum, s) => sum + s.bytesReceived, 0);\n\t\tconst totalLost = this.history.reduce((sum, s) => sum + s.packetsLost, 0);\n\t\tconst duration = (this.history[this.history.length - 1].timestamp - this.history[0].timestamp) / 1000;\n\n\t\tconst measuredBitrate = duration > 0 ? (totalBytes * 8) / duration : 0;\n\t\tconst lossRate = totalLost / (this.history.length + totalLost);\n\n\t\treturn { measuredBitrate, lossRate };\n\t}\n\n\tprivate upgrade() {\n\t\tif (this.currentQualityIndex >= this.ladder.length - 1) return;\n\n\t\tthis.currentQualityIndex++;\n\t\tthis.lastQualityChange = Date.now();\n\n\t\tconst newQuality = this.getCurrentQuality();\n\t\tconsole.log(`[ABR] Upgrading to ${newQuality.name} (${newQuality.bitrate / 1_000_000}Mbps)`);\n\n\t\tthis.onQualityChange?.(newQuality);\n\t}\n\n\tprivate downgrade() {\n\t\tif (this.currentQualityIndex <= 0) return;\n\n\t\tthis.currentQualityIndex--;\n\t\tthis.lastQualityChange = Date.now();\n\n\t\tconst newQuality = this.getCurrentQuality();\n\t\tconsole.log(`[ABR] Downgrading to ${newQuality.name} (${newQuality.bitrate / 1_000_000}Mbps)`);\n\n\t\tthis.onQualityChange?.(newQuality);\n\t}\n\n\t/**\n\t * Select a ladder rendition: the caller switched to it (a manual override,\n\t * or a switch the caller made). Throws for a name the ladder does not carry.\n\t */\n\tsetQuality(name: string) {\n\t\tconst index = this.ladder.findIndex((q) => q.name === name);\n\t\tif (index < 0) throw new RangeError(`[ABR] rendition ${name} is not in the ladder`);\n\t\tthis.currentQualityIndex = index;\n\t\tthis.lastQualityChange = Date.now();\n\t\tthis.onQualityChange?.(this.getCurrentQuality());\n\t}\n\n\t/**\n\t * Get current metrics including both raw and effective loss rates\n\t */\n\tgetMetrics(): {\n\t\tmeasuredBitrate: number;\n\t\trawLossRate: number;\n\t\teffectiveLossRate: number;\n\t\tfecEnabled: boolean;\n\t\tsampleCount: number;\n\t} {\n\t\tconst { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();\n\t\treturn {\n\t\t\tmeasuredBitrate,\n\t\t\trawLossRate,\n\t\t\teffectiveLossRate: this.fecStats.fecEnabled ? this.fecStats.effectiveLossRate : rawLossRate,\n\t\t\tfecEnabled: this.fecStats.fecEnabled,\n\t\t\tsampleCount: this.history.length,\n\t\t};\n\t}\n\n\t/**\n\t * Reset history and start fresh\n\t */\n\treset() {\n\t\tthis.history = [];\n\t\tthis.lastQualityChange = 0;\n\t}\n}\n", "import { compareNtpShort, isNtpShort } from \"@blockcast/mmt-base\";\nimport { carriesMedia, endpointKey, selectMulticastTier, type MulticastTierEndpoint } from \"./multicast-tier.js\";\n\nexport type OrchestratorState = \"stable\" | \"joining\";\nexport type OrchestratorCommand =\n\t| { kind: \"join-target\"; current: MulticastTierEndpoint; target: MulticastTierEndpoint }\n\t| { kind: \"commit-swap\"; target: MulticastTierEndpoint; old: MulticastTierEndpoint }\n\t| { kind: \"abandon-join\"; current: MulticastTierEndpoint; target: MulticastTierEndpoint };\n\nexport interface MulticastTierPacket {\n\tendpointKey: string;\n\tkeyframe: boolean;\n\t/**\n\t * The packet's MMTP header `timestamp`: its UTC sending instant in NTP short\n\t * format (ISO/IEC 23008-1 \u00A79.2.2), which wraps every 2^16 s (~18.2 h). The\n\t * orchestrator orders these only with RFC 1982 serial arithmetic\n\t * (`compareNtpShort`), never with `<` or `Math.max`.\n\t *\n\t * A non-finite value means the host could not read a header; that packet\n\t * neither raises the high-water mark nor commits. Any other value that is\n\t * not NTP short is a host bug and throws a RangeError.\n\t */\n\ttimestamp: number;\n\thasValidCodecConfig: boolean;\n}\n\nexport interface OrchestratorOptions {\n\tendpoints: readonly MulticastTierEndpoint[];\n\tinitialEndpoint?: MulticastTierEndpoint;\n\tadaptive?: boolean;\n\tnow: () => number;\n\tdwellMs?: number;\n\tjoinDeadlineMs?: number;\n}\n\nexport interface GoodputSample { bitrate: number | undefined; }\nexport interface HeadroomSample { healthy: boolean; }\n\nexport const DEFAULT_DWELL_MS = 3_000;\nexport const DEFAULT_JOIN_DEADLINE_MS = 5_000;\n\nexport function goodputFromStats(\n\tprevious: { bytesReceived: number; atMs: number } | undefined,\n\tcurrent: { bytesReceived: number; atMs: number },\n\tminSampleMs = 500,\n): number | undefined {\n\tif (!previous) return undefined;\n\tconst durationMs = current.atMs - previous.atMs;\n\tconst bytes = current.bytesReceived - previous.bytesReceived;\n\tif (durationMs < minSampleMs || bytes <= 0) return undefined;\n\treturn (bytes * 8 * 1000) / durationMs;\n}\n\nexport class MulticastOrchestrator {\n\t#state: OrchestratorState = \"stable\";\n\t#current: MulticastTierEndpoint | undefined;\n\t#target: MulticastTierEndpoint | undefined;\n\t#estimate: number | undefined;\n\t#favored: { key: string; since: number; source: \"goodput\" | \"headroom\" } | undefined;\n\t#joinStartedAt = 0;\n\t/**\n\t * Latest active-tier send time (NTP short), or undefined before the first.\n\t * \"Latest\" is serial order, not the numerically largest value: after the\n\t * field wraps, the largest value is the stale one.\n\t */\n\t#highWater: number | undefined;\n\treadonly #dwellMs: number;\n\treadonly #joinDeadlineMs: number;\n\n\tconstructor(private readonly options: OrchestratorOptions) {\n\t\tthis.#dwellMs = options.dwellMs ?? DEFAULT_DWELL_MS;\n\t\tthis.#joinDeadlineMs = options.joinDeadlineMs ?? DEFAULT_JOIN_DEADLINE_MS;\n\t\tif (!Number.isFinite(this.#dwellMs) || !Number.isFinite(this.#joinDeadlineMs) ||\n\t\t\tthis.#dwellMs <= 0 || this.#joinDeadlineMs <= 0) {\n\t\t\tthrow new Error(\"Multicast dwell and join deadline must be positive\");\n\t\t}\n\t}\n\n\tget state(): OrchestratorState { return this.#state; }\n\tget current(): MulticastTierEndpoint | undefined { return this.#current; }\n\tget target(): MulticastTierEndpoint | undefined { return this.#target; }\n\tget estimate(): number | undefined { return this.#estimate; }\n\n\tstart(): MulticastTierEndpoint | undefined {\n\t\tthis.#current ??= this.options.initialEndpoint ?? selectMulticastTier(this.options.endpoints, undefined);\n\t\treturn this.#current;\n\t}\n\n\tonGoodput(sample: GoodputSample): OrchestratorCommand | undefined {\n\t\tconst bitrate = sample.bitrate;\n\t\tif (!this.#current || this.options.adaptive === false) return undefined;\n\t\tif (this.#state === \"joining\") return this.poll();\n\t\tif (bitrate === undefined || !Number.isFinite(bitrate)) return undefined;\n\t\tthis.#estimate = bitrate;\n\t\tconst desired = selectMulticastTier(this.options.endpoints, bitrate);\n\t\tif (!desired) return undefined;\n\t\tconst desiredKey = endpointKey(desired);\n\t\tif (desiredKey === endpointKey(this.#current)) {\n\t\t\tif (this.#favored?.source === \"goodput\") this.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\t\tif (typeof desired.bandwidth !== \"number\" || typeof this.#current.bandwidth !== \"number\" ||\n\t\t\tdesired.bandwidth >= this.#current.bandwidth) {\n\t\t\tif (this.#favored?.source === \"goodput\") this.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst now = this.#now();\n\t\tif (!this.#favored || this.#favored.key !== desiredKey || this.#favored.source !== \"goodput\") {\n\t\t\tthis.#favored = { key: desiredKey, since: now, source: \"goodput\" };\n\t\t\treturn undefined;\n\t\t}\n\t\tif (now - this.#favored.since < this.#dwellMs) return undefined;\n\n\t\tthis.#state = \"joining\";\n\t\tthis.#target = desired;\n\t\tthis.#joinStartedAt = now;\n\t\tthis.#favored = undefined;\n\t\treturn { kind: \"join-target\", current: this.#current, target: desired };\n\t}\n\n\tonHeadroom(sample: HeadroomSample): OrchestratorCommand | undefined {\n\t\tif (!this.#current || this.options.adaptive === false || this.#state === \"joining\") return undefined;\n\t\tif (!sample.healthy) {\n\t\t\tif (this.#favored?.source === \"headroom\") this.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\t\tif (this.#favored?.source === \"goodput\") return undefined;\n\n\t\tconst currentBandwidth = this.#current.bandwidth;\n\t\tif (typeof currentBandwidth !== \"number\") return undefined;\n\t\t// Ranked here rather than through selectMulticastTier: up-selection\n\t\t// advances exactly one adjacent rung, where the selector fits a budget to\n\t\t// an estimate. The media filter is shared so a repair-only endpoint above\n\t\t// the current rung is not joined as an upgrade, which would swap the\n\t\t// receiver onto repair symbols and no media.\n\t\tconst desired = this.options.endpoints\n\t\t\t.filter((endpoint) =>\n\t\t\t\tcarriesMedia(endpoint) && typeof endpoint.bandwidth === \"number\" && endpoint.bandwidth > currentBandwidth,\n\t\t\t)\n\t\t\t.sort((a, b) => a.bandwidth! - b.bandwidth!)[0];\n\t\tif (!desired) {\n\t\t\tthis.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst desiredKey = endpointKey(desired);\n\t\tif (desiredKey === endpointKey(this.#current)) {\n\t\t\tthis.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\t\tconst now = this.#now();\n\t\tif (!this.#favored || this.#favored.key !== desiredKey) {\n\t\t\tthis.#favored = { key: desiredKey, since: now, source: \"headroom\" };\n\t\t\treturn undefined;\n\t\t}\n\t\tif (now - this.#favored.since < this.#dwellMs) return undefined;\n\n\t\tthis.#state = \"joining\";\n\t\tthis.#target = desired;\n\t\tthis.#joinStartedAt = now;\n\t\tthis.#favored = undefined;\n\t\treturn { kind: \"join-target\", current: this.#current, target: desired };\n\t}\n\n\t/**\n\t * Feed a packet from the active tier (to advance the high-water mark) or\n\t * from the in-flight target (to find a committable RAP).\n\t *\n\t * Serial order is only the true order while the compared instants are less\n\t * than `NTP_SHORT_HALF_RANGE` (2^15 s, ~9.1 h) apart. Feeding every\n\t * active-tier packet keeps the mark that close to the live stream. A mark\n\t * refreshed only by commits can fall further behind than that, and a\n\t * target RAP may then compare as older, so the join is abandoned.\n\t */\n\tonTierPacket(packet: MulticastTierPacket): OrchestratorCommand | undefined {\n\t\tif (!this.#current) return undefined;\n\t\tconst sentAt = tierPacketSendTime(packet);\n\t\t// An instant exactly half the space from the mark (RFC 1982's undefined\n\t\t// case) is neither later nor earlier, so it leaves the mark alone, as a\n\t\t// reordered earlier packet does.\n\t\tif (packet.endpointKey === endpointKey(this.#current) && sentAt !== undefined &&\n\t\t\t(this.#highWater === undefined || compareNtpShort(sentAt, this.#highWater) === \"after\")) {\n\t\t\tthis.#highWater = sentAt;\n\t\t}\n\t\tif (this.#state !== \"joining\" || !this.#target) return undefined;\n\t\tconst expired = this.poll();\n\t\tif (expired) return expired;\n\t\tif (\n\t\t\tpacket.endpointKey !== endpointKey(this.#target) ||\n\t\t\t!packet.keyframe ||\n\t\t\t!packet.hasValidCodecConfig ||\n\t\t\tsentAt === undefined ||\n\t\t\t!this.#reachesHighWater(sentAt)\n\t\t) return undefined;\n\n\t\tconst command: OrchestratorCommand = {\n\t\t\tkind: \"commit-swap\",\n\t\t\ttarget: this.#target,\n\t\t\told: this.#current,\n\t\t};\n\t\tthis.#highWater = sentAt;\n\t\tthis.#current = this.#target;\n\t\tthis.#target = undefined;\n\t\tthis.#state = \"stable\";\n\t\treturn command;\n\t}\n\n\tpoll(): OrchestratorCommand | undefined {\n\t\tif (this.#state !== \"joining\" || !this.#target || !this.#current) return undefined;\n\t\tif (this.#now() - this.#joinStartedAt < this.#joinDeadlineMs) return undefined;\n\t\treturn this.cancel();\n\t}\n\n\tcancel(): OrchestratorCommand | undefined {\n\t\tif (this.#state !== \"joining\" || !this.#target || !this.#current) return undefined;\n\t\tconst command: OrchestratorCommand = {\n\t\t\tkind: \"abandon-join\",\n\t\t\tcurrent: this.#current,\n\t\t\ttarget: this.#target,\n\t\t};\n\t\tthis.#target = undefined;\n\t\tthis.#state = \"stable\";\n\t\treturn command;\n\t}\n\n\t/**\n\t * Whether a target RAP sent at `sentAt` is at or beyond the active tier's\n\t * high-water mark. A pair exactly half the NTP-short space apart has no\n\t * serial order, so it cannot show that and does not commit.\n\t */\n\t#reachesHighWater(sentAt: number): boolean {\n\t\tif (this.#highWater === undefined) return true;\n\t\tconst order = compareNtpShort(sentAt, this.#highWater);\n\t\treturn order === \"after\" || order === \"equal\";\n\t}\n\n\t#now(): number {\n\t\tconst now = this.options.now();\n\t\tif (!Number.isFinite(now)) throw new Error(\"Multicast orchestrator clock must be finite\");\n\t\treturn now;\n\t}\n}\n\n/** A tier packet's NTP-short send time, or undefined when the host had no header to read. */\nfunction tierPacketSendTime(packet: MulticastTierPacket): number | undefined {\n\tif (!Number.isFinite(packet.timestamp)) return undefined;\n\tif (!isNtpShort(packet.timestamp)) {\n\t\tthrow new RangeError(\n\t\t\t`Multicast tier packet timestamp must be an MMTP NTP-short send time, got ${packet.timestamp}`,\n\t\t);\n\t}\n\treturn packet.timestamp;\n}\n", "import { MMTP_PACKET_ID_MAX, MMTP_SIGNALING_PACKET_ID } from \"@blockcast/mmt-base\";\nimport type {\n\tWindowMulticastSubscribeConfig,\n\tWindowMulticastSubscribeConfigBytes,\n} from \"./types.js\";\n\nexport type MulticastTransportPreference = \"auto\" | \"native\" | \"tunnel\";\n\nexport { DEFAULT_AMT_RELAY_PORT } from \"@blockcast/mmt-base\";\n\nexport interface MulticastEndpointConfig {\n\tgroupAddress: string;\n\tsourceAddress?: string;\n\tport: number;\n\tmode?: MulticastTransportPreference;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\n}\n\n/**\n * Input accepted by the canonical window.multicast request converter.\n *\n * `mode` and the `amtRelay*` fields are internal endpoint-selection state;\n * they are deliberately not emitted on the page-facing request. The\n * resulting `origin` is the only acquisition selector understood by the\n * gateway.\n */\nexport interface WindowMulticastEndpointInput {\n\tgroup: string;\n\tport: number;\n\tsource?: string;\n\tmode?: MulticastTransportPreference;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\n\tpacketIds?: readonly number[];\n\ttracks?: ReadonlyArray<{ name: string; packetId: number }>;\n}\n\n/**\n * Convert internal endpoint-selection state to the canonical page contract.\n *\n * Native is the default and an explicit `native` selection always wins over\n * stale relay metadata. Tunnel/AMT requests must carry the resolved relay\n * and port; there is no implicit relay-port default at this boundary.\n *\n * That rule is deliberate and is NOT the place to fix a relay that names no\n * port: this is the last hop before the page-facing request, so a default\n * applied here would be invisible to every caller and to the join it produces.\n * A catalog relay with no port gets `DEFAULT_AMT_RELAY_PORT` upstream, in the\n * readers that resolve it, and arrives here explicit like any other\n * (BLO-38176).\n */\nexport function toWindowMulticastSubscribeConfig(\n\tinput: WindowMulticastEndpointInput,\n): WindowMulticastSubscribeConfig {\n\tif (input.mode !== undefined && ![\"auto\", \"native\", \"tunnel\"].includes(input.mode)) {\n\t\tthrow new TypeError(`Unsupported multicast mode: ${String(input.mode)}`);\n\t}\n\t// An explicit non-array (the null \"endpoint unusable\" sentinel from\n\t// selectEndpointPacketIdsForTracks) must not degrade into \"no filter\".\n\tif (input.packetIds !== undefined && !Array.isArray(input.packetIds)) {\n\t\tthrow new TypeError(\n\t\t\t`multicast packetIds must be an array, got ${input.packetIds === null ? \"null\" : typeof input.packetIds}`,\n\t\t);\n\t}\n\t// Every join request passes through here, so the track packet_id range is\n\t// enforced here rather than trusted from each caller.\n\tif (input.packetIds?.some(id => !isMulticastTrackPacketId(id)) ||\n\t\tinput.tracks?.some(track => !isMulticastTrackPacketId(track.packetId))) {\n\t\tthrow new RangeError(\n\t\t\t`multicast packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}; ` +\n\t\t\t\"packetId 0 is the MMTP signaling flow and is never a track's\",\n\t\t);\n\t}\n\n\tconst base = {\n\t\tgroup: input.group,\n\t\tport: input.port,\n\t\t...(input.source?.trim() ? { source: input.source.trim() } : {}),\n\t\tconsume: \"bytes\" as const,\n\t\t...(input.packetIds?.length ? { packetIds: [...input.packetIds] } : {}),\n\t};\n\n\tconst relay = input.amtRelay?.trim();\n\tconst wantsAmt = input.mode === \"tunnel\" ||\n\t\t(input.mode !== \"native\" && relay !== undefined && relay !== \"\");\n\tif (!wantsAmt) {\n\t\tconst native: WindowMulticastSubscribeConfigBytes = {\n\t\t\t...base,\n\t\t\torigin: \"ip-multicast\",\n\t\t\t...(input.tracks?.length ? { tracks: input.tracks.map(({ name, packetId }) => ({ name, packetId })) } : {}),\n\t\t};\n\t\treturn native;\n\t}\n\n\tconst relayPort = input.amtRelayPort;\n\tif (!relay || relayPort === undefined || !isPositivePort(relayPort)) {\n\t\tthrow new TypeError(\"AMT requires a resolved relay and a valid relay port\");\n\t}\n\treturn {\n\t\t...base,\n\t\torigin: \"amt\",\n\t\trelay,\n\t\trelayPort,\n\t};\n}\n\nexport function isValidMulticastGroupAddress(address: string | undefined): boolean {\n\tconst value = address?.trim().toLowerCase();\n\tif (!value) return false;\n\tif (value.startsWith(\"ff\")) return isValidIpv6MulticastAddress(value);\n\n\tconst parts = value.split(\".\");\n\tif (parts.length !== 4) return false;\n\tconst octets = parts.map((part) => {\n\t\tif (!/^\\d+$/.test(part)) return Number.NaN;\n\t\tconst octet = Number(part);\n\t\treturn Number.isInteger(octet) && octet >= 0 && octet <= 255 ? octet : Number.NaN;\n\t});\n\treturn octets.every(Number.isInteger) && octets[0] >= 224 && octets[0] <= 239;\n}\n\nexport function normalizeMulticastEndpoint(\n\tendpoint: MulticastEndpointConfig,\n): MulticastEndpointConfig | undefined {\n\tconst groupAddress = endpoint.groupAddress.trim().toLowerCase();\n\tconst sourceAddress = endpoint.sourceAddress?.trim().toLowerCase() || undefined;\n\tconst amtRelay = endpoint.amtRelay?.trim().toLowerCase() || undefined;\n\tif (!isValidMulticastGroupAddress(groupAddress) || !isPositivePort(endpoint.port)) return undefined;\n\tif (endpoint.mode && ![\"auto\", \"native\", \"tunnel\"].includes(endpoint.mode)) return undefined;\n\tif (endpoint.amtRelayPort !== undefined && !isPositivePort(endpoint.amtRelayPort)) return undefined;\n\n\treturn {\n\t\tgroupAddress,\n\t\t...(sourceAddress ? { sourceAddress } : {}),\n\t\tport: endpoint.port,\n\t\t...(endpoint.mode ? { mode: endpoint.mode } : {}),\n\t\t...(amtRelay ? { amtRelay } : {}),\n\t\t...(endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {}),\n\t};\n}\n\n/** Stable ownership key for one IWA multicast/AMT subscription. */\nexport function multicastEndpointKey(endpoint: MulticastEndpointConfig): string {\n\tconst normalized = normalizeMulticastEndpoint(endpoint);\n\tif (!normalized) throw new Error(\"Invalid multicast endpoint\");\n\treturn JSON.stringify([\n\t\tnormalized.sourceAddress ?? \"\",\n\t\tnormalized.groupAddress,\n\t\tnormalized.port,\n\t\tnormalized.mode ?? \"auto\",\n\t\tnormalized.amtRelay ?? \"\",\n\t\tnormalized.amtRelayPort ?? \"\",\n\t]);\n}\n\n/**\n * True when `value` may be advertised as a multicast track's MMTP `packet_id`:\n * an integer in 1..MMTP_PACKET_ID_MAX. packet_id 0 (MMTP_SIGNALING_PACKET_ID)\n * is the MMTP signaling flow -- the FFmpeg `moq_mmt` muxer sends the AL-FEC\n * message (0x0203) on it into the same group as the media -- so it is never a\n * track's. Only a number qualifies: a string, boolean or null is not coerced.\n */\nexport function isMulticastTrackPacketId(value: unknown): value is number {\n\treturn Number.isInteger(value) &&\n\t\t(value as number) > MMTP_SIGNALING_PACKET_ID &&\n\t\t(value as number) <= MMTP_PACKET_ID_MAX;\n}\n\nexport function isUsableMulticastEndpoint(\n\tendpoint: { groupAddress?: string; port?: unknown } | undefined,\n): boolean {\n\tconst port = Number(endpoint?.port);\n\treturn isValidMulticastGroupAddress(endpoint?.groupAddress) && isPositivePort(port);\n}\n\n/**\n * Unprefixed aliases of the `mc_` URL override keys, retired in BLO-37507, each\n * paired with the key that replaces it.\n */\nconst RETIRED_MULTICAST_URL_PARAMS = [\n\t[\"amt_relay\", \"mc_amt_relay\"],\n\t[\"amt_relay_port\", \"mc_amt_relay_port\"],\n] as const;\n\n/**\n * Reject a URL multicast override that still carries a retired unprefixed alias.\n *\n * Both URL readers call this only once a valid override is established (a valid\n * multicast `mc_group` and a valid `mc_port`; `readUrlMulticastConfig` also\n * requires the whole endpoint to normalize), which is where they used to read\n * the aliases: a link that\n * relied on one fails here with its replacement named, instead of silently\n * losing its AMT relay (and, in `readUrlMulticastConfig`, its tunnel mode).\n * Presence is the error, whatever the value and whether or not the `mc_` form\n * is also present: quietly preferring one of two keys is the tolerant read being\n * retired. Outside an override the readers read no override key at all, the\n * `mc_` forms included, so the bare names are left to the page; they are live\n * page parameters elsewhere, and keeping clear of those is what the `mc_`\n * namespace is for.\n */\nexport function assertNoRetiredMulticastUrlParams(params: URLSearchParams): void {\n\tfor (const [retired, replacement] of RETIRED_MULTICAST_URL_PARAMS) {\n\t\tif (params.has(retired)) {\n\t\t\tthrow new Error(\n\t\t\t\t`multicast URL override: query parameter \"${retired}\" is retired; use \"${replacement}\"`,\n\t\t\t);\n\t\t}\n\t}\n}\n\n/**\n * Read an explicit multicast endpoint override from `mc_*` URL parameters.\n * Throws when an override carries a retired unprefixed alias\n * (`assertNoRetiredMulticastUrlParams`).\n */\nexport function readUrlMulticastConfig(\n\tsearch: string | URLSearchParams = typeof location === \"undefined\" ? \"\" : location.search,\n): MulticastEndpointConfig | undefined {\n\tconst params = typeof search === \"string\" ? new URLSearchParams(search) : search;\n\tconst groupAddress = params.get(\"mc_group\") || \"\";\n\tconst port = positivePort(params.get(\"mc_port\"));\n\tif (!groupAddress || !port) return undefined;\n\n\tconst amtRelay = params.get(\"mc_amt_relay\") || undefined;\n\tconst amtRelayPort = positivePort(params.get(\"mc_amt_relay_port\"));\n\tconst requestedMode = params.get(\"mc_mode\");\n\tconst mode = amtRelay ? \"tunnel\" : multicastMode(requestedMode);\n\n\tconst endpoint = normalizeMulticastEndpoint({\n\t\tgroupAddress,\n\t\tsourceAddress: params.get(\"mc_source\") || undefined,\n\t\tport,\n\t\t...(mode ? { mode } : {}),\n\t\t...(amtRelay ? { amtRelay } : {}),\n\t\t...(amtRelayPort ? { amtRelayPort } : {}),\n\t});\n\t// An endpoint that does not normalize (e.g. a non-multicast mc_group) is no\n\t// override, so the page keeps its own bare names; the same scope\n\t// readPlayerMulticastConfig applies by validating the group first.\n\tif (endpoint) assertNoRetiredMulticastUrlParams(params);\n\treturn endpoint;\n}\n\nexport function isMulticastExplicitlyDisabled(\n\tsearch: string | URLSearchParams = typeof location === \"undefined\" ? \"\" : location.search,\n): boolean {\n\tconst params = typeof search === \"string\" ? new URLSearchParams(search) : search;\n\tconst multicast = params.get(\"multicast\")?.trim().toLowerCase();\n\tconst transport = params.get(\"transport\")?.trim().toLowerCase();\n\treturn transport === \"moq-only\" || [\"0\", \"false\", \"no\", \"off\"].includes(multicast || \"\");\n}\n\nfunction isValidIpv6MulticastAddress(value: string): boolean {\n\tif (!/^[0-9a-f:]+$/.test(value)) return false;\n\tconst compressed = value.includes(\"::\");\n\tif (value.indexOf(\"::\") !== value.lastIndexOf(\"::\")) return false;\n\tconst groups = value.split(\":\").filter(Boolean);\n\tif (groups.length === 0 || groups.length > 8) return false;\n\tif (!compressed && groups.length !== 8) return false;\n\treturn groups.every((group) => /^[0-9a-f]{1,4}$/.test(group));\n}\n\nfunction multicastMode(value: string | null): MulticastTransportPreference | undefined {\n\treturn value === \"native\" || value === \"tunnel\" || value === \"auto\" ? value : undefined;\n}\n\nfunction positivePort(value: string | null): number | undefined {\n\tconst port = Number(value);\n\treturn isPositivePort(port) ? port : undefined;\n}\n\nfunction isPositivePort(port: number): boolean {\n\treturn Number.isInteger(port) && port > 0 && port <= 65535;\n}\n", "import type { WindowMulticastSubscription } from \"./types.js\";\n\n/**\n * True when `subscription` delivers packets in batches through the `packets`\n * provider extension (BLO-36599).\n *\n * The pim window-multicast-bridge (0.2.0 and later) advertises the extension\n * in `eventTypes`. It builds only the events some listener asked for, so a\n * consumer that listens to `packets` alone gets one event per reader batch\n * instead of one DOM event per packet. A provider without the field, or one\n * that does not list `packets`, is a v1 provider: listen to `bytes`.\n */\nexport function subscriptionDeliversPacketBatches(\n\tsubscription: Pick<WindowMulticastSubscription, \"eventTypes\">,\n): boolean {\n\treturn subscription.eventTypes?.includes(\"packets\") === true;\n}\n", "import {\n\tmulticastEndpointKey,\n\tnormalizeMulticastEndpoint,\n\ttoWindowMulticastSubscribeConfig,\n} from \"./endpoint-config.js\";\nimport { subscriptionDeliversPacketBatches } from \"./packet-batch.js\";\nimport type {\n\tTransportConfig,\n\tWindowMulticastGateway,\n\tWindowMulticastSubscription,\n\tWindowMulticastSubscriptionState,\n} from \"./types.js\";\n\nexport interface MulticastSubscriptionConsumer {\n\tonBytes(data: ArrayBuffer): void;\n\tonState?(state: WindowMulticastSubscriptionState): void;\n\tonError?(error: Error): void;\n}\n\ninterface PoolEntry {\n\tsubscription: WindowMulticastSubscription;\n\tconsumers: Map<symbol, MulticastSubscriptionConsumer>;\n\t/**\n\t * Exactly one packet surface is listened to: `packets` when the provider\n\t * advertises it, `bytes` otherwise (BLO-36599).\n\t */\n\tpacketListener:\n\t\t| { type: \"bytes\"; listener: (event: CustomEvent<ArrayBuffer>) => void }\n\t\t| { type: \"packets\"; listener: (event: CustomEvent<readonly ArrayBuffer[]>) => void };\n\tstateListener: (event: CustomEvent<WindowMulticastSubscriptionState>) => void;\n\terrorListener: (event: CustomEvent<unknown>) => void;\n\tclosed: boolean;\n}\n\ninterface PoolRecord { claims: number; entry: Promise<PoolEntry>; }\n\nexport interface MulticastSubscriptionLease {\n\treadonly key: string;\n\trelease(): Promise<void>;\n}\n\n/** Shares only IWA multicast/AMT subscriptions. Unicast sessions remain per manager. */\nexport class MulticastSubscriptionPool {\n\treadonly #entries = new Map<string, PoolRecord>();\n\treadonly #gatewayIds = new WeakMap<object, number>();\n\t#nextGatewayId = 1;\n\n\tasync acquire(\n\t\tgateway: WindowMulticastGateway,\n\t\tconfig: TransportConfig,\n\t\tconsumer: MulticastSubscriptionConsumer,\n\t): Promise<MulticastSubscriptionLease> {\n\t\tconst endpoint = normalizeMulticastEndpoint({\n\t\t\tgroupAddress: config.group,\n\t\t\tsourceAddress: config.source,\n\t\t\tport: config.port,\n\t\t\tmode: config.mode,\n\t\t\tamtRelay: config.amtRelay,\n\t\t\tamtRelayPort: config.amtRelayPort,\n\t\t});\n\t\tif (!endpoint) throw new Error(\"Invalid multicast endpoint\");\n\t\tif (typeof gateway.subscribe !== \"function\") throw new Error(\"window.multicast.subscribe unavailable\");\n\n\t\tconst key = multicastEndpointKey(endpoint);\n\t\tconst internalKey = `${this.#gatewayId(gateway)}|${key}`;\n\t\tlet record = this.#entries.get(internalKey);\n\t\tif (!record) {\n\t\t\trecord = { claims: 0, entry: Promise.resolve(null as unknown as PoolEntry) };\n\t\t\tconst createdRecord = record;\n\t\t\trecord.entry = this.#createEntry(gateway, endpoint, (entry) => {\n\t\t\t\tif (this.#entries.get(internalKey) === createdRecord) this.#entries.delete(internalKey);\n\t\t\t\tthis.#detach(entry);\n\t\t\t}).catch((error) => {\n\t\t\t\tif (this.#entries.get(internalKey) === createdRecord) this.#entries.delete(internalKey);\n\t\t\t\tthrow error;\n\t\t\t});\n\t\t\tthis.#entries.set(internalKey, record);\n\t\t}\n\n\t\trecord.claims++;\n\t\tlet entry: PoolEntry;\n\t\ttry {\n\t\t\tentry = await record.entry;\n\t\t} catch (error) {\n\t\t\trecord.claims--;\n\t\t\tthrow error;\n\t\t}\n\t\tif (entry.closed) {\n\t\t\trecord.claims--;\n\t\t\treturn this.acquire(gateway, config, consumer);\n\t\t}\n\t\tconst token = Symbol(key);\n\t\tentry.consumers.set(token, consumer);\n\t\tlet released = false;\n\t\treturn {\n\t\t\tkey,\n\t\t\trelease: async () => {\n\t\t\t\tif (released) return;\n\t\t\t\treleased = true;\n\t\t\t\tentry.consumers.delete(token);\n\t\t\t\trecord.claims--;\n\t\t\t\tif (record.claims > 0) return;\n\t\t\t\tif (this.#entries.get(internalKey) === record) this.#entries.delete(internalKey);\n\t\t\t\tthis.#detach(entry);\n\t\t\t\tif (!entry.closed) await entry.subscription.unsubscribe();\n\t\t\t},\n\t\t};\n\t}\n\n\tget size(): number { return this.#entries.size; }\n\n\tasync #createEntry(\n\t\tgateway: WindowMulticastGateway,\n\t\tendpoint: NonNullable<ReturnType<typeof normalizeMulticastEndpoint>>,\n\t\tonClosed: (entry: PoolEntry) => void,\n\t): Promise<PoolEntry> {\n\t\tconst subscription = await gateway.subscribe!(toWindowMulticastSubscribeConfig({\n\t\t\tgroup: endpoint.groupAddress,\n\t\t\t...(endpoint.sourceAddress ? { source: endpoint.sourceAddress } : {}),\n\t\t\tport: endpoint.port,\n\t\t\t...(endpoint.mode ? { mode: endpoint.mode } : {}),\n\t\t\t...(endpoint.amtRelay ? { amtRelay: endpoint.amtRelay } : {}),\n\t\t\t...(endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {}),\n\t\t}));\n\t\tconst consumers = new Map<symbol, MulticastSubscriptionConsumer>();\n\t\tlet entry: PoolEntry;\n\t\t// A batch is delivered as the same per-packet onBytes calls the bytes\n\t\t// surface makes, in arrival order and with each call isolated, so a\n\t\t// consumer cannot tell the two apart; only the DOM event per packet is\n\t\t// gone. The batch array and its elements are the provider's own, uncopied.\n\t\tconst packetListener: PoolEntry[\"packetListener\"] = subscriptionDeliversPacketBatches(subscription)\n\t\t\t? {\n\t\t\t\t\ttype: \"packets\",\n\t\t\t\t\tlistener: (event) => {\n\t\t\t\t\t\tfor (const data of event.detail) {\n\t\t\t\t\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onBytes(data));\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: {\n\t\t\t\t\ttype: \"bytes\",\n\t\t\t\t\tlistener: (event) => {\n\t\t\t\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onBytes(event.detail));\n\t\t\t\t\t},\n\t\t\t\t};\n\t\tconst stateListener = (event: CustomEvent<WindowMulticastSubscriptionState>) => {\n\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onState?.(event.detail));\n\t\t\tif (event.detail === \"closed\" && !entry.closed) {\n\t\t\t\tentry.closed = true;\n\t\t\t\tonClosed(entry);\n\t\t\t}\n\t\t};\n\t\tconst errorListener = (event: CustomEvent<unknown>) => {\n\t\t\tconst error = event.detail instanceof Error ? event.detail : new Error(String(event.detail));\n\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onError?.(error));\n\t\t};\n\t\tentry = { subscription, consumers, packetListener, stateListener, errorListener, closed: false };\n\t\tif (packetListener.type === \"packets\") {\n\t\t\tsubscription.addEventListener(\"packets\", packetListener.listener);\n\t\t} else {\n\t\t\tsubscription.addEventListener(\"bytes\", packetListener.listener);\n\t\t}\n\t\tsubscription.addEventListener(\"state\", stateListener);\n\t\tsubscription.addEventListener(\"error\", errorListener);\n\t\treturn entry;\n\t}\n\n\t#gatewayId(gateway: WindowMulticastGateway): number {\n\t\tconst object = gateway as object;\n\t\tlet id = this.#gatewayIds.get(object);\n\t\tif (!id) {\n\t\t\tid = this.#nextGatewayId++;\n\t\t\tthis.#gatewayIds.set(object, id);\n\t\t}\n\t\treturn id;\n\t}\n\n\t#detach(entry: PoolEntry): void {\n\t\tconst { packetListener } = entry;\n\t\tif (packetListener.type === \"packets\") {\n\t\t\tentry.subscription.removeEventListener(\"packets\", packetListener.listener);\n\t\t} else {\n\t\t\tentry.subscription.removeEventListener(\"bytes\", packetListener.listener);\n\t\t}\n\t\tentry.subscription.removeEventListener(\"state\", entry.stateListener);\n\t\tentry.subscription.removeEventListener(\"error\", entry.errorListener);\n\t}\n\n\t#dispatch(\n\t\tconsumers: Map<symbol, MulticastSubscriptionConsumer>,\n\t\tcallback: (consumer: MulticastSubscriptionConsumer) => void,\n\t): void {\n\t\tfor (const consumer of consumers.values()) {\n\t\t\ttry { callback(consumer); } catch (error) { console.error(\"[Transport] multicast consumer callback failed\", error); }\n\t\t}\n\t}\n}\n\nexport const multicastSubscriptionPool = new MulticastSubscriptionPool();\n", "/**\n * @blockcast/transport - Transport Manager\n *\n * Unified transport abstraction for MoQ players supporting:\n * - window.multicast gateway subscriptions\n * - AMT proxy tunneling (RFC 7450) with optional DRIAD relay discovery\n * - WebTransport/QUIC (MoQ-lite protocol)\n * - WebSocket fallback\n *\n * Transport promotion hierarchy (WebTransport-first for fast startup):\n * 1. WebTransport - immediate connection for fast channel select\n * 2. Background discovery - check if window.multicast is available\n * 3. Promote to gateway multicast if available\n * 4. WebSocket fallback if WebTransport unavailable\n *\n * Features:\n * - Progressive adaptive bitrate based on network conditions\n * - FEC (Forward Error Correction) for burst loss recovery\n * - Seamless transport switching with session continuity\n * - Unified FEC/ABR pipeline across all transport modes\n */\n\nimport type {\n\tTransportConfig,\n\tTransportConnectConfig,\n\tTransportSubscription,\n\tTransportCapabilities,\n\tTransportState,\n\tQualityLevel,\n\tWindowMulticastCapabilities,\n\tWindowMulticastGateway,\n\tWindowMulticastSubscriptionState,\n} from \"./types.js\";\nimport { DRIADDiscovery } from \"./driad-discovery.js\";\nimport { AdaptiveBitrateController } from \"./abr-controller.js\";\nimport {\n\tMulticastOrchestrator,\n\ttype MulticastTierPacket,\n\ttype OrchestratorCommand,\n\ttype OrchestratorOptions,\n} from \"./multicast-orchestrator.js\";\nimport type { MulticastTierEndpoint } from \"./multicast-tier.js\";\nimport { multicastSubscriptionPool } from \"./multicast-subscription-pool.js\";\nimport { normalizeMulticastEndpoint } from \"./endpoint-config.js\";\n\n/**\n * Transport Manager\n *\n * Manages transport connections with automatic mode selection:\n * WebTransport -> window.multicast -> AMT proxy -> WebSocket\n */\nexport class TransportManager {\n\tprivate state: TransportState = {\n\t\tmode: \"disconnected\",\n\t\tconnected: false,\n\t\tpacketsReceived: 0,\n\t\tbytesReceived: 0,\n\t\tlastPacketTime: 0,\n\t};\n\n\tprivate capabilities: TransportCapabilities | null = null;\n\tprivate subscription: TransportSubscription | null = null;\n\tprivate driad = new DRIADDiscovery();\n\t/** Built by `connect()` from the caller's ladder; there is no built-in one. */\n\tprivate abr: AdaptiveBitrateController | null = null;\n\tprivate multicastOrchestrator: MulticastOrchestrator | null = null;\n\tprivate onOrchestratorCommand?: (command: OrchestratorCommand) => void;\n\tprivate connectionGeneration = 0;\n\n\tprivate onPacket?: (data: ArrayBuffer) => void;\n\tprivate onError?: (error: Error) => void;\n\tprivate onStateChange?: (state: TransportState) => void;\n\n\tprivate getWindow(): Window | undefined {\n\t\treturn typeof window === \"undefined\" ? undefined : window;\n\t}\n\n\tprivate getWindowMulticastGateway(): WindowMulticastGateway | undefined {\n\t\treturn this.getWindow()?.multicast;\n\t}\n\n\tprivate async getGatewayCapabilities(\n\t\tgateway: WindowMulticastGateway,\n\t): Promise<WindowMulticastCapabilities | undefined> {\n\t\tif (!gateway?.capabilities) return undefined;\n\t\ttry {\n\t\t\treturn typeof gateway.capabilities === \"function\" ?\n\t\t\t\tawait gateway.capabilities() :\n\t\t\t\tgateway.capabilities;\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] window.multicast capabilities failed:\", error);\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async gatewaySupportsBytes(gateway: WindowMulticastGateway | undefined): Promise<boolean> {\n\t\tif (typeof gateway?.subscribe !== \"function\") return false;\n\t\tif (!gateway.capabilities) return true;\n\n\t\tconst capabilities = await this.getGatewayCapabilities(gateway);\n\t\tif (!capabilities) return false;\n\t\tconst consumeModes = capabilities.consumeModes;\n\t\treturn !consumeModes || consumeModes.includes(\"bytes\");\n\t}\n\n\t/**\n\t * Detect available transport capabilities\n\t */\n\tasync detectCapabilities(): Promise<TransportCapabilities> {\n\t\tif (this.capabilities) return this.capabilities;\n\n\t\tconst gateway = this.getWindowMulticastGateway();\n\t\tconst hasGatewayBytes = await this.gatewaySupportsBytes(gateway);\n\t\tconst win = this.getWindow();\n\n\t\tthis.capabilities = {\n\t\t\tnativeSSM: hasGatewayBytes,\n\t\t\tnativeIGMP: hasGatewayBytes,\n\t\t\tdirectSockets: typeof win?.UDPSocket !== \"undefined\",\n\t\t\tamt: false,\n\t\t\tdriad: true, // DRIAD uses HTTP proxy, always available\n\t\t\tfecDecode: false,\n\t\t};\n\n\t\tconsole.log(\"[Transport] Capabilities:\", this.capabilities);\n\t\treturn this.capabilities;\n\t}\n\n\t/**\n\t * Connect using best available transport\n\t *\n\t * Promotion hierarchy:\n\t * 1. window.multicast gateway bytes\n\t * 2. AMT proxy (specified or discovered via DRIAD)\n\t * 3. MOQ WebTransport (final fallback)\n\t *\n\t * The ABR ladder (`abrLadder`) and initial rendition (`initialQuality`)\n\t * are checked first, before anything is torn down. A config that fails is\n\t * refused with the same shape as an invalid endpoint, the error to the\n\t * error callback and `false` resolved; unlike one, it is caught before the\n\t * live subscription closes, so a refused reconnect leaves that subscription,\n\t * its state and any connect already in flight untouched.\n\t */\n\tasync connect(config: TransportConnectConfig): Promise<boolean> {\n\t\tlet abr: AdaptiveBitrateController;\n\t\ttry {\n\t\t\tabr = new AdaptiveBitrateController({\n\t\t\t\tladder: config.abrLadder,\n\t\t\t\tinitialQuality: config.initialQuality?.name,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\t// A refused config is a caller bug. Without an error callback, say so\n\t\t\t// on the console, since `false` alone also means \"no multicast path\";\n\t\t\t// with one, the callback is the report.\n\t\t\tconst refused = error instanceof Error ? error : new Error(String(error));\n\t\t\tif (this.onError) this.onError(refused);\n\t\t\telse console.error(\"[Transport] ABR config refused, not connecting:\", refused);\n\t\t\treturn false;\n\t\t}\n\t\tconst generation = ++this.connectionGeneration;\n\t\tthis.subscription?.close();\n\t\tthis.subscription = null;\n\t\t// The previous connection's ladder ends with it, as in disconnect(): a\n\t\t// reconnect that bails below (invalid endpoint, or superseded while\n\t\t// detecting capabilities) leaves no ladder, and only the success path\n\t\t// installs `abr`.\n\t\tthis.abr = null;\n\t\tthis.state = {\n\t\t\tmode: \"disconnected\",\n\t\t\tconnected: false,\n\t\t\tpacketsReceived: 0,\n\t\t\tbytesReceived: 0,\n\t\t\tlastPacketTime: 0,\n\t\t};\n\t\tthis.onStateChange?.(this.state);\n\t\tconst endpoint = normalizeMulticastEndpoint({\n\t\t\tgroupAddress: config.group,\n\t\t\tsourceAddress: config.source,\n\t\t\tport: config.port,\n\t\t\tmode: config.mode,\n\t\t\tamtRelay: config.amtRelay,\n\t\t\tamtRelayPort: config.amtRelayPort,\n\t\t});\n\t\tif (!endpoint) {\n\t\t\tconst error = new Error(\"Invalid multicast endpoint configuration\");\n\t\t\tthis.onError?.(error);\n\t\t\treturn false;\n\t\t}\n\t\tconfig = {\n\t\t\t...config,\n\t\t\tgroup: endpoint.groupAddress,\n\t\t\tsource: endpoint.sourceAddress ?? \"\",\n\t\t\tmode: endpoint.mode,\n\t\t\tamtRelay: endpoint.amtRelay,\n\t\t\tamtRelayPort: endpoint.amtRelayPort,\n\t\t};\n\t\tconst caps = await this.detectCapabilities();\n\t\tif (generation !== this.connectionGeneration) return false;\n\n\t\t// ABR adapts over the caller's ladder (the catalog's renditions),\n\t\t// validated above before anything was torn down.\n\t\tthis.abr = abr;\n\n\t\tabr.setCallback((quality) => {\n\t\t\tthis.state.quality = quality;\n\t\t\tthis.subscription?.onqualitychange?.(quality);\n\t\t\tthis.onStateChange?.(this.state);\n\t\t});\n\n\t\t// Try transport hierarchy\n\t\tlet connected = false;\n\n\t\t// 1. Try the canonical browser multicast gateway.\n\t\tif (caps.nativeSSM || caps.nativeIGMP) {\n\t\t\tconsole.log(\"[Transport] Attempting window.multicast...\");\n\t\t\tconnected = await this.connectWindowMulticast(config, generation);\n\t\t\tif (connected) {\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\t\t\t\tthis.state.mode = \"multicast\";\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// 2. Try AMT proxy (with optional DRIAD discovery)\n\t\tif (!connected) {\n\t\t\tconsole.log(\"[Transport] Attempting AMT tunnel...\");\n\t\t\tconnected = await this.connectAmt(config, generation);\n\t\t\tif (connected) {\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\t\t\t\tthis.state.mode = \"amt\";\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback to MOQ\n\t\tconsole.log(\"[Transport] All multicast transports failed, falling back to MOQ\");\n\t\treturn false;\n\t}\n\n\t/**\n\t * Connect via the lowercase window.multicast bytes gateway.\n\t */\n\tprivate async connectWindowMulticast(config: TransportConfig, generation: number): Promise<boolean> {\n\t\ttry {\n\t\t\tconst gateway = this.getWindowMulticastGateway();\n\t\t\tif (typeof gateway?.subscribe !== \"function\") return false;\n\n\t\t\tconst lease = await multicastSubscriptionPool.acquire(gateway, config, {\n\t\t\t\tonBytes: (data) => {\n\t\t\t\t\tif (generation === this.connectionGeneration) this.handlePacket(data);\n\t\t\t\t},\n\t\t\t\tonState: (state: WindowMulticastSubscriptionState) => {\n\t\t\t\t\tif (generation !== this.connectionGeneration) return;\n\t\t\t\t\tthis.state.connected = state !== \"closed\";\n\t\t\t\t\tthis.onStateChange?.(this.state);\n\t\t\t\t},\n\t\t\t\tonError: (error) => {\n\t\t\t\t\tif (generation !== this.connectionGeneration) return;\n\t\t\t\t\tconsole.error(\"[Transport] window.multicast error:\", error);\n\t\t\t\t\tthis.onError?.(error);\n\t\t\t\t},\n\t\t\t});\n\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\tawait lease.release();\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tthis.subscription = {\n\t\t\t\tid: lease.key,\n\t\t\t\tconfig,\n\t\t\t\tclose: () => { void lease.release(); },\n\t\t\t};\n\n\t\t\tthis.state = {\n\t\t\t\tmode: \"multicast\",\n\t\t\t\tconnected: true,\n\t\t\t\tgroup: config.group,\n\t\t\t\tsource: config.source,\n\t\t\t\tquality: this.requireAbr().getCurrentQuality(),\n\t\t\t\tpacketsReceived: 0,\n\t\t\t\tbytesReceived: 0,\n\t\t\t\tlastPacketTime: 0,\n\t\t\t};\n\n\t\t\tconsole.log(`[Transport] Connected via window.multicast: (${config.source}, ${config.group})`);\n\t\t\tthis.onStateChange?.(this.state);\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] window.multicast failed:\", error);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Connect via AMT proxy (with optional DRIAD discovery)\n\t */\n\tprivate async connectAmt(config: TransportConfig, generation: number): Promise<boolean> {\n\t\ttry {\n\t\t\tlet amtRelay = config.amtRelay;\n\n\t\t\t// Use DRIAD to discover relay if not specified (RFC 8777)\n\t\t\t// DRIAD discovers relays based on SOURCE address, not group\n\t\t\tif (!amtRelay && config.driadEnabled !== false && config.source) {\n\t\t\t\tconsole.log(`[Transport] Discovering AMT relay via DRIAD for source ${config.source}...`);\n\t\t\t\tconst relays = await this.driad.discoverRelays(config.source);\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\t\t\t\tconst bestRelay = await this.driad.selectBestRelay(relays);\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\n\t\t\t\tif (bestRelay) {\n\t\t\t\t\tamtRelay = bestRelay.host;\n\t\t\t\t\tthis.state.amtRelay = amtRelay;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!amtRelay) {\n\t\t\t\tconsole.warn(\"[Transport] No AMT relay available (DRIAD requires source address for SSM)\");\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Use the WebSocket-based AMT proxy when a relay is known.\n\t\t\treturn await this.connectAmtProxy(config, amtRelay, generation);\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] AMT connection failed:\", error);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Connect via WebSocket-based AMT proxy (browser fallback)\n\t */\n\tprivate async connectAmtProxy(config: TransportConfig, amtRelay: string, generation: number): Promise<boolean> {\n\t\ttry {\n\t\t\tconst relayHost = amtRelay.includes(\":\") && !amtRelay.startsWith(\"[\") ? `[${amtRelay}]` : amtRelay;\n\t\t\t// amtRelayPort is the relay's RFC 7450 UDP port, not the proxy's HTTPS port.\n\t\t\tconst wsUrl = new URL(`wss://${relayHost}/amt-proxy`);\n\t\t\twsUrl.searchParams.set(\"group\", config.group);\n\t\t\twsUrl.searchParams.set(\"source\", config.source || \"\");\n\t\t\twsUrl.searchParams.set(\"port\", String(config.port));\n\n\t\t\tconst ws = new WebSocket(wsUrl);\n\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tws.binaryType = \"arraybuffer\";\n\t\t\t\tconst timeout = setTimeout(() => {\n\t\t\t\t\tws.close();\n\t\t\t\t\treject(new Error(\"AMT proxy connection timeout\"));\n\t\t\t\t}, 10000);\n\n\t\t\t\tws.onopen = () => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\t\t\tws.close();\n\t\t\t\t\t\tresolve(false);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconsole.log(`[Transport] Connected to AMT proxy: ${amtRelay}`);\n\n\t\t\t\t\tthis.subscription = {\n\t\t\t\t\t\tid: `amt-proxy-${Date.now()}`,\n\t\t\t\t\t\tconfig,\n\t\t\t\t\t\tclose: () => ws.close(),\n\t\t\t\t\t};\n\n\t\t\t\t\tthis.state = {\n\t\t\t\t\t\tmode: \"amt\",\n\t\t\t\t\t\tconnected: true,\n\t\t\t\t\t\tgroup: config.group,\n\t\t\t\t\t\tsource: config.source,\n\t\t\t\t\t\tamtRelay,\n\t\t\t\t\t\tquality: this.requireAbr().getCurrentQuality(),\n\t\t\t\t\t\tpacketsReceived: 0,\n\t\t\t\t\t\tbytesReceived: 0,\n\t\t\t\t\t\tlastPacketTime: 0,\n\t\t\t\t\t};\n\n\t\t\t\t\tthis.onStateChange?.(this.state);\n\t\t\t\t\tresolve(true);\n\t\t\t\t};\n\n\t\t\t\tws.onmessage = (event) => {\n\t\t\t\t\tif (generation === this.connectionGeneration && event.data instanceof ArrayBuffer) {\n\t\t\t\t\t\tthis.handlePacket(event.data);\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tws.onerror = (error) => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\t\t\tresolve(false);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconsole.error(\"[Transport] AMT proxy error:\", error);\n\t\t\t\t\treject(error);\n\t\t\t\t};\n\n\t\t\t\tws.onclose = () => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\t\t\tresolve(false);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.state.connected = false;\n\t\t\t\t\tthis.onStateChange?.(this.state);\n\t\t\t\t\tresolve(false);\n\t\t\t\t};\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] AMT proxy failed:\", error);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Handle received packet\n\t */\n\tprivate handlePacket(data: ArrayBuffer) {\n\t\tthis.state.packetsReceived++;\n\t\tthis.state.bytesReceived += data.byteLength;\n\t\tthis.state.lastPacketTime = Date.now();\n\n\t\t// Update ABR with sample\n\t\t// Packets reach here only through a connect()ed join, which built the ABR.\n\t\tthis.abr?.recordSample(data.byteLength, 0);\n\n\t\t// Forward to callback\n\t\tthis.onPacket?.(data);\n\t}\n\n\t/**\n\t * Get current transport state\n\t */\n\tgetState(): TransportState {\n\t\treturn { ...this.state };\n\t}\n\n\t/**\n\t * The rung of the live connection's `abrLadder` ABR is on; `null` when\n\t * there is no ladder (before `connect(config)` succeeds, after\n\t * `disconnect()`, or after a reconnect that was torn down and bailed).\n\t */\n\tgetQuality(): QualityLevel | null {\n\t\treturn this.abr?.getCurrentQuality() ?? null;\n\t}\n\n\t/**\n\t * Force the rung named `name` of the live connection's `abrLadder`.\n\t * Throws `Error` when there is no ladder (see `getQuality()`), and\n\t * `RangeError` when the ladder has no rung of that name; it never falls\n\t * back to another rung.\n\t */\n\tsetQuality(name: string) {\n\t\tthis.requireAbr().setQuality(name);\n\t}\n\n\t/** The controller `connect()` built; before it there is no ladder to adapt over. */\n\tprivate requireAbr(): AdaptiveBitrateController {\n\t\tif (!this.abr) throw new Error(\"[Transport] ABR has no ladder before connect(config)\");\n\t\treturn this.abr;\n\t}\n\n\t/** Configures multicast-only ABR while leaving the caller's MoQ ABR untouched. */\n\tconfigureMulticastTiers(\n\t\toptions: OrchestratorOptions,\n\t\tonCommand?: (command: OrchestratorCommand) => void,\n\t): MulticastTierEndpoint | undefined {\n\t\tthis.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());\n\t\tthis.multicastOrchestrator = new MulticastOrchestrator(options);\n\t\tthis.onOrchestratorCommand = onCommand;\n\t\treturn this.multicastOrchestrator.start();\n\t}\n\n\trecordMulticastGoodput(bitrate: number | undefined): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onGoodput({ bitrate }));\n\t}\n\n\trecordMulticastHeadroom(healthy: boolean): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onHeadroom({ healthy }));\n\t}\n\n\trecordMulticastTierPacket(packet: MulticastTierPacket): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onTierPacket(packet));\n\t}\n\n\tpollMulticastTierSwitch(): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.poll());\n\t}\n\n\t/**\n\t * Set packet callback\n\t */\n\tsetPacketCallback(callback: (data: ArrayBuffer) => void) {\n\t\tthis.onPacket = callback;\n\t}\n\n\t/**\n\t * Set error callback\n\t */\n\tsetErrorCallback(callback: (error: Error) => void) {\n\t\tthis.onError = callback;\n\t}\n\n\t/**\n\t * Set state change callback\n\t */\n\tsetStateCallback(callback: (state: TransportState) => void) {\n\t\tthis.onStateChange = callback;\n\t}\n\n\t/**\n\t * Disconnect\n\t */\n\tdisconnect() {\n\t\tthis.connectionGeneration++;\n\t\tthis.subscription?.close();\n\t\tthis.subscription = null;\n\t\t// No ladder after a disconnect, as before the first connect():\n\t\t// getQuality() is null and setQuality() throws until connect(config).\n\t\tthis.abr = null;\n\t\tthis.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());\n\t\tthis.multicastOrchestrator = null;\n\t\tthis.onOrchestratorCommand = undefined;\n\t\tthis.state = {\n\t\t\tmode: \"disconnected\",\n\t\t\tconnected: false,\n\t\t\tpacketsReceived: 0,\n\t\t\tbytesReceived: 0,\n\t\t\tlastPacketTime: 0,\n\t\t};\n\t\tthis.onStateChange?.(this.state);\n\t}\n\n\tprivate dispatchOrchestratorCommand(command: OrchestratorCommand | undefined): OrchestratorCommand | undefined {\n\t\tif (command) this.onOrchestratorCommand?.(command);\n\t\treturn command;\n\t}\n}\n\n// Export singleton instance\nexport const transportManager = new TransportManager();\n", "const MAX_MOQT_PRIORITY = 0xff;\n\ndeclare const subscriberPriorityBrand: unique symbol;\ndeclare const publisherPriorityBrand: unique symbol;\n\nexport type MoqtSubscriberPriority = number & { readonly [subscriberPriorityBrand]: true };\nexport type MoqtPublisherPriority = number & { readonly [publisherPriorityBrand]: true };\n\nfunction moqtPriority(value: number, field: string): number {\n\tif (!Number.isInteger(value) || value < 0 || value > MAX_MOQT_PRIORITY) {\n\t\tthrow new RangeError(`${field} must be an integer in [0, ${MAX_MOQT_PRIORITY}], got ${value}`);\n\t}\n\treturn value;\n}\n\n/** Validate a protocol-native MOQT subscriber priority without transforming it. */\nexport function moqtSubscriberPriority(value: number): MoqtSubscriberPriority {\n\treturn moqtPriority(value, \"MOQT subscriber priority\") as MoqtSubscriberPriority;\n}\n\n/** Validate a protocol-native MOQT publisher priority without transforming it. */\nexport function moqtPublisherPriority(value: number): MoqtPublisherPriority {\n\treturn moqtPriority(value, \"MOQT publisher priority\") as MoqtPublisherPriority;\n}\n\n/** Shared media subscription policy on MOQT's native lower-number-wins scale. */\nexport const MOQT_SUBSCRIBER_PRIORITY = Object.freeze({\n\tcatalog: moqtSubscriberPriority(0),\n\t/**\n\t * @deprecated A repair FETCH's SUBSCRIBER_PRIORITY is derived per request\n\t * from the catalog source and repair priorities (draft-ramadan-moq-fec\n\t * \u00A711.4.5), not a player constant. Use the `subscriberPriority` that\n\t * `@blockcast/mmt-container`'s `wireRepairFetch` puts on each\n\t * `RepairFetchRequest` (`repairFetchSubscriberPriority`).\n\t */\n\tdeadlineRepair: moqtSubscriberPriority(0),\n\taudio: moqtSubscriberPriority(1),\n\tvideo: moqtSubscriberPriority(2),\n\t/**\n\t * @deprecated See `deadlineRepair`: use the request's catalog-derived\n\t * `RepairFetchRequest.subscriberPriority` (draft-ramadan-moq-fec \u00A711.4.5).\n\t */\n\tbackgroundRepair: moqtSubscriberPriority(3),\n});\n\n/** Publisher priority bands from draft-ramadan-moq-fec section 10. */\nexport const MOQT_PUBLISHER_PRIORITY = Object.freeze({\n\tcatalog: moqtPublisherPriority(32),\n\tsource: moqtPublisherPriority(128),\n\trepair: moqtPublisherPriority(240),\n});\n", "/**\n * @blockcast/transport - AMT Gateway\n *\n * Consolidated AMT (Automatic Multicast Tunneling) implementation\n * supporting both Service Worker and Direct Sockets approaches.\n *\n * RFC 7450: Automatic Multicast Tunneling\n */\n\nimport type { GroupKey, PacketMetadata, AmtRelayInfo } from \"./types.js\";\nimport { DRIADDiscovery } from \"./driad-discovery.js\";\nimport { DEFAULT_AMT_RELAY_PORT } from \"@blockcast/mmt-base\";\n\n/** AMT message types */\nconst AMT_MESSAGE_TYPES = {\n\tRELAY_DISCOVERY: 1,\n\tRELAY_ADVERTISEMENT: 2,\n\tREQUEST: 3,\n\tMEMBERSHIP_QUERY: 4,\n\tMEMBERSHIP_UPDATE: 5,\n\tMULTICAST_DATA: 6,\n\tTEARDOWN: 7,\n} as const;\n\n/** AMT gateway states */\nconst GATEWAY_STATES = {\n\tIDLE: \"idle\",\n\tDISCOVERING: \"discovering\",\n\tREQUESTING: \"requesting\",\n\tQUERYING: \"querying\",\n\tACTIVE: \"active\",\n\tLEAVING: \"leaving\",\n\tCLOSED: \"closed\",\n} as const;\n\ntype GatewayState = (typeof GATEWAY_STATES)[keyof typeof GATEWAY_STATES];\n\n/** AMT subscription configuration */\nexport interface AMTSubscriptionConfig {\n\tkey: GroupKey;\n\trelayAddress?: string;\n\trelayPort?: number;\n\tonPacket: (data: ArrayBuffer, metadata: PacketMetadata) => void;\n\tonError: (error: Error) => void;\n}\n\n/** AMT packet info */\nexport interface AMTPacketInfo {\n\tdata: Uint8Array;\n\tsrcAddress: string;\n\tdstAddress: string;\n\tsrcPort: number;\n\tdstPort: number;\n\tgroupKey: string;\n\ttimestamp: number;\n}\n\n/** AMT Gateway options */\nexport interface AMTGatewayOptions {\n\trelayAddress: string;\n\trelayPort?: number;\n\tonPacket?: (packet: AMTPacketInfo) => void;\n\tonStateChange?: (newState: GatewayState, oldState: GatewayState) => void;\n\tonError?: (error: Error) => void;\n\tonLog?: (message: string) => void;\n}\n\n/** Active AMT subscription */\ninterface AMTSubscription {\n\tkey: GroupKey;\n\tsubscriptionId: string;\n\tconfig: AMTSubscriptionConfig;\n\trelay?: AmtRelayInfo;\n\thandshakeComplete: boolean;\n\tkeepAliveTimer?: ReturnType<typeof setInterval>;\n\tlastKeepAlive?: number;\n}\n\n/** Check if address is IPv4 */\nfunction isIPv4(address: string): boolean {\n\treturn /^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(address);\n}\n\n/**\n * AMT Gateway Manager (Service Worker based)\n *\n * Manages AMT tunnel subscriptions using Service Worker for UDP communication.\n * Handles DRIAD discovery, AMT handshake, and packet decapsulation.\n */\nexport class AMTGatewayManager {\n\tprivate subscriptions = new Map<string, AMTSubscription>();\n\tprivate messageHandler?: (event: MessageEvent) => void;\n\tprivate pendingPackets = new Map<string, Array<{ data: ArrayBuffer; remoteAddress: string; remotePort: number }>>();\n\tprivate driad = new DRIADDiscovery();\n\n\tconstructor() {\n\t\t// Set up message handler for packets from service worker\n\t\tthis.messageHandler = (event: MessageEvent) => {\n\t\t\tconst data = event.data as { type: string; subscriptionId?: string; data?: unknown; error?: string; message?: string };\n\t\t\tconsole.log(`[AMT] SW message received:`, data.type);\n\t\t\tif (data.type === \"UDP_PACKET\") {\n\t\t\t\tthis.handlePacket(data);\n\t\t\t} else if (data.type === \"SOCKET_ERROR\") {\n\t\t\t\tthis.handleSocketError(data);\n\t\t\t} else if (data.type === \"DEBUG_LOG\") {\n\t\t\t\tconsole.log(`[AMT SW Debug] ${data.message}`);\n\t\t\t}\n\t\t};\n\n\t\t// Register message handler\n\t\tif (typeof navigator !== \"undefined\" && navigator.serviceWorker) {\n\t\t\tnavigator.serviceWorker.addEventListener(\"message\", this.messageHandler);\n\t\t\tconsole.log(\"[AMT] Registered service worker message handler\");\n\t\t} else {\n\t\t\tconsole.warn(\"[AMT] Service worker not available\");\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe via AMT tunnel\n\t */\n\tasync subscribe(config: AMTSubscriptionConfig): Promise<string> {\n\t\tconst { key } = config;\n\t\tconst keyStr = this.getKeyString(key);\n\n\t\tif (this.subscriptions.has(keyStr)) {\n\t\t\tconsole.log(`[AMT] Reusing existing subscription to ${keyStr}`);\n\t\t\treturn keyStr;\n\t\t}\n\n\t\ttry {\n\t\t\tlet relayAddress: string;\n\t\t\tlet discoveryMethod: \"driad\" | \"manual\";\n\t\t\tconst relayPort = config.relayPort || DEFAULT_AMT_RELAY_PORT;\n\n\t\t\tif (config.relayAddress) {\n\t\t\t\trelayAddress = config.relayAddress;\n\t\t\t\tdiscoveryMethod = \"manual\";\n\t\t\t\tconsole.log(`[AMT] Using configured relay ${relayAddress}:${relayPort}`);\n\t\t\t} else if (key.source) {\n\t\t\t\t// DRIAD (RFC 8777) discovers relays based on SOURCE address, not group\n\t\t\t\tconsole.log(`[AMT] No relay configured, discovering via DRIAD for source ${key.source}...`);\n\t\t\t\tconst relays = await this.driad.discoverRelays(key.source);\n\t\t\t\tconst bestRelay = await this.driad.selectBestRelay(relays);\n\n\t\t\t\tif (!bestRelay) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`AMT relay discovery failed for source ${key.source}. ` +\n\t\t\t\t\t\t\t`Please specify a relay address in the subscription configuration.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\trelayAddress = bestRelay.host;\n\t\t\t\tdiscoveryMethod = \"driad\";\n\t\t\t\tconsole.log(`[AMT] Discovered relay via DRIAD: ${relayAddress}:${relayPort}`);\n\t\t\t} else {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`AMT relay discovery requires a source address for SSM (RFC 8777). ` +\n\t\t\t\t\t\t`Please specify either a relay address or source address.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst subscriptionId = keyStr;\n\n\t\t\t// Initialize packet queue for handshake\n\t\t\tthis.pendingPackets.set(subscriptionId, []);\n\n\t\t\t// Create subscription\n\t\t\tconst subscription: AMTSubscription = {\n\t\t\t\tkey,\n\t\t\t\tsubscriptionId,\n\t\t\t\tconfig,\n\t\t\t\thandshakeComplete: false,\n\t\t\t\trelay: {\n\t\t\t\t\taddress: relayAddress,\n\t\t\t\t\tport: relayPort,\n\t\t\t\t\tdiscoveryMethod,\n\t\t\t\t\tdiscoveredAt: Date.now(),\n\t\t\t\t},\n\t\t\t};\n\n\t\t\tthis.subscriptions.set(keyStr, subscription);\n\n\t\t\t// Note: Actual UDP socket creation and handshake would require\n\t\t\t// Service Worker communication - this is a simplified version\n\t\t\tconsole.log(`[AMT] Subscription created for ${keyStr} via ${relayAddress}:${relayPort}`);\n\n\t\t\treturn keyStr;\n\t\t} catch (error) {\n\t\t\tconst subscription = this.subscriptions.get(keyStr);\n\t\t\tif (subscription) {\n\t\t\t\tthis.subscriptions.delete(keyStr);\n\t\t\t\tthis.pendingPackets.delete(subscription.subscriptionId);\n\t\t\t}\n\n\t\t\tthrow new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\n\t/**\n\t * Unsubscribe from AMT tunnel\n\t */\n\tasync unsubscribe(handle: string): Promise<void> {\n\t\tconst subscription = this.subscriptions.get(handle);\n\t\tif (!subscription) {\n\t\t\tthrow new Error(`Subscription ${handle} not found`);\n\t\t}\n\n\t\ttry {\n\t\t\t// Stop keep-alive timer\n\t\t\tif (subscription.keepAliveTimer) {\n\t\t\t\tclearInterval(subscription.keepAliveTimer);\n\t\t\t}\n\n\t\t\tconsole.log(`[AMT] Unsubscribed from ${handle}`);\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[AMT] Error during unsubscribe:\", error);\n\t\t} finally {\n\t\t\tthis.subscriptions.delete(handle);\n\t\t\tthis.pendingPackets.delete(subscription.subscriptionId);\n\t\t}\n\t}\n\n\t/**\n\t * Get list of active subscriptions\n\t */\n\tgetSubscriptions(): string[] {\n\t\treturn Array.from(this.subscriptions.keys());\n\t}\n\n\t/**\n\t * Handle packet from service worker\n\t */\n\tprivate handlePacket(message: { subscriptionId?: string; data?: unknown }): void {\n\t\tconst { subscriptionId, data } = message;\n\t\tif (!subscriptionId) return;\n\n\t\tconst subscription = this.subscriptions.get(subscriptionId);\n\t\tif (!subscription) {\n\t\t\tconsole.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);\n\t\t\treturn;\n\t\t}\n\n\t\t// During handshake, queue packets\n\t\tif (!subscription.handshakeComplete) {\n\t\t\tconst queue = this.pendingPackets.get(subscriptionId);\n\t\t\tconst packetData = data as { data: ArrayBuffer; remoteAddress: string; remotePort: number };\n\t\t\tif (queue) {\n\t\t\t\tqueue.push({\n\t\t\t\t\tdata: packetData.data,\n\t\t\t\t\tremoteAddress: packetData.remoteAddress,\n\t\t\t\t\tremotePort: packetData.remotePort,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// After handshake, forward to callback\n\t\tconst packetData = data as { data: ArrayBuffer };\n\t\tconst metadata: PacketMetadata = {\n\t\t\tsourceAddress: subscription.relay?.address || \"unknown\",\n\t\t\tsourcePort: subscription.relay?.port || 0,\n\t\t\ttransport: \"tunnel\",\n\t\t\ttimestamp: performance.now(),\n\t\t};\n\n\t\tsubscription.config.onPacket(packetData.data, metadata);\n\t}\n\n\t/**\n\t * Handle socket error from service worker\n\t */\n\tprivate handleSocketError(message: { subscriptionId?: string; error?: string }): void {\n\t\tconst { subscriptionId, error } = message;\n\t\tif (!subscriptionId) return;\n\n\t\tconst subscription = this.subscriptions.get(subscriptionId);\n\t\tif (!subscription) {\n\t\t\tconsole.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);\n\t\t\treturn;\n\t\t}\n\n\t\tsubscription.config.onError(new Error(error || \"Unknown error\"));\n\t}\n\n\t/**\n\t * Cleanup on destroy\n\t */\n\tdestroy(): void {\n\t\tif (this.messageHandler && typeof navigator !== \"undefined\" && navigator.serviceWorker) {\n\t\t\tnavigator.serviceWorker.removeEventListener(\"message\", this.messageHandler);\n\t\t}\n\n\t\tconst handles = Array.from(this.subscriptions.keys());\n\t\tfor (const handle of handles) {\n\t\t\tthis.unsubscribe(handle).catch((err) => {\n\t\t\t\tconsole.error(`[AMT] Error unsubscribing ${handle}:`, err);\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Get key string for HashMap\n\t */\n\tprivate getKeyString(key: GroupKey): string {\n\t\tconst source = key.source ? `@${key.source}` : \"\";\n\t\treturn `${key.group}:${key.port}${source}`;\n\t}\n}\n\n/**\n * AMT Gateway Client (Direct Sockets based)\n *\n * Connects to an AMT relay to receive multicast data via unicast tunnel.\n * Implements the gateway side of RFC 7450.\n * Supports multiple (S,G) groups over a single AMT tunnel.\n */\nexport class AMTGateway {\n\tprivate relayAddress: string;\n\tprivate relayPort: number;\n\tprivate groups = new Map<\n\t\tstring,\n\t\t{\n\t\t\tgroupPort: number;\n\t\t\tonPacket: ((packet: AMTPacketInfo) => void) | null;\n\t\t\tjoinedAt: number;\n\t\t}\n\t>();\n\n\t// Callbacks\n\tprivate onPacket: (packet: AMTPacketInfo) => void;\n\tprivate onStateChange: (newState: GatewayState, oldState: GatewayState) => void;\n\tprivate onError: (error: Error) => void;\n\tprivate onLog: (message: string) => void;\n\n\t// State\n\tprivate state: GatewayState = GATEWAY_STATES.IDLE;\n\tprivate nonce: Uint8Array | null = null;\n\tprivate responseMAC: Uint8Array | null = null;\n\tprivate relayConfirmedAddress: string | null = null;\n\tprivate lastDataTime = 0;\n\n\t// Stats\n\tprivate stats = {\n\t\tpacketsReceived: 0,\n\t\tbytesReceived: 0,\n\t\tqueriesReceived: 0,\n\t\tupdatesSet: 0,\n\t};\n\n\tconstructor(options: AMTGatewayOptions) {\n\t\tthis.relayAddress = options.relayAddress;\n\t\tthis.relayPort = options.relayPort || DEFAULT_AMT_RELAY_PORT;\n\n\t\tthis.onPacket = options.onPacket || (() => {});\n\t\tthis.onStateChange = options.onStateChange || (() => {});\n\t\tthis.onError = options.onError || ((err) => console.error(\"AMT Error:\", err));\n\t\tthis.onLog = options.onLog || ((msg) => console.log(\"AMT:\", msg));\n\t}\n\n\t/**\n\t * Generate random nonce\n\t */\n\tprivate generateNonce(): Uint8Array {\n\t\tconst nonce = new Uint8Array(4);\n\t\tcrypto.getRandomValues(nonce);\n\t\treturn nonce;\n\t}\n\n\t/**\n\t * Set gateway state\n\t */\n\tprivate setState(newState: GatewayState): void {\n\t\tconst oldState = this.state;\n\t\tthis.state = newState;\n\t\tthis.onLog(`State: ${oldState} -> ${newState}`);\n\t\tthis.onStateChange(newState, oldState);\n\t}\n\n\t/**\n\t * Join a multicast group\n\t */\n\tasync joinGroup(\n\t\tgroupAddress: string,\n\t\tgroupPort: number,\n\t\tsourceAddress: string | null = null,\n\t\tonPacket: ((packet: AMTPacketInfo) => void) | null = null,\n\t): Promise<string> {\n\t\tconst groupKey = `${sourceAddress || \"*\"},${groupAddress}`;\n\n\t\tif (this.groups.has(groupKey)) {\n\t\t\tthis.onLog(`Group already joined: ${groupKey}`);\n\t\t\treturn groupKey;\n\t\t}\n\n\t\tthis.groups.set(groupKey, {\n\t\t\tgroupPort,\n\t\t\tonPacket,\n\t\t\tjoinedAt: Date.now(),\n\t\t});\n\n\t\tthis.onLog(`Joined group: ${groupKey} (port ${groupPort})`);\n\t\treturn groupKey;\n\t}\n\n\t/**\n\t * Leave a multicast group\n\t */\n\tasync leaveGroup(groupAddress: string, sourceAddress: string | null = null): Promise<boolean> {\n\t\tconst groupKey = `${sourceAddress || \"*\"},${groupAddress}`;\n\n\t\tif (!this.groups.has(groupKey)) {\n\t\t\tthis.onLog(`Group not found: ${groupKey}`);\n\t\t\treturn false;\n\t\t}\n\n\t\tthis.groups.delete(groupKey);\n\t\tthis.onLog(`Left group: ${groupKey} (${this.groups.size} remaining)`);\n\n\t\tif (this.groups.size === 0) {\n\t\t\tthis.onLog(\"No groups remaining - closing AMT tunnel\");\n\t\t\tawait this.close();\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Get list of active groups\n\t */\n\tgetGroups(): Array<{\n\t\tgroupKey: string;\n\t\tgroupAddress: string;\n\t\tsourceAddress: string | null;\n\t\tgroupPort: number;\n\t\tjoinedAt: number;\n\t}> {\n\t\treturn Array.from(this.groups.entries()).map(([key, info]) => {\n\t\t\tconst [source, group] = key.split(\",\");\n\t\t\treturn {\n\t\t\t\tgroupKey: key,\n\t\t\t\tgroupAddress: group,\n\t\t\t\tsourceAddress: source !== \"*\" ? source : null,\n\t\t\t\tgroupPort: info.groupPort,\n\t\t\t\tjoinedAt: info.joinedAt,\n\t\t\t};\n\t\t});\n\t}\n\n\t/**\n\t * Open connection to AMT relay\n\t */\n\tasync open(): Promise<void> {\n\t\tif (this.state !== GATEWAY_STATES.IDLE) {\n\t\t\tthrow new Error(`Cannot open in state: ${this.state}`);\n\t\t}\n\n\t\tthis.setState(GATEWAY_STATES.DISCOVERING);\n\t\tthis.onLog(`Connecting to AMT relay ${this.relayAddress}:${this.relayPort}`);\n\t\tthis.onLog(`Active groups: ${this.groups.size}`);\n\n\t\t// Generate session nonce\n\t\tthis.nonce = this.generateNonce();\n\t\tthis.onLog(`Generated nonce: ${Array.from(this.nonce).map((b) => b.toString(16).padStart(2, \"0\")).join(\"\")}`);\n\n\t\t// Note: Actual UDPSocket connection would go here\n\t\t// This is a simplified version showing the state machine\n\t}\n\n\t/**\n\t * Close connection\n\t */\n\tasync close(): Promise<void> {\n\t\tif (this.state === GATEWAY_STATES.CLOSED) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setState(GATEWAY_STATES.LEAVING);\n\n\t\t// Clear all groups\n\t\tthis.groups.clear();\n\n\t\tthis.setState(GATEWAY_STATES.CLOSED);\n\t\tthis.onLog(\"AMT Gateway closed\");\n\t}\n\n\t/**\n\t * Get current statistics\n\t */\n\tgetStats(): typeof this.stats {\n\t\treturn { ...this.stats };\n\t}\n\n\t/**\n\t * Get current state\n\t */\n\tgetState(): GatewayState {\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Check if gateway is active and receiving data\n\t */\n\tisActive(): boolean {\n\t\treturn this.state === GATEWAY_STATES.ACTIVE;\n\t}\n}\n\n// Export singleton manager instance\nexport const amtGatewayManager = new AMTGatewayManager();\n", "/**\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", "/**\n * Thin transport wrapper for SSM multicast \u2014 join, socket, watchdog,\n * takeover/failback. Delegates raw packets to a callback (MmtpRouter\n * or any direct consumer).\n */\n\nimport {\n isMulticastTrackPacketId,\n subscriptionDeliversPacketBatches,\n toWindowMulticastSubscribeConfig,\n type WindowMulticastDelivery,\n type WindowMulticastGateway,\n type WindowMulticastSubscription,\n} from '@blockcast/mmt-transport'\nimport {\n MMTP_PACKET_ID_MAX,\n MMTP_SIGNALING_PACKET_ID,\n readMMTPPacketId,\n} from '@blockcast/mmt-base'\n\nexport interface MulticastTransportConfig {\n sourceAddress: string\n groupAddress: string\n port: number\n mode?: 'auto' | 'native' | 'tunnel'\n amtRelay?: string\n amtRelayPort?: number\n packetIds?: number[]\n tracks?: { name: string; packetId: number }[]\n}\n\nexport type ResolvedMulticastTransport = 'native' | 'amt'\n\n/**\n * Why a joined transport handed control back to MoQ.\n *\n * - `silence`: the data plane was live (at least one accepted packet since the\n * join) and then stayed silent for the catalog-derived failback window.\n * - `subscription-error`: the provider reported the subscription failed. This\n * is the provider's own explicit, typed outcome (a `MulticastError`, branch\n * on its `code`), e.g. a bridge whose AMT tunnel it declared dead.\n */\nexport type MulticastFailbackReason =\n | { readonly kind: 'silence'; readonly windowMs: number }\n | { readonly kind: 'subscription-error'; readonly error: unknown }\n\n/**\n * Why a join() resolved false, named by the step at which the join stopped,\n * never by what an error message says.\n *\n * - `disposed`: the transport was disposed before the join started, or while\n * the provider's subscribe() was pending.\n * - `unavailable`: there is no window.multicast gateway with a subscribe().\n * - `superseded`: a newer join() started while this join's subscribe() was\n * pending; the newer join owns the transport.\n * - `invalid-request`: the join config does not make a valid window.multicast\n * request (toWindowMulticastSubscribeConfig refused it), so the provider was\n * never asked.\n * - `provider-rejected`: the provider's subscribe() rejected or threw. The only\n * cause that carries the provider's own verdict (`providerError`).\n * - `invalid-subscription`: subscribe() resolved something that is not a usable\n * subscription: no event surface, a malformed `eventTypes`, an unknown\n * `delivery`, or an `addEventListener` that threw.\n * - `delivery-hook-failed`: the consumer's onDelivery hook threw for the\n * delivery the subscription declared, e.g. 'ordered-relay' to FEC tracks\n * configured without ordered geometry. A local configuration failure, not\n * the provider's.\n */\nexport type MulticastJoinDeclineReason =\n | 'disposed'\n | 'unavailable'\n | 'superseded'\n | 'invalid-request'\n | 'provider-rejected'\n | 'invalid-subscription'\n | 'delivery-hook-failed'\n\n/**\n * The typed part of a provider's subscribe() rejection. `name` and `code` are\n * the provider's own typed fields; the frozen multicast contract makes `code`\n * the one to branch on (e.g. a bridge's `amt_relay_refused`). `message` is\n * free text carried for humans only: never branch on it.\n */\nexport interface MulticastJoinProviderError {\n readonly name: string\n readonly message: string\n /** Present only when the provider's error carries a string `code`. */\n readonly code?: string\n}\n\n/**\n * How a join() resolved: joined, or the typed reason it resolved false.\n * `providerError` is present only for `provider-rejected`, and only when the\n * provider's rejection value carries a string `name` and `message`.\n */\nexport type MulticastJoinOutcome =\n | { readonly ok: true }\n | {\n readonly ok: false\n readonly reason: MulticastJoinDeclineReason\n readonly providerError?: MulticastJoinProviderError\n }\n\n/**\n * Receive counters plus the ingress-filter drop counters.\n *\n * `packetsReceived` / `bytesReceived` count only packets that passed the\n * allow-list and were handed to the page \u2014 a rejected packet must never\n * inflate them. The `dropped*` counters are the other half of that story:\n * without them a filtered PID is indistinguishable from a PID that was never\n * transmitted, which is exactly the ambiguity that makes an all-zero FEC\n * repair count unreadable (a repair leg dropped here and a repair leg the\n * publisher never sent produce byte-identical downstream telemetry).\n */\nexport interface MulticastTransportStats {\n packetsReceived: number\n bytesReceived: number\n /** Parsed cleanly, but the packet ID is absent from the join's allow-list. */\n packetsDroppedUnregisteredPid: number\n /** MMTP header did not parse; the packet ID is unknown, so it cannot be attributed. */\n packetsDroppedUnparseable: number\n /**\n * packet_id 0, the MMTP signaling flow. Expected traffic on a protected\n * session (moq_mmt sends the AL-FEC message on it), so it is counted apart\n * from the unregistered-PID fault counter and never takes a\n * `droppedPacketIds` slot.\n */\n packetsDroppedSignaling: number\n}\n\n/**\n * join() calls by how they resolved, cumulative for the life of the transport:\n * across rejoins, failed joins and dispose. `succeeded` plus every `declined`\n * count is the number of join() calls that resolved, superseded ones\n * included; a join() that rejects counts in none (see\n * MulticastTransport.lastJoinOutcome).\n */\nexport interface MulticastJoinStats {\n /** join() calls that resolved true. */\n readonly succeeded: number\n /** join() calls that resolved false, by the reason each one did. */\n readonly declined: Readonly<Record<MulticastJoinDeclineReason, number>>\n}\n\nconst JOIN_SUCCEEDED: MulticastJoinOutcome = Object.freeze({ ok: true })\n\n/**\n * The typed part of a provider's subscribe() rejection, or undefined when the\n * rejection value is not an error with a string `name` and `message`. Nothing\n * is filled in for a field the provider did not type, and the message is\n * carried, never read.\n */\nfunction providerErrorOf(error: unknown): MulticastJoinProviderError | undefined {\n if (typeof error !== 'object' || error === null) return undefined\n const fields = error as { readonly name?: unknown; readonly message?: unknown; readonly code?: unknown }\n let name: unknown\n let message: unknown\n let code: unknown\n try {\n name = fields.name\n message = fields.message\n code = fields.code\n } catch {\n // A rejection value whose fields throw on read (a revoked Proxy) types\n // nothing. Reading it must not turn a declined join into a rejected one.\n return undefined\n }\n if (typeof name !== 'string' || typeof message !== 'string') return undefined\n return Object.freeze(typeof code === 'string' ? { name, message, code } : { name, message })\n}\n\n/**\n * Distinct unregistered PIDs retained for diagnostics. A conformant catalog\n * advertises a handful of PIDs per endpoint, so this only has to be large\n * enough to name every leg of a mis-provisioned one; the cap exists so a\n * malformed or hostile stream cycling the 16-bit PID space cannot grow the map\n * without bound. Drops keep counting in aggregate after the cap.\n */\nconst MAX_TRACKED_DROPPED_PIDS = 16\n\ntype MulticastGlobals = typeof globalThis & {\n multicast?: WindowMulticastGateway\n}\n\nfunction getMulticastGlobals(): MulticastGlobals {\n return globalThis as MulticastGlobals\n}\n\nfunction hasMulticastGateway(): boolean {\n return typeof getMulticastGlobals().multicast?.subscribe === 'function'\n}\n\n/**\n * The packet one `bytes` event (or one element of a `packets` batch) carries,\n * as a view of the provider's own bytes. A Uint8Array is used as it is; the\n * bridge hands every packet over as one, so the common path builds nothing.\n */\nfunction normalizeBytesEventDetail(detail: unknown): Uint8Array | null {\n const value =\n detail && typeof detail === 'object' && 'data' in detail\n ? (detail as { data?: unknown }).data\n : detail\n if (value instanceof Uint8Array) return value\n if (value instanceof ArrayBuffer) return new Uint8Array(value)\n if (ArrayBuffer.isView(value)) {\n return new Uint8Array(value.buffer, value.byteOffset, value.byteLength)\n }\n return null\n}\n\n/**\n * A provider that can replace a live subscription's MMTP packet-ID filter\n * without rejoining its socket: the frozen multicast contract's\n * `Subscription.setPacketIds` (pim `packages/multicast-contract`). It is not\n * declared on every provider this transport meets, so it is detected, never\n * assumed.\n */\ninterface PacketFilterUpdater {\n setPacketIds(packetIds: readonly number[]): Promise<void> | void\n}\n\nfunction packetFilterUpdater(\n subscription: WindowMulticastSubscription,\n): PacketFilterUpdater['setPacketIds'] | undefined {\n const update = (subscription as Partial<PacketFilterUpdater>).setPacketIds\n return typeof update === 'function' ? update.bind(subscription) : undefined\n}\n\nfunction sameIds(a: ReadonlySet<number>, b: ReadonlySet<number>): boolean {\n if (a.size !== b.size) return false\n for (const id of a) if (!b.has(id)) return false\n return true\n}\n\n/**\n * The provider-side packet filter of one live join, and the in-place updates\n * sent to it. It lives exactly as long as the join: a later join or release\n * replaces it, and an acknowledgement for a replaced one is ignored.\n */\ninterface ProviderFilter {\n /**\n * The filter as the newest acknowledged update left it: the packet IDs the\n * provider forwards, or null when it forwards every ID. Until an update is\n * acknowledged it is the join's own filter.\n */\n acknowledged: ReadonlySet<number> | null\n /** Updates sent to the provider, numbered from 1 in send order. */\n sent: number\n /** The number of the newest acknowledged update; 0 for the join's own filter. */\n acknowledgedUpdate: number\n /** Updates sent and not yet settled. */\n inFlight: number\n}\n\n/** Which packet surface a join listens to, and its listener. */\ntype PacketListener =\n | { readonly type: 'bytes'; readonly listener: (event: CustomEvent<unknown>) => void }\n | { readonly type: 'packets'; readonly listener: (event: CustomEvent<readonly unknown[]>) => void }\n\nexport class MulticastTransport<TPacketContext = never> {\n #onPacket: (packet: Uint8Array, context?: TPacketContext) => void\n #subscription: WindowMulticastSubscription | null = null\n #packetListener: PacketListener | null = null\n #resolvedTransport: ResolvedMulticastTransport | undefined\n #videoConfirmed = false\n #audioConfirmed = false\n // One armed no-packets timer per silence window (BLO-36599). Packets only\n // record #lastPacketAtMs; the timer re-arms for the remainder when it wakes\n // inside the window, instead of a clear+set pair on every packet.\n #watchdog: ReturnType<typeof setTimeout> | null = null\n #lastPacketAtMs = 0\n #failbackWindowMs: number | null = null\n #onTakeover: (() => void) | null = null\n #onFailback: ((reason: MulticastFailbackReason) => void) | null = null\n // Data-plane-live edge: the first packet this join accepted. Everything\n // before it is join latency, not silence (see #resetWatchdog).\n #dataPlaneLive = false\n #onDataPlaneLive: (() => void) | null = null\n #errorListener: ((event: CustomEvent<unknown>) => void) | null = null\n #onDelivery: ((delivery: WindowMulticastDelivery) => void) | null = null\n #delivery: WindowMulticastDelivery | undefined\n #disposed = false\n #stats: MulticastTransportStats = {\n packetsReceived: 0,\n bytesReceived: 0,\n packetsDroppedUnregisteredPid: 0,\n packetsDroppedUnparseable: 0,\n packetsDroppedSignaling: 0,\n }\n // join() calls by how they resolved; see joinStats.\n #joinsSucceeded = 0\n readonly #joinsDeclined: Record<MulticastJoinDeclineReason, number> = {\n disposed: 0,\n unavailable: 0,\n superseded: 0,\n 'invalid-request': 0,\n 'provider-rejected': 0,\n 'invalid-subscription': 0,\n 'delivery-hook-failed': 0,\n }\n // The newest resolved join's outcome (see lastJoinOutcome) and the join\n // generation it belongs to. Generations are issued in start order, so a\n // join that resolves after a newer join has recorded leaves it standing.\n #lastJoinOutcome: MulticastJoinOutcome | undefined = undefined\n #lastJoinOutcomeGeneration = 0\n #droppedPidCounts = new Map<number, number>()\n // Accepted bytes per MMTP packet_id; see bytesReceivedForPacketIds. Each\n // value is a mutable cell, so the per-packet update is one lookup and an\n // in-place add: no insertion once the key exists, and no re-boxed number\n // per packet once a count outgrows the small-integer range.\n readonly #acceptedBytesByPacketId = new Map<number, { bytes: number }>()\n #packetIds: ReadonlySet<number> | null = null\n // What the live join delivers with each accepted packet. A field, not a\n // listener closure, so an in-place admission change (setContext) applies\n // from the next packet on without rejoining.\n #context: TPacketContext | undefined = undefined\n // The live join's provider-side filter. An in-place widening must reach it,\n // or the provider never delivers the new ID; null without a live join.\n #providerFilter: ProviderFilter | null = null\n // IDs an in-place update withdrew while the provider may still be\n // delivering them. Their late packets are still counted as filtered, but\n // they are the expected tail of a deliberate change, not a provisioning\n // fault, so they are counted apart and do not raise the unadvertised-PID\n // warning.\n readonly #withdrawnPacketIds = new Set<number>()\n #withdrawnPacketIdDrops = 0\n // Distinct packet IDs dropped as provisioning faults: on the wire but in\n // no allow-list the join or an in-place update declared. Capped with\n // droppedPacketIds.\n readonly #unadvertisedPacketIds = new Set<number>()\n #joinGeneration = 0\n\n constructor(onPacket: (packet: Uint8Array, context?: TPacketContext) => void) {\n this.#onPacket = onPacket\n }\n\n static isAvailable(): boolean {\n return hasMulticastGateway()\n }\n\n async join(config: MulticastTransportConfig, context?: TPacketContext): Promise<boolean> {\n const generation = ++this.#joinGeneration\n this.#releaseSubscription()\n this.#dataPlaneLive = false\n\n if (this.#disposed) return this.#declined(generation, 'disposed')\n if (!MulticastTransport.isAvailable()) {\n console.warn('[MulticastTransport] Multicast API not available')\n return this.#declined(generation, 'unavailable')\n }\n\n // What a throw from the step in progress means. Each step names its\n // failure before it runs, so the catch classifies a decline by where the\n // join stopped, never by what the error says.\n let failure: MulticastJoinDeclineReason = 'invalid-request'\n try {\n const packetIds = config.packetIds?.length ? new Set(config.packetIds) : null\n console.log(\n `[MulticastTransport] window.multicast.subscribe(${config.sourceAddress}, ${config.groupAddress}, ${config.port})`,\n )\n const subscribeConfig = toWindowMulticastSubscribeConfig({\n source: config.sourceAddress,\n group: config.groupAddress,\n port: config.port,\n mode: config.mode,\n amtRelay: config.amtRelay,\n amtRelayPort: config.amtRelayPort,\n packetIds: config.packetIds,\n tracks: config.tracks,\n })\n failure = 'provider-rejected'\n const subscription = await getMulticastGlobals().multicast!.subscribe!(subscribeConfig)\n failure = 'invalid-subscription'\n console.log(`[MulticastTransport] window.multicast.subscribe returned:`, !!subscription)\n\n if (this.#disposed || generation !== this.#joinGeneration) {\n void subscription.unsubscribe()\n return this.#declined(generation, this.#displacedReason())\n }\n if (typeof subscription.addEventListener !== 'function') {\n if (typeof subscription.unsubscribe === 'function') {\n void subscription.unsubscribe()\n }\n throw new Error('window.multicast subscription does not support bytes events')\n }\n\n // Own the subscription before anything below can throw, so a failed\n // join's release reaches unsubscribe() instead of leaking it.\n this.#subscription = subscription\n const live = () => !this.#disposed && generation === this.#joinGeneration\n // Exactly one packet surface. A provider that advertises the `packets`\n // extension delivers each reader batch as one event, so the page pays\n // one DOM event and one liveness read per batch instead of per packet\n // (BLO-36599). Every packet still goes through the same filter, counters\n // and callback, in arrival order, as a view of the provider's bytes.\n const packetListener: PacketListener = subscriptionDeliversPacketBatches(subscription)\n ? {\n type: 'packets',\n listener: (event) => {\n if (!live()) return\n this.#deliverBatch(event.detail, live)\n },\n }\n : {\n type: 'bytes',\n listener: (event) => {\n if (!live()) return\n if (this.#deliver(event.detail)) this.#resetWatchdog()\n },\n }\n this.#packetListener = packetListener\n this.#packetIds = packetIds\n this.#providerFilter = {\n acknowledged: packetIds,\n sent: 0,\n acknowledgedUpdate: 0,\n inFlight: 0,\n }\n this.#context = context\n // Seed the allow-list's counters so accepted packets never insert. An\n // id already counted by an earlier join keeps its count: the counters\n // are cumulative across rejoins, as bytesReceived is.\n if (packetIds) {\n for (const packetId of packetIds) {\n if (!this.#acceptedBytesByPacketId.has(packetId)) {\n this.#acceptedBytesByPacketId.set(packetId, { bytes: 0 })\n }\n }\n }\n // The request origin is the accepted acquisition contract. The\n // gateway no longer exposes a mutable transport/transportchange\n // surface, so assign this only after subscribe() has resolved and the\n // returned subscription has passed the event-surface check above.\n this.#resolvedTransport = subscribeConfig.origin === 'amt' ? 'amt' : 'native'\n // The provider alone knows whether its packets are independent\n // datagrams or an ordered relay of one socket; undeclared promises no\n // ordering. Announce it before the listener can deliver a packet. A\n // rejected declaration fails the join and releases the subscription.\n const delivery = subscription.delivery ?? 'datagram'\n if (delivery !== 'datagram' && delivery !== 'ordered-relay') {\n throw new Error(\n `window.multicast subscription declared unknown delivery ${String(delivery)}`,\n )\n }\n this.#delivery = delivery\n failure = 'delivery-hook-failed'\n this.#onDelivery?.(delivery)\n failure = 'invalid-subscription'\n // The provider's explicit, terminal subscription failure. Before the\n // first packet this is the only signal that bounds a join, so it must\n // reach the failback path rather than be dropped.\n const onError = (event: CustomEvent<unknown>) => {\n if (this.#disposed || generation !== this.#joinGeneration) return\n this.#failBackOnSubscriptionError(event.detail)\n }\n this.#errorListener = onError\n if (packetListener.type === 'packets') {\n subscription.addEventListener('packets', packetListener.listener)\n } else {\n subscription.addEventListener('bytes', packetListener.listener)\n }\n subscription.addEventListener('error', onError)\n // No watchdog here. A resolved subscribe() is the provider's join\n // outcome, not proof that the data plane is live: over AMT it resolves\n // once the gateway has sent its Membership Update, and the relay's\n // upstream join plus one path RTT still precede the first packet. The\n // silence window is a packet-gap clock for a stream that has flowed;\n // charging join latency to it failed back joins that went on to play.\n // The join phase is bounded by the provider's typed error or the host's\n // takeover deadline (see SessionManager.joinActive). The window starts\n // at the first accepted packet (#resetWatchdog).\n console.log(\n `[MulticastTransport] Joined ${config.groupAddress}:${config.port}`,\n )\n return this.#joined(generation)\n } catch (err) {\n if (this.#disposed || generation !== this.#joinGeneration) {\n return this.#declined(generation, this.#displacedReason())\n }\n this.#releaseSubscription()\n console.error('[MulticastTransport] Failed to join SSM group:', err)\n return this.#declined(\n generation,\n failure,\n failure === 'provider-rejected' ? providerErrorOf(err) : undefined,\n )\n }\n }\n\n /** Record a join that resolved true; returns its result. */\n #joined(generation: number): true {\n this.#joinsSucceeded++\n this.#recordJoinOutcome(generation, JOIN_SUCCEEDED)\n return true\n }\n\n /** Record a join that resolved false, and why; returns its result. */\n #declined(\n generation: number,\n reason: MulticastJoinDeclineReason,\n providerError?: MulticastJoinProviderError,\n ): false {\n this.#joinsDeclined[reason]++\n this.#recordJoinOutcome(\n generation,\n Object.freeze(\n providerError === undefined ? { ok: false, reason } : { ok: false, reason, providerError },\n ),\n )\n return false\n }\n\n /**\n * Why a join that no longer owns the transport resolved false. dispose()\n * advances the join generation too, so disposal is checked first.\n */\n #displacedReason(): 'disposed' | 'superseded' {\n return this.#disposed ? 'disposed' : 'superseded'\n }\n\n #recordJoinOutcome(generation: number, outcome: MulticastJoinOutcome): void {\n if (generation <= this.#lastJoinOutcomeGeneration) return\n this.#lastJoinOutcomeGeneration = generation\n this.#lastJoinOutcome = outcome\n }\n\n /**\n * One packet from the subscription: filter, count, and hand it to the\n * callback. Returns whether it was delivered; the caller notes liveness.\n */\n #deliver(detail: unknown): boolean {\n const packet = normalizeBytesEventDetail(detail)\n if (!packet) {\n this.#stats.packetsDroppedUnparseable++\n return false\n }\n if (!this.#acceptPacket(packet)) return false\n // Read per packet: a callback that changes the admission (an in-place\n // rendition switch committing on this very packet) takes effect from the\n // next packet, including the rest of a batch.\n this.#onPacket(packet, this.#context)\n return true\n }\n\n /**\n * One `packets` event. Each packet is delivered as a `bytes` event would\n * have delivered it, including isolation: a packet whose callback throws\n * does not cost the rest of its batch, as it would not have cost the next\n * `bytes` event. The first failure is rethrown once the batch is done, so\n * it reaches the event-listener error reporting a `bytes` listener's throw\n * reached; any further one is rethrown from its own microtask, which the\n * host reports the same way, so no failure is swallowed. A callback that\n * disposes the transport or starts another join ends the batch there, as\n * the per-packet guard would have ignored the remaining events.\n */\n #deliverBatch(\n batch: readonly unknown[],\n live: () => boolean,\n ): void {\n if (!Array.isArray(batch)) {\n throw new TypeError('[MulticastTransport] packets event detail must be an array of packets')\n }\n let delivered = false\n let failures: unknown[] | null = null\n for (let i = 0; i < batch.length; i++) {\n try {\n if (this.#deliver(batch[i])) delivered = true\n } catch (error) {\n if (failures === null) failures = []\n failures.push(error)\n }\n if (!live()) break\n }\n // One liveness read covers the batch: every packet in it arrived in the\n // same provider task. A batch that delivered nothing, like a rejected\n // bytes event, is not liveness.\n if (delivered && live()) this.#resetWatchdog()\n if (failures !== null) {\n for (let i = 1; i < failures.length; i++) {\n const failure = failures[i]\n queueMicrotask(() => {\n throw failure\n })\n }\n throw failures[0]\n }\n }\n\n /** Feed a raw packet from an external source (e.g., AMT tunnel or ASM subscription). */\n feedPacket(data: ArrayBuffer | Uint8Array, context?: TPacketContext): void {\n if (this.#disposed) return\n const packet = data instanceof Uint8Array ? data : new Uint8Array(data)\n if (!this.#acceptPacket(packet)) return\n this.#onPacket(packet, context)\n this.#resetWatchdog()\n }\n\n confirmVideo(): void {\n this.#videoConfirmed = true\n this.#checkTakeover()\n }\n\n confirmAudio(): void {\n this.#audioConfirmed = true\n this.#checkTakeover()\n }\n\n onTakeover(cb: () => void): void {\n this.#onTakeover = cb\n }\n\n onFailback(cb: (reason: MulticastFailbackReason) => void): void {\n this.#onFailback = cb\n }\n\n /**\n * Called on every successful join, before the joined subscription can\n * deliver a packet, with how that subscription delivers packets.\n */\n onDelivery(cb: (delivery: WindowMulticastDelivery) => void): void {\n this.#onDelivery = cb\n }\n\n /** How the current subscription delivers packets, once joined. */\n get delivery(): WindowMulticastDelivery | undefined {\n return this.#delivery\n }\n\n /**\n * Fires once per join, at the first packet the join accepts (after the PID\n * filter; a rejected packet is not liveness). This is the data-plane-live\n * edge, where this transport's silence window starts. A join whose source\n * never sends never reaches it, so a host may start a media-arrival budget\n * here only if the provider bounds the join phase with a typed subscription\n * error (see SessionManager.joinActive); otherwise the host starts it when\n * the subscribe settles.\n */\n onDataPlaneLive(cb: () => void): void {\n this.#onDataPlaneLive = cb\n }\n\n /** Whether the current join has accepted a packet (see onDataPlaneLive). */\n get dataPlaneLive(): boolean {\n return this.#dataPlaneLive\n }\n\n setFailbackWindowMs(windowMs: number): void {\n if (!Number.isFinite(windowMs) || windowMs <= 0) {\n throw new Error(\n `[MulticastTransport] failbackWindowMs=${windowMs}. ` +\n `Derive a positive failback window from the stream catalog.`,\n )\n }\n this.#failbackWindowMs = windowMs\n // An armed timer was scheduled against the previous window; re-derive its\n // wake-up from the last packet so a narrower window is not enforced late.\n if (this.#watchdog !== null) {\n clearTimeout(this.#watchdog)\n const silentMs = performance.now() - this.#lastPacketAtMs\n this.#armWatchdog(Math.max(0, Math.ceil(windowMs - silentMs)))\n }\n }\n\n get stats(): MulticastTransportStats {\n return this.#stats\n }\n\n /**\n * How the newest join() to resolve did: `{ ok: true }`, or `{ ok: false }`\n * with the typed reason it resolved false (and, for `provider-rejected`, the\n * provider's typed error fields). Undefined until a join resolves. Branch on\n * `reason` and `providerError.code`, never on log text or\n * `providerError.message`.\n *\n * Newest is by start order: a join that resolves after a newer join has\n * recorded its outcome does not replace it, so once `await join()` resolves\n * this names that join's outcome unless a newer join has already resolved.\n * It records how a join resolved, not whether it is still joined (see\n * `joined`); a join() that rejects instead of resolving records nothing,\n * the rejection being its report. Frozen: a later outcome replaces the\n * object, never changes it.\n */\n get lastJoinOutcome(): MulticastJoinOutcome | undefined {\n return this.#lastJoinOutcome\n }\n\n /**\n * join() calls by how they resolved (see MulticastJoinStats). A fresh frozen\n * snapshot on every read, so a later join never changes one already read.\n * Kept apart from `stats`, which is the live counter object.\n */\n get joinStats(): MulticastJoinStats {\n return Object.freeze({\n succeeded: this.#joinsSucceeded,\n declined: Object.freeze({ ...this.#joinsDeclined }),\n })\n }\n\n /**\n * Accepted bytes summed over `packetIds`: the `bytesReceived` share of those\n * MMTP packet_ids. Counted by the same rule, on every path that counts\n * `bytesReceived` (only packets that pass the ingress filter; packet_id 0\n * signaling never), and cumulative for the life of the transport, across\n * rejoins, as `bytesReceived` is. An id never accepted contributes 0. An\n * unparseable packet admitted by a join without an allow-list has no\n * packet_id, so it counts toward `bytesReceived` only.\n *\n * Use it to scope an ABR throughput sample to the active tier's source and\n * repair packet_ids when the join admits more than the active tier (a\n * shared group joined across rendition switches); `bytesReceived` is then\n * the whole allow-list's arrival, not the active tier's. A Set, so an id\n * shared by two tracks of the tier is counted once.\n *\n * Monotonic only for a fixed set: each id has counted since it was first\n * admitted, so after a tier switch the new set's sum is not a continuation\n * of the old one. A cumulative consumer takes a fresh baseline for the new\n * set and never differences across sets.\n */\n bytesReceivedForPacketIds(packetIds: ReadonlySet<number>): number {\n let bytes = 0\n for (const packetId of packetIds) {\n bytes += this.#acceptedBytesByPacketId.get(packetId)?.bytes ?? 0\n }\n return bytes\n }\n\n /**\n * Per-PID drop breakdown, newest-capped at MAX_TRACKED_DROPPED_PIDS distinct\n * PIDs. This is what names the offender: `packetsDroppedUnregisteredPid`\n * says the ingress filter fired, this says it fired on 0x8001.\n */\n get droppedPacketIds(): ReadonlyMap<number, number> {\n return this.#droppedPidCounts\n }\n\n /**\n * Distinct packet IDs dropped as provisioning faults: arriving on the wire\n * but in no allow-list the join or an in-place update (setPacketIds)\n * declared, the drops that raise the unadvertised-PID warning. A packet ID\n * an in-place update withdrew is not one (see withdrawnPacketIdDrops).\n * Cumulative across rejoins and capped with droppedPacketIds.\n */\n get unadvertisedPacketIds(): ReadonlySet<number> {\n return this.#unadvertisedPacketIds\n }\n\n /**\n * Packets dropped because an in-place update (setPacketIds) withdrew their\n * packet ID while the provider may still deliver it: the expected tail of\n * a deliberate change. A subset of packetsDroppedUnregisteredPid,\n * cumulative across rejoins.\n */\n get withdrawnPacketIdDrops(): number {\n return this.#withdrawnPacketIdDrops\n }\n\n /** Whether a subscription is live: joined, and not yet released or replaced. */\n get joined(): boolean {\n return this.#subscription !== null\n }\n\n /**\n * What the live join delivers with every accepted packet (the join's\n * `context`, or the last setContext), or undefined without a live join.\n */\n get context(): TPacketContext | undefined {\n return this.#context\n }\n\n /**\n * The live join's page-side packet-ID allow-list, or null when it admits\n * every packet ID (a join that carried none, or no live join).\n */\n get packetIds(): ReadonlySet<number> | null {\n return this.#packetIds\n }\n\n /**\n * Replace what the live join delivers with each accepted packet, from the\n * next packet on. The subscription (socket, group membership, AMT tunnel)\n * is untouched: this is how an in-place rendition switch moves the router's\n * ingress identities without leaving and re-joining the group.\n */\n setContext(context: TPacketContext): void {\n if (this.#subscription === null) {\n throw new Error(\n '[MulticastTransport] setContext needs a live join; join() carries the first context',\n )\n }\n this.#context = context\n }\n\n /**\n * Replace the live join's packet-ID allow-list in place, without rejoining.\n *\n * The page-side filter changes synchronously, before this returns: the\n * next packet is admitted or dropped by the new list. The provider's own\n * filter is updated through the subscription's `setPacketIds` when it has\n * one; the returned promise settles with the provider's acknowledgement of\n * this update. It resolves at once, telling the provider nothing, only\n * when the provider cannot be told, or when no earlier update is still in\n * flight and the provider already forwards exactly this list.\n *\n * While an update is in flight the provider's filter is not known, so every\n * later update is sent: one skipped because it matched the last\n * acknowledged filter would let the in-flight update land after it, and\n * leave the provider forwarding a list nobody asked for last. Updates are\n * numbered in send order and the newest acknowledged one describes the\n * provider, so an acknowledgement that overtakes an older one is not undone\n * by it.\n *\n * A provider without `setPacketIds` can still be narrowed: its wider filter\n * keeps delivering the dropped IDs and the page-side filter discards them.\n * It cannot be widened, since it would never deliver the added IDs; that is\n * refused with a throw before anything changes. An ID this update withdraws\n * may keep arriving until the provider applies the narrower list; those\n * packets are counted as filtered like any other and in\n * withdrawnPacketIdDrops, but are not unadvertised packet IDs: their\n * arrival is the expected tail of this change rather than a provisioning\n * fault.\n */\n setPacketIds(packetIds: readonly number[]): Promise<void> {\n const subscription = this.#subscription\n const filter = this.#providerFilter\n if (subscription === null || filter === null) {\n throw new Error(\n '[MulticastTransport] setPacketIds needs a live join; join() carries the first allow-list',\n )\n }\n if (!Array.isArray(packetIds) || packetIds.length === 0) {\n throw new RangeError(\n '[MulticastTransport] setPacketIds needs a non-empty packet-ID list: ' +\n 'an in-place update names the packet IDs it admits',\n )\n }\n if (!packetIds.every(isMulticastTrackPacketId)) {\n throw new RangeError(\n `[MulticastTransport] packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}; ` +\n \"packetId 0 is the MMTP signaling flow and is never a track's\",\n )\n }\n const next: ReadonlySet<number> = new Set(packetIds)\n const provider = filter.acknowledged\n const update = packetFilterUpdater(subscription)\n // Without an updater nothing is ever sent, so the join's filter is exact.\n if (update === undefined && provider !== null) {\n const missing = [...next].filter((id) => !provider.has(id))\n if (missing.length > 0) {\n throw new Error(\n '[MulticastTransport] this subscription cannot widen its packet filter in place ' +\n `(the provider exposes no setPacketIds); rejoin to admit ${missing.join(', ')}`,\n )\n }\n }\n const previous = this.#packetIds\n this.#packetIds = next\n for (const id of next) {\n this.#withdrawnPacketIds.delete(id)\n if (!this.#acceptedBytesByPacketId.has(id)) {\n this.#acceptedBytesByPacketId.set(id, { bytes: 0 })\n }\n }\n if (previous !== null) {\n for (const id of previous) if (!next.has(id)) this.#withdrawnPacketIds.add(id)\n }\n if (update === undefined) return Promise.resolve()\n if (filter.inFlight === 0 && provider !== null && sameIds(provider, next)) {\n return Promise.resolve()\n }\n const number = ++filter.sent\n filter.inFlight++\n let applied: Promise<void>\n try {\n applied = Promise.resolve(update([...next]))\n } catch (error) {\n applied = Promise.reject(error)\n }\n return applied.then(\n () => {\n if (this.#providerFilter !== filter) return\n filter.inFlight--\n if (number > filter.acknowledgedUpdate) {\n filter.acknowledgedUpdate = number\n filter.acknowledged = next\n }\n },\n (error: unknown) => {\n // A refused update leaves the provider on the filter it had.\n if (this.#providerFilter === filter) filter.inFlight--\n throw error\n },\n )\n }\n\n get resolvedTransport(): ResolvedMulticastTransport | undefined {\n return this.#resolvedTransport\n }\n\n dispose(): void {\n ++this.#joinGeneration\n this.#disposed = true\n this.#releaseSubscription()\n }\n\n #releaseSubscription(): void {\n if (this.#watchdog) clearTimeout(this.#watchdog)\n this.#watchdog = null\n if (this.#subscription) {\n const packetListener = this.#packetListener\n if (packetListener?.type === 'packets') {\n this.#subscription.removeEventListener('packets', packetListener.listener)\n } else if (packetListener) {\n this.#subscription.removeEventListener('bytes', packetListener.listener)\n }\n if (this.#errorListener) {\n this.#subscription.removeEventListener('error', this.#errorListener)\n }\n void this.#subscription.unsubscribe()\n }\n this.#subscription = null\n this.#packetListener = null\n this.#errorListener = null\n this.#resolvedTransport = undefined\n this.#delivery = undefined\n this.#packetIds = null\n this.#providerFilter = null\n this.#context = undefined\n this.#withdrawnPacketIds.clear()\n }\n\n /** The ingress filter; an accepted packet is counted here, once. */\n #acceptPacket(packet: Uint8Array): boolean {\n // This early read is intentional: rejected PIDs must not affect page\n // delivery, receive statistics, or the failback liveness watchdog. It\n // refuses exactly what parseMMTPHeader refuses but builds no header\n // object; the router parses the header once, later (BLO-36599).\n const packetId = readMMTPPacketId(packet)\n if (packetId === null) {\n // Without an allow-list there is no filter to enforce, so an unparseable\n // packet keeps flowing to the router as before.\n if (!this.#packetIds) {\n this.#countAccepted(packet, null)\n return true\n }\n this.#stats.packetsDroppedUnparseable++\n return false\n }\n // packet_id 0 is the MMTP signaling flow and never a track's. Drop it even\n // when the join carried no allow-list (a legacy catalog whose tracks omit\n // packetId): the discard rule must not depend on how the catalog was written.\n if (packetId === MMTP_SIGNALING_PACKET_ID) {\n this.#stats.packetsDroppedSignaling++\n return false\n }\n if (!this.#packetIds || this.#packetIds.has(packetId)) {\n this.#countAccepted(packet, packetId)\n return true\n }\n this.#recordUnregisteredPidDrop(packetId)\n return false\n }\n\n /** Receive counters for one accepted packet; `packetId` is null when it has none. */\n #countAccepted(packet: Uint8Array, packetId: number | null): void {\n this.#stats.packetsReceived++\n this.#stats.bytesReceived += packet.byteLength\n if (packetId === null) return\n const counter = this.#acceptedBytesByPacketId.get(packetId)\n if (counter !== undefined) {\n counter.bytes += packet.byteLength\n } else {\n // First sight of this id on a join with no allow-list (an allow-list's\n // ids are seeded at join). Bounded by the 16-bit packet_id space.\n this.#acceptedBytesByPacketId.set(packetId, { bytes: packet.byteLength })\n }\n }\n\n #recordUnregisteredPidDrop(packetId: number): void {\n this.#stats.packetsDroppedUnregisteredPid++\n const withdrawn = this.#withdrawnPacketIds.has(packetId)\n if (withdrawn) this.#withdrawnPacketIdDrops++\n const seen = this.#droppedPidCounts.get(packetId)\n if (seen !== undefined) {\n this.#droppedPidCounts.set(packetId, seen + 1)\n return\n }\n if (this.#droppedPidCounts.size >= MAX_TRACKED_DROPPED_PIDS) return\n this.#droppedPidCounts.set(packetId, 1)\n if (withdrawn) return\n this.#unadvertisedPacketIds.add(packetId)\n // Warn once per distinct PID. A PID arriving on the wire but missing from\n // the catalog's advertised set is a provisioning fault, not stream noise,\n // and it is silent in every downstream counter.\n console.warn(\n `[MulticastTransport] Dropping packetId=${packetId} (0x${packetId.toString(16)}): ` +\n `not in the advertised set [${[...(this.#packetIds ?? [])].join(', ')}]. ` +\n `If this is an FEC repair leg, the catalog is not advertising its packetId.`,\n )\n }\n\n #checkTakeover(): void {\n if (this.#videoConfirmed && this.#audioConfirmed && this.#onTakeover) {\n const cb = this.#onTakeover\n this.#onTakeover = null // fire exactly once\n cb()\n }\n }\n\n /**\n * Note liveness now; arm the no-packets timer if none is armed.\n *\n * Called only for accepted packets, so the first call of a join is the\n * data-plane-live edge and is what first arms the silence window: the window\n * measures silence of a flowing stream, which is undefined before the stream\n * has flowed.\n */\n #resetWatchdog(): void {\n this.#lastPacketAtMs = performance.now()\n if (!this.#dataPlaneLive) {\n this.#dataPlaneLive = true\n this.#onDataPlaneLive?.()\n }\n if (this.#watchdog !== null) return\n if (!this.#onFailback) return\n if (this.#failbackWindowMs === null) {\n throw new Error(\n '[MulticastTransport] Failback watchdog requires a catalog-derived window',\n )\n }\n this.#armWatchdog(this.#failbackWindowMs)\n }\n\n #armWatchdog(delayMs: number): void {\n this.#watchdog = setTimeout(() => {\n this.#watchdog = null\n if (this.#disposed || !this.#onFailback || this.#failbackWindowMs === null) return\n const silentMs = performance.now() - this.#lastPacketAtMs\n if (silentMs < this.#failbackWindowMs) {\n this.#armWatchdog(Math.ceil(this.#failbackWindowMs - silentMs))\n return\n }\n console.warn(\n `[MulticastTransport] No packets for ${this.#failbackWindowMs}ms; failing back to MoQ`,\n )\n this.#onFailback({ kind: 'silence', windowMs: this.#failbackWindowMs })\n }, delayMs)\n }\n\n /** Hand control back to MoQ for a provider-reported subscription failure. */\n #failBackOnSubscriptionError(error: unknown): void {\n // The subscription is dead; a silence timer armed against it would only\n // report the same failure a second time.\n if (this.#watchdog !== null) clearTimeout(this.#watchdog)\n this.#watchdog = null\n console.warn('[MulticastTransport] Subscription failed; failing back to MoQ:', error)\n this.#onFailback?.({ kind: 'subscription-error', error })\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["MoqFecAlgorithm", "
|
|
3
|
+
"sources": ["../node_modules/@blockcast/mmt-transport/src/types.ts", "../node_modules/@blockcast/mmt-base/src/mmtp.ts", "../node_modules/@blockcast/mmt-base/src/net-constants.ts", "../node_modules/@blockcast/mmt-base/src/ntp-short.ts", "../node_modules/@blockcast/mmt-base/src/fec-block-profile.ts", "../node_modules/@blockcast/mmt-transport/src/driad-discovery.ts", "../node_modules/@blockcast/mmt-transport/src/multicast-tier.ts", "../node_modules/@blockcast/mmt-transport/src/abr-controller.ts", "../node_modules/@blockcast/mmt-transport/src/multicast-orchestrator.ts", "../node_modules/@blockcast/mmt-transport/src/endpoint-config.ts", "../node_modules/@blockcast/mmt-transport/src/packet-batch.ts", "../node_modules/@blockcast/mmt-transport/src/multicast-subscription-pool.ts", "../node_modules/@blockcast/mmt-transport/src/transport-manager.ts", "../node_modules/@blockcast/mmt-transport/src/moqt-priority.ts", "../node_modules/@blockcast/mmt-transport/src/amt-gateway.ts", "../../mmt-base/src/mmtp.ts", "../../mmt-base/src/ntp-short.ts", "../../mmt-base/src/fec-block-profile.ts", "../src/multicast-transport.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * @blockcast/transport - Type Definitions\n *\n * Shared interfaces for unified transport (WebTransport, SSM, AMT, DRIAD) and FEC.\n */\n\n/** Native IP-multicast request for the window.multicast gateway. */\nexport interface WindowMulticastSubscribeConfigBytes {\n\tgroup: string;\n\tport: number;\n\tsource?: string;\n\tconsume: \"bytes\";\n\torigin?: \"ip-multicast\";\n\t/** MMTP packet IDs to forward before data enters the page transport queue. */\n\tpacketIds?: readonly number[];\n\t/** Catalog track identities corresponding to the selected packet IDs. */\n\ttracks?: ReadonlyArray<{ name: string; packetId: number }>;\n}\n\n/** AMT relay request for the window.multicast gateway. */\nexport interface WindowMulticastSubscribeConfigAmt {\n\tgroup: string;\n\tport: number;\n\tsource?: string;\n\tconsume: \"bytes\";\n\torigin: \"amt\";\n\trelay: string;\n\trelayPort: number;\n\t/** MMTP packet IDs to forward before data enters the page transport queue. */\n\tpacketIds?: readonly number[];\n}\n\n/** Canonical window.multicast config for byte subscriptions. */\nexport type WindowMulticastSubscribeConfig =\n\t| WindowMulticastSubscribeConfigBytes\n\t| WindowMulticastSubscribeConfigAmt;\n\nexport type WindowMulticastSubscriptionState = \"connecting\" | \"connected\" | \"reconnecting\" | \"closed\";\n\nexport interface WindowMulticastSubscriptionEventMap {\n\tbytes: CustomEvent<ArrayBuffer>;\n\t/**\n\t * Provider extension, not part of the frozen v1 contract (BLO-36599): every\n\t * packet the provider decoded in one dispatch batch, in arrival order. Each\n\t * element is exactly what one `bytes` event would have carried. Emitted\n\t * only by a subscription whose `eventTypes` lists `\"packets\"`; see\n\t * {@link subscriptionDeliversPacketBatches}. A consumer listens to exactly\n\t * one of `bytes` and `packets`, because both carry the same packets.\n\t */\n\tpackets: CustomEvent<readonly ArrayBuffer[]>;\n\tstate: CustomEvent<WindowMulticastSubscriptionState>;\n\terror: CustomEvent<unknown>;\n}\n\n/**\n * How a window.multicast subscription hands packets to the page.\n *\n * - `\"datagram\"`: packets are independent datagrams; a pause in arrivals may\n * mean loss.\n * - `\"ordered-relay\"`: the provider relays one receiving socket over a\n * reliable, ordered stream (for example a BCDS bridge WebTransport uni\n * stream). The relay may drop packets (a bounded queue discarding its\n * oldest on overflow) but never reorders them: what arrives is a\n * subsequence of the socket's receive order, so a pause in arrivals\n * carries no loss information.\n *\n * A provider declares `\"ordered-relay\"` only when every packet of the\n * subscription travels that one ordered stream for the life of the\n * subscription. Absent means no ordering promise, which consumers treat as\n * `\"datagram\"`.\n */\nexport type WindowMulticastDelivery = \"datagram\" | \"ordered-relay\";\n\n/** window.multicast subscription returned by the IWA gateway surface. */\nexport interface WindowMulticastSubscription {\n\treadonly id?: string;\n\treadonly state?: WindowMulticastSubscriptionState;\n\t/** How this subscription delivers packets; see WindowMulticastDelivery. */\n\treadonly delivery?: WindowMulticastDelivery;\n\t/**\n\t * Events this subscription emits, when the provider advertises them. A\n\t * provider without the field emits the v1 events only (`bytes`, `state`,\n\t * `error`).\n\t */\n\treadonly eventTypes?: readonly string[];\n\taddEventListener<K extends keyof WindowMulticastSubscriptionEventMap>(\n\t\ttype: K,\n\t\tlistener: (ev: WindowMulticastSubscriptionEventMap[K]) => void,\n\t\toptions?: AddEventListenerOptions | boolean,\n\t): void;\n\tremoveEventListener<K extends keyof WindowMulticastSubscriptionEventMap>(\n\t\ttype: K,\n\t\tlistener: (ev: WindowMulticastSubscriptionEventMap[K]) => void,\n\t\toptions?: EventListenerOptions | boolean,\n\t): void;\n\tunsubscribe: () => Promise<void> | void;\n}\n\nexport interface WindowMulticastCapabilities {\n\treadonly consumeModes?: ReadonlyArray<\"bytes\" | string>;\n}\n\n/** Lowercase window.multicast gateway surface. */\nexport interface WindowMulticastGateway {\n\tsubscribe?: (config: WindowMulticastSubscribeConfig) => Promise<WindowMulticastSubscription>;\n\tcapabilities?: WindowMulticastCapabilities | (() => Promise<WindowMulticastCapabilities>);\n}\n\ndeclare global {\n\tinterface Window {\n\t\tmulticast?: WindowMulticastGateway;\n\t\tUDPSocket?: unknown;\n\t}\n}\n\n/** Transport configuration for multicast subscriptions */\nexport interface TransportConfig {\n\t/** Multicast group address (232.x.x.x for SSM) */\n\tgroup: string;\n\t/** Source address for SSM filtering */\n\tsource: string;\n\t/** UDP port */\n\tport: number;\n\t/** Stream type */\n\tstreamType: \"video\" | \"audio\";\n\t/** Optional AMT relay host for unicast tunneling */\n\tamtRelay?: string;\n\t/** Optional AMT relay port. */\n\tamtRelayPort?: number;\n\t/** Browser gateway transport preference. */\n\tmode?: \"auto\" | \"native\" | \"tunnel\";\n\t/** DRIAD discovery enabled */\n\tdriadEnabled?: boolean;\n}\n\n/** What `TransportManager.connect()` needs beyond the subscription itself. */\nexport interface TransportConnectConfig extends TransportConfig {\n\t/**\n\t * The stream's renditions the ABR controller adapts over, e.g. its catalog\n\t * video tracks. Required: there is no built-in ladder. `connect()` checks\n\t * it (`validateQualityLadder`) and `initialQuality` before tearing anything\n\t * down; a config that fails is refused through the error callback and\n\t * `connect()` resolves `false`, leaving a live subscription untouched.\n\t * `catalogQualityLadder(catalog)` builds it from a catalog.\n\t */\n\tabrLadder: readonly QualityLevel[];\n\t/**\n\t * Ladder rendition to start on, matched by `name` against `abrLadder`; the\n\t * lowest-bitrate rung when omitted. A name `abrLadder` does not carry is\n\t * not a fallback to that rung: `TransportManager.connect()` refuses the\n\t * config (error callback, resolves `false`) before tearing anything down.\n\t */\n\tinitialQuality?: QualityLevel;\n}\n\nexport interface QualityLevel {\n\tname: string;\n\t/** Bits per second: what the ABR controller ranks and costs a rendition by. */\n\tbitrate: number;\n\t/**\n\t * Dimensions and frame rate as the stream's catalog states them; absent\n\t * when it does not. ABR never reads them, so none is defaulted.\n\t */\n\twidth?: number;\n\theight?: number;\n\tfps?: number;\n}\n\nexport interface TransportCapabilities {\n\tnativeSSM: boolean;\n\tnativeIGMP: boolean;\n\tdirectSockets: boolean;\n\tamt: boolean;\n\tdriad: boolean;\n\tfecDecode: boolean;\n}\n\nexport type TransportMode = \"multicast\" | \"amt\" | \"moq\" | \"disconnected\";\n\nexport interface TransportState {\n\tmode: TransportMode;\n\tconnected: boolean;\n\tgroup?: string;\n\tsource?: string;\n\tamtRelay?: string;\n\tquality?: QualityLevel;\n\tpacketsReceived: number;\n\tbytesReceived: number;\n\tlastPacketTime: number;\n}\n\n/** Transport subscription handle */\nexport interface TransportSubscription {\n\tid: string;\n\tconfig: TransportConfig;\n\tonpacket?: (data: ArrayBuffer) => void;\n\tonerror?: (error: Error) => void;\n\tonqualitychange?: (quality: QualityLevel) => void;\n\tclose: () => void;\n}\n\n/** DRIAD Relay information (RFC 8777) */\nexport interface DRIADRelay {\n\thost: string;\n\tport: number;\n\tpriority: number;\n\tweight: number;\n\tasn: number;\n\trtt?: number;\n}\n\n/** AMT Relay information */\nexport interface AmtRelayInfo {\n\taddress: string;\n\tport: number;\n\tdiscoveryMethod: \"driad\" | \"manual\";\n\tdiscoveredAt: number;\n}\n\n/** Group key for multicast subscriptions */\nexport interface GroupKey {\n\tgroup: string;\n\tport: number;\n\tsource?: string;\n}\n\n/** Packet metadata */\nexport interface PacketMetadata {\n\tsourceAddress: string;\n\tsourcePort: number;\n\ttransport: \"native\" | \"tunnel\" | \"websocket\";\n\ttimestamp: number;\n}\n\n/** Time types (microseconds) */\nexport type Micro = number & { readonly __micro: unique symbol };\n\n/** Sync decision for A/V synchronization */\nexport type SyncDecision = \"render\" | \"hold\" | \"discard\";\n\n// ============================================================\n// Multicast Bridge Types (consolidated from @blockcast/multicast)\n// ============================================================\n\n/** Transport mode for multicast subscription (auto-selection) */\nexport type MulticastTransportMode = \"auto\" | \"native\" | \"tunnel\";\n\n/** Multicast subscription configuration */\nexport interface MulticastConfig {\n\tgroup: string;\n\tport: number;\n\tsource?: string;\n\tmode?: MulticastTransportMode;\n\tonPacket: (data: ArrayBuffer, source: PacketSource) => void;\n\tonStateChange?: (state: SubscriptionState) => void;\n\tonError?: (error: Error) => void;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\n\tnativeFallbackTimeout?: number;\n}\n\n/** Packet source information */\nexport interface PacketSource {\n\taddress: string;\n\tport: number;\n\ttransport: \"native\" | \"tunnel\";\n\trecovered?: boolean;\n}\n\n/** Subscription state */\nexport type SubscriptionState = \"initializing\" | \"joining\" | \"active\" | \"suspended\" | \"closed\" | \"error\";\n\n/** Subscription statistics */\nexport interface SubscriptionStats {\n\tpacketsReceived: number;\n\tbytesReceived: number;\n\tpacketsLost: number;\n\tpacketsRecovered: number;\n\tbitrate: number;\n\tuptime: number;\n}\n\n/** Multicast subscription handle */\nexport interface MulticastSubscriptionHandle {\n\treadonly state: SubscriptionState;\n\treadonly group: string;\n\treadonly source?: string;\n\treadonly port: number;\n\treadonly transport: \"native\" | \"tunnel\";\n\treadonly stats: SubscriptionStats;\n\tclose(): Promise<void>;\n}\n\n/** Main Multicast Bridge API */\nexport interface MulticastBridge {\n\tsubscribe(config: MulticastConfig): Promise<MulticastSubscriptionHandle>;\n\tgetSubscriptions(): Promise<MulticastSubscriptionHandle[]>;\n\tisNativeSupported(): Promise<boolean>;\n}\n\n// ============================================================\n// FEC Constants and Utilities per draft-ramadan-moq-fec-00\n// ============================================================\n\n/**\n * Build a conventional repair track path by appending \"repair\" to a source\n * track path.\n *\n * Track names carry no protocol meaning. A repair track's layer, its source\n * and its symbol budget are resolved from the catalog via `resolveRepairLayers`\n * in `@blockcast/mmt-container`, never from a name or a name suffix. This\n * helper is a naming convenience for producers only.\n *\n * @deprecated Naming convenience only. Never classify a track or resolve a\n * repair layer from its path; use `resolveRepairLayers` from\n * `@blockcast/mmt-container`.\n *\n * @param sourceTrack - The source track path (e.g., \"live/video\")\n * @returns The repair track path (e.g., \"live/video/repair\")\n *\n * @example\n * repairTrackPath(\"live/video\") // returns \"live/video/repair\"\n * repairTrackPath(\"broadcast/video/1080p\") // returns \"broadcast/video/1080p/repair\"\n */\nexport function repairTrackPath(sourceTrack: string): string {\n\tif (!sourceTrack || sourceTrack.length === 0) {\n\t\treturn \"repair\";\n\t}\n\treturn `${sourceTrack}/repair`;\n}\n\n/**\n * MoQ FEC Configuration per draft-ramadan-moq-fec-00\n *\n * These parameters are signalled in each track's catalog `fec` block;\n * no FEC_CONFIG control message is sent or parsed.\n */\nexport interface MoqFecConfig {\n\t/** Subscribe ID this config applies to */\n\tsubscribeId: number;\n\t/** Whether FEC is enabled */\n\tenabled: boolean;\n\t/** FEC algorithm (0=none, 1=XOR, 2=RaptorQ) */\n\talgorithm: MoqFecAlgorithm;\n\t/** Number of source symbols per FEC block (K) */\n\tsourceSymbolsPerBlock: number;\n\t/** Number of repair symbols per FEC block (P) */\n\trepairSymbolsPerBlock: number;\n\t/** Interleave depth for burst loss protection */\n\tinterleaveDepth: number;\n\t/** Symbol size in bytes */\n\tsymbolSize: number;\n\t/** RFC 6330 Object Transmission Information (12 bytes) - only for RaptorQ */\n\toti?: Uint8Array;\n}\n\n/**\n * FEC algorithm identifier per draft-ramadan-moq-fec-00\n */\nexport enum MoqFecAlgorithm {\n\t/** No FEC (disabled) */\n\tNone = 0,\n\t/** Simple XOR parity */\n\tXor = 1,\n\t/** RaptorQ (RFC 6330) - recommended for burst loss */\n\tRaptorQ = 2,\n}\n\n/**\n * Repair object header per draft-ramadan-moq-fec-00 Section 6.1\n *\n * Each object on a repair track contains repair symbols for one source block.\n * The object payload begins with this header, followed by concatenated repair symbols.\n */\nexport interface RepairObjectHeader {\n\t/** Source block number being protected (matches group_id of source objects) */\n\tsourceBlockNumber: number;\n\t/** First Repair Symbol ESI (Encoding Symbol ID) - for RaptorQ, repair ESIs >= K */\n\tfirstRepairEsi: number;\n\t/** Number of repair symbols in this object */\n\tnumRepairSymbols: number;\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\n// Legacy detached per-packet authenticator suffix `[0xf0a1][len][auth]`. Its\n// producer was removed (BLO-16483) and nothing verifies it any more; provenance\n// is the signed per-group manifest. Receivers still skip an exact suffix so a\n// stale producer degrades to unauthenticated playback instead of dropped\n// packets. ponytail: hard-cut to reject once no deployed producer can emit it.\nconst LEGACY_TRAILER_EXT_TYPE = 0xf0a1\nconst LEGACY_TRAILER_HEADER_SIZE = 4\n\nfunction isEmptyOrExactLegacyTrailer(payload: Uint8Array, start: number, end: number): boolean {\n if (start === end) return true\n if (start < 0 || end > payload.length || start + LEGACY_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) !== LEGACY_TRAILER_EXT_TYPE) return false\n\n const authLength = view.getUint16(2, false)\n return start + LEGACY_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 isEmptyOrExactLegacyTrailer(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 || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {\n return null\n }\n return { hasFecId: true }\n }\n if (fecType !== 0) return null\n\n if (isEmptyOrExactLegacyTrailer(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. The legacy\n * authenticator suffix and the Source FEC trailer 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 legacy authenticator 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/** 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 * AMT well-known UDP port (RFC 7450 \u00A74).\n *\n * The single definition for the whole repo. It lives here, in the one package\n * both `@blockcast/mmt-transport` and `@blockcast/mmt-container` already\n * depend on, because the two apply it on opposite sides of a boundary that\n * does not otherwise share code:\n *\n * - container resolves it where `networkSource` semantics live, so a catalog\n * relay that names no port is reachable instead of handing a join a\n * portless relay that fails.\n * - transport applies it when reading a catalog, dialling a gateway, and\n * answering DRIAD \u2014 but deliberately not at every boundary. Which call\n * sites do and do not take the default is transport's business; the\n * reasoning lives beside those call sites, not here.\n */\nexport const DEFAULT_AMT_RELAY_PORT = 2268;\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 its block\n * profile in `fec.blockProfile`. A receiver applies all of a profile's rules\n * or none of them, so the token is the one explicit per-stream discriminator:\n * it is never defaulted and never inferred from the wire.\n *\n * draft-ramadan-moq-fec \u00A75.1 defines exactly one value, and it is the only\n * profile this library implements:\n *\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 *\n * A track whose `blockProfile` is absent or names any other token (the\n * retired Blockcast-private `k-padded-v1` included) has no FEC: a receiver\n * consumes its source without FEC and joins none of its repair tracks\n * (draft-ramadan-moq-fec \u00A75.1). `parseFecBlockProfile` throws for such a\n * token; the per-track catalog resolver turns the same rule into a typed\n * per-track \"FEC unavailable\" outcome instead of failing the session.\n */\nimport { serial32Add, serial32Distance, serial32Precedes } from './serial-number.js'\n\n/** The block profile a catalog `fec.blockProfile` may name: the one this library implements. */\nexport type FecBlockProfile = 'iso-ssbg1-v1'\n\n/** Every accepted `fec.blockProfile` token. */\nexport const FEC_BLOCK_PROFILES: readonly FecBlockProfile[] = Object.freeze([\n 'iso-ssbg1-v1',\n])\n\n/** True for a block-profile token this library implements. */\nexport function isFecBlockProfile(value: unknown): value is FecBlockProfile {\n return value === 'iso-ssbg1-v1'\n}\n\n/**\n * Read a required `fec.blockProfile`. Throws on an absent token and on any\n * token other than `iso-ssbg1-v1`: there is no default and nothing is\n * inferred from the wire (draft-ramadan-moq-fec \u00A75.1).\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(`${where} is required: name the FEC block profile (\"iso-ssbg1-v1\")`)\n }\n throw new Error(`${where} must be \"iso-ssbg1-v1\" (got ${JSON.stringify(value)})`)\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", "/**\n * @blockcast/transport - DRIAD Discovery (RFC 8777)\n *\n * DRIAD (DNS Reverse IP AMT Discovery) discovers AMT relays based on the\n * **source address**, NOT the multicast group. The source network operator\n * configures DNS records for their source IPs to advertise which AMT relay(s)\n * can tunnel their traffic.\n *\n * Example: For source 69.25.95.10 sending to group 232.0.0.1:\n * Query: 10.95.25.69.amt.in-addr.arpa (source-based, NOT group-based)\n */\n\nimport { DEFAULT_AMT_RELAY_PORT } from \"@blockcast/mmt-base\";\nimport type { DRIADRelay } from \"./types.js\";\n\n/**\n * DRIAD (DNS Reverse IP AMT Discovery) - RFC 8777\n * Discovers AMT relays based on source address via DNS lookups\n */\nexport class DRIADDiscovery {\n\tprivate cache = new Map<string, DRIADRelay[]>();\n\tprivate cacheExpiry = 300_000; // 5 minutes\n\n\t/**\n\t * Discover AMT relays for a multicast source address (RFC 8777)\n\t *\n\t * @param source - The multicast source IP address (NOT the group!)\n\t * @returns Array of discovered AMT relays that can tunnel traffic from this source\n\t */\n\tasync discoverRelays(source: string): Promise<DRIADRelay[]> {\n\t\t// Check cache first\n\t\tconst cached = this.cache.get(source);\n\t\tif (cached) {\n\t\t\treturn cached;\n\t\t}\n\n\t\ttry {\n\t\t\t// Convert source to DRIAD format (reversed octets)\n\t\t\tconst reversedSource = source.split(\".\").reverse().join(\".\");\n\t\t\tconst queryDomain = `${reversedSource}.amt.in-addr.arpa`;\n\n\t\t\tconsole.log(`[DRIAD] Discovering relays for source ${source} via ${queryDomain}`);\n\n\t\t\t// In browser, we use a DRIAD proxy service or DNS-over-HTTPS\n\t\t\tconst relays = await this.queryDRIADProxy(source);\n\n\t\t\tif (relays.length > 0) {\n\t\t\t\tthis.cache.set(source, relays);\n\t\t\t\tsetTimeout(() => this.cache.delete(source), this.cacheExpiry);\n\t\t\t}\n\n\t\t\treturn relays;\n\t\t} catch (error) {\n\t\t\tconsole.warn(`[DRIAD] Discovery failed for source ${source}:`, error);\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t/**\n\t * Query DRIAD proxy service (browser-compatible)\n\t */\n\tprivate async queryDRIADProxy(source: string): Promise<DRIADRelay[]> {\n\t\t// List of known DRIAD proxy endpoints\n\t\tconst proxyEndpoints = [\n\t\t\t\"https://driad.amt.net/api/v1/relays\",\n\t\t\t\"https://multicast-gateway.cloudflare.com/driad\",\n\t\t];\n\n\t\tfor (const endpoint of proxyEndpoints) {\n\t\t\ttry {\n\t\t\t\t// Query by source address per RFC 8777\n\t\t\t\tconst response = await fetch(`${endpoint}?source=${encodeURIComponent(source)}`, {\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\theaders: { Accept: \"application/json\" },\n\t\t\t\t\tsignal: AbortSignal.timeout(5000),\n\t\t\t\t});\n\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = (await response.json()) as { relays: DRIADRelay[] };\n\t\t\t\t\treturn data.relays || [];\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Try next proxy endpoint\n\t\t\t}\n\t\t}\n\n\t\t// Fallback: Try DNS-over-HTTPS query\n\t\tconst dohRelay = await this.queryDNSOverHTTPS(source);\n\t\tif (dohRelay) {\n\t\t\treturn [dohRelay];\n\t\t}\n\n\t\treturn [];\n\t}\n\n\t/**\n\t * Query DNS-over-HTTPS for DRIAD relay based on source address (RFC 8777)\n\t */\n\tprivate async queryDNSOverHTTPS(source: string): Promise<DRIADRelay | null> {\n\t\ttry {\n\t\t\t// Build DRIAD query name from source address (reversed octets)\n\t\t\tconst reversedSource = source.split(\".\").reverse().join(\".\");\n\t\t\tconst queryName = `${reversedSource}.amt.in-addr.arpa`;\n\n\t\t\tconst dohUrl = `https://cloudflare-dns.com/dns-query?name=${queryName}&type=A`;\n\t\t\tconst response = await fetch(dohUrl, {\n\t\t\t\theaders: { Accept: \"application/dns-json\" },\n\t\t\t\tsignal: AbortSignal.timeout(3000),\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst result = (await response.json()) as { Answer?: Array<{ data: string }> };\n\n\t\t\t// Extract first A/AAAA record\n\t\t\tif (result.Answer && result.Answer.length > 0) {\n\t\t\t\treturn {\n\t\t\t\t\thost: result.Answer[0].data,\n\t\t\t\t\tport: DEFAULT_AMT_RELAY_PORT,\n\t\t\t\t\tpriority: 10,\n\t\t\t\t\tweight: 100,\n\t\t\t\t\tasn: 0,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn null;\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[DRIAD] DNS-over-HTTPS query failed:\", error);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * Select best relay based on RTT measurements\n\t */\n\tasync selectBestRelay(relays: DRIADRelay[]): Promise<DRIADRelay | null> {\n\t\tif (relays.length === 0) return null;\n\n\t\t// Measure RTT to each relay\n\t\tconst measured = await Promise.all(\n\t\t\trelays.map(async (relay) => {\n\t\t\t\tconst rtt = await this.measureRTT(relay.host, relay.port);\n\t\t\t\treturn { ...relay, rtt };\n\t\t\t}),\n\t\t);\n\n\t\t// Sort by priority, then by RTT\n\t\tmeasured.sort((a, b) => {\n\t\t\tif (a.priority !== b.priority) return a.priority - b.priority;\n\t\t\treturn (a.rtt || Infinity) - (b.rtt || Infinity);\n\t\t});\n\n\t\tconsole.log(`[DRIAD] Selected relay: ${measured[0].host} (RTT: ${measured[0].rtt}ms)`);\n\t\treturn measured[0];\n\t}\n\n\tprivate async measureRTT(host: string, _port: number): Promise<number> {\n\t\tconst start = performance.now();\n\t\ttry {\n\t\t\t// Use HTTP/HTTPS probe since we can't do UDP from browser\n\t\t\tawait fetch(`https://${host}/health`, {\n\t\t\t\tmethod: \"HEAD\",\n\t\t\t\tsignal: AbortSignal.timeout(2000),\n\t\t\t\tmode: \"no-cors\",\n\t\t\t});\n\t\t\treturn performance.now() - start;\n\t\t} catch {\n\t\t\treturn Infinity;\n\t\t}\n\t}\n\n\t/**\n\t * Clear the relay cache\n\t */\n\tclearCache(): void {\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Get cached relays for a source address\n\t */\n\tgetCached(source: string): DRIADRelay[] | undefined {\n\t\treturn this.cache.get(source);\n\t}\n}\n\n// Export singleton instance\nexport const driadDiscovery = new DRIADDiscovery();\n", "export interface MulticastTierEndpoint {\n\tsourceAddress?: string;\n\tgroupAddress: string;\n\tport: number;\n\ttracks: { name: string; packetId?: number }[];\n\tbandwidth?: number;\n\tprotocol?: string;\n\t/** False when every track on this endpoint is a `fec-repair` track, so the\n\t * endpoint carries repair symbols only and no media. A repair-layer\n\t * endpoint is joined by the loss-driven layer policy, never by the\n\t * bandwidth ladder: selecting one as a tier would leave the receiver with\n\t * no media at all. Set by the catalog policy, which resolves repair-track\n\t * membership from `depends`/`fec.repairTrack` rather than from names.\n\t * Undefined means \"not classified\" and is treated as carrying media, so a\n\t * caller that builds endpoints without the policy is unaffected. */\n\tcarriesMedia?: boolean;\n}\n\nexport function endpointKey(endpoint: MulticastTierEndpoint): string {\n\treturn `${endpoint.sourceAddress ?? \"\"}|${endpoint.groupAddress}|${endpoint.port}`;\n}\n\n/**\n * Whether this endpoint may be selected as a media tier.\n *\n * Every path that picks a tier must apply this, not only `selectMulticastTier`:\n * the orchestrator's headroom up-selection ranks endpoints itself, because it\n * advances one adjacent rung rather than fitting a bandwidth budget. Exported\n * so both rankings share one definition of \"carries media\" instead of\n * repeating the comparison and drifting apart.\n */\nexport function carriesMedia(endpoint: MulticastTierEndpoint): boolean {\n\treturn endpoint.carriesMedia !== false;\n}\n\nexport function selectMulticastTier(\n\tendpoints: readonly MulticastTierEndpoint[],\n\testimate: number | undefined,\n): MulticastTierEndpoint | undefined {\n\t// Repair-only endpoints are never a media tier. Filtering once here covers\n\t// every return path below, including the two early returns and the fallback:\n\t// a ladder that has only repair-layer endpoints returns undefined rather\n\t// than joining one and receiving no media.\n\tconst media = endpoints.filter(carriesMedia);\n\tif (media.length <= 1) return media[0];\n\n\tconst ranked = media\n\t\t.filter((endpoint) => typeof endpoint.bandwidth === \"number\" && endpoint.bandwidth > 0)\n\t\t.sort((a, b) => a.bandwidth! - b.bandwidth!);\n\tif (ranked.length < 2) return media[0];\n\tif (estimate === undefined) return ranked[0];\n\n\tconst budget = estimate * 0.8;\n\tlet selected: MulticastTierEndpoint | undefined;\n\tfor (const endpoint of ranked) {\n\t\tif (endpoint.bandwidth! <= budget) selected = endpoint;\n\t}\n\treturn selected ?? ranked[0];\n}\n", "/**\n * @blockcast/transport - Adaptive Bitrate Controller\n *\n * Progressive adaptive bitrate increase based on network conditions.\n * Monitors received bandwidth and packet loss to adjust quality levels.\n *\n * Per draft-ramadan-moq-fec-00, ABR decisions should use the **effective loss rate**\n * after FEC recovery, not the raw transport-level loss rate. This allows FEC to\n * mask burst losses without unnecessarily downgrading quality.\n */\n\nimport type { QualityLevel } from \"./types.js\";\nimport { selectMulticastTier, type MulticastTierEndpoint } from \"./multicast-tier.js\";\n\n/**\n * FEC statistics for ABR decisions\n */\nexport interface FecStatsForAbr {\n\t/** Raw loss rate before FEC recovery (0.0-1.0) */\n\trawLossRate: number;\n\t/** Effective loss rate after FEC recovery (0.0-1.0) */\n\teffectiveLossRate: number;\n\t/** FEC recovery rate (percentage of lost packets recovered) */\n\trecoveryRate: number;\n\t/** Whether FEC is enabled */\n\tfecEnabled: boolean;\n}\n\n/**\n * A generic 240p..4K ladder, kept as public API only.\n *\n * @deprecated Not a stream's ladder and never a default: the controller\n * adapts over the ladder its caller supplies (`AdaptiveBitrateControllerOptions.ladder`,\n * e.g. the catalog's video renditions). Walking this list is how a player on a\n * 360p/480p/720p catalog logged upgrades to 1080p and 1440p it could never\n * switch to (BLO-35597).\n */\nexport const QUALITY_LEVELS: readonly QualityLevel[] = Object.freeze([\n\t{ name: \"240p\", bitrate: 300_000, width: 426, height: 240, fps: 15 },\n\t{ name: \"360p\", bitrate: 500_000, width: 640, height: 360, fps: 24 },\n\t{ name: \"480p\", bitrate: 1_000_000, width: 854, height: 480, fps: 30 },\n\t{ name: \"720p\", bitrate: 2_500_000, width: 1280, height: 720, fps: 30 },\n\t{ name: \"1080p\", bitrate: 5_000_000, width: 1920, height: 1080, fps: 30 },\n\t{ name: \"1440p\", bitrate: 8_000_000, width: 2560, height: 1440, fps: 30 },\n\t{ name: \"4K\", bitrate: 15_000_000, width: 3840, height: 2160, fps: 30 },\n].map((level) => Object.freeze(level)));\n\n/** What the controller adapts over and where it starts; both from the caller. */\nexport interface AdaptiveBitrateControllerOptions {\n\t/**\n\t * The renditions this stream carries (e.g. the catalog's video tracks), in\n\t * any order: a unique name and a bitrate (bps) each, plus dimensions and\n\t * fps when the catalog states them. Renditions may share a bitrate.\n\t */\n\tladder: readonly QualityLevel[];\n\t/** Name of the ladder rendition presented at construction; the lowest\n\t * rung when omitted. A name that matches no rung is refused. */\n\tinitialQuality?: string;\n}\n\nfunction positiveFinite(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n\n/**\n * Validate a caller ladder and return it frozen, ascending by bitrate. Throws\n * on an empty ladder, a rung without a name or a positive finite bitrate, a\n * width, height or fps that is present but not positive finite, or a name that\n * appears twice: a name is how `setQuality` and the initial rendition find a\n * rung. A shared bitrate is legal (e.g. 720p30 and 720p60, or two codecs);\n * `Array.prototype.sort` is stable, so equal bitrates keep the caller's order.\n */\nexport function validateQualityLadder(ladder: readonly QualityLevel[]): readonly QualityLevel[] {\n\tif (!Array.isArray(ladder) || ladder.length === 0) {\n\t\tthrow new TypeError(\"[ABR] ladder must list at least one rendition (e.g. the catalog's video tracks)\");\n\t}\n\tconst names = new Set<string>();\n\tfor (const level of ladder) {\n\t\tif (typeof level?.name !== \"string\" || level.name.length === 0) {\n\t\t\tthrow new TypeError(\"[ABR] every ladder rendition needs a name\");\n\t\t}\n\t\tif (!positiveFinite(level.bitrate)) {\n\t\t\tthrow new RangeError(`[ABR] rendition ${level.name}: bitrate must be a positive finite number, got ${String(level.bitrate)}`);\n\t\t}\n\t\tfor (const field of [\"width\", \"height\", \"fps\"] as const) {\n\t\t\tif (level[field] !== undefined && !positiveFinite(level[field])) {\n\t\t\t\tthrow new RangeError(`[ABR] rendition ${level.name}: ${field} must be a positive finite number when stated, got ${String(level[field])}`);\n\t\t\t}\n\t\t}\n\t\tif (names.has(level.name)) throw new RangeError(`[ABR] rendition ${level.name} appears twice in the ladder`);\n\t\tnames.add(level.name);\n\t}\n\treturn Object.freeze(\n\t\t[...ladder].sort((a, b) => a.bitrate - b.bitrate).map((level) => Object.freeze({ ...level })),\n\t);\n}\n\n/**\n * Progressive Adaptive Bitrate Controller\n *\n * Features:\n * - Monitors bandwidth via received bytes over time window\n * - Tracks packet loss rate (raw and effective after FEC)\n * - Uses effective loss rate for ABR decisions per draft-ramadan-moq-fec-00\n * - Progressive quality upgrades (waits for stability)\n * - Fast quality downgrades on congestion\n */\nexport class AdaptiveBitrateController {\n\t/** The caller's renditions, ascending by bitrate (`validateQualityLadder`). */\n\treadonly ladder: readonly QualityLevel[];\n\tprivate currentQualityIndex: number;\n\tprivate history: { timestamp: number; bytesReceived: number; packetsLost: number }[] = [];\n\tprivate readonly historyWindow = 10_000; // 10 second window\n\tprivate readonly upgradeThreshold = 0.8; // 80% of target bitrate sustained\n\tprivate readonly downgradeThreshold = 0.5; // 50% of target bitrate triggers downgrade\n\tprivate readonly stabilityPeriod = 5_000; // Wait 5s before upgrading\n\n\tprivate lastQualityChange = 0;\n\tprivate onQualityChange?: (quality: QualityLevel) => void;\n\n\t// FEC stats for effective loss rate calculation\n\tprivate fecStats: FecStatsForAbr = {\n\t\trawLossRate: 0,\n\t\teffectiveLossRate: 0,\n\t\trecoveryRate: 1,\n\t\tfecEnabled: false,\n\t};\n\n\tconstructor(options: AdaptiveBitrateControllerOptions) {\n\t\tif (!options || typeof options !== \"object\") {\n\t\t\tthrow new TypeError(\"[ABR] AdaptiveBitrateController needs { ladder, initialQuality }\");\n\t\t}\n\t\tthis.ladder = validateQualityLadder(options.ladder);\n\t\tthis.currentQualityIndex =\n\t\t\toptions.initialQuality === undefined ? 0 : this.ladder.findIndex((q) => q.name === options.initialQuality);\n\t\tif (this.currentQualityIndex < 0) {\n\t\t\tthrow new RangeError(`[ABR] initial rendition ${String(options.initialQuality)} is not in the ladder`);\n\t\t}\n\t}\n\n\tsetCallback(callback: (quality: QualityLevel) => void) {\n\t\tthis.onQualityChange = callback;\n\t}\n\n\tgetCurrentQuality(): QualityLevel {\n\t\treturn this.ladder[this.currentQualityIndex];\n\t}\n\n\t/** Selects the multicast (S,G) tier without replacing a player's unicast ABR manager. */\n\tselectMulticastTier(\n\t\tendpoints: readonly MulticastTierEndpoint[],\n\t\testimate: number | undefined = this.getMetrics().measuredBitrate || undefined,\n\t): MulticastTierEndpoint | undefined {\n\t\treturn selectMulticastTier(endpoints, estimate);\n\t}\n\n\t/**\n\t * Record network sample for bitrate adaptation\n\t *\n\t * @param bytesReceived - Bytes received in this sample\n\t * @param packetsLost - Packets lost (raw, before FEC recovery)\n\t */\n\trecordSample(bytesReceived: number, packetsLost: number) {\n\t\tconst now = Date.now();\n\n\t\tthis.history.push({ timestamp: now, bytesReceived, packetsLost });\n\n\t\t// Prune old samples\n\t\tthis.history = this.history.filter((s) => now - s.timestamp < this.historyWindow);\n\n\t\t// Calculate metrics\n\t\tconst { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();\n\t\tconst currentQuality = this.getCurrentQuality();\n\n\t\t// Per draft-ramadan-moq-fec-00: Use effective loss rate after FEC for ABR decisions\n\t\t// This allows FEC to mask burst losses without triggering unnecessary downgrades\n\t\tconst effectiveLossRate = this.fecStats.fecEnabled\n\t\t\t? this.fecStats.effectiveLossRate\n\t\t\t: rawLossRate;\n\n\t\t// Decision logic\n\t\tif (now - this.lastQualityChange < this.stabilityPeriod) {\n\t\t\treturn; // Wait for stability\n\t\t}\n\n\t\t// Check for downgrade - use effective loss rate (post-FEC)\n\t\t// Only downgrade if FEC cannot recover the losses\n\t\tif (effectiveLossRate > 0.05 || measuredBitrate < currentQuality.bitrate * this.downgradeThreshold) {\n\t\t\tthis.downgrade();\n\t\t\treturn;\n\t\t}\n\n\t\t// Check for upgrade (progressive increase) - use effective loss rate\n\t\tif (\n\t\t\tmeasuredBitrate > currentQuality.bitrate * this.upgradeThreshold &&\n\t\t\teffectiveLossRate < 0.01 &&\n\t\t\tthis.currentQualityIndex < this.ladder.length - 1\n\t\t) {\n\t\t\tthis.upgrade();\n\t\t}\n\t}\n\n\t/**\n\t * Update FEC statistics for ABR decisions\n\t *\n\t * Per draft-ramadan-moq-fec-00, ABR should use the effective loss rate\n\t * (after FEC recovery) rather than raw transport loss rate.\n\t *\n\t * @param stats - FEC statistics from the FEC decoder\n\t */\n\tsetFecStats(stats: FecStatsForAbr) {\n\t\tthis.fecStats = stats;\n\t}\n\n\t/**\n\t * Get current FEC statistics\n\t */\n\tgetFecStats(): FecStatsForAbr {\n\t\treturn { ...this.fecStats };\n\t}\n\n\tprivate calculateMetrics() {\n\t\tif (this.history.length < 2) {\n\t\t\treturn { measuredBitrate: 0, lossRate: 0 };\n\t\t}\n\n\t\tconst totalBytes = this.history.reduce((sum, s) => sum + s.bytesReceived, 0);\n\t\tconst totalLost = this.history.reduce((sum, s) => sum + s.packetsLost, 0);\n\t\tconst duration = (this.history[this.history.length - 1].timestamp - this.history[0].timestamp) / 1000;\n\n\t\tconst measuredBitrate = duration > 0 ? (totalBytes * 8) / duration : 0;\n\t\tconst lossRate = totalLost / (this.history.length + totalLost);\n\n\t\treturn { measuredBitrate, lossRate };\n\t}\n\n\tprivate upgrade() {\n\t\tif (this.currentQualityIndex >= this.ladder.length - 1) return;\n\n\t\tthis.currentQualityIndex++;\n\t\tthis.lastQualityChange = Date.now();\n\n\t\tconst newQuality = this.getCurrentQuality();\n\t\tconsole.log(`[ABR] Upgrading to ${newQuality.name} (${newQuality.bitrate / 1_000_000}Mbps)`);\n\n\t\tthis.onQualityChange?.(newQuality);\n\t}\n\n\tprivate downgrade() {\n\t\tif (this.currentQualityIndex <= 0) return;\n\n\t\tthis.currentQualityIndex--;\n\t\tthis.lastQualityChange = Date.now();\n\n\t\tconst newQuality = this.getCurrentQuality();\n\t\tconsole.log(`[ABR] Downgrading to ${newQuality.name} (${newQuality.bitrate / 1_000_000}Mbps)`);\n\n\t\tthis.onQualityChange?.(newQuality);\n\t}\n\n\t/**\n\t * Select a ladder rendition: the caller switched to it (a manual override,\n\t * or a switch the caller made). Throws for a name the ladder does not carry.\n\t */\n\tsetQuality(name: string) {\n\t\tconst index = this.ladder.findIndex((q) => q.name === name);\n\t\tif (index < 0) throw new RangeError(`[ABR] rendition ${name} is not in the ladder`);\n\t\tthis.currentQualityIndex = index;\n\t\tthis.lastQualityChange = Date.now();\n\t\tthis.onQualityChange?.(this.getCurrentQuality());\n\t}\n\n\t/**\n\t * Get current metrics including both raw and effective loss rates\n\t */\n\tgetMetrics(): {\n\t\tmeasuredBitrate: number;\n\t\trawLossRate: number;\n\t\teffectiveLossRate: number;\n\t\tfecEnabled: boolean;\n\t\tsampleCount: number;\n\t} {\n\t\tconst { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();\n\t\treturn {\n\t\t\tmeasuredBitrate,\n\t\t\trawLossRate,\n\t\t\teffectiveLossRate: this.fecStats.fecEnabled ? this.fecStats.effectiveLossRate : rawLossRate,\n\t\t\tfecEnabled: this.fecStats.fecEnabled,\n\t\t\tsampleCount: this.history.length,\n\t\t};\n\t}\n\n\t/**\n\t * Reset history and start fresh\n\t */\n\treset() {\n\t\tthis.history = [];\n\t\tthis.lastQualityChange = 0;\n\t}\n}\n", "import { compareNtpShort, isNtpShort } from \"@blockcast/mmt-base\";\nimport { carriesMedia, endpointKey, selectMulticastTier, type MulticastTierEndpoint } from \"./multicast-tier.js\";\n\nexport type OrchestratorState = \"stable\" | \"joining\";\nexport type OrchestratorCommand =\n\t| { kind: \"join-target\"; current: MulticastTierEndpoint; target: MulticastTierEndpoint }\n\t| { kind: \"commit-swap\"; target: MulticastTierEndpoint; old: MulticastTierEndpoint }\n\t| { kind: \"abandon-join\"; current: MulticastTierEndpoint; target: MulticastTierEndpoint };\n\nexport interface MulticastTierPacket {\n\tendpointKey: string;\n\tkeyframe: boolean;\n\t/**\n\t * The packet's MMTP header `timestamp`: its UTC sending instant in NTP short\n\t * format (ISO/IEC 23008-1 \u00A79.2.2), which wraps every 2^16 s (~18.2 h). The\n\t * orchestrator orders these only with RFC 1982 serial arithmetic\n\t * (`compareNtpShort`), never with `<` or `Math.max`.\n\t *\n\t * A non-finite value means the host could not read a header; that packet\n\t * neither raises the high-water mark nor commits. Any other value that is\n\t * not NTP short is a host bug and throws a RangeError.\n\t */\n\ttimestamp: number;\n\thasValidCodecConfig: boolean;\n}\n\nexport interface OrchestratorOptions {\n\tendpoints: readonly MulticastTierEndpoint[];\n\tinitialEndpoint?: MulticastTierEndpoint;\n\tadaptive?: boolean;\n\tnow: () => number;\n\tdwellMs?: number;\n\tjoinDeadlineMs?: number;\n}\n\nexport interface GoodputSample { bitrate: number | undefined; }\nexport interface HeadroomSample { healthy: boolean; }\n\nexport const DEFAULT_DWELL_MS = 3_000;\nexport const DEFAULT_JOIN_DEADLINE_MS = 5_000;\n\nexport function goodputFromStats(\n\tprevious: { bytesReceived: number; atMs: number } | undefined,\n\tcurrent: { bytesReceived: number; atMs: number },\n\tminSampleMs = 500,\n): number | undefined {\n\tif (!previous) return undefined;\n\tconst durationMs = current.atMs - previous.atMs;\n\tconst bytes = current.bytesReceived - previous.bytesReceived;\n\tif (durationMs < minSampleMs || bytes <= 0) return undefined;\n\treturn (bytes * 8 * 1000) / durationMs;\n}\n\nexport class MulticastOrchestrator {\n\t#state: OrchestratorState = \"stable\";\n\t#current: MulticastTierEndpoint | undefined;\n\t#target: MulticastTierEndpoint | undefined;\n\t#estimate: number | undefined;\n\t#favored: { key: string; since: number; source: \"goodput\" | \"headroom\" } | undefined;\n\t#joinStartedAt = 0;\n\t/**\n\t * Latest active-tier send time (NTP short), or undefined before the first.\n\t * \"Latest\" is serial order, not the numerically largest value: after the\n\t * field wraps, the largest value is the stale one.\n\t */\n\t#highWater: number | undefined;\n\treadonly #dwellMs: number;\n\treadonly #joinDeadlineMs: number;\n\n\tconstructor(private readonly options: OrchestratorOptions) {\n\t\tthis.#dwellMs = options.dwellMs ?? DEFAULT_DWELL_MS;\n\t\tthis.#joinDeadlineMs = options.joinDeadlineMs ?? DEFAULT_JOIN_DEADLINE_MS;\n\t\tif (!Number.isFinite(this.#dwellMs) || !Number.isFinite(this.#joinDeadlineMs) ||\n\t\t\tthis.#dwellMs <= 0 || this.#joinDeadlineMs <= 0) {\n\t\t\tthrow new Error(\"Multicast dwell and join deadline must be positive\");\n\t\t}\n\t}\n\n\tget state(): OrchestratorState { return this.#state; }\n\tget current(): MulticastTierEndpoint | undefined { return this.#current; }\n\tget target(): MulticastTierEndpoint | undefined { return this.#target; }\n\tget estimate(): number | undefined { return this.#estimate; }\n\n\tstart(): MulticastTierEndpoint | undefined {\n\t\tthis.#current ??= this.options.initialEndpoint ?? selectMulticastTier(this.options.endpoints, undefined);\n\t\treturn this.#current;\n\t}\n\n\tonGoodput(sample: GoodputSample): OrchestratorCommand | undefined {\n\t\tconst bitrate = sample.bitrate;\n\t\tif (!this.#current) return undefined;\n\t\t// Before the adaptive gate: a selected join runs with adaptation off, and a\n\t\t// target that delivers nothing reaches its deadline only on this clock.\n\t\tif (this.#state === \"joining\") return this.poll();\n\t\tif (this.options.adaptive === false) return undefined;\n\t\tif (bitrate === undefined || !Number.isFinite(bitrate)) return undefined;\n\t\tthis.#estimate = bitrate;\n\t\tconst desired = selectMulticastTier(this.options.endpoints, bitrate);\n\t\tif (!desired) return undefined;\n\t\tconst desiredKey = endpointKey(desired);\n\t\tif (desiredKey === endpointKey(this.#current)) {\n\t\t\tif (this.#favored?.source === \"goodput\") this.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\t\tif (typeof desired.bandwidth !== \"number\" || typeof this.#current.bandwidth !== \"number\" ||\n\t\t\tdesired.bandwidth >= this.#current.bandwidth) {\n\t\t\tif (this.#favored?.source === \"goodput\") this.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst now = this.#now();\n\t\tif (!this.#favored || this.#favored.key !== desiredKey || this.#favored.source !== \"goodput\") {\n\t\t\tthis.#favored = { key: desiredKey, since: now, source: \"goodput\" };\n\t\t\treturn undefined;\n\t\t}\n\t\tif (now - this.#favored.since < this.#dwellMs) return undefined;\n\n\t\tthis.#state = \"joining\";\n\t\tthis.#target = desired;\n\t\tthis.#joinStartedAt = now;\n\t\tthis.#favored = undefined;\n\t\treturn { kind: \"join-target\", current: this.#current, target: desired };\n\t}\n\n\tonHeadroom(sample: HeadroomSample): OrchestratorCommand | undefined {\n\t\tif (!this.#current || this.options.adaptive === false || this.#state === \"joining\") return undefined;\n\t\tif (!sample.healthy) {\n\t\t\tif (this.#favored?.source === \"headroom\") this.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\t\tif (this.#favored?.source === \"goodput\") return undefined;\n\n\t\tconst currentBandwidth = this.#current.bandwidth;\n\t\tif (typeof currentBandwidth !== \"number\") return undefined;\n\t\t// Ranked here rather than through selectMulticastTier: up-selection\n\t\t// advances exactly one adjacent rung, where the selector fits a budget to\n\t\t// an estimate. The media filter is shared so a repair-only endpoint above\n\t\t// the current rung is not joined as an upgrade, which would swap the\n\t\t// receiver onto repair symbols and no media.\n\t\tconst desired = this.options.endpoints\n\t\t\t.filter((endpoint) =>\n\t\t\t\tcarriesMedia(endpoint) && typeof endpoint.bandwidth === \"number\" && endpoint.bandwidth > currentBandwidth,\n\t\t\t)\n\t\t\t.sort((a, b) => a.bandwidth! - b.bandwidth!)[0];\n\t\tif (!desired) {\n\t\t\tthis.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst desiredKey = endpointKey(desired);\n\t\tif (desiredKey === endpointKey(this.#current)) {\n\t\t\tthis.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\t\tconst now = this.#now();\n\t\tif (!this.#favored || this.#favored.key !== desiredKey) {\n\t\t\tthis.#favored = { key: desiredKey, since: now, source: \"headroom\" };\n\t\t\treturn undefined;\n\t\t}\n\t\tif (now - this.#favored.since < this.#dwellMs) return undefined;\n\n\t\tthis.#state = \"joining\";\n\t\tthis.#target = desired;\n\t\tthis.#joinStartedAt = now;\n\t\tthis.#favored = undefined;\n\t\treturn { kind: \"join-target\", current: this.#current, target: desired };\n\t}\n\n\t/**\n\t * Join `target` because the viewer selected it, not because goodput or\n\t * headroom ranked it. It runs whether or not the orchestrator adapts, and\n\t * takes the path an adaptive join takes: commit on the target's RAP at or\n\t * beyond the high-water mark, abandon at the join deadline. While it is\n\t * pending nothing re-ranks the ladder; goodput only advances the deadline.\n\t *\n\t * Returns undefined, changing nothing, while another join is pending (cancel\n\t * it first), when `target` is the current tier, or when it carries no media.\n\t */\n\tselect(target: MulticastTierEndpoint): OrchestratorCommand | undefined {\n\t\tif (!this.#current || this.#state === \"joining\" || !carriesMedia(target)) return undefined;\n\t\tif (endpointKey(target) === endpointKey(this.#current)) return undefined;\n\t\tthis.#state = \"joining\";\n\t\tthis.#target = target;\n\t\tthis.#joinStartedAt = this.#now();\n\t\tthis.#favored = undefined;\n\t\treturn { kind: \"join-target\", current: this.#current, target };\n\t}\n\n\t/**\n\t * Feed a packet from the active tier (to advance the high-water mark) or\n\t * from the in-flight target (to find a committable RAP).\n\t *\n\t * Serial order is only the true order while the compared instants are less\n\t * than `NTP_SHORT_HALF_RANGE` (2^15 s, ~9.1 h) apart. Feeding every\n\t * active-tier packet keeps the mark that close to the live stream. A mark\n\t * refreshed only by commits can fall further behind than that, and a\n\t * target RAP may then compare as older, so the join is abandoned.\n\t */\n\tonTierPacket(packet: MulticastTierPacket): OrchestratorCommand | undefined {\n\t\tif (!this.#current) return undefined;\n\t\tconst sentAt = tierPacketSendTime(packet);\n\t\t// An instant exactly half the space from the mark (RFC 1982's undefined\n\t\t// case) is neither later nor earlier, so it leaves the mark alone, as a\n\t\t// reordered earlier packet does.\n\t\tif (packet.endpointKey === endpointKey(this.#current) && sentAt !== undefined &&\n\t\t\t(this.#highWater === undefined || compareNtpShort(sentAt, this.#highWater) === \"after\")) {\n\t\t\tthis.#highWater = sentAt;\n\t\t}\n\t\tif (this.#state !== \"joining\" || !this.#target) return undefined;\n\t\tconst expired = this.poll();\n\t\tif (expired) return expired;\n\t\tif (\n\t\t\tpacket.endpointKey !== endpointKey(this.#target) ||\n\t\t\t!packet.keyframe ||\n\t\t\t!packet.hasValidCodecConfig ||\n\t\t\tsentAt === undefined ||\n\t\t\t!this.#reachesHighWater(sentAt)\n\t\t) return undefined;\n\n\t\tconst command: OrchestratorCommand = {\n\t\t\tkind: \"commit-swap\",\n\t\t\ttarget: this.#target,\n\t\t\told: this.#current,\n\t\t};\n\t\tthis.#highWater = sentAt;\n\t\tthis.#current = this.#target;\n\t\tthis.#target = undefined;\n\t\tthis.#state = \"stable\";\n\t\treturn command;\n\t}\n\n\tpoll(): OrchestratorCommand | undefined {\n\t\tif (this.#state !== \"joining\" || !this.#target || !this.#current) return undefined;\n\t\tif (this.#now() - this.#joinStartedAt < this.#joinDeadlineMs) return undefined;\n\t\treturn this.cancel();\n\t}\n\n\tcancel(): OrchestratorCommand | undefined {\n\t\tif (this.#state !== \"joining\" || !this.#target || !this.#current) return undefined;\n\t\tconst command: OrchestratorCommand = {\n\t\t\tkind: \"abandon-join\",\n\t\t\tcurrent: this.#current,\n\t\t\ttarget: this.#target,\n\t\t};\n\t\tthis.#target = undefined;\n\t\tthis.#state = \"stable\";\n\t\treturn command;\n\t}\n\n\t/**\n\t * Whether a target RAP sent at `sentAt` is at or beyond the active tier's\n\t * high-water mark. A pair exactly half the NTP-short space apart has no\n\t * serial order, so it cannot show that and does not commit.\n\t */\n\t#reachesHighWater(sentAt: number): boolean {\n\t\tif (this.#highWater === undefined) return true;\n\t\tconst order = compareNtpShort(sentAt, this.#highWater);\n\t\treturn order === \"after\" || order === \"equal\";\n\t}\n\n\t#now(): number {\n\t\tconst now = this.options.now();\n\t\tif (!Number.isFinite(now)) throw new Error(\"Multicast orchestrator clock must be finite\");\n\t\treturn now;\n\t}\n}\n\n/** A tier packet's NTP-short send time, or undefined when the host had no header to read. */\nfunction tierPacketSendTime(packet: MulticastTierPacket): number | undefined {\n\tif (!Number.isFinite(packet.timestamp)) return undefined;\n\tif (!isNtpShort(packet.timestamp)) {\n\t\tthrow new RangeError(\n\t\t\t`Multicast tier packet timestamp must be an MMTP NTP-short send time, got ${packet.timestamp}`,\n\t\t);\n\t}\n\treturn packet.timestamp;\n}\n", "import { MMTP_PACKET_ID_MAX, MMTP_SIGNALING_PACKET_ID } from \"@blockcast/mmt-base\";\nimport type {\n\tWindowMulticastSubscribeConfig,\n\tWindowMulticastSubscribeConfigBytes,\n} from \"./types.js\";\n\nexport type MulticastTransportPreference = \"auto\" | \"native\" | \"tunnel\";\n\nexport { DEFAULT_AMT_RELAY_PORT } from \"@blockcast/mmt-base\";\n\nexport interface MulticastEndpointConfig {\n\tgroupAddress: string;\n\tsourceAddress?: string;\n\tport: number;\n\tmode?: MulticastTransportPreference;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\n}\n\n/**\n * Input accepted by the canonical window.multicast request converter.\n *\n * `mode` and the `amtRelay*` fields are internal endpoint-selection state;\n * they are deliberately not emitted on the page-facing request. The\n * resulting `origin` is the only acquisition selector understood by the\n * gateway.\n */\nexport interface WindowMulticastEndpointInput {\n\tgroup: string;\n\tport: number;\n\tsource?: string;\n\tmode?: MulticastTransportPreference;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\n\tpacketIds?: readonly number[];\n\ttracks?: ReadonlyArray<{ name: string; packetId: number }>;\n}\n\n/**\n * Convert internal endpoint-selection state to the canonical page contract.\n *\n * Native is the default and an explicit `native` selection always wins over\n * stale relay metadata. Tunnel/AMT requests must carry the resolved relay\n * and port; there is no implicit relay-port default at this boundary.\n *\n * That rule is deliberate and is NOT the place to fix a relay that names no\n * port: this is the last hop before the page-facing request, so a default\n * applied here would be invisible to every caller and to the join it produces.\n * A catalog relay with no port gets `DEFAULT_AMT_RELAY_PORT` upstream, in the\n * readers that resolve it, and arrives here explicit like any other\n * (BLO-38176).\n */\nexport function toWindowMulticastSubscribeConfig(\n\tinput: WindowMulticastEndpointInput,\n): WindowMulticastSubscribeConfig {\n\tif (input.mode !== undefined && ![\"auto\", \"native\", \"tunnel\"].includes(input.mode)) {\n\t\tthrow new TypeError(`Unsupported multicast mode: ${String(input.mode)}`);\n\t}\n\t// An explicit non-array (the null \"endpoint unusable\" sentinel from\n\t// selectEndpointPacketIdsForTracks) must not degrade into \"no filter\".\n\tif (input.packetIds !== undefined && !Array.isArray(input.packetIds)) {\n\t\tthrow new TypeError(\n\t\t\t`multicast packetIds must be an array, got ${input.packetIds === null ? \"null\" : typeof input.packetIds}`,\n\t\t);\n\t}\n\t// Every join request passes through here, so the track packet_id range is\n\t// enforced here rather than trusted from each caller.\n\tif (input.packetIds?.some(id => !isMulticastTrackPacketId(id)) ||\n\t\tinput.tracks?.some(track => !isMulticastTrackPacketId(track.packetId))) {\n\t\tthrow new RangeError(\n\t\t\t`multicast packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}; ` +\n\t\t\t\"packetId 0 is the MMTP signaling flow and is never a track's\",\n\t\t);\n\t}\n\n\tconst base = {\n\t\tgroup: input.group,\n\t\tport: input.port,\n\t\t...(input.source?.trim() ? { source: input.source.trim() } : {}),\n\t\tconsume: \"bytes\" as const,\n\t\t...(input.packetIds?.length ? { packetIds: [...input.packetIds] } : {}),\n\t};\n\n\tconst relay = input.amtRelay?.trim();\n\tconst wantsAmt = input.mode === \"tunnel\" ||\n\t\t(input.mode !== \"native\" && relay !== undefined && relay !== \"\");\n\tif (!wantsAmt) {\n\t\tconst native: WindowMulticastSubscribeConfigBytes = {\n\t\t\t...base,\n\t\t\torigin: \"ip-multicast\",\n\t\t\t...(input.tracks?.length ? { tracks: input.tracks.map(({ name, packetId }) => ({ name, packetId })) } : {}),\n\t\t};\n\t\treturn native;\n\t}\n\n\tconst relayPort = input.amtRelayPort;\n\tif (!relay || relayPort === undefined || !isPositivePort(relayPort)) {\n\t\tthrow new TypeError(\"AMT requires a resolved relay and a valid relay port\");\n\t}\n\treturn {\n\t\t...base,\n\t\torigin: \"amt\",\n\t\trelay,\n\t\trelayPort,\n\t};\n}\n\nexport function isValidMulticastGroupAddress(address: string | undefined): boolean {\n\tconst value = address?.trim().toLowerCase();\n\tif (!value) return false;\n\tif (value.startsWith(\"ff\")) return isValidIpv6MulticastAddress(value);\n\n\tconst parts = value.split(\".\");\n\tif (parts.length !== 4) return false;\n\tconst octets = parts.map((part) => {\n\t\tif (!/^\\d+$/.test(part)) return Number.NaN;\n\t\tconst octet = Number(part);\n\t\treturn Number.isInteger(octet) && octet >= 0 && octet <= 255 ? octet : Number.NaN;\n\t});\n\treturn octets.every(Number.isInteger) && octets[0] >= 224 && octets[0] <= 239;\n}\n\nexport function normalizeMulticastEndpoint(\n\tendpoint: MulticastEndpointConfig,\n): MulticastEndpointConfig | undefined {\n\tconst groupAddress = endpoint.groupAddress.trim().toLowerCase();\n\tconst sourceAddress = endpoint.sourceAddress?.trim().toLowerCase() || undefined;\n\tconst amtRelay = endpoint.amtRelay?.trim().toLowerCase() || undefined;\n\tif (!isValidMulticastGroupAddress(groupAddress) || !isPositivePort(endpoint.port)) return undefined;\n\tif (endpoint.mode && ![\"auto\", \"native\", \"tunnel\"].includes(endpoint.mode)) return undefined;\n\tif (endpoint.amtRelayPort !== undefined && !isPositivePort(endpoint.amtRelayPort)) return undefined;\n\n\treturn {\n\t\tgroupAddress,\n\t\t...(sourceAddress ? { sourceAddress } : {}),\n\t\tport: endpoint.port,\n\t\t...(endpoint.mode ? { mode: endpoint.mode } : {}),\n\t\t...(amtRelay ? { amtRelay } : {}),\n\t\t...(endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {}),\n\t};\n}\n\n/** Stable ownership key for one IWA multicast/AMT subscription. */\nexport function multicastEndpointKey(endpoint: MulticastEndpointConfig): string {\n\tconst normalized = normalizeMulticastEndpoint(endpoint);\n\tif (!normalized) throw new Error(\"Invalid multicast endpoint\");\n\treturn JSON.stringify([\n\t\tnormalized.sourceAddress ?? \"\",\n\t\tnormalized.groupAddress,\n\t\tnormalized.port,\n\t\tnormalized.mode ?? \"auto\",\n\t\tnormalized.amtRelay ?? \"\",\n\t\tnormalized.amtRelayPort ?? \"\",\n\t]);\n}\n\n/**\n * True when `value` may be advertised as a multicast track's MMTP `packet_id`:\n * an integer in 1..MMTP_PACKET_ID_MAX. packet_id 0 (MMTP_SIGNALING_PACKET_ID)\n * is the MMTP signaling flow -- the FFmpeg `moq_mmt` muxer sends the AL-FEC\n * message (0x0203) on it into the same group as the media -- so it is never a\n * track's. Only a number qualifies: a string, boolean or null is not coerced.\n */\nexport function isMulticastTrackPacketId(value: unknown): value is number {\n\treturn Number.isInteger(value) &&\n\t\t(value as number) > MMTP_SIGNALING_PACKET_ID &&\n\t\t(value as number) <= MMTP_PACKET_ID_MAX;\n}\n\nexport function isUsableMulticastEndpoint(\n\tendpoint: { groupAddress?: string; port?: unknown } | undefined,\n): boolean {\n\tconst port = Number(endpoint?.port);\n\treturn isValidMulticastGroupAddress(endpoint?.groupAddress) && isPositivePort(port);\n}\n\n/**\n * Unprefixed aliases of the `mc_` URL override keys, retired in BLO-37507, each\n * paired with the key that replaces it.\n */\nconst RETIRED_MULTICAST_URL_PARAMS = [\n\t[\"amt_relay\", \"mc_amt_relay\"],\n\t[\"amt_relay_port\", \"mc_amt_relay_port\"],\n] as const;\n\n/**\n * Reject a URL multicast override that still carries a retired unprefixed alias.\n *\n * Both URL readers call this only once a valid override is established (a valid\n * multicast `mc_group` and a valid `mc_port`; `readUrlMulticastConfig` also\n * requires the whole endpoint to normalize), which is where they used to read\n * the aliases: a link that\n * relied on one fails here with its replacement named, instead of silently\n * losing its AMT relay (and, in `readUrlMulticastConfig`, its tunnel mode).\n * Presence is the error, whatever the value and whether or not the `mc_` form\n * is also present: quietly preferring one of two keys is the tolerant read being\n * retired. Outside an override the readers read no override key at all, the\n * `mc_` forms included, so the bare names are left to the page; they are live\n * page parameters elsewhere, and keeping clear of those is what the `mc_`\n * namespace is for.\n */\nexport function assertNoRetiredMulticastUrlParams(params: URLSearchParams): void {\n\tfor (const [retired, replacement] of RETIRED_MULTICAST_URL_PARAMS) {\n\t\tif (params.has(retired)) {\n\t\t\tthrow new Error(\n\t\t\t\t`multicast URL override: query parameter \"${retired}\" is retired; use \"${replacement}\"`,\n\t\t\t);\n\t\t}\n\t}\n}\n\n/**\n * Read an explicit multicast endpoint override from `mc_*` URL parameters.\n * Throws when an override carries a retired unprefixed alias\n * (`assertNoRetiredMulticastUrlParams`).\n */\nexport function readUrlMulticastConfig(\n\tsearch: string | URLSearchParams = typeof location === \"undefined\" ? \"\" : location.search,\n): MulticastEndpointConfig | undefined {\n\tconst params = typeof search === \"string\" ? new URLSearchParams(search) : search;\n\tconst groupAddress = params.get(\"mc_group\") || \"\";\n\tconst port = positivePort(params.get(\"mc_port\"));\n\tif (!groupAddress || !port) return undefined;\n\n\tconst amtRelay = params.get(\"mc_amt_relay\") || undefined;\n\tconst amtRelayPort = positivePort(params.get(\"mc_amt_relay_port\"));\n\tconst requestedMode = params.get(\"mc_mode\");\n\tconst mode = amtRelay ? \"tunnel\" : multicastMode(requestedMode);\n\n\tconst endpoint = normalizeMulticastEndpoint({\n\t\tgroupAddress,\n\t\tsourceAddress: params.get(\"mc_source\") || undefined,\n\t\tport,\n\t\t...(mode ? { mode } : {}),\n\t\t...(amtRelay ? { amtRelay } : {}),\n\t\t...(amtRelayPort ? { amtRelayPort } : {}),\n\t});\n\t// An endpoint that does not normalize (e.g. a non-multicast mc_group) is no\n\t// override, so the page keeps its own bare names; the same scope\n\t// readPlayerMulticastConfig applies by validating the group first.\n\tif (endpoint) assertNoRetiredMulticastUrlParams(params);\n\treturn endpoint;\n}\n\nexport function isMulticastExplicitlyDisabled(\n\tsearch: string | URLSearchParams = typeof location === \"undefined\" ? \"\" : location.search,\n): boolean {\n\tconst params = typeof search === \"string\" ? new URLSearchParams(search) : search;\n\tconst multicast = params.get(\"multicast\")?.trim().toLowerCase();\n\tconst transport = params.get(\"transport\")?.trim().toLowerCase();\n\treturn transport === \"moq-only\" || [\"0\", \"false\", \"no\", \"off\"].includes(multicast || \"\");\n}\n\nfunction isValidIpv6MulticastAddress(value: string): boolean {\n\tif (!/^[0-9a-f:]+$/.test(value)) return false;\n\tconst compressed = value.includes(\"::\");\n\tif (value.indexOf(\"::\") !== value.lastIndexOf(\"::\")) return false;\n\tconst groups = value.split(\":\").filter(Boolean);\n\tif (groups.length === 0 || groups.length > 8) return false;\n\tif (!compressed && groups.length !== 8) return false;\n\treturn groups.every((group) => /^[0-9a-f]{1,4}$/.test(group));\n}\n\nfunction multicastMode(value: string | null): MulticastTransportPreference | undefined {\n\treturn value === \"native\" || value === \"tunnel\" || value === \"auto\" ? value : undefined;\n}\n\nfunction positivePort(value: string | null): number | undefined {\n\tconst port = Number(value);\n\treturn isPositivePort(port) ? port : undefined;\n}\n\nfunction isPositivePort(port: number): boolean {\n\treturn Number.isInteger(port) && port > 0 && port <= 65535;\n}\n", "import type { WindowMulticastSubscription } from \"./types.js\";\n\n/**\n * True when `subscription` delivers packets in batches through the `packets`\n * provider extension (BLO-36599).\n *\n * The pim window-multicast-bridge (0.2.0 and later) advertises the extension\n * in `eventTypes`. It builds only the events some listener asked for, so a\n * consumer that listens to `packets` alone gets one event per reader batch\n * instead of one DOM event per packet. A provider without the field, or one\n * that does not list `packets`, is a v1 provider: listen to `bytes`.\n */\nexport function subscriptionDeliversPacketBatches(\n\tsubscription: Pick<WindowMulticastSubscription, \"eventTypes\">,\n): boolean {\n\treturn subscription.eventTypes?.includes(\"packets\") === true;\n}\n", "import {\n\tmulticastEndpointKey,\n\tnormalizeMulticastEndpoint,\n\ttoWindowMulticastSubscribeConfig,\n} from \"./endpoint-config.js\";\nimport { subscriptionDeliversPacketBatches } from \"./packet-batch.js\";\nimport type {\n\tTransportConfig,\n\tWindowMulticastGateway,\n\tWindowMulticastSubscription,\n\tWindowMulticastSubscriptionState,\n} from \"./types.js\";\n\nexport interface MulticastSubscriptionConsumer {\n\tonBytes(data: ArrayBuffer): void;\n\tonState?(state: WindowMulticastSubscriptionState): void;\n\tonError?(error: Error): void;\n}\n\ninterface PoolEntry {\n\tsubscription: WindowMulticastSubscription;\n\tconsumers: Map<symbol, MulticastSubscriptionConsumer>;\n\t/**\n\t * Exactly one packet surface is listened to: `packets` when the provider\n\t * advertises it, `bytes` otherwise (BLO-36599).\n\t */\n\tpacketListener:\n\t\t| { type: \"bytes\"; listener: (event: CustomEvent<ArrayBuffer>) => void }\n\t\t| { type: \"packets\"; listener: (event: CustomEvent<readonly ArrayBuffer[]>) => void };\n\tstateListener: (event: CustomEvent<WindowMulticastSubscriptionState>) => void;\n\terrorListener: (event: CustomEvent<unknown>) => void;\n\tclosed: boolean;\n}\n\ninterface PoolRecord { claims: number; entry: Promise<PoolEntry>; }\n\nexport interface MulticastSubscriptionLease {\n\treadonly key: string;\n\trelease(): Promise<void>;\n}\n\n/** Shares only IWA multicast/AMT subscriptions. Unicast sessions remain per manager. */\nexport class MulticastSubscriptionPool {\n\treadonly #entries = new Map<string, PoolRecord>();\n\treadonly #gatewayIds = new WeakMap<object, number>();\n\t#nextGatewayId = 1;\n\n\tasync acquire(\n\t\tgateway: WindowMulticastGateway,\n\t\tconfig: TransportConfig,\n\t\tconsumer: MulticastSubscriptionConsumer,\n\t): Promise<MulticastSubscriptionLease> {\n\t\tconst endpoint = normalizeMulticastEndpoint({\n\t\t\tgroupAddress: config.group,\n\t\t\tsourceAddress: config.source,\n\t\t\tport: config.port,\n\t\t\tmode: config.mode,\n\t\t\tamtRelay: config.amtRelay,\n\t\t\tamtRelayPort: config.amtRelayPort,\n\t\t});\n\t\tif (!endpoint) throw new Error(\"Invalid multicast endpoint\");\n\t\tif (typeof gateway.subscribe !== \"function\") throw new Error(\"window.multicast.subscribe unavailable\");\n\n\t\tconst key = multicastEndpointKey(endpoint);\n\t\tconst internalKey = `${this.#gatewayId(gateway)}|${key}`;\n\t\tlet record = this.#entries.get(internalKey);\n\t\tif (!record) {\n\t\t\trecord = { claims: 0, entry: Promise.resolve(null as unknown as PoolEntry) };\n\t\t\tconst createdRecord = record;\n\t\t\trecord.entry = this.#createEntry(gateway, endpoint, (entry) => {\n\t\t\t\tif (this.#entries.get(internalKey) === createdRecord) this.#entries.delete(internalKey);\n\t\t\t\tthis.#detach(entry);\n\t\t\t}).catch((error) => {\n\t\t\t\tif (this.#entries.get(internalKey) === createdRecord) this.#entries.delete(internalKey);\n\t\t\t\tthrow error;\n\t\t\t});\n\t\t\tthis.#entries.set(internalKey, record);\n\t\t}\n\n\t\trecord.claims++;\n\t\tlet entry: PoolEntry;\n\t\ttry {\n\t\t\tentry = await record.entry;\n\t\t} catch (error) {\n\t\t\trecord.claims--;\n\t\t\tthrow error;\n\t\t}\n\t\tif (entry.closed) {\n\t\t\trecord.claims--;\n\t\t\treturn this.acquire(gateway, config, consumer);\n\t\t}\n\t\tconst token = Symbol(key);\n\t\tentry.consumers.set(token, consumer);\n\t\tlet released = false;\n\t\treturn {\n\t\t\tkey,\n\t\t\trelease: async () => {\n\t\t\t\tif (released) return;\n\t\t\t\treleased = true;\n\t\t\t\tentry.consumers.delete(token);\n\t\t\t\trecord.claims--;\n\t\t\t\tif (record.claims > 0) return;\n\t\t\t\tif (this.#entries.get(internalKey) === record) this.#entries.delete(internalKey);\n\t\t\t\tthis.#detach(entry);\n\t\t\t\tif (!entry.closed) await entry.subscription.unsubscribe();\n\t\t\t},\n\t\t};\n\t}\n\n\tget size(): number { return this.#entries.size; }\n\n\tasync #createEntry(\n\t\tgateway: WindowMulticastGateway,\n\t\tendpoint: NonNullable<ReturnType<typeof normalizeMulticastEndpoint>>,\n\t\tonClosed: (entry: PoolEntry) => void,\n\t): Promise<PoolEntry> {\n\t\tconst subscription = await gateway.subscribe!(toWindowMulticastSubscribeConfig({\n\t\t\tgroup: endpoint.groupAddress,\n\t\t\t...(endpoint.sourceAddress ? { source: endpoint.sourceAddress } : {}),\n\t\t\tport: endpoint.port,\n\t\t\t...(endpoint.mode ? { mode: endpoint.mode } : {}),\n\t\t\t...(endpoint.amtRelay ? { amtRelay: endpoint.amtRelay } : {}),\n\t\t\t...(endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {}),\n\t\t}));\n\t\tconst consumers = new Map<symbol, MulticastSubscriptionConsumer>();\n\t\tlet entry: PoolEntry;\n\t\t// A batch is delivered as the same per-packet onBytes calls the bytes\n\t\t// surface makes, in arrival order and with each call isolated, so a\n\t\t// consumer cannot tell the two apart; only the DOM event per packet is\n\t\t// gone. The batch array and its elements are the provider's own, uncopied.\n\t\tconst packetListener: PoolEntry[\"packetListener\"] = subscriptionDeliversPacketBatches(subscription)\n\t\t\t? {\n\t\t\t\t\ttype: \"packets\",\n\t\t\t\t\tlistener: (event) => {\n\t\t\t\t\t\tfor (const data of event.detail) {\n\t\t\t\t\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onBytes(data));\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: {\n\t\t\t\t\ttype: \"bytes\",\n\t\t\t\t\tlistener: (event) => {\n\t\t\t\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onBytes(event.detail));\n\t\t\t\t\t},\n\t\t\t\t};\n\t\tconst stateListener = (event: CustomEvent<WindowMulticastSubscriptionState>) => {\n\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onState?.(event.detail));\n\t\t\tif (event.detail === \"closed\" && !entry.closed) {\n\t\t\t\tentry.closed = true;\n\t\t\t\tonClosed(entry);\n\t\t\t}\n\t\t};\n\t\tconst errorListener = (event: CustomEvent<unknown>) => {\n\t\t\tconst error = event.detail instanceof Error ? event.detail : new Error(String(event.detail));\n\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onError?.(error));\n\t\t};\n\t\tentry = { subscription, consumers, packetListener, stateListener, errorListener, closed: false };\n\t\tif (packetListener.type === \"packets\") {\n\t\t\tsubscription.addEventListener(\"packets\", packetListener.listener);\n\t\t} else {\n\t\t\tsubscription.addEventListener(\"bytes\", packetListener.listener);\n\t\t}\n\t\tsubscription.addEventListener(\"state\", stateListener);\n\t\tsubscription.addEventListener(\"error\", errorListener);\n\t\treturn entry;\n\t}\n\n\t#gatewayId(gateway: WindowMulticastGateway): number {\n\t\tconst object = gateway as object;\n\t\tlet id = this.#gatewayIds.get(object);\n\t\tif (!id) {\n\t\t\tid = this.#nextGatewayId++;\n\t\t\tthis.#gatewayIds.set(object, id);\n\t\t}\n\t\treturn id;\n\t}\n\n\t#detach(entry: PoolEntry): void {\n\t\tconst { packetListener } = entry;\n\t\tif (packetListener.type === \"packets\") {\n\t\t\tentry.subscription.removeEventListener(\"packets\", packetListener.listener);\n\t\t} else {\n\t\t\tentry.subscription.removeEventListener(\"bytes\", packetListener.listener);\n\t\t}\n\t\tentry.subscription.removeEventListener(\"state\", entry.stateListener);\n\t\tentry.subscription.removeEventListener(\"error\", entry.errorListener);\n\t}\n\n\t#dispatch(\n\t\tconsumers: Map<symbol, MulticastSubscriptionConsumer>,\n\t\tcallback: (consumer: MulticastSubscriptionConsumer) => void,\n\t): void {\n\t\tfor (const consumer of consumers.values()) {\n\t\t\ttry { callback(consumer); } catch (error) { console.error(\"[Transport] multicast consumer callback failed\", error); }\n\t\t}\n\t}\n}\n\nexport const multicastSubscriptionPool = new MulticastSubscriptionPool();\n", "/**\n * @blockcast/transport - Transport Manager\n *\n * Unified transport abstraction for MoQ players supporting:\n * - window.multicast gateway subscriptions\n * - AMT proxy tunneling (RFC 7450) with optional DRIAD relay discovery\n * - WebTransport/QUIC (MoQ-lite protocol)\n * - WebSocket fallback\n *\n * Transport promotion hierarchy (WebTransport-first for fast startup):\n * 1. WebTransport - immediate connection for fast channel select\n * 2. Background discovery - check if window.multicast is available\n * 3. Promote to gateway multicast if available\n * 4. WebSocket fallback if WebTransport unavailable\n *\n * Features:\n * - Progressive adaptive bitrate based on network conditions\n * - FEC (Forward Error Correction) for burst loss recovery\n * - Seamless transport switching with session continuity\n * - Unified FEC/ABR pipeline across all transport modes\n */\n\nimport type {\n\tTransportConfig,\n\tTransportConnectConfig,\n\tTransportSubscription,\n\tTransportCapabilities,\n\tTransportState,\n\tQualityLevel,\n\tWindowMulticastCapabilities,\n\tWindowMulticastGateway,\n\tWindowMulticastSubscriptionState,\n} from \"./types.js\";\nimport { DRIADDiscovery } from \"./driad-discovery.js\";\nimport { AdaptiveBitrateController } from \"./abr-controller.js\";\nimport {\n\tMulticastOrchestrator,\n\ttype MulticastTierPacket,\n\ttype OrchestratorCommand,\n\ttype OrchestratorOptions,\n} from \"./multicast-orchestrator.js\";\nimport type { MulticastTierEndpoint } from \"./multicast-tier.js\";\nimport { multicastSubscriptionPool } from \"./multicast-subscription-pool.js\";\nimport { normalizeMulticastEndpoint } from \"./endpoint-config.js\";\n\n/**\n * Transport Manager\n *\n * Manages transport connections with automatic mode selection:\n * WebTransport -> window.multicast -> AMT proxy -> WebSocket\n */\nexport class TransportManager {\n\tprivate state: TransportState = {\n\t\tmode: \"disconnected\",\n\t\tconnected: false,\n\t\tpacketsReceived: 0,\n\t\tbytesReceived: 0,\n\t\tlastPacketTime: 0,\n\t};\n\n\tprivate capabilities: TransportCapabilities | null = null;\n\tprivate subscription: TransportSubscription | null = null;\n\tprivate driad = new DRIADDiscovery();\n\t/** Built by `connect()` from the caller's ladder; there is no built-in one. */\n\tprivate abr: AdaptiveBitrateController | null = null;\n\tprivate multicastOrchestrator: MulticastOrchestrator | null = null;\n\tprivate onOrchestratorCommand?: (command: OrchestratorCommand) => void;\n\tprivate connectionGeneration = 0;\n\n\tprivate onPacket?: (data: ArrayBuffer) => void;\n\tprivate onError?: (error: Error) => void;\n\tprivate onStateChange?: (state: TransportState) => void;\n\n\tprivate getWindow(): Window | undefined {\n\t\treturn typeof window === \"undefined\" ? undefined : window;\n\t}\n\n\tprivate getWindowMulticastGateway(): WindowMulticastGateway | undefined {\n\t\treturn this.getWindow()?.multicast;\n\t}\n\n\tprivate async getGatewayCapabilities(\n\t\tgateway: WindowMulticastGateway,\n\t): Promise<WindowMulticastCapabilities | undefined> {\n\t\tif (!gateway?.capabilities) return undefined;\n\t\ttry {\n\t\t\treturn typeof gateway.capabilities === \"function\" ?\n\t\t\t\tawait gateway.capabilities() :\n\t\t\t\tgateway.capabilities;\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] window.multicast capabilities failed:\", error);\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async gatewaySupportsBytes(gateway: WindowMulticastGateway | undefined): Promise<boolean> {\n\t\tif (typeof gateway?.subscribe !== \"function\") return false;\n\t\tif (!gateway.capabilities) return true;\n\n\t\tconst capabilities = await this.getGatewayCapabilities(gateway);\n\t\tif (!capabilities) return false;\n\t\tconst consumeModes = capabilities.consumeModes;\n\t\treturn !consumeModes || consumeModes.includes(\"bytes\");\n\t}\n\n\t/**\n\t * Detect available transport capabilities\n\t */\n\tasync detectCapabilities(): Promise<TransportCapabilities> {\n\t\tif (this.capabilities) return this.capabilities;\n\n\t\tconst gateway = this.getWindowMulticastGateway();\n\t\tconst hasGatewayBytes = await this.gatewaySupportsBytes(gateway);\n\t\tconst win = this.getWindow();\n\n\t\tthis.capabilities = {\n\t\t\tnativeSSM: hasGatewayBytes,\n\t\t\tnativeIGMP: hasGatewayBytes,\n\t\t\tdirectSockets: typeof win?.UDPSocket !== \"undefined\",\n\t\t\tamt: false,\n\t\t\tdriad: true, // DRIAD uses HTTP proxy, always available\n\t\t\tfecDecode: false,\n\t\t};\n\n\t\tconsole.log(\"[Transport] Capabilities:\", this.capabilities);\n\t\treturn this.capabilities;\n\t}\n\n\t/**\n\t * Connect using best available transport\n\t *\n\t * Promotion hierarchy:\n\t * 1. window.multicast gateway bytes\n\t * 2. AMT proxy (specified or discovered via DRIAD)\n\t * 3. MOQ WebTransport (final fallback)\n\t *\n\t * The ABR ladder (`abrLadder`) and initial rendition (`initialQuality`)\n\t * are checked first, before anything is torn down. A config that fails is\n\t * refused with the same shape as an invalid endpoint, the error to the\n\t * error callback and `false` resolved; unlike one, it is caught before the\n\t * live subscription closes, so a refused reconnect leaves that subscription,\n\t * its state and any connect already in flight untouched.\n\t */\n\tasync connect(config: TransportConnectConfig): Promise<boolean> {\n\t\tlet abr: AdaptiveBitrateController;\n\t\ttry {\n\t\t\tabr = new AdaptiveBitrateController({\n\t\t\t\tladder: config.abrLadder,\n\t\t\t\tinitialQuality: config.initialQuality?.name,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\t// A refused config is a caller bug. Without an error callback, say so\n\t\t\t// on the console, since `false` alone also means \"no multicast path\";\n\t\t\t// with one, the callback is the report.\n\t\t\tconst refused = error instanceof Error ? error : new Error(String(error));\n\t\t\tif (this.onError) this.onError(refused);\n\t\t\telse console.error(\"[Transport] ABR config refused, not connecting:\", refused);\n\t\t\treturn false;\n\t\t}\n\t\tconst generation = ++this.connectionGeneration;\n\t\tthis.subscription?.close();\n\t\tthis.subscription = null;\n\t\t// The previous connection's ladder ends with it, as in disconnect(): a\n\t\t// reconnect that bails below (invalid endpoint, or superseded while\n\t\t// detecting capabilities) leaves no ladder, and only the success path\n\t\t// installs `abr`.\n\t\tthis.abr = null;\n\t\tthis.state = {\n\t\t\tmode: \"disconnected\",\n\t\t\tconnected: false,\n\t\t\tpacketsReceived: 0,\n\t\t\tbytesReceived: 0,\n\t\t\tlastPacketTime: 0,\n\t\t};\n\t\tthis.onStateChange?.(this.state);\n\t\tconst endpoint = normalizeMulticastEndpoint({\n\t\t\tgroupAddress: config.group,\n\t\t\tsourceAddress: config.source,\n\t\t\tport: config.port,\n\t\t\tmode: config.mode,\n\t\t\tamtRelay: config.amtRelay,\n\t\t\tamtRelayPort: config.amtRelayPort,\n\t\t});\n\t\tif (!endpoint) {\n\t\t\tconst error = new Error(\"Invalid multicast endpoint configuration\");\n\t\t\tthis.onError?.(error);\n\t\t\treturn false;\n\t\t}\n\t\tconfig = {\n\t\t\t...config,\n\t\t\tgroup: endpoint.groupAddress,\n\t\t\tsource: endpoint.sourceAddress ?? \"\",\n\t\t\tmode: endpoint.mode,\n\t\t\tamtRelay: endpoint.amtRelay,\n\t\t\tamtRelayPort: endpoint.amtRelayPort,\n\t\t};\n\t\tconst caps = await this.detectCapabilities();\n\t\tif (generation !== this.connectionGeneration) return false;\n\n\t\t// ABR adapts over the caller's ladder (the catalog's renditions),\n\t\t// validated above before anything was torn down.\n\t\tthis.abr = abr;\n\n\t\tabr.setCallback((quality) => {\n\t\t\tthis.state.quality = quality;\n\t\t\tthis.subscription?.onqualitychange?.(quality);\n\t\t\tthis.onStateChange?.(this.state);\n\t\t});\n\n\t\t// Try transport hierarchy\n\t\tlet connected = false;\n\n\t\t// 1. Try the canonical browser multicast gateway.\n\t\tif (caps.nativeSSM || caps.nativeIGMP) {\n\t\t\tconsole.log(\"[Transport] Attempting window.multicast...\");\n\t\t\tconnected = await this.connectWindowMulticast(config, generation);\n\t\t\tif (connected) {\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\t\t\t\tthis.state.mode = \"multicast\";\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// 2. Try AMT proxy (with optional DRIAD discovery)\n\t\tif (!connected) {\n\t\t\tconsole.log(\"[Transport] Attempting AMT tunnel...\");\n\t\t\tconnected = await this.connectAmt(config, generation);\n\t\t\tif (connected) {\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\t\t\t\tthis.state.mode = \"amt\";\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback to MOQ\n\t\tconsole.log(\"[Transport] All multicast transports failed, falling back to MOQ\");\n\t\treturn false;\n\t}\n\n\t/**\n\t * Connect via the lowercase window.multicast bytes gateway.\n\t */\n\tprivate async connectWindowMulticast(config: TransportConfig, generation: number): Promise<boolean> {\n\t\ttry {\n\t\t\tconst gateway = this.getWindowMulticastGateway();\n\t\t\tif (typeof gateway?.subscribe !== \"function\") return false;\n\n\t\t\tconst lease = await multicastSubscriptionPool.acquire(gateway, config, {\n\t\t\t\tonBytes: (data) => {\n\t\t\t\t\tif (generation === this.connectionGeneration) this.handlePacket(data);\n\t\t\t\t},\n\t\t\t\tonState: (state: WindowMulticastSubscriptionState) => {\n\t\t\t\t\tif (generation !== this.connectionGeneration) return;\n\t\t\t\t\tthis.state.connected = state !== \"closed\";\n\t\t\t\t\tthis.onStateChange?.(this.state);\n\t\t\t\t},\n\t\t\t\tonError: (error) => {\n\t\t\t\t\tif (generation !== this.connectionGeneration) return;\n\t\t\t\t\tconsole.error(\"[Transport] window.multicast error:\", error);\n\t\t\t\t\tthis.onError?.(error);\n\t\t\t\t},\n\t\t\t});\n\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\tawait lease.release();\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tthis.subscription = {\n\t\t\t\tid: lease.key,\n\t\t\t\tconfig,\n\t\t\t\tclose: () => { void lease.release(); },\n\t\t\t};\n\n\t\t\tthis.state = {\n\t\t\t\tmode: \"multicast\",\n\t\t\t\tconnected: true,\n\t\t\t\tgroup: config.group,\n\t\t\t\tsource: config.source,\n\t\t\t\tquality: this.requireAbr().getCurrentQuality(),\n\t\t\t\tpacketsReceived: 0,\n\t\t\t\tbytesReceived: 0,\n\t\t\t\tlastPacketTime: 0,\n\t\t\t};\n\n\t\t\tconsole.log(`[Transport] Connected via window.multicast: (${config.source}, ${config.group})`);\n\t\t\tthis.onStateChange?.(this.state);\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] window.multicast failed:\", error);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Connect via AMT proxy (with optional DRIAD discovery)\n\t */\n\tprivate async connectAmt(config: TransportConfig, generation: number): Promise<boolean> {\n\t\ttry {\n\t\t\tlet amtRelay = config.amtRelay;\n\n\t\t\t// Use DRIAD to discover relay if not specified (RFC 8777)\n\t\t\t// DRIAD discovers relays based on SOURCE address, not group\n\t\t\tif (!amtRelay && config.driadEnabled !== false && config.source) {\n\t\t\t\tconsole.log(`[Transport] Discovering AMT relay via DRIAD for source ${config.source}...`);\n\t\t\t\tconst relays = await this.driad.discoverRelays(config.source);\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\t\t\t\tconst bestRelay = await this.driad.selectBestRelay(relays);\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\n\t\t\t\tif (bestRelay) {\n\t\t\t\t\tamtRelay = bestRelay.host;\n\t\t\t\t\tthis.state.amtRelay = amtRelay;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!amtRelay) {\n\t\t\t\tconsole.warn(\"[Transport] No AMT relay available (DRIAD requires source address for SSM)\");\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Use the WebSocket-based AMT proxy when a relay is known.\n\t\t\treturn await this.connectAmtProxy(config, amtRelay, generation);\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] AMT connection failed:\", error);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Connect via WebSocket-based AMT proxy (browser fallback)\n\t */\n\tprivate async connectAmtProxy(config: TransportConfig, amtRelay: string, generation: number): Promise<boolean> {\n\t\ttry {\n\t\t\tconst relayHost = amtRelay.includes(\":\") && !amtRelay.startsWith(\"[\") ? `[${amtRelay}]` : amtRelay;\n\t\t\t// amtRelayPort is the relay's RFC 7450 UDP port, not the proxy's HTTPS port.\n\t\t\tconst wsUrl = new URL(`wss://${relayHost}/amt-proxy`);\n\t\t\twsUrl.searchParams.set(\"group\", config.group);\n\t\t\twsUrl.searchParams.set(\"source\", config.source || \"\");\n\t\t\twsUrl.searchParams.set(\"port\", String(config.port));\n\n\t\t\tconst ws = new WebSocket(wsUrl);\n\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tws.binaryType = \"arraybuffer\";\n\t\t\t\tconst timeout = setTimeout(() => {\n\t\t\t\t\tws.close();\n\t\t\t\t\treject(new Error(\"AMT proxy connection timeout\"));\n\t\t\t\t}, 10000);\n\n\t\t\t\tws.onopen = () => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\t\t\tws.close();\n\t\t\t\t\t\tresolve(false);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconsole.log(`[Transport] Connected to AMT proxy: ${amtRelay}`);\n\n\t\t\t\t\tthis.subscription = {\n\t\t\t\t\t\tid: `amt-proxy-${Date.now()}`,\n\t\t\t\t\t\tconfig,\n\t\t\t\t\t\tclose: () => ws.close(),\n\t\t\t\t\t};\n\n\t\t\t\t\tthis.state = {\n\t\t\t\t\t\tmode: \"amt\",\n\t\t\t\t\t\tconnected: true,\n\t\t\t\t\t\tgroup: config.group,\n\t\t\t\t\t\tsource: config.source,\n\t\t\t\t\t\tamtRelay,\n\t\t\t\t\t\tquality: this.requireAbr().getCurrentQuality(),\n\t\t\t\t\t\tpacketsReceived: 0,\n\t\t\t\t\t\tbytesReceived: 0,\n\t\t\t\t\t\tlastPacketTime: 0,\n\t\t\t\t\t};\n\n\t\t\t\t\tthis.onStateChange?.(this.state);\n\t\t\t\t\tresolve(true);\n\t\t\t\t};\n\n\t\t\t\tws.onmessage = (event) => {\n\t\t\t\t\tif (generation === this.connectionGeneration && event.data instanceof ArrayBuffer) {\n\t\t\t\t\t\tthis.handlePacket(event.data);\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tws.onerror = (error) => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\t\t\tresolve(false);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconsole.error(\"[Transport] AMT proxy error:\", error);\n\t\t\t\t\treject(error);\n\t\t\t\t};\n\n\t\t\t\tws.onclose = () => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\t\t\tresolve(false);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.state.connected = false;\n\t\t\t\t\tthis.onStateChange?.(this.state);\n\t\t\t\t\tresolve(false);\n\t\t\t\t};\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] AMT proxy failed:\", error);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Handle received packet\n\t */\n\tprivate handlePacket(data: ArrayBuffer) {\n\t\tthis.state.packetsReceived++;\n\t\tthis.state.bytesReceived += data.byteLength;\n\t\tthis.state.lastPacketTime = Date.now();\n\n\t\t// Update ABR with sample\n\t\t// Packets reach here only through a connect()ed join, which built the ABR.\n\t\tthis.abr?.recordSample(data.byteLength, 0);\n\n\t\t// Forward to callback\n\t\tthis.onPacket?.(data);\n\t}\n\n\t/**\n\t * Get current transport state\n\t */\n\tgetState(): TransportState {\n\t\treturn { ...this.state };\n\t}\n\n\t/**\n\t * The rung of the live connection's `abrLadder` ABR is on; `null` when\n\t * there is no ladder (before `connect(config)` succeeds, after\n\t * `disconnect()`, or after a reconnect that was torn down and bailed).\n\t */\n\tgetQuality(): QualityLevel | null {\n\t\treturn this.abr?.getCurrentQuality() ?? null;\n\t}\n\n\t/**\n\t * Force the rung named `name` of the live connection's `abrLadder`.\n\t * Throws `Error` when there is no ladder (see `getQuality()`), and\n\t * `RangeError` when the ladder has no rung of that name; it never falls\n\t * back to another rung.\n\t */\n\tsetQuality(name: string) {\n\t\tthis.requireAbr().setQuality(name);\n\t}\n\n\t/** The controller `connect()` built; before it there is no ladder to adapt over. */\n\tprivate requireAbr(): AdaptiveBitrateController {\n\t\tif (!this.abr) throw new Error(\"[Transport] ABR has no ladder before connect(config)\");\n\t\treturn this.abr;\n\t}\n\n\t/** Configures multicast-only ABR while leaving the caller's MoQ ABR untouched. */\n\tconfigureMulticastTiers(\n\t\toptions: OrchestratorOptions,\n\t\tonCommand?: (command: OrchestratorCommand) => void,\n\t): MulticastTierEndpoint | undefined {\n\t\tthis.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());\n\t\tthis.multicastOrchestrator = new MulticastOrchestrator(options);\n\t\tthis.onOrchestratorCommand = onCommand;\n\t\treturn this.multicastOrchestrator.start();\n\t}\n\n\trecordMulticastGoodput(bitrate: number | undefined): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onGoodput({ bitrate }));\n\t}\n\n\trecordMulticastHeadroom(healthy: boolean): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onHeadroom({ healthy }));\n\t}\n\n\trecordMulticastTierPacket(packet: MulticastTierPacket): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onTierPacket(packet));\n\t}\n\n\tpollMulticastTierSwitch(): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.poll());\n\t}\n\n\t/**\n\t * Set packet callback\n\t */\n\tsetPacketCallback(callback: (data: ArrayBuffer) => void) {\n\t\tthis.onPacket = callback;\n\t}\n\n\t/**\n\t * Set error callback\n\t */\n\tsetErrorCallback(callback: (error: Error) => void) {\n\t\tthis.onError = callback;\n\t}\n\n\t/**\n\t * Set state change callback\n\t */\n\tsetStateCallback(callback: (state: TransportState) => void) {\n\t\tthis.onStateChange = callback;\n\t}\n\n\t/**\n\t * Disconnect\n\t */\n\tdisconnect() {\n\t\tthis.connectionGeneration++;\n\t\tthis.subscription?.close();\n\t\tthis.subscription = null;\n\t\t// No ladder after a disconnect, as before the first connect():\n\t\t// getQuality() is null and setQuality() throws until connect(config).\n\t\tthis.abr = null;\n\t\tthis.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());\n\t\tthis.multicastOrchestrator = null;\n\t\tthis.onOrchestratorCommand = undefined;\n\t\tthis.state = {\n\t\t\tmode: \"disconnected\",\n\t\t\tconnected: false,\n\t\t\tpacketsReceived: 0,\n\t\t\tbytesReceived: 0,\n\t\t\tlastPacketTime: 0,\n\t\t};\n\t\tthis.onStateChange?.(this.state);\n\t}\n\n\tprivate dispatchOrchestratorCommand(command: OrchestratorCommand | undefined): OrchestratorCommand | undefined {\n\t\tif (command) this.onOrchestratorCommand?.(command);\n\t\treturn command;\n\t}\n}\n\n// Export singleton instance\nexport const transportManager = new TransportManager();\n", "const MAX_MOQT_PRIORITY = 0xff;\n\ndeclare const subscriberPriorityBrand: unique symbol;\ndeclare const publisherPriorityBrand: unique symbol;\n\nexport type MoqtSubscriberPriority = number & { readonly [subscriberPriorityBrand]: true };\nexport type MoqtPublisherPriority = number & { readonly [publisherPriorityBrand]: true };\n\nfunction moqtPriority(value: number, field: string): number {\n\tif (!Number.isInteger(value) || value < 0 || value > MAX_MOQT_PRIORITY) {\n\t\tthrow new RangeError(`${field} must be an integer in [0, ${MAX_MOQT_PRIORITY}], got ${value}`);\n\t}\n\treturn value;\n}\n\n/** Validate a protocol-native MOQT subscriber priority without transforming it. */\nexport function moqtSubscriberPriority(value: number): MoqtSubscriberPriority {\n\treturn moqtPriority(value, \"MOQT subscriber priority\") as MoqtSubscriberPriority;\n}\n\n/** Validate a protocol-native MOQT publisher priority without transforming it. */\nexport function moqtPublisherPriority(value: number): MoqtPublisherPriority {\n\treturn moqtPriority(value, \"MOQT publisher priority\") as MoqtPublisherPriority;\n}\n\n/** Shared media subscription policy on MOQT's native lower-number-wins scale. */\nexport const MOQT_SUBSCRIBER_PRIORITY = Object.freeze({\n\tcatalog: moqtSubscriberPriority(0),\n\t/**\n\t * @deprecated A repair FETCH's SUBSCRIBER_PRIORITY is derived per request\n\t * from the catalog source and repair priorities (draft-ramadan-moq-fec\n\t * \u00A711.4.5), not a player constant. Use the `subscriberPriority` that\n\t * `@blockcast/mmt-container`'s `wireRepairFetch` puts on each\n\t * `RepairFetchRequest` (`repairFetchSubscriberPriority`).\n\t */\n\tdeadlineRepair: moqtSubscriberPriority(0),\n\taudio: moqtSubscriberPriority(1),\n\tvideo: moqtSubscriberPriority(2),\n\t/**\n\t * @deprecated See `deadlineRepair`: use the request's catalog-derived\n\t * `RepairFetchRequest.subscriberPriority` (draft-ramadan-moq-fec \u00A711.4.5).\n\t */\n\tbackgroundRepair: moqtSubscriberPriority(3),\n});\n\n/** Publisher priority bands from draft-ramadan-moq-fec section 10. */\nexport const MOQT_PUBLISHER_PRIORITY = Object.freeze({\n\tcatalog: moqtPublisherPriority(32),\n\tsource: moqtPublisherPriority(128),\n\trepair: moqtPublisherPriority(240),\n});\n", "/**\n * @blockcast/transport - AMT Gateway\n *\n * Consolidated AMT (Automatic Multicast Tunneling) implementation\n * supporting both Service Worker and Direct Sockets approaches.\n *\n * RFC 7450: Automatic Multicast Tunneling\n */\n\nimport type { GroupKey, PacketMetadata, AmtRelayInfo } from \"./types.js\";\nimport { DRIADDiscovery } from \"./driad-discovery.js\";\nimport { DEFAULT_AMT_RELAY_PORT } from \"@blockcast/mmt-base\";\n\n/** AMT message types */\nconst AMT_MESSAGE_TYPES = {\n\tRELAY_DISCOVERY: 1,\n\tRELAY_ADVERTISEMENT: 2,\n\tREQUEST: 3,\n\tMEMBERSHIP_QUERY: 4,\n\tMEMBERSHIP_UPDATE: 5,\n\tMULTICAST_DATA: 6,\n\tTEARDOWN: 7,\n} as const;\n\n/** AMT gateway states */\nconst GATEWAY_STATES = {\n\tIDLE: \"idle\",\n\tDISCOVERING: \"discovering\",\n\tREQUESTING: \"requesting\",\n\tQUERYING: \"querying\",\n\tACTIVE: \"active\",\n\tLEAVING: \"leaving\",\n\tCLOSED: \"closed\",\n} as const;\n\ntype GatewayState = (typeof GATEWAY_STATES)[keyof typeof GATEWAY_STATES];\n\n/** AMT subscription configuration */\nexport interface AMTSubscriptionConfig {\n\tkey: GroupKey;\n\trelayAddress?: string;\n\trelayPort?: number;\n\tonPacket: (data: ArrayBuffer, metadata: PacketMetadata) => void;\n\tonError: (error: Error) => void;\n}\n\n/** AMT packet info */\nexport interface AMTPacketInfo {\n\tdata: Uint8Array;\n\tsrcAddress: string;\n\tdstAddress: string;\n\tsrcPort: number;\n\tdstPort: number;\n\tgroupKey: string;\n\ttimestamp: number;\n}\n\n/** AMT Gateway options */\nexport interface AMTGatewayOptions {\n\trelayAddress: string;\n\trelayPort?: number;\n\tonPacket?: (packet: AMTPacketInfo) => void;\n\tonStateChange?: (newState: GatewayState, oldState: GatewayState) => void;\n\tonError?: (error: Error) => void;\n\tonLog?: (message: string) => void;\n}\n\n/** Active AMT subscription */\ninterface AMTSubscription {\n\tkey: GroupKey;\n\tsubscriptionId: string;\n\tconfig: AMTSubscriptionConfig;\n\trelay?: AmtRelayInfo;\n\thandshakeComplete: boolean;\n\tkeepAliveTimer?: ReturnType<typeof setInterval>;\n\tlastKeepAlive?: number;\n}\n\n/** Check if address is IPv4 */\nfunction isIPv4(address: string): boolean {\n\treturn /^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(address);\n}\n\n/**\n * AMT Gateway Manager (Service Worker based)\n *\n * Manages AMT tunnel subscriptions using Service Worker for UDP communication.\n * Handles DRIAD discovery, AMT handshake, and packet decapsulation.\n */\nexport class AMTGatewayManager {\n\tprivate subscriptions = new Map<string, AMTSubscription>();\n\tprivate messageHandler?: (event: MessageEvent) => void;\n\tprivate pendingPackets = new Map<string, Array<{ data: ArrayBuffer; remoteAddress: string; remotePort: number }>>();\n\tprivate driad = new DRIADDiscovery();\n\n\tconstructor() {\n\t\t// Set up message handler for packets from service worker\n\t\tthis.messageHandler = (event: MessageEvent) => {\n\t\t\tconst data = event.data as { type: string; subscriptionId?: string; data?: unknown; error?: string; message?: string };\n\t\t\tconsole.log(`[AMT] SW message received:`, data.type);\n\t\t\tif (data.type === \"UDP_PACKET\") {\n\t\t\t\tthis.handlePacket(data);\n\t\t\t} else if (data.type === \"SOCKET_ERROR\") {\n\t\t\t\tthis.handleSocketError(data);\n\t\t\t} else if (data.type === \"DEBUG_LOG\") {\n\t\t\t\tconsole.log(`[AMT SW Debug] ${data.message}`);\n\t\t\t}\n\t\t};\n\n\t\t// Register message handler\n\t\tif (typeof navigator !== \"undefined\" && navigator.serviceWorker) {\n\t\t\tnavigator.serviceWorker.addEventListener(\"message\", this.messageHandler);\n\t\t\tconsole.log(\"[AMT] Registered service worker message handler\");\n\t\t} else {\n\t\t\tconsole.warn(\"[AMT] Service worker not available\");\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe via AMT tunnel\n\t */\n\tasync subscribe(config: AMTSubscriptionConfig): Promise<string> {\n\t\tconst { key } = config;\n\t\tconst keyStr = this.getKeyString(key);\n\n\t\tif (this.subscriptions.has(keyStr)) {\n\t\t\tconsole.log(`[AMT] Reusing existing subscription to ${keyStr}`);\n\t\t\treturn keyStr;\n\t\t}\n\n\t\ttry {\n\t\t\tlet relayAddress: string;\n\t\t\tlet discoveryMethod: \"driad\" | \"manual\";\n\t\t\tconst relayPort = config.relayPort || DEFAULT_AMT_RELAY_PORT;\n\n\t\t\tif (config.relayAddress) {\n\t\t\t\trelayAddress = config.relayAddress;\n\t\t\t\tdiscoveryMethod = \"manual\";\n\t\t\t\tconsole.log(`[AMT] Using configured relay ${relayAddress}:${relayPort}`);\n\t\t\t} else if (key.source) {\n\t\t\t\t// DRIAD (RFC 8777) discovers relays based on SOURCE address, not group\n\t\t\t\tconsole.log(`[AMT] No relay configured, discovering via DRIAD for source ${key.source}...`);\n\t\t\t\tconst relays = await this.driad.discoverRelays(key.source);\n\t\t\t\tconst bestRelay = await this.driad.selectBestRelay(relays);\n\n\t\t\t\tif (!bestRelay) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`AMT relay discovery failed for source ${key.source}. ` +\n\t\t\t\t\t\t\t`Please specify a relay address in the subscription configuration.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\trelayAddress = bestRelay.host;\n\t\t\t\tdiscoveryMethod = \"driad\";\n\t\t\t\tconsole.log(`[AMT] Discovered relay via DRIAD: ${relayAddress}:${relayPort}`);\n\t\t\t} else {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`AMT relay discovery requires a source address for SSM (RFC 8777). ` +\n\t\t\t\t\t\t`Please specify either a relay address or source address.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst subscriptionId = keyStr;\n\n\t\t\t// Initialize packet queue for handshake\n\t\t\tthis.pendingPackets.set(subscriptionId, []);\n\n\t\t\t// Create subscription\n\t\t\tconst subscription: AMTSubscription = {\n\t\t\t\tkey,\n\t\t\t\tsubscriptionId,\n\t\t\t\tconfig,\n\t\t\t\thandshakeComplete: false,\n\t\t\t\trelay: {\n\t\t\t\t\taddress: relayAddress,\n\t\t\t\t\tport: relayPort,\n\t\t\t\t\tdiscoveryMethod,\n\t\t\t\t\tdiscoveredAt: Date.now(),\n\t\t\t\t},\n\t\t\t};\n\n\t\t\tthis.subscriptions.set(keyStr, subscription);\n\n\t\t\t// Note: Actual UDP socket creation and handshake would require\n\t\t\t// Service Worker communication - this is a simplified version\n\t\t\tconsole.log(`[AMT] Subscription created for ${keyStr} via ${relayAddress}:${relayPort}`);\n\n\t\t\treturn keyStr;\n\t\t} catch (error) {\n\t\t\tconst subscription = this.subscriptions.get(keyStr);\n\t\t\tif (subscription) {\n\t\t\t\tthis.subscriptions.delete(keyStr);\n\t\t\t\tthis.pendingPackets.delete(subscription.subscriptionId);\n\t\t\t}\n\n\t\t\tthrow new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\n\t/**\n\t * Unsubscribe from AMT tunnel\n\t */\n\tasync unsubscribe(handle: string): Promise<void> {\n\t\tconst subscription = this.subscriptions.get(handle);\n\t\tif (!subscription) {\n\t\t\tthrow new Error(`Subscription ${handle} not found`);\n\t\t}\n\n\t\ttry {\n\t\t\t// Stop keep-alive timer\n\t\t\tif (subscription.keepAliveTimer) {\n\t\t\t\tclearInterval(subscription.keepAliveTimer);\n\t\t\t}\n\n\t\t\tconsole.log(`[AMT] Unsubscribed from ${handle}`);\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[AMT] Error during unsubscribe:\", error);\n\t\t} finally {\n\t\t\tthis.subscriptions.delete(handle);\n\t\t\tthis.pendingPackets.delete(subscription.subscriptionId);\n\t\t}\n\t}\n\n\t/**\n\t * Get list of active subscriptions\n\t */\n\tgetSubscriptions(): string[] {\n\t\treturn Array.from(this.subscriptions.keys());\n\t}\n\n\t/**\n\t * Handle packet from service worker\n\t */\n\tprivate handlePacket(message: { subscriptionId?: string; data?: unknown }): void {\n\t\tconst { subscriptionId, data } = message;\n\t\tif (!subscriptionId) return;\n\n\t\tconst subscription = this.subscriptions.get(subscriptionId);\n\t\tif (!subscription) {\n\t\t\tconsole.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);\n\t\t\treturn;\n\t\t}\n\n\t\t// During handshake, queue packets\n\t\tif (!subscription.handshakeComplete) {\n\t\t\tconst queue = this.pendingPackets.get(subscriptionId);\n\t\t\tconst packetData = data as { data: ArrayBuffer; remoteAddress: string; remotePort: number };\n\t\t\tif (queue) {\n\t\t\t\tqueue.push({\n\t\t\t\t\tdata: packetData.data,\n\t\t\t\t\tremoteAddress: packetData.remoteAddress,\n\t\t\t\t\tremotePort: packetData.remotePort,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// After handshake, forward to callback\n\t\tconst packetData = data as { data: ArrayBuffer };\n\t\tconst metadata: PacketMetadata = {\n\t\t\tsourceAddress: subscription.relay?.address || \"unknown\",\n\t\t\tsourcePort: subscription.relay?.port || 0,\n\t\t\ttransport: \"tunnel\",\n\t\t\ttimestamp: performance.now(),\n\t\t};\n\n\t\tsubscription.config.onPacket(packetData.data, metadata);\n\t}\n\n\t/**\n\t * Handle socket error from service worker\n\t */\n\tprivate handleSocketError(message: { subscriptionId?: string; error?: string }): void {\n\t\tconst { subscriptionId, error } = message;\n\t\tif (!subscriptionId) return;\n\n\t\tconst subscription = this.subscriptions.get(subscriptionId);\n\t\tif (!subscription) {\n\t\t\tconsole.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);\n\t\t\treturn;\n\t\t}\n\n\t\tsubscription.config.onError(new Error(error || \"Unknown error\"));\n\t}\n\n\t/**\n\t * Cleanup on destroy\n\t */\n\tdestroy(): void {\n\t\tif (this.messageHandler && typeof navigator !== \"undefined\" && navigator.serviceWorker) {\n\t\t\tnavigator.serviceWorker.removeEventListener(\"message\", this.messageHandler);\n\t\t}\n\n\t\tconst handles = Array.from(this.subscriptions.keys());\n\t\tfor (const handle of handles) {\n\t\t\tthis.unsubscribe(handle).catch((err) => {\n\t\t\t\tconsole.error(`[AMT] Error unsubscribing ${handle}:`, err);\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Get key string for HashMap\n\t */\n\tprivate getKeyString(key: GroupKey): string {\n\t\tconst source = key.source ? `@${key.source}` : \"\";\n\t\treturn `${key.group}:${key.port}${source}`;\n\t}\n}\n\n/**\n * AMT Gateway Client (Direct Sockets based)\n *\n * Connects to an AMT relay to receive multicast data via unicast tunnel.\n * Implements the gateway side of RFC 7450.\n * Supports multiple (S,G) groups over a single AMT tunnel.\n */\nexport class AMTGateway {\n\tprivate relayAddress: string;\n\tprivate relayPort: number;\n\tprivate groups = new Map<\n\t\tstring,\n\t\t{\n\t\t\tgroupPort: number;\n\t\t\tonPacket: ((packet: AMTPacketInfo) => void) | null;\n\t\t\tjoinedAt: number;\n\t\t}\n\t>();\n\n\t// Callbacks\n\tprivate onPacket: (packet: AMTPacketInfo) => void;\n\tprivate onStateChange: (newState: GatewayState, oldState: GatewayState) => void;\n\tprivate onError: (error: Error) => void;\n\tprivate onLog: (message: string) => void;\n\n\t// State\n\tprivate state: GatewayState = GATEWAY_STATES.IDLE;\n\tprivate nonce: Uint8Array | null = null;\n\tprivate responseMAC: Uint8Array | null = null;\n\tprivate relayConfirmedAddress: string | null = null;\n\tprivate lastDataTime = 0;\n\n\t// Stats\n\tprivate stats = {\n\t\tpacketsReceived: 0,\n\t\tbytesReceived: 0,\n\t\tqueriesReceived: 0,\n\t\tupdatesSet: 0,\n\t};\n\n\tconstructor(options: AMTGatewayOptions) {\n\t\tthis.relayAddress = options.relayAddress;\n\t\tthis.relayPort = options.relayPort || DEFAULT_AMT_RELAY_PORT;\n\n\t\tthis.onPacket = options.onPacket || (() => {});\n\t\tthis.onStateChange = options.onStateChange || (() => {});\n\t\tthis.onError = options.onError || ((err) => console.error(\"AMT Error:\", err));\n\t\tthis.onLog = options.onLog || ((msg) => console.log(\"AMT:\", msg));\n\t}\n\n\t/**\n\t * Generate random nonce\n\t */\n\tprivate generateNonce(): Uint8Array {\n\t\tconst nonce = new Uint8Array(4);\n\t\tcrypto.getRandomValues(nonce);\n\t\treturn nonce;\n\t}\n\n\t/**\n\t * Set gateway state\n\t */\n\tprivate setState(newState: GatewayState): void {\n\t\tconst oldState = this.state;\n\t\tthis.state = newState;\n\t\tthis.onLog(`State: ${oldState} -> ${newState}`);\n\t\tthis.onStateChange(newState, oldState);\n\t}\n\n\t/**\n\t * Join a multicast group\n\t */\n\tasync joinGroup(\n\t\tgroupAddress: string,\n\t\tgroupPort: number,\n\t\tsourceAddress: string | null = null,\n\t\tonPacket: ((packet: AMTPacketInfo) => void) | null = null,\n\t): Promise<string> {\n\t\tconst groupKey = `${sourceAddress || \"*\"},${groupAddress}`;\n\n\t\tif (this.groups.has(groupKey)) {\n\t\t\tthis.onLog(`Group already joined: ${groupKey}`);\n\t\t\treturn groupKey;\n\t\t}\n\n\t\tthis.groups.set(groupKey, {\n\t\t\tgroupPort,\n\t\t\tonPacket,\n\t\t\tjoinedAt: Date.now(),\n\t\t});\n\n\t\tthis.onLog(`Joined group: ${groupKey} (port ${groupPort})`);\n\t\treturn groupKey;\n\t}\n\n\t/**\n\t * Leave a multicast group\n\t */\n\tasync leaveGroup(groupAddress: string, sourceAddress: string | null = null): Promise<boolean> {\n\t\tconst groupKey = `${sourceAddress || \"*\"},${groupAddress}`;\n\n\t\tif (!this.groups.has(groupKey)) {\n\t\t\tthis.onLog(`Group not found: ${groupKey}`);\n\t\t\treturn false;\n\t\t}\n\n\t\tthis.groups.delete(groupKey);\n\t\tthis.onLog(`Left group: ${groupKey} (${this.groups.size} remaining)`);\n\n\t\tif (this.groups.size === 0) {\n\t\t\tthis.onLog(\"No groups remaining - closing AMT tunnel\");\n\t\t\tawait this.close();\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Get list of active groups\n\t */\n\tgetGroups(): Array<{\n\t\tgroupKey: string;\n\t\tgroupAddress: string;\n\t\tsourceAddress: string | null;\n\t\tgroupPort: number;\n\t\tjoinedAt: number;\n\t}> {\n\t\treturn Array.from(this.groups.entries()).map(([key, info]) => {\n\t\t\tconst [source, group] = key.split(\",\");\n\t\t\treturn {\n\t\t\t\tgroupKey: key,\n\t\t\t\tgroupAddress: group,\n\t\t\t\tsourceAddress: source !== \"*\" ? source : null,\n\t\t\t\tgroupPort: info.groupPort,\n\t\t\t\tjoinedAt: info.joinedAt,\n\t\t\t};\n\t\t});\n\t}\n\n\t/**\n\t * Open connection to AMT relay\n\t */\n\tasync open(): Promise<void> {\n\t\tif (this.state !== GATEWAY_STATES.IDLE) {\n\t\t\tthrow new Error(`Cannot open in state: ${this.state}`);\n\t\t}\n\n\t\tthis.setState(GATEWAY_STATES.DISCOVERING);\n\t\tthis.onLog(`Connecting to AMT relay ${this.relayAddress}:${this.relayPort}`);\n\t\tthis.onLog(`Active groups: ${this.groups.size}`);\n\n\t\t// Generate session nonce\n\t\tthis.nonce = this.generateNonce();\n\t\tthis.onLog(`Generated nonce: ${Array.from(this.nonce).map((b) => b.toString(16).padStart(2, \"0\")).join(\"\")}`);\n\n\t\t// Note: Actual UDPSocket connection would go here\n\t\t// This is a simplified version showing the state machine\n\t}\n\n\t/**\n\t * Close connection\n\t */\n\tasync close(): Promise<void> {\n\t\tif (this.state === GATEWAY_STATES.CLOSED) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setState(GATEWAY_STATES.LEAVING);\n\n\t\t// Clear all groups\n\t\tthis.groups.clear();\n\n\t\tthis.setState(GATEWAY_STATES.CLOSED);\n\t\tthis.onLog(\"AMT Gateway closed\");\n\t}\n\n\t/**\n\t * Get current statistics\n\t */\n\tgetStats(): typeof this.stats {\n\t\treturn { ...this.stats };\n\t}\n\n\t/**\n\t * Get current state\n\t */\n\tgetState(): GatewayState {\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Check if gateway is active and receiving data\n\t */\n\tisActive(): boolean {\n\t\treturn this.state === GATEWAY_STATES.ACTIVE;\n\t}\n}\n\n// Export singleton manager instance\nexport const amtGatewayManager = new AMTGatewayManager();\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\n// Legacy detached per-packet authenticator suffix `[0xf0a1][len][auth]`. Its\n// producer was removed (BLO-16483) and nothing verifies it any more; provenance\n// is the signed per-group manifest. Receivers still skip an exact suffix so a\n// stale producer degrades to unauthenticated playback instead of dropped\n// packets. ponytail: hard-cut to reject once no deployed producer can emit it.\nconst LEGACY_TRAILER_EXT_TYPE = 0xf0a1\nconst LEGACY_TRAILER_HEADER_SIZE = 4\n\nfunction isEmptyOrExactLegacyTrailer(payload: Uint8Array, start: number, end: number): boolean {\n if (start === end) return true\n if (start < 0 || end > payload.length || start + LEGACY_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) !== LEGACY_TRAILER_EXT_TYPE) return false\n\n const authLength = view.getUint16(2, false)\n return start + LEGACY_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 isEmptyOrExactLegacyTrailer(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 || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {\n return null\n }\n return { hasFecId: true }\n }\n if (fecType !== 0) return null\n\n if (isEmptyOrExactLegacyTrailer(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. The legacy\n * authenticator suffix and the Source FEC trailer 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 legacy authenticator 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/** 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 its block\n * profile in `fec.blockProfile`. A receiver applies all of a profile's rules\n * or none of them, so the token is the one explicit per-stream discriminator:\n * it is never defaulted and never inferred from the wire.\n *\n * draft-ramadan-moq-fec \u00A75.1 defines exactly one value, and it is the only\n * profile this library implements:\n *\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 *\n * A track whose `blockProfile` is absent or names any other token (the\n * retired Blockcast-private `k-padded-v1` included) has no FEC: a receiver\n * consumes its source without FEC and joins none of its repair tracks\n * (draft-ramadan-moq-fec \u00A75.1). `parseFecBlockProfile` throws for such a\n * token; the per-track catalog resolver turns the same rule into a typed\n * per-track \"FEC unavailable\" outcome instead of failing the session.\n */\nimport { serial32Add, serial32Distance, serial32Precedes } from './serial-number.js'\n\n/** The block profile a catalog `fec.blockProfile` may name: the one this library implements. */\nexport type FecBlockProfile = 'iso-ssbg1-v1'\n\n/** Every accepted `fec.blockProfile` token. */\nexport const FEC_BLOCK_PROFILES: readonly FecBlockProfile[] = Object.freeze([\n 'iso-ssbg1-v1',\n])\n\n/** True for a block-profile token this library implements. */\nexport function isFecBlockProfile(value: unknown): value is FecBlockProfile {\n return value === 'iso-ssbg1-v1'\n}\n\n/**\n * Read a required `fec.blockProfile`. Throws on an absent token and on any\n * token other than `iso-ssbg1-v1`: there is no default and nothing is\n * inferred from the wire (draft-ramadan-moq-fec \u00A75.1).\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(`${where} is required: name the FEC block profile (\"iso-ssbg1-v1\")`)\n }\n throw new Error(`${where} must be \"iso-ssbg1-v1\" (got ${JSON.stringify(value)})`)\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", "/**\n * Thin transport wrapper for SSM multicast \u2014 join, socket, watchdog,\n * takeover/failback. Delegates raw packets to a callback (MmtpRouter\n * or any direct consumer).\n */\n\nimport {\n isMulticastTrackPacketId,\n subscriptionDeliversPacketBatches,\n toWindowMulticastSubscribeConfig,\n type WindowMulticastDelivery,\n type WindowMulticastGateway,\n type WindowMulticastSubscription,\n} from '@blockcast/mmt-transport'\nimport {\n MMTP_PACKET_ID_MAX,\n MMTP_SIGNALING_PACKET_ID,\n readMMTPPacketId,\n} from '@blockcast/mmt-base'\n\nexport interface MulticastTransportConfig {\n sourceAddress: string\n groupAddress: string\n port: number\n mode?: 'auto' | 'native' | 'tunnel'\n amtRelay?: string\n amtRelayPort?: number\n packetIds?: number[]\n tracks?: { name: string; packetId: number }[]\n}\n\nexport type ResolvedMulticastTransport = 'native' | 'amt'\n\n/**\n * Why a joined transport handed control back to MoQ.\n *\n * - `silence`: the data plane was live (at least one accepted packet since the\n * join) and then stayed silent for the catalog-derived failback window.\n * - `subscription-error`: the provider reported the subscription failed. This\n * is the provider's own explicit, typed outcome (a `MulticastError`, branch\n * on its `code`), e.g. a bridge whose AMT tunnel it declared dead.\n */\nexport type MulticastFailbackReason =\n | { readonly kind: 'silence'; readonly windowMs: number }\n | { readonly kind: 'subscription-error'; readonly error: unknown }\n\n/**\n * Why a join() resolved false, named by the step at which the join stopped,\n * never by what an error message says.\n *\n * - `disposed`: the transport was disposed before the join started, or while\n * the provider's subscribe() was pending.\n * - `unavailable`: there is no window.multicast gateway with a subscribe().\n * - `superseded`: a newer join() started while this join's subscribe() was\n * pending; the newer join owns the transport.\n * - `invalid-request`: the join config does not make a valid window.multicast\n * request (toWindowMulticastSubscribeConfig refused it), so the provider was\n * never asked.\n * - `provider-rejected`: the provider's subscribe() rejected or threw. The only\n * cause that carries the provider's own verdict (`providerError`).\n * - `invalid-subscription`: subscribe() resolved something that is not a usable\n * subscription: no event surface, a malformed `eventTypes`, an unknown\n * `delivery`, or an `addEventListener` that threw.\n * - `delivery-hook-failed`: the consumer's onDelivery hook threw for the\n * delivery the subscription declared, e.g. 'ordered-relay' to FEC tracks\n * configured without ordered geometry. A local configuration failure, not\n * the provider's.\n */\nexport type MulticastJoinDeclineReason =\n | 'disposed'\n | 'unavailable'\n | 'superseded'\n | 'invalid-request'\n | 'provider-rejected'\n | 'invalid-subscription'\n | 'delivery-hook-failed'\n\n/**\n * The typed part of a provider's subscribe() rejection. `name` and `code` are\n * the provider's own typed fields; the frozen multicast contract makes `code`\n * the one to branch on (e.g. a bridge's `amt_relay_refused`). `message` is\n * free text carried for humans only: never branch on it.\n */\nexport interface MulticastJoinProviderError {\n readonly name: string\n readonly message: string\n /** Present only when the provider's error carries a string `code`. */\n readonly code?: string\n}\n\n/**\n * How a join() resolved: joined, or the typed reason it resolved false.\n * `providerError` is present only for `provider-rejected`, and only when the\n * provider's rejection value carries a string `name` and `message`.\n */\nexport type MulticastJoinOutcome =\n | { readonly ok: true }\n | {\n readonly ok: false\n readonly reason: MulticastJoinDeclineReason\n readonly providerError?: MulticastJoinProviderError\n }\n\n/**\n * Receive counters plus the ingress-filter drop counters.\n *\n * `packetsReceived` / `bytesReceived` count only packets that passed the\n * allow-list and were handed to the page \u2014 a rejected packet must never\n * inflate them. The `dropped*` counters are the other half of that story:\n * without them a filtered PID is indistinguishable from a PID that was never\n * transmitted, which is exactly the ambiguity that makes an all-zero FEC\n * repair count unreadable (a repair leg dropped here and a repair leg the\n * publisher never sent produce byte-identical downstream telemetry).\n */\nexport interface MulticastTransportStats {\n packetsReceived: number\n bytesReceived: number\n /** Parsed cleanly, but the packet ID is absent from the join's allow-list. */\n packetsDroppedUnregisteredPid: number\n /** MMTP header did not parse; the packet ID is unknown, so it cannot be attributed. */\n packetsDroppedUnparseable: number\n /**\n * packet_id 0, the MMTP signaling flow. Expected traffic on a protected\n * session (moq_mmt sends the AL-FEC message on it), so it is counted apart\n * from the unregistered-PID fault counter and never takes a\n * `droppedPacketIds` slot.\n */\n packetsDroppedSignaling: number\n}\n\n/**\n * join() calls by how they resolved, cumulative for the life of the transport:\n * across rejoins, failed joins and dispose. `succeeded` plus every `declined`\n * count is the number of join() calls that resolved, superseded ones\n * included; a join() that rejects counts in none (see\n * MulticastTransport.lastJoinOutcome).\n */\nexport interface MulticastJoinStats {\n /** join() calls that resolved true. */\n readonly succeeded: number\n /** join() calls that resolved false, by the reason each one did. */\n readonly declined: Readonly<Record<MulticastJoinDeclineReason, number>>\n}\n\nconst JOIN_SUCCEEDED: MulticastJoinOutcome = Object.freeze({ ok: true })\n\n/**\n * The typed part of a provider's subscribe() rejection, or undefined when the\n * rejection value is not an error with a string `name` and `message`. Nothing\n * is filled in for a field the provider did not type, and the message is\n * carried, never read.\n */\nfunction providerErrorOf(error: unknown): MulticastJoinProviderError | undefined {\n if (typeof error !== 'object' || error === null) return undefined\n const fields = error as { readonly name?: unknown; readonly message?: unknown; readonly code?: unknown }\n let name: unknown\n let message: unknown\n let code: unknown\n try {\n name = fields.name\n message = fields.message\n code = fields.code\n } catch {\n // A rejection value whose fields throw on read (a revoked Proxy) types\n // nothing. Reading it must not turn a declined join into a rejected one.\n return undefined\n }\n if (typeof name !== 'string' || typeof message !== 'string') return undefined\n return Object.freeze(typeof code === 'string' ? { name, message, code } : { name, message })\n}\n\n/**\n * Distinct unregistered PIDs retained for diagnostics. A conformant catalog\n * advertises a handful of PIDs per endpoint, so this only has to be large\n * enough to name every leg of a mis-provisioned one; the cap exists so a\n * malformed or hostile stream cycling the 16-bit PID space cannot grow the map\n * without bound. Drops keep counting in aggregate after the cap.\n */\nconst MAX_TRACKED_DROPPED_PIDS = 16\n\ntype MulticastGlobals = typeof globalThis & {\n multicast?: WindowMulticastGateway\n}\n\nfunction getMulticastGlobals(): MulticastGlobals {\n return globalThis as MulticastGlobals\n}\n\nfunction hasMulticastGateway(): boolean {\n return typeof getMulticastGlobals().multicast?.subscribe === 'function'\n}\n\n/**\n * The packet one `bytes` event (or one element of a `packets` batch) carries,\n * as a view of the provider's own bytes. A Uint8Array is used as it is; the\n * bridge hands every packet over as one, so the common path builds nothing.\n */\nfunction normalizeBytesEventDetail(detail: unknown): Uint8Array | null {\n const value =\n detail && typeof detail === 'object' && 'data' in detail\n ? (detail as { data?: unknown }).data\n : detail\n if (value instanceof Uint8Array) return value\n if (value instanceof ArrayBuffer) return new Uint8Array(value)\n if (ArrayBuffer.isView(value)) {\n return new Uint8Array(value.buffer, value.byteOffset, value.byteLength)\n }\n return null\n}\n\n/**\n * A provider that can replace a live subscription's MMTP packet-ID filter\n * without rejoining its socket: the frozen multicast contract's\n * `Subscription.setPacketIds` (pim `packages/multicast-contract`). It is not\n * declared on every provider this transport meets, so it is detected, never\n * assumed.\n */\ninterface PacketFilterUpdater {\n setPacketIds(packetIds: readonly number[]): Promise<void> | void\n}\n\nfunction packetFilterUpdater(\n subscription: WindowMulticastSubscription,\n): PacketFilterUpdater['setPacketIds'] | undefined {\n const update = (subscription as Partial<PacketFilterUpdater>).setPacketIds\n return typeof update === 'function' ? update.bind(subscription) : undefined\n}\n\nfunction sameIds(a: ReadonlySet<number>, b: ReadonlySet<number>): boolean {\n if (a.size !== b.size) return false\n for (const id of a) if (!b.has(id)) return false\n return true\n}\n\n/**\n * The provider-side packet filter of one live join, and the in-place updates\n * sent to it. It lives exactly as long as the join: a later join or release\n * replaces it, and an acknowledgement for a replaced one is ignored.\n */\ninterface ProviderFilter {\n /**\n * The filter as the newest acknowledged update left it: the packet IDs the\n * provider forwards, or null when it forwards every ID. Until an update is\n * acknowledged it is the join's own filter.\n */\n acknowledged: ReadonlySet<number> | null\n /** Updates sent to the provider, numbered from 1 in send order. */\n sent: number\n /** The number of the newest acknowledged update; 0 for the join's own filter. */\n acknowledgedUpdate: number\n /** Updates sent and not yet settled. */\n inFlight: number\n}\n\n/** Which packet surface a join listens to, and its listener. */\ntype PacketListener =\n | { readonly type: 'bytes'; readonly listener: (event: CustomEvent<unknown>) => void }\n | { readonly type: 'packets'; readonly listener: (event: CustomEvent<readonly unknown[]>) => void }\n\nexport class MulticastTransport<TPacketContext = never> {\n #onPacket: (packet: Uint8Array, context?: TPacketContext) => void\n #subscription: WindowMulticastSubscription | null = null\n #packetListener: PacketListener | null = null\n #resolvedTransport: ResolvedMulticastTransport | undefined\n #videoConfirmed = false\n #audioConfirmed = false\n // One armed no-packets timer per silence window (BLO-36599). Packets only\n // record #lastPacketAtMs; the timer re-arms for the remainder when it wakes\n // inside the window, instead of a clear+set pair on every packet.\n #watchdog: ReturnType<typeof setTimeout> | null = null\n #lastPacketAtMs = 0\n #failbackWindowMs: number | null | undefined\n #onTakeover: (() => void) | null = null\n #onFailback: ((reason: MulticastFailbackReason) => void) | null = null\n // Data-plane-live edge: the first packet this join accepted. Everything\n // before it is join latency, not silence (see #resetWatchdog).\n #dataPlaneLive = false\n #onDataPlaneLive: (() => void) | null = null\n #errorListener: ((event: CustomEvent<unknown>) => void) | null = null\n #onDelivery: ((delivery: WindowMulticastDelivery) => void) | null = null\n #delivery: WindowMulticastDelivery | undefined\n #disposed = false\n #stats: MulticastTransportStats = {\n packetsReceived: 0,\n bytesReceived: 0,\n packetsDroppedUnregisteredPid: 0,\n packetsDroppedUnparseable: 0,\n packetsDroppedSignaling: 0,\n }\n // join() calls by how they resolved; see joinStats.\n #joinsSucceeded = 0\n readonly #joinsDeclined: Record<MulticastJoinDeclineReason, number> = {\n disposed: 0,\n unavailable: 0,\n superseded: 0,\n 'invalid-request': 0,\n 'provider-rejected': 0,\n 'invalid-subscription': 0,\n 'delivery-hook-failed': 0,\n }\n // The newest resolved join's outcome (see lastJoinOutcome) and the join\n // generation it belongs to. Generations are issued in start order, so a\n // join that resolves after a newer join has recorded leaves it standing.\n #lastJoinOutcome: MulticastJoinOutcome | undefined = undefined\n #lastJoinOutcomeGeneration = 0\n #droppedPidCounts = new Map<number, number>()\n // Accepted bytes per MMTP packet_id; see bytesReceivedForPacketIds. Each\n // value is a mutable cell, so the per-packet update is one lookup and an\n // in-place add: no insertion once the key exists, and no re-boxed number\n // per packet once a count outgrows the small-integer range.\n readonly #acceptedBytesByPacketId = new Map<number, { bytes: number }>()\n #packetIds: ReadonlySet<number> | null = null\n // What the live join delivers with each accepted packet. A field, not a\n // listener closure, so an in-place admission change (setContext) applies\n // from the next packet on without rejoining.\n #context: TPacketContext | undefined = undefined\n // The live join's provider-side filter. An in-place widening must reach it,\n // or the provider never delivers the new ID; null without a live join.\n #providerFilter: ProviderFilter | null = null\n // IDs an in-place update withdrew while the provider may still be\n // delivering them. Their late packets are still counted as filtered, but\n // they are the expected tail of a deliberate change, not a provisioning\n // fault, so they are counted apart and do not raise the unadvertised-PID\n // warning.\n readonly #withdrawnPacketIds = new Set<number>()\n #withdrawnPacketIdDrops = 0\n // Distinct packet IDs dropped as provisioning faults: on the wire but in\n // no allow-list the join or an in-place update declared. Capped with\n // droppedPacketIds.\n readonly #unadvertisedPacketIds = new Set<number>()\n #joinGeneration = 0\n\n constructor(onPacket: (packet: Uint8Array, context?: TPacketContext) => void) {\n this.#onPacket = onPacket\n }\n\n static isAvailable(): boolean {\n return hasMulticastGateway()\n }\n\n async join(config: MulticastTransportConfig, context?: TPacketContext): Promise<boolean> {\n const generation = ++this.#joinGeneration\n this.#releaseSubscription()\n this.#dataPlaneLive = false\n\n if (this.#disposed) return this.#declined(generation, 'disposed')\n if (!MulticastTransport.isAvailable()) {\n console.warn('[MulticastTransport] Multicast API not available')\n return this.#declined(generation, 'unavailable')\n }\n\n // What a throw from the step in progress means. Each step names its\n // failure before it runs, so the catch classifies a decline by where the\n // join stopped, never by what the error says.\n let failure: MulticastJoinDeclineReason = 'invalid-request'\n try {\n const packetIds = config.packetIds?.length ? new Set(config.packetIds) : null\n console.log(\n `[MulticastTransport] window.multicast.subscribe(${config.sourceAddress}, ${config.groupAddress}, ${config.port})`,\n )\n const subscribeConfig = toWindowMulticastSubscribeConfig({\n source: config.sourceAddress,\n group: config.groupAddress,\n port: config.port,\n mode: config.mode,\n amtRelay: config.amtRelay,\n amtRelayPort: config.amtRelayPort,\n packetIds: config.packetIds,\n tracks: config.tracks,\n })\n failure = 'provider-rejected'\n const subscription = await getMulticastGlobals().multicast!.subscribe!(subscribeConfig)\n failure = 'invalid-subscription'\n console.log(`[MulticastTransport] window.multicast.subscribe returned:`, !!subscription)\n\n if (this.#disposed || generation !== this.#joinGeneration) {\n void subscription.unsubscribe()\n return this.#declined(generation, this.#displacedReason())\n }\n if (typeof subscription.addEventListener !== 'function') {\n if (typeof subscription.unsubscribe === 'function') {\n void subscription.unsubscribe()\n }\n throw new Error('window.multicast subscription does not support bytes events')\n }\n\n // Own the subscription before anything below can throw, so a failed\n // join's release reaches unsubscribe() instead of leaking it.\n this.#subscription = subscription\n const live = () => !this.#disposed && generation === this.#joinGeneration\n // Exactly one packet surface. A provider that advertises the `packets`\n // extension delivers each reader batch as one event, so the page pays\n // one DOM event and one liveness read per batch instead of per packet\n // (BLO-36599). Every packet still goes through the same filter, counters\n // and callback, in arrival order, as a view of the provider's bytes.\n const packetListener: PacketListener = subscriptionDeliversPacketBatches(subscription)\n ? {\n type: 'packets',\n listener: (event) => {\n if (!live()) return\n this.#deliverBatch(event.detail, live)\n },\n }\n : {\n type: 'bytes',\n listener: (event) => {\n if (!live()) return\n if (this.#deliver(event.detail)) this.#resetWatchdog()\n },\n }\n this.#packetListener = packetListener\n this.#packetIds = packetIds\n this.#providerFilter = {\n acknowledged: packetIds,\n sent: 0,\n acknowledgedUpdate: 0,\n inFlight: 0,\n }\n this.#context = context\n // Seed the allow-list's counters so accepted packets never insert. An\n // id already counted by an earlier join keeps its count: the counters\n // are cumulative across rejoins, as bytesReceived is.\n if (packetIds) {\n for (const packetId of packetIds) {\n if (!this.#acceptedBytesByPacketId.has(packetId)) {\n this.#acceptedBytesByPacketId.set(packetId, { bytes: 0 })\n }\n }\n }\n // The request origin is the accepted acquisition contract. The\n // gateway no longer exposes a mutable transport/transportchange\n // surface, so assign this only after subscribe() has resolved and the\n // returned subscription has passed the event-surface check above.\n this.#resolvedTransport = subscribeConfig.origin === 'amt' ? 'amt' : 'native'\n // The provider alone knows whether its packets are independent\n // datagrams or an ordered relay of one socket; undeclared promises no\n // ordering. Announce it before the listener can deliver a packet. A\n // rejected declaration fails the join and releases the subscription.\n const delivery = subscription.delivery ?? 'datagram'\n if (delivery !== 'datagram' && delivery !== 'ordered-relay') {\n throw new Error(\n `window.multicast subscription declared unknown delivery ${String(delivery)}`,\n )\n }\n this.#delivery = delivery\n failure = 'delivery-hook-failed'\n this.#onDelivery?.(delivery)\n failure = 'invalid-subscription'\n // The provider's explicit, terminal subscription failure. Before the\n // first packet this is the only signal that bounds a join, so it must\n // reach the failback path rather than be dropped.\n const onError = (event: CustomEvent<unknown>) => {\n if (this.#disposed || generation !== this.#joinGeneration) return\n this.#failBackOnSubscriptionError(event.detail)\n }\n this.#errorListener = onError\n if (packetListener.type === 'packets') {\n subscription.addEventListener('packets', packetListener.listener)\n } else {\n subscription.addEventListener('bytes', packetListener.listener)\n }\n subscription.addEventListener('error', onError)\n // No watchdog here. A resolved subscribe() is the provider's join\n // outcome, not proof that the data plane is live: over AMT it resolves\n // once the gateway has sent its Membership Update, and the relay's\n // upstream join plus one path RTT still precede the first packet. The\n // silence window is a packet-gap clock for a stream that has flowed;\n // charging join latency to it failed back joins that went on to play.\n // The join phase is bounded by the provider's typed error or the host's\n // takeover deadline (see SessionManager.joinActive). The window starts\n // at the first accepted packet (#resetWatchdog).\n console.log(\n `[MulticastTransport] Joined ${config.groupAddress}:${config.port}`,\n )\n return this.#joined(generation)\n } catch (err) {\n if (this.#disposed || generation !== this.#joinGeneration) {\n return this.#declined(generation, this.#displacedReason())\n }\n this.#releaseSubscription()\n console.error('[MulticastTransport] Failed to join SSM group:', err)\n return this.#declined(\n generation,\n failure,\n failure === 'provider-rejected' ? providerErrorOf(err) : undefined,\n )\n }\n }\n\n /** Record a join that resolved true; returns its result. */\n #joined(generation: number): true {\n this.#joinsSucceeded++\n this.#recordJoinOutcome(generation, JOIN_SUCCEEDED)\n return true\n }\n\n /** Record a join that resolved false, and why; returns its result. */\n #declined(\n generation: number,\n reason: MulticastJoinDeclineReason,\n providerError?: MulticastJoinProviderError,\n ): false {\n this.#joinsDeclined[reason]++\n this.#recordJoinOutcome(\n generation,\n Object.freeze(\n providerError === undefined ? { ok: false, reason } : { ok: false, reason, providerError },\n ),\n )\n return false\n }\n\n /**\n * Why a join that no longer owns the transport resolved false. dispose()\n * advances the join generation too, so disposal is checked first.\n */\n #displacedReason(): 'disposed' | 'superseded' {\n return this.#disposed ? 'disposed' : 'superseded'\n }\n\n #recordJoinOutcome(generation: number, outcome: MulticastJoinOutcome): void {\n if (generation <= this.#lastJoinOutcomeGeneration) return\n this.#lastJoinOutcomeGeneration = generation\n this.#lastJoinOutcome = outcome\n }\n\n /**\n * One packet from the subscription: filter, count, and hand it to the\n * callback. Returns whether it was delivered; the caller notes liveness.\n */\n #deliver(detail: unknown): boolean {\n const packet = normalizeBytesEventDetail(detail)\n if (!packet) {\n this.#stats.packetsDroppedUnparseable++\n return false\n }\n if (!this.#acceptPacket(packet)) return false\n // Read per packet: a callback that changes the admission (an in-place\n // rendition switch committing on this very packet) takes effect from the\n // next packet, including the rest of a batch.\n this.#onPacket(packet, this.#context)\n return true\n }\n\n /**\n * One `packets` event. Each packet is delivered as a `bytes` event would\n * have delivered it, including isolation: a packet whose callback throws\n * does not cost the rest of its batch, as it would not have cost the next\n * `bytes` event. The first failure is rethrown once the batch is done, so\n * it reaches the event-listener error reporting a `bytes` listener's throw\n * reached; any further one is rethrown from its own microtask, which the\n * host reports the same way, so no failure is swallowed. A callback that\n * disposes the transport or starts another join ends the batch there, as\n * the per-packet guard would have ignored the remaining events.\n */\n #deliverBatch(\n batch: readonly unknown[],\n live: () => boolean,\n ): void {\n if (!Array.isArray(batch)) {\n throw new TypeError('[MulticastTransport] packets event detail must be an array of packets')\n }\n let delivered = false\n let failures: unknown[] | null = null\n for (let i = 0; i < batch.length; i++) {\n try {\n if (this.#deliver(batch[i])) delivered = true\n } catch (error) {\n if (failures === null) failures = []\n failures.push(error)\n }\n if (!live()) break\n }\n // One liveness read covers the batch: every packet in it arrived in the\n // same provider task. A batch that delivered nothing, like a rejected\n // bytes event, is not liveness.\n if (delivered && live()) this.#resetWatchdog()\n if (failures !== null) {\n for (let i = 1; i < failures.length; i++) {\n const failure = failures[i]\n queueMicrotask(() => {\n throw failure\n })\n }\n throw failures[0]\n }\n }\n\n /** Feed a raw packet from an external source (e.g., AMT tunnel or ASM subscription). */\n feedPacket(data: ArrayBuffer | Uint8Array, context?: TPacketContext): void {\n if (this.#disposed) return\n const packet = data instanceof Uint8Array ? data : new Uint8Array(data)\n if (!this.#acceptPacket(packet)) return\n this.#onPacket(packet, context)\n this.#resetWatchdog()\n }\n\n confirmVideo(): void {\n this.#videoConfirmed = true\n this.#checkTakeover()\n }\n\n confirmAudio(): void {\n this.#audioConfirmed = true\n this.#checkTakeover()\n }\n\n onTakeover(cb: () => void): void {\n this.#onTakeover = cb\n }\n\n onFailback(cb: (reason: MulticastFailbackReason) => void): void {\n this.#onFailback = cb\n }\n\n /**\n * Called on every successful join, before the joined subscription can\n * deliver a packet, with how that subscription delivers packets.\n */\n onDelivery(cb: (delivery: WindowMulticastDelivery) => void): void {\n this.#onDelivery = cb\n }\n\n /** How the current subscription delivers packets, once joined. */\n get delivery(): WindowMulticastDelivery | undefined {\n return this.#delivery\n }\n\n /**\n * Fires once per join, at the first packet the join accepts (after the PID\n * filter; a rejected packet is not liveness). This is the data-plane-live\n * edge, where this transport's silence window starts. A join whose source\n * never sends never reaches it, so a host may start a media-arrival budget\n * here only if the provider bounds the join phase with a typed subscription\n * error (see SessionManager.joinActive); otherwise the host starts it when\n * the subscribe settles.\n */\n onDataPlaneLive(cb: () => void): void {\n this.#onDataPlaneLive = cb\n }\n\n /** Whether the current join has accepted a packet (see onDataPlaneLive). */\n get dataPlaneLive(): boolean {\n return this.#dataPlaneLive\n }\n\n setFailbackWindowMs(windowMs: number | null): void {\n if (windowMs !== null && (!Number.isFinite(windowMs) || windowMs <= 0)) {\n throw new Error(\n `[MulticastTransport] failbackWindowMs=${windowMs}. ` +\n `Derive a positive failback window from the stream catalog.`,\n )\n }\n this.#failbackWindowMs = windowMs\n if (windowMs === null) {\n if (this.#watchdog !== null) clearTimeout(this.#watchdog)\n this.#watchdog = null\n return\n }\n // Re-enable against the last packet even when the previous policy disabled\n // the watchdog: a silent live stream may never deliver another packet.\n if (this.#watchdog !== null) clearTimeout(this.#watchdog)\n this.#watchdog = null\n if (this.#dataPlaneLive && this.#onFailback) {\n const silentMs = performance.now() - this.#lastPacketAtMs\n this.#armWatchdog(Math.max(0, Math.ceil(windowMs - silentMs)))\n }\n }\n\n get stats(): MulticastTransportStats {\n return this.#stats\n }\n\n /**\n * How the newest join() to resolve did: `{ ok: true }`, or `{ ok: false }`\n * with the typed reason it resolved false (and, for `provider-rejected`, the\n * provider's typed error fields). Undefined until a join resolves. Branch on\n * `reason` and `providerError.code`, never on log text or\n * `providerError.message`.\n *\n * Newest is by start order: a join that resolves after a newer join has\n * recorded its outcome does not replace it, so once `await join()` resolves\n * this names that join's outcome unless a newer join has already resolved.\n * It records how a join resolved, not whether it is still joined (see\n * `joined`); a join() that rejects instead of resolving records nothing,\n * the rejection being its report. Frozen: a later outcome replaces the\n * object, never changes it.\n */\n get lastJoinOutcome(): MulticastJoinOutcome | undefined {\n return this.#lastJoinOutcome\n }\n\n /**\n * join() calls by how they resolved (see MulticastJoinStats). A fresh frozen\n * snapshot on every read, so a later join never changes one already read.\n * Kept apart from `stats`, which is the live counter object.\n */\n get joinStats(): MulticastJoinStats {\n return Object.freeze({\n succeeded: this.#joinsSucceeded,\n declined: Object.freeze({ ...this.#joinsDeclined }),\n })\n }\n\n /**\n * Accepted bytes summed over `packetIds`: the `bytesReceived` share of those\n * MMTP packet_ids. Counted by the same rule, on every path that counts\n * `bytesReceived` (only packets that pass the ingress filter; packet_id 0\n * signaling never), and cumulative for the life of the transport, across\n * rejoins, as `bytesReceived` is. An id never accepted contributes 0. An\n * unparseable packet admitted by a join without an allow-list has no\n * packet_id, so it counts toward `bytesReceived` only.\n *\n * Use it to scope an ABR throughput sample to the active tier's source and\n * repair packet_ids when the join admits more than the active tier (a\n * shared group joined across rendition switches); `bytesReceived` is then\n * the whole allow-list's arrival, not the active tier's. A Set, so an id\n * shared by two tracks of the tier is counted once.\n *\n * Monotonic only for a fixed set: each id has counted since it was first\n * admitted, so after a tier switch the new set's sum is not a continuation\n * of the old one. A cumulative consumer takes a fresh baseline for the new\n * set and never differences across sets.\n */\n bytesReceivedForPacketIds(packetIds: ReadonlySet<number>): number {\n let bytes = 0\n for (const packetId of packetIds) {\n bytes += this.#acceptedBytesByPacketId.get(packetId)?.bytes ?? 0\n }\n return bytes\n }\n\n /**\n * Per-PID drop breakdown, newest-capped at MAX_TRACKED_DROPPED_PIDS distinct\n * PIDs. This is what names the offender: `packetsDroppedUnregisteredPid`\n * says the ingress filter fired, this says it fired on 0x8001.\n */\n get droppedPacketIds(): ReadonlyMap<number, number> {\n return this.#droppedPidCounts\n }\n\n /**\n * Distinct packet IDs dropped as provisioning faults: arriving on the wire\n * but in no allow-list the join or an in-place update (setPacketIds)\n * declared, the drops that raise the unadvertised-PID warning. A packet ID\n * an in-place update withdrew is not one (see withdrawnPacketIdDrops).\n * Cumulative across rejoins and capped with droppedPacketIds.\n */\n get unadvertisedPacketIds(): ReadonlySet<number> {\n return this.#unadvertisedPacketIds\n }\n\n /**\n * Packets dropped because an in-place update (setPacketIds) withdrew their\n * packet ID while the provider may still deliver it: the expected tail of\n * a deliberate change. A subset of packetsDroppedUnregisteredPid,\n * cumulative across rejoins.\n */\n get withdrawnPacketIdDrops(): number {\n return this.#withdrawnPacketIdDrops\n }\n\n /** Whether a subscription is live: joined, and not yet released or replaced. */\n get joined(): boolean {\n return this.#subscription !== null\n }\n\n /**\n * What the live join delivers with every accepted packet (the join's\n * `context`, or the last setContext), or undefined without a live join.\n */\n get context(): TPacketContext | undefined {\n return this.#context\n }\n\n /**\n * The live join's page-side packet-ID allow-list, or null when it admits\n * every packet ID (a join that carried none, or no live join).\n */\n get packetIds(): ReadonlySet<number> | null {\n return this.#packetIds\n }\n\n /**\n * Replace what the live join delivers with each accepted packet, from the\n * next packet on. The subscription (socket, group membership, AMT tunnel)\n * is untouched: this is how an in-place rendition switch moves the router's\n * ingress identities without leaving and re-joining the group.\n */\n setContext(context: TPacketContext): void {\n if (this.#subscription === null) {\n throw new Error(\n '[MulticastTransport] setContext needs a live join; join() carries the first context',\n )\n }\n this.#context = context\n }\n\n /**\n * Replace the live join's packet-ID allow-list in place, without rejoining.\n *\n * The page-side filter changes synchronously, before this returns: the\n * next packet is admitted or dropped by the new list. The provider's own\n * filter is updated through the subscription's `setPacketIds` when it has\n * one; the returned promise settles with the provider's acknowledgement of\n * this update. It resolves at once, telling the provider nothing, only\n * when the provider cannot be told, or when no earlier update is still in\n * flight and the provider already forwards exactly this list.\n *\n * While an update is in flight the provider's filter is not known, so every\n * later update is sent: one skipped because it matched the last\n * acknowledged filter would let the in-flight update land after it, and\n * leave the provider forwarding a list nobody asked for last. Updates are\n * numbered in send order and the newest acknowledged one describes the\n * provider, so an acknowledgement that overtakes an older one is not undone\n * by it.\n *\n * A provider without `setPacketIds` can still be narrowed: its wider filter\n * keeps delivering the dropped IDs and the page-side filter discards them.\n * It cannot be widened, since it would never deliver the added IDs; that is\n * refused with a throw before anything changes. An ID this update withdraws\n * may keep arriving until the provider applies the narrower list; those\n * packets are counted as filtered like any other and in\n * withdrawnPacketIdDrops, but are not unadvertised packet IDs: their\n * arrival is the expected tail of this change rather than a provisioning\n * fault.\n */\n setPacketIds(packetIds: readonly number[]): Promise<void> {\n const subscription = this.#subscription\n const filter = this.#providerFilter\n if (subscription === null || filter === null) {\n throw new Error(\n '[MulticastTransport] setPacketIds needs a live join; join() carries the first allow-list',\n )\n }\n if (!Array.isArray(packetIds) || packetIds.length === 0) {\n throw new RangeError(\n '[MulticastTransport] setPacketIds needs a non-empty packet-ID list: ' +\n 'an in-place update names the packet IDs it admits',\n )\n }\n if (!packetIds.every(isMulticastTrackPacketId)) {\n throw new RangeError(\n `[MulticastTransport] packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}; ` +\n \"packetId 0 is the MMTP signaling flow and is never a track's\",\n )\n }\n const next: ReadonlySet<number> = new Set(packetIds)\n const provider = filter.acknowledged\n const update = packetFilterUpdater(subscription)\n // Without an updater nothing is ever sent, so the join's filter is exact.\n if (update === undefined && provider !== null) {\n const missing = [...next].filter((id) => !provider.has(id))\n if (missing.length > 0) {\n throw new Error(\n '[MulticastTransport] this subscription cannot widen its packet filter in place ' +\n `(the provider exposes no setPacketIds); rejoin to admit ${missing.join(', ')}`,\n )\n }\n }\n const previous = this.#packetIds\n this.#packetIds = next\n for (const id of next) {\n this.#withdrawnPacketIds.delete(id)\n if (!this.#acceptedBytesByPacketId.has(id)) {\n this.#acceptedBytesByPacketId.set(id, { bytes: 0 })\n }\n }\n if (previous !== null) {\n for (const id of previous) if (!next.has(id)) this.#withdrawnPacketIds.add(id)\n }\n if (update === undefined) return Promise.resolve()\n if (filter.inFlight === 0 && provider !== null && sameIds(provider, next)) {\n return Promise.resolve()\n }\n const number = ++filter.sent\n filter.inFlight++\n let applied: Promise<void>\n try {\n applied = Promise.resolve(update([...next]))\n } catch (error) {\n applied = Promise.reject(error)\n }\n return applied.then(\n () => {\n if (this.#providerFilter !== filter) return\n filter.inFlight--\n if (number > filter.acknowledgedUpdate) {\n filter.acknowledgedUpdate = number\n filter.acknowledged = next\n }\n },\n (error: unknown) => {\n // A refused update leaves the provider on the filter it had.\n if (this.#providerFilter === filter) filter.inFlight--\n throw error\n },\n )\n }\n\n get resolvedTransport(): ResolvedMulticastTransport | undefined {\n return this.#resolvedTransport\n }\n\n dispose(): void {\n ++this.#joinGeneration\n this.#disposed = true\n this.#releaseSubscription()\n }\n\n #releaseSubscription(): void {\n if (this.#watchdog) clearTimeout(this.#watchdog)\n this.#watchdog = null\n if (this.#subscription) {\n const packetListener = this.#packetListener\n if (packetListener?.type === 'packets') {\n this.#subscription.removeEventListener('packets', packetListener.listener)\n } else if (packetListener) {\n this.#subscription.removeEventListener('bytes', packetListener.listener)\n }\n if (this.#errorListener) {\n this.#subscription.removeEventListener('error', this.#errorListener)\n }\n void this.#subscription.unsubscribe()\n }\n this.#subscription = null\n this.#packetListener = null\n this.#errorListener = null\n this.#resolvedTransport = undefined\n this.#delivery = undefined\n this.#packetIds = null\n this.#providerFilter = null\n this.#context = undefined\n this.#withdrawnPacketIds.clear()\n }\n\n /** The ingress filter; an accepted packet is counted here, once. */\n #acceptPacket(packet: Uint8Array): boolean {\n // This early read is intentional: rejected PIDs must not affect page\n // delivery, receive statistics, or the failback liveness watchdog. It\n // refuses exactly what parseMMTPHeader refuses but builds no header\n // object; the router parses the header once, later (BLO-36599).\n const packetId = readMMTPPacketId(packet)\n if (packetId === null) {\n // Without an allow-list there is no filter to enforce, so an unparseable\n // packet keeps flowing to the router as before.\n if (!this.#packetIds) {\n this.#countAccepted(packet, null)\n return true\n }\n this.#stats.packetsDroppedUnparseable++\n return false\n }\n // packet_id 0 is the MMTP signaling flow and never a track's. Drop it even\n // when the join carried no allow-list (a legacy catalog whose tracks omit\n // packetId): the discard rule must not depend on how the catalog was written.\n if (packetId === MMTP_SIGNALING_PACKET_ID) {\n this.#stats.packetsDroppedSignaling++\n return false\n }\n if (!this.#packetIds || this.#packetIds.has(packetId)) {\n this.#countAccepted(packet, packetId)\n return true\n }\n this.#recordUnregisteredPidDrop(packetId)\n return false\n }\n\n /** Receive counters for one accepted packet; `packetId` is null when it has none. */\n #countAccepted(packet: Uint8Array, packetId: number | null): void {\n this.#stats.packetsReceived++\n this.#stats.bytesReceived += packet.byteLength\n if (packetId === null) return\n const counter = this.#acceptedBytesByPacketId.get(packetId)\n if (counter !== undefined) {\n counter.bytes += packet.byteLength\n } else {\n // First sight of this id on a join with no allow-list (an allow-list's\n // ids are seeded at join). Bounded by the 16-bit packet_id space.\n this.#acceptedBytesByPacketId.set(packetId, { bytes: packet.byteLength })\n }\n }\n\n #recordUnregisteredPidDrop(packetId: number): void {\n this.#stats.packetsDroppedUnregisteredPid++\n const withdrawn = this.#withdrawnPacketIds.has(packetId)\n if (withdrawn) this.#withdrawnPacketIdDrops++\n const seen = this.#droppedPidCounts.get(packetId)\n if (seen !== undefined) {\n this.#droppedPidCounts.set(packetId, seen + 1)\n return\n }\n if (this.#droppedPidCounts.size >= MAX_TRACKED_DROPPED_PIDS) return\n this.#droppedPidCounts.set(packetId, 1)\n if (withdrawn) return\n this.#unadvertisedPacketIds.add(packetId)\n // Warn once per distinct PID. A PID arriving on the wire but missing from\n // the catalog's advertised set is a provisioning fault, not stream noise,\n // and it is silent in every downstream counter.\n console.warn(\n `[MulticastTransport] Dropping packetId=${packetId} (0x${packetId.toString(16)}): ` +\n `not in the advertised set [${[...(this.#packetIds ?? [])].join(', ')}]. ` +\n `If this is an FEC repair leg, the catalog is not advertising its packetId.`,\n )\n }\n\n #checkTakeover(): void {\n if (this.#videoConfirmed && this.#audioConfirmed && this.#onTakeover) {\n const cb = this.#onTakeover\n this.#onTakeover = null // fire exactly once\n cb()\n }\n }\n\n /**\n * Note liveness now; arm the no-packets timer if none is armed.\n *\n * Called only for accepted packets, so the first call of a join is the\n * data-plane-live edge and is what first arms the silence window: the window\n * measures silence of a flowing stream, which is undefined before the stream\n * has flowed.\n */\n #resetWatchdog(): void {\n this.#lastPacketAtMs = performance.now()\n if (!this.#dataPlaneLive) {\n this.#dataPlaneLive = true\n this.#onDataPlaneLive?.()\n }\n if (this.#watchdog !== null) return\n if (!this.#onFailback) return\n if (this.#failbackWindowMs === null) return\n if (this.#failbackWindowMs === undefined) {\n throw new Error(\n '[MulticastTransport] Failback watchdog requires a catalog-derived window',\n )\n }\n this.#armWatchdog(this.#failbackWindowMs)\n }\n\n #armWatchdog(delayMs: number): void {\n this.#watchdog = setTimeout(() => {\n this.#watchdog = null\n if (this.#disposed || !this.#onFailback || this.#failbackWindowMs == null) return\n const silentMs = performance.now() - this.#lastPacketAtMs\n if (silentMs < this.#failbackWindowMs) {\n this.#armWatchdog(Math.ceil(this.#failbackWindowMs - silentMs))\n return\n }\n console.warn(\n `[MulticastTransport] No packets for ${this.#failbackWindowMs}ms; failing back to MoQ`,\n )\n this.#onFailback({ kind: 'silence', windowMs: this.#failbackWindowMs })\n }, delayMs)\n }\n\n /** Hand control back to MoQ for a provider-reported subscription failure. */\n #failBackOnSubscriptionError(error: unknown): void {\n // The subscription is dead; a silence timer armed against it would only\n // report the same failure a second time.\n if (this.#watchdog !== null) clearTimeout(this.#watchdog)\n this.#watchdog = null\n console.warn('[MulticastTransport] Subscription failed; failing back to MoQ:', error)\n this.#onFailback?.({ kind: 'subscription-error', error })\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["MoqFecAlgorithm", "entry", "packetData", "MPU_HEADER_SIZE", "MPU_LENGTH_FIELD_SIZE", "MPU_HEADER_REMAINDER_SIZE", "MPU_HEADER_SIZE", "MMTP_SIGNALING_PACKET_ID", "MMTP_PACKET_ID_MAX", "NTP_SHORT_BITS", "NTP_SHORT_MODULUS", "NTP_SHORT_HALF_RANGE", "FEC_BLOCK_PROFILES", "MMTP_PACKET_ID_MAX", "MMTP_SIGNALING_PACKET_ID"]
|
|
7
7
|
}
|