@camstack/addon-pipeline 1.1.55 → 1.1.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/detection-pipeline/index.js +92 -88
  4. package/dist/detection-pipeline/index.mjs +91 -87
  5. package/dist/{dist-DI57FC8K.js → dist-CK8Ur-OS.js} +1045 -43
  6. package/dist/{dist-RWGGPwVx.mjs → dist-CL531uVA.mjs} +1045 -43
  7. package/dist/motion-wasm/index.js +9 -41
  8. package/dist/motion-wasm/index.mjs +9 -41
  9. package/dist/pipeline-runner/index.js +338 -664
  10. package/dist/pipeline-runner/index.mjs +327 -655
  11. package/dist/recorder/index.js +2 -2
  12. package/dist/recorder/index.mjs +2 -2
  13. package/dist/remote-restream-BeHi78PZ.mjs +25 -0
  14. package/dist/remote-restream-CO36Sr30.js +36 -0
  15. package/dist/session-decode/decode-worker-child.js +5 -3
  16. package/dist/session-decode/decode-worker-child.mjs +5 -3
  17. package/dist/{step-definitions-CP9kVSml.mjs → step-definitions-1d3_vQ6S.mjs} +45 -3
  18. package/dist/{step-definitions-CNBFKjZe.js → step-definitions-DHAxruAQ.js} +45 -3
  19. package/dist/stream-broker/_stub.js +351 -291
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-mDCPzmT3.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cm_B-hQT.mjs} +2 -2
  21. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Ba7Eyd47.mjs +26 -0
  22. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dn_pxzP-.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-TWCUWhCm.mjs} +2 -2
  23. package/dist/stream-broker/{hostInit-GM_CI22k.mjs → hostInit-DEaqJpGA.mjs} +2 -2
  24. package/dist/stream-broker/index.js +935 -38
  25. package/dist/stream-broker/index.mjs +932 -35
  26. package/dist/stream-broker/remoteEntry.js +1 -1
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BChW0ntM.js → MaskShapeCanvas-DI4BY7W2-CZUzu3MF.js} +1 -1
  28. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DDzqEbSe.js → MotionZonesSettings-NcxxQN8r-CmZbzLQi.js} +1 -1
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-B879GXaf.js → PrivacyMaskSettings-APgPLF7p-BOKDrH0N.js} +1 -1
  30. package/embed-dist/assets/index-Bmpl9O1K.css +2 -0
  31. package/embed-dist/assets/index-DOsK8CZb.js +81 -0
  32. package/embed-dist/index.html +2 -2
  33. package/package.json +7 -5
  34. package/python/inference_pool.py +183 -29
  35. package/python/postprocessors/__init__.py +2 -0
  36. package/python/postprocessors/ssd.py +128 -0
  37. package/python/requirements-edgetpu.txt +23 -0
  38. package/python/test_inference_pool_layout.py +16 -1
  39. package/python/test_inference_pool_preprocess.py +65 -0
  40. package/dist/remote-source-plane-CHgvzzA6.js +0 -1030
  41. package/dist/remote-source-plane-DU0aRSPv.mjs +0 -1001
  42. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B3gTdHEh.mjs +0 -26
  43. package/embed-dist/assets/index-CC06JBcl.css +0 -2
  44. package/embed-dist/assets/index-CNjQ5rAE.js +0 -81
  45. package/dist/{model-download-service-C-IHWnXx-BPy6aoAx.mjs → model-download-service-C-IHWnXx-3Mmeob3l.mjs} +1 -1
  46. package/dist/{model-download-service-C-IHWnXx-BLXzxpRB.js → model-download-service-C-IHWnXx-D326YNnt.js} +1 -1
