@camstack/addon-provider-reolink 1.2.61 → 1.2.64
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/addon.js +572 -23
- package/dist/addon.mjs +572 -23
- package/package.json +4 -1
package/dist/addon.mjs
CHANGED
|
@@ -8791,6 +8791,112 @@ var TIMEZONES = [
|
|
|
8791
8791
|
function findTimezone(id) {
|
|
8792
8792
|
return TIMEZONES.find((tz) => tz.id === id);
|
|
8793
8793
|
}
|
|
8794
|
+
/**
|
|
8795
|
+
* Distinct (device, family, variant) counters one instance will hold.
|
|
8796
|
+
*
|
|
8797
|
+
* A large fleet x the handful of families any single addon reports, with
|
|
8798
|
+
* slack. At ~200 B per counter this is a ~100 KB ceiling on a process that
|
|
8799
|
+
* already declares an RSS budget in the gigabytes.
|
|
8800
|
+
*/
|
|
8801
|
+
var MAX_KEYS = 1024;
|
|
8802
|
+
/**
|
|
8803
|
+
* Where reasons past {@link MAX_REASONS_PER_KEY} go.
|
|
8804
|
+
*
|
|
8805
|
+
* They are FOLDED, never dropped: `attempts - succeeded` must always equal the
|
|
8806
|
+
* sum of the reason counts, or the ratio stops adding up.
|
|
8807
|
+
*/
|
|
8808
|
+
var OVERFLOW_REASON = "other";
|
|
8809
|
+
/** `deviceId` + `family` + optional `variant`, flattened into the map key. */
|
|
8810
|
+
function counterKey(deviceId, family, variant) {
|
|
8811
|
+
return variant === void 0 ? `${deviceId}${family}` : `${deviceId}${family}${variant}`;
|
|
8812
|
+
}
|
|
8813
|
+
/**
|
|
8814
|
+
* A bounded set of per-camera, cumulative failure counters.
|
|
8815
|
+
*
|
|
8816
|
+
* One instance per contributing subsystem. `note` is O(1) and allocation-free
|
|
8817
|
+
* on the steady path; `snapshot` reads without mutating anything.
|
|
8818
|
+
*/
|
|
8819
|
+
var FailureCounters = class {
|
|
8820
|
+
maxKeys;
|
|
8821
|
+
maxReasons;
|
|
8822
|
+
counters = /* @__PURE__ */ new Map();
|
|
8823
|
+
refused = 0;
|
|
8824
|
+
constructor(maxKeys = MAX_KEYS, maxReasons = 16) {
|
|
8825
|
+
this.maxKeys = maxKeys;
|
|
8826
|
+
this.maxReasons = maxReasons;
|
|
8827
|
+
}
|
|
8828
|
+
/**
|
|
8829
|
+
* Counters refused because {@link MAX_KEYS} was already held.
|
|
8830
|
+
*
|
|
8831
|
+
* Cumulative for the life of the instance: a bound that bit is a fact about
|
|
8832
|
+
* the deployment, and a surface that hid it would under-report a fleet
|
|
8833
|
+
* precisely when the fleet got large enough to matter.
|
|
8834
|
+
*/
|
|
8835
|
+
get keysRefused() {
|
|
8836
|
+
return this.refused;
|
|
8837
|
+
}
|
|
8838
|
+
/** Counters currently held. */
|
|
8839
|
+
get size() {
|
|
8840
|
+
return this.counters.size;
|
|
8841
|
+
}
|
|
8842
|
+
/**
|
|
8843
|
+
* Fold one observation in.
|
|
8844
|
+
*
|
|
8845
|
+
* A non-positive or non-integer `deviceId` is REFUSED rather than bucketed:
|
|
8846
|
+
* see the module docblock — an entry that cannot name its camera is worse
|
|
8847
|
+
* than no entry.
|
|
8848
|
+
*/
|
|
8849
|
+
note(observation, nowMs) {
|
|
8850
|
+
if (!Number.isInteger(observation.deviceId) || observation.deviceId <= 0) return;
|
|
8851
|
+
const key = counterKey(observation.deviceId, observation.family, observation.variant);
|
|
8852
|
+
let counter = this.counters.get(key);
|
|
8853
|
+
if (counter === void 0) {
|
|
8854
|
+
if (this.counters.size >= this.maxKeys) {
|
|
8855
|
+
this.refused += 1;
|
|
8856
|
+
return;
|
|
8857
|
+
}
|
|
8858
|
+
counter = {
|
|
8859
|
+
deviceId: observation.deviceId,
|
|
8860
|
+
family: observation.family,
|
|
8861
|
+
variant: observation.variant,
|
|
8862
|
+
sinceMs: nowMs,
|
|
8863
|
+
attempts: 0,
|
|
8864
|
+
succeeded: 0,
|
|
8865
|
+
reasons: /* @__PURE__ */ new Map()
|
|
8866
|
+
};
|
|
8867
|
+
this.counters.set(key, counter);
|
|
8868
|
+
}
|
|
8869
|
+
counter.attempts += 1;
|
|
8870
|
+
if (observation.reason === void 0) {
|
|
8871
|
+
counter.succeeded += 1;
|
|
8872
|
+
return;
|
|
8873
|
+
}
|
|
8874
|
+
const reason = counter.reasons.has(observation.reason) || counter.reasons.size < this.maxReasons ? observation.reason : OVERFLOW_REASON;
|
|
8875
|
+
counter.reasons.set(reason, (counter.reasons.get(reason) ?? 0) + 1);
|
|
8876
|
+
}
|
|
8877
|
+
/** Read every counter. Never mutates — see the module docblock. */
|
|
8878
|
+
snapshot(nowMs) {
|
|
8879
|
+
const out = [];
|
|
8880
|
+
for (const counter of this.counters.values()) out.push({
|
|
8881
|
+
deviceId: counter.deviceId,
|
|
8882
|
+
family: counter.family,
|
|
8883
|
+
...counter.variant !== void 0 ? { variant: counter.variant } : {},
|
|
8884
|
+
sinceMs: counter.sinceMs,
|
|
8885
|
+
atMs: nowMs,
|
|
8886
|
+
attempts: counter.attempts,
|
|
8887
|
+
succeeded: counter.succeeded,
|
|
8888
|
+
reasons: [...counter.reasons.entries()].map(([reason, count]) => ({
|
|
8889
|
+
reason,
|
|
8890
|
+
count
|
|
8891
|
+
})).toSorted((a, b) => b.count - a.count)
|
|
8892
|
+
});
|
|
8893
|
+
return out;
|
|
8894
|
+
}
|
|
8895
|
+
/** Drop everything (host disposal). */
|
|
8896
|
+
clear() {
|
|
8897
|
+
this.counters.clear();
|
|
8898
|
+
}
|
|
8899
|
+
};
|
|
8794
8900
|
var MODEL_FORMATS = [
|
|
8795
8901
|
"onnx",
|
|
8796
8902
|
"coreml",
|
|
@@ -14067,7 +14173,26 @@ var FailureContributionSchema = object({
|
|
|
14067
14173
|
/** The loss, partitioned. Sums to `attempts - succeeded`. */
|
|
14068
14174
|
reasons: array(FailureReasonCountSchema).readonly()
|
|
14069
14175
|
});
|
|
14070
|
-
|
|
14176
|
+
var failureContributionCapability = {
|
|
14177
|
+
name: "failure-contribution",
|
|
14178
|
+
scope: "system",
|
|
14179
|
+
mode: "collection",
|
|
14180
|
+
internal: true,
|
|
14181
|
+
methods: {
|
|
14182
|
+
/**
|
|
14183
|
+
* This addon's per-camera failure counters, read live from bounded in-RAM
|
|
14184
|
+
* state it already keeps. Inert: no persistence, no sampling, no timer.
|
|
14185
|
+
*
|
|
14186
|
+
* READING NEVER RESETS. The counters are CUMULATIVE since `sinceMs`, and a
|
|
14187
|
+
* consumer that wants a rate differences two reads. A draining read would
|
|
14188
|
+
* make two operators with the page open each destroy half of the other's
|
|
14189
|
+
* numbers, and `load-contribution` already settled the same question the
|
|
14190
|
+
* same way for `cpuSeconds`.
|
|
14191
|
+
*/
|
|
14192
|
+
list: method(_void(), array(FailureContributionSchema).readonly()) },
|
|
14193
|
+
/** In-process only — enumerated through `addons.listCapabilityProviders`. */
|
|
14194
|
+
mount: { kind: "skip" }
|
|
14195
|
+
};
|
|
14071
14196
|
var LoadContributionSchema = object({
|
|
14072
14197
|
role: _enum([
|
|
14073
14198
|
"decode",
|
|
@@ -14375,6 +14500,50 @@ var NodeProcessSchema = object({
|
|
|
14375
14500
|
/** Wall-clock uptime (seconds). Parsed from `ps etime`. */
|
|
14376
14501
|
uptimeSec: number()
|
|
14377
14502
|
});
|
|
14503
|
+
/**
|
|
14504
|
+
* One retained container-memory reading.
|
|
14505
|
+
*
|
|
14506
|
+
* `atMs` is the timestamp of the PROCESS snapshot taken in the same tick, not
|
|
14507
|
+
* a second clock: that is what makes "processes sum to X, container says Y"
|
|
14508
|
+
* subtractable per point rather than an eyeballed comparison of two series
|
|
14509
|
+
* sampled at different instants.
|
|
14510
|
+
*
|
|
14511
|
+
* A reduced window keeps the sample with the LARGEST `currentBytes` in each
|
|
14512
|
+
* bucket, WHOLE. Taking a per-field maximum would synthesise a row whose parts
|
|
14513
|
+
* never coexisted, and a mean would smear away the peak this exists to find.
|
|
14514
|
+
*/
|
|
14515
|
+
var ContainerMemoryPointSchema = object({
|
|
14516
|
+
/** Which hierarchy answered, so a reading is never ambiguous. */
|
|
14517
|
+
source: _enum(["cgroup-v2", "cgroup-v1"]),
|
|
14518
|
+
/** `memory.current` (v2) / `memory.usage_in_bytes` (v1). Always known. */
|
|
14519
|
+
currentBytes: number(),
|
|
14520
|
+
/** The cgroup's ceiling. `null` = NO LIMIT — never a sentinel, never zero. */
|
|
14521
|
+
limitBytes: number().nullable(),
|
|
14522
|
+
/** Anonymous pages: the closest thing to "what the processes allocated". */
|
|
14523
|
+
anonBytes: number().nullable(),
|
|
14524
|
+
/** Page cache. Charged to the cgroup, owned by no process. */
|
|
14525
|
+
fileBytes: number().nullable(),
|
|
14526
|
+
/**
|
|
14527
|
+
* Shared memory — and the field that explained the largest single surprise.
|
|
14528
|
+
* The i915 driver backs GPU buffers with shmem, so an inference pool or a
|
|
14529
|
+
* hardware-decode session holding DRM objects is charged HERE and appears
|
|
14530
|
+
* nowhere in a `ps` scan.
|
|
14531
|
+
*/
|
|
14532
|
+
shmemBytes: number().nullable(),
|
|
14533
|
+
/** Kernel slab charged to this cgroup. `null` on v1, which never publishes it. */
|
|
14534
|
+
slabBytes: number().nullable(),
|
|
14535
|
+
/**
|
|
14536
|
+
* Shrinkable i915 GEM object bytes, from debugfs.
|
|
14537
|
+
*
|
|
14538
|
+
* **Host-wide across every DRM client, NOT cgroup-scoped.** It is not a
|
|
14539
|
+
* component of `currentBytes` and must not be subtracted from it; it says
|
|
14540
|
+
* what put the shmem there, where `shmemBytes` only says how much.
|
|
14541
|
+
*
|
|
14542
|
+
* `null` wherever debugfs is not mounted — which is inside every camstack
|
|
14543
|
+
* container today — and on any node with no Intel GPU.
|
|
14544
|
+
*/
|
|
14545
|
+
gpuShmemBytes: number().nullable()
|
|
14546
|
+
}).extend({ atMs: number() });
|
|
14378
14547
|
var DumpHeapSnapshotInputSchema = object({
|
|
14379
14548
|
/** The addon whose runner should dump a heap snapshot. */
|
|
14380
14549
|
addonId: string() });
|
|
@@ -14438,6 +14607,21 @@ var NodeLoadSeriesSchema = object({
|
|
|
14438
14607
|
/** One entry per function seen in the window, heaviest-first. */
|
|
14439
14608
|
series: array(LoadFunctionSeriesSchema).readonly(),
|
|
14440
14609
|
/**
|
|
14610
|
+
* The CONTAINER's memory over the same window, oldest-first.
|
|
14611
|
+
*
|
|
14612
|
+
* Sits next to `series` rather than in a method of its own because the whole
|
|
14613
|
+
* question is a subtraction: the per-process rows in `series` sum to one
|
|
14614
|
+
* number and this one is another, and an operator who has to issue two calls
|
|
14615
|
+
* to compare them will compare two different instants. Same reader, same
|
|
14616
|
+
* `sinceMs`, same `bucketMs`, same timestamps.
|
|
14617
|
+
*
|
|
14618
|
+
* **EMPTY means ABSENT, never zero.** A node with no cgroup — a developer
|
|
14619
|
+
* Mac, a bare-metal host, a container with the hierarchy hidden — reports no
|
|
14620
|
+
* points at all. A zero here would be indistinguishable from a healthy
|
|
14621
|
+
* container and is precisely the lie this field exists to avoid.
|
|
14622
|
+
*/
|
|
14623
|
+
containerMemory: array(ContainerMemoryPointSchema).readonly(),
|
|
14624
|
+
/**
|
|
14441
14625
|
* Width of one returned bucket, in ms. Equals the sampling cadence when no
|
|
14442
14626
|
* reduction was needed — so a caller can always say what one point covers
|
|
14443
14627
|
* without having to know whether it was reduced.
|
|
@@ -28324,10 +28508,10 @@ var rebootCapability = {
|
|
|
28324
28508
|
* recording config. NOTE on events (source of truth, R5/C3): this cap carries
|
|
28325
28509
|
* NO event surface — `getPlaybackManifest` returns playlist URLs only. Timeline
|
|
28326
28510
|
* events (motion/object/audio) come from `pipelineAnalytics` (durable SQLite
|
|
28327
|
-
* rows)
|
|
28328
|
-
*
|
|
28329
|
-
*
|
|
28330
|
-
* (`interfaces/recording-config.ts`).
|
|
28511
|
+
* rows) and are the ONLY event surface — the recorder has none. The in-RAM
|
|
28512
|
+
* playback markers it used to build were deleted on 2026-08-29 because nothing
|
|
28513
|
+
* ever read them. Event<->footage joins are by time, padded with the shared
|
|
28514
|
+
* `EVENT_PAD_MS` (`interfaces/recording-config.ts`).
|
|
28331
28515
|
*/
|
|
28332
28516
|
var RecordingStatusSchema = object({
|
|
28333
28517
|
deviceId: number(),
|
|
@@ -229014,6 +229198,90 @@ function buildInitialStatus(config) {
|
|
|
229014
229198
|
};
|
|
229015
229199
|
}
|
|
229016
229200
|
//#endregion
|
|
229201
|
+
//#region src/intercom-failure-report.ts
|
|
229202
|
+
/**
|
|
229203
|
+
* Per-camera talk-back counters, published through `failure-contribution`.
|
|
229204
|
+
*
|
|
229205
|
+
* ## The number that was never divided
|
|
229206
|
+
*
|
|
229207
|
+
* The rate-mismatch drop had ONE warn line and no counter, so "how much
|
|
229208
|
+
* talk-back is this camera losing" was answerable only by grepping Loki and
|
|
229209
|
+
* hand-correlating timestamps — the exact cost `failure-contribution` exists to
|
|
229210
|
+
* remove. And a bare drop count could not have answered it either: 40 drops out
|
|
229211
|
+
* of 40 pushes and 40 out of 40 000 are opposite findings that produce
|
|
229212
|
+
* identical log volume.
|
|
229213
|
+
*
|
|
229214
|
+
* So EVERY `pushTalkAudio` outcome on a live talk session is noted from the one
|
|
229215
|
+
* place that decides it — the accepted ones too. {@link FailureCounters}
|
|
229216
|
+
* carries `attempts` as the denominator and `succeeded` as the numerator, and
|
|
229217
|
+
* the reasons partition the rest. A success counted somewhere else would drift
|
|
229218
|
+
* from the failures and turn the ratio into fiction.
|
|
229219
|
+
*
|
|
229220
|
+
* ## `variant` is the wire codec, and it is honest
|
|
229221
|
+
*
|
|
229222
|
+
* `failure-contribution` keeps `variant` for a second dimension WITHIN a
|
|
229223
|
+
* family, and here the useful one is the format the caller pushed: an operator
|
|
229224
|
+
* asking "why is 617 silent" needs to know whether HomeKit's Opus or Alexa's
|
|
229225
|
+
* raw PCM is the half that is failing. The provider is handed that value on
|
|
229226
|
+
* every call, so it is reported rather than guessed — absent, never invented.
|
|
229227
|
+
*
|
|
229228
|
+
* ## Process-wide, because a counter is
|
|
229229
|
+
*
|
|
229230
|
+
* One addon is one process (D2) and every camera this addon owns lives in it,
|
|
229231
|
+
* so the instance is module-scoped: the cameras note into it and the addon
|
|
229232
|
+
* registers ONE `failure-contribution` provider that reads it. `sinceMs` is the
|
|
229233
|
+
* incarnation marker — a respawned runner restarts from zero and says so.
|
|
229234
|
+
* Reading NEVER drains.
|
|
229235
|
+
*/
|
|
229236
|
+
/** One `pushTalkAudio` call against an open talk session. */
|
|
229237
|
+
var FAMILY_INTERCOM_TALK = "intercom-talk";
|
|
229238
|
+
/** The push arrived with a sequence number at or below the last accepted one. */
|
|
229239
|
+
var REASON_TALK_OUT_OF_ORDER = "out-of-order";
|
|
229240
|
+
/** The payload decoded to zero bytes. */
|
|
229241
|
+
var REASON_TALK_EMPTY = "empty-frame";
|
|
229242
|
+
/** More than one channel — every camera here is mono-only. */
|
|
229243
|
+
var REASON_TALK_NOT_MONO = "not-mono";
|
|
229244
|
+
/** `s16le` push with no `sampleRate`; the rate is ambiguous, not assumed. */
|
|
229245
|
+
var REASON_TALK_NO_SAMPLE_RATE = "missing-sample-rate";
|
|
229246
|
+
/** The wire codec has no path onto this camera's talk channel. */
|
|
229247
|
+
var REASON_TALK_CODEC_UNSUPPORTED = "codec-unsupported";
|
|
229248
|
+
/** The Opus decode path threw or could not open its session. */
|
|
229249
|
+
var REASON_TALK_OPUS_FAILED = "opus-decode-failed";
|
|
229250
|
+
/**
|
|
229251
|
+
* The addon's talk-back counters. One instance per process; the export at the
|
|
229252
|
+
* bottom of this file IS that instance.
|
|
229253
|
+
*/
|
|
229254
|
+
var IntercomFailureReport = class {
|
|
229255
|
+
now;
|
|
229256
|
+
counters;
|
|
229257
|
+
constructor(now = Date.now, counters = new FailureCounters()) {
|
|
229258
|
+
this.now = now;
|
|
229259
|
+
this.counters = counters;
|
|
229260
|
+
}
|
|
229261
|
+
/**
|
|
229262
|
+
* Note one `pushTalkAudio` outcome. `reason` absent = the frame reached the
|
|
229263
|
+
* camera's talk channel.
|
|
229264
|
+
*/
|
|
229265
|
+
noteTalkFrame(deviceId, wireCodec, reason) {
|
|
229266
|
+
this.counters.note({
|
|
229267
|
+
deviceId,
|
|
229268
|
+
family: FAMILY_INTERCOM_TALK,
|
|
229269
|
+
variant: wireCodec,
|
|
229270
|
+
...reason !== void 0 ? { reason } : {}
|
|
229271
|
+
}, this.now());
|
|
229272
|
+
}
|
|
229273
|
+
/** The `failure-contribution` provider's payload. Reads, never resets. */
|
|
229274
|
+
list() {
|
|
229275
|
+
return this.counters.snapshot(this.now());
|
|
229276
|
+
}
|
|
229277
|
+
/** Addon disposal. */
|
|
229278
|
+
clear() {
|
|
229279
|
+
this.counters.clear();
|
|
229280
|
+
}
|
|
229281
|
+
};
|
|
229282
|
+
/** The process-wide instance every camera in this addon notes into. */
|
|
229283
|
+
var intercomFailureReport = new IntercomFailureReport();
|
|
229284
|
+
//#endregion
|
|
229017
229285
|
//#region src/log-channels.ts
|
|
229018
229286
|
/**
|
|
229019
229287
|
* The diagnostic log CHANNELS `provider-reolink` declares.
|
|
@@ -230847,6 +231115,15 @@ function encodeImaAdpcm(pcm, blockSizeBytes) {
|
|
|
230847
231115
|
var DEFAULT_BACKLOG_MS = 120;
|
|
230848
231116
|
var MAX_BACKLOG_MS = 5e3;
|
|
230849
231117
|
var MIN_BACKLOG_MS = 20;
|
|
231118
|
+
/**
|
|
231119
|
+
* The ONE place the operator's backlog request becomes the enforced bound.
|
|
231120
|
+
* `start()` sizes the byte window from it and `ability.maxBacklogMs` reports
|
|
231121
|
+
* it — a second clamp would let the number a caller reads drift from the
|
|
231122
|
+
* number the buffer honours.
|
|
231123
|
+
*/
|
|
231124
|
+
function clampBacklogMs(requested) {
|
|
231125
|
+
return Math.max(MIN_BACKLOG_MS, Math.min(MAX_BACKLOG_MS, requested ?? DEFAULT_BACKLOG_MS));
|
|
231126
|
+
}
|
|
230850
231127
|
var DEFAULT_BLOCKS_PER_PAYLOAD = 1;
|
|
230851
231128
|
var DEFAULT_GAIN = 1;
|
|
230852
231129
|
var MIN_GAIN = .1;
|
|
@@ -230874,6 +231151,36 @@ var ReolinkIntercomSession = class {
|
|
|
230874
231151
|
if (!this.session) throw new Error("ReolinkIntercomSession.sampleRate read before start()");
|
|
230875
231152
|
return this.session.info.audioConfig.sampleRate;
|
|
230876
231153
|
}
|
|
231154
|
+
/**
|
|
231155
|
+
* Effective PCM backlog bound, in ms — the operator's value clamped to
|
|
231156
|
+
* [{@link MIN_BACKLOG_MS}, {@link MAX_BACKLOG_MS}], i.e. what the session
|
|
231157
|
+
* is actually enforcing rather than what it was asked for. Readable before
|
|
231158
|
+
* `start()` because the clamp is pure.
|
|
231159
|
+
*/
|
|
231160
|
+
get backlogMs() {
|
|
231161
|
+
return clampBacklogMs(this.opts.maxBacklogMs);
|
|
231162
|
+
}
|
|
231163
|
+
/**
|
|
231164
|
+
* The firmware's talk-back format — `IntercomStatus.ability`. Throws before
|
|
231165
|
+
* `start()`, like `sampleRate`, because the rate is the camera's answer and
|
|
231166
|
+
* not a default.
|
|
231167
|
+
*
|
|
231168
|
+
* The field was declared, mirrored into runtime state and written by NOBODY
|
|
231169
|
+
* while these values were in hand and only reaching a log line (D281).
|
|
231170
|
+
*
|
|
231171
|
+
* `duplex` is the one judgement call: the Baichuan talk channel is a single
|
|
231172
|
+
* dedicated session and this provider enforces one at a time per camera, so
|
|
231173
|
+
* `half` is reported. `full` would be the dangerous direction — a consumer
|
|
231174
|
+
* that believes it may listen while speaking takes no lock.
|
|
231175
|
+
*/
|
|
231176
|
+
get ability() {
|
|
231177
|
+
return {
|
|
231178
|
+
codecs: ["adpcm-ima"],
|
|
231179
|
+
sampleRate: this.sampleRate,
|
|
231180
|
+
duplex: "half",
|
|
231181
|
+
maxBacklogMs: this.backlogMs
|
|
231182
|
+
};
|
|
231183
|
+
}
|
|
230877
231184
|
async start() {
|
|
230878
231185
|
if (this.session) return;
|
|
230879
231186
|
this.outputGain = clampGain(this.opts.outputGain);
|
|
@@ -230900,7 +231207,7 @@ var ReolinkIntercomSession = class {
|
|
|
230900
231207
|
} catch {}
|
|
230901
231208
|
throw new Error(`Reolink talk session reported invalid sampleRate: ${sampleRate}`);
|
|
230902
231209
|
}
|
|
230903
|
-
const wantedBacklogMs =
|
|
231210
|
+
const wantedBacklogMs = this.backlogMs;
|
|
230904
231211
|
this.maxBacklogBytes = Math.max(this.bytesPerBlock, Math.floor(wantedBacklogMs / 1e3 * sampleRate * 2));
|
|
230905
231212
|
this.session = session;
|
|
230906
231213
|
this.pcmBuffer = Buffer.alloc(0);
|
|
@@ -231028,6 +231335,14 @@ var IntercomOrchestrator = class {
|
|
|
231028
231335
|
return this.session !== null && !this.session.closed;
|
|
231029
231336
|
}
|
|
231030
231337
|
/**
|
|
231338
|
+
* The live talk session's firmware ability, or `null` when no session is
|
|
231339
|
+
* open. Read by the camera at `startSession` so the WebRTC path writes
|
|
231340
|
+
* `IntercomStatus.ability` from the same source the raw-PCM path does.
|
|
231341
|
+
*/
|
|
231342
|
+
get ability() {
|
|
231343
|
+
return this.session === null || this.session.closed ? null : this.session.talkSession.ability;
|
|
231344
|
+
}
|
|
231345
|
+
/**
|
|
231031
231346
|
* Open a fresh WebRTC peer + audio-codec decode session + Reolink
|
|
231032
231347
|
* talk session, wire them, return the SDP offer. Throws (and tears
|
|
231033
231348
|
* down everything it had spun up) on any failure — the cap router
|
|
@@ -231245,6 +231560,193 @@ function errMsg$1(err) {
|
|
|
231245
231560
|
return err instanceof Error ? err.message : String(err);
|
|
231246
231561
|
}
|
|
231247
231562
|
//#endregion
|
|
231563
|
+
//#region src/talk-pcm-transcoder.ts
|
|
231564
|
+
/** libav codec name of a linear little-endian 16-bit PCM decode session. */
|
|
231565
|
+
var TALK_PCM_CODEC = "pcm_s16le";
|
|
231566
|
+
/** The transcoder was already closed — the talk session ended under the push. */
|
|
231567
|
+
var REASON_PCM_CLOSED = "pcm-transcoder-closed";
|
|
231568
|
+
/** The caller's declared source rate is not a usable positive integer. */
|
|
231569
|
+
var REASON_PCM_BAD_RATE = "pcm-bad-source-rate";
|
|
231570
|
+
/** The frame is empty or holds half a sample — malformed, not convertible. */
|
|
231571
|
+
var REASON_PCM_ODD_BYTES = "pcm-odd-bytes";
|
|
231572
|
+
/** No `audio-codec` provider is mounted on this cluster. */
|
|
231573
|
+
var REASON_PCM_NO_CODEC_CAP = "pcm-audio-codec-unavailable";
|
|
231574
|
+
/** The codec cap refused to open a linear-PCM decode session. */
|
|
231575
|
+
var REASON_PCM_SESSION_OPEN_FAILED = "pcm-resample-session-failed";
|
|
231576
|
+
/** The push/pull round-trip through the codec cap threw. */
|
|
231577
|
+
var REASON_PCM_CONVERT_FAILED = "pcm-resample-failed";
|
|
231578
|
+
function errMessage(err) {
|
|
231579
|
+
return err instanceof Error ? err.message : String(err);
|
|
231580
|
+
}
|
|
231581
|
+
var TalkPcmTranscoder = class {
|
|
231582
|
+
opts;
|
|
231583
|
+
active = null;
|
|
231584
|
+
closed = false;
|
|
231585
|
+
constructor(opts) {
|
|
231586
|
+
this.opts = opts;
|
|
231587
|
+
}
|
|
231588
|
+
/** The open codec session, or `null` before the first converted frame. */
|
|
231589
|
+
get sessionId() {
|
|
231590
|
+
return this.active?.sessionId ?? null;
|
|
231591
|
+
}
|
|
231592
|
+
/**
|
|
231593
|
+
* Convert one frame to the camera's rate and hand every produced chunk to
|
|
231594
|
+
* `feed`.
|
|
231595
|
+
*
|
|
231596
|
+
* Returns `null` when the frame was converted and fed, or the REASON string
|
|
231597
|
+
* it was refused for — already logged, with nothing fed.
|
|
231598
|
+
*/
|
|
231599
|
+
async feedResampled(frame) {
|
|
231600
|
+
if (this.closed) {
|
|
231601
|
+
this.refuse(REASON_PCM_CLOSED, {});
|
|
231602
|
+
return REASON_PCM_CLOSED;
|
|
231603
|
+
}
|
|
231604
|
+
const sourceSampleRate = frame.sourceSampleRate;
|
|
231605
|
+
if (!Number.isInteger(sourceSampleRate) || sourceSampleRate <= 0) {
|
|
231606
|
+
this.refuse(REASON_PCM_BAD_RATE, { sourceSampleRate });
|
|
231607
|
+
return REASON_PCM_BAD_RATE;
|
|
231608
|
+
}
|
|
231609
|
+
if (frame.pcm.length === 0 || (frame.pcm.length & 1) !== 0) {
|
|
231610
|
+
this.refuse(REASON_PCM_ODD_BYTES, { bytes: frame.pcm.length });
|
|
231611
|
+
return REASON_PCM_ODD_BYTES;
|
|
231612
|
+
}
|
|
231613
|
+
let api;
|
|
231614
|
+
try {
|
|
231615
|
+
api = this.opts.resolveAudioCodec();
|
|
231616
|
+
} catch (err) {
|
|
231617
|
+
this.refuse(REASON_PCM_NO_CODEC_CAP, { error: errMessage(err) });
|
|
231618
|
+
return REASON_PCM_NO_CODEC_CAP;
|
|
231619
|
+
}
|
|
231620
|
+
if (this.active !== null && this.active.sourceSampleRate !== sourceSampleRate) {
|
|
231621
|
+
const previous = this.active.sourceSampleRate;
|
|
231622
|
+
await this.disposeSession(api, "source-rate-changed");
|
|
231623
|
+
this.opts.logger.info("intercom: pcm resample source rate changed — session recreated", {
|
|
231624
|
+
tags: { deviceId: this.opts.deviceId },
|
|
231625
|
+
meta: {
|
|
231626
|
+
previousSourceSampleRate: previous,
|
|
231627
|
+
sourceSampleRate
|
|
231628
|
+
}
|
|
231629
|
+
});
|
|
231630
|
+
}
|
|
231631
|
+
if (this.active === null) try {
|
|
231632
|
+
const created = await api.createDecodeSession({
|
|
231633
|
+
codec: TALK_PCM_CODEC,
|
|
231634
|
+
sourceSampleRate,
|
|
231635
|
+
sourceChannels: 1,
|
|
231636
|
+
targetSampleRate: this.opts.targetSampleRate,
|
|
231637
|
+
targetChannels: 1,
|
|
231638
|
+
targetFormat: "s16le",
|
|
231639
|
+
tag: this.opts.tag
|
|
231640
|
+
});
|
|
231641
|
+
this.active = {
|
|
231642
|
+
sessionId: created.sessionId,
|
|
231643
|
+
nodeId: created.nodeId,
|
|
231644
|
+
sourceSampleRate
|
|
231645
|
+
};
|
|
231646
|
+
this.opts.logger.info("intercom: pcm resample session opened", {
|
|
231647
|
+
tags: { deviceId: this.opts.deviceId },
|
|
231648
|
+
meta: {
|
|
231649
|
+
codec: TALK_PCM_CODEC,
|
|
231650
|
+
codecSessionId: created.sessionId,
|
|
231651
|
+
codecNodeId: created.nodeId,
|
|
231652
|
+
sourceSampleRate,
|
|
231653
|
+
targetSampleRate: this.opts.targetSampleRate,
|
|
231654
|
+
tag: this.opts.tag
|
|
231655
|
+
}
|
|
231656
|
+
});
|
|
231657
|
+
} catch (err) {
|
|
231658
|
+
this.refuse(REASON_PCM_SESSION_OPEN_FAILED, {
|
|
231659
|
+
sourceSampleRate,
|
|
231660
|
+
targetSampleRate: this.opts.targetSampleRate,
|
|
231661
|
+
error: errMessage(err)
|
|
231662
|
+
});
|
|
231663
|
+
return REASON_PCM_SESSION_OPEN_FAILED;
|
|
231664
|
+
}
|
|
231665
|
+
const session = this.active;
|
|
231666
|
+
try {
|
|
231667
|
+
await api.pushEncodedFrame({
|
|
231668
|
+
sessionId: session.sessionId,
|
|
231669
|
+
nodeId: session.nodeId,
|
|
231670
|
+
data: new Uint8Array(frame.pcm.buffer, frame.pcm.byteOffset, frame.pcm.byteLength)
|
|
231671
|
+
});
|
|
231672
|
+
const chunks = await api.pullPcm({
|
|
231673
|
+
sessionId: session.sessionId,
|
|
231674
|
+
nodeId: session.nodeId,
|
|
231675
|
+
maxCount: 8
|
|
231676
|
+
});
|
|
231677
|
+
for (const chunk of chunks) {
|
|
231678
|
+
const out = Buffer.from(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength);
|
|
231679
|
+
if (out.length > 0) this.opts.feed(out);
|
|
231680
|
+
}
|
|
231681
|
+
return null;
|
|
231682
|
+
} catch (err) {
|
|
231683
|
+
this.refuse(REASON_PCM_CONVERT_FAILED, {
|
|
231684
|
+
codecSessionId: session.sessionId,
|
|
231685
|
+
sourceSampleRate,
|
|
231686
|
+
targetSampleRate: this.opts.targetSampleRate,
|
|
231687
|
+
error: errMessage(err)
|
|
231688
|
+
});
|
|
231689
|
+
await this.disposeSession(api, "convert-failed");
|
|
231690
|
+
return REASON_PCM_CONVERT_FAILED;
|
|
231691
|
+
}
|
|
231692
|
+
}
|
|
231693
|
+
/**
|
|
231694
|
+
* Close the codec session. Idempotent, and called from the provider's
|
|
231695
|
+
* `endTalkSession` so the session dies with the talk session it served.
|
|
231696
|
+
*/
|
|
231697
|
+
async close() {
|
|
231698
|
+
this.closed = true;
|
|
231699
|
+
if (this.active === null) return;
|
|
231700
|
+
let api;
|
|
231701
|
+
try {
|
|
231702
|
+
api = this.opts.resolveAudioCodec();
|
|
231703
|
+
} catch (err) {
|
|
231704
|
+
this.opts.logger.debug("intercom: pcm resample close skipped — audio-codec gone", {
|
|
231705
|
+
tags: { deviceId: this.opts.deviceId },
|
|
231706
|
+
meta: {
|
|
231707
|
+
codecSessionId: this.active.sessionId,
|
|
231708
|
+
error: errMessage(err)
|
|
231709
|
+
}
|
|
231710
|
+
});
|
|
231711
|
+
this.active = null;
|
|
231712
|
+
return;
|
|
231713
|
+
}
|
|
231714
|
+
await this.disposeSession(api, "talk-session-ended");
|
|
231715
|
+
}
|
|
231716
|
+
/** Close + forget the current session. Never throws. */
|
|
231717
|
+
async disposeSession(api, why) {
|
|
231718
|
+
const session = this.active;
|
|
231719
|
+
this.active = null;
|
|
231720
|
+
if (session === null) return;
|
|
231721
|
+
try {
|
|
231722
|
+
await api.closeSession({
|
|
231723
|
+
sessionId: session.sessionId,
|
|
231724
|
+
nodeId: session.nodeId
|
|
231725
|
+
});
|
|
231726
|
+
} catch (err) {
|
|
231727
|
+
this.opts.logger.debug("intercom: pcm resample closeSession error (continuing)", {
|
|
231728
|
+
tags: { deviceId: this.opts.deviceId },
|
|
231729
|
+
meta: {
|
|
231730
|
+
codecSessionId: session.sessionId,
|
|
231731
|
+
why,
|
|
231732
|
+
error: errMessage(err)
|
|
231733
|
+
}
|
|
231734
|
+
});
|
|
231735
|
+
}
|
|
231736
|
+
}
|
|
231737
|
+
/** One warn per refused frame. A branch that drops work says so. */
|
|
231738
|
+
refuse(reason, meta) {
|
|
231739
|
+
this.opts.logger.warn("intercom: pcm talk frame refused — not converted, nothing fed", {
|
|
231740
|
+
tags: { deviceId: this.opts.deviceId },
|
|
231741
|
+
meta: {
|
|
231742
|
+
reason,
|
|
231743
|
+
targetSampleRate: this.opts.targetSampleRate,
|
|
231744
|
+
...meta
|
|
231745
|
+
}
|
|
231746
|
+
});
|
|
231747
|
+
}
|
|
231748
|
+
};
|
|
231749
|
+
//#endregion
|
|
231248
231750
|
//#region src/intercom-webrtc-peer.ts
|
|
231249
231751
|
var _werift;
|
|
231250
231752
|
/**
|
|
@@ -234514,13 +235016,20 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
234514
235016
|
* Called at the four points that open or close a session — and seeded at
|
|
234515
235017
|
* registration, so the slice says `talking: false` from boot rather than
|
|
234516
235018
|
* only after the first session.
|
|
235019
|
+
*
|
|
235020
|
+
* `ability` is STICKY: it is the firmware's negotiated format, learned when a
|
|
235021
|
+
* session opens and still true after it closes, so a caller reading between
|
|
235022
|
+
* sessions gets the last probed value rather than `null`. Passing it is what
|
|
235023
|
+
* changed — it used to be copied forward from `previous` at every one of the
|
|
235024
|
+
* four call sites and written by nobody, while `session.sampleRate` was in
|
|
235025
|
+
* hand and only reaching a log line (D281).
|
|
234517
235026
|
*/
|
|
234518
|
-
publishIntercomState(talking) {
|
|
235027
|
+
publishIntercomState(talking, ability) {
|
|
234519
235028
|
const previous = this.getCapSlice(intercomCapability);
|
|
234520
235029
|
this.setCapSlice(intercomCapability, {
|
|
234521
235030
|
talking,
|
|
234522
235031
|
lastSessionAt: talking ? Date.now() : previous?.lastSessionAt ?? null,
|
|
234523
|
-
ability: previous?.ability ?? null
|
|
235032
|
+
ability: ability ?? previous?.ability ?? null
|
|
234524
235033
|
});
|
|
234525
235034
|
}
|
|
234526
235035
|
registerIntercomIfSupported() {
|
|
@@ -234559,7 +235068,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
234559
235068
|
});
|
|
234560
235069
|
try {
|
|
234561
235070
|
const opened = await this.intercomOrchestrator.start();
|
|
234562
|
-
this.publishIntercomState(true);
|
|
235071
|
+
this.publishIntercomState(true, this.intercomOrchestrator.ability ?? void 0);
|
|
234563
235072
|
return opened;
|
|
234564
235073
|
} catch (err) {
|
|
234565
235074
|
this.publishIntercomState(false);
|
|
@@ -234581,8 +235090,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
234581
235090
|
if (deviceId !== this.id) throw new Error(`ReolinkCamera: intercom deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
234582
235091
|
if (this.disabled) throw new Error("Reolink intercom: device is disabled — re-enable it before opening a talk session");
|
|
234583
235092
|
if (this.intercomRawSession) {
|
|
234584
|
-
|
|
235093
|
+
const previous = this.intercomRawSession;
|
|
234585
235094
|
this.intercomRawSession = null;
|
|
235095
|
+
await previous.pcmTranscode.close();
|
|
235096
|
+
await previous.session.stop().catch(() => {});
|
|
234586
235097
|
}
|
|
234587
235098
|
const api = await this.ensureApi();
|
|
234588
235099
|
if (this.isBattery) await this.wakeForIntercom(api);
|
|
@@ -234604,9 +235115,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
234604
235115
|
id,
|
|
234605
235116
|
session,
|
|
234606
235117
|
lastSequenceNumber: -1,
|
|
234607
|
-
opusDecode: null
|
|
235118
|
+
opusDecode: null,
|
|
235119
|
+
pcmTranscode: new TalkPcmTranscoder({
|
|
235120
|
+
deviceId: this.id,
|
|
235121
|
+
logger: this.ctx.logger,
|
|
235122
|
+
resolveAudioCodec: () => this.resolveAudioCodecApi(),
|
|
235123
|
+
targetSampleRate: session.sampleRate,
|
|
235124
|
+
tag: `reolink-intercom-pcm:${this.id}:${id}`,
|
|
235125
|
+
feed: (pcm) => session.feedPcm(pcm)
|
|
235126
|
+
})
|
|
234608
235127
|
};
|
|
234609
|
-
this.publishIntercomState(true);
|
|
235128
|
+
this.publishIntercomState(true, session.ability);
|
|
234610
235129
|
this.ctx.logger.info("intercom talk session opened", {
|
|
234611
235130
|
tags: { deviceId: this.id },
|
|
234612
235131
|
meta: {
|
|
@@ -234618,13 +235137,24 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
234618
235137
|
},
|
|
234619
235138
|
pushTalkAudio: async ({ deviceId, audioBase64, codec, sampleRate, channels, sequenceNumber }) => {
|
|
234620
235139
|
if (deviceId !== this.id) return { accepted: false };
|
|
235140
|
+
const wireCodec = codec ?? "s16le";
|
|
235141
|
+
const note = (reason) => {
|
|
235142
|
+
intercomFailureReport.noteTalkFrame(this.id, wireCodec, reason);
|
|
235143
|
+
};
|
|
234621
235144
|
const active = this.intercomRawSession;
|
|
234622
235145
|
if (!active || !active.session.isOpen) return { accepted: false };
|
|
234623
|
-
if (sequenceNumber <= active.lastSequenceNumber)
|
|
235146
|
+
if (sequenceNumber <= active.lastSequenceNumber) {
|
|
235147
|
+
note(REASON_TALK_OUT_OF_ORDER);
|
|
235148
|
+
return { accepted: false };
|
|
235149
|
+
}
|
|
234624
235150
|
const buf = Buffer.from(audioBase64, "base64");
|
|
234625
|
-
if (buf.length === 0)
|
|
235151
|
+
if (buf.length === 0) {
|
|
235152
|
+
note(REASON_TALK_EMPTY);
|
|
235153
|
+
return { accepted: false };
|
|
235154
|
+
}
|
|
234626
235155
|
const ch = channels ?? 1;
|
|
234627
235156
|
if (ch !== 1) {
|
|
235157
|
+
note(REASON_TALK_NOT_MONO);
|
|
234628
235158
|
this.ctx.logger.warn("intercom: dropping non-mono talk frame (Reolink is mono-only)", {
|
|
234629
235159
|
tags: { deviceId: this.id },
|
|
234630
235160
|
meta: {
|
|
@@ -234634,8 +235164,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
234634
235164
|
});
|
|
234635
235165
|
return { accepted: false };
|
|
234636
235166
|
}
|
|
234637
|
-
const wireCodec = codec ?? "s16le";
|
|
234638
235167
|
if (wireCodec === "g711ulaw" || wireCodec === "g711alaw") {
|
|
235168
|
+
note(REASON_TALK_CODEC_UNSUPPORTED);
|
|
234639
235169
|
this.ctx.logger.warn("intercom: g711 passthrough not supported on Reolink (camera codec is ADPCM) — dropping frame", {
|
|
234640
235170
|
tags: { deviceId: this.id },
|
|
234641
235171
|
meta: { wireCodec }
|
|
@@ -234644,24 +235174,29 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
234644
235174
|
}
|
|
234645
235175
|
if (wireCodec === "s16le") {
|
|
234646
235176
|
if (!sampleRate) {
|
|
235177
|
+
note(REASON_TALK_NO_SAMPLE_RATE);
|
|
234647
235178
|
this.ctx.logger.warn("intercom: s16le push with no sampleRate — dropping (rate is ambiguous)", { tags: { deviceId: this.id } });
|
|
234648
235179
|
return { accepted: false };
|
|
234649
235180
|
}
|
|
234650
235181
|
if (sampleRate !== active.session.sampleRate) {
|
|
234651
|
-
|
|
234652
|
-
|
|
234653
|
-
|
|
234654
|
-
wireRate: sampleRate,
|
|
234655
|
-
cameraRate: active.session.sampleRate
|
|
234656
|
-
}
|
|
235182
|
+
const refusal = await active.pcmTranscode.feedResampled({
|
|
235183
|
+
pcm: buf,
|
|
235184
|
+
sourceSampleRate: sampleRate
|
|
234657
235185
|
});
|
|
234658
|
-
|
|
235186
|
+
if (refusal !== null) {
|
|
235187
|
+
note(refusal);
|
|
235188
|
+
return { accepted: false };
|
|
235189
|
+
}
|
|
235190
|
+
active.lastSequenceNumber = sequenceNumber;
|
|
235191
|
+
note();
|
|
235192
|
+
return { accepted: true };
|
|
234659
235193
|
}
|
|
234660
235194
|
active.lastSequenceNumber = sequenceNumber;
|
|
234661
235195
|
active.session.feedPcm(buf);
|
|
235196
|
+
note();
|
|
234662
235197
|
return { accepted: true };
|
|
234663
235198
|
}
|
|
234664
|
-
if (wireCodec === "opus") {
|
|
235199
|
+
if (wireCodec === "opus") try {
|
|
234665
235200
|
if (!active.opusDecode) {
|
|
234666
235201
|
const created = await this.resolveAudioCodecApi().createDecodeSession({
|
|
234667
235202
|
codec: "opus",
|
|
@@ -234704,8 +235239,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
234704
235239
|
const pcmBuf = Buffer.from(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength);
|
|
234705
235240
|
if (pcmBuf.length > 0) active.session.feedPcm(pcmBuf);
|
|
234706
235241
|
}
|
|
235242
|
+
note();
|
|
234707
235243
|
return { accepted: true };
|
|
235244
|
+
} catch (err) {
|
|
235245
|
+
note(REASON_TALK_OPUS_FAILED);
|
|
235246
|
+
throw err;
|
|
234708
235247
|
}
|
|
235248
|
+
note(REASON_TALK_CODEC_UNSUPPORTED);
|
|
235249
|
+
this.ctx.logger.warn("intercom: no path onto the talk channel for this wire codec", {
|
|
235250
|
+
tags: { deviceId: this.id },
|
|
235251
|
+
meta: { wireCodec }
|
|
235252
|
+
});
|
|
234709
235253
|
return { accepted: false };
|
|
234710
235254
|
},
|
|
234711
235255
|
endTalkSession: async ({ deviceId }) => {
|
|
@@ -234713,6 +235257,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
234713
235257
|
const active = this.intercomRawSession;
|
|
234714
235258
|
if (!active) return;
|
|
234715
235259
|
this.intercomRawSession = null;
|
|
235260
|
+
await active.pcmTranscode.close();
|
|
234716
235261
|
if (active.opusDecode) await this.resolveAudioCodecApi().closeSession({
|
|
234717
235262
|
sessionId: active.opusDecode.sessionId,
|
|
234718
235263
|
nodeId: active.opusDecode.nodeId
|
|
@@ -241586,6 +242131,10 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
241586
242131
|
capability: logChannelsCapability,
|
|
241587
242132
|
provider: this.logChannels
|
|
241588
242133
|
});
|
|
242134
|
+
regs.push({
|
|
242135
|
+
capability: failureContributionCapability,
|
|
242136
|
+
provider: { list: () => intercomFailureReport.list() }
|
|
242137
|
+
});
|
|
241589
242138
|
this.subscribe({ category: EventCategory.StreamBrokerOnRequestStreamSourceRefresh }, (event) => {
|
|
241590
242139
|
const data = event.data;
|
|
241591
242140
|
const deviceId = typeof data.deviceId === "number" ? data.deviceId : null;
|