@camstack/addon-provider-reolink 1.2.146 → 1.2.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 require_chunk_4KBXJ7U3 = require("./chunk-4KBXJ7U3-BpV_ui8q.js");
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-7AN_Txmu.mjs
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(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
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: `addon-provider-reolink`
25347
- * answers a hub child with both `native:reolink:onboard` and
25348
- * `native:reolink:hub`, which is why the per-device switch is keyed by
25349
- * SOURCE and not by this (D555).
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",
@@ -41442,6 +41440,12 @@ Object.freeze({
41442
41440
  addonId: null,
41443
41441
  access: "view"
41444
41442
  },
41443
+ "pipelineAnalytics.ownersWithMedia": {
41444
+ capName: "pipeline-analytics",
41445
+ capScope: "device",
41446
+ addonId: null,
41447
+ access: "view"
41448
+ },
41445
41449
  "pipelineAnalytics.pauseForStorageMigration": {
41446
41450
  capName: "pipeline-analytics",
41447
41451
  capScope: "device",
@@ -45342,6 +45346,11 @@ Object.freeze({
45342
45346
  form: "single",
45343
45347
  optional: false
45344
45348
  }],
45349
+ "pipelineAnalytics.ownersWithMedia": [{
45350
+ name: "deviceId",
45351
+ form: "single",
45352
+ optional: false
45353
+ }],
45345
45354
  "pipelineAnalytics.proposeRetrainAnnotations": [{
45346
45355
  name: "deviceId",
45347
45356
  form: "single",
@@ -69361,11 +69370,11 @@ var require_yazl = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
69361
69370
  }
69362
69371
  }));
69363
69372
  //#endregion
69364
- //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-W55OUIVZ.js
69373
+ //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-UIQ527B6.js
69365
69374
  var import_undici = require_undici();
69366
69375
  var import_yazl = /* @__PURE__ */ require_chunk.__toESM(require_yazl(), 1);
69367
69376
  var formatterCache = /* @__PURE__ */ new Map();
69368
- function formatterFor$1(timeZone) {
69377
+ function formatterFor$2(timeZone) {
69369
69378
  const cached = formatterCache.get(timeZone);
69370
69379
  if (cached) return cached;
69371
69380
  const fmt = new Intl.DateTimeFormat("en-US", {
@@ -69390,7 +69399,7 @@ function wallClockParts(d, timeZone) {
69390
69399
  minute: d.getMinutes(),
69391
69400
  second: d.getSeconds()
69392
69401
  };
69393
- const parts = formatterFor$1(timeZone).formatToParts(d);
69402
+ const parts = formatterFor$2(timeZone).formatToParts(d);
69394
69403
  const pick = (type) => {
69395
69404
  const p = parts.find((x) => x.type === type);
69396
69405
  return Number.parseInt(p?.value ?? "", 10);
@@ -70707,13 +70716,13 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
70707
70716
  sec: 59
70708
70717
  };
70709
70718
  }
70710
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Date conversion: start=${start.toISOString()} -> ReolinkTime=${JSON.stringify(startTime)}, end=${end.toISOString()} -> ReolinkTime=${JSON.stringify(endTime)}`);
70719
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Date conversion: start=${start.toISOString()} -> ReolinkTime=${JSON.stringify(startTime)}, end=${end.toISOString()} -> ReolinkTime=${JSON.stringify(endTime)}`);
70711
70720
  if (!bypassCache) {
70712
70721
  this.cleanRecordingsCache();
70713
70722
  const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
70714
70723
  const cached = this.recordingsCache.get(cacheKey);
70715
70724
  if (cached && cached.expiresAt > Date.now()) {
70716
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Cache hit: returning ${cached.data.length} cached enriched recordings`);
70725
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Cache hit: returning ${cached.data.length} cached enriched recordings`);
70717
70726
  return cached.data;
70718
70727
  }
70719
70728
  }
@@ -70765,7 +70774,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
70765
70774
  const firstResult = dayResponse[0];
70766
70775
  if (firstResult && firstResult.code === 0 && firstResult.value?.SearchResult?.File) {
70767
70776
  const files = firstResult.value.SearchResult.File;
70768
- if (files.length > 0 && files[0]) require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response for day ${day}/${month}/${year}: ${JSON.stringify({
70777
+ if (files.length > 0 && files[0]) require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response for day ${day}/${month}/${year}: ${JSON.stringify({
70769
70778
  fileCount: files.length,
70770
70779
  sampleFile: {
70771
70780
  name: files[0].name,
@@ -70801,7 +70810,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
70801
70810
  } catch (e) {}
70802
70811
  enriched2.push(this.enrichVodFile(vodFile, channel, streamUrl));
70803
70812
  }
70804
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched2.length} enriched recording files from autoSearchByDay`);
70813
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched2.length} enriched recording files from autoSearchByDay`);
70805
70814
  if (!bypassCache) {
70806
70815
  const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
70807
70816
  this.recordingsCache.set(cacheKey, {
@@ -70829,7 +70838,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
70829
70838
  const response = await this.call("Search", param, 0);
70830
70839
  allResults.push(...response);
70831
70840
  } else {
70832
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Range spans ${dayRanges.length} days, splitting into separate day queries`);
70841
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Range spans ${dayRanges.length} days, splitting into separate day queries`);
70833
70842
  for (const range of dayRanges) {
70834
70843
  const param = { Search: {
70835
70844
  channel,
@@ -70839,7 +70848,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
70839
70848
  EndTime: range.end
70840
70849
  } };
70841
70850
  if (iLogicChannel > 0) param.Search.iLogicChannel = iLogicChannel;
70842
- require_chunk_4KBXJ7U3.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})`);
70851
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Querying day: ${range.start.year}-${range.start.mon}-${range.start.day} (${range.start.hour}:${range.start.min}:${range.start.sec} to ${range.end.hour}:${range.end.min}:${range.end.sec})`);
70843
70852
  const response = await this.call("Search", param, 0);
70844
70853
  allResults.push(...response);
70845
70854
  }
@@ -70849,7 +70858,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
70849
70858
  const firstResult = result[0];
70850
70859
  if (firstResult && firstResult.code === 0 && firstResult.value?.SearchResult?.File) {
70851
70860
  const files = firstResult.value.SearchResult.File;
70852
- if (files.length > 0 && files[0]) require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response (normal search): ${JSON.stringify({
70861
+ if (files.length > 0 && files[0]) require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Raw API response (normal search): ${JSON.stringify({
70853
70862
  fileCount: files.length,
70854
70863
  sampleFile: {
70855
70864
  name: files[0].name,
@@ -70866,12 +70875,12 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
70866
70875
  const searchResult = res.value?.SearchResult;
70867
70876
  if (searchResult?.File) allFiles.push(...searchResult.File);
70868
70877
  }
70869
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Collected ${allFiles.length} raw VodFiles from API search results`);
70878
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Collected ${allFiles.length} raw VodFiles from API search results`);
70870
70879
  if (allFiles.length > 0) {
70871
70880
  const sampleSize = Math.min(3, allFiles.length);
70872
70881
  for (let i = 0; i < sampleSize; i++) {
70873
70882
  const file = allFiles[i];
70874
- if (file) require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Sample file ${i + 1}/${allFiles.length}: ${JSON.stringify({
70883
+ if (file) require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Sample file ${i + 1}/${allFiles.length}: ${JSON.stringify({
70875
70884
  name: file.name,
70876
70885
  type: file.type,
70877
70886
  size: file.size,
@@ -70894,7 +70903,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
70894
70903
  } catch (e) {}
70895
70904
  enriched.push(this.enrichVodFile(vodFile, channel, streamUrl));
70896
70905
  }
70897
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched.length} enriched recording files`);
70906
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclips", `Returning ${enriched.length} enriched recording files`);
70898
70907
  if (!bypassCache) {
70899
70908
  const cacheKey = this.getNvrRecordingsCacheKey(channel, startTime, endTime, streamType, iLogicChannel, autoSearchByDay);
70900
70909
  this.recordingsCache.set(cacheKey, {
@@ -70931,7 +70940,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
70931
70940
  prepare: true,
70932
70941
  seek: seekSeconds
70933
70942
  });
70934
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`);
70943
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`);
70935
70944
  const { spawn: spawn2 } = await import("child_process");
70936
70945
  return new Promise((resolve, reject) => {
70937
70946
  const chunks = [];
@@ -70979,7 +70988,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
70979
70988
  reject(/* @__PURE__ */ new Error(`ffmpeg produced too small output: ${jpeg.length} bytes`));
70980
70989
  return;
70981
70990
  }
70982
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Successfully extracted thumbnail: ${jpeg.length} bytes`);
70991
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "getVideoclipThumbnailJpeg", `Successfully extracted thumbnail: ${jpeg.length} bytes`);
70983
70992
  resolve(jpeg);
70984
70993
  } else reject(/* @__PURE__ */ new Error(`ffmpeg exited with code ${code}: ${stderr.slice(-500)}`));
70985
70994
  });
@@ -71028,10 +71037,10 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
71028
71037
  }
71029
71038
  } }
71030
71039
  }];
71031
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Request body: ${JSON.stringify(body)}`);
71040
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Request body: ${JSON.stringify(body)}`);
71032
71041
  try {
71033
71042
  const response = await this.callMany(body);
71034
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Response: ${JSON.stringify(response)}`);
71043
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Response: ${JSON.stringify(response)}`);
71035
71044
  const first = response[0];
71036
71045
  if (!first || first.code !== 0 || !first.value?.fileList) {
71037
71046
  const rspCode = first?.error?.rspCode;
@@ -71052,7 +71061,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
71052
71061
  if (!filename) throw new Error(`NvrDownload: no files found in response`);
71053
71062
  return filename;
71054
71063
  } catch (error) {
71055
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Error: ${error instanceof Error ? error.message : String(error)}`);
71064
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "prepareNvrVodDownload", `Error: ${error instanceof Error ? error.message : String(error)}`);
71056
71065
  throw error;
71057
71066
  }
71058
71067
  }
@@ -71248,7 +71257,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
71248
71257
  * Convert a VodFile to RecordingFile with parsed metadata.
71249
71258
  */
71250
71259
  enrichVodFile(vodFile, channel, streamUrl) {
71251
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `RAW VodFile from API: ${JSON.stringify({
71260
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `RAW VodFile from API: ${JSON.stringify({
71252
71261
  name: vodFile.name,
71253
71262
  type: vodFile.type,
71254
71263
  size: vodFile.size,
@@ -71301,7 +71310,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
71301
71310
  hashBits0to15: hashRevBin.slice(0, 16)
71302
71311
  };
71303
71312
  }
71304
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Parsed filename: ${JSON.stringify({
71313
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Parsed filename: ${JSON.stringify({
71305
71314
  fileName: vodFile.name,
71306
71315
  filenameParts,
71307
71316
  hexValueFromFilename,
@@ -71328,13 +71337,13 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
71328
71337
  const hexFlags = parsed?.flags;
71329
71338
  if (parsed?.devType === "hub" && parsed?.version === 4) {
71330
71339
  if (hexValueFromFilename && hexValueFromFilename !== "unknown") {
71331
- require_chunk_4KBXJ7U3.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.`);
71332
- require_chunk_4KBXJ7U3.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.`);
71340
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `WARNING: Hub v4 hex value "${hexValueFromFilename}" appears to be constant across files. Detection flags are likely NOT encoded in filename for this version. Detection information may need to be retrieved via a separate API call or may not be available.`);
71341
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `NOTE: For Hub v4, the API response only provides 'type' field which is "${vodFile.type}". No detection-specific fields are available in the Search API response.`);
71333
71342
  }
71334
71343
  }
71335
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Hex flags from filename: ${JSON.stringify(hexFlags)}`);
71344
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Hex flags from filename: ${JSON.stringify(hexFlags)}`);
71336
71345
  const typeFlags = this.parseRecordTypeFlags(vodFile.type);
71337
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Type flags from recordType "${vodFile.type}": ${JSON.stringify(typeFlags)}`);
71346
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Type flags from recordType "${vodFile.type}": ${JSON.stringify(typeFlags)}`);
71338
71347
  const hasPerson = (hexFlags?.aiPerson ?? false) || typeFlags.hasPerson;
71339
71348
  const hasVehicle = (hexFlags?.aiVehicle ?? false) || typeFlags.hasVehicle;
71340
71349
  const hasAnimal = (hexFlags?.aiAnimal ?? false) || typeFlags.hasAnimal;
@@ -71345,7 +71354,7 @@ var ReolinkCgiApi = class _ReolinkCgiApi {
71345
71354
  const hasPackage = (hexFlags?.package ?? false) || typeFlags.hasPackage;
71346
71355
  const hasRf = (hexFlags?.rf ?? false) || typeFlags.hasRf;
71347
71356
  const hasOther = (hexFlags?.aiOther ?? false) || typeFlags.hasOther;
71348
- require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Final merged flags: ${JSON.stringify({
71357
+ require_chunk_ZBACPERX.recordingsTraceLog(this.debugConfig, this.logger, "enrichVodFile", `Final merged flags: ${JSON.stringify({
71349
71358
  hasPerson,
71350
71359
  hasVehicle,
71351
71360
  hasAnimal,
@@ -71751,8 +71760,8 @@ function nowIsoCompact() {
71751
71760
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
71752
71761
  }
71753
71762
  function nalTypesSummary(videoType, accessUnitAnnexB) {
71754
- if (videoType === "H264") return require_chunk_4KBXJ7U3.splitAnnexBToNalPayloads(accessUnitAnnexB).map((n) => (n[0] ?? 0) & 31);
71755
- return require_chunk_4KBXJ7U3.splitAnnexBToNalPayloads2(accessUnitAnnexB).map((nal) => require_chunk_4KBXJ7U3.getH265NalType(nal)).filter((t) => typeof t === "number");
71763
+ if (videoType === "H264") return require_chunk_ZBACPERX.splitAnnexBToNalPayloads(accessUnitAnnexB).map((n) => (n[0] ?? 0) & 31);
71764
+ return require_chunk_ZBACPERX.splitAnnexBToNalPayloads2(accessUnitAnnexB).map((nal) => require_chunk_ZBACPERX.getH265NalType(nal)).filter((t) => typeof t === "number");
71756
71765
  }
71757
71766
  function normalizeProfiles(p) {
71758
71767
  const out = [];
@@ -72192,7 +72201,7 @@ async function sampleStreams(opts) {
72192
72201
  });
72193
72202
  };
72194
72203
  api.client.on("frame", onFrame);
72195
- const videoStream = new require_chunk_4KBXJ7U3.BaichuanVideoStream({
72204
+ const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
72196
72205
  client: api.client,
72197
72206
  api,
72198
72207
  channel,
@@ -73242,7 +73251,7 @@ async function runMultifocalDiagnosticsConsecutively(params) {
73242
73251
  let audioFrames = 0;
73243
73252
  let lastSnapshotAtMs = 0;
73244
73253
  const client = params.api.client;
73245
- const videoStream = new require_chunk_4KBXJ7U3.BaichuanVideoStream({
73254
+ const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
73246
73255
  client,
73247
73256
  channel: chNative,
73248
73257
  profile,
@@ -73326,7 +73335,7 @@ async function runMultifocalDiagnosticsConsecutively(params) {
73326
73335
  const baseStreamName = profile === "main" ? "mainStream" : profile === "sub" ? "subStream" : "externStream";
73327
73336
  const headerStreamTypeCandidates = profile === "sub" ? [1, 3] : profile === "main" ? [0, 2] : [0];
73328
73337
  const channelIdTagCandidates = uniq([chNative, chNative + 1].filter((n) => Number.isFinite(n) && n >= 0));
73329
- const messageClassCandidates = params.probeFull ? [require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24, 0] : [require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24];
73338
+ const messageClassCandidates = params.probeFull ? [require_chunk_ZBACPERX.BC_CLASS_MODERN_24, 0] : [require_chunk_ZBACPERX.BC_CLASS_MODERN_24];
73330
73339
  const extensionXmlCandidates = params.probeFull ? ["", buildChannelExtensionXml(chNative)] : [buildChannelExtensionXml(chNative)];
73331
73340
  const attempts = [];
73332
73341
  for (const messageClass of messageClassCandidates) for (const streamTypeHeader of headerStreamTypeCandidates) for (const extensionXml of extensionXmlCandidates) {
@@ -75847,14 +75856,14 @@ var require_lz4 = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
75847
75856
  };
75848
75857
  }));
75849
75858
  //#endregion
75850
- //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-BHO5QGTT.js
75859
+ //#region node_modules/@apocaliss92/nodelink-js/dist/chunk-5PJ6XPBF.js
75851
75860
  var import_fxp = require_fxp();
75852
75861
  var import_lz4 = /* @__PURE__ */ require_chunk.__toESM(require_lz4(), 1);
75853
75862
  function encodeHeader(h) {
75854
- const hasOffset = require_chunk_4KBXJ7U3.bcHeaderHasPayloadOffset(h.messageClass);
75863
+ const hasOffset = require_chunk_ZBACPERX.bcHeaderHasPayloadOffset(h.messageClass);
75855
75864
  const headerLen = hasOffset ? 24 : 20;
75856
75865
  const buf = Buffer.alloc(headerLen);
75857
- const magic = h.magic ?? require_chunk_4KBXJ7U3.BC_MAGIC;
75866
+ const magic = h.magic ?? require_chunk_ZBACPERX.BC_MAGIC;
75858
75867
  if (magic.length !== 4) throw new Error("magic must be 4 bytes");
75859
75868
  magic.copy(buf, 0);
75860
75869
  buf.writeUInt32LE(h.cmdId >>> 0, 4);
@@ -75870,7 +75879,7 @@ function encodeHeader(h) {
75870
75879
  function decodeHeader(buf) {
75871
75880
  if (buf.length < 20) throw new Error("not enough data for Baichuan header");
75872
75881
  const magic = buf.subarray(0, 4);
75873
- if (!magic.equals(require_chunk_4KBXJ7U3.BC_MAGIC) && !magic.equals(require_chunk_4KBXJ7U3.BC_MAGIC_REV)) throw new Error(`invalid Baichuan magic: ${magic.toString("hex")}`);
75882
+ if (!magic.equals(require_chunk_ZBACPERX.BC_MAGIC) && !magic.equals(require_chunk_ZBACPERX.BC_MAGIC_REV)) throw new Error(`invalid Baichuan magic: ${magic.toString("hex")}`);
75874
75883
  const cmdId = buf.readUInt32LE(4);
75875
75884
  const bodyLen = buf.readUInt32LE(8);
75876
75885
  const channelId = buf.readUInt8(12);
@@ -75878,7 +75887,7 @@ function decodeHeader(buf) {
75878
75887
  const msgNum = buf.readUInt16LE(14);
75879
75888
  const responseCode = buf.readUInt16LE(16);
75880
75889
  const messageClass = buf.readUInt16LE(18);
75881
- const headerLen = require_chunk_4KBXJ7U3.bcHeaderHasPayloadOffset(messageClass) ? 24 : 20;
75890
+ const headerLen = require_chunk_ZBACPERX.bcHeaderHasPayloadOffset(messageClass) ? 24 : 20;
75882
75891
  if (buf.length < headerLen) throw new Error("not enough data for Baichuan header (needs 24 bytes)");
75883
75892
  const messageKey = buf.readUInt32LE(12);
75884
75893
  const header = {
@@ -75948,9 +75957,9 @@ var BaichuanFrameParser = class {
75948
75957
  this.needed = 4;
75949
75958
  break;
75950
75959
  }
75951
- if (!this.buffer.subarray(0, 4).equals(require_chunk_4KBXJ7U3.BC_MAGIC) && !this.buffer.subarray(0, 4).equals(require_chunk_4KBXJ7U3.BC_MAGIC_REV)) {
75952
- const idx = this.buffer.indexOf(require_chunk_4KBXJ7U3.BC_MAGIC);
75953
- const idxRev = this.buffer.indexOf(require_chunk_4KBXJ7U3.BC_MAGIC_REV);
75960
+ if (!this.buffer.subarray(0, 4).equals(require_chunk_ZBACPERX.BC_MAGIC) && !this.buffer.subarray(0, 4).equals(require_chunk_ZBACPERX.BC_MAGIC_REV)) {
75961
+ const idx = this.buffer.indexOf(require_chunk_ZBACPERX.BC_MAGIC);
75962
+ const idxRev = this.buffer.indexOf(require_chunk_ZBACPERX.BC_MAGIC_REV);
75954
75963
  const next = idx === -1 ? idxRev : idxRev === -1 ? idx : Math.min(idx, idxRev);
75955
75964
  if (next === -1) {
75956
75965
  this.buffer = this.buffer.subarray(Math.max(0, this.buffer.length - 3));
@@ -77965,7 +77974,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
77965
77974
  constructor(options) {
77966
77975
  super();
77967
77976
  this.opts = options;
77968
- this.debugCfg = require_chunk_4KBXJ7U3.normalizeDebugOptions(options.debugOptions);
77977
+ this.debugCfg = require_chunk_ZBACPERX.normalizeDebugOptions(options.debugOptions);
77969
77978
  this.logger = createDebugGateLogger(options.logger, this.debugCfg.general || this.debugCfg.traceNativeStream || this.debugCfg.traceRecordings || this.debugCfg.traceTalk || this.debugCfg.traceEvents || this.debugCfg.debugRtsp);
77970
77979
  this.on("error", (err) => {
77971
77980
  this.logFixed("error", {
@@ -78447,7 +78456,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
78447
78456
  streamType: 0,
78448
78457
  msgNum,
78449
78458
  responseCode: 0,
78450
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
78459
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
78451
78460
  payloadOffset: 0
78452
78461
  });
78453
78462
  this.logDebug("udp_keepalive_ping_tx", {
@@ -78479,7 +78488,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
78479
78488
  cmdId: 93,
78480
78489
  channel: this.opts.channel ?? 0,
78481
78490
  channelIdOverride: 0,
78482
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
78491
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
78483
78492
  streamType: 0,
78484
78493
  extensionXml: "",
78485
78494
  internal: true
@@ -78818,7 +78827,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
78818
78827
  cmdId: 2,
78819
78828
  payloadXml: logoutXml,
78820
78829
  extensionXml: "",
78821
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
78830
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
78822
78831
  channelIdOverride: effectiveHostChannelId,
78823
78832
  timeoutMs: 5e3
78824
78833
  })).header.responseCode;
@@ -78931,12 +78940,12 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
78931
78940
  };
78932
78941
  s.frames++;
78933
78942
  if (now - s.lastLogMs >= 1e3) {
78934
- require_chunk_4KBXJ7U3.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}`);
78943
+ require_chunk_ZBACPERX.debugLog(this.debugCfg, this.logger, "BaichuanRx", `rx cmdId=3 frames=${s.frames} lastMsgNum=${frame.header.msgNum} lastResponseCode=${frame.header.responseCode} lastChannelId=${frame.header.channelId} lastStreamType=${frame.header.streamType} lastBodyLen=${frame.body.length} lastPayloadLen=${frame.payload.length}`);
78935
78944
  s.lastLogMs = now;
78936
78945
  s.frames = 0;
78937
78946
  }
78938
78947
  this.rxCmdTraceStats.set(3, s);
78939
- } else require_chunk_4KBXJ7U3.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}`);
78948
+ } else require_chunk_ZBACPERX.debugLog(this.debugCfg, this.logger, "BaichuanRx", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} streamType=${frame.header.streamType} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
78940
78949
  if (this.transport === "udp" && frame.header.cmdId === 234) {
78941
78950
  try {
78942
78951
  const header = encodeHeader({
@@ -78973,7 +78982,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
78973
78982
  }
78974
78983
  return;
78975
78984
  }
78976
- if (this.debugCfg.traceTalk && isTalkCmd(frame.header.cmdId)) require_chunk_4KBXJ7U3.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}`);
78985
+ if (this.debugCfg.traceTalk && isTalkCmd(frame.header.cmdId)) require_chunk_ZBACPERX.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
78977
78986
  this.emit("frame", frame);
78978
78987
  const key = `${frame.header.cmdId}:${frame.header.msgNum}`;
78979
78988
  const pending = this.pending.get(key);
@@ -78994,7 +79003,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
78994
79003
  };
78995
79004
  s.frames++;
78996
79005
  if (now2 - s.lastLogMs >= 500) {
78997
- require_chunk_4KBXJ7U3.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}`);
79006
+ require_chunk_ZBACPERX.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `rx stream frames cmdId=3 msgNum=${frame.header.msgNum} frames=${s.frames} lastChannelId=${frame.header.channelId} lastBodyLen=${frame.body.length} lastPayloadLen=${frame.payload.length} lastPayloadOffset=${frame.header.payloadOffset ?? 0}`);
78998
79007
  s.lastLogMs = now2;
78999
79008
  s.frames = 0;
79000
79009
  }
@@ -79025,7 +79034,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
79025
79034
  const eventLogTag = sid ? `BaichuanEvent sid=${sid}` : "BaichuanEvent";
79026
79035
  const events$1 = this.parseEvents(frame);
79027
79036
  for (const event of events$1) {
79028
- require_chunk_4KBXJ7U3.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 ?? ""}` : ""));
79037
+ require_chunk_ZBACPERX.eventTraceLog(this.debugCfg, this.logger, eventLogTag, `dispatch cmdId=33 msgNum=${frame.header.msgNum} channelId=${frame.header.channelId} type=${event.type} eventChannel=${event.channel}` + (event.type === "ai" ? ` aiType=${event.ai?.type ?? "unknown"} detected=${event.ai?.detected ?? ""}` : "") + (event.type === "motion" ? ` source=${event.motion?.source ?? ""}` : ""));
79029
79038
  this.emit("event", event);
79030
79039
  }
79031
79040
  } catch (error) {
@@ -79084,7 +79093,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
79084
79093
  if (!xml || !xml.startsWith("<?xml")) return [];
79085
79094
  if (this.debugCfg.traceEvents) {
79086
79095
  const snippet = xml.length > 500 ? `${xml.slice(0, 500)}...` : xml;
79087
- require_chunk_4KBXJ7U3.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)}`);
79096
+ require_chunk_ZBACPERX.eventTraceLog(this.debugCfg, this.logger, eventRawLogTag, `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} xml=${JSON.stringify(snippet)}`);
79088
79097
  }
79089
79098
  const fallbackChannelId = frame.header.channelId;
79090
79099
  const fallbackChannel = fallbackChannelId === 250 ? 0 : Math.max(0, fallbackChannelId - 1);
@@ -79102,7 +79111,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
79102
79111
  const timeStampRaw2 = (getXmlText(alarmXml, "timeStamp") ?? getXmlText(alarmXml, "timestamp") ?? "").trim();
79103
79112
  const timeStampNum2 = timeStampRaw2.length ? Number(timeStampRaw2) : void 0;
79104
79113
  const alarmTimeStamp2 = Number.isFinite(timeStampNum2) ? timeStampNum2 : void 0;
79105
- if (this.debugCfg.traceEvents) require_chunk_4KBXJ7U3.eventTraceLog(this.debugCfg, this.logger, eventRawLogTag, `AlarmEvent channel=${channel} status=${JSON.stringify(status2)} aiType=${JSON.stringify(aiTypeRaw2)}`);
79114
+ if (this.debugCfg.traceEvents) require_chunk_ZBACPERX.eventTraceLog(this.debugCfg, this.logger, eventRawLogTag, `AlarmEvent channel=${channel} status=${JSON.stringify(status2)} aiType=${JSON.stringify(aiTypeRaw2)}`);
79106
79115
  const statusTokens2 = status2.toLowerCase().split(",").map((t) => t.trim()).filter((t) => t.length > 0);
79107
79116
  const visitorActive2 = statusTokens2.includes("visitor");
79108
79117
  const dayNightActive2 = statusTokens2.includes("dn") || statusTokens2.includes("daynight");
@@ -79376,15 +79385,15 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
79376
79385
  const extBuf = Buffer.from(extXml, "utf8");
79377
79386
  const payloadBuf = Buffer.from(payloadXml, "utf8");
79378
79387
  if (enc.kind === "none") return Buffer.concat([extBuf, payloadBuf]);
79379
- if (enc.kind === "bc") return Buffer.concat([require_chunk_4KBXJ7U3.bcEncrypt(extBuf, channelId), require_chunk_4KBXJ7U3.bcEncrypt(payloadBuf, channelId)]);
79380
- if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_4KBXJ7U3.aesEncrypt(extBuf, enc.key), require_chunk_4KBXJ7U3.aesEncrypt(payloadBuf, enc.key)]);
79388
+ if (enc.kind === "bc") return Buffer.concat([require_chunk_ZBACPERX.bcEncrypt(extBuf, channelId), require_chunk_ZBACPERX.bcEncrypt(payloadBuf, channelId)]);
79389
+ if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_ZBACPERX.aesEncrypt(extBuf, enc.key), require_chunk_ZBACPERX.aesEncrypt(payloadBuf, enc.key)]);
79381
79390
  return Buffer.concat([extBuf, payloadBuf]);
79382
79391
  }
79383
79392
  encodeBodyBinary(extXml, payload, channelId, enc) {
79384
79393
  const extBuf = Buffer.from(extXml, "utf8");
79385
79394
  if (enc.kind === "none") return Buffer.concat([extBuf, payload]);
79386
- if (enc.kind === "bc") return Buffer.concat([require_chunk_4KBXJ7U3.bcEncrypt(extBuf, channelId), payload]);
79387
- if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_4KBXJ7U3.aesEncrypt(extBuf, enc.key), payload]);
79395
+ if (enc.kind === "bc") return Buffer.concat([require_chunk_ZBACPERX.bcEncrypt(extBuf, channelId), payload]);
79396
+ if (enc.kind === "aes" || enc.kind === "full_aes") return Buffer.concat([require_chunk_ZBACPERX.aesEncrypt(extBuf, enc.key), payload]);
79388
79397
  return Buffer.concat([extBuf, payload]);
79389
79398
  }
79390
79399
  /**
@@ -79429,7 +79438,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
79429
79438
  bodyLen,
79430
79439
  binaryPayload: true
79431
79440
  });
79432
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_4KBXJ7U3.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}`);
79441
+ if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset} binaryPayloadLen=${params.payload.length}`);
79433
79442
  this.recordTx({
79434
79443
  cmdId,
79435
79444
  responseCode: 0,
@@ -79446,8 +79455,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
79446
79455
  let dec;
79447
79456
  try {
79448
79457
  if (enc.kind === "none") dec = buf;
79449
- else if (enc.kind === "bc") dec = require_chunk_4KBXJ7U3.bcDecrypt(buf, channelId);
79450
- else dec = require_chunk_4KBXJ7U3.aesDecrypt(buf, enc.key);
79458
+ else if (enc.kind === "bc") dec = require_chunk_ZBACPERX.bcDecrypt(buf, channelId);
79459
+ else dec = require_chunk_ZBACPERX.aesDecrypt(buf, enc.key);
79451
79460
  } catch {
79452
79461
  return;
79453
79462
  }
@@ -79513,8 +79522,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
79513
79522
  messageClass,
79514
79523
  bodyLen
79515
79524
  });
79516
- if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_4KBXJ7U3.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}`);
79517
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_4KBXJ7U3.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}`);
79525
+ if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_ZBACPERX.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
79526
+ if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
79518
79527
  this.recordTx({
79519
79528
  cmdId,
79520
79529
  responseCode: 0,
@@ -79529,8 +79538,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
79529
79538
  responseCode: frame.header.responseCode,
79530
79539
  msgNum: frame.header.msgNum
79531
79540
  });
79532
- if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_4KBXJ7U3.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}`);
79533
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_4KBXJ7U3.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}`);
79541
+ if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_ZBACPERX.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
79542
+ if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
79534
79543
  if (frame.header.responseCode === 400) {
79535
79544
  if (frame.body.length === 0) throw new Error("Baichuan authentication failed (responseCode 400, empty body) - check username/password");
79536
79545
  }
@@ -79600,9 +79609,9 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
79600
79609
  messageClass,
79601
79610
  bodyLen
79602
79611
  });
79603
- if (_BaichuanClient.recordingCmdIds.has(cmdId)) require_chunk_4KBXJ7U3.recordingsTraceLog(this.debugCfg, this.logger, "BaichuanRecordings", `tx recording cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} bodyLen=${bodyLen}`);
79604
- if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_4KBXJ7U3.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}`);
79605
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_4KBXJ7U3.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}`);
79612
+ if (_BaichuanClient.recordingCmdIds.has(cmdId)) require_chunk_ZBACPERX.recordingsTraceLog(this.debugCfg, this.logger, "BaichuanRecordings", `tx recording cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} bodyLen=${bodyLen}`);
79613
+ if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_ZBACPERX.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
79614
+ if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `tx cmdId=${cmdId} msgNum=${msgNum} channelId=${channelId} streamType=${params.streamType ?? 0} class=0x${messageClass.toString(16)} bodyLen=${bodyLen} payloadOffset=${payloadOffset}`);
79606
79615
  this.recordTx({
79607
79616
  cmdId,
79608
79617
  responseCode: 0,
@@ -79617,9 +79626,9 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
79617
79626
  responseCode: frame.header.responseCode,
79618
79627
  msgNum: frame.header.msgNum
79619
79628
  });
79620
- if (_BaichuanClient.recordingCmdIds.has(frame.header.cmdId)) require_chunk_4KBXJ7U3.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}`);
79621
- if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_4KBXJ7U3.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}`);
79622
- if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_4KBXJ7U3.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}`);
79629
+ if (_BaichuanClient.recordingCmdIds.has(frame.header.cmdId)) require_chunk_ZBACPERX.recordingsTraceLog(this.debugCfg, this.logger, "BaichuanRecordings", `rx recording cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
79630
+ if (this.debugCfg.traceNativeStream && (cmdId === 3 || cmdId === 4)) require_chunk_ZBACPERX.traceLog(this.debugCfg, this.logger, "BaichuanTrace", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
79631
+ if (this.debugCfg.traceTalk && isTalkCmd(cmdId)) require_chunk_ZBACPERX.talkTraceLog(this.debugCfg, this.logger, "BaichuanTalk", `rx cmdId=${frame.header.cmdId} msgNum=${frame.header.msgNum} responseCode=${frame.header.responseCode} channelId=${frame.header.channelId} bodyLen=${frame.body.length} payloadLen=${frame.payload.length} payloadOffset=${frame.header.payloadOffset ?? 0}`);
79623
79632
  if (!internal) this.kickIdleDisconnectTimer();
79624
79633
  return frame;
79625
79634
  }
@@ -80441,8 +80450,8 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
80441
80450
  const tryAs = (enc) => {
80442
80451
  try {
80443
80452
  if (enc.kind === "none") return buf;
80444
- if (enc.kind === "bc") return require_chunk_4KBXJ7U3.bcDecrypt(buf, channelId);
80445
- if (enc.kind === "aes" || enc.kind === "full_aes") return require_chunk_4KBXJ7U3.aesDecrypt(buf, enc.key);
80453
+ if (enc.kind === "bc") return require_chunk_ZBACPERX.bcDecrypt(buf, channelId);
80454
+ if (enc.kind === "aes" || enc.kind === "full_aes") return require_chunk_ZBACPERX.aesDecrypt(buf, enc.key);
80446
80455
  return null;
80447
80456
  } catch {
80448
80457
  return null;
@@ -80472,7 +80481,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
80472
80481
  streamType: 0,
80473
80482
  msgNum,
80474
80483
  responseCode: encByte,
80475
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_LEGACY
80484
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_LEGACY
80476
80485
  });
80477
80486
  const pendingKey = `${cmdId}:${msgNum}`;
80478
80487
  const framePromise = new Promise((resolve, reject) => {
@@ -80504,15 +80513,15 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
80504
80513
  else if (encType === 1) this.enc = { kind: "bc" };
80505
80514
  else if (encType === 2) this.enc = {
80506
80515
  kind: "aes",
80507
- key: require_chunk_4KBXJ7U3.deriveAesKey(nonce, this.opts.password)
80516
+ key: require_chunk_ZBACPERX.deriveAesKey(nonce, this.opts.password)
80508
80517
  };
80509
80518
  else if (encType === 18) this.enc = {
80510
80519
  kind: "full_aes",
80511
- key: require_chunk_4KBXJ7U3.deriveAesKey(nonce, this.opts.password)
80520
+ key: require_chunk_ZBACPERX.deriveAesKey(nonce, this.opts.password)
80512
80521
  };
80513
80522
  else throw new Error(`Baichuan login: unknown encType=0x${encType.toString(16)}`);
80514
- const userHash = require_chunk_4KBXJ7U3.md5StrModern(`${this.opts.username}${nonce}`);
80515
- const passHash = require_chunk_4KBXJ7U3.md5StrModern(`${this.opts.password}${nonce}`);
80523
+ const userHash = require_chunk_ZBACPERX.md5StrModern(`${this.opts.username}${nonce}`);
80524
+ const passHash = require_chunk_ZBACPERX.md5StrModern(`${this.opts.password}${nonce}`);
80516
80525
  const loginXml = buildLoginXml(userHash, passHash);
80517
80526
  this.logDebug("login_hash", {
80518
80527
  username: this.opts.username,
@@ -80527,7 +80536,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
80527
80536
  cmdId: 1,
80528
80537
  payloadXml: loginXml,
80529
80538
  extensionXml: "",
80530
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
80539
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
80531
80540
  channelIdOverride: effectiveHostChannelId,
80532
80541
  encryption: loginEnc,
80533
80542
  timeoutMs: 1e4
@@ -80587,7 +80596,7 @@ async function* createNativeStream(api, channel, profile, options) {
80587
80596
  client = api.client;
80588
80597
  }
80589
80598
  }
80590
- const videoStream = new require_chunk_4KBXJ7U3.BaichuanVideoStream({
80599
+ const videoStream = new require_chunk_ZBACPERX.BaichuanVideoStream({
80591
80600
  client,
80592
80601
  api,
80593
80602
  channel,
@@ -81105,7 +81114,7 @@ var H264Flow = class extends BaseFlow {
81105
81114
  super(transport, "H264");
81106
81115
  }
81107
81116
  extractParameterSets(accessUnitAnnexB) {
81108
- const nals = require_chunk_4KBXJ7U3.splitAnnexBToNalPayloads(accessUnitAnnexB);
81117
+ const nals = require_chunk_ZBACPERX.splitAnnexBToNalPayloads(accessUnitAnnexB);
81109
81118
  for (const nal of nals) {
81110
81119
  if (nal.length < 1) continue;
81111
81120
  const nalType = (nal[0] ?? 0) & 31;
@@ -81150,9 +81159,9 @@ var H265Flow = class extends BaseFlow {
81150
81159
  super(transport, "H265");
81151
81160
  }
81152
81161
  extractParameterSets(accessUnitAnnexB) {
81153
- const vps = require_chunk_4KBXJ7U3.extractVpsFromAnnexB(accessUnitAnnexB);
81154
- const sps = require_chunk_4KBXJ7U3.extractSpsFromAnnexB(accessUnitAnnexB);
81155
- const pps = require_chunk_4KBXJ7U3.extractPpsFromAnnexB(accessUnitAnnexB);
81162
+ const vps = require_chunk_ZBACPERX.extractVpsFromAnnexB(accessUnitAnnexB);
81163
+ const sps = require_chunk_ZBACPERX.extractSpsFromAnnexB(accessUnitAnnexB);
81164
+ const pps = require_chunk_ZBACPERX.extractPpsFromAnnexB(accessUnitAnnexB);
81156
81165
  if (vps && !this.vps) this.vps = vps;
81157
81166
  if (sps && !this.sps) this.sps = sps;
81158
81167
  if (pps && !this.pps) this.pps = pps;
@@ -81536,8 +81545,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
81536
81545
  /** Returns true if the raw (packed/Annex B) frame is an IDR (H.264) or IRAP (H.265). */
81537
81546
  isRawFrameKeyframe(frame) {
81538
81547
  try {
81539
- if (frame.videoType === "H264") return _BaichuanRtspServer.splitAnnexBNals(require_chunk_4KBXJ7U3.convertToAnnexB(frame.data)).some((n) => n.length >= 1 && (n[0] & 31) === 5);
81540
- if (frame.videoType === "H265") return require_chunk_4KBXJ7U3.splitAnnexBToNalPayloads2(require_chunk_4KBXJ7U3.convertToAnnexB2(frame.data)).some((n) => n.length >= 2 && require_chunk_4KBXJ7U3.isH265Irap(n[0] >> 1 & 63));
81548
+ if (frame.videoType === "H264") return _BaichuanRtspServer.splitAnnexBNals(require_chunk_ZBACPERX.convertToAnnexB(frame.data)).some((n) => n.length >= 1 && (n[0] & 31) === 5);
81549
+ if (frame.videoType === "H265") return require_chunk_ZBACPERX.splitAnnexBToNalPayloads2(require_chunk_ZBACPERX.convertToAnnexB2(frame.data)).some((n) => n.length >= 2 && require_chunk_ZBACPERX.isH265Irap(n[0] >> 1 & 63));
81541
81550
  } catch {}
81542
81551
  return false;
81543
81552
  }
@@ -82437,13 +82446,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
82437
82446
  if (resources?.rtpAudioTimestamp !== void 0) resources.rtpAudioTimestamp = resources.rtpAudioTimestamp + 1024 >>> 0;
82438
82447
  };
82439
82448
  const isH265IrapAccessUnit = (annexB) => {
82440
- const nals = require_chunk_4KBXJ7U3.splitAnnexBToNalPayloads2(annexB);
82449
+ const nals = require_chunk_ZBACPERX.splitAnnexBToNalPayloads2(annexB);
82441
82450
  for (const nal of nals) {
82442
82451
  if (nal.length < 2) continue;
82443
82452
  const b0 = nal[0];
82444
82453
  if (b0 === void 0) continue;
82445
82454
  if ((b0 & 128) !== 0) continue;
82446
- if (require_chunk_4KBXJ7U3.isH265Irap(b0 >> 1 & 63)) return true;
82455
+ if (require_chunk_ZBACPERX.isH265Irap(b0 >> 1 & 63)) return true;
82447
82456
  }
82448
82457
  return false;
82449
82458
  };
@@ -82599,7 +82608,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
82599
82608
  continue;
82600
82609
  }
82601
82610
  if (frame.videoType === "H264" || frame.videoType === "H265") {
82602
- const normalizedVideoData = frame.videoType === "H264" ? require_chunk_4KBXJ7U3.convertToAnnexB(frame.data) : require_chunk_4KBXJ7U3.convertToAnnexB2(frame.data);
82611
+ const normalizedVideoData = frame.videoType === "H264" ? require_chunk_ZBACPERX.convertToAnnexB(frame.data) : require_chunk_ZBACPERX.convertToAnnexB2(frame.data);
82603
82612
  if (frameCount === 0) this.setFlowVideoType(frame.videoType, "first video frame");
82604
82613
  if (!this.flow.getFmtp().hasParamSets) {
82605
82614
  this.flow.extractParameterSets(normalizedVideoData);
@@ -82615,7 +82624,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
82615
82624
  }
82616
82625
  if (useDirectRtp) {
82617
82626
  const videoType = frame.videoType ?? this.flow.videoType;
82618
- const normalizedVideoData = videoType === "H264" ? require_chunk_4KBXJ7U3.convertToAnnexB(frame.data) : require_chunk_4KBXJ7U3.convertToAnnexB2(frame.data);
82627
+ const normalizedVideoData = videoType === "H264" ? require_chunk_ZBACPERX.convertToAnnexB(frame.data) : require_chunk_ZBACPERX.convertToAnnexB2(frame.data);
82619
82628
  if (!resources?.seenFirstVideoKeyframe) if (videoType === "H265") {
82620
82629
  const { hasParamSets } = this.flow.getFmtp();
82621
82630
  if (!hasParamSets) {
@@ -82691,7 +82700,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
82691
82700
  const headHex = frame.data.subarray(0, 16).toString("hex");
82692
82701
  this.rtspDebugLog(`First video frame written to ffmpeg stdin for client ${clientId} (len=${frame.data.length}, head=${headHex})`);
82693
82702
  }
82694
- if (!stdin.write(frame.videoType === "H264" ? require_chunk_4KBXJ7U3.convertToAnnexB(frame.data) : frame.data)) await new Promise((resolve) => {
82703
+ if (!stdin.write(frame.videoType === "H264" ? require_chunk_ZBACPERX.convertToAnnexB(frame.data) : frame.data)) await new Promise((resolve) => {
82695
82704
  if (stdin) stdin.once("drain", () => resolve());
82696
82705
  else resolve();
82697
82706
  });
@@ -82803,7 +82812,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends events.EventEmitter {
82803
82812
  ensureContinuousStream(dedicatedClient) {
82804
82813
  if (this.continuousStream) return this.continuousStream;
82805
82814
  const createLiveStream = async () => {
82806
- return new require_chunk_4KBXJ7U3.BaichuanVideoStream({
82815
+ return new require_chunk_ZBACPERX.BaichuanVideoStream({
82807
82816
  client: dedicatedClient ?? this.api.client,
82808
82817
  api: this.api,
82809
82818
  channel: this.channel,
@@ -83951,6 +83960,128 @@ function patchMotionSensitivityListXml(currentXml, bands) {
83951
83960
  const fragment = encodeMotionSensitivityListXml(bands);
83952
83961
  return currentXml.replace(/<sensitivityInfoList[\s\S]*?(?:\/>|<\/sensitivityInfoList>)/, fragment);
83953
83962
  }
83963
+ var ADTS_SAMPLES_PER_FRAME = 1024;
83964
+ var ADTS_SAMPLE_RATES = [
83965
+ 96e3,
83966
+ 88200,
83967
+ 64e3,
83968
+ 48e3,
83969
+ 44100,
83970
+ 32e3,
83971
+ 24e3,
83972
+ 22050,
83973
+ 16e3,
83974
+ 12e3,
83975
+ 11025,
83976
+ 8e3,
83977
+ 7350,
83978
+ null,
83979
+ null,
83980
+ null
83981
+ ];
83982
+ var ADTS_PROFILES = [
83983
+ "Main",
83984
+ "LC",
83985
+ "SSR",
83986
+ "LTP"
83987
+ ];
83988
+ var parseAdtsHeader = (frame) => {
83989
+ if (frame.length < 7) return null;
83990
+ const b0 = frame[0];
83991
+ const b1 = frame[1];
83992
+ if (b0 !== 255 || (b1 & 240) !== 240) return null;
83993
+ const b2 = frame[2];
83994
+ const b3 = frame[3];
83995
+ const b4 = frame[4];
83996
+ const b5 = frame[5];
83997
+ const profile = b2 >> 6 & 3;
83998
+ const sampleRateIndex = b2 >> 2 & 15;
83999
+ const channelConfig = (b2 & 1) << 2 | b3 >> 6 & 3;
84000
+ const frameLength = (b3 & 3) << 11 | b4 << 3 | b5 >> 5 & 7;
84001
+ if (frameLength < 7) return null;
84002
+ return {
84003
+ profile,
84004
+ profileName: ADTS_PROFILES[profile] ?? "unknown",
84005
+ sampleRate: ADTS_SAMPLE_RATES[sampleRateIndex] ?? null,
84006
+ channels: channelConfig > 0 ? channelConfig : null,
84007
+ frameLength
84008
+ };
84009
+ };
84010
+ var buildRecordingAudioTrack = (params) => {
84011
+ const { codec, frames } = params;
84012
+ if (codec == null || frames.length === 0) return null;
84013
+ const data = frames.length === 1 ? frames[0] : Buffer.concat([...frames]);
84014
+ if (codec === "Adpcm") return {
84015
+ codec,
84016
+ format: "adpcm",
84017
+ data,
84018
+ frames: frames.length,
84019
+ bytes: data.length,
84020
+ sampleRate: null,
84021
+ channels: null,
84022
+ durationSeconds: null
84023
+ };
84024
+ const head = parseAdtsHeader(frames[0]);
84025
+ if (head == null) return {
84026
+ codec,
84027
+ format: "unknown",
84028
+ data,
84029
+ frames: frames.length,
84030
+ bytes: data.length,
84031
+ sampleRate: null,
84032
+ channels: null,
84033
+ durationSeconds: null
84034
+ };
84035
+ const durationSeconds = head.sampleRate != null && head.sampleRate > 0 ? frames.length * ADTS_SAMPLES_PER_FRAME / head.sampleRate : null;
84036
+ return {
84037
+ codec,
84038
+ format: "adts",
84039
+ data,
84040
+ frames: frames.length,
84041
+ bytes: data.length,
84042
+ sampleRate: head.sampleRate,
84043
+ channels: head.channels,
84044
+ durationSeconds
84045
+ };
84046
+ };
84047
+ var roundRecordingFps = (fps) => {
84048
+ if (fps > 14 && fps < 16) return 15;
84049
+ if (fps > 23 && fps < 26) return 25;
84050
+ if (fps > 29 && fps < 31) return 30;
84051
+ return Math.round(fps * 100) / 100;
84052
+ };
84053
+ var estimateVideoTiming = (params) => {
84054
+ const { timestampsUs } = params;
84055
+ const infoFps = params.infoFps != null && params.infoFps > 0 ? params.infoFps : null;
84056
+ const accessUnits = timestampsUs.length;
84057
+ if (accessUnits >= 2) {
84058
+ const spanUs = timestampsUs[accessUnits - 1] - timestampsUs[0];
84059
+ if (spanUs > 0) {
84060
+ const durationSeconds = spanUs / 1e6;
84061
+ return {
84062
+ accessUnits,
84063
+ fps: roundRecordingFps((accessUnits - 1) / durationSeconds),
84064
+ durationSeconds,
84065
+ infoFps,
84066
+ fpsSource: "timestamps"
84067
+ };
84068
+ }
84069
+ }
84070
+ if (infoFps != null) return {
84071
+ accessUnits,
84072
+ fps: infoFps,
84073
+ durationSeconds: accessUnits > 0 ? accessUnits / infoFps : null,
84074
+ infoFps,
84075
+ fpsSource: "infoFps"
84076
+ };
84077
+ return {
84078
+ accessUnits,
84079
+ fps: null,
84080
+ durationSeconds: null,
84081
+ infoFps,
84082
+ fpsSource: "unknown"
84083
+ };
84084
+ };
83954
84085
  var buildDetectionClasses = (parsed, recordType) => {
83955
84086
  const classes = [];
83956
84087
  const flags = parsed?.flags;
@@ -86460,7 +86591,7 @@ var sendTalkConfigWithReset = async (params) => {
86460
86591
  channel: params.channel,
86461
86592
  ...params.channelIdOverride != null ? { channelIdOverride: params.channelIdOverride } : {},
86462
86593
  payloadXml: params.payloadXml,
86463
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24
86594
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
86464
86595
  })).header.responseCode;
86465
86596
  };
86466
86597
  const code = await trySend();
@@ -86470,7 +86601,7 @@ var sendTalkConfigWithReset = async (params) => {
86470
86601
  channel: params.channel,
86471
86602
  ...params.channelIdOverride != null ? { channelIdOverride: params.channelIdOverride } : {},
86472
86603
  payloadXml: "",
86473
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24
86604
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
86474
86605
  });
86475
86606
  const retryCode = await trySend();
86476
86607
  if (retryCode !== 200) throw new Error(`TalkConfig rejected after reset (responseCode ${retryCode})`);
@@ -86537,7 +86668,7 @@ var createBufferedTalkSession = (params) => {
86537
86668
  ...channelIdOverride != null ? { channelIdOverride } : {},
86538
86669
  extensionXml: buildBinaryExtensionXml(channel),
86539
86670
  payload: Buffer.concat(parts),
86540
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24
86671
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
86541
86672
  });
86542
86673
  const playLengthMs = samplesSent / info.audioConfig.sampleRate * 1e3;
86543
86674
  const now = Date.now();
@@ -86583,7 +86714,7 @@ var createBufferedTalkSession = (params) => {
86583
86714
  channel,
86584
86715
  ...channelIdOverride != null ? { channelIdOverride } : {},
86585
86716
  payloadXml: "",
86586
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24
86717
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
86587
86718
  });
86588
86719
  if (frame.header.responseCode !== 200) throw new Error(`TalkReset rejected (responseCode ${frame.header.responseCode})`);
86589
86720
  if (params.closeSocketOnStop) try {
@@ -88266,7 +88397,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
88266
88397
  if (debugCfg) {
88267
88398
  const sid = this.client.getSocketSessionId?.();
88268
88399
  const tag2 = sid ? `ReolinkSimpleEvent sid=${sid}` : "ReolinkSimpleEvent";
88269
- require_chunk_4KBXJ7U3.eventTraceLog(debugCfg, this.logger, tag2, `dispatch type=${evt.type} channel=${evt.channel} timestamp=${evt.timestamp}`);
88400
+ require_chunk_ZBACPERX.eventTraceLog(debugCfg, this.logger, tag2, `dispatch type=${evt.type} channel=${evt.channel} timestamp=${evt.timestamp}`);
88270
88401
  }
88271
88402
  for (const cb of this.simpleEventListeners) try {
88272
88403
  Promise.resolve(cb(evt)).catch((e) => {
@@ -88277,7 +88408,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
88277
88408
  }
88278
88409
  }
88279
88410
  constructor(opts) {
88280
- const dbg = require_chunk_4KBXJ7U3.normalizeDebugOptions(opts.debugOptions);
88411
+ const dbg = require_chunk_ZBACPERX.normalizeDebugOptions(opts.debugOptions);
88281
88412
  this.logger = createDebugGateLogger(opts.logger, dbg.general || dbg.traceNativeStream || dbg.traceRecordings || dbg.traceTalk || dbg.traceEvents || dbg.debugRtsp);
88282
88413
  this.clientOptions = {
88283
88414
  host: opts.host,
@@ -88865,7 +88996,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
88865
88996
  return;
88866
88997
  }
88867
88998
  entry.startInFlight = (async () => {
88868
- const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => require("./BaichuanVideoStream-GFJX465B-EThalnJS.js"));
88999
+ const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => require("./BaichuanVideoStream-FOE2DBVZ-DJcEkVak.js"));
88869
89000
  const sessionKey = `live:object-detections:ch${entry.channel}:${entry.profile}`;
88870
89001
  const dedicated = await this.createDedicatedSession(sessionKey);
88871
89002
  const stream$12 = new BaichuanVideoStream2({
@@ -89346,7 +89477,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
89346
89477
  channel: ch,
89347
89478
  ...channelIdOverride != null ? { channelIdOverride } : {},
89348
89479
  payloadXml: "",
89349
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24
89480
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
89350
89481
  });
89351
89482
  if (frame.header.responseCode !== 200) throw new Error(`TalkReset rejected (responseCode ${frame.header.responseCode})`);
89352
89483
  }
@@ -89365,7 +89496,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
89365
89496
  channel: ch,
89366
89497
  ...channelIdOverride != null ? { channelIdOverride } : {},
89367
89498
  payloadXml,
89368
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24
89499
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
89369
89500
  })).header.responseCode;
89370
89501
  };
89371
89502
  const code = await trySend();
@@ -89517,7 +89648,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
89517
89648
  cmdId: 10,
89518
89649
  channel,
89519
89650
  payloadXml: "",
89520
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24
89651
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
89521
89652
  });
89522
89653
  return parseTalkAbilityXml(frame.body.length === 0 ? "" : client.tryDecryptXml(frame.body, frame.header.channelId, client.enc));
89523
89654
  }
@@ -90590,8 +90721,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90590
90721
  const start = params.start;
90591
90722
  const endOfStartDay = endOfWallClockDay(start, timeZone);
90592
90723
  const end = params.end.getTime() > endOfStartDay.getTime() ? endOfStartDay : params.end;
90593
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "getVideoclips", `Query: start=${start.toISOString()}, end=${end.toISOString()} (forced same day)`);
90594
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "getVideoclips", `Using UID for channel ${channel}: ${uid}`);
90724
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclips", `Query: start=${start.toISOString()}, end=${end.toISOString()} (forced same day)`);
90725
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclips", `Using UID for channel ${channel}: ${uid}`);
90595
90726
  const streamType = params.streamType ?? "subStream";
90596
90727
  const recordType = params.recordType ?? "manual,sched,io,md,people,face,vehicle,dog_cat,visitor,other,package,crossline,intrusion,loitering,legacy,loss";
90597
90728
  const fileRecordTypes = params.fileRecordTypes ?? (params.recordType === void 0 ? DEFAULT_RECORDING_SEARCH_FILE_RECORD_TYPES : void 0);
@@ -90610,7 +90741,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90610
90741
  ...timeZone !== void 0 ? { timeZone } : {}
90611
90742
  });
90612
90743
  const unique = dedupeRecordingFiles(files);
90613
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "getVideoclips", `FileInfoList complete: ${unique.length} unique files (from ${files.length} total)`);
90744
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclips", `FileInfoList complete: ${unique.length} unique files (from ${files.length} total)`);
90614
90745
  return unique;
90615
90746
  });
