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