@camstack/addon-provider-reolink 1.2.160 → 1.2.162

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_ZBACPERX = require("./chunk-ZBACPERX-BM6Ll-rh.js");
3
+ const require_chunk_OSVYEFMM = require("./chunk-OSVYEFMM-Czx77gto.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-De1BvmWo.mjs
5394
+ //#region ../types/dist/sleep-i3eUVc-d.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).
@@ -14154,6 +14154,210 @@ method(object({ integrationId: string() }), object({ filters: array(AdoptionFilt
14154
14154
  auth: "admin"
14155
14155
  });
14156
14156
  /**
14157
+ * device-admin-link — "this device has a management page of its own, and here
14158
+ * is its address".
14159
+ *
14160
+ * ## Why this is not a `deviceConfig` cap
14161
+ *
14162
+ * There is nothing to edit. A `deviceConfig` cap (D14) exists so the framework
14163
+ * can DERIVE a settings form from `getOptions` + `getStatus` and route a flat
14164
+ * patch back through a setter; it costs a `builderId` reducer in
14165
+ * `device-config-contribution.ts` and a `*-config-schema.ts` beside it, and it
14166
+ * renders a form section. This cap answers ONE question with ONE read and
14167
+ * renders a button. Nothing about it is a form, so it carries no `deviceConfig`
14168
+ * block, no `settings`, no `runtimeState` and no reducer — exactly like
14169
+ * `reboot`, the other pure-RPC device-native cap.
14170
+ *
14171
+ * ## Absent, and the difference between "no page" and "we cannot say"
14172
+ *
14173
+ * The two are answered at DIFFERENT layers, on purpose:
14174
+ *
14175
+ * - **"We cannot say"** → the provider never registers the cap for that
14176
+ * device. A VeSync humidifier, a Petkit feeder, a Dreame vacuum and a Dreo
14177
+ * fan are reached only through a vendor cloud; there is no address to hand
14178
+ * out and no page to open. A Tuya plug, a Wyze camera and a Gree air
14179
+ * conditioner DO have a LAN IP, and still have no HTTP management page
14180
+ * behind it. None of them register, so `deviceManager.getBindings` never
14181
+ * lists the cap and no surface asks.
14182
+ * - **"This device has no page, and I know that"** → the provider registers
14183
+ * and `getAdminLink` returns `null`. This is the answer for a device whose
14184
+ * sibling DOES have a page: a Reolink battery camera reached over UDP by
14185
+ * `uid` with a blank `host`, an Ecowitt gateway configured in `listener`
14186
+ * transport, a Home Assistant broker authenticated by supervisor token
14187
+ * (which carries no `baseUrl` at all).
14188
+ *
14189
+ * Both draw NOTHING. A button that opens a browser error is worse than no
14190
+ * button, and D62 is the same rule from the other side: an off switch is
14191
+ * reported off, never made to look broken. There is no third state where the
14192
+ * UI renders a disabled button "because the device might have a page".
14193
+ *
14194
+ * ## The URL never carries credentials
14195
+ *
14196
+ * Not in userinfo, not in a query string. Every provider builds through
14197
+ * `buildDeviceAdminUrl` (`device-admin-link-url.ts`), which takes host, port,
14198
+ * scheme and path as separate arguments — there is no parameter a secret could
14199
+ * arrive in — and re-checks its own output for the `scheme://user:pass@` shape
14200
+ * that `scripts/check-no-credential-urls-in-fixtures.ts` bans from recorded
14201
+ * output. `scripts/check-admin-link-builder-is-the-only-url-source.ts` is what
14202
+ * keeps providers from hand-rolling one anyway.
14203
+ *
14204
+ * This matters here more than anywhere else in the repo, because every provider
14205
+ * that knows a device's host knows its PASSWORD too: `{ host, port, username,
14206
+ * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
14207
+ * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
14208
+ * camera's own page will ask for its own login. That is correct, and pre-
14209
+ * filling it is the operator's business, not ours.
14210
+ *
14211
+ * ## It is a LAN fact
14212
+ *
14213
+ * The URL addresses the device where the NODE can see it. It is not proxied,
14214
+ * not made reachable from outside, and not sent anywhere. A surface renders it
14215
+ * as a link the operator's own browser follows, on the operator's own network,
14216
+ * or renders nothing.
14217
+ */
14218
+ /**
14219
+ * Whose page is it. The distinction is for the OPERATOR, who needs to know
14220
+ * before clicking whether he is about to land on a camera's own web server or
14221
+ * inside Home Assistant.
14222
+ */
14223
+ var AdminLinkTargetEnum = _enum(["device", "integration"]);
14224
+ var DeviceAdminLinkSchema = object({
14225
+ /**
14226
+ * Absolute `http(s)://` URL. Built by `buildDeviceAdminUrl` and therefore
14227
+ * free of userinfo and of any credential-shaped query key.
14228
+ */
14229
+ url: string(),
14230
+ /**
14231
+ * What the surface calls it — "Web UI", "Home Assistant", "UniFi controller".
14232
+ * The PROVIDER names it, because only the provider knows what the page is;
14233
+ * a UI that invented the label from the addon id would call the Home
14234
+ * Assistant device page "Provider Homeassistant".
14235
+ */
14236
+ label: string(),
14237
+ target: AdminLinkTargetEnum,
14238
+ /**
14239
+ * Host the URL points at, without scheme, port or path — for the tooltip, so
14240
+ * an operator can see WHERE the button goes before he follows it. Redundant
14241
+ * with `url` by construction; carried separately so no surface has to parse
14242
+ * a URL to show it.
14243
+ */
14244
+ host: string()
14245
+ });
14246
+ var deviceAdminLinkCapability = {
14247
+ name: "device-admin-link",
14248
+ scope: "device",
14249
+ deviceNative: true,
14250
+ mode: "singleton",
14251
+ methods: {
14252
+ /**
14253
+ * The device's management page, or `null` when this device has none.
14254
+ *
14255
+ * `auth: 'admin'` deliberately. This is administration, not actuation —
14256
+ * the same bucket as `reboot` and `camera-credentials`, and explicitly NOT
14257
+ * the actuation set `scripts/check-actuation-not-admin.ts` protects (D403).
14258
+ * The URL is also a statement about the LAN, which a household member with
14259
+ * a `view` grant on a light has no reason to be handed.
14260
+ *
14261
+ * The surfaces gate on the QUERY, never on a role they guessed: a caller
14262
+ * without the right loses the query and draws nothing, which is the same
14263
+ * thing a device with no page draws. There is no path on which a button
14264
+ * appears and then fails — the D403 failure mode, from the other end.
14265
+ */
14266
+ getAdminLink: method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
14267
+ };
14268
+ /**
14269
+ * Query keys that may never appear on an admin link. A credential smuggled as
14270
+ * `?password=` is the same leak as userinfo, in a form the userinfo check
14271
+ * cannot see: it survives copy-paste, referrer headers and proxy logs
14272
+ * identically.
14273
+ */
14274
+ var CREDENTIAL_QUERY_KEYS = [
14275
+ "password",
14276
+ "passwd",
14277
+ "pwd",
14278
+ "pass",
14279
+ "user",
14280
+ "username",
14281
+ "usr",
14282
+ "login",
14283
+ "token",
14284
+ "access_token",
14285
+ "auth",
14286
+ "authorization",
14287
+ "apikey",
14288
+ "api_key",
14289
+ "secret",
14290
+ "credential",
14291
+ "credentials",
14292
+ "session",
14293
+ "sessionid",
14294
+ "key"
14295
+ ];
14296
+ /**
14297
+ * The same userinfo shape `scripts/check-no-credential-urls-in-fixtures.ts`
14298
+ * scans recorded fixtures for, applied to what we are about to EMIT. Kept
14299
+ * structurally identical on purpose: a URL this function returns is a URL that
14300
+ * guard would pass.
14301
+ */
14302
+ var CREDENTIAL_URL = /[a-z][a-z0-9+.-]*:\/\/[^/\s:@'"]+:[^/\s:@'"]+@/i;
14303
+ /** A host that is safe to place in an authority component verbatim. */
14304
+ var PLAIN_HOST = /^[a-zA-Z0-9._-]+$/;
14305
+ /** A bracketed IPv6 literal, the only other authority form we emit. */
14306
+ var IPV6_HOST = /^\[[0-9a-fA-F:.]+\]$/;
14307
+ function defaultPortFor(scheme) {
14308
+ return scheme === "https" ? 443 : 80;
14309
+ }
14310
+ /**
14311
+ * Build the admin-page URL, or refuse by name.
14312
+ *
14313
+ * The refusal is never thrown: a provider answering "no page for this device"
14314
+ * is an ordinary answer (`getAdminLink` → `null`), and a throw here would turn
14315
+ * a missing host on one device into a failed query for the whole surface.
14316
+ */
14317
+ function buildDeviceAdminUrl(parts) {
14318
+ const host = parts.host.trim();
14319
+ if (host.length === 0) return {
14320
+ ok: false,
14321
+ reason: "empty-host"
14322
+ };
14323
+ if (host.includes("@")) return {
14324
+ ok: false,
14325
+ reason: "host-carries-userinfo"
14326
+ };
14327
+ if (!PLAIN_HOST.test(host) && !IPV6_HOST.test(host)) return {
14328
+ ok: false,
14329
+ reason: "host-not-plain"
14330
+ };
14331
+ if (parts.port !== null) {
14332
+ if (!Number.isInteger(parts.port) || parts.port < 1 || parts.port > 65535) return {
14333
+ ok: false,
14334
+ reason: "port-out-of-range"
14335
+ };
14336
+ }
14337
+ if (!parts.path.startsWith("/")) return {
14338
+ ok: false,
14339
+ reason: "path-not-absolute"
14340
+ };
14341
+ const query = parts.query ?? {};
14342
+ for (const key of Object.keys(query)) if (CREDENTIAL_QUERY_KEYS.includes(key.toLowerCase())) return {
14343
+ ok: false,
14344
+ reason: "credential-query-key"
14345
+ };
14346
+ const authority = parts.port === null || parts.port === defaultPortFor(parts.scheme) ? host : `${host}:${String(parts.port)}`;
14347
+ const search = new URLSearchParams();
14348
+ for (const [key, value] of Object.entries(query)) search.append(key, value);
14349
+ const suffix = search.size > 0 ? `?${search.toString()}` : "";
14350
+ const url = `${parts.scheme}://${authority}${parts.path}${suffix}`;
14351
+ if (CREDENTIAL_URL.test(url)) return {
14352
+ ok: false,
14353
+ reason: "userinfo-in-result"
14354
+ };
14355
+ return {
14356
+ ok: true,
14357
+ url
14358
+ };
14359
+ }
14360
+ /**
14157
14361
  * `device-export` — collection cap for addons that export camstack
14158
14362
  * devices to external ecosystems (HomeAssistant via MQTT discovery,
14159
14363
  * HomeKit/HAP, Alexa Smart Home, …).
@@ -25156,6 +25360,39 @@ _enum([
25156
25360
  _enum(["deferred", "final"]);
25157
25361
  /** The camera's / provider's own words. Kept verbatim, never parsed. */
25158
25362
  var CLIP_STILL_REASON_HEADER = "x-camstack-reason";
25363
+ /**
25364
+ * Make a refusal sentence safe to put in an HTTP header.
25365
+ *
25366
+ * **An HTTP header value is Latin-1.** Node throws `Invalid character in
25367
+ * header content` on anything above U+00FF, the handler dies mid-response,
25368
+ * and the client gets a socket hang up — no status, no reason, nothing. The
25369
+ * mechanism that exists to EXPLAIN a refusal is then the thing that silences
25370
+ * it, and the surface shows "nothing happened".
25371
+ *
25372
+ * That is not hypothetical: measured 2026-09-23 on 1436, a Hikvision replay
25373
+ * refusal read `SETUP of the video track answered 500 — the camera refused
25374
+ * this replay`, and the EM DASH in it threw on every clip the operator
25375
+ * opened. This repository writes its refusals in prose, and its prose uses
25376
+ * `—`, `’` and `…` everywhere, so every reason string is a candidate.
25377
+ *
25378
+ * Substitutes rather than strips: an operator reading a log or a response
25379
+ * should get a readable sentence, not one with holes in it. The typographic
25380
+ * characters this repo actually writes have exact ASCII equivalents; anything
25381
+ * else non-Latin-1 becomes `?`, which is visible rather than silent. Control
25382
+ * characters go too — a newline in a header value is a response-splitting
25383
+ * bug, not a formatting one.
25384
+ */
25385
+ function clipReasonHeaderValue(reason) {
25386
+ const folded = reason.replace(/[\u2013\u2014\u2212]/g, "-").replace(/[\u2018\u2019\u201b]/g, "'").replace(/[\u201c\u201d]/g, "\"").replace(/\u2026/g, "...").replace(/[\u00a0\u2007\u202f]/g, " ");
25387
+ let out = "";
25388
+ for (const ch of folded) {
25389
+ const code = ch.codePointAt(0) ?? 0;
25390
+ if (code < 32 || code === 127) out += " ";
25391
+ else if (code > 255) out += "?";
25392
+ else out += ch;
25393
+ }
25394
+ return out.slice(0, 500).trim();
25395
+ }
25159
25396
  /** One {@link ClipStillRefusalCodeSchema} token, alone. */
25160
25397
  var CLIP_STILL_REASON_CODE_HEADER = "x-camstack-reason-code";
25161
25398
  /** One {@link ClipStillDispositionSchema} token, alone. */
@@ -27982,6 +28219,511 @@ var dayNightCapability = {
27982
28219
  volatileStateFields: ["lastFetchedAt"]
27983
28220
  };
27984
28221
  /**
28222
+ * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
28223
+ * writes to the CAMERA's own card, on the camera's own schedule.
28224
+ *
28225
+ * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
28226
+ * footage ledger, our storage locations, our retention. This one has a
28227
+ * different authority — the camera's firmware — and per D62 it stores
28228
+ * nothing of its own. Every value here is read from the camera and every
28229
+ * write goes back to the camera; there is no CamStack-side mirror that
28230
+ * could disagree with the device.
28231
+ *
28232
+ * ## One shape, two firmwares
28233
+ *
28234
+ * Measured 2026-09-22 against the live fleet:
28235
+ *
28236
+ * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
28237
+ * | --- | --- | --- |
28238
+ * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
28239
+ * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
28240
+ * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
28241
+ * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
28242
+ * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
28243
+ * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
28244
+ * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
28245
+ * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
28246
+ *
28247
+ * The two schedule models look different and are the same thing in
28248
+ * different coordinates: both answer "for this trigger, during which
28249
+ * weekly windows does the camera record". {@link RecordWindow} is that
28250
+ * question in one shape — Hikvision's ranges map straight onto it,
28251
+ * Reolink's mask expands into hour-aligned windows.
28252
+ *
28253
+ * ## Union, not intersection
28254
+ *
28255
+ * **The same fields exist on every camera.** What differs per device is
28256
+ * which VALUES that device accepts, and that is what {@link
28257
+ * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
28258
+ * per field plus the schedule's own limits. A control a camera cannot
28259
+ * honour is rendered DISABLED WITH ITS REASON, never missing and never
28260
+ * dead: disabled must not look like broken.
28261
+ *
28262
+ * ## Refusal by name
28263
+ *
28264
+ * A write a camera cannot honour is refused with a sentence the operator
28265
+ * can read — never accepted and dropped. Both providers refuse through
28266
+ * {@link describeOnboardRefusal}, so the vocabulary is one function and
28267
+ * one test, not two hand-written vendor opinions.
28268
+ *
28269
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
28270
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
28271
+ * injected from `status`) reports the live values, and a single
28272
+ * `setSettings` mutation applies a partial change. No hand-written
28273
+ * settings-contribution methods.
28274
+ */
28275
+ /**
28276
+ * What makes the camera start recording during a window.
28277
+ *
28278
+ * The union of both vendors' vocabularies. `continuous` is Hikvision's
28279
+ * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
28280
+ * object-class triggers are Reolink-only today and the smart-event ones
28281
+ * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
28282
+ * firmwares measured — a camera that cannot record on a trigger simply
28283
+ * does not list it in `options.schedule.triggers`, and a window naming
28284
+ * it is REFUSED, not dropped.
28285
+ */
28286
+ var RecordTriggerSchema = _enum([
28287
+ "continuous",
28288
+ "motion",
28289
+ "person",
28290
+ "vehicle",
28291
+ "animal",
28292
+ "lineCrossing",
28293
+ "intrusion",
28294
+ "loitering",
28295
+ "alarmInput"
28296
+ ]);
28297
+ /**
28298
+ * One weekly recording window: "on `day`, from `startMinute` to
28299
+ * `endMinute`, record on `trigger`".
28300
+ *
28301
+ * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
28302
+ * both firmwares enumerate). Minutes are local camera time since
28303
+ * midnight; `endMinute` may be 1440, meaning end of day — that is
28304
+ * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
28305
+ * collapsing it to 0 would turn a whole-day window into an empty one.
28306
+ */
28307
+ var RecordWindowSchema = object({
28308
+ trigger: RecordTriggerSchema,
28309
+ day: number().int().min(0).max(6),
28310
+ startMinute: number().int().min(0).max(1439),
28311
+ endMinute: number().int().min(1).max(1440)
28312
+ });
28313
+ /** Status of one physical volume, as the camera itself describes it. */
28314
+ var OnboardStorageVolumeSchema = object({
28315
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
28316
+ id: string(),
28317
+ /** The camera's own name for it, when it gives one (`hddName`). */
28318
+ label: string().optional(),
28319
+ status: _enum([
28320
+ "ok",
28321
+ "unformatted",
28322
+ "error",
28323
+ "offline",
28324
+ "unknown"
28325
+ ]),
28326
+ /**
28327
+ * Total size in MB, or **null when the camera did not say**.
28328
+ *
28329
+ * Never 0 for an unreadable value: a measurement that failed is not a
28330
+ * measurement (D393), and a card whose size is unknown must not be
28331
+ * rendered as a card of size zero.
28332
+ */
28333
+ capacityMb: number().nullable(),
28334
+ /**
28335
+ * Free space in MB, or null when unknown.
28336
+ *
28337
+ * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
28338
+ * 1439 both report exactly 11776 MB free — the fixed reserve a looping
28339
+ * card converges on once it has wrapped. At loop steady state the
28340
+ * number is identical whether the camera recorded yesterday or stopped
28341
+ * a month ago.
28342
+ */
28343
+ freeMb: number().nullable(),
28344
+ /** True when the camera reports the volume writable (`property` RW). */
28345
+ writable: boolean().optional()
28346
+ });
28347
+ /**
28348
+ * What the camera is doing with its own storage, right now.
28349
+ *
28350
+ * Every scalar is nullable and **null means the camera did not answer**,
28351
+ * never a default. A form that seeds `0` from an unanswered read invites
28352
+ * the operator to save that 0 back onto the camera.
28353
+ */
28354
+ var RecordingOnboardStatusSchema = object({
28355
+ storage: discriminatedUnion("kind", [
28356
+ object({
28357
+ kind: literal("present"),
28358
+ volumes: array(OnboardStorageVolumeSchema)
28359
+ }),
28360
+ object({
28361
+ kind: literal("absent"),
28362
+ reason: string()
28363
+ }),
28364
+ object({
28365
+ kind: literal("unknown"),
28366
+ reason: string()
28367
+ })
28368
+ ]),
28369
+ tracks: array(object({
28370
+ id: string(),
28371
+ enabled: boolean(),
28372
+ isVideo: boolean(),
28373
+ /** From the camera's own track description. Null when it does not say. */
28374
+ codec: string().nullable(),
28375
+ resolution: string().nullable(),
28376
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
28377
+ overwriteWhenFull: boolean().nullable()
28378
+ })),
28379
+ /**
28380
+ * The track the write path targets — the enabled VIDEO one. Null when
28381
+ * no track could be identified, which is itself a refusal reason.
28382
+ */
28383
+ primaryTrackId: string().nullable(),
28384
+ /** Master "record to the card at all" switch. */
28385
+ enabled: boolean().nullable(),
28386
+ overwriteWhenFull: boolean().nullable(),
28387
+ preRecordSec: number().nullable(),
28388
+ postRecordSec: number().nullable(),
28389
+ /** Length of one recorded file, in minutes. */
28390
+ segmentMinutes: number().nullable(),
28391
+ /** The primary track's weekly windows, flattened. */
28392
+ windows: array(RecordWindowSchema),
28393
+ /**
28394
+ * How many windows the camera described that CamStack could NOT read —
28395
+ * an unrecognised trigger, an unparseable clock, a weekday it does not
28396
+ * name.
28397
+ *
28398
+ * A dropped window is work the reader threw away, and a schedule that
28399
+ * silently shows fewer rows than the camera holds is how an operator
28400
+ * saves back a schedule shorter than the one they were looking at
28401
+ * (D391). Non-zero means the window list is INCOMPLETE and a write
28402
+ * that replaces it would delete what was not shown — which is why a
28403
+ * provider reporting a non-zero count also reports the schedule as not
28404
+ * writable.
28405
+ */
28406
+ unreadableWindows: number(),
28407
+ /**
28408
+ * The camera is scheduled to record and has NO usable storage.
28409
+ *
28410
+ * A first-class fact because it is the fleet's most common silent
28411
+ * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
28412
+ * to a card that is not there. Neither the schedule nor the storage
28413
+ * read says anything wrong on its own; only the pair does.
28414
+ */
28415
+ recordingToNowhere: boolean(),
28416
+ lastFetchedAt: number()
28417
+ });
28418
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
28419
+ var RangeSchema = object({
28420
+ min: number(),
28421
+ max: number(),
28422
+ step: number()
28423
+ });
28424
+ /**
28425
+ * The values a camera actually takes for a numeric field, when they are a SET
28426
+ * rather than a range.
28427
+ *
28428
+ * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
28429
+ * (I91DN) on 2026-09-22 by writing each value and reading it back:
28430
+ *
28431
+ * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
28432
+ * camera's "no limit" — `-1` and `4294967295` both land on it);
28433
+ * - post-record: `5, 10, 30, 60, 120, 300, 600`.
28434
+ *
28435
+ * Neither is expressible as a step: the first has a sentinel two billion away
28436
+ * from its neighbours, the second doubles and then jumps. A range that tried
28437
+ * would forbid values the camera takes AND permit values it silently replaces
28438
+ * with 5 — wrong in both directions at once.
28439
+ *
28440
+ * `sentinel` names the member that is not a duration, so a surface can render
28441
+ * "no limit" instead of `2147483647` seconds.
28442
+ */
28443
+ var AllowedValuesSchema = object({
28444
+ values: array(number()).min(1),
28445
+ sentinel: object({
28446
+ value: number(),
28447
+ meaning: _enum(["no-limit", "disabled"])
28448
+ }).optional()
28449
+ });
28450
+ /**
28451
+ * Per-field availability on ONE camera.
28452
+ *
28453
+ * The field exists on every camera — this says whether this one can be
28454
+ * read and whether it can be written, and `reason` says why not when
28455
+ * either is false. The UI renders the control DISABLED with the reason
28456
+ * rather than hiding it, so a limitation is legible instead of looking
28457
+ * like a missing feature.
28458
+ */
28459
+ var OnboardFieldSupportSchema = object({
28460
+ readable: boolean(),
28461
+ writable: boolean(),
28462
+ /** Required whenever `readable` or `writable` is false. */
28463
+ reason: string().optional()
28464
+ });
28465
+ /** What this camera's schedule model can express. */
28466
+ var OnboardScheduleSupportSchema = object({
28467
+ support: OnboardFieldSupportSchema,
28468
+ /**
28469
+ * The smallest time step the camera can express, in minutes.
28470
+ *
28471
+ * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
28472
+ * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
28473
+ * window whose edges are not a multiple of this is REFUSED rather than
28474
+ * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
28475
+ * and nothing says so.
28476
+ */
28477
+ granularityMinutes: number(),
28478
+ /** Triggers this camera can record on. A window naming another is refused. */
28479
+ triggers: array(RecordTriggerSchema),
28480
+ /**
28481
+ * False when the camera stores ONE trigger per time range, so two
28482
+ * windows overlapping on the same day cannot carry different triggers.
28483
+ * True on Reolink, whose mask is per-trigger and independent.
28484
+ */
28485
+ supportsOverlappingTriggers: boolean()
28486
+ });
28487
+ var RecordingOnboardOptionsSchema = object({
28488
+ enabled: OnboardFieldSupportSchema,
28489
+ overwriteWhenFull: OnboardFieldSupportSchema,
28490
+ preRecordSec: OnboardFieldSupportSchema,
28491
+ preRecordSecRange: RangeSchema.optional(),
28492
+ /** Preferred over the range when the camera takes a SET, not a span. */
28493
+ preRecordSecAllowed: AllowedValuesSchema.optional(),
28494
+ postRecordSec: OnboardFieldSupportSchema,
28495
+ postRecordSecRange: RangeSchema.optional(),
28496
+ /** Preferred over the range when the camera takes a SET, not a span. */
28497
+ postRecordSecAllowed: AllowedValuesSchema.optional(),
28498
+ segmentMinutes: OnboardFieldSupportSchema,
28499
+ segmentMinutesRange: RangeSchema.optional(),
28500
+ /** Preferred over the range when the camera takes a SET, not a span. */
28501
+ segmentMinutesAllowed: AllowedValuesSchema.optional(),
28502
+ schedule: OnboardScheduleSupportSchema
28503
+ });
28504
+ /**
28505
+ * A partial change. Every field optional.
28506
+ *
28507
+ * Unlike the other `deviceConfig` caps, a provider here does **NOT**
28508
+ * silently ignore a field it cannot support — it refuses, by name,
28509
+ * through {@link describeOnboardRefusal}. Silence on a recording setting
28510
+ * is the failure D62 exists to prevent: the operator believes the camera
28511
+ * is recording the way the form says, and it is not.
28512
+ */
28513
+ var RecordingOnboardPatchSchema = object({
28514
+ enabled: boolean().optional(),
28515
+ overwriteWhenFull: boolean().optional(),
28516
+ preRecordSec: number().optional(),
28517
+ postRecordSec: number().optional(),
28518
+ segmentMinutes: number().optional(),
28519
+ /** The complete new window set for the primary track — not a delta. */
28520
+ windows: array(RecordWindowSchema).optional()
28521
+ });
28522
+ var recordingOnboardCapability = {
28523
+ name: "recording-onboard",
28524
+ scope: "device",
28525
+ deviceNative: true,
28526
+ mode: "singleton",
28527
+ deviceTypes: [DeviceType.Camera],
28528
+ deviceConfig: { ui: {
28529
+ kind: "derived-form",
28530
+ builderId: "recording-onboard",
28531
+ tab: "recording"
28532
+ } },
28533
+ methods: {
28534
+ getOptions: method(object({ deviceId: number() }), RecordingOnboardOptionsSchema),
28535
+ setSettings: method(object({
28536
+ deviceId: number(),
28537
+ settings: RecordingOnboardPatchSchema
28538
+ }), _void(), {
28539
+ kind: "mutation",
28540
+ auth: "admin"
28541
+ })
28542
+ },
28543
+ status: {
28544
+ schema: RecordingOnboardStatusSchema,
28545
+ kind: "poll"
28546
+ },
28547
+ runtimeState: RecordingOnboardStatusSchema,
28548
+ /**
28549
+ * Runtime-state durability: **restored** — operator-set camera-side
28550
+ * recording config; mutation-driven, and the storage half is the last
28551
+ * thing the camera said about its own card.
28552
+ *
28553
+ * See `RuntimeStateDurability`. Enforced by
28554
+ * `scripts/check-runtime-state-durability.ts`.
28555
+ */
28556
+ durability: "restored",
28557
+ /** Clock fields: written, but excluded from the compare that decides
28558
+ * whether persisting is worth a SQLite commit. */
28559
+ volatileStateFields: ["lastFetchedAt"]
28560
+ };
28561
+ /**
28562
+ * Day-of-week names in the cap's index order (0 = Monday), for messages
28563
+ * an operator reads and for Hikvision's `<DayOfWeek>` element.
28564
+ */
28565
+ var DAY_NAMES = [
28566
+ "Monday",
28567
+ "Tuesday",
28568
+ "Wednesday",
28569
+ "Thursday",
28570
+ "Friday",
28571
+ "Saturday",
28572
+ "Sunday"
28573
+ ];
28574
+ /**
28575
+ * Does `patch` ask this camera for something it cannot do?
28576
+ *
28577
+ * Returns the operator-readable reason, or `null` when every field in
28578
+ * the patch is within what `options` says this camera accepts. A
28579
+ * provider calls this BEFORE touching the camera and throws the string:
28580
+ * refusing is the point, and refusing identically on both vendors is why
28581
+ * this is one function.
28582
+ *
28583
+ * The order of checks is the order an operator would read them: the
28584
+ * scalar knobs first, then the schedule, because a schedule complaint is
28585
+ * longer and a scalar one is usually the real problem.
28586
+ */
28587
+ function describeOnboardRefusal(options, patch) {
28588
+ if (patch.enabled !== void 0 && !options.enabled.writable) return unwritable("the master recording switch", options.enabled.reason);
28589
+ if (patch.overwriteWhenFull !== void 0 && !options.overwriteWhenFull.writable) return unwritable("overwrite-when-full", options.overwriteWhenFull.reason);
28590
+ const preRefusal = refuseNumeric("pre-record seconds", patch.preRecordSec, options.preRecordSec, options.preRecordSecRange, options.preRecordSecAllowed);
28591
+ if (preRefusal) return preRefusal;
28592
+ const postRefusal = refuseNumeric("post-record seconds", patch.postRecordSec, options.postRecordSec, options.postRecordSecRange, options.postRecordSecAllowed);
28593
+ if (postRefusal) return postRefusal;
28594
+ const segmentRefusal = refuseNumeric("segment length (minutes)", patch.segmentMinutes, options.segmentMinutes, options.segmentMinutesRange, options.segmentMinutesAllowed);
28595
+ if (segmentRefusal) return segmentRefusal;
28596
+ if (patch.windows !== void 0) {
28597
+ const scheduleRefusal = refuseSchedule(options, patch.windows);
28598
+ if (scheduleRefusal) return scheduleRefusal;
28599
+ }
28600
+ return null;
28601
+ }
28602
+ function unwritable(what, reason) {
28603
+ return reason ? `this camera cannot change ${what}: ${reason}` : `this camera cannot change ${what}`;
28604
+ }
28605
+ function refuseNumeric(what, value, support, range, allowed) {
28606
+ if (value === void 0) return null;
28607
+ if (!support.writable) return unwritable(what, support.reason);
28608
+ if (!Number.isFinite(value)) return `${what} must be a number, got ${String(value)}`;
28609
+ if (allowed) {
28610
+ if (allowed.values.includes(value)) return null;
28611
+ const say = (v) => allowed.sentinel !== void 0 && v === allowed.sentinel.value ? allowed.sentinel.meaning === "no-limit" ? "no limit" : "off" : String(v);
28612
+ return `this camera accepts ${what} only as ${allowed.values.map(say).join(", ")}; ${say(value)} is not one of them`;
28613
+ }
28614
+ if (!range) return null;
28615
+ 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`;
28616
+ 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`;
28617
+ return null;
28618
+ }
28619
+ function refuseSchedule(options, windows) {
28620
+ const schedule = options.schedule;
28621
+ if (!schedule.support.writable) return unwritable("the recording schedule", schedule.support.reason);
28622
+ const allowed = new Set(schedule.triggers);
28623
+ for (const window of windows) {
28624
+ if (!allowed.has(window.trigger)) {
28625
+ const offer = schedule.triggers.length > 0 ? schedule.triggers.join(", ") : "none";
28626
+ return `this camera cannot record on "${window.trigger}"; it records on: ${offer}`;
28627
+ }
28628
+ if (window.endMinute <= window.startMinute) return `a window on ${dayName(window.day)} ends at or before it starts (${String(window.startMinute)} → ${String(window.endMinute)})`;
28629
+ const step = schedule.granularityMinutes;
28630
+ 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`;
28631
+ }
28632
+ if (!schedule.supportsOverlappingTriggers) {
28633
+ const clash = findOverlapWithDifferentTrigger(windows);
28634
+ 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))}`;
28635
+ }
28636
+ return null;
28637
+ }
28638
+ function findOverlapWithDifferentTrigger(windows) {
28639
+ for (let i = 0; i < windows.length; i += 1) for (let j = i + 1; j < windows.length; j += 1) {
28640
+ const a = windows[i];
28641
+ const b = windows[j];
28642
+ if (!a || !b) continue;
28643
+ if (a.day !== b.day) continue;
28644
+ if (a.trigger === b.trigger) continue;
28645
+ if (a.startMinute < b.endMinute && b.startMinute < a.endMinute) return {
28646
+ a,
28647
+ b
28648
+ };
28649
+ }
28650
+ return null;
28651
+ }
28652
+ function dayName(day) {
28653
+ return DAY_NAMES[day] ?? `day ${String(day)}`;
28654
+ }
28655
+ /** `510` → `08:30`. For messages, not for the wire. */
28656
+ function formatMinutes(minute) {
28657
+ const hour = Math.floor(minute / 60);
28658
+ const rest = minute % 60;
28659
+ return `${String(hour).padStart(2, "0")}:${String(rest).padStart(2, "0")}`;
28660
+ }
28661
+ /**
28662
+ * Index of the mask character covering `day` at `hour`.
28663
+ *
28664
+ * **Layout: day-major, Monday first** — `day * 24 + hour`.
28665
+ *
28666
+ * This is an ASSUMPTION, and it is recorded as one because the fleet
28667
+ * cannot currently confirm it: every `valueTable` on every Reolink
28668
+ * measured on 2026-09-22 is uniform (all `1` or all `0`), and a uniform
28669
+ * mask reads identically under either layout. To settle it, set a single
28670
+ * hour on one day from the Reolink app and re-read `getRecordSchedule`.
28671
+ * Until then, a NON-uniform mask is reported as windows under this
28672
+ * layout and the day labels may be transposed; a uniform one is exact.
28673
+ */
28674
+ function weeklyMaskIndex(day, hour) {
28675
+ return day * 24 + hour;
28676
+ }
28677
+ /**
28678
+ * Expand one trigger's weekly hour mask into windows, merging adjacent
28679
+ * set hours into one window per run so a full day is ONE window rather
28680
+ * than twenty-four.
28681
+ *
28682
+ * A mask of the wrong length yields no windows: a truncated table is not
28683
+ * a schedule of fewer days, it is an answer we did not understand.
28684
+ */
28685
+ function weeklyMaskToWindows(mask, trigger) {
28686
+ if (mask.length !== 168) return [];
28687
+ const out = [];
28688
+ for (let day = 0; day < 7; day += 1) {
28689
+ let runStart = null;
28690
+ for (let hour = 0; hour <= 24; hour += 1) {
28691
+ const set = hour < 24 && mask[weeklyMaskIndex(day, hour)] === "1";
28692
+ if (set && runStart === null) runStart = hour;
28693
+ else if (!set && runStart !== null) {
28694
+ out.push({
28695
+ trigger,
28696
+ day,
28697
+ startMinute: runStart * 60,
28698
+ endMinute: hour * 60
28699
+ });
28700
+ runStart = null;
28701
+ }
28702
+ }
28703
+ }
28704
+ return out;
28705
+ }
28706
+ /**
28707
+ * Collapse this trigger's windows back into a weekly hour mask.
28708
+ *
28709
+ * Windows for other triggers are ignored — the mask is per trigger. A
28710
+ * window that is not hour-aligned is REFUSED upstream by
28711
+ * {@link describeOnboardRefusal}; this function rounds nothing, it
28712
+ * simply sets every hour the window fully or partly covers, so a caller
28713
+ * that skipped the refusal cannot silently lose coverage.
28714
+ */
28715
+ function windowsToWeeklyMask(windows, trigger) {
28716
+ const slots = new Array(168).fill("0");
28717
+ for (const window of windows) {
28718
+ if (window.trigger !== trigger) continue;
28719
+ if (window.day < 0 || window.day >= 7) continue;
28720
+ const firstHour = Math.floor(window.startMinute / 60);
28721
+ const lastHour = Math.ceil(window.endMinute / 60);
28722
+ for (let hour = firstHour; hour < lastHour && hour < 24; hour += 1) slots[weeklyMaskIndex(window.day, hour)] = "1";
28723
+ }
28724
+ return slots.join("");
28725
+ }
28726
+ /**
27985
28727
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
27986
28728
  * for every device, regardless of provider — the kernel needs a uniform
27987
28729
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -36984,6 +37726,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
36984
37726
  pressureSensor: pressureSensorCapability,
36985
37727
  privacyMask: privacyMaskCapability,
36986
37728
  ptzAutotrack: ptzAutotrackCapability,
37729
+ recordingOnboard: recordingOnboardCapability,
36987
37730
  recordingSignal: recordingSignalCapability,
36988
37731
  sceneMonitor: sceneMonitorCapability,
36989
37732
  scriptRunner: scriptRunnerCapability,
@@ -40934,6 +41677,12 @@ Object.freeze({
40934
41677
  addonId: null,
40935
41678
  access: "view"
40936
41679
  },
41680
+ "deviceAdminLink.getAdminLink": {
41681
+ capName: "device-admin-link",
41682
+ capScope: "device",
41683
+ addonId: null,
41684
+ access: "view"
41685
+ },
40937
41686
  "deviceAdoption.adopt": {
40938
41687
  capName: "device-adoption",
40939
41688
  capScope: "system",
@@ -44972,6 +45721,24 @@ Object.freeze({
44972
45721
  addonId: null,
44973
45722
  access: "view"
44974
45723
  },
45724
+ "recordingOnboard.getOptions": {
45725
+ capName: "recording-onboard",
45726
+ capScope: "device",
45727
+ addonId: null,
45728
+ access: "view"
45729
+ },
45730
+ "recordingOnboard.getStatus": {
45731
+ capName: "recording-onboard",
45732
+ capScope: "device",
45733
+ addonId: null,
45734
+ access: "view"
45735
+ },
45736
+ "recordingOnboard.setSettings": {
45737
+ capName: "recording-onboard",
45738
+ capScope: "device",
45739
+ addonId: null,
45740
+ access: "create"
45741
+ },
44975
45742
  "recordingSignal.getStatus": {
44976
45743
  capName: "recording-signal",
44977
45744
  capScope: "device",
@@ -46858,6 +47625,11 @@ Object.freeze({
46858
47625
  form: "single",
46859
47626
  optional: true
46860
47627
  }],
47628
+ "deviceAdminLink.getAdminLink": [{
47629
+ name: "deviceId",
47630
+ form: "single",
47631
+ optional: false
47632
+ }],
46861
47633
  "deviceAdoption.release": [{
46862
47634
  name: "camDeviceId",
46863
47635
  form: "single",
@@ -48243,6 +49015,16 @@ Object.freeze({
48243
49015
  form: "single",
48244
49016
  optional: true
48245
49017
  }],
49018
+ "recordingOnboard.getOptions": [{
49019
+ name: "deviceId",
49020
+ form: "single",
49021
+ optional: false
49022
+ }],
49023
+ "recordingOnboard.setSettings": [{
49024
+ name: "deviceId",
49025
+ form: "single",
49026
+ optional: false
49027
+ }],
48246
49028
  "recordingSignal.getStatus": [{
48247
49029
  name: "deviceId",
48248
49030
  form: "single",
@@ -71802,7 +72584,7 @@ var require_yazl = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
71802
72584
  }
71803
72585
  }));
71804
72586
  //#endregion
71805
- //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-UIQ527B6.js
72587
+ //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-E2N7WTCC.js
71806
72588
  var import_undici = require_undici();
71807
72589
  var import_yazl = /* @__PURE__ */ require_chunk.__toESM(require_yazl(), 1);
71808
72590
  var formatterCache = /* @__PURE__ */ new Map();
@@ -73148,13 +73930,13 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73148
73930
  sec: 59
73149
73931
  };
73150
73932
  }
73151
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Date conversion: start=${start.toISOString()} -> ReolinkTime=${JSON.stringify(startTime)}, end=${end.toISOString()} -> ReolinkTime=${JSON.stringify(endTime)}`);
73933
+ 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)}`);
73152
73934
  if (!bypassCache) {
73153
73935
  this.cleanRecordingsCache();
73154
73936
  const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
73155
73937
  const cached = this.recordingsCache.get(cacheKey);
73156
73938
  if (cached && cached.expiresAt > Date.now()) {
73157
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Cache hit: returning ${cached.data.length} cached enriched recordings`);
73939
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Cache hit: returning ${cached.data.length} cached enriched recordings`);
73158
73940
  return cached.data;
73159
73941
  }
73160
73942
  }
@@ -73206,7 +73988,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73206
73988
  const firstResult = dayResponse[0];
73207
73989
  if (firstResult && firstResult.code === 0 && firstResult.value?.SearchResult?.File) {
73208
73990
  const files = firstResult.value.SearchResult.File;
73209
- if (files.length > 0 && files[0]) require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response for day ${day}/${month}/${year}: ${JSON.stringify({
73991
+ 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({
73210
73992
  fileCount: files.length,
73211
73993
  sampleFile: {
73212
73994
  name: files[0].name,
@@ -73242,7 +74024,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73242
74024
  } catch (e) {}
73243
74025
  enriched2.push(this.enrichVodFile(vodFile, channel, streamUrl));
73244
74026
  }
73245
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched2.length} enriched recording files from autoSearchByDay`);
74027
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched2.length} enriched recording files from autoSearchByDay`);
73246
74028
  if (!bypassCache) {
73247
74029
  const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
73248
74030
  this.recordingsCache.set(cacheKey, {
@@ -73270,7 +74052,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73270
74052
  const response = await this.call("Search", param, 0);
73271
74053
  allResults.push(...response);
73272
74054
  } else {
73273
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Range spans ${dayRanges.length} days, splitting into separate day queries`);
74055
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Range spans ${dayRanges.length} days, splitting into separate day queries`);
73274
74056
  for (const range of dayRanges) {
73275
74057
  const param = { Search: {
73276
74058
  channel,
@@ -73280,7 +74062,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73280
74062
  EndTime: range.end
73281
74063
  } };
73282
74064
  if (iLogicChannel > 0) param.Search.iLogicChannel = iLogicChannel;
73283
- require_chunk_ZBACPERX.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})`);
74065
+ 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})`);
73284
74066
  const response = await this.call("Search", param, 0);
73285
74067
  allResults.push(...response);
73286
74068
  }
@@ -73290,7 +74072,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73290
74072
  const firstResult = result[0];
73291
74073
  if (firstResult && firstResult.code === 0 && firstResult.value?.SearchResult?.File) {
73292
74074
  const files = firstResult.value.SearchResult.File;
73293
- if (files.length > 0 && files[0]) require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response (normal search): ${JSON.stringify({
74075
+ if (files.length > 0 && files[0]) require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response (normal search): ${JSON.stringify({
73294
74076
  fileCount: files.length,
73295
74077
  sampleFile: {
73296
74078
  name: files[0].name,
@@ -73307,12 +74089,12 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73307
74089
  const searchResult = res.value?.SearchResult;
73308
74090
  if (searchResult?.File) allFiles.push(...searchResult.File);
73309
74091
  }
73310
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Collected ${allFiles.length} raw VodFiles from API search results`);
74092
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Collected ${allFiles.length} raw VodFiles from API search results`);
73311
74093
  if (allFiles.length > 0) {
73312
74094
  const sampleSize = Math.min(3, allFiles.length);
73313
74095
  for (let i = 0; i < sampleSize; i++) {
73314
74096
  const file = allFiles[i];
73315
- if (file) require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Sample file ${i + 1}/${allFiles.length}: ${JSON.stringify({
74097
+ if (file) require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Sample file ${i + 1}/${allFiles.length}: ${JSON.stringify({
73316
74098
  name: file.name,
73317
74099
  type: file.type,
73318
74100
  size: file.size,
@@ -73335,7 +74117,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73335
74117
  } catch (e) {}
73336
74118
  enriched.push(this.enrichVodFile(vodFile, channel, streamUrl));
73337
74119
  }
73338
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched.length} enriched recording files`);
74120
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched.length} enriched recording files`);
73339
74121
  if (!bypassCache) {
73340
74122
  const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
73341
74123
  this.recordingsCache.set(cacheKey, {
@@ -73372,7 +74154,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73372
74154
  prepare: true,
73373
74155
  seek: seekSeconds
73374
74156
  });
73375
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`);
74157
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`);
73376
74158
  const { spawn: spawn2 } = await import("child_process");
73377
74159
  return new Promise((resolve, reject) => {
73378
74160
  const chunks = [];
@@ -73420,7 +74202,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73420
74202
  reject(/* @__PURE__ */ new Error(`ffmpeg produced too small output: ${jpeg.length} bytes`));
73421
74203
  return;
73422
74204
  }
73423
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Successfully extracted thumbnail: ${jpeg.length} bytes`);
74205
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Successfully extracted thumbnail: ${jpeg.length} bytes`);
73424
74206
  resolve(jpeg);
73425
74207
  } else reject(/* @__PURE__ */ new Error(`ffmpeg exited with code ${code}: ${stderr.slice(-500)}`));
73426
74208
  });
@@ -73469,10 +74251,10 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73469
74251
  }
73470
74252
  } }
73471
74253
  }];
73472
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Request body: ${JSON.stringify(body)}`);
74254
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Request body: ${JSON.stringify(body)}`);
73473
74255
  try {
73474
74256
  const response = await this.callMany(body);
73475
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Response: ${JSON.stringify(response)}`);
74257
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Response: ${JSON.stringify(response)}`);
73476
74258
  const first = response[0];
73477
74259
  if (!first || first.code !== 0 || !first.value?.fileList) {
73478
74260
  const rspCode = first?.error?.rspCode;
@@ -73493,7 +74275,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73493
74275
  if (!filename) throw new Error(`NvrDownload: no files found in response`);
73494
74276
  return filename;
73495
74277
  } catch (error) {
73496
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Error: ${error instanceof Error ? error.message : String(error)}`);
74278
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Error: ${error instanceof Error ? error.message : String(error)}`);
73497
74279
  throw error;
73498
74280
  }
73499
74281
  }
@@ -73689,7 +74471,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73689
74471
  * Convert a VodFile to RecordingFile with parsed metadata.
73690
74472
  */
73691
74473
  enrichVodFile(vodFile, channel, streamUrl) {
73692
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `RAW VodFile from API: ${JSON.stringify({
74474
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `RAW VodFile from API: ${JSON.stringify({
73693
74475
  name: vodFile.name,
73694
74476
  type: vodFile.type,
73695
74477
  size: vodFile.size,
@@ -73742,7 +74524,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73742
74524
  hashBits0to15: hashRevBin.slice(0, 16)
73743
74525
  };
73744
74526
  }
73745
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Parsed filename: ${JSON.stringify({
74527
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Parsed filename: ${JSON.stringify({
73746
74528
  fileName: vodFile.name,
73747
74529
  filenameParts,
73748
74530
  hexValueFromFilename,
@@ -73769,13 +74551,13 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73769
74551
  const hexFlags = parsed?.flags;
73770
74552
  if (parsed?.devType === "hub" && parsed?.version === 4) {
73771
74553
  if (hexValueFromFilename && hexValueFromFilename !== "unknown") {
73772
- require_chunk_ZBACPERX.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.`);
73773
- require_chunk_ZBACPERX.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.`);
74554
+ 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.`);
74555
+ 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.`);
73774
74556
  }
73775
74557
  }
73776
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Hex flags from filename: ${JSON.stringify(hexFlags)}`);
74558
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Hex flags from filename: ${JSON.stringify(hexFlags)}`);
73777
74559
  const typeFlags = this.parseRecordTypeFlags(vodFile.type);
73778
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Type flags from recordType "${vodFile.type}": ${JSON.stringify(typeFlags)}`);
74560
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Type flags from recordType "${vodFile.type}": ${JSON.stringify(typeFlags)}`);
73779
74561
  const hasPerson = (hexFlags?.aiPerson ?? false) || typeFlags.hasPerson;
73780
74562
  const hasVehicle = (hexFlags?.aiVehicle ?? false) || typeFlags.hasVehicle;
73781
74563
  const hasAnimal = (hexFlags?.aiAnimal ?? false) || typeFlags.hasAnimal;
@@ -73786,7 +74568,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73786
74568
  const hasPackage = (hexFlags?.package ?? false) || typeFlags.hasPackage;
73787
74569
  const hasRf = (hexFlags?.rf ?? false) || typeFlags.hasRf;
73788
74570
  const hasOther = (hexFlags?.aiOther ?? false) || typeFlags.hasOther;
73789
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Final merged flags: ${JSON.stringify({
74571
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Final merged flags: ${JSON.stringify({
73790
74572
  hasPerson,
73791
74573
  hasVehicle,
73792
74574
  hasAnimal,
@@ -74192,8 +74974,8 @@ function nowIsoCompact() {
74192
74974
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
74193
74975
  }
74194
74976
  function nalTypesSummary(videoType, accessUnitAnnexB) {
74195
- if (videoType === "H264") return require_chunk_ZBACPERX.splitAnnexBToNalPayloads(accessUnitAnnexB).map((n) => (n[0] ?? 0) & 31);
74196
- return require_chunk_ZBACPERX.splitAnnexBToNalPayloads2(accessUnitAnnexB).map((nal) => require_chunk_ZBACPERX.getH265NalType(nal)).filter((t) => typeof t === "number");
74977
+ if (videoType === "H264") return require_chunk_OSVYEFMM.splitAnnexBToNalPayloads(accessUnitAnnexB).map((n) => (n[0] ?? 0) & 31);
74978
+ return require_chunk_OSVYEFMM.splitAnnexBToNalPayloads2(accessUnitAnnexB).map((nal) => require_chunk_OSVYEFMM.getH265NalType(nal)).filter((t) => typeof t === "number");
74197
74979
  }
74198
74980
  function normalizeProfiles(p) {
74199
74981
  const out = [];
@@ -74633,7 +75415,7 @@ async function sampleStreams(opts) {
74633
75415
  });
74634
75416
  };
74635
75417
  api.client.on("frame", onFrame);
74636
- const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
75418
+ const videoStream = new require_chunk_OSVYEFMM.BaichuanVideoStream({
74637
75419
  client: api.client,
74638
75420
  api,
74639
75421
  channel,
@@ -75683,7 +76465,7 @@ async function runMultifocalDiagnosticsConsecutively(params) {
75683
76465
  let audioFrames = 0;
75684
76466
  let lastSnapshotAtMs = 0;
75685
76467
  const client = params.api.client;
75686
- const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
76468
+ const videoStream = new require_chunk_OSVYEFMM.BaichuanVideoStream({
75687
76469
  client,
75688
76470
  channel: chNative,
75689
76471
  profile,
@@ -75767,7 +76549,7 @@ async function runMultifocalDiagnosticsConsecutively(params) {
75767
76549
  const baseStreamName = profile === "main" ? "mainStream" : profile === "sub" ? "subStream" : "externStream";
75768
76550
  const headerStreamTypeCandidates = profile === "sub" ? [1, 3] : profile === "main" ? [0, 2] : [0];
75769
76551
  const channelIdTagCandidates = uniq([chNative, chNative + 1].filter((n) => Number.isFinite(n) && n >= 0));
75770
- const messageClassCandidates = params.probeFull ? [require_chunk_ZBACPERX.BC_CLASS_MODERN_24, 0] : [require_chunk_ZBACPERX.BC_CLASS_MODERN_24];
76552
+ const messageClassCandidates = params.probeFull ? [require_chunk_OSVYEFMM.BC_CLASS_MODERN_24, 0] : [require_chunk_OSVYEFMM.BC_CLASS_MODERN_24];
75771
76553
  const extensionXmlCandidates = params.probeFull ? ["", buildChannelExtensionXml(chNative)] : [buildChannelExtensionXml(chNative)];
75772
76554
  const attempts = [];
75773
76555
  for (const messageClass of messageClassCandidates) for (const streamTypeHeader of headerStreamTypeCandidates) for (const extensionXml of extensionXmlCandidates) {
@@ -78288,14 +79070,14 @@ var require_lz4 = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
78288
79070
  };
78289
79071
  }));
78290
79072
  //#endregion
78291
- //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-GNC5U3YA.js
79073
+ //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-IPKY5AWA.js
78292
79074
  var import_fxp = require_fxp();
78293
79075
  var import_lz4 = /* @__PURE__ */ require_chunk.__toESM(require_lz4(), 1);
78294
79076
  function encodeHeader(h) {
78295
- const hasOffset = require_chunk_ZBACPERX.bcHeaderHasPayloadOffset(h.messageClass);
79077
+ const hasOffset = require_chunk_OSVYEFMM.bcHeaderHasPayloadOffset(h.messageClass);
78296
79078
  const headerLen = hasOffset ? 24 : 20;
78297
79079
  const buf = Buffer.alloc(headerLen);
78298
- const magic = h.magic ?? require_chunk_ZBACPERX.BC_MAGIC;
79080
+ const magic = h.magic ?? require_chunk_OSVYEFMM.BC_MAGIC;
78299
79081
  if (magic.length !== 4) throw new Error("magic must be 4 bytes");
78300
79082
  magic.copy(buf, 0);
78301
79083
  buf.writeUInt32LE(h.cmdId >>> 0, 4);
@@ -78311,7 +79093,7 @@ function encodeHeader(h) {
78311
79093
  function decodeHeader(buf) {
78312
79094
  if (buf.length < 20) throw new Error("not enough data for Baichuan header");
78313
79095
  const magic = buf.subarray(0, 4);
78314
- if (!magic.equals(require_chunk_ZBACPERX.BC_MAGIC) && !magic.equals(require_chunk_ZBACPERX.BC_MAGIC_REV)) throw new Error(`invalid Baichuan magic: ${magic.toString("hex")}`);
79096
+ 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")}`);
78315
79097
  const cmdId = buf.readUInt32LE(4);
78316
79098
  const bodyLen = buf.readUInt32LE(8);
78317
79099
  const channelId = buf.readUInt8(12);
@@ -78319,7 +79101,7 @@ function decodeHeader(buf) {
78319
79101
  const msgNum = buf.readUInt16LE(14);
78320
79102
  const responseCode = buf.readUInt16LE(16);
78321
79103
  const messageClass = buf.readUInt16LE(18);
78322
- const headerLen = require_chunk_ZBACPERX.bcHeaderHasPayloadOffset(messageClass) ? 24 : 20;
79104
+ const headerLen = require_chunk_OSVYEFMM.bcHeaderHasPayloadOffset(messageClass) ? 24 : 20;
78323
79105
  if (buf.length < headerLen) throw new Error("not enough data for Baichuan header (needs 24 bytes)");
78324
79106
  const messageKey = buf.readUInt32LE(12);
78325
79107
  const header = {
@@ -78389,9 +79171,9 @@ var BaichuanFrameParser = class {
78389
79171
  this.needed = 4;
78390
79172
  break;
78391
79173
  }
78392
- if (!this.buffer.subarray(0, 4).equals(require_chunk_ZBACPERX.BC_MAGIC) && !this.buffer.subarray(0, 4).equals(require_chunk_ZBACPERX.BC_MAGIC_REV)) {
78393
- const idx = this.buffer.indexOf(require_chunk_ZBACPERX.BC_MAGIC);
78394
- const idxRev = this.buffer.indexOf(require_chunk_ZBACPERX.BC_MAGIC_REV);
79174
+ if (!this.buffer.subarray(0, 4).equals(require_chunk_OSVYEFMM.BC_MAGIC) && !this.buffer.subarray(0, 4).equals(require_chunk_OSVYEFMM.BC_MAGIC_REV)) {
79175
+ const idx = this.buffer.indexOf(require_chunk_OSVYEFMM.BC_MAGIC);
79176
+ const idxRev = this.buffer.indexOf(require_chunk_OSVYEFMM.BC_MAGIC_REV);
78395
79177
  const next = idx === -1 ? idxRev : idxRev === -1 ? idx : Math.min(idx, idxRev);
78396
79178
  if (next === -1) {
78397
79179
  this.buffer = this.buffer.subarray(Math.max(0, this.buffer.length - 3));
@@ -80320,6 +81102,17 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
80320
81102
  * Impact: Snapshots are ~0–50ms slower per camera (negligible for users).
80321
81103
  */
80322
81104
  snapshotQueueTail = Promise.resolve();
81105
+ /**
81106
+ * Live cmd 5 replay transfers on this socket.
81107
+ *
81108
+ * Opening a replay SUPERSEDES every earlier one: nothing on the wire ends a
81109
+ * cmd 5 transfer, so an abandoned one keeps delivering (measured: 8 126
81110
+ * further frames over 3 475 ms after the consumer let go) and its successor
81111
+ * used to adopt them. A superseded session now refuses every frame and
81112
+ * counts it.
81113
+ */
81114
+ replaySessions = /* @__PURE__ */ new Set();
81115
+ replaySessionSeq = 0;
80323
81116
  opts;
80324
81117
  debugCfg;
80325
81118
  logger;
@@ -80406,7 +81199,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
80406
81199
  constructor(options) {
80407
81200
  super();
80408
81201
  this.opts = options;
80409
- this.debugCfg = require_chunk_ZBACPERX.normalizeDebugOptions(options.debugOptions);
81202
+ this.debugCfg = require_chunk_OSVYEFMM.normalizeDebugOptions(options.debugOptions);
80410
81203
  this.logger = createDebugGateLogger(options.logger, this.debugCfg.general || this.debugCfg.traceNativeStream || this.debugCfg.traceRecordings || this.debugCfg.traceTalk || this.debugCfg.traceEvents || this.debugCfg.debugRtsp);
80411
81204
  this.on("error", (err) => {
80412
81205
  this.logFixed("error", {
@@ -80888,7 +81681,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
80888
81681
  streamType: 0,
80889
81682
  msgNum,
80890
81683
  responseCode: 0,
80891
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
81684
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
80892
81685
  payloadOffset: 0
80893
81686
  });
80894
81687
  this.logDebug("udp_keepalive_ping_tx", {
@@ -80920,7 +81713,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
80920
81713
  cmdId: 93,
80921
81714
  channel: this.opts.channel ?? 0,
80922
81715
  channelIdOverride: 0,
80923
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
81716
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
80924
81717
  streamType: 0,
80925
81718
  extensionXml: "",
80926
81719
  internal: true
@@ -81259,7 +82052,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81259
82052
  cmdId: 2,
81260
82053
  payloadXml: logoutXml,
81261
82054
  extensionXml: "",
81262
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
82055
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
81263
82056
  channelIdOverride: effectiveHostChannelId,
81264
82057
  timeoutMs: 5e3
81265
82058
  })).header.responseCode;
@@ -81372,12 +82165,12 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81372
82165
  };
81373
82166
  s.frames++;
81374
82167
  if (now - s.lastLogMs >= 1e3) {
81375
- require_chunk_ZBACPERX.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}`);
82168
+ 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}`);
81376
82169
  s.lastLogMs = now;
81377
82170
  s.frames = 0;
81378
82171
  }
81379
82172
  this.rxCmdTraceStats.set(3, s);
81380
- } else require_chunk_ZBACPERX.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}`);
82173
+ } 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}`);
81381
82174
  if (this.transport === "udp" && frame.header.cmdId === 234) {
81382
82175
  try {
81383
82176
  const header = encodeHeader({
@@ -81414,7 +82207,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81414
82207
  }
81415
82208
  return;
81416
82209
  }
81417
- if (this.debugCfg.traceTalk && isTalkCmd(frame.header.cmdId)) require_chunk_ZBACPERX.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}`);
82210
+ 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}`);
81418
82211
  this.emit("frame", frame);
81419
82212
  const key = `${frame.header.cmdId}:${frame.header.msgNum}`;
81420
82213
  const pending = this.pending.get(key);
@@ -81435,7 +82228,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81435
82228
  };
81436
82229
  s.frames++;
81437
82230
  if (now2 - s.lastLogMs >= 500) {
81438
- require_chunk_ZBACPERX.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}`);
82231
+ 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}`);
81439
82232
  s.lastLogMs = now2;
81440
82233
  s.frames = 0;
81441
82234
  }
@@ -81466,7 +82259,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81466
82259
  const eventLogTag = sid ? `BaichuanEvent sid=${sid}` : "BaichuanEvent";
81467
82260
  const events$1 = this.parseEvents(frame);
81468
82261
  for (const event of events$1) {
81469
- require_chunk_ZBACPERX.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 ?? ""}` : ""));
82262
+ 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 ?? ""}` : ""));
81470
82263
  this.emit("event", event);
81471
82264
  }
81472
82265
  } catch (error) {
@@ -81525,7 +82318,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81525
82318
  if (!xml || !xml.startsWith("<?xml")) return [];
81526
82319
  if (this.debugCfg.traceEvents) {
81527
82320
  const snippet = xml.length > 500 ? `${xml.slice(0, 500)}...` : xml;
81528
- require_chunk_ZBACPERX.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)}`);
82321
+ 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)}`);
81529
82322
  }
81530
82323
  const fallbackChannelId = frame.header.channelId;
81531
82324
  const fallbackChannel = fallbackChannelId === 250 ? 0 : Math.max(0, fallbackChannelId - 1);
@@ -81543,7 +82336,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81543
82336
  const timeStampRaw2 = (getXmlText(alarmXml, "timeStamp") ?? getXmlText(alarmXml, "timestamp") ?? "").trim();
81544
82337
  const timeStampNum2 = timeStampRaw2.length ? Number(timeStampRaw2) : void 0;
81545
82338
  const alarmTimeStamp2 = Number.isFinite(timeStampNum2) ? timeStampNum2 : void 0;
81546
- if (this.debugCfg.traceEvents) require_chunk_ZBACPERX.eventTraceLog(this.debugCfg, this.logger, eventRawLogTag, `AlarmEvent channel=${channel} status=${JSON.stringify(status2)} aiType=${JSON.stringify(aiTypeRaw2)}`);
82339
+ if (this.debugCfg.traceEvents) require_chunk_OSVYEFMM.eventTraceLog(this.debugCfg, this.logger, eventRawLogTag, `AlarmEvent channel=${channel} status=${JSON.stringify(status2)} aiType=${JSON.stringify(aiTypeRaw2)}`);
81547
82340
  const statusTokens2 = status2.toLowerCase().split(",").map((t) => t.trim()).filter((t) => t.length > 0);
81548
82341
  const visitorActive2 = statusTokens2.includes("visitor");
81549
82342
  const dayNightActive2 = statusTokens2.includes("dn") || statusTokens2.includes("daynight");
@@ -81777,6 +82570,85 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81777
82570
  this.alarmEventState.set(fallbackChannel, curr);
81778
82571
  return out;
81779
82572
  }
82573
+ /**
82574
+ * Register a cmd 5 replay transfer and supersede every earlier one on this
82575
+ * socket.
82576
+ */
82577
+ openReplaySession(discriminator) {
82578
+ for (const prev of this.replaySessions) {
82579
+ if (prev.superseded) continue;
82580
+ prev.superseded = true;
82581
+ this.logger?.warn?.(`[BaichuanClient] cmd 5 replay session #${prev.id} (channelId=${prev.discriminator.channelId}, ${prev.discriminator.kind}) superseded by #${this.replaySessionSeq + 1} after ${prev.acceptedFrames} accepted frame(s). Frames still in flight for #${prev.id} will be dropped, not handed to its successor.`);
82582
+ }
82583
+ const session = {
82584
+ id: ++this.replaySessionSeq,
82585
+ discriminator,
82586
+ superseded: false,
82587
+ acceptedFrames: 0,
82588
+ droppedFrames: 0,
82589
+ droppedChannelIds: /* @__PURE__ */ new Set(),
82590
+ reportedDrop: false
82591
+ };
82592
+ this.replaySessions.add(session);
82593
+ return session;
82594
+ }
82595
+ closeReplaySession(session) {
82596
+ this.replaySessions.delete(session);
82597
+ if (session.droppedFrames > 0) this.logger?.debug?.(`[BaichuanClient] cmd 5 replay session #${session.id} (channelId=${session.discriminator.channelId}) ended: ${session.acceptedFrames} frame(s) accepted, ${session.droppedFrames} dropped from channelId ${[...session.droppedChannelIds].join(",")}.`);
82598
+ }
82599
+ /**
82600
+ * Count a cmd 5 frame this transfer refused, and say so the first time.
82601
+ *
82602
+ * Silence here is what made the original defect invisible: the successor
82603
+ * transfer simply returned a few extra access units and a short duration,
82604
+ * with nothing anywhere saying a frame had changed hands.
82605
+ */
82606
+ countReplayDrop(session, frame, reason) {
82607
+ session.droppedFrames++;
82608
+ session.droppedChannelIds.add(frame.header.channelId);
82609
+ if (!session.reportedDrop) {
82610
+ session.reportedDrop = true;
82611
+ this.logger?.warn?.(`[BaichuanClient] cmd 5 replay session #${session.id} dropping frames (${reason}): frame channelId=${frame.header.channelId} streamType=${frame.header.streamType} msgNum=${frame.header.msgNum} vs session channelId=${session.discriminator.channelId} (${session.discriminator.kind}). Further drops for this session are counted, not logged.`);
82612
+ }
82613
+ }
82614
+ /**
82615
+ * Wait until the socket has gone quiet of cmd 5 frames.
82616
+ *
82617
+ * A cmd 7 stop is acknowledged `rc=200` before the camera has finished
82618
+ * sending: four of eleven stopped sessions in the Reolink app's own capture
82619
+ * were still delivering when the ack came back (26–91 further frames). A
82620
+ * caller that opens the next replay the instant the ack lands is racing that
82621
+ * tail; this is how it waits for it instead.
82622
+ *
82623
+ * @returns how many cmd 5 frames arrived during the drain.
82624
+ */
82625
+ async drainReplayFrames(options) {
82626
+ const quietMs = options?.quietMs ?? 300;
82627
+ const maxMs = options?.maxMs ?? 4e3;
82628
+ let drained = 0;
82629
+ await new Promise((resolve) => {
82630
+ let quiet;
82631
+ const hard = setTimeout(() => settle(), maxMs);
82632
+ const onFrame = (frame) => {
82633
+ if (frame.header.cmdId !== 5) return;
82634
+ drained++;
82635
+ if (quiet) clearTimeout(quiet);
82636
+ quiet = setTimeout(() => settle(), quietMs);
82637
+ };
82638
+ let settled = false;
82639
+ const settle = () => {
82640
+ if (settled) return;
82641
+ settled = true;
82642
+ clearTimeout(hard);
82643
+ if (quiet) clearTimeout(quiet);
82644
+ this.off("frame", onFrame);
82645
+ resolve();
82646
+ };
82647
+ this.on("frame", onFrame);
82648
+ quiet = setTimeout(() => settle(), quietMs);
82649
+ });
82650
+ return drained;
82651
+ }
81780
82652
  nextMsgNum() {
81781
82653
  this.msgNum = this.msgNum + 1 & 65535;
81782
82654
  return this.msgNum;
@@ -81817,15 +82689,15 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81817
82689
  const extBuf = Buffer.from(extXml, "utf8");
81818
82690
  const payloadBuf = Buffer.from(payloadXml, "utf8");
81819
82691
  if (enc.kind === "none") return Buffer.concat([extBuf, payloadBuf]);
81820
- if (enc.kind === "bc") return Buffer.concat([require_chunk_ZBACPERX.bcEncrypt(extBuf, channelId), require_chunk_ZBACPERX.bcEncrypt(payloadBuf, channelId)]);
81821
- if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_ZBACPERX.aesEncrypt(extBuf, enc.key), require_chunk_ZBACPERX.aesEncrypt(payloadBuf, enc.key)]);
82692
+ if (enc.kind === "bc") return Buffer.concat([require_chunk_OSVYEFMM.bcEncrypt(extBuf, channelId), require_chunk_OSVYEFMM.bcEncrypt(payloadBuf, channelId)]);
82693
+ 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)]);
81822
82694
  return Buffer.concat([extBuf, payloadBuf]);
81823
82695
  }
81824
82696
  encodeBodyBinary(extXml, payload, channelId, enc) {
81825
82697
  const extBuf = Buffer.from(extXml, "utf8");
81826
82698
  if (enc.kind === "none") return Buffer.concat([extBuf, payload]);
81827
- if (enc.kind === "bc") return Buffer.concat([require_chunk_ZBACPERX.bcEncrypt(extBuf, channelId), payload]);
81828
- if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_ZBACPERX.aesEncrypt(extBuf, enc.key), payload]);
82699
+ if (enc.kind === "bc") return Buffer.concat([require_chunk_OSVYEFMM.bcEncrypt(extBuf, channelId), payload]);
82700
+ if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_OSVYEFMM.aesEncrypt(extBuf, enc.key), payload]);
81829
82701
  return Buffer.concat([extBuf, payload]);
