@camstack/addon-pipeline 1.2.288 → 1.2.290

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 (33) 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-DTvsx_iX.mjs → default-detection-model-B64MIW2h.mjs} +1 -1
  4. package/dist/{default-detection-model-D5kMByfZ.js → default-detection-model-CN_Iu8NF.js} +1 -1
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-CegiJ2Pw.js → dist-BIlnUdBn.js} +1223 -860
  8. package/dist/{dist-mEtwupOP.mjs → dist-C4JqXCw-.mjs} +1206 -861
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/{node-B8p-rV9H.mjs → node-C-WfKJ6b.mjs} +1 -1
  12. package/dist/{node-C8jhiPW2.js → node-CVlJhqng.js} +1 -1
  13. package/dist/pipeline-runner/index.js +3 -3
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/{process-memory-C5bRH8-t.mjs → process-memory-CJKKFNar.mjs} +1 -1
  16. package/dist/{process-memory-lPbJ1ta2.js → process-memory-ZUucMC8Z.js} +1 -1
  17. package/dist/recorder/index.js +7426 -7264
  18. package/dist/recorder/index.mjs +7426 -7264
  19. package/dist/{segment-demux-js-BP5vqRPY.mjs → segment-demux-js-4OAHyQ3t.mjs} +1 -1
  20. package/dist/{segment-demux-js-Dv1EPPdH.js → segment-demux-js-D0mqN4RC.js} +1 -1
  21. package/dist/stream-broker/_stub.js +2 -2
  22. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BqjkSRCu.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-r5TULO-I.mjs} +2 -2
  23. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D7VLO8g1.mjs +26 -0
  24. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-UqRrHdtw.mjs +26 -0
  25. package/dist/stream-broker/demux-worker-child.js +1 -1
  26. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  27. package/dist/stream-broker/{hostInit-C4EuFQ_w.mjs → hostInit-DPdwl8-T.mjs} +2 -2
  28. package/dist/stream-broker/index.js +412 -44
  29. package/dist/stream-broker/index.mjs +412 -44
  30. package/dist/stream-broker/remoteEntry.js +1 -1
  31. package/package.json +5 -3
  32. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DqcT4fRG.mjs +0 -26
  33. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DCbVs3IC.mjs +0 -26
@@ -1,10 +1,10 @@
1
1
  import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
2
- import { $t as buildFfmpegArgs$1, B as addonWidgetsSourceCapability, Cn as array, Dn as literal, G as cameraStreamsCapability, Gt as streamBrokerCapability, I as RingBuffer, J as createHwAccelCache, M as RATE_CONTROL_TIGHT, Mn as string, Mt as rankStreams, Nn as union, On as number, Pn as EventCategory, Q as deriveBatteryPresence, Rt as resolveEgressDecodeHwAccel, Sn as _enum, Tn as discriminatedUnion, W as batteryCapability, X as declareLogChannel, Xt as errMsg, Y as createLogChannelsProvider, Yt as webrtcSessionCapability, Zt as AUDIO_PRESETS, _n as nodePin, a as CLIP_BROKER_PACED_RATES, an as CamProfileSchema, en as invocationFromEncodeProfile, fn as expandAudioChunkToF32le, gn as makeSourceBrokerId, gt as logChannelsCapability, hn as makeProfileBrokerId, ht as loadContributionCapability, i as BatteryStatusSchema, in as CAM_PROFILE_ORDER, j as RATE_CONTROL_RELAXED, jn as record, kn as object, ln as createEvent, mn as isEvent, nn as BOOT_RECOVERY_BACKOFF_MS, nt as egressTranscodeSharingKey, on as DeviceFeature, q as clipExportFailureOf, rn as BaseAddon, rt as egressTransportFromRequest, sn as DeviceType, tn as isSoftwareDecode, vt as maskUrlCredentials, w as G711_SCALE_CORRECTION_DB, wn as boolean, x as EncodeProfileSchema, yn as parseProfileBrokerId } from "../dist-mEtwupOP.mjs";
3
- import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-B8p-rV9H.mjs";
2
+ import { $t as errMsg, An as literal, Dn as boolean, En as array, Fn as string, H as addonWidgetsSourceCapability, In as union, Jt as streamBrokerCapability, K as batteryCapability, Ln as EventCategory, M as RATE_CONTROL_TIGHT, Mn as object, On as discriminatedUnion, Pn as record, Pt as rankStreams, Q as declareLogChannel, Qt as webrtcSessionCapability, R as RingBuffer, Sn as parseProfileBrokerId, Tn as _enum, Vt as resolveEgressDecodeHwAccel, X as createHwAccelCache, Y as clipExportFailureOf, Z as createLogChannelsProvider, _n as isEvent, _t as loadContributionCapability, a as CLIP_BROKER_PACED_RATES, an as BOOT_RECOVERY_BACKOFF_MS, at as egressTransportFromRequest, bn as nodePin, bt as maskUrlCredentials, cn as CamProfileSchema, en as AUDIO_PRESETS, et as deriveBatteryPresence, fn as createEvent, hn as expandAudioChunkToF32le, i as BatteryStatusSchema, in as isSoftwareDecode, it as egressTranscodeSharingKey, j as RATE_CONTROL_RELAXED, jn as number, ln as DeviceFeature, nn as buildFfmpegArgs$1, o as CLIP_STILL_REASON_CODE_HEADER, on as BaseAddon, q as cameraStreamsCapability, rn as invocationFromEncodeProfile, sn as CAM_PROFILE_ORDER, un as DeviceType, vn as makeProfileBrokerId, vt as logChannelsCapability, w as G711_SCALE_CORRECTION_DB, x as EncodeProfileSchema, yn as makeSourceBrokerId } from "../dist-C4JqXCw-.mjs";
3
+ import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-C-WfKJ6b.mjs";
4
4
  import { a as parseRestreamPath, d as PREBUFFER_MAX_MS, f as capPreRollRequestMs, i as isLiveEdgeOnlyDial, m as resolvePrebuffer, n as NO_DECLARATION, p as prebufferRetentionMs, s as sdpVariantFor, u as withSentryIntent } from "../restream-intent-DyGmLAU9.mjs";
