@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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { $t as
|
|
3
|
-
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-
|
|
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-
|
|
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(
|
|
26645
|
+
rate: number().min(0).max(MAX_PACED_RATE)
|
|
26630
26646
|
}),
|
|
26631
26647
|
object({
|
|
26632
26648
|
t: literal("stepFrame"),
|
|
@@ -26639,14 +26655,40 @@ var ControlMessageSchema = discriminatedUnion("t", [
|
|
|
26639
26655
|
})
|
|
26640
26656
|
]);
|
|
26641
26657
|
/**
|
|
26642
|
-
* Clamp a requested playback rate onto the supported domain:
|
|
26643
|
-
* 0
|
|
26644
|
-
*
|
|
26645
|
-
*
|
|
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(
|
|
26672
|
+
return Math.min(MAX_PACED_RATE, Math.max(MIN_PACED_RATE, rate));
|
|
26673
|
+
}
|
|
26674
|
+
/**
|
|
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.
|
|
26689
|
+
*/
|
|
26690
|
+
function audioPlaysAtRate(rate) {
|
|
26691
|
+
return rate === 1;
|
|
26650
26692
|
}
|
|
26651
26693
|
var ServerMessageSchema = discriminatedUnion("t", [
|
|
26652
26694
|
object({
|
|
@@ -26689,12 +26731,12 @@ var ServerMessageSchema = discriminatedUnion("t", [
|
|
|
26689
26731
|
durationMs: number().positive()
|
|
26690
26732
|
}),
|
|
26691
26733
|
object({
|
|
26692
|
-
t: literal("
|
|
26734
|
+
t: literal("clipIncomplete"),
|
|
26693
26735
|
clipId: string().min(1),
|
|
26694
|
-
|
|
26695
|
-
|
|
26696
|
-
|
|
26697
|
-
|
|
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)
|
|
26698
26740
|
}),
|
|
26699
26741
|
object({
|
|
26700
26742
|
t: literal("clipAudio"),
|
|
@@ -26708,8 +26750,23 @@ var ServerMessageSchema = discriminatedUnion("t", [
|
|
|
26708
26750
|
"no-audio-track",
|
|
26709
26751
|
"unsupported-codec",
|
|
26710
26752
|
"codec-mismatch",
|
|
26711
|
-
"stream-failed"
|
|
26712
|
-
|
|
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
|
+
});
|
|
26713
26770
|
})
|
|
26714
26771
|
]);
|
|
26715
26772
|
function parseControlMessage(raw) {
|
|
@@ -27681,7 +27738,7 @@ var RecordedFeeder = class {
|
|
|
27681
27738
|
/** Audio clock (PCMU 8 kHz / Opus 48 kHz); re-anchored together with video. */
|
|
27682
27739
|
audioClock;
|
|
27683
27740
|
clockHasAdvanced = false;
|
|
27684
|
-
/** Playback rate: 0 (paused) or a continuous factor in [0.25,
|
|
27741
|
+
/** Playback rate: 0 (paused) or a continuous factor in [0.25, 8]. */
|
|
27685
27742
|
rate = 1;
|
|
27686
27743
|
disposed = false;
|
|
27687
27744
|
seekToken = 0;
|
|
@@ -28262,7 +28319,7 @@ var RecordedFeeder = class {
|
|
|
28262
28319
|
}
|
|
28263
28320
|
/**
|
|
28264
28321
|
* Set playback rate. 0 pauses in place (the pacing loop aborts on its next
|
|
28265
|
-
* guard check); any other value is clamped to [0.25,
|
|
28322
|
+
* guard check); any other value is clamped to [0.25, 8].
|
|
28266
28323
|
*
|
|
28267
28324
|
* Rate-change strategy (documented choice): every transition TO a non-zero
|
|
28268
28325
|
* rate — resume from pause AND a playing→playing rate change — re-enters via
|
|
@@ -30127,58 +30184,102 @@ var TimelineSession = class {
|
|
|
30127
30184
|
this.lastPositionMs = null;
|
|
30128
30185
|
const feeder = this.enterRecorded();
|
|
30129
30186
|
await feeder.seek(offsetMs);
|
|
30130
|
-
|
|
30131
|
-
|
|
30187
|
+
await Promise.all([
|
|
30188
|
+
this.reportClipDuration(clipId, feeder),
|
|
30189
|
+
this.reportClipAudio(clipId, feeder),
|
|
30190
|
+
this.reportClipIncomplete(clipId, feeder)
|
|
30191
|
+
]);
|
|
30132
30192
|
}
|
|
30133
30193
|
/**
|
|
30134
|
-
* Tell the viewer
|
|
30194
|
+
* Tell the viewer whether this clip has sound.
|
|
30135
30195
|
*
|
|
30136
|
-
*
|
|
30137
|
-
*
|
|
30138
|
-
*
|
|
30139
|
-
*
|
|
30140
|
-
*
|
|
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.
|
|
30141
30203
|
*
|
|
30142
|
-
*
|
|
30143
|
-
*
|
|
30144
|
-
* be this surface inventing a capability.
|
|
30204
|
+
* The first answer SETTLES; the rate does not, so this also subscribes for
|
|
30205
|
+
* as long as the clip is on the pointer.
|
|
30145
30206
|
*/
|
|
30146
|
-
|
|
30147
|
-
|
|
30148
|
-
|
|
30149
|
-
this.deps.onLog?.("clip playback: transport not reported — this feeder cannot say", {
|
|
30207
|
+
async reportClipAudio(clipId, feeder) {
|
|
30208
|
+
if (feeder.audio === void 0) {
|
|
30209
|
+
this.deps.onLog?.("clip playback: audio not reported — this feeder cannot say", {
|
|
30150
30210
|
deviceId: this.deps.deviceId,
|
|
30151
30211
|
clipId
|
|
30152
30212
|
});
|
|
30153
30213
|
return;
|
|
30154
30214
|
}
|
|
30155
|
-
|
|
30156
|
-
|
|
30215
|
+
let report;
|
|
30216
|
+
try {
|
|
30217
|
+
report = await feeder.audio();
|
|
30218
|
+
} catch (err) {
|
|
30219
|
+
this.deps.onLog?.("clip playback: audio not reported — the answer failed", {
|
|
30220
|
+
deviceId: this.deps.deviceId,
|
|
30221
|
+
clipId,
|
|
30222
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30223
|
+
});
|
|
30224
|
+
return;
|
|
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) {
|
|
30239
|
+
if (this.clip?.clipId !== clipId) {
|
|
30240
|
+
this.deps.onLog?.("clip playback: audio dropped — the pointer has left this clip", {
|
|
30241
|
+
deviceId: this.deps.deviceId,
|
|
30242
|
+
clipId,
|
|
30243
|
+
carried: report.carried,
|
|
30244
|
+
reason: report.reason
|
|
30245
|
+
});
|
|
30246
|
+
return;
|
|
30247
|
+
}
|
|
30248
|
+
this.deps.onLog?.("clip playback: audio reported", {
|
|
30157
30249
|
deviceId: this.deps.deviceId,
|
|
30158
30250
|
clipId,
|
|
30159
|
-
|
|
30160
|
-
|
|
30251
|
+
carried: report.carried,
|
|
30252
|
+
codec: report.codec,
|
|
30253
|
+
reason: report.reason,
|
|
30254
|
+
rate: report.rate,
|
|
30255
|
+
detail: report.detail
|
|
30161
30256
|
});
|
|
30162
30257
|
this.deps.session.sendControl(serializeServerMessage({
|
|
30163
|
-
t: "
|
|
30258
|
+
t: "clipAudio",
|
|
30164
30259
|
clipId,
|
|
30165
|
-
|
|
30166
|
-
|
|
30260
|
+
carried: report.carried,
|
|
30261
|
+
...report.codec !== null ? { codec: report.codec } : {},
|
|
30262
|
+
...report.reason !== null ? { reason: report.reason } : {},
|
|
30263
|
+
...report.reason === "rate" && report.rate !== void 0 ? { rate: report.rate } : {}
|
|
30167
30264
|
}));
|
|
30168
30265
|
}
|
|
30169
30266
|
/**
|
|
30170
|
-
* Tell the
|
|
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.
|
|
30171
30269
|
*
|
|
30172
|
-
*
|
|
30173
|
-
*
|
|
30174
|
-
*
|
|
30175
|
-
*
|
|
30176
|
-
*
|
|
30177
|
-
*
|
|
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.
|
|
30178
30279
|
*/
|
|
30179
|
-
async
|
|
30180
|
-
if (feeder.
|
|
30181
|
-
this.deps.onLog?.("clip playback:
|
|
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", {
|
|
30182
30283
|
deviceId: this.deps.deviceId,
|
|
30183
30284
|
clipId
|
|
30184
30285
|
});
|
|
@@ -30186,37 +30287,35 @@ var TimelineSession = class {
|
|
|
30186
30287
|
}
|
|
30187
30288
|
let report;
|
|
30188
30289
|
try {
|
|
30189
|
-
report = await feeder.
|
|
30290
|
+
report = await feeder.incomplete();
|
|
30190
30291
|
} catch (err) {
|
|
30191
|
-
this.deps.onLog?.("clip playback:
|
|
30292
|
+
this.deps.onLog?.("clip playback: completeness not reported — the answer failed", {
|
|
30192
30293
|
deviceId: this.deps.deviceId,
|
|
30193
30294
|
clipId,
|
|
30194
30295
|
error: err instanceof Error ? err.message : String(err)
|
|
30195
30296
|
});
|
|
30196
30297
|
return;
|
|
30197
30298
|
}
|
|
30299
|
+
if (report === null) return;
|
|
30198
30300
|
if (this.clip?.clipId !== clipId) {
|
|
30199
|
-
this.deps.onLog?.("clip playback:
|
|
30301
|
+
this.deps.onLog?.("clip playback: completeness dropped — the pointer has left this clip", {
|
|
30200
30302
|
deviceId: this.deps.deviceId,
|
|
30201
30303
|
clipId,
|
|
30202
|
-
|
|
30304
|
+
code: report.code
|
|
30203
30305
|
});
|
|
30204
30306
|
return;
|
|
30205
30307
|
}
|
|
30206
|
-
this.deps.onLog?.("clip playback:
|
|
30308
|
+
this.deps.onLog?.("clip playback: the clip did not arrive whole", {
|
|
30207
30309
|
deviceId: this.deps.deviceId,
|
|
30208
30310
|
clipId,
|
|
30209
|
-
|
|
30210
|
-
|
|
30211
|
-
reason: report.reason,
|
|
30212
|
-
detail: report.detail
|
|
30311
|
+
code: report.code,
|
|
30312
|
+
reason: report.reason
|
|
30213
30313
|
});
|
|
30214
30314
|
this.deps.session.sendControl(serializeServerMessage({
|
|
30215
|
-
t: "
|
|
30315
|
+
t: "clipIncomplete",
|
|
30216
30316
|
clipId,
|
|
30217
|
-
|
|
30218
|
-
|
|
30219
|
-
...report.reason !== null ? { reason: report.reason } : {}
|
|
30317
|
+
code: report.code,
|
|
30318
|
+
reason: report.reason
|
|
30220
30319
|
}));
|
|
30221
30320
|
}
|
|
30222
30321
|
/**
|
|
@@ -30803,7 +30902,7 @@ function createRefusedClipFeeder(deps) {
|
|
|
30803
30902
|
};
|
|
30804
30903
|
return {
|
|
30805
30904
|
seek: async () => refuse("seek"),
|
|
30806
|
-
setRate: () =>
|
|
30905
|
+
setRate: () => refuse("setRate"),
|
|
30807
30906
|
stepFrame: () => refuse("stepFrame"),
|
|
30808
30907
|
beginScrub: () => refuse("beginScrub"),
|
|
30809
30908
|
scrubTo: () => {},
|
|
@@ -31618,7 +31717,7 @@ function createFmp4StreamDemuxer(options = {}) {
|
|
|
31618
31717
|
* faster than the camera sends.
|
|
31619
31718
|
* - `stepFrame(-1)` and the scrub verbs are refused by name; `stepFrame(+1)`
|
|
31620
31719
|
* pushes the next buffered unit.
|
|
31621
|
-
* - `setRate` is the PLAYER's rate over frames already here ({0} ∪ [0.25,
|
|
31720
|
+
* - `setRate` is the PLAYER's rate over frames already here ({0} ∪ [0.25, 8],
|
|
31622
31721
|
* the recorded domain). The camera's own `playSpeed` (8, 16 — real, the app
|
|
31623
31722
|
* reopens the replay at 8) is a different mechanism and is NOT wired: it
|
|
31624
31723
|
* would go on the dial (`videoclips.dialClipStream` → the cmd-5 replay's
|
|
@@ -31684,6 +31783,11 @@ function createFmp4StreamDemuxer(options = {}) {
|
|
|
31684
31783
|
* `data`, which the broker's `onLog` puts in `tags`.
|
|
31685
31784
|
*/
|
|
31686
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
|
+
/**
|
|
31687
31791
|
* The same ceiling as the file path (`CLIP_SOURCE_MAX_BYTES`): a clip the
|
|
31688
31792
|
* file path would refuse `too-large` is one this path stops taking. Frames
|
|
31689
31793
|
* behind the head are released as they are pushed, so the backlog is the
|
|
@@ -31704,18 +31808,10 @@ function createClipStreamFeeder(deps) {
|
|
|
31704
31808
|
}
|
|
31705
31809
|
var ClipStreamFeeder = class {
|
|
31706
31810
|
deps;
|
|
31707
|
-
/**
|
|
31708
|
-
* Forward-only, and no backward frame-step: the bytes behind the head were
|
|
31709
|
-
* never kept, and the camera offers no replay-from-offset. Stated here so
|
|
31710
|
-
* the viewer can DISABLE those verbs rather than offer them and have every
|
|
31711
|
-
* use refused — the whole content of this report.
|
|
31712
|
-
*/
|
|
31713
|
-
transport = {
|
|
31714
|
-
seek: "forward",
|
|
31715
|
-
stepBack: false
|
|
31716
|
-
};
|
|
31717
31811
|
phase = "idle";
|
|
31718
31812
|
connection = null;
|
|
31813
|
+
/** Armed for the length of one `open()`; aborted by {@link teardown}. */
|
|
31814
|
+
opening = null;
|
|
31719
31815
|
demuxer;
|
|
31720
31816
|
clock = new MonotonicClock();
|
|
31721
31817
|
/** The RTP rate the audio sender negotiated — a CONVERSION, not a clock. */
|
|
@@ -31728,10 +31824,28 @@ var ClipStreamFeeder = class {
|
|
|
31728
31824
|
audioPushed = 0;
|
|
31729
31825
|
audioDroppedLate = 0;
|
|
31730
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;
|
|
31731
31840
|
audioDroppedSeek = 0;
|
|
31732
31841
|
firstAudioLogged = false;
|
|
31733
31842
|
audioReport = null;
|
|
31734
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 = [];
|
|
31735
31849
|
clockHasAdvanced = false;
|
|
31736
31850
|
rate = 1;
|
|
31737
31851
|
queue = [];
|
|
@@ -31754,6 +31868,8 @@ var ClipStreamFeeder = class {
|
|
|
31754
31868
|
lastAnnounced = null;
|
|
31755
31869
|
scrubRefusedFor = void 0;
|
|
31756
31870
|
pushed = 0;
|
|
31871
|
+
/** How many times the pacer wanted a frame the stream had not produced. */
|
|
31872
|
+
starved = 0;
|
|
31757
31873
|
dropped = 0;
|
|
31758
31874
|
bytesIn = 0;
|
|
31759
31875
|
dialedAt = null;
|
|
@@ -31761,12 +31877,16 @@ var ClipStreamFeeder = class {
|
|
|
31761
31877
|
firstFrameLogged = false;
|
|
31762
31878
|
maxBufferedBytes;
|
|
31763
31879
|
stallTimeoutMs;
|
|
31880
|
+
prerollMs;
|
|
31881
|
+
/** True while the pacer is waiting for the cushion to refill after a starve. */
|
|
31882
|
+
rebuffering = false;
|
|
31764
31883
|
now;
|
|
31765
31884
|
tags;
|
|
31766
31885
|
constructor(deps) {
|
|
31767
31886
|
this.deps = deps;
|
|
31768
31887
|
this.maxBufferedBytes = deps.maxBufferedBytes ?? CLIP_STREAM_BUFFER_MAX_BYTES;
|
|
31769
31888
|
this.stallTimeoutMs = deps.stallTimeoutMs ?? 15e3;
|
|
31889
|
+
this.prerollMs = deps.prerollMs ?? 600;
|
|
31770
31890
|
this.now = deps.now ?? (() => Date.now());
|
|
31771
31891
|
this.tags = {
|
|
31772
31892
|
deviceId: deps.handle.deviceId,
|
|
@@ -31849,6 +31969,11 @@ var ClipStreamFeeder = class {
|
|
|
31849
31969
|
}
|
|
31850
31970
|
setRate(rate) {
|
|
31851
31971
|
const next = clampPlaybackRate(rate);
|
|
31972
|
+
if (next !== rate) this.log("clip-stream:rate-not-delivered", {
|
|
31973
|
+
requested: rate,
|
|
31974
|
+
applied: next,
|
|
31975
|
+
deliverable: BROKER_PACED_RATES
|
|
31976
|
+
});
|
|
31852
31977
|
if (next === 0) {
|
|
31853
31978
|
this.rate = 0;
|
|
31854
31979
|
this.stopPacing();
|
|
@@ -31859,6 +31984,8 @@ var ClipStreamFeeder = class {
|
|
|
31859
31984
|
this.clock.setRate(next);
|
|
31860
31985
|
const wasPaused = this.rate === 0;
|
|
31861
31986
|
this.rate = next;
|
|
31987
|
+
this.audioRate = next;
|
|
31988
|
+
this.refreshAudioReport();
|
|
31862
31989
|
if (this.phase !== "streaming" || this.pendingSeekMs !== null) return;
|
|
31863
31990
|
if (wasPaused) this.emitState("playing", "set-rate-resume", this.seekToken);
|
|
31864
31991
|
this.startPacing();
|
|
@@ -31893,6 +32020,19 @@ var ClipStreamFeeder = class {
|
|
|
31893
32020
|
return new Promise((resolve) => this.durationWaiters.push(resolve));
|
|
31894
32021
|
}
|
|
31895
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
|
+
/**
|
|
31896
32036
|
* What this clip turned out to carry as sound — the answer the viewer needs
|
|
31897
32037
|
* to say "this clip has no audio" rather than leave the operator wondering.
|
|
31898
32038
|
*
|
|
@@ -31902,15 +32042,31 @@ var ClipStreamFeeder = class {
|
|
|
31902
32042
|
*/
|
|
31903
32043
|
audio() {
|
|
31904
32044
|
const settled = this.audioReport;
|
|
31905
|
-
if (settled !== null) return Promise.resolve(settled);
|
|
32045
|
+
if (settled !== null) return Promise.resolve(this.audioNow(settled));
|
|
31906
32046
|
return new Promise((resolve) => this.audioWaiters.push(resolve));
|
|
31907
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
|
+
}
|
|
31908
32063
|
dispose() {
|
|
31909
32064
|
if (this.phase === "disposed") return;
|
|
31910
32065
|
const before = this.phase;
|
|
31911
32066
|
this.phase = "disposed";
|
|
31912
32067
|
this.teardown();
|
|
31913
32068
|
this.settleDuration(null);
|
|
32069
|
+
this.settleIncomplete(null);
|
|
31914
32070
|
this.settleAudio({
|
|
31915
32071
|
carried: false,
|
|
31916
32072
|
codec: null,
|
|
@@ -31981,13 +32137,15 @@ var ClipStreamFeeder = class {
|
|
|
31981
32137
|
audioReason: answer.audioReason ?? null
|
|
31982
32138
|
});
|
|
31983
32139
|
}
|
|
32140
|
+
const abandon = new AbortController();
|
|
32141
|
+
this.opening = abandon;
|
|
31984
32142
|
let opened;
|
|
31985
32143
|
try {
|
|
31986
32144
|
opened = await this.deps.open(answer.url, {
|
|
31987
32145
|
onData: (chunk) => this.onData(chunk),
|
|
31988
|
-
onEnd: () => this.onEnd(),
|
|
32146
|
+
onEnd: (report) => this.onEnd(report),
|
|
31989
32147
|
onError: (err) => this.cut("stream-failed", err.message)
|
|
31990
|
-
});
|
|
32148
|
+
}, abandon.signal);
|
|
31991
32149
|
} catch (err) {
|
|
31992
32150
|
opened = {
|
|
31993
32151
|
kind: "refused",
|
|
@@ -31996,8 +32154,15 @@ var ClipStreamFeeder = class {
|
|
|
31996
32154
|
detail: describe(err)
|
|
31997
32155
|
};
|
|
31998
32156
|
}
|
|
32157
|
+
if (this.opening === abandon) this.opening = null;
|
|
31999
32158
|
if (this.phase !== "opening") {
|
|
32000
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
|
+
});
|
|
32001
32166
|
return;
|
|
32002
32167
|
}
|
|
32003
32168
|
if (opened.kind === "refused") {
|
|
@@ -32017,6 +32182,7 @@ var ClipStreamFeeder = class {
|
|
|
32017
32182
|
this.phase = "refused";
|
|
32018
32183
|
this.teardown();
|
|
32019
32184
|
this.settleDuration(null);
|
|
32185
|
+
this.settleIncomplete(null);
|
|
32020
32186
|
this.settleAudio({
|
|
32021
32187
|
carried: false,
|
|
32022
32188
|
codec: null,
|
|
@@ -32138,14 +32304,29 @@ var ClipStreamFeeder = class {
|
|
|
32138
32304
|
return;
|
|
32139
32305
|
}
|
|
32140
32306
|
while (this.rate === 0 && this.pendingSteps > 0 && this.pushNext()) this.pendingSteps -= 1;
|
|
32141
|
-
if (this.rate
|
|
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();
|
|
32142
32321
|
}
|
|
32143
|
-
onEnd() {
|
|
32322
|
+
onEnd(report) {
|
|
32144
32323
|
if (!this.takesBytes()) return;
|
|
32324
|
+
this.settleIncomplete(report);
|
|
32145
32325
|
this.streamEnded = true;
|
|
32146
32326
|
this.clearStall();
|
|
32147
32327
|
this.streamEnd = this.demuxer.end();
|
|
32148
32328
|
this.log("clip-stream:ended", {
|
|
32329
|
+
starved: this.starved,
|
|
32149
32330
|
samples: this.streamEnd.samples,
|
|
32150
32331
|
durationMs: this.streamEnd.durationMs,
|
|
32151
32332
|
bytesIn: this.bytesIn,
|
|
@@ -32158,13 +32339,20 @@ var ClipStreamFeeder = class {
|
|
|
32158
32339
|
this.resolvePendingSeek(this.seekToken);
|
|
32159
32340
|
return;
|
|
32160
32341
|
}
|
|
32161
|
-
if (this.rate
|
|
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();
|
|
32162
32349
|
}
|
|
32163
32350
|
cut(reason, detail) {
|
|
32164
32351
|
if (this.phase !== "streaming" && this.phase !== "opening") return;
|
|
32165
32352
|
this.phase = "cut";
|
|
32166
32353
|
this.teardown();
|
|
32167
32354
|
this.settleDuration(null);
|
|
32355
|
+
this.settleIncomplete(null);
|
|
32168
32356
|
this.settleAudio({
|
|
32169
32357
|
carried: false,
|
|
32170
32358
|
codec: null,
|
|
@@ -32298,9 +32486,24 @@ var ClipStreamFeeder = class {
|
|
|
32298
32486
|
this.audioQueue = this.audioQueue.slice(drop);
|
|
32299
32487
|
this.audioDroppedSeek += drop;
|
|
32300
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
|
+
}
|
|
32301
32503
|
startPacing() {
|
|
32302
32504
|
this.stopPacing();
|
|
32303
|
-
|
|
32505
|
+
const shortfall = this.cushionShortfallMs();
|
|
32506
|
+
this.anchorWall = this.now() + shortfall;
|
|
32304
32507
|
this.anchorPts = this.headMs ?? this.queue[0]?.ptsMs ?? 0;
|
|
32305
32508
|
this.startPositionReports();
|
|
32306
32509
|
this.tick();
|
|
@@ -32325,6 +32528,10 @@ var ClipStreamFeeder = class {
|
|
|
32325
32528
|
const audio = this.audioQueue[0];
|
|
32326
32529
|
if (video === void 0 && audio === void 0) {
|
|
32327
32530
|
if (this.streamEnded) this.finish();
|
|
32531
|
+
else {
|
|
32532
|
+
this.rebuffering = true;
|
|
32533
|
+
this.noteStarved();
|
|
32534
|
+
}
|
|
32328
32535
|
return;
|
|
32329
32536
|
}
|
|
32330
32537
|
const takeVideo = video !== void 0 && (audio === void 0 || video.ptsMs <= audio.ptsMs);
|
|
@@ -32360,7 +32567,7 @@ var ClipStreamFeeder = class {
|
|
|
32360
32567
|
if (frame === void 0) return false;
|
|
32361
32568
|
this.audioQueue = this.audioQueue.slice(1);
|
|
32362
32569
|
this.audioQueuedBytes -= frame.data.byteLength;
|
|
32363
|
-
if (this.rate
|
|
32570
|
+
if (!audioPlaysAtRate(this.rate)) {
|
|
32364
32571
|
this.audioDroppedRate += 1;
|
|
32365
32572
|
return true;
|
|
32366
32573
|
}
|
|
@@ -32458,11 +32665,53 @@ var ClipStreamFeeder = class {
|
|
|
32458
32665
|
audioQueued: this.audioQueue.length
|
|
32459
32666
|
};
|
|
32460
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
|
+
}
|
|
32461
32680
|
settleAudio(report) {
|
|
32462
32681
|
if (this.audioReport !== null) return;
|
|
32463
32682
|
this.audioReport = report;
|
|
32683
|
+
const now = this.audioNow(report);
|
|
32684
|
+
this.audioEmitted = now;
|
|
32464
32685
|
const waiters = this.audioWaiters.splice(0);
|
|
32465
|
-
for (const resolve of waiters) resolve(
|
|
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);
|
|
32466
32715
|
}
|
|
32467
32716
|
settleDuration(value) {
|
|
32468
32717
|
if (this.duration !== null) return;
|
|
@@ -32477,10 +32726,40 @@ var ClipStreamFeeder = class {
|
|
|
32477
32726
|
this.stopPacing();
|
|
32478
32727
|
this.stopPositionReports();
|
|
32479
32728
|
this.clearStall();
|
|
32729
|
+
const opening = this.opening;
|
|
32730
|
+
this.opening = null;
|
|
32731
|
+
opening?.abort(/* @__PURE__ */ new Error(CLIP_STREAM_ABANDONED));
|
|
32480
32732
|
const connection = this.connection;
|
|
32481
32733
|
this.connection = null;
|
|
32482
32734
|
connection?.destroy();
|
|
32483
32735
|
}
|
|
32736
|
+
/**
|
|
32737
|
+
* The pacer asked for a frame the stream has not produced yet.
|
|
32738
|
+
*
|
|
32739
|
+
* **A branch that accepts work and produces nothing is the same branch as
|
|
32740
|
+
* one that drops it** (D391), and this is the one that hid Hikvision's whole
|
|
32741
|
+
* acceleration defect: an RTSP replay arrives at 1×, so a pacer set to 2×
|
|
32742
|
+
* or 4× eats the queue faster than the socket fills it, stops, waits, and
|
|
32743
|
+
* says nothing at all. The picture froze, the operator reported *"pare non
|
|
32744
|
+
* funzionare"*, and the only line anywhere was the provider's `peer-stalled`
|
|
32745
|
+
* ten seconds later — which names the wrong end of the pipe.
|
|
32746
|
+
*
|
|
32747
|
+
* ONE line per run, not one per starve. At rate 1 against a realtime source
|
|
32748
|
+
* the queue legitimately empties between frames, so a line per occurrence is
|
|
32749
|
+
* the storm that buries the line that matters; the count travels on the
|
|
32750
|
+
* run's own end.
|
|
32751
|
+
*/
|
|
32752
|
+
noteStarved() {
|
|
32753
|
+
this.starved += 1;
|
|
32754
|
+
if (this.starved > 1) return;
|
|
32755
|
+
this.log("clip-stream:starved", {
|
|
32756
|
+
rate: this.rate,
|
|
32757
|
+
headMs: this.headMs,
|
|
32758
|
+
arrivedMs: this.arrivedMs,
|
|
32759
|
+
bytesIn: this.bytesIn,
|
|
32760
|
+
pushed: this.pushed
|
|
32761
|
+
});
|
|
32762
|
+
}
|
|
32484
32763
|
log(msg, data) {
|
|
32485
32764
|
this.deps.onLog?.(msg, {
|
|
32486
32765
|
...this.tags,
|
|
@@ -32491,6 +32770,62 @@ var ClipStreamFeeder = class {
|
|
|
32491
32770
|
function describe(err) {
|
|
32492
32771
|
return err instanceof Error ? err.message : String(err);
|
|
32493
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
|
+
}
|
|
32494
32829
|
function headerText(res, name) {
|
|
32495
32830
|
const value = res.headers[name];
|
|
32496
32831
|
if (Array.isArray(value)) return value[0];
|
|
@@ -32498,14 +32833,26 @@ function headerText(res, name) {
|
|
|
32498
32833
|
}
|
|
32499
32834
|
function createHttpClipStreamOpener(options = {}) {
|
|
32500
32835
|
const headersTimeoutMs = options.headersTimeoutMs ?? 3e4;
|
|
32501
|
-
return (url, handlers) => new Promise((resolve) => {
|
|
32836
|
+
return (url, handlers, signal) => new Promise((resolve) => {
|
|
32502
32837
|
let settled = false;
|
|
32838
|
+
let onAbort = null;
|
|
32503
32839
|
const settle = (value) => {
|
|
32504
32840
|
if (settled) return;
|
|
32505
32841
|
settled = true;
|
|
32506
32842
|
clearTimeout(timer);
|
|
32843
|
+
if (onAbort !== null) signal.removeEventListener("abort", onAbort);
|
|
32507
32844
|
resolve(value);
|
|
32508
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
|
+
}
|
|
32509
32856
|
let target;
|
|
32510
32857
|
try {
|
|
32511
32858
|
target = new URL(url);
|
|
@@ -32534,7 +32881,16 @@ function createHttpClipStreamOpener(options = {}) {
|
|
|
32534
32881
|
const timer = setTimeout(() => {
|
|
32535
32882
|
req.destroy(/* @__PURE__ */ new Error(`no status line in ${headersTimeoutMs} ms`));
|
|
32536
32883
|
}, headersTimeoutMs);
|
|
32884
|
+
onAbort = () => {
|
|
32885
|
+
req.destroy(new Error(describeReason(signal.reason)));
|
|
32886
|
+
settle(abandoned());
|
|
32887
|
+
};
|
|
32888
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
32537
32889
|
req.on("error", (err) => {
|
|
32890
|
+
if (signal.aborted) {
|
|
32891
|
+
settle(abandoned());
|
|
32892
|
+
return;
|
|
32893
|
+
}
|
|
32538
32894
|
settle({
|
|
32539
32895
|
kind: "refused",
|
|
32540
32896
|
status: 0,
|
|
@@ -32565,7 +32921,7 @@ function createHttpClipStreamOpener(options = {}) {
|
|
|
32565
32921
|
return;
|
|
32566
32922
|
}
|
|
32567
32923
|
res.on("data", (chunk) => handlers.onData(chunk));
|
|
32568
|
-
res.on("end", () => handlers.onEnd());
|
|
32924
|
+
res.on("end", () => handlers.onEnd(readClipStreamTrailers(res.trailers)));
|
|
32569
32925
|
res.on("error", (err) => handlers.onError(err));
|
|
32570
32926
|
settle({
|
|
32571
32927
|
kind: "open",
|
|
@@ -35900,8 +36256,8 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
35900
36256
|
clearTimeout(timer);
|
|
35901
36257
|
}
|
|
35902
36258
|
},
|
|
35903
|
-
locateRecordedSegment: (args) => this.ctx.api.
|
|
35904
|
-
readRecordedSegmentBytes: async (args) => (await this.ctx.api.
|
|
36259
|
+
locateRecordedSegment: (args) => this.ctx.api.recordingArchive.locateSegment.query(args),
|
|
36260
|
+
readRecordedSegmentBytes: async (args) => (await this.ctx.api.recordingArchive.readSegmentBytes.query(args)).data,
|
|
35905
36261
|
readClipBytes: async (args) => {
|
|
35906
36262
|
const res = await this.ctx.api.videoclips.readClipBytes.query({
|
|
35907
36263
|
deviceId: args.deviceId,
|
|
@@ -35934,7 +36290,7 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
35934
36290
|
},
|
|
35935
36291
|
readRecordedGopBytes: async (args) => {
|
|
35936
36292
|
try {
|
|
35937
|
-
return await this.ctx.api.
|
|
36293
|
+
return await this.ctx.api.recordingArchive.readGopBytes.query({
|
|
35938
36294
|
deviceId: args.deviceId,
|
|
35939
36295
|
profile: args.profile,
|
|
35940
36296
|
startMs: args.startMs,
|
|
@@ -35949,7 +36305,7 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
35949
36305
|
});
|
|
35950
36306
|
}
|
|
35951
36307
|
return {
|
|
35952
|
-
data: (await this.ctx.api.
|
|
36308
|
+
data: (await this.ctx.api.recordingArchive.readSegmentBytes.query({
|
|
35953
36309
|
deviceId: args.deviceId,
|
|
35954
36310
|
profile: args.profile,
|
|
35955
36311
|
startMs: args.startMs
|