@camstack/addon-provider-reolink 1.2.160 → 1.2.161

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, …).
@@ -27982,6 +28186,511 @@ var dayNightCapability = {
27982
28186
  volatileStateFields: ["lastFetchedAt"]
27983
28187
  };
27984
28188
  /**
28189
+ * Vendor-neutral **onboard** recording + storage cap — what the CAMERA
28190
+ * writes to the CAMERA's own card, on the camera's own schedule.
28191
+ *
28192
+ * This is NOT `recording.cap.ts`. That one is CamStack's recorder: our
28193
+ * footage ledger, our storage locations, our retention. This one has a
28194
+ * different authority — the camera's firmware — and per D62 it stores
28195
+ * nothing of its own. Every value here is read from the camera and every
28196
+ * write goes back to the camera; there is no CamStack-side mirror that
28197
+ * could disagree with the device.
28198
+ *
28199
+ * ## One shape, two firmwares
28200
+ *
28201
+ * Measured 2026-09-22 against the live fleet:
28202
+ *
28203
+ * | fact | Hikvision (ISAPI) | Reolink (Baichuan) |
28204
+ * | --- | --- | --- |
28205
+ * | storage | `ContentMgmt/Storage` `<hdd>` rows: status, capacity, freeSpace (MB) | `getHddInfoList` (cmd 102): `mount`, `format`, `capacity` GB + `capacityM` MB remainder |
28206
+ * | tracks | several (101 **and** 103 on both 1436 and 3833), each with its own schedule | one per channel |
28207
+ * | schedule | per track, 7 `ScheduleAction` blocks: DayOfWeek + TimeOfDay range + ONE `ActionRecordingMode` | per trigger type, a 168-char weekly HOUR mask |
28208
+ * | triggers | `CMR`, `MOTION` | `Normal`, `MD`, `people`, `vehicle`, `dog_cat`, `crossline`, `intrude`, `loitering` |
28209
+ * | pre-record | `PreRecordTimeSeconds` | `preRecordTime` |
28210
+ * | post-record | `PostRecordTimeSeconds` | `recordDelayTime` |
28211
+ * | overwrite | per track `LoopEnable` | `cycle`, with `cycleList` enumerating the accepted values |
28212
+ * | segment length | not exposed on V5.7.1 | `packageTime` (minutes) |
28213
+ *
28214
+ * The two schedule models look different and are the same thing in
28215
+ * different coordinates: both answer "for this trigger, during which
28216
+ * weekly windows does the camera record". {@link RecordWindow} is that
28217
+ * question in one shape — Hikvision's ranges map straight onto it,
28218
+ * Reolink's mask expands into hour-aligned windows.
28219
+ *
28220
+ * ## Union, not intersection
28221
+ *
28222
+ * **The same fields exist on every camera.** What differs per device is
28223
+ * which VALUES that device accepts, and that is what {@link
28224
+ * RecordingOnboardOptions} reports — a `{ readable, writable, reason }`
28225
+ * per field plus the schedule's own limits. A control a camera cannot
28226
+ * honour is rendered DISABLED WITH ITS REASON, never missing and never
28227
+ * dead: disabled must not look like broken.
28228
+ *
28229
+ * ## Refusal by name
28230
+ *
28231
+ * A write a camera cannot honour is refused with a sentence the operator
28232
+ * can read — never accepted and dropped. Both providers refuse through
28233
+ * {@link describeOnboardRefusal}, so the vocabulary is one function and
28234
+ * one test, not two hand-written vendor opinions.
28235
+ *
28236
+ * Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
28237
+ * `getOptions` advertises per-camera availability, `getStatus` (auto-
28238
+ * injected from `status`) reports the live values, and a single
28239
+ * `setSettings` mutation applies a partial change. No hand-written
28240
+ * settings-contribution methods.
28241
+ */
28242
+ /**
28243
+ * What makes the camera start recording during a window.
28244
+ *
28245
+ * The union of both vendors' vocabularies. `continuous` is Hikvision's
28246
+ * `CMR` and Reolink's `Normal`; `motion` is `MOTION` / `MD`. The
28247
+ * object-class triggers are Reolink-only today and the smart-event ones
28248
+ * (`lineCrossing`, `intrusion`, `loitering`) are Reolink-only on the
28249
+ * firmwares measured — a camera that cannot record on a trigger simply
28250
+ * does not list it in `options.schedule.triggers`, and a window naming
28251
+ * it is REFUSED, not dropped.
28252
+ */
28253
+ var RecordTriggerSchema = _enum([
28254
+ "continuous",
28255
+ "motion",
28256
+ "person",
28257
+ "vehicle",
28258
+ "animal",
28259
+ "lineCrossing",
28260
+ "intrusion",
28261
+ "loitering",
28262
+ "alarmInput"
28263
+ ]);
28264
+ /**
28265
+ * One weekly recording window: "on `day`, from `startMinute` to
28266
+ * `endMinute`, record on `trigger`".
28267
+ *
28268
+ * `day` is 0 = Monday … 6 = Sunday (ISO order, which is also the order
28269
+ * both firmwares enumerate). Minutes are local camera time since
28270
+ * midnight; `endMinute` may be 1440, meaning end of day — that is
28271
+ * Hikvision's literal `24:00` and Reolink's 24th mask slot, and
28272
+ * collapsing it to 0 would turn a whole-day window into an empty one.
28273
+ */
28274
+ var RecordWindowSchema = object({
28275
+ trigger: RecordTriggerSchema,
28276
+ day: number().int().min(0).max(6),
28277
+ startMinute: number().int().min(0).max(1439),
28278
+ endMinute: number().int().min(1).max(1440)
28279
+ });
28280
+ /** Status of one physical volume, as the camera itself describes it. */
28281
+ var OnboardStorageVolumeSchema = object({
28282
+ /** The camera's own id for the volume (`hdd/id`, Reolink `HddInfo/number`). */
28283
+ id: string(),
28284
+ /** The camera's own name for it, when it gives one (`hddName`). */
28285
+ label: string().optional(),
28286
+ status: _enum([
28287
+ "ok",
28288
+ "unformatted",
28289
+ "error",
28290
+ "offline",
28291
+ "unknown"
28292
+ ]),
28293
+ /**
28294
+ * Total size in MB, or **null when the camera did not say**.
28295
+ *
28296
+ * Never 0 for an unreadable value: a measurement that failed is not a
28297
+ * measurement (D393), and a card whose size is unknown must not be
28298
+ * rendered as a card of size zero.
28299
+ */
28300
+ capacityMb: number().nullable(),
28301
+ /**
28302
+ * Free space in MB, or null when unknown.
28303
+ *
28304
+ * **Not a proxy for "has footage".** Measured 2026-09-22: 1436 and
28305
+ * 1439 both report exactly 11776 MB free — the fixed reserve a looping
28306
+ * card converges on once it has wrapped. At loop steady state the
28307
+ * number is identical whether the camera recorded yesterday or stopped
28308
+ * a month ago.
28309
+ */
28310
+ freeMb: number().nullable(),
28311
+ /** True when the camera reports the volume writable (`property` RW). */
28312
+ writable: boolean().optional()
28313
+ });
28314
+ /**
28315
+ * What the camera is doing with its own storage, right now.
28316
+ *
28317
+ * Every scalar is nullable and **null means the camera did not answer**,
28318
+ * never a default. A form that seeds `0` from an unanswered read invites
28319
+ * the operator to save that 0 back onto the camera.
28320
+ */
28321
+ var RecordingOnboardStatusSchema = object({
28322
+ storage: discriminatedUnion("kind", [
28323
+ object({
28324
+ kind: literal("present"),
28325
+ volumes: array(OnboardStorageVolumeSchema)
28326
+ }),
28327
+ object({
28328
+ kind: literal("absent"),
28329
+ reason: string()
28330
+ }),
28331
+ object({
28332
+ kind: literal("unknown"),
28333
+ reason: string()
28334
+ })
28335
+ ]),
28336
+ tracks: array(object({
28337
+ id: string(),
28338
+ enabled: boolean(),
28339
+ isVideo: boolean(),
28340
+ /** From the camera's own track description. Null when it does not say. */
28341
+ codec: string().nullable(),
28342
+ resolution: string().nullable(),
28343
+ /** Per-track overwrite flag, where the firmware keeps it per track. */
28344
+ overwriteWhenFull: boolean().nullable()
28345
+ })),
28346
+ /**
28347
+ * The track the write path targets — the enabled VIDEO one. Null when
28348
+ * no track could be identified, which is itself a refusal reason.
28349
+ */
28350
+ primaryTrackId: string().nullable(),
28351
+ /** Master "record to the card at all" switch. */
28352
+ enabled: boolean().nullable(),
28353
+ overwriteWhenFull: boolean().nullable(),
28354
+ preRecordSec: number().nullable(),
28355
+ postRecordSec: number().nullable(),
28356
+ /** Length of one recorded file, in minutes. */
28357
+ segmentMinutes: number().nullable(),
28358
+ /** The primary track's weekly windows, flattened. */
28359
+ windows: array(RecordWindowSchema),
28360
+ /**
28361
+ * How many windows the camera described that CamStack could NOT read —
28362
+ * an unrecognised trigger, an unparseable clock, a weekday it does not
28363
+ * name.
28364
+ *
28365
+ * A dropped window is work the reader threw away, and a schedule that
28366
+ * silently shows fewer rows than the camera holds is how an operator
28367
+ * saves back a schedule shorter than the one they were looking at
28368
+ * (D391). Non-zero means the window list is INCOMPLETE and a write
28369
+ * that replaces it would delete what was not shown — which is why a
28370
+ * provider reporting a non-zero count also reports the schedule as not
28371
+ * writable.
28372
+ */
28373
+ unreadableWindows: number(),
28374
+ /**
28375
+ * The camera is scheduled to record and has NO usable storage.
28376
+ *
28377
+ * A first-class fact because it is the fleet's most common silent
28378
+ * defect: measured 2026-09-22, 1441 and 3831 are both motion-recording
28379
+ * to a card that is not there. Neither the schedule nor the storage
28380
+ * read says anything wrong on its own; only the pair does.
28381
+ */
28382
+ recordingToNowhere: boolean(),
28383
+ lastFetchedAt: number()
28384
+ });
28385
+ /** Numeric range descriptor — `{ min, max, step }` per the getOptions convention. */
28386
+ var RangeSchema = object({
28387
+ min: number(),
28388
+ max: number(),
28389
+ step: number()
28390
+ });
28391
+ /**
28392
+ * The values a camera actually takes for a numeric field, when they are a SET
28393
+ * rather than a range.
28394
+ *
28395
+ * `{min,max,step}` cannot say what these two firmwares do. Measured on 1436
28396
+ * (I91DN) on 2026-09-22 by writing each value and reading it back:
28397
+ *
28398
+ * - pre-record: `0, 5, 10, 15, 20, 25, 30` and `2147483647` (INT32_MAX, the
28399
+ * camera's "no limit" — `-1` and `4294967295` both land on it);
28400
+ * - post-record: `5, 10, 30, 60, 120, 300, 600`.
28401
+ *
28402
+ * Neither is expressible as a step: the first has a sentinel two billion away
28403
+ * from its neighbours, the second doubles and then jumps. A range that tried
28404
+ * would forbid values the camera takes AND permit values it silently replaces
28405
+ * with 5 — wrong in both directions at once.
28406
+ *
28407
+ * `sentinel` names the member that is not a duration, so a surface can render
28408
+ * "no limit" instead of `2147483647` seconds.
28409
+ */
28410
+ var AllowedValuesSchema = object({
28411
+ values: array(number()).min(1),
28412
+ sentinel: object({
28413
+ value: number(),
28414
+ meaning: _enum(["no-limit", "disabled"])
28415
+ }).optional()
28416
+ });
28417
+ /**
28418
+ * Per-field availability on ONE camera.
28419
+ *
28420
+ * The field exists on every camera — this says whether this one can be
28421
+ * read and whether it can be written, and `reason` says why not when
28422
+ * either is false. The UI renders the control DISABLED with the reason
28423
+ * rather than hiding it, so a limitation is legible instead of looking
28424
+ * like a missing feature.
28425
+ */
28426
+ var OnboardFieldSupportSchema = object({
28427
+ readable: boolean(),
28428
+ writable: boolean(),
28429
+ /** Required whenever `readable` or `writable` is false. */
28430
+ reason: string().optional()
28431
+ });
28432
+ /** What this camera's schedule model can express. */
28433
+ var OnboardScheduleSupportSchema = object({
28434
+ support: OnboardFieldSupportSchema,
28435
+ /**
28436
+ * The smallest time step the camera can express, in minutes.
28437
+ *
28438
+ * Hikvision takes arbitrary minutes (`00:05:00`–`23:57:00` observed on
28439
+ * 1436's track 103). Reolink's schedule is a 7×24 HOUR mask, so 60. A
28440
+ * window whose edges are not a multiple of this is REFUSED rather than
28441
+ * quietly rounded — rounding is how an operator's 06:30 becomes 06:00
28442
+ * and nothing says so.
28443
+ */
28444
+ granularityMinutes: number(),
28445
+ /** Triggers this camera can record on. A window naming another is refused. */
28446
+ triggers: array(RecordTriggerSchema),
28447
+ /**
28448
+ * False when the camera stores ONE trigger per time range, so two
28449
+ * windows overlapping on the same day cannot carry different triggers.
28450
+ * True on Reolink, whose mask is per-trigger and independent.
28451
+ */
28452
+ supportsOverlappingTriggers: boolean()
28453
+ });
28454
+ var RecordingOnboardOptionsSchema = object({
28455
+ enabled: OnboardFieldSupportSchema,
28456
+ overwriteWhenFull: OnboardFieldSupportSchema,
28457
+ preRecordSec: OnboardFieldSupportSchema,
28458
+ preRecordSecRange: RangeSchema.optional(),
28459
+ /** Preferred over the range when the camera takes a SET, not a span. */
28460
+ preRecordSecAllowed: AllowedValuesSchema.optional(),
28461
+ postRecordSec: OnboardFieldSupportSchema,
28462
+ postRecordSecRange: RangeSchema.optional(),
28463
+ /** Preferred over the range when the camera takes a SET, not a span. */
28464
+ postRecordSecAllowed: AllowedValuesSchema.optional(),
28465
+ segmentMinutes: OnboardFieldSupportSchema,
28466
+ segmentMinutesRange: RangeSchema.optional(),
28467
+ /** Preferred over the range when the camera takes a SET, not a span. */
28468
+ segmentMinutesAllowed: AllowedValuesSchema.optional(),
28469
+ schedule: OnboardScheduleSupportSchema
28470
+ });
28471
+ /**
28472
+ * A partial change. Every field optional.
28473
+ *
28474
+ * Unlike the other `deviceConfig` caps, a provider here does **NOT**
28475
+ * silently ignore a field it cannot support — it refuses, by name,
28476
+ * through {@link describeOnboardRefusal}. Silence on a recording setting
28477
+ * is the failure D62 exists to prevent: the operator believes the camera
28478
+ * is recording the way the form says, and it is not.
28479
+ */
28480
+ var RecordingOnboardPatchSchema = object({
28481
+ enabled: boolean().optional(),
28482
+ overwriteWhenFull: boolean().optional(),
28483
+ preRecordSec: number().optional(),
28484
+ postRecordSec: number().optional(),
28485
+ segmentMinutes: number().optional(),
28486
+ /** The complete new window set for the primary track — not a delta. */
28487
+ windows: array(RecordWindowSchema).optional()
28488
+ });
28489
+ var recordingOnboardCapability = {
28490
+ name: "recording-onboard",
28491
+ scope: "device",
28492
+ deviceNative: true,
28493
+ mode: "singleton",
28494
+ deviceTypes: [DeviceType.Camera],
28495
+ deviceConfig: { ui: {
28496
+ kind: "derived-form",
28497
+ builderId: "recording-onboard",
28498
+ tab: "recording"
28499
+ } },
28500
+ methods: {
28501
+ getOptions: method(object({ deviceId: number() }), RecordingOnboardOptionsSchema),
28502
+ setSettings: method(object({
28503
+ deviceId: number(),
28504
+ settings: RecordingOnboardPatchSchema
28505
+ }), _void(), {
28506
+ kind: "mutation",
28507
+ auth: "admin"
28508
+ })
28509
+ },
28510
+ status: {
28511
+ schema: RecordingOnboardStatusSchema,
28512
+ kind: "poll"
28513
+ },
28514
+ runtimeState: RecordingOnboardStatusSchema,
28515
+ /**
28516
+ * Runtime-state durability: **restored** — operator-set camera-side
28517
+ * recording config; mutation-driven, and the storage half is the last
28518
+ * thing the camera said about its own card.
28519
+ *
28520
+ * See `RuntimeStateDurability`. Enforced by
28521
+ * `scripts/check-runtime-state-durability.ts`.
28522
+ */
28523
+ durability: "restored",
28524
+ /** Clock fields: written, but excluded from the compare that decides
28525
+ * whether persisting is worth a SQLite commit. */
28526
+ volatileStateFields: ["lastFetchedAt"]
28527
+ };
28528
+ /**
28529
+ * Day-of-week names in the cap's index order (0 = Monday), for messages
28530
+ * an operator reads and for Hikvision's `<DayOfWeek>` element.
28531
+ */
28532
+ var DAY_NAMES = [
28533
+ "Monday",
28534
+ "Tuesday",
28535
+ "Wednesday",
28536
+ "Thursday",
28537
+ "Friday",
28538
+ "Saturday",
28539
+ "Sunday"
28540
+ ];
28541
+ /**
28542
+ * Does `patch` ask this camera for something it cannot do?
28543
+ *
28544
+ * Returns the operator-readable reason, or `null` when every field in
28545
+ * the patch is within what `options` says this camera accepts. A
28546
+ * provider calls this BEFORE touching the camera and throws the string:
28547
+ * refusing is the point, and refusing identically on both vendors is why
28548
+ * this is one function.
28549
+ *
28550
+ * The order of checks is the order an operator would read them: the
28551
+ * scalar knobs first, then the schedule, because a schedule complaint is
28552
+ * longer and a scalar one is usually the real problem.
28553
+ */
28554
+ function describeOnboardRefusal(options, patch) {
28555
+ if (patch.enabled !== void 0 && !options.enabled.writable) return unwritable("the master recording switch", options.enabled.reason);
28556
+ if (patch.overwriteWhenFull !== void 0 && !options.overwriteWhenFull.writable) return unwritable("overwrite-when-full", options.overwriteWhenFull.reason);
28557
+ const preRefusal = refuseNumeric("pre-record seconds", patch.preRecordSec, options.preRecordSec, options.preRecordSecRange, options.preRecordSecAllowed);
28558
+ if (preRefusal) return preRefusal;
28559
+ const postRefusal = refuseNumeric("post-record seconds", patch.postRecordSec, options.postRecordSec, options.postRecordSecRange, options.postRecordSecAllowed);
28560
+ if (postRefusal) return postRefusal;
28561
+ const segmentRefusal = refuseNumeric("segment length (minutes)", patch.segmentMinutes, options.segmentMinutes, options.segmentMinutesRange, options.segmentMinutesAllowed);
28562
+ if (segmentRefusal) return segmentRefusal;
28563
+ if (patch.windows !== void 0) {
28564
+ const scheduleRefusal = refuseSchedule(options, patch.windows);
28565
+ if (scheduleRefusal) return scheduleRefusal;
28566
+ }
28567
+ return null;
28568
+ }
28569
+ function unwritable(what, reason) {
28570
+ return reason ? `this camera cannot change ${what}: ${reason}` : `this camera cannot change ${what}`;
28571
+ }
28572
+ function refuseNumeric(what, value, support, range, allowed) {
28573
+ if (value === void 0) return null;
28574
+ if (!support.writable) return unwritable(what, support.reason);
28575
+ if (!Number.isFinite(value)) return `${what} must be a number, got ${String(value)}`;
28576
+ if (allowed) {
28577
+ if (allowed.values.includes(value)) return null;
28578
+ const say = (v) => allowed.sentinel !== void 0 && v === allowed.sentinel.value ? allowed.sentinel.meaning === "no-limit" ? "no limit" : "off" : String(v);
28579
+ return `this camera accepts ${what} only as ${allowed.values.map(say).join(", ")}; ${say(value)} is not one of them`;
28580
+ }
28581
+ if (!range) return null;
28582
+ 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`;
28583
+ 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`;
28584
+ return null;
28585
+ }
28586
+ function refuseSchedule(options, windows) {
28587
+ const schedule = options.schedule;
28588
+ if (!schedule.support.writable) return unwritable("the recording schedule", schedule.support.reason);
28589
+ const allowed = new Set(schedule.triggers);
28590
+ for (const window of windows) {
28591
+ if (!allowed.has(window.trigger)) {
28592
+ const offer = schedule.triggers.length > 0 ? schedule.triggers.join(", ") : "none";
28593
+ return `this camera cannot record on "${window.trigger}"; it records on: ${offer}`;
28594
+ }
28595
+ if (window.endMinute <= window.startMinute) return `a window on ${dayName(window.day)} ends at or before it starts (${String(window.startMinute)} → ${String(window.endMinute)})`;
28596
+ const step = schedule.granularityMinutes;
28597
+ 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`;
28598
+ }
28599
+ if (!schedule.supportsOverlappingTriggers) {
28600
+ const clash = findOverlapWithDifferentTrigger(windows);
28601
+ 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))}`;
28602
+ }
28603
+ return null;
28604
+ }
28605
+ function findOverlapWithDifferentTrigger(windows) {
28606
+ for (let i = 0; i < windows.length; i += 1) for (let j = i + 1; j < windows.length; j += 1) {
28607
+ const a = windows[i];
28608
+ const b = windows[j];
28609
+ if (!a || !b) continue;
28610
+ if (a.day !== b.day) continue;
28611
+ if (a.trigger === b.trigger) continue;
28612
+ if (a.startMinute < b.endMinute && b.startMinute < a.endMinute) return {
28613
+ a,
28614
+ b
28615
+ };
28616
+ }
28617
+ return null;
28618
+ }
28619
+ function dayName(day) {
28620
+ return DAY_NAMES[day] ?? `day ${String(day)}`;
28621
+ }
28622
+ /** `510` → `08:30`. For messages, not for the wire. */
28623
+ function formatMinutes(minute) {
28624
+ const hour = Math.floor(minute / 60);
28625
+ const rest = minute % 60;
28626
+ return `${String(hour).padStart(2, "0")}:${String(rest).padStart(2, "0")}`;
28627
+ }
28628
+ /**
28629
+ * Index of the mask character covering `day` at `hour`.
28630
+ *
28631
+ * **Layout: day-major, Monday first** — `day * 24 + hour`.
28632
+ *
28633
+ * This is an ASSUMPTION, and it is recorded as one because the fleet
28634
+ * cannot currently confirm it: every `valueTable` on every Reolink
28635
+ * measured on 2026-09-22 is uniform (all `1` or all `0`), and a uniform
28636
+ * mask reads identically under either layout. To settle it, set a single
28637
+ * hour on one day from the Reolink app and re-read `getRecordSchedule`.
28638
+ * Until then, a NON-uniform mask is reported as windows under this
28639
+ * layout and the day labels may be transposed; a uniform one is exact.
28640
+ */
28641
+ function weeklyMaskIndex(day, hour) {
28642
+ return day * 24 + hour;
28643
+ }
28644
+ /**
28645
+ * Expand one trigger's weekly hour mask into windows, merging adjacent
28646
+ * set hours into one window per run so a full day is ONE window rather
28647
+ * than twenty-four.
28648
+ *
28649
+ * A mask of the wrong length yields no windows: a truncated table is not
28650
+ * a schedule of fewer days, it is an answer we did not understand.
28651
+ */
28652
+ function weeklyMaskToWindows(mask, trigger) {
28653
+ if (mask.length !== 168) return [];
28654
+ const out = [];
28655
+ for (let day = 0; day < 7; day += 1) {
28656
+ let runStart = null;
28657
+ for (let hour = 0; hour <= 24; hour += 1) {
28658
+ const set = hour < 24 && mask[weeklyMaskIndex(day, hour)] === "1";
28659
+ if (set && runStart === null) runStart = hour;
28660
+ else if (!set && runStart !== null) {
28661
+ out.push({
28662
+ trigger,
28663
+ day,
28664
+ startMinute: runStart * 60,
28665
+ endMinute: hour * 60
28666
+ });
28667
+ runStart = null;
28668
+ }
28669
+ }
28670
+ }
28671
+ return out;
28672
+ }
28673
+ /**
28674
+ * Collapse this trigger's windows back into a weekly hour mask.
28675
+ *
28676
+ * Windows for other triggers are ignored — the mask is per trigger. A
28677
+ * window that is not hour-aligned is REFUSED upstream by
28678
+ * {@link describeOnboardRefusal}; this function rounds nothing, it
28679
+ * simply sets every hour the window fully or partly covers, so a caller
28680
+ * that skipped the refusal cannot silently lose coverage.
28681
+ */
28682
+ function windowsToWeeklyMask(windows, trigger) {
28683
+ const slots = new Array(168).fill("0");
28684
+ for (const window of windows) {
28685
+ if (window.trigger !== trigger) continue;
28686
+ if (window.day < 0 || window.day >= 7) continue;
28687
+ const firstHour = Math.floor(window.startMinute / 60);
28688
+ const lastHour = Math.ceil(window.endMinute / 60);
28689
+ for (let hour = firstHour; hour < lastHour && hour < 24; hour += 1) slots[weeklyMaskIndex(window.day, hour)] = "1";
28690
+ }
28691
+ return slots.join("");
28692
+ }
28693
+ /**
27985
28694
  * Generic device-level status snapshot. Auto-registered by `BaseDevice`
27986
28695
  * for every device, regardless of provider — the kernel needs a uniform
27987
28696
  * cap-keyed slice for the basic device flags every consumer expects to
@@ -36984,6 +37693,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
36984
37693
  pressureSensor: pressureSensorCapability,
36985
37694
  privacyMask: privacyMaskCapability,
36986
37695
  ptzAutotrack: ptzAutotrackCapability,
37696
+ recordingOnboard: recordingOnboardCapability,
36987
37697
  recordingSignal: recordingSignalCapability,
36988
37698
  sceneMonitor: sceneMonitorCapability,
36989
37699
  scriptRunner: scriptRunnerCapability,
@@ -40934,6 +41644,12 @@ Object.freeze({
40934
41644
  addonId: null,
40935
41645
  access: "view"
40936
41646
  },
41647
+ "deviceAdminLink.getAdminLink": {
41648
+ capName: "device-admin-link",
41649
+ capScope: "device",
41650
+ addonId: null,
41651
+ access: "view"
41652
+ },
40937
41653
  "deviceAdoption.adopt": {
40938
41654
  capName: "device-adoption",
40939
41655
  capScope: "system",
@@ -44972,6 +45688,24 @@ Object.freeze({
44972
45688
  addonId: null,
44973
45689
  access: "view"
44974
45690
  },
45691
+ "recordingOnboard.getOptions": {
45692
+ capName: "recording-onboard",
45693
+ capScope: "device",
45694
+ addonId: null,
45695
+ access: "view"
45696
+ },
45697
+ "recordingOnboard.getStatus": {
45698
+ capName: "recording-onboard",
45699
+ capScope: "device",
45700
+ addonId: null,
45701
+ access: "view"
45702
+ },
45703
+ "recordingOnboard.setSettings": {
45704
+ capName: "recording-onboard",
45705
+ capScope: "device",
45706
+ addonId: null,
45707
+ access: "create"
45708
+ },
44975
45709
  "recordingSignal.getStatus": {
44976
45710
  capName: "recording-signal",
44977
45711
  capScope: "device",
@@ -46858,6 +47592,11 @@ Object.freeze({
46858
47592
  form: "single",
46859
47593
  optional: true
46860
47594
  }],
47595
+ "deviceAdminLink.getAdminLink": [{
47596
+ name: "deviceId",
47597
+ form: "single",
47598
+ optional: false
47599
+ }],
46861
47600
  "deviceAdoption.release": [{
46862
47601
  name: "camDeviceId",
46863
47602
  form: "single",
@@ -48243,6 +48982,16 @@ Object.freeze({
48243
48982
  form: "single",
48244
48983
  optional: true
48245
48984
  }],
48985
+ "recordingOnboard.getOptions": [{
48986
+ name: "deviceId",
48987
+ form: "single",
48988
+ optional: false
48989
+ }],
48990
+ "recordingOnboard.setSettings": [{
48991
+ name: "deviceId",
48992
+ form: "single",
48993
+ optional: false
48994
+ }],
48246
48995
  "recordingSignal.getStatus": [{
48247
48996
  name: "deviceId",
48248
48997
  form: "single",
@@ -71802,7 +72551,7 @@ var require_yazl = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
71802
72551
  }
71803
72552
  }));
71804
72553
  //#endregion
71805
- //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-UIQ527B6.js
72554
+ //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-E2N7WTCC.js
71806
72555
  var import_undici = require_undici();
71807
72556
  var import_yazl = /* @__PURE__ */ require_chunk.__toESM(require_yazl(), 1);
