@camstack/addon-pipeline 1.1.39 → 1.1.40
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/audio-analyzer/index.js +168 -61
- package/dist/audio-analyzer/index.mjs +168 -61
- package/dist/detection-pipeline/index.js +2 -2
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-DbGdZ8Nr.js → dist-BJTPkJFw.js} +247 -3
- package/dist/{dist-DvvYzO58.mjs → dist-v6cKLmU3.mjs} +247 -3
- package/dist/{frame-handle-plane-B7jMIZc2.js → frame-handle-plane-BP8YV4sF.js} +54 -5
- package/dist/{frame-handle-plane-BFzoIfkd.mjs → frame-handle-plane-DKAXTtfn.mjs} +54 -5
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +893 -12
- package/dist/pipeline-runner/index.mjs +892 -13
- package/dist/recorder/index.js +2 -2
- package/dist/recorder/index.mjs +2 -2
- package/dist/session-decode/decode-worker-child.js +511 -0
- package/dist/session-decode/decode-worker-child.mjs +510 -0
- package/dist/stream-broker/_stub.js +44 -44
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DezFy4Fu.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-6I7-nufj.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DqJvWBKS.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-dGka8yZY.mjs +26 -0
- package/dist/stream-broker/{hostInit-BOf0hg9q.mjs → hostInit-CUkFGqiI.mjs} +3 -3
- package/dist/stream-broker/index.js +268 -38
- package/dist/stream-broker/index.mjs +268 -38
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/worker-protocol-BCfO8gUF.js +67 -0
- package/dist/worker-protocol-pk7qdYXt.mjs +56 -0
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Br2fqwe7.js → MaskShapeCanvas-DI4BY7W2-Cr_fCzY5.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DYXRf30g.js → MotionZonesSettings-NcxxQN8r-COcvoHDH.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DLpqLgPo.js → PrivacyMaskSettings-APgPLF7p-Bv-bbyJl.js} +1 -1
- package/embed-dist/assets/index-Ctbq9AXh.css +2 -0
- package/embed-dist/assets/index-jMj5PyJk.js +81 -0
- package/embed-dist/index.html +2 -2
- package/package.json +6 -3
- package/python/inference_pool.py +3 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DhY3MZ2C.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-BsrrdOBU.mjs +0 -26
- package/embed-dist/assets/index-C5UpuPr8.css +0 -2
- package/embed-dist/assets/index-CR367rYR.js +0 -81
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { n as isWorkerRequest } from "../worker-protocol-pk7qdYXt.mjs";
|
|
2
|
+
//#region src/session-decode/frame-slot.ts
|
|
3
|
+
function toReply(held) {
|
|
4
|
+
return {
|
|
5
|
+
frameId: held.frameId,
|
|
6
|
+
timestamp: held.timestamp,
|
|
7
|
+
width: held.width,
|
|
8
|
+
height: held.height
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Latest-wins decoded-frame slot + reserved-for-`toBuffer` holder +
|
|
13
|
+
* pending-pull flag. One instance per decode-worker child session.
|
|
14
|
+
*/
|
|
15
|
+
var FrameSlot = class {
|
|
16
|
+
nextFrameId = 1;
|
|
17
|
+
slot = null;
|
|
18
|
+
/**
|
|
19
|
+
* The frame handed to the MOST RECENT `pull`, retained so a following
|
|
20
|
+
* `toBuffer(frameId)` still resolves even though `publish` keeps advancing
|
|
21
|
+
* the latest-wins `slot`. Freed when the next `pull` supersedes it (or on
|
|
22
|
+
* `free()`).
|
|
23
|
+
*/
|
|
24
|
+
reserved = null;
|
|
25
|
+
pendingPull = false;
|
|
26
|
+
/**
|
|
27
|
+
* Publish a newly decoded frame into the latest-wins slot, freeing
|
|
28
|
+
* whatever the slot held before (the drop-older half of "latest-wins").
|
|
29
|
+
* Returns the reply to send if a `pull` was pending (resolving it
|
|
30
|
+
* immediately and reserving the frame); otherwise returns `null` and the
|
|
31
|
+
* frame simply waits in the slot for the next `pull`.
|
|
32
|
+
*/
|
|
33
|
+
publish(frame, timestamp, minIntervalMs) {
|
|
34
|
+
if (minIntervalMs > 0 && this.slot && timestamp - this.slot.timestamp < minIntervalMs) {
|
|
35
|
+
frame.free();
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const previous = this.slot;
|
|
39
|
+
const frameId = this.nextFrameId++;
|
|
40
|
+
this.slot = {
|
|
41
|
+
frameId,
|
|
42
|
+
frame,
|
|
43
|
+
timestamp,
|
|
44
|
+
width: frame.width,
|
|
45
|
+
height: frame.height
|
|
46
|
+
};
|
|
47
|
+
previous?.frame.free();
|
|
48
|
+
if (this.pendingPull) {
|
|
49
|
+
this.pendingPull = false;
|
|
50
|
+
return this.reserveSlot();
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Hand out the newest slot frame and RESERVE it (freeing the previously
|
|
56
|
+
* reserved frame), clearing the slot. Returns `null` if no frame has been
|
|
57
|
+
* decoded yet — the caller should then call `markPendingPull()`.
|
|
58
|
+
*/
|
|
59
|
+
pull() {
|
|
60
|
+
if (!this.slot) return null;
|
|
61
|
+
return this.reserveSlot();
|
|
62
|
+
}
|
|
63
|
+
/** Record that a pull is waiting for the next `publish` to resolve it. */
|
|
64
|
+
markPendingPull() {
|
|
65
|
+
this.pendingPull = true;
|
|
66
|
+
}
|
|
67
|
+
/** The reserved frame for `frameId`, or `null` if unknown/stale. */
|
|
68
|
+
toBuffer(frameId) {
|
|
69
|
+
if (!this.reserved || this.reserved.frameId !== frameId) return null;
|
|
70
|
+
return this.reserved.frame;
|
|
71
|
+
}
|
|
72
|
+
/** Free both the slot and the reserved frame (teardown). */
|
|
73
|
+
free() {
|
|
74
|
+
this.slot?.frame.free();
|
|
75
|
+
this.slot = null;
|
|
76
|
+
this.reserved?.frame.free();
|
|
77
|
+
this.reserved = null;
|
|
78
|
+
}
|
|
79
|
+
reserveSlot() {
|
|
80
|
+
const held = this.slot;
|
|
81
|
+
if (!held) throw new Error("frame-slot: reserveSlot called with no slot held");
|
|
82
|
+
const previousReserved = this.reserved;
|
|
83
|
+
this.reserved = held;
|
|
84
|
+
this.slot = null;
|
|
85
|
+
previousReserved?.frame.free();
|
|
86
|
+
return toReply(held);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/session-decode/decode-worker-child.ts
|
|
91
|
+
/** Re-dial backoff on a transient dial error/EOF (mirrors the decoder's PULL_REDIAL_MS). */
|
|
92
|
+
var REDIAL_MS = 3e3;
|
|
93
|
+
var _nav = null;
|
|
94
|
+
var _consts = null;
|
|
95
|
+
async function getNodeAv() {
|
|
96
|
+
if (!_nav) _nav = await import("node-av");
|
|
97
|
+
return _nav;
|
|
98
|
+
}
|
|
99
|
+
async function getConstants() {
|
|
100
|
+
if (!_consts) _consts = await import("node-av/constants");
|
|
101
|
+
return _consts;
|
|
102
|
+
}
|
|
103
|
+
function errMessage(err) {
|
|
104
|
+
if (err instanceof Error) return err.message;
|
|
105
|
+
return String(err);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* RTSP low-latency demuxer options — mirrors
|
|
109
|
+
* `addon-decoder-nodeav/src/pull-demuxer-options.ts` (duplicated here rather
|
|
110
|
+
* than cross-imported: this file is a standalone forked entry point, not an
|
|
111
|
+
* addon consumer of that package). `analyzeduration`/`probesize` stay small
|
|
112
|
+
* but non-zero — zeroing them starves the Reolink rfc4571 restream demuxer.
|
|
113
|
+
*/
|
|
114
|
+
function buildDemuxerOptions() {
|
|
115
|
+
return {
|
|
116
|
+
rtsp_transport: "tcp",
|
|
117
|
+
fflags: "nobuffer",
|
|
118
|
+
analyzeduration: "1000000",
|
|
119
|
+
probesize: "1000000",
|
|
120
|
+
max_delay: "0",
|
|
121
|
+
reorder_queue_size: "0",
|
|
122
|
+
user_agent: "decode-worker"
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Ordered hwaccel candidates per platform — first successful create() wins, else software. */
|
|
126
|
+
function candidateBackends() {
|
|
127
|
+
switch (process.platform) {
|
|
128
|
+
case "darwin": return ["videotoolbox"];
|
|
129
|
+
case "linux": return [
|
|
130
|
+
"vaapi",
|
|
131
|
+
"qsv",
|
|
132
|
+
"cuda"
|
|
133
|
+
];
|
|
134
|
+
case "win32": return [
|
|
135
|
+
"d3d11va",
|
|
136
|
+
"dxva2",
|
|
137
|
+
"cuda"
|
|
138
|
+
];
|
|
139
|
+
default: return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Map a canonical backend name to the node-av `AV_HWDEVICE_TYPE_*` constant. */
|
|
143
|
+
function backendToHwDeviceConst(backend, consts) {
|
|
144
|
+
switch (backend) {
|
|
145
|
+
case "videotoolbox": return consts.AV_HWDEVICE_TYPE_VIDEOTOOLBOX;
|
|
146
|
+
case "cuda":
|
|
147
|
+
case "nvdec": return consts.AV_HWDEVICE_TYPE_CUDA;
|
|
148
|
+
case "vaapi": return consts.AV_HWDEVICE_TYPE_VAAPI;
|
|
149
|
+
case "qsv": return consts.AV_HWDEVICE_TYPE_QSV;
|
|
150
|
+
case "d3d11va": return consts.AV_HWDEVICE_TYPE_D3D11VA;
|
|
151
|
+
case "dxva2": return consts.AV_HWDEVICE_TYPE_DXVA2;
|
|
152
|
+
case "amf": return consts.AV_HWDEVICE_TYPE_AMF;
|
|
153
|
+
case "vdpau": return consts.AV_HWDEVICE_TYPE_VDPAU;
|
|
154
|
+
case "drm": return consts.AV_HWDEVICE_TYPE_DRM;
|
|
155
|
+
default: return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Clamp to [min,max] then floor to the nearest even number (YUV420P chroma is 2x2 subsampled). */
|
|
159
|
+
function clampEven(value, min, max) {
|
|
160
|
+
const bounded = Math.min(Math.max(value, min), max);
|
|
161
|
+
return bounded - bounded % 2;
|
|
162
|
+
}
|
|
163
|
+
/** Resolve `FrameImageOptions.crop` against the decoded frame's dimensions, defaulting to the full frame. */
|
|
164
|
+
function resolveCropRegion(frameWidth, frameHeight, crop) {
|
|
165
|
+
const left = clampEven(crop?.left ?? 0, 0, Math.max(0, frameWidth - 2));
|
|
166
|
+
const top = clampEven(crop?.top ?? 0, 0, Math.max(0, frameHeight - 2));
|
|
167
|
+
return {
|
|
168
|
+
left,
|
|
169
|
+
top,
|
|
170
|
+
width: clampEven(crop?.width ?? frameWidth - left, 2, frameWidth - left),
|
|
171
|
+
height: clampEven(crop?.height ?? frameHeight - top, 2, frameHeight - top)
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** Resolve `FrameImageOptions.resize`, defaulting to the (already cropped) source dimensions. */
|
|
175
|
+
function resolveResizeTarget(cropWidth, cropHeight, resize) {
|
|
176
|
+
if (!resize) return {
|
|
177
|
+
width: cropWidth,
|
|
178
|
+
height: cropHeight
|
|
179
|
+
};
|
|
180
|
+
return {
|
|
181
|
+
width: Math.max(1, Math.round(resize.width)),
|
|
182
|
+
height: Math.max(1, Math.round(resize.height))
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The forked child's whole runtime: decode-loop lifecycle, the latest-wins
|
|
187
|
+
* frame slot, and the ROI scaler for `toBuffer`. One instance per process.
|
|
188
|
+
*/
|
|
189
|
+
var DecodeWorkerChild = class {
|
|
190
|
+
nav = null;
|
|
191
|
+
consts = null;
|
|
192
|
+
hwContext = null;
|
|
193
|
+
demuxer = null;
|
|
194
|
+
decoder = null;
|
|
195
|
+
abortController = null;
|
|
196
|
+
started = false;
|
|
197
|
+
stopped = false;
|
|
198
|
+
sessionFormat = "rgb";
|
|
199
|
+
minIntervalMs = 0;
|
|
200
|
+
/**
|
|
201
|
+
* The latest-wins decoded-frame slot + reserved-for-`toBuffer` holder +
|
|
202
|
+
* pending-pull flag — extracted to `FrameSlot` (Epic C P2 Task 2 / I2) so
|
|
203
|
+
* the reserve-on-pull invariant (`e3c78a9b`) is unit-tested without
|
|
204
|
+
* node-av. See that file's doc comment for the full semantics.
|
|
205
|
+
*/
|
|
206
|
+
frames = new FrameSlot();
|
|
207
|
+
/**
|
|
208
|
+
* Mirrors whether `this.frames` currently has a pull waiting, so
|
|
209
|
+
* `teardown` knows to resolve it with `{kind:'ended'}` — `FrameSlot`
|
|
210
|
+
* intentionally exposes no pending-pull query, only `markPendingPull()`,
|
|
211
|
+
* so the child tracks this in lockstep (set in `handlePull`, cleared
|
|
212
|
+
* whenever `consumeDecodedFrame`'s `publish` resolves it).
|
|
213
|
+
*/
|
|
214
|
+
pendingPull = false;
|
|
215
|
+
ended = false;
|
|
216
|
+
redialTimer = null;
|
|
217
|
+
handleMessage(message) {
|
|
218
|
+
if (!isWorkerRequest(message)) {
|
|
219
|
+
this.sendError("decode-worker-child: malformed request");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
switch (message.kind) {
|
|
223
|
+
case "start":
|
|
224
|
+
this.handleStart(message.source, message.opts);
|
|
225
|
+
return;
|
|
226
|
+
case "pull":
|
|
227
|
+
this.handlePull();
|
|
228
|
+
return;
|
|
229
|
+
case "toBuffer":
|
|
230
|
+
this.handleToBuffer(message.frameId, message.opts);
|
|
231
|
+
return;
|
|
232
|
+
case "stop":
|
|
233
|
+
this.handleStop();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Channel teardown (explicit `stop` or the parent disconnecting) — dispose, then exit. */
|
|
238
|
+
teardown() {
|
|
239
|
+
this.stopped = true;
|
|
240
|
+
this.ended = true;
|
|
241
|
+
if (this.redialTimer) {
|
|
242
|
+
clearTimeout(this.redialTimer);
|
|
243
|
+
this.redialTimer = null;
|
|
244
|
+
}
|
|
245
|
+
this.abortController?.abort();
|
|
246
|
+
this.closeInput();
|
|
247
|
+
this.frames.free();
|
|
248
|
+
this.hwContext?.[Symbol.dispose]?.();
|
|
249
|
+
this.hwContext = null;
|
|
250
|
+
if (this.pendingPull) {
|
|
251
|
+
this.pendingPull = false;
|
|
252
|
+
this.send({ kind: "ended" });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
handleStart(source, opts) {
|
|
256
|
+
if (this.started) {
|
|
257
|
+
process.stderr.write("decode-worker-child: duplicate start ignored\n");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
this.started = true;
|
|
261
|
+
this.sessionFormat = opts.format ?? "rgb";
|
|
262
|
+
this.minIntervalMs = opts.fps && opts.fps > 0 ? 1e3 / opts.fps : 0;
|
|
263
|
+
this.runDecodeLoop(source.restreamUrl).catch((err) => {
|
|
264
|
+
this.sendError(`decode-worker-child: decode loop crashed — ${errMessage(err)}`);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
handlePull() {
|
|
268
|
+
if (this.ended) {
|
|
269
|
+
this.send({ kind: "ended" });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const reply = this.frames.pull();
|
|
273
|
+
if (reply) {
|
|
274
|
+
this.send(this.toFrameReply(reply));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.pendingPull = true;
|
|
278
|
+
this.frames.markPendingPull();
|
|
279
|
+
}
|
|
280
|
+
handleToBuffer(frameId, opts) {
|
|
281
|
+
const frame = this.frames.toBuffer(frameId);
|
|
282
|
+
if (!frame) {
|
|
283
|
+
this.sendError(`decode-worker-child: toBuffer for unknown or stale frameId ${frameId}`, frameId);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const bytes = this.scaleRoiToBuffer(frame, opts);
|
|
288
|
+
this.send({
|
|
289
|
+
kind: "buffer",
|
|
290
|
+
frameId,
|
|
291
|
+
bytes
|
|
292
|
+
});
|
|
293
|
+
} catch (err) {
|
|
294
|
+
this.sendError(`decode-worker-child: toBuffer failed — ${errMessage(err)}`, frameId);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
handleStop() {
|
|
298
|
+
this.teardown();
|
|
299
|
+
process.exit(0);
|
|
300
|
+
}
|
|
301
|
+
async runDecodeLoop(url) {
|
|
302
|
+
const nav = await getNodeAv();
|
|
303
|
+
const C = await getConstants();
|
|
304
|
+
if (this.stopped) return;
|
|
305
|
+
this.nav = nav;
|
|
306
|
+
this.consts = C;
|
|
307
|
+
nav.Log.setLevel(C.AV_LOG_FATAL);
|
|
308
|
+
await this.ensureHwContext(nav, C);
|
|
309
|
+
while (!this.stopped) {
|
|
310
|
+
try {
|
|
311
|
+
await this.dialAndDecode(nav, C, url);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
if (this.stopped) break;
|
|
314
|
+
process.stderr.write(`decode-worker-child: dial ended — re-dial scheduled: ${errMessage(err)}\n`);
|
|
315
|
+
} finally {
|
|
316
|
+
this.closeInput();
|
|
317
|
+
}
|
|
318
|
+
if (this.stopped) break;
|
|
319
|
+
await this.sleep(REDIAL_MS);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* One dial: open the demuxer, pick the video stream, build a HW/SW decoder
|
|
324
|
+
* that downloads + normalises frames to YUV420P system memory (so the ROI
|
|
325
|
+
* scaler always reads plain software planes regardless of hwaccel), and
|
|
326
|
+
* pump `demuxer.packets → decoder.frames → consumeDecodedFrame` until the
|
|
327
|
+
* stream ends, errors, or teardown aborts it.
|
|
328
|
+
*
|
|
329
|
+
* Copied from `nodeav-decoder-session.ts` `pullDialAndDecode` (lines
|
|
330
|
+
* ~916-996): same demuxer options, same `rescale.pixelFormat: YUV420P`
|
|
331
|
+
* download-always contract (keeping decoded frames on the GPU leaked the
|
|
332
|
+
* VAAPI surface pool on every re-dial — see that file's comment), same
|
|
333
|
+
* `exitOnError: false` bad-frame tolerance.
|
|
334
|
+
*/
|
|
335
|
+
async dialAndDecode(nav, C, url) {
|
|
336
|
+
this.abortController = new AbortController();
|
|
337
|
+
const demuxer = await nav.Demuxer.open(url, {
|
|
338
|
+
options: buildDemuxerOptions(),
|
|
339
|
+
signal: this.abortController.signal
|
|
340
|
+
});
|
|
341
|
+
if (this.stopped) {
|
|
342
|
+
demuxer[Symbol.dispose]?.();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
this.demuxer = demuxer;
|
|
346
|
+
const videoStream = demuxer.video();
|
|
347
|
+
if (!videoStream) throw new Error("decode-worker-child: pull input has no video stream");
|
|
348
|
+
const decoder = await nav.Decoder.create(videoStream, {
|
|
349
|
+
...this.hwContext ? { hardware: this.hwContext } : {},
|
|
350
|
+
rescale: { pixelFormat: C.AV_PIX_FMT_YUV420P },
|
|
351
|
+
exitOnError: false
|
|
352
|
+
});
|
|
353
|
+
if (this.stopped) {
|
|
354
|
+
decoder[Symbol.dispose]?.();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.decoder = decoder;
|
|
358
|
+
for await (const frame of decoder.frames(demuxer.packets(videoStream.index))) {
|
|
359
|
+
if (this.stopped) break;
|
|
360
|
+
if (!frame) continue;
|
|
361
|
+
this.consumeDecodedFrame(frame);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Publish a decoded frame into the latest-wins slot (mirrors Scrypted's
|
|
366
|
+
* `libav.py:32,50-53` single-slot queue) via `FrameSlot`, which handles
|
|
367
|
+
* drop-older, the `minIntervalMs` throttle, and resolving a pending `pull`.
|
|
368
|
+
*/
|
|
369
|
+
consumeDecodedFrame(frame) {
|
|
370
|
+
if (frame.isHwFrame()) {
|
|
371
|
+
frame.free();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const reply = this.frames.publish(frame, Date.now(), this.minIntervalMs);
|
|
375
|
+
if (reply) {
|
|
376
|
+
this.pendingPull = false;
|
|
377
|
+
this.send(this.toFrameReply(reply));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
toFrameReply(reply) {
|
|
381
|
+
return {
|
|
382
|
+
kind: "frame",
|
|
383
|
+
frameId: reply.frameId,
|
|
384
|
+
timestamp: reply.timestamp,
|
|
385
|
+
width: reply.width,
|
|
386
|
+
height: reply.height,
|
|
387
|
+
format: this.sessionFormat
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* ROI `toBuffer`: crop (via plane-pointer offset, not a re-decode) + resize
|
|
392
|
+
* + format-convert a retained decoded frame with `SoftwareScaleContext`.
|
|
393
|
+
* Writes straight into a tightly-packed destination buffer via the
|
|
394
|
+
* low-level `scaleSync` (no `dstFrame` + row-strip step needed — the same
|
|
395
|
+
* zero-copy destination technique as `scaleIntoRingSlot` in
|
|
396
|
+
* `nodeav-decoder-session.ts`, adapted to also OFFSET the source planes so
|
|
397
|
+
* a single scaler call handles the crop.
|
|
398
|
+
*/
|
|
399
|
+
scaleRoiToBuffer(frame, opts) {
|
|
400
|
+
const nav = this.nav;
|
|
401
|
+
const C = this.consts;
|
|
402
|
+
if (!nav || !C) throw new Error("node-av not initialized");
|
|
403
|
+
const region = resolveCropRegion(frame.width, frame.height, opts?.crop);
|
|
404
|
+
const target = resolveResizeTarget(region.width, region.height, opts?.resize);
|
|
405
|
+
const format = opts?.format ?? this.sessionFormat;
|
|
406
|
+
const dstFmt = format === "gray" ? C.AV_PIX_FMT_GRAY8 : C.AV_PIX_FMT_RGB24;
|
|
407
|
+
const channels = format === "gray" ? 1 : 3;
|
|
408
|
+
const planes = frame.data;
|
|
409
|
+
const strides = frame.linesize;
|
|
410
|
+
const yPlane = planes?.[0];
|
|
411
|
+
const uPlane = planes?.[1];
|
|
412
|
+
const vPlane = planes?.[2];
|
|
413
|
+
if (!yPlane || !uPlane || !vPlane) throw new Error("decode-worker-child: decoded frame missing planar YUV420P data");
|
|
414
|
+
const yStride = strides[0] ?? 0;
|
|
415
|
+
const cStride = strides[1] ?? 0;
|
|
416
|
+
const ySlice = yPlane.subarray(region.top * yStride + region.left);
|
|
417
|
+
const uSlice = uPlane.subarray(region.top / 2 * cStride + region.left / 2);
|
|
418
|
+
const vSlice = vPlane.subarray(region.top / 2 * cStride + region.left / 2);
|
|
419
|
+
const scaler = new nav.SoftwareScaleContext();
|
|
420
|
+
try {
|
|
421
|
+
scaler.getContext(region.width, region.height, C.AV_PIX_FMT_YUV420P, target.width, target.height, dstFmt, C.SWS_FAST_BILINEAR);
|
|
422
|
+
const initRet = scaler.initContext();
|
|
423
|
+
if (initRet < 0) throw new Error(`sws_init_context failed: ${initRet}`);
|
|
424
|
+
const dstStride = target.width * channels;
|
|
425
|
+
const dstBuffer = Buffer.allocUnsafe(dstStride * target.height);
|
|
426
|
+
const scaledHeight = scaler.scaleSync([
|
|
427
|
+
ySlice,
|
|
428
|
+
uSlice,
|
|
429
|
+
vSlice
|
|
430
|
+
], [
|
|
431
|
+
yStride,
|
|
432
|
+
cStride,
|
|
433
|
+
cStride
|
|
434
|
+
], 0, region.height, [dstBuffer], [dstStride]);
|
|
435
|
+
if (scaledHeight < 0) throw new Error(`sws_scale failed: ${scaledHeight}`);
|
|
436
|
+
if (scaledHeight !== target.height) throw new Error(`scaler produced ${scaledHeight} of ${target.height} rows`);
|
|
437
|
+
return dstBuffer;
|
|
438
|
+
} finally {
|
|
439
|
+
scaler[Symbol.dispose]?.();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Resolve the HW context ONCE (reused across every re-dial, freed in
|
|
444
|
+
* `teardown`). Tries the platform's ordered candidate list; the first
|
|
445
|
+
* `HardwareContext.create` that succeeds wins; every failure falls through
|
|
446
|
+
* to software decode (`hardware` omitted from `Decoder.create`).
|
|
447
|
+
*/
|
|
448
|
+
async ensureHwContext(nav, C) {
|
|
449
|
+
for (const backend of candidateBackends()) {
|
|
450
|
+
const deviceType = backendToHwDeviceConst(backend, C);
|
|
451
|
+
if (deviceType === null) continue;
|
|
452
|
+
const hw = nav.HardwareContext.create(deviceType);
|
|
453
|
+
if (!hw) {
|
|
454
|
+
process.stderr.write(`decode-worker-child: hwaccel candidate '${backend}' failed — trying next\n`);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
this.hwContext = hw;
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
process.stderr.write("decode-worker-child: no hwaccel backend available — using software decode\n");
|
|
461
|
+
}
|
|
462
|
+
/** Interruptible re-dial backoff — `teardown` clears the pending timer. */
|
|
463
|
+
sleep(ms) {
|
|
464
|
+
return new Promise((resolve) => {
|
|
465
|
+
this.redialTimer = setTimeout(() => {
|
|
466
|
+
this.redialTimer = null;
|
|
467
|
+
resolve();
|
|
468
|
+
}, ms);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
/** Dispose the CURRENT dial's demuxer + decoder (the HW context is reused across dials). */
|
|
472
|
+
closeInput() {
|
|
473
|
+
this.decoder?.[Symbol.dispose]?.();
|
|
474
|
+
this.decoder = null;
|
|
475
|
+
this.demuxer?.[Symbol.dispose]?.();
|
|
476
|
+
this.demuxer = null;
|
|
477
|
+
}
|
|
478
|
+
send(reply) {
|
|
479
|
+
process.send?.(reply);
|
|
480
|
+
}
|
|
481
|
+
sendError(message, frameId) {
|
|
482
|
+
process.stderr.write(`${message}\n`);
|
|
483
|
+
this.send(frameId === void 0 ? {
|
|
484
|
+
kind: "error",
|
|
485
|
+
message
|
|
486
|
+
} : {
|
|
487
|
+
kind: "error",
|
|
488
|
+
message,
|
|
489
|
+
frameId
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
var worker = new DecodeWorkerChild();
|
|
494
|
+
process.on("message", (message) => {
|
|
495
|
+
worker.handleMessage(message);
|
|
496
|
+
});
|
|
497
|
+
process.on("disconnect", () => {
|
|
498
|
+
worker.teardown();
|
|
499
|
+
process.exit(0);
|
|
500
|
+
});
|
|
501
|
+
process.on("uncaughtException", (err) => {
|
|
502
|
+
process.stderr.write(`decode-worker-child: uncaught exception — ${errMessage(err)}\n`);
|
|
503
|
+
worker.teardown();
|
|
504
|
+
process.exit(1);
|
|
505
|
+
});
|
|
506
|
+
process.on("unhandledRejection", (reason) => {
|
|
507
|
+
process.stderr.write(`decode-worker-child: unhandled rejection — ${errMessage(reason)}\n`);
|
|
508
|
+
});
|
|
509
|
+
//#endregion
|
|
510
|
+
export { DecodeWorkerChild };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c, u as l } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-
|
|
1
|
+
import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c, u as l } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-dGka8yZY.mjs";
|
|
2
2
|
import { a as u, i as d, n as f, o as p, r as m, t as h } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs";
|
|
3
3
|
import { n as g, r as _, t as v } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs";
|
|
4
4
|
import { n as y, t as b } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs";
|
|
5
|
-
import { t as x } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-
|
|
5
|
+
import { t as x } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DqJvWBKS.mjs";
|
|
6
6
|
//#region ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
|
|
7
7
|
var S = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), C = (e) => e.replace(/^([A-Z])|[\s-_]+(\w)/g, (e, t, n) => n ? n.toUpperCase() : t.toLowerCase()), w = (e) => {
|
|
8
8
|
let t = C(e);
|
|
@@ -918,42 +918,42 @@ function le({ profile: e }) {
|
|
|
918
918
|
] })
|
|
919
919
|
] });
|
|
920
920
|
}
|
|
921
|
-
function ue(
|
|
922
|
-
let
|
|
923
|
-
if (a(
|
|
924
|
-
let i =
|
|
921
|
+
function ue(t) {
|
|
922
|
+
let n = t.config ?? {}, r = e(), [i, a] = p(() => n.initialValue), o = m((e) => {
|
|
923
|
+
if (a(e), t.deviceId === void 0) return;
|
|
924
|
+
let i = n.writerCapName, o = n.writerAddonId, s = n.fieldKey;
|
|
925
925
|
if (i === void 0 || o === void 0 || s === void 0) {
|
|
926
926
|
console.warn(`[ffmpeg-params] widgetConfig is missing writerCapName/writerAddonId/fieldKey — field ${s ?? "(unknown)"} cannot be saved. Fix the contribution builder.`);
|
|
927
927
|
return;
|
|
928
928
|
}
|
|
929
|
-
|
|
930
|
-
deviceId:
|
|
929
|
+
r.mutate({
|
|
930
|
+
deviceId: t.deviceId,
|
|
931
931
|
writerCapName: i,
|
|
932
932
|
writerAddonId: o,
|
|
933
933
|
key: s,
|
|
934
|
-
value:
|
|
934
|
+
value: e ?? null
|
|
935
935
|
});
|
|
936
936
|
}, [
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
937
|
+
t.deviceId,
|
|
938
|
+
n.fieldKey,
|
|
939
|
+
n.writerCapName,
|
|
940
|
+
n.writerAddonId,
|
|
941
|
+
r
|
|
942
942
|
]);
|
|
943
|
-
if (
|
|
943
|
+
if (t.deviceId === void 0) return /* @__PURE__ */ g("div", {
|
|
944
944
|
className: "rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning",
|
|
945
945
|
children: "ffmpeg-params: no deviceId in widget context — this widget only renders inside a device tab."
|
|
946
946
|
});
|
|
947
|
-
let s =
|
|
947
|
+
let s = n.fieldKey;
|
|
948
948
|
return s === void 0 || s.length === 0 ? /* @__PURE__ */ g("div", {
|
|
949
949
|
className: "rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning",
|
|
950
950
|
children: "ffmpeg-params: widgetConfig.fieldKey is required — fix the contribution builder."
|
|
951
951
|
}) : /* @__PURE__ */ g(ce, {
|
|
952
952
|
config: {
|
|
953
|
-
...
|
|
953
|
+
...n,
|
|
954
954
|
fieldKey: s
|
|
955
955
|
},
|
|
956
|
-
value:
|
|
956
|
+
value: i,
|
|
957
957
|
onSave: o
|
|
958
958
|
});
|
|
959
959
|
}
|
|
@@ -1081,14 +1081,14 @@ function _e(e) {
|
|
|
1081
1081
|
hideEmpty: t.hideEmpty
|
|
1082
1082
|
});
|
|
1083
1083
|
}
|
|
1084
|
-
function ve({ deviceId: e, title: t = "Stream Brokers", variant:
|
|
1085
|
-
|
|
1084
|
+
function ve({ deviceId: e, title: t = "Stream Brokers", variant: r = "full", hideEmpty: o = !1 }) {
|
|
1085
|
+
s(["streamBroker", "listAllProfileSlots"], [
|
|
1086
1086
|
"device.streams-registered",
|
|
1087
1087
|
"device.streams-unregistered",
|
|
1088
1088
|
"device.registered",
|
|
1089
1089
|
"device.unregistered"
|
|
1090
1090
|
]);
|
|
1091
|
-
let { data:
|
|
1091
|
+
let { data: c } = i(void 0, { staleTime: 3e4 }), l = (c ?? []).filter((t) => t.deviceId === e && t.sourceCamStreamId !== null).slice().toSorted((e, t) => W.indexOf(e.profile) - W.indexOf(t.profile)), u = a(n().trpcClient, e), { data: d } = b({
|
|
1092
1092
|
queryKey: [
|
|
1093
1093
|
"device",
|
|
1094
1094
|
e,
|
|
@@ -1100,7 +1100,7 @@ function ve({ deviceId: e, title: t = "Stream Brokers", variant: i = "full", hid
|
|
|
1100
1100
|
}), f = /* @__PURE__ */ new Map();
|
|
1101
1101
|
for (let e of d ?? []) f.set(e.camStreamId, e);
|
|
1102
1102
|
let p = new Set(l.map((e) => e.sourceCamStreamId).filter((e) => e !== null)), m = (d ?? []).filter((e) => !p.has(e.camStreamId)), h = l.length > 0, v = m.length > 0;
|
|
1103
|
-
return
|
|
1103
|
+
return o && !h && !v ? null : r === "compact" ? h ? /* @__PURE__ */ g("div", {
|
|
1104
1104
|
className: "divide-y divide-border/30",
|
|
1105
1105
|
children: l.map((e) => /* @__PURE__ */ g(q, {
|
|
1106
1106
|
slot: e,
|
|
@@ -1170,8 +1170,8 @@ function ye({ deviceId: e, camStreamIds: t, camStreams: n }) {
|
|
|
1170
1170
|
})]
|
|
1171
1171
|
});
|
|
1172
1172
|
}
|
|
1173
|
-
function be({ deviceId:
|
|
1174
|
-
let i = x(
|
|
1173
|
+
function be({ deviceId: e, camStreamId: n, camStreams: r }) {
|
|
1174
|
+
let i = x(e, n), a = t("stream-broker.metrics-snapshot", (e) => e.brokerId === i)?.stats, o = r.get(n), s = a?.status === "streaming";
|
|
1175
1175
|
return /* @__PURE__ */ _("div", {
|
|
1176
1176
|
className: "px-4 py-2",
|
|
1177
1177
|
children: [/* @__PURE__ */ g("div", {
|
|
@@ -1266,29 +1266,29 @@ function K(e, t) {
|
|
|
1266
1266
|
if (t !== void 0) return t;
|
|
1267
1267
|
}
|
|
1268
1268
|
}
|
|
1269
|
-
function q({ slot:
|
|
1270
|
-
let
|
|
1271
|
-
|
|
1272
|
-
let { data: d } =
|
|
1269
|
+
function q({ slot: e, camStreams: n, compact: i }) {
|
|
1270
|
+
let a = y(), u = t("stream-broker.metrics-snapshot", (t) => t.brokerId === e.brokerId)?.stats;
|
|
1271
|
+
s(["streamBroker", "listClients"], ["stream-broker.metrics-snapshot"]);
|
|
1272
|
+
let { data: d } = o({ brokerId: e.brokerId }, {
|
|
1273
1273
|
enabled: u?.status === "streaming",
|
|
1274
1274
|
staleTime: 2e3
|
|
1275
|
-
}), { data: f } =
|
|
1276
|
-
|
|
1277
|
-
} }), S = (
|
|
1275
|
+
}), { data: f } = l({ deviceId: e.deviceId }, { staleTime: 15e3 }), p = e.sourceCamStreamId ?? e.profile, h = !!(K(f, `preBufferEnabled:${p}`) ?? !0), b = K(f, `preBufferSec:${p}`), x = r({ onSuccess: () => {
|
|
1276
|
+
a.invalidateQueries({ queryKey: [["streamBroker", "getDeviceSettingsContribution"]] });
|
|
1277
|
+
} }), S = (t) => {
|
|
1278
1278
|
x.mutate({
|
|
1279
|
-
deviceId:
|
|
1280
|
-
patch: { [`preBufferEnabled:${p}`]:
|
|
1279
|
+
deviceId: e.deviceId,
|
|
1280
|
+
patch: { [`preBufferEnabled:${p}`]: t }
|
|
1281
1281
|
});
|
|
1282
|
-
}, C =
|
|
1283
|
-
|
|
1284
|
-
} }), w = m((
|
|
1282
|
+
}, C = c({ onSuccess: () => {
|
|
1283
|
+
a.invalidateQueries({ queryKey: [["streamBroker", "listClients"]] });
|
|
1284
|
+
} }), w = m((t) => {
|
|
1285
1285
|
C.mutate({
|
|
1286
|
-
brokerId:
|
|
1287
|
-
...
|
|
1286
|
+
brokerId: e.brokerId,
|
|
1287
|
+
...t
|
|
1288
1288
|
});
|
|
1289
|
-
}, [C,
|
|
1289
|
+
}, [C, e.brokerId]), T = (e.sourceCamStreamId ? n.get(e.sourceCamStreamId) : void 0)?.resolution ?? e.resolution, E = u?.status === "streaming", D = T ? `${U[e.profile]} (${T.width}×${T.height})` : U[e.profile], O = u ? u.encodedSubscribers + u.decodedSubscribers + (u.rtspClients ?? 0) + (u.pipeClients ?? 0) : 0;
|
|
1290
1290
|
return /* @__PURE__ */ _("div", {
|
|
1291
|
-
className: `@container ${
|
|
1291
|
+
className: `@container ${i ? "px-3 py-1.5" : "px-4 py-2"}`,
|
|
1292
1292
|
style: { containerType: "inline-size" },
|
|
1293
1293
|
children: [
|
|
1294
1294
|
/* @__PURE__ */ _("div", {
|
|
@@ -1296,7 +1296,7 @@ function q({ slot: r, camStreams: i, compact: o }) {
|
|
|
1296
1296
|
children: [/* @__PURE__ */ _("div", {
|
|
1297
1297
|
className: "flex items-center gap-1.5",
|
|
1298
1298
|
children: [
|
|
1299
|
-
/* @__PURE__ */ g("span", { className: `h-1.5 w-1.5 rounded-full ${E ?
|
|
1299
|
+
/* @__PURE__ */ g("span", { className: `h-1.5 w-1.5 rounded-full ${E ? e.profile === "high" ? "bg-success" : e.profile === "mid" ? "bg-warning" : "bg-foreground-subtle" : "bg-foreground-subtle/40"}` }),
|
|
1300
1300
|
/* @__PURE__ */ g("span", {
|
|
1301
1301
|
className: "text-[11px] font-medium text-foreground",
|
|
1302
1302
|
children: D
|
|
@@ -1305,9 +1305,9 @@ function q({ slot: r, camStreams: i, compact: o }) {
|
|
|
1305
1305
|
className: `text-[10px] ${E ? "text-success" : "text-foreground-subtle"}`,
|
|
1306
1306
|
children: u?.status ?? "loading"
|
|
1307
1307
|
}),
|
|
1308
|
-
|
|
1308
|
+
e.sourceCamStreamId && /* @__PURE__ */ _("span", {
|
|
1309
1309
|
className: "text-[10px] text-foreground-subtle",
|
|
1310
|
-
children: ["← ",
|
|
1310
|
+
children: ["← ", e.sourceCamStreamId]
|
|
1311
1311
|
})
|
|
1312
1312
|
]
|
|
1313
1313
|
}), /* @__PURE__ */ g("span", {
|
|
@@ -1315,7 +1315,7 @@ function q({ slot: r, camStreams: i, compact: o }) {
|
|
|
1315
1315
|
children: u && u.uptimeMs > 0 ? pe(u.uptimeMs) : "—"
|
|
1316
1316
|
})]
|
|
1317
1317
|
}),
|
|
1318
|
-
u && E ?
|
|
1318
|
+
u && E ? i ? /* @__PURE__ */ _("div", {
|
|
1319
1319
|
className: "flex flex-wrap gap-x-3 gap-y-0.5 text-[10px]",
|
|
1320
1320
|
children: [
|
|
1321
1321
|
/* @__PURE__ */ _("span", {
|