@camstack/system 1.2.230 → 1.2.232
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-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- 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-YD6rwIuz.js → custom-action-registry-YHpAdksL.js} +1 -1
- package/dist/{custom-action-registry-zTQXMaNb.mjs → custom-action-registry-gaO1AlT2.mjs} +1 -1
- package/dist/{dist-C0M6CBhV.mjs → dist-BX6SpI0i.mjs} +235 -11
- package/dist/{dist-zr6q3Aad.js → dist-CEIkHEdy.js} +235 -11
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{retired-settings-keys-DFwQTxAT.mjs → retired-settings-keys-DDV06eUr.mjs} +1 -1
- package/dist/{retired-settings-keys-BqKPfC0_.js → retired-settings-keys-FLZlwa2U.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-DEuj7E3j.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).
|
|
@@ -3467,7 +3481,23 @@ var VideoEncodeSchema = zod.z.object({
|
|
|
3467
3481
|
"zerolatency",
|
|
3468
3482
|
"film",
|
|
3469
3483
|
"animation"
|
|
3470
|
-
]).optional()
|
|
3484
|
+
]).optional(),
|
|
3485
|
+
/**
|
|
3486
|
+
* ONE slice per access unit.
|
|
3487
|
+
*
|
|
3488
|
+
* `-tune zerolatency` turns on x264's sliced threads, and a frame then leaves
|
|
3489
|
+
* the encoder as five NAL slices that share one RTP timestamp and carry one
|
|
3490
|
+
* marker bit. A libwebrtc depacketiser sees five frame-starts and one
|
|
3491
|
+
* frame-end per frame: the first pictures render and the video then freezes
|
|
3492
|
+
* for good while the audio, on its own plane, plays on. Measured on this hub
|
|
3493
|
+
* 2026-09-13 against the Echo, and reduced to one slice by this flag alone.
|
|
3494
|
+
*
|
|
3495
|
+
* A NAMED field and not a raw flag, because {@link EgressEncodeSchema} omits
|
|
3496
|
+
* `outputArgs` on purpose: an opaque array is part of the sharing key, so two
|
|
3497
|
+
* consumers meaning the same thing spelled differently would stop sharing one
|
|
3498
|
+
* child. Absent means "whatever the encoder does" — today's behaviour.
|
|
3499
|
+
*/
|
|
3500
|
+
singleSlicePerFrame: zod.z.boolean().optional()
|
|
3471
3501
|
});
|
|
3472
3502
|
var AudioEncodeSchema = zod.z.union([zod.z.literal("passthrough"), zod.z.object({
|
|
3473
3503
|
codec: zod.z.enum([
|
|
@@ -3520,7 +3550,7 @@ var BASE_LIVE_EGRESS_PROFILE = {
|
|
|
3520
3550
|
audio: "passthrough"
|
|
3521
3551
|
};
|
|
3522
3552
|
({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
|
|
3523
|
-
({ ...BASE_LIVE_EGRESS_PROFILE });
|
|
3553
|
+
({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
|
|
3524
3554
|
/**
|
|
3525
3555
|
* Deep wiring healthcheck — snapshot of active reachability probes across
|
|
3526
3556
|
* every declared capability + widget of every installed plugin, on every
|
|
@@ -8470,6 +8500,72 @@ var cameraPipelineConfigCapability = {
|
|
|
8470
8500
|
exposesDeviceSettings: true,
|
|
8471
8501
|
methods: {}
|
|
8472
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
|
+
};
|
|
8473
8569
|
/** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
|
|
8474
8570
|
var StreamFormatSchema = zod.z.enum([
|
|
8475
8571
|
"webrtc",
|
|
@@ -8879,6 +8975,46 @@ var streamBrokerCapability = {
|
|
|
8879
8975
|
mode: "singleton",
|
|
8880
8976
|
exposesDeviceSettings: true,
|
|
8881
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" }),
|
|
8882
9018
|
publishCameraStream: method(zod.z.object({
|
|
8883
9019
|
deviceId: zod.z.number().int().nonnegative(),
|
|
8884
9020
|
camStreamId: zod.z.string().min(1),
|
|
@@ -9356,6 +9492,16 @@ var PickStreamRequirementsSchema = zod.z.object({
|
|
|
9356
9492
|
acceptCodecs: zod.z.array(StreamCodecSchema).readonly().optional(),
|
|
9357
9493
|
/** Minimum vertical resolution. Streams shorter than this are dropped. */
|
|
9358
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(),
|
|
9359
9505
|
/** Minimum horizontal resolution. */
|
|
9360
9506
|
minWidth: zod.z.number().int().positive().optional(),
|
|
9361
9507
|
/**
|
|
@@ -9372,7 +9518,22 @@ var PickStreamRequirementsSchema = zod.z.object({
|
|
|
9372
9518
|
* transcoded" guard: if the device is already serving the consumer's
|
|
9373
9519
|
* codec end-to-end, there's nothing to optimise.
|
|
9374
9520
|
*/
|
|
9375
|
-
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()
|
|
9376
9537
|
}).readonly();
|
|
9377
9538
|
var PickStreamPreferencesSchema = zod.z.object({
|
|
9378
9539
|
/**
|
|
@@ -9386,12 +9547,32 @@ var PickStreamPreferencesSchema = zod.z.object({
|
|
|
9386
9547
|
* picks the tallest stream; `'lowest'` picks the shortest (used by
|
|
9387
9548
|
* memory-constrained consumers / Apple Home guest sessions).
|
|
9388
9549
|
*/
|
|
9389
|
-
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()
|
|
9390
9563
|
}).readonly();
|
|
9391
9564
|
var PickedCamStreamSchema = zod.z.object({
|
|
9392
9565
|
camStreamId: zod.z.string(),
|
|
9393
9566
|
codec: zod.z.string().optional(),
|
|
9394
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(),
|
|
9395
9576
|
/** One-line explanation of why this stream won — for logs / debug UI. */
|
|
9396
9577
|
reason: zod.z.string()
|
|
9397
9578
|
});
|
|
@@ -15468,6 +15649,8 @@ var NcSystemEventKindSchema = zod.z.enum([
|
|
|
15468
15649
|
"device-enabled",
|
|
15469
15650
|
"device-battery-low",
|
|
15470
15651
|
"device-battery-normal",
|
|
15652
|
+
"device-consumable-low",
|
|
15653
|
+
"device-consumable-normal",
|
|
15471
15654
|
"stream-online",
|
|
15472
15655
|
"stream-offline",
|
|
15473
15656
|
"node-online",
|
|
@@ -15486,6 +15669,7 @@ var NcSystemEventKindSchema = zod.z.enum([
|
|
|
15486
15669
|
"addon-updated",
|
|
15487
15670
|
"server-updated",
|
|
15488
15671
|
"export-completed",
|
|
15672
|
+
"backup-completed",
|
|
15489
15673
|
"camera-online",
|
|
15490
15674
|
"camera-offline",
|
|
15491
15675
|
"camera-disabled",
|
|
@@ -24927,10 +25111,28 @@ var webrtcSessionCapability = {
|
|
|
24927
25111
|
getSessionState: method(zod.z.object({
|
|
24928
25112
|
deviceId: zod.z.number().int().nonnegative(),
|
|
24929
25113
|
sessionId: zod.z.string()
|
|
24930
|
-
}), zod.z.object({
|
|
24931
|
-
|
|
24932
|
-
|
|
24933
|
-
|
|
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
|
+
}))
|
|
24934
25136
|
},
|
|
24935
25137
|
/** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
|
|
24936
25138
|
mount: { kind: "server-provided" }
|
|
@@ -36953,6 +37155,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
|
|
|
36953
37155
|
streamBrokerCapability,
|
|
36954
37156
|
streamCatalogCapability,
|
|
36955
37157
|
streamParamsCapability,
|
|
37158
|
+
streamSignalsCapability,
|
|
36956
37159
|
switchCapability,
|
|
36957
37160
|
systemCapability,
|
|
36958
37161
|
tamperCapability,
|
|
@@ -42395,6 +42598,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
42395
42598
|
addonId: null,
|
|
42396
42599
|
access: "create"
|
|
42397
42600
|
},
|
|
42601
|
+
"streamBroker.reportStreamSignal": {
|
|
42602
|
+
capName: "stream-broker",
|
|
42603
|
+
capScope: "system",
|
|
42604
|
+
addonId: null,
|
|
42605
|
+
access: "create"
|
|
42606
|
+
},
|
|
42398
42607
|
"streamBroker.restartProfile": {
|
|
42399
42608
|
capName: "stream-broker",
|
|
42400
42609
|
capScope: "system",
|
|
@@ -42479,6 +42688,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
42479
42688
|
addonId: null,
|
|
42480
42689
|
access: "create"
|
|
42481
42690
|
},
|
|
42691
|
+
"streamSignals.listSignals": {
|
|
42692
|
+
capName: "stream-signals",
|
|
42693
|
+
capScope: "device",
|
|
42694
|
+
addonId: null,
|
|
42695
|
+
access: "view"
|
|
42696
|
+
},
|
|
42482
42697
|
"switch.setState": {
|
|
42483
42698
|
capName: "switch",
|
|
42484
42699
|
capScope: "device",
|
|
@@ -44851,6 +45066,11 @@ Object.freeze({
|
|
|
44851
45066
|
form: "single",
|
|
44852
45067
|
optional: false
|
|
44853
45068
|
}],
|
|
45069
|
+
"streamBroker.reportStreamSignal": [{
|
|
45070
|
+
name: "deviceId",
|
|
45071
|
+
form: "single",
|
|
45072
|
+
optional: false
|
|
45073
|
+
}],
|
|
44854
45074
|
"streamBroker.restartProfile": [{
|
|
44855
45075
|
name: "deviceId",
|
|
44856
45076
|
form: "single",
|
|
@@ -45083,7 +45303,7 @@ Object.freeze({
|
|
|
45083
45303
|
});
|
|
45084
45304
|
/**
|
|
45085
45305
|
* Per-cap runtime-state policy, projected from every `*.cap.ts` that declares
|
|
45086
|
-
* `runtimeState`.
|
|
45306
|
+
* `runtimeState`. 68 caps: 35 `restored`, 33 `session`.
|
|
45087
45307
|
*
|
|
45088
45308
|
* A cap absent from this map has no runtime-state slice at all. A cap PRESENT
|
|
45089
45309
|
* with `durability: 'session'` has one and has decided not to keep it.
|
|
@@ -45309,6 +45529,10 @@ var RUNTIME_STATE_POLICY = {
|
|
|
45309
45529
|
durability: "restored",
|
|
45310
45530
|
volatileFields: ["lastFetchedAt"]
|
|
45311
45531
|
},
|
|
45532
|
+
"stream-signals": {
|
|
45533
|
+
durability: "session",
|
|
45534
|
+
volatileFields: []
|
|
45535
|
+
},
|
|
45312
45536
|
"switch": {
|
|
45313
45537
|
durability: "restored",
|
|
45314
45538
|
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-CEIkHEdy.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-YHpAdksL.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-BX6SpI0i.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-gaO1AlT2.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 };
|