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