90616
90747
  }
@@ -90651,7 +90782,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90651
90782
  const start = params.start;
90652
90783
  const endOfStartDay = endOfWallClockDay(start, timeZone);
90653
90784
  const end = params.end.getTime() > endOfStartDay.getTime() ? endOfStartDay : params.end;
90654
- if (end !== params.end) require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "searchAlarmVideos", `Window clamped to one camera-local day: ${params.end.toISOString()} \u2192 ${end.toISOString()} (the firmware answers the START day only)`);
90785
+ if (end !== params.end) require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "searchAlarmVideos", `Window clamped to one camera-local day: ${params.end.toISOString()} \u2192 ${end.toISOString()} (the firmware answers the START day only)`);
90655
90786
  const windows = await searchAlarmVideosViaFindAlarmVideo({
90656
90787
  sendXml: (p) => this.sendXml(p),
90657
90788
  channel,
@@ -90667,7 +90798,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90667
90798
  maxPages: params.maxPages ?? 200,
90668
90799
  ...params.timeoutMs != null ? { timeoutMs: params.timeoutMs } : {}
90669
90800
  });
90670
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "searchAlarmVideos", `findAlarmVideo complete: ${windows.length} windows over ${new Set(windows.map((w) => w.fileName)).size} files (channel=${channel})`);
90801
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "searchAlarmVideos", `findAlarmVideo complete: ${windows.length} windows over ${new Set(windows.map((w) => w.fileName)).size} files (channel=${channel})`);
90671
90802
  return windows;
