@camstack/addon-provider-reolink 1.2.163 → 1.2.164

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-Cek0wNdY.js");
3
- const require_chunk_OSVYEFMM = require("./chunk-OSVYEFMM-Czx77gto.js");
3
+ const require_chunk_XAKOGDFB = require("./chunk-XAKOGDFB-CEh96Ccz.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let events = require("events");
6
6
  let fs = require("fs");
@@ -5391,7 +5391,7 @@ var ZodIssueCode = {
5391
5391
  var ZodFirstPartyTypeKind;
5392
5392
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5393
5393
  //#endregion
5394
- //#region ../types/dist/sleep-COWaSCAi.mjs
5394
+ //#region ../types/dist/sleep-PEo0-Fz9.mjs
5395
5395
  /**
5396
5396
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5397
5397
  * window to float samples (D455).
@@ -6527,6 +6527,24 @@ function normalizeAddonInitResult(result) {
6527
6527
  if (Array.isArray(result)) return { providers: result };
6528
6528
  return result;
6529
6529
  }
6530
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
6531
+ var PeerBytesTicketSchema = object({
6532
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
6533
+ url: string().min(1),
6534
+ /**
6535
+ * The HOST node this URL means something on — the hub or a named agent,
6536
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
6537
+ * refuses `cross-node` by name when they differ, without dialling.
6538
+ */
6539
+ hostNodeId: string().min(1),
6540
+ expiresAtMs: number().int().nonnegative(),
6541
+ /**
6542
+ * What the producer DECLARED the body to be, when it knows — `null` when it
6543
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
6544
+ * must be able to tell "the producer did not say" from "the body is empty".
6545
+ */
6546
+ declaredBytes: number().int().nonnegative().nullable()
6547
+ });
6530
6548
  /** Shared Zod schemas used across streaming capabilities. */
6531
6549
  var CamProfileSchema = _enum([
6532
6550
  "high",
@@ -7607,24 +7625,6 @@ object({
7607
7625
  unreachable: number()
7608
7626
  })
7609
7627
  });
