@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/BaichuanVideoStream-UG3DIMIE-BRbIHCdL.mjs +2 -0
- package/dist/BaichuanVideoStream-UG3DIMIE-DViuQDJ4.js +3 -0
- package/dist/addon.js +2409 -569
- package/dist/addon.mjs +2249 -409
- package/dist/{chunk-ZBACPERX-cDJK1KXE.mjs → chunk-OSVYEFMM-Bl0F4OJc.mjs} +5 -4
- package/dist/{chunk-ZBACPERX-BM6Ll-rh.js → chunk-OSVYEFMM-Czx77gto.js} +10 -3
- package/package.json +5 -2
- package/dist/BaichuanVideoStream-FOE2DBVZ-94yIU75L.mjs +0 -2
- package/dist/BaichuanVideoStream-FOE2DBVZ-DJcEkVak.js +0 -3
- /package/dist/{DiagnosticsTools-ZC7I7QM6-9NV95vRN.mjs → DiagnosticsTools-YIWXO7IJ-9NV95vRN.mjs} +0 -0
- /package/dist/{DiagnosticsTools-ZC7I7QM6-CD4rRF8a.js → DiagnosticsTools-YIWXO7IJ-CD4rRF8a.js} +0 -0
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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])
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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])
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73773
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
74196
|
-
return
|
|
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
|
|
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
|
|
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 ? [
|
|
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-
|
|
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 =
|
|
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 ??
|
|
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(
|
|
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 =
|
|
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(
|
|
78393
|
-
const idx = this.buffer.indexOf(
|
|
78394
|
-
const idxRev = this.buffer.indexOf(
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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([
|
|
81821
|
-
if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([
|
|
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([
|
|
81828
|
-
if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([
|
|
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))
|
|
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 =
|
|
81891
|
-
else dec =
|
|
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))
|
|
81958
|
-
if (this.debugCfg.traceTalk && isTalkCmd(cmdId))
|
|
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))
|
|
81974
|
-
if (this.debugCfg.traceTalk && isTalkCmd(cmdId))
|
|
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))
|
|
82045
|
-
if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4))
|
|
82046
|
-
if (this.debugCfg.traceTalk && isTalkCmd(cmdId))
|
|
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))
|
|
82062
|
-
if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4))
|
|
82063
|
-
if (this.debugCfg.traceTalk && isTalkCmd(cmdId))
|
|
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)
|
|
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
|
-
|
|
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
|
|
82887
|
-
if (enc.kind === "aes" || enc.kind === "full_aes") return
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
82957
|
-
const passHash =
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
83596
|
-
const sps =
|
|
83597
|
-
const pps =
|
|
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(
|
|
83982
|
-
if (frame.videoType === "H265") return
|
|
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 =
|
|
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 (
|
|
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" ?
|
|
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" ?
|
|
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" ?
|
|
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
|
|
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 (
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
93158
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94643
|
+
require_chunk_OSVYEFMM.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using device UID from cmd 114 GetUid: ${viaGetUid}`);
|
|
93619
94644
|
return viaGetUid;
|
|
93620
94645
|
}
|
|
93621
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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:
|
|
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) =>
|
|
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:
|
|
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) =>
|
|
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) =>
|
|
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
|
-
...
|
|
95484
|
+
...pinnedHeaderChannelId != null ? { channelIdOverride: pinnedHeaderChannelId } : {},
|
|
94458
95485
|
msgNumOverride: 0,
|
|
94459
|
-
messageClass:
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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-
|
|
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-
|
|
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
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
151214
|
-
|
|
151215
|
-
|
|
151216
|
-
|
|
151217
|
-
|
|
151218
|
-
|
|
151219
|
-
|
|
151220
|
-
|
|
151221
|
-
|
|
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
|
-
|
|
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
|
|
151240
|
-
this.
|
|
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.
|
|
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.
|
|
151310
|
-
|
|
151311
|
-
|
|
151312
|
-
|
|
151313
|
-
|
|
151314
|
-
|
|
151315
|
-
|
|
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.
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
158873
|
-
*
|
|
158874
|
-
*
|
|
158875
|
-
* UDP
|
|
158876
|
-
*
|
|
158877
|
-
*
|
|
158878
|
-
*
|
|
158879
|
-
*
|
|
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
|
-
*
|
|
160163
|
-
*
|
|
160164
|
-
*
|
|
160165
|
-
*
|
|
160166
|
-
*
|
|
160167
|
-
*
|
|
160168
|
-
*
|
|
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
|
-
|
|
160178
|
-
|
|
160179
|
-
|
|
160180
|
-
|
|
160181
|
-
|
|
160182
|
-
|
|
160183
|
-
|
|
160184
|
-
|
|
160185
|
-
|
|
160186
|
-
|
|
160187
|
-
|
|
160188
|
-
|
|
160189
|
-
|
|
160190
|
-
|
|
160191
|
-
|
|
160192
|
-
|
|
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-
|
|
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) => {
|