90672
90803
  });
90673
90804
  }
@@ -90732,7 +90863,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90732
90863
  seen.add(key);
90733
90864
  return true;
90734
90865
  });
90735
- require_chunk_4KBXJ7U3.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`);
90866
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "searchEventLog", `findEventLog complete: ${unique.length} events over ${devices.length} device(s) (${events$6.length - unique.length} repeated across pages), ${unique.filter((e) => !e.hasRecFile).length} without a recording file`);
90736
90867
  return unique;
90737
90868
  });
90738
90869
  }
@@ -90785,7 +90916,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90785
90916
  });
90786
90917
  const msgNum = 0;
90787
90918
  dedicatedClient.subscribeVideoStream(5, msgNum);
90788
- const stream$14 = new require_chunk_4KBXJ7U3.BaichuanVideoStream({
90919
+ const stream$14 = new require_chunk_ZBACPERX.BaichuanVideoStream({
90789
90920
  client: dedicatedClient,
90790
90921
  channel,
90791
90922
  profile: streamType === "subStream" ? "sub" : "main",
@@ -90804,7 +90935,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90804
90935
  channel,
90805
90936
  channelIdOverride: sessionCounter,
90806
90937
  payloadXml,
90807
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
90938
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
90808
90939
  msgNumOverride: 0,
90809
90940
  timeoutMs: params.timeoutMs,
90810
90941
  internal: true
@@ -90836,7 +90967,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90836
90967
  cmdId: 7,
90837
90968
  channel,
90838
90969
  payloadXml: stopXml,
90839
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
90970
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
90840
90971
  timeoutMs: 2e3,
90841
90972
  internal: true
90842
90973
  });
@@ -90893,7 +91024,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90893
91024
  });
90894
91025
  const msgNum = 0;
90895
91026
  dedicatedClient.subscribeVideoStream(5, msgNum);
90896
- const stream$15 = new require_chunk_4KBXJ7U3.BaichuanVideoStream({
91027
+ const stream$15 = new require_chunk_ZBACPERX.BaichuanVideoStream({
90897
91028
  client: dedicatedClient,
90898
91029
  channel,
90899
91030
  profile: streamType === "subStream" ? "sub" : "main",
@@ -90913,7 +91044,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90913
91044
  channelIdOverride,
90914
91045
  payloadXml,
90915
91046
  extensionXml: "",
90916
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
91047
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
90917
91048
  msgNumOverride: msgNum,
90918
91049
  timeoutMs: params.timeoutMs,
90919
91050
  internal: true
@@ -90945,7 +91076,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
90945
91076
  cmdId: 7,
90946
91077
  channel,
90947
91078
  payloadXml: stopXml,
90948
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
91079
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
90949
91080
  timeoutMs: 2e3,
90950
91081
  internal: true
90951
91082
  });
@@ -91032,35 +91163,35 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
91032
91163
  async ensureUidForRecordings(channel, explicitUid) {
91033
91164
  const dbg = this.client.getDebugConfig?.();
91034
91165
  const logger = this.logger;
91035
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Checking UID: explicitUid=${explicitUid}, this.uid=${this.uid}`);
91166
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Checking UID: explicitUid=${explicitUid}, this.uid=${this.uid}`);
91036
91167
  const trimmedExplicit = explicitUid?.trim();
91037
91168
  if (trimmedExplicit) {
91038
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using explicit UID: ${trimmedExplicit}`);
91169
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using explicit UID: ${trimmedExplicit}`);
91039
91170
  return trimmedExplicit;
91040
91171
  }
91041
91172
  const perChannel = await this.discoverUidForRecordingsForChannel(channel);
91042
91173
  if (perChannel) {
91043
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID: ${perChannel}`);
91174
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID: ${perChannel}`);
91044
91175
  return perChannel;
91045
91176
  }
91046
91177
  const configured = this.uid?.trim();
91047
91178
  if (configured) {
91048
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using configured UID: ${configured}`);
91179
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using configured UID: ${configured}`);
91049
91180
  return configured;
91050
91181
  }
91051
91182
  if (this.nativeOnly) {
91052
91183
  const viaGetUid = await this.discoverUidViaGetUidNative(channel);
91053
91184
  if (viaGetUid) {
91054
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using device UID from cmd 114 GetUid: ${viaGetUid}`);
91185
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using device UID from cmd 114 GetUid: ${viaGetUid}`);
91055
91186
  return viaGetUid;
91056
91187
  }
91057
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: no UID available (channel=${channel})`);
91188
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: no UID available (channel=${channel})`);
91058
91189
  throw new Error("UID is required to access recordings in native-only mode. Provide a UID explicitly, configure the client with a UID, or wait for cmd_id=145 push cache to populate per-channel UIDs.");
91059
91190
  }
91060
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `No UID available, attempting device auto-discovery (ch=${channel})`);
91191
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `No UID available, attempting device auto-discovery (ch=${channel})`);
91061
91192
  const discovered = await this.discoverDeviceUidForRecordings(channel);
91062
91193
  if (discovered) return discovered;
91063
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Auto-discovery failed - no UID found`);
91194
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Auto-discovery failed - no UID found`);
91064
91195
  throw new Error("UID is required to access recordings. Provide a UID explicitly or configure the client with a UID.");
91065
91196
  }
91066
91197
  cacheChannelUid(channel, uid) {
@@ -91083,28 +91214,28 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
91083
91214
  const logger = this.logger;
91084
91215
  const fromPush = this.getUidFromPushCacheForChannel(channel);
91085
91216
  if (fromPush) {
91086
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from push cache: ${fromPush}`);
91217
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from push cache: ${fromPush}`);
91087
91218
  return fromPush;
91088
91219
  }
91089
91220
  if (this.nativeOnly) {
91090
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: skipping per-channel UID discovery via HTTP/CGI (channel=${channel})`);
91221
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Native-only: skipping per-channel UID discovery via HTTP/CGI (channel=${channel})`);
91091
91222
  return;
91092
91223
  }
91093
91224
  try {
91094
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Attempting per-channel UID discovery via HTTP CGI GetChannelstatus (channel=${channel})`);
91225
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Attempting per-channel UID discovery via HTTP CGI GetChannelstatus (channel=${channel})`);
91095
91226
  const uidCandidate = await discoverPerChannelUidViaCgiChannelstatus({
91096
91227
  channel,
91097
91228
  login: () => this.cgiApi.login(),
91098
91229
  getChannelstatus: () => this.cgiApi.GetChannelstatus()
91099
91230
  });
91100
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus channel=${channel} uid=${uidCandidate || "(missing)"}`);
91231
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus channel=${channel} uid=${uidCandidate || "(missing)"}`);
91101
91232
  if (uidCandidate) {
91102
91233
  this.cacheChannelUid(channel, uidCandidate);
91103
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from GetChannelstatus: ${uidCandidate}`);
91234
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `Using per-channel UID from GetChannelstatus: ${uidCandidate}`);
91104
91235
  return uidCandidate;