71808
72557
  var formatterCache = /* @__PURE__ */ new Map();
@@ -73148,13 +73897,13 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73148
73897
  sec: 59
73149
73898
  };
73150
73899
  }
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)}`);
73900
+ 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
73901
  if (!bypassCache) {
73153
73902
  this.cleanRecordingsCache();
73154
73903
  const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
73155
73904
  const cached = this.recordingsCache.get(cacheKey);
73156
73905
  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`);
73906
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Cache hit: returning ${cached.data.length} cached enriched recordings`);
73158
73907
  return cached.data;
73159
73908
  }
73160
73909
  }
@@ -73206,7 +73955,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73206
73955
  const firstResult = dayResponse[0];
73207
73956
  if (firstResult && firstResult.code === 0 && firstResult.value?.SearchResult?.File) {
73208
73957
  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({
73958
+ 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
73959
  fileCount: files.length,
73211
73960
  sampleFile: {
73212
73961
  name: files[0].name,
@@ -73242,7 +73991,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73242
73991
  } catch (e) {}
73243
73992
  enriched2.push(this.enrichVodFile(vodFile, channel, streamUrl));
73244
73993
  }
73245
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched2.length} enriched recording files from autoSearchByDay`);
73994
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched2.length} enriched recording files from autoSearchByDay`);
73246
73995
  if (!bypassCache) {
73247
73996
  const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
73248
73997
  this.recordingsCache.set(cacheKey, {
@@ -73270,7 +74019,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73270
74019
  const response = await this.call("Search", param, 0);
73271
74020
  allResults.push(...response);
73272
74021
  } else {
73273
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Range spans ${dayRanges.length} days, splitting into separate day queries`);
74022
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Range spans ${dayRanges.length} days, splitting into separate day queries`);
73274
74023
  for (const range of dayRanges) {
73275
74024
  const param = { Search: {
73276
74025
  channel,
@@ -73280,7 +74029,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73280
74029
  EndTime: range.end
73281
74030
  } };
73282
74031
  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})`);
74032
+ 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
74033
  const response = await this.call("Search", param, 0);
73285
74034
  allResults.push(...response);
73286
74035
  }
@@ -73290,7 +74039,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73290
74039
  const firstResult = result[0];
73291
74040
  if (firstResult && firstResult.code === 0 && firstResult.value?.SearchResult?.File) {
73292
74041
  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({
74042
+ if (files.length > 0 && files[0]) require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response (normal search): ${JSON.stringify({
73294
74043
  fileCount: files.length,
73295
74044
  sampleFile: {
73296
74045
  name: files[0].name,
@@ -73307,12 +74056,12 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73307
74056
  const searchResult = res.value?.SearchResult;
73308
74057
  if (searchResult?.File) allFiles.push(...searchResult.File);
73309
74058
  }
73310
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Collected ${allFiles.length} raw VodFiles from API search results`);
74059
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Collected ${allFiles.length} raw VodFiles from API search results`);
73311
74060
  if (allFiles.length > 0) {
73312
74061
  const sampleSize = Math.min(3, allFiles.length);
73313
74062
  for (let i = 0; i < sampleSize; i++) {
73314
74063
  const file = allFiles[i];
73315
- if (file) require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Sample file ${i + 1}/${allFiles.length}: ${JSON.stringify({
74064
+ if (file) require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Sample file ${i + 1}/${allFiles.length}: ${JSON.stringify({
73316
74065
  name: file.name,
73317
74066
  type: file.type,
73318
74067
  size: file.size,
@@ -73335,7 +74084,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73335
74084
  } catch (e) {}
73336
74085
  enriched.push(this.enrichVodFile(vodFile, channel, streamUrl));
73337
74086
  }
73338
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched.length} enriched recording files`);
74087
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched.length} enriched recording files`);
73339
74088
  if (!bypassCache) {
73340
74089
  const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
73341
74090
  this.recordingsCache.set(cacheKey, {
@@ -73372,7 +74121,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73372
74121
  prepare: true,
73373
74122
  seek: seekSeconds
73374
74123
  });
73375
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`);
74124
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`);
73376
74125
  const { spawn: spawn2 } = await import("child_process");
73377
74126
  return new Promise((resolve, reject) => {
73378
74127
  const chunks = [];
@@ -73420,7 +74169,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73420
74169
  reject(/* @__PURE__ */ new Error(`ffmpeg produced too small output: ${jpeg.length} bytes`));
73421
74170
  return;
73422
74171
  }
73423
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Successfully extracted thumbnail: ${jpeg.length} bytes`);
74172
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Successfully extracted thumbnail: ${jpeg.length} bytes`);
73424
74173
  resolve(jpeg);
73425
74174
  } else reject(/* @__PURE__ */ new Error(`ffmpeg exited with code ${code}: ${stderr.slice(-500)}`));
73426
74175
  });
@@ -73469,10 +74218,10 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73469
74218
  }
73470
74219
  } }
73471
74220
  }];
73472
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Request body: ${JSON.stringify(body)}`);
74221
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Request body: ${JSON.stringify(body)}`);
73473
74222
  try {
73474
74223
  const response = await this.callMany(body);
73475
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Response: ${JSON.stringify(response)}`);
74224
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Response: ${JSON.stringify(response)}`);
73476
74225
  const first = response[0];
73477
74226
  if (!first || first.code !== 0 || !first.value?.fileList) {
73478
74227
  const rspCode = first?.error?.rspCode;
@@ -73493,7 +74242,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73493
74242
  if (!filename) throw new Error(`NvrDownload: no files found in response`);
73494
74243
  return filename;
73495
74244
  } catch (error) {
73496
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Error: ${error instanceof Error ? error.message : String(error)}`);
74245
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Error: ${error instanceof Error ? error.message : String(error)}`);
73497
74246
  throw error;
73498
74247
  }
73499
74248
  }
@@ -73689,7 +74438,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73689
74438
  * Convert a VodFile to RecordingFile with parsed metadata.
73690
74439
  */
73691
74440
  enrichVodFile(vodFile, channel, streamUrl) {
73692
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `RAW VodFile from API: ${JSON.stringify({
74441
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `RAW VodFile from API: ${JSON.stringify({
73693
74442
  name: vodFile.name,
73694
74443
  type: vodFile.type,
73695
74444
  size: vodFile.size,
@@ -73742,7 +74491,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73742
74491
  hashBits0to15: hashRevBin.slice(0, 16)
73743
74492
  };
73744
74493
  }
73745
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Parsed filename: ${JSON.stringify({
74494
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Parsed filename: ${JSON.stringify({
73746
74495
  fileName: vodFile.name,
73747
74496
  filenameParts,
73748
74497
  hexValueFromFilename,
@@ -73769,13 +74518,13 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73769
74518
  const hexFlags = parsed?.flags;
73770
74519
  if (parsed?.devType === "hub" && parsed?.version === 4) {
73771
74520
  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.`);
74521
+ 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.`);
74522
+ 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
74523
  }
73775
74524
  }
73776
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Hex flags from filename: ${JSON.stringify(hexFlags)}`);
74525
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Hex flags from filename: ${JSON.stringify(hexFlags)}`);
73777
74526
  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)}`);
74527
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Type flags from recordType "${vodFile.type}": ${JSON.stringify(typeFlags)}`);
73779
74528
  const hasPerson = (hexFlags?.aiPerson ?? false) || typeFlags.hasPerson;
73780
74529
  const hasVehicle = (hexFlags?.aiVehicle ?? false) || typeFlags.hasVehicle;
73781
74530
  const hasAnimal = (hexFlags?.aiAnimal ?? false) || typeFlags.hasAnimal;
@@ -73786,7 +74535,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
73786
74535
  const hasPackage = (hexFlags?.package ?? false) || typeFlags.hasPackage;
73787
74536
  const hasRf = (hexFlags?.rf ?? false) || typeFlags.hasRf;
73788
74537
  const hasOther = (hexFlags?.aiOther ?? false) || typeFlags.hasOther;
73789
- require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Final merged flags: ${JSON.stringify({
74538
+ require_chunk_OSVYEFMM.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Final merged flags: ${JSON.stringify({
73790
74539
  hasPerson,
73791
74540
  hasVehicle,
73792
74541
  hasAnimal,
@@ -74192,8 +74941,8 @@ function nowIsoCompact() {
74192
74941
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
74193
74942
  }
74194
74943
  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");
74944
+ if (videoType === "H264") return require_chunk_OSVYEFMM.splitAnnexBToNalPayloads(accessUnitAnnexB).map((n) => (n[0] ?? 0) & 31);
74945
+ return require_chunk_OSVYEFMM.splitAnnexBToNalPayloads2(accessUnitAnnexB).map((nal) => require_chunk_OSVYEFMM.getH265NalType(nal)).filter((t) => typeof t === "number");
74197
74946
  }
74198
74947
  function normalizeProfiles(p) {
74199
74948
  const out = [];
@@ -74633,7 +75382,7 @@ async function sampleStreams(opts) {
74633
75382
  });
74634
75383
  };
74635
75384
  api.client.on("frame", onFrame);
74636
- const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
75385
+ const videoStream = new require_chunk_OSVYEFMM.BaichuanVideoStream({
74637
75386
  client: api.client,
74638
75387
  api,
74639
75388
  channel,
@@ -75683,7 +76432,7 @@ async function runMultifocalDiagnosticsConsecutively(params) {
75683
76432
  let audioFrames = 0;
75684
76433
  let lastSnapshotAtMs = 0;
75685
76434
  const client = params.api.client;
75686
- const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
76435
+ const videoStream = new require_chunk_OSVYEFMM.BaichuanVideoStream({
75687
76436
  client,
75688
76437
  channel: chNative,
75689
76438
  profile,
@@ -75767,7 +76516,7 @@ async function runMultifocalDiagnosticsConsecutively(params) {
75767
76516
  const baseStreamName = profile === "main" ? "mainStream" : profile === "sub" ? "subStream" : "externStream";
75768
76517
  const headerStreamTypeCandidates = profile === "sub" ? [1, 3] : profile === "main" ? [0, 2] : [0];
75769
76518
  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];
76519
+ const messageClassCandidates = params.probeFull ? [require_chunk_OSVYEFMM.BC_CLASS_MODERN_24, 0] : [require_chunk_OSVYEFMM.BC_CLASS_MODERN_24];
75771
76520
  const extensionXmlCandidates = params.probeFull ? ["", buildChannelExtensionXml(chNative)] : [buildChannelExtensionXml(chNative)];
75772
76521
  const attempts = [];
75773
76522
  for (const messageClass of messageClassCandidates) for (const streamTypeHeader of headerStreamTypeCandidates) for (const extensionXml of extensionXmlCandidates) {
@@ -78288,14 +79037,14 @@ var require_lz4 = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
78288
79037
  };
78289
79038
  }));
78290
79039
  //#endregion
78291
- //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-GNC5U3YA.js
79040
+ //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-IPKY5AWA.js
78292
79041
  var import_fxp = require_fxp();
78293
79042
  var import_lz4 = /* @__PURE__ */ require_chunk.__toESM(require_lz4(), 1);
78294
79043
  function encodeHeader(h) {
78295
- const hasOffset = require_chunk_ZBACPERX.bcHeaderHasPayloadOffset(h.messageClass);
79044
+ const hasOffset = require_chunk_OSVYEFMM.bcHeaderHasPayloadOffset(h.messageClass);
78296
79045
  const headerLen = hasOffset ? 24 : 20;
78297
79046
  const buf = Buffer.alloc(headerLen);
78298
- const magic = h.magic ?? require_chunk_ZBACPERX.BC_MAGIC;
79047
+ const magic = h.magic ?? require_chunk_OSVYEFMM.BC_MAGIC;
78299
79048
  if (magic.length !== 4) throw new Error("magic must be 4 bytes");
78300
79049
  magic.copy(buf, 0);
78301
79050
  buf.writeUInt32LE(h.cmdId >>> 0, 4);
@@ -78311,7 +79060,7 @@ function encodeHeader(h) {
78311
79060
  function decodeHeader(buf) {
78312
79061
  if (buf.length < 20) throw new Error("not enough data for Baichuan header");
78313
79062
  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")}`);
79063
+ 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
79064
  const cmdId = buf.readUInt32LE(4);
78316
79065
  const bodyLen = buf.readUInt32LE(8);
78317
79066
  const channelId = buf.readUInt8(12);
@@ -78319,7 +79068,7 @@ function decodeHeader(buf) {
78319
79068
  const msgNum = buf.readUInt16LE(14);
78320
79069
  const responseCode = buf.readUInt16LE(16);
78321
79070
  const messageClass = buf.readUInt16LE(18);
78322
- const headerLen = require_chunk_ZBACPERX.bcHeaderHasPayloadOffset(messageClass) ? 24 : 20;
79071
+ const headerLen = require_chunk_OSVYEFMM.bcHeaderHasPayloadOffset(messageClass) ? 24 : 20;
78323
79072
  if (buf.length < headerLen) throw new Error("not enough data for Baichuan header (needs 24 bytes)");
78324
79073
  const messageKey = buf.readUInt32LE(12);
78325
79074
  const header = {
@@ -78389,9 +79138,9 @@ var BaichuanFrameParser = class {
78389
79138
  this.needed = 4;
78390
79139
  break;
78391
79140
  }
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);
79141
+ if (!this.buffer.subarray(0, 4).equals(require_chunk_OSVYEFMM.BC_MAGIC) && !this.buffer.subarray(0, 4).equals(require_chunk_OSVYEFMM.BC_MAGIC_REV)) {
79142
+ const idx = this.buffer.indexOf(require_chunk_OSVYEFMM.BC_MAGIC);
79143
+ const idxRev = this.buffer.indexOf(require_chunk_OSVYEFMM.BC_MAGIC_REV);
78395
79144
  const next = idx === -1 ? idxRev : idxRev === -1 ? idx : Math.min(idx, idxRev);
78396
79145
  if (next === -1) {
78397
79146
  this.buffer = this.buffer.subarray(Math.max(0, this.buffer.length - 3));
@@ -80320,6 +81069,17 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
80320
81069
  * Impact: Snapshots are ~0–50ms slower per camera (negligible for users).
80321
81070
  */
80322
81071
  snapshotQueueTail = Promise.resolve();
81072
+ /**
81073
+ * Live cmd 5 replay transfers on this socket.
81074
+ *
81075
+ * Opening a replay SUPERSEDES every earlier one: nothing on the wire ends a
81076
+ * cmd 5 transfer, so an abandoned one keeps delivering (measured: 8 126
81077
+ * further frames over 3 475 ms after the consumer let go) and its successor
81078
+ * used to adopt them. A superseded session now refuses every frame and
81079
+ * counts it.
81080
+ */
81081
+ replaySessions = /* @__PURE__ */ new Set();
81082
+ replaySessionSeq = 0;
80323
81083
  opts;
80324
81084
  debugCfg;
80325
81085
  logger;
@@ -80406,7 +81166,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
80406
81166
  constructor(options) {
80407
81167
  super();
80408
81168
  this.opts = options;
80409
- this.debugCfg = require_chunk_ZBACPERX.normalizeDebugOptions(options.debugOptions);
81169
+ this.debugCfg = require_chunk_OSVYEFMM.normalizeDebugOptions(options.debugOptions);
80410
81170
  this.logger = createDebugGateLogger(options.logger, this.debugCfg.general || this.debugCfg.traceNativeStream || this.debugCfg.traceRecordings || this.debugCfg.traceTalk || this.debugCfg.traceEvents || this.debugCfg.debugRtsp);
80411
81171
  this.on("error", (err) => {
80412
81172
  this.logFixed("error", {
@@ -80888,7 +81648,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
80888
81648
  streamType: 0,
80889
81649
  msgNum,
80890
81650
  responseCode: 0,
80891
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
81651
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
80892
81652
  payloadOffset: 0
80893
81653
  });
80894
81654
  this.logDebug("udp_keepalive_ping_tx", {
@@ -80920,7 +81680,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
80920
81680
  cmdId: 93,
80921
81681
  channel: this.opts.channel ?? 0,
80922
81682
  channelIdOverride: 0,
80923
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
81683
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
80924
81684
  streamType: 0,
80925
81685
  extensionXml: "",
80926
81686
  internal: true
@@ -81259,7 +82019,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81259
82019
  cmdId: 2,
81260
82020
  payloadXml: logoutXml,
81261
82021
  extensionXml: "",
81262
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
82022
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
81263
82023
  channelIdOverride: effectiveHostChannelId,
81264
82024
  timeoutMs: 5e3
81265
82025
  })).header.responseCode;
@@ -81372,12 +82132,12 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81372
82132
  };
81373
82133
  s.frames++;
81374
82134
  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}`);
82135
+ 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
82136
  s.lastLogMs = now;
81377
82137
  s.frames = 0;
81378
82138
  }
81379
82139
  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}`);
82140
+ } 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
82141
  if (this.transport === "udp" && frame.header.cmdId === 234) {
81382
82142
  try {
81383
82143
  const header = encodeHeader({
@@ -81414,7 +82174,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81414
82174
  }
81415
82175
  return;
81416
82176
  }
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}`);
82177
+ 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
82178
  this.emit("frame", frame);
81419
82179
  const key = `${frame.header.cmdId}:${frame.header.msgNum}`;
81420
82180
  const pending = this.pending.get(key);
@@ -81435,7 +82195,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81435
82195
  };
81436
82196
  s.frames++;
81437
82197
  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}`);
82198
+ 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
82199
  s.lastLogMs = now2;
81440
82200
  s.frames = 0;
81441
82201
  }
@@ -81466,7 +82226,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81466
82226
  const eventLogTag = sid ? `BaichuanEvent sid=${sid}` : "BaichuanEvent";
81467
82227
  const events$1 = this.parseEvents(frame);
81468
82228
  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 ?? ""}` : ""));
82229
+ 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
82230
  this.emit("event", event);
81471
82231
  }
81472
82232
  } catch (error) {
@@ -81525,7 +82285,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81525
82285
  if (!xml || !xml.startsWith("<?xml")) return [];
81526
82286
  if (this.debugCfg.traceEvents) {
81527
82287
  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)}`);
82288
+ 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
82289
  }
81530
82290
  const fallbackChannelId = frame.header.channelId;
81531
82291
  const fallbackChannel = fallbackChannelId === 250 ? 0 : Math.max(0, fallbackChannelId - 1);
@@ -81543,7 +82303,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81543
82303
  const timeStampRaw2 = (getXmlText(alarmXml, "timeStamp") ?? getXmlText(alarmXml, "timestamp") ?? "").trim();
81544
82304
  const timeStampNum2 = timeStampRaw2.length ? Number(timeStampRaw2) : void 0;
81545
82305
  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)}`);
82306
+ 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
82307
  const statusTokens2 = status2.toLowerCase().split(",").map((t) => t.trim()).filter((t) => t.length > 0);
81548
82308
  const visitorActive2 = statusTokens2.includes("visitor");
81549
82309
  const dayNightActive2 = statusTokens2.includes("dn") || statusTokens2.includes("daynight");
@@ -81777,6 +82537,85 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81777
82537
  this.alarmEventState.set(fallbackChannel, curr);
81778
82538
  return out;
81779
82539
  }
82540
+ /**
82541
+ * Register a cmd 5 replay transfer and supersede every earlier one on this
82542
+ * socket.
82543
+ */
82544
+ openReplaySession(discriminator) {
82545
+ for (const prev of this.replaySessions) {
82546
+ if (prev.superseded) continue;
82547
+ prev.superseded = true;
82548
+ 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.`);
82549
+ }
82550
+ const session = {
82551
+ id: ++this.replaySessionSeq,
82552
+ discriminator,
82553
+ superseded: false,
82554
+ acceptedFrames: 0,
82555
+ droppedFrames: 0,
82556
+ droppedChannelIds: /* @__PURE__ */ new Set(),
82557
+ reportedDrop: false
82558
+ };
82559
+ this.replaySessions.add(session);
82560
+ return session;
82561
+ }
82562
+ closeReplaySession(session) {
82563
+ this.replaySessions.delete(session);
82564
+ 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(",")}.`);
82565
+ }
82566
+ /**
82567
+ * Count a cmd 5 frame this transfer refused, and say so the first time.
82568
+ *
82569
+ * Silence here is what made the original defect invisible: the successor
82570
+ * transfer simply returned a few extra access units and a short duration,
82571
+ * with nothing anywhere saying a frame had changed hands.
82572
+ */
82573
+ countReplayDrop(session, frame, reason) {
82574
+ session.droppedFrames++;
82575
+ session.droppedChannelIds.add(frame.header.channelId);
82576
+ if (!session.reportedDrop) {
82577
+ session.reportedDrop = true;
82578
+ 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.`);
82579
+ }
82580
+ }
82581
+ /**
82582
+ * Wait until the socket has gone quiet of cmd 5 frames.
82583
+ *
82584
+ * A cmd 7 stop is acknowledged `rc=200` before the camera has finished
82585
+ * sending: four of eleven stopped sessions in the Reolink app's own capture
82586
+ * were still delivering when the ack came back (26–91 further frames). A
82587
+ * caller that opens the next replay the instant the ack lands is racing that
82588
+ * tail; this is how it waits for it instead.
82589
+ *
82590
+ * @returns how many cmd 5 frames arrived during the drain.
82591
+ */
82592
+ async drainReplayFrames(options) {
82593
+ const quietMs = options?.quietMs ?? 300;
82594
+ const maxMs = options?.maxMs ?? 4e3;
82595
+ let drained = 0;
82596
+ await new Promise((resolve) => {
82597
+ let quiet;
82598
+ const hard = setTimeout(() => settle(), maxMs);
82599
+ const onFrame = (frame) => {
82600
+ if (frame.header.cmdId !== 5) return;
82601
+ drained++;
82602
+ if (quiet) clearTimeout(quiet);
82603
+ quiet = setTimeout(() => settle(), quietMs);
82604
+ };
82605
+ let settled = false;
82606
+ const settle = () => {
82607
+ if (settled) return;
82608
+ settled = true;
82609
+ clearTimeout(hard);
82610
+ if (quiet) clearTimeout(quiet);
82611
+ this.off("frame", onFrame);
82612
+ resolve();
82613
+ };
82614
+ this.on("frame", onFrame);
82615
+ quiet = setTimeout(() => settle(), quietMs);
82616
+ });
82617
+ return drained;
82618
+ }
81780
82619
  nextMsgNum() {
81781
82620
  this.msgNum = this.msgNum + 1 & 65535;
81782
82621
  return this.msgNum;
@@ -81817,15 +82656,15 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81817
82656
  const extBuf = Buffer.from(extXml, "utf8");
81818
82657
  const payloadBuf = Buffer.from(payloadXml, "utf8");
81819
82658
  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)]);
