@camstack/addon-provider-reolink 1.2.146 → 1.2.148
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 +1632 -649
- package/dist/addon.mjs +1473 -490
- 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).
|
|
@@ -7437,6 +7437,8 @@ var EVENT_OWNER_TYPES = [
|
|
|
7437
7437
|
* nothing failing until a caller asked.
|
|
7438
7438
|
*/
|
|
7439
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"]);
|
|
7440
7442
|
new Set(EVENT_OWNER_TYPES);
|
|
7441
7443
|
var EncodeProfileSchema = object({
|
|
7442
7444
|
video: object({
|
|
@@ -14982,8 +14984,6 @@ method(object({
|
|
|
14982
14984
|
deviceId: number(),
|
|
14983
14985
|
capName: string(),
|
|
14984
14986
|
wrapperAddonId: string(),
|
|
14985
|
-
/** The `ClipSource.source` id to toggle. Absent = all of the addon's. */
|
|
14986
|
-
sourceId: string().optional(),
|
|
14987
14987
|
active: boolean()
|
|
14988
14988
|
}), _void(), {
|
|
14989
14989
|
kind: "mutation",
|
|
@@ -22012,7 +22012,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
22012
22012
|
ownerType: EventOwnerTypeSchema,
|
|
22013
22013
|
eventId: number().int(),
|
|
22014
22014
|
deviceId: number()
|
|
22015
|
-
}), 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, {
|
|
22016
22020
|
kind: "mutation",
|
|
22017
22021
|
auth: "admin"
|
|
22018
22022
|
}), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
|
|
@@ -25110,6 +25114,24 @@ method(VectorDeclareIndexInputSchema, _void(), {
|
|
|
25110
25114
|
kind: "mutation",
|
|
25111
25115
|
auth: "admin"
|
|
25112
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";
|
|
25113
25135
|
var ClipSchema = object({
|
|
25114
25136
|
/** Opaque, provider-namespaced id. The default provider encodes the time
|
|
25115
25137
|
* window so `getClipPlayback` is self-contained (no event re-query). */
|
|
@@ -25260,7 +25282,25 @@ var ClipSchema = object({
|
|
|
25260
25282
|
*/
|
|
25261
25283
|
playable: boolean().optional(),
|
|
25262
25284
|
/** Why {@link playable} is false, verbatim (`no-file-for-window`). */
|
|
25263
|
-
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()
|
|
25264
25304
|
});
|
|
25265
25305
|
var ClipPlaybackSchema = object({
|
|
25266
25306
|
/**
|
|
@@ -25343,59 +25383,17 @@ var ClipSourceSchema = object({
|
|
|
25343
25383
|
* alternative — a `native:reolink:* → provider-reolink` table inside the
|
|
25344
25384
|
* widget — is a second authority on provider identity living in the one
|
|
25345
25385
|
* package with no business knowing it, wrong the day a third source appears
|
|
25346
|
-
* (D557). One addon may serve SEVERAL sources
|
|
25347
|
-
*
|
|
25348
|
-
*
|
|
25349
|
-
*
|
|
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).
|
|
25350
25390
|
*
|
|
25351
25391
|
* Optional for version skew only. The collection dispatcher stamps it from
|
|
25352
25392
|
* the registry, so a row that travelled through the fan-out carries the
|
|
25353
25393
|
* authoritative id whatever the provider filled in.
|
|
25354
25394
|
*/
|
|
25355
25395
|
addonId: string().optional(),
|
|
25356
|
-
|
|
25357
|
-
* Which API this source resolved to FOR THIS CAMERA, when it has a choice.
|
|
25358
|
-
*
|
|
25359
|
-
* A source may cover one store through more than one surface — the Reolink
|
|
25360
|
-
* provider reads a hub child through the parent's event log and a standalone
|
|
25361
|
-
* through its own file list, because that is what each camera answers. The
|
|
25362
|
-
* CHOICE is the provider's, made from what the camera is, and is never a row
|
|
25363
|
-
* the operator has to understand; but it is REPORTED, because a source that
|
|
25364
|
-
* silently reads a different API on two cameras and then behaves differently
|
|
25365
|
-
* is the thing nobody can debug later. Absent when the source has only one
|
|
25366
|
-
* way to read its store.
|
|
25367
|
-
*/
|
|
25368
|
-
via: string().optional(),
|
|
25369
|
-
availability: ClipSourceAvailabilitySchema,
|
|
25370
|
-
/**
|
|
25371
|
-
* The operator switched this source OFF for this camera.
|
|
25372
|
-
*
|
|
25373
|
-
* Deliberately NOT a member of {@link ClipSourceAvailabilitySchema}'s
|
|
25374
|
-
* vocabulary. That enum models what the source CAN do — a sleeping camera, an
|
|
25375
|
-
* unmounted card, an index that disagrees with its own calendar — and a
|
|
25376
|
-
* switched-off source could answer perfectly well; the operator decided it
|
|
25377
|
-
* should not. Folding the choice in is how `disabled` and `broken` stop being
|
|
25378
|
-
* distinguishable, which is the D62 rule this repo has already paid for twice:
|
|
25379
|
-
* an off switch is REPORTED off (`CameraStatus.switchedOff`, the same word),
|
|
25380
|
-
* and disabled must never look like broken. It is also what lets every
|
|
25381
|
-
* exhaustive consumer of the availability enum keep compiling.
|
|
25382
|
-
*
|
|
25383
|
-
* A switched-off source contributes NO clips (`listClips` never calls it) and
|
|
25384
|
-
* its row carries no `catalogAsOf`: nothing confirms a catalog it is not
|
|
25385
|
-
* allowed to serve, and a frozen age that can only grow draws a stalling
|
|
25386
|
-
* source rather than an off switch.
|
|
25387
|
-
*
|
|
25388
|
-
* The row survives BECAUSE it is the control the operator switches the source
|
|
25389
|
-
* back on from — D554 decision 4's rule ("a source that cannot answer
|
|
25390
|
-
* produces a ROW, not an absence") applied to the one case D556 carved out of
|
|
25391
|
-
* it, and D557's own kept property ("a source is never hidden, only its
|
|
25392
|
-
* rows"). Absent means on.
|
|
25393
|
-
*
|
|
25394
|
-
* A provider never sets this — like {@link ClipSourceSchema.addonId} it is
|
|
25395
|
-
* stamped by the collection dispatcher, which holds the registry's projection
|
|
25396
|
-
* of the persisted authority (D556) and is the only place that knows it.
|
|
25397
|
-
*/
|
|
25398
|
-
switchedOff: boolean().optional()
|
|
25396
|
+
availability: ClipSourceAvailabilitySchema
|
|
25399
25397
|
});
|
|
25400
25398
|
var videoclipsCapability = {
|
|
25401
25399
|
name: "videoclips",
|
|
@@ -25449,22 +25447,40 @@ var videoclipsCapability = {
|
|
|
25449
25447
|
limit: number().int().positive().optional(),
|
|
25450
25448
|
/**
|
|
25451
25449
|
* WHICH provider to ask — the `addonId` a {@link ClipSourceSchema} row
|
|
25452
|
-
* carries, never a source id and never a list.
|
|
25450
|
+
* carries, never a source id and never a list. **Required** (D554 amended).
|
|
25453
25451
|
*
|
|
25454
|
-
*
|
|
25455
|
-
*
|
|
25456
|
-
*
|
|
25457
|
-
*
|
|
25458
|
-
*
|
|
25452
|
+
* A provider the device is not bound to is refused by name rather than
|
|
25453
|
+
* answered by another one (D552's `rejectUnresolvedAddonPin` rule).
|
|
25454
|
+
*
|
|
25455
|
+
* It was optional, documented as "absent means the device's BOUND
|
|
25456
|
+
* provider, which is CamStack on every camera". No code implemented
|
|
25457
|
+
* that. Measured on the live hub 2026-09-20 — device 592, bound to
|
|
25458
|
+
* `recorder` AND `provider-reolink` — a bare call with `limit: 3`
|
|
25459
|
+
* answered SIX rows, three from each source, merged newest-first:
|
|
25460
|
+
* `device-collection-dispatch.ts` simply left the fan-out un-narrowed,
|
|
25461
|
+
* so absence bought the union this method exists not to be, and
|
|
25462
|
+
* `limit` meant `limit × sources`.
|
|
25463
|
+
*
|
|
25464
|
+
* There is nothing to restore the default to. `getBindings` answers a
|
|
25465
|
+
* collection cap with a DERIVED, PLURAL set (D554 amended, step 0);
|
|
25466
|
+
* `setWrapperActive` — the singleton authority that could name one —
|
|
25467
|
+
* throws for a collection cap by design. Naming CamStack here instead
|
|
25468
|
+
* would privilege one addon by id inside a surface whose premise is
|
|
25469
|
+
* that sources are peers, and would be wrong on the first camera with
|
|
25470
|
+
* no recorder binding. So absence is REFUSED, in the schema, where the
|
|
25471
|
+
* generated types make it unomittable rather than merely discouraged.
|
|
25472
|
+
*
|
|
25473
|
+
* The default belongs to the SURFACE, which has the `listSources` rows
|
|
25474
|
+
* and can say which one it picked (D569 § 1–3; the viewer already
|
|
25475
|
+
* always sends this).
|
|
25459
25476
|
*
|
|
25460
25477
|
* It replaced `sources?: string[]`, a VIEW filter over source ids that
|
|
25461
25478
|
* assumed the answer was a fan-out over everything a camera has. The
|
|
25462
|
-
* operator settled otherwise on 2026-09-20 — *"
|
|
25463
|
-
*
|
|
25464
|
-
*
|
|
25465
|
-
* at a time and there is nothing to filter out of it.
|
|
25479
|
+
* operator settled otherwise on 2026-09-20 — *"l'utilizzatore è uno
|
|
25480
|
+
* solo"* — so the list is asked of one provider at a time and there is
|
|
25481
|
+
* nothing to filter out of it.
|
|
25466
25482
|
*/
|
|
25467
|
-
provider: string().
|
|
25483
|
+
provider: string().min(1)
|
|
25468
25484
|
}), array(ClipSchema).readonly(), {
|
|
25469
25485
|
kind: "query",
|
|
25470
25486
|
auth: "protected"
|
|
@@ -41442,6 +41458,12 @@ Object.freeze({
|
|
|
41442
41458
|
addonId: null,
|
|
41443
41459
|
access: "view"
|
|
41444
41460
|
},
|
|
41461
|
+
"pipelineAnalytics.ownersWithMedia": {
|
|
41462
|
+
capName: "pipeline-analytics",
|
|
41463
|
+
capScope: "device",
|
|
41464
|
+
addonId: null,
|
|
41465
|
+
access: "view"
|
|
41466
|
+
},
|
|
41445
41467
|
"pipelineAnalytics.pauseForStorageMigration": {
|
|
41446
41468
|
capName: "pipeline-analytics",
|
|
41447
41469
|
capScope: "device",
|
|
@@ -45342,6 +45364,11 @@ Object.freeze({
|
|
|
45342
45364
|
form: "single",
|
|
45343
45365
|
optional: false
|
|
45344
45366
|
}],
|
|
45367
|
+
"pipelineAnalytics.ownersWithMedia": [{
|
|
45368
|
+
name: "deviceId",
|
|
45369
|
+
form: "single",
|
|
45370
|
+
optional: false
|
|
45371
|
+
}],
|
|
45345
45372
|
"pipelineAnalytics.proposeRetrainAnnotations": [{
|
|
45346
45373
|
name: "deviceId",
|
|
45347
45374
|
form: "single",
|
|
@@ -69361,11 +69388,11 @@ var require_yazl = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
|
|
|
69361
69388
|
}
|
|
69362
69389
|
}));
|
|
69363
69390
|
//#endregion
|
|
69364
|
-
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-
|
|
69391
|
+
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-UIQ527B6.js
|
|
69365
69392
|
var import_undici = require_undici();
|
|
69366
69393
|
var import_yazl = /* @__PURE__ */ require_chunk.__toESM(require_yazl(), 1);
|
|
69367
69394
|
var formatterCache = /* @__PURE__ */ new Map();
|
|
69368
|
-
function formatterFor$
|
|
69395
|
+
function formatterFor$2(timeZone) {
|
|
69369
69396
|
const cached = formatterCache.get(timeZone);
|
|
69370
69397
|
if (cached) return cached;
|
|
69371
69398
|
const fmt = new Intl.DateTimeFormat("en-US", {
|
|
@@ -69390,7 +69417,7 @@ function wallClockParts(d, timeZone) {
|
|
|
69390
69417
|
minute: d.getMinutes(),
|
|
69391
69418
|
second: d.getSeconds()
|
|
69392
69419
|
};
|
|
69393
|
-
const parts = formatterFor$
|
|
69420
|
+
const parts = formatterFor$2(timeZone).formatToParts(d);
|
|
69394
69421
|
const pick = (type) => {
|
|
69395
69422
|
const p = parts.find((x) => x.type === type);
|
|
69396
69423
|
return Number.parseInt(p?.value ?? "", 10);
|
|
@@ -70707,13 +70734,13 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70707
70734
|
sec: 59
|
|
70708
70735
|
};
|
|
70709
70736
|
}
|
|
70710
|
-
|
|
70737
|
+
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)}`);
|
|
70711
70738
|
if (!bypassCache) {
|
|
70712
70739
|
this.cleanRecordingsCache();
|
|
70713
70740
|
const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
|
|
70714
70741
|
const cached = this.recordingsCache.get(cacheKey);
|
|
70715
70742
|
if (cached && cached.expiresAt > Date.now()) {
|
|
70716
|
-
|
|
70743
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Cache hit: returning ${cached.data.length} cached enriched recordings`);
|
|
70717
70744
|
return cached.data;
|
|
70718
70745
|
}
|
|
70719
70746
|
}
|
|
@@ -70765,7 +70792,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70765
70792
|
const firstResult = dayResponse[0];
|
|
70766
70793
|
if (firstResult && firstResult.code === 0 && firstResult.value?.SearchResult?.File) {
|
|
70767
70794
|
const files = firstResult.value.SearchResult.File;
|
|
70768
|
-
if (files.length > 0 && files[0])
|
|
70795
|
+
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({
|
|
70769
70796
|
fileCount: files.length,
|
|
70770
70797
|
sampleFile: {
|
|
70771
70798
|
name: files[0].name,
|
|
@@ -70801,7 +70828,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70801
70828
|
} catch (e) {}
|
|
70802
70829
|
enriched2.push(this.enrichVodFile(vodFile, channel, streamUrl));
|
|
70803
70830
|
}
|
|
70804
|
-
|
|
70831
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched2.length} enriched recording files from autoSearchByDay`);
|
|
70805
70832
|
if (!bypassCache) {
|
|
70806
70833
|
const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
|
|
70807
70834
|
this.recordingsCache.set(cacheKey, {
|
|
@@ -70829,7 +70856,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70829
70856
|
const response = await this.call("Search", param, 0);
|
|
70830
70857
|
allResults.push(...response);
|
|
70831
70858
|
} else {
|
|
70832
|
-
|
|
70859
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Range spans ${dayRanges.length} days, splitting into separate day queries`);
|
|
70833
70860
|
for (const range of dayRanges) {
|
|
70834
70861
|
const param = { Search: {
|
|
70835
70862
|
channel,
|
|
@@ -70839,7 +70866,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70839
70866
|
EndTime: range.end
|
|
70840
70867
|
} };
|
|
70841
70868
|
if (iLogicChannel > 0) param.Search.iLogicChannel = iLogicChannel;
|
|
70842
|
-
|
|
70869
|
+
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})`);
|
|
70843
70870
|
const response = await this.call("Search", param, 0);
|
|
70844
70871
|
allResults.push(...response);
|
|
70845
70872
|
}
|
|
@@ -70849,7 +70876,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70849
70876
|
const firstResult = result[0];
|
|
70850
70877
|
if (firstResult && firstResult.code === 0 && firstResult.value?.SearchResult?.File) {
|
|
70851
70878
|
const files = firstResult.value.SearchResult.File;
|
|
70852
|
-
if (files.length > 0 && files[0])
|
|
70879
|
+
if (files.length > 0 && files[0]) require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response (normal search): ${JSON.stringify({
|
|
70853
70880
|
fileCount: files.length,
|
|
70854
70881
|
sampleFile: {
|
|
70855
70882
|
name: files[0].name,
|
|
@@ -70866,12 +70893,12 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70866
70893
|
const searchResult = res.value?.SearchResult;
|
|
70867
70894
|
if (searchResult?.File) allFiles.push(...searchResult.File);
|
|
70868
70895
|
}
|
|
70869
|
-
|
|
70896
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Collected ${allFiles.length} raw VodFiles from API search results`);
|
|
70870
70897
|
if (allFiles.length > 0) {
|
|
70871
70898
|
const sampleSize = Math.min(3, allFiles.length);
|
|
70872
70899
|
for (let i = 0; i < sampleSize; i++) {
|
|
70873
70900
|
const file = allFiles[i];
|
|
70874
|
-
if (file)
|
|
70901
|
+
if (file) require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Sample file ${i + 1}/${allFiles.length}: ${JSON.stringify({
|
|
70875
70902
|
name: file.name,
|
|
70876
70903
|
type: file.type,
|
|
70877
70904
|
size: file.size,
|
|
@@ -70894,7 +70921,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70894
70921
|
} catch (e) {}
|
|
70895
70922
|
enriched.push(this.enrichVodFile(vodFile, channel, streamUrl));
|
|
70896
70923
|
}
|
|
70897
|
-
|
|
70924
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched.length} enriched recording files`);
|
|
70898
70925
|
if (!bypassCache) {
|
|
70899
70926
|
const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
|
|
70900
70927
|
this.recordingsCache.set(cacheKey, {
|
|
@@ -70931,7 +70958,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70931
70958
|
prepare: true,
|
|
70932
70959
|
seek: seekSeconds
|
|
70933
70960
|
});
|
|
70934
|
-
|
|
70961
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`);
|
|
70935
70962
|
const { spawn: spawn2 } = await import("child_process");
|
|
70936
70963
|
return new Promise((resolve, reject) => {
|
|
70937
70964
|
const chunks = [];
|
|
@@ -70979,7 +71006,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
70979
71006
|
reject(/* @__PURE__ */ new Error(`ffmpeg produced too small output: ${jpeg.length} bytes`));
|
|
70980
71007
|
return;
|
|
70981
71008
|
}
|
|
70982
|
-
|
|
71009
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Successfully extracted thumbnail: ${jpeg.length} bytes`);
|
|
70983
71010
|
resolve(jpeg);
|
|
70984
71011
|
} else reject(/* @__PURE__ */ new Error(`ffmpeg exited with code ${code}: ${stderr.slice(-500)}`));
|
|
70985
71012
|
});
|
|
@@ -71028,10 +71055,10 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
71028
71055
|
}
|
|
71029
71056
|
} }
|
|
71030
71057
|
}];
|
|
71031
|
-
|
|
71058
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Request body: ${JSON.stringify(body)}`);
|
|
71032
71059
|
try {
|
|
71033
71060
|
const response = await this.callMany(body);
|
|
71034
|
-
|
|
71061
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Response: ${JSON.stringify(response)}`);
|
|
71035
71062
|
const first = response[0];
|
|
71036
71063
|
if (!first || first.code !== 0 || !first.value?.fileList) {
|
|
71037
71064
|
const rspCode = first?.error?.rspCode;
|
|
@@ -71052,7 +71079,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
71052
71079
|
if (!filename) throw new Error(`NvrDownload: no files found in response`);
|
|
71053
71080
|
return filename;
|
|
71054
71081
|
} catch (error) {
|
|
71055
|
-
|
|
71082
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
71056
71083
|
throw error;
|
|
71057
71084
|
}
|
|
71058
71085
|
}
|
|
@@ -71248,7 +71275,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
71248
71275
|
* Convert a VodFile to RecordingFile with parsed metadata.
|
|
71249
71276
|
*/
|
|
71250
71277
|
enrichVodFile(vodFile, channel, streamUrl) {
|
|
71251
|
-
|
|
71278
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `RAW VodFile from API: ${JSON.stringify({
|
|
71252
71279
|
name: vodFile.name,
|
|
71253
71280
|
type: vodFile.type,
|
|
71254
71281
|
size: vodFile.size,
|
|
@@ -71301,7 +71328,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
71301
71328
|
hashBits0to15: hashRevBin.slice(0, 16)
|
|
71302
71329
|
};
|
|
71303
71330
|
}
|
|
71304
|
-
|
|
71331
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Parsed filename: ${JSON.stringify({
|
|
71305
71332
|
fileName: vodFile.name,
|
|
71306
71333
|
filenameParts,
|
|
71307
71334
|
hexValueFromFilename,
|
|
@@ -71328,13 +71355,13 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
71328
71355
|
const hexFlags = parsed?.flags;
|
|
71329
71356
|
if (parsed?.devType === "hub" && parsed?.version === 4) {
|
|
71330
71357
|
if (hexValueFromFilename && hexValueFromFilename !== "unknown") {
|
|
71331
|
-
|
|
71332
|
-
|
|
71358
|
+
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.`);
|
|
71359
|
+
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.`);
|
|
71333
71360
|
}
|
|
71334
71361
|
}
|
|
71335
|
-
|
|
71362
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Hex flags from filename: ${JSON.stringify(hexFlags)}`);
|
|
71336
71363
|
const typeFlags = this.parseRecordTypeFlags(vodFile.type);
|
|
71337
|
-
|
|
71364
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Type flags from recordType "${vodFile.type}": ${JSON.stringify(typeFlags)}`);
|
|
71338
71365
|
const hasPerson = (hexFlags?.aiPerson ?? false) || typeFlags.hasPerson;
|
|
71339
71366
|
const hasVehicle = (hexFlags?.aiVehicle ?? false) || typeFlags.hasVehicle;
|
|
71340
71367
|
const hasAnimal = (hexFlags?.aiAnimal ?? false) || typeFlags.hasAnimal;
|
|
@@ -71345,7 +71372,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
|
|
|
71345
71372
|
const hasPackage = (hexFlags?.package ?? false) || typeFlags.hasPackage;
|
|
71346
71373
|
const hasRf = (hexFlags?.rf ?? false) || typeFlags.hasRf;
|
|
71347
71374
|
const hasOther = (hexFlags?.aiOther ?? false) || typeFlags.hasOther;
|
|
71348
|
-
|
|
71375
|
+
require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Final merged flags: ${JSON.stringify({
|
|
71349
71376
|
hasPerson,
|
|
71350
71377
|
hasVehicle,
|
|
71351
71378
|
hasAnimal,
|
|
@@ -71751,8 +71778,8 @@ function nowIsoCompact() {
|
|
|
71751
71778
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
71752
71779
|
}
|
|
71753
71780
|
function nalTypesSummary(videoType, accessUnitAnnexB) {
|
|
71754
|
-
if (videoType === "H264") return
|
|
71755
|
-
return
|
|
71781
|
+
if (videoType === "H264") return require_chunk_ZBACPERX.splitAnnexBToNalPayloads(accessUnitAnnexB).map((n) => (n[0] ?? 0) & 31);
|
|
71782
|
+
return require_chunk_ZBACPERX.splitAnnexBToNalPayloads2(accessUnitAnnexB).map((nal) => require_chunk_ZBACPERX.getH265NalType(nal)).filter((t) => typeof t === "number");
|
|
71756
71783
|
}
|
|
71757
71784
|
function normalizeProfiles(p) {
|
|
71758
71785
|
const out = [];
|
|
@@ -72192,7 +72219,7 @@ async function sampleStreams(opts) {
|
|
|
72192
72219
|
});
|
|
72193
72220
|
};
|
|
72194
72221
|
api.client.on("frame", onFrame);
|
|
72195
|
-
const videoStream = new
|
|
72222
|
+
const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
|
|
72196
72223
|
client: api.client,
|
|
72197
72224
|
api,
|
|
72198
72225
|
channel,
|
|
@@ -73242,7 +73269,7 @@ async function runMultifocalDiagnosticsConsecutively(params) {
|
|
|
73242
73269
|
let audioFrames = 0;
|
|
73243
73270
|
let lastSnapshotAtMs = 0;
|
|
73244
73271
|
const client = params.api.client;
|
|
73245
|
-
const videoStream = new
|
|
73272
|
+
const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
|
|
73246
73273
|
client,
|
|
73247
73274
|
channel: chNative,
|
|
73248
73275
|
profile,
|
|
@@ -73326,7 +73353,7 @@ async function runMultifocalDiagnosticsConsecutively(params) {
|
|
|
73326
73353
|
const baseStreamName = profile === "main" ? "mainStream" : profile === "sub" ? "subStream" : "externStream";
|
|
73327
73354
|
const headerStreamTypeCandidates = profile === "sub" ? [1, 3] : profile === "main" ? [0, 2] : [0];
|
|
73328
73355
|
const channelIdTagCandidates = uniq([chNative, chNative + 1].filter((n) => Number.isFinite(n) && n >= 0));
|
|
73329
|
-
const messageClassCandidates = params.probeFull ? [
|
|
73356
|
+
const messageClassCandidates = params.probeFull ? [require_chunk_ZBACPERX.BC_CLASS_MODERN_24, 0] : [require_chunk_ZBACPERX.BC_CLASS_MODERN_24];
|
|
73330
73357
|
const extensionXmlCandidates = params.probeFull ? ["", buildChannelExtensionXml(chNative)] : [buildChannelExtensionXml(chNative)];
|
|
73331
73358
|
const attempts = [];
|
|
73332
73359
|
for (const messageClass of messageClassCandidates) for (const streamTypeHeader of headerStreamTypeCandidates) for (const extensionXml of extensionXmlCandidates) {
|
|
@@ -75847,14 +75874,14 @@ var require_lz4 = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
|
|
|
75847
75874
|
};
|
|
75848
75875
|
}));
|
|
75849
75876
|
//#endregion
|
|
75850
|
-
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-
|
|
75877
|
+
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-5PJ6XPBF.js
|
|
75851
75878
|
var import_fxp = require_fxp();
|
|
75852
75879
|
var import_lz4 = /* @__PURE__ */ require_chunk.__toESM(require_lz4(), 1);
|
|
75853
75880
|
function encodeHeader(h) {
|
|
75854
|
-
const hasOffset =
|
|
75881
|
+
const hasOffset = require_chunk_ZBACPERX.bcHeaderHasPayloadOffset(h.messageClass);
|
|
75855
75882
|
const headerLen = hasOffset ? 24 : 20;
|
|
75856
75883
|
const buf = Buffer.alloc(headerLen);
|
|
75857
|
-
const magic = h.magic ??
|
|
75884
|
+
const magic = h.magic ?? require_chunk_ZBACPERX.BC_MAGIC;
|
|
75858
75885
|
if (magic.length !== 4) throw new Error("magic must be 4 bytes");
|
|
75859
75886
|
magic.copy(buf, 0);
|
|
75860
75887
|
buf.writeUInt32LE(h.cmdId >>> 0, 4);
|
|
@@ -75870,7 +75897,7 @@ function encodeHeader(h) {
|
|
|
75870
75897
|
function decodeHeader(buf) {
|
|
75871
75898
|
if (buf.length < 20) throw new Error("not enough data for Baichuan header");
|
|
75872
75899
|
const magic = buf.subarray(0, 4);
|
|
75873
|
-
if (!magic.equals(
|
|
75900
|
+
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")}`);
|
|
75874
75901
|
const cmdId = buf.readUInt32LE(4);
|
|
75875
75902
|
const bodyLen = buf.readUInt32LE(8);
|
|
75876
75903
|
const channelId = buf.readUInt8(12);
|
|
@@ -75878,7 +75905,7 @@ function decodeHeader(buf) {
|
|
|
75878
75905
|
const msgNum = buf.readUInt16LE(14);
|
|
75879
75906
|
const responseCode = buf.readUInt16LE(16);
|
|
75880
75907
|
const messageClass = buf.readUInt16LE(18);
|
|
75881
|
-
const headerLen =
|
|
75908
|
+
const headerLen = require_chunk_ZBACPERX.bcHeaderHasPayloadOffset(messageClass) ? 24 : 20;
|
|
75882
75909
|
if (buf.length < headerLen) throw new Error("not enough data for Baichuan header (needs 24 bytes)");
|
|
75883
75910
|
const messageKey = buf.readUInt32LE(12);
|
|
75884
75911
|
const header = {
|
|
@@ -75948,9 +75975,9 @@ var BaichuanFrameParser = class {
|
|
|
75948
75975
|
this.needed = 4;
|
|
75949
75976
|
break;
|
|
75950
75977
|
}
|
|
75951
|
-
if (!this.buffer.subarray(0, 4).equals(
|
|
75952
|
-
const idx = this.buffer.indexOf(
|
|
75953
|
-
const idxRev = this.buffer.indexOf(
|
|
75978
|
+
if (!this.buffer.subarray(0, 4).equals(require_chunk_ZBACPERX.BC_MAGIC) && !this.buffer.subarray(0, 4).equals(require_chunk_ZBACPERX.BC_MAGIC_REV)) {
|
|
75979
|
+
const idx = this.buffer.indexOf(require_chunk_ZBACPERX.BC_MAGIC);
|
|
75980
|
+
const idxRev = this.buffer.indexOf(require_chunk_ZBACPERX.BC_MAGIC_REV);
|
|
75954
75981
|
const next = idx === -1 ? idxRev : idxRev === -1 ? idx : Math.min(idx, idxRev);
|
|
75955
75982
|
if (next === -1) {
|
|
75956
75983
|
this.buffer = this.buffer.subarray(Math.max(0, this.buffer.length - 3));
|
|
@@ -77965,7 +77992,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
77965
77992
|
constructor(options) {
|
|
77966
77993
|
super();
|
|
77967
77994
|
this.opts = options;
|
|
77968
|
-
this.debugCfg =
|
|
77995
|
+
this.debugCfg = require_chunk_ZBACPERX.normalizeDebugOptions(options.debugOptions);
|
|
77969
77996
|
this.logger = createDebugGateLogger(options.logger, this.debugCfg.general || this.debugCfg.traceNativeStream || this.debugCfg.traceRecordings || this.debugCfg.traceTalk || this.debugCfg.traceEvents || this.debugCfg.debugRtsp);
|
|
77970
77997
|
this.on("error", (err) => {
|
|
77971
77998
|
this.logFixed("error", {
|
|
@@ -78447,7 +78474,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
78447
78474
|
streamType: 0,
|
|
78448
78475
|
msgNum,
|
|
78449
78476
|
responseCode: 0,
|
|
78450
|
-
messageClass:
|
|
78477
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
78451
78478
|
payloadOffset: 0
|
|
78452
78479
|
});
|
|
78453
78480
|
this.logDebug("udp_keepalive_ping_tx", {
|
|
@@ -78479,7 +78506,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
78479
78506
|
cmdId: 93,
|
|
78480
78507
|
channel: this.opts.channel ?? 0,
|
|
78481
78508
|
channelIdOverride: 0,
|
|
78482
|
-
messageClass:
|
|
78509
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
78483
78510
|
streamType: 0,
|
|
78484
78511
|
extensionXml: "",
|
|
78485
78512
|
internal: true
|
|
@@ -78818,7 +78845,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
78818
78845
|
cmdId: 2,
|
|
78819
78846
|
payloadXml: logoutXml,
|
|
78820
78847
|
extensionXml: "",
|
|
78821
|
-
messageClass:
|
|
78848
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
78822
78849
|
channelIdOverride: effectiveHostChannelId,
|
|
78823
78850
|
timeoutMs: 5e3
|
|
78824
78851
|
})).header.responseCode;
|
|
@@ -78931,12 +78958,12 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
78931
78958
|
};
|
|
78932
78959
|
s.frames++;
|
|
78933
78960
|
if (now - s.lastLogMs >= 1e3) {
|
|
78934
|
-
|
|
78961
|
+
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}`);
|
|
78935
78962
|
s.lastLogMs = now;
|
|
78936
78963
|
s.frames = 0;
|
|
78937
78964
|
}
|
|
78938
78965
|
this.rxCmdTraceStats.set(3, s);
|
|
78939
|
-
} else
|
|
78966
|
+
} 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}`);
|
|
78940
78967
|
if (this.transport === "udp" && frame.header.cmdId === 234) {
|
|
78941
78968
|
try {
|
|
78942
78969
|
const header = encodeHeader({
|
|
@@ -78973,7 +79000,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
78973
79000
|
}
|
|
78974
79001
|
return;
|
|
78975
79002
|
}
|
|
78976
|
-
if (this.debugCfg.traceTalk && isTalkCmd(frame.header.cmdId))
|
|
79003
|
+
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}`);
|
|
78977
79004
|
this.emit("frame", frame);
|
|
78978
79005
|
const key = `${frame.header.cmdId}:${frame.header.msgNum}`;
|
|
78979
79006
|
const pending = this.pending.get(key);
|
|
@@ -78994,7 +79021,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
78994
79021
|
};
|
|
78995
79022
|
s.frames++;
|
|
78996
79023
|
if (now2 - s.lastLogMs >= 500) {
|
|
78997
|
-
|
|
79024
|
+
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}`);
|
|
78998
79025
|
s.lastLogMs = now2;
|
|
78999
79026
|
s.frames = 0;
|
|
79000
79027
|
}
|
|
@@ -79025,7 +79052,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79025
79052
|
const eventLogTag = sid ? `BaichuanEvent sid=${sid}` : "BaichuanEvent";
|
|
79026
79053
|
const events$1 = this.parseEvents(frame);
|
|
79027
79054
|
for (const event of events$1) {
|
|
79028
|
-
|
|
79055
|
+
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 ?? ""}` : ""));
|
|
79029
79056
|
this.emit("event", event);
|
|
79030
79057
|
}
|
|
79031
79058
|
} catch (error) {
|
|
@@ -79084,7 +79111,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79084
79111
|
if (!xml || !xml.startsWith("<?xml")) return [];
|
|
79085
79112
|
if (this.debugCfg.traceEvents) {
|
|
79086
79113
|
const snippet = xml.length > 500 ? `${xml.slice(0, 500)}...` : xml;
|
|
79087
|
-
|
|
79114
|
+
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)}`);
|
|
79088
79115
|
}
|
|
79089
79116
|
const fallbackChannelId = frame.header.channelId;
|
|
79090
79117
|
const fallbackChannel = fallbackChannelId === 250 ? 0 : Math.max(0, fallbackChannelId - 1);
|
|
@@ -79102,7 +79129,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79102
79129
|
const timeStampRaw2 = (getXmlText(alarmXml, "timeStamp") ?? getXmlText(alarmXml, "timestamp") ?? "").trim();
|
|
79103
79130
|
const timeStampNum2 = timeStampRaw2.length ? Number(timeStampRaw2) : void 0;
|
|
79104
79131
|
const alarmTimeStamp2 = Number.isFinite(timeStampNum2) ? timeStampNum2 : void 0;
|
|
79105
|
-
if (this.debugCfg.traceEvents)
|
|
79132
|
+
if (this.debugCfg.traceEvents) require_chunk_ZBACPERX.eventTraceLog(this.debugCfg, this.logger, eventRawLogTag, `AlarmEvent channel=${channel} status=${JSON.stringify(status2)} aiType=${JSON.stringify(aiTypeRaw2)}`);
|
|
79106
79133
|
const statusTokens2 = status2.toLowerCase().split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
79107
79134
|
const visitorActive2 = statusTokens2.includes("visitor");
|
|
79108
79135
|
const dayNightActive2 = statusTokens2.includes("dn") || statusTokens2.includes("daynight");
|
|
@@ -79376,15 +79403,15 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79376
79403
|
const extBuf = Buffer.from(extXml, "utf8");
|
|
79377
79404
|
const payloadBuf = Buffer.from(payloadXml, "utf8");
|
|
79378
79405
|
if (enc.kind === "none") return Buffer.concat([extBuf, payloadBuf]);
|
|
79379
|
-
if (enc.kind === "bc") return Buffer.concat([
|
|
79380
|
-
if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([
|
|
79406
|
+
if (enc.kind === "bc") return Buffer.concat([require_chunk_ZBACPERX.bcEncrypt(extBuf, channelId), require_chunk_ZBACPERX.bcEncrypt(payloadBuf, channelId)]);
|
|
79407
|
+
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)]);
|
|
79381
79408
|
return Buffer.concat([extBuf, payloadBuf]);
|
|
79382
79409
|
}
|
|
79383
79410
|
encodeBodyBinary(extXml, payload, channelId, enc) {
|
|
79384
79411
|
const extBuf = Buffer.from(extXml, "utf8");
|
|
79385
79412
|
if (enc.kind === "none") return Buffer.concat([extBuf, payload]);
|
|
79386
|
-
if (enc.kind === "bc") return Buffer.concat([
|
|
79387
|
-
if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([
|
|
79413
|
+
if (enc.kind === "bc") return Buffer.concat([require_chunk_ZBACPERX.bcEncrypt(extBuf, channelId), payload]);
|
|
79414
|
+
if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_ZBACPERX.aesEncrypt(extBuf, enc.key), payload]);
|
|
79388
79415
|
return Buffer.concat([extBuf, payload]);
|
|
79389
79416
|
}
|
|
79390
79417
|
/**
|
|
@@ -79429,7 +79456,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79429
79456
|
bodyLen,
|
|
79430
79457
|
binaryPayload: true
|
|
79431
79458
|
});
|
|
79432
|
-
if (this.debugCfg.traceTalk && isTalkCmd(cmdId))
|
|
79459
|
+
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}`);
|
|
79433
79460
|
this.recordTx({
|
|
79434
79461
|
cmdId,
|
|
79435
79462
|
responseCode: 0,
|
|
@@ -79446,8 +79473,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79446
79473
|
let dec;
|
|
79447
79474
|
try {
|
|
79448
79475
|
if (enc.kind === "none") dec = buf;
|
|
79449
|
-
else if (enc.kind === "bc") dec =
|
|
79450
|
-
else dec =
|
|
79476
|
+
else if (enc.kind === "bc") dec = require_chunk_ZBACPERX.bcDecrypt(buf, channelId);
|
|
79477
|
+
else dec = require_chunk_ZBACPERX.aesDecrypt(buf, enc.key);
|
|
79451
79478
|
} catch {
|
|
79452
79479
|
return;
|
|
79453
79480
|
}
|
|
@@ -79513,8 +79540,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79513
79540
|
messageClass,
|
|
79514
79541
|
bodyLen
|
|
79515
79542
|
});
|
|
79516
|
-
if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4))
|
|
79517
|
-
if (this.debugCfg.traceTalk && isTalkCmd(cmdId))
|
|
79543
|
+
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}`);
|
|
79544
|
+
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}`);
|
|
79518
79545
|
this.recordTx({
|
|
79519
79546
|
cmdId,
|
|
79520
79547
|
responseCode: 0,
|
|
@@ -79529,8 +79556,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79529
79556
|
responseCode: frame.header.responseCode,
|
|
79530
79557
|
msgNum: frame.header.msgNum
|
|
79531
79558
|
});
|
|
79532
|
-
if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4))
|
|
79533
|
-
if (this.debugCfg.traceTalk && isTalkCmd(cmdId))
|
|
79559
|
+
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}`);
|
|
79560
|
+
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}`);
|
|
79534
79561
|
if (frame.header.responseCode === 400) {
|
|
79535
79562
|
if (frame.body.length === 0) throw new Error("Baichuan authentication failed (responseCode 400, empty body) - check username/password");
|
|
79536
79563
|
}
|
|
@@ -79600,9 +79627,9 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79600
79627
|
messageClass,
|
|
79601
79628
|
bodyLen
|
|
79602
79629
|
});
|
|
79603
|
-
if (_BaichuanClient.recordingCmdIds.has(cmdId))
|
|
79604
|
-
if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4))
|
|
79605
|
-
if (this.debugCfg.traceTalk && isTalkCmd(cmdId))
|
|
79630
|
+
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}`);
|
|
79631
|
+
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}`);
|
|
79632
|
+
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}`);
|
|
79606
79633
|
this.recordTx({
|
|
79607
79634
|
cmdId,
|
|
79608
79635
|
responseCode: 0,
|
|
@@ -79617,9 +79644,9 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
79617
79644
|
responseCode: frame.header.responseCode,
|
|
79618
79645
|
msgNum: frame.header.msgNum
|
|
79619
79646
|
});
|
|
79620
|
-
if (_BaichuanClient.recordingCmdIds.has(frame.header.cmdId))
|
|
79621
|
-
if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4))
|
|
79622
|
-
if (this.debugCfg.traceTalk && isTalkCmd(cmdId))
|
|
79647
|
+
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}`);
|
|
79648
|
+
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}`);
|
|
79649
|
+
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}`);
|
|
79623
79650
|
if (!internal) this.kickIdleDisconnectTimer();
|
|
79624
79651
|
return frame;
|
|
79625
79652
|
}
|
|
@@ -80441,8 +80468,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
80441
80468
|
const tryAs = (enc) => {
|
|
80442
80469
|
try {
|
|
80443
80470
|
if (enc.kind === "none") return buf;
|
|
80444
|
-
if (enc.kind === "bc") return
|
|
80445
|
-
if (enc.kind === "aes" || enc.kind === "full_aes") return
|
|
80471
|
+
if (enc.kind === "bc") return require_chunk_ZBACPERX.bcDecrypt(buf, channelId);
|
|
80472
|
+
if (enc.kind === "aes" || enc.kind === "full_aes") return require_chunk_ZBACPERX.aesDecrypt(buf, enc.key);
|
|
80446
80473
|
return null;
|
|
80447
80474
|
} catch {
|
|
80448
80475
|
return null;
|
|
@@ -80472,7 +80499,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
80472
80499
|
streamType: 0,
|
|
80473
80500
|
msgNum,
|
|
80474
80501
|
responseCode: encByte,
|
|
80475
|
-
messageClass:
|
|
80502
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_LEGACY
|
|
80476
80503
|
});
|
|
80477
80504
|
const pendingKey = `${cmdId}:${msgNum}`;
|
|
80478
80505
|
const framePromise = new Promise((resolve, reject) => {
|
|
@@ -80504,15 +80531,15 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
80504
80531
|
else if (encType === 1) this.enc = { kind: "bc" };
|
|
80505
80532
|
else if (encType === 2) this.enc = {
|
|
80506
80533
|
kind: "aes",
|
|
80507
|
-
key:
|
|
80534
|
+
key: require_chunk_ZBACPERX.deriveAesKey(nonce, this.opts.password)
|
|
80508
80535
|
};
|
|
80509
80536
|
else if (encType === 18) this.enc = {
|
|
80510
80537
|
kind: "full_aes",
|
|
80511
|
-
key:
|
|
80538
|
+
key: require_chunk_ZBACPERX.deriveAesKey(nonce, this.opts.password)
|
|
80512
80539
|
};
|
|
80513
80540
|
else throw new Error(`Baichuan login: unknown encType=0x${encType.toString(16)}`);
|
|
80514
|
-
const userHash =
|
|
80515
|
-
const passHash =
|
|
80541
|
+
const userHash = require_chunk_ZBACPERX.md5StrModern(`${this.opts.username}${nonce}`);
|
|
80542
|
+
const passHash = require_chunk_ZBACPERX.md5StrModern(`${this.opts.password}${nonce}`);
|
|
80516
80543
|
const loginXml = buildLoginXml(userHash, passHash);
|
|
80517
80544
|
this.logDebug("login_hash", {
|
|
80518
80545
|
username: this.opts.username,
|
|
@@ -80527,7 +80554,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
80527
80554
|
cmdId: 1,
|
|
80528
80555
|
payloadXml: loginXml,
|
|
80529
80556
|
extensionXml: "",
|
|
80530
|
-
messageClass:
|
|
80557
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
80531
80558
|
channelIdOverride: effectiveHostChannelId,
|
|
80532
80559
|
encryption: loginEnc,
|
|
80533
80560
|
timeoutMs: 1e4
|
|
@@ -80587,7 +80614,7 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
80587
80614
|
client = api.client;
|
|
80588
80615
|
}
|
|
80589
80616
|
}
|
|
80590
|
-
const videoStream = new
|
|
80617
|
+
const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
|
|
80591
80618
|
client,
|
|
80592
80619
|
api,
|
|
80593
80620
|
channel,
|
|
@@ -81105,7 +81132,7 @@ var H264Flow = class extends BaseFlow {
|
|
|
81105
81132
|
super(transport, "H264");
|
|
81106
81133
|
}
|
|
81107
81134
|
extractParameterSets(accessUnitAnnexB) {
|
|
81108
|
-
const nals =
|
|
81135
|
+
const nals = require_chunk_ZBACPERX.splitAnnexBToNalPayloads(accessUnitAnnexB);
|
|
81109
81136
|
for (const nal of nals) {
|
|
81110
81137
|
if (nal.length < 1) continue;
|
|
81111
81138
|
const nalType = (nal[0] ?? 0) & 31;
|
|
@@ -81150,9 +81177,9 @@ var H265Flow = class extends BaseFlow {
|
|
|
81150
81177
|
super(transport, "H265");
|
|
81151
81178
|
}
|
|
81152
81179
|
extractParameterSets(accessUnitAnnexB) {
|
|
81153
|
-
const vps =
|
|
81154
|
-
const sps =
|
|
81155
|
-
const pps =
|
|
81180
|
+
const vps = require_chunk_ZBACPERX.extractVpsFromAnnexB(accessUnitAnnexB);
|
|
81181
|
+
const sps = require_chunk_ZBACPERX.extractSpsFromAnnexB(accessUnitAnnexB);
|
|
81182
|
+
const pps = require_chunk_ZBACPERX.extractPpsFromAnnexB(accessUnitAnnexB);
|
|
81156
81183
|
if (vps && !this.vps) this.vps = vps;
|
|
81157
81184
|
if (sps && !this.sps) this.sps = sps;
|
|
81158
81185
|
if (pps && !this.pps) this.pps = pps;
|
|
@@ -81536,8 +81563,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
|
|
|
81536
81563
|
/** Returns true if the raw (packed/Annex B) frame is an IDR (H.264) or IRAP (H.265). */
|
|
81537
81564
|
isRawFrameKeyframe(frame) {
|
|
81538
81565
|
try {
|
|
81539
|
-
if (frame.videoType === "H264") return _BaichuanRtspServer.splitAnnexBNals(
|
|
81540
|
-
if (frame.videoType === "H265") return
|
|
81566
|
+
if (frame.videoType === "H264") return _BaichuanRtspServer.splitAnnexBNals(require_chunk_ZBACPERX.convertToAnnexB(frame.data)).some((n) => n.length >= 1 && (n[0] & 31) === 5);
|
|
81567
|
+
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));
|
|
81541
81568
|
} catch {}
|
|
81542
81569
|
return false;
|
|
81543
81570
|
}
|
|
@@ -82437,13 +82464,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
|
|
|
82437
82464
|
if (resources?.rtpAudioTimestamp !== void 0) resources.rtpAudioTimestamp = resources.rtpAudioTimestamp + 1024 >>> 0;
|
|
82438
82465
|
};
|
|
82439
82466
|
const isH265IrapAccessUnit = (annexB) => {
|
|
82440
|
-
const nals =
|
|
82467
|
+
const nals = require_chunk_ZBACPERX.splitAnnexBToNalPayloads2(annexB);
|
|
82441
82468
|
for (const nal of nals) {
|
|
82442
82469
|
if (nal.length < 2) continue;
|
|
82443
82470
|
const b0 = nal[0];
|
|
82444
82471
|
if (b0 === void 0) continue;
|
|
82445
82472
|
if ((b0 & 128) !== 0) continue;
|
|
82446
|
-
if (
|
|
82473
|
+
if (require_chunk_ZBACPERX.isH265Irap(b0 >> 1 & 63)) return true;
|
|
82447
82474
|
}
|
|
82448
82475
|
return false;
|
|
82449
82476
|
};
|
|
@@ -82599,7 +82626,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
|
|
|
82599
82626
|
continue;
|
|
82600
82627
|
}
|
|
82601
82628
|
if (frame.videoType === "H264" || frame.videoType === "H265") {
|
|
82602
|
-
const normalizedVideoData = frame.videoType === "H264" ?
|
|
82629
|
+
const normalizedVideoData = frame.videoType === "H264" ? require_chunk_ZBACPERX.convertToAnnexB(frame.data) : require_chunk_ZBACPERX.convertToAnnexB2(frame.data);
|
|
82603
82630
|
if (frameCount === 0) this.setFlowVideoType(frame.videoType, "first video frame");
|
|
82604
82631
|
if (!this.flow.getFmtp().hasParamSets) {
|
|
82605
82632
|
this.flow.extractParameterSets(normalizedVideoData);
|
|
@@ -82615,7 +82642,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
|
|
|
82615
82642
|
}
|
|
82616
82643
|
if (useDirectRtp) {
|
|
82617
82644
|
const videoType = frame.videoType ?? this.flow.videoType;
|
|
82618
|
-
const normalizedVideoData = videoType === "H264" ?
|
|
82645
|
+
const normalizedVideoData = videoType === "H264" ? require_chunk_ZBACPERX.convertToAnnexB(frame.data) : require_chunk_ZBACPERX.convertToAnnexB2(frame.data);
|
|
82619
82646
|
if (!resources?.seenFirstVideoKeyframe) if (videoType === "H265") {
|
|
82620
82647
|
const { hasParamSets } = this.flow.getFmtp();
|
|
82621
82648
|
if (!hasParamSets) {
|
|
@@ -82691,7 +82718,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
|
|
|
82691
82718
|
const headHex = frame.data.subarray(0, 16).toString("hex");
|
|
82692
82719
|
this.rtspDebugLog(`First video frame written to ffmpeg stdin for client ${clientId} (len=${frame.data.length}, head=${headHex})`);
|
|
82693
82720
|
}
|
|
82694
|
-
if (!stdin.write(frame.videoType === "H264" ?
|
|
82721
|
+
if (!stdin.write(frame.videoType === "H264" ? require_chunk_ZBACPERX.convertToAnnexB(frame.data) : frame.data)) await new Promise((resolve) => {
|
|
82695
82722
|
if (stdin) stdin.once("drain", () => resolve());
|
|
82696
82723
|
else resolve();
|
|
82697
82724
|
});
|
|
@@ -82803,7 +82830,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
|
|
|
82803
82830
|
ensureContinuousStream(dedicatedClient) {
|
|
82804
82831
|
if (this.continuousStream) return this.continuousStream;
|
|
82805
82832
|
const createLiveStream = async () => {
|
|
82806
|
-
return new
|
|
82833
|
+
return new require_chunk_ZBACPERX.BaichuanVideoStream({
|
|
82807
82834
|
client: dedicatedClient ?? this.api.client,
|
|
82808
82835
|
api: this.api,
|
|
82809
82836
|
channel: this.channel,
|
|
@@ -83951,6 +83978,128 @@ function patchMotionSensitivityListXml(currentXml, bands) {
|
|
|
83951
83978
|
const fragment = encodeMotionSensitivityListXml(bands);
|
|
83952
83979
|
return currentXml.replace(/<sensitivityInfoList[\s\S]*?(?:\/>|<\/sensitivityInfoList>)/, fragment);
|
|
83953
83980
|
}
|
|
83981
|
+
var ADTS_SAMPLES_PER_FRAME = 1024;
|
|
83982
|
+
var ADTS_SAMPLE_RATES = [
|
|
83983
|
+
96e3,
|
|
83984
|
+
88200,
|
|
83985
|
+
64e3,
|
|
83986
|
+
48e3,
|
|
83987
|
+
44100,
|
|
83988
|
+
32e3,
|
|
83989
|
+
24e3,
|
|
83990
|
+
22050,
|
|
83991
|
+
16e3,
|
|
83992
|
+
12e3,
|
|
83993
|
+
11025,
|
|
83994
|
+
8e3,
|
|
83995
|
+
7350,
|
|
83996
|
+
null,
|
|
83997
|
+
null,
|
|
83998
|
+
null
|
|
83999
|
+
];
|
|
84000
|
+
var ADTS_PROFILES = [
|
|
84001
|
+
"Main",
|
|
84002
|
+
"LC",
|
|
84003
|
+
"SSR",
|
|
84004
|
+
"LTP"
|
|
84005
|
+
];
|
|
84006
|
+
var parseAdtsHeader = (frame) => {
|
|
84007
|
+
if (frame.length < 7) return null;
|
|
84008
|
+
const b0 = frame[0];
|
|
84009
|
+
const b1 = frame[1];
|
|
84010
|
+
if (b0 !== 255 || (b1 & 240) !== 240) return null;
|
|
84011
|
+
const b2 = frame[2];
|
|
84012
|
+
const b3 = frame[3];
|
|
84013
|
+
const b4 = frame[4];
|
|
84014
|
+
const b5 = frame[5];
|
|
84015
|
+
const profile = b2 >> 6 & 3;
|
|
84016
|
+
const sampleRateIndex = b2 >> 2 & 15;
|
|
84017
|
+
const channelConfig = (b2 & 1) << 2 | b3 >> 6 & 3;
|
|
84018
|
+
const frameLength = (b3 & 3) << 11 | b4 << 3 | b5 >> 5 & 7;
|
|
84019
|
+
if (frameLength < 7) return null;
|
|
84020
|
+
return {
|
|
84021
|
+
profile,
|
|
84022
|
+
profileName: ADTS_PROFILES[profile] ?? "unknown",
|
|
84023
|
+
sampleRate: ADTS_SAMPLE_RATES[sampleRateIndex] ?? null,
|
|
84024
|
+
channels: channelConfig > 0 ? channelConfig : null,
|
|
84025
|
+
frameLength
|
|
84026
|
+
};
|
|
84027
|
+
};
|
|
84028
|
+
var buildRecordingAudioTrack = (params) => {
|
|
84029
|
+
const { codec, frames } = params;
|
|
84030
|
+
if (codec == null || frames.length === 0) return null;
|
|
84031
|
+
const data = frames.length === 1 ? frames[0] : Buffer.concat([...frames]);
|
|
84032
|
+
if (codec === "Adpcm") return {
|
|
84033
|
+
codec,
|
|
84034
|
+
format: "adpcm",
|
|
84035
|
+
data,
|
|
84036
|
+
frames: frames.length,
|
|
84037
|
+
bytes: data.length,
|
|
84038
|
+
sampleRate: null,
|
|
84039
|
+
channels: null,
|
|
84040
|
+
durationSeconds: null
|
|
84041
|
+
};
|
|
84042
|
+
const head = parseAdtsHeader(frames[0]);
|
|
84043
|
+
if (head == null) return {
|
|
84044
|
+
codec,
|
|
84045
|
+
format: "unknown",
|
|
84046
|
+
data,
|
|
84047
|
+
frames: frames.length,
|
|
84048
|
+
bytes: data.length,
|
|
84049
|
+
sampleRate: null,
|
|
84050
|
+
channels: null,
|
|
84051
|
+
durationSeconds: null
|
|
84052
|
+
};
|
|
84053
|
+
const durationSeconds = head.sampleRate != null && head.sampleRate > 0 ? frames.length * ADTS_SAMPLES_PER_FRAME / head.sampleRate : null;
|
|
84054
|
+
return {
|
|
84055
|
+
codec,
|
|
84056
|
+
format: "adts",
|
|
84057
|
+
data,
|
|
84058
|
+
frames: frames.length,
|
|
84059
|
+
bytes: data.length,
|
|
84060
|
+
sampleRate: head.sampleRate,
|
|
84061
|
+
channels: head.channels,
|
|
84062
|
+
durationSeconds
|
|
84063
|
+
};
|
|
84064
|
+
};
|
|
84065
|
+
var roundRecordingFps = (fps) => {
|
|
84066
|
+
if (fps > 14 && fps < 16) return 15;
|
|
84067
|
+
if (fps > 23 && fps < 26) return 25;
|
|
84068
|
+
if (fps > 29 && fps < 31) return 30;
|
|
84069
|
+
return Math.round(fps * 100) / 100;
|
|
84070
|
+
};
|
|
84071
|
+
var estimateVideoTiming = (params) => {
|
|
84072
|
+
const { timestampsUs } = params;
|
|
84073
|
+
const infoFps = params.infoFps != null && params.infoFps > 0 ? params.infoFps : null;
|
|
84074
|
+
const accessUnits = timestampsUs.length;
|
|
84075
|
+
if (accessUnits >= 2) {
|
|
84076
|
+
const spanUs = timestampsUs[accessUnits - 1] - timestampsUs[0];
|
|
84077
|
+
if (spanUs > 0) {
|
|
84078
|
+
const durationSeconds = spanUs / 1e6;
|
|
84079
|
+
return {
|
|
84080
|
+
accessUnits,
|
|
84081
|
+
fps: roundRecordingFps((accessUnits - 1) / durationSeconds),
|
|
84082
|
+
durationSeconds,
|
|
84083
|
+
infoFps,
|
|
84084
|
+
fpsSource: "timestamps"
|
|
84085
|
+
};
|
|
84086
|
+
}
|
|
84087
|
+
}
|
|
84088
|
+
if (infoFps != null) return {
|
|
84089
|
+
accessUnits,
|
|
84090
|
+
fps: infoFps,
|
|
84091
|
+
durationSeconds: accessUnits > 0 ? accessUnits / infoFps : null,
|
|
84092
|
+
infoFps,
|
|
84093
|
+
fpsSource: "infoFps"
|
|
84094
|
+
};
|
|
84095
|
+
return {
|
|
84096
|
+
accessUnits,
|
|
84097
|
+
fps: null,
|
|
84098
|
+
durationSeconds: null,
|
|
84099
|
+
infoFps,
|
|
84100
|
+
fpsSource: "unknown"
|
|
84101
|
+
};
|
|
84102
|
+
};
|
|
83954
84103
|
var buildDetectionClasses = (parsed, recordType) => {
|
|
83955
84104
|
const classes = [];
|
|
83956
84105
|
const flags = parsed?.flags;
|
|
@@ -86460,7 +86609,7 @@ var sendTalkConfigWithReset = async (params) => {
|
|
|
86460
86609
|
channel: params.channel,
|
|
86461
86610
|
...params.channelIdOverride != null ? { channelIdOverride: params.channelIdOverride } : {},
|
|
86462
86611
|
payloadXml: params.payloadXml,
|
|
86463
|
-
messageClass:
|
|
86612
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
86464
86613
|
})).header.responseCode;
|
|
86465
86614
|
};
|
|
86466
86615
|
const code = await trySend();
|
|
@@ -86470,7 +86619,7 @@ var sendTalkConfigWithReset = async (params) => {
|
|
|
86470
86619
|
channel: params.channel,
|
|
86471
86620
|
...params.channelIdOverride != null ? { channelIdOverride: params.channelIdOverride } : {},
|
|
86472
86621
|
payloadXml: "",
|
|
86473
|
-
messageClass:
|
|
86622
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
86474
86623
|
});
|
|
86475
86624
|
const retryCode = await trySend();
|
|
86476
86625
|
if (retryCode !== 200) throw new Error(`TalkConfig rejected after reset (responseCode ${retryCode})`);
|
|
@@ -86537,7 +86686,7 @@ var createBufferedTalkSession = (params) => {
|
|
|
86537
86686
|
...channelIdOverride != null ? { channelIdOverride } : {},
|
|
86538
86687
|
extensionXml: buildBinaryExtensionXml(channel),
|
|
86539
86688
|
payload: Buffer.concat(parts),
|
|
86540
|
-
messageClass:
|
|
86689
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
86541
86690
|
});
|
|
86542
86691
|
const playLengthMs = samplesSent / info.audioConfig.sampleRate * 1e3;
|
|
86543
86692
|
const now = Date.now();
|
|
@@ -86583,7 +86732,7 @@ var createBufferedTalkSession = (params) => {
|
|
|
86583
86732
|
channel,
|
|
86584
86733
|
...channelIdOverride != null ? { channelIdOverride } : {},
|
|
86585
86734
|
payloadXml: "",
|
|
86586
|
-
messageClass:
|
|
86735
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
86587
86736
|
});
|
|
86588
86737
|
if (frame.header.responseCode !== 200) throw new Error(`TalkReset rejected (responseCode ${frame.header.responseCode})`);
|
|
86589
86738
|
if (params.closeSocketOnStop) try {
|
|
@@ -88266,7 +88415,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
88266
88415
|
if (debugCfg) {
|
|
88267
88416
|
const sid = this.client.getSocketSessionId?.();
|
|
88268
88417
|
const tag2 = sid ? `ReolinkSimpleEvent sid=${sid}` : "ReolinkSimpleEvent";
|
|
88269
|
-
|
|
88418
|
+
require_chunk_ZBACPERX.eventTraceLog(debugCfg, this.logger, tag2, `dispatch type=${evt.type} channel=${evt.channel} timestamp=${evt.timestamp}`);
|
|
88270
88419
|
}
|
|
88271
88420
|
for (const cb of this.simpleEventListeners) try {
|
|
88272
88421
|
Promise.resolve(cb(evt)).catch((e) => {
|
|
@@ -88277,7 +88426,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
88277
88426
|
}
|
|
88278
88427
|
}
|
|
88279
88428
|
constructor(opts) {
|
|
88280
|
-
const dbg =
|
|
88429
|
+
const dbg = require_chunk_ZBACPERX.normalizeDebugOptions(opts.debugOptions);
|
|
88281
88430
|
this.logger = createDebugGateLogger(opts.logger, dbg.general || dbg.traceNativeStream || dbg.traceRecordings || dbg.traceTalk || dbg.traceEvents || dbg.debugRtsp);
|
|
88282
88431
|
this.clientOptions = {
|
|
88283
88432
|
host: opts.host,
|
|
@@ -88865,7 +89014,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
88865
89014
|
return;
|
|
88866
89015
|
}
|
|
88867
89016
|
entry.startInFlight = (async () => {
|
|
88868
|
-
const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => require("./BaichuanVideoStream-
|
|
89017
|
+
const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => require("./BaichuanVideoStream-FOE2DBVZ-DJcEkVak.js"));
|
|
88869
89018
|
const sessionKey = `live:object-detections:ch${entry.channel}:${entry.profile}`;
|
|
88870
89019
|
const dedicated = await this.createDedicatedSession(sessionKey);
|
|
88871
89020
|
const stream$12 = new BaichuanVideoStream2({
|
|
@@ -89346,7 +89495,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
89346
89495
|
channel: ch,
|
|
89347
89496
|
...channelIdOverride != null ? { channelIdOverride } : {},
|
|
89348
89497
|
payloadXml: "",
|
|
89349
|
-
messageClass:
|
|
89498
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
89350
89499
|
});
|
|
89351
89500
|
if (frame.header.responseCode !== 200) throw new Error(`TalkReset rejected (responseCode ${frame.header.responseCode})`);
|
|
89352
89501
|
}
|
|
@@ -89365,7 +89514,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
89365
89514
|
channel: ch,
|
|
89366
89515
|
...channelIdOverride != null ? { channelIdOverride } : {},
|
|
89367
89516
|
payloadXml,
|
|
89368
|
-
messageClass:
|
|
89517
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
89369
89518
|
})).header.responseCode;
|
|
89370
89519
|
};
|
|
89371
89520
|
const code = await trySend();
|
|
@@ -89517,7 +89666,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
89517
89666
|
cmdId: 10,
|
|
89518
89667
|
channel,
|
|
89519
89668
|
payloadXml: "",
|
|
89520
|
-
messageClass:
|
|
89669
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
89521
89670
|
});
|
|
89522
89671
|
return parseTalkAbilityXml(frame.body.length === 0 ? "" : client.tryDecryptXml(frame.body, frame.header.channelId, client.enc));
|
|
89523
89672
|
}
|
|
@@ -90590,8 +90739,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90590
90739
|
const start = params.start;
|
|
90591
90740
|
const endOfStartDay = endOfWallClockDay(start, timeZone);
|
|
90592
90741
|
const end = params.end.getTime() > endOfStartDay.getTime() ? endOfStartDay : params.end;
|
|
90593
|
-
|
|
90594
|
-
|
|
90742
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclips", `Query: start=${start.toISOString()}, end=${end.toISOString()} (forced same day)`);
|
|
90743
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclips", `Using UID for channel ${channel}: ${uid}`);
|
|
90595
90744
|
const streamType = params.streamType ?? "subStream";
|
|
90596
90745
|
const recordType = params.recordType ?? "manual,sched,io,md,people,face,vehicle,dog_cat,visitor,other,package,crossline,intrusion,loitering,legacy,loss";
|
|
90597
90746
|
const fileRecordTypes = params.fileRecordTypes ?? (params.recordType === void 0 ? DEFAULT_RECORDING_SEARCH_FILE_RECORD_TYPES : void 0);
|
|
@@ -90610,7 +90759,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90610
90759
|
...timeZone !== void 0 ? { timeZone } : {}
|
|
90611
90760
|
});
|
|
90612
90761
|
const unique = dedupeRecordingFiles(files);
|
|
90613
|
-
|
|
90762
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclips", `FileInfoList complete: ${unique.length} unique files (from ${files.length} total)`);
|
|
90614
90763
|
return unique;
|
|
90615
90764
|
});
|
|
90616
90765
|
}
|
|
@@ -90651,7 +90800,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90651
90800
|
const start = params.start;
|
|
90652
90801
|
const endOfStartDay = endOfWallClockDay(start, timeZone);
|
|
90653
90802
|
const end = params.end.getTime() > endOfStartDay.getTime() ? endOfStartDay : params.end;
|
|
90654
|
-
if (end !== params.end)
|
|
90803
|
+
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)`);
|
|
90655
90804
|
const windows = await searchAlarmVideosViaFindAlarmVideo({
|
|
90656
90805
|
sendXml: (p) => this.sendXml(p),
|
|
90657
90806
|
channel,
|
|
@@ -90667,7 +90816,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90667
90816
|
maxPages: params.maxPages ?? 200,
|
|
90668
90817
|
...params.timeoutMs != null ? { timeoutMs: params.timeoutMs } : {}
|
|
90669
90818
|
});
|
|
90670
|
-
|
|
90819
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "searchAlarmVideos", `findAlarmVideo complete: ${windows.length} windows over ${new Set(windows.map((w) => w.fileName)).size} files (channel=${channel})`);
|
|
90671
90820
|
return windows;
|
|
90672
90821
|
});
|
|
90673
90822
|
}
|
|
@@ -90732,7 +90881,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90732
90881
|
seen.add(key);
|
|
90733
90882
|
return true;
|
|
90734
90883
|
});
|
|
90735
|
-
|
|
90884
|
+
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`);
|
|
90736
90885
|
return unique;
|
|
90737
90886
|
});
|
|
90738
90887
|
}
|
|
@@ -90785,7 +90934,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90785
90934
|
});
|
|
90786
90935
|
const msgNum = 0;
|
|
90787
90936
|
dedicatedClient.subscribeVideoStream(5, msgNum);
|
|
90788
|
-
const stream$14 = new
|
|
90937
|
+
const stream$14 = new require_chunk_ZBACPERX.BaichuanVideoStream({
|
|
90789
90938
|
client: dedicatedClient,
|
|
90790
90939
|
channel,
|
|
90791
90940
|
profile: streamType === "subStream" ? "sub" : "main",
|
|
@@ -90804,7 +90953,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90804
90953
|
channel,
|
|
90805
90954
|
channelIdOverride: sessionCounter,
|
|
90806
90955
|
payloadXml,
|
|
90807
|
-
messageClass:
|
|
90956
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
90808
90957
|
msgNumOverride: 0,
|
|
90809
90958
|
timeoutMs: params.timeoutMs,
|
|
90810
90959
|
internal: true
|
|
@@ -90836,7 +90985,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90836
90985
|
cmdId: 7,
|
|
90837
90986
|
channel,
|
|
90838
90987
|
payloadXml: stopXml,
|
|
90839
|
-
messageClass:
|
|
90988
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
90840
90989
|
timeoutMs: 2e3,
|
|
90841
90990
|
internal: true
|
|
90842
90991
|
});
|
|
@@ -90893,7 +91042,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90893
91042
|
});
|
|
90894
91043
|
const msgNum = 0;
|
|
90895
91044
|
dedicatedClient.subscribeVideoStream(5, msgNum);
|
|
90896
|
-
const stream$15 = new
|
|
91045
|
+
const stream$15 = new require_chunk_ZBACPERX.BaichuanVideoStream({
|
|
90897
91046
|
client: dedicatedClient,
|
|
90898
91047
|
channel,
|
|
90899
91048
|
profile: streamType === "subStream" ? "sub" : "main",
|
|
@@ -90913,7 +91062,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90913
91062
|
channelIdOverride,
|
|
90914
91063
|
payloadXml,
|
|
90915
91064
|
extensionXml: "",
|
|
90916
|
-
messageClass:
|
|
91065
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
90917
91066
|
msgNumOverride: msgNum,
|
|
90918
91067
|
timeoutMs: params.timeoutMs,
|
|
90919
91068
|
internal: true
|
|
@@ -90945,7 +91094,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
90945
91094
|
cmdId: 7,
|
|
90946
91095
|
channel,
|
|
90947
91096
|
payloadXml: stopXml,
|
|
90948
|
-
messageClass:
|
|
91097
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
90949
91098
|
timeoutMs: 2e3,
|
|
90950
91099
|
internal: true
|
|
90951
91100
|
});
|
|
@@ -91032,35 +91181,35 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
91032
91181
|
async ensureUidForRecordings(channel, explicitUid) {
|
|
91033
91182
|
const dbg = this.client.getDebugConfig?.();
|
|
91034
91183
|
const logger = this.logger;
|
|
91035
|
-
|
|
91184
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Checking UID: explicitUid=${explicitUid}, this.uid=${this.uid}`);
|
|
91036
91185
|
const trimmedExplicit = explicitUid?.trim();
|
|
91037
91186
|
if (trimmedExplicit) {
|
|
91038
|
-
|
|
91187
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using explicit UID: ${trimmedExplicit}`);
|
|
91039
91188
|
return trimmedExplicit;
|
|
91040
91189
|
}
|
|
91041
91190
|
const perChannel = await this.discoverUidForRecordingsForChannel(channel);
|
|
91042
91191
|
if (perChannel) {
|
|
91043
|
-
|
|
91192
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID: ${perChannel}`);
|
|
91044
91193
|
return perChannel;
|
|
91045
91194
|
}
|
|
91046
91195
|
const configured = this.uid?.trim();
|
|
91047
91196
|
if (configured) {
|
|
91048
|
-
|
|
91197
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using configured UID: ${configured}`);
|
|
91049
91198
|
return configured;
|
|
91050
91199
|
}
|
|
91051
91200
|
if (this.nativeOnly) {
|
|
91052
91201
|
const viaGetUid = await this.discoverUidViaGetUidNative(channel);
|
|
91053
91202
|
if (viaGetUid) {
|
|
91054
|
-
|
|
91203
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using device UID from cmd 114 GetUid: ${viaGetUid}`);
|
|
91055
91204
|
return viaGetUid;
|
|
91056
91205
|
}
|
|
91057
|
-
|
|
91206
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: no UID available (channel=${channel})`);
|
|
91058
91207
|
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.");
|
|
91059
91208
|
}
|
|
91060
|
-
|
|
91209
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `No UID available, attempting device auto-discovery (ch=${channel})`);
|
|
91061
91210
|
const discovered = await this.discoverDeviceUidForRecordings(channel);
|
|
91062
91211
|
if (discovered) return discovered;
|
|
91063
|
-
|
|
91212
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Auto-discovery failed - no UID found`);
|
|
91064
91213
|
throw new Error("UID is required to access recordings. Provide a UID explicitly or configure the client with a UID.");
|
|
91065
91214
|
}
|
|
91066
91215
|
cacheChannelUid(channel, uid) {
|
|
@@ -91083,28 +91232,28 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
91083
91232
|
const logger = this.logger;
|
|
91084
91233
|
const fromPush = this.getUidFromPushCacheForChannel(channel);
|
|
91085
91234
|
if (fromPush) {
|
|
91086
|
-
|
|
91235
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from push cache: ${fromPush}`);
|
|
91087
91236
|
return fromPush;
|
|
91088
91237
|
}
|
|
91089
91238
|
if (this.nativeOnly) {
|
|
91090
|
-
|
|
91239
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: skipping per-channel UID discovery via HTTP/CGI (channel=${channel})`);
|
|
91091
91240
|
return;
|
|
91092
91241
|
}
|
|
91093
91242
|
try {
|
|
91094
|
-
|
|
91243
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Attempting per-channel UID discovery via HTTP CGI GetChannelstatus (channel=${channel})`);
|
|
91095
91244
|
const uidCandidate = await discoverPerChannelUidViaCgiChannelstatus({
|
|
91096
91245
|
channel,
|
|
91097
91246
|
login: () => this.cgiApi.login(),
|
|
91098
91247
|
getChannelstatus: () => this.cgiApi.GetChannelstatus()
|
|
91099
91248
|
});
|
|
91100
|
-
|
|
91249
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus channel=${channel} uid=${uidCandidate || "(missing)"}`);
|
|
91101
91250
|
if (uidCandidate) {
|
|
91102
91251
|
this.cacheChannelUid(channel, uidCandidate);
|
|
91103
|
-
|
|
91252
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from GetChannelstatus: ${uidCandidate}`);
|
|
91104
91253
|
return uidCandidate;
|
|
91105
91254
|
}
|
|
91106
91255
|
} catch (e) {
|
|
91107
|
-
|
|
91256
|
+
require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus failed: ${formatErrorForLog(e)}`);
|
|
91108
91257
|
}
|
|
91109
91258
|
}
|
|
91110
91259
|
/**
|
|
@@ -91116,7 +91265,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
91116
91265
|
*/
|
|
91117
91266
|
async discoverUidViaGetUidNative(channel) {
|
|
91118
91267
|
const dbg = this.client.getDebugConfig?.();
|
|
91119
|
-
const trace = (message) =>
|
|
91268
|
+
const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, this.logger, "ensureUidForRecordings", message);
|
|
91120
91269
|
let isNvr = this._isNvr;
|
|
91121
91270
|
if (isNvr === void 0) try {
|
|
91122
91271
|
isNvr = await this.isNvrDevice();
|
|
@@ -91142,7 +91291,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
91142
91291
|
if (this.nativeOnly) return void 0;
|
|
91143
91292
|
const dbg = this.client.getDebugConfig?.();
|
|
91144
91293
|
const logger = this.logger;
|
|
91145
|
-
const trace = (message) =>
|
|
91294
|
+
const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", message);
|
|
91146
91295
|
const discoveredUid = await discoverDeviceUidForRecordings({
|
|
91147
91296
|
channel,
|
|
91148
91297
|
getInfo: () => this.getInfo(),
|
|
@@ -91217,7 +91366,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
91217
91366
|
await this.client.login();
|
|
91218
91367
|
const dbg = this.client.getDebugConfig?.();
|
|
91219
91368
|
const logger = this.logger;
|
|
91220
|
-
const trace = (message) =>
|
|
91369
|
+
const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclipThumbnail", message);
|
|
91221
91370
|
const channel = this.normalizeChannel(params.channel);
|
|
91222
91371
|
const snapStreamType = (params.snapType ?? "sub") === "main" ? "mainStream" : "subStream";
|
|
91223
91372
|
const timeoutMs = params.timeoutMs ?? 3e4;
|
|
@@ -91272,7 +91421,7 @@ ${xml}`);
|
|
|
91272
91421
|
cmdId: 298,
|
|
91273
91422
|
...isNvr && headerChannelIdOverride != null ? { channelIdOverride: headerChannelIdOverride } : {},
|
|
91274
91423
|
msgNumOverride: 0,
|
|
91275
|
-
messageClass:
|
|
91424
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
91276
91425
|
streamType: 0,
|
|
91277
91426
|
payloadXml: xml,
|
|
91278
91427
|
timeoutMs
|
|
@@ -91751,7 +91900,7 @@ ${stderr}`));
|
|
|
91751
91900
|
await this.client.login();
|
|
91752
91901
|
const dbg = this.client.getDebugConfig?.();
|
|
91753
91902
|
const logger = this.logger;
|
|
91754
|
-
const trace = (message) =>
|
|
91903
|
+
const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListDownload", message);
|
|
91755
91904
|
const channel = this.normalizeChannel(params.channel);
|
|
91756
91905
|
const uid = await this.ensureUidForRecordings(channel, params.uid);
|
|
91757
91906
|
const headerChannelIdOverride = this.resolveHeaderChannelIdForLogicalChannel(channel);
|
|
@@ -91797,7 +91946,7 @@ ${stderr}`));
|
|
|
91797
91946
|
cmdId: 13,
|
|
91798
91947
|
channel,
|
|
91799
91948
|
...typeof chId === "number" ? { channelIdOverride: chId } : {},
|
|
91800
|
-
messageClass:
|
|
91949
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_FILE_DOWNLOAD,
|
|
91801
91950
|
extensionXml: buildBinaryExtensionXml(channel),
|
|
91802
91951
|
payloadXml,
|
|
91803
91952
|
streamType: st,
|
|
@@ -91826,7 +91975,7 @@ ${stderr}`));
|
|
|
91826
91975
|
await this.client.login();
|
|
91827
91976
|
const dbg = this.client.getDebugConfig?.();
|
|
91828
91977
|
const logger = this.logger;
|
|
91829
|
-
const trace = (message) =>
|
|
91978
|
+
const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListPagedDownload", message);
|
|
91830
91979
|
const channel = this.normalizeChannel(params.channel);
|
|
91831
91980
|
const uid = await this.ensureUidForRecordings(channel, params.uid);
|
|
91832
91981
|
trace(`Starting paged download: channel=${channel}, uid=${uid}, fileName=${params.fileName}`);
|
|
@@ -91863,7 +92012,7 @@ ${stderr}`));
|
|
|
91863
92012
|
await this.client.login();
|
|
91864
92013
|
const dbg = this.client.getDebugConfig?.();
|
|
91865
92014
|
const logger = this.logger;
|
|
91866
|
-
const trace = (message) =>
|
|
92015
|
+
const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListReplayBinaryDownload", message);
|
|
91867
92016
|
const channel = this.normalizeChannel(params.channel);
|
|
91868
92017
|
const headerChannelIdOverride = this.resolveHeaderChannelIdForLogicalChannel(channel);
|
|
91869
92018
|
const ident = params.fileName;
|
|
@@ -91892,7 +92041,7 @@ ${stderr}`));
|
|
|
91892
92041
|
channel,
|
|
91893
92042
|
...isNvr ? { channelIdOverride: headerChannelIdOverride ?? 82 } : {},
|
|
91894
92043
|
msgNumOverride: 0,
|
|
91895
|
-
messageClass:
|
|
92044
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
91896
92045
|
payloadXml,
|
|
91897
92046
|
streamType: 0,
|
|
91898
92047
|
timeoutMs,
|
|
@@ -91978,25 +92127,46 @@ ${stderr}`));
|
|
|
91978
92127
|
async downloadRecordingDemuxed(params) {
|
|
91979
92128
|
const raw = await this.downloadRecording(params);
|
|
91980
92129
|
const frames = [];
|
|
91981
|
-
const
|
|
92130
|
+
const timestampsUs = [];
|
|
92131
|
+
const audioFrames = [];
|
|
92132
|
+
const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
|
|
91982
92133
|
strict: false,
|
|
91983
92134
|
logger: this.logger,
|
|
91984
|
-
onVideoAccessUnit: ({ annexB }) => {
|
|
92135
|
+
onVideoAccessUnit: ({ annexB, microseconds }) => {
|
|
91985
92136
|
frames.push(annexB);
|
|
92137
|
+
timestampsUs.push(microseconds);
|
|
92138
|
+
},
|
|
92139
|
+
onAudioFrame: ({ data }) => {
|
|
92140
|
+
audioFrames.push(data);
|
|
91986
92141
|
}
|
|
91987
92142
|
});
|
|
91988
92143
|
decoder.push(raw);
|
|
91989
92144
|
const stats = decoder.getStats();
|
|
92145
|
+
const audio = buildRecordingAudioTrack({
|
|
92146
|
+
codec: stats.audioType,
|
|
92147
|
+
frames: audioFrames
|
|
92148
|
+
});
|
|
92149
|
+
const timing = estimateVideoTiming({
|
|
92150
|
+
timestampsUs,
|
|
92151
|
+
infoFps: stats.infos[0]?.fps ?? null
|
|
92152
|
+
});
|
|
92153
|
+
if (audio == null) this.logger?.debug?.(`[downloadRecordingDemuxed] no audio packet in ${stats.packets} BcMedia packets (${stats.bytesIn} bytes) for ${params.fileName}`);
|
|
91990
92154
|
return {
|
|
91991
92155
|
annexB: Buffer.concat(frames),
|
|
91992
92156
|
videoType: stats.videoType,
|
|
92157
|
+
audio,
|
|
91993
92158
|
stats: {
|
|
91994
92159
|
bytesIn: stats.bytesIn,
|
|
91995
92160
|
bytesOut: stats.bytesOut,
|
|
91996
92161
|
packets: stats.packets,
|
|
91997
92162
|
videoPackets: stats.videoPackets,
|
|
91998
92163
|
audioPackets: stats.audioPackets,
|
|
91999
|
-
|
|
92164
|
+
audioBytes: stats.audioBytesOut,
|
|
92165
|
+
keyframes: stats.keyframes,
|
|
92166
|
+
fps: timing.fps,
|
|
92167
|
+
durationSeconds: timing.durationSeconds,
|
|
92168
|
+
infoFps: timing.infoFps,
|
|
92169
|
+
fpsSource: timing.fpsSource
|
|
92000
92170
|
}
|
|
92001
92171
|
};
|
|
92002
92172
|
}
|
|
@@ -92025,7 +92195,7 @@ ${stderr}`));
|
|
|
92025
92195
|
const videoFrames = [];
|
|
92026
92196
|
const audioFrames = [];
|
|
92027
92197
|
let audioCodec = null;
|
|
92028
|
-
const decoder = new
|
|
92198
|
+
const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
|
|
92029
92199
|
strict: false,
|
|
92030
92200
|
logger: this.logger,
|
|
92031
92201
|
onVideoAccessUnit: ({ annexB, microseconds }) => {
|
|
@@ -92042,28 +92212,12 @@ ${stderr}`));
|
|
|
92042
92212
|
decoder.push(raw);
|
|
92043
92213
|
const decoderStats = decoder.getStats();
|
|
92044
92214
|
const videoCodec = decoderStats.videoType ?? "H264";
|
|
92045
|
-
|
|
92046
|
-
|
|
92047
|
-
|
|
92048
|
-
|
|
92049
|
-
|
|
92050
|
-
|
|
92051
|
-
durationSeconds = durationUs / 1e6;
|
|
92052
|
-
fps = (videoFrames.length - 1) / durationSeconds;
|
|
92053
|
-
} else {
|
|
92054
|
-
const infoFps = decoderStats.infos[0]?.fps;
|
|
92055
|
-
fps = infoFps && infoFps > 0 ? infoFps : 15;
|
|
92056
|
-
durationSeconds = videoFrames.length / fps;
|
|
92057
|
-
}
|
|
92058
|
-
} else {
|
|
92059
|
-
const infoFps = decoderStats.infos[0]?.fps;
|
|
92060
|
-
fps = infoFps && infoFps > 0 ? infoFps : 15;
|
|
92061
|
-
durationSeconds = videoFrames.length / fps;
|
|
92062
|
-
}
|
|
92063
|
-
if (fps > 14 && fps < 16) fps = 15;
|
|
92064
|
-
else if (fps > 23 && fps < 26) fps = 25;
|
|
92065
|
-
else if (fps > 29 && fps < 31) fps = 30;
|
|
92066
|
-
else fps = Math.round(fps * 100) / 100;
|
|
92215
|
+
const timing = estimateVideoTiming({
|
|
92216
|
+
timestampsUs: videoFrames.map((f) => f.microseconds),
|
|
92217
|
+
infoFps: decoderStats.infos[0]?.fps ?? null
|
|
92218
|
+
});
|
|
92219
|
+
const fps = timing.fps ?? 15;
|
|
92220
|
+
const durationSeconds = timing.durationSeconds ?? videoFrames.length / fps;
|
|
92067
92221
|
const videoData = Buffer.concat(videoFrames.map((f) => f.annexB));
|
|
92068
92222
|
const audioData = audioFrames.length > 0 ? Buffer.concat(audioFrames) : null;
|
|
92069
92223
|
const hasAudio = audioData != null && audioData.length > 0;
|
|
@@ -92439,7 +92593,7 @@ ${stderr}`));
|
|
|
92439
92593
|
params: {
|
|
92440
92594
|
cmdId: 31,
|
|
92441
92595
|
channelIdOverride: channel,
|
|
92442
|
-
messageClass:
|
|
92596
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
92443
92597
|
}
|
|
92444
92598
|
},
|
|
92445
92599
|
{
|
|
@@ -92447,7 +92601,7 @@ ${stderr}`));
|
|
|
92447
92601
|
params: {
|
|
92448
92602
|
cmdId: 31,
|
|
92449
92603
|
channelIdOverride: 251,
|
|
92450
|
-
messageClass:
|
|
92604
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
92451
92605
|
}
|
|
92452
92606
|
},
|
|
92453
92607
|
{
|
|
@@ -92455,7 +92609,7 @@ ${stderr}`));
|
|
|
92455
92609
|
params: {
|
|
92456
92610
|
cmdId: 31,
|
|
92457
92611
|
channelIdOverride: 250,
|
|
92458
|
-
messageClass:
|
|
92612
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
92459
92613
|
}
|
|
92460
92614
|
},
|
|
92461
92615
|
{
|
|
@@ -92463,7 +92617,7 @@ ${stderr}`));
|
|
|
92463
92617
|
params: {
|
|
92464
92618
|
cmdId: 31,
|
|
92465
92619
|
channelIdOverride: 250,
|
|
92466
|
-
messageClass:
|
|
92620
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
92467
92621
|
extensionXml: `<?xml version="1.0" encoding="UTF-8" ?><Extension version="1.1"><channelId>251</channelId></Extension>`
|
|
92468
92622
|
}
|
|
92469
92623
|
}
|
|
@@ -92506,7 +92660,7 @@ ${stderr}`));
|
|
|
92506
92660
|
const frame = await this.client.sendFrame({
|
|
92507
92661
|
cmdId: 31,
|
|
92508
92662
|
channelIdOverride: Number(channel),
|
|
92509
|
-
messageClass:
|
|
92663
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
92510
92664
|
timeoutMs
|
|
92511
92665
|
});
|
|
92512
92666
|
lastCode = frame.header.responseCode;
|
|
@@ -92787,7 +92941,7 @@ ${stderr}`));
|
|
|
92787
92941
|
msgNumOverride: msgNum,
|
|
92788
92942
|
extensionXml: buildChannelExtensionXml(channelId),
|
|
92789
92943
|
payloadXml,
|
|
92790
|
-
messageClass:
|
|
92944
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
92791
92945
|
streamType: config.streamType,
|
|
92792
92946
|
timeoutMs: 2e4
|
|
92793
92947
|
};
|
|
@@ -92919,7 +93073,7 @@ ${stderr}`));
|
|
|
92919
93073
|
channelIdOverride: channelId,
|
|
92920
93074
|
extensionXml: a.extensionXml,
|
|
92921
93075
|
payloadXml: a.payloadXml,
|
|
92922
|
-
messageClass:
|
|
93076
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
92923
93077
|
streamType: a.streamType,
|
|
92924
93078
|
...msgNum !== void 0 ? { msgNumOverride: msgNum } : {},
|
|
92925
93079
|
timeoutMs: 2e3
|
|
@@ -92949,7 +93103,7 @@ ${stderr}`));
|
|
|
92949
93103
|
channel: ch,
|
|
92950
93104
|
channelIdOverride: channelId,
|
|
92951
93105
|
extensionXml: buildChannelExtensionXml(channelId),
|
|
92952
|
-
messageClass:
|
|
93106
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
92953
93107
|
streamType: 0
|
|
92954
93108
|
});
|
|
92955
93109
|
} catch (e) {
|
|
@@ -92991,7 +93145,7 @@ ${stderr}`));
|
|
|
92991
93145
|
channel: ch,
|
|
92992
93146
|
channelIdOverride: channelId,
|
|
92993
93147
|
extensionXml: buildChannelExtensionXml(channelId),
|
|
92994
|
-
messageClass:
|
|
93148
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
92995
93149
|
streamType: 0
|
|
92996
93150
|
});
|
|
92997
93151
|
} catch (e) {
|
|
@@ -93013,7 +93167,7 @@ ${stderr}`));
|
|
|
93013
93167
|
channelIdOverride: channelId,
|
|
93014
93168
|
extensionXml,
|
|
93015
93169
|
payloadXml,
|
|
93016
|
-
messageClass:
|
|
93170
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93017
93171
|
streamType: 0
|
|
93018
93172
|
});
|
|
93019
93173
|
if (frame.header.responseCode !== 200) {
|
|
@@ -93045,7 +93199,7 @@ ${stderr}`));
|
|
|
93045
93199
|
channelIdOverride: channelId,
|
|
93046
93200
|
extensionXml,
|
|
93047
93201
|
payloadXml,
|
|
93048
|
-
messageClass:
|
|
93202
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93049
93203
|
streamType: 0
|
|
93050
93204
|
});
|
|
93051
93205
|
if (frame.header.responseCode !== 200) throw new Error(`PTZ preset move rejected (response_code ${frame.header.responseCode})`);
|
|
@@ -93066,7 +93220,7 @@ ${stderr}`));
|
|
|
93066
93220
|
channelIdOverride: channelId,
|
|
93067
93221
|
extensionXml,
|
|
93068
93222
|
payloadXml,
|
|
93069
|
-
messageClass:
|
|
93223
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93070
93224
|
streamType: 0
|
|
93071
93225
|
});
|
|
93072
93226
|
if (frame.header.responseCode !== 200) throw new Error(`PTZ preset save rejected (response_code ${frame.header.responseCode})`);
|
|
@@ -93083,7 +93237,7 @@ ${stderr}`));
|
|
|
93083
93237
|
channelIdOverride: channelId,
|
|
93084
93238
|
extensionXml,
|
|
93085
93239
|
payloadXml,
|
|
93086
|
-
messageClass:
|
|
93240
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93087
93241
|
streamType: 0
|
|
93088
93242
|
});
|
|
93089
93243
|
if (frame.header.responseCode !== 200) throw new Error(`PTZ goto preset rejected (response_code ${frame.header.responseCode})`);
|
|
@@ -93110,7 +93264,7 @@ ${stderr}`));
|
|
|
93110
93264
|
channelIdOverride: channelId,
|
|
93111
93265
|
extensionXml,
|
|
93112
93266
|
payloadXml,
|
|
93113
|
-
messageClass:
|
|
93267
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93114
93268
|
streamType: 0
|
|
93115
93269
|
})).header.responseCode };
|
|
93116
93270
|
},
|
|
@@ -93164,7 +93318,7 @@ ${stderr}`));
|
|
|
93164
93318
|
channel: ch,
|
|
93165
93319
|
channelIdOverride: ch,
|
|
93166
93320
|
extensionXml: buildPtzGuardExtensionXml(ch, "read"),
|
|
93167
|
-
messageClass:
|
|
93321
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93168
93322
|
streamType: 0
|
|
93169
93323
|
}));
|
|
93170
93324
|
}
|
|
@@ -93190,7 +93344,7 @@ ${stderr}`));
|
|
|
93190
93344
|
timeoutSeconds: options.timeoutSeconds,
|
|
93191
93345
|
setPosition: options.setPosition === true
|
|
93192
93346
|
}),
|
|
93193
|
-
messageClass:
|
|
93347
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93194
93348
|
streamType: 0
|
|
93195
93349
|
});
|
|
93196
93350
|
}
|
|
@@ -93211,7 +93365,7 @@ ${stderr}`));
|
|
|
93211
93365
|
channelIdOverride: ch,
|
|
93212
93366
|
extensionXml: buildPtzGuardExtensionXml(ch, "write"),
|
|
93213
93367
|
payloadXml: buildPtzGuardGoXml(ch),
|
|
93214
|
-
messageClass:
|
|
93368
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93215
93369
|
streamType: 0
|
|
93216
93370
|
});
|
|
93217
93371
|
}
|
|
@@ -93227,7 +93381,7 @@ ${stderr}`));
|
|
|
93227
93381
|
channel: ch,
|
|
93228
93382
|
channelIdOverride: channelId,
|
|
93229
93383
|
extensionXml: buildChannelExtensionXml(channelId),
|
|
93230
|
-
messageClass:
|
|
93384
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93231
93385
|
streamType: 0
|
|
93232
93386
|
});
|
|
93233
93387
|
const parseTriplet = (sectionTag) => {
|
|
@@ -93265,7 +93419,7 @@ ${stderr}`));
|
|
|
93265
93419
|
channelIdOverride: channelId,
|
|
93266
93420
|
extensionXml,
|
|
93267
93421
|
payloadXml,
|
|
93268
|
-
messageClass:
|
|
93422
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
|
|
93269
93423
|
streamType: 0
|
|
93270
93424
|
});
|
|
93271
93425
|
if (frame.header.responseCode !== 200) throw new Error(`Zoom rejected (response_code ${frame.header.responseCode})`);
|
|
@@ -95016,7 +95170,7 @@ ${xml}`);
|
|
|
95016
95170
|
* @returns Test results for all stream types and profiles
|
|
95017
95171
|
*/
|
|
95018
95172
|
async testChannelStreams(channel, logger) {
|
|
95019
|
-
const { testChannelStreams } = await Promise.resolve().then(() => require("./DiagnosticsTools-
|
|
95173
|
+
const { testChannelStreams } = await Promise.resolve().then(() => require("./DiagnosticsTools-ZC7I7QM6-CD4rRF8a.js"));
|
|
95020
95174
|
return await testChannelStreams({
|
|
95021
95175
|
api: this,
|
|
95022
95176
|
channel: this.normalizeChannel(channel),
|
|
@@ -95032,7 +95186,7 @@ ${xml}`);
|
|
|
95032
95186
|
* @returns Complete diagnostics for all channels and streams
|
|
95033
95187
|
*/
|
|
95034
95188
|
async collectMultifocalDiagnostics(logger) {
|
|
95035
|
-
const { collectMultifocalDiagnostics } = await Promise.resolve().then(() => require("./DiagnosticsTools-
|
|
95189
|
+
const { collectMultifocalDiagnostics } = await Promise.resolve().then(() => require("./DiagnosticsTools-ZC7I7QM6-CD4rRF8a.js"));
|
|
95036
95190
|
return await collectMultifocalDiagnostics({
|
|
95037
95191
|
api: this,
|
|
95038
95192
|
logger
|
|
@@ -97117,7 +97271,7 @@ ${scheduleItems}
|
|
|
97117
97271
|
if (raw.length === 0) throw new Error("Downloaded recording is empty");
|
|
97118
97272
|
const videoFrames = [];
|
|
97119
97273
|
let videoType = null;
|
|
97120
|
-
const decoder = new
|
|
97274
|
+
const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
|
|
97121
97275
|
strict: false,
|
|
97122
97276
|
logger: this.logger,
|
|
97123
97277
|
onVideoAccessUnit: ({ annexB: annexB2, microseconds }) => {
|
|
@@ -97465,7 +97619,7 @@ ${scheduleItems}
|
|
|
97465
97619
|
return {
|
|
97466
97620
|
getPlaylist: () => {
|
|
97467
97621
|
try {
|
|
97468
|
-
const { readFileSync } =
|
|
97622
|
+
const { readFileSync } = require_chunk_ZBACPERX.__require("fs");
|
|
97469
97623
|
return readFileSync(playlistPath, "utf8");
|
|
97470
97624
|
} catch {
|
|
97471
97625
|
return "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:4\n";
|
|
@@ -97474,7 +97628,7 @@ ${scheduleItems}
|
|
|
97474
97628
|
getSegment: (name) => {
|
|
97475
97629
|
if (segments.has(name)) return segments.get(name);
|
|
97476
97630
|
try {
|
|
97477
|
-
const { readFileSync } =
|
|
97631
|
+
const { readFileSync } = require_chunk_ZBACPERX.__require("fs");
|
|
97478
97632
|
const data = readFileSync(path$3.join(tempDir, name));
|
|
97479
97633
|
segments.set(name, data);
|
|
97480
97634
|
return data;
|
|
@@ -97484,7 +97638,7 @@ ${scheduleItems}
|
|
|
97484
97638
|
},
|
|
97485
97639
|
listSegments: () => {
|
|
97486
97640
|
try {
|
|
97487
|
-
const { readdirSync } =
|
|
97641
|
+
const { readdirSync } = require_chunk_ZBACPERX.__require("fs");
|
|
97488
97642
|
return readdirSync(tempDir).filter((f) => f.endsWith(".ts"));
|
|
97489
97643
|
} catch {
|
|
97490
97644
|
return [];
|
|
@@ -99069,41 +99223,41 @@ async function autoDetectDeviceType(inputs) {
|
|
|
99069
99223
|
variant: "cmd80 class=0x6414",
|
|
99070
99224
|
op: () => api.getInfo(void 0, {
|
|
99071
99225
|
timeoutMs: 2500,
|
|
99072
|
-
messageClass:
|
|
99226
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
99073
99227
|
})
|
|
99074
99228
|
},
|
|
99075
99229
|
{
|
|
99076
99230
|
variant: "cmd80 class=0x6614",
|
|
99077
99231
|
op: () => api.getInfo(void 0, {
|
|
99078
99232
|
timeoutMs: 3e3,
|
|
99079
|
-
messageClass:
|
|
99233
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
|
|
99080
99234
|
})
|
|
99081
99235
|
},
|
|
99082
99236
|
{
|
|
99083
99237
|
variant: "cmd318(ch0) class=0x6414",
|
|
99084
99238
|
op: () => api.getInfo(0, {
|
|
99085
99239
|
timeoutMs: 3e3,
|
|
99086
|
-
messageClass:
|
|
99240
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
99087
99241
|
})
|
|
99088
99242
|
},
|
|
99089
99243
|
{
|
|
99090
99244
|
variant: "cmd318(ch0) class=0x6614",
|
|
99091
99245
|
op: () => api.getInfo(0, {
|
|
99092
99246
|
timeoutMs: 3500,
|
|
99093
|
-
messageClass:
|
|
99247
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
|
|
99094
99248
|
})
|
|
99095
99249
|
}
|
|
99096
99250
|
]), runProbeVariants("getSupportInfo", [{
|
|
99097
99251
|
variant: "cmd199 class=0x6414",
|
|
99098
99252
|
op: () => api.getSupportInfo({
|
|
99099
99253
|
timeoutMs: 2500,
|
|
99100
|
-
messageClass:
|
|
99254
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
|
|
99101
99255
|
})
|
|
99102
99256
|
}, {
|
|
99103
99257
|
variant: "cmd199 class=0x6614",
|
|
99104
99258
|
op: () => api.getSupportInfo({
|
|
99105
99259
|
timeoutMs: 3500,
|
|
99106
|
-
messageClass:
|
|
99260
|
+
messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
|
|
99107
99261
|
})
|
|
99108
99262
|
}])]);
|
|
99109
99263
|
const deviceInfo = infoProbe?.value;
|
|
@@ -144094,7 +144248,7 @@ function buildAacAudioSpecificConfigHex(params) {
|
|
|
144094
144248
|
if (!Number.isFinite(audioObjectType) || audioObjectType <= 0 || audioObjectType > 31) return;
|
|
144095
144249
|
return ((audioObjectType & 31) << 11 | (samplingFreqIndex & 15) << 7 | (channels & 15) << 3).toString(16).padStart(4, "0");
|
|
144096
144250
|
}
|
|
144097
|
-
function
|
|
144251
|
+
function parseAdtsHeader2(adtsFrame) {
|
|
144098
144252
|
if (adtsFrame.length < 7) return null;
|
|
144099
144253
|
if (adtsFrame[0] !== 255 || (adtsFrame[1] & 240) !== 240) return null;
|
|
144100
144254
|
const protectionAbsent = (adtsFrame[1] & 1) === 1;
|
|
@@ -144199,7 +144353,7 @@ function packetizeH265(nal, rtp, opts, markerOnLast, isLastNal) {
|
|
|
144199
144353
|
return out;
|
|
144200
144354
|
}
|
|
144201
144355
|
function packetizeAacAdtsFrame(adts, rtp) {
|
|
144202
|
-
const parsed =
|
|
144356
|
+
const parsed = parseAdtsHeader2(adts);
|
|
144203
144357
|
if (!parsed) return { packets: [] };
|
|
144204
144358
|
const raw = adts.subarray(parsed.headerLength);
|
|
144205
144359
|
if (!raw.length) return { packets: [] };
|
|
@@ -145632,7 +145786,7 @@ async function createRfc4571TcpServerInternal(options) {
|
|
|
145632
145786
|
streamClient = dedicatedSession.client;
|
|
145633
145787
|
logSessionsSummary(`dedicated session created: ${sessionKey}`);
|
|
145634
145788
|
} else streamClient = baseApi.client;
|
|
145635
|
-
const createLiveStream = async () => new
|
|
145789
|
+
const createLiveStream = async () => new require_chunk_ZBACPERX.BaichuanVideoStream({
|
|
145636
145790
|
client: streamClient,
|
|
145637
145791
|
api: baseApi,
|
|
145638
145792
|
channel: ch,
|
|
@@ -145849,7 +146003,7 @@ async function createRfc4571TcpServerInternal(options) {
|
|
|
145849
146003
|
}, isCompositeStream ? Math.min(1e4, keyframeTimeoutMs) : 5e3);
|
|
145850
146004
|
const onAudio = (frame) => {
|
|
145851
146005
|
sawAnyAudio = true;
|
|
145852
|
-
const parsed =
|
|
146006
|
+
const parsed = parseAdtsHeader2(frame);
|
|
145853
146007
|
if (!parsed) {
|
|
145854
146008
|
if (debugLogsLeft-- > 0) {
|
|
145855
146009
|
const head = frame.subarray(0, Math.min(16, frame.length)).toString("hex");
|
|
@@ -147032,6 +147186,23 @@ var reolinkCameraSchema = object({
|
|
|
147032
147186
|
motionEnabled: boolean().optional(),
|
|
147033
147187
|
motionSensitivity: number().int().min(1).max(50).optional(),
|
|
147034
147188
|
/**
|
|
147189
|
+
* Per-device clip-catalog refresh interval, in minutes — the OVERRIDE half
|
|
147190
|
+
* of the cascade whose global default lives in the addon store under the
|
|
147191
|
+
* same key. Absent means "use the global", which is the same thing a reset
|
|
147192
|
+
* means (`resolveClipCatalogRefreshMinutes`, D562). `0` turns the timer off
|
|
147193
|
+
* for this camera. What the timer DOES with it differs by topology and the
|
|
147194
|
+
* field description says both: a free read on a hub child and on mains, and
|
|
147195
|
+
* stale-marking only on a standalone battery camera.
|
|
147196
|
+
*/
|
|
147197
|
+
clipCatalogRefreshMinutes: number().int().min(0).max(1440).optional(),
|
|
147198
|
+
/**
|
|
147199
|
+
* Let the clip timer WAKE this standalone battery camera. Off by default and
|
|
147200
|
+
* per device only — a 15-minute forced timer is 96 wakes/day against a
|
|
147201
|
+
* natural rate of 1–20 clips/day on this fleet (D562). Inert on every other
|
|
147202
|
+
* topology, which is why it is one switch and not a second meaning.
|
|
147203
|
+
*/
|
|
147204
|
+
clipCatalogForcedWake: boolean().optional(),
|
|
147205
|
+
/**
|
|
147035
147206
|
* Per-AI-class sensitivity overrides — pushed via
|
|
147036
147207
|
* `setAiDetection(aiType, sensitivity)` (cmdId=343). Reolink
|
|
147037
147208
|
* range 0..100 — same scale the reolink_aio CGI exposes
|
|
@@ -147576,6 +147747,20 @@ var ClipCatalogCache = class ClipCatalogCache {
|
|
|
147576
147747
|
}
|
|
147577
147748
|
return null;
|
|
147578
147749
|
}
|
|
147750
|
+
/**
|
|
147751
|
+
* Every day key this device holds a listing for, in no particular order.
|
|
147752
|
+
*
|
|
147753
|
+
* It is what lets an availability probe say WHEN a catalog was last
|
|
147754
|
+
* confirmed without listing anything: a camera nobody may wake reports the
|
|
147755
|
+
* age of what is held, and a source with nothing held says so rather than
|
|
147756
|
+
* claiming to be current.
|
|
147757
|
+
*/
|
|
147758
|
+
daysHeld(deviceId) {
|
|
147759
|
+
const out = [];
|
|
147760
|
+
const prefix = `${deviceId}|`;
|
|
147761
|
+
for (const key of this.slots.keys()) if (key.startsWith(prefix)) out.push(key.slice(prefix.length));
|
|
147762
|
+
return out;
|
|
147763
|
+
}
|
|
147579
147764
|
/** Every row held for a device, in no particular order. The prefetch sorts. */
|
|
147580
147765
|
allRows(deviceId) {
|
|
147581
147766
|
const out = [];
|
|
@@ -147629,6 +147814,57 @@ var ClipCatalogCache = class ClipCatalogCache {
|
|
|
147629
147814
|
}
|
|
147630
147815
|
};
|
|
147631
147816
|
//#endregion
|
|
147817
|
+
//#region src/clips/clip-still-refusal.ts
|
|
147818
|
+
/** "You are in the queue." */
|
|
147819
|
+
function deferStill(code, reason, retryAfterMs) {
|
|
147820
|
+
return {
|
|
147821
|
+
ok: false,
|
|
147822
|
+
code,
|
|
147823
|
+
disposition: "deferred",
|
|
147824
|
+
reason,
|
|
147825
|
+
retryAfterMs
|
|
147826
|
+
};
|
|
147827
|
+
}
|
|
147828
|
+
/** "This will not happen." */
|
|
147829
|
+
function refuseStill(code, reason) {
|
|
147830
|
+
return {
|
|
147831
|
+
ok: false,
|
|
147832
|
+
code,
|
|
147833
|
+
disposition: "final",
|
|
147834
|
+
reason
|
|
147835
|
+
};
|
|
147836
|
+
}
|
|
147837
|
+
/**
|
|
147838
|
+
* The queue's own refusal — the only failure that is about the LANE rather
|
|
147839
|
+
* than about the camera, and therefore the only one that is deferred.
|
|
147840
|
+
*
|
|
147841
|
+
* It carries its own `retryAfterMs` because the queue is the only thing that
|
|
147842
|
+
* knows it: the depth it is holding, or the exact instant a host's backoff
|
|
147843
|
+
* closes.
|
|
147844
|
+
*/
|
|
147845
|
+
var ThumbQueueRefusedError = class extends Error {
|
|
147846
|
+
code;
|
|
147847
|
+
retryAfterMs;
|
|
147848
|
+
constructor(code, message, retryAfterMs) {
|
|
147849
|
+
super(message);
|
|
147850
|
+
this.name = "ThumbQueueRefusedError";
|
|
147851
|
+
this.code = code;
|
|
147852
|
+
this.retryAfterMs = retryAfterMs;
|
|
147853
|
+
}
|
|
147854
|
+
};
|
|
147855
|
+
/**
|
|
147856
|
+
* A thrown value from the queue, as a wire answer — or `null` when it was not
|
|
147857
|
+
* the queue at all.
|
|
147858
|
+
*
|
|
147859
|
+
* `null` is not a default: it is what makes the caller name `camera-refused`
|
|
147860
|
+
* explicitly, instead of a socket close quietly inheriting "come back later"
|
|
147861
|
+
* and a tile retrying four times against a camera that is not there.
|
|
147862
|
+
*/
|
|
147863
|
+
function queueRefusal(err) {
|
|
147864
|
+
if (!(err instanceof ThumbQueueRefusedError)) return null;
|
|
147865
|
+
return deferStill(err.code, err.message, err.retryAfterMs);
|
|
147866
|
+
}
|
|
147867
|
+
//#endregion
|
|
147632
147868
|
//#region src/clips/clip-data-plane.ts
|
|
147633
147869
|
/**
|
|
147634
147870
|
* The addon's two data-plane handlers — the still and the bytes.
|
|
@@ -147650,6 +147886,19 @@ var ClipCatalogCache = class ClipCatalogCache {
|
|
|
147650
147886
|
* which. Waiting instead would hold a request open behind a camera that is
|
|
147651
147887
|
* asleep for the night.
|
|
147652
147888
|
*
|
|
147889
|
+
* **And it says whether to COME BACK.** Those reasons are two kinds of answer:
|
|
147890
|
+
* `queue-full` and `camera-backoff` mean the still is still coming, the rest
|
|
147891
|
+
* mean it will not. The branch that refused decides — nothing here maps a code
|
|
147892
|
+
* to a disposition, and no client parses one out of the prose — and the answer
|
|
147893
|
+
* travels as its own headers (`x-camstack-reason-code`,
|
|
147894
|
+
* `x-camstack-disposition`, plus `retry-after` on a deferred one) beside the
|
|
147895
|
+
* prose, which stays verbatim because a `camera-refused` is only ever
|
|
147896
|
+
* explained by the camera's own words.
|
|
147897
|
+
*
|
|
147898
|
+
* A response with no disposition header is read as FINAL by every surface,
|
|
147899
|
+
* which is exactly how this route behaved before: an older addon under a newer
|
|
147900
|
+
* admin UI keeps the old behaviour instead of gaining a retry loop.
|
|
147901
|
+
*
|
|
147653
147902
|
* ## Why Range is hand-rolled here
|
|
147654
147903
|
*
|
|
147655
147904
|
* One file, one path, no root mapping — and `createReadStream().pipe(res)`
|
|
@@ -147692,26 +147941,45 @@ function createClipThumbHandler(deps) {
|
|
|
147692
147941
|
try {
|
|
147693
147942
|
minted = await deps.mint(deviceId, clipKey);
|
|
147694
147943
|
} catch (err) {
|
|
147695
|
-
minted =
|
|
147696
|
-
ok: false,
|
|
147697
|
-
reason: err instanceof Error ? err.message : String(err)
|
|
147698
|
-
};
|
|
147944
|
+
minted = refuseStill("camera-refused", err instanceof Error ? err.message : String(err));
|
|
147699
147945
|
}
|
|
147700
147946
|
if (!minted.ok) {
|
|
147701
|
-
|
|
147702
|
-
tags: { deviceId },
|
|
147703
|
-
meta: {
|
|
147704
|
-
clipKey,
|
|
147705
|
-
reason: minted.reason
|
|
147706
|
-
}
|
|
147707
|
-
});
|
|
147708
|
-
res.writeHead(204, { "x-camstack-reason": minted.reason.slice(0, 500) });
|
|
147709
|
-
res.end();
|
|
147947
|
+
writeRefusal(res, minted, deviceId, deps.logger, clipKey);
|
|
147710
147948
|
return;
|
|
147711
147949
|
}
|
|
147712
147950
|
writeJpeg(res, minted.bytes, req.method === "HEAD");
|
|
147713
147951
|
};
|
|
147714
147952
|
}
|
|
147953
|
+
/**
|
|
147954
|
+
* The 204, with everything the tile needs to decide what to draw next.
|
|
147955
|
+
*
|
|
147956
|
+
* A FINAL refusal is work that was dropped, so it is warned, per camera. A
|
|
147957
|
+
* DEFERRED one dropped nothing — the still is queued behind another — and
|
|
147958
|
+
* warning it would put 43 lines on the log for one 60-tile page against a
|
|
147959
|
+
* 16-deep lane, which is how a real drop stops being visible.
|
|
147960
|
+
*/
|
|
147961
|
+
function writeRefusal(res, refusal, deviceId, logger, clipKey) {
|
|
147962
|
+
const line = "videoclips: thumbnail not minted";
|
|
147963
|
+
const extras = {
|
|
147964
|
+
tags: { deviceId },
|
|
147965
|
+
meta: {
|
|
147966
|
+
clipKey,
|
|
147967
|
+
code: refusal.code,
|
|
147968
|
+
disposition: refusal.disposition,
|
|
147969
|
+
reason: refusal.reason
|
|
147970
|
+
}
|
|
147971
|
+
};
|
|
147972
|
+
if (refusal.disposition === "final") logger.warn(line, extras);
|
|
147973
|
+
else logger.debug(line, extras);
|
|
147974
|
+
const retryAfterMs = refusal.disposition === "deferred" ? refusal.retryAfterMs : void 0;
|
|
147975
|
+
res.writeHead(204, {
|
|
147976
|
+
[CLIP_STILL_REASON_HEADER]: refusal.reason.slice(0, 500),
|
|
147977
|
+
[CLIP_STILL_REASON_CODE_HEADER]: refusal.code,
|
|
147978
|
+
[CLIP_STILL_DISPOSITION_HEADER]: refusal.disposition,
|
|
147979
|
+
...retryAfterMs === void 0 ? {} : { "retry-after": String(Math.max(1, Math.ceil(retryAfterMs / 1e3))) }
|
|
147980
|
+
});
|
|
147981
|
+
res.end();
|
|
147982
|
+
}
|
|
147715
147983
|
function writeJpeg(res, bytes, headOnly) {
|
|
147716
147984
|
res.writeHead(200, {
|
|
147717
147985
|
"content-type": "image/jpeg",
|
|
@@ -147830,21 +148098,73 @@ function createClipPlaybackHandler(deps) {
|
|
|
147830
148098
|
//#region src/clips/clip-mux.ts
|
|
147831
148099
|
/** A clip that has not transferred by here is not going to. */
|
|
147832
148100
|
var CLIP_MUX_TIMEOUT_MS = 6e4;
|
|
148101
|
+
/**
|
|
148102
|
+
* Decide what to do with the track the download returned.
|
|
148103
|
+
*
|
|
148104
|
+
* The gate is `format === 'adts'` and NOT `audio !== null`: an `adpcm` or
|
|
148105
|
+
* `unknown` track stream-copied into MP4 produces a file whose audio track is
|
|
148106
|
+
* garbage, which is worse than a clip that is honestly silent. A track that
|
|
148107
|
+
* arrived with no frames is no track — counting it would promise sound the
|
|
148108
|
+
* file does not have.
|
|
148109
|
+
*/
|
|
148110
|
+
function decideClipAudio(audio) {
|
|
148111
|
+
if (audio === null) return {
|
|
148112
|
+
kind: "silent",
|
|
148113
|
+
reason: "no-audio-track",
|
|
148114
|
+
format: null
|
|
148115
|
+
};
|
|
148116
|
+
if (audio.frames === 0 || audio.bytes === 0) return {
|
|
148117
|
+
kind: "silent",
|
|
148118
|
+
reason: "no-audio-track",
|
|
148119
|
+
format: audio.format
|
|
148120
|
+
};
|
|
148121
|
+
if (audio.format !== "adts") return {
|
|
148122
|
+
kind: "silent",
|
|
148123
|
+
reason: "audio-not-copyable",
|
|
148124
|
+
format: audio.format
|
|
148125
|
+
};
|
|
148126
|
+
return {
|
|
148127
|
+
kind: "copy",
|
|
148128
|
+
format: "adts",
|
|
148129
|
+
data: audio.data
|
|
148130
|
+
};
|
|
148131
|
+
}
|
|
148132
|
+
function usableFps(fps) {
|
|
148133
|
+
if (fps === null || fps === void 0) return null;
|
|
148134
|
+
return Number.isFinite(fps) && fps > 0 ? fps : null;
|
|
148135
|
+
}
|
|
147833
148136
|
function buildClipMuxArgs(input) {
|
|
148137
|
+
const demuxer = input.videoType === "H265" ? "hevc" : "h264";
|
|
148138
|
+
const fps = usableFps(input.fps);
|
|
147834
148139
|
return [
|
|
147835
148140
|
"-hide_banner",
|
|
147836
148141
|
"-loglevel",
|
|
147837
148142
|
"error",
|
|
147838
148143
|
"-nostdin",
|
|
147839
148144
|
"-y",
|
|
148145
|
+
...fps !== null ? ["-r", String(fps)] : [],
|
|
147840
148146
|
"-f",
|
|
147841
|
-
|
|
148147
|
+
demuxer,
|
|
147842
148148
|
"-i",
|
|
147843
148149
|
input.inputPath,
|
|
147844
|
-
|
|
148150
|
+
...input.audio !== void 0 ? [
|
|
148151
|
+
"-f",
|
|
148152
|
+
"aac",
|
|
148153
|
+
"-i",
|
|
148154
|
+
input.audio.path
|
|
148155
|
+
] : [],
|
|
148156
|
+
"-c:v",
|
|
147845
148157
|
"copy",
|
|
148158
|
+
...input.audio !== void 0 ? [
|
|
148159
|
+
"-c:a",
|
|
148160
|
+
"copy",
|
|
148161
|
+
"-bsf:a",
|
|
148162
|
+
"aac_adtstoasc"
|
|
148163
|
+
] : [],
|
|
147846
148164
|
"-movflags",
|
|
147847
148165
|
"+faststart",
|
|
148166
|
+
"-f",
|
|
148167
|
+
"mp4",
|
|
147848
148168
|
input.outputPath
|
|
147849
148169
|
];
|
|
147850
148170
|
}
|
|
@@ -147931,6 +148251,85 @@ function describeMuxFailure(run) {
|
|
|
147931
148251
|
if (run.code !== 0) return `mux-failed (exit ${String(run.code)}): ${run.stderr.slice(0, 300)}`;
|
|
147932
148252
|
return null;
|
|
147933
148253
|
}
|
|
148254
|
+
//#endregion
|
|
148255
|
+
//#region src/clips/clip-truncation.ts
|
|
148256
|
+
/**
|
|
148257
|
+
* The window a TRUNCATED fetch is retried on: the library's own default, the
|
|
148258
|
+
* one this repo shipped for its whole life. A retry exists to answer "was that
|
|
148259
|
+
* the window or the camera?", so it must be the conservative number and not a
|
|
148260
|
+
* second short one.
|
|
148261
|
+
*/
|
|
148262
|
+
var CLIP_IDLE_WINDOW_RETRY_MS = 2e3;
|
|
148263
|
+
/**
|
|
148264
|
+
* Did this pass deliver the clip the row describes?
|
|
148265
|
+
*
|
|
148266
|
+
* Only a SHORTFALL is a defect. A decode that runs longer than the row is
|
|
148267
|
+
* routine — the row's stamps are whole seconds and its twins differ by up to
|
|
148268
|
+
* four — and calling it truncated would refuse a clip that is more complete
|
|
148269
|
+
* than the catalog knew.
|
|
148270
|
+
*/
|
|
148271
|
+
function judgeClipDuration(input) {
|
|
148272
|
+
if (input.inProgress === true) return {
|
|
148273
|
+
kind: "unknown",
|
|
148274
|
+
reason: "clip-open"
|
|
148275
|
+
};
|
|
148276
|
+
if (input.decodedSeconds === null || !Number.isFinite(input.decodedSeconds)) return {
|
|
148277
|
+
kind: "unknown",
|
|
148278
|
+
reason: "duration-unmeasured"
|
|
148279
|
+
};
|
|
148280
|
+
if (!Number.isFinite(input.expectedMs) || input.expectedMs <= 0) return {
|
|
148281
|
+
kind: "unknown",
|
|
148282
|
+
reason: "row-window-unknown"
|
|
148283
|
+
};
|
|
148284
|
+
const expectedSeconds = input.expectedMs / 1e3;
|
|
148285
|
+
const decodedSeconds = input.decodedSeconds;
|
|
148286
|
+
const shortfallSeconds = expectedSeconds - decodedSeconds;
|
|
148287
|
+
if (shortfallSeconds > 1.5) return {
|
|
148288
|
+
kind: "short",
|
|
148289
|
+
expectedSeconds,
|
|
148290
|
+
decodedSeconds,
|
|
148291
|
+
shortfallSeconds
|
|
148292
|
+
};
|
|
148293
|
+
return {
|
|
148294
|
+
kind: "complete",
|
|
148295
|
+
expectedSeconds,
|
|
148296
|
+
decodedSeconds
|
|
148297
|
+
};
|
|
148298
|
+
}
|
|
148299
|
+
//#endregion
|
|
148300
|
+
//#region src/clips/clip-thumb-queue.ts
|
|
148301
|
+
/**
|
|
148302
|
+
* The provider's OWN CoverPreview lane — two queues, one in flight per host.
|
|
148303
|
+
*
|
|
148304
|
+
* The library already serialises CoverPreview: one in flight per api instance,
|
|
148305
|
+
* a 50-deep FIFO that throws past that, and a process-wide host-keyed backoff
|
|
148306
|
+
* that doubles on a 400. What a FIFO cannot express is PRIORITY, and the two
|
|
148307
|
+
* kinds of mint this feature has are not equal — an operator staring at a tile
|
|
148308
|
+
* must not wait behind twenty prefetches for clips nobody opened.
|
|
148309
|
+
*
|
|
148310
|
+
* So the provider keeps two queues of its own, hands the library at most ONE
|
|
148311
|
+
* CoverPreview per host at a time, drains `interactive` before `prefetch`, and
|
|
148312
|
+
* re-checks between every prefetch mint (D549 16).
|
|
148313
|
+
*
|
|
148314
|
+
* ## Two things it refuses to do
|
|
148315
|
+
*
|
|
148316
|
+
* **Wait when the queue is full.** A request past the bound is REFUSED with
|
|
148317
|
+
* `queue-full`, so the route can answer 204 with a reason. A request held open
|
|
148318
|
+
* behind forty-nine others is a spinner the operator cannot cancel, and the
|
|
148319
|
+
* camera will have rolled the clip over before it is served.
|
|
148320
|
+
*
|
|
148321
|
+
* **Let a prefetch probe a sulking host.** While a host's 400 backoff is open
|
|
148322
|
+
* a prefetch is skipped entirely; only an interactive request may probe it,
|
|
148323
|
+
* because only an interactive request has somebody waiting for the answer.
|
|
148324
|
+
*
|
|
148325
|
+
* The backoff mirrors the library's (1 s doubling to 30 s) rather than
|
|
148326
|
+
* replacing it: this one decides whether to ASK at all, the library's decides
|
|
148327
|
+
* how long to wait once asked.
|
|
148328
|
+
*/
|
|
148329
|
+
/** How long one waiting mint is worth, as a retry hint. Coarse on purpose:
|
|
148330
|
+
* the client floors it with its own backoff and caps its own attempts, so a
|
|
148331
|
+
* wrong hint can neither cause a hammer nor a hang. */
|
|
148332
|
+
var THUMB_DRAIN_HINT_PER_WAITER_MS = 1e3;
|
|
147934
148333
|
var THUMB_BACKOFF_INITIAL_MS = 1e3;
|
|
147935
148334
|
var THUMB_BACKOFF_MAX_MS = 3e4;
|
|
147936
148335
|
/**
|
|
@@ -147951,6 +148350,28 @@ var ClipThumbQueue = class {
|
|
|
147951
148350
|
const state = this.hosts.get(host);
|
|
147952
148351
|
return state !== void 0 && state.backoffUntilMs > this.now();
|
|
147953
148352
|
}
|
|
148353
|
+
/**
|
|
148354
|
+
* Is this host busy with, or owed, operator-facing work?
|
|
148355
|
+
*
|
|
148356
|
+
* The periodic catalog refresh asks before it spends the host's one lane:
|
|
148357
|
+
* an operator staring at a tile must never wait behind a timer, and a host
|
|
148358
|
+
* whose 400 backoff is open must not be probed by anything that has nobody
|
|
148359
|
+
* waiting for the answer — the same rule that already keeps a PREFETCH off
|
|
148360
|
+
* a sulking host.
|
|
148361
|
+
*
|
|
148362
|
+
* Either a mint is in flight or the host is sulking. There is deliberately
|
|
148363
|
+
* no third clause for "an interactive request is queued": a queued request
|
|
148364
|
+
* means a mint is running, so `running` already covers it, and a clause
|
|
148365
|
+
* that can never be the reason for the answer is a leftover that reads as
|
|
148366
|
+
* verification. The lane is one per host, so a PREFETCH holding it defers
|
|
148367
|
+
* the refresh too — which is right: the refresh is the lowest-priority
|
|
148368
|
+
* thing on this host.
|
|
148369
|
+
*/
|
|
148370
|
+
busy(host) {
|
|
148371
|
+
const state = this.hosts.get(host);
|
|
148372
|
+
if (state === void 0) return false;
|
|
148373
|
+
return state.backoffUntilMs > this.now() || state.running;
|
|
148374
|
+
}
|
|
147954
148375
|
backoffUntil(host) {
|
|
147955
148376
|
return this.hosts.get(host)?.backoffUntilMs ?? 0;
|
|
147956
148377
|
}
|
|
@@ -147973,8 +148394,8 @@ var ClipThumbQueue = class {
|
|
|
147973
148394
|
*/
|
|
147974
148395
|
async submit(job) {
|
|
147975
148396
|
const state = this.stateFor(job.host);
|
|
147976
|
-
if (job.lane === "prefetch" && state.backoffUntilMs > this.now()) throw new
|
|
147977
|
-
if (state.waiting.length >= 16) throw new
|
|
148397
|
+
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());
|
|
148398
|
+
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);
|
|
147978
148399
|
return new Promise((resolve, reject) => {
|
|
147979
148400
|
state.waiting.push({
|
|
147980
148401
|
lane: job.lane,
|
|
@@ -147994,7 +148415,7 @@ var ClipThumbQueue = class {
|
|
|
147994
148415
|
if (next === void 0) return;
|
|
147995
148416
|
state.waiting.splice(pick, 1);
|
|
147996
148417
|
if (next.lane === "prefetch" && state.backoffUntilMs > this.now()) {
|
|
147997
|
-
next.reject(
|
|
148418
|
+
next.reject(new ThumbQueueRefusedError("camera-backoff", `camera-backoff: host ${host} is backing off; prefetch skipped`, state.backoffUntilMs - this.now()));
|
|
147998
148419
|
this.drain(host);
|
|
147999
148420
|
return;
|
|
148000
148421
|
}
|
|
@@ -148047,12 +148468,13 @@ var ClipThumbQueue = class {
|
|
|
148047
148468
|
* exactly — 29 events, 28 inside the 20 listed files, and all 20 files
|
|
148048
148469
|
* carrying an event. They are two VIEWS of one store.
|
|
148049
148470
|
*
|
|
148050
|
-
* So the operator collapsed them (2026-09-20): one source, which
|
|
148051
|
-
* best API for what the camera IS
|
|
148052
|
-
*
|
|
148053
|
-
*
|
|
148054
|
-
*
|
|
148055
|
-
*
|
|
148471
|
+
* So the operator collapsed them (2026-09-20): one source, which picked the
|
|
148472
|
+
* best API for what the camera IS, and REPORTED the pick on `ClipSource.via`.
|
|
148473
|
+
* Later the same day the pick went too (D565): `FileInfoList` is the catalog
|
|
148474
|
+
* on every topology, so `via` had one value and is gone. What is left is one
|
|
148475
|
+
* source reading one API — and `clipKeyFor` below is still keyed off
|
|
148476
|
+
* `(channel, fileName)` rather than the source, which is now simply true
|
|
148477
|
+
* rather than a concession.
|
|
148056
148478
|
*/
|
|
148057
148479
|
var CLIP_SOURCE_ONBOARD = "native:reolink:onboard";
|
|
148058
148480
|
/** Every source this provider can name. Order is the picker's order. */
|
|
@@ -148103,10 +148525,10 @@ function parseClipId(clipId) {
|
|
|
148103
148525
|
}
|
|
148104
148526
|
/**
|
|
148105
148527
|
* The on-disk key for a clip's derived artifacts (its thumbnail, its muxed
|
|
148106
|
-
* MP4). Derived from `(channel, fileName)` and NOT from the source:
|
|
148107
|
-
*
|
|
148108
|
-
*
|
|
148109
|
-
*
|
|
148528
|
+
* MP4). Derived from `(channel, fileName)` and NOT from the source: a
|
|
148529
|
+
* thumbnail belongs to the FILE, so any surface that names the same file
|
|
148530
|
+
* finds the same picture and the camera is never asked for a second
|
|
148531
|
+
* CoverPreview of it.
|
|
148110
148532
|
*
|
|
148111
148533
|
* A hash rather than the encoded name because a hub `<Id>` is a full path and
|
|
148112
148534
|
* the encoded form is long enough to hit a file-name limit.
|
|
@@ -148255,7 +148677,7 @@ function startOf(file) {
|
|
|
148255
148677
|
return file.startTime?.getTime() ?? 0;
|
|
148256
148678
|
}
|
|
148257
148679
|
var partsCache = /* @__PURE__ */ new Map();
|
|
148258
|
-
function formatterFor(timeZone) {
|
|
148680
|
+
function formatterFor$1(timeZone) {
|
|
148259
148681
|
const cached = partsCache.get(timeZone);
|
|
148260
148682
|
if (cached !== void 0) return cached;
|
|
148261
148683
|
const made = new Intl.DateTimeFormat("en-US", {
|
|
@@ -148272,7 +148694,7 @@ function formatterFor(timeZone) {
|
|
|
148272
148694
|
return made;
|
|
148273
148695
|
}
|
|
148274
148696
|
function partsAt(epochMs, timeZone) {
|
|
148275
|
-
const parts = formatterFor(timeZone).formatToParts(new Date(epochMs));
|
|
148697
|
+
const parts = formatterFor$1(timeZone).formatToParts(new Date(epochMs));
|
|
148276
148698
|
const read = (type) => {
|
|
148277
148699
|
const found = parts.find((p) => p.type === type);
|
|
148278
148700
|
return found === void 0 ? 0 : Number(found.value);
|
|
@@ -148343,36 +148765,6 @@ function enumerateWallClockDays(sinceMs, untilMs, timeZone) {
|
|
|
148343
148765
|
//#endregion
|
|
148344
148766
|
//#region src/clips/clip-catalog.ts
|
|
148345
148767
|
/**
|
|
148346
|
-
* Reading a Reolink camera's own clip catalog: the cheap question, the day
|
|
148347
|
-
* index, then one day's files.
|
|
148348
|
-
*
|
|
148349
|
-
* ## The order is the point
|
|
148350
|
-
*
|
|
148351
|
-
* 1. **Is there a card at all?** `getHddInfoList` (cmd 102). 640 answers
|
|
148352
|
-
* `mount 0, capacity 0` — no SD card — and searching it is 400s and wasted
|
|
148353
|
-
* wake budget. "The cheap question comes first" (D549 4 / D54).
|
|
148354
|
-
* 2. **Which days hold footage?** `getDayRecords` (cmd 142) once per month.
|
|
148355
|
-
* The per-day listing loop of the earlier plan is deleted, not kept as a
|
|
148356
|
-
* fallback (D549 8): 19 days in 50 ms beats 30 searches.
|
|
148357
|
-
* 3. **What is in one day?** `FileInfoList` (cmd 14/15/16), sub AND main, then
|
|
148358
|
-
* twinned (D549 14b). A clip IS a file: its name is its id, its bytes
|
|
148359
|
-
* (cmd 5) and its still (cmd 298) are addressed from it (D549 10).
|
|
148360
|
-
*
|
|
148361
|
-
* This is the API a STANDALONE camera's one source resolves to — a standalone
|
|
148362
|
-
* answers `findEventLog` with an empty body. A hub child resolves to the event
|
|
148363
|
-
* view instead (`clip-event-catalog.ts`), and still uses this reader for the
|
|
148364
|
-
* day's files, which is what an event's window is joined against (D555).
|
|
148365
|
-
*
|
|
148366
|
-
* ## What an empty answer means
|
|
148367
|
-
*
|
|
148368
|
-
* A day the CALENDAR names and the INDEX answers with nothing is a defect on
|
|
148369
|
-
* the camera, measured on 618: the calendar marks 3–20 September, days 6–20
|
|
148370
|
-
* list zero files on both streams, on both filters, on a hand-built window,
|
|
148371
|
-
* with 177 GB free. The reader reports `indexEmpty` for that day so the
|
|
148372
|
-
* provider can say `index-empty` — never "no clips", which would be a lie
|
|
148373
|
-
* about a card that is full of them (D549 18).
|
|
148374
|
-
*/
|
|
148375
|
-
/**
|
|
148376
148768
|
* Locate the disk record structurally.
|
|
148377
148769
|
*
|
|
148378
148770
|
* The library's declared `HddInfoListConfig` and the captured wire disagree —
|
|
@@ -148457,6 +148849,30 @@ function monthsBetween(sinceMs, untilMs, timeZone) {
|
|
|
148457
148849
|
}
|
|
148458
148850
|
return out;
|
|
148459
148851
|
}
|
|
148852
|
+
/**
|
|
148853
|
+
* Is this file still being written?
|
|
148854
|
+
*
|
|
148855
|
+
* A Reolink recording is named `<prefix>_DST<date>_<start>_<end>_<hex>_<hex>`,
|
|
148856
|
+
* and while it is open the camera writes the END stamp as `000000` and the
|
|
148857
|
+
* size as `0` — measured on 592, 2026-09-20:
|
|
148858
|
+
* `RecS03_DST20260920_192927_000000_2B1E808_0.mp4`. Its `<endTime>` block is
|
|
148859
|
+
* FROZEN at whatever the index entry held when it was created (19:29:56, read
|
|
148860
|
+
* three times over fifteen minutes while the file grew), so a duration drawn
|
|
148861
|
+
* from it is a lie about the one row the operator sees first.
|
|
148862
|
+
*
|
|
148863
|
+
* BOTH signals are required. A recording that happens to end at midnight is
|
|
148864
|
+
* named `_000000_` and is closed; its bytes are what separate the two. Nothing
|
|
148865
|
+
* on the wire distinguishes them otherwise, and a false positive here would
|
|
148866
|
+
* replace a correct duration with `Recording…` for ever. Re-measured
|
|
148867
|
+
* read-only the same evening: of 355 sub and 355 main files on that camera's
|
|
148868
|
+
* day, zero carried a `_000000_` segment and zero carried a size of 0.
|
|
148869
|
+
*/
|
|
148870
|
+
function isFileStillBeingWritten(file) {
|
|
148871
|
+
if (file.sizeBytes !== void 0 && file.sizeBytes > 0) return false;
|
|
148872
|
+
return OPEN_END_STAMP.test(file.fileName);
|
|
148873
|
+
}
|
|
148874
|
+
/** The end-stamp field of an open recording, between its own separators. */
|
|
148875
|
+
var OPEN_END_STAMP = /_000000_/;
|
|
148460
148876
|
function handleOf(file) {
|
|
148461
148877
|
const id = file.id;
|
|
148462
148878
|
return id !== void 0 && id !== "" ? id : file.fileName;
|
|
@@ -148497,139 +148913,123 @@ async function listDayRows(api, input) {
|
|
|
148497
148913
|
...pair.sub.sizeBytes !== void 0 ? { subBytes: pair.sub.sizeBytes } : {},
|
|
148498
148914
|
...pair.main !== void 0 ? { mainFileName: handleOf(pair.main) } : {},
|
|
148499
148915
|
...pair.main?.sizeBytes !== void 0 ? { mainBytes: pair.main.sizeBytes } : {},
|
|
148916
|
+
...pair.main?.startTime !== void 0 ? { mainStartMs: pair.main.startTime.getTime() } : {},
|
|
148500
148917
|
supportSub: pair.sub.supportSub !== false,
|
|
148501
|
-
nativeTypes: parseNativeTypes(pair.sub.recordType)
|
|
148918
|
+
nativeTypes: parseNativeTypes(pair.sub.recordType),
|
|
148919
|
+
...isFileStillBeingWritten(pair.sub) ? { inProgress: true } : {}
|
|
148502
148920
|
};
|
|
148503
148921
|
}),
|
|
148504
148922
|
indexEmpty: subs.length === 0,
|
|
148505
148923
|
...mainSearchFailed !== void 0 ? { mainSearchFailed } : {}
|
|
148506
148924
|
};
|
|
148507
148925
|
}
|
|
148508
|
-
|
|
148509
|
-
|
|
148510
|
-
|
|
148511
|
-
* The
|
|
148512
|
-
*
|
|
148513
|
-
*
|
|
148514
|
-
|
|
148515
|
-
|
|
148516
|
-
|
|
148517
|
-
|
|
148518
|
-
|
|
148519
|
-
|
|
148520
|
-
|
|
148521
|
-
|
|
148522
|
-
|
|
148523
|
-
|
|
148524
|
-
* ## An event is not a promise of a clip
|
|
148525
|
-
*
|
|
148526
|
-
* A row whose `hasRecFile` is false has nothing to play, and both of the wrong
|
|
148527
|
-
* things are easy: dropping it under-reports a camera's events, and showing it
|
|
148528
|
-
* as playable fails on click. It is listed with `playable: false` and a
|
|
148529
|
-
* reason, which is what `ClipSchema.unplayableReason` exists for.
|
|
148530
|
-
*
|
|
148531
|
-
* **How often that bites is a per-camera fact.** Measured 2026-09-20 over 20
|
|
148532
|
-
* days: 3628 → 222 rows with 0 orphans, 3629 → 1 059 rows with 0 orphans, and
|
|
148533
|
-
* the orphans D549 19 describes are on 3825 — the child whose uid was never
|
|
148534
|
-
* persisted, which this provider cannot address at all today. So one camera's
|
|
148535
|
-
* events are all playable and another's are all not, and a surface built for
|
|
148536
|
-
* either extreme is wrong on the other.
|
|
148537
|
-
*
|
|
148538
|
-
* ## Where the file comes from
|
|
148539
|
-
*
|
|
148540
|
-
* `findEventLog` rows carry no file name — they name events, not files. So the
|
|
148541
|
-
* file is resolved by WINDOW CONTAINMENT against the day's `FileInfoList`
|
|
148542
|
-
* rows, which the `onboard` reader already fetches: an event whose
|
|
148543
|
-
* `[start, end]` lies inside a file's `[start − 3 s, end + 3 s]`. 28 of 29 on
|
|
148544
|
-
* the measured day; the one that did not is a real orphan (the hub refused its
|
|
148545
|
-
* CoverPreview too), not a join bug.
|
|
148546
|
-
*
|
|
148547
|
-
* Deliberately NOT used here: `searchAlarmVideos` (272/273/274). It does carry
|
|
148548
|
-
* a `fileName`, and its containment is measured at 1271/1271 — but it is
|
|
148549
|
-
* clamped to ONE camera-local day (an 18→19 September window silently answered
|
|
148550
|
-
* 462 windows all dated the 18th) where `searchEventLog` spans the whole
|
|
148551
|
-
* window in one open, and D549 19 chose the event list on the measured timing.
|
|
148552
|
-
* Its place is the per-window label upgrade for a clip's DETAIL (D549 20), and
|
|
148553
|
-
* that is a separate slice.
|
|
148554
|
-
*
|
|
148555
|
-
* ## `devices` is the selector; `chnbits` is inert
|
|
148556
|
-
*
|
|
148557
|
-
* Measured: `chnbits: 3` answered exactly the same 9 rows as the default, and
|
|
148558
|
-
* OMITTING `devices` is refused outright by the library ("no devices to
|
|
148559
|
-
* search") rather than answered emptily — a hub that has pushed nothing yet is
|
|
148560
|
-
* a refusal, not an empty result. This module always passes `devices`, never
|
|
148561
|
-
* sends `chnbits`, and never derives one from the other. With one child named,
|
|
148562
|
-
* foreign rows in the reply were 0; the uid filter below is belt and braces.
|
|
148563
|
-
*/
|
|
148564
|
-
/**
|
|
148565
|
-
* Every event for ONE child in the window, newest first, in ONE call.
|
|
148566
|
-
*
|
|
148567
|
-
* The event list is multi-day by construction — a per-day loop would pay the
|
|
148568
|
-
* open/close for each day and answer the same rows. The bound the caller owes
|
|
148569
|
-
* is on the WINDOW, and it has a number behind it: measured on one hub child,
|
|
148570
|
-
* ONE day is 267 ms and 20 days is 3.6–5.4 s. A day-range default is therefore
|
|
148571
|
-
* a UI decision, not a free one.
|
|
148572
|
-
*/
|
|
148573
|
-
async function listEventRows(api, input) {
|
|
148574
|
-
const entries = await api.searchEventLog({
|
|
148575
|
-
devices: [{ uid: input.uid }],
|
|
148576
|
-
start: new Date(input.since),
|
|
148577
|
-
end: new Date(input.until),
|
|
148578
|
-
timeZone: input.timeZone
|
|
148926
|
+
/**
|
|
148927
|
+
* Every alarm window of ONE camera-local day.
|
|
148928
|
+
*
|
|
148929
|
+
* The one-day clamp is the firmware's, not ours: an 18→19 September ask
|
|
148930
|
+
* answered 462 windows all dated the 18th. The caller owns the iteration, and
|
|
148931
|
+
* this is asked from inside the day loop the listing already runs.
|
|
148932
|
+
*/
|
|
148933
|
+
async function readAlarmWindows(api, input) {
|
|
148934
|
+
const { startMs, endMs } = dayBounds(input.day, input.timeZone);
|
|
148935
|
+
return await api.searchAlarmVideos({
|
|
148936
|
+
channel: input.channel,
|
|
148937
|
+
start: new Date(startMs),
|
|
148938
|
+
end: new Date(endMs),
|
|
148939
|
+
streamType: 1,
|
|
148940
|
+
timeZone: input.timeZone,
|
|
148941
|
+
...input.uid !== void 0 ? { uid: input.uid } : {}
|
|
148579
148942
|
});
|
|
148580
|
-
|
|
148581
|
-
|
|
148582
|
-
|
|
148583
|
-
|
|
148584
|
-
|
|
148585
|
-
|
|
148586
|
-
|
|
148587
|
-
|
|
148588
|
-
|
|
148589
|
-
|
|
148590
|
-
|
|
148591
|
-
|
|
148943
|
+
}
|
|
148944
|
+
/** The camera declined to class this file: `none`, or no `recordType` at all. */
|
|
148945
|
+
function fileCarriesNoClass(nativeTypes) {
|
|
148946
|
+
return nativeTypes.every((type) => type === "none");
|
|
148947
|
+
}
|
|
148948
|
+
/**
|
|
148949
|
+
* Should this day's labels be asked for separately?
|
|
148950
|
+
*
|
|
148951
|
+
* Only when NOTHING in the day carried a class. One file saying `md` is proof
|
|
148952
|
+
* that this camera writes `recordType`, and then the free answer is the
|
|
148953
|
+
* answer — paying 8–9 s to restate it is the cheap-question rule inverted.
|
|
148954
|
+
*/
|
|
148955
|
+
function dayNeedsAlarmLabels(rows) {
|
|
148956
|
+
return rows.length > 0 && rows.every((row) => fileCarriesNoClass(row.nativeTypes));
|
|
148957
|
+
}
|
|
148958
|
+
/**
|
|
148959
|
+
* Attach each window's `alarmType`s to the file whose START it names.
|
|
148960
|
+
*
|
|
148961
|
+
* A row the camera already classed is returned untouched: the file's own
|
|
148962
|
+
* `recordType` is the authority wherever it exists, and a per-window answer
|
|
148963
|
+
* must not narrow it. A window that names no listed file contributes nothing
|
|
148964
|
+
* and is COUNTED — it is never attached to the nearest neighbour, which is how
|
|
148965
|
+
* a surface ends up showing one recording's class on another's row.
|
|
148966
|
+
*/
|
|
148967
|
+
function joinAlarmLabels(rows, windows, timeZone) {
|
|
148968
|
+
const byStamp = /* @__PURE__ */ new Map();
|
|
148969
|
+
for (const window of windows) {
|
|
148970
|
+
const key = window.fileName.slice(-STAMP_LENGTH);
|
|
148971
|
+
const existing = byStamp.get(key);
|
|
148972
|
+
const target = existing ?? [];
|
|
148973
|
+
if (existing === void 0) byStamp.set(key, target);
|
|
148974
|
+
for (const type of window.alarmTypes) if (type !== "" && !target.includes(type)) target.push(type);
|
|
148592
148975
|
}
|
|
148593
|
-
|
|
148976
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
148977
|
+
let matched = 0;
|
|
148978
|
+
return {
|
|
148979
|
+
rows: rows.map((row) => {
|
|
148980
|
+
if (!fileCarriesNoClass(row.nativeTypes)) return row;
|
|
148981
|
+
const subKey = alarmJoinKey(row.startMs, timeZone);
|
|
148982
|
+
const mainKey = row.mainStartMs !== void 0 ? alarmJoinKey(row.mainStartMs, timeZone) : void 0;
|
|
148983
|
+
const key = byStamp.has(subKey) ? subKey : mainKey !== void 0 && byStamp.has(mainKey) ? mainKey : void 0;
|
|
148984
|
+
if (key === void 0) return row;
|
|
148985
|
+
const types = byStamp.get(key);
|
|
148986
|
+
if (types === void 0 || types.length === 0) return row;
|
|
148987
|
+
claimed.add(key);
|
|
148988
|
+
matched += 1;
|
|
148989
|
+
return {
|
|
148990
|
+
...row,
|
|
148991
|
+
nativeTypes: [...types]
|
|
148992
|
+
};
|
|
148993
|
+
}),
|
|
148994
|
+
matched,
|
|
148995
|
+
unmatchedWindows: byStamp.size - claimed.size
|
|
148996
|
+
};
|
|
148594
148997
|
}
|
|
148595
|
-
|
|
148596
|
-
|
|
148597
|
-
|
|
148598
|
-
|
|
148599
|
-
|
|
148600
|
-
|
|
148601
|
-
|
|
148602
|
-
|
|
148603
|
-
|
|
148604
|
-
|
|
148605
|
-
|
|
148606
|
-
|
|
148607
|
-
|
|
148608
|
-
|
|
148609
|
-
|
|
148610
|
-
return {
|
|
148611
|
-
...event,
|
|
148612
|
-
clipKey: file.clipKey,
|
|
148613
|
-
fileName: file.subFileName,
|
|
148614
|
-
playable: true
|
|
148615
|
-
};
|
|
148998
|
+
/** `YYYYMMDDHHMMSS` — 14 digits. */
|
|
148999
|
+
var STAMP_LENGTH = 14;
|
|
149000
|
+
var FORMATTER_BY_ZONE = /* @__PURE__ */ new Map();
|
|
149001
|
+
function formatterFor(timeZone) {
|
|
149002
|
+
const cached = FORMATTER_BY_ZONE.get(timeZone);
|
|
149003
|
+
if (cached !== void 0) return cached;
|
|
149004
|
+
const made = new Intl.DateTimeFormat("en-CA", {
|
|
149005
|
+
timeZone,
|
|
149006
|
+
year: "numeric",
|
|
149007
|
+
month: "2-digit",
|
|
149008
|
+
day: "2-digit",
|
|
149009
|
+
hour: "2-digit",
|
|
149010
|
+
minute: "2-digit",
|
|
149011
|
+
second: "2-digit",
|
|
149012
|
+
hour12: false
|
|
148616
149013
|
});
|
|
149014
|
+
FORMATTER_BY_ZONE.set(timeZone, made);
|
|
149015
|
+
return made;
|
|
148617
149016
|
}
|
|
148618
|
-
/**
|
|
148619
|
-
|
|
148620
|
-
|
|
149017
|
+
/**
|
|
149018
|
+
* The 14-digit tail of the short name a window carries — the file's start in
|
|
149019
|
+
* the CAMERA's wall clock. Never a UTC stamp: 00:30 Rome on the 17th is 22:30
|
|
149020
|
+
* UTC on the 16th, and a UTC key would file it under the wrong day and join it
|
|
149021
|
+
* to nothing.
|
|
149022
|
+
*/
|
|
149023
|
+
function alarmJoinKey(atMs, timeZone) {
|
|
149024
|
+
const parts = new Map(formatterFor(timeZone).formatToParts(new Date(atMs)).map((part) => [part.type, part.value]));
|
|
149025
|
+
return `${parts.get("year") ?? ""}${parts.get("month") ?? ""}${parts.get("day") ?? ""}${normaliseHour(parts.get("hour"))}${parts.get("minute") ?? ""}${parts.get("second") ?? ""}`;
|
|
149026
|
+
}
|
|
149027
|
+
/** `hourCycle: h23` still renders midnight as `24` in some ICU builds. */
|
|
149028
|
+
function normaliseHour(hour) {
|
|
149029
|
+
return hour === "24" ? "00" : hour ?? "";
|
|
148621
149030
|
}
|
|
148622
149031
|
//#endregion
|
|
148623
149032
|
//#region src/clips/reolink-videoclips-provider.ts
|
|
148624
|
-
/**
|
|
148625
|
-
* Which API the ONE source resolved to for a camera — reported on
|
|
148626
|
-
* `ClipSource.via` and on the listing log line.
|
|
148627
|
-
*
|
|
148628
|
-
* The CHOICE is made from what the camera is (`viaFor`); naming it is what
|
|
148629
|
-
* makes "why do these two cameras behave differently" answerable at all.
|
|
148630
|
-
*/
|
|
148631
|
-
var CATALOG_VIA_EVENTS = "events";
|
|
148632
|
-
var CATALOG_VIA_FILES = "files";
|
|
148633
149033
|
function createReolinkVideoclipsProvider(deps) {
|
|
148634
149034
|
/** Last known availability per `deviceId|source`, written by every read. */
|
|
148635
149035
|
const health = /* @__PURE__ */ new Map();
|
|
@@ -148753,7 +149153,7 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
148753
149153
|
error: result.mainSearchFailed
|
|
148754
149154
|
}
|
|
148755
149155
|
});
|
|
148756
|
-
return result.rows;
|
|
149156
|
+
return await labelDay(device, api, day, result.rows);
|
|
148757
149157
|
});
|
|
148758
149158
|
if (entry.clips.length === 0) emptyDays.push(day);
|
|
148759
149159
|
rows.push(...entry.clips);
|
|
@@ -148781,11 +149181,7 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
148781
149181
|
days: emptyDays
|
|
148782
149182
|
}
|
|
148783
149183
|
});
|
|
148784
|
-
noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD,
|
|
148785
|
-
state: "index-empty",
|
|
148786
|
-
reason: `The camera’s calendar marks ${emptyDays.length} day(s) with footage that its own index lists no files for: ${emptyDays.join(", ")}.`,
|
|
148787
|
-
catalogAsOf: deps.now()
|
|
148788
|
-
});
|
|
149184
|
+
noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, indexEmpty(emptyDays));
|
|
148789
149185
|
} else noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, {
|
|
148790
149186
|
state: "ok",
|
|
148791
149187
|
catalogAsOf: deps.now()
|
|
@@ -148793,140 +149189,55 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
148793
149189
|
return rows;
|
|
148794
149190
|
};
|
|
148795
149191
|
/**
|
|
148796
|
-
*
|
|
149192
|
+
* Give a day's rows a class when the FILES carried none.
|
|
148797
149193
|
*
|
|
148798
|
-
*
|
|
148799
|
-
*
|
|
148800
|
-
*
|
|
148801
|
-
*
|
|
148802
|
-
*
|
|
148803
|
-
*
|
|
149194
|
+
* The gate is what the day actually said, never what the camera is: a hub
|
|
149195
|
+
* child's files are `recordType: none` on every row (measured), a
|
|
149196
|
+
* standalone's carry `md,people,dog_cat,sched`, and a camera that changes
|
|
149197
|
+
* firmware changes which of those it is without this code learning a new
|
|
149198
|
+
* topology. The cheap question comes first (D54) — on 592 an alarm search
|
|
149199
|
+
* costs 8.2–9.5 s for 1 271 windows to restate labels already on the row.
|
|
148804
149200
|
*
|
|
148805
|
-
*
|
|
148806
|
-
*
|
|
148807
|
-
*
|
|
148808
|
-
* rows still list, each saying why it cannot play.
|
|
149201
|
+
* A refusal costs the LABELS and nothing else. Losing the day because its
|
|
149202
|
+
* class could not be read would be the upgrade destroying the thing it
|
|
149203
|
+
* upgrades — the same rule the MAIN search already lives by.
|
|
148809
149204
|
*/
|
|
148810
|
-
const
|
|
149205
|
+
const labelDay = async (device, api, day, rows) => {
|
|
148811
149206
|
const tags = { deviceId: device.deviceId };
|
|
148812
|
-
if (!
|
|
148813
|
-
const uid = device.uid;
|
|
148814
|
-
if (uid === void 0) {
|
|
148815
|
-
deps.logger.warn("videoclips: no uid for this camera — the hub cannot be asked about it", {
|
|
148816
|
-
tags,
|
|
148817
|
-
meta: {
|
|
148818
|
-
branch: "uid-missing",
|
|
148819
|
-
via: CATALOG_VIA_EVENTS
|
|
148820
|
-
}
|
|
148821
|
-
});
|
|
148822
|
-
noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, {
|
|
148823
|
-
state: "unreachable",
|
|
148824
|
-
reason: UID_MISSING_REASON
|
|
148825
|
-
});
|
|
148826
|
-
return [];
|
|
148827
|
-
}
|
|
148828
|
-
let api;
|
|
148829
|
-
try {
|
|
148830
|
-
api = await device.getApi();
|
|
148831
|
-
} catch (err) {
|
|
148832
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
148833
|
-
deps.logger.warn("videoclips: could not reach the hub", {
|
|
148834
|
-
tags,
|
|
148835
|
-
meta: {
|
|
148836
|
-
branch: "camera-refused",
|
|
148837
|
-
via: CATALOG_VIA_EVENTS,
|
|
148838
|
-
error: reason
|
|
148839
|
-
}
|
|
148840
|
-
});
|
|
148841
|
-
noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, {
|
|
148842
|
-
state: "unreachable",
|
|
148843
|
-
reason
|
|
148844
|
-
});
|
|
148845
|
-
return [];
|
|
148846
|
-
}
|
|
149207
|
+
if (!dayNeedsAlarmLabels(rows)) return rows;
|
|
148847
149208
|
const startedAt = deps.now();
|
|
148848
|
-
let
|
|
148849
|
-
let joined = [];
|
|
149209
|
+
let windows;
|
|
148850
149210
|
try {
|
|
148851
|
-
|
|
148852
|
-
uid,
|
|
149211
|
+
windows = await readAlarmWindows(api, {
|
|
148853
149212
|
channel: device.channel,
|
|
149213
|
+
uid: device.uid,
|
|
148854
149214
|
timeZone: device.timeZone,
|
|
148855
|
-
|
|
148856
|
-
until
|
|
149215
|
+
day
|
|
148857
149216
|
});
|
|
148858
|
-
events = rows;
|
|
148859
|
-
joined = joinEventsToFiles(rows, await readOnboard(device, since, until).catch(() => []));
|
|
148860
149217
|
} catch (err) {
|
|
148861
|
-
|
|
148862
|
-
deps.logger.warn("videoclips: the hub event log refused", {
|
|
149218
|
+
deps.logger.warn("videoclips: the label search refused — the day lists without classes", {
|
|
148863
149219
|
tags,
|
|
148864
149220
|
meta: {
|
|
148865
149221
|
branch: "camera-refused",
|
|
148866
|
-
|
|
148867
|
-
error:
|
|
149222
|
+
day,
|
|
149223
|
+
error: err instanceof Error ? err.message : String(err)
|
|
148868
149224
|
}
|
|
148869
149225
|
});
|
|
148870
|
-
|
|
148871
|
-
state: "unreachable",
|
|
148872
|
-
reason
|
|
148873
|
-
});
|
|
148874
|
-
return [];
|
|
149226
|
+
return rows;
|
|
148875
149227
|
}
|
|
148876
|
-
const
|
|
148877
|
-
deps.logger.info("videoclips: read the
|
|
149228
|
+
const joined = joinAlarmLabels(rows, windows, device.timeZone);
|
|
149229
|
+
deps.logger.info("videoclips: read the classes a day of files does not carry", {
|
|
148878
149230
|
tags,
|
|
148879
149231
|
meta: {
|
|
148880
|
-
|
|
148881
|
-
|
|
148882
|
-
|
|
148883
|
-
|
|
148884
|
-
|
|
149232
|
+
day,
|
|
149233
|
+
windows: windows.length,
|
|
149234
|
+
rows: rows.length,
|
|
149235
|
+
labelled: joined.matched,
|
|
149236
|
+
unmatchedWindows: joined.unmatchedWindows,
|
|
149237
|
+
elapsedMs: deps.now() - startedAt
|
|
148885
149238
|
}
|
|
148886
149239
|
});
|
|
148887
|
-
|
|
148888
|
-
state: "ok",
|
|
148889
|
-
catalogAsOf: deps.now()
|
|
148890
|
-
});
|
|
148891
|
-
return joined;
|
|
148892
|
-
};
|
|
148893
|
-
/**
|
|
148894
|
-
* An events-view row as a `Clip`.
|
|
148895
|
-
*
|
|
148896
|
-
* The id carries the event WINDOW as well as the file, because a file holds
|
|
148897
|
-
* 1–13 windows and two events of one recording must not collapse into one
|
|
148898
|
-
* row. A row with nothing to play carries `playable: false` and its reason,
|
|
148899
|
-
* and offers no thumbnail mint — the hub refused the CoverPreview for
|
|
148900
|
-
* exactly that orphan event, so an offer here would be a tile that spins and
|
|
148901
|
-
* then fails.
|
|
148902
|
-
*/
|
|
148903
|
-
const toEventClip = async (device, row, catalogAsOf) => {
|
|
148904
|
-
const classification = classifyNativeTypes(row.nativeTypes);
|
|
148905
|
-
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" } };
|
|
148906
|
-
return {
|
|
148907
|
-
id: mintClipId({
|
|
148908
|
-
source: CLIP_SOURCE_ONBOARD,
|
|
148909
|
-
channel: row.channel,
|
|
148910
|
-
fileName: row.fileName ?? `event:${row.startMs}-${row.endMs}`,
|
|
148911
|
-
window: {
|
|
148912
|
-
startMs: row.startMs,
|
|
148913
|
-
endMs: row.endMs
|
|
148914
|
-
}
|
|
148915
|
-
}),
|
|
148916
|
-
source: CLIP_SOURCE_ONBOARD,
|
|
148917
|
-
kind: classification.kind,
|
|
148918
|
-
timeRange: {
|
|
148919
|
-
startMs: row.startMs,
|
|
148920
|
-
endMs: row.endMs
|
|
148921
|
-
},
|
|
148922
|
-
...classification.labels.length > 0 ? { labels: [...classification.labels] } : {},
|
|
148923
|
-
...classification.behaviours.length > 0 ? { behaviours: [...classification.behaviours] } : {},
|
|
148924
|
-
...classification.nativeTypes.length > 0 ? { nativeTypes: [...classification.nativeTypes] } : {},
|
|
148925
|
-
...still,
|
|
148926
|
-
...catalogAsOf !== void 0 ? { catalogAsOf } : {},
|
|
148927
|
-
playable: row.playable,
|
|
148928
|
-
...row.unplayableReason !== void 0 ? { unplayableReason: row.unplayableReason } : {}
|
|
148929
|
-
};
|
|
149240
|
+
return joined.rows;
|
|
148930
149241
|
};
|
|
148931
149242
|
const toClip = async (device, source, row, catalogAsOf) => {
|
|
148932
149243
|
rowsByKey.set(`${device.deviceId}|${row.clipKey}`, row);
|
|
@@ -148945,6 +149256,7 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
148945
149256
|
startMs: row.startMs,
|
|
148946
149257
|
endMs: row.endMs
|
|
148947
149258
|
},
|
|
149259
|
+
...row.inProgress === true ? { inProgress: true } : {},
|
|
148948
149260
|
...classification.labels.length > 0 ? { labels: [...classification.labels] } : {},
|
|
148949
149261
|
...classification.behaviours.length > 0 ? { behaviours: [...classification.behaviours] } : {},
|
|
148950
149262
|
...classification.nativeTypes.length > 0 ? { nativeTypes: [...classification.nativeTypes] } : {},
|
|
@@ -148975,36 +149287,26 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
148975
149287
|
listSources: async ({ deviceId }) => {
|
|
148976
149288
|
const device = await deps.resolveDevice(deviceId);
|
|
148977
149289
|
if (device === null) return [];
|
|
148978
|
-
const via = viaFor(device);
|
|
148979
149290
|
return [{
|
|
148980
149291
|
source: CLIP_SOURCE_ONBOARD,
|
|
148981
149292
|
addonId: REOLINK_ADDON_ID,
|
|
148982
149293
|
label: "Camera storage",
|
|
148983
|
-
|
|
148984
|
-
availability: await resolveAvailability(device, via)
|
|
149294
|
+
availability: await resolveAvailability(device)
|
|
148985
149295
|
}];
|
|
148986
149296
|
},
|
|
148987
149297
|
listClips: async ({ deviceId, since, until, limit }) => {
|
|
148988
149298
|
const device = await deps.resolveDevice(deviceId);
|
|
148989
149299
|
if (device === null) return [];
|
|
148990
149300
|
const tags = { deviceId };
|
|
148991
|
-
const
|
|
148992
|
-
const
|
|
148993
|
-
|
|
148994
|
-
const asOf = health.get(healthKey(deviceId, CLIP_SOURCE_ONBOARD))?.availability.catalogAsOf;
|
|
148995
|
-
return await Promise.all(events.map((row) => toEventClip(device, row, asOf)));
|
|
148996
|
-
})() : await (async () => {
|
|
148997
|
-
const rows = await readOnboard(device, since, until);
|
|
148998
|
-
const asOf = health.get(healthKey(deviceId, CLIP_SOURCE_ONBOARD))?.availability.catalogAsOf;
|
|
148999
|
-
return await Promise.all(rows.toSorted((a, b) => b.startMs - a.startMs).map((row) => toClip(device, CLIP_SOURCE_ONBOARD, row, asOf)));
|
|
149000
|
-
})()).toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
149301
|
+
const rows = await readOnboard(device, since, until);
|
|
149302
|
+
const asOf = health.get(healthKey(deviceId, CLIP_SOURCE_ONBOARD))?.availability.catalogAsOf;
|
|
149303
|
+
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);
|
|
149001
149304
|
const capped = limit !== void 0 ? ordered.slice(0, limit) : ordered;
|
|
149002
149305
|
const vouched = capped.filter((c) => c.thumbnail !== void 0).length;
|
|
149003
149306
|
deps.logger.info("videoclips: listed a Reolink catalog", {
|
|
149004
149307
|
tags,
|
|
149005
149308
|
meta: {
|
|
149006
149309
|
source: CLIP_SOURCE_ONBOARD,
|
|
149007
|
-
via,
|
|
149008
149310
|
clips: capped.length,
|
|
149009
149311
|
truncated: limit !== void 0 && ordered.length > limit,
|
|
149010
149312
|
unplayable: capped.filter((c) => c.playable === false).length,
|
|
@@ -149013,6 +149315,18 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
149013
149315
|
availability: health.get(healthKey(deviceId, CLIP_SOURCE_ONBOARD))?.availability.state
|
|
149014
149316
|
}
|
|
149015
149317
|
});
|
|
149318
|
+
const availability = health.get(healthKey(deviceId, CLIP_SOURCE_ONBOARD))?.availability;
|
|
149319
|
+
if (capped.length === 0 && availability !== void 0 && availability.state !== "ok") {
|
|
149320
|
+
deps.logger.warn("videoclips: nothing to list, and the source cannot answer", {
|
|
149321
|
+
tags,
|
|
149322
|
+
meta: {
|
|
149323
|
+
branch: "refused-empty",
|
|
149324
|
+
state: availability.state,
|
|
149325
|
+
reason: availability.reason ?? null
|
|
149326
|
+
}
|
|
149327
|
+
});
|
|
149328
|
+
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}` : "."));
|
|
149329
|
+
}
|
|
149016
149330
|
return capped;
|
|
149017
149331
|
},
|
|
149018
149332
|
getClipPlayback: async ({ deviceId, clipId, profile }) => {
|
|
@@ -149021,7 +149335,6 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
149021
149335
|
const device = await deps.resolveDevice(deviceId);
|
|
149022
149336
|
if (device === null) throw new Error(`videoclips: device ${deviceId} is not a Reolink camera`);
|
|
149023
149337
|
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.`);
|
|
149024
|
-
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.`);
|
|
149025
149338
|
const row = rowsByKey.get(`${deviceId}|${clipKeyFor(parsed.channel, parsed.fileName)}`) ?? null;
|
|
149026
149339
|
const wantsMain = profile === "high" && row?.mainFileName !== void 0;
|
|
149027
149340
|
const fileName = wantsMain ? row?.mainFileName ?? parsed.fileName : parsed.fileName;
|
|
@@ -149040,46 +149353,350 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
149040
149353
|
}
|
|
149041
149354
|
};
|
|
149042
149355
|
/**
|
|
149043
|
-
*
|
|
149044
|
-
*
|
|
149045
|
-
*
|
|
149046
|
-
*
|
|
149047
|
-
*
|
|
149048
|
-
*
|
|
149049
|
-
*
|
|
149050
|
-
*
|
|
149051
|
-
|
|
149052
|
-
|
|
149053
|
-
|
|
149054
|
-
|
|
149055
|
-
|
|
149356
|
+
* What a source is saying right now — MEASURED, never defaulted.
|
|
149357
|
+
*
|
|
149358
|
+
* A listing that already ran is the better answer and wins: `listClips`
|
|
149359
|
+
* asked the whole window and this probe asks one day, so re-deciding from
|
|
149360
|
+
* the narrower question would throw away 618's other nineteen empty days.
|
|
149361
|
+
* With nothing established, the probe runs — and `ok` is only ever returned
|
|
149362
|
+
* because something was really read.
|
|
149363
|
+
*
|
|
149364
|
+
* ## What it costs, per call, and why
|
|
149365
|
+
*
|
|
149366
|
+
* This used to answer `ok` after at most one storage command, and `ok` is
|
|
149367
|
+
* the one state whose list may be read as complete. On the live hub, on the
|
|
149368
|
+
* cold runners a deploy had just produced, that default made 640 (no SD
|
|
149369
|
+
* card) and 618 (an index that lists nothing for days its own calendar
|
|
149370
|
+
* marks) both answer `ok` and then an empty list — the exact lie this line
|
|
149371
|
+
* of work exists to end. So `listSources` pays for its answer:
|
|
149372
|
+
*
|
|
149373
|
+
* - **a standalone battery camera: NOTHING, ever.** No `getApi`, no command,
|
|
149374
|
+
* not even the cheap one — on UDP the login IS the wake. `sleeping`, with
|
|
149375
|
+
* the age of whatever is cached. The gate is `isBattery`, never the
|
|
149376
|
+
* BELIEF `sleeping`: that belief is `state.battery.sleeping ?? false`, so
|
|
149377
|
+
* a slice that has not restored reads as awake, and a probe on it is a
|
|
149378
|
+
* wake nobody asked for.
|
|
149379
|
+
* - **a hub child: one `getDayRecords` through the parent's existing socket**
|
|
149380
|
+
* (65 ms measured on 3628). The child never hears it; the storage question
|
|
149381
|
+
* is the HUB's and is not asked.
|
|
149382
|
+
* - **a mains standalone: `getHddInfoList` (ms) + `getDayRecords`
|
|
149383
|
+
* (38 ms–1.9 s) + ONE day listing of the NEWEST day the calendar claims**
|
|
149384
|
+
* (0.3–6 s, and cache-backed — `serveAndRevalidate`, so a day already
|
|
149385
|
+
* listed costs nothing and a day listed here is free for the `listClips`
|
|
149386
|
+
* that follows). The day listing is what distinguishes `index-empty` from
|
|
149387
|
+
* `ok`, and nothing cheaper can: `no-storage` is the cheap question and it
|
|
149388
|
+
* comes first precisely so a camera with no card never pays the rest.
|
|
149389
|
+
*
|
|
149390
|
+
* The calendar is read for the CURRENT month only. A picker asks "can this
|
|
149391
|
+
* source answer", not "what does it hold since April", and the measured
|
|
149392
|
+
* defect (618) is in the month the operator is looking at.
|
|
149393
|
+
*/
|
|
149394
|
+
async function resolveAvailability(device) {
|
|
149056
149395
|
const known = health.get(healthKey(device.deviceId, CLIP_SOURCE_ONBOARD))?.availability;
|
|
149057
149396
|
if (known !== void 0) return known;
|
|
149058
|
-
|
|
149059
|
-
|
|
149060
|
-
|
|
149061
|
-
|
|
149062
|
-
|
|
149063
|
-
|
|
149064
|
-
|
|
149397
|
+
const availability = await probeAvailability(device);
|
|
149398
|
+
noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, availability);
|
|
149399
|
+
return availability;
|
|
149400
|
+
}
|
|
149401
|
+
async function probeAvailability(device) {
|
|
149402
|
+
const tags = { deviceId: device.deviceId };
|
|
149403
|
+
if (device.isBattery && !device.isHubChild) {
|
|
149404
|
+
const asOf = newestCachedAsOf(device.deviceId);
|
|
149405
|
+
deps.logger.warn("videoclips: not probing a standalone battery camera — login is the wake", {
|
|
149406
|
+
tags,
|
|
149407
|
+
meta: {
|
|
149408
|
+
branch: "sleeping",
|
|
149409
|
+
believedSleeping: device.sleeping,
|
|
149410
|
+
confirmedAt: asOf
|
|
149411
|
+
}
|
|
149412
|
+
});
|
|
149413
|
+
return {
|
|
149414
|
+
state: "sleeping",
|
|
149415
|
+
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.",
|
|
149416
|
+
...asOf !== void 0 ? { catalogAsOf: asOf } : {}
|
|
149417
|
+
};
|
|
149418
|
+
}
|
|
149419
|
+
if (uidIsMissing(device)) {
|
|
149420
|
+
deps.logger.warn("videoclips: no uid for this camera — its catalog cannot be addressed", {
|
|
149421
|
+
tags,
|
|
149422
|
+
meta: {
|
|
149423
|
+
branch: "uid-missing",
|
|
149424
|
+
probe: true
|
|
149425
|
+
}
|
|
149426
|
+
});
|
|
149427
|
+
return {
|
|
149428
|
+
state: "unreachable",
|
|
149429
|
+
reason: UID_MISSING_REASON
|
|
149430
|
+
};
|
|
149431
|
+
}
|
|
149432
|
+
let api;
|
|
149433
|
+
try {
|
|
149434
|
+
api = await device.getApi();
|
|
149435
|
+
} catch (err) {
|
|
149436
|
+
return unreachableFrom(device, "camera-refused", err);
|
|
149437
|
+
}
|
|
149438
|
+
if (!device.isHubChild) {
|
|
149439
|
+
const storage = await readStorageState(api);
|
|
149440
|
+
if (storage.state === "absent") {
|
|
149441
|
+
deps.logger.info("videoclips: no SD card mounted — nothing to list", {
|
|
149442
|
+
tags,
|
|
149443
|
+
meta: {
|
|
149444
|
+
branch: "no-storage",
|
|
149445
|
+
probe: true
|
|
149446
|
+
}
|
|
149447
|
+
});
|
|
149448
|
+
return {
|
|
149449
|
+
state: "no-storage",
|
|
149450
|
+
reason: "The camera reports no mounted storage (HddInfo mount 0)."
|
|
149451
|
+
};
|
|
149452
|
+
}
|
|
149453
|
+
if (storage.state === "unknown") {
|
|
149454
|
+
deps.logger.warn("videoclips: the storage question was not answered", {
|
|
149455
|
+
tags,
|
|
149456
|
+
meta: {
|
|
149457
|
+
branch: "camera-refused",
|
|
149458
|
+
probe: true,
|
|
149459
|
+
error: storage.reason ?? null
|
|
149460
|
+
}
|
|
149461
|
+
});
|
|
149462
|
+
return {
|
|
149463
|
+
state: "unreachable",
|
|
149464
|
+
reason: storage.reason ?? "The camera did not answer the storage question (HddInfo unreadable)."
|
|
149465
|
+
};
|
|
149466
|
+
}
|
|
149467
|
+
}
|
|
149468
|
+
const today = wallClockDayKey(deps.now(), device.timeZone);
|
|
149469
|
+
let calendar;
|
|
149470
|
+
try {
|
|
149471
|
+
calendar = await readDayIndex(api, {
|
|
149472
|
+
channel: device.channel,
|
|
149473
|
+
uid: device.uid,
|
|
149474
|
+
months: [{
|
|
149475
|
+
year: Number(today.slice(0, 4)),
|
|
149476
|
+
month: Number(today.slice(5, 7))
|
|
149477
|
+
}]
|
|
149478
|
+
});
|
|
149479
|
+
} catch (err) {
|
|
149480
|
+
return unreachableFrom(device, "camera-refused", err);
|
|
149481
|
+
}
|
|
149482
|
+
const claimed = [...calendar].toSorted().toReversed();
|
|
149483
|
+
if (claimed.length === 0) return {
|
|
149484
|
+
state: "ok",
|
|
149485
|
+
catalogAsOf: deps.now()
|
|
149065
149486
|
};
|
|
149066
|
-
|
|
149487
|
+
const newest = claimed[0];
|
|
149488
|
+
if (newest === void 0) return {
|
|
149489
|
+
state: "ok",
|
|
149490
|
+
catalogAsOf: deps.now()
|
|
149491
|
+
};
|
|
149492
|
+
try {
|
|
149493
|
+
await deps.cache.serveAndRevalidate(device.deviceId, newest, { closed: newest !== today }, async () => (await listDayRows(api, {
|
|
149494
|
+
channel: device.channel,
|
|
149495
|
+
uid: device.uid,
|
|
149496
|
+
timeZone: device.timeZone,
|
|
149497
|
+
day: newest
|
|
149498
|
+
})).rows);
|
|
149499
|
+
} catch (err) {
|
|
149500
|
+
return unreachableFrom(device, "camera-refused", err);
|
|
149501
|
+
}
|
|
149502
|
+
const emptyClaimed = claimed.filter((day) => deps.cache.peek(device.deviceId, day)?.clips.length === 0);
|
|
149503
|
+
if (emptyClaimed.length > 0) {
|
|
149504
|
+
deps.logger.warn("videoclips: the calendar marks days the index will not list", {
|
|
149505
|
+
tags,
|
|
149506
|
+
meta: {
|
|
149507
|
+
branch: "index-empty",
|
|
149508
|
+
probe: true,
|
|
149509
|
+
days: emptyClaimed
|
|
149510
|
+
}
|
|
149511
|
+
});
|
|
149512
|
+
return indexEmpty(emptyClaimed);
|
|
149513
|
+
}
|
|
149514
|
+
return {
|
|
149515
|
+
state: "ok",
|
|
149516
|
+
catalogAsOf: deps.now()
|
|
149517
|
+
};
|
|
149518
|
+
}
|
|
149519
|
+
function unreachableFrom(device, branch, err) {
|
|
149520
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
149521
|
+
deps.logger.warn("videoclips: the source could not be probed", {
|
|
149522
|
+
tags: { deviceId: device.deviceId },
|
|
149523
|
+
meta: {
|
|
149524
|
+
branch,
|
|
149525
|
+
probe: true,
|
|
149526
|
+
error: reason
|
|
149527
|
+
}
|
|
149528
|
+
});
|
|
149529
|
+
return {
|
|
149067
149530
|
state: "unreachable",
|
|
149068
|
-
reason
|
|
149531
|
+
reason
|
|
149069
149532
|
};
|
|
149533
|
+
}
|
|
149534
|
+
/** When any day of this device's catalog was last confirmed, newest first. */
|
|
149535
|
+
function newestCachedAsOf(deviceId) {
|
|
149536
|
+
let newest;
|
|
149537
|
+
for (const day of deps.cache.daysHeld(deviceId)) {
|
|
149538
|
+
const asOf = deps.cache.peek(deviceId, day)?.catalogAsOf;
|
|
149539
|
+
if (asOf !== void 0 && (newest === void 0 || asOf > newest)) newest = asOf;
|
|
149540
|
+
}
|
|
149541
|
+
return newest;
|
|
149542
|
+
}
|
|
149543
|
+
/** The one wording for "the camera's calendar and its own index disagree". */
|
|
149544
|
+
function indexEmpty(days) {
|
|
149545
|
+
return {
|
|
149546
|
+
state: "index-empty",
|
|
149547
|
+
reason: `The camera’s calendar marks ${days.length} day(s) with footage that its own index lists no files for: ${days.join(", ")}.`,
|
|
149548
|
+
catalogAsOf: deps.now()
|
|
149549
|
+
};
|
|
149550
|
+
}
|
|
149551
|
+
}
|
|
149552
|
+
//#endregion
|
|
149553
|
+
//#region src/clips/clip-refresh.ts
|
|
149554
|
+
/** The settings key, global default and per-device override (D562). */
|
|
149555
|
+
var CLIP_CATALOG_REFRESH_MINUTES_KEY = "clipCatalogRefreshMinutes";
|
|
149556
|
+
/** The settings key, per device only, OFF by default (D562). */
|
|
149557
|
+
var CLIP_CATALOG_FORCED_WAKE_KEY = "clipCatalogForcedWake";
|
|
149558
|
+
/** A day. Past this the catalog's own TTLs are the thing doing the work. */
|
|
149559
|
+
var CLIP_CATALOG_REFRESH_MINUTES_MAX = 1440;
|
|
149560
|
+
/**
|
|
149561
|
+
* How much of the catalog one refresh confirms.
|
|
149562
|
+
*
|
|
149563
|
+
* A day. The listing cost is per DAY (0.3–6 s on a standalone, 267 ms for one
|
|
149564
|
+
* child-day on the hub) and a 20-day window measured 3.6–5.4 s per child, so a
|
|
149565
|
+
* timer that re-confirmed everything would spend the whole budget re-reading
|
|
149566
|
+
* days that cannot change. Older days are immutable and the cache holds them
|
|
149567
|
+
* under their own 24 h TTL.
|
|
149568
|
+
*/
|
|
149569
|
+
var CLIP_CATALOG_REFRESH_WINDOW_MS = 1440 * 6e4;
|
|
149570
|
+
/**
|
|
149571
|
+
* The mode for one camera. Pure, and decided from what the camera IS — never
|
|
149572
|
+
* from a belief about whether it is asleep right now: the gate has to hold for
|
|
149573
|
+
* a camera whose sleep slice has not restored, which reads "awake" and is how
|
|
149574
|
+
* a picker draw once logged into 640 (D561).
|
|
149575
|
+
*/
|
|
149576
|
+
function resolveClipRefreshMode(topology) {
|
|
149577
|
+
if (topology.isHubChild) return "via-hub";
|
|
149578
|
+
if (!topology.isBattery) return "direct";
|
|
149579
|
+
return topology.forcedWake ? "forced-wake" : "mark-stale";
|
|
149580
|
+
}
|
|
149581
|
+
/**
|
|
149582
|
+
* The refresh interval for one camera: the per-device override on top of the
|
|
149583
|
+
* global default, with **reset == unset == the default**.
|
|
149584
|
+
*
|
|
149585
|
+
* Never throws on a bad blob. An invalid value at either level resolves to the
|
|
149586
|
+
* level above it, so a store written by an older release cannot stop the timer
|
|
149587
|
+
* — the same cascade `stationary-settings.ts` uses, for the same reason.
|
|
149588
|
+
*/
|
|
149589
|
+
function resolveClipCatalogRefreshMinutes(globalValue, deviceValue) {
|
|
149590
|
+
return asRefreshMinutes(deviceValue) ?? asRefreshMinutes(globalValue) ?? 30;
|
|
149591
|
+
}
|
|
149592
|
+
function asRefreshMinutes(value) {
|
|
149593
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
149594
|
+
const rounded = Math.round(value);
|
|
149595
|
+
if (rounded === 0) return 0;
|
|
149596
|
+
if (rounded < 5) return null;
|
|
149597
|
+
if (rounded > 1440) return null;
|
|
149598
|
+
return rounded;
|
|
149599
|
+
}
|
|
149600
|
+
/** Is the operator's forced-wake switch on for this camera? */
|
|
149601
|
+
function resolveClipCatalogForcedWake(value) {
|
|
149602
|
+
return value === true;
|
|
149603
|
+
}
|
|
149604
|
+
/**
|
|
149605
|
+
* The timer. `tick()` is the whole surface, so a test drives it from COLD and
|
|
149606
|
+
* the scheduler owns no clock of its own — the addon supplies the interval.
|
|
149607
|
+
*/
|
|
149608
|
+
var ClipCatalogRefreshScheduler = class {
|
|
149609
|
+
deps;
|
|
149610
|
+
/** Last time an interval was SPENT on a camera, successful or not. */
|
|
149611
|
+
lastRunAt = /* @__PURE__ */ new Map();
|
|
149612
|
+
/** Cameras whose refresh has not come back yet. */
|
|
149613
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
149614
|
+
constructor(deps) {
|
|
149615
|
+
this.deps = deps;
|
|
149616
|
+
}
|
|
149617
|
+
async tick() {
|
|
149618
|
+
const now = this.deps.now();
|
|
149619
|
+
const due = this.deps.listTargets().filter((target) => this.isDue(target, now));
|
|
149620
|
+
for (const target of due) await this.run(target, now);
|
|
149621
|
+
}
|
|
149622
|
+
isDue(target, now) {
|
|
149623
|
+
if (target.intervalMinutes <= 0) return false;
|
|
149624
|
+
if (this.inFlight.has(target.deviceId)) return false;
|
|
149625
|
+
const last = this.lastRunAt.get(target.deviceId);
|
|
149626
|
+
if (last === void 0) return true;
|
|
149627
|
+
return now - last >= target.intervalMinutes * 6e4;
|
|
149628
|
+
}
|
|
149629
|
+
async run(target, now) {
|
|
149630
|
+
const tags = { deviceId: target.deviceId };
|
|
149631
|
+
const mode = resolveClipRefreshMode(target);
|
|
149632
|
+
if (this.deps.hostBusy(target.hostKey)) {
|
|
149633
|
+
this.deps.logger.warn("videoclips: catalog refresh deferred — the host is serving a read", {
|
|
149634
|
+
tags,
|
|
149635
|
+
meta: {
|
|
149636
|
+
branch: "deferred-interactive",
|
|
149637
|
+
mode,
|
|
149638
|
+
host: target.hostKey
|
|
149639
|
+
}
|
|
149640
|
+
});
|
|
149641
|
+
return;
|
|
149642
|
+
}
|
|
149643
|
+
if (mode === "mark-stale") {
|
|
149644
|
+
this.deps.markStale(target.deviceId);
|
|
149645
|
+
this.lastRunAt.set(target.deviceId, now);
|
|
149646
|
+
this.deps.logger.info("videoclips: catalog marked stale — a timer cannot read a standalone battery camera", {
|
|
149647
|
+
tags,
|
|
149648
|
+
meta: {
|
|
149649
|
+
branch: "mark-stale",
|
|
149650
|
+
mode,
|
|
149651
|
+
refreshesOn: "onWakeTransition",
|
|
149652
|
+
intervalMinutes: target.intervalMinutes
|
|
149653
|
+
}
|
|
149654
|
+
});
|
|
149655
|
+
return;
|
|
149656
|
+
}
|
|
149657
|
+
this.lastRunAt.set(target.deviceId, now);
|
|
149658
|
+
this.inFlight.add(target.deviceId);
|
|
149070
149659
|
try {
|
|
149071
|
-
if (
|
|
149072
|
-
|
|
149073
|
-
|
|
149074
|
-
|
|
149075
|
-
|
|
149660
|
+
if (mode === "forced-wake") try {
|
|
149661
|
+
await this.deps.wake(target.deviceId);
|
|
149662
|
+
} catch (err) {
|
|
149663
|
+
this.deps.markStale(target.deviceId);
|
|
149664
|
+
this.deps.logger.warn("videoclips: the authorised wake did not bring the camera up — catalog left stale", {
|
|
149665
|
+
tags,
|
|
149666
|
+
meta: {
|
|
149667
|
+
branch: "forced-wake-failed",
|
|
149668
|
+
mode,
|
|
149669
|
+
error: errMessage$1(err)
|
|
149670
|
+
}
|
|
149671
|
+
});
|
|
149672
|
+
return;
|
|
149673
|
+
}
|
|
149674
|
+
await this.deps.refresh(target.deviceId);
|
|
149675
|
+
this.deps.logger.info("videoclips: catalog refreshed", {
|
|
149676
|
+
tags,
|
|
149677
|
+
meta: {
|
|
149678
|
+
branch: "refreshed",
|
|
149679
|
+
mode,
|
|
149680
|
+
elapsedMs: this.deps.now() - now
|
|
149681
|
+
}
|
|
149682
|
+
});
|
|
149076
149683
|
} catch (err) {
|
|
149077
|
-
|
|
149078
|
-
|
|
149079
|
-
|
|
149080
|
-
|
|
149684
|
+
this.deps.markStale(target.deviceId);
|
|
149685
|
+
this.deps.logger.warn("videoclips: catalog refresh failed — the catalog stays as it was", {
|
|
149686
|
+
tags,
|
|
149687
|
+
meta: {
|
|
149688
|
+
branch: "refresh-failed",
|
|
149689
|
+
mode,
|
|
149690
|
+
error: errMessage$1(err)
|
|
149691
|
+
}
|
|
149692
|
+
});
|
|
149693
|
+
} finally {
|
|
149694
|
+
this.inFlight.delete(target.deviceId);
|
|
149081
149695
|
}
|
|
149082
149696
|
}
|
|
149697
|
+
};
|
|
149698
|
+
function errMessage$1(err) {
|
|
149699
|
+
return err instanceof Error ? err.message : String(err);
|
|
149083
149700
|
}
|
|
149084
149701
|
function createClipService(deps) {
|
|
149085
149702
|
const now = deps.now ?? (() => Date.now());
|
|
@@ -149110,23 +149727,11 @@ function createClipService(deps) {
|
|
|
149110
149727
|
};
|
|
149111
149728
|
const mintThumbnail = async (deviceId, clipKey, lane) => {
|
|
149112
149729
|
const device = await deps.resolveDevice(deviceId);
|
|
149113
|
-
if (device === null) return {
|
|
149114
|
-
|
|
149115
|
-
|
|
149116
|
-
};
|
|
149117
|
-
if (device.isBattery && device.sleeping && !device.isHubChild) return {
|
|
149118
|
-
ok: false,
|
|
149119
|
-
reason: "sleeping: a still would wake this standalone battery camera"
|
|
149120
|
-
};
|
|
149121
|
-
if (uidIsMissing(device)) return {
|
|
149122
|
-
ok: false,
|
|
149123
|
-
reason: UID_MISSING_REASON
|
|
149124
|
-
};
|
|
149730
|
+
if (device === null) return refuseStill("unknown-device", `unknown-device: ${deviceId} is not a Reolink camera here`);
|
|
149731
|
+
if (device.isBattery && device.sleeping && !device.isHubChild) return refuseStill("sleeping", "sleeping: a still would wake this standalone battery camera");
|
|
149732
|
+
if (uidIsMissing(device)) return refuseStill("uid-missing", UID_MISSING_REASON);
|
|
149125
149733
|
const row = rowFor(deviceId, clipKey);
|
|
149126
|
-
if (row === null) return
|
|
149127
|
-
ok: false,
|
|
149128
|
-
reason: "no-catalog-row: list this day before asking for its stills"
|
|
149129
|
-
};
|
|
149734
|
+
if (row === null) return refuseStill("no-catalog-row", "no-catalog-row: list this day before asking for its stills");
|
|
149130
149735
|
try {
|
|
149131
149736
|
const bytes = await queue.submit({
|
|
149132
149737
|
host: device.hostKey,
|
|
@@ -149143,10 +149748,7 @@ function createClipService(deps) {
|
|
|
149143
149748
|
});
|
|
149144
149749
|
}
|
|
149145
149750
|
});
|
|
149146
|
-
if (bytes.byteLength === 0) return
|
|
149147
|
-
ok: false,
|
|
149148
|
-
reason: "no-keyframe: the camera returned an empty frame"
|
|
149149
|
-
};
|
|
149751
|
+
if (bytes.byteLength === 0) return refuseStill("no-keyframe", "no-keyframe: the camera returned an empty frame");
|
|
149150
149752
|
await thumbs.put(deviceId, clipKey, bytes);
|
|
149151
149753
|
return {
|
|
149152
149754
|
ok: true,
|
|
@@ -149154,6 +149756,19 @@ function createClipService(deps) {
|
|
|
149154
149756
|
};
|
|
149155
149757
|
} catch (err) {
|
|
149156
149758
|
const reason = err instanceof Error ? err.message : String(err);
|
|
149759
|
+
const deferred = queueRefusal(err);
|
|
149760
|
+
if (deferred !== null) {
|
|
149761
|
+
deps.logger.debug("videoclips: a thumbnail mint was deferred", {
|
|
149762
|
+
tags: { deviceId },
|
|
149763
|
+
meta: {
|
|
149764
|
+
clipKey,
|
|
149765
|
+
lane,
|
|
149766
|
+
code: deferred.code,
|
|
149767
|
+
retryAfterMs: deferred.retryAfterMs
|
|
149768
|
+
}
|
|
149769
|
+
});
|
|
149770
|
+
return deferred;
|
|
149771
|
+
}
|
|
149157
149772
|
deps.logger.warn("videoclips: a thumbnail mint refused", {
|
|
149158
149773
|
tags: { deviceId },
|
|
149159
149774
|
meta: {
|
|
@@ -149163,11 +149778,97 @@ function createClipService(deps) {
|
|
|
149163
149778
|
error: reason
|
|
149164
149779
|
}
|
|
149165
149780
|
});
|
|
149166
|
-
return
|
|
149167
|
-
|
|
149168
|
-
|
|
149781
|
+
return refuseStill("camera-refused", reason);
|
|
149782
|
+
}
|
|
149783
|
+
};
|
|
149784
|
+
/**
|
|
149785
|
+
* ONE clip's bytes, on the short window, with the fetch PROVED (D568).
|
|
149786
|
+
*
|
|
149787
|
+
* Completion is an idle window because nothing on the wire ends a cmd 5
|
|
149788
|
+
* transfer, and that window is short. What makes it safe is not the window,
|
|
149789
|
+
* it is the decoded duration judged against the row that asked for it.
|
|
149790
|
+
*/
|
|
149791
|
+
const fetchVerifiedClipBytes = async (device, row, clipKey) => {
|
|
149792
|
+
const deviceId = device.deviceId;
|
|
149793
|
+
const api = await device.getApi();
|
|
149794
|
+
const fetchPass = (idleTimeoutMs) => fetchClipBytes({
|
|
149795
|
+
download: () => api.downloadRecordingDemuxed({
|
|
149796
|
+
channel: device.channel,
|
|
149797
|
+
...device.uid !== void 0 ? { uid: device.uid } : {},
|
|
149798
|
+
fileName: row.subFileName,
|
|
149799
|
+
idleTimeoutMs
|
|
149800
|
+
}),
|
|
149801
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
149802
|
+
});
|
|
149803
|
+
const judge = (pass) => judgeClipDuration({
|
|
149804
|
+
expectedMs: row.endMs - row.startMs,
|
|
149805
|
+
decodedSeconds: pass.stats.durationSeconds,
|
|
149806
|
+
...row.inProgress === true ? { inProgress: true } : {}
|
|
149807
|
+
});
|
|
149808
|
+
const first = await fetchPass(300);
|
|
149809
|
+
const firstVerdict = judge(first);
|
|
149810
|
+
if (firstVerdict.kind === "unknown") {
|
|
149811
|
+
const line = {
|
|
149812
|
+
tags: { deviceId },
|
|
149813
|
+
meta: {
|
|
149814
|
+
clipKey,
|
|
149815
|
+
branch: "completeness-unverified",
|
|
149816
|
+
reason: firstVerdict.reason,
|
|
149817
|
+
idleTimeoutMs: 300,
|
|
149818
|
+
expectedMs: row.endMs - row.startMs,
|
|
149819
|
+
decodedSeconds: first.stats.durationSeconds
|
|
149820
|
+
}
|
|
149169
149821
|
};
|
|
149822
|
+
if (firstVerdict.reason === "clip-open") deps.logger.debug("videoclips: an open clip cannot be checked for truncation", line);
|
|
149823
|
+
else deps.logger.warn("videoclips: a clip fetch could not be checked for truncation", line);
|
|
149824
|
+
return first;
|
|
149825
|
+
}
|
|
149826
|
+
if (firstVerdict.kind === "complete") return first;
|
|
149827
|
+
deps.logger.warn("videoclips: a clip fetch came back truncated", {
|
|
149828
|
+
tags: { deviceId },
|
|
149829
|
+
meta: {
|
|
149830
|
+
clipKey,
|
|
149831
|
+
branch: "truncated-fetch",
|
|
149832
|
+
idleTimeoutMs: 300,
|
|
149833
|
+
expectedSeconds: firstVerdict.expectedSeconds,
|
|
149834
|
+
decodedSeconds: firstVerdict.decodedSeconds,
|
|
149835
|
+
shortfallSeconds: firstVerdict.shortfallSeconds,
|
|
149836
|
+
bytes: first.annexB.byteLength,
|
|
149837
|
+
keyframes: first.stats.keyframes
|
|
149838
|
+
}
|
|
149839
|
+
});
|
|
149840
|
+
const retry = await fetchPass(CLIP_IDLE_WINDOW_RETRY_MS);
|
|
149841
|
+
const keepRetry = (retry.stats.durationSeconds ?? 0) >= (first.stats.durationSeconds ?? 0);
|
|
149842
|
+
const kept = keepRetry ? retry : first;
|
|
149843
|
+
const verdict = keepRetry ? judge(retry) : firstVerdict;
|
|
149844
|
+
const keptWindowMs = keepRetry ? CLIP_IDLE_WINDOW_RETRY_MS : 300;
|
|
149845
|
+
if (verdict.kind === "short") {
|
|
149846
|
+
deps.logger.warn("videoclips: a clip is still short after the long window", {
|
|
149847
|
+
tags: { deviceId },
|
|
149848
|
+
meta: {
|
|
149849
|
+
clipKey,
|
|
149850
|
+
branch: "truncated-after-retry",
|
|
149851
|
+
idleTimeoutMs: keptWindowMs,
|
|
149852
|
+
retriedOnMs: CLIP_IDLE_WINDOW_RETRY_MS,
|
|
149853
|
+
expectedSeconds: verdict.expectedSeconds,
|
|
149854
|
+
decodedSeconds: verdict.decodedSeconds,
|
|
149855
|
+
shortfallSeconds: verdict.shortfallSeconds,
|
|
149856
|
+
bytes: kept.annexB.byteLength
|
|
149857
|
+
}
|
|
149858
|
+
});
|
|
149859
|
+
return kept;
|
|
149170
149860
|
}
|
|
149861
|
+
deps.logger.info("videoclips: a truncated clip fetch landed whole on the long window", {
|
|
149862
|
+
tags: { deviceId },
|
|
149863
|
+
meta: {
|
|
149864
|
+
clipKey,
|
|
149865
|
+
branch: "truncation-recovered",
|
|
149866
|
+
idleTimeoutMs: keptWindowMs,
|
|
149867
|
+
decodedSeconds: verdict.kind === "complete" ? verdict.decodedSeconds : null,
|
|
149868
|
+
bytes: kept.annexB.byteLength
|
|
149869
|
+
}
|
|
149870
|
+
});
|
|
149871
|
+
return kept;
|
|
149171
149872
|
};
|
|
149172
149873
|
const prepareClip = async (deviceId, clipKey) => {
|
|
149173
149874
|
const ready = await media.pathIfPresent(deviceId, clipKey);
|
|
@@ -149205,25 +149906,44 @@ function createClipService(deps) {
|
|
|
149205
149906
|
});
|
|
149206
149907
|
return null;
|
|
149207
149908
|
}
|
|
149208
|
-
const
|
|
149209
|
-
const
|
|
149210
|
-
|
|
149211
|
-
|
|
149212
|
-
|
|
149213
|
-
|
|
149214
|
-
|
|
149215
|
-
|
|
149216
|
-
|
|
149909
|
+
const downloaded = await fetchVerifiedClipBytes(device, row, clipKey);
|
|
149910
|
+
const audio = decideClipAudio(downloaded.audio);
|
|
149911
|
+
if (audio.kind === "silent") deps.logger.warn("videoclips: serving a clip with no sound", {
|
|
149912
|
+
tags: { deviceId },
|
|
149913
|
+
meta: {
|
|
149914
|
+
clipKey,
|
|
149915
|
+
branch: audio.reason,
|
|
149916
|
+
audioFormat: audio.format,
|
|
149917
|
+
audioPackets: downloaded.stats.audioPackets,
|
|
149918
|
+
audioBytes: downloaded.stats.audioBytes
|
|
149919
|
+
}
|
|
149920
|
+
});
|
|
149921
|
+
if (downloaded.stats.fpsSource !== "timestamps") deps.logger.warn("videoclips: no measured frame rate for this clip", {
|
|
149922
|
+
tags: { deviceId },
|
|
149923
|
+
meta: {
|
|
149924
|
+
clipKey,
|
|
149925
|
+
branch: "fps-unmeasured",
|
|
149926
|
+
fpsSource: downloaded.stats.fpsSource,
|
|
149927
|
+
fps: downloaded.stats.fps,
|
|
149928
|
+
infoFps: downloaded.stats.infoFps
|
|
149929
|
+
}
|
|
149217
149930
|
});
|
|
149218
149931
|
const scratch = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "camstack-clip-mux-"));
|
|
149219
149932
|
try {
|
|
149220
149933
|
const rawPath = (0, node_path.join)(scratch, "clip.h264");
|
|
149934
|
+
const audioPath = (0, node_path.join)(scratch, "clip.aac");
|
|
149221
149935
|
const outPath = (0, node_path.join)(scratch, "clip.mp4");
|
|
149222
149936
|
await (0, node_fs_promises.writeFile)(rawPath, downloaded.annexB);
|
|
149937
|
+
if (audio.kind === "copy") await (0, node_fs_promises.writeFile)(audioPath, audio.data);
|
|
149223
149938
|
const failure = describeMuxFailure(await muxRunner(buildClipMuxArgs({
|
|
149224
149939
|
inputPath: rawPath,
|
|
149225
149940
|
outputPath: outPath,
|
|
149226
|
-
videoType: downloaded.videoType
|
|
149941
|
+
videoType: downloaded.videoType,
|
|
149942
|
+
fps: downloaded.stats.fps,
|
|
149943
|
+
...audio.kind === "copy" ? { audio: {
|
|
149944
|
+
path: audioPath,
|
|
149945
|
+
format: audio.format
|
|
149946
|
+
} } : {}
|
|
149227
149947
|
})));
|
|
149228
149948
|
if (failure !== null) {
|
|
149229
149949
|
deps.logger.warn("videoclips: the clip mux failed", {
|
|
@@ -149245,21 +149965,32 @@ function createClipService(deps) {
|
|
|
149245
149965
|
});
|
|
149246
149966
|
}
|
|
149247
149967
|
};
|
|
149248
|
-
|
|
149249
|
-
|
|
149250
|
-
|
|
149251
|
-
|
|
149252
|
-
|
|
149253
|
-
|
|
149254
|
-
|
|
149255
|
-
|
|
149256
|
-
|
|
149257
|
-
|
|
149258
|
-
served: profile === "high" ? "high" : "low"
|
|
149259
|
-
}),
|
|
149260
|
-
logger: deps.logger
|
|
149968
|
+
const provider = createReolinkVideoclipsProvider({
|
|
149969
|
+
now,
|
|
149970
|
+
cache,
|
|
149971
|
+
resolveDevice: deps.resolveDevice,
|
|
149972
|
+
hasVouchedThumbnail: (deviceId, clipKey) => thumbs.has(deviceId, clipKey),
|
|
149973
|
+
thumbnailUrl: (deviceId, clipKey) => `/addon/${REOLINK_ADDON_ID}/${CLIP_THUMB_PREFIX}/${deviceId}/${clipKey}`,
|
|
149974
|
+
thumbnailMintUrl: (deviceId, clipKey) => `/addon/${REOLINK_ADDON_ID}/${CLIP_THUMB_PREFIX}/${deviceId}/${clipKey}`,
|
|
149975
|
+
preparePlayback: async ({ device, clipKey, profile }) => ({
|
|
149976
|
+
playbackUrl: `/addon/${REOLINK_ADDON_ID}/${CLIP_MEDIA_PREFIX}/${device.deviceId}/${clipKey}.mp4`,
|
|
149977
|
+
served: profile === "high" ? "high" : "low"
|
|
149261
149978
|
}),
|
|
149979
|
+
logger: deps.logger
|
|
149980
|
+
});
|
|
149981
|
+
return {
|
|
149982
|
+
provider,
|
|
149262
149983
|
cache,
|
|
149984
|
+
refreshCatalog: async (deviceId) => {
|
|
149985
|
+
const until = now();
|
|
149986
|
+
await provider.listClips({
|
|
149987
|
+
deviceId,
|
|
149988
|
+
since: until - CLIP_CATALOG_REFRESH_WINDOW_MS,
|
|
149989
|
+
until,
|
|
149990
|
+
provider: REOLINK_ADDON_ID
|
|
149991
|
+
});
|
|
149992
|
+
},
|
|
149993
|
+
hostBusy: (hostKey) => queue.busy(hostKey),
|
|
149263
149994
|
thumbPrefix: CLIP_THUMB_PREFIX,
|
|
149264
149995
|
mediaPrefix: CLIP_MEDIA_PREFIX,
|
|
149265
149996
|
thumbHandler: createClipThumbHandler({
|
|
@@ -149284,6 +150015,79 @@ function createClipService(deps) {
|
|
|
149284
150015
|
};
|
|
149285
150016
|
}
|
|
149286
150017
|
//#endregion
|
|
150018
|
+
//#region src/clips/clip-refresh-settings.ts
|
|
150019
|
+
/** The section id, so the addon's declaration and this schema cannot drift. */
|
|
150020
|
+
var CLIP_REFRESH_SECTION_ID = "clip-catalog-refresh";
|
|
150021
|
+
/**
|
|
150022
|
+
* The one sentence the interval must always carry.
|
|
150023
|
+
*
|
|
150024
|
+
* It does TWO different things depending on what the camera is, and a knob
|
|
150025
|
+
* that silently means two things on two cameras is how this repo has shipped
|
|
150026
|
+
* settings nobody could reason about. So the difference is in the operator's
|
|
150027
|
+
* own text, at both levels, not only in a record.
|
|
150028
|
+
*/
|
|
150029
|
+
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.";
|
|
150030
|
+
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.";
|
|
150031
|
+
/**
|
|
150032
|
+
* The addon-level (global) default. Per-camera overrides live on the device,
|
|
150033
|
+
* under the SAME key, and a device that sets nothing — or resets — resolves to
|
|
150034
|
+
* this value (`resolveClipCatalogRefreshMinutes`).
|
|
150035
|
+
*/
|
|
150036
|
+
function buildClipRefreshSettingsSchema() {
|
|
150037
|
+
return { sections: [{
|
|
150038
|
+
id: CLIP_REFRESH_SECTION_ID,
|
|
150039
|
+
title: "Clip catalog refresh",
|
|
150040
|
+
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.",
|
|
150041
|
+
columns: 1,
|
|
150042
|
+
fields: [{
|
|
150043
|
+
type: "number",
|
|
150044
|
+
key: CLIP_CATALOG_REFRESH_MINUTES_KEY,
|
|
150045
|
+
label: "Refresh interval (min)",
|
|
150046
|
+
description: CLIP_CATALOG_REFRESH_MINUTES_DESCRIPTION,
|
|
150047
|
+
default: 30,
|
|
150048
|
+
min: 0,
|
|
150049
|
+
max: CLIP_CATALOG_REFRESH_MINUTES_MAX,
|
|
150050
|
+
step: 5
|
|
150051
|
+
}]
|
|
150052
|
+
}] };
|
|
150053
|
+
}
|
|
150054
|
+
/**
|
|
150055
|
+
* The per-camera section: the interval override, and — only where it can mean
|
|
150056
|
+
* anything — the forced wake.
|
|
150057
|
+
*
|
|
150058
|
+
* `forcedWake` is a plain persisted switch on purpose. It is NOT a
|
|
150059
|
+
* `device-action-button` and there is no custom action behind it, so nothing
|
|
150060
|
+
* that dispatches device actions (a schedule, an automation, a rule) can spend
|
|
150061
|
+
* a battery camera's wakes; the only way to turn it on is an operator editing
|
|
150062
|
+
* this camera's settings.
|
|
150063
|
+
*/
|
|
150064
|
+
function buildClipRefreshDeviceSection(input) {
|
|
150065
|
+
const forcedWakeApplies = input.isBattery && !input.isHubChild;
|
|
150066
|
+
return [{
|
|
150067
|
+
id: CLIP_REFRESH_SECTION_ID,
|
|
150068
|
+
tab: "recording",
|
|
150069
|
+
title: "Clip catalog refresh",
|
|
150070
|
+
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.",
|
|
150071
|
+
columns: 1,
|
|
150072
|
+
fields: [{
|
|
150073
|
+
type: "number",
|
|
150074
|
+
key: CLIP_CATALOG_REFRESH_MINUTES_KEY,
|
|
150075
|
+
label: "Refresh interval (min)",
|
|
150076
|
+
description: CLIP_CATALOG_REFRESH_MINUTES_DESCRIPTION,
|
|
150077
|
+
min: 0,
|
|
150078
|
+
max: CLIP_CATALOG_REFRESH_MINUTES_MAX,
|
|
150079
|
+
step: 5
|
|
150080
|
+
}, ...forcedWakeApplies ? [{
|
|
150081
|
+
type: "boolean",
|
|
150082
|
+
key: CLIP_CATALOG_FORCED_WAKE_KEY,
|
|
150083
|
+
label: "Wake this camera to refresh",
|
|
150084
|
+
description: CLIP_CATALOG_FORCED_WAKE_DESCRIPTION,
|
|
150085
|
+
default: false,
|
|
150086
|
+
style: "switch"
|
|
150087
|
+
}] : []]
|
|
150088
|
+
}];
|
|
150089
|
+
}
|
|
150090
|
+
//#endregion
|
|
149287
150091
|
//#region src/intercom-failure-report.ts
|
|
149288
150092
|
/**
|
|
149289
150093
|
* Per-camera talk-back counters, published through `failure-contribution`.
|
|
@@ -154361,6 +155165,73 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
154361
155165
|
* provider still treats a missing `uid` as a dropped branch rather than as
|
|
154362
155166
|
* an empty catalog.
|
|
154363
155167
|
*/
|
|
155168
|
+
/**
|
|
155169
|
+
* Re-read this camera's clip catalog. Set by the addon for every camera it
|
|
155170
|
+
* owns; `null` until it is wired (a redeploy re-registers the device, and
|
|
155171
|
+
* the addon re-wires on `DeviceRegistered` AND `DeviceMetaChanged` — the
|
|
155172
|
+
* same fan-out the email-push resolver needed, for the same reason).
|
|
155173
|
+
*/
|
|
155174
|
+
clipCatalogRefresher = null;
|
|
155175
|
+
setClipCatalogRefresher(refresh) {
|
|
155176
|
+
this.clipCatalogRefresher = refresh;
|
|
155177
|
+
}
|
|
155178
|
+
/**
|
|
155179
|
+
* This camera's OVERRIDE of the clip-refresh interval, or `undefined` when
|
|
155180
|
+
* it overrides nothing — which is what a reset leaves behind too, and is
|
|
155181
|
+
* why the resolver takes `unknown` rather than a number (D562).
|
|
155182
|
+
*/
|
|
155183
|
+
getClipCatalogRefreshMinutes() {
|
|
155184
|
+
return this.config.get("clipCatalogRefreshMinutes");
|
|
155185
|
+
}
|
|
155186
|
+
/** Whether the operator authorised the clip timer to wake this camera. */
|
|
155187
|
+
getClipCatalogForcedWake() {
|
|
155188
|
+
return this.config.get("clipCatalogForcedWake");
|
|
155189
|
+
}
|
|
155190
|
+
/**
|
|
155191
|
+
* The clip refresh the TIMER cannot do.
|
|
155192
|
+
*
|
|
155193
|
+
* On a standalone battery camera a catalog read is a login and a login is
|
|
155194
|
+
* the wake, so the periodic refresh only marks the catalog stale (D562).
|
|
155195
|
+
* This is the other half: the moment the camera confirmed itself awake for
|
|
155196
|
+
* its own reasons — a camera event, a stream, an operator action — is a free
|
|
155197
|
+
* read, and it is the same moment that already warms the stream catalog and
|
|
155198
|
+
* every settings slice. Called from `onWakeTransition`, best-effort, last:
|
|
155199
|
+
* a clip listing must never delay the live-stream catalog the operator is
|
|
155200
|
+
* waiting for.
|
|
155201
|
+
*/
|
|
155202
|
+
async refreshClipCatalogOnWake() {
|
|
155203
|
+
const refresh = this.clipCatalogRefresher;
|
|
155204
|
+
if (refresh === null) {
|
|
155205
|
+
this.ctx.logger.debug("onWakeTransition: no clip catalog refresher wired", {
|
|
155206
|
+
tags: { deviceId: this.id },
|
|
155207
|
+
meta: { branch: "clip-refresher-unwired" }
|
|
155208
|
+
});
|
|
155209
|
+
return;
|
|
155210
|
+
}
|
|
155211
|
+
try {
|
|
155212
|
+
await refresh();
|
|
155213
|
+
} catch (err) {
|
|
155214
|
+
this.ctx.logger.warn("onWakeTransition: the clip catalog refresh failed", {
|
|
155215
|
+
tags: { deviceId: this.id },
|
|
155216
|
+
meta: {
|
|
155217
|
+
branch: "clip-refresh-failed",
|
|
155218
|
+
error: err instanceof Error ? err.message : String(err)
|
|
155219
|
+
}
|
|
155220
|
+
});
|
|
155221
|
+
}
|
|
155222
|
+
}
|
|
155223
|
+
/**
|
|
155224
|
+
* Wake this camera because the OPERATOR authorised the clip timer to
|
|
155225
|
+
* (`clipCatalogForcedWake`, off by default — D562).
|
|
155226
|
+
*
|
|
155227
|
+
* The only path from the timer to a battery camera's radio, and it is the
|
|
155228
|
+
* same forced wake `wakeIfSleeping` already drives for a config button.
|
|
155229
|
+
* Throws when the camera did not come up, so the scheduler can say so and
|
|
155230
|
+
* leave the catalog stale rather than stamping it fresh.
|
|
155231
|
+
*/
|
|
155232
|
+
async wakeForClipCatalogRefresh() {
|
|
155233
|
+
if (!await this.wakeIfSleeping()) throw new Error(`ReolinkCamera ${this.id}: the authorised clip-refresh wake did not bring the camera up`);
|
|
155234
|
+
}
|
|
154364
155235
|
getClipDeviceRef() {
|
|
154365
155236
|
const uid = this.config.get("uid");
|
|
154366
155237
|
return {
|
|
@@ -158621,6 +159492,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
158621
159492
|
await this.warmSnapshotOnWake();
|
|
158622
159493
|
await this.alignAuxDevicesState("wake");
|
|
158623
159494
|
await this.refreshParentSettingsSnapshot();
|
|
159495
|
+
await this.refreshClipCatalogOnWake();
|
|
158624
159496
|
}
|
|
158625
159497
|
static PING_INTERVAL_MS = 3e4;
|
|
158626
159498
|
static PING_MAX_FAILURES = 3;
|
|
@@ -159806,7 +160678,11 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
159806
160678
|
}),
|
|
159807
160679
|
...this.buildSessionsTabSections(),
|
|
159808
160680
|
...this.buildEmailPushSection(),
|
|
159809
|
-
...this.buildEmailTabSections()
|
|
160681
|
+
...this.buildEmailTabSections(),
|
|
160682
|
+
...buildClipRefreshDeviceSection({
|
|
160683
|
+
isHubChild: this.isHubChild(),
|
|
160684
|
+
isBattery: this.isBattery
|
|
160685
|
+
})
|
|
159810
160686
|
]
|
|
159811
160687
|
};
|
|
159812
160688
|
const emailSnap = cache?.emailConfigSnapshot;
|
|
@@ -159825,6 +160701,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
159825
160701
|
emailRecipient3: emailSnap?.recipients?.[2] ?? "",
|
|
159826
160702
|
emailSendNickname: emailSnap?.sendNickname ?? this.name,
|
|
159827
160703
|
emailScheduleEnabled: emailSnap?.taskEnabled ?? false,
|
|
160704
|
+
clipCatalogRefreshMinutes: this.config.get("clipCatalogRefreshMinutes"),
|
|
160705
|
+
clipCatalogForcedWake: this.config.get("clipCatalogForcedWake") ?? false,
|
|
159828
160706
|
debugGeneral: this.config.get("debugGeneral") ?? false,
|
|
159829
160707
|
debugSocketLogs: this.config.get("debugSocketLogs") ?? [],
|
|
159830
160708
|
motionEnabled: this.config.get("motionEnabled") ?? motionSnap.enabled ?? true,
|
|
@@ -163098,6 +163976,10 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
163098
163976
|
throw new Error(`Reolink: ${reason}`);
|
|
163099
163977
|
}
|
|
163100
163978
|
async onShutdown() {
|
|
163979
|
+
if (this.clipRefreshTimer) {
|
|
163980
|
+
clearInterval(this.clipRefreshTimer);
|
|
163981
|
+
this.clipRefreshTimer = null;
|
|
163982
|
+
}
|
|
163101
163983
|
this.logChannels?.stop();
|
|
163102
163984
|
await this.emailPushServer?.stop().catch(() => {});
|
|
163103
163985
|
await this.autodetectCache.dispose();
|
|
@@ -163108,6 +163990,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
163108
163990
|
* logged inside the server and leaves it stopped until the next edit.
|
|
163109
163991
|
*/
|
|
163110
163992
|
async onConfigChanged() {
|
|
163993
|
+
await this.refreshClipRefreshGlobalStore();
|
|
163111
163994
|
await this.emailPushServer?.restart().catch((err) => {
|
|
163112
163995
|
this.ctx.logger.warn("email-push: restart after settings change failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
163113
163996
|
});
|
|
@@ -163127,7 +164010,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
163127
164010
|
* for core to select from.
|
|
163128
164011
|
*/
|
|
163129
164012
|
globalSettingsSchema() {
|
|
163130
|
-
return buildEmailPushSettingsSchema(listLanHosts()[0] ?? "127.0.0.1");
|
|
164013
|
+
return { sections: [...buildEmailPushSettingsSchema(listLanHosts()[0] ?? "127.0.0.1").sections, ...buildClipRefreshSettingsSchema().sections] };
|
|
163131
164014
|
}
|
|
163132
164015
|
/**
|
|
163133
164016
|
* Both email-push sections ARE this integration's configuration — one SMTP
|
|
@@ -163174,6 +164057,9 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
163174
164057
|
capability: videoclipsCapability,
|
|
163175
164058
|
provider: this.clipService.provider
|
|
163176
164059
|
});
|
|
164060
|
+
await this.refreshClipRefreshGlobalStore();
|
|
164061
|
+
this.startClipCatalogRefresh(this.clipService);
|
|
164062
|
+
this.wireClipCatalogRefreshers();
|
|
163177
164063
|
await this.serveClipPlanes(this.clipService);
|
|
163178
164064
|
this.ensureEmailPushServer().start().catch((err) => {
|
|
163179
164065
|
this.ctx.logger.warn("email-push: SMTP intake failed to start at init", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
@@ -163186,6 +164072,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
163186
164072
|
if (dev instanceof ReolinkCamera) {
|
|
163187
164073
|
const server = this.ensureEmailPushServer();
|
|
163188
164074
|
dev.setEmailPushParamsResolver(() => server.buildCameraSetupParams(dev));
|
|
164075
|
+
this.wireClipCatalogRefreshers();
|
|
163189
164076
|
this.ctx.logger.debug("email-push: resolver wired", {
|
|
163190
164077
|
tags: { deviceId: data.deviceId },
|
|
163191
164078
|
meta: { via }
|
|
@@ -163199,6 +164086,102 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
163199
164086
|
}
|
|
163200
164087
|
/** The clip catalog, its thumbnail cache and its two routes (D549). */
|
|
163201
164088
|
clipService = null;
|
|
164089
|
+
clipRefreshTimer = null;
|
|
164090
|
+
/**
|
|
164091
|
+
* The addon store as the refresh timer last saw it.
|
|
164092
|
+
*
|
|
164093
|
+
* `listTargets` is synchronous on purpose — a tick that awaited a settings
|
|
164094
|
+
* read per camera would be a store round-trip per camera per tick — so the
|
|
164095
|
+
* global half of the cascade is mirrored here and refreshed at init and on
|
|
164096
|
+
* every `onConfigChanged`. An empty mirror resolves to the DEFAULT, never to
|
|
164097
|
+
* "off": a settings read that has not happened yet must not switch the timer
|
|
164098
|
+
* off for the whole fleet.
|
|
164099
|
+
*/
|
|
164100
|
+
clipRefreshGlobalStore = {};
|
|
164101
|
+
/**
|
|
164102
|
+
* Give every camera the wake-side refresh.
|
|
164103
|
+
*
|
|
164104
|
+
* Idempotent, and re-run on both device lifecycle events for the reason the
|
|
164105
|
+
* email-push resolver documents: an addon-only redeploy re-registers a
|
|
164106
|
+
* device the hub already knows, which emits `DeviceMetaChanged` and NOT
|
|
164107
|
+
* `DeviceRegistered`, and subscribing to one left every battery camera
|
|
164108
|
+
* unwired.
|
|
164109
|
+
*/
|
|
164110
|
+
wireClipCatalogRefreshers() {
|
|
164111
|
+
const service = this.clipService;
|
|
164112
|
+
if (service === null) return;
|
|
164113
|
+
for (const camera of this.listReolinkCameras()) {
|
|
164114
|
+
const deviceId = camera.getClipDeviceRef().deviceId;
|
|
164115
|
+
camera.setClipCatalogRefresher(async () => {
|
|
164116
|
+
await service.refreshCatalog(deviceId);
|
|
164117
|
+
});
|
|
164118
|
+
}
|
|
164119
|
+
}
|
|
164120
|
+
/** Re-read the global half of the clip-refresh cascade. */
|
|
164121
|
+
async refreshClipRefreshGlobalStore() {
|
|
164122
|
+
try {
|
|
164123
|
+
this.clipRefreshGlobalStore = await this.resolveGlobalStore();
|
|
164124
|
+
} catch (err) {
|
|
164125
|
+
this.ctx.logger.warn("videoclips: could not read the clip refresh default — keeping the last", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
164126
|
+
}
|
|
164127
|
+
}
|
|
164128
|
+
/**
|
|
164129
|
+
* Arm the clip-catalog refresh.
|
|
164130
|
+
*
|
|
164131
|
+
* The scheduler owns no clock: it is ticked, so its rules are testable from
|
|
164132
|
+
* COLD (`clip-refresh.spec.ts` — the discipline D561 cost us). The tick
|
|
164133
|
+
* cadence is the FLOOR of what any camera can be configured to, not the
|
|
164134
|
+
* interval itself; each camera's own interval is decided inside the tick
|
|
164135
|
+
* from the cascade, so changing a setting takes effect without re-arming.
|
|
164136
|
+
*/
|
|
164137
|
+
startClipCatalogRefresh(service) {
|
|
164138
|
+
const scheduler = new ClipCatalogRefreshScheduler({
|
|
164139
|
+
now: () => Date.now(),
|
|
164140
|
+
listTargets: () => this.clipRefreshTargets(),
|
|
164141
|
+
refresh: async (deviceId) => {
|
|
164142
|
+
await service.refreshCatalog(deviceId);
|
|
164143
|
+
},
|
|
164144
|
+
markStale: (deviceId) => {
|
|
164145
|
+
service.cache.markStale(deviceId);
|
|
164146
|
+
},
|
|
164147
|
+
wake: async (deviceId) => {
|
|
164148
|
+
const device = this.ctx.kernel.deviceRegistry?.getById(deviceId);
|
|
164149
|
+
if (!(device instanceof ReolinkCamera)) throw new Error(`clip refresh: device ${String(deviceId)} is not a Reolink camera here`);
|
|
164150
|
+
await device.wakeForClipCatalogRefresh();
|
|
164151
|
+
},
|
|
164152
|
+
hostBusy: (hostKey) => service.hostBusy(hostKey),
|
|
164153
|
+
logger: this.ctx.logger
|
|
164154
|
+
});
|
|
164155
|
+
this.clipRefreshTimer = setInterval(() => {
|
|
164156
|
+
scheduler.tick().catch((err) => {
|
|
164157
|
+
this.ctx.logger.warn("videoclips: the catalog refresh tick failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
164158
|
+
});
|
|
164159
|
+
}, 5 * 6e4);
|
|
164160
|
+
if (typeof this.clipRefreshTimer.unref === "function") this.clipRefreshTimer.unref();
|
|
164161
|
+
}
|
|
164162
|
+
/**
|
|
164163
|
+
* Every Reolink CAMERA this process owns, with its cascade resolved.
|
|
164164
|
+
*
|
|
164165
|
+
* Cameras only — the Home Hub (639) is `type: hub` and never binds
|
|
164166
|
+
* `videoclips` (`deviceTypes: [Camera]`); its catalog IS its children's and
|
|
164167
|
+
* is refreshed per child, on its one session. Accessories are not cameras.
|
|
164168
|
+
*/
|
|
164169
|
+
clipRefreshTargets() {
|
|
164170
|
+
const globalMinutes = this.clipRefreshGlobalStore[CLIP_CATALOG_REFRESH_MINUTES_KEY];
|
|
164171
|
+
const out = [];
|
|
164172
|
+
for (const device of this.listReolinkCameras()) {
|
|
164173
|
+
const ref = device.getClipDeviceRef();
|
|
164174
|
+
out.push({
|
|
164175
|
+
deviceId: ref.deviceId,
|
|
164176
|
+
hostKey: ref.hostKey,
|
|
164177
|
+
isHubChild: ref.isHubChild,
|
|
164178
|
+
isBattery: ref.isBattery,
|
|
164179
|
+
forcedWake: resolveClipCatalogForcedWake(device.getClipCatalogForcedWake()),
|
|
164180
|
+
intervalMinutes: resolveClipCatalogRefreshMinutes(globalMinutes, device.getClipCatalogRefreshMinutes())
|
|
164181
|
+
});
|
|
164182
|
+
}
|
|
164183
|
+
return out;
|
|
164184
|
+
}
|
|
163202
164185
|
/**
|
|
163203
164186
|
* Host the thumbnail and media planes, and never let a host without the
|
|
163204
164187
|
* facility take the addon down with it — a data plane is an enhancement to
|