@camstack/system 1.2.231 → 1.2.233
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/addon-utils.d.ts +1 -0
- package/dist/addon-utils.js +6 -1
- package/dist/addon-utils.mjs +3 -2
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +17 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +17 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-config-contribution.d.ts +1 -0
- package/dist/builtins/device-manager/device-manager.addon.js +46 -3
- package/dist/builtins/device-manager/device-manager.addon.mjs +46 -3
- package/dist/builtins/device-manager/native-object-detection-config-schema.d.ts +3 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +5 -140
- package/dist/builtins/snapshot/index.mjs +2 -137
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{custom-action-registry-CM03Xda0.mjs → custom-action-registry-CFHYDHAu.mjs} +1 -1
- package/dist/{custom-action-registry-DAFhWd0h.js → custom-action-registry-CWFK5UDj.js} +1 -1
- package/dist/{dist-Zhe9cTnK.mjs → dist-BIvXRexm.mjs} +291 -18
- package/dist/{dist-DUSfUAgz.js → dist-D3lJV9M7.js} +291 -18
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{retired-settings-keys-BK_geSOq.mjs → retired-settings-keys-CV3ZLGnJ.mjs} +1 -1
- package/dist/{retired-settings-keys-D61PxN8L.js → retired-settings-keys-YPsd2Exa.js} +1 -1
- package/dist/snapshot-courtesy-LoJNgHHZ.mjs +140 -0
- package/dist/snapshot-courtesy-oeTWCXPv.js +165 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
let zod = require("zod");
|
|
2
|
-
//#region ../types/dist/event-category-
|
|
2
|
+
//#region ../types/dist/event-category-BVDXG4tB.mjs
|
|
3
3
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4
4
|
EventCategory["SystemBoot"] = "system.boot";
|
|
5
5
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -616,6 +616,20 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
616
616
|
* pull-reconcile from the provider's `getStatus` on reconnect. */
|
|
617
617
|
EventCategory["MeshNetworkChanged"] = "network.mesh.changed";
|
|
618
618
|
EventCategory["BackupCompleted"] = "backup.completed";
|
|
619
|
+
/**
|
|
620
|
+
* A whole backup RUN finished — every destination attempted, win or lose.
|
|
621
|
+
*
|
|
622
|
+
* `backup.completed` fires once per DESTINATION, which is the right grain for
|
|
623
|
+
* a progress UI and the wrong one for a notification: an operator with three
|
|
624
|
+
* destinations would be told three times. And a run where two of three
|
|
625
|
+
* destinations succeeded is not a clean success — a single "backup
|
|
626
|
+
* completed" that hid the failed one would be a lie, so the count of each is
|
|
627
|
+
* carried here and the message says both.
|
|
628
|
+
*
|
|
629
|
+
* Emitted by the backup orchestrator only after the destination loop, so a
|
|
630
|
+
* run that dies during the BUILD phase produces no completion at all.
|
|
631
|
+
*/
|
|
632
|
+
EventCategory["BackupRunCompleted"] = "backup.run-completed";
|
|
619
633
|
EventCategory["BackupRestored"] = "backup.restored";
|
|
620
634
|
EventCategory["NotificationDispatched"] = "notification.dispatched";
|
|
621
635
|
EventCategory["NotificationFailed"] = "notification.failed";
|
|
@@ -676,7 +690,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
676
690
|
return EventCategory;
|
|
677
691
|
}({});
|
|
678
692
|
//#endregion
|
|
679
|
-
//#region ../types/dist/sleep-
|
|
693
|
+
//#region ../types/dist/sleep-BEyvfshj.mjs
|
|
680
694
|
/**
|
|
681
695
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
682
696
|
* window to float samples (D455).
|
|
@@ -8486,6 +8500,72 @@ var cameraPipelineConfigCapability = {
|
|
|
8486
8500
|
exposesDeviceSettings: true,
|
|
8487
8501
|
methods: {}
|
|
8488
8502
|
};
|
|
8503
|
+
/**
|
|
8504
|
+
* The signals a device can emit to WAKE its own stream.
|
|
8505
|
+
*
|
|
8506
|
+
* A camera whose stream is built on demand sleeps until something asks for it,
|
|
8507
|
+
* and "something" cannot be a consumer that is merely attached — a Frigate-style
|
|
8508
|
+
* puller holds a session open for ever, and treating that as demand would keep
|
|
8509
|
+
* a battery camera awake for ever, which is the whole thing the battery is for
|
|
8510
|
+
* (D173). So the wake has to come from the CAMERA: an event it noticed by
|
|
8511
|
+
* itself, with no stream running.
|
|
8512
|
+
*
|
|
8513
|
+
* ## The vocabulary is the PROVIDER'S, not ours
|
|
8514
|
+
*
|
|
8515
|
+
* Like `consumables`, this cap declares no vocabulary of its own. A provider
|
|
8516
|
+
* names each signal with a `code` it chooses and a `label` an operator reads.
|
|
8517
|
+
* Reolink offers motion and camera-native detection; another provider may offer
|
|
8518
|
+
* a tamper, a doorbell press, a PIR, or something no camera in this fleet has
|
|
8519
|
+
* yet. A fixed enum here would mean every new signal is a framework release.
|
|
8520
|
+
*
|
|
8521
|
+
* It is deliberately NOT derived from the caps a device already binds. Whether
|
|
8522
|
+
* a camera CAN push firmware motion is expressed by `motionSources` containing
|
|
8523
|
+
* `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
|
|
8524
|
+
* binding — but both answer "what drives the detection pipeline", which is a
|
|
8525
|
+
* different question from "what may wake a sleeping stream". A camera can do
|
|
8526
|
+
* the first and not be trusted with the second, and the operator picks per
|
|
8527
|
+
* camera. Two questions, two authorities.
|
|
8528
|
+
*
|
|
8529
|
+
* ## Availability is not permission
|
|
8530
|
+
*
|
|
8531
|
+
* `listSignals` says what the device CAN emit. Whether a given signal actually
|
|
8532
|
+
* wakes the stream is the operator's per-camera choice, held by the broker
|
|
8533
|
+
* alongside the cooldown — see the stream-broker cap's wake settings. A
|
|
8534
|
+
* provider declaring a signal is not a provider enabling it.
|
|
8535
|
+
*/
|
|
8536
|
+
/** One signal a device can emit. */
|
|
8537
|
+
var StreamSignalSchema = zod.z.object({
|
|
8538
|
+
/** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
|
|
8539
|
+
code: zod.z.string().min(1),
|
|
8540
|
+
/** What an operator reads in the picker. The provider's own wording. */
|
|
8541
|
+
label: zod.z.string().min(1),
|
|
8542
|
+
/**
|
|
8543
|
+
* Whether the provider recommends this signal ON when a camera is first set
|
|
8544
|
+
* up. A provider knows which of its signals are cheap and reliable; an
|
|
8545
|
+
* operator should not have to discover that by trial. Reolink recommends
|
|
8546
|
+
* both of its own.
|
|
8547
|
+
*/
|
|
8548
|
+
recommended: zod.z.boolean()
|
|
8549
|
+
});
|
|
8550
|
+
var StreamSignalsStatusSchema = zod.z.object({
|
|
8551
|
+
signals: zod.z.array(StreamSignalSchema),
|
|
8552
|
+
lastFetchedAt: zod.z.number()
|
|
8553
|
+
});
|
|
8554
|
+
var streamSignalsCapability = {
|
|
8555
|
+
name: "stream-signals",
|
|
8556
|
+
scope: "device",
|
|
8557
|
+
deviceNative: true,
|
|
8558
|
+
mode: "singleton",
|
|
8559
|
+
deviceTypes: Object.values(DeviceType),
|
|
8560
|
+
runtimeState: StreamSignalsStatusSchema,
|
|
8561
|
+
methods: {
|
|
8562
|
+
/**
|
|
8563
|
+
* What this device can emit. Empty is a valid and common answer — most
|
|
8564
|
+
* cameras have nothing to offer here, and an empty list is what makes the
|
|
8565
|
+
* broker's picker show nothing rather than a false choice.
|
|
8566
|
+
*/
|
|
8567
|
+
listSignals: method(zod.z.void(), zod.z.array(StreamSignalSchema).readonly()) }
|
|
8568
|
+
};
|
|
8489
8569
|
/** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
|
|
8490
8570
|
var StreamFormatSchema = zod.z.enum([
|
|
8491
8571
|
"webrtc",
|
|
@@ -8895,6 +8975,46 @@ var streamBrokerCapability = {
|
|
|
8895
8975
|
mode: "singleton",
|
|
8896
8976
|
exposesDeviceSettings: true,
|
|
8897
8977
|
methods: {
|
|
8978
|
+
/**
|
|
8979
|
+
* A device reporting that one of ITS OWN signals just fired.
|
|
8980
|
+
*
|
|
8981
|
+
* The provider calls this; nothing polls. The broker then decides whether
|
|
8982
|
+
* to wake the source, and the decision has two terms that must BOTH hold:
|
|
8983
|
+
*
|
|
8984
|
+
* - the signal is ENABLED for this camera (the operator's per-camera
|
|
8985
|
+
* choice — a camera may declare a signal the operator does not want
|
|
8986
|
+
* waking anything), and
|
|
8987
|
+
* - a consumer is actually attached and waiting on the prestream. Waking
|
|
8988
|
+
* a camera nobody is watching is the one thing a battery camera must
|
|
8989
|
+
* never do, and a signal arriving with no consumer is exactly that.
|
|
8990
|
+
*
|
|
8991
|
+
* Reported, never inferred: a signal that fires while the stream is
|
|
8992
|
+
* already up refreshes the hold instead of starting a second one, which is
|
|
8993
|
+
* what makes continuous motion keep a camera awake without stacking
|
|
8994
|
+
* timers.
|
|
8995
|
+
*
|
|
8996
|
+
* `code` is the provider's own, from `streamSignals.listSignals`. An
|
|
8997
|
+
* unknown code is REFUSED rather than treated as a wildcard wake — a
|
|
8998
|
+
* provider and an operator disagreeing about a name must not become a
|
|
8999
|
+
* camera that wakes for everything.
|
|
9000
|
+
*/
|
|
9001
|
+
reportStreamSignal: method(zod.z.object({
|
|
9002
|
+
deviceId: zod.z.number().int().nonnegative(),
|
|
9003
|
+
/** The provider's signal code. */
|
|
9004
|
+
code: zod.z.string().min(1),
|
|
9005
|
+
/** Ms epoch. Absent ⇒ now. */
|
|
9006
|
+
at: zod.z.number().optional()
|
|
9007
|
+
}), zod.z.object({
|
|
9008
|
+
/** Whether the broker acted on it, and if not, why. */
|
|
9009
|
+
accepted: zod.z.boolean(),
|
|
9010
|
+
reason: zod.z.enum([
|
|
9011
|
+
"woke",
|
|
9012
|
+
"hold-extended",
|
|
9013
|
+
"not-enabled",
|
|
9014
|
+
"no-consumer",
|
|
9015
|
+
"unknown-code"
|
|
9016
|
+
])
|
|
9017
|
+
}), { kind: "mutation" }),
|
|
8898
9018
|
publishCameraStream: method(zod.z.object({
|
|
8899
9019
|
deviceId: zod.z.number().int().nonnegative(),
|
|
8900
9020
|
camStreamId: zod.z.string().min(1),
|
|
@@ -9372,6 +9492,16 @@ var PickStreamRequirementsSchema = zod.z.object({
|
|
|
9372
9492
|
acceptCodecs: zod.z.array(StreamCodecSchema).readonly().optional(),
|
|
9373
9493
|
/** Minimum vertical resolution. Streams shorter than this are dropped. */
|
|
9374
9494
|
minHeight: zod.z.number().int().positive().optional(),
|
|
9495
|
+
/**
|
|
9496
|
+
* Maximum vertical resolution. Streams TALLER than this are dropped.
|
|
9497
|
+
*
|
|
9498
|
+
* A consumer can have a ceiling as real as its floor: Alexa documents
|
|
9499
|
+
* 480p to 1080p, and a 4K stream is as unusable to an Echo as a 360p one.
|
|
9500
|
+
* Without this the ceiling had to be re-implemented by every caller — and
|
|
9501
|
+
* one caller implementing it privately is how a second scoring authority
|
|
9502
|
+
* gets born.
|
|
9503
|
+
*/
|
|
9504
|
+
maxHeight: zod.z.number().int().positive().optional(),
|
|
9375
9505
|
/** Minimum horizontal resolution. */
|
|
9376
9506
|
minWidth: zod.z.number().int().positive().optional(),
|
|
9377
9507
|
/**
|
|
@@ -9388,7 +9518,22 @@ var PickStreamRequirementsSchema = zod.z.object({
|
|
|
9388
9518
|
* transcoded" guard: if the device is already serving the consumer's
|
|
9389
9519
|
* codec end-to-end, there's nothing to optimise.
|
|
9390
9520
|
*/
|
|
9391
|
-
requireSiblingCodec: zod.z.array(StreamCodecSchema).readonly().optional()
|
|
9521
|
+
requireSiblingCodec: zod.z.array(StreamCodecSchema).readonly().optional(),
|
|
9522
|
+
/**
|
|
9523
|
+
* Whether a stream the consumer CANNOT decode may still be picked, on the
|
|
9524
|
+
* understanding that it will be transcoded.
|
|
9525
|
+
*
|
|
9526
|
+
* Default `false` — today's behaviour, and the right one for a bypass
|
|
9527
|
+
* question ("is there a stream I can forward untouched?"). Set `true` to
|
|
9528
|
+
* ask the larger question: "what is the best stream for me, transcoding if
|
|
9529
|
+
* I must?" A stream that satisfies `acceptCodecs` always outranks one that
|
|
9530
|
+
* does not, so a passthrough is never lost to a transcode; the answer says
|
|
9531
|
+
* which it is in {@link PickedCamStreamSchema.transcodes}.
|
|
9532
|
+
*
|
|
9533
|
+
* This is what lets one picker serve both the bypass and the full source
|
|
9534
|
+
* choice, instead of a consumer scoring privately when the bypass misses.
|
|
9535
|
+
*/
|
|
9536
|
+
allowTranscode: zod.z.boolean().optional()
|
|
9392
9537
|
}).readonly();
|
|
9393
9538
|
var PickStreamPreferencesSchema = zod.z.object({
|
|
9394
9539
|
/**
|
|
@@ -9402,12 +9547,32 @@ var PickStreamPreferencesSchema = zod.z.object({
|
|
|
9402
9547
|
* picks the tallest stream; `'lowest'` picks the shortest (used by
|
|
9403
9548
|
* memory-constrained consumers / Apple Home guest sessions).
|
|
9404
9549
|
*/
|
|
9405
|
-
resolutionPreference: zod.z.enum(["highest", "lowest"]).optional()
|
|
9550
|
+
resolutionPreference: zod.z.enum(["highest", "lowest"]).optional(),
|
|
9551
|
+
/**
|
|
9552
|
+
* The height the consumer actually wants to DELIVER.
|
|
9553
|
+
*
|
|
9554
|
+
* Not a constraint — an ordering. With it set, the SMALLEST stream at or
|
|
9555
|
+
* above the target wins, and only if nothing reaches it does the tallest
|
|
9556
|
+
* take over. Pulling 1296 lines to draw 720 on a 1280x800 Echo panel costs
|
|
9557
|
+
* a decode and buys nothing, and `resolutionPreference: 'highest'` cannot
|
|
9558
|
+
* express that: it says "as big as possible", which is a different wish.
|
|
9559
|
+
*
|
|
9560
|
+
* Ignored when absent, so every existing caller keeps its ordering.
|
|
9561
|
+
*/
|
|
9562
|
+
targetHeight: zod.z.number().int().positive().optional()
|
|
9406
9563
|
}).readonly();
|
|
9407
9564
|
var PickedCamStreamSchema = zod.z.object({
|
|
9408
9565
|
camStreamId: zod.z.string(),
|
|
9409
9566
|
codec: zod.z.string().optional(),
|
|
9410
9567
|
resolution: CamStreamResolutionSchema.optional(),
|
|
9568
|
+
/**
|
|
9569
|
+
* Whether serving this stream requires a decode + re-encode.
|
|
9570
|
+
*
|
|
9571
|
+
* `false` is a stream the consumer can take as it stands. Only ever `true`
|
|
9572
|
+
* when the caller asked for it with `allowTranscode`, so a caller that did
|
|
9573
|
+
* not ask cannot be handed a cost it never agreed to pay.
|
|
9574
|
+
*/
|
|
9575
|
+
transcodes: zod.z.boolean(),
|
|
9411
9576
|
/** One-line explanation of why this stream won — for logs / debug UI. */
|
|
9412
9577
|
reason: zod.z.string()
|
|
9413
9578
|
});
|
|
@@ -15484,6 +15649,8 @@ var NcSystemEventKindSchema = zod.z.enum([
|
|
|
15484
15649
|
"device-enabled",
|
|
15485
15650
|
"device-battery-low",
|
|
15486
15651
|
"device-battery-normal",
|
|
15652
|
+
"device-consumable-low",
|
|
15653
|
+
"device-consumable-normal",
|
|
15487
15654
|
"stream-online",
|
|
15488
15655
|
"stream-offline",
|
|
15489
15656
|
"node-online",
|
|
@@ -15502,6 +15669,7 @@ var NcSystemEventKindSchema = zod.z.enum([
|
|
|
15502
15669
|
"addon-updated",
|
|
15503
15670
|
"server-updated",
|
|
15504
15671
|
"export-completed",
|
|
15672
|
+
"backup-completed",
|
|
15505
15673
|
"camera-online",
|
|
15506
15674
|
"camera-offline",
|
|
15507
15675
|
"camera-disabled",
|
|
@@ -24943,10 +25111,28 @@ var webrtcSessionCapability = {
|
|
|
24943
25111
|
getSessionState: method(zod.z.object({
|
|
24944
25112
|
deviceId: zod.z.number().int().nonnegative(),
|
|
24945
25113
|
sessionId: zod.z.string()
|
|
24946
|
-
}), zod.z.object({
|
|
24947
|
-
|
|
24948
|
-
|
|
24949
|
-
|
|
25114
|
+
}), zod.z.object({
|
|
25115
|
+
pendingRenegotiation: zod.z.object({
|
|
25116
|
+
target: WebrtcStreamTargetSchema,
|
|
25117
|
+
epoch: zod.z.number()
|
|
25118
|
+
}).nullable(),
|
|
25119
|
+
/**
|
|
25120
|
+
* Whether the session still EXISTS.
|
|
25121
|
+
*
|
|
25122
|
+
* A consumer that holds a resource for the life of a session needs to
|
|
25123
|
+
* be able to ask, because a session does not always end the way it
|
|
25124
|
+
* began. Measured on this hub 2026-09-13: an Echo that got stuck never
|
|
25125
|
+
* sent `SessionDisconnected`, so the Alexa exporter's
|
|
25126
|
+
* `releaseEgressTranscode` never ran, and a 2304x1296 HEVC to 720p
|
|
25127
|
+
* H.264 transcode kept running for NOBODY for more than twenty
|
|
25128
|
+
* minutes. The WebRTC session had logged `WebRTC session closed`
|
|
25129
|
+
* minutes earlier — the broker knew; the holder had no way to ask.
|
|
25130
|
+
*
|
|
25131
|
+
* `false` for a session id the provider has never heard of, which is
|
|
25132
|
+
* the same answer as "it ended": either way nothing is holding it up.
|
|
25133
|
+
*/
|
|
25134
|
+
alive: zod.z.boolean()
|
|
25135
|
+
}))
|
|
24950
25136
|
},
|
|
24951
25137
|
/** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
|
|
24952
25138
|
mount: { kind: "server-provided" }
|
|
@@ -29995,11 +30181,39 @@ var NativeObjectDetectionStatusSchema = zod.z.object({
|
|
|
29995
30181
|
supportedClasses: zod.z.array(NativeObjectClassEnum).readonly(),
|
|
29996
30182
|
/**
|
|
29997
30183
|
* Whether forwarding of onboard AI detections is enabled for this device.
|
|
29998
|
-
*
|
|
29999
|
-
*
|
|
30184
|
+
*
|
|
30185
|
+
* The COLD-START default is the provider's, and it is not one value for
|
|
30186
|
+
* every camera: a mains camera carries the boxed side-channel and its
|
|
30187
|
+
* detections are usable subjects, so it defaults ON; a battery camera never
|
|
30188
|
+
* attaches that channel, so the same toggle would buy it nothing but a write
|
|
30189
|
+
* — it defaults OFF and an operator who wants it turns it on knowing what it
|
|
30190
|
+
* costs. {@link NativeObjectDetectionOptionsSchema.geometry} is how the UI
|
|
30191
|
+
* says which of the two a camera is.
|
|
30000
30192
|
*/
|
|
30001
30193
|
enabled: zod.z.boolean()
|
|
30002
30194
|
});
|
|
30195
|
+
/**
|
|
30196
|
+
* What a camera's onboard detections actually CARRY.
|
|
30197
|
+
*
|
|
30198
|
+
* `boxed` — the firmware ships a bounding box with the class, so a detection is
|
|
30199
|
+
* a subject the pipeline can track, crop and score.
|
|
30200
|
+
* `flag-only` — the firmware ships the class and nothing else. The pipeline
|
|
30201
|
+
* needs geometry to make a subject out of it, so such a push reaches the
|
|
30202
|
+
* tracker as motion and no further. On Reolink this is every battery camera:
|
|
30203
|
+
* the boxes ride the BcMedia sub-stream, which a battery camera never attaches.
|
|
30204
|
+
*
|
|
30205
|
+
* Reported and not inferred, because the operator's question in front of the
|
|
30206
|
+
* toggle is "what do I get", and "a class with no box" and "a subject" are
|
|
30207
|
+
* different answers (D14: the derived form is only as honest as `getOptions`).
|
|
30208
|
+
*/
|
|
30209
|
+
var NativeObjectGeometryEnum = zod.z.enum(["boxed", "flag-only"]);
|
|
30210
|
+
var NativeObjectDetectionOptionsSchema = zod.z.object({
|
|
30211
|
+
/** Classes this firmware can detect — the same list the status reports. */
|
|
30212
|
+
supportedClasses: zod.z.array(NativeObjectClassEnum).readonly(),
|
|
30213
|
+
/** What a detection from this camera carries. */
|
|
30214
|
+
geometry: NativeObjectGeometryEnum
|
|
30215
|
+
});
|
|
30216
|
+
var NativeObjectDetectionSettingsPatchSchema = zod.z.object({ enabled: zod.z.boolean().optional() });
|
|
30003
30217
|
var NativeObjectDetectionRuntimeStateSchema = NativeObjectDetectionStatusSchema.extend({
|
|
30004
30218
|
/** Required by createRuntimeStateBridge — epoch ms of last refresh. */
|
|
30005
30219
|
lastFetchedAt: zod.z.number() });
|
|
@@ -30009,13 +30223,28 @@ var nativeObjectDetectionCapability = {
|
|
|
30009
30223
|
deviceNative: true,
|
|
30010
30224
|
mode: "singleton",
|
|
30011
30225
|
deviceTypes: [DeviceType.Camera],
|
|
30012
|
-
|
|
30013
|
-
|
|
30014
|
-
|
|
30015
|
-
|
|
30016
|
-
|
|
30017
|
-
|
|
30018
|
-
|
|
30226
|
+
deviceConfig: { ui: {
|
|
30227
|
+
kind: "derived-form",
|
|
30228
|
+
builderId: "native-object-detection",
|
|
30229
|
+
tab: "motion"
|
|
30230
|
+
} },
|
|
30231
|
+
methods: {
|
|
30232
|
+
getOptions: method(zod.z.object({ deviceId: zod.z.number() }), NativeObjectDetectionOptionsSchema),
|
|
30233
|
+
setSettings: method(zod.z.object({
|
|
30234
|
+
deviceId: zod.z.number(),
|
|
30235
|
+
settings: NativeObjectDetectionSettingsPatchSchema
|
|
30236
|
+
}), zod.z.void(), {
|
|
30237
|
+
kind: "mutation",
|
|
30238
|
+
auth: "admin"
|
|
30239
|
+
}),
|
|
30240
|
+
setEnabled: method(zod.z.object({
|
|
30241
|
+
deviceId: zod.z.number(),
|
|
30242
|
+
enabled: zod.z.boolean()
|
|
30243
|
+
}), zod.z.void(), {
|
|
30244
|
+
kind: "mutation",
|
|
30245
|
+
auth: "admin"
|
|
30246
|
+
})
|
|
30247
|
+
},
|
|
30019
30248
|
events: { onDetected: { data: zod.z.object({
|
|
30020
30249
|
deviceId: zod.z.number(),
|
|
30021
30250
|
detection: NativeDetectionSchema
|
|
@@ -36969,6 +37198,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
|
|
|
36969
37198
|
streamBrokerCapability,
|
|
36970
37199
|
streamCatalogCapability,
|
|
36971
37200
|
streamParamsCapability,
|
|
37201
|
+
streamSignalsCapability,
|
|
36972
37202
|
switchCapability,
|
|
36973
37203
|
systemCapability,
|
|
36974
37204
|
tamperCapability,
|
|
@@ -39747,12 +39977,24 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
39747
39977
|
addonId: null,
|
|
39748
39978
|
access: "create"
|
|
39749
39979
|
},
|
|
39980
|
+
"nativeObjectDetection.getOptions": {
|
|
39981
|
+
capName: "native-object-detection",
|
|
39982
|
+
capScope: "device",
|
|
39983
|
+
addonId: null,
|
|
39984
|
+
access: "view"
|
|
39985
|
+
},
|
|
39750
39986
|
"nativeObjectDetection.setEnabled": {
|
|
39751
39987
|
capName: "native-object-detection",
|
|
39752
39988
|
capScope: "device",
|
|
39753
39989
|
addonId: null,
|
|
39754
39990
|
access: "create"
|
|
39755
39991
|
},
|
|
39992
|
+
"nativeObjectDetection.setSettings": {
|
|
39993
|
+
capName: "native-object-detection",
|
|
39994
|
+
capScope: "device",
|
|
39995
|
+
addonId: null,
|
|
39996
|
+
access: "create"
|
|
39997
|
+
},
|
|
39756
39998
|
"navigation.getFeatures": {
|
|
39757
39999
|
capName: "navigation",
|
|
39758
40000
|
capScope: "device",
|
|
@@ -42411,6 +42653,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
42411
42653
|
addonId: null,
|
|
42412
42654
|
access: "create"
|
|
42413
42655
|
},
|
|
42656
|
+
"streamBroker.reportStreamSignal": {
|
|
42657
|
+
capName: "stream-broker",
|
|
42658
|
+
capScope: "system",
|
|
42659
|
+
addonId: null,
|
|
42660
|
+
access: "create"
|
|
42661
|
+
},
|
|
42414
42662
|
"streamBroker.restartProfile": {
|
|
42415
42663
|
capName: "stream-broker",
|
|
42416
42664
|
capScope: "system",
|
|
@@ -42495,6 +42743,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
42495
42743
|
addonId: null,
|
|
42496
42744
|
access: "create"
|
|
42497
42745
|
},
|
|
42746
|
+
"streamSignals.listSignals": {
|
|
42747
|
+
capName: "stream-signals",
|
|
42748
|
+
capScope: "device",
|
|
42749
|
+
addonId: null,
|
|
42750
|
+
access: "view"
|
|
42751
|
+
},
|
|
42498
42752
|
"switch.setState": {
|
|
42499
42753
|
capName: "switch",
|
|
42500
42754
|
capScope: "device",
|
|
@@ -43967,11 +44221,21 @@ Object.freeze({
|
|
|
43967
44221
|
form: "single",
|
|
43968
44222
|
optional: false
|
|
43969
44223
|
}],
|
|
44224
|
+
"nativeObjectDetection.getOptions": [{
|
|
44225
|
+
name: "deviceId",
|
|
44226
|
+
form: "single",
|
|
44227
|
+
optional: false
|
|
44228
|
+
}],
|
|
43970
44229
|
"nativeObjectDetection.setEnabled": [{
|
|
43971
44230
|
name: "deviceId",
|
|
43972
44231
|
form: "single",
|
|
43973
44232
|
optional: false
|
|
43974
44233
|
}],
|
|
44234
|
+
"nativeObjectDetection.setSettings": [{
|
|
44235
|
+
name: "deviceId",
|
|
44236
|
+
form: "single",
|
|
44237
|
+
optional: false
|
|
44238
|
+
}],
|
|
43975
44239
|
"navigation.getFeatures": [{
|
|
43976
44240
|
name: "deviceId",
|
|
43977
44241
|
form: "single",
|
|
@@ -44867,6 +45131,11 @@ Object.freeze({
|
|
|
44867
45131
|
form: "single",
|
|
44868
45132
|
optional: false
|
|
44869
45133
|
}],
|
|
45134
|
+
"streamBroker.reportStreamSignal": [{
|
|
45135
|
+
name: "deviceId",
|
|
45136
|
+
form: "single",
|
|
45137
|
+
optional: false
|
|
45138
|
+
}],
|
|
44870
45139
|
"streamBroker.restartProfile": [{
|
|
44871
45140
|
name: "deviceId",
|
|
44872
45141
|
form: "single",
|
|
@@ -45099,7 +45368,7 @@ Object.freeze({
|
|
|
45099
45368
|
});
|
|
45100
45369
|
/**
|
|
45101
45370
|
* Per-cap runtime-state policy, projected from every `*.cap.ts` that declares
|
|
45102
|
-
* `runtimeState`.
|
|
45371
|
+
* `runtimeState`. 68 caps: 35 `restored`, 33 `session`.
|
|
45103
45372
|
*
|
|
45104
45373
|
* A cap absent from this map has no runtime-state slice at all. A cap PRESENT
|
|
45105
45374
|
* with `durability: 'session'` has one and has decided not to keep it.
|
|
@@ -45325,6 +45594,10 @@ var RUNTIME_STATE_POLICY = {
|
|
|
45325
45594
|
durability: "restored",
|
|
45326
45595
|
volatileFields: ["lastFetchedAt"]
|
|
45327
45596
|
},
|
|
45597
|
+
"stream-signals": {
|
|
45598
|
+
durability: "session",
|
|
45599
|
+
volatileFields: []
|
|
45600
|
+
},
|
|
45328
45601
|
"switch": {
|
|
45329
45602
|
durability: "restored",
|
|
45330
45603
|
volatileFields: ["lastChangedAt"]
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("./dist-
|
|
3
|
+
const require_dist = require("./dist-D3lJV9M7.js");
|
|
4
4
|
const require_builtins_alerts_alerts_addon = require("./builtins/alerts/alerts.addon.js");
|
|
5
5
|
require("./builtins/alerts/index.js");
|
|
6
6
|
const require_formatter = require("./formatter-DqAKDlvN.js");
|
|
@@ -29,7 +29,7 @@ const require_event_loop_stall_monitor = require("./event-loop-stall-monitor-CAs
|
|
|
29
29
|
const require_tls$1 = require("./tls-BFs4PzoW.js");
|
|
30
30
|
const require_manifest_system_deps = require("./manifest-system-deps-CrSP0Kkp.js");
|
|
31
31
|
const require_resource_monitor = require("./resource-monitor-LVE1BoLs.js");
|
|
32
|
-
const require_custom_action_registry = require("./custom-action-registry-
|
|
32
|
+
const require_custom_action_registry = require("./custom-action-registry-CWFK5UDj.js");
|
|
33
33
|
let zod = require("zod");
|
|
34
34
|
let node_crypto = require("node:crypto");
|
|
35
35
|
let _camstack_types_node = require("@camstack/types/node");
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM$1, r as __exportAll, t as __commonJSMin$1 } from "./chunk-CNf5ZN-e.mjs";
|
|
2
|
-
import { A as addonSettingsCapability, Dt as procedureAuthKey, Ft as scopesAllowDeviceCap, Jt as errMsg$1, Mt as resolveMethodAuth, Pt as scopesAllowAddon, Qt as DEVICE_STATUS_METHOD, Xt as DATAPLANE_SECRET_HEADER$1, Zt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, _ as RUNTIME_DEFAULTS, _n as readinessKey, _t as logLevelAtMost, an as asJsonObject$1, at as isArrayOutputSchema, cn as createEvent, ct as isObjectInput, dn as expandCapMethods, dt as isVoidInput, ft as kebabToCamel, gn as parseJsonUnknown, h as METHOD_ACCESS_MAP, hn as parseJsonObject, ln as emitDownForOwnedCaps, mt as lifecycleJobSchema, nn as ReadinessRegistry, on as asNumber, ot as isCollectionArrayMethod, p as LOG_LEVEL_RANK, rn as ReadinessTimeoutError, sn as asString$1, t as ALL_CAPABILITY_DEFINITIONS, tt as extractNestedAddonId, vn as resolveCapMount, vt as looseSchema, wt as objectInputDeclaresAddonId, xn as EventCategory$1, yn as scopeKey } from "./dist-
|
|
2
|
+
import { A as addonSettingsCapability, Dt as procedureAuthKey, Ft as scopesAllowDeviceCap, Jt as errMsg$1, Mt as resolveMethodAuth, Pt as scopesAllowAddon, Qt as DEVICE_STATUS_METHOD, Xt as DATAPLANE_SECRET_HEADER$1, Zt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, _ as RUNTIME_DEFAULTS, _n as readinessKey, _t as logLevelAtMost, an as asJsonObject$1, at as isArrayOutputSchema, cn as createEvent, ct as isObjectInput, dn as expandCapMethods, dt as isVoidInput, ft as kebabToCamel, gn as parseJsonUnknown, h as METHOD_ACCESS_MAP, hn as parseJsonObject, ln as emitDownForOwnedCaps, mt as lifecycleJobSchema, nn as ReadinessRegistry, on as asNumber, ot as isCollectionArrayMethod, p as LOG_LEVEL_RANK, rn as ReadinessTimeoutError, sn as asString$1, t as ALL_CAPABILITY_DEFINITIONS, tt as extractNestedAddonId, vn as resolveCapMount, vt as looseSchema, wt as objectInputDeclaresAddonId, xn as EventCategory$1, yn as scopeKey } from "./dist-BIvXRexm.mjs";
|
|
3
3
|
import { AlertCenterAddon } from "./builtins/alerts/alerts.addon.mjs";
|
|
4
4
|
import "./builtins/alerts/index.mjs";
|
|
5
5
|
import { t as formatLogLine } from "./formatter-B7qW8bPJ.mjs";
|
|
@@ -28,7 +28,7 @@ import { _ as fetchJson, a as createFileDataPlaneHandler, c as parseRangeHeader,
|
|
|
28
28
|
import { C as registerLanHttpHandler, S as readLanHttpState, _ as DEFAULT_LAN_HTTP_PORT, a as writeTlsMode, b as bindPendingLanHttp, c as loadTlsCert, d as collectCertIdentity, f as CA_VALIDITY_DAYS, g as evaluateExistingCert, h as SERVER_AUTH_OID, i as writeExtraSans, l as reissueTlsLeaf, m as MAX_LEAF_VALIDITY_DAYS, n as readTlsAccessStatus, o as validateUploadedTls, p as LEAF_RENEWAL_WINDOW_DAYS, r as readTlsMode, s as ensureTlsCert, t as readExtraSans, u as CA_COMMON_NAME, v as allFamiliesListenHost, x as closeLanHttp, y as applyLanHttp } from "./tls-2xbgg78V.mjs";
|
|
29
29
|
import { $ as buildUdsNativeCapProxy, $t as ARRAY_BUFFER_CENSUS_THRESHOLD_MB, A as createUdsLoggerWithControl, An as recordSocketFrame, At as HUB_MAIN_HEAP_WATCH_LABEL, B as readMoleculerFanoutMode, Bn as diffCapActionCensus, Bt as createLoopDelayMeter, C as ipcParentLink, Cn as createSocketPlaneMeter, Ct as runNpm, D as createUdsEventBus, Dn as readSocketQueues, Dt as HEAP_RECLAIM_TRIGGER_MB, E as createUdsEventBridge, En as formatSocketPlane, Et as HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS, F as CapRouteError, Fn as CAP_ACTION_UNLABELLED, Ft as RUNNER_HEAP_WATCH_INTERVAL_MS, G as createLocalTransport, Gn as formatEventPlane, Gt as parseRssBudgetMb, H as PARENT_ROUTED_PROVIDER_ADDON_ID, Hn as createEventPlaneMeter, Ht as describeRss, I as classifyCapRoute, In as EMPTY_CAP_ACTION_READING, It as RUNNER_RSS_BUDGET_ENV, J as SocketChannel, Jt as startHeapWatch, K as UdsLocalTransportClient, Kt as reclaimIntervalMs, L as LocalChildClient, Ln as MAX_CAP_ACTIONS_PER_PEER, Lt as ZERO_LOOP_DELAY, M as AGENT_CAP_FWD_SERVICE, Mn as socketDirectionBytes, Mt as HUB_RSS_BUDGET_ENV, N as CapRouteResolver, Nn as socketDirectionMessages, Nt as RSS_BUDGET_REANNOUNCE_MIN_MS, O as udsChildLogToWorkerEntry, On as EMPTY_SOCKET_DIRECTION, Ot as HEAP_WATCH_INTERVAL_MS, P as callWithServiceDiscovery, Pn as CAP_ACTION_OVERFLOW_LABEL, Pt as RSS_BUDGET_RELEASE_RATIO, Q as buildNativeCapProxy, Qt as ARRAY_BUFFER_CENSUS_SUSTAIN_MS, R as LocalChildRegistry, Rn as capActionLabel, Rt as buildHeapSample, S as buildLinkChain, Sn as SOCKET_QUEUE_TOP_N, St as resolveNpmInvocation, T as createParentUnownedCallHandler, Tn as diffSocketPlane, Tt as HEAP_RECLAIM_MIN_INTERVAL_MS, U as __resetCapUsageRegistryForTests, Un as createEventPlaneReader, Ut as hubMainRssBudget, V as CapUsageRegistry, Vn as EVENT_PLANE_TOP_N, Vt as createV8Reclaimer, W as getCapUsageRegistry, Wn as diffEventPlane, Wt as nextRssBudgetState, X as FrameDecoder, Xt as strandedMb, Y as localEndpointPath, Yt as startRunnerHeapWatch, Z as encodeFrame, Zt as ARRAY_BUFFER_CENSUS_COOLDOWN_MS, _ as AddonDepsManager, _n as MESH_QUEUE_TOP_N, _t as CapabilityUnavailableError, a as createUdsAddonContext, an as DECISIVE_HEAP_SPACES, at as createAddonService, b as brokerCallForCap, bn as EMPTY_SOCKET_PLANE_BASELINE, bt as resolveAddonClass, c as getOrInitReadinessRegistryForClient, cn as formatHeapSpaces, ct as capActionName, d as clusterEventTopic, dn as selectReportedSpaces, dt as capServiceName, en as ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO, et as createBrokerDeviceManagerApi, f as getBrokerEventBus, fn as OUTBOUND_QUEUE_COOLDOWN_MS, ft as parseCapAction, g as subscribePassthrough, gn as formatOutboundQueueVerdict, gt as CapabilityHandle, h as setNodeEventInterest, hn as createOutboundQueueGate, ht as DeviceRegistry, i as createAddonContext, in as isArrayBufferCensusReport, j as AGENT_CAP_FWD_ACTION, jn as sampleSocketDirection, jt as HUB_MAIN_RSS_BUDGET_MB, k as createUdsLogger, kn as createSocketDirectionCounters, kt as HEAP_WATCH_WARN_RATIO, l as setHubConnected, ln as heapSpaceField, lt as capActionSuffix, m as registerEventBusService, mn as OUTBOUND_QUEUE_THRESHOLD_BYTES, mt as serializeTypedArrays, n as installManifestPythonDeps, nn as createInspectorArrayBufferCensus, o as getWorkerDeviceRegistry, on as HEAP_SPACE_REPORT_MIN_MB, ot as validateProviderRegistrations, p as getMoleculerEventStats, pn as OUTBOUND_QUEUE_SUSTAIN_MS, pt as deserializeTypedArrays, q as UdsLocalTransportServer, qt as shouldReclaim, r as adaptBrokerToCluster, rn as formatArrayBufferCensus, rt as mountNativeCapService, s as getOrInitReadinessRegistry, sn as emitHeapDiagnosticReport, st as NATIVE_PROVIDER_SERVICE_INFIX, t as installManifestSystemDeps, tn as createArrayBufferCensusGate, u as EVENT_TOPIC_PREFIX, un as readHeapSpaces, ut as capBareAction, v as createKernelHwAccel, vn as createMeshQueueReader, vt as copyBundledNativeModules, w as localProviderLink, wn as createSocketPlaneReader, wt as createAddonDataPlaneFacility, x as brokerTransportLink, xn as SOCKET_PLANE_TOP_N, xt as buildNpmChildEnv, y as resolveHwAccel, yn as formatMeshQueue, yt as installManifestNativeDeps, z as UDS_NO_ROUTE_PREFIX, zn as createCapActionCensus, zt as createDeferredHeapWatchSink } from "./manifest-system-deps-BEj9Dpu1.mjs";
|
|
30
30
|
import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-Vp0bPV3U.mjs";
|
|
31
|
-
import { t as CustomActionRegistry } from "./custom-action-registry-
|
|
31
|
+
import { t as CustomActionRegistry } from "./custom-action-registry-CFHYDHAu.mjs";
|
|
32
32
|
import { z } from "zod";
|
|
33
33
|
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
34
34
|
import { PYTHON_VERSION, buildBinaryPath, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements } from "@camstack/types/node";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import sharp from "sharp";
|
|
2
|
+
//#region src/builtins/snapshot/snapshot-courtesy.ts
|
|
3
|
+
/**
|
|
4
|
+
* State treatment for a camera that cannot produce a current frame.
|
|
5
|
+
*
|
|
6
|
+
* ── Why this exists ───────────────────────────────────────────────────────
|
|
7
|
+
* A disabled, offline or sleeping camera has no frame, and until 2026-08-11 the
|
|
8
|
+
* snapshot service simply had nothing to say about it: the media route answered
|
|
9
|
+
* **404** and the client painted "Unavailable". Measured on the live grid that
|
|
10
|
+
* day, 10 of 26 tiles were 404s — one genuinely disabled camera plus nine dead
|
|
11
|
+
* legacy rows. A 404 is indistinguishable from a broken camera, so an operator
|
|
12
|
+
* who deliberately switched a camera off saw the same thing as a fault ([D62]:
|
|
13
|
+
* "an off switch is REPORTED off; disabled must never look like broken").
|
|
14
|
+
*
|
|
15
|
+
* So the service answers with an image that SAYS what is going on, carrying
|
|
16
|
+
* the camera's own name. When a real cached frame exists it remains visible
|
|
17
|
+
* under the treatment; otherwise the generated courtesy background is used.
|
|
18
|
+
*
|
|
19
|
+
* ── Why sharp and not ffmpeg ──────────────────────────────────────────────
|
|
20
|
+
* The first draft of this file shelled out to `ffmpeg` with a `drawtext`
|
|
21
|
+
* filter. That was written before the resize path was measured, and it was the
|
|
22
|
+
* wrong call for the same reason: this runs in the snapshot wrapper, a system
|
|
23
|
+
* builtin loaded in the hub's ROOT process, so every render was a fork + exec
|
|
24
|
+
* on the loop that serves the API — 52 ms against sharp's 4.8 ms in that
|
|
25
|
+
* container, to draw two lines of text.
|
|
26
|
+
*
|
|
27
|
+
* `terminal-frame-renderer.ts` already renders text this way (SVG → sharp), so
|
|
28
|
+
* this is the house pattern rather than a new one. It also drops the font-PATH
|
|
29
|
+
* probing the ffmpeg version needed: an SVG names a font FAMILY and fontconfig
|
|
30
|
+
* resolves it — verified in the hub image, where `fc-match "DejaVu Sans Mono"`
|
|
31
|
+
* answers with the real file.
|
|
32
|
+
*
|
|
33
|
+
* Rendering is pure input → bytes with no I/O of its own, and the geometry and
|
|
34
|
+
* escaping are separated out so they stay testable without rasterizing.
|
|
35
|
+
*/
|
|
36
|
+
/** Default tile size when the caller asks for no particular width. */
|
|
37
|
+
var COURTESY_DEFAULT_WIDTH = 640;
|
|
38
|
+
/**
|
|
39
|
+
* Font stack for the rendered text. Family names, not paths — librsvg resolves
|
|
40
|
+
* them through fontconfig, and the trailing generics keep a host without DejaVu
|
|
41
|
+
* rendering something legible instead of nothing.
|
|
42
|
+
*/
|
|
43
|
+
var COURTESY_FONT_STACK = "DejaVu Sans,DejaVu Sans Mono,Helvetica,Arial,sans-serif";
|
|
44
|
+
/** The word the frame carries. Deliberately the operator's vocabulary. */
|
|
45
|
+
function courtesyLabel(reason) {
|
|
46
|
+
switch (reason) {
|
|
47
|
+
case "disabled": return "Disabled";
|
|
48
|
+
case "offline": return "Offline";
|
|
49
|
+
case "sleeping": return "Sleeping";
|
|
50
|
+
case "unreachable": return "Unreachable";
|
|
51
|
+
case "waking": return "Waking";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Background per reason. A disabled camera is a DELIBERATE state and must not
|
|
56
|
+
* read as an alarm, so it is neutral grey; offline is a fault and is warmer.
|
|
57
|
+
*/
|
|
58
|
+
function courtesyBackground(reason) {
|
|
59
|
+
switch (reason) {
|
|
60
|
+
case "disabled": return "#2b2b31";
|
|
61
|
+
case "offline": return "#3a2b2b";
|
|
62
|
+
case "sleeping": return "#232b3a";
|
|
63
|
+
case "waking": return "#243247";
|
|
64
|
+
case "unreachable": return "#3a2b2b";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** XML escaping for text placed inside an SVG `<text>` node. */
|
|
68
|
+
function escapeCourtesyText(value) {
|
|
69
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The SVG the frame is rasterized from: the state large and centred, the
|
|
73
|
+
* camera's name under it, smaller and dimmer.
|
|
74
|
+
*
|
|
75
|
+
* Sizes derive from the width so a 240 px grid tile and a 1920 px full-bleed
|
|
76
|
+
* frame read the same, with a floor so a thumbnail stays legible.
|
|
77
|
+
*/
|
|
78
|
+
function buildCourtesySvg(spec) {
|
|
79
|
+
return buildStateSvg(spec, false);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The same state treatment as a courtesy frame, but translucent so the last
|
|
83
|
+
* real camera frame remains visible underneath it.
|
|
84
|
+
*/
|
|
85
|
+
function buildCourtesyOverlaySvg(spec) {
|
|
86
|
+
return buildStateSvg(spec, true);
|
|
87
|
+
}
|
|
88
|
+
function buildStateSvg(spec, overlay) {
|
|
89
|
+
const stateSize = Math.max(12, Math.round(spec.width / 12));
|
|
90
|
+
const nameSize = Math.max(9, Math.round(spec.width / 26));
|
|
91
|
+
const state = escapeCourtesyText(courtesyLabel(spec.reason));
|
|
92
|
+
const name = escapeCourtesyText(spec.deviceName);
|
|
93
|
+
const midY = spec.height / 2;
|
|
94
|
+
const washOpacity = overlay ? "0.62" : "1";
|
|
95
|
+
const textStroke = Math.max(1, Math.round(spec.width / 320));
|
|
96
|
+
return [
|
|
97
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${String(spec.width)}" height="${String(spec.height)}">`,
|
|
98
|
+
`<rect width="100%" height="100%" fill="${courtesyBackground(spec.reason)}" fill-opacity="${washOpacity}"/>`,
|
|
99
|
+
`<g font-family="${COURTESY_FONT_STACK}" text-anchor="middle" paint-order="stroke" stroke="#111118" stroke-width="${String(textStroke)}">`,
|
|
100
|
+
`<text x="50%" y="${String(Math.round(midY))}" font-size="${String(stateSize)}" fill="#e8e8ee">${state}</text>`,
|
|
101
|
+
`<text x="50%" y="${String(Math.round(midY + stateSize))}" font-size="${String(nameSize)}" fill="#9a9aa8">${name}</text>`,
|
|
102
|
+
`</g></svg>`
|
|
103
|
+
].join("");
|
|
104
|
+
}
|
|
105
|
+
/** Cache key — a courtesy frame is a pure function of these four. */
|
|
106
|
+
function courtesyCacheKey(spec) {
|
|
107
|
+
return `${spec.reason}:${String(spec.width)}x${String(spec.height)}:${spec.deviceName}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Render the frame.
|
|
111
|
+
*
|
|
112
|
+
* Rejects on failure — the caller decides what to do, exactly as `resizeJpeg`
|
|
113
|
+
* does, so a broken courtesy path is never mistaken for a broken camera.
|
|
114
|
+
*/
|
|
115
|
+
function renderCourtesyJpeg(spec) {
|
|
116
|
+
return sharp(Buffer.from(buildCourtesySvg(spec))).jpeg({ quality: 82 }).toBuffer().then((bytes) => {
|
|
117
|
+
if (bytes.length === 0) throw new Error("courtesy frame produced no bytes");
|
|
118
|
+
return bytes;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Paint the state treatment over a real cached frame.
|
|
123
|
+
*
|
|
124
|
+
* The source dimensions are authoritative: overlays preserve the original
|
|
125
|
+
* frame rather than resizing it to the generated courtesy-frame default.
|
|
126
|
+
*/
|
|
127
|
+
async function renderCourtesyOverlayJpeg(background, spec) {
|
|
128
|
+
const metadata = await sharp(background).metadata();
|
|
129
|
+
if (!metadata.width || !metadata.height) throw new Error("cached snapshot has no renderable dimensions");
|
|
130
|
+
const overlay = Buffer.from(buildCourtesyOverlaySvg({
|
|
131
|
+
...spec,
|
|
132
|
+
width: metadata.width,
|
|
133
|
+
height: metadata.height
|
|
134
|
+
}));
|
|
135
|
+
const bytes = await sharp(background).composite([{ input: overlay }]).jpeg({ quality: 82 }).toBuffer();
|
|
136
|
+
if (bytes.length === 0) throw new Error("courtesy overlay produced no bytes");
|
|
137
|
+
return bytes;
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
export { renderCourtesyOverlayJpeg as i, courtesyCacheKey as n, renderCourtesyJpeg as r, COURTESY_DEFAULT_WIDTH as t };
|