81830
82702
  }
81831
82703
  /**
@@ -81870,7 +82742,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81870
82742
  bodyLen,
81871
82743
  binaryPayload: true
81872
82744
  });
81873
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.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}`);
82745
+ 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}`);
81874
82746
  this.recordTx({
81875
82747
  cmdId,
81876
82748
  responseCode: 0,
@@ -81887,8 +82759,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81887
82759
  let dec;
81888
82760
  try {
81889
82761
  if (enc.kind === "none") dec = buf;
81890
- else if (enc.kind === "bc") dec = require_chunk_ZBACPERX.bcDecrypt(buf, channelId);
81891
- else dec = require_chunk_ZBACPERX.aesDecrypt(buf, enc.key);
82762
+ else if (enc.kind === "bc") dec = require_chunk_OSVYEFMM.bcDecrypt(buf, channelId);
82763
+ else dec = require_chunk_OSVYEFMM.aesDecrypt(buf, enc.key);
81892
82764
  } catch {
81893
82765
  return;
81894
82766
  }
@@ -81954,8 +82826,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81954
82826
  messageClass,
81955
82827
  bodyLen
81956
82828
  });
81957
- if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_ZBACPERX.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}`);
81958
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.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}`);
82829
+ 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}`);
82830
+ 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}`);
81959
82831
  this.recordTx({
81960
82832
  cmdId,
81961
82833
  responseCode: 0,
@@ -81970,8 +82842,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81970
82842
  responseCode: frame.header.responseCode,
81971
82843
  msgNum: frame.header.msgNum
81972
82844
  });
81973
- if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_ZBACPERX.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}`);
81974
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.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}`);
82845
+ 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}`);
82846
+ 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}`);
81975
82847
  if (frame.header.responseCode === 400) {
81976
82848
  if (frame.body.length === 0) throw new Error("Baichuan authentication failed (responseCode 400, empty body) - check username/password");
81977
82849
  }
@@ -82041,9 +82913,9 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82041
82913
  messageClass,
82042
82914
  bodyLen
82043
82915
  });
82044
- if (_BaichuanClient.recordingCmdIds.has(cmdId)) require_chunk_ZBACPERX.recordingsTraceLog(this.debugCfg, this.logger, "BaichuanRecordings", `tx recording cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} bodyLen=${bodyLen}`);
82045
- if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_ZBACPERX.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}`);
82046
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.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}`);
82916
+ 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}`);
82917
+ 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}`);
82918
+ 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}`);
82047
82919
  this.recordTx({
82048
82920
  cmdId,
82049
82921
  responseCode: 0,
@@ -82058,9 +82930,9 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82058
82930
  responseCode: frame.header.responseCode,
82059
82931
  msgNum: frame.header.msgNum
82060
82932
  });
82061
- if (_BaichuanClient.recordingCmdIds.has(frame.header.cmdId)) require_chunk_ZBACPERX.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}`);
82062
- if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_ZBACPERX.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}`);
82063
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.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}`);
82933
+ 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}`);
82934
+ 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}`);
82935
+ 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}`);
82064
82936
  if (!internal) this.kickIdleDisconnectTimer();
82065
82937
  return frame;
82066
82938
  }
@@ -82330,6 +83202,13 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82330
83202
  params.channel ?? this.opts.channel;
82331
83203
  const sessionCounter = this.nextMsgNum();
82332
83204
  const channelId = params.channelIdOverride ?? sessionCounter;
83205
+ const discriminator = params.channelIdOverride == null ? {
83206
+ kind: "minted",
83207
+ channelId
83208
+ } : {
83209
+ kind: "pinned",
83210
+ channelId
83211
+ };
82333
83212
  const msgNum = params.msgNumOverride ?? 0;
82334
83213
  const cmdId = params.cmdId;
82335
83214
  const extXml = params.extensionXml ?? "";
@@ -82355,9 +83234,10 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82355
83234
  const idleTimeoutMs = params.idleTimeoutMs ?? 2e3;
82356
83235
  const chunks = [];
82357
83236
  let streamMsgNum;
82358
- let lockedChannelId;
83237
+ let lockedChannelId = discriminator.kind === "minted" ? discriminator.channelId : void 0;
82359
83238
  let lockedStreamType;
82360
83239
  let idleTimer;
83240
+ const session = this.openReplaySession(discriminator);
82361
83241
  const looksLikeXml = (buf) => {
82362
83242
  let i = 0;
82363
83243
  while (i < buf.length && (buf[i] === 0 || buf[i] === 9 || buf[i] === 10 || buf[i] === 13 || buf[i] === 32)) i++;
@@ -82407,6 +83287,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82407
83287
  this.off("frame", onFrame);
82408
83288
  if (timeout) clearTimeout(timeout);
82409
83289
  if (idleTimer) clearTimeout(idleTimer);
83290
+ this.closeReplaySession(session);
82410
83291
  };
82411
83292
  const finish = (buf) => {
82412
83293
  if (done) return;
@@ -82428,8 +83309,15 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82428
83309
  };
82429
83310
  const onFrame = (frame) => {
82430
83311
  if (frame.header.cmdId !== cmdId) return;
83312
+ if (session.superseded) {
83313
+ this.countReplayDrop(session, frame, "superseded");
83314
+ return;
83315
+ }
82431
83316
  if (lockedStreamType !== void 0 && frame.header.streamType !== lockedStreamType) return;
82432
- if (lockedChannelId !== void 0 && frame.header.channelId !== lockedChannelId) return;
83317
+ if (lockedChannelId !== void 0 && frame.header.channelId !== lockedChannelId) {
83318
+ this.countReplayDrop(session, frame, "foreign-channel");
83319
+ return;
83320
+ }
82433
83321
  if (streamMsgNum !== void 0 && frame.header.msgNum !== streamMsgNum) return;
82434
83322
  const rc = frame.header.responseCode;
82435
83323
  if (rc >= 400 && rc < 6e4 && frame.header.msgNum === msgNum) {
@@ -82453,6 +83341,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82453
83341
  lockedChannelId = frame.header.channelId;
82454
83342
  lockedStreamType = frame.header.streamType;
82455
83343
  }
83344
+ session.acceptedFrames++;
82456
83345
  params.onChunk?.(decrypted);
82457
83346
  chunks.push(decrypted);
82458
83347
  armIdleFinish();
@@ -82466,7 +83355,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82466
83355
  finish(Buffer.concat(chunks));
82467
83356
  return;
82468
83357
  }
82469
- fail(/* @__PURE__ */ new Error(`Baichuan timeout waiting FileInfoList replay binary chunks cmdId=${cmdId} channelId=${channelId} streamType=${expectedStreamType}`));
83358
+ const seen = session.droppedFrames > 0 ? ` (rejected ${session.droppedFrames} cmd ${cmdId} frame(s) on channelId ${[...session.droppedChannelIds].join(",")})` : "";
83359
+ fail(/* @__PURE__ */ new Error(`Baichuan timeout waiting FileInfoList replay binary chunks cmdId=${cmdId} channelId=${channelId} streamType=${expectedStreamType}${seen}`));
82470
83360
  }, timeoutMs);
