@camstack/addon-provider-reolink 1.1.17 → 1.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon.js +736 -15
- package/dist/addon.mjs +736 -15
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -4655,7 +4655,7 @@ function _instanceof(cls, params = {}) {
|
|
|
4655
4655
|
return inst;
|
|
4656
4656
|
}
|
|
4657
4657
|
//#endregion
|
|
4658
|
-
//#region ../types/dist/sleep-
|
|
4658
|
+
//#region ../types/dist/sleep-CZDdRBua.mjs
|
|
4659
4659
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4660
4660
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4661
4661
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -7453,7 +7453,16 @@ var DecoderStatsSchema = object({
|
|
|
7453
7453
|
inputFps: number(),
|
|
7454
7454
|
outputFps: number(),
|
|
7455
7455
|
avgDecodeTimeMs: number(),
|
|
7456
|
-
droppedFrames: number()
|
|
7456
|
+
droppedFrames: number(),
|
|
7457
|
+
/**
|
|
7458
|
+
* Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
|
|
7459
|
+
* lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
|
|
7460
|
+
* the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
|
|
7461
|
+
* is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
|
|
7462
|
+
*/
|
|
7463
|
+
lagMs: number().optional(),
|
|
7464
|
+
effectiveFps: number().optional(),
|
|
7465
|
+
adaptiveFps: number().optional()
|
|
7457
7466
|
});
|
|
7458
7467
|
var DecoderSessionConfigSchema = object({
|
|
7459
7468
|
codec: string(),
|
|
@@ -7494,7 +7503,15 @@ var DecoderSessionConfigSchema = object({
|
|
|
7494
7503
|
* other — `pullFrames` returns nothing for an `'shm'` session and
|
|
7495
7504
|
* `pullHandles` returns nothing for a `'callback'` session.
|
|
7496
7505
|
*/
|
|
7497
|
-
frameSink: _enum(["callback", "shm"]).default("callback")
|
|
7506
|
+
frameSink: _enum(["callback", "shm"]).default("callback"),
|
|
7507
|
+
/**
|
|
7508
|
+
* Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
|
|
7509
|
+
* a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
|
|
7510
|
+
* real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
|
|
7511
|
+
* stream-broker's `streamingDebug` gate — off by default so production logs
|
|
7512
|
+
* stay quiet and the emit path pays zero per-frame cost when disabled.
|
|
7513
|
+
*/
|
|
7514
|
+
debug: boolean().optional()
|
|
7498
7515
|
});
|
|
7499
7516
|
var EncodeProfileSchema = object({
|
|
7500
7517
|
video: object({
|
|
@@ -10558,6 +10575,100 @@ var coverCapability = {
|
|
|
10558
10575
|
runtimeState: CoverStatusSchema
|
|
10559
10576
|
};
|
|
10560
10577
|
/**
|
|
10578
|
+
* Vendor-neutral day/night (IR-cut) control — the per-camera config cap
|
|
10579
|
+
* shared by reolink / hikvision / amcrest. Models the common firmware
|
|
10580
|
+
* surface: the IR-cut switching MODE plus the two knobs that gate it
|
|
10581
|
+
* (photocell `sensitivity` + `switchDelaySec`). Each vendor maps these
|
|
10582
|
+
* onto its own ISAPI / Baichuan / Dahua-CGI fields; the cap standardises
|
|
10583
|
+
* the shape so ONE derived-form renders every camera.
|
|
10584
|
+
*
|
|
10585
|
+
* Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
|
|
10586
|
+
* `getOptions` advertises per-camera availability, `getStatus` (auto-
|
|
10587
|
+
* injected from `status`) reports the live values, and a single
|
|
10588
|
+
* `setSettings` mutation applies a partial change. No hand-written
|
|
10589
|
+
* settings-contribution methods — the framework derives the UI + save
|
|
10590
|
+
* routing from this surface.
|
|
10591
|
+
*/
|
|
10592
|
+
/** IR-cut switching mode. `schedule` = time-of-day table configured on the camera. */
|
|
10593
|
+
var DayNightModeSchema = _enum([
|
|
10594
|
+
"auto",
|
|
10595
|
+
"day",
|
|
10596
|
+
"night",
|
|
10597
|
+
"schedule"
|
|
10598
|
+
]);
|
|
10599
|
+
/** Normalized numeric range descriptor — `{ min, max, step }` per the
|
|
10600
|
+
* getOptions availability convention. Normalized values are 0–100. */
|
|
10601
|
+
var NormalizedRangeSchema$1 = object({
|
|
10602
|
+
min: number(),
|
|
10603
|
+
max: number(),
|
|
10604
|
+
step: number()
|
|
10605
|
+
});
|
|
10606
|
+
/**
|
|
10607
|
+
* Current day/night state. Optional fields are absent when the camera
|
|
10608
|
+
* does not expose that knob (a photocell-less model reports no
|
|
10609
|
+
* `sensitivity`). `lastFetchedAt` feeds the runtime-state bridge.
|
|
10610
|
+
*/
|
|
10611
|
+
var DayNightStatusSchema = object({
|
|
10612
|
+
mode: DayNightModeSchema,
|
|
10613
|
+
/** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
|
|
10614
|
+
sensitivity: number().optional(),
|
|
10615
|
+
/** Delay before the IR-cut filter flips, in seconds. */
|
|
10616
|
+
switchDelaySec: number().optional(),
|
|
10617
|
+
lastFetchedAt: number()
|
|
10618
|
+
});
|
|
10619
|
+
/**
|
|
10620
|
+
* Per-camera availability descriptor — drives which controls the admin UI
|
|
10621
|
+
* renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
|
|
10622
|
+
* (normalized 0–100); the mode choice-set as an array. A provider returns
|
|
10623
|
+
* honest, camera-probed values — never hardcoded.
|
|
10624
|
+
*/
|
|
10625
|
+
var DayNightOptionsSchema = object({
|
|
10626
|
+
/** Modes this camera accepts. Empty → the camera has no configurable day/night mode. */
|
|
10627
|
+
modes: array(DayNightModeSchema),
|
|
10628
|
+
supportsSensitivity: boolean(),
|
|
10629
|
+
/** Present when `supportsSensitivity` — the normalized 0–100 range. */
|
|
10630
|
+
sensitivity: NormalizedRangeSchema$1.optional(),
|
|
10631
|
+
supportsSwitchDelay: boolean(),
|
|
10632
|
+
/** Present when `supportsSwitchDelay` — the allowed delay range in seconds. */
|
|
10633
|
+
switchDelaySec: NormalizedRangeSchema$1.optional()
|
|
10634
|
+
});
|
|
10635
|
+
/**
|
|
10636
|
+
* Partial change to the day/night config — every field optional. A
|
|
10637
|
+
* provider ignores fields it does not support.
|
|
10638
|
+
*/
|
|
10639
|
+
var DayNightSettingsPatchSchema = object({
|
|
10640
|
+
mode: DayNightModeSchema.optional(),
|
|
10641
|
+
sensitivity: number().optional(),
|
|
10642
|
+
switchDelaySec: number().optional()
|
|
10643
|
+
});
|
|
10644
|
+
var dayNightCapability = {
|
|
10645
|
+
name: "day-night",
|
|
10646
|
+
scope: "device",
|
|
10647
|
+
deviceNative: true,
|
|
10648
|
+
mode: "singleton",
|
|
10649
|
+
deviceTypes: [DeviceType.Camera],
|
|
10650
|
+
deviceConfig: { ui: {
|
|
10651
|
+
kind: "derived-form",
|
|
10652
|
+
builderId: "day-night",
|
|
10653
|
+
tab: "image"
|
|
10654
|
+
} },
|
|
10655
|
+
methods: {
|
|
10656
|
+
getOptions: method(object({ deviceId: number() }), DayNightOptionsSchema),
|
|
10657
|
+
setSettings: method(object({
|
|
10658
|
+
deviceId: number(),
|
|
10659
|
+
settings: DayNightSettingsPatchSchema
|
|
10660
|
+
}), _void(), {
|
|
10661
|
+
kind: "mutation",
|
|
10662
|
+
auth: "admin"
|
|
10663
|
+
})
|
|
10664
|
+
},
|
|
10665
|
+
status: {
|
|
10666
|
+
schema: DayNightStatusSchema,
|
|
10667
|
+
kind: "poll"
|
|
10668
|
+
},
|
|
10669
|
+
runtimeState: DayNightStatusSchema
|
|
10670
|
+
};
|
|
10671
|
+
/**
|
|
10561
10672
|
* Identity envelope for a device's upstream-system metadata.
|
|
10562
10673
|
*
|
|
10563
10674
|
* Two jobs:
|
|
@@ -11216,6 +11327,155 @@ var imageCapability = {
|
|
|
11216
11327
|
runtimeState: ImageStatusSchema
|
|
11217
11328
|
};
|
|
11218
11329
|
/**
|
|
11330
|
+
* Vendor-neutral image / picture-adjustment cap — the per-camera config
|
|
11331
|
+
* cap shared by reolink / hikvision / amcrest. Models the common ISP
|
|
11332
|
+
* surface: the four picture sliders (brightness / contrast / saturation /
|
|
11333
|
+
* sharpness), orientation (mirror / flip / rotate), white-balance,
|
|
11334
|
+
* exposure and backlight-compensation modes.
|
|
11335
|
+
*
|
|
11336
|
+
* NORMALIZATION: every slider is a normalized int 0–100. Vendors expose
|
|
11337
|
+
* these natively as 0–100 or 0–255 (or other ranges); each provider maps
|
|
11338
|
+
* its native range to/from this normalized 0–100 space so the cap surface
|
|
11339
|
+
* (and the derived form) is identical across cameras. `warmth` (manual
|
|
11340
|
+
* white-balance) is likewise normalized 0–100.
|
|
11341
|
+
*
|
|
11342
|
+
* Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
|
|
11343
|
+
* `getOptions` advertises per-camera availability, `getStatus` (auto-
|
|
11344
|
+
* injected from `status`) reports the live values, and a single
|
|
11345
|
+
* `setSettings` mutation applies a partial change. No hand-written
|
|
11346
|
+
* settings-contribution methods — the framework derives the UI + save
|
|
11347
|
+
* routing from this surface.
|
|
11348
|
+
*/
|
|
11349
|
+
/** Sensor/image rotation, degrees clockwise. */
|
|
11350
|
+
var ImageRotateSchema = _enum([
|
|
11351
|
+
"0",
|
|
11352
|
+
"90",
|
|
11353
|
+
"180",
|
|
11354
|
+
"270"
|
|
11355
|
+
]);
|
|
11356
|
+
/** White-balance mode. `manual` unlocks the normalized `warmth` knob. */
|
|
11357
|
+
var WhiteBalanceModeSchema = _enum(["auto", "manual"]);
|
|
11358
|
+
/** Exposure mode. */
|
|
11359
|
+
var ExposureModeSchema = _enum(["auto", "manual"]);
|
|
11360
|
+
/**
|
|
11361
|
+
* Backlight-compensation mode:
|
|
11362
|
+
* - `off` — disabled
|
|
11363
|
+
* - `blc` — backlight compensation
|
|
11364
|
+
* - `wdr` — wide dynamic range
|
|
11365
|
+
* - `hlc` — highlight compensation
|
|
11366
|
+
*/
|
|
11367
|
+
var BacklightModeSchema = _enum([
|
|
11368
|
+
"off",
|
|
11369
|
+
"blc",
|
|
11370
|
+
"wdr",
|
|
11371
|
+
"hlc"
|
|
11372
|
+
]);
|
|
11373
|
+
/** Normalized numeric range descriptor — `{ min, max, step }` per the
|
|
11374
|
+
* getOptions availability convention. Slider values are normalized 0–100. */
|
|
11375
|
+
var NormalizedRangeSchema = object({
|
|
11376
|
+
min: number(),
|
|
11377
|
+
max: number(),
|
|
11378
|
+
step: number()
|
|
11379
|
+
});
|
|
11380
|
+
/**
|
|
11381
|
+
* Current image-adjustment state. Every field optional — absent when the
|
|
11382
|
+
* camera does not expose that control. Slider values are NORMALIZED 0–100.
|
|
11383
|
+
* `lastFetchedAt` feeds the runtime-state bridge.
|
|
11384
|
+
*/
|
|
11385
|
+
var ImageSettingsStatusSchema = object({
|
|
11386
|
+
/** Normalized 0–100. */
|
|
11387
|
+
brightness: number().optional(),
|
|
11388
|
+
/** Normalized 0–100. */
|
|
11389
|
+
contrast: number().optional(),
|
|
11390
|
+
/** Normalized 0–100. */
|
|
11391
|
+
saturation: number().optional(),
|
|
11392
|
+
/** Normalized 0–100. */
|
|
11393
|
+
sharpness: number().optional(),
|
|
11394
|
+
mirror: boolean().optional(),
|
|
11395
|
+
flip: boolean().optional(),
|
|
11396
|
+
rotate: ImageRotateSchema.optional(),
|
|
11397
|
+
whiteBalance: WhiteBalanceModeSchema.optional(),
|
|
11398
|
+
/** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
|
|
11399
|
+
warmth: number().optional(),
|
|
11400
|
+
exposureMode: ExposureModeSchema.optional(),
|
|
11401
|
+
backlightMode: BacklightModeSchema.optional(),
|
|
11402
|
+
lastFetchedAt: number()
|
|
11403
|
+
});
|
|
11404
|
+
/**
|
|
11405
|
+
* Per-camera availability descriptor — drives which controls the admin UI
|
|
11406
|
+
* renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
|
|
11407
|
+
* (the normalized 0–100 range); enums as arrays of supported values (empty
|
|
11408
|
+
* array → control hidden). A provider returns honest, camera-probed values
|
|
11409
|
+
* — never hardcoded.
|
|
11410
|
+
*/
|
|
11411
|
+
var ImageSettingsOptionsSchema = object({
|
|
11412
|
+
supportsBrightness: boolean(),
|
|
11413
|
+
brightness: NormalizedRangeSchema.optional(),
|
|
11414
|
+
supportsContrast: boolean(),
|
|
11415
|
+
contrast: NormalizedRangeSchema.optional(),
|
|
11416
|
+
supportsSaturation: boolean(),
|
|
11417
|
+
saturation: NormalizedRangeSchema.optional(),
|
|
11418
|
+
supportsSharpness: boolean(),
|
|
11419
|
+
sharpness: NormalizedRangeSchema.optional(),
|
|
11420
|
+
supportsMirror: boolean(),
|
|
11421
|
+
supportsFlip: boolean(),
|
|
11422
|
+
/** Supported rotation values. Empty → rotation not configurable. */
|
|
11423
|
+
rotateOptions: array(ImageRotateSchema),
|
|
11424
|
+
/** Supported white-balance modes. Empty → white-balance not configurable. */
|
|
11425
|
+
whiteBalanceModes: array(WhiteBalanceModeSchema),
|
|
11426
|
+
supportsWarmth: boolean(),
|
|
11427
|
+
/** Present when `supportsWarmth` — the normalized 0–100 range. */
|
|
11428
|
+
warmth: NormalizedRangeSchema.optional(),
|
|
11429
|
+
/** Supported exposure modes. Empty → exposure not configurable. */
|
|
11430
|
+
exposureModes: array(ExposureModeSchema),
|
|
11431
|
+
/** Supported backlight modes. Empty → backlight-compensation not configurable. */
|
|
11432
|
+
backlightModes: array(BacklightModeSchema)
|
|
11433
|
+
});
|
|
11434
|
+
/**
|
|
11435
|
+
* Partial change to the image config — every field optional. Slider values
|
|
11436
|
+
* are normalized 0–100. A provider ignores fields it does not support.
|
|
11437
|
+
*/
|
|
11438
|
+
var ImageSettingsPatchSchema = object({
|
|
11439
|
+
brightness: number().optional(),
|
|
11440
|
+
contrast: number().optional(),
|
|
11441
|
+
saturation: number().optional(),
|
|
11442
|
+
sharpness: number().optional(),
|
|
11443
|
+
mirror: boolean().optional(),
|
|
11444
|
+
flip: boolean().optional(),
|
|
11445
|
+
rotate: ImageRotateSchema.optional(),
|
|
11446
|
+
whiteBalance: WhiteBalanceModeSchema.optional(),
|
|
11447
|
+
warmth: number().optional(),
|
|
11448
|
+
exposureMode: ExposureModeSchema.optional(),
|
|
11449
|
+
backlightMode: BacklightModeSchema.optional()
|
|
11450
|
+
});
|
|
11451
|
+
var imageSettingsCapability = {
|
|
11452
|
+
name: "image-settings",
|
|
11453
|
+
scope: "device",
|
|
11454
|
+
deviceNative: true,
|
|
11455
|
+
mode: "singleton",
|
|
11456
|
+
deviceTypes: [DeviceType.Camera],
|
|
11457
|
+
deviceConfig: { ui: {
|
|
11458
|
+
kind: "derived-form",
|
|
11459
|
+
builderId: "image-settings",
|
|
11460
|
+
tab: "image"
|
|
11461
|
+
} },
|
|
11462
|
+
methods: {
|
|
11463
|
+
getOptions: method(object({ deviceId: number() }), ImageSettingsOptionsSchema),
|
|
11464
|
+
setSettings: method(object({
|
|
11465
|
+
deviceId: number(),
|
|
11466
|
+
settings: ImageSettingsPatchSchema
|
|
11467
|
+
}), _void(), {
|
|
11468
|
+
kind: "mutation",
|
|
11469
|
+
auth: "admin"
|
|
11470
|
+
})
|
|
11471
|
+
},
|
|
11472
|
+
status: {
|
|
11473
|
+
schema: ImageSettingsStatusSchema,
|
|
11474
|
+
kind: "poll"
|
|
11475
|
+
},
|
|
11476
|
+
runtimeState: ImageSettingsStatusSchema
|
|
11477
|
+
};
|
|
11478
|
+
/**
|
|
11219
11479
|
* Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
|
|
11220
11480
|
* with a mowing lifecycle plus a dock action.
|
|
11221
11481
|
*
|
|
@@ -12214,6 +12474,16 @@ var RunnerCameraConfigSchema = object({
|
|
|
12214
12474
|
* this gate is bypassed.
|
|
12215
12475
|
*/
|
|
12216
12476
|
onboardMotionDrivesAnalyzer: boolean().default(true),
|
|
12477
|
+
/**
|
|
12478
|
+
* Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
|
|
12479
|
+
* never arms the periodic recheck timer, regardless of `occupancyRecheckSec` —
|
|
12480
|
+
* this is off by default because the recheck re-subscribes a detection session
|
|
12481
|
+
* every N seconds while `watching`, a major source of pull-decoder re-dial
|
|
12482
|
+
* churn (each cycle creates+tears a session → RTSP re-dial → latency). The
|
|
12483
|
+
* `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
|
|
12484
|
+
* (and only render) when this is enabled.
|
|
12485
|
+
*/
|
|
12486
|
+
occupancyRecheckEnabled: boolean().default(false),
|
|
12217
12487
|
occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
|
|
12218
12488
|
occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
|
|
12219
12489
|
/**
|
|
@@ -14219,6 +14489,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
14219
14489
|
contact: contactCapability,
|
|
14220
14490
|
control: controlCapability,
|
|
14221
14491
|
cover: coverCapability,
|
|
14492
|
+
dayNight: dayNightCapability,
|
|
14222
14493
|
deviceDiscovery: deviceDiscoveryCapability,
|
|
14223
14494
|
deviceStatus: deviceStatusCapability,
|
|
14224
14495
|
doorbell: doorbellCapability,
|
|
@@ -14231,6 +14502,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
14231
14502
|
humidifier: humidifierCapability,
|
|
14232
14503
|
humiditySensor: humiditySensorCapability,
|
|
14233
14504
|
image: imageCapability,
|
|
14505
|
+
imageSettings: imageSettingsCapability,
|
|
14234
14506
|
lawnMowerControl: lawnMowerControlCapability,
|
|
14235
14507
|
lockControl: lockControlCapability,
|
|
14236
14508
|
mediaPlayer: mediaPlayerCapability,
|
|
@@ -16157,7 +16429,9 @@ method(object({ codec: string() }), boolean()), method(_void(), object({
|
|
|
16157
16429
|
id: string(),
|
|
16158
16430
|
name: string(),
|
|
16159
16431
|
isPullMode: boolean().optional(),
|
|
16160
|
-
priority: number().optional()
|
|
16432
|
+
priority: number().optional(),
|
|
16433
|
+
hwaccel: string().optional(),
|
|
16434
|
+
probedBestHwaccel: string().optional()
|
|
16161
16435
|
})), method(DecoderSessionConfigSchema, object({
|
|
16162
16436
|
sessionId: string(),
|
|
16163
16437
|
nodeId: string()
|
|
@@ -18071,7 +18345,17 @@ var AgentAddonConfigSchema = object({
|
|
|
18071
18345
|
});
|
|
18072
18346
|
var AgentPipelineSettingsSchema = object({
|
|
18073
18347
|
addonDefaults: record(string(), AgentAddonConfigSchema).readonly(),
|
|
18074
|
-
maxCameras: number().int().nonnegative().nullable().default(null)
|
|
18348
|
+
maxCameras: number().int().nonnegative().nullable().default(null),
|
|
18349
|
+
/** Per-node detection weight (relative share for the quota balancer). */
|
|
18350
|
+
detectWeight: number().positive().optional(),
|
|
18351
|
+
/** Node is eligible to run the detection pipeline (decode + inference). */
|
|
18352
|
+
detect: boolean().optional(),
|
|
18353
|
+
/** Node is eligible to host decoder sessions. */
|
|
18354
|
+
decode: boolean().optional(),
|
|
18355
|
+
/** Node is eligible to run audio-analyzer sessions. */
|
|
18356
|
+
audio: boolean().optional(),
|
|
18357
|
+
/** Node is eligible to be the ingest / source-owner (serve the restream). */
|
|
18358
|
+
ingest: boolean().optional()
|
|
18075
18359
|
});
|
|
18076
18360
|
var CameraPipelineForAgentSchema = object({
|
|
18077
18361
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
@@ -18377,6 +18661,21 @@ method(object({
|
|
|
18377
18661
|
}), object({ success: literal(true) }), {
|
|
18378
18662
|
kind: "mutation",
|
|
18379
18663
|
auth: "admin"
|
|
18664
|
+
}), method(object({
|
|
18665
|
+
agentNodeId: string(),
|
|
18666
|
+
detectWeight: number().positive().nullable()
|
|
18667
|
+
}), object({ success: literal(true) }), {
|
|
18668
|
+
kind: "mutation",
|
|
18669
|
+
auth: "admin"
|
|
18670
|
+
}), method(object({
|
|
18671
|
+
agentNodeId: string(),
|
|
18672
|
+
detect: boolean().nullable().optional(),
|
|
18673
|
+
decode: boolean().nullable().optional(),
|
|
18674
|
+
audio: boolean().nullable().optional(),
|
|
18675
|
+
ingest: boolean().nullable().optional()
|
|
18676
|
+
}), object({ success: literal(true) }), {
|
|
18677
|
+
kind: "mutation",
|
|
18678
|
+
auth: "admin"
|
|
18380
18679
|
}), method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()), method(object({
|
|
18381
18680
|
deviceId: number(),
|
|
18382
18681
|
addonId: string(),
|
|
@@ -22496,6 +22795,18 @@ Object.freeze({
|
|
|
22496
22795
|
addonId: null,
|
|
22497
22796
|
access: "view"
|
|
22498
22797
|
},
|
|
22798
|
+
"dayNight.getOptions": {
|
|
22799
|
+
capName: "day-night",
|
|
22800
|
+
capScope: "device",
|
|
22801
|
+
addonId: null,
|
|
22802
|
+
access: "view"
|
|
22803
|
+
},
|
|
22804
|
+
"dayNight.setSettings": {
|
|
22805
|
+
capName: "day-night",
|
|
22806
|
+
capScope: "device",
|
|
22807
|
+
addonId: null,
|
|
22808
|
+
access: "create"
|
|
22809
|
+
},
|
|
22499
22810
|
"decoder.createSession": {
|
|
22500
22811
|
capName: "decoder",
|
|
22501
22812
|
capScope: "system",
|
|
@@ -23426,6 +23737,18 @@ Object.freeze({
|
|
|
23426
23737
|
addonId: null,
|
|
23427
23738
|
access: "create"
|
|
23428
23739
|
},
|
|
23740
|
+
"imageSettings.getOptions": {
|
|
23741
|
+
capName: "image-settings",
|
|
23742
|
+
capScope: "device",
|
|
23743
|
+
addonId: null,
|
|
23744
|
+
access: "view"
|
|
23745
|
+
},
|
|
23746
|
+
"imageSettings.setSettings": {
|
|
23747
|
+
capName: "image-settings",
|
|
23748
|
+
capScope: "device",
|
|
23749
|
+
addonId: null,
|
|
23750
|
+
access: "create"
|
|
23751
|
+
},
|
|
23429
23752
|
"integrations.create": {
|
|
23430
23753
|
capName: "integrations",
|
|
23431
23754
|
capScope: "system",
|
|
@@ -24608,6 +24931,18 @@ Object.freeze({
|
|
|
24608
24931
|
addonId: null,
|
|
24609
24932
|
access: "create"
|
|
24610
24933
|
},
|
|
24934
|
+
"pipelineOrchestrator.setAgentCapabilities": {
|
|
24935
|
+
capName: "pipeline-orchestrator",
|
|
24936
|
+
capScope: "system",
|
|
24937
|
+
addonId: null,
|
|
24938
|
+
access: "create"
|
|
24939
|
+
},
|
|
24940
|
+
"pipelineOrchestrator.setAgentDetectWeight": {
|
|
24941
|
+
capName: "pipeline-orchestrator",
|
|
24942
|
+
capScope: "system",
|
|
24943
|
+
addonId: null,
|
|
24944
|
+
access: "create"
|
|
24945
|
+
},
|
|
24611
24946
|
"pipelineOrchestrator.setAgentMaxCameras": {
|
|
24612
24947
|
capName: "pipeline-orchestrator",
|
|
24613
24948
|
capScope: "system",
|
|
@@ -214519,6 +214854,118 @@ function buildRawState(reader) {
|
|
|
214519
214854
|
};
|
|
214520
214855
|
}
|
|
214521
214856
|
//#endregion
|
|
214857
|
+
//#region src/day-night-mapping.ts
|
|
214858
|
+
/**
|
|
214859
|
+
* Maps between the vendor-neutral `day-night` cap's `DayNightMode` and
|
|
214860
|
+
* Reolink's raw `VideoInput.dayNight` / `InputAdvanceCfg.DayNight.mode`
|
|
214861
|
+
* string (Baichuan cmdId 25/26 via `setIsp`/`getVideoInput`). Firmwares
|
|
214862
|
+
* report/accept camel-cased values such as `auto`, `color`,
|
|
214863
|
+
* `blackAndWhite` — case varies by model; the lib's `normalizeDayNightMode`
|
|
214864
|
+
* lower-cases the first letter before push, so the mapping here works off
|
|
214865
|
+
* a case-insensitive compare.
|
|
214866
|
+
*
|
|
214867
|
+
* There is no on-camera "schedule" mode in the Baichuan protocol — the
|
|
214868
|
+
* `day-night` cap's `getOptions.modes` never advertises `'schedule'` for
|
|
214869
|
+
* a Reolink device.
|
|
214870
|
+
*/
|
|
214871
|
+
var NIGHT_TOKENS = new Set([
|
|
214872
|
+
"blackandwhite",
|
|
214873
|
+
"black&white",
|
|
214874
|
+
"bw",
|
|
214875
|
+
"night",
|
|
214876
|
+
"blackwhite"
|
|
214877
|
+
]);
|
|
214878
|
+
var DAY_TOKENS = new Set([
|
|
214879
|
+
"color",
|
|
214880
|
+
"colour",
|
|
214881
|
+
"day"
|
|
214882
|
+
]);
|
|
214883
|
+
/**
|
|
214884
|
+
* Map the camera's raw `dayNight` string to the cap's normalized mode.
|
|
214885
|
+
* Falls back to `'auto'` for an unrecognized or missing value — every
|
|
214886
|
+
* Reolink camera defaults to auto IR-cut switching out of the box.
|
|
214887
|
+
*/
|
|
214888
|
+
function reolinkDayNightModeToCap(raw) {
|
|
214889
|
+
if (typeof raw !== "string" || raw.length === 0) return "auto";
|
|
214890
|
+
const normalized = raw.toLowerCase();
|
|
214891
|
+
if (normalized === "auto") return "auto";
|
|
214892
|
+
if (NIGHT_TOKENS.has(normalized)) return "night";
|
|
214893
|
+
if (DAY_TOKENS.has(normalized)) return "day";
|
|
214894
|
+
return "auto";
|
|
214895
|
+
}
|
|
214896
|
+
/**
|
|
214897
|
+
* Map the cap's normalized mode to the raw value Reolink's `setIsp`
|
|
214898
|
+
* expects. `'schedule'` has no Reolink equivalent — `getOptions.modes`
|
|
214899
|
+
* never advertises it, so a well-behaved caller never passes it here;
|
|
214900
|
+
* guard defensively in case one does anyway.
|
|
214901
|
+
*/
|
|
214902
|
+
function capDayNightModeToReolink(mode) {
|
|
214903
|
+
switch (mode) {
|
|
214904
|
+
case "auto": return "auto";
|
|
214905
|
+
case "day": return "color";
|
|
214906
|
+
case "night": return "blackAndWhite";
|
|
214907
|
+
case "schedule": throw new Error("Reolink cameras do not support a scheduled day/night mode");
|
|
214908
|
+
}
|
|
214909
|
+
}
|
|
214910
|
+
//#endregion
|
|
214911
|
+
//#region src/image-settings-mapping.ts
|
|
214912
|
+
/**
|
|
214913
|
+
* Reolink's `InputAdvanceCfg.Exposure.mode` (Baichuan cmdId 25/26, via
|
|
214914
|
+
* `setIsp`/`getIsp`) uses the same two values as the `image-settings`
|
|
214915
|
+
* cap's `ExposureMode` (`'auto' | 'manual'`), modulo case — firmwares
|
|
214916
|
+
* report it in varying case. Returns `undefined` for any other/missing
|
|
214917
|
+
* value so the caller can omit the field rather than report a lie.
|
|
214918
|
+
*/
|
|
214919
|
+
function reolinkExposureModeToCap(raw) {
|
|
214920
|
+
if (typeof raw !== "string") return void 0;
|
|
214921
|
+
const normalized = raw.toLowerCase();
|
|
214922
|
+
return normalized === "auto" || normalized === "manual" ? normalized : void 0;
|
|
214923
|
+
}
|
|
214924
|
+
//#endregion
|
|
214925
|
+
//#region src/image-value-normalization.ts
|
|
214926
|
+
/**
|
|
214927
|
+
* Generic native-range <-> normalized-0-100 conversion shared by the
|
|
214928
|
+
* `day-night` (sensitivity) and `image-settings` (brightness / contrast /
|
|
214929
|
+
* saturation / sharpness) native caps. Both caps' vendor-neutral contract
|
|
214930
|
+
* normalizes every slider to an integer 0-100 scale; each provider maps
|
|
214931
|
+
* its own native range (Reolink's image sliders are 0..255, the day/night
|
|
214932
|
+
* threshold range is camera-probed) to/from that scale here.
|
|
214933
|
+
*/
|
|
214934
|
+
/** Map a native value inside `[min, max]` to the normalized 0-100 scale. */
|
|
214935
|
+
function normalizeToPercent(value, min, max) {
|
|
214936
|
+
if (max <= min) return 0;
|
|
214937
|
+
const percent = (value - min) / (max - min) * 100;
|
|
214938
|
+
return Math.max(0, Math.min(100, Math.round(percent)));
|
|
214939
|
+
}
|
|
214940
|
+
/** Map a normalized 0-100 value back to the native `[min, max]` range. */
|
|
214941
|
+
function denormalizeFromPercent(percent, min, max) {
|
|
214942
|
+
return Math.round(min + Math.max(0, Math.min(100, percent)) / 100 * (max - min));
|
|
214943
|
+
}
|
|
214944
|
+
//#endregion
|
|
214945
|
+
//#region src/native-sdp-overlay.ts
|
|
214946
|
+
/**
|
|
214947
|
+
* Return a new descriptor list in which every native `pull-rfc4571` entry with
|
|
214948
|
+
* a live upstream server carries that server's real `tcp://` URL + real SDP.
|
|
214949
|
+
* Pure + immutable: non-native or cold entries pass through unchanged;
|
|
214950
|
+
* upgraded entries are fresh objects and sibling metadata keys are preserved.
|
|
214951
|
+
*/
|
|
214952
|
+
function overlayLiveNativeRfc4571Sdp(descriptors, liveServerFor) {
|
|
214953
|
+
return descriptors.map((descriptor) => {
|
|
214954
|
+
if (descriptor.kind !== "pull-rfc4571") return descriptor;
|
|
214955
|
+
const live = liveServerFor(descriptor.camStreamId);
|
|
214956
|
+
if (!live) return descriptor;
|
|
214957
|
+
const auth = `${encodeURIComponent(live.username)}:${encodeURIComponent(live.password)}`;
|
|
214958
|
+
return {
|
|
214959
|
+
...descriptor,
|
|
214960
|
+
url: `tcp://${auth}@${live.host}:${live.port}`,
|
|
214961
|
+
metadata: {
|
|
214962
|
+
...descriptor.metadata,
|
|
214963
|
+
sdp: live.sdp
|
|
214964
|
+
}
|
|
214965
|
+
};
|
|
214966
|
+
});
|
|
214967
|
+
}
|
|
214968
|
+
//#endregion
|
|
214522
214969
|
//#region src/accessory-probe-flags.ts
|
|
214523
214970
|
var FLAG_KEYS = [
|
|
214524
214971
|
"hasBattery",
|
|
@@ -218729,6 +219176,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
218729
219176
|
});
|
|
218730
219177
|
this.registerMotionZonesCap();
|
|
218731
219178
|
this.registerPrivacyMaskCap();
|
|
219179
|
+
this.registerDayNightCap();
|
|
219180
|
+
this.registerImageSettingsCap();
|
|
218732
219181
|
this.registerStreamCatalogProvider();
|
|
218733
219182
|
this.registerNativeObjectDetectionCap();
|
|
218734
219183
|
const cache = this.config.get("deviceCache");
|
|
@@ -218769,6 +219218,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
218769
219218
|
* cached from `getMotionAlarm` so `setZone` can re-encode a patch's
|
|
218770
219219
|
* `cells` without an extra GET round-trip. */
|
|
218771
219220
|
motionZonesGrid = null;
|
|
219221
|
+
/** Single-flight guard for the `day-night` cap refresh. */
|
|
219222
|
+
dayNightRefreshInFlight = null;
|
|
219223
|
+
/** Single-flight guard for the `image-settings` cap refresh. */
|
|
219224
|
+
imageSettingsRefreshInFlight = null;
|
|
218772
219225
|
/**
|
|
218773
219226
|
* Single-flight guard for snapshot fetches (Slice 10). When two
|
|
218774
219227
|
* consumers hit `getSnapshot` concurrently we issue ONE Baichuan
|
|
@@ -220030,6 +220483,244 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
220030
220483
|
this.ctx.logger.info("Reolink privacy-mask cap registered (read + enable + zone write)", { tags: { deviceId: this.id } });
|
|
220031
220484
|
}
|
|
220032
220485
|
/**
|
|
220486
|
+
* Register the vendor-neutral `day-night` cap.
|
|
220487
|
+
*
|
|
220488
|
+
* Mirrors `registerMotionZonesCap`: the trampoline pattern over
|
|
220489
|
+
* `createRuntimeStateBridge`, with a single-flight `refresh` that
|
|
220490
|
+
* round-trips `getVideoInput` (mode) + `getDayNightThreshold`
|
|
220491
|
+
* (sensitivity). Both are Baichuan cmdId 25/26/297 reads already used
|
|
220492
|
+
* elsewhere in this file (`refreshParentSettingsSnapshot`,
|
|
220493
|
+
* `applySettingsPatch`'s ISP section) — this cap just wraps them
|
|
220494
|
+
* behind the shared `getOptions`/`getStatus`/`setSettings` surface
|
|
220495
|
+
* instead of the legacy settings-form blob.
|
|
220496
|
+
*
|
|
220497
|
+
* - `getStatus` — slice-driven via bridge (stale → refresh).
|
|
220498
|
+
* - `getOptions` — on-demand probe; `modes` is gated on the camera
|
|
220499
|
+
* actually reporting a `dayNight` value, `supportsSensitivity` on a
|
|
220500
|
+
* successful `getDayNightThreshold` probe. Reolink has no
|
|
220501
|
+
* switch-delay knob, so `supportsSwitchDelay` is always `false`.
|
|
220502
|
+
* - `setSettings` — read-modify-write via `setIsp`; `sensitivity` is
|
|
220503
|
+
* denormalized against the camera's OWN probed threshold range
|
|
220504
|
+
* (never a hardcoded 0..255 guess).
|
|
220505
|
+
*/
|
|
220506
|
+
registerDayNightCap() {
|
|
220507
|
+
const channel = this.getChannel();
|
|
220508
|
+
const CAP_NAME = "day-night";
|
|
220509
|
+
const STALE_MS = 1e4;
|
|
220510
|
+
/** Round-trip `getVideoInput` + `getDayNightThreshold`, map into the
|
|
220511
|
+
* cap status schema, write slice. */
|
|
220512
|
+
const refreshFromCamera = async () => {
|
|
220513
|
+
if (this.dayNightRefreshInFlight) return this.dayNightRefreshInFlight;
|
|
220514
|
+
const promise = (async () => {
|
|
220515
|
+
try {
|
|
220516
|
+
const api = await this.ensureApi();
|
|
220517
|
+
const vi = (await api.getVideoInput(channel))?.body?.VideoInput;
|
|
220518
|
+
const next = {
|
|
220519
|
+
mode: reolinkDayNightModeToCap(vi?.dayNight),
|
|
220520
|
+
lastFetchedAt: Date.now()
|
|
220521
|
+
};
|
|
220522
|
+
try {
|
|
220523
|
+
const range = (await api.getDayNightThreshold(channel))?.body?.DayNightThreshold?.thresholdval;
|
|
220524
|
+
if (typeof range?.min === "number" && typeof range?.max === "number" && typeof range?.cur === "number") next.sensitivity = normalizeToPercent(range.cur, range.min, range.max);
|
|
220525
|
+
} catch (err) {
|
|
220526
|
+
this.ctx.logger.debug("day-night threshold probe failed", {
|
|
220527
|
+
tags: { deviceId: this.id },
|
|
220528
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
220529
|
+
});
|
|
220530
|
+
}
|
|
220531
|
+
this.runtimeState.setCapState(CAP_NAME, next);
|
|
220532
|
+
} catch (err) {
|
|
220533
|
+
this.ctx.logger.debug("day-night refresh failed — keeping last slice", {
|
|
220534
|
+
tags: { deviceId: this.id },
|
|
220535
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
220536
|
+
});
|
|
220537
|
+
}
|
|
220538
|
+
})();
|
|
220539
|
+
this.dayNightRefreshInFlight = promise;
|
|
220540
|
+
try {
|
|
220541
|
+
await promise;
|
|
220542
|
+
} finally {
|
|
220543
|
+
this.dayNightRefreshInFlight = null;
|
|
220544
|
+
}
|
|
220545
|
+
};
|
|
220546
|
+
const provider = {
|
|
220547
|
+
getStatus: createRuntimeStateBridge({
|
|
220548
|
+
runtimeState: this.runtimeState,
|
|
220549
|
+
cap: dayNightCapability,
|
|
220550
|
+
ownDeviceId: this.id,
|
|
220551
|
+
refresh: refreshFromCamera,
|
|
220552
|
+
staleMs: STALE_MS,
|
|
220553
|
+
empty: () => ({
|
|
220554
|
+
mode: "auto",
|
|
220555
|
+
lastFetchedAt: 0
|
|
220556
|
+
})
|
|
220557
|
+
}).getStatus,
|
|
220558
|
+
getOptions: async ({ deviceId }) => {
|
|
220559
|
+
if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
220560
|
+
const api = await this.ensureApi();
|
|
220561
|
+
const modes = ((await api.getVideoInput(channel))?.body?.VideoInput)?.dayNight !== void 0 ? [
|
|
220562
|
+
"auto",
|
|
220563
|
+
"day",
|
|
220564
|
+
"night"
|
|
220565
|
+
] : [];
|
|
220566
|
+
let supportsSensitivity = false;
|
|
220567
|
+
let sensitivityRange;
|
|
220568
|
+
try {
|
|
220569
|
+
const range = (await api.getDayNightThreshold(channel))?.body?.DayNightThreshold?.thresholdval;
|
|
220570
|
+
if (typeof range?.min === "number" && typeof range?.max === "number") {
|
|
220571
|
+
supportsSensitivity = true;
|
|
220572
|
+
sensitivityRange = {
|
|
220573
|
+
min: 0,
|
|
220574
|
+
max: 100,
|
|
220575
|
+
step: 1
|
|
220576
|
+
};
|
|
220577
|
+
}
|
|
220578
|
+
} catch (err) {
|
|
220579
|
+
this.ctx.logger.debug("day-night threshold options probe failed", {
|
|
220580
|
+
tags: { deviceId: this.id },
|
|
220581
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
220582
|
+
});
|
|
220583
|
+
}
|
|
220584
|
+
return {
|
|
220585
|
+
modes,
|
|
220586
|
+
supportsSensitivity,
|
|
220587
|
+
...sensitivityRange !== void 0 ? { sensitivity: sensitivityRange } : {},
|
|
220588
|
+
supportsSwitchDelay: false
|
|
220589
|
+
};
|
|
220590
|
+
},
|
|
220591
|
+
setSettings: async ({ deviceId, settings }) => {
|
|
220592
|
+
if (deviceId !== this.id) return;
|
|
220593
|
+
const api = await this.ensureApi();
|
|
220594
|
+
const ispPatch = {};
|
|
220595
|
+
if (settings.mode !== void 0) ispPatch.dayNight = capDayNightModeToReolink(settings.mode);
|
|
220596
|
+
if (settings.sensitivity !== void 0) {
|
|
220597
|
+
const range = (await api.getDayNightThreshold(channel))?.body?.DayNightThreshold?.thresholdval;
|
|
220598
|
+
const min = typeof range?.min === "number" ? range.min : 0;
|
|
220599
|
+
const max = typeof range?.max === "number" ? range.max : 100;
|
|
220600
|
+
ispPatch.dayNightThreshold = denormalizeFromPercent(settings.sensitivity, min, max);
|
|
220601
|
+
}
|
|
220602
|
+
if (Object.keys(ispPatch).length > 0) await api.setIsp(channel, ispPatch);
|
|
220603
|
+
await refreshFromCamera();
|
|
220604
|
+
}
|
|
220605
|
+
};
|
|
220606
|
+
this.ctx.registerNativeCap(dayNightCapability, provider);
|
|
220607
|
+
this.ctx.logger.info("Reolink day-night cap registered", { tags: { deviceId: this.id } });
|
|
220608
|
+
}
|
|
220609
|
+
/**
|
|
220610
|
+
* Register the vendor-neutral `image-settings` cap.
|
|
220611
|
+
*
|
|
220612
|
+
* Mirrors `registerDayNightCap` / `registerMotionZonesCap`: the
|
|
220613
|
+
* trampoline pattern over `createRuntimeStateBridge`, with a
|
|
220614
|
+
* single-flight `refresh` that round-trips `getIsp` (cmdId 26 — the
|
|
220615
|
+
* merged `VideoInput` + `InputAdvanceCfg` blob covers brightness /
|
|
220616
|
+
* contrast / saturation / sharpness AND the exposure mode in one
|
|
220617
|
+
* call). Sliders are normalized against Reolink's native 0..255 image
|
|
220618
|
+
* range; `exposureMode` maps 1:1 onto the cap's `'auto' | 'manual'`.
|
|
220619
|
+
*
|
|
220620
|
+
* Reolink's Baichuan API (`@apocaliss92/nodelink-js` 0.6.7) has no
|
|
220621
|
+
* write path for mirror / flip / rotate / white-balance / warmth, and
|
|
220622
|
+
* `InputAdvanceCfg.BLC` (backlight) has no matching setter — those
|
|
220623
|
+
* fields are honestly reported as unsupported (`false` / `[]`) rather
|
|
220624
|
+
* than rendering dead controls. TODO: revisit if nodelink-js adds
|
|
220625
|
+
* setters for any of these.
|
|
220626
|
+
*/
|
|
220627
|
+
registerImageSettingsCap() {
|
|
220628
|
+
const channel = this.getChannel();
|
|
220629
|
+
const CAP_NAME = "image-settings";
|
|
220630
|
+
const STALE_MS = 1e4;
|
|
220631
|
+
const SLIDER_MIN = 0;
|
|
220632
|
+
const SLIDER_MAX = 255;
|
|
220633
|
+
const NORMALIZED_RANGE = {
|
|
220634
|
+
min: 0,
|
|
220635
|
+
max: 100,
|
|
220636
|
+
step: 1
|
|
220637
|
+
};
|
|
220638
|
+
/** Round-trip `getIsp`, map into the cap status schema, write slice. */
|
|
220639
|
+
const refreshFromCamera = async () => {
|
|
220640
|
+
if (this.imageSettingsRefreshInFlight) return this.imageSettingsRefreshInFlight;
|
|
220641
|
+
const promise = (async () => {
|
|
220642
|
+
try {
|
|
220643
|
+
const isp = await (await this.ensureApi()).getIsp(channel);
|
|
220644
|
+
const vi = isp?.body?.VideoInput;
|
|
220645
|
+
const exposureRaw = isp?.body?.InputAdvanceCfg?.Exposure?.mode;
|
|
220646
|
+
const next = { lastFetchedAt: Date.now() };
|
|
220647
|
+
if (typeof vi?.bright === "number") next.brightness = normalizeToPercent(vi.bright, SLIDER_MIN, SLIDER_MAX);
|
|
220648
|
+
if (typeof vi?.contrast === "number") next.contrast = normalizeToPercent(vi.contrast, SLIDER_MIN, SLIDER_MAX);
|
|
220649
|
+
if (typeof vi?.saturation === "number") next.saturation = normalizeToPercent(vi.saturation, SLIDER_MIN, SLIDER_MAX);
|
|
220650
|
+
if (typeof vi?.sharpen === "number") next.sharpness = normalizeToPercent(vi.sharpen, SLIDER_MIN, SLIDER_MAX);
|
|
220651
|
+
const exposureMode = reolinkExposureModeToCap(typeof exposureRaw === "string" ? exposureRaw : void 0);
|
|
220652
|
+
if (exposureMode !== void 0) next.exposureMode = exposureMode;
|
|
220653
|
+
this.runtimeState.setCapState(CAP_NAME, next);
|
|
220654
|
+
} catch (err) {
|
|
220655
|
+
this.ctx.logger.debug("image-settings refresh failed — keeping last slice", {
|
|
220656
|
+
tags: { deviceId: this.id },
|
|
220657
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
220658
|
+
});
|
|
220659
|
+
}
|
|
220660
|
+
})();
|
|
220661
|
+
this.imageSettingsRefreshInFlight = promise;
|
|
220662
|
+
try {
|
|
220663
|
+
await promise;
|
|
220664
|
+
} finally {
|
|
220665
|
+
this.imageSettingsRefreshInFlight = null;
|
|
220666
|
+
}
|
|
220667
|
+
};
|
|
220668
|
+
const provider = {
|
|
220669
|
+
getStatus: createRuntimeStateBridge({
|
|
220670
|
+
runtimeState: this.runtimeState,
|
|
220671
|
+
cap: imageSettingsCapability,
|
|
220672
|
+
ownDeviceId: this.id,
|
|
220673
|
+
refresh: refreshFromCamera,
|
|
220674
|
+
staleMs: STALE_MS,
|
|
220675
|
+
empty: () => ({ lastFetchedAt: 0 })
|
|
220676
|
+
}).getStatus,
|
|
220677
|
+
getOptions: async ({ deviceId }) => {
|
|
220678
|
+
if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
220679
|
+
const isp = await (await this.ensureApi()).getIsp(channel);
|
|
220680
|
+
const vi = isp?.body?.VideoInput;
|
|
220681
|
+
const exposureRaw = isp?.body?.InputAdvanceCfg?.Exposure?.mode;
|
|
220682
|
+
const supportsBrightness = typeof vi?.bright === "number";
|
|
220683
|
+
const supportsContrast = typeof vi?.contrast === "number";
|
|
220684
|
+
const supportsSaturation = typeof vi?.saturation === "number";
|
|
220685
|
+
const supportsSharpness = typeof vi?.sharpen === "number";
|
|
220686
|
+
const exposureModes = typeof exposureRaw === "string" ? ["auto", "manual"] : [];
|
|
220687
|
+
return {
|
|
220688
|
+
supportsBrightness,
|
|
220689
|
+
...supportsBrightness ? { brightness: NORMALIZED_RANGE } : {},
|
|
220690
|
+
supportsContrast,
|
|
220691
|
+
...supportsContrast ? { contrast: NORMALIZED_RANGE } : {},
|
|
220692
|
+
supportsSaturation,
|
|
220693
|
+
...supportsSaturation ? { saturation: NORMALIZED_RANGE } : {},
|
|
220694
|
+
supportsSharpness,
|
|
220695
|
+
...supportsSharpness ? { sharpness: NORMALIZED_RANGE } : {},
|
|
220696
|
+
supportsMirror: false,
|
|
220697
|
+
supportsFlip: false,
|
|
220698
|
+
rotateOptions: [],
|
|
220699
|
+
whiteBalanceModes: [],
|
|
220700
|
+
supportsWarmth: false,
|
|
220701
|
+
exposureModes,
|
|
220702
|
+
backlightModes: []
|
|
220703
|
+
};
|
|
220704
|
+
},
|
|
220705
|
+
setSettings: async ({ deviceId, settings }) => {
|
|
220706
|
+
if (deviceId !== this.id) return;
|
|
220707
|
+
const api = await this.ensureApi();
|
|
220708
|
+
const imagePatch = {};
|
|
220709
|
+
if (settings.brightness !== void 0) imagePatch.bright = denormalizeFromPercent(settings.brightness, SLIDER_MIN, SLIDER_MAX);
|
|
220710
|
+
if (settings.contrast !== void 0) imagePatch.contrast = denormalizeFromPercent(settings.contrast, SLIDER_MIN, SLIDER_MAX);
|
|
220711
|
+
if (settings.saturation !== void 0) imagePatch.saturation = denormalizeFromPercent(settings.saturation, SLIDER_MIN, SLIDER_MAX);
|
|
220712
|
+
if (settings.sharpness !== void 0) imagePatch.sharpen = denormalizeFromPercent(settings.sharpness, SLIDER_MIN, SLIDER_MAX);
|
|
220713
|
+
if (Object.keys(imagePatch).length > 0) await api.setImage(channel, imagePatch);
|
|
220714
|
+
const ispPatch = {};
|
|
220715
|
+
if (settings.exposureMode !== void 0) ispPatch.exposure = settings.exposureMode;
|
|
220716
|
+
if (Object.keys(ispPatch).length > 0) await api.setIsp(channel, ispPatch);
|
|
220717
|
+
await refreshFromCamera();
|
|
220718
|
+
}
|
|
220719
|
+
};
|
|
220720
|
+
this.ctx.registerNativeCap(imageSettingsCapability, provider);
|
|
220721
|
+
this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
|
|
220722
|
+
}
|
|
220723
|
+
/**
|
|
220033
220724
|
* Register the `native-object-detection` cap.
|
|
220034
220725
|
*
|
|
220035
220726
|
* The runtimeState holds three fields:
|
|
@@ -220060,6 +220751,12 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
220060
220751
|
}
|
|
220061
220752
|
return classes;
|
|
220062
220753
|
};
|
|
220754
|
+
const buildEmptyState = () => ({
|
|
220755
|
+
enabled: false,
|
|
220756
|
+
lastByClass: {},
|
|
220757
|
+
supportedClasses: buildSupportedClasses(),
|
|
220758
|
+
lastFetchedAt: 0
|
|
220759
|
+
});
|
|
220063
220760
|
const provider = {
|
|
220064
220761
|
getStatus: createRuntimeStateBridge({
|
|
220065
220762
|
runtimeState: this.runtimeState,
|
|
@@ -220067,12 +220764,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
220067
220764
|
ownDeviceId: this.id,
|
|
220068
220765
|
refresh: async () => {},
|
|
220069
220766
|
staleMs: Infinity,
|
|
220070
|
-
empty:
|
|
220071
|
-
enabled: false,
|
|
220072
|
-
lastByClass: {},
|
|
220073
|
-
supportedClasses: buildSupportedClasses(),
|
|
220074
|
-
lastFetchedAt: 0
|
|
220075
|
-
})
|
|
220767
|
+
empty: buildEmptyState
|
|
220076
220768
|
}).getStatus,
|
|
220077
220769
|
setEnabled: async ({ deviceId, enabled }) => {
|
|
220078
220770
|
if (deviceId !== this.id) return;
|
|
@@ -220086,6 +220778,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
220086
220778
|
}
|
|
220087
220779
|
};
|
|
220088
220780
|
this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
|
|
220781
|
+
if (this.runtimeState.getCapState(CAP_NAME) === void 0) this.runtimeState.setCapState(CAP_NAME, {
|
|
220782
|
+
...buildEmptyState(),
|
|
220783
|
+
lastFetchedAt: Date.now()
|
|
220784
|
+
});
|
|
220089
220785
|
this.ctx.logger.info("Reolink native-object-detection cap registered", { tags: { deviceId: this.id } });
|
|
220090
220786
|
}
|
|
220091
220787
|
/**
|
|
@@ -220529,17 +221225,39 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
220529
221225
|
* session or touches the broker.
|
|
220530
221226
|
*/
|
|
220531
221227
|
async buildStreamCatalog() {
|
|
220532
|
-
if (this.cachedStreamDescriptors?.length) return this.cachedStreamDescriptors;
|
|
220533
|
-
if (this.buildStreamCatalogInFlight) return this.buildStreamCatalogInFlight;
|
|
221228
|
+
if (this.cachedStreamDescriptors?.length) return this.withLiveNativeSdp(this.cachedStreamDescriptors);
|
|
221229
|
+
if (this.buildStreamCatalogInFlight) return this.withLiveNativeSdp(await this.buildStreamCatalogInFlight);
|
|
220534
221230
|
const build = this.buildStreamCatalogUncached();
|
|
220535
221231
|
this.buildStreamCatalogInFlight = build;
|
|
220536
221232
|
try {
|
|
220537
|
-
return await build;
|
|
221233
|
+
return this.withLiveNativeSdp(await build);
|
|
220538
221234
|
} finally {
|
|
220539
221235
|
this.buildStreamCatalogInFlight = null;
|
|
220540
221236
|
}
|
|
220541
221237
|
}
|
|
220542
221238
|
/**
|
|
221239
|
+
* Upgrade native `pull-rfc4571` descriptors to the live upstream server's
|
|
221240
|
+
* real `tcp://` URL + real (sprop-bearing) SDP whenever a server is
|
|
221241
|
+
* currently listening in `this.active`. The synthetic catalog SDP omits
|
|
221242
|
+
* `sprop-parameter-sets`, which the raw ffmpeg decode reader cannot recover
|
|
221243
|
+
* (it has no in-band SPS/PPS merge) — so a decode-path consumer that pulls
|
|
221244
|
+
* the catalog after `materializeStreamSocket` must still see the real SDP.
|
|
221245
|
+
* See `native-sdp-overlay.ts`.
|
|
221246
|
+
*/
|
|
221247
|
+
withLiveNativeSdp(descriptors) {
|
|
221248
|
+
return overlayLiveNativeRfc4571Sdp(descriptors, (camStreamId) => {
|
|
221249
|
+
const server = this.active.get(camStreamId)?.server;
|
|
221250
|
+
if (!server?.server?.listening) return null;
|
|
221251
|
+
return {
|
|
221252
|
+
host: server.host,
|
|
221253
|
+
port: server.port,
|
|
221254
|
+
username: server.username,
|
|
221255
|
+
password: server.password,
|
|
221256
|
+
sdp: server.sdp
|
|
221257
|
+
};
|
|
221258
|
+
});
|
|
221259
|
+
}
|
|
221260
|
+
/**
|
|
220543
221261
|
* Synthesize the HTTP-FLV pull URL for a (channel, profile) pair. FLV is
|
|
220544
221262
|
* served off the Reolink Baichuan media port (default 1935, app `bcs`) as
|
|
220545
221263
|
* `channel<channel>_<profile>.bcs` — the same shape Frigate/go2rtc use. The
|
|
@@ -220763,13 +221481,16 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
220763
221481
|
}
|
|
220764
221482
|
const url = `tcp://${`${encodeURIComponent(server.username)}:${encodeURIComponent(server.password)}`}@${server.host}:${server.port}`;
|
|
220765
221483
|
const codec = server.videoType === "H265" ? "h265" : "h264";
|
|
221484
|
+
const descriptor = this.cachedStreamDescriptors?.find((d) => d.camStreamId === camStreamId);
|
|
220766
221485
|
await this.publishOne({
|
|
220767
221486
|
camStreamId,
|
|
220768
221487
|
kind: "pull-rfc4571",
|
|
220769
|
-
label: camStreamId,
|
|
221488
|
+
label: descriptor?.label ?? camStreamId,
|
|
220770
221489
|
url,
|
|
220771
221490
|
codec,
|
|
220772
221491
|
autoEligible: true,
|
|
221492
|
+
...descriptor?.resolution ? { resolution: descriptor.resolution } : {},
|
|
221493
|
+
...descriptor?.fps !== void 0 ? { fps: descriptor.fps } : {},
|
|
220773
221494
|
metadata: { sdp: server.sdp }
|
|
220774
221495
|
});
|
|
220775
221496
|
this.ctx.logger.info("materialized rfc4571 stream socket on broker demand", {
|