@blockcast/mmt-container 2.1.0 → 2.2.0-main.050b52fb9b00
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 +29 -17
- package/dist/browser-runtime.js +1949 -374
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.js.map +1 -1
- package/dist/catalog-fec-params.d.ts +17 -1
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +15 -0
- package/dist/catalog-fec-params.js.map +2 -2
- package/dist/catalog-network-source.js.map +1 -1
- package/dist/cmcd-producer.d.ts +65 -47
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js +138 -109
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +26 -16
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +70 -12
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +403 -58
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-state.js.map +1 -1
- package/dist/fec-worker-setup.d.ts +1 -1
- package/dist/fec-worker-setup.js +1 -1
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/held-layer-admission.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.d.ts +48 -13
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +97 -25
- package/dist/iso-block-locator.js.map +2 -2
- package/dist/iso-bmff-timing.d.ts +10 -1
- package/dist/iso-bmff-timing.d.ts.map +1 -1
- package/dist/iso-bmff-timing.js +36 -2
- package/dist/iso-bmff-timing.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mfu-reassembler.js +12 -19
- package/dist/mfu-reassembler.js.map +2 -2
- package/dist/mmtp-container-parser.d.ts +79 -10
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +219 -45
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +342 -63
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +218 -45
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-fec-session.d.ts +41 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +118 -13
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +43 -9
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +2006 -95
- package/dist/mmtp-router.js.map +4 -4
- package/dist/multicast-auth-catalog.js.map +1 -1
- package/dist/multicast-input.js.map +1 -1
- package/dist/multicast-manager.d.ts +28 -5
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +16116 -15005
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +11 -3
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +281 -35
- package/dist/multicast-transport.js.map +4 -4
- package/dist/multicast-unwrap.js +12 -19
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +336 -59
- package/dist/no-modules.js.map +3 -3
- package/dist/repair-fetch.d.ts +36 -8
- package/dist/repair-fetch.d.ts.map +1 -1
- package/dist/repair-fetch.js +285 -26
- package/dist/repair-fetch.js.map +3 -3
- package/dist/repair-layer-subscriptions.d.ts +82 -0
- package/dist/repair-layer-subscriptions.d.ts.map +1 -0
- package/dist/repair-layer-subscriptions.js +192 -0
- package/dist/repair-layer-subscriptions.js.map +7 -0
- package/dist/signaling.js.map +2 -2
- package/dist/timestamp-codec.js +12 -19
- package/dist/timestamp-codec.js.map +2 -2
- package/dist/track-session.d.ts +1 -1
- package/dist/track-session.d.ts.map +1 -1
- package/dist/track-session.js +1 -0
- package/dist/track-session.js.map +2 -2
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +218 -45
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +8 -5
- package/schemas/catalog.schema.json +4 -0
- package/src/catalog-fec-params.ts +36 -1
- package/src/cmcd-producer.ts +194 -179
- package/src/fec-manager.ts +433 -32
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +20 -2
- package/src/iso-block-locator.ts +133 -31
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +306 -29
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +629 -54
- package/src/multicast-manager.ts +70 -11
- package/src/multicast-transport.ts +44 -20
- package/src/repair-fetch.ts +363 -49
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +4 -1
package/dist/cmcd-producer.js
CHANGED
|
@@ -1,62 +1,64 @@
|
|
|
1
1
|
// src/cmcd-producer.ts
|
|
2
2
|
var PLAY_MODES = ["full", "base-only", "single-track"];
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return params;
|
|
14
|
-
}
|
|
15
|
-
function buildTransportParams(snap) {
|
|
16
|
-
if (!snap) return {};
|
|
17
|
-
const params = {};
|
|
18
|
-
params["x-bc-transport"] = snap.transport;
|
|
19
|
-
if (snap.multicastGroup != null) {
|
|
20
|
-
params["x-bc-multicast-group"] = snap.multicastGroup;
|
|
21
|
-
}
|
|
22
|
-
if (snap.multicastSource != null) {
|
|
23
|
-
params["x-bc-multicast-source"] = snap.multicastSource;
|
|
24
|
-
}
|
|
25
|
-
if (snap.amtRelay != null) {
|
|
26
|
-
params["x-bc-amt-relay"] = snap.amtRelay;
|
|
27
|
-
}
|
|
28
|
-
return params;
|
|
3
|
+
var DEFAULT_INTERVAL_MS = 3e4;
|
|
4
|
+
var CUSTOM_PREFIX = "com.blockcast-";
|
|
5
|
+
var MAX_CUSTOM_VALUE = 64;
|
|
6
|
+
var TRANSPORT_TOKEN = {
|
|
7
|
+
native: "ssm",
|
|
8
|
+
amt: "amt",
|
|
9
|
+
moq: "moq"
|
|
10
|
+
};
|
|
11
|
+
function sfString(value) {
|
|
12
|
+
return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
|
|
29
13
|
}
|
|
30
|
-
function
|
|
31
|
-
return Object.keys(
|
|
32
|
-
const
|
|
33
|
-
if (typeof
|
|
34
|
-
return `${key}=${
|
|
14
|
+
function encodeCmcdRecord(record) {
|
|
15
|
+
return Object.keys(record).sort().map((key) => {
|
|
16
|
+
const value = record[key];
|
|
17
|
+
if (typeof value === "number") return `${key}=${value}`;
|
|
18
|
+
if (typeof value === "string") return `${key}=${sfString(value)}`;
|
|
19
|
+
if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
|
|
20
|
+
return `${key}=${value.token}`;
|
|
35
21
|
}).join(",");
|
|
36
22
|
}
|
|
23
|
+
var token = (value) => ({ token: value });
|
|
24
|
+
function putCustom(record, name, value) {
|
|
25
|
+
if (value === void 0) return;
|
|
26
|
+
if (typeof value === "number") value = String(value);
|
|
27
|
+
if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
|
|
28
|
+
record[CUSTOM_PREFIX + name] = value;
|
|
29
|
+
}
|
|
37
30
|
var CmcdProducer = class {
|
|
38
|
-
#onCmcdParams;
|
|
39
31
|
#onCmcdPost;
|
|
32
|
+
#intervalMs;
|
|
33
|
+
#sessionId;
|
|
34
|
+
#now;
|
|
35
|
+
#sn = 0;
|
|
36
|
+
#lastHeartbeat = null;
|
|
40
37
|
#transport;
|
|
41
38
|
#lastStats;
|
|
42
|
-
// Phase 14 state — Plan 03 Task 2 (D-12, D-15).
|
|
43
39
|
#playMode = null;
|
|
44
40
|
#deltaFailRate = null;
|
|
45
41
|
#switchMiscount = 0;
|
|
46
42
|
#manifestStats = null;
|
|
47
43
|
#fecFailClosed = null;
|
|
44
|
+
/** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
|
|
45
|
+
#lifecycle = null;
|
|
48
46
|
constructor(options) {
|
|
49
|
-
this.#onCmcdParams = options.onCmcdParams;
|
|
50
47
|
this.#onCmcdPost = options.onCmcdPost;
|
|
48
|
+
this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
49
|
+
if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
|
|
50
|
+
throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
|
|
51
|
+
}
|
|
52
|
+
this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
|
|
53
|
+
this.#now = options.now ?? Date.now;
|
|
51
54
|
}
|
|
52
55
|
/** Update the stored transport state. Called by player wiring layer. */
|
|
53
56
|
updateTransport(snapshot) {
|
|
54
57
|
this.#transport = snapshot;
|
|
55
58
|
}
|
|
56
59
|
/**
|
|
57
|
-
* Set the current play-mode
|
|
58
|
-
*
|
|
59
|
-
* (CLAUDE.md: error on bad init).
|
|
60
|
+
* Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
|
|
61
|
+
* value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
|
|
60
62
|
*/
|
|
61
63
|
updatePlayMode(mode) {
|
|
62
64
|
if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
|
|
@@ -67,9 +69,8 @@ var CmcdProducer = class {
|
|
|
67
69
|
this.#playMode = mode;
|
|
68
70
|
}
|
|
69
71
|
/**
|
|
70
|
-
* Set the delta-track FEC block-failure rate (0..1 inclusive)
|
|
71
|
-
* `
|
|
72
|
-
* Omitted on single-track streams.
|
|
72
|
+
* Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
|
|
73
|
+
* `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
|
|
73
74
|
*/
|
|
74
75
|
updateDeltaFailRate(rate) {
|
|
75
76
|
if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
|
|
@@ -79,115 +80,143 @@ var CmcdProducer = class {
|
|
|
79
80
|
}
|
|
80
81
|
this.#deltaFailRate = rate;
|
|
81
82
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Bump the cumulative switching-set misalignment counter. Emitted as
|
|
84
|
-
* `x-bc-switch-miscount` on snapshots when the counter is > 0.
|
|
85
|
-
*/
|
|
83
|
+
/** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
|
|
86
84
|
incrementSwitchMiscount() {
|
|
87
85
|
this.#switchMiscount += 1;
|
|
88
86
|
}
|
|
89
87
|
/**
|
|
90
|
-
* Record that FEC failed closed at runtime (fec-01 6.3.9).
|
|
91
|
-
*
|
|
92
|
-
* `
|
|
93
|
-
* stream playing without FEC is told apart from one
|
|
88
|
+
* Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
|
|
89
|
+
* as `e=e` with the reason code in `ec`, and on every later counter report
|
|
90
|
+
* as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
|
|
91
|
+
* B_i was undercut), so a stream playing without FEC is told apart from one
|
|
92
|
+
* with no loss.
|
|
94
93
|
*/
|
|
95
94
|
updateFecFailClosed(reason) {
|
|
95
|
+
const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
|
|
96
96
|
this.#fecFailClosed = reason;
|
|
97
|
+
if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
|
|
97
98
|
}
|
|
98
99
|
updateManifestVerification(stats) {
|
|
99
100
|
this.#manifestStats = stats;
|
|
100
101
|
}
|
|
101
102
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
103
|
+
* Count a video block's resolution from the repair lifecycle: recovered in
|
|
104
|
+
* band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
|
|
105
|
+
* as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
|
|
106
|
+
* the video-track `-fbf`. Until the player feeds this, the three are absent.
|
|
106
107
|
*/
|
|
108
|
+
onRepairLifecycle(event) {
|
|
109
|
+
this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
|
|
110
|
+
if (event.type === "recovered" && event.blockClass !== "audio") {
|
|
111
|
+
if (event.via === "inband") this.#lifecycle.inband++;
|
|
112
|
+
else this.#lifecycle.fetch++;
|
|
113
|
+
} else if (event.type === "failed" && event.blockClass === "keyframe") {
|
|
114
|
+
this.#lifecycle.failedKeyframe++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
|
|
107
118
|
emitSwitchDrift(driftUs) {
|
|
108
119
|
if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
|
|
109
120
|
throw new Error(
|
|
110
121
|
`[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
|
|
111
122
|
);
|
|
112
123
|
}
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (this.#switchMiscount > 0) params["x-bc-switch-miscount"] = this.#switchMiscount;
|
|
117
|
-
params["x-bc-switch-drift-us"] = driftUs;
|
|
118
|
-
this.#emit(params);
|
|
124
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
|
|
125
|
+
putCustom(record, "sdu", Math.round(driftUs));
|
|
126
|
+
this.#post([record]);
|
|
119
127
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Process a FecTrackStats snapshot. Finds the video track, builds
|
|
122
|
-
* x-bc-* vendor keys, and fires both delivery callbacks.
|
|
123
|
-
*/
|
|
128
|
+
/** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
|
|
124
129
|
onSnapshot(data) {
|
|
125
130
|
const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
|
|
126
131
|
if (!videoTrack) return;
|
|
127
132
|
this.#lastStats = videoTrack;
|
|
128
|
-
|
|
129
|
-
...buildFecParams(videoTrack),
|
|
130
|
-
...buildTransportParams(this.#transport)
|
|
131
|
-
};
|
|
132
|
-
if (this.#playMode !== null) {
|
|
133
|
-
params["x-bc-play-mode"] = this.#playMode;
|
|
134
|
-
}
|
|
135
|
-
if (this.#fecFailClosed) {
|
|
136
|
-
params["x-bc-fec-fail-closed"] = this.#fecFailClosed.code;
|
|
137
|
-
params["x-bc-fec-fail-layer"] = this.#fecFailClosed.layer;
|
|
138
|
-
}
|
|
139
|
-
if (this.#playMode !== null && this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
140
|
-
params["x-bc-delta-fail-rate"] = parseFloat(this.#deltaFailRate.toFixed(4));
|
|
141
|
-
}
|
|
142
|
-
if (this.#switchMiscount > 0) {
|
|
143
|
-
params["x-bc-switch-miscount"] = this.#switchMiscount;
|
|
144
|
-
}
|
|
145
|
-
if (this.#manifestStats) {
|
|
146
|
-
params["x-bc-manifest-verified"] = this.#manifestStats.verified > 0;
|
|
147
|
-
params["x-bc-manifest-failures"] = Object.values(this.#manifestStats.failures).reduce((total, count) => total + count, 0);
|
|
148
|
-
}
|
|
149
|
-
this.#emit(params);
|
|
133
|
+
this.#heartbeat();
|
|
150
134
|
}
|
|
151
135
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
136
|
+
* Take transport state for receivers that do not own FEC stats, and send the
|
|
137
|
+
* heartbeat when due.
|
|
154
138
|
*/
|
|
155
139
|
onTransportSnapshot(data) {
|
|
156
140
|
if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
|
|
157
141
|
throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
|
|
158
142
|
}
|
|
159
143
|
if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
|
|
160
|
-
if (data.switchMiscount !== void 0
|
|
161
|
-
|
|
144
|
+
if (data.switchMiscount !== void 0) {
|
|
145
|
+
if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
|
|
146
|
+
throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
|
|
147
|
+
}
|
|
148
|
+
this.#switchMiscount = data.switchMiscount;
|
|
162
149
|
}
|
|
163
150
|
this.#transport = data.transport;
|
|
164
|
-
|
|
165
|
-
if (this.#playMode !== null) params["x-bc-play-mode"] = this.#playMode;
|
|
166
|
-
const switchMiscount = data.switchMiscount ?? this.#switchMiscount;
|
|
167
|
-
if (switchMiscount > 0) params["x-bc-switch-miscount"] = switchMiscount;
|
|
168
|
-
this.#emit(params);
|
|
151
|
+
this.#heartbeat();
|
|
169
152
|
}
|
|
170
|
-
/**
|
|
171
|
-
* Signal a transport transition event. Fires an immediate out-of-band
|
|
172
|
-
* emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
|
|
173
|
-
* x-bc-ct (one-shot).
|
|
174
|
-
*/
|
|
153
|
+
/** Report a transport transition at once, as custom event `com.blockcast-ct`. */
|
|
175
154
|
onTransition(event) {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
155
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
|
|
156
|
+
putCustom(record, "ct", token(event));
|
|
157
|
+
this.#putTransport(record);
|
|
158
|
+
this.#post([record]);
|
|
159
|
+
}
|
|
160
|
+
#base(event) {
|
|
161
|
+
return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
|
|
162
|
+
}
|
|
163
|
+
#heartbeat() {
|
|
164
|
+
const now = this.#now();
|
|
165
|
+
if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
|
|
166
|
+
this.#lastHeartbeat = now;
|
|
167
|
+
this.#post([this.#base("t"), this.#counters()]);
|
|
168
|
+
}
|
|
169
|
+
#putTransport(record) {
|
|
170
|
+
const snap = this.#transport;
|
|
171
|
+
if (!snap) return;
|
|
172
|
+
putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
|
|
173
|
+
if (snap.multicastGroup != null && snap.multicastSource != null) {
|
|
174
|
+
putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
|
|
175
|
+
}
|
|
176
|
+
putCustom(record, "ar", snap.amtRelay);
|
|
177
|
+
}
|
|
178
|
+
/** The `e=ce` counter report. */
|
|
179
|
+
#counters() {
|
|
180
|
+
const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
|
|
181
|
+
const stats = this.#lastStats;
|
|
182
|
+
if (stats) {
|
|
183
|
+
putCustom(record, "fa", stats.config.algorithm);
|
|
184
|
+
putCustom(record, "fk", stats.config.k);
|
|
185
|
+
putCustom(record, "fp", stats.config.repairCount);
|
|
186
|
+
putCustom(record, "pr", stats.sourceSymbols);
|
|
187
|
+
putCustom(record, "pl", stats.sourcePacketsMissing);
|
|
188
|
+
putCustom(record, "pu", stats.sourcePacketsUnrecovered);
|
|
189
|
+
putCustom(record, "fbf", stats.blocksFailed);
|
|
190
|
+
}
|
|
191
|
+
if (this.#lifecycle) {
|
|
192
|
+
putCustom(record, "fbr", this.#lifecycle.inband);
|
|
193
|
+
putCustom(record, "fbq", this.#lifecycle.fetch);
|
|
194
|
+
putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
|
|
195
|
+
}
|
|
196
|
+
this.#putTransport(record);
|
|
197
|
+
if (this.#playMode !== null) {
|
|
198
|
+
putCustom(record, "pm", token(this.#playMode));
|
|
199
|
+
if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
|
|
200
|
+
putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (this.#fecFailClosed) {
|
|
204
|
+
putCustom(record, "ffc", token(this.#fecFailClosed.code));
|
|
205
|
+
putCustom(record, "ffl", this.#fecFailClosed.layer);
|
|
206
|
+
}
|
|
207
|
+
if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
|
|
208
|
+
if (this.#manifestStats) {
|
|
209
|
+
putCustom(record, "mv", this.#manifestStats.verified);
|
|
210
|
+
putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
|
|
179
211
|
}
|
|
180
|
-
|
|
181
|
-
params["x-bc-ct"] = event;
|
|
182
|
-
this.#emit(params);
|
|
212
|
+
return record;
|
|
183
213
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
this.#onCmcdParams?.(params);
|
|
187
|
-
this.#onCmcdPost?.(formatQueryString(params));
|
|
214
|
+
#post(records) {
|
|
215
|
+
this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
|
|
188
216
|
}
|
|
189
217
|
};
|
|
190
218
|
export {
|
|
191
|
-
CmcdProducer
|
|
219
|
+
CmcdProducer,
|
|
220
|
+
encodeCmcdRecord
|
|
192
221
|
};
|
|
193
222
|
//# sourceMappingURL=cmcd-producer.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/cmcd-producer.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * CmcdProducer \u2014 transport-agnostic CMCD telemetry producer.\n *\n * Maps FecTrackStats snapshots and TransportSnapshot state to x-bc-*\n * CMCD vendor keys (proto tags 70-99). Fires callbacks for two delivery\n * channels: MoQ SUBSCRIBE params and multicast HTTP POST.\n *\n * Does NOT call fetch() or write to QUIC directly \u2014 the player wiring\n * layer is responsible for attaching params to SUBSCRIBE or POSTing\n * the payload to the CDN CMCD endpoint.\n *\n * @see docs/superpowers/specs/2026-04-15-iwa-multicast-stack-design.md \u00A73.3\n * @see CDN+ Track D BLO-1044\n */\n\nimport type { FecTrackStats } from '@blockcast/fec-worker'\nimport type { FecFailClosedReason } from './fec-manager.js'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Source-auth counters for CMCD: records verified, and drops per reason. */\nexport interface ManifestVerificationStats {\n verified: number\n failures: Record<string, number>\n}\n\n/** Transport state snapshot, built by the player wiring layer from SocketPool. */\nexport interface TransportSnapshot {\n transport: 'native' | 'amt' | 'moq'\n /** IPv4/IPv6 multicast group address */\n multicastGroup?: string\n /** SSM source address */\n multicastSource?: string\n /** AMT relay address (omit when undefined) */\n amtRelay?: string\n}\n\n/** Transport transition event identifiers for x-bc-ct. */\nexport type TransportTransitionEvent = 'mc-start' | 'mc-to-uc' | 'amt-start'\n\n/**\n * Phase 14 play-mode state (D-12, x-bc-play-mode):\n * - `full` \u2014 base + delta both active\n * - `base-only` \u2014 delta fell back; base carries playback\n * - `single-track` \u2014 stream is not a multi-track switching set (legacy)\n */\nexport type PlayMode = 'full' | 'base-only' | 'single-track'\n\nconst PLAY_MODES: readonly PlayMode[] = ['full', 'base-only', 'single-track']\n\n/** CMCD vendor key map. Values are string, number, or boolean. */\nexport type CmcdParams = Record<string, string | number | boolean>\n\n/** Options for CmcdProducer construction. */\nexport interface CmcdProducerOptions {\n /** Fired on each snapshot and on transport transitions. Params for MoQ SUBSCRIBE. */\n onCmcdParams?: (params: CmcdParams) => void\n /** Fired on each snapshot and on transport transitions. RFC 9559 query-string for HTTP POST body. */\n onCmcdPost?: (payload: string) => void\n}\n\n/** Snapshot data passed to onSnapshot(). */\nexport interface SnapshotData {\n tracks: FecTrackStats[]\n timestampMs: number\n}\n\n/** Transport-only telemetry for receivers that do not own FEC track stats. */\nexport interface TransportOnlySnapshotData {\n transport: TransportSnapshot\n timestampMs: number\n playMode?: PlayMode\n switchMiscount?: number\n}\n\n// ---------------------------------------------------------------------------\n// Value sanitization (T-06-01: header injection mitigation)\n// ---------------------------------------------------------------------------\n\n/** Strip commas, newlines, and carriage returns from string values. */\nfunction sanitize(value: string): string {\n return value.replace(/[,\\r\\n]/g, '')\n}\n\n// ---------------------------------------------------------------------------\n// Key building (pure functions, no side effects)\n// ---------------------------------------------------------------------------\n\n/** Build FEC vendor keys from a FecTrackStats snapshot. */\nfunction buildFecParams(stats: FecTrackStats): CmcdParams {\n const params: CmcdParams = {}\n const cfg = stats.config\n\n params['x-bc-fec-algo'] = cfg.algorithm\n params['x-bc-fec-k'] = cfg.k\n // P as this receiver JOINED it: the worker is configured from\n // TrackSessionConfig.joinedRepairCount, the sum of repairSymbols over the\n // joined base layers. Not the catalog total, which a receiver on a clean path\n // does not receive, and never including a keyframe overlay (separate FEC\n // instance with its own RSB_length).\n params['x-bc-fec-p'] = cfg.repairCount\n params['x-bc-fec-recovery'] = parseFloat((stats.recoveryRate / 100).toFixed(4))\n\n return params\n}\n\n/** Build transport vendor keys from a TransportSnapshot. */\nfunction buildTransportParams(snap: TransportSnapshot | undefined): CmcdParams {\n if (!snap) return {}\n\n const params: CmcdParams = {}\n params['x-bc-transport'] = snap.transport\n\n if (snap.multicastGroup != null) {\n params['x-bc-multicast-group'] = snap.multicastGroup\n }\n if (snap.multicastSource != null) {\n params['x-bc-multicast-source'] = snap.multicastSource\n }\n if (snap.amtRelay != null) {\n params['x-bc-amt-relay'] = snap.amtRelay\n }\n\n return params\n}\n\n// ---------------------------------------------------------------------------\n// Query-string serialization (RFC 9559 \u00A72)\n// ---------------------------------------------------------------------------\n\n/**\n * Serialize CmcdParams to RFC 9559 query-string format.\n * Keys sorted for deterministic output. String values sanitized.\n */\nfunction formatQueryString(params: CmcdParams): string {\n return Object.keys(params)\n .sort()\n .map(key => {\n const v = params[key]\n if (typeof v === 'string') return `${key}=${sanitize(v)}`\n return `${key}=${v}`\n })\n .join(',')\n}\n\n// ---------------------------------------------------------------------------\n// CmcdProducer\n// ---------------------------------------------------------------------------\n\nexport class CmcdProducer {\n #onCmcdParams: ((params: CmcdParams) => void) | undefined\n #onCmcdPost: ((payload: string) => void) | undefined\n #transport: TransportSnapshot | undefined\n #lastStats: FecTrackStats | undefined\n // Phase 14 state \u2014 Plan 03 Task 2 (D-12, D-15).\n #playMode: PlayMode | null = null\n #deltaFailRate: number | null = null\n #switchMiscount = 0\n #manifestStats: ManifestVerificationStats | null = null\n #fecFailClosed: FecFailClosedReason | null = null\n\n constructor(options: CmcdProducerOptions) {\n this.#onCmcdParams = options.onCmcdParams\n this.#onCmcdPost = options.onCmcdPost\n }\n\n /** Update the stored transport state. Called by player wiring layer. */\n updateTransport(snapshot: TransportSnapshot): void {\n this.#transport = snapshot\n }\n\n /**\n * Set the current play-mode. Emitted as `x-bc-play-mode` on subsequent\n * snapshots. Throws on any value outside {'full','base-only','single-track'}\n * (CLAUDE.md: error on bad init).\n */\n updatePlayMode(mode: PlayMode): void {\n if (typeof mode !== 'string' || !PLAY_MODES.includes(mode as PlayMode)) {\n throw new Error(\n `[CmcdProducer] invalid play-mode \"${String(mode)}\"; expected one of: ${PLAY_MODES.join(', ')}`,\n )\n }\n this.#playMode = mode\n }\n\n /**\n * Set the delta-track FEC block-failure rate (0..1 inclusive). Emitted as\n * `x-bc-delta-fail-rate` on snapshots when play-mode is 'full' or 'base-only'.\n * Omitted on single-track streams.\n */\n updateDeltaFailRate(rate: number): void {\n if (typeof rate !== 'number' || !Number.isFinite(rate) || rate < 0 || rate > 1) {\n throw new Error(\n `[CmcdProducer] invalid delta-fail-rate \"${String(rate)}\"; expected a finite number in [0, 1]`,\n )\n }\n this.#deltaFailRate = rate\n }\n\n /**\n * Bump the cumulative switching-set misalignment counter. Emitted as\n * `x-bc-switch-miscount` on snapshots when the counter is > 0.\n */\n incrementSwitchMiscount(): void {\n this.#switchMiscount += 1\n }\n\n /**\n * Record that FEC failed closed at runtime (fec-01 6.3.9). Every later\n * snapshot carries `x-bc-fec-fail-closed` (the reason code) and\n * `x-bc-fec-fail-layer` (the held layer i whose total B_i was undercut), so a\n * stream playing without FEC is told apart from one with no loss.\n */\n updateFecFailClosed(reason: FecFailClosedReason): void {\n this.#fecFailClosed = reason\n }\n\n updateManifestVerification(stats: ManifestVerificationStats): void {\n this.#manifestStats = stats\n }\n\n /**\n * One-shot emit of `x-bc-switch-drift-us` for a single misalignment event.\n * Does NOT mutate snapshot state \u2014 subsequent onSnapshot() calls will not\n * include the drift key until the next emitSwitchDrift(). Mirrors the\n * existing onTransition() pattern.\n */\n emitSwitchDrift(driftUs: number): void {\n if (\n typeof driftUs !== 'number' ||\n !Number.isFinite(driftUs) ||\n driftUs < 0\n ) {\n throw new Error(\n `[CmcdProducer] invalid drift-us \"${String(driftUs)}\"; expected a non-negative finite number`,\n )\n }\n const params: CmcdParams = {}\n if (this.#lastStats) Object.assign(params, buildFecParams(this.#lastStats))\n Object.assign(params, buildTransportParams(this.#transport))\n if (this.#switchMiscount > 0) params['x-bc-switch-miscount'] = this.#switchMiscount\n params['x-bc-switch-drift-us'] = driftUs\n this.#emit(params)\n }\n\n /**\n * Process a FecTrackStats snapshot. Finds the video track, builds\n * x-bc-* vendor keys, and fires both delivery callbacks.\n */\n onSnapshot(data: SnapshotData): void {\n // Find first video track; skip if none\n const videoTrack = data.tracks.find(t => t.config.trackType === 'video')\n if (!videoTrack) return\n\n this.#lastStats = videoTrack\n\n const params: CmcdParams = {\n ...buildFecParams(videoTrack),\n ...buildTransportParams(this.#transport),\n }\n\n if (this.#playMode !== null) {\n params['x-bc-play-mode'] = this.#playMode\n }\n\n if (this.#fecFailClosed) {\n params['x-bc-fec-fail-closed'] = this.#fecFailClosed.code\n params['x-bc-fec-fail-layer'] = this.#fecFailClosed.layer\n }\n\n // Delta-fail-rate is only meaningful when a delta track exists.\n if (\n this.#playMode !== null &&\n this.#playMode !== 'single-track' &&\n this.#deltaFailRate !== null\n ) {\n params['x-bc-delta-fail-rate'] = parseFloat(this.#deltaFailRate.toFixed(4))\n }\n\n if (this.#switchMiscount > 0) {\n params['x-bc-switch-miscount'] = this.#switchMiscount\n }\n if (this.#manifestStats) {\n params['x-bc-manifest-verified'] = this.#manifestStats.verified > 0\n params['x-bc-manifest-failures'] = Object.values(this.#manifestStats.failures)\n .reduce((total, count) => total + count, 0)\n }\n\n this.#emit(params)\n }\n\n /**\n * Emit transport state without inventing FEC metrics. Cadence is owned by\n * the caller because only the transport integration knows when state changed.\n */\n onTransportSnapshot(data: TransportOnlySnapshotData): void {\n if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {\n throw new Error('[CmcdProducer] transport snapshot timestamp must be a non-negative finite number')\n }\n if (data.playMode !== undefined) this.updatePlayMode(data.playMode)\n if (\n data.switchMiscount !== undefined &&\n (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0)\n ) {\n throw new Error('[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer')\n }\n\n this.#transport = data.transport\n const params = buildTransportParams(data.transport)\n if (this.#playMode !== null) params['x-bc-play-mode'] = this.#playMode\n const switchMiscount = data.switchMiscount ?? this.#switchMiscount\n if (switchMiscount > 0) params['x-bc-switch-miscount'] = switchMiscount\n this.#emit(params)\n }\n\n /**\n * Signal a transport transition event. Fires an immediate out-of-band\n * emit with x-bc-ct set. The subsequent onSnapshot() will NOT include\n * x-bc-ct (one-shot).\n */\n onTransition(event: TransportTransitionEvent): void {\n const params: CmcdParams = {}\n\n // Include last known FEC stats if available\n if (this.#lastStats) {\n Object.assign(params, buildFecParams(this.#lastStats))\n }\n\n // Include transport state if available\n Object.assign(params, buildTransportParams(this.#transport))\n\n // Add the transition event key (one-shot)\n params['x-bc-ct'] = event\n\n this.#emit(params)\n }\n\n /** Fire both delivery callbacks with the given params. */\n #emit(params: CmcdParams): void {\n this.#onCmcdParams?.(params)\n this.#onCmcdPost?.(formatQueryString(params))\n }\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["/**\n * CmcdProducer \u2014 CMCD v2 Event-mode reports (CTA-5004-B \u00A72.2) from a player.\n *\n * Maps FecTrackStats snapshots, repair lifecycle events and transport state to\n * newline-separated `application/cmcd` records (CTA-5004-B \u00A73.3), each an\n * RFC 8941 dictionary carrying `v=2`, `ts`, `e`, `sn` and `sid`:\n *\n * - `e=t` \u2014 time-interval heartbeat, Table 1 keys only;\n * - `e=ce` \u2014 custom event `cen=\"com.blockcast-fec\"` carrying the\n * `com.blockcast-*` counters at the heartbeat cadence. Custom keys ride\n * only on `e=ce`: \u00A72.2 admits on Event reports only Table 1 keys, and the\n * Table 1 `cen` row admits a custom key-value pair with a custom event;\n * - `e=e` \u2014 FEC failed closed, with `ec`.\n *\n * Custom values are STRING or TOKEN of at most 64 characters (\u00A74.1): numbers\n * go out as strings. A value that is unknown is omitted, never sent as zero\n * (\u00A73). Counters are absolute since session start, so a lost report loses\n * nothing.\n *\n * Does NOT call fetch(): the player POSTs the body handed to `onCmcdPost` with\n * `Content-Type: application/cmcd`. The heartbeat is driven by the snapshots\n * the player already feeds; there is no timer here.\n */\n\nimport type { FecTrackStats } from '@blockcast/fec-worker'\nimport type { FecFailClosedReason } from './fec-manager.js'\nimport type { RepairLifecycleEvent } from './multicast-manager.js'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Source-auth counters: records verified, and drops per reason. */\nexport interface ManifestVerificationStats {\n verified: number\n failures: Record<string, number>\n}\n\n/** Transport state snapshot, built by the player wiring layer from SocketPool. */\nexport interface TransportSnapshot {\n /**\n * CMCD has no user-agent value: report a `'user-agent'` join\n * (`ResolvedMulticastTransport`, the browser's own MulticastReceiver) as\n * `'native'`, since the browser does not say which leg it chose.\n */\n transport: 'native' | 'amt' | 'moq'\n /** IPv4/IPv6 multicast group address */\n multicastGroup?: string\n /** SSM source address */\n multicastSource?: string\n /** AMT relay address (omit when undefined) */\n amtRelay?: string\n}\n\n/** Transport transition event identifiers for `com.blockcast-ct`. */\nexport type TransportTransitionEvent = 'mc-start' | 'mc-to-uc' | 'amt-start'\n\n/**\n * Play-mode state (`com.blockcast-pm`):\n * - `full` \u2014 base + delta both active\n * - `base-only` \u2014 delta fell back; base carries playback\n * - `single-track` \u2014 stream is not a multi-track switching set (legacy)\n */\nexport type PlayMode = 'full' | 'base-only' | 'single-track'\n\nconst PLAY_MODES: readonly PlayMode[] = ['full', 'base-only', 'single-track']\n\n/** An RFC 8941 token (bare, unquoted). */\nexport interface CmcdToken {\n readonly token: string\n}\n\n/** A record value: integer, string, token, or inner list of strings. */\nexport type CmcdValue = number | string | CmcdToken | readonly string[]\n\n/** One CMCD record, before encoding. */\nexport type CmcdRecord = Record<string, CmcdValue>\n\n/** Options for CmcdProducer construction. */\nexport interface CmcdProducerOptions {\n /** Fired with an `application/cmcd` body: one record per line. */\n onCmcdPost?: (body: string) => void\n /** Heartbeat period for the `e=t` and counter reports. Default 30 s (CTA-5004-B \u00A75). */\n intervalMs?: number\n /** CTA-5004-B `sid`. Default: a random UUID per producer. */\n sessionId?: string\n /** Wall clock in ms since the epoch, for `ts` and the heartbeat. Default `Date.now`. */\n now?: () => number\n}\n\n/** Snapshot data passed to onSnapshot(). */\nexport interface SnapshotData {\n tracks: FecTrackStats[]\n timestampMs: number\n}\n\n/** Transport-only telemetry for receivers that do not own FEC track stats. */\nexport interface TransportOnlySnapshotData {\n transport: TransportSnapshot\n timestampMs: number\n playMode?: PlayMode\n switchMiscount?: number\n}\n\nconst DEFAULT_INTERVAL_MS = 30_000\nconst CUSTOM_PREFIX = 'com.blockcast-'\n/** CTA-5004-B \u00A74.1: a custom value is at most 64 characters. */\nconst MAX_CUSTOM_VALUE = 64\n\nconst TRANSPORT_TOKEN: Record<TransportSnapshot['transport'], string> = {\n native: 'ssm',\n amt: 'amt',\n moq: 'moq',\n}\n\n// ---------------------------------------------------------------------------\n// Encoding (RFC 8941 dictionary, keys sorted \u2014 CTA-5004-B \u00A73)\n// ---------------------------------------------------------------------------\n\n/** RFC 8941 \u00A73.3.3 string: printable ASCII, `\\` and `\"` escaped. */\nfunction sfString(value: string): string {\n return `\"${value.replace(/[^\\x20-\\x7e]/g, '').replace(/[\\\\\"]/g, '\\\\$&')}\"`\n}\n\n/** Encode one record as an RFC 8941 dictionary with keys in alphabetical order. */\nexport function encodeCmcdRecord(record: CmcdRecord): string {\n return Object.keys(record)\n .sort()\n .map(key => {\n const value = record[key]\n if (typeof value === 'number') return `${key}=${value}`\n if (typeof value === 'string') return `${key}=${sfString(value)}`\n if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(' ')})`\n return `${key}=${(value as CmcdToken).token}`\n })\n .join(',')\n}\n\nconst token = (value: string): CmcdToken => ({ token: value })\n\n/** Set a custom key, numbers as strings; drop anything absent or over 64 characters. */\nfunction putCustom(record: CmcdRecord, name: string, value: string | number | CmcdToken | undefined): void {\n if (value === undefined) return\n if (typeof value === 'number') value = String(value)\n if (typeof value === 'string' && value.length > MAX_CUSTOM_VALUE) return\n record[CUSTOM_PREFIX + name] = value\n}\n\n// ---------------------------------------------------------------------------\n// CmcdProducer\n// ---------------------------------------------------------------------------\n\nexport class CmcdProducer {\n #onCmcdPost: ((body: string) => void) | undefined\n #intervalMs: number\n #sessionId: string\n #now: () => number\n #sn = 0\n #lastHeartbeat: number | null = null\n\n #transport: TransportSnapshot | undefined\n #lastStats: FecTrackStats | undefined\n #playMode: PlayMode | null = null\n #deltaFailRate: number | null = null\n #switchMiscount = 0\n #manifestStats: ManifestVerificationStats | null = null\n #fecFailClosed: FecFailClosedReason | null = null\n /** Video-block resolutions from the repair lifecycle; null until the player feeds one. */\n #lifecycle: { inband: number; fetch: number; failedKeyframe: number } | null = null\n\n constructor(options: CmcdProducerOptions) {\n this.#onCmcdPost = options.onCmcdPost\n this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error(`[CmcdProducer] invalid intervalMs \"${String(options.intervalMs)}\"; expected a positive number`)\n }\n this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID()\n this.#now = options.now ?? Date.now\n }\n\n /** Update the stored transport state. Called by player wiring layer. */\n updateTransport(snapshot: TransportSnapshot): void {\n this.#transport = snapshot\n }\n\n /**\n * Set the current play-mode, reported as `com.blockcast-pm`. Throws on any\n * value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).\n */\n updatePlayMode(mode: PlayMode): void {\n if (typeof mode !== 'string' || !PLAY_MODES.includes(mode as PlayMode)) {\n throw new Error(\n `[CmcdProducer] invalid play-mode \"${String(mode)}\"; expected one of: ${PLAY_MODES.join(', ')}`,\n )\n }\n this.#playMode = mode\n }\n\n /**\n * Set the delta-track FEC block-failure rate (0..1 inclusive), reported as\n * `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.\n */\n updateDeltaFailRate(rate: number): void {\n if (typeof rate !== 'number' || !Number.isFinite(rate) || rate < 0 || rate > 1) {\n throw new Error(\n `[CmcdProducer] invalid delta-fail-rate \"${String(rate)}\"; expected a finite number in [0, 1]`,\n )\n }\n this.#deltaFailRate = rate\n }\n\n /** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */\n incrementSwitchMiscount(): void {\n this.#switchMiscount += 1\n }\n\n /**\n * Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once\n * as `e=e` with the reason code in `ec`, and on every later counter report\n * as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total\n * B_i was undercut), so a stream playing without FEC is told apart from one\n * with no loss.\n */\n updateFecFailClosed(reason: FecFailClosedReason): void {\n const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer\n this.#fecFailClosed = reason\n if (changed) this.#post([{ ...this.#base('e'), ec: [reason.code] }])\n }\n\n updateManifestVerification(stats: ManifestVerificationStats): void {\n this.#manifestStats = stats\n }\n\n /**\n * Count a video block's resolution from the repair lifecycle: recovered in\n * band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed\n * as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching\n * the video-track `-fbf`. Until the player feeds this, the three are absent.\n */\n onRepairLifecycle(event: RepairLifecycleEvent): void {\n this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 }\n if (event.type === 'recovered' && event.blockClass !== 'audio') {\n if (event.via === 'inband') this.#lifecycle.inband++\n else this.#lifecycle.fetch++\n } else if (event.type === 'failed' && event.blockClass === 'keyframe') {\n this.#lifecycle.failedKeyframe++\n }\n }\n\n /** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */\n emitSwitchDrift(driftUs: number): void {\n if (typeof driftUs !== 'number' || !Number.isFinite(driftUs) || driftUs < 0) {\n throw new Error(\n `[CmcdProducer] invalid drift-us \"${String(driftUs)}\"; expected a non-negative finite number`,\n )\n }\n const record: CmcdRecord = { ...this.#base('ce'), cen: CUSTOM_PREFIX + 'switch-drift' }\n putCustom(record, 'sdu', Math.round(driftUs))\n this.#post([record])\n }\n\n /** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */\n onSnapshot(data: SnapshotData): void {\n const videoTrack = data.tracks.find(t => t.config.trackType === 'video')\n if (!videoTrack) return\n this.#lastStats = videoTrack\n this.#heartbeat()\n }\n\n /**\n * Take transport state for receivers that do not own FEC stats, and send the\n * heartbeat when due.\n */\n onTransportSnapshot(data: TransportOnlySnapshotData): void {\n if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {\n throw new Error('[CmcdProducer] transport snapshot timestamp must be a non-negative finite number')\n }\n if (data.playMode !== undefined) this.updatePlayMode(data.playMode)\n if (data.switchMiscount !== undefined) {\n if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {\n throw new Error('[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer')\n }\n this.#switchMiscount = data.switchMiscount\n }\n this.#transport = data.transport\n this.#heartbeat()\n }\n\n /** Report a transport transition at once, as custom event `com.blockcast-ct`. */\n onTransition(event: TransportTransitionEvent): void {\n const record: CmcdRecord = { ...this.#base('ce'), cen: CUSTOM_PREFIX + 'ct' }\n putCustom(record, 'ct', token(event))\n this.#putTransport(record)\n this.#post([record])\n }\n\n #base(event: string): CmcdRecord {\n return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 }\n }\n\n #heartbeat(): void {\n const now = this.#now()\n if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return\n this.#lastHeartbeat = now\n this.#post([this.#base('t'), this.#counters()])\n }\n\n #putTransport(record: CmcdRecord): void {\n const snap = this.#transport\n if (!snap) return\n putCustom(record, 'tr', token(TRANSPORT_TOKEN[snap.transport]))\n if (snap.multicastGroup != null && snap.multicastSource != null) {\n putCustom(record, 'sg', `${snap.multicastGroup}@${snap.multicastSource}`)\n }\n putCustom(record, 'ar', snap.amtRelay)\n }\n\n /** The `e=ce` counter report. */\n #counters(): CmcdRecord {\n const record: CmcdRecord = { ...this.#base('ce'), cen: CUSTOM_PREFIX + 'fec' }\n const stats = this.#lastStats\n if (stats) {\n putCustom(record, 'fa', stats.config.algorithm)\n putCustom(record, 'fk', stats.config.k)\n // P as this receiver JOINED it: the sum of repairSymbols over the joined\n // base layers, never including a keyframe overlay.\n putCustom(record, 'fp', stats.config.repairCount)\n putCustom(record, 'pr', stats.sourceSymbols)\n putCustom(record, 'pl', stats.sourcePacketsMissing)\n putCustom(record, 'pu', stats.sourcePacketsUnrecovered)\n putCustom(record, 'fbf', stats.blocksFailed)\n }\n if (this.#lifecycle) {\n putCustom(record, 'fbr', this.#lifecycle.inband)\n putCustom(record, 'fbq', this.#lifecycle.fetch)\n putCustom(record, 'fbfk', this.#lifecycle.failedKeyframe)\n }\n this.#putTransport(record)\n if (this.#playMode !== null) {\n putCustom(record, 'pm', token(this.#playMode))\n if (this.#playMode !== 'single-track' && this.#deltaFailRate !== null) {\n putCustom(record, 'dfr', this.#deltaFailRate.toFixed(3))\n }\n }\n if (this.#fecFailClosed) {\n putCustom(record, 'ffc', token(this.#fecFailClosed.code))\n putCustom(record, 'ffl', this.#fecFailClosed.layer)\n }\n if (this.#switchMiscount > 0) putCustom(record, 'sm', this.#switchMiscount)\n if (this.#manifestStats) {\n putCustom(record, 'mv', this.#manifestStats.verified)\n putCustom(record, 'mf', Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0))\n }\n return record\n }\n\n #post(records: CmcdRecord[]): void {\n this.#onCmcdPost?.(records.map(encodeCmcdRecord).join('\\n'))\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/delivery-catalog.js
CHANGED
|
@@ -1268,14 +1268,15 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
|
|
|
1268
1268
|
});
|
|
1269
1269
|
|
|
1270
1270
|
// node_modules/@blockcast/mmt-transport/dist/amt-gateway.js
|
|
1271
|
+
var MAX_PENDING_PACKETS = 256;
|
|
1271
1272
|
var AMTGatewayManager = class {
|
|
1272
1273
|
subscriptions = /* @__PURE__ */ new Map();
|
|
1273
1274
|
messageHandler;
|
|
1275
|
+
messageListenerRegistered = false;
|
|
1274
1276
|
pendingPackets = /* @__PURE__ */ new Map();
|
|
1275
1277
|
constructor() {
|
|
1276
1278
|
this.messageHandler = (event) => {
|
|
1277
1279
|
const data = event.data;
|
|
1278
|
-
console.log(`[AMT] SW message received:`, data.type);
|
|
1279
1280
|
if (data.type === "UDP_PACKET") {
|
|
1280
1281
|
this.handlePacket(data);
|
|
1281
1282
|
} else if (data.type === "SOCKET_ERROR") {
|
|
@@ -1284,12 +1285,22 @@ var AMTGatewayManager = class {
|
|
|
1284
1285
|
console.log(`[AMT SW Debug] ${data.message}`);
|
|
1285
1286
|
}
|
|
1286
1287
|
};
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1288
|
+
}
|
|
1289
|
+
ensureMessageListener() {
|
|
1290
|
+
if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
navigator.serviceWorker.addEventListener("message", this.messageHandler);
|
|
1294
|
+
this.messageListenerRegistered = true;
|
|
1295
|
+
console.log("[AMT] Registered service worker message handler");
|
|
1296
|
+
}
|
|
1297
|
+
removeMessageListener() {
|
|
1298
|
+
if (!this.messageListenerRegistered)
|
|
1299
|
+
return;
|
|
1300
|
+
if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
|
|
1301
|
+
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
1292
1302
|
}
|
|
1303
|
+
this.messageListenerRegistered = false;
|
|
1293
1304
|
}
|
|
1294
1305
|
/**
|
|
1295
1306
|
* Subscribe via AMT tunnel
|
|
@@ -1299,6 +1310,7 @@ var AMTGatewayManager = class {
|
|
|
1299
1310
|
if (config.protected) {
|
|
1300
1311
|
throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
|
|
1301
1312
|
}
|
|
1313
|
+
this.ensureMessageListener();
|
|
1302
1314
|
const keyStr = this.getKeyString(key);
|
|
1303
1315
|
if (this.subscriptions.has(keyStr)) {
|
|
1304
1316
|
console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
|
|
@@ -1334,6 +1346,8 @@ var AMTGatewayManager = class {
|
|
|
1334
1346
|
this.subscriptions.delete(keyStr);
|
|
1335
1347
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
1336
1348
|
}
|
|
1349
|
+
if (this.subscriptions.size === 0)
|
|
1350
|
+
this.removeMessageListener();
|
|
1337
1351
|
throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
|
|
1338
1352
|
}
|
|
1339
1353
|
}
|
|
@@ -1355,6 +1369,8 @@ var AMTGatewayManager = class {
|
|
|
1355
1369
|
} finally {
|
|
1356
1370
|
this.subscriptions.delete(handle);
|
|
1357
1371
|
this.pendingPackets.delete(subscription.subscriptionId);
|
|
1372
|
+
if (this.subscriptions.size === 0)
|
|
1373
|
+
this.removeMessageListener();
|
|
1358
1374
|
}
|
|
1359
1375
|
}
|
|
1360
1376
|
/**
|
|
@@ -1371,14 +1387,12 @@ var AMTGatewayManager = class {
|
|
|
1371
1387
|
if (!subscriptionId)
|
|
1372
1388
|
return;
|
|
1373
1389
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
1374
|
-
if (!subscription)
|
|
1375
|
-
console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
|
|
1390
|
+
if (!subscription)
|
|
1376
1391
|
return;
|
|
1377
|
-
}
|
|
1378
1392
|
if (!subscription.handshakeComplete) {
|
|
1379
1393
|
const queue = this.pendingPackets.get(subscriptionId);
|
|
1380
1394
|
const packetData2 = data;
|
|
1381
|
-
if (queue) {
|
|
1395
|
+
if (queue && queue.length < MAX_PENDING_PACKETS) {
|
|
1382
1396
|
queue.push({
|
|
1383
1397
|
data: packetData2.data,
|
|
1384
1398
|
remoteAddress: packetData2.remoteAddress,
|
|
@@ -1404,19 +1418,15 @@ var AMTGatewayManager = class {
|
|
|
1404
1418
|
if (!subscriptionId)
|
|
1405
1419
|
return;
|
|
1406
1420
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
1407
|
-
if (!subscription)
|
|
1408
|
-
console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
|
|
1421
|
+
if (!subscription)
|
|
1409
1422
|
return;
|
|
1410
|
-
}
|
|
1411
1423
|
subscription.config.onError(new Error(error || "Unknown error"));
|
|
1412
1424
|
}
|
|
1413
1425
|
/**
|
|
1414
1426
|
* Cleanup on destroy
|
|
1415
1427
|
*/
|
|
1416
1428
|
destroy() {
|
|
1417
|
-
|
|
1418
|
-
navigator.serviceWorker.removeEventListener("message", this.messageHandler);
|
|
1419
|
-
}
|
|
1429
|
+
this.removeMessageListener();
|
|
1420
1430
|
const handles = Array.from(this.subscriptions.keys());
|
|
1421
1431
|
for (const handle of handles) {
|
|
1422
1432
|
this.unsubscribe(handle).catch((err) => {
|