@blockcast/mmt-container 2.2.0-main.c4fa878f4ff4 → 2.2.0-main.da2e7cd1dd5d

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.
Files changed (62) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +241 -149
  3. package/dist/browser-runtime.js.map +3 -3
  4. package/dist/catalog-fec-params.d.ts +17 -1
  5. package/dist/catalog-fec-params.d.ts.map +1 -1
  6. package/dist/catalog-fec-params.js +15 -0
  7. package/dist/catalog-fec-params.js.map +2 -2
  8. package/dist/cmcd-producer.d.ts +60 -47
  9. package/dist/cmcd-producer.d.ts.map +1 -1
  10. package/dist/cmcd-producer.js +138 -109
  11. package/dist/cmcd-producer.js.map +2 -2
  12. package/dist/delivery-catalog.js +24 -15
  13. package/dist/delivery-catalog.js.map +2 -2
  14. package/dist/delta-fallback-config.js.map +1 -1
  15. package/dist/fec-manager.d.ts +3 -1
  16. package/dist/fec-manager.d.ts.map +1 -1
  17. package/dist/fec-manager.js +26 -4
  18. package/dist/fec-manager.js.map +2 -2
  19. package/dist/fec-worker-setup.d.ts +1 -1
  20. package/dist/fec-worker-setup.js +1 -1
  21. package/dist/fec-worker-setup.js.map +1 -1
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -1
  25. package/dist/index.js.map +2 -2
  26. package/dist/media-router.js.map +1 -1
  27. package/dist/mmtp-container-parser.d.ts +2 -0
  28. package/dist/mmtp-container-parser.d.ts.map +1 -1
  29. package/dist/mmtp-container-parser.js +4 -0
  30. package/dist/mmtp-container-parser.js.map +2 -2
  31. package/dist/mmtp-container-parser.no-modules.js +4 -0
  32. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  33. package/dist/mmtp-decode-pipeline.js +4 -0
  34. package/dist/mmtp-decode-pipeline.js.map +2 -2
  35. package/dist/mmtp-router.d.ts.map +1 -1
  36. package/dist/mmtp-router.js +38 -15
  37. package/dist/mmtp-router.js.map +2 -2
  38. package/dist/multicast-manager.js +64 -19
  39. package/dist/multicast-manager.js.map +2 -2
  40. package/dist/multicast-transport.js +24 -15
  41. package/dist/multicast-transport.js.map +2 -2
  42. package/dist/no-modules.js +4 -0
  43. package/dist/no-modules.js.map +2 -2
  44. package/dist/repair-layer-subscriptions.d.ts +6 -2
  45. package/dist/repair-layer-subscriptions.d.ts.map +1 -1
  46. package/dist/repair-layer-subscriptions.js +6 -3
  47. package/dist/repair-layer-subscriptions.js.map +2 -2
  48. package/dist/track-session.d.ts +1 -1
  49. package/dist/track-session.js.map +1 -1
  50. package/dist/wasm-mmtp-container-parser.js +4 -0
  51. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  52. package/package.json +8 -5
  53. package/schemas/catalog.schema.json +4 -0
  54. package/src/catalog-fec-params.ts +36 -1
  55. package/src/cmcd-producer.ts +189 -179
  56. package/src/fec-manager.ts +26 -4
  57. package/src/fec-worker-setup.ts +2 -2
  58. package/src/index.ts +6 -2
  59. package/src/mmtp-container-parser.ts +12 -0
  60. package/src/mmtp-router.ts +21 -0
  61. package/src/repair-layer-subscriptions.ts +13 -5
  62. package/src/track-session.ts +1 -1
@@ -7988,8 +7988,8 @@ var MulticastSubscriptionPool = class {
7988
7988
  record.claims--;
7989
7989
  return this.acquire(gateway, config, consumer);
7990
7990
  }
7991
- const token = Symbol(key);
7992
- entry.consumers.set(token, consumer);
7991
+ const token2 = Symbol(key);
7992
+ entry.consumers.set(token2, consumer);
7993
7993
  let released = false;
