@camstack/addon-pipeline 1.2.198 → 1.2.200

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 (43) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-B5v8qEI0.js → default-detection-model-BsmxtyZx.js} +1 -1
  4. package/dist/{default-detection-model-CASpAESz.mjs → default-detection-model-c1LY6thP.mjs} +1 -1
  5. package/dist/detection-pipeline/index.js +5 -5
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-BxP0SvPz.js → dist-D2YTTkRf.js} +436 -419
  8. package/dist/{dist-oFIHnD88.mjs → dist-mEsrAAc3.mjs} +436 -419
  9. package/dist/{lazy-sharp-VKOG3WW4.js → lazy-sharp-B5bW34uC.js} +1 -1
  10. package/dist/motion-wasm/index.js +2 -2
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-B4zETsJ9.mjs → node-CUxep5tc.mjs} +1 -1
  13. package/dist/{node-BIKrNEtS.js → node-x3YokJkD.js} +1 -1
  14. package/dist/pipeline-runner/index.js +8 -7
  15. package/dist/pipeline-runner/index.mjs +7 -6
  16. package/dist/{process-memory-D9xYwm6L.mjs → process-memory-BCqpTruN.mjs} +1 -1
  17. package/dist/{process-memory-CMHOmj75.js → process-memory-CFc4D9A2.js} +1 -1
  18. package/dist/recorder/index.js +169 -334
  19. package/dist/recorder/index.mjs +164 -329
  20. package/dist/restream-intent-B1Difcnq.js +313 -0
  21. package/dist/restream-intent-DWe2kInY.mjs +248 -0
  22. package/dist/{prebuffer-DmBYRQWg.mjs → retire-root-keys-Csb8z2f3.js} +12 -64
  23. package/dist/{prebuffer-DqdeLSsk.js → retire-root-keys-DZvlso2l.mjs} +1 -99
  24. package/dist/{segment-demux-js-DLwL1vmq.js → segment-demux-js-DUZVURQd.js} +1 -1
  25. package/dist/{segment-demux-js-DZysrgdC.mjs → segment-demux-js-xHtB450F.mjs} +1 -1
  26. package/dist/session-decode/decode-worker-child.js +25 -5
  27. package/dist/session-decode/decode-worker-child.mjs +24 -4
  28. package/dist/stream-broker/_stub.js +2 -2
  29. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C2eTdxvd.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CLXImQ3R.mjs} +3 -3
  30. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-NqqfpkYF.mjs +26 -0
  31. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dj_qoc1e.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D62O5bRa.mjs} +1 -1
  32. package/dist/stream-broker/demux-worker-child.js +1 -1
  33. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  34. package/dist/stream-broker/{hostInit-C8tu7NS4.mjs → hostInit-D_cZQhBA.mjs} +3 -3
  35. package/dist/stream-broker/index.js +181 -48
  36. package/dist/stream-broker/index.mjs +172 -39
  37. package/dist/stream-broker/remoteEntry.js +1 -1
  38. package/dist/{worker-protocol-D2mMjUXv.js → worker-protocol-CRDaBlJl.js} +1 -1
  39. package/dist/{worker-protocol-DFlZm_wr.mjs → worker-protocol-oUamwZSn.mjs} +1 -1
  40. package/package.json +1 -1
  41. package/dist/restream-intent-B4BXZra7.mjs +0 -72
  42. package/dist/restream-intent-Cv9x3jmu.js +0 -89
  43. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BcjLrxhF.mjs +0 -26
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.160",
39
+ version: "1.2.162",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.2.80",
48
+ version: "1.2.82",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.128",
84
+ version: "1.2.130",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -2,12 +2,12 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-BxP0SvPz.js");
6
- const require_node = require("../node-BIKrNEtS.js");
7
- const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
5
+ const require_dist = require("../dist-D2YTTkRf.js");
6
+ const require_node = require("../node-x3YokJkD.js");
7
+ const require_restream_intent = require("../restream-intent-B1Difcnq.js");
8
8
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
9
- const require_prebuffer = require("../prebuffer-DqdeLSsk.js");
10
- const require_segment_demux_js = require("../segment-demux-js-DLwL1vmq.js");
9
+ const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
10
+ const require_segment_demux_js = require("../segment-demux-js-DUZVURQd.js");
11
11
  let node_os = require("node:os");