82659
+ if (enc.kind === "bc") return Buffer.concat([require_chunk_OSVYEFMM.bcEncrypt(extBuf, channelId), require_chunk_OSVYEFMM.bcEncrypt(payloadBuf, channelId)]);
82660
+ 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
82661
  return Buffer.concat([extBuf, payloadBuf]);
81823
82662
  }
81824
82663
  encodeBodyBinary(extXml, payload, channelId, enc) {
81825
82664
  const extBuf = Buffer.from(extXml, "utf8");
81826
82665
  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]);
82666
+ if (enc.kind === "bc") return Buffer.concat([require_chunk_OSVYEFMM.bcEncrypt(extBuf, channelId), payload]);
82667
+ if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_OSVYEFMM.aesEncrypt(extBuf, enc.key), payload]);
81829
82668
  return Buffer.concat([extBuf, payload]);
81830
82669
  }
81831
82670
  /**
@@ -81870,7 +82709,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81870
82709
  bodyLen,
81871
82710
  binaryPayload: true
81872
82711
  });
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}`);
82712
+ 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
82713
  this.recordTx({
81875
82714
  cmdId,
81876
82715
  responseCode: 0,
@@ -81887,8 +82726,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81887
82726
  let dec;
81888
82727
  try {
81889
82728
  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);
82729
+ else if (enc.kind === "bc") dec = require_chunk_OSVYEFMM.bcDecrypt(buf, channelId);
82730
+ else dec = require_chunk_OSVYEFMM.aesDecrypt(buf, enc.key);
81892
82731
  } catch {
81893
82732
  return;
81894
82733
  }
@@ -81954,8 +82793,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81954
82793
  messageClass,
81955
82794
  bodyLen
81956
82795
  });
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}`);
82796
+ 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}`);
82797
+ 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
82798
  this.recordTx({
81960
82799
  cmdId,
81961
82800
  responseCode: 0,
@@ -81970,8 +82809,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
81970
82809
  responseCode: frame.header.responseCode,
81971
82810
  msgNum: frame.header.msgNum
81972
82811
  });
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}`);
82812
+ 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}`);
82813
+ 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
82814
  if (frame.header.responseCode === 400) {
81976
82815
  if (frame.body.length === 0) throw new Error("Baichuan authentication failed (responseCode 400, empty body) - check username/password");
81977
82816
  }
@@ -82041,9 +82880,9 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82041
82880
  messageClass,
82042
82881
  bodyLen
82043
82882
  });
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}`);
82883
+ 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}`);
82884
+ 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}`);
82885
+ 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
82886
  this.recordTx({
82048
82887
  cmdId,
82049
82888
  responseCode: 0,
@@ -82058,9 +82897,9 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82058
82897
  responseCode: frame.header.responseCode,
82059
82898
  msgNum: frame.header.msgNum
82060
82899
  });
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}`);
82900
+ 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}`);
82901
+ 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}`);
82902
+ 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
82903
  if (!internal) this.kickIdleDisconnectTimer();
82065
82904
  return frame;
82066
82905
  }
@@ -82330,6 +83169,13 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82330
83169
  params.channel ?? this.opts.channel;
82331
83170
  const sessionCounter = this.nextMsgNum();
82332
83171
  const channelId = params.channelIdOverride ?? sessionCounter;
83172
+ const discriminator = params.channelIdOverride == null ? {
83173
+ kind: "minted",
83174
+ channelId
83175
+ } : {
83176
+ kind: "pinned",
83177
+ channelId
83178
+ };
82333
83179
  const msgNum = params.msgNumOverride ?? 0;
82334
83180
  const cmdId = params.cmdId;
82335
83181
  const extXml = params.extensionXml ?? "";
@@ -82355,9 +83201,10 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82355
83201
  const idleTimeoutMs = params.idleTimeoutMs ?? 2e3;
82356
83202
  const chunks = [];
82357
83203
  let streamMsgNum;
82358
- let lockedChannelId;
83204
+ let lockedChannelId = discriminator.kind === "minted" ? discriminator.channelId : void 0;
82359
83205
  let lockedStreamType;
82360
83206
  let idleTimer;
83207
+ const session = this.openReplaySession(discriminator);
82361
83208
  const looksLikeXml = (buf) => {
82362
83209
  let i = 0;
82363
83210
  while (i < buf.length && (buf[i] === 0 || buf[i] === 9 || buf[i] === 10 || buf[i] === 13 || buf[i] === 32)) i++;
@@ -82407,6 +83254,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82407
83254
  this.off("frame", onFrame);
82408
83255
  if (timeout) clearTimeout(timeout);
82409
83256
  if (idleTimer) clearTimeout(idleTimer);
83257
+ this.closeReplaySession(session);
82410
83258
  };
82411
83259
  const finish = (buf) => {
82412
83260
  if (done) return;
@@ -82428,8 +83276,15 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82428
83276
  };
82429
83277
  const onFrame = (frame) => {
82430
83278
  if (frame.header.cmdId !== cmdId) return;
83279
+ if (session.superseded) {
83280
+ this.countReplayDrop(session, frame, "superseded");
83281
+ return;
83282
+ }
82431
83283
  if (lockedStreamType !== void 0 && frame.header.streamType !== lockedStreamType) return;
82432
- if (lockedChannelId !== void 0 && frame.header.channelId !== lockedChannelId) return;
83284
+ if (lockedChannelId !== void 0 && frame.header.channelId !== lockedChannelId) {
83285
+ this.countReplayDrop(session, frame, "foreign-channel");
83286
+ return;
83287
+ }
82433
83288
  if (streamMsgNum !== void 0 && frame.header.msgNum !== streamMsgNum) return;
82434
83289
  const rc = frame.header.responseCode;
82435
83290
  if (rc >= 400 && rc < 6e4 && frame.header.msgNum === msgNum) {
@@ -82453,6 +83308,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82453
83308
  lockedChannelId = frame.header.channelId;
82454
83309
  lockedStreamType = frame.header.streamType;
82455
83310
  }
83311
+ session.acceptedFrames++;
82456
83312
  params.onChunk?.(decrypted);
82457
83313
  chunks.push(decrypted);
82458
83314
  armIdleFinish();
@@ -82466,7 +83322,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82466
83322
  finish(Buffer.concat(chunks));
82467
83323
  return;
82468
83324
  }
82469
- fail(/* @__PURE__ */ new Error(`Baichuan timeout waiting FileInfoList replay binary chunks cmdId=${cmdId} channelId=${channelId} streamType=${expectedStreamType}`));
83325
+ const seen = session.droppedFrames > 0 ? ` (rejected ${session.droppedFrames} cmd ${cmdId} frame(s) on channelId ${[...session.droppedChannelIds].join(",")})` : "";
83326
+ fail(/* @__PURE__ */ new Error(`Baichuan timeout waiting FileInfoList replay binary chunks cmdId=${cmdId} channelId=${channelId} streamType=${expectedStreamType}${seen}`));
82470
83327
  }, timeoutMs);
82471
83328
  this.on("frame", onFrame);
82472
83329
  try {
@@ -82883,8 +83740,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82883
83740
  const tryAs = (enc) => {
82884
83741
  try {
82885
83742
  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);
83743
+ if (enc.kind === "bc") return require_chunk_OSVYEFMM.bcDecrypt(buf, channelId);
83744
+ if (enc.kind === "aes" || enc.kind === "full_aes") return require_chunk_OSVYEFMM.aesDecrypt(buf, enc.key);
82888
83745
  return null;
82889
83746
  } catch {
82890
83747
  return null;
@@ -82914,7 +83771,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82914
83771
  streamType: 0,
82915
83772
  msgNum,
82916
83773
  responseCode: encByte,
82917
- messageClass: require_chunk_ZBACPERX.BC_CLASS_LEGACY
83774
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_LEGACY
82918
83775
  });
82919
83776
  const pendingKey = `${cmdId}:${msgNum}`;
