@camstack/addon-model-studio 1.1.165 → 1.1.167

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.
Files changed (18) hide show
  1. package/dist/{MotionZonesSettings-B6DWvwTt.mjs → MotionZonesSettings-sJUloF9d.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-GC9AJ7u9.mjs → PrivacyMaskSettings-v_y0uLR7.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-CDRtXS44.mjs → SceneMonitorEditor-DW8-_EvS.mjs} +3 -3
  4. package/dist/_stub.js +10 -10
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-C3EH8dXO.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-C6ovYeg2.mjs} +3 -3
  6. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BeO6iU8W.mjs +26 -0
  7. package/dist/{hostInit-D7wKaStx.mjs → hostInit-DtYZHu6O.mjs} +2 -2
  8. package/dist/model-studio.addon.js +1143 -816
  9. package/dist/model-studio.addon.mjs +1143 -816
  10. package/dist/{player-overlays-C5aNbV6c.mjs → player-overlays-DZI3Us9O.mjs} +1 -1
  11. package/dist/remoteEntry.js +1 -1
  12. package/dist/{responsive-B6Yj9AVT.mjs → responsive-6KHz1FeQ.mjs} +1 -1
  13. package/dist/{scene-monitor-copy-DoFpRkTB.mjs → scene-monitor-copy-pDRHUCNN.mjs} +1 -1
  14. package/dist/{square-CbUN1Re_.mjs → square-G5IxIOOA.mjs} +1 -1
  15. package/dist/{trash-2-DalUiaTk.mjs → trash-2-BQ6DXX-P.mjs} +1 -1
  16. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-CMIDBF8a.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-XdC34FqG.mjs} +1 -1
  17. package/package.json +1 -1
  18. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CrxDjxBh.mjs +0 -26
@@ -5884,7 +5884,7 @@ var ZodIssueCode = {
5884
5884
  var ZodFirstPartyTypeKind;
5885
5885
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5886
5886
  //#endregion
5887
- //#region ../types/dist/sleep-i3eUVc-d.mjs
5887
+ //#region ../types/dist/sleep-PEo0-Fz9.mjs
5888
5888
  /**
5889
5889
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5890
5890
  * window to float samples (D455).
@@ -7020,6 +7020,24 @@ function normalizeAddonInitResult(result) {
7020
7020
  if (Array.isArray(result)) return { providers: result };
7021
7021
  return result;
7022
7022
  }
7023
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
7024
+ var PeerBytesTicketSchema = object({
7025
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
7026
+ url: string().min(1),
7027
+ /**
7028
+ * The HOST node this URL means something on — the hub or a named agent,
7029
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
7030
+ * refuses `cross-node` by name when they differ, without dialling.
7031
+ */
7032
+ hostNodeId: string().min(1),
7033
+ expiresAtMs: number().int().nonnegative(),
7034
+ /**
7035
+ * What the producer DECLARED the body to be, when it knows — `null` when it
7036
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
7037
+ * must be able to tell "the producer did not say" from "the body is empty".
7038
+ */
7039
+ declaredBytes: number().int().nonnegative().nullable()
7040
+ });
7023
7041
  /** Shared Zod schemas used across streaming capabilities. */