91105
91236
  }
91106
91237
  } catch (e) {
91107
- require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus failed: ${formatErrorForLog(e)}`);
91238
+ require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", `[HTTP CGI] GetChannelstatus failed: ${formatErrorForLog(e)}`);
91108
91239
  }
91109
91240
  }
91110
91241
  /**
@@ -91116,7 +91247,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
91116
91247
  */
91117
91248
  async discoverUidViaGetUidNative(channel) {
91118
91249
  const dbg = this.client.getDebugConfig?.();
91119
- const trace = (message) => require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, this.logger, "ensureUidForRecordings", message);
91250
+ const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, this.logger, "ensureUidForRecordings", message);
91120
91251
  let isNvr = this._isNvr;
91121
91252
  if (isNvr === void 0) try {
91122
91253
  isNvr = await this.isNvrDevice();
@@ -91142,7 +91273,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
91142
91273
  if (this.nativeOnly) return void 0;
91143
91274
  const dbg = this.client.getDebugConfig?.();
91144
91275
  const logger = this.logger;
91145
- const trace = (message) => require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", message);
91276
+ const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "ensureUidForRecordings", message);
91146
91277
  const discoveredUid = await discoverDeviceUidForRecordings({
91147
91278
  channel,
91148
91279
  getInfo: () => this.getInfo(),
@@ -91217,7 +91348,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
91217
91348
  await this.client.login();
91218
91349
  const dbg = this.client.getDebugConfig?.();
91219
91350
  const logger = this.logger;
91220
- const trace = (message) => require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "getVideoclipThumbnail", message);
91351
+ const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "getVideoclipThumbnail", message);
91221
91352
  const channel = this.normalizeChannel(params.channel);
91222
91353
  const snapStreamType = (params.snapType ?? "sub") === "main" ? "mainStream" : "subStream";
91223
91354
  const timeoutMs = params.timeoutMs ?? 3e4;
@@ -91272,7 +91403,7 @@ ${xml}`);
91272
91403
  cmdId: 298,
91273
91404
  ...isNvr && headerChannelIdOverride != null ? { channelIdOverride: headerChannelIdOverride } : {},
91274
91405
  msgNumOverride: 0,
91275
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
91406
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
91276
91407
  streamType: 0,
91277
91408
  payloadXml: xml,
91278
91409
  timeoutMs
@@ -91751,7 +91882,7 @@ ${stderr}`));
91751
91882
  await this.client.login();
91752
91883
  const dbg = this.client.getDebugConfig?.();
91753
91884
  const logger = this.logger;
91754
- const trace = (message) => require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "fileInfoListDownload", message);
91885
+ const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListDownload", message);
91755
91886
  const channel = this.normalizeChannel(params.channel);
91756
91887
  const uid = await this.ensureUidForRecordings(channel, params.uid);
91757
91888
  const headerChannelIdOverride = this.resolveHeaderChannelIdForLogicalChannel(channel);
@@ -91797,7 +91928,7 @@ ${stderr}`));
91797
91928
  cmdId: 13,
91798
91929
  channel,
91799
91930
  ...typeof chId === "number" ? { channelIdOverride: chId } : {},
91800
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_FILE_DOWNLOAD,
91931
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_FILE_DOWNLOAD,
91801
91932
  extensionXml: buildBinaryExtensionXml(channel),
91802
91933
  payloadXml,
91803
91934
  streamType: st,
@@ -91826,7 +91957,7 @@ ${stderr}`));
91826
91957
  await this.client.login();
91827
91958
  const dbg = this.client.getDebugConfig?.();
91828
91959
  const logger = this.logger;
91829
- const trace = (message) => require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "fileInfoListPagedDownload", message);
91960
+ const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListPagedDownload", message);
91830
91961
  const channel = this.normalizeChannel(params.channel);
91831
91962
  const uid = await this.ensureUidForRecordings(channel, params.uid);
91832
91963
  trace(`Starting paged download: channel=${channel}, uid=${uid}, fileName=${params.fileName}`);
@@ -91863,7 +91994,7 @@ ${stderr}`));
91863
91994
  await this.client.login();
91864
91995
  const dbg = this.client.getDebugConfig?.();
91865
91996
  const logger = this.logger;
91866
- const trace = (message) => require_chunk_4KBXJ7U3.recordingsTraceLog(dbg, logger, "fileInfoListReplayBinaryDownload", message);
91997
+ const trace = (message) => require_chunk_ZBACPERX.recordingsTraceLog(dbg, logger, "fileInfoListReplayBinaryDownload", message);
91867
91998
  const channel = this.normalizeChannel(params.channel);
91868
91999
  const headerChannelIdOverride = this.resolveHeaderChannelIdForLogicalChannel(channel);
91869
92000
  const ident = params.fileName;
@@ -91892,7 +92023,7 @@ ${stderr}`));
91892
92023
  channel,
91893
92024
  ...isNvr ? { channelIdOverride: headerChannelIdOverride ?? 82 } : {},
91894
92025
  msgNumOverride: 0,
91895
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
92026
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
91896
92027
  payloadXml,
91897
92028
  streamType: 0,
91898
92029
  timeoutMs,
@@ -91978,25 +92109,46 @@ ${stderr}`));
91978
92109
  async downloadRecordingDemuxed(params) {
91979
92110
  const raw = await this.downloadRecording(params);
91980
92111
  const frames = [];
91981
- const decoder = new require_chunk_4KBXJ7U3.BcMediaAnnexBDecoder({
92112
+ const timestampsUs = [];
92113
+ const audioFrames = [];
92114
+ const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
91982
92115
  strict: false,
91983
92116
  logger: this.logger,
91984
- onVideoAccessUnit: ({ annexB }) => {
92117
+ onVideoAccessUnit: ({ annexB, microseconds }) => {
91985
92118
  frames.push(annexB);
92119
+ timestampsUs.push(microseconds);
92120
+ },
92121
+ onAudioFrame: ({ data }) => {
92122
+ audioFrames.push(data);
91986
92123
  }
91987
92124
  });
91988
92125
  decoder.push(raw);
91989
92126
  const stats = decoder.getStats();
92127
+ const audio = buildRecordingAudioTrack({
92128
+ codec: stats.audioType,
92129
+ frames: audioFrames
92130
+ });
92131
+ const timing = estimateVideoTiming({
92132
+ timestampsUs,
92133
+ infoFps: stats.infos[0]?.fps ?? null
92134
+ });
92135
+ if (audio == null) this.logger?.debug?.(`[downloadRecordingDemuxed] no audio packet in ${stats.packets} BcMedia packets (${stats.bytesIn} bytes) for ${params.fileName}`);
91990
92136
  return {
91991
92137
  annexB: Buffer.concat(frames),
91992
92138
  videoType: stats.videoType,
92139
+ audio,
91993
92140
  stats: {
91994
92141
  bytesIn: stats.bytesIn,
91995
92142
  bytesOut: stats.bytesOut,
91996
92143
  packets: stats.packets,
91997
92144
  videoPackets: stats.videoPackets,
91998
92145
  audioPackets: stats.audioPackets,
91999
- keyframes: stats.keyframes
92146
+ audioBytes: stats.audioBytesOut,
92147
+ keyframes: stats.keyframes,
92148
+ fps: timing.fps,
92149
+ durationSeconds: timing.durationSeconds,
92150
+ infoFps: timing.infoFps,
92151
+ fpsSource: timing.fpsSource
92000
92152
  }
92001
92153
  };
92002
92154
  }
@@ -92025,7 +92177,7 @@ ${stderr}`));
92025
92177
  const videoFrames = [];
92026
92178
  const audioFrames = [];
92027
92179
  let audioCodec = null;
92028
- const decoder = new require_chunk_4KBXJ7U3.BcMediaAnnexBDecoder({
92180
+ const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
92029
92181
  strict: false,
92030
92182
  logger: this.logger,
92031
92183
  onVideoAccessUnit: ({ annexB, microseconds }) => {
@@ -92042,28 +92194,12 @@ ${stderr}`));
92042
92194
  decoder.push(raw);
92043
92195
  const decoderStats = decoder.getStats();
92044
92196
  const videoCodec = decoderStats.videoType ?? "H264";
92045
- let fps;
92046
- let durationSeconds;
92047
- if (videoFrames.length >= 2) {
92048
- const firstTs = videoFrames[0].microseconds;
92049
- const durationUs = videoFrames[videoFrames.length - 1].microseconds - firstTs;
92050
- if (durationUs > 0) {
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;
92197
+ const timing = estimateVideoTiming({
92198
+ timestampsUs: videoFrames.map((f) => f.microseconds),
92199
+ infoFps: decoderStats.infos[0]?.fps ?? null
92200
+ });
92201
+ const fps = timing.fps ?? 15;
92202
+ const durationSeconds = timing.durationSeconds ?? videoFrames.length / fps;
92067
92203
  const videoData = Buffer.concat(videoFrames.map((f) => f.annexB));
92068
92204
  const audioData = audioFrames.length > 0 ? Buffer.concat(audioFrames) : null;
92069
92205
  const hasAudio = audioData != null && audioData.length > 0;
@@ -92439,7 +92575,7 @@ ${stderr}`));
92439
92575
  params: {
92440
92576
  cmdId: 31,
92441
92577
  channelIdOverride: channel,
92442
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24
92578
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
92443
92579
  }
92444
92580
  },
92445
92581
  {
@@ -92447,7 +92583,7 @@ ${stderr}`));
92447
92583
  params: {
92448
92584
  cmdId: 31,
92449
92585
  channelIdOverride: 251,
92450
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24
92586
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
92451
92587
  }
92452
92588
  },
92453
92589
  {
@@ -92455,7 +92591,7 @@ ${stderr}`));
92455
92591
  params: {
92456
92592
  cmdId: 31,
92457
92593
  channelIdOverride: 250,
92458
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24
92594
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
92459
92595
  }
92460
92596
  },
92461
92597
  {
@@ -92463,7 +92599,7 @@ ${stderr}`));
92463
92599
  params: {
92464
92600
  cmdId: 31,
92465
92601
  channelIdOverride: 250,
92466
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
92602
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
92467
92603
  extensionXml: `<?xml version="1.0" encoding="UTF-8" ?><Extension version="1.1"><channelId>251</channelId></Extension>`
92468
92604
  }
92469
92605
  }
@@ -92506,7 +92642,7 @@ ${stderr}`));
92506
92642
  const frame = await this.client.sendFrame({
92507
92643
  cmdId: 31,
92508
92644
  channelIdOverride: Number(channel),
92509
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
92645
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
92510
92646
  timeoutMs
92511
92647
  });
92512
92648
  lastCode = frame.header.responseCode;
@@ -92787,7 +92923,7 @@ ${stderr}`));
92787
92923
  msgNumOverride: msgNum,
92788
92924
  extensionXml: buildChannelExtensionXml(channelId),
92789
92925
  payloadXml,
92790
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
92926
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
92791
92927
  streamType: config.streamType,
92792
92928
  timeoutMs: 2e4
92793
92929
  };
@@ -92919,7 +93055,7 @@ ${stderr}`));
92919
93055
  channelIdOverride: channelId,
92920
93056
  extensionXml: a.extensionXml,
92921
93057
  payloadXml: a.payloadXml,
92922
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
93058
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
92923
93059
  streamType: a.streamType,
92924
93060
  ...msgNum !== void 0 ? { msgNumOverride: msgNum } : {},
92925
93061
  timeoutMs: 2e3
@@ -92949,7 +93085,7 @@ ${stderr}`));
92949
93085
  channel: ch,
92950
93086
  channelIdOverride: channelId,
92951
93087
  extensionXml: buildChannelExtensionXml(channelId),
92952
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
93088
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
92953
93089
  streamType: 0
92954
93090
  });
92955
93091
  } catch (e) {
@@ -92991,7 +93127,7 @@ ${stderr}`));
92991
93127
  channel: ch,
92992
93128
  channelIdOverride: channelId,
92993
93129
  extensionXml: buildChannelExtensionXml(channelId),
92994
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
93130
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
92995
93131
  streamType: 0
92996
93132
  });
92997
93133
  } catch (e) {
@@ -93013,7 +93149,7 @@ ${stderr}`));
93013
93149
  channelIdOverride: channelId,
93014
93150
  extensionXml,
93015
93151
  payloadXml,
93016
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
93152
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
93017
93153
  streamType: 0
93018
93154
  });
93019
93155
  if (frame.header.responseCode !== 200) {
@@ -93045,7 +93181,7 @@ ${stderr}`));
93045
93181
  channelIdOverride: channelId,
93046
93182
  extensionXml,
93047
93183
  payloadXml,
93048
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
93184
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
93049
93185
  streamType: 0
93050
93186
  });
93051
93187
  if (frame.header.responseCode !== 200) throw new Error(`PTZ preset move rejected (response_code ${frame.header.responseCode})`);
@@ -93066,7 +93202,7 @@ ${stderr}`));
93066
93202
  channelIdOverride: channelId,
93067
93203
  extensionXml,
93068
93204
  payloadXml,
93069
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
93205
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
93070
93206
  streamType: 0
93071
93207
  });
93072
93208
  if (frame.header.responseCode !== 200) throw new Error(`PTZ preset save rejected (response_code ${frame.header.responseCode})`);
@@ -93083,7 +93219,7 @@ ${stderr}`));
93083
93219
  channelIdOverride: channelId,
93084
93220
  extensionXml,
93085
93221
  payloadXml,
93086
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
93222
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
93087
93223
  streamType: 0
93088
93224
  });
93089
93225
  if (frame.header.responseCode !== 200) throw new Error(`PTZ goto preset rejected (response_code ${frame.header.responseCode})`);
@@ -93110,7 +93246,7 @@ ${stderr}`));
93110
93246
  channelIdOverride: channelId,
93111
93247
  extensionXml,
93112
93248
  payloadXml,
93113
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
93249
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
93114
93250
  streamType: 0
93115
93251
  })).header.responseCode };
93116
93252
  },
@@ -93164,7 +93300,7 @@ ${stderr}`));
93164
93300
  channel: ch,
93165
93301
  channelIdOverride: ch,
93166
93302
  extensionXml: buildPtzGuardExtensionXml(ch, "read"),
93167
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
93303
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
93168
93304
  streamType: 0
93169
93305
  }));
93170
93306
  }
@@ -93190,7 +93326,7 @@ ${stderr}`));
93190
93326
  timeoutSeconds: options.timeoutSeconds,
93191
93327
  setPosition: options.setPosition === true
93192
93328
  }),
93193
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
93329
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
93194
93330
  streamType: 0
93195
93331
  });
93196
93332
  }
@@ -93211,7 +93347,7 @@ ${stderr}`));
93211
93347
  channelIdOverride: ch,
93212
93348
  extensionXml: buildPtzGuardExtensionXml(ch, "write"),
93213
93349
  payloadXml: buildPtzGuardGoXml(ch),
93214
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
93350
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
93215
93351
  streamType: 0
93216
93352
  });
93217
93353
  }
@@ -93227,7 +93363,7 @@ ${stderr}`));
93227
93363
  channel: ch,
93228
93364
  channelIdOverride: channelId,
93229
93365
  extensionXml: buildChannelExtensionXml(channelId),
93230
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
93366
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
93231
93367
  streamType: 0
93232
93368
  });
93233
93369
  const parseTriplet = (sectionTag) => {
@@ -93265,7 +93401,7 @@ ${stderr}`));
93265
93401
  channelIdOverride: channelId,
93266
93402
  extensionXml,
93267
93403
  payloadXml,
93268
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24,
93404
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24,
93269
93405
  streamType: 0
93270
93406
  });
93271
93407
  if (frame.header.responseCode !== 200) throw new Error(`Zoom rejected (response_code ${frame.header.responseCode})`);
@@ -95016,7 +95152,7 @@ ${xml}`);
95016
95152
  * @returns Test results for all stream types and profiles
95017
95153
  */