82920
83777
  const framePromise = new Promise((resolve, reject) => {
@@ -82946,15 +83803,15 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82946
83803
  else if (encType === 1) this.enc = { kind: "bc" };
82947
83804
  else if (encType === 2) this.enc = {
82948
83805
  kind: "aes",
82949
- key: require_chunk_ZBACPERX.deriveAesKey(nonce, this.opts.password)
83806
+ key: require_chunk_OSVYEFMM.deriveAesKey(nonce, this.opts.password)
82950
83807
  };
82951
83808
  else if (encType === 18) this.enc = {
82952
83809
  kind: "full_aes",
82953
- key: require_chunk_ZBACPERX.deriveAesKey(nonce, this.opts.password)
83810
+ key: require_chunk_OSVYEFMM.deriveAesKey(nonce, this.opts.password)
82954
83811
  };
82955
83812
  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}`);
83813
+ const userHash = require_chunk_OSVYEFMM.md5StrModern(`${this.opts.username}${nonce}`);
83814
+ const passHash = require_chunk_OSVYEFMM.md5StrModern(`${this.opts.password}${nonce}`);
82958
83815
  const loginXml = buildLoginXml(userHash, passHash);
82959
83816
  this.logDebug("login_hash", {
82960
83817
  username: this.opts.username,
@@ -82969,7 +83826,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
82969
83826
  cmdId: 1,
82970
83827
  payloadXml: loginXml,
82971
83828
  extensionXml: "",
82972
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
83829
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
82973
83830
  channelIdOverride: effectiveHostChannelId,
82974
83831
  encryption: loginEnc,
82975
83832
  timeoutMs: 1e4
@@ -83029,7 +83886,7 @@ async function* createNativeStream(api, channel, profile, options) {
83029
83886
  client = api.client;
83030
83887
  }
83031
83888
  }
83032
- const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
83889
+ const videoStream = new require_chunk_OSVYEFMM.BaichuanVideoStream({
83033
83890
  client,
83034
83891
  api,
83035
83892
  channel,
@@ -83547,7 +84404,7 @@ var H264Flow = class extends BaseFlow {
83547
84404
  super(transport, "H264");
83548
84405
  }
83549
84406
  extractParameterSets(accessUnitAnnexB) {
83550
- const nals = require_chunk_ZBACPERX.splitAnnexBToNalPayloads(accessUnitAnnexB);
84407
+ const nals = require_chunk_OSVYEFMM.splitAnnexBToNalPayloads(accessUnitAnnexB);
83551
84408
  for (const nal of nals) {
83552
84409
  if (nal.length < 1) continue;
83553
84410
  const nalType = (nal[0] ?? 0) & 31;
@@ -83592,9 +84449,9 @@ var H265Flow = class extends BaseFlow {
83592
84449
  super(transport, "H265");
83593
84450
  }
83594
84451
  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);
84452
+ const vps = require_chunk_OSVYEFMM.extractVpsFromAnnexB(accessUnitAnnexB);
84453
+ const sps = require_chunk_OSVYEFMM.extractSpsFromAnnexB(accessUnitAnnexB);
84454
+ const pps = require_chunk_OSVYEFMM.extractPpsFromAnnexB(accessUnitAnnexB);
83598
84455
  if (vps && !this.vps) this.vps = vps;
83599
84456
  if (sps && !this.sps) this.sps = sps;
83600
84457
  if (pps && !this.pps) this.pps = pps;
@@ -83978,8 +84835,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
83978
84835
  /** Returns true if the raw (packed/Annex B) frame is an IDR (H.264) or IRAP (H.265). */
83979
84836
  isRawFrameKeyframe(frame) {
83980
84837
  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));
84838
+ if (frame.videoType === "H264") return _BaichuanRtspServer.splitAnnexBNals(require_chunk_OSVYEFMM.convertToAnnexB(frame.data)).some((n) => n.length >= 1 && (n[0] & 31) === 5);
84839
+ 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
84840
  } catch {}
83984
84841
  return false;
83985
84842
  }
@@ -84879,13 +85736,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
84879
85736
  if (resources?.rtpAudioTimestamp !== void 0) resources.rtpAudioTimestamp = resources.rtpAudioTimestamp + 1024 >>> 0;
84880
85737
  };
84881
85738
  const isH265IrapAccessUnit = (annexB) => {
84882
- const nals = require_chunk_ZBACPERX.splitAnnexBToNalPayloads2(annexB);
85739
+ const nals = require_chunk_OSVYEFMM.splitAnnexBToNalPayloads2(annexB);
84883
85740
  for (const nal of nals) {
84884
85741
  if (nal.length < 2) continue;
84885
85742
  const b0 = nal[0];
84886
85743
  if (b0 === void 0) continue;
84887
85744
  if ((b0 & 128) !== 0) continue;
84888
- if (require_chunk_ZBACPERX.isH265Irap(b0 >> 1 & 63)) return true;
85745
+ if (require_chunk_OSVYEFMM.isH265Irap(b0 >> 1 & 63)) return true;
84889
85746
  }
84890
85747
  return false;
84891
85748
  };
@@ -85041,7 +85898,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
85041
85898
  continue;
85042
85899
  }
85043
85900
  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);
85901
+ const normalizedVideoData = frame.videoType === "H264" ? require_chunk_OSVYEFMM.convertToAnnexB(frame.data) : require_chunk_OSVYEFMM.convertToAnnexB2(frame.data);
85045
85902
  if (frameCount === 0) this.setFlowVideoType(frame.videoType, "first video frame");
85046
85903
  if (!this.flow.getFmtp().hasParamSets) {
85047
85904
  this.flow.extractParameterSets(normalizedVideoData);
@@ -85057,7 +85914,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
85057
85914
  }
85058
85915
  if (useDirectRtp) {
85059
85916
  const videoType = frame.videoType ?? this.flow.videoType;
85060
- const normalizedVideoData = videoType === "H264" ? require_chunk_ZBACPERX.convertToAnnexB(frame.data) : require_chunk_ZBACPERX.convertToAnnexB2(frame.data);
85917
+ const normalizedVideoData = videoType === "H264" ? require_chunk_OSVYEFMM.convertToAnnexB(frame.data) : require_chunk_OSVYEFMM.convertToAnnexB2(frame.data);
85061
85918
  if (!resources?.seenFirstVideoKeyframe) if (videoType === "H265") {
85062
85919
  const { hasParamSets } = this.flow.getFmtp();
85063
85920
  if (!hasParamSets) {
@@ -85133,7 +85990,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
85133
85990
  const headHex = frame.data.subarray(0, 16).toString("hex");
85134
85991
  this.rtspDebugLog(`First video frame written to ffmpeg stdin for client ${clientId} (len=${frame.data.length}, head=${headHex})`);
85135
85992
  }
85136
- if (!stdin.write(frame.videoType === "H264" ? require_chunk_ZBACPERX.convertToAnnexB(frame.data) : frame.data)) await new Promise((resolve) => {
85993
+ if (!stdin.write(frame.videoType === "H264" ? require_chunk_OSVYEFMM.convertToAnnexB(frame.data) : frame.data)) await new Promise((resolve) => {
85137
85994
  if (stdin) stdin.once("drain", () => resolve());
85138
85995
  else resolve();
85139
85996
  });
@@ -85245,7 +86102,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
85245
86102
  ensureContinuousStream(dedicatedClient) {
85246
86103
  if (this.continuousStream) return this.continuousStream;
85247
86104
  const createLiveStream = async () => {
85248
- return new require_chunk_ZBACPERX.BaichuanVideoStream({
86105
+ return new require_chunk_OSVYEFMM.BaichuanVideoStream({
85249
86106
  client: dedicatedClient ?? this.api.client,
85250
86107
  api: this.api,
85251
86108
  channel: this.channel,
@@ -85902,6 +86759,433 @@ var MpegTsMuxer = class {
85902
86759
  this.tablesSent = false;
85903
86760
  }
85904
86761
  };
86762
+ var buildDetectionClasses = (parsed, recordType) => {
86763
+ const classes = [];
86764
+ const flags = parsed?.flags;
86765
+ if (flags) {
86766
+ if (flags.aiPerson) classes.push("person");
86767
+ if (flags.aiVehicle) classes.push("vehicle");
86768
+ if (flags.aiAnimal) classes.push("animal");
86769
+ if (flags.aiFace) classes.push("face");
86770
+ if (flags.motion) classes.push("motion");
86771
+ if (flags.schedule) classes.push("schedule");
86772
+ if (flags.doorbell) classes.push("doorbell");
86773
+ if (flags.package) classes.push("package");
86774
+ if (flags.rf) classes.push("rf");
86775
+ if (flags.aiOther) classes.push("other");
86776
+ }
86777
+ if (classes.length === 0 && recordType) {
86778
+ const rt = recordType.toLowerCase();
86779
+ if (rt.includes("people") || rt.includes("person")) classes.push("person");
86780
+ if (rt.includes("vehicle")) classes.push("vehicle");
86781
+ if (rt.includes("dog_cat") || rt.includes("animal")) classes.push("animal");
86782
+ if (rt.includes("face")) classes.push("face");
86783
+ if (rt.includes("md") || rt.includes("motion")) classes.push("motion");
86784
+ if (rt.includes("sched")) classes.push("schedule");
86785
+ if (rt.includes("visitor") || rt.includes("doorbell")) classes.push("doorbell");
86786
+ if (rt.includes("package")) classes.push("package");
86787
+ if (rt.includes("rf")) classes.push("rf");
86788
+ }
86789
+ if (classes.length === 0) classes.push("motion");
86790
+ return classes;
86791
+ };
86792
+ var getXmlTexts = (xml, tags) => {
86793
+ const out = {};
86794
+ for (const tag2 of tags) {
86795
+ const v = getXmlText(xml, tag2);
86796
+ if (v !== void 0) out[tag2] = v;
86797
+ }
86798
+ return out;
86799
+ };
86800
+ var getXmlBlocks = (xml, tagName) => {
86801
+ const re = new RegExp(`<${tagName}\\b[^>]*>([\\s\\S]*?)<\\/${tagName}>`, "g");
86802
+ const out = [];
86803
+ let match;
86804
+ while (match = re.exec(xml)) out.push(match[1] ?? "");
86805
+ return out;
86806
+ };
86807
+ var parseXmlDateTimeBlock = (block, timeZone) => {
86808
+ const year = Number.parseInt(getXmlText(block, "year") ?? "", 10);
86809
+ const month = Number.parseInt(getXmlText(block, "month") ?? "", 10);
86810
+ const day = Number.parseInt(getXmlText(block, "day") ?? "", 10);
86811
+ const hour = Number.parseInt(getXmlText(block, "hour") ?? "", 10);
86812
+ const minute = Number.parseInt(getXmlText(block, "minute") ?? "", 10);
86813
+ const second = Number.parseInt(getXmlText(block, "second") ?? "", 10);
86814
+ if ([
86815
+ year,
86816
+ month,
86817
+ day,
86818
+ hour,
86819
+ minute,
86820
+ second
86821
+ ].every(Number.isFinite)) return dateFromWallClock({
86822
+ year,
86823
+ month,
86824
+ day,
86825
+ hour,
86826
+ minute,
86827
+ second
86828
+ }, timeZone);
86829
+ 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}))?)?/);
86830
+ if (!m) return void 0;
86831
+ const y = Number.parseInt(m[1] ?? "", 10);
86832
+ const mo = Number.parseInt(m[2] ?? "", 10);
86833
+ const da = Number.parseInt(m[3] ?? "", 10);
86834
+ const ho = Number.parseInt(m[4] ?? "0", 10);
86835
+ const mi = Number.parseInt(m[5] ?? "0", 10);
86836
+ const se = Number.parseInt(m[6] ?? "0", 10);
86837
+ if (![
86838
+ y,
86839
+ mo,
86840
+ da,
86841
+ ho,
86842
+ mi,
86843
+ se
86844
+ ].every(Number.isFinite)) return void 0;
86845
+ return dateFromWallClock({
86846
+ year: y,
86847
+ month: mo,
86848
+ day: da,
86849
+ hour: ho,
86850
+ minute: mi,
86851
+ second: se
86852
+ }, timeZone);
86853
+ };
86854
+ var parseRecordingFilesFromXml = (xml, options) => {
86855
+ const timeZone = options?.timeZone;
86856
+ const fileNameOptions = timeZone !== void 0 ? { timeZone } : void 0;
86857
+ const out = [];
86858
+ const fileInfoBlocks = getXmlBlocks(xml, "FileInfo");
86859
+ for (const b of fileInfoBlocks) {
86860
+ const id = getXmlText(b, "Id") ?? getXmlText(b, "ID") ?? getXmlText(b, "id");
86861
+ const name = getXmlText(b, "name") ?? getXmlText(b, "fileName");
86862
+ const chosen = (id ?? name)?.trim();
86863
+ if (!chosen) continue;
86864
+ const item = { fileName: chosen };
86865
+ if (name != null && name.trim()) item.name = name.trim();
86866
+ if (id != null && id.trim()) item.id = id.trim();
86867
+ const recordType = getXmlText(b, "type") ?? getXmlText(b, "recordType") ?? getXmlText(b, "alarmType");
86868
+ if (recordType != null) item.recordType = recordType;
86869
+ const sizeText = getXmlText(b, "size") ?? getXmlText(b, "fileSize");
86870
+ const sizeBytes = sizeText ? Number.parseInt(sizeText, 10) : void 0;
86871
+ if (sizeBytes != null && Number.isFinite(sizeBytes)) item.sizeBytes = sizeBytes;
86872
+ const start = getXmlBlocks(b, "startTime")[0];
86873
+ const end = getXmlBlocks(b, "endTime")[0];
86874
+ const startDt = start ? parseXmlDateTimeBlock(start, timeZone) : void 0;
86875
+ const endDt = end ? parseXmlDateTimeBlock(end, timeZone) : void 0;
86876
+ if (startDt) item.startTime = startDt;
86877
+ if (endDt) item.endTime = endDt;
86878
+ const parsed = parseRecordingFileName(item.name ?? item.fileName, fileNameOptions);
86879
+ const parsedFromPath = item.fileName !== item.name ? parseRecordingFileName(item.fileName, fileNameOptions) : void 0;
86880
+ const bestParsed = parsedFromPath?.sizeBytes != null ? parsedFromPath : parsed;
86881
+ if (bestParsed) {
86882
+ item.parsedFileName = bestParsed;
86883
+ if (!item.startTime) item.startTime = bestParsed.start;
86884
+ if (!item.endTime) item.endTime = bestParsed.end;
86885
+ if (item.sizeBytes == null && bestParsed.sizeBytes != null) item.sizeBytes = bestParsed.sizeBytes;
86886
+ }
86887
+ item.detectionClasses = buildDetectionClasses(parsed, item.recordType);
86888
+ out.push(item);
86889
+ }
86890
+ const fileBlocks = getXmlBlocks(xml, "File");
86891
+ for (const b of fileBlocks) {
86892
+ const fileName = (getXmlText(b, "fileName") ?? getXmlText(b, "name"))?.trim();
86893
+ if (!fileName) continue;
86894
+ const sizeText = getXmlText(b, "size") ?? getXmlText(b, "fileSize");
86895
+ const sizeBytes = sizeText ? Number.parseInt(sizeText, 10) : void 0;
86896
+ const recordType = getXmlText(b, "type") ?? getXmlText(b, "recordType") ?? getXmlText(b, "alarmType");
86897
+ const start = getXmlBlocks(b, "startTime")[0];
86898
+ const end = getXmlBlocks(b, "endTime")[0];
86899
+ const item = { fileName };
86900
+ if (sizeBytes != null && Number.isFinite(sizeBytes)) item.sizeBytes = sizeBytes;
86901
+ if (recordType != null) item.recordType = recordType;
86902
+ const startDt = start ? parseXmlDateTimeBlock(start, timeZone) : void 0;
86903
+ const endDt = end ? parseXmlDateTimeBlock(end, timeZone) : void 0;
86904
+ if (startDt) item.startTime = startDt;
86905
+ if (endDt) item.endTime = endDt;
86906
+ const parsed = parseRecordingFileName(item.fileName, fileNameOptions);
86907
+ if (parsed) {
86908
+ item.parsedFileName = parsed;
86909
+ if (!item.startTime) item.startTime = parsed.start;
86910
+ if (!item.endTime) item.endTime = parsed.end;
86911
+ if (item.sizeBytes == null && parsed.sizeBytes != null) item.sizeBytes = parsed.sizeBytes;
86912
+ }
86913
+ item.detectionClasses = buildDetectionClasses(parsed, item.recordType);
86914
+ out.push(item);
86915
+ }
86916
+ if (out.length === 0) {
86917
+ const re = /<fileName>([\s\S]*?)<\/fileName>/g;
86918
+ const seenNames = /* @__PURE__ */ new Set();
86919
+ let match;
86920
+ while (match = re.exec(xml)) {
86921
+ const fileName = (match[1] ?? "").trim();
86922
+ if (!fileName) continue;
86923
+ if (seenNames.has(fileName)) continue;
86924
+ seenNames.add(fileName);
86925
+ const item = { fileName };
86926
+ const parsed = parseRecordingFileName(fileName, fileNameOptions);
86927
+ if (parsed) {
86928
+ item.parsedFileName = parsed;
86929
+ item.startTime = parsed.start;
86930
+ item.endTime = parsed.end;
86931
+ }
86932
+ item.detectionClasses = buildDetectionClasses(parsed, void 0);
86933
+ out.push(item);
86934
+ }
86935
+ }
86936
+ const alarmBlocks = getXmlBlocks(xml, "alarmVideo");
86937
+ if (alarmBlocks.length > 0) {
86938
+ const byName = /* @__PURE__ */ new Map();
86939
+ for (const existing of out) {
86940
+ const key = existing.fileName.trim();
86941
+ if (!key) continue;
86942
+ if (!byName.has(key)) byName.set(key, existing);
86943
+ }
86944
+ for (const b of alarmBlocks) {
86945
+ const fileName = (getXmlText(b, "fileName") ?? getXmlText(b, "name"))?.trim();
86946
+ if (!fileName) continue;
86947
+ 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();
86948
+ const alarmType = getXmlText(b, "alarmType")?.trim();
86949
+ const start = getXmlBlocks(b, "startTime")[0];
86950
+ const end = getXmlBlocks(b, "endTime")[0];
86951
+ const startDt = start ? parseXmlDateTimeBlock(start, timeZone) : void 0;
86952
+ const endDt = end ? parseXmlDateTimeBlock(end, timeZone) : void 0;
86953
+ const target = byName.get(fileName) ?? { fileName };
86954
+ if (alarmId && !target.id) target.id = alarmId;
86955
+ if (alarmType) target.recordType = alarmType;
86956
+ if (startDt) target.startTime = startDt;
86957
+ if (endDt) target.endTime = endDt;
86958
+ if (!target.parsedFileName) {
86959
+ const parsed = parseRecordingFileName(target.fileName, fileNameOptions);
86960
+ if (parsed) {
86961
+ target.parsedFileName = parsed;
86962
+ if (!target.startTime) target.startTime = parsed.start;
86963
+ if (!target.endTime) target.endTime = parsed.end;
86964
+ }
86965
+ }
86966
+ if (!byName.has(fileName)) {
86967
+ out.push(target);
86968
+ byName.set(fileName, target);
86969
+ }
86970
+ }
86971
+ }
86972
+ const seen = /* @__PURE__ */ new Set();
86973
+ return out.filter((f) => {
86974
+ const key = f.fileName.trim();
86975
+ if (seen.has(key)) return false;
86976
+ seen.add(key);
86977
+ return true;
86978
+ });
86979
+ };
86980
+ function parseTalkAudioConfig(block) {
86981
+ const audioType = getXmlText(block, "audioType");
86982
+ const sampleRate = Number.parseInt(getXmlText(block, "sampleRate") ?? "", 10);
86983
+ const samplePrecision = Number.parseInt(getXmlText(block, "samplePrecision") ?? "", 10);
86984
+ const lengthPerEncoder = Number.parseInt(getXmlText(block, "lengthPerEncoder") ?? "", 10);
86985
+ const soundTrack = getXmlText(block, "soundTrack");
86986
+ const priorityText = getXmlText(block, "priority");
86987
+ if (!audioType || !Number.isFinite(sampleRate) || !Number.isFinite(samplePrecision) || !Number.isFinite(lengthPerEncoder) || !soundTrack) return null;
86988
+ const config = {
86989
+ audioType,
86990
+ sampleRate,
86991
+ samplePrecision,
86992
+ lengthPerEncoder,
86993
+ soundTrack
86994
+ };
86995
+ if (priorityText !== void 0) {
86996
+ const pr = Number.parseInt(priorityText, 10);
86997
+ if (Number.isFinite(pr)) config.priority = pr;
86998
+ }
86999
+ return config;
87000
+ }
87001
+ var parseTalkAbilityXml = (xml) => {
87002
+ const talkAbilityBlock = getXmlBlocks(xml, "TalkAbility")[0];
87003
+ if (!talkAbilityBlock) throw new Error("TalkAbility XML not found in response");
87004
+ return {
87005
+ duplexList: getXmlBlocks(talkAbilityBlock, "duplexList").map((b) => getXmlText(b, "duplex")).filter((v) => Boolean(v)),
87006
+ audioStreamModeList: getXmlBlocks(talkAbilityBlock, "audioStreamModeList").map((b) => getXmlText(b, "audioStreamMode")).filter((v) => Boolean(v)),
87007
+ audioConfigList: getXmlBlocks(talkAbilityBlock, "audioConfig").map(parseTalkAudioConfig).filter((v) => Boolean(v))
87008
+ };
87009
+ };
87010
+ function parseNumber(v) {
87011
+ if (v == null) return void 0;
87012
+ const n = Number(v);
87013
+ return Number.isFinite(n) ? n : void 0;
87014
+ }
87015
+ function parseBoolean01(v) {
87016
+ if (v == null) return void 0;
87017
+ const s = v.trim();
87018
+ if (s === "1" || s.toLowerCase() === "true") return true;
87019
+ if (s === "0" || s.toLowerCase() === "false") return false;
87020
+ }
87021
+ var BYTES_PER_MB = 1024 * 1024;
87022
+ var num = (xml, tag2) => {
87023
+ const value = parseNumber(getXmlText(xml, tag2));
87024
+ return value === void 0 ? null : value;
87025
+ };
87026
+ var megabytes$1 = (bytes, whole, remainder) => {
87027
+ if (bytes !== null) return bytes / BYTES_PER_MB;
87028
+ if (whole === null && remainder === null) return null;
87029
+ return (whole ?? 0) * 1024 + (remainder ?? 0);
87030
+ };
87031
+ var parseHddInfoListXml = (xml) => {
87032
+ const list = getXmlBlocks(xml, "HddInfoList")[0];
87033
+ if (list === void 0) return [];
87034
+ return getXmlBlocks(list, "HddInfo").map((block) => {
87035
+ const capacity = num(block, "capacity");
87036
+ const capacityM = num(block, "capacityM");
87037
+ const capacityBytes = num(block, "capacityV2");
87038
+ const remainSize = num(block, "remainSize");
87039
+ const remainSizeM = num(block, "remainSizeM");
87040
+ const remainSizeBytes = num(block, "remainSizeV2");
87041
+ return {
87042
+ number: num(block, "number"),
87043
+ capacity,
87044
+ capacityM,
87045
+ capacityBytes,
87046
+ remainSize,
87047
+ remainSizeM,
87048
+ remainSizeBytes,
87049
+ format: num(block, "format"),
87050
+ mount: num(block, "mount"),
87051
+ type: num(block, "type"),
87052
+ index: num(block, "index"),
87053
+ capacityMb: megabytes$1(capacityBytes, capacity, capacityM),
87054
+ remainMb: megabytes$1(remainSizeBytes, remainSize, remainSizeM)
87055
+ };
87056
+ });
87057
+ };
87058
+ var DEFAULT_PRE_RECORD_SECONDS = 10;
87059
+ var parseRecordCfgLimits = (xml) => {
87060
+ const block = getXmlBlocks(xml, "RecordCfg")[0] ?? "";
87061
+ const numbersIn = (container, tag2) => {
87062
+ const outer = getXmlBlocks(block, container)[0];
87063
+ if (outer === void 0) return [];
87064
+ return getXmlBlocks(outer, tag2).map((t) => parseNumber(t.trim())).filter((n) => n !== void 0);
87065
+ };
87066
+ const pre = parseNumber(getXmlText(block, "preRecordTime"));
87067
+ return {
87068
+ cycleList: numbersIn("cyclelist", "item"),
87069
+ recordDelayTimeChoices: numbersIn("timeList", "time"),
87070
+ preRecordSupported: pre !== void 0,
87071
+ preRecordSeconds: pre === void 0 || pre === 0 ? null : pre
87072
+ };
87073
+ };
87074
+ var parseRecordScheduleRows = (xml) => {
87075
+ const block = getXmlBlocks(xml, "Record")[0];
87076
+ const list = block === void 0 ? void 0 : getXmlBlocks(block, "typeScheduleList")[0];
87077
+ if (list === void 0) return [];
87078
+ const rows = [];
87079
+ for (const item of getXmlBlocks(list, "item")) {
87080
+ const type = (getXmlText(item, "type") ?? "").trim();
87081
+ if (type === "") continue;
87082
+ const index = parseNumber(getXmlText(item, "index"));
87083
+ rows.push({
87084
+ type,
87085
+ index: index === void 0 ? null : index,
87086
+ valueTable: (getXmlText(item, "valueTable") ?? "").trim()
87087
+ });
87088
+ }
87089
+ return rows;
87090
+ };
87091
+ var BaichuanRecordConfigError = class extends Error {
87092
+ constructor(message) {
87093
+ super(message);
87094
+ this.name = "BaichuanRecordConfigError";
87095
+ }
87096
+ };
87097
+ var requireInteger = (field, value) => {
87098
+ if (!Number.isInteger(value)) throw new BaichuanRecordConfigError(`${field} must be a whole number, got ${String(value)}`);
87099
+ };
87100
+ var validateRecordCfgPatch = (patch, limits) => {
87101
+ if (patch.cycle !== void 0) {
87102
+ requireInteger("cycle", patch.cycle);
87103
+ 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`);
87104
+ }
87105
+ if (patch.recordDelayTime !== void 0) {
87106
+ requireInteger("recordDelayTime", patch.recordDelayTime);
87107
+ if (patch.recordDelayTime < 0) throw new BaichuanRecordConfigError(`recordDelayTime must not be negative, got ${patch.recordDelayTime}`);
87108
+ }
87109
+ if (patch.packageTime !== void 0) {
87110
+ requireInteger("packageTime", patch.packageTime);
87111
+ if (patch.packageTime < 1) throw new BaichuanRecordConfigError(`packageTime is a length in minutes and must be at least 1, got ${patch.packageTime}`);
87112
+ }
87113
+ 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");
87114
+ };
87115
+ var isWeeklyMask = (mask) => mask.length === 168 && /^[01]+$/.test(mask);
87116
+ var validateRecordSchedulePatch = (patch, rows) => {
87117
+ for (const entry of patch.entries ?? []) {
87118
+ 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`);
87119
+ 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"}`);
87120
+ }
87121
+ };
87122
+ var describeEntry = (entry) => entry.index === void 0 ? entry.type : `${entry.type}[${entry.index}]`;
87123
+ var describeRow = (row) => row.index === null ? row.type : `${row.type}[${row.index}]`;
87124
+ var replaceTag = (xml, tag2, value) => xml.replace(new RegExp(`<${tag2}>[^<]*</${tag2}>`), `<${tag2}>${value}</${tag2}>`);
87125
+ var buildRecordCfgSetXml = (currentXml, patch, limits) => {
87126
+ let xml = currentXml;
87127
+ if (patch.cycle !== void 0) xml = replaceTag(xml, "cycle", String(patch.cycle));
87128
+ if (patch.recordDelayTime !== void 0) xml = replaceTag(xml, "recordDelayTime", String(patch.recordDelayTime));
87129
+ if (patch.packageTime !== void 0) xml = replaceTag(xml, "packageTime", String(patch.packageTime));
87130
+ if (patch.preRecordEnabled !== void 0) {
87131
+ const seconds = patch.preRecordEnabled ? limits.preRecordSeconds ?? DEFAULT_PRE_RECORD_SECONDS : 0;
87132
+ xml = replaceTag(xml, "preRecordTime", String(seconds));
87133
+ }
87134
+ return xml;
87135
+ };
87136
+ var buildRecordScheduleSetXml = (currentXml, patch) => {
87137
+ let xml = currentXml;
87138
+ if (patch.enable !== void 0) xml = replaceTag(xml, "enable", patch.enable ? "1" : "0");
87139
+ for (const entry of patch.entries ?? []) xml = replaceItemValueTable(xml, entry);
87140
+ return xml;
87141
+ };
87142
+ var ITEM_RE = /<item>([\s\S]*?)<\/item>/g;
87143
+ var replaceItemValueTable = (xml, entry) => xml.replace(ITEM_RE, (whole, inner) => {
87144
+ if ((getXmlText(inner, "type") ?? "").trim() !== entry.type) return whole;
87145
+ if (entry.index !== void 0) {
87146
+ if (parseNumber(getXmlText(inner, "index")) !== entry.index) return whole;
87147
+ }
87148
+ if (getXmlText(inner, "valueTable") === void 0) return whole;
87149
+ return `<item>${replaceTag(inner, "valueTable", entry.valueTable)}</item>`;
87150
+ });
87151
+ var verdict = (outcomes) => ({
87152
+ outcomes,
87153
+ allApplied: outcomes.every((o) => o.status === "applied")
87154
+ });
87155
+ var compare = (field, requested, actual) => actual === requested ? {
87156
+ field,
87157
+ status: "applied",
87158
+ value: requested
87159
+ } : {
87160
+ field,
87161
+ status: "ignored",
87162
+ requested,
87163
+ actual: actual ?? "(absent)"
87164
+ };
87165
+ var diffRecordCfg = (patch, afterXml) => {
87166
+ const block = getXmlBlocks(afterXml, "RecordCfg")[0] ?? "";
87167
+ const outcomes = [];
87168
+ if (patch.cycle !== void 0) outcomes.push(compare("cycle", String(patch.cycle), getXmlText(block, "cycle")));
87169
+ if (patch.recordDelayTime !== void 0) outcomes.push(compare("recordDelayTime", String(patch.recordDelayTime), getXmlText(block, "recordDelayTime")));
87170
+ if (patch.packageTime !== void 0) outcomes.push(compare("packageTime", String(patch.packageTime), getXmlText(block, "packageTime")));
87171
+ if (patch.preRecordEnabled !== void 0) {
87172
+ const seconds = parseNumber(getXmlText(block, "preRecordTime"));
87173
+ const on = seconds !== void 0 && seconds !== 0;
87174
+ outcomes.push(compare("preRecordEnabled", String(patch.preRecordEnabled), seconds === void 0 ? void 0 : String(on)));
87175
+ }
87176
+ return verdict(outcomes);
87177
+ };
87178
+ var diffRecordSchedule = (patch, afterXml) => {
87179
+ const outcomes = [];
87180
+ const block = getXmlBlocks(afterXml, "Record")[0] ?? "";
87181
+ if (patch.enable !== void 0) outcomes.push(compare("enable", patch.enable ? "1" : "0", getXmlText(block, "enable")));
87182
+ const rows = parseRecordScheduleRows(afterXml);
87183
+ for (const entry of patch.entries ?? []) {
87184
+ const row = rows.find((r) => r.type === entry.type && (entry.index === void 0 || r.index === entry.index));
87185
+ outcomes.push(compare(`schedule.${describeEntry(entry)}`, entry.valueTable, row === void 0 ? void 0 : row.valueTable));
87186
+ }
87187
+ return verdict(outcomes);
87188
+ };
85905
87189
  function toNumberOrUndefined(value) {
85906
87190
  if (value == null) return void 0;
85907
87191
  const n = Number(value);
@@ -86515,254 +87799,6 @@ var estimateVideoTiming = (params) => {
86515
87799
  fpsSource: "unknown"
86516
87800
  };
86517
87801
  };
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
87802
  function daysInMonth(year, month) {
86767
87803
  return new Date(Date.UTC(year, month, 0)).getUTCDate();
86768
87804
  }
