@camstack/addon-provider-hikvision 1.2.46 → 1.2.49
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 +578 -24
- package/dist/addon.mjs +578 -24
- package/package.json +4 -1
package/dist/addon.mjs
CHANGED
|
@@ -8502,6 +8502,112 @@ var TIMEZONES = [
|
|
|
8502
8502
|
function findTimezone(id) {
|
|
8503
8503
|
return TIMEZONES.find((tz) => tz.id === id);
|
|
8504
8504
|
}
|
|
8505
|
+
/**
|
|
8506
|
+
* Distinct (device, family, variant) counters one instance will hold.
|
|
8507
|
+
*
|
|
8508
|
+
* A large fleet x the handful of families any single addon reports, with
|
|
8509
|
+
* slack. At ~200 B per counter this is a ~100 KB ceiling on a process that
|
|
8510
|
+
* already declares an RSS budget in the gigabytes.
|
|
8511
|
+
*/
|
|
8512
|
+
var MAX_KEYS = 1024;
|
|
8513
|
+
/**
|
|
8514
|
+
* Where reasons past {@link MAX_REASONS_PER_KEY} go.
|
|
8515
|
+
*
|
|
8516
|
+
* They are FOLDED, never dropped: `attempts - succeeded` must always equal the
|
|
8517
|
+
* sum of the reason counts, or the ratio stops adding up.
|
|
8518
|
+
*/
|
|
8519
|
+
var OVERFLOW_REASON = "other";
|
|
8520
|
+
/** `deviceId` + `family` + optional `variant`, flattened into the map key. */
|
|
8521
|
+
function counterKey(deviceId, family, variant) {
|
|
8522
|
+
return variant === void 0 ? `${deviceId}${family}` : `${deviceId}${family}${variant}`;
|
|
8523
|
+
}
|
|
8524
|
+
/**
|
|
8525
|
+
* A bounded set of per-camera, cumulative failure counters.
|
|
8526
|
+
*
|
|
8527
|
+
* One instance per contributing subsystem. `note` is O(1) and allocation-free
|
|
8528
|
+
* on the steady path; `snapshot` reads without mutating anything.
|
|
8529
|
+
*/
|
|
8530
|
+
var FailureCounters = class {
|
|
8531
|
+
maxKeys;
|
|
8532
|
+
maxReasons;
|
|
8533
|
+
counters = /* @__PURE__ */ new Map();
|
|
8534
|
+
refused = 0;
|
|
8535
|
+
constructor(maxKeys = MAX_KEYS, maxReasons = 16) {
|
|
8536
|
+
this.maxKeys = maxKeys;
|
|
8537
|
+
this.maxReasons = maxReasons;
|
|
8538
|
+
}
|
|
8539
|
+
/**
|
|
8540
|
+
* Counters refused because {@link MAX_KEYS} was already held.
|
|
8541
|
+
*
|
|
8542
|
+
* Cumulative for the life of the instance: a bound that bit is a fact about
|
|
8543
|
+
* the deployment, and a surface that hid it would under-report a fleet
|
|
8544
|
+
* precisely when the fleet got large enough to matter.
|
|
8545
|
+
*/
|
|
8546
|
+
get keysRefused() {
|
|
8547
|
+
return this.refused;
|
|
8548
|
+
}
|
|
8549
|
+
/** Counters currently held. */
|
|
8550
|
+
get size() {
|
|
8551
|
+
return this.counters.size;
|
|
8552
|
+
}
|
|
8553
|
+
/**
|
|
8554
|
+
* Fold one observation in.
|
|
8555
|
+
*
|
|
8556
|
+
* A non-positive or non-integer `deviceId` is REFUSED rather than bucketed:
|
|
8557
|
+
* see the module docblock — an entry that cannot name its camera is worse
|
|
8558
|
+
* than no entry.
|
|
8559
|
+
*/
|
|
8560
|
+
note(observation, nowMs) {
|
|
8561
|
+
if (!Number.isInteger(observation.deviceId) || observation.deviceId <= 0) return;
|
|
8562
|
+
const key = counterKey(observation.deviceId, observation.family, observation.variant);
|
|
8563
|
+
let counter = this.counters.get(key);
|
|
8564
|
+
if (counter === void 0) {
|
|
8565
|
+
if (this.counters.size >= this.maxKeys) {
|
|
8566
|
+
this.refused += 1;
|
|
8567
|
+
return;
|
|
8568
|
+
}
|
|
8569
|
+
counter = {
|
|
8570
|
+
deviceId: observation.deviceId,
|
|
8571
|
+
family: observation.family,
|
|
8572
|
+
variant: observation.variant,
|
|
8573
|
+
sinceMs: nowMs,
|
|
8574
|
+
attempts: 0,
|
|
8575
|
+
succeeded: 0,
|
|
8576
|
+
reasons: /* @__PURE__ */ new Map()
|
|
8577
|
+
};
|
|
8578
|
+
this.counters.set(key, counter);
|
|
8579
|
+
}
|
|
8580
|
+
counter.attempts += 1;
|
|
8581
|
+
if (observation.reason === void 0) {
|
|
8582
|
+
counter.succeeded += 1;
|
|
8583
|
+
return;
|
|
8584
|
+
}
|
|
8585
|
+
const reason = counter.reasons.has(observation.reason) || counter.reasons.size < this.maxReasons ? observation.reason : OVERFLOW_REASON;
|
|
8586
|
+
counter.reasons.set(reason, (counter.reasons.get(reason) ?? 0) + 1);
|
|
8587
|
+
}
|
|
8588
|
+
/** Read every counter. Never mutates — see the module docblock. */
|
|
8589
|
+
snapshot(nowMs) {
|
|
8590
|
+
const out = [];
|
|
8591
|
+
for (const counter of this.counters.values()) out.push({
|
|
8592
|
+
deviceId: counter.deviceId,
|
|
8593
|
+
family: counter.family,
|
|
8594
|
+
...counter.variant !== void 0 ? { variant: counter.variant } : {},
|
|
8595
|
+
sinceMs: counter.sinceMs,
|
|
8596
|
+
atMs: nowMs,
|
|
8597
|
+
attempts: counter.attempts,
|
|
8598
|
+
succeeded: counter.succeeded,
|
|
8599
|
+
reasons: [...counter.reasons.entries()].map(([reason, count]) => ({
|
|
8600
|
+
reason,
|
|
8601
|
+
count
|
|
8602
|
+
})).toSorted((a, b) => b.count - a.count)
|
|
8603
|
+
});
|
|
8604
|
+
return out;
|
|
8605
|
+
}
|
|
8606
|
+
/** Drop everything (host disposal). */
|
|
8607
|
+
clear() {
|
|
8608
|
+
this.counters.clear();
|
|
8609
|
+
}
|
|
8610
|
+
};
|
|
8505
8611
|
var MODEL_FORMATS = [
|
|
8506
8612
|
"onnx",
|
|
8507
8613
|
"coreml",
|
|
@@ -13757,7 +13863,26 @@ var FailureContributionSchema = object({
|
|
|
13757
13863
|
/** The loss, partitioned. Sums to `attempts - succeeded`. */
|
|
13758
13864
|
reasons: array(FailureReasonCountSchema).readonly()
|
|
13759
13865
|
});
|
|
13760
|
-
|
|
13866
|
+
var failureContributionCapability = {
|
|
13867
|
+
name: "failure-contribution",
|
|
13868
|
+
scope: "system",
|
|
13869
|
+
mode: "collection",
|
|
13870
|
+
internal: true,
|
|
13871
|
+
methods: {
|
|
13872
|
+
/**
|
|
13873
|
+
* This addon's per-camera failure counters, read live from bounded in-RAM
|
|
13874
|
+
* state it already keeps. Inert: no persistence, no sampling, no timer.
|
|
13875
|
+
*
|
|
13876
|
+
* READING NEVER RESETS. The counters are CUMULATIVE since `sinceMs`, and a
|
|
13877
|
+
* consumer that wants a rate differences two reads. A draining read would
|
|
13878
|
+
* make two operators with the page open each destroy half of the other's
|
|
13879
|
+
* numbers, and `load-contribution` already settled the same question the
|
|
13880
|
+
* same way for `cpuSeconds`.
|
|
13881
|
+
*/
|
|
13882
|
+
list: method(_void(), array(FailureContributionSchema).readonly()) },
|
|
13883
|
+
/** In-process only — enumerated through `addons.listCapabilityProviders`. */
|
|
13884
|
+
mount: { kind: "skip" }
|
|
13885
|
+
};
|
|
13761
13886
|
var LoadContributionSchema = object({
|
|
13762
13887
|
role: _enum([
|
|
13763
13888
|
"decode",
|
|
@@ -14065,6 +14190,50 @@ var NodeProcessSchema = object({
|
|
|
14065
14190
|
/** Wall-clock uptime (seconds). Parsed from `ps etime`. */
|
|
14066
14191
|
uptimeSec: number()
|
|
14067
14192
|
});
|
|
14193
|
+
/**
|
|
14194
|
+
* One retained container-memory reading.
|
|
14195
|
+
*
|
|
14196
|
+
* `atMs` is the timestamp of the PROCESS snapshot taken in the same tick, not
|
|
14197
|
+
* a second clock: that is what makes "processes sum to X, container says Y"
|
|
14198
|
+
* subtractable per point rather than an eyeballed comparison of two series
|
|
14199
|
+
* sampled at different instants.
|
|
14200
|
+
*
|
|
14201
|
+
* A reduced window keeps the sample with the LARGEST `currentBytes` in each
|
|
14202
|
+
* bucket, WHOLE. Taking a per-field maximum would synthesise a row whose parts
|
|
14203
|
+
* never coexisted, and a mean would smear away the peak this exists to find.
|
|
14204
|
+
*/
|
|
14205
|
+
var ContainerMemoryPointSchema = object({
|
|
14206
|
+
/** Which hierarchy answered, so a reading is never ambiguous. */
|
|
14207
|
+
source: _enum(["cgroup-v2", "cgroup-v1"]),
|
|
14208
|
+
/** `memory.current` (v2) / `memory.usage_in_bytes` (v1). Always known. */
|
|
14209
|
+
currentBytes: number(),
|
|
14210
|
+
/** The cgroup's ceiling. `null` = NO LIMIT — never a sentinel, never zero. */
|
|
14211
|
+
limitBytes: number().nullable(),
|
|
14212
|
+
/** Anonymous pages: the closest thing to "what the processes allocated". */
|
|
14213
|
+
anonBytes: number().nullable(),
|
|
14214
|
+
/** Page cache. Charged to the cgroup, owned by no process. */
|
|
14215
|
+
fileBytes: number().nullable(),
|
|
14216
|
+
/**
|
|
14217
|
+
* Shared memory — and the field that explained the largest single surprise.
|
|
14218
|
+
* The i915 driver backs GPU buffers with shmem, so an inference pool or a
|
|
14219
|
+
* hardware-decode session holding DRM objects is charged HERE and appears
|
|
14220
|
+
* nowhere in a `ps` scan.
|
|
14221
|
+
*/
|
|
14222
|
+
shmemBytes: number().nullable(),
|
|
14223
|
+
/** Kernel slab charged to this cgroup. `null` on v1, which never publishes it. */
|
|
14224
|
+
slabBytes: number().nullable(),
|
|
14225
|
+
/**
|
|
14226
|
+
* Shrinkable i915 GEM object bytes, from debugfs.
|
|
14227
|
+
*
|
|
14228
|
+
* **Host-wide across every DRM client, NOT cgroup-scoped.** It is not a
|
|
14229
|
+
* component of `currentBytes` and must not be subtracted from it; it says
|
|
14230
|
+
* what put the shmem there, where `shmemBytes` only says how much.
|
|
14231
|
+
*
|
|
14232
|
+
* `null` wherever debugfs is not mounted — which is inside every camstack
|
|
14233
|
+
* container today — and on any node with no Intel GPU.
|
|
14234
|
+
*/
|
|
14235
|
+
gpuShmemBytes: number().nullable()
|
|
14236
|
+
}).extend({ atMs: number() });
|
|
14068
14237
|
var DumpHeapSnapshotInputSchema = object({
|
|
14069
14238
|
/** The addon whose runner should dump a heap snapshot. */
|
|
14070
14239
|
addonId: string() });
|
|
@@ -14128,6 +14297,21 @@ var NodeLoadSeriesSchema = object({
|
|
|
14128
14297
|
/** One entry per function seen in the window, heaviest-first. */
|
|
14129
14298
|
series: array(LoadFunctionSeriesSchema).readonly(),
|
|
14130
14299
|
/**
|
|
14300
|
+
* The CONTAINER's memory over the same window, oldest-first.
|
|
14301
|
+
*
|
|
14302
|
+
* Sits next to `series` rather than in a method of its own because the whole
|
|
14303
|
+
* question is a subtraction: the per-process rows in `series` sum to one
|
|
14304
|
+
* number and this one is another, and an operator who has to issue two calls
|
|
14305
|
+
* to compare them will compare two different instants. Same reader, same
|
|
14306
|
+
* `sinceMs`, same `bucketMs`, same timestamps.
|
|
14307
|
+
*
|
|
14308
|
+
* **EMPTY means ABSENT, never zero.** A node with no cgroup — a developer
|
|
14309
|
+
* Mac, a bare-metal host, a container with the hierarchy hidden — reports no
|
|
14310
|
+
* points at all. A zero here would be indistinguishable from a healthy
|
|
14311
|
+
* container and is precisely the lie this field exists to avoid.
|
|
14312
|
+
*/
|
|
14313
|
+
containerMemory: array(ContainerMemoryPointSchema).readonly(),
|
|
14314
|
+
/**
|
|
14131
14315
|
* Width of one returned bucket, in ms. Equals the sampling cadence when no
|
|
14132
14316
|
* reduction was needed — so a caller can always say what one point covers
|
|
14133
14317
|
* without having to know whether it was reduced.
|
|
@@ -28060,10 +28244,10 @@ var rebootCapability = {
|
|
|
28060
28244
|
* recording config. NOTE on events (source of truth, R5/C3): this cap carries
|
|
28061
28245
|
* NO event surface — `getPlaybackManifest` returns playlist URLs only. Timeline
|
|
28062
28246
|
* events (motion/object/audio) come from `pipelineAnalytics` (durable SQLite
|
|
28063
|
-
* rows)
|
|
28064
|
-
*
|
|
28065
|
-
*
|
|
28066
|
-
* (`interfaces/recording-config.ts`).
|
|
28247
|
+
* rows) and are the ONLY event surface — the recorder has none. The in-RAM
|
|
28248
|
+
* playback markers it used to build were deleted on 2026-08-29 because nothing
|
|
28249
|
+
* ever read them. Event<->footage joins are by time, padded with the shared
|
|
28250
|
+
* `EVENT_PAD_MS` (`interfaces/recording-config.ts`).
|
|
28067
28251
|
*/
|
|
28068
28252
|
var RecordingStatusSchema = object({
|
|
28069
28253
|
deviceId: number(),
|
|
@@ -43590,6 +43774,15 @@ var DEFAULT_BACKLOG_MS = 200;
|
|
|
43590
43774
|
var MAX_BACKLOG_MS = 5e3;
|
|
43591
43775
|
var MIN_BACKLOG_MS = 20;
|
|
43592
43776
|
/**
|
|
43777
|
+
* The ONE place the operator's backlog request becomes the enforced bound.
|
|
43778
|
+
* `start()` sizes the byte window from it and `ability.maxBacklogMs` reports
|
|
43779
|
+
* it — a second clamp would let the number a caller reads drift from the
|
|
43780
|
+
* number the buffer honours.
|
|
43781
|
+
*/
|
|
43782
|
+
function clampBacklogMs(requested) {
|
|
43783
|
+
return Math.max(MIN_BACKLOG_MS, Math.min(MAX_BACKLOG_MS, requested ?? DEFAULT_BACKLOG_MS));
|
|
43784
|
+
}
|
|
43785
|
+
/**
|
|
43593
43786
|
* Fixed audioData PUT-body chunk size, in bytes. Encoded µ-law bytes are
|
|
43594
43787
|
* accumulated and flushed to the sticky PUT in FIXED chunks of this size
|
|
43595
43788
|
* (the sub-chunk remainder is held until the next flush, and the trailing
|
|
@@ -43658,6 +43851,36 @@ var HikvisionIntercomSession = class {
|
|
|
43658
43851
|
get audioCodec() {
|
|
43659
43852
|
return this.codec;
|
|
43660
43853
|
}
|
|
43854
|
+
/**
|
|
43855
|
+
* Effective PCM backlog bound, in ms — the operator's value clamped to
|
|
43856
|
+
* [{@link MIN_BACKLOG_MS}, {@link MAX_BACKLOG_MS}], i.e. what the session is
|
|
43857
|
+
* actually enforcing rather than what it was asked for. Readable before
|
|
43858
|
+
* `start()` because the clamp is pure.
|
|
43859
|
+
*/
|
|
43860
|
+
get backlogMs() {
|
|
43861
|
+
return clampBacklogMs(this.opts.maxBacklogMs);
|
|
43862
|
+
}
|
|
43863
|
+
/**
|
|
43864
|
+
* The firmware's talk-back format — `IntercomStatus.ability`.
|
|
43865
|
+
*
|
|
43866
|
+
* Every field is a value this session already resolved against the camera;
|
|
43867
|
+
* nothing here is a default standing in for a probe. It was declared,
|
|
43868
|
+
* mirrored into runtime state and written by NOBODY while these exact
|
|
43869
|
+
* values were in hand and only reaching a log line (D281).
|
|
43870
|
+
*
|
|
43871
|
+
* `duplex` is the one judgement call: ISAPI two-way audio is a single
|
|
43872
|
+
* `audioData` channel and the provider enforces one active session per
|
|
43873
|
+
* camera, so `half` is reported. `full` would be the dangerous direction —
|
|
43874
|
+
* a consumer that believes it may listen while speaking takes no lock.
|
|
43875
|
+
*/
|
|
43876
|
+
get ability() {
|
|
43877
|
+
return {
|
|
43878
|
+
codecs: [this.codec],
|
|
43879
|
+
sampleRate: HIKVISION_INTERCOM_SAMPLE_RATE,
|
|
43880
|
+
duplex: "half",
|
|
43881
|
+
maxBacklogMs: this.backlogMs
|
|
43882
|
+
};
|
|
43883
|
+
}
|
|
43661
43884
|
async start() {
|
|
43662
43885
|
if (this.stream) return;
|
|
43663
43886
|
const desiredChannel = this.opts.channelId ?? "1";
|
|
@@ -43716,7 +43939,7 @@ var HikvisionIntercomSession = class {
|
|
|
43716
43939
|
});
|
|
43717
43940
|
this.stop(reason).catch(() => {});
|
|
43718
43941
|
});
|
|
43719
|
-
const wantedBacklogMs =
|
|
43942
|
+
const wantedBacklogMs = this.backlogMs;
|
|
43720
43943
|
this.bytesPerSecond = HIKVISION_INTERCOM_SAMPLE_RATE * 2;
|
|
43721
43944
|
this.maxBacklogBytes = Math.max(160, Math.floor(wantedBacklogMs / 1e3 * this.bytesPerSecond));
|
|
43722
43945
|
this.stream = stream;
|
|
@@ -43898,6 +44121,90 @@ var HikvisionIntercomSession = class {
|
|
|
43898
44121
|
}
|
|
43899
44122
|
};
|
|
43900
44123
|
//#endregion
|
|
44124
|
+
//#region src/intercom/intercom-failure-report.ts
|
|
44125
|
+
/**
|
|
44126
|
+
* Per-camera talk-back counters, published through `failure-contribution`.
|
|
44127
|
+
*
|
|
44128
|
+
* ## The number that was never divided
|
|
44129
|
+
*
|
|
44130
|
+
* The rate-mismatch drop had ONE warn line and no counter, so "how much
|
|
44131
|
+
* talk-back is this camera losing" was answerable only by grepping Loki and
|
|
44132
|
+
* hand-correlating timestamps — the exact cost `failure-contribution` exists to
|
|
44133
|
+
* remove. And a bare drop count could not have answered it either: 40 drops out
|
|
44134
|
+
* of 40 pushes and 40 out of 40 000 are opposite findings that produce
|
|
44135
|
+
* identical log volume.
|
|
44136
|
+
*
|
|
44137
|
+
* So EVERY `pushTalkAudio` outcome on a live talk session is noted from the one
|
|
44138
|
+
* place that decides it — the accepted ones too. {@link FailureCounters}
|
|
44139
|
+
* carries `attempts` as the denominator and `succeeded` as the numerator, and
|
|
44140
|
+
* the reasons partition the rest. A success counted somewhere else would drift
|
|
44141
|
+
* from the failures and turn the ratio into fiction.
|
|
44142
|
+
*
|
|
44143
|
+
* ## `variant` is the wire codec, and it is honest
|
|
44144
|
+
*
|
|
44145
|
+
* `failure-contribution` keeps `variant` for a second dimension WITHIN a
|
|
44146
|
+
* family, and here the useful one is the format the caller pushed: an operator
|
|
44147
|
+
* asking "why is 617 silent" needs to know whether HomeKit's Opus or Alexa's
|
|
44148
|
+
* raw PCM is the half that is failing. The provider is handed that value on
|
|
44149
|
+
* every call, so it is reported rather than guessed — absent, never invented.
|
|
44150
|
+
*
|
|
44151
|
+
* ## Process-wide, because a counter is
|
|
44152
|
+
*
|
|
44153
|
+
* One addon is one process (D2) and every camera this addon owns lives in it,
|
|
44154
|
+
* so the instance is module-scoped: the cameras note into it and the addon
|
|
44155
|
+
* registers ONE `failure-contribution` provider that reads it. `sinceMs` is the
|
|
44156
|
+
* incarnation marker — a respawned runner restarts from zero and says so.
|
|
44157
|
+
* Reading NEVER drains.
|
|
44158
|
+
*/
|
|
44159
|
+
/** One `pushTalkAudio` call against an open talk session. */
|
|
44160
|
+
var FAMILY_INTERCOM_TALK = "intercom-talk";
|
|
44161
|
+
/** The push arrived with a sequence number at or below the last accepted one. */
|
|
44162
|
+
var REASON_TALK_OUT_OF_ORDER = "out-of-order";
|
|
44163
|
+
/** The payload decoded to zero bytes. */
|
|
44164
|
+
var REASON_TALK_EMPTY = "empty-frame";
|
|
44165
|
+
/** More than one channel — every camera here is mono-only. */
|
|
44166
|
+
var REASON_TALK_NOT_MONO = "not-mono";
|
|
44167
|
+
/** `s16le` push with no `sampleRate`; the rate is ambiguous, not assumed. */
|
|
44168
|
+
var REASON_TALK_NO_SAMPLE_RATE = "missing-sample-rate";
|
|
44169
|
+
/** The wire codec has no path onto this camera's talk channel. */
|
|
44170
|
+
var REASON_TALK_CODEC_UNSUPPORTED = "codec-unsupported";
|
|
44171
|
+
/** The Opus decode path threw or could not open its session. */
|
|
44172
|
+
var REASON_TALK_OPUS_FAILED = "opus-decode-failed";
|
|
44173
|
+
/**
|
|
44174
|
+
* The addon's talk-back counters. One instance per process; the export at the
|
|
44175
|
+
* bottom of this file IS that instance.
|
|
44176
|
+
*/
|
|
44177
|
+
var IntercomFailureReport = class {
|
|
44178
|
+
now;
|
|
44179
|
+
counters;
|
|
44180
|
+
constructor(now = Date.now, counters = new FailureCounters()) {
|
|
44181
|
+
this.now = now;
|
|
44182
|
+
this.counters = counters;
|
|
44183
|
+
}
|
|
44184
|
+
/**
|
|
44185
|
+
* Note one `pushTalkAudio` outcome. `reason` absent = the frame reached the
|
|
44186
|
+
* camera's talk channel.
|
|
44187
|
+
*/
|
|
44188
|
+
noteTalkFrame(deviceId, wireCodec, reason) {
|
|
44189
|
+
this.counters.note({
|
|
44190
|
+
deviceId,
|
|
44191
|
+
family: FAMILY_INTERCOM_TALK,
|
|
44192
|
+
variant: wireCodec,
|
|
44193
|
+
...reason !== void 0 ? { reason } : {}
|
|
44194
|
+
}, this.now());
|
|
44195
|
+
}
|
|
44196
|
+
/** The `failure-contribution` provider's payload. Reads, never resets. */
|
|
44197
|
+
list() {
|
|
44198
|
+
return this.counters.snapshot(this.now());
|
|
44199
|
+
}
|
|
44200
|
+
/** Addon disposal. */
|
|
44201
|
+
clear() {
|
|
44202
|
+
this.counters.clear();
|
|
44203
|
+
}
|
|
44204
|
+
};
|
|
44205
|
+
/** The process-wide instance every camera in this addon notes into. */
|
|
44206
|
+
var intercomFailureReport = new IntercomFailureReport();
|
|
44207
|
+
//#endregion
|
|
43901
44208
|
//#region src/intercom/intercom-orchestrator.ts
|
|
43902
44209
|
var DEFAULT_OPUS_SAMPLE_RATE = 48e3;
|
|
43903
44210
|
var DEFAULT_OPUS_CHANNELS = 1;
|
|
@@ -43915,6 +44222,14 @@ var IntercomOrchestrator = class {
|
|
|
43915
44222
|
return this.session !== null && !this.session.closed;
|
|
43916
44223
|
}
|
|
43917
44224
|
/**
|
|
44225
|
+
* The live talk session's firmware ability, or `null` when no session is
|
|
44226
|
+
* open. Read by the camera at `startSession` so the WebRTC path writes
|
|
44227
|
+
* `IntercomStatus.ability` from the same source the raw-PCM path does.
|
|
44228
|
+
*/
|
|
44229
|
+
get ability() {
|
|
44230
|
+
return this.session === null || this.session.closed ? null : this.session.talkSession.ability;
|
|
44231
|
+
}
|
|
44232
|
+
/**
|
|
43918
44233
|
* Subscribe to session-close notifications. The listener fires exactly
|
|
43919
44234
|
* once per session with the resolved `IntercomCloseReason` + stats, so
|
|
43920
44235
|
* the camera layer can surface WHY a talk session ended (the prime
|
|
@@ -44176,6 +44491,193 @@ function errMsg$1(err) {
|
|
|
44176
44491
|
return err instanceof Error ? err.message : String(err);
|
|
44177
44492
|
}
|
|
44178
44493
|
//#endregion
|
|
44494
|
+
//#region src/intercom/talk-pcm-transcoder.ts
|
|
44495
|
+
/** libav codec name of a linear little-endian 16-bit PCM decode session. */
|
|
44496
|
+
var TALK_PCM_CODEC = "pcm_s16le";
|
|
44497
|
+
/** The transcoder was already closed — the talk session ended under the push. */
|
|
44498
|
+
var REASON_PCM_CLOSED = "pcm-transcoder-closed";
|
|
44499
|
+
/** The caller's declared source rate is not a usable positive integer. */
|
|
44500
|
+
var REASON_PCM_BAD_RATE = "pcm-bad-source-rate";
|
|
44501
|
+
/** The frame is empty or holds half a sample — malformed, not convertible. */
|
|
44502
|
+
var REASON_PCM_ODD_BYTES = "pcm-odd-bytes";
|
|
44503
|
+
/** No `audio-codec` provider is mounted on this cluster. */
|
|
44504
|
+
var REASON_PCM_NO_CODEC_CAP = "pcm-audio-codec-unavailable";
|
|
44505
|
+
/** The codec cap refused to open a linear-PCM decode session. */
|
|
44506
|
+
var REASON_PCM_SESSION_OPEN_FAILED = "pcm-resample-session-failed";
|
|
44507
|
+
/** The push/pull round-trip through the codec cap threw. */
|
|
44508
|
+
var REASON_PCM_CONVERT_FAILED = "pcm-resample-failed";
|
|
44509
|
+
function errMessage(err) {
|
|
44510
|
+
return err instanceof Error ? err.message : String(err);
|
|
44511
|
+
}
|
|
44512
|
+
var TalkPcmTranscoder = class {
|
|
44513
|
+
opts;
|
|
44514
|
+
active = null;
|
|
44515
|
+
closed = false;
|
|
44516
|
+
constructor(opts) {
|
|
44517
|
+
this.opts = opts;
|
|
44518
|
+
}
|
|
44519
|
+
/** The open codec session, or `null` before the first converted frame. */
|
|
44520
|
+
get sessionId() {
|
|
44521
|
+
return this.active?.sessionId ?? null;
|
|
44522
|
+
}
|
|
44523
|
+
/**
|
|
44524
|
+
* Convert one frame to the camera's rate and hand every produced chunk to
|
|
44525
|
+
* `feed`.
|
|
44526
|
+
*
|
|
44527
|
+
* Returns `null` when the frame was converted and fed, or the REASON string
|
|
44528
|
+
* it was refused for — already logged, with nothing fed.
|
|
44529
|
+
*/
|
|
44530
|
+
async feedResampled(frame) {
|
|
44531
|
+
if (this.closed) {
|
|
44532
|
+
this.refuse(REASON_PCM_CLOSED, {});
|
|
44533
|
+
return REASON_PCM_CLOSED;
|
|
44534
|
+
}
|
|
44535
|
+
const sourceSampleRate = frame.sourceSampleRate;
|
|
44536
|
+
if (!Number.isInteger(sourceSampleRate) || sourceSampleRate <= 0) {
|
|
44537
|
+
this.refuse(REASON_PCM_BAD_RATE, { sourceSampleRate });
|
|
44538
|
+
return REASON_PCM_BAD_RATE;
|
|
44539
|
+
}
|
|
44540
|
+
if (frame.pcm.length === 0 || (frame.pcm.length & 1) !== 0) {
|
|
44541
|
+
this.refuse(REASON_PCM_ODD_BYTES, { bytes: frame.pcm.length });
|
|
44542
|
+
return REASON_PCM_ODD_BYTES;
|
|
44543
|
+
}
|
|
44544
|
+
let api;
|
|
44545
|
+
try {
|
|
44546
|
+
api = this.opts.resolveAudioCodec();
|
|
44547
|
+
} catch (err) {
|
|
44548
|
+
this.refuse(REASON_PCM_NO_CODEC_CAP, { error: errMessage(err) });
|
|
44549
|
+
return REASON_PCM_NO_CODEC_CAP;
|
|
44550
|
+
}
|
|
44551
|
+
if (this.active !== null && this.active.sourceSampleRate !== sourceSampleRate) {
|
|
44552
|
+
const previous = this.active.sourceSampleRate;
|
|
44553
|
+
await this.disposeSession(api, "source-rate-changed");
|
|
44554
|
+
this.opts.logger.info("intercom: pcm resample source rate changed — session recreated", {
|
|
44555
|
+
tags: { deviceId: this.opts.deviceId },
|
|
44556
|
+
meta: {
|
|
44557
|
+
previousSourceSampleRate: previous,
|
|
44558
|
+
sourceSampleRate
|
|
44559
|
+
}
|
|
44560
|
+
});
|
|
44561
|
+
}
|
|
44562
|
+
if (this.active === null) try {
|
|
44563
|
+
const created = await api.createDecodeSession({
|
|
44564
|
+
codec: TALK_PCM_CODEC,
|
|
44565
|
+
sourceSampleRate,
|
|
44566
|
+
sourceChannels: 1,
|
|
44567
|
+
targetSampleRate: this.opts.targetSampleRate,
|
|
44568
|
+
targetChannels: 1,
|
|
44569
|
+
targetFormat: "s16le",
|
|
44570
|
+
tag: this.opts.tag
|
|
44571
|
+
});
|
|
44572
|
+
this.active = {
|
|
44573
|
+
sessionId: created.sessionId,
|
|
44574
|
+
nodeId: created.nodeId,
|
|
44575
|
+
sourceSampleRate
|
|
44576
|
+
};
|
|
44577
|
+
this.opts.logger.info("intercom: pcm resample session opened", {
|
|
44578
|
+
tags: { deviceId: this.opts.deviceId },
|
|
44579
|
+
meta: {
|
|
44580
|
+
codec: TALK_PCM_CODEC,
|
|
44581
|
+
codecSessionId: created.sessionId,
|
|
44582
|
+
codecNodeId: created.nodeId,
|
|
44583
|
+
sourceSampleRate,
|
|
44584
|
+
targetSampleRate: this.opts.targetSampleRate,
|
|
44585
|
+
tag: this.opts.tag
|
|
44586
|
+
}
|
|
44587
|
+
});
|
|
44588
|
+
} catch (err) {
|
|
44589
|
+
this.refuse(REASON_PCM_SESSION_OPEN_FAILED, {
|
|
44590
|
+
sourceSampleRate,
|
|
44591
|
+
targetSampleRate: this.opts.targetSampleRate,
|
|
44592
|
+
error: errMessage(err)
|
|
44593
|
+
});
|
|
44594
|
+
return REASON_PCM_SESSION_OPEN_FAILED;
|
|
44595
|
+
}
|
|
44596
|
+
const session = this.active;
|
|
44597
|
+
try {
|
|
44598
|
+
await api.pushEncodedFrame({
|
|
44599
|
+
sessionId: session.sessionId,
|
|
44600
|
+
nodeId: session.nodeId,
|
|
44601
|
+
data: new Uint8Array(frame.pcm.buffer, frame.pcm.byteOffset, frame.pcm.byteLength)
|
|
44602
|
+
});
|
|
44603
|
+
const chunks = await api.pullPcm({
|
|
44604
|
+
sessionId: session.sessionId,
|
|
44605
|
+
nodeId: session.nodeId,
|
|
44606
|
+
maxCount: 8
|
|
44607
|
+
});
|
|
44608
|
+
for (const chunk of chunks) {
|
|
44609
|
+
const out = Buffer.from(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength);
|
|
44610
|
+
if (out.length > 0) this.opts.feed(out);
|
|
44611
|
+
}
|
|
44612
|
+
return null;
|
|
44613
|
+
} catch (err) {
|
|
44614
|
+
this.refuse(REASON_PCM_CONVERT_FAILED, {
|
|
44615
|
+
codecSessionId: session.sessionId,
|
|
44616
|
+
sourceSampleRate,
|
|
44617
|
+
targetSampleRate: this.opts.targetSampleRate,
|
|
44618
|
+
error: errMessage(err)
|
|
44619
|
+
});
|
|
44620
|
+
await this.disposeSession(api, "convert-failed");
|
|
44621
|
+
return REASON_PCM_CONVERT_FAILED;
|
|
44622
|
+
}
|
|
44623
|
+
}
|
|
44624
|
+
/**
|
|
44625
|
+
* Close the codec session. Idempotent, and called from the provider's
|
|
44626
|
+
* `endTalkSession` so the session dies with the talk session it served.
|
|
44627
|
+
*/
|
|
44628
|
+
async close() {
|
|
44629
|
+
this.closed = true;
|
|
44630
|
+
if (this.active === null) return;
|
|
44631
|
+
let api;
|
|
44632
|
+
try {
|
|
44633
|
+
api = this.opts.resolveAudioCodec();
|
|
44634
|
+
} catch (err) {
|
|
44635
|
+
this.opts.logger.debug("intercom: pcm resample close skipped — audio-codec gone", {
|
|
44636
|
+
tags: { deviceId: this.opts.deviceId },
|
|
44637
|
+
meta: {
|
|
44638
|
+
codecSessionId: this.active.sessionId,
|
|
44639
|
+
error: errMessage(err)
|
|
44640
|
+
}
|
|
44641
|
+
});
|
|
44642
|
+
this.active = null;
|
|
44643
|
+
return;
|
|
44644
|
+
}
|
|
44645
|
+
await this.disposeSession(api, "talk-session-ended");
|
|
44646
|
+
}
|
|
44647
|
+
/** Close + forget the current session. Never throws. */
|
|
44648
|
+
async disposeSession(api, why) {
|
|
44649
|
+
const session = this.active;
|
|
44650
|
+
this.active = null;
|
|
44651
|
+
if (session === null) return;
|
|
44652
|
+
try {
|
|
44653
|
+
await api.closeSession({
|
|
44654
|
+
sessionId: session.sessionId,
|
|
44655
|
+
nodeId: session.nodeId
|
|
44656
|
+
});
|
|
44657
|
+
} catch (err) {
|
|
44658
|
+
this.opts.logger.debug("intercom: pcm resample closeSession error (continuing)", {
|
|
44659
|
+
tags: { deviceId: this.opts.deviceId },
|
|
44660
|
+
meta: {
|
|
44661
|
+
codecSessionId: session.sessionId,
|
|
44662
|
+
why,
|
|
44663
|
+
error: errMessage(err)
|
|
44664
|
+
}
|
|
44665
|
+
});
|
|
44666
|
+
}
|
|
44667
|
+
}
|
|
44668
|
+
/** One warn per refused frame. A branch that drops work says so. */
|
|
44669
|
+
refuse(reason, meta) {
|
|
44670
|
+
this.opts.logger.warn("intercom: pcm talk frame refused — not converted, nothing fed", {
|
|
44671
|
+
tags: { deviceId: this.opts.deviceId },
|
|
44672
|
+
meta: {
|
|
44673
|
+
reason,
|
|
44674
|
+
targetSampleRate: this.opts.targetSampleRate,
|
|
44675
|
+
...meta
|
|
44676
|
+
}
|
|
44677
|
+
});
|
|
44678
|
+
}
|
|
44679
|
+
};
|
|
44680
|
+
//#endregion
|
|
44179
44681
|
//#region src/intercom/werift-intercom-peer.ts
|
|
44180
44682
|
var _werift;
|
|
44181
44683
|
async function loadWerift() {
|
|
@@ -45223,13 +45725,20 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
45223
45725
|
* Called at the four points that open or close a session — and seeded at
|
|
45224
45726
|
* registration, so the slice says `talking: false` from boot rather than
|
|
45225
45727
|
* only after the first session.
|
|
45728
|
+
*
|
|
45729
|
+
* `ability` is STICKY: it is the firmware's negotiated format, learned when a
|
|
45730
|
+
* session opens and still true after it closes, so a caller reading between
|
|
45731
|
+
* sessions gets the last probed value rather than `null`. Passing it is what
|
|
45732
|
+
* changed — it used to be copied forward from `previous` at every one of the
|
|
45733
|
+
* four call sites and written by nobody, while `session.sampleRate` and
|
|
45734
|
+
* `session.audioCodec` were in hand and only reaching a log line.
|
|
45226
45735
|
*/
|
|
45227
|
-
publishIntercomState(talking) {
|
|
45736
|
+
publishIntercomState(talking, ability) {
|
|
45228
45737
|
const previous = this.getCapSlice(intercomCapability);
|
|
45229
45738
|
this.setCapSlice(intercomCapability, {
|
|
45230
45739
|
talking,
|
|
45231
45740
|
lastSessionAt: talking ? Date.now() : previous?.lastSessionAt ?? null,
|
|
45232
|
-
ability: previous?.ability ?? null
|
|
45741
|
+
ability: ability ?? previous?.ability ?? null
|
|
45233
45742
|
});
|
|
45234
45743
|
}
|
|
45235
45744
|
registerIntercomIfSupported() {
|
|
@@ -45258,7 +45767,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
45258
45767
|
});
|
|
45259
45768
|
try {
|
|
45260
45769
|
const opened = await this.intercomOrchestrator.start();
|
|
45261
|
-
this.publishIntercomState(true);
|
|
45770
|
+
this.publishIntercomState(true, this.intercomOrchestrator.ability ?? void 0);
|
|
45262
45771
|
return opened;
|
|
45263
45772
|
} catch (err) {
|
|
45264
45773
|
this.publishIntercomState(false);
|
|
@@ -45280,8 +45789,10 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
45280
45789
|
if (deviceId !== this.id) throw new Error(`HikvisionCamera: intercom deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
45281
45790
|
if (this.disabled) throw new Error("Hikvision intercom: device is disabled — re-enable it before opening a talk session");
|
|
45282
45791
|
if (this.intercomRawSession) {
|
|
45283
|
-
|
|
45792
|
+
const previous = this.intercomRawSession;
|
|
45284
45793
|
this.intercomRawSession = null;
|
|
45794
|
+
await previous.pcmTranscode.close();
|
|
45795
|
+
await previous.session.stop().catch(() => {});
|
|
45285
45796
|
}
|
|
45286
45797
|
const session = new HikvisionIntercomSession({
|
|
45287
45798
|
client: this.ensureClient(),
|
|
@@ -45297,9 +45808,17 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
45297
45808
|
id,
|
|
45298
45809
|
session,
|
|
45299
45810
|
lastSequenceNumber: -1,
|
|
45300
|
-
opusDecode: null
|
|
45811
|
+
opusDecode: null,
|
|
45812
|
+
pcmTranscode: new TalkPcmTranscoder({
|
|
45813
|
+
deviceId: this.id,
|
|
45814
|
+
logger: this.ctx.logger,
|
|
45815
|
+
resolveAudioCodec: () => this.resolveAudioCodecApi(),
|
|
45816
|
+
targetSampleRate: session.sampleRate,
|
|
45817
|
+
tag: `hikvision-intercom-pcm:${this.id}:${id}`,
|
|
45818
|
+
feed: (pcm) => session.feedPcm(pcm)
|
|
45819
|
+
})
|
|
45301
45820
|
};
|
|
45302
|
-
this.publishIntercomState(true);
|
|
45821
|
+
this.publishIntercomState(true, session.ability);
|
|
45303
45822
|
this.ctx.logger.info("intercom talk session opened", {
|
|
45304
45823
|
tags: { deviceId: this.id },
|
|
45305
45824
|
meta: {
|
|
@@ -45312,13 +45831,24 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
45312
45831
|
},
|
|
45313
45832
|
pushTalkAudio: async ({ deviceId, audioBase64, codec, sampleRate, channels, sequenceNumber }) => {
|
|
45314
45833
|
if (deviceId !== this.id) return { accepted: false };
|
|
45834
|
+
const wireCodec = codec ?? "s16le";
|
|
45835
|
+
const note = (reason) => {
|
|
45836
|
+
intercomFailureReport.noteTalkFrame(this.id, wireCodec, reason);
|
|
45837
|
+
};
|
|
45315
45838
|
const active = this.intercomRawSession;
|
|
45316
45839
|
if (!active || !active.session.isOpen) return { accepted: false };
|
|
45317
|
-
if (sequenceNumber <= active.lastSequenceNumber)
|
|
45840
|
+
if (sequenceNumber <= active.lastSequenceNumber) {
|
|
45841
|
+
note(REASON_TALK_OUT_OF_ORDER);
|
|
45842
|
+
return { accepted: false };
|
|
45843
|
+
}
|
|
45318
45844
|
const buf = Buffer.from(audioBase64, "base64");
|
|
45319
|
-
if (buf.length === 0)
|
|
45845
|
+
if (buf.length === 0) {
|
|
45846
|
+
note(REASON_TALK_EMPTY);
|
|
45847
|
+
return { accepted: false };
|
|
45848
|
+
}
|
|
45320
45849
|
const ch = channels ?? 1;
|
|
45321
45850
|
if (ch !== 1) {
|
|
45851
|
+
note(REASON_TALK_NOT_MONO);
|
|
45322
45852
|
this.ctx.logger.warn("intercom: dropping non-mono talk frame (Hikvision is mono-only)", {
|
|
45323
45853
|
tags: { deviceId: this.id },
|
|
45324
45854
|
meta: {
|
|
@@ -45328,9 +45858,9 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
45328
45858
|
});
|
|
45329
45859
|
return { accepted: false };
|
|
45330
45860
|
}
|
|
45331
|
-
const wireCodec = codec ?? "s16le";
|
|
45332
45861
|
if (wireCodec === "g711ulaw" || wireCodec === "g711alaw") {
|
|
45333
45862
|
if (wireCodec !== active.session.audioCodec) {
|
|
45863
|
+
note(REASON_TALK_CODEC_UNSUPPORTED);
|
|
45334
45864
|
this.ctx.logger.warn("intercom: codec mismatch — wire codec is not what the camera negotiated, dropping frame", {
|
|
45335
45865
|
tags: { deviceId: this.id },
|
|
45336
45866
|
meta: {
|
|
@@ -45342,28 +45872,34 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
45342
45872
|
}
|
|
45343
45873
|
active.lastSequenceNumber = sequenceNumber;
|
|
45344
45874
|
active.session.feedEncoded(buf);
|
|
45875
|
+
note();
|
|
45345
45876
|
return { accepted: true };
|
|
45346
45877
|
}
|
|
45347
45878
|
if (wireCodec === "s16le") {
|
|
45348
45879
|
if (!sampleRate) {
|
|
45880
|
+
note(REASON_TALK_NO_SAMPLE_RATE);
|
|
45349
45881
|
this.ctx.logger.warn("intercom: s16le push with no sampleRate — dropping (rate is ambiguous)", { tags: { deviceId: this.id } });
|
|
45350
45882
|
return { accepted: false };
|
|
45351
45883
|
}
|
|
45352
45884
|
if (sampleRate !== active.session.sampleRate) {
|
|
45353
|
-
|
|
45354
|
-
|
|
45355
|
-
|
|
45356
|
-
wireRate: sampleRate,
|
|
45357
|
-
cameraRate: active.session.sampleRate
|
|
45358
|
-
}
|
|
45885
|
+
const refusal = await active.pcmTranscode.feedResampled({
|
|
45886
|
+
pcm: buf,
|
|
45887
|
+
sourceSampleRate: sampleRate
|
|
45359
45888
|
});
|
|
45360
|
-
|
|
45889
|
+
if (refusal !== null) {
|
|
45890
|
+
note(refusal);
|
|
45891
|
+
return { accepted: false };
|
|
45892
|
+
}
|
|
45893
|
+
active.lastSequenceNumber = sequenceNumber;
|
|
45894
|
+
note();
|
|
45895
|
+
return { accepted: true };
|
|
45361
45896
|
}
|
|
45362
45897
|
active.lastSequenceNumber = sequenceNumber;
|
|
45363
45898
|
active.session.feedPcm(buf);
|
|
45899
|
+
note();
|
|
45364
45900
|
return { accepted: true };
|
|
45365
45901
|
}
|
|
45366
|
-
if (wireCodec === "opus") {
|
|
45902
|
+
if (wireCodec === "opus") try {
|
|
45367
45903
|
if (!active.opusDecode) {
|
|
45368
45904
|
const created = await this.resolveAudioCodecApi().createDecodeSession({
|
|
45369
45905
|
codec: "opus",
|
|
@@ -45406,8 +45942,20 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
45406
45942
|
const pcmBuf = Buffer.from(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength);
|
|
45407
45943
|
if (pcmBuf.length > 0) active.session.feedPcm(pcmBuf);
|
|
45408
45944
|
}
|
|
45945
|
+
note();
|
|
45409
45946
|
return { accepted: true };
|
|
45947
|
+
} catch (err) {
|
|
45948
|
+
note(REASON_TALK_OPUS_FAILED);
|
|
45949
|
+
throw err;
|
|
45410
45950
|
}
|
|
45951
|
+
note(REASON_TALK_CODEC_UNSUPPORTED);
|
|
45952
|
+
this.ctx.logger.warn("intercom: no path onto the talk channel for this wire codec", {
|
|
45953
|
+
tags: { deviceId: this.id },
|
|
45954
|
+
meta: {
|
|
45955
|
+
wireCodec,
|
|
45956
|
+
cameraCodec: active.session.audioCodec
|
|
45957
|
+
}
|
|
45958
|
+
});
|
|
45411
45959
|
return { accepted: false };
|
|
45412
45960
|
},
|
|
45413
45961
|
endTalkSession: async ({ deviceId }) => {
|
|
@@ -45415,6 +45963,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
45415
45963
|
const active = this.intercomRawSession;
|
|
45416
45964
|
if (!active) return;
|
|
45417
45965
|
this.intercomRawSession = null;
|
|
45966
|
+
await active.pcmTranscode.close();
|
|
45418
45967
|
if (active.opusDecode) await this.resolveAudioCodecApi().closeSession({
|
|
45419
45968
|
sessionId: active.opusDecode.sessionId,
|
|
45420
45969
|
nodeId: active.opusDecode.nodeId
|
|
@@ -57617,7 +58166,12 @@ var HikvisionProviderAddon = class extends BaseDeviceProvider {
|
|
|
57617
58166
|
throw new Error(`Hikvision: ${reason}`);
|
|
57618
58167
|
}
|
|
57619
58168
|
async onInitialize() {
|
|
57620
|
-
|
|
58169
|
+
const regs = await super.onInitialize();
|
|
58170
|
+
regs.push({
|
|
58171
|
+
capability: failureContributionCapability,
|
|
58172
|
+
provider: { list: () => intercomFailureReport.list() }
|
|
58173
|
+
});
|
|
58174
|
+
return regs;
|
|
57621
58175
|
}
|
|
57622
58176
|
async supportsDiscovery() {
|
|
57623
58177
|
return true;
|