@@ -1,1001 +0,0 @@
1
- import { D as nodePin, I as errMsg, d as RingBuffer } from "./dist-RWGGPwVx.mjs";
2
- import { randomUUID } from "node:crypto";
3
- //#region src/stream-broker/stream-broker/decoder-session-proxy.ts
4
- /**
5
- * Long-poll window handed to `pullFrames`/`pullHandles`: an empty drain blocks
6
- * decoder-side until a frame commits or this elapses, replacing the old ~1kHz
7
- * `setTimeout(…,1)` busy-spin with a single await-notify round-trip. Kept
8
- * modest so a hung UDS/Moleculer transport can't pin the call; the decoder
9
- * cancels the waiter on `destroySession` so teardown never waits it out.
10
- */
11
- var POLL_WAIT_MS = 50;
12
- var DecoderSessionProxy = class {
13
- api;
14
- sessionId;
15
- polling = false;
16
- constructor(api, sessionId) {
17
- this.api = api;
18
- this.sessionId = sessionId;
19
- }
20
- async pushPacket(packet) {
21
- await this.api.pushPacket({
22
- sessionId: this.sessionId,
23
- packet
24
- });
25
- }
26
- async openStream(url) {
27
- await this.api.openStream({
28
- sessionId: this.sessionId,
29
- url
30
- });
31
- }
32
- async startPolling(onFrame) {
33
- this.polling = true;
34
- while (this.polling) {
35
- const frames = await this.api.pullFrames({
36
- sessionId: this.sessionId,
37
- maxCount: 4,
38
- waitMs: POLL_WAIT_MS
39
- });
40
- for (const frame of frames) onFrame(frame);
41
- }
42
- }
43
- /**
44
- * Poll a `frameSink: 'shm'` session for `FrameHandle`s (Phase 5 / D9).
45
- * Mirrors `startPolling` but drains `pullHandles` — the decoder has
46
- * already written the pixels into a shared-memory ring, so what crosses
47
- * the cap boundary is the tiny serialisable handle. Runs until
48
- * `stopPolling`, `destroy`, or the session is destroyed externally.
49
- *
50
- * When the decoder reports the session is gone (`unknown sessionId` /
51
- * `Service … not found`) the loop exits gracefully — those errors are
52
- * expected during decoder restart/shutdown and must not be re-thrown to
53
- * the caller's `.catch()` handler, which would otherwise spin forever.
54
- */
55
- async startHandlePolling(onHandle) {
56
- this.polling = true;
57
- while (this.polling) try {
58
- const handles = await this.api.pullHandles({
59
- sessionId: this.sessionId,
60
- maxCount: 4,
61
- waitMs: POLL_WAIT_MS
62
- });
63
- for (const handle of handles) onHandle(handle);
64
- } catch (err) {
65
- this.polling = false;
66
- const msg = err instanceof Error ? err.message : String(err);
67
- if (!msg.includes("unknown sessionId") && !msg.includes("Service") && !msg.includes("not found")) throw err;
68
- return;
69
- }
70
- }
71
- stopPolling() {
72
- this.polling = false;
73
- }
74
- async updateConfig(config) {
75
- await this.api.updateConfig({
76
- sessionId: this.sessionId,
77
- config
78
- });
79
- }
80
- async getStats() {
81
- return this.api.getStats({ sessionId: this.sessionId });
82
- }
83
- async destroy() {
84
- this.stopPolling();
85
- await this.api.destroySession({ sessionId: this.sessionId });
86
- }
87
- };
88
- //#endregion
89
- //#region src/stream-broker/stream-broker/frame-handle-plane.ts
90
- /**
91
- * `FrameHandlePlane` — the broker's shared-memory frame-handle surface
92
- * (Phase 5 / D9).
93
- *
94
- * This is the handle-based replacement for `StreamBroker.onDecodedFrame`'s
95
- * live-object callback path. Instead of fanning pixel `DecodedFrame`s out to
96
- * in-process callbacks (which cannot cross a process boundary), the broker
97
- * publishes zero-pixel, serialisable `FrameHandle`s a consumer drains over
98
- * tRPC and reads back from a shared-memory ring with a `FrameRingReader`.
99
- *
100
- * ## Ring-per-format model
101
- *
102
- * A `frameSink: 'shm'` decoder session produces exactly ONE pixel format
103
- * (the decoder's scaler is configured for it). The broker therefore keeps
104
- * one decoder session — hence one shm ring — per `(brokerId, format)` that a
105
- * consumer actually subscribes to:
106
- *
107
- * - motion subscribes `gray` → one `gray` decoder session + ring
108
- * - detection subscribes `rgb` → one `rgb` decoder session + ring
109
- *
110
- * Only the formats consumers ask for are materialised. There is NO
111
- * broker-side `sharp` conversion any more — each consumer reads the exact
112
- * format it requested. A session is reference-counted by its subscriber set
113
- * and torn down when the last subscriber of that format leaves.
114
- *
115
- * ## fps throttling
116
- *
117
- * Throttling is implicit: the ring is latest-wins, so a slow consumer that
118
- * polls `pullFrameHandles` infrequently simply receives the newest handles
119
- * and the intervening frames are dropped at the ring (a recycled slot the
120
- * consumer never read). The optional `maxFps` from `subscribeFrames` is NOT
121
- * enforced here — it is echoed back to the consumer as a reader-side polling
122
- * cadence hint.
123
- *
124
- * ## Packet feed
125
- *
126
- * The plane's decoder sessions are push-mode by default: the broker forwards
127
- * every video `EncodedPacket` to `pushPacket`. For a pull-mode decoder the
128
- * broker supplies a `pullModeUrl` and the plane calls `openStream` once the
129
- * session exists.
130
- */
131
- /**
132
- * Collapse a hierarchical Moleculer nodeID (`hub/child-abc`) to its owning
133
- * node (`hub`). Frame-plane co-location only cares about the physical node, not
134
- * which forked child within it hosts the session.
135
- */
136
- function collapseNodeId(nodeId) {
137
- const slash = nodeId.indexOf("/");
138
- return slash >= 0 ? nodeId.slice(0, slash) : nodeId;
139
- }
140
- /**
141
- * Decide whether a decoder session created on `decoderNodeId` is co-located
142
- * with the broker whose local node is `brokerNodeId`.
143
- *
144
- * The broker's shm frame ring is NODE-LOCAL: a decoder session on any other
145
- * node can never deliver frame handles to this broker's consumers, so a
146
- * mis-placed session must be rejected. Both ids are collapsed (`hub/child`→
147
- * `hub`) before comparison.
148
- *
149
- * A `null` `brokerNodeId` means the broker's own node is unknown — enforcement
150
- * is skipped (legacy behavior, accept any placement).
151
- */
152
- function isDecoderNodeColocated(brokerNodeId, decoderNodeId) {
153
- if (brokerNodeId === null) return true;
154
- return collapseNodeId(brokerNodeId) === collapseNodeId(decoderNodeId);
155
- }
156
- /** Per-subscription handle-queue capacity. A handle is tiny; 64 absorbs a
157
- * burst between two `pullFrameHandles` polls without unbounded growth. */
158
- var HANDLE_QUEUE_CAPACITY = 64;
159
- /** Default reader-side cadence hint when a subscriber omits `maxFps`. */
160
- var DEFAULT_HINT_FPS = 5;
161
- /** Rolling window for the aggregate `decodeFps` rate, ms. Mirrors the
162
- * broker's 2s `inputFps` window so the two stats are comparable. */
163
- var DECODE_FPS_WINDOW_MS = 2e3;
164
- /**
165
- * Keep-warm grace before a decoder session with no subscribers is torn down.
166
- *
167
- * Subscribers flap on ~30s cadences (occupancy recheck bursts, detection
168
- * motion-gating watch↔active, onboard gray gating). Destroying the
169
- * ffmpeg/VAAPI decode session the instant the last subscriber leaves spawns +
170
- * destroys a full session every cycle — wasted setup/teardown. Chosen >30s so
171
- * consecutive bursts and watch↔active flips reuse the warm session, yet a
172
- * genuinely idle session still frees its ffmpeg within ~a minute.
173
- */
174
- var SESSION_LINGER_MS = 45e3;
175
- /**
176
- * Short keep-warm grace for a PURE-OCCUPANCY session (every remaining
177
- * subscriber is a periodic on-motion occupancy recheck, tag `'occupancy'`).
178
- *
179
- * Occupancy rechecks subscribe to decoded frames for ~1s every ~30s. The
180
- * normal `SESSION_LINGER_MS` (45s) EXCEEDS that ~30s interval, so the linger
181
- * timer is perpetually cancelled by the next recheck before it can fire — the
182
- * decoder never tears down and decodes continuously at source-rate to serve a
183
- * 1s/30s consumer (~30x wasted decode; multiplied across cameras this is the
184
- * iGPU baseline that saturated the decoder). A pure-occupancy session instead
185
- * tears down between polls: short enough to fire well before the next ~30s
186
- * recheck, long enough to absorb the ~1s poll plus minor jitter. Re-dial is
187
- * cheap on GPU-scale decode and the restream burst re-primes the live edge
188
- * fast, so on-demand re-arm on the next recheck is fine.
189
- */
190
- var OCCUPANCY_LINGER_MS = 5e3;
191
- /** Subscription tag emitted by the periodic on-motion occupancy recheck. A
192
- * session whose subscribers have only ever carried this tag gets the short
193
- * `OCCUPANCY_LINGER_MS` teardown; any other subscriber biases to the long
194
- * `SESSION_LINGER_MS` warm linger (interactive viewers, detection, motion). */
195
- var OCCUPANCY_TAG = "occupancy";
196
- /**
197
- * Default retry cooldown after a decoder session lands on a NON-LOCAL node
198
- * (see `FrameHandlePlaneOptions.sessionRetryCooldownMs`). Long enough to
199
- * turn a per-packet storm into a slow probe; short enough that the plane
200
- * recovers within seconds once the local decoder respawns.
201
- */
202
- var SESSION_RETRY_COOLDOWN_MS = 1e4;
203
- /**
204
- * Owns the broker's shared-memory frame-handle subscriptions and the
205
- * per-format `frameSink: 'shm'` decoder sessions that feed them.
206
- */
207
- var FrameHandlePlane = class {
208
- decoderApi;
209
- logger;
210
- resolveStreamInfo;
211
- localNodeId;
212
- sessionRetryCooldownMs;
213
- getSeedPackets;
214
- isDebugEnabled;
215
- subscriptions = /* @__PURE__ */ new Map();
216
- sessions = /* @__PURE__ */ new Map();
217
- /** Per-format retry gate set when a session lands on a non-local node:
218
- * `ensureSession` skips creation until the epoch-ms deadline passes. */
219
- sessionRetryCooldownUntil = /* @__PURE__ */ new Map();
220
- /** In-flight `ensureSession` creation per format. Coalesces concurrent
221
- * same-format callers onto ONE decoder session — without it two callers
222
- * both pass the `sessions.get` check, both `createSession` (two ffmpeg),
223
- * and the second registration orphans the first with no `destroySession`. */
224
- sessionCreating = /* @__PURE__ */ new Map();
225
- disposed = false;
226
- /** Re-entrancy guard for `armPendingSessions` — collapses the per-packet
227
- * `pushPacket` calls into a single in-flight deferred-arm pass. */
228
- armInFlight = false;
229
- /** DIAG(592): P-frames skipped on an un-bootstrapped session before its
230
- * first live keyframe. Reset when a keyframe bootstraps the session. */
231
- unbootstrappedSkips = 0;
232
- /** Aggregate decoded-frame rate state — frames fanned out in the current
233
- * rolling window, the window's start epoch, and the last computed fps. */
234
- framesInWindow = 0;
235
- windowStartMs = Date.now();
236
- decodedFps = 0;
237
- constructor(options) {
238
- this.decoderApi = options.decoderApi;
239
- this.logger = options.logger;
240
- this.resolveStreamInfo = options.resolveStreamInfo;
241
- this.localNodeId = options.localNodeId;
242
- this.sessionRetryCooldownMs = options.sessionRetryCooldownMs ?? SESSION_RETRY_COOLDOWN_MS;
243
- this.getSeedPackets = options.getSeedPackets;
244
- this.isDebugEnabled = options.isDebugEnabled;
245
- }
246
- /** Number of active handle subscriptions — surfaced for diagnostics. */
247
- get subscriberCount() {
248
- return this.subscriptions.size;
249
- }
250
- /**
251
- * Moleculer nodeIDs of the decoder providers currently hosting this plane's
252
- * `frameSink: 'shm'` sessions. Used by the broker-manager's per-agent
253
- * hwaccel-change handler to decide which brokers need a decoder rotation.
254
- */
255
- decoderNodeIds() {
256
- const ids = [];
257
- for (const session of this.sessions.values()) if (session.nodeId) ids.push(session.nodeId);
258
- return ids;
259
- }
260
- /**
261
- * Rotate every shm decoder session hosted on `agentNodeId` — destroy and
262
- * rebuild it so the new session re-pulls the latest per-agent decoder
263
- * preferences (hwaccel backend, …). Subscriptions are preserved: the same
264
- * `subscriptionId`s read from the rebuilt session's fresh ring. A no-op for
265
- * sessions on other nodes. Returns the number of sessions rotated.
266
- */
267
- async rotate(agentNodeId, reason) {
268
- if (this.disposed) return 0;
269
- let rotated = 0;
270
- for (const session of this.sessions.values()) {
271
- if ((session.nodeId && session.nodeId.includes("/") ? session.nodeId.split("/")[0] : session.nodeId) !== agentNodeId) continue;
272
- this.logger?.info("frame-handle plane: rotating shm decoder session", { meta: {
273
- format: session.format,
274
- reason,
275
- nodeId: session.nodeId
276
- } });
277
- await this.destroySession(session);
278
- let restarted = false;
279
- try {
280
- restarted = await this.startSessionDecoder(session);
281
- } finally {
282
- if (!restarted) this.sessions.delete(session.format);
283
- }
284
- if (restarted) rotated += 1;
285
- }
286
- return rotated;
287
- }
288
- /**
289
- * Register a frame-handle subscription. Spins up (or reuses) a
290
- * `frameSink: 'shm'` decoder session producing `format`. The returned
291
- * `subscriptionId` is the handle the consumer passes to
292
- * `pullFrameHandles` / `unsubscribe`.
293
- */
294
- async subscribe(input) {
295
- if (this.disposed) throw new Error("FrameHandlePlane: subscribe after dispose");
296
- const subscriptionId = `fh-${randomUUID()}`;
297
- const maxFps = input.maxFps !== void 0 && input.maxFps > 0 ? input.maxFps : DEFAULT_HINT_FPS;
298
- const subscription = {
299
- id: subscriptionId,
300
- format: input.format,
301
- maxFps,
302
- tag: input.tag ?? "unknown",
303
- subscribedAt: Date.now(),
304
- queue: new RingBuffer(HANDLE_QUEUE_CAPACITY),
305
- framesDelivered: 0
306
- };
307
- this.subscriptions.set(subscriptionId, subscription);
308
- await this.ensureSession(input.format, subscriptionId);
309
- this.reconcileSessionFps(input.format);
310
- this.logger?.info("frame-handle subscription added", { meta: {
311
- subscriptionId,
312
- format: input.format,
313
- tag: subscription.tag,
314
- maxFps
315
- } });
316
- return {
317
- subscriptionId,
318
- maxFps
319
- };
320
- }
321
- /**
322
- * The emit `maxFps` a format's decoder session should run at — the MAX of all
323
- * current subscribers' `maxFps` for that format (fallback `DEFAULT_HINT_FPS`
324
- * when none, e.g. mid-teardown). The decoder emits at this rate; each
325
- * subscriber then down-samples to its own `maxFps` on read (latest-wins).
326
- */
327
- computeFormatFps(format) {
328
- let max = 0;
329
- for (const sub of this.subscriptions.values()) if (sub.format === format && sub.maxFps > max) max = sub.maxFps;
330
- return max > 0 ? max : DEFAULT_HINT_FPS;
331
- }
332
- /**
333
- * Recompute a format session's emit `maxFps` after a subscriber joins/leaves
334
- * and push it to the live decoder via `updateConfig` when it changed — so the
335
- * decoder emits only as fast as the busiest reader needs (saving the
336
- * scale+convert+shm-write for frames nobody reads).
337
- */
338
- reconcileSessionFps(format) {
339
- const session = this.sessions.get(format);
340
- if (!session || !session.proxy) return;
341
- const target = this.computeFormatFps(format);
342
- if (target === session.targetFps) return;
343
- session.targetFps = target;
344
- session.proxy.updateConfig({ maxFps: target }).catch((err) => {
345
- this.logger?.warn("frame-handle plane: updateConfig(maxFps) failed", { meta: {
346
- format,
347
- maxFps: target,
348
- error: errMsg(err)
349
- } });
350
- });
351
- }
352
- /**
353
- * Drain up to `maxCount` `FrameHandle`s for a subscription, latest-wins.
354
- * An unknown subscription id returns `[]` (the consumer may have been
355
- * torn down concurrently).
356
- */
357
- pullHandles(subscriptionId, maxCount) {
358
- const subscription = this.subscriptions.get(subscriptionId);
359
- if (!subscription) return [];
360
- const stale = subscription.queue.size - maxCount;
361
- if (stale > 0) subscription.queue.drain(stale);
362
- const handles = subscription.queue.drain(maxCount);
363
- subscription.framesDelivered += handles.length;
364
- return handles;
365
- }
366
- /**
367
- * Release a subscription. When it was the last reader of its format the
368
- * underlying decoder session is destroyed (and its shm segment unlinked
369
- * by the decoder). Returns `true` when a known subscription was released.
370
- */
371
- async unsubscribe(subscriptionId, immediate = false) {
372
- const subscription = this.subscriptions.get(subscriptionId);
373
- if (!subscription) return false;
374
- this.subscriptions.delete(subscriptionId);
375
- const session = this.sessions.get(subscription.format);
376
- if (session) {
377
- session.subscriberIds.delete(subscriptionId);
378
- if (session.subscriberIds.size === 0) if (immediate) {
379
- this.clearLinger(session);
380
- this.sessions.delete(subscription.format);
381
- await this.destroySession(session);
382
- } else this.scheduleLinger(session);
383
- else this.reconcileSessionFps(subscription.format);
384
- }
385
- this.logger?.info("frame-handle subscription removed", { meta: {
386
- subscriptionId,
387
- format: subscription.format
388
- } });
389
- return true;
390
- }
391
- /**
392
- * Arm the keep-warm teardown timer for a session whose last subscriber just
393
- * left. Clears any existing timer first (idempotent). On fire it re-checks
394
- * `subscriberIds.size === 0` — a subscriber may have returned in the interim,
395
- * in which case the timer is a no-op — and only then removes the session and
396
- * destroys the decoder. The timer is `unref`'d so a lingering warm session
397
- * never keeps the process alive.
398
- */
399
- scheduleLinger(session) {
400
- this.clearLinger(session);
401
- const occupancyOnly = !session.hasNonOccupancySubscriberEver;
402
- const lingerMs = occupancyOnly ? OCCUPANCY_LINGER_MS : SESSION_LINGER_MS;
403
- this.logger?.debug(occupancyOnly ? "frame-handle plane: short occupancy linger scheduled" : "frame-handle plane: keep-warm linger scheduled", { meta: {
404
- format: session.format,
405
- lingerMs,
406
- occupancyOnly
407
- } });
408
- const timer = setTimeout(() => {
409
- session.lingerTimer = null;
410
- if (session.subscriberIds.size > 0) return;
411
- this.sessions.delete(session.format);
412
- this.destroySession(session).catch(() => {});
413
- }, lingerMs);
414
- timer.unref?.();
415
- session.lingerTimer = timer;
416
- }
417
- /**
418
- * Latch `session.hasNonOccupancySubscriberEver` when the subscription
419
- * `subscriptionId` carries any tag other than `OCCUPANCY_TAG`. Called at
420
- * every point a subscription is attached to a session so the keep-warm
421
- * decision only fast-tears-down sessions that have been PURELY on-demand
422
- * occupancy rechecks. Unknown ids (mid-teardown) are ignored.
423
- */
424
- noteSessionSubscriberTag(session, subscriptionId) {
425
- const subscription = this.subscriptions.get(subscriptionId);
426
- if (subscription && subscription.tag !== OCCUPANCY_TAG) session.hasNonOccupancySubscriberEver = true;
427
- }
428
- /** Cancel a session's pending keep-warm teardown timer, if any. */
429
- clearLinger(session) {
430
- if (session.lingerTimer) {
431
- clearTimeout(session.lingerTimer);
432
- session.lingerTimer = null;
433
- }
434
- }
435
- /**
436
- * Forward a video `EncodedPacket` to every active shm decoder session.
437
- * Push-mode decoders consume this; a pull-mode decoder ignores it (it
438
- * reads its own pipe via `openStream`). Audio packets are not forwarded.
439
- *
440
- * The packet feed doubles as the plane's stream-ready signal: the broker
441
- * only calls `pushPacket` once it has a source and the first keyframe has
442
- * landed — exactly the point `resolveStreamInfo` starts returning a
443
- * descriptor. Each packet therefore triggers `armPendingSessions`, which
444
- * re-attempts session creation for any deferred subscription so a lone
445
- * first subscriber on a not-yet-started broker self-arms without a
446
- * redundant re-`subscribe`.
447
- */
448
- pushPacket(packet) {
449
- if (this.disposed || packet.type !== "video") return;
450
- this.armPendingSessions(packet).catch((err) => {
451
- this.logger?.warn("frame-handle plane: arm pending sessions error", { meta: { error: errMsg(err) } });
452
- });
453
- this.feedSessions(packet);
454
- }
455
- /** Forward a video `EncodedPacket` to every live PUSH-mode shm decoder session. */
456
- feedSessions(packet) {
457
- for (const session of this.sessions.values()) {
458
- if (!session.proxy) continue;
459
- if (session.pullMode) continue;
460
- if (!session.bootstrapped) {
461
- if (!packet.keyframe) {
462
- this.unbootstrappedSkips = (this.unbootstrappedSkips ?? 0) + 1;
463
- continue;
464
- }
465
- this.logger?.debug("frame-handle plane: session bootstrapped from live keyframe", { meta: {
466
- format: session.format,
467
- pFramesSkippedBefore: this.unbootstrappedSkips ?? 0
468
- } });
469
- this.unbootstrappedSkips = 0;
470
- session.bootstrapped = true;
471
- }
472
- session.proxy.pushPacket(packet).catch((err) => {
473
- const msg = errMsg(err);
474
- this.logger?.warn("frame-handle plane: decoder push error", { meta: {
475
- format: session.format,
476
- error: msg
477
- } });
478
- if (msg.includes("unknown sessionId") || msg.includes("Service") || msg.includes("not found")) {
479
- this.logger?.info("frame-handle plane: invalidating stale proxy", { meta: { format: session.format } });
480
- this.destroySession(session).catch(() => {});
481
- this.sessions.delete(session.format);
482
- }
483
- });
484
- }
485
- }
486
- /**
487
- * Re-attempt session creation for every subscription whose format has no
488
- * live `FormatSession` yet — the subscriptions whose `startSessionDecoder`
489
- * was deferred because the broker had no stream at `subscribe` time. Driven
490
- * off the `pushPacket` feed (event-driven, no polling timer); a no-op once
491
- * every subscribed format already owns a session.
492
- *
493
- * `armInFlight` collapses re-entrant calls: `pushPacket` fires per packet,
494
- * but `ensureSession` is async — without the guard a burst of packets would
495
- * launch overlapping decoder-creation round-trips for the same format.
496
- *
497
- * `triggerPacket` is the packet whose arrival armed the pass. Because
498
- * `ensureSession` is async, the synchronous `feedSessions` in `pushPacket`
499
- * runs before the session lands in `this.sessions`, so the triggering packet
500
- * would be lost to the new session. `startSessionDecoder` normally bootstraps
501
- * it by replaying the broker's keyframe-aligned seed GOP; only when NO seed
502
- * was available (no `getSeedPackets`, or the ring held no keyframe yet) is the
503
- * session left un-bootstrapped — and then this method hands it the triggering
504
- * packet, but ONLY if that packet is a keyframe. A P-frame trigger is left for
505
- * `feedSessions` to skip until the first live keyframe, so a push-mode decoder
506
- * never sees a slice referencing an unseen param set.
507
- */
508
- async armPendingSessions(triggerPacket) {
509
- if (this.disposed || this.armInFlight) return;
510
- const pending = /* @__PURE__ */ new Map();
511
- for (const subscription of this.subscriptions.values()) {
512
- if (this.sessions.has(subscription.format)) continue;
513
- if (!pending.has(subscription.format)) pending.set(subscription.format, subscription.id);
514
- }
515
- if (pending.size === 0) return;
516
- this.armInFlight = true;
517
- try {
518
- for (const [format, subscriptionId] of pending) {
519
- if (this.disposed) return;
520
- await this.ensureSession(format, subscriptionId);
521
- const armed = this.sessions.get(format);
522
- if (armed?.proxy && !armed.bootstrapped && triggerPacket.keyframe) {
523
- armed.bootstrapped = true;
524
- armed.proxy.pushPacket(triggerPacket).catch((err) => {
525
- this.logger?.warn("frame-handle plane: decoder push error", { meta: {
526
- format,
527
- error: errMsg(err)
528
- } });
529
- });
530
- }
531
- }
532
- } finally {
533
- this.armInFlight = false;
534
- }
535
- }
536
- /**
537
- * Aggregate decoded-frame rate across every `frameSink: 'shm'` session in
538
- * this plane, frames/s. Each `fanoutHandle` call is one decoded frame
539
- * delivered into the plane; the rate is recomputed over a rolling
540
- * `DECODE_FPS_WINDOW_MS` window. Returns the last computed value between
541
- * window rolls, and `0` while no session is producing.
542
- */
543
- decodeFps() {
544
- this.rollDecodeFpsWindow();
545
- return this.decodedFps;
546
- }
547
- /**
548
- * Roll the decoded-fps window when it has elapsed: convert frames seen in
549
- * the window into a per-second rate, then reset the counter. Called both
550
- * on every fanout and on every `decodeFps()` read so a stalled session
551
- * (no fanout) decays its rate to `0` rather than reporting a stale value.
552
- */
553
- rollDecodeFpsWindow() {
554
- const now = Date.now();
555
- const windowMs = now - this.windowStartMs;
556
- if (windowMs < DECODE_FPS_WINDOW_MS) return;
557
- this.decodedFps = this.framesInWindow / windowMs * 1e3;
558
- this.framesInWindow = 0;
559
- this.windowStartMs = now;
560
- }
561
- /** Diagnostic snapshot of every active frame-handle subscription. */
562
- listSubscribers() {
563
- return [...this.subscriptions.values()].map((s) => ({
564
- tag: s.tag,
565
- subscribedAt: s.subscribedAt,
566
- maxFps: s.maxFps,
567
- framesDelivered: s.framesDelivered
568
- }));
569
- }
570
- /**
571
- * Force-release every subscription whose tag matches `tag`. Returns the
572
- * number released. Decoder sessions wind down when their last subscriber
573
- * leaves, same as a normal `unsubscribe`.
574
- */
575
- async killByTag(tag) {
576
- const victims = [...this.subscriptions.values()].filter((s) => s.tag === tag).map((s) => s.id);
577
- for (const id of victims) await this.unsubscribe(id, true);
578
- return victims.length;
579
- }
580
- /** Tear down every subscription + decoder session. Idempotent. */
581
- async dispose() {
582
- if (this.disposed) return;
583
- this.disposed = true;
584
- this.subscriptions.clear();
585
- const sessions = [...this.sessions.values()];
586
- this.sessions.clear();
587
- await Promise.all(sessions.map((s) => this.destroySession(s)));
588
- }
589
- /**
590
- * Ensure a `frameSink: 'shm'` decoder session for `format` exists and add
591
- * `subscriptionId` to its reader set. Reuses the session when one already
592
- * runs for that format.
593
- *
594
- * When a session is created fresh, its reader set is seeded with EVERY
595
- * subscription currently reading `format`, not just `subscriptionId` — a
596
- * deferred-arm pass (`armPendingSessions`) may create the session long
597
- * after several subscriptions of that format have registered, and the
598
- * session is reference-counted by `subscriberIds`. Seeding only the one id
599
- * would let an `unsubscribe` of that id tear the session down while its
600
- * sibling subscriptions still read it.
601
- */
602
- async ensureSession(format, subscriptionId) {
603
- const existing = this.sessions.get(format);
604
- if (existing) {
605
- this.clearLinger(existing);
606
- existing.subscriberIds.add(subscriptionId);
607
- this.noteSessionSubscriberTag(existing, subscriptionId);
608
- return;
609
- }
610
- const inflight = this.sessionCreating.get(format);
611
- if (inflight) {
612
- await inflight;
613
- const session = this.sessions.get(format);
614
- if (session) {
615
- session.subscriberIds.add(subscriptionId);
616
- this.noteSessionSubscriberTag(session, subscriptionId);
617
- }
618
- return;
619
- }
620
- if (Date.now() < (this.sessionRetryCooldownUntil.get(format) ?? 0)) return;
621
- const creation = (async () => {
622
- const subscriberIds = new Set([subscriptionId]);
623
- for (const subscription of this.subscriptions.values()) if (subscription.format === format) subscriberIds.add(subscription.id);
624
- const session = {
625
- format,
626
- proxy: null,
627
- nodeId: null,
628
- subscriberIds,
629
- bootstrapped: false,
630
- pullMode: false,
631
- targetFps: this.computeFormatFps(format),
632
- lingerTimer: null,
633
- hasNonOccupancySubscriberEver: false
634
- };
635
- for (const id of subscriberIds) this.noteSessionSubscriberTag(session, id);
636
- let started = false;
637
- try {
638
- started = await this.startSessionDecoder(session);
639
- } catch (err) {
640
- this.sessionRetryCooldownUntil.set(format, Date.now() + this.sessionRetryCooldownMs);
641
- throw err;
642
- }
643
- if (started) this.sessions.set(format, session);
644
- })();
645
- this.sessionCreating.set(format, creation);
646
- try {
647
- await creation;
648
- } finally {
649
- this.sessionCreating.delete(format);
650
- }
651
- }
652
- /**
653
- * Create the `frameSink: 'shm'` decoder for a `FormatSession` and start
654
- * draining its handle queue. Mutates `session.proxy` / `session.nodeId` in
655
- * place so a `rotate()` can rebuild a registered session without disturbing
656
- * its `subscriberIds`. Returns `false` when no stream / unsupported codec
657
- * means no decoder could be created.
658
- */
659
- async startSessionDecoder(session) {
660
- const { format } = session;
661
- const info = this.resolveStreamInfo();
662
- if (!info) {
663
- this.logger?.info("frame-handle plane: no stream — session deferred", { meta: { format } });
664
- return false;
665
- }
666
- if (!await this.decoderApi.supportsCodec({ codec: info.codec })) {
667
- this.logger?.warn("frame-handle plane: codec unsupported — session skipped", { meta: {
668
- format,
669
- codec: info.codec
670
- } });
671
- return false;
672
- }
673
- const targetFps = this.computeFormatFps(format);
674
- session.targetFps = targetFps;
675
- const { sessionId, nodeId } = await this.decoderApi.createSession({
676
- codec: info.codec,
677
- maxFps: targetFps,
678
- outputFormat: format,
679
- scale: 1,
680
- frameSink: "shm",
681
- ...info.numericDeviceId !== void 0 ? { deviceId: info.numericDeviceId } : {},
682
- tag: `${info.tag}:shm:${format}`,
683
- ...this.isDebugEnabled?.() ? { debug: true } : {}
684
- });
685
- if (!isDecoderNodeColocated(this.localNodeId, nodeId)) {
686
- this.sessionRetryCooldownUntil.set(format, Date.now() + this.sessionRetryCooldownMs);
687
- this.logger?.warn("frame-handle plane: decoder session landed on a non-local node — destroying (shm ring is node-local)", { meta: {
688
- format,
689
- requestedNode: this.localNodeId,
690
- gotNode: nodeId,
691
- sessionId,
692
- retryInMs: this.sessionRetryCooldownMs
693
- } });
694
- await this.decoderApi.destroySession({ sessionId }).catch((err) => {
695
- this.logger?.warn("frame-handle plane: failed to destroy mis-placed decoder session", { meta: {
696
- format,
697
- sessionId,
698
- error: errMsg(err)
699
- } });
700
- });
701
- return false;
702
- }
703
- this.sessionRetryCooldownUntil.delete(format);
704
- const proxy = new DecoderSessionProxy(this.decoderApi, sessionId);
705
- session.proxy = proxy;
706
- session.nodeId = nodeId;
707
- session.bootstrapped = false;
708
- session.pullMode = false;
709
- proxy.startHandlePolling((handle) => {
710
- this.fanoutHandle(format, handle);
711
- }).catch((err) => {
712
- this.logger?.warn("frame-handle plane: handle polling error", { meta: {
713
- format,
714
- error: errMsg(err)
715
- } });
716
- });
717
- if (info.pullModeUrl) {
718
- session.pullMode = true;
719
- session.bootstrapped = true;
720
- proxy.openStream(info.pullModeUrl).catch((err) => {
721
- this.logger?.error("frame-handle plane: pull-mode openStream failed", { meta: {
722
- format,
723
- error: errMsg(err)
724
- } });
725
- });
726
- this.logger?.info("frame-handle plane: shm decoder session created (pull-mode RTSP)", { meta: {
727
- format,
728
- codec: info.codec,
729
- sessionId,
730
- nodeId,
731
- pullModeUrl: info.pullModeUrl
732
- } });
733
- return true;
734
- }
735
- const seed = this.getSeedPackets?.() ?? [];
736
- if (seed.length > 0) {
737
- for (const seedPacket of seed) await proxy.pushPacket(seedPacket).catch((err) => {
738
- this.logger?.warn("frame-handle plane: seed-replay push error", { meta: {
739
- format,
740
- error: errMsg(err)
741
- } });
742
- });
743
- session.bootstrapped = true;
744
- this.logger?.debug("frame-handle plane: session bootstrapped from seed GOP", { meta: {
745
- format,
746
- seedPackets: seed.length
747
- } });
748
- }
749
- this.logger?.info("frame-handle plane: shm decoder session created", { meta: {
750
- format,
751
- codec: info.codec,
752
- sessionId,
753
- nodeId
754
- } });
755
- return true;
756
- }
757
- /** Push one decoded `FrameHandle` to every subscription of `format`. */
758
- fanoutHandle(format, handle) {
759
- this.rollDecodeFpsWindow();
760
- this.framesInWindow += 1;
761
- for (const subscription of this.subscriptions.values()) {
762
- if (subscription.format !== format) continue;
763
- subscription.queue.push(handle);
764
- }
765
- }
766
- /** Destroy a decoder session, swallowing teardown errors. */
767
- async destroySession(session) {
768
- this.clearLinger(session);
769
- const proxy = session.proxy;
770
- session.proxy = null;
771
- session.nodeId = null;
772
- if (!proxy) return;
773
- try {
774
- await proxy.destroy();
775
- } catch (err) {
776
- this.logger?.warn("frame-handle plane: session destroy failed", { meta: {
777
- format: session.format,
778
- error: errMsg(err)
779
- } });
780
- }
781
- }
782
- };
783
- //#endregion
784
- //#region src/pipeline-runner/remote-source-plane.ts
785
- /**
786
- * Runner-side mode selection (pure): whether an attach payload routes this
787
- * camera through the remote-source leg. Absent `frameSource` (a pre-P2c
788
- * payload) and `local-broker` both take today's co-located broker path —
789
- * the rollout-OFF safety invariant.
790
- */
791
- function isRemoteRestream(frameSource) {
792
- return frameSource !== void 0 && frameSource.kind === "remote-restream";
793
- }
794
- /**
795
- * Resolve WHICH broker profile a camStream id feeds for a device (pure).
796
- *
797
- * `getStreamWithCodec` targets a PROFILE (its assigned camStream), while the
798
- * runner is dispatched with camStream ids (`motionStreamId` /
799
- * `detectionStreamId`). The broker's profile slots carry the mapping
800
- * (`sourceCamStreamId`); a stream no slot feeds returns `null` — the acquire
801
- * fails fast and the poller's backoff retries (the assignment may still be
802
- * propagating).
803
- */
804
- function profileForStreamId(slots, deviceId, streamId) {
805
- for (const slot of slots) if (slot.deviceId === deviceId && slot.sourceCamStreamId === streamId) return slot.profile;
806
- return null;
807
- }
808
- var RemoteSourcePlane = class {
809
- api;
810
- logger;
811
- deviceId;
812
- ownNodeId;
813
- ownerNodeId;
814
- hostname;
815
- /** Single-flight entry creation per camStream id. */
816
- entries = /* @__PURE__ */ new Map();
817
- disposed = false;
818
- constructor(options) {
819
- this.api = options.api;
820
- this.logger = options.logger.withTags({ deviceId: options.deviceId });
821
- this.deviceId = options.deviceId;
822
- this.ownNodeId = options.ownNodeId;
823
- this.ownerNodeId = options.ownerNodeId;
824
- this.hostname = options.hostname;
825
- }
826
- /**
827
- * A {@link FrameHandleSource} for one camStream id, pluggable into
828
- * `startFrameHandlePoller`. Motion (`gray`) and detection (`rgb`)
829
- * subscriptions of the same stream share ONE acquired restream and ONE
830
- * satellite plane (per-format decoder sessions inside it — hub parity).
831
- */
832
- makeSource(streamId) {
833
- return {
834
- subscribe: async (input) => {
835
- if (this.disposed) throw new Error("RemoteSourcePlane: subscribe after dispose");
836
- const entry = await this.ensureEntry(streamId);
837
- const result = await entry.plane.subscribe({
838
- format: input.format,
839
- maxFps: input.maxFps,
840
- tag: input.tag
841
- });
842
- entry.subscriptionIds.add(result.subscriptionId);
843
- return result;
844
- },
845
- pullHandles: async (input) => {
846
- const entry = await this.peekEntry(streamId);
847
- if (!entry) return [];
848
- return entry.plane.pullHandles(input.subscriptionId, input.maxCount);
849
- },
850
- unsubscribe: async (input) => {
851
- const entry = await this.peekEntry(streamId);
852
- if (!entry) return;
853
- entry.subscriptionIds.delete(input.subscriptionId);
854
- await entry.plane.unsubscribe(input.subscriptionId).catch(() => {});
855
- if (entry.subscriptionIds.size === 0) {
856
- this.entries.delete(streamId);
857
- await this.teardownEntry(entry, streamId);
858
- }
859
- }
860
- };
861
- }
862
- /** Dispose every entry: satellite planes destroyed, restreams released. Idempotent. */
863
- async dispose() {
864
- if (this.disposed) return;
865
- this.disposed = true;
866
- const pending = [...this.entries.entries()];
867
- this.entries.clear();
868
- for (const [streamId, entryPromise] of pending) try {
869
- const entry = await entryPromise;
870
- await this.teardownEntry(entry, streamId);
871
- } catch {}
872
- }
873
- /** The live entry for a stream, or `null` (never creates one). */
874
- async peekEntry(streamId) {
875
- const pending = this.entries.get(streamId);
876
- if (!pending) return null;
877
- try {
878
- return await pending;
879
- } catch {
880
- return null;
881
- }
882
- }
883
- /** Single-flight: acquire the restream + build the satellite plane once per stream. */
884
- ensureEntry(streamId) {
885
- const existing = this.entries.get(streamId);
886
- if (existing) return existing;
887
- const creation = this.createEntry(streamId);
888
- this.entries.set(streamId, creation);
889
- creation.catch(() => {
890
- if (this.entries.get(streamId) === creation) this.entries.delete(streamId);
891
- });
892
- return creation;
893
- }
894
- async createEntry(streamId) {
895
- const { api, deviceId, ownNodeId, ownerNodeId } = this;
896
- if (this.hostname === void 0) throw new Error("RemoteSourcePlane: owner hostname unresolvable — set frameSource.hubHostnameOverride or CAMSTACK_HUB_URL (a 127.0.0.1 restream URL would dial the wrong box)");
897
- const profile = profileForStreamId(await api.streamBroker.listAllProfileSlots.query(void 0, nodePin(ownerNodeId)), deviceId, streamId);
898
- if (profile === null) throw new Error(`RemoteSourcePlane: no broker profile is fed by camStream '${streamId}' of device ${deviceId} — cannot acquire the owner restream`);
899
- const rtpSource = await api.streamBroker.getStreamWithCodec.mutate({
900
- deviceId,
901
- video: "copy",
902
- audio: "none",
903
- profile,
904
- hostname: this.hostname,
905
- tag: `runner:${ownNodeId}:remote-src:${deviceId}/${streamId}`
906
- }, nodePin(ownerNodeId));
907
- const codec = rtpSource.videoCodec === "H265" ? "h265" : "h264";
908
- const plane = new FrameHandlePlane({
909
- decoderApi: this.buildPinnedDecoderCapApi(),
910
- logger: this.logger,
911
- localNodeId: ownNodeId,
912
- resolveStreamInfo: () => ({
913
- codec,
914
- numericDeviceId: deviceId,
915
- tag: `runner-remote:${deviceId}/${streamId}`,
916
- pullModeUrl: rtpSource.url
917
- })
918
- });
919
- this.logger.info("remote-source plane: owner restream acquired", { meta: {
920
- streamId,
921
- profile,
922
- codec,
923
- pipelineKey: rtpSource.pipelineKey,
924
- hostname: this.hostname
925
- } });
926
- return {
927
- plane,
928
- pipelineKey: rtpSource.pipelineKey,
929
- subscriptionIds: /* @__PURE__ */ new Set(),
930
- released: false
931
- };
932
- }
933
- /** Destroy an entry's satellite plane and release its restream refcount once. */
934
- async teardownEntry(entry, streamId) {
935
- await entry.plane.dispose().catch((err) => {
936
- this.logger.warn("remote-source plane: satellite plane dispose failed", { meta: {
937
- streamId,
938
- error: errMsg(err)
939
- } });
940
- });
941
- if (entry.released) return;
942
- entry.released = true;
943
- await this.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: entry.pipelineKey }, nodePin(this.ownerNodeId)).catch((err) => {
944
- this.logger.debug("remote-source plane: releaseStreamWithCodec failed (benign on hub restart)", { meta: {
945
- streamId,
946
- pipelineKey: entry.pipelineKey,
947
- error: errMsg(err)
948
- } });
949
- });
950
- }
951
- /**
952
- * Every decoder call pinned to THIS runner's node — mirror of the hub
953
- * broker's wrapper (`stream-broker-manager.buildDecoderCapApi`): the
954
- * `decoder` cap is a singleton whose UNPINNED calls resolve to the
955
- * cluster-elected node, which is generally NOT this agent. Pinning create
956
- * AND every session op to the same local node keeps the session + its shm
957
- * ring here, where this plane's readers live.
958
- */
959
- buildPinnedDecoderCapApi() {
960
- const api = this.api;
961
- const pin = nodePin(this.ownNodeId);
962
- const toWireBytes = (bytes) => {
963
- const copy = new Uint8Array(new ArrayBuffer(bytes.byteLength));
964
- copy.set(bytes);
965
- return copy;
966
- };
967
- return {
968
- supportsCodec: async (input) => api.decoder.supportsCodec.query({ ...input }, pin),
969
- getInfo: async () => api.decoder.getInfo.query(void 0, pin),
970
- createSession: async (config) => api.decoder.createSession.query({ ...config }, pin),
971
- pushPacket: async (input) => {
972
- await api.decoder.pushPacket.query({
973
- sessionId: input.sessionId,
974
- packet: {
975
- ...input.packet,
976
- data: toWireBytes(input.packet.data)
977
- }
978
- }, pin);
979
- },
980
- pullFrames: async (input) => {
981
- return (await api.decoder.pullFrames.query({ ...input }, pin)).map((frame) => ({
982
- ...frame,
983
- data: Buffer.from(frame.data)
984
- }));
985
- },
986
- pullHandles: async (input) => api.decoder.pullHandles.query({ ...input }, pin),
987
- destroySession: async (input) => {
988
- await api.decoder.destroySession.query({ ...input }, pin);
989
- },
990
- openStream: async (input) => {
991
- await api.decoder.openStream.query({ ...input }, pin);
992
- },
993
- updateConfig: async (input) => {
994
- await api.decoder.updateConfig.query({ ...input }, pin);
995
- },
996
- getStats: async (input) => api.decoder.getStats.query({ ...input }, pin)
997
- };
998
- }
999
- };
1000
- //#endregion
1001
- export { DecoderSessionProxy as a, FrameHandlePlane as i, isRemoteRestream as n, profileForStreamId as r, RemoteSourcePlane as t };