@@ -87371,17 +88407,6 @@ var TtlCache = class {
87371
88407
  this.entries.clear();
87372
88408
  }
87373
88409
  };
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
88410
  function parseOsdDatetimeXml(rawXml) {
87386
88411
  const osdBlock = getXmlBlocks(rawXml, "OsdDatetime")[0];
87387
88412
  const nameBlock = getXmlBlocks(rawXml, "OsdChannelName")[0];
@@ -88724,12 +89749,12 @@ var parseRecStartParamIfPresent = (fileName) => {
88724
89749
  if (!m) return void 0;
88725
89750
  return `${m[2]}${m[3]}`;
88726
89751
  };
88727
- var buildReplayStopNameFromFileName = (fileName) => {
89752
+ var buildReplayStopNameFromFileName = (fileName, channel = 0) => {
88728
89753
  const trimmed = (fileName ?? "").trim();
88729
- if (/^01\d{14}$/.test(trimmed)) return trimmed;
89754
+ if (/^\d{2}\d{14}$/.test(trimmed)) return trimmed;
88730
89755
  const start = parseRecStartParamIfPresent(fileName);
88731
89756
  if (!start) return void 0;
88732
- return `01${start}`;
89757
+ return `${String(channel + 1).padStart(2, "0")}${start}`;
88733
89758
  };
88734
89759
  var buildFileInfoListReplayByIdXml = (params) => {
88735
89760
  const st = params.streamType ?? "mainStream";
@@ -89024,7 +90049,7 @@ var sendTalkConfigWithReset = async (params) => {
89024
90049
  channel: params.channel,
89025
90050
  ...params.channelIdOverride != null ? { channelIdOverride: params.channelIdOverride } : {},
89026
90051
  payloadXml: params.payloadXml,
89027
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
90052
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
89028
90053
  })).header.responseCode;
89029
90054
  };
89030
90055
  const code = await trySend();
@@ -89034,7 +90059,7 @@ var sendTalkConfigWithReset = async (params) => {
89034
90059
  channel: params.channel,
89035
90060
  ...params.channelIdOverride != null ? { channelIdOverride: params.channelIdOverride } : {},
89036
90061
  payloadXml: "",
89037
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
90062
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
89038
90063
  });
89039
90064
  const retryCode = await trySend();
89040
90065
  if (retryCode !== 200) throw new Error(`TalkConfig rejected after reset (responseCode ${retryCode})`);
@@ -89101,7 +90126,7 @@ var createBufferedTalkSession = (params) => {
89101
90126
  ...channelIdOverride != null ? { channelIdOverride } : {},
89102
90127
  extensionXml: buildBinaryExtensionXml(channel),
89103
90128
  payload: Buffer.concat(parts),
89104
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
90129
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
89105
90130
  });
89106
90131
  const playLengthMs = samplesSent / info.audioConfig.sampleRate * 1e3;
89107
90132
  const now = Date.now();
@@ -89147,7 +90172,7 @@ var createBufferedTalkSession = (params) => {
89147
90172
  channel,
89148
90173
  ...channelIdOverride != null ? { channelIdOverride } : {},
89149
90174
  payloadXml: "",
89150
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
90175
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
89151
90176
  });
89152
90177
  if (frame.header.responseCode !== 200) throw new Error(`TalkReset rejected (responseCode ${frame.header.responseCode})`);
89153
90178
  if (params.closeSocketOnStop) try {
@@ -90830,7 +91855,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90830
91855
  if (debugCfg) {
90831
91856
  const sid = this.client.getSocketSessionId?.();
90832
91857
  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}`);
91858
+ require_chunk_OSVYEFMM.eventTraceLog(debugCfg, this.logger, tag2, `dispatch type=${evt.type} channel=${evt.channel} timestamp=${evt.timestamp}`);
90834
91859
  }
90835
91860
  for (const cb of this.simpleEventListeners) try {
90836
91861
  Promise.resolve(cb(evt)).catch((e) => {
@@ -90841,7 +91866,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90841
91866
  }
90842
91867
  }
90843
91868
  constructor(opts) {
90844
- const dbg = require_chunk_ZBACPERX.normalizeDebugOptions(opts.debugOptions);
91869
+ const dbg = require_chunk_OSVYEFMM.normalizeDebugOptions(opts.debugOptions);
90845
91870
  this.logger = createDebugGateLogger(opts.logger, dbg.general || dbg.traceNativeStream || dbg.traceRecordings || dbg.traceTalk || dbg.traceEvents || dbg.debugRtsp);
90846
91871
  this.clientOptions = {
90847
91872
  host: opts.host,
@@ -91429,7 +92454,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
91429
92454
  return;
91430
92455
  }
91431
92456
  entry.startInFlight = (async () => {
91432
- const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => require("./BaichuanVideoStream-FOE2DBVZ-DJcEkVak.js"));
92457
+ const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => require("./BaichuanVideoStream-UG3DIMIE-DViuQDJ4.js"));
91433
92458
  const sessionKey = `live:object-detections:ch${entry.channel}:${entry.profile}`;
91434
92459
  const dedicated = await this.createDedicatedSession(sessionKey);
91435
92460
  const stream$12 = new BaichuanVideoStream2({
@@ -91910,7 +92935,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
91910
92935
  channel: ch,
91911
92936
  ...channelIdOverride != null ? { channelIdOverride } : {},
91912
92937
  payloadXml: "",
91913
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
92938
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
91914
92939
  });
91915
92940
  if (frame.header.responseCode !== 200) throw new Error(`TalkReset rejected (responseCode ${frame.header.responseCode})`);
91916
92941
  }
@@ -91929,7 +92954,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
91929
92954
  channel: ch,
91930
92955
  ...channelIdOverride != null ? { channelIdOverride } : {},
91931
92956
  payloadXml,
91932
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
92957
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
91933
92958
  })).header.responseCode;
91934
92959
  };
91935
92960
  const code = await trySend();
@@ -92081,7 +93106,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
92081
93106
  cmdId: 10,
92082
93107
  channel,
92083
93108
  payloadXml: "",
92084
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
93109
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
92085
93110
  });
92086
93111
  return parseTalkAbilityXml(frame.body.length === 0 ? "" : client.tryDecryptXml(frame.body, frame.header.channelId, client.enc));
92087
93112
  }
@@ -93154,8 +94179,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93154
94179
  const start = params.start;
93155
94180
  const endOfStartDay = endOfWallClockDay(start, timeZone);
93156
94181
  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}`);
94182
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "getVideoclips", `Query: start=${start.toISOString()}, end=${end.toISOString()} (forced same day)`);
94183
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "getVideoclips", `Using UID for channel ${channel}: ${uid}`);
93159
94184
  const streamType = params.streamType ?? "subStream";
93160
94185
  const recordType = params.recordType ?? "manual,sched,io,md,people,face,vehicle,dog_cat,visitor,other,package,crossline,intrusion,loitering,legacy,loss";
93161
94186
  const fileRecordTypes = params.fileRecordTypes ?? (params.recordType === void 0 ? DEFAULT_RECORDING_SEARCH_FILE_RECORD_TYPES : void 0);
@@ -93174,7 +94199,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93174
94199
  ...timeZone !== void 0 ? { timeZone } : {}
93175
94200
  });
93176
94201
  const unique = dedupeRecordingFiles(files);
93177
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclips", `FileInfoList complete: ${unique.length} unique files (from ${files.length} total)`);
94202
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "getVideoclips", `FileInfoList complete: ${unique.length} unique files (from ${files.length} total)`);
93178
94203
  return unique;
93179
94204
  });
93180
94205
  }
@@ -93215,7 +94240,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93215
94240
  const start = params.start;
93216
94241
  const endOfStartDay = endOfWallClockDay(start, timeZone);
93217
94242
  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)`);
94243
+ 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
94244
  const windows = await searchAlarmVideosViaFindAlarmVideo({
93220
94245
  sendXml: (p) => this.sendXml(p),
93221
94246
  channel,
@@ -93231,7 +94256,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93231
94256
  maxPages: params.maxPages ?? 200,
93232
94257
  ...params.timeoutMs != null ? { timeoutMs: params.timeoutMs } : {}
93233
94258
  });
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})`);
94259
+ 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
94260
  return windows;
93236
94261
  });
93237
94262
  }
@@ -93296,7 +94321,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93296
94321
  seen.add(key);
93297
94322
  return true;
93298
94323
  });
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`);
94324
+ 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
94325
  return unique;
93301
94326
  });
93302
94327
  }
@@ -93349,7 +94374,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93349
94374
  });
93350
94375
  const msgNum = 0;
93351
94376
  dedicatedClient.subscribeVideoStream(5, msgNum);
93352
- const stream$14 = new require_chunk_ZBACPERX.BaichuanVideoStream({
94377
+ const stream$14 = new require_chunk_OSVYEFMM.BaichuanVideoStream({
93353
94378
  client: dedicatedClient,
93354
94379
  channel,
93355
94380
  profile: streamType === "subStream" ? "sub" : "main",
@@ -93368,7 +94393,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93368
94393
  channel,
93369
94394
  channelIdOverride: sessionCounter,
93370
94395
  payloadXml,
93371
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
94396
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
93372
94397
  msgNumOverride: 0,
93373
94398
  timeoutMs: params.timeoutMs,
93374
94399
  internal: true
@@ -93389,7 +94414,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93389
94414
  const stop = async () => {
93390
94415
  if (tornDown) return;
93391
94416
  tornDown = true;
93392
- const stopName = buildReplayStopNameFromFileName(params.fileName);
94417
+ const stopName = buildReplayStopNameFromFileName(params.fileName, channel);
93393
94418
  if (started && stopName) try {
93394
94419
  const stopXml = buildFileInfoListStopXml({
93395
94420
  channel,
@@ -93400,7 +94425,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93400
94425
  cmdId: 7,
93401
94426
  channel,
93402
94427
  payloadXml: stopXml,
93403
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
94428
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
93404
94429
  timeoutMs: 2e3,
93405
94430
  internal: true
93406
94431
  });
@@ -93457,7 +94482,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93457
94482
  });
93458
94483
  const msgNum = 0;
93459
94484
  dedicatedClient.subscribeVideoStream(5, msgNum);
93460
- const stream$15 = new require_chunk_ZBACPERX.BaichuanVideoStream({
94485
+ const stream$15 = new require_chunk_OSVYEFMM.BaichuanVideoStream({
93461
94486
  client: dedicatedClient,
93462
94487
  channel,
93463
94488
  profile: streamType === "subStream" ? "sub" : "main",
@@ -93477,7 +94502,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93477
94502
  channelIdOverride,
93478
94503
  payloadXml,
93479
94504
  extensionXml: "",
93480
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
94505
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
93481
94506
  msgNumOverride: msgNum,
93482
94507
  timeoutMs: params.timeoutMs,
93483
94508
  internal: true
@@ -93498,7 +94523,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93498
94523
  const stop = async () => {
93499
94524
  if (tornDown) return;
93500
94525
  tornDown = true;
93501
- const stopName = buildReplayStopNameFromFileName(params.fileName);
94526
+ const stopName = buildReplayStopNameFromFileName(params.fileName, channel);
93502
94527
  if (started && stopName) try {
93503
94528
  const stopXml = buildFileInfoListStopXml({
93504
94529
  channel,
@@ -93509,7 +94534,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93509
94534
  cmdId: 7,
93510
94535
  channel,
93511
94536
  payloadXml: stopXml,
93512
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
94537
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
93513
94538
  timeoutMs: 2e3,
93514
94539
  internal: true
93515
94540
  });
@@ -93596,35 +94621,35 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93596
94621
  async ensureUidForRecordings(channel, explicitUid) {
93597
94622
  const dbg = this.client.getDebugConfig?.();
93598
94623
  const logger = this.logger;
93599
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Checking UID: explicitUid=${explicitUid}, this.uid=${this.uid}`);
94624
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Checking UID: explicitUid=${explicitUid}, this.uid=${this.uid}`);
93600
94625
  const trimmedExplicit = explicitUid?.trim();
93601
94626
  if (trimmedExplicit) {
93602
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using explicit UID: ${trimmedExplicit}`);
94627
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using explicit UID: ${trimmedExplicit}`);
93603
94628
  return trimmedExplicit;
93604
94629
  }
93605
94630
  const perChannel = await this.discoverUidForRecordingsForChannel(channel);
93606
94631
  if (perChannel) {
93607
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID: ${perChannel}`);
94632
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID: ${perChannel}`);
93608
94633
  return perChannel;
93609
94634
  }
93610
94635
  const configured = this.uid?.trim();
93611
94636
  if (configured) {
93612
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using configured UID: ${configured}`);
94637
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using configured UID: ${configured}`);
93613
94638
  return configured;
93614
94639
  }
93615
94640
  if (this.nativeOnly) {
93616
94641
  const viaGetUid = await this.discoverUidViaGetUidNative(channel);
93617
94642
  if (viaGetUid) {
93618
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using device UID from cmd 114 GetUid: ${viaGetUid}`);
94643
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using device UID from cmd 114 GetUid: ${viaGetUid}`);
93619
94644
  return viaGetUid;
93620
94645
  }
93621
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: no UID available (channel=${channel})`);
94646
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: no UID available (channel=${channel})`);
93622
94647
  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
94648
  }
93624
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `No UID available, attempting device auto-discovery (ch=${channel})`);
94649
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `No UID available, attempting device auto-discovery (ch=${channel})`);
93625
94650
  const discovered = await this.discoverDeviceUidForRecordings(channel);
93626
94651
  if (discovered) return discovered;
93627
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Auto-discovery failed - no UID found`);
94652
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Auto-discovery failed - no UID found`);
93628
94653
  throw new Error("UID is required to access recordings. Provide a UID explicitly or configure the client with a UID.");
93629
94654
  }
93630
94655
  cacheChannelUid(channel, uid) {
@@ -93647,28 +94672,28 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93647
94672
  const logger = this.logger;
93648
94673
  const fromPush = this.getUidFromPushCacheForChannel(channel);
93649
94674
  if (fromPush) {
93650
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from push cache: ${fromPush}`);
94675
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from push cache: ${fromPush}`);
93651
94676
  return fromPush;
93652
94677
  }
93653
94678
  if (this.nativeOnly) {
93654
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: skipping per-channel UID discovery via HTTP/CGI (channel=${channel})`);
94679
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: skipping per-channel UID discovery via HTTP/CGI (channel=${channel})`);
93655
94680
  return;
93656
94681
  }
93657
94682
  try {
93658
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Attempting per-channel UID discovery via HTTP CGI GetChannelstatus (channel=${channel})`);
94683
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Attempting per-channel UID discovery via HTTP CGI GetChannelstatus (channel=${channel})`);
93659
94684
  const uidCandidate = await discoverPerChannelUidViaCgiChannelstatus({
93660
94685
  channel,
93661
94686
  login: () => this.cgiApi.login(),
93662
94687
  getChannelstatus: () => this.cgiApi.GetChannelstatus()
93663
94688
  });
93664
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus channel=${channel} uid=${uidCandidate || "(missing)"}`);
94689
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus channel=${channel} uid=${uidCandidate || "(missing)"}`);
93665
94690
  if (uidCandidate) {
93666
94691
  this.cacheChannelUid(channel, uidCandidate);
93667
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from GetChannelstatus: ${uidCandidate}`);
94692
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from GetChannelstatus: ${uidCandidate}`);
93668
94693
  return uidCandidate;
93669
94694
  }
93670
94695
  } catch (e) {
93671
- require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus failed: ${formatErrorForLog(e)}`);
94696
+ require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus failed: ${formatErrorForLog(e)}`);
93672
94697
  }
93673
94698
  }
93674
94699
  /**
@@ -93680,7 +94705,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93680
94705
  */
93681
94706
  async discoverUidViaGetUidNative(channel) {
93682
94707
  const dbg = this.client.getDebugConfig?.();
93683
- const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, this.logger, "ensureUidForRecordings", message);
94708
+ const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, this.logger, "ensureUidForRecordings", message);
93684
94709
  let isNvr = this._isNvr;
93685
94710
  if (isNvr === void 0) try {
93686
94711
  isNvr = await this.isNvrDevice();
@@ -93706,7 +94731,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93706
94731
  if (this.nativeOnly) return void 0;
93707
94732
  const dbg = this.client.getDebugConfig?.();
93708
94733
  const logger = this.logger;
93709
- const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", message);
94734
+ const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", message);
93710
94735
  const discoveredUid = await discoverDeviceUidForRecordings({
93711
94736
  channel,
93712
94737
  getInfo: () => this.getInfo(),
@@ -93781,7 +94806,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
93781
94806
  await this.client.login();
93782
94807
  const dbg = this.client.getDebugConfig?.();
93783
94808
  const logger = this.logger;
93784
- const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclipThumbnail", message);
94809
+ const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "getVideoclipThumbnail", message);
93785
94810
  const channel = this.normalizeChannel(params.channel);
93786
94811
  const snapStreamType = (params.snapType ?? "sub") === "main" ? "mainStream" : "subStream";
93787
94812
  const timeoutMs = params.timeoutMs ?? 3e4;
@@ -93836,7 +94861,7 @@ ${xml}`);
93836
94861
  cmdId: 298,
93837
94862
  ...isNvr && headerChannelIdOverride != null ? { channelIdOverride: headerChannelIdOverride } : {},
93838
94863
  msgNumOverride: 0,
93839
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
94864
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
93840
94865
  streamType: 0,
93841
94866
  payloadXml: xml,
93842
94867
  timeoutMs
@@ -94315,7 +95340,7 @@ ${stderr}`));
94315
95340
  await this.client.login();
94316
95341
  const dbg = this.client.getDebugConfig?.();
94317
95342
  const logger = this.logger;
94318
- const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListDownload", message);
95343
+ const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "fileInfoListDownload", message);
94319
95344
  const channel = this.normalizeChannel(params.channel);
94320
95345
  const uid = await this.ensureUidForRecordings(channel, params.uid);
94321
95346
  const headerChannelIdOverride = this.resolveHeaderChannelIdForLogicalChannel(channel);
@@ -94361,7 +95386,7 @@ ${stderr}`));
94361
95386
  cmdId: 13,
94362
95387
  channel,
94363
95388
  ...typeof chId === "number" ? { channelIdOverride: chId } : {},
94364
- messageClass: require_chunk_ZBACPERX.BC_CLASS_FILE_DOWNLOAD,
95389
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_FILE_DOWNLOAD,
94365
95390
  extensionXml: buildBinaryExtensionXml(channel),
94366
95391
  payloadXml,
94367
95392
  streamType: st,
@@ -94390,7 +95415,7 @@ ${stderr}`));
94390
95415
  await this.client.login();
94391
95416
  const dbg = this.client.getDebugConfig?.();
94392
95417
  const logger = this.logger;
94393
- const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListPagedDownload", message);
95418
+ const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "fileInfoListPagedDownload", message);
94394
95419
  const channel = this.normalizeChannel(params.channel);
94395
95420
  const uid = await this.ensureUidForRecordings(channel, params.uid);
94396
95421
  trace(`Starting paged download: channel=${channel}, uid=${uid}, fileName=${params.fileName}`);
@@ -94427,7 +95452,7 @@ ${stderr}`));
94427
95452
  await this.client.login();
94428
95453
  const dbg = this.client.getDebugConfig?.();
94429
95454
  const logger = this.logger;
94430
- const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListReplayBinaryDownload", message);
95455
+ const trace = (message) => require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "fileInfoListReplayBinaryDownload", message);
94431
95456
  const channel = this.normalizeChannel(params.channel);
94432
95457
  const headerChannelIdOverride = this.resolveHeaderChannelIdForLogicalChannel(channel);
94433
95458
  const ident = params.fileName;
@@ -94450,13 +95475,15 @@ ${stderr}`));
94450
95475
  });
94451
95476
  const timeoutMs = params.timeoutMs ?? 12e4;
94452
95477
  trace(`download: channel=${channel} uid=${uid || "(missing)"} ident=${ident} streamType=${streamType} isNvr=${isNvr} timeoutMs=${timeoutMs}`);
95478
+ const pinnedHeaderChannelId = params.headerChannelIdOverride;
95479
+ let abandoned = false;
94453
95480
  try {
94454
95481
  return await this.client.sendBinary({
94455
95482
  cmdId: 5,
94456
95483
  channel,
94457
- ...isNvr ? { channelIdOverride: headerChannelIdOverride ?? 82 } : {},
95484
+ ...pinnedHeaderChannelId != null ? { channelIdOverride: pinnedHeaderChannelId } : {},
94458
95485
  msgNumOverride: 0,
94459
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
95486
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
94460
95487
  payloadXml,
94461
95488
  streamType: 0,
94462
95489
  timeoutMs,
@@ -94465,8 +95492,53 @@ ${stderr}`));
94465
95492
  });
94466
95493
  } catch (e) {
94467
95494
  trace(`download failed: ${e instanceof Error ? e.message : String(e)}`);
95495
+ abandoned = true;
94468
95496
  throw e;
95497
+ } finally {
95498
+ await this.stopFileInfoListReplay({
95499
+ channel,
95500
+ fileName: ident,
95501
+ streamType,
95502
+ drain: abandoned
95503
+ });
95504
+ }
95505
+ }
95506
+ /**
95507
+ * Close a cmd 5 replay session at the camera (cmd 7) and wait for the wire to
95508
+ * go quiet.
95509
+ *
95510
+ * Best-effort by design: a session that has already ended answers with an
95511
+ * error on some firmwares and there is nothing to do about it. The DRAIN is
95512
+ * not best-effort — the ack comes back before the camera has finished
95513
+ * sending (the Reolink app's own capture shows 26–91 further frames after
95514
+ * four of eleven stops), and the next replay must not be opened into that
95515
+ * tail.
95516
+ */
95517
+ async stopFileInfoListReplay(params) {
95518
+ const stopName = buildReplayStopNameFromFileName(params.fileName, params.channel);
95519
+ if (!stopName) {
95520
+ this.logger?.debug?.(`[stopFileInfoListReplay] no stop name derivable from ${params.fileName}; the session is left to time out at the camera`);
95521
+ return;
94469
95522
  }
95523
+ try {
95524
+ await this.client.sendXml({
95525
+ cmdId: 7,
95526
+ channel: params.channel,
95527
+ payloadXml: buildFileInfoListStopXml({
95528
+ channel: params.channel,
95529
+ name: stopName,
95530
+ streamType: params.streamType
95531
+ }),
95532
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95533
+ timeoutMs: 4e3,
95534
+ internal: true
95535
+ });
95536
+ } catch (e) {
95537
+ this.logger?.debug?.(`[stopFileInfoListReplay] stop <name>${stopName}</name> failed: ${e instanceof Error ? e.message : String(e)}`);
95538
+ }
95539
+ if (!params.drain) return;
95540
+ const drained = await this.client.drainReplayFrames();
95541
+ 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`);
94470
95542
  }
94471
95543
  async downloadRecording(params) {
94472
95544
  this.logger?.debug?.(`[downloadRecording] Queuing download for: ${params.fileName}, channel=${params.channel}`);
@@ -94545,7 +95617,7 @@ ${stderr}`));
94545
95617
  const frames = [];
