@camstack/addon-provider-reolink 1.2.147 → 1.2.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +166 -22
  2. package/dist/addon.mjs +166 -22
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -25447,22 +25447,40 @@ var videoclipsCapability = {
25447
25447
  limit: number().int().positive().optional(),
25448
25448
  /**
25449
25449
  * WHICH provider to ask — the `addonId` a {@link ClipSourceSchema} row
25450
- * carries, never a source id and never a list.
25450
+ * carries, never a source id and never a list. **Required** (D554 amended).
25451
25451
  *
25452
- * Absent means the device's BOUND provider, which is CamStack on every
25453
- * camera today because that is what `deviceManager.getBindings`
25454
- * answers with. A provider the device is not bound to is refused by
25455
- * name rather than answered by another one (D552's
25456
- * `rejectUnresolvedAddonPin` rule).
25452
+ * A provider the device is not bound to is refused by name rather than
25453
+ * answered by another one (D552's `rejectUnresolvedAddonPin` rule).
25454
+ *
25455
+ * It was optional, documented as "absent means the device's BOUND
25456
+ * provider, which is CamStack on every camera". No code implemented
25457
+ * that. Measured on the live hub 2026-09-20 — device 592, bound to
25458
+ * `recorder` AND `provider-reolink` — a bare call with `limit: 3`
25459
+ * answered SIX rows, three from each source, merged newest-first:
25460
+ * `device-collection-dispatch.ts` simply left the fan-out un-narrowed,
25461
+ * so absence bought the union this method exists not to be, and
25462
+ * `limit` meant `limit × sources`.
25463
+ *
25464
+ * There is nothing to restore the default to. `getBindings` answers a
25465
+ * collection cap with a DERIVED, PLURAL set (D554 amended, step 0);
25466
+ * `setWrapperActive` — the singleton authority that could name one —
25467
+ * throws for a collection cap by design. Naming CamStack here instead
25468
+ * would privilege one addon by id inside a surface whose premise is
25469
+ * that sources are peers, and would be wrong on the first camera with
25470
+ * no recorder binding. So absence is REFUSED, in the schema, where the
25471
+ * generated types make it unomittable rather than merely discouraged.
25472
+ *
25473
+ * The default belongs to the SURFACE, which has the `listSources` rows
25474
+ * and can say which one it picked (D569 § 1–3; the viewer already
25475
+ * always sends this).
25457
25476
  *
25458
25477
  * It replaced `sources?: string[]`, a VIEW filter over source ids that
25459
25478
  * assumed the answer was a fan-out over everything a camera has. The
25460
- * operator settled otherwise on 2026-09-20 — *"il get videoclips
25461
- * prendere un provider opzionale, di default va su camstack"*,
25462
- * *"l'utilizzatore è uno solo"* — so the list is asked of one provider
25463
- * at a time and there is nothing to filter out of it.
25479
+ * operator settled otherwise on 2026-09-20 — *"l'utilizzatore è uno
25480
+ * solo"* — so the list is asked of one provider at a time and there is
25481
+ * nothing to filter out of it.
25464
25482
  */
25465
- provider: string().optional()
25483
+ provider: string().min(1)
25466
25484
  }), array(ClipSchema).readonly(), {
25467
25485
  kind: "query",
25468
25486
  auth: "protected"
@@ -148234,6 +148252,51 @@ function describeMuxFailure(run) {
148234
148252
  return null;
148235
148253
  }
148236
148254
  //#endregion
148255
+ //#region src/clips/clip-truncation.ts
148256
+ /**
148257
+ * The window a TRUNCATED fetch is retried on: the library's own default, the
148258
+ * one this repo shipped for its whole life. A retry exists to answer "was that
148259
+ * the window or the camera?", so it must be the conservative number and not a
148260
+ * second short one.
148261
+ */
148262
+ var CLIP_IDLE_WINDOW_RETRY_MS = 2e3;
148263
+ /**
148264
+ * Did this pass deliver the clip the row describes?
148265
+ *
148266
+ * Only a SHORTFALL is a defect. A decode that runs longer than the row is
148267
+ * routine — the row's stamps are whole seconds and its twins differ by up to
148268
+ * four — and calling it truncated would refuse a clip that is more complete
148269
+ * than the catalog knew.
148270
+ */
148271
+ function judgeClipDuration(input) {
148272
+ if (input.inProgress === true) return {
148273
+ kind: "unknown",
148274
+ reason: "clip-open"
148275
+ };
148276
+ if (input.decodedSeconds === null || !Number.isFinite(input.decodedSeconds)) return {
148277
+ kind: "unknown",
148278
+ reason: "duration-unmeasured"
148279
+ };
148280
+ if (!Number.isFinite(input.expectedMs) || input.expectedMs <= 0) return {
148281
+ kind: "unknown",
148282
+ reason: "row-window-unknown"
148283
+ };
148284
+ const expectedSeconds = input.expectedMs / 1e3;
148285
+ const decodedSeconds = input.decodedSeconds;
148286
+ const shortfallSeconds = expectedSeconds - decodedSeconds;
148287
+ if (shortfallSeconds > 1.5) return {
148288
+ kind: "short",
148289
+ expectedSeconds,
148290
+ decodedSeconds,
148291
+ shortfallSeconds
148292
+ };
148293
+ return {
148294
+ kind: "complete",
148295
+ expectedSeconds,
148296
+ decodedSeconds
148297
+ };
148298
+ }
148299
+ //#endregion
148237
148300
  //#region src/clips/clip-thumb-queue.ts
148238
148301
  /**
148239
148302
  * The provider's OWN CoverPreview lane — two queues, one in flight per host.
@@ -149718,6 +149781,95 @@ function createClipService(deps) {
149718
149781
  return refuseStill("camera-refused", reason);
149719
149782
  }
149720
149783
  };
149784
+ /**
149785
+ * ONE clip's bytes, on the short window, with the fetch PROVED (D568).
149786
+ *
149787
+ * Completion is an idle window because nothing on the wire ends a cmd 5
149788
+ * transfer, and that window is short. What makes it safe is not the window,
149789
+ * it is the decoded duration judged against the row that asked for it.
149790
+ */
149791
+ const fetchVerifiedClipBytes = async (device, row, clipKey) => {
149792
+ const deviceId = device.deviceId;
149793
+ const api = await device.getApi();
149794
+ const fetchPass = (idleTimeoutMs) => fetchClipBytes({
149795
+ download: () => api.downloadRecordingDemuxed({
149796
+ channel: device.channel,
149797
+ ...device.uid !== void 0 ? { uid: device.uid } : {},
149798
+ fileName: row.subFileName,
149799
+ idleTimeoutMs
149800
+ }),
149801
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
149802
+ });
149803
+ const judge = (pass) => judgeClipDuration({
149804
+ expectedMs: row.endMs - row.startMs,
149805
+ decodedSeconds: pass.stats.durationSeconds,
149806
+ ...row.inProgress === true ? { inProgress: true } : {}
149807
+ });
149808
+ const first = await fetchPass(300);
149809
+ const firstVerdict = judge(first);
149810
+ if (firstVerdict.kind === "unknown") {
149811
+ const line = {
149812
+ tags: { deviceId },
149813
+ meta: {
149814
+ clipKey,
149815
+ branch: "completeness-unverified",
149816
+ reason: firstVerdict.reason,
149817
+ idleTimeoutMs: 300,
149818
+ expectedMs: row.endMs - row.startMs,
149819
+ decodedSeconds: first.stats.durationSeconds
149820
+ }
149821
+ };
149822
+ if (firstVerdict.reason === "clip-open") deps.logger.debug("videoclips: an open clip cannot be checked for truncation", line);
149823
+ else deps.logger.warn("videoclips: a clip fetch could not be checked for truncation", line);
149824
+ return first;
149825
+ }
149826
+ if (firstVerdict.kind === "complete") return first;
149827
+ deps.logger.warn("videoclips: a clip fetch came back truncated", {
149828
+ tags: { deviceId },
149829
+ meta: {
149830
+ clipKey,
149831
+ branch: "truncated-fetch",
149832
+ idleTimeoutMs: 300,
149833
+ expectedSeconds: firstVerdict.expectedSeconds,
149834
+ decodedSeconds: firstVerdict.decodedSeconds,
149835
+ shortfallSeconds: firstVerdict.shortfallSeconds,
149836
+ bytes: first.annexB.byteLength,
149837
+ keyframes: first.stats.keyframes
149838
+ }
149839
+ });
149840
+ const retry = await fetchPass(CLIP_IDLE_WINDOW_RETRY_MS);
149841
+ const keepRetry = (retry.stats.durationSeconds ?? 0) >= (first.stats.durationSeconds ?? 0);
149842
+ const kept = keepRetry ? retry : first;
149843
+ const verdict = keepRetry ? judge(retry) : firstVerdict;
149844
+ const keptWindowMs = keepRetry ? CLIP_IDLE_WINDOW_RETRY_MS : 300;
149845
+ if (verdict.kind === "short") {
149846
+ deps.logger.warn("videoclips: a clip is still short after the long window", {
149847
+ tags: { deviceId },
149848
+ meta: {
149849
+ clipKey,
149850
+ branch: "truncated-after-retry",
149851
+ idleTimeoutMs: keptWindowMs,
149852
+ retriedOnMs: CLIP_IDLE_WINDOW_RETRY_MS,
149853
+ expectedSeconds: verdict.expectedSeconds,
149854
+ decodedSeconds: verdict.decodedSeconds,
149855
+ shortfallSeconds: verdict.shortfallSeconds,
149856
+ bytes: kept.annexB.byteLength
149857
+ }
149858
+ });
149859
+ return kept;
149860
+ }
149861
+ deps.logger.info("videoclips: a truncated clip fetch landed whole on the long window", {
149862
+ tags: { deviceId },
149863
+ meta: {
149864
+ clipKey,
149865
+ branch: "truncation-recovered",
149866
+ idleTimeoutMs: keptWindowMs,
149867
+ decodedSeconds: verdict.kind === "complete" ? verdict.decodedSeconds : null,
149868
+ bytes: kept.annexB.byteLength
149869
+ }
149870
+ });
149871
+ return kept;
149872
+ };
149721
149873
  const prepareClip = async (deviceId, clipKey) => {
149722
149874
  const ready = await media.pathIfPresent(deviceId, clipKey);
149723
149875
  if (ready !== null) return ready;
@@ -149754,16 +149906,7 @@ function createClipService(deps) {
149754
149906
  });
149755
149907
  return null;
149756
149908
  }