7994
7994
  return {
7995
7995
  key,
@@ -7997,7 +7997,7 @@ var MulticastSubscriptionPool = class {
7997
7997
  if (released)
7998
7998
  return;
7999
7999
  released = true;
8000
- entry.consumers.delete(token);
8000
+ entry.consumers.delete(token2);
8001
8001
  record.claims--;
8002
8002
  if (record.claims > 0)
8003
8003
  return;
@@ -8749,11 +8749,11 @@ var MOQT_PUBLISHER_PRIORITY = Object.freeze({
8749
8749
  var AMTGatewayManager = class {
8750
8750
  subscriptions = /* @__PURE__ */ new Map();
8751
8751
  messageHandler;
8752
+ messageListenerRegistered = false;
8752
8753
  pendingPackets = /* @__PURE__ */ new Map();
8753
8754
  constructor() {
8754
8755
  this.messageHandler = (event) => {
8755
8756
  const data = event.data;
8756
- console.log(`[AMT] SW message received:`, data.type);
8757
8757
  if (data.type === "UDP_PACKET") {
8758
8758
  this.handlePacket(data);
8759
8759
  } else if (data.type === "SOCKET_ERROR") {
@@ -8762,12 +8762,22 @@ var AMTGatewayManager = class {
8762
8762
  console.log(`[AMT SW Debug] ${data.message}`);
8763
8763
  }
8764
8764
  };
8765
- if (typeof navigator !== "undefined" && navigator.serviceWorker) {
8766
- navigator.serviceWorker.addEventListener("message", this.messageHandler);
8767
- console.log("[AMT] Registered service worker message handler");
8768
- } else {
8769
- console.warn("[AMT] Service worker not available");
8765
+ }
8766
+ ensureMessageListener() {
8767
+ if (this.messageListenerRegistered || typeof navigator === "undefined" || !navigator.serviceWorker || !this.messageHandler) {
8768
+ return;
8769
+ }
8770
+ navigator.serviceWorker.addEventListener("message", this.messageHandler);
8771
+ this.messageListenerRegistered = true;
8772
+ console.log("[AMT] Registered service worker message handler");
8773
+ }
8774
+ removeMessageListener() {
8775
+ if (!this.messageListenerRegistered)
8776
+ return;
8777
+ if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8778
+ navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8770
8779
  }
8780
+ this.messageListenerRegistered = false;
8771
8781
  }
8772
8782
  /**
8773
8783
  * Subscribe via AMT tunnel
@@ -8777,6 +8787,7 @@ var AMTGatewayManager = class {
8777
8787
  if (config.protected) {
8778
8788
  throw new Error("AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)");
8779
8789
  }
8790
+ this.ensureMessageListener();
8780
8791
  const keyStr = this.getKeyString(key);
8781
8792
  if (this.subscriptions.has(keyStr)) {
8782
8793
  console.log(`[AMT] Reusing existing subscription to ${keyStr}`);
@@ -8812,6 +8823,8 @@ var AMTGatewayManager = class {
8812
8823
  this.subscriptions.delete(keyStr);
8813
8824
  this.pendingPackets.delete(subscription.subscriptionId);
8814
8825
  }
8826
+ if (this.subscriptions.size === 0)
8827
+ this.removeMessageListener();
8815
8828
  throw new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);
8816
8829
  }
8817
8830
  }
@@ -8833,6 +8846,8 @@ var AMTGatewayManager = class {
8833
8846
  } finally {
8834
8847
  this.subscriptions.delete(handle);
8835
8848
  this.pendingPackets.delete(subscription.subscriptionId);
8849
+ if (this.subscriptions.size === 0)
8850
+ this.removeMessageListener();
8836
8851
  }
8837
8852
  }
8838
8853
  /**
@@ -8849,10 +8864,8 @@ var AMTGatewayManager = class {
8849
8864
  if (!subscriptionId)
8850
8865
  return;
8851
8866
  const subscription = this.subscriptions.get(subscriptionId);
8852
- if (!subscription) {
8853
- console.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);
8867
+ if (!subscription)
8854
8868
  return;
8855
- }
8856
8869
  if (!subscription.handshakeComplete) {
8857
8870
  const queue = this.pendingPackets.get(subscriptionId);
8858
8871
  const packetData2 = data;
@@ -8882,19 +8895,15 @@ var AMTGatewayManager = class {
8882
8895
  if (!subscriptionId)
8883
8896
  return;
8884
8897
  const subscription = this.subscriptions.get(subscriptionId);
8885
- if (!subscription) {
8886
- console.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);
8898
+ if (!subscription)
8887
8899
  return;
8888
- }
8889
8900
  subscription.config.onError(new Error(error || "Unknown error"));
8890
8901
  }
8891
8902
  /**
8892
8903
  * Cleanup on destroy
8893
8904
  */
8894
8905
  destroy() {
8895
- if (this.messageHandler && typeof navigator !== "undefined" && navigator.serviceWorker) {
8896
- navigator.serviceWorker.removeEventListener("message", this.messageHandler);
8897
- }
8906
+ this.removeMessageListener();
8898
8907
  const handles = Array.from(this.subscriptions.keys());
8899
8908
  for (const handle of handles) {
8900
8909
  this.unsubscribe(handle).catch((err) => {
@@ -14052,6 +14061,10 @@ var FECManager = class _FECManager {
14052
14061
  // ── Keyframe-protected repair policy (block-policy.ts) ──
14053
14062
  // One classifier per physical stream, built from that stream's geometry.
14054
14063
  this.#classifiers = /* @__PURE__ */ new Map();
14064
+ // A stream without catalog timing has no classifier, so the last RAP it
14065
+ // released stands in for one: "an MPU boundary bounds its RAPs" holds until
14066
+ // a second RAP is released inside one MPU (BLO-52085).
14067
+ this.#noSpanRaps = /* @__PURE__ */ new Map();
14055
14068
  // Geometry a stream-scoped configuration gave one stream (streamKey). ABR
14056
14069
  // rungs can share a packetId with a different framerate or GOP, so the
14057
14070
  // packet-level geometry is only the geometry of a packet whose streams
@@ -14251,6 +14264,7 @@ var FECManager = class _FECManager {
14251
14264
  #orderedFrontiers;
14252
14265
  #orderedPendingBlocks;
14253
14266
  #classifiers;
14267
+ #noSpanRaps;
14254
14268
  #streamGeometries;
14255
14269
  #failedBlocks;
14256
14270
  #resolvedBlockClasses;
@@ -14912,6 +14926,7 @@ var FECManager = class _FECManager {
14912
14926
  };
14913
14927
  const discontinuity = () => {
14914
14928
  this.#classifiers.get(streamKey)?.noteSourceDiscontinuity();
14929
+ this.#noSpanRaps.delete(streamKey);
14915
14930
  this.#highestSbn.set(streamKey, { sbn, atMs: now });
14916
14931
  this.#sourceFrontiers.set(streamKey, { sbn, atMs: now });
14917
14932
  if (psn === void 0) this.#sourceAnchors.delete(streamKey);
@@ -15324,6 +15339,9 @@ var FECManager = class _FECManager {
15324
15339
  }
15325
15340
  if (policyChanged) {
15326
15341
  this.#bumpEpoch(packetId);
15342
+ for (const key of [...this.#noSpanRaps.keys()]) {
15343
+ if (this.#belongsToPacket(key, packetId)) this.#noSpanRaps.delete(key);
15344
+ }
15327
15345
  for (const [key, classifier] of [...this.#classifiers]) {
15328
15346
  if (!this.#belongsToPacket(key, packetId)) continue;
15329
15347
  const stream = this.#parseStreamKey(key);
@@ -15559,6 +15577,7 @@ var FECManager = class _FECManager {
15559
15577
  resetMap(this.#isoLocators);
15560
15578
  resetMap(this.#sourceAnchors);
15561
15579
  this.#classifiers.delete(this.#streamKey(packetId, catalogTrackName));
15580
+ this.#noSpanRaps.delete(this.#streamKey(packetId, catalogTrackName));
15562
15581
  this.#bumpEpoch(packetId);
15563
15582
  for (const [sbn, keys] of this.#blockKeysBySbn) {
15564
15583
  for (const key of keys.keys()) {
@@ -15665,6 +15684,7 @@ var FECManager = class _FECManager {
15665
15684
  dropMap(this.#isoLocators);
15666
15685
  dropMap(this.#sourceAnchors);
15667
15686
  dropMap(this.#classifiers);
15687
+ dropMap(this.#noSpanRaps);
15668
15688
  dropMap(this.#streamGeometries);
15669
15689
  dropSet(this.#pendingWorkerCleanups);
15670
15690
  dropSet(this.#completedBlocks);
@@ -16295,11 +16315,14 @@ var FECManager = class _FECManager {
16295
16315
  /**
16296
16316
  * Whether an MPU boundary still bounds this stream's RAPs (see
16297
16317
  * `BlockClassifier.mpuBoundsRaps`). Gap decisions drop MPU evidence once
16298
- * it does not. A stream with no classifier yet has shown nothing against it.
16318
+ * it does not. A stream with no classifier yet has shown nothing against it,
16319
+ * and neither has a stream without catalog timing until it releases two
16320
+ * RAPs inside one MPU.
16299
16321
  */
16300
16322
  mpuBoundsRapsFor(packetId, catalogTrackName) {
16301
- if (!this.#hasCatalogTimingForPacket(packetId)) return false;
16302
- return this.#classifiers.get(this.#streamKey(packetId, catalogTrackName))?.mpuBoundsRaps ?? true;
16323
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16324
+ if (!this.#hasCatalogTimingForPacket(packetId)) return this.#noSpanRaps.get(streamKey)?.inside !== true;
16325
+ return this.#classifiers.get(streamKey)?.mpuBoundsRaps ?? true;
16303
16326
  }
16304
16327
  /** A block's class now, or frozen at its resolution. */
16305
16328
  blockClassFor(sbn, packetId, catalogTrackName) {
@@ -16325,7 +16348,14 @@ var FECManager = class _FECManager {
16325
16348
  * invariant (consecutive MPUs exactly one keyframe interval apart).
16326
16349
  */
16327
16350
  noteReleasedRap(packetId, catalogTrackName, mpu, presentationTime, mediaTimescale) {
16328
- if (!this.#trackConfigs.has(packetId) || !this.#hasCatalogTimingForPacket(packetId)) return;
16351
+ if (!this.#trackConfigs.has(packetId)) return;
16352
+ if (!this.#hasCatalogTimingForPacket(packetId)) {
16353
+ const streamKey = this.#streamKey(packetId, catalogTrackName);
16354
+ const last = this.#noSpanRaps.get(streamKey);
16355
+ const inside = last?.inside === true || last?.mpu === mpu && last.presentationTime !== presentationTime;
16356
+ this.#noSpanRaps.set(streamKey, { mpu, presentationTime, inside });
16357
+ return;
16358
+ }
16329
16359
  this.#applyClassifierChanges(
16330
16360
  packetId,
16331
16361
  catalogTrackName,
@@ -17340,6 +17370,7 @@ var FECManager = class _FECManager {
17340
17370
  this.#blockAnchorsMs.clear();
17341
17371
  this.#blockCatalogTrackNames.clear();
17342
17372
  this.#classifiers.clear();
17373
+ this.#noSpanRaps.clear();
17343
17374
  this.#streamGeometries.clear();
17344
17375
  this.#failedBlocks.clear();
17345
17376
  this.#resolvedBlockClasses.clear();
@@ -18850,10 +18881,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
18850
18881
  get reassemblerDataUnitsReturned() {
18851
18882
  return this.#reassemblerDataUnitsReturned;
18852
18883
  }
18853
- #countOutcome(token) {
18854
- const count = (this.#feedOutcomes.get(token) ?? 0) + 1;
18855
- this.#feedOutcomes.set(token, count);
18856
- this.#lastOutcome = token;
18884
+ #countOutcome(token2) {
18885
+ const count = (this.#feedOutcomes.get(token2) ?? 0) + 1;
18886
+ this.#feedOutcomes.set(token2, count);
18887
+ this.#lastOutcome = token2;
18857
18888
  this.#lastOutcomeCount = count;
18858
18889
  }
18859
18890
  #countDeferredOutcome(reason) {
@@ -19381,6 +19412,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
19381
19412
  }
19382
19413
  this.#emitFrame(data, meta);
19383
19414
  }
19415
+ /** Whether FT=1 metadata already names this sample, without claiming it. */
19416
+ hasAuthoredTimingFor(packetId, mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber, catalogTrackName) {
19417
+ return this.#timingResolver(this.#streamKey(packetId, catalogTrackName), packetId, false)?.sampleTiming(mpuSequenceNumber2, movieFragmentSequenceNumber, sampleNumber) != null;
19418
+ }
19384
19419
  /**
19385
19420
  * Claim and return the authored timing for a recovered sample. Recovery does
19386
19421
  * not pass through the ordinary `feed()` completion path, so it must retire
@@ -25905,6 +25940,16 @@ var MmtpRouter = class {
25905
25940
  }
25906
25941
  );
25907
25942
  }
25943
+ result = {
25944
+ ...result,
25945
+ symbols: result.symbols.filter((sym) => sym.fragmentType !== 2 || sym.fragment.fragmentationIndicator !== 1 || sym.fragment.movieFragmentSequenceNumber === void 0 || sym.fragment.sampleNumber === void 0 || this.#parser.hasAuthoredTimingFor(
25946
+ ownerPacketId,
25947
+ sym.fragment.mpuSequenceNumber,
25948
+ sym.fragment.movieFragmentSequenceNumber,
25949
+ sym.fragment.sampleNumber,
25950
+ catalogTrackName
25951
+ ))
25952
+ };
25908
25953
  const gated = this.#isFecTrack(ownerPacketId);
25909
25954
  const assembler = this.#mseAssemblerFor(ownerPacketId, catalogTrackName);
25910
25955
  if (assembler) {
@@ -29666,7 +29711,7 @@ async function setupFecWorkerForTrack(opts) {
29666
29711
  blockProfile,
29667
29712
  k: opts.fecK,
29668
29713
  // P as this receiver joined it, not the catalog total: the worker snapshot
29669
- // and CMCD `x-bc-fec-p` are read from this. Identity on a single layer.
29714
+ // and CMCD `com.blockcast-fp` are read from this. Identity on a single layer.
29670
29715
  repairCount: opts.joinedRepairCount ?? opts.fecRepairCount,
29671
29716
  symbolSize: opts.fecSymbolSize,
29672
29717
  interleaveDepth: interleaveDepthBlocks,
@@ -30208,24 +30253,24 @@ var MediaRouter = class {
30208
30253
  if (!this.#multicastVideoKeyed) return;
30209
30254
  if (this.#requireAudio && !this.#multicastAudioReceived) return;
30210
30255
  if (this.#requireAudio && !this.#moqAudioWarmed) return;
30211
- const token = ++this.#sourceSwitchToken;
30256
+ const token2 = ++this.#sourceSwitchToken;
30212
30257
  const maybeReady = this.#onSourceSwitch?.("multicast");
30213
30258
  if (!maybeReady || typeof maybeReady.then !== "function") {
30214
- this.#commitMulticastActivation(token);
30259
+ this.#commitMulticastActivation(token2);
30215
30260
  return;
30216
30261
  }
30217
30262
  const ready = Promise.resolve(maybeReady);
30218
- this.#pendingMulticastToken = token;
30263
+ this.#pendingMulticastToken = token2;
30219
30264
  this.#sourceSwitchReady = ready;
30220
30265
  void ready.then(
30221
- () => this.#commitMulticastActivation(token),
30222
- (err) => this.#rejectMulticastActivation(token, err)
30266
+ () => this.#commitMulticastActivation(token2),
30267
+ (err) => this.#rejectMulticastActivation(token2, err)
30223
30268
  ).finally(() => {
30224
30269
  if (this.#sourceSwitchReady === ready) this.#sourceSwitchReady = null;
30225
30270
  });
30226
30271
  }
30227
- #commitMulticastActivation(token) {
30228
- if (token !== this.#sourceSwitchToken) return;
30272
+ #commitMulticastActivation(token2) {
30273
+ if (token2 !== this.#sourceSwitchToken) return;
30229
30274
  this.#active = "multicast";
30230
30275
  this.#suppressed = true;
30231
30276
  this.#pendingMulticastToken = null;
@@ -30236,9 +30281,9 @@ var MediaRouter = class {
30236
30281
  if (video) this.#onVideoFrame?.(video.data, video.meta);
30237
30282
  if (audio) this.#onAudioFrame?.(audio.data, audio.meta);
30238
30283
  }
30239
- #rejectMulticastActivation(token, err) {
30240
- if (token !== this.#sourceSwitchToken) return;
30241
- if (this.#pendingMulticastToken === token) this.#pendingMulticastToken = null;
30284
+ #rejectMulticastActivation(token2, err) {
30285
+ if (token2 !== this.#sourceSwitchToken) return;
30286
+ if (this.#pendingMulticastToken === token2) this.#pendingMulticastToken = null;
30242
30287
  this.#pendingMulticastVideo = null;
30243
30288
  this.#pendingMulticastAudio = null;
30244
30289
  console.warn("[MediaRouter] onSourceSwitch(multicast) failed", err);
@@ -31207,63 +31252,65 @@ var FecStarvationDetector = class {
31207
31252
 
31208
31253
  // src/cmcd-producer.ts
31209
31254
  var PLAY_MODES = ["full", "base-only", "single-track"];
31210
- function sanitize(value) {
31211
- return value.replace(/[,\r\n]/g, "");
31212
- }
31213
- function buildFecParams(stats) {
31214
- const params = {};
31215
- const cfg = stats.config;
31216
- params["x-bc-fec-algo"] = cfg.algorithm;
31217
- params["x-bc-fec-k"] = cfg.k;
31218
- params["x-bc-fec-p"] = cfg.repairCount;
31219
- params["x-bc-fec-recovery"] = parseFloat((stats.recoveryRate / 100).toFixed(4));
31220
- return params;
31221
- }
31222
- function buildTransportParams(snap) {
31223
- if (!snap) return {};
31224
- const params = {};
31225
- params["x-bc-transport"] = snap.transport;
31226
- if (snap.multicastGroup != null) {
31227
- params["x-bc-multicast-group"] = snap.multicastGroup;
31228
- }
31229
- if (snap.multicastSource != null) {
31230
- params["x-bc-multicast-source"] = snap.multicastSource;
31231
- }
31232
- if (snap.amtRelay != null) {
31233
- params["x-bc-amt-relay"] = snap.amtRelay;
31234
- }
31235
- return params;
31236
- }
31237
- function formatQueryString(params) {
31238
- return Object.keys(params).sort().map((key) => {
31239
- const v = params[key];
31240
- if (typeof v === "string") return `${key}=${sanitize(v)}`;
31241
- return `${key}=${v}`;
31255
+ var DEFAULT_INTERVAL_MS = 3e4;
31256
+ var CUSTOM_PREFIX = "com.blockcast-";
31257
+ var MAX_CUSTOM_VALUE = 64;
31258
+ var TRANSPORT_TOKEN = {
31259
+ native: "ssm",
31260
+ amt: "amt",
31261
+ moq: "moq"
31262
+ };
31263
+ function sfString(value) {
31264
+ return `"${value.replace(/[^\x20-\x7e]/g, "").replace(/[\\"]/g, "\\$&")}"`;
31265
+ }
31266
+ function encodeCmcdRecord(record) {
31267
+ return Object.keys(record).sort().map((key) => {
31268
+ const value = record[key];
31269
+ if (typeof value === "number") return `${key}=${value}`;
31270
+ if (typeof value === "string") return `${key}=${sfString(value)}`;
31271
+ if (Array.isArray(value)) return `${key}=(${value.map(sfString).join(" ")})`;
31272
+ return `${key}=${value.token}`;
31242
31273
  }).join(",");
31243
31274
  }
31275
+ var token = (value) => ({ token: value });
31276
+ function putCustom(record, name, value) {
31277
+ if (value === void 0) return;
31278
+ if (typeof value === "number") value = String(value);
31279
+ if (typeof value === "string" && value.length > MAX_CUSTOM_VALUE) return;
31280
+ record[CUSTOM_PREFIX + name] = value;
31281
+ }
31244
31282
  var CmcdProducer = class {
31245
- #onCmcdParams;
31246
31283
  #onCmcdPost;
31284
+ #intervalMs;
31285
+ #sessionId;
31286
+ #now;
31287
+ #sn = 0;
31288
+ #lastHeartbeat = null;
31247
31289
  #transport;
31248
31290
  #lastStats;
31249
- // Phase 14 state — Plan 03 Task 2 (D-12, D-15).
31250
31291
  #playMode = null;
31251
31292
  #deltaFailRate = null;
31252
31293
  #switchMiscount = 0;
31253
31294
  #manifestStats = null;
31254
31295
  #fecFailClosed = null;
31296
+ /** Video-block resolutions from the repair lifecycle; null until the player feeds one. */
31297
+ #lifecycle = null;
31255
31298
  constructor(options) {
31256
- this.#onCmcdParams = options.onCmcdParams;
31257
31299
  this.#onCmcdPost = options.onCmcdPost;
31300
+ this.#intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
31301
+ if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
31302
+ throw new Error(`[CmcdProducer] invalid intervalMs "${String(options.intervalMs)}"; expected a positive number`);
31303
+ }
31304
+ this.#sessionId = options.sessionId ?? globalThis.crypto.randomUUID();
31305
+ this.#now = options.now ?? Date.now;
31258
31306
  }
31259
31307
  /** Update the stored transport state. Called by player wiring layer. */
31260
31308
  updateTransport(snapshot) {
31261
31309
  this.#transport = snapshot;
31262
31310
  }
31263
31311
  /**
31264
- * Set the current play-mode. Emitted as `x-bc-play-mode` on subsequent
31265
- * snapshots. Throws on any value outside {'full','base-only','single-track'}
31266
- * (CLAUDE.md: error on bad init).
31312
+ * Set the current play-mode, reported as `com.blockcast-pm`. Throws on any
31313
+ * value outside {'full','base-only','single-track'} (CLAUDE.md: error on bad init).
31267
31314
  */
31268
31315
  updatePlayMode(mode) {
31269
31316
  if (typeof mode !== "string" || !PLAY_MODES.includes(mode)) {
@@ -31274,9 +31321,8 @@ var CmcdProducer = class {
31274
31321
  this.#playMode = mode;
31275
31322
  }
31276
31323
  /**
31277
- * Set the delta-track FEC block-failure rate (0..1 inclusive). Emitted as
31278
- * `x-bc-delta-fail-rate` on snapshots when play-mode is 'full' or 'base-only'.
31279
- * Omitted on single-track streams.
31324
+ * Set the delta-track FEC block-failure rate (0..1 inclusive), reported as
31325
+ * `com.blockcast-dfr` when play-mode is 'full' or 'base-only'.
31280
31326
  */
31281
31327
  updateDeltaFailRate(rate) {
31282
31328
  if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1) {
@@ -31286,112 +31332,139 @@ var CmcdProducer = class {
31286
31332
  }
31287
31333
  this.#deltaFailRate = rate;
31288
31334
  }
31289
- /**
31290
- * Bump the cumulative switching-set misalignment counter. Emitted as
31291
- * `x-bc-switch-miscount` on snapshots when the counter is > 0.
31292
- */
31335
+ /** Bump the cumulative switching-set misalignment counter (`com.blockcast-sm`). */
31293
31336
  incrementSwitchMiscount() {
31294
31337
  this.#switchMiscount += 1;
31295
31338
  }
31296
31339
  /**
31297
- * Record that FEC failed closed at runtime (fec-01 6.3.9). Every later
31298
- * snapshot carries `x-bc-fec-fail-closed` (the reason code) and
31299
- * `x-bc-fec-fail-layer` (the held layer i whose total B_i was undercut), so a
31300
- * stream playing without FEC is told apart from one with no loss.
31340
+ * Record that FEC failed closed at runtime (fec-01 6.3.9). Reported at once
31341
+ * as `e=e` with the reason code in `ec`, and on every later counter report
31342
+ * as `com.blockcast-ffc` / `com.blockcast-ffl` (the held layer whose total
31343
+ * B_i was undercut), so a stream playing without FEC is told apart from one
31344
+ * with no loss.
31301
31345
  */
31302
31346
  updateFecFailClosed(reason) {
31347
+ const changed = this.#fecFailClosed?.code !== reason.code || this.#fecFailClosed?.layer !== reason.layer;
31303
31348
  this.#fecFailClosed = reason;
31349
+ if (changed) this.#post([{ ...this.#base("e"), ec: [reason.code] }]);
31304
31350
  }
31305
31351
  updateManifestVerification(stats) {
31306
31352
  this.#manifestStats = stats;
31307
31353
  }
31308
31354
  /**
31309
- * One-shot emit of `x-bc-switch-drift-us` for a single misalignment event.
31310
- * Does NOT mutate snapshot state — subsequent onSnapshot() calls will not
31311
- * include the drift key until the next emitSwitchDrift(). Mirrors the
31312
- * existing onTransition() pattern.
31355
+ * Count a video block's resolution from the repair lifecycle: recovered in
31356
+ * band (`com.blockcast-fbr`), recovered by repair FETCH (`-fbq`), or failed
31357
+ * as a keyframe-class block (`-fbfk`). Audio blocks are not counted, matching
31358
+ * the video-track `-fbf`. Until the player feeds this, the three are absent.
31313
31359
  */
31360
+ onRepairLifecycle(event) {
31361
+ this.#lifecycle ??= { inband: 0, fetch: 0, failedKeyframe: 0 };
31362
+ if (event.type === "recovered" && event.blockClass !== "audio") {
31363
+ if (event.via === "inband") this.#lifecycle.inband++;
31364
+ else this.#lifecycle.fetch++;
31365
+ } else if (event.type === "failed" && event.blockClass === "keyframe") {
31366
+ this.#lifecycle.failedKeyframe++;
31367
+ }
31368
+ }
31369
+ /** One-shot custom event `com.blockcast-switch-drift` for a single misalignment. */
31314
31370
  emitSwitchDrift(driftUs) {
31315
31371
  if (typeof driftUs !== "number" || !Number.isFinite(driftUs) || driftUs < 0) {
31316
31372
  throw new Error(
31317
31373
  `[CmcdProducer] invalid drift-us "${String(driftUs)}"; expected a non-negative finite number`
31318
31374
  );
31319
31375
  }
31320
- const params = {};
31321
- if (this.#lastStats) Object.assign(params, buildFecParams(this.#lastStats));
31322
- Object.assign(params, buildTransportParams(this.#transport));
31323
- if (this.#switchMiscount > 0) params["x-bc-switch-miscount"] = this.#switchMiscount;
31324
- params["x-bc-switch-drift-us"] = driftUs;
31325
- this.#emit(params);
31376
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "switch-drift" };
31377
+ putCustom(record, "sdu", Math.round(driftUs));
31378
+ this.#post([record]);
31326
31379
  }
31327
- /**
31328
- * Process a FecTrackStats snapshot. Finds the video track, builds
31329
- * x-bc-* vendor keys, and fires both delivery callbacks.
31330
- */
31380
+ /** Take a FecTrackStats snapshot (first video track) and send the heartbeat when due. */
31331
31381
  onSnapshot(data) {
31332
31382
  const videoTrack = data.tracks.find((t) => t.config.trackType === "video");
31333
31383
  if (!videoTrack) return;
31334
31384
  this.#lastStats = videoTrack;
31335
- const params = {
31336
- ...buildFecParams(videoTrack),
31337
- ...buildTransportParams(this.#transport)
31338
- };
31339
- if (this.#playMode !== null) {
31340
- params["x-bc-play-mode"] = this.#playMode;
31341
- }
31342
- if (this.#fecFailClosed) {
31343
- params["x-bc-fec-fail-closed"] = this.#fecFailClosed.code;
31344
- params["x-bc-fec-fail-layer"] = this.#fecFailClosed.layer;
31345
- }
31346
- if (this.#playMode !== null && this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
31347
- params["x-bc-delta-fail-rate"] = parseFloat(this.#deltaFailRate.toFixed(4));
31348
- }
31349
- if (this.#switchMiscount > 0) {
31350
- params["x-bc-switch-miscount"] = this.#switchMiscount;
31351
- }
31352
- if (this.#manifestStats) {
31353
- params["x-bc-manifest-verified"] = this.#manifestStats.verified > 0;
31354
- params["x-bc-manifest-failures"] = Object.values(this.#manifestStats.failures).reduce((total, count) => total + count, 0);
31355
- }
31356
- this.#emit(params);
31385
+ this.#heartbeat();
31357
31386
  }
31358
31387
  /**
31359
- * Emit transport state without inventing FEC metrics. Cadence is owned by
31360
- * the caller because only the transport integration knows when state changed.
31388
+ * Take transport state for receivers that do not own FEC stats, and send the
31389
+ * heartbeat when due.
31361
31390
  */
31362
31391
  onTransportSnapshot(data) {
31363
31392
  if (!Number.isFinite(data.timestampMs) || data.timestampMs < 0) {
31364
31393
  throw new Error("[CmcdProducer] transport snapshot timestamp must be a non-negative finite number");
31365
31394
  }
31366
31395
  if (data.playMode !== void 0) this.updatePlayMode(data.playMode);
31367
- if (data.switchMiscount !== void 0 && (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0)) {
31368
- throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
31396
+ if (data.switchMiscount !== void 0) {
31397
+ if (!Number.isSafeInteger(data.switchMiscount) || data.switchMiscount < 0) {
31398
+ throw new Error("[CmcdProducer] transport snapshot switch-miscount must be a non-negative safe integer");
31399
+ }
31400
+ this.#switchMiscount = data.switchMiscount;
31369
31401
  }
31370
31402
  this.#transport = data.transport;
31371
- const params = buildTransportParams(data.transport);
31372
- if (this.#playMode !== null) params["x-bc-play-mode"] = this.#playMode;
31373
- const switchMiscount = data.switchMiscount ?? this.#switchMiscount;
31374
- if (switchMiscount > 0) params["x-bc-switch-miscount"] = switchMiscount;
31375
- this.#emit(params);
31403
+ this.#heartbeat();
31376
31404
  }
31377
- /**
31378
- * Signal a transport transition event. Fires an immediate out-of-band
31379
- * emit with x-bc-ct set. The subsequent onSnapshot() will NOT include
31380
- * x-bc-ct (one-shot).
31381
- */
31405
+ /** Report a transport transition at once, as custom event `com.blockcast-ct`. */
31382
31406
  onTransition(event) {
31383
- const params = {};
31384
- if (this.#lastStats) {
31385
- Object.assign(params, buildFecParams(this.#lastStats));
31407
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "ct" };
31408
+ putCustom(record, "ct", token(event));
31409
+ this.#putTransport(record);
31410
+ this.#post([record]);
31411
+ }
31412
+ #base(event) {
31413
+ return { e: token(event), sid: this.#sessionId, sn: this.#sn++, ts: Math.floor(this.#now()), v: 2 };
31414
+ }
31415
+ #heartbeat() {
31416
+ const now = this.#now();
31417
+ if (this.#lastHeartbeat !== null && now - this.#lastHeartbeat < this.#intervalMs) return;
31418
+ this.#lastHeartbeat = now;
31419
+ this.#post([this.#base("t"), this.#counters()]);
31420
+ }
31421
+ #putTransport(record) {
31422
+ const snap = this.#transport;
31423
+ if (!snap) return;
31424
+ putCustom(record, "tr", token(TRANSPORT_TOKEN[snap.transport]));
31425
+ if (snap.multicastGroup != null && snap.multicastSource != null) {
31426
+ putCustom(record, "sg", `${snap.multicastGroup}@${snap.multicastSource}`);
31427
+ }
31428
+ putCustom(record, "ar", snap.amtRelay);
31429
+ }
31430
+ /** The `e=ce` counter report. */
31431
+ #counters() {
31432
+ const record = { ...this.#base("ce"), cen: CUSTOM_PREFIX + "fec" };
31433
+ const stats = this.#lastStats;
31434
+ if (stats) {
31435
+ putCustom(record, "fa", stats.config.algorithm);
31436
+ putCustom(record, "fk", stats.config.k);
31437
+ putCustom(record, "fp", stats.config.repairCount);
31438
+ putCustom(record, "pr", stats.sourceSymbols);
31439
+ putCustom(record, "pl", stats.sourcePacketsMissing);
31440
+ putCustom(record, "pu", stats.sourcePacketsUnrecovered);
31441
+ putCustom(record, "fbf", stats.blocksFailed);
31442
+ }
31443
+ if (this.#lifecycle) {
31444
+ putCustom(record, "fbr", this.#lifecycle.inband);
31445
+ putCustom(record, "fbq", this.#lifecycle.fetch);
31446
+ putCustom(record, "fbfk", this.#lifecycle.failedKeyframe);
31447
+ }
31448
+ this.#putTransport(record);
31449
+ if (this.#playMode !== null) {
31450
+ putCustom(record, "pm", token(this.#playMode));
31451
+ if (this.#playMode !== "single-track" && this.#deltaFailRate !== null) {
31452
+ putCustom(record, "dfr", this.#deltaFailRate.toFixed(3));
31453
+ }
31454
+ }
31455
+ if (this.#fecFailClosed) {
31456
+ putCustom(record, "ffc", token(this.#fecFailClosed.code));
31457
+ putCustom(record, "ffl", this.#fecFailClosed.layer);
31386
31458
  }
31387
- Object.assign(params, buildTransportParams(this.#transport));
31388
- params["x-bc-ct"] = event;
31389
- this.#emit(params);
31459
+ if (this.#switchMiscount > 0) putCustom(record, "sm", this.#switchMiscount);
31460
+ if (this.#manifestStats) {
31461
+ putCustom(record, "mv", this.#manifestStats.verified);
31462
+ putCustom(record, "mf", Object.values(this.#manifestStats.failures).reduce((a, b) => a + b, 0));
31463
+ }
31464
+ return record;
31390
31465
  }
31391
- /** Fire both delivery callbacks with the given params. */
31392
- #emit(params) {
31393
- this.#onCmcdParams?.(params);
31394
- this.#onCmcdPost?.(formatQueryString(params));
31466
+ #post(records) {
31467
+ this.#onCmcdPost?.(records.map(encodeCmcdRecord).join("\n"));
31395
31468
  }
31396
31469
  };
31397
31470
 
@@ -31487,6 +31560,19 @@ function resolveRepairLayers(catalog, source) {
31487
31560
  totalBaseRepairSymbols: binding.totalBaseRepairSymbols
31488
31561
  };
31489
31562
  }
31563
+ function valueAt(catalog, pointer) {
31564
+ let node = catalog;
31565
+ for (const segment of pointer.split("/").slice(1)) {
31566
+ if (typeof node !== "object" || node === null) return void 0;
31567
+ node = node[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
31568
+ }
31569
+ return node;
31570
+ }
31571
+ function catalogTrackDeliveryMode(catalog, source) {
31572
+ const binding = bindingOf(catalog, source);
31573
+ const entry = binding ? valueAt(catalog, binding.pointer) : void 0;
31574
+ return entry?.deliveryMode === "stream" ? "stream" : "datagram";
31575
+ }
31490
31576
  function moqtPriority2(value) {
31491
31577
  return Number.isSafeInteger(value) && value >= 0 && value <= 255 ? value : null;
31492
31578
  }
@@ -31530,6 +31616,7 @@ function fecParams(catalog, track, fps, sampleRate) {
31530
31616
  repairTrackName: base.repairTrack,
31531
31617
  repairLayers: repair.baseLayers,
31532
31618
  repairOverlays: repair.overlays,
31619
+ deliveryMode: catalogTrackDeliveryMode(catalog, track),
31533
31620
  ...mediaRate
31534
31621
  };
31535
31622
  }
@@ -32054,7 +32141,7 @@ var RepairLayerSubscriptions = class {
32054
32141
  * or interleaved sequences read as loss.
32055
32142
  */
32056
32143
  noteSourceSequence(seq) {
32057
- if (this.#disposed || this.#opts.delivery === "stream") return;
32144
+ if (this.#disposed || this.#delivery() === "stream") return;
32058
32145
  seq >>>= 0;
32059
32146
  if (this.#lastSeq === void 0) {
32060
32147
  this.#lastSeq = seq;
@@ -32087,12 +32174,12 @@ var RepairLayerSubscriptions = class {
32087
32174
  this.#expected = 0;
32088
32175
  this.#lost = 0;
32089
32176
  this.#wantFewer = 0;
32090
- this.#layerCount = Math.min(this.#layerCount, this.#layers().length);
32177
+ this.#layerCount = this.#delivery() === "stream" ? 0 : Math.min(this.#layerCount, this.#layers().length);
32091
32178
  this.#reconcile();
32092
32179
  }
32093
32180
  snapshot() {
32094
32181
  return {
32095
- delivery: this.#opts.delivery,
32182
+ delivery: this.#delivery(),
32096
32183
  layerCount: this.#layerCount,
32097
32184
  subscribedTracks: [...this.#subs.keys()],
32098
32185
  repairPackets: this.#repairPackets,
@@ -32106,6 +32193,9 @@ var RepairLayerSubscriptions = class {
32106
32193
  this.#closeAll();
32107
32194
  this.#disposed = true;
32108
32195
  }
32196
+ #delivery() {
32197
+ return this.#source.deliveryMode === "stream" ? "stream" : "datagram";
32198
+ }
32109
32199
  #layers() {
32110
32200
  return this.#source.repairLayers ?? [];
32111
32201
  }
@@ -32848,6 +32938,7 @@ export {
32848
32938
  catalogGroupDurationMs,
32849
32939
  catalogKeyframeCadenceMs,
32850
32940
  catalogRepairTarget,
32941
+ catalogTrackDeliveryMode,
32851
32942
  catalogTrackFecUnavailable,
32852
32943
  classifyH264Keyframe,
32853
32944
  classifyHevcKeyframe,
@@ -32868,6 +32959,7 @@ export {
32868
32959
  detectH264Keyframe,
32869
32960
  detectHevcKeyframe,
32870
32961
  driftCheck,
32962
+ encodeCmcdRecord,
32871
32963
  encodeDataSegment,
32872
32964
  encodeFecSourceInfo,
32873
32965
  encodeLocTimestamp,