@camstack/addon-pipeline 1.2.287 → 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.
- package/dist/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/{default-detection-model-GPkHN13Q.mjs → default-detection-model-B64MIW2h.mjs} +1 -1
- package/dist/{default-detection-model-COWU8Rz0.js → default-detection-model-CN_Iu8NF.js} +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-DiLFfaYv.js → dist-BIlnUdBn.js} +1456 -833
- package/dist/{dist-B0wh1O_e.mjs → dist-C4JqXCw-.mjs} +1427 -834
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-UjSfDx5h.mjs → node-C-WfKJ6b.mjs} +1 -1
- package/dist/{node-Beq3CXsJ.js → node-CVlJhqng.js} +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-CU-Vka6-.mjs → process-memory-CJKKFNar.mjs} +1 -1
- package/dist/{process-memory-hlScqZ3l.js → process-memory-ZUucMC8Z.js} +1 -1
- package/dist/recorder/index.js +3960 -3666
- package/dist/recorder/index.mjs +3960 -3666
- package/dist/{segment-demux-js-CbeSistI.mjs → segment-demux-js-4OAHyQ3t.mjs} +1 -1
- package/dist/{segment-demux-js-Cd4K5AFm.js → segment-demux-js-D0mqN4RC.js} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BN28nLCx.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-r5TULO-I.mjs} +2 -2
- 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
- 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
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-2Mi5oqGe.mjs → hostInit-DPdwl8-T.mjs} +2 -2
- package/dist/stream-broker/index.js +446 -90
- package/dist/stream-broker/index.mjs +446 -90
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +5 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DVp8RDes.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-6yZguAUw.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-
|
|
7
|
-
const require_node = require("../node-
|
|
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-
|
|
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(
|
|
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,14 +26659,40 @@ var ControlMessageSchema = require_dist.discriminatedUnion("t", [
|
|
|
26643
26659
|
})
|
|
26644
26660
|
]);
|
|
26645
26661
|
/**
|
|
26646
|
-
* Clamp a requested playback rate onto the supported domain:
|
|
26647
|
-
* 0
|
|
26648
|
-
*
|
|
26649
|
-
*
|
|
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(
|
|
26676
|
+
return Math.min(MAX_PACED_RATE, Math.max(MIN_PACED_RATE, rate));
|
|
26677
|
+
}
|
|
26678
|
+
/**
|
|
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.
|
|
26693
|
+
*/
|
|
26694
|
+
function audioPlaysAtRate(rate) {
|
|
26695
|
+
return rate === 1;
|
|
26654
26696
|
}
|
|
26655
26697
|
var ServerMessageSchema = require_dist.discriminatedUnion("t", [
|
|
26656
26698
|
require_dist.object({
|
|
@@ -26693,12 +26735,12 @@ var ServerMessageSchema = require_dist.discriminatedUnion("t", [
|
|
|
26693
26735
|
durationMs: require_dist.number().positive()
|
|
26694
26736
|
}),
|
|
26695
26737
|
require_dist.object({
|
|
26696
|
-
t: require_dist.literal("
|
|
26738
|
+
t: require_dist.literal("clipIncomplete"),
|
|
26697
26739
|
clipId: require_dist.string().min(1),
|
|
26698
|
-
|
|
26699
|
-
|
|
26700
|
-
|
|
26701
|
-
|
|
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)
|
|
26702
26744
|
}),
|
|
26703
26745
|
require_dist.object({
|
|
26704
26746
|
t: require_dist.literal("clipAudio"),
|
|
@@ -26712,8 +26754,23 @@ var ServerMessageSchema = require_dist.discriminatedUnion("t", [
|
|
|
26712
26754
|
"no-audio-track",
|
|
26713
26755
|
"unsupported-codec",
|
|
26714
26756
|
"codec-mismatch",
|
|
26715
|
-
"stream-failed"
|
|
26716
|
-
|
|
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
|
+
});
|
|
26717
26774
|
})
|
|
26718
26775
|
]);
|
|
26719
26776
|
function parseControlMessage(raw) {
|
|
@@ -27685,7 +27742,7 @@ var RecordedFeeder = class {
|
|
|
27685
27742
|
/** Audio clock (PCMU 8 kHz / Opus 48 kHz); re-anchored together with video. */
|
|
27686
27743
|
audioClock;
|
|
27687
27744
|
clockHasAdvanced = false;
|
|
27688
|
-
/** Playback rate: 0 (paused) or a continuous factor in [0.25,
|
|
27745
|
+
/** Playback rate: 0 (paused) or a continuous factor in [0.25, 8]. */
|
|
27689
27746
|
rate = 1;
|
|
27690
27747
|
disposed = false;
|
|
27691
27748
|
seekToken = 0;
|
|
@@ -28266,7 +28323,7 @@ var RecordedFeeder = class {
|
|
|
28266
28323
|
}
|
|
28267
28324
|
/**
|
|
28268
28325
|
* Set playback rate. 0 pauses in place (the pacing loop aborts on its next
|
|
28269
|
-
* guard check); any other value is clamped to [0.25,
|
|
28326
|
+
* guard check); any other value is clamped to [0.25, 8].
|
|
28270
28327
|
*
|
|
28271
28328
|
* Rate-change strategy (documented choice): every transition TO a non-zero
|
|
28272
28329
|
* rate — resume from pause AND a playing→playing rate change — re-enters via
|
|
@@ -30131,58 +30188,102 @@ var TimelineSession = class {
|
|
|
30131
30188
|
this.lastPositionMs = null;
|
|
30132
30189
|
const feeder = this.enterRecorded();
|
|
30133
30190
|
await feeder.seek(offsetMs);
|
|
30134
|
-
|
|
30135
|
-
|
|
30191
|
+
await Promise.all([
|
|
30192
|
+
this.reportClipDuration(clipId, feeder),
|
|
30193
|
+
this.reportClipAudio(clipId, feeder),
|
|
30194
|
+
this.reportClipIncomplete(clipId, feeder)
|
|
30195
|
+
]);
|
|
30136
30196
|
}
|
|
30137
30197
|
/**
|
|
30138
|
-
* Tell the viewer
|
|
30198
|
+
* Tell the viewer whether this clip has sound.
|
|
30139
30199
|
*
|
|
30140
|
-
*
|
|
30141
|
-
*
|
|
30142
|
-
*
|
|
30143
|
-
*
|
|
30144
|
-
*
|
|
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.
|
|
30145
30207
|
*
|
|
30146
|
-
*
|
|
30147
|
-
*
|
|
30148
|
-
* be this surface inventing a capability.
|
|
30208
|
+
* The first answer SETTLES; the rate does not, so this also subscribes for
|
|
30209
|
+
* as long as the clip is on the pointer.
|
|
30149
30210
|
*/
|
|
30150
|
-
|
|
30151
|
-
|
|
30152
|
-
|
|
30153
|
-
this.deps.onLog?.("clip playback: transport not reported — this feeder cannot say", {
|
|
30211
|
+
async reportClipAudio(clipId, feeder) {
|
|
30212
|
+
if (feeder.audio === void 0) {
|
|
30213
|
+
this.deps.onLog?.("clip playback: audio not reported — this feeder cannot say", {
|
|
30154
30214
|
deviceId: this.deps.deviceId,
|
|
30155
30215
|
clipId
|
|
30156
30216
|
});
|
|
30157
30217
|
return;
|
|
30158
30218
|
}
|
|
30159
|
-
|
|
30160
|
-
|
|
30219
|
+
let report;
|
|
30220
|
+
try {
|
|
30221
|
+
report = await feeder.audio();
|
|
30222
|
+
} catch (err) {
|
|
30223
|
+
this.deps.onLog?.("clip playback: audio not reported — the answer failed", {
|
|
30224
|
+
deviceId: this.deps.deviceId,
|
|
30225
|
+
clipId,
|
|
30226
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30227
|
+
});
|
|
30228
|
+
return;
|
|
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) {
|
|
30243
|
+
if (this.clip?.clipId !== clipId) {
|
|
30244
|
+
this.deps.onLog?.("clip playback: audio dropped — the pointer has left this clip", {
|
|
30245
|
+
deviceId: this.deps.deviceId,
|
|
30246
|
+
clipId,
|
|
30247
|
+
carried: report.carried,
|
|
30248
|
+
reason: report.reason
|
|
30249
|
+
});
|
|
30250
|
+
return;
|
|
30251
|
+
}
|
|
30252
|
+
this.deps.onLog?.("clip playback: audio reported", {
|
|
30161
30253
|
deviceId: this.deps.deviceId,
|
|
30162
30254
|
clipId,
|
|
30163
|
-
|
|
30164
|
-
|
|
30255
|
+
carried: report.carried,
|
|
30256
|
+
codec: report.codec,
|
|
30257
|
+
reason: report.reason,
|
|
30258
|
+
rate: report.rate,
|
|
30259
|
+
detail: report.detail
|
|
30165
30260
|
});
|
|
30166
30261
|
this.deps.session.sendControl(serializeServerMessage({
|
|
30167
|
-
t: "
|
|
30262
|
+
t: "clipAudio",
|
|
30168
30263
|
clipId,
|
|
30169
|
-
|
|
30170
|
-
|
|
30264
|
+
carried: report.carried,
|
|
30265
|
+
...report.codec !== null ? { codec: report.codec } : {},
|
|
30266
|
+
...report.reason !== null ? { reason: report.reason } : {},
|
|
30267
|
+
...report.reason === "rate" && report.rate !== void 0 ? { rate: report.rate } : {}
|
|
30171
30268
|
}));
|
|
30172
30269
|
}
|
|
30173
30270
|
/**
|
|
30174
|
-
* Tell the
|
|
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.
|
|
30175
30273
|
*
|
|
30176
|
-
*
|
|
30177
|
-
*
|
|
30178
|
-
*
|
|
30179
|
-
*
|
|
30180
|
-
*
|
|
30181
|
-
*
|
|
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.
|
|
30182
30283
|
*/
|
|
30183
|
-
async
|
|
30184
|
-
if (feeder.
|
|
30185
|
-
this.deps.onLog?.("clip playback:
|
|
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", {
|
|
30186
30287
|
deviceId: this.deps.deviceId,
|
|
30187
30288
|
clipId
|
|
30188
30289
|
});
|
|
@@ -30190,37 +30291,35 @@ var TimelineSession = class {
|
|
|
30190
30291
|
}
|
|
30191
30292
|
let report;
|
|
30192
30293
|
try {
|
|
30193
|
-
report = await feeder.
|
|
30294
|
+
report = await feeder.incomplete();
|
|
30194
30295
|
} catch (err) {
|
|
30195
|
-
this.deps.onLog?.("clip playback:
|
|
30296
|
+
this.deps.onLog?.("clip playback: completeness not reported — the answer failed", {
|
|
30196
30297
|
deviceId: this.deps.deviceId,
|
|
30197
30298
|
clipId,
|
|
30198
30299
|
error: err instanceof Error ? err.message : String(err)
|
|
30199
30300
|
});
|
|
30200
30301
|
return;
|
|
30201
30302
|
}
|
|
30303
|
+
if (report === null) return;
|
|
30202
30304
|
if (this.clip?.clipId !== clipId) {
|
|
30203
|
-
this.deps.onLog?.("clip playback:
|
|
30305
|
+
this.deps.onLog?.("clip playback: completeness dropped — the pointer has left this clip", {
|
|
30204
30306
|
deviceId: this.deps.deviceId,
|
|
30205
30307
|
clipId,
|
|
30206
|
-
|
|
30308
|
+
code: report.code
|
|
30207
30309
|
});
|
|
30208
30310
|
return;
|
|
30209
30311
|
}
|
|
30210
|
-
this.deps.onLog?.("clip playback:
|
|
30312
|
+
this.deps.onLog?.("clip playback: the clip did not arrive whole", {
|
|
30211
30313
|
deviceId: this.deps.deviceId,
|
|
30212
30314
|
clipId,
|
|
30213
|
-
|
|
30214
|
-
|
|
30215
|
-
reason: report.reason,
|
|
30216
|
-
detail: report.detail
|
|
30315
|
+
code: report.code,
|
|
30316
|
+
reason: report.reason
|
|
30217
30317
|
});
|
|
30218
30318
|
this.deps.session.sendControl(serializeServerMessage({
|
|
30219
|
-
t: "
|
|
30319
|
+
t: "clipIncomplete",
|
|
30220
30320
|
clipId,
|
|
30221
|
-
|
|
30222
|
-
|
|
30223
|
-
...report.reason !== null ? { reason: report.reason } : {}
|
|
30321
|
+
code: report.code,
|
|
30322
|
+
reason: report.reason
|
|
30224
30323
|
}));
|
|
30225
30324
|
}
|
|
30226
30325
|
/**
|
|
@@ -30807,7 +30906,7 @@ function createRefusedClipFeeder(deps) {
|
|
|
30807
30906
|
};
|
|
30808
30907
|
return {
|
|
30809
30908
|
seek: async () => refuse("seek"),
|
|
30810
|
-
setRate: () =>
|
|
30909
|
+
setRate: () => refuse("setRate"),
|
|
30811
30910
|
stepFrame: () => refuse("stepFrame"),
|
|
30812
30911
|
beginScrub: () => refuse("beginScrub"),
|
|
30813
30912
|
scrubTo: () => {},
|
|
@@ -31622,7 +31721,7 @@ function createFmp4StreamDemuxer(options = {}) {
|
|
|
31622
31721
|
* faster than the camera sends.
|
|
31623
31722
|
* - `stepFrame(-1)` and the scrub verbs are refused by name; `stepFrame(+1)`
|
|
31624
31723
|
* pushes the next buffered unit.
|
|
31625
|
-
* - `setRate` is the PLAYER's rate over frames already here ({0} ∪ [0.25,
|
|
31724
|
+
* - `setRate` is the PLAYER's rate over frames already here ({0} ∪ [0.25, 8],
|
|
31626
31725
|
* the recorded domain). The camera's own `playSpeed` (8, 16 — real, the app
|
|
31627
31726
|
* reopens the replay at 8) is a different mechanism and is NOT wired: it
|
|
31628
31727
|
* would go on the dial (`videoclips.dialClipStream` → the cmd-5 replay's
|
|
@@ -31688,6 +31787,11 @@ function createFmp4StreamDemuxer(options = {}) {
|
|
|
31688
31787
|
* `data`, which the broker's `onLog` puts in `tags`.
|
|
31689
31788
|
*/
|
|
31690
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
|
+
/**
|
|
31691
31795
|
* The same ceiling as the file path (`CLIP_SOURCE_MAX_BYTES`): a clip the
|
|
31692
31796
|
* file path would refuse `too-large` is one this path stops taking. Frames
|
|
31693
31797
|
* behind the head are released as they are pushed, so the backlog is the
|
|
@@ -31708,18 +31812,10 @@ function createClipStreamFeeder(deps) {
|
|
|
31708
31812
|
}
|
|
31709
31813
|
var ClipStreamFeeder = class {
|
|
31710
31814
|
deps;
|
|
31711
|
-
/**
|
|
31712
|
-
* Forward-only, and no backward frame-step: the bytes behind the head were
|
|
31713
|
-
* never kept, and the camera offers no replay-from-offset. Stated here so
|
|
31714
|
-
* the viewer can DISABLE those verbs rather than offer them and have every
|
|
31715
|
-
* use refused — the whole content of this report.
|
|
31716
|
-
*/
|
|
31717
|
-
transport = {
|
|
31718
|
-
seek: "forward",
|
|
31719
|
-
stepBack: false
|
|
31720
|
-
};
|
|
31721
31815
|
phase = "idle";
|
|
31722
31816
|
connection = null;
|
|
31817
|
+
/** Armed for the length of one `open()`; aborted by {@link teardown}. */
|
|
31818
|
+
opening = null;
|
|
31723
31819
|
demuxer;
|
|
31724
31820
|
clock = new MonotonicClock();
|
|
31725
31821
|
/** The RTP rate the audio sender negotiated — a CONVERSION, not a clock. */
|
|
@@ -31732,10 +31828,28 @@ var ClipStreamFeeder = class {
|
|
|
31732
31828
|
audioPushed = 0;
|
|
31733
31829
|
audioDroppedLate = 0;
|
|
31734
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;
|
|
31735
31844
|
audioDroppedSeek = 0;
|
|
31736
31845
|
firstAudioLogged = false;
|
|
31737
31846
|
audioReport = null;
|
|
31738
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 = [];
|
|
31739
31853
|
clockHasAdvanced = false;
|
|
31740
31854
|
rate = 1;
|
|
31741
31855
|
queue = [];
|
|
@@ -31758,6 +31872,8 @@ var ClipStreamFeeder = class {
|
|
|
31758
31872
|
lastAnnounced = null;
|
|
31759
31873
|
scrubRefusedFor = void 0;
|
|
31760
31874
|
pushed = 0;
|
|
31875
|
+
/** How many times the pacer wanted a frame the stream had not produced. */
|
|
31876
|
+
starved = 0;
|
|
31761
31877
|
dropped = 0;
|
|
31762
31878
|
bytesIn = 0;
|
|
31763
31879
|
dialedAt = null;
|
|
@@ -31765,12 +31881,16 @@ var ClipStreamFeeder = class {
|
|
|
31765
31881
|
firstFrameLogged = false;
|
|
31766
31882
|
maxBufferedBytes;
|
|
31767
31883
|
stallTimeoutMs;
|
|
31884
|
+
prerollMs;
|
|
31885
|
+
/** True while the pacer is waiting for the cushion to refill after a starve. */
|
|
31886
|
+
rebuffering = false;
|
|
31768
31887
|
now;
|
|
31769
31888
|
tags;
|
|
31770
31889
|
constructor(deps) {
|
|
31771
31890
|
this.deps = deps;
|
|
31772
31891
|
this.maxBufferedBytes = deps.maxBufferedBytes ?? CLIP_STREAM_BUFFER_MAX_BYTES;
|
|
31773
31892
|
this.stallTimeoutMs = deps.stallTimeoutMs ?? 15e3;
|
|
31893
|
+
this.prerollMs = deps.prerollMs ?? 600;
|
|
31774
31894
|
this.now = deps.now ?? (() => Date.now());
|
|
31775
31895
|
this.tags = {
|
|
31776
31896
|
deviceId: deps.handle.deviceId,
|
|
@@ -31853,6 +31973,11 @@ var ClipStreamFeeder = class {
|
|
|
31853
31973
|
}
|
|
31854
31974
|
setRate(rate) {
|
|
31855
31975
|
const next = clampPlaybackRate(rate);
|
|
31976
|
+
if (next !== rate) this.log("clip-stream:rate-not-delivered", {
|
|
31977
|
+
requested: rate,
|
|
31978
|
+
applied: next,
|
|
31979
|
+
deliverable: BROKER_PACED_RATES
|
|
31980
|
+
});
|
|
31856
31981
|
if (next === 0) {
|
|
31857
31982
|
this.rate = 0;
|
|
31858
31983
|
this.stopPacing();
|
|
@@ -31863,6 +31988,8 @@ var ClipStreamFeeder = class {
|
|
|
31863
31988
|
this.clock.setRate(next);
|
|
31864
31989
|
const wasPaused = this.rate === 0;
|
|
31865
31990
|
this.rate = next;
|
|
31991
|
+
this.audioRate = next;
|
|
31992
|
+
this.refreshAudioReport();
|
|
31866
31993
|
if (this.phase !== "streaming" || this.pendingSeekMs !== null) return;
|
|
31867
31994
|
if (wasPaused) this.emitState("playing", "set-rate-resume", this.seekToken);
|
|
31868
31995
|
this.startPacing();
|
|
@@ -31897,6 +32024,19 @@ var ClipStreamFeeder = class {
|
|
|
31897
32024
|
return new Promise((resolve) => this.durationWaiters.push(resolve));
|
|
31898
32025
|
}
|
|
31899
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
|
+
/**
|
|
31900
32040
|
* What this clip turned out to carry as sound — the answer the viewer needs
|
|
31901
32041
|
* to say "this clip has no audio" rather than leave the operator wondering.
|
|
31902
32042
|
*
|
|
@@ -31906,15 +32046,31 @@ var ClipStreamFeeder = class {
|
|
|
31906
32046
|
*/
|
|
31907
32047
|
audio() {
|
|
31908
32048
|
const settled = this.audioReport;
|
|
31909
|
-
if (settled !== null) return Promise.resolve(settled);
|
|
32049
|
+
if (settled !== null) return Promise.resolve(this.audioNow(settled));
|
|
31910
32050
|
return new Promise((resolve) => this.audioWaiters.push(resolve));
|
|
31911
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
|
+
}
|
|
31912
32067
|
dispose() {
|
|
31913
32068
|
if (this.phase === "disposed") return;
|
|
31914
32069
|
const before = this.phase;
|
|
31915
32070
|
this.phase = "disposed";
|
|
31916
32071
|
this.teardown();
|
|
31917
32072
|
this.settleDuration(null);
|
|
32073
|
+
this.settleIncomplete(null);
|
|
31918
32074
|
this.settleAudio({
|
|
31919
32075
|
carried: false,
|
|
31920
32076
|
codec: null,
|
|
@@ -31985,13 +32141,15 @@ var ClipStreamFeeder = class {
|
|
|
31985
32141
|
audioReason: answer.audioReason ?? null
|
|
31986
32142
|
});
|
|
31987
32143
|
}
|
|
32144
|
+
const abandon = new AbortController();
|
|
32145
|
+
this.opening = abandon;
|
|
31988
32146
|
let opened;
|
|
31989
32147
|
try {
|
|
31990
32148
|
opened = await this.deps.open(answer.url, {
|
|
31991
32149
|
onData: (chunk) => this.onData(chunk),
|
|
31992
|
-
onEnd: () => this.onEnd(),
|
|
32150
|
+
onEnd: (report) => this.onEnd(report),
|
|
31993
32151
|
onError: (err) => this.cut("stream-failed", err.message)
|
|
31994
|
-
});
|
|
32152
|
+
}, abandon.signal);
|
|
31995
32153
|
} catch (err) {
|
|
31996
32154
|
opened = {
|
|
31997
32155
|
kind: "refused",
|
|
@@ -32000,8 +32158,15 @@ var ClipStreamFeeder = class {
|
|
|
32000
32158
|
detail: describe(err)
|
|
32001
32159
|
};
|
|
32002
32160
|
}
|
|
32161
|
+
if (this.opening === abandon) this.opening = null;
|
|
32003
32162
|
if (this.phase !== "opening") {
|
|
32004
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
|
+
});
|
|
32005
32170
|
return;
|
|
32006
32171
|
}
|
|
32007
32172
|
if (opened.kind === "refused") {
|
|
@@ -32021,6 +32186,7 @@ var ClipStreamFeeder = class {
|
|
|
32021
32186
|
this.phase = "refused";
|
|
32022
32187
|
this.teardown();
|
|
32023
32188
|
this.settleDuration(null);
|
|
32189
|
+
this.settleIncomplete(null);
|
|
32024
32190
|
this.settleAudio({
|
|
32025
32191
|
carried: false,
|
|
32026
32192
|
codec: null,
|
|
@@ -32142,14 +32308,29 @@ var ClipStreamFeeder = class {
|
|
|
32142
32308
|
return;
|
|
32143
32309
|
}
|
|
32144
32310
|
while (this.rate === 0 && this.pendingSteps > 0 && this.pushNext()) this.pendingSteps -= 1;
|
|
32145
|
-
if (this.rate
|
|
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();
|
|
32146
32325
|
}
|
|
32147
|
-
onEnd() {
|
|
32326
|
+
onEnd(report) {
|
|
32148
32327
|
if (!this.takesBytes()) return;
|
|
32328
|
+
this.settleIncomplete(report);
|
|
32149
32329
|
this.streamEnded = true;
|
|
32150
32330
|
this.clearStall();
|
|
32151
32331
|
this.streamEnd = this.demuxer.end();
|
|
32152
32332
|
this.log("clip-stream:ended", {
|
|
32333
|
+
starved: this.starved,
|
|
32153
32334
|
samples: this.streamEnd.samples,
|
|
32154
32335
|
durationMs: this.streamEnd.durationMs,
|
|
32155
32336
|
bytesIn: this.bytesIn,
|
|
@@ -32162,13 +32343,20 @@ var ClipStreamFeeder = class {
|
|
|
32162
32343
|
this.resolvePendingSeek(this.seekToken);
|
|
32163
32344
|
return;
|
|
32164
32345
|
}
|
|
32165
|
-
if (this.rate
|
|
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();
|
|
32166
32353
|
}
|
|
32167
32354
|
cut(reason, detail) {
|
|
32168
32355
|
if (this.phase !== "streaming" && this.phase !== "opening") return;
|
|
32169
32356
|
this.phase = "cut";
|
|
32170
32357
|
this.teardown();
|
|
32171
32358
|
this.settleDuration(null);
|
|
32359
|
+
this.settleIncomplete(null);
|
|
32172
32360
|
this.settleAudio({
|
|
32173
32361
|
carried: false,
|
|
32174
32362
|
codec: null,
|
|
@@ -32302,9 +32490,24 @@ var ClipStreamFeeder = class {
|
|
|
32302
32490
|
this.audioQueue = this.audioQueue.slice(drop);
|
|
32303
32491
|
this.audioDroppedSeek += drop;
|
|
32304
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
|
+
}
|
|
32305
32507
|
startPacing() {
|
|
32306
32508
|
this.stopPacing();
|
|
32307
|
-
|
|
32509
|
+
const shortfall = this.cushionShortfallMs();
|
|
32510
|
+
this.anchorWall = this.now() + shortfall;
|
|
32308
32511
|
this.anchorPts = this.headMs ?? this.queue[0]?.ptsMs ?? 0;
|
|
32309
32512
|
this.startPositionReports();
|
|
32310
32513
|
this.tick();
|
|
@@ -32329,6 +32532,10 @@ var ClipStreamFeeder = class {
|
|
|
32329
32532
|
const audio = this.audioQueue[0];
|
|
32330
32533
|
if (video === void 0 && audio === void 0) {
|
|
32331
32534
|
if (this.streamEnded) this.finish();
|
|
32535
|
+
else {
|
|
32536
|
+
this.rebuffering = true;
|
|
32537
|
+
this.noteStarved();
|
|
32538
|
+
}
|
|
32332
32539
|
return;
|
|
32333
32540
|
}
|
|
32334
32541
|
const takeVideo = video !== void 0 && (audio === void 0 || video.ptsMs <= audio.ptsMs);
|
|
@@ -32364,7 +32571,7 @@ var ClipStreamFeeder = class {
|
|
|
32364
32571
|
if (frame === void 0) return false;
|
|
32365
32572
|
this.audioQueue = this.audioQueue.slice(1);
|
|
32366
32573
|
this.audioQueuedBytes -= frame.data.byteLength;
|
|
32367
|
-
if (this.rate
|
|
32574
|
+
if (!audioPlaysAtRate(this.rate)) {
|
|
32368
32575
|
this.audioDroppedRate += 1;
|
|
32369
32576
|
return true;
|
|
32370
32577
|
}
|
|
@@ -32462,11 +32669,53 @@ var ClipStreamFeeder = class {
|
|
|
32462
32669
|
audioQueued: this.audioQueue.length
|
|
32463
32670
|
};
|
|
32464
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
|
+
}
|
|
32465
32684
|
settleAudio(report) {
|
|
32466
32685
|
if (this.audioReport !== null) return;
|
|
32467
32686
|
this.audioReport = report;
|
|
32687
|
+
const now = this.audioNow(report);
|
|
32688
|
+
this.audioEmitted = now;
|
|
32468
32689
|
const waiters = this.audioWaiters.splice(0);
|
|
32469
|
-
for (const resolve of waiters) resolve(
|
|
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);
|
|
32470
32719
|
}
|
|
32471
32720
|
settleDuration(value) {
|
|
32472
32721
|
if (this.duration !== null) return;
|
|
@@ -32481,10 +32730,40 @@ var ClipStreamFeeder = class {
|
|
|
32481
32730
|
this.stopPacing();
|
|
32482
32731
|
this.stopPositionReports();
|
|
32483
32732
|
this.clearStall();
|
|
32733
|
+
const opening = this.opening;
|
|
32734
|
+
this.opening = null;
|
|
32735
|
+
opening?.abort(/* @__PURE__ */ new Error(CLIP_STREAM_ABANDONED));
|
|
32484
32736
|
const connection = this.connection;
|
|
32485
32737
|
this.connection = null;
|
|
32486
32738
|
connection?.destroy();
|
|
32487
32739
|
}
|
|
32740
|
+
/**
|
|
32741
|
+
* The pacer asked for a frame the stream has not produced yet.
|
|
32742
|
+
*
|
|
32743
|
+
* **A branch that accepts work and produces nothing is the same branch as
|
|
32744
|
+
* one that drops it** (D391), and this is the one that hid Hikvision's whole
|
|
32745
|
+
* acceleration defect: an RTSP replay arrives at 1×, so a pacer set to 2×
|
|
32746
|
+
* or 4× eats the queue faster than the socket fills it, stops, waits, and
|
|
32747
|
+
* says nothing at all. The picture froze, the operator reported *"pare non
|
|
32748
|
+
* funzionare"*, and the only line anywhere was the provider's `peer-stalled`
|
|
32749
|
+
* ten seconds later — which names the wrong end of the pipe.
|
|
32750
|
+
*
|
|
32751
|
+
* ONE line per run, not one per starve. At rate 1 against a realtime source
|
|
32752
|
+
* the queue legitimately empties between frames, so a line per occurrence is
|
|
32753
|
+
* the storm that buries the line that matters; the count travels on the
|
|
32754
|
+
* run's own end.
|
|
32755
|
+
*/
|
|
32756
|
+
noteStarved() {
|
|
32757
|
+
this.starved += 1;
|
|
32758
|
+
if (this.starved > 1) return;
|
|
32759
|
+
this.log("clip-stream:starved", {
|
|
32760
|
+
rate: this.rate,
|
|
32761
|
+
headMs: this.headMs,
|
|
32762
|
+
arrivedMs: this.arrivedMs,
|
|
32763
|
+
bytesIn: this.bytesIn,
|
|
32764
|
+
pushed: this.pushed
|
|
32765
|
+
});
|
|
32766
|
+
}
|
|
32488
32767
|
log(msg, data) {
|
|
32489
32768
|
this.deps.onLog?.(msg, {
|
|
32490
32769
|
...this.tags,
|
|
@@ -32495,6 +32774,62 @@ var ClipStreamFeeder = class {
|
|
|
32495
32774
|
function describe(err) {
|
|
32496
32775
|
return err instanceof Error ? err.message : String(err);
|
|
32497
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
|
+
}
|
|
32498
32833
|
function headerText(res, name) {
|
|
32499
32834
|
const value = res.headers[name];
|
|
32500
32835
|
if (Array.isArray(value)) return value[0];
|
|
@@ -32502,14 +32837,26 @@ function headerText(res, name) {
|
|
|
32502
32837
|
}
|
|
32503
32838
|
function createHttpClipStreamOpener(options = {}) {
|
|
32504
32839
|
const headersTimeoutMs = options.headersTimeoutMs ?? 3e4;
|
|
32505
|
-
return (url, handlers) => new Promise((resolve) => {
|
|
32840
|
+
return (url, handlers, signal) => new Promise((resolve) => {
|
|
32506
32841
|
let settled = false;
|
|
32842
|
+
let onAbort = null;
|
|
32507
32843
|
const settle = (value) => {
|
|
32508
32844
|
if (settled) return;
|
|
32509
32845
|
settled = true;
|
|
32510
32846
|
clearTimeout(timer);
|
|
32847
|
+
if (onAbort !== null) signal.removeEventListener("abort", onAbort);
|
|
32511
32848
|
resolve(value);
|
|
32512
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
|
+
}
|
|
32513
32860
|
let target;
|
|
32514
32861
|
try {
|
|
32515
32862
|
target = new URL(url);
|
|
@@ -32538,7 +32885,16 @@ function createHttpClipStreamOpener(options = {}) {
|
|
|
32538
32885
|
const timer = setTimeout(() => {
|
|
32539
32886
|
req.destroy(/* @__PURE__ */ new Error(`no status line in ${headersTimeoutMs} ms`));
|
|
32540
32887
|
}, headersTimeoutMs);
|
|
32888
|
+
onAbort = () => {
|
|
32889
|
+
req.destroy(new Error(describeReason(signal.reason)));
|
|
32890
|
+
settle(abandoned());
|
|
32891
|
+
};
|
|
32892
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
32541
32893
|
req.on("error", (err) => {
|
|
32894
|
+
if (signal.aborted) {
|
|
32895
|
+
settle(abandoned());
|
|
32896
|
+
return;
|
|
32897
|
+
}
|
|
32542
32898
|
settle({
|
|
32543
32899
|
kind: "refused",
|
|
32544
32900
|
status: 0,
|
|
@@ -32569,7 +32925,7 @@ function createHttpClipStreamOpener(options = {}) {
|
|
|
32569
32925
|
return;
|
|
32570
32926
|
}
|
|
32571
32927
|
res.on("data", (chunk) => handlers.onData(chunk));
|
|
32572
|
-
res.on("end", () => handlers.onEnd());
|
|
32928
|
+
res.on("end", () => handlers.onEnd(readClipStreamTrailers(res.trailers)));
|
|
32573
32929
|
res.on("error", (err) => handlers.onError(err));
|
|
32574
32930
|
settle({
|
|
32575
32931
|
kind: "open",
|
|
@@ -35904,8 +36260,8 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
35904
36260
|
clearTimeout(timer);
|
|
35905
36261
|
}
|
|
35906
36262
|
},
|
|
35907
|
-
locateRecordedSegment: (args) => this.ctx.api.
|
|
35908
|
-
readRecordedSegmentBytes: async (args) => (await this.ctx.api.
|
|
36263
|
+
locateRecordedSegment: (args) => this.ctx.api.recordingArchive.locateSegment.query(args),
|
|
36264
|
+
readRecordedSegmentBytes: async (args) => (await this.ctx.api.recordingArchive.readSegmentBytes.query(args)).data,
|
|
35909
36265
|
readClipBytes: async (args) => {
|
|
35910
36266
|
const res = await this.ctx.api.videoclips.readClipBytes.query({
|
|
35911
36267
|
deviceId: args.deviceId,
|
|
@@ -35938,7 +36294,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
35938
36294
|
},
|
|
35939
36295
|
readRecordedGopBytes: async (args) => {
|
|
35940
36296
|
try {
|
|
35941
|
-
return await this.ctx.api.
|
|
36297
|
+
return await this.ctx.api.recordingArchive.readGopBytes.query({
|
|
35942
36298
|
deviceId: args.deviceId,
|
|
35943
36299
|
profile: args.profile,
|
|
35944
36300
|
startMs: args.startMs,
|
|
@@ -35953,7 +36309,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
35953
36309
|
});
|
|
35954
36310
|
}
|
|
35955
36311
|
return {
|
|
35956
|
-
data: (await this.ctx.api.
|
|
36312
|
+
data: (await this.ctx.api.recordingArchive.readSegmentBytes.query({
|
|
35957
36313
|
deviceId: args.deviceId,
|
|
35958
36314
|
profile: args.profile,
|
|
35959
36315
|
startMs: args.startMs
|