@camstack/addon-pipeline 1.2.373 → 1.2.374
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 +3 -3
- package/dist/audio-analyzer/index.mjs +3 -3
- package/dist/{custom-model-catalog-UaaDgBow.mjs → custom-model-catalog-1LDuDw8S.mjs} +2 -2
- package/dist/{custom-model-catalog-M-ECFeNJ.js → custom-model-catalog-W0DHDhfo.js} +2 -2
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-QBVFmPYQ.js → dist-CBEUVjTS.js} +172 -24
- package/dist/{dist-BT3uSeR7.mjs → dist-CmdLCb_F.mjs} +167 -25
- package/dist/{model-catalogs-ChW0i2P6.mjs → model-catalogs-BnCdB2A3.mjs} +1 -1
- package/dist/{model-catalogs-C42Mo0jo.js → model-catalogs-D5YksA5A.js} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-BVRFl1hZ.mjs → node-C3r8kiOo.mjs} +1 -1
- package/dist/{node-B6ev-Hqa.js → node-DMgi6pND.js} +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-Cdf2A2qg.js → process-memory-GFR7fxpJ.js} +1 -1
- package/dist/{process-memory-DdeedEgU.mjs → process-memory-X0ws1SGJ.mjs} +1 -1
- package/dist/recorder/index.js +251 -94
- package/dist/recorder/index.mjs +249 -92
- package/dist/{segment-demux-js-Dp0OI-2n.js → segment-demux-js-CSQVhbFI.js} +1 -1
- package/dist/{segment-demux-js-8CfbEFeD.mjs → segment-demux-js-IKEO1XBz.mjs} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Dl3wrJnM.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DPFTlVoW.mjs} +2 -2
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C3ssSNKh.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B7wsxLrK.mjs} +1 -1
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BFRtKBOe.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BohOb2Cl.mjs} +1 -1
- 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-Bq36nWMP.mjs → hostInit-BOp1_Hhk.mjs} +2 -2
- package/dist/stream-broker/index.js +683 -265
- package/dist/stream-broker/index.mjs +683 -265
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +5 -1
- package/dist/retire-root-keys-Csb8z2f3.js +0 -338
- package/dist/retire-root-keys-DZvlso2l.mjs +0 -327
|
@@ -3,12 +3,11 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_node = require("../node-
|
|
6
|
+
const require_dist = require("../dist-CBEUVjTS.js");
|
|
7
|
+
const require_node = require("../node-DMgi6pND.js");
|
|
8
8
|
const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
|
|
9
9
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
10
|
-
const
|
|
11
|
-
const require_segment_demux_js = require("../segment-demux-js-Dp0OI-2n.js");
|
|
10
|
+
const require_segment_demux_js = require("../segment-demux-js-CSQVhbFI.js");
|
|
12
11
|
let node_os = require("node:os");
|
|
13
12
|
node_os = require_chunk.__toESM(node_os);
|
|
14
13
|
let node_crypto = require("node:crypto");
|
|
@@ -364,207 +363,253 @@ function formatProbedSummary(declared, snapshot, opts) {
|
|
|
364
363
|
return parts.join(SEP);
|
|
365
364
|
}
|
|
366
365
|
//#endregion
|
|
367
|
-
//#region src/stream-broker/broker-
|
|
368
|
-
/**
|
|
369
|
-
* The stream-broker's
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
* The row survives either way: it is also where `BaseAddon.resolveConfig` reads
|
|
394
|
-
* the operator's config fields from. The problem was a 33 KB row, not a blob.
|
|
395
|
-
*/
|
|
396
|
-
/**
|
|
397
|
-
* The addon id — the settings-store namespace whose `root` row the flattened
|
|
398
|
-
* keys are being retired from. Matches `packages/addon-pipeline/package.json`
|
|
399
|
-
* (`camstack.addons[].id`).
|
|
400
|
-
*/
|
|
401
|
-
var STREAM_BROKER_ADDON_ID = "stream-broker";
|
|
402
|
-
/**
|
|
403
|
-
* Every one of these collections is keyed by the BROKER ID
|
|
404
|
-
* (`<deviceId>/<camStreamId>`) or by the numeric deviceId. `brokerDeviceId`
|
|
405
|
-
* recovers the numeric device from either, for the projected column that makes
|
|
406
|
-
* a per-camera question an indexed query — and for `tags: { deviceId }` on
|
|
407
|
-
* every line these stores log.
|
|
408
|
-
*
|
|
409
|
-
* Returns `null` rather than guessing: a key whose head is not a number is a
|
|
410
|
-
* key this addon did not write, and inventing a device for it would put a row
|
|
411
|
-
* on a camera at random.
|
|
412
|
-
*/
|
|
413
|
-
function brokerDeviceId(key) {
|
|
414
|
-
const head = key.includes("/") ? key.slice(0, key.indexOf("/")) : key;
|
|
415
|
-
const id = Number(head);
|
|
416
|
-
return Number.isInteger(id) && id > 0 ? id : null;
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* Bound for a whole-map read. `settings-store.query` silently caps an unbounded
|
|
420
|
-
* read at 2 000 rows and answers as if that were everything; at 974 devices ×
|
|
421
|
-
* up to 5 streams the default is HALF the margin, so every read here names its
|
|
422
|
-
* own ceiling and a truncation is reported rather than shrunk.
|
|
423
|
-
*/
|
|
424
|
-
var BROKER_ROWS_LIMIT = 2e4;
|
|
425
|
-
/**
|
|
426
|
-
* @durable class=mirror owner=stream-broker
|
|
427
|
-
* write="one row per broker (`<deviceId>/<camStreamId>`), replaced wholesale from the
|
|
428
|
-
* manager's live probe map by the throttled metrics flush and once on shutdown"
|
|
429
|
-
* retention="the flush is authoritative: a broker the manager no longer holds loses its
|
|
430
|
-
* row. Losing the whole collection costs the settings 'Probed' field showing 'not yet
|
|
431
|
-
* probed' until the stream is pulled again — it is re-derived from the next probe."
|
|
432
|
-
*/
|
|
433
|
-
var BROKER_PROBE_SNAPSHOTS_COLLECTION = "stream-broker:probe-snapshots";
|
|
434
|
-
var BROKER_PROBE_SNAPSHOTS_SPEC = {
|
|
435
|
-
collection: BROKER_PROBE_SNAPSHOTS_COLLECTION,
|
|
436
|
-
schema: ProbedSnapshotSchema,
|
|
437
|
-
columns: [{
|
|
438
|
-
name: "deviceId",
|
|
439
|
-
type: "INTEGER"
|
|
440
|
-
}, {
|
|
441
|
-
name: "at",
|
|
442
|
-
type: "INTEGER",
|
|
443
|
-
notNull: true
|
|
444
|
-
}],
|
|
445
|
-
indexes: [{
|
|
446
|
-
name: "idx_stream_broker_probe_device",
|
|
447
|
-
columns: ["deviceId"]
|
|
448
|
-
}],
|
|
449
|
-
project: (key, value) => ({
|
|
450
|
-
deviceId: brokerDeviceId(key),
|
|
451
|
-
at: value.at
|
|
452
|
-
}),
|
|
453
|
-
deviceIdColumn: "deviceId",
|
|
454
|
-
loadLimit: BROKER_ROWS_LIMIT
|
|
455
|
-
};
|
|
456
|
-
function probeSnapshotsStore(store, logger) {
|
|
457
|
-
return new require_retire_root_keys.RowMapStore({
|
|
458
|
-
spec: BROKER_PROBE_SNAPSHOTS_SPEC,
|
|
459
|
-
store,
|
|
460
|
-
logger
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* The token, wrapped in an object rather than stored bare.
|
|
465
|
-
*
|
|
466
|
-
* A bare string in a `JSON` column round-trips only because the engine's
|
|
467
|
-
* decoder JSON-parses a stored string ONLY when it starts with `{` or `[`. That
|
|
468
|
-
* is a heuristic, not a contract; a token is opaque hex today and a future
|
|
469
|
-
* format that starts with a brace would come back as an object. One field costs
|
|
470
|
-
* nothing and makes the row self-describing.
|
|
471
|
-
*/
|
|
472
|
-
var RtspTokenRowSchema = require_dist.object({ token: require_dist.string().min(1) });
|
|
473
|
-
/**
|
|
474
|
-
* @durable class=ledger owner=stream-broker
|
|
475
|
-
* write="one row per broker (`<deviceId>/<camStreamId>`), upserted when the restream
|
|
476
|
-
* server mints a token. MERGE only — a token is stable forever per stream unless it
|
|
477
|
-
* is explicitly regenerated"
|
|
478
|
-
* retention="nothing deletes a row. The persister receives only the CURRENTLY registered
|
|
479
|
-
* restreamers, so a replace would erase the token of an unregistered broker and turn
|
|
480
|
-
* an external NVR URL into a dead 404 at the next idle or reboot."
|
|
481
|
-
*/
|
|
482
|
-
var BROKER_RTSP_TOKENS_COLLECTION = "stream-broker:rtsp-tokens";
|
|
483
|
-
var BROKER_RTSP_TOKENS_SPEC = {
|
|
484
|
-
collection: BROKER_RTSP_TOKENS_COLLECTION,
|
|
485
|
-
schema: RtspTokenRowSchema,
|
|
486
|
-
columns: [{
|
|
487
|
-
name: "deviceId",
|
|
488
|
-
type: "INTEGER"
|
|
489
|
-
}],
|
|
490
|
-
indexes: [{
|
|
491
|
-
name: "idx_stream_broker_rtsp_tokens_device",
|
|
492
|
-
columns: ["deviceId"]
|
|
493
|
-
}],
|
|
494
|
-
project: (key) => ({ deviceId: brokerDeviceId(key) }),
|
|
495
|
-
deviceIdColumn: "deviceId",
|
|
496
|
-
loadLimit: BROKER_ROWS_LIMIT
|
|
497
|
-
};
|
|
498
|
-
function rtspTokensStore(store, logger) {
|
|
499
|
-
return new require_retire_root_keys.RowMapStore({
|
|
500
|
-
spec: BROKER_RTSP_TOKENS_SPEC,
|
|
501
|
-
store,
|
|
502
|
-
logger
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
/** One camera's profile → camStreamId assignment, plus whether it is automatic. */
|
|
366
|
+
//#region src/stream-broker/broker-device-state.ts
|
|
367
|
+
/**
|
|
368
|
+
* The stream-broker's per-camera state, kept as per-device settings (D870).
|
|
369
|
+
*
|
|
370
|
+
* Three maps used to be collections of their own (`stream-broker:probe-snapshots`,
|
|
371
|
+
* `:profile-map`, `:rtsp-tokens`), each keyed by the camera. A camera's data in
|
|
372
|
+
* a side table is a row somebody must remember to delete when the camera goes;
|
|
373
|
+
* as a KEY of the camera's own settings it is cleared by the central device
|
|
374
|
+
* delete (D865) with everything else.
|
|
375
|
+
*
|
|
376
|
+
* | key | value for one camera |
|
|
377
|
+
* | ---------------- | --------------------------------------------------------- |
|
|
378
|
+
* | `probeSnapshots` | `{ [camStreamId]: ProbedSnapshot }` - what the broker last probed |
|
|
379
|
+
* | `brokerProfile` | `{ map, auto }` - the profile -> camStreamId assignment |
|
|
380
|
+
* | `rtspTokens` | JSON text of `{ [camStreamId]: token }` - a SECRET: declared in `secretStatePaths`, so the settings view stores a keychain marker and the text lives in the keychain |
|
|
381
|
+
*
|
|
382
|
+
* Failure direction: a load that cannot ask the store THROWS (D49). The writers
|
|
383
|
+
* built on {@link DeviceKeyMirror} refuse to write until a load has succeeded,
|
|
384
|
+
* because a replace driven by an empty "known" set would otherwise drop or
|
|
385
|
+
* shrink rows it never saw.
|
|
386
|
+
*/
|
|
387
|
+
/** Per-device settings keys. */
|
|
388
|
+
var PROBE_SNAPSHOTS_KEY = "probeSnapshots";
|
|
389
|
+
var BROKER_PROFILE_KEY = "brokerProfile";
|
|
390
|
+
var RTSP_TOKENS_KEY = "rtspTokens";
|
|
391
|
+
/** One camera's profile -> camStreamId assignment, plus whether it is automatic. */
|
|
506
392
|
var ProfileAssignmentSchema = require_dist.object({
|
|
507
393
|
map: require_dist.record(require_dist.string(), require_dist.string()),
|
|
508
394
|
auto: require_dist.boolean()
|
|
509
395
|
});
|
|
396
|
+
var ProbeSnapshotsOfDeviceSchema = require_dist.record(require_dist.string(), ProbedSnapshotSchema);
|
|
397
|
+
var RtspTokensOfDeviceSchema = require_dist.record(require_dist.string(), require_dist.string().min(1));
|
|
510
398
|
/**
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
|
|
399
|
+
* A broker id is `<deviceId>/<camStreamId>`. Returns `null` rather than
|
|
400
|
+
* guessing: a key whose head is not a number is a key this addon did not
|
|
401
|
+
* write, and inventing a device for it would put a value on a camera at random.
|
|
402
|
+
*/
|
|
403
|
+
function splitBrokerId(brokerId) {
|
|
404
|
+
const slash = brokerId.indexOf("/");
|
|
405
|
+
if (slash <= 0) return null;
|
|
406
|
+
const deviceId = Number(brokerId.slice(0, slash));
|
|
407
|
+
if (!Number.isInteger(deviceId) || deviceId <= 0) return null;
|
|
408
|
+
return {
|
|
409
|
+
deviceId,
|
|
410
|
+
camStreamId: brokerId.slice(slash + 1)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/** Group `<deviceId>/<camStreamId>` -> value into deviceId -> camStreamId -> value. */
|
|
414
|
+
function groupByDevice(flat) {
|
|
415
|
+
const entries = flat instanceof Map ? [...flat.entries()] : Object.entries(flat);
|
|
416
|
+
const out = /* @__PURE__ */ new Map();
|
|
417
|
+
for (const [brokerId, value] of entries) {
|
|
418
|
+
const parts = splitBrokerId(brokerId);
|
|
419
|
+
if (parts === null) continue;
|
|
420
|
+
const group = out.get(parts.deviceId) ?? {};
|
|
421
|
+
group[parts.camStreamId] = value;
|
|
422
|
+
out.set(parts.deviceId, group);
|
|
423
|
+
}
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
/** The inverse of {@link groupByDevice}. */
|
|
427
|
+
function flattenByDevice(grouped) {
|
|
428
|
+
const out = /* @__PURE__ */ new Map();
|
|
429
|
+
for (const [deviceId, group] of grouped) for (const [camStreamId, value] of Object.entries(group)) out.set(`${deviceId}/${camStreamId}`, value);
|
|
430
|
+
return out;
|
|
431
|
+
}
|
|
432
|
+
/** A stored value that exists but cannot be read now (see `DeviceKeyMirrorDeps.decode`). */
|
|
433
|
+
var HELD_UNAVAILABLE = Symbol("held-unavailable");
|
|
434
|
+
/**
|
|
435
|
+
* One settings key across the fleet: loaded once, then written by diff.
|
|
436
|
+
*
|
|
437
|
+
* `replace` is authoritative (a camera the caller no longer holds loses the
|
|
438
|
+
* key, but only if this mirror has SEEN it - it never deletes what it did not
|
|
439
|
+
* load); `merge` only ever adds and changes.
|
|
440
|
+
*/
|
|
441
|
+
var DeviceKeyMirror = class {
|
|
442
|
+
deps;
|
|
443
|
+
/** The JSON of what the store holds per device, as of the last load or write. */
|
|
444
|
+
held = /* @__PURE__ */ new Map();
|
|
445
|
+
values = /* @__PURE__ */ new Map();
|
|
446
|
+
blocked = /* @__PURE__ */ new Map();
|
|
447
|
+
/** Cameras whose stored value is GONE: a load never re-blocks them. */
|
|
448
|
+
released = /* @__PURE__ */ new Set();
|
|
449
|
+
loaded = false;
|
|
450
|
+
constructor(deps) {
|
|
451
|
+
this.deps = deps;
|
|
452
|
+
}
|
|
453
|
+
/** Read the key of every camera that has it. THROWS when the store cannot be asked. */
|
|
454
|
+
async load() {
|
|
455
|
+
const ids = await this.deps.settings.listDeviceStoreIds();
|
|
456
|
+
const blobs = await this.deps.settings.readDeviceStoreBatch(ids);
|
|
457
|
+
const out = /* @__PURE__ */ new Map();
|
|
458
|
+
this.held.clear();
|
|
459
|
+
this.values.clear();
|
|
460
|
+
this.loaded = false;
|
|
461
|
+
this.blocked.clear();
|
|
462
|
+
for (const [deviceId, blob] of blobs) {
|
|
463
|
+
const stored = blob[this.deps.key];
|
|
464
|
+
if (stored === void 0) continue;
|
|
465
|
+
const value = this.deps.decode(stored);
|
|
466
|
+
if (value === HELD_UNAVAILABLE) {
|
|
467
|
+
if (!this.released.has(deviceId)) this.blocked.set(deviceId, stored);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (value === null) {
|
|
471
|
+
this.deps.logger.warn("broker device state skipped - it does not parse", {
|
|
472
|
+
tags: { deviceId },
|
|
473
|
+
meta: { key: this.deps.key }
|
|
474
|
+
});
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
out.set(deviceId, value);
|
|
478
|
+
this.values.set(deviceId, value);
|
|
479
|
+
this.held.set(deviceId, JSON.stringify(this.deps.encode(value)));
|
|
480
|
+
}
|
|
481
|
+
this.loaded = true;
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
/** Cameras whose stored value exists but could not be read: never written, never replaced. */
|
|
485
|
+
heldDevices() {
|
|
486
|
+
return this.blocked;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Stop protecting a camera whose stored value is GONE (the secret was deleted
|
|
490
|
+
* or the keychain restored without it): there is nothing left to overwrite.
|
|
491
|
+
*/
|
|
492
|
+
release(deviceId) {
|
|
493
|
+
this.blocked.delete(deviceId);
|
|
494
|
+
this.released.add(deviceId);
|
|
495
|
+
}
|
|
496
|
+
/** Is this camera's token write (and restream URL) blocked right now? */
|
|
497
|
+
isBlocked(deviceId) {
|
|
498
|
+
return this.blocked.has(deviceId);
|
|
499
|
+
}
|
|
500
|
+
assertLoaded() {
|
|
501
|
+
if (!this.loaded) throw new Error(`broker device state "${this.deps.key}" was never loaded - refusing to write`);
|
|
502
|
+
}
|
|
503
|
+
async writeChanged(next) {
|
|
504
|
+
let written = 0;
|
|
505
|
+
for (const [deviceId, value] of next) {
|
|
506
|
+
if (this.blocked.has(deviceId)) continue;
|
|
507
|
+
const encoded = this.deps.encode(value);
|
|
508
|
+
const text = JSON.stringify(encoded);
|
|
509
|
+
if (this.held.get(deviceId) === text) continue;
|
|
510
|
+
await this.deps.settings.writeDeviceStore(deviceId, { [this.deps.key]: encoded });
|
|
511
|
+
this.held.set(deviceId, text);
|
|
512
|
+
this.values.set(deviceId, value);
|
|
513
|
+
written += 1;
|
|
514
|
+
}
|
|
515
|
+
return written;
|
|
516
|
+
}
|
|
517
|
+
/** Make the store hold exactly `next` for the cameras this mirror knows. */
|
|
518
|
+
async replace(next) {
|
|
519
|
+
this.assertLoaded();
|
|
520
|
+
const written = await this.writeChanged(next);
|
|
521
|
+
let removed = 0;
|
|
522
|
+
for (const deviceId of [...this.held.keys()]) {
|
|
523
|
+
if (next.has(deviceId) || this.blocked.has(deviceId)) continue;
|
|
524
|
+
await this.deps.settings.writeDeviceStore(deviceId, { [this.deps.key]: void 0 });
|
|
525
|
+
this.held.delete(deviceId);
|
|
526
|
+
this.values.delete(deviceId);
|
|
527
|
+
removed += 1;
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
written,
|
|
531
|
+
removed
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/** Add or change; never remove. */
|
|
535
|
+
async merge(next) {
|
|
536
|
+
this.assertLoaded();
|
|
537
|
+
return this.writeChanged(next);
|
|
538
|
+
}
|
|
539
|
+
/** Drop one camera's key (a device that changed hardware under its number). */
|
|
540
|
+
async remove(deviceId) {
|
|
541
|
+
this.assertLoaded();
|
|
542
|
+
if (!this.held.has(deviceId) || this.blocked.has(deviceId)) return false;
|
|
543
|
+
await this.deps.settings.writeDeviceStore(deviceId, { [this.deps.key]: void 0 });
|
|
544
|
+
this.held.delete(deviceId);
|
|
545
|
+
this.values.delete(deviceId);
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
/** What the store holds for the camera as of the last load or write. */
|
|
549
|
+
current(deviceId) {
|
|
550
|
+
return this.values.get(deviceId);
|
|
551
|
+
}
|
|
552
|
+
/** Whether a load has answered; a writer that was never loaded refuses. */
|
|
553
|
+
get isLoaded() {
|
|
554
|
+
return this.loaded;
|
|
555
|
+
}
|
|
538
556
|
};
|
|
539
|
-
function
|
|
540
|
-
return
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
557
|
+
function parseWith(schema) {
|
|
558
|
+
return (stored) => {
|
|
559
|
+
const parsed = schema.safeParse(stored);
|
|
560
|
+
return parsed.success ? parsed.data : null;
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function probeSnapshotsMirror(settings, logger) {
|
|
564
|
+
return new DeviceKeyMirror({
|
|
565
|
+
settings,
|
|
566
|
+
key: PROBE_SNAPSHOTS_KEY,
|
|
567
|
+
logger,
|
|
568
|
+
decode: parseWith(ProbeSnapshotsOfDeviceSchema),
|
|
569
|
+
encode: (value) => value
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
function profileMapMirror(settings, logger) {
|
|
573
|
+
return new DeviceKeyMirror({
|
|
574
|
+
settings,
|
|
575
|
+
key: BROKER_PROFILE_KEY,
|
|
576
|
+
logger,
|
|
577
|
+
decode: parseWith(ProfileAssignmentSchema),
|
|
578
|
+
encode: (value) => value
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
/** The tokens of one camera are ONE keychain-held text: JSON of `{ camStreamId: token }`. */
|
|
582
|
+
function rtspTokensMirror(settings, logger) {
|
|
583
|
+
return new DeviceKeyMirror({
|
|
584
|
+
settings,
|
|
585
|
+
key: RTSP_TOKENS_KEY,
|
|
586
|
+
logger,
|
|
587
|
+
decode: (stored) => {
|
|
588
|
+
if (require_dist.isSecretMarker(stored)) return HELD_UNAVAILABLE;
|
|
589
|
+
if (typeof stored !== "string") return null;
|
|
590
|
+
try {
|
|
591
|
+
const parsed = RtspTokensOfDeviceSchema.safeParse(JSON.parse(stored));
|
|
592
|
+
return parsed.success ? parsed.data : null;
|
|
593
|
+
} catch {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
encode: (value) => JSON.stringify(value)
|
|
544
598
|
});
|
|
545
599
|
}
|
|
546
600
|
/**
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
601
|
+
* Fold freshly minted tokens (`<deviceId>/<camStreamId>` -> token) into what
|
|
602
|
+
* each camera already holds. Only ever adds or changes: a token is stable
|
|
603
|
+
* forever per stream unless explicitly regenerated.
|
|
550
604
|
*/
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
successor: BROKER_RTSP_TOKENS_COLLECTION,
|
|
560
|
-
reason: "one row per broker since the flatten; as a blob a single minted token rewrote 33 KB"
|
|
561
|
-
},
|
|
562
|
-
{
|
|
563
|
-
key: "profileMap",
|
|
564
|
-
successor: BROKER_PROFILE_MAP_COLLECTION,
|
|
565
|
-
reason: "one row per camera since the flatten; a per-DEVICE map, so as a blob it grew with the fleet — 2.3 KB at 29 cameras, ~77 KB at 974"
|
|
566
|
-
}
|
|
567
|
-
];
|
|
605
|
+
function mergeRtspTokens(mirror, minted) {
|
|
606
|
+
const out = /* @__PURE__ */ new Map();
|
|
607
|
+
for (const [deviceId, group] of groupByDevice(minted)) out.set(deviceId, {
|
|
608
|
+
...mirror.current(deviceId),
|
|
609
|
+
...group
|
|
610
|
+
});
|
|
611
|
+
return out;
|
|
612
|
+
}
|
|
568
613
|
//#endregion
|
|
569
614
|
//#region src/stream-broker/live-viewers.ts
|
|
570
615
|
/**
|
|
@@ -5532,6 +5577,18 @@ var RtspListenServer = class RtspListenServer {
|
|
|
5532
5577
|
brokerTokens = /* @__PURE__ */ new Map();
|
|
5533
5578
|
/** token → brokerId (reverse map) */
|
|
5534
5579
|
tokenBrokers = /* @__PURE__ */ new Map();
|
|
5580
|
+
/**
|
|
5581
|
+
* brokerId → INTERNAL loopback-only alias token (D870). A camera whose
|
|
5582
|
+
* external restream token is held in an unreadable keychain has NO external
|
|
5583
|
+
* URL, but the broker's own consumers (recording, decode, snapshots, grid,
|
|
5584
|
+
* egress) still ride its restream: they get this ephemeral token, which is
|
|
5585
|
+
* never persisted, never handed out through the consumer URL APIs, and only
|
|
5586
|
+
* answers loopback / local-network peers.
|
|
5587
|
+
*/
|
|
5588
|
+
internalAliases = /* @__PURE__ */ new Map();
|
|
5589
|
+
internalAliasTokens = /* @__PURE__ */ new Set();
|
|
5590
|
+
/** Which peers may dial an internal alias; replaceable for tests. */
|
|
5591
|
+
aliasPeerPolicy = isClusterLocalAddress;
|
|
5535
5592
|
/** brokerId → enabled (default true) */
|
|
5536
5593
|
brokerEnabled = /* @__PURE__ */ new Map();
|
|
5537
5594
|
/** brokerId → warm-up closure registered alongside the restreamer */
|
|
@@ -5562,7 +5619,37 @@ var RtspListenServer = class RtspListenServer {
|
|
|
5562
5619
|
if (sentryOpener) this.brokerPrestreamOpeners.set(brokerId, sentryOpener);
|
|
5563
5620
|
return token;
|
|
5564
5621
|
}
|
|
5622
|
+
/**
|
|
5623
|
+
* Register (or refresh) the internal loopback-only alias of a broker. Returns
|
|
5624
|
+
* its token. The external token map is untouched, so the alias can never be
|
|
5625
|
+
* listed, persisted or surfaced as a consumer URL.
|
|
5626
|
+
*/
|
|
5627
|
+
registerInternalAlias(brokerId, restreamer, warmUp, sentryOpener) {
|
|
5628
|
+
const token = this.internalAliases.get(brokerId) ?? RtspListenServer.generateToken();
|
|
5629
|
+
this.restreamers.set(token, restreamer);
|
|
5630
|
+
this.tokenBrokers.set(token, brokerId);
|
|
5631
|
+
this.internalAliases.set(brokerId, token);
|
|
5632
|
+
this.internalAliasTokens.add(token);
|
|
5633
|
+
if (warmUp) this.brokerWarmUps.set(brokerId, warmUp);
|
|
5634
|
+
if (sentryOpener) this.brokerPrestreamOpeners.set(brokerId, sentryOpener);
|
|
5635
|
+
return token;
|
|
5636
|
+
}
|
|
5637
|
+
/** Replace the peer policy of the internal aliases (tests only). */
|
|
5638
|
+
setAliasPeerPolicy(policy) {
|
|
5639
|
+
this.aliasPeerPolicy = policy;
|
|
5640
|
+
}
|
|
5641
|
+
/** The token an IN-PROCESS consumer dials: the internal alias when there is one, else the external token. */
|
|
5642
|
+
getInternalToken(brokerId) {
|
|
5643
|
+
return this.internalAliases.get(brokerId) ?? this.brokerTokens.get(brokerId);
|
|
5644
|
+
}
|
|
5565
5645
|
unregisterRestreamer(brokerId) {
|
|
5646
|
+
const alias = this.internalAliases.get(brokerId);
|
|
5647
|
+
if (alias !== void 0) {
|
|
5648
|
+
this.restreamers.delete(alias);
|
|
5649
|
+
this.tokenBrokers.delete(alias);
|
|
5650
|
+
this.internalAliasTokens.delete(alias);
|
|
5651
|
+
this.internalAliases.delete(brokerId);
|
|
5652
|
+
}
|
|
5566
5653
|
const token = this.brokerTokens.get(brokerId);
|
|
5567
5654
|
if (token) {
|
|
5568
5655
|
this.restreamers.delete(token);
|
|
@@ -5584,6 +5671,17 @@ var RtspListenServer = class RtspListenServer {
|
|
|
5584
5671
|
this.restreamers.set(newToken, restreamer);
|
|
5585
5672
|
this.brokerTokens.set(brokerId, newToken);
|
|
5586
5673
|
this.tokenBrokers.set(newToken, brokerId);
|
|
5674
|
+
const alias = this.internalAliases.get(brokerId);
|
|
5675
|
+
if (alias !== void 0) {
|
|
5676
|
+
this.restreamers.delete(alias);
|
|
5677
|
+
this.tokenBrokers.delete(alias);
|
|
5678
|
+
this.internalAliasTokens.delete(alias);
|
|
5679
|
+
const newAlias = RtspListenServer.generateToken();
|
|
5680
|
+
this.restreamers.set(newAlias, restreamer);
|
|
5681
|
+
this.tokenBrokers.set(newAlias, brokerId);
|
|
5682
|
+
this.internalAliases.set(brokerId, newAlias);
|
|
5683
|
+
this.internalAliasTokens.add(newAlias);
|
|
5684
|
+
}
|
|
5587
5685
|
return newToken;
|
|
5588
5686
|
}
|
|
5589
5687
|
/** Get the current token for a broker. */
|
|
@@ -5667,6 +5765,11 @@ var RtspListenServer = class RtspListenServer {
|
|
|
5667
5765
|
socket.destroy();
|
|
5668
5766
|
return;
|
|
5669
5767
|
}
|
|
5768
|
+
if (this.internalAliasTokens.has(lookupPath) && !this.aliasPeerPolicy(socket.remoteAddress)) {
|
|
5769
|
+
socket.write(`RTSP/1.0 403 Forbidden\r\nCSeq: 1\r\n\r\n`);
|
|
5770
|
+
socket.destroy();
|
|
5771
|
+
return;
|
|
5772
|
+
}
|
|
5670
5773
|
const brokerId = this.tokenBrokers.get(lookupPath);
|
|
5671
5774
|
if (brokerId && !this.isEnabled(brokerId)) {
|
|
5672
5775
|
socket.write(`RTSP/1.0 403 Forbidden\r\nCSeq: 1\r\n\r\n`);
|
|
@@ -5725,6 +5828,24 @@ var RtspListenServer = class RtspListenServer {
|
|
|
5725
5828
|
});
|
|
5726
5829
|
}
|
|
5727
5830
|
};
|
|
5831
|
+
/**
|
|
5832
|
+
* Is the peer on the loopback interface or the local network (RFC 1918, link-local,
|
|
5833
|
+
* ULA)? The internal alias answers only these: a recorder or decoder on another
|
|
5834
|
+
* node of the cluster is a legitimate consumer, the public internet is not.
|
|
5835
|
+
*/
|
|
5836
|
+
function isClusterLocalAddress(address) {
|
|
5837
|
+
if (address === void 0) return false;
|
|
5838
|
+
const v4 = address.startsWith("::ffff:") ? address.slice(7) : address;
|
|
5839
|
+
if (v4 === "::1") return true;
|
|
5840
|
+
const m = /^(\d+)\.(\d+)\.\d+\.\d+$/.exec(v4);
|
|
5841
|
+
if (m !== null) {
|
|
5842
|
+
const a = Number(m[1]);
|
|
5843
|
+
const b = Number(m[2]);
|
|
5844
|
+
return a === 127 || a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254 || a === 100 && b >= 64 && b <= 127;
|
|
5845
|
+
}
|
|
5846
|
+
const lower = address.toLowerCase();
|
|
5847
|
+
return lower.startsWith("fe80:") || lower.startsWith("fc") || lower.startsWith("fd");
|
|
5848
|
+
}
|
|
5728
5849
|
//#endregion
|
|
5729
5850
|
//#region src/stream-broker/rtsp/rtsp-restream-provider.ts
|
|
5730
5851
|
/**
|
|
@@ -5806,6 +5927,24 @@ var RtspRestreamProviderImpl = class {
|
|
|
5806
5927
|
enabled: this.server.isEnabled(brokerId)
|
|
5807
5928
|
};
|
|
5808
5929
|
}
|
|
5930
|
+
/**
|
|
5931
|
+
* The entry an IN-PROCESS consumer dials (recording, decode, snapshots, grid,
|
|
5932
|
+
* egress): the loopback-only internal alias when the camera's external token
|
|
5933
|
+
* is held back (D870), else the external entry. NEVER hand this out through a
|
|
5934
|
+
* consumer URL API - use {@link getEntry} for that.
|
|
5935
|
+
*/
|
|
5936
|
+
getInternalEntry(brokerId, hostname) {
|
|
5937
|
+
const token = this.server.getInternalToken(brokerId);
|
|
5938
|
+
if (!token) return null;
|
|
5939
|
+
const port = this.server.getPort();
|
|
5940
|
+
const host = hostname ?? "127.0.0.1";
|
|
5941
|
+
return {
|
|
5942
|
+
brokerId,
|
|
5943
|
+
url: `rtsp://${host}:${port}/${token}`,
|
|
5944
|
+
mutedUrl: `rtsp://${host}:${port}/${token}/muted`,
|
|
5945
|
+
enabled: this.server.isEnabled(brokerId)
|
|
5946
|
+
};
|
|
5947
|
+
}
|
|
5809
5948
|
regenerateToken(brokerId) {
|
|
5810
5949
|
const token = this.server.regenerateToken(brokerId);
|
|
5811
5950
|
if (token) {
|
|
@@ -17539,6 +17678,14 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17539
17678
|
rtspProvider;
|
|
17540
17679
|
persistedTokens = /* @__PURE__ */ new Map();
|
|
17541
17680
|
/**
|
|
17681
|
+
* True for a camera whose restream tokens are held in an unreadable keychain
|
|
17682
|
+
* (D870). Such a camera gets NO restream URL - minting a temporary token would
|
|
17683
|
+
* hand consumers a URL that dies as soon as the real one is restored - while
|
|
17684
|
+
* every other camera, and the camera's own streams and recording, are
|
|
17685
|
+
* untouched.
|
|
17686
|
+
*/
|
|
17687
|
+
restreamGate = () => false;
|
|
17688
|
+
/**
|
|
17542
17689
|
* Last runtime probe per broker, captured while the broker was streaming.
|
|
17543
17690
|
* Survives the stream going idle so the per-stream "Probed" settings field
|
|
17544
17691
|
* stays populated (with an "as of" tag). In-memory only — repopulates on the
|
|
@@ -17738,7 +17885,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17738
17885
|
hwaccel: this.hwaccel ?? void 0,
|
|
17739
17886
|
cameraStreamLookup: (deviceId) => this.getCameraStreamsForDevice(deviceId),
|
|
17740
17887
|
rtspEntryLookup: (brokerId) => {
|
|
17741
|
-
const entry = this.rtspProvider.
|
|
17888
|
+
const entry = this.rtspProvider.getInternalEntry(brokerId);
|
|
17742
17889
|
return entry ? {
|
|
17743
17890
|
url: entry.url,
|
|
17744
17891
|
mutedUrl: entry.mutedUrl
|
|
@@ -17797,7 +17944,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17797
17944
|
cameraStreamLookup: (deviceId) => this.getCameraStreamsForDevice(deviceId),
|
|
17798
17945
|
resolveProfileSource: (deviceId, profile) => this.assignments.get(deviceId)?.map[profile] ?? null,
|
|
17799
17946
|
rtspEntryLookup: (brokerId) => {
|
|
17800
|
-
const entry = this.rtspProvider.
|
|
17947
|
+
const entry = this.rtspProvider.getInternalEntry(brokerId);
|
|
17801
17948
|
return entry ? { url: entry.url } : null;
|
|
17802
17949
|
},
|
|
17803
17950
|
sourceHasMedia: (brokerId) => {
|
|
@@ -18861,6 +19008,20 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18861
19008
|
loadPersistedTokens(tokens) {
|
|
18862
19009
|
this.persistedTokens = new Map(tokens);
|
|
18863
19010
|
}
|
|
19011
|
+
/** See {@link restreamGate}. */
|
|
19012
|
+
setRestreamGate(gate) {
|
|
19013
|
+
this.restreamGate = gate;
|
|
19014
|
+
}
|
|
19015
|
+
/**
|
|
19016
|
+
* Register the restream URLs of ONE camera now (its tokens became readable, or
|
|
19017
|
+
* were issued fresh): each of its source brokers and the profile restreamers
|
|
19018
|
+
* over them. No broker is torn down or recreated, so nothing stops recording.
|
|
19019
|
+
*/
|
|
19020
|
+
registerRestreamersForDevice(deviceId) {
|
|
19021
|
+
if (this.restreamGate(deviceId)) return;
|
|
19022
|
+
for (const [brokerId, broker] of this.brokers) if (brokerId.startsWith(`${deviceId}/`)) this.registerSourceRestreamer(brokerId, broker);
|
|
19023
|
+
this.syncProfileRestreamers(deviceId);
|
|
19024
|
+
}
|
|
18864
19025
|
/** Upsert a single SOURCE (non-derived) cam-stream into the registry
|
|
18865
19026
|
* WITHOUT ranking or broker creation. Returns the prior entry (if any)
|
|
18866
19027
|
* so callers can detect url refresh / already-existed. */
|
|
@@ -19604,6 +19765,20 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19604
19765
|
return { success: true };
|
|
19605
19766
|
}
|
|
19606
19767
|
/**
|
|
19768
|
+
* Drop a DELETED camera's whole override entry and persist (D870). The
|
|
19769
|
+
* device-removal hook's one reach into the override map; `forgetDeviceHardware`
|
|
19770
|
+
* keeps the entry's non-hardware fields, a deleted camera keeps nothing.
|
|
19771
|
+
*/
|
|
19772
|
+
dropDeviceOverride(deviceId) {
|
|
19773
|
+
if (!this.deviceOverrides.delete(deviceId)) return false;
|
|
19774
|
+
this.deviceOverridePersister?.(this.deviceOverrides);
|
|
19775
|
+
return true;
|
|
19776
|
+
}
|
|
19777
|
+
/** Cameras that have an override entry. */
|
|
19778
|
+
listDeviceOverrideIds() {
|
|
19779
|
+
return [...this.deviceOverrides.keys()];
|
|
19780
|
+
}
|
|
19781
|
+
/**
|
|
19607
19782
|
* The hardware behind `deviceId` was replaced (`deviceManager.migrateDevice`)
|
|
19608
19783
|
* — forget everything this broker knows that described the OLD box. See the
|
|
19609
19784
|
* cap doc for the inventory. What deliberately SURVIVES is the role-scoped
|
|
@@ -20382,6 +20557,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
20382
20557
|
const sourceBrokerId = brokerIdFor(deviceId, camStreamId);
|
|
20383
20558
|
const sourceBroker = this.brokers.get(sourceBrokerId);
|
|
20384
20559
|
if (!sourceBroker) continue;
|
|
20560
|
+
if (this.restreamGate(deviceId)) continue;
|
|
20385
20561
|
const existingToken = this.persistedTokens.get(profileBrokerId);
|
|
20386
20562
|
const warmUp = () => sourceBroker.onEncodedData(() => {}, RTSP_DESCRIBE_WARMUP_ATTRIBUTION);
|
|
20387
20563
|
const openPrestreamWindow = () => sourceBroker.openPrestreamWindow();
|
|
@@ -22041,11 +22217,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
22041
22217
|
}
|
|
22042
22218
|
this.brokers.set(brokerId, broker);
|
|
22043
22219
|
this.applyClipRetention(deviceId);
|
|
22044
|
-
|
|
22045
|
-
const warmUp = () => broker.onEncodedData(() => {}, RTSP_DESCRIBE_WARMUP_ATTRIBUTION);
|
|
22046
|
-
const openPrestreamWindow = () => broker.openPrestreamWindow();
|
|
22047
|
-
this.rtspServer.registerRestreamer(brokerId, broker.getRtspRestreamer(), warmUp, existingToken, openPrestreamWindow);
|
|
22048
|
-
if (!existingToken) this.rtspProvider.persistTokens();
|
|
22220
|
+
this.registerSourceRestreamer(brokerId, broker);
|
|
22049
22221
|
await this.fanOutRegistration(deviceId, brokerId, cam, broker);
|
|
22050
22222
|
this.syncProfileRestreamers(deviceId);
|
|
22051
22223
|
if (cam.kind === "derived") {
|
|
@@ -22054,6 +22226,28 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
22054
22226
|
}
|
|
22055
22227
|
}
|
|
22056
22228
|
/**
|
|
22229
|
+
* Register one source broker's RTSP restreamer with its persisted token (or a
|
|
22230
|
+
* freshly minted one). A camera whose tokens are held in an unreadable keychain
|
|
22231
|
+
* gets no EXTERNAL token, only the internal loopback-only alias.
|
|
22232
|
+
*/
|
|
22233
|
+
registerSourceRestreamer(brokerId, broker) {
|
|
22234
|
+
const deviceId = parseBrokerId(brokerId)?.deviceId;
|
|
22235
|
+
if (deviceId === void 0) return;
|
|
22236
|
+
if (this.restreamGate(deviceId)) {
|
|
22237
|
+
this.logger.warn("external restream URL withheld - the tokens are held but unreadable", {
|
|
22238
|
+
tags: { deviceId },
|
|
22239
|
+
meta: { brokerId }
|
|
22240
|
+
});
|
|
22241
|
+
this.rtspServer.registerInternalAlias(brokerId, broker.getRtspRestreamer(), () => broker.onEncodedData(() => {}, RTSP_DESCRIBE_WARMUP_ATTRIBUTION), () => broker.openPrestreamWindow());
|
|
22242
|
+
return;
|
|
22243
|
+
}
|
|
22244
|
+
const existingToken = this.persistedTokens.get(brokerId);
|
|
22245
|
+
const warmUp = () => broker.onEncodedData(() => {}, RTSP_DESCRIBE_WARMUP_ATTRIBUTION);
|
|
22246
|
+
const openPrestreamWindow = () => broker.openPrestreamWindow();
|
|
22247
|
+
this.rtspServer.registerRestreamer(brokerId, broker.getRtspRestreamer(), warmUp, existingToken, openPrestreamWindow);
|
|
22248
|
+
if (!existingToken) this.rtspProvider.persistTokens();
|
|
22249
|
+
}
|
|
22250
|
+
/**
|
|
22057
22251
|
* Tear down the broker for a `(deviceId, camStreamId)` pair. No-op
|
|
22058
22252
|
* when no broker exists. Called by `retractCameraStream` and by the
|
|
22059
22253
|
* teardown half of `restartBroker`.
|
|
@@ -22173,7 +22367,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
22173
22367
|
};
|
|
22174
22368
|
} else {
|
|
22175
22369
|
const sourceBrokerId = brokerIdFor(deviceId, request.camStreamId);
|
|
22176
|
-
const entry = this.rtspProvider.
|
|
22370
|
+
const entry = this.rtspProvider.getInternalEntry(sourceBrokerId, "127.0.0.1");
|
|
22177
22371
|
if (!entry) throw new Error(`derived source camStream loopback unavailable: ${sourceBrokerId}`);
|
|
22178
22372
|
loopbackUrl = entry.url;
|
|
22179
22373
|
release = async () => {};
|
|
@@ -38308,18 +38502,177 @@ function subscribeDeviceRemoval(subscribe, deps) {
|
|
|
38308
38502
|
});
|
|
38309
38503
|
}
|
|
38310
38504
|
//#endregion
|
|
38505
|
+
//#region src/stream-broker/device-removal-hook.ts
|
|
38506
|
+
/**
|
|
38507
|
+
* The stream-broker's `device-removal-hook` (D870).
|
|
38508
|
+
*
|
|
38509
|
+
* What the broker keeps PER CAMERA is a key of the camera's own settings
|
|
38510
|
+
* (`probeSnapshots`, `brokerProfile`, `rtspTokens` - see `broker-device-state.ts`),
|
|
38511
|
+
* and the central device delete (D865) clears those for every addon. What is
|
|
38512
|
+
* left is the broker's live state and its one addon-level map keyed by camera,
|
|
38513
|
+
* `deviceOverrides`: the hook retracts the camera's streams, purges its slot
|
|
38514
|
+
* assignments and drops its override entry. Idempotent - a camera with nothing
|
|
38515
|
+
* left answers zero counts.
|
|
38516
|
+
*/
|
|
38517
|
+
function createBrokerRemovalHook(deps) {
|
|
38518
|
+
return {
|
|
38519
|
+
onDeviceRemoved: async (input) => {
|
|
38520
|
+
const { deviceId } = input;
|
|
38521
|
+
try {
|
|
38522
|
+
await deps.retractDevice(deviceId);
|
|
38523
|
+
} catch (err) {
|
|
38524
|
+
deps.logger.warn("stream-broker: retract of a deleted device failed", {
|
|
38525
|
+
tags: { deviceId },
|
|
38526
|
+
meta: { error: require_dist.errMsg(err) }
|
|
38527
|
+
});
|
|
38528
|
+
throw err;
|
|
38529
|
+
}
|
|
38530
|
+
const overrides = deps.dropDeviceOverride(deviceId) ? 1 : 0;
|
|
38531
|
+
if (overrides > 0) deps.logger.info("stream-broker: removed the override of a deleted device", {
|
|
38532
|
+
tags: { deviceId },
|
|
38533
|
+
meta: { deletedAt: input.deletedAt }
|
|
38534
|
+
});
|
|
38535
|
+
return { counts: { "device-overrides": overrides } };
|
|
38536
|
+
},
|
|
38537
|
+
listDeviceData: async () => deps.overrideDeviceIds().filter((deviceId) => Number.isInteger(deviceId) && deviceId > 0).map((deviceId) => ({
|
|
38538
|
+
store: "device-overrides",
|
|
38539
|
+
deviceId,
|
|
38540
|
+
count: 1
|
|
38541
|
+
}))
|
|
38542
|
+
};
|
|
38543
|
+
}
|
|
38544
|
+
/** Classify every held camera against the keychain: wait for it, or give up on the value. */
|
|
38545
|
+
async function classifyHeld(held, secrets) {
|
|
38546
|
+
const refs = /* @__PURE__ */ new Map();
|
|
38547
|
+
for (const [deviceId, stored] of held) if (require_dist.isSecretMarker(stored)) refs.set(deviceId, stored.$secret);
|
|
38548
|
+
if (refs.size === 0) return [];
|
|
38549
|
+
const resolved = await secrets.resolve([...refs.values()]);
|
|
38550
|
+
const out = [];
|
|
38551
|
+
for (const [deviceId, ref] of refs) {
|
|
38552
|
+
const entry = resolved[ref];
|
|
38553
|
+
if (entry === void 0 || entry.kind === "unavailable") out.push({
|
|
38554
|
+
deviceId,
|
|
38555
|
+
ref,
|
|
38556
|
+
kind: "unavailable",
|
|
38557
|
+
reason: entry?.kind === "unavailable" ? entry.reason : null
|
|
38558
|
+
});
|
|
38559
|
+
else if (entry.kind === "missing") out.push({
|
|
38560
|
+
deviceId,
|
|
38561
|
+
ref,
|
|
38562
|
+
kind: "missing",
|
|
38563
|
+
reason: null
|
|
38564
|
+
});
|
|
38565
|
+
else if (entry.kind === "forbidden") out.push({
|
|
38566
|
+
deviceId,
|
|
38567
|
+
ref,
|
|
38568
|
+
kind: "unavailable",
|
|
38569
|
+
reason: "forbidden"
|
|
38570
|
+
});
|
|
38571
|
+
}
|
|
38572
|
+
return out;
|
|
38573
|
+
}
|
|
38574
|
+
var UNAVAILABLE_TEXT = "The camera's RTSP restream URLs are unavailable (consumers such as Home Assistant or an NVR get errors) until the keychain is readable. Its streams and recording are not affected.";
|
|
38575
|
+
var issuedText = (deviceId) => `New RTSP restream URLs were issued for camera ${deviceId} - update the consumers (NVR, Home Assistant). The previous tokens were lost: the keychain no longer holds them.`;
|
|
38576
|
+
var TokenRecovery = class {
|
|
38577
|
+
deps;
|
|
38578
|
+
/** Cameras alerted as unavailable: the alert is cleared when the value resolves. */
|
|
38579
|
+
waiting = /* @__PURE__ */ new Set();
|
|
38580
|
+
/** Consecutive `missing` reads per camera (D49: two before the value is given up). */
|
|
38581
|
+
missingReads = /* @__PURE__ */ new Map();
|
|
38582
|
+
timer = null;
|
|
38583
|
+
running = false;
|
|
38584
|
+
constructor(deps) {
|
|
38585
|
+
this.deps = deps;
|
|
38586
|
+
}
|
|
38587
|
+
/** Cameras currently blocked. */
|
|
38588
|
+
blocked() {
|
|
38589
|
+
return [...this.deps.mirror.heldDevices().keys()].toSorted((a, b) => a - b);
|
|
38590
|
+
}
|
|
38591
|
+
/** Check now, then on a timer while any camera is still held. */
|
|
38592
|
+
start() {
|
|
38593
|
+
if (this.timer !== null) return;
|
|
38594
|
+
this.tick();
|
|
38595
|
+
this.timer = setInterval(() => void this.tick(), this.deps.recheckMs ?? 3e4);
|
|
38596
|
+
this.timer.unref?.();
|
|
38597
|
+
}
|
|
38598
|
+
stop() {
|
|
38599
|
+
if (this.timer !== null) clearInterval(this.timer);
|
|
38600
|
+
this.timer = null;
|
|
38601
|
+
}
|
|
38602
|
+
/** One scheduled / change-triggered check; never overlaps itself, never throws. */
|
|
38603
|
+
async tick() {
|
|
38604
|
+
if (this.running) return;
|
|
38605
|
+
this.running = true;
|
|
38606
|
+
try {
|
|
38607
|
+
await this.check();
|
|
38608
|
+
} catch (err) {
|
|
38609
|
+
this.deps.logger.warn("restream-token re-check failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
38610
|
+
} finally {
|
|
38611
|
+
this.running = false;
|
|
38612
|
+
}
|
|
38613
|
+
if (this.blocked().length === 0 && this.waiting.size === 0) this.stop();
|
|
38614
|
+
}
|
|
38615
|
+
/** See the module doc. Idempotent. */
|
|
38616
|
+
async check() {
|
|
38617
|
+
const before = new Set(this.deps.mirror.heldDevices().keys());
|
|
38618
|
+
if (before.size === 0 && this.waiting.size === 0) return;
|
|
38619
|
+
const tokens = await this.deps.mirror.load();
|
|
38620
|
+
const now = new Set(this.deps.mirror.heldDevices().keys());
|
|
38621
|
+
const classified = await classifyHeld(this.deps.mirror.heldDevices(), this.deps.secrets);
|
|
38622
|
+
const released = [];
|
|
38623
|
+
for (const camera of classified) {
|
|
38624
|
+
if (camera.kind === "missing") {
|
|
38625
|
+
const reads = (this.missingReads.get(camera.deviceId) ?? 0) + 1;
|
|
38626
|
+
this.missingReads.set(camera.deviceId, reads);
|
|
38627
|
+
if (reads < 2) continue;
|
|
38628
|
+
this.deps.mirror.release(camera.deviceId);
|
|
38629
|
+
this.missingReads.delete(camera.deviceId);
|
|
38630
|
+
if (this.waiting.delete(camera.deviceId)) await this.deps.alerts.clear(camera.deviceId);
|
|
38631
|
+
released.push(camera.deviceId);
|
|
38632
|
+
continue;
|
|
38633
|
+
}
|
|
38634
|
+
this.missingReads.delete(camera.deviceId);
|
|
38635
|
+
if (this.waiting.has(camera.deviceId)) continue;
|
|
38636
|
+
this.waiting.add(camera.deviceId);
|
|
38637
|
+
this.deps.logger.error("restream tokens held but not readable", {
|
|
38638
|
+
tags: { deviceId: camera.deviceId },
|
|
38639
|
+
meta: {
|
|
38640
|
+
reason: camera.reason,
|
|
38641
|
+
ref: camera.ref
|
|
38642
|
+
}
|
|
38643
|
+
});
|
|
38644
|
+
await this.deps.alerts.raise({
|
|
38645
|
+
deviceId: camera.deviceId,
|
|
38646
|
+
kind: "unavailable",
|
|
38647
|
+
message: UNAVAILABLE_TEXT
|
|
38648
|
+
});
|
|
38649
|
+
}
|
|
38650
|
+
const recovered = [...before].filter((id) => !now.has(id) && !released.includes(id));
|
|
38651
|
+
for (const deviceId of recovered) if (this.waiting.delete(deviceId)) await this.deps.alerts.clear(deviceId);
|
|
38652
|
+
if (recovered.length > 0) this.deps.logger.info("restream tokens readable again", { meta: { cameras: recovered.length } });
|
|
38653
|
+
const cameras = [...recovered, ...released];
|
|
38654
|
+
if (cameras.length > 0) await this.deps.onRegister(tokens, cameras);
|
|
38655
|
+
for (const deviceId of released) {
|
|
38656
|
+
this.deps.logger.error("restream tokens lost - new URLs issued", { tags: { deviceId } });
|
|
38657
|
+
await this.deps.alerts.raise({
|
|
38658
|
+
deviceId,
|
|
38659
|
+
kind: "missing",
|
|
38660
|
+
message: issuedText(deviceId)
|
|
38661
|
+
});
|
|
38662
|
+
}
|
|
38663
|
+
}
|
|
38664
|
+
};
|
|
38665
|
+
//#endregion
|
|
38311
38666
|
//#region src/stream-broker/addon.ts
|
|
38312
38667
|
/**
|
|
38313
|
-
* Hydrate the persisted RTSP restream state. Tokens come from
|
|
38314
|
-
*
|
|
38315
|
-
*
|
|
38316
|
-
*
|
|
38317
|
-
*
|
|
38668
|
+
* Hydrate the persisted RTSP restream state. Tokens come from the cameras' own
|
|
38669
|
+
* settings; `rtspEnabled` is still a key of the settings row. An unreadable
|
|
38670
|
+
* token set THROWS (the caller refuses to write tokens afterwards - a
|
|
38671
|
+
* replacement minted over a token that was merely unreadable would turn every
|
|
38672
|
+
* external NVR URL into a 404), and an empty set keeps first-boot silence.
|
|
38318
38673
|
*/
|
|
38319
38674
|
async function hydratePersistedRtspState(deps) {
|
|
38320
|
-
const
|
|
38321
|
-
const tokens = /* @__PURE__ */ new Map();
|
|
38322
|
-
for (const [brokerId, row] of rows) tokens.set(brokerId, row.token);
|
|
38675
|
+
const tokens = flattenByDevice(await deps.tokensMirror.load());
|
|
38323
38676
|
if (tokens.size > 0) {
|
|
38324
38677
|
deps.loadTokens(tokens);
|
|
38325
38678
|
deps.logger.info("Loaded persisted RTSP tokens", { meta: { tokenCount: tokens.size } });
|
|
@@ -38388,6 +38741,8 @@ var PROBE_SNAPSHOTS_PERSIST_INTERVAL_MS = 6e4;
|
|
|
38388
38741
|
* the sample as absent (the RTCP Receiver-Report path drives the ladder anyway). */
|
|
38389
38742
|
var NETWORK_QUALITY_STATS_TIMEOUT_MS = 2e3;
|
|
38390
38743
|
var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
38744
|
+
/** The restream tokens of a camera are a per-device setting held in the keychain (D870). */
|
|
38745
|
+
secretStatePaths = ["rtspTokens"];
|
|
38391
38746
|
brokerManager = null;
|
|
38392
38747
|
/** The node's pinned ffmpeg, mirrored here so the recorded-playback demuxers
|
|
38393
38748
|
* (segment-demux/segment-audio) spawn the same binary as the transcode
|
|
@@ -38444,6 +38799,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38444
38799
|
* restarts). Loaded on activate, flushed throttled from the metrics loop and
|
|
38445
38800
|
* once on shutdown. */
|
|
38446
38801
|
probeSnapshots = null;
|
|
38802
|
+
removalHook = null;
|
|
38447
38803
|
lastProbePersistAt = 0;
|
|
38448
38804
|
constructor() {
|
|
38449
38805
|
super({
|
|
@@ -38506,24 +38862,26 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38506
38862
|
logger: this.ctx.logger
|
|
38507
38863
|
});
|
|
38508
38864
|
await this.ffmpegWiring.load();
|
|
38509
|
-
|
|
38510
|
-
await this.purgeFlattenedSettingsKeys();
|
|
38865
|
+
const deviceSettings = this.deviceSettings();
|
|
38511
38866
|
const rtspProvider = this.brokerManager.getRtspRestreamProvider();
|
|
38512
38867
|
const brokerManager = this.brokerManager;
|
|
38513
|
-
const rtspTokens =
|
|
38868
|
+
const rtspTokens = rtspTokensMirror(deviceSettings, this.ctx.logger);
|
|
38514
38869
|
const rtspEnabledState = this.state("rtspEnabled", RtspEnabledSchema, {});
|
|
38515
38870
|
try {
|
|
38516
38871
|
await hydratePersistedRtspState({
|
|
38517
|
-
|
|
38872
|
+
tokensMirror: rtspTokens,
|
|
38518
38873
|
enabledState: rtspEnabledState,
|
|
38519
38874
|
loadTokens: (tokens) => brokerManager.loadPersistedTokens(tokens),
|
|
38520
38875
|
loadEnabled: (states) => rtspProvider.loadPersistedEnabled(states),
|
|
38521
38876
|
logger: this.ctx.logger
|
|
38522
38877
|
});
|
|
38523
|
-
} catch {
|
|
38878
|
+
} catch (err) {
|
|
38879
|
+
this.ctx.logger.warn("Failed to load persisted RTSP state - tokens will not be written", { meta: { error: require_dist.errMsg(err) } });
|
|
38880
|
+
}
|
|
38881
|
+
this.startTokenRecovery(rtspTokens, brokerManager);
|
|
38524
38882
|
rtspProvider.setTokenPersister(async (tokens) => {
|
|
38525
38883
|
try {
|
|
38526
|
-
|
|
38884
|
+
await rtspTokens.merge(mergeRtspTokens(rtspTokens, tokens));
|
|
38527
38885
|
this.ctx.logger.info("Persisted RTSP tokens", { meta: { current: tokens.size } });
|
|
38528
38886
|
} catch (err) {
|
|
38529
38887
|
this.ctx.logger.warn("Failed to persist RTSP tokens", { meta: { error: require_dist.errMsg(err) } });
|
|
@@ -38556,24 +38914,22 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38556
38914
|
this.ctx.logger.warn("Failed to persist device overrides", { meta: { error: require_dist.errMsg(err) } });
|
|
38557
38915
|
});
|
|
38558
38916
|
});
|
|
38559
|
-
const profileMap =
|
|
38917
|
+
const profileMap = profileMapMirror(deviceSettings, this.ctx.logger);
|
|
38560
38918
|
const profileMapPersister = (assignments) => {
|
|
38561
38919
|
const next = /* @__PURE__ */ new Map();
|
|
38562
|
-
for (const [deviceId, assignment] of assignments) next.set(
|
|
38920
|
+
for (const [deviceId, assignment] of assignments) next.set(deviceId, {
|
|
38563
38921
|
map: { ...assignment.map },
|
|
38564
38922
|
auto: assignment.auto
|
|
38565
38923
|
});
|
|
38566
|
-
profileMap.
|
|
38924
|
+
profileMap.replace(next).catch((err) => {
|
|
38567
38925
|
this.ctx.logger.warn("Failed to persist profile map", { meta: { error: require_dist.errMsg(err) } });
|
|
38568
38926
|
});
|
|
38569
38927
|
};
|
|
38570
38928
|
this.brokerManager.setProfileMapPersister(profileMapPersister);
|
|
38571
38929
|
try {
|
|
38572
|
-
const rows = await profileMap.
|
|
38930
|
+
const rows = await profileMap.load();
|
|
38573
38931
|
const loaded = /* @__PURE__ */ new Map();
|
|
38574
|
-
for (const [
|
|
38575
|
-
const deviceId = Number(rawKey);
|
|
38576
|
-
if (!Number.isInteger(deviceId)) continue;
|
|
38932
|
+
for (const [deviceId, val] of rows) {
|
|
38577
38933
|
const map = {};
|
|
38578
38934
|
for (const profile of PROFILE_KEYS) {
|
|
38579
38935
|
const v = val.map[profile];
|
|
@@ -38589,18 +38945,18 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38589
38945
|
} catch (err) {
|
|
38590
38946
|
this.ctx.logger.warn("Failed to load profile map", { meta: { error: require_dist.errMsg(err) } });
|
|
38591
38947
|
}
|
|
38592
|
-
const probeSnapshots =
|
|
38948
|
+
const probeSnapshots = probeSnapshotsMirror(deviceSettings, this.ctx.logger);
|
|
38593
38949
|
this.probeSnapshots = probeSnapshots;
|
|
38594
38950
|
try {
|
|
38595
|
-
const rows = await probeSnapshots.
|
|
38596
|
-
const blob = Object.fromEntries(rows);
|
|
38951
|
+
const rows = await probeSnapshots.load();
|
|
38952
|
+
const blob = Object.fromEntries(flattenByDevice(rows));
|
|
38597
38953
|
this.brokerManager.loadProbeSnapshots(blob);
|
|
38598
|
-
this.ctx.logger.info("Loaded persisted probe snapshots", { meta: { streamCount:
|
|
38954
|
+
this.ctx.logger.info("Loaded persisted probe snapshots", { meta: { streamCount: Object.keys(blob).length } });
|
|
38599
38955
|
} catch (err) {
|
|
38600
38956
|
this.ctx.logger.warn("Failed to load probe snapshots", { meta: { error: require_dist.errMsg(err) } });
|
|
38601
38957
|
}
|
|
38602
38958
|
this.brokerManager.setDeviceHardwareForgetHook(async (deviceId) => {
|
|
38603
|
-
const rtspTokenRowsDeleted = await rtspTokens.
|
|
38959
|
+
const rtspTokenRowsDeleted = await rtspTokens.remove(deviceId) ? 1 : 0;
|
|
38604
38960
|
this.flushProbeSnapshots(Date.now(), true);
|
|
38605
38961
|
return { rtspTokenRowsDeleted };
|
|
38606
38962
|
});
|
|
@@ -38858,6 +39214,14 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38858
39214
|
} catch (err) {
|
|
38859
39215
|
this.ctx.logger.warn("stream-broker: client-log ingest failed to serve", { meta: { error: require_dist.errMsg(err) } });
|
|
38860
39216
|
}
|
|
39217
|
+
this.removalHook = createBrokerRemovalHook({
|
|
39218
|
+
logger: this.ctx.logger,
|
|
39219
|
+
retractDevice: async (deviceId) => {
|
|
39220
|
+
await retractRemovedDevice(deviceId, this.deviceRemovalDeps());
|
|
39221
|
+
},
|
|
39222
|
+
dropDeviceOverride: (deviceId) => brokerManager.dropDeviceOverride(deviceId),
|
|
39223
|
+
overrideDeviceIds: () => brokerManager.listDeviceOverrideIds()
|
|
39224
|
+
});
|
|
38861
39225
|
return [
|
|
38862
39226
|
{
|
|
38863
39227
|
capability: require_dist.streamBrokerCapability,
|
|
@@ -38876,9 +39240,90 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38876
39240
|
provider: widgetsProvider
|
|
38877
39241
|
},
|
|
38878
39242
|
logChannelsRegistration,
|
|
38879
|
-
loadContributionRegistration
|
|
39243
|
+
loadContributionRegistration,
|
|
39244
|
+
{
|
|
39245
|
+
capability: require_dist.deviceRemovalHookCapability,
|
|
39246
|
+
provider: this.removalHook
|
|
39247
|
+
}
|
|
38880
39248
|
];
|
|
38881
39249
|
}
|
|
39250
|
+
/**
|
|
39251
|
+
* Watch the cameras whose restream tokens are held but unreadable (D870): no
|
|
39252
|
+
* restream URL is registered for them (the manager's gate), an alert says so,
|
|
39253
|
+
* and when a value resolves ONLY that camera's restreamers are registered with
|
|
39254
|
+
* the stored tokens - no teardown. A lost secret is released after two
|
|
39255
|
+
* consistent reads and gets new, persisted tokens.
|
|
39256
|
+
*/
|
|
39257
|
+
startTokenRecovery(mirror, brokerManager) {
|
|
39258
|
+
const secrets = this.ctx.secrets;
|
|
39259
|
+
if (secrets === void 0) {
|
|
39260
|
+
if (mirror.heldDevices().size > 0) this.ctx.logger.error("restream tokens look held but this addon has no keychain access", { meta: { cameras: mirror.heldDevices().size } });
|
|
39261
|
+
return;
|
|
39262
|
+
}
|
|
39263
|
+
brokerManager.setRestreamGate((deviceId) => mirror.isBlocked(deviceId));
|
|
39264
|
+
if (mirror.heldDevices().size === 0) return;
|
|
39265
|
+
const alertId = (deviceId) => `stream-broker-tokens:${deviceId}`;
|
|
39266
|
+
const warn = (what) => (err) => this.ctx.logger.warn(what, { meta: { error: require_dist.errMsg(err) } });
|
|
39267
|
+
const recovery = new TokenRecovery({
|
|
39268
|
+
mirror,
|
|
39269
|
+
secrets,
|
|
39270
|
+
logger: this.ctx.logger,
|
|
39271
|
+
alerts: {
|
|
39272
|
+
raise: async ({ deviceId, kind, message }) => {
|
|
39273
|
+
const now = Date.now();
|
|
39274
|
+
await this.ctx.api.alerts.emit.mutate({
|
|
39275
|
+
id: alertId(deviceId),
|
|
39276
|
+
category: "streams",
|
|
39277
|
+
severity: "warning",
|
|
39278
|
+
title: kind === "missing" ? "New RTSP restream URLs issued" : "RTSP restream URLs unavailable",
|
|
39279
|
+
message,
|
|
39280
|
+
status: "active",
|
|
39281
|
+
read: false,
|
|
39282
|
+
createdAt: now,
|
|
39283
|
+
updatedAt: now,
|
|
39284
|
+
source: {
|
|
39285
|
+
type: "device",
|
|
39286
|
+
id: String(deviceId)
|
|
39287
|
+
},
|
|
39288
|
+
metadata: {
|
|
39289
|
+
deviceId,
|
|
39290
|
+
kind
|
|
39291
|
+
}
|
|
39292
|
+
}).catch(warn("could not raise the restream-token alert"));
|
|
39293
|
+
},
|
|
39294
|
+
clear: async (deviceId) => {
|
|
39295
|
+
await this.ctx.api.alerts.update.mutate({
|
|
39296
|
+
alertId: alertId(deviceId),
|
|
39297
|
+
patch: {
|
|
39298
|
+
status: "completed",
|
|
39299
|
+
updatedAt: Date.now()
|
|
39300
|
+
}
|
|
39301
|
+
}).catch(warn("could not clear the restream-token alert"));
|
|
39302
|
+
}
|
|
39303
|
+
},
|
|
39304
|
+
onRegister: async (tokens, cameras) => {
|
|
39305
|
+
brokerManager.loadPersistedTokens(flattenByDevice(tokens));
|
|
39306
|
+
for (const deviceId of cameras) brokerManager.registerRestreamersForDevice(deviceId);
|
|
39307
|
+
}
|
|
39308
|
+
});
|
|
39309
|
+
recovery.start();
|
|
39310
|
+
const unsubscribe = secrets.onChange((ref) => {
|
|
39311
|
+
if (ref.startsWith("secret://stream-broker/device/")) recovery.tick();
|
|
39312
|
+
});
|
|
39313
|
+
this.ctx.addDisposer(() => {
|
|
39314
|
+
recovery.stop();
|
|
39315
|
+
unsubscribe();
|
|
39316
|
+
});
|
|
39317
|
+
}
|
|
39318
|
+
async onHubReachable() {
|
|
39319
|
+
const removalHook = this.removalHook;
|
|
39320
|
+
if (removalHook === null) return;
|
|
39321
|
+
this.ctx.addDisposer(require_dist.armRemovalReconcile({
|
|
39322
|
+
api: this.ctx.api,
|
|
39323
|
+
logger: this.ctx.logger,
|
|
39324
|
+
remove: (input) => removalHook.onDeviceRemoved(input)
|
|
39325
|
+
}));
|
|
39326
|
+
}
|
|
38882
39327
|
async onShutdown() {
|
|
38883
39328
|
this.logChannels?.stop();
|
|
38884
39329
|
this.logChannels = null;
|
|
@@ -38979,41 +39424,14 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38979
39424
|
this.flushProbeSnapshots(timestamp, false);
|
|
38980
39425
|
}
|
|
38981
39426
|
/**
|
|
38982
|
-
*
|
|
38983
|
-
*
|
|
38984
|
-
*
|
|
38985
|
-
*
|
|
38986
|
-
* Not swallowed: a broker whose `stream-broker:rtsp-tokens` is undeclared
|
|
38987
|
-
* mints a new token for every stream and every external NVR URL in the
|
|
38988
|
-
* installation goes dead. Crashing loudly is bounded by `CrashSupervisor`
|
|
38989
|
-
* (D6); silently reissuing tokens is not.
|
|
38990
|
-
*/
|
|
38991
|
-
async declareRowCollections() {
|
|
38992
|
-
const store = this.ctx.api.settingsStore;
|
|
38993
|
-
const logger = this.ctx.logger;
|
|
38994
|
-
await probeSnapshotsStore(store, logger).declare();
|
|
38995
|
-
await rtspTokensStore(store, logger).declare();
|
|
38996
|
-
await profileMapStore(store, logger).declare();
|
|
38997
|
-
}
|
|
38998
|
-
/**
|
|
38999
|
-
* Drop the flattened keys from `stream-broker:addon-settings` / `root`, each
|
|
39000
|
-
* gated on its own successor collection being non-empty. `rtspEnabled` and
|
|
39001
|
-
* `deviceOverrides` stay — see `broker-row-stores.ts` for why.
|
|
39002
|
-
*
|
|
39003
|
-
* Best-effort: a purge that throws must not take the broker down. The keys
|
|
39004
|
-
* are dead weight, not a fault.
|
|
39427
|
+
* The per-device settings, as the broker persists through them. Resolved
|
|
39428
|
+
* reads/writes (keychain markers never reach this code), and a context with
|
|
39429
|
+
* no settings is a broker that cannot start.
|
|
39005
39430
|
*/
|
|
39006
|
-
|
|
39007
|
-
|
|
39008
|
-
|
|
39009
|
-
|
|
39010
|
-
addonId: STREAM_BROKER_ADDON_ID,
|
|
39011
|
-
logger: this.ctx.logger,
|
|
39012
|
-
specs: STREAM_BROKER_RETIRED_ROOT_KEYS
|
|
39013
|
-
});
|
|
39014
|
-
} catch (err) {
|
|
39015
|
-
this.ctx.logger.warn("stream-broker: retiring the flattened settings keys failed", { meta: { error: require_dist.errMsg(err) } });
|
|
39016
|
-
}
|
|
39431
|
+
deviceSettings() {
|
|
39432
|
+
const settings = this.ctx.settings;
|
|
39433
|
+
if (settings === void 0) throw new Error("stream-broker: device settings are not available");
|
|
39434
|
+
return settings;
|
|
39017
39435
|
}
|
|
39018
39436
|
/**
|
|
39019
39437
|
* Persist the manager's probe map to the durable handle. Throttled to
|
|
@@ -39026,7 +39444,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
39026
39444
|
if (!store || !manager) return;
|
|
39027
39445
|
if (!force && now - this.lastProbePersistAt < PROBE_SNAPSHOTS_PERSIST_INTERVAL_MS) return;
|
|
39028
39446
|
this.lastProbePersistAt = now;
|
|
39029
|
-
store.
|
|
39447
|
+
store.replace(groupByDevice(manager.getProbeSnapshots())).catch((err) => {
|
|
39030
39448
|
this.ctx.logger.warn("Failed to persist probe snapshots", { meta: { error: require_dist.errMsg(err) } });
|
|
39031
39449
|
});
|
|
39032
39450
|
}
|