82471
83361
  this.on("frame", onFrame);
82472
83362
  try {
@@ -82883,8 +83773,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82883
83773
  const tryAs = (enc) => {
82884
83774
  try {
82885
83775
  if (enc.kind === "none") return buf;
82886
- if (enc.kind === "bc") return require_chunk_ZBACPERX.bcDecrypt(buf, channelId);
82887
- if (enc.kind === "aes" || enc.kind === "full_aes") return require_chunk_ZBACPERX.aesDecrypt(buf, enc.key);
83776
+ if (enc.kind === "bc") return require_chunk_OSVYEFMM.bcDecrypt(buf, channelId);
83777
+ if (enc.kind === "aes" || enc.kind === "full_aes") return require_chunk_OSVYEFMM.aesDecrypt(buf, enc.key);
82888
83778
  return null;
82889
83779
  } catch {
82890
83780
  return null;
@@ -82914,7 +83804,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82914
83804
  streamType: 0,
82915
83805
  msgNum,
82916
83806
  responseCode: encByte,
82917
- messageClass: require_chunk_ZBACPERX.BC_CLASS_LEGACY
83807
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_LEGACY
82918
83808
  });
82919
83809
  const pendingKey = `${cmdId}:${msgNum}`;
82920
83810
  const framePromise = new Promise((resolve, reject) => {
@@ -82946,15 +83836,15 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82946
83836
  else if (encType === 1) this.enc = { kind: "bc" };
82947
83837
  else if (encType === 2) this.enc = {
82948
83838
  kind: "aes",
82949
- key: require_chunk_ZBACPERX.deriveAesKey(nonce, this.opts.password)
83839
+ key: require_chunk_OSVYEFMM.deriveAesKey(nonce, this.opts.password)
82950
83840
  };
82951
83841
  else if (encType === 18) this.enc = {
82952
83842
  kind: "full_aes",
82953
- key: require_chunk_ZBACPERX.deriveAesKey(nonce, this.opts.password)
83843
+ key: require_chunk_OSVYEFMM.deriveAesKey(nonce, this.opts.password)
82954
83844
  };
82955
83845
  else throw new Error(`Baichuan login: unknown encType=0x${encType.toString(16)}`);
82956
- const userHash = require_chunk_ZBACPERX.md5StrModern(`${this.opts.username}${nonce}`);
82957
- const passHash = require_chunk_ZBACPERX.md5StrModern(`${this.opts.password}${nonce}`);
83846
+ const userHash = require_chunk_OSVYEFMM.md5StrModern(`${this.opts.username}${nonce}`);
83847
+ const passHash = require_chunk_OSVYEFMM.md5StrModern(`${this.opts.password}${nonce}`);
82958
83848
  const loginXml = buildLoginXml(userHash, passHash);
82959
83849
  this.logDebug("login_hash", {
82960
83850
  username: this.opts.username,
@@ -82969,7 +83859,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82969
83859
  cmdId: 1,
82970
83860
  payloadXml: loginXml,
82971
83861
  extensionXml: "",
82972
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
83862
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
82973
83863
  channelIdOverride: effectiveHostChannelId,
82974
83864
  encryption: loginEnc,
82975
83865
  timeoutMs: 1e4
@@ -83029,7 +83919,7 @@ async function* createNativeStream(api, channel, profile, options) {
83029
83919
  client = api.client;
83030
83920
  }
83031
83921
  }
83032
- const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
83922
+ const videoStream = new require_chunk_OSVYEFMM.BaichuanVideoStream({
83033
83923
  client,
83034
83924
  api,
83035
83925
  channel,
@@ -83547,7 +84437,7 @@ var H264Flow = class extends BaseFlow {
83547
84437
  super(transport, "H264");
83548
84438
  }
83549
84439
  extractParameterSets(accessUnitAnnexB) {
83550
- const nals = require_chunk_ZBACPERX.splitAnnexBToNalPayloads(accessUnitAnnexB);
84440
+ const nals = require_chunk_OSVYEFMM.splitAnnexBToNalPayloads(accessUnitAnnexB);
83551
84441
  for (const nal of nals) {
83552
84442
  if (nal.length < 1) continue;
83553
84443
  const nalType = (nal[0] ?? 0) & 31;
@@ -83592,9 +84482,9 @@ var H265Flow = class extends BaseFlow {
83592
84482
  super(transport, "H265");
83593
84483
  }
83594
84484
  extractParameterSets(accessUnitAnnexB) {
83595
- const vps = require_chunk_ZBACPERX.extractVpsFromAnnexB(accessUnitAnnexB);
83596
- const sps = require_chunk_ZBACPERX.extractSpsFromAnnexB(accessUnitAnnexB);
83597
- const pps = require_chunk_ZBACPERX.extractPpsFromAnnexB(accessUnitAnnexB);
84485
+ const vps = require_chunk_OSVYEFMM.extractVpsFromAnnexB(accessUnitAnnexB);
84486
+ const sps = require_chunk_OSVYEFMM.extractSpsFromAnnexB(accessUnitAnnexB);
84487
+ const pps = require_chunk_OSVYEFMM.extractPpsFromAnnexB(accessUnitAnnexB);
83598
84488
  if (vps && !this.vps) this.vps = vps;
83599
84489
  if (sps && !this.sps) this.sps = sps;
83600
84490
  if (pps && !this.pps) this.pps = pps;
@@ -83978,8 +84868,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
83978
84868
  /** Returns true if the raw (packed/Annex B) frame is an IDR (H.264) or IRAP (H.265). */
83979
84869
  isRawFrameKeyframe(frame) {
83980
84870
  try {
83981
- if (frame.videoType === "H264") return _BaichuanRtspServer.splitAnnexBNals(require_chunk_ZBACPERX.convertToAnnexB(frame.data)).some((n) => n.length >= 1 && (n[0] & 31) === 5);
83982
- if (frame.videoType === "H265") return require_chunk_ZBACPERX.splitAnnexBToNalPayloads2(require_chunk_ZBACPERX.convertToAnnexB2(frame.data)).some((n) => n.length >= 2 && require_chunk_ZBACPERX.isH265Irap(n[0] >> 1 & 63));
84871
+ if (frame.videoType === "H264") return _BaichuanRtspServer.splitAnnexBNals(require_chunk_OSVYEFMM.convertToAnnexB(frame.data)).some((n) => n.length >= 1 && (n[0] & 31) === 5);
84872
+ 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));
83983
84873
  } catch {}
83984
84874
  return false;
83985
84875
  }
@@ -84879,13 +85769,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
84879
85769
  if (resources?.rtpAudioTimestamp !== void 0) resources.rtpAudioTimestamp = resources.rtpAudioTimestamp + 1024 >>> 0;
84880
85770
  };
84881
85771
  const isH265IrapAccessUnit = (annexB) => {
84882
- const nals = require_chunk_ZBACPERX.splitAnnexBToNalPayloads2(annexB);
85772
+ const nals = require_chunk_OSVYEFMM.splitAnnexBToNalPayloads2(annexB);
84883
85773
  for (const nal of nals) {
84884
85774
  if (nal.length < 2) continue;
84885
85775
  const b0 = nal[0];
84886
85776
  if (b0 === void 0) continue;
84887
85777
  if ((b0 & 128) !== 0) continue;
84888
- if (require_chunk_ZBACPERX.isH265Irap(b0 >> 1 & 63)) return true;
85778
+ if (require_chunk_OSVYEFMM.isH265Irap(b0 >> 1 & 63)) return true;
84889
85779
  }
84890
85780
  return false;
84891
85781
  };
@@ -85041,7 +85931,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
85041
85931
  continue;
85042
85932
  }
85043
85933
  if (frame.videoType === "H264" || frame.videoType === "H265") {
85044
- const normalizedVideoData = frame.videoType === "H264" ? require_chunk_ZBACPERX.convertToAnnexB(frame.data) : require_chunk_ZBACPERX.convertToAnnexB2(frame.data);
85934
+ const normalizedVideoData = frame.videoType === "H264" ? require_chunk_OSVYEFMM.convertToAnnexB(frame.data) : require_chunk_OSVYEFMM.convertToAnnexB2(frame.data);
85045
85935
  if (frameCount === 0) this.setFlowVideoType(frame.videoType, "first video frame");
85046
85936
  if (!this.flow.getFmtp().hasParamSets) {
85047
85937
  this.flow.extractParameterSets(normalizedVideoData);
@@ -85057,7 +85947,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
85057
85947
  }
85058
85948
  if (useDirectRtp) {
85059
85949
  const videoType = frame.videoType ?? this.flow.videoType;
85060
- const normalizedVideoData = videoType === "H264" ? require_chunk_ZBACPERX.convertToAnnexB(frame.data) : require_chunk_ZBACPERX.convertToAnnexB2(frame.data);
85950
+ const normalizedVideoData = videoType === "H264" ? require_chunk_OSVYEFMM.convertToAnnexB(frame.data) : require_chunk_OSVYEFMM.convertToAnnexB2(frame.data);
85061
85951
  if (!resources?.seenFirstVideoKeyframe) if (videoType === "H265") {
85062
85952
  const { hasParamSets } = this.flow.getFmtp();
85063
85953
  if (!hasParamSets) {
@@ -85133,7 +86023,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
85133
86023
  const headHex = frame.data.subarray(0, 16).toString("hex");
85134
86024
  this.rtspDebugLog(`First video frame written to ffmpeg stdin for client ${clientId} (len=${frame.data.length}, head=${headHex})`);
85135
86025
  }
85136
- if (!stdin.write(frame.videoType === "H264" ? require_chunk_ZBACPERX.convertToAnnexB(frame.data) : frame.data)) await new Promise((resolve) => {
86026
+ if (!stdin.write(frame.videoType === "H264" ? require_chunk_OSVYEFMM.convertToAnnexB(frame.data) : frame.data)) await new Promise((resolve) => {
85137
86027
  if (stdin) stdin.once("drain", () => resolve());
85138
86028
  else resolve();
85139
86029
  });
@@ -85245,7 +86135,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
85245
86135
  ensureContinuousStream(dedicatedClient) {
85246
86136
  if (this.continuousStream) return this.continuousStream;
85247
86137
  const createLiveStream = async () => {
85248
- return new require_chunk_ZBACPERX.BaichuanVideoStream({
86138
+ return new require_chunk_OSVYEFMM.BaichuanVideoStream({
85249
86139
  client: dedicatedClient ?? this.api.client,
85250
86140
  api: this.api,
85251
86141
  channel: this.channel,
@@ -85902,6 +86792,433 @@ var MpegTsMuxer = class {
85902
86792
  this.tablesSent = false;
85903
86793
  }
85904
86794
  };
86795
+ var buildDetectionClasses = (parsed, recordType) => {
86796
+ const classes = [];
86797
+ const flags = parsed?.flags;
86798
+ if (flags) {
86799
+ if (flags.aiPerson) classes.push("person");
86800
+ if (flags.aiVehicle) classes.push("vehicle");
86801
+ if (flags.aiAnimal) classes.push("animal");
86802
+ if (flags.aiFace) classes.push("face");
86803
+ if (flags.motion) classes.push("motion");
86804
+ if (flags.schedule) classes.push("schedule");
86805
+ if (flags.doorbell) classes.push("doorbell");
86806
+ if (flags.package) classes.push("package");
86807
+ if (flags.rf) classes.push("rf");
86808
+ if (flags.aiOther) classes.push("other");
86809
+ }
86810
+ if (classes.length === 0 && recordType) {
86811
+ const rt = recordType.toLowerCase();
86812
+ if (rt.includes("people") || rt.includes("person")) classes.push("person");
86813
+ if (rt.includes("vehicle")) classes.push("vehicle");
86814
+ if (rt.includes("dog_cat") || rt.includes("animal")) classes.push("animal");
86815
+ if (rt.includes("face")) classes.push("face");
86816
+ if (rt.includes("md") || rt.includes("motion")) classes.push("motion");
86817
+ if (rt.includes("sched")) classes.push("schedule");
86818
+ if (rt.includes("visitor") || rt.includes("doorbell")) classes.push("doorbell");
86819
+ if (rt.includes("package")) classes.push("package");
86820
+ if (rt.includes("rf")) classes.push("rf");
86821
+ }
86822
+ if (classes.length === 0) classes.push("motion");
86823
+ return classes;
86824
+ };
86825
+ var getXmlTexts = (xml, tags) => {
86826
+ const out = {};
86827
+ for (const tag2 of tags) {
86828
+ const v = getXmlText(xml, tag2);
86829
+ if (v !== void 0) out[tag2] = v;
86830
+ }
86831
+ return out;
86832
+ };
86833
+ var getXmlBlocks = (xml, tagName) => {
86834
+ const re = new RegExp(`<${tagName}\\b[^>]*>([\\s\\S]*?)<\\/${tagName}>`, "g");
86835
+ const out = [];
86836
+ let match;
86837
+ while (match = re.exec(xml)) out.push(match[1] ?? "");
86838
+ return out;
86839
+ };
86840
+ var parseXmlDateTimeBlock = (block, timeZone) => {
86841
+ const year = Number.parseInt(getXmlText(block, "year") ?? "", 10);
86842
+ const month = Number.parseInt(getXmlText(block, "month") ?? "", 10);
86843
+ const day = Number.parseInt(getXmlText(block, "day") ?? "", 10);
86844
+ const hour = Number.parseInt(getXmlText(block, "hour") ?? "", 10);
86845
+ const minute = Number.parseInt(getXmlText(block, "minute") ?? "", 10);
86846
+ const second = Number.parseInt(getXmlText(block, "second") ?? "", 10);
86847
+ if ([
86848
+ year,
86849
+ month,
86850
+ day,
86851
+ hour,
86852
+ minute,
86853
+ second
86854
+ ].every(Number.isFinite)) return dateFromWallClock({
86855
+ year,
86856
+ month,
86857
+ day,
86858
+ hour,
86859
+ minute,
86860
+ second
86861
+ }, timeZone);
86862
+ const m = block.replace(/<[^>]*>/g, "").trim().match(/(\d{4})[-/](\d{1,2})[-/](\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?/);
86863
+ if (!m) return void 0;
86864
+ const y = Number.parseInt(m[1] ?? "", 10);
86865
+ const mo = Number.parseInt(m[2] ?? "", 10);
86866
+ const da = Number.parseInt(m[3] ?? "", 10);
86867
+ const ho = Number.parseInt(m[4] ?? "0", 10);
86868
+ const mi = Number.parseInt(m[5] ?? "0", 10);
86869
+ const se = Number.parseInt(m[6] ?? "0", 10);
86870
+ if (![
86871
+ y,
86872
+ mo,
86873
+ da,
86874
+ ho,
86875
+ mi,
86876
+ se
86877
+ ].every(Number.isFinite)) return void 0;
86878
+ return dateFromWallClock({
86879
+ year: y,
86880
+ month: mo,
86881
+ day: da,
86882
+ hour: ho,
86883
+ minute: mi,
86884
+ second: se
86885
+ }, timeZone);
86886
+ };
86887
+ var parseRecordingFilesFromXml = (xml, options) => {
86888
+ const timeZone = options?.timeZone;
86889
+ const fileNameOptions = timeZone !== void 0 ? { timeZone } : void 0;
86890
+ const out = [];
86891
+ const fileInfoBlocks = getXmlBlocks(xml, "FileInfo");
86892
+ for (const b of fileInfoBlocks) {
86893
+ const id = getXmlText(b, "Id") ?? getXmlText(b, "ID") ?? getXmlText(b, "id");
86894
+ const name = getXmlText(b, "name") ?? getXmlText(b, "fileName");
86895
+ const chosen = (id ?? name)?.trim();
86896
+ if (!chosen) continue;
86897
+ const item = { fileName: chosen };
86898
+ if (name != null && name.trim()) item.name = name.trim();
86899
+ if (id != null && id.trim()) item.id = id.trim();
86900
+ const recordType = getXmlText(b, "type") ?? getXmlText(b, "recordType") ?? getXmlText(b, "alarmType");
86901
+ if (recordType != null) item.recordType = recordType;
86902
+ const sizeText = getXmlText(b, "size") ?? getXmlText(b, "fileSize");
86903
+ const sizeBytes = sizeText ? Number.parseInt(sizeText, 10) : void 0;
86904
+ if (sizeBytes != null && Number.isFinite(sizeBytes)) item.sizeBytes = sizeBytes;
86905
+ const start = getXmlBlocks(b, "startTime")[0];
86906
+ const end = getXmlBlocks(b, "endTime")[0];
86907
+ const startDt = start ? parseXmlDateTimeBlock(start, timeZone) : void 0;
86908
+ const endDt = end ? parseXmlDateTimeBlock(end, timeZone) : void 0;
86909
+ if (startDt) item.startTime = startDt;
86910
+ if (endDt) item.endTime = endDt;
86911
+ const parsed = parseRecordingFileName(item.name ?? item.fileName, fileNameOptions);
86912
+ const parsedFromPath = item.fileName !== item.name ? parseRecordingFileName(item.fileName, fileNameOptions) : void 0;
86913
+ const bestParsed = parsedFromPath?.sizeBytes != null ? parsedFromPath : parsed;
86914
+ if (bestParsed) {
86915
+ item.parsedFileName = bestParsed;
86916
+ if (!item.startTime) item.startTime = bestParsed.start;
86917
+ if (!item.endTime) item.endTime = bestParsed.end;
86918
+ if (item.sizeBytes == null && bestParsed.sizeBytes != null) item.sizeBytes = bestParsed.sizeBytes;
86919
+ }
86920
+ item.detectionClasses = buildDetectionClasses(parsed, item.recordType);
86921
+ out.push(item);
86922
+ }
86923
+ const fileBlocks = getXmlBlocks(xml, "File");
86924
+ for (const b of fileBlocks) {
86925
+ const fileName = (getXmlText(b, "fileName") ?? getXmlText(b, "name"))?.trim();
86926
+ if (!fileName) continue;
86927
+ const sizeText = getXmlText(b, "size") ?? getXmlText(b, "fileSize");
86928
+ const sizeBytes = sizeText ? Number.parseInt(sizeText, 10) : void 0;
86929
+ const recordType = getXmlText(b, "type") ?? getXmlText(b, "recordType") ?? getXmlText(b, "alarmType");
86930
+ const start = getXmlBlocks(b, "startTime")[0];
86931
+ const end = getXmlBlocks(b, "endTime")[0];
86932
+ const item = { fileName };
86933
+ if (sizeBytes != null && Number.isFinite(sizeBytes)) item.sizeBytes = sizeBytes;
86934
+ if (recordType != null) item.recordType = recordType;
86935
+ const startDt = start ? parseXmlDateTimeBlock(start, timeZone) : void 0;
86936
+ const endDt = end ? parseXmlDateTimeBlock(end, timeZone) : void 0;
86937
+ if (startDt) item.startTime = startDt;
86938
+ if (endDt) item.endTime = endDt;
86939
+ const parsed = parseRecordingFileName(item.fileName, fileNameOptions);
86940
+ if (parsed) {
86941
+ item.parsedFileName = parsed;
86942
+ if (!item.startTime) item.startTime = parsed.start;
86943
+ if (!item.endTime) item.endTime = parsed.end;
86944
+ if (item.sizeBytes == null && parsed.sizeBytes != null) item.sizeBytes = parsed.sizeBytes;
86945
+ }
86946
+ item.detectionClasses = buildDetectionClasses(parsed, item.recordType);
86947
+ out.push(item);
86948
+ }
86949
+ if (out.length === 0) {
86950
+ const re = /<fileName>([\s\S]*?)<\/fileName>/g;
86951
+ const seenNames = /* @__PURE__ */ new Set();
86952
+ let match;
86953
+ while (match = re.exec(xml)) {
86954
+ const fileName = (match[1] ?? "").trim();
86955
+ if (!fileName) continue;
86956
+ if (seenNames.has(fileName)) continue;
86957
+ seenNames.add(fileName);
86958
+ const item = { fileName };
86959
+ const parsed = parseRecordingFileName(fileName, fileNameOptions);
86960
+ if (parsed) {
86961
+ item.parsedFileName = parsed;
86962
+ item.startTime = parsed.start;
86963
+ item.endTime = parsed.end;
86964
+ }
86965
+ item.detectionClasses = buildDetectionClasses(parsed, void 0);
86966
+ out.push(item);
86967
+ }
86968
+ }
86969
+ const alarmBlocks = getXmlBlocks(xml, "alarmVideo");
86970
+ if (alarmBlocks.length > 0) {
86971
+ const byName = /* @__PURE__ */ new Map();
86972
+ for (const existing of out) {
86973
+ const key = existing.fileName.trim();
86974
+ if (!key) continue;
86975
+ if (!byName.has(key)) byName.set(key, existing);
86976
+ }
86977
+ for (const b of alarmBlocks) {
86978
+ const fileName = (getXmlText(b, "fileName") ?? getXmlText(b, "name"))?.trim();
86979
+ if (!fileName) continue;
86980
+ const alarmId = (getXmlText(b, "fileId") ?? getXmlText(b, "recFileId") ?? getXmlText(b, "recFileName") ?? getXmlText(b, "recFile") ?? getXmlText(b, "recordFile") ?? getXmlText(b, "Id") ?? getXmlText(b, "ID") ?? getXmlText(b, "id") ?? "").trim();
86981
+ const alarmType = getXmlText(b, "alarmType")?.trim();
86982
+ const start = getXmlBlocks(b, "startTime")[0];
86983
+ const end = getXmlBlocks(b, "endTime")[0];
86984
+ const startDt = start ? parseXmlDateTimeBlock(start, timeZone) : void 0;
86985
+ const endDt = end ? parseXmlDateTimeBlock(end, timeZone) : void 0;
86986
+ const target = byName.get(fileName) ?? { fileName };
86987
+ if (alarmId && !target.id) target.id = alarmId;
86988
+ if (alarmType) target.recordType = alarmType;
86989
+ if (startDt) target.startTime = startDt;
86990
+ if (endDt) target.endTime = endDt;
86991
+ if (!target.parsedFileName) {
86992
+ const parsed = parseRecordingFileName(target.fileName, fileNameOptions);
86993
+ if (parsed) {
86994
+ target.parsedFileName = parsed;
86995
+ if (!target.startTime) target.startTime = parsed.start;
86996
+ if (!target.endTime) target.endTime = parsed.end;
86997
+ }
86998
+ }
86999
+ if (!byName.has(fileName)) {
87000
+ out.push(target);
87001
+ byName.set(fileName, target);
87002
+ }
87003
+ }
87004
+ }
87005
+ const seen = /* @__PURE__ */ new Set();
87006
+ return out.filter((f) => {
87007
+ const key = f.fileName.trim();
87008
+ if (seen.has(key)) return false;
87009
+ seen.add(key);
87010
+ return true;
87011
+ });
87012
+ };
87013
+ function parseTalkAudioConfig(block) {
87014
+ const audioType = getXmlText(block, "audioType");
87015
+ const sampleRate = Number.parseInt(getXmlText(block, "sampleRate") ?? "", 10);
87016
+ const samplePrecision = Number.parseInt(getXmlText(block, "samplePrecision") ?? "", 10);
87017
+ const lengthPerEncoder = Number.parseInt(getXmlText(block, "lengthPerEncoder") ?? "", 10);
87018
+ const soundTrack = getXmlText(block, "soundTrack");
87019
+ const priorityText = getXmlText(block, "priority");
87020
+ if (!audioType || !Number.isFinite(sampleRate) || !Number.isFinite(samplePrecision) || !Number.isFinite(lengthPerEncoder) || !soundTrack) return null;
87021
+ const config = {
87022
+ audioType,
87023
+ sampleRate,
87024
+ samplePrecision,
87025
+ lengthPerEncoder,
87026
+ soundTrack
87027
+ };
87028
+ if (priorityText !== void 0) {
87029
+ const pr = Number.parseInt(priorityText, 10);
87030
+ if (Number.isFinite(pr)) config.priority = pr;
87031
+ }
87032
+ return config;
87033
+ }
87034
+ var parseTalkAbilityXml = (xml) => {
87035
+ const talkAbilityBlock = getXmlBlocks(xml, "TalkAbility")[0];
87036
+ if (!talkAbilityBlock) throw new Error("TalkAbility XML not found in response");
87037
+ return {
87038
+ duplexList: getXmlBlocks(talkAbilityBlock, "duplexList").map((b) => getXmlText(b, "duplex")).filter((v) => Boolean(v)),
87039
+ audioStreamModeList: getXmlBlocks(talkAbilityBlock, "audioStreamModeList").map((b) => getXmlText(b, "audioStreamMode")).filter((v) => Boolean(v)),
87040
+ audioConfigList: getXmlBlocks(talkAbilityBlock, "audioConfig").map(parseTalkAudioConfig).filter((v) => Boolean(v))
87041
+ };
87042
+ };
87043
+ function parseNumber(v) {
87044
+ if (v == null) return void 0;
87045
+ const n = Number(v);
87046
+ return Number.isFinite(n) ? n : void 0;
87047
+ }
87048
+ function parseBoolean01(v) {
87049
+ if (v == null) return void 0;
87050
+ const s = v.trim();
87051
+ if (s === "1" || s.toLowerCase() === "true") return true;
87052
+ if (s === "0" || s.toLowerCase() === "false") return false;
87053
+ }
87054
+ var BYTES_PER_MB = 1024 * 1024;
87055
+ var num = (xml, tag2) => {
87056
+ const value = parseNumber(getXmlText(xml, tag2));
87057
+ return value === void 0 ? null : value;
87058
+ };
87059
+ var megabytes$1 = (bytes, whole, remainder) => {
87060
+ if (bytes !== null) return bytes / BYTES_PER_MB;
87061
+ if (whole === null && remainder === null) return null;
87062
+ return (whole ?? 0) * 1024 + (remainder ?? 0);
87063
+ };
87064
+ var parseHddInfoListXml = (xml) => {
87065
+ const list = getXmlBlocks(xml, "HddInfoList")[0];
87066
+ if (list === void 0) return [];
87067
+ return getXmlBlocks(list, "HddInfo").map((block) => {
87068
+ const capacity = num(block, "capacity");
87069
+ const capacityM = num(block, "capacityM");
87070
+ const capacityBytes = num(block, "capacityV2");
87071
+ const remainSize = num(block, "remainSize");
87072
+ const remainSizeM = num(block, "remainSizeM");
87073
+ const remainSizeBytes = num(block, "remainSizeV2");
87074
+ return {
87075
+ number: num(block, "number"),
87076
+ capacity,
87077
+ capacityM,
87078
+ capacityBytes,
87079
+ remainSize,
87080
+ remainSizeM,
87081
+ remainSizeBytes,
87082
+ format: num(block, "format"),
87083
+ mount: num(block, "mount"),
87084
+ type: num(block, "type"),
87085
+ index: num(block, "index"),
87086
+ capacityMb: megabytes$1(capacityBytes, capacity, capacityM),
87087
+ remainMb: megabytes$1(remainSizeBytes, remainSize, remainSizeM)
87088
+ };
87089
+ });
87090
+ };
87091
+ var DEFAULT_PRE_RECORD_SECONDS = 10;
87092
+ var parseRecordCfgLimits = (xml) => {
87093
+ const block = getXmlBlocks(xml, "RecordCfg")[0] ?? "";
87094
+ const numbersIn = (container, tag2) => {
87095
+ const outer = getXmlBlocks(block, container)[0];
87096
+ if (outer === void 0) return [];
87097
+ return getXmlBlocks(outer, tag2).map((t) => parseNumber(t.trim())).filter((n) => n !== void 0);
87098
+ };
87099
+ const pre = parseNumber(getXmlText(block, "preRecordTime"));
87100
+ return {
87101
+ cycleList: numbersIn("cyclelist", "item"),
87102
+ recordDelayTimeChoices: numbersIn("timeList", "time"),
87103
+ preRecordSupported: pre !== void 0,
87104
+ preRecordSeconds: pre === void 0 || pre === 0 ? null : pre
87105
+ };
87106
+ };
87107
+ var parseRecordScheduleRows = (xml) => {
87108
+ const block = getXmlBlocks(xml, "Record")[0];
87109
+ const list = block === void 0 ? void 0 : getXmlBlocks(block, "typeScheduleList")[0];
87110
+ if (list === void 0) return [];
87111
+ const rows = [];
87112
+ for (const item of getXmlBlocks(list, "item")) {
87113
+ const type = (getXmlText(item, "type") ?? "").trim();
87114
+ if (type === "") continue;
87115
+ const index = parseNumber(getXmlText(item, "index"));
87116
+ rows.push({
87117
+ type,
87118
+ index: index === void 0 ? null : index,
87119
+ valueTable: (getXmlText(item, "valueTable") ?? "").trim()
87120
+ });
87121
+ }
87122
+ return rows;
87123
+ };
87124
+ var BaichuanRecordConfigError = class extends Error {
87125
+ constructor(message) {
87126
+ super(message);
87127
+ this.name = "BaichuanRecordConfigError";
87128
+ }
87129
+ };
87130
+ var requireInteger = (field, value) => {
87131
+ if (!Number.isInteger(value)) throw new BaichuanRecordConfigError(`${field} must be a whole number, got ${String(value)}`);
87132
+ };
87133
+ var validateRecordCfgPatch = (patch, limits) => {
87134
+ if (patch.cycle !== void 0) {
87135
+ requireInteger("cycle", patch.cycle);
87136
+ if (limits.cycleList.length > 0 && !limits.cycleList.includes(patch.cycle)) throw new BaichuanRecordConfigError(`cycle ${patch.cycle} is not one of the values this camera accepts (${limits.cycleList.join(", ")}); it would answer 200 and keep the old value`);
87137
+ }
87138
+ if (patch.recordDelayTime !== void 0) {
87139
+ requireInteger("recordDelayTime", patch.recordDelayTime);
87140
+ if (patch.recordDelayTime < 0) throw new BaichuanRecordConfigError(`recordDelayTime must not be negative, got ${patch.recordDelayTime}`);
87141
+ }
87142
+ if (patch.packageTime !== void 0) {
87143
+ requireInteger("packageTime", patch.packageTime);
87144
+ if (patch.packageTime < 1) throw new BaichuanRecordConfigError(`packageTime is a length in minutes and must be at least 1, got ${patch.packageTime}`);
87145
+ }
87146
+ if (patch.preRecordEnabled !== void 0 && !limits.preRecordSupported) throw new BaichuanRecordConfigError("this camera does not report a pre-record setting, so writing one would do nothing");
87147
+ };
87148
+ var isWeeklyMask = (mask) => mask.length === 168 && /^[01]+$/.test(mask);
87149
+ var validateRecordSchedulePatch = (patch, rows) => {
87150
+ for (const entry of patch.entries ?? []) {
87151
+ if (!isWeeklyMask(entry.valueTable)) throw new BaichuanRecordConfigError(`the weekly mask for ${describeEntry(entry)} must be exactly 168 characters of 0 or 1, got ${entry.valueTable.length}; the camera would answer 200 and keep the old mask`);
87152
+ if (!rows.some((row) => row.type === entry.type && (entry.index === void 0 || row.index === entry.index))) throw new BaichuanRecordConfigError(`this camera has no recording schedule for ${describeEntry(entry)}; it lists ${rows.map(describeRow).join(", ") || "nothing"}`);
87153
+ }
87154
+ };
87155
+ var describeEntry = (entry) => entry.index === void 0 ? entry.type : `${entry.type}[${entry.index}]`;
87156
+ var describeRow = (row) => row.index === null ? row.type : `${row.type}[${row.index}]`;
87157
+ var replaceTag = (xml, tag2, value) => xml.replace(new RegExp(`<${tag2}>[^<]*</${tag2}>`), `<${tag2}>${value}</${tag2}>`);
87158
+ var buildRecordCfgSetXml = (currentXml, patch, limits) => {
87159
+ let xml = currentXml;
87160
+ if (patch.cycle !== void 0) xml = replaceTag(xml, "cycle", String(patch.cycle));
87161
+ if (patch.recordDelayTime !== void 0) xml = replaceTag(xml, "recordDelayTime", String(patch.recordDelayTime));
87162
+ if (patch.packageTime !== void 0) xml = replaceTag(xml, "packageTime", String(patch.packageTime));
87163
+ if (patch.preRecordEnabled !== void 0) {
87164
+ const seconds = patch.preRecordEnabled ? limits.preRecordSeconds ?? DEFAULT_PRE_RECORD_SECONDS : 0;
87165
+ xml = replaceTag(xml, "preRecordTime", String(seconds));
87166
+ }
87167
+ return xml;
87168
+ };
87169
+ var buildRecordScheduleSetXml = (currentXml, patch) => {
87170
+ let xml = currentXml;
87171
+ if (patch.enable !== void 0) xml = replaceTag(xml, "enable", patch.enable ? "1" : "0");
87172
+ for (const entry of patch.entries ?? []) xml = replaceItemValueTable(xml, entry);
87173
+ return xml;
87174
+ };
87175
+ var ITEM_RE = /<item>([\s\S]*?)<\/item>/g;
87176
+ var replaceItemValueTable = (xml, entry) => xml.replace(ITEM_RE, (whole, inner) => {
87177
+ if ((getXmlText(inner, "type") ?? "").trim() !== entry.type) return whole;
87178
+ if (entry.index !== void 0) {
87179
+ if (parseNumber(getXmlText(inner, "index")) !== entry.index) return whole;
87180
+ }
87181
+ if (getXmlText(inner, "valueTable") === void 0) return whole;
87182
+ return `<item>${replaceTag(inner, "valueTable", entry.valueTable)}</item>`;
87183
+ });
87184
+ var verdict = (outcomes) => ({
87185
+ outcomes,
87186
+ allApplied: outcomes.every((o) => o.status === "applied")
87187
+ });
87188
+ var compare = (field, requested, actual) => actual === requested ? {
87189
+ field,
87190
+ status: "applied",
87191
+ value: requested
87192
+ } : {
87193
+ field,
87194
+ status: "ignored",
87195
+ requested,
87196
+ actual: actual ?? "(absent)"
87197
+ };
87198
+ var diffRecordCfg = (patch, afterXml) => {
87199
+ const block = getXmlBlocks(afterXml, "RecordCfg")[0] ?? "";
87200
+ const outcomes = [];
87201
+ if (patch.cycle !== void 0) outcomes.push(compare("cycle", String(patch.cycle), getXmlText(block, "cycle")));
87202
+ if (patch.recordDelayTime !== void 0) outcomes.push(compare("recordDelayTime", String(patch.recordDelayTime), getXmlText(block, "recordDelayTime")));
87203
+ if (patch.packageTime !== void 0) outcomes.push(compare("packageTime", String(patch.packageTime), getXmlText(block, "packageTime")));
87204
+ if (patch.preRecordEnabled !== void 0) {
87205
+ const seconds = parseNumber(getXmlText(block, "preRecordTime"));
87206
+ const on = seconds !== void 0 && seconds !== 0;
87207
+ outcomes.push(compare("preRecordEnabled", String(patch.preRecordEnabled), seconds === void 0 ? void 0 : String(on)));
87208
+ }
87209
+ return verdict(outcomes);
87210
+ };
87211
+ var diffRecordSchedule = (patch, afterXml) => {
87212
+ const outcomes = [];
87213
+ const block = getXmlBlocks(afterXml, "Record")[0] ?? "";
87214
+ if (patch.enable !== void 0) outcomes.push(compare("enable", patch.enable ? "1" : "0", getXmlText(block, "enable")));
87215
+ const rows = parseRecordScheduleRows(afterXml);
87216
+ for (const entry of patch.entries ?? []) {
87217
+ const row = rows.find((r) => r.type === entry.type && (entry.index === void 0 || r.index === entry.index));
87218
+ outcomes.push(compare(`schedule.${describeEntry(entry)}`, entry.valueTable, row === void 0 ? void 0 : row.valueTable));
87219
+ }
87220
+ return verdict(outcomes);
87221
+ };
85905
87222
  function toNumberOrUndefined(value) {
85906
87223
  if (value == null) return void 0;
85907
87224
  const n = Number(value);
@@ -86515,254 +87832,6 @@ var estimateVideoTiming = (params) => {
86515
87832
  fpsSource: "unknown"
86516
87833
  };
86517
87834
  };
86518
- var buildDetectionClasses = (parsed, recordType) => {
86519
- const classes = [];
86520
- const flags = parsed?.flags;
86521
- if (flags) {
86522
- if (flags.aiPerson) classes.push("person");
86523
- if (flags.aiVehicle) classes.push("vehicle");
86524
- if (flags.aiAnimal) classes.push("animal");
86525
- if (flags.aiFace) classes.push("face");
86526
- if (flags.motion) classes.push("motion");
86527
- if (flags.schedule) classes.push("schedule");
86528
- if (flags.doorbell) classes.push("doorbell");
86529
- if (flags.package) classes.push("package");
86530
- if (flags.rf) classes.push("rf");
86531
- if (flags.aiOther) classes.push("other");
86532
- }
86533
- if (classes.length === 0 && recordType) {
86534
- const rt = recordType.toLowerCase();
86535
- if (rt.includes("people") || rt.includes("person")) classes.push("person");
86536
- if (rt.includes("vehicle")) classes.push("vehicle");
86537
- if (rt.includes("dog_cat") || rt.includes("animal")) classes.push("animal");
86538
- if (rt.includes("face")) classes.push("face");
86539
- if (rt.includes("md") || rt.includes("motion")) classes.push("motion");
86540
- if (rt.includes("sched")) classes.push("schedule");
86541
- if (rt.includes("visitor") || rt.includes("doorbell")) classes.push("doorbell");
86542
- if (rt.includes("package")) classes.push("package");
86543
- if (rt.includes("rf")) classes.push("rf");
86544
- }
86545
- if (classes.length === 0) classes.push("motion");
86546
- return classes;
86547
- };
86548
- var getXmlTexts = (xml, tags) => {
86549
- const out = {};
86550
- for (const tag2 of tags) {
86551
- const v = getXmlText(xml, tag2);
86552
- if (v !== void 0) out[tag2] = v;
86553
- }
86554
- return out;
86555
- };
86556
- var getXmlBlocks = (xml, tagName) => {
86557
- const re = new RegExp(`<${tagName}\\b[^>]*>([\\s\\S]*?)<\\/${tagName}>`, "g");
86558
- const out = [];
86559
- let match;
86560
- while (match = re.exec(xml)) out.push(match[1] ?? "");
86561
- return out;
86562
- };
86563
- var parseXmlDateTimeBlock = (block, timeZone) => {
86564
- const year = Number.parseInt(getXmlText(block, "year") ?? "", 10);
86565
- const month = Number.parseInt(getXmlText(block, "month") ?? "", 10);
86566
- const day = Number.parseInt(getXmlText(block, "day") ?? "", 10);
86567
- const hour = Number.parseInt(getXmlText(block, "hour") ?? "", 10);
86568
- const minute = Number.parseInt(getXmlText(block, "minute") ?? "", 10);
86569
- const second = Number.parseInt(getXmlText(block, "second") ?? "", 10);
86570
- if ([
86571
- year,
86572
- month,
86573
- day,
86574
- hour,
86575
- minute,
86576
- second
86577
- ].every(Number.isFinite)) return dateFromWallClock({
86578
- year,
86579
- month,
86580
- day,
86581
- hour,
86582
- minute,
86583
- second
86584
- }, timeZone);
86585
- const m = block.replace(/<[^>]*>/g, "").trim().match(/(\d{4})[-/](\d{1,2})[-/](\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?/);
86586
- if (!m) return void 0;
86587
- const y = Number.parseInt(m[1] ?? "", 10);
86588
- const mo = Number.parseInt(m[2] ?? "", 10);
86589
- const da = Number.parseInt(m[3] ?? "", 10);
86590
- const ho = Number.parseInt(m[4] ?? "0", 10);
86591
- const mi = Number.parseInt(m[5] ?? "0", 10);
86592
- const se = Number.parseInt(m[6] ?? "0", 10);
86593
- if (![
86594
- y,
86595
- mo,
86596
- da,
86597
- ho,
86598
- mi,
86599
- se
86600
- ].every(Number.isFinite)) return void 0;
86601
- return dateFromWallClock({
86602
- year: y,
86603
- month: mo,
86604
- day: da,
86605
- hour: ho,
86606
- minute: mi,
86607
- second: se
86608
- }, timeZone);
86609
- };
86610
- var parseRecordingFilesFromXml = (xml, options) => {
86611
- const timeZone = options?.timeZone;
86612
- const fileNameOptions = timeZone !== void 0 ? { timeZone } : void 0;
86613
- const out = [];
86614
- const fileInfoBlocks = getXmlBlocks(xml, "FileInfo");
86615
- for (const b of fileInfoBlocks) {
86616
- const id = getXmlText(b, "Id") ?? getXmlText(b, "ID") ?? getXmlText(b, "id");
86617
- const name = getXmlText(b, "name") ?? getXmlText(b, "fileName");
86618
- const chosen = (id ?? name)?.trim();
86619
- if (!chosen) continue;
86620
- const item = { fileName: chosen };
86621
- if (name != null && name.trim()) item.name = name.trim();
86622
- if (id != null && id.trim()) item.id = id.trim();
86623
- const recordType = getXmlText(b, "type") ?? getXmlText(b, "recordType") ?? getXmlText(b, "alarmType");
86624
- if (recordType != null) item.recordType = recordType;
86625
- const sizeText = getXmlText(b, "size") ?? getXmlText(b, "fileSize");
86626
- const sizeBytes = sizeText ? Number.parseInt(sizeText, 10) : void 0;
86627
- if (sizeBytes != null && Number.isFinite(sizeBytes)) item.sizeBytes = sizeBytes;
86628
- const start = getXmlBlocks(b, "startTime")[0];
86629
- const end = getXmlBlocks(b, "endTime")[0];
86630
- const startDt = start ? parseXmlDateTimeBlock(start, timeZone) : void 0;
86631
- const endDt = end ? parseXmlDateTimeBlock(end, timeZone) : void 0;
86632
- if (startDt) item.startTime = startDt;
86633
- if (endDt) item.endTime = endDt;
86634
- const parsed = parseRecordingFileName(item.name ?? item.fileName, fileNameOptions);
86635
- const parsedFromPath = item.fileName !== item.name ? parseRecordingFileName(item.fileName, fileNameOptions) : void 0;
86636
- const bestParsed = parsedFromPath?.sizeBytes != null ? parsedFromPath : parsed;
86637
- if (bestParsed) {
86638
- item.parsedFileName = bestParsed;
86639
- if (!item.startTime) item.startTime = bestParsed.start;
86640
- if (!item.endTime) item.endTime = bestParsed.end;
86641
- if (item.sizeBytes == null && bestParsed.sizeBytes != null) item.sizeBytes = bestParsed.sizeBytes;
86642
- }
86643
- item.detectionClasses = buildDetectionClasses(parsed, item.recordType);
86644
- out.push(item);
86645
- }
86646
- const fileBlocks = getXmlBlocks(xml, "File");
86647
- for (const b of fileBlocks) {
86648
- const fileName = (getXmlText(b, "fileName") ?? getXmlText(b, "name"))?.trim();
86649
- if (!fileName) continue;
86650
- const sizeText = getXmlText(b, "size") ?? getXmlText(b, "fileSize");
86651
- const sizeBytes = sizeText ? Number.parseInt(sizeText, 10) : void 0;
86652
- const recordType = getXmlText(b, "type") ?? getXmlText(b, "recordType") ?? getXmlText(b, "alarmType");
86653
- const start = getXmlBlocks(b, "startTime")[0];
86654
- const end = getXmlBlocks(b, "endTime")[0];
86655
- const item = { fileName };
86656
- if (sizeBytes != null && Number.isFinite(sizeBytes)) item.sizeBytes = sizeBytes;
86657
- if (recordType != null) item.recordType = recordType;
86658
- const startDt = start ? parseXmlDateTimeBlock(start, timeZone) : void 0;
86659
- const endDt = end ? parseXmlDateTimeBlock(end, timeZone) : void 0;
86660
- if (startDt) item.startTime = startDt;
86661
- if (endDt) item.endTime = endDt;
86662
- const parsed = parseRecordingFileName(item.fileName, fileNameOptions);
86663
- if (parsed) {
86664
- item.parsedFileName = parsed;
86665
- if (!item.startTime) item.startTime = parsed.start;
86666
- if (!item.endTime) item.endTime = parsed.end;
86667
- if (item.sizeBytes == null && parsed.sizeBytes != null) item.sizeBytes = parsed.sizeBytes;
86668
- }
86669
- item.detectionClasses = buildDetectionClasses(parsed, item.recordType);
86670
- out.push(item);
86671
- }
86672
- if (out.length === 0) {
86673
- const re = /<fileName>([\s\S]*?)<\/fileName>/g;
86674
- const seenNames = /* @__PURE__ */ new Set();
86675
- let match;
86676
- while (match = re.exec(xml)) {
86677
- const fileName = (match[1] ?? "").trim();
86678
- if (!fileName) continue;
86679
- if (seenNames.has(fileName)) continue;
86680
- seenNames.add(fileName);
86681
- const item = { fileName };
86682
- const parsed = parseRecordingFileName(fileName, fileNameOptions);
86683
- if (parsed) {
86684
- item.parsedFileName = parsed;
86685
- item.startTime = parsed.start;
86686
- item.endTime = parsed.end;
86687
- }
86688
- item.detectionClasses = buildDetectionClasses(parsed, void 0);
86689
- out.push(item);
86690
- }
86691
- }
86692
- const alarmBlocks = getXmlBlocks(xml, "alarmVideo");
86693
- if (alarmBlocks.length > 0) {
86694
- const byName = /* @__PURE__ */ new Map();
86695
- for (const existing of out) {
86696
- const key = existing.fileName.trim();
86697
- if (!key) continue;
86698
- if (!byName.has(key)) byName.set(key, existing);
86699
- }
86700
- for (const b of alarmBlocks) {
86701
- const fileName = (getXmlText(b, "fileName") ?? getXmlText(b, "name"))?.trim();
86702
- if (!fileName) continue;
86703
- const alarmId = (getXmlText(b, "fileId") ?? getXmlText(b, "recFileId") ?? getXmlText(b, "recFileName") ?? getXmlText(b, "recFile") ?? getXmlText(b, "recordFile") ?? getXmlText(b, "Id") ?? getXmlText(b, "ID") ?? getXmlText(b, "id") ?? "").trim();
86704
- const alarmType = getXmlText(b, "alarmType")?.trim();
86705
- const start = getXmlBlocks(b, "startTime")[0];
86706
- const end = getXmlBlocks(b, "endTime")[0];
86707
- const startDt = start ? parseXmlDateTimeBlock(start, timeZone) : void 0;
86708
- const endDt = end ? parseXmlDateTimeBlock(end, timeZone) : void 0;
86709
- const target = byName.get(fileName) ?? { fileName };
86710
- if (alarmId && !target.id) target.id = alarmId;
86711
- if (alarmType) target.recordType = alarmType;
86712
- if (startDt) target.startTime = startDt;
86713
- if (endDt) target.endTime = endDt;
86714
- if (!target.parsedFileName) {
86715
- const parsed = parseRecordingFileName(target.fileName, fileNameOptions);
86716
- if (parsed) {
86717
- target.parsedFileName = parsed;
86718
- if (!target.startTime) target.startTime = parsed.start;
86719
- if (!target.endTime) target.endTime = parsed.end;
86720
- }
86721
- }
86722
- if (!byName.has(fileName)) {
86723
- out.push(target);
86724
- byName.set(fileName, target);
86725
- }
86726
- }
86727
- }
86728
- const seen = /* @__PURE__ */ new Set();
86729
- return out.filter((f) => {
86730
- const key = f.fileName.trim();
86731
- if (seen.has(key)) return false;
86732
- seen.add(key);
86733
- return true;
86734
- });
86735
- };
86736
- function parseTalkAudioConfig(block) {
86737
- const audioType = getXmlText(block, "audioType");
86738
- const sampleRate = Number.parseInt(getXmlText(block, "sampleRate") ?? "", 10);
86739
- const samplePrecision = Number.parseInt(getXmlText(block, "samplePrecision") ?? "", 10);
86740
- const lengthPerEncoder = Number.parseInt(getXmlText(block, "lengthPerEncoder") ?? "", 10);
86741
- const soundTrack = getXmlText(block, "soundTrack");
86742
- const priorityText = getXmlText(block, "priority");
86743
- if (!audioType || !Number.isFinite(sampleRate) || !Number.isFinite(samplePrecision) || !Number.isFinite(lengthPerEncoder) || !soundTrack) return null;
86744
- const config = {
86745
- audioType,
86746
- sampleRate,
86747
- samplePrecision,
86748
- lengthPerEncoder,
86749
- soundTrack
86750
- };
86751
- if (priorityText !== void 0) {
86752
- const pr = Number.parseInt(priorityText, 10);
86753
- if (Number.isFinite(pr)) config.priority = pr;
86754
- }
86755
- return config;
86756
- }
86757
- var parseTalkAbilityXml = (xml) => {
86758
- const talkAbilityBlock = getXmlBlocks(xml, "TalkAbility")[0];
86759
- if (!talkAbilityBlock) throw new Error("TalkAbility XML not found in response");
86760
- return {
86761
- duplexList: getXmlBlocks(talkAbilityBlock, "duplexList").map((b) => getXmlText(b, "duplex")).filter((v) => Boolean(v)),
86762
- audioStreamModeList: getXmlBlocks(talkAbilityBlock, "audioStreamModeList").map((b) => getXmlText(b, "audioStreamMode")).filter((v) => Boolean(v)),
86763
- audioConfigList: getXmlBlocks(talkAbilityBlock, "audioConfig").map(parseTalkAudioConfig).filter((v) => Boolean(v))
86764
- };
86765
- };
86766
87835
  function daysInMonth(year, month) {
86767
87836
  return new Date(Date.UTC(year, month, 0)).getUTCDate();
86768
87837
  }
@@ -87371,17 +88440,6 @@ var TtlCache = class {
87371
88440
  this.entries.clear();
87372
88441
  }
87373
88442
  };
87374
- function parseNumber(v) {
87375
- if (v == null) return void 0;
87376
- const n = Number(v);
87377
- return Number.isFinite(n) ? n : void 0;
87378
- }
87379
- function parseBoolean01(v) {
87380
- if (v == null) return void 0;
87381
- const s = v.trim();
87382
- if (s === "1" || s.toLowerCase() === "true") return true;
87383
- if (s === "0" || s.toLowerCase() === "false") return false;
87384
- }
87385
88443
  function parseOsdDatetimeXml(rawXml) {
87386
88444
  const osdBlock = getXmlBlocks(rawXml, "OsdDatetime")[0];
87387
88445
  const nameBlock = getXmlBlocks(rawXml, "OsdChannelName")[0];
@@ -88724,12 +89782,12 @@ var parseRecStartParamIfPresent = (fileName) => {
88724
89782
  if (!m) return void 0;
88725
89783
  return `${m[2]}${m[3]}`;
88726
89784
  };
88727
- var buildReplayStopNameFromFileName = (fileName) => {
89785
+ var buildReplayStopNameFromFileName = (fileName, channel = 0) => {
88728
89786
  const trimmed = (fileName ?? "").trim();
88729
- if (/^01\d{14}$/.test(trimmed)) return trimmed;
89787
+ if (/^\d{2}\d{14}$/.test(trimmed)) return trimmed;
88730
89788
  const start = parseRecStartParamIfPresent(fileName);
88731
89789
  if (!start) return void 0;
88732
- return `01${start}`;
89790
+ return `${String(channel + 1).padStart(2, "0")}${start}`;
88733
89791
  };
88734
89792
  var buildFileInfoListReplayByIdXml = (params) => {
88735
89793
  const st = params.streamType ?? "mainStream";
@@ -89024,7 +90082,7 @@ var sendTalkConfigWithReset = async (params) => {
89024
90082
  channel: params.channel,
89025
90083
  ...params.channelIdOverride != null ? { channelIdOverride: params.channelIdOverride } : {},
89026
90084
  payloadXml: params.payloadXml,
89027
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
90085
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
89028
90086
  })).header.responseCode;
89029
90087
  };
89030
90088
  const code = await trySend();
@@ -89034,7 +90092,7 @@ var sendTalkConfigWithReset = async (params) => {
89034
90092
  channel: params.channel,
89035
90093
  ...params.channelIdOverride != null ? { channelIdOverride: params.channelIdOverride } : {},
89036
90094
  payloadXml: "",
89037
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
90095
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
89038
90096
  });
89039
90097
  const retryCode = await trySend();
89040
90098
  if (retryCode !== 200) throw new Error(`TalkConfig rejected after reset (responseCode ${retryCode})`);
@@ -89101,7 +90159,7 @@ var createBufferedTalkSession = (params) => {
89101
90159
  ...channelIdOverride != null ? { channelIdOverride } : {},
89102
90160
  extensionXml: buildBinaryExtensionXml(channel),
89103
90161
  payload: Buffer.concat(parts),
89104
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
90162
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
89105
90163
  });
89106
90164
  const playLengthMs = samplesSent / info.audioConfig.sampleRate * 1e3;
89107
90165
  const now = Date.now();
@@ -89147,7 +90205,7 @@ var createBufferedTalkSession = (params) => {
89147
90205
  channel,
89148
90206
  ...channelIdOverride != null ? { channelIdOverride } : {},
89149
90207
  payloadXml: "",
89150
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
90208
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
89151
90209
  });
89152
90210
  if (frame.header.responseCode !== 200) throw new Error(`TalkReset rejected (responseCode ${frame.header.responseCode})`);
89153
90211
  if (params.closeSocketOnStop) try {
@@ -90830,7 +91888,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90830
91888
  if (debugCfg) {
90831
91889
  const sid = this.client.getSocketSessionId?.();
90832
91890
  const tag2 = sid ? `ReolinkSimpleEvent sid=${sid}` : "ReolinkSimpleEvent";
90833
- require_chunk_ZBACPERX.eventTraceLog(debugCfg, this.logger, tag2, `dispatch type=${evt.type} channel=${evt.channel} timestamp=${evt.timestamp}`);
91891
+ require_chunk_OSVYEFMM.eventTraceLog(debugCfg, this.logger, tag2, `dispatch type=${evt.type} channel=${evt.channel} timestamp=${evt.timestamp}`);
90834
91892
  }
90835
91893
  for (const cb of this.simpleEventListeners) try {
90836
91894
  Promise.resolve(cb(evt)).catch((e) => {
@@ -90841,7 +91899,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90841
91899
  }
90842
91900
  }
90843
91901
  constructor(opts) {
90844
- const dbg = require_chunk_ZBACPERX.normalizeDebugOptions(opts.debugOptions);
91902
+ const dbg = require_chunk_OSVYEFMM.normalizeDebugOptions(opts.debugOptions);
90845
91903
  this.logger = createDebugGateLogger(opts.logger, dbg.general || dbg.traceNativeStream || dbg.traceRecordings || dbg.traceTalk || dbg.traceEvents || dbg.debugRtsp);
90846
91904
  this.clientOptions = {
90847
91905
  host: opts.host,
@@ -91429,7 +92487,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
91429
92487
  return;
91430
92488
  }
91431
92489
  entry.startInFlight = (async () => {
91432
- const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => require("./BaichuanVideoStream-FOE2DBVZ-DJcEkVak.js"));
92490
+ const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => require("./BaichuanVideoStream-UG3DIMIE-DViuQDJ4.js"));
91433
92491
  const sessionKey = `live:object-detections:ch${entry.channel}:${entry.profile}`;
91434
92492
  const dedicated = await this.createDedicatedSession(sessionKey);
91435
92493
  const stream$12 = new BaichuanVideoStream2({
@@ -91910,7 +92968,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
91910
92968
  channel: ch,
91911
92969
  ...channelIdOverride != null ? { channelIdOverride } : {},
91912
92970
  payloadXml: "",
91913
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
92971
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
91914
92972
  });
91915
92973
  if (frame.header.responseCode !== 200) throw new Error(`TalkReset rejected (responseCode ${frame.header.responseCode})`);
91916
92974
  }
@@ -91929,7 +92987,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
91929
92987
  channel: ch,
91930
92988
  ...channelIdOverride != null ? { channelIdOverride } : {},
91931
92989
  payloadXml,
91932
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
92990
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
91933
92991
  })).header.responseCode;
91934
92992
  };
91935
92993
  const code = await trySend();
@@ -92081,7 +93139,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
92081
93139
  cmdId: 10,
92082
93140
  channel,
92083
93141
  payloadXml: "",
92084
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
93142
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
92085
93143
  });
92086
93144
  return parseTalkAbilityXml(frame.body.length === 0 ? "" : client.tryDecryptXml(frame.body, frame.header.channelId, client.enc));
92087
93145
  }
@@ -93154,8 +94212,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93154
94212
  const start = params.start;
93155
94213
  const endOfStartDay = endOfWallClockDay(start, timeZone);
93156
94214
  const end = params.end.getTime() > endOfStartDay.getTime() ? endOfStartDay : params.end;
93157
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclips", `Query: start=${start.toISOString()}, end=${end.toISOString()} (forced same day)`);
93158
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclips", `Using UID for channel ${channel}: ${uid}`);
94215
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "getVideoclips", `Query: start=${start.toISOString()}, end=${end.toISOString()} (forced same day)`);
94216
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "getVideoclips", `Using UID for channel ${channel}: ${uid}`);
93159
94217
  const streamType = params.streamType ?? "subStream";
93160
94218
  const recordType = params.recordType ?? "manual,sched,io,md,people,face,vehicle,dog_cat,visitor,other,package,crossline,intrusion,loitering,legacy,loss";
93161
94219
  const fileRecordTypes = params.fileRecordTypes ?? (params.recordType === void 0 ? DEFAULT_RECORDING_SEARCH_FILE_RECORD_TYPES : void 0);
@@ -93174,7 +94232,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93174
94232
  ...timeZone !== void 0 ? { timeZone } : {}
93175
94233
  });
93176
94234
  const unique = dedupeRecordingFiles(files);
93177
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclips", `FileInfoList complete: ${unique.length} unique files (from ${files.length} total)`);
94235
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "getVideoclips", `FileInfoList complete: ${unique.length} unique files (from ${files.length} total)`);
93178
94236
  return unique;
93179
94237
  });
93180
94238
  }
