@camstack/addon-pipeline 1.1.32 → 1.1.34
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 +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/audio-codec-ffmpeg/index.js +118 -3
- package/dist/audio-codec-ffmpeg/index.mjs +118 -3
- package/dist/decoder-ffmpeg/index.js +75 -10
- package/dist/decoder-ffmpeg/index.mjs +75 -10
- package/dist/decoder-nodeav/index.js +209 -9
- package/dist/decoder-nodeav/index.mjs +209 -9
- package/dist/detection-pipeline/index.js +1 -1
- package/dist/detection-pipeline/index.mjs +1 -1
- package/dist/{dist-Cwc0TUQr.js → dist-BrIE85BG.js} +38 -1
- package/dist/{dist-DjuGmyG9.mjs → dist-CfSORkv2.mjs} +38 -1
- package/dist/{ffmpeg-args-C5GPp8Cw.mjs → ffmpeg-args-CpeRWwL4.mjs} +7 -4
- package/dist/{ffmpeg-args-D6h1edXK.js → ffmpeg-args-Dl9mLcOC.js} +7 -4
- package/dist/{frame-handle-plane-Bkxz-TTD.mjs → frame-handle-plane-C05rq9Zs.mjs} +108 -17
- package/dist/{frame-handle-plane-DQNCTrpC.js → frame-handle-plane-zMju9vmk.js} +108 -17
- package/dist/{frame-ring-sink-sY-8XUsH.mjs → frame-ring-sink-1aLGcNNf.mjs} +81 -31
- package/dist/{frame-ring-sink-DvdhCFr3.js → frame-ring-sink-CvlcAUOu.js} +86 -30
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +86 -16
- package/dist/pipeline-runner/index.mjs +85 -17
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D567z31g.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DPFAbcUo.mjs} +3 -3
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DtoYGpTp.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BsrrdOBU.mjs} +1 -1
- package/dist/stream-broker/{hostInit-B_b3PIZB.mjs → hostInit-C8slrxw1.mjs} +3 -3
- package/dist/stream-broker/index.js +165 -27
- package/dist/stream-broker/index.mjs +165 -27
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-x-DZOuQL.js → MaskShapeCanvas-DI4BY7W2-WUVfFzT-.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DTeuDGCU.js → MotionZonesSettings-NcxxQN8r-CF7FXXLs.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-CirtxO3e.js → PrivacyMaskSettings-APgPLF7p-qQCpVob3.js} +1 -1
- package/embed-dist/assets/{index-CpUy8OIE.js → index-BNhisVUZ.js} +4 -4
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/constants-CAqYVigN.js +0 -6615
- package/dist/constants-oYgW36Ty.mjs +0 -3587
- package/dist/dist-D3Ytt-_Y.js +0 -41111
- package/dist/dist-DluJuxV5.mjs +0 -37976
|
@@ -16328,7 +16328,17 @@ var AgentAddonConfigSchema = object({
|
|
|
16328
16328
|
});
|
|
16329
16329
|
var AgentPipelineSettingsSchema = object({
|
|
16330
16330
|
addonDefaults: record(string(), AgentAddonConfigSchema).readonly(),
|
|
16331
|
-
maxCameras: number().int().nonnegative().nullable().default(null)
|
|
16331
|
+
maxCameras: number().int().nonnegative().nullable().default(null),
|
|
16332
|
+
/** Per-node detection weight (relative share for the quota balancer). */
|
|
16333
|
+
detectWeight: number().positive().optional(),
|
|
16334
|
+
/** Node is eligible to run the detection pipeline (decode + inference). */
|
|
16335
|
+
detect: boolean().optional(),
|
|
16336
|
+
/** Node is eligible to host decoder sessions. */
|
|
16337
|
+
decode: boolean().optional(),
|
|
16338
|
+
/** Node is eligible to run audio-analyzer sessions. */
|
|
16339
|
+
audio: boolean().optional(),
|
|
16340
|
+
/** Node is eligible to be the ingest / source-owner (serve the restream). */
|
|
16341
|
+
ingest: boolean().optional()
|
|
16332
16342
|
});
|
|
16333
16343
|
var CameraPipelineForAgentSchema = object({
|
|
16334
16344
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
@@ -16634,6 +16644,21 @@ method(object({
|
|
|
16634
16644
|
}), object({ success: literal(true) }), {
|
|
16635
16645
|
kind: "mutation",
|
|
16636
16646
|
auth: "admin"
|
|
16647
|
+
}), method(object({
|
|
16648
|
+
agentNodeId: string(),
|
|
16649
|
+
detectWeight: number().positive().nullable()
|
|
16650
|
+
}), object({ success: literal(true) }), {
|
|
16651
|
+
kind: "mutation",
|
|
16652
|
+
auth: "admin"
|
|
16653
|
+
}), method(object({
|
|
16654
|
+
agentNodeId: string(),
|
|
16655
|
+
detect: boolean().nullable().optional(),
|
|
16656
|
+
decode: boolean().nullable().optional(),
|
|
16657
|
+
audio: boolean().nullable().optional(),
|
|
16658
|
+
ingest: boolean().nullable().optional()
|
|
16659
|
+
}), object({ success: literal(true) }), {
|
|
16660
|
+
kind: "mutation",
|
|
16661
|
+
auth: "admin"
|
|
16637
16662
|
}), method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()), method(object({
|
|
16638
16663
|
deviceId: number(),
|
|
16639
16664
|
addonId: string(),
|
|
@@ -22621,6 +22646,18 @@ Object.freeze({
|
|
|
22621
22646
|
addonId: null,
|
|
22622
22647
|
access: "create"
|
|
22623
22648
|
},
|
|
22649
|
+
"pipelineOrchestrator.setAgentCapabilities": {
|
|
22650
|
+
capName: "pipeline-orchestrator",
|
|
22651
|
+
capScope: "system",
|
|
22652
|
+
addonId: null,
|
|
22653
|
+
access: "create"
|
|
22654
|
+
},
|
|
22655
|
+
"pipelineOrchestrator.setAgentDetectWeight": {
|
|
22656
|
+
capName: "pipeline-orchestrator",
|
|
22657
|
+
capScope: "system",
|
|
22658
|
+
addonId: null,
|
|
22659
|
+
access: "create"
|
|
22660
|
+
},
|
|
22624
22661
|
"pipelineOrchestrator.setAgentMaxCameras": {
|
|
22625
22662
|
capName: "pipeline-orchestrator",
|
|
22626
22663
|
capScope: "system",
|
|
@@ -194,6 +194,12 @@ function buildFfmpegDecodeArgs(config, options) {
|
|
|
194
194
|
"-i",
|
|
195
195
|
"pipe:0"
|
|
196
196
|
];
|
|
197
|
+
const probeArgs = options.inputUrl !== void 0 ? [] : [
|
|
198
|
+
"-probesize",
|
|
199
|
+
"1M",
|
|
200
|
+
"-analyzeduration",
|
|
201
|
+
"0"
|
|
202
|
+
];
|
|
197
203
|
return [
|
|
198
204
|
...logBannerArgs("info"),
|
|
199
205
|
"-nostats",
|
|
@@ -201,10 +207,7 @@ function buildFfmpegDecodeArgs(config, options) {
|
|
|
201
207
|
"+nobuffer+flush_packets",
|
|
202
208
|
"-flags",
|
|
203
209
|
"low_delay",
|
|
204
|
-
|
|
205
|
-
"1M",
|
|
206
|
-
"-analyzeduration",
|
|
207
|
-
"0",
|
|
210
|
+
...probeArgs,
|
|
208
211
|
...hwaccelArgs,
|
|
209
212
|
...inputArgs,
|
|
210
213
|
"-vf",
|
|
@@ -194,6 +194,12 @@ function buildFfmpegDecodeArgs(config, options) {
|
|
|
194
194
|
"-i",
|
|
195
195
|
"pipe:0"
|
|
196
196
|
];
|
|
197
|
+
const probeArgs = options.inputUrl !== void 0 ? [] : [
|
|
198
|
+
"-probesize",
|
|
199
|
+
"1M",
|
|
200
|
+
"-analyzeduration",
|
|
201
|
+
"0"
|
|
202
|
+
];
|
|
197
203
|
return [
|
|
198
204
|
...logBannerArgs("info"),
|
|
199
205
|
"-nostats",
|
|
@@ -201,10 +207,7 @@ function buildFfmpegDecodeArgs(config, options) {
|
|
|
201
207
|
"+nobuffer+flush_packets",
|
|
202
208
|
"-flags",
|
|
203
209
|
"low_delay",
|
|
204
|
-
|
|
205
|
-
"1M",
|
|
206
|
-
"-analyzeduration",
|
|
207
|
-
"0",
|
|
210
|
+
...probeArgs,
|
|
208
211
|
...hwaccelArgs,
|
|
209
212
|
...inputArgs,
|
|
210
213
|
"-vf",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as errMsg, p as RingBuffer } from "./dist-
|
|
1
|
+
import { B as errMsg, p as RingBuffer } from "./dist-CfSORkv2.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
//#region src/stream-broker/stream-broker/decoder-session-proxy.ts
|
|
4
4
|
/**
|
|
@@ -189,6 +189,7 @@ var FrameHandlePlane = class {
|
|
|
189
189
|
resolveStreamInfo;
|
|
190
190
|
localNodeId;
|
|
191
191
|
sessionRetryCooldownMs;
|
|
192
|
+
getSeedPackets;
|
|
192
193
|
subscriptions = /* @__PURE__ */ new Map();
|
|
193
194
|
sessions = /* @__PURE__ */ new Map();
|
|
194
195
|
/** Per-format retry gate set when a session lands on a non-local node:
|
|
@@ -203,6 +204,9 @@ var FrameHandlePlane = class {
|
|
|
203
204
|
/** Re-entrancy guard for `armPendingSessions` — collapses the per-packet
|
|
204
205
|
* `pushPacket` calls into a single in-flight deferred-arm pass. */
|
|
205
206
|
armInFlight = false;
|
|
207
|
+
/** DIAG(592): P-frames skipped on an un-bootstrapped session before its
|
|
208
|
+
* first live keyframe. Reset when a keyframe bootstraps the session. */
|
|
209
|
+
unbootstrappedSkips = 0;
|
|
206
210
|
/** Aggregate decoded-frame rate state — frames fanned out in the current
|
|
207
211
|
* rolling window, the window's start epoch, and the last computed fps. */
|
|
208
212
|
framesInWindow = 0;
|
|
@@ -214,6 +218,7 @@ var FrameHandlePlane = class {
|
|
|
214
218
|
this.resolveStreamInfo = options.resolveStreamInfo;
|
|
215
219
|
this.localNodeId = options.localNodeId;
|
|
216
220
|
this.sessionRetryCooldownMs = options.sessionRetryCooldownMs ?? SESSION_RETRY_COOLDOWN_MS;
|
|
221
|
+
this.getSeedPackets = options.getSeedPackets;
|
|
217
222
|
}
|
|
218
223
|
/** Number of active handle subscriptions — surfaced for diagnostics. */
|
|
219
224
|
get subscriberCount() {
|
|
@@ -278,6 +283,7 @@ var FrameHandlePlane = class {
|
|
|
278
283
|
};
|
|
279
284
|
this.subscriptions.set(subscriptionId, subscription);
|
|
280
285
|
await this.ensureSession(input.format, subscriptionId);
|
|
286
|
+
this.reconcileSessionFps(input.format);
|
|
281
287
|
this.logger?.info("frame-handle subscription added", { meta: {
|
|
282
288
|
subscriptionId,
|
|
283
289
|
format: input.format,
|
|
@@ -290,6 +296,37 @@ var FrameHandlePlane = class {
|
|
|
290
296
|
};
|
|
291
297
|
}
|
|
292
298
|
/**
|
|
299
|
+
* The emit `maxFps` a format's decoder session should run at — the MAX of all
|
|
300
|
+
* current subscribers' `maxFps` for that format (fallback `DEFAULT_HINT_FPS`
|
|
301
|
+
* when none, e.g. mid-teardown). The decoder emits at this rate; each
|
|
302
|
+
* subscriber then down-samples to its own `maxFps` on read (latest-wins).
|
|
303
|
+
*/
|
|
304
|
+
computeFormatFps(format) {
|
|
305
|
+
let max = 0;
|
|
306
|
+
for (const sub of this.subscriptions.values()) if (sub.format === format && sub.maxFps > max) max = sub.maxFps;
|
|
307
|
+
return max > 0 ? max : DEFAULT_HINT_FPS;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Recompute a format session's emit `maxFps` after a subscriber joins/leaves
|
|
311
|
+
* and push it to the live decoder via `updateConfig` when it changed — so the
|
|
312
|
+
* decoder emits only as fast as the busiest reader needs (saving the
|
|
313
|
+
* scale+convert+shm-write for frames nobody reads).
|
|
314
|
+
*/
|
|
315
|
+
reconcileSessionFps(format) {
|
|
316
|
+
const session = this.sessions.get(format);
|
|
317
|
+
if (!session || !session.proxy) return;
|
|
318
|
+
const target = this.computeFormatFps(format);
|
|
319
|
+
if (target === session.targetFps) return;
|
|
320
|
+
session.targetFps = target;
|
|
321
|
+
session.proxy.updateConfig({ maxFps: target }).catch((err) => {
|
|
322
|
+
this.logger?.warn("frame-handle plane: updateConfig(maxFps) failed", { meta: {
|
|
323
|
+
format,
|
|
324
|
+
maxFps: target,
|
|
325
|
+
error: errMsg(err)
|
|
326
|
+
} });
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
293
330
|
* Drain up to `maxCount` `FrameHandle`s for a subscription, latest-wins.
|
|
294
331
|
* An unknown subscription id returns `[]` (the consumer may have been
|
|
295
332
|
* torn down concurrently).
|
|
@@ -320,6 +357,7 @@ var FrameHandlePlane = class {
|
|
|
320
357
|
this.sessions.delete(subscription.format);
|
|
321
358
|
await this.destroySession(session);
|
|
322
359
|
} else this.scheduleLinger(session);
|
|
360
|
+
else this.reconcileSessionFps(subscription.format);
|
|
323
361
|
}
|
|
324
362
|
this.logger?.info("frame-handle subscription removed", { meta: {
|
|
325
363
|
subscriptionId,
|
|
@@ -373,10 +411,23 @@ var FrameHandlePlane = class {
|
|
|
373
411
|
});
|
|
374
412
|
this.feedSessions(packet);
|
|
375
413
|
}
|
|
376
|
-
/** Forward a video `EncodedPacket` to every live shm decoder session. */
|
|
414
|
+
/** Forward a video `EncodedPacket` to every live PUSH-mode shm decoder session. */
|
|
377
415
|
feedSessions(packet) {
|
|
378
416
|
for (const session of this.sessions.values()) {
|
|
379
417
|
if (!session.proxy) continue;
|
|
418
|
+
if (session.pullMode) continue;
|
|
419
|
+
if (!session.bootstrapped) {
|
|
420
|
+
if (!packet.keyframe) {
|
|
421
|
+
this.unbootstrappedSkips = (this.unbootstrappedSkips ?? 0) + 1;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
this.logger?.debug("frame-handle plane: session bootstrapped from live keyframe", { meta: {
|
|
425
|
+
format: session.format,
|
|
426
|
+
pFramesSkippedBefore: this.unbootstrappedSkips ?? 0
|
|
427
|
+
} });
|
|
428
|
+
this.unbootstrappedSkips = 0;
|
|
429
|
+
session.bootstrapped = true;
|
|
430
|
+
}
|
|
380
431
|
session.proxy.pushPacket(packet).catch((err) => {
|
|
381
432
|
const msg = errMsg(err);
|
|
382
433
|
this.logger?.warn("frame-handle plane: decoder push error", { meta: {
|
|
@@ -404,10 +455,14 @@ var FrameHandlePlane = class {
|
|
|
404
455
|
*
|
|
405
456
|
* `triggerPacket` is the packet whose arrival armed the pass. Because
|
|
406
457
|
* `ensureSession` is async, the synchronous `feedSessions` in `pushPacket`
|
|
407
|
-
* runs before the session lands in `this.sessions
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
458
|
+
* runs before the session lands in `this.sessions`, so the triggering packet
|
|
459
|
+
* would be lost to the new session. `startSessionDecoder` normally bootstraps
|
|
460
|
+
* it by replaying the broker's keyframe-aligned seed GOP; only when NO seed
|
|
461
|
+
* was available (no `getSeedPackets`, or the ring held no keyframe yet) is the
|
|
462
|
+
* session left un-bootstrapped — and then this method hands it the triggering
|
|
463
|
+
* packet, but ONLY if that packet is a keyframe. A P-frame trigger is left for
|
|
464
|
+
* `feedSessions` to skip until the first live keyframe, so a push-mode decoder
|
|
465
|
+
* never sees a slice referencing an unseen param set.
|
|
411
466
|
*/
|
|
412
467
|
async armPendingSessions(triggerPacket) {
|
|
413
468
|
if (this.disposed || this.armInFlight) return;
|
|
@@ -423,12 +478,15 @@ var FrameHandlePlane = class {
|
|
|
423
478
|
if (this.disposed) return;
|
|
424
479
|
await this.ensureSession(format, subscriptionId);
|
|
425
480
|
const armed = this.sessions.get(format);
|
|
426
|
-
if (armed?.proxy
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
error:
|
|
430
|
-
|
|
431
|
-
|
|
481
|
+
if (armed?.proxy && !armed.bootstrapped && triggerPacket.keyframe) {
|
|
482
|
+
armed.bootstrapped = true;
|
|
483
|
+
armed.proxy.pushPacket(triggerPacket).catch((err) => {
|
|
484
|
+
this.logger?.warn("frame-handle plane: decoder push error", { meta: {
|
|
485
|
+
format,
|
|
486
|
+
error: errMsg(err)
|
|
487
|
+
} });
|
|
488
|
+
});
|
|
489
|
+
}
|
|
432
490
|
}
|
|
433
491
|
} finally {
|
|
434
492
|
this.armInFlight = false;
|
|
@@ -522,6 +580,9 @@ var FrameHandlePlane = class {
|
|
|
522
580
|
proxy: null,
|
|
523
581
|
nodeId: null,
|
|
524
582
|
subscriberIds,
|
|
583
|
+
bootstrapped: false,
|
|
584
|
+
pullMode: false,
|
|
585
|
+
targetFps: this.computeFormatFps(format),
|
|
525
586
|
lingerTimer: null
|
|
526
587
|
};
|
|
527
588
|
let started = false;
|
|
@@ -561,9 +622,11 @@ var FrameHandlePlane = class {
|
|
|
561
622
|
} });
|
|
562
623
|
return false;
|
|
563
624
|
}
|
|
625
|
+
const targetFps = this.computeFormatFps(format);
|
|
626
|
+
session.targetFps = targetFps;
|
|
564
627
|
const { sessionId, nodeId } = await this.decoderApi.createSession({
|
|
565
628
|
codec: info.codec,
|
|
566
|
-
maxFps:
|
|
629
|
+
maxFps: targetFps,
|
|
567
630
|
outputFormat: format,
|
|
568
631
|
scale: 1,
|
|
569
632
|
frameSink: "shm",
|
|
@@ -592,6 +655,8 @@ var FrameHandlePlane = class {
|
|
|
592
655
|
const proxy = new DecoderSessionProxy(this.decoderApi, sessionId);
|
|
593
656
|
session.proxy = proxy;
|
|
594
657
|
session.nodeId = nodeId;
|
|
658
|
+
session.bootstrapped = false;
|
|
659
|
+
session.pullMode = false;
|
|
595
660
|
proxy.startHandlePolling((handle) => {
|
|
596
661
|
this.fanoutHandle(format, handle);
|
|
597
662
|
}).catch((err) => {
|
|
@@ -600,12 +665,38 @@ var FrameHandlePlane = class {
|
|
|
600
665
|
error: errMsg(err)
|
|
601
666
|
} });
|
|
602
667
|
});
|
|
603
|
-
if (info.pullModeUrl)
|
|
604
|
-
|
|
668
|
+
if (info.pullModeUrl) {
|
|
669
|
+
session.pullMode = true;
|
|
670
|
+
session.bootstrapped = true;
|
|
671
|
+
proxy.openStream(info.pullModeUrl).catch((err) => {
|
|
672
|
+
this.logger?.error("frame-handle plane: pull-mode openStream failed", { meta: {
|
|
673
|
+
format,
|
|
674
|
+
error: errMsg(err)
|
|
675
|
+
} });
|
|
676
|
+
});
|
|
677
|
+
this.logger?.info("frame-handle plane: shm decoder session created (pull-mode RTSP)", { meta: {
|
|
605
678
|
format,
|
|
606
|
-
|
|
679
|
+
codec: info.codec,
|
|
680
|
+
sessionId,
|
|
681
|
+
nodeId,
|
|
682
|
+
pullModeUrl: info.pullModeUrl
|
|
607
683
|
} });
|
|
608
|
-
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
const seed = this.getSeedPackets?.() ?? [];
|
|
687
|
+
if (seed.length > 0) {
|
|
688
|
+
for (const seedPacket of seed) await proxy.pushPacket(seedPacket).catch((err) => {
|
|
689
|
+
this.logger?.warn("frame-handle plane: seed-replay push error", { meta: {
|
|
690
|
+
format,
|
|
691
|
+
error: errMsg(err)
|
|
692
|
+
} });
|
|
693
|
+
});
|
|
694
|
+
session.bootstrapped = true;
|
|
695
|
+
this.logger?.debug("frame-handle plane: session bootstrapped from seed GOP", { meta: {
|
|
696
|
+
format,
|
|
697
|
+
seedPackets: seed.length
|
|
698
|
+
} });
|
|
699
|
+
}
|
|
609
700
|
this.logger?.info("frame-handle plane: shm decoder session created", { meta: {
|
|
610
701
|
format,
|
|
611
702
|
codec: info.codec,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_dist = require("./dist-
|
|
1
|
+
const require_dist = require("./dist-BrIE85BG.js");
|
|
2
2
|
let node_crypto = require("node:crypto");
|
|
3
3
|
//#region src/stream-broker/stream-broker/decoder-session-proxy.ts
|
|
4
4
|
/**
|
|
@@ -189,6 +189,7 @@ var FrameHandlePlane = class {
|
|
|
189
189
|
resolveStreamInfo;
|
|
190
190
|
localNodeId;
|
|
191
191
|
sessionRetryCooldownMs;
|
|
192
|
+
getSeedPackets;
|
|
192
193
|
subscriptions = /* @__PURE__ */ new Map();
|
|
193
194
|
sessions = /* @__PURE__ */ new Map();
|
|
194
195
|
/** Per-format retry gate set when a session lands on a non-local node:
|
|
@@ -203,6 +204,9 @@ var FrameHandlePlane = class {
|
|
|
203
204
|
/** Re-entrancy guard for `armPendingSessions` — collapses the per-packet
|
|
204
205
|
* `pushPacket` calls into a single in-flight deferred-arm pass. */
|
|
205
206
|
armInFlight = false;
|
|
207
|
+
/** DIAG(592): P-frames skipped on an un-bootstrapped session before its
|
|
208
|
+
* first live keyframe. Reset when a keyframe bootstraps the session. */
|
|
209
|
+
unbootstrappedSkips = 0;
|
|
206
210
|
/** Aggregate decoded-frame rate state — frames fanned out in the current
|
|
207
211
|
* rolling window, the window's start epoch, and the last computed fps. */
|
|
208
212
|
framesInWindow = 0;
|
|
@@ -214,6 +218,7 @@ var FrameHandlePlane = class {
|
|
|
214
218
|
this.resolveStreamInfo = options.resolveStreamInfo;
|
|
215
219
|
this.localNodeId = options.localNodeId;
|
|
216
220
|
this.sessionRetryCooldownMs = options.sessionRetryCooldownMs ?? SESSION_RETRY_COOLDOWN_MS;
|
|
221
|
+
this.getSeedPackets = options.getSeedPackets;
|
|
217
222
|
}
|
|
218
223
|
/** Number of active handle subscriptions — surfaced for diagnostics. */
|
|
219
224
|
get subscriberCount() {
|
|
@@ -278,6 +283,7 @@ var FrameHandlePlane = class {
|
|
|
278
283
|
};
|
|
279
284
|
this.subscriptions.set(subscriptionId, subscription);
|
|
280
285
|
await this.ensureSession(input.format, subscriptionId);
|
|
286
|
+
this.reconcileSessionFps(input.format);
|
|
281
287
|
this.logger?.info("frame-handle subscription added", { meta: {
|
|
282
288
|
subscriptionId,
|
|
283
289
|
format: input.format,
|
|
@@ -290,6 +296,37 @@ var FrameHandlePlane = class {
|
|
|
290
296
|
};
|
|
291
297
|
}
|
|
292
298
|
/**
|
|
299
|
+
* The emit `maxFps` a format's decoder session should run at — the MAX of all
|
|
300
|
+
* current subscribers' `maxFps` for that format (fallback `DEFAULT_HINT_FPS`
|
|
301
|
+
* when none, e.g. mid-teardown). The decoder emits at this rate; each
|
|
302
|
+
* subscriber then down-samples to its own `maxFps` on read (latest-wins).
|
|
303
|
+
*/
|
|
304
|
+
computeFormatFps(format) {
|
|
305
|
+
let max = 0;
|
|
306
|
+
for (const sub of this.subscriptions.values()) if (sub.format === format && sub.maxFps > max) max = sub.maxFps;
|
|
307
|
+
return max > 0 ? max : DEFAULT_HINT_FPS;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Recompute a format session's emit `maxFps` after a subscriber joins/leaves
|
|
311
|
+
* and push it to the live decoder via `updateConfig` when it changed — so the
|
|
312
|
+
* decoder emits only as fast as the busiest reader needs (saving the
|
|
313
|
+
* scale+convert+shm-write for frames nobody reads).
|
|
314
|
+
*/
|
|
315
|
+
reconcileSessionFps(format) {
|
|
316
|
+
const session = this.sessions.get(format);
|
|
317
|
+
if (!session || !session.proxy) return;
|
|
318
|
+
const target = this.computeFormatFps(format);
|
|
319
|
+
if (target === session.targetFps) return;
|
|
320
|
+
session.targetFps = target;
|
|
321
|
+
session.proxy.updateConfig({ maxFps: target }).catch((err) => {
|
|
322
|
+
this.logger?.warn("frame-handle plane: updateConfig(maxFps) failed", { meta: {
|
|
323
|
+
format,
|
|
324
|
+
maxFps: target,
|
|
325
|
+
error: require_dist.errMsg(err)
|
|
326
|
+
} });
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
293
330
|
* Drain up to `maxCount` `FrameHandle`s for a subscription, latest-wins.
|
|
294
331
|
* An unknown subscription id returns `[]` (the consumer may have been
|
|
295
332
|
* torn down concurrently).
|
|
@@ -320,6 +357,7 @@ var FrameHandlePlane = class {
|
|
|
320
357
|
this.sessions.delete(subscription.format);
|
|
321
358
|
await this.destroySession(session);
|
|
322
359
|
} else this.scheduleLinger(session);
|
|
360
|
+
else this.reconcileSessionFps(subscription.format);
|
|
323
361
|
}
|
|
324
362
|
this.logger?.info("frame-handle subscription removed", { meta: {
|
|
325
363
|
subscriptionId,
|
|
@@ -373,10 +411,23 @@ var FrameHandlePlane = class {
|
|
|
373
411
|
});
|
|
374
412
|
this.feedSessions(packet);
|
|
375
413
|
}
|
|
376
|
-
/** Forward a video `EncodedPacket` to every live shm decoder session. */
|
|
414
|
+
/** Forward a video `EncodedPacket` to every live PUSH-mode shm decoder session. */
|
|
377
415
|
feedSessions(packet) {
|
|
378
416
|
for (const session of this.sessions.values()) {
|
|
379
417
|
if (!session.proxy) continue;
|
|
418
|
+
if (session.pullMode) continue;
|
|
419
|
+
if (!session.bootstrapped) {
|
|
420
|
+
if (!packet.keyframe) {
|
|
421
|
+
this.unbootstrappedSkips = (this.unbootstrappedSkips ?? 0) + 1;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
this.logger?.debug("frame-handle plane: session bootstrapped from live keyframe", { meta: {
|
|
425
|
+
format: session.format,
|
|
426
|
+
pFramesSkippedBefore: this.unbootstrappedSkips ?? 0
|
|
427
|
+
} });
|
|
428
|
+
this.unbootstrappedSkips = 0;
|
|
429
|
+
session.bootstrapped = true;
|
|
430
|
+
}
|
|
380
431
|
session.proxy.pushPacket(packet).catch((err) => {
|
|
381
432
|
const msg = require_dist.errMsg(err);
|
|
382
433
|
this.logger?.warn("frame-handle plane: decoder push error", { meta: {
|
|
@@ -404,10 +455,14 @@ var FrameHandlePlane = class {
|
|
|
404
455
|
*
|
|
405
456
|
* `triggerPacket` is the packet whose arrival armed the pass. Because
|
|
406
457
|
* `ensureSession` is async, the synchronous `feedSessions` in `pushPacket`
|
|
407
|
-
* runs before the session lands in `this.sessions
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
458
|
+
* runs before the session lands in `this.sessions`, so the triggering packet
|
|
459
|
+
* would be lost to the new session. `startSessionDecoder` normally bootstraps
|
|
460
|
+
* it by replaying the broker's keyframe-aligned seed GOP; only when NO seed
|
|
461
|
+
* was available (no `getSeedPackets`, or the ring held no keyframe yet) is the
|
|
462
|
+
* session left un-bootstrapped — and then this method hands it the triggering
|
|
463
|
+
* packet, but ONLY if that packet is a keyframe. A P-frame trigger is left for
|
|
464
|
+
* `feedSessions` to skip until the first live keyframe, so a push-mode decoder
|
|
465
|
+
* never sees a slice referencing an unseen param set.
|
|
411
466
|
*/
|
|
412
467
|
async armPendingSessions(triggerPacket) {
|
|
413
468
|
if (this.disposed || this.armInFlight) return;
|
|
@@ -423,12 +478,15 @@ var FrameHandlePlane = class {
|
|
|
423
478
|
if (this.disposed) return;
|
|
424
479
|
await this.ensureSession(format, subscriptionId);
|
|
425
480
|
const armed = this.sessions.get(format);
|
|
426
|
-
if (armed?.proxy
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
error:
|
|
430
|
-
|
|
431
|
-
|
|
481
|
+
if (armed?.proxy && !armed.bootstrapped && triggerPacket.keyframe) {
|
|
482
|
+
armed.bootstrapped = true;
|
|
483
|
+
armed.proxy.pushPacket(triggerPacket).catch((err) => {
|
|
484
|
+
this.logger?.warn("frame-handle plane: decoder push error", { meta: {
|
|
485
|
+
format,
|
|
486
|
+
error: require_dist.errMsg(err)
|
|
487
|
+
} });
|
|
488
|
+
});
|
|
489
|
+
}
|
|
432
490
|
}
|
|
433
491
|
} finally {
|
|
434
492
|
this.armInFlight = false;
|
|
@@ -522,6 +580,9 @@ var FrameHandlePlane = class {
|
|
|
522
580
|
proxy: null,
|
|
523
581
|
nodeId: null,
|
|
524
582
|
subscriberIds,
|
|
583
|
+
bootstrapped: false,
|
|
584
|
+
pullMode: false,
|
|
585
|
+
targetFps: this.computeFormatFps(format),
|
|
525
586
|
lingerTimer: null
|
|
526
587
|
};
|
|
527
588
|
let started = false;
|
|
@@ -561,9 +622,11 @@ var FrameHandlePlane = class {
|
|
|
561
622
|
} });
|
|
562
623
|
return false;
|
|
563
624
|
}
|
|
625
|
+
const targetFps = this.computeFormatFps(format);
|
|
626
|
+
session.targetFps = targetFps;
|
|
564
627
|
const { sessionId, nodeId } = await this.decoderApi.createSession({
|
|
565
628
|
codec: info.codec,
|
|
566
|
-
maxFps:
|
|
629
|
+
maxFps: targetFps,
|
|
567
630
|
outputFormat: format,
|
|
568
631
|
scale: 1,
|
|
569
632
|
frameSink: "shm",
|
|
@@ -592,6 +655,8 @@ var FrameHandlePlane = class {
|
|
|
592
655
|
const proxy = new DecoderSessionProxy(this.decoderApi, sessionId);
|
|
593
656
|
session.proxy = proxy;
|
|
594
657
|
session.nodeId = nodeId;
|
|
658
|
+
session.bootstrapped = false;
|
|
659
|
+
session.pullMode = false;
|
|
595
660
|
proxy.startHandlePolling((handle) => {
|
|
596
661
|
this.fanoutHandle(format, handle);
|
|
597
662
|
}).catch((err) => {
|
|
@@ -600,12 +665,38 @@ var FrameHandlePlane = class {
|
|
|
600
665
|
error: require_dist.errMsg(err)
|
|
601
666
|
} });
|
|
602
667
|
});
|
|
603
|
-
if (info.pullModeUrl)
|
|
604
|
-
|
|
668
|
+
if (info.pullModeUrl) {
|
|
669
|
+
session.pullMode = true;
|
|
670
|
+
session.bootstrapped = true;
|
|
671
|
+
proxy.openStream(info.pullModeUrl).catch((err) => {
|
|
672
|
+
this.logger?.error("frame-handle plane: pull-mode openStream failed", { meta: {
|
|
673
|
+
format,
|
|
674
|
+
error: require_dist.errMsg(err)
|
|
675
|
+
} });
|
|
676
|
+
});
|
|
677
|
+
this.logger?.info("frame-handle plane: shm decoder session created (pull-mode RTSP)", { meta: {
|
|
605
678
|
format,
|
|
606
|
-
|
|
679
|
+
codec: info.codec,
|
|
680
|
+
sessionId,
|
|
681
|
+
nodeId,
|
|
682
|
+
pullModeUrl: info.pullModeUrl
|
|
607
683
|
} });
|
|
608
|
-
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
const seed = this.getSeedPackets?.() ?? [];
|
|
687
|
+
if (seed.length > 0) {
|
|
688
|
+
for (const seedPacket of seed) await proxy.pushPacket(seedPacket).catch((err) => {
|
|
689
|
+
this.logger?.warn("frame-handle plane: seed-replay push error", { meta: {
|
|
690
|
+
format,
|
|
691
|
+
error: require_dist.errMsg(err)
|
|
692
|
+
} });
|
|
693
|
+
});
|
|
694
|
+
session.bootstrapped = true;
|
|
695
|
+
this.logger?.debug("frame-handle plane: session bootstrapped from seed GOP", { meta: {
|
|
696
|
+
format,
|
|
697
|
+
seedPackets: seed.length
|
|
698
|
+
} });
|
|
699
|
+
}
|
|
609
700
|
this.logger?.info("frame-handle plane: shm decoder session created", { meta: {
|
|
610
701
|
format,
|
|
611
702
|
codec: info.codec,
|