94546
95618
  const timestampsUs = [];
94547
95619
  const audioFrames = [];
94548
- const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
95620
+ const decoder = new require_chunk_OSVYEFMM.BcMediaAnnexBDecoder({
94549
95621
  strict: false,
94550
95622
  logger: this.logger,
94551
95623
  onVideoAccessUnit: ({ annexB, microseconds }) => {
@@ -94611,7 +95683,7 @@ ${stderr}`));
94611
95683
  const videoFrames = [];
94612
95684
  const audioFrames = [];
94613
95685
  let audioCodec = null;
94614
- const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
95686
+ const decoder = new require_chunk_OSVYEFMM.BcMediaAnnexBDecoder({
94615
95687
  strict: false,
94616
95688
  logger: this.logger,
94617
95689
  onVideoAccessUnit: ({ annexB, microseconds }) => {
@@ -95009,7 +96081,7 @@ ${stderr}`));
95009
96081
  params: {
95010
96082
  cmdId: 31,
95011
96083
  channelIdOverride: channel,
95012
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
96084
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
95013
96085
  }
95014
96086
  },
95015
96087
  {
@@ -95017,7 +96089,7 @@ ${stderr}`));
95017
96089
  params: {
95018
96090
  cmdId: 31,
95019
96091
  channelIdOverride: 251,
95020
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
96092
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
95021
96093
  }
95022
96094
  },
95023
96095
  {
@@ -95025,7 +96097,7 @@ ${stderr}`));
95025
96097
  params: {
95026
96098
  cmdId: 31,
95027
96099
  channelIdOverride: 250,
95028
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
96100
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
95029
96101
  }
95030
96102
  },
95031
96103
  {
@@ -95033,7 +96105,7 @@ ${stderr}`));
95033
96105
  params: {
95034
96106
  cmdId: 31,
95035
96107
  channelIdOverride: 250,
95036
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96108
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95037
96109
  extensionXml: `<?xml version="1.0" encoding="UTF-8" ?><Extension version="1.1"><channelId>251</channelId></Extension>`
95038
96110
  }
95039
96111
  }
@@ -95076,7 +96148,7 @@ ${stderr}`));
95076
96148
  const frame = await this.client.sendFrame({
95077
96149
  cmdId: 31,
95078
96150
  channelIdOverride: Number(channel),
95079
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96151
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95080
96152
  timeoutMs
95081
96153
  });
95082
96154
  lastCode = frame.header.responseCode;
@@ -95357,7 +96429,7 @@ ${stderr}`));
95357
96429
  msgNumOverride: msgNum,
95358
96430
  extensionXml: buildChannelExtensionXml(channelId),
95359
96431
  payloadXml,
95360
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96432
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95361
96433
  streamType: config.streamType,
95362
96434
  timeoutMs: 2e4
95363
96435
  };
@@ -95489,7 +96561,7 @@ ${stderr}`));
95489
96561
  channelIdOverride: channelId,
95490
96562
  extensionXml: a.extensionXml,
95491
96563
  payloadXml: a.payloadXml,
95492
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96564
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95493
96565
  streamType: a.streamType,
95494
96566
  ...msgNum !== void 0 ? { msgNumOverride: msgNum } : {},
95495
96567
  timeoutMs: 2e3
@@ -95519,7 +96591,7 @@ ${stderr}`));
95519
96591
  channel: ch,
95520
96592
  channelIdOverride: channelId,
95521
96593
  extensionXml: buildChannelExtensionXml(channelId),
95522
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96594
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95523
96595
  streamType: 0
95524
96596
  });
95525
96597
  } catch (e) {
@@ -95561,7 +96633,7 @@ ${stderr}`));
95561
96633
  channel: ch,
95562
96634
  channelIdOverride: channelId,
95563
96635
  extensionXml: buildChannelExtensionXml(channelId),
95564
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96636
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95565
96637
  streamType: 0
95566
96638
  });
95567
96639
  } catch (e) {
@@ -95583,7 +96655,7 @@ ${stderr}`));
95583
96655
  channelIdOverride: channelId,
95584
96656
  extensionXml,
95585
96657
  payloadXml,
95586
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96658
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95587
96659
  streamType: 0
95588
96660
  });
95589
96661
  if (frame.header.responseCode !== 200) {
@@ -95615,7 +96687,7 @@ ${stderr}`));
95615
96687
  channelIdOverride: channelId,
95616
96688
  extensionXml,
95617
96689
  payloadXml,
95618
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96690
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95619
96691
  streamType: 0
95620
96692
  });
95621
96693
  if (frame.header.responseCode !== 200) throw new Error(`PTZ preset move rejected (response_code ${frame.header.responseCode})`);
@@ -95636,7 +96708,7 @@ ${stderr}`));
95636
96708
  channelIdOverride: channelId,
95637
96709
  extensionXml,
95638
96710
  payloadXml,
95639
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96711
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95640
96712
  streamType: 0
95641
96713
  });
95642
96714
  if (frame.header.responseCode !== 200) throw new Error(`PTZ preset save rejected (response_code ${frame.header.responseCode})`);
@@ -95653,7 +96725,7 @@ ${stderr}`));
95653
96725
  channelIdOverride: channelId,
95654
96726
  extensionXml,
95655
96727
  payloadXml,
95656
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96728
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95657
96729
  streamType: 0
95658
96730
  });
95659
96731
  if (frame.header.responseCode !== 200) throw new Error(`PTZ goto preset rejected (response_code ${frame.header.responseCode})`);
@@ -95680,7 +96752,7 @@ ${stderr}`));
95680
96752
  channelIdOverride: channelId,
95681
96753
  extensionXml,
95682
96754
  payloadXml,
95683
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96755
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95684
96756
  streamType: 0
95685
96757
  })).header.responseCode };
95686
96758
  },
@@ -95734,7 +96806,7 @@ ${stderr}`));
95734
96806
  channel: ch,
95735
96807
  channelIdOverride: ch,
95736
96808
  extensionXml: buildPtzGuardExtensionXml(ch, "read"),
95737
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96809
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95738
96810
  streamType: 0
95739
96811
  }));
95740
96812
  }
@@ -95760,7 +96832,7 @@ ${stderr}`));
95760
96832
  timeoutSeconds: options.timeoutSeconds,
95761
96833
  setPosition: options.setPosition === true
95762
96834
  }),
95763
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96835
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95764
96836
  streamType: 0
95765
96837
  });
95766
96838
  }
@@ -95781,7 +96853,7 @@ ${stderr}`));
95781
96853
  channelIdOverride: ch,
95782
96854
  extensionXml: buildPtzGuardExtensionXml(ch, "write"),
95783
96855
  payloadXml: buildPtzGuardGoXml(ch),
95784
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96856
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95785
96857
  streamType: 0
95786
96858
  });
95787
96859
  }
@@ -95797,7 +96869,7 @@ ${stderr}`));
95797
96869
  channel: ch,
95798
96870
  channelIdOverride: channelId,
95799
96871
  extensionXml: buildChannelExtensionXml(channelId),
95800
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96872
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95801
96873
  streamType: 0
95802
96874
  });
95803
96875
  const parseTriplet = (sectionTag) => {
@@ -95835,7 +96907,7 @@ ${stderr}`));
95835
96907
  channelIdOverride: channelId,
95836
96908
  extensionXml,
95837
96909
  payloadXml,
95838
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
96910
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24,
95839
96911
  streamType: 0
95840
96912
  });
95841
96913
  if (frame.header.responseCode !== 200) throw new Error(`Zoom rejected (response_code ${frame.header.responseCode})`);
@@ -97586,7 +98658,7 @@ ${xml}`);
97586
98658
  * @returns Test results for all stream types and profiles
97587
98659
  */
97588
98660
  async testChannelStreams(channel, logger) {
97589
- const { testChannelStreams } = await Promise.resolve().then(() => require("./DiagnosticsTools-ZC7I7QM6-CD4rRF8a.js"));
98661
+ const { testChannelStreams } = await Promise.resolve().then(() => require("./DiagnosticsTools-YIWXO7IJ-CD4rRF8a.js"));
97590
98662
  return await testChannelStreams({
97591
98663
  api: this,
97592
98664
  channel: this.normalizeChannel(channel),
@@ -97602,7 +98674,7 @@ ${xml}`);
97602
98674
  * @returns Complete diagnostics for all channels and streams
97603
98675
  */
97604
98676
  async collectMultifocalDiagnostics(logger) {
97605
- const { collectMultifocalDiagnostics } = await Promise.resolve().then(() => require("./DiagnosticsTools-ZC7I7QM6-CD4rRF8a.js"));
98677
+ const { collectMultifocalDiagnostics } = await Promise.resolve().then(() => require("./DiagnosticsTools-YIWXO7IJ-CD4rRF8a.js"));
97606
98678
  return await collectMultifocalDiagnostics({
97607
98679
  api: this,
97608
98680
  logger
@@ -98424,6 +99496,115 @@ ${xml}`);
98424
99496
  };
98425
99497
  })() : {};
98426
99498
  }
99499
+ /**
99500
+ * The limits THIS camera states for `<RecordCfg>` (cmd 54), read from its
99501
+ * own reply — `<cyclelist>`, `<timeList>`, whether pre-record exists at all.
99502
+ *
99503
+ * Read this before offering a control: a firmware that stops accepting a
99504
+ * value stops listing it, and {@link setRecordCfg} refuses against the same
99505
+ * facts.
99506
+ */
99507
+ async getRecordCfgLimits(channel, options) {
99508
+ return parseRecordCfgLimits(await this.sendPcapDerivedSettingsGetXml({
99509
+ cmdId: 54,
99510
+ channel,
99511
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
99512
+ }));
99513
+ }
99514
+ /**
99515
+ * SetRecCfg (cmd 55) — write the onboard recording configuration.
99516
+ *
99517
+ * Read-modify-write over the camera's own cmd 54 document, then a re-read to
99518
+ * find out what it actually kept. Both halves are load-bearing, because this
99519
+ * command **never reports a bad value**: measured on 2026-09-22, a
99520
+ * `preRecordTime` of 9999 and a `cycle` outside the camera's own
99521
+ * `<cyclelist>` were each answered `200` and discarded. The returned
99522
+ * {@link BaichuanRecordWriteResult} is the only honest answer to "did that
99523
+ * work", so callers should look at `allApplied` rather than at the absence
99524
+ * of a thrown error.
99525
+ *
99526
+ * Values the camera would certainly drop are refused BEFORE the wire with a
99527
+ * {@link BaichuanRecordConfigError}.
99528
+ *
99529
+ * `preRecordEnabled` is a boolean on purpose — see `utils/recordConfig.ts`.
99530
+ *
99531
+ * Verified live: `packageTime` 5 → 10 and `recordDelayTime` 15 → 30 on E1
99532
+ * Outdoor PoE v3.1.0.5223, `recordDelayTime` 15 → 30 → 20 on a Home Hub
99533
+ * v3.3.0.456 channel 0, each read back and restored.
99534
+ */
99535
+ async setRecordCfg(channel, patch, options) {
99536
+ const ch = this.normalizeChannel(channel);
99537
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
99538
+ const currentXml = await this.sendXml({
99539
+ cmdId: 54,
99540
+ channel: ch,
99541
+ ...timeoutOpts
99542
+ });
99543
+ const limits = parseRecordCfgLimits(currentXml);
99544
+ validateRecordCfgPatch(patch, limits);
99545
+ await this.sendXml({
99546
+ cmdId: 55,
99547
+ channel: ch,
99548
+ payloadXml: ensureXmlHeader(buildRecordCfgSetXml(currentXml, patch, limits)),
99549
+ ...timeoutOpts
99550
+ });
99551
+ return diffRecordCfg(patch, await this.sendXml({
99552
+ cmdId: 54,
99553
+ channel: ch,
99554
+ ...timeoutOpts
99555
+ }));
99556
+ }
99557
+ /**
99558
+ * The schedule rows THIS camera lists (cmd 81), `(type, index)` and the
99559
+ * current mask for each.
99560
+ *
99561
+ * `<index>` matters: an E1 Outdoor Pro lists `crossline`, `intrude` and
99562
+ * `loitering` three times each, one row per configured rule, and a write
99563
+ * addressed by `type` alone would overwrite all three.
99564
+ */
99565
+ async getRecordScheduleRows(channel, options) {
99566
+ return parseRecordScheduleRows(await this.sendPcapDerivedSettingsGetXml({
99567
+ cmdId: 81,
99568
+ channel,
99569
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
99570
+ }));
99571
+ }
99572
+ /**
99573
+ * SetRec (cmd 82) — write the weekly recording schedule.
99574
+ *
99575
+ * Same contract as {@link setRecordCfg}: refuse what is certainly bad before
99576
+ * the wire, then verify by re-reading. A 167-character mask and a `<type>`
99577
+ * the camera does not have were each answered `200` and dropped, and the
99578
+ * drop is **per item** — one document carrying a bad `Normal` mask and a
99579
+ * good `MD` mask applied the `MD` one and discarded the other. So a caller
99580
+ * cannot treat this write as atomic, and the per-field
99581
+ * {@link BaichuanRecordWriteResult} says which rows landed.
99582
+ *
99583
+ * Verified live on E1 Outdoor PoE v3.1.0.5223: `Normal` hour 0 flipped on
99584
+ * and restored, via both a full document and a one-item document (the camera
99585
+ * merges per row and leaves the other rows alone).
99586
+ */
99587
+ async setRecordSchedule(channel, patch, options) {
99588
+ const ch = this.normalizeChannel(channel);
99589
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
99590
+ const currentXml = await this.sendXml({
99591
+ cmdId: 81,
99592
+ channel: ch,
99593
+ ...timeoutOpts
99594
+ });
99595
+ validateRecordSchedulePatch(patch, parseRecordScheduleRows(currentXml));
99596
+ await this.sendXml({
99597
+ cmdId: 82,
99598
+ channel: ch,
99599
+ payloadXml: ensureXmlHeader(buildRecordScheduleSetXml(currentXml, patch)),
99600
+ ...timeoutOpts
99601
+ });
99602
+ return diffRecordSchedule(patch, await this.sendXml({
99603
+ cmdId: 81,
99604
+ channel: ch,
99605
+ ...timeoutOpts
99606
+ }));
99607
+ }
98427
99608
  async getWifiSignal(channel, options) {
98428
99609
  const signal = parseNumber(getXmlText(await this.sendPcapDerivedSettingsGetXml({
98429
99610
  cmdId: 115,
@@ -98622,6 +99803,22 @@ ${xml}`);
98622
99803
  }));
98623
99804
  }
98624
99805
  /**
99806
+ * cmd 102 `<HddInfoList>`, typed — one record per volume with the size
99807
+ * arithmetic already done.
99808
+ *
99809
+ * Prefer this over {@link getHddInfoList}. The raw reply splits size across
99810
+ * `capacity` (whole GB) and `capacityM` (MB remainder), and newer firmwares
99811
+ * add exact byte counts in `capacityV2` / `remainSizeV2`; `capacityMb` and
99812
+ * `remainMb` fold whichever pair the camera sent, and are **null when it
99813
+ * sent neither** rather than 0.
99814
+ */
99815
+ async getHddInfo(options) {
99816
+ return parseHddInfoListXml(await this.sendXml({
99817
+ cmdId: 102,
99818
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
99819
+ }));
99820
+ }
99821
+ /**
98625
99822
  * cmd 142 `<DayRecords>` — which days of a month have footage, for one
98626
99823
  * channel. The calendar the official app draws.
98627
99824
  *
@@ -99687,7 +100884,7 @@ ${scheduleItems}
99687
100884
  if (raw.length === 0) throw new Error("Downloaded recording is empty");
99688
100885
  const videoFrames = [];
99689
100886
  let videoType = null;
99690
- const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
100887
+ const decoder = new require_chunk_OSVYEFMM.BcMediaAnnexBDecoder({
99691
100888
  strict: false,
99692
100889
  logger: this.logger,
99693
100890
  onVideoAccessUnit: ({ annexB: annexB2, microseconds }) => {
@@ -100035,7 +101232,7 @@ ${scheduleItems}
100035
101232
  return {
100036
101233
  getPlaylist: () => {
100037
101234
  try {
100038
- const { readFileSync } = require_chunk_ZBACPERX.__require("fs");
101235
+ const { readFileSync } = require_chunk_OSVYEFMM.__require("fs");
100039
101236
  return readFileSync(playlistPath, "utf8");
100040
101237
  } catch {
100041
101238
  return "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:4\n";
@@ -100044,7 +101241,7 @@ ${scheduleItems}
100044
101241
  getSegment: (name) => {
100045
101242
  if (segments.has(name)) return segments.get(name);
100046
101243
  try {
100047
- const { readFileSync } = require_chunk_ZBACPERX.__require("fs");
101244
+ const { readFileSync } = require_chunk_OSVYEFMM.__require("fs");
100048
101245
  const data = readFileSync(path$3.join(tempDir, name));
100049
101246
  segments.set(name, data);
100050
101247
  return data;
@@ -100054,7 +101251,7 @@ ${scheduleItems}
100054
101251
  },
100055
101252
  listSegments: () => {
100056
101253
  try {
100057
- const { readdirSync } = require_chunk_ZBACPERX.__require("fs");
101254
+ const { readdirSync } = require_chunk_OSVYEFMM.__require("fs");
100058
101255
  return readdirSync(tempDir).filter((f) => f.endsWith(".ts"));
100059
101256
  } catch {
100060
101257
  return [];
@@ -101639,41 +102836,41 @@ async function autoDetectDeviceType(inputs) {
101639
102836
  variant: "cmd80 class=0x6414",
101640
102837
  op: () => api.getInfo(void 0, {
101641
102838
  timeoutMs: 2500,
101642
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
102839
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
101643
102840
  })
101644
102841
  },
101645
102842
  {
101646
102843
  variant: "cmd80 class=0x6614",
101647
102844
  op: () => api.getInfo(void 0, {
101648
102845
  timeoutMs: 3e3,
101649
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
102846
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_20
101650
102847
  })
101651
102848
  },
101652
102849
  {
101653
102850
  variant: "cmd318(ch0) class=0x6414",
101654
102851
  op: () => api.getInfo(0, {
101655
102852
  timeoutMs: 3e3,
101656
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
102853
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
101657
102854
  })
101658
102855
  },
101659
102856
  {
101660
102857
  variant: "cmd318(ch0) class=0x6614",
101661
102858
  op: () => api.getInfo(0, {
101662
102859
  timeoutMs: 3500,
101663
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
102860
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_20
101664
102861
  })
101665
102862
  }
101666
102863
  ]), runProbeVariants("getSupportInfo", [{
101667
102864
  variant: "cmd199 class=0x6414",
101668
102865
  op: () => api.getSupportInfo({
101669
102866
  timeoutMs: 2500,
101670
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
102867
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_24
101671
102868
  })
101672
102869
  }, {
101673
102870
  variant: "cmd199 class=0x6614",
101674
102871
  op: () => api.getSupportInfo({
101675
102872
  timeoutMs: 3500,
101676
- messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
102873
+ messageClass: require_chunk_OSVYEFMM.BC_CLASS_MODERN_20
101677
102874
  })
101678
102875
  }])]);
101679
102876
  const deviceInfo = infoProbe?.value;
@@ -148202,7 +149399,7 @@ async function createRfc4571TcpServerInternal(options) {
148202
149399
  streamClient = dedicatedSession.client;
148203
149400
  logSessionsSummary(`dedicated session created: ${sessionKey}`);
148204
149401
  } else streamClient = baseApi.client;
148205
- const createLiveStream = async () => new require_chunk_ZBACPERX.BaichuanVideoStream({
149402
+ const createLiveStream = async () => new require_chunk_OSVYEFMM.BaichuanVideoStream({
148206
149403
  client: streamClient,
148207
149404
  api: baseApi,
148208
149405
  channel: ch,
@@ -150948,6 +152145,14 @@ function buildClipStreamMuxArgs(input) {
150948
152145
  * not push the deadline out. `drain` — the hop's own word that it took what
150949
152146
  * it held — disarms it. Stall fires at most once, and a disposed gate is
150950
152147
  * silent for ever.
152148
+ *
152149
+ * **A gate must be DISPOSED when its hop stops taking bytes for good** — when
152150
+ * the input is ended, and when the run is over. Node emits no `drain` after
152151
+ * `end()` (`afterWrite` guards it with `!state.ending`), so a gate armed at
152152
+ * EOF can never be disarmed: it fires `timeoutMs` later and severs a stream
152153
+ * that was already served. Measured on real bytes: a 42 s clip delivered at
152154
+ * 18× leaves the audio pipe armed at EOF every time, with zero `drain`
152155
+ * events after `end()`. (D604)
150951
152156
  */