5
5
  import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
6
6
  import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
7
- import { a as avccSampleToAnnexB, i as require_mp4box_all, n as demuxSegmentPreferJs, o as buildKeyframeAnnexB, r as runFfmpegOnce, s as parseNalLengthSize, t as demuxSegmentFullJs } from "../segment-demux-js-BP5vqRPY.mjs";
7
+ import { a as avccSampleToAnnexB, i as require_mp4box_all, n as demuxSegmentPreferJs, o as buildKeyframeAnnexB, r as runFfmpegOnce, s as parseNalLengthSize, t as demuxSegmentFullJs } from "../segment-demux-js-4OAHyQ3t.mjs";
8
8
  import { createRequire } from "node:module";
9
9
  import * as os from "node:os";
10
10
  import { networkInterfaces, tmpdir } from "node:os";
@@ -26560,6 +26560,22 @@ function gapReachable(gap, epochMs) {
26560
26560
  }
26561
26561
  //#endregion
26562
26562
  //#region src/stream-broker/recorded/control-protocol.ts
26563
+ /**
26564
+ * The rates a clip may be OFFERED at — re-exported from the capability, which
26565
+ * is where the one list lives (D612). The broker owns the clamp; the cap owns
26566
+ * the ladder; a second list here would be the second authority D62 forbids.
26567
+ *
26568
+ * Declared at the top of the file because the clamp and the `setRate` bound
26569
+ * are DERIVED from its ends (D621). They were three literals — this array, a
26570
+ * `Math.min(4, …)` and a `.max(4)` — and that is precisely how 8 and 16 came
26571
+ * to be offered while the clamp served 4 with no line anywhere saying so. One
26572
+ * decision now has one place to be written down.
26573
+ */
26574
+ var BROKER_PACED_RATES = CLIP_BROKER_PACED_RATES;
26575
+ /** The slowest offered rate. Anything slower is clamped up to it. */
26576
+ var MIN_PACED_RATE = Math.min(...BROKER_PACED_RATES);
26577
+ /** The fastest offered rate. Anything faster is refused at the wire. */
26578
+ var MAX_PACED_RATE = Math.max(...BROKER_PACED_RATES);
26563
26579
  var ControlMessageSchema = discriminatedUnion("t", [
26564
26580
  object({
26565
26581
  t: literal("playRecorded"),
@@ -26626,7 +26642,7 @@ var ControlMessageSchema = discriminatedUnion("t", [
26626
26642
  }),
26627
26643
  object({
26628
26644
  t: literal("setRate"),
26629
- rate: number().min(0).max(4)
26645
+ rate: number().min(0).max(MAX_PACED_RATE)
26630
26646
  }),
26631
26647
  object({
26632
26648
  t: literal("stepFrame"),
@@ -26639,22 +26655,41 @@ var ControlMessageSchema = discriminatedUnion("t", [
26639
26655
  })
26640
26656
  ]);
26641
26657
  /**
26642
- * Clamp a requested playback rate onto the supported domain: {0} ∪ [0.25, 4].
26643
- * 0 (and any non-finite / non-positive input) pauses; anything else is a
26644
- * continuous rate clamped into [0.25, 4]. Single server-side authority — the
26645
- * feeder applies it to every inbound `setRate`.
26658
+ * Clamp a requested playback rate onto the supported domain:
26659
+ * {0} ∪ [{@link MIN_PACED_RATE}, {@link MAX_PACED_RATE}] — the ends of
26660
+ * {@link BROKER_PACED_RATES} itself. 0 (and any non-finite / non-positive
26661
+ * input) pauses; anything else is a continuous rate clamped into that band.
26662
+ * Single server-side authority — the feeder applies it to every inbound
26663
+ * `setRate`.
26664
+ *
26665
+ * That every ladder entry survives this unchanged is now structural rather
26666
+ * than a discipline. What is NOT structural, and is therefore the gate that
26667
+ * matters, is whether the pacer really empties a clip `rate` times faster:
26668
+ * `clip-stream-feeder.spec` drives every entry and measures it.
26646
26669
  */
26647
26670
  function clampPlaybackRate(rate) {
26648
26671
  if (!Number.isFinite(rate) || rate <= 0) return 0;
26649
- return Math.min(4, Math.max(.25, rate));
26672
+ return Math.min(MAX_PACED_RATE, Math.max(MIN_PACED_RATE, rate));
26650
26673
  }
26651
26674
  /**
26652
- * The rates a clip may be OFFERED at — re-exported from the capability, which
26653
- * is where the one list lives (D612). The broker owns the clamp; the cap owns
26654
- * the ladder; a second list here would be the second authority D62 forbids.
26655
- * `control-protocol.spec` pins that every entry survives {@link clampPlaybackRate}.
26675
+ * Does a clip carry SOUND at this rate?
26676
+ *
26677
+ * ONE predicate, read by the branch that DROPS the audio
26678
+ * (`ClipStreamFeeder.pushNextAudio`) and by the branch that TELLS the viewer
26679
+ * why it is silent — because a second copy of `rate !== 1` is exactly the
26680
+ * disagreeing second switch D62 forbids, and the telling would then be able
26681
+ * to describe a drop that did not happen (or miss one that did).
26682
+ *
26683
+ * *"L'audio non serve in velocità non 1x!!!"* — the operator, 2026-09-23.
26684
+ * Resampling would need a decoder this path has not got, and pitch-shifted
26685
+ * sound is worse than none.
26686
+ *
26687
+ * Rate 0 is PAUSE, not a rate: nothing plays at all there, so it is not this
26688
+ * predicate's business and callers hold their last non-zero rate instead.
26656
26689
  */
26657
- var BROKER_PACED_RATES = CLIP_BROKER_PACED_RATES;
26690
+ function audioPlaysAtRate(rate) {
26691
+ return rate === 1;
26692
+ }
26658
26693
  var ServerMessageSchema = discriminatedUnion("t", [
26659
26694
  object({
26660
26695
  t: literal("state"),
@@ -26695,6 +26730,14 @@ var ServerMessageSchema = discriminatedUnion("t", [
26695
26730
  clipId: string().min(1),
26696
26731
  durationMs: number().positive()
26697
26732
  }),
26733
+ object({
26734
+ t: literal("clipIncomplete"),
26735
+ clipId: string().min(1),
26736
+ code: _enum(["stream-short", "stream-empty"]),
26737
+ /** The provider's own sentence, already Latin-1 safe. Shown where the
26738
+ * stream error is shown. */
26739
+ reason: string().min(1)
26740
+ }),
26698
26741
  object({
26699
26742
  t: literal("clipAudio"),
26700
26743
  clipId: string().min(1),
@@ -26707,8 +26750,23 @@ var ServerMessageSchema = discriminatedUnion("t", [
26707
26750
  "no-audio-track",
26708
26751
  "unsupported-codec",
26709
26752
  "codec-mismatch",
26710
- "stream-failed"
26711
- ]).optional()
26753
+ "stream-failed",
26754
+ "rate"
26755
+ ]).optional(),
26756
+ /**
26757
+ * The rate the sound is suppressed AT. Present iff `reason` is `rate`,
26758
+ * and refused otherwise — see the check below.
26759
+ */
26760
+ rate: number().positive().optional()
26761
+ }).check((ctx) => {
26762
+ const value = ctx.value;
26763
+ if (value.reason === "rate" === (value.rate !== void 0)) return;
26764
+ ctx.issues.push({
26765
+ code: "custom",
26766
+ message: "`rate` is present exactly when `reason` is `rate`",
26767
+ input: value,
26768
+ path: ["rate"]
26769
+ });
26712
26770
  })
26713
26771
  ]);
26714
26772
  function parseControlMessage(raw) {
@@ -27680,7 +27738,7 @@ var RecordedFeeder = class {
27680
27738
  /** Audio clock (PCMU 8 kHz / Opus 48 kHz); re-anchored together with video. */
27681
27739
  audioClock;
27682
27740
  clockHasAdvanced = false;
27683
- /** Playback rate: 0 (paused) or a continuous factor in [0.25, 4]. */
27741
+ /** Playback rate: 0 (paused) or a continuous factor in [0.25, 8]. */
27684
27742
  rate = 1;
27685
27743
  disposed = false;
27686
27744
  seekToken = 0;
@@ -28261,7 +28319,7 @@ var RecordedFeeder = class {
28261
28319
  }
28262
28320
  /**
28263
28321
  * Set playback rate. 0 pauses in place (the pacing loop aborts on its next
28264
- * guard check); any other value is clamped to [0.25, 4].
28322
+ * guard check); any other value is clamped to [0.25, 8].
28265
28323
  *
28266
28324
  * Rate-change strategy (documented choice): every transition TO a non-zero
28267
28325
  * rate — resume from pause AND a playing→playing rate change — re-enters via
@@ -30126,17 +30184,25 @@ var TimelineSession = class {
30126
30184
  this.lastPositionMs = null;
30127
30185
  const feeder = this.enterRecorded();
30128
30186
  await feeder.seek(offsetMs);
30129
- await Promise.all([this.reportClipDuration(clipId, feeder), this.reportClipAudio(clipId, feeder)]);
30187
+ await Promise.all([
30188
+ this.reportClipDuration(clipId, feeder),
30189
+ this.reportClipAudio(clipId, feeder),
30190
+ this.reportClipIncomplete(clipId, feeder)
30191
+ ]);
30130
30192
  }
30131
30193
  /**
30132
30194
  * Tell the viewer whether this clip has sound.
30133
30195
  *
30134
- * Silence has three causes the operator cannot tell apart by listening —
30135
- * a camera with no microphone, a transport that carried no audio, and a
30136
- * session that negotiated none — so the viewer is told which. Every branch
30137
- * that sends nothing says so (D391): a feeder that cannot answer, an answer
30138
- * that rejected, and an answer that landed after the pointer had left the
30139
- * clip, which must not be pinned on whatever came next.
30196
+ * Silence has causes the operator cannot tell apart by listening — a camera
30197
+ * with no microphone, a transport that carried no audio, a session that
30198
+ * negotiated none, and (D621) a clip playing at anything but 1×, where the
30199
+ * broker drops every frame on purpose — so the viewer is told which. Every
30200
+ * branch that sends nothing says so (D391): a feeder that cannot answer, an
30201
+ * answer that rejected, and an answer that landed after the pointer had left
30202
+ * the clip, which must not be pinned on whatever came next.
30203
+ *
30204
+ * The first answer SETTLES; the rate does not, so this also subscribes for
30205
+ * as long as the clip is on the pointer.
30140
30206
  */
30141
30207
  async reportClipAudio(clipId, feeder) {
30142
30208
  if (feeder.audio === void 0) {
@@ -30157,11 +30223,25 @@ var TimelineSession = class {
30157
30223
  });
30158
30224
  return;
30159
30225
  }
30226
+ this.sendClipAudio(clipId, report);
30227
+ feeder.watchAudio?.((later) => this.sendClipAudio(clipId, later));
30228
+ }
30229
+ /**
30230
+ * Send one `clipAudio`, or say why nothing was sent.
30231
+ *
30232
+ * ONE place, for the settle and for every later change: they are the same
30233
+ * statement about the same clip, and a second builder is how the two would
30234
+ * come to disagree about what `reason` means. The clip-identity guard lives
30235
+ * here too — a report that lands after the pointer has left names a clip
30236
+ * nobody is playing, and the viewer would mute whatever came next.
30237
+ */
30238
+ sendClipAudio(clipId, report) {
30160
30239
  if (this.clip?.clipId !== clipId) {
30161
30240
  this.deps.onLog?.("clip playback: audio dropped — the pointer has left this clip", {
30162
30241
  deviceId: this.deps.deviceId,
30163
30242
  clipId,
30164
- carried: report.carried
30243
+ carried: report.carried,
30244
+ reason: report.reason
30165
30245
  });
30166
30246
  return;
30167
30247
  }
@@ -30171,6 +30251,7 @@ var TimelineSession = class {
30171
30251
  carried: report.carried,
30172
30252
  codec: report.codec,
30173
30253
  reason: report.reason,
30254
+ rate: report.rate,
30174
30255
  detail: report.detail
30175
30256
  });
30176
30257
  this.deps.session.sendControl(serializeServerMessage({
@@ -30178,7 +30259,63 @@ var TimelineSession = class {
30178
30259
  clipId,
30179
30260
  carried: report.carried,
30180
30261
  ...report.codec !== null ? { codec: report.codec } : {},
30181
- ...report.reason !== null ? { reason: report.reason } : {}
30262
+ ...report.reason !== null ? { reason: report.reason } : {},
30263
+ ...report.reason === "rate" && report.rate !== void 0 ? { rate: report.rate } : {}
30264
+ }));
30265
+ }
30266
+ /**
30267
+ * Tell the operator that the clip he is watching did not arrive whole
30268
+ * (D623), in the surface that already shows him a stream error.
30269
+ *
30270
+ * Awaited AFTER the stream ends, which is when the provider gets to speak,
30271
+ * and guarded by the clip identity for the same reason the audio report is:
30272
+ * a late answer names a clip nobody is playing and the viewer would put the
30273
+ * complaint on whatever came next.
30274
+ *
30275
+ * There is no retry here and there must not be one. *"Se cade la sessione
30276
+ * cade non fa niente"* — the operator, 2026-09-23. A second dial would
30277
+ * contend for the camera's single replay lane exactly while that lane is
30278
+ * already failing.
30279
+ */
30280
+ async reportClipIncomplete(clipId, feeder) {
30281
+ if (feeder.incomplete === void 0) {
30282
+ this.deps.onLog?.("clip playback: completeness not reported — this feeder cannot ask the provider", {
30283
+ deviceId: this.deps.deviceId,
30284
+ clipId
30285
+ });
30286
+ return;
30287
+ }
30288
+ let report;
30289
+ try {
30290
+ report = await feeder.incomplete();
30291
+ } catch (err) {
30292
+ this.deps.onLog?.("clip playback: completeness not reported — the answer failed", {
30293
+ deviceId: this.deps.deviceId,
30294
+ clipId,
30295
+ error: err instanceof Error ? err.message : String(err)
30296
+ });
30297
+ return;
30298
+ }
30299
+ if (report === null) return;
30300
+ if (this.clip?.clipId !== clipId) {
30301
+ this.deps.onLog?.("clip playback: completeness dropped — the pointer has left this clip", {
30302
+ deviceId: this.deps.deviceId,
30303
+ clipId,
30304
+ code: report.code
30305
+ });
30306
+ return;
30307
+ }
30308
+ this.deps.onLog?.("clip playback: the clip did not arrive whole", {
30309
+ deviceId: this.deps.deviceId,
30310
+ clipId,
30311
+ code: report.code,
30312
+ reason: report.reason
30313
+ });
30314
+ this.deps.session.sendControl(serializeServerMessage({
30315
+ t: "clipIncomplete",
30316
+ clipId,
30317
+ code: report.code,
30318
+ reason: report.reason
30182
30319
  }));
30183
30320
  }
30184
30321
  /**
@@ -31580,7 +31717,7 @@ function createFmp4StreamDemuxer(options = {}) {
31580
31717
  * faster than the camera sends.
31581
31718
  * - `stepFrame(-1)` and the scrub verbs are refused by name; `stepFrame(+1)`
31582
31719
  * pushes the next buffered unit.
31583
- * - `setRate` is the PLAYER's rate over frames already here ({0} ∪ [0.25, 4],
31720
+ * - `setRate` is the PLAYER's rate over frames already here ({0} ∪ [0.25, 8],
31584
31721
  * the recorded domain). The camera's own `playSpeed` (8, 16 — real, the app
31585
31722
  * reopens the replay at 8) is a different mechanism and is NOT wired: it
31586
31723
  * would go on the dial (`videoclips.dialClipStream` → the cmd-5 replay's
@@ -31646,6 +31783,11 @@ function createFmp4StreamDemuxer(options = {}) {
31646
31783
  * `data`, which the broker's `onLog` puts in `tags`.
31647
31784
  */
31648
31785
  /**
31786
+ * The reason an in-flight open is abandoned, carried on the abort so the
31787
+ * opener's refusal and the provider's log line say the same thing.
31788
+ */
31789
+ var CLIP_STREAM_ABANDONED = "clip-stream-abandoned";
31790
+ /**
31649
31791
  * The same ceiling as the file path (`CLIP_SOURCE_MAX_BYTES`): a clip the
31650
31792
  * file path would refuse `too-large` is one this path stops taking. Frames
31651
31793
  * behind the head are released as they are pushed, so the backlog is the
@@ -31668,6 +31810,8 @@ var ClipStreamFeeder = class {
31668
31810
  deps;
31669
31811
  phase = "idle";
31670
31812
  connection = null;
31813
+ /** Armed for the length of one `open()`; aborted by {@link teardown}. */
31814
+ opening = null;
31671
31815
  demuxer;
31672
31816
  clock = new MonotonicClock();
31673
31817
  /** The RTP rate the audio sender negotiated — a CONVERSION, not a clock. */
@@ -31680,10 +31824,28 @@ var ClipStreamFeeder = class {
31680
31824
  audioPushed = 0;
31681
31825
  audioDroppedLate = 0;
31682
31826
  audioDroppedRate = 0;
31827
+ /**
31828
+ * The last report handed to a caller — the settle promise's value, or the
31829
+ * newest one {@link watchAudio}'s listener was given. What a change is
31830
+ * measured against, so the same answer is never announced twice.
31831
+ */
31832
+ audioEmitted = null;
31833
+ /**
31834
+ * The last NON-ZERO rate. Pause is the absence of a rate, not a rate: a
31835
+ * `clipAudio` blaming the speed while the clip is stopped would be false,
31836
+ * and un-pausing would have to take it back.
31837
+ */
31838
+ audioRate = 1;
31839
+ audioListener = null;
31683
31840
  audioDroppedSeek = 0;
31684
31841
  firstAudioLogged = false;
31685
31842
  audioReport = null;
31686
31843
  audioWaiters = [];
31844
+ /** The provider's last word, and whether it has been given. Settled is not
31845
+ * the same as non-null: a whole stream settles `null` (D623). */
31846
+ incompleteSettled = false;
31847
+ incompleteReport = null;
31848
+ incompleteWaiters = [];
31687
31849
  clockHasAdvanced = false;
31688
31850
  rate = 1;
31689
31851
  queue = [];
@@ -31715,12 +31877,16 @@ var ClipStreamFeeder = class {
31715
31877
  firstFrameLogged = false;
31716
31878
  maxBufferedBytes;
31717
31879
  stallTimeoutMs;
31880
+ prerollMs;
31881
+ /** True while the pacer is waiting for the cushion to refill after a starve. */
31882
+ rebuffering = false;
31718
31883
  now;
31719
31884
  tags;
31720
31885
  constructor(deps) {
31721
31886
  this.deps = deps;
31722
31887
  this.maxBufferedBytes = deps.maxBufferedBytes ?? CLIP_STREAM_BUFFER_MAX_BYTES;
31723
31888
  this.stallTimeoutMs = deps.stallTimeoutMs ?? 15e3;
31889
+ this.prerollMs = deps.prerollMs ?? 600;
31724
31890
  this.now = deps.now ?? (() => Date.now());
31725
31891
  this.tags = {
31726
31892
  deviceId: deps.handle.deviceId,
@@ -31818,6 +31984,8 @@ var ClipStreamFeeder = class {
31818
31984
  this.clock.setRate(next);
31819
31985
  const wasPaused = this.rate === 0;
31820
31986
  this.rate = next;
31987
+ this.audioRate = next;
31988
+ this.refreshAudioReport();
31821
31989
  if (this.phase !== "streaming" || this.pendingSeekMs !== null) return;
31822
31990
  if (wasPaused) this.emitState("playing", "set-rate-resume", this.seekToken);
31823
31991
  this.startPacing();
@@ -31852,6 +32020,19 @@ var ClipStreamFeeder = class {
31852
32020
  return new Promise((resolve) => this.durationWaiters.push(resolve));
31853
32021
  }
31854
32022
  /**
32023
+ * Did the provider deliver this clip whole? (D623)
32024
+ *
32025
+ * Resolves `null` for a whole stream AND for one that never got to say —
32026
+ * cut, disposed, or opened against a provider that adds no trailer. Both are
32027
+ * "nothing to tell the operator", and neither may be reported as a defect:
32028
+ * a cut already announces itself as `state: error`, and inventing a second
32029
+ * complaint for it would file one fault twice.
32030
+ */
32031
+ incomplete() {
32032
+ if (this.incompleteSettled) return Promise.resolve(this.incompleteReport);
32033
+ return new Promise((resolve) => this.incompleteWaiters.push(resolve));
32034
+ }
32035
+ /**
31855
32036
  * What this clip turned out to carry as sound — the answer the viewer needs
31856
32037
  * to say "this clip has no audio" rather than leave the operator wondering.
31857
32038
  *
@@ -31861,15 +32042,31 @@ var ClipStreamFeeder = class {
31861
32042
  */
31862
32043
  audio() {
31863
32044
  const settled = this.audioReport;
31864
- if (settled !== null) return Promise.resolve(settled);
32045
+ if (settled !== null) return Promise.resolve(this.audioNow(settled));
31865
32046
  return new Promise((resolve) => this.audioWaiters.push(resolve));
31866
32047
  }
32048
+ /**
32049
+ * Follow the audio answer for as long as this clip is on the pointer.
32050
+ *
32051
+ * {@link audio} settles ONCE, because what the stream carries is decided
32052
+ * once — at the `moov`. The RATE is not: it moves under the operator's
32053
+ * finger, and a one-shot report cannot say that the clip which had sound a
32054
+ * second ago is silent now because he pressed 2×. The listener is told only
32055
+ * when the answer really changes, never the settle it already awaited.
32056
+ *
32057
+ * One listener, replaced rather than added to: there is one session behind
32058
+ * one clip, and a list would outlive the pointer.
32059
+ */
32060
+ watchAudio(listener) {
32061
+ this.audioListener = listener;
32062
+ }
31867
32063
  dispose() {
31868
32064
  if (this.phase === "disposed") return;
31869
32065
  const before = this.phase;
31870
32066
  this.phase = "disposed";
31871
32067
  this.teardown();
31872
32068
  this.settleDuration(null);
32069
+ this.settleIncomplete(null);
31873
32070
  this.settleAudio({
31874
32071
  carried: false,
31875
32072
  codec: null,
@@ -31940,13 +32137,15 @@ var ClipStreamFeeder = class {
31940
32137
  audioReason: answer.audioReason ?? null
31941
32138
  });
31942
32139
  }
32140
+ const abandon = new AbortController();
32141
+ this.opening = abandon;
31943
32142
  let opened;
31944
32143
  try {
31945
32144
  opened = await this.deps.open(answer.url, {
31946
32145
  onData: (chunk) => this.onData(chunk),
31947
- onEnd: () => this.onEnd(),
32146
+ onEnd: (report) => this.onEnd(report),
31948
32147
  onError: (err) => this.cut("stream-failed", err.message)
31949
- });
32148
+ }, abandon.signal);
31950
32149
  } catch (err) {
31951
32150
  opened = {
31952
32151
  kind: "refused",
@@ -31955,8 +32154,15 @@ var ClipStreamFeeder = class {
31955
32154
  detail: describe(err)
31956
32155
  };
31957
32156
  }
32157
+ if (this.opening === abandon) this.opening = null;
31958
32158
  if (this.phase !== "opening") {
31959
32159
  if (opened.kind === "open") opened.connection.destroy();
32160
+ this.log("clip-stream:open-abandoned", {
32161
+ phase: this.phase,
32162
+ opened: opened.kind,
32163
+ sinceDialMs: this.now() - (this.dialedAt ?? this.now()),
32164
+ ...opened.kind === "refused" ? { code: opened.code } : {}
32165
+ });
31960
32166
  return;
31961
32167
  }
31962
32168
  if (opened.kind === "refused") {
@@ -31976,6 +32182,7 @@ var ClipStreamFeeder = class {
31976
32182
  this.phase = "refused";
31977
32183
  this.teardown();
31978
32184
  this.settleDuration(null);
32185
+ this.settleIncomplete(null);
31979
32186
  this.settleAudio({
31980
32187
  carried: false,
31981
32188
  codec: null,
@@ -32097,10 +32304,24 @@ var ClipStreamFeeder = class {
32097
32304
  return;
32098
32305
  }
32099
32306
  while (this.rate === 0 && this.pendingSteps > 0 && this.pushNext()) this.pendingSteps -= 1;
32100
- if (this.rate > 0 && this.paceTimer === null) this.tick();
32307
+ if (this.rate === 0) return;
32308
+ if (this.rebuffering) {
32309
+ if (this.cushionShortfallMs() > 0) return;
32310
+ this.rebuffering = false;
32311
+ this.log("clip-stream:refilled", {
32312
+ headMs: this.headMs,
32313
+ arrivedMs: this.arrivedMs,
32314
+ queued: this.queue.length,
32315
+ starved: this.starved
32316
+ });
32317
+ this.startPacing();
32318
+ return;
32319
+ }
32320
+ if (this.paceTimer === null) this.tick();
32101
32321
  }
32102
- onEnd() {
32322
+ onEnd(report) {
32103
32323
  if (!this.takesBytes()) return;
32324
+ this.settleIncomplete(report);
32104
32325
  this.streamEnded = true;
32105
32326
  this.clearStall();
32106
32327
  this.streamEnd = this.demuxer.end();
@@ -32118,13 +32339,20 @@ var ClipStreamFeeder = class {
32118
32339
  this.resolvePendingSeek(this.seekToken);
32119
32340
  return;
32120
32341
  }
32121
- if (this.rate > 0 && this.paceTimer === null) this.tick();
32342
+ if (this.rate === 0) return;
32343
+ if (this.rebuffering || this.anchorWall > this.now()) {
32344
+ this.rebuffering = false;
32345
+ this.startPacing();
32346
+ return;
32347
+ }
32348
+ if (this.paceTimer === null) this.tick();
32122
32349
  }
32123
32350
  cut(reason, detail) {
32124
32351
  if (this.phase !== "streaming" && this.phase !== "opening") return;
32125
32352
  this.phase = "cut";
32126
32353
  this.teardown();
32127
32354
  this.settleDuration(null);
32355
+ this.settleIncomplete(null);
32128
32356
  this.settleAudio({
32129
32357
  carried: false,
32130
32358
  codec: null,
@@ -32258,9 +32486,24 @@ var ClipStreamFeeder = class {
32258
32486
  this.audioQueue = this.audioQueue.slice(drop);
32259
32487
  this.audioDroppedSeek += drop;
32260
32488
  }
32489
+ /**
32490
+ * How much of the cushion is MISSING, in milliseconds of media.
32491
+ *
32492
+ * Zero once the stream has ended — there is nothing more coming, so waiting
32493
+ * for it would hang the clip — and zero for any source that already holds
32494
+ * more than the cushion, which is every fast one.
32495
+ */
32496
+ cushionShortfallMs() {
32497
+ if (this.streamEnded) return 0;
32498
+ const head = this.headMs;
32499
+ const arrived = this.arrivedMs;
32500
+ if (head === null || arrived === null) return this.prerollMs;
32501
+ return Math.max(0, this.prerollMs - (arrived - head));
32502
+ }
32261
32503
  startPacing() {
32262
32504
  this.stopPacing();
32263
- this.anchorWall = this.now();
32505
+ const shortfall = this.cushionShortfallMs();
32506
+ this.anchorWall = this.now() + shortfall;
32264
32507
  this.anchorPts = this.headMs ?? this.queue[0]?.ptsMs ?? 0;
32265
32508
  this.startPositionReports();
32266
32509
  this.tick();
@@ -32285,7 +32528,10 @@ var ClipStreamFeeder = class {
32285
32528
  const audio = this.audioQueue[0];
32286
32529
  if (video === void 0 && audio === void 0) {
32287
32530
  if (this.streamEnded) this.finish();
32288
- else this.noteStarved();
32531
+ else {
32532
+ this.rebuffering = true;
32533
+ this.noteStarved();
32534
+ }
32289
32535
  return;
32290
32536
  }
32291
32537
  const takeVideo = video !== void 0 && (audio === void 0 || video.ptsMs <= audio.ptsMs);
@@ -32321,7 +32567,7 @@ var ClipStreamFeeder = class {
32321
32567
  if (frame === void 0) return false;
32322
32568
  this.audioQueue = this.audioQueue.slice(1);
32323
32569
  this.audioQueuedBytes -= frame.data.byteLength;
32324
- if (this.rate !== 1) {
32570
+ if (!audioPlaysAtRate(this.rate)) {
32325
32571
  this.audioDroppedRate += 1;
32326
32572
  return true;
32327
32573
  }
@@ -32419,11 +32665,53 @@ var ClipStreamFeeder = class {
32419
32665
  audioQueued: this.audioQueue.length
32420
32666
  };
32421
32667
  }
32668
+ /** One-shot, like the duration beside it: the provider speaks once, at the
32669
+ * end of the body, and a cut settles `null` so nobody waits for ever. */
32670
+ settleIncomplete(report) {
32671
+ if (this.incompleteSettled) return;
32672
+ this.incompleteSettled = true;
32673
+ this.incompleteReport = report;
32674
+ if (report !== null) this.log("clip-stream:incomplete", {
32675
+ code: report.code,
32676
+ reason: report.reason
32677
+ });
32678
+ for (const resolve of this.incompleteWaiters.splice(0)) resolve(report);
32679
+ }
32422
32680
  settleAudio(report) {
32423
32681
  if (this.audioReport !== null) return;
32424
32682
  this.audioReport = report;
32683
+ const now = this.audioNow(report);
32684
+ this.audioEmitted = now;
32425
32685
  const waiters = this.audioWaiters.splice(0);
32426
- for (const resolve of waiters) resolve(report);
32686
+ for (const resolve of waiters) resolve(now);
32687
+ }
32688
+ /**
32689
+ * The audio answer AS IT STANDS — the stream's own settled fact, with the
32690
+ * rate laid over it.
32691
+ *
32692
+ * The overlay only ever applies to a clip that really is carrying sound. A
32693
+ * clip whose camera has no microphone is silent at 1× too, and replacing
32694
+ * `no-audio-track` with `rate` would swap the true cause for a transient
32695
+ * one and send the operator to the speed chip.
32696
+ */
32697
+ audioNow(settled) {
32698
+ if (!settled.carried || audioPlaysAtRate(this.audioRate)) return settled;
32699
+ return {
32700
+ carried: false,
32701
+ codec: null,
32702
+ reason: "rate",
32703
+ rate: this.audioRate
32704
+ };
32705
+ }
32706
+ /** Tell the listener when — and only when — the answer has actually moved. */
32707
+ refreshAudioReport() {
32708
+ const settled = this.audioReport;
32709
+ if (settled === null) return;
32710
+ const next = this.audioNow(settled);
32711
+ const last = this.audioEmitted;
32712
+ if (last !== null && last.reason === next.reason && last.rate === next.rate) return;
32713
+ this.audioEmitted = next;
32714
+ this.audioListener?.(next);
32427
32715
  }
32428
32716
  settleDuration(value) {
32429
32717
  if (this.duration !== null) return;
@@ -32438,6 +32726,9 @@ var ClipStreamFeeder = class {
32438
32726
  this.stopPacing();
32439
32727
  this.stopPositionReports();
32440
32728
  this.clearStall();
32729
+ const opening = this.opening;
32730
+ this.opening = null;
32731
+ opening?.abort(/* @__PURE__ */ new Error(CLIP_STREAM_ABANDONED));
32441
32732
  const connection = this.connection;
32442
32733
  this.connection = null;
32443
32734
  connection?.destroy();
@@ -32479,6 +32770,62 @@ var ClipStreamFeeder = class {
32479
32770
  function describe(err) {
32480
32771
  return err instanceof Error ? err.message : String(err);
32481
32772
  }
32773
+ //#endregion
32774
+ //#region src/stream-broker/recorded/clip-stream-http.ts
32775
+ /**
32776
+ * clip-stream-http — the `node:http` half of {@link ClipStreamOpener}.
32777
+ *
32778
+ * Dials the URL `videoclips.dialClipStream` handed back — plain HTTP on the
32779
+ * provider's own token-gated listener, never the hub's proxy (an addon holds
32780
+ * no user credential, and the hub proxy takes only those) — and turns the
32781
+ * response into what the feeder drives: `pause`/`resume`/`destroy` on the
32782
+ * socket and the three events.
32783
+ *
32784
+ * A status other than 200 is a REFUSAL, named by the provider's own headers
32785
+ * (`x-camstack-reason-code`, `x-camstack-reason`), never a stream of nothing.
32786
+ * A 200 that does not declare `x-camstack-clip-transport: forward-only` is
32787
+ * refused too: it means the URL is not the stream route, and playing a
32788
+ * progressive file through a forward-only feeder would look like it worked
32789
+ * until the first seek.
32790
+ */
32791
+ /**
32792
+ * The codes a provider may add AFTER its body, and the only two this hop
32793
+ * carries onward.
32794
+ *
32795
+ * A stream's status line is spent on its first fragment, so a serve the camera
32796
+ * cut short has nothing left to say it with but a trailer — and a trailer this
32797
+ * reader did not know about would be read as a whole clip, which is the defect
32798
+ * D623 is about, one hop further out. Anything else a provider writes there is
32799
+ * ITS diagnostic and stays in ITS log: a viewer is shown what it can act on.
32800
+ */
32801
+ var CARRIED_TRAILER_CODES = ["stream-short", "stream-empty"];
32802
+ function trailerText(trailers, name) {
32803
+ const value = trailers[name];
32804
+ return typeof value === "string" && value.length > 0 ? value : void 0;
32805
+ }
32806
+ /**
32807
+ * What the provider said about its own stream once the last byte was out.
32808
+ *
32809
+ * `null` for a stream that added nothing, which is a whole one — the absence
32810
+ * is the statement, and it is never read as "unknown". A code with no sentence
32811
+ * is still reported, with the code standing in for one: a consumer that
32812
+ * branches on the code must not lose the branch because the prose was dropped
32813
+ * by a proxy.
32814
+ */
32815
+ function readClipStreamTrailers(trailers) {
32816
+ const code = trailerText(trailers, CLIP_STILL_REASON_CODE_HEADER);
32817
+ if (code === void 0 || !CARRIED_TRAILER_CODES.includes(code)) return null;
32818
+ return {
32819
+ code: code === "stream-empty" ? "stream-empty" : "stream-short",
32820
+ reason: trailerText(trailers, "x-camstack-reason") ?? code
32821
+ };
32822
+ }
32823
+ /** The abort's reason, as a sentence the provider's log can be matched against. */
32824
+ function describeReason(reason) {
32825
+ if (reason instanceof Error) return reason.message;
32826
+ if (typeof reason === "string") return reason;
32827
+ return "the clip stream was abandoned before it opened";
32828
+ }
32482
32829
  function headerText(res, name) {
32483
32830
  const value = res.headers[name];
32484
32831
  if (Array.isArray(value)) return value[0];
@@ -32486,14 +32833,26 @@ function headerText(res, name) {
32486
32833
  }
32487
32834
  function createHttpClipStreamOpener(options = {}) {
32488
32835
  const headersTimeoutMs = options.headersTimeoutMs ?? 3e4;
32489
- return (url, handlers) => new Promise((resolve) => {
32836
+ return (url, handlers, signal) => new Promise((resolve) => {
32490
32837
  let settled = false;
32838
+ let onAbort = null;
32491
32839
  const settle = (value) => {
32492
32840
  if (settled) return;
32493
32841
  settled = true;
32494
32842
  clearTimeout(timer);
32843
+ if (onAbort !== null) signal.removeEventListener("abort", onAbort);
32495
32844
  resolve(value);
32496
32845
  };
32846
+ const abandoned = () => ({
32847
+ kind: "refused",
32848
+ status: 0,
32849
+ code: "abandoned",
32850
+ detail: describeReason(signal.reason)
32851
+ });
32852
+ if (signal.aborted) {
32853
+ resolve(abandoned());
32854
+ return;
32855
+ }
32497
32856
  let target;
32498
32857
  try {
32499
32858
  target = new URL(url);
@@ -32522,7 +32881,16 @@ function createHttpClipStreamOpener(options = {}) {
32522
32881
  const timer = setTimeout(() => {
32523
32882
  req.destroy(/* @__PURE__ */ new Error(`no status line in ${headersTimeoutMs} ms`));
32524
32883
  }, headersTimeoutMs);
32884
+ onAbort = () => {
32885
+ req.destroy(new Error(describeReason(signal.reason)));
32886
+ settle(abandoned());
32887
+ };
32888
+ signal.addEventListener("abort", onAbort, { once: true });
32525
32889
  req.on("error", (err) => {
32890
+ if (signal.aborted) {
32891
+ settle(abandoned());
32892
+ return;
32893
+ }
32526
32894
  settle({
32527
32895
  kind: "refused",
32528
32896
  status: 0,
@@ -32553,7 +32921,7 @@ function createHttpClipStreamOpener(options = {}) {
32553
32921
  return;
32554
32922
  }
32555
32923
  res.on("data", (chunk) => handlers.onData(chunk));
32556
- res.on("end", () => handlers.onEnd());
32924
+ res.on("end", () => handlers.onEnd(readClipStreamTrailers(res.trailers)));
32557
32925
  res.on("error", (err) => handlers.onError(err));
32558
32926
  settle({
32559
32927
  kind: "open",
@@ -35888,8 +36256,8 @@ var StreamBrokerAddon = class extends BaseAddon {
35888
36256
  clearTimeout(timer);
35889
36257
  }
35890
36258
  },
35891
- locateRecordedSegment: (args) => this.ctx.api.recording.locateSegment.query(args),
35892
- readRecordedSegmentBytes: async (args) => (await this.ctx.api.recording.readSegmentBytes.query(args)).data,
36259
+ locateRecordedSegment: (args) => this.ctx.api.recordingArchive.locateSegment.query(args),
36260
+ readRecordedSegmentBytes: async (args) => (await this.ctx.api.recordingArchive.readSegmentBytes.query(args)).data,
35893
36261
  readClipBytes: async (args) => {
35894
36262
  const res = await this.ctx.api.videoclips.readClipBytes.query({
35895
36263
  deviceId: args.deviceId,
@@ -35922,7 +36290,7 @@ var StreamBrokerAddon = class extends BaseAddon {
35922
36290
  },
35923
36291
  readRecordedGopBytes: async (args) => {
35924
36292
  try {
35925
- return await this.ctx.api.recording.readGopBytes.query({
36293
+ return await this.ctx.api.recordingArchive.readGopBytes.query({
35926
36294
  deviceId: args.deviceId,
35927
36295
  profile: args.profile,
35928
36296
  startMs: args.startMs,
@@ -35937,7 +36305,7 @@ var StreamBrokerAddon = class extends BaseAddon {
35937
36305
  });
35938
36306
  }
35939
36307
  return {
35940
- data: (await this.ctx.api.recording.readSegmentBytes.query({
36308
+ data: (await this.ctx.api.recordingArchive.readSegmentBytes.query({
35941
36309
  deviceId: args.deviceId,
35942
36310
  profile: args.profile,
35943
36311
  startMs: args.startMs