@camstack/addon-pipeline 1.2.74 → 1.2.75
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-utils-CQs-AjTJ.js → addon-utils-a_36HH_N.js} +4 -4
- package/dist/audio-analyzer/index.js +5 -6
- package/dist/audio-analyzer/index.mjs +3 -3
- package/dist/detection-pipeline/index.js +9 -10
- package/dist/detection-pipeline/index.mjs +5 -5
- package/dist/{dist-CgJ8MRAy.js → dist-6ZJCV0jY.js} +1900 -27
- package/dist/{dist-CKY0efl3.mjs → dist-T_8jMw7d.mjs} +1865 -27
- package/dist/{event-loop-stall-monitor-Dx-zXcNg.js → event-loop-stall-monitor-Cjof8vA1.js} +1 -1
- package/dist/{event-loop-stall-monitor-CIapFIez.mjs → event-loop-stall-monitor-XeGmL4xr.mjs} +1 -1
- package/dist/lazy-sharp-6oymT_yf.mjs +341 -0
- package/dist/lazy-sharp-DvwPafeP.js +360 -0
- package/dist/motion-wasm/index.js +3 -3
- package/dist/motion-wasm/index.mjs +3 -3
- package/dist/pipeline-runner/index.js +6 -7
- package/dist/pipeline-runner/index.mjs +4 -4
- package/dist/recorder/index.js +138 -32
- package/dist/recorder/index.mjs +137 -30
- package/dist/session-decode/decode-worker-child.js +311 -126
- package/dist/session-decode/decode-worker-child.mjs +311 -123
- package/dist/stream-broker/_stub.js +94 -94
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DVJ4gKkk.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-H2OTmgR6.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-RYTOS6B-.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DfsLsn2e.mjs +26 -0
- package/dist/stream-broker/{hostInit-5SFdfUyc.mjs → hostInit-4XDN_Nt0.mjs} +2 -2
- package/dist/stream-broker/index.js +12 -13
- package/dist/stream-broker/index.mjs +3 -3
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-D44DCkcs.js → worker-protocol-B0cbTXyw.js} +1 -1
- package/dist/{worker-protocol-BzAQPUR-.mjs → worker-protocol-SqED7Y9i.mjs} +1 -1
- package/package.json +1 -1
- package/dist/chunk-emK7D4bc.js +0 -35
- package/dist/lazy-sharp-B-mb8uWx.mjs +0 -20
- package/dist/lazy-sharp-CcYuXtsa.js +0 -25
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C_plRUtD.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CExO0piw.mjs +0 -26
- package/dist/{addon-utils-CZ2xo67g.mjs → addon-utils-A2S9D7pu.mjs} +2 -2
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
const require_dist = require("./dist-6ZJCV0jY.js");
|
|
2
|
+
let node_url = require("node:url");
|
|
3
|
+
let node_module = require("node:module");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
node_path = require_dist.__toESM(node_path);
|
|
6
|
+
//#region src/shared/host-external-import.ts
|
|
7
|
+
/**
|
|
8
|
+
* Resolving a HOST-EXTERNAL package (`sharp`, `node-av`, …) from a process that
|
|
9
|
+
* does NOT have the framework ESM resolver hook registered.
|
|
10
|
+
*
|
|
11
|
+
* ## The problem this exists for
|
|
12
|
+
*
|
|
13
|
+
* An addon bundle leaves the packages in `HOST_EXTERNAL_SPECIFIERS`
|
|
14
|
+
* (`@camstack/system` → `kernel/host-externals.ts`) unbundled and expects the
|
|
15
|
+
* HOST to provide them. What provides them is a module-customization hook
|
|
16
|
+
* (`framework-resolver-hook.mjs`) that `registerFrameworkResolver` installs in
|
|
17
|
+
* the **addon-runner process**, re-anchoring those bare specifiers at
|
|
18
|
+
* `<frameworkDir>/node_modules`.
|
|
19
|
+
*
|
|
20
|
+
* A worker started with a plain `child_process.fork` — the session decode
|
|
21
|
+
* worker (`session-decode/fork-decode-worker.ts`) is the one in this package —
|
|
22
|
+
* does NOT inherit that hook. In it, `import('sharp')` and `import('node-av')`
|
|
23
|
+
* throw `Cannot find package 'X' imported from /data/addons/@camstack/…`,
|
|
24
|
+
* because the addon directory has no `node_modules` to walk up into.
|
|
25
|
+
*
|
|
26
|
+
* ## Why an explicit path and not the hook
|
|
27
|
+
*
|
|
28
|
+
* Registering the framework resolver in the child was considered and rejected:
|
|
29
|
+
*
|
|
30
|
+
* - it would need the child to reach INTO another package's build output
|
|
31
|
+
* (`<frameworkDir>/node_modules/@camstack/system/dist/…/framework-resolver-hook.mjs`)
|
|
32
|
+
* plus an `--import` shim, i.e. a hard coupling to a layout nothing checks;
|
|
33
|
+
* - a registered hook makes **every** `import()` in that process re-enter the
|
|
34
|
+
* loader and block the main thread on `Atomics.wait` until the hooks worker
|
|
35
|
+
* answers — the exact cost `shared/lazy-sharp.ts` was written to remove from
|
|
36
|
+
* the frame path. Paying it per decode worker to load two modules is the
|
|
37
|
+
* wrong trade;
|
|
38
|
+
* - the explicit-path fallback is already the shipped answer for `node-av` in
|
|
39
|
+
* this same worker, and had been for months. A second mechanism for `sharp`
|
|
40
|
+
* would mean two ways to fail.
|
|
41
|
+
*
|
|
42
|
+
* So: try the bare specifier (works on the hub, where the fork inherits the
|
|
43
|
+
* hook, and in dev, where workspace symlinks resolve), then resolve the package
|
|
44
|
+
* explicitly out of the module roots an agent is known to lay out.
|
|
45
|
+
*
|
|
46
|
+
* ## The anchors, in order
|
|
47
|
+
*
|
|
48
|
+
* - **DOCKER / single-copy node** — the framework overlay
|
|
49
|
+
* `CAMSTACK_FRAMEWORK_DIR` (`<frameworkDir>/node_modules/<pkg>`). The
|
|
50
|
+
* launcher points this at the ACTIVE closure in data-root mode and forked
|
|
51
|
+
* children inherit it (`server/backend/src/launcher.ts`).
|
|
52
|
+
* - **ELECTRON agent** — the app bundle beside `CAMSTACK_BUNDLED_ADDONS_DIR`
|
|
53
|
+
* (`<resources>/addons`): `<resources>/node_modules/<pkg>` and the
|
|
54
|
+
* `app.asar.unpacked` mirror.
|
|
55
|
+
* - **the process cwd** — always tried last, and it is NOT redundant:
|
|
56
|
+
* `CAMSTACK_FRAMEWORK_DIR` is only exported on some boots (the Docker
|
|
57
|
+
* entrypoint sets it in its fallback branch; the launcher only in data-root
|
|
58
|
+
* mode), while the launcher ALWAYS extends `NODE_PATH` with the framework and
|
|
59
|
+
* hub `node_modules`. A CJS-style resolution honours `NODE_PATH`; an ESM
|
|
60
|
+
* `import()` of a bare specifier does not. This is the same escape hatch
|
|
61
|
+
* `stream-broker/webrtc/session.ts` uses to find the framework's `werift`.
|
|
62
|
+
*
|
|
63
|
+
* An anchor that cannot resolve the specifier is skipped, so an empty result
|
|
64
|
+
* leaves the bare specifier as the only path, exactly as before.
|
|
65
|
+
*
|
|
66
|
+
* Resolution goes through `createRequire(...).resolve(specifier)` rather than a
|
|
67
|
+
* hardcoded relative entry (`dist/index.js`, …): that honours the package's own
|
|
68
|
+
* `exports`/`main`, so it keeps working across a dependency's file-layout
|
|
69
|
+
* changes, handles SUBPATH specifiers (`node-av/constants`) with no mapping
|
|
70
|
+
* table, and consults `NODE_PATH`. It also verifies the file exists, which is
|
|
71
|
+
* what the previous `fs.existsSync` check did by hand.
|
|
72
|
+
*/
|
|
73
|
+
/**
|
|
74
|
+
* The anchor filename resolution walks up from. It never has to EXIST —
|
|
75
|
+
* `createRequire` only needs a path to anchor at — but naming it after camstack
|
|
76
|
+
* keeps a stack trace readable if one ever escapes.
|
|
77
|
+
*/
|
|
78
|
+
var ANCHOR_FILE = "__camstack_host_external_anchor__.cjs";
|
|
79
|
+
/** Node error codes that mean "the module is not there", not "it failed to load". */
|
|
80
|
+
var RESOLUTION_ERROR_CODES = [
|
|
81
|
+
"ERR_MODULE_NOT_FOUND",
|
|
82
|
+
"MODULE_NOT_FOUND",
|
|
83
|
+
"ERR_PACKAGE_PATH_NOT_EXPORTED",
|
|
84
|
+
"ERR_UNSUPPORTED_DIR_IMPORT"
|
|
85
|
+
];
|
|
86
|
+
/** `err.code` when the error carries a string one. No cast: `in` narrows. */
|
|
87
|
+
function errorCode(err) {
|
|
88
|
+
if (typeof err !== "object" || err === null) return null;
|
|
89
|
+
if (!("code" in err)) return null;
|
|
90
|
+
const code = err.code;
|
|
91
|
+
return typeof code === "string" ? code : null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* True when `err` says the module could not be FOUND (as opposed to found and
|
|
95
|
+
* failed to load — a broken native binary, a throwing top level).
|
|
96
|
+
*
|
|
97
|
+
* The distinction is load-bearing for callers that memoize failure: a
|
|
98
|
+
* resolution failure is deterministic for the life of the process (the env and
|
|
99
|
+
* the framework layout do not change under a running worker), so retrying it
|
|
100
|
+
* per frame can only re-pay the loader round-trip and re-log. A LOAD failure
|
|
101
|
+
* can plausibly be a deploy blip and stays retryable.
|
|
102
|
+
*
|
|
103
|
+
* The message check is a belt for the same braces: Node has moved these codes
|
|
104
|
+
* before, and a miss here degrades to "retry forever", which is the bug.
|
|
105
|
+
*/
|
|
106
|
+
function isModuleResolutionError(err) {
|
|
107
|
+
const code = errorCode(err);
|
|
108
|
+
if (code !== null && RESOLUTION_ERROR_CODES.includes(code)) return true;
|
|
109
|
+
if (!(err instanceof Error)) return false;
|
|
110
|
+
return err.message.includes("Cannot find package") || err.message.includes("Cannot find module");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The files a resolution is anchored at, in the order they are tried. Each is a
|
|
114
|
+
* path INSIDE a candidate `node_modules` (so `<dir>/node_modules/<pkg>` is on
|
|
115
|
+
* the walk), except the cwd anchor, which walks up from the cwd itself.
|
|
116
|
+
*
|
|
117
|
+
* The anchor files never have to exist — `createRequire` only needs a path.
|
|
118
|
+
*/
|
|
119
|
+
function hostExternalAnchors(env, cwd) {
|
|
120
|
+
const anchors = [];
|
|
121
|
+
const frameworkDir = env["CAMSTACK_FRAMEWORK_DIR"];
|
|
122
|
+
if (frameworkDir !== void 0 && frameworkDir.length > 0) anchors.push(node_path.join(frameworkDir, "node_modules", ANCHOR_FILE));
|
|
123
|
+
const bundledAddons = env["CAMSTACK_BUNDLED_ADDONS_DIR"];
|
|
124
|
+
if (bundledAddons !== void 0 && bundledAddons.length > 0) {
|
|
125
|
+
const resourcesRoot = node_path.dirname(bundledAddons);
|
|
126
|
+
anchors.push(node_path.join(resourcesRoot, "node_modules", ANCHOR_FILE));
|
|
127
|
+
anchors.push(node_path.join(resourcesRoot, "app.asar.unpacked", "node_modules", ANCHOR_FILE));
|
|
128
|
+
}
|
|
129
|
+
anchors.push(node_path.join(cwd, ANCHOR_FILE));
|
|
130
|
+
return anchors;
|
|
131
|
+
}
|
|
132
|
+
/** `createRequire(anchor).resolve(specifier)`, or `null` when it is not there. */
|
|
133
|
+
function resolveFromAnchor(anchorPath, specifier) {
|
|
134
|
+
try {
|
|
135
|
+
return (0, node_module.createRequire)(anchorPath).resolve(specifier);
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Importable `file:` URLs for `specifier`, in the order a caller should try them
|
|
142
|
+
* after the bare specifier has already failed. Deduplicated (two anchors
|
|
143
|
+
* routinely resolve to the SAME file — a NODE_PATH hit, or a symlink), and empty
|
|
144
|
+
* when nothing resolves it, which leaves the bare specifier the only path.
|
|
145
|
+
*/
|
|
146
|
+
function hostExternalEntryUrls(specifier, options = {}) {
|
|
147
|
+
const env = options.env ?? process.env;
|
|
148
|
+
const cwd = options.cwd ?? process.cwd();
|
|
149
|
+
const resolveEntry = options.resolve ?? resolveFromAnchor;
|
|
150
|
+
const urls = [];
|
|
151
|
+
for (const anchor of hostExternalAnchors(env, cwd)) {
|
|
152
|
+
const entry = resolveEntry(anchor, specifier);
|
|
153
|
+
if (entry === null || entry.length === 0) continue;
|
|
154
|
+
const url = (0, node_url.pathToFileURL)(entry).href;
|
|
155
|
+
if (!urls.includes(url)) urls.push(url);
|
|
156
|
+
}
|
|
157
|
+
return urls;
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/shared/lazy-sharp.ts
|
|
161
|
+
/**
|
|
162
|
+
* lazy-sharp — resolve `sharp` ONCE, not once per frame, and give up ONCE when
|
|
163
|
+
* it cannot be resolved at all.
|
|
164
|
+
*
|
|
165
|
+
* ## What this fixes (2026-08-02)
|
|
166
|
+
*
|
|
167
|
+
* `await import('sharp')` sat directly in the frame path, in a spot whose own
|
|
168
|
+
* comment reads "This is the production hot path (camera frames from broker)",
|
|
169
|
+
* and in six other places along the same path. At ~13 fps per camera across the
|
|
170
|
+
* fleet that is thousands of dynamic imports a minute.
|
|
171
|
+
*
|
|
172
|
+
* Node caches the MODULE after the first import, so this looks free. It is not:
|
|
173
|
+
* the addon runner registers module-customization hooks (the framework
|
|
174
|
+
* resolver), and with hooks registered every `import()` re-enters the loader and
|
|
175
|
+
* resolves the specifier through the hooks worker — **synchronously**. A CPU
|
|
176
|
+
* profile of the live detection runner caught it exactly:
|
|
177
|
+
*
|
|
178
|
+
* makeSyncRequest@hooks:631
|
|
179
|
+
* resolveSync@hooks:860
|
|
180
|
+
* #resolveAndMaybeBlockOnLoaderThread@loader:715
|
|
181
|
+
* …
|
|
182
|
+
* import@loader:626
|
|
183
|
+
* runPipeline@index.js:5565
|
|
184
|
+
*
|
|
185
|
+
* `#resolveAndMaybeBlockOnLoaderThread` is the name of the problem:
|
|
186
|
+
* `makeSyncRequest` blocks the MAIN THREAD on `Atomics.wait` until the hooks
|
|
187
|
+
* worker answers. That is a synchronous block on the frame path, which is the
|
|
188
|
+
* shape the 22-88s event-loop freezes had.
|
|
189
|
+
*
|
|
190
|
+
* ## What it fixes a second time (2026-08-14)
|
|
191
|
+
*
|
|
192
|
+
* A memo that CLEARS on failure re-pays that entire cost on every call once the
|
|
193
|
+
* module genuinely is not there. The subject-tile tier hit exactly that: in the
|
|
194
|
+
* decode worker (a plain `child_process.fork`, no resolver hook) `import('sharp')`
|
|
195
|
+
* cannot resolve, and because the memo was dropped on failure, every subject of
|
|
196
|
+
* every frame re-entered the loader and logged a WARN — 3 800 WARN/min
|
|
197
|
+
* cluster-wide, with a synchronous loader round-trip on the frame path behind
|
|
198
|
+
* each one, and not one tile ever cut.
|
|
199
|
+
*
|
|
200
|
+
* So this module now does two more things:
|
|
201
|
+
*
|
|
202
|
+
* 1. **Falls back to an explicit path.** Same mechanism `node-av` has used in
|
|
203
|
+
* the same worker for months, extracted into
|
|
204
|
+
* `shared/host-external-import.ts` — read its docblock for the layouts and
|
|
205
|
+
* for why the framework resolver is NOT registered in the child instead.
|
|
206
|
+
* 2. **Distinguishes the two failure kinds.**
|
|
207
|
+
* - **Resolution** failure (nothing, anywhere, resolves `sharp`): the answer
|
|
208
|
+
* cannot change while this process lives, so it is memoized as a sticky
|
|
209
|
+
* {@link SharpUnavailableError}. Later calls reject IMMEDIATELY, with no
|
|
210
|
+
* loader re-entry, and exactly ONE warning is emitted for the process
|
|
211
|
+
* naming every candidate tried and the cause.
|
|
212
|
+
* - **Load** failure (found, but the native binary or the module body threw):
|
|
213
|
+
* the memo is cleared, so a later call retries. A partially-installed
|
|
214
|
+
* native dep during a deploy must not poison the encoder for the whole
|
|
215
|
+
* process lifetime — that would turn a blip into a camera that never
|
|
216
|
+
* produces media again. This is the original behaviour, kept on purpose,
|
|
217
|
+
* and it is safe to keep only because it is now the NARROW case.
|
|
218
|
+
*
|
|
219
|
+
* ## Why a memo and not a static import
|
|
220
|
+
*
|
|
221
|
+
* `sharp` is a heavy native module. The dynamic import was deliberate — nodes
|
|
222
|
+
* that never encode a frame should not pay to load it, and a node whose native
|
|
223
|
+
* build is missing must fail at first use rather than at boot. Both properties
|
|
224
|
+
* survive: the module is still loaded on FIRST use, and a failure still surfaces
|
|
225
|
+
* at that call. Only the repeat resolutions are removed.
|
|
226
|
+
*
|
|
227
|
+
* The promise itself is cached (not just the resolved value), so N concurrent
|
|
228
|
+
* first-callers share one import instead of racing into N.
|
|
229
|
+
*/
|
|
230
|
+
/** The bare specifier. Also the first entry of a failure's candidate list. */
|
|
231
|
+
var SHARP_SPECIFIER = "sharp";
|
|
232
|
+
/**
|
|
233
|
+
* `sharp` cannot be resolved in this process, and nothing about that can change
|
|
234
|
+
* while it runs. Carries the candidates tried so the log answers "where did you
|
|
235
|
+
* look" without a second round of diagnosis.
|
|
236
|
+
*/
|
|
237
|
+
var SharpUnavailableError = class extends Error {
|
|
238
|
+
attempts;
|
|
239
|
+
constructor(attempts, cause) {
|
|
240
|
+
super(`sharp could not be resolved in this process — tried ${attempts.join(", ")}; cause: ${describeError(cause)}`, { cause });
|
|
241
|
+
this.name = "SharpUnavailableError";
|
|
242
|
+
this.attempts = attempts;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
function describeError(err) {
|
|
246
|
+
return err instanceof Error ? err.message : String(err);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The bare specifier stays a LITERAL `import('sharp')` so the bundler keeps
|
|
250
|
+
* treating it as the declared external it is; only the fallback candidates go
|
|
251
|
+
* through a variable specifier (typed `any` by TS, so the declared return type
|
|
252
|
+
* applies without a cast — the same shape `importNodeAv` uses).
|
|
253
|
+
*/
|
|
254
|
+
async function importSharpModule(specifier) {
|
|
255
|
+
if (specifier === SHARP_SPECIFIER) return import("sharp");
|
|
256
|
+
return await import(specifier);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Build a loader. Exported for tests; production uses the module-level
|
|
260
|
+
* {@link getSharp}, which is one of these.
|
|
261
|
+
*/
|
|
262
|
+
function createSharpLoader(deps = {}) {
|
|
263
|
+
const importModule = deps.importModule ?? importSharpModule;
|
|
264
|
+
const fallbackUrls = deps.fallbackUrls ?? (() => hostExternalEntryUrls(SHARP_SPECIFIER));
|
|
265
|
+
const warn = deps.warn;
|
|
266
|
+
let pending = null;
|
|
267
|
+
let unavailable = null;
|
|
268
|
+
let warned = false;
|
|
269
|
+
/** Bare first, then each fallback candidate, in order. */
|
|
270
|
+
const attempt = async () => {
|
|
271
|
+
const attempts = [SHARP_SPECIFIER];
|
|
272
|
+
let lastError;
|
|
273
|
+
try {
|
|
274
|
+
return (await importModule(SHARP_SPECIFIER)).default;
|
|
275
|
+
} catch (bareError) {
|
|
276
|
+
if (!isModuleResolutionError(bareError)) {
|
|
277
|
+
pending = null;
|
|
278
|
+
throw bareError;
|
|
279
|
+
}
|
|
280
|
+
lastError = bareError;
|
|
281
|
+
}
|
|
282
|
+
for (const url of fallbackUrls()) {
|
|
283
|
+
attempts.push(url);
|
|
284
|
+
try {
|
|
285
|
+
return (await importModule(url)).default;
|
|
286
|
+
} catch (candidateError) {
|
|
287
|
+
if (!isModuleResolutionError(candidateError)) {
|
|
288
|
+
pending = null;
|
|
289
|
+
throw candidateError;
|
|
290
|
+
}
|
|
291
|
+
lastError = candidateError;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const failure = new SharpUnavailableError(attempts, lastError);
|
|
295
|
+
unavailable = failure;
|
|
296
|
+
pending = null;
|
|
297
|
+
if (!warned) {
|
|
298
|
+
warned = true;
|
|
299
|
+
if (warn) warn(failure.message);
|
|
300
|
+
else process.stderr.write(`${failure.message}\n`);
|
|
301
|
+
}
|
|
302
|
+
throw failure;
|
|
303
|
+
};
|
|
304
|
+
return {
|
|
305
|
+
load: () => {
|
|
306
|
+
if (unavailable !== null) return Promise.reject(unavailable);
|
|
307
|
+
if (pending === null) pending = attempt();
|
|
308
|
+
return pending;
|
|
309
|
+
},
|
|
310
|
+
reset: () => {
|
|
311
|
+
pending = null;
|
|
312
|
+
unavailable = null;
|
|
313
|
+
warned = false;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/** Where the process-wide loader's single diagnostic goes. */
|
|
318
|
+
var warnSink = null;
|
|
319
|
+
/**
|
|
320
|
+
* Route the one-off "sharp is unavailable" diagnostic somewhere it will be seen.
|
|
321
|
+
* Without a sink it goes to stderr, which the decode worker's parent inherits
|
|
322
|
+
* but does NOT forward to the scoped logger — so the worker installs one at
|
|
323
|
+
* boot (`decode-worker-child.ts`) to get the line into `addons.getLogs` with its
|
|
324
|
+
* `[dev:<id>]` prefix.
|
|
325
|
+
*/
|
|
326
|
+
function setSharpWarnSink(sink) {
|
|
327
|
+
warnSink = sink;
|
|
328
|
+
}
|
|
329
|
+
var defaultLoader = createSharpLoader({ warn: (message) => {
|
|
330
|
+
if (warnSink) warnSink(message);
|
|
331
|
+
else process.stderr.write(`${message}\n`);
|
|
332
|
+
} });
|
|
333
|
+
/**
|
|
334
|
+
* The `sharp` factory, resolved once per process.
|
|
335
|
+
*
|
|
336
|
+
* Rejects with {@link SharpUnavailableError} — immediately, and without touching
|
|
337
|
+
* the module loader — once resolution has been proven impossible here.
|
|
338
|
+
*/
|
|
339
|
+
function getSharp() {
|
|
340
|
+
return defaultLoader.load();
|
|
341
|
+
}
|
|
342
|
+
//#endregion
|
|
343
|
+
Object.defineProperty(exports, "getSharp", {
|
|
344
|
+
enumerable: true,
|
|
345
|
+
get: function() {
|
|
346
|
+
return getSharp;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
Object.defineProperty(exports, "hostExternalEntryUrls", {
|
|
350
|
+
enumerable: true,
|
|
351
|
+
get: function() {
|
|
352
|
+
return hostExternalEntryUrls;
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
Object.defineProperty(exports, "setSharpWarnSink", {
|
|
356
|
+
enumerable: true,
|
|
357
|
+
get: function() {
|
|
358
|
+
return setSharpWarnSink;
|
|
359
|
+
}
|
|
360
|
+
});
|
|
@@ -2,10 +2,10 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
7
|
-
let node_fs = require("node:fs");
|
|
5
|
+
const require_dist = require("../dist-6ZJCV0jY.js");
|
|
6
|
+
const require_lazy_sharp = require("../lazy-sharp-DvwPafeP.js");
|
|
8
7
|
let node_path = require("node:path");
|
|
8
|
+
let node_fs = require("node:fs");
|
|
9
9
|
//#region src/motion-wasm/wasm-motion-detector.ts
|
|
10
10
|
/**
|
|
11
11
|
* WasmMotionDetector — loads the AssemblyScript WASM module and provides
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { H as motionDetectionCapability, R as evaluateZoneRules, ft as hydrateSchema, st as BaseAddon, ut as DeviceType } from "../dist-
|
|
2
|
-
import { t as getSharp } from "../lazy-sharp-
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { H as motionDetectionCapability, R as evaluateZoneRules, ft as hydrateSchema, st as BaseAddon, ut as DeviceType } from "../dist-T_8jMw7d.mjs";
|
|
2
|
+
import { t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
|
|
4
3
|
import { join } from "node:path";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
5
|
//#region src/motion-wasm/wasm-motion-detector.ts
|
|
6
6
|
/**
|
|
7
7
|
* WasmMotionDetector — loads the AssemblyScript WASM module and provides
|
|
@@ -2,20 +2,19 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const require_worker_protocol = require("../worker-protocol-D44DCkcs.js");
|
|
5
|
+
const require_dist = require("../dist-6ZJCV0jY.js");
|
|
6
|
+
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-Cjof8vA1.js");
|
|
7
|
+
const require_worker_protocol = require("../worker-protocol-B0cbTXyw.js");
|
|
9
8
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
10
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
9
|
+
const require_lazy_sharp = require("../lazy-sharp-DvwPafeP.js");
|
|
11
10
|
const require_remote_restream = require("../remote-restream-BYbAsgUf.js");
|
|
12
11
|
let sharp = require("sharp");
|
|
13
|
-
sharp =
|
|
12
|
+
sharp = require_dist.__toESM(sharp);
|
|
14
13
|
let node_child_process = require("node:child_process");
|
|
15
14
|
let node_url = require("node:url");
|
|
16
15
|
let node_perf_hooks = require("node:perf_hooks");
|
|
17
16
|
let node_os = require("node:os");
|
|
18
|
-
node_os =
|
|
17
|
+
node_os = require_dist.__toESM(node_os);
|
|
19
18
|
//#region src/session-decode/fork-decode-worker.ts
|
|
20
19
|
/** Absolute path to the built decode-worker child entry (a vite build entry). */
|
|
21
20
|
var CHILD_ENTRY_PATH = (0, node_url.fileURLToPath)(new URL("../session-decode/decode-worker-child.js", require("url").pathToFileURL(__filename).href));
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { At as string, Ct as boolean, Dt as number, Et as literal, K as pipelineRunnerCapability, M as deriveDetailCropRect, Mt as EventCategory, Ot as object, St as array, Tt as lazy, U as pickDetailCropConvention, W as pickNativeLeaseOverride, dt as createEvent, gt as nodePin, j as defineCustomActions, k as customAction, s as DEFAULT_DETAIL_CROP_CONVENTION, st as BaseAddon, tt as errMsg, u as DEVICE_BACKEND_TO_FORMAT, xt as _enum } from "../dist-
|
|
2
|
-
import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-
|
|
3
|
-
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-
|
|
1
|
+
import { At as string, Ct as boolean, Dt as number, Et as literal, K as pipelineRunnerCapability, M as deriveDetailCropRect, Mt as EventCategory, Ot as object, St as array, Tt as lazy, U as pickDetailCropConvention, W as pickNativeLeaseOverride, dt as createEvent, gt as nodePin, j as defineCustomActions, k as customAction, s as DEFAULT_DETAIL_CROP_CONVENTION, st as BaseAddon, tt as errMsg, u as DEVICE_BACKEND_TO_FORMAT, xt as _enum } from "../dist-T_8jMw7d.mjs";
|
|
2
|
+
import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-XeGmL4xr.mjs";
|
|
3
|
+
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-SqED7Y9i.mjs";
|
|
4
4
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
5
|
-
import { t as getSharp } from "../lazy-sharp-
|
|
5
|
+
import { t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
|
|
6
6
|
import { i as withDetectionIntent, n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-Ci7RXNGb.mjs";
|
|
7
7
|
import sharp from "sharp";
|
|
8
8
|
import { fork } from "node:child_process";
|
package/dist/recorder/index.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
const
|
|
2
|
-
const require_dist = require("../dist-CgJ8MRAy.js");
|
|
1
|
+
const require_dist = require("../dist-6ZJCV0jY.js");
|
|
3
2
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
4
|
-
const require_addon_utils = require("../addon-utils-
|
|
3
|
+
const require_addon_utils = require("../addon-utils-a_36HH_N.js");
|
|
5
4
|
let node_crypto = require("node:crypto");
|
|
6
5
|
let node_child_process = require("node:child_process");
|
|
7
|
-
let node_fs = require("node:fs");
|
|
8
6
|
let node_path = require("node:path");
|
|
9
|
-
node_path =
|
|
7
|
+
node_path = require_dist.__toESM(node_path);
|
|
8
|
+
let node_fs = require("node:fs");
|
|
10
9
|
let node_fs_promises = require("node:fs/promises");
|
|
11
10
|
//#region src/recorder/event-capture.ts
|
|
12
11
|
function markerFromMotion(data, pad) {
|
|
@@ -2251,7 +2250,7 @@ var DEFAULT_DEVICE_CONFIG = {
|
|
|
2251
2250
|
enabled: false,
|
|
2252
2251
|
bands: []
|
|
2253
2252
|
};
|
|
2254
|
-
function serialize(map) {
|
|
2253
|
+
function serialize$1(map) {
|
|
2255
2254
|
const out = {};
|
|
2256
2255
|
for (const [id, config] of map) out[String(id)] = config;
|
|
2257
2256
|
return out;
|
|
@@ -2306,7 +2305,7 @@ async function saveDeviceConfig(state, deviceId, config) {
|
|
|
2306
2305
|
};
|
|
2307
2306
|
const map = await readDeviceConfigs(state);
|
|
2308
2307
|
map.set(deviceId, validated);
|
|
2309
|
-
await state.set(serialize(map));
|
|
2308
|
+
await state.set(serialize$1(map));
|
|
2310
2309
|
return validated;
|
|
2311
2310
|
}
|
|
2312
2311
|
/**
|
|
@@ -2644,6 +2643,33 @@ function buildExportArgs(input) {
|
|
|
2644
2643
|
var RECORDING_EXPORTS_KEY = "recordingExports";
|
|
2645
2644
|
/** Persisted shape: export id → full ExportRecord. */
|
|
2646
2645
|
var RecordingExportsBlobSchema = require_dist.record(require_dist.string(), require_dist.ExportRecordSchema);
|
|
2646
|
+
/**
|
|
2647
|
+
* One in-flight mutation chain per durable handle.
|
|
2648
|
+
*
|
|
2649
|
+
* EVERY mutation here is a read-modify-write of the SAME blob, and both halves
|
|
2650
|
+
* are async — so two overlapping cycles read the same snapshot and the second
|
|
2651
|
+
* `set` discards whatever the first one added. Last writer wins, silently.
|
|
2652
|
+
*
|
|
2653
|
+
* That is not a hypothetical. The engine patches `progressPct` roughly once a
|
|
2654
|
+
* second for an in-flight render, unawaited; on 2026-08-14 a `createExport` for
|
|
2655
|
+
* camera 615 landed between one such patch's read and its write, the row was
|
|
2656
|
+
* erased, and five seconds later the poll answered `unknown export
|
|
2657
|
+
* "547ea182-…"`. The camera's whole night was lost to it — the scheduler read a
|
|
2658
|
+
* vanished row as a failed render and re-queued, forever.
|
|
2659
|
+
*
|
|
2660
|
+
* Serialising per handle is the smallest fix that makes the loss impossible:
|
|
2661
|
+
* the cycles queue instead of interleaving, and the queue is per state object
|
|
2662
|
+
* so two recorders in one process never contend. A `WeakMap` keeps a disposed
|
|
2663
|
+
* handle collectable. Reads stay OUTSIDE the chain — a blob replacement is
|
|
2664
|
+
* atomic, so a reader always sees one consistent generation.
|
|
2665
|
+
*/
|
|
2666
|
+
var mutationChains = /* @__PURE__ */ new WeakMap();
|
|
2667
|
+
/** Run `work` after every mutation already queued for `state`, never before. */
|
|
2668
|
+
async function serialize(state, work) {
|
|
2669
|
+
const next = (mutationChains.get(state) ?? Promise.resolve()).then(work, work);
|
|
2670
|
+
mutationChains.set(state, next.catch(() => void 0));
|
|
2671
|
+
return next;
|
|
2672
|
+
}
|
|
2647
2673
|
/** Read every persisted export row, keyed by export id. */
|
|
2648
2674
|
async function readExports(state) {
|
|
2649
2675
|
const blob = await state.get();
|
|
@@ -2652,26 +2678,30 @@ async function readExports(state) {
|
|
|
2652
2678
|
/** Insert or replace one export row. Validates at the boundary. Returns it. */
|
|
2653
2679
|
async function upsertExport(state, record) {
|
|
2654
2680
|
const validated = require_dist.ExportRecordSchema.parse(record);
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2681
|
+
return serialize(state, async () => {
|
|
2682
|
+
const map = await readExports(state);
|
|
2683
|
+
map.set(validated.id, validated);
|
|
2684
|
+
await state.set(Object.fromEntries(map));
|
|
2685
|
+
return validated;
|
|
2686
|
+
});
|
|
2659
2687
|
}
|
|
2660
2688
|
/**
|
|
2661
2689
|
* Shallow-merge a patch into one export row and persist. Returns the merged
|
|
2662
2690
|
* record, or null when the id is unknown.
|
|
2663
2691
|
*/
|
|
2664
2692
|
async function patchExport(state, id, patch) {
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2693
|
+
return serialize(state, async () => {
|
|
2694
|
+
const map = await readExports(state);
|
|
2695
|
+
const existing = map.get(id);
|
|
2696
|
+
if (!existing) return null;
|
|
2697
|
+
const merged = require_dist.ExportRecordSchema.parse({
|
|
2698
|
+
...existing,
|
|
2699
|
+
...patch
|
|
2700
|
+
});
|
|
2701
|
+
map.set(id, merged);
|
|
2702
|
+
await state.set(Object.fromEntries(map));
|
|
2703
|
+
return merged;
|
|
2671
2704
|
});
|
|
2672
|
-
map.set(id, merged);
|
|
2673
|
-
await state.set(Object.fromEntries(map));
|
|
2674
|
-
return merged;
|
|
2675
2705
|
}
|
|
2676
2706
|
/** Read one export row by id, or null. */
|
|
2677
2707
|
async function getExportRecord(state, id) {
|
|
@@ -3002,6 +3032,80 @@ async function markDownloaded(id, now, deps, scheduleDelete, graceMs) {
|
|
|
3002
3032
|
}, graceMs);
|
|
3003
3033
|
}
|
|
3004
3034
|
//#endregion
|
|
3035
|
+
//#region src/recorder/addon/export-reconcile.ts
|
|
3036
|
+
/**
|
|
3037
|
+
* States that can only mean "a render was owed and the process went away".
|
|
3038
|
+
*
|
|
3039
|
+
* There is no live render at reconcile time by construction: the engine is
|
|
3040
|
+
* built fresh with an empty queue, and this runs before anything is enqueued.
|
|
3041
|
+
*/
|
|
3042
|
+
function isInterrupted(record) {
|
|
3043
|
+
return record.state === "queued" || record.state === "rendering";
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Hand every interrupted row back to the engine, once, at boot.
|
|
3047
|
+
*
|
|
3048
|
+
* A row past its `expiresAt` is FAILED rather than re-rendered: hours of
|
|
3049
|
+
* footage re-encoded for a file the janitor is about to drop costs a recording
|
|
3050
|
+
* node an ffmpeg for nothing. Failing it is also what makes the outcome
|
|
3051
|
+
* visible — a row that silently stays `queued` forever is the defect this
|
|
3052
|
+
* module exists to remove, and swapping it for a quieter one would not be a
|
|
3053
|
+
* fix.
|
|
3054
|
+
*/
|
|
3055
|
+
async function reconcileInterruptedExports(deps) {
|
|
3056
|
+
const now = deps.now();
|
|
3057
|
+
const interrupted = (await listExportsFor(deps.state)).filter(isInterrupted);
|
|
3058
|
+
if (interrupted.length === 0) return {
|
|
3059
|
+
requeued: 0,
|
|
3060
|
+
expired: 0
|
|
3061
|
+
};
|
|
3062
|
+
let requeued = 0;
|
|
3063
|
+
let expired = 0;
|
|
3064
|
+
for (const row of interrupted) {
|
|
3065
|
+
const tags = { deviceId: row.deviceId };
|
|
3066
|
+
const meta = {
|
|
3067
|
+
exportId: row.id,
|
|
3068
|
+
wasState: row.state,
|
|
3069
|
+
fromMs: row.fromMs,
|
|
3070
|
+
toMs: row.toMs,
|
|
3071
|
+
createdAt: row.createdAt
|
|
3072
|
+
};
|
|
3073
|
+
if (row.expiresAt <= now) {
|
|
3074
|
+
expired += 1;
|
|
3075
|
+
await patchExport(deps.state, row.id, {
|
|
3076
|
+
state: "failed",
|
|
3077
|
+
progressPct: null,
|
|
3078
|
+
error: "interrupted by a restart and expired before it could be rendered"
|
|
3079
|
+
});
|
|
3080
|
+
deps.logger.warn("export abandoned: interrupted by a restart, and already expired", {
|
|
3081
|
+
tags,
|
|
3082
|
+
meta
|
|
3083
|
+
});
|
|
3084
|
+
continue;
|
|
3085
|
+
}
|
|
3086
|
+
requeued += 1;
|
|
3087
|
+
await patchExport(deps.state, row.id, {
|
|
3088
|
+
state: "queued",
|
|
3089
|
+
progressPct: null,
|
|
3090
|
+
error: null
|
|
3091
|
+
});
|
|
3092
|
+
deps.logger.warn("export re-queued: it was interrupted by a restart", {
|
|
3093
|
+
tags,
|
|
3094
|
+
meta
|
|
3095
|
+
});
|
|
3096
|
+
deps.engine.enqueue(row.id);
|
|
3097
|
+
}
|
|
3098
|
+
deps.logger.info("recording exports reconciled after restart", { meta: {
|
|
3099
|
+
interrupted: interrupted.length,
|
|
3100
|
+
requeued,
|
|
3101
|
+
expired
|
|
3102
|
+
} });
|
|
3103
|
+
return {
|
|
3104
|
+
requeued,
|
|
3105
|
+
expired
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
//#endregion
|
|
3005
3109
|
//#region src/recorder/playlist.ts
|
|
3006
3110
|
/** A VOD variant playlist with an absolute wall-clock anchor per segment. */
|
|
3007
3111
|
function buildVariantPlaylist(segments) {
|
|
@@ -7479,23 +7583,25 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
7479
7583
|
this.retentionSweeper?.start();
|
|
7480
7584
|
}
|
|
7481
7585
|
/**
|
|
7482
|
-
* Resume interrupted exports and arm the expiry sweep.
|
|
7483
|
-
*
|
|
7484
|
-
*
|
|
7485
|
-
*
|
|
7586
|
+
* Resume interrupted exports and arm the expiry sweep.
|
|
7587
|
+
*
|
|
7588
|
+
* The resume itself lives in `export-reconcile.ts` — pure, tested, and loud
|
|
7589
|
+
* about every row it moves. It used to be an untested loop inlined here that
|
|
7590
|
+
* said nothing at all, so "why is this export still queued an hour later" had
|
|
7591
|
+
* no answer in the log, and a row whose life had already run out was
|
|
7592
|
+
* re-rendered anyway. Both halves are best-effort: a failure never blocks
|
|
7593
|
+
* boot.
|
|
7486
7594
|
*/
|
|
7487
7595
|
async bootstrapExports() {
|
|
7488
7596
|
const engine = this.exportEngine;
|
|
7489
7597
|
if (!engine) return;
|
|
7490
|
-
const state = this.exportsStore();
|
|
7491
7598
|
try {
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
}
|
|
7599
|
+
await reconcileInterruptedExports({
|
|
7600
|
+
state: this.exportsStore(),
|
|
7601
|
+
engine,
|
|
7602
|
+
now: () => Date.now(),
|
|
7603
|
+
logger: this.ctx.logger.child("export")
|
|
7604
|
+
});
|
|
7499
7605
|
} catch (err) {
|
|
7500
7606
|
this.ctx.logger.warn("recorder: export boot re-queue failed", { meta: { error: require_dist.errMsg(err) } });
|
|
7501
7607
|
}
|