7610
- /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
7611
- var PeerBytesTicketSchema = object({
7612
- /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
7613
- url: string().min(1),
7614
- /**
7615
- * The HOST node this URL means something on — the hub or a named agent,
7616
- * never a runner. {@link AddonPeerBytes.open} compares it to its own and
7617
- * refuses `cross-node` by name when they differ, without dialling.
7618
- */
7619
- hostNodeId: string().min(1),
7620
- expiresAtMs: number().int().nonnegative(),
7621
- /**
7622
- * What the producer DECLARED the body to be, when it knows — `null` when it
7623
- * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
7624
- * must be able to tell "the producer did not say" from "the body is empty".
7625
- */
7626
- declaredBytes: number().int().nonnegative().nullable()
7627
- });
7628
7628
  /**
7629
7629
  * Adoption job — the background form of `device-adoption.adopt`.
7630
7630
  *
@@ -7736,7 +7736,7 @@ var AdoptionJobSchema = object({
7736
7736
  * component's original options — detection to the detection-pipeline wrapper
7737
7737
  * binding, audio analysis to its own, recording to `RecordingConfig.enabled`
7738
7738
  * (which was always first-class; the switch was a veneer over
7739
- * `recording.setDeviceConfig`), notifications to a notification-center
7739
+ * `recordingArchive.setDeviceConfig`), notifications to a notification-center
7740
7740
  * per-device setting, the two camera planes to their own components.
7741
7741
  *
7742
7742
  * What survives is {@link composeSwitchedOff}: `CameraStatus.switchedOff`, the
@@ -7762,7 +7762,7 @@ var AdoptionJobSchema = object({
7762
7762
  * | `stream-broker` | `deviceManager.setDisabled` | `StreamBrokerManager.reconcileAllCatalogs` releases the brokers; `ensureBroker` refuses re-creation |
7763
7763
  * | `object-detection` | `deviceManager.setWrapperActive('detection-pipeline')` | `PipelineSettingsStore.resolvePipelineForDevice` returns `{ steps: [], audio: null }` |
7764
7764
  * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
7765
- * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
7765
+ * | `recording` | `recordingArchive.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
7766
7766
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
7767
7767
  * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
7768
7768
  * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
@@ -12192,87 +12192,6 @@ method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, Broke
12192
12192
  auth: "admin"
12193
12193
  }), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
12194
12194
  DeviceType.Camera;
12195
- /**
12196
- * The signals a device can emit to WAKE its own stream.
12197
- *
12198
- * A camera whose stream is built on demand sleeps until something asks for it,
12199
- * and "something" cannot be a consumer that is merely attached — a Frigate-style
12200
- * puller holds a session open for ever, and treating that as demand would keep
12201
- * a battery camera awake for ever, which is the whole thing the battery is for
12202
- * (D173). So the wake has to come from the CAMERA: an event it noticed by
12203
- * itself, with no stream running.
12204
- *
12205
- * ## The vocabulary is the PROVIDER'S, not ours
12206
- *
12207
- * Like `consumables`, this cap declares no vocabulary of its own. A provider
12208
- * names each signal with a `code` it chooses and a `label` an operator reads.
12209
- * Reolink offers motion and camera-native detection; another provider may offer
12210
- * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
12211
- * yet. A fixed enum here would mean every new signal is a framework release.
12212
- *
12213
- * It is deliberately NOT derived from the caps a device already binds. Whether
12214
- * a camera CAN push firmware motion is expressed by `motionSources` containing
12215
- * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
12216
- * binding — but both answer "what drives the detection pipeline", which is a
12217
- * different question from "what may wake a sleeping stream". A camera can do
12218
- * the first and not be trusted with the second, and the operator picks per
12219
- * camera. Two questions, two authorities.
12220
- *
12221
- * ## Availability is not permission
12222
- *
12223
- * `listSignals` says what the device CAN emit. Whether a given signal actually
12224
- * wakes the stream is the operator's per-camera choice, held by the broker
12225
- * alongside the cooldown — see the stream-broker cap's wake settings. A
12226
- * provider declaring a signal is not a provider enabling it.
12227
- */
12228
- /** One signal a device can emit. */
12229
- var StreamSignalSchema = object({
12230
- /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
12231
- code: string().min(1),
12232
- /** What an operator reads in the picker. The provider's own wording. */
12233
- label: string().min(1),
12234
- /**
12235
- * Whether the provider recommends this signal ON when a camera is first set
12236
- * up. A provider knows which of its signals are cheap and reliable; an
12237
- * operator should not have to discover that by trial. Reolink recommends
12238
- * both of its own.
12239
- */
12240
- recommended: boolean()
12241
- });
12242
- var StreamSignalsStatusSchema = object({
12243
- signals: array(StreamSignalSchema),
12244
- lastFetchedAt: number()
12245
- });
12246
- var streamSignalsCapability = {
12247
- name: "stream-signals",
12248
- scope: "device",
12249
- deviceNative: true,
12250
- mode: "singleton",
12251
- deviceTypes: Object.values(DeviceType),
12252
- runtimeState: StreamSignalsStatusSchema,
12253
- /**
12254
- * Runtime-state durability: **session** — mirrored in RAM, never written.
12255
- *
12256
- * The slice holds what the DEVICE says it can emit. That is a probed fact,
12257
- * not an operator choice: the provider re-declares it on every registration,
12258
- * so losing it loses nothing and persisting it would freeze an answer the
12259
- * camera is entitled to change. Measured the same day on the sibling case —
12260
- * `native-object-detection.supportedClasses` was persisted, and a firmware
12261
- * class the camera really detected stayed missing for the life of the row
12262
- * because the fix could not reach it.
12263
- *
12264
- * See `RuntimeStateDurability`. Enforced by
12265
- * `scripts/check-runtime-state-durability.ts`.
12266
- */
12267
- durability: "session",
12268
- methods: {
12269
- /**
12270
- * What this device can emit. Empty is a valid and common answer — most
12271
- * cameras have nothing to offer here, and an empty list is what makes the
12272
- * broker's picker show nothing rather than a false choice.
12273
- */
12274
- listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
12275
- };
12276
12195
  /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
12277
12196
  var StreamFormatSchema = _enum([
12278
12197
  "webrtc",
@@ -13738,6 +13657,210 @@ method(object({ codec: string() }), boolean()), method(_void(), object({
13738
13657
  });
13739
13658
  DeviceType.Camera;
13740
13659
  /**
13660
+ * device-admin-link — "this device has a management page of its own, and here
13661
+ * is its address".
13662
+ *
13663
+ * ## Why this is not a `deviceConfig` cap
13664
+ *
13665
+ * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
13666
+ * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
13667
+ * patch back through a setter; it costs a `builderId` reducer in
13668
+ * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
13669
+ * renders a form section. This cap answers ONE question with ONE read and
13670
+ * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
13671
+ * block, no `settings`, no `runtimeState` and no reducer — exactly like
13672
+ * `reboot`, the other pure-RPC device-native cap.
13673
+ *
13674
+ * ## Absent, and the difference between "no page" and "we cannot say"
13675
+ *
13676
+ * The two are answered at DIFFERENT layers, on purpose:
13677
+ *
13678
+ * - **"We cannot say"** → the provider never registers the cap for that
13679
+ * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
13680
+ * fan are reached only through a vendor cloud; there is no address to hand
13681
+ * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
13682
+ * conditioner DO have a LAN IP, and still have no HTTP management page
13683
+ * behind it. None of them register, so `deviceManager.getBindings` never
13684
+ * lists the cap and no surface asks.
13685
+ * - **"This device has no page, and I know that"** → the provider registers
13686
+ * and `getAdminLink` returns `null`. This is the answer for a device whose
13687
+ * sibling DOES have a page: a Reolink battery camera reached over UDP by
13688
+ * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
13689
+ * transport, a Home Assistant broker authenticated by supervisor token
13690
+ * (which carries no `baseUrl` at all).
13691
+ *
13692
+ * Both draw NOTHING. A button that opens a browser error is worse than no
13693
+ * button, and D62 is the same rule from the other side: an off switch is
13694
+ * reported off, never made to look broken. There is no third state where the
13695
+ * UI renders a disabled button "because the device might have a page".
13696
+ *
13697
+ * ## The URL never carries credentials
13698
+ *
13699
+ * Not in userinfo, not in a query string. Every provider builds through
13700
+ * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
13701
+ * scheme and path as separate arguments — there is no parameter a secret could
13702
+ * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
13703
+ * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
13704
+ * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
13705
+ * keeps providers from hand-rolling one anyway.
13706
+ *
13707
+ * This matters here more than anywhere else in the repo, because every provider
13708
+ * that knows a device's host knows its PASSWORD too: `{ host, port, username,
13709
+ * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
13710
+ * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
13711
+ * camera's own page will ask for its own login. That is correct, and pre-
13712
+ * filling it is the operator's business, not ours.
13713
+ *
13714
+ * ## It is a LAN fact
13715
+ *
13716
+ * The URL addresses the device where the NODE can see it. It is not proxied,
13717
+ * not made reachable from outside, and not sent anywhere. A surface renders it
13718
+ * as a link the operator's own browser follows, on the operator's own network,
13719
+ * or renders nothing.
13720
+ */
13721
+ /**
13722
+ * Whose page is it. The distinction is for the OPERATOR, who needs to know
13723
+ * before clicking whether he is about to land on a camera's own web server or
13724
+ * inside Home Assistant.
13725
+ */
13726
+ var AdminLinkTargetEnum = _enum(["device", "integration"]);
13727
+ var DeviceAdminLinkSchema = object({
13728
+ /**
13729
+ * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
13730
+ * free of userinfo and of any credential-shaped query key.
13731
+ */
13732
+ url: string(),
13733
+ /**
13734
+ * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
13735
+ * The PROVIDER names it, because only the provider knows what the page is;
13736
+ * a UI that invented the label from the addon id would call the Home
13737
+ * Assistant device page "Provider Homeassistant".
13738
+ */
13739
+ label: string(),
13740
+ target: AdminLinkTargetEnum,
13741
+ /**
13742
+ * Host the URL points at, without scheme, port or path — for the tooltip, so
13743
+ * an operator can see WHERE the button goes before he follows it. Redundant
13744
+ * with `url` by construction; carried separately so no surface has to parse
13745
+ * a URL to show it.
13746
+ */
13747
+ host: string()
13748
+ });
13749
+ var deviceAdminLinkCapability = {
13750
+ name: "device-admin-link",
13751
+ scope: "device",
13752
+ deviceNative: true,
13753
+ mode: "singleton",
13754
+ methods: {
13755
+ /**
13756
+ * The device's management page, or `null` when this device has none.
13757
+ *
13758
+ * `auth: 'admin'` deliberately. This is administration, not actuation —
13759
+ * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
13760
+ * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
13761
+ * The URL is also a statement about the LAN, which a household member with
13762
+ * a `view` grant on a light has no reason to be handed.
13763
+ *
13764
+ * The surfaces gate on the QUERY, never on a role they guessed: a caller
13765
+ * without the right loses the query and draws nothing, which is the same
13766
+ * thing a device with no page draws. There is no path on which a button
13767
+ * appears and then fails — the D403 failure mode, from the other end.
13768
+ */
13769
+ getAdminLink: method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
13770
+ };
13771
+ /**
13772
+ * Query keys that may never appear on an admin link. A credential smuggled as
13773
+ * `?password=` is the same leak as userinfo, in a form the userinfo check
13774
+ * cannot see: it survives copy-paste, referrer headers and proxy logs
13775
+ * identically.
13776
+ */
13777
+ var CREDENTIAL_QUERY_KEYS = [
13778
+ "password",
13779
+ "passwd",
13780
+ "pwd",
13781
+ "pass",
13782
+ "user",
13783
+ "username",
13784
+ "usr",
13785
+ "login",
13786
+ "token",
13787
+ "access_token",
13788
+ "auth",
13789
+ "authorization",
13790
+ "apikey",
13791
+ "api_key",
13792
+ "secret",
13793
+ "credential",
13794
+ "credentials",
13795
+ "session",
13796
+ "sessionid",
13797
+ "key"
13798
+ ];
13799
+ /**
13800
+ * The same userinfo shape `scripts/check-no-credential-urls-in-fixtures.ts`
13801
+ * scans recorded fixtures for, applied to what we are about to EMIT. Kept
13802
+ * structurally identical on purpose: a URL this function returns is a URL that
13803
+ * guard would pass.
13804
+ */
13805
+ var CREDENTIAL_URL = /[a-z][a-z0-9+.-]*:\/\/[^/\s:@'"]+:[^/\s:@'"]+@/i;
13806
+ /** A host that is safe to place in an authority component verbatim. */
13807
+ var PLAIN_HOST = /^[a-zA-Z0-9._-]+$/;
13808
+ /** A bracketed IPv6 literal, the only other authority form we emit. */
13809
+ var IPV6_HOST = /^\[[0-9a-fA-F:.]+\]$/;
13810
+ function defaultPortFor(scheme) {
13811
+ return scheme === "https" ? 443 : 80;
13812
+ }
13813
+ /**
13814
+ * Build the admin-page URL, or refuse by name.
13815
+ *
13816
+ * The refusal is never thrown: a provider answering "no page for this device"
13817
+ * is an ordinary answer (`getAdminLink` → `null`), and a throw here would turn
13818
+ * a missing host on one device into a failed query for the whole surface.
13819
+ */
13820
+ function buildDeviceAdminUrl(parts) {
13821
+ const host = parts.host.trim();
13822
+ if (host.length === 0) return {
13823
+ ok: false,
13824
+ reason: "empty-host"
13825
+ };
13826
+ if (host.includes("@")) return {
13827
+ ok: false,
13828
+ reason: "host-carries-userinfo"
13829
+ };
13830
+ if (!PLAIN_HOST.test(host) && !IPV6_HOST.test(host)) return {
13831
+ ok: false,
13832
+ reason: "host-not-plain"
13833
+ };
13834
+ if (parts.port !== null) {
13835
+ if (!Number.isInteger(parts.port) || parts.port < 1 || parts.port > 65535) return {
13836
+ ok: false,
13837
+ reason: "port-out-of-range"
13838
+ };
13839
+ }
13840
+ if (!parts.path.startsWith("/")) return {
13841
+ ok: false,
13842
+ reason: "path-not-absolute"
13843
+ };
13844
+ const query = parts.query ?? {};
13845
+ for (const key of Object.keys(query)) if (CREDENTIAL_QUERY_KEYS.includes(key.toLowerCase())) return {
13846
+ ok: false,
13847
+ reason: "credential-query-key"
13848
+ };
13849
+ const authority = parts.port === null || parts.port === defaultPortFor(parts.scheme) ? host : `${host}:${String(parts.port)}`;
13850
+ const search = new URLSearchParams();
13851
+ for (const [key, value] of Object.entries(query)) search.append(key, value);
13852
+ const suffix = search.size > 0 ? `?${search.toString()}` : "";
13853
+ const url = `${parts.scheme}://${authority}${parts.path}${suffix}`;
13854
+ if (CREDENTIAL_URL.test(url)) return {
13855
+ ok: false,
13856
+ reason: "userinfo-in-result"
13857
+ };
13858
+ return {
13859
+ ok: true,
13860
+ url
13861
+ };
13862
+ }
13863
+ /**
13741
13864
  * Identity envelope for a device's upstream-system metadata.
13742
13865
  *
13743
13866
  * Two jobs:
@@ -14172,210 +14295,6 @@ method(object({ integrationId: string() }), object({ filters: array(AdoptionFilt
14172
14295
  auth: "admin"
14173
14296
  });
14174
14297
  /**
14175
- * device-admin-link — "this device has a management page of its own, and here
14176
- * is its address".
14177
- *
14178
- * ## Why this is not a `deviceConfig` cap
14179
- *
14180
- * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
14181
- * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
14182
- * patch back through a setter; it costs a `builderId` reducer in
14183
- * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
14184
- * renders a form section. This cap answers ONE question with ONE read and
14185
- * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
14186
- * block, no `settings`, no `runtimeState` and no reducer — exactly like
14187
- * `reboot`, the other pure-RPC device-native cap.
14188
- *
14189
- * ## Absent, and the difference between "no page" and "we cannot say"
14190
- *
14191
- * The two are answered at DIFFERENT layers, on purpose:
14192
- *
14193
- * - **"We cannot say"** → the provider never registers the cap for that
14194
- * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
14195
- * fan are reached only through a vendor cloud; there is no address to hand
14196
- * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
14197
- * conditioner DO have a LAN IP, and still have no HTTP management page
14198
- * behind it. None of them register, so `deviceManager.getBindings` never
14199
- * lists the cap and no surface asks.
14200
- * - **"This device has no page, and I know that"** → the provider registers
14201
- * and `getAdminLink` returns `null`. This is the answer for a device whose
14202
- * sibling DOES have a page: a Reolink battery camera reached over UDP by
14203
- * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
14204
- * transport, a Home Assistant broker authenticated by supervisor token
14205
- * (which carries no `baseUrl` at all).
14206
- *
14207
- * Both draw NOTHING. A button that opens a browser error is worse than no
14208
- * button, and D62 is the same rule from the other side: an off switch is
14209
- * reported off, never made to look broken. There is no third state where the
14210
- * UI renders a disabled button "because the device might have a page".
14211
- *
14212
- * ## The URL never carries credentials
14213
- *
14214
- * Not in userinfo, not in a query string. Every provider builds through
14215
- * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
14216
- * scheme and path as separate arguments — there is no parameter a secret could
14217
- * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
14218
- * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
14219
- * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
14220
- * keeps providers from hand-rolling one anyway.
14221
- *
14222
- * This matters here more than anywhere else in the repo, because every provider
14223
- * that knows a device's host knows its PASSWORD too: `{ host, port, username,
14224
- * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
14225
- * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
14226
- * camera's own page will ask for its own login. That is correct, and pre-
14227
- * filling it is the operator's business, not ours.
14228
- *
14229
- * ## It is a LAN fact
14230
- *
14231
- * The URL addresses the device where the NODE can see it. It is not proxied,
14232
- * not made reachable from outside, and not sent anywhere. A surface renders it
14233
- * as a link the operator's own browser follows, on the operator's own network,
14234
- * or renders nothing.
14235
- */
14236
- /**
14237
- * Whose page is it. The distinction is for the OPERATOR, who needs to know
14238
- * before clicking whether he is about to land on a camera's own web server or
14239
- * inside Home Assistant.
14240
- */
14241
- var AdminLinkTargetEnum = _enum(["device", "integration"]);
14242
- var DeviceAdminLinkSchema = object({
14243
- /**
14244
- * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
14245
- * free of userinfo and of any credential-shaped query key.
14246
- */
14247
- url: string(),
14248
- /**
14249
- * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
14250
- * The PROVIDER names it, because only the provider knows what the page is;
14251
- * a UI that invented the label from the addon id would call the Home
14252
- * Assistant device page "Provider Homeassistant".
14253
- */
14254
- label: string(),
14255
- target: AdminLinkTargetEnum,
14256
- /**
14257
- * Host the URL points at, without scheme, port or path — for the tooltip, so
14258
- * an operator can see WHERE the button goes before he follows it. Redundant
14259
- * with `url` by construction; carried separately so no surface has to parse
14260
- * a URL to show it.
14261
- */
14262
- host: string()
14263
- });
14264
- var deviceAdminLinkCapability = {
14265
- name: "device-admin-link",
14266
- scope: "device",
14267
- deviceNative: true,
14268
- mode: "singleton",
14269
- methods: {
14270
- /**
14271
- * The device's management page, or `null` when this device has none.
14272
- *
14273
- * `auth: 'admin'` deliberately. This is administration, not actuation —
14274
- * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
14275
- * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
14276
- * The URL is also a statement about the LAN, which a household member with
14277
- * a `view` grant on a light has no reason to be handed.
14278
- *
14279
- * The surfaces gate on the QUERY, never on a role they guessed: a caller
14280
- * without the right loses the query and draws nothing, which is the same
14281
- * thing a device with no page draws. There is no path on which a button
14282
- * appears and then fails — the D403 failure mode, from the other end.
14283
- */
14284
- getAdminLink: method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
14285
- };
14286
- /**
14287
- * Query keys that may never appear on an admin link. A credential smuggled as
14288
- * `?password=` is the same leak as userinfo, in a form the userinfo check
14289
- * cannot see: it survives copy-paste, referrer headers and proxy logs
14290
- * identically.
14291
- */
14292
- var CREDENTIAL_QUERY_KEYS = [
14293
- "password",
14294
- "passwd",
14295
- "pwd",
14296
- "pass",
14297
- "user",
14298
- "username",
14299
- "usr",
14300
- "login",
14301
- "token",
14302
- "access_token",
14303
- "auth",
14304
- "authorization",
14305
- "apikey",
14306
- "api_key",
14307
- "secret",
14308
- "credential",
14309
- "credentials",
14310
- "session",
14311
- "sessionid",
14312
- "key"
14313
- ];
14314
- /**
14315
- * The same userinfo shape `scripts/check-no-credential-urls-in-fixtures.ts`
14316
- * scans recorded fixtures for, applied to what we are about to EMIT. Kept
14317
- * structurally identical on purpose: a URL this function returns is a URL that
14318
- * guard would pass.
14319
- */
14320
- var CREDENTIAL_URL = /[a-z][a-z0-9+.-]*:\/\/[^/\s:@'"]+:[^/\s:@'"]+@/i;
14321
- /** A host that is safe to place in an authority component verbatim. */
14322
- var PLAIN_HOST = /^[a-zA-Z0-9._-]+$/;
14323
- /** A bracketed IPv6 literal, the only other authority form we emit. */
14324
- var IPV6_HOST = /^\[[0-9a-fA-F:.]+\]$/;
14325
- function defaultPortFor(scheme) {
14326
- return scheme === "https" ? 443 : 80;
14327
- }
14328
- /**
14329
- * Build the admin-page URL, or refuse by name.
14330
- *
14331
- * The refusal is never thrown: a provider answering "no page for this device"
14332
- * is an ordinary answer (`getAdminLink` → `null`), and a throw here would turn
14333
- * a missing host on one device into a failed query for the whole surface.
14334
- */
14335
- function buildDeviceAdminUrl(parts) {
14336
- const host = parts.host.trim();
14337
- if (host.length === 0) return {
14338
- ok: false,
14339
- reason: "empty-host"
14340
- };
14341
- if (host.includes("@")) return {
14342
- ok: false,
14343
- reason: "host-carries-userinfo"
14344
- };
14345
- if (!PLAIN_HOST.test(host) && !IPV6_HOST.test(host)) return {
14346
- ok: false,
14347
- reason: "host-not-plain"
14348
- };
14349
- if (parts.port !== null) {
14350
- if (!Number.isInteger(parts.port) || parts.port < 1 || parts.port > 65535) return {
14351
- ok: false,
14352
- reason: "port-out-of-range"
14353
- };
14354
- }
14355
- if (!parts.path.startsWith("/")) return {
14356
- ok: false,
14357
- reason: "path-not-absolute"
14358
- };
14359
- const query = parts.query ?? {};
14360
- for (const key of Object.keys(query)) if (CREDENTIAL_QUERY_KEYS.includes(key.toLowerCase())) return {
14361
- ok: false,
14362
- reason: "credential-query-key"
14363
- };
14364
- const authority = parts.port === null || parts.port === defaultPortFor(parts.scheme) ? host : `${host}:${String(parts.port)}`;
14365
- const search = new URLSearchParams();
14366
- for (const [key, value] of Object.entries(query)) search.append(key, value);
14367
- const suffix = search.size > 0 ? `?${search.toString()}` : "";
14368
- const url = `${parts.scheme}://${authority}${parts.path}${suffix}`;
14369
- if (CREDENTIAL_URL.test(url)) return {
14370
- ok: false,
14371
- reason: "userinfo-in-result"
14372
- };
14373
- return {
14374
- ok: true,
14375
- url
14376
- };
14377
- }
14378
- /**
14379
14298
  * `device-export` — collection cap for addons that export camstack
14380
14299
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
14381
14300
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -24798,6 +24717,87 @@ method(_void(), ProviderInfoSchema, { auth: "admin" }), method(object({ config:
24798
24717
  kind: "mutation",
24799
24718
  auth: "admin"
24800
24719
  });
24720
+ /**
24721
+ * The signals a device can emit to WAKE its own stream.
24722
+ *
24723
+ * A camera whose stream is built on demand sleeps until something asks for it,
24724
+ * and "something" cannot be a consumer that is merely attached — a Frigate-style
24725
+ * puller holds a session open for ever, and treating that as demand would keep
24726
+ * a battery camera awake for ever, which is the whole thing the battery is for
24727
+ * (D173). So the wake has to come from the CAMERA: an event it noticed by
24728
+ * itself, with no stream running.
24729
+ *
24730
+ * ## The vocabulary is the PROVIDER'S, not ours
24731
+ *
24732
+ * Like `consumables`, this cap declares no vocabulary of its own. A provider
24733
+ * names each signal with a `code` it chooses and a `label` an operator reads.
24734
+ * Reolink offers motion and camera-native detection; another provider may offer
24735
+ * a tamper, a doorbell press, a PIR, or something no camera in this fleet has
24736
+ * yet. A fixed enum here would mean every new signal is a framework release.
24737
+ *
24738
+ * It is deliberately NOT derived from the caps a device already binds. Whether
24739
+ * a camera CAN push firmware motion is expressed by `motionSources` containing
24740
+ * `'onboard'`, and whether it does AI on-camera by the `native-object-detection`
24741
+ * binding — but both answer "what drives the detection pipeline", which is a
24742
+ * different question from "what may wake a sleeping stream". A camera can do
24743
+ * the first and not be trusted with the second, and the operator picks per
24744
+ * camera. Two questions, two authorities.
24745
+ *
24746
+ * ## Availability is not permission
24747
+ *
24748
+ * `listSignals` says what the device CAN emit. Whether a given signal actually
24749
+ * wakes the stream is the operator's per-camera choice, held by the broker
24750
+ * alongside the cooldown — see the stream-broker cap's wake settings. A
24751
+ * provider declaring a signal is not a provider enabling it.
24752
+ */
24753
+ /** One signal a device can emit. */
24754
+ var StreamSignalSchema = object({
24755
+ /** Stable id chosen by the provider, e.g. `'motion'`, `'person'`, `'tamper'`. */
24756
+ code: string().min(1),
24757
+ /** What an operator reads in the picker. The provider's own wording. */
24758
+ label: string().min(1),
24759
+ /**
24760
+ * Whether the provider recommends this signal ON when a camera is first set
24761
+ * up. A provider knows which of its signals are cheap and reliable; an
24762
+ * operator should not have to discover that by trial. Reolink recommends
24763
+ * both of its own.
24764
+ */
24765
+ recommended: boolean()
24766
+ });
24767
+ var StreamSignalsStatusSchema = object({
24768
+ signals: array(StreamSignalSchema),
24769
+ lastFetchedAt: number()
24770
+ });
24771
+ var streamSignalsCapability = {
24772
+ name: "stream-signals",
24773
+ scope: "device",
24774
+ deviceNative: true,
24775
+ mode: "singleton",
24776
+ deviceTypes: Object.values(DeviceType),
24777
+ runtimeState: StreamSignalsStatusSchema,
24778
+ /**
24779
+ * Runtime-state durability: **session** — mirrored in RAM, never written.
24780
+ *
24781
+ * The slice holds what the DEVICE says it can emit. That is a probed fact,
24782
+ * not an operator choice: the provider re-declares it on every registration,
24783
+ * so losing it loses nothing and persisting it would freeze an answer the
24784
+ * camera is entitled to change. Measured the same day on the sibling case —
24785
+ * `native-object-detection.supportedClasses` was persisted, and a firmware
24786
+ * class the camera really detected stayed missing for the life of the row
24787
+ * because the fix could not reach it.
24788
+ *
24789
+ * See `RuntimeStateDurability`. Enforced by
24790
+ * `scripts/check-runtime-state-durability.ts`.
24791
+ */
24792
+ durability: "session",
24793
+ methods: {
24794
+ /**
24795
+ * What this device can emit. Empty is a valid and common answer — most
24796
+ * cameras have nothing to offer here, and an empty list is what makes the
24797
+ * broker's picker show nothing rather than a false choice.
24798
+ */
24799
+ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
24800
+ };
24801
24801
  /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
24802
24802
  var ProfileSettingsSchemaBridge = unknown().nullable();
24803
24803
  var ProfileSettingsBagSchema = record(string(), unknown());
@@ -25838,27 +25838,74 @@ var ClipStreamDialSchema = object({
25838
25838
  audioReason: ClipStreamAudioReasonSchema.optional()
25839
25839
  });
25840
25840
  /**
25841
- * The playback rates a clip can be DELIVERED at, ascending, always with `1`.
25841
+ * The playback rates the broker can pace over media it HOLDS, ascending,
25842
+ * always with `1` — and the WIDEST ladder there is. It is the domain the
25843
+ * broker's `clampPlaybackRate` is derived from, at both ends.
25842
25844
  *
25843
25845
  * These are the BROKER's: it re-paces frames it has already demuxed — the
25844
25846
  * `MonotonicClock` divides source elapsed time by the factor and the pacer
25845
- * pushes that much faster — so the domain is the recorded one, {0} ∪ [0.25, 4],
25846
- * and this is the discrete ladder drawn from it. `1.5` is in it because a
25847
- * re-pacer has no reason to refuse it.
25848
- *
25849
- * `8` and `16` are deliberately NOT here. They are the CAMERA's own
25850
- * `<playSpeed>` (Reolink cmd-5 replay), a different mechanism, still unwired
25851
- * (D597, D600) — a rate change there costs a new dial and a new stream, and
25852
- * the camera's 8x would need a resample the clip audio path has no decoder
25853
- * for. Offering them today accepts a rate and delivers 1x, which is the whole
25854
- * defect this list exists to end (D612). When `playSpeed` IS wired its rates
25855
- * join THIS array — a second list elsewhere is the second authority D62
25856
- * forbids.
25857
- *
25858
- * Every entry must survive the broker's `clampPlaybackRate` unchanged: a set
25859
- * that offers what the clamp then moves is the same lie one step later.
25847
+ * pushes that much faster. `1.5` is in it because a re-pacer has no reason to
25848
+ * refuse it.
25849
+ *
25850
+ * **`8` is here since D621, and it is the pacer's, not a camera's.** D620
25851
+ * measured the thing that was assumed for a year and was never true: a
25852
+ * Reolink `<playSpeed>` does not time-compress anything. At 1, 2, 4 and 8 the
25853
+ * transfer is byte-identical — same 947 472 bytes, same 594 access units,
25854
+ * same 39.855 s presentation span, same 624 AAC frames — and only the wall
25855
+ * clock moves. It buys SUPPLY, never speed. So there was never a second
25856
+ * mechanism to wire for 8×: there is one, this one, and what it needs is
25857
+ * media in hand and a clamp that does not move the number.
25858
+ *
25859
+ * **`16` is deliberately NOT here, and will not join by widening this array.**
25860
+ * The camera's `playSpeed 16` is not a rate at all but an I-frame-only MODE —
25861
+ * measured on 592: 10 access units for a whole 36.3 s sub clip, 20 for the
25862
+ * main twin at a 2 025 ms median spacing, and no audio track. That is
25863
+ * different CONTENT, a stream of its own, and the honest broker-side twin of
25864
+ * it would be a pacer that DROPS whole GOPs rather than one that pushes 16×
25865
+ * the bitrate at a live WebRTC track. Neither exists. A `16` in this array
25866
+ * today would be a rate accepted and delivered at 8 — D612's defect, which
25867
+ * this list exists to end, one number further along.
25868
+ *
25869
+ * Every entry must survive the broker's `clampPlaybackRate` unchanged. Since
25870
+ * D621 that is structural rather than a discipline: the clamp reads this
25871
+ * array's own ends. What still has to be proved behaviourally — and is, in
25872
+ * the broker's `clip-stream-feeder.spec` — is that the PACER really empties a
25873
+ * clip `rate` times faster, because a clamp agreeing with a ladder is a
25874
+ * constant compared against itself.
25875
+ *
25876
+ * Narrower ladders exist for transports whose SUPPLY is bounded; they are
25877
+ * subsets of this one ({@link CLIP_STREAM_SOURCE_RATES},
25878
+ * {@link CLIP_REALTIME_SOURCE_RATES}).
25860
25879
  */
25861
25880
  var CLIP_BROKER_PACED_RATES = [
25881
+ .25,
25882
+ .5,
25883
+ 1,
25884
+ 1.5,
25885
+ 2,
25886
+ 4,
25887
+ 8
25888
+ ];
25889
+ /**
25890
+ * The rates a provider-STREAMED clip can be delivered at — a subset of
25891
+ * {@link CLIP_BROKER_PACED_RATES}, and frozen below it on purpose (D621).
25892
+ *
25893
+ * A `stream` clip's media does not exist yet: it arrives on a socket from the
25894
+ * camera while the pacer consumes it. The pacer takes `rate` seconds of media
25895
+ * per second of wall clock, so the ceiling is whatever the SOURCE supplies,
25896
+ * and that is a measurement per vendor rather than a preference.
25897
+ *
25898
+ * Reolink, the one provider on this envelope, was measured on 592
25899
+ * (2026-09-23, D620): with the `<ReplaySeek>` that 0.11.1 sends before every
25900
+ * replay, a sub clip arrives at **1.2×** and a main twin at **1.37×**. The
25901
+ * ladder below is therefore ALREADY beyond this transport above 1 — the D612
25902
+ * defect returned through a door nobody opened — and D620 fixed the repair
25903
+ * order: the supply first, the ladder after. So this array does not move with
25904
+ * the broker's, and it does not gain `8`; what it gains, when the library
25905
+ * forwards a `<playSpeed>` large enough to feed the rate being paced over it,
25906
+ * is the right to be widened on a measurement rather than shortened on one.
25907
+ */
25908
+ var CLIP_STREAM_SOURCE_RATES = [
25862
25909
  .25,
25863
25910
  .5,
25864
25911
  1,
@@ -26343,7 +26390,7 @@ var CLIP_PLAYBACK_OPTIONS = {
26343
26390
  seek: "forward",
26344
26391
  stepBack: false,
26345
26392
  scrub: false,
26346
- rates: CLIP_BROKER_PACED_RATES
26393
+ rates: CLIP_STREAM_SOURCE_RATES
26347
26394
  },
26348
26395
  /**
26349
26396
  * A `stream` whose SOURCE runs at realtime — a Hikvision RTSP replay.
@@ -27727,6 +27774,143 @@ DeviceType.Camera, method(object({ deviceId: number() }), CameraCredentialsSchem
27727
27774
  auth: "admin"
27728
27775
  });
27729
27776
  /**
27777
+ * camera-grid-layout — the geometry of a COMPOSITE camera, on the camera's own
27778
+ * page.
27779
+ *
27780
+ * ## Why this is a capability and not an addon settings schema
27781
+ *
27782
+ * It was one, and it did not render. The addon declared the editor as a
27783
+ * `type: 'widget'` field inside its own `deviceSettingsSchema()`; the hub
27784
+ * returned that section correctly and `ConfigFormField` renders `type:'widget'`
27785
+ * perfectly well — and nothing ever asked camera-grid for it. The Cluster →
27786
+ * Pipeline → Device Overrides page interrogates a HAND-WRITTEN list of four
27787
+ * addons (`PIPELINE_CLUSTER_DEVICE_ADDONS`), whose own comment says an addon
27788
+ * not on it "falls off silently".
27789
+ *
27790
+ * Adding a fifth name to that list would have been the wrong fix twice over:
27791
+ * that page is per-camera DETECTION tuning, and a grid's geometry belongs
27792
+ * beside PTZ and motion zones on the camera itself. The device page is
27793
+ * BINDING-driven (D12), so the way in is a capability bound to the device —
27794
+ * and this cap carries its section the way `recording` does, by RETURNING it
27795
+ * from `getDeviceSettingsContribution`.
27796
+ *
27797
+ * Seven other widgets are still declared the other way, through a
27798
+ * `deviceConfig.ui` block the framework derives a section from. That route
27799
+ * gives the addon no say in where its own panel lands and no way to decline
27800
+ * for a device the panel does not suit, which is why this one does not use it.
27801
+ *
27802
+ * ## Why one addon may implement it
27803
+ *
27804
+ * It is a device-scoped NATIVE cap, registered by the grid camera device
27805
+ * itself. Nothing else declares a composite camera, so nothing else has a
27806
+ * layout — and the device-scoped route means the widget asks THE camera, not
27807
+ * "the camera-grid addon", which is what let the old custom-action pair be
27808
+ * reached only by a caller that already knew the addon id.
27809
+ *
27810
+ * ## The tab
27811
+ *
27812
+ * `streaming`, not a top-tab of its own. A grid's geometry IS what its stream
27813
+ * is, so the Streaming tab is where it belongs; a `grid` top-tab would need an
27814
+ * entry in `WELL_KNOWN_TAB_MAP` or the device page renders the raw id as the
27815
+ * label (measured on the robot camera, 2026-09-06 — a tab called "navigation"
27816
+ * next to "PTZ").
27817
+ */
27818
+ /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
27819
+ var GridNormalizedRectSchema = object({
27820
+ x: number().min(0).max(1),
27821
+ y: number().min(0).max(1),
27822
+ width: number().gt(0).max(1),
27823
+ height: number().gt(0).max(1)
27824
+ });
27825
+ /**
27826
+ * One source camera, the part of its picture taken, and where that part lands.
27827
+ *
27828
+ * Both rectangles are NORMALIZED (D519): a source camera can change resolution
27829
+ * — a profile switch, a firmware update, a substream that comes back different
27830
+ * — and a stored PIXEL rectangle would quietly start cutting the wrong region,
27831
+ * which is the class of bug nobody files.
27832
+ */
27833
+ var GridLayoutCellSchema = object({
27834
+ deviceId: number().int().positive(),
27835
+ /** The part of the SOURCE taken, normalized against the source. */
27836
+ source: GridNormalizedRectSchema,
27837
+ /** Where it lands, normalized against the CANVAS. */
27838
+ cell: GridNormalizedRectSchema
27839
+ });
27840
+ /**
27841
+ * Which profiles this grid can actually compose, and why not.
27842
+ *
27843
+ * A grid's `high` composes its sources' `high` and its `low` their `low`, so a
27844
+ * profile is on offer only when EVERY source can serve it. The refusal NAMES
27845
+ * the sources, because "this grid has no low" is not a finding — "615 has no
27846
+ * low" is, and it is the one an operator can act on.
27847
+ */
27848
+ var GridProfileOfferSchema = object({
27849
+ profile: _enum([
27850
+ "high",
27851
+ "mid",
27852
+ "low"
27853
+ ]),
27854
+ offered: boolean(),
27855
+ /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
27856
+ missingSources: array(number().int().positive()),
27857
+ /**
27858
+ * The canvas this profile composes onto, `WxH`, or empty when it is not
27859
+ * offered. DERIVED from the cells and the sources' own size at this profile —
27860
+ * it is reported because nothing else in the system would ever say what the
27861
+ * grid came out as, and because it is the number an operator would otherwise
27862
+ * expect to type.
27863
+ */
27864
+ canvas: string(),
27865
+ /**
27866
+ * Whether this profile is PUBLISHED, of the ones the grid could serve.
27867
+ *
27868
+ * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
27869
+ * a 4K canvas built from 4K decodes — something to opt into, not something a
27870
+ * viewer's adaptive should be handed by climbing to the top rung it can see.
27871
+ * Default is `mid` + `low`.
27872
+ */
27873
+ published: boolean()
27874
+ });
27875
+ var GridLayoutViewSchema = object({
27876
+ /** The persisted grid row this camera was declared from. */
27877
+ instanceId: string(),
27878
+ deviceId: number().int().nonnegative(),
27879
+ name: string(),
27880
+ /**
27881
+ * NO canvas size. A grid's resolution is not authored: each profile derives
27882
+ * its own from the cells and its sources' dimensions. The two numbers that
27883
+ * used to be here were a text field that silently decided both how much the
27884
+ * composite cost and how sharp it was — see `profiles[].canvas` for what it
27885
+ * came out as.
27886
+ */
27887
+ fps: number().int(),
27888
+ cells: array(GridLayoutCellSchema),
27889
+ /** What the catalog will publish, and what it refuses to. Read-only. */
27890
+ profiles: array(GridProfileOfferSchema)
27891
+ });
27892
+ var GridLayoutPatchSchema = object({
27893
+ deviceId: number().int().nonnegative(),
27894
+ name: string().min(1).max(160).optional(),
27895
+ fps: number().int().min(1).max(60).optional(),
27896
+ /** Which profiles to publish. See `GridProfileOffer.published`. */
27897
+ publishedProfiles: array(_enum([
27898
+ "high",
27899
+ "mid",
27900
+ "low"
27901
+ ])).max(3).optional(),
27902
+ /**
27903
+ * The whole cell list at once. A per-cell patch would need an ordering the
27904
+ * editor does not have, and a half-applied layout is a picture nobody asked
27905
+ * for.
27906
+ */
27907
+ cells: array(GridLayoutCellSchema).max(16)
27908
+ });
27909
+ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), GridLayoutViewSchema.nullable(), { auth: "admin" }), method(GridLayoutPatchSchema, GridLayoutViewSchema, {
27910
+ kind: "mutation",
27911
+ auth: "admin"
27912
+ });
27913
+ /**
27730
27914
  * Carbon-monoxide alarm sensor. Drives Home Assistant `binary_sensor`
27731
27915
  * entries with `device_class: carbon_monoxide`. Push-driven.
27732
27916
  */
@@ -28529,511 +28713,6 @@ var dayNightCapability = {
28529
28713
  volatileStateFields: ["lastFetchedAt"]
28530
28714
  };
28531
28715
  /**
28532
- * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
28533
- * writes to the CAMERA's own card, on the camera's own schedule.
28534
- *
28535
- * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
28536
- * footage ledger, our storage locations, our retention. This one has a
28537
- * different authority — the camera's firmware — and per D62 it stores
28538
- * nothing of its own. Every value here is read from the camera and every
28539
- * write goes back to the camera; there is no CamStack-side mirror that
28540
- * could disagree with the device.
28541
- *
28542
- * ## One shape, two firmwares
28543
- *
28544
- * Measured 2026-09-22 against the live fleet:
28545
- *
28546
- * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
28547
- * | --- | --- | --- |
28548
- * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
28549
- * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
28550
- * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
28551
- * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
28552
- * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
28553
- * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
28554
- * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
28555
- * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
28556
- *
28557
- * The two schedule models look different and are the same thing in
28558
- * different coordinates: both answer "for this trigger, during which
28559
- * weekly windows does the camera record". {@link RecordWindow} is that
28560
- * question in one shape — Hikvision's ranges map straight onto it,
28561
- * Reolink's mask expands into hour-aligned windows.
28562
- *
28563
- * ## Union, not intersection
28564
- *
28565
- * **The same fields exist on every camera.** What differs per device is
28566
- * which VALUES that device accepts, and that is what {@link
28567
- * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
28568
- * per field plus the schedule's own limits. A control a camera cannot
28569
- * honour is rendered DISABLED WITH ITS REASON, never missing and never
28570
- * dead: disabled must not look like broken.
28571
- *
28572
- * ## Refusal by name
28573
- *
28574
- * A write a camera cannot honour is refused with a sentence the operator
28575
- * can read — never accepted and dropped. Both providers refuse through
28576
- * {@link describeOnboardRefusal}, so the vocabulary is one function and
28577
- * one test, not two hand-written vendor opinions.
28578
- *
28579
- * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
28580
- * `getOptions` advertises per-camera availability, `getStatus` (auto-
28581
- * injected from `status`) reports the live values, and a single
28582
- * `setSettings` mutation applies a partial change. No hand-written
28583
- * settings-contribution methods.
28584
- */
28585
- /**
28586
- * What makes the camera start recording during a window.
28587
- *
28588
- * The union of both vendors' vocabularies. `continuous` is Hikvision's
28589
- * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
28590
- * object-class triggers are Reolink-only today and the smart-event ones
28591
- * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
28592
- * firmwares measured — a camera that cannot record on a trigger simply
28593
- * does not list it in `options.schedule.triggers`, and a window naming
28594
- * it is REFUSED, not dropped.
28595
- */
28596
- var RecordTriggerSchema = _enum([
28597
- "continuous",
28598
- "motion",
28599
- "person",
28600
- "vehicle",
28601
- "animal",
28602
- "lineCrossing",
28603
- "intrusion",
28604
- "loitering",
28605
- "alarmInput"
28606
- ]);
28607
- /**
28608
- * One weekly recording window: "on `day`, from `startMinute` to
28609
- * `endMinute`, record on `trigger`".
28610
- *
28611
- * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
28612
- * both firmwares enumerate). Minutes are local camera time since
28613
- * midnight; `endMinute` may be 1440, meaning end of day — that is
28614
- * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
28615
- * collapsing it to 0 would turn a whole-day window into an empty one.
28616
- */
28617
- var RecordWindowSchema = object({
28618
- trigger: RecordTriggerSchema,
28619
- day: number().int().min(0).max(6),
28620
- startMinute: number().int().min(0).max(1439),
28621
- endMinute: number().int().min(1).max(1440)
28622
- });
28623
- /** Status of one physical volume, as the camera itself describes it. */
28624
- var OnboardStorageVolumeSchema = object({
28625
- /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
28626
- id: string(),
28627
- /** The camera's own name for it, when it gives one (`hddName`). */
28628
- label: string().optional(),
28629
- status: _enum([
28630
- "ok",
28631
- "unformatted",
28632
- "error",
28633
- "offline",
28634
- "unknown"
28635
- ]),
28636
- /**
28637
- * Total size in MB, or **null when the camera did not say**.
28638
- *
28639
- * Never 0 for an unreadable value: a measurement that failed is not a
28640
- * measurement (D393), and a card whose size is unknown must not be
28641
- * rendered as a card of size zero.
28642
- */
28643
- capacityMb: number().nullable(),
28644
- /**
28645
- * Free space in MB, or null when unknown.
28646
- *
28647
- * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
28648
- * 1439 both report exactly 11776 MB free — the fixed reserve a looping
28649
- * card converges on once it has wrapped. At loop steady state the
28650
- * number is identical whether the camera recorded yesterday or stopped
28651
- * a month ago.
28652
- */
28653
- freeMb: number().nullable(),
28654
- /** True when the camera reports the volume writable (`property` RW). */
28655
- writable: boolean().optional()
28656
- });
28657
- /**
28658
- * What the camera is doing with its own storage, right now.
28659
- *
28660
- * Every scalar is nullable and **null means the camera did not answer**,
28661
- * never a default. A form that seeds `0` from an unanswered read invites
28662
- * the operator to save that 0 back onto the camera.
28663
- */
28664
- var RecordingOnboardStatusSchema = object({
28665
- storage: discriminatedUnion("kind", [
28666
- object({
28667
- kind: literal("present"),
28668
- volumes: array(OnboardStorageVolumeSchema)
28669
- }),
28670
- object({
28671
- kind: literal("absent"),
28672
- reason: string()
28673
- }),
28674
- object({
28675
- kind: literal("unknown"),
28676
- reason: string()
28677
- })
28678
- ]),
28679
- tracks: array(object({
28680
- id: string(),
28681
- enabled: boolean(),
28682
- isVideo: boolean(),
28683
- /** From the camera's own track description. Null when it does not say. */
28684
- codec: string().nullable(),
28685
- resolution: string().nullable(),
28686
- /** Per-track overwrite flag, where the firmware keeps it per track. */
28687
- overwriteWhenFull: boolean().nullable()
28688
- })),
28689
- /**
28690
- * The track the write path targets — the enabled VIDEO one. Null when
28691
- * no track could be identified, which is itself a refusal reason.
28692
- */
28693
- primaryTrackId: string().nullable(),
28694
- /** Master "record to the card at all" switch. */
28695
- enabled: boolean().nullable(),
28696
- overwriteWhenFull: boolean().nullable(),
28697
- preRecordSec: number().nullable(),
28698
- postRecordSec: number().nullable(),
28699
- /** Length of one recorded file, in minutes. */
28700
- segmentMinutes: number().nullable(),
28701
- /** The primary track's weekly windows, flattened. */
28702
- windows: array(RecordWindowSchema),
28703
- /**
28704
- * How many windows the camera described that CamStack could NOT read —
28705
- * an unrecognised trigger, an unparseable clock, a weekday it does not
28706
- * name.
28707
- *
28708
- * A dropped window is work the reader threw away, and a schedule that
28709
- * silently shows fewer rows than the camera holds is how an operator
28710
- * saves back a schedule shorter than the one they were looking at
28711
- * (D391). Non-zero means the window list is INCOMPLETE and a write
28712
- * that replaces it would delete what was not shown — which is why a
28713
- * provider reporting a non-zero count also reports the schedule as not
28714
- * writable.
28715
- */
28716
- unreadableWindows: number(),
28717
- /**
28718
- * The camera is scheduled to record and has NO usable storage.
28719
- *
28720
- * A first-class fact because it is the fleet's most common silent
28721
- * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
28722
- * to a card that is not there. Neither the schedule nor the storage
28723
- * read says anything wrong on its own; only the pair does.
28724
- */
28725
- recordingToNowhere: boolean(),
28726
- lastFetchedAt: number()
28727
- });
28728
- /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
28729
- var RangeSchema = object({
28730
- min: number(),
28731
- max: number(),
28732
- step: number()
28733
- });
28734
- /**
28735
- * The values a camera actually takes for a numeric field, when they are a SET
28736
- * rather than a range.
28737
- *
28738
- * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
28739
- * (I91DN) on 2026-09-22 by writing each value and reading it back:
28740
- *
28741
- * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
28742
- * camera's "no limit" — `-1` and `4294967295` both land on it);
28743
- * - post-record: `5, 10, 30, 60, 120, 300, 600`.
28744
- *
28745
- * Neither is expressible as a step: the first has a sentinel two billion away
28746
- * from its neighbours, the second doubles and then jumps. A range that tried
28747
- * would forbid values the camera takes AND permit values it silently replaces
28748
- * with 5 — wrong in both directions at once.
28749
- *
28750
- * `sentinel` names the member that is not a duration, so a surface can render
28751
- * "no limit" instead of `2147483647` seconds.
28752
- */
28753
- var AllowedValuesSchema = object({
28754
- values: array(number()).min(1),
28755
- sentinel: object({
28756
- value: number(),
28757
- meaning: _enum(["no-limit", "disabled"])
28758
- }).optional()
28759
- });
28760
- /**
28761
- * Per-field availability on ONE camera.
28762
- *
28763
- * The field exists on every camera — this says whether this one can be
28764
- * read and whether it can be written, and `reason` says why not when
28765
- * either is false. The UI renders the control DISABLED with the reason
28766
- * rather than hiding it, so a limitation is legible instead of looking
28767
- * like a missing feature.
28768
- */
28769
- var OnboardFieldSupportSchema = object({
28770
- readable: boolean(),
28771
- writable: boolean(),
28772
- /** Required whenever `readable` or `writable` is false. */
28773
- reason: string().optional()
28774
- });
28775
- /** What this camera's schedule model can express. */
28776
- var OnboardScheduleSupportSchema = object({
28777
- support: OnboardFieldSupportSchema,
28778
- /**
28779
- * The smallest time step the camera can express, in minutes.
28780
- *
28781
- * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
28782
- * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
28783
- * window whose edges are not a multiple of this is REFUSED rather than
28784
- * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
28785
- * and nothing says so.
28786
- */
28787
- granularityMinutes: number(),
28788
- /** Triggers this camera can record on. A window naming another is refused. */
28789
- triggers: array(RecordTriggerSchema),
28790
- /**
28791
- * False when the camera stores ONE trigger per time range, so two
28792
- * windows overlapping on the same day cannot carry different triggers.
28793
- * True on Reolink, whose mask is per-trigger and independent.
28794
- */
28795
- supportsOverlappingTriggers: boolean()
28796
- });
28797
- var RecordingOnboardOptionsSchema = object({
28798
- enabled: OnboardFieldSupportSchema,
28799
- overwriteWhenFull: OnboardFieldSupportSchema,
28800
- preRecordSec: OnboardFieldSupportSchema,
28801
- preRecordSecRange: RangeSchema.optional(),
28802
- /** Preferred over the range when the camera takes a SET, not a span. */
28803
- preRecordSecAllowed: AllowedValuesSchema.optional(),
28804
- postRecordSec: OnboardFieldSupportSchema,
28805
- postRecordSecRange: RangeSchema.optional(),
28806
- /** Preferred over the range when the camera takes a SET, not a span. */
28807
- postRecordSecAllowed: AllowedValuesSchema.optional(),
28808
- segmentMinutes: OnboardFieldSupportSchema,
28809
- segmentMinutesRange: RangeSchema.optional(),
28810
- /** Preferred over the range when the camera takes a SET, not a span. */
28811
- segmentMinutesAllowed: AllowedValuesSchema.optional(),
28812
- schedule: OnboardScheduleSupportSchema
28813
- });
28814
- /**
28815
- * A partial change. Every field optional.
28816
- *
28817
- * Unlike the other `deviceConfig` caps, a provider here does **NOT**
28818
- * silently ignore a field it cannot support — it refuses, by name,
28819
- * through {@link describeOnboardRefusal}. Silence on a recording setting
28820
- * is the failure D62 exists to prevent: the operator believes the camera
28821
- * is recording the way the form says, and it is not.
28822
- */
28823
- var RecordingOnboardPatchSchema = object({
28824
- enabled: boolean().optional(),
28825
- overwriteWhenFull: boolean().optional(),
28826
- preRecordSec: number().optional(),
28827
- postRecordSec: number().optional(),
28828
- segmentMinutes: number().optional(),
28829
- /** The complete new window set for the primary track — not a delta. */
28830
- windows: array(RecordWindowSchema).optional()
28831
- });
28832
- var recordingOnboardCapability = {
28833
- name: "recording-onboard",
28834
- scope: "device",
28835
- deviceNative: true,
28836
- mode: "singleton",
28837
- deviceTypes: [DeviceType.Camera],
28838
- deviceConfig: { ui: {
28839
- kind: "derived-form",
28840
- builderId: "recording-onboard",
28841
- tab: "recording"
28842
- } },
28843
- methods: {
28844
- getOptions: method(object({ deviceId: number() }), RecordingOnboardOptionsSchema),
28845
- setSettings: method(object({
28846
- deviceId: number(),
28847
- settings: RecordingOnboardPatchSchema
28848
- }), _void(), {
28849
- kind: "mutation",
28850
- auth: "admin"
28851
- })
28852
- },
28853
- status: {
28854
- schema: RecordingOnboardStatusSchema,
28855
- kind: "poll"
28856
- },
28857
- runtimeState: RecordingOnboardStatusSchema,
28858
- /**
28859
- * Runtime-state durability: **restored** — operator-set camera-side
28860
- * recording config; mutation-driven, and the storage half is the last
28861
- * thing the camera said about its own card.
28862
- *
28863
- * See `RuntimeStateDurability`. Enforced by
28864
- * `scripts/check-runtime-state-durability.ts`.
28865
- */
28866
- durability: "restored",
28867
- /** Clock fields: written, but excluded from the compare that decides
28868
- * whether persisting is worth a SQLite commit. */
28869
- volatileStateFields: ["lastFetchedAt"]
28870
- };
28871
- /**
28872
- * Day-of-week names in the cap's index order (0 = Monday), for messages
28873
- * an operator reads and for Hikvision's `<DayOfWeek>` element.
28874
- */
28875
- var DAY_NAMES = [
28876
- "Monday",
28877
- "Tuesday",
28878
- "Wednesday",
28879
- "Thursday",
28880
- "Friday",
28881
- "Saturday",
28882
- "Sunday"
28883
- ];
28884
- /**
28885
- * Does `patch` ask this camera for something it cannot do?
28886
- *
28887
- * Returns the operator-readable reason, or `null` when every field in
28888
- * the patch is within what `options` says this camera accepts. A
28889
- * provider calls this BEFORE touching the camera and throws the string:
28890
- * refusing is the point, and refusing identically on both vendors is why
28891
- * this is one function.
28892
- *
28893
- * The order of checks is the order an operator would read them: the
28894
- * scalar knobs first, then the schedule, because a schedule complaint is
28895
- * longer and a scalar one is usually the real problem.
28896
- */
28897
- function describeOnboardRefusal(options, patch) {
28898
- if (patch.enabled !== void 0 && !options.enabled.writable) return unwritable("the master recording switch", options.enabled.reason);
28899
- if (patch.overwriteWhenFull !== void 0 && !options.overwriteWhenFull.writable) return unwritable("overwrite-when-full", options.overwriteWhenFull.reason);
28900
- const preRefusal = refuseNumeric("pre-record seconds", patch.preRecordSec, options.preRecordSec, options.preRecordSecRange, options.preRecordSecAllowed);
28901
- if (preRefusal) return preRefusal;
28902
- const postRefusal = refuseNumeric("post-record seconds", patch.postRecordSec, options.postRecordSec, options.postRecordSecRange, options.postRecordSecAllowed);
28903
- if (postRefusal) return postRefusal;
28904
- const segmentRefusal = refuseNumeric("segment length (minutes)", patch.segmentMinutes, options.segmentMinutes, options.segmentMinutesRange, options.segmentMinutesAllowed);
28905
- if (segmentRefusal) return segmentRefusal;
28906
- if (patch.windows !== void 0) {
28907
- const scheduleRefusal = refuseSchedule(options, patch.windows);
28908
- if (scheduleRefusal) return scheduleRefusal;
28909
- }
28910
- return null;
28911
- }
28912
- function unwritable(what, reason) {
28913
- return reason ? `this camera cannot change ${what}: ${reason}` : `this camera cannot change ${what}`;
28914
- }
28915
- function refuseNumeric(what, value, support, range, allowed) {
28916
- if (value === void 0) return null;
28917
- if (!support.writable) return unwritable(what, support.reason);
28918
- if (!Number.isFinite(value)) return `${what} must be a number, got ${String(value)}`;
28919
- if (allowed) {
28920
- if (allowed.values.includes(value)) return null;
28921
- const say = (v) => allowed.sentinel !== void 0 && v === allowed.sentinel.value ? allowed.sentinel.meaning === "no-limit" ? "no limit" : "off" : String(v);
28922
- return `this camera accepts ${what} only as ${allowed.values.map(say).join(", ")}; ${say(value)} is not one of them`;
28923
- }
28924
- if (!range) return null;
28925
- if (value < range.min || value > range.max) return `this camera accepts ${what} between ${String(range.min)} and ${String(range.max)}; ${String(value)} is outside that`;
28926
- if (range.step > 0 && (value - range.min) % range.step !== 0) return `this camera accepts ${what} in steps of ${String(range.step)} from ${String(range.min)}; ${String(value)} is not one of them`;
28927
- return null;
28928
- }
28929
- function refuseSchedule(options, windows) {
28930
- const schedule = options.schedule;
28931
- if (!schedule.support.writable) return unwritable("the recording schedule", schedule.support.reason);
28932
- const allowed = new Set(schedule.triggers);
28933
- for (const window of windows) {
28934
- if (!allowed.has(window.trigger)) {
28935
- const offer = schedule.triggers.length > 0 ? schedule.triggers.join(", ") : "none";
28936
- return `this camera cannot record on "${window.trigger}"; it records on: ${offer}`;
28937
- }
28938
- if (window.endMinute <= window.startMinute) return `a window on ${dayName(window.day)} ends at or before it starts (${String(window.startMinute)} → ${String(window.endMinute)})`;
28939
- const step = schedule.granularityMinutes;
28940
- if (step > 1 && (window.startMinute % step !== 0 || window.endMinute % step !== 0)) return `this camera's schedule only moves in ${String(step)}-minute steps; the ${dayName(window.day)} window ${formatMinutes(window.startMinute)}–${formatMinutes(window.endMinute)} is not aligned to them`;
28941
- }
28942
- if (!schedule.supportsOverlappingTriggers) {
28943
- const clash = findOverlapWithDifferentTrigger(windows);
28944
- if (clash) return `this camera stores one trigger per time range, so "${clash.a.trigger}" and "${clash.b.trigger}" cannot both cover ${dayName(clash.a.day)} ${formatMinutes(Math.max(clash.a.startMinute, clash.b.startMinute))}–${formatMinutes(Math.min(clash.a.endMinute, clash.b.endMinute))}`;
28945
- }
28946
- return null;
28947
- }
28948
- function findOverlapWithDifferentTrigger(windows) {
28949
- for (let i = 0; i < windows.length; i += 1) for (let j = i + 1; j < windows.length; j += 1) {
28950
- const a = windows[i];
28951
- const b = windows[j];
28952
- if (!a || !b) continue;
28953
- if (a.day !== b.day) continue;
28954
- if (a.trigger === b.trigger) continue;
28955
- if (a.startMinute < b.endMinute && b.startMinute < a.endMinute) return {
28956
- a,
28957
- b
28958
- };
28959
- }
28960
- return null;
28961
- }
28962
- function dayName(day) {
28963
- return DAY_NAMES[day] ?? `day ${String(day)}`;
28964
- }
28965
- /** `510` → `08:30`. For messages, not for the wire. */
28966
- function formatMinutes(minute) {
28967
- const hour = Math.floor(minute / 60);
28968
- const rest = minute % 60;
28969
- return `${String(hour).padStart(2, "0")}:${String(rest).padStart(2, "0")}`;
28970
- }
28971
- /**
28972
- * Index of the mask character covering `day` at `hour`.
28973
- *
28974
- * **Layout: day-major, Monday first** — `day * 24 + hour`.
28975
- *
28976
- * This is an ASSUMPTION, and it is recorded as one because the fleet
28977
- * cannot currently confirm it: every `valueTable` on every Reolink
28978
- * measured on 2026-09-22 is uniform (all `1` or all `0`), and a uniform
28979
- * mask reads identically under either layout. To settle it, set a single
28980
- * hour on one day from the Reolink app and re-read `getRecordSchedule`.
28981
- * Until then, a NON-uniform mask is reported as windows under this
28982
- * layout and the day labels may be transposed; a uniform one is exact.
28983
- */
28984
- function weeklyMaskIndex(day, hour) {
28985
- return day * 24 + hour;
28986
- }
28987
- /**
28988
- * Expand one trigger's weekly hour mask into windows, merging adjacent
28989
- * set hours into one window per run so a full day is ONE window rather
28990
- * than twenty-four.
28991
- *
28992
- * A mask of the wrong length yields no windows: a truncated table is not
28993
- * a schedule of fewer days, it is an answer we did not understand.
28994
- */
28995
- function weeklyMaskToWindows(mask, trigger) {
28996
- if (mask.length !== 168) return [];
28997
- const out = [];
28998
- for (let day = 0; day < 7; day += 1) {
28999
- let runStart = null;
29000
- for (let hour = 0; hour <= 24; hour += 1) {
29001
- const set = hour < 24 && mask[weeklyMaskIndex(day, hour)] === "1";
29002
- if (set && runStart === null) runStart = hour;
29003
- else if (!set && runStart !== null) {
29004
- out.push({
29005
- trigger,
29006
- day,
29007
- startMinute: runStart * 60,
29008
- endMinute: hour * 60
29009
- });
29010
- runStart = null;
29011
- }
29012
- }
29013
- }
29014
- return out;
29015
- }
29016
- /**
29017
- * Collapse this trigger's windows back into a weekly hour mask.
29018
- *
29019
- * Windows for other triggers are ignored — the mask is per trigger. A
29020
- * window that is not hour-aligned is REFUSED upstream by
29021
- * {@link describeOnboardRefusal}; this function rounds nothing, it
29022
- * simply sets every hour the window fully or partly covers, so a caller
29023
- * that skipped the refusal cannot silently lose coverage.
29024
- */
29025
- function windowsToWeeklyMask(windows, trigger) {
29026
- const slots = new Array(168).fill("0");
29027
- for (const window of windows) {
29028
- if (window.trigger !== trigger) continue;
29029
- if (window.day < 0 || window.day >= 7) continue;
29030
- const firstHour = Math.floor(window.startMinute / 60);
29031
- const lastHour = Math.ceil(window.endMinute / 60);
29032
- for (let hour = firstHour; hour < lastHour && hour < 24; hour += 1) slots[weeklyMaskIndex(window.day, hour)] = "1";
29033
- }
29034
- return slots.join("");
29035
- }
29036
- /**
29037
28716
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
29038
28717
  * for every device, regardless of provider — the kernel needs a uniform
29039
28718
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -29252,30 +28931,6 @@ var eventEmitterCapability = {
29252
28931
  */
29253
28932
  durability: "session"
29254
28933
  };
29255
- var EventItemSchema = object({
29256
- id: string(),
29257
- type: string(),
29258
- timestamp: number(),
29259
- label: string().optional(),
29260
- thumbnailUrl: string().optional(),
29261
- clipUrl: string().optional(),
29262
- metadata: record(string(), unknown()).optional()
29263
- });
29264
- DeviceType.Camera, method(object({
29265
- deviceId: number(),
29266
- from: number().optional(),
29267
- to: number().optional(),
29268
- limit: number().optional()
29269
- }), array(EventItemSchema)), method(object({
29270
- deviceId: number(),
29271
- eventId: string()
29272
- }), object({
29273
- base64: string(),
29274
- contentType: string()
29275
- }).nullable()), method(object({
29276
- deviceId: number(),
29277
- eventId: string()
29278
- }), string().nullable());
29279
28934
  var IdentitySchema = object({
29280
28935
  id: string(),
29281
28936
  name: string(),
@@ -31528,143 +31183,6 @@ var motionTriggerCapability = {
31528
31183
  durability: "session"
31529
31184
  };
31530
31185
  /**
31531
- * camera-grid-layout — the geometry of a COMPOSITE camera, on the camera's own
31532
- * page.
31533
- *
31534
- * ## Why this is a capability and not an addon settings schema
31535
- *
31536
- * It was one, and it did not render. The addon declared the editor as a
31537
- * `type: 'widget'` field inside its own `deviceSettingsSchema()`; the hub
31538
- * returned that section correctly and `ConfigFormField` renders `type:'widget'`
31539
- * perfectly well — and nothing ever asked camera-grid for it. The Cluster →
31540
- * Pipeline → Device Overrides page interrogates a HAND-WRITTEN list of four
31541
- * addons (`PIPELINE_CLUSTER_DEVICE_ADDONS`), whose own comment says an addon
31542
- * not on it "falls off silently".
31543
- *
31544
- * Adding a fifth name to that list would have been the wrong fix twice over:
31545
- * that page is per-camera DETECTION tuning, and a grid's geometry belongs
31546
- * beside PTZ and motion zones on the camera itself. The device page is
31547
- * BINDING-driven (D12), so the way in is a capability bound to the device —
31548
- * and this cap carries its section the way `recording` does, by RETURNING it
31549
- * from `getDeviceSettingsContribution`.
31550
- *
31551
- * Seven other widgets are still declared the other way, through a
31552
- * `deviceConfig.ui` block the framework derives a section from. That route
31553
- * gives the addon no say in where its own panel lands and no way to decline
31554
- * for a device the panel does not suit, which is why this one does not use it.
31555
- *
31556
- * ## Why one addon may implement it
31557
- *
31558
- * It is a device-scoped NATIVE cap, registered by the grid camera device
31559
- * itself. Nothing else declares a composite camera, so nothing else has a
31560
- * layout — and the device-scoped route means the widget asks THE camera, not
31561
- * "the camera-grid addon", which is what let the old custom-action pair be
31562
- * reached only by a caller that already knew the addon id.
31563
- *
31564
- * ## The tab
31565
- *
31566
- * `streaming`, not a top-tab of its own. A grid's geometry IS what its stream
31567
- * is, so the Streaming tab is where it belongs; a `grid` top-tab would need an
31568
- * entry in `WELL_KNOWN_TAB_MAP` or the device page renders the raw id as the
31569
- * label (measured on the robot camera, 2026-09-06 — a tab called "navigation"
31570
- * next to "PTZ").
31571
- */
31572
- /** A rectangle in normalized [0,1] coordinates of whatever contains it. */
31573
- var GridNormalizedRectSchema = object({
31574
- x: number().min(0).max(1),
31575
- y: number().min(0).max(1),
31576
- width: number().gt(0).max(1),
31577
- height: number().gt(0).max(1)
31578
- });
31579
- /**
31580
- * One source camera, the part of its picture taken, and where that part lands.
31581
- *
31582
- * Both rectangles are NORMALIZED (D519): a source camera can change resolution
31583
- * — a profile switch, a firmware update, a substream that comes back different
31584
- * — and a stored PIXEL rectangle would quietly start cutting the wrong region,
31585
- * which is the class of bug nobody files.
31586
- */
31587
- var GridLayoutCellSchema = object({
31588
- deviceId: number().int().positive(),
31589
- /** The part of the SOURCE taken, normalized against the source. */
31590
- source: GridNormalizedRectSchema,
31591
- /** Where it lands, normalized against the CANVAS. */
31592
- cell: GridNormalizedRectSchema
31593
- });
31594
- /**
31595
- * Which profiles this grid can actually compose, and why not.
31596
- *
31597
- * A grid's `high` composes its sources' `high` and its `low` their `low`, so a
31598
- * profile is on offer only when EVERY source can serve it. The refusal NAMES
31599
- * the sources, because "this grid has no low" is not a finding — "615 has no
31600
- * low" is, and it is the one an operator can act on.
31601
- */
31602
- var GridProfileOfferSchema = object({
31603
- profile: _enum([
31604
- "high",
31605
- "mid",
31606
- "low"
31607
- ]),
31608
- offered: boolean(),
31609
- /** Sources that cannot serve it. Empty when it is offered, or when there are no cells. */
31610
- missingSources: array(number().int().positive()),
31611
- /**
31612
- * The canvas this profile composes onto, `WxH`, or empty when it is not
31613
- * offered. DERIVED from the cells and the sources' own size at this profile —
31614
- * it is reported because nothing else in the system would ever say what the
31615
- * grid came out as, and because it is the number an operator would otherwise
31616
- * expect to type.
31617
- */
31618
- canvas: string(),
31619
- /**
31620
- * Whether this profile is PUBLISHED, of the ones the grid could serve.
31621
- *
31622
- * A grid's `high` is composed of its sources' `high`, so on a 4K fleet it is
31623
- * a 4K canvas built from 4K decodes — something to opt into, not something a
31624
- * viewer's adaptive should be handed by climbing to the top rung it can see.
31625
- * Default is `mid` + `low`.
31626
- */
31627
- published: boolean()
31628
- });
31629
- var GridLayoutViewSchema = object({
31630
- /** The persisted grid row this camera was declared from. */
31631
- instanceId: string(),
31632
- deviceId: number().int().nonnegative(),
31633
- name: string(),
31634
- /**
31635
- * NO canvas size. A grid's resolution is not authored: each profile derives
31636
- * its own from the cells and its sources' dimensions. The two numbers that
31637
- * used to be here were a text field that silently decided both how much the
31638
- * composite cost and how sharp it was — see `profiles[].canvas` for what it
31639
- * came out as.
31640
- */
31641
- fps: number().int(),
31642
- cells: array(GridLayoutCellSchema),
31643
- /** What the catalog will publish, and what it refuses to. Read-only. */
31644
- profiles: array(GridProfileOfferSchema)
31645
- });
31646
- var GridLayoutPatchSchema = object({
31647
- deviceId: number().int().nonnegative(),
31648
- name: string().min(1).max(160).optional(),
31649
- fps: number().int().min(1).max(60).optional(),
31650
- /** Which profiles to publish. See `GridProfileOffer.published`. */
31651
- publishedProfiles: array(_enum([
31652
- "high",
31653
- "mid",
31654
- "low"
31655
- ])).max(3).optional(),
31656
- /**
31657
- * The whole cell list at once. A per-cell patch would need an ordering the
31658
- * editor does not have, and a half-applied layout is a picture nobody asked
31659
- * for.
31660
- */
31661
- cells: array(GridLayoutCellSchema).max(16)
31662
- });
31663
- DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), GridLayoutViewSchema.nullable(), { auth: "admin" }), method(GridLayoutPatchSchema, GridLayoutViewSchema, {
31664
- kind: "mutation",
31665
- auth: "admin"
31666
- });
31667
- /**
31668
31186
  * Motion-zones share the same MaskShape vocabulary as privacy-mask — the
31669
31187
  * on-camera motion-detection mask is a single `grid` region (a row-major
31670
31188
  * boolean cell lattice the camera's onboard VMD evaluates). Composing it as
@@ -34184,37 +33702,37 @@ var rebootCapability = {
34184
33702
  auth: "admin"
34185
33703
  }) }
34186
33704
  };
34187
- /**
34188
- * `recording` cap — footage availability + HLS playback manifests + per-device
34189
- * recording config. NOTE on events (source of truth, R5/C3): this cap carries
34190
- * NO event surface — `getPlaybackManifest` returns playlist URLs only. Timeline
34191
- * events (motion/object/audio) come from `pipelineAnalytics` (durable SQLite
34192
- * rows) and are the ONLY event surface — the recorder has none. The in-RAM
34193
- * playback markers it used to build were deleted on 2026-08-29 because nothing
34194
- * ever read them. Event<->footage joins are by time, padded with the shared
34195
- * `EVENT_PAD_MS` (`interfaces/recording-config.ts`).
34196
- */
34197
- var RecordingStatusSchema = object({
34198
- deviceId: number(),
34199
- enabled: boolean(),
34200
- /** THE derived storage mode, from the one definition
34201
- * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
34202
- * `on-device-decision` could have reached the recorder and not the status. */
34203
- activeMode: RecordingStorageModeSchema,
34204
- nodeId: string(),
34205
- storageBytes: number()
34206
- });
34207
33705
  var RecordingRangeSchema = object({
34208
33706
  profile: string(),
34209
33707
  startMs: number(),
34210
33708
  endMs: number()
34211
33709
  });
33710
+ /**
33711
+ * How a source ANSWERED, on every singular read of this cap.
33712
+ *
33713
+ * `'read'` — it looked. `ranges: []` / `days: []` is then a real claim: this
33714
+ * source has no coverage in the window. `'unreadable'` — nobody could look
33715
+ * (the camera was unreachable, the calendar rung threw, the location is
33716
+ * unmounted, the node is still on the old build), and the emptiness beside it
33717
+ * means NOTHING.
33718
+ *
33719
+ * The batch rows have carried this since the grid existed; the SINGULAR
33720
+ * answers gained it with the collection (D625 §10.4), because they are the
33721
+ * ones the single-camera picker uses and because a half-converted fleet makes
33722
+ * "nobody looked" common for the length of a deploy. Without it the timeline
33723
+ * has no vocabulary for it, and `(data ?? [])` in a viewer turns a rollout into
33724
+ * a fleet of cameras that appear to have lost their recordings (D315, D393).
33725
+ */
33726
+ var RecordingReadSchema = _enum(["read", "unreadable"]);
34212
33727
  var RecordingAvailabilitySchema = object({
34213
33728
  deviceId: number(),
33729
+ /** See {@link RecordingReadSchema}. An `'unreadable'` answer carries an empty
33730
+ * `ranges` that means nothing — never draw it as "no footage". */
33731
+ read: RecordingReadSchema,
34214
33732
  ranges: array(RecordingRangeSchema),
34215
33733
  /**
34216
- * Every profile this camera has footage in — not only the one `ranges`
34217
- * describes (D433).
33734
+ * Every profile this camera has footage in AT THIS SOURCE — not only the one
33735
+ * `ranges` describes (D433).
34218
33736
  *
34219
33737
  * `ranges` answers for ONE profile by design: the timeline is a single bar,
34220
33738
  * and enumerating all of them triples the directory reads for a bar that
@@ -34231,15 +33749,285 @@ var RecordingAvailabilitySchema = object({
34231
33749
  });
34232
33750
  var RecordingDaysSchema = object({
34233
33751
  deviceId: number(),
33752
+ /** See {@link RecordingReadSchema}. `days: []` on an `'unreadable'` answer is
33753
+ * "nobody could look", and the date-picker must not spell it the same as
33754
+ * "no footage this month". */
33755
+ read: RecordingReadSchema,
34234
33756
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
34235
33757
  days: array(number())
34236
33758
  });
33759
+ var RecordingManifestSchema = object({
33760
+ deviceId: number(),
33761
+ /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
33762
+ localMasterPath: string().nullable(),
33763
+ /** HTTP(S) URL to the master playlist on the recording node's playback server
33764
+ * (the PRIMARY candidate); null when no recording / server. Carries the
33765
+ * scoped playback token in its path. */
33766
+ playbackUrl: string().nullable(),
33767
+ /**
33768
+ * Candidate master-playlist URLs the client tries in order (LAN first, then
33769
+ * remote — Tailscale/Cloudflare if the operator configured extra hosts), each
33770
+ * carrying the same scoped token. `playbackUrl` is the first entry. Empty when
33771
+ * there is no recording / server.
33772
+ */
33773
+ playbackEndpoints: array(string())
33774
+ });
33775
+ var RecordingSourceAvailabilitySchema = object({
33776
+ state: _enum([
33777
+ "ok",
33778
+ "sleeping",
33779
+ "unreachable",
33780
+ "no-storage",
33781
+ "index-empty"
33782
+ ]),
33783
+ /** Free text, shown verbatim. Names the camera's own refusal when there is one. */
33784
+ reason: string().optional(),
33785
+ /** When this source's coverage was last CONFIRMED. A cached answer is never
33786
+ * drawn as current: the surface shows the age whenever it is older than the
33787
+ * refresh interval. The clip catalog's `catalogAsOf`, under the name the
33788
+ * timeline uses for it. */
33789
+ coverageAsOf: number().optional()
33790
+ });
33791
+ /**
33792
+ * One SOURCE of recorded coverage for a camera — a row of the picker.
33793
+ *
33794
+ * A provider lists the sources IT serves for that device, and answers for each
33795
+ * of them whether it can answer at all. A provider with nothing to offer on a
33796
+ * camera returns `[]` — it is not that camera's business. The five availability
33797
+ * states are `ClipSourceAvailability`'s verbatim: they mean exactly the same
33798
+ * things about a coverage index as about a clip catalog, and `sleeping` in
33799
+ * particular is what stops a battery camera being woken to paint a bar.
33800
+ */
33801
+ var RecordingSourceSchema = object({
33802
+ /** The source id. {@link RECORDING_SOURCE_CAMSTACK} for ours (RESERVED), a
33803
+ * vendor namespace (`native:reolink:onboard`, …) for a camera's own store. */
33804
+ source: string(),
33805
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
33806
+ label: string(),
33807
+ /**
33808
+ * The addon that SERVES this row, and the value a later call passes as
33809
+ * `provider`.
33810
+ *
33811
+ * Optional for version skew only. The collection dispatcher stamps it from
33812
+ * the registry, so a row that travelled through the fan-out carries the
33813
+ * authoritative id whatever the provider filled in (D557 §4).
33814
+ */
33815
+ addonId: string().optional(),
33816
+ availability: RecordingSourceAvailabilitySchema
33817
+ });
33818
+ /**
33819
+ * What a surface may DRAW for this (camera, source) — D612's rule applied to a
33820
+ * timeline: **the source declares what it can do, and the surface draws what
33821
+ * was declared. It never assumes, and never offers a gesture it will then
33822
+ * refuse.** D612 exists because `8` and `16` were offered as clip rates, the
33823
+ * broker clamped them to `4`, and no line anywhere said so.
33824
+ *
33825
+ * Asked once per (camera, source) before anything is drawn — never replaced by
33826
+ * a constant the surface keeps, which is the second authority D612 ends.
33827
+ */
33828
+ var RecordingSourceOptionsSchema = object({
33829
+ /** How this source's media reaches the player.
33830
+ * `archive` = our own indexed segment tree; `stream` = the provider's
33831
+ * forward-only fMP4 (D597); `realtime` = a replay bound to wall clock. */
33832
+ transport: _enum([
33833
+ "archive",
33834
+ "stream",
33835
+ "realtime"
33836
+ ]),
33837
+ /** What the BAR means. `continuous` = gaps are holes in a recording;
33838
+ * `sparse` = gaps are the absence of one, and must be drawn as such.
33839
+ *
33840
+ * Not an onboard-only concession: measured 2026-09-24, OUR bar covers 98.8 %
33841
+ * of 592's day and 1.2 % of 1436's. It is a fact about a (source, camera)
33842
+ * pair, and ours answers it per camera from `deriveRecordingMode`. */
33843
+ coverage: _enum(["continuous", "sparse"]),
33844
+ /** Where the playhead may be put.
33845
+ * `free` — anywhere, to the frame.
33846
+ * `forward` — only ahead of the current position.
33847
+ * `segment` — a position SNAPS to the head of the covering segment; a finer
33848
+ * ask is accepted by the camera and SILENTLY IGNORED. Measured
33849
+ * on 1436 (Hikvision V5.7.1, 2026-09-23): a window-narrowed
33850
+ * `ContentMgmt/search` returns a row and a `playbackURI`, the
33851
+ * replay opens 200 and delivers media — and the burned-in OSD of
33852
+ * the first frame reads the SEGMENT HEAD every time. Calling
33853
+ * that `forward` would tell the surface it may move the playhead
33854
+ * ahead within a loaded segment, which it may not. */
33855
+ seek: _enum([
33856
+ "free",
33857
+ "forward",
33858
+ "segment"
33859
+ ]),
33860
+ /** Frame-step BACKWARD is meaningful. */
33861
+ stepBack: boolean(),
33862
+ /** Whether the drag-scrub gesture is served, as opposed to refused by name. */
33863
+ scrub: boolean(),
33864
+ /** Deliverable rates, ascending, always containing `1`. The surface draws its
33865
+ * picker from this and from NOTHING else (D612, D620, D621). `0` is not a
33866
+ * member: pause is the absence of a rate. */
33867
+ rates: array(number().positive()).min(1).readonly(),
33868
+ /** TRUE when a read of this source HOLDS the camera's only playback session.
33869
+ * A surface with this set makes at most ONE read at a time and draws no
33870
+ * scrub-thumbnail strip, no hover preview, no prefetch and no background
33871
+ * refresh. The precedent is exact and expensive: filling one screen of
33872
+ * Hikvision thumbnails at 1.01× realtime consumed fifteen minutes of that
33873
+ * camera's only playback session (1.2.126, reported within minutes), and a
33874
+ * timeline is a screenful of reads by construction. */
33875
+ exclusive: boolean()
33876
+ });
33877
+ /**
33878
+ * How to PLAY the instant that was asked for, from the chosen source.
33879
+ *
33880
+ * No new media transport is built for onboard sources: the `clip` arm is a
33881
+ * DELEGATION to the `videoclips` transport that vendor already has (D597 /
33882
+ * D616 / D617). The onboard half of this collection is a PROJECTION of
33883
+ * `videoclips` for coverage and a delegation to it for bytes.
33884
+ */
33885
+ var RecordingPlaybackSchema = discriminatedUnion("kind", [
33886
+ object({
33887
+ kind: literal("hls"),
33888
+ manifest: RecordingManifestSchema
33889
+ }),
33890
+ object({
33891
+ kind: literal("clip"),
33892
+ /** The `videoclips` source namespace this clip id belongs to. */
33893
+ source: string(),
33894
+ clipId: string(),
33895
+ /** Where this clip actually STARTS. On a `seek: 'segment'` source the
33896
+ * playhead lands here, not at the requested instant — the surface must be
33897
+ * TOLD, not left to discover it from a burned-in OSD. */
33898
+ startsAtMs: number()
33899
+ }),
33900
+ object({
33901
+ kind: literal("none"),
33902
+ reason: string()
33903
+ })
33904
+ ]);
33905
+ DeviceType.Camera, method(object({ deviceId: number() }), array(RecordingSourceSchema).readonly(), {
33906
+ kind: "query",
33907
+ auth: "protected"
33908
+ }), method(object({
33909
+ deviceId: number(),
33910
+ /**
33911
+ * WHICH provider to ask — the `addonId` a {@link RecordingSourceSchema}
33912
+ * row carries, never a source id and never a list. **REQUIRED**, in the
33913
+ * schema, where the generated types make it unomittable rather than
33914
+ * merely discouraged (D554 amended).
33915
+ *
33916
+ * It was learned the expensive way on `videoclips.listClips`: measured
33917
+ * on the live hub 2026-09-20, device 592 bound to `recorder` AND
33918
+ * `provider-reolink`, a bare call with `limit: 3` answered SIX rows,
33919
+ * three from each source, merged — `device-collection-dispatch.ts`
33920
+ * leaves an unpinned fan-out un-narrowed, so absence buys the union the
33921
+ * method exists not to be. An un-narrowed `getAvailability` would do
33922
+ * that to a TIMELINE: our ranges and the card's clips unioned into one
33923
+ * bar, which is "two sources are never drawn together" broken in the
33924
+ * one place it matters most.
33925
+ *
33926
+ * A provider the device is not bound to is refused BY NAME (D552's
33927
+ * `rejectUnresolvedAddonPin`), never answered by another one.
33928
+ */
33929
+ provider: string().min(1),
33930
+ fromMs: number(),
33931
+ toMs: number(),
33932
+ /**
33933
+ * Answer for THIS profile instead of the source's preferred one (D433).
33934
+ * Absent keeps the timeline's behaviour — one bar, one profile, one set
33935
+ * of reads. `profilesWithFootage` on the answer says what may be asked
33936
+ * for.
33937
+ */
33938
+ profile: string().optional()
33939
+ }), RecordingAvailabilitySchema, {
33940
+ kind: "query",
33941
+ auth: "protected"
33942
+ }), method(object({
33943
+ deviceId: number(),
33944
+ provider: string().min(1),
33945
+ fromMs: number(),
33946
+ toMs: number(),
33947
+ tzOffsetMinutes: number()
33948
+ }), RecordingDaysSchema, {
33949
+ kind: "query",
33950
+ auth: "protected"
33951
+ }), method(object({
33952
+ deviceId: number(),
33953
+ provider: string().min(1),
33954
+ fromMs: number(),
33955
+ toMs: number(),
33956
+ profile: CamProfileSchema.optional()
33957
+ }), RecordingPlaybackSchema, {
33958
+ kind: "query",
33959
+ auth: "protected"
33960
+ }), method(object({
33961
+ deviceId: number(),
33962
+ provider: string().min(1)
33963
+ }), RecordingSourceOptionsSchema, {
33964
+ kind: "query",
33965
+ auth: "protected"
33966
+ });
33967
+ /**
33968
+ * `recording-archive` — OUR archive, and the intent that fills it.
33969
+ *
33970
+ * The system-singleton half of the 2026-09-24 cut (D625). `recording` used to
33971
+ * be one 33-method system singleton holding two unrelated subjects: three
33972
+ * per-camera READS about coverage and playback, and everything else — storage
33973
+ * locations, retention, relocation, rebalance, the ops log, the placement
33974
+ * table and the byte-plane primitives our scrub and export are built on.
33975
+ *
33976
+ * The reads became a device-scoped COLLECTION, so a camera's own card can be a
33977
+ * source beside ours (`recording.cap.ts`). Everything that is about OUR store,
33978
+ * or unimplementable by a camera, stayed here.
33979
+ *
33980
+ * ## On the name
33981
+ *
33982
+ * `recording-storage` was the obvious choice and is wrong: this cap also holds
33983
+ * `getDeviceConfig`/`setDeviceConfig`, which are recording INTENT — bands,
33984
+ * retention, the D62 switch authority — and a name that says "storage" invites
33985
+ * the next reader to move them out again. An archive is a thing we keep, and
33986
+ * what we keep it under is a policy; the name covers both halves honestly and
33987
+ * sits in the existing family (`recording-onboard`, `recording-export`,
33988
+ * `recording-signal`).
33989
+ *
33990
+ * ## What must NOT happen to it
33991
+ *
33992
+ * It stays a SINGLETON. It is registered by `recorder`, which is
33993
+ * `placement: 'any-node'` and runs on every recording node; the hub dispatches
33994
+ * to one of them. Putting the ledger, the placement table or the relocation
33995
+ * jobs behind a fan-out is the one genuinely dangerous move in this cut.
33996
+ *
33997
+ * `getDeviceConfig` / `setDeviceConfig` in particular are the D62 recording
33998
+ * authority (`CameraSwitch.authority`). If a write reached a different provider
33999
+ * than the read — which a collection fan-out permits — two authorities would
34000
+ * decide when one camera records, and the symptom (recording silently off, or
34001
+ * a `bands` array clobbered by a partial write) is durable and silent. Keeping
34002
+ * them here means the worst case during a rollout is a 412: the switch refuses
34003
+ * to flip and SAYS so. **Do not move them into the collection, at any point,
34004
+ * for any reason.**
34005
+ *
34006
+ * ## The two batch reads
34007
+ *
34008
+ * `getAvailabilityBatch` / `getDaysWithRecordingsBatch` take `deviceIds:
34009
+ * number[]` with no single `deviceId`, and a device-scoped mount routes
34010
+ * through `getProviderForDevice(deviceId)` — there is nothing for it to route
34011
+ * on. They stay here, and on this cap the batch is explicitly OURS: a grid has
34012
+ * no per-camera picker, and a caller that wants another source's coverage asks
34013
+ * `recording.getAvailability` per device with that source's `provider`.
34014
+ */
34015
+ var RecordingStatusSchema = object({
34016
+ deviceId: number(),
34017
+ enabled: boolean(),
34018
+ /** THE derived storage mode, from the one definition
34019
+ * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
34020
+ * `on-device-decision` could have reached the recorder and not the status. */
34021
+ activeMode: RecordingStorageModeSchema,
34022
+ nodeId: string(),
34023
+ storageBytes: number()
34024
+ });
34237
34025
  /**
34238
34026
  * One camera's row in a `getAvailabilityBatch` answer.
34239
34027
  *
34240
- * `ranges` is EXACTLY what `getAvailability` returns for that camera — the
34241
- * batch collapses the transport, not the work — plus the one thing the singular
34242
- * method never had to say:
34028
+ * `ranges` is EXACTLY what `recording.getAvailability` returns for that camera
34029
+ * at OUR source — the batch collapses the transport, not the work — plus the
34030
+ * `read` mark the singular answer now carries too (D625):
34243
34031
  *
34244
34032
  * - `read: 'read'` — answered. `ranges: []` means "read, and this camera has
34245
34033
  * no footage in the window", which is a real claim.
@@ -34271,22 +34059,6 @@ var RecordingDaysForDeviceSchema = object({
34271
34059
  /** Local-midnight epochs (UTC ms) of days that have ≥1 recorded segment. */
34272
34060
  days: array(number()).readonly()
34273
34061
  });
34274
- var RecordingManifestSchema = object({
34275
- deviceId: number(),
34276
- /** Local filesystem path to the master playlist; null when no recording exists for the requested range. */
34277
- localMasterPath: string().nullable(),
34278
- /** HTTP(S) URL to the master playlist on the recording node's playback server
34279
- * (the PRIMARY candidate); null when no recording / server. Carries the
34280
- * scoped playback token in its path. */
34281
- playbackUrl: string().nullable(),
34282
- /**
34283
- * Candidate master-playlist URLs the client tries in order (LAN first, then
34284
- * remote — Tailscale/Cloudflare if the operator configured extra hosts), each
34285
- * carrying the same scoped token. `playbackUrl` is the first entry. Empty when
34286
- * there is no recording / server.
34287
- */
34288
- playbackEndpoints: array(string())
34289
- });
34290
34062
  /**
34291
34063
  * Recording storage usage for one camera — what the ARCHIVE holds for it,
34292
34064
  * across every profile and every resolvable location on this node.
@@ -34545,34 +34317,12 @@ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
34545
34317
  segmentEndMs: number()
34546
34318
  })]);
34547
34319
  method(object({
34548
- deviceId: number(),
34549
- fromMs: number(),
34550
- toMs: number(),
34551
- /**
34552
- * Answer for THIS profile instead of the preferred one (D433). Absent
34553
- * keeps the timeline's behaviour — one bar, one profile, one set of
34554
- * reads. `profilesWithFootage` on the answer says what may be asked
34555
- * for.
34556
- */
34557
- profile: string().optional()
34558
- }), RecordingAvailabilitySchema, {
34559
- kind: "query",
34560
- auth: "protected"
34561
- }), method(object({
34562
34320
  deviceIds: array(number()).min(1).max(200),
34563
34321
  fromMs: number(),
34564
34322
  toMs: number()
34565
34323
  }), array(RecordingAvailabilityForDeviceSchema).readonly(), {
34566
34324
  kind: "query",
34567
34325
  auth: "protected"
34568
- }), method(object({
34569
- deviceId: number(),
34570
- fromMs: number(),
34571
- toMs: number(),
34572
- tzOffsetMinutes: number()
34573
- }), RecordingDaysSchema, {
34574
- kind: "query",
34575
- auth: "protected"
34576
34326
  }), method(object({
34577
34327
  deviceIds: array(number()).min(1).max(200),
34578
34328
  fromMs: number(),
@@ -34581,13 +34331,6 @@ method(object({
34581
34331
  }), array(RecordingDaysForDeviceSchema).readonly(), {
34582
34332
  kind: "query",
34583
34333
  auth: "protected"
34584
- }), method(object({
34585
- deviceId: number(),
34586
- fromMs: number(),
34587
- toMs: number()
34588
- }), RecordingManifestSchema, {
34589
- kind: "query",
34590
- auth: "protected"
34591
34334
  }), method(object({}), RecordingStorageUsageSchema, {
34592
34335
  kind: "query",
34593
34336
  auth: "admin"
@@ -35039,6 +34782,511 @@ method(object({
35039
34782
  auth: "protected"
35040
34783
  });
35041
34784
  /**
34785
+ * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
34786
+ * writes to the CAMERA's own card, on the camera's own schedule.
34787
+ *
34788
+ * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
34789
+ * footage ledger, our storage locations, our retention. This one has a
34790
+ * different authority — the camera's firmware — and per D62 it stores
34791
+ * nothing of its own. Every value here is read from the camera and every
34792
+ * write goes back to the camera; there is no CamStack-side mirror that
34793
+ * could disagree with the device.
34794
+ *
34795
+ * ## One shape, two firmwares
34796
+ *
34797
+ * Measured 2026-09-22 against the live fleet:
34798
+ *
34799
+ * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
34800
+ * | --- | --- | --- |
34801
+ * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
34802
+ * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
34803
+ * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
34804
+ * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
34805
+ * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
34806
+ * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
34807
+ * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
34808
+ * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
34809
+ *
34810
+ * The two schedule models look different and are the same thing in
34811
+ * different coordinates: both answer "for this trigger, during which
34812
+ * weekly windows does the camera record". {@link RecordWindow} is that
34813
+ * question in one shape — Hikvision's ranges map straight onto it,
34814
+ * Reolink's mask expands into hour-aligned windows.
34815
+ *
34816
+ * ## Union, not intersection
34817
+ *
34818
+ * **The same fields exist on every camera.** What differs per device is
34819
+ * which VALUES that device accepts, and that is what {@link
34820
+ * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
34821
+ * per field plus the schedule's own limits. A control a camera cannot
34822
+ * honour is rendered DISABLED WITH ITS REASON, never missing and never
34823
+ * dead: disabled must not look like broken.
34824
+ *
34825
+ * ## Refusal by name
34826
+ *
34827
+ * A write a camera cannot honour is refused with a sentence the operator
34828
+ * can read — never accepted and dropped. Both providers refuse through
34829
+ * {@link describeOnboardRefusal}, so the vocabulary is one function and
34830
+ * one test, not two hand-written vendor opinions.
34831
+ *
34832
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
34833
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
34834
+ * injected from `status`) reports the live values, and a single
34835
+ * `setSettings` mutation applies a partial change. No hand-written
34836
+ * settings-contribution methods.
34837
+ */
34838
+ /**
34839
+ * What makes the camera start recording during a window.
34840
+ *
34841
+ * The union of both vendors' vocabularies. `continuous` is Hikvision's
34842
+ * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
34843
+ * object-class triggers are Reolink-only today and the smart-event ones
34844
+ * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
34845
+ * firmwares measured — a camera that cannot record on a trigger simply
34846
+ * does not list it in `options.schedule.triggers`, and a window naming
34847
+ * it is REFUSED, not dropped.
34848
+ */
34849
+ var RecordTriggerSchema = _enum([
34850
+ "continuous",
34851
+ "motion",
34852
+ "person",
34853
+ "vehicle",
34854
+ "animal",
34855
+ "lineCrossing",
34856
+ "intrusion",
34857
+ "loitering",
34858
+ "alarmInput"
34859
+ ]);
34860
+ /**
34861
+ * One weekly recording window: "on `day`, from `startMinute` to
34862
+ * `endMinute`, record on `trigger`".
34863
+ *
34864
+ * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
34865
+ * both firmwares enumerate). Minutes are local camera time since
34866
+ * midnight; `endMinute` may be 1440, meaning end of day — that is
34867
+ * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
34868
+ * collapsing it to 0 would turn a whole-day window into an empty one.
34869
+ */
34870
+ var RecordWindowSchema = object({
34871
+ trigger: RecordTriggerSchema,
34872
+ day: number().int().min(0).max(6),
34873
+ startMinute: number().int().min(0).max(1439),
34874
+ endMinute: number().int().min(1).max(1440)
34875
+ });
34876
+ /** Status of one physical volume, as the camera itself describes it. */
34877
+ var OnboardStorageVolumeSchema = object({
34878
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
34879
+ id: string(),
34880
+ /** The camera's own name for it, when it gives one (`hddName`). */
34881
+ label: string().optional(),
34882
+ status: _enum([
34883
+ "ok",
34884
+ "unformatted",
34885
+ "error",
34886
+ "offline",
34887
+ "unknown"
34888
+ ]),
34889
+ /**
34890
+ * Total size in MB, or **null when the camera did not say**.
34891
+ *
34892
+ * Never 0 for an unreadable value: a measurement that failed is not a
34893
+ * measurement (D393), and a card whose size is unknown must not be
34894
+ * rendered as a card of size zero.
34895
+ */
34896
+ capacityMb: number().nullable(),
34897
+ /**
34898
+ * Free space in MB, or null when unknown.
34899
+ *
34900
+ * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
34901
+ * 1439 both report exactly 11776 MB free — the fixed reserve a looping
34902
+ * card converges on once it has wrapped. At loop steady state the
34903
+ * number is identical whether the camera recorded yesterday or stopped
34904
+ * a month ago.
34905
+ */
34906
+ freeMb: number().nullable(),
34907
+ /** True when the camera reports the volume writable (`property` RW). */
34908
+ writable: boolean().optional()
34909
+ });
34910
+ /**
34911
+ * What the camera is doing with its own storage, right now.
34912
+ *
34913
+ * Every scalar is nullable and **null means the camera did not answer**,
34914
+ * never a default. A form that seeds `0` from an unanswered read invites
34915
+ * the operator to save that 0 back onto the camera.
34916
+ */
34917
+ var RecordingOnboardStatusSchema = object({
34918
+ storage: discriminatedUnion("kind", [
34919
+ object({
34920
+ kind: literal("present"),
34921
+ volumes: array(OnboardStorageVolumeSchema)
34922
+ }),
34923
+ object({
34924
+ kind: literal("absent"),
34925
+ reason: string()
34926
+ }),
34927
+ object({
34928
+ kind: literal("unknown"),
34929
+ reason: string()
34930
+ })
34931
+ ]),
34932
+ tracks: array(object({
34933
+ id: string(),
34934
+ enabled: boolean(),
34935
+ isVideo: boolean(),
34936
+ /** From the camera's own track description. Null when it does not say. */
34937
+ codec: string().nullable(),
34938
+ resolution: string().nullable(),
34939
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
34940
+ overwriteWhenFull: boolean().nullable()
34941
+ })),
34942
+ /**
34943
+ * The track the write path targets — the enabled VIDEO one. Null when
34944
+ * no track could be identified, which is itself a refusal reason.
34945
+ */
34946
+ primaryTrackId: string().nullable(),
34947
+ /** Master "record to the card at all" switch. */
34948
+ enabled: boolean().nullable(),
34949
+ overwriteWhenFull: boolean().nullable(),
34950
+ preRecordSec: number().nullable(),
34951
+ postRecordSec: number().nullable(),
34952
+ /** Length of one recorded file, in minutes. */
34953
+ segmentMinutes: number().nullable(),
34954
+ /** The primary track's weekly windows, flattened. */
34955
+ windows: array(RecordWindowSchema),
34956
+ /**
34957
+ * How many windows the camera described that CamStack could NOT read —
34958
+ * an unrecognised trigger, an unparseable clock, a weekday it does not
34959
+ * name.
34960
+ *
34961
+ * A dropped window is work the reader threw away, and a schedule that
34962
+ * silently shows fewer rows than the camera holds is how an operator
34963
+ * saves back a schedule shorter than the one they were looking at
34964
+ * (D391). Non-zero means the window list is INCOMPLETE and a write
34965
+ * that replaces it would delete what was not shown — which is why a
34966
+ * provider reporting a non-zero count also reports the schedule as not
34967
+ * writable.
34968
+ */
34969
+ unreadableWindows: number(),
34970
+ /**
34971
+ * The camera is scheduled to record and has NO usable storage.
34972
+ *
34973
+ * A first-class fact because it is the fleet's most common silent
34974
+ * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
34975
+ * to a card that is not there. Neither the schedule nor the storage
34976
+ * read says anything wrong on its own; only the pair does.
34977
+ */
34978
+ recordingToNowhere: boolean(),
34979
+ lastFetchedAt: number()
34980
+ });
34981
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
34982
+ var RangeSchema = object({
34983
+ min: number(),
34984
+ max: number(),
34985
+ step: number()
34986
+ });
34987
+ /**
34988
+ * The values a camera actually takes for a numeric field, when they are a SET
34989
+ * rather than a range.
34990
+ *
34991
+ * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
34992
+ * (I91DN) on 2026-09-22 by writing each value and reading it back:
34993
+ *
34994
+ * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
34995
+ * camera's "no limit" — `-1` and `4294967295` both land on it);
34996
+ * - post-record: `5, 10, 30, 60, 120, 300, 600`.
34997
+ *
34998
+ * Neither is expressible as a step: the first has a sentinel two billion away
34999
+ * from its neighbours, the second doubles and then jumps. A range that tried
35000
+ * would forbid values the camera takes AND permit values it silently replaces
35001
+ * with 5 — wrong in both directions at once.
35002
+ *
35003
+ * `sentinel` names the member that is not a duration, so a surface can render
35004
+ * "no limit" instead of `2147483647` seconds.
35005
+ */
35006
+ var AllowedValuesSchema = object({
35007
+ values: array(number()).min(1),
35008
+ sentinel: object({
35009
+ value: number(),
35010
+ meaning: _enum(["no-limit", "disabled"])
35011
+ }).optional()
35012
+ });
35013
+ /**
35014
+ * Per-field availability on ONE camera.
35015
+ *
35016
+ * The field exists on every camera — this says whether this one can be
35017
+ * read and whether it can be written, and `reason` says why not when
35018
+ * either is false. The UI renders the control DISABLED with the reason
35019
+ * rather than hiding it, so a limitation is legible instead of looking
35020
+ * like a missing feature.
35021
+ */
35022
+ var OnboardFieldSupportSchema = object({
35023
+ readable: boolean(),
35024
+ writable: boolean(),
35025
+ /** Required whenever `readable` or `writable` is false. */
35026
+ reason: string().optional()
35027
+ });
35028
+ /** What this camera's schedule model can express. */
35029
+ var OnboardScheduleSupportSchema = object({
35030
+ support: OnboardFieldSupportSchema,
35031
+ /**
35032
+ * The smallest time step the camera can express, in minutes.
35033
+ *
35034
+ * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
35035
+ * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
35036
+ * window whose edges are not a multiple of this is REFUSED rather than
35037
+ * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
35038
+ * and nothing says so.
35039
+ */
35040
+ granularityMinutes: number(),
35041
+ /** Triggers this camera can record on. A window naming another is refused. */
35042
+ triggers: array(RecordTriggerSchema),
35043
+ /**
35044
+ * False when the camera stores ONE trigger per time range, so two
35045
+ * windows overlapping on the same day cannot carry different triggers.
35046
+ * True on Reolink, whose mask is per-trigger and independent.
35047
+ */
35048
+ supportsOverlappingTriggers: boolean()
35049
+ });
35050
+ var RecordingOnboardOptionsSchema = object({
35051
+ enabled: OnboardFieldSupportSchema,
35052
+ overwriteWhenFull: OnboardFieldSupportSchema,
35053
+ preRecordSec: OnboardFieldSupportSchema,
35054
+ preRecordSecRange: RangeSchema.optional(),
35055
+ /** Preferred over the range when the camera takes a SET, not a span. */
35056
+ preRecordSecAllowed: AllowedValuesSchema.optional(),
35057
+ postRecordSec: OnboardFieldSupportSchema,
35058
+ postRecordSecRange: RangeSchema.optional(),
35059
+ /** Preferred over the range when the camera takes a SET, not a span. */
35060
+ postRecordSecAllowed: AllowedValuesSchema.optional(),
35061
+ segmentMinutes: OnboardFieldSupportSchema,
35062
+ segmentMinutesRange: RangeSchema.optional(),
35063
+ /** Preferred over the range when the camera takes a SET, not a span. */
35064
+ segmentMinutesAllowed: AllowedValuesSchema.optional(),
35065
+ schedule: OnboardScheduleSupportSchema
35066
+ });
35067
+ /**
35068
+ * A partial change. Every field optional.
35069
+ *
35070
+ * Unlike the other `deviceConfig` caps, a provider here does **NOT**
35071
+ * silently ignore a field it cannot support — it refuses, by name,
35072
+ * through {@link describeOnboardRefusal}. Silence on a recording setting
35073
+ * is the failure D62 exists to prevent: the operator believes the camera
35074
+ * is recording the way the form says, and it is not.
35075
+ */
35076
+ var RecordingOnboardPatchSchema = object({
35077
+ enabled: boolean().optional(),
35078
+ overwriteWhenFull: boolean().optional(),
35079
+ preRecordSec: number().optional(),
35080
+ postRecordSec: number().optional(),
35081
+ segmentMinutes: number().optional(),
35082
+ /** The complete new window set for the primary track — not a delta. */
35083
+ windows: array(RecordWindowSchema).optional()
35084
+ });
35085
+ var recordingOnboardCapability = {
35086
+ name: "recording-onboard",
35087
+ scope: "device",
35088
+ deviceNative: true,
35089
+ mode: "singleton",
35090
+ deviceTypes: [DeviceType.Camera],
35091
+ deviceConfig: { ui: {
35092
+ kind: "derived-form",
35093
+ builderId: "recording-onboard",
35094
+ tab: "recording"
35095
+ } },
35096
+ methods: {
35097
+ getOptions: method(object({ deviceId: number() }), RecordingOnboardOptionsSchema),
35098
+ setSettings: method(object({
35099
+ deviceId: number(),
35100
+ settings: RecordingOnboardPatchSchema
35101
+ }), _void(), {
35102
+ kind: "mutation",
35103
+ auth: "admin"
35104
+ })
35105
+ },
35106
+ status: {
35107
+ schema: RecordingOnboardStatusSchema,
35108
+ kind: "poll"
35109
+ },
35110
+ runtimeState: RecordingOnboardStatusSchema,
35111
+ /**
35112
+ * Runtime-state durability: **restored** — operator-set camera-side
35113
+ * recording config; mutation-driven, and the storage half is the last
35114
+ * thing the camera said about its own card.
35115
+ *
35116
+ * See `RuntimeStateDurability`. Enforced by
35117
+ * `scripts/check-runtime-state-durability.ts`.
35118
+ */
35119
+ durability: "restored",
35120
+ /** Clock fields: written, but excluded from the compare that decides
35121
+ * whether persisting is worth a SQLite commit. */
35122
+ volatileStateFields: ["lastFetchedAt"]
35123
+ };
35124
+ /**
35125
+ * Day-of-week names in the cap's index order (0 = Monday), for messages
35126
+ * an operator reads and for Hikvision's `<DayOfWeek>` element.
35127
+ */
35128
+ var DAY_NAMES = [
35129
+ "Monday",
35130
+ "Tuesday",
35131
+ "Wednesday",
35132
+ "Thursday",
35133
+ "Friday",
35134
+ "Saturday",
35135
+ "Sunday"
35136
+ ];
35137
+ /**
35138
+ * Does `patch` ask this camera for something it cannot do?
35139
+ *
35140
+ * Returns the operator-readable reason, or `null` when every field in
35141
+ * the patch is within what `options` says this camera accepts. A
35142
+ * provider calls this BEFORE touching the camera and throws the string:
35143
+ * refusing is the point, and refusing identically on both vendors is why
35144
+ * this is one function.
35145
+ *
35146
+ * The order of checks is the order an operator would read them: the
35147
+ * scalar knobs first, then the schedule, because a schedule complaint is
35148
+ * longer and a scalar one is usually the real problem.
35149
+ */
35150
+ function describeOnboardRefusal(options, patch) {
35151
+ if (patch.enabled !== void 0 && !options.enabled.writable) return unwritable("the master recording switch", options.enabled.reason);
35152
+ if (patch.overwriteWhenFull !== void 0 && !options.overwriteWhenFull.writable) return unwritable("overwrite-when-full", options.overwriteWhenFull.reason);
35153
+ const preRefusal = refuseNumeric("pre-record seconds", patch.preRecordSec, options.preRecordSec, options.preRecordSecRange, options.preRecordSecAllowed);
35154
+ if (preRefusal) return preRefusal;
35155
+ const postRefusal = refuseNumeric("post-record seconds", patch.postRecordSec, options.postRecordSec, options.postRecordSecRange, options.postRecordSecAllowed);
35156
+ if (postRefusal) return postRefusal;
35157
+ const segmentRefusal = refuseNumeric("segment length (minutes)", patch.segmentMinutes, options.segmentMinutes, options.segmentMinutesRange, options.segmentMinutesAllowed);
35158
+ if (segmentRefusal) return segmentRefusal;
35159
+ if (patch.windows !== void 0) {
35160
+ const scheduleRefusal = refuseSchedule(options, patch.windows);
35161
+ if (scheduleRefusal) return scheduleRefusal;
35162
+ }
35163
+ return null;
35164
+ }
35165
+ function unwritable(what, reason) {
35166
+ return reason ? `this camera cannot change ${what}: ${reason}` : `this camera cannot change ${what}`;
35167
+ }
35168
+ function refuseNumeric(what, value, support, range, allowed) {
35169
+ if (value === void 0) return null;
35170
+ if (!support.writable) return unwritable(what, support.reason);
35171
+ if (!Number.isFinite(value)) return `${what} must be a number, got ${String(value)}`;
35172
+ if (allowed) {
35173
+ if (allowed.values.includes(value)) return null;
35174
+ const say = (v) => allowed.sentinel !== void 0 && v === allowed.sentinel.value ? allowed.sentinel.meaning === "no-limit" ? "no limit" : "off" : String(v);
35175
+ return `this camera accepts ${what} only as ${allowed.values.map(say).join(", ")}; ${say(value)} is not one of them`;
35176
+ }
35177
+ if (!range) return null;
35178
+ if (value < range.min || value > range.max) return `this camera accepts ${what} between ${String(range.min)} and ${String(range.max)}; ${String(value)} is outside that`;
35179
+ if (range.step > 0 && (value - range.min) % range.step !== 0) return `this camera accepts ${what} in steps of ${String(range.step)} from ${String(range.min)}; ${String(value)} is not one of them`;
35180
+ return null;
35181
+ }
35182
+ function refuseSchedule(options, windows) {
35183
+ const schedule = options.schedule;
35184
+ if (!schedule.support.writable) return unwritable("the recording schedule", schedule.support.reason);
35185
+ const allowed = new Set(schedule.triggers);
35186
+ for (const window of windows) {
35187
+ if (!allowed.has(window.trigger)) {
35188
+ const offer = schedule.triggers.length > 0 ? schedule.triggers.join(", ") : "none";
35189
+ return `this camera cannot record on "${window.trigger}"; it records on: ${offer}`;
35190
+ }
35191
+ if (window.endMinute <= window.startMinute) return `a window on ${dayName(window.day)} ends at or before it starts (${String(window.startMinute)} → ${String(window.endMinute)})`;
35192
+ const step = schedule.granularityMinutes;
35193
+ if (step > 1 && (window.startMinute % step !== 0 || window.endMinute % step !== 0)) return `this camera's schedule only moves in ${String(step)}-minute steps; the ${dayName(window.day)} window ${formatMinutes(window.startMinute)}–${formatMinutes(window.endMinute)} is not aligned to them`;
35194
+ }
35195
+ if (!schedule.supportsOverlappingTriggers) {
35196
+ const clash = findOverlapWithDifferentTrigger(windows);
35197
+ if (clash) return `this camera stores one trigger per time range, so "${clash.a.trigger}" and "${clash.b.trigger}" cannot both cover ${dayName(clash.a.day)} ${formatMinutes(Math.max(clash.a.startMinute, clash.b.startMinute))}–${formatMinutes(Math.min(clash.a.endMinute, clash.b.endMinute))}`;
35198
+ }
35199
+ return null;
35200
+ }
35201
+ function findOverlapWithDifferentTrigger(windows) {
35202
+ for (let i = 0; i < windows.length; i += 1) for (let j = i + 1; j < windows.length; j += 1) {
35203
+ const a = windows[i];
35204
+ const b = windows[j];
35205
+ if (!a || !b) continue;
35206
+ if (a.day !== b.day) continue;
35207
+ if (a.trigger === b.trigger) continue;
35208
+ if (a.startMinute < b.endMinute && b.startMinute < a.endMinute) return {
35209
+ a,
35210
+ b
35211
+ };
35212
+ }
35213
+ return null;
35214
+ }
35215
+ function dayName(day) {
35216
+ return DAY_NAMES[day] ?? `day ${String(day)}`;
35217
+ }
35218
+ /** `510` → `08:30`. For messages, not for the wire. */
35219
+ function formatMinutes(minute) {
35220
+ const hour = Math.floor(minute / 60);
35221
+ const rest = minute % 60;
35222
+ return `${String(hour).padStart(2, "0")}:${String(rest).padStart(2, "0")}`;
35223
+ }
35224
+ /**
35225
+ * Index of the mask character covering `day` at `hour`.
35226
+ *
35227
+ * **Layout: day-major, Monday first** — `day * 24 + hour`.
35228
+ *
35229
+ * This is an ASSUMPTION, and it is recorded as one because the fleet
35230
+ * cannot currently confirm it: every `valueTable` on every Reolink
35231
+ * measured on 2026-09-22 is uniform (all `1` or all `0`), and a uniform
35232
+ * mask reads identically under either layout. To settle it, set a single
35233
+ * hour on one day from the Reolink app and re-read `getRecordSchedule`.
35234
+ * Until then, a NON-uniform mask is reported as windows under this
35235
+ * layout and the day labels may be transposed; a uniform one is exact.
35236
+ */
35237
+ function weeklyMaskIndex(day, hour) {
35238
+ return day * 24 + hour;
35239
+ }
35240
+ /**
35241
+ * Expand one trigger's weekly hour mask into windows, merging adjacent
35242
+ * set hours into one window per run so a full day is ONE window rather
35243
+ * than twenty-four.
35244
+ *
35245
+ * A mask of the wrong length yields no windows: a truncated table is not
35246
+ * a schedule of fewer days, it is an answer we did not understand.
35247
+ */
35248
+ function weeklyMaskToWindows(mask, trigger) {
35249
+ if (mask.length !== 168) return [];
35250
+ const out = [];
35251
+ for (let day = 0; day < 7; day += 1) {
35252
+ let runStart = null;
35253
+ for (let hour = 0; hour <= 24; hour += 1) {
35254
+ const set = hour < 24 && mask[weeklyMaskIndex(day, hour)] === "1";
35255
+ if (set && runStart === null) runStart = hour;
35256
+ else if (!set && runStart !== null) {
35257
+ out.push({
35258
+ trigger,
35259
+ day,
35260
+ startMinute: runStart * 60,
35261
+ endMinute: hour * 60
35262
+ });
35263
+ runStart = null;
35264
+ }
35265
+ }
35266
+ }
35267
+ return out;
35268
+ }
35269
+ /**
35270
+ * Collapse this trigger's windows back into a weekly hour mask.
35271
+ *
35272
+ * Windows for other triggers are ignored — the mask is per trigger. A
35273
+ * window that is not hour-aligned is REFUSED upstream by
35274
+ * {@link describeOnboardRefusal}; this function rounds nothing, it
35275
+ * simply sets every hour the window fully or partly covers, so a caller
35276
+ * that skipped the refusal cannot silently lose coverage.
35277
+ */
35278
+ function windowsToWeeklyMask(windows, trigger) {
35279
+ const slots = new Array(168).fill("0");
35280
+ for (const window of windows) {
35281
+ if (window.trigger !== trigger) continue;
35282
+ if (window.day < 0 || window.day >= 7) continue;
35283
+ const firstHour = Math.floor(window.startMinute / 60);
35284
+ const lastHour = Math.ceil(window.endMinute / 60);
35285
+ for (let hour = firstHour; hour < lastHour && hour < 24; hour += 1) slots[weeklyMaskIndex(window.day, hour)] = "1";
35286
+ }
35287
+ return slots.join("");
35288
+ }
35289
+ /**
35042
35290
  * A camera's own "record me NOW" LEVEL — a signal the device raises while
35043
35291
  * something it knows about is happening (a robot vacuum cleaning, a machine
35044
35292
  * running, a gate open) and lowers when it stops.
@@ -42875,24 +43123,6 @@ Object.freeze({
42875
43123
  addonId: null,
42876
43124
  access: "view"
42877
43125
  },
42878
- "events.getEventClipUrl": {
42879
- capName: "events",
42880
- capScope: "device",
42881
- addonId: null,
42882
- access: "view"
42883
- },
42884
- "events.getEvents": {
42885
- capName: "events",
42886
- capScope: "device",
42887
- addonId: null,
42888
- access: "view"
42889
- },
42890
- "events.getEventThumbnail": {
42891
- capName: "events",
42892
- capScope: "device",
42893
- addonId: null,
42894
- access: "view"
42895
- },
42896
43126
  "faceGallery.assignFace": {
42897
43127
  capName: "face-gallery",
42898
43128
  capScope: "system",
@@ -45767,224 +45997,236 @@ Object.freeze({
45767
45997
  addonId: null,
45768
45998
  access: "create"
45769
45999
  },
45770
- "recording.applyDeviceSettingsPatch": {
46000
+ "recording.getAvailability": {
45771
46001
  capName: "recording",
45772
- capScope: "system",
46002
+ capScope: "device",
45773
46003
  addonId: null,
45774
- access: "create"
46004
+ access: "view"
45775
46005
  },
45776
- "recording.cancelRelocateJob": {
46006
+ "recording.getDaysWithRecordings": {
45777
46007
  capName: "recording",
45778
- capScope: "system",
46008
+ capScope: "device",
45779
46009
  addonId: null,
45780
- access: "create"
46010
+ access: "view"
45781
46011
  },
45782
- "recording.cancelStorageMigrationMove": {
46012
+ "recording.getPlayback": {
45783
46013
  capName: "recording",
45784
- capScope: "system",
46014
+ capScope: "device",
45785
46015
  addonId: null,
45786
- access: "create"
46016
+ access: "view"
45787
46017
  },
45788
- "recording.deleteFootprint": {
46018
+ "recording.getPlaybackOptions": {
45789
46019
  capName: "recording",
45790
- capScope: "system",
46020
+ capScope: "device",
45791
46021
  addonId: null,
45792
- access: "delete"
46022
+ access: "view"
45793
46023
  },
45794
- "recording.getAvailability": {
46024
+ "recording.listSources": {
45795
46025
  capName: "recording",
45796
- capScope: "system",
46026
+ capScope: "device",
45797
46027
  addonId: null,
45798
46028
  access: "view"
45799
46029
  },
45800
- "recording.getAvailabilityBatch": {
45801
- capName: "recording",
46030
+ "recordingArchive.applyDeviceSettingsPatch": {
46031
+ capName: "recording-archive",
45802
46032
  capScope: "system",
45803
46033
  addonId: null,
45804
- access: "view"
46034
+ access: "create"
45805
46035
  },
45806
- "recording.getDaysWithRecordings": {
45807
- capName: "recording",
46036
+ "recordingArchive.cancelRelocateJob": {
46037
+ capName: "recording-archive",
45808
46038
  capScope: "system",
45809
46039
  addonId: null,
45810
- access: "view"
46040
+ access: "create"
45811
46041
  },
45812
- "recording.getDaysWithRecordingsBatch": {
45813
- capName: "recording",
46042
+ "recordingArchive.cancelStorageMigrationMove": {
46043
+ capName: "recording-archive",
46044
+ capScope: "system",
46045
+ addonId: null,
46046
+ access: "create"
46047
+ },
46048
+ "recordingArchive.deleteFootprint": {
46049
+ capName: "recording-archive",
46050
+ capScope: "system",
46051
+ addonId: null,
46052
+ access: "delete"
46053
+ },
46054
+ "recordingArchive.getAvailabilityBatch": {
46055
+ capName: "recording-archive",
45814
46056
  capScope: "system",
45815
46057
  addonId: null,
45816
46058
  access: "view"
45817
46059
  },
45818
- "recording.getDeviceConfig": {
45819
- capName: "recording",
46060
+ "recordingArchive.getDaysWithRecordingsBatch": {
46061
+ capName: "recording-archive",
45820
46062
  capScope: "system",
45821
46063
  addonId: null,
45822
46064
  access: "view"
45823
46065
  },
45824
- "recording.getDeviceLiveContribution": {
45825
- capName: "recording",
46066
+ "recordingArchive.getDeviceConfig": {
46067
+ capName: "recording-archive",
45826
46068
  capScope: "system",
45827
46069
  addonId: null,
45828
46070
  access: "view"
45829
46071
  },
45830
- "recording.getDeviceSettingsContribution": {
45831
- capName: "recording",
46072
+ "recordingArchive.getDeviceLiveContribution": {
46073
+ capName: "recording-archive",
45832
46074
  capScope: "system",
45833
46075
  addonId: null,
45834
46076
  access: "view"
45835
46077
  },
45836
- "recording.getPlacement": {
45837
- capName: "recording",
46078
+ "recordingArchive.getDeviceSettingsContribution": {
46079
+ capName: "recording-archive",
45838
46080
  capScope: "system",
45839
46081
  addonId: null,
45840
46082
  access: "view"
45841
46083
  },
45842
- "recording.getPlaybackManifest": {
45843
- capName: "recording",
46084
+ "recordingArchive.getPlacement": {
46085
+ capName: "recording-archive",
45844
46086
  capScope: "system",
45845
46087
  addonId: null,
45846
46088
  access: "view"
45847
46089
  },
45848
- "recording.getRelocateResidue": {
45849
- capName: "recording",
46090
+ "recordingArchive.getRelocateResidue": {
46091
+ capName: "recording-archive",
45850
46092
  capScope: "system",
45851
46093
  addonId: null,
45852
46094
  access: "view"
45853
46095
  },
45854
- "recording.getStatus": {
45855
- capName: "recording",
46096
+ "recordingArchive.getStatus": {
46097
+ capName: "recording-archive",
45856
46098
  capScope: "system",
45857
46099
  addonId: null,
45858
46100
  access: "view"
45859
46101
  },
45860
- "recording.getStorageMigrationMoveStatus": {
45861
- capName: "recording",
46102
+ "recordingArchive.getStorageMigrationMoveStatus": {
46103
+ capName: "recording-archive",
45862
46104
  capScope: "system",
45863
46105
  addonId: null,
45864
46106
  access: "view"
45865
46107
  },
45866
- "recording.getStorageUsage": {
45867
- capName: "recording",
46108
+ "recordingArchive.getStorageUsage": {
46109
+ capName: "recording-archive",
45868
46110
  capScope: "system",
45869
46111
  addonId: null,
45870
46112
  access: "view"
45871
46113
  },
45872
- "recording.listOpsLog": {
45873
- capName: "recording",
46114
+ "recordingArchive.listOpsLog": {
46115
+ capName: "recording-archive",
45874
46116
  capScope: "system",
45875
46117
  addonId: null,
45876
46118
  access: "view"
45877
46119
  },
45878
- "recording.listRelocateJobs": {
45879
- capName: "recording",
46120
+ "recordingArchive.listRelocateJobs": {
46121
+ capName: "recording-archive",
45880
46122
  capScope: "system",
45881
46123
  addonId: null,
45882
46124
  access: "view"
45883
46125
  },
45884
- "recording.locateSegment": {
45885
- capName: "recording",
46126
+ "recordingArchive.locateSegment": {
46127
+ capName: "recording-archive",
45886
46128
  capScope: "system",
45887
46129
  addonId: null,
45888
46130
  access: "view"
45889
46131
  },
45890
- "recording.pauseForStorageMigration": {
45891
- capName: "recording",
46132
+ "recordingArchive.pauseForStorageMigration": {
46133
+ capName: "recording-archive",
45892
46134
  capScope: "system",
45893
46135
  addonId: null,
45894
46136
  access: "create"
45895
46137
  },
45896
- "recording.planStorageRebalance": {
45897
- capName: "recording",
46138
+ "recordingArchive.planStorageRebalance": {
46139
+ capName: "recording-archive",
45898
46140
  capScope: "system",
45899
46141
  addonId: null,
45900
46142
  access: "view"
45901
46143
  },
45902
- "recording.pruneFootage": {
45903
- capName: "recording",
46144
+ "recordingArchive.pruneFootage": {
46145
+ capName: "recording-archive",
45904
46146
  capScope: "system",
45905
46147
  addonId: null,
45906
46148
  access: "create"
45907
46149
  },
45908
- "recording.readGopBytes": {
45909
- capName: "recording",
46150
+ "recordingArchive.readGopBytes": {
46151
+ capName: "recording-archive",
45910
46152
  capScope: "system",
45911
46153
  addonId: null,
45912
46154
  access: "view"
45913
46155
  },
45914
- "recording.readSegmentBytes": {
45915
- capName: "recording",
46156
+ "recordingArchive.readSegmentBytes": {
46157
+ capName: "recording-archive",
45916
46158
  capScope: "system",
45917
46159
  addonId: null,
45918
46160
  access: "view"
45919
46161
  },
45920
- "recording.readWindowBytes": {
45921
- capName: "recording",
46162
+ "recordingArchive.readWindowBytes": {
46163
+ capName: "recording-archive",
45922
46164
  capScope: "system",
45923
46165
  addonId: null,
45924
46166
  access: "view"
45925
46167
  },
45926
- "recording.reconcileLedgerAgainstDisk": {
45927
- capName: "recording",
46168
+ "recordingArchive.reconcileLedgerAgainstDisk": {
46169
+ capName: "recording-archive",
45928
46170
  capScope: "system",
45929
46171
  addonId: null,
45930
46172
  access: "create"
45931
46173
  },
45932
- "recording.refreshStorageLocationsForMigration": {
45933
- capName: "recording",
46174
+ "recordingArchive.refreshStorageLocationsForMigration": {
46175
+ capName: "recording-archive",
45934
46176
  capScope: "system",
45935
46177
  addonId: null,
45936
46178
  access: "create"
45937
46179
  },
45938
- "recording.relocateFootage": {
45939
- capName: "recording",
46180
+ "recordingArchive.relocateFootage": {
46181
+ capName: "recording-archive",
45940
46182
  capScope: "system",
45941
46183
  addonId: null,
45942
46184
  access: "create"
45943
46185
  },
45944
- "recording.renderClip": {
45945
- capName: "recording",
46186
+ "recordingArchive.renderClip": {
46187
+ capName: "recording-archive",
45946
46188
  capScope: "system",
45947
46189
  addonId: null,
45948
46190
  access: "create"
45949
46191
  },
45950
- "recording.renderGif": {
45951
- capName: "recording",
46192
+ "recordingArchive.renderGif": {
46193
+ capName: "recording-archive",
45952
46194
  capScope: "system",
45953
46195
  addonId: null,
45954
46196
  access: "create"
45955
46197
  },
45956
- "recording.rescanStorage": {
45957
- capName: "recording",
46198
+ "recordingArchive.rescanStorage": {
46199
+ capName: "recording-archive",
45958
46200
  capScope: "system",
45959
46201
  addonId: null,
45960
46202
  access: "create"
45961
46203
  },
45962
- "recording.resumeForStorageMigration": {
45963
- capName: "recording",
46204
+ "recordingArchive.resumeForStorageMigration": {
46205
+ capName: "recording-archive",
45964
46206
  capScope: "system",
45965
46207
  addonId: null,
45966
46208
  access: "create"
45967
46209
  },
45968
- "recording.setDeviceConfig": {
45969
- capName: "recording",
46210
+ "recordingArchive.setDeviceConfig": {
46211
+ capName: "recording-archive",
45970
46212
  capScope: "system",
45971
46213
  addonId: null,
45972
46214
  access: "create"
45973
46215
  },
45974
- "recording.setDevicePlacement": {
45975
- capName: "recording",
46216
+ "recordingArchive.setDevicePlacement": {
46217
+ capName: "recording-archive",
45976
46218
  capScope: "system",
45977
46219
  addonId: null,
45978
46220
  access: "create"
45979
46221
  },
45980
- "recording.startStorageMigrationMove": {
45981
- capName: "recording",
46222
+ "recordingArchive.startStorageMigrationMove": {
46223
+ capName: "recording-archive",
45982
46224
  capScope: "system",
45983
46225
  addonId: null,
45984
46226
  access: "create"
45985
46227
  },
45986
- "recording.startStorageRebalance": {
45987
- capName: "recording",
46228
+ "recordingArchive.startStorageRebalance": {
46229
+ capName: "recording-archive",
45988
46230
  capScope: "system",
45989
46231
  addonId: null,
45990
46232
  access: "create"
@@ -48289,21 +48531,6 @@ Object.freeze({
48289
48531
  form: "single",
48290
48532
  optional: false
48291
48533
  }],
48292
- "events.getEventClipUrl": [{
48293
- name: "deviceId",
48294
- form: "single",
48295
- optional: false
48296
- }],
48297
- "events.getEvents": [{
48298
- name: "deviceId",
48299
- form: "single",
48300
- optional: false
48301
- }],
48302
- "events.getEventThumbnail": [{
48303
- name: "deviceId",
48304
- form: "single",
48305
- optional: false
48306
- }],
48307
48534
  "faceGallery.getFaceByTrack": [{
48308
48535
  name: "deviceId",
48309
48536
  form: "single",
@@ -49222,107 +49449,117 @@ Object.freeze({
49222
49449
  form: "single",
49223
49450
  optional: false
49224
49451
  }],
49225
- "recording.deleteFootprint": [{
49452
+ "recording.getAvailability": [{
49226
49453
  name: "deviceId",
49227
49454
  form: "single",
49228
49455
  optional: false
49229
49456
  }],
49230
- "recording.getAvailability": [{
49457
+ "recording.getDaysWithRecordings": [{
49231
49458
  name: "deviceId",
49232
49459
  form: "single",
49233
49460
  optional: false
49234
49461
  }],
49235
- "recording.getAvailabilityBatch": [{
49236
- name: "deviceIds",
49237
- form: "array",
49462
+ "recording.getPlayback": [{
49463
+ name: "deviceId",
49464
+ form: "single",
49238
49465
  optional: false
49239
49466
  }],
49240
- "recording.getDaysWithRecordings": [{
49467
+ "recording.getPlaybackOptions": [{
49241
49468
  name: "deviceId",
49242
49469
  form: "single",
49243
49470
  optional: false
49244
49471
  }],
49245
- "recording.getDaysWithRecordingsBatch": [{
49246
- name: "deviceIds",
49247
- form: "array",
49472
+ "recording.listSources": [{
49473
+ name: "deviceId",
49474
+ form: "single",
49248
49475
  optional: false
49249
49476
  }],
49250
- "recording.getDeviceConfig": [{
49477
+ "recordingArchive.deleteFootprint": [{
49251
49478
  name: "deviceId",
49252
49479
  form: "single",
49253
49480
  optional: false
49254
49481
  }],
49255
- "recording.getPlaybackManifest": [{
49482
+ "recordingArchive.getAvailabilityBatch": [{
49483
+ name: "deviceIds",
49484
+ form: "array",
49485
+ optional: false
49486
+ }],
49487
+ "recordingArchive.getDaysWithRecordingsBatch": [{
49488
+ name: "deviceIds",
49489
+ form: "array",
49490
+ optional: false
49491
+ }],
49492
+ "recordingArchive.getDeviceConfig": [{
49256
49493
  name: "deviceId",
49257
49494
  form: "single",
49258
49495
  optional: false
49259
49496
  }],
49260
- "recording.listOpsLog": [{
49497
+ "recordingArchive.listOpsLog": [{
49261
49498
  name: "deviceId",
49262
49499
  form: "single",
49263
49500
  optional: true
49264
49501
  }],
49265
- "recording.locateSegment": [{
49502
+ "recordingArchive.locateSegment": [{
49266
49503
  name: "deviceId",
49267
49504
  form: "single",
49268
49505
  optional: false
49269
49506
  }],
49270
- "recording.pruneFootage": [{
49507
+ "recordingArchive.pruneFootage": [{
49271
49508
  name: "deviceId",
49272
49509
  form: "single",
49273
49510
  optional: false
49274
49511
  }],
49275
- "recording.readGopBytes": [{
49512
+ "recordingArchive.readGopBytes": [{
49276
49513
  name: "deviceId",
49277
49514
  form: "single",
49278
49515
  optional: false
49279
49516
  }],
49280
- "recording.readSegmentBytes": [{
49517
+ "recordingArchive.readSegmentBytes": [{
49281
49518
  name: "deviceId",
49282
49519
  form: "single",
49283
49520
  optional: false
49284
49521
  }],
49285
- "recording.readWindowBytes": [{
49522
+ "recordingArchive.readWindowBytes": [{
49286
49523
  name: "deviceId",
49287
49524
  form: "single",
49288
49525
  optional: false
49289
49526
  }],
49290
- "recording.reconcileLedgerAgainstDisk": [{
49527
+ "recordingArchive.reconcileLedgerAgainstDisk": [{
49291
49528
  name: "deviceId",
49292
49529
  form: "single",
49293
49530
  optional: true
49294
49531
  }],
49295
- "recording.relocateFootage": [{
49532
+ "recordingArchive.relocateFootage": [{
49296
49533
  name: "deviceId",
49297
49534
  form: "single",
49298
49535
  optional: true
49299
49536
  }],
49300
- "recording.renderClip": [{
49537
+ "recordingArchive.renderClip": [{
49301
49538
  name: "deviceId",
49302
49539
  form: "single",
49303
49540
  optional: false
49304
49541
  }],
49305
- "recording.renderGif": [{
49542
+ "recordingArchive.renderGif": [{
49306
49543
  name: "deviceId",
49307
49544
  form: "single",
49308
49545
  optional: false
49309
49546
  }],
49310
- "recording.rescanStorage": [{
49547
+ "recordingArchive.rescanStorage": [{
49311
49548
  name: "deviceId",
49312
49549
  form: "single",
49313
49550
  optional: false
49314
49551
  }],
49315
- "recording.setDeviceConfig": [{
49552
+ "recordingArchive.setDeviceConfig": [{
49316
49553
  name: "deviceId",
49317
49554
  form: "single",
49318
49555
  optional: false
49319
49556
  }],
49320
- "recording.setDevicePlacement": [{
49557
+ "recordingArchive.setDevicePlacement": [{
49321
49558
  name: "deviceId",
49322
49559
  form: "single",
49323
49560
  optional: false
49324
49561
  }],
49325
- "recording.startStorageMigrationMove": [{
49562
+ "recordingArchive.startStorageMigrationMove": [{
49326
49563
  name: "deviceId",
49327
49564
  form: "single",
49328
49565
  optional: true
@@ -72916,7 +73153,7 @@ var require_yazl = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
72916
73153
  }
72917
73154
  }));
72918
73155
  //#endregion
72919
- //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-E2N7WTCC.js
73156
+ //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-AQQAL7CF.js
72920
73157
  var import_undici = require_undici();
72921
73158
  var import_yazl = /* @__PURE__ */ require_chunk.__toESM(require_yazl(), 1);
72922
73159
  var formatterCache = /* @__PURE__ */ new Map();
@@ -74262,13 +74499,13 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74262
74499
  sec: 59
74263
74500
  };
74264
74501
  }
74265
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Date conversion: start=${start.toISOString()} -> ReolinkTime=${JSON.stringify(startTime)}, end=${end.toISOString()} -> ReolinkTime=${JSON.stringify(endTime)}`);
74502
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Date conversion: start=${start.toISOString()} -> ReolinkTime=${JSON.stringify(startTime)}, end=${end.toISOString()} -> ReolinkTime=${JSON.stringify(endTime)}`);
74266
74503
  if (!bypassCache) {
74267
74504
  this.cleanRecordingsCache();
74268
74505
  const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
74269
74506
  const cached = this.recordingsCache.get(cacheKey);
74270
74507
  if (cached && cached.expiresAt > Date.now()) {
74271
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Cache hit: returning ${cached.data.length} cached enriched recordings`);
74508
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Cache hit: returning ${cached.data.length} cached enriched recordings`);
74272
74509
  return cached.data;
74273
74510
  }
74274
74511
  }
@@ -74320,7 +74557,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74320
74557
  const firstResult = dayResponse[0];
74321
74558
  if (firstResult && firstResult.code === 0 && firstResult.value?.SearchResult?.File) {
74322
74559
  const files = firstResult.value.SearchResult.File;
74323
- if (files.length > 0 && files[0]) require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response for day ${day}/${month}/${year}: ${JSON.stringify({
74560
+ if (files.length > 0 && files[0]) require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response for day ${day}/${month}/${year}: ${JSON.stringify({
74324
74561
  fileCount: files.length,
74325
74562
  sampleFile: {
74326
74563
  name: files[0].name,
@@ -74356,7 +74593,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74356
74593
  } catch (e) {}
74357
74594
  enriched2.push(this.enrichVodFile(vodFile, channel, streamUrl));
74358
74595
  }
74359
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched2.length} enriched recording files from autoSearchByDay`);
74596
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched2.length} enriched recording files from autoSearchByDay`);
74360
74597
  if (!bypassCache) {
74361
74598
  const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
74362
74599
  this.recordingsCache.set(cacheKey, {
@@ -74384,7 +74621,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74384
74621
  const response = await this.call("Search", param, 0);
74385
74622
  allResults.push(...response);
74386
74623
  } else {
74387
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Range spans ${dayRanges.length} days, splitting into separate day queries`);
74624
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Range spans ${dayRanges.length} days, splitting into separate day queries`);
74388
74625
  for (const range of dayRanges) {
74389
74626
  const param = { Search: {
74390
74627
  channel,
@@ -74394,7 +74631,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74394
74631
  EndTime: range.end
74395
74632
  } };
74396
74633
  if (iLogicChannel > 0) param.Search.iLogicChannel = iLogicChannel;
74397
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Querying day: ${range.start.year}-${range.start.mon}-${range.start.day} (${range.start.hour}:${range.start.min}:${range.start.sec} to ${range.end.hour}:${range.end.min}:${range.end.sec})`);
74634
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Querying day: ${range.start.year}-${range.start.mon}-${range.start.day} (${range.start.hour}:${range.start.min}:${range.start.sec} to ${range.end.hour}:${range.end.min}:${range.end.sec})`);
74398
74635
  const response = await this.call("Search", param, 0);
74399
74636
  allResults.push(...response);
74400
74637
  }
@@ -74404,7 +74641,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74404
74641
  const firstResult = result[0];
74405
74642
  if (firstResult && firstResult.code === 0 && firstResult.value?.SearchResult?.File) {
74406
74643
  const files = firstResult.value.SearchResult.File;
74407
- if (files.length > 0 && files[0]) require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response (normal search): ${JSON.stringify({
74644
+ if (files.length > 0 && files[0]) require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response (normal search): ${JSON.stringify({
74408
74645
  fileCount: files.length,
74409
74646
  sampleFile: {
74410
74647
  name: files[0].name,
@@ -74421,12 +74658,12 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74421
74658
  const searchResult = res.value?.SearchResult;
74422
74659
  if (searchResult?.File) allFiles.push(...searchResult.File);
74423
74660
  }
74424
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Collected ${allFiles.length} raw VodFiles from API search results`);
74661
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Collected ${allFiles.length} raw VodFiles from API search results`);
74425
74662
  if (allFiles.length > 0) {
74426
74663
  const sampleSize = Math.min(3, allFiles.length);
74427
74664
  for (let i = 0; i < sampleSize; i++) {
74428
74665
  const file = allFiles[i];
74429
- if (file) require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Sample file ${i + 1}/${allFiles.length}: ${JSON.stringify({
74666
+ if (file) require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Sample file ${i + 1}/${allFiles.length}: ${JSON.stringify({
74430
74667
  name: file.name,
74431
74668
  type: file.type,
74432
74669
  size: file.size,
@@ -74449,7 +74686,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74449
74686
  } catch (e) {}
74450
74687
  enriched.push(this.enrichVodFile(vodFile, channel, streamUrl));
74451
74688
  }
74452
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched.length} enriched recording files`);
74689
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched.length} enriched recording files`);
74453
74690
  if (!bypassCache) {
74454
74691
  const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
74455
74692
  this.recordingsCache.set(cacheKey, {
@@ -74486,7 +74723,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74486
74723
  prepare: true,
74487
74724
  seek: seekSeconds
74488
74725
  });
74489
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`);
74726
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`);
74490
74727
  const { spawn: spawn2 } = await import("child_process");
74491
74728
  return new Promise((resolve, reject) => {
74492
74729
  const chunks = [];
@@ -74534,7 +74771,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74534
74771
  reject(/* @__PURE__ */ new Error(`ffmpeg produced too small output: ${jpeg.length} bytes`));
74535
74772
  return;
74536
74773
  }
74537
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Successfully extracted thumbnail: ${jpeg.length} bytes`);
74774
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Successfully extracted thumbnail: ${jpeg.length} bytes`);
74538
74775
  resolve(jpeg);
74539
74776
  } else reject(/* @__PURE__ */ new Error(`ffmpeg exited with code ${code}: ${stderr.slice(-500)}`));
74540
74777
  });
@@ -74583,10 +74820,10 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74583
74820
  }
74584
74821
  } }
74585
74822
  }];
74586
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Request body: ${JSON.stringify(body)}`);
74823
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Request body: ${JSON.stringify(body)}`);
74587
74824
  try {
74588
74825
  const response = await this.callMany(body);
74589
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Response: ${JSON.stringify(response)}`);
74826
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Response: ${JSON.stringify(response)}`);
74590
74827
  const first = response[0];
74591
74828
  if (!first || first.code !== 0 || !first.value?.fileList) {
74592
74829
  const rspCode = first?.error?.rspCode;
@@ -74607,7 +74844,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74607
74844
  if (!filename) throw new Error(`NvrDownload: no files found in response`);
74608
74845
  return filename;
74609
74846
  } catch (error) {
74610
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Error: ${error instanceof Error ? error.message : String(error)}`);
74847
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Error: ${error instanceof Error ? error.message : String(error)}`);
74611
74848
  throw error;
74612
74849
  }
74613
74850
  }
@@ -74803,7 +75040,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74803
75040
  * Convert a VodFile to RecordingFile with parsed metadata.
74804
75041
  */
74805
75042
  enrichVodFile(vodFile, channel, streamUrl) {
74806
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `RAW VodFile from API: ${JSON.stringify({
75043
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `RAW VodFile from API: ${JSON.stringify({
74807
75044
  name: vodFile.name,
74808
75045
  type: vodFile.type,
74809
75046
  size: vodFile.size,
@@ -74856,7 +75093,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74856
75093
  hashBits0to15: hashRevBin.slice(0, 16)
74857
75094
  };
74858
75095
  }
74859
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Parsed filename: ${JSON.stringify({
75096
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Parsed filename: ${JSON.stringify({
74860
75097
  fileName: vodFile.name,
74861
75098
  filenameParts,
74862
75099
  hexValueFromFilename,
@@ -74883,13 +75120,13 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74883
75120
  const hexFlags = parsed?.flags;
74884
75121
  if (parsed?.devType === "hub" && parsed?.version === 4) {
74885
75122
  if (hexValueFromFilename && hexValueFromFilename !== "unknown") {
74886
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `WARNING: Hub v4 hex value "${hexValueFromFilename}" appears to be constant across files. Detection flags are likely NOT encoded in filename for this version. Detection information may need to be retrieved via a separate API call or may not be available.`);
74887
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `NOTE: For Hub v4, the API response only provides 'type' field which is "${vodFile.type}". No detection-specific fields are available in the Search API response.`);
75123
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `WARNING: Hub v4 hex value "${hexValueFromFilename}" appears to be constant across files. Detection flags are likely NOT encoded in filename for this version. Detection information may need to be retrieved via a separate API call or may not be available.`);
75124
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `NOTE: For Hub v4, the API response only provides 'type' field which is "${vodFile.type}". No detection-specific fields are available in the Search API response.`);
74888
75125
  }
74889
75126
  }
74890
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Hex flags from filename: ${JSON.stringify(hexFlags)}`);
75127
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Hex flags from filename: ${JSON.stringify(hexFlags)}`);
74891
75128
  const typeFlags = this.parseRecordTypeFlags(vodFile.type);
74892
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Type flags from recordType "${vodFile.type}": ${JSON.stringify(typeFlags)}`);
75129
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Type flags from recordType "${vodFile.type}": ${JSON.stringify(typeFlags)}`);
74893
75130
  const hasPerson = (hexFlags?.aiPerson ?? false) || typeFlags.hasPerson;
74894
75131
  const hasVehicle = (hexFlags?.aiVehicle ?? false) || typeFlags.hasVehicle;
74895
75132
  const hasAnimal = (hexFlags?.aiAnimal ?? false) || typeFlags.hasAnimal;
@@ -74900,7 +75137,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
74900
75137
  const hasPackage = (hexFlags?.package ?? false) || typeFlags.hasPackage;
74901
75138
  const hasRf = (hexFlags?.rf ?? false) || typeFlags.hasRf;
74902
75139
  const hasOther = (hexFlags?.aiOther ?? false) || typeFlags.hasOther;
74903
- require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Final merged flags: ${JSON.stringify({
75140
+ require_chunk_XAKOGDFB.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Final merged flags: ${JSON.stringify({
74904
75141
  hasPerson,
74905
75142
  hasVehicle,
74906
75143
  hasAnimal,
@@ -75306,8 +75543,8 @@ function nowIsoCompact() {
75306
75543
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
75307
75544
  }
75308
75545
  function nalTypesSummary(videoType, accessUnitAnnexB) {
75309
- if (videoType === "H264") return require_chunk_OSVYEFMM.splitAnnexBToNalPayloads(accessUnitAnnexB).map((n) => (n[0] ?? 0) & 31);
75310
- return require_chunk_OSVYEFMM.splitAnnexBToNalPayloads2(accessUnitAnnexB).map((nal) => require_chunk_OSVYEFMM.getH265NalType(nal)).filter((t) => typeof t === "number");
75546
+ if (videoType === "H264") return require_chunk_XAKOGDFB.splitAnnexBToNalPayloads(accessUnitAnnexB).map((n) => (n[0] ?? 0) & 31);
75547
+ return require_chunk_XAKOGDFB.splitAnnexBToNalPayloads2(accessUnitAnnexB).map((nal) => require_chunk_XAKOGDFB.getH265NalType(nal)).filter((t) => typeof t === "number");
75311
75548
  }
75312
75549
  function normalizeProfiles(p) {
75313
75550
  const out = [];
@@ -75747,7 +75984,7 @@ async function sampleStreams(opts) {
75747
75984
  });
75748
75985
  };
75749
75986
  api.client.on("frame", onFrame);
75750
- const videoStream = new require_chunk_OSVYEFMM.BaichuanVideoStream({
75987
+ const videoStream = new require_chunk_XAKOGDFB.BaichuanVideoStream({
75751
75988
  client: api.client,
75752
75989
  api,
75753
75990
  channel,
@@ -76797,7 +77034,7 @@ async function runMultifocalDiagnosticsConsecutively(params) {
76797
77034
  let audioFrames = 0;
76798
77035
  let lastSnapshotAtMs = 0;
76799
77036
  const client = params.api.client;
76800
- const videoStream = new require_chunk_OSVYEFMM.BaichuanVideoStream({
77037
+ const videoStream = new require_chunk_XAKOGDFB.BaichuanVideoStream({
76801
77038
  client,
76802
77039
  channel: chNative,
76803
77040
  profile,
@@ -76881,7 +77118,7 @@ async function runMultifocalDiagnosticsConsecutively(params) {
76881
77118
  const baseStreamName = profile === "main" ? "mainStream" : profile === "sub" ? "subStream" : "externStream";
76882
77119
  const headerStreamTypeCandidates = profile === "sub" ? [1, 3] : profile === "main" ? [0, 2] : [0];
76883
77120
  const channelIdTagCandidates = uniq([chNative, chNative + 1].filter((n) => Number.isFinite(n) && n >= 0));
76884
- const messageClassCandidates = params.probeFull ? [require_chunk_OSVYEFMM.BC_CLASS_MODERN_24, 0] : [require_chunk_OSVYEFMM.BC_CLASS_MODERN_24];
77121
+ const messageClassCandidates = params.probeFull ? [require_chunk_XAKOGDFB.BC_CLASS_MODERN_24, 0] : [require_chunk_XAKOGDFB.BC_CLASS_MODERN_24];
76885
77122
  const extensionXmlCandidates = params.probeFull ? ["", buildChannelExtensionXml(chNative)] : [buildChannelExtensionXml(chNative)];
76886
77123
  const attempts = [];
76887
77124
  for (const messageClass of messageClassCandidates) for (const streamTypeHeader of headerStreamTypeCandidates) for (const extensionXml of extensionXmlCandidates) {
@@ -79402,14 +79639,21 @@ var require_lz4 = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
79402
79639
  };
79403
79640
  }));
79404
79641
  //#endregion
79405
- //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-7B2Z5FIA.js
79642
+ //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-OXQH2ZT3.js
79406
79643
  var import_fxp = require_fxp();
79407
79644
  var import_lz4 = /* @__PURE__ */ require_chunk.__toESM(require_lz4(), 1);
79645
+ function bcHeaderLen(messageClass, buf, bodyLen) {
79646
+ if (require_chunk_XAKOGDFB.bcHeaderHasPayloadOffset(messageClass)) return 24;
79647
+ if (require_chunk_XAKOGDFB.bcHeaderIsKnown20(messageClass)) return 20;
79648
+ if (buf.length < 24) throw new Error("not enough data for Baichuan header (needs 24 bytes)");
79649
+ const candidate = buf.readUInt32LE(20);
79650
+ return candidate >= 20 && candidate <= bodyLen ? 24 : 20;
79651
+ }
79408
79652
  function encodeHeader(h) {
79409
- const hasOffset = require_chunk_OSVYEFMM.bcHeaderHasPayloadOffset(h.messageClass);
79653
+ const hasOffset = require_chunk_XAKOGDFB.bcHeaderHasPayloadOffset(h.messageClass);
79410
79654
  const headerLen = hasOffset ? 24 : 20;
79411
79655
  const buf = Buffer.alloc(headerLen);
79412
- const magic = h.magic ?? require_chunk_OSVYEFMM.BC_MAGIC;
79656
+ const magic = h.magic ?? require_chunk_XAKOGDFB.BC_MAGIC;
79413
79657
  if (magic.length !== 4) throw new Error("magic must be 4 bytes");
79414
79658
  magic.copy(buf, 0);
79415
79659
  buf.writeUInt32LE(h.cmdId >>> 0, 4);
@@ -79425,7 +79669,7 @@ function encodeHeader(h) {
79425
79669
  function decodeHeader(buf) {
79426
79670
  if (buf.length < 20) throw new Error("not enough data for Baichuan header");
79427
79671
  const magic = buf.subarray(0, 4);
79428
- if (!magic.equals(require_chunk_OSVYEFMM.BC_MAGIC) && !magic.equals(require_chunk_OSVYEFMM.BC_MAGIC_REV)) throw new Error(`invalid Baichuan magic: ${magic.toString("hex")}`);
79672
+ if (!magic.equals(require_chunk_XAKOGDFB.BC_MAGIC) && !magic.equals(require_chunk_XAKOGDFB.BC_MAGIC_REV)) throw new Error(`invalid Baichuan magic: ${magic.toString("hex")}`);
79429
79673
  const cmdId = buf.readUInt32LE(4);
79430
79674
  const bodyLen = buf.readUInt32LE(8);
79431
79675
  const channelId = buf.readUInt8(12);
@@ -79433,7 +79677,7 @@ function decodeHeader(buf) {
79433
79677
  const msgNum = buf.readUInt16LE(14);
79434
79678
  const responseCode = buf.readUInt16LE(16);
79435
79679
  const messageClass = buf.readUInt16LE(18);
79436
- const headerLen = require_chunk_OSVYEFMM.bcHeaderHasPayloadOffset(messageClass) ? 24 : 20;
79680
+ const headerLen = bcHeaderLen(messageClass, buf, bodyLen);
79437
79681
  if (buf.length < headerLen) throw new Error("not enough data for Baichuan header (needs 24 bytes)");
79438
79682
  const messageKey = buf.readUInt32LE(12);
79439
79683
  const header = {
@@ -79503,9 +79747,9 @@ var BaichuanFrameParser = class {
79503
79747
  this.needed = 4;
79504
79748
  break;
79505
79749
  }
79506
- if (!this.buffer.subarray(0, 4).equals(require_chunk_OSVYEFMM.BC_MAGIC) && !this.buffer.subarray(0, 4).equals(require_chunk_OSVYEFMM.BC_MAGIC_REV)) {
79507
- const idx = this.buffer.indexOf(require_chunk_OSVYEFMM.BC_MAGIC);
79508
- const idxRev = this.buffer.indexOf(require_chunk_OSVYEFMM.BC_MAGIC_REV);
79750
+ if (!this.buffer.subarray(0, 4).equals(require_chunk_XAKOGDFB.BC_MAGIC) && !this.buffer.subarray(0, 4).equals(require_chunk_XAKOGDFB.BC_MAGIC_REV)) {
79751
+ const idx = this.buffer.indexOf(require_chunk_XAKOGDFB.BC_MAGIC);
79752
+ const idxRev = this.buffer.indexOf(require_chunk_XAKOGDFB.BC_MAGIC_REV);
79509
79753
  const next = idx === -1 ? idxRev : idxRev === -1 ? idx : Math.min(idx, idxRev);
79510
79754
  if (next === -1) {
79511
79755
  this.buffer = this.buffer.subarray(Math.max(0, this.buffer.length - 3));
@@ -81366,6 +81610,9 @@ function createDebugGateLogger(base, enabled = false) {
81366
81610
  function isTalkCmd(cmdId) {
81367
81611
  return cmdId === 10 || cmdId === 11 || cmdId === 201 || cmdId === 202;
81368
81612
  }
81613
+ function toWireChannelId(channelId) {
81614
+ return channelId & 255;
81615
+ }
81369
81616
  var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81370
81617
  /**
81371
81618
  * Process-wide streaming activity registry.
@@ -81531,7 +81778,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81531
81778
  constructor(options) {
81532
81779
  super();
81533
81780
  this.opts = options;
81534
- this.debugCfg = require_chunk_OSVYEFMM.normalizeDebugOptions(options.debugOptions);
81781
+ this.debugCfg = require_chunk_XAKOGDFB.normalizeDebugOptions(options.debugOptions);
81535
81782
  this.logger = createDebugGateLogger(options.logger, this.debugCfg.general || this.debugCfg.traceNativeStream || this.debugCfg.traceRecordings || this.debugCfg.traceTalk || this.debugCfg.traceEvents || this.debugCfg.debugRtsp);
81536
81783
  this.on("error", (err) => {
81537
81784
  this.logFixed("error", {
@@ -82013,7 +82260,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82013
82260
  streamType: 0,
82014
82261
  msgNum,
82015
82262
  responseCode: 0,
82016
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
82263
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
82017
82264
  payloadOffset: 0
82018
82265
  });
82019
82266
  this.logDebug("udp_keepalive_ping_tx", {
@@ -82045,7 +82292,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82045
82292
  cmdId: 93,
82046
82293
  channel: this.opts.channel ?? 0,
82047
82294
  channelIdOverride: 0,
82048
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
82295
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
82049
82296
  streamType: 0,
82050
82297
  extensionXml: "",
82051
82298
  internal: true
@@ -82384,7 +82631,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82384
82631
  cmdId: 2,
82385
82632
  payloadXml: logoutXml,
82386
82633
  extensionXml: "",
82387
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
82634
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
82388
82635
  channelIdOverride: effectiveHostChannelId,
82389
82636
  timeoutMs: 5e3
82390
82637
  })).header.responseCode;
@@ -82497,12 +82744,12 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82497
82744
  };
82498
82745
  s.frames++;
82499
82746
  if (now - s.lastLogMs >= 1e3) {
82500
- require_chunk_OSVYEFMM.debugLog(this.debugCfg, this.logger, "BaichuanRx", `rx cmdId=3 frames=${s.frames} lastMsgNum=${frame.header.msgNum} lastResponseCode=${frame.header.responseCode} lastChannelId=${frame.header.channelId} lastStreamType=${frame.header.streamType} lastBodyLen=${frame.body.length} lastPayloadLen=${frame.payload.length}`);
82747
+ require_chunk_XAKOGDFB.debugLog(this.debugCfg, this.logger, "BaichuanRx", `rx cmdId=3 frames=${s.frames} lastMsgNum=${frame.header.msgNum} lastResponseCode=${frame.header.responseCode} lastChannelId=${frame.header.channelId} lastStreamType=${frame.header.streamType} lastBodyLen=${frame.body.length} lastPayloadLen=${frame.payload.length}`);
82501
82748
  s.lastLogMs = now;
82502
82749
  s.frames = 0;
82503
82750
  }
82504
82751
  this.rxCmdTraceStats.set(3, s);
82505
- } else require_chunk_OSVYEFMM.debugLog(this.debugCfg, this.logger, "BaichuanRx", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} streamType=${frame.header.streamType} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
82752
+ } else require_chunk_XAKOGDFB.debugLog(this.debugCfg, this.logger, "BaichuanRx", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} streamType=${frame.header.streamType} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
82506
82753
  if (this.transport === "udp" && frame.header.cmdId === 234) {
82507
82754
  try {
82508
82755
  const header = encodeHeader({
@@ -82539,7 +82786,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82539
82786
  }
82540
82787
  return;
82541
82788
  }
82542
- if (this.debugCfg.traceTalk && isTalkCmd(frame.header.cmdId)) require_chunk_OSVYEFMM.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
82789
+ if (this.debugCfg.traceTalk && isTalkCmd(frame.header.cmdId)) require_chunk_XAKOGDFB.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
82543
82790
  this.emit("frame", frame);
82544
82791
  const key = `${frame.header.cmdId}:${frame.header.msgNum}`;
82545
82792
  const pending = this.pending.get(key);
@@ -82560,7 +82807,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82560
82807
  };
82561
82808
  s.frames++;
82562
82809
  if (now2 - s.lastLogMs >= 500) {
82563
- require_chunk_OSVYEFMM.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `rx stream frames cmdId=3 msgNum=${frame.header.msgNum} frames=${s.frames} lastChannelId=${frame.header.channelId} lastBodyLen=${frame.body.length} lastPayloadLen=${frame.payload.length} lastPayloadOffset=${frame.header.payloadOffset ?? 0}`);
82810
+ require_chunk_XAKOGDFB.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `rx stream frames cmdId=3 msgNum=${frame.header.msgNum} frames=${s.frames} lastChannelId=${frame.header.channelId} lastBodyLen=${frame.body.length} lastPayloadLen=${frame.payload.length} lastPayloadOffset=${frame.header.payloadOffset ?? 0}`);
82564
82811
  s.lastLogMs = now2;
82565
82812
  s.frames = 0;
82566
82813
  }
@@ -82591,7 +82838,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82591
82838
  const eventLogTag = sid ? `BaichuanEvent sid=${sid}` : "BaichuanEvent";
82592
82839
  const events$1 = this.parseEvents(frame);
82593
82840
  for (const event of events$1) {
82594
- require_chunk_OSVYEFMM.eventTraceLog(this.debugCfg, this.logger, eventLogTag, `dispatch cmdId=33 msgNum=${frame.header.msgNum} channelId=${frame.header.channelId} type=${event.type} eventChannel=${event.channel}` + (event.type === "ai" ? ` aiType=${event.ai?.type ?? "unknown"} detected=${event.ai?.detected ?? ""}` : "") + (event.type === "motion" ? ` source=${event.motion?.source ?? ""}` : ""));
82841
+ require_chunk_XAKOGDFB.eventTraceLog(this.debugCfg, this.logger, eventLogTag, `dispatch cmdId=33 msgNum=${frame.header.msgNum} channelId=${frame.header.channelId} type=${event.type} eventChannel=${event.channel}` + (event.type === "ai" ? ` aiType=${event.ai?.type ?? "unknown"} detected=${event.ai?.detected ?? ""}` : "") + (event.type === "motion" ? ` source=${event.motion?.source ?? ""}` : ""));
82595
82842
  this.emit("event", event);
82596
82843
  }
82597
82844
  } catch (error) {
@@ -82650,7 +82897,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82650
82897
  if (!xml || !xml.startsWith("<?xml")) return [];
82651
82898
  if (this.debugCfg.traceEvents) {
82652
82899
  const snippet = xml.length > 500 ? `${xml.slice(0, 500)}...` : xml;
82653
- require_chunk_OSVYEFMM.eventTraceLog(this.debugCfg, this.logger, eventRawLogTag, `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} xml=${JSON.stringify(snippet)}`);
82900
+ require_chunk_XAKOGDFB.eventTraceLog(this.debugCfg, this.logger, eventRawLogTag, `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} xml=${JSON.stringify(snippet)}`);
82654
82901
  }
82655
82902
  const fallbackChannelId = frame.header.channelId;
82656
82903
  const fallbackChannel = fallbackChannelId === 250 ? 0 : Math.max(0, fallbackChannelId - 1);
@@ -82668,7 +82915,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82668
82915
  const timeStampRaw2 = (getXmlText(alarmXml, "timeStamp") ?? getXmlText(alarmXml, "timestamp") ?? "").trim();
82669
82916
  const timeStampNum2 = timeStampRaw2.length ? Number(timeStampRaw2) : void 0;
82670
82917
  const alarmTimeStamp2 = Number.isFinite(timeStampNum2) ? timeStampNum2 : void 0;
82671
- if (this.debugCfg.traceEvents) require_chunk_OSVYEFMM.eventTraceLog(this.debugCfg, this.logger, eventRawLogTag, `AlarmEvent channel=${channel} status=${JSON.stringify(status2)} aiType=${JSON.stringify(aiTypeRaw2)}`);
82918
+ if (this.debugCfg.traceEvents) require_chunk_XAKOGDFB.eventTraceLog(this.debugCfg, this.logger, eventRawLogTag, `AlarmEvent channel=${channel} status=${JSON.stringify(status2)} aiType=${JSON.stringify(aiTypeRaw2)}`);
82672
82919
  const statusTokens2 = status2.toLowerCase().split(",").map((t) => t.trim()).filter((t) => t.length > 0);
82673
82920
  const visitorActive2 = statusTokens2.includes("visitor");
82674
82921
  const dayNightActive2 = statusTokens2.includes("dn") || statusTokens2.includes("daynight");
@@ -83022,15 +83269,15 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
83022
83269
  const extBuf = Buffer.from(extXml, "utf8");
83023
83270
  const payloadBuf = Buffer.from(payloadXml, "utf8");
83024
83271
  if (enc.kind === "none") return Buffer.concat([extBuf, payloadBuf]);
83025
- if (enc.kind === "bc") return Buffer.concat([require_chunk_OSVYEFMM.bcEncrypt(extBuf, channelId), require_chunk_OSVYEFMM.bcEncrypt(payloadBuf, channelId)]);
83026
- if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_OSVYEFMM.aesEncrypt(extBuf, enc.key), require_chunk_OSVYEFMM.aesEncrypt(payloadBuf, enc.key)]);
83272
+ if (enc.kind === "bc") return Buffer.concat([require_chunk_XAKOGDFB.bcEncrypt(extBuf, channelId), require_chunk_XAKOGDFB.bcEncrypt(payloadBuf, channelId)]);
83273
+ if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_XAKOGDFB.aesEncrypt(extBuf, enc.key), require_chunk_XAKOGDFB.aesEncrypt(payloadBuf, enc.key)]);
83027
83274
  return Buffer.concat([extBuf, payloadBuf]);
83028
83275
  }
83029
83276
  encodeBodyBinary(extXml, payload, channelId, enc) {
83030
83277
  const extBuf = Buffer.from(extXml, "utf8");
83031
83278
  if (enc.kind === "none") return Buffer.concat([extBuf, payload]);
83032
- if (enc.kind === "bc") return Buffer.concat([require_chunk_OSVYEFMM.bcEncrypt(extBuf, channelId), payload]);
83033
- if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_OSVYEFMM.aesEncrypt(extBuf, enc.key), payload]);
83279
+ if (enc.kind === "bc") return Buffer.concat([require_chunk_XAKOGDFB.bcEncrypt(extBuf, channelId), payload]);
83280
+ if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_XAKOGDFB.aesEncrypt(extBuf, enc.key), payload]);
83034
83281
  return Buffer.concat([extBuf, payload]);
83035
83282
  }
83036
83283
  /**
@@ -83075,7 +83322,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
83075
83322
  bodyLen,
83076
83323
  binaryPayload: true
83077
83324
  });
83078
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_OSVYEFMM.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset} binaryPayloadLen=${params.payload.length}`);
83325
+ if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_XAKOGDFB.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset} binaryPayloadLen=${params.payload.length}`);
83079
83326
  this.recordTx({
83080
83327
  cmdId,
83081
83328
  responseCode: 0,
@@ -83092,8 +83339,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
83092
83339
  let dec;
83093
83340
  try {
83094
83341
  if (enc.kind === "none") dec = buf;
83095
- else if (enc.kind === "bc") dec = require_chunk_OSVYEFMM.bcDecrypt(buf, channelId);
83096
- else dec = require_chunk_OSVYEFMM.aesDecrypt(buf, enc.key);
83342
+ else if (enc.kind === "bc") dec = require_chunk_XAKOGDFB.bcDecrypt(buf, channelId);
83343
+ else dec = require_chunk_XAKOGDFB.aesDecrypt(buf, enc.key);
83097
83344
  } catch {
83098
83345
  return;
83099
83346
  }
@@ -83159,8 +83406,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
83159
83406
  messageClass,
83160
83407
  bodyLen
83161
83408
  });
83162
- if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_OSVYEFMM.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
83163
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_OSVYEFMM.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
83409
+ if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_XAKOGDFB.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
83410
+ if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_XAKOGDFB.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
83164
83411
  this.recordTx({
83165
83412
  cmdId,
83166
83413
  responseCode: 0,
@@ -83175,8 +83422,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
83175
83422
  responseCode: frame.header.responseCode,
83176
83423
  msgNum: frame.header.msgNum
83177
83424
  });
83178
- if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_OSVYEFMM.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
83179
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_OSVYEFMM.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
83425
+ if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_XAKOGDFB.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
83426
+ if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_XAKOGDFB.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
83180
83427
  if (frame.header.responseCode === 400) {
83181
83428
  if (frame.body.length === 0) throw new Error("Baichuan authentication failed (responseCode 400, empty body) - check username/password");
83182
83429
  }
@@ -83246,9 +83493,9 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
83246
83493
  messageClass,
83247
83494
  bodyLen
83248
83495
  });
83249
- if (_BaichuanClient.recordingCmdIds.has(cmdId)) require_chunk_OSVYEFMM.recordingsTraceLog(this.debugCfg, this.logger, "BaichuanRecordings", `tx recording cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} bodyLen=${bodyLen}`);
83250
- if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_OSVYEFMM.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
83251
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_OSVYEFMM.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
83496
+ if (_BaichuanClient.recordingCmdIds.has(cmdId)) require_chunk_XAKOGDFB.recordingsTraceLog(this.debugCfg, this.logger, "BaichuanRecordings", `tx recording cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} bodyLen=${bodyLen}`);
83497
+ if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_XAKOGDFB.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
83498
+ if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_XAKOGDFB.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
83252
83499
  this.recordTx({
83253
83500
  cmdId,
83254
83501
  responseCode: 0,
@@ -83263,9 +83510,9 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
83263
83510
  responseCode: frame.header.responseCode,
83264
83511
  msgNum: frame.header.msgNum
83265
83512
  });
83266
- if (_BaichuanClient.recordingCmdIds.has(frame.header.cmdId)) require_chunk_OSVYEFMM.recordingsTraceLog(this.debugCfg, this.logger, "BaichuanRecordings", `rx recording cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
83267
- if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_OSVYEFMM.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
83268
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_OSVYEFMM.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
83513
+ if (_BaichuanClient.recordingCmdIds.has(frame.header.cmdId)) require_chunk_XAKOGDFB.recordingsTraceLog(this.debugCfg, this.logger, "BaichuanRecordings", `rx recording cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
83514
+ if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_XAKOGDFB.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
83515
+ if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_XAKOGDFB.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
83269
83516
  if (!internal) this.kickIdleDisconnectTimer();
83270
83517
  return frame;
83271
83518
  }
@@ -83534,7 +83781,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
83534
83781
  await this.connect();
83535
83782
  params.channel ?? this.opts.channel;
83536
83783
  const sessionCounter = this.nextMsgNum();
83537
- const channelId = params.channelIdOverride ?? sessionCounter;
83784
+ const channelId = toWireChannelId(params.channelIdOverride ?? sessionCounter);
83538
83785
  const discriminator = params.channelIdOverride == null ? {
83539
83786
  kind: "minted",
83540
83787
  channelId
@@ -83651,25 +83898,25 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
83651
83898
  return;
83652
83899
  }
83653
83900
  if (lockedStreamType !== void 0 && frame.header.streamType !== lockedStreamType) return;
83654
- if (lockedChannelId !== void 0 && frame.header.channelId !== lockedChannelId) {
83901
+ if (lockedChannelId !== void 0 && frame.header.channelId !== toWireChannelId(lockedChannelId)) {
83655
83902
  this.countReplayDrop(session, frame, "foreign-channel");
83656
83903
  return;
83657
83904
  }
83658
83905
  if (streamMsgNum !== void 0 && frame.header.msgNum !== streamMsgNum) return;
83906
+ let markedBinary = false;
83907
+ let encryptLen;
83908
+ if (frame.extension.length > 0) try {
83909
+ const extDec = this.tryDecryptXml(frame.extension, frame.header.channelId, enc);
83910
+ if (extDec.includes("<binaryData>1</binaryData>")) markedBinary = true;
83911
+ const encryptLenMatch = extDec.match(/<encryptLen>(\d+)<\/encryptLen>/i);
83912
+ if (encryptLenMatch && encryptLenMatch[1]) encryptLen = parseInt(encryptLenMatch[1], 10);
83913
+ } catch {}
83659
83914
  const rc = frame.header.responseCode;
83660
- if (rc >= 400 && rc < 6e4 && frame.header.msgNum === msgNum) {
83915
+ if (streamMsgNum === void 0 && !markedBinary && !(rc === 0 || rc === 200 || rc === 201) && frame.header.msgNum === msgNum) {
83661
83916
  fail(/* @__PURE__ */ new Error(`Baichuan FileInfoList replay rejected (cmdId=${cmdId} reqChannelId=${channelId} rspChannelId=${frame.header.channelId} streamType=${expectedStreamType} msgNum=${frame.header.msgNum} responseCode=${rc})`));
83662
83917
  return;
83663
83918
  }
83664
83919
  try {
83665
- let markedBinary = false;
83666
- let encryptLen;
83667
- if (frame.extension.length > 0) try {
83668
- const extDec = this.tryDecryptXml(frame.extension, frame.header.channelId, enc);
83669
- if (extDec.includes("<binaryData>1</binaryData>")) markedBinary = true;
83670
- const encryptLenMatch = extDec.match(/<encryptLen>(\d+)<\/encryptLen>/i);
83671
- if (encryptLenMatch && encryptLenMatch[1]) encryptLen = parseInt(encryptLenMatch[1], 10);
83672
- } catch {}
83673
83920
  const decrypted = decryptBinaryForReplay(frame.payload, frame.header.channelId, encryptLen);
83674
83921
  if (decrypted.length === 0) return;
83675
83922
  if (!markedBinary && looksLikeXml(decrypted)) return;
@@ -83899,7 +84146,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
83899
84146
  await this.connect();
83900
84147
  params.channel ?? this.opts.channel;
83901
84148
  const sessionCounter = this.nextMsgNum();
83902
- const channelId = params.channelIdOverride ?? sessionCounter;
84149
+ const channelId = toWireChannelId(params.channelIdOverride ?? sessionCounter);
83903
84150
  const msgNum = params.msgNumOverride ?? 0;
83904
84151
  const cmdId = params.cmdId;
83905
84152
  const extXml = params.extensionXml ?? "";
@@ -84110,8 +84357,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
84110
84357
  const tryAs = (enc) => {
84111
84358
  try {
84112
84359
  if (enc.kind === "none") return buf;
84113
- if (enc.kind === "bc") return require_chunk_OSVYEFMM.bcDecrypt(buf, channelId);
84114
- if (enc.kind === "aes" || enc.kind === "full_aes") return require_chunk_OSVYEFMM.aesDecrypt(buf, enc.key);
84360
+ if (enc.kind === "bc") return require_chunk_XAKOGDFB.bcDecrypt(buf, channelId);
84361
+ if (enc.kind === "aes" || enc.kind === "full_aes") return require_chunk_XAKOGDFB.aesDecrypt(buf, enc.key);
84115
84362
  return null;
84116
84363
  } catch {
84117
84364
  return null;
@@ -84141,7 +84388,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
84141
84388
  streamType: 0,
84142
84389
  msgNum,
84143
84390
  responseCode: encByte,
84144
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_LEGACY
84391
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_LEGACY
84145
84392
  });
84146
84393
  const pendingKey = `${cmdId}:${msgNum}`;
84147
84394
  const framePromise = new Promise((resolve, reject) => {
@@ -84173,15 +84420,15 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
84173
84420
  else if (encType === 1) this.enc = { kind: "bc" };
84174
84421
  else if (encType === 2) this.enc = {
84175
84422
  kind: "aes",
84176
- key: require_chunk_OSVYEFMM.deriveAesKey(nonce, this.opts.password)
84423
+ key: require_chunk_XAKOGDFB.deriveAesKey(nonce, this.opts.password)
84177
84424
  };
84178
84425
  else if (encType === 18) this.enc = {
84179
84426
  kind: "full_aes",
84180
- key: require_chunk_OSVYEFMM.deriveAesKey(nonce, this.opts.password)
84427
+ key: require_chunk_XAKOGDFB.deriveAesKey(nonce, this.opts.password)
84181
84428
  };
84182
84429
  else throw new Error(`Baichuan login: unknown encType=0x${encType.toString(16)}`);
84183
- const userHash = require_chunk_OSVYEFMM.md5StrModern(`${this.opts.username}${nonce}`);
84184
- const passHash = require_chunk_OSVYEFMM.md5StrModern(`${this.opts.password}${nonce}`);
84430
+ const userHash = require_chunk_XAKOGDFB.md5StrModern(`${this.opts.username}${nonce}`);
84431
+ const passHash = require_chunk_XAKOGDFB.md5StrModern(`${this.opts.password}${nonce}`);
84185
84432
  const loginXml = buildLoginXml(userHash, passHash);
84186
84433
  this.logDebug("login_hash", {
84187
84434
  username: this.opts.username,
@@ -84196,7 +84443,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
84196
84443
  cmdId: 1,
84197
84444
  payloadXml: loginXml,
84198
84445
  extensionXml: "",
84199
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
84446
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
84200
84447
  channelIdOverride: effectiveHostChannelId,
84201
84448
  encryption: loginEnc,
84202
84449
  timeoutMs: 1e4
@@ -84256,7 +84503,7 @@ async function* createNativeStream(api, channel, profile, options) {
84256
84503
  client = api.client;
84257
84504
  }
84258
84505
  }
84259
- const videoStream = new require_chunk_OSVYEFMM.BaichuanVideoStream({
84506
+ const videoStream = new require_chunk_XAKOGDFB.BaichuanVideoStream({
84260
84507
  client,
84261
84508
  api,
84262
84509
  channel,
@@ -84774,7 +85021,7 @@ var H264Flow = class extends BaseFlow {
84774
85021
  super(transport, "H264");
84775
85022
  }
84776
85023
  extractParameterSets(accessUnitAnnexB) {
84777
- const nals = require_chunk_OSVYEFMM.splitAnnexBToNalPayloads(accessUnitAnnexB);
85024
+ const nals = require_chunk_XAKOGDFB.splitAnnexBToNalPayloads(accessUnitAnnexB);
84778
85025
  for (const nal of nals) {
84779
85026
  if (nal.length < 1) continue;
84780
85027
  const nalType = (nal[0] ?? 0) & 31;
@@ -84819,9 +85066,9 @@ var H265Flow = class extends BaseFlow {
84819
85066
  super(transport, "H265");
84820
85067
  }
84821
85068
  extractParameterSets(accessUnitAnnexB) {
84822
- const vps = require_chunk_OSVYEFMM.extractVpsFromAnnexB(accessUnitAnnexB);
84823
- const sps = require_chunk_OSVYEFMM.extractSpsFromAnnexB(accessUnitAnnexB);
84824
- const pps = require_chunk_OSVYEFMM.extractPpsFromAnnexB(accessUnitAnnexB);
85069
+ const vps = require_chunk_XAKOGDFB.extractVpsFromAnnexB(accessUnitAnnexB);
85070
+ const sps = require_chunk_XAKOGDFB.extractSpsFromAnnexB(accessUnitAnnexB);
85071
+ const pps = require_chunk_XAKOGDFB.extractPpsFromAnnexB(accessUnitAnnexB);
84825
85072
  if (vps && !this.vps) this.vps = vps;
84826
85073
  if (sps && !this.sps) this.sps = sps;
84827
85074
  if (pps && !this.pps) this.pps = pps;
@@ -85205,8 +85452,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
85205
85452
  /** Returns true if the raw (packed/Annex B) frame is an IDR (H.264) or IRAP (H.265). */
85206
85453
  isRawFrameKeyframe(frame) {
85207
85454
  try {
85208
- if (frame.videoType === "H264") return _BaichuanRtspServer.splitAnnexBNals(require_chunk_OSVYEFMM.convertToAnnexB(frame.data)).some((n) => n.length >= 1 && (n[0] & 31) === 5);
85209
- if (frame.videoType === "H265") return require_chunk_OSVYEFMM.splitAnnexBToNalPayloads2(require_chunk_OSVYEFMM.convertToAnnexB2(frame.data)).some((n) => n.length >= 2 && require_chunk_OSVYEFMM.isH265Irap(n[0] >> 1 & 63));
85455
+ if (frame.videoType === "H264") return _BaichuanRtspServer.splitAnnexBNals(require_chunk_XAKOGDFB.convertToAnnexB(frame.data)).some((n) => n.length >= 1 && (n[0] & 31) === 5);
85456
+ if (frame.videoType === "H265") return require_chunk_XAKOGDFB.splitAnnexBToNalPayloads2(require_chunk_XAKOGDFB.convertToAnnexB2(frame.data)).some((n) => n.length >= 2 && require_chunk_XAKOGDFB.isH265Irap(n[0] >> 1 & 63));
85210
85457
  } catch {}
85211
85458
  return false;
85212
85459
  }
@@ -86106,13 +86353,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
86106
86353
  if (resources?.rtpAudioTimestamp !== void 0) resources.rtpAudioTimestamp = resources.rtpAudioTimestamp + 1024 >>> 0;
86107
86354
  };
86108
86355
  const isH265IrapAccessUnit = (annexB) => {
86109
- const nals = require_chunk_OSVYEFMM.splitAnnexBToNalPayloads2(annexB);
86356
+ const nals = require_chunk_XAKOGDFB.splitAnnexBToNalPayloads2(annexB);
86110
86357
  for (const nal of nals) {
86111
86358
  if (nal.length < 2) continue;
86112
86359
  const b0 = nal[0];
86113
86360
  if (b0 === void 0) continue;
86114
86361
  if ((b0 & 128) !== 0) continue;
86115
- if (require_chunk_OSVYEFMM.isH265Irap(b0 >> 1 & 63)) return true;
86362
+ if (require_chunk_XAKOGDFB.isH265Irap(b0 >> 1 & 63)) return true;
86116
86363
  }
86117
86364
  return false;
86118
86365
  };
@@ -86268,7 +86515,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
86268
86515
  continue;
86269
86516
  }
86270
86517
  if (frame.videoType === "H264" || frame.videoType === "H265") {
86271
- const normalizedVideoData = frame.videoType === "H264" ? require_chunk_OSVYEFMM.convertToAnnexB(frame.data) : require_chunk_OSVYEFMM.convertToAnnexB2(frame.data);
86518
+ const normalizedVideoData = frame.videoType === "H264" ? require_chunk_XAKOGDFB.convertToAnnexB(frame.data) : require_chunk_XAKOGDFB.convertToAnnexB2(frame.data);
86272
86519
  if (frameCount === 0) this.setFlowVideoType(frame.videoType, "first video frame");
86273
86520
  if (!this.flow.getFmtp().hasParamSets) {
86274
86521
  this.flow.extractParameterSets(normalizedVideoData);
@@ -86284,7 +86531,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
86284
86531
  }
86285
86532
  if (useDirectRtp) {
86286
86533
  const videoType = frame.videoType ?? this.flow.videoType;
86287
- const normalizedVideoData = videoType === "H264" ? require_chunk_OSVYEFMM.convertToAnnexB(frame.data) : require_chunk_OSVYEFMM.convertToAnnexB2(frame.data);
86534
+ const normalizedVideoData = videoType === "H264" ? require_chunk_XAKOGDFB.convertToAnnexB(frame.data) : require_chunk_XAKOGDFB.convertToAnnexB2(frame.data);
86288
86535
  if (!resources?.seenFirstVideoKeyframe) if (videoType === "H265") {
86289
86536
  const { hasParamSets } = this.flow.getFmtp();
86290
86537
  if (!hasParamSets) {
@@ -86360,7 +86607,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
86360
86607
  const headHex = frame.data.subarray(0, 16).toString("hex");
86361
86608
  this.rtspDebugLog(`First video frame written to ffmpeg stdin for client ${clientId} (len=${frame.data.length}, head=${headHex})`);
86362
86609
  }
86363
- if (!stdin.write(frame.videoType === "H264" ? require_chunk_OSVYEFMM.convertToAnnexB(frame.data) : frame.data)) await new Promise((resolve) => {
86610
+ if (!stdin.write(frame.videoType === "H264" ? require_chunk_XAKOGDFB.convertToAnnexB(frame.data) : frame.data)) await new Promise((resolve) => {
86364
86611
  if (stdin) stdin.once("drain", () => resolve());
86365
86612
  else resolve();
86366
86613
  });
@@ -86472,7 +86719,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
86472
86719
  ensureContinuousStream(dedicatedClient) {
86473
86720
  if (this.continuousStream) return this.continuousStream;
86474
86721
  const createLiveStream = async () => {
86475
- return new require_chunk_OSVYEFMM.BaichuanVideoStream({
86722
+ return new require_chunk_XAKOGDFB.BaichuanVideoStream({
86476
86723
  client: dedicatedClient ?? this.api.client,
86477
86724
  api: this.api,
86478
86725
  channel: this.channel,
@@ -88169,6 +88416,117 @@ var estimateVideoTiming = (params) => {
88169
88416
  fpsSource: "unknown"
88170
88417
  };
88171
88418
  };
88419
+ var parseRecStartParamIfPresent = (fileName) => {
88420
+ const m = /Rec(\w{3})(?:_|_DST)(\d{8})_(\d{6})_.*/.exec(fileName);
88421
+ if (!m) return void 0;
88422
+ return `${m[2]}${m[3]}`;
88423
+ };
88424
+ var buildReplayStopNameFromFileName = (fileName, channel = 0) => {
88425
+ const trimmed = (fileName ?? "").trim();
88426
+ if (/^\d{2}\d{14}$/.test(trimmed)) return trimmed;
88427
+ const start = parseRecStartParamIfPresent(fileName);
88428
+ if (!start) return void 0;
88429
+ return `${String(channel + 1).padStart(2, "0")}${start}`;
88430
+ };
88431
+ var buildFileInfoListReplayByIdXml = (params) => {
88432
+ const st = params.streamType ?? "mainStream";
88433
+ const supportSub = st === "subStream" ? 1 : 0;
88434
+ const xmlCh = params.xmlChannelId ?? params.channel;
88435
+ const iframe = params.iframeReplay;
88436
+ const iframeXml = iframe === true || iframe === "both" ? "<bIframeReplay>1</bIframeReplay><iIframeReplay>1</iIframeReplay>" : iframe === "b" ? "<bIframeReplay>1</bIframeReplay>" : iframe === "i" ? "<iIframeReplay>1</iIframeReplay>" : "";
88437
+ const lines = [
88438
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>",
88439
+ "<body>",
88440
+ "<FileInfoList version=\"1.1\">",
88441
+ "<FileInfo>",
88442
+ `<channelId>${xmlCh}</channelId>`,
88443
+ `<Id>${xmlEscape(params.id)}</Id>`
88444
+ ];
88445
+ if (params.uid) lines.push(`<uid>${xmlEscape(params.uid)}</uid>`);
88446
+ lines.push(`<supportSub>${supportSub}</supportSub>`, "<playSpeed>1</playSpeed>", `<streamType>${xmlEscape(st)}</streamType>`);
88447
+ if (iframeXml) lines.push(iframeXml);
88448
+ lines.push("</FileInfo>", "</FileInfoList>", "</body>");
88449
+ return lines.join("\n");
88450
+ };
88451
+ var buildFileInfoListReplayByNameXml = (params) => {
88452
+ const st = params.streamType ?? "mainStream";
88453
+ const supportSub = st === "subStream" ? 1 : 0;
88454
+ const xmlCh = params.xmlChannelId ?? params.channel;
88455
+ const iframe = params.iframeReplay;
88456
+ const iframeXml = iframe === true || iframe === "both" ? "<bIframeReplay>1</bIframeReplay><iIframeReplay>1</iIframeReplay>" : iframe === "b" ? "<bIframeReplay>1</bIframeReplay>" : iframe === "i" ? "<iIframeReplay>1</iIframeReplay>" : "";
88457
+ const lines = [
88458
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>",
88459
+ "<body>",
88460
+ "<FileInfoList version=\"1.1\">",
88461
+ "<FileInfo>",
88462
+ `<channelId>${xmlCh}</channelId>`,
88463
+ `<Id>${xmlEscape(params.name)}</Id>`
88464
+ ];
88465
+ if (params.uid) lines.push(`<uid>${xmlEscape(params.uid)}</uid>`);
88466
+ lines.push(`<supportSub>${supportSub}</supportSub>`, "<playSpeed>1</playSpeed>", `<streamType>${xmlEscape(st)}</streamType>`);
88467
+ if (iframeXml) lines.push(iframeXml);
88468
+ lines.push("</FileInfo>", "</FileInfoList>", "</body>");
88469
+ return lines.join("\n");
88470
+ };
88471
+ var buildFileInfoListStopXml = (params) => {
88472
+ const st = params.streamType ?? "mainStream";
88473
+ return `<?xml version="1.0" encoding="UTF-8" ?>
88474
+ <body>
88475
+ <FileInfoList version="1.1">
88476
+ <FileInfo>
88477
+ <channelId>${params.channel}</channelId>
88478
+ <name>${xmlEscape(params.name)}</name>
88479
+ <streamType>${xmlEscape(st)}</streamType>
88480
+ </FileInfo>
88481
+ </FileInfoList>
88482
+ </body>`;
88483
+ };
88484
+ var REPLAY_SEEK_MAX_DRIFT_MS = 3e3;
88485
+ var buildReplaySeekXml = (params) => {
88486
+ const p = params.parts;
88487
+ const seq = params.seq ?? Math.floor(Date.now() / 1e3);
88488
+ return `<?xml version="1.0" encoding="UTF-8" ?>
88489
+ <body>
88490
+ <ReplaySeek version="1.1">
88491
+ <channelId>${params.channel}</channelId>
88492
+ <seq>${seq}</seq>
88493
+ <seekTime>
88494
+ <year>${p.year}</year>
88495
+ <month>${p.month}</month>
88496
+ <day>${p.day}</day>
88497
+ <hour>${p.hour}</hour>
88498
+ <minute>${p.minute}</minute>
88499
+ <second>${p.second}</second>
88500
+ </seekTime>
88501
+ </ReplaySeek>
88502
+ </body>
88503
+ `;
88504
+ };
88505
+ var BC_IFRAME_MAGIC_MIN = 1667510320;
88506
+ var BC_IFRAME_MAGIC_MAX = 1667510329;
88507
+ var BC_MAX_ADDITIONAL_HEADER = 4096;
88508
+ var readFirstIframeWallClock = (chunk) => {
88509
+ const limit = chunk.length - 28;
88510
+ for (let i = 0; i <= limit; i++) {
88511
+ const magic = chunk.readUInt32LE(i);
88512
+ if (magic < BC_IFRAME_MAGIC_MIN || magic > BC_IFRAME_MAGIC_MAX) continue;
88513
+ const videoType = chunk.toString("utf8", i + 4, i + 8);
88514
+ if (videoType !== "H264" && videoType !== "H265") continue;
88515
+ const additionalHeaderSize = chunk.readUInt32LE(i + 12);
88516
+ if (additionalHeaderSize < 4 || additionalHeaderSize > BC_MAX_ADDITIONAL_HEADER) continue;
88517
+ const t = chunk.readUInt32LE(i + 24);
88518
+ if (t < 1e9 || t > 4e9) continue;
88519
+ const d = /* @__PURE__ */ new Date(t * 1e3);
88520
+ return {
88521
+ year: d.getUTCFullYear(),
88522
+ month: d.getUTCMonth() + 1,
88523
+ day: d.getUTCDate(),
88524
+ hour: d.getUTCHours(),
88525
+ minute: d.getUTCMinutes(),
88526
+ second: d.getUTCSeconds()
88527
+ };
88528
+ }
88529
+ };
88172
88530
  function daysInMonth(year, month) {
88173
88531
  return new Date(Date.UTC(year, month, 0)).getUTCDate();
88174
88532
  }
@@ -90114,71 +90472,6 @@ var buildFileInfoListDownloadXml = (params) => {
90114
90472
  </FileInfoList>
90115
90473
  </body>`;
90116
90474
  };
90117
- var parseRecStartParamIfPresent = (fileName) => {
90118
- const m = /Rec(\w{3})(?:_|_DST)(\d{8})_(\d{6})_.*/.exec(fileName);
90119
- if (!m) return void 0;
90120
- return `${m[2]}${m[3]}`;
90121
- };
90122
- var buildReplayStopNameFromFileName = (fileName, channel = 0) => {
90123
- const trimmed = (fileName ?? "").trim();
90124
- if (/^\d{2}\d{14}$/.test(trimmed)) return trimmed;
90125
- const start = parseRecStartParamIfPresent(fileName);
90126
- if (!start) return void 0;
90127
- return `${String(channel + 1).padStart(2, "0")}${start}`;
90128
- };
90129
- var buildFileInfoListReplayByIdXml = (params) => {
90130
- const st = params.streamType ?? "mainStream";
90131
- const supportSub = st === "subStream" ? 1 : 0;
90132
- const xmlCh = params.xmlChannelId ?? params.channel;
90133
- const iframe = params.iframeReplay;
90134
- const iframeXml = iframe === true || iframe === "both" ? "<bIframeReplay>1</bIframeReplay><iIframeReplay>1</iIframeReplay>" : iframe === "b" ? "<bIframeReplay>1</bIframeReplay>" : iframe === "i" ? "<iIframeReplay>1</iIframeReplay>" : "";
90135
- const lines = [
90136
- "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>",
90137
- "<body>",
90138
- "<FileInfoList version=\"1.1\">",
90139
- "<FileInfo>",
90140
- `<channelId>${xmlCh}</channelId>`,
90141
- `<Id>${xmlEscape(params.id)}</Id>`
90142
- ];
90143
- if (params.uid) lines.push(`<uid>${xmlEscape(params.uid)}</uid>`);
90144
- lines.push(`<supportSub>${supportSub}</supportSub>`, "<playSpeed>1</playSpeed>", `<streamType>${xmlEscape(st)}</streamType>`);
90145
- if (iframeXml) lines.push(iframeXml);
90146
- lines.push("</FileInfo>", "</FileInfoList>", "</body>");
90147
- return lines.join("\n");
90148
- };
90149
- var buildFileInfoListReplayByNameXml = (params) => {
90150
- const st = params.streamType ?? "mainStream";
90151
- const supportSub = st === "subStream" ? 1 : 0;
90152
- const xmlCh = params.xmlChannelId ?? params.channel;
90153
- const iframe = params.iframeReplay;
90154
- const iframeXml = iframe === true || iframe === "both" ? "<bIframeReplay>1</bIframeReplay><iIframeReplay>1</iIframeReplay>" : iframe === "b" ? "<bIframeReplay>1</bIframeReplay>" : iframe === "i" ? "<iIframeReplay>1</iIframeReplay>" : "";
90155
- const lines = [
90156
- "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>",
90157
- "<body>",
90158
- "<FileInfoList version=\"1.1\">",
90159
- "<FileInfo>",
90160
- `<channelId>${xmlCh}</channelId>`,
90161
- `<Id>${xmlEscape(params.name)}</Id>`
90162
- ];
90163
- if (params.uid) lines.push(`<uid>${xmlEscape(params.uid)}</uid>`);
90164
- lines.push(`<supportSub>${supportSub}</supportSub>`, "<playSpeed>1</playSpeed>", `<streamType>${xmlEscape(st)}</streamType>`);
90165
- if (iframeXml) lines.push(iframeXml);
90166
- lines.push("</FileInfo>", "</FileInfoList>", "</body>");
90167
- return lines.join("\n");
90168
- };
90169
- var buildFileInfoListStopXml = (params) => {
90170
- const st = params.streamType ?? "mainStream";
90171
- return `<?xml version="1.0" encoding="UTF-8" ?>
90172
- <body>
90173
- <FileInfoList version="1.1">
90174
- <FileInfo>
90175
- <channelId>${params.channel}</channelId>
90176
- <name>${xmlEscape(params.name)}</name>
90177
- <streamType>${xmlEscape(st)}</streamType>
90178
- </FileInfo>
90179
- </FileInfoList>
90180
- </body>`;
90181
- };
90182
90475
  var videoCodecMap = {
90183
90476
  0: "H.264",
90184
90477
  1: "H.265",
@@ -90419,7 +90712,7 @@ var sendTalkConfigWithReset = async (params) => {
90419
90712
  channel: params.channel,
90420
90713
  ...params.channelIdOverride != null ? { channelIdOverride: params.channelIdOverride } : {},
90421
90714
  payloadXml: params.payloadXml,
90422
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
90715
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24
90423
90716
  })).header.responseCode;
90424
90717
  };
90425
90718
  const code = await trySend();
@@ -90429,7 +90722,7 @@ var sendTalkConfigWithReset = async (params) => {
90429
90722
  channel: params.channel,
90430
90723
  ...params.channelIdOverride != null ? { channelIdOverride: params.channelIdOverride } : {},
90431
90724
  payloadXml: "",
90432
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
90725
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24
90433
90726
  });
90434
90727
  const retryCode = await trySend();
90435
90728
  if (retryCode !== 200) throw new Error(`TalkConfig rejected after reset (responseCode ${retryCode})`);
@@ -90496,7 +90789,7 @@ var createBufferedTalkSession = (params) => {
90496
90789
  ...channelIdOverride != null ? { channelIdOverride } : {},
90497
90790
  extensionXml: buildBinaryExtensionXml(channel),
90498
90791
  payload: Buffer.concat(parts),
90499
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
90792
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24
90500
90793
  });
90501
90794
  const playLengthMs = samplesSent / info.audioConfig.sampleRate * 1e3;
90502
90795
  const now = Date.now();
@@ -90542,7 +90835,7 @@ var createBufferedTalkSession = (params) => {
90542
90835
  channel,
90543
90836
  ...channelIdOverride != null ? { channelIdOverride } : {},
90544
90837
  payloadXml: "",
90545
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
90838
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24
90546
90839
  });
90547
90840
  if (frame.header.responseCode !== 200) throw new Error(`TalkReset rejected (responseCode ${frame.header.responseCode})`);
90548
90841
  if (params.closeSocketOnStop) try {
@@ -91067,6 +91360,7 @@ var isNvrHubModel = (model) => {
91067
91360
  if (NVR_HUB_EXACT_TYPES.includes(upper)) return true;
91068
91361
  return NVR_HUB_MODEL_PATTERNS.some((pattern) => pattern.test(normalized));
91069
91362
  };
91363
+ var SEEKED_CHANNELS = /* @__PURE__ */ new WeakMap();
91070
91364
  var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
91071
91365
  logger;
91072
91366
  httpClient;
@@ -92225,7 +92519,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
92225
92519
  if (debugCfg) {
92226
92520
  const sid = this.client.getSocketSessionId?.();
92227
92521
  const tag2 = sid ? `ReolinkSimpleEvent sid=${sid}` : "ReolinkSimpleEvent";
92228
- require_chunk_OSVYEFMM.eventTraceLog(debugCfg, this.logger, tag2, `dispatch type=${evt.type} channel=${evt.channel} timestamp=${evt.timestamp}`);
92522
+ require_chunk_XAKOGDFB.eventTraceLog(debugCfg, this.logger, tag2, `dispatch type=${evt.type} channel=${evt.channel} timestamp=${evt.timestamp}`);
92229
92523
  }
92230
92524
  for (const cb of this.simpleEventListeners) try {
92231
92525
  Promise.resolve(cb(evt)).catch((e) => {
@@ -92236,7 +92530,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
92236
92530
  }
92237
92531
  }
92238
92532
  constructor(opts) {
92239
- const dbg = require_chunk_OSVYEFMM.normalizeDebugOptions(opts.debugOptions);
92533
+ const dbg = require_chunk_XAKOGDFB.normalizeDebugOptions(opts.debugOptions);
92240
92534
  this.logger = createDebugGateLogger(opts.logger, dbg.general || dbg.traceNativeStream || dbg.traceRecordings || dbg.traceTalk || dbg.traceEvents || dbg.debugRtsp);
92241
92535
  this.clientOptions = {
92242
92536
  host: opts.host,
@@ -92824,7 +93118,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
92824
93118
  return;
92825
93119
  }
92826
93120
  entry.startInFlight = (async () => {
92827
- const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => require("./BaichuanVideoStream-UG3DIMIE-DViuQDJ4.js"));
93121
+ const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => require("./BaichuanVideoStream-PR2SYJAE-JpggQ5Wq.js"));
92828
93122
  const sessionKey = `live:object-detections:ch${entry.channel}:${entry.profile}`;
92829
93123
  const dedicated = await this.createDedicatedSession(sessionKey);
92830
93124
  const stream$12 = new BaichuanVideoStream2({
@@ -93305,7 +93599,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93305
93599
  channel: ch,
93306
93600
  ...channelIdOverride != null ? { channelIdOverride } : {},
93307
93601
  payloadXml: "",
93308
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
93602
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24
93309
93603
  });
93310
93604
  if (frame.header.responseCode !== 200) throw new Error(`TalkReset rejected (responseCode ${frame.header.responseCode})`);
93311
93605
  }
@@ -93324,7 +93618,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93324
93618
  channel: ch,
93325
93619
  ...channelIdOverride != null ? { channelIdOverride } : {},
93326
93620
  payloadXml,
93327
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
93621
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24
93328
93622
  })).header.responseCode;
93329
93623
  };
93330
93624
  const code = await trySend();
@@ -93476,7 +93770,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93476
93770
  cmdId: 10,
93477
93771
  channel,
93478
93772
  payloadXml: "",
93479
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
93773
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24
93480
93774
  });
93481
93775
  return parseTalkAbilityXml(frame.body.length === 0 ? "" : client.tryDecryptXml(frame.body, frame.header.channelId, client.enc));
93482
93776
  }
@@ -94549,8 +94843,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
94549
94843
  const start = params.start;
94550
94844
  const endOfStartDay = endOfWallClockDay(start, timeZone);
94551
94845
  const end = params.end.getTime() > endOfStartDay.getTime() ? endOfStartDay : params.end;
94552
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "getVideoclips", `Query: start=${start.toISOString()}, end=${end.toISOString()} (forced same day)`);
94553
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "getVideoclips", `Using UID for channel ${channel}: ${uid}`);
94846
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "getVideoclips", `Query: start=${start.toISOString()}, end=${end.toISOString()} (forced same day)`);
94847
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "getVideoclips", `Using UID for channel ${channel}: ${uid}`);
94554
94848
  const streamType = params.streamType ?? "subStream";
94555
94849
  const recordType = params.recordType ?? "manual,sched,io,md,people,face,vehicle,dog_cat,visitor,other,package,crossline,intrusion,loitering,legacy,loss";
94556
94850
  const fileRecordTypes = params.fileRecordTypes ?? (params.recordType === void 0 ? DEFAULT_RECORDING_SEARCH_FILE_RECORD_TYPES : void 0);
@@ -94569,7 +94863,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
94569
94863
  ...timeZone !== void 0 ? { timeZone } : {}
94570
94864
  });
94571
94865
  const unique = dedupeRecordingFiles(files);
94572
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "getVideoclips", `FileInfoList complete: ${unique.length} unique files (from ${files.length} total)`);
94866
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "getVideoclips", `FileInfoList complete: ${unique.length} unique files (from ${files.length} total)`);
94573
94867
  return unique;
94574
94868
  });
94575
94869
  }
@@ -94610,7 +94904,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
94610
94904
  const start = params.start;
94611
94905
  const endOfStartDay = endOfWallClockDay(start, timeZone);
94612
94906
  const end = params.end.getTime() > endOfStartDay.getTime() ? endOfStartDay : params.end;
94613
- if (end !== params.end) require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "searchAlarmVideos", `Window clamped to one camera-local day: ${params.end.toISOString()} \u2192 ${end.toISOString()} (the firmware answers the START day only)`);
94907
+ if (end !== params.end) require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "searchAlarmVideos", `Window clamped to one camera-local day: ${params.end.toISOString()} \u2192 ${end.toISOString()} (the firmware answers the START day only)`);
94614
94908
  const windows = await searchAlarmVideosViaFindAlarmVideo({
94615
94909
  sendXml: (p) => this.sendXml(p),
94616
94910
  channel,
@@ -94626,7 +94920,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
94626
94920
  maxPages: params.maxPages ?? 200,
94627
94921
  ...params.timeoutMs != null ? { timeoutMs: params.timeoutMs } : {}
94628
94922
  });
94629
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "searchAlarmVideos", `findAlarmVideo complete: ${windows.length} windows over ${new Set(windows.map((w) => w.fileName)).size} files (channel=${channel})`);
94923
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "searchAlarmVideos", `findAlarmVideo complete: ${windows.length} windows over ${new Set(windows.map((w) => w.fileName)).size} files (channel=${channel})`);
94630
94924
  return windows;
94631
94925
  });
94632
94926
  }
@@ -94691,7 +94985,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
94691
94985
  seen.add(key);
94692
94986
  return true;
94693
94987
  });
94694
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "searchEventLog", `findEventLog complete: ${unique.length} events over ${devices.length} device(s) (${events$6.length - unique.length} repeated across pages), ${unique.filter((e) => !e.hasRecFile).length} without a recording file`);
94988
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "searchEventLog", `findEventLog complete: ${unique.length} events over ${devices.length} device(s) (${events$6.length - unique.length} repeated across pages), ${unique.filter((e) => !e.hasRecFile).length} without a recording file`);
94695
94989
  return unique;
94696
94990
  });
94697
94991
  }
@@ -94744,7 +95038,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
94744
95038
  });
94745
95039
  const msgNum = 0;
94746
95040
  dedicatedClient.subscribeVideoStream(5, msgNum);
94747
- const stream$14 = new require_chunk_OSVYEFMM.BaichuanVideoStream({
95041
+ const stream$14 = new require_chunk_XAKOGDFB.BaichuanVideoStream({
94748
95042
  client: dedicatedClient,
94749
95043
  channel,
94750
95044
  profile: streamType === "subStream" ? "sub" : "main",
@@ -94763,7 +95057,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
94763
95057
  channel,
94764
95058
  channelIdOverride: sessionCounter,
94765
95059
  payloadXml,
94766
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95060
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
94767
95061
  msgNumOverride: 0,
94768
95062
  timeoutMs: params.timeoutMs,
94769
95063
  internal: true
@@ -94795,7 +95089,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
94795
95089
  cmdId: 7,
94796
95090
  channel,
94797
95091
  payloadXml: stopXml,
94798
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95092
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
94799
95093
  timeoutMs: 2e3,
94800
95094
  internal: true
94801
95095
  });
@@ -94852,7 +95146,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
94852
95146
  });
94853
95147
  const msgNum = 0;
94854
95148
  dedicatedClient.subscribeVideoStream(5, msgNum);
94855
- const stream$15 = new require_chunk_OSVYEFMM.BaichuanVideoStream({
95149
+ const stream$15 = new require_chunk_XAKOGDFB.BaichuanVideoStream({
94856
95150
  client: dedicatedClient,
94857
95151
  channel,
94858
95152
  profile: streamType === "subStream" ? "sub" : "main",
@@ -94872,7 +95166,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
94872
95166
  channelIdOverride,
94873
95167
  payloadXml,
94874
95168
  extensionXml: "",
94875
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95169
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
94876
95170
  msgNumOverride: msgNum,
94877
95171
  timeoutMs: params.timeoutMs,
94878
95172
  internal: true
@@ -94904,7 +95198,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
94904
95198
  cmdId: 7,
94905
95199
  channel,
94906
95200
  payloadXml: stopXml,
94907
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95201
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
94908
95202
  timeoutMs: 2e3,
94909
95203
  internal: true
94910
95204
  });
@@ -94991,35 +95285,35 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
94991
95285
  async ensureUidForRecordings(channel, explicitUid) {
94992
95286
  const dbg = this.client.getDebugConfig?.();
94993
95287
  const logger = this.logger;
94994
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Checking UID: explicitUid=${explicitUid}, this.uid=${this.uid}`);
95288
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Checking UID: explicitUid=${explicitUid}, this.uid=${this.uid}`);
94995
95289
  const trimmedExplicit = explicitUid?.trim();
94996
95290
  if (trimmedExplicit) {
94997
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using explicit UID: ${trimmedExplicit}`);
95291
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using explicit UID: ${trimmedExplicit}`);
94998
95292
  return trimmedExplicit;
94999
95293
  }
95000
95294
  const perChannel = await this.discoverUidForRecordingsForChannel(channel);
95001
95295
  if (perChannel) {
95002
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID: ${perChannel}`);
95296
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID: ${perChannel}`);
95003
95297
  return perChannel;
95004
95298
  }
95005
95299
  const configured = this.uid?.trim();
95006
95300
  if (configured) {
95007
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using configured UID: ${configured}`);
95301
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using configured UID: ${configured}`);
95008
95302
  return configured;
95009
95303
  }
95010
95304
  if (this.nativeOnly) {
95011
95305
  const viaGetUid = await this.discoverUidViaGetUidNative(channel);
95012
95306
  if (viaGetUid) {
95013
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using device UID from cmd 114 GetUid: ${viaGetUid}`);
95307
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using device UID from cmd 114 GetUid: ${viaGetUid}`);
95014
95308
  return viaGetUid;
95015
95309
  }
95016
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: no UID available (channel=${channel})`);
95310
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: no UID available (channel=${channel})`);
95017
95311
  throw new Error("UID is required to access recordings in native-only mode. Provide a UID explicitly, configure the client with a UID, or wait for cmd_id=145 push cache to populate per-channel UIDs.");
95018
95312
  }
95019
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `No UID available, attempting device auto-discovery (ch=${channel})`);
95313
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `No UID available, attempting device auto-discovery (ch=${channel})`);
95020
95314
  const discovered = await this.discoverDeviceUidForRecordings(channel);
95021
95315
  if (discovered) return discovered;
95022
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Auto-discovery failed - no UID found`);
95316
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Auto-discovery failed - no UID found`);
95023
95317
  throw new Error("UID is required to access recordings. Provide a UID explicitly or configure the client with a UID.");
95024
95318
  }
95025
95319
  cacheChannelUid(channel, uid) {
@@ -95042,28 +95336,28 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
95042
95336
  const logger = this.logger;
95043
95337
  const fromPush = this.getUidFromPushCacheForChannel(channel);
95044
95338
  if (fromPush) {
95045
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from push cache: ${fromPush}`);
95339
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from push cache: ${fromPush}`);
95046
95340
  return fromPush;
95047
95341
  }
95048
95342
  if (this.nativeOnly) {
95049
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: skipping per-channel UID discovery via HTTP/CGI (channel=${channel})`);
95343
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: skipping per-channel UID discovery via HTTP/CGI (channel=${channel})`);
95050
95344
  return;
95051
95345
  }
95052
95346
  try {
95053
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Attempting per-channel UID discovery via HTTP CGI GetChannelstatus (channel=${channel})`);
95347
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Attempting per-channel UID discovery via HTTP CGI GetChannelstatus (channel=${channel})`);
95054
95348
  const uidCandidate = await discoverPerChannelUidViaCgiChannelstatus({
95055
95349
  channel,
95056
95350
  login: () => this.cgiApi.login(),
95057
95351
  getChannelstatus: () => this.cgiApi.GetChannelstatus()
95058
95352
  });
95059
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus channel=${channel} uid=${uidCandidate || "(missing)"}`);
95353
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus channel=${channel} uid=${uidCandidate || "(missing)"}`);
95060
95354
  if (uidCandidate) {
95061
95355
  this.cacheChannelUid(channel, uidCandidate);
95062
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from GetChannelstatus: ${uidCandidate}`);
95356
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from GetChannelstatus: ${uidCandidate}`);
95063
95357
  return uidCandidate;
95064
95358
  }
95065
95359
  } catch (e) {
95066
- require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus failed: ${formatErrorForLog(e)}`);
95360
+ require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus failed: ${formatErrorForLog(e)}`);
95067
95361
  }
95068
95362
  }
95069
95363
  /**
@@ -95075,7 +95369,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
95075
95369
  */
95076
95370
  async discoverUidViaGetUidNative(channel) {
95077
95371
  const dbg = this.client.getDebugConfig?.();
95078
- const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, this.logger, "ensureUidForRecordings", message);
95372
+ const trace = (message) => require_chunk_XAKOGDFB.recordingsTraceLog(dbg, this.logger, "ensureUidForRecordings", message);
95079
95373
  let isNvr = this._isNvr;
95080
95374
  if (isNvr === void 0) try {
95081
95375
  isNvr = await this.isNvrDevice();
@@ -95101,7 +95395,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
95101
95395
  if (this.nativeOnly) return void 0;
95102
95396
  const dbg = this.client.getDebugConfig?.();
95103
95397
  const logger = this.logger;
95104
- const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", message);
95398
+ const trace = (message) => require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", message);
95105
95399
  const discoveredUid = await discoverDeviceUidForRecordings({
95106
95400
  channel,
95107
95401
  getInfo: () => this.getInfo(),
@@ -95176,7 +95470,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
95176
95470
  await this.client.login();
95177
95471
  const dbg = this.client.getDebugConfig?.();
95178
95472
  const logger = this.logger;
95179
- const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "getVideoclipThumbnail", message);
95473
+ const trace = (message) => require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "getVideoclipThumbnail", message);
95180
95474
  const channel = this.normalizeChannel(params.channel);
95181
95475
  const snapStreamType = (params.snapType ?? "sub") === "main" ? "mainStream" : "subStream";
95182
95476
  const timeoutMs = params.timeoutMs ?? 3e4;
@@ -95231,7 +95525,7 @@ ${xml}`);
95231
95525
  cmdId: 298,
95232
95526
  ...isNvr && headerChannelIdOverride != null ? { channelIdOverride: headerChannelIdOverride } : {},
95233
95527
  msgNumOverride: 0,
95234
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95528
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
95235
95529
  streamType: 0,
95236
95530
  payloadXml: xml,
95237
95531
  timeoutMs
@@ -95710,7 +96004,7 @@ ${stderr}`));
95710
96004
  await this.client.login();
95711
96005
  const dbg = this.client.getDebugConfig?.();
95712
96006
  const logger = this.logger;
95713
- const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "fileInfoListDownload", message);
96007
+ const trace = (message) => require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "fileInfoListDownload", message);
95714
96008
  const channel = this.normalizeChannel(params.channel);
95715
96009
  const uid = await this.ensureUidForRecordings(channel, params.uid);
95716
96010
  const headerChannelIdOverride = this.resolveHeaderChannelIdForLogicalChannel(channel);
@@ -95756,7 +96050,7 @@ ${stderr}`));
95756
96050
  cmdId: 13,
95757
96051
  channel,
95758
96052
  ...typeof chId === "number" ? { channelIdOverride: chId } : {},
95759
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_FILE_DOWNLOAD,
96053
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_FILE_DOWNLOAD,
95760
96054
  extensionXml: buildBinaryExtensionXml(channel),
95761
96055
  payloadXml,
95762
96056
  streamType: st,
@@ -95785,7 +96079,7 @@ ${stderr}`));
95785
96079
  await this.client.login();
95786
96080
  const dbg = this.client.getDebugConfig?.();
95787
96081
  const logger = this.logger;
95788
- const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "fileInfoListPagedDownload", message);
96082
+ const trace = (message) => require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "fileInfoListPagedDownload", message);
95789
96083
  const channel = this.normalizeChannel(params.channel);
95790
96084
  const uid = await this.ensureUidForRecordings(channel, params.uid);
95791
96085
  trace(`Starting paged download: channel=${channel}, uid=${uid}, fileName=${params.fileName}`);
@@ -95822,7 +96116,7 @@ ${stderr}`));
95822
96116
  await this.client.login();
95823
96117
  const dbg = this.client.getDebugConfig?.();
95824
96118
  const logger = this.logger;
95825
- const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "fileInfoListReplayBinaryDownload", message);
96119
+ const trace = (message) => require_chunk_XAKOGDFB.recordingsTraceLog(dbg, logger, "fileInfoListReplayBinaryDownload", message);
95826
96120
  const channel = this.normalizeChannel(params.channel);
95827
96121
  const headerChannelIdOverride = this.resolveHeaderChannelIdForLogicalChannel(channel);
95828
96122
  const ident = params.fileName;
@@ -95847,24 +96141,66 @@ ${stderr}`));
95847
96141
  trace(`download: channel=${channel} uid=${uid || "(missing)"} ident=${ident} streamType=${streamType} isNvr=${isNvr} timeoutMs=${timeoutMs}`);
95848
96142
  const pinnedHeaderChannelId = params.headerChannelIdOverride;
95849
96143
  let abandoned = false;
96144
+ const timeZone = params.timeZone ?? this.recordingsTimeZone;
96145
+ const clipStart = this.replayStartInstantOf(ident, timeZone);
96146
+ const requestedAt = params.seekTo ?? clipStart;
96147
+ const outcome = {
96148
+ requestedAt: params.seekTo ?? null,
96149
+ deliveredAt: null,
96150
+ driftMs: null,
96151
+ seekApplied: false
96152
+ };
96153
+ const dirty = SEEKED_CHANNELS.get(this.client)?.has(channel) === true;
96154
+ if (requestedAt && (params.seekTo !== void 0 || dirty)) {
96155
+ outcome.seekApplied = await this.replaySeek({
96156
+ channel,
96157
+ at: requestedAt,
96158
+ ...timeZone !== void 0 ? { timeZone } : {}
96159
+ });
96160
+ if (!outcome.seekApplied) {
96161
+ outcome.reason = "camera did not accept cmd 123 <ReplaySeek>; the transfer runs unpositioned";
96162
+ trace(`seek not applied for ${ident}: ${outcome.reason}`);
96163
+ }
96164
+ } else if (!requestedAt) {
96165
+ outcome.reason = `no start instant derivable from ${ident}; the transfer runs unpositioned`;
96166
+ trace(outcome.reason);
96167
+ } else trace(`channel ${String(channel)} never seeked on this connection; ${ident} runs unpositioned at full rate`);
96168
+ let deliveredParts;
96169
+ const observeChunk = (chunk) => {
96170
+ if (deliveredParts === void 0) deliveredParts = readFirstIframeWallClock(chunk);
96171
+ };
96172
+ const settleOutcome = () => {
96173
+ if (deliveredParts !== void 0) {
96174
+ outcome.deliveredAt = dateFromWallClock(deliveredParts, timeZone);
96175
+ if (outcome.requestedAt) {
96176
+ outcome.driftMs = outcome.deliveredAt.getTime() - outcome.requestedAt.getTime();
96177
+ if (Math.abs(outcome.driftMs) > 3e3) this.logger?.warn?.(`[fileInfoListReplayBinaryDownload] seek drift ${outcome.driftMs} ms for ${ident}: asked ${outcome.requestedAt.toISOString()}, delivered ${outcome.deliveredAt.toISOString()} (> ${REPLAY_SEEK_MAX_DRIFT_MS} ms \u2014 the camera did not honour the position)`);
96178
+ }
96179
+ } else if (outcome.reason === void 0) outcome.reason = "no I-frame in the transfer; delivered position unknown";
96180
+ params.onSeekOutcome?.(outcome);
96181
+ };
95850
96182
  try {
95851
96183
  return await this.client.sendBinary({
95852
96184
  cmdId: 5,
95853
96185
  channel,
95854
96186
  ...pinnedHeaderChannelId != null ? { channelIdOverride: pinnedHeaderChannelId } : {},
95855
96187
  msgNumOverride: 0,
95856
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
96188
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
95857
96189
  payloadXml,
95858
96190
  streamType: 0,
95859
96191
  timeoutMs,
95860
96192
  ...params.idleTimeoutMs != null ? { idleTimeoutMs: params.idleTimeoutMs } : {},
95861
- ...params.onChunk ? { onChunk: params.onChunk } : {}
96193
+ onChunk: (chunk) => {
96194
+ observeChunk(chunk);
96195
+ params.onChunk?.(chunk);
96196
+ }
95862
96197
  });
95863
96198
  } catch (e) {
95864
96199
  trace(`download failed: ${e instanceof Error ? e.message : String(e)}`);
95865
96200
  abandoned = true;
95866
96201
  throw e;
95867
96202
  } finally {
96203
+ settleOutcome();
95868
96204
  await this.stopFileInfoListReplay({
95869
96205
  channel,
95870
96206
  fileName: ident,
@@ -95874,6 +96210,76 @@ ${stderr}`));
95874
96210
  }
95875
96211
  }
95876
96212
  /**
96213
+ * The instant a recording begins, read out of its own file name.
96214
+ *
96215
+ * `Rec*_YYYYMMDD_HHMMSS_…` is the camera's wall clock with no offset on the
96216
+ * wire, so it is resolved through the same zone as every other recording
96217
+ * timestamp. `undefined` for a name that does not carry one — an identifier
96218
+ * this library did not get from a FileInfoList listing, for instance.
96219
+ */
96220
+ replayStartInstantOf(fileName, timeZone) {
96221
+ const stamp = parseRecStartParamIfPresent(fileName);
96222
+ if (!stamp) return void 0;
96223
+ return dateFromWallClock({
96224
+ year: Number(stamp.slice(0, 4)),
96225
+ month: Number(stamp.slice(4, 6)),
96226
+ day: Number(stamp.slice(6, 8)),
96227
+ hour: Number(stamp.slice(8, 10)),
96228
+ minute: Number(stamp.slice(10, 12)),
96229
+ second: Number(stamp.slice(12, 14))
96230
+ }, timeZone);
96231
+ }
96232
+ /**
96233
+ * Position the NEXT replay of this channel — cmd 123 `<ReplaySeek>`.
96234
+ *
96235
+ * The camera can replay one recording from an arbitrary instant inside it,
96236
+ * and this is how the official app asks: a wall-clock second, sent BEFORE
96237
+ * the cmd 5 that opens the stream. Measured 2026-09-23 — asked +40 s into a
96238
+ * 75 s clip, the first I-frame delivered was the clip's start +40 s, on an
96239
+ * E1 Outdoor PoE (v3.1.0.5223) and on a Home Hub child (v3.3.0.456).
96240
+ *
96241
+ * Two things a caller must know:
96242
+ *
96243
+ * - **The granularity is a KEYFRAME, not a second.** The stream begins at
96244
+ * the I-frame nearest the instant asked for — 592 rounded up, the hub
96245
+ * child rounded down, both by up to one GOP (~2 s). Read the position back
96246
+ * (`fileInfoListReplayBinaryDownload` reports it) rather than assuming it.
96247
+ * - **The position is STICKY for the life of the connection.** Measured: a
96248
+ * seek, then a replay, then ANOTHER replay that asked for no seek — and
96249
+ * the second one still started at the seek point. This is why
96250
+ * `fileInfoListReplayBinaryDownload` always sends a `<ReplaySeek>`, to the
96251
+ * clip's own start when the caller asked for no offset. Calling this
96252
+ * method directly leaves that discipline to you.
96253
+ *
96254
+ * Returns `true` when the camera accepted the command. A camera that does
96255
+ * not support cmd 123 returns `false` rather than throwing, because a seek
96256
+ * is an enhancement to a download and must never be the reason one fails.
96257
+ */
96258
+ async replaySeek(params) {
96259
+ const channel = this.normalizeChannel(params.channel);
96260
+ const timeZone = params.timeZone ?? this.recordingsTimeZone;
96261
+ const payloadXml = buildReplaySeekXml({
96262
+ channel,
96263
+ parts: wallClockParts(params.at, timeZone)
96264
+ });
96265
+ try {
96266
+ await this.client.sendXml({
96267
+ cmdId: 123,
96268
+ extensionXml: "",
96269
+ payloadXml,
96270
+ timeoutMs: params.timeoutMs ?? 8e3
96271
+ });
96272
+ const client = this.client;
96273
+ const seeked = SEEKED_CHANNELS.get(client) ?? /* @__PURE__ */ new Set();
96274
+ seeked.add(channel);
96275
+ SEEKED_CHANNELS.set(client, seeked);
96276
+ return true;
96277
+ } catch (e) {
96278
+ this.logger?.warn?.(`[replaySeek] channel=${channel} at=${params.at.toISOString()} not applied: ${e instanceof Error ? e.message : String(e)}`);
96279
+ return false;
96280
+ }
96281
+ }
96282
+ /**
95877
96283
  * Close a cmd 5 replay session at the camera (cmd 7) and wait for the wire to
95878
96284
  * go quiet.
95879
96285
  *
@@ -95899,7 +96305,7 @@ ${stderr}`));
95899
96305
  name: stopName,
95900
96306
  streamType: params.streamType
95901
96307
  }),
95902
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
96308
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
95903
96309
  timeoutMs: 4e3,
95904
96310
  internal: true
95905
96311
  });
@@ -95987,7 +96393,7 @@ ${stderr}`));
95987
96393
  const frames = [];
95988
96394
  const timestampsUs = [];
95989
96395
  const audioFrames = [];
95990
- const decoder = new require_chunk_OSVYEFMM.BcMediaAnnexBDecoder({
96396
+ const decoder = new require_chunk_XAKOGDFB.BcMediaAnnexBDecoder({
95991
96397
  strict: false,
95992
96398
  logger: this.logger,
95993
96399
  onVideoAccessUnit: ({ annexB, microseconds }) => {
@@ -96053,7 +96459,7 @@ ${stderr}`));
96053
96459
  const videoFrames = [];
96054
96460
  const audioFrames = [];
96055
96461
  let audioCodec = null;
96056
- const decoder = new require_chunk_OSVYEFMM.BcMediaAnnexBDecoder({
96462
+ const decoder = new require_chunk_XAKOGDFB.BcMediaAnnexBDecoder({
96057
96463
  strict: false,
96058
96464
  logger: this.logger,
96059
96465
  onVideoAccessUnit: ({ annexB, microseconds }) => {
@@ -96451,7 +96857,7 @@ ${stderr}`));
96451
96857
  params: {
96452
96858
  cmdId: 31,
96453
96859
  channelIdOverride: channel,
96454
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
96860
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24
96455
96861
  }
96456
96862
  },
96457
96863
  {
@@ -96459,7 +96865,7 @@ ${stderr}`));
96459
96865
  params: {
96460
96866
  cmdId: 31,
96461
96867
  channelIdOverride: 251,
96462
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
96868
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24
96463
96869
  }
96464
96870
  },
96465
96871
  {
@@ -96467,7 +96873,7 @@ ${stderr}`));
96467
96873
  params: {
96468
96874
  cmdId: 31,
96469
96875
  channelIdOverride: 250,
96470
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
96876
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24
96471
96877
  }
96472
96878
  },
96473
96879
  {
@@ -96475,7 +96881,7 @@ ${stderr}`));
96475
96881
  params: {
96476
96882
  cmdId: 31,
96477
96883
  channelIdOverride: 250,
96478
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
96884
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
96479
96885
  extensionXml: `<?xml version="1.0" encoding="UTF-8" ?><Extension version="1.1"><channelId>251</channelId></Extension>`
96480
96886
  }
96481
96887
  }
@@ -96518,7 +96924,7 @@ ${stderr}`));
96518
96924
  const frame = await this.client.sendFrame({
96519
96925
  cmdId: 31,
96520
96926
  channelIdOverride: Number(channel),
96521
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
96927
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
96522
96928
  timeoutMs
96523
96929
  });
96524
96930
  lastCode = frame.header.responseCode;
@@ -96799,7 +97205,7 @@ ${stderr}`));
96799
97205
  msgNumOverride: msgNum,
96800
97206
  extensionXml: buildChannelExtensionXml(channelId),
96801
97207
  payloadXml,
96802
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
97208
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
96803
97209
  streamType: config.streamType,
96804
97210
  timeoutMs: 2e4
96805
97211
  };
@@ -96931,7 +97337,7 @@ ${stderr}`));
96931
97337
  channelIdOverride: channelId,
96932
97338
  extensionXml: a.extensionXml,
96933
97339
  payloadXml: a.payloadXml,
96934
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
97340
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
96935
97341
  streamType: a.streamType,
96936
97342
  ...msgNum !== void 0 ? { msgNumOverride: msgNum } : {},
96937
97343
  timeoutMs: 2e3
@@ -96961,7 +97367,7 @@ ${stderr}`));
96961
97367
  channel: ch,
96962
97368
  channelIdOverride: channelId,
96963
97369
  extensionXml: buildChannelExtensionXml(channelId),
96964
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
97370
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
96965
97371
  streamType: 0
96966
97372
  });
96967
97373
  } catch (e) {
@@ -97003,7 +97409,7 @@ ${stderr}`));
97003
97409
  channel: ch,
97004
97410
  channelIdOverride: channelId,
97005
97411
  extensionXml: buildChannelExtensionXml(channelId),
97006
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
97412
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
97007
97413
  streamType: 0
97008
97414
  });
97009
97415
  } catch (e) {
@@ -97025,7 +97431,7 @@ ${stderr}`));
97025
97431
  channelIdOverride: channelId,
97026
97432
  extensionXml,
97027
97433
  payloadXml,
97028
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
97434
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
97029
97435
  streamType: 0
97030
97436
  });
97031
97437
  if (frame.header.responseCode !== 200) {
@@ -97057,7 +97463,7 @@ ${stderr}`));
97057
97463
  channelIdOverride: channelId,
97058
97464
  extensionXml,
97059
97465
  payloadXml,
97060
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
97466
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
97061
97467
  streamType: 0
97062
97468
  });
97063
97469
  if (frame.header.responseCode !== 200) throw new Error(`PTZ preset move rejected (response_code ${frame.header.responseCode})`);
@@ -97078,7 +97484,7 @@ ${stderr}`));
97078
97484
  channelIdOverride: channelId,
97079
97485
  extensionXml,
97080
97486
  payloadXml,
97081
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
97487
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
97082
97488
  streamType: 0
97083
97489
  });
97084
97490
  if (frame.header.responseCode !== 200) throw new Error(`PTZ preset save rejected (response_code ${frame.header.responseCode})`);
@@ -97095,7 +97501,7 @@ ${stderr}`));
97095
97501
  channelIdOverride: channelId,
97096
97502
  extensionXml,
97097
97503
  payloadXml,
97098
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
97504
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
97099
97505
  streamType: 0
97100
97506
  });
97101
97507
  if (frame.header.responseCode !== 200) throw new Error(`PTZ goto preset rejected (response_code ${frame.header.responseCode})`);
@@ -97122,7 +97528,7 @@ ${stderr}`));
97122
97528
  channelIdOverride: channelId,
97123
97529
  extensionXml,
97124
97530
  payloadXml,
97125
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
97531
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
97126
97532
  streamType: 0
97127
97533
  })).header.responseCode };
97128
97534
  },
@@ -97176,7 +97582,7 @@ ${stderr}`));
97176
97582
  channel: ch,
97177
97583
  channelIdOverride: ch,
97178
97584
  extensionXml: buildPtzGuardExtensionXml(ch, "read"),
97179
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
97585
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
97180
97586
  streamType: 0
97181
97587
  }));
97182
97588
  }
@@ -97202,7 +97608,7 @@ ${stderr}`));
97202
97608
  timeoutSeconds: options.timeoutSeconds,
97203
97609
  setPosition: options.setPosition === true
97204
97610
  }),
97205
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
97611
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
97206
97612
  streamType: 0
97207
97613
  });
97208
97614
  }
@@ -97223,7 +97629,7 @@ ${stderr}`));
97223
97629
  channelIdOverride: ch,
97224
97630
  extensionXml: buildPtzGuardExtensionXml(ch, "write"),
97225
97631
  payloadXml: buildPtzGuardGoXml(ch),
97226
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
97632
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
97227
97633
  streamType: 0
97228
97634
  });
97229
97635
  }
@@ -97239,7 +97645,7 @@ ${stderr}`));
97239
97645
  channel: ch,
97240
97646
  channelIdOverride: channelId,
97241
97647
  extensionXml: buildChannelExtensionXml(channelId),
97242
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
97648
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
97243
97649
  streamType: 0
97244
97650
  });
97245
97651
  const parseTriplet = (sectionTag) => {
@@ -97277,7 +97683,7 @@ ${stderr}`));
97277
97683
  channelIdOverride: channelId,
97278
97684
  extensionXml,
97279
97685
  payloadXml,
97280
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
97686
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24,
97281
97687
  streamType: 0
97282
97688
  });
97283
97689
  if (frame.header.responseCode !== 200) throw new Error(`Zoom rejected (response_code ${frame.header.responseCode})`);
@@ -99028,7 +99434,7 @@ ${xml}`);
99028
99434
  * @returns Test results for all stream types and profiles
99029
99435
  */
99030
99436
  async testChannelStreams(channel, logger) {
99031
- const { testChannelStreams } = await Promise.resolve().then(() => require("./DiagnosticsTools-YIWXO7IJ-CD4rRF8a.js"));
99437
+ const { testChannelStreams } = await Promise.resolve().then(() => require("./DiagnosticsTools-DS5X2UVY-CD4rRF8a.js"));
99032
99438
  return await testChannelStreams({
99033
99439
  api: this,
99034
99440
  channel: this.normalizeChannel(channel),
@@ -99044,7 +99450,7 @@ ${xml}`);
99044
99450
  * @returns Complete diagnostics for all channels and streams
99045
99451
  */
99046
99452
  async collectMultifocalDiagnostics(logger) {
99047
- const { collectMultifocalDiagnostics } = await Promise.resolve().then(() => require("./DiagnosticsTools-YIWXO7IJ-CD4rRF8a.js"));
99453
+ const { collectMultifocalDiagnostics } = await Promise.resolve().then(() => require("./DiagnosticsTools-DS5X2UVY-CD4rRF8a.js"));
99048
99454
  return await collectMultifocalDiagnostics({
99049
99455
  api: this,
99050
99456
  logger
@@ -101254,7 +101660,7 @@ ${scheduleItems}
101254
101660
  if (raw.length === 0) throw new Error("Downloaded recording is empty");
101255
101661
  const videoFrames = [];
101256
101662
  let videoType = null;
101257
- const decoder = new require_chunk_OSVYEFMM.BcMediaAnnexBDecoder({
101663
+ const decoder = new require_chunk_XAKOGDFB.BcMediaAnnexBDecoder({
101258
101664
  strict: false,
101259
101665
  logger: this.logger,
101260
101666
  onVideoAccessUnit: ({ annexB: annexB2, microseconds }) => {
@@ -101602,7 +102008,7 @@ ${scheduleItems}
101602
102008
  return {
101603
102009
  getPlaylist: () => {
101604
102010
  try {
101605
- const { readFileSync } = require_chunk_OSVYEFMM.__require("fs");
102011
+ const { readFileSync } = require_chunk_XAKOGDFB.__require("fs");
101606
102012
  return readFileSync(playlistPath, "utf8");
101607
102013
  } catch {
101608
102014
  return "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:4\n";
@@ -101611,7 +102017,7 @@ ${scheduleItems}
101611
102017
  getSegment: (name) => {
101612
102018
  if (segments.has(name)) return segments.get(name);
101613
102019
  try {
101614
- const { readFileSync } = require_chunk_OSVYEFMM.__require("fs");
102020
+ const { readFileSync } = require_chunk_XAKOGDFB.__require("fs");
101615
102021
  const data = readFileSync(path$3.join(tempDir, name));
101616
102022
  segments.set(name, data);
101617
102023
  return data;
@@ -101621,7 +102027,7 @@ ${scheduleItems}
101621
102027
  },
101622
102028
  listSegments: () => {
101623
102029
  try {
101624
- const { readdirSync } = require_chunk_OSVYEFMM.__require("fs");
102030
+ const { readdirSync } = require_chunk_XAKOGDFB.__require("fs");
101625
102031
  return readdirSync(tempDir).filter((f) => f.endsWith(".ts"));
101626
102032
  } catch {
101627
102033
  return [];
@@ -103206,41 +103612,41 @@ async function autoDetectDeviceType(inputs) {
103206
103612
  variant: "cmd80 class=0x6414",
103207
103613
  op: () => api.getInfo(void 0, {
103208
103614
  timeoutMs: 2500,
103209
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
103615
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24
103210
103616
  })
103211
103617
  },
103212
103618
  {
103213
103619
  variant: "cmd80 class=0x6614",
103214
103620
  op: () => api.getInfo(void 0, {
103215
103621
  timeoutMs: 3e3,
103216
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_20
103622
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_20
103217
103623
  })
103218
103624
  },
103219
103625
  {
103220
103626
  variant: "cmd318(ch0) class=0x6414",
103221
103627
  op: () => api.getInfo(0, {
103222
103628
  timeoutMs: 3e3,
103223
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
103629
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24
103224
103630
  })
103225
103631
  },
103226
103632
  {
103227
103633
  variant: "cmd318(ch0) class=0x6614",
103228
103634
  op: () => api.getInfo(0, {
103229
103635
  timeoutMs: 3500,
103230
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_20
103636
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_20
103231
103637
  })
103232
103638
  }
103233
103639
  ]), runProbeVariants("getSupportInfo", [{
103234
103640
  variant: "cmd199 class=0x6414",
103235
103641
  op: () => api.getSupportInfo({
103236
103642
  timeoutMs: 2500,
103237
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
103643
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_24
103238
103644
  })
103239
103645
  }, {
103240
103646
  variant: "cmd199 class=0x6614",
103241
103647
  op: () => api.getSupportInfo({
103242
103648
  timeoutMs: 3500,
103243
- messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_20
103649
+ messageClass: require_chunk_XAKOGDFB.BC_CLASS_MODERN_20
103244
103650
  })
103245
103651
  }])]);
103246
103652
  const deviceInfo = infoProbe?.value;
@@ -149769,7 +150175,7 @@ async function createRfc4571TcpServerInternal(options) {
149769
150175
  streamClient = dedicatedSession.client;
149770
150176
  logSessionsSummary(`dedicated session created: ${sessionKey}`);
149771
150177
  } else streamClient = baseApi.client;
149772
- const createLiveStream = async () => new require_chunk_OSVYEFMM.BaichuanVideoStream({
150178
+ const createLiveStream = async () => new require_chunk_XAKOGDFB.BaichuanVideoStream({
149773
150179
  client: streamClient,
149774
150180
  api: baseApi,
149775
150181
  channel: ch,
@@ -152609,7 +153015,7 @@ function writableAt(stdio, index) {
152609
153015
  if (candidate === null || candidate === void 0) return null;
152610
153016
  return isWritable(candidate) ? candidate : null;
152611
153017
  }
152612
- var defaultDecoder = (handlers) => new require_chunk_OSVYEFMM.BcMediaAnnexBDecoder({
153018
+ var defaultDecoder = (handlers) => new require_chunk_XAKOGDFB.BcMediaAnnexBDecoder({
152613
153019
  strict: false,
152614
153020
  onVideoAccessUnit: handlers.onVideoAccessUnit,
152615
153021
  onAudioFrame: (frame) => handlers.onAudioFrame({
@@ -152623,6 +153029,7 @@ function createClipStreamProducer(deps) {
152623
153029
  const createDecoder = deps.createDecoder ?? defaultDecoder;
152624
153030
  const drainTimeoutMs = deps.drainTimeoutMs ?? 1e4;
152625
153031
  const flushTimeoutMs = deps.flushTimeoutMs ?? 15e3;
153032
+ const cutGraceMs = deps.cutGraceMs ?? 5e3;
152626
153033
  const lanes = /* @__PURE__ */ new Map();
152627
153034
  const activeByHost = /* @__PURE__ */ new Map();
152628
153035
  const runAttempt = (input, attempt) => new ClipStreamRun({
@@ -152630,7 +153037,8 @@ function createClipStreamProducer(deps) {
152630
153037
  spawnMux: deps.spawnMux,
152631
153038
  createDecoder,
152632
153039
  drainTimeoutMs,
152633
- flushTimeoutMs
153040
+ flushTimeoutMs,
153041
+ cutGraceMs
152634
153042
  }, input, attempt).run();
152635
153043
  const runAttempts = async (input) => {
152636
153044
  const { deviceId } = input.device;
@@ -152731,6 +153139,8 @@ var ClipStreamRun = class {
152731
153139
  input;
152732
153140
  attempt;
152733
153141
  accessUnits = 0;
153142
+ /** Armed by {@link sever}; cleared when the run finally returns. */
153143
+ cutWatch = null;
152734
153144
  /** Every audio frame the decoder handed over, of any codec. */
152735
153145
  audioFrames = 0;
152736
153146
  audioFramesDropped = 0;
@@ -152835,6 +153245,8 @@ var ClipStreamRun = class {
152835
153245
  return await this.onTransferEnded();
152836
153246
  } finally {
152837
153247
  this.done = true;
153248
+ if (this.cutWatch !== null) clearTimeout(this.cutWatch);
153249
+ this.cutWatch = null;
152838
153250
  this.disposeGates();
152839
153251
  }
152840
153252
  }
@@ -153112,6 +153524,36 @@ var ClipStreamRun = class {
153112
153524
  this.input.sink.end();
153113
153525
  }
153114
153526
  /** Sever every hop, once, and say why. */
153527
+ /**
153528
+ * The lane this run is still holding, some time after it was cut.
153529
+ *
153530
+ * D391: a branch that ACCEPTED work and produces nothing is the same branch
153531
+ * as one that drops it. `sever()` already says the stream was cut; it does
153532
+ * not say that the camera never let go, which is the part that makes every
153533
+ * later tap on this host fail.
153534
+ */
153535
+ armCutWatch(reason) {
153536
+ const cutAt = Date.now();
153537
+ this.cutWatch = setTimeout(() => {
153538
+ this.cutWatch = null;
153539
+ if (this.done) return;
153540
+ this.deps.logger.warn("videoclips: a cut clip stream is still holding its camera lane", {
153541
+ tags: this.tags,
153542
+ meta: {
153543
+ clipKey: this.input.row.clipKey,
153544
+ twin: this.input.twin,
153545
+ branch: "cut-not-honoured",
153546
+ reason,
153547
+ hostKey: this.input.device.hostKey,
153548
+ heldSinceCutMs: Date.now() - cutAt,
153549
+ accessUnits: this.accessUnits,
153550
+ bytesOut: this.bytesOut,
153551
+ begun: this.begun
153552
+ }
153553
+ });
153554
+ }, this.deps.cutGraceMs);
153555
+ this.cutWatch.unref?.();
153556
+ }
153115
153557
  sever(reason, detail) {
153116
153558
  if (this.cut !== null || this.done) return;
153117
153559
  this.cut = {
@@ -153136,6 +153578,7 @@ var ClipStreamRun = class {
153136
153578
  }
153137
153579
  });
153138
153580
  if (this.begun) this.input.sink.destroy();
153581
+ this.armCutWatch(reason);
153139
153582
  }
153140
153583
  cutOutcome(cut) {
153141
153584
  if (this.begun) return {
@@ -155043,11 +155486,24 @@ function createReolinkVideoclipsProvider(deps) {
155043
155486
  * reads only whether the dial and the file read are wired, and never the
155044
155487
  * clip or the profile.
155045
155488
  *
155046
- * The rates are the BROKER's re-pacing ladder. The camera's own
155047
- * `<playSpeed>` (8, 16) is real and still unwired (D597, D600): it would
155048
- * ride on the dial and cost a new stream per change, and its audio has no
155049
- * resampler on this path. When it is wired its rates join
155050
- * `CLIP_BROKER_PACED_RATES` — not a second list here.
155489
+ * The rates are the BROKER's re-pacing ladder, and they are the only
155490
+ * rates there are. The camera's own `<playSpeed>` is NOT a second rate
155491
+ * mechanism waiting to be wired — measured on 592, 2026-09-23 (D620):
155492
+ * at 1, 2, 4 and 8 it returns a byte-identical transfer (947 472 B, 594
155493
+ * access units, the same 39.855 s presentation span, the same 624 AAC
155494
+ * frames) and changes only how fast those bytes cross. It buys SUPPLY,
155495
+ * never speed. `playSpeed 16` is not a rate either: the camera switches
155496
+ * to one frame per GOP across the whole clip (20 units, 2 025 ms apart,
155497
+ * no audio) — a different STREAM, dialled as one, never a rate applied to
155498
+ * this one.
155499
+ *
155500
+ * So 8x and 16x do not arrive here. They arrive when the broker's pacer
155501
+ * is allowed above 4 (`clampPlaybackRate`, the `setRate` bound and
155502
+ * `CLIP_BROKER_PACED_RATES`, together) — and only once the supply behind
155503
+ * them is real: D620 measured this camera delivering at 1.2–1.37x while
155504
+ * the library positions every replay with cmd 123, against 95–116x when
155505
+ * it does not. A ladder widened before that is a rate accepted and not
155506
+ * delivered, which is the whole defect D612 exists to end.
155051
155507
  */
155052
155508
  getPlaybackOptions: async () => CLIP_PLAYBACK_OPTIONS.stream,
155053
155509
  /**