149757
- const fileName = row.subFileName;
149758
- const api = await device.getApi();
149759
- const downloaded = await fetchClipBytes({
149760
- download: () => api.downloadRecordingDemuxed({
149761
- channel: device.channel,
149762
- ...device.uid !== void 0 ? { uid: device.uid } : {},
149763
- fileName
149764
- }),
149765
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
149766
- });
149909
+ const downloaded = await fetchVerifiedClipBytes(device, row, clipKey);
149767
149910
  const audio = decideClipAudio(downloaded.audio);
149768
149911
  if (audio.kind === "silent") deps.logger.warn("videoclips: serving a clip with no sound", {
149769
149912
  tags: { deviceId },
@@ -149843,7 +149986,8 @@ function createClipService(deps) {
149843
149986
  await provider.listClips({
149844
149987
  deviceId,
149845
149988
  since: until - CLIP_CATALOG_REFRESH_WINDOW_MS,
149846
- until
149989
+ until,
149990
+ provider: REOLINK_ADDON_ID
149847
149991
  });
149848
149992
  },
149849
149993
  hostBusy: (hostKey) => queue.busy(hostKey),
package/dist/addon.mjs CHANGED
@@ -25442,22 +25442,40 @@ var videoclipsCapability = {
25442
25442
  limit: number().int().positive().optional(),
25443
25443
  /**
25444
25444
  * WHICH provider to ask — the `addonId` a {@link ClipSourceSchema} row
25445
- * carries, never a source id and never a list.
25445
+ * carries, never a source id and never a list. **Required** (D554 amended).
25446
25446
  *
25447
- * Absent means the device's BOUND provider, which is CamStack on every
25448
- * camera today because that is what `deviceManager.getBindings`
25449
- * answers with. A provider the device is not bound to is refused by
25450
- * name rather than answered by another one (D552's
25451
- * `rejectUnresolvedAddonPin` rule).
25447
+ * A provider the device is not bound to is refused by name rather than
25448
+ * answered by another one (D552's `rejectUnresolvedAddonPin` rule).
25449
+ *
25450
+ * It was optional, documented as "absent means the device's BOUND
25451
+ * provider, which is CamStack on every camera". No code implemented
25452
+ * that. Measured on the live hub 2026-09-20 — device 592, bound to
25453
+ * `recorder` AND `provider-reolink` — a bare call with `limit: 3`
25454
+ * answered SIX rows, three from each source, merged newest-first:
25455
+ * `device-collection-dispatch.ts` simply left the fan-out un-narrowed,
25456
+ * so absence bought the union this method exists not to be, and
25457
+ * `limit` meant `limit × sources`.
25458
+ *
25459
+ * There is nothing to restore the default to. `getBindings` answers a
25460
+ * collection cap with a DERIVED, PLURAL set (D554 amended, step 0);
25461
+ * `setWrapperActive` — the singleton authority that could name one —
25462
+ * throws for a collection cap by design. Naming CamStack here instead
25463
+ * would privilege one addon by id inside a surface whose premise is
25464
+ * that sources are peers, and would be wrong on the first camera with
25465
+ * no recorder binding. So absence is REFUSED, in the schema, where the
25466
+ * generated types make it unomittable rather than merely discouraged.
25467
+ *
25468
+ * The default belongs to the SURFACE, which has the `listSources` rows
25469
+ * and can say which one it picked (D569 § 1–3; the viewer already
25470
+ * always sends this).
25452
25471
  *
25453
25472
  * It replaced `sources?: string[]`, a VIEW filter over source ids that
25454
25473
  * assumed the answer was a fan-out over everything a camera has. The
25455
- * operator settled otherwise on 2026-09-20 — *"il get videoclips
25456
- * prendere un provider opzionale, di default va su camstack"*,
25457
- * *"l'utilizzatore è uno solo"* — so the list is asked of one provider
25458
- * at a time and there is nothing to filter out of it.
25474
+ * operator settled otherwise on 2026-09-20 — *"l'utilizzatore è uno
25475
+ * solo"* — so the list is asked of one provider at a time and there is
25476
+ * nothing to filter out of it.
25459
25477
  */
25460
- provider: string().optional()
25478
+ provider: string().min(1)
25461
25479
  }), array(ClipSchema).readonly(), {
25462
25480
  kind: "query",
25463
25481
  auth: "protected"
@@ -148229,6 +148247,51 @@ function describeMuxFailure(run) {
148229
148247
  return null;
148230
148248
  }
148231
148249
  //#endregion
148250
+ //#region src/clips/clip-truncation.ts
148251
+ /**
148252
+ * The window a TRUNCATED fetch is retried on: the library's own default, the
148253
+ * one this repo shipped for its whole life. A retry exists to answer "was that
148254
+ * the window or the camera?", so it must be the conservative number and not a
148255
+ * second short one.
148256
+ */
148257
+ var CLIP_IDLE_WINDOW_RETRY_MS = 2e3;
148258
+ /**
148259
+ * Did this pass deliver the clip the row describes?
148260
+ *
148261
+ * Only a SHORTFALL is a defect. A decode that runs longer than the row is
148262
+ * routine — the row's stamps are whole seconds and its twins differ by up to
148263
+ * four — and calling it truncated would refuse a clip that is more complete
148264
+ * than the catalog knew.
148265
+ */
148266
+ function judgeClipDuration(input) {
148267
+ if (input.inProgress === true) return {
148268
+ kind: "unknown",
148269
+ reason: "clip-open"
148270
+ };
148271
+ if (input.decodedSeconds === null || !Number.isFinite(input.decodedSeconds)) return {
148272
+ kind: "unknown",
148273
+ reason: "duration-unmeasured"
148274
+ };
148275
+ if (!Number.isFinite(input.expectedMs) || input.expectedMs <= 0) return {
148276
+ kind: "unknown",
148277
+ reason: "row-window-unknown"
148278
+ };
148279
+ const expectedSeconds = input.expectedMs / 1e3;
148280
+ const decodedSeconds = input.decodedSeconds;
148281
+ const shortfallSeconds = expectedSeconds - decodedSeconds;
148282
+ if (shortfallSeconds > 1.5) return {
148283
+ kind: "short",
148284
+ expectedSeconds,
148285
+ decodedSeconds,
148286
+ shortfallSeconds
148287
+ };
148288
+ return {
148289
+ kind: "complete",
148290
+ expectedSeconds,
148291
+ decodedSeconds
148292
+ };
148293
+ }
148294
+ //#endregion
148232
148295
  //#region src/clips/clip-thumb-queue.ts
148233
148296
  /**
148234
148297
  * The provider's OWN CoverPreview lane — two queues, one in flight per host.
@@ -149713,6 +149776,95 @@ function createClipService(deps) {
149713
149776
  return refuseStill("camera-refused", reason);
149714
149777
  }
149715
149778
  };
149779
+ /**
149780
+ * ONE clip's bytes, on the short window, with the fetch PROVED (D568).
149781
+ *
149782
+ * Completion is an idle window because nothing on the wire ends a cmd 5
149783
+ * transfer, and that window is short. What makes it safe is not the window,
149784
+ * it is the decoded duration judged against the row that asked for it.
149785
+ */
149786
+ const fetchVerifiedClipBytes = async (device, row, clipKey) => {
149787
+ const deviceId = device.deviceId;
149788
+ const api = await device.getApi();
149789
+ const fetchPass = (idleTimeoutMs) => fetchClipBytes({
149790
+ download: () => api.downloadRecordingDemuxed({
149791
+ channel: device.channel,
149792
+ ...device.uid !== void 0 ? { uid: device.uid } : {},
149793
+ fileName: row.subFileName,
149794
+ idleTimeoutMs
149795
+ }),
149796
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
149797
+ });
149798
+ const judge = (pass) => judgeClipDuration({
149799
+ expectedMs: row.endMs - row.startMs,
149800
+ decodedSeconds: pass.stats.durationSeconds,
149801
+ ...row.inProgress === true ? { inProgress: true } : {}
149802
+ });
149803
+ const first = await fetchPass(300);
149804
+ const firstVerdict = judge(first);
149805
+ if (firstVerdict.kind === "unknown") {
149806
+ const line = {
149807
+ tags: { deviceId },
149808
+ meta: {
149809
+ clipKey,
149810
+ branch: "completeness-unverified",
149811
+ reason: firstVerdict.reason,
149812
+ idleTimeoutMs: 300,
149813
+ expectedMs: row.endMs - row.startMs,
149814
+ decodedSeconds: first.stats.durationSeconds
149815
+ }
149816
+ };
149817
+ if (firstVerdict.reason === "clip-open") deps.logger.debug("videoclips: an open clip cannot be checked for truncation", line);
149818
+ else deps.logger.warn("videoclips: a clip fetch could not be checked for truncation", line);
149819
+ return first;
149820
+ }
149821
+ if (firstVerdict.kind === "complete") return first;
149822
+ deps.logger.warn("videoclips: a clip fetch came back truncated", {
149823
+ tags: { deviceId },
149824
+ meta: {
149825
+ clipKey,
149826
+ branch: "truncated-fetch",
149827
+ idleTimeoutMs: 300,
149828
+ expectedSeconds: firstVerdict.expectedSeconds,
149829
+ decodedSeconds: firstVerdict.decodedSeconds,
149830
+ shortfallSeconds: firstVerdict.shortfallSeconds,
149831
+ bytes: first.annexB.byteLength,
149832
+ keyframes: first.stats.keyframes
149833
+ }
149834
+ });
149835
+ const retry = await fetchPass(CLIP_IDLE_WINDOW_RETRY_MS);
149836
+ const keepRetry = (retry.stats.durationSeconds ?? 0) >= (first.stats.durationSeconds ?? 0);
149837
+ const kept = keepRetry ? retry : first;
149838
+ const verdict = keepRetry ? judge(retry) : firstVerdict;
149839
+ const keptWindowMs = keepRetry ? CLIP_IDLE_WINDOW_RETRY_MS : 300;
149840
+ if (verdict.kind === "short") {
149841
+ deps.logger.warn("videoclips: a clip is still short after the long window", {
149842
+ tags: { deviceId },
149843
+ meta: {
149844
+ clipKey,
149845
+ branch: "truncated-after-retry",
149846
+ idleTimeoutMs: keptWindowMs,
149847
+ retriedOnMs: CLIP_IDLE_WINDOW_RETRY_MS,
149848
+ expectedSeconds: verdict.expectedSeconds,
149849
+ decodedSeconds: verdict.decodedSeconds,
149850
+ shortfallSeconds: verdict.shortfallSeconds,
149851
+ bytes: kept.annexB.byteLength
149852
+ }
149853
+ });
149854
+ return kept;
149855
+ }
149856
+ deps.logger.info("videoclips: a truncated clip fetch landed whole on the long window", {
149857
+ tags: { deviceId },
149858
+ meta: {
149859
+ clipKey,
149860
+ branch: "truncation-recovered",
149861
+ idleTimeoutMs: keptWindowMs,
149862
+ decodedSeconds: verdict.kind === "complete" ? verdict.decodedSeconds : null,
149863
+ bytes: kept.annexB.byteLength
149864
+ }
149865
+ });
149866
+ return kept;
149867
+ };
149716
149868
  const prepareClip = async (deviceId, clipKey) => {
149717
149869
  const ready = await media.pathIfPresent(deviceId, clipKey);
149718
149870
  if (ready !== null) return ready;
@@ -149749,16 +149901,7 @@ function createClipService(deps) {
149749
149901
  });
149750
149902
  return null;
149751
149903
  }
149752
- const fileName = row.subFileName;
149753
- const api = await device.getApi();
149754
- const downloaded = await fetchClipBytes({
149755
- download: () => api.downloadRecordingDemuxed({
149756
- channel: device.channel,
149757
- ...device.uid !== void 0 ? { uid: device.uid } : {},
149758
- fileName
149759
- }),
149760
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
149761
- });
149904
+ const downloaded = await fetchVerifiedClipBytes(device, row, clipKey);
149762
149905
  const audio = decideClipAudio(downloaded.audio);
149763
149906
  if (audio.kind === "silent") deps.logger.warn("videoclips: serving a clip with no sound", {
149764
149907
  tags: { deviceId },
@@ -149838,7 +149981,8 @@ function createClipService(deps) {
149838
149981
  await provider.listClips({
149839
149982
  deviceId,
149840
149983
  since: until - CLIP_CATALOG_REFRESH_WINDOW_MS,
149841
- until
149984
+ until,
149985
+ provider: REOLINK_ADDON_ID
149842
149986
  });
149843
149987
  },
149844
149988
  hostBusy: (hostKey) => queue.busy(hostKey),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.147",
3
+ "version": "1.2.148",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",