@camstack/addon-provider-reolink 1.2.161 → 1.2.162
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 +63 -4
- package/dist/addon.mjs +63 -4
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -25360,6 +25360,39 @@ _enum([
|
|
|
25360
25360
|
_enum(["deferred", "final"]);
|
|
25361
25361
|
/** The camera's / provider's own words. Kept verbatim, never parsed. */
|
|
25362
25362
|
var CLIP_STILL_REASON_HEADER = "x-camstack-reason";
|
|
25363
|
+
/**
|
|
25364
|
+
* Make a refusal sentence safe to put in an HTTP header.
|
|
25365
|
+
*
|
|
25366
|
+
* **An HTTP header value is Latin-1.** Node throws `Invalid character in
|
|
25367
|
+
* header content` on anything above U+00FF, the handler dies mid-response,
|
|
25368
|
+
* and the client gets a socket hang up — no status, no reason, nothing. The
|
|
25369
|
+
* mechanism that exists to EXPLAIN a refusal is then the thing that silences
|
|
25370
|
+
* it, and the surface shows "nothing happened".
|
|
25371
|
+
*
|
|
25372
|
+
* That is not hypothetical: measured 2026-09-23 on 1436, a Hikvision replay
|
|
25373
|
+
* refusal read `SETUP of the video track answered 500 — the camera refused
|
|
25374
|
+
* this replay`, and the EM DASH in it threw on every clip the operator
|
|
25375
|
+
* opened. This repository writes its refusals in prose, and its prose uses
|
|
25376
|
+
* `—`, `’` and `…` everywhere, so every reason string is a candidate.
|
|
25377
|
+
*
|
|
25378
|
+
* Substitutes rather than strips: an operator reading a log or a response
|
|
25379
|
+
* should get a readable sentence, not one with holes in it. The typographic
|
|
25380
|
+
* characters this repo actually writes have exact ASCII equivalents; anything
|
|
25381
|
+
* else non-Latin-1 becomes `?`, which is visible rather than silent. Control
|
|
25382
|
+
* characters go too — a newline in a header value is a response-splitting
|
|
25383
|
+
* bug, not a formatting one.
|
|
25384
|
+
*/
|
|
25385
|
+
function clipReasonHeaderValue(reason) {
|
|
25386
|
+
const folded = reason.replace(/[\u2013\u2014\u2212]/g, "-").replace(/[\u2018\u2019\u201b]/g, "'").replace(/[\u201c\u201d]/g, "\"").replace(/\u2026/g, "...").replace(/[\u00a0\u2007\u202f]/g, " ");
|
|
25387
|
+
let out = "";
|
|
25388
|
+
for (const ch of folded) {
|
|
25389
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
25390
|
+
if (code < 32 || code === 127) out += " ";
|
|
25391
|
+
else if (code > 255) out += "?";
|
|
25392
|
+
else out += ch;
|
|
25393
|
+
}
|
|
25394
|
+
return out.slice(0, 500).trim();
|
|
25395
|
+
}
|
|
25363
25396
|
/** One {@link ClipStillRefusalCodeSchema} token, alone. */
|
|
25364
25397
|
var CLIP_STILL_REASON_CODE_HEADER = "x-camstack-reason-code";
|
|
25365
25398
|
/** One {@link ClipStillDispositionSchema} token, alone. */
|
|
@@ -151601,7 +151634,7 @@ function writeRefusal(res, refusal, deviceId, logger, clipKey) {
|
|
|
151601
151634
|
else logger.debug(line, extras);
|
|
151602
151635
|
const retryAfterMs = refusal.disposition === "deferred" ? refusal.retryAfterMs : void 0;
|
|
151603
151636
|
res.writeHead(204, {
|
|
151604
|
-
[CLIP_STILL_REASON_HEADER]: refusal.reason
|
|
151637
|
+
[CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(refusal.reason),
|
|
151605
151638
|
[CLIP_STILL_REASON_CODE_HEADER]: refusal.code,
|
|
151606
151639
|
[CLIP_STILL_DISPOSITION_HEADER]: refusal.disposition,
|
|
151607
151640
|
...retryAfterMs === void 0 ? {} : { "retry-after": String(Math.max(1, Math.ceil(retryAfterMs / 1e3))) }
|
|
@@ -151798,7 +151831,7 @@ function createClipStreamHandler(deps) {
|
|
|
151798
151831
|
status: resolved.status
|
|
151799
151832
|
} });
|
|
151800
151833
|
res.writeHead(resolved.status, {
|
|
151801
|
-
[CLIP_STILL_REASON_HEADER]: resolved.detail
|
|
151834
|
+
[CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(resolved.detail),
|
|
151802
151835
|
[CLIP_STILL_REASON_CODE_HEADER]: resolved.code
|
|
151803
151836
|
});
|
|
151804
151837
|
res.end();
|
|
@@ -151871,7 +151904,7 @@ function createClipStreamHandler(deps) {
|
|
|
151871
151904
|
}
|
|
151872
151905
|
});
|
|
151873
151906
|
res.writeHead(streamRefusalStatus(outcome.code), {
|
|
151874
|
-
[CLIP_STILL_REASON_HEADER]: outcome.detail
|
|
151907
|
+
[CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(outcome.detail),
|
|
151875
151908
|
[CLIP_STILL_REASON_CODE_HEADER]: outcome.code
|
|
151876
151909
|
});
|
|
151877
151910
|
res.end();
|
|
@@ -152313,7 +152346,33 @@ function createClipStreamProducer(deps) {
|
|
|
152313
152346
|
}
|
|
152314
152347
|
});
|
|
152315
152348
|
activeByHost.set(hostKey, ahead + 1);
|
|
152316
|
-
|
|
152349
|
+
let abandonedAt = null;
|
|
152350
|
+
input.sink.onceClose(() => {
|
|
152351
|
+
abandonedAt ??= Date.now();
|
|
152352
|
+
});
|
|
152353
|
+
const queuedAt = Date.now();
|
|
152354
|
+
const run = (lanes.get(hostKey) ?? Promise.resolve()).then(async () => {
|
|
152355
|
+
if (abandonedAt !== null) {
|
|
152356
|
+
const waitedMs = abandonedAt - queuedAt;
|
|
152357
|
+
deps.logger.info("videoclips: a queued clip stream was dropped, its peer had gone", {
|
|
152358
|
+
tags: { deviceId },
|
|
152359
|
+
meta: {
|
|
152360
|
+
clipKey: input.row.clipKey,
|
|
152361
|
+
twin: input.twin,
|
|
152362
|
+
branch: "queued-peer-gone",
|
|
152363
|
+
waitedMs,
|
|
152364
|
+
aheadWhenQueued: ahead
|
|
152365
|
+
}
|
|
152366
|
+
});
|
|
152367
|
+
return {
|
|
152368
|
+
kind: "refused",
|
|
152369
|
+
code: "fetch-failed",
|
|
152370
|
+
detail: `the peer left after ${String(waitedMs)} ms in the queue, so this clip was never fetched`,
|
|
152371
|
+
attempts: 0
|
|
152372
|
+
};
|
|
152373
|
+
}
|
|
152374
|
+
return await runAttempts(input);
|
|
152375
|
+
}).finally(() => {
|
|
152317
152376
|
const left = (activeByHost.get(hostKey) ?? 1) - 1;
|
|
152318
152377
|
if (left <= 0) activeByHost.delete(hostKey);
|
|
152319
152378
|
else activeByHost.set(hostKey, left);
|
package/dist/addon.mjs
CHANGED
|
@@ -25355,6 +25355,39 @@ _enum([
|
|
|
25355
25355
|
_enum(["deferred", "final"]);
|
|
25356
25356
|
/** The camera's / provider's own words. Kept verbatim, never parsed. */
|
|
25357
25357
|
var CLIP_STILL_REASON_HEADER = "x-camstack-reason";
|
|
25358
|
+
/**
|
|
25359
|
+
* Make a refusal sentence safe to put in an HTTP header.
|
|
25360
|
+
*
|
|
25361
|
+
* **An HTTP header value is Latin-1.** Node throws `Invalid character in
|
|
25362
|
+
* header content` on anything above U+00FF, the handler dies mid-response,
|
|
25363
|
+
* and the client gets a socket hang up — no status, no reason, nothing. The
|
|
25364
|
+
* mechanism that exists to EXPLAIN a refusal is then the thing that silences
|
|
25365
|
+
* it, and the surface shows "nothing happened".
|
|
25366
|
+
*
|
|
25367
|
+
* That is not hypothetical: measured 2026-09-23 on 1436, a Hikvision replay
|
|
25368
|
+
* refusal read `SETUP of the video track answered 500 — the camera refused
|
|
25369
|
+
* this replay`, and the EM DASH in it threw on every clip the operator
|
|
25370
|
+
* opened. This repository writes its refusals in prose, and its prose uses
|
|
25371
|
+
* `—`, `’` and `…` everywhere, so every reason string is a candidate.
|
|
25372
|
+
*
|
|
25373
|
+
* Substitutes rather than strips: an operator reading a log or a response
|
|
25374
|
+
* should get a readable sentence, not one with holes in it. The typographic
|
|
25375
|
+
* characters this repo actually writes have exact ASCII equivalents; anything
|
|
25376
|
+
* else non-Latin-1 becomes `?`, which is visible rather than silent. Control
|
|
25377
|
+
* characters go too — a newline in a header value is a response-splitting
|
|
25378
|
+
* bug, not a formatting one.
|
|
25379
|
+
*/
|
|
25380
|
+
function clipReasonHeaderValue(reason) {
|
|
25381
|
+
const folded = reason.replace(/[\u2013\u2014\u2212]/g, "-").replace(/[\u2018\u2019\u201b]/g, "'").replace(/[\u201c\u201d]/g, "\"").replace(/\u2026/g, "...").replace(/[\u00a0\u2007\u202f]/g, " ");
|
|
25382
|
+
let out = "";
|
|
25383
|
+
for (const ch of folded) {
|
|
25384
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
25385
|
+
if (code < 32 || code === 127) out += " ";
|
|
25386
|
+
else if (code > 255) out += "?";
|
|
25387
|
+
else out += ch;
|
|
25388
|
+
}
|
|
25389
|
+
return out.slice(0, 500).trim();
|
|
25390
|
+
}
|
|
25358
25391
|
/** One {@link ClipStillRefusalCodeSchema} token, alone. */
|
|
25359
25392
|
var CLIP_STILL_REASON_CODE_HEADER = "x-camstack-reason-code";
|
|
25360
25393
|
/** One {@link ClipStillDispositionSchema} token, alone. */
|
|
@@ -151596,7 +151629,7 @@ function writeRefusal(res, refusal, deviceId, logger, clipKey) {
|
|
|
151596
151629
|
else logger.debug(line, extras);
|
|
151597
151630
|
const retryAfterMs = refusal.disposition === "deferred" ? refusal.retryAfterMs : void 0;
|
|
151598
151631
|
res.writeHead(204, {
|
|
151599
|
-
[CLIP_STILL_REASON_HEADER]: refusal.reason
|
|
151632
|
+
[CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(refusal.reason),
|
|
151600
151633
|
[CLIP_STILL_REASON_CODE_HEADER]: refusal.code,
|
|
151601
151634
|
[CLIP_STILL_DISPOSITION_HEADER]: refusal.disposition,
|
|
151602
151635
|
...retryAfterMs === void 0 ? {} : { "retry-after": String(Math.max(1, Math.ceil(retryAfterMs / 1e3))) }
|
|
@@ -151793,7 +151826,7 @@ function createClipStreamHandler(deps) {
|
|
|
151793
151826
|
status: resolved.status
|
|
151794
151827
|
} });
|
|
151795
151828
|
res.writeHead(resolved.status, {
|
|
151796
|
-
[CLIP_STILL_REASON_HEADER]: resolved.detail
|
|
151829
|
+
[CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(resolved.detail),
|
|
151797
151830
|
[CLIP_STILL_REASON_CODE_HEADER]: resolved.code
|
|
151798
151831
|
});
|
|
151799
151832
|
res.end();
|
|
@@ -151866,7 +151899,7 @@ function createClipStreamHandler(deps) {
|
|
|
151866
151899
|
}
|
|
151867
151900
|
});
|
|
151868
151901
|
res.writeHead(streamRefusalStatus(outcome.code), {
|
|
151869
|
-
[CLIP_STILL_REASON_HEADER]: outcome.detail
|
|
151902
|
+
[CLIP_STILL_REASON_HEADER]: clipReasonHeaderValue(outcome.detail),
|
|
151870
151903
|
[CLIP_STILL_REASON_CODE_HEADER]: outcome.code
|
|
151871
151904
|
});
|
|
151872
151905
|
res.end();
|
|
@@ -152308,7 +152341,33 @@ function createClipStreamProducer(deps) {
|
|
|
152308
152341
|
}
|
|
152309
152342
|
});
|
|
152310
152343
|
activeByHost.set(hostKey, ahead + 1);
|
|
152311
|
-
|
|
152344
|
+
let abandonedAt = null;
|
|
152345
|
+
input.sink.onceClose(() => {
|
|
152346
|
+
abandonedAt ??= Date.now();
|
|
152347
|
+
});
|
|
152348
|
+
const queuedAt = Date.now();
|
|
152349
|
+
const run = (lanes.get(hostKey) ?? Promise.resolve()).then(async () => {
|
|
152350
|
+
if (abandonedAt !== null) {
|
|
152351
|
+
const waitedMs = abandonedAt - queuedAt;
|
|
152352
|
+
deps.logger.info("videoclips: a queued clip stream was dropped, its peer had gone", {
|
|
152353
|
+
tags: { deviceId },
|
|
152354
|
+
meta: {
|
|
152355
|
+
clipKey: input.row.clipKey,
|
|
152356
|
+
twin: input.twin,
|
|
152357
|
+
branch: "queued-peer-gone",
|
|
152358
|
+
waitedMs,
|
|
152359
|
+
aheadWhenQueued: ahead
|
|
152360
|
+
}
|
|
152361
|
+
});
|
|
152362
|
+
return {
|
|
152363
|
+
kind: "refused",
|
|
152364
|
+
code: "fetch-failed",
|
|
152365
|
+
detail: `the peer left after ${String(waitedMs)} ms in the queue, so this clip was never fetched`,
|
|
152366
|
+
attempts: 0
|
|
152367
|
+
};
|
|
152368
|
+
}
|
|
152369
|
+
return await runAttempts(input);
|
|
152370
|
+
}).finally(() => {
|
|
152312
152371
|
const left = (activeByHost.get(hostKey) ?? 1) - 1;
|
|
152313
152372
|
if (left <= 0) activeByHost.delete(hostKey);
|
|
152314
152373
|
else activeByHost.set(hostKey, left);
|