@camstack/addon-pipeline 1.2.372 → 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-C6C2cSwd.mjs → custom-model-catalog-1LDuDw8S.mjs} +2 -2
- package/dist/{custom-model-catalog-n9v8MUL6.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-DmuX7IFJ.js → dist-CBEUVjTS.js} +197 -29
- package/dist/{dist-8AEiKOFO.mjs → dist-CmdLCb_F.mjs} +192 -30
- package/dist/{model-catalogs-BSWAHZJh.mjs → model-catalogs-BnCdB2A3.mjs} +1 -1
- package/dist/{model-catalogs-DO4aA1qo.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-Bd4va7c-.mjs → node-C3r8kiOo.mjs} +1 -1
- package/dist/{node-DqA6nD1z.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-2OQ2oxrz.js → process-memory-GFR7fxpJ.js} +1 -1
- package/dist/{process-memory-BvS2Z88w.mjs → process-memory-X0ws1SGJ.mjs} +1 -1
- package/dist/recorder/index.js +267 -94
- package/dist/recorder/index.mjs +265 -92
- package/dist/{segment-demux-js-BkGK1rhu.js → segment-demux-js-CSQVhbFI.js} +1 -1
- package/dist/{segment-demux-js-By9MLn_g.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-DjoAY42S.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-nua-KuDg.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-DPiGNc5u.mjs → hostInit-BOp1_Hhk.mjs} +2 -2
- package/dist/stream-broker/index.js +1087 -334
- package/dist/stream-broker/index.mjs +1087 -334
- 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-BkGK1rhu.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
|
/**
|
|
@@ -3089,6 +3134,223 @@ var RtpPacketizer = class {
|
|
|
3089
3134
|
};
|
|
3090
3135
|
}
|
|
3091
3136
|
};
|
|
3137
|
+
function isTransferable(buf) {
|
|
3138
|
+
return typeof Reflect.get(buf, "transfer") === "function";
|
|
3139
|
+
}
|
|
3140
|
+
var Slab = class {
|
|
3141
|
+
now;
|
|
3142
|
+
buffer;
|
|
3143
|
+
offset = 0;
|
|
3144
|
+
holds = 0;
|
|
3145
|
+
state = "active";
|
|
3146
|
+
releasedAt = 0;
|
|
3147
|
+
constructor(buffer, now) {
|
|
3148
|
+
this.now = now;
|
|
3149
|
+
this.buffer = buffer;
|
|
3150
|
+
}
|
|
3151
|
+
};
|
|
3152
|
+
/** ArrayBuffer -> slab, for every slab ArrayBuffer currently in service. */
|
|
3153
|
+
var slabOf = /* @__PURE__ */ new WeakMap();
|
|
3154
|
+
var slabsLive = 0;
|
|
3155
|
+
var slabsFree = 0;
|
|
3156
|
+
var slabAllocs = 0;
|
|
3157
|
+
var slabRecycles = 0;
|
|
3158
|
+
function readSlabTotals() {
|
|
3159
|
+
return {
|
|
3160
|
+
live: slabsLive,
|
|
3161
|
+
free: slabsFree,
|
|
3162
|
+
allocs: slabAllocs,
|
|
3163
|
+
recycles: slabRecycles
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3166
|
+
/** True when `buf` is a view of a pool slab. */
|
|
3167
|
+
function isSlabView(buf) {
|
|
3168
|
+
return slabOf.has(buf.buffer);
|
|
3169
|
+
}
|
|
3170
|
+
/** A retaining structure keeps `buf`: its slab cannot be recycled until released. */
|
|
3171
|
+
function holdPacket(buf) {
|
|
3172
|
+
const slab = slabOf.get(buf.buffer);
|
|
3173
|
+
if (slab === void 0) return;
|
|
3174
|
+
slab.holds += 1;
|
|
3175
|
+
if (slab.state === "quarantined") slab.state = "held";
|
|
3176
|
+
}
|
|
3177
|
+
/** The matching drop. Safe on a non-slab buffer and on a stale (recycled) view. */
|
|
3178
|
+
function releasePacket(buf) {
|
|
3179
|
+
const slab = slabOf.get(buf.buffer);
|
|
3180
|
+
if (slab === void 0 || slab.holds === 0) return;
|
|
3181
|
+
slab.holds -= 1;
|
|
3182
|
+
if (slab.holds === 0 && slab.state === "held") {
|
|
3183
|
+
slab.state = "quarantined";
|
|
3184
|
+
slab.releasedAt = slab.now();
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
function releasePackets(bufs) {
|
|
3188
|
+
for (const b of bufs) releasePacket(b);
|
|
3189
|
+
}
|
|
3190
|
+
/**
|
|
3191
|
+
* A consumer's own hold on the last `limit` packets it forwarded.
|
|
3192
|
+
*
|
|
3193
|
+
* werift keeps the last RTP_HISTORY_SIZE (4096) packets of each sender for NACK
|
|
3194
|
+
* retransmission, and those packets' payloads are views of the broker's slabs.
|
|
3195
|
+
* The session mirrors that window here: each forwarded packet is held, and
|
|
3196
|
+
* the one falling out of the window is released, so a retransmit asked for
|
|
3197
|
+
* within the history is served from bytes nobody has recycled.
|
|
3198
|
+
*/
|
|
3199
|
+
var HeldPacketWindow = class {
|
|
3200
|
+
limit;
|
|
3201
|
+
slots;
|
|
3202
|
+
next = 0;
|
|
3203
|
+
constructor(limit) {
|
|
3204
|
+
this.limit = limit;
|
|
3205
|
+
this.slots = new Array(limit).fill(void 0);
|
|
3206
|
+
}
|
|
3207
|
+
hold(buf) {
|
|
3208
|
+
holdPacket(buf);
|
|
3209
|
+
const evicted = this.slots[this.next];
|
|
3210
|
+
if (evicted !== void 0) releasePacket(evicted);
|
|
3211
|
+
this.slots[this.next] = buf;
|
|
3212
|
+
this.next = (this.next + 1) % this.limit;
|
|
3213
|
+
}
|
|
3214
|
+
releaseAll() {
|
|
3215
|
+
for (let i = 0; i < this.slots.length; i++) {
|
|
3216
|
+
const held = this.slots[i];
|
|
3217
|
+
if (held !== void 0) releasePacket(held);
|
|
3218
|
+
this.slots[i] = void 0;
|
|
3219
|
+
}
|
|
3220
|
+
this.next = 0;
|
|
3221
|
+
}
|
|
3222
|
+
};
|
|
3223
|
+
var PacketSlabPool = class {
|
|
3224
|
+
slabBytes;
|
|
3225
|
+
maxPacket;
|
|
3226
|
+
quarantineMs;
|
|
3227
|
+
maxFree;
|
|
3228
|
+
now;
|
|
3229
|
+
active = null;
|
|
3230
|
+
/** Sealed slabs, oldest first; entries may have been re-held since. */
|
|
3231
|
+
sealed = [];
|
|
3232
|
+
free = [];
|
|
3233
|
+
allocs = 0;
|
|
3234
|
+
recycles = 0;
|
|
3235
|
+
ownBuffers = 0;
|
|
3236
|
+
constructor(options = {}) {
|
|
3237
|
+
this.slabBytes = options.slabBytes ?? 65536;
|
|
3238
|
+
this.maxPacket = this.slabBytes >> 2;
|
|
3239
|
+
this.quarantineMs = options.quarantineMs ?? 2e3;
|
|
3240
|
+
this.maxFree = options.maxFree ?? 2;
|
|
3241
|
+
this.now = options.now ?? Date.now;
|
|
3242
|
+
}
|
|
3243
|
+
/**
|
|
3244
|
+
* A copy of `src[start, end)` in this pool's active slab. An oversized
|
|
3245
|
+
* packet gets its own buffer (it owns its ArrayBuffer, like the old
|
|
3246
|
+
* `copyOwned`).
|
|
3247
|
+
*/
|
|
3248
|
+
copy(src, start, end) {
|
|
3249
|
+
const length = end - start;
|
|
3250
|
+
recordCopyOwned(length);
|
|
3251
|
+
if (length > this.maxPacket) {
|
|
3252
|
+
this.ownBuffers += 1;
|
|
3253
|
+
const own = Buffer.allocUnsafeSlow(length);
|
|
3254
|
+
src.copy(own, 0, start, end);
|
|
3255
|
+
return own;
|
|
3256
|
+
}
|
|
3257
|
+
let slab = this.active;
|
|
3258
|
+
if (slab === null || slab.offset + length > this.slabBytes) slab = this.rotate();
|
|
3259
|
+
const view = Buffer.from(slab.buffer, slab.offset, length);
|
|
3260
|
+
src.copy(view, 0, start, end);
|
|
3261
|
+
slab.offset += length;
|
|
3262
|
+
return view;
|
|
3263
|
+
}
|
|
3264
|
+
/**
|
|
3265
|
+
* `buf` itself when it is already owned (a slab view or a whole-ArrayBuffer
|
|
3266
|
+
* buffer), else a copy in the active slab. Used for packets that arrive from
|
|
3267
|
+
* paths that do not cut through {@link copy} (push-rtp).
|
|
3268
|
+
*/
|
|
3269
|
+
adopt(buf) {
|
|
3270
|
+
if (isSlabView(buf)) return buf;
|
|
3271
|
+
if (buf.byteOffset === 0 && buf.buffer.byteLength === buf.length) return buf;
|
|
3272
|
+
return this.copy(buf, 0, buf.length);
|
|
3273
|
+
}
|
|
3274
|
+
stats() {
|
|
3275
|
+
return {
|
|
3276
|
+
live: this.sealed.length + (this.active === null ? 0 : 1),
|
|
3277
|
+
free: this.free.length,
|
|
3278
|
+
allocs: this.allocs,
|
|
3279
|
+
recycles: this.recycles,
|
|
3280
|
+
ownBuffers: this.ownBuffers
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
/** Drop every slab of this pool from the process counters (stream torn down). */
|
|
3284
|
+
dispose() {
|
|
3285
|
+
const live = this.sealed.length + (this.active === null ? 0 : 1);
|
|
3286
|
+
slabsLive -= live;
|
|
3287
|
+
slabsFree -= this.free.length;
|
|
3288
|
+
this.sealed = [];
|
|
3289
|
+
this.free = [];
|
|
3290
|
+
this.active = null;
|
|
3291
|
+
}
|
|
3292
|
+
rotate() {
|
|
3293
|
+
const previous = this.active;
|
|
3294
|
+
if (previous !== null) {
|
|
3295
|
+
this.sealed.push(previous);
|
|
3296
|
+
if (previous.holds === 0) {
|
|
3297
|
+
previous.state = "quarantined";
|
|
3298
|
+
previous.releasedAt = this.now();
|
|
3299
|
+
} else previous.state = "held";
|
|
3300
|
+
}
|
|
3301
|
+
this.reclaim();
|
|
3302
|
+
const next = this.free.pop() ?? this.allocate();
|
|
3303
|
+
if (next.state === "free") {
|
|
3304
|
+
slabsFree -= 1;
|
|
3305
|
+
slabsLive += 1;
|
|
3306
|
+
}
|
|
3307
|
+
next.state = "active";
|
|
3308
|
+
next.offset = 0;
|
|
3309
|
+
next.holds = 0;
|
|
3310
|
+
this.active = next;
|
|
3311
|
+
return next;
|
|
3312
|
+
}
|
|
3313
|
+
allocate() {
|
|
3314
|
+
this.allocs += 1;
|
|
3315
|
+
slabAllocs += 1;
|
|
3316
|
+
slabsLive += 1;
|
|
3317
|
+
const slab = new Slab(new ArrayBuffer(this.slabBytes), this.now);
|
|
3318
|
+
slabOf.set(slab.buffer, slab);
|
|
3319
|
+
return slab;
|
|
3320
|
+
}
|
|
3321
|
+
/** Move quarantined slabs whose time is up to the free list. */
|
|
3322
|
+
reclaim() {
|
|
3323
|
+
if (this.sealed.length === 0) return;
|
|
3324
|
+
const cutoff = this.now() - this.quarantineMs;
|
|
3325
|
+
const keep = [];
|
|
3326
|
+
for (const slab of this.sealed) if (slab.state === "quarantined" && slab.releasedAt <= cutoff) this.recycle(slab);
|
|
3327
|
+
else keep.push(slab);
|
|
3328
|
+
this.sealed = keep;
|
|
3329
|
+
}
|
|
3330
|
+
recycle(slab) {
|
|
3331
|
+
slabsLive -= 1;
|
|
3332
|
+
if (this.free.length >= this.maxFree) {
|
|
3333
|
+
slabOf.delete(slab.buffer);
|
|
3334
|
+
slab.state = "free";
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
const old = slab.buffer;
|
|
3338
|
+
slabOf.delete(old);
|
|
3339
|
+
slab.buffer = isTransferable(old) ? old.transfer() : new ArrayBuffer(this.slabBytes);
|
|
3340
|
+
if (!isTransferable(old)) {
|
|
3341
|
+
this.allocs += 1;
|
|
3342
|
+
slabAllocs += 1;
|
|
3343
|
+
}
|
|
3344
|
+
slabOf.set(slab.buffer, slab);
|
|
3345
|
+
slab.state = "free";
|
|
3346
|
+
slab.offset = 0;
|
|
3347
|
+
slab.holds = 0;
|
|
3348
|
+
this.free.push(slab);
|
|
3349
|
+
slabsFree += 1;
|
|
3350
|
+
this.recycles += 1;
|
|
3351
|
+
slabRecycles += 1;
|
|
3352
|
+
}
|
|
3353
|
+
};
|
|
3092
3354
|
//#endregion
|
|
3093
3355
|
//#region src/stream-broker/stream-broker/owned-buffer.ts
|
|
3094
3356
|
/**
|
|
@@ -3115,10 +3377,20 @@ function ownsBackingStore(buf) {
|
|
|
3115
3377
|
}
|
|
3116
3378
|
/**
|
|
3117
3379
|
* What retaining `buf` really costs: its own length when it owns its
|
|
3118
|
-
* ArrayBuffer
|
|
3380
|
+
* ArrayBuffer (a packet slab is densely packed, so a slab view counts as its
|
|
3381
|
+
* own length too), the whole shared slab when it is a view of a pooled one.
|
|
3382
|
+
* The diagnostic `BackingTally` still sums the distinct ArrayBuffers.
|
|
3119
3383
|
*/
|
|
3120
3384
|
function backingStoreBytes(buf) {
|
|
3121
|
-
return ownsBackingStore(buf) ? buf.length : buf.buffer.byteLength;
|
|
3385
|
+
return ownsBackingStore(buf) || isSlabView(buf) ? buf.length : buf.buffer.byteLength;
|
|
3386
|
+
}
|
|
3387
|
+
/**
|
|
3388
|
+
* What `buf` PINS: its own length when it is the whole of its ArrayBuffer, the
|
|
3389
|
+
* whole ArrayBuffer otherwise — a packet slab view included. This is the
|
|
3390
|
+
* diagnostic figure (`BackingTally`); caps charge {@link backingStoreBytes}.
|
|
3391
|
+
*/
|
|
3392
|
+
function pinnedBackingBytes(buf) {
|
|
3393
|
+
return buf.byteOffset === 0 && buf.buffer.byteLength === buf.length ? buf.length : buf.buffer.byteLength;
|
|
3122
3394
|
}
|
|
3123
3395
|
/**
|
|
3124
3396
|
* `buf` itself when it already owns its ArrayBuffer (decided by inspecting the
|
|
@@ -3167,7 +3439,7 @@ var BackingTally = class {
|
|
|
3167
3439
|
}
|
|
3168
3440
|
this.entries += 1;
|
|
3169
3441
|
this.payload += buf.length;
|
|
3170
|
-
const backing =
|
|
3442
|
+
const backing = pinnedBackingBytes(buf);
|
|
3171
3443
|
this.naive += backing;
|
|
3172
3444
|
if (!this.seen.has(buf.buffer)) {
|
|
3173
3445
|
this.seen.add(buf.buffer);
|
|
@@ -3460,6 +3732,7 @@ function formatBrokerMem(input) {
|
|
|
3460
3732
|
const w = input.worker;
|
|
3461
3733
|
if (w !== void 0 && w !== null) parts.push(`workers=${w.workers}`, `workerHeapMb=${mb(w.heapBytes)}`, `workerPhysicalMb=${mb(w.physicalBytes)}`, `workerUsedMb=${mb(w.usedBytes)}`, `workerAgeS=${Math.round(w.maxAgeMs / 1e3)}`, `workerIdleExit=${w.idleTerminates ? "yes" : "never"}`);
|
|
3462
3734
|
parts.push(`brokers=${samples.length}`, `ingesting=${ingesting}`, `annexBPlanes=${planes}`, `rtsp=${rtsp}`, `webrtc=${input.webrtcSessions}`, `encoded=${kindList(consumers)}`, `audioSubs=${audio}`, `pipeClients=${pipe}`, `egress=${input.egressTranscodes}`, `children=${input.childProcesses}`, `opened=${churn.opened}`, `closed=${churn.closed}`, `openedKinds=${kindList(churn.openedByKind)}`, `closedKinds=${kindList(churn.closedByKind)}`, `preBufferMb=${mb(pre)}`, `clipMb=${mb(clip)}`, `rtpRingMb=${mb(rtp)}`, `keyframeMb=${mb(kf)}`, `writeQueueSumMb=${mb(qSum)}`, `writeQueueMaxMb=${mb(qMax)}`, `copiedPackets=${copied.packets}`, `copiedMb=${mb(copied.bytes)}`, `topRingMb=${top === "" ? "none" : top}`);
|
|
3735
|
+
if (input.slabs !== void 0) parts.push(`slabs=${input.slabs.live}/${input.slabs.free}`, `slabAllocs=${input.slabs.allocsDelta}`);
|
|
3463
3736
|
const sessions = input.sessions;
|
|
3464
3737
|
if (sessions !== void 0) parts.push(...formatSessionMem(sessions, input));
|
|
3465
3738
|
return parts.join(" ");
|
|
@@ -3849,8 +4122,7 @@ var RtspRestreamer = class {
|
|
|
3849
4122
|
this.mutedSdp = null;
|
|
3850
4123
|
this.deframer = null;
|
|
3851
4124
|
this.detectedCodec = null;
|
|
3852
|
-
this.
|
|
3853
|
-
this.lastKeyframeRtpBytes = 0;
|
|
4125
|
+
this.dropKeyframeRtpCache();
|
|
3854
4126
|
this._rtpPassthroughMode = false;
|
|
3855
4127
|
this.collectingKeyframeRtp = false;
|
|
3856
4128
|
this.egressAudioBlock = null;
|
|
@@ -4203,21 +4475,21 @@ var RtspRestreamer = class {
|
|
|
4203
4475
|
if (isKeyframe || isParameterSet) {
|
|
4204
4476
|
if (!this.collectingKeyframeRtp) {
|
|
4205
4477
|
this.collectingKeyframeRtp = true;
|
|
4206
|
-
this.
|
|
4207
|
-
this.lastKeyframeRtpBytes = 0;
|
|
4478
|
+
this.dropKeyframeRtpCache();
|
|
4208
4479
|
}
|
|
4209
4480
|
}
|
|
4210
4481
|
if (this.collectingKeyframeRtp && !isKeyframe && !isParameterSet) this.collectingKeyframeRtp = false;
|
|
4211
4482
|
if (this.collectingKeyframeRtp) {
|
|
4483
|
+
const cached = isSlabView(rtpData) ? rtpData : detachFromPool(rtpData);
|
|
4484
|
+
holdPacket(cached);
|
|
4212
4485
|
this.lastKeyframeRtpPackets.push({
|
|
4213
|
-
data:
|
|
4486
|
+
data: cached,
|
|
4214
4487
|
marker: rtpPacket.marker
|
|
4215
4488
|
});
|
|
4216
4489
|
this.lastKeyframeRtpBytes += rtpData.length;
|
|
4217
4490
|
if (this.lastKeyframeRtpBytes > MAX_KEYFRAME_CACHE_BYTES) {
|
|
4218
4491
|
this.warnCacheCapExceeded("rtp", this.lastKeyframeRtpBytes, this.lastKeyframeRtpPackets.length);
|
|
4219
|
-
this.
|
|
4220
|
-
this.lastKeyframeRtpBytes = 0;
|
|
4492
|
+
this.dropKeyframeRtpCache();
|
|
4221
4493
|
this.collectingKeyframeRtp = false;
|
|
4222
4494
|
}
|
|
4223
4495
|
}
|
|
@@ -4655,6 +4927,7 @@ var RtspRestreamer = class {
|
|
|
4655
4927
|
}
|
|
4656
4928
|
destroy() {
|
|
4657
4929
|
this._onSessionCountChanged = null;
|
|
4930
|
+
this.dropKeyframeRtpCache();
|
|
4658
4931
|
this.sentryAudioSilence?.release("restreamer destroyed");
|
|
4659
4932
|
this.abortAllReplays();
|
|
4660
4933
|
this.deframer?.flush();
|
|
@@ -4663,6 +4936,12 @@ var RtspRestreamer = class {
|
|
|
4663
4936
|
this.packetizer = null;
|
|
4664
4937
|
this.deframer = null;
|
|
4665
4938
|
}
|
|
4939
|
+
/** Empty the keyframe RTP cache and let go of the slabs it pinned. */
|
|
4940
|
+
dropKeyframeRtpCache() {
|
|
4941
|
+
for (const cached of this.lastKeyframeRtpPackets) releasePacket(cached.data);
|
|
4942
|
+
this.lastKeyframeRtpPackets = [];
|
|
4943
|
+
this.lastKeyframeRtpBytes = 0;
|
|
4944
|
+
}
|
|
4666
4945
|
/** Called by deframer for each complete NAL unit. */
|
|
4667
4946
|
handleNal(nal, isKeyframe) {
|
|
4668
4947
|
if (!this.codecParams) this.tryInitCodec(nal);
|
|
@@ -4792,6 +5071,13 @@ function resolveJoinBurstBound(input) {
|
|
|
4792
5071
|
};
|
|
4793
5072
|
}
|
|
4794
5073
|
//#endregion
|
|
5074
|
+
//#region src/stream-broker/stream-broker/held-write.ts
|
|
5075
|
+
/** Write `payload`, holding its slab until the write completes (or fails). */
|
|
5076
|
+
function writeHeld(sink, payload) {
|
|
5077
|
+
holdPacket(payload);
|
|
5078
|
+
return sink.write(payload, () => releasePacket(payload));
|
|
5079
|
+
}
|
|
5080
|
+
//#endregion
|
|
4795
5081
|
//#region src/stream-broker/rtsp/rtsp-session.ts
|
|
4796
5082
|
/**
|
|
4797
5083
|
* Handles one RTSP client connection.
|
|
@@ -5107,15 +5393,17 @@ var RtspSession = class {
|
|
|
5107
5393
|
*
|
|
5108
5394
|
* ## Why sharing the payload is safe, and what would make it unsafe
|
|
5109
5395
|
*
|
|
5110
|
-
* The payload
|
|
5111
|
-
*
|
|
5112
|
-
*
|
|
5113
|
-
*
|
|
5114
|
-
*
|
|
5115
|
-
*
|
|
5116
|
-
*
|
|
5117
|
-
*
|
|
5118
|
-
*
|
|
5396
|
+
* The payload is a view of the broker's packet slab (`slab-pool.ts`, D868),
|
|
5397
|
+
* never of the reader's socket buffer, which advances on the next read.
|
|
5398
|
+
* Nothing in the broker writes into a captured packet: every mutation in this
|
|
5399
|
+
* path targets a freshly allocated buffer. A slab is RECYCLED once the
|
|
5400
|
+
* broker's rings let go of it, and a queued socket write outlives that: Node
|
|
5401
|
+
* keeps the chunk behind a full kernel buffer for as long as the peer takes to
|
|
5402
|
+
* drain (seconds, for a recorder or a LAN decoder), and libuv reads the bytes
|
|
5403
|
+
* only when it writes. So the payload goes out through `writeHeld`, which
|
|
5404
|
+
* holds its slab until Node's write callback (success, error or destroy). A
|
|
5405
|
+
* write that skipped the hold would send a header promising N bytes followed
|
|
5406
|
+
* by zeros.
|
|
5119
5407
|
*
|
|
5120
5408
|
* The rule to keep: **`socket.write` forbids MUTATION before the write
|
|
5121
5409
|
* completes, not retention.** So the header, which IS built per call, must
|
|
@@ -5152,7 +5440,7 @@ var RtspSession = class {
|
|
|
5152
5440
|
process.nextTick(this.uncorkAccessUnit);
|
|
5153
5441
|
}
|
|
5154
5442
|
this.socket.write(header);
|
|
5155
|
-
this.socket
|
|
5443
|
+
writeHeld(this.socket, payload);
|
|
5156
5444
|
if (endOfAccessUnit) this.uncorkAccessUnit();
|
|
5157
5445
|
this.lastRtpAt = Date.now();
|
|
5158
5446
|
this.bytesSent += 4 + payload.length;
|
|
@@ -5289,6 +5577,18 @@ var RtspListenServer = class RtspListenServer {
|
|
|
5289
5577
|
brokerTokens = /* @__PURE__ */ new Map();
|
|
5290
5578
|
/** token → brokerId (reverse map) */
|
|
5291
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;
|
|
5292
5592
|
/** brokerId → enabled (default true) */
|
|
5293
5593
|
brokerEnabled = /* @__PURE__ */ new Map();
|
|
5294
5594
|
/** brokerId → warm-up closure registered alongside the restreamer */
|
|
@@ -5319,7 +5619,37 @@ var RtspListenServer = class RtspListenServer {
|
|
|
5319
5619
|
if (sentryOpener) this.brokerPrestreamOpeners.set(brokerId, sentryOpener);
|
|
5320
5620
|
return token;
|
|
5321
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
|
+
}
|
|
5322
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
|
+
}
|
|
5323
5653
|
const token = this.brokerTokens.get(brokerId);
|
|
5324
5654
|
if (token) {
|
|
5325
5655
|
this.restreamers.delete(token);
|
|
@@ -5341,6 +5671,17 @@ var RtspListenServer = class RtspListenServer {
|
|
|
5341
5671
|
this.restreamers.set(newToken, restreamer);
|
|
5342
5672
|
this.brokerTokens.set(brokerId, newToken);
|
|
5343
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
|
+
}
|
|
5344
5685
|
return newToken;
|
|
5345
5686
|
}
|
|
5346
5687
|
/** Get the current token for a broker. */
|
|
@@ -5424,6 +5765,11 @@ var RtspListenServer = class RtspListenServer {
|
|
|
5424
5765
|
socket.destroy();
|
|
5425
5766
|
return;
|
|
5426
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
|
+
}
|
|
5427
5773
|
const brokerId = this.tokenBrokers.get(lookupPath);
|
|
5428
5774
|
if (brokerId && !this.isEnabled(brokerId)) {
|
|
5429
5775
|
socket.write(`RTSP/1.0 403 Forbidden\r\nCSeq: 1\r\n\r\n`);
|
|
@@ -5482,6 +5828,24 @@ var RtspListenServer = class RtspListenServer {
|
|
|
5482
5828
|
});
|
|
5483
5829
|
}
|
|
5484
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
|
+
}
|
|
5485
5849
|
//#endregion
|
|
5486
5850
|
//#region src/stream-broker/rtsp/rtsp-restream-provider.ts
|
|
5487
5851
|
/**
|
|
@@ -5563,6 +5927,24 @@ var RtspRestreamProviderImpl = class {
|
|
|
5563
5927
|
enabled: this.server.isEnabled(brokerId)
|
|
5564
5928
|
};
|
|
5565
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
|
+
}
|
|
5566
5948
|
regenerateToken(brokerId) {
|
|
5567
5949
|
const token = this.server.regenerateToken(brokerId);
|
|
5568
5950
|
if (token) {
|
|
@@ -10515,6 +10897,7 @@ var Rfc4571Reader = class {
|
|
|
10515
10897
|
options;
|
|
10516
10898
|
callbacks;
|
|
10517
10899
|
logger;
|
|
10900
|
+
allocatePacket;
|
|
10518
10901
|
host;
|
|
10519
10902
|
port;
|
|
10520
10903
|
authLine;
|
|
@@ -10528,6 +10911,7 @@ var Rfc4571Reader = class {
|
|
|
10528
10911
|
constructor(options, callbacks) {
|
|
10529
10912
|
this.options = options;
|
|
10530
10913
|
this.callbacks = callbacks;
|
|
10914
|
+
this.allocatePacket = options.packetAllocator ?? copyOwned;
|
|
10531
10915
|
this.logger = options.logger;
|
|
10532
10916
|
const parsed = new URL(options.url);
|
|
10533
10917
|
this.host = parsed.hostname;
|
|
@@ -10592,7 +10976,7 @@ var Rfc4571Reader = class {
|
|
|
10592
10976
|
while (this.readBuffer.length >= 2) {
|
|
10593
10977
|
const length = this.readBuffer.readUInt16BE(0);
|
|
10594
10978
|
if (this.readBuffer.length < 2 + length) return;
|
|
10595
|
-
const rtpPacket =
|
|
10979
|
+
const rtpPacket = this.allocatePacket(this.readBuffer, 2, 2 + length);
|
|
10596
10980
|
this.readBuffer = this.readBuffer.subarray(2 + length);
|
|
10597
10981
|
this.dispatchRtp(rtpPacket);
|
|
10598
10982
|
}
|
|
@@ -11113,6 +11497,7 @@ var NativeRtspClient = class {
|
|
|
11113
11497
|
keepaliveIntervalMs;
|
|
11114
11498
|
logger;
|
|
11115
11499
|
callbacks;
|
|
11500
|
+
allocatePacket;
|
|
11116
11501
|
/** Parsed SDP from DESCRIBE response. */
|
|
11117
11502
|
parsedSdp = null;
|
|
11118
11503
|
rawSdpText = null;
|
|
@@ -11139,6 +11524,7 @@ var NativeRtspClient = class {
|
|
|
11139
11524
|
tornDown = false;
|
|
11140
11525
|
constructor(options, callbacks) {
|
|
11141
11526
|
this.callbacks = callbacks;
|
|
11527
|
+
this.allocatePacket = options.packetAllocator ?? copyOwned;
|
|
11142
11528
|
this.logger = options.logger;
|
|
11143
11529
|
this.connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
11144
11530
|
this.keepaliveIntervalMs = options.keepaliveIntervalMs ?? DEFAULT_KEEPALIVE_INTERVAL_MS;
|
|
@@ -11311,7 +11697,7 @@ var NativeRtspClient = class {
|
|
|
11311
11697
|
const channel = this.readBuffer[1];
|
|
11312
11698
|
const length = this.readBuffer.readUInt16BE(2);
|
|
11313
11699
|
if (this.readBuffer.length < 4 + length) return;
|
|
11314
|
-
const payload =
|
|
11700
|
+
const payload = this.allocatePacket(this.readBuffer, 4, 4 + length);
|
|
11315
11701
|
this.readBuffer = this.readBuffer.subarray(4 + length);
|
|
11316
11702
|
this.handleInterleavedPacket(channel, payload);
|
|
11317
11703
|
} else {
|
|
@@ -11766,7 +12152,7 @@ var AudioCodecSession = class {
|
|
|
11766
12152
|
processPacket(rtpData) {
|
|
11767
12153
|
if (this.closed) return;
|
|
11768
12154
|
if (rtpData.length <= RTP_HEADER_LENGTH) return;
|
|
11769
|
-
const payload = rtpData.subarray(RTP_HEADER_LENGTH);
|
|
12155
|
+
const payload = Buffer.from(rtpData.subarray(RTP_HEADER_LENGTH));
|
|
11770
12156
|
this.packetsIn++;
|
|
11771
12157
|
this.ensureSession().then((sid) => {
|
|
11772
12158
|
if (!sid || this.closed) return;
|
|
@@ -13384,6 +13770,13 @@ var StreamBroker = class StreamBroker {
|
|
|
13384
13770
|
* Only populated for RTP sources (`isRtpSource()`).
|
|
13385
13771
|
*/
|
|
13386
13772
|
rtpRing = [];
|
|
13773
|
+
/**
|
|
13774
|
+
* Where this stream's source packets are cut (`slab-pool.ts`, D868): the
|
|
13775
|
+
* readers copy into the pool's slabs, the ring and the restreamer's keyframe
|
|
13776
|
+
* cache hold/release them, and a slab no ring holds is recycled.
|
|
13777
|
+
*/
|
|
13778
|
+
packetPool = new PacketSlabPool();
|
|
13779
|
+
packetAllocator = (src, start, end) => this.packetPool.copy(src, start, end);
|
|
13387
13780
|
rtpRingCurAuStart = 0;
|
|
13388
13781
|
rtpRingPrevMarker = true;
|
|
13389
13782
|
rtpRingHasKeyframe = false;
|
|
@@ -13904,6 +14297,8 @@ var StreamBroker = class StreamBroker {
|
|
|
13904
14297
|
this.annexBAssembler.reset();
|
|
13905
14298
|
this.encodedCallbacks.clear();
|
|
13906
14299
|
this.audioChunkPlane.dispose();
|
|
14300
|
+
this.resetRtpPreBuffer();
|
|
14301
|
+
this.packetPool.dispose();
|
|
13907
14302
|
}
|
|
13908
14303
|
/** Whether the broker is currently suspended (idle, no RTSP connection). */
|
|
13909
14304
|
get suspended() {
|
|
@@ -14658,7 +15053,8 @@ var StreamBroker = class StreamBroker {
|
|
|
14658
15053
|
captureRtpForPreBuffer(rtpData) {
|
|
14659
15054
|
const marker = rtpData.length > 1 && (rtpData[1] & 128) !== 0;
|
|
14660
15055
|
if (this.rtpRingPrevMarker || this.rtpRing.length === 0) this.rtpRingCurAuStart = this.rtpRing.length;
|
|
14661
|
-
const owned =
|
|
15056
|
+
const owned = this.packetPool.adopt(rtpData);
|
|
15057
|
+
holdPacket(owned);
|
|
14662
15058
|
this.rtpRing.push(owned);
|
|
14663
15059
|
this.rtpRingBytes += backingStoreBytes(owned);
|
|
14664
15060
|
if (this._lastNalParamSet || this._lastNalKeyframe) {
|
|
@@ -14754,6 +15150,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14754
15150
|
* non-decodable slice instead of waiting for the new dial's first keyframe.
|
|
14755
15151
|
*/
|
|
14756
15152
|
resetRtpPreBuffer() {
|
|
15153
|
+
releasePackets(this.rtpRing);
|
|
14757
15154
|
this.rtpRing = [];
|
|
14758
15155
|
this.rtpRingBytes = 0;
|
|
14759
15156
|
this.rtpRingCurAuStart = 0;
|
|
@@ -14850,6 +15247,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14850
15247
|
if (cut <= 0) return;
|
|
14851
15248
|
const dropped = this.rtpRing.splice(0, cut);
|
|
14852
15249
|
for (const b of dropped) this.rtpRingBytes -= backingStoreBytes(b);
|
|
15250
|
+
releasePackets(dropped);
|
|
14853
15251
|
this.rtpRingKeyframeMarks = this.rtpRingKeyframeMarks.filter((mark) => mark.index >= cut).map((mark) => ({
|
|
14854
15252
|
index: mark.index - cut,
|
|
14855
15253
|
ts: mark.ts
|
|
@@ -15530,7 +15928,7 @@ var StreamBroker = class StreamBroker {
|
|
|
15530
15928
|
this.audioRtpDecoder?.processPacket(rtpData);
|
|
15531
15929
|
if (this.hasAudioConsumer()) this.audioCodecSession?.processPacket(rtpData);
|
|
15532
15930
|
if ((this.audioCodec === "PCMU" || this.audioCodec === "PCMA") && this.encodedCallbacks.size > 0 && rtpData.length > 12) {
|
|
15533
|
-
const payload = rtpData.subarray(12);
|
|
15931
|
+
const payload = Buffer.from(rtpData.subarray(12));
|
|
15534
15932
|
if (payload.length > 0) {
|
|
15535
15933
|
const rtpTimestamp = rtpData.readUInt32BE(4);
|
|
15536
15934
|
const pts = Math.floor(rtpTimestamp / 8);
|
|
@@ -15593,7 +15991,8 @@ var StreamBroker = class StreamBroker {
|
|
|
15593
15991
|
this.logger?.info("starting native RTSP reader");
|
|
15594
15992
|
const client = new NativeRtspClient({
|
|
15595
15993
|
url: source.url,
|
|
15596
|
-
logger: this.logger?.child("rtsp-native")
|
|
15994
|
+
logger: this.logger?.child("rtsp-native"),
|
|
15995
|
+
packetAllocator: this.packetAllocator
|
|
15597
15996
|
}, this.buildRtpStreamCallbacks("native RTSP", () => this.destroyNativeClient(), () => this.nativeClient !== client));
|
|
15598
15997
|
this.nativeClient = client;
|
|
15599
15998
|
client.connect().catch((err) => {
|
|
@@ -16100,7 +16499,8 @@ var StreamBroker = class StreamBroker {
|
|
|
16100
16499
|
const reader = new Rfc4571Reader({
|
|
16101
16500
|
url: source.url,
|
|
16102
16501
|
sdp,
|
|
16103
|
-
...this.logger ? { logger: this.logger.child("rfc4571") } : {}
|
|
16502
|
+
...this.logger ? { logger: this.logger.child("rfc4571") } : {},
|
|
16503
|
+
packetAllocator: this.packetAllocator
|
|
16104
16504
|
}, this.buildRtpStreamCallbacks("rfc4571", () => this.destroyRfc4571Reader(), () => this.rfc4571Reader !== reader));
|
|
16105
16505
|
this.rfc4571Reader = reader;
|
|
16106
16506
|
reader.connect().catch((err) => {
|
|
@@ -17278,6 +17678,14 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17278
17678
|
rtspProvider;
|
|
17279
17679
|
persistedTokens = /* @__PURE__ */ new Map();
|
|
17280
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
|
+
/**
|
|
17281
17689
|
* Last runtime probe per broker, captured while the broker was streaming.
|
|
17282
17690
|
* Survives the stream going idle so the per-stream "Probed" settings field
|
|
17283
17691
|
* stays populated (with an "as of" tag). In-memory only — repopulates on the
|
|
@@ -17477,7 +17885,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17477
17885
|
hwaccel: this.hwaccel ?? void 0,
|
|
17478
17886
|
cameraStreamLookup: (deviceId) => this.getCameraStreamsForDevice(deviceId),
|
|
17479
17887
|
rtspEntryLookup: (brokerId) => {
|
|
17480
|
-
const entry = this.rtspProvider.
|
|
17888
|
+
const entry = this.rtspProvider.getInternalEntry(brokerId);
|
|
17481
17889
|
return entry ? {
|
|
17482
17890
|
url: entry.url,
|
|
17483
17891
|
mutedUrl: entry.mutedUrl
|
|
@@ -17536,7 +17944,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17536
17944
|
cameraStreamLookup: (deviceId) => this.getCameraStreamsForDevice(deviceId),
|
|
17537
17945
|
resolveProfileSource: (deviceId, profile) => this.assignments.get(deviceId)?.map[profile] ?? null,
|
|
17538
17946
|
rtspEntryLookup: (brokerId) => {
|
|
17539
|
-
const entry = this.rtspProvider.
|
|
17947
|
+
const entry = this.rtspProvider.getInternalEntry(brokerId);
|
|
17540
17948
|
return entry ? { url: entry.url } : null;
|
|
17541
17949
|
},
|
|
17542
17950
|
sourceHasMedia: (brokerId) => {
|
|
@@ -18600,6 +19008,20 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18600
19008
|
loadPersistedTokens(tokens) {
|
|
18601
19009
|
this.persistedTokens = new Map(tokens);
|
|
18602
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
|
+
}
|
|
18603
19025
|
/** Upsert a single SOURCE (non-derived) cam-stream into the registry
|
|
18604
19026
|
* WITHOUT ranking or broker creation. Returns the prior entry (if any)
|
|
18605
19027
|
* so callers can detect url refresh / already-existed. */
|
|
@@ -19343,6 +19765,20 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19343
19765
|
return { success: true };
|
|
19344
19766
|
}
|
|
19345
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
|
+
/**
|
|
19346
19782
|
* The hardware behind `deviceId` was replaced (`deviceManager.migrateDevice`)
|
|
19347
19783
|
* — forget everything this broker knows that described the OLD box. See the
|
|
19348
19784
|
* cap doc for the inventory. What deliberately SURVIVES is the role-scoped
|
|
@@ -20121,6 +20557,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
20121
20557
|
const sourceBrokerId = brokerIdFor(deviceId, camStreamId);
|
|
20122
20558
|
const sourceBroker = this.brokers.get(sourceBrokerId);
|
|
20123
20559
|
if (!sourceBroker) continue;
|
|
20560
|
+
if (this.restreamGate(deviceId)) continue;
|
|
20124
20561
|
const existingToken = this.persistedTokens.get(profileBrokerId);
|
|
20125
20562
|
const warmUp = () => sourceBroker.onEncodedData(() => {}, RTSP_DESCRIBE_WARMUP_ATTRIBUTION);
|
|
20126
20563
|
const openPrestreamWindow = () => sourceBroker.openPrestreamWindow();
|
|
@@ -21780,11 +22217,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21780
22217
|
}
|
|
21781
22218
|
this.brokers.set(brokerId, broker);
|
|
21782
22219
|
this.applyClipRetention(deviceId);
|
|
21783
|
-
|
|
21784
|
-
const warmUp = () => broker.onEncodedData(() => {}, RTSP_DESCRIBE_WARMUP_ATTRIBUTION);
|
|
21785
|
-
const openPrestreamWindow = () => broker.openPrestreamWindow();
|
|
21786
|
-
this.rtspServer.registerRestreamer(brokerId, broker.getRtspRestreamer(), warmUp, existingToken, openPrestreamWindow);
|
|
21787
|
-
if (!existingToken) this.rtspProvider.persistTokens();
|
|
22220
|
+
this.registerSourceRestreamer(brokerId, broker);
|
|
21788
22221
|
await this.fanOutRegistration(deviceId, brokerId, cam, broker);
|
|
21789
22222
|
this.syncProfileRestreamers(deviceId);
|
|
21790
22223
|
if (cam.kind === "derived") {
|
|
@@ -21793,6 +22226,28 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21793
22226
|
}
|
|
21794
22227
|
}
|
|
21795
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
|
+
/**
|
|
21796
22251
|
* Tear down the broker for a `(deviceId, camStreamId)` pair. No-op
|
|
21797
22252
|
* when no broker exists. Called by `retractCameraStream` and by the
|
|
21798
22253
|
* teardown half of `restartBroker`.
|
|
@@ -21912,7 +22367,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21912
22367
|
};
|
|
21913
22368
|
} else {
|
|
21914
22369
|
const sourceBrokerId = brokerIdFor(deviceId, request.camStreamId);
|
|
21915
|
-
const entry = this.rtspProvider.
|
|
22370
|
+
const entry = this.rtspProvider.getInternalEntry(sourceBrokerId, "127.0.0.1");
|
|
21916
22371
|
if (!entry) throw new Error(`derived source camStream loopback unavailable: ${sourceBrokerId}`);
|
|
21917
22372
|
loopbackUrl = entry.url;
|
|
21918
22373
|
release = async () => {};
|
|
@@ -22690,19 +23145,19 @@ var H265Repacketizer = class {
|
|
|
22690
23145
|
return this.codecInfo;
|
|
22691
23146
|
}
|
|
22692
23147
|
updateVps(vps) {
|
|
22693
|
-
this.ensureCodecInfo().vps = vps;
|
|
23148
|
+
this.ensureCodecInfo().vps = Buffer.from(vps);
|
|
22694
23149
|
}
|
|
22695
23150
|
updateSps(sps) {
|
|
22696
|
-
this.ensureCodecInfo().sps = sps;
|
|
23151
|
+
this.ensureCodecInfo().sps = Buffer.from(sps);
|
|
22697
23152
|
}
|
|
22698
23153
|
updatePps(pps) {
|
|
22699
|
-
this.ensureCodecInfo().pps = pps;
|
|
23154
|
+
this.ensureCodecInfo().pps = Buffer.from(pps);
|
|
22700
23155
|
}
|
|
22701
23156
|
updateSeiPrefix(sei) {
|
|
22702
|
-
this.ensureCodecInfo().seiPrefix = sei;
|
|
23157
|
+
this.ensureCodecInfo().seiPrefix = Buffer.from(sei);
|
|
22703
23158
|
}
|
|
22704
23159
|
updateSeiSuffix(sei) {
|
|
22705
|
-
this.ensureCodecInfo().seiSuffix = sei;
|
|
23160
|
+
this.ensureCodecInfo().seiSuffix = Buffer.from(sei);
|
|
22706
23161
|
}
|
|
22707
23162
|
shouldFilter(_nalType) {
|
|
22708
23163
|
return false;
|
|
@@ -23013,6 +23468,48 @@ var H265Repacketizer = class {
|
|
|
23013
23468
|
}
|
|
23014
23469
|
}
|
|
23015
23470
|
};
|
|
23471
|
+
//#endregion
|
|
23472
|
+
//#region src/stream-broker/webrtc/output-arena.ts
|
|
23473
|
+
/**
|
|
23474
|
+
* A bump allocator for the buffers the WebRTC send path builds per packet
|
|
23475
|
+
* (repacketizer outputs, STAP-A aggregates).
|
|
23476
|
+
*
|
|
23477
|
+
* `Buffer.from([...])` and `Buffer.concat` below 4 KiB are served from Node's
|
|
23478
|
+
* shared 8 KiB pool: one ~1.2 KB fragment per pool slab share, interleaved
|
|
23479
|
+
* with every other short-lived pooled buffer in the process. werift keeps the
|
|
23480
|
+
* last RTP_HISTORY_SIZE (4096) outputs of a sender for NACK, so each pinned
|
|
23481
|
+
* the shared slab it came from and everything beside it. Here every output is
|
|
23482
|
+
* a dense view of an arena that holds nothing else.
|
|
23483
|
+
*
|
|
23484
|
+
* Nothing is ever reused: an arena is left to the GC once full, and lives as
|
|
23485
|
+
* long as the last view of it (a NACK slot, a datagram still in flight). That
|
|
23486
|
+
* is why there is no hold/release protocol here, unlike `slab-pool.ts`.
|
|
23487
|
+
*/
|
|
23488
|
+
/** Bytes per arena: ~50 packets of ~1.2 KB per allocation instead of one per 6. */
|
|
23489
|
+
var OUTPUT_ARENA_BYTES = 64 * 1024;
|
|
23490
|
+
/** A request larger than a quarter of an arena gets its own buffer. */
|
|
23491
|
+
var OUTPUT_ARENA_MAX_REQUEST = 16384;
|
|
23492
|
+
var OutputArena = class {
|
|
23493
|
+
chunk = Buffer.allocUnsafeSlow(0);
|
|
23494
|
+
offset = 0;
|
|
23495
|
+
chunks = 0;
|
|
23496
|
+
/** `size` writable bytes (contents unspecified: the caller fills all of them). */
|
|
23497
|
+
take(size) {
|
|
23498
|
+
if (size > OUTPUT_ARENA_MAX_REQUEST) return Buffer.allocUnsafeSlow(size);
|
|
23499
|
+
if (this.offset + size > this.chunk.length) {
|
|
23500
|
+
this.chunk = Buffer.allocUnsafeSlow(OUTPUT_ARENA_BYTES);
|
|
23501
|
+
this.chunks += 1;
|
|
23502
|
+
this.offset = 0;
|
|
23503
|
+
}
|
|
23504
|
+
const out = this.chunk.subarray(this.offset, this.offset + size);
|
|
23505
|
+
this.offset += size;
|
|
23506
|
+
return out;
|
|
23507
|
+
}
|
|
23508
|
+
/** ArrayBuffers allocated by this arena so far. */
|
|
23509
|
+
get allocations() {
|
|
23510
|
+
return this.chunks;
|
|
23511
|
+
}
|
|
23512
|
+
};
|
|
23016
23513
|
var NAL_HEADER_SIZE = 1;
|
|
23017
23514
|
var FU_A_HEADER_SIZE = 2;
|
|
23018
23515
|
var LENGTH_FIELD_SIZE = 2;
|
|
@@ -23057,6 +23554,8 @@ var H264Repacketizer = class {
|
|
|
23057
23554
|
pendingFuA;
|
|
23058
23555
|
stapa;
|
|
23059
23556
|
fuaMin;
|
|
23557
|
+
/** Dense home for every buffer this repacketizer builds (`output-arena.ts`). */
|
|
23558
|
+
arena = new OutputArena();
|
|
23060
23559
|
constructor(console, maxPacketSize, codecInfo, jitterBuffer = new JitterBuffer(console, 4)) {
|
|
23061
23560
|
this.console = console;
|
|
23062
23561
|
this.maxPacketSize = maxPacketSize;
|
|
@@ -23074,70 +23573,78 @@ var H264Repacketizer = class {
|
|
|
23074
23573
|
return this.codecInfo;
|
|
23075
23574
|
}
|
|
23076
23575
|
updateSps(sps) {
|
|
23077
|
-
this.ensureCodecInfo().sps = sps;
|
|
23576
|
+
this.ensureCodecInfo().sps = Buffer.from(sps);
|
|
23078
23577
|
}
|
|
23079
23578
|
updatePps(pps) {
|
|
23080
|
-
this.ensureCodecInfo().pps = pps;
|
|
23579
|
+
this.ensureCodecInfo().pps = Buffer.from(pps);
|
|
23081
23580
|
}
|
|
23082
23581
|
updateSei(sei) {
|
|
23083
|
-
this.ensureCodecInfo().sei = sei;
|
|
23582
|
+
this.ensureCodecInfo().sei = Buffer.from(sei);
|
|
23084
23583
|
}
|
|
23085
23584
|
shouldFilter(_nalType) {
|
|
23086
23585
|
return false;
|
|
23087
23586
|
}
|
|
23088
23587
|
packetizeFuA(data, noStart, noEnd) {
|
|
23588
|
+
let nalHeader;
|
|
23589
|
+
let bodyStart;
|
|
23089
23590
|
if ((data[0] & 31) === 28) {
|
|
23090
|
-
const fnri = data[0] & 224;
|
|
23091
|
-
const originalNalType = data[1] & 31;
|
|
23092
23591
|
const isFuStart = !!(data[1] & 128);
|
|
23093
23592
|
const isFuEnd = !!(data[1] & 64);
|
|
23094
|
-
|
|
23095
|
-
|
|
23096
|
-
data = Buffer.concat([originalNalHeader, data.subarray(FU_A_HEADER_SIZE)]);
|
|
23593
|
+
nalHeader = data[0] & 224 | data[1] & 31;
|
|
23594
|
+
bodyStart = FU_A_HEADER_SIZE;
|
|
23097
23595
|
if (isFuStart) noEnd = true;
|
|
23098
23596
|
else if (isFuEnd) noStart = true;
|
|
23099
|
-
else
|
|
23597
|
+
else {
|
|
23100
23598
|
noStart = true;
|
|
23101
23599
|
noEnd = true;
|
|
23102
23600
|
}
|
|
23601
|
+
} else {
|
|
23602
|
+
nalHeader = data[0];
|
|
23603
|
+
bodyStart = NAL_HEADER_SIZE;
|
|
23103
23604
|
}
|
|
23104
|
-
const
|
|
23105
|
-
const
|
|
23106
|
-
const
|
|
23107
|
-
const
|
|
23108
|
-
const fuHeaderStart = noStart ? fuHeaderMiddle : Buffer.from([fuIndicator, nalType | 128]);
|
|
23109
|
-
const fuHeaderEnd = noEnd ? fuHeaderMiddle : Buffer.from([fuIndicator, nalType | 64]);
|
|
23110
|
-
let fuHeader = fuHeaderStart;
|
|
23605
|
+
const nalType = nalHeader & 31;
|
|
23606
|
+
const fuIndicator = nalHeader & 224 | 28;
|
|
23607
|
+
const startHeader = noStart ? nalType : nalType | 128;
|
|
23608
|
+
const endHeader = noEnd ? nalType : nalType | 64;
|
|
23111
23609
|
const packages = [];
|
|
23112
|
-
let offset =
|
|
23610
|
+
let offset = bodyStart;
|
|
23611
|
+
let first = true;
|
|
23113
23612
|
while (offset < data.length) {
|
|
23114
23613
|
const packageSize = Math.min(this.fuaMax, data.length - offset);
|
|
23115
|
-
const
|
|
23614
|
+
const last = offset + packageSize === data.length;
|
|
23615
|
+
const out = this.arena.take(FU_A_HEADER_SIZE + packageSize);
|
|
23616
|
+
out[0] = fuIndicator;
|
|
23617
|
+
out[1] = last ? endHeader : first ? startHeader : nalType;
|
|
23618
|
+
data.copy(out, FU_A_HEADER_SIZE, offset, offset + packageSize);
|
|
23619
|
+
packages.push(out);
|
|
23116
23620
|
offset += packageSize;
|
|
23117
|
-
|
|
23118
|
-
packages.push(Buffer.concat([fuHeader, payload]));
|
|
23119
|
-
fuHeader = fuHeaderMiddle;
|
|
23621
|
+
first = false;
|
|
23120
23622
|
}
|
|
23121
23623
|
return packages;
|
|
23122
23624
|
}
|
|
23123
23625
|
packetizeOneStapA(datas) {
|
|
23124
|
-
const payload = [];
|
|
23125
23626
|
if (!datas.length) throw new Error("packetizeOneStapA requires at least one NAL");
|
|
23126
|
-
|
|
23627
|
+
const taken = [];
|
|
23127
23628
|
let availableSize = this.maxPacketSize - STAP_A_HEADER_SIZE;
|
|
23128
23629
|
const stapHeader = 24;
|
|
23129
|
-
while (datas.length && datas[0].length + LENGTH_FIELD_SIZE <= availableSize &&
|
|
23630
|
+
while (datas.length && datas[0].length + LENGTH_FIELD_SIZE <= availableSize && taken.length < 9) {
|
|
23130
23631
|
const nalu = datas.shift();
|
|
23131
23632
|
availableSize -= LENGTH_FIELD_SIZE + nalu.length;
|
|
23132
|
-
|
|
23133
|
-
|
|
23134
|
-
|
|
23135
|
-
|
|
23633
|
+
taken.push(nalu);
|
|
23634
|
+
}
|
|
23635
|
+
if (taken.length === 0) return datas.shift();
|
|
23636
|
+
if (taken.length === 1) return taken[0];
|
|
23637
|
+
let total = 1;
|
|
23638
|
+
for (const nalu of taken) total += LENGTH_FIELD_SIZE + nalu.length;
|
|
23639
|
+
const out = this.arena.take(total);
|
|
23640
|
+
out[0] = stapHeader;
|
|
23641
|
+
let offset = 1;
|
|
23642
|
+
for (const nalu of taken) {
|
|
23643
|
+
out.writeUInt16BE(nalu.length, offset);
|
|
23644
|
+
nalu.copy(out, offset + LENGTH_FIELD_SIZE);
|
|
23645
|
+
offset += LENGTH_FIELD_SIZE + nalu.length;
|
|
23136
23646
|
}
|
|
23137
|
-
|
|
23138
|
-
if (counter === 1) return payload[1];
|
|
23139
|
-
payload.unshift(Buffer.from([stapHeader]));
|
|
23140
|
-
return Buffer.concat(payload);
|
|
23647
|
+
return out;
|
|
23141
23648
|
}
|
|
23142
23649
|
packetizeStapA(datas) {
|
|
23143
23650
|
const ret = [];
|
|
@@ -23169,12 +23676,15 @@ var H264Repacketizer = class {
|
|
|
23169
23676
|
const originalNalType = first.payload[1] & 31;
|
|
23170
23677
|
const hasFuStart = !!(first.payload[1] & 128);
|
|
23171
23678
|
const hasFuEnd = !!(last.payload[1] & 64);
|
|
23172
|
-
const
|
|
23173
|
-
const originalNalHeader = Buffer.from([fnri | originalNalType]);
|
|
23679
|
+
const originalNalHeader = first.payload[0] & 224 | originalNalType;
|
|
23174
23680
|
const getDefragmentedPendingFua = () => {
|
|
23175
|
-
|
|
23176
|
-
|
|
23177
|
-
|
|
23681
|
+
let total = NAL_HEADER_SIZE;
|
|
23682
|
+
for (const packet of pending) total += packet.payload.length - FU_A_HEADER_SIZE;
|
|
23683
|
+
const out = this.arena.take(total);
|
|
23684
|
+
out[0] = originalNalHeader;
|
|
23685
|
+
let offset = NAL_HEADER_SIZE;
|
|
23686
|
+
for (const packet of pending) offset += packet.payload.copy(out, offset, FU_A_HEADER_SIZE);
|
|
23687
|
+
return out;
|
|
23178
23688
|
};
|
|
23179
23689
|
if (originalNalType === 7) {
|
|
23180
23690
|
const splits = splitH264NaluStartCode(getDefragmentedPendingFua());
|
|
@@ -23699,6 +24209,8 @@ function selectAudioClock(mimeType) {
|
|
|
23699
24209
|
*/
|
|
23700
24210
|
/** A REMB unchanged this long is no estimate (`RembWatch`). */
|
|
23701
24211
|
var REMB_STALE_MS = 3e4;
|
|
24212
|
+
/** werift's NACK history depth (patched RTP_HISTORY_SIZE, `patches/werift+0.22.9.patch`). */
|
|
24213
|
+
var SOURCE_RTP_HOLD_PACKETS = 4096;
|
|
23702
24214
|
var _werift;
|
|
23703
24215
|
async function loadWerift() {
|
|
23704
24216
|
if (_werift) return _werift;
|
|
@@ -24246,6 +24758,8 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
24246
24758
|
* recovery). While active, live source-RTP packets are enqueued behind
|
|
24247
24759
|
* the replay tail to preserve packet order. Aborted on close/rebind. */
|
|
24248
24760
|
activeRtpReplay = null;
|
|
24761
|
+
/** The source packets werift's NACK history may still read (`slab-pool.ts`). */
|
|
24762
|
+
sourceRtpHold = new HeldPacketWindow(SOURCE_RTP_HOLD_PACKETS);
|
|
24249
24763
|
/**
|
|
24250
24764
|
* Trickle ICE: the server's locally-gathered candidates, buffered as werift
|
|
24251
24765
|
* surfaces them via `onIceCandidate`. The client polls `getIceCandidates`
|
|
@@ -25124,6 +25638,7 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
25124
25638
|
this.activeRtpReplay.abort();
|
|
25125
25639
|
this.activeRtpReplay = null;
|
|
25126
25640
|
}
|
|
25641
|
+
this.sourceRtpHold.releaseAll();
|
|
25127
25642
|
await this.releaseTranscodeEgress();
|
|
25128
25643
|
try {
|
|
25129
25644
|
await this.source.return(void 0);
|
|
@@ -25257,6 +25772,7 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
25257
25772
|
} });
|
|
25258
25773
|
return false;
|
|
25259
25774
|
}
|
|
25775
|
+
this.sourceRtpHold.hold(rtpData);
|
|
25260
25776
|
if (this.sourceVideoSsrc === null) this.sourceVideoSsrc = srcPkt.header.ssrc;
|
|
25261
25777
|
const pt = this.videoSender.codec?.payloadType ?? (codec === "H265" ? 97 : 96);
|
|
25262
25778
|
let outPkts;
|
|
@@ -37862,6 +38378,16 @@ function dialAnswerFromError(err) {
|
|
|
37862
38378
|
}
|
|
37863
38379
|
//#endregion
|
|
37864
38380
|
//#region src/stream-broker/broker-mem-report.ts
|
|
38381
|
+
function readSlabReading() {
|
|
38382
|
+
const totals = readSlabTotals();
|
|
38383
|
+
const allocsDelta = totals.allocs - lastSlabAllocs;
|
|
38384
|
+
lastSlabAllocs = totals.allocs;
|
|
38385
|
+
return {
|
|
38386
|
+
live: totals.live,
|
|
38387
|
+
free: totals.free,
|
|
38388
|
+
allocsDelta
|
|
38389
|
+
};
|
|
38390
|
+
}
|
|
37865
38391
|
/** Linux only: elsewhere (or on a read error) the native view is simply absent. */
|
|
37866
38392
|
async function readProc() {
|
|
37867
38393
|
if (process.platform !== "linux") return {
|
|
@@ -37874,6 +38400,8 @@ async function readProc() {
|
|
|
37874
38400
|
maps
|
|
37875
38401
|
};
|
|
37876
38402
|
}
|
|
38403
|
+
/** `slabAllocs` is a delta: the totals the previous line saw. */
|
|
38404
|
+
var lastSlabAllocs = readSlabTotals().allocs;
|
|
37877
38405
|
async function emitOnce(deps) {
|
|
37878
38406
|
const now = Date.now();
|
|
37879
38407
|
const [proc, worker] = await Promise.all([readProc(), readDemuxWorkerHeaps(now)]);
|
|
@@ -37886,6 +38414,7 @@ async function emitOnce(deps) {
|
|
|
37886
38414
|
proc,
|
|
37887
38415
|
churn: consumerChurn.drain(),
|
|
37888
38416
|
copied: readCopyOwnedTotals(),
|
|
38417
|
+
slabs: readSlabReading(),
|
|
37889
38418
|
v8: readV8Heap(),
|
|
37890
38419
|
worker,
|
|
37891
38420
|
sessions: deps.sessionMem(),
|
|
@@ -37973,18 +38502,177 @@ function subscribeDeviceRemoval(subscribe, deps) {
|
|
|
37973
38502
|
});
|
|
37974
38503
|
}
|
|
37975
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
|
|
37976
38666
|
//#region src/stream-broker/addon.ts
|
|
37977
38667
|
/**
|
|
37978
|
-
* Hydrate the persisted RTSP restream state. Tokens come from
|
|
37979
|
-
*
|
|
37980
|
-
*
|
|
37981
|
-
*
|
|
37982
|
-
*
|
|
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.
|
|
37983
38673
|
*/
|
|
37984
38674
|
async function hydratePersistedRtspState(deps) {
|
|
37985
|
-
const
|
|
37986
|
-
const tokens = /* @__PURE__ */ new Map();
|
|
37987
|
-
for (const [brokerId, row] of rows) tokens.set(brokerId, row.token);
|
|
38675
|
+
const tokens = flattenByDevice(await deps.tokensMirror.load());
|
|
37988
38676
|
if (tokens.size > 0) {
|
|
37989
38677
|
deps.loadTokens(tokens);
|
|
37990
38678
|
deps.logger.info("Loaded persisted RTSP tokens", { meta: { tokenCount: tokens.size } });
|
|
@@ -38053,6 +38741,8 @@ var PROBE_SNAPSHOTS_PERSIST_INTERVAL_MS = 6e4;
|
|
|
38053
38741
|
* the sample as absent (the RTCP Receiver-Report path drives the ladder anyway). */
|
|
38054
38742
|
var NETWORK_QUALITY_STATS_TIMEOUT_MS = 2e3;
|
|
38055
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"];
|
|
38056
38746
|
brokerManager = null;
|
|
38057
38747
|
/** The node's pinned ffmpeg, mirrored here so the recorded-playback demuxers
|
|
38058
38748
|
* (segment-demux/segment-audio) spawn the same binary as the transcode
|
|
@@ -38109,6 +38799,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38109
38799
|
* restarts). Loaded on activate, flushed throttled from the metrics loop and
|
|
38110
38800
|
* once on shutdown. */
|
|
38111
38801
|
probeSnapshots = null;
|
|
38802
|
+
removalHook = null;
|
|
38112
38803
|
lastProbePersistAt = 0;
|
|
38113
38804
|
constructor() {
|
|
38114
38805
|
super({
|
|
@@ -38171,24 +38862,26 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38171
38862
|
logger: this.ctx.logger
|
|
38172
38863
|
});
|
|
38173
38864
|
await this.ffmpegWiring.load();
|
|
38174
|
-
|
|
38175
|
-
await this.purgeFlattenedSettingsKeys();
|
|
38865
|
+
const deviceSettings = this.deviceSettings();
|
|
38176
38866
|
const rtspProvider = this.brokerManager.getRtspRestreamProvider();
|
|
38177
38867
|
const brokerManager = this.brokerManager;
|
|
38178
|
-
const rtspTokens =
|
|
38868
|
+
const rtspTokens = rtspTokensMirror(deviceSettings, this.ctx.logger);
|
|
38179
38869
|
const rtspEnabledState = this.state("rtspEnabled", RtspEnabledSchema, {});
|
|
38180
38870
|
try {
|
|
38181
38871
|
await hydratePersistedRtspState({
|
|
38182
|
-
|
|
38872
|
+
tokensMirror: rtspTokens,
|
|
38183
38873
|
enabledState: rtspEnabledState,
|
|
38184
38874
|
loadTokens: (tokens) => brokerManager.loadPersistedTokens(tokens),
|
|
38185
38875
|
loadEnabled: (states) => rtspProvider.loadPersistedEnabled(states),
|
|
38186
38876
|
logger: this.ctx.logger
|
|
38187
38877
|
});
|
|
38188
|
-
} 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);
|
|
38189
38882
|
rtspProvider.setTokenPersister(async (tokens) => {
|
|
38190
38883
|
try {
|
|
38191
|
-
|
|
38884
|
+
await rtspTokens.merge(mergeRtspTokens(rtspTokens, tokens));
|
|
38192
38885
|
this.ctx.logger.info("Persisted RTSP tokens", { meta: { current: tokens.size } });
|
|
38193
38886
|
} catch (err) {
|
|
38194
38887
|
this.ctx.logger.warn("Failed to persist RTSP tokens", { meta: { error: require_dist.errMsg(err) } });
|
|
@@ -38221,24 +38914,22 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38221
38914
|
this.ctx.logger.warn("Failed to persist device overrides", { meta: { error: require_dist.errMsg(err) } });
|
|
38222
38915
|
});
|
|
38223
38916
|
});
|
|
38224
|
-
const profileMap =
|
|
38917
|
+
const profileMap = profileMapMirror(deviceSettings, this.ctx.logger);
|
|
38225
38918
|
const profileMapPersister = (assignments) => {
|
|
38226
38919
|
const next = /* @__PURE__ */ new Map();
|
|
38227
|
-
for (const [deviceId, assignment] of assignments) next.set(
|
|
38920
|
+
for (const [deviceId, assignment] of assignments) next.set(deviceId, {
|
|
38228
38921
|
map: { ...assignment.map },
|
|
38229
38922
|
auto: assignment.auto
|
|
38230
38923
|
});
|
|
38231
|
-
profileMap.
|
|
38924
|
+
profileMap.replace(next).catch((err) => {
|
|
38232
38925
|
this.ctx.logger.warn("Failed to persist profile map", { meta: { error: require_dist.errMsg(err) } });
|
|
38233
38926
|
});
|
|
38234
38927
|
};
|
|
38235
38928
|
this.brokerManager.setProfileMapPersister(profileMapPersister);
|
|
38236
38929
|
try {
|
|
38237
|
-
const rows = await profileMap.
|
|
38930
|
+
const rows = await profileMap.load();
|
|
38238
38931
|
const loaded = /* @__PURE__ */ new Map();
|
|
38239
|
-
for (const [
|
|
38240
|
-
const deviceId = Number(rawKey);
|
|
38241
|
-
if (!Number.isInteger(deviceId)) continue;
|
|
38932
|
+
for (const [deviceId, val] of rows) {
|
|
38242
38933
|
const map = {};
|
|
38243
38934
|
for (const profile of PROFILE_KEYS) {
|
|
38244
38935
|
const v = val.map[profile];
|
|
@@ -38254,18 +38945,18 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38254
38945
|
} catch (err) {
|
|
38255
38946
|
this.ctx.logger.warn("Failed to load profile map", { meta: { error: require_dist.errMsg(err) } });
|
|
38256
38947
|
}
|
|
38257
|
-
const probeSnapshots =
|
|
38948
|
+
const probeSnapshots = probeSnapshotsMirror(deviceSettings, this.ctx.logger);
|
|
38258
38949
|
this.probeSnapshots = probeSnapshots;
|
|
38259
38950
|
try {
|
|
38260
|
-
const rows = await probeSnapshots.
|
|
38261
|
-
const blob = Object.fromEntries(rows);
|
|
38951
|
+
const rows = await probeSnapshots.load();
|
|
38952
|
+
const blob = Object.fromEntries(flattenByDevice(rows));
|
|
38262
38953
|
this.brokerManager.loadProbeSnapshots(blob);
|
|
38263
|
-
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 } });
|
|
38264
38955
|
} catch (err) {
|
|
38265
38956
|
this.ctx.logger.warn("Failed to load probe snapshots", { meta: { error: require_dist.errMsg(err) } });
|
|
38266
38957
|
}
|
|
38267
38958
|
this.brokerManager.setDeviceHardwareForgetHook(async (deviceId) => {
|
|
38268
|
-
const rtspTokenRowsDeleted = await rtspTokens.
|
|
38959
|
+
const rtspTokenRowsDeleted = await rtspTokens.remove(deviceId) ? 1 : 0;
|
|
38269
38960
|
this.flushProbeSnapshots(Date.now(), true);
|
|
38270
38961
|
return { rtspTokenRowsDeleted };
|
|
38271
38962
|
});
|
|
@@ -38523,6 +39214,14 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38523
39214
|
} catch (err) {
|
|
38524
39215
|
this.ctx.logger.warn("stream-broker: client-log ingest failed to serve", { meta: { error: require_dist.errMsg(err) } });
|
|
38525
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
|
+
});
|
|
38526
39225
|
return [
|
|
38527
39226
|
{
|
|
38528
39227
|
capability: require_dist.streamBrokerCapability,
|
|
@@ -38541,9 +39240,90 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38541
39240
|
provider: widgetsProvider
|
|
38542
39241
|
},
|
|
38543
39242
|
logChannelsRegistration,
|
|
38544
|
-
loadContributionRegistration
|
|
39243
|
+
loadContributionRegistration,
|
|
39244
|
+
{
|
|
39245
|
+
capability: require_dist.deviceRemovalHookCapability,
|
|
39246
|
+
provider: this.removalHook
|
|
39247
|
+
}
|
|
38545
39248
|
];
|
|
38546
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
|
+
}
|
|
38547
39327
|
async onShutdown() {
|
|
38548
39328
|
this.logChannels?.stop();
|
|
38549
39329
|
this.logChannels = null;
|
|
@@ -38644,41 +39424,14 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38644
39424
|
this.flushProbeSnapshots(timestamp, false);
|
|
38645
39425
|
}
|
|
38646
39426
|
/**
|
|
38647
|
-
*
|
|
38648
|
-
*
|
|
38649
|
-
*
|
|
38650
|
-
*
|
|
38651
|
-
* Not swallowed: a broker whose `stream-broker:rtsp-tokens` is undeclared
|
|
38652
|
-
* mints a new token for every stream and every external NVR URL in the
|
|
38653
|
-
* installation goes dead. Crashing loudly is bounded by `CrashSupervisor`
|
|
38654
|
-
* (D6); silently reissuing tokens is not.
|
|
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.
|
|
38655
39430
|
*/
|
|
38656
|
-
|
|
38657
|
-
const
|
|
38658
|
-
|
|
38659
|
-
|
|
38660
|
-
await rtspTokensStore(store, logger).declare();
|
|
38661
|
-
await profileMapStore(store, logger).declare();
|
|
38662
|
-
}
|
|
38663
|
-
/**
|
|
38664
|
-
* Drop the flattened keys from `stream-broker:addon-settings` / `root`, each
|
|
38665
|
-
* gated on its own successor collection being non-empty. `rtspEnabled` and
|
|
38666
|
-
* `deviceOverrides` stay — see `broker-row-stores.ts` for why.
|
|
38667
|
-
*
|
|
38668
|
-
* Best-effort: a purge that throws must not take the broker down. The keys
|
|
38669
|
-
* are dead weight, not a fault.
|
|
38670
|
-
*/
|
|
38671
|
-
async purgeFlattenedSettingsKeys() {
|
|
38672
|
-
try {
|
|
38673
|
-
await require_retire_root_keys.retireRootKeys({
|
|
38674
|
-
store: this.ctx.api.settingsStore,
|
|
38675
|
-
addonId: STREAM_BROKER_ADDON_ID,
|
|
38676
|
-
logger: this.ctx.logger,
|
|
38677
|
-
specs: STREAM_BROKER_RETIRED_ROOT_KEYS
|
|
38678
|
-
});
|
|
38679
|
-
} catch (err) {
|
|
38680
|
-
this.ctx.logger.warn("stream-broker: retiring the flattened settings keys failed", { meta: { error: require_dist.errMsg(err) } });
|
|
38681
|
-
}
|
|
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;
|
|
38682
39435
|
}
|
|
38683
39436
|
/**
|
|
38684
39437
|
* Persist the manager's probe map to the durable handle. Throttled to
|
|
@@ -38691,7 +39444,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
38691
39444
|
if (!store || !manager) return;
|
|
38692
39445
|
if (!force && now - this.lastProbePersistAt < PROBE_SNAPSHOTS_PERSIST_INTERVAL_MS) return;
|
|
38693
39446
|
this.lastProbePersistAt = now;
|
|
38694
|
-
store.
|
|
39447
|
+
store.replace(groupByDevice(manager.getProbeSnapshots())).catch((err) => {
|
|
38695
39448
|
this.ctx.logger.warn("Failed to persist probe snapshots", { meta: { error: require_dist.errMsg(err) } });
|
|
38696
39449
|
});
|
|
38697
39450
|
}
|