@camstack/addon-provider-reolink 1.2.145 → 1.2.147
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-FOE2DBVZ-94yIU75L.mjs +2 -0
- package/dist/BaichuanVideoStream-FOE2DBVZ-DJcEkVak.js +3 -0
- package/dist/addon.js +1523 -651
- package/dist/addon.mjs +1364 -492
- package/dist/{chunk-4KBXJ7U3-BpV_ui8q.js → chunk-ZBACPERX-BM6Ll-rh.js} +6 -8
- package/dist/{chunk-4KBXJ7U3-CCQfaWmi.mjs → chunk-ZBACPERX-cDJK1KXE.mjs} +6 -8
- package/package.json +2 -2
- package/dist/BaichuanVideoStream-GFJX465B-D_s-iWc1.mjs +0 -2
- package/dist/BaichuanVideoStream-GFJX465B-EThalnJS.js +0 -3
- /package/dist/{DiagnosticsTools-DZ5RWXSM-9NV95vRN.mjs → DiagnosticsTools-ZC7I7QM6-9NV95vRN.mjs} +0 -0
- /package/dist/{DiagnosticsTools-DZ5RWXSM-CD4rRF8a.js → DiagnosticsTools-ZC7I7QM6-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_ZBACPERX = require("./chunk-ZBACPERX-BM6Ll-rh.js");
|
|
4
4
|
let node_crypto = require("node:crypto");
|
|
5
5
|
let events = require("events");
|
|
6
6
|
let fs = require("fs");
|
|
@@ -5390,7 +5390,7 @@ var ZodIssueCode = {
|
|
|
5390
5390
|
var ZodFirstPartyTypeKind;
|
|
5391
5391
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5392
5392
|
//#endregion
|
|
5393
|
-
//#region ../types/dist/sleep-
|
|
5393
|
+
//#region ../types/dist/sleep-CaB_hL5g.mjs
|
|
5394
5394
|
/**
|
|
5395
5395
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5396
5396
|
* window to float samples (D455).
|
|
@@ -5590,12 +5590,6 @@ Object.fromEntries([
|
|
|
5590
5590
|
icon: "circle-dot",
|
|
5591
5591
|
order: 40
|
|
5592
5592
|
},
|
|
5593
|
-
{
|
|
5594
|
-
id: "clips",
|
|
5595
|
-
label: "Clips",
|
|
5596
|
-
icon: "clapperboard",
|
|
5597
|
-
order: 41
|
|
5598
|
-
},
|
|
5599
5593
|
{
|
|
5600
5594
|
id: "engine",
|
|
5601
5595
|
label: "Inference Engine",
|
|
@@ -7443,6 +7437,8 @@ var EVENT_OWNER_TYPES = [
|
|
|
7443
7437
|
* nothing failing until a caller asked.
|
|
7444
7438
|
*/
|
|
7445
7439
|
var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
|
|
7440
|
+
/** The same list as a Zod enum, for the cap input that carries it. */
|
|
7441
|
+
var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
|
|
7446
7442
|
new Set(EVENT_OWNER_TYPES);
|
|
7447
7443
|
var EncodeProfileSchema = object({
|
|
7448
7444
|
video: object({
|
|
@@ -14988,8 +14984,6 @@ method(object({
|
|
|
14988
14984
|
deviceId: number(),
|
|
14989
14985
|
capName: string(),
|
|
14990
14986
|
wrapperAddonId: string(),
|
|
14991
|
-
/** The `ClipSource.source` id to toggle. Absent = all of the addon's. */
|
|
14992
|
-
sourceId: string().optional(),
|
|
14993
14987
|
active: boolean()
|
|
14994
14988
|
}), _void(), {
|
|
14995
14989
|
kind: "mutation",
|
|
@@ -22018,7 +22012,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
22018
22012
|
ownerType: EventOwnerTypeSchema,
|
|
22019
22013
|
eventId: number().int(),
|
|
22020
22014
|
deviceId: number()
|
|
22021
|
-
}), array(MediaFileInfoSchema).readonly()), method(
|
|
22015
|
+
}), array(MediaFileInfoSchema).readonly()), method(object({
|
|
22016
|
+
deviceId: number(),
|
|
22017
|
+
ownerKind: MediaPresenceOwnerKindSchema,
|
|
22018
|
+
ownerIds: array(string()).max(5e3)
|
|
22019
|
+
}), array(string()).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
|
|
22022
22020
|
kind: "mutation",
|
|
22023
22021
|
auth: "admin"
|
|
22024
22022
|
}), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
|
|
@@ -25116,6 +25114,24 @@ method(VectorDeclareIndexInputSchema, _void(), {
|
|
|
25116
25114
|
kind: "mutation",
|
|
25117
25115
|
auth: "admin"
|
|
25118
25116
|
}), method(VectorStatsInputSchema, VectorStatsResultSchema, { auth: "admin" });
|
|
25117
|
+
_enum([
|
|
25118
|
+
"queue-full",
|
|
25119
|
+
"camera-backoff",
|
|
25120
|
+
"sleeping",
|
|
25121
|
+
"camera-refused",
|
|
25122
|
+
"no-keyframe",
|
|
25123
|
+
"no-catalog-row",
|
|
25124
|
+
"unsupported",
|
|
25125
|
+
"unknown-device",
|
|
25126
|
+
"uid-missing"
|
|
25127
|
+
]);
|
|
25128
|
+
_enum(["deferred", "final"]);
|
|
25129
|
+
/** The camera's / provider's own words. Kept verbatim, never parsed. */
|
|
25130
|
+
var CLIP_STILL_REASON_HEADER = "x-camstack-reason";
|
|
25131
|
+
/** One {@link ClipStillRefusalCodeSchema} token, alone. */
|
|
25132
|
+
var CLIP_STILL_REASON_CODE_HEADER = "x-camstack-reason-code";
|
|
25133
|
+
/** One {@link ClipStillDispositionSchema} token, alone. */
|
|
25134
|
+
var CLIP_STILL_DISPOSITION_HEADER = "x-camstack-disposition";
|
|
25119
25135
|
var ClipSchema = object({
|
|
25120
25136
|
/** Opaque, provider-namespaced id. The default provider encodes the time
|
|
25121
25137
|
* window so `getClipPlayback` is self-contained (no event re-query). */
|
|
@@ -25266,7 +25282,25 @@ var ClipSchema = object({
|
|
|
25266
25282
|
*/
|
|
25267
25283
|
playable: boolean().optional(),
|
|
25268
25284
|
/** Why {@link playable} is false, verbatim (`no-file-for-window`). */
|
|
25269
|
-
unplayableReason: string().optional()
|
|
25285
|
+
unplayableReason: string().optional(),
|
|
25286
|
+
/**
|
|
25287
|
+
* This clip is STILL BEING WRITTEN, so {@link ClipSchema.timeRange}`.endMs`
|
|
25288
|
+
* is not its end.
|
|
25289
|
+
*
|
|
25290
|
+
* Absent — the common case — means the row is closed and its `endMs` is the
|
|
25291
|
+
* end of the recording. Present and `true` means the source told us the file
|
|
25292
|
+
* has no end yet, and the `endMs` we carry is whatever the camera's index
|
|
25293
|
+
* entry happened to hold: on a Reolink E1 Outdoor PoE (592, measured
|
|
25294
|
+
* 2026-09-20) the newest file `…_192927_000000_…_0.mp4` reported an `endTime`
|
|
25295
|
+
* of 19:29:56 and STILL reported it eight minutes later, while the file went
|
|
25296
|
+
* on growing. A surface that drew 29 s there was lying about a clip that
|
|
25297
|
+
* plays for minutes, on the one row the operator looks at first.
|
|
25298
|
+
*
|
|
25299
|
+
* `endMs` is deliberately still a number: it is the best bound anything has
|
|
25300
|
+
* for a mint window or a byte fetch, and every consumer already requires it.
|
|
25301
|
+
* This flag says what it is WORTH, and a duration is not drawn from it.
|
|
25302
|
+
*/
|
|
25303
|
+
inProgress: boolean().optional()
|
|
25270
25304
|
});
|
|
25271
25305
|
var ClipPlaybackSchema = object({
|
|
25272
25306
|
/**
|
|
@@ -25349,59 +25383,17 @@ var ClipSourceSchema = object({
|
|
|
25349
25383
|
* alternative — a `native:reolink:* → provider-reolink` table inside the
|
|
25350
25384
|
* widget — is a second authority on provider identity living in the one
|
|
25351
25385
|
* package with no business knowing it, wrong the day a third source appears
|
|
25352
|
-
* (D557). One addon may serve SEVERAL sources
|
|
25353
|
-
*
|
|
25354
|
-
*
|
|
25355
|
-
*
|
|
25386
|
+
* (D557). One addon may serve SEVERAL sources, which is why the per-device
|
|
25387
|
+
* switch is keyed by SOURCE and not by this (D555) — `addon-provider-reolink`
|
|
25388
|
+
* served a hub child two of them until the two views were measured to be one
|
|
25389
|
+
* store read twice (D555) and then read ONE way (D565).
|
|
25356
25390
|
*
|
|
25357
25391
|
* Optional for version skew only. The collection dispatcher stamps it from
|
|
25358
25392
|
* the registry, so a row that travelled through the fan-out carries the
|
|
25359
25393
|
* authoritative id whatever the provider filled in.
|
|
25360
25394
|
*/
|
|
25361
25395
|
addonId: string().optional(),
|
|
25362
|
-
|
|
25363
|
-
* Which API this source resolved to FOR THIS CAMERA, when it has a choice.
|
|
25364
|
-
*
|
|
25365
|
-
* A source may cover one store through more than one surface — the Reolink
|
|
25366
|
-
* provider reads a hub child through the parent's event log and a standalone
|
|
25367
|
-
* through its own file list, because that is what each camera answers. The
|
|
25368
|
-
* CHOICE is the provider's, made from what the camera is, and is never a row
|
|
25369
|
-
* the operator has to understand; but it is REPORTED, because a source that
|
|
25370
|
-
* silently reads a different API on two cameras and then behaves differently
|
|
25371
|
-
* is the thing nobody can debug later. Absent when the source has only one
|
|
25372
|
-
* way to read its store.
|
|
25373
|
-
*/
|
|
25374
|
-
via: string().optional(),
|
|
25375
|
-
availability: ClipSourceAvailabilitySchema,
|
|
25376
|
-
/**
|
|
25377
|
-
* The operator switched this source OFF for this camera.
|
|
25378
|
-
*
|
|
25379
|
-
* Deliberately NOT a member of {@link ClipSourceAvailabilitySchema}'s
|
|
25380
|
-
* vocabulary. That enum models what the source CAN do — a sleeping camera, an
|
|
25381
|
-
* unmounted card, an index that disagrees with its own calendar — and a
|
|
25382
|
-
* switched-off source could answer perfectly well; the operator decided it
|
|
25383
|
-
* should not. Folding the choice in is how `disabled` and `broken` stop being
|
|
25384
|
-
* distinguishable, which is the D62 rule this repo has already paid for twice:
|
|
25385
|
-
* an off switch is REPORTED off (`CameraStatus.switchedOff`, the same word),
|
|
25386
|
-
* and disabled must never look like broken. It is also what lets every
|
|
25387
|
-
* exhaustive consumer of the availability enum keep compiling.
|
|
25388
|
-
*
|
|
25389
|
-
* A switched-off source contributes NO clips (`listClips` never calls it) and
|
|
25390
|
-
* its row carries no `catalogAsOf`: nothing confirms a catalog it is not
|
|
25391
|
-
* allowed to serve, and a frozen age that can only grow draws a stalling
|
|
25392
|
-
* source rather than an off switch.
|
|
25393
|
-
*
|
|
25394
|
-
* The row survives BECAUSE it is the control the operator switches the source
|
|
25395
|
-
* back on from — D554 decision 4's rule ("a source that cannot answer
|
|
25396
|
-
* produces a ROW, not an absence") applied to the one case D556 carved out of
|
|
25397
|
-
* it, and D557's own kept property ("a source is never hidden, only its
|
|
25398
|
-
* rows"). Absent means on.
|
|
25399
|
-
*
|
|
25400
|
-
* A provider never sets this — like {@link ClipSourceSchema.addonId} it is
|
|
25401
|
-
* stamped by the collection dispatcher, which holds the registry's projection
|
|
25402
|
-
* of the persisted authority (D556) and is the only place that knows it.
|
|
25403
|
-
*/
|
|
25404
|
-
switchedOff: boolean().optional()
|
|
25396
|
+
availability: ClipSourceAvailabilitySchema
|
|
25405
25397
|
});
|
|
25406
25398
|
var videoclipsCapability = {
|
|
25407
25399
|
name: "videoclips",
|
|
@@ -25421,17 +25413,31 @@ var videoclipsCapability = {
|
|
|
25421
25413
|
* `device-config-widget-wrapped-binding.spec.ts` pins that a `kind:'wrapped'`
|
|
25422
25414
|
* binding entry derives the same section a native one does.
|
|
25423
25415
|
*
|
|
25424
|
-
*
|
|
25425
|
-
*
|
|
25426
|
-
*
|
|
25416
|
+
* **It is a SECTION of the Recording tab, at the end of it — not a tab of
|
|
25417
|
+
* its own.** It shipped as a `clips` top-tab and the operator rejected the
|
|
25418
|
+
* placement: *"utilizzerei la stessa tab recordings, lì abbiamo già tutto il
|
|
25419
|
+
* necessario, una nuova sezione alla fine per le clips"*. The Recording tab
|
|
25420
|
+
* already holds the recorder's panel, the schedule bands and the unified
|
|
25421
|
+
* retention policy; footage the camera itself holds is the same question,
|
|
25422
|
+
* asked of a different store. `order: 100` puts it after all of them with
|
|
25423
|
+
* room left in front. The `clips` entry in `WELL_KNOWN_TABS` went with it —
|
|
25424
|
+
* a well-known id nobody declares is an invitation to mint the tab again.
|
|
25425
|
+
*
|
|
25426
|
+
* `topTab` stays: the browser owns a pane (a day's tiles, a source list and
|
|
25427
|
+
* a player) and the Config tab's inner bar has no room for one. The widget
|
|
25428
|
+
* itself is `host/clips-browser` in ui-library's `HOST_WIDGETS`, and because
|
|
25429
|
+
* the admin Recordings page renders every `location:'top-tab'` +
|
|
25430
|
+
* `tab:'recording'` section behind its camera picker
|
|
25431
|
+
* (`CameraRecordingSettingsSection`), this declaration lands the section on
|
|
25432
|
+
* BOTH surfaces with no second wiring.
|
|
25427
25433
|
*/
|
|
25428
25434
|
deviceConfig: { ui: {
|
|
25429
25435
|
kind: "widget",
|
|
25430
25436
|
widgetId: "host/clips-browser",
|
|
25431
|
-
tab: "
|
|
25437
|
+
tab: "recording",
|
|
25432
25438
|
topTab: true,
|
|
25433
25439
|
label: "Clips",
|
|
25434
|
-
order:
|
|
25440
|
+
order: 100
|
|
25435
25441
|
} },
|
|
25436
25442
|
methods: {
|
|
25437
25443
|
listClips: method(object({
|
|
@@ -25440,13 +25446,23 @@ var videoclipsCapability = {
|
|
|
25440
25446
|
until: number(),
|
|
25441
25447
|
limit: number().int().positive().optional(),
|
|
25442
25448
|
/**
|
|
25443
|
-
*
|
|
25444
|
-
*
|
|
25445
|
-
*
|
|
25446
|
-
*
|
|
25447
|
-
*
|
|
25449
|
+
* WHICH provider to ask — the `addonId` a {@link ClipSourceSchema} row
|
|
25450
|
+
* carries, never a source id and never a list.
|
|
25451
|
+
*
|
|
25452
|
+
* Absent means the device's BOUND provider, which is CamStack on every
|
|
25453
|
+
* camera today because that is what `deviceManager.getBindings`
|
|
25454
|
+
* answers with. A provider the device is not bound to is refused by
|
|
25455
|
+
* name rather than answered by another one (D552's
|
|
25456
|
+
* `rejectUnresolvedAddonPin` rule).
|
|
25457
|
+
*
|
|
25458
|
+
* It replaced `sources?: string[]`, a VIEW filter over source ids that
|
|
25459
|
+
* assumed the answer was a fan-out over everything a camera has. The
|
|
25460
|
+
* operator settled otherwise on 2026-09-20 — *"il get videoclips
|
|
25461
|
+
* prendere un provider opzionale, di default va su camstack"*,
|
|
25462
|
+
* *"l'utilizzatore è uno solo"* — so the list is asked of one provider
|
|
25463
|
+
* at a time and there is nothing to filter out of it.
|
|
25448
25464
|
*/
|
|
25449
|
-
|
|
25465
|
+
provider: string().optional()
|
|
25450
25466
|
}), array(ClipSchema).readonly(), {
|
|
25451
25467
|
kind: "query",
|
|
25452
25468
|
auth: "protected"
|
|
@@ -41424,6 +41440,12 @@ Object.freeze({
|
|
|
41424
41440
|
addonId: null,
|
|
41425
41441
|
access: "view"
|
|
41426
41442
|
},
|
|
41443
|
+
"pipelineAnalytics.ownersWithMedia": {
|
|
41444
|
+
capName: "pipeline-analytics",
|
|
41445
|
+
capScope: "device",
|
|
41446
|
+
addonId: null,
|
|
41447
|
+
access: "view"
|
|
41448
|
+
},
|
|
41427
41449
|
"pipelineAnalytics.pauseForStorageMigration": {
|
|
41428
41450
|
capName: "pipeline-analytics",
|
|
41429
41451
|
capScope: "device",
|
|
@@ -45324,6 +45346,11 @@ Object.freeze({
|
|
|
45324
45346
|
form: "single",
|
|
45325
45347
|
optional: false
|
|
45326
45348
|
}],
|
|
45349
|
+
"pipelineAnalytics.ownersWithMedia": [{
|
|
45350
|
+
name: "deviceId",
|
|
45351
|
+
form: "single",
|
|
45352
|
+
optional: false
|
|
45353
|
+
}],
|
|
45327
45354
|
"pipelineAnalytics.proposeRetrainAnnotations": [{
|
|
45328
45355
|
name: "deviceId",
|
|
45329
45356
|
form: "single",
|
|
@@ -69343,11 +69370,11 @@ var require_yazl = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
|
|
|
69343
69370
|
}
|
|
69344
69371
|
}));
|
|
69345
69372
|
//#endregion
|
|
69346
|
-
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-
|
|
69373
|
+
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-UIQ527B6.js
|
|
69347
69374
|
var import_undici = require_undici();
|
|
69348
69375
|
var import_yazl = /* @__PURE__ */ require_chunk.__toESM(require_yazl(), 1);
|
|
69349
69376
|
var formatterCache = /* @__PURE__ */ new Map();
|
|
69350
|
-
function formatterFor$
|
|
69377
|
+
function formatterFor$2(timeZone) {
|
|
69351
69378
|
const cached = formatterCache.get(timeZone);
|
|
69352
69379
|
if (cached) return cached;
|
|
69353
69380
|
const fmt = new Intl.DateTimeFormat("en-US", {
|
|
@@ -69372,7 +69399,7 @@ function wallClockParts(d, timeZone) {
|
|
|
69372
69399
|
minute: d.getMinutes(),
|
|
69373
69400
|
second: d.getSeconds()
|
|
69374
69401
|
};
|
|
69375
|
-
const parts = formatterFor$
|
|
69402
|
+
const parts = formatterFor$2(timeZone).formatToParts(d);
|
|
69376
69403
|
const pick = (type) => {
|
|
69377
69404
|
const p = parts.find((x) => x.type === type);
|
|
69378
69405
|
return Number.parseInt(p?.value ?? "", 10);
|
|
@@ -70689,13 +70716,13 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70689
70716
|
sec: 59
|
|
70690
70717
|
};
|
|
70691
70718
|
}
|
|
70692
|
-
|
|
70719
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Date conversion: start=${start.toISOString()} -> ReolinkTime=${JSON.stringify(startTime)}, end=${end.toISOString()} -> ReolinkTime=${JSON.stringify(endTime)}`);
|
|
70693
70720
|
if (!bypassCache) {
|
|
70694
70721
|
this.cleanRecordingsCache();
|
|
70695
70722
|
const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
|
|
70696
70723
|
const cached = this.recordingsCache.get(cacheKey);
|
|
70697
70724
|
if (cached && cached.expiresAt > Date.now()) {
|
|
70698
|
-
|
|
70725
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Cache hit: returning ${cached.data.length} cached enriched recordings`);
|
|
70699
70726
|
return cached.data;
|
|
70700
70727
|
}
|
|
70701
70728
|
}
|
|
@@ -70747,7 +70774,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70747
70774
|
const firstResult = dayResponse[0];
|
|
70748
70775
|
if (firstResult && firstResult.code === 0 && firstResult.value?.SearchResult?.File) {
|
|
70749
70776
|
const files = firstResult.value.SearchResult.File;
|
|
70750
|
-
if (files.length > 0 && files[0])
|
|
70777
|
+
if (files.length > 0 && files[0]) require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response for day ${day}/${month}/${year}: ${JSON.stringify({
|
|
70751
70778
|
fileCount: files.length,
|
|
70752
70779
|
sampleFile: {
|
|
70753
70780
|
name: files[0].name,
|
|
@@ -70783,7 +70810,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70783
70810
|
} catch (e) {}
|
|
70784
70811
|
enriched2.push(this.enrichVodFile(vodFile, channel, streamUrl));
|
|
70785
70812
|
}
|
|
70786
|
-
|
|
70813
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched2.length} enriched recording files from autoSearchByDay`);
|
|
70787
70814
|
if (!bypassCache) {
|
|
70788
70815
|
const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
|
|
70789
70816
|
this.recordingsCache.set(cacheKey, {
|
|
@@ -70811,7 +70838,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70811
70838
|
const response = await this.call("Search", param, 0);
|
|
70812
70839
|
allResults.push(...response);
|
|
70813
70840
|
} else {
|
|
70814
|
-
|
|
70841
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Range spans ${dayRanges.length} days, splitting into separate day queries`);
|
|
70815
70842
|
for (const range of dayRanges) {
|
|
70816
70843
|
const param = { Search: {
|
|
70817
70844
|
channel,
|
|
@@ -70821,7 +70848,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70821
70848
|
EndTime: range.end
|
|
70822
70849
|
} };
|
|
70823
70850
|
if (iLogicChannel > 0) param.Search.iLogicChannel = iLogicChannel;
|
|
70824
|
-
|
|
70851
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Querying day: ${range.start.year}-${range.start.mon}-${range.start.day} (${range.start.hour}:${range.start.min}:${range.start.sec} to ${range.end.hour}:${range.end.min}:${range.end.sec})`);
|
|
70825
70852
|
const response = await this.call("Search", param, 0);
|
|
70826
70853
|
allResults.push(...response);
|
|
70827
70854
|
}
|
|
@@ -70831,7 +70858,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70831
70858
|
const firstResult = result[0];
|
|
70832
70859
|
if (firstResult && firstResult.code === 0 && firstResult.value?.SearchResult?.File) {
|
|
70833
70860
|
const files = firstResult.value.SearchResult.File;
|
|
70834
|
-
if (files.length > 0 && files[0])
|
|
70861
|
+
if (files.length > 0 && files[0]) require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response (normal search): ${JSON.stringify({
|
|
70835
70862
|
fileCount: files.length,
|
|
70836
70863
|
sampleFile: {
|
|
70837
70864
|
name: files[0].name,
|
|
@@ -70848,12 +70875,12 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70848
70875
|
const searchResult = res.value?.SearchResult;
|
|
70849
70876
|
if (searchResult?.File) allFiles.push(...searchResult.File);
|
|
70850
70877
|
}
|
|
70851
|
-
|
|
70878
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Collected ${allFiles.length} raw VodFiles from API search results`);
|
|
70852
70879
|
if (allFiles.length > 0) {
|
|
70853
70880
|
const sampleSize = Math.min(3, allFiles.length);
|
|
70854
70881
|
for (let i = 0; i < sampleSize; i++) {
|
|
70855
70882
|
const file = allFiles[i];
|
|
70856
|
-
if (file)
|
|
70883
|
+
if (file) require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Sample file ${i + 1}/${allFiles.length}: ${JSON.stringify({
|
|
70857
70884
|
name: file.name,
|
|
70858
70885
|
type: file.type,
|
|
70859
70886
|
size: file.size,
|
|
@@ -70876,7 +70903,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70876
70903
|
} catch (e) {}
|
|
70877
70904
|
enriched.push(this.enrichVodFile(vodFile, channel, streamUrl));
|
|
70878
70905
|
}
|
|
70879
|
-
|
|
70906
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched.length} enriched recording files`);
|
|
70880
70907
|
if (!bypassCache) {
|
|
70881
70908
|
const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
|
|
70882
70909
|
this.recordingsCache.set(cacheKey, {
|
|
@@ -70913,7 +70940,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70913
70940
|
prepare: true,
|
|
70914
70941
|
seek: seekSeconds
|
|
70915
70942
|
});
|
|
70916
|
-
|
|
70943
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`);
|
|
70917
70944
|
const { spawn: spawn2 } = await import("child_process");
|
|
70918
70945
|
return new Promise((resolve, reject) => {
|
|
70919
70946
|
const chunks = [];
|
|
@@ -70961,7 +70988,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70961
70988
|
reject(/* @__PURE__ */ new Error(`ffmpeg produced too small output: ${jpeg.length} bytes`));
|
|
70962
70989
|
return;
|
|
70963
70990
|
}
|
|
70964
|
-
|
|
70991
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Successfully extracted thumbnail: ${jpeg.length} bytes`);
|
|
70965
70992
|
resolve(jpeg);
|
|
70966
70993
|
} else reject(/* @__PURE__ */ new Error(`ffmpeg exited with code ${code}: ${stderr.slice(-500)}`));
|
|
70967
70994
|
});
|
|
@@ -71010,10 +71037,10 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
71010
71037
|
}
|
|
71011
71038
|
} }
|
|
71012
71039
|
}];
|
|
71013
|
-
|
|
71040
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Request body: ${JSON.stringify(body)}`);
|
|
71014
71041
|
try {
|
|
71015
71042
|
const response = await this.callMany(body);
|
|
71016
|
-
|
|
71043
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Response: ${JSON.stringify(response)}`);
|
|
71017
71044
|
const first = response[0];
|
|
71018
71045
|
if (!first || first.code !== 0 || !first.value?.fileList) {
|
|
71019
71046
|
const rspCode = first?.error?.rspCode;
|
|
@@ -71034,7 +71061,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
71034
71061
|
if (!filename) throw new Error(`NvrDownload: no files found in response`);
|
|
71035
71062
|
return filename;
|
|
71036
71063
|
} catch (error) {
|
|
71037
|
-
|
|
71064
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
71038
71065
|
throw error;
|
|
71039
71066
|
}
|
|
71040
71067
|
}
|
|
@@ -71230,7 +71257,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
71230
71257
|
* Convert a VodFile to RecordingFile with parsed metadata.
|
|
71231
71258
|
*/
|
|
71232
71259
|
enrichVodFile(vodFile, channel, streamUrl) {
|
|
71233
|
-
|
|
71260
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `RAW VodFile from API: ${JSON.stringify({
|
|
71234
71261
|
name: vodFile.name,
|
|
71235
71262
|
type: vodFile.type,
|
|
71236
71263
|
size: vodFile.size,
|
|
@@ -71283,7 +71310,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
71283
71310
|
hashBits0to15: hashRevBin.slice(0, 16)
|
|
71284
71311
|
};
|
|
71285
71312
|
}
|
|
71286
|
-
|
|
71313
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Parsed filename: ${JSON.stringify({
|
|
71287
71314
|
fileName: vodFile.name,
|
|
71288
71315
|
filenameParts,
|
|
71289
71316
|
hexValueFromFilename,
|
|
@@ -71310,13 +71337,13 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
71310
71337
|
const hexFlags = parsed?.flags;
|
|
71311
71338
|
if (parsed?.devType === "hub" && parsed?.version === 4) {
|
|
71312
71339
|
if (hexValueFromFilename && hexValueFromFilename !== "unknown") {
|
|
71313
|
-
|
|
71314
|
-
|
|
71340
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `WARNING: Hub v4 hex value "${hexValueFromFilename}" appears to be constant across files. Detection flags are likely NOT encoded in filename for this version. Detection information may need to be retrieved via a separate API call or may not be available.`);
|
|
71341
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `NOTE: For Hub v4, the API response only provides 'type' field which is "${vodFile.type}". No detection-specific fields are available in the Search API response.`);
|
|
71315
71342
|
}
|
|
71316
71343
|
}
|
|
71317
|
-
|
|
71344
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Hex flags from filename: ${JSON.stringify(hexFlags)}`);
|
|
71318
71345
|
const typeFlags = this.parseRecordTypeFlags(vodFile.type);
|
|
71319
|
-
|
|
71346
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Type flags from recordType "${vodFile.type}": ${JSON.stringify(typeFlags)}`);
|
|
71320
71347
|
const hasPerson = (hexFlags?.aiPerson ?? false) || typeFlags.hasPerson;
|
|
71321
71348
|
const hasVehicle = (hexFlags?.aiVehicle ?? false) || typeFlags.hasVehicle;
|
|
71322
71349
|
const hasAnimal = (hexFlags?.aiAnimal ?? false) || typeFlags.hasAnimal;
|
|
@@ -71327,7 +71354,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
71327
71354
|
const hasPackage = (hexFlags?.package ?? false) || typeFlags.hasPackage;
|
|
71328
71355
|
const hasRf = (hexFlags?.rf ?? false) || typeFlags.hasRf;
|
|
71329
71356
|
const hasOther = (hexFlags?.aiOther ?? false) || typeFlags.hasOther;
|
|
71330
|
-
|
|
71357
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Final merged flags: ${JSON.stringify({
|
|
71331
71358
|
hasPerson,
|
|
71332
71359
|
hasVehicle,
|
|
71333
71360
|
hasAnimal,
|
|
@@ -71733,8 +71760,8 @@ function nowIsoCompact() {
|
|
|
71733
71760
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
71734
71761
|
}
|
|
71735
71762
|
function nalTypesSummary(videoType, accessUnitAnnexB) {
|
|
71736
|
-
if (videoType === "H264") return
|
|
71737
|
-
return
|
|
71763
|
+
if (videoType === "H264") return require_chunk_ZBACPERX.splitAnnexBToNalPayloads(accessUnitAnnexB).map((n) => (n[0] ?? 0) & 31);
|
|
71764
|
+
return require_chunk_ZBACPERX.splitAnnexBToNalPayloads2(accessUnitAnnexB).map((nal) => require_chunk_ZBACPERX.getH265NalType(nal)).filter((t) => typeof t === "number");
|
|
71738
71765
|
}
|
|
71739
71766
|
function normalizeProfiles(p) {
|
|
71740
71767
|
const out = [];
|
|
@@ -72174,7 +72201,7 @@ async function sampleStreams(opts) {
|
|
|
72174
72201
|
});
|
|
72175
72202
|
};
|
|
72176
72203
|
api.client.on("frame", onFrame);
|
|
72177
|
-
const videoStream = new
|
|
72204
|
+
const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
|
|
72178
72205
|
client: api.client,
|
|
72179
72206
|
api,
|
|
72180
72207
|
channel,
|
|
@@ -73224,7 +73251,7 @@ async function runMultifocalDiagnosticsConsecutively(params) {
|
|
|
73224
73251
|
let audioFrames = 0;
|
|
73225
73252
|
let lastSnapshotAtMs = 0;
|
|
73226
73253
|
const client = params.api.client;
|
|
73227
|
-
const videoStream = new
|
|
73254
|
+
const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
|
|
73228
73255
|
client,
|
|
73229
73256
|
channel: chNative,
|
|
73230
73257
|
profile,
|
|
@@ -73308,7 +73335,7 @@ async function runMultifocalDiagnosticsConsecutively(params) {
|
|
|
73308
73335
|
const baseStreamName = profile === "main" ? "mainStream" : profile === "sub" ? "subStream" : "externStream";
|
|
73309
73336
|
const headerStreamTypeCandidates = profile === "sub" ? [1, 3] : profile === "main" ? [0, 2] : [0];
|
|
73310
73337
|
const channelIdTagCandidates = uniq([chNative, chNative + 1].filter((n) => Number.isFinite(n) && n >= 0));
|
|
73311
|
-
const messageClassCandidates = params.probeFull ? [
|
|
73338
|
+
const messageClassCandidates = params.probeFull ? [require_chunk_ZBACPERX.BC_CLASS_MODERN_24, 0] : [require_chunk_ZBACPERX.BC_CLASS_MODERN_24];
|
|
73312
73339
|
const extensionXmlCandidates = params.probeFull ? ["", buildChannelExtensionXml(chNative)] : [buildChannelExtensionXml(chNative)];
|
|
73313
73340
|
const attempts = [];
|
|
73314
73341
|
for (const messageClass of messageClassCandidates) for (const streamTypeHeader of headerStreamTypeCandidates) for (const extensionXml of extensionXmlCandidates) {
|
|
@@ -75829,14 +75856,14 @@ var require_lz4 = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
|
|
|
75829
75856
|
};
|
|
75830
75857
|
}));
|
|
75831
75858
|
//#endregion
|
|
75832
|
-
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-
|
|
75859
|
+
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-5PJ6XPBF.js
|
|
75833
75860
|
var import_fxp = require_fxp();
|
|
75834
75861
|
var import_lz4 = /* @__PURE__ */ require_chunk.__toESM(require_lz4(), 1);
|
|
75835
75862
|
function encodeHeader(h) {
|
|
75836
|
-
const hasOffset =
|
|
75863
|
+
const hasOffset = require_chunk_ZBACPERX.bcHeaderHasPayloadOffset(h.messageClass);
|
|
75837
75864
|
const headerLen = hasOffset ? 24 : 20;
|
|
75838
75865
|
const buf = Buffer.alloc(headerLen);
|
|
75839
|
-
const magic = h.magic ??
|
|
75866
|
+
const magic = h.magic ?? require_chunk_ZBACPERX.BC_MAGIC;
|
|
75840
75867
|
if (magic.length !== 4) throw new Error("magic must be 4 bytes");
|
|
75841
75868
|
magic.copy(buf, 0);
|
|
75842
75869
|
buf.writeUInt32LE(h.cmdId >>> 0, 4);
|
|
@@ -75852,7 +75879,7 @@ function encodeHeader(h) {
|
|
|
75852
75879
|
function decodeHeader(buf) {
|
|
75853
75880
|
if (buf.length < 20) throw new Error("not enough data for Baichuan header");
|
|
75854
75881
|
const magic = buf.subarray(0, 4);
|
|
75855
|
-
if (!magic.equals(
|
|
75882
|
+
if (!magic.equals(require_chunk_ZBACPERX.BC_MAGIC) && !magic.equals(require_chunk_ZBACPERX.BC_MAGIC_REV)) throw new Error(`invalid Baichuan magic: ${magic.toString("hex")}`);
|
|
75856
75883
|
const cmdId = buf.readUInt32LE(4);
|
|
75857
75884
|
const bodyLen = buf.readUInt32LE(8);
|
|
75858
75885
|
const channelId = buf.readUInt8(12);
|
|
@@ -75860,7 +75887,7 @@ function decodeHeader(buf) {
|
|
|
75860
75887
|
const msgNum = buf.readUInt16LE(14);
|
|
75861
75888
|
const responseCode = buf.readUInt16LE(16);
|
|
75862
75889
|
const messageClass = buf.readUInt16LE(18);
|
|
75863
|
-
const headerLen =
|
|
75890
|
+
const headerLen = require_chunk_ZBACPERX.bcHeaderHasPayloadOffset(messageClass) ? 24 : 20;
|
|
75864
75891
|
if (buf.length < headerLen) throw new Error("not enough data for Baichuan header (needs 24 bytes)");
|
|
75865
75892
|
const messageKey = buf.readUInt32LE(12);
|
|
75866
75893
|
const header = {
|
|
@@ -75930,9 +75957,9 @@ var BaichuanFrameParser = class {
|
|
|
75930
75957
|
this.needed = 4;
|
|
75931
75958
|
break;
|
|
75932
75959
|
}
|
|
75933
|
-
if (!this.buffer.subarray(0, 4).equals(
|
|
75934
|
-
const idx = this.buffer.indexOf(
|
|
75935
|
-
const idxRev = this.buffer.indexOf(
|
|
75960
|
+
if (!this.buffer.subarray(0, 4).equals(require_chunk_ZBACPERX.BC_MAGIC) && !this.buffer.subarray(0, 4).equals(require_chunk_ZBACPERX.BC_MAGIC_REV)) {
|
|
75961
|
+
const idx = this.buffer.indexOf(require_chunk_ZBACPERX.BC_MAGIC);
|
|
75962
|
+
const idxRev = this.buffer.indexOf(require_chunk_ZBACPERX.BC_MAGIC_REV);
|
|
75936
75963
|
const next = idx === -1 ? idxRev : idxRev === -1 ? idx : Math.min(idx, idxRev);
|
|
75937
75964
|
if (next === -1) {
|
|
75938
75965
|
this.buffer = this.buffer.subarray(Math.max(0, this.buffer.length - 3));
|
|
@@ -77947,7 +77974,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
77947
77974
|
constructor(options) {
|
|
77948
77975
|
super();
|
|
77949
77976
|
this.opts = options;
|
|
77950
|
-
this.debugCfg =
|
|
77977
|
+
this.debugCfg = require_chunk_ZBACPERX.normalizeDebugOptions(options.debugOptions);
|
|
77951
77978
|
this.logger = createDebugGateLogger(options.logger, this.debugCfg.general || this.debugCfg.traceNativeStream || this.debugCfg.traceRecordings || this.debugCfg.traceTalk || this.debugCfg.traceEvents || this.debugCfg.debugRtsp);
|
|
77952
77979
|
this.on("error", (err) => {
|
|
77953
77980
|
this.logFixed("error", {
|
|
@@ -78429,7 +78456,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
78429
78456
|
streamType: 0,
|
|
78430
78457
|
msgNum,
|
|
78431
78458
|
responseCode: 0,
|
|
78432
|
-
messageClass:
|
|
78459
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
78433
78460
|
payloadOffset: 0
|
|
78434
78461
|
});
|
|
78435
78462
|
this.logDebug("udp_keepalive_ping_tx", {
|
|
@@ -78461,7 +78488,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
78461
78488
|
cmdId: 93,
|
|
78462
78489
|
channel: this.opts.channel ?? 0,
|
|
78463
78490
|
channelIdOverride: 0,
|
|
78464
|
-
messageClass:
|
|
78491
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
78465
78492
|
streamType: 0,
|
|
78466
78493
|
extensionXml: "",
|
|
78467
78494
|
internal: true
|
|
@@ -78800,7 +78827,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
78800
78827
|
cmdId: 2,
|
|
78801
78828
|
payloadXml: logoutXml,
|
|
78802
78829
|
extensionXml: "",
|
|
78803
|
-
messageClass:
|
|
78830
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
78804
78831
|
channelIdOverride: effectiveHostChannelId,
|
|
78805
78832
|
timeoutMs: 5e3
|
|
78806
78833
|
})).header.responseCode;
|
|
@@ -78913,12 +78940,12 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
78913
78940
|
};
|
|
78914
78941
|
s.frames++;
|
|
78915
78942
|
if (now - s.lastLogMs >= 1e3) {
|
|
78916
|
-
|
|
78943
|
+
require_chunk_ZBACPERX.debugLog(this.debugCfg, this.logger, "BaichuanRx", `rx cmdId=3 frames=${s.frames} lastMsgNum=${frame.header.msgNum} lastResponseCode=${frame.header.responseCode} lastChannelId=${frame.header.channelId} lastStreamType=${frame.header.streamType} lastBodyLen=${frame.body.length} lastPayloadLen=${frame.payload.length}`);
|
|
78917
78944
|
s.lastLogMs = now;
|
|
78918
78945
|
s.frames = 0;
|
|
78919
78946
|
}
|
|
78920
78947
|
this.rxCmdTraceStats.set(3, s);
|
|
78921
|
-
} else
|
|
78948
|
+
} else require_chunk_ZBACPERX.debugLog(this.debugCfg, this.logger, "BaichuanRx", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} streamType=${frame.header.streamType} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
|
|
78922
78949
|
if (this.transport === "udp" && frame.header.cmdId === 234) {
|
|
78923
78950
|
try {
|
|
78924
78951
|
const header = encodeHeader({
|
|
@@ -78955,7 +78982,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
78955
78982
|
}
|
|
78956
78983
|
return;
|
|
78957
78984
|
}
|
|
78958
|
-
if (this.debugCfg.traceTalk && isTalkCmd(frame.header.cmdId))
|
|
78985
|
+
if (this.debugCfg.traceTalk && isTalkCmd(frame.header.cmdId)) require_chunk_ZBACPERX.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
|
|
78959
78986
|
this.emit("frame", frame);
|
|
78960
78987
|
const key = `${frame.header.cmdId}:${frame.header.msgNum}`;
|
|
78961
78988
|
const pending = this.pending.get(key);
|
|
@@ -78976,7 +79003,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
78976
79003
|
};
|
|
78977
79004
|
s.frames++;
|
|
78978
79005
|
if (now2 - s.lastLogMs >= 500) {
|
|
78979
|
-
|
|
79006
|
+
require_chunk_ZBACPERX.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `rx stream frames cmdId=3 msgNum=${frame.header.msgNum} frames=${s.frames} lastChannelId=${frame.header.channelId} lastBodyLen=${frame.body.length} lastPayloadLen=${frame.payload.length} lastPayloadOffset=${frame.header.payloadOffset ?? 0}`);
|
|
78980
79007
|
s.lastLogMs = now2;
|
|
78981
79008
|
s.frames = 0;
|
|
78982
79009
|
}
|
|
@@ -79007,7 +79034,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79007
79034
|
const eventLogTag = sid ? `BaichuanEvent sid=${sid}` : "BaichuanEvent";
|
|
79008
79035
|
const events$1 = this.parseEvents(frame);
|
|
79009
79036
|
for (const event of events$1) {
|
|
79010
|
-
|
|
79037
|
+
require_chunk_ZBACPERX.eventTraceLog(this.debugCfg, this.logger, eventLogTag, `dispatch cmdId=33 msgNum=${frame.header.msgNum} channelId=${frame.header.channelId} type=${event.type} eventChannel=${event.channel}` + (event.type === "ai" ? ` aiType=${event.ai?.type ?? "unknown"} detected=${event.ai?.detected ?? ""}` : "") + (event.type === "motion" ? ` source=${event.motion?.source ?? ""}` : ""));
|
|
79011
79038
|
this.emit("event", event);
|
|
79012
79039
|
}
|
|
79013
79040
|
} catch (error) {
|
|
@@ -79066,7 +79093,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79066
79093
|
if (!xml || !xml.startsWith("<?xml")) return [];
|
|
79067
79094
|
if (this.debugCfg.traceEvents) {
|
|
79068
79095
|
const snippet = xml.length > 500 ? `${xml.slice(0, 500)}...` : xml;
|
|
79069
|
-
|
|
79096
|
+
require_chunk_ZBACPERX.eventTraceLog(this.debugCfg, this.logger, eventRawLogTag, `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} xml=${JSON.stringify(snippet)}`);
|
|
79070
79097
|
}
|
|
79071
79098
|
const fallbackChannelId = frame.header.channelId;
|
|
79072
79099
|
const fallbackChannel = fallbackChannelId === 250 ? 0 : Math.max(0, fallbackChannelId - 1);
|
|
@@ -79084,7 +79111,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79084
79111
|
const timeStampRaw2 = (getXmlText(alarmXml, "timeStamp") ?? getXmlText(alarmXml, "timestamp") ?? "").trim();
|
|
79085
79112
|
const timeStampNum2 = timeStampRaw2.length ? Number(timeStampRaw2) : void 0;
|
|
79086
79113
|
const alarmTimeStamp2 = Number.isFinite(timeStampNum2) ? timeStampNum2 : void 0;
|
|
79087
|
-
if (this.debugCfg.traceEvents)
|
|
79114
|
+
if (this.debugCfg.traceEvents) require_chunk_ZBACPERX.eventTraceLog(this.debugCfg, this.logger, eventRawLogTag, `AlarmEvent channel=${channel} status=${JSON.stringify(status2)} aiType=${JSON.stringify(aiTypeRaw2)}`);
|
|
79088
79115
|
const statusTokens2 = status2.toLowerCase().split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
79089
79116
|
const visitorActive2 = statusTokens2.includes("visitor");
|
|
79090
79117
|
const dayNightActive2 = statusTokens2.includes("dn") || statusTokens2.includes("daynight");
|
|
@@ -79358,15 +79385,15 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79358
79385
|
const extBuf = Buffer.from(extXml, "utf8");
|
|
79359
79386
|
const payloadBuf = Buffer.from(payloadXml, "utf8");
|
|
79360
79387
|
if (enc.kind === "none") return Buffer.concat([extBuf, payloadBuf]);
|
|
79361
|
-
if (enc.kind === "bc") return Buffer.concat([
|
|
79362
|
-
if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([
|
|
79388
|
+
if (enc.kind === "bc") return Buffer.concat([require_chunk_ZBACPERX.bcEncrypt(extBuf, channelId), require_chunk_ZBACPERX.bcEncrypt(payloadBuf, channelId)]);
|
|
79389
|
+
if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_ZBACPERX.aesEncrypt(extBuf, enc.key), require_chunk_ZBACPERX.aesEncrypt(payloadBuf, enc.key)]);
|
|
79363
79390
|
return Buffer.concat([extBuf, payloadBuf]);
|
|
79364
79391
|
}
|
|
79365
79392
|
encodeBodyBinary(extXml, payload, channelId, enc) {
|
|
79366
79393
|
const extBuf = Buffer.from(extXml, "utf8");
|
|
79367
79394
|
if (enc.kind === "none") return Buffer.concat([extBuf, payload]);
|
|
79368
|
-
if (enc.kind === "bc") return Buffer.concat([
|
|
79369
|
-
if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([
|
|
79395
|
+
if (enc.kind === "bc") return Buffer.concat([require_chunk_ZBACPERX.bcEncrypt(extBuf, channelId), payload]);
|
|
79396
|
+
if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_ZBACPERX.aesEncrypt(extBuf, enc.key), payload]);
|
|
79370
79397
|
return Buffer.concat([extBuf, payload]);
|
|
79371
79398
|
}
|
|
79372
79399
|
/**
|
|
@@ -79411,7 +79438,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79411
79438
|
bodyLen,
|
|
79412
79439
|
binaryPayload: true
|
|
79413
79440
|
});
|
|
79414
|
-
if (this.debugCfg.traceTalk && isTalkCmd(cmdId))
|
|
79441
|
+
if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset} binaryPayloadLen=${params.payload.length}`);
|
|
79415
79442
|
this.recordTx({
|
|
79416
79443
|
cmdId,
|
|
79417
79444
|
responseCode: 0,
|
|
@@ -79428,8 +79455,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79428
79455
|
let dec;
|
|
79429
79456
|
try {
|
|
79430
79457
|
if (enc.kind === "none") dec = buf;
|
|
79431
|
-
else if (enc.kind === "bc") dec =
|
|
79432
|
-
else dec =
|
|
79458
|
+
else if (enc.kind === "bc") dec = require_chunk_ZBACPERX.bcDecrypt(buf, channelId);
|
|
79459
|
+
else dec = require_chunk_ZBACPERX.aesDecrypt(buf, enc.key);
|
|
79433
79460
|
} catch {
|
|
79434
79461
|
return;
|
|
79435
79462
|
}
|
|
@@ -79495,8 +79522,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79495
79522
|
messageClass,
|
|
79496
79523
|
bodyLen
|
|
79497
79524
|
});
|
|
79498
|
-
if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4))
|
|
79499
|
-
if (this.debugCfg.traceTalk && isTalkCmd(cmdId))
|
|
79525
|
+
if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_ZBACPERX.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
|
|
79526
|
+
if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
|
|
79500
79527
|
this.recordTx({
|
|
79501
79528
|
cmdId,
|
|
79502
79529
|
responseCode: 0,
|
|
@@ -79511,8 +79538,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79511
79538
|
responseCode: frame.header.responseCode,
|
|
79512
79539
|
msgNum: frame.header.msgNum
|
|
79513
79540
|
});
|
|
79514
|
-
if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4))
|
|
79515
|
-
if (this.debugCfg.traceTalk && isTalkCmd(cmdId))
|
|
79541
|
+
if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_ZBACPERX.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
|
|
79542
|
+
if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
|
|
79516
79543
|
if (frame.header.responseCode === 400) {
|
|
79517
79544
|
if (frame.body.length === 0) throw new Error("Baichuan authentication failed (responseCode 400, empty body) - check username/password");
|
|
79518
79545
|
}
|
|
@@ -79582,9 +79609,9 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79582
79609
|
messageClass,
|
|
79583
79610
|
bodyLen
|
|
79584
79611
|
});
|
|
79585
|
-
if (_BaichuanClient.recordingCmdIds.has(cmdId))
|
|
79586
|
-
if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4))
|
|
79587
|
-
if (this.debugCfg.traceTalk && isTalkCmd(cmdId))
|
|
79612
|
+
if (_BaichuanClient.recordingCmdIds.has(cmdId)) require_chunk_ZBACPERX.recordingsTraceLog(this.debugCfg, this.logger, "BaichuanRecordings", `tx recording cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} bodyLen=${bodyLen}`);
|
|
79613
|
+
if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_ZBACPERX.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
|
|
79614
|
+
if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
|
|
79588
79615
|
this.recordTx({
|
|
79589
79616
|
cmdId,
|
|
79590
79617
|
responseCode: 0,
|
|
@@ -79599,9 +79626,9 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79599
79626
|
responseCode: frame.header.responseCode,
|
|
79600
79627
|
msgNum: frame.header.msgNum
|
|
79601
79628
|
});
|
|
79602
|
-
if (_BaichuanClient.recordingCmdIds.has(frame.header.cmdId))
|
|
79603
|
-
if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4))
|
|
79604
|
-
if (this.debugCfg.traceTalk && isTalkCmd(cmdId))
|
|
79629
|
+
if (_BaichuanClient.recordingCmdIds.has(frame.header.cmdId)) require_chunk_ZBACPERX.recordingsTraceLog(this.debugCfg, this.logger, "BaichuanRecordings", `rx recording cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
|
|
79630
|
+
if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_ZBACPERX.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
|
|
79631
|
+
if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
|
|
79605
79632
|
if (!internal) this.kickIdleDisconnectTimer();
|
|
79606
79633
|
return frame;
|
|
79607
79634
|
}
|
|
@@ -80423,8 +80450,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
80423
80450
|
const tryAs = (enc) => {
|
|
80424
80451
|
try {
|
|
80425
80452
|
if (enc.kind === "none") return buf;
|
|
80426
|
-
if (enc.kind === "bc") return
|
|
80427
|
-
if (enc.kind === "aes" || enc.kind === "full_aes") return
|
|
80453
|
+
if (enc.kind === "bc") return require_chunk_ZBACPERX.bcDecrypt(buf, channelId);
|
|
80454
|
+
if (enc.kind === "aes" || enc.kind === "full_aes") return require_chunk_ZBACPERX.aesDecrypt(buf, enc.key);
|
|
80428
80455
|
return null;
|
|
80429
80456
|
} catch {
|
|
80430
80457
|
return null;
|
|
@@ -80454,7 +80481,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
80454
80481
|
streamType: 0,
|
|
80455
80482
|
msgNum,
|
|
80456
80483
|
responseCode: encByte,
|
|
80457
|
-
messageClass:
|
|
80484
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_LEGACY
|
|
80458
80485
|
});
|
|
80459
80486
|
const pendingKey = `${cmdId}:${msgNum}`;
|
|
80460
80487
|
const framePromise = new Promise((resolve, reject) => {
|
|
@@ -80486,15 +80513,15 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
80486
80513
|
else if (encType === 1) this.enc = { kind: "bc" };
|
|
80487
80514
|
else if (encType === 2) this.enc = {
|
|
80488
80515
|
kind: "aes",
|
|
80489
|
-
key:
|
|
80516
|
+
key: require_chunk_ZBACPERX.deriveAesKey(nonce, this.opts.password)
|
|
80490
80517
|
};
|
|
80491
80518
|
else if (encType === 18) this.enc = {
|
|
80492
80519
|
kind: "full_aes",
|
|
80493
|
-
key:
|
|
80520
|
+
key: require_chunk_ZBACPERX.deriveAesKey(nonce, this.opts.password)
|
|
80494
80521
|
};
|
|
80495
80522
|
else throw new Error(`Baichuan login: unknown encType=0x${encType.toString(16)}`);
|
|
80496
|
-
const userHash =
|
|
80497
|
-
const passHash =
|
|
80523
|
+
const userHash = require_chunk_ZBACPERX.md5StrModern(`${this.opts.username}${nonce}`);
|
|
80524
|
+
const passHash = require_chunk_ZBACPERX.md5StrModern(`${this.opts.password}${nonce}`);
|
|
80498
80525
|
const loginXml = buildLoginXml(userHash, passHash);
|
|
80499
80526
|
this.logDebug("login_hash", {
|
|
80500
80527
|
username: this.opts.username,
|
|
@@ -80509,7 +80536,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
80509
80536
|
cmdId: 1,
|
|
80510
80537
|
payloadXml: loginXml,
|
|
80511
80538
|
extensionXml: "",
|
|
80512
|
-
messageClass:
|
|
80539
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
80513
80540
|
channelIdOverride: effectiveHostChannelId,
|
|
80514
80541
|
encryption: loginEnc,
|
|
80515
80542
|
timeoutMs: 1e4
|
|
@@ -80569,7 +80596,7 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
80569
80596
|
client = api.client;
|
|
80570
80597
|
}
|
|
80571
80598
|
}
|
|
80572
|
-
const videoStream = new
|
|
80599
|
+
const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
|
|
80573
80600
|
client,
|
|
80574
80601
|
api,
|
|
80575
80602
|
channel,
|
|
@@ -81087,7 +81114,7 @@ var H264Flow = class extends BaseFlow {
|
|
|
81087
81114
|
super(transport, "H264");
|
|
81088
81115
|
}
|
|
81089
81116
|
extractParameterSets(accessUnitAnnexB) {
|
|
81090
|
-
const nals =
|
|
81117
|
+
const nals = require_chunk_ZBACPERX.splitAnnexBToNalPayloads(accessUnitAnnexB);
|
|
81091
81118
|
for (const nal of nals) {
|
|
81092
81119
|
if (nal.length < 1) continue;
|
|
81093
81120
|
const nalType = (nal[0] ?? 0) & 31;
|
|
@@ -81132,9 +81159,9 @@ var H265Flow = class extends BaseFlow {
|
|
|
81132
81159
|
super(transport, "H265");
|
|
81133
81160
|
}
|
|
81134
81161
|
extractParameterSets(accessUnitAnnexB) {
|
|
81135
|
-
const vps =
|
|
81136
|
-
const sps =
|
|
81137
|
-
const pps =
|
|
81162
|
+
const vps = require_chunk_ZBACPERX.extractVpsFromAnnexB(accessUnitAnnexB);
|
|
81163
|
+
const sps = require_chunk_ZBACPERX.extractSpsFromAnnexB(accessUnitAnnexB);
|
|
81164
|
+
const pps = require_chunk_ZBACPERX.extractPpsFromAnnexB(accessUnitAnnexB);
|
|
81138
81165
|
if (vps && !this.vps) this.vps = vps;
|
|
81139
81166
|
if (sps && !this.sps) this.sps = sps;
|
|
81140
81167
|
if (pps && !this.pps) this.pps = pps;
|
|
@@ -81518,8 +81545,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
|
|
|
81518
81545
|
/** Returns true if the raw (packed/Annex B) frame is an IDR (H.264) or IRAP (H.265). */
|
|
81519
81546
|
isRawFrameKeyframe(frame) {
|
|
81520
81547
|
try {
|
|
81521
|
-
if (frame.videoType === "H264") return _BaichuanRtspServer.splitAnnexBNals(
|
|
81522
|
-
if (frame.videoType === "H265") return
|
|
81548
|
+
if (frame.videoType === "H264") return _BaichuanRtspServer.splitAnnexBNals(require_chunk_ZBACPERX.convertToAnnexB(frame.data)).some((n) => n.length >= 1 && (n[0] & 31) === 5);
|
|
81549
|
+
if (frame.videoType === "H265") return require_chunk_ZBACPERX.splitAnnexBToNalPayloads2(require_chunk_ZBACPERX.convertToAnnexB2(frame.data)).some((n) => n.length >= 2 && require_chunk_ZBACPERX.isH265Irap(n[0] >> 1 & 63));
|
|
81523
81550
|
} catch {}
|
|
81524
81551
|
return false;
|
|
81525
81552
|
}
|
|
@@ -82419,13 +82446,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
|
|
|
82419
82446
|
if (resources?.rtpAudioTimestamp !== void 0) resources.rtpAudioTimestamp = resources.rtpAudioTimestamp + 1024 >>> 0;
|
|
82420
82447
|
};
|
|
82421
82448
|
const isH265IrapAccessUnit = (annexB) => {
|
|
82422
|
-
const nals =
|
|
82449
|
+
const nals = require_chunk_ZBACPERX.splitAnnexBToNalPayloads2(annexB);
|
|
82423
82450
|
for (const nal of nals) {
|
|
82424
82451
|
if (nal.length < 2) continue;
|
|
82425
82452
|
const b0 = nal[0];
|
|
82426
82453
|
if (b0 === void 0) continue;
|
|
82427
82454
|
if ((b0 & 128) !== 0) continue;
|
|
82428
|
-
if (
|
|
82455
|
+
if (require_chunk_ZBACPERX.isH265Irap(b0 >> 1 & 63)) return true;
|
|
82429
82456
|
}
|
|
82430
82457
|
return false;
|
|
82431
82458
|
};
|
|
@@ -82581,7 +82608,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
|
|
|
82581
82608
|
continue;
|
|
82582
82609
|
}
|
|
82583
82610
|
if (frame.videoType === "H264" || frame.videoType === "H265") {
|
|
82584
|
-
const normalizedVideoData = frame.videoType === "H264" ?
|
|
82611
|
+
const normalizedVideoData = frame.videoType === "H264" ? require_chunk_ZBACPERX.convertToAnnexB(frame.data) : require_chunk_ZBACPERX.convertToAnnexB2(frame.data);
|
|
82585
82612
|
if (frameCount === 0) this.setFlowVideoType(frame.videoType, "first video frame");
|
|
82586
82613
|
if (!this.flow.getFmtp().hasParamSets) {
|
|
82587
82614
|
this.flow.extractParameterSets(normalizedVideoData);
|
|
@@ -82597,7 +82624,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
|
|
|
82597
82624
|
}
|
|
82598
82625
|
if (useDirectRtp) {
|
|
82599
82626
|
const videoType = frame.videoType ?? this.flow.videoType;
|
|
82600
|
-
const normalizedVideoData = videoType === "H264" ?
|
|
82627
|
+
const normalizedVideoData = videoType === "H264" ? require_chunk_ZBACPERX.convertToAnnexB(frame.data) : require_chunk_ZBACPERX.convertToAnnexB2(frame.data);
|
|
82601
82628
|
if (!resources?.seenFirstVideoKeyframe) if (videoType === "H265") {
|
|
82602
82629
|
const { hasParamSets } = this.flow.getFmtp();
|
|
82603
82630
|
if (!hasParamSets) {
|
|
@@ -82673,7 +82700,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
|
|
|
82673
82700
|
const headHex = frame.data.subarray(0, 16).toString("hex");
|
|
82674
82701
|
this.rtspDebugLog(`First video frame written to ffmpeg stdin for client ${clientId} (len=${frame.data.length}, head=${headHex})`);
|
|
82675
82702
|
}
|
|
82676
|
-
if (!stdin.write(frame.videoType === "H264" ?
|
|
82703
|
+
if (!stdin.write(frame.videoType === "H264" ? require_chunk_ZBACPERX.convertToAnnexB(frame.data) : frame.data)) await new Promise((resolve) => {
|
|
82677
82704
|
if (stdin) stdin.once("drain", () => resolve());
|
|
82678
82705
|
else resolve();
|
|
82679
82706
|
});
|
|
@@ -82785,7 +82812,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
|
|
|
82785
82812
|
ensureContinuousStream(dedicatedClient) {
|
|
82786
82813
|
if (this.continuousStream) return this.continuousStream;
|
|
82787
82814
|
const createLiveStream = async () => {
|
|
82788
|
-
return new
|
|
82815
|
+
return new require_chunk_ZBACPERX.BaichuanVideoStream({
|
|
82789
82816
|
client: dedicatedClient ?? this.api.client,
|
|
82790
82817
|
api: this.api,
|
|
82791
82818
|
channel: this.channel,
|
|
@@ -83933,6 +83960,128 @@ function patchMotionSensitivityListXml(currentXml, bands) {
|
|
|
83933
83960
|
const fragment = encodeMotionSensitivityListXml(bands);
|
|
83934
83961
|
return currentXml.replace(/<sensitivityInfoList[\s\S]*?(?:\/>|<\/sensitivityInfoList>)/, fragment);
|
|
83935
83962
|
}
|
|
83963
|
+
var ADTS_SAMPLES_PER_FRAME = 1024;
|
|
83964
|
+
var ADTS_SAMPLE_RATES = [
|
|
83965
|
+
96e3,
|
|
83966
|
+
88200,
|
|
83967
|
+
64e3,
|
|
83968
|
+
48e3,
|
|
83969
|
+
44100,
|
|
83970
|
+
32e3,
|
|
83971
|
+
24e3,
|
|
83972
|
+
22050,
|
|
83973
|
+
16e3,
|
|
83974
|
+
12e3,
|
|
83975
|
+
11025,
|
|
83976
|
+
8e3,
|
|
83977
|
+
7350,
|
|
83978
|
+
null,
|
|
83979
|
+
null,
|
|
83980
|
+
null
|
|
83981
|
+
];
|
|
83982
|
+
var ADTS_PROFILES = [
|
|
83983
|
+
"Main",
|
|
83984
|
+
"LC",
|
|
83985
|
+
"SSR",
|
|
83986
|
+
"LTP"
|
|
83987
|
+
];
|
|
83988
|
+
var parseAdtsHeader = (frame) => {
|
|
83989
|
+
if (frame.length < 7) return null;
|
|
83990
|
+
const b0 = frame[0];
|
|
83991
|
+
const b1 = frame[1];
|
|
83992
|
+
if (b0 !== 255 || (b1 & 240) !== 240) return null;
|
|
83993
|
+
const b2 = frame[2];
|
|
83994
|
+
const b3 = frame[3];
|
|
83995
|
+
const b4 = frame[4];
|
|
83996
|
+
const b5 = frame[5];
|
|
83997
|
+
const profile = b2 >> 6 & 3;
|
|
83998
|
+
const sampleRateIndex = b2 >> 2 & 15;
|
|
83999
|
+
const channelConfig = (b2 & 1) << 2 | b3 >> 6 & 3;
|
|
84000
|
+
const frameLength = (b3 & 3) << 11 | b4 << 3 | b5 >> 5 & 7;
|
|
84001
|
+
if (frameLength < 7) return null;
|
|
84002
|
+
return {
|
|
84003
|
+
profile,
|
|
84004
|
+
profileName: ADTS_PROFILES[profile] ?? "unknown",
|
|
84005
|
+
sampleRate: ADTS_SAMPLE_RATES[sampleRateIndex] ?? null,
|
|
84006
|
+
channels: channelConfig > 0 ? channelConfig : null,
|
|
84007
|
+
frameLength
|
|
84008
|
+
};
|
|
84009
|
+
};
|
|
84010
|
+
var buildRecordingAudioTrack = (params) => {
|
|
84011
|
+
const { codec, frames } = params;
|
|
84012
|
+
if (codec == null || frames.length === 0) return null;
|
|
84013
|
+
const data = frames.length === 1 ? frames[0] : Buffer.concat([...frames]);
|
|
84014
|
+
if (codec === "Adpcm") return {
|
|
84015
|
+
codec,
|
|
84016
|
+
format: "adpcm",
|
|
84017
|
+
data,
|
|
84018
|
+
frames: frames.length,
|
|
84019
|
+
bytes: data.length,
|
|
84020
|
+
sampleRate: null,
|
|
84021
|
+
channels: null,
|
|
84022
|
+
durationSeconds: null
|
|
84023
|
+
};
|
|
84024
|
+
const head = parseAdtsHeader(frames[0]);
|
|
84025
|
+
if (head == null) return {
|
|
84026
|
+
codec,
|
|
84027
|
+
format: "unknown",
|
|
84028
|
+
data,
|
|
84029
|
+
frames: frames.length,
|
|
84030
|
+
bytes: data.length,
|
|
84031
|
+
sampleRate: null,
|
|
84032
|
+
channels: null,
|
|
84033
|
+
durationSeconds: null
|
|
84034
|
+
};
|
|
84035
|
+
const durationSeconds = head.sampleRate != null && head.sampleRate > 0 ? frames.length * ADTS_SAMPLES_PER_FRAME / head.sampleRate : null;
|
|
84036
|
+
return {
|
|
84037
|
+
codec,
|
|
84038
|
+
format: "adts",
|
|
84039
|
+
data,
|
|
84040
|
+
frames: frames.length,
|
|
84041
|
+
bytes: data.length,
|
|
84042
|
+
sampleRate: head.sampleRate,
|
|
84043
|
+
channels: head.channels,
|
|
84044
|
+
durationSeconds
|
|
84045
|
+
};
|
|
84046
|
+
};
|
|
84047
|
+
var roundRecordingFps = (fps) => {
|
|
84048
|
+
if (fps > 14 && fps < 16) return 15;
|
|
84049
|
+
if (fps > 23 && fps < 26) return 25;
|
|
84050
|
+
if (fps > 29 && fps < 31) return 30;
|
|
84051
|
+
return Math.round(fps * 100) / 100;
|
|
84052
|
+
};
|
|
84053
|
+
var estimateVideoTiming = (params) => {
|
|
84054
|
+
const { timestampsUs } = params;
|
|
84055
|
+
const infoFps = params.infoFps != null && params.infoFps > 0 ? params.infoFps : null;
|
|
84056
|
+
const accessUnits = timestampsUs.length;
|
|
84057
|
+
if (accessUnits >= 2) {
|
|
84058
|
+
const spanUs = timestampsUs[accessUnits - 1] - timestampsUs[0];
|
|
84059
|
+
if (spanUs > 0) {
|
|
84060
|
+
const durationSeconds = spanUs / 1e6;
|
|
84061
|
+
return {
|
|
84062
|
+
accessUnits,
|
|
84063
|
+
fps: roundRecordingFps((accessUnits - 1) / durationSeconds),
|
|
84064
|
+
durationSeconds,
|
|
84065
|
+
infoFps,
|
|
84066
|
+
fpsSource: "timestamps"
|
|
84067
|
+
};
|
|
84068
|
+
}
|
|
84069
|
+
}
|
|
84070
|
+
if (infoFps != null) return {
|
|
84071
|
+
accessUnits,
|
|
84072
|
+
fps: infoFps,
|
|
84073
|
+
durationSeconds: accessUnits > 0 ? accessUnits / infoFps : null,
|
|
84074
|
+
infoFps,
|
|
84075
|
+
fpsSource: "infoFps"
|
|
84076
|
+
};
|
|
84077
|
+
return {
|
|
84078
|
+
accessUnits,
|
|
84079
|
+
fps: null,
|
|
84080
|
+
durationSeconds: null,
|
|
84081
|
+
infoFps,
|
|
84082
|
+
fpsSource: "unknown"
|
|
84083
|
+
};
|
|
84084
|
+
};
|
|
83936
84085
|
var buildDetectionClasses = (parsed, recordType) => {
|
|
83937
84086
|
const classes = [];
|
|
83938
84087
|
const flags = parsed?.flags;
|
|
@@ -86442,7 +86591,7 @@ var sendTalkConfigWithReset = async (params) => {
|
|
|
86442
86591
|
channel: params.channel,
|
|
86443
86592
|
...params.channelIdOverride != null ? { channelIdOverride: params.channelIdOverride } : {},
|
|
86444
86593
|
payloadXml: params.payloadXml,
|
|
86445
|
-
messageClass:
|
|
86594
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
86446
86595
|
})).header.responseCode;
|
|
86447
86596
|
};
|
|
86448
86597
|
const code = await trySend();
|
|
@@ -86452,7 +86601,7 @@ var sendTalkConfigWithReset = async (params) => {
|
|
|
86452
86601
|
channel: params.channel,
|
|
86453
86602
|
...params.channelIdOverride != null ? { channelIdOverride: params.channelIdOverride } : {},
|
|
86454
86603
|
payloadXml: "",
|
|
86455
|
-
messageClass:
|
|
86604
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
86456
86605
|
});
|
|
86457
86606
|
const retryCode = await trySend();
|
|
86458
86607
|
if (retryCode !== 200) throw new Error(`TalkConfig rejected after reset (responseCode ${retryCode})`);
|
|
@@ -86519,7 +86668,7 @@ var createBufferedTalkSession = (params) => {
|
|
|
86519
86668
|
...channelIdOverride != null ? { channelIdOverride } : {},
|
|
86520
86669
|
extensionXml: buildBinaryExtensionXml(channel),
|
|
86521
86670
|
payload: Buffer.concat(parts),
|
|
86522
|
-
messageClass:
|
|
86671
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
86523
86672
|
});
|
|
86524
86673
|
const playLengthMs = samplesSent / info.audioConfig.sampleRate * 1e3;
|
|
86525
86674
|
const now = Date.now();
|
|
@@ -86565,7 +86714,7 @@ var createBufferedTalkSession = (params) => {
|
|
|
86565
86714
|
channel,
|
|
86566
86715
|
...channelIdOverride != null ? { channelIdOverride } : {},
|
|
86567
86716
|
payloadXml: "",
|
|
86568
|
-
messageClass:
|
|
86717
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
86569
86718
|
});
|
|
86570
86719
|
if (frame.header.responseCode !== 200) throw new Error(`TalkReset rejected (responseCode ${frame.header.responseCode})`);
|
|
86571
86720
|
if (params.closeSocketOnStop) try {
|
|
@@ -88248,7 +88397,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
88248
88397
|
if (debugCfg) {
|
|
88249
88398
|
const sid = this.client.getSocketSessionId?.();
|
|
88250
88399
|
const tag2 = sid ? `ReolinkSimpleEvent sid=${sid}` : "ReolinkSimpleEvent";
|
|
88251
|
-
|
|
88400
|
+
require_chunk_ZBACPERX.eventTraceLog(debugCfg, this.logger, tag2, `dispatch type=${evt.type} channel=${evt.channel} timestamp=${evt.timestamp}`);
|
|
88252
88401
|
}
|
|
88253
88402
|
for (const cb of this.simpleEventListeners) try {
|
|
88254
88403
|
Promise.resolve(cb(evt)).catch((e) => {
|
|
@@ -88259,7 +88408,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
88259
88408
|
}
|
|
88260
88409
|
}
|
|
88261
88410
|
constructor(opts) {
|
|
88262
|
-
const dbg =
|
|
88411
|
+
const dbg = require_chunk_ZBACPERX.normalizeDebugOptions(opts.debugOptions);
|
|
88263
88412
|
this.logger = createDebugGateLogger(opts.logger, dbg.general || dbg.traceNativeStream || dbg.traceRecordings || dbg.traceTalk || dbg.traceEvents || dbg.debugRtsp);
|
|
88264
88413
|
this.clientOptions = {
|
|
88265
88414
|
host: opts.host,
|
|
@@ -88847,7 +88996,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
88847
88996
|
return;
|
|
88848
88997
|
}
|
|
88849
88998
|
entry.startInFlight = (async () => {
|
|
88850
|
-
const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => require("./BaichuanVideoStream-
|
|
88999
|
+
const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => require("./BaichuanVideoStream-FOE2DBVZ-DJcEkVak.js"));
|
|
88851
89000
|
const sessionKey = `live:object-detections:ch${entry.channel}:${entry.profile}`;
|
|
88852
89001
|
const dedicated = await this.createDedicatedSession(sessionKey);
|
|
88853
89002
|
const stream$12 = new BaichuanVideoStream2({
|
|
@@ -89328,7 +89477,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
89328
89477
|
channel: ch,
|
|
89329
89478
|
...channelIdOverride != null ? { channelIdOverride } : {},
|
|
89330
89479
|
payloadXml: "",
|
|
89331
|
-
messageClass:
|
|
89480
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
89332
89481
|
});
|
|
89333
89482
|
if (frame.header.responseCode !== 200) throw new Error(`TalkReset rejected (responseCode ${frame.header.responseCode})`);
|
|
89334
89483
|
}
|
|
@@ -89347,7 +89496,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
89347
89496
|
channel: ch,
|
|
89348
89497
|
...channelIdOverride != null ? { channelIdOverride } : {},
|
|
89349
89498
|
payloadXml,
|
|
89350
|
-
messageClass:
|
|
89499
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
89351
89500
|
})).header.responseCode;
|
|
89352
89501
|
};
|
|
89353
89502
|
const code = await trySend();
|
|
@@ -89499,7 +89648,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
89499
89648
|
cmdId: 10,
|
|
89500
89649
|
channel,
|
|
89501
89650
|
payloadXml: "",
|
|
89502
|
-
messageClass:
|
|
89651
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
89503
89652
|
});
|
|
89504
89653
|
return parseTalkAbilityXml(frame.body.length === 0 ? "" : client.tryDecryptXml(frame.body, frame.header.channelId, client.enc));
|
|
89505
89654
|
}
|
|
@@ -90572,8 +90721,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90572
90721
|
const start = params.start;
|
|
90573
90722
|
const endOfStartDay = endOfWallClockDay(start, timeZone);
|
|
90574
90723
|
const end = params.end.getTime() > endOfStartDay.getTime() ? endOfStartDay : params.end;
|
|
90575
|
-
|
|
90576
|
-
|
|
90724
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclips", `Query: start=${start.toISOString()}, end=${end.toISOString()} (forced same day)`);
|
|
90725
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclips", `Using UID for channel ${channel}: ${uid}`);
|
|
90577
90726
|
const streamType = params.streamType ?? "subStream";
|
|
90578
90727
|
const recordType = params.recordType ?? "manual,sched,io,md,people,face,vehicle,dog_cat,visitor,other,package,crossline,intrusion,loitering,legacy,loss";
|
|
90579
90728
|
const fileRecordTypes = params.fileRecordTypes ?? (params.recordType === void 0 ? DEFAULT_RECORDING_SEARCH_FILE_RECORD_TYPES : void 0);
|
|
@@ -90592,7 +90741,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90592
90741
|
...timeZone !== void 0 ? { timeZone } : {}
|
|
90593
90742
|
});
|
|
90594
90743
|
const unique = dedupeRecordingFiles(files);
|
|
90595
|
-
|
|
90744
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclips", `FileInfoList complete: ${unique.length} unique files (from ${files.length} total)`);
|
|
90596
90745
|
return unique;
|
|
90597
90746
|
});
|
|
90598
90747
|
}
|
|
@@ -90633,7 +90782,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90633
90782
|
const start = params.start;
|
|
90634
90783
|
const endOfStartDay = endOfWallClockDay(start, timeZone);
|
|
90635
90784
|
const end = params.end.getTime() > endOfStartDay.getTime() ? endOfStartDay : params.end;
|
|
90636
|
-
if (end !== params.end)
|
|
90785
|
+
if (end !== params.end) require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "searchAlarmVideos", `Window clamped to one camera-local day: ${params.end.toISOString()} \u2192 ${end.toISOString()} (the firmware answers the START day only)`);
|
|
90637
90786
|
const windows = await searchAlarmVideosViaFindAlarmVideo({
|
|
90638
90787
|
sendXml: (p) => this.sendXml(p),
|
|
90639
90788
|
channel,
|
|
@@ -90649,7 +90798,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90649
90798
|
maxPages: params.maxPages ?? 200,
|
|
90650
90799
|
...params.timeoutMs != null ? { timeoutMs: params.timeoutMs } : {}
|
|
90651
90800
|
});
|
|
90652
|
-
|
|
90801
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "searchAlarmVideos", `findAlarmVideo complete: ${windows.length} windows over ${new Set(windows.map((w) => w.fileName)).size} files (channel=${channel})`);
|
|
90653
90802
|
return windows;
|
|
90654
90803
|
});
|
|
90655
90804
|
}
|
|
@@ -90714,7 +90863,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90714
90863
|
seen.add(key);
|
|
90715
90864
|
return true;
|
|
90716
90865
|
});
|
|
90717
|
-
|
|
90866
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "searchEventLog", `findEventLog complete: ${unique.length} events over ${devices.length} device(s) (${events$6.length - unique.length} repeated across pages), ${unique.filter((e) => !e.hasRecFile).length} without a recording file`);
|
|
90718
90867
|
return unique;
|
|
90719
90868
|
});
|
|
90720
90869
|
}
|
|
@@ -90767,7 +90916,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90767
90916
|
});
|
|
90768
90917
|
const msgNum = 0;
|
|
90769
90918
|
dedicatedClient.subscribeVideoStream(5, msgNum);
|
|
90770
|
-
const stream$14 = new
|
|
90919
|
+
const stream$14 = new require_chunk_ZBACPERX.BaichuanVideoStream({
|
|
90771
90920
|
client: dedicatedClient,
|
|
90772
90921
|
channel,
|
|
90773
90922
|
profile: streamType === "subStream" ? "sub" : "main",
|
|
@@ -90786,7 +90935,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90786
90935
|
channel,
|
|
90787
90936
|
channelIdOverride: sessionCounter,
|
|
90788
90937
|
payloadXml,
|
|
90789
|
-
messageClass:
|
|
90938
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
90790
90939
|
msgNumOverride: 0,
|
|
90791
90940
|
timeoutMs: params.timeoutMs,
|
|
90792
90941
|
internal: true
|
|
@@ -90818,7 +90967,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90818
90967
|
cmdId: 7,
|
|
90819
90968
|
channel,
|
|
90820
90969
|
payloadXml: stopXml,
|
|
90821
|
-
messageClass:
|
|
90970
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
90822
90971
|
timeoutMs: 2e3,
|
|
90823
90972
|
internal: true
|
|
90824
90973
|
});
|
|
@@ -90875,7 +91024,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90875
91024
|
});
|
|
90876
91025
|
const msgNum = 0;
|
|
90877
91026
|
dedicatedClient.subscribeVideoStream(5, msgNum);
|
|
90878
|
-
const stream$15 = new
|
|
91027
|
+
const stream$15 = new require_chunk_ZBACPERX.BaichuanVideoStream({
|
|
90879
91028
|
client: dedicatedClient,
|
|
90880
91029
|
channel,
|
|
90881
91030
|
profile: streamType === "subStream" ? "sub" : "main",
|
|
@@ -90895,7 +91044,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90895
91044
|
channelIdOverride,
|
|
90896
91045
|
payloadXml,
|
|
90897
91046
|
extensionXml: "",
|
|
90898
|
-
messageClass:
|
|
91047
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
90899
91048
|
msgNumOverride: msgNum,
|
|
90900
91049
|
timeoutMs: params.timeoutMs,
|
|
90901
91050
|
internal: true
|
|
@@ -90927,7 +91076,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90927
91076
|
cmdId: 7,
|
|
90928
91077
|
channel,
|
|
90929
91078
|
payloadXml: stopXml,
|
|
90930
|
-
messageClass:
|
|
91079
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
90931
91080
|
timeoutMs: 2e3,
|
|
90932
91081
|
internal: true
|
|
90933
91082
|
});
|
|
@@ -91014,35 +91163,35 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
91014
91163
|
async ensureUidForRecordings(channel, explicitUid) {
|
|
91015
91164
|
const dbg = this.client.getDebugConfig?.();
|
|
91016
91165
|
const logger = this.logger;
|
|
91017
|
-
|
|
91166
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Checking UID: explicitUid=${explicitUid}, this.uid=${this.uid}`);
|
|
91018
91167
|
const trimmedExplicit = explicitUid?.trim();
|
|
91019
91168
|
if (trimmedExplicit) {
|
|
91020
|
-
|
|
91169
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using explicit UID: ${trimmedExplicit}`);
|
|
91021
91170
|
return trimmedExplicit;
|
|
91022
91171
|
}
|
|
91023
91172
|
const perChannel = await this.discoverUidForRecordingsForChannel(channel);
|
|
91024
91173
|
if (perChannel) {
|
|
91025
|
-
|
|
91174
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID: ${perChannel}`);
|
|
91026
91175
|
return perChannel;
|
|
91027
91176
|
}
|
|
91028
91177
|
const configured = this.uid?.trim();
|
|
91029
91178
|
if (configured) {
|
|
91030
|
-
|
|
91179
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using configured UID: ${configured}`);
|
|
91031
91180
|
return configured;
|
|
91032
91181
|
}
|
|
91033
91182
|
if (this.nativeOnly) {
|
|
91034
91183
|
const viaGetUid = await this.discoverUidViaGetUidNative(channel);
|
|
91035
91184
|
if (viaGetUid) {
|
|
91036
|
-
|
|
91185
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using device UID from cmd 114 GetUid: ${viaGetUid}`);
|
|
91037
91186
|
return viaGetUid;
|
|
91038
91187
|
}
|
|
91039
|
-
|
|
91188
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: no UID available (channel=${channel})`);
|
|
91040
91189
|
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.");
|
|
91041
91190
|
}
|
|
91042
|
-
|
|
91191
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `No UID available, attempting device auto-discovery (ch=${channel})`);
|
|
91043
91192
|
const discovered = await this.discoverDeviceUidForRecordings(channel);
|
|
91044
91193
|
if (discovered) return discovered;
|
|
91045
|
-
|
|
91194
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Auto-discovery failed - no UID found`);
|
|
91046
91195
|
throw new Error("UID is required to access recordings. Provide a UID explicitly or configure the client with a UID.");
|
|
91047
91196
|
}
|
|
91048
91197
|
cacheChannelUid(channel, uid) {
|
|
@@ -91065,28 +91214,28 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
91065
91214
|
const logger = this.logger;
|
|
91066
91215
|
const fromPush = this.getUidFromPushCacheForChannel(channel);
|
|
91067
91216
|
if (fromPush) {
|
|
91068
|
-
|
|
91217
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from push cache: ${fromPush}`);
|
|
91069
91218
|
return fromPush;
|
|
91070
91219
|
}
|
|
91071
91220
|
if (this.nativeOnly) {
|
|
91072
|
-
|
|
91221
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: skipping per-channel UID discovery via HTTP/CGI (channel=${channel})`);
|
|
91073
91222
|
return;
|
|
91074
91223
|
}
|
|
91075
91224
|
try {
|
|
91076
|
-
|
|
91225
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Attempting per-channel UID discovery via HTTP CGI GetChannelstatus (channel=${channel})`);
|
|
91077
91226
|
const uidCandidate = await discoverPerChannelUidViaCgiChannelstatus({
|
|
91078
91227
|
channel,
|
|
91079
91228
|
login: () => this.cgiApi.login(),
|
|
91080
91229
|
getChannelstatus: () => this.cgiApi.GetChannelstatus()
|
|
91081
91230
|
});
|
|
91082
|
-
|
|
91231
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus channel=${channel} uid=${uidCandidate || "(missing)"}`);
|
|
91083
91232
|
if (uidCandidate) {
|
|
91084
91233
|
this.cacheChannelUid(channel, uidCandidate);
|
|
91085
|
-
|
|
91234
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from GetChannelstatus: ${uidCandidate}`);
|
|
91086
91235
|
return uidCandidate;
|
|
91087
91236
|
}
|
|
91088
91237
|
} catch (e) {
|
|
91089
|
-
|
|
91238
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus failed: ${formatErrorForLog(e)}`);
|
|
91090
91239
|
}
|
|
91091
91240
|
}
|
|
91092
91241
|
/**
|
|
@@ -91098,7 +91247,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
91098
91247
|
*/
|
|
91099
91248
|
async discoverUidViaGetUidNative(channel) {
|
|
91100
91249
|
const dbg = this.client.getDebugConfig?.();
|
|
91101
|
-
const trace = (message) =>
|
|
91250
|
+
const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, this.logger, "ensureUidForRecordings", message);
|
|
91102
91251
|
let isNvr = this._isNvr;
|
|
91103
91252
|
if (isNvr === void 0) try {
|
|
91104
91253
|
isNvr = await this.isNvrDevice();
|
|
@@ -91124,7 +91273,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
91124
91273
|
if (this.nativeOnly) return void 0;
|
|
91125
91274
|
const dbg = this.client.getDebugConfig?.();
|
|
91126
91275
|
const logger = this.logger;
|
|
91127
|
-
const trace = (message) =>
|
|
91276
|
+
const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", message);
|
|
91128
91277
|
const discoveredUid = await discoverDeviceUidForRecordings({
|
|
91129
91278
|
channel,
|
|
91130
91279
|
getInfo: () => this.getInfo(),
|
|
@@ -91199,7 +91348,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
91199
91348
|
await this.client.login();
|
|
91200
91349
|
const dbg = this.client.getDebugConfig?.();
|
|
91201
91350
|
const logger = this.logger;
|
|
91202
|
-
const trace = (message) =>
|
|
91351
|
+
const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclipThumbnail", message);
|
|
91203
91352
|
const channel = this.normalizeChannel(params.channel);
|
|
91204
91353
|
const snapStreamType = (params.snapType ?? "sub") === "main" ? "mainStream" : "subStream";
|
|
91205
91354
|
const timeoutMs = params.timeoutMs ?? 3e4;
|
|
@@ -91254,7 +91403,7 @@ ${xml}`);
|
|
|
91254
91403
|
cmdId: 298,
|
|
91255
91404
|
...isNvr && headerChannelIdOverride != null ? { channelIdOverride: headerChannelIdOverride } : {},
|
|
91256
91405
|
msgNumOverride: 0,
|
|
91257
|
-
messageClass:
|
|
91406
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
91258
91407
|
streamType: 0,
|
|
91259
91408
|
payloadXml: xml,
|
|
91260
91409
|
timeoutMs
|
|
@@ -91733,7 +91882,7 @@ ${stderr}`));
|
|
|
91733
91882
|
await this.client.login();
|
|
91734
91883
|
const dbg = this.client.getDebugConfig?.();
|
|
91735
91884
|
const logger = this.logger;
|
|
91736
|
-
const trace = (message) =>
|
|
91885
|
+
const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListDownload", message);
|
|
91737
91886
|
const channel = this.normalizeChannel(params.channel);
|
|
91738
91887
|
const uid = await this.ensureUidForRecordings(channel, params.uid);
|
|
91739
91888
|
const headerChannelIdOverride = this.resolveHeaderChannelIdForLogicalChannel(channel);
|
|
@@ -91779,7 +91928,7 @@ ${stderr}`));
|
|
|
91779
91928
|
cmdId: 13,
|
|
91780
91929
|
channel,
|
|
91781
91930
|
...typeof chId === "number" ? { channelIdOverride: chId } : {},
|
|
91782
|
-
messageClass:
|
|
91931
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_FILE_DOWNLOAD,
|
|
91783
91932
|
extensionXml: buildBinaryExtensionXml(channel),
|
|
91784
91933
|
payloadXml,
|
|
91785
91934
|
streamType: st,
|
|
@@ -91808,7 +91957,7 @@ ${stderr}`));
|
|
|
91808
91957
|
await this.client.login();
|
|
91809
91958
|
const dbg = this.client.getDebugConfig?.();
|
|
91810
91959
|
const logger = this.logger;
|
|
91811
|
-
const trace = (message) =>
|
|
91960
|
+
const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListPagedDownload", message);
|
|
91812
91961
|
const channel = this.normalizeChannel(params.channel);
|
|
91813
91962
|
const uid = await this.ensureUidForRecordings(channel, params.uid);
|
|
91814
91963
|
trace(`Starting paged download: channel=${channel}, uid=${uid}, fileName=${params.fileName}`);
|
|
@@ -91845,7 +91994,7 @@ ${stderr}`));
|
|
|
91845
91994
|
await this.client.login();
|
|
91846
91995
|
const dbg = this.client.getDebugConfig?.();
|
|
91847
91996
|
const logger = this.logger;
|
|
91848
|
-
const trace = (message) =>
|
|
91997
|
+
const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListReplayBinaryDownload", message);
|
|
91849
91998
|
const channel = this.normalizeChannel(params.channel);
|
|
91850
91999
|
const headerChannelIdOverride = this.resolveHeaderChannelIdForLogicalChannel(channel);
|
|
91851
92000
|
const ident = params.fileName;
|
|
@@ -91874,7 +92023,7 @@ ${stderr}`));
|
|
|
91874
92023
|
channel,
|
|
91875
92024
|
...isNvr ? { channelIdOverride: headerChannelIdOverride ?? 82 } : {},
|
|
91876
92025
|
msgNumOverride: 0,
|
|
91877
|
-
messageClass:
|
|
92026
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
91878
92027
|
payloadXml,
|
|
91879
92028
|
streamType: 0,
|
|
91880
92029
|
timeoutMs,
|
|
@@ -91960,25 +92109,46 @@ ${stderr}`));
|
|
|
91960
92109
|
async downloadRecordingDemuxed(params) {
|
|
91961
92110
|
const raw = await this.downloadRecording(params);
|
|
91962
92111
|
const frames = [];
|
|
91963
|
-
const
|
|
92112
|
+
const timestampsUs = [];
|
|
92113
|
+
const audioFrames = [];
|
|
92114
|
+
const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
|
|
91964
92115
|
strict: false,
|
|
91965
92116
|
logger: this.logger,
|
|
91966
|
-
onVideoAccessUnit: ({ annexB }) => {
|
|
92117
|
+
onVideoAccessUnit: ({ annexB, microseconds }) => {
|
|
91967
92118
|
frames.push(annexB);
|
|
92119
|
+
timestampsUs.push(microseconds);
|
|
92120
|
+
},
|
|
92121
|
+
onAudioFrame: ({ data }) => {
|
|
92122
|
+
audioFrames.push(data);
|
|
91968
92123
|
}
|
|
91969
92124
|
});
|
|
91970
92125
|
decoder.push(raw);
|
|
91971
92126
|
const stats = decoder.getStats();
|
|
92127
|
+
const audio = buildRecordingAudioTrack({
|
|
92128
|
+
codec: stats.audioType,
|
|
92129
|
+
frames: audioFrames
|
|
92130
|
+
});
|
|
92131
|
+
const timing = estimateVideoTiming({
|
|
92132
|
+
timestampsUs,
|
|
92133
|
+
infoFps: stats.infos[0]?.fps ?? null
|
|
92134
|
+
});
|
|
92135
|
+
if (audio == null) this.logger?.debug?.(`[downloadRecordingDemuxed] no audio packet in ${stats.packets} BcMedia packets (${stats.bytesIn} bytes) for ${params.fileName}`);
|
|
91972
92136
|
return {
|
|
91973
92137
|
annexB: Buffer.concat(frames),
|
|
91974
92138
|
videoType: stats.videoType,
|
|
92139
|
+
audio,
|
|
91975
92140
|
stats: {
|
|
91976
92141
|
bytesIn: stats.bytesIn,
|
|
91977
92142
|
bytesOut: stats.bytesOut,
|
|
91978
92143
|
packets: stats.packets,
|
|
91979
92144
|
videoPackets: stats.videoPackets,
|
|
91980
92145
|
audioPackets: stats.audioPackets,
|
|
91981
|
-
|
|
92146
|
+
audioBytes: stats.audioBytesOut,
|
|
92147
|
+
keyframes: stats.keyframes,
|
|
92148
|
+
fps: timing.fps,
|
|
92149
|
+
durationSeconds: timing.durationSeconds,
|
|
92150
|
+
infoFps: timing.infoFps,
|
|
92151
|
+
fpsSource: timing.fpsSource
|
|
91982
92152
|
}
|
|
91983
92153
|
};
|
|
91984
92154
|
}
|
|
@@ -92007,7 +92177,7 @@ ${stderr}`));
|
|
|
92007
92177
|
const videoFrames = [];
|
|
92008
92178
|
const audioFrames = [];
|
|
92009
92179
|
let audioCodec = null;
|
|
92010
|
-
const decoder = new
|
|
92180
|
+
const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
|
|
92011
92181
|
strict: false,
|
|
92012
92182
|
logger: this.logger,
|
|
92013
92183
|
onVideoAccessUnit: ({ annexB, microseconds }) => {
|
|
@@ -92024,28 +92194,12 @@ ${stderr}`));
|
|
|
92024
92194
|
decoder.push(raw);
|
|
92025
92195
|
const decoderStats = decoder.getStats();
|
|
92026
92196
|
const videoCodec = decoderStats.videoType ?? "H264";
|
|
92027
|
-
|
|
92028
|
-
|
|
92029
|
-
|
|
92030
|
-
|
|
92031
|
-
|
|
92032
|
-
|
|
92033
|
-
durationSeconds = durationUs / 1e6;
|
|
92034
|
-
fps = (videoFrames.length - 1) / durationSeconds;
|
|
92035
|
-
} else {
|
|
92036
|
-
const infoFps = decoderStats.infos[0]?.fps;
|
|
92037
|
-
fps = infoFps && infoFps > 0 ? infoFps : 15;
|
|
92038
|
-
durationSeconds = videoFrames.length / fps;
|
|
92039
|
-
}
|
|
92040
|
-
} else {
|
|
92041
|
-
const infoFps = decoderStats.infos[0]?.fps;
|
|
92042
|
-
fps = infoFps && infoFps > 0 ? infoFps : 15;
|
|
92043
|
-
durationSeconds = videoFrames.length / fps;
|
|
92044
|
-
}
|
|
92045
|
-
if (fps > 14 && fps < 16) fps = 15;
|
|
92046
|
-
else if (fps > 23 && fps < 26) fps = 25;
|
|
92047
|
-
else if (fps > 29 && fps < 31) fps = 30;
|
|
92048
|
-
else fps = Math.round(fps * 100) / 100;
|
|
92197
|
+
const timing = estimateVideoTiming({
|
|
92198
|
+
timestampsUs: videoFrames.map((f) => f.microseconds),
|
|
92199
|
+
infoFps: decoderStats.infos[0]?.fps ?? null
|
|
92200
|
+
});
|
|
92201
|
+
const fps = timing.fps ?? 15;
|
|
92202
|
+
const durationSeconds = timing.durationSeconds ?? videoFrames.length / fps;
|
|
92049
92203
|
const videoData = Buffer.concat(videoFrames.map((f) => f.annexB));
|
|
92050
92204
|
const audioData = audioFrames.length > 0 ? Buffer.concat(audioFrames) : null;
|
|
92051
92205
|
const hasAudio = audioData != null && audioData.length > 0;
|
|
@@ -92421,7 +92575,7 @@ ${stderr}`));
|
|
|
92421
92575
|
params: {
|
|
92422
92576
|
cmdId: 31,
|
|
92423
92577
|
channelIdOverride: channel,
|
|
92424
|
-
messageClass:
|
|
92578
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
92425
92579
|
}
|
|
92426
92580
|
},
|
|
92427
92581
|
{
|
|
@@ -92429,7 +92583,7 @@ ${stderr}`));
|
|
|
92429
92583
|
params: {
|
|
92430
92584
|
cmdId: 31,
|
|
92431
92585
|
channelIdOverride: 251,
|
|
92432
|
-
messageClass:
|
|
92586
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
92433
92587
|
}
|
|
92434
92588
|
},
|
|
92435
92589
|
{
|
|
@@ -92437,7 +92591,7 @@ ${stderr}`));
|
|
|
92437
92591
|
params: {
|
|
92438
92592
|
cmdId: 31,
|
|
92439
92593
|
channelIdOverride: 250,
|
|
92440
|
-
messageClass:
|
|
92594
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
92441
92595
|
}
|
|
92442
92596
|
},
|
|
92443
92597
|
{
|
|
@@ -92445,7 +92599,7 @@ ${stderr}`));
|
|
|
92445
92599
|
params: {
|
|
92446
92600
|
cmdId: 31,
|
|
92447
92601
|
channelIdOverride: 250,
|
|
92448
|
-
messageClass:
|
|
92602
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
92449
92603
|
extensionXml: `<?xml version="1.0" encoding="UTF-8" ?><Extension version="1.1"><channelId>251</channelId></Extension>`
|
|
92450
92604
|
}
|
|
92451
92605
|
}
|
|
@@ -92488,7 +92642,7 @@ ${stderr}`));
|
|
|
92488
92642
|
const frame = await this.client.sendFrame({
|
|
92489
92643
|
cmdId: 31,
|
|
92490
92644
|
channelIdOverride: Number(channel),
|
|
92491
|
-
messageClass:
|
|
92645
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
92492
92646
|
timeoutMs
|
|
92493
92647
|
});
|
|
92494
92648
|
lastCode = frame.header.responseCode;
|
|
@@ -92769,7 +92923,7 @@ ${stderr}`));
|
|
|
92769
92923
|
msgNumOverride: msgNum,
|
|
92770
92924
|
extensionXml: buildChannelExtensionXml(channelId),
|
|
92771
92925
|
payloadXml,
|
|
92772
|
-
messageClass:
|
|
92926
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
92773
92927
|
streamType: config.streamType,
|
|
92774
92928
|
timeoutMs: 2e4
|
|
92775
92929
|
};
|
|
@@ -92901,7 +93055,7 @@ ${stderr}`));
|
|
|
92901
93055
|
channelIdOverride: channelId,
|
|
92902
93056
|
extensionXml: a.extensionXml,
|
|
92903
93057
|
payloadXml: a.payloadXml,
|
|
92904
|
-
messageClass:
|
|
93058
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
92905
93059
|
streamType: a.streamType,
|
|
92906
93060
|
...msgNum !== void 0 ? { msgNumOverride: msgNum } : {},
|
|
92907
93061
|
timeoutMs: 2e3
|
|
@@ -92931,7 +93085,7 @@ ${stderr}`));
|
|
|
92931
93085
|
channel: ch,
|
|
92932
93086
|
channelIdOverride: channelId,
|
|
92933
93087
|
extensionXml: buildChannelExtensionXml(channelId),
|
|
92934
|
-
messageClass:
|
|
93088
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
92935
93089
|
streamType: 0
|
|
92936
93090
|
});
|
|
92937
93091
|
} catch (e) {
|
|
@@ -92973,7 +93127,7 @@ ${stderr}`));
|
|
|
92973
93127
|
channel: ch,
|
|
92974
93128
|
channelIdOverride: channelId,
|
|
92975
93129
|
extensionXml: buildChannelExtensionXml(channelId),
|
|
92976
|
-
messageClass:
|
|
93130
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
92977
93131
|
streamType: 0
|
|
92978
93132
|
});
|
|
92979
93133
|
} catch (e) {
|
|
@@ -92995,7 +93149,7 @@ ${stderr}`));
|
|
|
92995
93149
|
channelIdOverride: channelId,
|
|
92996
93150
|
extensionXml,
|
|
92997
93151
|
payloadXml,
|
|
92998
|
-
messageClass:
|
|
93152
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
92999
93153
|
streamType: 0
|
|
93000
93154
|
});
|
|
93001
93155
|
if (frame.header.responseCode !== 200) {
|
|
@@ -93027,7 +93181,7 @@ ${stderr}`));
|
|
|
93027
93181
|
channelIdOverride: channelId,
|
|
93028
93182
|
extensionXml,
|
|
93029
93183
|
payloadXml,
|
|
93030
|
-
messageClass:
|
|
93184
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93031
93185
|
streamType: 0
|
|
93032
93186
|
});
|
|
93033
93187
|
if (frame.header.responseCode !== 200) throw new Error(`PTZ preset move rejected (response_code ${frame.header.responseCode})`);
|
|
@@ -93048,7 +93202,7 @@ ${stderr}`));
|
|
|
93048
93202
|
channelIdOverride: channelId,
|
|
93049
93203
|
extensionXml,
|
|
93050
93204
|
payloadXml,
|
|
93051
|
-
messageClass:
|
|
93205
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93052
93206
|
streamType: 0
|
|
93053
93207
|
});
|
|
93054
93208
|
if (frame.header.responseCode !== 200) throw new Error(`PTZ preset save rejected (response_code ${frame.header.responseCode})`);
|
|
@@ -93065,7 +93219,7 @@ ${stderr}`));
|
|
|
93065
93219
|
channelIdOverride: channelId,
|
|
93066
93220
|
extensionXml,
|
|
93067
93221
|
payloadXml,
|
|
93068
|
-
messageClass:
|
|
93222
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93069
93223
|
streamType: 0
|
|
93070
93224
|
});
|
|
93071
93225
|
if (frame.header.responseCode !== 200) throw new Error(`PTZ goto preset rejected (response_code ${frame.header.responseCode})`);
|
|
@@ -93092,7 +93246,7 @@ ${stderr}`));
|
|
|
93092
93246
|
channelIdOverride: channelId,
|
|
93093
93247
|
extensionXml,
|
|
93094
93248
|
payloadXml,
|
|
93095
|
-
messageClass:
|
|
93249
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93096
93250
|
streamType: 0
|
|
93097
93251
|
})).header.responseCode };
|
|
93098
93252
|
},
|
|
@@ -93146,7 +93300,7 @@ ${stderr}`));
|
|
|
93146
93300
|
channel: ch,
|
|
93147
93301
|
channelIdOverride: ch,
|
|
93148
93302
|
extensionXml: buildPtzGuardExtensionXml(ch, "read"),
|
|
93149
|
-
messageClass:
|
|
93303
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93150
93304
|
streamType: 0
|
|
93151
93305
|
}));
|
|
93152
93306
|
}
|
|
@@ -93172,7 +93326,7 @@ ${stderr}`));
|
|
|
93172
93326
|
timeoutSeconds: options.timeoutSeconds,
|
|
93173
93327
|
setPosition: options.setPosition === true
|
|
93174
93328
|
}),
|
|
93175
|
-
messageClass:
|
|
93329
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93176
93330
|
streamType: 0
|
|
93177
93331
|
});
|
|
93178
93332
|
}
|
|
@@ -93193,7 +93347,7 @@ ${stderr}`));
|
|
|
93193
93347
|
channelIdOverride: ch,
|
|
93194
93348
|
extensionXml: buildPtzGuardExtensionXml(ch, "write"),
|
|
93195
93349
|
payloadXml: buildPtzGuardGoXml(ch),
|
|
93196
|
-
messageClass:
|
|
93350
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93197
93351
|
streamType: 0
|
|
93198
93352
|
});
|
|
93199
93353
|
}
|
|
@@ -93209,7 +93363,7 @@ ${stderr}`));
|
|
|
93209
93363
|
channel: ch,
|
|
93210
93364
|
channelIdOverride: channelId,
|
|
93211
93365
|
extensionXml: buildChannelExtensionXml(channelId),
|
|
93212
|
-
messageClass:
|
|
93366
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93213
93367
|
streamType: 0
|
|
93214
93368
|
});
|
|
93215
93369
|
const parseTriplet = (sectionTag) => {
|
|
@@ -93247,7 +93401,7 @@ ${stderr}`));
|
|
|
93247
93401
|
channelIdOverride: channelId,
|
|
93248
93402
|
extensionXml,
|
|
93249
93403
|
payloadXml,
|
|
93250
|
-
messageClass:
|
|
93404
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93251
93405
|
streamType: 0
|
|
93252
93406
|
});
|
|
93253
93407
|
if (frame.header.responseCode !== 200) throw new Error(`Zoom rejected (response_code ${frame.header.responseCode})`);
|
|
@@ -94998,7 +95152,7 @@ ${xml}`);
|
|
|
94998
95152
|
* @returns Test results for all stream types and profiles
|
|
94999
95153
|
*/
|
|
95000
95154
|
async testChannelStreams(channel, logger) {
|
|
95001
|
-
const { testChannelStreams } = await Promise.resolve().then(() => require("./DiagnosticsTools-
|
|
95155
|
+
const { testChannelStreams } = await Promise.resolve().then(() => require("./DiagnosticsTools-ZC7I7QM6-CD4rRF8a.js"));
|
|
95002
95156
|
return await testChannelStreams({
|
|
95003
95157
|
api: this,
|
|
95004
95158
|
channel: this.normalizeChannel(channel),
|
|
@@ -95014,7 +95168,7 @@ ${xml}`);
|
|
|
95014
95168
|
* @returns Complete diagnostics for all channels and streams
|
|
95015
95169
|
*/
|
|
95016
95170
|
async collectMultifocalDiagnostics(logger) {
|
|
95017
|
-
const { collectMultifocalDiagnostics } = await Promise.resolve().then(() => require("./DiagnosticsTools-
|
|
95171
|
+
const { collectMultifocalDiagnostics } = await Promise.resolve().then(() => require("./DiagnosticsTools-ZC7I7QM6-CD4rRF8a.js"));
|
|
95018
95172
|
return await collectMultifocalDiagnostics({
|
|
95019
95173
|
api: this,
|
|
95020
95174
|
logger
|
|
@@ -97099,7 +97253,7 @@ ${scheduleItems}
|
|
|
97099
97253
|
if (raw.length === 0) throw new Error("Downloaded recording is empty");
|
|
97100
97254
|
const videoFrames = [];
|
|
97101
97255
|
let videoType = null;
|
|
97102
|
-
const decoder = new
|
|
97256
|
+
const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
|
|
97103
97257
|
strict: false,
|
|
97104
97258
|
logger: this.logger,
|
|
97105
97259
|
onVideoAccessUnit: ({ annexB: annexB2, microseconds }) => {
|
|
@@ -97447,7 +97601,7 @@ ${scheduleItems}
|
|
|
97447
97601
|
return {
|
|
97448
97602
|
getPlaylist: () => {
|
|
97449
97603
|
try {
|
|
97450
|
-
const { readFileSync } =
|
|
97604
|
+
const { readFileSync } = require_chunk_ZBACPERX.__require("fs");
|
|
97451
97605
|
return readFileSync(playlistPath, "utf8");
|
|
97452
97606
|
} catch {
|
|
97453
97607
|
return "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:4\n";
|
|
@@ -97456,7 +97610,7 @@ ${scheduleItems}
|
|
|
97456
97610
|
getSegment: (name) => {
|
|
97457
97611
|
if (segments.has(name)) return segments.get(name);
|
|
97458
97612
|
try {
|
|
97459
|
-
const { readFileSync } =
|
|
97613
|
+
const { readFileSync } = require_chunk_ZBACPERX.__require("fs");
|
|
97460
97614
|
const data = readFileSync(path$3.join(tempDir, name));
|
|
97461
97615
|
segments.set(name, data);
|
|
97462
97616
|
return data;
|
|
@@ -97466,7 +97620,7 @@ ${scheduleItems}
|
|
|
97466
97620
|
},
|
|
97467
97621
|
listSegments: () => {
|
|
97468
97622
|
try {
|
|
97469
|
-
const { readdirSync } =
|
|
97623
|
+
const { readdirSync } = require_chunk_ZBACPERX.__require("fs");
|
|
97470
97624
|
return readdirSync(tempDir).filter((f) => f.endsWith(".ts"));
|
|
97471
97625
|
} catch {
|
|
97472
97626
|
return [];
|
|
@@ -99051,41 +99205,41 @@ async function autoDetectDeviceType(inputs) {
|
|
|
99051
99205
|
variant: "cmd80 class=0x6414",
|
|
99052
99206
|
op: () => api.getInfo(void 0, {
|
|
99053
99207
|
timeoutMs: 2500,
|
|
99054
|
-
messageClass:
|
|
99208
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
99055
99209
|
})
|
|
99056
99210
|
},
|
|
99057
99211
|
{
|
|
99058
99212
|
variant: "cmd80 class=0x6614",
|
|
99059
99213
|
op: () => api.getInfo(void 0, {
|
|
99060
99214
|
timeoutMs: 3e3,
|
|
99061
|
-
messageClass:
|
|
99215
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
|
|
99062
99216
|
})
|
|
99063
99217
|
},
|
|
99064
99218
|
{
|
|
99065
99219
|
variant: "cmd318(ch0) class=0x6414",
|
|
99066
99220
|
op: () => api.getInfo(0, {
|
|
99067
99221
|
timeoutMs: 3e3,
|
|
99068
|
-
messageClass:
|
|
99222
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
99069
99223
|
})
|
|
99070
99224
|
},
|
|
99071
99225
|
{
|
|
99072
99226
|
variant: "cmd318(ch0) class=0x6614",
|
|
99073
99227
|
op: () => api.getInfo(0, {
|
|
99074
99228
|
timeoutMs: 3500,
|
|
99075
|
-
messageClass:
|
|
99229
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
|
|
99076
99230
|
})
|
|
99077
99231
|
}
|
|
99078
99232
|
]), runProbeVariants("getSupportInfo", [{
|
|
99079
99233
|
variant: "cmd199 class=0x6414",
|
|
99080
99234
|
op: () => api.getSupportInfo({
|
|
99081
99235
|
timeoutMs: 2500,
|
|
99082
|
-
messageClass:
|
|
99236
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
99083
99237
|
})
|
|
99084
99238
|
}, {
|
|
99085
99239
|
variant: "cmd199 class=0x6614",
|
|
99086
99240
|
op: () => api.getSupportInfo({
|
|
99087
99241
|
timeoutMs: 3500,
|
|
99088
|
-
messageClass:
|
|
99242
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
|
|
99089
99243
|
})
|
|
99090
99244
|
}])]);
|
|
99091
99245
|
const deviceInfo = infoProbe?.value;
|
|
@@ -144076,7 +144230,7 @@ function buildAacAudioSpecificConfigHex(params) {
|
|
|
144076
144230
|
if (!Number.isFinite(audioObjectType) || audioObjectType <= 0 || audioObjectType > 31) return;
|
|
144077
144231
|
return ((audioObjectType & 31) << 11 | (samplingFreqIndex & 15) << 7 | (channels & 15) << 3).toString(16).padStart(4, "0");
|
|
144078
144232
|
}
|
|
144079
|
-
function
|
|
144233
|
+
function parseAdtsHeader2(adtsFrame) {
|
|
144080
144234
|
if (adtsFrame.length < 7) return null;
|
|
144081
144235
|
if (adtsFrame[0] !== 255 || (adtsFrame[1] & 240) !== 240) return null;
|
|
144082
144236
|
const protectionAbsent = (adtsFrame[1] & 1) === 1;
|
|
@@ -144181,7 +144335,7 @@ function packetizeH265(nal, rtp, opts, markerOnLast, isLastNal) {
|
|
|
144181
144335
|
return out;
|
|
144182
144336
|
}
|
|
144183
144337
|
function packetizeAacAdtsFrame(adts, rtp) {
|
|
144184
|
-
const parsed =
|
|
144338
|
+
const parsed = parseAdtsHeader2(adts);
|
|
144185
144339
|
if (!parsed) return { packets: [] };
|
|
144186
144340
|
const raw = adts.subarray(parsed.headerLength);
|
|
144187
144341
|
if (!raw.length) return { packets: [] };
|
|
@@ -145614,7 +145768,7 @@ async function createRfc4571TcpServerInternal(options) {
|
|
|
145614
145768
|
streamClient = dedicatedSession.client;
|
|
145615
145769
|
logSessionsSummary(`dedicated session created: ${sessionKey}`);
|
|
145616
145770
|
} else streamClient = baseApi.client;
|
|
145617
|
-
const createLiveStream = async () => new
|
|
145771
|
+
const createLiveStream = async () => new require_chunk_ZBACPERX.BaichuanVideoStream({
|
|
145618
145772
|
client: streamClient,
|
|
145619
145773
|
api: baseApi,
|
|
145620
145774
|
channel: ch,
|
|
@@ -145831,7 +145985,7 @@ async function createRfc4571TcpServerInternal(options) {
|
|
|
145831
145985
|
}, isCompositeStream ? Math.min(1e4, keyframeTimeoutMs) : 5e3);
|
|
145832
145986
|
const onAudio = (frame) => {
|
|
145833
145987
|
sawAnyAudio = true;
|
|
145834
|
-
const parsed =
|
|
145988
|
+
const parsed = parseAdtsHeader2(frame);
|
|
145835
145989
|
if (!parsed) {
|
|
145836
145990
|
if (debugLogsLeft-- > 0) {
|
|
145837
145991
|
const head = frame.subarray(0, Math.min(16, frame.length)).toString("hex");
|
|
@@ -147014,6 +147168,23 @@ var reolinkCameraSchema = object({
|
|
|
147014
147168
|
motionEnabled: boolean().optional(),
|
|
147015
147169
|
motionSensitivity: number().int().min(1).max(50).optional(),
|
|
147016
147170
|
/**
|
|
147171
|
+
* Per-device clip-catalog refresh interval, in minutes — the OVERRIDE half
|
|
147172
|
+
* of the cascade whose global default lives in the addon store under the
|
|
147173
|
+
* same key. Absent means "use the global", which is the same thing a reset
|
|
147174
|
+
* means (`resolveClipCatalogRefreshMinutes`, D562). `0` turns the timer off
|
|
147175
|
+
* for this camera. What the timer DOES with it differs by topology and the
|
|
147176
|
+
* field description says both: a free read on a hub child and on mains, and
|
|
147177
|
+
* stale-marking only on a standalone battery camera.
|
|
147178
|
+
*/
|
|
147179
|
+
clipCatalogRefreshMinutes: number().int().min(0).max(1440).optional(),
|
|
147180
|
+
/**
|
|
147181
|
+
* Let the clip timer WAKE this standalone battery camera. Off by default and
|
|
147182
|
+
* per device only — a 15-minute forced timer is 96 wakes/day against a
|
|
147183
|
+
* natural rate of 1–20 clips/day on this fleet (D562). Inert on every other
|
|
147184
|
+
* topology, which is why it is one switch and not a second meaning.
|
|
147185
|
+
*/
|
|
147186
|
+
clipCatalogForcedWake: boolean().optional(),
|
|
147187
|
+
/**
|
|
147017
147188
|
* Per-AI-class sensitivity overrides — pushed via
|
|
147018
147189
|
* `setAiDetection(aiType, sensitivity)` (cmdId=343). Reolink
|
|
147019
147190
|
* range 0..100 — same scale the reolink_aio CGI exposes
|
|
@@ -147455,6 +147626,21 @@ var ClipArtifactStore = class {
|
|
|
147455
147626
|
}
|
|
147456
147627
|
};
|
|
147457
147628
|
//#endregion
|
|
147629
|
+
//#region src/clips/clip-addressing.ts
|
|
147630
|
+
/** Does addressing this camera's catalog REQUIRE a persisted uid? */
|
|
147631
|
+
function uidIsRequired(subject) {
|
|
147632
|
+
return subject.isHubChild;
|
|
147633
|
+
}
|
|
147634
|
+
/**
|
|
147635
|
+
* Is this camera unaddressable for want of a uid? The gate every catalog,
|
|
147636
|
+
* still and byte read runs — and the only one that may answer `uid-missing`.
|
|
147637
|
+
*/
|
|
147638
|
+
function uidIsMissing(subject) {
|
|
147639
|
+
return uidIsRequired(subject) && subject.uid === void 0;
|
|
147640
|
+
}
|
|
147641
|
+
/** The one wording for the refusal, so two surfaces cannot disagree about it. */
|
|
147642
|
+
var UID_MISSING_REASON = "uid-missing: the hub selects a child by uid, and this camera has none persisted.";
|
|
147643
|
+
//#endregion
|
|
147458
147644
|
//#region src/clips/clip-catalog-cache.ts
|
|
147459
147645
|
/**
|
|
147460
147646
|
* The per-`(device, day)` clip catalog cache.
|
|
@@ -147543,6 +147729,20 @@ var ClipCatalogCache = class ClipCatalogCache {
|
|
|
147543
147729
|
}
|
|
147544
147730
|
return null;
|
|
147545
147731
|
}
|
|
147732
|
+
/**
|
|
147733
|
+
* Every day key this device holds a listing for, in no particular order.
|
|
147734
|
+
*
|
|
147735
|
+
* It is what lets an availability probe say WHEN a catalog was last
|
|
147736
|
+
* confirmed without listing anything: a camera nobody may wake reports the
|
|
147737
|
+
* age of what is held, and a source with nothing held says so rather than
|
|
147738
|
+
* claiming to be current.
|
|
147739
|
+
*/
|
|
147740
|
+
daysHeld(deviceId) {
|
|
147741
|
+
const out = [];
|
|
147742
|
+
const prefix = `${deviceId}|`;
|
|
147743
|
+
for (const key of this.slots.keys()) if (key.startsWith(prefix)) out.push(key.slice(prefix.length));
|
|
147744
|
+
return out;
|
|
147745
|
+
}
|
|
147546
147746
|
/** Every row held for a device, in no particular order. The prefetch sorts. */
|
|
147547
147747
|
allRows(deviceId) {
|
|
147548
147748
|
const out = [];
|
|
@@ -147596,6 +147796,57 @@ var ClipCatalogCache = class ClipCatalogCache {
|
|
|
147596
147796
|
}
|
|
147597
147797
|
};
|
|
147598
147798
|
//#endregion
|
|
147799
|
+
//#region src/clips/clip-still-refusal.ts
|
|
147800
|
+
/** "You are in the queue." */
|
|
147801
|
+
function deferStill(code, reason, retryAfterMs) {
|
|
147802
|
+
return {
|
|
147803
|
+
ok: false,
|
|
147804
|
+
code,
|
|
147805
|
+
disposition: "deferred",
|
|
147806
|
+
reason,
|
|
147807
|
+
retryAfterMs
|
|
147808
|
+
};
|
|
147809
|
+
}
|
|
147810
|
+
/** "This will not happen." */
|
|
147811
|
+
function refuseStill(code, reason) {
|
|
147812
|
+
return {
|
|
147813
|
+
ok: false,
|
|
147814
|
+
code,
|
|
147815
|
+
disposition: "final",
|
|
147816
|
+
reason
|
|
147817
|
+
};
|
|
147818
|
+
}
|
|
147819
|
+
/**
|
|
147820
|
+
* The queue's own refusal — the only failure that is about the LANE rather
|
|
147821
|
+
* than about the camera, and therefore the only one that is deferred.
|
|
147822
|
+
*
|
|
147823
|
+
* It carries its own `retryAfterMs` because the queue is the only thing that
|
|
147824
|
+
* knows it: the depth it is holding, or the exact instant a host's backoff
|
|
147825
|
+
* closes.
|
|
147826
|
+
*/
|
|
147827
|
+
var ThumbQueueRefusedError = class extends Error {
|
|
147828
|
+
code;
|
|
147829
|
+
retryAfterMs;
|
|
147830
|
+
constructor(code, message, retryAfterMs) {
|
|
147831
|
+
super(message);
|
|
147832
|
+
this.name = "ThumbQueueRefusedError";
|
|
147833
|
+
this.code = code;
|
|
147834
|
+
this.retryAfterMs = retryAfterMs;
|
|
147835
|
+
}
|
|
147836
|
+
};
|
|
147837
|
+
/**
|
|
147838
|
+
* A thrown value from the queue, as a wire answer — or `null` when it was not
|
|
147839
|
+
* the queue at all.
|
|
147840
|
+
*
|
|
147841
|
+
* `null` is not a default: it is what makes the caller name `camera-refused`
|
|
147842
|
+
* explicitly, instead of a socket close quietly inheriting "come back later"
|
|
147843
|
+
* and a tile retrying four times against a camera that is not there.
|
|
147844
|
+
*/
|
|
147845
|
+
function queueRefusal(err) {
|
|
147846
|
+
if (!(err instanceof ThumbQueueRefusedError)) return null;
|
|
147847
|
+
return deferStill(err.code, err.message, err.retryAfterMs);
|
|
147848
|
+
}
|
|
147849
|
+
//#endregion
|
|
147599
147850
|
//#region src/clips/clip-data-plane.ts
|
|
147600
147851
|
/**
|
|
147601
147852
|
* The addon's two data-plane handlers — the still and the bytes.
|
|
@@ -147617,6 +147868,19 @@ var ClipCatalogCache = class ClipCatalogCache {
|
|
|
147617
147868
|
* which. Waiting instead would hold a request open behind a camera that is
|
|
147618
147869
|
* asleep for the night.
|
|
147619
147870
|
*
|
|
147871
|
+
* **And it says whether to COME BACK.** Those reasons are two kinds of answer:
|
|
147872
|
+
* `queue-full` and `camera-backoff` mean the still is still coming, the rest
|
|
147873
|
+
* mean it will not. The branch that refused decides — nothing here maps a code
|
|
147874
|
+
* to a disposition, and no client parses one out of the prose — and the answer
|
|
147875
|
+
* travels as its own headers (`x-camstack-reason-code`,
|
|
147876
|
+
* `x-camstack-disposition`, plus `retry-after` on a deferred one) beside the
|
|
147877
|
+
* prose, which stays verbatim because a `camera-refused` is only ever
|
|
147878
|
+
* explained by the camera's own words.
|
|
147879
|
+
*
|
|
147880
|
+
* A response with no disposition header is read as FINAL by every surface,
|
|
147881
|
+
* which is exactly how this route behaved before: an older addon under a newer
|
|
147882
|
+
* admin UI keeps the old behaviour instead of gaining a retry loop.
|
|
147883
|
+
*
|
|
147620
147884
|
* ## Why Range is hand-rolled here
|
|
147621
147885
|
*
|
|
147622
147886
|
* One file, one path, no root mapping — and `createReadStream().pipe(res)`
|
|
@@ -147659,26 +147923,45 @@ function createClipThumbHandler(deps) {
|
|
|
147659
147923
|
try {
|
|
147660
147924
|
minted = await deps.mint(deviceId, clipKey);
|
|
147661
147925
|
} catch (err) {
|
|
147662
|
-
minted =
|
|
147663
|
-
ok: false,
|
|
147664
|
-
reason: err instanceof Error ? err.message : String(err)
|
|
147665
|
-
};
|
|
147926
|
+
minted = refuseStill("camera-refused", err instanceof Error ? err.message : String(err));
|
|
147666
147927
|
}
|
|
147667
147928
|
if (!minted.ok) {
|
|
147668
|
-
|
|
147669
|
-
tags: { deviceId },
|
|
147670
|
-
meta: {
|
|
147671
|
-
clipKey,
|
|
147672
|
-
reason: minted.reason
|
|
147673
|
-
}
|
|
147674
|
-
});
|
|
147675
|
-
res.writeHead(204, { "x-camstack-reason": minted.reason.slice(0, 500) });
|
|
147676
|
-
res.end();
|
|
147929
|
+
writeRefusal(res, minted, deviceId, deps.logger, clipKey);
|
|
147677
147930
|
return;
|
|
147678
147931
|
}
|
|
147679
147932
|
writeJpeg(res, minted.bytes, req.method === "HEAD");
|
|
147680
147933
|
};
|
|
147681
147934
|
}
|
|
147935
|
+
/**
|
|
147936
|
+
* The 204, with everything the tile needs to decide what to draw next.
|
|
147937
|
+
*
|
|
147938
|
+
* A FINAL refusal is work that was dropped, so it is warned, per camera. A
|
|
147939
|
+
* DEFERRED one dropped nothing — the still is queued behind another — and
|
|
147940
|
+
* warning it would put 43 lines on the log for one 60-tile page against a
|
|
147941
|
+
* 16-deep lane, which is how a real drop stops being visible.
|
|
147942
|
+
*/
|
|
147943
|
+
function writeRefusal(res, refusal, deviceId, logger, clipKey) {
|
|
147944
|
+
const line = "videoclips: thumbnail not minted";
|
|
147945
|
+
const extras = {
|
|
147946
|
+
tags: { deviceId },
|
|
147947
|
+
meta: {
|
|
147948
|
+
clipKey,
|
|
147949
|
+
code: refusal.code,
|
|
147950
|
+
disposition: refusal.disposition,
|
|
147951
|
+
reason: refusal.reason
|
|
147952
|
+
}
|
|
147953
|
+
};
|
|
147954
|
+
if (refusal.disposition === "final") logger.warn(line, extras);
|
|
147955
|
+
else logger.debug(line, extras);
|
|
147956
|
+
const retryAfterMs = refusal.disposition === "deferred" ? refusal.retryAfterMs : void 0;
|
|
147957
|
+
res.writeHead(204, {
|
|
147958
|
+
[CLIP_STILL_REASON_HEADER]: refusal.reason.slice(0, 500),
|
|
147959
|
+
[CLIP_STILL_REASON_CODE_HEADER]: refusal.code,
|
|
147960
|
+
[CLIP_STILL_DISPOSITION_HEADER]: refusal.disposition,
|
|
147961
|
+
...retryAfterMs === void 0 ? {} : { "retry-after": String(Math.max(1, Math.ceil(retryAfterMs / 1e3))) }
|
|
147962
|
+
});
|
|
147963
|
+
res.end();
|
|
147964
|
+
}
|
|
147682
147965
|
function writeJpeg(res, bytes, headOnly) {
|
|
147683
147966
|
res.writeHead(200, {
|
|
147684
147967
|
"content-type": "image/jpeg",
|
|
@@ -147797,21 +148080,73 @@ function createClipPlaybackHandler(deps) {
|
|
|
147797
148080
|
//#region src/clips/clip-mux.ts
|
|
147798
148081
|
/** A clip that has not transferred by here is not going to. */
|
|
147799
148082
|
var CLIP_MUX_TIMEOUT_MS = 6e4;
|
|
148083
|
+
/**
|
|
148084
|
+
* Decide what to do with the track the download returned.
|
|
148085
|
+
*
|
|
148086
|
+
* The gate is `format === 'adts'` and NOT `audio !== null`: an `adpcm` or
|
|
148087
|
+
* `unknown` track stream-copied into MP4 produces a file whose audio track is
|
|
148088
|
+
* garbage, which is worse than a clip that is honestly silent. A track that
|
|
148089
|
+
* arrived with no frames is no track — counting it would promise sound the
|
|
148090
|
+
* file does not have.
|
|
148091
|
+
*/
|
|
148092
|
+
function decideClipAudio(audio) {
|
|
148093
|
+
if (audio === null) return {
|
|
148094
|
+
kind: "silent",
|
|
148095
|
+
reason: "no-audio-track",
|
|
148096
|
+
format: null
|
|
148097
|
+
};
|
|
148098
|
+
if (audio.frames === 0 || audio.bytes === 0) return {
|
|
148099
|
+
kind: "silent",
|
|
148100
|
+
reason: "no-audio-track",
|
|
148101
|
+
format: audio.format
|
|
148102
|
+
};
|
|
148103
|
+
if (audio.format !== "adts") return {
|
|
148104
|
+
kind: "silent",
|
|
148105
|
+
reason: "audio-not-copyable",
|
|
148106
|
+
format: audio.format
|
|
148107
|
+
};
|
|
148108
|
+
return {
|
|
148109
|
+
kind: "copy",
|
|
148110
|
+
format: "adts",
|
|
148111
|
+
data: audio.data
|
|
148112
|
+
};
|
|
148113
|
+
}
|
|
148114
|
+
function usableFps(fps) {
|
|
148115
|
+
if (fps === null || fps === void 0) return null;
|
|
148116
|
+
return Number.isFinite(fps) && fps > 0 ? fps : null;
|
|
148117
|
+
}
|
|
147800
148118
|
function buildClipMuxArgs(input) {
|
|
148119
|
+
const demuxer = input.videoType === "H265" ? "hevc" : "h264";
|
|
148120
|
+
const fps = usableFps(input.fps);
|
|
147801
148121
|
return [
|
|
147802
148122
|
"-hide_banner",
|
|
147803
148123
|
"-loglevel",
|
|
147804
148124
|
"error",
|
|
147805
148125
|
"-nostdin",
|
|
147806
148126
|
"-y",
|
|
148127
|
+
...fps !== null ? ["-r", String(fps)] : [],
|
|
147807
148128
|
"-f",
|
|
147808
|
-
|
|
148129
|
+
demuxer,
|
|
147809
148130
|
"-i",
|
|
147810
148131
|
input.inputPath,
|
|
147811
|
-
|
|
148132
|
+
...input.audio !== void 0 ? [
|
|
148133
|
+
"-f",
|
|
148134
|
+
"aac",
|
|
148135
|
+
"-i",
|
|
148136
|
+
input.audio.path
|
|
148137
|
+
] : [],
|
|
148138
|
+
"-c:v",
|
|
147812
148139
|
"copy",
|
|
148140
|
+
...input.audio !== void 0 ? [
|
|
148141
|
+
"-c:a",
|
|
148142
|
+
"copy",
|
|
148143
|
+
"-bsf:a",
|
|
148144
|
+
"aac_adtstoasc"
|
|
148145
|
+
] : [],
|
|
147813
148146
|
"-movflags",
|
|
147814
148147
|
"+faststart",
|
|
148148
|
+
"-f",
|
|
148149
|
+
"mp4",
|
|
147815
148150
|
input.outputPath
|
|
147816
148151
|
];
|
|
147817
148152
|
}
|
|
@@ -147898,6 +148233,40 @@ function describeMuxFailure(run) {
|
|
|
147898
148233
|
if (run.code !== 0) return `mux-failed (exit ${String(run.code)}): ${run.stderr.slice(0, 300)}`;
|
|
147899
148234
|
return null;
|
|
147900
148235
|
}
|
|
148236
|
+
//#endregion
|
|
148237
|
+
//#region src/clips/clip-thumb-queue.ts
|
|
148238
|
+
/**
|
|
148239
|
+
* The provider's OWN CoverPreview lane — two queues, one in flight per host.
|
|
148240
|
+
*
|
|
148241
|
+
* The library already serialises CoverPreview: one in flight per api instance,
|
|
148242
|
+
* a 50-deep FIFO that throws past that, and a process-wide host-keyed backoff
|
|
148243
|
+
* that doubles on a 400. What a FIFO cannot express is PRIORITY, and the two
|
|
148244
|
+
* kinds of mint this feature has are not equal — an operator staring at a tile
|
|
148245
|
+
* must not wait behind twenty prefetches for clips nobody opened.
|
|
148246
|
+
*
|
|
148247
|
+
* So the provider keeps two queues of its own, hands the library at most ONE
|
|
148248
|
+
* CoverPreview per host at a time, drains `interactive` before `prefetch`, and
|
|
148249
|
+
* re-checks between every prefetch mint (D549 16).
|
|
148250
|
+
*
|
|
148251
|
+
* ## Two things it refuses to do
|
|
148252
|
+
*
|
|
148253
|
+
* **Wait when the queue is full.** A request past the bound is REFUSED with
|
|
148254
|
+
* `queue-full`, so the route can answer 204 with a reason. A request held open
|
|
148255
|
+
* behind forty-nine others is a spinner the operator cannot cancel, and the
|
|
148256
|
+
* camera will have rolled the clip over before it is served.
|
|
148257
|
+
*
|
|
148258
|
+
* **Let a prefetch probe a sulking host.** While a host's 400 backoff is open
|
|
148259
|
+
* a prefetch is skipped entirely; only an interactive request may probe it,
|
|
148260
|
+
* because only an interactive request has somebody waiting for the answer.
|
|
148261
|
+
*
|
|
148262
|
+
* The backoff mirrors the library's (1 s doubling to 30 s) rather than
|
|
148263
|
+
* replacing it: this one decides whether to ASK at all, the library's decides
|
|
148264
|
+
* how long to wait once asked.
|
|
148265
|
+
*/
|
|
148266
|
+
/** How long one waiting mint is worth, as a retry hint. Coarse on purpose:
|
|
148267
|
+
* the client floors it with its own backoff and caps its own attempts, so a
|
|
148268
|
+
* wrong hint can neither cause a hammer nor a hang. */
|
|
148269
|
+
var THUMB_DRAIN_HINT_PER_WAITER_MS = 1e3;
|
|
147901
148270
|
var THUMB_BACKOFF_INITIAL_MS = 1e3;
|
|
147902
148271
|
var THUMB_BACKOFF_MAX_MS = 3e4;
|
|
147903
148272
|
/**
|
|
@@ -147918,6 +148287,28 @@ var ClipThumbQueue = class {
|
|
|
147918
148287
|
const state = this.hosts.get(host);
|
|
147919
148288
|
return state !== void 0 && state.backoffUntilMs > this.now();
|
|
147920
148289
|
}
|
|
148290
|
+
/**
|
|
148291
|
+
* Is this host busy with, or owed, operator-facing work?
|
|
148292
|
+
*
|
|
148293
|
+
* The periodic catalog refresh asks before it spends the host's one lane:
|
|
148294
|
+
* an operator staring at a tile must never wait behind a timer, and a host
|
|
148295
|
+
* whose 400 backoff is open must not be probed by anything that has nobody
|
|
148296
|
+
* waiting for the answer — the same rule that already keeps a PREFETCH off
|
|
148297
|
+
* a sulking host.
|
|
148298
|
+
*
|
|
148299
|
+
* Either a mint is in flight or the host is sulking. There is deliberately
|
|
148300
|
+
* no third clause for "an interactive request is queued": a queued request
|
|
148301
|
+
* means a mint is running, so `running` already covers it, and a clause
|
|
148302
|
+
* that can never be the reason for the answer is a leftover that reads as
|
|
148303
|
+
* verification. The lane is one per host, so a PREFETCH holding it defers
|
|
148304
|
+
* the refresh too — which is right: the refresh is the lowest-priority
|
|
148305
|
+
* thing on this host.
|
|
148306
|
+
*/
|
|
148307
|
+
busy(host) {
|
|
148308
|
+
const state = this.hosts.get(host);
|
|
148309
|
+
if (state === void 0) return false;
|
|
148310
|
+
return state.backoffUntilMs > this.now() || state.running;
|
|
148311
|
+
}
|
|
147921
148312
|
backoffUntil(host) {
|
|
147922
148313
|
return this.hosts.get(host)?.backoffUntilMs ?? 0;
|
|
147923
148314
|
}
|
|
@@ -147940,8 +148331,8 @@ var ClipThumbQueue = class {
|
|
|
147940
148331
|
*/
|
|
147941
148332
|
async submit(job) {
|
|
147942
148333
|
const state = this.stateFor(job.host);
|
|
147943
|
-
if (job.lane === "prefetch" && state.backoffUntilMs > this.now()) throw new
|
|
147944
|
-
if (state.waiting.length >= 16) throw new
|
|
148334
|
+
if (job.lane === "prefetch" && state.backoffUntilMs > this.now()) throw new ThumbQueueRefusedError("camera-backoff", `camera-backoff: host ${job.host} refused a CoverPreview recently; prefetch skipped until the backoff closes (only an interactive request probes it)`, state.backoffUntilMs - this.now());
|
|
148335
|
+
if (state.waiting.length >= 16) throw new ThumbQueueRefusedError("queue-full", `queue-full: ${state.waiting.length} thumbnail mints already waiting for host ${job.host}`, state.waiting.length * THUMB_DRAIN_HINT_PER_WAITER_MS);
|
|
147945
148336
|
return new Promise((resolve, reject) => {
|
|
147946
148337
|
state.waiting.push({
|
|
147947
148338
|
lane: job.lane,
|
|
@@ -147961,7 +148352,7 @@ var ClipThumbQueue = class {
|
|
|
147961
148352
|
if (next === void 0) return;
|
|
147962
148353
|
state.waiting.splice(pick, 1);
|
|
147963
148354
|
if (next.lane === "prefetch" && state.backoffUntilMs > this.now()) {
|
|
147964
|
-
next.reject(
|
|
148355
|
+
next.reject(new ThumbQueueRefusedError("camera-backoff", `camera-backoff: host ${host} is backing off; prefetch skipped`, state.backoffUntilMs - this.now()));
|
|
147965
148356
|
this.drain(host);
|
|
147966
148357
|
return;
|
|
147967
148358
|
}
|
|
@@ -148014,12 +148405,13 @@ var ClipThumbQueue = class {
|
|
|
148014
148405
|
* exactly — 29 events, 28 inside the 20 listed files, and all 20 files
|
|
148015
148406
|
* carrying an event. They are two VIEWS of one store.
|
|
148016
148407
|
*
|
|
148017
|
-
* So the operator collapsed them (2026-09-20): one source, which
|
|
148018
|
-
* best API for what the camera IS
|
|
148019
|
-
*
|
|
148020
|
-
*
|
|
148021
|
-
*
|
|
148022
|
-
*
|
|
148408
|
+
* So the operator collapsed them (2026-09-20): one source, which picked the
|
|
148409
|
+
* best API for what the camera IS, and REPORTED the pick on `ClipSource.via`.
|
|
148410
|
+
* Later the same day the pick went too (D565): `FileInfoList` is the catalog
|
|
148411
|
+
* on every topology, so `via` had one value and is gone. What is left is one
|
|
148412
|
+
* source reading one API — and `clipKeyFor` below is still keyed off
|
|
148413
|
+
* `(channel, fileName)` rather than the source, which is now simply true
|
|
148414
|
+
* rather than a concession.
|
|
148023
148415
|
*/
|
|
148024
148416
|
var CLIP_SOURCE_ONBOARD = "native:reolink:onboard";
|
|
148025
148417
|
/** Every source this provider can name. Order is the picker's order. */
|
|
@@ -148070,10 +148462,10 @@ function parseClipId(clipId) {
|
|
|
148070
148462
|
}
|
|
148071
148463
|
/**
|
|
148072
148464
|
* The on-disk key for a clip's derived artifacts (its thumbnail, its muxed
|
|
148073
|
-
* MP4). Derived from `(channel, fileName)` and NOT from the source:
|
|
148074
|
-
*
|
|
148075
|
-
*
|
|
148076
|
-
*
|
|
148465
|
+
* MP4). Derived from `(channel, fileName)` and NOT from the source: a
|
|
148466
|
+
* thumbnail belongs to the FILE, so any surface that names the same file
|
|
148467
|
+
* finds the same picture and the camera is never asked for a second
|
|
148468
|
+
* CoverPreview of it.
|
|
148077
148469
|
*
|
|
148078
148470
|
* A hash rather than the encoded name because a hub `<Id>` is a full path and
|
|
148079
148471
|
* the encoded form is long enough to hit a file-name limit.
|
|
@@ -148222,7 +148614,7 @@ function startOf(file) {
|
|
|
148222
148614
|
return file.startTime?.getTime() ?? 0;
|
|
148223
148615
|
}
|
|
148224
148616
|
var partsCache = /* @__PURE__ */ new Map();
|
|
148225
|
-
function formatterFor(timeZone) {
|
|
148617
|
+
function formatterFor$1(timeZone) {
|
|
148226
148618
|
const cached = partsCache.get(timeZone);
|
|
148227
148619
|
if (cached !== void 0) return cached;
|
|
148228
148620
|
const made = new Intl.DateTimeFormat("en-US", {
|
|
@@ -148239,7 +148631,7 @@ function formatterFor(timeZone) {
|
|
|
148239
148631
|
return made;
|
|
148240
148632
|
}
|
|
148241
148633
|
function partsAt(epochMs, timeZone) {
|
|
148242
|
-
const parts = formatterFor(timeZone).formatToParts(new Date(epochMs));
|
|
148634
|
+
const parts = formatterFor$1(timeZone).formatToParts(new Date(epochMs));
|
|
148243
148635
|
const read = (type) => {
|
|
148244
148636
|
const found = parts.find((p) => p.type === type);
|
|
148245
148637
|
return found === void 0 ? 0 : Number(found.value);
|
|
@@ -148310,36 +148702,6 @@ function enumerateWallClockDays(sinceMs, untilMs, timeZone) {
|
|
|
148310
148702
|
//#endregion
|
|
148311
148703
|
//#region src/clips/clip-catalog.ts
|
|
148312
148704
|
/**
|
|
148313
|
-
* Reading a Reolink camera's own clip catalog: the cheap question, the day
|
|
148314
|
-
* index, then one day's files.
|
|
148315
|
-
*
|
|
148316
|
-
* ## The order is the point
|
|
148317
|
-
*
|
|
148318
|
-
* 1. **Is there a card at all?** `getHddInfoList` (cmd 102). 640 answers
|
|
148319
|
-
* `mount 0, capacity 0` — no SD card — and searching it is 400s and wasted
|
|
148320
|
-
* wake budget. "The cheap question comes first" (D549 4 / D54).
|
|
148321
|
-
* 2. **Which days hold footage?** `getDayRecords` (cmd 142) once per month.
|
|
148322
|
-
* The per-day listing loop of the earlier plan is deleted, not kept as a
|
|
148323
|
-
* fallback (D549 8): 19 days in 50 ms beats 30 searches.
|
|
148324
|
-
* 3. **What is in one day?** `FileInfoList` (cmd 14/15/16), sub AND main, then
|
|
148325
|
-
* twinned (D549 14b). A clip IS a file: its name is its id, its bytes
|
|
148326
|
-
* (cmd 5) and its still (cmd 298) are addressed from it (D549 10).
|
|
148327
|
-
*
|
|
148328
|
-
* This is the API a STANDALONE camera's one source resolves to — a standalone
|
|
148329
|
-
* answers `findEventLog` with an empty body. A hub child resolves to the event
|
|
148330
|
-
* view instead (`clip-event-catalog.ts`), and still uses this reader for the
|
|
148331
|
-
* day's files, which is what an event's window is joined against (D555).
|
|
148332
|
-
*
|
|
148333
|
-
* ## What an empty answer means
|
|
148334
|
-
*
|
|
148335
|
-
* A day the CALENDAR names and the INDEX answers with nothing is a defect on
|
|
148336
|
-
* the camera, measured on 618: the calendar marks 3–20 September, days 6–20
|
|
148337
|
-
* list zero files on both streams, on both filters, on a hand-built window,
|
|
148338
|
-
* with 177 GB free. The reader reports `indexEmpty` for that day so the
|
|
148339
|
-
* provider can say `index-empty` — never "no clips", which would be a lie
|
|
148340
|
-
* about a card that is full of them (D549 18).
|
|
148341
|
-
*/
|
|
148342
|
-
/**
|
|
148343
148705
|
* Locate the disk record structurally.
|
|
148344
148706
|
*
|
|
148345
148707
|
* The library's declared `HddInfoListConfig` and the captured wire disagree —
|
|
@@ -148424,6 +148786,30 @@ function monthsBetween(sinceMs, untilMs, timeZone) {
|
|
|
148424
148786
|
}
|
|
148425
148787
|
return out;
|
|
148426
148788
|
}
|
|
148789
|
+
/**
|
|
148790
|
+
* Is this file still being written?
|
|
148791
|
+
*
|
|
148792
|
+
* A Reolink recording is named `<prefix>_DST<date>_<start>_<end>_<hex>_<hex>`,
|
|
148793
|
+
* and while it is open the camera writes the END stamp as `000000` and the
|
|
148794
|
+
* size as `0` — measured on 592, 2026-09-20:
|
|
148795
|
+
* `RecS03_DST20260920_192927_000000_2B1E808_0.mp4`. Its `<endTime>` block is
|
|
148796
|
+
* FROZEN at whatever the index entry held when it was created (19:29:56, read
|
|
148797
|
+
* three times over fifteen minutes while the file grew), so a duration drawn
|
|
148798
|
+
* from it is a lie about the one row the operator sees first.
|
|
148799
|
+
*
|
|
148800
|
+
* BOTH signals are required. A recording that happens to end at midnight is
|
|
148801
|
+
* named `_000000_` and is closed; its bytes are what separate the two. Nothing
|
|
148802
|
+
* on the wire distinguishes them otherwise, and a false positive here would
|
|
148803
|
+
* replace a correct duration with `Recording…` for ever. Re-measured
|
|
148804
|
+
* read-only the same evening: of 355 sub and 355 main files on that camera's
|
|
148805
|
+
* day, zero carried a `_000000_` segment and zero carried a size of 0.
|
|
148806
|
+
*/
|
|
148807
|
+
function isFileStillBeingWritten(file) {
|
|
148808
|
+
if (file.sizeBytes !== void 0 && file.sizeBytes > 0) return false;
|
|
148809
|
+
return OPEN_END_STAMP.test(file.fileName);
|
|
148810
|
+
}
|
|
148811
|
+
/** The end-stamp field of an open recording, between its own separators. */
|
|
148812
|
+
var OPEN_END_STAMP = /_000000_/;
|
|
148427
148813
|
function handleOf(file) {
|
|
148428
148814
|
const id = file.id;
|
|
148429
148815
|
return id !== void 0 && id !== "" ? id : file.fileName;
|
|
@@ -148464,139 +148850,123 @@ async function listDayRows(api, input) {
|
|
|
148464
148850
|
...pair.sub.sizeBytes !== void 0 ? { subBytes: pair.sub.sizeBytes } : {},
|
|
148465
148851
|
...pair.main !== void 0 ? { mainFileName: handleOf(pair.main) } : {},
|
|
148466
148852
|
...pair.main?.sizeBytes !== void 0 ? { mainBytes: pair.main.sizeBytes } : {},
|
|
148853
|
+
...pair.main?.startTime !== void 0 ? { mainStartMs: pair.main.startTime.getTime() } : {},
|
|
148467
148854
|
supportSub: pair.sub.supportSub !== false,
|
|
148468
|
-
nativeTypes: parseNativeTypes(pair.sub.recordType)
|
|
148855
|
+
nativeTypes: parseNativeTypes(pair.sub.recordType),
|
|
148856
|
+
...isFileStillBeingWritten(pair.sub) ? { inProgress: true } : {}
|
|
148469
148857
|
};
|
|
148470
148858
|
}),
|
|
148471
148859
|
indexEmpty: subs.length === 0,
|
|
148472
148860
|
...mainSearchFailed !== void 0 ? { mainSearchFailed } : {}
|
|
148473
148861
|
};
|
|
148474
148862
|
}
|
|
148475
|
-
|
|
148476
|
-
|
|
148477
|
-
|
|
148478
|
-
* The
|
|
148479
|
-
*
|
|
148480
|
-
*
|
|
148481
|
-
|
|
148482
|
-
|
|
148483
|
-
|
|
148484
|
-
|
|
148485
|
-
|
|
148486
|
-
|
|
148487
|
-
|
|
148488
|
-
|
|
148489
|
-
|
|
148490
|
-
|
|
148491
|
-
* ## An event is not a promise of a clip
|
|
148492
|
-
*
|
|
148493
|
-
* A row whose `hasRecFile` is false has nothing to play, and both of the wrong
|
|
148494
|
-
* things are easy: dropping it under-reports a camera's events, and showing it
|
|
148495
|
-
* as playable fails on click. It is listed with `playable: false` and a
|
|
148496
|
-
* reason, which is what `ClipSchema.unplayableReason` exists for.
|
|
148497
|
-
*
|
|
148498
|
-
* **How often that bites is a per-camera fact.** Measured 2026-09-20 over 20
|
|
148499
|
-
* days: 3628 → 222 rows with 0 orphans, 3629 → 1 059 rows with 0 orphans, and
|
|
148500
|
-
* the orphans D549 19 describes are on 3825 — the child whose uid was never
|
|
148501
|
-
* persisted, which this provider cannot address at all today. So one camera's
|
|
148502
|
-
* events are all playable and another's are all not, and a surface built for
|
|
148503
|
-
* either extreme is wrong on the other.
|
|
148504
|
-
*
|
|
148505
|
-
* ## Where the file comes from
|
|
148506
|
-
*
|
|
148507
|
-
* `findEventLog` rows carry no file name — they name events, not files. So the
|
|
148508
|
-
* file is resolved by WINDOW CONTAINMENT against the day's `FileInfoList`
|
|
148509
|
-
* rows, which the `onboard` reader already fetches: an event whose
|
|
148510
|
-
* `[start, end]` lies inside a file's `[start − 3 s, end + 3 s]`. 28 of 29 on
|
|
148511
|
-
* the measured day; the one that did not is a real orphan (the hub refused its
|
|
148512
|
-
* CoverPreview too), not a join bug.
|
|
148513
|
-
*
|
|
148514
|
-
* Deliberately NOT used here: `searchAlarmVideos` (272/273/274). It does carry
|
|
148515
|
-
* a `fileName`, and its containment is measured at 1271/1271 — but it is
|
|
148516
|
-
* clamped to ONE camera-local day (an 18→19 September window silently answered
|
|
148517
|
-
* 462 windows all dated the 18th) where `searchEventLog` spans the whole
|
|
148518
|
-
* window in one open, and D549 19 chose the event list on the measured timing.
|
|
148519
|
-
* Its place is the per-window label upgrade for a clip's DETAIL (D549 20), and
|
|
148520
|
-
* that is a separate slice.
|
|
148521
|
-
*
|
|
148522
|
-
* ## `devices` is the selector; `chnbits` is inert
|
|
148523
|
-
*
|
|
148524
|
-
* Measured: `chnbits: 3` answered exactly the same 9 rows as the default, and
|
|
148525
|
-
* OMITTING `devices` is refused outright by the library ("no devices to
|
|
148526
|
-
* search") rather than answered emptily — a hub that has pushed nothing yet is
|
|
148527
|
-
* a refusal, not an empty result. This module always passes `devices`, never
|
|
148528
|
-
* sends `chnbits`, and never derives one from the other. With one child named,
|
|
148529
|
-
* foreign rows in the reply were 0; the uid filter below is belt and braces.
|
|
148530
|
-
*/
|
|
148531
|
-
/**
|
|
148532
|
-
* Every event for ONE child in the window, newest first, in ONE call.
|
|
148533
|
-
*
|
|
148534
|
-
* The event list is multi-day by construction — a per-day loop would pay the
|
|
148535
|
-
* open/close for each day and answer the same rows. The bound the caller owes
|
|
148536
|
-
* is on the WINDOW, and it has a number behind it: measured on one hub child,
|
|
148537
|
-
* ONE day is 267 ms and 20 days is 3.6–5.4 s. A day-range default is therefore
|
|
148538
|
-
* a UI decision, not a free one.
|
|
148539
|
-
*/
|
|
148540
|
-
async function listEventRows(api, input) {
|
|
148541
|
-
const entries = await api.searchEventLog({
|
|
148542
|
-
devices: [{ uid: input.uid }],
|
|
148543
|
-
start: new Date(input.since),
|
|
148544
|
-
end: new Date(input.until),
|
|
148545
|
-
timeZone: input.timeZone
|
|
148863
|
+
/**
|
|
148864
|
+
* Every alarm window of ONE camera-local day.
|
|
148865
|
+
*
|
|
148866
|
+
* The one-day clamp is the firmware's, not ours: an 18→19 September ask
|
|
148867
|
+
* answered 462 windows all dated the 18th. The caller owns the iteration, and
|
|
148868
|
+
* this is asked from inside the day loop the listing already runs.
|
|
148869
|
+
*/
|
|
148870
|
+
async function readAlarmWindows(api, input) {
|
|
148871
|
+
const { startMs, endMs } = dayBounds(input.day, input.timeZone);
|
|
148872
|
+
return await api.searchAlarmVideos({
|
|
148873
|
+
channel: input.channel,
|
|
148874
|
+
start: new Date(startMs),
|
|
148875
|
+
end: new Date(endMs),
|
|
148876
|
+
streamType: 1,
|
|
148877
|
+
timeZone: input.timeZone,
|
|
148878
|
+
...input.uid !== void 0 ? { uid: input.uid } : {}
|
|
148546
148879
|
});
|
|
148547
|
-
|
|
148548
|
-
|
|
148549
|
-
|
|
148550
|
-
|
|
148551
|
-
|
|
148552
|
-
|
|
148553
|
-
|
|
148554
|
-
|
|
148555
|
-
|
|
148556
|
-
|
|
148557
|
-
|
|
148558
|
-
|
|
148880
|
+
}
|
|
148881
|
+
/** The camera declined to class this file: `none`, or no `recordType` at all. */
|
|
148882
|
+
function fileCarriesNoClass(nativeTypes) {
|
|
148883
|
+
return nativeTypes.every((type) => type === "none");
|
|
148884
|
+
}
|
|
148885
|
+
/**
|
|
148886
|
+
* Should this day's labels be asked for separately?
|
|
148887
|
+
*
|
|
148888
|
+
* Only when NOTHING in the day carried a class. One file saying `md` is proof
|
|
148889
|
+
* that this camera writes `recordType`, and then the free answer is the
|
|
148890
|
+
* answer — paying 8–9 s to restate it is the cheap-question rule inverted.
|
|
148891
|
+
*/
|
|
148892
|
+
function dayNeedsAlarmLabels(rows) {
|
|
148893
|
+
return rows.length > 0 && rows.every((row) => fileCarriesNoClass(row.nativeTypes));
|
|
148894
|
+
}
|
|
148895
|
+
/**
|
|
148896
|
+
* Attach each window's `alarmType`s to the file whose START it names.
|
|
148897
|
+
*
|
|
148898
|
+
* A row the camera already classed is returned untouched: the file's own
|
|
148899
|
+
* `recordType` is the authority wherever it exists, and a per-window answer
|
|
148900
|
+
* must not narrow it. A window that names no listed file contributes nothing
|
|
148901
|
+
* and is COUNTED — it is never attached to the nearest neighbour, which is how
|
|
148902
|
+
* a surface ends up showing one recording's class on another's row.
|
|
148903
|
+
*/
|
|
148904
|
+
function joinAlarmLabels(rows, windows, timeZone) {
|
|
148905
|
+
const byStamp = /* @__PURE__ */ new Map();
|
|
148906
|
+
for (const window of windows) {
|
|
148907
|
+
const key = window.fileName.slice(-STAMP_LENGTH);
|
|
148908
|
+
const existing = byStamp.get(key);
|
|
148909
|
+
const target = existing ?? [];
|
|
148910
|
+
if (existing === void 0) byStamp.set(key, target);
|
|
148911
|
+
for (const type of window.alarmTypes) if (type !== "" && !target.includes(type)) target.push(type);
|
|
148559
148912
|
}
|
|
148560
|
-
|
|
148913
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
148914
|
+
let matched = 0;
|
|
148915
|
+
return {
|
|
148916
|
+
rows: rows.map((row) => {
|
|
148917
|
+
if (!fileCarriesNoClass(row.nativeTypes)) return row;
|
|
148918
|
+
const subKey = alarmJoinKey(row.startMs, timeZone);
|
|
148919
|
+
const mainKey = row.mainStartMs !== void 0 ? alarmJoinKey(row.mainStartMs, timeZone) : void 0;
|
|
148920
|
+
const key = byStamp.has(subKey) ? subKey : mainKey !== void 0 && byStamp.has(mainKey) ? mainKey : void 0;
|
|
148921
|
+
if (key === void 0) return row;
|
|
148922
|
+
const types = byStamp.get(key);
|
|
148923
|
+
if (types === void 0 || types.length === 0) return row;
|
|
148924
|
+
claimed.add(key);
|
|
148925
|
+
matched += 1;
|
|
148926
|
+
return {
|
|
148927
|
+
...row,
|
|
148928
|
+
nativeTypes: [...types]
|
|
148929
|
+
};
|
|
148930
|
+
}),
|
|
148931
|
+
matched,
|
|
148932
|
+
unmatchedWindows: byStamp.size - claimed.size
|
|
148933
|
+
};
|
|
148561
148934
|
}
|
|
148562
|
-
|
|
148563
|
-
|
|
148564
|
-
|
|
148565
|
-
|
|
148566
|
-
|
|
148567
|
-
|
|
148568
|
-
|
|
148569
|
-
|
|
148570
|
-
|
|
148571
|
-
|
|
148572
|
-
|
|
148573
|
-
|
|
148574
|
-
|
|
148575
|
-
|
|
148576
|
-
|
|
148577
|
-
return {
|
|
148578
|
-
...event,
|
|
148579
|
-
clipKey: file.clipKey,
|
|
148580
|
-
fileName: file.subFileName,
|
|
148581
|
-
playable: true
|
|
148582
|
-
};
|
|
148935
|
+
/** `YYYYMMDDHHMMSS` — 14 digits. */
|
|
148936
|
+
var STAMP_LENGTH = 14;
|
|
148937
|
+
var FORMATTER_BY_ZONE = /* @__PURE__ */ new Map();
|
|
148938
|
+
function formatterFor(timeZone) {
|
|
148939
|
+
const cached = FORMATTER_BY_ZONE.get(timeZone);
|
|
148940
|
+
if (cached !== void 0) return cached;
|
|
148941
|
+
const made = new Intl.DateTimeFormat("en-CA", {
|
|
148942
|
+
timeZone,
|
|
148943
|
+
year: "numeric",
|
|
148944
|
+
month: "2-digit",
|
|
148945
|
+
day: "2-digit",
|
|
148946
|
+
hour: "2-digit",
|
|
148947
|
+
minute: "2-digit",
|
|
148948
|
+
second: "2-digit",
|
|
148949
|
+
hour12: false
|
|
148583
148950
|
});
|
|
148951
|
+
FORMATTER_BY_ZONE.set(timeZone, made);
|
|
148952
|
+
return made;
|
|
148953
|
+
}
|
|
148954
|
+
/**
|
|
148955
|
+
* The 14-digit tail of the short name a window carries — the file's start in
|
|
148956
|
+
* the CAMERA's wall clock. Never a UTC stamp: 00:30 Rome on the 17th is 22:30
|
|
148957
|
+
* UTC on the 16th, and a UTC key would file it under the wrong day and join it
|
|
148958
|
+
* to nothing.
|
|
148959
|
+
*/
|
|
148960
|
+
function alarmJoinKey(atMs, timeZone) {
|
|
148961
|
+
const parts = new Map(formatterFor(timeZone).formatToParts(new Date(atMs)).map((part) => [part.type, part.value]));
|
|
148962
|
+
return `${parts.get("year") ?? ""}${parts.get("month") ?? ""}${parts.get("day") ?? ""}${normaliseHour(parts.get("hour"))}${parts.get("minute") ?? ""}${parts.get("second") ?? ""}`;
|
|
148584
148963
|
}
|
|
148585
|
-
/**
|
|
148586
|
-
function
|
|
148587
|
-
return
|
|
148964
|
+
/** `hourCycle: h23` still renders midnight as `24` in some ICU builds. */
|
|
148965
|
+
function normaliseHour(hour) {
|
|
148966
|
+
return hour === "24" ? "00" : hour ?? "";
|
|
148588
148967
|
}
|
|
148589
148968
|
//#endregion
|
|
148590
148969
|
//#region src/clips/reolink-videoclips-provider.ts
|
|
148591
|
-
/**
|
|
148592
|
-
* Which API the ONE source resolved to for a camera — reported on
|
|
148593
|
-
* `ClipSource.via` and on the listing log line.
|
|
148594
|
-
*
|
|
148595
|
-
* The CHOICE is made from what the camera is (`viaFor`); naming it is what
|
|
148596
|
-
* makes "why do these two cameras behave differently" answerable at all.
|
|
148597
|
-
*/
|
|
148598
|
-
var CATALOG_VIA_EVENTS = "events";
|
|
148599
|
-
var CATALOG_VIA_FILES = "files";
|
|
148600
148970
|
function createReolinkVideoclipsProvider(deps) {
|
|
148601
148971
|
/** Last known availability per `deviceId|source`, written by every read. */
|
|
148602
148972
|
const health = /* @__PURE__ */ new Map();
|
|
@@ -148633,7 +149003,7 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
148633
149003
|
});
|
|
148634
149004
|
return cached;
|
|
148635
149005
|
}
|
|
148636
|
-
if (device
|
|
149006
|
+
if (uidIsMissing(device)) {
|
|
148637
149007
|
deps.logger.warn("videoclips: no uid for this camera — its catalog cannot be addressed", {
|
|
148638
149008
|
tags,
|
|
148639
149009
|
meta: {
|
|
@@ -148643,7 +149013,7 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
148643
149013
|
});
|
|
148644
149014
|
noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, {
|
|
148645
149015
|
state: "unreachable",
|
|
148646
|
-
reason:
|
|
149016
|
+
reason: UID_MISSING_REASON
|
|
148647
149017
|
});
|
|
148648
149018
|
return [];
|
|
148649
149019
|
}
|
|
@@ -148720,7 +149090,7 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
148720
149090
|
error: result.mainSearchFailed
|
|
148721
149091
|
}
|
|
148722
149092
|
});
|
|
148723
|
-
return result.rows;
|
|
149093
|
+
return await labelDay(device, api, day, result.rows);
|
|
148724
149094
|
});
|
|
148725
149095
|
if (entry.clips.length === 0) emptyDays.push(day);
|
|
148726
149096
|
rows.push(...entry.clips);
|
|
@@ -148748,11 +149118,7 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
148748
149118
|
days: emptyDays
|
|
148749
149119
|
}
|
|
148750
149120
|
});
|
|
148751
|
-
noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD,
|
|
148752
|
-
state: "index-empty",
|
|
148753
|
-
reason: `The camera’s calendar marks ${emptyDays.length} day(s) with footage that its own index lists no files for: ${emptyDays.join(", ")}.`,
|
|
148754
|
-
catalogAsOf: deps.now()
|
|
148755
|
-
});
|
|
149121
|
+
noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, indexEmpty(emptyDays));
|
|
148756
149122
|
} else noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, {
|
|
148757
149123
|
state: "ok",
|
|
148758
149124
|
catalogAsOf: deps.now()
|
|
@@ -148760,139 +149126,55 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
148760
149126
|
return rows;
|
|
148761
149127
|
};
|
|
148762
149128
|
/**
|
|
148763
|
-
*
|
|
149129
|
+
* Give a day's rows a class when the FILES carried none.
|
|
148764
149130
|
*
|
|
148765
|
-
*
|
|
148766
|
-
*
|
|
148767
|
-
*
|
|
148768
|
-
*
|
|
148769
|
-
*
|
|
148770
|
-
*
|
|
149131
|
+
* The gate is what the day actually said, never what the camera is: a hub
|
|
149132
|
+
* child's files are `recordType: none` on every row (measured), a
|
|
149133
|
+
* standalone's carry `md,people,dog_cat,sched`, and a camera that changes
|
|
149134
|
+
* firmware changes which of those it is without this code learning a new
|
|
149135
|
+
* topology. The cheap question comes first (D54) — on 592 an alarm search
|
|
149136
|
+
* costs 8.2–9.5 s for 1 271 windows to restate labels already on the row.
|
|
148771
149137
|
*
|
|
148772
|
-
*
|
|
148773
|
-
*
|
|
148774
|
-
*
|
|
148775
|
-
* rows still list, each saying why it cannot play.
|
|
149138
|
+
* A refusal costs the LABELS and nothing else. Losing the day because its
|
|
149139
|
+
* class could not be read would be the upgrade destroying the thing it
|
|
149140
|
+
* upgrades — the same rule the MAIN search already lives by.
|
|
148776
149141
|
*/
|
|
148777
|
-
const
|
|
149142
|
+
const labelDay = async (device, api, day, rows) => {
|
|
148778
149143
|
const tags = { deviceId: device.deviceId };
|
|
148779
|
-
if (!
|
|
148780
|
-
if (device.uid === void 0) {
|
|
148781
|
-
deps.logger.warn("videoclips: no uid for this camera — the hub cannot be asked about it", {
|
|
148782
|
-
tags,
|
|
148783
|
-
meta: {
|
|
148784
|
-
branch: "uid-missing",
|
|
148785
|
-
via: CATALOG_VIA_EVENTS
|
|
148786
|
-
}
|
|
148787
|
-
});
|
|
148788
|
-
noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, {
|
|
148789
|
-
state: "unreachable",
|
|
148790
|
-
reason: "uid-missing: the hub selects a child by uid, and this camera has none persisted."
|
|
148791
|
-
});
|
|
148792
|
-
return [];
|
|
148793
|
-
}
|
|
148794
|
-
let api;
|
|
148795
|
-
try {
|
|
148796
|
-
api = await device.getApi();
|
|
148797
|
-
} catch (err) {
|
|
148798
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
148799
|
-
deps.logger.warn("videoclips: could not reach the hub", {
|
|
148800
|
-
tags,
|
|
148801
|
-
meta: {
|
|
148802
|
-
branch: "camera-refused",
|
|
148803
|
-
via: CATALOG_VIA_EVENTS,
|
|
148804
|
-
error: reason
|
|
148805
|
-
}
|
|
148806
|
-
});
|
|
148807
|
-
noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, {
|
|
148808
|
-
state: "unreachable",
|
|
148809
|
-
reason
|
|
148810
|
-
});
|
|
148811
|
-
return [];
|
|
148812
|
-
}
|
|
149144
|
+
if (!dayNeedsAlarmLabels(rows)) return rows;
|
|
148813
149145
|
const startedAt = deps.now();
|
|
148814
|
-
let
|
|
148815
|
-
let joined = [];
|
|
149146
|
+
let windows;
|
|
148816
149147
|
try {
|
|
148817
|
-
|
|
148818
|
-
uid: device.uid,
|
|
149148
|
+
windows = await readAlarmWindows(api, {
|
|
148819
149149
|
channel: device.channel,
|
|
149150
|
+
uid: device.uid,
|
|
148820
149151
|
timeZone: device.timeZone,
|
|
148821
|
-
|
|
148822
|
-
until
|
|
149152
|
+
day
|
|
148823
149153
|
});
|
|
148824
|
-
events = rows;
|
|
148825
|
-
joined = joinEventsToFiles(rows, await readOnboard(device, since, until).catch(() => []));
|
|
148826
149154
|
} catch (err) {
|
|
148827
|
-
|
|
148828
|
-
deps.logger.warn("videoclips: the hub event log refused", {
|
|
149155
|
+
deps.logger.warn("videoclips: the label search refused — the day lists without classes", {
|
|
148829
149156
|
tags,
|
|
148830
149157
|
meta: {
|
|
148831
149158
|
branch: "camera-refused",
|
|
148832
|
-
|
|
148833
|
-
error:
|
|
149159
|
+
day,
|
|
149160
|
+
error: err instanceof Error ? err.message : String(err)
|
|
148834
149161
|
}
|
|
148835
149162
|
});
|
|
148836
|
-
|
|
148837
|
-
state: "unreachable",
|
|
148838
|
-
reason
|
|
148839
|
-
});
|
|
148840
|
-
return [];
|
|
149163
|
+
return rows;
|
|
148841
149164
|
}
|
|
148842
|
-
const
|
|
148843
|
-
deps.logger.info("videoclips: read the
|
|
149165
|
+
const joined = joinAlarmLabels(rows, windows, device.timeZone);
|
|
149166
|
+
deps.logger.info("videoclips: read the classes a day of files does not carry", {
|
|
148844
149167
|
tags,
|
|
148845
149168
|
meta: {
|
|
148846
|
-
|
|
148847
|
-
|
|
148848
|
-
|
|
148849
|
-
|
|
148850
|
-
|
|
149169
|
+
day,
|
|
149170
|
+
windows: windows.length,
|
|
149171
|
+
rows: rows.length,
|
|
149172
|
+
labelled: joined.matched,
|
|
149173
|
+
unmatchedWindows: joined.unmatchedWindows,
|
|
149174
|
+
elapsedMs: deps.now() - startedAt
|
|
148851
149175
|
}
|
|
148852
149176
|
});
|
|
148853
|
-
|
|
148854
|
-
state: "ok",
|
|
148855
|
-
catalogAsOf: deps.now()
|
|
148856
|
-
});
|
|
148857
|
-
return joined;
|
|
148858
|
-
};
|
|
148859
|
-
/**
|
|
148860
|
-
* An events-view row as a `Clip`.
|
|
148861
|
-
*
|
|
148862
|
-
* The id carries the event WINDOW as well as the file, because a file holds
|
|
148863
|
-
* 1–13 windows and two events of one recording must not collapse into one
|
|
148864
|
-
* row. A row with nothing to play carries `playable: false` and its reason,
|
|
148865
|
-
* and offers no thumbnail mint — the hub refused the CoverPreview for
|
|
148866
|
-
* exactly that orphan event, so an offer here would be a tile that spins and
|
|
148867
|
-
* then fails.
|
|
148868
|
-
*/
|
|
148869
|
-
const toEventClip = async (device, row, catalogAsOf) => {
|
|
148870
|
-
const classification = classifyNativeTypes(row.nativeTypes);
|
|
148871
|
-
const still = await deps.hasVouchedThumbnail(device.deviceId, row.clipKey) ? { thumbnail: deps.thumbnailUrl(device.deviceId, row.clipKey) } : row.playable ? { thumbnailMint: deps.thumbnailMintUrl(device.deviceId, row.clipKey) } : { thumbnailUnavailable: { reason: "no-keyframe" } };
|
|
148872
|
-
return {
|
|
148873
|
-
id: mintClipId({
|
|
148874
|
-
source: CLIP_SOURCE_ONBOARD,
|
|
148875
|
-
channel: row.channel,
|
|
148876
|
-
fileName: row.fileName ?? `event:${row.startMs}-${row.endMs}`,
|
|
148877
|
-
window: {
|
|
148878
|
-
startMs: row.startMs,
|
|
148879
|
-
endMs: row.endMs
|
|
148880
|
-
}
|
|
148881
|
-
}),
|
|
148882
|
-
source: CLIP_SOURCE_ONBOARD,
|
|
148883
|
-
kind: classification.kind,
|
|
148884
|
-
timeRange: {
|
|
148885
|
-
startMs: row.startMs,
|
|
148886
|
-
endMs: row.endMs
|
|
148887
|
-
},
|
|
148888
|
-
...classification.labels.length > 0 ? { labels: [...classification.labels] } : {},
|
|
148889
|
-
...classification.behaviours.length > 0 ? { behaviours: [...classification.behaviours] } : {},
|
|
148890
|
-
...classification.nativeTypes.length > 0 ? { nativeTypes: [...classification.nativeTypes] } : {},
|
|
148891
|
-
...still,
|
|
148892
|
-
...catalogAsOf !== void 0 ? { catalogAsOf } : {},
|
|
148893
|
-
playable: row.playable,
|
|
148894
|
-
...row.unplayableReason !== void 0 ? { unplayableReason: row.unplayableReason } : {}
|
|
148895
|
-
};
|
|
149177
|
+
return joined.rows;
|
|
148896
149178
|
};
|
|
148897
149179
|
const toClip = async (device, source, row, catalogAsOf) => {
|
|
148898
149180
|
rowsByKey.set(`${device.deviceId}|${row.clipKey}`, row);
|
|
@@ -148911,6 +149193,7 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
148911
149193
|
startMs: row.startMs,
|
|
148912
149194
|
endMs: row.endMs
|
|
148913
149195
|
},
|
|
149196
|
+
...row.inProgress === true ? { inProgress: true } : {},
|
|
148914
149197
|
...classification.labels.length > 0 ? { labels: [...classification.labels] } : {},
|
|
148915
149198
|
...classification.behaviours.length > 0 ? { behaviours: [...classification.behaviours] } : {},
|
|
148916
149199
|
...classification.nativeTypes.length > 0 ? { nativeTypes: [...classification.nativeTypes] } : {},
|
|
@@ -148941,37 +149224,26 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
148941
149224
|
listSources: async ({ deviceId }) => {
|
|
148942
149225
|
const device = await deps.resolveDevice(deviceId);
|
|
148943
149226
|
if (device === null) return [];
|
|
148944
|
-
const via = viaFor(device);
|
|
148945
149227
|
return [{
|
|
148946
149228
|
source: CLIP_SOURCE_ONBOARD,
|
|
148947
149229
|
addonId: REOLINK_ADDON_ID,
|
|
148948
149230
|
label: "Camera storage",
|
|
148949
|
-
|
|
148950
|
-
availability: await resolveAvailability(device, via)
|
|
149231
|
+
availability: await resolveAvailability(device)
|
|
148951
149232
|
}];
|
|
148952
149233
|
},
|
|
148953
|
-
listClips: async ({ deviceId, since, until, limit
|
|
149234
|
+
listClips: async ({ deviceId, since, until, limit }) => {
|
|
148954
149235
|
const device = await deps.resolveDevice(deviceId);
|
|
148955
149236
|
if (device === null) return [];
|
|
148956
149237
|
const tags = { deviceId };
|
|
148957
|
-
|
|
148958
|
-
const
|
|
148959
|
-
const ordered = (
|
|
148960
|
-
const events = await readHubEvents(device, since, until);
|
|
148961
|
-
const asOf = health.get(healthKey(deviceId, CLIP_SOURCE_ONBOARD))?.availability.catalogAsOf;
|
|
148962
|
-
return await Promise.all(events.map((row) => toEventClip(device, row, asOf)));
|
|
148963
|
-
})() : await (async () => {
|
|
148964
|
-
const rows = await readOnboard(device, since, until);
|
|
148965
|
-
const asOf = health.get(healthKey(deviceId, CLIP_SOURCE_ONBOARD))?.availability.catalogAsOf;
|
|
148966
|
-
return await Promise.all(rows.toSorted((a, b) => b.startMs - a.startMs).map((row) => toClip(device, CLIP_SOURCE_ONBOARD, row, asOf)));
|
|
148967
|
-
})()).toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
149238
|
+
const rows = await readOnboard(device, since, until);
|
|
149239
|
+
const asOf = health.get(healthKey(deviceId, CLIP_SOURCE_ONBOARD))?.availability.catalogAsOf;
|
|
149240
|
+
const ordered = (await Promise.all(rows.toSorted((a, b) => b.startMs - a.startMs).map((row) => toClip(device, CLIP_SOURCE_ONBOARD, row, asOf)))).toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
148968
149241
|
const capped = limit !== void 0 ? ordered.slice(0, limit) : ordered;
|
|
148969
149242
|
const vouched = capped.filter((c) => c.thumbnail !== void 0).length;
|
|
148970
149243
|
deps.logger.info("videoclips: listed a Reolink catalog", {
|
|
148971
149244
|
tags,
|
|
148972
149245
|
meta: {
|
|
148973
149246
|
source: CLIP_SOURCE_ONBOARD,
|
|
148974
|
-
via,
|
|
148975
149247
|
clips: capped.length,
|
|
148976
149248
|
truncated: limit !== void 0 && ordered.length > limit,
|
|
148977
149249
|
unplayable: capped.filter((c) => c.playable === false).length,
|
|
@@ -148980,6 +149252,18 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
148980
149252
|
availability: health.get(healthKey(deviceId, CLIP_SOURCE_ONBOARD))?.availability.state
|
|
148981
149253
|
}
|
|
148982
149254
|
});
|
|
149255
|
+
const availability = health.get(healthKey(deviceId, CLIP_SOURCE_ONBOARD))?.availability;
|
|
149256
|
+
if (capped.length === 0 && availability !== void 0 && availability.state !== "ok") {
|
|
149257
|
+
deps.logger.warn("videoclips: nothing to list, and the source cannot answer", {
|
|
149258
|
+
tags,
|
|
149259
|
+
meta: {
|
|
149260
|
+
branch: "refused-empty",
|
|
149261
|
+
state: availability.state,
|
|
149262
|
+
reason: availability.reason ?? null
|
|
149263
|
+
}
|
|
149264
|
+
});
|
|
149265
|
+
throw new Error(`videoclips: device ${deviceId} listed no clips, and that is not an empty day — the source is "${availability.state}"` + (availability.reason !== void 0 ? `: ${availability.reason}` : "."));
|
|
149266
|
+
}
|
|
148983
149267
|
return capped;
|
|
148984
149268
|
},
|
|
148985
149269
|
getClipPlayback: async ({ deviceId, clipId, profile }) => {
|
|
@@ -148988,7 +149272,6 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
148988
149272
|
const device = await deps.resolveDevice(deviceId);
|
|
148989
149273
|
if (device === null) throw new Error(`videoclips: device ${deviceId} is not a Reolink camera`);
|
|
148990
149274
|
if (device.isBattery && device.sleeping && !device.isHubChild) throw new Error(`videoclips: device ${deviceId} is a standalone battery camera and is sleeping — fetching a clip would wake it. Use "Wake and refresh" first.`);
|
|
148991
|
-
if (parsed.fileName.startsWith("event:")) throw new Error(`videoclips: clip ${clipId} is an event with no recording file (no-rec-file) — the hub listed the event, not a clip.`);
|
|
148992
149275
|
const row = rowsByKey.get(`${deviceId}|${clipKeyFor(parsed.channel, parsed.fileName)}`) ?? null;
|
|
148993
149276
|
const wantsMain = profile === "high" && row?.mainFileName !== void 0;
|
|
148994
149277
|
const fileName = wantsMain ? row?.mainFileName ?? parsed.fileName : parsed.fileName;
|
|
@@ -149007,46 +149290,350 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
149007
149290
|
}
|
|
149008
149291
|
};
|
|
149009
149292
|
/**
|
|
149010
|
-
*
|
|
149011
|
-
*
|
|
149012
|
-
*
|
|
149013
|
-
*
|
|
149014
|
-
*
|
|
149015
|
-
*
|
|
149016
|
-
*
|
|
149017
|
-
*
|
|
149018
|
-
|
|
149019
|
-
|
|
149020
|
-
|
|
149021
|
-
|
|
149022
|
-
|
|
149293
|
+
* What a source is saying right now — MEASURED, never defaulted.
|
|
149294
|
+
*
|
|
149295
|
+
* A listing that already ran is the better answer and wins: `listClips`
|
|
149296
|
+
* asked the whole window and this probe asks one day, so re-deciding from
|
|
149297
|
+
* the narrower question would throw away 618's other nineteen empty days.
|
|
149298
|
+
* With nothing established, the probe runs — and `ok` is only ever returned
|
|
149299
|
+
* because something was really read.
|
|
149300
|
+
*
|
|
149301
|
+
* ## What it costs, per call, and why
|
|
149302
|
+
*
|
|
149303
|
+
* This used to answer `ok` after at most one storage command, and `ok` is
|
|
149304
|
+
* the one state whose list may be read as complete. On the live hub, on the
|
|
149305
|
+
* cold runners a deploy had just produced, that default made 640 (no SD
|
|
149306
|
+
* card) and 618 (an index that lists nothing for days its own calendar
|
|
149307
|
+
* marks) both answer `ok` and then an empty list — the exact lie this line
|
|
149308
|
+
* of work exists to end. So `listSources` pays for its answer:
|
|
149309
|
+
*
|
|
149310
|
+
* - **a standalone battery camera: NOTHING, ever.** No `getApi`, no command,
|
|
149311
|
+
* not even the cheap one — on UDP the login IS the wake. `sleeping`, with
|
|
149312
|
+
* the age of whatever is cached. The gate is `isBattery`, never the
|
|
149313
|
+
* BELIEF `sleeping`: that belief is `state.battery.sleeping ?? false`, so
|
|
149314
|
+
* a slice that has not restored reads as awake, and a probe on it is a
|
|
149315
|
+
* wake nobody asked for.
|
|
149316
|
+
* - **a hub child: one `getDayRecords` through the parent's existing socket**
|
|
149317
|
+
* (65 ms measured on 3628). The child never hears it; the storage question
|
|
149318
|
+
* is the HUB's and is not asked.
|
|
149319
|
+
* - **a mains standalone: `getHddInfoList` (ms) + `getDayRecords`
|
|
149320
|
+
* (38 ms–1.9 s) + ONE day listing of the NEWEST day the calendar claims**
|
|
149321
|
+
* (0.3–6 s, and cache-backed — `serveAndRevalidate`, so a day already
|
|
149322
|
+
* listed costs nothing and a day listed here is free for the `listClips`
|
|
149323
|
+
* that follows). The day listing is what distinguishes `index-empty` from
|
|
149324
|
+
* `ok`, and nothing cheaper can: `no-storage` is the cheap question and it
|
|
149325
|
+
* comes first precisely so a camera with no card never pays the rest.
|
|
149326
|
+
*
|
|
149327
|
+
* The calendar is read for the CURRENT month only. A picker asks "can this
|
|
149328
|
+
* source answer", not "what does it hold since April", and the measured
|
|
149329
|
+
* defect (618) is in the month the operator is looking at.
|
|
149330
|
+
*/
|
|
149331
|
+
async function resolveAvailability(device) {
|
|
149023
149332
|
const known = health.get(healthKey(device.deviceId, CLIP_SOURCE_ONBOARD))?.availability;
|
|
149024
149333
|
if (known !== void 0) return known;
|
|
149025
|
-
|
|
149026
|
-
|
|
149027
|
-
|
|
149028
|
-
|
|
149029
|
-
|
|
149030
|
-
|
|
149031
|
-
|
|
149334
|
+
const availability = await probeAvailability(device);
|
|
149335
|
+
noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, availability);
|
|
149336
|
+
return availability;
|
|
149337
|
+
}
|
|
149338
|
+
async function probeAvailability(device) {
|
|
149339
|
+
const tags = { deviceId: device.deviceId };
|
|
149340
|
+
if (device.isBattery && !device.isHubChild) {
|
|
149341
|
+
const asOf = newestCachedAsOf(device.deviceId);
|
|
149342
|
+
deps.logger.warn("videoclips: not probing a standalone battery camera — login is the wake", {
|
|
149343
|
+
tags,
|
|
149344
|
+
meta: {
|
|
149345
|
+
branch: "sleeping",
|
|
149346
|
+
believedSleeping: device.sleeping,
|
|
149347
|
+
confirmedAt: asOf
|
|
149348
|
+
}
|
|
149349
|
+
});
|
|
149350
|
+
return {
|
|
149351
|
+
state: "sleeping",
|
|
149352
|
+
reason: asOf === void 0 ? "A read would wake this standalone battery camera. Nothing has been confirmed yet." : "A read would wake this standalone battery camera; showing the catalog as last confirmed.",
|
|
149353
|
+
...asOf !== void 0 ? { catalogAsOf: asOf } : {}
|
|
149354
|
+
};
|
|
149355
|
+
}
|
|
149356
|
+
if (uidIsMissing(device)) {
|
|
149357
|
+
deps.logger.warn("videoclips: no uid for this camera — its catalog cannot be addressed", {
|
|
149358
|
+
tags,
|
|
149359
|
+
meta: {
|
|
149360
|
+
branch: "uid-missing",
|
|
149361
|
+
probe: true
|
|
149362
|
+
}
|
|
149363
|
+
});
|
|
149364
|
+
return {
|
|
149365
|
+
state: "unreachable",
|
|
149366
|
+
reason: UID_MISSING_REASON
|
|
149367
|
+
};
|
|
149368
|
+
}
|
|
149369
|
+
let api;
|
|
149370
|
+
try {
|
|
149371
|
+
api = await device.getApi();
|
|
149372
|
+
} catch (err) {
|
|
149373
|
+
return unreachableFrom(device, "camera-refused", err);
|
|
149374
|
+
}
|
|
149375
|
+
if (!device.isHubChild) {
|
|
149376
|
+
const storage = await readStorageState(api);
|
|
149377
|
+
if (storage.state === "absent") {
|
|
149378
|
+
deps.logger.info("videoclips: no SD card mounted — nothing to list", {
|
|
149379
|
+
tags,
|
|
149380
|
+
meta: {
|
|
149381
|
+
branch: "no-storage",
|
|
149382
|
+
probe: true
|
|
149383
|
+
}
|
|
149384
|
+
});
|
|
149385
|
+
return {
|
|
149386
|
+
state: "no-storage",
|
|
149387
|
+
reason: "The camera reports no mounted storage (HddInfo mount 0)."
|
|
149388
|
+
};
|
|
149389
|
+
}
|
|
149390
|
+
if (storage.state === "unknown") {
|
|
149391
|
+
deps.logger.warn("videoclips: the storage question was not answered", {
|
|
149392
|
+
tags,
|
|
149393
|
+
meta: {
|
|
149394
|
+
branch: "camera-refused",
|
|
149395
|
+
probe: true,
|
|
149396
|
+
error: storage.reason ?? null
|
|
149397
|
+
}
|
|
149398
|
+
});
|
|
149399
|
+
return {
|
|
149400
|
+
state: "unreachable",
|
|
149401
|
+
reason: storage.reason ?? "The camera did not answer the storage question (HddInfo unreadable)."
|
|
149402
|
+
};
|
|
149403
|
+
}
|
|
149404
|
+
}
|
|
149405
|
+
const today = wallClockDayKey(deps.now(), device.timeZone);
|
|
149406
|
+
let calendar;
|
|
149407
|
+
try {
|
|
149408
|
+
calendar = await readDayIndex(api, {
|
|
149409
|
+
channel: device.channel,
|
|
149410
|
+
uid: device.uid,
|
|
149411
|
+
months: [{
|
|
149412
|
+
year: Number(today.slice(0, 4)),
|
|
149413
|
+
month: Number(today.slice(5, 7))
|
|
149414
|
+
}]
|
|
149415
|
+
});
|
|
149416
|
+
} catch (err) {
|
|
149417
|
+
return unreachableFrom(device, "camera-refused", err);
|
|
149418
|
+
}
|
|
149419
|
+
const claimed = [...calendar].toSorted().toReversed();
|
|
149420
|
+
if (claimed.length === 0) return {
|
|
149421
|
+
state: "ok",
|
|
149422
|
+
catalogAsOf: deps.now()
|
|
149032
149423
|
};
|
|
149033
|
-
|
|
149424
|
+
const newest = claimed[0];
|
|
149425
|
+
if (newest === void 0) return {
|
|
149426
|
+
state: "ok",
|
|
149427
|
+
catalogAsOf: deps.now()
|
|
149428
|
+
};
|
|
149429
|
+
try {
|
|
149430
|
+
await deps.cache.serveAndRevalidate(device.deviceId, newest, { closed: newest !== today }, async () => (await listDayRows(api, {
|
|
149431
|
+
channel: device.channel,
|
|
149432
|
+
uid: device.uid,
|
|
149433
|
+
timeZone: device.timeZone,
|
|
149434
|
+
day: newest
|
|
149435
|
+
})).rows);
|
|
149436
|
+
} catch (err) {
|
|
149437
|
+
return unreachableFrom(device, "camera-refused", err);
|
|
149438
|
+
}
|
|
149439
|
+
const emptyClaimed = claimed.filter((day) => deps.cache.peek(device.deviceId, day)?.clips.length === 0);
|
|
149440
|
+
if (emptyClaimed.length > 0) {
|
|
149441
|
+
deps.logger.warn("videoclips: the calendar marks days the index will not list", {
|
|
149442
|
+
tags,
|
|
149443
|
+
meta: {
|
|
149444
|
+
branch: "index-empty",
|
|
149445
|
+
probe: true,
|
|
149446
|
+
days: emptyClaimed
|
|
149447
|
+
}
|
|
149448
|
+
});
|
|
149449
|
+
return indexEmpty(emptyClaimed);
|
|
149450
|
+
}
|
|
149451
|
+
return {
|
|
149452
|
+
state: "ok",
|
|
149453
|
+
catalogAsOf: deps.now()
|
|
149454
|
+
};
|
|
149455
|
+
}
|
|
149456
|
+
function unreachableFrom(device, branch, err) {
|
|
149457
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
149458
|
+
deps.logger.warn("videoclips: the source could not be probed", {
|
|
149459
|
+
tags: { deviceId: device.deviceId },
|
|
149460
|
+
meta: {
|
|
149461
|
+
branch,
|
|
149462
|
+
probe: true,
|
|
149463
|
+
error: reason
|
|
149464
|
+
}
|
|
149465
|
+
});
|
|
149466
|
+
return {
|
|
149034
149467
|
state: "unreachable",
|
|
149035
|
-
reason
|
|
149468
|
+
reason
|
|
149036
149469
|
};
|
|
149470
|
+
}
|
|
149471
|
+
/** When any day of this device's catalog was last confirmed, newest first. */
|
|
149472
|
+
function newestCachedAsOf(deviceId) {
|
|
149473
|
+
let newest;
|
|
149474
|
+
for (const day of deps.cache.daysHeld(deviceId)) {
|
|
149475
|
+
const asOf = deps.cache.peek(deviceId, day)?.catalogAsOf;
|
|
149476
|
+
if (asOf !== void 0 && (newest === void 0 || asOf > newest)) newest = asOf;
|
|
149477
|
+
}
|
|
149478
|
+
return newest;
|
|
149479
|
+
}
|
|
149480
|
+
/** The one wording for "the camera's calendar and its own index disagree". */
|
|
149481
|
+
function indexEmpty(days) {
|
|
149482
|
+
return {
|
|
149483
|
+
state: "index-empty",
|
|
149484
|
+
reason: `The camera’s calendar marks ${days.length} day(s) with footage that its own index lists no files for: ${days.join(", ")}.`,
|
|
149485
|
+
catalogAsOf: deps.now()
|
|
149486
|
+
};
|
|
149487
|
+
}
|
|
149488
|
+
}
|
|
149489
|
+
//#endregion
|
|
149490
|
+
//#region src/clips/clip-refresh.ts
|
|
149491
|
+
/** The settings key, global default and per-device override (D562). */
|
|
149492
|
+
var CLIP_CATALOG_REFRESH_MINUTES_KEY = "clipCatalogRefreshMinutes";
|
|
149493
|
+
/** The settings key, per device only, OFF by default (D562). */
|
|
149494
|
+
var CLIP_CATALOG_FORCED_WAKE_KEY = "clipCatalogForcedWake";
|
|
149495
|
+
/** A day. Past this the catalog's own TTLs are the thing doing the work. */
|
|
149496
|
+
var CLIP_CATALOG_REFRESH_MINUTES_MAX = 1440;
|
|
149497
|
+
/**
|
|
149498
|
+
* How much of the catalog one refresh confirms.
|
|
149499
|
+
*
|
|
149500
|
+
* A day. The listing cost is per DAY (0.3–6 s on a standalone, 267 ms for one
|
|
149501
|
+
* child-day on the hub) and a 20-day window measured 3.6–5.4 s per child, so a
|
|
149502
|
+
* timer that re-confirmed everything would spend the whole budget re-reading
|
|
149503
|
+
* days that cannot change. Older days are immutable and the cache holds them
|
|
149504
|
+
* under their own 24 h TTL.
|
|
149505
|
+
*/
|
|
149506
|
+
var CLIP_CATALOG_REFRESH_WINDOW_MS = 1440 * 6e4;
|
|
149507
|
+
/**
|
|
149508
|
+
* The mode for one camera. Pure, and decided from what the camera IS — never
|
|
149509
|
+
* from a belief about whether it is asleep right now: the gate has to hold for
|
|
149510
|
+
* a camera whose sleep slice has not restored, which reads "awake" and is how
|
|
149511
|
+
* a picker draw once logged into 640 (D561).
|
|
149512
|
+
*/
|
|
149513
|
+
function resolveClipRefreshMode(topology) {
|
|
149514
|
+
if (topology.isHubChild) return "via-hub";
|
|
149515
|
+
if (!topology.isBattery) return "direct";
|
|
149516
|
+
return topology.forcedWake ? "forced-wake" : "mark-stale";
|
|
149517
|
+
}
|
|
149518
|
+
/**
|
|
149519
|
+
* The refresh interval for one camera: the per-device override on top of the
|
|
149520
|
+
* global default, with **reset == unset == the default**.
|
|
149521
|
+
*
|
|
149522
|
+
* Never throws on a bad blob. An invalid value at either level resolves to the
|
|
149523
|
+
* level above it, so a store written by an older release cannot stop the timer
|
|
149524
|
+
* — the same cascade `stationary-settings.ts` uses, for the same reason.
|
|
149525
|
+
*/
|
|
149526
|
+
function resolveClipCatalogRefreshMinutes(globalValue, deviceValue) {
|
|
149527
|
+
return asRefreshMinutes(deviceValue) ?? asRefreshMinutes(globalValue) ?? 30;
|
|
149528
|
+
}
|
|
149529
|
+
function asRefreshMinutes(value) {
|
|
149530
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
149531
|
+
const rounded = Math.round(value);
|
|
149532
|
+
if (rounded === 0) return 0;
|
|
149533
|
+
if (rounded < 5) return null;
|
|
149534
|
+
if (rounded > 1440) return null;
|
|
149535
|
+
return rounded;
|
|
149536
|
+
}
|
|
149537
|
+
/** Is the operator's forced-wake switch on for this camera? */
|
|
149538
|
+
function resolveClipCatalogForcedWake(value) {
|
|
149539
|
+
return value === true;
|
|
149540
|
+
}
|
|
149541
|
+
/**
|
|
149542
|
+
* The timer. `tick()` is the whole surface, so a test drives it from COLD and
|
|
149543
|
+
* the scheduler owns no clock of its own — the addon supplies the interval.
|
|
149544
|
+
*/
|
|
149545
|
+
var ClipCatalogRefreshScheduler = class {
|
|
149546
|
+
deps;
|
|
149547
|
+
/** Last time an interval was SPENT on a camera, successful or not. */
|
|
149548
|
+
lastRunAt = /* @__PURE__ */ new Map();
|
|
149549
|
+
/** Cameras whose refresh has not come back yet. */
|
|
149550
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
149551
|
+
constructor(deps) {
|
|
149552
|
+
this.deps = deps;
|
|
149553
|
+
}
|
|
149554
|
+
async tick() {
|
|
149555
|
+
const now = this.deps.now();
|
|
149556
|
+
const due = this.deps.listTargets().filter((target) => this.isDue(target, now));
|
|
149557
|
+
for (const target of due) await this.run(target, now);
|
|
149558
|
+
}
|
|
149559
|
+
isDue(target, now) {
|
|
149560
|
+
if (target.intervalMinutes <= 0) return false;
|
|
149561
|
+
if (this.inFlight.has(target.deviceId)) return false;
|
|
149562
|
+
const last = this.lastRunAt.get(target.deviceId);
|
|
149563
|
+
if (last === void 0) return true;
|
|
149564
|
+
return now - last >= target.intervalMinutes * 6e4;
|
|
149565
|
+
}
|
|
149566
|
+
async run(target, now) {
|
|
149567
|
+
const tags = { deviceId: target.deviceId };
|
|
149568
|
+
const mode = resolveClipRefreshMode(target);
|
|
149569
|
+
if (this.deps.hostBusy(target.hostKey)) {
|
|
149570
|
+
this.deps.logger.warn("videoclips: catalog refresh deferred — the host is serving a read", {
|
|
149571
|
+
tags,
|
|
149572
|
+
meta: {
|
|
149573
|
+
branch: "deferred-interactive",
|
|
149574
|
+
mode,
|
|
149575
|
+
host: target.hostKey
|
|
149576
|
+
}
|
|
149577
|
+
});
|
|
149578
|
+
return;
|
|
149579
|
+
}
|
|
149580
|
+
if (mode === "mark-stale") {
|
|
149581
|
+
this.deps.markStale(target.deviceId);
|
|
149582
|
+
this.lastRunAt.set(target.deviceId, now);
|
|
149583
|
+
this.deps.logger.info("videoclips: catalog marked stale — a timer cannot read a standalone battery camera", {
|
|
149584
|
+
tags,
|
|
149585
|
+
meta: {
|
|
149586
|
+
branch: "mark-stale",
|
|
149587
|
+
mode,
|
|
149588
|
+
refreshesOn: "onWakeTransition",
|
|
149589
|
+
intervalMinutes: target.intervalMinutes
|
|
149590
|
+
}
|
|
149591
|
+
});
|
|
149592
|
+
return;
|
|
149593
|
+
}
|
|
149594
|
+
this.lastRunAt.set(target.deviceId, now);
|
|
149595
|
+
this.inFlight.add(target.deviceId);
|
|
149037
149596
|
try {
|
|
149038
|
-
if (
|
|
149039
|
-
|
|
149040
|
-
|
|
149041
|
-
|
|
149042
|
-
|
|
149597
|
+
if (mode === "forced-wake") try {
|
|
149598
|
+
await this.deps.wake(target.deviceId);
|
|
149599
|
+
} catch (err) {
|
|
149600
|
+
this.deps.markStale(target.deviceId);
|
|
149601
|
+
this.deps.logger.warn("videoclips: the authorised wake did not bring the camera up — catalog left stale", {
|
|
149602
|
+
tags,
|
|
149603
|
+
meta: {
|
|
149604
|
+
branch: "forced-wake-failed",
|
|
149605
|
+
mode,
|
|
149606
|
+
error: errMessage$1(err)
|
|
149607
|
+
}
|
|
149608
|
+
});
|
|
149609
|
+
return;
|
|
149610
|
+
}
|
|
149611
|
+
await this.deps.refresh(target.deviceId);
|
|
149612
|
+
this.deps.logger.info("videoclips: catalog refreshed", {
|
|
149613
|
+
tags,
|
|
149614
|
+
meta: {
|
|
149615
|
+
branch: "refreshed",
|
|
149616
|
+
mode,
|
|
149617
|
+
elapsedMs: this.deps.now() - now
|
|
149618
|
+
}
|
|
149619
|
+
});
|
|
149043
149620
|
} catch (err) {
|
|
149044
|
-
|
|
149045
|
-
|
|
149046
|
-
|
|
149047
|
-
|
|
149621
|
+
this.deps.markStale(target.deviceId);
|
|
149622
|
+
this.deps.logger.warn("videoclips: catalog refresh failed — the catalog stays as it was", {
|
|
149623
|
+
tags,
|
|
149624
|
+
meta: {
|
|
149625
|
+
branch: "refresh-failed",
|
|
149626
|
+
mode,
|
|
149627
|
+
error: errMessage$1(err)
|
|
149628
|
+
}
|
|
149629
|
+
});
|
|
149630
|
+
} finally {
|
|
149631
|
+
this.inFlight.delete(target.deviceId);
|
|
149048
149632
|
}
|
|
149049
149633
|
}
|
|
149634
|
+
};
|
|
149635
|
+
function errMessage$1(err) {
|
|
149636
|
+
return err instanceof Error ? err.message : String(err);
|
|
149050
149637
|
}
|
|
149051
149638
|
function createClipService(deps) {
|
|
149052
149639
|
const now = deps.now ?? (() => Date.now());
|
|
@@ -149077,23 +149664,11 @@ function createClipService(deps) {
|
|
|
149077
149664
|
};
|
|
149078
149665
|
const mintThumbnail = async (deviceId, clipKey, lane) => {
|
|
149079
149666
|
const device = await deps.resolveDevice(deviceId);
|
|
149080
|
-
if (device === null) return {
|
|
149081
|
-
|
|
149082
|
-
|
|
149083
|
-
};
|
|
149084
|
-
if (device.isBattery && device.sleeping && !device.isHubChild) return {
|
|
149085
|
-
ok: false,
|
|
149086
|
-
reason: "sleeping: a still would wake this standalone battery camera"
|
|
149087
|
-
};
|
|
149088
|
-
if (device.uid === void 0) return {
|
|
149089
|
-
ok: false,
|
|
149090
|
-
reason: "uid-missing: this camera’s stills cannot be addressed"
|
|
149091
|
-
};
|
|
149667
|
+
if (device === null) return refuseStill("unknown-device", `unknown-device: ${deviceId} is not a Reolink camera here`);
|
|
149668
|
+
if (device.isBattery && device.sleeping && !device.isHubChild) return refuseStill("sleeping", "sleeping: a still would wake this standalone battery camera");
|
|
149669
|
+
if (uidIsMissing(device)) return refuseStill("uid-missing", UID_MISSING_REASON);
|
|
149092
149670
|
const row = rowFor(deviceId, clipKey);
|
|
149093
|
-
if (row === null) return
|
|
149094
|
-
ok: false,
|
|
149095
|
-
reason: "no-catalog-row: list this day before asking for its stills"
|
|
149096
|
-
};
|
|
149671
|
+
if (row === null) return refuseStill("no-catalog-row", "no-catalog-row: list this day before asking for its stills");
|
|
149097
149672
|
try {
|
|
149098
149673
|
const bytes = await queue.submit({
|
|
149099
149674
|
host: device.hostKey,
|
|
@@ -149104,16 +149679,13 @@ function createClipService(deps) {
|
|
|
149104
149679
|
time: new Date(row.startMs),
|
|
149105
149680
|
endTime: new Date(row.endMs),
|
|
149106
149681
|
snapType: "sub",
|
|
149107
|
-
uid: device.uid,
|
|
149682
|
+
...device.uid === void 0 ? {} : { uid: device.uid },
|
|
149108
149683
|
isNvr: device.isHubChild,
|
|
149109
149684
|
timeZone: device.timeZone
|
|
149110
149685
|
});
|
|
149111
149686
|
}
|
|
149112
149687
|
});
|
|
149113
|
-
if (bytes.byteLength === 0) return
|
|
149114
|
-
ok: false,
|
|
149115
|
-
reason: "no-keyframe: the camera returned an empty frame"
|
|
149116
|
-
};
|
|
149688
|
+
if (bytes.byteLength === 0) return refuseStill("no-keyframe", "no-keyframe: the camera returned an empty frame");
|
|
149117
149689
|
await thumbs.put(deviceId, clipKey, bytes);
|
|
149118
149690
|
return {
|
|
149119
149691
|
ok: true,
|
|
@@ -149121,6 +149693,19 @@ function createClipService(deps) {
|
|
|
149121
149693
|
};
|
|
149122
149694
|
} catch (err) {
|
|
149123
149695
|
const reason = err instanceof Error ? err.message : String(err);
|
|
149696
|
+
const deferred = queueRefusal(err);
|
|
149697
|
+
if (deferred !== null) {
|
|
149698
|
+
deps.logger.debug("videoclips: a thumbnail mint was deferred", {
|
|
149699
|
+
tags: { deviceId },
|
|
149700
|
+
meta: {
|
|
149701
|
+
clipKey,
|
|
149702
|
+
lane,
|
|
149703
|
+
code: deferred.code,
|
|
149704
|
+
retryAfterMs: deferred.retryAfterMs
|
|
149705
|
+
}
|
|
149706
|
+
});
|
|
149707
|
+
return deferred;
|
|
149708
|
+
}
|
|
149124
149709
|
deps.logger.warn("videoclips: a thumbnail mint refused", {
|
|
149125
149710
|
tags: { deviceId },
|
|
149126
149711
|
meta: {
|
|
@@ -149130,10 +149715,7 @@ function createClipService(deps) {
|
|
|
149130
149715
|
error: reason
|
|
149131
149716
|
}
|
|
149132
149717
|
});
|
|
149133
|
-
return
|
|
149134
|
-
ok: false,
|
|
149135
|
-
reason
|
|
149136
|
-
};
|
|
149718
|
+
return refuseStill("camera-refused", reason);
|
|
149137
149719
|
}
|
|
149138
149720
|
};
|
|
149139
149721
|
const prepareClip = async (deviceId, clipKey) => {
|
|
@@ -149182,15 +149764,43 @@ function createClipService(deps) {
|
|
|
149182
149764
|
}),
|
|
149183
149765
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
149184
149766
|
});
|
|
149767
|
+
const audio = decideClipAudio(downloaded.audio);
|
|
149768
|
+
if (audio.kind === "silent") deps.logger.warn("videoclips: serving a clip with no sound", {
|
|
149769
|
+
tags: { deviceId },
|
|
149770
|
+
meta: {
|
|
149771
|
+
clipKey,
|
|
149772
|
+
branch: audio.reason,
|
|
149773
|
+
audioFormat: audio.format,
|
|
149774
|
+
audioPackets: downloaded.stats.audioPackets,
|
|
149775
|
+
audioBytes: downloaded.stats.audioBytes
|
|
149776
|
+
}
|
|
149777
|
+
});
|
|
149778
|
+
if (downloaded.stats.fpsSource !== "timestamps") deps.logger.warn("videoclips: no measured frame rate for this clip", {
|
|
149779
|
+
tags: { deviceId },
|
|
149780
|
+
meta: {
|
|
149781
|
+
clipKey,
|
|
149782
|
+
branch: "fps-unmeasured",
|
|
149783
|
+
fpsSource: downloaded.stats.fpsSource,
|
|
149784
|
+
fps: downloaded.stats.fps,
|
|
149785
|
+
infoFps: downloaded.stats.infoFps
|
|
149786
|
+
}
|
|
149787
|
+
});
|
|
149185
149788
|
const scratch = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "camstack-clip-mux-"));
|
|
149186
149789
|
try {
|
|
149187
149790
|
const rawPath = (0, node_path.join)(scratch, "clip.h264");
|
|
149791
|
+
const audioPath = (0, node_path.join)(scratch, "clip.aac");
|
|
149188
149792
|
const outPath = (0, node_path.join)(scratch, "clip.mp4");
|
|
149189
149793
|
await (0, node_fs_promises.writeFile)(rawPath, downloaded.annexB);
|
|
149794
|
+
if (audio.kind === "copy") await (0, node_fs_promises.writeFile)(audioPath, audio.data);
|
|
149190
149795
|
const failure = describeMuxFailure(await muxRunner(buildClipMuxArgs({
|
|
149191
149796
|
inputPath: rawPath,
|
|
149192
149797
|
outputPath: outPath,
|
|
149193
|
-
videoType: downloaded.videoType
|
|
149798
|
+
videoType: downloaded.videoType,
|
|
149799
|
+
fps: downloaded.stats.fps,
|
|
149800
|
+
...audio.kind === "copy" ? { audio: {
|
|
149801
|
+
path: audioPath,
|
|
149802
|
+
format: audio.format
|
|
149803
|
+
} } : {}
|
|
149194
149804
|
})));
|
|
149195
149805
|
if (failure !== null) {
|
|
149196
149806
|
deps.logger.warn("videoclips: the clip mux failed", {
|
|
@@ -149212,21 +149822,31 @@ function createClipService(deps) {
|
|
|
149212
149822
|
});
|
|
149213
149823
|
}
|
|
149214
149824
|
};
|
|
149215
|
-
|
|
149216
|
-
|
|
149217
|
-
|
|
149218
|
-
|
|
149219
|
-
|
|
149220
|
-
|
|
149221
|
-
|
|
149222
|
-
|
|
149223
|
-
|
|
149224
|
-
|
|
149225
|
-
served: profile === "high" ? "high" : "low"
|
|
149226
|
-
}),
|
|
149227
|
-
logger: deps.logger
|
|
149825
|
+
const provider = createReolinkVideoclipsProvider({
|
|
149826
|
+
now,
|
|
149827
|
+
cache,
|
|
149828
|
+
resolveDevice: deps.resolveDevice,
|
|
149829
|
+
hasVouchedThumbnail: (deviceId, clipKey) => thumbs.has(deviceId, clipKey),
|
|
149830
|
+
thumbnailUrl: (deviceId, clipKey) => `/addon/${REOLINK_ADDON_ID}/${CLIP_THUMB_PREFIX}/${deviceId}/${clipKey}`,
|
|
149831
|
+
thumbnailMintUrl: (deviceId, clipKey) => `/addon/${REOLINK_ADDON_ID}/${CLIP_THUMB_PREFIX}/${deviceId}/${clipKey}`,
|
|
149832
|
+
preparePlayback: async ({ device, clipKey, profile }) => ({
|
|
149833
|
+
playbackUrl: `/addon/${REOLINK_ADDON_ID}/${CLIP_MEDIA_PREFIX}/${device.deviceId}/${clipKey}.mp4`,
|
|
149834
|
+
served: profile === "high" ? "high" : "low"
|
|
149228
149835
|
}),
|
|
149836
|
+
logger: deps.logger
|
|
149837
|
+
});
|
|
149838
|
+
return {
|
|
149839
|
+
provider,
|
|
149229
149840
|
cache,
|
|
149841
|
+
refreshCatalog: async (deviceId) => {
|
|
149842
|
+
const until = now();
|
|
149843
|
+
await provider.listClips({
|
|
149844
|
+
deviceId,
|
|
149845
|
+
since: until - CLIP_CATALOG_REFRESH_WINDOW_MS,
|
|
149846
|
+
until
|
|
149847
|
+
});
|
|
149848
|
+
},
|
|
149849
|
+
hostBusy: (hostKey) => queue.busy(hostKey),
|
|
149230
149850
|
thumbPrefix: CLIP_THUMB_PREFIX,
|
|
149231
149851
|
mediaPrefix: CLIP_MEDIA_PREFIX,
|
|
149232
149852
|
thumbHandler: createClipThumbHandler({
|
|
@@ -149251,6 +149871,79 @@ function createClipService(deps) {
|
|
|
149251
149871
|
};
|
|
149252
149872
|
}
|
|
149253
149873
|
//#endregion
|
|
149874
|
+
//#region src/clips/clip-refresh-settings.ts
|
|
149875
|
+
/** The section id, so the addon's declaration and this schema cannot drift. */
|
|
149876
|
+
var CLIP_REFRESH_SECTION_ID = "clip-catalog-refresh";
|
|
149877
|
+
/**
|
|
149878
|
+
* The one sentence the interval must always carry.
|
|
149879
|
+
*
|
|
149880
|
+
* It does TWO different things depending on what the camera is, and a knob
|
|
149881
|
+
* that silently means two things on two cameras is how this repo has shipped
|
|
149882
|
+
* settings nobody could reason about. So the difference is in the operator's
|
|
149883
|
+
* own text, at both levels, not only in a record.
|
|
149884
|
+
*/
|
|
149885
|
+
var CLIP_CATALOG_REFRESH_MINUTES_DESCRIPTION = "How often CamStack re-reads what a Reolink camera holds on its own storage. It costs almost nothing on a camera it can reach: a hub child is read through the hub on the session that is already open (measured 65 ms, and the camera itself never hears it), and a mains camera is read on its own socket. On a STANDALONE BATTERY camera the timer cannot read anything at all — logging in is what wakes such a camera — so there it only marks the catalog as ageing, and the real re-read happens the next time the camera wakes up for its own reasons (motion, a stream, something you did). Set 0 to switch the timer off.";
|
|
149886
|
+
var CLIP_CATALOG_FORCED_WAKE_DESCRIPTION = "Let the refresh timer WAKE this camera. Off unless you mean it: on a standalone battery camera every read is a login and every login is a wake, so a 15-minute timer is 96 wakes a day (24 at 60 minutes) against the 1–20 clips a day these cameras actually record — five to a hundred times their normal wake count, and the battery pays for all of it. One interval buys exactly one wake, successful or not. It has no effect on a hub child or a mains camera, which are never woken by this timer.";
|
|
149887
|
+
/**
|
|
149888
|
+
* The addon-level (global) default. Per-camera overrides live on the device,
|
|
149889
|
+
* under the SAME key, and a device that sets nothing — or resets — resolves to
|
|
149890
|
+
* this value (`resolveClipCatalogRefreshMinutes`).
|
|
149891
|
+
*/
|
|
149892
|
+
function buildClipRefreshSettingsSchema() {
|
|
149893
|
+
return { sections: [{
|
|
149894
|
+
id: CLIP_REFRESH_SECTION_ID,
|
|
149895
|
+
title: "Clip catalog refresh",
|
|
149896
|
+
description: "The default for every Reolink camera. Each camera can override it in its own Recording settings; a camera that overrides nothing uses this value.",
|
|
149897
|
+
columns: 1,
|
|
149898
|
+
fields: [{
|
|
149899
|
+
type: "number",
|
|
149900
|
+
key: CLIP_CATALOG_REFRESH_MINUTES_KEY,
|
|
149901
|
+
label: "Refresh interval (min)",
|
|
149902
|
+
description: CLIP_CATALOG_REFRESH_MINUTES_DESCRIPTION,
|
|
149903
|
+
default: 30,
|
|
149904
|
+
min: 0,
|
|
149905
|
+
max: CLIP_CATALOG_REFRESH_MINUTES_MAX,
|
|
149906
|
+
step: 5
|
|
149907
|
+
}]
|
|
149908
|
+
}] };
|
|
149909
|
+
}
|
|
149910
|
+
/**
|
|
149911
|
+
* The per-camera section: the interval override, and — only where it can mean
|
|
149912
|
+
* anything — the forced wake.
|
|
149913
|
+
*
|
|
149914
|
+
* `forcedWake` is a plain persisted switch on purpose. It is NOT a
|
|
149915
|
+
* `device-action-button` and there is no custom action behind it, so nothing
|
|
149916
|
+
* that dispatches device actions (a schedule, an automation, a rule) can spend
|
|
149917
|
+
* a battery camera's wakes; the only way to turn it on is an operator editing
|
|
149918
|
+
* this camera's settings.
|
|
149919
|
+
*/
|
|
149920
|
+
function buildClipRefreshDeviceSection(input) {
|
|
149921
|
+
const forcedWakeApplies = input.isBattery && !input.isHubChild;
|
|
149922
|
+
return [{
|
|
149923
|
+
id: CLIP_REFRESH_SECTION_ID,
|
|
149924
|
+
tab: "recording",
|
|
149925
|
+
title: "Clip catalog refresh",
|
|
149926
|
+
description: "How often this camera’s own clip list is re-read. Leave the interval empty to use the default from the Reolink addon settings.",
|
|
149927
|
+
columns: 1,
|
|
149928
|
+
fields: [{
|
|
149929
|
+
type: "number",
|
|
149930
|
+
key: CLIP_CATALOG_REFRESH_MINUTES_KEY,
|
|
149931
|
+
label: "Refresh interval (min)",
|
|
149932
|
+
description: CLIP_CATALOG_REFRESH_MINUTES_DESCRIPTION,
|
|
149933
|
+
min: 0,
|
|
149934
|
+
max: CLIP_CATALOG_REFRESH_MINUTES_MAX,
|
|
149935
|
+
step: 5
|
|
149936
|
+
}, ...forcedWakeApplies ? [{
|
|
149937
|
+
type: "boolean",
|
|
149938
|
+
key: CLIP_CATALOG_FORCED_WAKE_KEY,
|
|
149939
|
+
label: "Wake this camera to refresh",
|
|
149940
|
+
description: CLIP_CATALOG_FORCED_WAKE_DESCRIPTION,
|
|
149941
|
+
default: false,
|
|
149942
|
+
style: "switch"
|
|
149943
|
+
}] : []]
|
|
149944
|
+
}];
|
|
149945
|
+
}
|
|
149946
|
+
//#endregion
|
|
149254
149947
|
//#region src/intercom-failure-report.ts
|
|
149255
149948
|
/**
|
|
149256
149949
|
* Per-camera talk-back counters, published through `failure-contribution`.
|
|
@@ -154328,6 +155021,73 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
154328
155021
|
* provider still treats a missing `uid` as a dropped branch rather than as
|
|
154329
155022
|
* an empty catalog.
|
|
154330
155023
|
*/
|
|
155024
|
+
/**
|
|
155025
|
+
* Re-read this camera's clip catalog. Set by the addon for every camera it
|
|
155026
|
+
* owns; `null` until it is wired (a redeploy re-registers the device, and
|
|
155027
|
+
* the addon re-wires on `DeviceRegistered` AND `DeviceMetaChanged` — the
|
|
155028
|
+
* same fan-out the email-push resolver needed, for the same reason).
|
|
155029
|
+
*/
|
|
155030
|
+
clipCatalogRefresher = null;
|
|
155031
|
+
setClipCatalogRefresher(refresh) {
|
|
155032
|
+
this.clipCatalogRefresher = refresh;
|
|
155033
|
+
}
|
|
155034
|
+
/**
|
|
155035
|
+
* This camera's OVERRIDE of the clip-refresh interval, or `undefined` when
|
|
155036
|
+
* it overrides nothing — which is what a reset leaves behind too, and is
|
|
155037
|
+
* why the resolver takes `unknown` rather than a number (D562).
|
|
155038
|
+
*/
|
|
155039
|
+
getClipCatalogRefreshMinutes() {
|
|
155040
|
+
return this.config.get("clipCatalogRefreshMinutes");
|
|
155041
|
+
}
|
|
155042
|
+
/** Whether the operator authorised the clip timer to wake this camera. */
|
|
155043
|
+
getClipCatalogForcedWake() {
|
|
155044
|
+
return this.config.get("clipCatalogForcedWake");
|
|
155045
|
+
}
|
|
155046
|
+
/**
|
|
155047
|
+
* The clip refresh the TIMER cannot do.
|
|
155048
|
+
*
|
|
155049
|
+
* On a standalone battery camera a catalog read is a login and a login is
|
|
155050
|
+
* the wake, so the periodic refresh only marks the catalog stale (D562).
|
|
155051
|
+
* This is the other half: the moment the camera confirmed itself awake for
|
|
155052
|
+
* its own reasons — a camera event, a stream, an operator action — is a free
|
|
155053
|
+
* read, and it is the same moment that already warms the stream catalog and
|
|
155054
|
+
* every settings slice. Called from `onWakeTransition`, best-effort, last:
|
|
155055
|
+
* a clip listing must never delay the live-stream catalog the operator is
|
|
155056
|
+
* waiting for.
|
|
155057
|
+
*/
|
|
155058
|
+
async refreshClipCatalogOnWake() {
|
|
155059
|
+
const refresh = this.clipCatalogRefresher;
|
|
155060
|
+
if (refresh === null) {
|
|
155061
|
+
this.ctx.logger.debug("onWakeTransition: no clip catalog refresher wired", {
|
|
155062
|
+
tags: { deviceId: this.id },
|
|
155063
|
+
meta: { branch: "clip-refresher-unwired" }
|
|
155064
|
+
});
|
|
155065
|
+
return;
|
|
155066
|
+
}
|
|
155067
|
+
try {
|
|
155068
|
+
await refresh();
|
|
155069
|
+
} catch (err) {
|
|
155070
|
+
this.ctx.logger.warn("onWakeTransition: the clip catalog refresh failed", {
|
|
155071
|
+
tags: { deviceId: this.id },
|
|
155072
|
+
meta: {
|
|
155073
|
+
branch: "clip-refresh-failed",
|
|
155074
|
+
error: err instanceof Error ? err.message : String(err)
|
|
155075
|
+
}
|
|
155076
|
+
});
|
|
155077
|
+
}
|
|
155078
|
+
}
|
|
155079
|
+
/**
|
|
155080
|
+
* Wake this camera because the OPERATOR authorised the clip timer to
|
|
155081
|
+
* (`clipCatalogForcedWake`, off by default — D562).
|
|
155082
|
+
*
|
|
155083
|
+
* The only path from the timer to a battery camera's radio, and it is the
|
|
155084
|
+
* same forced wake `wakeIfSleeping` already drives for a config button.
|
|
155085
|
+
* Throws when the camera did not come up, so the scheduler can say so and
|
|
155086
|
+
* leave the catalog stale rather than stamping it fresh.
|
|
155087
|
+
*/
|
|
155088
|
+
async wakeForClipCatalogRefresh() {
|
|
155089
|
+
if (!await this.wakeIfSleeping()) throw new Error(`ReolinkCamera ${this.id}: the authorised clip-refresh wake did not bring the camera up`);
|
|
155090
|
+
}
|
|
154331
155091
|
getClipDeviceRef() {
|
|
154332
155092
|
const uid = this.config.get("uid");
|
|
154333
155093
|
return {
|
|
@@ -158588,6 +159348,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
158588
159348
|
await this.warmSnapshotOnWake();
|
|
158589
159349
|
await this.alignAuxDevicesState("wake");
|
|
158590
159350
|
await this.refreshParentSettingsSnapshot();
|
|
159351
|
+
await this.refreshClipCatalogOnWake();
|
|
158591
159352
|
}
|
|
158592
159353
|
static PING_INTERVAL_MS = 3e4;
|
|
158593
159354
|
static PING_MAX_FAILURES = 3;
|
|
@@ -159773,7 +160534,11 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
159773
160534
|
}),
|
|
159774
160535
|
...this.buildSessionsTabSections(),
|
|
159775
160536
|
...this.buildEmailPushSection(),
|
|
159776
|
-
...this.buildEmailTabSections()
|
|
160537
|
+
...this.buildEmailTabSections(),
|
|
160538
|
+
...buildClipRefreshDeviceSection({
|
|
160539
|
+
isHubChild: this.isHubChild(),
|
|
160540
|
+
isBattery: this.isBattery
|
|
160541
|
+
})
|
|
159777
160542
|
]
|
|
159778
160543
|
};
|
|
159779
160544
|
const emailSnap = cache?.emailConfigSnapshot;
|
|
@@ -159792,6 +160557,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
159792
160557
|
emailRecipient3: emailSnap?.recipients?.[2] ?? "",
|
|
159793
160558
|
emailSendNickname: emailSnap?.sendNickname ?? this.name,
|
|
159794
160559
|
emailScheduleEnabled: emailSnap?.taskEnabled ?? false,
|
|
160560
|
+
clipCatalogRefreshMinutes: this.config.get("clipCatalogRefreshMinutes"),
|
|
160561
|
+
clipCatalogForcedWake: this.config.get("clipCatalogForcedWake") ?? false,
|
|
159795
160562
|
debugGeneral: this.config.get("debugGeneral") ?? false,
|
|
159796
160563
|
debugSocketLogs: this.config.get("debugSocketLogs") ?? [],
|
|
159797
160564
|
motionEnabled: this.config.get("motionEnabled") ?? motionSnap.enabled ?? true,
|
|
@@ -163065,6 +163832,10 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
163065
163832
|
throw new Error(`Reolink: ${reason}`);
|
|
163066
163833
|
}
|
|
163067
163834
|
async onShutdown() {
|
|
163835
|
+
if (this.clipRefreshTimer) {
|
|
163836
|
+
clearInterval(this.clipRefreshTimer);
|
|
163837
|
+
this.clipRefreshTimer = null;
|
|
163838
|
+
}
|
|
163068
163839
|
this.logChannels?.stop();
|
|
163069
163840
|
await this.emailPushServer?.stop().catch(() => {});
|
|
163070
163841
|
await this.autodetectCache.dispose();
|
|
@@ -163075,6 +163846,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
163075
163846
|
* logged inside the server and leaves it stopped until the next edit.
|
|
163076
163847
|
*/
|
|
163077
163848
|
async onConfigChanged() {
|
|
163849
|
+
await this.refreshClipRefreshGlobalStore();
|
|
163078
163850
|
await this.emailPushServer?.restart().catch((err) => {
|
|
163079
163851
|
this.ctx.logger.warn("email-push: restart after settings change failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
163080
163852
|
});
|
|
@@ -163094,7 +163866,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
163094
163866
|
* for core to select from.
|
|
163095
163867
|
*/
|
|
163096
163868
|
globalSettingsSchema() {
|
|
163097
|
-
return buildEmailPushSettingsSchema(listLanHosts()[0] ?? "127.0.0.1");
|
|
163869
|
+
return { sections: [...buildEmailPushSettingsSchema(listLanHosts()[0] ?? "127.0.0.1").sections, ...buildClipRefreshSettingsSchema().sections] };
|
|
163098
163870
|
}
|
|
163099
163871
|
/**
|
|
163100
163872
|
* Both email-push sections ARE this integration's configuration — one SMTP
|
|
@@ -163141,6 +163913,9 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
163141
163913
|
capability: videoclipsCapability,
|
|
163142
163914
|
provider: this.clipService.provider
|
|
163143
163915
|
});
|
|
163916
|
+
await this.refreshClipRefreshGlobalStore();
|
|
163917
|
+
this.startClipCatalogRefresh(this.clipService);
|
|
163918
|
+
this.wireClipCatalogRefreshers();
|
|
163144
163919
|
await this.serveClipPlanes(this.clipService);
|
|
163145
163920
|
this.ensureEmailPushServer().start().catch((err) => {
|
|
163146
163921
|
this.ctx.logger.warn("email-push: SMTP intake failed to start at init", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
@@ -163153,6 +163928,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
163153
163928
|
if (dev instanceof ReolinkCamera) {
|
|
163154
163929
|
const server = this.ensureEmailPushServer();
|
|
163155
163930
|
dev.setEmailPushParamsResolver(() => server.buildCameraSetupParams(dev));
|
|
163931
|
+
this.wireClipCatalogRefreshers();
|
|
163156
163932
|
this.ctx.logger.debug("email-push: resolver wired", {
|
|
163157
163933
|
tags: { deviceId: data.deviceId },
|
|
163158
163934
|
meta: { via }
|
|
@@ -163166,6 +163942,102 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
163166
163942
|
}
|
|
163167
163943
|
/** The clip catalog, its thumbnail cache and its two routes (D549). */
|
|
163168
163944
|
clipService = null;
|
|
163945
|
+
clipRefreshTimer = null;
|
|
163946
|
+
/**
|
|
163947
|
+
* The addon store as the refresh timer last saw it.
|
|
163948
|
+
*
|
|
163949
|
+
* `listTargets` is synchronous on purpose — a tick that awaited a settings
|
|
163950
|
+
* read per camera would be a store round-trip per camera per tick — so the
|
|
163951
|
+
* global half of the cascade is mirrored here and refreshed at init and on
|
|
163952
|
+
* every `onConfigChanged`. An empty mirror resolves to the DEFAULT, never to
|
|
163953
|
+
* "off": a settings read that has not happened yet must not switch the timer
|
|
163954
|
+
* off for the whole fleet.
|
|
163955
|
+
*/
|
|
163956
|
+
clipRefreshGlobalStore = {};
|
|
163957
|
+
/**
|
|
163958
|
+
* Give every camera the wake-side refresh.
|
|
163959
|
+
*
|
|
163960
|
+
* Idempotent, and re-run on both device lifecycle events for the reason the
|
|
163961
|
+
* email-push resolver documents: an addon-only redeploy re-registers a
|
|
163962
|
+
* device the hub already knows, which emits `DeviceMetaChanged` and NOT
|
|
163963
|
+
* `DeviceRegistered`, and subscribing to one left every battery camera
|
|
163964
|
+
* unwired.
|
|
163965
|
+
*/
|
|
163966
|
+
wireClipCatalogRefreshers() {
|
|
163967
|
+
const service = this.clipService;
|
|
163968
|
+
if (service === null) return;
|
|
163969
|
+
for (const camera of this.listReolinkCameras()) {
|
|
163970
|
+
const deviceId = camera.getClipDeviceRef().deviceId;
|
|
163971
|
+
camera.setClipCatalogRefresher(async () => {
|
|
163972
|
+
await service.refreshCatalog(deviceId);
|
|
163973
|
+
});
|
|
163974
|
+
}
|
|
163975
|
+
}
|
|
163976
|
+
/** Re-read the global half of the clip-refresh cascade. */
|
|
163977
|
+
async refreshClipRefreshGlobalStore() {
|
|
163978
|
+
try {
|
|
163979
|
+
this.clipRefreshGlobalStore = await this.resolveGlobalStore();
|
|
163980
|
+
} catch (err) {
|
|
163981
|
+
this.ctx.logger.warn("videoclips: could not read the clip refresh default — keeping the last", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
163982
|
+
}
|
|
163983
|
+
}
|
|
163984
|
+
/**
|
|
163985
|
+
* Arm the clip-catalog refresh.
|
|
163986
|
+
*
|
|
163987
|
+
* The scheduler owns no clock: it is ticked, so its rules are testable from
|
|
163988
|
+
* COLD (`clip-refresh.spec.ts` — the discipline D561 cost us). The tick
|
|
163989
|
+
* cadence is the FLOOR of what any camera can be configured to, not the
|
|
163990
|
+
* interval itself; each camera's own interval is decided inside the tick
|
|
163991
|
+
* from the cascade, so changing a setting takes effect without re-arming.
|
|
163992
|
+
*/
|
|
163993
|
+
startClipCatalogRefresh(service) {
|
|
163994
|
+
const scheduler = new ClipCatalogRefreshScheduler({
|
|
163995
|
+
now: () => Date.now(),
|
|
163996
|
+
listTargets: () => this.clipRefreshTargets(),
|
|
163997
|
+
refresh: async (deviceId) => {
|
|
163998
|
+
await service.refreshCatalog(deviceId);
|
|
163999
|
+
},
|
|
164000
|
+
markStale: (deviceId) => {
|
|
164001
|
+
service.cache.markStale(deviceId);
|
|
164002
|
+
},
|
|
164003
|
+
wake: async (deviceId) => {
|
|
164004
|
+
const device = this.ctx.kernel.deviceRegistry?.getById(deviceId);
|
|
164005
|
+
if (!(device instanceof ReolinkCamera)) throw new Error(`clip refresh: device ${String(deviceId)} is not a Reolink camera here`);
|
|
164006
|
+
await device.wakeForClipCatalogRefresh();
|
|
164007
|
+
},
|
|
164008
|
+
hostBusy: (hostKey) => service.hostBusy(hostKey),
|
|
164009
|
+
logger: this.ctx.logger
|
|
164010
|
+
});
|
|
164011
|
+
this.clipRefreshTimer = setInterval(() => {
|
|
164012
|
+
scheduler.tick().catch((err) => {
|
|
164013
|
+
this.ctx.logger.warn("videoclips: the catalog refresh tick failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
164014
|
+
});
|
|
164015
|
+
}, 5 * 6e4);
|
|
164016
|
+
if (typeof this.clipRefreshTimer.unref === "function") this.clipRefreshTimer.unref();
|
|
164017
|
+
}
|
|
164018
|
+
/**
|
|
164019
|
+
* Every Reolink CAMERA this process owns, with its cascade resolved.
|
|
164020
|
+
*
|
|
164021
|
+
* Cameras only — the Home Hub (639) is `type: hub` and never binds
|
|
164022
|
+
* `videoclips` (`deviceTypes: [Camera]`); its catalog IS its children's and
|
|
164023
|
+
* is refreshed per child, on its one session. Accessories are not cameras.
|
|
164024
|
+
*/
|
|
164025
|
+
clipRefreshTargets() {
|
|
164026
|
+
const globalMinutes = this.clipRefreshGlobalStore[CLIP_CATALOG_REFRESH_MINUTES_KEY];
|
|
164027
|
+
const out = [];
|
|
164028
|
+
for (const device of this.listReolinkCameras()) {
|
|
164029
|
+
const ref = device.getClipDeviceRef();
|
|
164030
|
+
out.push({
|
|
164031
|
+
deviceId: ref.deviceId,
|
|
164032
|
+
hostKey: ref.hostKey,
|
|
164033
|
+
isHubChild: ref.isHubChild,
|
|
164034
|
+
isBattery: ref.isBattery,
|
|
164035
|
+
forcedWake: resolveClipCatalogForcedWake(device.getClipCatalogForcedWake()),
|
|
164036
|
+
intervalMinutes: resolveClipCatalogRefreshMinutes(globalMinutes, device.getClipCatalogRefreshMinutes())
|
|
164037
|
+
});
|
|
164038
|
+
}
|
|
164039
|
+
return out;
|
|
164040
|
+
}
|
|
163169
164041
|
/**
|
|
163170
164042
|
* Host the thumbnail and media planes, and never let a host without the
|
|
163171
164043
|
* facility take the addon down with it — a data plane is an enhancement to
|