@@ -93215,7 +94273,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93215
94273
  const start = params.start;
93216
94274
  const endOfStartDay = endOfWallClockDay(start, timeZone);
93217
94275
  const end = params.end.getTime() > endOfStartDay.getTime() ? endOfStartDay : params.end;
93218
- if (end !== params.end) require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "searchAlarmVideos", `Window clamped to one camera-local day: ${params.end.toISOString()} \u2192 ${end.toISOString()} (the firmware answers the START day only)`);
94276
+ 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)`);
93219
94277
  const windows = await searchAlarmVideosViaFindAlarmVideo({
93220
94278
  sendXml: (p) => this.sendXml(p),
93221
94279
  channel,
@@ -93231,7 +94289,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93231
94289
  maxPages: params.maxPages ?? 200,
93232
94290
  ...params.timeoutMs != null ? { timeoutMs: params.timeoutMs } : {}
93233
94291
  });
93234
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "searchAlarmVideos", `findAlarmVideo complete: ${windows.length} windows over ${new Set(windows.map((w) => w.fileName)).size} files (channel=${channel})`);
94292
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "searchAlarmVideos", `findAlarmVideo complete: ${windows.length} windows over ${new Set(windows.map((w) => w.fileName)).size} files (channel=${channel})`);
93235
94293
  return windows;
93236
94294
  });
93237
94295
  }
@@ -93296,7 +94354,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93296
94354
  seen.add(key);
93297
94355
  return true;
93298
94356
  });
93299
- require_chunk_ZBACPERX.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`);
94357
+ 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`);
93300
94358
  return unique;
93301
94359
  });
93302
94360
  }
@@ -93349,7 +94407,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93349
94407
  });
93350
94408
  const msgNum = 0;
93351
94409
  dedicatedClient.subscribeVideoStream(5, msgNum);
93352
- const stream$14 = new require_chunk_ZBACPERX.BaichuanVideoStream({
94410
+ const stream$14 = new require_chunk_OSVYEFMM.BaichuanVideoStream({
93353
94411
  client: dedicatedClient,
93354
94412
  channel,
93355
94413
  profile: streamType === "subStream" ? "sub" : "main",
@@ -93368,7 +94426,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93368
94426
  channel,
93369
94427
  channelIdOverride: sessionCounter,
93370
94428
  payloadXml,
93371
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
94429
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
93372
94430
  msgNumOverride: 0,
93373
94431
  timeoutMs: params.timeoutMs,
93374
94432
  internal: true
@@ -93389,7 +94447,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93389
94447
  const stop = async () => {
93390
94448
  if (tornDown) return;
93391
94449
  tornDown = true;
93392
- const stopName = buildReplayStopNameFromFileName(params.fileName);
94450
+ const stopName = buildReplayStopNameFromFileName(params.fileName, channel);
93393
94451
  if (started && stopName) try {
93394
94452
  const stopXml = buildFileInfoListStopXml({
93395
94453
  channel,
@@ -93400,7 +94458,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93400
94458
  cmdId: 7,
93401
94459
  channel,
93402
94460
  payloadXml: stopXml,
93403
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
94461
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
93404
94462
  timeoutMs: 2e3,
93405
94463
  internal: true
93406
94464
  });
@@ -93457,7 +94515,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93457
94515
  });
93458
94516
  const msgNum = 0;
93459
94517
  dedicatedClient.subscribeVideoStream(5, msgNum);
93460
- const stream$15 = new require_chunk_ZBACPERX.BaichuanVideoStream({
94518
+ const stream$15 = new require_chunk_OSVYEFMM.BaichuanVideoStream({
93461
94519
  client: dedicatedClient,
93462
94520
  channel,
93463
94521
  profile: streamType === "subStream" ? "sub" : "main",
@@ -93477,7 +94535,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93477
94535
  channelIdOverride,
93478
94536
  payloadXml,
93479
94537
  extensionXml: "",
93480
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
94538
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
93481
94539
  msgNumOverride: msgNum,
93482
94540
  timeoutMs: params.timeoutMs,
93483
94541
  internal: true
@@ -93498,7 +94556,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93498
94556
  const stop = async () => {
93499
94557
  if (tornDown) return;
93500
94558
  tornDown = true;
93501
- const stopName = buildReplayStopNameFromFileName(params.fileName);
94559
+ const stopName = buildReplayStopNameFromFileName(params.fileName, channel);
93502
94560
  if (started && stopName) try {
93503
94561
  const stopXml = buildFileInfoListStopXml({
93504
94562
  channel,
@@ -93509,7 +94567,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93509
94567
  cmdId: 7,
93510
94568
  channel,
93511
94569
  payloadXml: stopXml,
93512
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
94570
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
93513
94571
  timeoutMs: 2e3,
93514
94572
  internal: true
93515
94573
  });
@@ -93596,35 +94654,35 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93596
94654
  async ensureUidForRecordings(channel, explicitUid) {
93597
94655
  const dbg = this.client.getDebugConfig?.();
93598
94656
  const logger = this.logger;
93599
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Checking UID: explicitUid=${explicitUid}, this.uid=${this.uid}`);
94657
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Checking UID: explicitUid=${explicitUid}, this.uid=${this.uid}`);
93600
94658
  const trimmedExplicit = explicitUid?.trim();
93601
94659
  if (trimmedExplicit) {
93602
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using explicit UID: ${trimmedExplicit}`);
94660
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using explicit UID: ${trimmedExplicit}`);
93603
94661
  return trimmedExplicit;
93604
94662
  }
93605
94663
  const perChannel = await this.discoverUidForRecordingsForChannel(channel);
93606
94664
  if (perChannel) {
93607
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID: ${perChannel}`);
94665
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID: ${perChannel}`);
93608
94666
  return perChannel;
93609
94667
  }
93610
94668
  const configured = this.uid?.trim();
93611
94669
  if (configured) {
93612
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using configured UID: ${configured}`);
94670
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using configured UID: ${configured}`);
93613
94671
  return configured;
93614
94672
  }
93615
94673
  if (this.nativeOnly) {
93616
94674
  const viaGetUid = await this.discoverUidViaGetUidNative(channel);
93617
94675
  if (viaGetUid) {
93618
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using device UID from cmd 114 GetUid: ${viaGetUid}`);
94676
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using device UID from cmd 114 GetUid: ${viaGetUid}`);
93619
94677
  return viaGetUid;
93620
94678
  }
93621
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: no UID available (channel=${channel})`);
94679
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: no UID available (channel=${channel})`);
93622
94680
  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.");
93623
94681
  }
93624
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `No UID available, attempting device auto-discovery (ch=${channel})`);
94682
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `No UID available, attempting device auto-discovery (ch=${channel})`);
93625
94683
  const discovered = await this.discoverDeviceUidForRecordings(channel);
93626
94684
  if (discovered) return discovered;
93627
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Auto-discovery failed - no UID found`);
94685
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Auto-discovery failed - no UID found`);
93628
94686
  throw new Error("UID is required to access recordings. Provide a UID explicitly or configure the client with a UID.");
93629
94687
  }
93630
94688
  cacheChannelUid(channel, uid) {
@@ -93647,28 +94705,28 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93647
94705
  const logger = this.logger;
93648
94706
  const fromPush = this.getUidFromPushCacheForChannel(channel);
93649
94707
  if (fromPush) {
93650
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from push cache: ${fromPush}`);
94708
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from push cache: ${fromPush}`);
93651
94709
  return fromPush;
93652
94710
  }
93653
94711
  if (this.nativeOnly) {
93654
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: skipping per-channel UID discovery via HTTP/CGI (channel=${channel})`);
94712
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: skipping per-channel UID discovery via HTTP/CGI (channel=${channel})`);
93655
94713
  return;
93656
94714
  }
93657
94715
  try {
93658
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Attempting per-channel UID discovery via HTTP CGI GetChannelstatus (channel=${channel})`);
94716
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Attempting per-channel UID discovery via HTTP CGI GetChannelstatus (channel=${channel})`);
93659
94717
  const uidCandidate = await discoverPerChannelUidViaCgiChannelstatus({
93660
94718
  channel,
93661
94719
  login: () => this.cgiApi.login(),
93662
94720
  getChannelstatus: () => this.cgiApi.GetChannelstatus()
93663
94721
  });
93664
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus channel=${channel} uid=${uidCandidate || "(missing)"}`);
94722
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus channel=${channel} uid=${uidCandidate || "(missing)"}`);
93665
94723
  if (uidCandidate) {
93666
94724
  this.cacheChannelUid(channel, uidCandidate);
93667
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from GetChannelstatus: ${uidCandidate}`);
94725
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from GetChannelstatus: ${uidCandidate}`);
93668
94726
  return uidCandidate;
93669
94727
  }
93670
94728
  } catch (e) {
93671
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus failed: ${formatErrorForLog(e)}`);
94729
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus failed: ${formatErrorForLog(e)}`);
93672
94730
  }
93673
94731
  }
93674
94732
  /**
@@ -93680,7 +94738,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93680
94738
  */
93681
94739
  async discoverUidViaGetUidNative(channel) {
93682
94740
  const dbg = this.client.getDebugConfig?.();
93683
- const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, this.logger, "ensureUidForRecordings", message);
94741
+ const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, this.logger, "ensureUidForRecordings", message);
93684
94742
  let isNvr = this._isNvr;
93685
94743
  if (isNvr === void 0) try {
93686
94744
  isNvr = await this.isNvrDevice();
@@ -93706,7 +94764,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93706
94764
  if (this.nativeOnly) return void 0;
93707
94765
  const dbg = this.client.getDebugConfig?.();
93708
94766
  const logger = this.logger;
93709
- const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", message);
94767
+ const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", message);
93710
94768
  const discoveredUid = await discoverDeviceUidForRecordings({
93711
94769
  channel,
93712
94770
  getInfo: () => this.getInfo(),
@@ -93781,7 +94839,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93781
94839
  await this.client.login();
93782
94840
  const dbg = this.client.getDebugConfig?.();
93783
94841
  const logger = this.logger;
93784
- const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclipThumbnail", message);
94842
+ const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "getVideoclipThumbnail", message);
93785
94843
  const channel = this.normalizeChannel(params.channel);
93786
94844
  const snapStreamType = (params.snapType ?? "sub") === "main" ? "mainStream" : "subStream";
93787
94845
  const timeoutMs = params.timeoutMs ?? 3e4;
@@ -93836,7 +94894,7 @@ ${xml}`);
93836
94894
  cmdId: 298,
