@camstack/addon-pipeline 1.2.199 → 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-CMe44Kid.js → default-detection-model-BsmxtyZx.js} +1 -1
  4. package/dist/{default-detection-model-O1uBusV2.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-DmCrC2yx.js → dist-D2YTTkRf.js} +424 -426
  8. package/dist/{dist-B23u1tmN.mjs → dist-mEsrAAc3.mjs} +424 -426
  9. package/dist/{lazy-sharp-DBHsD2lh.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-D1MLn6oV.mjs → node-CUxep5tc.mjs} +1 -1
  13. package/dist/{node-DCSTbt6C.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-P24NTDb1.mjs → process-memory-BCqpTruN.mjs} +1 -1
  17. package/dist/{process-memory-9lvvhHym.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-Ck_F5bsO.js → segment-demux-js-DUZVURQd.js} +1 -1
  25. package/dist/{segment-demux-js-Bm0woPkn.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-Sq93hI-g.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-COt259tU.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-c1r1Yddg.js → worker-protocol-CRDaBlJl.js} +1 -1
  39. package/dist/{worker-protocol-BxpGZ0Dt.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
@@ -1,10 +1,10 @@
1
1
  import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
2
- import { A as addonWidgetsSourceCapability, C as RATE_CONTROL_RELAXED, Dt as webrtcSessionCapability, F as cameraStreamsCapability, Ft as BaseAddon, H as deriveBatteryPresence, Ht as makeProfileBrokerId, I as createHwAccelCache, It as CAM_PROFILE_ORDER, K as egressTranscodeSharingKey, Kt as parseProfileBrokerId, L as createLogChannelsProvider, Lt as DeviceFeature, Mt as invocationFromEncodeProfile, Nt as isSoftwareDecode, O as RingBuffer, Ot as errMsg, P as batteryCapability, Pt as BOOT_RECOVERY_BACKOFF_MS, Qt as discriminatedUnion, Rt as DeviceType, Ut as makeSourceBrokerId, Vt as isEvent, Wt as nodePin, Xt as array, Yt as _enum, Zt as boolean, an as string, en as literal, h as EncodeProfileSchema, i as BatteryStatusSchema, in as record, it as maskUrlCredentials, jt as buildFfmpegArgs$1, kt as AUDIO_PRESETS, nn as object, nt as logChannelsCapability, on as union, q as egressTransportFromRequest, sn as EventCategory, tn as number, tt as loadContributionCapability, w as RATE_CONTROL_TIGHT, wt as streamBrokerCapability, yt as resolveEgressDecodeHwAccel, z as declareLogChannel, zt as createEvent } from "../dist-B23u1tmN.mjs";
3
- import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-D1MLn6oV.mjs";
4
- import { t as parseRestreamPath } from "../restream-intent-B4BXZra7.mjs";
2
+ import { A as addonWidgetsSourceCapability, C as RATE_CONTROL_RELAXED, Dt as webrtcSessionCapability, F as cameraStreamsCapability, Ft as BaseAddon, H as deriveBatteryPresence, Ht as makeProfileBrokerId, I as createHwAccelCache, It as CAM_PROFILE_ORDER, K as egressTranscodeSharingKey, Kt as parseProfileBrokerId, L as createLogChannelsProvider, Lt as DeviceFeature, Mt as invocationFromEncodeProfile, Nt as isSoftwareDecode, O as RingBuffer, Ot as errMsg, P as batteryCapability, Pt as BOOT_RECOVERY_BACKOFF_MS, Qt as discriminatedUnion, Rt as DeviceType, Ut as makeSourceBrokerId, Vt as isEvent, Wt as nodePin, Xt as array, Yt as _enum, Zt as boolean, an as string, en as literal, h as EncodeProfileSchema, i as BatteryStatusSchema, in as record, it as maskUrlCredentials, jt as buildFfmpegArgs$1, kt as AUDIO_PRESETS, nn as object, nt as logChannelsCapability, on as union, q as egressTransportFromRequest, sn as EventCategory, tn as number, tt as loadContributionCapability, w as RATE_CONTROL_TIGHT, wt as streamBrokerCapability, yt as resolveEgressDecodeHwAccel, z as declareLogChannel, zt as createEvent } from "../dist-mEsrAAc3.mjs";
3
+ import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-CUxep5tc.mjs";
4
+ import { c as PREBUFFER_MAX_MS, d as resolvePrebuffer, i as parseRestreamPath, l as capPreRollRequestMs, n as NO_DECLARATION, u as prebufferRetentionMs } from "../restream-intent-DWe2kInY.mjs";
5
5
  import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
6
- import { a as retireRootKeys, i as resolvePrebuffer, n as capPreRollRequestMs, o as RowMapStore, r as prebufferRetentionMs, t as PREBUFFER_MAX_MS } from "../prebuffer-DmBYRQWg.mjs";
7
- import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-Bm0woPkn.mjs";
6
+ import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
7
+ import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-xHtB450F.mjs";
8
8
  import { createRequire } from "node:module";
9
9
  import * as os from "node:os";
10
10
  import { networkInterfaces, tmpdir } from "node:os";
@@ -2580,6 +2580,18 @@ var DEFAULT_PRIME_BURST_BOUND = {
2580
2580
  /** Drop/withhold accounting is aggregated and reported at most this often. */
2581
2581
  var DROP_REPORT_INTERVAL_MS$1 = 3e4;
2582
2582
  /**
2583
+ * How often a HISTORY-FREE attach is reported per stream.
2584
+ *
2585
+ * The baseline population is large and uninteresting one line at a time — a
2586
+ * WebRTC bootstrap, a HomeKit leg, an ffprobe — but it is the control group the
2587
+ * pre-roll cost is measured against, so it cannot be silent either. Throttled
2588
+ * here and NOT throttled for an attach that carried history: that population is
2589
+ * rare (17 of ~130 agganci in the 2026-09-07 census) and it is the one the
2590
+ * question is about, so a rate limit sized for the common case would drop
2591
+ * exactly the samples nobody has.
2592
+ */
2593
+ var PLAIN_ATTACH_REPORT_INTERVAL_MS = 1e4;
2594
+ /**
2583
2595
  * Defensive hard bound on the per-keyframe RTP/NAL cache (both the passthrough
2584
2596
  * `lastKeyframeRtpPackets` and the Annex-B `lastKeyframeNals`). A single
2585
2597
  * keyframe GOP is at most a few large NALs — well under a megabyte or two even
@@ -2729,6 +2741,8 @@ var RtspRestreamer = class {
2729
2741
  dropsLiveLatched = 0;
2730
2742
  dropsLiveClosed = 0;
2731
2743
  dropReportMs = 0;
2744
+ /** Last history-free attach reported — see {@link PLAIN_ATTACH_REPORT_INTERVAL_MS}. */
2745
+ plainAttachReportMs = 0;
2732
2746
  /** Numeric device id parsed once from the brokerId, for `tags.deviceId`. */
2733
2747
  deviceId;
2734
2748
  constructor(streamName) {
@@ -2988,7 +3002,7 @@ var RtspRestreamer = class {
2988
3002
  if (primePackets.length === 0) return;
2989
3003
  const firstTs = primePackets[0].data.readUInt32BE(4);
2990
3004
  const spanMs = (primePackets[primePackets.length - 1].data.readUInt32BE(4) - firstTs | 0) / 90;
2991
- const preRollState = { plan: null };
3005
+ const preRollPlans = /* @__PURE__ */ new Map();
2992
3006
  const bound = this.currentPrimeBound();
2993
3007
  /**
2994
3008
  * Resolve (and memoize) the plan for one intent.
@@ -3000,28 +3014,33 @@ var RtspRestreamer = class {
3000
3014
  * current GOP or a supplier bug can, and D57's remedy (join clean at the
3001
3015
  * next live keyframe) is the right one for a burst nobody asked for.
3002
3016
  */
3003
- const planFor = () => {
3004
- if (preRollState.plan) return preRollState.plan;
3017
+ const planFor = (requestedMs) => {
3018
+ const memo = preRollPlans.get(requestedMs);
3019
+ if (memo) return memo;
3005
3020
  const burst = this.preRollSupplier?.();
3006
- const window = fromRing && burst && burst.preRollMs > 0 ? burst.packets() : [];
3021
+ const window = fromRing && burst && burst.preRollMs > 0 && requestedMs > 0 ? burst.packets(requestedMs) : [];
3007
3022
  const packets = window.length > primePackets.length ? window.map((data) => ({
3008
3023
  data,
3009
3024
  marker: (data[1] & 128) !== 0
3010
3025
  })) : [];
3011
3026
  const windowSpanMs = packets.length > 0 ? (packets[packets.length - 1].data.readUInt32BE(4) - packets[0].data.readUInt32BE(4) | 0) / 90 : 0;
3012
- const windowBound = packets.length > 0 ? preRollSpanBound(bound, burst?.preRollMs ?? 0) : bound;
3027
+ const servableMs = Math.min(requestedMs, burst?.preRollMs ?? 0);
3028
+ const windowBound = packets.length > 0 ? preRollSpanBound(bound, servableMs) : bound;
3013
3029
  const plan = {
3014
3030
  packets,
3015
3031
  active: packets.length > 0,
3016
- configuredMs: burst?.preRollMs ?? 0,
3032
+ retainedMs: burst?.preRollMs ?? 0,
3033
+ requestedMs,
3017
3034
  spanMs: windowSpanMs,
3018
3035
  overBound: packets.length > 0 && (windowSpanMs > windowBound.maxSpanMs || packets.length > windowBound.maxPackets)
3019
3036
  };
3020
- preRollState.plan = plan;
3037
+ preRollPlans.set(requestedMs, plan);
3021
3038
  return plan;
3022
3039
  };
3023
3040
  const served = [];
3024
3041
  const servedPreRoll = /* @__PURE__ */ new Map();
3042
+ /** The plan each pre-roll session was actually served, for its attach line. */
3043
+ const servedPlan = /* @__PURE__ */ new Map();
3025
3044
  let withheld = 0;
3026
3045
  let detectionIntentServed = 0;
3027
3046
  for (const sessionId of ready) {
@@ -3029,8 +3048,9 @@ var RtspRestreamer = class {
3029
3048
  if (!session) continue;
3030
3049
  const detectionIntent = session.isDetectionIntent();
3031
3050
  const preRollIntent = session.preRollIntent();
3032
- if (preRollIntent !== null) {
3033
- const plan = planFor();
3051
+ const requestedMs = session.preRollRequestMs();
3052
+ if (preRollIntent !== null && requestedMs > 0) {
3053
+ const plan = planFor(requestedMs);
3034
3054
  if (plan.active) {
3035
3055
  if (plan.overBound) {
3036
3056
  this.dropsWithheldSpan++;
@@ -3038,6 +3058,7 @@ var RtspRestreamer = class {
3038
3058
  continue;
3039
3059
  }
3040
3060
  servedPreRoll.set(sessionId, preRollIntent);
3061
+ servedPlan.set(sessionId, plan);
3041
3062
  served.push(sessionId);
3042
3063
  this.logger?.info("restream pre-roll served", {
3043
3064
  tags: { deviceId: this.deviceId },
@@ -3046,7 +3067,8 @@ var RtspRestreamer = class {
3046
3067
  sessionId,
3047
3068
  intent: preRollIntent,
3048
3069
  consumer: session.getUserAgent() ?? "unknown",
3049
- configuredMs: plan.configuredMs,
3070
+ requestedMs: plan.requestedMs,
3071
+ retainedMs: plan.retainedMs,
3050
3072
  spanMs: Math.round(plan.spanMs),
3051
3073
  packets: plan.packets.length
3052
3074
  }
@@ -3075,8 +3097,16 @@ var RtspRestreamer = class {
3075
3097
  }
3076
3098
  if (withheld > 0) this.reportDrops(false);
3077
3099
  if (served.length === 0) return;
3078
- /** Media actually delivered, or `-1` when nobody asked for history at all. */
3079
- const servedSpanMs = preRollState.plan === null ? -1 : Math.round(preRollState.plan.spanMs);
3100
+ /**
3101
+ * Media actually delivered — the WIDEST window any session took, or `-1`
3102
+ * when nobody asked for history at all.
3103
+ *
3104
+ * A max and not a single plan's span since D382: two sessions on the same
3105
+ * keyframe can be served different windows, and the number this aggregate
3106
+ * line owes its reader is the largest burst this stream let out. The
3107
+ * per-attach truth is on `restream attach live-edge`, one line per session.
3108
+ */
3109
+ const servedSpanMs = preRollPlans.size === 0 ? -1 : Math.round(Math.max(...[...preRollPlans.values()].map((p) => p.spanMs)));
3080
3110
  /** Which declared intents joined this burst — `0` is "none", not "off". */
3081
3111
  const intentsServed = new Set(servedPreRoll.values());
3082
3112
  this.logger?.info("restream prime (lat-probe)", {
@@ -3103,15 +3133,30 @@ var RtspRestreamer = class {
3103
3133
  prime: true,
3104
3134
  keyframeAuStart: false
3105
3135
  }));
3106
- const preRollItems = servedPreRoll.size > 0 ? (preRollState.plan?.packets ?? []).map((packet) => ({
3107
- packet,
3108
- prime: true,
3109
- keyframeAuStart: false
3110
- })) : [];
3136
+ const preRollItems = /* @__PURE__ */ new Map();
3137
+ const itemsFor = (plan) => {
3138
+ const memo = preRollItems.get(plan.requestedMs);
3139
+ if (memo) return memo;
3140
+ const built = plan.packets.map((packet) => ({
3141
+ packet,
3142
+ prime: true,
3143
+ keyframeAuStart: false
3144
+ }));
3145
+ preRollItems.set(plan.requestedMs, built);
3146
+ return built;
3147
+ };
3111
3148
  for (const sessionId of served) {
3112
3149
  this.pendingKeyframe.delete(sessionId);
3113
- const intent = servedPreRoll.get(sessionId);
3114
- this.startPrimeReplay(sessionId, intent ? preRollItems : items, intent ?? null);
3150
+ const intent = servedPreRoll.get(sessionId) ?? null;
3151
+ const plan = servedPlan.get(sessionId) ?? null;
3152
+ this.startPrimeReplay(sessionId, plan ? itemsFor(plan) : items, intent, {
3153
+ intent,
3154
+ requestedMs: plan?.requestedMs ?? 0,
3155
+ retainedMs: plan?.retainedMs ?? this.preRollSupplier?.().preRollMs ?? 0,
3156
+ preRollSpanMs: plan ? Math.round(plan.spanMs - spanMs) : 0,
3157
+ burstSpanMs: Math.round(plan ? plan.spanMs : spanMs),
3158
+ gopSpanMs: Math.round(spanMs)
3159
+ });
3115
3160
  }
3116
3161
  }
3117
3162
  /**
@@ -3124,7 +3169,7 @@ var RtspRestreamer = class {
3124
3169
  * session that went away — aborts the whole replay rather than punching holes
3125
3170
  * in the GOP: the session is left latched and rejoins at the next keyframe AU.
3126
3171
  */
3127
- startPrimeReplay(sessionId, items, preRollIntent) {
3172
+ startPrimeReplay(sessionId, items, preRollIntent, attach) {
3128
3173
  const replay = new PacedRtpReplay((item) => {
3129
3174
  const session = this.sessions.get(sessionId);
3130
3175
  if (!session) {
@@ -3154,6 +3199,7 @@ var RtspRestreamer = class {
3154
3199
  durationMs: stats.durationMs
3155
3200
  }
3156
3201
  });
3202
+ this.reportAttach(sessionId, attach, stats);
3157
3203
  this.logger?.debug("restream prime drained", {
3158
3204
  tags: { deviceId: this.deviceId },
3159
3205
  meta: {
@@ -3172,6 +3218,58 @@ var RtspRestreamer = class {
3172
3218
  this.activeReplays.set(sessionId, replay);
3173
3219
  replay.start(items);
3174
3220
  }
3221
+ /**
3222
+ * ONE LINE PER ATTACH: what this session asked for, what it was handed, and
3223
+ * how long it took to reach the live edge.
3224
+ *
3225
+ * The question this exists to answer is "does the pre-roll delay the first
3226
+ * inference, and by how much" — and it could not be answered before. The only
3227
+ * measurement in reach was `pipeline stats`, a 10 s window whose averages
3228
+ * dissolve a 4 s transient by construction, and the broker's own lines were
3229
+ * per-STREAM aggregates that named a window without naming a session or a
3230
+ * duration. Seventeen served bursts inside ninety minutes of ten-second
3231
+ * averages is not a measurement, it is a hope.
3232
+ *
3233
+ * `liveEdgeMs` is measured from PLAY, the first instant the restreamer owes
3234
+ * this session bytes, to the moment its burst has drained and it is on the
3235
+ * live feed. It therefore contains the wait for a keyframe AND the paced
3236
+ * drain — the two things a burst can cost a joiner on this side of the
3237
+ * socket. What it deliberately does NOT contain is the consumer's own decode
3238
+ * of that burst: that is measured where it happens, on
3239
+ * `session-decode: detection first frame`, and the two lines are joined by
3240
+ * `deviceId` plus the per-attach `consumer` tag.
3241
+ *
3242
+ * `aborted` is on the line because a burst that never reached the socket is
3243
+ * not a burst this session paid for, and averaging those in would understate
3244
+ * the cost of the ones that did.
3245
+ */
3246
+ reportAttach(sessionId, attach, stats) {
3247
+ const session = this.sessions.get(sessionId);
3248
+ const playAt = session?.getPlayAt() ?? 0;
3249
+ if (!(attach.preRollSpanMs > 0)) {
3250
+ const now = Date.now();
3251
+ if (now - this.plainAttachReportMs < PLAIN_ATTACH_REPORT_INTERVAL_MS) return;
3252
+ this.plainAttachReportMs = now;
3253
+ }
3254
+ this.logger?.info("restream attach live-edge", {
3255
+ tags: { deviceId: this.deviceId },
3256
+ meta: {
3257
+ streamName: this.streamName,
3258
+ sessionId,
3259
+ consumer: session?.getUserAgent() ?? "unknown",
3260
+ intent: attach.intent ?? "none",
3261
+ requestedMs: attach.requestedMs,
3262
+ retainedMs: attach.retainedMs,
3263
+ preRollSpanMs: attach.preRollSpanMs,
3264
+ burstSpanMs: attach.burstSpanMs,
3265
+ gopSpanMs: attach.gopSpanMs,
3266
+ liveEdgeMs: playAt > 0 ? Date.now() - playAt : -1,
3267
+ drainMs: Math.round(stats.durationMs),
3268
+ packets: stats.sent,
3269
+ aborted: stats.aborted
3270
+ }
3271
+ });
3272
+ }
3175
3273
  /** Cancel a session's in-flight join replay (teardown / stream reset). */
3176
3274
  abortReplay(sessionId) {
3177
3275
  const replay = this.activeReplays.get(sessionId);
@@ -3454,15 +3552,31 @@ var RtspSession = class {
3454
3552
  /** When true, this session receives video-only (no audio RTP). */
3455
3553
  muted;
3456
3554
  /**
3457
- * The consumer purpose this session DECLARED on its dial path (see
3458
- * `restream-intent.ts`), or `null` when it declared none — which is every
3459
- * consumer except the per-session detection decode.
3555
+ * What this session DECLARED on its dial path (see `restream-intent.ts`): its
3556
+ * consumer purpose, and how many ms of retained history it asked for.
3557
+ * {@link NO_DECLARATION} when it declared nothing — which is every consumer
3558
+ * except the per-session detection decode and the recorder's writer.
3460
3559
  *
3461
3560
  * Kept separate from {@link muted} deliberately: `isMuted()` is shared by the
3462
3561
  * WebRTC/HomeKit loopback legs, and the join rule that keys on it must stay
3463
3562
  * true for them. Only an explicit declaration changes behaviour.
3563
+ *
3564
+ * One field and not two, because the purpose and the amount are one
3565
+ * statement (D382): a session that names a purpose without naming an amount
3566
+ * is asking for nothing, and that has to be as unmistakable here as it is on
3567
+ * the wire.
3464
3568
  */
3465
- intent;
3569
+ declaration;
3570
+ /**
3571
+ * Epoch ms at which this session reached PLAY, or `0` before it does.
3572
+ *
3573
+ * The clock the join is measured against. `connectedAt` is the TCP accept,
3574
+ * which includes DESCRIBE/SETUP and, for a cold broker, the whole upstream
3575
+ * dial — so it answers "how slow was the camera", not "what did the burst
3576
+ * cost this session". Time to the live edge is measured from PLAY, because
3577
+ * PLAY is the first instant at which the restreamer owes this session bytes.
3578
+ */
3579
+ playAt = 0;
3466
3580
  /**
3467
3581
  * Latch for ANY session that has started dropping video packets on socket
3468
3582
  * backlog — it was written for the `muted` decoder loopback, which was the
@@ -3476,12 +3590,12 @@ var RtspSession = class {
3476
3590
  /** Consecutive keyframe AUs at which this session was still unwritable.
3477
3591
  * Reset by any successful write — the bound is CONSECUTIVE misses. */
3478
3592
  missedKeyframes = 0;
3479
- constructor(socket, sdp, onClose, muted, intent) {
3593
+ constructor(socket, sdp, onClose, muted, declaration) {
3480
3594
  this.socket = socket;
3481
3595
  this.sdp = sdp;
3482
3596
  this.onClose = onClose;
3483
3597
  this.muted = muted ?? false;
3484
- this.intent = intent ?? null;
3598
+ this.declaration = declaration ?? NO_DECLARATION;
3485
3599
  this.sessionId = randomUUID().replace(/-/g, "").slice(0, 16);
3486
3600
  socket.on("data", (chunk) => {
3487
3601
  if (this.closed) return;
@@ -3525,7 +3639,23 @@ var RtspSession = class {
3525
3639
  * construction and drains the burst instead of accumulating it.
3526
3640
  */
3527
3641
  isDetectionIntent() {
3528
- return this.intent === "detection";
3642
+ return this.declaration.intent === "detection";
3643
+ }
3644
+ /**
3645
+ * Ms of retained history this session ASKED for on its dial. `0` = none.
3646
+ *
3647
+ * Read by `RtspRestreamer.servePendingSessionsRtp`, which serves the smaller
3648
+ * of this and what the ring holds. A session that declared an intent and no
3649
+ * amount takes the ordinary current-GOP burst — the intent still buys it
3650
+ * whatever else the intent buys (for detection, the live-edge exemption), and
3651
+ * nothing more.
3652
+ */
3653
+ preRollRequestMs() {
3654
+ return this.declaration.preRollRequestMs;
3655
+ }
3656
+ /** Epoch ms at which this session reached PLAY; `0` if it has not. */
3657
+ getPlayAt() {
3658
+ return this.playAt;
3529
3659
  }
3530
3660
  /**
3531
3661
  * Does this session get the PRE-ROLL window instead of the current GOP?
@@ -3542,12 +3672,14 @@ var RtspSession = class {
3542
3672
  * WHICH pre-roll window this session gets, or `null` when it declared no
3543
3673
  * intent and takes the ordinary current-GOP join burst.
3544
3674
  *
3545
- * The two windows are two numbers (D193), so the restreamer needs the intent
3546
- * and not just the bit: a recorder handed detection's window would carry the
3547
- * pre-roll of a feature it is not, and on this fleet detection's is 0.
3675
+ * There is ONE window now (D381) and one ASK per session (D382), so what the
3676
+ * restreamer still needs from this is WHO joined — the identity it prints
3677
+ * beside the window it served, and the reason a recorder and a detector are
3678
+ * distinguishable on a line that reports one number.
3548
3679
  */
3549
3680
  preRollIntent() {
3550
- return this.intent === "detection" || this.intent === "recording" ? this.intent : null;
3681
+ const intent = this.declaration.intent;
3682
+ return intent === "detection" || intent === "recording" ? intent : null;
3551
3683
  }
3552
3684
  /**
3553
3685
  * Diagnostic snapshot consumed by `StreamBroker.listClients` (Phase 10).
@@ -3768,6 +3900,7 @@ var RtspSession = class {
3768
3900
  }
3769
3901
  handlePlay(req) {
3770
3902
  this.playing = true;
3903
+ if (this.playAt === 0) this.playAt = Date.now();
3771
3904
  this.respond(req, 200, "OK", {
3772
3905
  Session: this.sessionId,
3773
3906
  Range: "npt=0.000-"
@@ -3924,7 +4057,7 @@ var RtspListenServer = class RtspListenServer {
3924
4057
  async dispatch(socket, requestBuffer) {
3925
4058
  const uriMatch = (requestBuffer.split("\r\n")[0] ?? "").match(/\s(rtsp:\/\/[^\s]+)\s/);
3926
4059
  const pathMatch = (uriMatch ? uriMatch[1] : "").match(/rtsp:\/\/[^/]+(\/.*?)(?:\s|$)/);
3927
- const { lookupPath, muted: isMuted, intent } = parseRestreamPath(pathMatch ? pathMatch[1].replace(/^\//, "").replace(/\/trackID=\d+$/, "") : "");
4060
+ const { lookupPath, muted: isMuted, declaration } = parseRestreamPath(pathMatch ? pathMatch[1].replace(/^\//, "").replace(/\/trackID=\d+$/, "") : "");
3928
4061
  const restreamer = this.restreamers.get(lookupPath);
3929
4062
  if (!restreamer) {
3930
4063
  socket.write(`RTSP/1.0 404 Not Found\r\nCSeq: 1\r\n\r\n`);
@@ -3953,7 +4086,7 @@ var RtspListenServer = class RtspListenServer {
3953
4086
  const session = new RtspSession(socket, isMuted ? restreamer.getMutedSdp() ?? restreamer.getSdp() ?? "" : restreamer.getSdp() ?? "", () => {
3954
4087
  restreamer.removeSession(session.getSessionId());
3955
4088
  releaseOnce();
3956
- }, isMuted, intent);
4089
+ }, isMuted, declaration);
3957
4090
  restreamer.addSession(session);
3958
4091
  releaseOnce();
3959
4092
  socket.unshift(Buffer.from(requestBuffer, "ascii"));
@@ -11698,7 +11831,7 @@ var StreamBroker = class StreamBroker {
11698
11831
  this.rtspRestreamer.setPrimeBoundSupplier(() => this.joinBurstBound);
11699
11832
  this.rtspRestreamer.setPreRollSupplier(() => {
11700
11833
  return {
11701
- packets: () => this.getRtpPreRollBuffer(),
11834
+ packets: (requestedMs) => this.getRtpPreRollBuffer(requestedMs),
11702
11835
  preRollMs: this.prebufferMs
11703
11836
  };
11704
11837
  });
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Sq93hI-g.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CLXImQ3R.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-DmCrC2yx.js");
1
+ const require_dist = require("./dist-D2YTTkRf.js");
2
2
  //#region src/session-decode/native-lease-config.ts
3
3
  /**
4
4
  * Resolution of the native-frame hold / tile knobs for one decode worker.
@@ -1,4 +1,4 @@
1
- import { b as NativeLeaseSettingsSchema, p as DEFAULT_NATIVE_LEASE_SETTINGS, y as NativeLeaseAdmissionSchema } from "./dist-B23u1tmN.mjs";
1
+ import { b as NativeLeaseSettingsSchema, p as DEFAULT_NATIVE_LEASE_SETTINGS, y as NativeLeaseAdmissionSchema } from "./dist-mEsrAAc3.mjs";
2
2
  //#region src/session-decode/native-lease-config.ts
3
3
  /**
4
4
  * Resolution of the native-frame hold / tile knobs for one decode worker.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.199",
3
+ "version": "1.2.200",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",
@@ -1,72 +0,0 @@
1
- /**
2
- * Path suffix for the audio-free variant dialed by a DETECTION decode session.
3
- * Muted in every respect (no audio, still `isMuted()`), plus exempt from the
4
- * live-edge join withhold.
5
- */
6
- var DETECTION_MUTED_PATH_SUFFIX = "/muted-detection";
7
- /**
8
- * Path suffix for a RECORDING dial — the `events`-band recorder's ffmpeg.
9
- *
10
- * NOT muted: a recording keeps its audio, so this cannot ride the detection
11
- * suffix. What it shares with detection is the reason for existing — it attaches
12
- * AFTER the trigger, so without the retained history its footage begins after
13
- * the event. Measured on camera 1438 (audit 2026-08-20): the footage started
14
- * after the motion in 60 % of episodes, because the writer is spawned at the
15
- * trigger and ffmpeg's first frame is at best the live edge.
16
- */
17
- var RECORDING_PATH_SUFFIX = "/recording";
18
- /**
19
- * Split a restream request path (already stripped of `rtsp://host:port/` and of
20
- * any `/trackID=N` control suffix) into its token, mute bit and intent.
21
- */
22
- function parseRestreamPath(streamPath) {
23
- if (streamPath.endsWith("/recording")) return {
24
- lookupPath: streamPath.slice(0, -10),
25
- muted: false,
26
- intent: "recording"
27
- };
28
- if (streamPath.endsWith("/muted-detection")) return {
29
- lookupPath: streamPath.slice(0, -16),
30
- muted: true,
31
- intent: "detection"
32
- };
33
- if (streamPath.endsWith("/muted")) return {
34
- lookupPath: streamPath.slice(0, -6),
35
- muted: true,
36
- intent: null
37
- };
38
- return {
39
- lookupPath: streamPath,
40
- muted: false,
41
- intent: null
42
- };
43
- }
44
- /**
45
- * Rewrite an acquired restream URL to declare the detection intent.
46
- *
47
- * Only a MUTED url is rewritten. `resolveSourceUrl` falls back to the
48
- * audio-bearing url when the broker has no muted variant; that session is not
49
- * muted, the live-edge withhold never applied to it, and appending the suffix
50
- * would only break the token lookup. Idempotent.
51
- */
52
- function withDetectionIntent(restreamUrl) {
53
- if (restreamUrl.endsWith("/muted-detection")) return restreamUrl;
54
- if (!restreamUrl.endsWith("/muted")) return restreamUrl;
55
- return `${restreamUrl.slice(0, -6)}${DETECTION_MUTED_PATH_SUFFIX}`;
56
- }
57
- /**
58
- * Rewrite an acquired restream URL to declare the RECORDING intent.
59
- *
60
- * The mirror image of {@link withDetectionIntent}'s guard: only a NON-muted url
61
- * is rewritten. A recorder that was handed the muted variant (a device with no
62
- * audio, or `audio: 'none'`) is already the detection population's shape, and
63
- * appending this suffix would only break the token lookup. Idempotent.
64
- */
65
- function withRecordingIntent(restreamUrl) {
66
- if (restreamUrl.endsWith("/recording")) return restreamUrl;
67
- if (restreamUrl.endsWith("/muted")) return restreamUrl;
68
- if (restreamUrl.endsWith("/muted-detection")) return restreamUrl;
69
- return `${restreamUrl}${RECORDING_PATH_SUFFIX}`;
70
- }
71
- //#endregion
72
- export { withDetectionIntent as n, withRecordingIntent as r, parseRestreamPath as t };
@@ -1,89 +0,0 @@
1
- /**
2
- * Path suffix for the audio-free variant dialed by a DETECTION decode session.
3
- * Muted in every respect (no audio, still `isMuted()`), plus exempt from the
4
- * live-edge join withhold.
5
- */
6
- var DETECTION_MUTED_PATH_SUFFIX = "/muted-detection";
7
- /**
8
- * Path suffix for a RECORDING dial — the `events`-band recorder's ffmpeg.
9
- *
10
- * NOT muted: a recording keeps its audio, so this cannot ride the detection
11
- * suffix. What it shares with detection is the reason for existing — it attaches
12
- * AFTER the trigger, so without the retained history its footage begins after
13
- * the event. Measured on camera 1438 (audit 2026-08-20): the footage started
14
- * after the motion in 60 % of episodes, because the writer is spawned at the
15
- * trigger and ffmpeg's first frame is at best the live edge.
16
- */
17
- var RECORDING_PATH_SUFFIX = "/recording";
18
- /**
19
- * Split a restream request path (already stripped of `rtsp://host:port/` and of
20
- * any `/trackID=N` control suffix) into its token, mute bit and intent.
21
- */
22
- function parseRestreamPath(streamPath) {
23
- if (streamPath.endsWith("/recording")) return {
24
- lookupPath: streamPath.slice(0, -10),
25
- muted: false,
26
- intent: "recording"
27
- };
28
- if (streamPath.endsWith("/muted-detection")) return {
29
- lookupPath: streamPath.slice(0, -16),
30
- muted: true,
31
- intent: "detection"
32
- };
33
- if (streamPath.endsWith("/muted")) return {
34
- lookupPath: streamPath.slice(0, -6),
35
- muted: true,
36
- intent: null
37
- };
38
- return {
39
- lookupPath: streamPath,
40
- muted: false,
41
- intent: null
42
- };
43
- }
44
- /**
45
- * Rewrite an acquired restream URL to declare the detection intent.
46
- *
47
- * Only a MUTED url is rewritten. `resolveSourceUrl` falls back to the
48
- * audio-bearing url when the broker has no muted variant; that session is not
49
- * muted, the live-edge withhold never applied to it, and appending the suffix
50
- * would only break the token lookup. Idempotent.
51
- */
52
- function withDetectionIntent(restreamUrl) {
53
- if (restreamUrl.endsWith("/muted-detection")) return restreamUrl;
54
- if (!restreamUrl.endsWith("/muted")) return restreamUrl;
55
- return `${restreamUrl.slice(0, -6)}${DETECTION_MUTED_PATH_SUFFIX}`;
56
- }
57
- /**
58
- * Rewrite an acquired restream URL to declare the RECORDING intent.
59
- *
60
- * The mirror image of {@link withDetectionIntent}'s guard: only a NON-muted url
61
- * is rewritten. A recorder that was handed the muted variant (a device with no
62
- * audio, or `audio: 'none'`) is already the detection population's shape, and
63
- * appending this suffix would only break the token lookup. Idempotent.
64
- */
65
- function withRecordingIntent(restreamUrl) {
66
- if (restreamUrl.endsWith("/recording")) return restreamUrl;
67
- if (restreamUrl.endsWith("/muted")) return restreamUrl;
68
- if (restreamUrl.endsWith("/muted-detection")) return restreamUrl;
69
- return `${restreamUrl}${RECORDING_PATH_SUFFIX}`;
70
- }
71
- //#endregion
72
- Object.defineProperty(exports, "parseRestreamPath", {
73
- enumerable: true,
74
- get: function() {
75
- return parseRestreamPath;
76
- }
77
- });
78
- Object.defineProperty(exports, "withDetectionIntent", {
79
- enumerable: true,
80
- get: function() {
81
- return withDetectionIntent;
82
- }
83
- });
84
- Object.defineProperty(exports, "withRecordingIntent", {
85
- enumerable: true,
86
- get: function() {
87
- return withRecordingIntent;
88
- }
89
- });