12
12
  node_os = require_dist.__toESM(node_os);
13
13
  let node_crypto = require("node:crypto");
@@ -370,7 +370,7 @@ var BROKER_PROBE_SNAPSHOTS_SPEC = {
370
370
  loadLimit: BROKER_ROWS_LIMIT
371
371
  };
372
372
  function probeSnapshotsStore(store, logger) {
373
- return new require_prebuffer.RowMapStore({
373
+ return new require_retire_root_keys.RowMapStore({
374
374
  spec: BROKER_PROBE_SNAPSHOTS_SPEC,
375
375
  store,
376
376
  logger
@@ -412,7 +412,7 @@ var BROKER_RTSP_TOKENS_SPEC = {
412
412
  loadLimit: BROKER_ROWS_LIMIT
413
413
  };
414
414
  function rtspTokensStore(store, logger) {
415
- return new require_prebuffer.RowMapStore({
415
+ return new require_retire_root_keys.RowMapStore({
416
416
  spec: BROKER_RTSP_TOKENS_SPEC,
417
417
  store,
418
418
  logger
@@ -453,7 +453,7 @@ var BROKER_PROFILE_MAP_SPEC = {
453
453
  loadLimit: BROKER_ROWS_LIMIT
454
454
  };
455
455
  function profileMapStore(store, logger) {
456
- return new require_prebuffer.RowMapStore({
456
+ return new require_retire_root_keys.RowMapStore({
457
457
  spec: BROKER_PROFILE_MAP_SPEC,
458
458
  store,
459
459
  logger
@@ -2070,7 +2070,7 @@ var NOTHING = {
2070
2070
  function usablePreRoll(value) {
2071
2071
  if (value === void 0) return 0;
2072
2072
  if (!Number.isFinite(value) || value <= 0) return 0;
2073
- return Math.min(require_prebuffer.PREBUFFER_MAX_MS, value);
2073
+ return Math.min(require_restream_intent.PREBUFFER_MAX_MS, value);
2074
2074
  }
2075
2075
  /**
2076
2076
  * Choose the keyframe access unit a pre-roll burst starts at.
@@ -2583,6 +2583,18 @@ var DEFAULT_PRIME_BURST_BOUND = {
2583
2583
  /** Drop/withhold accounting is aggregated and reported at most this often. */
2584
2584
  var DROP_REPORT_INTERVAL_MS$1 = 3e4;
2585
2585
  /**
2586
+ * How often a HISTORY-FREE attach is reported per stream.
2587
+ *
2588
+ * The baseline population is large and uninteresting one line at a time — a
2589
+ * WebRTC bootstrap, a HomeKit leg, an ffprobe — but it is the control group the
2590
+ * pre-roll cost is measured against, so it cannot be silent either. Throttled
2591
+ * here and NOT throttled for an attach that carried history: that population is
2592
+ * rare (17 of ~130 agganci in the 2026-09-07 census) and it is the one the
2593
+ * question is about, so a rate limit sized for the common case would drop
2594
+ * exactly the samples nobody has.
2595
+ */
2596
+ var PLAIN_ATTACH_REPORT_INTERVAL_MS = 1e4;
2597
+ /**
2586
2598
  * Defensive hard bound on the per-keyframe RTP/NAL cache (both the passthrough
2587
2599
  * `lastKeyframeRtpPackets` and the Annex-B `lastKeyframeNals`). A single
2588
2600
  * keyframe GOP is at most a few large NALs — well under a megabyte or two even
@@ -2732,6 +2744,8 @@ var RtspRestreamer = class {
2732
2744
  dropsLiveLatched = 0;
2733
2745
  dropsLiveClosed = 0;
2734
2746
  dropReportMs = 0;
2747
+ /** Last history-free attach reported — see {@link PLAIN_ATTACH_REPORT_INTERVAL_MS}. */
2748
+ plainAttachReportMs = 0;
2735
2749
  /** Numeric device id parsed once from the brokerId, for `tags.deviceId`. */
2736
2750
  deviceId;
2737
2751
  constructor(streamName) {
@@ -2991,7 +3005,7 @@ var RtspRestreamer = class {
2991
3005
  if (primePackets.length === 0) return;
2992
3006
  const firstTs = primePackets[0].data.readUInt32BE(4);
2993
3007
  const spanMs = (primePackets[primePackets.length - 1].data.readUInt32BE(4) - firstTs | 0) / 90;
2994
- const preRollState = { plan: null };
3008
+ const preRollPlans = /* @__PURE__ */ new Map();
2995
3009
  const bound = this.currentPrimeBound();
2996
3010
  /**
2997
3011
  * Resolve (and memoize) the plan for one intent.
@@ -3003,28 +3017,33 @@ var RtspRestreamer = class {
3003
3017
  * current GOP or a supplier bug can, and D57's remedy (join clean at the
3004
3018
  * next live keyframe) is the right one for a burst nobody asked for.
3005
3019
  */
3006
- const planFor = () => {
3007
- if (preRollState.plan) return preRollState.plan;
3020
+ const planFor = (requestedMs) => {
3021
+ const memo = preRollPlans.get(requestedMs);
3022
+ if (memo) return memo;
3008
3023
  const burst = this.preRollSupplier?.();
3009
- const window = fromRing && burst && burst.preRollMs > 0 ? burst.packets() : [];
3024
+ const window = fromRing && burst && burst.preRollMs > 0 && requestedMs > 0 ? burst.packets(requestedMs) : [];
3010
3025
  const packets = window.length > primePackets.length ? window.map((data) => ({
3011
3026
  data,
3012
3027
  marker: (data[1] & 128) !== 0
3013
3028
  })) : [];
3014
3029
  const windowSpanMs = packets.length > 0 ? (packets[packets.length - 1].data.readUInt32BE(4) - packets[0].data.readUInt32BE(4) | 0) / 90 : 0;
3015
- const windowBound = packets.length > 0 ? preRollSpanBound(bound, burst?.preRollMs ?? 0) : bound;
3030
+ const servableMs = Math.min(requestedMs, burst?.preRollMs ?? 0);
3031
+ const windowBound = packets.length > 0 ? preRollSpanBound(bound, servableMs) : bound;
3016
3032
  const plan = {
3017
3033
  packets,
3018
3034
  active: packets.length > 0,
3019
- configuredMs: burst?.preRollMs ?? 0,
3035
+ retainedMs: burst?.preRollMs ?? 0,
3036
+ requestedMs,
3020
3037
  spanMs: windowSpanMs,
3021
3038
  overBound: packets.length > 0 && (windowSpanMs > windowBound.maxSpanMs || packets.length > windowBound.maxPackets)
3022
3039
  };
3023
- preRollState.plan = plan;
3040
+ preRollPlans.set(requestedMs, plan);
3024
3041
  return plan;
3025
3042
  };
3026
3043
  const served = [];
3027
3044
  const servedPreRoll = /* @__PURE__ */ new Map();
3045
+ /** The plan each pre-roll session was actually served, for its attach line. */
3046
+ const servedPlan = /* @__PURE__ */ new Map();
3028
3047
  let withheld = 0;
3029
3048
  let detectionIntentServed = 0;
3030
3049
  for (const sessionId of ready) {
@@ -3032,8 +3051,9 @@ var RtspRestreamer = class {
3032
3051
  if (!session) continue;
3033
3052
  const detectionIntent = session.isDetectionIntent();
3034
3053
  const preRollIntent = session.preRollIntent();
3035
- if (preRollIntent !== null) {
3036
- const plan = planFor();
3054
+ const requestedMs = session.preRollRequestMs();
3055
+ if (preRollIntent !== null && requestedMs > 0) {
3056
+ const plan = planFor(requestedMs);
3037
3057
  if (plan.active) {
3038
3058
  if (plan.overBound) {
3039
3059
  this.dropsWithheldSpan++;
@@ -3041,6 +3061,7 @@ var RtspRestreamer = class {
3041
3061
  continue;
3042
3062
  }
3043
3063
  servedPreRoll.set(sessionId, preRollIntent);
3064
+ servedPlan.set(sessionId, plan);
3044
3065
  served.push(sessionId);
3045
3066
  this.logger?.info("restream pre-roll served", {
3046
3067
  tags: { deviceId: this.deviceId },
@@ -3049,7 +3070,8 @@ var RtspRestreamer = class {
3049
3070
  sessionId,
3050
3071
  intent: preRollIntent,
3051
3072
  consumer: session.getUserAgent() ?? "unknown",
3052
- configuredMs: plan.configuredMs,
3073
+ requestedMs: plan.requestedMs,
3074
+ retainedMs: plan.retainedMs,
3053
3075
  spanMs: Math.round(plan.spanMs),
3054
3076
  packets: plan.packets.length
3055
3077
  }
@@ -3078,8 +3100,16 @@ var RtspRestreamer = class {
3078
3100
  }
3079
3101
  if (withheld > 0) this.reportDrops(false);
3080
3102
  if (served.length === 0) return;
3081
- /** Media actually delivered, or `-1` when nobody asked for history at all. */
3082
- const servedSpanMs = preRollState.plan === null ? -1 : Math.round(preRollState.plan.spanMs);
3103
+ /**
3104
+ * Media actually delivered — the WIDEST window any session took, or `-1`
3105
+ * when nobody asked for history at all.
3106
+ *
3107
+ * A max and not a single plan's span since D382: two sessions on the same
3108
+ * keyframe can be served different windows, and the number this aggregate
3109
+ * line owes its reader is the largest burst this stream let out. The
3110
+ * per-attach truth is on `restream attach live-edge`, one line per session.
3111
+ */
3112
+ const servedSpanMs = preRollPlans.size === 0 ? -1 : Math.round(Math.max(...[...preRollPlans.values()].map((p) => p.spanMs)));
3083
3113
  /** Which declared intents joined this burst — `0` is "none", not "off". */
3084
3114
  const intentsServed = new Set(servedPreRoll.values());
3085
3115
  this.logger?.info("restream prime (lat-probe)", {
@@ -3106,15 +3136,30 @@ var RtspRestreamer = class {
3106
3136
  prime: true,
3107
3137
  keyframeAuStart: false
3108
3138
  }));
3109
- const preRollItems = servedPreRoll.size > 0 ? (preRollState.plan?.packets ?? []).map((packet) => ({
3110
- packet,
3111
- prime: true,
3112
- keyframeAuStart: false
3113
- })) : [];
3139
+ const preRollItems = /* @__PURE__ */ new Map();
3140
+ const itemsFor = (plan) => {
3141
+ const memo = preRollItems.get(plan.requestedMs);
3142
+ if (memo) return memo;
3143
+ const built = plan.packets.map((packet) => ({
3144
+ packet,
3145
+ prime: true,
3146
+ keyframeAuStart: false
3147
+ }));
3148
+ preRollItems.set(plan.requestedMs, built);
3149
+ return built;
3150
+ };
3114
3151
  for (const sessionId of served) {
3115
3152
  this.pendingKeyframe.delete(sessionId);
3116
- const intent = servedPreRoll.get(sessionId);
3117
- this.startPrimeReplay(sessionId, intent ? preRollItems : items, intent ?? null);
3153
+ const intent = servedPreRoll.get(sessionId) ?? null;
3154
+ const plan = servedPlan.get(sessionId) ?? null;
3155
+ this.startPrimeReplay(sessionId, plan ? itemsFor(plan) : items, intent, {
3156
+ intent,
3157
+ requestedMs: plan?.requestedMs ?? 0,
3158
+ retainedMs: plan?.retainedMs ?? this.preRollSupplier?.().preRollMs ?? 0,
3159
+ preRollSpanMs: plan ? Math.round(plan.spanMs - spanMs) : 0,
3160
+ burstSpanMs: Math.round(plan ? plan.spanMs : spanMs),
3161
+ gopSpanMs: Math.round(spanMs)
3162
+ });
3118
3163
  }
3119
3164
  }
3120
3165
  /**
@@ -3127,7 +3172,7 @@ var RtspRestreamer = class {
3127
3172
  * session that went away — aborts the whole replay rather than punching holes
3128
3173
  * in the GOP: the session is left latched and rejoins at the next keyframe AU.
3129
3174
  */
3130
- startPrimeReplay(sessionId, items, preRollIntent) {
3175
+ startPrimeReplay(sessionId, items, preRollIntent, attach) {
3131
3176
  const replay = new PacedRtpReplay((item) => {
3132
3177
  const session = this.sessions.get(sessionId);
3133
3178
  if (!session) {
@@ -3157,6 +3202,7 @@ var RtspRestreamer = class {
3157
3202
  durationMs: stats.durationMs
3158
3203
  }
3159
3204
  });
3205
+ this.reportAttach(sessionId, attach, stats);
3160
3206
  this.logger?.debug("restream prime drained", {
3161
3207
  tags: { deviceId: this.deviceId },
3162
3208
  meta: {
@@ -3175,6 +3221,58 @@ var RtspRestreamer = class {
3175
3221
  this.activeReplays.set(sessionId, replay);
3176
3222
  replay.start(items);
3177
3223
  }
3224
+ /**
3225
+ * ONE LINE PER ATTACH: what this session asked for, what it was handed, and
3226
+ * how long it took to reach the live edge.
3227
+ *
3228
+ * The question this exists to answer is "does the pre-roll delay the first
3229
+ * inference, and by how much" — and it could not be answered before. The only
3230
+ * measurement in reach was `pipeline stats`, a 10 s window whose averages
3231
+ * dissolve a 4 s transient by construction, and the broker's own lines were
3232
+ * per-STREAM aggregates that named a window without naming a session or a
3233
+ * duration. Seventeen served bursts inside ninety minutes of ten-second
3234
+ * averages is not a measurement, it is a hope.
3235
+ *
3236
+ * `liveEdgeMs` is measured from PLAY, the first instant the restreamer owes
3237
+ * this session bytes, to the moment its burst has drained and it is on the
3238
+ * live feed. It therefore contains the wait for a keyframe AND the paced
3239
+ * drain — the two things a burst can cost a joiner on this side of the
3240
+ * socket. What it deliberately does NOT contain is the consumer's own decode
3241
+ * of that burst: that is measured where it happens, on
3242
+ * `session-decode: detection first frame`, and the two lines are joined by
3243
+ * `deviceId` plus the per-attach `consumer` tag.
3244
+ *
3245
+ * `aborted` is on the line because a burst that never reached the socket is
3246
+ * not a burst this session paid for, and averaging those in would understate
3247
+ * the cost of the ones that did.
3248
+ */
3249
+ reportAttach(sessionId, attach, stats) {
3250
+ const session = this.sessions.get(sessionId);
3251
+ const playAt = session?.getPlayAt() ?? 0;
3252
+ if (!(attach.preRollSpanMs > 0)) {
3253
+ const now = Date.now();
3254
+ if (now - this.plainAttachReportMs < PLAIN_ATTACH_REPORT_INTERVAL_MS) return;
3255
+ this.plainAttachReportMs = now;
3256
+ }
3257
+ this.logger?.info("restream attach live-edge", {
3258
+ tags: { deviceId: this.deviceId },
3259
+ meta: {
3260
+ streamName: this.streamName,
3261
+ sessionId,
3262
+ consumer: session?.getUserAgent() ?? "unknown",
3263
+ intent: attach.intent ?? "none",
3264
+ requestedMs: attach.requestedMs,
3265
+ retainedMs: attach.retainedMs,
3266
+ preRollSpanMs: attach.preRollSpanMs,
3267
+ burstSpanMs: attach.burstSpanMs,
3268
+ gopSpanMs: attach.gopSpanMs,
3269
+ liveEdgeMs: playAt > 0 ? Date.now() - playAt : -1,
3270
+ drainMs: Math.round(stats.durationMs),
3271
+ packets: stats.sent,
3272
+ aborted: stats.aborted
3273
+ }
3274
+ });
3275
+ }
3178
3276
  /** Cancel a session's in-flight join replay (teardown / stream reset). */
3179
3277
  abortReplay(sessionId) {
3180
3278
  const replay = this.activeReplays.get(sessionId);
@@ -3457,15 +3555,31 @@ var RtspSession = class {
3457
3555
  /** When true, this session receives video-only (no audio RTP). */
3458
3556
  muted;
3459
3557
  /**
3460
- * The consumer purpose this session DECLARED on its dial path (see
3461
- * `restream-intent.ts`), or `null` when it declared none — which is every
3462
- * consumer except the per-session detection decode.
3558
+ * What this session DECLARED on its dial path (see `restream-intent.ts`): its
3559
+ * consumer purpose, and how many ms of retained history it asked for.
3560
+ * {@link NO_DECLARATION} when it declared nothing — which is every consumer
3561
+ * except the per-session detection decode and the recorder's writer.
3463
3562
  *
3464
3563
  * Kept separate from {@link muted} deliberately: `isMuted()` is shared by the
3465
3564
  * WebRTC/HomeKit loopback legs, and the join rule that keys on it must stay
3466
3565
  * true for them. Only an explicit declaration changes behaviour.
3566
+ *
3567
+ * One field and not two, because the purpose and the amount are one
3568
+ * statement (D382): a session that names a purpose without naming an amount
3569
+ * is asking for nothing, and that has to be as unmistakable here as it is on
3570
+ * the wire.
3467
3571
  */
3468
- intent;
3572
+ declaration;
3573
+ /**
3574
+ * Epoch ms at which this session reached PLAY, or `0` before it does.
3575
+ *
3576
+ * The clock the join is measured against. `connectedAt` is the TCP accept,
3577
+ * which includes DESCRIBE/SETUP and, for a cold broker, the whole upstream
3578
+ * dial — so it answers "how slow was the camera", not "what did the burst
3579
+ * cost this session". Time to the live edge is measured from PLAY, because
3580
+ * PLAY is the first instant at which the restreamer owes this session bytes.
3581
+ */
3582
+ playAt = 0;
3469
3583
  /**
3470
3584
  * Latch for ANY session that has started dropping video packets on socket
3471
3585
  * backlog — it was written for the `muted` decoder loopback, which was the
@@ -3479,12 +3593,12 @@ var RtspSession = class {
3479
3593
  /** Consecutive keyframe AUs at which this session was still unwritable.
3480
3594
  * Reset by any successful write — the bound is CONSECUTIVE misses. */
3481
3595
  missedKeyframes = 0;
3482
- constructor(socket, sdp, onClose, muted, intent) {
3596
+ constructor(socket, sdp, onClose, muted, declaration) {
3483
3597
  this.socket = socket;
3484
3598
  this.sdp = sdp;
3485
3599
  this.onClose = onClose;
3486
3600
  this.muted = muted ?? false;
3487
- this.intent = intent ?? null;
3601
+ this.declaration = declaration ?? require_restream_intent.NO_DECLARATION;
3488
3602
  this.sessionId = (0, node_crypto.randomUUID)().replace(/-/g, "").slice(0, 16);
3489
3603
  socket.on("data", (chunk) => {
3490
3604
  if (this.closed) return;
@@ -3528,7 +3642,23 @@ var RtspSession = class {
3528
3642
  * construction and drains the burst instead of accumulating it.
3529
3643
  */
3530
3644
  isDetectionIntent() {
3531
- return this.intent === "detection";
3645
+ return this.declaration.intent === "detection";
3646
+ }
3647
+ /**
3648
+ * Ms of retained history this session ASKED for on its dial. `0` = none.
3649
+ *
3650
+ * Read by `RtspRestreamer.servePendingSessionsRtp`, which serves the smaller
3651
+ * of this and what the ring holds. A session that declared an intent and no
3652
+ * amount takes the ordinary current-GOP burst — the intent still buys it
3653
+ * whatever else the intent buys (for detection, the live-edge exemption), and
3654
+ * nothing more.
3655
+ */
3656
+ preRollRequestMs() {
3657
+ return this.declaration.preRollRequestMs;
3658
+ }
3659
+ /** Epoch ms at which this session reached PLAY; `0` if it has not. */
3660
+ getPlayAt() {
3661
+ return this.playAt;
3532
3662
  }
3533
3663
  /**
3534
3664
  * Does this session get the PRE-ROLL window instead of the current GOP?
@@ -3545,12 +3675,14 @@ var RtspSession = class {
3545
3675
  * WHICH pre-roll window this session gets, or `null` when it declared no
3546
3676
  * intent and takes the ordinary current-GOP join burst.
3547
3677
  *
3548
- * The two windows are two numbers (D193), so the restreamer needs the intent
3549
- * and not just the bit: a recorder handed detection's window would carry the
3550
- * pre-roll of a feature it is not, and on this fleet detection's is 0.
3678
+ * There is ONE window now (D381) and one ASK per session (D382), so what the
3679
+ * restreamer still needs from this is WHO joined — the identity it prints
3680
+ * beside the window it served, and the reason a recorder and a detector are
3681
+ * distinguishable on a line that reports one number.
3551
3682
  */
3552
3683
  preRollIntent() {
3553
- return this.intent === "detection" || this.intent === "recording" ? this.intent : null;
3684
+ const intent = this.declaration.intent;
3685
+ return intent === "detection" || intent === "recording" ? intent : null;
3554
3686
  }
3555
3687
  /**
3556
3688
  * Diagnostic snapshot consumed by `StreamBroker.listClients` (Phase 10).
@@ -3771,6 +3903,7 @@ var RtspSession = class {
3771
3903
  }
3772
3904
  handlePlay(req) {
3773
3905
  this.playing = true;
3906
+ if (this.playAt === 0) this.playAt = Date.now();
3774
3907
  this.respond(req, 200, "OK", {
3775
3908
  Session: this.sessionId,
3776
3909
  Range: "npt=0.000-"
@@ -3927,7 +4060,7 @@ var RtspListenServer = class RtspListenServer {
3927
4060
  async dispatch(socket, requestBuffer) {
3928
4061
  const uriMatch = (requestBuffer.split("\r\n")[0] ?? "").match(/\s(rtsp:\/\/[^\s]+)\s/);
3929
4062
  const pathMatch = (uriMatch ? uriMatch[1] : "").match(/rtsp:\/\/[^/]+(\/.*?)(?:\s|$)/);
3930
- const { lookupPath, muted: isMuted, intent } = require_restream_intent.parseRestreamPath(pathMatch ? pathMatch[1].replace(/^\//, "").replace(/\/trackID=\d+$/, "") : "");
4063
+ const { lookupPath, muted: isMuted, declaration } = require_restream_intent.parseRestreamPath(pathMatch ? pathMatch[1].replace(/^\//, "").replace(/\/trackID=\d+$/, "") : "");
3931
4064
  const restreamer = this.restreamers.get(lookupPath);
3932
4065
  if (!restreamer) {
3933
4066
  socket.write(`RTSP/1.0 404 Not Found\r\nCSeq: 1\r\n\r\n`);
@@ -3956,7 +4089,7 @@ var RtspListenServer = class RtspListenServer {
3956
4089
  const session = new RtspSession(socket, isMuted ? restreamer.getMutedSdp() ?? restreamer.getSdp() ?? "" : restreamer.getSdp() ?? "", () => {
3957
4090
  restreamer.removeSession(session.getSessionId());
3958
4091
  releaseOnce();
3959
- }, isMuted, intent);
4092
+ }, isMuted, declaration);
3960
4093
  restreamer.addSession(session);
3961
4094
  releaseOnce();
3962
4095
  socket.unshift(Buffer.from(requestBuffer, "ascii"));
@@ -11701,7 +11834,7 @@ var StreamBroker = class StreamBroker {
11701
11834
  this.rtspRestreamer.setPrimeBoundSupplier(() => this.joinBurstBound);
11702
11835
  this.rtspRestreamer.setPreRollSupplier(() => {
11703
11836
  return {
11704
- packets: () => this.getRtpPreRollBuffer(),
11837
+ packets: (requestedMs) => this.getRtpPreRollBuffer(requestedMs),
11705
11838
  preRollMs: this.prebufferMs
11706
11839
  };
11707
11840
  });
@@ -12778,7 +12911,7 @@ var StreamBroker = class StreamBroker {
12778
12911
  * other numbers quietly sized the history that did exist.
12779
12912
  */
12780
12913
  setPrebuffer(config) {
12781
- const next = require_prebuffer.prebufferRetentionMs(config);
12914
+ const next = require_restream_intent.prebufferRetentionMs(config);
12782
12915
  this.setPreBufferEnabled(config.enabled);
12783
12916
  this.setPreBufferDuration(config.seconds);
12784
12917
  if (next === this.prebufferMs) return;
@@ -12913,7 +13046,7 @@ var StreamBroker = class StreamBroker {
12913
13046
  */
12914
13047
  getRtpPreRollBuffer(requestedMs) {
12915
13048
  if (!this.rtpRingHasKeyframe) return [];
12916
- const windowMs = require_prebuffer.capPreRollRequestMs(requestedMs, this.prebufferMs);
13049
+ const windowMs = require_restream_intent.capPreRollRequestMs(requestedMs, this.prebufferMs);
12917
13050
  if (windowMs <= 0 || this.rtpRing.length === 0) return this.getRtpPreBuffer();
12918
13051
  const newestTs = this.rtpRing[this.rtpRing.length - 1].readUInt32BE(4);
12919
13052
  const selection = selectPreRollStart(this.rtpPreRollMarks(newestTs), windowMs);
@@ -18295,7 +18428,7 @@ var StreamBrokerManager = class StreamBrokerManager {
18295
18428
  };
18296
18429
  nextOverride.preBuffer = pb;
18297
18430
  const broker = this.brokers.get(brokerIdFor(deviceId, camStreamId));
18298
- if (broker) broker.setPrebuffer(require_prebuffer.resolvePrebuffer({
18431
+ if (broker) broker.setPrebuffer(require_restream_intent.resolvePrebuffer({
18299
18432
  camStreamId,
18300
18433
  clusterSeconds: this.defaultPreBufferSec,
18301
18434
  overrides: pb,
@@ -19238,7 +19371,7 @@ var StreamBrokerManager = class StreamBrokerManager {
19238
19371
  * broker's own flag shipped `true`.
19239
19372
  */
19240
19373
  effectivePrebuffer(deviceId, camStreamId) {
19241
- return require_prebuffer.resolvePrebuffer({
19374
+ return require_restream_intent.resolvePrebuffer({
19242
19375
  camStreamId,
19243
19376
  clusterSeconds: this.defaultPreBufferSec,
19244
19377
  overrides: this.deviceOverrides.get(deviceId)?.preBuffer,
@@ -30154,7 +30287,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
30154
30287
  */
30155
30288
  async purgeFlattenedSettingsKeys() {
30156
30289
  try {
30157
- await require_prebuffer.retireRootKeys({
30290
+ await require_retire_root_keys.retireRootKeys({
30158
30291
  store: this.ctx.api.settingsStore,
30159
30292
  addonId: STREAM_BROKER_ADDON_ID,
30160
30293
  logger: this.ctx.logger,