95018
95154
  async testChannelStreams(channel, logger) {
95019
- const { testChannelStreams } = await Promise.resolve().then(() => require("./DiagnosticsTools-DZ5RWXSM-CD4rRF8a.js"));
95155
+ const { testChannelStreams } = await Promise.resolve().then(() => require("./DiagnosticsTools-ZC7I7QM6-CD4rRF8a.js"));
95020
95156
  return await testChannelStreams({
95021
95157
  api: this,
95022
95158
  channel: this.normalizeChannel(channel),
@@ -95032,7 +95168,7 @@ ${xml}`);
95032
95168
  * @returns Complete diagnostics for all channels and streams
95033
95169
  */
95034
95170
  async collectMultifocalDiagnostics(logger) {
95035
- const { collectMultifocalDiagnostics } = await Promise.resolve().then(() => require("./DiagnosticsTools-DZ5RWXSM-CD4rRF8a.js"));
95171
+ const { collectMultifocalDiagnostics } = await Promise.resolve().then(() => require("./DiagnosticsTools-ZC7I7QM6-CD4rRF8a.js"));
95036
95172
  return await collectMultifocalDiagnostics({
95037
95173
  api: this,
95038
95174
  logger
@@ -97117,7 +97253,7 @@ ${scheduleItems}
97117
97253
  if (raw.length === 0) throw new Error("Downloaded recording is empty");
97118
97254
  const videoFrames = [];
97119
97255
  let videoType = null;
97120
- const decoder = new require_chunk_4KBXJ7U3.BcMediaAnnexBDecoder({
97256
+ const decoder = new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
97121
97257
  strict: false,
97122
97258
  logger: this.logger,
97123
97259
  onVideoAccessUnit: ({ annexB: annexB2, microseconds }) => {
@@ -97465,7 +97601,7 @@ ${scheduleItems}
97465
97601
  return {
97466
97602
  getPlaylist: () => {
97467
97603
  try {
97468
- const { readFileSync } = require_chunk_4KBXJ7U3.__require("fs");
97604
+ const { readFileSync } = require_chunk_ZBACPERX.__require("fs");
97469
97605
  return readFileSync(playlistPath, "utf8");
97470
97606
  } catch {
97471
97607
  return "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:4\n";
@@ -97474,7 +97610,7 @@ ${scheduleItems}
97474
97610
  getSegment: (name) => {
97475
97611
  if (segments.has(name)) return segments.get(name);
97476
97612
  try {
97477
- const { readFileSync } = require_chunk_4KBXJ7U3.__require("fs");
97613
+ const { readFileSync } = require_chunk_ZBACPERX.__require("fs");
97478
97614
  const data = readFileSync(path$3.join(tempDir, name));
97479
97615
  segments.set(name, data);
97480
97616
  return data;
@@ -97484,7 +97620,7 @@ ${scheduleItems}
97484
97620
  },
97485
97621
  listSegments: () => {
97486
97622
  try {
97487
- const { readdirSync } = require_chunk_4KBXJ7U3.__require("fs");
97623
+ const { readdirSync } = require_chunk_ZBACPERX.__require("fs");
97488
97624
  return readdirSync(tempDir).filter((f) => f.endsWith(".ts"));
97489
97625
  } catch {
97490
97626
  return [];
@@ -99069,41 +99205,41 @@ async function autoDetectDeviceType(inputs) {
99069
99205
  variant: "cmd80 class=0x6414",
99070
99206
  op: () => api.getInfo(void 0, {
99071
99207
  timeoutMs: 2500,
99072
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24
99208
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
99073
99209
  })
99074
99210
  },
99075
99211
  {
99076
99212
  variant: "cmd80 class=0x6614",
99077
99213
  op: () => api.getInfo(void 0, {
99078
99214
  timeoutMs: 3e3,
99079
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_20
99215
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
99080
99216
  })
99081
99217
  },
99082
99218
  {
99083
99219
  variant: "cmd318(ch0) class=0x6414",
99084
99220
  op: () => api.getInfo(0, {
99085
99221
  timeoutMs: 3e3,
99086
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24
99222
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
99087
99223
  })
99088
99224
  },
99089
99225
  {
99090
99226
  variant: "cmd318(ch0) class=0x6614",
99091
99227
  op: () => api.getInfo(0, {
99092
99228
  timeoutMs: 3500,
99093
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_20
99229
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
99094
99230
  })
99095
99231
  }
99096
99232
  ]), runProbeVariants("getSupportInfo", [{
99097
99233
  variant: "cmd199 class=0x6414",
99098
99234
  op: () => api.getSupportInfo({
99099
99235
  timeoutMs: 2500,
99100
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_24
99236
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_24
99101
99237
  })
99102
99238
  }, {
99103
99239
  variant: "cmd199 class=0x6614",
99104
99240
  op: () => api.getSupportInfo({
99105
99241
  timeoutMs: 3500,
99106
- messageClass: require_chunk_4KBXJ7U3.BC_CLASS_MODERN_20
99242
+ messageClass: require_chunk_ZBACPERX.BC_CLASS_MODERN_20
99107
99243
  })
99108
99244
  }])]);
99109
99245
  const deviceInfo = infoProbe?.value;
@@ -144094,7 +144230,7 @@ function buildAacAudioSpecificConfigHex(params) {
144094
144230
  if (!Number.isFinite(audioObjectType) || audioObjectType <= 0 || audioObjectType > 31) return;
144095
144231
  return ((audioObjectType & 31) << 11 | (samplingFreqIndex & 15) << 7 | (channels & 15) << 3).toString(16).padStart(4, "0");
144096
144232
  }
144097
- function parseAdtsHeader(adtsFrame) {
144233
+ function parseAdtsHeader2(adtsFrame) {
144098
144234
  if (adtsFrame.length < 7) return null;
144099
144235
  if (adtsFrame[0] !== 255 || (adtsFrame[1] & 240) !== 240) return null;
144100
144236
  const protectionAbsent = (adtsFrame[1] & 1) === 1;
@@ -144199,7 +144335,7 @@ function packetizeH265(nal, rtp, opts, markerOnLast, isLastNal) {
144199
144335
  return out;
144200
144336
  }
144201
144337
  function packetizeAacAdtsFrame(adts, rtp) {
144202
- const parsed = parseAdtsHeader(adts);
144338
+ const parsed = parseAdtsHeader2(adts);
144203
144339
  if (!parsed) return { packets: [] };
144204
144340
  const raw = adts.subarray(parsed.headerLength);
144205
144341
  if (!raw.length) return { packets: [] };
@@ -145632,7 +145768,7 @@ async function createRfc4571TcpServerInternal(options) {
145632
145768
  streamClient = dedicatedSession.client;
145633
145769
  logSessionsSummary(`dedicated session created: ${sessionKey}`);
145634
145770
  } else streamClient = baseApi.client;
145635
- const createLiveStream = async () => new require_chunk_4KBXJ7U3.BaichuanVideoStream({
145771
+ const createLiveStream = async () => new require_chunk_ZBACPERX.BaichuanVideoStream({
145636
145772
  client: streamClient,
145637
145773
  api: baseApi,
145638
145774
  channel: ch,
@@ -145849,7 +145985,7 @@ async function createRfc4571TcpServerInternal(options) {
145849
145985
  }, isCompositeStream ? Math.min(1e4, keyframeTimeoutMs) : 5e3);
145850
145986
  const onAudio = (frame) => {
145851
145987
  sawAnyAudio = true;
145852
- const parsed = parseAdtsHeader(frame);
145988
+ const parsed = parseAdtsHeader2(frame);
145853
145989
  if (!parsed) {
145854
145990
  if (debugLogsLeft-- > 0) {
145855
145991
  const head = frame.subarray(0, Math.min(16, frame.length)).toString("hex");
@@ -147032,6 +147168,23 @@ var reolinkCameraSchema = object({
147032
147168
  motionEnabled: boolean().optional(),
147033
147169
  motionSensitivity: number().int().min(1).max(50).optional(),
147034
147170
  /**
147171
+ * Per-device clip-catalog refresh interval, in minutes — the OVERRIDE half
147172
+ * of the cascade whose global default lives in the addon store under the
147173
+ * same key. Absent means "use the global", which is the same thing a reset
147174
+ * means (`resolveClipCatalogRefreshMinutes`, D562). `0` turns the timer off
147175
+ * for this camera. What the timer DOES with it differs by topology and the
147176
+ * field description says both: a free read on a hub child and on mains, and
147177
+ * stale-marking only on a standalone battery camera.
147178
+ */
147179
+ clipCatalogRefreshMinutes: number().int().min(0).max(1440).optional(),
147180
+ /**
147181
+ * Let the clip timer WAKE this standalone battery camera. Off by default and
147182
+ * per device only — a 15-minute forced timer is 96 wakes/day against a
147183
+ * natural rate of 1–20 clips/day on this fleet (D562). Inert on every other
147184
+ * topology, which is why it is one switch and not a second meaning.
147185
+ */
147186
+ clipCatalogForcedWake: boolean().optional(),
147187
+ /**
147035
147188
  * Per-AI-class sensitivity overrides — pushed via
147036
147189
  * `setAiDetection(aiType, sensitivity)` (cmdId=343). Reolink
147037
147190
  * range 0..100 — same scale the reolink_aio CGI exposes
@@ -147576,6 +147729,20 @@ var ClipCatalogCache = class ClipCatalogCache {
147576
147729
  }
147577
147730
  return null;
147578
147731
  }
147732
+ /**
147733
+ * Every day key this device holds a listing for, in no particular order.
147734
+ *
147735
+ * It is what lets an availability probe say WHEN a catalog was last
147736
+ * confirmed without listing anything: a camera nobody may wake reports the
147737
+ * age of what is held, and a source with nothing held says so rather than
147738
+ * claiming to be current.
147739
+ */
147740
+ daysHeld(deviceId) {
147741
+ const out = [];
147742
+ const prefix = `${deviceId}|`;
147743
+ for (const key of this.slots.keys()) if (key.startsWith(prefix)) out.push(key.slice(prefix.length));
147744
+ return out;
147745
+ }
147579
147746
  /** Every row held for a device, in no particular order. The prefetch sorts. */
147580
147747
  allRows(deviceId) {
147581
147748
  const out = [];
@@ -147629,6 +147796,57 @@ var ClipCatalogCache = class ClipCatalogCache {
147629
147796
  }
147630
147797
  };
147631
147798
  //#endregion
147799
+ //#region src/clips/clip-still-refusal.ts
147800
+ /** "You are in the queue." */
147801
+ function deferStill(code, reason, retryAfterMs) {
147802
+ return {
147803
+ ok: false,
147804
+ code,
147805
+ disposition: "deferred",
147806
+ reason,
147807
+ retryAfterMs
147808
+ };
147809
+ }
147810
+ /** "This will not happen." */
147811
+ function refuseStill(code, reason) {
147812
+ return {
147813
+ ok: false,
147814
+ code,
147815
+ disposition: "final",
147816
+ reason
147817
+ };
147818
+ }
147819
+ /**
147820
+ * The queue's own refusal — the only failure that is about the LANE rather
147821
+ * than about the camera, and therefore the only one that is deferred.
147822
+ *
147823
+ * It carries its own `retryAfterMs` because the queue is the only thing that
147824
+ * knows it: the depth it is holding, or the exact instant a host's backoff
147825
+ * closes.
147826
+ */
147827
+ var ThumbQueueRefusedError = class extends Error {
147828
+ code;
147829
+ retryAfterMs;
147830
+ constructor(code, message, retryAfterMs) {
147831
+ super(message);
147832
+ this.name = "ThumbQueueRefusedError";
147833
+ this.code = code;
147834
+ this.retryAfterMs = retryAfterMs;
147835
+ }
147836
+ };
147837
+ /**
147838
+ * A thrown value from the queue, as a wire answer — or `null` when it was not
147839
+ * the queue at all.
147840
+ *
147841
+ * `null` is not a default: it is what makes the caller name `camera-refused`
147842
+ * explicitly, instead of a socket close quietly inheriting "come back later"
147843
+ * and a tile retrying four times against a camera that is not there.
147844
+ */
147845
+ function queueRefusal(err) {
147846
+ if (!(err instanceof ThumbQueueRefusedError)) return null;
147847
+ return deferStill(err.code, err.message, err.retryAfterMs);
147848
+ }
147849
+ //#endregion
147632
147850
  //#region src/clips/clip-data-plane.ts
147633
147851
  /**
147634
147852
  * The addon's two data-plane handlers — the still and the bytes.
@@ -147650,6 +147868,19 @@ var ClipCatalogCache = class ClipCatalogCache {
147650
147868
  * which. Waiting instead would hold a request open behind a camera that is
147651
147869
  * asleep for the night.
147652
147870
  *
147871
+ * **And it says whether to COME BACK.** Those reasons are two kinds of answer:
147872
+ * `queue-full` and `camera-backoff` mean the still is still coming, the rest
147873
+ * mean it will not. The branch that refused decides — nothing here maps a code
147874
+ * to a disposition, and no client parses one out of the prose — and the answer
147875
+ * travels as its own headers (`x-camstack-reason-code`,
147876
+ * `x-camstack-disposition`, plus `retry-after` on a deferred one) beside the
147877
+ * prose, which stays verbatim because a `camera-refused` is only ever
147878
+ * explained by the camera's own words.
147879
+ *
147880
+ * A response with no disposition header is read as FINAL by every surface,
147881
+ * which is exactly how this route behaved before: an older addon under a newer
147882
+ * admin UI keeps the old behaviour instead of gaining a retry loop.
147883
+ *
147653
147884
  * ## Why Range is hand-rolled here
147654
147885
  *
147655
147886
  * One file, one path, no root mapping — and `createReadStream().pipe(res)`
@@ -147692,26 +147923,45 @@ function createClipThumbHandler(deps) {
147692
147923
  try {
147693
147924
  minted = await deps.mint(deviceId, clipKey);
147694
147925
  } catch (err) {
147695
- minted = {
147696
- ok: false,
147697
- reason: err instanceof Error ? err.message : String(err)
147698
- };
147926
+ minted = refuseStill("camera-refused", err instanceof Error ? err.message : String(err));
147699
147927
  }
147700
147928
  if (!minted.ok) {
147701
- deps.logger.debug("videoclips: thumbnail not minted", {
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();
147929
+ writeRefusal(res, minted, deviceId, deps.logger, clipKey);
147710
147930
  return;
147711
147931
  }
147712
147932
  writeJpeg(res, minted.bytes, req.method === "HEAD");
147713
147933
  };
147714
147934
  }
147935
+ /**
147936
+ * The 204, with everything the tile needs to decide what to draw next.
147937
+ *
147938
+ * A FINAL refusal is work that was dropped, so it is warned, per camera. A
147939
+ * DEFERRED one dropped nothing — the still is queued behind another — and
147940
+ * warning it would put 43 lines on the log for one 60-tile page against a
147941
+ * 16-deep lane, which is how a real drop stops being visible.
147942
+ */
147943
+ function writeRefusal(res, refusal, deviceId, logger, clipKey) {
147944
+ const line = "videoclips: thumbnail not minted";
147945
+ const extras = {
147946
+ tags: { deviceId },
147947
+ meta: {
147948
+ clipKey,
147949
+ code: refusal.code,
147950
+ disposition: refusal.disposition,
147951
+ reason: refusal.reason
147952
+ }
147953
+ };
147954
+ if (refusal.disposition === "final") logger.warn(line, extras);
147955
+ else logger.debug(line, extras);
147956
+ const retryAfterMs = refusal.disposition === "deferred" ? refusal.retryAfterMs : void 0;
147957
+ res.writeHead(204, {
147958
+ [CLIP_STILL_REASON_HEADER]: refusal.reason.slice(0, 500),
147959
+ [CLIP_STILL_REASON_CODE_HEADER]: refusal.code,
147960
+ [CLIP_STILL_DISPOSITION_HEADER]: refusal.disposition,
147961
+ ...retryAfterMs === void 0 ? {} : { "retry-after": String(Math.max(1, Math.ceil(retryAfterMs / 1e3))) }
147962
+ });
147963
+ res.end();
147964
+ }
147715
147965
  function writeJpeg(res, bytes, headOnly) {
147716
147966
  res.writeHead(200, {
147717
147967
  "content-type": "image/jpeg",
@@ -147830,21 +148080,73 @@ function createClipPlaybackHandler(deps) {
147830
148080
  //#region src/clips/clip-mux.ts
147831
148081
  /** A clip that has not transferred by here is not going to. */
147832
148082
  var CLIP_MUX_TIMEOUT_MS = 6e4;
148083
+ /**
148084
+ * Decide what to do with the track the download returned.
148085
+ *
148086
+ * The gate is `format === 'adts'` and NOT `audio !== null`: an `adpcm` or
148087
+ * `unknown` track stream-copied into MP4 produces a file whose audio track is
148088
+ * garbage, which is worse than a clip that is honestly silent. A track that
148089
+ * arrived with no frames is no track — counting it would promise sound the
148090
+ * file does not have.
148091
+ */
148092
+ function decideClipAudio(audio) {
148093
+ if (audio === null) return {
148094
+ kind: "silent",
148095
+ reason: "no-audio-track",
148096
+ format: null
148097
+ };
148098
+ if (audio.frames === 0 || audio.bytes === 0) return {
148099
+ kind: "silent",
148100
+ reason: "no-audio-track",
148101
+ format: audio.format
148102
+ };
148103
+ if (audio.format !== "adts") return {
148104
+ kind: "silent",
148105
+ reason: "audio-not-copyable",
148106
+ format: audio.format
148107
+ };
148108
+ return {
148109
+ kind: "copy",
148110
+ format: "adts",
148111
+ data: audio.data
148112
+ };
148113
+ }
148114
+ function usableFps(fps) {
148115
+ if (fps === null || fps === void 0) return null;
148116
+ return Number.isFinite(fps) && fps > 0 ? fps : null;
148117
+ }
147833
148118
  function buildClipMuxArgs(input) {
148119
+ const demuxer = input.videoType === "H265" ? "hevc" : "h264";
148120
+ const fps = usableFps(input.fps);
147834
148121
  return [
147835
148122
  "-hide_banner",
147836
148123
  "-loglevel",
147837
148124
  "error",
147838
148125
  "-nostdin",
147839
148126
  "-y",
148127
+ ...fps !== null ? ["-r", String(fps)] : [],
147840
148128
  "-f",
147841
- input.videoType === "H265" ? "hevc" : "h264",
148129
+ demuxer,
147842
148130
  "-i",
147843
148131
  input.inputPath,
147844
- "-c",
148132
+ ...input.audio !== void 0 ? [
148133
+ "-f",
148134
+ "aac",
148135
+ "-i",
148136
+ input.audio.path
148137
+ ] : [],
148138
+ "-c:v",
147845
148139
  "copy",
148140
+ ...input.audio !== void 0 ? [
148141
+ "-c:a",
148142
+ "copy",
148143
+ "-bsf:a",
148144
+ "aac_adtstoasc"
148145
+ ] : [],
147846
148146
  "-movflags",
147847
148147
  "+faststart",
148148
+ "-f",
148149
+ "mp4",
147848
148150
  input.outputPath
147849
148151
  ];
147850
148152
  }
@@ -147931,6 +148233,40 @@ function describeMuxFailure(run) {
147931
148233
  if (run.code !== 0) return `mux-failed (exit ${String(run.code)}): ${run.stderr.slice(0, 300)}`;
147932
148234
  return null;
147933
148235
  }
148236
+ //#endregion
148237
+ //#region src/clips/clip-thumb-queue.ts
148238
+ /**
148239
+ * The provider's OWN CoverPreview lane — two queues, one in flight per host.
148240
+ *
148241
+ * The library already serialises CoverPreview: one in flight per api instance,
148242
+ * a 50-deep FIFO that throws past that, and a process-wide host-keyed backoff
148243
+ * that doubles on a 400. What a FIFO cannot express is PRIORITY, and the two
148244
+ * kinds of mint this feature has are not equal — an operator staring at a tile
148245
+ * must not wait behind twenty prefetches for clips nobody opened.
148246
+ *
148247
+ * So the provider keeps two queues of its own, hands the library at most ONE
148248
+ * CoverPreview per host at a time, drains `interactive` before `prefetch`, and
148249
+ * re-checks between every prefetch mint (D549 16).
148250
+ *
148251
+ * ## Two things it refuses to do
148252
+ *
148253
+ * **Wait when the queue is full.** A request past the bound is REFUSED with
148254
+ * `queue-full`, so the route can answer 204 with a reason. A request held open
148255
+ * behind forty-nine others is a spinner the operator cannot cancel, and the
148256
+ * camera will have rolled the clip over before it is served.
148257
+ *
148258
+ * **Let a prefetch probe a sulking host.** While a host's 400 backoff is open
148259
+ * a prefetch is skipped entirely; only an interactive request may probe it,
148260
+ * because only an interactive request has somebody waiting for the answer.
148261
+ *
148262
+ * The backoff mirrors the library's (1 s doubling to 30 s) rather than
148263
+ * replacing it: this one decides whether to ASK at all, the library's decides
148264
+ * how long to wait once asked.
148265
+ */
148266
+ /** How long one waiting mint is worth, as a retry hint. Coarse on purpose:
148267
+ * the client floors it with its own backoff and caps its own attempts, so a
148268
+ * wrong hint can neither cause a hammer nor a hang. */
148269
+ var THUMB_DRAIN_HINT_PER_WAITER_MS = 1e3;
147934
148270
  var THUMB_BACKOFF_INITIAL_MS = 1e3;
147935
148271
  var THUMB_BACKOFF_MAX_MS = 3e4;
147936
148272
  /**
@@ -147951,6 +148287,28 @@ var ClipThumbQueue = class {
147951
148287
  const state = this.hosts.get(host);
147952
148288
  return state !== void 0 && state.backoffUntilMs > this.now();
147953
148289
  }
148290
+ /**
148291
+ * Is this host busy with, or owed, operator-facing work?
148292
+ *
148293
+ * The periodic catalog refresh asks before it spends the host's one lane:
148294
+ * an operator staring at a tile must never wait behind a timer, and a host
148295
+ * whose 400 backoff is open must not be probed by anything that has nobody
148296
+ * waiting for the answer — the same rule that already keeps a PREFETCH off
148297
+ * a sulking host.
148298
+ *
148299
+ * Either a mint is in flight or the host is sulking. There is deliberately
148300
+ * no third clause for "an interactive request is queued": a queued request
148301
+ * means a mint is running, so `running` already covers it, and a clause
148302
+ * that can never be the reason for the answer is a leftover that reads as
148303
+ * verification. The lane is one per host, so a PREFETCH holding it defers
148304
+ * the refresh too — which is right: the refresh is the lowest-priority
148305
+ * thing on this host.
148306
+ */
148307
+ busy(host) {
148308
+ const state = this.hosts.get(host);
148309
+ if (state === void 0) return false;
148310
+ return state.backoffUntilMs > this.now() || state.running;
148311
+ }
147954
148312
  backoffUntil(host) {
147955
148313
  return this.hosts.get(host)?.backoffUntilMs ?? 0;
147956
148314
  }
@@ -147973,8 +148331,8 @@ var ClipThumbQueue = class {
147973
148331
  */
147974
148332
  async submit(job) {
147975
148333
  const state = this.stateFor(job.host);
147976
- if (job.lane === "prefetch" && state.backoffUntilMs > this.now()) throw new Error(`camera-backoff: host ${job.host} refused a CoverPreview recently; prefetch skipped until the backoff closes (only an interactive request probes it)`);
147977
- if (state.waiting.length >= 16) throw new Error(`queue-full: ${state.waiting.length} thumbnail mints already waiting for host ${job.host}`);
148334
+ if (job.lane === "prefetch" && state.backoffUntilMs > this.now()) throw new ThumbQueueRefusedError("camera-backoff", `camera-backoff: host ${job.host} refused a CoverPreview recently; prefetch skipped until the backoff closes (only an interactive request probes it)`, state.backoffUntilMs - this.now());
148335
+ if (state.waiting.length >= 16) throw new ThumbQueueRefusedError("queue-full", `queue-full: ${state.waiting.length} thumbnail mints already waiting for host ${job.host}`, state.waiting.length * THUMB_DRAIN_HINT_PER_WAITER_MS);
147978
148336
  return new Promise((resolve, reject) => {
147979
148337
  state.waiting.push({
147980
148338
  lane: job.lane,
@@ -147994,7 +148352,7 @@ var ClipThumbQueue = class {
147994
148352
  if (next === void 0) return;
147995
148353
  state.waiting.splice(pick, 1);
147996
148354
  if (next.lane === "prefetch" && state.backoffUntilMs > this.now()) {
147997
- next.reject(/* @__PURE__ */ new Error(`camera-backoff: host ${host} is backing off; prefetch skipped`));
148355
+ next.reject(new ThumbQueueRefusedError("camera-backoff", `camera-backoff: host ${host} is backing off; prefetch skipped`, state.backoffUntilMs - this.now()));
147998
148356
  this.drain(host);
147999
148357
  return;
148000
148358
  }
@@ -148047,12 +148405,13 @@ var ClipThumbQueue = class {
148047
148405
  * exactly — 29 events, 28 inside the 20 listed files, and all 20 files
148048
148406
  * carrying an event. They are two VIEWS of one store.
148049
148407
  *
148050
- * So the operator collapsed them (2026-09-20): one source, which picks the
148051
- * best API for what the camera IS. Which view it resolved to is REPORTED
148052
- * (`ClipSource.via`, and the listing log line) — a source that silently reads
148053
- * a different API on two cameras and then behaves differently is the thing
148054
- * nobody can debug later — but it is never a row the operator has to
148055
- * understand.
148408
+ * So the operator collapsed them (2026-09-20): one source, which picked the
148409
+ * best API for what the camera IS, and REPORTED the pick on `ClipSource.via`.
148410
+ * Later the same day the pick went too (D565): `FileInfoList` is the catalog
148411
+ * on every topology, so `via` had one value and is gone. What is left is one
148412
+ * source reading one API — and `clipKeyFor` below is still keyed off
148413
+ * `(channel, fileName)` rather than the source, which is now simply true
148414
+ * rather than a concession.
148056
148415
  */
148057
148416
  var CLIP_SOURCE_ONBOARD = "native:reolink:onboard";
148058
148417
  /** Every source this provider can name. Order is the picker's order. */
@@ -148103,10 +148462,10 @@ function parseClipId(clipId) {
148103
148462
  }
148104
148463
  /**
148105
148464
  * 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: the two
148107
- * views of a hub child's store are two views of the SAME file, so a thumbnail
148108
- * minted for the files view is the same picture the events view wants and
148109
- * minting it twice would cost the camera a second CoverPreview.
148465
+ * MP4). Derived from `(channel, fileName)` and NOT from the source: a
148466
+ * thumbnail belongs to the FILE, so any surface that names the same file
148467
+ * finds the same picture and the camera is never asked for a second
148468
+ * CoverPreview of it.
148110
148469
  *
148111
148470
  * A hash rather than the encoded name because a hub `<Id>` is a full path and
148112
148471
  * the encoded form is long enough to hit a file-name limit.
@@ -148255,7 +148614,7 @@ function startOf(file) {
148255
148614
  return file.startTime?.getTime() ?? 0;
148256
148615
  }
148257
148616
  var partsCache = /* @__PURE__ */ new Map();
148258
- function formatterFor(timeZone) {
148617
+ function formatterFor$1(timeZone) {
148259
148618
  const cached = partsCache.get(timeZone);
148260
148619
  if (cached !== void 0) return cached;
148261
148620
  const made = new Intl.DateTimeFormat("en-US", {
@@ -148272,7 +148631,7 @@ function formatterFor(timeZone) {
148272
148631
  return made;
148273
148632
  }
148274
148633
  function partsAt(epochMs, timeZone) {
148275
- const parts = formatterFor(timeZone).formatToParts(new Date(epochMs));
148634
+ const parts = formatterFor$1(timeZone).formatToParts(new Date(epochMs));
148276
148635
  const read = (type) => {
148277
148636
  const found = parts.find((p) => p.type === type);
148278
148637
  return found === void 0 ? 0 : Number(found.value);
@@ -148343,36 +148702,6 @@ function enumerateWallClockDays(sinceMs, untilMs, timeZone) {
148343
148702
  //#endregion
148344
148703
  //#region src/clips/clip-catalog.ts
148345
148704
  /**
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
148705
  * Locate the disk record structurally.
148377
148706
  *
148378
148707
  * The library's declared `HddInfoListConfig` and the captured wire disagree —
@@ -148457,6 +148786,30 @@ function monthsBetween(sinceMs, untilMs, timeZone) {
148457
148786
  }
148458
148787
  return out;
148459
148788
  }
148789
+ /**
148790
+ * Is this file still being written?
148791
+ *
148792
+ * A Reolink recording is named `<prefix>_DST<date>_<start>_<end>_<hex>_<hex>`,
148793
+ * and while it is open the camera writes the END stamp as `000000` and the
148794
+ * size as `0` — measured on 592, 2026-09-20:
148795
+ * `RecS03_DST20260920_192927_000000_2B1E808_0.mp4`. Its `<endTime>` block is
148796
+ * FROZEN at whatever the index entry held when it was created (19:29:56, read
148797
+ * three times over fifteen minutes while the file grew), so a duration drawn
148798
+ * from it is a lie about the one row the operator sees first.
148799
+ *
148800
+ * BOTH signals are required. A recording that happens to end at midnight is
148801
+ * named `_000000_` and is closed; its bytes are what separate the two. Nothing
148802
+ * on the wire distinguishes them otherwise, and a false positive here would
148803
+ * replace a correct duration with `Recording…` for ever. Re-measured
148804
+ * read-only the same evening: of 355 sub and 355 main files on that camera's
148805
+ * day, zero carried a `_000000_` segment and zero carried a size of 0.
148806
+ */
148807
+ function isFileStillBeingWritten(file) {
148808
+ if (file.sizeBytes !== void 0 && file.sizeBytes > 0) return false;
148809
+ return OPEN_END_STAMP.test(file.fileName);
148810
+ }
148811
+ /** The end-stamp field of an open recording, between its own separators. */
148812
+ var OPEN_END_STAMP = /_000000_/;
148460
148813
  function handleOf(file) {
148461
148814
  const id = file.id;
148462
148815
  return id !== void 0 && id !== "" ? id : file.fileName;
@@ -148497,139 +148850,123 @@ async function listDayRows(api, input) {
148497
148850
  ...pair.sub.sizeBytes !== void 0 ? { subBytes: pair.sub.sizeBytes } : {},
148498
148851
  ...pair.main !== void 0 ? { mainFileName: handleOf(pair.main) } : {},
148499
148852
  ...pair.main?.sizeBytes !== void 0 ? { mainBytes: pair.main.sizeBytes } : {},
148853
+ ...pair.main?.startTime !== void 0 ? { mainStartMs: pair.main.startTime.getTime() } : {},
148500
148854
  supportSub: pair.sub.supportSub !== false,
148501
- nativeTypes: parseNativeTypes(pair.sub.recordType)
148855
+ nativeTypes: parseNativeTypes(pair.sub.recordType),
148856
+ ...isFileStillBeingWritten(pair.sub) ? { inProgress: true } : {}
148502
148857
  };
148503
148858
  }),
148504
148859
  indexEmpty: subs.length === 0,
148505
148860
  ...mainSearchFailed !== void 0 ? { mainSearchFailed } : {}
148506
148861
  };
148507
148862
  }
148508
- //#endregion
148509
- //#region src/clips/clip-event-catalog.ts
148510
- /**
148511
- * The HUB events view — the API a HUB CHILD's one source resolves to (D549 19,
148512
- * D555).
148513
- *
148514
- * On a hub child the catalog is the EVENT list, not the file list: 29 events
148515
- * in 293 ms against 20 files in 503 ms, and it is the ONLY class source there
148516
- * because a hub child's files come back with `recordType none`. The file view
148517
- * reads the SAME store from the other side — the hub's uid addresses no
148518
- * catalog at all while the child's addresses all of it, and cmd 5 serves
148519
- * identical bytes either way. Two views, one store. They were two SOURCES for
148520
- * a few hours; the operator collapsed them once that measurement landed, so
148521
- * this module is now one of the two ways the single source reads itself, and
148522
- * the file reader is the other (`clip-catalog.ts`).
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
148863
+ /**
148864
+ * Every alarm window of ONE camera-local day.
148865
+ *
148866
+ * The one-day clamp is the firmware's, not ours: an 18→19 September ask
148867
+ * answered 462 windows all dated the 18th. The caller owns the iteration, and
148868
+ * this is asked from inside the day loop the listing already runs.
148869
+ */
148870
+ async function readAlarmWindows(api, input) {
148871
+ const { startMs, endMs } = dayBounds(input.day, input.timeZone);
148872
+ return await api.searchAlarmVideos({
148873
+ channel: input.channel,
148874
+ start: new Date(startMs),
148875
+ end: new Date(endMs),
148876
+ streamType: 1,
148877
+ timeZone: input.timeZone,
148878
+ ...input.uid !== void 0 ? { uid: input.uid } : {}
148579
148879
  });
148580
- const rows = [];
148581
- for (const entry of entries) {
148582
- if (entry.uid !== input.uid) continue;
148583
- const startMs = entry.startTime?.getTime();
148584
- if (startMs === void 0 || !Number.isFinite(startMs)) continue;
148585
- rows.push({
148586
- channel: input.channel,
148587
- startMs,
148588
- endMs: entry.endTime?.getTime() ?? startMs,
148589
- nativeTypes: [...entry.alarmTypes],
148590
- hasRecFile: entry.hasRecFile
148591
- });
148880
+ }
148881
+ /** The camera declined to class this file: `none`, or no `recordType` at all. */
148882
+ function fileCarriesNoClass(nativeTypes) {
148883
+ return nativeTypes.every((type) => type === "none");
148884
+ }
148885
+ /**
148886
+ * Should this day's labels be asked for separately?
148887
+ *
148888
+ * Only when NOTHING in the day carried a class. One file saying `md` is proof
148889
+ * that this camera writes `recordType`, and then the free answer is the
148890
+ * answer — paying 8–9 s to restate it is the cheap-question rule inverted.
148891
+ */
148892
+ function dayNeedsAlarmLabels(rows) {
148893
+ return rows.length > 0 && rows.every((row) => fileCarriesNoClass(row.nativeTypes));
148894
+ }
148895
+ /**
148896
+ * Attach each window's `alarmType`s to the file whose START it names.
148897
+ *
148898
+ * A row the camera already classed is returned untouched: the file's own
148899
+ * `recordType` is the authority wherever it exists, and a per-window answer
148900
+ * must not narrow it. A window that names no listed file contributes nothing
148901
+ * and is COUNTED — it is never attached to the nearest neighbour, which is how
148902
+ * a surface ends up showing one recording's class on another's row.
148903
+ */
148904
+ function joinAlarmLabels(rows, windows, timeZone) {
148905
+ const byStamp = /* @__PURE__ */ new Map();
148906
+ for (const window of windows) {
148907
+ const key = window.fileName.slice(-STAMP_LENGTH);
148908
+ const existing = byStamp.get(key);
148909
+ const target = existing ?? [];
148910
+ if (existing === void 0) byStamp.set(key, target);
148911
+ for (const type of window.alarmTypes) if (type !== "" && !target.includes(type)) target.push(type);
148592
148912
  }
148593
- return rows.toSorted((a, b) => b.startMs - a.startMs);
148913
+ const claimed = /* @__PURE__ */ new Set();
148914
+ let matched = 0;
148915
+ return {
148916
+ rows: rows.map((row) => {
148917
+ if (!fileCarriesNoClass(row.nativeTypes)) return row;
148918
+ const subKey = alarmJoinKey(row.startMs, timeZone);
148919
+ const mainKey = row.mainStartMs !== void 0 ? alarmJoinKey(row.mainStartMs, timeZone) : void 0;
148920
+ const key = byStamp.has(subKey) ? subKey : mainKey !== void 0 && byStamp.has(mainKey) ? mainKey : void 0;
148921
+ if (key === void 0) return row;
148922
+ const types = byStamp.get(key);
148923
+ if (types === void 0 || types.length === 0) return row;
148924
+ claimed.add(key);
148925
+ matched += 1;
148926
+ return {
148927
+ ...row,
148928
+ nativeTypes: [...types]
148929
+ };
148930
+ }),
148931
+ matched,
148932
+ unmatchedWindows: byStamp.size - claimed.size
148933
+ };
148594
148934
  }
148595
- function joinEventsToFiles(events, files) {
148596
- return events.map((event) => {
148597
- if (!event.hasRecFile) return {
148598
- ...event,
148599
- clipKey: eventKey(event),
148600
- playable: false,
148601
- unplayableReason: "no-rec-file"
148602
- };
148603
- const file = files.find((candidate) => event.startMs >= candidate.startMs - 3e3 && event.endMs <= candidate.endMs + 3e3);
148604
- if (file === void 0) return {
148605
- ...event,
148606
- clipKey: eventKey(event),
148607
- playable: false,
148608
- unplayableReason: "no-file-for-window"
148609
- };
148610
- return {
148611
- ...event,
148612
- clipKey: file.clipKey,
148613
- fileName: file.subFileName,
148614
- playable: true
148615
- };
148935
+ /** `YYYYMMDDHHMMSS` — 14 digits. */
148936
+ var STAMP_LENGTH = 14;
148937
+ var FORMATTER_BY_ZONE = /* @__PURE__ */ new Map();
148938
+ function formatterFor(timeZone) {
148939
+ const cached = FORMATTER_BY_ZONE.get(timeZone);
148940
+ if (cached !== void 0) return cached;
148941
+ const made = new Intl.DateTimeFormat("en-CA", {
148942
+ timeZone,
148943
+ year: "numeric",
148944
+ month: "2-digit",
148945
+ day: "2-digit",
148946
+ hour: "2-digit",
148947
+ minute: "2-digit",
148948
+ second: "2-digit",
148949
+ hour12: false
148616
148950
  });
148951
+ FORMATTER_BY_ZONE.set(timeZone, made);
148952
+ return made;
148617
148953
  }
148618
- /** A key for a row with no file — stable, and its OWN. */
148619
- function eventKey(event) {
148620
- return clipKeyFor(event.channel, `event:${event.startMs}-${event.endMs}`);
148954
+ /**
148955
+ * The 14-digit tail of the short name a window carries — the file's start in
148956
+ * the CAMERA's wall clock. Never a UTC stamp: 00:30 Rome on the 17th is 22:30
148957
+ * UTC on the 16th, and a UTC key would file it under the wrong day and join it
148958
+ * to nothing.
148959
+ */
148960
+ function alarmJoinKey(atMs, timeZone) {
148961
+ const parts = new Map(formatterFor(timeZone).formatToParts(new Date(atMs)).map((part) => [part.type, part.value]));
148962
+ return `${parts.get("year") ?? ""}${parts.get("month") ?? ""}${parts.get("day") ?? ""}${normaliseHour(parts.get("hour"))}${parts.get("minute") ?? ""}${parts.get("second") ?? ""}`;
148963
+ }
148964
+ /** `hourCycle: h23` still renders midnight as `24` in some ICU builds. */
148965
+ function normaliseHour(hour) {
148966
+ return hour === "24" ? "00" : hour ?? "";
148621
148967
  }
148622
148968
  //#endregion
148623
148969
  //#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
148970
  function createReolinkVideoclipsProvider(deps) {
148634
148971
  /** Last known availability per `deviceId|source`, written by every read. */
148635
148972
  const health = /* @__PURE__ */ new Map();
@@ -148753,7 +149090,7 @@ function createReolinkVideoclipsProvider(deps) {
148753
149090
  error: result.mainSearchFailed
148754
149091
  }
148755
149092
  });
148756
- return result.rows;
149093
+ return await labelDay(device, api, day, result.rows);
148757
149094
  });
148758
149095
  if (entry.clips.length === 0) emptyDays.push(day);
148759
149096
  rows.push(...entry.clips);
@@ -148781,11 +149118,7 @@ function createReolinkVideoclipsProvider(deps) {
148781
149118
  days: emptyDays
148782
149119
  }
148783
149120
  });
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
- });
149121
+ noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, indexEmpty(emptyDays));
148789
149122
  } else noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, {
148790
149123
  state: "ok",
148791
149124
  catalogAsOf: deps.now()
@@ -148793,140 +149126,55 @@ function createReolinkVideoclipsProvider(deps) {
148793
149126
  return rows;
148794
149127
  };
148795
149128
  /**
148796
- * Read the HUB's event view for one child.
149129
+ * Give a day's rows a class when the FILES carried none.
148797
149130
  *
148798
- * ONE `searchEventLog` open for the whole window — the event list is
148799
- * multi-day by construction, so a per-day loop would pay the open/close per
148800
- * day for the same rows. The bound is the WINDOW, and it has a number
148801
- * behind it: 59 events over 3 children and 3 days took 2 372 ms, 1 886
148802
- * events over 20 days took 11 908 ms. The elapsed time and the row count
148803
- * are logged per camera so "why is this camera slow" is answerable.
149131
+ * The gate is what the day actually said, never what the camera is: a hub
149132
+ * child's files are `recordType: none` on every row (measured), a
149133
+ * standalone's carry `md,people,dog_cat,sched`, and a camera that changes
149134
+ * firmware changes which of those it is without this code learning a new
149135
+ * topology. The cheap question comes first (D54) — on 592 an alarm search
149136
+ * costs 8.2–9.5 s for 1 271 windows to restate labels already on the row.
148804
149137
  *
148805
- * The day's FILE rows come from the `onboard` reader, cache-backed, because
148806
- * `findEventLog` names events and not files: the join is by window
148807
- * containment. Losing the file list costs PLAYABILITY, not the events — the
148808
- * rows still list, each saying why it cannot play.
149138
+ * A refusal costs the LABELS and nothing else. Losing the day because its
149139
+ * class could not be read would be the upgrade destroying the thing it
149140
+ * upgrades — the same rule the MAIN search already lives by.
148809
149141
  */
148810
- const readHubEvents = async (device, since, until) => {
149142
+ const labelDay = async (device, api, day, rows) => {
148811
149143
  const tags = { deviceId: device.deviceId };
148812
- if (!device.isHubChild) return [];
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
- }
149144
+ if (!dayNeedsAlarmLabels(rows)) return rows;
148847
149145
  const startedAt = deps.now();
148848
- let events = [];
148849
- let joined = [];
149146
+ let windows;
148850
149147
  try {
148851
- const rows = await listEventRows(api, {
148852
- uid,
149148
+ windows = await readAlarmWindows(api, {
148853
149149
  channel: device.channel,
149150
+ uid: device.uid,
148854
149151
  timeZone: device.timeZone,
148855
- since,
148856
- until
149152
+ day
148857
149153
  });
148858
- events = rows;
148859
- joined = joinEventsToFiles(rows, await readOnboard(device, since, until).catch(() => []));
148860
149154
  } catch (err) {
148861
- const reason = err instanceof Error ? err.message : String(err);
148862
- deps.logger.warn("videoclips: the hub event log refused", {
149155
+ deps.logger.warn("videoclips: the label search refused — the day lists without classes", {
148863
149156
  tags,
148864
149157
  meta: {
148865
149158
  branch: "camera-refused",
148866
- via: CATALOG_VIA_EVENTS,
148867
- error: reason
149159
+ day,
149160
+ error: err instanceof Error ? err.message : String(err)
148868
149161
  }
148869
149162
  });
148870
- noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, {
148871
- state: "unreachable",
148872
- reason
148873
- });
148874
- return [];
149163
+ return rows;
148875
149164
  }
148876
- const unplayable = joined.filter((row) => !row.playable).length;
148877
- deps.logger.info("videoclips: read the hub event log", {
149165
+ const joined = joinAlarmLabels(rows, windows, device.timeZone);
149166
+ deps.logger.info("videoclips: read the classes a day of files does not carry", {
148878
149167
  tags,
148879
149168
  meta: {
148880
- via: CATALOG_VIA_EVENTS,
148881
- events: events.length,
148882
- unplayable,
148883
- elapsedMs: deps.now() - startedAt,
148884
- windowDays: Math.max(1, Math.round((until - since) / 864e5))
149169
+ day,
149170
+ windows: windows.length,
149171
+ rows: rows.length,
149172
+ labelled: joined.matched,
149173
+ unmatchedWindows: joined.unmatchedWindows,
149174
+ elapsedMs: deps.now() - startedAt
148885
149175
  }
148886
149176
  });
148887
- noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, {
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
- };
149177
+ return joined.rows;
148930
149178
  };
148931
149179
  const toClip = async (device, source, row, catalogAsOf) => {
148932
149180
  rowsByKey.set(`${device.deviceId}|${row.clipKey}`, row);
@@ -148945,6 +149193,7 @@ function createReolinkVideoclipsProvider(deps) {
148945
149193
  startMs: row.startMs,
148946
149194
  endMs: row.endMs
148947
149195
  },
149196
+ ...row.inProgress === true ? { inProgress: true } : {},
148948
149197
  ...classification.labels.length > 0 ? { labels: [...classification.labels] } : {},
148949
149198
  ...classification.behaviours.length > 0 ? { behaviours: [...classification.behaviours] } : {},
148950
149199
  ...classification.nativeTypes.length > 0 ? { nativeTypes: [...classification.nativeTypes] } : {},
@@ -148975,36 +149224,26 @@ function createReolinkVideoclipsProvider(deps) {
148975
149224
  listSources: async ({ deviceId }) => {
148976
149225
  const device = await deps.resolveDevice(deviceId);
148977
149226
  if (device === null) return [];
148978
- const via = viaFor(device);
148979
149227
  return [{
148980
149228
  source: CLIP_SOURCE_ONBOARD,
148981
149229
  addonId: REOLINK_ADDON_ID,
148982
149230
  label: "Camera storage",
148983
- via,
148984
- availability: await resolveAvailability(device, via)
149231
+ availability: await resolveAvailability(device)
148985
149232
  }];
148986
149233
  },
148987
149234
  listClips: async ({ deviceId, since, until, limit }) => {
148988
149235
  const device = await deps.resolveDevice(deviceId);
148989
149236
  if (device === null) return [];
148990
149237
  const tags = { deviceId };
148991
- const via = viaFor(device);
148992
- const ordered = (via === "events" ? await (async () => {
148993
- const events = await readHubEvents(device, since, until);
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);
149238
+ const rows = await readOnboard(device, since, until);
149239
+ const asOf = health.get(healthKey(deviceId, CLIP_SOURCE_ONBOARD))?.availability.catalogAsOf;
149240
+ const ordered = (await Promise.all(rows.toSorted((a, b) => b.startMs - a.startMs).map((row) => toClip(device, CLIP_SOURCE_ONBOARD, row, asOf)))).toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
149001
149241
  const capped = limit !== void 0 ? ordered.slice(0, limit) : ordered;
149002
149242
  const vouched = capped.filter((c) => c.thumbnail !== void 0).length;
149003
149243
  deps.logger.info("videoclips: listed a Reolink catalog", {
149004
149244
  tags,
149005
149245
  meta: {
149006
149246
  source: CLIP_SOURCE_ONBOARD,
149007
- via,
149008
149247
  clips: capped.length,
149009
149248
  truncated: limit !== void 0 && ordered.length > limit,
149010
149249
  unplayable: capped.filter((c) => c.playable === false).length,
@@ -149013,6 +149252,18 @@ function createReolinkVideoclipsProvider(deps) {
149013
149252
  availability: health.get(healthKey(deviceId, CLIP_SOURCE_ONBOARD))?.availability.state
149014
149253
  }
149015
149254
  });
149255
+ const availability = health.get(healthKey(deviceId, CLIP_SOURCE_ONBOARD))?.availability;
149256
+ if (capped.length === 0 && availability !== void 0 && availability.state !== "ok") {
149257
+ deps.logger.warn("videoclips: nothing to list, and the source cannot answer", {
149258
+ tags,
149259
+ meta: {
149260
+ branch: "refused-empty",
149261
+ state: availability.state,
149262
+ reason: availability.reason ?? null
149263
+ }
149264
+ });
149265
+ throw new Error(`videoclips: device ${deviceId} listed no clips, and that is not an empty day — the source is "${availability.state}"` + (availability.reason !== void 0 ? `: ${availability.reason}` : "."));
149266
+ }
149016
149267
  return capped;
149017
149268
  },
149018
149269
  getClipPlayback: async ({ deviceId, clipId, profile }) => {
@@ -149021,7 +149272,6 @@ function createReolinkVideoclipsProvider(deps) {
149021
149272
  const device = await deps.resolveDevice(deviceId);
149022
149273
  if (device === null) throw new Error(`videoclips: device ${deviceId} is not a Reolink camera`);
149023
149274
  if (device.isBattery && device.sleeping && !device.isHubChild) throw new Error(`videoclips: device ${deviceId} is a standalone battery camera and is sleeping — fetching a clip would wake it. Use "Wake and refresh" first.`);
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
149275
  const row = rowsByKey.get(`${deviceId}|${clipKeyFor(parsed.channel, parsed.fileName)}`) ?? null;
149026
149276
  const wantsMain = profile === "high" && row?.mainFileName !== void 0;
149027
149277
  const fileName = wantsMain ? row?.mainFileName ?? parsed.fileName : parsed.fileName;
@@ -149040,46 +149290,350 @@ function createReolinkVideoclipsProvider(deps) {
149040
149290
  }
149041
149291
  };
149042
149292
  /**
149043
- * Which API this camera's ONE source reads.
149044
- *
149045
- * From what the camera IS, never from a stored preference and never from a
149046
- * row the operator ticked: a hub child is served by its parent's event log
149047
- * (faster, and the only class source there), a standalone by its own file
149048
- * list (a standalone answers `findEventLog` with an empty body). Pure, so
149049
- * the choice is the same on every call and the reported `via` cannot drift
149050
- * from the API that actually ran.
149051
- */
149052
- function viaFor(device) {
149053
- return device.isHubChild ? CATALOG_VIA_EVENTS : CATALOG_VIA_FILES;
149054
- }
149055
- async function resolveAvailability(device, via) {
149293
+ * What a source is saying right now — MEASURED, never defaulted.
149294
+ *
149295
+ * A listing that already ran is the better answer and wins: `listClips`
149296
+ * asked the whole window and this probe asks one day, so re-deciding from
149297
+ * the narrower question would throw away 618's other nineteen empty days.
149298
+ * With nothing established, the probe runs — and `ok` is only ever returned
149299
+ * because something was really read.
149300
+ *
149301
+ * ## What it costs, per call, and why
149302
+ *
149303
+ * This used to answer `ok` after at most one storage command, and `ok` is
149304
+ * the one state whose list may be read as complete. On the live hub, on the
149305
+ * cold runners a deploy had just produced, that default made 640 (no SD
149306
+ * card) and 618 (an index that lists nothing for days its own calendar
149307
+ * marks) both answer `ok` and then an empty list — the exact lie this line
149308
+ * of work exists to end. So `listSources` pays for its answer:
149309
+ *
149310
+ * - **a standalone battery camera: NOTHING, ever.** No `getApi`, no command,
149311
+ * not even the cheap one — on UDP the login IS the wake. `sleeping`, with
149312
+ * the age of whatever is cached. The gate is `isBattery`, never the
149313
+ * BELIEF `sleeping`: that belief is `state.battery.sleeping ?? false`, so
149314
+ * a slice that has not restored reads as awake, and a probe on it is a
149315
+ * wake nobody asked for.
149316
+ * - **a hub child: one `getDayRecords` through the parent's existing socket**
149317
+ * (65 ms measured on 3628). The child never hears it; the storage question
149318
+ * is the HUB's and is not asked.
149319
+ * - **a mains standalone: `getHddInfoList` (ms) + `getDayRecords`
149320
+ * (38 ms–1.9 s) + ONE day listing of the NEWEST day the calendar claims**
149321
+ * (0.3–6 s, and cache-backed — `serveAndRevalidate`, so a day already
149322
+ * listed costs nothing and a day listed here is free for the `listClips`
149323
+ * that follows). The day listing is what distinguishes `index-empty` from
149324
+ * `ok`, and nothing cheaper can: `no-storage` is the cheap question and it
149325
+ * comes first precisely so a camera with no card never pays the rest.
149326
+ *
149327
+ * The calendar is read for the CURRENT month only. A picker asks "can this
149328
+ * source answer", not "what does it hold since April", and the measured
149329
+ * defect (618) is in the month the operator is looking at.
149330
+ */
149331
+ async function resolveAvailability(device) {
149056
149332
  const known = health.get(healthKey(device.deviceId, CLIP_SOURCE_ONBOARD))?.availability;
149057
149333
  if (known !== void 0) return known;
149058
- if (via === "events") return uidIsMissing(device) ? {
149059
- state: "unreachable",
149060
- reason: UID_MISSING_REASON
149061
- } : { state: "ok" };
149062
- if (device.isBattery && device.sleeping && !device.isHubChild) return {
149063
- state: "sleeping",
149064
- reason: "A read would wake this camera. Nothing has been listed yet."
149334
+ const availability = await probeAvailability(device);
149335
+ noteHealth(device.deviceId, CLIP_SOURCE_ONBOARD, availability);
149336
+ return availability;
149337
+ }
149338
+ async function probeAvailability(device) {
149339
+ const tags = { deviceId: device.deviceId };
149340
+ if (device.isBattery && !device.isHubChild) {
149341
+ const asOf = newestCachedAsOf(device.deviceId);
149342
+ deps.logger.warn("videoclips: not probing a standalone battery camera — login is the wake", {
149343
+ tags,
149344
+ meta: {
149345
+ branch: "sleeping",
149346
+ believedSleeping: device.sleeping,
149347
+ confirmedAt: asOf
149348
+ }
149349
+ });
149350
+ return {
149351
+ state: "sleeping",
149352
+ reason: asOf === void 0 ? "A read would wake this standalone battery camera. Nothing has been confirmed yet." : "A read would wake this standalone battery camera; showing the catalog as last confirmed.",
149353
+ ...asOf !== void 0 ? { catalogAsOf: asOf } : {}
149354
+ };
149355
+ }
149356
+ if (uidIsMissing(device)) {
149357
+ deps.logger.warn("videoclips: no uid for this camera — its catalog cannot be addressed", {
149358
+ tags,
149359
+ meta: {
149360
+ branch: "uid-missing",
149361
+ probe: true
149362
+ }
149363
+ });
149364
+ return {
149365
+ state: "unreachable",
149366
+ reason: UID_MISSING_REASON
149367
+ };
149368
+ }
149369
+ let api;
149370
+ try {
149371
+ api = await device.getApi();
149372
+ } catch (err) {
149373
+ return unreachableFrom(device, "camera-refused", err);
149374
+ }
149375
+ if (!device.isHubChild) {
149376
+ const storage = await readStorageState(api);
149377
+ if (storage.state === "absent") {
149378
+ deps.logger.info("videoclips: no SD card mounted — nothing to list", {
149379
+ tags,
149380
+ meta: {
149381
+ branch: "no-storage",
149382
+ probe: true
149383
+ }
149384
+ });
149385
+ return {
149386
+ state: "no-storage",
149387
+ reason: "The camera reports no mounted storage (HddInfo mount 0)."
149388
+ };
149389
+ }
149390
+ if (storage.state === "unknown") {
149391
+ deps.logger.warn("videoclips: the storage question was not answered", {
149392
+ tags,
149393
+ meta: {
149394
+ branch: "camera-refused",
149395
+ probe: true,
149396
+ error: storage.reason ?? null
149397
+ }
149398
+ });
149399
+ return {
149400
+ state: "unreachable",
149401
+ reason: storage.reason ?? "The camera did not answer the storage question (HddInfo unreadable)."
149402
+ };
149403
+ }
149404
+ }
149405
+ const today = wallClockDayKey(deps.now(), device.timeZone);
149406
+ let calendar;
149407
+ try {
149408
+ calendar = await readDayIndex(api, {
149409
+ channel: device.channel,
149410
+ uid: device.uid,
149411
+ months: [{
149412
+ year: Number(today.slice(0, 4)),
149413
+ month: Number(today.slice(5, 7))
149414
+ }]
149415
+ });
149416
+ } catch (err) {
149417
+ return unreachableFrom(device, "camera-refused", err);
149418
+ }
149419
+ const claimed = [...calendar].toSorted().toReversed();
149420
+ if (claimed.length === 0) return {
149421
+ state: "ok",
149422
+ catalogAsOf: deps.now()
149423
+ };
149424
+ const newest = claimed[0];
149425
+ if (newest === void 0) return {
149426
+ state: "ok",
149427
+ catalogAsOf: deps.now()
149065
149428
  };
149066
- if (uidIsMissing(device)) return {
149429
+ try {
149430
+ await deps.cache.serveAndRevalidate(device.deviceId, newest, { closed: newest !== today }, async () => (await listDayRows(api, {
149431
+ channel: device.channel,
149432
+ uid: device.uid,
149433
+ timeZone: device.timeZone,
149434
+ day: newest
149435
+ })).rows);
149436
+ } catch (err) {
149437
+ return unreachableFrom(device, "camera-refused", err);
149438
+ }
149439
+ const emptyClaimed = claimed.filter((day) => deps.cache.peek(device.deviceId, day)?.clips.length === 0);
149440
+ if (emptyClaimed.length > 0) {
149441
+ deps.logger.warn("videoclips: the calendar marks days the index will not list", {
149442
+ tags,
149443
+ meta: {
149444
+ branch: "index-empty",
149445
+ probe: true,
149446
+ days: emptyClaimed
149447
+ }
149448
+ });
149449
+ return indexEmpty(emptyClaimed);
149450
+ }
149451
+ return {
149452
+ state: "ok",
149453
+ catalogAsOf: deps.now()
149454
+ };
149455
+ }
149456
+ function unreachableFrom(device, branch, err) {
149457
+ const reason = err instanceof Error ? err.message : String(err);
149458
+ deps.logger.warn("videoclips: the source could not be probed", {
149459
+ tags: { deviceId: device.deviceId },
149460
+ meta: {
149461
+ branch,
149462
+ probe: true,
149463
+ error: reason
149464
+ }
149465
+ });
149466
+ return {
149067
149467
  state: "unreachable",
149068
- reason: UID_MISSING_REASON
149468
+ reason
149069
149469
  };
149470
+ }
149471
+ /** When any day of this device's catalog was last confirmed, newest first. */
149472
+ function newestCachedAsOf(deviceId) {
149473
+ let newest;
149474
+ for (const day of deps.cache.daysHeld(deviceId)) {
149475
+ const asOf = deps.cache.peek(deviceId, day)?.catalogAsOf;
149476
+ if (asOf !== void 0 && (newest === void 0 || asOf > newest)) newest = asOf;
149477
+ }
149478
+ return newest;
149479
+ }
149480
+ /** The one wording for "the camera's calendar and its own index disagree". */
149481
+ function indexEmpty(days) {
149482
+ return {
149483
+ state: "index-empty",
149484
+ reason: `The camera’s calendar marks ${days.length} day(s) with footage that its own index lists no files for: ${days.join(", ")}.`,
149485
+ catalogAsOf: deps.now()
149486
+ };
149487
+ }
149488
+ }
149489
+ //#endregion
149490
+ //#region src/clips/clip-refresh.ts
149491
+ /** The settings key, global default and per-device override (D562). */
149492
+ var CLIP_CATALOG_REFRESH_MINUTES_KEY = "clipCatalogRefreshMinutes";
149493
+ /** The settings key, per device only, OFF by default (D562). */
149494
+ var CLIP_CATALOG_FORCED_WAKE_KEY = "clipCatalogForcedWake";
149495
+ /** A day. Past this the catalog's own TTLs are the thing doing the work. */
149496
+ var CLIP_CATALOG_REFRESH_MINUTES_MAX = 1440;
149497
+ /**
149498
+ * How much of the catalog one refresh confirms.
149499
+ *
149500
+ * A day. The listing cost is per DAY (0.3–6 s on a standalone, 267 ms for one
149501
+ * child-day on the hub) and a 20-day window measured 3.6–5.4 s per child, so a
149502
+ * timer that re-confirmed everything would spend the whole budget re-reading
149503
+ * days that cannot change. Older days are immutable and the cache holds them
149504
+ * under their own 24 h TTL.
149505
+ */
149506
+ var CLIP_CATALOG_REFRESH_WINDOW_MS = 1440 * 6e4;
149507
+ /**
149508
+ * The mode for one camera. Pure, and decided from what the camera IS — never
149509
+ * from a belief about whether it is asleep right now: the gate has to hold for
149510
+ * a camera whose sleep slice has not restored, which reads "awake" and is how
149511
+ * a picker draw once logged into 640 (D561).
149512
+ */
149513
+ function resolveClipRefreshMode(topology) {
149514
+ if (topology.isHubChild) return "via-hub";
149515
+ if (!topology.isBattery) return "direct";
149516
+ return topology.forcedWake ? "forced-wake" : "mark-stale";
149517
+ }
149518
+ /**
149519
+ * The refresh interval for one camera: the per-device override on top of the
149520
+ * global default, with **reset == unset == the default**.
149521
+ *
149522
+ * Never throws on a bad blob. An invalid value at either level resolves to the
149523
+ * level above it, so a store written by an older release cannot stop the timer
149524
+ * — the same cascade `stationary-settings.ts` uses, for the same reason.
149525
+ */
149526
+ function resolveClipCatalogRefreshMinutes(globalValue, deviceValue) {
149527
+ return asRefreshMinutes(deviceValue) ?? asRefreshMinutes(globalValue) ?? 30;
149528
+ }
149529
+ function asRefreshMinutes(value) {
149530
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
149531
+ const rounded = Math.round(value);
149532
+ if (rounded === 0) return 0;
149533
+ if (rounded < 5) return null;
149534
+ if (rounded > 1440) return null;
149535
+ return rounded;
149536
+ }
149537
+ /** Is the operator's forced-wake switch on for this camera? */
149538
+ function resolveClipCatalogForcedWake(value) {
149539
+ return value === true;
149540
+ }
149541
+ /**
149542
+ * The timer. `tick()` is the whole surface, so a test drives it from COLD and
149543
+ * the scheduler owns no clock of its own — the addon supplies the interval.
149544
+ */
149545
+ var ClipCatalogRefreshScheduler = class {
149546
+ deps;
149547
+ /** Last time an interval was SPENT on a camera, successful or not. */
149548
+ lastRunAt = /* @__PURE__ */ new Map();
149549
+ /** Cameras whose refresh has not come back yet. */
149550
+ inFlight = /* @__PURE__ */ new Set();
149551
+ constructor(deps) {
149552
+ this.deps = deps;
149553
+ }
149554
+ async tick() {
149555
+ const now = this.deps.now();
149556
+ const due = this.deps.listTargets().filter((target) => this.isDue(target, now));
149557
+ for (const target of due) await this.run(target, now);
149558
+ }
149559
+ isDue(target, now) {
149560
+ if (target.intervalMinutes <= 0) return false;
149561
+ if (this.inFlight.has(target.deviceId)) return false;
149562
+ const last = this.lastRunAt.get(target.deviceId);
149563
+ if (last === void 0) return true;
149564
+ return now - last >= target.intervalMinutes * 6e4;
149565
+ }
149566
+ async run(target, now) {
149567
+ const tags = { deviceId: target.deviceId };
149568
+ const mode = resolveClipRefreshMode(target);
149569
+ if (this.deps.hostBusy(target.hostKey)) {
149570
+ this.deps.logger.warn("videoclips: catalog refresh deferred — the host is serving a read", {
149571
+ tags,
149572
+ meta: {
149573
+ branch: "deferred-interactive",
149574
+ mode,
149575
+ host: target.hostKey
149576
+ }
149577
+ });
149578
+ return;
149579
+ }
149580
+ if (mode === "mark-stale") {
149581
+ this.deps.markStale(target.deviceId);
149582
+ this.lastRunAt.set(target.deviceId, now);
149583
+ this.deps.logger.info("videoclips: catalog marked stale — a timer cannot read a standalone battery camera", {
149584
+ tags,
149585
+ meta: {
149586
+ branch: "mark-stale",
149587
+ mode,
149588
+ refreshesOn: "onWakeTransition",
149589
+ intervalMinutes: target.intervalMinutes
149590
+ }
149591
+ });
149592
+ return;
149593
+ }
149594
+ this.lastRunAt.set(target.deviceId, now);
149595
+ this.inFlight.add(target.deviceId);
149070
149596
  try {
149071
- if ((await readStorageState(await device.getApi())).state === "absent") return {
149072
- state: "no-storage",
149073
- reason: "The camera reports no mounted storage (HddInfo mount 0)."
149074
- };
149075
- return { state: "ok" };
149597
+ if (mode === "forced-wake") try {
149598
+ await this.deps.wake(target.deviceId);
149599
+ } catch (err) {
149600
+ this.deps.markStale(target.deviceId);
149601
+ this.deps.logger.warn("videoclips: the authorised wake did not bring the camera up — catalog left stale", {
149602
+ tags,
149603
+ meta: {
149604
+ branch: "forced-wake-failed",
149605
+ mode,
149606
+ error: errMessage$1(err)
149607
+ }
149608
+ });
149609
+ return;
149610
+ }
149611
+ await this.deps.refresh(target.deviceId);
149612
+ this.deps.logger.info("videoclips: catalog refreshed", {
149613
+ tags,
149614
+ meta: {
149615
+ branch: "refreshed",
149616
+ mode,
149617
+ elapsedMs: this.deps.now() - now
149618
+ }
149619
+ });
149076
149620
  } catch (err) {
149077
- return {
149078
- state: "unreachable",
149079
- reason: err instanceof Error ? err.message : String(err)
149080
- };
149621
+ this.deps.markStale(target.deviceId);
149622
+ this.deps.logger.warn("videoclips: catalog refresh failed — the catalog stays as it was", {
149623
+ tags,
149624
+ meta: {
149625
+ branch: "refresh-failed",
149626
+ mode,
149627
+ error: errMessage$1(err)
149628
+ }
149629
+ });
149630
+ } finally {
149631
+ this.inFlight.delete(target.deviceId);
149081
149632
  }
149082
149633
  }
149634
+ };
149635
+ function errMessage$1(err) {
149636
+ return err instanceof Error ? err.message : String(err);
149083
149637
  }
149084
149638
  function createClipService(deps) {
149085
149639
  const now = deps.now ?? (() => Date.now());
@@ -149110,23 +149664,11 @@ function createClipService(deps) {
149110
149664
  };
149111
149665
  const mintThumbnail = async (deviceId, clipKey, lane) => {
149112
149666
  const device = await deps.resolveDevice(deviceId);
149113
- if (device === null) return {
149114
- ok: false,
149115
- reason: `unknown-device: ${deviceId} is not a Reolink camera here`
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
- };
149667
+ if (device === null) return refuseStill("unknown-device", `unknown-device: ${deviceId} is not a Reolink camera here`);
149668
+ if (device.isBattery && device.sleeping && !device.isHubChild) return refuseStill("sleeping", "sleeping: a still would wake this standalone battery camera");
149669
+ if (uidIsMissing(device)) return refuseStill("uid-missing", UID_MISSING_REASON);
149125
149670
  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
- };
149671
+ if (row === null) return refuseStill("no-catalog-row", "no-catalog-row: list this day before asking for its stills");
149130
149672
  try {
149131
149673
  const bytes = await queue.submit({
149132
149674
  host: device.hostKey,
@@ -149143,10 +149685,7 @@ function createClipService(deps) {
149143
149685
  });
149144
149686
  }
149145
149687
  });
149146
- if (bytes.byteLength === 0) return {
149147
- ok: false,
149148
- reason: "no-keyframe: the camera returned an empty frame"
149149
- };
149688
+ if (bytes.byteLength === 0) return refuseStill("no-keyframe", "no-keyframe: the camera returned an empty frame");
149150
149689
  await thumbs.put(deviceId, clipKey, bytes);
149151
149690
  return {
149152
149691
  ok: true,
@@ -149154,6 +149693,19 @@ function createClipService(deps) {
149154
149693
  };
149155
149694
  } catch (err) {
149156
149695
  const reason = err instanceof Error ? err.message : String(err);
149696
+ const deferred = queueRefusal(err);
149697
+ if (deferred !== null) {
149698
+ deps.logger.debug("videoclips: a thumbnail mint was deferred", {
149699
+ tags: { deviceId },
149700
+ meta: {
149701
+ clipKey,
149702
+ lane,
149703
+ code: deferred.code,
149704
+ retryAfterMs: deferred.retryAfterMs
149705
+ }
149706
+ });
149707
+ return deferred;
149708
+ }
149157
149709
  deps.logger.warn("videoclips: a thumbnail mint refused", {
149158
149710
  tags: { deviceId },
149159
149711
  meta: {
@@ -149163,10 +149715,7 @@ function createClipService(deps) {
149163
149715
  error: reason
149164
149716
  }
149165
149717
  });
149166
- return {
149167
- ok: false,
149168
- reason
149169
- };
149718
+ return refuseStill("camera-refused", reason);
149170
149719
  }
149171
149720
  };
149172
149721
  const prepareClip = async (deviceId, clipKey) => {
@@ -149215,15 +149764,43 @@ function createClipService(deps) {
149215
149764
  }),
149216
149765
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
149217
149766
  });
149767
+ const audio = decideClipAudio(downloaded.audio);
149768
+ if (audio.kind === "silent") deps.logger.warn("videoclips: serving a clip with no sound", {
149769
+ tags: { deviceId },
149770
+ meta: {
149771
+ clipKey,
149772
+ branch: audio.reason,
149773
+ audioFormat: audio.format,
149774
+ audioPackets: downloaded.stats.audioPackets,
149775
+ audioBytes: downloaded.stats.audioBytes
149776
+ }
149777
+ });
149778
+ if (downloaded.stats.fpsSource !== "timestamps") deps.logger.warn("videoclips: no measured frame rate for this clip", {
149779
+ tags: { deviceId },
149780
+ meta: {
149781
+ clipKey,
149782
+ branch: "fps-unmeasured",
149783
+ fpsSource: downloaded.stats.fpsSource,
149784
+ fps: downloaded.stats.fps,
149785
+ infoFps: downloaded.stats.infoFps
149786
+ }
149787
+ });
149218
149788
  const scratch = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "camstack-clip-mux-"));
149219
149789
  try {
149220
149790
  const rawPath = (0, node_path.join)(scratch, "clip.h264");
149791
+ const audioPath = (0, node_path.join)(scratch, "clip.aac");
149221
149792
  const outPath = (0, node_path.join)(scratch, "clip.mp4");
149222
149793
  await (0, node_fs_promises.writeFile)(rawPath, downloaded.annexB);
149794
+ if (audio.kind === "copy") await (0, node_fs_promises.writeFile)(audioPath, audio.data);
149223
149795
  const failure = describeMuxFailure(await muxRunner(buildClipMuxArgs({
149224
149796
  inputPath: rawPath,
149225
149797
  outputPath: outPath,
149226
- videoType: downloaded.videoType
149798
+ videoType: downloaded.videoType,
149799
+ fps: downloaded.stats.fps,
149800
+ ...audio.kind === "copy" ? { audio: {
149801
+ path: audioPath,
149802
+ format: audio.format
149803
+ } } : {}
149227
149804
  })));
149228
149805
  if (failure !== null) {
149229
149806
  deps.logger.warn("videoclips: the clip mux failed", {
@@ -149245,21 +149822,31 @@ function createClipService(deps) {
149245
149822
  });
149246
149823
  }
149247
149824
  };
149248
- return {
149249
- provider: createReolinkVideoclipsProvider({
149250
- now,
149251
- cache,
149252
- resolveDevice: deps.resolveDevice,
149253
- hasVouchedThumbnail: (deviceId, clipKey) => thumbs.has(deviceId, clipKey),
149254
- thumbnailUrl: (deviceId, clipKey) => `/addon/${REOLINK_ADDON_ID}/${CLIP_THUMB_PREFIX}/${deviceId}/${clipKey}`,
149255
- thumbnailMintUrl: (deviceId, clipKey) => `/addon/${REOLINK_ADDON_ID}/${CLIP_THUMB_PREFIX}/${deviceId}/${clipKey}`,
149256
- preparePlayback: async ({ device, clipKey, profile }) => ({
149257
- playbackUrl: `/addon/${REOLINK_ADDON_ID}/${CLIP_MEDIA_PREFIX}/${device.deviceId}/${clipKey}.mp4`,
149258
- served: profile === "high" ? "high" : "low"
149259
- }),
149260
- logger: deps.logger
149825
+ const provider = createReolinkVideoclipsProvider({
149826
+ now,
149827
+ cache,
149828
+ resolveDevice: deps.resolveDevice,
149829
+ hasVouchedThumbnail: (deviceId, clipKey) => thumbs.has(deviceId, clipKey),
149830
+ thumbnailUrl: (deviceId, clipKey) => `/addon/${REOLINK_ADDON_ID}/${CLIP_THUMB_PREFIX}/${deviceId}/${clipKey}`,
149831
+ thumbnailMintUrl: (deviceId, clipKey) => `/addon/${REOLINK_ADDON_ID}/${CLIP_THUMB_PREFIX}/${deviceId}/${clipKey}`,
149832
+ preparePlayback: async ({ device, clipKey, profile }) => ({
149833
+ playbackUrl: `/addon/${REOLINK_ADDON_ID}/${CLIP_MEDIA_PREFIX}/${device.deviceId}/${clipKey}.mp4`,
149834
+ served: profile === "high" ? "high" : "low"
149261
149835
  }),
149836
+ logger: deps.logger
149837
+ });
149838
+ return {
149839
+ provider,
149262
149840
  cache,
149841
+ refreshCatalog: async (deviceId) => {
149842
+ const until = now();
149843
+ await provider.listClips({
149844
+ deviceId,
149845
+ since: until - CLIP_CATALOG_REFRESH_WINDOW_MS,
149846
+ until
149847
+ });
149848
+ },
149849
+ hostBusy: (hostKey) => queue.busy(hostKey),
149263
149850
  thumbPrefix: CLIP_THUMB_PREFIX,
149264
149851
  mediaPrefix: CLIP_MEDIA_PREFIX,
149265
149852
  thumbHandler: createClipThumbHandler({
@@ -149284,6 +149871,79 @@ function createClipService(deps) {
149284
149871
  };
149285
149872
  }
149286
149873
  //#endregion
149874
+ //#region src/clips/clip-refresh-settings.ts
149875
+ /** The section id, so the addon's declaration and this schema cannot drift. */
149876
+ var CLIP_REFRESH_SECTION_ID = "clip-catalog-refresh";
149877
+ /**
149878
+ * The one sentence the interval must always carry.
149879
+ *
149880
+ * It does TWO different things depending on what the camera is, and a knob
149881
+ * that silently means two things on two cameras is how this repo has shipped
149882
+ * settings nobody could reason about. So the difference is in the operator's
149883
+ * own text, at both levels, not only in a record.
149884
+ */
149885
+ var CLIP_CATALOG_REFRESH_MINUTES_DESCRIPTION = "How often CamStack re-reads what a Reolink camera holds on its own storage. It costs almost nothing on a camera it can reach: a hub child is read through the hub on the session that is already open (measured 65 ms, and the camera itself never hears it), and a mains camera is read on its own socket. On a STANDALONE BATTERY camera the timer cannot read anything at all — logging in is what wakes such a camera — so there it only marks the catalog as ageing, and the real re-read happens the next time the camera wakes up for its own reasons (motion, a stream, something you did). Set 0 to switch the timer off.";
149886
+ var CLIP_CATALOG_FORCED_WAKE_DESCRIPTION = "Let the refresh timer WAKE this camera. Off unless you mean it: on a standalone battery camera every read is a login and every login is a wake, so a 15-minute timer is 96 wakes a day (24 at 60 minutes) against the 1–20 clips a day these cameras actually record — five to a hundred times their normal wake count, and the battery pays for all of it. One interval buys exactly one wake, successful or not. It has no effect on a hub child or a mains camera, which are never woken by this timer.";
149887
+ /**
149888
+ * The addon-level (global) default. Per-camera overrides live on the device,
149889
+ * under the SAME key, and a device that sets nothing — or resets — resolves to
149890
+ * this value (`resolveClipCatalogRefreshMinutes`).
149891
+ */
149892
+ function buildClipRefreshSettingsSchema() {
149893
+ return { sections: [{
149894
+ id: CLIP_REFRESH_SECTION_ID,
149895
+ title: "Clip catalog refresh",
149896
+ description: "The default for every Reolink camera. Each camera can override it in its own Recording settings; a camera that overrides nothing uses this value.",
149897
+ columns: 1,
149898
+ fields: [{
149899
+ type: "number",
149900
+ key: CLIP_CATALOG_REFRESH_MINUTES_KEY,
149901
+ label: "Refresh interval (min)",
149902
+ description: CLIP_CATALOG_REFRESH_MINUTES_DESCRIPTION,
149903
+ default: 30,
149904
+ min: 0,
149905
+ max: CLIP_CATALOG_REFRESH_MINUTES_MAX,
149906
+ step: 5
149907
+ }]
149908
+ }] };
149909
+ }
149910
+ /**
149911
+ * The per-camera section: the interval override, and — only where it can mean
149912
+ * anything — the forced wake.
149913
+ *
149914
+ * `forcedWake` is a plain persisted switch on purpose. It is NOT a
149915
+ * `device-action-button` and there is no custom action behind it, so nothing
149916
+ * that dispatches device actions (a schedule, an automation, a rule) can spend
149917
+ * a battery camera's wakes; the only way to turn it on is an operator editing
149918
+ * this camera's settings.
149919
+ */
149920
+ function buildClipRefreshDeviceSection(input) {
149921
+ const forcedWakeApplies = input.isBattery && !input.isHubChild;
149922
+ return [{
149923
+ id: CLIP_REFRESH_SECTION_ID,
149924
+ tab: "recording",
149925
+ title: "Clip catalog refresh",
149926
+ description: "How often this camera’s own clip list is re-read. Leave the interval empty to use the default from the Reolink addon settings.",
149927
+ columns: 1,
149928
+ fields: [{
149929
+ type: "number",
149930
+ key: CLIP_CATALOG_REFRESH_MINUTES_KEY,
149931
+ label: "Refresh interval (min)",
149932
+ description: CLIP_CATALOG_REFRESH_MINUTES_DESCRIPTION,
149933
+ min: 0,
149934
+ max: CLIP_CATALOG_REFRESH_MINUTES_MAX,
149935
+ step: 5
149936
+ }, ...forcedWakeApplies ? [{
149937
+ type: "boolean",
149938
+ key: CLIP_CATALOG_FORCED_WAKE_KEY,
149939
+ label: "Wake this camera to refresh",
149940
+ description: CLIP_CATALOG_FORCED_WAKE_DESCRIPTION,
149941
+ default: false,
149942
+ style: "switch"
149943
+ }] : []]
149944
+ }];
149945
+ }
149946
+ //#endregion
149287
149947
  //#region src/intercom-failure-report.ts
149288
149948
  /**
149289
149949
  * Per-camera talk-back counters, published through `failure-contribution`.
@@ -154361,6 +155021,73 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
154361
155021
  * provider still treats a missing `uid` as a dropped branch rather than as
154362
155022
  * an empty catalog.
154363
155023
  */
155024
+ /**
155025
+ * Re-read this camera's clip catalog. Set by the addon for every camera it
155026
+ * owns; `null` until it is wired (a redeploy re-registers the device, and
155027
+ * the addon re-wires on `DeviceRegistered` AND `DeviceMetaChanged` — the
155028
+ * same fan-out the email-push resolver needed, for the same reason).
155029
+ */
155030
+ clipCatalogRefresher = null;
155031
+ setClipCatalogRefresher(refresh) {
155032
+ this.clipCatalogRefresher = refresh;
155033
+ }
155034
+ /**
155035
+ * This camera's OVERRIDE of the clip-refresh interval, or `undefined` when
155036
+ * it overrides nothing — which is what a reset leaves behind too, and is
155037
+ * why the resolver takes `unknown` rather than a number (D562).
155038
+ */
155039
+ getClipCatalogRefreshMinutes() {
155040
+ return this.config.get("clipCatalogRefreshMinutes");
155041
+ }
155042
+ /** Whether the operator authorised the clip timer to wake this camera. */
155043
+ getClipCatalogForcedWake() {
155044
+ return this.config.get("clipCatalogForcedWake");
155045
+ }
155046
+ /**
155047
+ * The clip refresh the TIMER cannot do.
155048
+ *
155049
+ * On a standalone battery camera a catalog read is a login and a login is
155050
+ * the wake, so the periodic refresh only marks the catalog stale (D562).
155051
+ * This is the other half: the moment the camera confirmed itself awake for
155052
+ * its own reasons — a camera event, a stream, an operator action — is a free
155053
+ * read, and it is the same moment that already warms the stream catalog and
155054
+ * every settings slice. Called from `onWakeTransition`, best-effort, last:
155055
+ * a clip listing must never delay the live-stream catalog the operator is
155056
+ * waiting for.
155057
+ */
155058
+ async refreshClipCatalogOnWake() {
155059
+ const refresh = this.clipCatalogRefresher;
155060
+ if (refresh === null) {
155061
+ this.ctx.logger.debug("onWakeTransition: no clip catalog refresher wired", {
155062
+ tags: { deviceId: this.id },
155063
+ meta: { branch: "clip-refresher-unwired" }
155064
+ });
155065
+ return;
155066
+ }
155067
+ try {
155068
+ await refresh();
155069
+ } catch (err) {
155070
+ this.ctx.logger.warn("onWakeTransition: the clip catalog refresh failed", {
155071
+ tags: { deviceId: this.id },
155072
+ meta: {
155073
+ branch: "clip-refresh-failed",
155074
+ error: err instanceof Error ? err.message : String(err)
155075
+ }
155076
+ });
155077
+ }
155078
+ }
155079
+ /**
155080
+ * Wake this camera because the OPERATOR authorised the clip timer to
155081
+ * (`clipCatalogForcedWake`, off by default — D562).
155082
+ *
155083
+ * The only path from the timer to a battery camera's radio, and it is the
155084
+ * same forced wake `wakeIfSleeping` already drives for a config button.
155085
+ * Throws when the camera did not come up, so the scheduler can say so and
155086
+ * leave the catalog stale rather than stamping it fresh.
155087
+ */
155088
+ async wakeForClipCatalogRefresh() {
155089
+ if (!await this.wakeIfSleeping()) throw new Error(`ReolinkCamera ${this.id}: the authorised clip-refresh wake did not bring the camera up`);
155090
+ }
154364
155091
  getClipDeviceRef() {
154365
155092
  const uid = this.config.get("uid");
154366
155093
  return {
@@ -158621,6 +159348,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
158621
159348
  await this.warmSnapshotOnWake();
158622
159349
  await this.alignAuxDevicesState("wake");
158623
159350
  await this.refreshParentSettingsSnapshot();
159351
+ await this.refreshClipCatalogOnWake();
158624
159352
  }
158625
159353
  static PING_INTERVAL_MS = 3e4;
158626
159354
  static PING_MAX_FAILURES = 3;
@@ -159806,7 +160534,11 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
159806
160534
  }),
159807
160535
  ...this.buildSessionsTabSections(),
159808
160536
  ...this.buildEmailPushSection(),
159809
- ...this.buildEmailTabSections()
160537
+ ...this.buildEmailTabSections(),
160538
+ ...buildClipRefreshDeviceSection({
160539
+ isHubChild: this.isHubChild(),
160540
+ isBattery: this.isBattery
160541
+ })
159810
160542
  ]
159811
160543
  };
159812
160544
  const emailSnap = cache?.emailConfigSnapshot;
@@ -159825,6 +160557,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
159825
160557
  emailRecipient3: emailSnap?.recipients?.[2] ?? "",
159826
160558
  emailSendNickname: emailSnap?.sendNickname ?? this.name,
159827
160559
  emailScheduleEnabled: emailSnap?.taskEnabled ?? false,
160560
+ clipCatalogRefreshMinutes: this.config.get("clipCatalogRefreshMinutes"),
160561
+ clipCatalogForcedWake: this.config.get("clipCatalogForcedWake") ?? false,
159828
160562
  debugGeneral: this.config.get("debugGeneral") ?? false,
159829
160563
  debugSocketLogs: this.config.get("debugSocketLogs") ?? [],
159830
160564
  motionEnabled: this.config.get("motionEnabled") ?? motionSnap.enabled ?? true,
@@ -163098,6 +163832,10 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
163098
163832
  throw new Error(`Reolink: ${reason}`);
163099
163833
  }
163100
163834
  async onShutdown() {
163835
+ if (this.clipRefreshTimer) {
163836
+ clearInterval(this.clipRefreshTimer);
163837
+ this.clipRefreshTimer = null;
163838
+ }
163101
163839
  this.logChannels?.stop();
163102
163840
  await this.emailPushServer?.stop().catch(() => {});
163103
163841
  await this.autodetectCache.dispose();
@@ -163108,6 +163846,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
163108
163846
  * logged inside the server and leaves it stopped until the next edit.
163109
163847
  */
163110
163848
  async onConfigChanged() {
163849
+ await this.refreshClipRefreshGlobalStore();
163111
163850
  await this.emailPushServer?.restart().catch((err) => {
163112
163851
  this.ctx.logger.warn("email-push: restart after settings change failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
163113
163852
  });
@@ -163127,7 +163866,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
163127
163866
  * for core to select from.
163128
163867
  */
163129
163868
  globalSettingsSchema() {
163130
- return buildEmailPushSettingsSchema(listLanHosts()[0] ?? "127.0.0.1");
163869
+ return { sections: [...buildEmailPushSettingsSchema(listLanHosts()[0] ?? "127.0.0.1").sections, ...buildClipRefreshSettingsSchema().sections] };
163131
163870
  }
163132
163871
  /**
163133
163872
  * Both email-push sections ARE this integration's configuration — one SMTP
@@ -163174,6 +163913,9 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
163174
163913
  capability: videoclipsCapability,
163175
163914
  provider: this.clipService.provider
163176
163915
  });
163916
+ await this.refreshClipRefreshGlobalStore();
163917
+ this.startClipCatalogRefresh(this.clipService);
163918
+ this.wireClipCatalogRefreshers();
163177
163919
  await this.serveClipPlanes(this.clipService);
163178
163920
  this.ensureEmailPushServer().start().catch((err) => {
163179
163921
  this.ctx.logger.warn("email-push: SMTP intake failed to start at init", { meta: { error: err instanceof Error ? err.message : String(err) } });
@@ -163186,6 +163928,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
163186
163928
  if (dev instanceof ReolinkCamera) {
163187
163929
  const server = this.ensureEmailPushServer();
163188
163930
  dev.setEmailPushParamsResolver(() => server.buildCameraSetupParams(dev));
163931
+ this.wireClipCatalogRefreshers();
163189
163932
  this.ctx.logger.debug("email-push: resolver wired", {
163190
163933
  tags: { deviceId: data.deviceId },
163191
163934
  meta: { via }
@@ -163199,6 +163942,102 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
163199
163942
  }
163200
163943
  /** The clip catalog, its thumbnail cache and its two routes (D549). */
163201
163944
  clipService = null;
163945
+ clipRefreshTimer = null;
163946
+ /**
163947
+ * The addon store as the refresh timer last saw it.
163948
+ *
163949
+ * `listTargets` is synchronous on purpose — a tick that awaited a settings
163950
+ * read per camera would be a store round-trip per camera per tick — so the
163951
+ * global half of the cascade is mirrored here and refreshed at init and on
163952
+ * every `onConfigChanged`. An empty mirror resolves to the DEFAULT, never to
163953
+ * "off": a settings read that has not happened yet must not switch the timer
163954
+ * off for the whole fleet.
163955
+ */
163956
+ clipRefreshGlobalStore = {};
163957
+ /**
163958
+ * Give every camera the wake-side refresh.
163959
+ *
163960
+ * Idempotent, and re-run on both device lifecycle events for the reason the
163961
+ * email-push resolver documents: an addon-only redeploy re-registers a
163962
+ * device the hub already knows, which emits `DeviceMetaChanged` and NOT
163963
+ * `DeviceRegistered`, and subscribing to one left every battery camera
163964
+ * unwired.
163965
+ */
163966
+ wireClipCatalogRefreshers() {
163967
+ const service = this.clipService;
163968
+ if (service === null) return;
163969
+ for (const camera of this.listReolinkCameras()) {
163970
+ const deviceId = camera.getClipDeviceRef().deviceId;
163971
+ camera.setClipCatalogRefresher(async () => {
163972
+ await service.refreshCatalog(deviceId);
163973
+ });
163974
+ }
163975
+ }
163976
+ /** Re-read the global half of the clip-refresh cascade. */
163977
+ async refreshClipRefreshGlobalStore() {
163978
+ try {
163979
+ this.clipRefreshGlobalStore = await this.resolveGlobalStore();
163980
+ } catch (err) {
163981
+ this.ctx.logger.warn("videoclips: could not read the clip refresh default — keeping the last", { meta: { error: err instanceof Error ? err.message : String(err) } });
163982
+ }
163983
+ }
163984
+ /**
163985
+ * Arm the clip-catalog refresh.
163986
+ *
163987
+ * The scheduler owns no clock: it is ticked, so its rules are testable from
163988
+ * COLD (`clip-refresh.spec.ts` — the discipline D561 cost us). The tick
163989
+ * cadence is the FLOOR of what any camera can be configured to, not the
163990
+ * interval itself; each camera's own interval is decided inside the tick
163991
+ * from the cascade, so changing a setting takes effect without re-arming.
163992
+ */
163993
+ startClipCatalogRefresh(service) {
163994
+ const scheduler = new ClipCatalogRefreshScheduler({
163995
+ now: () => Date.now(),
163996
+ listTargets: () => this.clipRefreshTargets(),
163997
+ refresh: async (deviceId) => {
163998
+ await service.refreshCatalog(deviceId);
163999
+ },
164000
+ markStale: (deviceId) => {
164001
+ service.cache.markStale(deviceId);
164002
+ },
164003
+ wake: async (deviceId) => {
164004
+ const device = this.ctx.kernel.deviceRegistry?.getById(deviceId);
164005
+ if (!(device instanceof ReolinkCamera)) throw new Error(`clip refresh: device ${String(deviceId)} is not a Reolink camera here`);
164006
+ await device.wakeForClipCatalogRefresh();
164007
+ },
164008
+ hostBusy: (hostKey) => service.hostBusy(hostKey),
164009
+ logger: this.ctx.logger
164010
+ });
164011
+ this.clipRefreshTimer = setInterval(() => {
164012
+ scheduler.tick().catch((err) => {
164013
+ this.ctx.logger.warn("videoclips: the catalog refresh tick failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
164014
+ });
164015
+ }, 5 * 6e4);
164016
+ if (typeof this.clipRefreshTimer.unref === "function") this.clipRefreshTimer.unref();
164017
+ }
164018
+ /**
164019
+ * Every Reolink CAMERA this process owns, with its cascade resolved.
164020
+ *
164021
+ * Cameras only — the Home Hub (639) is `type: hub` and never binds
164022
+ * `videoclips` (`deviceTypes: [Camera]`); its catalog IS its children's and
164023
+ * is refreshed per child, on its one session. Accessories are not cameras.
164024
+ */
164025
+ clipRefreshTargets() {
164026
+ const globalMinutes = this.clipRefreshGlobalStore[CLIP_CATALOG_REFRESH_MINUTES_KEY];
164027
+ const out = [];
164028
+ for (const device of this.listReolinkCameras()) {
164029
+ const ref = device.getClipDeviceRef();
164030
+ out.push({
164031
+ deviceId: ref.deviceId,
164032
+ hostKey: ref.hostKey,
164033
+ isHubChild: ref.isHubChild,
164034
+ isBattery: ref.isBattery,
164035
+ forcedWake: resolveClipCatalogForcedWake(device.getClipCatalogForcedWake()),
164036
+ intervalMinutes: resolveClipCatalogRefreshMinutes(globalMinutes, device.getClipCatalogRefreshMinutes())
164037
+ });
164038
+ }
164039
+ return out;
164040
+ }
163202
164041
  /**
163203
164042
  * Host the thumbnail and media planes, and never let a host without the
163204
164043
  * facility take the addon down with it — a data plane is an enhancement to