93837
94895
  ...isNvr && headerChannelIdOverride != null ? { channelIdOverride: headerChannelIdOverride } : {},
93838
94896
  msgNumOverride: 0,
93839
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
94897
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
93840
94898
  streamType: 0,
93841
94899
  payloadXml: xml,
93842
94900
  timeoutMs
@@ -94315,7 +95373,7 @@ ${stderr}`));
94315
95373
  await this.client.login();
94316
95374
  const dbg = this.client.getDebugConfig?.();
94317
95375
  const logger = this.logger;
94318
- const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListDownload", message);
95376
+ const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "fileInfoListDownload", message);
94319
95377
  const channel = this.normalizeChannel(params.channel);
94320
95378
  const uid = await this.ensureUidForRecordings(channel, params.uid);
94321
95379
  const headerChannelIdOverride = this.resolveHeaderChannelIdForLogicalChannel(channel);
@@ -94361,7 +95419,7 @@ ${stderr}`));
94361
95419
  cmdId: 13,
94362
95420
  channel,
94363
95421
  ...typeof chId === "number" ? { channelIdOverride: chId } : {},
94364
- messageClass: require_chunk_ZBACPERX.BC_CLASS_FILE_DOWNLOAD,
95422
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_FILE_DOWNLOAD,
94365
95423
  extensionXml: buildBinaryExtensionXml(channel),
