@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.
- package/dist/addon.js +166 -22
- package/dist/addon.mjs +166 -22
- 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
|
-
*
|
|
25453
|
-
*
|
|
25454
|
-
*
|
|
25455
|
-
*
|
|
25456
|
-
*
|
|
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 — *"
|
|
25461
|
-
*
|
|
25462
|
-
*
|
|
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().
|
|
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
|
|
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
|
-
*
|
|
25448
|
-
*
|
|
25449
|
-
*
|
|
25450
|
-
*
|
|
25451
|
-
*
|
|
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 — *"
|
|
25456
|
-
*
|
|
25457
|
-
*
|
|
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().
|
|
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
|
|
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),
|