7024
7042
  var CamProfileSchema = _enum([
7025
7043
  "high",
@@ -8230,7 +8248,7 @@ var AdoptionJobSchema = object({
8230
8248
  * component's original options — detection to the detection-pipeline wrapper
8231
8249
  * binding, audio analysis to its own, recording to `RecordingConfig.enabled`
8232
8250
  * (which was always first-class; the switch was a veneer over
8233
- * `recording.setDeviceConfig`), notifications to a notification-center
8251
+ * `recordingArchive.setDeviceConfig`), notifications to a notification-center
8234
8252
  * per-device setting, the two camera planes to their own components.
8235
8253
  *
8236
8254
  * What survives is {@link composeSwitchedOff}: `CameraStatus.switchedOff`, the
@@ -8256,7 +8274,7 @@ var AdoptionJobSchema = object({
8256
8274
  * | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
8257
8275
  * | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
8258
8276
  * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
8259
- * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
8277
+ * | `recording` | `recordingArchive.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
8260
8278
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
8261
8279
  * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
8262
8280
  * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
@@ -12172,58 +12190,6 @@ method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, Broke
12172
12190
  auth: "admin"
12173
12191
  }), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
12174
12192
  DeviceType.Camera;
12175
- /**
12176
- * The signals a device can emit to WAKE its own stream.
12177
- *
12178
- * A camera whose stream is built on demand sleeps until something asks for it,
12179
- * and "something" cannot be a consumer that is merely attached — a Frigate-style
12180
- * puller holds a session open for ever, and treating that as demand would keep
12181
- * a battery camera awake for ever, which is the whole thing the battery is for
12182
- * (D173). So the wake has to come from the CAMERA: an event it noticed by
12183
- * itself, with no stream running.
12184
- *
12185
- * ## The vocabulary is the PROVIDER'S, not ours
12186
- *
12187
- * Like `consumables`, this cap declares no vocabulary of its own. A provider
12188
- * names each signal with a `code` it chooses and a `label` an operator reads.
12189
- * Reolink offers motion and camera-native detection; another provider may offer
12190
- * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
12191
- * yet. A fixed enum here would mean every new signal is a framework release.
12192
- *
12193
- * It is deliberately NOT derived from the caps a device already binds. Whether
12194
- * a camera CAN push firmware motion is expressed by `motionSources` containing
12195
- * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
12196
- * binding — but both answer "what drives the detection pipeline", which is a
12197
- * different question from "what may wake a sleeping stream". A camera can do
12198
- * the first and not be trusted with the second, and the operator picks per
12199
- * camera. Two questions, two authorities.
12200
- *
12201
- * ## Availability is not permission
12202
- *
12203
- * `listSignals` says what the device CAN emit. Whether a given signal actually
12204
- * wakes the stream is the operator's per-camera choice, held by the broker
12205
- * alongside the cooldown — see the stream-broker cap's wake settings. A
12206
- * provider declaring a signal is not a provider enabling it.
12207
- */
12208
- /** One signal a device can emit. */
12209
- var StreamSignalSchema = object({
12210
- /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
12211
- code: string().min(1),
12212
- /** What an operator reads in the picker. The provider's own wording. */
12213
- label: string().min(1),
12214
- /**
12215
- * Whether the provider recommends this signal ON when a camera is first set
12216
- * up. A provider knows which of its signals are cheap and reliable; an
12217
- * operator should not have to discover that by trial. Reolink recommends
12218
- * both of its own.
12219
- */
12220
- recommended: boolean()
12221
- });
12222
- object({
12223
- signals: array(StreamSignalSchema),
12224
- lastFetchedAt: number()
12225
- });
12226
- Object.values(DeviceType), method(_void(), array(StreamSignalSchema).readonly());
12227
12193
  /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
12228
12194
  var StreamFormatSchema = _enum([
12229
12195
  "webrtc",
@@ -13603,6 +13569,97 @@ method(object({ codec: string() }), boolean()), method(_void(), object({
13603
13569
  });
13604
13570
  DeviceType.Camera;
13605
13571
  /**
13572
+ * device-admin-link — "this device has a management page of its own, and here
13573
+ * is its address".
13574
+ *
13575
+ * ## Why this is not a `deviceConfig` cap
13576
+ *
13577
+ * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
13578
+ * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
13579
+ * patch back through a setter; it costs a `builderId` reducer in
13580
+ * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
13581
+ * renders a form section. This cap answers ONE question with ONE read and
13582
+ * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
13583
+ * block, no `settings`, no `runtimeState` and no reducer — exactly like
13584
+ * `reboot`, the other pure-RPC device-native cap.
13585
+ *
13586
+ * ## Absent, and the difference between "no page" and "we cannot say"
13587
+ *
13588
+ * The two are answered at DIFFERENT layers, on purpose:
13589
+ *
13590
+ * - **"We cannot say"** → the provider never registers the cap for that
13591
+ * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
13592
+ * fan are reached only through a vendor cloud; there is no address to hand
13593
+ * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
13594
+ * conditioner DO have a LAN IP, and still have no HTTP management page
13595
+ * behind it. None of them register, so `deviceManager.getBindings` never
13596
+ * lists the cap and no surface asks.
13597
+ * - **"This device has no page, and I know that"** → the provider registers
13598
+ * and `getAdminLink` returns `null`. This is the answer for a device whose
13599
+ * sibling DOES have a page: a Reolink battery camera reached over UDP by
13600
+ * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
13601
+ * transport, a Home Assistant broker authenticated by supervisor token
13602
+ * (which carries no `baseUrl` at all).
13603
+ *
13604
+ * Both draw NOTHING. A button that opens a browser error is worse than no
13605
+ * button, and D62 is the same rule from the other side: an off switch is
13606
+ * reported off, never made to look broken. There is no third state where the
13607
+ * UI renders a disabled button "because the device might have a page".
13608
+ *
13609
+ * ## The URL never carries credentials
13610
+ *
13611
+ * Not in userinfo, not in a query string. Every provider builds through
13612
+ * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
13613
+ * scheme and path as separate arguments — there is no parameter a secret could
13614
+ * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
13615
+ * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
13616
+ * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
13617
+ * keeps providers from hand-rolling one anyway.
13618
+ *
13619
+ * This matters here more than anywhere else in the repo, because every provider
13620
+ * that knows a device's host knows its PASSWORD too: `{ host, port, username,
13621
+ * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
13622
+ * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
13623
+ * camera's own page will ask for its own login. That is correct, and pre-
13624
+ * filling it is the operator's business, not ours.
13625
+ *
13626
+ * ## It is a LAN fact
13627
+ *
13628
+ * The URL addresses the device where the NODE can see it. It is not proxied,
13629
+ * not made reachable from outside, and not sent anywhere. A surface renders it
13630
+ * as a link the operator's own browser follows, on the operator's own network,
13631
+ * or renders nothing.
13632
+ */
13633
+ /**
13634
+ * Whose page is it. The distinction is for the OPERATOR, who needs to know
13635
+ * before clicking whether he is about to land on a camera's own web server or
13636
+ * inside Home Assistant.
13637
+ */
13638
+ var AdminLinkTargetEnum = _enum(["device", "integration"]);
13639
+ var DeviceAdminLinkSchema = object({
13640
+ /**
13641
+ * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
13642
+ * free of userinfo and of any credential-shaped query key.
13643
+ */
13644
+ url: string(),
13645
+ /**
13646
+ * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
13647
+ * The PROVIDER names it, because only the provider knows what the page is;
13648
+ * a UI that invented the label from the addon id would call the Home
13649
+ * Assistant device page "Provider Homeassistant".
13650
+ */
13651
+ label: string(),
13652
+ target: AdminLinkTargetEnum,
13653
+ /**
13654
+ * Host the URL points at, without scheme, port or path — for the tooltip, so
13655
+ * an operator can see WHERE the button goes before he follows it. Redundant
13656
+ * with `url` by construction; carried separately so no surface has to parse
13657
+ * a URL to show it.
13658
+ */
13659
+ host: string()
13660
+ });
13661
+ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" });
13662
+ /**
13606
13663
  * Identity envelope for a device's upstream-system metadata.
13607
13664
  *
13608
13665
  * Two jobs:
@@ -13930,97 +13987,6 @@ method(object({ integrationId: string() }), object({ filters: array(AdoptionFilt
13930
13987
  auth: "admin"
13931
13988
  });
13932
13989
  /**
13933
- * device-admin-link — "this device has a management page of its own, and here
13934
- * is its address".
13935
- *
13936
- * ## Why this is not a `deviceConfig` cap
13937
- *
13938
- * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
13939
- * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
13940
- * patch back through a setter; it costs a `builderId` reducer in
13941
- * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
13942
- * renders a form section. This cap answers ONE question with ONE read and
13943
- * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
13944
- * block, no `settings`, no `runtimeState` and no reducer — exactly like
13945
- * `reboot`, the other pure-RPC device-native cap.
13946
- *
13947
- * ## Absent, and the difference between "no page" and "we cannot say"
13948
- *
13949
- * The two are answered at DIFFERENT layers, on purpose:
13950
- *
13951
- * - **"We cannot say"** → the provider never registers the cap for that
13952
- * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
13953
- * fan are reached only through a vendor cloud; there is no address to hand
13954
- * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
13955
- * conditioner DO have a LAN IP, and still have no HTTP management page
13956
- * behind it. None of them register, so `deviceManager.getBindings` never
13957
- * lists the cap and no surface asks.
13958
- * - **"This device has no page, and I know that"** → the provider registers
13959
- * and `getAdminLink` returns `null`. This is the answer for a device whose
13960
- * sibling DOES have a page: a Reolink battery camera reached over UDP by
13961
- * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
13962
- * transport, a Home Assistant broker authenticated by supervisor token
13963
- * (which carries no `baseUrl` at all).
13964
- *
13965
- * Both draw NOTHING. A button that opens a browser error is worse than no
13966
- * button, and D62 is the same rule from the other side: an off switch is
13967
- * reported off, never made to look broken. There is no third state where the
13968
- * UI renders a disabled button "because the device might have a page".
13969
- *
13970
- * ## The URL never carries credentials
13971
- *
13972
- * Not in userinfo, not in a query string. Every provider builds through
13973
- * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
13974
- * scheme and path as separate arguments — there is no parameter a secret could
13975
- * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
13976
- * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
13977
- * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
13978
- * keeps providers from hand-rolling one anyway.
13979
- *
13980
- * This matters here more than anywhere else in the repo, because every provider
13981
- * that knows a device's host knows its PASSWORD too: `{ host, port, username,
13982
- * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
13983
- * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
13984
- * camera's own page will ask for its own login. That is correct, and pre-
13985
- * filling it is the operator's business, not ours.
13986
- *
13987
- * ## It is a LAN fact
13988
- *
13989
- * The URL addresses the device where the NODE can see it. It is not proxied,
13990
- * not made reachable from outside, and not sent anywhere. A surface renders it
13991
- * as a link the operator's own browser follows, on the operator's own network,
13992
- * or renders nothing.
13993
- */
13994
- /**
13995
- * Whose page is it. The distinction is for the OPERATOR, who needs to know
13996
- * before clicking whether he is about to land on a camera's own web server or
13997
- * inside Home Assistant.
13998
- */
13999
- var AdminLinkTargetEnum = _enum(["device", "integration"]);
14000
- var DeviceAdminLinkSchema = object({
14001
- /**
14002
- * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
14003
- * free of userinfo and of any credential-shaped query key.
14004
- */
14005
- url: string(),
14006
- /**
14007
- * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
14008
- * The PROVIDER names it, because only the provider knows what the page is;
14009
- * a UI that invented the label from the addon id would call the Home
14010
- * Assistant device page "Provider Homeassistant".
14011
- */
14012
- label: string(),
14013
- target: AdminLinkTargetEnum,
14014
- /**
14015
- * Host the URL points at, without scheme, port or path — for the tooltip, so
14016
- * an operator can see WHERE the button goes before he follows it. Redundant
14017
- * with `url` by construction; carried separately so no surface has to parse
14018
- * a URL to show it.
14019
- */
14020
- host: string()
14021
- });
14022
- method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" });
14023
- /**
14024
13990
  * `device-export` — collection cap for addons that export camstack
14025
13991
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
14026
13992
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -24146,6 +24112,58 @@ method(_void(), ProviderInfoSchema, { auth: "admin" }), method(object({ config:
24146
24112
  kind: "mutation",
24147
24113
  auth: "admin"
24148
24114
  });
24115
+ /**
24116
+ * The signals a device can emit to WAKE its own stream.
24117
+ *
24118
+ * A camera whose stream is built on demand sleeps until something asks for it,
24119
+ * and "something" cannot be a consumer that is merely attached — a Frigate-style
24120
+ * puller holds a session open for ever, and treating that as demand would keep
24121
+ * a battery camera awake for ever, which is the whole thing the battery is for
24122
+ * (D173). So the wake has to come from the CAMERA: an event it noticed by
24123
+ * itself, with no stream running.
24124
+ *
24125
+ * ## The vocabulary is the PROVIDER'S, not ours
24126
+ *
24127
+ * Like `consumables`, this cap declares no vocabulary of its own. A provider
24128
+ * names each signal with a `code` it chooses and a `label` an operator reads.
24129
+ * Reolink offers motion and camera-native detection; another provider may offer
24130
+ * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
24131
+ * yet. A fixed enum here would mean every new signal is a framework release.
24132
+ *
24133
+ * It is deliberately NOT derived from the caps a device already binds. Whether
24134
+ * a camera CAN push firmware motion is expressed by `motionSources` containing
24135
+ * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
24136
+ * binding — but both answer "what drives the detection pipeline", which is a
24137
+ * different question from "what may wake a sleeping stream". A camera can do
24138
+ * the first and not be trusted with the second, and the operator picks per
24139
+ * camera. Two questions, two authorities.
24140
+ *
24141
+ * ## Availability is not permission
24142
+ *
24143
+ * `listSignals` says what the device CAN emit. Whether a given signal actually
24144
+ * wakes the stream is the operator's per-camera choice, held by the broker
24145
+ * alongside the cooldown — see the stream-broker cap's wake settings. A
24146
+ * provider declaring a signal is not a provider enabling it.
24147
+ */
24148
+ /** One signal a device can emit. */
24149
+ var StreamSignalSchema = object({
24150
+ /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
24151
+ code: string().min(1),
24152
+ /** What an operator reads in the picker. The provider's own wording. */
24153
+ label: string().min(1),
24154
+ /**
24155
+ * Whether the provider recommends this signal ON when a camera is first set
24156
+ * up. A provider knows which of its signals are cheap and reliable; an
24157
+ * operator should not have to discover that by trial. Reolink recommends
24158
+ * both of its own.
24159
+ */
24160
+ recommended: boolean()
24161
+ });
24162
+ object({
24163
+ signals: array(StreamSignalSchema),
24164
+ lastFetchedAt: number()
24165
+ });
24166
+ Object.values(DeviceType), method(_void(), array(StreamSignalSchema).readonly());
24149
24167
  /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
24150
24168
  var ProfileSettingsSchemaBridge = unknown().nullable();
24151
24169
  var ProfileSettingsBagSchema = record(string(), unknown());
@@ -24718,6 +24736,7 @@ _enum([
24718
24736
  "sleeping",
24719
24737
  "camera-refused",
24720
24738
  "no-keyframe",
24739
+ "decode-failed",
24721
24740
  "no-catalog-row",
24722
24741
  "unsupported",
24723
24742
  "unknown-device",
@@ -24992,6 +25011,32 @@ var ClipBytesSchema = object({
24992
25011
  durationMs: number().positive().optional()
24993
25012
  });
24994
25013
  /**
25014
+ * Where a clip's finished bytes can be TAKEN (D613) — the answer to
25015
+ * {@link videoclipsCapability.methods.offerClipBytes}.
25016
+ *
25017
+ * Everything {@link ClipBytesSchema} carries except the bytes themselves, plus
25018
+ * the one-shot ticket that leads to them. The metadata is answered BEFORE the
25019
+ * transfer on purpose: a consumer learns which twin it got, what to call the
25020
+ * file and how long the clip runs without having to read a byte, so a decision
25021
+ * it would make on that metadata (a wrong twin, an implausible duration) costs
25022
+ * no transfer at all.
25023
+ */
25024
+ var ClipBytesOfferSchema = object({
25025
+ /**
25026
+ * One shot, seconds-long, loopback, on the PROVIDER's own host. Open it with
25027
+ * `ctx.peerBytes.open(...)`, which refuses a ticket from another node by
25028
+ * name rather than dialling a port that means something else here.
25029
+ */
25030
+ ticket: PeerBytesTicketSchema,
25031
+ contentType: string(),
25032
+ /** Suggested filename, extension included. */
25033
+ name: string(),
25034
+ /** Which twin was actually served — see {@link ClipBytesSchema.served}. */
25035
+ served: CamProfileSchema,
25036
+ /** See {@link ClipBytesSchema.durationMs}. Absent when nothing measured it. */
25037
+ durationMs: number().positive().optional()
25038
+ });
25039
+ /**
24995
25040
  * Where a clip's STREAM can be dialled (D597) — the answer to
24996
25041
  * {@link videoclipsCapability.methods.dialClipStream}.
24997
25042
  *
@@ -25067,6 +25112,44 @@ var ClipStreamDialSchema = object({
25067
25112
  /** Why `servedAudio` is `none` although sound was asked for. */
25068
25113
  audioReason: ClipStreamAudioReasonSchema.optional()
25069
25114
  });
25115
+ /**
25116
+ * What a surface may DRAW for this provider's clips — the answer to
25117
+ * {@link videoclipsCapability.methods.getPlaybackOptions} (D612).
25118
+ *
25119
+ * The envelope is a PROVIDER fact, not a clip fact, and that is measured, not
25120
+ * assumed: the broker's `chooseClipPath` reads exactly two inputs — whether
25121
+ * `dialClipStream` and `readClipBytes` are wired — and both are constants of
25122
+ * the broker's own closure over the provider's methods. The `profile` it is
25123
+ * handed is explicitly not read. So every clip of a provider is served the
25124
+ * same way, and a per-clip channel carried a value that could not vary. The
25125
+ * per-clip `clipTransport` server message was removed for exactly that reason.
25126
+ *
25127
+ * Queried per camera, before a clip is picked, so a control is rendered or
25128
+ * DISABLED rather than offered and refused at play time (D62: a disabled
25129
+ * control reads as unavailable, one that undoes the gesture reads as broken).
25130
+ */
25131
+ var ClipPlaybackOptionsSchema = object({
25132
+ /**
25133
+ * How this provider's clips reach the player. `stream` is the provider's
25134
+ * forward-only fMP4 (D597); `file` is one bounded by-handle fetch of the
25135
+ * whole clip, `stbl` indexed (D575).
25136
+ */
25137
+ transport: _enum(["stream", "file"]),
25138
+ /** `forward` = only ahead of the playhead. `free` = anywhere. */
25139
+ seek: _enum(["forward", "free"]),
25140
+ /** Frame-step BACKWARD is meaningful. Forward always is. */
25141
+ stepBack: boolean(),
25142
+ /** Whether the scrub gesture is served, as opposed to refused by name. */
25143
+ scrub: boolean(),
25144
+ /**
25145
+ * The rates that can be delivered, ascending, always containing `1`. The
25146
+ * viewer draws its picker from this and from nothing else — a constant it
25147
+ * keeps instead is the second authority that produced the defect: `8` and
25148
+ * `16` were offered, the broker clamped them to `4`, and no line anywhere
25149
+ * said so. `0` is not a member: pause is the absence of a rate.
25150
+ */
25151
+ rates: array(number().positive()).min(1).readonly()
25152
+ });
25070
25153
  var ClipSourceAvailabilitySchema = object({
25071
25154
  state: _enum([
25072
25155
  "ok",
@@ -25226,6 +25309,29 @@ DeviceType.Camera, method(object({
25226
25309
  }), ClipBytesSchema, {
25227
25310
  kind: "query",
25228
25311
  auth: "protected"
25312
+ }), optionalMethod(object({
25313
+ deviceId: number(),
25314
+ clipId: string().min(1),
25315
+ /** WHICH provider holds the bytes — see `readClipBytes.provider`. */
25316
+ provider: string().min(1),
25317
+ /** Which twin — `low | mid` → the sub file, `high` → the main twin. */
25318
+ profile: CamProfileSchema.optional(),
25319
+ /**
25320
+ * The CALLER's byte bound, so an over-size clip is refused before the
25321
+ * camera is touched rather than after. Capped by
25322
+ * {@link VIDEOCLIPS_MAX_OFFER_BYTES} whatever is passed; absent means
25323
+ * that ceiling.
25324
+ */
25325
+ maxBytes: number().int().positive().optional(),
25326
+ /**
25327
+ * The operator's authorisation to wake a sleeping camera for this
25328
+ * read. Absent — the default — means a sleeping standalone battery
25329
+ * camera is REFUSED by name, before any session is opened.
25330
+ */
25331
+ wake: ClipWakeSchema.optional()
25332
+ }), ClipBytesOfferSchema, {
25333
+ kind: "query",
25334
+ auth: "protected"
25229
25335
  }), optionalMethod(object({
25230
25336
  deviceId: number(),
25231
25337
  clipId: string().min(1),
@@ -25247,6 +25353,15 @@ DeviceType.Camera, method(object({
25247
25353
  }), ClipStreamDialSchema, {
25248
25354
  kind: "query",
25249
25355
  auth: "protected"
25356
+ }), optionalMethod(object({
25357
+ deviceId: number(),
25358
+ /** WHICH provider to ask — the `addonId` a {@link ClipSourceSchema}
25359
+ * row carries. Required for the same reason `listClips` requires it:
25360
+ * a collection cap has no "the bound one" to resolve to (D554). */
25361
+ provider: string().min(1)
25362
+ }), ClipPlaybackOptionsSchema, {
25363
+ kind: "query",
25364
+ auth: "protected"
25250
25365
  });
25251
25366
  /**
25252
25367
  * Optional client-side hints sent at session creation to help the provider
@@ -26331,6 +26446,143 @@ DeviceType.Camera, method(object({ deviceId: number() }), CameraCredentialsSchem
26331
26446
  kind: "query",
26332
26447
  auth: "admin"
26333
26448
  });
26449
+ /**
26450
+ * camera-grid-layout — the geometry of a COMPOSITE camera, on the camera's own
26451
+ * page.
26452
+ *
26453
+ * ## Why this is a capability and not an addon settings schema
26454
+ *
26455
+ * It was one, and it did not render. The addon declared the editor as a
26456
+ * `type: 'widget'` field inside its own `deviceSettingsSchema()`; the hub
26457
+ * returned that section correctly and `ConfigFormField` renders `type:'widget'`
26458
+ * perfectly well — and nothing ever asked camera-grid for it. The Cluster →
26459
+ * Pipeline → Device Overrides page interrogates a HAND-WRITTEN list of four
26460
+ * addons (`PIPELINE_CLUSTER_DEVICE_ADDONS`), whose own comment says an addon
26461
+ * not on it "falls off silently".
26462
+ *
26463
+ * Adding a fifth name to that list would have been the wrong fix twice over:
26464
+ * that page is per-camera DETECTION tuning, and a grid's geometry belongs
26465
+ * beside PTZ and motion zones on the camera itself. The device page is
26466
+ * BINDING-driven (D12), so the way in is a capability bound to the device —
26467
+ * and this cap carries its section the way `recording` does, by RETURNING it
26468
+ * from `getDeviceSettingsContribution`.
26469
+ *
26470
+ * Seven other widgets are still declared the other way, through a
26471
+ * `deviceConfig.ui` block the framework derives a section from. That route
26472
+ * gives the addon no say in where its own panel lands and no way to decline
26473
+ * for a device the panel does not suit, which is why this one does not use it.
26474
+ *
26475
+ * ## Why one addon may implement it
26476
+ *
26477
+ * It is a device-scoped NATIVE cap, registered by the grid camera device
26478
+ * itself. Nothing else declares a composite camera, so nothing else has a
26479
+ * layout — and the device-scoped route means the widget asks THE camera, not
26480
+ * "the camera-grid addon", which is what let the old custom-action pair be
26481
+ * reached only by a caller that already knew the addon id.
26482
+ *
26483
+ * ## The tab
26484
+ *
26485
+ * `streaming`, not a top-tab of its own. A grid's geometry IS what its stream
26486
+ * is, so the Streaming tab is where it belongs; a `grid` top-tab would need an
26487
+ * entry in `WELL_KNOWN_TAB_MAP` or the device page renders the raw id as the
26488
+ * label (measured on the robot camera, 2026-09-06 — a tab called "navigation"
26489
+ * next to "PTZ").
26490
+ */
26491
+ /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
26492
+ var GridNormalizedRectSchema = object({
26493
+ x: number().min(0).max(1),
26494
+ y: number().min(0).max(1),
26495
+ width: number().gt(0).max(1),
26496
+ height: number().gt(0).max(1)
26497
+ });
26498
+ /**
26499
+ * One source camera, the part of its picture taken, and where that part lands.
26500
+ *
26501
+ * Both rectangles are NORMALIZED (D519): a source camera can change resolution
26502
+ * — a profile switch, a firmware update, a substream that comes back different
26503
+ * — and a stored PIXEL rectangle would quietly start cutting the wrong region,
26504
+ * which is the class of bug nobody files.
26505
+ */
26506
+ var GridLayoutCellSchema = object({
26507
+ deviceId: number().int().positive(),
26508
+ /** The part of the SOURCE taken, normalized against the source. */
26509
+ source: GridNormalizedRectSchema,
26510
+ /** Where it lands, normalized against the CANVAS. */
26511
+ cell: GridNormalizedRectSchema
26512
+ });
26513
+ /**
26514
+ * Which profiles this grid can actually compose, and why not.
26515
+ *
26516
+ * A grid's `high` composes its sources' `high` and its `low` their `low`, so a
26517
+ * profile is on offer only when EVERY source can serve it. The refusal NAMES
26518
+ * the sources, because "this grid has no low" is not a finding — "615 has no
26519
+ * low" is, and it is the one an operator can act on.
26520
+ */
26521
+ var GridProfileOfferSchema = object({
26522
+ profile: _enum([
26523
+ "high",
26524
+ "mid",
26525
+ "low"
26526
+ ]),
26527
+ offered: boolean(),
26528
+ /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
26529
+ missingSources: array(number().int().positive()),
26530
+ /**
26531
+ * The canvas this profile composes onto, `WxH`, or empty when it is not
26532
+ * offered. DERIVED from the cells and the sources' own size at this profile —
26533
+ * it is reported because nothing else in the system would ever say what the
26534
+ * grid came out as, and because it is the number an operator would otherwise
26535
+ * expect to type.
26536
+ */
26537
+ canvas: string(),
26538
+ /**
26539
+ * Whether this profile is PUBLISHED, of the ones the grid could serve.
26540
+ *
26541
+ * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
26542
+ * a 4K canvas built from 4K decodes — something to opt into, not something a
26543
+ * viewer's adaptive should be handed by climbing to the top rung it can see.
26544
+ * Default is `mid` + `low`.
26545
+ */
26546
+ published: boolean()
26547
+ });
26548
+ var GridLayoutViewSchema = object({
26549
+ /** The persisted grid row this camera was declared from. */
26550
+ instanceId: string(),
26551
+ deviceId: number().int().nonnegative(),
26552
+ name: string(),
26553
+ /**
26554
+ * NO canvas size. A grid's resolution is not authored: each profile derives
26555
+ * its own from the cells and its sources' dimensions. The two numbers that
26556
+ * used to be here were a text field that silently decided both how much the
26557
+ * composite cost and how sharp it was — see `profiles[].canvas` for what it
26558
+ * came out as.
26559
+ */
26560
+ fps: number().int(),
26561
+ cells: array(GridLayoutCellSchema),
26562
+ /** What the catalog will publish, and what it refuses to. Read-only. */
26563
+ profiles: array(GridProfileOfferSchema)
26564
+ });
26565
+ var GridLayoutPatchSchema = object({
26566
+ deviceId: number().int().nonnegative(),
26567
+ name: string().min(1).max(160).optional(),
26568
+ fps: number().int().min(1).max(60).optional(),
26569
+ /** Which profiles to publish. See `GridProfileOffer.published`. */
26570
+ publishedProfiles: array(_enum([
26571
+ "high",
26572
+ "mid",
26573
+ "low"
26574
+ ])).max(3).optional(),
26575
+ /**
26576
+ * The whole cell list at once. A per-cell patch would need an ordering the
26577
+ * editor does not have, and a half-applied layout is a picture nobody asked
26578
+ * for.
26579
+ */
26580
+ cells: array(GridLayoutCellSchema).max(16)
26581
+ });
26582
+ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), GridLayoutViewSchema.nullable(), { auth: "admin" }), method(GridLayoutPatchSchema, GridLayoutViewSchema, {
26583
+ kind: "mutation",
26584
+ auth: "admin"
26585
+ });
26334
26586
  object({
26335
26587
  detected: boolean(),
26336
26588
  /** Ms epoch of the last transition. 0 if never observed. */
@@ -26804,307 +27056,6 @@ DeviceType.Camera, method(object({ deviceId: number() }), DayNightOptionsSchema)
26804
27056
  auth: "admin"
26805
27057
  });
26806
27058
  /**
26807
- * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
26808
- * writes to the CAMERA's own card, on the camera's own schedule.
26809
- *
26810
- * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
26811
- * footage ledger, our storage locations, our retention. This one has a
26812
- * different authority — the camera's firmware — and per D62 it stores
26813
- * nothing of its own. Every value here is read from the camera and every
26814
- * write goes back to the camera; there is no CamStack-side mirror that
26815
- * could disagree with the device.
26816
- *
26817
- * ## One shape, two firmwares
26818
- *
26819
- * Measured 2026-09-22 against the live fleet:
26820
- *
26821
- * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
26822
- * | --- | --- | --- |
26823
- * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
26824
- * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
26825
- * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
26826
- * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
26827
- * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
26828
- * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
26829
- * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
26830
- * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
26831
- *
26832
- * The two schedule models look different and are the same thing in
26833
- * different coordinates: both answer "for this trigger, during which
26834
- * weekly windows does the camera record". {@link RecordWindow} is that
26835
- * question in one shape — Hikvision's ranges map straight onto it,
26836
- * Reolink's mask expands into hour-aligned windows.
26837
- *
26838
- * ## Union, not intersection
26839
- *
26840
- * **The same fields exist on every camera.** What differs per device is
26841
- * which VALUES that device accepts, and that is what {@link
26842
- * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
26843
- * per field plus the schedule's own limits. A control a camera cannot
26844
- * honour is rendered DISABLED WITH ITS REASON, never missing and never
26845
- * dead: disabled must not look like broken.
26846
- *
26847
- * ## Refusal by name
26848
- *
26849
- * A write a camera cannot honour is refused with a sentence the operator
26850
- * can read — never accepted and dropped. Both providers refuse through
26851
- * {@link describeOnboardRefusal}, so the vocabulary is one function and
26852
- * one test, not two hand-written vendor opinions.
26853
- *
26854
- * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
26855
- * `getOptions` advertises per-camera availability, `getStatus` (auto-
26856
- * injected from `status`) reports the live values, and a single
26857
- * `setSettings` mutation applies a partial change. No hand-written
26858
- * settings-contribution methods.
26859
- */
26860
- /**
26861
- * What makes the camera start recording during a window.
26862
- *
26863
- * The union of both vendors' vocabularies. `continuous` is Hikvision's
26864
- * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
26865
- * object-class triggers are Reolink-only today and the smart-event ones
26866
- * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
26867
- * firmwares measured — a camera that cannot record on a trigger simply
26868
- * does not list it in `options.schedule.triggers`, and a window naming
26869
- * it is REFUSED, not dropped.
26870
- */
26871
- var RecordTriggerSchema = _enum([
26872
- "continuous",
26873
- "motion",
26874
- "person",
26875
- "vehicle",
26876
- "animal",
26877
- "lineCrossing",
26878
- "intrusion",
26879
- "loitering",
26880
- "alarmInput"
26881
- ]);
26882
- /**
26883
- * One weekly recording window: "on `day`, from `startMinute` to
26884
- * `endMinute`, record on `trigger`".
26885
- *
26886
- * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
26887
- * both firmwares enumerate). Minutes are local camera time since
26888
- * midnight; `endMinute` may be 1440, meaning end of day — that is
26889
- * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
26890
- * collapsing it to 0 would turn a whole-day window into an empty one.
26891
- */
26892
- var RecordWindowSchema = object({
26893
- trigger: RecordTriggerSchema,
26894
- day: number().int().min(0).max(6),
26895
- startMinute: number().int().min(0).max(1439),
26896
- endMinute: number().int().min(1).max(1440)
26897
- });
26898
- /** Status of one physical volume, as the camera itself describes it. */
26899
- var OnboardStorageVolumeSchema = object({
26900
- /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
26901
- id: string(),
26902
- /** The camera's own name for it, when it gives one (`hddName`). */
26903
- label: string().optional(),
26904
- status: _enum([
26905
- "ok",
26906
- "unformatted",
26907
- "error",
26908
- "offline",
26909
- "unknown"
26910
- ]),
26911
- /**
26912
- * Total size in MB, or **null when the camera did not say**.
26913
- *
26914
- * Never 0 for an unreadable value: a measurement that failed is not a
26915
- * measurement (D393), and a card whose size is unknown must not be
26916
- * rendered as a card of size zero.
26917
- */
26918
- capacityMb: number().nullable(),
26919
- /**
26920
- * Free space in MB, or null when unknown.
26921
- *
26922
- * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
26923
- * 1439 both report exactly 11776 MB free — the fixed reserve a looping
26924
- * card converges on once it has wrapped. At loop steady state the
26925
- * number is identical whether the camera recorded yesterday or stopped
26926
- * a month ago.
26927
- */
26928
- freeMb: number().nullable(),
26929
- /** True when the camera reports the volume writable (`property` RW). */
26930
- writable: boolean().optional()
26931
- });
26932
- object({
26933
- storage: discriminatedUnion("kind", [
26934
- object({
26935
- kind: literal("present"),
26936
- volumes: array(OnboardStorageVolumeSchema)
26937
- }),
26938
- object({
26939
- kind: literal("absent"),
26940
- reason: string()
26941
- }),
26942
- object({
26943
- kind: literal("unknown"),
26944
- reason: string()
26945
- })
26946
- ]),
26947
- tracks: array(object({
26948
- id: string(),
26949
- enabled: boolean(),
26950
- isVideo: boolean(),
26951
- /** From the camera's own track description. Null when it does not say. */
26952
- codec: string().nullable(),
26953
- resolution: string().nullable(),
26954
- /** Per-track overwrite flag, where the firmware keeps it per track. */
26955
- overwriteWhenFull: boolean().nullable()
26956
- })),
26957
- /**
26958
- * The track the write path targets — the enabled VIDEO one. Null when
26959
- * no track could be identified, which is itself a refusal reason.
26960
- */
26961
- primaryTrackId: string().nullable(),
26962
- /** Master "record to the card at all" switch. */
26963
- enabled: boolean().nullable(),
26964
- overwriteWhenFull: boolean().nullable(),
26965
- preRecordSec: number().nullable(),
26966
- postRecordSec: number().nullable(),
26967
- /** Length of one recorded file, in minutes. */
26968
- segmentMinutes: number().nullable(),
26969
- /** The primary track's weekly windows, flattened. */
26970
- windows: array(RecordWindowSchema),
26971
- /**
26972
- * How many windows the camera described that CamStack could NOT read —
26973
- * an unrecognised trigger, an unparseable clock, a weekday it does not
26974
- * name.
26975
- *
26976
- * A dropped window is work the reader threw away, and a schedule that
26977
- * silently shows fewer rows than the camera holds is how an operator
26978
- * saves back a schedule shorter than the one they were looking at
26979
- * (D391). Non-zero means the window list is INCOMPLETE and a write
26980
- * that replaces it would delete what was not shown — which is why a
26981
- * provider reporting a non-zero count also reports the schedule as not
26982
- * writable.
26983
- */
26984
- unreadableWindows: number(),
26985
- /**
26986
- * The camera is scheduled to record and has NO usable storage.
26987
- *
26988
- * A first-class fact because it is the fleet's most common silent
26989
- * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
26990
- * to a card that is not there. Neither the schedule nor the storage
26991
- * read says anything wrong on its own; only the pair does.
26992
- */
26993
- recordingToNowhere: boolean(),
26994
- lastFetchedAt: number()
26995
- });
26996
- /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
26997
- var RangeSchema = object({
26998
- min: number(),
26999
- max: number(),
27000
- step: number()
27001
- });
27002
- /**
27003
- * The values a camera actually takes for a numeric field, when they are a SET
27004
- * rather than a range.
27005
- *
27006
- * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
27007
- * (I91DN) on 2026-09-22 by writing each value and reading it back:
27008
- *
27009
- * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
27010
- * camera's "no limit" — `-1` and `4294967295` both land on it);
27011
- * - post-record: `5, 10, 30, 60, 120, 300, 600`.
27012
- *
27013
- * Neither is expressible as a step: the first has a sentinel two billion away
27014
- * from its neighbours, the second doubles and then jumps. A range that tried
27015
- * would forbid values the camera takes AND permit values it silently replaces
27016
- * with 5 — wrong in both directions at once.
27017
- *
27018
- * `sentinel` names the member that is not a duration, so a surface can render
27019
- * "no limit" instead of `2147483647` seconds.
27020
- */
27021
- var AllowedValuesSchema = object({
27022
- values: array(number()).min(1),
27023
- sentinel: object({
27024
- value: number(),
27025
- meaning: _enum(["no-limit", "disabled"])
27026
- }).optional()
27027
- });
27028
- /**
27029
- * Per-field availability on ONE camera.
27030
- *
27031
- * The field exists on every camera — this says whether this one can be
27032
- * read and whether it can be written, and `reason` says why not when
27033
- * either is false. The UI renders the control DISABLED with the reason
27034
- * rather than hiding it, so a limitation is legible instead of looking
27035
- * like a missing feature.
27036
- */
27037
- var OnboardFieldSupportSchema = object({
27038
- readable: boolean(),
27039
- writable: boolean(),
27040
- /** Required whenever `readable` or `writable` is false. */
27041
- reason: string().optional()
27042
- });
27043
- /** What this camera's schedule model can express. */
27044
- var OnboardScheduleSupportSchema = object({
27045
- support: OnboardFieldSupportSchema,
27046
- /**
27047
- * The smallest time step the camera can express, in minutes.
27048
- *
27049
- * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
27050
- * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
27051
- * window whose edges are not a multiple of this is REFUSED rather than
27052
- * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
27053
- * and nothing says so.
27054
- */
27055
- granularityMinutes: number(),
27056
- /** Triggers this camera can record on. A window naming another is refused. */
27057
- triggers: array(RecordTriggerSchema),
27058
- /**
27059
- * False when the camera stores ONE trigger per time range, so two
27060
- * windows overlapping on the same day cannot carry different triggers.
27061
- * True on Reolink, whose mask is per-trigger and independent.
27062
- */
27063
- supportsOverlappingTriggers: boolean()
27064
- });
27065
- var RecordingOnboardOptionsSchema = object({
27066
- enabled: OnboardFieldSupportSchema,
27067
- overwriteWhenFull: OnboardFieldSupportSchema,
27068
- preRecordSec: OnboardFieldSupportSchema,
27069
- preRecordSecRange: RangeSchema.optional(),
27070
- /** Preferred over the range when the camera takes a SET, not a span. */
27071
- preRecordSecAllowed: AllowedValuesSchema.optional(),
27072
- postRecordSec: OnboardFieldSupportSchema,
27073
- postRecordSecRange: RangeSchema.optional(),
27074
- /** Preferred over the range when the camera takes a SET, not a span. */
27075
- postRecordSecAllowed: AllowedValuesSchema.optional(),
27076
- segmentMinutes: OnboardFieldSupportSchema,
27077
- segmentMinutesRange: RangeSchema.optional(),
27078
- /** Preferred over the range when the camera takes a SET, not a span. */
27079
- segmentMinutesAllowed: AllowedValuesSchema.optional(),
27080
- schedule: OnboardScheduleSupportSchema
27081
- });
27082
- /**
27083
- * A partial change. Every field optional.
27084
- *
27085
- * Unlike the other `deviceConfig` caps, a provider here does **NOT**
27086
- * silently ignore a field it cannot support — it refuses, by name,
27087
- * through {@link describeOnboardRefusal}. Silence on a recording setting
27088
- * is the failure D62 exists to prevent: the operator believes the camera
27089
- * is recording the way the form says, and it is not.
27090
- */
27091
- var RecordingOnboardPatchSchema = object({
27092
- enabled: boolean().optional(),
27093
- overwriteWhenFull: boolean().optional(),
27094
- preRecordSec: number().optional(),
27095
- postRecordSec: number().optional(),
27096
- segmentMinutes: number().optional(),
27097
- /** The complete new window set for the primary track — not a delta. */
27098
- windows: array(RecordWindowSchema).optional()
27099
- });
27100
- DeviceType.Camera, method(object({ deviceId: number() }), RecordingOnboardOptionsSchema), method(object({
27101
- deviceId: number(),
27102
- settings: RecordingOnboardPatchSchema
27103
- }), _void(), {
27104
- kind: "mutation",
27105
- auth: "admin"
27106
- });
27107
- /**
27108
27059
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
27109
27060
  * for every device, regardless of provider — the kernel needs a uniform
27110
27061
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -27198,30 +27149,6 @@ object({
27198
27149
  eventCountSinceStart: number()
27199
27150
  });
27200
27151
  DeviceType.EventEmitter;
27201
- var EventItemSchema = object({
27202
- id: string(),
27203
- type: string(),
27204
- timestamp: number(),
27205
- label: string().optional(),
27206
- thumbnailUrl: string().optional(),
27207
- clipUrl: string().optional(),
27208
- metadata: record(string(), unknown()).optional()
27209
- });
27210
- DeviceType.Camera, method(object({
27211
- deviceId: number(),
27212
- from: number().optional(),
27213
- to: number().optional(),
27214
- limit: number().optional()
27215
- }), array(EventItemSchema)), method(object({
27216
- deviceId: number(),
27217
- eventId: string()
27218
- }), object({
27219
- base64: string(),
27220
- contentType: string()
27221
- }).nullable()), method(object({
27222
- deviceId: number(),
27223
- eventId: string()
27224
- }), string().nullable());
27225
27152
  var IdentitySchema = object({
27226
27153
  id: string(),
27227
27154
  name: string(),
@@ -28893,143 +28820,6 @@ DeviceType.Light, DeviceType.Siren, DeviceType.Switch, method(object({
28893
28820
  lastChangedAt: number()
28894
28821
  });
28895
28822
  /**
28896
- * camera-grid-layout — the geometry of a COMPOSITE camera, on the camera's own
28897
- * page.
28898
- *
28899
- * ## Why this is a capability and not an addon settings schema
28900
- *
28901
- * It was one, and it did not render. The addon declared the editor as a
28902
- * `type: 'widget'` field inside its own `deviceSettingsSchema()`; the hub
28903
- * returned that section correctly and `ConfigFormField` renders `type:'widget'`
28904
- * perfectly well — and nothing ever asked camera-grid for it. The Cluster →
28905
- * Pipeline → Device Overrides page interrogates a HAND-WRITTEN list of four
28906
- * addons (`PIPELINE_CLUSTER_DEVICE_ADDONS`), whose own comment says an addon
28907
- * not on it "falls off silently".
28908
- *
28909
- * Adding a fifth name to that list would have been the wrong fix twice over:
28910
- * that page is per-camera DETECTION tuning, and a grid's geometry belongs
28911
- * beside PTZ and motion zones on the camera itself. The device page is
28912
- * BINDING-driven (D12), so the way in is a capability bound to the device —
28913
- * and this cap carries its section the way `recording` does, by RETURNING it
28914
- * from `getDeviceSettingsContribution`.
28915
- *
28916
- * Seven other widgets are still declared the other way, through a
28917
- * `deviceConfig.ui` block the framework derives a section from. That route
28918
- * gives the addon no say in where its own panel lands and no way to decline
28919
- * for a device the panel does not suit, which is why this one does not use it.
28920
- *
28921
- * ## Why one addon may implement it
28922
- *
28923
- * It is a device-scoped NATIVE cap, registered by the grid camera device
28924
- * itself. Nothing else declares a composite camera, so nothing else has a
28925
- * layout — and the device-scoped route means the widget asks THE camera, not
28926
- * "the camera-grid addon", which is what let the old custom-action pair be
28927
- * reached only by a caller that already knew the addon id.
28928
- *
28929
- * ## The tab
28930
- *
28931
- * `streaming`, not a top-tab of its own. A grid's geometry IS what its stream
28932
- * is, so the Streaming tab is where it belongs; a `grid` top-tab would need an
28933
- * entry in `WELL_KNOWN_TAB_MAP` or the device page renders the raw id as the
28934
- * label (measured on the robot camera, 2026-09-06 — a tab called "navigation"
28935
- * next to "PTZ").
28936
- */
28937
- /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
28938
- var GridNormalizedRectSchema = object({
28939
- x: number().min(0).max(1),
28940
- y: number().min(0).max(1),
28941
- width: number().gt(0).max(1),
28942
- height: number().gt(0).max(1)
28943
- });
28944
- /**
28945
- * One source camera, the part of its picture taken, and where that part lands.
28946
- *
28947
- * Both rectangles are NORMALIZED (D519): a source camera can change resolution
28948
- * — a profile switch, a firmware update, a substream that comes back different
28949
- * — and a stored PIXEL rectangle would quietly start cutting the wrong region,
28950
- * which is the class of bug nobody files.
28951
- */
28952
- var GridLayoutCellSchema = object({
28953
- deviceId: number().int().positive(),
28954
- /** The part of the SOURCE taken, normalized against the source. */
28955
- source: GridNormalizedRectSchema,
28956
- /** Where it lands, normalized against the CANVAS. */
28957
- cell: GridNormalizedRectSchema
28958
- });
28959
- /**
28960
- * Which profiles this grid can actually compose, and why not.
28961
- *
28962
- * A grid's `high` composes its sources' `high` and its `low` their `low`, so a
28963
- * profile is on offer only when EVERY source can serve it. The refusal NAMES
28964
- * the sources, because "this grid has no low" is not a finding — "615 has no
28965
- * low" is, and it is the one an operator can act on.
28966
- */
28967
- var GridProfileOfferSchema = object({
28968
- profile: _enum([
28969
- "high",
28970
- "mid",
28971
- "low"
28972
- ]),
28973
- offered: boolean(),
28974
- /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
28975
- missingSources: array(number().int().positive()),
28976
- /**
28977
- * The canvas this profile composes onto, `WxH`, or empty when it is not
28978
- * offered. DERIVED from the cells and the sources' own size at this profile —
28979
- * it is reported because nothing else in the system would ever say what the
28980
- * grid came out as, and because it is the number an operator would otherwise
28981
- * expect to type.
28982
- */
28983
- canvas: string(),
28984
- /**
28985
- * Whether this profile is PUBLISHED, of the ones the grid could serve.
28986
- *
28987
- * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
28988
- * a 4K canvas built from 4K decodes — something to opt into, not something a
28989
- * viewer's adaptive should be handed by climbing to the top rung it can see.
28990
- * Default is `mid` + `low`.
28991
- */
28992
- published: boolean()
28993
- });
28994
- var GridLayoutViewSchema = object({
28995
- /** The persisted grid row this camera was declared from. */
28996
- instanceId: string(),
28997
- deviceId: number().int().nonnegative(),
28998
- name: string(),
28999
- /**
29000
- * NO canvas size. A grid's resolution is not authored: each profile derives
29001
- * its own from the cells and its sources' dimensions. The two numbers that
29002
- * used to be here were a text field that silently decided both how much the
29003
- * composite cost and how sharp it was — see `profiles[].canvas` for what it
29004
- * came out as.
29005
- */
29006
- fps: number().int(),
29007
- cells: array(GridLayoutCellSchema),
29008
- /** What the catalog will publish, and what it refuses to. Read-only. */
29009
- profiles: array(GridProfileOfferSchema)
29010
- });
29011
- var GridLayoutPatchSchema = object({
29012
- deviceId: number().int().nonnegative(),
29013
- name: string().min(1).max(160).optional(),
29014
- fps: number().int().min(1).max(60).optional(),
29015
- /** Which profiles to publish. See `GridProfileOffer.published`. */
29016
- publishedProfiles: array(_enum([
29017
- "high",
29018
- "mid",
29019
- "low"
29020
- ])).max(3).optional(),
29021
- /**
29022
- * The whole cell list at once. A per-cell patch would need an ordering the
29023
- * editor does not have, and a half-applied layout is a picture nobody asked
29024
- * for.
29025
- */
29026
- cells: array(GridLayoutCellSchema).max(16)
29027
- });
29028
- DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), GridLayoutViewSchema.nullable(), { auth: "admin" }), method(GridLayoutPatchSchema, GridLayoutViewSchema, {
29029
- kind: "mutation",
29030
- auth: "admin"
29031
- });
29032
- /**
29033
28823
  * Motion-zones share the same MaskShape vocabulary as privacy-mask — the
29034
28824
  * on-camera motion-detection mask is a single `grid` region (a row-major
29035
28825
  * boolean cell lattice the camera's onboard VMD evaluates). Composing it as
@@ -30841,37 +30631,37 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceI
30841
30631
  kind: "mutation",
30842
30632
  auth: "admin"
30843
30633
  });
30844
- /**
30845
- * `recording` cap — footage availability + HLS playback manifests + per-device
30846
- * recording config. NOTE on events (source of truth, R5/C3): this cap carries
30847
- * NO event surface — `getPlaybackManifest` returns playlist URLs only. Timeline
30848
- * events (motion/object/audio) come from `pipelineAnalytics` (durable SQLite
30849
- * rows) and are the ONLY event surface — the recorder has none. The in-RAM
30850
- * playback markers it used to build were deleted on 2026-08-29 because nothing
30851
- * ever read them. Event<->footage joins are by time, padded with the shared
30852
- * `EVENT_PAD_MS` (`interfaces/recording-config.ts`).
30853
- */
30854
- var RecordingStatusSchema = object({
30855
- deviceId: number(),
30856
- enabled: boolean(),
30857
- /** THE derived storage mode, from the one definition
30858
- * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
30859
- * `on-device-decision` could have reached the recorder and not the status. */
30860
- activeMode: RecordingStorageModeSchema,
30861
- nodeId: string(),
30862
- storageBytes: number()
30863
- });
30864
30634
  var RecordingRangeSchema = object({
30865
30635
  profile: string(),
30866
30636
  startMs: number(),
30867
30637
  endMs: number()
30868
30638
  });
30639
+ /**
30640
+ * How a source ANSWERED, on every singular read of this cap.
30641
+ *
30642
+ * `'read'` — it looked. `ranges: []` / `days: []` is then a real claim: this
30643
+ * source has no coverage in the window. `'unreadable'` — nobody could look
30644
+ * (the camera was unreachable, the calendar rung threw, the location is
30645
+ * unmounted, the node is still on the old build), and the emptiness beside it
30646
+ * means NOTHING.
30647
+ *
30648
+ * The batch rows have carried this since the grid existed; the SINGULAR
30649
+ * answers gained it with the collection (D625 §10.4), because they are the
30650
+ * ones the single-camera picker uses and because a half-converted fleet makes
30651
+ * "nobody looked" common for the length of a deploy. Without it the timeline
30652
+ * has no vocabulary for it, and `(data ?? [])` in a viewer turns a rollout into
30653
+ * a fleet of cameras that appear to have lost their recordings (D315, D393).
30654
+ */
30655
+ var RecordingReadSchema = _enum(["read", "unreadable"]);
30869
30656
  var RecordingAvailabilitySchema = object({
30870
30657
  deviceId: number(),
30658
+ /** See {@link RecordingReadSchema}. An `'unreadable'` answer carries an empty
30659
+ * `ranges` that means nothing — never draw it as "no footage". */
30660
+ read: RecordingReadSchema,
30871
30661
  ranges: array(RecordingRangeSchema),
30872
30662
  /**
30873
- * Every profile this camera has footage in — not only the one `ranges`
30874
- * describes (D433).
30663
+ * Every profile this camera has footage in AT THIS SOURCE — not only the one
30664
+ * `ranges` describes (D433).
30875
30665
  *
30876
30666
  * `ranges` answers for ONE profile by design: the timeline is a single bar,
30877
30667
  * and enumerating all of them triples the directory reads for a bar that
@@ -30888,15 +30678,285 @@ var RecordingAvailabilitySchema = object({
30888
30678
  });
30889
30679
  var RecordingDaysSchema = object({
30890
30680
  deviceId: number(),
30681
+ /** See {@link RecordingReadSchema}. `days: []` on an `'unreadable'` answer is
30682
+ * "nobody could look", and the date-picker must not spell it the same as
30683
+ * "no footage this month". */
30684
+ read: RecordingReadSchema,
30891
30685
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
30892
30686
  days: array(number())
30893
30687
  });
30688
+ var RecordingManifestSchema = object({
30689
+ deviceId: number(),
30690
+ /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
30691
+ localMasterPath: string().nullable(),
30692
+ /** HTTP(S) URL to the master playlist on the recording node's playback server
30693
+ * (the PRIMARY candidate); null when no recording / server. Carries the
30694
+ * scoped playback token in its path. */
30695
+ playbackUrl: string().nullable(),
30696
+ /**
30697
+ * Candidate master-playlist URLs the client tries in order (LAN first, then
30698
+ * remote — Tailscale/Cloudflare if the operator configured extra hosts), each
30699
+ * carrying the same scoped token. `playbackUrl` is the first entry. Empty when
30700
+ * there is no recording / server.
30701
+ */
30702
+ playbackEndpoints: array(string())
30703
+ });
30704
+ var RecordingSourceAvailabilitySchema = object({
30705
+ state: _enum([
30706
+ "ok",
30707
+ "sleeping",
30708
+ "unreachable",
30709
+ "no-storage",
30710
+ "index-empty"
30711
+ ]),
30712
+ /** Free text, shown verbatim. Names the camera's own refusal when there is one. */
30713
+ reason: string().optional(),
30714
+ /** When this source's coverage was last CONFIRMED. A cached answer is never
30715
+ * drawn as current: the surface shows the age whenever it is older than the
30716
+ * refresh interval. The clip catalog's `catalogAsOf`, under the name the
30717
+ * timeline uses for it. */
30718
+ coverageAsOf: number().optional()
30719
+ });
30720
+ /**
30721
+ * One SOURCE of recorded coverage for a camera — a row of the picker.
30722
+ *
30723
+ * A provider lists the sources IT serves for that device, and answers for each
30724
+ * of them whether it can answer at all. A provider with nothing to offer on a
30725
+ * camera returns `[]` — it is not that camera's business. The five availability
30726
+ * states are `ClipSourceAvailability`'s verbatim: they mean exactly the same
30727
+ * things about a coverage index as about a clip catalog, and `sleeping` in
30728
+ * particular is what stops a battery camera being woken to paint a bar.
30729
+ */
30730
+ var RecordingSourceSchema = object({
30731
+ /** The source id. {@link RECORDING_SOURCE_CAMSTACK} for ours (RESERVED), a
30732
+ * vendor namespace (`native:reolink:onboard`, …) for a camera's own store. */
30733
+ source: string(),
30734
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
30735
+ label: string(),
30736
+ /**
30737
+ * The addon that SERVES this row, and the value a later call passes as
30738
+ * `provider`.
30739
+ *
30740
+ * Optional for version skew only. The collection dispatcher stamps it from
30741
+ * the registry, so a row that travelled through the fan-out carries the
30742
+ * authoritative id whatever the provider filled in (D557 §4).
30743
+ */
30744
+ addonId: string().optional(),
30745
+ availability: RecordingSourceAvailabilitySchema
30746
+ });
30747
+ /**
30748
+ * What a surface may DRAW for this (camera, source) — D612's rule applied to a
30749
+ * timeline: **the source declares what it can do, and the surface draws what
30750
+ * was declared. It never assumes, and never offers a gesture it will then
30751
+ * refuse.** D612 exists because `8` and `16` were offered as clip rates, the
30752
+ * broker clamped them to `4`, and no line anywhere said so.
30753
+ *
30754
+ * Asked once per (camera, source) before anything is drawn — never replaced by
30755
+ * a constant the surface keeps, which is the second authority D612 ends.
30756
+ */
30757
+ var RecordingSourceOptionsSchema = object({
30758
+ /** How this source's media reaches the player.
30759
+ * `archive` = our own indexed segment tree; `stream` = the provider's
30760
+ * forward-only fMP4 (D597); `realtime` = a replay bound to wall clock. */
30761
+ transport: _enum([
30762
+ "archive",
30763
+ "stream",
30764
+ "realtime"
30765
+ ]),
30766
+ /** What the BAR means. `continuous` = gaps are holes in a recording;
30767
+ * `sparse` = gaps are the absence of one, and must be drawn as such.
30768
+ *
30769
+ * Not an onboard-only concession: measured 2026-09-24, OUR bar covers 98.8 %
30770
+ * of 592's day and 1.2 % of 1436's. It is a fact about a (source, camera)
30771
+ * pair, and ours answers it per camera from `deriveRecordingMode`. */
30772
+ coverage: _enum(["continuous", "sparse"]),
30773
+ /** Where the playhead may be put.
30774
+ * `free` — anywhere, to the frame.
30775
+ * `forward` — only ahead of the current position.
30776
+ * `segment` — a position SNAPS to the head of the covering segment; a finer
30777
+ * ask is accepted by the camera and SILENTLY IGNORED. Measured
30778
+ * on 1436 (Hikvision V5.7.1, 2026-09-23): a window-narrowed
30779
+ * `ContentMgmt/search` returns a row and a `playbackURI`, the
30780
+ * replay opens 200 and delivers media — and the burned-in OSD of
30781
+ * the first frame reads the SEGMENT HEAD every time. Calling
30782
+ * that `forward` would tell the surface it may move the playhead
30783
+ * ahead within a loaded segment, which it may not. */
30784
+ seek: _enum([
30785
+ "free",
30786
+ "forward",
30787
+ "segment"
30788
+ ]),
30789
+ /** Frame-step BACKWARD is meaningful. */
30790
+ stepBack: boolean(),
30791
+ /** Whether the drag-scrub gesture is served, as opposed to refused by name. */
30792
+ scrub: boolean(),
30793
+ /** Deliverable rates, ascending, always containing `1`. The surface draws its
30794
+ * picker from this and from NOTHING else (D612, D620, D621). `0` is not a
30795
+ * member: pause is the absence of a rate. */
30796
+ rates: array(number().positive()).min(1).readonly(),
30797
+ /** TRUE when a read of this source HOLDS the camera's only playback session.
30798
+ * A surface with this set makes at most ONE read at a time and draws no
30799
+ * scrub-thumbnail strip, no hover preview, no prefetch and no background
30800
+ * refresh. The precedent is exact and expensive: filling one screen of
30801
+ * Hikvision thumbnails at 1.01× realtime consumed fifteen minutes of that
30802
+ * camera's only playback session (1.2.126, reported within minutes), and a
30803
+ * timeline is a screenful of reads by construction. */
30804
+ exclusive: boolean()
30805
+ });
30806
+ /**
30807
+ * How to PLAY the instant that was asked for, from the chosen source.
30808
+ *
30809
+ * No new media transport is built for onboard sources: the `clip` arm is a
30810
+ * DELEGATION to the `videoclips` transport that vendor already has (D597 /
30811
+ * D616 / D617). The onboard half of this collection is a PROJECTION of
30812
+ * `videoclips` for coverage and a delegation to it for bytes.
30813
+ */
30814
+ var RecordingPlaybackSchema = discriminatedUnion("kind", [
30815
+ object({
30816
+ kind: literal("hls"),
30817
+ manifest: RecordingManifestSchema
30818
+ }),
30819
+ object({
30820
+ kind: literal("clip"),
30821
+ /** The `videoclips` source namespace this clip id belongs to. */
30822
+ source: string(),
30823
+ clipId: string(),
30824
+ /** Where this clip actually STARTS. On a `seek: 'segment'` source the
30825
+ * playhead lands here, not at the requested instant — the surface must be
30826
+ * TOLD, not left to discover it from a burned-in OSD. */
30827
+ startsAtMs: number()
30828
+ }),
30829
+ object({
30830
+ kind: literal("none"),
30831
+ reason: string()
30832
+ })
30833
+ ]);
30834
+ DeviceType.Camera, method(object({ deviceId: number() }), array(RecordingSourceSchema).readonly(), {
30835
+ kind: "query",
30836
+ auth: "protected"
30837
+ }), method(object({
30838
+ deviceId: number(),
30839
+ /**
30840
+ * WHICH provider to ask — the `addonId` a {@link RecordingSourceSchema}
30841
+ * row carries, never a source id and never a list. **REQUIRED**, in the
30842
+ * schema, where the generated types make it unomittable rather than
30843
+ * merely discouraged (D554 amended).
30844
+ *
30845
+ * It was learned the expensive way on `videoclips.listClips`: measured
30846
+ * on the live hub 2026-09-20, device 592 bound to `recorder` AND
30847
+ * `provider-reolink`, a bare call with `limit: 3` answered SIX rows,
30848
+ * three from each source, merged — `device-collection-dispatch.ts`
30849
+ * leaves an unpinned fan-out un-narrowed, so absence buys the union the
30850
+ * method exists not to be. An un-narrowed `getAvailability` would do
30851
+ * that to a TIMELINE: our ranges and the card's clips unioned into one
30852
+ * bar, which is "two sources are never drawn together" broken in the
30853
+ * one place it matters most.
30854
+ *
30855
+ * A provider the device is not bound to is refused BY NAME (D552's
30856
+ * `rejectUnresolvedAddonPin`), never answered by another one.
30857
+ */
30858
+ provider: string().min(1),
30859
+ fromMs: number(),
30860
+ toMs: number(),
30861
+ /**
30862
+ * Answer for THIS profile instead of the source's preferred one (D433).
30863
+ * Absent keeps the timeline's behaviour — one bar, one profile, one set
30864
+ * of reads. `profilesWithFootage` on the answer says what may be asked
30865
+ * for.
30866
+ */
30867
+ profile: string().optional()
30868
+ }), RecordingAvailabilitySchema, {
30869
+ kind: "query",
30870
+ auth: "protected"
30871
+ }), method(object({
30872
+ deviceId: number(),
30873
+ provider: string().min(1),
30874
+ fromMs: number(),
30875
+ toMs: number(),
30876
+ tzOffsetMinutes: number()
30877
+ }), RecordingDaysSchema, {
30878
+ kind: "query",
30879
+ auth: "protected"
30880
+ }), method(object({
30881
+ deviceId: number(),
30882
+ provider: string().min(1),
30883
+ fromMs: number(),
30884
+ toMs: number(),
30885
+ profile: CamProfileSchema.optional()
30886
+ }), RecordingPlaybackSchema, {
30887
+ kind: "query",
30888
+ auth: "protected"
30889
+ }), method(object({
30890
+ deviceId: number(),
30891
+ provider: string().min(1)
30892
+ }), RecordingSourceOptionsSchema, {
30893
+ kind: "query",
30894
+ auth: "protected"
30895
+ });
30896
+ /**
30897
+ * `recording-archive` — OUR archive, and the intent that fills it.
30898
+ *
30899
+ * The system-singleton half of the 2026-09-24 cut (D625). `recording` used to
30900
+ * be one 33-method system singleton holding two unrelated subjects: three
30901
+ * per-camera READS about coverage and playback, and everything else — storage
30902
+ * locations, retention, relocation, rebalance, the ops log, the placement
30903
+ * table and the byte-plane primitives our scrub and export are built on.
30904
+ *
30905
+ * The reads became a device-scoped COLLECTION, so a camera's own card can be a
30906
+ * source beside ours (`recording.cap.ts`). Everything that is about OUR store,
30907
+ * or unimplementable by a camera, stayed here.
30908
+ *
30909
+ * ## On the name
30910
+ *
30911
+ * `recording-storage` was the obvious choice and is wrong: this cap also holds
30912
+ * `getDeviceConfig`/`setDeviceConfig`, which are recording INTENT — bands,
30913
+ * retention, the D62 switch authority — and a name that says "storage" invites
30914
+ * the next reader to move them out again. An archive is a thing we keep, and
30915
+ * what we keep it under is a policy; the name covers both halves honestly and
30916
+ * sits in the existing family (`recording-onboard`, `recording-export`,
30917
+ * `recording-signal`).
30918
+ *
30919
+ * ## What must NOT happen to it
30920
+ *
30921
+ * It stays a SINGLETON. It is registered by `recorder`, which is
30922
+ * `placement: 'any-node'` and runs on every recording node; the hub dispatches
30923
+ * to one of them. Putting the ledger, the placement table or the relocation
30924
+ * jobs behind a fan-out is the one genuinely dangerous move in this cut.
30925
+ *
30926
+ * `getDeviceConfig` / `setDeviceConfig` in particular are the D62 recording
30927
+ * authority (`CameraSwitch.authority`). If a write reached a different provider
30928
+ * than the read — which a collection fan-out permits — two authorities would
30929
+ * decide when one camera records, and the symptom (recording silently off, or
30930
+ * a `bands` array clobbered by a partial write) is durable and silent. Keeping
30931
+ * them here means the worst case during a rollout is a 412: the switch refuses
30932
+ * to flip and SAYS so. **Do not move them into the collection, at any point,
30933
+ * for any reason.**
30934
+ *
30935
+ * ## The two batch reads
30936
+ *
30937
+ * `getAvailabilityBatch` / `getDaysWithRecordingsBatch` take `deviceIds:
30938
+ * number[]` with no single `deviceId`, and a device-scoped mount routes
30939
+ * through `getProviderForDevice(deviceId)` — there is nothing for it to route
30940
+ * on. They stay here, and on this cap the batch is explicitly OURS: a grid has
30941
+ * no per-camera picker, and a caller that wants another source's coverage asks
30942
+ * `recording.getAvailability` per device with that source's `provider`.
30943
+ */
30944
+ var RecordingStatusSchema = object({
30945
+ deviceId: number(),
30946
+ enabled: boolean(),
30947
+ /** THE derived storage mode, from the one definition
30948
+ * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
30949
+ * `on-device-decision` could have reached the recorder and not the status. */
30950
+ activeMode: RecordingStorageModeSchema,
30951
+ nodeId: string(),
30952
+ storageBytes: number()
30953
+ });
30894
30954
  /**
30895
30955
  * One camera's row in a `getAvailabilityBatch` answer.
30896
30956
  *
30897
- * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
30898
- * batch collapses the transport, not the work — plus the one thing the singular
30899
- * method never had to say:
30957
+ * `ranges` is EXACTLY what `recording.getAvailability` returns for that camera
30958
+ * at OUR source — the batch collapses the transport, not the work — plus the
30959
+ * `read` mark the singular answer now carries too (D625):
30900
30960
  *
30901
30961
  * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
30902
30962
  * no footage in the window", which is a real claim.
@@ -30928,22 +30988,6 @@ var RecordingDaysForDeviceSchema = object({
30928
30988
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
30929
30989
  days: array(number()).readonly()
30930
30990
  });
30931
- var RecordingManifestSchema = object({
30932
- deviceId: number(),
30933
- /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
30934
- localMasterPath: string().nullable(),
30935
- /** HTTP(S) URL to the master playlist on the recording node's playback server
30936
- * (the PRIMARY candidate); null when no recording / server. Carries the
30937
- * scoped playback token in its path. */
30938
- playbackUrl: string().nullable(),
30939
- /**
30940
- * Candidate master-playlist URLs the client tries in order (LAN first, then
30941
- * remote — Tailscale/Cloudflare if the operator configured extra hosts), each
30942
- * carrying the same scoped token. `playbackUrl` is the first entry. Empty when
30943
- * there is no recording / server.
30944
- */
30945
- playbackEndpoints: array(string())
30946
- });
30947
30991
  /**
30948
30992
  * Recording storage usage for one camera — what the ARCHIVE holds for it,
30949
30993
  * across every profile and every resolvable location on this node.
@@ -31202,34 +31246,12 @@ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
31202
31246
  segmentEndMs: number()
31203
31247
  })]);
31204
31248
  method(object({
31205
- deviceId: number(),
31206
- fromMs: number(),
31207
- toMs: number(),
31208
- /**
31209
- * Answer for THIS profile instead of the preferred one (D433). Absent
31210
- * keeps the timeline's behaviour — one bar, one profile, one set of
31211
- * reads. `profilesWithFootage` on the answer says what may be asked
31212
- * for.
31213
- */
31214
- profile: string().optional()
31215
- }), RecordingAvailabilitySchema, {
31216
- kind: "query",
31217
- auth: "protected"
31218
- }), method(object({
31219
31249
  deviceIds: array(number()).min(1).max(200),
31220
31250
  fromMs: number(),
31221
31251
  toMs: number()
31222
31252
  }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
31223
31253
  kind: "query",
31224
31254
  auth: "protected"
31225
- }), method(object({
31226
- deviceId: number(),
31227
- fromMs: number(),
31228
- toMs: number(),
31229
- tzOffsetMinutes: number()
31230
- }), RecordingDaysSchema, {
31231
- kind: "query",
31232
- auth: "protected"
31233
31255
  }), method(object({
31234
31256
  deviceIds: array(number()).min(1).max(200),
31235
31257
  fromMs: number(),
@@ -31238,13 +31260,6 @@ method(object({
31238
31260
  }), array(RecordingDaysForDeviceSchema).readonly(), {
31239
31261
  kind: "query",
31240
31262
  auth: "protected"
31241
- }), method(object({
31242
- deviceId: number(),
31243
- fromMs: number(),
31244
- toMs: number()
31245
- }), RecordingManifestSchema, {
31246
- kind: "query",
31247
- auth: "protected"
31248
31263
  }), method(object({}), RecordingStorageUsageSchema, {
31249
31264
  kind: "query",
31250
31265
  auth: "admin"
@@ -31692,6 +31707,307 @@ method(object({
31692
31707
  auth: "protected"
31693
31708
  });
31694
31709
  /**
31710
+ * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
31711
+ * writes to the CAMERA's own card, on the camera's own schedule.
31712
+ *
31713
+ * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
31714
+ * footage ledger, our storage locations, our retention. This one has a
31715
+ * different authority — the camera's firmware — and per D62 it stores
31716
+ * nothing of its own. Every value here is read from the camera and every
31717
+ * write goes back to the camera; there is no CamStack-side mirror that
31718
+ * could disagree with the device.
31719
+ *
31720
+ * ## One shape, two firmwares
31721
+ *
31722
+ * Measured 2026-09-22 against the live fleet:
31723
+ *
31724
+ * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
31725
+ * | --- | --- | --- |
31726
+ * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
31727
+ * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
31728
+ * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
31729
+ * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
31730
+ * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
31731
+ * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
31732
+ * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
31733
+ * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
31734
+ *
31735
+ * The two schedule models look different and are the same thing in
31736
+ * different coordinates: both answer "for this trigger, during which
31737
+ * weekly windows does the camera record". {@link RecordWindow} is that
31738
+ * question in one shape — Hikvision's ranges map straight onto it,
31739
+ * Reolink's mask expands into hour-aligned windows.
31740
+ *
31741
+ * ## Union, not intersection
31742
+ *
31743
+ * **The same fields exist on every camera.** What differs per device is
31744
+ * which VALUES that device accepts, and that is what {@link
31745
+ * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
31746
+ * per field plus the schedule's own limits. A control a camera cannot
31747
+ * honour is rendered DISABLED WITH ITS REASON, never missing and never
31748
+ * dead: disabled must not look like broken.
31749
+ *
31750
+ * ## Refusal by name
31751
+ *
31752
+ * A write a camera cannot honour is refused with a sentence the operator
31753
+ * can read — never accepted and dropped. Both providers refuse through
31754
+ * {@link describeOnboardRefusal}, so the vocabulary is one function and
31755
+ * one test, not two hand-written vendor opinions.
31756
+ *
31757
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
31758
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
31759
+ * injected from `status`) reports the live values, and a single
31760
+ * `setSettings` mutation applies a partial change. No hand-written
31761
+ * settings-contribution methods.
31762
+ */
31763
+ /**
31764
+ * What makes the camera start recording during a window.
31765
+ *
31766
+ * The union of both vendors' vocabularies. `continuous` is Hikvision's
31767
+ * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
31768
+ * object-class triggers are Reolink-only today and the smart-event ones
31769
+ * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
31770
+ * firmwares measured — a camera that cannot record on a trigger simply
31771
+ * does not list it in `options.schedule.triggers`, and a window naming
31772
+ * it is REFUSED, not dropped.
31773
+ */
31774
+ var RecordTriggerSchema = _enum([
31775
+ "continuous",
31776
+ "motion",
31777
+ "person",
31778
+ "vehicle",
31779
+ "animal",
31780
+ "lineCrossing",
31781
+ "intrusion",
31782
+ "loitering",
31783
+ "alarmInput"
31784
+ ]);
31785
+ /**
31786
+ * One weekly recording window: "on `day`, from `startMinute` to
31787
+ * `endMinute`, record on `trigger`".
31788
+ *
31789
+ * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
31790
+ * both firmwares enumerate). Minutes are local camera time since
31791
+ * midnight; `endMinute` may be 1440, meaning end of day — that is
31792
+ * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
31793
+ * collapsing it to 0 would turn a whole-day window into an empty one.
31794
+ */
31795
+ var RecordWindowSchema = object({
31796
+ trigger: RecordTriggerSchema,
31797
+ day: number().int().min(0).max(6),
31798
+ startMinute: number().int().min(0).max(1439),
31799
+ endMinute: number().int().min(1).max(1440)
31800
+ });
31801
+ /** Status of one physical volume, as the camera itself describes it. */
31802
+ var OnboardStorageVolumeSchema = object({
31803
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
31804
+ id: string(),
31805
+ /** The camera's own name for it, when it gives one (`hddName`). */
31806
+ label: string().optional(),
31807
+ status: _enum([
31808
+ "ok",
31809
+ "unformatted",
31810
+ "error",
31811
+ "offline",
31812
+ "unknown"
31813
+ ]),
31814
+ /**
31815
+ * Total size in MB, or **null when the camera did not say**.
31816
+ *
31817
+ * Never 0 for an unreadable value: a measurement that failed is not a
31818
+ * measurement (D393), and a card whose size is unknown must not be
31819
+ * rendered as a card of size zero.
31820
+ */
31821
+ capacityMb: number().nullable(),
31822
+ /**
31823
+ * Free space in MB, or null when unknown.
31824
+ *
31825
+ * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
31826
+ * 1439 both report exactly 11776 MB free — the fixed reserve a looping
31827
+ * card converges on once it has wrapped. At loop steady state the
31828
+ * number is identical whether the camera recorded yesterday or stopped
31829
+ * a month ago.
31830
+ */
31831
+ freeMb: number().nullable(),
31832
+ /** True when the camera reports the volume writable (`property` RW). */
31833
+ writable: boolean().optional()
31834
+ });
31835
+ object({
31836
+ storage: discriminatedUnion("kind", [
31837
+ object({
31838
+ kind: literal("present"),
31839
+ volumes: array(OnboardStorageVolumeSchema)
31840
+ }),
31841
+ object({
31842
+ kind: literal("absent"),
31843
+ reason: string()
31844
+ }),
31845
+ object({
31846
+ kind: literal("unknown"),
31847
+ reason: string()
31848
+ })
31849
+ ]),
31850
+ tracks: array(object({
31851
+ id: string(),
31852
+ enabled: boolean(),
31853
+ isVideo: boolean(),
31854
+ /** From the camera's own track description. Null when it does not say. */
31855
+ codec: string().nullable(),
31856
+ resolution: string().nullable(),
31857
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
31858
+ overwriteWhenFull: boolean().nullable()
31859
+ })),
31860
+ /**
31861
+ * The track the write path targets — the enabled VIDEO one. Null when
31862
+ * no track could be identified, which is itself a refusal reason.
31863
+ */
31864
+ primaryTrackId: string().nullable(),
31865
+ /** Master "record to the card at all" switch. */
31866
+ enabled: boolean().nullable(),
31867
+ overwriteWhenFull: boolean().nullable(),
31868
+ preRecordSec: number().nullable(),
31869
+ postRecordSec: number().nullable(),
31870
+ /** Length of one recorded file, in minutes. */
31871
+ segmentMinutes: number().nullable(),
31872
+ /** The primary track's weekly windows, flattened. */
31873
+ windows: array(RecordWindowSchema),
31874
+ /**
31875
+ * How many windows the camera described that CamStack could NOT read —
31876
+ * an unrecognised trigger, an unparseable clock, a weekday it does not
31877
+ * name.
31878
+ *
31879
+ * A dropped window is work the reader threw away, and a schedule that
31880
+ * silently shows fewer rows than the camera holds is how an operator
31881
+ * saves back a schedule shorter than the one they were looking at
31882
+ * (D391). Non-zero means the window list is INCOMPLETE and a write
31883
+ * that replaces it would delete what was not shown — which is why a
31884
+ * provider reporting a non-zero count also reports the schedule as not
31885
+ * writable.
31886
+ */
31887
+ unreadableWindows: number(),
31888
+ /**
31889
+ * The camera is scheduled to record and has NO usable storage.
31890
+ *
31891
+ * A first-class fact because it is the fleet's most common silent
31892
+ * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
31893
+ * to a card that is not there. Neither the schedule nor the storage
31894
+ * read says anything wrong on its own; only the pair does.
31895
+ */
31896
+ recordingToNowhere: boolean(),
31897
+ lastFetchedAt: number()
31898
+ });
31899
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
31900
+ var RangeSchema = object({
31901
+ min: number(),
31902
+ max: number(),
31903
+ step: number()
31904
+ });
31905
+ /**
31906
+ * The values a camera actually takes for a numeric field, when they are a SET
31907
+ * rather than a range.
31908
+ *
31909
+ * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
31910
+ * (I91DN) on 2026-09-22 by writing each value and reading it back:
31911
+ *
31912
+ * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
31913
+ * camera's "no limit" — `-1` and `4294967295` both land on it);
31914
+ * - post-record: `5, 10, 30, 60, 120, 300, 600`.
31915
+ *
31916
+ * Neither is expressible as a step: the first has a sentinel two billion away
31917
+ * from its neighbours, the second doubles and then jumps. A range that tried
31918
+ * would forbid values the camera takes AND permit values it silently replaces
31919
+ * with 5 — wrong in both directions at once.
31920
+ *
31921
+ * `sentinel` names the member that is not a duration, so a surface can render
31922
+ * "no limit" instead of `2147483647` seconds.
31923
+ */
31924
+ var AllowedValuesSchema = object({
31925
+ values: array(number()).min(1),
31926
+ sentinel: object({
31927
+ value: number(),
31928
+ meaning: _enum(["no-limit", "disabled"])
31929
+ }).optional()
31930
+ });
31931
+ /**
31932
+ * Per-field availability on ONE camera.
31933
+ *
31934
+ * The field exists on every camera — this says whether this one can be
31935
+ * read and whether it can be written, and `reason` says why not when
31936
+ * either is false. The UI renders the control DISABLED with the reason
31937
+ * rather than hiding it, so a limitation is legible instead of looking
31938
+ * like a missing feature.
31939
+ */
31940
+ var OnboardFieldSupportSchema = object({
31941
+ readable: boolean(),
31942
+ writable: boolean(),
31943
+ /** Required whenever `readable` or `writable` is false. */
31944
+ reason: string().optional()
31945
+ });
31946
+ /** What this camera's schedule model can express. */
31947
+ var OnboardScheduleSupportSchema = object({
31948
+ support: OnboardFieldSupportSchema,
31949
+ /**
31950
+ * The smallest time step the camera can express, in minutes.
31951
+ *
31952
+ * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
31953
+ * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
31954
+ * window whose edges are not a multiple of this is REFUSED rather than
31955
+ * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
31956
+ * and nothing says so.
31957
+ */
31958
+ granularityMinutes: number(),
31959
+ /** Triggers this camera can record on. A window naming another is refused. */
31960
+ triggers: array(RecordTriggerSchema),
31961
+ /**
31962
+ * False when the camera stores ONE trigger per time range, so two
31963
+ * windows overlapping on the same day cannot carry different triggers.
31964
+ * True on Reolink, whose mask is per-trigger and independent.
31965
+ */
31966
+ supportsOverlappingTriggers: boolean()
31967
+ });
31968
+ var RecordingOnboardOptionsSchema = object({
31969
+ enabled: OnboardFieldSupportSchema,
31970
+ overwriteWhenFull: OnboardFieldSupportSchema,
31971
+ preRecordSec: OnboardFieldSupportSchema,
31972
+ preRecordSecRange: RangeSchema.optional(),
31973
+ /** Preferred over the range when the camera takes a SET, not a span. */
31974
+ preRecordSecAllowed: AllowedValuesSchema.optional(),
31975
+ postRecordSec: OnboardFieldSupportSchema,
31976
+ postRecordSecRange: RangeSchema.optional(),
31977
+ /** Preferred over the range when the camera takes a SET, not a span. */
31978
+ postRecordSecAllowed: AllowedValuesSchema.optional(),
31979
+ segmentMinutes: OnboardFieldSupportSchema,
31980
+ segmentMinutesRange: RangeSchema.optional(),
31981
+ /** Preferred over the range when the camera takes a SET, not a span. */
31982
+ segmentMinutesAllowed: AllowedValuesSchema.optional(),
31983
+ schedule: OnboardScheduleSupportSchema
31984
+ });
31985
+ /**
31986
+ * A partial change. Every field optional.
31987
+ *
31988
+ * Unlike the other `deviceConfig` caps, a provider here does **NOT**
31989
+ * silently ignore a field it cannot support — it refuses, by name,
31990
+ * through {@link describeOnboardRefusal}. Silence on a recording setting
31991
+ * is the failure D62 exists to prevent: the operator believes the camera
31992
+ * is recording the way the form says, and it is not.
31993
+ */
31994
+ var RecordingOnboardPatchSchema = object({
31995
+ enabled: boolean().optional(),
31996
+ overwriteWhenFull: boolean().optional(),
31997
+ preRecordSec: number().optional(),
31998
+ postRecordSec: number().optional(),
31999
+ segmentMinutes: number().optional(),
32000
+ /** The complete new window set for the primary track — not a delta. */
32001
+ windows: array(RecordWindowSchema).optional()
32002
+ });
32003
+ DeviceType.Camera, method(object({ deviceId: number() }), RecordingOnboardOptionsSchema), method(object({
32004
+ deviceId: number(),
32005
+ settings: RecordingOnboardPatchSchema
32006
+ }), _void(), {
32007
+ kind: "mutation",
32008
+ auth: "admin"
32009
+ });
32010
+ /**
31695
32011
  * A camera's own "record me NOW" LEVEL — a signal the device raises while
31696
32012
  * something it knows about is happening (a robot vacuum cleaning, a machine
31697
32013
  * running, a gate open) and lowers when it stops.
@@ -36985,24 +37301,6 @@ Object.freeze({
36985
37301
  addonId: null,
36986
37302
  access: "view"
36987
37303
  },
36988
- "events.getEventClipUrl": {
36989
- capName: "events",
36990
- capScope: "device",
36991
- addonId: null,
36992
- access: "view"
36993
- },
36994
- "events.getEvents": {
36995
- capName: "events",
36996
- capScope: "device",
36997
- addonId: null,
36998
- access: "view"
36999
- },
37000
- "events.getEventThumbnail": {
37001
- capName: "events",
37002
- capScope: "device",
37003
- addonId: null,
37004
- access: "view"
37005
- },
37006
37304
  "faceGallery.assignFace": {
37007
37305
  capName: "face-gallery",
37008
37306
  capScope: "system",
@@ -39877,224 +40175,236 @@ Object.freeze({
39877
40175
  addonId: null,
39878
40176
  access: "create"
39879
40177
  },
39880
- "recording.applyDeviceSettingsPatch": {
40178
+ "recording.getAvailability": {
39881
40179
  capName: "recording",
39882
- capScope: "system",
40180
+ capScope: "device",
39883
40181
  addonId: null,
39884
- access: "create"
40182
+ access: "view"
39885
40183
  },
39886
- "recording.cancelRelocateJob": {
40184
+ "recording.getDaysWithRecordings": {
39887
40185
  capName: "recording",
39888
- capScope: "system",
40186
+ capScope: "device",
39889
40187
  addonId: null,
39890
- access: "create"
40188
+ access: "view"
39891
40189
  },
39892
- "recording.cancelStorageMigrationMove": {
40190
+ "recording.getPlayback": {
39893
40191
  capName: "recording",
39894
- capScope: "system",
40192
+ capScope: "device",
39895
40193
  addonId: null,
39896
- access: "create"
40194
+ access: "view"
39897
40195
  },
39898
- "recording.deleteFootprint": {
40196
+ "recording.getPlaybackOptions": {
39899
40197
  capName: "recording",
39900
- capScope: "system",
40198
+ capScope: "device",
39901
40199
  addonId: null,
39902
- access: "delete"
40200
+ access: "view"
39903
40201
  },
39904
- "recording.getAvailability": {
40202
+ "recording.listSources": {
39905
40203
  capName: "recording",
39906
- capScope: "system",
40204
+ capScope: "device",
39907
40205
  addonId: null,
39908
40206
  access: "view"
39909
40207
  },
39910
- "recording.getAvailabilityBatch": {
39911
- capName: "recording",
40208
+ "recordingArchive.applyDeviceSettingsPatch": {
40209
+ capName: "recording-archive",
39912
40210
  capScope: "system",
39913
40211
  addonId: null,
39914
- access: "view"
40212
+ access: "create"
39915
40213
  },
39916
- "recording.getDaysWithRecordings": {
39917
- capName: "recording",
40214
+ "recordingArchive.cancelRelocateJob": {
40215
+ capName: "recording-archive",
39918
40216
  capScope: "system",
39919
40217
  addonId: null,
39920
- access: "view"
40218
+ access: "create"
39921
40219
  },
39922
- "recording.getDaysWithRecordingsBatch": {
39923
- capName: "recording",
40220
+ "recordingArchive.cancelStorageMigrationMove": {
40221
+ capName: "recording-archive",
40222
+ capScope: "system",
40223
+ addonId: null,
40224
+ access: "create"
40225
+ },
40226
+ "recordingArchive.deleteFootprint": {
40227
+ capName: "recording-archive",
40228
+ capScope: "system",
40229
+ addonId: null,
40230
+ access: "delete"
40231
+ },
40232
+ "recordingArchive.getAvailabilityBatch": {
40233
+ capName: "recording-archive",
39924
40234
  capScope: "system",
39925
40235
  addonId: null,
39926
40236
  access: "view"
39927
40237
  },
39928
- "recording.getDeviceConfig": {
39929
- capName: "recording",
40238
+ "recordingArchive.getDaysWithRecordingsBatch": {
40239
+ capName: "recording-archive",
39930
40240
  capScope: "system",
39931
40241
  addonId: null,
39932
40242
  access: "view"
39933
40243
  },
39934
- "recording.getDeviceLiveContribution": {
39935
- capName: "recording",
40244
+ "recordingArchive.getDeviceConfig": {
40245
+ capName: "recording-archive",
39936
40246
  capScope: "system",
39937
40247
  addonId: null,
39938
40248
  access: "view"
39939
40249
  },
39940
- "recording.getDeviceSettingsContribution": {
39941
- capName: "recording",
40250
+ "recordingArchive.getDeviceLiveContribution": {
40251
+ capName: "recording-archive",
39942
40252
  capScope: "system",
39943
40253
  addonId: null,
39944
40254
  access: "view"
39945
40255
  },
39946
- "recording.getPlacement": {
39947
- capName: "recording",
40256
+ "recordingArchive.getDeviceSettingsContribution": {
40257
+ capName: "recording-archive",
39948
40258
  capScope: "system",
39949
40259
  addonId: null,
39950
40260
  access: "view"
39951
40261
  },
39952
- "recording.getPlaybackManifest": {
39953
- capName: "recording",
40262
+ "recordingArchive.getPlacement": {
40263
+ capName: "recording-archive",
39954
40264
  capScope: "system",
39955
40265
  addonId: null,
39956
40266
  access: "view"
39957
40267
  },
39958
- "recording.getRelocateResidue": {
39959
- capName: "recording",
40268
+ "recordingArchive.getRelocateResidue": {
40269
+ capName: "recording-archive",
39960
40270
  capScope: "system",
39961
40271
  addonId: null,
39962
40272
  access: "view"
39963
40273
  },
39964
- "recording.getStatus": {
39965
- capName: "recording",
40274
+ "recordingArchive.getStatus": {
40275
+ capName: "recording-archive",
39966
40276
  capScope: "system",
39967
40277
  addonId: null,
39968
40278
  access: "view"
39969
40279
  },
39970
- "recording.getStorageMigrationMoveStatus": {
39971
- capName: "recording",
40280
+ "recordingArchive.getStorageMigrationMoveStatus": {
40281
+ capName: "recording-archive",
39972
40282
  capScope: "system",
39973
40283
  addonId: null,
39974
40284
  access: "view"
39975
40285
  },
39976
- "recording.getStorageUsage": {
39977
- capName: "recording",
40286
+ "recordingArchive.getStorageUsage": {
40287
+ capName: "recording-archive",
39978
40288
  capScope: "system",
39979
40289
  addonId: null,
39980
40290
  access: "view"
39981
40291
  },
39982
- "recording.listOpsLog": {
39983
- capName: "recording",
40292
+ "recordingArchive.listOpsLog": {
40293
+ capName: "recording-archive",
39984
40294
  capScope: "system",
39985
40295
  addonId: null,
39986
40296
  access: "view"
39987
40297
  },
39988
- "recording.listRelocateJobs": {
39989
- capName: "recording",
40298
+ "recordingArchive.listRelocateJobs": {
40299
+ capName: "recording-archive",
39990
40300
  capScope: "system",
39991
40301
  addonId: null,
39992
40302
  access: "view"
39993
40303
  },
39994
- "recording.locateSegment": {
39995
- capName: "recording",
40304
+ "recordingArchive.locateSegment": {
40305
+ capName: "recording-archive",
39996
40306
  capScope: "system",
39997
40307
  addonId: null,
39998
40308
  access: "view"
39999
40309
  },
40000
- "recording.pauseForStorageMigration": {
40001
- capName: "recording",
40310
+ "recordingArchive.pauseForStorageMigration": {
40311
+ capName: "recording-archive",
40002
40312
  capScope: "system",
40003
40313
  addonId: null,
40004
40314
  access: "create"
40005
40315
  },
40006
- "recording.planStorageRebalance": {
40007
- capName: "recording",
40316
+ "recordingArchive.planStorageRebalance": {
40317
+ capName: "recording-archive",
40008
40318
  capScope: "system",
40009
40319
  addonId: null,
40010
40320
  access: "view"
40011
40321
  },
40012
- "recording.pruneFootage": {
40013
- capName: "recording",
40322
+ "recordingArchive.pruneFootage": {
40323
+ capName: "recording-archive",
40014
40324
  capScope: "system",
40015
40325
  addonId: null,
40016
40326
  access: "create"
40017
40327
  },
40018
- "recording.readGopBytes": {
40019
- capName: "recording",
40328
+ "recordingArchive.readGopBytes": {
40329
+ capName: "recording-archive",
40020
40330
  capScope: "system",
40021
40331
  addonId: null,
40022
40332
  access: "view"
40023
40333
  },
40024
- "recording.readSegmentBytes": {
40025
- capName: "recording",
40334
+ "recordingArchive.readSegmentBytes": {
40335
+ capName: "recording-archive",
40026
40336
  capScope: "system",
40027
40337
  addonId: null,
40028
40338
  access: "view"
40029
40339
  },
40030
- "recording.readWindowBytes": {
40031
- capName: "recording",
40340
+ "recordingArchive.readWindowBytes": {
40341
+ capName: "recording-archive",
40032
40342
  capScope: "system",
40033
40343
  addonId: null,
40034
40344
  access: "view"
40035
40345
  },
40036
- "recording.reconcileLedgerAgainstDisk": {
40037
- capName: "recording",
40346
+ "recordingArchive.reconcileLedgerAgainstDisk": {
40347
+ capName: "recording-archive",
40038
40348
  capScope: "system",
40039
40349
  addonId: null,
40040
40350
  access: "create"
40041
40351
  },
40042
- "recording.refreshStorageLocationsForMigration": {
40043
- capName: "recording",
40352
+ "recordingArchive.refreshStorageLocationsForMigration": {
40353
+ capName: "recording-archive",
40044
40354
  capScope: "system",
40045
40355
  addonId: null,
40046
40356
  access: "create"
40047
40357
  },
40048
- "recording.relocateFootage": {
40049
- capName: "recording",
40358
+ "recordingArchive.relocateFootage": {
40359
+ capName: "recording-archive",
40050
40360
  capScope: "system",
40051
40361
  addonId: null,
40052
40362
  access: "create"
40053
40363
  },
40054
- "recording.renderClip": {
40055
- capName: "recording",
40364
+ "recordingArchive.renderClip": {
40365
+ capName: "recording-archive",
40056
40366
  capScope: "system",
40057
40367
  addonId: null,
40058
40368
  access: "create"
40059
40369
  },
40060
- "recording.renderGif": {
40061
- capName: "recording",
40370
+ "recordingArchive.renderGif": {
40371
+ capName: "recording-archive",
40062
40372
  capScope: "system",
40063
40373
  addonId: null,
40064
40374
  access: "create"
40065
40375
  },
40066
- "recording.rescanStorage": {
40067
- capName: "recording",
40376
+ "recordingArchive.rescanStorage": {
40377
+ capName: "recording-archive",
40068
40378
  capScope: "system",
40069
40379
  addonId: null,
40070
40380
  access: "create"
40071
40381
  },
40072
- "recording.resumeForStorageMigration": {
40073
- capName: "recording",
40382
+ "recordingArchive.resumeForStorageMigration": {
40383
+ capName: "recording-archive",
40074
40384
  capScope: "system",
40075
40385
  addonId: null,
40076
40386
  access: "create"
40077
40387
  },
40078
- "recording.setDeviceConfig": {
40079
- capName: "recording",
40388
+ "recordingArchive.setDeviceConfig": {
40389
+ capName: "recording-archive",
40080
40390
  capScope: "system",
40081
40391
  addonId: null,
40082
40392
  access: "create"
40083
40393
  },
40084
- "recording.setDevicePlacement": {
40085
- capName: "recording",
40394
+ "recordingArchive.setDevicePlacement": {
40395
+ capName: "recording-archive",
40086
40396
  capScope: "system",
40087
40397
  addonId: null,
40088
40398
  access: "create"
40089
40399
  },
40090
- "recording.startStorageMigrationMove": {
40091
- capName: "recording",
40400
+ "recordingArchive.startStorageMigrationMove": {
40401
+ capName: "recording-archive",
40092
40402
  capScope: "system",
40093
40403
  addonId: null,
40094
40404
  access: "create"
40095
40405
  },
40096
- "recording.startStorageRebalance": {
40097
- capName: "recording",
40406
+ "recordingArchive.startStorageRebalance": {
40407
+ capName: "recording-archive",
40098
40408
  capScope: "system",
40099
40409
  addonId: null,
40100
40410
  access: "create"
@@ -41623,6 +41933,12 @@ Object.freeze({
41623
41933
  addonId: null,
41624
41934
  access: "view"
41625
41935
  },
41936
+ "videoclips.getPlaybackOptions": {
41937
+ capName: "videoclips",
41938
+ capScope: "device",
41939
+ addonId: null,
41940
+ access: "view"
41941
+ },
41626
41942
  "videoclips.listClips": {
41627
41943
  capName: "videoclips",
41628
41944
  capScope: "device",
@@ -41635,6 +41951,12 @@ Object.freeze({
41635
41951
  addonId: null,
41636
41952
  access: "view"
41637
41953
  },
41954
+ "videoclips.offerClipBytes": {
41955
+ capName: "videoclips",
41956
+ capScope: "device",
41957
+ addonId: null,
41958
+ access: "view"
41959
+ },
41638
41960
  "videoclips.readClipBytes": {
41639
41961
  capName: "videoclips",
41640
41962
  capScope: "device",
@@ -42387,21 +42709,6 @@ Object.freeze({
42387
42709
  form: "single",
42388
42710
  optional: false
42389
42711
  }],
42390
- "events.getEventClipUrl": [{
42391
- name: "deviceId",
42392
- form: "single",
42393
- optional: false
42394
- }],
42395
- "events.getEvents": [{
42396
- name: "deviceId",
42397
- form: "single",
42398
- optional: false
42399
- }],
42400
- "events.getEventThumbnail": [{
42401
- name: "deviceId",
42402
- form: "single",
42403
- optional: false
42404
- }],
42405
42712
  "faceGallery.getFaceByTrack": [{
42406
42713
  name: "deviceId",
42407
42714
  form: "single",
@@ -43320,107 +43627,117 @@ Object.freeze({
43320
43627
  form: "single",
43321
43628
  optional: false
43322
43629
  }],
43323
- "recording.deleteFootprint": [{
43630
+ "recording.getAvailability": [{
43324
43631
  name: "deviceId",
43325
43632
  form: "single",
43326
43633
  optional: false
43327
43634
  }],
43328
- "recording.getAvailability": [{
43635
+ "recording.getDaysWithRecordings": [{
43329
43636
  name: "deviceId",
43330
43637
  form: "single",
43331
43638
  optional: false
43332
43639
  }],
43333
- "recording.getAvailabilityBatch": [{
43334
- name: "deviceIds",
43335
- form: "array",
43640
+ "recording.getPlayback": [{
43641
+ name: "deviceId",
43642
+ form: "single",
43336
43643
  optional: false
43337
43644
  }],
43338
- "recording.getDaysWithRecordings": [{
43645
+ "recording.getPlaybackOptions": [{
43339
43646
  name: "deviceId",
43340
43647
  form: "single",
43341
43648
  optional: false
43342
43649
  }],
43343
- "recording.getDaysWithRecordingsBatch": [{
43344
- name: "deviceIds",
43345
- form: "array",
43650
+ "recording.listSources": [{
43651
+ name: "deviceId",
43652
+ form: "single",
43346
43653
  optional: false
43347
43654
  }],
43348
- "recording.getDeviceConfig": [{
43655
+ "recordingArchive.deleteFootprint": [{
43349
43656
  name: "deviceId",
43350
43657
  form: "single",
43351
43658
  optional: false
43352
43659
  }],
43353
- "recording.getPlaybackManifest": [{
43660
+ "recordingArchive.getAvailabilityBatch": [{
43661
+ name: "deviceIds",
43662
+ form: "array",
43663
+ optional: false
43664
+ }],
43665
+ "recordingArchive.getDaysWithRecordingsBatch": [{
43666
+ name: "deviceIds",
43667
+ form: "array",
43668
+ optional: false
43669
+ }],
43670
+ "recordingArchive.getDeviceConfig": [{
43354
43671
  name: "deviceId",
43355
43672
  form: "single",
43356
43673
  optional: false
43357
43674
  }],
43358
- "recording.listOpsLog": [{
43675
+ "recordingArchive.listOpsLog": [{
43359
43676
  name: "deviceId",
43360
43677
  form: "single",
43361
43678
  optional: true
43362
43679
  }],
43363
- "recording.locateSegment": [{
43680
+ "recordingArchive.locateSegment": [{
43364
43681
  name: "deviceId",
43365
43682
  form: "single",
43366
43683
  optional: false
43367
43684
  }],
43368
- "recording.pruneFootage": [{
43685
+ "recordingArchive.pruneFootage": [{
43369
43686
  name: "deviceId",
43370
43687
  form: "single",
43371
43688
  optional: false
43372
43689
  }],
43373
- "recording.readGopBytes": [{
43690
+ "recordingArchive.readGopBytes": [{
43374
43691
  name: "deviceId",
43375
43692
  form: "single",
43376
43693
  optional: false
43377
43694
  }],
43378
- "recording.readSegmentBytes": [{
43695
+ "recordingArchive.readSegmentBytes": [{
43379
43696
  name: "deviceId",
43380
43697
  form: "single",
43381
43698
  optional: false
43382
43699
  }],
43383
- "recording.readWindowBytes": [{
43700
+ "recordingArchive.readWindowBytes": [{
43384
43701
  name: "deviceId",
43385
43702
  form: "single",
43386
43703
  optional: false
43387
43704
  }],
43388
- "recording.reconcileLedgerAgainstDisk": [{
43705
+ "recordingArchive.reconcileLedgerAgainstDisk": [{
43389
43706
  name: "deviceId",
43390
43707
  form: "single",
43391
43708
  optional: true
43392
43709
  }],
43393
- "recording.relocateFootage": [{
43710
+ "recordingArchive.relocateFootage": [{
43394
43711
  name: "deviceId",
43395
43712
  form: "single",
43396
43713
  optional: true
43397
43714
  }],
43398
- "recording.renderClip": [{
43715
+ "recordingArchive.renderClip": [{
43399
43716
  name: "deviceId",
43400
43717
  form: "single",
43401
43718
  optional: false
43402
43719
  }],
43403
- "recording.renderGif": [{
43720
+ "recordingArchive.renderGif": [{
43404
43721
  name: "deviceId",
43405
43722
  form: "single",
43406
43723
  optional: false
43407
43724
  }],
43408
- "recording.rescanStorage": [{
43725
+ "recordingArchive.rescanStorage": [{
43409
43726
  name: "deviceId",
43410
43727
  form: "single",
43411
43728
  optional: false
43412
43729
  }],
43413
- "recording.setDeviceConfig": [{
43730
+ "recordingArchive.setDeviceConfig": [{
43414
43731
  name: "deviceId",
43415
43732
  form: "single",
43416
43733
  optional: false
43417
43734
  }],
43418
- "recording.setDevicePlacement": [{
43735
+ "recordingArchive.setDevicePlacement": [{
43419
43736
  name: "deviceId",
43420
43737
  form: "single",
43421
43738
  optional: false
43422
43739
  }],
43423
- "recording.startStorageMigrationMove": [{
43740
+ "recordingArchive.startStorageMigrationMove": [{
43424
43741
  name: "deviceId",
43425
43742
  form: "single",
43426
43743
  optional: true
@@ -43681,6 +43998,11 @@ Object.freeze({
43681
43998
  form: "single",
43682
43999
  optional: false
43683
44000
  }],
44001
+ "videoclips.getPlaybackOptions": [{
44002
+ name: "deviceId",
44003
+ form: "single",
44004
+ optional: false
44005
+ }],
43684
44006
  "videoclips.listClips": [{
43685
44007
  name: "deviceId",
43686
44008
  form: "single",
@@ -43691,6 +44013,11 @@ Object.freeze({
43691
44013
  form: "single",
43692
44014
  optional: false
43693
44015
  }],
44016
+ "videoclips.offerClipBytes": [{
44017
+ name: "deviceId",
44018
+ form: "single",
44019
+ optional: false
44020
+ }],
43694
44021
  "videoclips.readClipBytes": [{
43695
44022
  name: "deviceId",
43696
44023
  form: "single",