94366
95424
  payloadXml,
94367
95425
  streamType: st,
@@ -94390,7 +95448,7 @@ ${stderr}`));
94390
95448
  await this.client.login();
94391
95449
  const dbg = this.client.getDebugConfig?.();
94392
95450
  const logger = this.logger;
94393
- const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListPagedDownload", message);
95451
+ const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "fileInfoListPagedDownload", message);
94394
95452
  const channel = this.normalizeChannel(params.channel);
94395
95453
  const uid = await this.ensureUidForRecordings(channel, params.uid);
94396
95454
  trace(`Starting paged download: channel=${channel}, uid=${uid}, fileName=${params.fileName}`);
@@ -94427,7 +95485,7 @@ ${stderr}`));
94427
95485
  await this.client.login();
94428
95486
  const dbg = this.client.getDebugConfig?.();
94429
95487
  const logger = this.logger;
94430
- const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListReplayBinaryDownload", message);
95488
+ const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "fileInfoListReplayBinaryDownload", message);
94431
95489
  const channel = this.normalizeChannel(params.channel);
94432
95490
  const headerChannelIdOverride = this.resolveHeaderChannelIdForLogicalChannel(channel);
94433
95491
  const ident = params.fileName;
@@ -94450,13 +95508,15 @@ ${stderr}`));
94450
95508
  });
94451
95509
  const timeoutMs = params.timeoutMs ?? 12e4;
94452
95510
  trace(`download: channel=${channel} uid=${uid || "(missing)"} ident=${ident} streamType=${streamType} isNvr=${isNvr} timeoutMs=${timeoutMs}`);
95511
+ const pinnedHeaderChannelId = params.headerChannelIdOverride;
95512
+ let abandoned = false;
94453
95513
  try {
94454
95514
  return await this.client.sendBinary({
94455
95515
  cmdId: 5,
94456
95516
  channel,
94457
- ...isNvr ? { channelIdOverride: headerChannelIdOverride ?? 82 } : {},
95517
+ ...pinnedHeaderChannelId != null ? { channelIdOverride: pinnedHeaderChannelId } : {},
94458
95518
  msgNumOverride: 0,
94459
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
95519
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
94460
95520
  payloadXml,
94461
95521
  streamType: 0,
94462
95522
  timeoutMs,
@@ -94465,9 +95525,54 @@ ${stderr}`));
94465
95525
  });
94466
95526
  } catch (e) {
94467
95527
  trace(`download failed: ${e instanceof Error ? e.message : String(e)}`);
95528
+ abandoned = true;
94468
95529
  throw e;
95530
+ } finally {
95531
+ await this.stopFileInfoListReplay({
95532
+ channel,
95533
+ fileName: ident,
95534
+ streamType,
95535
+ drain: abandoned
95536
+ });
94469
95537
  }
94470
95538
  }
95539
+ /**
95540
+ * Close a cmd 5 replay session at the camera (cmd 7) and wait for the wire to
95541
+ * go quiet.
95542
+ *
95543
+ * Best-effort by design: a session that has already ended answers with an
95544
+ * error on some firmwares and there is nothing to do about it. The DRAIN is
95545
+ * not best-effort — the ack comes back before the camera has finished
95546
+ * sending (the Reolink app's own capture shows 26–91 further frames after
95547
+ * four of eleven stops), and the next replay must not be opened into that
95548
+ * tail.
95549
+ */
95550
+ async stopFileInfoListReplay(params) {
95551
+ const stopName = buildReplayStopNameFromFileName(params.fileName, params.channel);
95552
+ if (!stopName) {
95553
+ this.logger?.debug?.(`[stopFileInfoListReplay] no stop name derivable from ${params.fileName}; the session is left to time out at the camera`);
95554
+ return;
95555
+ }
95556
+ try {
95557
+ await this.client.sendXml({
95558
+ cmdId: 7,
95559
+ channel: params.channel,
95560
+ payloadXml: buildFileInfoListStopXml({
95561
+ channel: params.channel,
95562
+ name: stopName,
95563
+ streamType: params.streamType
95564
+ }),
95565
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95566
+ timeoutMs: 4e3,
95567
+ internal: true
95568
+ });
95569
+ } catch (e) {
95570
+ this.logger?.debug?.(`[stopFileInfoListReplay] stop <name>${stopName}</name> failed: ${e instanceof Error ? e.message : String(e)}`);
95571
+ }
95572
+ if (!params.drain) return;
95573
+ const drained = await this.client.drainReplayFrames();
95574
+ if (drained > 0) this.logger?.debug?.(`[stopFileInfoListReplay] ${drained} cmd 5 frame(s) still arrived after the stop (<name>${stopName}</name>); drained before releasing the socket`);
95575
+ }
94471
95576
  async downloadRecording(params) {
94472
95577
  this.logger?.debug?.(`[downloadRecording] Queuing download for: ${params.fileName}, channel=${params.channel}`);
94473
95578
  return this.enqueueReplayOperation(async () => {
@@ -94545,7 +95650,7 @@ ${stderr}`));
94545
95650
  const frames = [];
94546
95651
  const timestampsUs = [];
94547
95652
  const audioFrames = [];
94548
- const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
95653
+ const decoder = new require_chunk_OSVYEFMM.BcMediaAnnexBDecoder({
94549
95654
  strict: false,
94550
95655
  logger: this.logger,
94551
95656
  onVideoAccessUnit: ({ annexB, microseconds }) => {
@@ -94611,7 +95716,7 @@ ${stderr}`));
94611
95716
  const videoFrames = [];
94612
95717
  const audioFrames = [];
94613
95718
  let audioCodec = null;
94614
- const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
95719
+ const decoder = new require_chunk_OSVYEFMM.BcMediaAnnexBDecoder({
94615
95720
  strict: false,
94616
95721
  logger: this.logger,
94617
95722
  onVideoAccessUnit: ({ annexB, microseconds }) => {
@@ -95009,7 +96114,7 @@ ${stderr}`));
95009
96114
  params: {
95010
96115
  cmdId: 31,
95011
96116
  channelIdOverride: channel,
95012
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
96117
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
95013
96118
  }
95014
96119
  },
95015
96120
  {
@@ -95017,7 +96122,7 @@ ${stderr}`));
95017
96122
  params: {
95018
96123
  cmdId: 31,
95019
96124
  channelIdOverride: 251,
95020
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
96125
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
95021
96126
  }
95022
96127
  },
95023
96128
  {
@@ -95025,7 +96130,7 @@ ${stderr}`));
95025
96130
  params: {
95026
96131
  cmdId: 31,
95027
96132
  channelIdOverride: 250,
95028
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
96133
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
95029
96134
  }
95030
96135
  },
95031
96136
  {
@@ -95033,7 +96138,7 @@ ${stderr}`));
95033
96138
  params: {
95034
96139
  cmdId: 31,
95035
96140
  channelIdOverride: 250,
95036
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96141
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95037
96142
  extensionXml: `<?xml version="1.0" encoding="UTF-8" ?><Extension version="1.1"><channelId>251</channelId></Extension>`
95038
96143
  }
95039
96144
  }
@@ -95076,7 +96181,7 @@ ${stderr}`));
95076
96181
  const frame = await this.client.sendFrame({
95077
96182
  cmdId: 31,
95078
96183
  channelIdOverride: Number(channel),
95079
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96184
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95080
96185
  timeoutMs
95081
96186
  });
95082
96187
  lastCode = frame.header.responseCode;
@@ -95357,7 +96462,7 @@ ${stderr}`));
95357
96462
  msgNumOverride: msgNum,
95358
96463
  extensionXml: buildChannelExtensionXml(channelId),
95359
96464
  payloadXml,
95360
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96465
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95361
96466
  streamType: config.streamType,
95362
96467
  timeoutMs: 2e4
95363
96468
  };
@@ -95489,7 +96594,7 @@ ${stderr}`));
95489
96594
  channelIdOverride: channelId,
95490
96595
  extensionXml: a.extensionXml,
95491
96596
  payloadXml: a.payloadXml,
95492
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96597
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95493
96598
  streamType: a.streamType,
95494
96599
  ...msgNum !== void 0 ? { msgNumOverride: msgNum } : {},
95495
96600
  timeoutMs: 2e3
@@ -95519,7 +96624,7 @@ ${stderr}`));
95519
96624
  channel: ch,
95520
96625
  channelIdOverride: channelId,
95521
96626
  extensionXml: buildChannelExtensionXml(channelId),
95522
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96627
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95523
96628
  streamType: 0
95524
96629
  });
95525
96630
  } catch (e) {
@@ -95561,7 +96666,7 @@ ${stderr}`));
95561
96666
  channel: ch,
95562
96667
  channelIdOverride: channelId,
95563
96668
  extensionXml: buildChannelExtensionXml(channelId),
95564
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96669
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95565
96670
  streamType: 0
95566
96671
  });
95567
96672
  } catch (e) {
@@ -95583,7 +96688,7 @@ ${stderr}`));
95583
96688
  channelIdOverride: channelId,
95584
96689
  extensionXml,
95585
96690
  payloadXml,
95586
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96691
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95587
96692
  streamType: 0
95588
96693
  });
95589
96694
  if (frame.header.responseCode !== 200) {
@@ -95615,7 +96720,7 @@ ${stderr}`));
95615
96720
  channelIdOverride: channelId,
95616
96721
  extensionXml,
95617
96722
  payloadXml,
95618
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96723
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95619
96724
  streamType: 0
95620
96725
  });
95621
96726
  if (frame.header.responseCode !== 200) throw new Error(`PTZ preset move rejected (response_code ${frame.header.responseCode})`);
@@ -95636,7 +96741,7 @@ ${stderr}`));
95636
96741
  channelIdOverride: channelId,
95637
96742
  extensionXml,
95638
96743
  payloadXml,
95639
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96744
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95640
96745
  streamType: 0
95641
96746
  });
95642
96747
  if (frame.header.responseCode !== 200) throw new Error(`PTZ preset save rejected (response_code ${frame.header.responseCode})`);
@@ -95653,7 +96758,7 @@ ${stderr}`));
95653
96758
  channelIdOverride: channelId,
95654
96759
  extensionXml,
95655
96760
  payloadXml,
95656
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96761
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95657
96762
  streamType: 0
95658
96763
  });
95659
96764
  if (frame.header.responseCode !== 200) throw new Error(`PTZ goto preset rejected (response_code ${frame.header.responseCode})`);
@@ -95680,7 +96785,7 @@ ${stderr}`));
95680
96785
  channelIdOverride: channelId,
95681
96786
  extensionXml,
95682
96787
  payloadXml,
95683
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96788
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95684
96789
  streamType: 0
95685
96790
  })).header.responseCode };
95686
96791
  },
@@ -95734,7 +96839,7 @@ ${stderr}`));
95734
96839
  channel: ch,
95735
96840
  channelIdOverride: ch,
95736
96841
  extensionXml: buildPtzGuardExtensionXml(ch, "read"),
95737
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96842
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95738
96843
  streamType: 0
95739
96844
  }));
95740
96845
  }
@@ -95760,7 +96865,7 @@ ${stderr}`));
95760
96865
  timeoutSeconds: options.timeoutSeconds,
95761
96866
  setPosition: options.setPosition === true
95762
96867
  }),
95763
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96868
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95764
96869
  streamType: 0
95765
96870
  });
95766
96871
  }
@@ -95781,7 +96886,7 @@ ${stderr}`));
95781
96886
  channelIdOverride: ch,
95782
96887
  extensionXml: buildPtzGuardExtensionXml(ch, "write"),
95783
96888
  payloadXml: buildPtzGuardGoXml(ch),
95784
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96889
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95785
96890
  streamType: 0
95786
96891
  });
95787
96892
  }
@@ -95797,7 +96902,7 @@ ${stderr}`));
95797
96902
  channel: ch,
95798
96903
  channelIdOverride: channelId,
95799
96904
  extensionXml: buildChannelExtensionXml(channelId),
95800
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96905
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95801
96906
  streamType: 0
95802
96907
  });
95803
96908
  const parseTriplet = (sectionTag) => {
@@ -95835,7 +96940,7 @@ ${stderr}`));
95835
96940
  channelIdOverride: channelId,
95836
96941
  extensionXml,
95837
96942
  payloadXml,
95838
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96943
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95839
96944
  streamType: 0
95840
96945
  });
95841
96946
  if (frame.header.responseCode !== 200) throw new Error(`Zoom rejected (response_code ${frame.header.responseCode})`);
@@ -97586,7 +98691,7 @@ ${xml}`);
97586
98691
  * @returns Test results for all stream types and profiles
97587
98692
  */
97588
98693
  async testChannelStreams(channel, logger) {
97589
- const { testChannelStreams } = await Promise.resolve().then(() => require("./DiagnosticsTools-ZC7I7QM6-CD4rRF8a.js"));
98694
+ const { testChannelStreams } = await Promise.resolve().then(() => require("./DiagnosticsTools-YIWXO7IJ-CD4rRF8a.js"));
97590
98695
  return await testChannelStreams({
97591
98696
  api: this,
97592
98697
  channel: this.normalizeChannel(channel),
@@ -97602,7 +98707,7 @@ ${xml}`);
97602
98707
  * @returns Complete diagnostics for all channels and streams
97603
98708
  */
97604
98709
  async collectMultifocalDiagnostics(logger) {
97605
- const { collectMultifocalDiagnostics } = await Promise.resolve().then(() => require("./DiagnosticsTools-ZC7I7QM6-CD4rRF8a.js"));
98710
+ const { collectMultifocalDiagnostics } = await Promise.resolve().then(() => require("./DiagnosticsTools-YIWXO7IJ-CD4rRF8a.js"));
97606
98711
  return await collectMultifocalDiagnostics({
97607
98712
  api: this,
97608
98713
  logger
@@ -98424,6 +99529,115 @@ ${xml}`);
98424
99529
  };
98425
99530
  })() : {};
98426
99531
  }
