@camstack/addon-pipeline 1.2.135 → 1.2.137
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{addon-utils-CKf8kbwh.js → addon-utils-C6b5Tnia.js} +3 -3
- package/dist/audio-analyzer/index.js +5 -5
- package/dist/audio-analyzer/index.mjs +4 -4
- package/dist/detection-pipeline/index.js +55 -17
- package/dist/detection-pipeline/index.mjs +53 -15
- package/dist/{dist-BzLiQ8Ue.js → dist-4EMKvxCp.js} +275 -25
- package/dist/{dist-C1dXLLQq.mjs → dist-g0Or5_pR.mjs} +270 -26
- package/dist/{event-loop-stall-monitor-CU-xxX5c.mjs → event-loop-stall-monitor-CskL9Lgu.mjs} +1 -1
- package/dist/{event-loop-stall-monitor-DjV4Bow7.js → event-loop-stall-monitor-DDm07Ei6.js} +1 -1
- package/dist/{lazy-sharp-DxWmhTqk.js → lazy-sharp-BlA9PxWk.js} +3 -3
- package/dist/motion-wasm/index.js +3 -3
- package/dist/motion-wasm/index.mjs +3 -3
- package/dist/node-BOHn8gpk.js +721 -0
- package/dist/node-Bo1g8hLO.mjs +690 -0
- package/dist/pipeline-runner/index.js +37 -11
- package/dist/pipeline-runner/index.mjs +37 -11
- package/dist/{process-memory-CZK3D3kM.js → process-memory-C3084DOt.js} +2 -2
- package/dist/{process-memory-CaEwi5Cd.mjs → process-memory-DmrHWybb.mjs} +2 -2
- package/dist/recorder/index.js +382 -35
- package/dist/recorder/index.mjs +382 -35
- package/dist/{segment-demux-js-BbpqygHL.js → segment-demux-js-BrQ_A0oi.js} +1 -1
- package/dist/{segment-demux-js-njRO85gd.mjs → segment-demux-js-C-a2pghM.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +2 -2
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C14Qqrqn.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-AH6OJ3hJ.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CQyiEd0l.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-BrTM_tsb.mjs +26 -0
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-kjsveCjG.mjs → hostInit-XNyn-FTM.mjs} +3 -3
- package/dist/stream-broker/index.js +88 -515
- package/dist/stream-broker/index.mjs +85 -510
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-DvTZBvh2.js → worker-protocol-BLfaD5uC.js} +1 -1
- package/dist/{worker-protocol-DxsVx0G7.mjs → worker-protocol-vc8WWhjx.mjs} +1 -1
- package/package.json +17 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DqplWP1i.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-DKMXG57X.mjs +0 -26
- package/dist/{addon-utils-CgBCF-mE.mjs → addon-utils-C8KTuPdE.mjs} +2 -2
- package/dist/{lazy-sharp-BtT5x-qP.mjs → lazy-sharp-DXsqwpph.mjs} +1 -1
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $ as loadContributionCapability, $t as union, At as CAM_PROFILE_ORDER, Bt as parseProfileBrokerId, C as RATE_CONTROL_RELAXED, Ct as errMsg, D as RingBuffer, Dt as invocationFromEncodeProfile, Et as buildFfmpegArgs$1, F as createHwAccelCache, Ft as isEvent, G as egressTranscodeSharingKey, Gt as boolean, I as createLogChannelsProvider, It as makeProfileBrokerId, Jt as literal, K as egressTransportFromRequest, Kt as discriminatedUnion, Lt as makeSourceBrokerId, Mt as DeviceType, N as batteryCapability, Nt as createEvent, Ot as isSoftwareDecode, P as cameraStreamsCapability, Qt as string, R as declareLogChannel, Rt as nodePin, St as webrtcSessionCapability, Ut as _enum, V as deriveBatteryPresence, Wt as array, Xt as object, Yt as number, Zt as record, bt as streamBrokerCapability, en as EventCategory, et as logChannelsCapability, g as EncodeProfileSchema, ht as resolveEgressDecodeHwAccel, i as BatteryStatusSchema, jt as DeviceFeature, k as addonWidgetsSourceCapability, kt as BaseAddon, nt as maskUrlCredentials, w as RATE_CONTROL_TIGHT, wt as AUDIO_PRESETS } from "../dist-g0Or5_pR.mjs";
|
|
3
|
+
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-Bo1g8hLO.mjs";
|
|
3
4
|
import { t as parseRestreamPath } from "../restream-intent-B4BXZra7.mjs";
|
|
4
5
|
import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
|
|
5
6
|
import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DMolfhsP.mjs";
|
|
6
|
-
import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-
|
|
7
|
+
import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-C-a2pghM.mjs";
|
|
7
8
|
import { createRequire } from "node:module";
|
|
8
9
|
import * as crypto$1 from "node:crypto";
|
|
9
10
|
import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
12
13
|
import * as path$1 from "node:path";
|
|
13
14
|
import { join } from "node:path";
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
18
|
import * as os from "node:os";
|
|
15
19
|
import { networkInterfaces, tmpdir } from "node:os";
|
|
16
|
-
import * as fs from "node:fs";
|
|
17
|
-
import { existsSync } from "node:fs";
|
|
18
20
|
import * as net$1 from "node:net";
|
|
19
21
|
import net from "node:net";
|
|
20
|
-
import { readFile } from "node:fs/promises";
|
|
21
22
|
import { createSocket } from "node:dgram";
|
|
22
23
|
import { Socket } from "net";
|
|
23
24
|
import { once } from "events";
|
|
@@ -1261,6 +1262,9 @@ var FfmpegPushReader = class {
|
|
|
1261
1262
|
spawnFn;
|
|
1262
1263
|
callbacks;
|
|
1263
1264
|
logLabel;
|
|
1265
|
+
claimCost;
|
|
1266
|
+
/** The claim for the child currently running, if any. */
|
|
1267
|
+
costClaim = NO_COST_CLAIM;
|
|
1264
1268
|
child = null;
|
|
1265
1269
|
demux = null;
|
|
1266
1270
|
audioCodec;
|
|
@@ -1278,6 +1282,7 @@ var FfmpegPushReader = class {
|
|
|
1278
1282
|
this.spawnFn = options.spawnFn ?? spawn;
|
|
1279
1283
|
this.callbacks = callbacks;
|
|
1280
1284
|
this.logLabel = logLabel;
|
|
1285
|
+
this.claimCost = options.claimCost;
|
|
1281
1286
|
}
|
|
1282
1287
|
/** Spawn ffmpeg and pipe its MPEG-TS stdout into the demux. */
|
|
1283
1288
|
connect() {
|
|
@@ -1299,6 +1304,7 @@ var FfmpegPushReader = class {
|
|
|
1299
1304
|
return;
|
|
1300
1305
|
}
|
|
1301
1306
|
this.child = child;
|
|
1307
|
+
this.costClaim = this.claimCost?.(child.pid) ?? NO_COST_CLAIM;
|
|
1302
1308
|
child.stdin?.on("error", () => {});
|
|
1303
1309
|
child.stdout?.on("data", (chunk) => demux.push(chunk));
|
|
1304
1310
|
child.stderr?.on("data", (chunk) => this.appendStderr(chunk));
|
|
@@ -1392,6 +1398,8 @@ var FfmpegPushReader = class {
|
|
|
1392
1398
|
fireTeardown() {
|
|
1393
1399
|
if (this.toreDown) return;
|
|
1394
1400
|
this.toreDown = true;
|
|
1401
|
+
this.costClaim.release();
|
|
1402
|
+
this.costClaim = NO_COST_CLAIM;
|
|
1395
1403
|
this.callbacks.onTeardown?.();
|
|
1396
1404
|
}
|
|
1397
1405
|
/** SIGTERM, then SIGKILL if the child has not actually exited (gate on exit
|
|
@@ -4559,506 +4567,6 @@ function pushDimension(into, constraint, requested, known) {
|
|
|
4559
4567
|
reason: `source ${known} > requested ${requested}`
|
|
4560
4568
|
});
|
|
4561
4569
|
}
|
|
4562
|
-
/**
|
|
4563
|
-
* Fmp4FragmentPlane — a SUBSCRIBABLE fragmented-MP4 plane, fed by one
|
|
4564
|
-
* {@link import('./fmp4-box-splitter.js').Fmp4BoxSplitter}.
|
|
4565
|
-
*
|
|
4566
|
-
* ## Why a plane and not a callback
|
|
4567
|
-
*
|
|
4568
|
-
* The operator's requirement for HKSV was explicit: the live fMP4 source built
|
|
4569
|
-
* for it must be **dual-use**, so a HomeKit-triggered recording also lands in
|
|
4570
|
-
* CamStack as an additional videoclip source alongside the recorder and the NC
|
|
4571
|
-
* clip ring — *one fragmenter, two consumers; do not build an HKSV-only pipe*
|
|
4572
|
-
* (`docs/roadmap.md` item 4b). A single-callback pipe makes the second consumer
|
|
4573
|
-
* a second ffmpeg child of the same camera. So this is the same shape the
|
|
4574
|
-
* broker's other multi-consumer surfaces already have
|
|
4575
|
-
* (`AudioChunkPlane`, the push packet plane): N independent subscriptions over
|
|
4576
|
-
* one producer.
|
|
4577
|
-
*
|
|
4578
|
-
* **Nothing consumes it yet.** Phase 4 brings the HKSV delegate and phase 4b the
|
|
4579
|
-
* clip source; both are named here so the seam is not re-invented, and neither
|
|
4580
|
-
* is built.
|
|
4581
|
-
*
|
|
4582
|
-
* ## The init segment is RETAINED
|
|
4583
|
-
*
|
|
4584
|
-
* A subscriber that attaches mid-stream — the clip consumer joining an already
|
|
4585
|
-
* running HKSV session, which is the whole dual-use case — receives the
|
|
4586
|
-
* retained `ftyp`+`moov` as its first packet and then live fragments. Without
|
|
4587
|
-
* retention its fragments are undecodable and the failure looks like a codec
|
|
4588
|
-
* problem.
|
|
4589
|
-
*
|
|
4590
|
-
* ## A slow subscriber is CLOSED, never silently gapped
|
|
4591
|
-
*
|
|
4592
|
-
* `AudioChunkPlane` drops its oldest chunk on overflow, which for audio costs a
|
|
4593
|
-
* click. An fMP4 stream with a hole is not a shorter clip, it is a corrupt one:
|
|
4594
|
-
* `moof` sequence numbers jump, the consumer's demuxer desynchronises, and HKSV
|
|
4595
|
-
* shows a clip that fails to play with nothing anywhere saying why. So a
|
|
4596
|
-
* subscription whose queue overflows is ENDED with a reason, loudly, and the
|
|
4597
|
-
* other subscriptions are untouched.
|
|
4598
|
-
*
|
|
4599
|
-
* ## The PREBUFFER (phase 3)
|
|
4600
|
-
*
|
|
4601
|
-
* HKSV asks for context BEFORE the trigger — `CameraRecordingOptions.prebufferLength`
|
|
4602
|
-
* is a HAP-mandated minimum of 4000 ms — and a subscriber that attaches at the
|
|
4603
|
-
* motion edge has none. So the plane optionally retains the last few fragments
|
|
4604
|
-
* and replays them to a subscriber that asks for them.
|
|
4605
|
-
*
|
|
4606
|
-
* Three things this ring gets right, each of which is a measured fact rather
|
|
4607
|
-
* than a preference (see [D84](../../../../docs/decisions/adr-0084.md)):
|
|
4608
|
-
*
|
|
4609
|
-
* - **It is bounded by TIME *and* BYTES.** On the live fleet a 720p copy
|
|
4610
|
-
* fragment is ~255 KB and a 4K one is ~6.35 MB — a 25× spread over the same
|
|
4611
|
-
* window. A time-only bound is a per-camera RAM figure nobody can predict.
|
|
4612
|
-
* - **The window is measured on ARRIVAL, not parsed from `tfdt`.** The
|
|
4613
|
-
* splitter deliberately never computes a fragment's duration (a second
|
|
4614
|
-
* opinion about a fact the muxer owns), and a prebuffer cares about how long
|
|
4615
|
-
* ago the bytes turned up, which is exactly what arrival time answers.
|
|
4616
|
-
* - **A replay is not backlog.** A subscriber taking N retained fragments gets
|
|
4617
|
-
* its queue capacity raised by N for them, because closing a subscriber as a
|
|
4618
|
-
* slow consumer for the prebuffer it explicitly asked for would be the
|
|
4619
|
-
* stupidest possible failure — and, with `DEFAULT_QUEUE_CAPACITY` of 4 and a
|
|
4620
|
-
* ring of 4, the guaranteed one.
|
|
4621
|
-
*
|
|
4622
|
-
* ## `isLast`
|
|
4623
|
-
*
|
|
4624
|
-
* hap-nodejs requires the delegate to mark exactly one `RecordingPacket` with
|
|
4625
|
-
* `isLast` — a generator that finishes without it produces the twelve-second
|
|
4626
|
-
* timeout loop [D50](../../../../../docs/decisions/adr-0050.md) deleted. The
|
|
4627
|
-
* plane therefore computes it at DELIVERY time: a packet is last when the plane
|
|
4628
|
-
* has ended and nothing remains queued behind it. A subscription that ends
|
|
4629
|
-
* having delivered NOTHING says so through {@link Fmp4Subscription.delivered};
|
|
4630
|
-
* the future delegate must not open an HDS stream it cannot feed.
|
|
4631
|
-
*/
|
|
4632
|
-
var DEFAULT_QUEUE_CAPACITY = 4;
|
|
4633
|
-
var Fmp4FragmentPlane = class {
|
|
4634
|
-
logger;
|
|
4635
|
-
prebuffer;
|
|
4636
|
-
now;
|
|
4637
|
-
subscriptions = /* @__PURE__ */ new Map();
|
|
4638
|
-
/** The last init unit seen, handed to every later subscriber. */
|
|
4639
|
-
retainedInit = null;
|
|
4640
|
-
ended = false;
|
|
4641
|
-
/** Oldest first. Empty unless {@link Fmp4PrebufferOptions} was supplied. */
|
|
4642
|
-
ring = [];
|
|
4643
|
-
ringBytes = 0;
|
|
4644
|
-
constructor(logger, prebuffer, now = Date.now) {
|
|
4645
|
-
this.logger = logger;
|
|
4646
|
-
this.prebuffer = prebuffer;
|
|
4647
|
-
this.now = now;
|
|
4648
|
-
}
|
|
4649
|
-
get subscriberCount() {
|
|
4650
|
-
return this.subscriptions.size;
|
|
4651
|
-
}
|
|
4652
|
-
/** True once {@link end} has been called — no further units are accepted. */
|
|
4653
|
-
get isEnded() {
|
|
4654
|
-
return this.ended;
|
|
4655
|
-
}
|
|
4656
|
-
/** What the prebuffer ring holds right now. All zeroes when disabled. */
|
|
4657
|
-
prebufferStats() {
|
|
4658
|
-
const oldest = this.ring[0];
|
|
4659
|
-
return {
|
|
4660
|
-
fragments: this.ring.length,
|
|
4661
|
-
bytes: this.ringBytes,
|
|
4662
|
-
spanMs: oldest === void 0 ? 0 : this.now() - oldest.arrivedAt
|
|
4663
|
-
};
|
|
4664
|
-
}
|
|
4665
|
-
subscribe(input) {
|
|
4666
|
-
const replay = input.withPrebuffer === true ? this.trimmedRing() : [];
|
|
4667
|
-
const requested = Math.max(1, input.queueCapacity ?? DEFAULT_QUEUE_CAPACITY);
|
|
4668
|
-
const sub = {
|
|
4669
|
-
id: `fmp4-${randomUUID()}`,
|
|
4670
|
-
tag: input.tag,
|
|
4671
|
-
subscribedAt: this.now(),
|
|
4672
|
-
capacity: requested + replay.length,
|
|
4673
|
-
queue: [],
|
|
4674
|
-
delivered: 0,
|
|
4675
|
-
closedReason: null,
|
|
4676
|
-
wake: null,
|
|
4677
|
-
iterating: false
|
|
4678
|
-
};
|
|
4679
|
-
this.subscriptions.set(sub.id, sub);
|
|
4680
|
-
if (this.retainedInit !== null) this.enqueue(sub, this.retainedInit);
|
|
4681
|
-
for (const retained of replay) this.enqueue(sub, retained.unit);
|
|
4682
|
-
if (this.ended) this.closeSubscription(sub, "ended");
|
|
4683
|
-
this.logger?.info("fmp4 plane: subscribed", { meta: {
|
|
4684
|
-
subscriptionId: sub.id,
|
|
4685
|
-
tag: sub.tag,
|
|
4686
|
-
hasRetainedInit: this.retainedInit !== null,
|
|
4687
|
-
prebufferFragments: replay.length,
|
|
4688
|
-
prebufferBytes: replay.reduce((n, r) => n + r.unit.data.length, 0)
|
|
4689
|
-
} });
|
|
4690
|
-
return this.facade(sub);
|
|
4691
|
-
}
|
|
4692
|
-
/**
|
|
4693
|
-
* Fan one splitter unit out. An `init` REPLACES the retained one — ffmpeg
|
|
4694
|
-
* emits exactly one per child, and a second means the child was respawned, in
|
|
4695
|
-
* which case the old one describes a stream that no longer exists.
|
|
4696
|
-
*/
|
|
4697
|
-
publish(unit) {
|
|
4698
|
-
if (this.ended) return;
|
|
4699
|
-
if (unit.kind === "init") {
|
|
4700
|
-
this.retainedInit = unit;
|
|
4701
|
-
this.ring.length = 0;
|
|
4702
|
-
this.ringBytes = 0;
|
|
4703
|
-
} else this.retain(unit);
|
|
4704
|
-
for (const sub of this.subscriptions.values()) {
|
|
4705
|
-
if (sub.closedReason !== null) continue;
|
|
4706
|
-
this.enqueue(sub, unit);
|
|
4707
|
-
}
|
|
4708
|
-
}
|
|
4709
|
-
/**
|
|
4710
|
-
* The producer stopped. Every subscriber drains what it holds; its final
|
|
4711
|
-
* packet carries `isLast`, and its generator then completes.
|
|
4712
|
-
*/
|
|
4713
|
-
end(reason = "producer ended") {
|
|
4714
|
-
if (this.ended) return;
|
|
4715
|
-
this.ended = true;
|
|
4716
|
-
this.logger?.info("fmp4 plane: ended", { meta: {
|
|
4717
|
-
reason,
|
|
4718
|
-
subscribers: this.subscriptions.size
|
|
4719
|
-
} });
|
|
4720
|
-
for (const sub of this.subscriptions.values()) if (sub.closedReason === null) this.closeSubscription(sub, "ended");
|
|
4721
|
-
}
|
|
4722
|
-
listSubscribers() {
|
|
4723
|
-
return [...this.subscriptions.values()].map((s) => ({
|
|
4724
|
-
tag: s.tag,
|
|
4725
|
-
subscribedAt: s.subscribedAt,
|
|
4726
|
-
delivered: s.delivered,
|
|
4727
|
-
closedReason: s.closedReason
|
|
4728
|
-
}));
|
|
4729
|
-
}
|
|
4730
|
-
/** End and forget everything. Idempotent. */
|
|
4731
|
-
dispose() {
|
|
4732
|
-
this.end("disposed");
|
|
4733
|
-
this.subscriptions.clear();
|
|
4734
|
-
this.retainedInit = null;
|
|
4735
|
-
this.ring.length = 0;
|
|
4736
|
-
this.ringBytes = 0;
|
|
4737
|
-
}
|
|
4738
|
-
/**
|
|
4739
|
-
* Add one fragment to the ring and evict from the front until BOTH bounds
|
|
4740
|
-
* hold. Eviction is oldest-first, which is the one place in this file where
|
|
4741
|
-
* dropping is correct: the ring is context, not stream — nobody is mid-decode
|
|
4742
|
-
* on it, and a subscriber only ever receives a contiguous tail of it.
|
|
4743
|
-
*/
|
|
4744
|
-
retain(unit) {
|
|
4745
|
-
const prebuffer = this.prebuffer;
|
|
4746
|
-
if (prebuffer === void 0) return;
|
|
4747
|
-
const arrivedAt = this.now();
|
|
4748
|
-
this.ring.push({
|
|
4749
|
-
unit,
|
|
4750
|
-
arrivedAt
|
|
4751
|
-
});
|
|
4752
|
-
this.ringBytes += unit.data.length;
|
|
4753
|
-
const cutoff = arrivedAt - prebuffer.windowMs;
|
|
4754
|
-
while (this.ring.length > 0) {
|
|
4755
|
-
const oldest = this.ring[0];
|
|
4756
|
-
if (oldest === void 0) break;
|
|
4757
|
-
const tooOld = oldest.arrivedAt < cutoff;
|
|
4758
|
-
const tooBig = this.ringBytes > prebuffer.maxBytes;
|
|
4759
|
-
if (!tooOld && !tooBig || this.ring.length === 1) break;
|
|
4760
|
-
this.ring.shift();
|
|
4761
|
-
this.ringBytes -= oldest.unit.data.length;
|
|
4762
|
-
}
|
|
4763
|
-
}
|
|
4764
|
-
/**
|
|
4765
|
-
* The ring as a subscriber should receive it — window applied AT SUBSCRIBE
|
|
4766
|
-
* time, not only at publish time. A camera that went quiet keeps its last
|
|
4767
|
-
* fragment in the ring indefinitely (see the never-evict-the-newest rule),
|
|
4768
|
-
* and replaying a 40-second-old fragment as "prebuffer" would put stale video
|
|
4769
|
-
* at the head of a clip iOS presents as the moment of the event.
|
|
4770
|
-
*/
|
|
4771
|
-
trimmedRing() {
|
|
4772
|
-
const prebuffer = this.prebuffer;
|
|
4773
|
-
if (prebuffer === void 0) return [];
|
|
4774
|
-
const cutoff = this.now() - prebuffer.windowMs;
|
|
4775
|
-
return this.ring.filter((r) => r.arrivedAt >= cutoff);
|
|
4776
|
-
}
|
|
4777
|
-
enqueue(sub, unit) {
|
|
4778
|
-
if (sub.queue.length >= sub.capacity) {
|
|
4779
|
-
this.logger?.warn("fmp4 plane: subscriber fell behind — CLOSING it rather than gapping it", { meta: {
|
|
4780
|
-
subscriptionId: sub.id,
|
|
4781
|
-
tag: sub.tag,
|
|
4782
|
-
capacity: sub.capacity,
|
|
4783
|
-
delivered: sub.delivered
|
|
4784
|
-
} });
|
|
4785
|
-
this.closeSubscription(sub, "slow-consumer");
|
|
4786
|
-
return;
|
|
4787
|
-
}
|
|
4788
|
-
sub.queue.push({
|
|
4789
|
-
kind: unit.kind,
|
|
4790
|
-
data: unit.data,
|
|
4791
|
-
sequence: unit.sequence,
|
|
4792
|
-
isLast: false
|
|
4793
|
-
});
|
|
4794
|
-
this.wake(sub);
|
|
4795
|
-
}
|
|
4796
|
-
closeSubscription(sub, reason) {
|
|
4797
|
-
if (sub.closedReason !== null) return;
|
|
4798
|
-
sub.closedReason = reason;
|
|
4799
|
-
if (reason === "slow-consumer") sub.queue.length = 0;
|
|
4800
|
-
this.wake(sub);
|
|
4801
|
-
}
|
|
4802
|
-
wake(sub) {
|
|
4803
|
-
const resume = sub.wake;
|
|
4804
|
-
sub.wake = null;
|
|
4805
|
-
resume?.();
|
|
4806
|
-
}
|
|
4807
|
-
facade(sub) {
|
|
4808
|
-
const plane = this;
|
|
4809
|
-
return {
|
|
4810
|
-
id: sub.id,
|
|
4811
|
-
tag: sub.tag,
|
|
4812
|
-
get delivered() {
|
|
4813
|
-
return sub.delivered;
|
|
4814
|
-
},
|
|
4815
|
-
get closedReason() {
|
|
4816
|
-
return sub.closedReason;
|
|
4817
|
-
},
|
|
4818
|
-
packets: () => plane.iterate(sub),
|
|
4819
|
-
release: () => {
|
|
4820
|
-
plane.closeSubscription(sub, "released");
|
|
4821
|
-
plane.subscriptions.delete(sub.id);
|
|
4822
|
-
}
|
|
4823
|
-
};
|
|
4824
|
-
}
|
|
4825
|
-
async *iterate(sub) {
|
|
4826
|
-
if (sub.iterating) throw new Error(`fmp4 plane: subscription ${sub.tag} is already being consumed — take a second subscription`);
|
|
4827
|
-
sub.iterating = true;
|
|
4828
|
-
for (;;) {
|
|
4829
|
-
const next = sub.queue.shift();
|
|
4830
|
-
if (next === void 0) {
|
|
4831
|
-
if (sub.closedReason !== null) return;
|
|
4832
|
-
await new Promise((resolve) => {
|
|
4833
|
-
sub.wake = resolve;
|
|
4834
|
-
});
|
|
4835
|
-
continue;
|
|
4836
|
-
}
|
|
4837
|
-
const isLast = sub.closedReason === "ended" && sub.queue.length === 0;
|
|
4838
|
-
sub.delivered += 1;
|
|
4839
|
-
yield {
|
|
4840
|
-
...next,
|
|
4841
|
-
isLast
|
|
4842
|
-
};
|
|
4843
|
-
if (isLast) return;
|
|
4844
|
-
}
|
|
4845
|
-
}
|
|
4846
|
-
};
|
|
4847
|
-
var DEFAULT_FIRST_UNIT_TIMEOUT_MS = 12e3;
|
|
4848
|
-
/** Heartbeat cadence — ~2 minutes of 4 s fragments. */
|
|
4849
|
-
var FRAGMENT_LOG_EVERY = 30;
|
|
4850
|
-
var Fmp4FragmentChild = class {
|
|
4851
|
-
deps;
|
|
4852
|
-
args;
|
|
4853
|
-
child = null;
|
|
4854
|
-
splitter = new Fmp4BoxSplitter();
|
|
4855
|
-
stopped = false;
|
|
4856
|
-
unitsOut = 0;
|
|
4857
|
-
activeHwAccel = null;
|
|
4858
|
-
constructor(deps, args) {
|
|
4859
|
-
this.deps = deps;
|
|
4860
|
-
this.args = args;
|
|
4861
|
-
}
|
|
4862
|
-
/** Spawn, and resolve once the INIT segment has been cut out of stdout. */
|
|
4863
|
-
async start() {
|
|
4864
|
-
const requested = this.args.invocation.decodeHwAccel;
|
|
4865
|
-
this.activeHwAccel = requested;
|
|
4866
|
-
try {
|
|
4867
|
-
await this.spawnAttempt(requested);
|
|
4868
|
-
return;
|
|
4869
|
-
} catch (err) {
|
|
4870
|
-
if (this.stopped) throw err;
|
|
4871
|
-
if (requested === null || isSoftwareDecode(requested)) throw err;
|
|
4872
|
-
this.deps.logger.warn("fmp4 fragment child: hardware decode produced NO fragment — retrying in SOFTWARE", {
|
|
4873
|
-
tags: { deviceId: this.args.deviceId },
|
|
4874
|
-
meta: {
|
|
4875
|
-
sourceId: this.args.sourceId,
|
|
4876
|
-
decodeHwAccel: requested,
|
|
4877
|
-
error: errMsg(err)
|
|
4878
|
-
}
|
|
4879
|
-
});
|
|
4880
|
-
this.killChild();
|
|
4881
|
-
this.splitter = new Fmp4BoxSplitter();
|
|
4882
|
-
this.activeHwAccel = null;
|
|
4883
|
-
await this.spawnAttempt(null);
|
|
4884
|
-
}
|
|
4885
|
-
}
|
|
4886
|
-
/** The backend the child ACTUALLY ran with — `null` for software. */
|
|
4887
|
-
activeDecodeHwAccel() {
|
|
4888
|
-
const value = this.activeHwAccel;
|
|
4889
|
-
return value === null || value === "none" || value === "copy" ? null : value;
|
|
4890
|
-
}
|
|
4891
|
-
/** Kill ffmpeg and end the plane. Idempotent. */
|
|
4892
|
-
async stop() {
|
|
4893
|
-
if (this.stopped) return;
|
|
4894
|
-
this.stopped = true;
|
|
4895
|
-
this.killChild();
|
|
4896
|
-
this.args.plane.end("the fragment child stopped");
|
|
4897
|
-
}
|
|
4898
|
-
spawnAttempt(decodeHwAccel) {
|
|
4899
|
-
const args = buildFfmpegArgs$1({
|
|
4900
|
-
...this.args.invocation,
|
|
4901
|
-
decodeHwAccel,
|
|
4902
|
-
sink: {
|
|
4903
|
-
kind: "stdout",
|
|
4904
|
-
container: "mp4",
|
|
4905
|
-
fragmentMs: this.args.fragmentMs
|
|
4906
|
-
}
|
|
4907
|
-
});
|
|
4908
|
-
this.deps.logger.info("fmp4 fragment child: spawning ffmpeg", {
|
|
4909
|
-
tags: { deviceId: this.args.deviceId },
|
|
4910
|
-
meta: {
|
|
4911
|
-
sourceId: this.args.sourceId,
|
|
4912
|
-
fragmentMs: this.args.fragmentMs,
|
|
4913
|
-
decodeHwAccel: decodeHwAccel ?? "software",
|
|
4914
|
-
argv: args.join(" ")
|
|
4915
|
-
}
|
|
4916
|
-
});
|
|
4917
|
-
return new Promise((resolve, reject) => {
|
|
4918
|
-
const child = this.deps.spawnFn(this.deps.ffmpegBinaryPath, args, { stdio: [
|
|
4919
|
-
"ignore",
|
|
4920
|
-
"pipe",
|
|
4921
|
-
"pipe"
|
|
4922
|
-
] });
|
|
4923
|
-
this.child = child;
|
|
4924
|
-
let settled = false;
|
|
4925
|
-
/**
|
|
4926
|
-
* This attempt FAILED. Set before the kill, because SIGTERM makes the
|
|
4927
|
-
* child exit and that exit must not be reported as a death: the retry —
|
|
4928
|
-
* or the caller's rejection — already owns what happens next. Without it
|
|
4929
|
-
* the timeout path ends the plane the software retry is about to fill,
|
|
4930
|
-
* and the consumer sees a stream that stopped for no reason. A "which
|
|
4931
|
-
* spawn is current" counter does NOT cover this: the retry has not been
|
|
4932
|
-
* spawned when the kill's exit arrives.
|
|
4933
|
-
*/
|
|
4934
|
-
let failed = false;
|
|
4935
|
-
/**
|
|
4936
|
-
* This attempt is still the live producer: it has not failed (a failure
|
|
4937
|
-
* hands ownership to the retry, or to the caller's rejection) and nothing
|
|
4938
|
-
* has stopped the child. Those two cover every way an attempt stops being
|
|
4939
|
-
* current — `start` only respawns after a rejection.
|
|
4940
|
-
*/
|
|
4941
|
-
const isCurrent = () => !this.stopped && !failed;
|
|
4942
|
-
const timeoutMs = this.deps.firstUnitTimeoutMs ?? DEFAULT_FIRST_UNIT_TIMEOUT_MS;
|
|
4943
|
-
const settle = (fail) => {
|
|
4944
|
-
if (settled) return;
|
|
4945
|
-
settled = true;
|
|
4946
|
-
clearTimeout(timer);
|
|
4947
|
-
if (fail) {
|
|
4948
|
-
failed = true;
|
|
4949
|
-
reject(fail);
|
|
4950
|
-
} else resolve();
|
|
4951
|
-
};
|
|
4952
|
-
const timer = setTimeout(() => {
|
|
4953
|
-
settle(/* @__PURE__ */ new Error(`fmp4 fragment child: no fragment within ${timeoutMs}ms`));
|
|
4954
|
-
this.killChild();
|
|
4955
|
-
}, timeoutMs);
|
|
4956
|
-
timer.unref?.();
|
|
4957
|
-
child.stdout?.on("data", (chunk) => {
|
|
4958
|
-
for (const unit of this.splitter.push(chunk)) {
|
|
4959
|
-
this.unitsOut += 1;
|
|
4960
|
-
this.args.plane.publish(unit);
|
|
4961
|
-
if (unit.kind === "init") {
|
|
4962
|
-
this.deps.logger.info("fmp4 fragment child: INIT segment cut", {
|
|
4963
|
-
tags: { deviceId: this.args.deviceId },
|
|
4964
|
-
meta: {
|
|
4965
|
-
sourceId: this.args.sourceId,
|
|
4966
|
-
bytes: unit.data.length
|
|
4967
|
-
}
|
|
4968
|
-
});
|
|
4969
|
-
settle();
|
|
4970
|
-
} else if (this.unitsOut % FRAGMENT_LOG_EVERY === 0) this.deps.logger.info("fmp4 fragment child: fragments still flowing", {
|
|
4971
|
-
tags: { deviceId: this.args.deviceId },
|
|
4972
|
-
meta: {
|
|
4973
|
-
sourceId: this.args.sourceId,
|
|
4974
|
-
unitsOut: this.unitsOut,
|
|
4975
|
-
bytes: unit.data.length,
|
|
4976
|
-
subscribers: this.args.plane.subscriberCount
|
|
4977
|
-
}
|
|
4978
|
-
});
|
|
4979
|
-
}
|
|
4980
|
-
const fault = this.splitter.fault;
|
|
4981
|
-
if (fault !== null) this.onFault(fault, settled, isCurrent(), settle);
|
|
4982
|
-
});
|
|
4983
|
-
child.stderr?.setEncoding("utf8");
|
|
4984
|
-
child.stderr?.on("data", (line) => {
|
|
4985
|
-
this.deps.logger.debug("fmp4 fragment child ffmpeg", {
|
|
4986
|
-
tags: { deviceId: this.args.deviceId },
|
|
4987
|
-
meta: {
|
|
4988
|
-
sourceId: this.args.sourceId,
|
|
4989
|
-
line: line.trim()
|
|
4990
|
-
}
|
|
4991
|
-
});
|
|
4992
|
-
});
|
|
4993
|
-
child.once("error", (err) => {
|
|
4994
|
-
if (!settled) {
|
|
4995
|
-
settle(err);
|
|
4996
|
-
return;
|
|
4997
|
-
}
|
|
4998
|
-
if (!isCurrent()) return;
|
|
4999
|
-
this.args.plane.end("the fragment child errored");
|
|
5000
|
-
this.deps.onChildExit?.(err);
|
|
5001
|
-
});
|
|
5002
|
-
child.once("exit", (code, signal) => {
|
|
5003
|
-
if (!settled) {
|
|
5004
|
-
settle(/* @__PURE__ */ new Error(`fmp4 fragment child: ffmpeg exited before any fragment (code=${code} signal=${signal})`));
|
|
5005
|
-
return;
|
|
5006
|
-
}
|
|
5007
|
-
if (!isCurrent()) return;
|
|
5008
|
-
const error = /* @__PURE__ */ new Error(`fmp4 fragment child: ffmpeg exited while live (code=${code} signal=${signal})`);
|
|
5009
|
-
this.deps.logger.warn("fmp4 fragment child: ffmpeg exited while live", {
|
|
5010
|
-
tags: { deviceId: this.args.deviceId },
|
|
5011
|
-
meta: {
|
|
5012
|
-
sourceId: this.args.sourceId,
|
|
5013
|
-
code,
|
|
5014
|
-
signal,
|
|
5015
|
-
unitsOut: this.unitsOut
|
|
5016
|
-
}
|
|
5017
|
-
});
|
|
5018
|
-
this.args.plane.end("the fragment child exited");
|
|
5019
|
-
this.deps.onChildExit?.(error);
|
|
5020
|
-
});
|
|
5021
|
-
});
|
|
5022
|
-
}
|
|
5023
|
-
/**
|
|
5024
|
-
* The byte stream stopped being splittable. Not recoverable — the splitter
|
|
5025
|
-
* cannot resynchronise mid-box — so the child is a corpse and every consumer
|
|
5026
|
-
* has to be told, loudly, with the reason.
|
|
5027
|
-
*/
|
|
5028
|
-
onFault(reason, wasLive, current, settle) {
|
|
5029
|
-
const error = /* @__PURE__ */ new Error(`fmp4 fragment child: ${reason}`);
|
|
5030
|
-
this.deps.logger.error("fmp4 fragment child: the ffmpeg output stopped parsing as fMP4", {
|
|
5031
|
-
tags: { deviceId: this.args.deviceId },
|
|
5032
|
-
meta: {
|
|
5033
|
-
sourceId: this.args.sourceId,
|
|
5034
|
-
unitsOut: this.unitsOut,
|
|
5035
|
-
interstitial: this.splitter.discardedInterstitialTypes,
|
|
5036
|
-
reason
|
|
5037
|
-
}
|
|
5038
|
-
});
|
|
5039
|
-
this.killChild();
|
|
5040
|
-
settle(error);
|
|
5041
|
-
if (wasLive && current) {
|
|
5042
|
-
this.args.plane.end("the fragment child produced unsplittable output");
|
|
5043
|
-
this.deps.onChildExit?.(error);
|
|
5044
|
-
}
|
|
5045
|
-
}
|
|
5046
|
-
killChild() {
|
|
5047
|
-
const child = this.child;
|
|
5048
|
-
this.child = null;
|
|
5049
|
-
if (child && !child.killed) try {
|
|
5050
|
-
child.kill("SIGTERM");
|
|
5051
|
-
} catch (err) {
|
|
5052
|
-
this.deps.logger.warn("fmp4 fragment child: kill error", {
|
|
5053
|
-
tags: { deviceId: this.args.deviceId },
|
|
5054
|
-
meta: {
|
|
5055
|
-
sourceId: this.args.sourceId,
|
|
5056
|
-
error: errMsg(err)
|
|
5057
|
-
}
|
|
5058
|
-
});
|
|
5059
|
-
}
|
|
5060
|
-
}
|
|
5061
|
-
};
|
|
5062
4570
|
//#endregion
|
|
5063
4571
|
//#region src/stream-broker/stream-broker/rtsp-host.ts
|
|
5064
4572
|
/**
|
|
@@ -5263,6 +4771,7 @@ var TranscodeEgress = class {
|
|
|
5263
4771
|
] });
|
|
5264
4772
|
this.child = child;
|
|
5265
4773
|
this.startedAtMs = Date.now();
|
|
4774
|
+
const costClaim = this.deps.claimCost?.(child.pid) ?? NO_COST_CLAIM;
|
|
5266
4775
|
let settled = false;
|
|
5267
4776
|
const timeoutMs = this.deps.firstDataTimeoutMs ?? DEFAULT_FIRST_DATA_TIMEOUT_MS;
|
|
5268
4777
|
const timer = setTimeout(() => {
|
|
@@ -5299,12 +4808,14 @@ var TranscodeEgress = class {
|
|
|
5299
4808
|
} });
|
|
5300
4809
|
});
|
|
5301
4810
|
child.once("error", (err) => {
|
|
4811
|
+
costClaim.release();
|
|
5302
4812
|
if (settled) return;
|
|
5303
4813
|
settled = true;
|
|
5304
4814
|
clearTimeout(timer);
|
|
5305
4815
|
reject(err);
|
|
5306
4816
|
});
|
|
5307
4817
|
child.once("exit", (code, signal) => {
|
|
4818
|
+
costClaim.release();
|
|
5308
4819
|
if (settled) {
|
|
5309
4820
|
if (this.stopped) return;
|
|
5310
4821
|
const error = /* @__PURE__ */ new Error(`transcode egress: ffmpeg exited while live (code=${code} signal=${signal})`);
|
|
@@ -5865,7 +5376,13 @@ var EgressTranscodeManager = class {
|
|
|
5865
5376
|
ffmpegBinaryPath: this.opts.ffmpegConfig().binaryPath,
|
|
5866
5377
|
spawnFn: this.opts.spawnFn ?? spawn,
|
|
5867
5378
|
acquireChannel: () => channel,
|
|
5868
|
-
...spec.onDied ? { onChildExit: spec.onDied } : {}
|
|
5379
|
+
...spec.onDied ? { onChildExit: spec.onDied } : {},
|
|
5380
|
+
claimCost: (pid) => this.opts.costRegistry?.claim({
|
|
5381
|
+
role: "transcode",
|
|
5382
|
+
deviceId: spec.deviceId,
|
|
5383
|
+
unit: spec.brokerId,
|
|
5384
|
+
pid
|
|
5385
|
+
}) ?? NO_COST_CLAIM
|
|
5869
5386
|
}, {
|
|
5870
5387
|
brokerId: spec.brokerId,
|
|
5871
5388
|
targetCodec: spec.targetCodec,
|
|
@@ -11371,6 +10888,14 @@ var StreamBroker = class StreamBroker {
|
|
|
11371
10888
|
* and a line without the tag cannot be grouped to answer it.
|
|
11372
10889
|
*/
|
|
11373
10890
|
numericDeviceId;
|
|
10891
|
+
/**
|
|
10892
|
+
* Where this broker records the ffmpeg ingest it spawns, so the stream-broker
|
|
10893
|
+
* addon reports its OWN per-camera cost through `load-contribution` instead
|
|
10894
|
+
* of leaving an observer to join a pid against a camera. Null until the
|
|
10895
|
+
* manager injects it; a broker without one still streams, and its ffmpeg is
|
|
10896
|
+
* simply unclaimed in the node's process snapshot.
|
|
10897
|
+
*/
|
|
10898
|
+
costRegistry = null;
|
|
11374
10899
|
_status = "idle";
|
|
11375
10900
|
source;
|
|
11376
10901
|
/**
|
|
@@ -11904,6 +11429,30 @@ var StreamBroker = class StreamBroker {
|
|
|
11904
11429
|
this.ffmpegBinaryPath = binaryPath;
|
|
11905
11430
|
}
|
|
11906
11431
|
/**
|
|
11432
|
+
* Inject the addon's child-cost registry. Injected rather than constructed
|
|
11433
|
+
* here because the registry is per-ADDON-PROCESS (it is what the addon's
|
|
11434
|
+
* `load-contribution` provider reads) while a broker is per-camera.
|
|
11435
|
+
*/
|
|
11436
|
+
setCostRegistry(registry) {
|
|
11437
|
+
this.costRegistry = registry;
|
|
11438
|
+
}
|
|
11439
|
+
/**
|
|
11440
|
+
* Claim one ffmpeg ingest generation for this camera, or nothing when the
|
|
11441
|
+
* camera cannot be named. Never a placeholder device id: an unnamed
|
|
11442
|
+
* per-camera claim is indistinguishable from a shared cost and would quietly
|
|
11443
|
+
* turn one camera's ffmpeg into everybody's.
|
|
11444
|
+
*/
|
|
11445
|
+
claimIngestCost(pid) {
|
|
11446
|
+
const deviceId = this.numericDeviceId;
|
|
11447
|
+
if (this.costRegistry === null || deviceId === void 0) return NO_COST_CLAIM;
|
|
11448
|
+
return this.costRegistry.claim({
|
|
11449
|
+
role: "streaming",
|
|
11450
|
+
deviceId,
|
|
11451
|
+
unit: this.deviceId,
|
|
11452
|
+
pid
|
|
11453
|
+
});
|
|
11454
|
+
}
|
|
11455
|
+
/**
|
|
11907
11456
|
* Inject a manager-owned closure that derives the current
|
|
11908
11457
|
* `StreamSource` from the live cam-stream registry. Every dial
|
|
11909
11458
|
* attempt (initial start, scheduled reconnect, demand-driven resume)
|
|
@@ -13519,7 +13068,8 @@ var StreamBroker = class StreamBroker {
|
|
|
13519
13068
|
const reader = new FfmpegSourceReader({
|
|
13520
13069
|
ffmpegPath: this.ffmpegBinaryPath,
|
|
13521
13070
|
args,
|
|
13522
|
-
...this.logger ? { logger: this.logger.child("ffmpeg-source") } : {}
|
|
13071
|
+
...this.logger ? { logger: this.logger.child("ffmpeg-source") } : {},
|
|
13072
|
+
claimCost: (pid) => this.claimIngestCost(pid)
|
|
13523
13073
|
}, {
|
|
13524
13074
|
onVideoCodec: (codec) => {
|
|
13525
13075
|
this.detectedCodec = codec;
|
|
@@ -15297,6 +14847,8 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
15297
14847
|
* that reclaims a refcount whose holder died without releasing.
|
|
15298
14848
|
*/
|
|
15299
14849
|
egressTranscodeManager = null;
|
|
14850
|
+
/** See {@link StreamBrokerManager.setCostRegistry}. */
|
|
14851
|
+
costRegistry = null;
|
|
15300
14852
|
/**
|
|
15301
14853
|
* Test/injection seam for the egress primitive's child. Production leaves it
|
|
15302
14854
|
* null and `EgressTranscodeManager` builds a real `TranscodeEgress` /
|
|
@@ -15339,6 +14891,15 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
15339
14891
|
this.rtspProvider = new RtspRestreamProviderImpl(this.rtspServer, logger.child("rtsp-restream"));
|
|
15340
14892
|
}
|
|
15341
14893
|
/**
|
|
14894
|
+
* The addon's child-cost registry, threaded down to every broker and to the
|
|
14895
|
+
* egress transcode manager so each spawn site claims its OWN child. Injected
|
|
14896
|
+
* (not constructed) because it is per-addon-process and is what the addon's
|
|
14897
|
+
* `load-contribution` provider reads.
|
|
14898
|
+
*/
|
|
14899
|
+
setCostRegistry(registry) {
|
|
14900
|
+
this.costRegistry = registry;
|
|
14901
|
+
}
|
|
14902
|
+
/**
|
|
15342
14903
|
* Lazily create the transcode manager wired with closures rather than
|
|
15343
14904
|
* direct refs so it only sees the surface it needs.
|
|
15344
14905
|
*/
|
|
@@ -15406,6 +14967,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
15406
14967
|
return entry ? { url: entry.url } : null;
|
|
15407
14968
|
},
|
|
15408
14969
|
ffmpegConfig: () => this.ffmpegConfig,
|
|
14970
|
+
...this.costRegistry === null ? {} : { costRegistry: this.costRegistry },
|
|
15409
14971
|
resolveDecodeHwAccel: (deviceId, override) => this.resolveEgressHwAccel(deviceId, override),
|
|
15410
14972
|
acquireChannel: (brokerId) => {
|
|
15411
14973
|
const restreamer = new RtspRestreamer(brokerId);
|
|
@@ -19056,6 +18618,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19056
18618
|
const broker = new StreamBroker(brokerId, this.buildDecoderCapApi(deviceId), brokerLogger, buildAudioCodecCapApi(this.api), this.localNodeId);
|
|
19057
18619
|
broker.setRestreamUrlResolver(() => this.rtspProvider.getEntry(brokerId)?.mutedUrl ?? null);
|
|
19058
18620
|
broker.setFfmpegBinaryPath(this.ffmpegConfig.binaryPath);
|
|
18621
|
+
if (this.costRegistry !== null) broker.setCostRegistry(this.costRegistry);
|
|
19059
18622
|
this.applyPreBufferConfig(broker, deviceId, camStreamId);
|
|
19060
18623
|
this.applyAudioMuteConfig(broker, deviceId);
|
|
19061
18624
|
this.applyJoinBurstBound(broker, deviceId, camStreamId);
|
|
@@ -29502,6 +29065,12 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
29502
29065
|
* diagnostic timer never keeps a runner from exiting. */
|
|
29503
29066
|
logChannels = null;
|
|
29504
29067
|
/**
|
|
29068
|
+
* The ffmpeg children this addon started, claimed at their spawn sites and
|
|
29069
|
+
* released at their exits. One registry per PROCESS: it dies with the
|
|
29070
|
+
* process, which is correct — those children were this process's children.
|
|
29071
|
+
*/
|
|
29072
|
+
costRegistry = new ChildCostRegistry();
|
|
29073
|
+
/**
|
|
29505
29074
|
* Snapshot-equality cache for the broker-metrics emit. Each
|
|
29506
29075
|
* broker emits a stats snapshot every BROKER_METRICS_SNAPSHOT_-
|
|
29507
29076
|
* INTERVAL_MS; for an idle broker (status=streaming, fps=0,
|
|
@@ -29587,11 +29156,16 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
29587
29156
|
capability: logChannelsCapability,
|
|
29588
29157
|
provider: this.logChannels
|
|
29589
29158
|
};
|
|
29159
|
+
const loadContributionRegistration = {
|
|
29160
|
+
capability: loadContributionCapability,
|
|
29161
|
+
provider: { list: () => this.costRegistry.contributions() }
|
|
29162
|
+
};
|
|
29590
29163
|
this.loopStallMonitor = new EventLoopStallMonitor({ onStall: (stallMs) => {
|
|
29591
29164
|
this.ctx?.logger?.warn?.("event-loop stall — every session in this process froze", { meta: { stallMs } });
|
|
29592
29165
|
} });
|
|
29593
29166
|
this.loopStallMonitor.start();
|
|
29594
29167
|
this.brokerManager = new StreamBrokerManager(void 0, this.ctx.logger);
|
|
29168
|
+
this.brokerManager.setCostRegistry(this.costRegistry);
|
|
29595
29169
|
this.brokerManager.setDefaultPreBufferSec(this.config.defaultPreBufferSec);
|
|
29596
29170
|
this.brokerManager.setDefaultJoinBurstMaxSpanMs(this.config.joinBurstMaxSpanMs);
|
|
29597
29171
|
this.brokerManager.setDefaultPreRollMs(this.config.preRollMs);
|
|
@@ -29944,7 +29518,8 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
29944
29518
|
capability: addonWidgetsSourceCapability,
|
|
29945
29519
|
provider: widgetsProvider
|
|
29946
29520
|
},
|
|
29947
|
-
logChannelsRegistration
|
|
29521
|
+
logChannelsRegistration,
|
|
29522
|
+
loadContributionRegistration
|
|
29948
29523
|
];
|
|
29949
29524
|
}
|
|
29950
29525
|
async onShutdown() {
|