150952
152157
  function createDrainGate(deps) {
150953
152158
  let handle = null;
@@ -151034,7 +152239,7 @@ function writableAt(stdio, index) {
151034
152239
  if (candidate === null || candidate === void 0) return null;
151035
152240
  return isWritable(candidate) ? candidate : null;
151036
152241
  }
151037
- var defaultDecoder = (handlers) => new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
152242
+ var defaultDecoder = (handlers) => new require_chunk_OSVYEFMM.BcMediaAnnexBDecoder({
151038
152243
  strict: false,
151039
152244
  onVideoAccessUnit: handlers.onVideoAccessUnit,
151040
152245
  onAudioFrame: (frame) => handlers.onAudioFrame({
@@ -151152,8 +152357,18 @@ var ClipStreamRun = class {
151152
152357
  accessUnits: 0
151153
152358
  };
151154
152359
  videoType = null;
152360
+ /** The last access unit's timestamp, to catch a transfer that went BACK. */
152361
+ lastUs = null;
152362
+ /** Units of a PREVIOUS transfer, dropped before the mux existed (D604). */
152363
+ residueUnits = 0;
152364
+ residueAudioFrames = 0;
152365
+ /** A backwards timestamp that arrived too late to drop: the duration of
152366
+ * this pass cannot be measured, and is never guessed (D393). */
152367
+ timestampsBroken = false;
151155
152368
  begun = false;
151156
152369
  ended = false;
152370
+ /** The run is over. Every late callback is silent from here (D604). */
152371
+ done = false;
151157
152372
  bytesOut = 0;
151158
152373
  cut = null;
151159
152374
  stdinGate;
@@ -151210,17 +152425,28 @@ var ClipStreamRun = class {
151210
152425
  };
151211
152426
  }
151212
152427
  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);
152428
+ try {
152429
+ await api.fileInfoListReplayBinaryDownload({
152430
+ channel: device.channel,
152431
+ ...device.uid !== void 0 ? { uid: device.uid } : {},
152432
+ fileName,
152433
+ idleTimeoutMs: 300,
152434
+ onChunk: (chunk) => this.onChunk(decoder, chunk)
152435
+ });
152436
+ } catch (err) {
152437
+ return this.onTransferFailed(err);
152438
+ }
152439
+ return await this.onTransferEnded();
152440
+ } finally {
152441
+ this.done = true;
152442
+ this.disposeGates();
151222
152443
  }
151223
- return this.onTransferEnded();
152444
+ }
152445
+ /** Every hop's drain gate, silenced. Idempotent. */
152446
+ disposeGates() {
152447
+ this.stdinGate.dispose();
152448
+ this.audioStdinGate.dispose();
152449
+ this.peerGate.dispose();
151224
152450
  }
151225
152451
  /** The camera's bytes, as they arrive. A cut run THROWS here: the only stop. */
151226
152452
  onChunk(decoder, chunk) {
@@ -151234,14 +152460,81 @@ var ClipStreamRun = class {
151234
152460
  }
151235
152461
  onAccessUnit(unit) {
151236
152462
  if (this.cut !== null) return;
152463
+ const previousUs = this.lastUs;
152464
+ this.lastUs = unit.microseconds;
152465
+ if (previousUs !== null && unit.microseconds < previousUs) this.onTimestampsWentBack(previousUs, unit.microseconds);
151237
152466
  this.accessUnits += 1;
151238
152467
  this.timestampsUs.push(unit.microseconds);
151239
- if (this.mux === null) {
151240
- this.probe = [...this.probe, unit];
151241
- if (this.probe.length >= 16) this.startMux();
152468
+ if (this.mux !== null) {
152469
+ this.feed(unit.annexB);
151242
152470
  return;
151243
152471
  }
151244
- this.feed(unit.annexB);
152472
+ this.probe = [...this.probe, unit];
152473
+ if (this.probe.length < 16) return;
152474
+ if (probeClipStreamRate(this.timestampsUs).fpsSource === "timestamps" || this.probe.length >= 128) this.startMux();
152475
+ }
152476
+ /**
152477
+ * A timestamp that went BACKWARD: this is a second transfer's first frame.
152478
+ *
152479
+ * The library's replay listener cannot tell one transfer from the next —
152480
+ * every cmd 5 replay carries `msgNum: 0` and, on a hub child, the same
152481
+ * header channel id — and a CUT transfer is never stopped at the camera
152482
+ * (nothing on the wire aborts one; the producer's only lever is to throw
152483
+ * from the next chunk). So the frames still in flight for the previous,
152484
+ * aborted stream are handed to THIS one, ahead of its own first frame.
152485
+ *
152486
+ * Measured on 3628, 2026-09-22 16:41: a stream cut at 55 access units was
152487
+ * followed 1 ms later by another, which decoded 640 units instead of 635
152488
+ * and 666 audio frames instead of 661 — the same five extra of each — and
152489
+ * spanned 38.52 s instead of 42.21 s, exactly the 3.69 s the cut stream had
152490
+ * already delivered. Its rate probe therefore measured a NEGATIVE span and
152491
+ * refused to name a rate, which is the only way `estimateVideoTiming` can
152492
+ * answer anything but `timestamps` here.
152493
+ *
152494
+ * Before the mux exists the answer is to DROP what was held and probe
152495
+ * again: nothing has reached the peer, so the residue can simply not be
152496
+ * part of this stream. After it exists there is nothing to drop — the bytes
152497
+ * are out — so it is named, counted, and this pass's duration becomes
152498
+ * unmeasurable rather than a number measured across two transfers.
152499
+ */
152500
+ onTimestampsWentBack(previousUs, currentUs) {
152501
+ const { row, twin } = this.input;
152502
+ const meta = {
152503
+ clipKey: row.clipKey,
152504
+ twin,
152505
+ previousUs,
152506
+ currentUs
152507
+ };
152508
+ if (this.mux !== null) {
152509
+ if (this.timestampsBroken) return;
152510
+ this.timestampsBroken = true;
152511
+ this.deps.logger.warn("videoclips: a clip stream’s timestamps went backwards mid-mux", {
152512
+ tags: this.tags,
152513
+ meta: {
152514
+ ...meta,
152515
+ branch: "stream-timestamp-jump",
152516
+ accessUnits: this.accessUnits
152517
+ }
152518
+ });
152519
+ return;
152520
+ }
152521
+ this.residueUnits += this.probe.length;
152522
+ this.residueAudioFrames += this.audioProbe.length;
152523
+ this.audioFramesDropped += this.audioProbe.length;
152524
+ this.probe = [];
152525
+ this.audioProbe = [];
152526
+ this.audioProbeBytes = 0;
152527
+ this.timestampsUs.length = 0;
152528
+ this.accessUnits = 0;
152529
+ this.deps.logger.warn("videoclips: a clip stream dropped the previous transfer’s frames", {
152530
+ tags: this.tags,
152531
+ meta: {
152532
+ ...meta,
152533
+ branch: "stream-transfer-residue",
152534
+ residueAccessUnits: this.residueUnits,
152535
+ residueAudioFrames: this.residueAudioFrames
152536
+ }
152537
+ });
151245
152538
  }
151246
152539
  /**
151247
152540
  * One audio frame from the decoder.
@@ -151306,15 +152599,20 @@ var ClipStreamRun = class {
151306
152599
  this.rate = probeClipStreamRate(this.timestampsUs);
151307
152600
  this.decideAudio();
151308
152601
  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
- });
152602
+ if (this.rate.fps === null || this.rate.fpsSource !== "timestamps") {
152603
+ this.deps.logger.warn("videoclips: no measured frame rate for this clip stream", {
152604
+ tags: this.tags,
152605
+ meta: {
152606
+ clipKey: row.clipKey,
152607
+ twin,
152608
+ branch: "fps-unmeasured",
152609
+ probeAccessUnits: this.rate.accessUnits,
152610
+ residueAccessUnits: this.residueUnits
152611
+ }
152612
+ });
152613
+ this.sever("rate-unmeasured", `no frame rate could be measured over ${String(this.rate.accessUnits)} access units`);
152614
+ return;
152615
+ }
151318
152616
  let mux;
151319
152617
  try {
151320
152618
  mux = this.deps.spawnMux(buildClipStreamMuxArgs({
@@ -151419,14 +152717,12 @@ var ClipStreamRun = class {
151419
152717
  }
151420
152718
  /** Sever every hop, once, and say why. */
151421
152719
  sever(reason, detail) {
151422
- if (this.cut !== null) return;
152720
+ if (this.cut !== null || this.done) return;
151423
152721
  this.cut = {
151424
152722
  reason,
151425
152723
  detail
151426
152724
  };
151427
- this.stdinGate.dispose();
151428
- this.audioStdinGate.dispose();
151429
- this.peerGate.dispose();
152725
+ this.disposeGates();
151430
152726
  this.mux?.kill("SIGKILL");
151431
152727
  this.deps.logger.warn("videoclips: a clip stream was cut", {
151432
152728
  tags: this.tags,
@@ -151487,6 +152783,8 @@ var ClipStreamRun = class {
151487
152783
  });
151488
152784
  mux.stdin?.end();
151489
152785
  this.audioIn?.end();
152786
+ this.stdinGate.dispose();
152787
+ this.audioStdinGate.dispose();
151490
152788
  const flush = setTimeout(() => this.sever("mux-hung", `ffmpeg did not exit ${this.deps.flushTimeoutMs} ms after its input ended`), this.deps.flushTimeoutMs);
151491
152789
  const code = await this.muxClosed;
151492
152790
  clearTimeout(flush);
@@ -151506,7 +152804,7 @@ var ClipStreamRun = class {
151506
152804
  served() {
151507
152805
  const { row, twin } = this.input;
151508
152806
  const timing = estimateVideoTiming({ timestampsUs: this.timestampsUs });
151509
- const durationSeconds = timing.fpsSource === "timestamps" ? timing.durationSeconds : null;
152807
+ const durationSeconds = this.timestampsBroken || timing.fpsSource !== "timestamps" ? null : timing.durationSeconds;
151510
152808
  const verdict = judgeClipDuration({
151511
152809
  expectedMs: row.endMs - row.startMs,
151512
152810
  decodedSeconds: durationSeconds,
@@ -151524,7 +152822,9 @@ var ClipStreamRun = class {
151524
152822
  expectedMs: row.endMs - row.startMs,
151525
152823
  fps: this.rate.fps,
151526
152824
  fpsSource: this.rate.fpsSource,
151527
- videoType: this.videoType
152825
+ videoType: this.videoType,
152826
+ residueAccessUnits: this.residueUnits,
152827
+ timestampsBroken: this.timestampsBroken
151528
152828
  };
151529
152829
  if (verdict.kind === "short") this.deps.logger.warn("videoclips: a clip stream came up short against its row", {
151530
152830
  tags: this.tags,
@@ -152506,11 +153806,11 @@ function enumerateWallClockDays(sinceMs, untilMs, timeZone) {
152506
153806
  * nowhere). Searching for the KEY rather than the path survives both, and
152507
153807
  * survives the next firmware that nests it differently.
152508
153808
  */
152509
- function findRecordWithKey$1(node, key) {
153809
+ function findRecordWithKey$2(node, key) {
152510
153810
  if (node === null || typeof node !== "object") return null;
152511
153811
  if (Array.isArray(node)) {
152512
153812
  for (const child of node) {
152513
- const found = findRecordWithKey$1(child, key);
153813
+ const found = findRecordWithKey$2(child, key);
152514
153814
  if (found !== null) return found;
152515
153815
  }
152516
153816
  return null;
@@ -152518,7 +153818,7 @@ function findRecordWithKey$1(node, key) {
152518
153818
  const record = { ...node };
152519
153819
  if (key in record) return record;
152520
153820
  for (const value of Object.values(record)) {
152521
- const found = findRecordWithKey$1(value, key);
153821
+ const found = findRecordWithKey$2(value, key);
152522
153822
  if (found !== null) return found;
152523
153823
  }
152524
153824
  return null;
@@ -152538,7 +153838,7 @@ async function readStorageState(api) {
152538
153838
  reason: err instanceof Error ? err.message : String(err)
152539
153839
  };
152540
153840
  }
152541
- const record = findRecordWithKey$1(raw, "mount");
153841
+ const record = findRecordWithKey$2(raw, "mount");
152542
153842
  if (record === null) return { state: "unknown" };
152543
153843
  const mount = asNumber(record["mount"]);
152544
153844
  if (mount === null) return { state: "unknown" };
@@ -155127,6 +156427,313 @@ var reolinkDebugActions = defineCustomActions({ debugRawRead: customAction(RawRe
155127
156427
  auth: "admin"
155128
156428
  }) });
155129
156429
  //#endregion
156430
+ //#region src/recording-onboard-mapping.ts
156431
+ /**
156432
+ * Reolink's schedule `type` → the cap's trigger vocabulary.
156433
+ *
156434
+ * `legacy` is deliberately absent: it appears on 618 and names nothing
156435
+ * an operator would recognise, so a window carrying it is COUNTED as
156436
+ * unread rather than mapped onto a guess.
156437
+ */
156438
+ var TRIGGER_BY_TYPE = {
156439
+ MD: "motion",
156440
+ Normal: "continuous",
156441
+ people: "person",
156442
+ vehicle: "vehicle",
156443
+ dog_cat: "animal",
156444
+ crossline: "lineCrossing",
156445
+ intrude: "intrusion",
156446
+ loitering: "loitering"
156447
+ };
156448
+ /** Triggers a Reolink camera's schedule can name. */
156449
+ var REOLINK_TRIGGERS = [
156450
+ "continuous",
156451
+ "motion",
156452
+ "person",
156453
+ "vehicle",
156454
+ "animal",
156455
+ "lineCrossing",
156456
+ "intrusion",
156457
+ "loitering"
156458
+ ];
156459
+ /**
156460
+ * Find the first record anywhere in the reply that carries `key`.
156461
+ *
156462
+ * Structural, not path-based, because the library's declared shape for
156463
+ * this reply is wrong and a path written against it finds nothing.
156464
+ */
156465
+ function findRecordWithKey$1(node, key) {
156466
+ if (typeof node !== "object" || node === null) return null;
156467
+ if (Array.isArray(node)) {
156468
+ for (const item of node) {
156469
+ const found = findRecordWithKey$1(item, key);
156470
+ if (found) return found;
156471
+ }
156472
+ return null;
156473
+ }
156474
+ const record = { ...node };
156475
+ if (key in record) return record;
156476
+ for (const value of Object.values(record)) {
156477
+ const found = findRecordWithKey$1(value, key);
156478
+ if (found) return found;
156479
+ }
156480
+ return null;
156481
+ }
156482
+ function finiteNumber(value) {
156483
+ if (typeof value !== "number") return null;
156484
+ return Number.isFinite(value) ? value : null;
156485
+ }
156486
+ /**
156487
+ * Total size in MB from Reolink's split pair.
156488
+ *
156489
+ * Null when the camera stated NEITHER half — a size we did not read is
156490
+ * not a size of zero (D393). A stated 0 GB with a stated MB remainder is
156491
+ * a real, small number, and survives as one.
156492
+ */
156493
+ function megabytes(whole, remainder) {
156494
+ const gb = finiteNumber(whole);
156495
+ const mb = finiteNumber(remainder);
156496
+ if (gb === null && mb === null) return null;
156497
+ return (gb ?? 0) * 1024 + (mb ?? 0);
156498
+ }
156499
+ /**
156500
+ * Read `getHddInfoList`.
156501
+ *
156502
+ * `mount === 0` is a camera with no card — the same signal
156503
+ * `clips/clip-catalog.ts` keys off. `format === 0` is a card present but
156504
+ * not usable, which is `absent` with a reason that says so rather than
156505
+ * `present` with a card nothing can be written to.
156506
+ */
156507
+ function parseOnboardStorage(reply) {
156508
+ const record = findRecordWithKey$1(reply, "mount");
156509
+ if (!record) return {
156510
+ kind: "unknown",
156511
+ reason: "The camera answered, but with nothing this reader recognised as storage info."
156512
+ };
156513
+ if (finiteNumber(record.mount) === 0) return {
156514
+ kind: "absent",
156515
+ reason: "The camera reports no mounted storage (HddInfo mount 0)."
156516
+ };
156517
+ const formatted = finiteNumber(record.format);
156518
+ const id = finiteNumber(record.number);
156519
+ const volume = {
156520
+ id: id === null ? "0" : String(id),
156521
+ status: formatted === 0 ? "unformatted" : "ok",
156522
+ capacityMb: megabytes(record.capacity, record.capacityM),
156523
+ freeMb: megabytes(record.remainSize, record.remainSizeM)
156524
+ };
156525
+ if (formatted === 0) return {
156526
+ kind: "absent",
156527
+ reason: "The camera reports a card that is not formatted, so it cannot record to it."
156528
+ };
156529
+ return {
156530
+ kind: "present",
156531
+ volumes: [volume]
156532
+ };
156533
+ }
156534
+ /**
156535
+ * Expand every `typeScheduleList` entry into windows.
156536
+ *
156537
+ * Entries sharing a trigger are merged slot-wise before expansion, so
156538
+ * three `crossline` rules produce one set of line-crossing windows
156539
+ * rather than three overlapping copies.
156540
+ *
156541
+ * An entry whose `type` is unknown, or whose table is not 168 characters,
156542
+ * is counted as unread. A table of the wrong length is not a schedule of
156543
+ * fewer days.
156544
+ */
156545
+ function parseSchedule(schedule) {
156546
+ const entries = schedule?.typeScheduleList ?? [];
156547
+ const merged = /* @__PURE__ */ new Map();
156548
+ let unreadable = 0;
156549
+ for (const entry of entries) {
156550
+ const trigger = TRIGGER_BY_TYPE[entry.type];
156551
+ if (trigger === void 0 || entry.valueTable.length !== 168) {
156552
+ if (entry.valueTable.includes("1")) unreadable += 1;
156553
+ continue;
156554
+ }
156555
+ const existing = merged.get(trigger);
156556
+ if (!existing) {
156557
+ merged.set(trigger, [...entry.valueTable]);
156558
+ continue;
156559
+ }
156560
+ for (let i = 0; i < existing.length; i += 1) if (entry.valueTable[i] === "1") existing[i] = "1";
156561
+ }
156562
+ const windows = [];
156563
+ for (const [trigger, slots] of merged) windows.push(...weeklyMaskToWindows(slots.join(""), trigger));
156564
+ return {
156565
+ windows,
156566
+ unreadable
156567
+ };
156568
+ }
156569
+ /**
156570
+ * Fold the three reads into the cap's status.
156571
+ *
156572
+ * `tracks` is empty and `primaryTrackId` null on purpose: a Reolink
156573
+ * channel exposes no track list, and inventing one synthetic row would
156574
+ * put a fact in the UI that the camera never stated. The form renders
156575
+ * "the camera reports no recording track", which is true.
156576
+ */
156577
+ function buildOnboardStatus(input) {
156578
+ const { windows, unreadable } = parseSchedule(input.schedule);
156579
+ const masterOn = input.schedule?.enable ?? null;
156580
+ const scheduled = masterOn === true && windows.length > 0;
156581
+ const cycle = input.cfg?.cycle;
156582
+ return {
156583
+ storage: input.storage,
156584
+ tracks: [],
156585
+ primaryTrackId: null,
156586
+ enabled: masterOn,
156587
+ overwriteWhenFull: cycle === void 0 ? null : cycle !== 0,
156588
+ preRecordSec: input.cfg?.preRecordTime ?? null,
156589
+ postRecordSec: input.cfg?.recordDelayTime ?? null,
156590
+ segmentMinutes: input.cfg?.packageTime ?? null,
156591
+ windows: [...windows],
156592
+ unreadableWindows: unreadable,
156593
+ recordingToNowhere: scheduled && input.storage.kind === "absent",
156594
+ lastFetchedAt: input.now
156595
+ };
156596
+ }
156597
+ /** Why a Reolink camera cannot be written until CamStack has read it. */
156598
+ var REOLINK_NOT_READ_REASON = "CamStack has not read this camera yet";
156599
+ /**
156600
+ * Why the schedule is unwritable when one trigger occupies several rows.
156601
+ *
156602
+ * An E1 Outdoor Pro lists `crossline`, `intrude` and `loitering` three times
156603
+ * each — one row per configured rule. The status READ merges them, because
156604
+ * the cap's question is "does this camera record on line-crossing". A write
156605
+ * cannot un-merge them: addressed by type alone it rewrites all three rules
156606
+ * with one mask (measured — `replaceItemValueTable` matches every row of a
156607
+ * type when no index is given), and the cap's patch carries no room to say
156608
+ * which rule an operator meant. So the schedule is REPORTED unwritable on
156609
+ * such a camera rather than writable and destructive.
156610
+ */
156611
+ 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";
156612
+ /** How many rows each trigger occupies, from the camera's own list. */
156613
+ function rowsPerTrigger(schedule) {
156614
+ const counts = /* @__PURE__ */ new Map();
156615
+ for (const entry of schedule?.typeScheduleList ?? []) {
156616
+ const trigger = TRIGGER_BY_TYPE[entry.type];
156617
+ if (trigger === void 0) continue;
156618
+ counts.set(trigger, (counts.get(trigger) ?? 0) + 1);
156619
+ }
156620
+ return counts;
156621
+ }
156622
+ /**
156623
+ * What THIS Reolink accepts.
156624
+ *
156625
+ * Writability is decided per field from the camera's OWN statement, never
156626
+ * from a vendor default:
156627
+ *
156628
+ * - `overwriteWhenFull` needs `<cyclelist>` to offer both 0 and 1; the
156629
+ * cap's field is a boolean and a camera offering one value cannot be
156630
+ * set both ways. `<cyclelist>` IS enforced by the firmware (writing 7
156631
+ * was answered 200 and dropped), so it is a real refusal.
156632
+ * - `preRecordSec` is a two-member SET, not a span. The wire field is
156633
+ * seconds and the firmware honours exactly two states: 0 is off, and
156634
+ * 5, 15 and 30 all read back as 10 — identically on two firmwares. A
156635
+ * slider here would move and change nothing.
156636
+ * - `postRecordSec` carries NO allowed set although the camera states
156637
+ * `<timeList>`: that list is advisory, and a Home Hub offering
156638
+ * `8,15,30` accepted and applied 20. Refusing against it would block a
156639
+ * value the firmware honours, so the write goes through unconstrained
156640
+ * and the read-back diff is what reports a value the camera dropped.
156641
+ * - `segmentMinutes` likewise carries no bound, because no camera states
156642
+ * one and an invented maximum refuses values the firmware may take.
156643
+ */
156644
+ function buildOnboardOptions(input) {
156645
+ const reached = input.status !== null;
156646
+ const notRead = {
156647
+ readable: false,
156648
+ writable: false,
156649
+ reason: REOLINK_NOT_READ_REASON
156650
+ };
156651
+ const writable = {
156652
+ readable: true,
156653
+ writable: true
156654
+ };
156655
+ const refuse = (reason) => ({
156656
+ readable: true,
156657
+ writable: false,
156658
+ reason
156659
+ });
156660
+ if (!reached || input.limits === null) return {
156661
+ enabled: notRead,
156662
+ overwriteWhenFull: notRead,
156663
+ preRecordSec: notRead,
156664
+ postRecordSec: notRead,
156665
+ segmentMinutes: notRead,
156666
+ schedule: {
156667
+ support: notRead,
156668
+ granularityMinutes: 60,
156669
+ triggers: [...REOLINK_TRIGGERS],
156670
+ supportsOverlappingTriggers: true
156671
+ }
156672
+ };
156673
+ const limits = input.limits;
156674
+ const cycleChoices = limits.cycleList;
156675
+ 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`);
156676
+ const preRecord = limits.preRecordSupported ? writable : refuse("this camera reports no pre-record setting, so writing one would do nothing");
156677
+ const merged = [...rowsPerTrigger(input.schedule).entries()].filter(([, count]) => count > 1);
156678
+ const unreadable = input.status?.unreadableWindows ?? 0;
156679
+ 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;
156680
+ return {
156681
+ enabled: writable,
156682
+ overwriteWhenFull: overwrite,
156683
+ preRecordSec: preRecord,
156684
+ ...limits.preRecordSupported ? { preRecordSecAllowed: { values: [0, limits.preRecordSeconds ?? 10] } } : {},
156685
+ postRecordSec: writable,
156686
+ segmentMinutes: writable,
156687
+ schedule: {
156688
+ support: scheduleSupport,
156689
+ granularityMinutes: 60,
156690
+ triggers: [...REOLINK_TRIGGERS],
156691
+ supportsOverlappingTriggers: true
156692
+ }
156693
+ };
156694
+ }
156695
+ /** The cap's trigger vocabulary → Reolink's schedule `type`. */
156696
+ var TYPE_BY_TRIGGER = Object.fromEntries(Object.entries(TRIGGER_BY_TYPE).filter((pair) => pair[1] !== void 0).map(([type, trigger]) => [trigger, type]));
156697
+ /**
156698
+ * Turn a cap patch into the two Baichuan writes.
156699
+ *
156700
+ * `windows` is the COMPLETE new set, so every trigger the camera lists gets
156701
+ * a row — including one the new set does not mention, which must be written
156702
+ * as an all-zero mask or the camera keeps recording on a trigger the
156703
+ * operator just removed. A trigger the camera does NOT list is skipped: a
156704
+ * row the camera never had is answered 200 and discarded, so sending it
156705
+ * would report success for nothing.
156706
+ *
156707
+ * `preRecordSec` collapses to the boolean the firmware really has: any
156708
+ * non-zero ask is "on", because 5, 15 and 30 all store as 10.
156709
+ */
156710
+ function buildOnboardWrites(patch, schedule) {
156711
+ const cfg = {};
156712
+ if (patch.overwriteWhenFull !== void 0) cfg.cycle = patch.overwriteWhenFull ? 1 : 0;
156713
+ if (patch.postRecordSec !== void 0) cfg.recordDelayTime = patch.postRecordSec;
156714
+ if (patch.preRecordSec !== void 0) cfg.preRecordEnabled = patch.preRecordSec !== 0;
156715
+ if (patch.segmentMinutes !== void 0) cfg.packageTime = patch.segmentMinutes;
156716
+ const scheduleOut = {};
156717
+ if (patch.enabled !== void 0) scheduleOut.enable = patch.enabled;
156718
+ if (patch.windows !== void 0) {
156719
+ const present = new Set((schedule?.typeScheduleList ?? []).map((e) => e.type));
156720
+ const entries = [];
156721
+ for (const trigger of REOLINK_TRIGGERS) {
156722
+ const type = TYPE_BY_TRIGGER[trigger];
156723
+ if (type === void 0 || !present.has(type)) continue;
156724
+ entries.push({
156725
+ type,
156726
+ valueTable: windowsToWeeklyMask(patch.windows, trigger)
156727
+ });
156728
+ }
156729
+ scheduleOut.entries = entries;
156730
+ }
156731
+ return {
156732
+ cfg: Object.keys(cfg).length > 0 ? cfg : null,
156733
+ schedule: Object.keys(scheduleOut).length > 0 ? scheduleOut : null
156734
+ };
156735
+ }
156736
+ //#endregion
155130
156737
  //#region src/battery-charging.ts
155131
156738
  /**
155132
156739
  * Da `adapterStatus` + `chargeStatus` alla sorgente di alimentazione.
@@ -155623,6 +157230,82 @@ function decideSnapshotCameraAccess(input) {
155623
157230
  };
155624
157231
  }
155625
157232
  //#endregion
157233
+ //#region src/sleep-state-decision.ts
157234
+ /**
157235
+ * Hysteresis is ASYMMETRIC, because the two directions have very different
157236
+ * costs when we get them wrong.
157237
+ *
157238
+ * `→ awake` (`WAKE_HYSTERESIS_MS`): expensive if wrong. Believing an asleep
157239
+ * camera is awake un-gates every background probe and makes `wakeForStream`
157240
+ * early-out without actually waking. Keep the window >= the library's UDP
157241
+ * inference cycle (~12 s on battery cams with idle-disconnect) and >= its
157242
+ * `getIdleDisconnectTimeoutMs` (30 s default).
157243
+ *
157244
+ * `→ sleeping` (`SLEEP_HYSTERESIS_MS`): cheap if wrong. Believing an awake
157245
+ * camera is asleep only means we skip background probes and serve cached
157246
+ * values; the demand paths still drive a real wake. The old symmetric 30 s
157247
+ * window was LONGER than the camera's own awake window (~14 s), so a natural
157248
+ * wake→sleep cycle had its `sleeping` transition dropped and the slice stayed
157249
+ * stuck on "awake" while the camera slept.
157250
+ */
157251
+ var WAKE_HYSTERESIS_MS = 3e4;
157252
+ /** See {@link WAKE_HYSTERESIS_MS}. */
157253
+ var SLEEP_HYSTERESIS_MS = 5e3;
157254
+ /**
157255
+ * How long a freshly committed FIRST-HAND state is protected from a lagging
157256
+ * summary's awake→sleeping flip. The summary lags real pushes by over a
157257
+ * minute (measured >75 s on 2026-08-16); 3 min covers two full
157258
+ * discovery-refresh cycles of lag without letting a genuinely stuck "awake"
157259
+ * slice stand forever — the reconcile still wins after the grace, which is
157260
+ * exactly its recovery role.
157261
+ */
157262
+ var HUB_SUMMARY_SLEEP_GRACE_MS = 18e4;
157263
+ /** A source whose verdict comes from the parent NVR's channel summary. */
157264
+ function isSummarySourced(source) {
157265
+ return source === "hub-summary" || source === "hub-reconcile";
157266
+ }
157267
+ /**
157268
+ * Decide one commit. Pure: the caller owns the slice, the clock and the log.
157269
+ *
157270
+ * Order of the gates, and why:
157271
+ * 1. No-op first — re-asserting the current value is never a transition.
157272
+ * 2. A summary-sourced verdict is firmware state, not socket inference, so
157273
+ * the anti-flap window does not apply to it at all.
157274
+ * 3. A `confirmed-wake` in the `→ awake` direction is first-hand evidence
157275
+ * this process produced: gating it with the anti-flap window is what
157276
+ * stranded 3825's mirror on "asleep" while `wakeForStream` answered
157277
+ * `awoke: true`.
157278
+ * 4. Everything else is subject to the asymmetric hysteresis.
157279
+ * 5. A summary flip toward `sleeping` is refused while a first-hand
157280
+ * observation inside the grace says otherwise.
157281
+ */
157282
+ function decideSleepCommit(input) {
157283
+ const { current, next, source, lastFirstHandCommitAt, nowMs } = input;
157284
+ if (current === next) return { kind: "unchanged" };
157285
+ const summarySourced = isSummarySourced(source);
157286
+ const firstHandWake = source === "confirmed-wake" && next === false;
157287
+ const sinceLastChangeMs = nowMs - lastFirstHandCommitAt;
157288
+ if (!summarySourced && !firstHandWake) {
157289
+ const windowMs = next ? SLEEP_HYSTERESIS_MS : WAKE_HYSTERESIS_MS;
157290
+ if (lastFirstHandCommitAt > 0 && sinceLastChangeMs < windowMs) return {
157291
+ kind: "refused",
157292
+ reason: "hysteresis",
157293
+ windowMs,
157294
+ sinceLastChangeMs
157295
+ };
157296
+ }
157297
+ if (summarySourced && next === true && lastFirstHandCommitAt > 0 && sinceLastChangeMs < 18e4) return {
157298
+ kind: "refused",
157299
+ reason: "summary-grace",
157300
+ windowMs: HUB_SUMMARY_SLEEP_GRACE_MS,
157301
+ sinceLastChangeMs
157302
+ };
157303
+ return {
157304
+ kind: "commit",
157305
+ stampFirstHand: source !== "hub-reconcile"
157306
+ };
157307
+ }
157308
+ //#endregion
155626
157309
  //#region src/raw-state.ts
155627
157310
  /**
155628
157311
  * Source tag for every raw-state blob this provider emits.
@@ -158869,53 +160552,20 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
158869
160552
  return this.state.battery.sleeping ?? false;
158870
160553
  }
158871
160554
  /**
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.
160555
+ * Wall-clock ms when a FIRST-HAND observation last moved `this.sleeping`.
160556
+ *
160557
+ * It drives two windows — the anti-flap hysteresis that filters the lib's
160558
+ * UDP sleep inference, and the grace that stops the parent's LAGGING
160559
+ * summary from clobbering a fresher verdict. Both want the same thing:
160560
+ * "when did something that was actually looking at the camera last speak".
160561
+ *
160562
+ * The parent's periodic summary sweep (`hub-reconcile`) therefore never
160563
+ * stamps this. It is firmware truth, but it is minutes old, and stamping
160564
+ * it armed the 30 s wake window against the camera's own wake push — see
160565
+ * `sleep-state-decision.ts` for the measurement (D603).
158880
160566
  */
158881
160567
  sleepStateChangedAt = 0;
158882
160568
  /**
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
160569
  * Wall-clock ms of the last PROACTIVE wake — one we issued on our own
158920
160570
  * initiative (background snapshot refresh), not because the operator
158921
160571
  * or a stream consumer asked for the camera. Drives
@@ -159830,6 +161480,32 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
159830
161480
  await (await this.ensureApi()).reboot(this.getChannel());
159831
161481
  return { success: true };
159832
161482
  } });
161483
+ this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
161484
+ if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
161485
+ const host = this.config.get("host") ?? "";
161486
+ const built = buildDeviceAdminUrl({
161487
+ host,
161488
+ port: null,
161489
+ scheme: "http",
161490
+ path: "/"
161491
+ });
161492
+ if (!built.ok) {
161493
+ this.ctx.logger.debug("reolink admin link unavailable", {
161494
+ tags: { deviceId: this.id },
161495
+ meta: {
161496
+ reason: built.reason,
161497
+ transport: this.config.get("transport") ?? "tcp"
161498
+ }
161499
+ });
161500
+ return null;
161501
+ }
161502
+ return {
161503
+ url: built.url,
161504
+ label: "Web UI",
161505
+ target: "device",
161506
+ host
161507
+ };
161508
+ } });
159833
161509
  this.registerNetworkLink();
159834
161510
  this.ctx.registerNativeCap(motionCapability, { isDetected: async ({ deviceId }) => {
159835
161511
  if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
@@ -159876,6 +161552,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
159876
161552
  this.registerPrivacyMaskCap();
159877
161553
  this.registerDayNightCap();
159878
161554
  this.registerImageSettingsCap();
161555
+ this.registerRecordingOnboardCap();
159879
161556
  this.registerStreamCatalogProvider();
159880
161557
  this.registerNativeObjectDetectionCap();
159881
161558
  const cache = this.config.get("deviceCache");
@@ -159919,6 +161596,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
159919
161596
  dayNightRefreshInFlight = null;
159920
161597
  /** Single-flight guard for the `image-settings` cap refresh. */
159921
161598
  imageSettingsRefreshInFlight = null;
161599
+ /** Single-flight guard for the `recording-onboard` cap refresh. */
161600
+ recordingOnboardRefreshInFlight = null;
159922
161601
  /**
159923
161602
  * Single-flight guard for snapshot fetches (Slice 10). When two
159924
161603
  * consumers hit `getSnapshot` concurrently we issue ONE Baichuan
@@ -160120,35 +161799,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
160120
161799
  }
160121
161800
  }
160122
161801
  /**
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
161802
  * THE single writer of the `battery.sleeping` slice.
160153
161803
  *
160154
161804
  * Before this existed the state had three independent writers with
@@ -160159,38 +161809,38 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
160159
161809
  * ancient timestamp). Funnelling every writer through here keeps one
160160
161810
  * rule set and one timestamp.
160161
161811
  *
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).
161812
+ * The rule itself lives in `sleep-state-decision.ts`, pure, so it can be
161813
+ * driven by a test instead of asserted from this file's source text. Read
161814
+ * that module for why `hub-reconcile` — the parent's periodic summary
161815
+ * sweep — stamps nothing: it lags real state by more than a minute, and
161816
+ * arming the anti-flap window from it threw away the camera's real wake
161817
+ * for up to 2 min 26 s on the live hub (D603).
161818
+ *
160173
161819
  * @returns `true` when the slice actually changed.
160174
161820
  */
160175
161821
  commitSleepState(next, source) {
160176
161822
  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();
161823
+ const decision = decideSleepCommit({
161824
+ current: this.sleeping,
161825
+ next,
161826
+ source,
161827
+ lastFirstHandCommitAt: this.sleepStateChangedAt,
161828
+ nowMs: Date.now()
161829
+ });
161830
+ if (decision.kind === "unchanged") return false;
161831
+ if (decision.kind === "refused") {
161832
+ 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", {
161833
+ tags: { deviceId: this.id },
161834
+ meta: {
161835
+ next,
161836
+ source,
161837
+ windowMs: decision.windowMs,
161838
+ sinceLastChangeMs: decision.sinceLastChangeMs
161839
+ }
161840
+ });
161841
+ return false;
160193
161842
  }
161843
+ if (decision.stampFirstHand) this.sleepStateChangedAt = Date.now();
160194
161844
  this.state.battery.sleeping = next;
160195
161845
  this.ctx.logger.info("battery sleep state committed", {
160196
161846
  tags: { deviceId: this.id },
@@ -160217,7 +161867,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
160217
161867
  */
160218
161868
  applyHubSleepState(sleeping) {
160219
161869
  if (!this.isBattery) return;
160220
- if (this.commitSleepState(sleeping, "hub-summary") && !sleeping) this.onWakeTransition("hub-summary").catch(() => {});
161870
+ if (this.commitSleepState(sleeping, "hub-reconcile") && !sleeping) this.onWakeTransition("hub-reconcile").catch(() => {});
160221
161871
  }
160222
161872
  /**
160223
161873
  * Rate-limit for wakes we issue on our OWN initiative. Returns
@@ -161914,6 +163564,173 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
161914
163564
  * denormalized against the camera's OWN probed threshold range
161915
163565
  * (never a hardcoded 0..255 guess).
161916
163566
  */
163567
+ /**
163568
+ * Register the vendor-neutral `recording-onboard` cap.
163569
+ *
163570
+ * READ-ONLY on Reolink, and it says so per field. `@apocaliss92/
163571
+ * nodelink-js` exposes a reader for the record config and the record
163572
+ * schedule and no writer; the Baichuan SET command ids exist but
163573
+ * nothing in this repo has ever sent them, and the only route is
163574
+ * hand-built XML through the generic passthrough. So `getOptions`
163575
+ * reports every field `writable: false` with
163576
+ * {@link REOLINK_NO_WRITE_REASON}, the UI renders each control
163577
+ * disabled WITH that sentence — disabled must not look like broken —
163578
+ * and `setSettings` refuses by name instead of accepting a write and
163579
+ * dropping it.
163580
+ *
163581
+ * The cap's FIELD SET is identical to Hikvision's. Only the answers
163582
+ * differ.
163583
+ */
163584
+ registerRecordingOnboardCap() {
163585
+ const channel = this.getChannel();
163586
+ const CAP_NAME = "recording-onboard";
163587
+ const emptyStatus = () => ({
163588
+ storage: {
163589
+ kind: "unknown",
163590
+ reason: "CamStack has not read this camera yet."
163591
+ },
163592
+ tracks: [],
163593
+ primaryTrackId: null,
163594
+ enabled: null,
163595
+ overwriteWhenFull: null,
163596
+ preRecordSec: null,
163597
+ postRecordSec: null,
163598
+ segmentMinutes: null,
163599
+ windows: [],
163600
+ unreadableWindows: 0,
163601
+ recordingToNowhere: false,
163602
+ lastFetchedAt: 0
163603
+ });
163604
+ /** Last `getRecordCfg` reply. */
163605
+ let lastCfg = null;
163606
+ /** Last `getRecordCfgLimits` — what the camera says it accepts. */
163607
+ let lastLimits = null;
163608
+ /** Last schedule read — which trigger rows this camera actually has. */
163609
+ let lastSchedule = null;
163610
+ const refreshFromCamera = async () => {
163611
+ if (this.recordingOnboardRefreshInFlight) return this.recordingOnboardRefreshInFlight;
163612
+ const promise = (async () => {
163613
+ try {
163614
+ const api = await this.ensureApi();
163615
+ const [cfg, limits, schedule, hdd] = await Promise.all([
163616
+ api.getRecordCfg(channel).catch(() => null),
163617
+ api.getRecordCfgLimits(channel).catch(() => null),
163618
+ api.getRecordSchedule(channel).catch(() => null),
163619
+ this.isHubChild() ? Promise.resolve(null) : api.getHddInfoList().catch(() => null)
163620
+ ]);
163621
+ lastCfg = cfg;
163622
+ lastLimits = limits;
163623
+ lastSchedule = schedule;
163624
+ const next = buildOnboardStatus({
163625
+ storage: this.isHubChild() ? {
163626
+ kind: "unknown",
163627
+ reason: "This camera hangs off a Reolink Hub, so its footage lives on the Hub’s disk, not on a card of its own."
163628
+ } : hdd === null ? {
163629
+ kind: "unknown",
163630
+ reason: "The camera did not answer the storage read."
163631
+ } : parseOnboardStorage(hdd),
163632
+ cfg,
163633
+ schedule,
163634
+ now: Date.now()
163635
+ });
163636
+ this.runtimeState.setCapState(CAP_NAME, next);
163637
+ if (next.recordingToNowhere) this.ctx.logger.warn("reolink onboard recording: camera is scheduled to record and has no usable storage", {
163638
+ tags: { deviceId: this.id },
163639
+ meta: { windows: next.windows.length }
163640
+ });
163641
+ if (next.unreadableWindows > 0) this.ctx.logger.warn("reolink onboard recording: some schedule tables could not be read — the schedule shown is incomplete", {
163642
+ tags: { deviceId: this.id },
163643
+ meta: {
163644
+ unreadableWindows: next.unreadableWindows,
163645
+ readWindows: next.windows.length
163646
+ }
163647
+ });
163648
+ } catch (err) {
163649
+ this.ctx.logger.debug("reolink onboard recording refresh failed — keeping last slice", {
163650
+ tags: { deviceId: this.id },
163651
+ meta: { error: err instanceof Error ? err.message : String(err) }
163652
+ });
163653
+ }
163654
+ })();
163655
+ this.recordingOnboardRefreshInFlight = promise;
163656
+ try {
163657
+ await promise;
163658
+ } finally {
163659
+ this.recordingOnboardRefreshInFlight = null;
163660
+ }
163661
+ };
163662
+ const bridge = createRuntimeStateBridge({
163663
+ runtimeState: this.runtimeState,
163664
+ cap: recordingOnboardCapability,
163665
+ ownDeviceId: this.id,
163666
+ refresh: this.sleepGatedRefresh(CAP_NAME, refreshFromCamera),
163667
+ staleMs: OPERATOR_WRITTEN_STALE_MS,
163668
+ staleRead: "serve-and-revalidate",
163669
+ empty: emptyStatus
163670
+ });
163671
+ const readOptions = () => buildOnboardOptions({
163672
+ status: this.runtimeState.getCapState(CAP_NAME) ?? null,
163673
+ cfg: lastCfg,
163674
+ limits: lastLimits,
163675
+ schedule: lastSchedule
163676
+ });
163677
+ const provider = {
163678
+ getStatus: bridge.getStatus,
163679
+ getOptions: async ({ deviceId }) => {
163680
+ if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
163681
+ await bridge.ensureFresh();
163682
+ return readOptions();
163683
+ },
163684
+ setSettings: async ({ deviceId, settings }) => {
163685
+ if (deviceId !== this.id) return;
163686
+ if (Object.keys(settings).length === 0) return;
163687
+ await bridge.ensureFresh();
163688
+ const refusal = describeOnboardRefusal(readOptions(), settings);
163689
+ if (refusal !== null) {
163690
+ this.ctx.logger.warn("reolink onboard recording: refusing a write this camera cannot take", {
163691
+ tags: { deviceId: this.id },
163692
+ meta: {
163693
+ refusal,
163694
+ fields: Object.keys(settings)
163695
+ }
163696
+ });
163697
+ throw new Error(`device ${String(this.id)}: ${refusal}`);
163698
+ }
163699
+ const writes = buildOnboardWrites(settings, lastSchedule);
163700
+ const api = await this.ensureApi();
163701
+ const ignored = [];
163702
+ if (writes.cfg !== null) {
163703
+ const result = await api.setRecordCfg(channel, writes.cfg);
163704
+ for (const outcome of result.outcomes) if (outcome.status === "ignored") ignored.push(`${outcome.field}: asked ${outcome.requested}, camera kept ${outcome.actual}`);
163705
+ }
163706
+ if (writes.schedule !== null) {
163707
+ const result = await api.setRecordSchedule(channel, writes.schedule);
163708
+ for (const outcome of result.outcomes) if (outcome.status === "ignored") ignored.push(`schedule ${outcome.field}: the camera kept its own value`);
163709
+ }
163710
+ await refreshFromCamera();
163711
+ if (ignored.length > 0) {
163712
+ this.ctx.logger.warn("reolink onboard recording: the camera answered 200 and discarded part of the write", {
163713
+ tags: { deviceId: this.id },
163714
+ meta: {
163715
+ ignored,
163716
+ fields: Object.keys(settings)
163717
+ }
163718
+ });
163719
+ throw new Error(`device ${String(this.id)}: the camera accepted the request and kept its own values — ${ignored.join("; ")}`);
163720
+ }
163721
+ this.ctx.logger.info("reolink onboard recording: write applied", {
163722
+ tags: { deviceId: this.id },
163723
+ meta: { fields: Object.keys(settings) }
163724
+ });
163725
+ }
163726
+ };
163727
+ this.registerCapWarmer(CAP_NAME, async () => {
163728
+ await provider.getOptions({ deviceId: this.id });
163729
+ await refreshFromCamera();
163730
+ });
163731
+ this.ctx.registerNativeCap(recordingOnboardCapability, provider);
163732
+ this.ctx.logger.info("Reolink recording-onboard cap registered", { tags: { deviceId: this.id } });
163733
+ }
161917
163734
  registerDayNightCap() {
161918
163735
  const channel = this.getChannel();
161919
163736
  const CAP_NAME = "day-night";
@@ -166343,6 +168160,29 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
166343
168160
  await (await this.ensureApi()).reboot();
166344
168161
  return { success: true };
166345
168162
  } });
168163
+ this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
168164
+ if (deviceId !== this.id) throw new Error(`ReolinkHub: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
168165
+ const host = this.config.get("host") ?? "";
168166
+ const built = buildDeviceAdminUrl({
168167
+ host,
168168
+ port: null,
168169
+ scheme: "http",
168170
+ path: "/"
168171
+ });
168172
+ if (!built.ok) {
168173
+ this.ctx.logger.debug("reolink hub admin link unavailable", {
168174
+ tags: { deviceId: this.id },
168175
+ meta: { reason: built.reason }
168176
+ });
168177
+ return null;
168178
+ }
168179
+ return {
168180
+ url: built.url,
168181
+ label: "Web UI",
168182
+ target: "device",
168183
+ host
168184
+ };
168185
+ } });
166346
168186
  }
166347
168187
  async onActivate() {
166348
168188
  this.ensureApi().catch((err) => {