99532
+ /**
99533
+ * The limits THIS camera states for `<RecordCfg>` (cmd 54), read from its
99534
+ * own reply — `<cyclelist>`, `<timeList>`, whether pre-record exists at all.
99535
+ *
99536
+ * Read this before offering a control: a firmware that stops accepting a
99537
+ * value stops listing it, and {@link setRecordCfg} refuses against the same
99538
+ * facts.
99539
+ */
99540
+ async getRecordCfgLimits(channel, options) {
99541
+ return parseRecordCfgLimits(await this.sendPcapDerivedSettingsGetXml({
99542
+ cmdId: 54,
99543
+ channel,
99544
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
99545
+ }));
99546
+ }
99547
+ /**
99548
+ * SetRecCfg (cmd 55) — write the onboard recording configuration.
99549
+ *
99550
+ * Read-modify-write over the camera's own cmd 54 document, then a re-read to
99551
+ * find out what it actually kept. Both halves are load-bearing, because this
99552
+ * command **never reports a bad value**: measured on 2026-09-22, a
99553
+ * `preRecordTime` of 9999 and a `cycle` outside the camera's own
99554
+ * `<cyclelist>` were each answered `200` and discarded. The returned
99555
+ * {@link BaichuanRecordWriteResult} is the only honest answer to "did that
99556
+ * work", so callers should look at `allApplied` rather than at the absence
99557
+ * of a thrown error.
99558
+ *
99559
+ * Values the camera would certainly drop are refused BEFORE the wire with a
99560
+ * {@link BaichuanRecordConfigError}.
99561
+ *
99562
+ * `preRecordEnabled` is a boolean on purpose — see `utils/recordConfig.ts`.
99563
+ *
99564
+ * Verified live: `packageTime` 5 → 10 and `recordDelayTime` 15 → 30 on E1
99565
+ * Outdoor PoE v3.1.0.5223, `recordDelayTime` 15 → 30 → 20 on a Home Hub
99566
+ * v3.3.0.456 channel 0, each read back and restored.
99567
+ */
99568
+ async setRecordCfg(channel, patch, options) {
99569
+ const ch = this.normalizeChannel(channel);
99570
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
99571
+ const currentXml = await this.sendXml({
99572
+ cmdId: 54,
99573
+ channel: ch,
99574
+ ...timeoutOpts
99575
+ });
99576
+ const limits = parseRecordCfgLimits(currentXml);
99577
+ validateRecordCfgPatch(patch, limits);
99578
+ await this.sendXml({
99579
+ cmdId: 55,
99580
+ channel: ch,
99581
+ payloadXml: ensureXmlHeader(buildRecordCfgSetXml(currentXml, patch, limits)),
99582
+ ...timeoutOpts
99583
+ });
99584
+ return diffRecordCfg(patch, await this.sendXml({
99585
+ cmdId: 54,
99586
+ channel: ch,
99587
+ ...timeoutOpts
99588
+ }));
99589
+ }
99590
+ /**
99591
+ * The schedule rows THIS camera lists (cmd 81), `(type, index)` and the
99592
+ * current mask for each.
99593
+ *
99594
+ * `<index>` matters: an E1 Outdoor Pro lists `crossline`, `intrude` and
99595
+ * `loitering` three times each, one row per configured rule, and a write
99596
+ * addressed by `type` alone would overwrite all three.
99597
+ */
99598
+ async getRecordScheduleRows(channel, options) {
99599
+ return parseRecordScheduleRows(await this.sendPcapDerivedSettingsGetXml({
99600
+ cmdId: 81,
99601
+ channel,
99602
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
99603
+ }));
99604
+ }
99605
+ /**
99606
+ * SetRec (cmd 82) — write the weekly recording schedule.
99607
+ *
99608
+ * Same contract as {@link setRecordCfg}: refuse what is certainly bad before
99609
+ * the wire, then verify by re-reading. A 167-character mask and a `<type>`
99610
+ * the camera does not have were each answered `200` and dropped, and the
99611
+ * drop is **per item** — one document carrying a bad `Normal` mask and a
99612
+ * good `MD` mask applied the `MD` one and discarded the other. So a caller
99613
+ * cannot treat this write as atomic, and the per-field
99614
+ * {@link BaichuanRecordWriteResult} says which rows landed.
99615
+ *
99616
+ * Verified live on E1 Outdoor PoE v3.1.0.5223: `Normal` hour 0 flipped on
99617
+ * and restored, via both a full document and a one-item document (the camera
99618
+ * merges per row and leaves the other rows alone).
99619
+ */
99620
+ async setRecordSchedule(channel, patch, options) {
99621
+ const ch = this.normalizeChannel(channel);
99622
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
99623
+ const currentXml = await this.sendXml({
99624
+ cmdId: 81,
99625
+ channel: ch,
99626
+ ...timeoutOpts
99627
+ });
99628
+ validateRecordSchedulePatch(patch, parseRecordScheduleRows(currentXml));
99629
+ await this.sendXml({
99630
+ cmdId: 82,
99631
+ channel: ch,
99632
+ payloadXml: ensureXmlHeader(buildRecordScheduleSetXml(currentXml, patch)),
99633
+ ...timeoutOpts
99634
+ });
99635
+ return diffRecordSchedule(patch, await this.sendXml({
99636
+ cmdId: 81,
99637
+ channel: ch,
99638
+ ...timeoutOpts
99639
+ }));
99640
+ }
98427
99641
  async getWifiSignal(channel, options) {
98428
99642
  const signal = parseNumber(getXmlText(await this.sendPcapDerivedSettingsGetXml({
98429
99643
  cmdId: 115,
@@ -98622,6 +99836,22 @@ ${xml}`);
98622
99836
  }));
98623
99837
  }
98624
99838
  /**
99839
+ * cmd 102 `<HddInfoList>`, typed — one record per volume with the size
99840
+ * arithmetic already done.
99841
+ *
99842
+ * Prefer this over {@link getHddInfoList}. The raw reply splits size across
99843
+ * `capacity` (whole GB) and `capacityM` (MB remainder), and newer firmwares
99844
+ * add exact byte counts in `capacityV2` / `remainSizeV2`; `capacityMb` and
99845
+ * `remainMb` fold whichever pair the camera sent, and are **null when it
99846
+ * sent neither** rather than 0.
99847
+ */
99848
+ async getHddInfo(options) {
99849
+ return parseHddInfoListXml(await this.sendXml({
99850
+ cmdId: 102,
99851
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
99852
+ }));
99853
+ }
99854
+ /**
98625
99855
  * cmd 142 `<DayRecords>` — which days of a month have footage, for one
98626
99856
  * channel. The calendar the official app draws.
98627
99857
  *
@@ -99687,7 +100917,7 @@ ${scheduleItems}
99687
100917
  if (raw.length === 0) throw new Error("Downloaded recording is empty");
99688
100918
  const videoFrames = [];
99689
100919
  let videoType = null;
99690
- const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
100920
+ const decoder = new require_chunk_OSVYEFMM.BcMediaAnnexBDecoder({
99691
100921
  strict: false,
99692
100922
  logger: this.logger,
99693
100923
  onVideoAccessUnit: ({ annexB: annexB2, microseconds }) => {
@@ -100035,7 +101265,7 @@ ${scheduleItems}
100035
101265
  return {
100036
101266
  getPlaylist: () => {
100037
101267
  try {
100038
- const { readFileSync } = require_chunk_ZBACPERX.__require("fs");
101268
+ const { readFileSync } = require_chunk_OSVYEFMM.__require("fs");
100039
101269
  return readFileSync(playlistPath, "utf8");
100040
101270
  } catch {
100041
101271
  return "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:4\n";
@@ -100044,7 +101274,7 @@ ${scheduleItems}
100044
101274
  getSegment: (name) => {
100045
101275
  if (segments.has(name)) return segments.get(name);
100046
101276
  try {
100047
- const { readFileSync } = require_chunk_ZBACPERX.__require("fs");
101277
+ const { readFileSync } = require_chunk_OSVYEFMM.__require("fs");
100048
101278
  const data = readFileSync(path$3.join(tempDir, name));
100049
101279
  segments.set(name, data);
100050
101280
  return data;
@@ -100054,7 +101284,7 @@ ${scheduleItems}
100054
101284
  },
100055
101285
  listSegments: () => {
100056
101286
  try {
100057
- const { readdirSync } = require_chunk_ZBACPERX.__require("fs");
101287
+ const { readdirSync } = require_chunk_OSVYEFMM.__require("fs");
100058
101288
  return readdirSync(tempDir).filter((f) => f.endsWith(".ts"));
100059
101289
  } catch {
100060
101290
  return [];
@@ -101639,41 +102869,41 @@ async function autoDetectDeviceType(inputs) {
101639
102869
  variant: "cmd80 class=0x6414",
101640
102870
  op: () => api.getInfo(void 0, {
101641
102871
  timeoutMs: 2500,
101642
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
102872
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
101643
102873
  })
101644
102874
  },
101645
102875
  {
101646
102876
  variant: "cmd80 class=0x6614",
101647
102877
  op: () => api.getInfo(void 0, {
101648
102878
  timeoutMs: 3e3,
101649
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
102879
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_20
101650
102880
  })
101651
102881
  },
101652
102882
  {
101653
102883
  variant: "cmd318(ch0) class=0x6414",
101654
102884
  op: () => api.getInfo(0, {
101655
102885
  timeoutMs: 3e3,
101656
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
102886
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
101657
102887
  })
101658
102888
  },
101659
102889
  {
101660
102890
  variant: "cmd318(ch0) class=0x6614",
101661
102891
  op: () => api.getInfo(0, {
101662
102892
  timeoutMs: 3500,
101663
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
102893
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_20
101664
102894
  })
101665
102895
  }
101666
102896
  ]), runProbeVariants("getSupportInfo", [{
101667
102897
  variant: "cmd199 class=0x6414",
101668
102898
  op: () => api.getSupportInfo({
101669
102899
  timeoutMs: 2500,
101670
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
102900
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
101671
102901
  })
101672
102902
  }, {
101673
102903
  variant: "cmd199 class=0x6614",
101674
102904
  op: () => api.getSupportInfo({
101675
102905
  timeoutMs: 3500,
101676
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
102906
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_20
101677
102907
  })
101678
102908
  }])]);
101679
102909
  const deviceInfo = infoProbe?.value;
@@ -148202,7 +149432,7 @@ async function createRfc4571TcpServerInternal(options) {
148202
149432
  streamClient = dedicatedSession.client;
148203
149433
  logSessionsSummary(`dedicated session created: ${sessionKey}`);
148204
149434
  } else streamClient = baseApi.client;
148205
- const createLiveStream = async () => new require_chunk_ZBACPERX.BaichuanVideoStream({
149435
+ const createLiveStream = async () => new require_chunk_OSVYEFMM.BaichuanVideoStream({
148206
149436
  client: streamClient,
148207
149437
  api: baseApi,
148208
149438
  channel: ch,
@@ -150404,7 +151634,7 @@ function writeRefusal(res, refusal, deviceId, logger, clipKey) {
150404
151634
  else logger.debug(line, extras);
150405
151635
  const retryAfterMs = refusal.disposition === "deferred" ? refusal.retryAfterMs : void 0;
150406
151636
  res.writeHead(204, {
150407
- [CLIP_STILL_REASON_HEADER]: refusal.reason.slice(0, 500),
151637
+ [CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(refusal.reason),
150408
151638
  [CLIP_STILL_REASON_CODE_HEADER]: refusal.code,
150409
151639
  [CLIP_STILL_DISPOSITION_HEADER]: refusal.disposition,
150410
151640
  ...retryAfterMs === void 0 ? {} : { "retry-after": String(Math.max(1, Math.ceil(retryAfterMs / 1e3))) }
@@ -150601,7 +151831,7 @@ function createClipStreamHandler(deps) {
150601
151831
  status: resolved.status
150602
151832
  } });
150603
151833
  res.writeHead(resolved.status, {
150604
- [CLIP_STILL_REASON_HEADER]: resolved.detail.slice(0, 500),
151834
+ [CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(resolved.detail),
150605
151835
  [CLIP_STILL_REASON_CODE_HEADER]: resolved.code
150606
151836
  });
150607
151837
  res.end();
@@ -150674,7 +151904,7 @@ function createClipStreamHandler(deps) {
150674
151904
  }
150675
151905
  });
150676
151906
  res.writeHead(streamRefusalStatus(outcome.code), {
150677
- [CLIP_STILL_REASON_HEADER]: outcome.detail.slice(0, 500),
151907
+ [CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(outcome.detail),
150678
151908
  [CLIP_STILL_REASON_CODE_HEADER]: outcome.code
150679
151909
  });
150680
151910
  res.end();
@@ -150948,6 +152178,14 @@ function buildClipStreamMuxArgs(input) {
150948
152178
  * not push the deadline out. `drain` — the hop's own word that it took what
150949
152179
  * it held — disarms it. Stall fires at most once, and a disposed gate is
150950
152180
  * silent for ever.
152181
+ *
152182
+ * **A gate must be DISPOSED when its hop stops taking bytes for good** — when
152183
+ * the input is ended, and when the run is over. Node emits no `drain` after
152184
+ * `end()` (`afterWrite` guards it with `!state.ending`), so a gate armed at
152185
+ * EOF can never be disarmed: it fires `timeoutMs` later and severs a stream
152186
+ * that was already served. Measured on real bytes: a 42 s clip delivered at
152187
+ * 18× leaves the audio pipe armed at EOF every time, with zero `drain`
152188
+ * events after `end()`. (D604)
150951
152189
  */
150952
152190
  function createDrainGate(deps) {
150953
152191
  let handle = null;
@@ -151034,7 +152272,7 @@ function writableAt(stdio, index) {
151034
152272
  if (candidate === null || candidate === void 0) return null;
151035
152273
  return isWritable(candidate) ? candidate : null;
151036
152274
  }
151037
- var defaultDecoder = (handlers) => new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
152275
+ var defaultDecoder = (handlers) => new require_chunk_OSVYEFMM.BcMediaAnnexBDecoder({
151038
152276
  strict: false,
151039
152277
  onVideoAccessUnit: handlers.onVideoAccessUnit,
151040
152278
  onAudioFrame: (frame) => handlers.onAudioFrame({
@@ -151108,7 +152346,33 @@ function createClipStreamProducer(deps) {
151108
152346
  }
151109
152347
  });
151110
152348
  activeByHost.set(hostKey, ahead + 1);
151111
- const run = (lanes.get(hostKey) ?? Promise.resolve()).then(() => runAttempts(input)).finally(() => {
152349
+ let abandonedAt = null;
152350
+ input.sink.onceClose(() => {
152351
+ abandonedAt ??= Date.now();
152352
+ });
152353
+ const queuedAt = Date.now();
152354
+ const run = (lanes.get(hostKey) ?? Promise.resolve()).then(async () => {
152355
+ if (abandonedAt !== null) {
152356
+ const waitedMs = abandonedAt - queuedAt;
152357
+ deps.logger.info("videoclips: a queued clip stream was dropped, its peer had gone", {
152358
+ tags: { deviceId },
152359
+ meta: {
152360
+ clipKey: input.row.clipKey,
152361
+ twin: input.twin,
152362
+ branch: "queued-peer-gone",
152363
+ waitedMs,
152364
+ aheadWhenQueued: ahead
152365
+ }
152366
+ });
152367
+ return {
152368
+ kind: "refused",
152369
+ code: "fetch-failed",
152370
+ detail: `the peer left after ${String(waitedMs)} ms in the queue, so this clip was never fetched`,
152371
+ attempts: 0
152372
+ };
152373
+ }
152374
+ return await runAttempts(input);
152375
+ }).finally(() => {
151112
152376
  const left = (activeByHost.get(hostKey) ?? 1) - 1;
151113
152377
  if (left <= 0) activeByHost.delete(hostKey);
151114
152378
  else activeByHost.set(hostKey, left);
@@ -151152,8 +152416,18 @@ var ClipStreamRun = class {
151152
152416
  accessUnits: 0
151153
152417
  };
151154
152418
  videoType = null;
152419
+ /** The last access unit's timestamp, to catch a transfer that went BACK. */
152420
+ lastUs = null;
152421
+ /** Units of a PREVIOUS transfer, dropped before the mux existed (D604). */
152422
+ residueUnits = 0;
152423
+ residueAudioFrames = 0;
152424
+ /** A backwards timestamp that arrived too late to drop: the duration of
152425
+ * this pass cannot be measured, and is never guessed (D393). */
152426
+ timestampsBroken = false;
151155
152427
  begun = false;
151156
152428
  ended = false;
152429
+ /** The run is over. Every late callback is silent from here (D604). */
152430
+ done = false;
151157
152431
  bytesOut = 0;
151158
152432
  cut = null;
151159
152433
  stdinGate;
@@ -151210,17 +152484,28 @@ var ClipStreamRun = class {
151210
152484
  };
151211
152485
  }
151212
152486
  try {
151213
- await api.fileInfoListReplayBinaryDownload({
151214
- channel: device.channel,
151215
- ...device.uid !== void 0 ? { uid: device.uid } : {},
151216
- fileName,
151217
- idleTimeoutMs: 300,
151218
- onChunk: (chunk) => this.onChunk(decoder, chunk)
151219
- });
151220
- } catch (err) {
151221
- return this.onTransferFailed(err);
152487
+ try {
152488
+ await api.fileInfoListReplayBinaryDownload({
152489
+ channel: device.channel,
152490
+ ...device.uid !== void 0 ? { uid: device.uid } : {},
152491
+ fileName,
152492
+ idleTimeoutMs: 300,
152493
+ onChunk: (chunk) => this.onChunk(decoder, chunk)
152494
+ });
152495
+ } catch (err) {
152496
+ return this.onTransferFailed(err);
152497
+ }
152498
+ return await this.onTransferEnded();
152499
+ } finally {
152500
+ this.done = true;
152501
+ this.disposeGates();
151222
152502
  }
151223
- return this.onTransferEnded();
152503
+ }
152504
+ /** Every hop's drain gate, silenced. Idempotent. */
152505
+ disposeGates() {
152506
+ this.stdinGate.dispose();
152507
+ this.audioStdinGate.dispose();
152508
+ this.peerGate.dispose();
151224
152509
  }
151225
152510
  /** The camera's bytes, as they arrive. A cut run THROWS here: the only stop. */
151226
152511
  onChunk(decoder, chunk) {
@@ -151234,14 +152519,81 @@ var ClipStreamRun = class {
151234
152519
  }
151235
152520
  onAccessUnit(unit) {
151236
152521
  if (this.cut !== null) return;
152522
+ const previousUs = this.lastUs;
152523
+ this.lastUs = unit.microseconds;
152524
+ if (previousUs !== null && unit.microseconds < previousUs) this.onTimestampsWentBack(previousUs, unit.microseconds);
151237
152525
  this.accessUnits += 1;
151238
152526
  this.timestampsUs.push(unit.microseconds);
151239
- if (this.mux === null) {
151240
- this.probe = [...this.probe, unit];
151241
- if (this.probe.length >= 16) this.startMux();
152527
+ if (this.mux !== null) {
152528
+ this.feed(unit.annexB);
151242
152529
  return;
151243
152530
  }
151244
- this.feed(unit.annexB);
152531
+ this.probe = [...this.probe, unit];
152532
+ if (this.probe.length < 16) return;
152533
+ if (probeClipStreamRate(this.timestampsUs).fpsSource === "timestamps" || this.probe.length >= 128) this.startMux();
152534
+ }
152535
+ /**
152536
+ * A timestamp that went BACKWARD: this is a second transfer's first frame.
152537
+ *
152538
+ * The library's replay listener cannot tell one transfer from the next —
152539
+ * every cmd 5 replay carries `msgNum: 0` and, on a hub child, the same
152540
+ * header channel id — and a CUT transfer is never stopped at the camera
152541
+ * (nothing on the wire aborts one; the producer's only lever is to throw
152542
+ * from the next chunk). So the frames still in flight for the previous,
152543
+ * aborted stream are handed to THIS one, ahead of its own first frame.
152544
+ *
152545
+ * Measured on 3628, 2026-09-22 16:41: a stream cut at 55 access units was
152546
+ * followed 1 ms later by another, which decoded 640 units instead of 635
152547
+ * and 666 audio frames instead of 661 — the same five extra of each — and
152548
+ * spanned 38.52 s instead of 42.21 s, exactly the 3.69 s the cut stream had
152549
+ * already delivered. Its rate probe therefore measured a NEGATIVE span and
152550
+ * refused to name a rate, which is the only way `estimateVideoTiming` can
152551
+ * answer anything but `timestamps` here.
152552
+ *
152553
+ * Before the mux exists the answer is to DROP what was held and probe
152554
+ * again: nothing has reached the peer, so the residue can simply not be
152555
+ * part of this stream. After it exists there is nothing to drop — the bytes
152556
+ * are out — so it is named, counted, and this pass's duration becomes
152557
+ * unmeasurable rather than a number measured across two transfers.
152558
+ */
152559
+ onTimestampsWentBack(previousUs, currentUs) {
152560
+ const { row, twin } = this.input;
152561
+ const meta = {
152562
+ clipKey: row.clipKey,
152563
+ twin,
152564
+ previousUs,
152565
+ currentUs
152566
+ };
152567
+ if (this.mux !== null) {
152568
+ if (this.timestampsBroken) return;
152569
+ this.timestampsBroken = true;
152570
+ this.deps.logger.warn("videoclips: a clip stream’s timestamps went backwards mid-mux", {
152571
+ tags: this.tags,
152572
+ meta: {
152573
+ ...meta,
152574
+ branch: "stream-timestamp-jump",
152575
+ accessUnits: this.accessUnits
152576
+ }
152577
+ });
152578
+ return;
152579
+ }
152580
+ this.residueUnits += this.probe.length;
152581
+ this.residueAudioFrames += this.audioProbe.length;
152582
+ this.audioFramesDropped += this.audioProbe.length;
152583
+ this.probe = [];
152584
+ this.audioProbe = [];
152585
+ this.audioProbeBytes = 0;
152586
+ this.timestampsUs.length = 0;
152587
+ this.accessUnits = 0;
152588
+ this.deps.logger.warn("videoclips: a clip stream dropped the previous transfer’s frames", {
152589
+ tags: this.tags,
152590
+ meta: {
152591
+ ...meta,
152592
+ branch: "stream-transfer-residue",
152593
+ residueAccessUnits: this.residueUnits,
152594
+ residueAudioFrames: this.residueAudioFrames
152595
+ }
152596
+ });
151245
152597
  }
151246
152598
  /**
151247
152599
  * One audio frame from the decoder.
@@ -151306,15 +152658,20 @@ var ClipStreamRun = class {
151306
152658
  this.rate = probeClipStreamRate(this.timestampsUs);
151307
152659
  this.decideAudio();
151308
152660
  const { row, twin } = this.input;
151309
- if (this.rate.fpsSource !== "timestamps") this.deps.logger.warn("videoclips: no measured frame rate for this clip stream", {
151310
- tags: this.tags,
151311
- meta: {
151312
- clipKey: row.clipKey,
151313
- twin,
151314
- branch: "fps-unmeasured",
151315
- probeAccessUnits: this.rate.accessUnits
151316
- }
151317
- });
152661
+ if (this.rate.fps === null || this.rate.fpsSource !== "timestamps") {
152662
+ this.deps.logger.warn("videoclips: no measured frame rate for this clip stream", {
152663
+ tags: this.tags,
152664
+ meta: {
152665
+ clipKey: row.clipKey,
152666
+ twin,
152667
+ branch: "fps-unmeasured",
152668
+ probeAccessUnits: this.rate.accessUnits,
152669
+ residueAccessUnits: this.residueUnits
152670
+ }
152671
+ });
152672
+ this.sever("rate-unmeasured", `no frame rate could be measured over ${String(this.rate.accessUnits)} access units`);
152673
+ return;
152674
+ }
151318
152675
  let mux;
151319
152676
  try {
151320
152677
  mux = this.deps.spawnMux(buildClipStreamMuxArgs({
@@ -151419,14 +152776,12 @@ var ClipStreamRun = class {
151419
152776
  }
151420
152777
  /** Sever every hop, once, and say why. */
151421
152778
  sever(reason, detail) {
151422
- if (this.cut !== null) return;
152779
+ if (this.cut !== null || this.done) return;
151423
152780
  this.cut = {
151424
152781
  reason,
151425
152782
  detail
151426
152783
  };
151427
- this.stdinGate.dispose();
151428
- this.audioStdinGate.dispose();
151429
- this.peerGate.dispose();
152784
+ this.disposeGates();
151430
152785
  this.mux?.kill("SIGKILL");
151431
152786
  this.deps.logger.warn("videoclips: a clip stream was cut", {
151432
152787
  tags: this.tags,
@@ -151487,6 +152842,8 @@ var ClipStreamRun = class {
151487
152842
  });
151488
152843
  mux.stdin?.end();
151489
152844
  this.audioIn?.end();
152845
+ this.stdinGate.dispose();
152846
+ this.audioStdinGate.dispose();
151490
152847
  const flush = setTimeout(() => this.sever("mux-hung", `ffmpeg did not exit ${this.deps.flushTimeoutMs} ms after its input ended`), this.deps.flushTimeoutMs);
151491
152848
  const code = await this.muxClosed;
151492
152849
  clearTimeout(flush);
@@ -151506,7 +152863,7 @@ var ClipStreamRun = class {
151506
152863
  served() {
151507
152864
  const { row, twin } = this.input;
151508
152865
  const timing = estimateVideoTiming({ timestampsUs: this.timestampsUs });
151509
- const durationSeconds = timing.fpsSource === "timestamps" ? timing.durationSeconds : null;
152866
+ const durationSeconds = this.timestampsBroken || timing.fpsSource !== "timestamps" ? null : timing.durationSeconds;
151510
152867
  const verdict = judgeClipDuration({
151511
152868
  expectedMs: row.endMs - row.startMs,
151512
152869
  decodedSeconds: durationSeconds,
@@ -151524,7 +152881,9 @@ var ClipStreamRun = class {
151524
152881
  expectedMs: row.endMs - row.startMs,
151525
152882
  fps: this.rate.fps,
151526
152883
  fpsSource: this.rate.fpsSource,
151527
- videoType: this.videoType
152884
+ videoType: this.videoType,
152885
+ residueAccessUnits: this.residueUnits,
152886
+ timestampsBroken: this.timestampsBroken
151528
152887
  };
151529
152888
  if (verdict.kind === "short") this.deps.logger.warn("videoclips: a clip stream came up short against its row", {
151530
152889
  tags: this.tags,
@@ -152506,11 +153865,11 @@ function enumerateWallClockDays(sinceMs, untilMs, timeZone) {
152506
153865
  * nowhere). Searching for the KEY rather than the path survives both, and
152507
153866
  * survives the next firmware that nests it differently.
152508
153867
  */
152509
- function findRecordWithKey$1(node, key) {
153868
+ function findRecordWithKey$2(node, key) {
152510
153869
  if (node === null || typeof node !== "object") return null;
152511
153870
  if (Array.isArray(node)) {
152512
153871
  for (const child of node) {
152513
- const found = findRecordWithKey$1(child, key);
153872
+ const found = findRecordWithKey$2(child, key);
152514
153873
  if (found !== null) return found;
152515
153874
  }
152516
153875
  return null;
@@ -152518,7 +153877,7 @@ function findRecordWithKey$1(node, key) {
152518
153877
  const record = { ...node };
152519
153878
  if (key in record) return record;
152520
153879
  for (const value of Object.values(record)) {
152521
- const found = findRecordWithKey$1(value, key);
153880
+ const found = findRecordWithKey$2(value, key);
152522
153881
  if (found !== null) return found;
152523
153882
  }
152524
153883
  return null;
@@ -152538,7 +153897,7 @@ async function readStorageState(api) {
152538
153897
  reason: err instanceof Error ? err.message : String(err)
152539
153898
  };
152540
153899
  }
152541
- const record = findRecordWithKey$1(raw, "mount");
153900
+ const record = findRecordWithKey$2(raw, "mount");
152542
153901
  if (record === null) return { state: "unknown" };
152543
153902
  const mount = asNumber(record["mount"]);
152544
153903
  if (mount === null) return { state: "unknown" };
@@ -155127,6 +156486,313 @@ var reolinkDebugActions = defineCustomActions({ debugRawRead: customAction(RawRe
155127
156486
  auth: "admin"
155128
156487
  }) });
155129
156488
  //#endregion
156489
+ //#region src/recording-onboard-mapping.ts
156490
+ /**
156491
+ * Reolink's schedule `type` → the cap's trigger vocabulary.
156492
+ *
156493
+ * `legacy` is deliberately absent: it appears on 618 and names nothing
156494
+ * an operator would recognise, so a window carrying it is COUNTED as
156495
+ * unread rather than mapped onto a guess.
156496
+ */
156497
+ var TRIGGER_BY_TYPE = {
156498
+ MD: "motion",
156499
+ Normal: "continuous",
156500
+ people: "person",
156501
+ vehicle: "vehicle",
156502
+ dog_cat: "animal",
156503
+ crossline: "lineCrossing",
156504
+ intrude: "intrusion",
156505
+ loitering: "loitering"
156506
+ };
156507
+ /** Triggers a Reolink camera's schedule can name. */
156508
+ var REOLINK_TRIGGERS = [
156509
+ "continuous",
156510
+ "motion",
156511
+ "person",
156512
+ "vehicle",
156513
+ "animal",
156514
+ "lineCrossing",
156515
+ "intrusion",
156516
+ "loitering"
156517
+ ];
156518
+ /**
156519
+ * Find the first record anywhere in the reply that carries `key`.
156520
+ *
156521
+ * Structural, not path-based, because the library's declared shape for
156522
+ * this reply is wrong and a path written against it finds nothing.
156523
+ */
156524
+ function findRecordWithKey$1(node, key) {
156525
+ if (typeof node !== "object" || node === null) return null;
156526
+ if (Array.isArray(node)) {
156527
+ for (const item of node) {
156528
+ const found = findRecordWithKey$1(item, key);
156529
+ if (found) return found;
156530
+ }
156531
+ return null;
156532
+ }
156533
+ const record = { ...node };
156534
+ if (key in record) return record;
156535
+ for (const value of Object.values(record)) {
156536
+ const found = findRecordWithKey$1(value, key);
156537
+ if (found) return found;
156538
+ }
156539
+ return null;
156540
+ }
156541
+ function finiteNumber(value) {
156542
+ if (typeof value !== "number") return null;
156543
+ return Number.isFinite(value) ? value : null;
156544
+ }
156545
+ /**
156546
+ * Total size in MB from Reolink's split pair.
156547
+ *
156548
+ * Null when the camera stated NEITHER half — a size we did not read is
156549
+ * not a size of zero (D393). A stated 0 GB with a stated MB remainder is
156550
+ * a real, small number, and survives as one.
156551
+ */
156552
+ function megabytes(whole, remainder) {
156553
+ const gb = finiteNumber(whole);
156554
+ const mb = finiteNumber(remainder);
156555
+ if (gb === null && mb === null) return null;
156556
+ return (gb ?? 0) * 1024 + (mb ?? 0);
156557
+ }
156558
+ /**
156559
+ * Read `getHddInfoList`.
156560
+ *
156561
+ * `mount === 0` is a camera with no card — the same signal
156562
+ * `clips/clip-catalog.ts` keys off. `format === 0` is a card present but
156563
+ * not usable, which is `absent` with a reason that says so rather than
156564
+ * `present` with a card nothing can be written to.
156565
+ */
156566
+ function parseOnboardStorage(reply) {
156567
+ const record = findRecordWithKey$1(reply, "mount");
156568
+ if (!record) return {
156569
+ kind: "unknown",
156570
+ reason: "The camera answered, but with nothing this reader recognised as storage info."
156571
+ };
156572
+ if (finiteNumber(record.mount) === 0) return {
156573
+ kind: "absent",
156574
+ reason: "The camera reports no mounted storage (HddInfo mount 0)."
156575
+ };
156576
+ const formatted = finiteNumber(record.format);
156577
+ const id = finiteNumber(record.number);
156578
+ const volume = {
156579
+ id: id === null ? "0" : String(id),
156580
+ status: formatted === 0 ? "unformatted" : "ok",
156581
+ capacityMb: megabytes(record.capacity, record.capacityM),
156582
+ freeMb: megabytes(record.remainSize, record.remainSizeM)
156583
+ };
156584
+ if (formatted === 0) return {
156585
+ kind: "absent",
156586
+ reason: "The camera reports a card that is not formatted, so it cannot record to it."
156587
+ };
156588
+ return {
156589
+ kind: "present",
156590
+ volumes: [volume]
156591
+ };
156592
+ }
156593
+ /**
156594
+ * Expand every `typeScheduleList` entry into windows.
156595
+ *
156596
+ * Entries sharing a trigger are merged slot-wise before expansion, so
156597
+ * three `crossline` rules produce one set of line-crossing windows
156598
+ * rather than three overlapping copies.
156599
+ *
156600
+ * An entry whose `type` is unknown, or whose table is not 168 characters,
156601
+ * is counted as unread. A table of the wrong length is not a schedule of
156602
+ * fewer days.
156603
+ */
156604
+ function parseSchedule(schedule) {
156605
+ const entries = schedule?.typeScheduleList ?? [];
156606
+ const merged = /* @__PURE__ */ new Map();
156607
+ let unreadable = 0;
156608
+ for (const entry of entries) {
156609
+ const trigger = TRIGGER_BY_TYPE[entry.type];
156610
+ if (trigger === void 0 || entry.valueTable.length !== 168) {
156611
+ if (entry.valueTable.includes("1")) unreadable += 1;
156612
+ continue;
156613
+ }
156614
+ const existing = merged.get(trigger);
156615
+ if (!existing) {
156616
+ merged.set(trigger, [...entry.valueTable]);
156617
+ continue;
156618
+ }
156619
+ for (let i = 0; i < existing.length; i += 1) if (entry.valueTable[i] === "1") existing[i] = "1";
156620
+ }
156621
+ const windows = [];
156622
+ for (const [trigger, slots] of merged) windows.push(...weeklyMaskToWindows(slots.join(""), trigger));
156623
+ return {
156624
+ windows,
156625
+ unreadable
156626
+ };
156627
+ }
156628
+ /**
156629
+ * Fold the three reads into the cap's status.
156630
+ *
156631
+ * `tracks` is empty and `primaryTrackId` null on purpose: a Reolink
156632
+ * channel exposes no track list, and inventing one synthetic row would
156633
+ * put a fact in the UI that the camera never stated. The form renders
156634
+ * "the camera reports no recording track", which is true.
156635
+ */
156636
+ function buildOnboardStatus(input) {
156637
+ const { windows, unreadable } = parseSchedule(input.schedule);
156638
+ const masterOn = input.schedule?.enable ?? null;
156639
+ const scheduled = masterOn === true && windows.length > 0;
156640
+ const cycle = input.cfg?.cycle;
156641
+ return {
156642
+ storage: input.storage,
156643
+ tracks: [],
156644
+ primaryTrackId: null,
156645
+ enabled: masterOn,
156646
+ overwriteWhenFull: cycle === void 0 ? null : cycle !== 0,
156647
+ preRecordSec: input.cfg?.preRecordTime ?? null,
156648
+ postRecordSec: input.cfg?.recordDelayTime ?? null,
156649
+ segmentMinutes: input.cfg?.packageTime ?? null,
156650
+ windows: [...windows],
156651
+ unreadableWindows: unreadable,
156652
+ recordingToNowhere: scheduled && input.storage.kind === "absent",
156653
+ lastFetchedAt: input.now
156654
+ };
156655
+ }
156656
+ /** Why a Reolink camera cannot be written until CamStack has read it. */
156657
+ var REOLINK_NOT_READ_REASON = "CamStack has not read this camera yet";
156658
+ /**
156659
+ * Why the schedule is unwritable when one trigger occupies several rows.
156660
+ *
156661
+ * An E1 Outdoor Pro lists `crossline`, `intrude` and `loitering` three times
156662
+ * each — one row per configured rule. The status READ merges them, because
156663
+ * the cap's question is "does this camera record on line-crossing". A write
156664
+ * cannot un-merge them: addressed by type alone it rewrites all three rules
156665
+ * with one mask (measured — `replaceItemValueTable` matches every row of a
156666
+ * type when no index is given), and the cap's patch carries no room to say
156667
+ * which rule an operator meant. So the schedule is REPORTED unwritable on
156668
+ * such a camera rather than writable and destructive.
156669
+ */
156670
+ var REOLINK_MERGED_ROWS_REASON = "this camera stores several rules of the same kind (one row per configured rule) and CamStack cannot tell them apart, so writing the schedule would overwrite all of them with one mask";
156671
+ /** How many rows each trigger occupies, from the camera's own list. */
156672
+ function rowsPerTrigger(schedule) {
156673
+ const counts = /* @__PURE__ */ new Map();
156674
+ for (const entry of schedule?.typeScheduleList ?? []) {
156675
+ const trigger = TRIGGER_BY_TYPE[entry.type];
156676
+ if (trigger === void 0) continue;
156677
+ counts.set(trigger, (counts.get(trigger) ?? 0) + 1);
156678
+ }
156679
+ return counts;
156680
+ }
156681
+ /**
156682
+ * What THIS Reolink accepts.
156683
+ *
156684
+ * Writability is decided per field from the camera's OWN statement, never
156685
+ * from a vendor default:
156686
+ *
156687
+ * - `overwriteWhenFull` needs `<cyclelist>` to offer both 0 and 1; the
156688
+ * cap's field is a boolean and a camera offering one value cannot be
156689
+ * set both ways. `<cyclelist>` IS enforced by the firmware (writing 7
156690
+ * was answered 200 and dropped), so it is a real refusal.
156691
+ * - `preRecordSec` is a two-member SET, not a span. The wire field is
156692
+ * seconds and the firmware honours exactly two states: 0 is off, and
156693
+ * 5, 15 and 30 all read back as 10 — identically on two firmwares. A
156694
+ * slider here would move and change nothing.
156695
+ * - `postRecordSec` carries NO allowed set although the camera states
156696
+ * `<timeList>`: that list is advisory, and a Home Hub offering
156697
+ * `8,15,30` accepted and applied 20. Refusing against it would block a
156698
+ * value the firmware honours, so the write goes through unconstrained
156699
+ * and the read-back diff is what reports a value the camera dropped.
156700
+ * - `segmentMinutes` likewise carries no bound, because no camera states
156701
+ * one and an invented maximum refuses values the firmware may take.
156702
+ */
156703
+ function buildOnboardOptions(input) {
156704
+ const reached = input.status !== null;
156705
+ const notRead = {
156706
+ readable: false,
156707
+ writable: false,
156708
+ reason: REOLINK_NOT_READ_REASON
156709
+ };
156710
+ const writable = {
156711
+ readable: true,
156712
+ writable: true
156713
+ };
156714
+ const refuse = (reason) => ({
156715
+ readable: true,
156716
+ writable: false,
156717
+ reason
156718
+ });
156719
+ if (!reached || input.limits === null) return {
156720
+ enabled: notRead,
156721
+ overwriteWhenFull: notRead,
156722
+ preRecordSec: notRead,
156723
+ postRecordSec: notRead,
156724
+ segmentMinutes: notRead,
156725
+ schedule: {
156726
+ support: notRead,
156727
+ granularityMinutes: 60,
156728
+ triggers: [...REOLINK_TRIGGERS],
156729
+ supportsOverlappingTriggers: true
156730
+ }
156731
+ };
156732
+ const limits = input.limits;
156733
+ const cycleChoices = limits.cycleList;
156734
+ const overwrite = cycleChoices.includes(0) && cycleChoices.includes(1) ? writable : refuse(cycleChoices.length === 0 ? "this camera did not say which overwrite settings it accepts" : `this camera accepts only ${cycleChoices.join(", ")} for overwrite, so it cannot be switched both ways`);
156735
+ const preRecord = limits.preRecordSupported ? writable : refuse("this camera reports no pre-record setting, so writing one would do nothing");
156736
+ const merged = [...rowsPerTrigger(input.schedule).entries()].filter(([, count]) => count > 1);
156737
+ const unreadable = input.status?.unreadableWindows ?? 0;
156738
+ const scheduleSupport = unreadable > 0 ? refuse(`CamStack could not read ${String(unreadable)} of this camera's schedule rows, and a write would replace what it could not show`) : merged.length > 0 ? refuse(REOLINK_MERGED_ROWS_REASON) : writable;
156739
+ return {
156740
+ enabled: writable,
156741
+ overwriteWhenFull: overwrite,
156742
+ preRecordSec: preRecord,
156743
+ ...limits.preRecordSupported ? { preRecordSecAllowed: { values: [0, limits.preRecordSeconds ?? 10] } } : {},
156744
+ postRecordSec: writable,
156745
+ segmentMinutes: writable,
156746
+ schedule: {
156747
+ support: scheduleSupport,
156748
+ granularityMinutes: 60,
156749
+ triggers: [...REOLINK_TRIGGERS],
156750
+ supportsOverlappingTriggers: true
156751
+ }
156752
+ };
156753
+ }
156754
+ /** The cap's trigger vocabulary → Reolink's schedule `type`. */
156755
+ var TYPE_BY_TRIGGER = Object.fromEntries(Object.entries(TRIGGER_BY_TYPE).filter((pair) => pair[1] !== void 0).map(([type, trigger]) => [trigger, type]));
156756
+ /**
156757
+ * Turn a cap patch into the two Baichuan writes.
156758
+ *
156759
+ * `windows` is the COMPLETE new set, so every trigger the camera lists gets
156760
+ * a row — including one the new set does not mention, which must be written
156761
+ * as an all-zero mask or the camera keeps recording on a trigger the
156762
+ * operator just removed. A trigger the camera does NOT list is skipped: a
156763
+ * row the camera never had is answered 200 and discarded, so sending it
156764
+ * would report success for nothing.
156765
+ *
156766
+ * `preRecordSec` collapses to the boolean the firmware really has: any
156767
+ * non-zero ask is "on", because 5, 15 and 30 all store as 10.
156768
+ */
156769
+ function buildOnboardWrites(patch, schedule) {
156770
+ const cfg = {};
156771
+ if (patch.overwriteWhenFull !== void 0) cfg.cycle = patch.overwriteWhenFull ? 1 : 0;
156772
+ if (patch.postRecordSec !== void 0) cfg.recordDelayTime = patch.postRecordSec;
156773
+ if (patch.preRecordSec !== void 0) cfg.preRecordEnabled = patch.preRecordSec !== 0;
156774
+ if (patch.segmentMinutes !== void 0) cfg.packageTime = patch.segmentMinutes;
156775
+ const scheduleOut = {};
156776
+ if (patch.enabled !== void 0) scheduleOut.enable = patch.enabled;
156777
+ if (patch.windows !== void 0) {
156778
+ const present = new Set((schedule?.typeScheduleList ?? []).map((e) => e.type));
156779
+ const entries = [];
156780
+ for (const trigger of REOLINK_TRIGGERS) {
156781
+ const type = TYPE_BY_TRIGGER[trigger];
156782
+ if (type === void 0 || !present.has(type)) continue;
156783
+ entries.push({
156784
+ type,
156785
+ valueTable: windowsToWeeklyMask(patch.windows, trigger)
156786
+ });
156787
+ }
156788
+ scheduleOut.entries = entries;
156789
+ }
156790
+ return {
156791
+ cfg: Object.keys(cfg).length > 0 ? cfg : null,
156792
+ schedule: Object.keys(scheduleOut).length > 0 ? scheduleOut : null
156793
+ };
156794
+ }
156795
+ //#endregion
155130
156796
  //#region src/battery-charging.ts
155131
156797
  /**
155132
156798
  * Da `adapterStatus` + `chargeStatus` alla sorgente di alimentazione.
@@ -155623,6 +157289,82 @@ function decideSnapshotCameraAccess(input) {
155623
157289
  };
155624
157290
  }
155625
157291
  //#endregion
157292
+ //#region src/sleep-state-decision.ts
157293
+ /**
157294
+ * Hysteresis is ASYMMETRIC, because the two directions have very different
157295
+ * costs when we get them wrong.
157296
+ *
157297
+ * `→ awake` (`WAKE_HYSTERESIS_MS`): expensive if wrong. Believing an asleep
157298
+ * camera is awake un-gates every background probe and makes `wakeForStream`
157299
+ * early-out without actually waking. Keep the window >= the library's UDP
157300
+ * inference cycle (~12 s on battery cams with idle-disconnect) and >= its
157301
+ * `getIdleDisconnectTimeoutMs` (30 s default).
157302
+ *
157303
+ * `→ sleeping` (`SLEEP_HYSTERESIS_MS`): cheap if wrong. Believing an awake
157304
+ * camera is asleep only means we skip background probes and serve cached
157305
+ * values; the demand paths still drive a real wake. The old symmetric 30 s
157306
+ * window was LONGER than the camera's own awake window (~14 s), so a natural
157307
+ * wake→sleep cycle had its `sleeping` transition dropped and the slice stayed
157308
+ * stuck on "awake" while the camera slept.
157309
+ */
157310
+ var WAKE_HYSTERESIS_MS = 3e4;
157311
+ /** See {@link WAKE_HYSTERESIS_MS}. */
157312
+ var SLEEP_HYSTERESIS_MS = 5e3;
157313
+ /**
157314
+ * How long a freshly committed FIRST-HAND state is protected from a lagging
157315
+ * summary's awake→sleeping flip. The summary lags real pushes by over a
157316
+ * minute (measured >75 s on 2026-08-16); 3 min covers two full
157317
+ * discovery-refresh cycles of lag without letting a genuinely stuck "awake"
157318
+ * slice stand forever — the reconcile still wins after the grace, which is
157319
+ * exactly its recovery role.
157320
+ */
157321
+ var HUB_SUMMARY_SLEEP_GRACE_MS = 18e4;
157322
+ /** A source whose verdict comes from the parent NVR's channel summary. */
157323
+ function isSummarySourced(source) {
157324
+ return source === "hub-summary" || source === "hub-reconcile";
157325
+ }
157326
+ /**
157327
+ * Decide one commit. Pure: the caller owns the slice, the clock and the log.
157328
+ *
157329
+ * Order of the gates, and why:
157330
+ * 1. No-op first — re-asserting the current value is never a transition.
157331
+ * 2. A summary-sourced verdict is firmware state, not socket inference, so
157332
+ * the anti-flap window does not apply to it at all.
157333
+ * 3. A `confirmed-wake` in the `→ awake` direction is first-hand evidence
157334
+ * this process produced: gating it with the anti-flap window is what
157335
+ * stranded 3825's mirror on "asleep" while `wakeForStream` answered
157336
+ * `awoke: true`.
157337
+ * 4. Everything else is subject to the asymmetric hysteresis.
157338
+ * 5. A summary flip toward `sleeping` is refused while a first-hand
157339
+ * observation inside the grace says otherwise.
157340
+ */
157341
+ function decideSleepCommit(input) {
157342
+ const { current, next, source, lastFirstHandCommitAt, nowMs } = input;
157343
+ if (current === next) return { kind: "unchanged" };
157344
+ const summarySourced = isSummarySourced(source);
157345
+ const firstHandWake = source === "confirmed-wake" && next === false;
157346
+ const sinceLastChangeMs = nowMs - lastFirstHandCommitAt;
157347
+ if (!summarySourced && !firstHandWake) {
157348
+ const windowMs = next ? SLEEP_HYSTERESIS_MS : WAKE_HYSTERESIS_MS;
157349
+ if (lastFirstHandCommitAt > 0 && sinceLastChangeMs < windowMs) return {
157350
+ kind: "refused",
157351
+ reason: "hysteresis",
157352
+ windowMs,
157353
+ sinceLastChangeMs
157354
+ };
157355
+ }
157356
+ if (summarySourced && next === true && lastFirstHandCommitAt > 0 && sinceLastChangeMs < 18e4) return {
157357
+ kind: "refused",
157358
+ reason: "summary-grace",
157359
+ windowMs: HUB_SUMMARY_SLEEP_GRACE_MS,
157360
+ sinceLastChangeMs
157361
+ };
157362
+ return {
157363
+ kind: "commit",
157364
+ stampFirstHand: source !== "hub-reconcile"
157365
+ };
157366
+ }
157367
+ //#endregion
155626
157368
  //#region src/raw-state.ts
155627
157369
  /**
155628
157370
  * Source tag for every raw-state blob this provider emits.
@@ -158869,53 +160611,20 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
158869
160611
  return this.state.battery.sleeping ?? false;
158870
160612
  }
158871
160613
  /**
158872
- * Wall-clock ms when `this.sleeping` last flipped. Drives a
158873
- * hysteresis window that filters out the lib's UDP sleep-inference
158874
- * flapping. The lib runs an internal `getSleepStatus` every 2s on
158875
- * UDP/battery cameras and emits awake/sleeping events from socket
158876
- * I/O patterns alone — those events do NOT reflect real firmware
158877
- * state, especially on battery cams with idle-disconnect where
158878
- * socket activity drops periodically. Without this window every
158879
- * inferred flip would close active streams + emit a cap event.
160614
+ * Wall-clock ms when a FIRST-HAND observation last moved `this.sleeping`.
160615
+ *
160616
+ * It drives two windows — the anti-flap hysteresis that filters the lib's
160617
+ * UDP sleep inference, and the grace that stops the parent's LAGGING
160618
+ * summary from clobbering a fresher verdict. Both want the same thing:
160619
+ * "when did something that was actually looking at the camera last speak".
160620
+ *
160621
+ * The parent's periodic summary sweep (`hub-reconcile`) therefore never
160622
+ * stamps this. It is firmware truth, but it is minutes old, and stamping
160623
+ * it armed the 30 s wake window against the camera's own wake push — see
160624
+ * `sleep-state-decision.ts` for the measurement (D603).
158880
160625
  */
158881
160626
  sleepStateChangedAt = 0;
158882
160627
  /**
158883
- * Hysteresis is ASYMMETRIC, because the two directions have very
158884
- * different costs when we get them wrong.
158885
- *
158886
- * `→ awake` (`WAKE_HYSTERESIS_MS`): expensive if wrong. Believing an
158887
- * asleep camera is awake un-gates every background probe
158888
- * (`refreshParentSettingsSnapshot`, the device-config cap refreshes,
158889
- * aux align) and makes `wakeForStream` early-out without actually
158890
- * waking. Keep the full window >= the lib's UDP inference cycle
158891
- * (~12s on battery cams with idle-disconnect) and >= the lib's
158892
- * `getIdleDisconnectTimeoutMs` (30s default), so neither an
158893
- * inference flap nor a transient socket close→reopen can walk us
158894
- * into a false "awake".
158895
- *
158896
- * `→ sleeping` (`SLEEP_HYSTERESIS_MS`): cheap if wrong. Believing an
158897
- * awake camera is asleep only means we skip background probes and
158898
- * serve cached values; the demand paths (`wakeForStream`,
158899
- * `wakeIfSleeping`) still drive a real wake. The old symmetric 30s
158900
- * window was LONGER than the camera's own awake window (~14s — see
158901
- * `onWakeTransition`), so a natural wake→sleep cycle had its
158902
- * `sleeping` transition dropped and the slice stayed stuck on
158903
- * "awake" while the camera slept: the exact "at rest we lose the
158904
- * sleeping state" symptom. 5s still absorbs a single 2s inference
158905
- * tick while tracking the real cycle.
158906
- */
158907
- static WAKE_HYSTERESIS_MS = 3e4;
158908
- static SLEEP_HYSTERESIS_MS = 5e3;
158909
- /**
158910
- * How long a freshly committed state is protected from a hub-summary
158911
- * awake→sleeping flip. The summary's per-channel sleep flag lags real
158912
- * pushes by over a minute (measured >75 s on 2026-08-16); 3 min covers
158913
- * two full discovery-refresh cycles of lag without letting a genuinely
158914
- * stuck "awake" slice stand forever — the reconcile still wins after
158915
- * the grace, which is exactly its recovery role.
158916
- */
158917
- static HUB_SUMMARY_SLEEP_GRACE_MS = 18e4;
158918
- /**
158919
160628
  * Wall-clock ms of the last PROACTIVE wake — one we issued on our own
158920
160629
  * initiative (background snapshot refresh), not because the operator
158921
160630
  * or a stream consumer asked for the camera. Drives
@@ -159830,6 +161539,32 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
159830
161539
  await (await this.ensureApi()).reboot(this.getChannel());
159831
161540
  return { success: true };
159832
161541
  } });
161542
+ this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
161543
+ if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
161544
+ const host = this.config.get("host") ?? "";
161545
+ const built = buildDeviceAdminUrl({
161546
+ host,
161547
+ port: null,
161548
+ scheme: "http",
161549
+ path: "/"
161550
+ });
161551
+ if (!built.ok) {
161552
+ this.ctx.logger.debug("reolink admin link unavailable", {
161553
+ tags: { deviceId: this.id },
161554
+ meta: {
161555
+ reason: built.reason,
161556
+ transport: this.config.get("transport") ?? "tcp"
161557
+ }
161558
+ });
161559
+ return null;
161560
+ }
161561
+ return {
161562
+ url: built.url,
161563
+ label: "Web UI",
161564
+ target: "device",
161565
+ host
161566
+ };
161567
+ } });
159833
161568
  this.registerNetworkLink();
159834
161569
  this.ctx.registerNativeCap(motionCapability, { isDetected: async ({ deviceId }) => {
159835
161570
  if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
@@ -159876,6 +161611,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
159876
161611
  this.registerPrivacyMaskCap();
159877
161612
  this.registerDayNightCap();
159878
161613
  this.registerImageSettingsCap();
161614
+ this.registerRecordingOnboardCap();
159879
161615
  this.registerStreamCatalogProvider();
159880
161616
  this.registerNativeObjectDetectionCap();
159881
161617
  const cache = this.config.get("deviceCache");
@@ -159919,6 +161655,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
159919
161655
  dayNightRefreshInFlight = null;
159920
161656
  /** Single-flight guard for the `image-settings` cap refresh. */
159921
161657
  imageSettingsRefreshInFlight = null;
161658
+ /** Single-flight guard for the `recording-onboard` cap refresh. */
161659
+ recordingOnboardRefreshInFlight = null;
159922
161660
  /**
159923
161661
  * Single-flight guard for snapshot fetches (Slice 10). When two
159924
161662
  * consumers hit `getSnapshot` concurrently we issue ONE Baichuan
@@ -160120,35 +161858,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
160120
161858
  }
160121
161859
  }
160122
161860
  /**
160123
- * Decide whether to honour a sleep-state transition. Returns
160124
- * `false` when the lib's UDP inference is still inside the
160125
- * hysteresis window so we don't flap. The window is asymmetric —
160126
- * see `WAKE_HYSTERESIS_MS` / `SLEEP_HYSTERESIS_MS`. The first
160127
- * transition AFTER an api login / restart is always honoured —
160128
- * no prior-flip timestamp gating it.
160129
- *
160130
- * @param next - the desired next value of `this.sleeping`
160131
- * @returns `true` if the caller should commit the new state
160132
- */
160133
- acceptSleepingTransition(next) {
160134
- if (this.sleeping === next) return false;
160135
- const now = Date.now();
160136
- const windowMs = next ? ReolinkCamera.SLEEP_HYSTERESIS_MS : ReolinkCamera.WAKE_HYSTERESIS_MS;
160137
- if (this.sleepStateChangedAt > 0 && now - this.sleepStateChangedAt < windowMs) {
160138
- this.ctx.logger.debug("ignoring sleep inference flap within hysteresis window", {
160139
- tags: { deviceId: this.id },
160140
- meta: {
160141
- next,
160142
- windowMs,
160143
- sinceLastChangeMs: now - this.sleepStateChangedAt
160144
- }
160145
- });
160146
- return false;
160147
- }
160148
- this.sleepStateChangedAt = now;
160149
- return true;
160150
- }
160151
- /**
160152
161861
  * THE single writer of the `battery.sleeping` slice.
160153
161862
  *
160154
161863
  * Before this existed the state had three independent writers with
@@ -160159,38 +161868,38 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
160159
161868
  * ancient timestamp). Funnelling every writer through here keeps one
160160
161869
  * rule set and one timestamp.
160161
161870
  *
160162
- * @param source - `'hub-summary'` is AUTHORITATIVE (the NVR reports
160163
- * per-channel sleep state from its own firmware, not from socket
160164
- * I/O inference) and therefore bypasses the hysteresis — with ONE
160165
- * exception: the summary LAGS real push state (measured >75 s behind
160166
- * an `awake` push, 2026-08-16), so an awake→sleeping flip from the
160167
- * summary is refused while a fresher committed state is still inside
160168
- * `HUB_SUMMARY_SLEEP_GRACE_MS`. Without the guard the 60 s reconcile
160169
- * clobbered the fresh awake right back to sleeping for a full cycle —
160170
- * the viewer flashed the live frame, then the sleeping overlay, then
160171
- * live again. Sleeping→awake from the summary is always honoured
160172
- * (it strands nothing; the camera wakes regardless).
161871
+ * The rule itself lives in `sleep-state-decision.ts`, pure, so it can be
161872
+ * driven by a test instead of asserted from this file's source text. Read
161873
+ * that module for why `hub-reconcile` — the parent's periodic summary
161874
+ * sweep — stamps nothing: it lags real state by more than a minute, and
161875
+ * arming the anti-flap window from it threw away the camera's real wake
161876
+ * for up to 2 min 26 s on the live hub (D603).
161877
+ *
160173
161878
  * @returns `true` when the slice actually changed.
160174
161879
  */
160175
161880
  commitSleepState(next, source) {
160176
161881
  if (!this.isBattery) return false;
160177
- if (this.sleeping === next) return false;
160178
- if (source === "confirmed-wake" && next === false) this.sleepStateChangedAt = Date.now();
160179
- else if (source !== "hub-summary" && !this.acceptSleepingTransition(next)) return false;
160180
- if (source === "hub-summary") {
160181
- const sinceCommitMs = Date.now() - this.sleepStateChangedAt;
160182
- if (next === true && this.sleepStateChangedAt > 0 && sinceCommitMs < ReolinkCamera.HUB_SUMMARY_SLEEP_GRACE_MS) {
160183
- this.ctx.logger.debug("hub-summary sleep flip refused — fresher state inside grace", {
160184
- tags: { deviceId: this.id },
160185
- meta: {
160186
- sinceCommitMs,
160187
- graceMs: ReolinkCamera.HUB_SUMMARY_SLEEP_GRACE_MS
160188
- }
160189
- });
160190
- return false;
160191
- }
160192
- this.sleepStateChangedAt = Date.now();
161882
+ const decision = decideSleepCommit({
161883
+ current: this.sleeping,
161884
+ next,
161885
+ source,
161886
+ lastFirstHandCommitAt: this.sleepStateChangedAt,
161887
+ nowMs: Date.now()
161888
+ });
161889
+ if (decision.kind === "unchanged") return false;
161890
+ if (decision.kind === "refused") {
161891
+ this.ctx.logger.debug(decision.reason === "hysteresis" ? "ignoring sleep inference flap within hysteresis window" : "hub-summary sleep flip refused — fresher first-hand state inside grace", {
161892
+ tags: { deviceId: this.id },
161893
+ meta: {
161894
+ next,
161895
+ source,
161896
+ windowMs: decision.windowMs,
161897
+ sinceLastChangeMs: decision.sinceLastChangeMs
161898
+ }
161899
+ });
161900
+ return false;
160193
161901
  }
161902
+ if (decision.stampFirstHand) this.sleepStateChangedAt = Date.now();
160194
161903
  this.state.battery.sleeping = next;
160195
161904
  this.ctx.logger.info("battery sleep state committed", {
160196
161905
  tags: { deviceId: this.id },
@@ -160217,7 +161926,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
160217
161926
  */
160218
161927
  applyHubSleepState(sleeping) {
160219
161928
  if (!this.isBattery) return;
160220
- if (this.commitSleepState(sleeping, "hub-summary") && !sleeping) this.onWakeTransition("hub-summary").catch(() => {});
161929
+ if (this.commitSleepState(sleeping, "hub-reconcile") && !sleeping) this.onWakeTransition("hub-reconcile").catch(() => {});
160221
161930
  }
160222
161931
  /**
160223
161932
  * Rate-limit for wakes we issue on our OWN initiative. Returns
@@ -161914,6 +163623,173 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
161914
163623
  * denormalized against the camera's OWN probed threshold range
161915
163624
  * (never a hardcoded 0..255 guess).
161916
163625
  */
163626
+ /**
163627
+ * Register the vendor-neutral `recording-onboard` cap.
163628
+ *
163629
+ * READ-ONLY on Reolink, and it says so per field. `@apocaliss92/
163630
+ * nodelink-js` exposes a reader for the record config and the record
163631
+ * schedule and no writer; the Baichuan SET command ids exist but
163632
+ * nothing in this repo has ever sent them, and the only route is
163633
+ * hand-built XML through the generic passthrough. So `getOptions`
163634
+ * reports every field `writable: false` with
163635
+ * {@link REOLINK_NO_WRITE_REASON}, the UI renders each control
163636
+ * disabled WITH that sentence — disabled must not look like broken —
163637
+ * and `setSettings` refuses by name instead of accepting a write and
163638
+ * dropping it.
163639
+ *
163640
+ * The cap's FIELD SET is identical to Hikvision's. Only the answers
163641
+ * differ.
163642
+ */
163643
+ registerRecordingOnboardCap() {
163644
+ const channel = this.getChannel();
163645
+ const CAP_NAME = "recording-onboard";
163646
+ const emptyStatus = () => ({
163647
+ storage: {
163648
+ kind: "unknown",
163649
+ reason: "CamStack has not read this camera yet."
163650
+ },
163651
+ tracks: [],
163652
+ primaryTrackId: null,
163653
+ enabled: null,
163654
+ overwriteWhenFull: null,
163655
+ preRecordSec: null,
163656
+ postRecordSec: null,
163657
+ segmentMinutes: null,
163658
+ windows: [],
163659
+ unreadableWindows: 0,
163660
+ recordingToNowhere: false,
163661
+ lastFetchedAt: 0
163662
+ });
163663
+ /** Last `getRecordCfg` reply. */
163664
+ let lastCfg = null;
163665
+ /** Last `getRecordCfgLimits` — what the camera says it accepts. */
163666
+ let lastLimits = null;
163667
+ /** Last schedule read — which trigger rows this camera actually has. */
163668
+ let lastSchedule = null;
163669
+ const refreshFromCamera = async () => {
163670
+ if (this.recordingOnboardRefreshInFlight) return this.recordingOnboardRefreshInFlight;
163671
+ const promise = (async () => {
163672
+ try {
163673
+ const api = await this.ensureApi();
163674
+ const [cfg, limits, schedule, hdd] = await Promise.all([
163675
+ api.getRecordCfg(channel).catch(() => null),
163676
+ api.getRecordCfgLimits(channel).catch(() => null),
163677
+ api.getRecordSchedule(channel).catch(() => null),
163678
+ this.isHubChild() ? Promise.resolve(null) : api.getHddInfoList().catch(() => null)
163679
+ ]);
163680
+ lastCfg = cfg;
163681
+ lastLimits = limits;
163682
+ lastSchedule = schedule;
163683
+ const next = buildOnboardStatus({
163684
+ storage: this.isHubChild() ? {
163685
+ kind: "unknown",
163686
+ reason: "This camera hangs off a Reolink Hub, so its footage lives on the Hub’s disk, not on a card of its own."
163687
+ } : hdd === null ? {
163688
+ kind: "unknown",
163689
+ reason: "The camera did not answer the storage read."
163690
+ } : parseOnboardStorage(hdd),
163691
+ cfg,
163692
+ schedule,
163693
+ now: Date.now()
163694
+ });
163695
+ this.runtimeState.setCapState(CAP_NAME, next);
163696
+ if (next.recordingToNowhere) this.ctx.logger.warn("reolink onboard recording: camera is scheduled to record and has no usable storage", {
163697
+ tags: { deviceId: this.id },
163698
+ meta: { windows: next.windows.length }
163699
+ });
163700
+ if (next.unreadableWindows > 0) this.ctx.logger.warn("reolink onboard recording: some schedule tables could not be read — the schedule shown is incomplete", {
163701
+ tags: { deviceId: this.id },
163702
+ meta: {
163703
+ unreadableWindows: next.unreadableWindows,
163704
+ readWindows: next.windows.length
163705
+ }
163706
+ });
163707
+ } catch (err) {
163708
+ this.ctx.logger.debug("reolink onboard recording refresh failed — keeping last slice", {
163709
+ tags: { deviceId: this.id },
163710
+ meta: { error: err instanceof Error ? err.message : String(err) }
163711
+ });
163712
+ }
163713
+ })();
163714
+ this.recordingOnboardRefreshInFlight = promise;
163715
+ try {
163716
+ await promise;
163717
+ } finally {
163718
+ this.recordingOnboardRefreshInFlight = null;
163719
+ }
163720
+ };
163721
+ const bridge = createRuntimeStateBridge({
163722
+ runtimeState: this.runtimeState,
163723
+ cap: recordingOnboardCapability,
163724
+ ownDeviceId: this.id,
163725
+ refresh: this.sleepGatedRefresh(CAP_NAME, refreshFromCamera),
163726
+ staleMs: OPERATOR_WRITTEN_STALE_MS,
163727
+ staleRead: "serve-and-revalidate",
163728
+ empty: emptyStatus
163729
+ });
163730
+ const readOptions = () => buildOnboardOptions({
163731
+ status: this.runtimeState.getCapState(CAP_NAME) ?? null,
163732
+ cfg: lastCfg,
163733
+ limits: lastLimits,
163734
+ schedule: lastSchedule
163735
+ });
163736
+ const provider = {
163737
+ getStatus: bridge.getStatus,
163738
+ getOptions: async ({ deviceId }) => {
163739
+ if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
163740
+ await bridge.ensureFresh();
163741
+ return readOptions();
163742
+ },
163743
+ setSettings: async ({ deviceId, settings }) => {
163744
+ if (deviceId !== this.id) return;
163745
+ if (Object.keys(settings).length === 0) return;
163746
+ await bridge.ensureFresh();
163747
+ const refusal = describeOnboardRefusal(readOptions(), settings);
163748
+ if (refusal !== null) {
163749
+ this.ctx.logger.warn("reolink onboard recording: refusing a write this camera cannot take", {
163750
+ tags: { deviceId: this.id },
163751
+ meta: {
163752
+ refusal,
163753
+ fields: Object.keys(settings)
163754
+ }
163755
+ });
163756
+ throw new Error(`device ${String(this.id)}: ${refusal}`);
163757
+ }
163758
+ const writes = buildOnboardWrites(settings, lastSchedule);
163759
+ const api = await this.ensureApi();
163760
+ const ignored = [];
163761
+ if (writes.cfg !== null) {
163762
+ const result = await api.setRecordCfg(channel, writes.cfg);
163763
+ for (const outcome of result.outcomes) if (outcome.status === "ignored") ignored.push(`${outcome.field}: asked ${outcome.requested}, camera kept ${outcome.actual}`);
163764
+ }
163765
+ if (writes.schedule !== null) {
163766
+ const result = await api.setRecordSchedule(channel, writes.schedule);
163767
+ for (const outcome of result.outcomes) if (outcome.status === "ignored") ignored.push(`schedule ${outcome.field}: the camera kept its own value`);
163768
+ }
163769
+ await refreshFromCamera();
163770
+ if (ignored.length > 0) {
163771
+ this.ctx.logger.warn("reolink onboard recording: the camera answered 200 and discarded part of the write", {
163772
+ tags: { deviceId: this.id },
163773
+ meta: {
163774
+ ignored,
163775
+ fields: Object.keys(settings)
163776
+ }
163777
+ });
163778
+ throw new Error(`device ${String(this.id)}: the camera accepted the request and kept its own values — ${ignored.join("; ")}`);
163779
+ }
163780
+ this.ctx.logger.info("reolink onboard recording: write applied", {
163781
+ tags: { deviceId: this.id },
163782
+ meta: { fields: Object.keys(settings) }
163783
+ });
163784
+ }
163785
+ };
163786
+ this.registerCapWarmer(CAP_NAME, async () => {
163787
+ await provider.getOptions({ deviceId: this.id });
163788
+ await refreshFromCamera();
163789
+ });
163790
+ this.ctx.registerNativeCap(recordingOnboardCapability, provider);
163791
+ this.ctx.logger.info("Reolink recording-onboard cap registered", { tags: { deviceId: this.id } });
163792
+ }
161917
163793
  registerDayNightCap() {
161918
163794
  const channel = this.getChannel();
161919
163795
  const CAP_NAME = "day-night";
@@ -166343,6 +168219,29 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
166343
168219
  await (await this.ensureApi()).reboot();
166344
168220
  return { success: true };
166345
168221
  } });
168222
+ this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
168223
+ if (deviceId !== this.id) throw new Error(`ReolinkHub: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
168224
+ const host = this.config.get("host") ?? "";
168225
+ const built = buildDeviceAdminUrl({
168226
+ host,
168227
+ port: null,
168228
+ scheme: "http",
168229
+ path: "/"
168230
+ });
168231
+ if (!built.ok) {
168232
+ this.ctx.logger.debug("reolink hub admin link unavailable", {
168233
+ tags: { deviceId: this.id },
168234
+ meta: { reason: built.reason }
168235
+ });
168236
+ return null;
168237
+ }
168238
+ return {
168239
+ url: built.url,
168240
+ label: "Web UI",
168241
+ target: "device",
168242
+ host
168243
+ };
168244
+ } });
166346
168245
  }
166347
168246
  async onActivate() {
166348
168247
  this.ensureApi().catch((err) => {