@camstack/addon-pipeline 1.2.307 → 1.2.309
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/{default-detection-model-DgdiQtQb.js → default-detection-model-BZMS04Wn.js} +1 -1
- package/dist/{default-detection-model-BV9bIuOo.mjs → default-detection-model-ClQ1gcZ9.mjs} +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-DdVMFlJ6.mjs → dist-B3_sbBDM.mjs} +305 -48
- package/dist/{dist-CyJWk-_f.js → dist-BLf0YjAa.js} +310 -47
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-COu_tHy4.mjs → node-DJFKTJ0B.mjs} +1 -53
- package/dist/{node-HsWG5fGp.js → node-Vwf2PuIT.js} +1 -53
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-Bz3oIMzl.mjs → process-memory-BvwauBlX.mjs} +1 -1
- package/dist/{process-memory-TQXG7qrJ.js → process-memory-DGTVVYlX.js} +1 -1
- package/dist/recorder/index.js +147 -22
- package/dist/recorder/index.mjs +147 -22
- package/dist/{segment-demux-js-BrKa7tpr.js → segment-demux-js-BUAAuSVF.js} +22 -4
- package/dist/{segment-demux-js-CEJn4BR0.mjs → segment-demux-js-C0oz7IVm.mjs} +17 -5
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DdYUVQSA.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-7H0Oxf23.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C6EaWavG.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-BhO-szf8.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-D--a9rVM.mjs → hostInit-DEY-Dqqo.mjs} +2 -2
- package/dist/stream-broker/index.js +434 -111
- package/dist/stream-broker/index.mjs +434 -111
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DPHv0v3Q.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-CdKbuxqG.mjs +0 -26
|
@@ -3,12 +3,12 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_node = require("../node-
|
|
6
|
+
const require_dist = require("../dist-BLf0YjAa.js");
|
|
7
|
+
const require_node = require("../node-Vwf2PuIT.js");
|
|
8
8
|
const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
|
|
9
9
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
10
10
|
const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
|
|
11
|
-
const require_segment_demux_js = require("../segment-demux-js-
|
|
11
|
+
const require_segment_demux_js = require("../segment-demux-js-BUAAuSVF.js");
|
|
12
12
|
let node_os = require("node:os");
|
|
13
13
|
node_os = require_chunk.__toESM(node_os);
|
|
14
14
|
let node_crypto = require("node:crypto");
|
|
@@ -103,6 +103,86 @@ function createClientLogIngest(onEntries) {
|
|
|
103
103
|
});
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
|
+
var FfmpegReresolver = class {
|
|
107
|
+
deps;
|
|
108
|
+
inFlight = null;
|
|
109
|
+
lastAttemptAt = null;
|
|
110
|
+
constructor(deps) {
|
|
111
|
+
this.deps = deps;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Ask again, unless an attempt is running or one started inside the bound.
|
|
115
|
+
* Never throws; the outcome is logged either way.
|
|
116
|
+
*/
|
|
117
|
+
request(reason) {
|
|
118
|
+
if (this.inFlight !== null) return "in-flight";
|
|
119
|
+
const now = (this.deps.now ?? Date.now)();
|
|
120
|
+
const minIntervalMs = this.deps.minIntervalMs ?? 3e4;
|
|
121
|
+
if (this.lastAttemptAt !== null && now - this.lastAttemptAt < minIntervalMs) return "too-soon";
|
|
122
|
+
this.lastAttemptAt = now;
|
|
123
|
+
this.deps.logger.warn("stream-broker: re-resolving the pinned ffmpeg after a refusal (D540)", { meta: { reason } });
|
|
124
|
+
this.inFlight = this.attempt().finally(() => {
|
|
125
|
+
this.inFlight = null;
|
|
126
|
+
});
|
|
127
|
+
return "started";
|
|
128
|
+
}
|
|
129
|
+
/** The attempt in flight, for a caller (or a spec) that wants to wait. */
|
|
130
|
+
settled() {
|
|
131
|
+
return this.inFlight ?? Promise.resolve();
|
|
132
|
+
}
|
|
133
|
+
async attempt() {
|
|
134
|
+
try {
|
|
135
|
+
const binaryPath = await this.deps.resolve();
|
|
136
|
+
this.deps.apply(binaryPath);
|
|
137
|
+
this.deps.logger.info("stream-broker: pinned ffmpeg resolved on retry — children may spawn", { meta: { binaryPath } });
|
|
138
|
+
} catch (err) {
|
|
139
|
+
this.deps.logger.error("stream-broker: pinned ffmpeg still unavailable — every ffmpeg child keeps refusing", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* The broker addon's whole ffmpeg wiring, in one place a spec can drive: the
|
|
145
|
+
* boot-time resolution, the refusal handler on the manager, and the
|
|
146
|
+
* re-resolve that hands a later success back to the manager's config — which
|
|
147
|
+
* every live broker reads per dial.
|
|
148
|
+
*/
|
|
149
|
+
function wireBrokerFfmpeg(deps) {
|
|
150
|
+
const give = (binaryPath) => {
|
|
151
|
+
deps.host.setFfmpegConfig({
|
|
152
|
+
binaryPath,
|
|
153
|
+
hwAccel: deps.hwAccel(),
|
|
154
|
+
threadCount: 0
|
|
155
|
+
});
|
|
156
|
+
deps.onBinaryPath?.(binaryPath);
|
|
157
|
+
};
|
|
158
|
+
const reresolver = new FfmpegReresolver({
|
|
159
|
+
resolve: deps.resolve,
|
|
160
|
+
apply: give,
|
|
161
|
+
logger: deps.logger,
|
|
162
|
+
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
163
|
+
...deps.minIntervalMs !== void 0 ? { minIntervalMs: deps.minIntervalMs } : {}
|
|
164
|
+
});
|
|
165
|
+
deps.host.setFfmpegMissingHandler(() => {
|
|
166
|
+
reresolver.request("a broker ffmpeg child was refused");
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
load: async () => {
|
|
170
|
+
try {
|
|
171
|
+
const binaryPath = await deps.resolve();
|
|
172
|
+
give(binaryPath);
|
|
173
|
+
deps.logger.info("Loaded ffmpeg config", { meta: {
|
|
174
|
+
binaryPath,
|
|
175
|
+
hwAccel: deps.hwAccel()
|
|
176
|
+
} });
|
|
177
|
+
} catch (err) {
|
|
178
|
+
give(null);
|
|
179
|
+
deps.logger.error("stream-broker: could not resolve the pinned ffmpeg — every ffmpeg child on this node refuses until this is fixed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
request: (reason) => reresolver.request(reason),
|
|
183
|
+
settled: () => reresolver.settled()
|
|
184
|
+
};
|
|
185
|
+
}
|
|
106
186
|
//#endregion
|
|
107
187
|
//#region src/stream-broker/log-channels.ts
|
|
108
188
|
/**
|
|
@@ -5453,6 +5533,16 @@ function classifyDecodeHwAccel(setting) {
|
|
|
5453
5533
|
prefer: setting
|
|
5454
5534
|
};
|
|
5455
5535
|
}
|
|
5536
|
+
/**
|
|
5537
|
+
* The binary to spawn, or a THROW after an ERROR naming the camera and the
|
|
5538
|
+
* child that was refused. Never a PATH name (D540).
|
|
5539
|
+
*/
|
|
5540
|
+
function requireBrokerFfmpeg(binaryPath, ask) {
|
|
5541
|
+
if (binaryPath !== null) return binaryPath;
|
|
5542
|
+
ask.logger.error(`${ask.purpose} refused — no pinned ffmpeg on this node (D540)`, { ...ask.deviceId !== null ? { tags: { deviceId: ask.deviceId } } : {} });
|
|
5543
|
+
ask.onMissing?.();
|
|
5544
|
+
throw new Error(`${ask.purpose}: no pinned ffmpeg resolved on this node`);
|
|
5545
|
+
}
|
|
5456
5546
|
/** Narrow an audio target to a sidecar-capable codec, or `null` (copy/none). */
|
|
5457
5547
|
function toAudioSidecarCodec(audio) {
|
|
5458
5548
|
return audio === "aac" || audio === "opus" || audio === "pcmu" ? audio : null;
|
|
@@ -6570,13 +6660,19 @@ var EgressTranscodeManager = class {
|
|
|
6570
6660
|
}
|
|
6571
6661
|
createChild(spec) {
|
|
6572
6662
|
if (this.opts.createChild) return this.opts.createChild(spec);
|
|
6573
|
-
|
|
6574
|
-
|
|
6663
|
+
const ffmpegBinaryPath = requireBrokerFfmpeg(this.opts.ffmpegConfig().binaryPath, {
|
|
6664
|
+
logger: this.opts.logger,
|
|
6665
|
+
deviceId: spec.deviceId,
|
|
6666
|
+
purpose: "egress transcode",
|
|
6667
|
+
...this.opts.onFfmpegMissing === void 0 ? {} : { onMissing: this.opts.onFfmpegMissing }
|
|
6668
|
+
});
|
|
6669
|
+
if (spec.fragments) return this.createFragmentChild(spec, spec.fragments, ffmpegBinaryPath);
|
|
6670
|
+
if (spec.pushSink) return this.createPushChild(spec, spec.pushSink, ffmpegBinaryPath);
|
|
6575
6671
|
const channel = spec.channel;
|
|
6576
6672
|
if (channel === void 0) throw new Error("egress transcode: a dial child was created without a channel");
|
|
6577
6673
|
const egress = new TranscodeEgress({
|
|
6578
6674
|
logger: this.opts.logger.child("egress"),
|
|
6579
|
-
ffmpegBinaryPath
|
|
6675
|
+
ffmpegBinaryPath,
|
|
6580
6676
|
spawnFn: this.opts.spawnFn ?? node_child_process.spawn,
|
|
6581
6677
|
acquireChannel: () => channel,
|
|
6582
6678
|
...spec.onDied ? { onChildExit: spec.onDied } : {},
|
|
@@ -6608,10 +6704,10 @@ var EgressTranscodeManager = class {
|
|
|
6608
6704
|
* same shape as the other two transports; this wrapper is the adapter, not
|
|
6609
6705
|
* the logic. See the primitive's `ffmpeg/fmp4-fragment-child.ts`.
|
|
6610
6706
|
*/
|
|
6611
|
-
createFragmentChild(spec, fragments) {
|
|
6707
|
+
createFragmentChild(spec, fragments, ffmpegBinaryPath) {
|
|
6612
6708
|
const child = new require_node.Fmp4FragmentChild({
|
|
6613
6709
|
logger: this.opts.logger.child("egress-fmp4"),
|
|
6614
|
-
ffmpegBinaryPath
|
|
6710
|
+
ffmpegBinaryPath,
|
|
6615
6711
|
spawnFn: this.opts.spawnFn ?? node_child_process.spawn,
|
|
6616
6712
|
...this.opts.fragmentFirstUnitTimeoutMs === void 0 ? {} : { firstUnitTimeoutMs: this.opts.fragmentFirstUnitTimeoutMs },
|
|
6617
6713
|
...spec.onDied ? { onChildExit: spec.onDied } : {}
|
|
@@ -6661,7 +6757,7 @@ var EgressTranscodeManager = class {
|
|
|
6661
6757
|
* masked the same failure on the dial path with its own software retry, which
|
|
6662
6758
|
* is why only this transport was dark.
|
|
6663
6759
|
*/
|
|
6664
|
-
createPushChild(spec, sink) {
|
|
6760
|
+
createPushChild(spec, sink, ffmpegBinaryPath) {
|
|
6665
6761
|
const logger = this.opts.logger.child("egress-push");
|
|
6666
6762
|
const deviceId = spec.deviceId;
|
|
6667
6763
|
const timeoutMs = this.opts.pushFirstPacketTimeoutMs ?? DEFAULT_PUSH_FIRST_PACKET_TIMEOUT_MS;
|
|
@@ -6679,7 +6775,7 @@ var EgressTranscodeManager = class {
|
|
|
6679
6775
|
const makeReader = (input) => {
|
|
6680
6776
|
if (this.opts.createPushReader) return this.opts.createPushReader(input);
|
|
6681
6777
|
return new FfmpegPushReader({
|
|
6682
|
-
ffmpegPath:
|
|
6778
|
+
ffmpegPath: ffmpegBinaryPath,
|
|
6683
6779
|
args: [...input.args],
|
|
6684
6780
|
logger,
|
|
6685
6781
|
...this.opts.spawnFn ? { spawnFn: this.opts.spawnFn } : {}
|
|
@@ -6706,7 +6802,7 @@ var EgressTranscodeManager = class {
|
|
|
6706
6802
|
meta: {
|
|
6707
6803
|
brokerId: spec.brokerId,
|
|
6708
6804
|
camStreamId: spec.publishedCamStreamId,
|
|
6709
|
-
ffmpegPath:
|
|
6805
|
+
ffmpegPath: ffmpegBinaryPath,
|
|
6710
6806
|
decodeHwAccel: decodeHwAccel ?? "software",
|
|
6711
6807
|
argv: args.join(" ")
|
|
6712
6808
|
}
|
|
@@ -7125,7 +7221,7 @@ async function gifFromMp4(input) {
|
|
|
7125
7221
|
}
|
|
7126
7222
|
function runGif(input, args) {
|
|
7127
7223
|
return new Promise((resolve, reject) => {
|
|
7128
|
-
const proc = input.spawnFn(input.ffmpegPath
|
|
7224
|
+
const proc = input.spawnFn(input.ffmpegPath, [...args], { stdio: [
|
|
7129
7225
|
"pipe",
|
|
7130
7226
|
"pipe",
|
|
7131
7227
|
"pipe"
|
|
@@ -7989,7 +8085,7 @@ async function renderPreBufferClip(input) {
|
|
|
7989
8085
|
hevc,
|
|
7990
8086
|
sourceWidth: input.sourceWidth ?? 0
|
|
7991
8087
|
});
|
|
7992
|
-
const proc = input.spawn(input.ffmpegPath
|
|
8088
|
+
const proc = input.spawn(input.ffmpegPath, clipFfmpegArgs({
|
|
7993
8089
|
...input,
|
|
7994
8090
|
hevc,
|
|
7995
8091
|
copyVideo
|
|
@@ -12806,11 +12902,12 @@ var StreamBroker = class StreamBroker {
|
|
|
12806
12902
|
/** Active "ffmpeg parser" ingress reader when `source.ffmpegParser` is set. */
|
|
12807
12903
|
ffmpegSourceReader = null;
|
|
12808
12904
|
/**
|
|
12809
|
-
*
|
|
12810
|
-
*
|
|
12811
|
-
*
|
|
12905
|
+
* Where the ffmpeg-parser ingress reads the node's pinned ffmpeg — the
|
|
12906
|
+
* manager's LIVE config, asked at each dial rather than copied at creation,
|
|
12907
|
+
* so a broker created while a re-resolve was in flight still sees its result.
|
|
12908
|
+
* `null` — the ingress REFUSES by name; there is no PATH default (D540).
|
|
12812
12909
|
*/
|
|
12813
|
-
|
|
12910
|
+
ffmpegBinaryPathSource = () => null;
|
|
12814
12911
|
rtpDepacketizer = null;
|
|
12815
12912
|
/** Flag-only depacketizer for the `push-rtp` source-RTP pre-buffer path
|
|
12816
12913
|
* (sets keyframe/param-set flags without re-emitting AnnexB packets). */
|
|
@@ -13393,12 +13490,21 @@ var StreamBroker = class StreamBroker {
|
|
|
13393
13490
|
this.restreamUrlResolver = resolver;
|
|
13394
13491
|
}
|
|
13395
13492
|
/**
|
|
13396
|
-
* Inject
|
|
13397
|
-
*
|
|
13398
|
-
* system-config (`ffmpeg.binaryPath`). Absent = bare `'ffmpeg'` (PATH lookup).
|
|
13493
|
+
* Inject where the "ffmpeg parser" ingress ({@link startFfmpegSourceReader})
|
|
13494
|
+
* reads the node's pinned ffmpeg: the manager's live config, asked per dial.
|
|
13399
13495
|
*/
|
|
13400
|
-
|
|
13401
|
-
this.
|
|
13496
|
+
setFfmpegBinaryPathSource(source) {
|
|
13497
|
+
this.ffmpegBinaryPathSource = source;
|
|
13498
|
+
}
|
|
13499
|
+
/** The pinned ffmpeg this broker would spawn right now (`null` = none). */
|
|
13500
|
+
getFfmpegBinaryPath() {
|
|
13501
|
+
return this.ffmpegBinaryPathSource();
|
|
13502
|
+
}
|
|
13503
|
+
/** Told when the ingress was refused for want of the pinned ffmpeg. */
|
|
13504
|
+
ffmpegMissingHandler = null;
|
|
13505
|
+
/** Wire the manager's re-resolve request. */
|
|
13506
|
+
setFfmpegMissingHandler(handler) {
|
|
13507
|
+
this.ffmpegMissingHandler = handler;
|
|
13402
13508
|
}
|
|
13403
13509
|
/**
|
|
13404
13510
|
* Inject the addon's child-cost registry. Injected rather than constructed
|
|
@@ -15324,18 +15430,28 @@ var StreamBroker = class StreamBroker {
|
|
|
15324
15430
|
transport: source.ffmpegParser?.transport ?? "tcp"
|
|
15325
15431
|
} });
|
|
15326
15432
|
this.destroyFfmpegSourceReader();
|
|
15433
|
+
const ffmpegPath = this.ffmpegBinaryPathSource();
|
|
15434
|
+
if (ffmpegPath === null) {
|
|
15435
|
+
this.logger?.error("ffmpeg source reader refused — no pinned ffmpeg on this node (D540)", {
|
|
15436
|
+
...this.numericDeviceId === void 0 ? {} : { tags: { deviceId: this.numericDeviceId } },
|
|
15437
|
+
meta: { url: require_dist.maskUrlCredentials(source.url) }
|
|
15438
|
+
});
|
|
15439
|
+
this._status = "error";
|
|
15440
|
+
this.ffmpegMissingHandler?.();
|
|
15441
|
+
this.scheduleReconnect();
|
|
15442
|
+
return;
|
|
15443
|
+
}
|
|
15327
15444
|
const parser = source.ffmpegParser;
|
|
15328
15445
|
const sourceKind = source.type === "rtmp" ? "rtmp" : source.type === "flv" || source.type === "http" ? "http" : "rtsp";
|
|
15329
|
-
const args = buildFfmpegSourceReaderArgs({
|
|
15330
|
-
sourceUrl: source.url,
|
|
15331
|
-
sourceKind,
|
|
15332
|
-
transport: parser?.transport ?? "tcp",
|
|
15333
|
-
...parser?.inputArgs ? { inputArgs: parser.inputArgs } : {},
|
|
15334
|
-
...parser?.outputArgs ? { outputArgs: parser.outputArgs } : {}
|
|
15335
|
-
});
|
|
15336
15446
|
const reader = new FfmpegSourceReader({
|
|
15337
|
-
ffmpegPath
|
|
15338
|
-
args
|
|
15447
|
+
ffmpegPath,
|
|
15448
|
+
args: buildFfmpegSourceReaderArgs({
|
|
15449
|
+
sourceUrl: source.url,
|
|
15450
|
+
sourceKind,
|
|
15451
|
+
transport: parser?.transport ?? "tcp",
|
|
15452
|
+
...parser?.inputArgs ? { inputArgs: parser.inputArgs } : {},
|
|
15453
|
+
...parser?.outputArgs ? { outputArgs: parser.outputArgs } : {}
|
|
15454
|
+
}),
|
|
15339
15455
|
...this.logger ? { logger: this.logger.child("ffmpeg-source") } : {},
|
|
15340
15456
|
claimCost: (pid) => this.claimIngestCost(pid)
|
|
15341
15457
|
}, {
|
|
@@ -16311,7 +16427,7 @@ function hexToBytes(hex) {
|
|
|
16311
16427
|
* pipe is the pattern proven by the WebRTC transcode feed.
|
|
16312
16428
|
*/
|
|
16313
16429
|
var DEFAULT_FFMPEG_CONFIG = {
|
|
16314
|
-
binaryPath:
|
|
16430
|
+
binaryPath: null,
|
|
16315
16431
|
hwAccel: "copy",
|
|
16316
16432
|
threadCount: 0
|
|
16317
16433
|
};
|
|
@@ -16345,6 +16461,7 @@ var TranscodePipelineManager = class {
|
|
|
16345
16461
|
resolveProfileSource;
|
|
16346
16462
|
releaseGraceMs;
|
|
16347
16463
|
ffmpegConfig;
|
|
16464
|
+
onFfmpegMissing;
|
|
16348
16465
|
resolveStreamHwAccel;
|
|
16349
16466
|
spawnFn;
|
|
16350
16467
|
acquireChannel;
|
|
@@ -16357,6 +16474,7 @@ var TranscodePipelineManager = class {
|
|
|
16357
16474
|
this.resolveProfileSource = opts.resolveProfileSource ?? null;
|
|
16358
16475
|
this.releaseGraceMs = opts.releaseGraceMs ?? 5e3;
|
|
16359
16476
|
this.ffmpegConfig = opts.ffmpegConfig ?? (() => DEFAULT_FFMPEG_CONFIG);
|
|
16477
|
+
this.onFfmpegMissing = opts.onFfmpegMissing ?? null;
|
|
16360
16478
|
this.probeFfmpeg = opts.probeFfmpeg ?? require_node.probeFfmpegBinary;
|
|
16361
16479
|
this.resolveStreamHwAccel = opts.resolveStreamHwAccel ?? null;
|
|
16362
16480
|
this.spawnFn = opts.spawnFn ?? node_child_process.spawn;
|
|
@@ -16429,11 +16547,17 @@ var TranscodePipelineManager = class {
|
|
|
16429
16547
|
async startTranscode(opts) {
|
|
16430
16548
|
if (!this.acquireChannel) throw new Error("getStreamWithCodec: transcode egress not wired (acquireChannel missing)");
|
|
16431
16549
|
const cfg = this.ffmpegConfig();
|
|
16550
|
+
const ffmpegBinaryPath = requireBrokerFfmpeg(cfg.binaryPath, {
|
|
16551
|
+
logger: this.logger,
|
|
16552
|
+
deviceId: opts.deviceId,
|
|
16553
|
+
purpose: "transcode egress",
|
|
16554
|
+
...this.onFfmpegMissing === null ? {} : { onMissing: this.onFfmpegMissing }
|
|
16555
|
+
});
|
|
16432
16556
|
const effectiveHwAccel = this.effectiveHwAccelSetting(opts.deviceId, opts.camStreamId, cfg.hwAccel);
|
|
16433
16557
|
const choice = chooseVideoEncoder({
|
|
16434
16558
|
target: opts.targetCodec,
|
|
16435
16559
|
backend: await this.resolveHardwareBackend(),
|
|
16436
|
-
binaryEncoders: await this.resolveBinaryEncoders(
|
|
16560
|
+
binaryEncoders: await this.resolveBinaryEncoders(ffmpegBinaryPath),
|
|
16437
16561
|
hwAccelSetting: effectiveHwAccel,
|
|
16438
16562
|
renderNode: DEFAULT_VAAPI_RENDER_DEVICE
|
|
16439
16563
|
});
|
|
@@ -16463,7 +16587,7 @@ var TranscodePipelineManager = class {
|
|
|
16463
16587
|
const sidecarCodec = opts.audioEgress ? toAudioSidecarCodec(opts.audio) : null;
|
|
16464
16588
|
const egress = new TranscodeEgress({
|
|
16465
16589
|
logger: this.logger.child("egress"),
|
|
16466
|
-
ffmpegBinaryPath
|
|
16590
|
+
ffmpegBinaryPath,
|
|
16467
16591
|
spawnFn: this.spawnFn,
|
|
16468
16592
|
acquireChannel: this.acquireChannel
|
|
16469
16593
|
}, {
|
|
@@ -16692,11 +16816,12 @@ var TranscodePipelineManager = class {
|
|
|
16692
16816
|
try {
|
|
16693
16817
|
const backend = rankDecodeHwAccels((await this.hwaccel.resolve(prefer)).preferred, [])[0] ?? null;
|
|
16694
16818
|
if (backend === null) return null;
|
|
16695
|
-
const
|
|
16819
|
+
const binaryPath = this.ffmpegConfig().binaryPath;
|
|
16820
|
+
const caps = binaryPath === null ? null : await this.resolveBinaryCapabilities(binaryPath);
|
|
16696
16821
|
if (caps !== null && !caps.hwaccels.has(backend)) {
|
|
16697
16822
|
this.logger.warn("this ffmpeg has no such decode backend compiled in — decoding in software instead of dying on a device that cannot exist", { meta: {
|
|
16698
16823
|
backend,
|
|
16699
|
-
ffmpeg:
|
|
16824
|
+
ffmpeg: binaryPath,
|
|
16700
16825
|
hwaccels: [...caps.hwaccels].join(",") || "none"
|
|
16701
16826
|
} });
|
|
16702
16827
|
return null;
|
|
@@ -16958,6 +17083,21 @@ var DeviceOverridesMapSchema = require_dist.record(require_dist.string(), Device
|
|
|
16958
17083
|
* prune driven by a map that shrinks on every unregister.
|
|
16959
17084
|
*/
|
|
16960
17085
|
var RtspEnabledSchema = require_dist.record(require_dist.string(), require_dist.boolean());
|
|
17086
|
+
/**
|
|
17087
|
+
* How far a {@link StreamBrokerManager.retractDevice} goes.
|
|
17088
|
+
*
|
|
17089
|
+
* The two flags answer different questions and a permanent removal sets BOTH:
|
|
17090
|
+
* `preserveAssignments` is about the per-stream teardown (skip the
|
|
17091
|
+
* re-ranking churn as each stream disappears), `purgeAssignments` is about the
|
|
17092
|
+
* device's profile-slot ENTRY (drop it outright, and persist that).
|
|
17093
|
+
*/
|
|
17094
|
+
/**
|
|
17095
|
+
* The departed-device sweep's second read must be at least this long after
|
|
17096
|
+
* the first mark (D685): one default catalog-poll interval. Two reads closer
|
|
17097
|
+
* than that — the startup pass's 1.5 s re-runs, two overlapping polls — are
|
|
17098
|
+
* one observation.
|
|
17099
|
+
*/
|
|
17100
|
+
var DEPARTURE_CONFIRM_MIN_MS = 3e4;
|
|
16961
17101
|
var PUSH_KINDS = new Set(["push-annexb"]);
|
|
16962
17102
|
/** Walk a profile map and return the slot that points at `camStreamId`, or null. */
|
|
16963
17103
|
function findProfileForStream(map, camStreamId) {
|
|
@@ -17157,6 +17297,13 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17157
17297
|
* outage that put it here.
|
|
17158
17298
|
*/
|
|
17159
17299
|
catalogPullFailures = /* @__PURE__ */ new Map();
|
|
17300
|
+
/**
|
|
17301
|
+
* Devices with registered streams that a successful fleet read did not
|
|
17302
|
+
* list, and WHEN that read was (D685). A mark, not a verdict: the sweep
|
|
17303
|
+
* retracts only when a read at least {@link DEPARTURE_CONFIRM_MIN_MS} later
|
|
17304
|
+
* agrees, and a reappearance clears it (D49).
|
|
17305
|
+
*/
|
|
17306
|
+
departureMarks = /* @__PURE__ */ new Map();
|
|
17160
17307
|
eventBus = null;
|
|
17161
17308
|
/**
|
|
17162
17309
|
* Per-broker stream health flag set by the watchdog. `undefined`
|
|
@@ -17377,12 +17524,14 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17377
17524
|
/**
|
|
17378
17525
|
* Snapshot of the system `ffmpeg` config section (binaryPath / hwAccel /
|
|
17379
17526
|
* threadCount). Refreshed by the host addon via `setFfmpegConfig`; the
|
|
17380
|
-
* transcode manager reads it live per spawn.
|
|
17381
|
-
*
|
|
17382
|
-
*
|
|
17527
|
+
* transcode manager reads it live per spawn. Until the addon resolves the
|
|
17528
|
+
* node's pinned binary there is NONE (`binaryPath: null`) and every child
|
|
17529
|
+
* refuses by name — never a PATH `ffmpeg` (D540). `'copy'` software decode
|
|
17530
|
+
* and auto threads stay the defaults: software decode never hard-fails on a
|
|
17531
|
+
* fragile HEVC source.
|
|
17383
17532
|
*/
|
|
17384
17533
|
ffmpegConfig = {
|
|
17385
|
-
binaryPath:
|
|
17534
|
+
binaryPath: null,
|
|
17386
17535
|
hwAccel: "copy",
|
|
17387
17536
|
threadCount: 0
|
|
17388
17537
|
};
|
|
@@ -17418,6 +17567,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17418
17567
|
},
|
|
17419
17568
|
resolveProfileSource: (deviceId, profile) => this.assignments.get(deviceId)?.map[profile] ?? null,
|
|
17420
17569
|
ffmpegConfig: () => this.ffmpegConfig,
|
|
17570
|
+
onFfmpegMissing: () => this.reportFfmpegMissing(),
|
|
17421
17571
|
resolveStreamHwAccel: (deviceId, camStreamId) => this.getStreamHwAccelOverride(deviceId, camStreamId),
|
|
17422
17572
|
sentryOffered: async (deviceId) => sentryEligible({
|
|
17423
17573
|
batteryOperated: await this.isBatteryOperated(deviceId),
|
|
@@ -17480,6 +17630,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17480
17630
|
});
|
|
17481
17631
|
},
|
|
17482
17632
|
ffmpegConfig: () => this.ffmpegConfig,
|
|
17633
|
+
onFfmpegMissing: () => this.reportFfmpegMissing(),
|
|
17483
17634
|
...this.costRegistry === null ? {} : { costRegistry: this.costRegistry },
|
|
17484
17635
|
resolveDecodeHwAccel: (deviceId, override) => this.resolveEgressHwAccel(deviceId, override),
|
|
17485
17636
|
acquireChannel: (brokerId) => {
|
|
@@ -17855,11 +18006,35 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17855
18006
|
if (localNodeId !== void 0) this.localNodeId = localNodeId;
|
|
17856
18007
|
if (hwaccel !== void 0) this.hwaccel = hwaccel;
|
|
17857
18008
|
}
|
|
17858
|
-
/**
|
|
18009
|
+
/**
|
|
18010
|
+
* Update the node's ffmpeg config snapshot. Every broker READS it per dial
|
|
18011
|
+
* ({@link bindBrokerFfmpeg}), so a re-resolved binary reaches live brokers —
|
|
18012
|
+
* including one created while the re-resolve was in flight — with no copy to
|
|
18013
|
+
* fan out (D540).
|
|
18014
|
+
*/
|
|
17859
18015
|
setFfmpegConfig(cfg) {
|
|
17860
18016
|
this.ffmpegConfig = cfg;
|
|
17861
18017
|
}
|
|
17862
18018
|
/**
|
|
18019
|
+
* Point a broker at the node's LIVE pinned ffmpeg and at the re-resolve
|
|
18020
|
+
* request, so its ffmpeg-parser ingress spawns the current binary and a
|
|
18021
|
+
* refusal asks for it again.
|
|
18022
|
+
*/
|
|
18023
|
+
bindBrokerFfmpeg(broker) {
|
|
18024
|
+
broker.setFfmpegBinaryPathSource(() => this.ffmpegConfig.binaryPath);
|
|
18025
|
+
broker.setFfmpegMissingHandler(() => this.reportFfmpegMissing());
|
|
18026
|
+
}
|
|
18027
|
+
/** Who asks for the pinned binary again after a refusal. Set by the addon. */
|
|
18028
|
+
ffmpegMissingHandler = null;
|
|
18029
|
+
/** Wire the host's re-resolve (`FfmpegReresolver.request`). */
|
|
18030
|
+
setFfmpegMissingHandler(handler) {
|
|
18031
|
+
this.ffmpegMissingHandler = handler;
|
|
18032
|
+
}
|
|
18033
|
+
/** A child was refused for want of the pinned ffmpeg: ask the host again. */
|
|
18034
|
+
reportFfmpegMissing() {
|
|
18035
|
+
this.ffmpegMissingHandler?.();
|
|
18036
|
+
}
|
|
18037
|
+
/**
|
|
17863
18038
|
* The decode `hwaccel` options surfaceable in a (future) UI: the always-valid
|
|
17864
18039
|
* settings (`copy`, `auto`, `none`) plus the concrete hardware backends THIS
|
|
17865
18040
|
* node can use, resolved from the LOCAL `ctx.kernel.hwaccel` (os.platform +
|
|
@@ -18200,7 +18375,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18200
18375
|
let annexB;
|
|
18201
18376
|
try {
|
|
18202
18377
|
annexB = await encodePrestreamIdr({
|
|
18203
|
-
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
18378
|
+
ffmpegPath: requireBrokerFfmpeg(this.ffmpegConfig.binaryPath, {
|
|
18379
|
+
logger,
|
|
18380
|
+
deviceId,
|
|
18381
|
+
purpose: "sentry pre-stream encode",
|
|
18382
|
+
onMissing: () => this.reportFfmpegMissing()
|
|
18383
|
+
}),
|
|
18204
18384
|
plan,
|
|
18205
18385
|
jpeg: picture.jpeg,
|
|
18206
18386
|
logger,
|
|
@@ -18991,6 +19171,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18991
19171
|
this.logger.debug("reconcileAllCatalogs: deviceManager.listAll failed — will retry", { meta: { error: require_dist.errMsg(err) } });
|
|
18992
19172
|
return;
|
|
18993
19173
|
}
|
|
19174
|
+
await this.sweepDepartedDevices(devices);
|
|
18994
19175
|
const cameras = devices.filter((d) => d.isCamera || d.type === require_dist.DeviceType.Camera);
|
|
18995
19176
|
for (const d of cameras) if (d.disabled) {
|
|
18996
19177
|
const firstSeen = !this.disabledDevices.has(d.id);
|
|
@@ -19006,6 +19187,71 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19006
19187
|
for (const deviceId of [...this.catalogPullFailures.keys()]) if (!liveIds.has(deviceId)) this.catalogPullFailures.delete(deviceId);
|
|
19007
19188
|
}
|
|
19008
19189
|
/**
|
|
19190
|
+
* Retract and purge a device that left the fleet WITHOUT a
|
|
19191
|
+
* `DeviceUnregistered` (D685).
|
|
19192
|
+
*
|
|
19193
|
+
* The event is the fast path and it may be dropped (D8/D11). Since D685 no
|
|
19194
|
+
* provider retracts on teardown (that hook also runs on every runner
|
|
19195
|
+
* restart), so a lost event used to leave the device's cam streams
|
|
19196
|
+
* registered — and `high` is default-warm, so its prebuffer hold kept
|
|
19197
|
+
* dialling the camera with no consumer at all, until the broker restarted.
|
|
19198
|
+
*
|
|
19199
|
+
* Destroying on a fallible read, so every way the read can be WRONG decides
|
|
19200
|
+
* nothing (D49): the caller has already returned on a failed read, an EMPTY
|
|
19201
|
+
* listing is device-restore not having run, and one absence only MARKS the
|
|
19202
|
+
* device — the retract needs the next successful read to agree. A device
|
|
19203
|
+
* that reappears in between is unmarked.
|
|
19204
|
+
*/
|
|
19205
|
+
async sweepDepartedDevices(devices) {
|
|
19206
|
+
if (devices.length === 0) {
|
|
19207
|
+
if (this.departureMarks.size > 0) this.logger.debug("departed-device sweep skipped — device enumeration came back empty", { meta: { marked: [...this.departureMarks.keys()] } });
|
|
19208
|
+
return;
|
|
19209
|
+
}
|
|
19210
|
+
const listed = new Set(devices.map((d) => d.id));
|
|
19211
|
+
for (const deviceId of [...this.departureMarks.keys()]) {
|
|
19212
|
+
if (!this.cameraStreams.has(deviceId)) {
|
|
19213
|
+
this.departureMarks.delete(deviceId);
|
|
19214
|
+
this.logger.debug("departure mark dropped — the device has no streams any more", { tags: { deviceId } });
|
|
19215
|
+
continue;
|
|
19216
|
+
}
|
|
19217
|
+
if (listed.has(deviceId)) {
|
|
19218
|
+
this.departureMarks.delete(deviceId);
|
|
19219
|
+
this.logger.info("device is back in the fleet listing — departure sweep cancelled", { tags: { deviceId } });
|
|
19220
|
+
}
|
|
19221
|
+
}
|
|
19222
|
+
const nowMs = Date.now();
|
|
19223
|
+
for (const deviceId of [...this.cameraStreams.keys()]) {
|
|
19224
|
+
if (listed.has(deviceId)) continue;
|
|
19225
|
+
const markedAt = this.departureMarks.get(deviceId);
|
|
19226
|
+
if (markedAt === void 0) {
|
|
19227
|
+
this.departureMarks.set(deviceId, nowMs);
|
|
19228
|
+
this.logger.info("device absent from the fleet listing — its streams are swept if a later read agrees", {
|
|
19229
|
+
tags: { deviceId },
|
|
19230
|
+
meta: { confirmAfterMs: DEPARTURE_CONFIRM_MIN_MS }
|
|
19231
|
+
});
|
|
19232
|
+
continue;
|
|
19233
|
+
}
|
|
19234
|
+
if (nowMs - markedAt < DEPARTURE_CONFIRM_MIN_MS) continue;
|
|
19235
|
+
this.departureMarks.delete(deviceId);
|
|
19236
|
+
this.logger.warn("device left the fleet without DeviceUnregistered — swept", {
|
|
19237
|
+
tags: { deviceId },
|
|
19238
|
+
meta: {
|
|
19239
|
+
camStreamIds: [...this.cameraStreams.get(deviceId)?.keys() ?? []],
|
|
19240
|
+
absentForMs: nowMs - markedAt
|
|
19241
|
+
}
|
|
19242
|
+
});
|
|
19243
|
+
await this.retractDevice(deviceId, {
|
|
19244
|
+
preserveAssignments: true,
|
|
19245
|
+
purgeAssignments: true
|
|
19246
|
+
}).catch((err) => {
|
|
19247
|
+
this.logger.warn("departed-device sweep: retract failed — the next two polls try again", {
|
|
19248
|
+
tags: { deviceId },
|
|
19249
|
+
meta: { error: require_dist.errMsg(err) }
|
|
19250
|
+
});
|
|
19251
|
+
});
|
|
19252
|
+
}
|
|
19253
|
+
}
|
|
19254
|
+
/**
|
|
19009
19255
|
* ONE device's audio-plane reconcile pass. See `audio-plane-reconcile.ts`
|
|
19010
19256
|
* for the decision rules; this half only gathers and acts.
|
|
19011
19257
|
*
|
|
@@ -19452,7 +19698,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19452
19698
|
height: scaledHeight(cut, clipWidth)
|
|
19453
19699
|
});
|
|
19454
19700
|
const gifSpeed = input.gifSpeed ?? 4;
|
|
19455
|
-
|
|
19701
|
+
const gifFfmpeg = this.ffmpegConfig.binaryPath;
|
|
19702
|
+
if (input.kinds.includes("gif") && gifFfmpeg === null) {
|
|
19703
|
+
this.logger?.error("event media gif refused — no pinned ffmpeg on this node (D540)", { tags: { deviceId: input.deviceId } });
|
|
19704
|
+
this.reportFfmpegMissing();
|
|
19705
|
+
}
|
|
19706
|
+
if (input.kinds.includes("gif") && gifFfmpeg !== null) {
|
|
19456
19707
|
const gif = await gifFromMp4({
|
|
19457
19708
|
mp4,
|
|
19458
19709
|
deviceId: input.deviceId,
|
|
@@ -19461,7 +19712,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19461
19712
|
speed: require_dist.gifDeriveSpeed(gifSpeed, input.speed),
|
|
19462
19713
|
...this.logger !== void 0 ? { logger: this.logger } : {},
|
|
19463
19714
|
spawnFn: node_child_process.spawn,
|
|
19464
|
-
|
|
19715
|
+
ffmpegPath: gifFfmpeg
|
|
19465
19716
|
});
|
|
19466
19717
|
if (gif !== null) {
|
|
19467
19718
|
const height = scaledHeight(cut, input.gifMaxWidth);
|
|
@@ -19630,7 +19881,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19630
19881
|
"pipe",
|
|
19631
19882
|
"pipe"
|
|
19632
19883
|
] }),
|
|
19633
|
-
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
19884
|
+
ffmpegPath: requireBrokerFfmpeg(this.ffmpegConfig.binaryPath, {
|
|
19885
|
+
logger: this.logger,
|
|
19886
|
+
deviceId: cut.deviceId,
|
|
19887
|
+
purpose: "pre-buffer clip render",
|
|
19888
|
+
onMissing: () => this.reportFfmpegMissing()
|
|
19889
|
+
}),
|
|
19634
19890
|
sdpParameterSets: cut.broker.getSdpParameterSets()
|
|
19635
19891
|
});
|
|
19636
19892
|
this.logger?.info("pre-buffer clip rendered", {
|
|
@@ -21708,7 +21964,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21708
21964
|
const source = this.buildSourceFromCamStream(cam);
|
|
21709
21965
|
const broker = new StreamBroker(brokerId, this.buildDecoderCapApi(deviceId), brokerLogger, buildAudioCodecCapApi(this.api), this.localNodeId);
|
|
21710
21966
|
broker.setRestreamUrlResolver(() => this.rtspProvider.getEntry(brokerId)?.mutedUrl ?? null);
|
|
21711
|
-
|
|
21967
|
+
this.bindBrokerFfmpeg(broker);
|
|
21712
21968
|
if (this.costRegistry !== null) broker.setCostRegistry(this.costRegistry);
|
|
21713
21969
|
this.applyPrebufferConfig(broker, deviceId, camStreamId);
|
|
21714
21970
|
this.applyAudioMuteConfig(broker, deviceId);
|
|
@@ -21886,7 +22142,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21886
22142
|
}
|
|
21887
22143
|
});
|
|
21888
22144
|
const feeder = new DerivedStreamFeeder({
|
|
21889
|
-
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
22145
|
+
ffmpegPath: requireBrokerFfmpeg(this.ffmpegConfig.binaryPath, {
|
|
22146
|
+
logger: this.logger,
|
|
22147
|
+
deviceId,
|
|
22148
|
+
purpose: "derived transcode",
|
|
22149
|
+
onMissing: () => this.reportFfmpegMissing()
|
|
22150
|
+
}),
|
|
21890
22151
|
args,
|
|
21891
22152
|
logger: this.logger.child(`derived-ffmpeg:${camStreamId}`).withTags({ deviceId })
|
|
21892
22153
|
}, {
|
|
@@ -33834,7 +34095,7 @@ var offThreadJsParse = (bytes) => {
|
|
|
33834
34095
|
* Drop-in for the session `demuxVideo` hook: JS parse off the live loop,
|
|
33835
34096
|
* ffmpeg subprocess fallback unchanged.
|
|
33836
34097
|
*/
|
|
33837
|
-
async function demuxSegmentPreferJsOffThread(bytes, opts
|
|
34098
|
+
async function demuxSegmentPreferJsOffThread(bytes, opts) {
|
|
33838
34099
|
return require_segment_demux_js.demuxSegmentPreferJs(bytes, {
|
|
33839
34100
|
...opts,
|
|
33840
34101
|
jsParse: offThreadJsParse
|
|
@@ -34019,9 +34280,9 @@ function sliceMuLawToPcmuFrames(mulaw) {
|
|
|
34019
34280
|
* can't decode) — ffmpeg exits non-zero, which is tolerated as empty output.
|
|
34020
34281
|
*
|
|
34021
34282
|
* @param bytes - The full `.m4s` fragment.
|
|
34022
|
-
* @param ffmpegPath -
|
|
34283
|
+
* @param ffmpegPath - The node's pinned ffmpeg (D540). Required: no PATH default.
|
|
34023
34284
|
*/
|
|
34024
|
-
async function demuxSegmentToPcmu(bytes, ffmpegPath
|
|
34285
|
+
async function demuxSegmentToPcmu(bytes, ffmpegPath) {
|
|
34025
34286
|
const mulaw = await require_segment_demux_js.runFfmpegOnce({
|
|
34026
34287
|
ffmpegPath,
|
|
34027
34288
|
args: SEGMENT_AUDIO_FFMPEG_ARGS,
|
|
@@ -34153,15 +34414,19 @@ var SEGMENT_AUDIO_OPUS_FFMPEG_ARGS = [
|
|
|
34153
34414
|
* best-effort and must never block recorded video. Mirrors the video-side
|
|
34154
34415
|
* `demuxSegmentPreferJs`.
|
|
34155
34416
|
*/
|
|
34156
|
-
async function demuxSegmentAudioOpusPreferJs(bytes, opts
|
|
34417
|
+
async function demuxSegmentAudioOpusPreferJs(bytes, opts) {
|
|
34157
34418
|
try {
|
|
34158
34419
|
const direct = await demuxSegmentAudioOpusJs(bytes);
|
|
34159
34420
|
if (direct) return direct;
|
|
34160
34421
|
} catch {}
|
|
34422
|
+
if (opts.ffmpegPath === null) {
|
|
34423
|
+
opts.onFfmpegFallback?.("audio track is not Opus (legacy fragment) and the ffmpeg fallback is refused — no pinned ffmpeg on this node (D540)");
|
|
34424
|
+
return [];
|
|
34425
|
+
}
|
|
34161
34426
|
opts.onFfmpegFallback?.("audio track is not Opus (legacy fragment)");
|
|
34162
34427
|
try {
|
|
34163
34428
|
const opusFmp4 = await require_segment_demux_js.runFfmpegOnce({
|
|
34164
|
-
ffmpegPath: opts.ffmpegPath
|
|
34429
|
+
ffmpegPath: opts.ffmpegPath,
|
|
34165
34430
|
args: SEGMENT_AUDIO_OPUS_FFMPEG_ARGS,
|
|
34166
34431
|
input: bytes,
|
|
34167
34432
|
tolerateNonZeroExit: true
|
|
@@ -35223,12 +35488,25 @@ var BrokerWebrtcServer = class {
|
|
|
35223
35488
|
session.onClosed = () => this.cleanupSession(sessionId);
|
|
35224
35489
|
const locateRecorded = this.locateRecordedSegment;
|
|
35225
35490
|
const readRecorded = this.readRecordedSegmentBytes;
|
|
35226
|
-
const
|
|
35491
|
+
const resolveFfmpegPath = this.resolveFfmpegPath;
|
|
35492
|
+
const resolveFfmpeg = () => resolveFfmpegPath?.() ?? null;
|
|
35493
|
+
let noPinnedFfmpegLogged = false;
|
|
35494
|
+
const reportNoPinnedFfmpeg = (what) => {
|
|
35495
|
+
if (noPinnedFfmpegLogged) return;
|
|
35496
|
+
noPinnedFfmpegLogged = true;
|
|
35497
|
+
sessionLogger.error("recorded playback: ffmpeg fallback refused — no pinned ffmpeg on this node (D540)", {
|
|
35498
|
+
tags: { deviceId },
|
|
35499
|
+
meta: {
|
|
35500
|
+
sessionId,
|
|
35501
|
+
what
|
|
35502
|
+
}
|
|
35503
|
+
});
|
|
35504
|
+
};
|
|
35227
35505
|
const demuxTally = new DemuxPathTally();
|
|
35228
35506
|
let audioOpusFallbackLogged = false;
|
|
35229
35507
|
if (locateRecorded && readRecorded) {
|
|
35230
35508
|
const demuxVideo = (b) => demuxSegmentPreferJsOffThread(b, {
|
|
35231
|
-
ffmpegPath: resolveFfmpeg
|
|
35509
|
+
ffmpegPath: resolveFfmpeg(),
|
|
35232
35510
|
onDemuxPath: (outcome) => {
|
|
35233
35511
|
const report = demuxTally.record(outcome, Date.now());
|
|
35234
35512
|
if (!report) return;
|
|
@@ -35243,6 +35521,9 @@ var BrokerWebrtcServer = class {
|
|
|
35243
35521
|
}
|
|
35244
35522
|
});
|
|
35245
35523
|
}
|
|
35524
|
+
}).catch((err) => {
|
|
35525
|
+
if (err instanceof require_segment_demux_js.FfmpegFallbackRefusedError) reportNoPinnedFfmpeg("video");
|
|
35526
|
+
throw err;
|
|
35246
35527
|
});
|
|
35247
35528
|
const scrubCache = new ScrubFragmentCache();
|
|
35248
35529
|
const initialLiveProfile = this.profileTierResolver?.(brokerId) ?? null;
|
|
@@ -35358,9 +35639,11 @@ var BrokerWebrtcServer = class {
|
|
|
35358
35639
|
pushAudioPcmu: (pcmu, ts) => session.writeRecordedAudio(Buffer.from(pcmu), ts)
|
|
35359
35640
|
};
|
|
35360
35641
|
const demuxAudio = opusSession ? async (b) => {
|
|
35642
|
+
const ffmpegPath = resolveFfmpeg();
|
|
35361
35643
|
return (await demuxSegmentAudioOpusPreferJs(b, {
|
|
35362
|
-
ffmpegPath
|
|
35644
|
+
ffmpegPath,
|
|
35363
35645
|
onFfmpegFallback: (reason) => {
|
|
35646
|
+
if (ffmpegPath === null) reportNoPinnedFfmpeg("audio");
|
|
35364
35647
|
if (audioOpusFallbackLogged) return;
|
|
35365
35648
|
audioOpusFallbackLogged = true;
|
|
35366
35649
|
sessionLogger.warn("recorded audio: JS Opus demux missed — ffmpeg AAC→Opus fallback (once/session)", { meta: {
|
|
@@ -35374,7 +35657,14 @@ var BrokerWebrtcServer = class {
|
|
|
35374
35657
|
data: au.data,
|
|
35375
35658
|
ptsMs: au.ptsMs
|
|
35376
35659
|
}));
|
|
35377
|
-
} : (b) =>
|
|
35660
|
+
} : async (b) => {
|
|
35661
|
+
const ffmpegPath = resolveFfmpeg();
|
|
35662
|
+
if (ffmpegPath === null) {
|
|
35663
|
+
reportNoPinnedFfmpeg("audio");
|
|
35664
|
+
return [];
|
|
35665
|
+
}
|
|
35666
|
+
return await demuxSegmentToPcmu(b, ffmpegPath);
|
|
35667
|
+
};
|
|
35378
35668
|
const clipSource = clip && readClip ? createClipFileSource({
|
|
35379
35669
|
handle: clip,
|
|
35380
35670
|
read: readClip,
|
|
@@ -36743,6 +37033,39 @@ var EventLoopStallMonitor = class {
|
|
|
36743
37033
|
}
|
|
36744
37034
|
};
|
|
36745
37035
|
//#endregion
|
|
37036
|
+
//#region src/stream-broker/device-removal.ts
|
|
37037
|
+
/**
|
|
37038
|
+
* The broker's half of an operator's device DELETE.
|
|
37039
|
+
*
|
|
37040
|
+
* Since D685 this subscription is the fast path of a removal: providers no
|
|
37041
|
+
* longer retract their streams from `removeDevice()`, because that hook also
|
|
37042
|
+
* runs on every runner restart. The backstop for a dropped event is the
|
|
37043
|
+
* catalog poll's departed-device sweep (`StreamBrokerManager`,
|
|
37044
|
+
* `sweepDepartedDevices`). Extracted from `addon.ts` so the registration the
|
|
37045
|
+
* addon performs is the one a test drives.
|
|
37046
|
+
*/
|
|
37047
|
+
/** A PERMANENT removal: retract every stream AND purge the slot assignments. */
|
|
37048
|
+
async function retractRemovedDevice(deviceId, deps) {
|
|
37049
|
+
try {
|
|
37050
|
+
await deps.retractDevice(deviceId, {
|
|
37051
|
+
preserveAssignments: true,
|
|
37052
|
+
purgeAssignments: true
|
|
37053
|
+
});
|
|
37054
|
+
} catch (err) {
|
|
37055
|
+
deps.logger.warn("device retract failed", {
|
|
37056
|
+
tags: { deviceId },
|
|
37057
|
+
meta: { error: require_dist.errMsg(err) }
|
|
37058
|
+
});
|
|
37059
|
+
}
|
|
37060
|
+
}
|
|
37061
|
+
/** Retract a device the moment the device-manager says it was deleted. */
|
|
37062
|
+
function subscribeDeviceRemoval(subscribe, deps) {
|
|
37063
|
+
subscribe({ category: require_dist.EventCategory.DeviceUnregistered }, (event) => {
|
|
37064
|
+
const deviceId = event.data["deviceId"];
|
|
37065
|
+
if (typeof deviceId === "number") retractRemovedDevice(deviceId, deps());
|
|
37066
|
+
});
|
|
37067
|
+
}
|
|
37068
|
+
//#endregion
|
|
36746
37069
|
//#region src/stream-broker/addon.ts
|
|
36747
37070
|
/**
|
|
36748
37071
|
* Hydrate the persisted RTSP restream state. Tokens come from their own
|
|
@@ -36824,10 +37147,21 @@ var PROBE_SNAPSHOTS_PERSIST_INTERVAL_MS = 6e4;
|
|
|
36824
37147
|
var NETWORK_QUALITY_STATS_TIMEOUT_MS = 2e3;
|
|
36825
37148
|
var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
36826
37149
|
brokerManager = null;
|
|
36827
|
-
/**
|
|
36828
|
-
*
|
|
36829
|
-
*
|
|
36830
|
-
|
|
37150
|
+
/** The node's pinned ffmpeg, mirrored here so the recorded-playback demuxers
|
|
37151
|
+
* (segment-demux/segment-audio) spawn the same binary as the transcode
|
|
37152
|
+
* egress. Kept in sync by the ffmpeg wiring. `null` = none could be resolved:
|
|
37153
|
+
* their ffmpeg fallbacks refuse by name — never a PATH `ffmpeg` (D540). */
|
|
37154
|
+
ffmpegBinaryPath = null;
|
|
37155
|
+
/** The node's pinned build, asked once on success (D540). A failure is not
|
|
37156
|
+
* remembered, so the re-resolve below can succeed later. */
|
|
37157
|
+
pinnedFfmpeg = require_dist.memoizePinnedFfmpeg(() => this.ctx.deps.ensureFfmpeg());
|
|
37158
|
+
/**
|
|
37159
|
+
* The way back from a failed boot-time resolution: a refused child asks, and
|
|
37160
|
+
* ONE bounded re-resolve hands the binary to the manager config (which every
|
|
37161
|
+
* live broker reads per dial) and the demuxers. Without it one failed
|
|
37162
|
+
* download disabled every broker ffmpeg child until the runner restarted.
|
|
37163
|
+
*/
|
|
37164
|
+
ffmpegWiring = null;
|
|
36831
37165
|
/** Once-per-addon log guard for the readGopBytes → whole-segment downgrade. */
|
|
36832
37166
|
gopReadFallbackLogged = false;
|
|
36833
37167
|
metricsSnapshotTimer = null;
|
|
@@ -36888,36 +37222,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
36888
37222
|
async resolveFfmpegBinaryPath() {
|
|
36889
37223
|
const override = this.config.binaryPath;
|
|
36890
37224
|
if (typeof override === "string" && override.trim().length > 0) return override;
|
|
36891
|
-
return await this.
|
|
36892
|
-
}
|
|
36893
|
-
/**
|
|
36894
|
-
* Push THIS node's ffmpeg config into the broker — all node-local now, NOT the
|
|
36895
|
-
* retired global `ffmpeg` section. `binaryPath` via {@link resolveFfmpegBinaryPath}
|
|
36896
|
-
* (per-node override → ensureFfmpeg); `hwAccel` = the per-node `hwaccel`
|
|
36897
|
-
* preference (default `'copy'` = software, the safe HEVC-compatible choice; the
|
|
36898
|
-
* concrete backend is resolved node-locally by the egress transcode manager,
|
|
36899
|
-
* which asks the DECODER ADDON — never the kernel resolver, whose qsv-first
|
|
36900
|
-
* order is the known-bad one here). `threadCount` is always `0` (auto — ffmpeg uses the
|
|
36901
|
-
* node's own cores; the manual knob was removed).
|
|
36902
|
-
*/
|
|
36903
|
-
async loadFfmpegConfig() {
|
|
36904
|
-
if (!this.brokerManager) return;
|
|
36905
|
-
try {
|
|
36906
|
-
const binaryPath = await this.resolveFfmpegBinaryPath();
|
|
36907
|
-
const hwAccel = this.config.hwaccel;
|
|
36908
|
-
this.brokerManager.setFfmpegConfig({
|
|
36909
|
-
binaryPath,
|
|
36910
|
-
hwAccel,
|
|
36911
|
-
threadCount: 0
|
|
36912
|
-
});
|
|
36913
|
-
this.ffmpegBinaryPath = binaryPath;
|
|
36914
|
-
this.ctx.logger.info("Loaded ffmpeg config", { meta: {
|
|
36915
|
-
binaryPath,
|
|
36916
|
-
hwAccel
|
|
36917
|
-
} });
|
|
36918
|
-
} catch (err) {
|
|
36919
|
-
this.ctx.logger.error("stream-broker: could not resolve an ffmpeg binary — no transcode on this node can be trusted until this is fixed", { meta: { error: require_dist.errMsg(err) } });
|
|
36920
|
-
}
|
|
37225
|
+
return await this.pinnedFfmpeg();
|
|
36921
37226
|
}
|
|
36922
37227
|
async onInitialize() {
|
|
36923
37228
|
this.ctx.logger.info("log channels declared", { meta: { channels: STREAM_BROKER_LOG_CHANNELS.map((c) => c.descriptor.name) } });
|
|
@@ -36947,7 +37252,16 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
36947
37252
|
};
|
|
36948
37253
|
this.brokerManager.setDeviceSignalReader(async (deviceId) => (await readSignals(deviceId)).map((s) => s.code));
|
|
36949
37254
|
this.brokerManager.setDeviceSignalDetailReader(readSignals);
|
|
36950
|
-
|
|
37255
|
+
this.ffmpegWiring = wireBrokerFfmpeg({
|
|
37256
|
+
host: this.brokerManager,
|
|
37257
|
+
resolve: () => this.resolveFfmpegBinaryPath(),
|
|
37258
|
+
hwAccel: () => this.config.hwaccel,
|
|
37259
|
+
onBinaryPath: (binaryPath) => {
|
|
37260
|
+
this.ffmpegBinaryPath = binaryPath;
|
|
37261
|
+
},
|
|
37262
|
+
logger: this.ctx.logger
|
|
37263
|
+
});
|
|
37264
|
+
await this.ffmpegWiring.load();
|
|
36951
37265
|
await this.declareRowCollections();
|
|
36952
37266
|
await this.purgeFlattenedSettingsKeys();
|
|
36953
37267
|
const rtspProvider = this.brokerManager.getRtspRestreamProvider();
|
|
@@ -37116,10 +37430,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
37116
37430
|
const deviceId = event.data.deviceId;
|
|
37117
37431
|
if (typeof deviceId === "number") reconcileDevice(deviceId, "device-registered");
|
|
37118
37432
|
});
|
|
37119
|
-
this.subscribe(
|
|
37120
|
-
const deviceId = event.data.deviceId;
|
|
37121
|
-
if (typeof deviceId === "number") this.handleDeviceRemoved(deviceId);
|
|
37122
|
-
});
|
|
37433
|
+
subscribeDeviceRemoval((filter, handler) => this.subscribe(filter, handler), () => this.deviceRemovalDeps());
|
|
37123
37434
|
this.subscribe({ category: require_dist.EventCategory.StreamParamsChanged }, (event) => {
|
|
37124
37435
|
const deviceId = event.data.deviceId;
|
|
37125
37436
|
if (typeof deviceId !== "number") return;
|
|
@@ -37232,7 +37543,10 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
37232
37543
|
};
|
|
37233
37544
|
}
|
|
37234
37545
|
},
|
|
37235
|
-
resolveFfmpegPath: () =>
|
|
37546
|
+
resolveFfmpegPath: () => {
|
|
37547
|
+
if (this.ffmpegBinaryPath === null) this.ffmpegWiring?.request("a recorded-playback demux needed ffmpeg");
|
|
37548
|
+
return this.ffmpegBinaryPath;
|
|
37549
|
+
}
|
|
37236
37550
|
});
|
|
37237
37551
|
this.brokerManager.setWebrtcServer(webrtcServer);
|
|
37238
37552
|
this.webrtcServer = webrtcServer;
|
|
@@ -37373,18 +37687,13 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
37373
37687
|
* persisted `profileMap` blob, so it returns at every boot. Live ids
|
|
37374
37688
|
* 3217/3218 outlived their cameras exactly that way.
|
|
37375
37689
|
*/
|
|
37376
|
-
|
|
37377
|
-
|
|
37378
|
-
|
|
37379
|
-
|
|
37380
|
-
|
|
37381
|
-
|
|
37382
|
-
}
|
|
37383
|
-
this.ctx.logger.warn("device retract failed", {
|
|
37384
|
-
tags: { deviceId },
|
|
37385
|
-
meta: { error: require_dist.errMsg(err) }
|
|
37386
|
-
});
|
|
37387
|
-
}
|
|
37690
|
+
deviceRemovalDeps() {
|
|
37691
|
+
return {
|
|
37692
|
+
retractDevice: async (deviceId, opts) => {
|
|
37693
|
+
await this.brokerManager?.retractDevice(deviceId, opts);
|
|
37694
|
+
},
|
|
37695
|
+
logger: this.ctx.logger
|
|
37696
|
+
};
|
|
37388
37697
|
}
|
|
37389
37698
|
/**
|
|
37390
37699
|
* Emit one `stream-broker.metrics-snapshot` event per active broker.
|
|
@@ -37642,6 +37951,8 @@ var FfmpegDecoderSession = class {
|
|
|
37642
37951
|
outputBuffer = Buffer.alloc(0);
|
|
37643
37952
|
destroyed = false;
|
|
37644
37953
|
logger;
|
|
37954
|
+
/** The pinned ffmpeg to spawn (D540). Required: there is no PATH default. */
|
|
37955
|
+
ffmpegPath;
|
|
37645
37956
|
cachedWidth = 0;
|
|
37646
37957
|
cachedHeight = 0;
|
|
37647
37958
|
inputPackets = 0;
|
|
@@ -37650,8 +37961,9 @@ var FfmpegDecoderSession = class {
|
|
|
37650
37961
|
totalDecodeTimeMs = 0;
|
|
37651
37962
|
decodeCount = 0;
|
|
37652
37963
|
startTime = Date.now();
|
|
37653
|
-
constructor(config, logger = noopLogger) {
|
|
37964
|
+
constructor(config, ffmpegPath, logger = noopLogger) {
|
|
37654
37965
|
this.config = { ...config };
|
|
37966
|
+
this.ffmpegPath = ffmpegPath;
|
|
37655
37967
|
this.logger = logger;
|
|
37656
37968
|
this.frameDropper = new FrameDropper(config.maxFps);
|
|
37657
37969
|
this.spawnFfmpeg();
|
|
@@ -37662,7 +37974,8 @@ var FfmpegDecoderSession = class {
|
|
|
37662
37974
|
this.outputBuffer = Buffer.alloc(0);
|
|
37663
37975
|
this.cachedWidth = 0;
|
|
37664
37976
|
this.cachedHeight = 0;
|
|
37665
|
-
const
|
|
37977
|
+
const args = buildFfmpegArgs(this.config);
|
|
37978
|
+
const child = (0, node_child_process.spawn)(this.ffmpegPath, args);
|
|
37666
37979
|
this.process = child;
|
|
37667
37980
|
child.stdin?.on("error", () => {});
|
|
37668
37981
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -37795,7 +38108,14 @@ var SUPPORTED_CODECS = new Set([
|
|
|
37795
38108
|
"hevc",
|
|
37796
38109
|
"mjpeg"
|
|
37797
38110
|
]);
|
|
38111
|
+
/**
|
|
38112
|
+
* A static decoder provider for stand-alone broker harnesses. Production
|
|
38113
|
+
* decodes through the `decoder` cap; this exists for the manual streaming
|
|
38114
|
+
* diagnostics. It is handed the pinned ffmpeg (D540) — there is no PATH
|
|
38115
|
+
* default for it to fall back to.
|
|
38116
|
+
*/
|
|
37798
38117
|
var FfmpegDecoderProvider = class {
|
|
38118
|
+
ffmpegPath;
|
|
37799
38119
|
id = "ffmpeg";
|
|
37800
38120
|
name = "FFmpeg Decoder";
|
|
37801
38121
|
/** Software decoder — used as fallback when hardware decoders are unavailable. */
|
|
@@ -37803,8 +38123,11 @@ var FfmpegDecoderProvider = class {
|
|
|
37803
38123
|
async supportsCodec(input) {
|
|
37804
38124
|
return SUPPORTED_CODECS.has(input.codec);
|
|
37805
38125
|
}
|
|
38126
|
+
constructor(ffmpegPath) {
|
|
38127
|
+
this.ffmpegPath = ffmpegPath;
|
|
38128
|
+
}
|
|
37806
38129
|
async createSession(config) {
|
|
37807
|
-
return new FfmpegDecoderSession(config);
|
|
38130
|
+
return new FfmpegDecoderSession(config, this.ffmpegPath);
|
|
37808
38131
|
}
|
|
37809
38132
|
};
|
|
37810
38133
|
//#endregion
|