@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
@@ -3,12 +3,12 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-CegiJ2Pw.js");
7
- const require_node = require("../node-C8jhiPW2.js");
6
+ const require_dist = require("../dist-BIlnUdBn.js");
7
+ const require_node = require("../node-CVlJhqng.js");
8
8
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
9
9
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
10
10
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
11
- const require_segment_demux_js = require("../segment-demux-js-Dv1EPPdH.js");
11
+ const require_segment_demux_js = require("../segment-demux-js-D0mqN4RC.js");
12
12
  let node_os = require("node:os");
13
13
  node_os = require_chunk.__toESM(node_os);
14
14
  let node_crypto = require("node:crypto");
@@ -26564,6 +26564,22 @@ function gapReachable(gap, epochMs) {
26564
26564
  }
26565
26565
  //#endregion
26566
26566
  //#region src/stream-broker/recorded/control-protocol.ts
26567
+ /**
26568
+ * The rates a clip may be OFFERED at — re-exported from the capability, which
26569
+ * is where the one list lives (D612). The broker owns the clamp; the cap owns
26570
+ * the ladder; a second list here would be the second authority D62 forbids.
26571
+ *
26572
+ * Declared at the top of the file because the clamp and the `setRate` bound
26573
+ * are DERIVED from its ends (D621). They were three literals — this array, a
26574
+ * `Math.min(4, …)` and a `.max(4)` — and that is precisely how 8 and 16 came
26575
+ * to be offered while the clamp served 4 with no line anywhere saying so. One
26576
+ * decision now has one place to be written down.
26577
+ */
26578
+ var BROKER_PACED_RATES = require_dist.CLIP_BROKER_PACED_RATES;
26579
+ /** The slowest offered rate. Anything slower is clamped up to it. */
26580
+ var MIN_PACED_RATE = Math.min(...BROKER_PACED_RATES);
26581
+ /** The fastest offered rate. Anything faster is refused at the wire. */
26582
+ var MAX_PACED_RATE = Math.max(...BROKER_PACED_RATES);
26567
26583
  var ControlMessageSchema = require_dist.discriminatedUnion("t", [
26568
26584
  require_dist.object({
26569
26585
  t: require_dist.literal("playRecorded"),
@@ -26630,7 +26646,7 @@ var ControlMessageSchema = require_dist.discriminatedUnion("t", [
26630
26646
  }),
26631
26647
  require_dist.object({
26632
26648
  t: require_dist.literal("setRate"),
26633
- rate: require_dist.number().min(0).max(4)
26649
+ rate: require_dist.number().min(0).max(MAX_PACED_RATE)
26634
26650
  }),
26635
26651
  require_dist.object({
26636
26652
  t: require_dist.literal("stepFrame"),
@@ -26643,22 +26659,41 @@ var ControlMessageSchema = require_dist.discriminatedUnion("t", [
26643
26659
  })
26644
26660
  ]);
26645
26661
  /**
26646
- * Clamp a requested playback rate onto the supported domain: {0} ∪ [0.25, 4].
26647
- * 0 (and any non-finite / non-positive input) pauses; anything else is a
26648
- * continuous rate clamped into [0.25, 4]. Single server-side authority — the
26649
- * feeder applies it to every inbound `setRate`.
26662
+ * Clamp a requested playback rate onto the supported domain:
26663
+ * {0} ∪ [{@link MIN_PACED_RATE}, {@link MAX_PACED_RATE}] — the ends of
26664
+ * {@link BROKER_PACED_RATES} itself. 0 (and any non-finite / non-positive
26665
+ * input) pauses; anything else is a continuous rate clamped into that band.
26666
+ * Single server-side authority — the feeder applies it to every inbound
26667
+ * `setRate`.
26668
+ *
26669
+ * That every ladder entry survives this unchanged is now structural rather
26670
+ * than a discipline. What is NOT structural, and is therefore the gate that
26671
+ * matters, is whether the pacer really empties a clip `rate` times faster:
26672
+ * `clip-stream-feeder.spec` drives every entry and measures it.
26650
26673
  */
26651
26674
  function clampPlaybackRate(rate) {
26652
26675
  if (!Number.isFinite(rate) || rate <= 0) return 0;
26653
- return Math.min(4, Math.max(.25, rate));
26676
+ return Math.min(MAX_PACED_RATE, Math.max(MIN_PACED_RATE, rate));
26654
26677
  }
26655
26678
  /**
26656
- * The rates a clip may be OFFERED at — re-exported from the capability, which
26657
- * is where the one list lives (D612). The broker owns the clamp; the cap owns
26658
- * the ladder; a second list here would be the second authority D62 forbids.
26659
- * `control-protocol.spec` pins that every entry survives {@link clampPlaybackRate}.
26679
+ * Does a clip carry SOUND at this rate?
26680
+ *
26681
+ * ONE predicate, read by the branch that DROPS the audio
26682
+ * (`ClipStreamFeeder.pushNextAudio`) and by the branch that TELLS the viewer
26683
+ * why it is silent — because a second copy of `rate !== 1` is exactly the
26684
+ * disagreeing second switch D62 forbids, and the telling would then be able
26685
+ * to describe a drop that did not happen (or miss one that did).
26686
+ *
26687
+ * *"L'audio non serve in velocità non 1x!!!"* — the operator, 2026-09-23.
26688
+ * Resampling would need a decoder this path has not got, and pitch-shifted
26689
+ * sound is worse than none.
26690
+ *
26691
+ * Rate 0 is PAUSE, not a rate: nothing plays at all there, so it is not this
26692
+ * predicate's business and callers hold their last non-zero rate instead.
26660
26693
  */
26661
- var BROKER_PACED_RATES = require_dist.CLIP_BROKER_PACED_RATES;
26694
+ function audioPlaysAtRate(rate) {
26695
+ return rate === 1;
26696
+ }
26662
26697
  var ServerMessageSchema = require_dist.discriminatedUnion("t", [
26663
26698
  require_dist.object({
26664
26699
  t: require_dist.literal("state"),
@@ -26699,6 +26734,14 @@ var ServerMessageSchema = require_dist.discriminatedUnion("t", [
26699
26734
  clipId: require_dist.string().min(1),
26700
26735
  durationMs: require_dist.number().positive()
26701
26736
  }),
26737
+ require_dist.object({
26738
+ t: require_dist.literal("clipIncomplete"),
26739
+ clipId: require_dist.string().min(1),
26740
+ code: require_dist._enum(["stream-short", "stream-empty"]),
26741
+ /** The provider's own sentence, already Latin-1 safe. Shown where the
26742
+ * stream error is shown. */
26743
+ reason: require_dist.string().min(1)
26744
+ }),
26702
26745
  require_dist.object({
26703
26746
  t: require_dist.literal("clipAudio"),
26704
26747
  clipId: require_dist.string().min(1),
@@ -26711,8 +26754,23 @@ var ServerMessageSchema = require_dist.discriminatedUnion("t", [
26711
26754
  "no-audio-track",
26712
26755
  "unsupported-codec",
26713
26756
  "codec-mismatch",
26714
- "stream-failed"
26715
- ]).optional()
26757
+ "stream-failed",
26758
+ "rate"
26759
+ ]).optional(),
26760
+ /**
26761
+ * The rate the sound is suppressed AT. Present iff `reason` is `rate`,
26762
+ * and refused otherwise — see the check below.
26763
+ */
26764
+ rate: require_dist.number().positive().optional()
26765
+ }).check((ctx) => {
26766
+ const value = ctx.value;
26767
+ if (value.reason === "rate" === (value.rate !== void 0)) return;
26768
+ ctx.issues.push({
26769
+ code: "custom",
26770
+ message: "`rate` is present exactly when `reason` is `rate`",
26771
+ input: value,
26772
+ path: ["rate"]
26773
+ });
26716
26774
  })
26717
26775
  ]);
26718
26776
  function parseControlMessage(raw) {
@@ -27684,7 +27742,7 @@ var RecordedFeeder = class {
27684
27742
  /** Audio clock (PCMU 8 kHz / Opus 48 kHz); re-anchored together with video. */
27685
27743
  audioClock;
27686
27744
  clockHasAdvanced = false;
27687
- /** Playback rate: 0 (paused) or a continuous factor in [0.25, 4]. */
27745
+ /** Playback rate: 0 (paused) or a continuous factor in [0.25, 8]. */
27688
27746
  rate = 1;
27689
27747
  disposed = false;
27690
27748
  seekToken = 0;
@@ -28265,7 +28323,7 @@ var RecordedFeeder = class {
28265
28323
  }
28266
28324
  /**
28267
28325
  * Set playback rate. 0 pauses in place (the pacing loop aborts on its next
28268
- * guard check); any other value is clamped to [0.25, 4].
28326
+ * guard check); any other value is clamped to [0.25, 8].
28269
28327
  *
28270
28328
  * Rate-change strategy (documented choice): every transition TO a non-zero
28271
28329
  * rate — resume from pause AND a playing→playing rate change — re-enters via
@@ -30130,17 +30188,25 @@ var TimelineSession = class {
30130
30188
  this.lastPositionMs = null;
30131
30189
  const feeder = this.enterRecorded();
30132
30190
  await feeder.seek(offsetMs);
30133
- await Promise.all([this.reportClipDuration(clipId, feeder), this.reportClipAudio(clipId, feeder)]);
30191
+ await Promise.all([
30192
+ this.reportClipDuration(clipId, feeder),
30193
+ this.reportClipAudio(clipId, feeder),
30194
+ this.reportClipIncomplete(clipId, feeder)
30195
+ ]);
30134
30196
  }
30135
30197
  /**
30136
30198
  * Tell the viewer whether this clip has sound.
30137
30199
  *
30138
- * Silence has three causes the operator cannot tell apart by listening —
30139
- * a camera with no microphone, a transport that carried no audio, and a
30140
- * session that negotiated none — so the viewer is told which. Every branch
30141
- * that sends nothing says so (D391): a feeder that cannot answer, an answer
30142
- * that rejected, and an answer that landed after the pointer had left the
30143
- * clip, which must not be pinned on whatever came next.
30200
+ * Silence has causes the operator cannot tell apart by listening — a camera
30201
+ * with no microphone, a transport that carried no audio, a session that
30202
+ * negotiated none, and (D621) a clip playing at anything but 1×, where the
30203
+ * broker drops every frame on purpose — so the viewer is told which. Every
30204
+ * branch that sends nothing says so (D391): a feeder that cannot answer, an
30205
+ * answer that rejected, and an answer that landed after the pointer had left
30206
+ * the clip, which must not be pinned on whatever came next.
30207
+ *
30208
+ * The first answer SETTLES; the rate does not, so this also subscribes for
30209
+ * as long as the clip is on the pointer.
30144
30210
  */
30145
30211
  async reportClipAudio(clipId, feeder) {
30146
30212
  if (feeder.audio === void 0) {
@@ -30161,11 +30227,25 @@ var TimelineSession = class {
30161
30227
  });
30162
30228
  return;
30163
30229
  }
30230
+ this.sendClipAudio(clipId, report);
30231
+ feeder.watchAudio?.((later) => this.sendClipAudio(clipId, later));
30232
+ }
30233
+ /**
30234
+ * Send one `clipAudio`, or say why nothing was sent.
30235
+ *
30236
+ * ONE place, for the settle and for every later change: they are the same
30237
+ * statement about the same clip, and a second builder is how the two would
30238
+ * come to disagree about what `reason` means. The clip-identity guard lives
30239
+ * here too — a report that lands after the pointer has left names a clip
30240
+ * nobody is playing, and the viewer would mute whatever came next.
30241
+ */
30242
+ sendClipAudio(clipId, report) {
30164
30243
  if (this.clip?.clipId !== clipId) {
30165
30244
  this.deps.onLog?.("clip playback: audio dropped — the pointer has left this clip", {
30166
30245
  deviceId: this.deps.deviceId,
30167
30246
  clipId,
30168
- carried: report.carried
30247
+ carried: report.carried,
30248
+ reason: report.reason
30169
30249
  });
30170
30250
  return;
30171
30251
  }
@@ -30175,6 +30255,7 @@ var TimelineSession = class {
30175
30255
  carried: report.carried,
30176
30256
  codec: report.codec,
30177
30257
  reason: report.reason,
30258
+ rate: report.rate,
30178
30259
  detail: report.detail
30179
30260
  });
30180
30261
  this.deps.session.sendControl(serializeServerMessage({
@@ -30182,7 +30263,63 @@ var TimelineSession = class {
30182
30263
  clipId,
30183
30264
  carried: report.carried,
30184
30265
  ...report.codec !== null ? { codec: report.codec } : {},
30185
- ...report.reason !== null ? { reason: report.reason } : {}
30266
+ ...report.reason !== null ? { reason: report.reason } : {},
30267
+ ...report.reason === "rate" && report.rate !== void 0 ? { rate: report.rate } : {}
30268
+ }));
30269
+ }
30270
+ /**
30271
+ * Tell the operator that the clip he is watching did not arrive whole
30272
+ * (D623), in the surface that already shows him a stream error.
30273
+ *
30274
+ * Awaited AFTER the stream ends, which is when the provider gets to speak,
30275
+ * and guarded by the clip identity for the same reason the audio report is:
30276
+ * a late answer names a clip nobody is playing and the viewer would put the
30277
+ * complaint on whatever came next.
30278
+ *
30279
+ * There is no retry here and there must not be one. *"Se cade la sessione
30280
+ * cade non fa niente"* — the operator, 2026-09-23. A second dial would
30281
+ * contend for the camera's single replay lane exactly while that lane is
30282
+ * already failing.
30283
+ */
30284
+ async reportClipIncomplete(clipId, feeder) {
30285
+ if (feeder.incomplete === void 0) {
30286
+ this.deps.onLog?.("clip playback: completeness not reported — this feeder cannot ask the provider", {
30287
+ deviceId: this.deps.deviceId,
30288
+ clipId
30289
+ });
30290
+ return;
30291
+ }
30292
+ let report;
30293
+ try {
30294
+ report = await feeder.incomplete();
30295
+ } catch (err) {
30296
+ this.deps.onLog?.("clip playback: completeness not reported — the answer failed", {
30297
+ deviceId: this.deps.deviceId,
30298
+ clipId,
30299
+ error: err instanceof Error ? err.message : String(err)
30300
+ });
30301
+ return;
30302
+ }
30303
+ if (report === null) return;
30304
+ if (this.clip?.clipId !== clipId) {
30305
+ this.deps.onLog?.("clip playback: completeness dropped — the pointer has left this clip", {
30306
+ deviceId: this.deps.deviceId,
30307
+ clipId,
30308
+ code: report.code
30309
+ });
30310
+ return;
30311
+ }
30312
+ this.deps.onLog?.("clip playback: the clip did not arrive whole", {
30313
+ deviceId: this.deps.deviceId,
30314
+ clipId,
30315
+ code: report.code,
30316
+ reason: report.reason
30317
+ });
30318
+ this.deps.session.sendControl(serializeServerMessage({
30319
+ t: "clipIncomplete",
30320
+ clipId,
30321
+ code: report.code,
30322
+ reason: report.reason
30186
30323
  }));
30187
30324
  }
30188
30325
  /**
@@ -31584,7 +31721,7 @@ function createFmp4StreamDemuxer(options = {}) {
31584
31721
  * faster than the camera sends.
31585
31722
  * - `stepFrame(-1)` and the scrub verbs are refused by name; `stepFrame(+1)`
31586
31723
  * pushes the next buffered unit.
31587
- * - `setRate` is the PLAYER's rate over frames already here ({0} ∪ [0.25, 4],
31724
+ * - `setRate` is the PLAYER's rate over frames already here ({0} ∪ [0.25, 8],
31588
31725
  * the recorded domain). The camera's own `playSpeed` (8, 16 — real, the app
31589
31726
  * reopens the replay at 8) is a different mechanism and is NOT wired: it
31590
31727
  * would go on the dial (`videoclips.dialClipStream` → the cmd-5 replay's
@@ -31650,6 +31787,11 @@ function createFmp4StreamDemuxer(options = {}) {
31650
31787
  * `data`, which the broker's `onLog` puts in `tags`.
31651
31788
  */
31652
31789
  /**
31790
+ * The reason an in-flight open is abandoned, carried on the abort so the
31791
+ * opener's refusal and the provider's log line say the same thing.
31792
+ */
31793
+ var CLIP_STREAM_ABANDONED = "clip-stream-abandoned";
31794
+ /**
31653
31795
  * The same ceiling as the file path (`CLIP_SOURCE_MAX_BYTES`): a clip the
31654
31796
  * file path would refuse `too-large` is one this path stops taking. Frames
31655
31797
  * behind the head are released as they are pushed, so the backlog is the
@@ -31672,6 +31814,8 @@ var ClipStreamFeeder = class {
31672
31814
  deps;
31673
31815
  phase = "idle";
31674
31816
  connection = null;
31817
+ /** Armed for the length of one `open()`; aborted by {@link teardown}. */
31818
+ opening = null;
31675
31819
  demuxer;
31676
31820
  clock = new MonotonicClock();
31677
31821
  /** The RTP rate the audio sender negotiated — a CONVERSION, not a clock. */
@@ -31684,10 +31828,28 @@ var ClipStreamFeeder = class {
31684
31828
  audioPushed = 0;
31685
31829
  audioDroppedLate = 0;
31686
31830
  audioDroppedRate = 0;
31831
+ /**
31832
+ * The last report handed to a caller — the settle promise's value, or the
31833
+ * newest one {@link watchAudio}'s listener was given. What a change is
31834
+ * measured against, so the same answer is never announced twice.
31835
+ */
31836
+ audioEmitted = null;
31837
+ /**
31838
+ * The last NON-ZERO rate. Pause is the absence of a rate, not a rate: a
31839
+ * `clipAudio` blaming the speed while the clip is stopped would be false,
31840
+ * and un-pausing would have to take it back.
31841
+ */
31842
+ audioRate = 1;
31843
+ audioListener = null;
31687
31844
  audioDroppedSeek = 0;
31688
31845
  firstAudioLogged = false;
31689
31846
  audioReport = null;
31690
31847
  audioWaiters = [];
31848
+ /** The provider's last word, and whether it has been given. Settled is not
31849
+ * the same as non-null: a whole stream settles `null` (D623). */
31850
+ incompleteSettled = false;
31851
+ incompleteReport = null;
31852
+ incompleteWaiters = [];
31691
31853
  clockHasAdvanced = false;
31692
31854
  rate = 1;
31693
31855
  queue = [];
@@ -31719,12 +31881,16 @@ var ClipStreamFeeder = class {
31719
31881
  firstFrameLogged = false;
31720
31882
  maxBufferedBytes;
31721
31883
  stallTimeoutMs;
31884
+ prerollMs;
31885
+ /** True while the pacer is waiting for the cushion to refill after a starve. */
31886
+ rebuffering = false;
31722
31887
  now;
31723
31888
  tags;
31724
31889
  constructor(deps) {
31725
31890
  this.deps = deps;
31726
31891
  this.maxBufferedBytes = deps.maxBufferedBytes ?? CLIP_STREAM_BUFFER_MAX_BYTES;
31727
31892
  this.stallTimeoutMs = deps.stallTimeoutMs ?? 15e3;
31893
+ this.prerollMs = deps.prerollMs ?? 600;
31728
31894
  this.now = deps.now ?? (() => Date.now());
31729
31895
  this.tags = {
31730
31896
  deviceId: deps.handle.deviceId,
@@ -31822,6 +31988,8 @@ var ClipStreamFeeder = class {
31822
31988
  this.clock.setRate(next);
31823
31989
  const wasPaused = this.rate === 0;
31824
31990
  this.rate = next;
31991
+ this.audioRate = next;
31992
+ this.refreshAudioReport();
31825
31993
  if (this.phase !== "streaming" || this.pendingSeekMs !== null) return;
31826
31994
  if (wasPaused) this.emitState("playing", "set-rate-resume", this.seekToken);
31827
31995
  this.startPacing();
@@ -31856,6 +32024,19 @@ var ClipStreamFeeder = class {
31856
32024
  return new Promise((resolve) => this.durationWaiters.push(resolve));
31857
32025
  }
31858
32026
  /**
32027
+ * Did the provider deliver this clip whole? (D623)
32028
+ *
32029
+ * Resolves `null` for a whole stream AND for one that never got to say —
32030
+ * cut, disposed, or opened against a provider that adds no trailer. Both are
32031
+ * "nothing to tell the operator", and neither may be reported as a defect:
32032
+ * a cut already announces itself as `state: error`, and inventing a second
32033
+ * complaint for it would file one fault twice.
32034
+ */
32035
+ incomplete() {
32036
+ if (this.incompleteSettled) return Promise.resolve(this.incompleteReport);
32037
+ return new Promise((resolve) => this.incompleteWaiters.push(resolve));
32038
+ }
32039
+ /**
31859
32040
  * What this clip turned out to carry as sound — the answer the viewer needs
31860
32041
  * to say "this clip has no audio" rather than leave the operator wondering.
31861
32042
  *
@@ -31865,15 +32046,31 @@ var ClipStreamFeeder = class {
31865
32046
  */
31866
32047
  audio() {
31867
32048
  const settled = this.audioReport;
31868
- if (settled !== null) return Promise.resolve(settled);
32049
+ if (settled !== null) return Promise.resolve(this.audioNow(settled));
31869
32050
  return new Promise((resolve) => this.audioWaiters.push(resolve));
31870
32051
  }
32052
+ /**
32053
+ * Follow the audio answer for as long as this clip is on the pointer.
32054
+ *
32055
+ * {@link audio} settles ONCE, because what the stream carries is decided
32056
+ * once — at the `moov`. The RATE is not: it moves under the operator's
32057
+ * finger, and a one-shot report cannot say that the clip which had sound a
32058
+ * second ago is silent now because he pressed 2×. The listener is told only
32059
+ * when the answer really changes, never the settle it already awaited.
32060
+ *
32061
+ * One listener, replaced rather than added to: there is one session behind
32062
+ * one clip, and a list would outlive the pointer.
32063
+ */
32064
+ watchAudio(listener) {
32065
+ this.audioListener = listener;
32066
+ }
31871
32067
  dispose() {
31872
32068
  if (this.phase === "disposed") return;
31873
32069
  const before = this.phase;
31874
32070
  this.phase = "disposed";
31875
32071
  this.teardown();
31876
32072
  this.settleDuration(null);
32073
+ this.settleIncomplete(null);
31877
32074
  this.settleAudio({
31878
32075
  carried: false,
31879
32076
  codec: null,
@@ -31944,13 +32141,15 @@ var ClipStreamFeeder = class {
31944
32141
  audioReason: answer.audioReason ?? null
31945
32142
  });
31946
32143
  }
32144
+ const abandon = new AbortController();
32145
+ this.opening = abandon;
31947
32146
  let opened;
31948
32147
  try {
31949
32148
  opened = await this.deps.open(answer.url, {
31950
32149
  onData: (chunk) => this.onData(chunk),
31951
- onEnd: () => this.onEnd(),
32150
+ onEnd: (report) => this.onEnd(report),
31952
32151
  onError: (err) => this.cut("stream-failed", err.message)
31953
- });
32152
+ }, abandon.signal);
31954
32153
  } catch (err) {
31955
32154
  opened = {
31956
32155
  kind: "refused",
@@ -31959,8 +32158,15 @@ var ClipStreamFeeder = class {
31959
32158
  detail: describe(err)
31960
32159
  };
31961
32160
  }
32161
+ if (this.opening === abandon) this.opening = null;
31962
32162
  if (this.phase !== "opening") {
31963
32163
  if (opened.kind === "open") opened.connection.destroy();
32164
+ this.log("clip-stream:open-abandoned", {
32165
+ phase: this.phase,
32166
+ opened: opened.kind,
32167
+ sinceDialMs: this.now() - (this.dialedAt ?? this.now()),
32168
+ ...opened.kind === "refused" ? { code: opened.code } : {}
32169
+ });
31964
32170
  return;
31965
32171
  }
31966
32172
  if (opened.kind === "refused") {
@@ -31980,6 +32186,7 @@ var ClipStreamFeeder = class {
31980
32186
  this.phase = "refused";
31981
32187
  this.teardown();
31982
32188
  this.settleDuration(null);
32189
+ this.settleIncomplete(null);
31983
32190
  this.settleAudio({
31984
32191
  carried: false,
31985
32192
  codec: null,
@@ -32101,10 +32308,24 @@ var ClipStreamFeeder = class {
32101
32308
  return;
32102
32309
  }
32103
32310
  while (this.rate === 0 && this.pendingSteps > 0 && this.pushNext()) this.pendingSteps -= 1;
32104
- if (this.rate > 0 && this.paceTimer === null) this.tick();
32311
+ if (this.rate === 0) return;
32312
+ if (this.rebuffering) {
32313
+ if (this.cushionShortfallMs() > 0) return;
32314
+ this.rebuffering = false;
32315
+ this.log("clip-stream:refilled", {
32316
+ headMs: this.headMs,
32317
+ arrivedMs: this.arrivedMs,
32318
+ queued: this.queue.length,
32319
+ starved: this.starved
32320
+ });
32321
+ this.startPacing();
32322
+ return;
32323
+ }
32324
+ if (this.paceTimer === null) this.tick();
32105
32325
  }
32106
- onEnd() {
32326
+ onEnd(report) {
32107
32327
  if (!this.takesBytes()) return;
32328
+ this.settleIncomplete(report);
32108
32329
  this.streamEnded = true;
32109
32330
  this.clearStall();
32110
32331
  this.streamEnd = this.demuxer.end();
@@ -32122,13 +32343,20 @@ var ClipStreamFeeder = class {
32122
32343
  this.resolvePendingSeek(this.seekToken);
32123
32344
  return;
32124
32345
  }
32125
- if (this.rate > 0 && this.paceTimer === null) this.tick();
32346
+ if (this.rate === 0) return;
32347
+ if (this.rebuffering || this.anchorWall > this.now()) {
32348
+ this.rebuffering = false;
32349
+ this.startPacing();
32350
+ return;
32351
+ }
32352
+ if (this.paceTimer === null) this.tick();
32126
32353
  }
32127
32354
  cut(reason, detail) {
32128
32355
  if (this.phase !== "streaming" && this.phase !== "opening") return;
32129
32356
  this.phase = "cut";
32130
32357
  this.teardown();
32131
32358
  this.settleDuration(null);
32359
+ this.settleIncomplete(null);
32132
32360
  this.settleAudio({
32133
32361
  carried: false,
32134
32362
  codec: null,
@@ -32262,9 +32490,24 @@ var ClipStreamFeeder = class {
32262
32490
  this.audioQueue = this.audioQueue.slice(drop);
32263
32491
  this.audioDroppedSeek += drop;
32264
32492
  }
32493
+ /**
32494
+ * How much of the cushion is MISSING, in milliseconds of media.
32495
+ *
32496
+ * Zero once the stream has ended — there is nothing more coming, so waiting
32497
+ * for it would hang the clip — and zero for any source that already holds
32498
+ * more than the cushion, which is every fast one.
32499
+ */
32500
+ cushionShortfallMs() {
32501
+ if (this.streamEnded) return 0;
32502
+ const head = this.headMs;
32503
+ const arrived = this.arrivedMs;
32504
+ if (head === null || arrived === null) return this.prerollMs;
32505
+ return Math.max(0, this.prerollMs - (arrived - head));
32506
+ }
32265
32507
  startPacing() {
32266
32508
  this.stopPacing();
32267
- this.anchorWall = this.now();
32509
+ const shortfall = this.cushionShortfallMs();
32510
+ this.anchorWall = this.now() + shortfall;
32268
32511
  this.anchorPts = this.headMs ?? this.queue[0]?.ptsMs ?? 0;
32269
32512
  this.startPositionReports();
32270
32513
  this.tick();
@@ -32289,7 +32532,10 @@ var ClipStreamFeeder = class {
32289
32532
  const audio = this.audioQueue[0];
32290
32533
  if (video === void 0 && audio === void 0) {
32291
32534
  if (this.streamEnded) this.finish();
32292
- else this.noteStarved();
32535
+ else {
32536
+ this.rebuffering = true;
32537
+ this.noteStarved();
32538
+ }
32293
32539
  return;
32294
32540
  }
32295
32541
  const takeVideo = video !== void 0 && (audio === void 0 || video.ptsMs <= audio.ptsMs);
@@ -32325,7 +32571,7 @@ var ClipStreamFeeder = class {
32325
32571
  if (frame === void 0) return false;
32326
32572
  this.audioQueue = this.audioQueue.slice(1);
32327
32573
  this.audioQueuedBytes -= frame.data.byteLength;
32328
- if (this.rate !== 1) {
32574
+ if (!audioPlaysAtRate(this.rate)) {
32329
32575
  this.audioDroppedRate += 1;
32330
32576
  return true;
32331
32577
  }
@@ -32423,11 +32669,53 @@ var ClipStreamFeeder = class {
32423
32669
  audioQueued: this.audioQueue.length
32424
32670
  };
32425
32671
  }
32672
+ /** One-shot, like the duration beside it: the provider speaks once, at the
32673
+ * end of the body, and a cut settles `null` so nobody waits for ever. */
32674
+ settleIncomplete(report) {
32675
+ if (this.incompleteSettled) return;
32676
+ this.incompleteSettled = true;
32677
+ this.incompleteReport = report;
32678
+ if (report !== null) this.log("clip-stream:incomplete", {
32679
+ code: report.code,
32680
+ reason: report.reason
32681
+ });
32682
+ for (const resolve of this.incompleteWaiters.splice(0)) resolve(report);
32683
+ }
32426
32684
  settleAudio(report) {
32427
32685
  if (this.audioReport !== null) return;
32428
32686
  this.audioReport = report;
32687
+ const now = this.audioNow(report);
32688
+ this.audioEmitted = now;
32429
32689
  const waiters = this.audioWaiters.splice(0);
32430
- for (const resolve of waiters) resolve(report);
32690
+ for (const resolve of waiters) resolve(now);
32691
+ }
32692
+ /**
32693
+ * The audio answer AS IT STANDS — the stream's own settled fact, with the
32694
+ * rate laid over it.
32695
+ *
32696
+ * The overlay only ever applies to a clip that really is carrying sound. A
32697
+ * clip whose camera has no microphone is silent at 1× too, and replacing
32698
+ * `no-audio-track` with `rate` would swap the true cause for a transient
32699
+ * one and send the operator to the speed chip.
32700
+ */
32701
+ audioNow(settled) {
32702
+ if (!settled.carried || audioPlaysAtRate(this.audioRate)) return settled;
32703
+ return {
32704
+ carried: false,
32705
+ codec: null,
32706
+ reason: "rate",
32707
+ rate: this.audioRate
32708
+ };
32709
+ }
32710
+ /** Tell the listener when — and only when — the answer has actually moved. */
32711
+ refreshAudioReport() {
32712
+ const settled = this.audioReport;
32713
+ if (settled === null) return;
32714
+ const next = this.audioNow(settled);
32715
+ const last = this.audioEmitted;
32716
+ if (last !== null && last.reason === next.reason && last.rate === next.rate) return;
32717
+ this.audioEmitted = next;
32718
+ this.audioListener?.(next);
32431
32719
  }
32432
32720
  settleDuration(value) {
32433
32721
  if (this.duration !== null) return;
@@ -32442,6 +32730,9 @@ var ClipStreamFeeder = class {
32442
32730
  this.stopPacing();
32443
32731
  this.stopPositionReports();
32444
32732
  this.clearStall();
32733
+ const opening = this.opening;
32734
+ this.opening = null;
32735
+ opening?.abort(/* @__PURE__ */ new Error(CLIP_STREAM_ABANDONED));
32445
32736
  const connection = this.connection;
32446
32737
  this.connection = null;
32447
32738
  connection?.destroy();
@@ -32483,6 +32774,62 @@ var ClipStreamFeeder = class {
32483
32774
  function describe(err) {
32484
32775
  return err instanceof Error ? err.message : String(err);
32485
32776
  }
32777
+ //#endregion
32778
+ //#region src/stream-broker/recorded/clip-stream-http.ts
32779
+ /**
32780
+ * clip-stream-http — the `node:http` half of {@link ClipStreamOpener}.
32781
+ *
32782
+ * Dials the URL `videoclips.dialClipStream` handed back — plain HTTP on the
32783
+ * provider's own token-gated listener, never the hub's proxy (an addon holds
32784
+ * no user credential, and the hub proxy takes only those) — and turns the
32785
+ * response into what the feeder drives: `pause`/`resume`/`destroy` on the
32786
+ * socket and the three events.
32787
+ *
32788
+ * A status other than 200 is a REFUSAL, named by the provider's own headers
32789
+ * (`x-camstack-reason-code`, `x-camstack-reason`), never a stream of nothing.
32790
+ * A 200 that does not declare `x-camstack-clip-transport: forward-only` is
32791
+ * refused too: it means the URL is not the stream route, and playing a
32792
+ * progressive file through a forward-only feeder would look like it worked
32793
+ * until the first seek.
32794
+ */
32795
+ /**
32796
+ * The codes a provider may add AFTER its body, and the only two this hop
32797
+ * carries onward.
32798
+ *
32799
+ * A stream's status line is spent on its first fragment, so a serve the camera
32800
+ * cut short has nothing left to say it with but a trailer — and a trailer this
32801
+ * reader did not know about would be read as a whole clip, which is the defect
32802
+ * D623 is about, one hop further out. Anything else a provider writes there is
32803
+ * ITS diagnostic and stays in ITS log: a viewer is shown what it can act on.
32804
+ */
32805
+ var CARRIED_TRAILER_CODES = ["stream-short", "stream-empty"];
32806
+ function trailerText(trailers, name) {
32807
+ const value = trailers[name];
32808
+ return typeof value === "string" && value.length > 0 ? value : void 0;
32809
+ }
32810
+ /**
32811
+ * What the provider said about its own stream once the last byte was out.
32812
+ *
32813
+ * `null` for a stream that added nothing, which is a whole one — the absence
32814
+ * is the statement, and it is never read as "unknown". A code with no sentence
32815
+ * is still reported, with the code standing in for one: a consumer that
32816
+ * branches on the code must not lose the branch because the prose was dropped
32817
+ * by a proxy.
32818
+ */
32819
+ function readClipStreamTrailers(trailers) {
32820
+ const code = trailerText(trailers, require_dist.CLIP_STILL_REASON_CODE_HEADER);
32821
+ if (code === void 0 || !CARRIED_TRAILER_CODES.includes(code)) return null;
32822
+ return {
32823
+ code: code === "stream-empty" ? "stream-empty" : "stream-short",
32824
+ reason: trailerText(trailers, "x-camstack-reason") ?? code
32825
+ };
32826
+ }
32827
+ /** The abort's reason, as a sentence the provider's log can be matched against. */
32828
+ function describeReason(reason) {
32829
+ if (reason instanceof Error) return reason.message;
32830
+ if (typeof reason === "string") return reason;
32831
+ return "the clip stream was abandoned before it opened";
32832
+ }
32486
32833
  function headerText(res, name) {
32487
32834
  const value = res.headers[name];
32488
32835
  if (Array.isArray(value)) return value[0];
@@ -32490,14 +32837,26 @@ function headerText(res, name) {
32490
32837
  }
32491
32838
  function createHttpClipStreamOpener(options = {}) {
32492
32839
  const headersTimeoutMs = options.headersTimeoutMs ?? 3e4;
32493
- return (url, handlers) => new Promise((resolve) => {
32840
+ return (url, handlers, signal) => new Promise((resolve) => {
32494
32841
  let settled = false;
32842
+ let onAbort = null;
32495
32843
  const settle = (value) => {
32496
32844
  if (settled) return;
32497
32845
  settled = true;
32498
32846
  clearTimeout(timer);
32847
+ if (onAbort !== null) signal.removeEventListener("abort", onAbort);
32499
32848
  resolve(value);
32500
32849
  };
32850
+ const abandoned = () => ({
32851
+ kind: "refused",
32852
+ status: 0,
32853
+ code: "abandoned",
32854
+ detail: describeReason(signal.reason)
32855
+ });
32856
+ if (signal.aborted) {
32857
+ resolve(abandoned());
32858
+ return;
32859
+ }
32501
32860
  let target;
32502
32861
  try {
32503
32862
  target = new URL(url);
@@ -32526,7 +32885,16 @@ function createHttpClipStreamOpener(options = {}) {
32526
32885
  const timer = setTimeout(() => {
32527
32886
  req.destroy(/* @__PURE__ */ new Error(`no status line in ${headersTimeoutMs} ms`));
32528
32887
  }, headersTimeoutMs);
32888
+ onAbort = () => {
32889
+ req.destroy(new Error(describeReason(signal.reason)));
32890
+ settle(abandoned());
32891
+ };
32892
+ signal.addEventListener("abort", onAbort, { once: true });
32529
32893
  req.on("error", (err) => {
32894
+ if (signal.aborted) {
32895
+ settle(abandoned());
32896
+ return;
32897
+ }
32530
32898
  settle({
32531
32899
  kind: "refused",
32532
32900
  status: 0,
@@ -32557,7 +32925,7 @@ function createHttpClipStreamOpener(options = {}) {
32557
32925
  return;
32558
32926
  }
32559
32927
  res.on("data", (chunk) => handlers.onData(chunk));
32560
- res.on("end", () => handlers.onEnd());
32928
+ res.on("end", () => handlers.onEnd(readClipStreamTrailers(res.trailers)));
32561
32929
  res.on("error", (err) => handlers.onError(err));
32562
32930
  settle({
32563
32931
  kind: "open",
@@ -35892,8 +36260,8 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
35892
36260
  clearTimeout(timer);
35893
36261
  }
35894
36262
  },
35895
- locateRecordedSegment: (args) => this.ctx.api.recording.locateSegment.query(args),
35896
- readRecordedSegmentBytes: async (args) => (await this.ctx.api.recording.readSegmentBytes.query(args)).data,
36263
+ locateRecordedSegment: (args) => this.ctx.api.recordingArchive.locateSegment.query(args),
36264
+ readRecordedSegmentBytes: async (args) => (await this.ctx.api.recordingArchive.readSegmentBytes.query(args)).data,
35897
36265
  readClipBytes: async (args) => {
35898
36266
  const res = await this.ctx.api.videoclips.readClipBytes.query({
35899
36267
  deviceId: args.deviceId,
@@ -35926,7 +36294,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
35926
36294
  },
35927
36295
  readRecordedGopBytes: async (args) => {
35928
36296
  try {
35929
- return await this.ctx.api.recording.readGopBytes.query({
36297
+ return await this.ctx.api.recordingArchive.readGopBytes.query({
35930
36298
  deviceId: args.deviceId,
35931
36299
  profile: args.profile,
35932
36300
  startMs: args.startMs,
@@ -35941,7 +36309,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
35941
36309
  });
35942
36310
  }
35943
36311
  return {
35944
- data: (await this.ctx.api.recording.readSegmentBytes.query({
36312
+ data: (await this.ctx.api.recordingArchive.readSegmentBytes.query({
35945
36313
  deviceId: args.deviceId,
35946
36314
  profile: args.profile,
35947
36315
  startMs: args.startMs