@camstack/addon-pipeline 1.2.306 → 1.2.308
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-CeYzI_ws.js → default-detection-model-2rHeRf0Y.js} +1 -1
- package/dist/{default-detection-model-CmdkU7Ik.mjs → default-detection-model-DATyir0t.mjs} +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-C2GsZSso.js → dist-CdRsXAov.js} +196 -1
- package/dist/{dist-B0mdAGsn.mjs → dist-jy-7xoW5.mjs} +173 -2
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-C4FxPOmq.mjs → node-NCge1qZM.mjs} +1 -53
- package/dist/{node-DA9sHeo7.js → node-pWHpwpBa.js} +1 -53
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-DtcP6dwa.mjs → process-memory-4K7QaQab.mjs} +1 -1
- package/dist/{process-memory-CT7jffcr.js → process-memory-C4ayPnww.js} +1 -1
- package/dist/recorder/index.js +82 -15
- package/dist/recorder/index.mjs +82 -15
- package/dist/{segment-demux-js-C6Z9t7EK.js → segment-demux-js-0bM2Anej.js} +22 -4
- package/dist/{segment-demux-js-CDDu7U5-.mjs → segment-demux-js-BaI2VdPX.mjs} +17 -5
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BRWrphBU.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CkxD__Uy.mjs} +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DEPTgXS_.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-CdU3jJMF.mjs → hostInit-D56Gr-hD.mjs} +1 -1
- package/dist/stream-broker/index.js +401 -242
- package/dist/stream-broker/index.mjs +398 -239
- 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-DKNdMa3E.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-CdRsXAov.js");
|
|
7
|
+
const require_node = require("../node-pWHpwpBa.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-0bM2Anej.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
|
}
|
|
@@ -7081,6 +7177,100 @@ function defaultId() {
|
|
|
7081
7177
|
counter += 1;
|
|
7082
7178
|
return `${Date.now().toString(36)}${counter.toString(36)}`;
|
|
7083
7179
|
}
|
|
7180
|
+
/** A derive that has not finished by now will not finish inside the drain. */
|
|
7181
|
+
var GIF_RENDER_TIMEOUT_MS = 2e4;
|
|
7182
|
+
/**
|
|
7183
|
+
* Derive the gif. `null` on every failure — a notification that lost its gif is
|
|
7184
|
+
* a notification, and one that failed is not.
|
|
7185
|
+
*
|
|
7186
|
+
* The child is guaranteed dead before this settles: these run on every
|
|
7187
|
+
* notification that asks for a gif, and a leaked ffmpeg accumulates on the node
|
|
7188
|
+
* until somebody notices the host is slow.
|
|
7189
|
+
*/
|
|
7190
|
+
async function gifFromMp4(input) {
|
|
7191
|
+
const args = require_dist.gifFromMp4Args({
|
|
7192
|
+
maxWidth: input.maxWidth ?? 640,
|
|
7193
|
+
fps: input.fps ?? 12,
|
|
7194
|
+
speed: input.speed ?? 4
|
|
7195
|
+
});
|
|
7196
|
+
const startedAt = Date.now();
|
|
7197
|
+
try {
|
|
7198
|
+
const bytes = await runGif(input, args);
|
|
7199
|
+
input.logger?.info("nc gif: derived from the notification video", {
|
|
7200
|
+
tags: { deviceId: input.deviceId },
|
|
7201
|
+
meta: {
|
|
7202
|
+
sourceBytes: input.mp4.length,
|
|
7203
|
+
gifBytes: bytes.length,
|
|
7204
|
+
width: input.maxWidth ?? 640,
|
|
7205
|
+
fps: input.fps ?? 12,
|
|
7206
|
+
speed: input.speed ?? 4,
|
|
7207
|
+
encodeMs: Date.now() - startedAt
|
|
7208
|
+
}
|
|
7209
|
+
});
|
|
7210
|
+
return bytes;
|
|
7211
|
+
} catch (err) {
|
|
7212
|
+
input.logger?.warn("nc gif: the derive failed — this notification ships without a gif", {
|
|
7213
|
+
tags: { deviceId: input.deviceId },
|
|
7214
|
+
meta: {
|
|
7215
|
+
sourceBytes: input.mp4.length,
|
|
7216
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7217
|
+
}
|
|
7218
|
+
});
|
|
7219
|
+
return null;
|
|
7220
|
+
}
|
|
7221
|
+
}
|
|
7222
|
+
function runGif(input, args) {
|
|
7223
|
+
return new Promise((resolve, reject) => {
|
|
7224
|
+
const proc = input.spawnFn(input.ffmpegPath, [...args], { stdio: [
|
|
7225
|
+
"pipe",
|
|
7226
|
+
"pipe",
|
|
7227
|
+
"pipe"
|
|
7228
|
+
] });
|
|
7229
|
+
const chunks = [];
|
|
7230
|
+
let stderrTail = "";
|
|
7231
|
+
let done = false;
|
|
7232
|
+
let timer = null;
|
|
7233
|
+
const settle = (err, out) => {
|
|
7234
|
+
if (done) return;
|
|
7235
|
+
done = true;
|
|
7236
|
+
if (timer !== null) clearTimeout(timer);
|
|
7237
|
+
if (proc.exitCode === null && proc.signalCode === null) try {
|
|
7238
|
+
proc.kill("SIGKILL");
|
|
7239
|
+
} catch {}
|
|
7240
|
+
try {
|
|
7241
|
+
proc.stdout?.removeAllListeners();
|
|
7242
|
+
proc.stderr?.removeAllListeners();
|
|
7243
|
+
proc.stdin?.destroy();
|
|
7244
|
+
} catch {}
|
|
7245
|
+
if (err !== null) reject(err);
|
|
7246
|
+
else resolve(out ?? new Uint8Array(0));
|
|
7247
|
+
};
|
|
7248
|
+
timer = setTimeout(() => settle(/* @__PURE__ */ new Error("gif derive timed out")), GIF_RENDER_TIMEOUT_MS);
|
|
7249
|
+
timer.unref?.();
|
|
7250
|
+
proc.stdout?.on("data", (c) => chunks.push(c));
|
|
7251
|
+
proc.stderr?.on("data", (c) => {
|
|
7252
|
+
stderrTail = (stderrTail + c.toString()).slice(-400);
|
|
7253
|
+
});
|
|
7254
|
+
proc.on("error", (err) => settle(err instanceof Error ? err : new Error(String(err))));
|
|
7255
|
+
proc.on("close", (code) => {
|
|
7256
|
+
if (code !== 0) {
|
|
7257
|
+
settle(/* @__PURE__ */ new Error(`ffmpeg gif derive exited ${code}: ${stderrTail}`));
|
|
7258
|
+
return;
|
|
7259
|
+
}
|
|
7260
|
+
const out = Buffer.concat(chunks);
|
|
7261
|
+
if (out.byteLength === 0) {
|
|
7262
|
+
settle(/* @__PURE__ */ new Error("gif derive produced no bytes"));
|
|
7263
|
+
return;
|
|
7264
|
+
}
|
|
7265
|
+
const bytes = new Uint8Array(out.byteLength);
|
|
7266
|
+
bytes.set(out);
|
|
7267
|
+
settle(null, bytes);
|
|
7268
|
+
});
|
|
7269
|
+
proc.stdin?.on("error", () => {});
|
|
7270
|
+
proc.stdin?.write(Buffer.from(input.mp4));
|
|
7271
|
+
proc.stdin?.end();
|
|
7272
|
+
});
|
|
7273
|
+
}
|
|
7084
7274
|
//#endregion
|
|
7085
7275
|
//#region src/stream-broker/webrtc/nal-utils.ts
|
|
7086
7276
|
var NAL_START_CODE_4B = Buffer.from([
|
|
@@ -7787,15 +7977,6 @@ function eventClipEncodeWidth(sourceWidth, speed) {
|
|
|
7787
7977
|
return Math.min(source, EVENT_CLIP_ENCODE_MAX_WIDTH);
|
|
7788
7978
|
}
|
|
7789
7979
|
/**
|
|
7790
|
-
* Format a frame rate for an ffmpeg filter argument.
|
|
7791
|
-
*
|
|
7792
|
-
* `12 / 4` must reach the filter as `3` and `15 / 4` as `3.75`, not as a float
|
|
7793
|
-
* tail that `av_parse_video_rate` then has to guess at.
|
|
7794
|
-
*/
|
|
7795
|
-
function ffmpegRate(rate) {
|
|
7796
|
-
return String(Math.round(rate * 1e3) / 1e3);
|
|
7797
|
-
}
|
|
7798
|
-
/**
|
|
7799
7980
|
* Whether this render can COPY the camera's own H.264 instead of re-encoding it.
|
|
7800
7981
|
*
|
|
7801
7982
|
* The ring already holds the camera's elementary stream. Re-encoding it is only
|
|
@@ -7831,7 +8012,7 @@ function clipFfmpegArgs(input) {
|
|
|
7831
8012
|
const demuxer = input.hevc === true ? "hevc" : "h264";
|
|
7832
8013
|
const speed = input.speed ?? 2;
|
|
7833
8014
|
const setpts = speed !== 1 ? `,setpts=PTS/${speed}` : "";
|
|
7834
|
-
const gifSetpts = speed !== 1 ? `${setpts},fps=${ffmpegRate(input.fps * speed)}` : setpts;
|
|
8015
|
+
const gifSetpts = speed !== 1 ? `${setpts},fps=${require_dist.ffmpegRate(input.fps * speed)}` : setpts;
|
|
7835
8016
|
if (input.format === "gif") return [
|
|
7836
8017
|
"-y",
|
|
7837
8018
|
"-f",
|
|
@@ -7839,7 +8020,7 @@ function clipFfmpegArgs(input) {
|
|
|
7839
8020
|
"-i",
|
|
7840
8021
|
"pipe:0",
|
|
7841
8022
|
"-filter_complex",
|
|
7842
|
-
`[0:v]fps=${ffmpegRate(input.fps)}${gifSetpts},${scale},split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle`,
|
|
8023
|
+
`[0:v]fps=${require_dist.ffmpegRate(input.fps)}${gifSetpts},${scale},split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle`,
|
|
7843
8024
|
"-loop",
|
|
7844
8025
|
"0",
|
|
7845
8026
|
"-f",
|
|
@@ -7904,7 +8085,7 @@ async function renderPreBufferClip(input) {
|
|
|
7904
8085
|
hevc,
|
|
7905
8086
|
sourceWidth: input.sourceWidth ?? 0
|
|
7906
8087
|
});
|
|
7907
|
-
const proc = input.spawn(input.ffmpegPath
|
|
8088
|
+
const proc = input.spawn(input.ffmpegPath, clipFfmpegArgs({
|
|
7908
8089
|
...input,
|
|
7909
8090
|
hevc,
|
|
7910
8091
|
copyVideo
|
|
@@ -7950,142 +8131,6 @@ async function renderPreBufferClip(input) {
|
|
|
7950
8131
|
proc.stdin?.end();
|
|
7951
8132
|
});
|
|
7952
8133
|
}
|
|
7953
|
-
/**
|
|
7954
|
-
* The factor the derive must apply ON TOP of the speed the mp4 already carries.
|
|
7955
|
-
*
|
|
7956
|
-
* The gif is derived from the muxed mp4, so if that mux already ran `setpts` at
|
|
7957
|
-
* the caller's `speed`, applying the absolute gif rate again would compound
|
|
7958
|
-
* them — a 4× gif beside a 2× video would come out at 8×. Discounting here is
|
|
7959
|
-
* what keeps {@link GIF_SPEED} an ABSOLUTE rate against real time whatever the
|
|
7960
|
-
* video does.
|
|
7961
|
-
*/
|
|
7962
|
-
function gifDeriveSpeed(gifSpeed, videoSpeed) {
|
|
7963
|
-
if (!Number.isFinite(videoSpeed) || videoSpeed <= 0) return gifSpeed;
|
|
7964
|
-
return gifSpeed / videoSpeed;
|
|
7965
|
-
}
|
|
7966
|
-
/** A derive that has not finished by now will not finish inside the drain. */
|
|
7967
|
-
var GIF_RENDER_TIMEOUT_MS = 2e4;
|
|
7968
|
-
/**
|
|
7969
|
-
* The filter chain. Extracted so a test can assert the palette pair without
|
|
7970
|
-
* spawning ffmpeg — an argv test cannot see quality, but it can see a filter
|
|
7971
|
-
* that somebody deleted.
|
|
7972
|
-
*/
|
|
7973
|
-
function gifFilterComplex(input) {
|
|
7974
|
-
const speed = input.speed > 0 ? input.speed : 1;
|
|
7975
|
-
if (speed === 1) return `[0:v]fps=${ffmpegRate(input.fps)},scale=${input.maxWidth}:-2:flags=lanczos,split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle`;
|
|
7976
|
-
return `[0:v]fps=${ffmpegRate(input.fps / speed)},setpts=PTS/${speed},fps=${ffmpegRate(input.fps)},scale=${input.maxWidth}:-2:flags=lanczos,split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle`;
|
|
7977
|
-
}
|
|
7978
|
-
/** The full argv, for the same reason. */
|
|
7979
|
-
function gifFromMp4Args(input) {
|
|
7980
|
-
return [
|
|
7981
|
-
"-y",
|
|
7982
|
-
"-v",
|
|
7983
|
-
"error",
|
|
7984
|
-
"-f",
|
|
7985
|
-
"mp4",
|
|
7986
|
-
"-i",
|
|
7987
|
-
"pipe:0",
|
|
7988
|
-
"-filter_complex",
|
|
7989
|
-
gifFilterComplex(input),
|
|
7990
|
-
"-loop",
|
|
7991
|
-
"0",
|
|
7992
|
-
"-f",
|
|
7993
|
-
"gif",
|
|
7994
|
-
"pipe:1"
|
|
7995
|
-
];
|
|
7996
|
-
}
|
|
7997
|
-
/**
|
|
7998
|
-
* Derive the gif. `null` on every failure — a notification that lost its gif is
|
|
7999
|
-
* a notification, and one that failed is not.
|
|
8000
|
-
*
|
|
8001
|
-
* The child is guaranteed dead before this settles: these run on every
|
|
8002
|
-
* notification that asks for a gif, and a leaked ffmpeg accumulates on the node
|
|
8003
|
-
* until somebody notices the host is slow.
|
|
8004
|
-
*/
|
|
8005
|
-
async function gifFromMp4(input) {
|
|
8006
|
-
const args = gifFromMp4Args({
|
|
8007
|
-
maxWidth: input.maxWidth ?? 640,
|
|
8008
|
-
fps: input.fps ?? 12,
|
|
8009
|
-
speed: input.speed ?? 4
|
|
8010
|
-
});
|
|
8011
|
-
const startedAt = Date.now();
|
|
8012
|
-
try {
|
|
8013
|
-
const bytes = await runGif(input, args);
|
|
8014
|
-
input.logger?.info("nc gif: derived from the notification video", {
|
|
8015
|
-
tags: { deviceId: input.deviceId },
|
|
8016
|
-
meta: {
|
|
8017
|
-
sourceBytes: input.mp4.length,
|
|
8018
|
-
gifBytes: bytes.length,
|
|
8019
|
-
width: input.maxWidth ?? 640,
|
|
8020
|
-
fps: input.fps ?? 12,
|
|
8021
|
-
speed: input.speed ?? 4,
|
|
8022
|
-
encodeMs: Date.now() - startedAt
|
|
8023
|
-
}
|
|
8024
|
-
});
|
|
8025
|
-
return bytes;
|
|
8026
|
-
} catch (err) {
|
|
8027
|
-
input.logger?.warn("nc gif: the derive failed — this notification ships without a gif", {
|
|
8028
|
-
tags: { deviceId: input.deviceId },
|
|
8029
|
-
meta: {
|
|
8030
|
-
sourceBytes: input.mp4.length,
|
|
8031
|
-
error: err instanceof Error ? err.message : String(err)
|
|
8032
|
-
}
|
|
8033
|
-
});
|
|
8034
|
-
return null;
|
|
8035
|
-
}
|
|
8036
|
-
}
|
|
8037
|
-
function runGif(input, args) {
|
|
8038
|
-
return new Promise((resolve, reject) => {
|
|
8039
|
-
const proc = input.spawnFn(input.ffmpegPath ?? "ffmpeg", [...args], { stdio: [
|
|
8040
|
-
"pipe",
|
|
8041
|
-
"pipe",
|
|
8042
|
-
"pipe"
|
|
8043
|
-
] });
|
|
8044
|
-
const chunks = [];
|
|
8045
|
-
let stderrTail = "";
|
|
8046
|
-
let done = false;
|
|
8047
|
-
let timer = null;
|
|
8048
|
-
const settle = (err, out) => {
|
|
8049
|
-
if (done) return;
|
|
8050
|
-
done = true;
|
|
8051
|
-
if (timer !== null) clearTimeout(timer);
|
|
8052
|
-
if (proc.exitCode === null && proc.signalCode === null) try {
|
|
8053
|
-
proc.kill("SIGKILL");
|
|
8054
|
-
} catch {}
|
|
8055
|
-
try {
|
|
8056
|
-
proc.stdout?.removeAllListeners();
|
|
8057
|
-
proc.stderr?.removeAllListeners();
|
|
8058
|
-
proc.stdin?.destroy();
|
|
8059
|
-
} catch {}
|
|
8060
|
-
if (err !== null) reject(err);
|
|
8061
|
-
else resolve(out ?? new Uint8Array(0));
|
|
8062
|
-
};
|
|
8063
|
-
timer = setTimeout(() => settle(/* @__PURE__ */ new Error("gif derive timed out")), GIF_RENDER_TIMEOUT_MS);
|
|
8064
|
-
timer.unref?.();
|
|
8065
|
-
proc.stdout?.on("data", (c) => chunks.push(c));
|
|
8066
|
-
proc.stderr?.on("data", (c) => {
|
|
8067
|
-
stderrTail = (stderrTail + c.toString()).slice(-400);
|
|
8068
|
-
});
|
|
8069
|
-
proc.on("error", (err) => settle(err instanceof Error ? err : new Error(String(err))));
|
|
8070
|
-
proc.on("close", (code) => {
|
|
8071
|
-
if (code !== 0) {
|
|
8072
|
-
settle(/* @__PURE__ */ new Error(`ffmpeg gif derive exited ${code}: ${stderrTail}`));
|
|
8073
|
-
return;
|
|
8074
|
-
}
|
|
8075
|
-
const out = Buffer.concat(chunks);
|
|
8076
|
-
if (out.byteLength === 0) {
|
|
8077
|
-
settle(/* @__PURE__ */ new Error("gif derive produced no bytes"));
|
|
8078
|
-
return;
|
|
8079
|
-
}
|
|
8080
|
-
const bytes = new Uint8Array(out.byteLength);
|
|
8081
|
-
bytes.set(out);
|
|
8082
|
-
settle(null, bytes);
|
|
8083
|
-
});
|
|
8084
|
-
proc.stdin?.on("error", () => {});
|
|
8085
|
-
proc.stdin?.write(Buffer.from(input.mp4));
|
|
8086
|
-
proc.stdin?.end();
|
|
8087
|
-
});
|
|
8088
|
-
}
|
|
8089
8134
|
//#endregion
|
|
8090
8135
|
//#region src/stream-broker/stream-broker/sentry-encode.ts
|
|
8091
8136
|
/**
|
|
@@ -12857,11 +12902,12 @@ var StreamBroker = class StreamBroker {
|
|
|
12857
12902
|
/** Active "ffmpeg parser" ingress reader when `source.ffmpegParser` is set. */
|
|
12858
12903
|
ffmpegSourceReader = null;
|
|
12859
12904
|
/**
|
|
12860
|
-
*
|
|
12861
|
-
*
|
|
12862
|
-
*
|
|
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).
|
|
12863
12909
|
*/
|
|
12864
|
-
|
|
12910
|
+
ffmpegBinaryPathSource = () => null;
|
|
12865
12911
|
rtpDepacketizer = null;
|
|
12866
12912
|
/** Flag-only depacketizer for the `push-rtp` source-RTP pre-buffer path
|
|
12867
12913
|
* (sets keyframe/param-set flags without re-emitting AnnexB packets). */
|
|
@@ -13444,12 +13490,21 @@ var StreamBroker = class StreamBroker {
|
|
|
13444
13490
|
this.restreamUrlResolver = resolver;
|
|
13445
13491
|
}
|
|
13446
13492
|
/**
|
|
13447
|
-
* Inject
|
|
13448
|
-
*
|
|
13449
|
-
* 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.
|
|
13450
13495
|
*/
|
|
13451
|
-
|
|
13452
|
-
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;
|
|
13453
13508
|
}
|
|
13454
13509
|
/**
|
|
13455
13510
|
* Inject the addon's child-cost registry. Injected rather than constructed
|
|
@@ -15375,18 +15430,28 @@ var StreamBroker = class StreamBroker {
|
|
|
15375
15430
|
transport: source.ffmpegParser?.transport ?? "tcp"
|
|
15376
15431
|
} });
|
|
15377
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
|
+
}
|
|
15378
15444
|
const parser = source.ffmpegParser;
|
|
15379
15445
|
const sourceKind = source.type === "rtmp" ? "rtmp" : source.type === "flv" || source.type === "http" ? "http" : "rtsp";
|
|
15380
|
-
const args = buildFfmpegSourceReaderArgs({
|
|
15381
|
-
sourceUrl: source.url,
|
|
15382
|
-
sourceKind,
|
|
15383
|
-
transport: parser?.transport ?? "tcp",
|
|
15384
|
-
...parser?.inputArgs ? { inputArgs: parser.inputArgs } : {},
|
|
15385
|
-
...parser?.outputArgs ? { outputArgs: parser.outputArgs } : {}
|
|
15386
|
-
});
|
|
15387
15446
|
const reader = new FfmpegSourceReader({
|
|
15388
|
-
ffmpegPath
|
|
15389
|
-
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
|
+
}),
|
|
15390
15455
|
...this.logger ? { logger: this.logger.child("ffmpeg-source") } : {},
|
|
15391
15456
|
claimCost: (pid) => this.claimIngestCost(pid)
|
|
15392
15457
|
}, {
|
|
@@ -16362,7 +16427,7 @@ function hexToBytes(hex) {
|
|
|
16362
16427
|
* pipe is the pattern proven by the WebRTC transcode feed.
|
|
16363
16428
|
*/
|
|
16364
16429
|
var DEFAULT_FFMPEG_CONFIG = {
|
|
16365
|
-
binaryPath:
|
|
16430
|
+
binaryPath: null,
|
|
16366
16431
|
hwAccel: "copy",
|
|
16367
16432
|
threadCount: 0
|
|
16368
16433
|
};
|
|
@@ -16396,6 +16461,7 @@ var TranscodePipelineManager = class {
|
|
|
16396
16461
|
resolveProfileSource;
|
|
16397
16462
|
releaseGraceMs;
|
|
16398
16463
|
ffmpegConfig;
|
|
16464
|
+
onFfmpegMissing;
|
|
16399
16465
|
resolveStreamHwAccel;
|
|
16400
16466
|
spawnFn;
|
|
16401
16467
|
acquireChannel;
|
|
@@ -16408,6 +16474,7 @@ var TranscodePipelineManager = class {
|
|
|
16408
16474
|
this.resolveProfileSource = opts.resolveProfileSource ?? null;
|
|
16409
16475
|
this.releaseGraceMs = opts.releaseGraceMs ?? 5e3;
|
|
16410
16476
|
this.ffmpegConfig = opts.ffmpegConfig ?? (() => DEFAULT_FFMPEG_CONFIG);
|
|
16477
|
+
this.onFfmpegMissing = opts.onFfmpegMissing ?? null;
|
|
16411
16478
|
this.probeFfmpeg = opts.probeFfmpeg ?? require_node.probeFfmpegBinary;
|
|
16412
16479
|
this.resolveStreamHwAccel = opts.resolveStreamHwAccel ?? null;
|
|
16413
16480
|
this.spawnFn = opts.spawnFn ?? node_child_process.spawn;
|
|
@@ -16480,11 +16547,17 @@ var TranscodePipelineManager = class {
|
|
|
16480
16547
|
async startTranscode(opts) {
|
|
16481
16548
|
if (!this.acquireChannel) throw new Error("getStreamWithCodec: transcode egress not wired (acquireChannel missing)");
|
|
16482
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
|
+
});
|
|
16483
16556
|
const effectiveHwAccel = this.effectiveHwAccelSetting(opts.deviceId, opts.camStreamId, cfg.hwAccel);
|
|
16484
16557
|
const choice = chooseVideoEncoder({
|
|
16485
16558
|
target: opts.targetCodec,
|
|
16486
16559
|
backend: await this.resolveHardwareBackend(),
|
|
16487
|
-
binaryEncoders: await this.resolveBinaryEncoders(
|
|
16560
|
+
binaryEncoders: await this.resolveBinaryEncoders(ffmpegBinaryPath),
|
|
16488
16561
|
hwAccelSetting: effectiveHwAccel,
|
|
16489
16562
|
renderNode: DEFAULT_VAAPI_RENDER_DEVICE
|
|
16490
16563
|
});
|
|
@@ -16514,7 +16587,7 @@ var TranscodePipelineManager = class {
|
|
|
16514
16587
|
const sidecarCodec = opts.audioEgress ? toAudioSidecarCodec(opts.audio) : null;
|
|
16515
16588
|
const egress = new TranscodeEgress({
|
|
16516
16589
|
logger: this.logger.child("egress"),
|
|
16517
|
-
ffmpegBinaryPath
|
|
16590
|
+
ffmpegBinaryPath,
|
|
16518
16591
|
spawnFn: this.spawnFn,
|
|
16519
16592
|
acquireChannel: this.acquireChannel
|
|
16520
16593
|
}, {
|
|
@@ -16743,11 +16816,12 @@ var TranscodePipelineManager = class {
|
|
|
16743
16816
|
try {
|
|
16744
16817
|
const backend = rankDecodeHwAccels((await this.hwaccel.resolve(prefer)).preferred, [])[0] ?? null;
|
|
16745
16818
|
if (backend === null) return null;
|
|
16746
|
-
const
|
|
16819
|
+
const binaryPath = this.ffmpegConfig().binaryPath;
|
|
16820
|
+
const caps = binaryPath === null ? null : await this.resolveBinaryCapabilities(binaryPath);
|
|
16747
16821
|
if (caps !== null && !caps.hwaccels.has(backend)) {
|
|
16748
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: {
|
|
16749
16823
|
backend,
|
|
16750
|
-
ffmpeg:
|
|
16824
|
+
ffmpeg: binaryPath,
|
|
16751
16825
|
hwaccels: [...caps.hwaccels].join(",") || "none"
|
|
16752
16826
|
} });
|
|
16753
16827
|
return null;
|
|
@@ -17428,12 +17502,14 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17428
17502
|
/**
|
|
17429
17503
|
* Snapshot of the system `ffmpeg` config section (binaryPath / hwAccel /
|
|
17430
17504
|
* threadCount). Refreshed by the host addon via `setFfmpegConfig`; the
|
|
17431
|
-
* transcode manager reads it live per spawn.
|
|
17432
|
-
*
|
|
17433
|
-
*
|
|
17505
|
+
* transcode manager reads it live per spawn. Until the addon resolves the
|
|
17506
|
+
* node's pinned binary there is NONE (`binaryPath: null`) and every child
|
|
17507
|
+
* refuses by name — never a PATH `ffmpeg` (D540). `'copy'` software decode
|
|
17508
|
+
* and auto threads stay the defaults: software decode never hard-fails on a
|
|
17509
|
+
* fragile HEVC source.
|
|
17434
17510
|
*/
|
|
17435
17511
|
ffmpegConfig = {
|
|
17436
|
-
binaryPath:
|
|
17512
|
+
binaryPath: null,
|
|
17437
17513
|
hwAccel: "copy",
|
|
17438
17514
|
threadCount: 0
|
|
17439
17515
|
};
|
|
@@ -17469,6 +17545,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17469
17545
|
},
|
|
17470
17546
|
resolveProfileSource: (deviceId, profile) => this.assignments.get(deviceId)?.map[profile] ?? null,
|
|
17471
17547
|
ffmpegConfig: () => this.ffmpegConfig,
|
|
17548
|
+
onFfmpegMissing: () => this.reportFfmpegMissing(),
|
|
17472
17549
|
resolveStreamHwAccel: (deviceId, camStreamId) => this.getStreamHwAccelOverride(deviceId, camStreamId),
|
|
17473
17550
|
sentryOffered: async (deviceId) => sentryEligible({
|
|
17474
17551
|
batteryOperated: await this.isBatteryOperated(deviceId),
|
|
@@ -17531,6 +17608,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17531
17608
|
});
|
|
17532
17609
|
},
|
|
17533
17610
|
ffmpegConfig: () => this.ffmpegConfig,
|
|
17611
|
+
onFfmpegMissing: () => this.reportFfmpegMissing(),
|
|
17534
17612
|
...this.costRegistry === null ? {} : { costRegistry: this.costRegistry },
|
|
17535
17613
|
resolveDecodeHwAccel: (deviceId, override) => this.resolveEgressHwAccel(deviceId, override),
|
|
17536
17614
|
acquireChannel: (brokerId) => {
|
|
@@ -17906,11 +17984,35 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17906
17984
|
if (localNodeId !== void 0) this.localNodeId = localNodeId;
|
|
17907
17985
|
if (hwaccel !== void 0) this.hwaccel = hwaccel;
|
|
17908
17986
|
}
|
|
17909
|
-
/**
|
|
17987
|
+
/**
|
|
17988
|
+
* Update the node's ffmpeg config snapshot. Every broker READS it per dial
|
|
17989
|
+
* ({@link bindBrokerFfmpeg}), so a re-resolved binary reaches live brokers —
|
|
17990
|
+
* including one created while the re-resolve was in flight — with no copy to
|
|
17991
|
+
* fan out (D540).
|
|
17992
|
+
*/
|
|
17910
17993
|
setFfmpegConfig(cfg) {
|
|
17911
17994
|
this.ffmpegConfig = cfg;
|
|
17912
17995
|
}
|
|
17913
17996
|
/**
|
|
17997
|
+
* Point a broker at the node's LIVE pinned ffmpeg and at the re-resolve
|
|
17998
|
+
* request, so its ffmpeg-parser ingress spawns the current binary and a
|
|
17999
|
+
* refusal asks for it again.
|
|
18000
|
+
*/
|
|
18001
|
+
bindBrokerFfmpeg(broker) {
|
|
18002
|
+
broker.setFfmpegBinaryPathSource(() => this.ffmpegConfig.binaryPath);
|
|
18003
|
+
broker.setFfmpegMissingHandler(() => this.reportFfmpegMissing());
|
|
18004
|
+
}
|
|
18005
|
+
/** Who asks for the pinned binary again after a refusal. Set by the addon. */
|
|
18006
|
+
ffmpegMissingHandler = null;
|
|
18007
|
+
/** Wire the host's re-resolve (`FfmpegReresolver.request`). */
|
|
18008
|
+
setFfmpegMissingHandler(handler) {
|
|
18009
|
+
this.ffmpegMissingHandler = handler;
|
|
18010
|
+
}
|
|
18011
|
+
/** A child was refused for want of the pinned ffmpeg: ask the host again. */
|
|
18012
|
+
reportFfmpegMissing() {
|
|
18013
|
+
this.ffmpegMissingHandler?.();
|
|
18014
|
+
}
|
|
18015
|
+
/**
|
|
17914
18016
|
* The decode `hwaccel` options surfaceable in a (future) UI: the always-valid
|
|
17915
18017
|
* settings (`copy`, `auto`, `none`) plus the concrete hardware backends THIS
|
|
17916
18018
|
* node can use, resolved from the LOCAL `ctx.kernel.hwaccel` (os.platform +
|
|
@@ -18251,7 +18353,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18251
18353
|
let annexB;
|
|
18252
18354
|
try {
|
|
18253
18355
|
annexB = await encodePrestreamIdr({
|
|
18254
|
-
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
18356
|
+
ffmpegPath: requireBrokerFfmpeg(this.ffmpegConfig.binaryPath, {
|
|
18357
|
+
logger,
|
|
18358
|
+
deviceId,
|
|
18359
|
+
purpose: "sentry pre-stream encode",
|
|
18360
|
+
onMissing: () => this.reportFfmpegMissing()
|
|
18361
|
+
}),
|
|
18255
18362
|
plan,
|
|
18256
18363
|
jpeg: picture.jpeg,
|
|
18257
18364
|
logger,
|
|
@@ -19503,16 +19610,21 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19503
19610
|
height: scaledHeight(cut, clipWidth)
|
|
19504
19611
|
});
|
|
19505
19612
|
const gifSpeed = input.gifSpeed ?? 4;
|
|
19506
|
-
|
|
19613
|
+
const gifFfmpeg = this.ffmpegConfig.binaryPath;
|
|
19614
|
+
if (input.kinds.includes("gif") && gifFfmpeg === null) {
|
|
19615
|
+
this.logger?.error("event media gif refused — no pinned ffmpeg on this node (D540)", { tags: { deviceId: input.deviceId } });
|
|
19616
|
+
this.reportFfmpegMissing();
|
|
19617
|
+
}
|
|
19618
|
+
if (input.kinds.includes("gif") && gifFfmpeg !== null) {
|
|
19507
19619
|
const gif = await gifFromMp4({
|
|
19508
19620
|
mp4,
|
|
19509
19621
|
deviceId: input.deviceId,
|
|
19510
19622
|
maxWidth: input.gifMaxWidth,
|
|
19511
19623
|
fps: input.gifFps,
|
|
19512
|
-
speed: gifDeriveSpeed(gifSpeed, input.speed),
|
|
19624
|
+
speed: require_dist.gifDeriveSpeed(gifSpeed, input.speed),
|
|
19513
19625
|
...this.logger !== void 0 ? { logger: this.logger } : {},
|
|
19514
19626
|
spawnFn: node_child_process.spawn,
|
|
19515
|
-
|
|
19627
|
+
ffmpegPath: gifFfmpeg
|
|
19516
19628
|
});
|
|
19517
19629
|
if (gif !== null) {
|
|
19518
19630
|
const height = scaledHeight(cut, input.gifMaxWidth);
|
|
@@ -19681,7 +19793,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19681
19793
|
"pipe",
|
|
19682
19794
|
"pipe"
|
|
19683
19795
|
] }),
|
|
19684
|
-
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
19796
|
+
ffmpegPath: requireBrokerFfmpeg(this.ffmpegConfig.binaryPath, {
|
|
19797
|
+
logger: this.logger,
|
|
19798
|
+
deviceId: cut.deviceId,
|
|
19799
|
+
purpose: "pre-buffer clip render",
|
|
19800
|
+
onMissing: () => this.reportFfmpegMissing()
|
|
19801
|
+
}),
|
|
19685
19802
|
sdpParameterSets: cut.broker.getSdpParameterSets()
|
|
19686
19803
|
});
|
|
19687
19804
|
this.logger?.info("pre-buffer clip rendered", {
|
|
@@ -21759,7 +21876,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21759
21876
|
const source = this.buildSourceFromCamStream(cam);
|
|
21760
21877
|
const broker = new StreamBroker(brokerId, this.buildDecoderCapApi(deviceId), brokerLogger, buildAudioCodecCapApi(this.api), this.localNodeId);
|
|
21761
21878
|
broker.setRestreamUrlResolver(() => this.rtspProvider.getEntry(brokerId)?.mutedUrl ?? null);
|
|
21762
|
-
|
|
21879
|
+
this.bindBrokerFfmpeg(broker);
|
|
21763
21880
|
if (this.costRegistry !== null) broker.setCostRegistry(this.costRegistry);
|
|
21764
21881
|
this.applyPrebufferConfig(broker, deviceId, camStreamId);
|
|
21765
21882
|
this.applyAudioMuteConfig(broker, deviceId);
|
|
@@ -21937,7 +22054,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21937
22054
|
}
|
|
21938
22055
|
});
|
|
21939
22056
|
const feeder = new DerivedStreamFeeder({
|
|
21940
|
-
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
22057
|
+
ffmpegPath: requireBrokerFfmpeg(this.ffmpegConfig.binaryPath, {
|
|
22058
|
+
logger: this.logger,
|
|
22059
|
+
deviceId,
|
|
22060
|
+
purpose: "derived transcode",
|
|
22061
|
+
onMissing: () => this.reportFfmpegMissing()
|
|
22062
|
+
}),
|
|
21941
22063
|
args,
|
|
21942
22064
|
logger: this.logger.child(`derived-ffmpeg:${camStreamId}`).withTags({ deviceId })
|
|
21943
22065
|
}, {
|
|
@@ -33885,7 +34007,7 @@ var offThreadJsParse = (bytes) => {
|
|
|
33885
34007
|
* Drop-in for the session `demuxVideo` hook: JS parse off the live loop,
|
|
33886
34008
|
* ffmpeg subprocess fallback unchanged.
|
|
33887
34009
|
*/
|
|
33888
|
-
async function demuxSegmentPreferJsOffThread(bytes, opts
|
|
34010
|
+
async function demuxSegmentPreferJsOffThread(bytes, opts) {
|
|
33889
34011
|
return require_segment_demux_js.demuxSegmentPreferJs(bytes, {
|
|
33890
34012
|
...opts,
|
|
33891
34013
|
jsParse: offThreadJsParse
|
|
@@ -34070,9 +34192,9 @@ function sliceMuLawToPcmuFrames(mulaw) {
|
|
|
34070
34192
|
* can't decode) — ffmpeg exits non-zero, which is tolerated as empty output.
|
|
34071
34193
|
*
|
|
34072
34194
|
* @param bytes - The full `.m4s` fragment.
|
|
34073
|
-
* @param ffmpegPath -
|
|
34195
|
+
* @param ffmpegPath - The node's pinned ffmpeg (D540). Required: no PATH default.
|
|
34074
34196
|
*/
|
|
34075
|
-
async function demuxSegmentToPcmu(bytes, ffmpegPath
|
|
34197
|
+
async function demuxSegmentToPcmu(bytes, ffmpegPath) {
|
|
34076
34198
|
const mulaw = await require_segment_demux_js.runFfmpegOnce({
|
|
34077
34199
|
ffmpegPath,
|
|
34078
34200
|
args: SEGMENT_AUDIO_FFMPEG_ARGS,
|
|
@@ -34204,15 +34326,19 @@ var SEGMENT_AUDIO_OPUS_FFMPEG_ARGS = [
|
|
|
34204
34326
|
* best-effort and must never block recorded video. Mirrors the video-side
|
|
34205
34327
|
* `demuxSegmentPreferJs`.
|
|
34206
34328
|
*/
|
|
34207
|
-
async function demuxSegmentAudioOpusPreferJs(bytes, opts
|
|
34329
|
+
async function demuxSegmentAudioOpusPreferJs(bytes, opts) {
|
|
34208
34330
|
try {
|
|
34209
34331
|
const direct = await demuxSegmentAudioOpusJs(bytes);
|
|
34210
34332
|
if (direct) return direct;
|
|
34211
34333
|
} catch {}
|
|
34334
|
+
if (opts.ffmpegPath === null) {
|
|
34335
|
+
opts.onFfmpegFallback?.("audio track is not Opus (legacy fragment) and the ffmpeg fallback is refused — no pinned ffmpeg on this node (D540)");
|
|
34336
|
+
return [];
|
|
34337
|
+
}
|
|
34212
34338
|
opts.onFfmpegFallback?.("audio track is not Opus (legacy fragment)");
|
|
34213
34339
|
try {
|
|
34214
34340
|
const opusFmp4 = await require_segment_demux_js.runFfmpegOnce({
|
|
34215
|
-
ffmpegPath: opts.ffmpegPath
|
|
34341
|
+
ffmpegPath: opts.ffmpegPath,
|
|
34216
34342
|
args: SEGMENT_AUDIO_OPUS_FFMPEG_ARGS,
|
|
34217
34343
|
input: bytes,
|
|
34218
34344
|
tolerateNonZeroExit: true
|
|
@@ -35274,12 +35400,25 @@ var BrokerWebrtcServer = class {
|
|
|
35274
35400
|
session.onClosed = () => this.cleanupSession(sessionId);
|
|
35275
35401
|
const locateRecorded = this.locateRecordedSegment;
|
|
35276
35402
|
const readRecorded = this.readRecordedSegmentBytes;
|
|
35277
|
-
const
|
|
35403
|
+
const resolveFfmpegPath = this.resolveFfmpegPath;
|
|
35404
|
+
const resolveFfmpeg = () => resolveFfmpegPath?.() ?? null;
|
|
35405
|
+
let noPinnedFfmpegLogged = false;
|
|
35406
|
+
const reportNoPinnedFfmpeg = (what) => {
|
|
35407
|
+
if (noPinnedFfmpegLogged) return;
|
|
35408
|
+
noPinnedFfmpegLogged = true;
|
|
35409
|
+
sessionLogger.error("recorded playback: ffmpeg fallback refused — no pinned ffmpeg on this node (D540)", {
|
|
35410
|
+
tags: { deviceId },
|
|
35411
|
+
meta: {
|
|
35412
|
+
sessionId,
|
|
35413
|
+
what
|
|
35414
|
+
}
|
|
35415
|
+
});
|
|
35416
|
+
};
|
|
35278
35417
|
const demuxTally = new DemuxPathTally();
|
|
35279
35418
|
let audioOpusFallbackLogged = false;
|
|
35280
35419
|
if (locateRecorded && readRecorded) {
|
|
35281
35420
|
const demuxVideo = (b) => demuxSegmentPreferJsOffThread(b, {
|
|
35282
|
-
ffmpegPath: resolveFfmpeg
|
|
35421
|
+
ffmpegPath: resolveFfmpeg(),
|
|
35283
35422
|
onDemuxPath: (outcome) => {
|
|
35284
35423
|
const report = demuxTally.record(outcome, Date.now());
|
|
35285
35424
|
if (!report) return;
|
|
@@ -35294,6 +35433,9 @@ var BrokerWebrtcServer = class {
|
|
|
35294
35433
|
}
|
|
35295
35434
|
});
|
|
35296
35435
|
}
|
|
35436
|
+
}).catch((err) => {
|
|
35437
|
+
if (err instanceof require_segment_demux_js.FfmpegFallbackRefusedError) reportNoPinnedFfmpeg("video");
|
|
35438
|
+
throw err;
|
|
35297
35439
|
});
|
|
35298
35440
|
const scrubCache = new ScrubFragmentCache();
|
|
35299
35441
|
const initialLiveProfile = this.profileTierResolver?.(brokerId) ?? null;
|
|
@@ -35409,9 +35551,11 @@ var BrokerWebrtcServer = class {
|
|
|
35409
35551
|
pushAudioPcmu: (pcmu, ts) => session.writeRecordedAudio(Buffer.from(pcmu), ts)
|
|
35410
35552
|
};
|
|
35411
35553
|
const demuxAudio = opusSession ? async (b) => {
|
|
35554
|
+
const ffmpegPath = resolveFfmpeg();
|
|
35412
35555
|
return (await demuxSegmentAudioOpusPreferJs(b, {
|
|
35413
|
-
ffmpegPath
|
|
35556
|
+
ffmpegPath,
|
|
35414
35557
|
onFfmpegFallback: (reason) => {
|
|
35558
|
+
if (ffmpegPath === null) reportNoPinnedFfmpeg("audio");
|
|
35415
35559
|
if (audioOpusFallbackLogged) return;
|
|
35416
35560
|
audioOpusFallbackLogged = true;
|
|
35417
35561
|
sessionLogger.warn("recorded audio: JS Opus demux missed — ffmpeg AAC→Opus fallback (once/session)", { meta: {
|
|
@@ -35425,7 +35569,14 @@ var BrokerWebrtcServer = class {
|
|
|
35425
35569
|
data: au.data,
|
|
35426
35570
|
ptsMs: au.ptsMs
|
|
35427
35571
|
}));
|
|
35428
|
-
} : (b) =>
|
|
35572
|
+
} : async (b) => {
|
|
35573
|
+
const ffmpegPath = resolveFfmpeg();
|
|
35574
|
+
if (ffmpegPath === null) {
|
|
35575
|
+
reportNoPinnedFfmpeg("audio");
|
|
35576
|
+
return [];
|
|
35577
|
+
}
|
|
35578
|
+
return await demuxSegmentToPcmu(b, ffmpegPath);
|
|
35579
|
+
};
|
|
35429
35580
|
const clipSource = clip && readClip ? createClipFileSource({
|
|
35430
35581
|
handle: clip,
|
|
35431
35582
|
read: readClip,
|
|
@@ -36875,10 +37026,21 @@ var PROBE_SNAPSHOTS_PERSIST_INTERVAL_MS = 6e4;
|
|
|
36875
37026
|
var NETWORK_QUALITY_STATS_TIMEOUT_MS = 2e3;
|
|
36876
37027
|
var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
36877
37028
|
brokerManager = null;
|
|
36878
|
-
/**
|
|
36879
|
-
*
|
|
36880
|
-
*
|
|
36881
|
-
|
|
37029
|
+
/** The node's pinned ffmpeg, mirrored here so the recorded-playback demuxers
|
|
37030
|
+
* (segment-demux/segment-audio) spawn the same binary as the transcode
|
|
37031
|
+
* egress. Kept in sync by the ffmpeg wiring. `null` = none could be resolved:
|
|
37032
|
+
* their ffmpeg fallbacks refuse by name — never a PATH `ffmpeg` (D540). */
|
|
37033
|
+
ffmpegBinaryPath = null;
|
|
37034
|
+
/** The node's pinned build, asked once on success (D540). A failure is not
|
|
37035
|
+
* remembered, so the re-resolve below can succeed later. */
|
|
37036
|
+
pinnedFfmpeg = require_dist.memoizePinnedFfmpeg(() => this.ctx.deps.ensureFfmpeg());
|
|
37037
|
+
/**
|
|
37038
|
+
* The way back from a failed boot-time resolution: a refused child asks, and
|
|
37039
|
+
* ONE bounded re-resolve hands the binary to the manager config (which every
|
|
37040
|
+
* live broker reads per dial) and the demuxers. Without it one failed
|
|
37041
|
+
* download disabled every broker ffmpeg child until the runner restarted.
|
|
37042
|
+
*/
|
|
37043
|
+
ffmpegWiring = null;
|
|
36882
37044
|
/** Once-per-addon log guard for the readGopBytes → whole-segment downgrade. */
|
|
36883
37045
|
gopReadFallbackLogged = false;
|
|
36884
37046
|
metricsSnapshotTimer = null;
|
|
@@ -36939,36 +37101,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
36939
37101
|
async resolveFfmpegBinaryPath() {
|
|
36940
37102
|
const override = this.config.binaryPath;
|
|
36941
37103
|
if (typeof override === "string" && override.trim().length > 0) return override;
|
|
36942
|
-
return await this.
|
|
36943
|
-
}
|
|
36944
|
-
/**
|
|
36945
|
-
* Push THIS node's ffmpeg config into the broker — all node-local now, NOT the
|
|
36946
|
-
* retired global `ffmpeg` section. `binaryPath` via {@link resolveFfmpegBinaryPath}
|
|
36947
|
-
* (per-node override → ensureFfmpeg); `hwAccel` = the per-node `hwaccel`
|
|
36948
|
-
* preference (default `'copy'` = software, the safe HEVC-compatible choice; the
|
|
36949
|
-
* concrete backend is resolved node-locally by the egress transcode manager,
|
|
36950
|
-
* which asks the DECODER ADDON — never the kernel resolver, whose qsv-first
|
|
36951
|
-
* order is the known-bad one here). `threadCount` is always `0` (auto — ffmpeg uses the
|
|
36952
|
-
* node's own cores; the manual knob was removed).
|
|
36953
|
-
*/
|
|
36954
|
-
async loadFfmpegConfig() {
|
|
36955
|
-
if (!this.brokerManager) return;
|
|
36956
|
-
try {
|
|
36957
|
-
const binaryPath = await this.resolveFfmpegBinaryPath();
|
|
36958
|
-
const hwAccel = this.config.hwaccel;
|
|
36959
|
-
this.brokerManager.setFfmpegConfig({
|
|
36960
|
-
binaryPath,
|
|
36961
|
-
hwAccel,
|
|
36962
|
-
threadCount: 0
|
|
36963
|
-
});
|
|
36964
|
-
this.ffmpegBinaryPath = binaryPath;
|
|
36965
|
-
this.ctx.logger.info("Loaded ffmpeg config", { meta: {
|
|
36966
|
-
binaryPath,
|
|
36967
|
-
hwAccel
|
|
36968
|
-
} });
|
|
36969
|
-
} catch (err) {
|
|
36970
|
-
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) } });
|
|
36971
|
-
}
|
|
37104
|
+
return await this.pinnedFfmpeg();
|
|
36972
37105
|
}
|
|
36973
37106
|
async onInitialize() {
|
|
36974
37107
|
this.ctx.logger.info("log channels declared", { meta: { channels: STREAM_BROKER_LOG_CHANNELS.map((c) => c.descriptor.name) } });
|
|
@@ -36998,7 +37131,16 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
36998
37131
|
};
|
|
36999
37132
|
this.brokerManager.setDeviceSignalReader(async (deviceId) => (await readSignals(deviceId)).map((s) => s.code));
|
|
37000
37133
|
this.brokerManager.setDeviceSignalDetailReader(readSignals);
|
|
37001
|
-
|
|
37134
|
+
this.ffmpegWiring = wireBrokerFfmpeg({
|
|
37135
|
+
host: this.brokerManager,
|
|
37136
|
+
resolve: () => this.resolveFfmpegBinaryPath(),
|
|
37137
|
+
hwAccel: () => this.config.hwaccel,
|
|
37138
|
+
onBinaryPath: (binaryPath) => {
|
|
37139
|
+
this.ffmpegBinaryPath = binaryPath;
|
|
37140
|
+
},
|
|
37141
|
+
logger: this.ctx.logger
|
|
37142
|
+
});
|
|
37143
|
+
await this.ffmpegWiring.load();
|
|
37002
37144
|
await this.declareRowCollections();
|
|
37003
37145
|
await this.purgeFlattenedSettingsKeys();
|
|
37004
37146
|
const rtspProvider = this.brokerManager.getRtspRestreamProvider();
|
|
@@ -37283,7 +37425,10 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
37283
37425
|
};
|
|
37284
37426
|
}
|
|
37285
37427
|
},
|
|
37286
|
-
resolveFfmpegPath: () =>
|
|
37428
|
+
resolveFfmpegPath: () => {
|
|
37429
|
+
if (this.ffmpegBinaryPath === null) this.ffmpegWiring?.request("a recorded-playback demux needed ffmpeg");
|
|
37430
|
+
return this.ffmpegBinaryPath;
|
|
37431
|
+
}
|
|
37287
37432
|
});
|
|
37288
37433
|
this.brokerManager.setWebrtcServer(webrtcServer);
|
|
37289
37434
|
this.webrtcServer = webrtcServer;
|
|
@@ -37693,6 +37838,8 @@ var FfmpegDecoderSession = class {
|
|
|
37693
37838
|
outputBuffer = Buffer.alloc(0);
|
|
37694
37839
|
destroyed = false;
|
|
37695
37840
|
logger;
|
|
37841
|
+
/** The pinned ffmpeg to spawn (D540). Required: there is no PATH default. */
|
|
37842
|
+
ffmpegPath;
|
|
37696
37843
|
cachedWidth = 0;
|
|
37697
37844
|
cachedHeight = 0;
|
|
37698
37845
|
inputPackets = 0;
|
|
@@ -37701,8 +37848,9 @@ var FfmpegDecoderSession = class {
|
|
|
37701
37848
|
totalDecodeTimeMs = 0;
|
|
37702
37849
|
decodeCount = 0;
|
|
37703
37850
|
startTime = Date.now();
|
|
37704
|
-
constructor(config, logger = noopLogger) {
|
|
37851
|
+
constructor(config, ffmpegPath, logger = noopLogger) {
|
|
37705
37852
|
this.config = { ...config };
|
|
37853
|
+
this.ffmpegPath = ffmpegPath;
|
|
37706
37854
|
this.logger = logger;
|
|
37707
37855
|
this.frameDropper = new FrameDropper(config.maxFps);
|
|
37708
37856
|
this.spawnFfmpeg();
|
|
@@ -37713,7 +37861,8 @@ var FfmpegDecoderSession = class {
|
|
|
37713
37861
|
this.outputBuffer = Buffer.alloc(0);
|
|
37714
37862
|
this.cachedWidth = 0;
|
|
37715
37863
|
this.cachedHeight = 0;
|
|
37716
|
-
const
|
|
37864
|
+
const args = buildFfmpegArgs(this.config);
|
|
37865
|
+
const child = (0, node_child_process.spawn)(this.ffmpegPath, args);
|
|
37717
37866
|
this.process = child;
|
|
37718
37867
|
child.stdin?.on("error", () => {});
|
|
37719
37868
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -37846,7 +37995,14 @@ var SUPPORTED_CODECS = new Set([
|
|
|
37846
37995
|
"hevc",
|
|
37847
37996
|
"mjpeg"
|
|
37848
37997
|
]);
|
|
37998
|
+
/**
|
|
37999
|
+
* A static decoder provider for stand-alone broker harnesses. Production
|
|
38000
|
+
* decodes through the `decoder` cap; this exists for the manual streaming
|
|
38001
|
+
* diagnostics. It is handed the pinned ffmpeg (D540) — there is no PATH
|
|
38002
|
+
* default for it to fall back to.
|
|
38003
|
+
*/
|
|
37849
38004
|
var FfmpegDecoderProvider = class {
|
|
38005
|
+
ffmpegPath;
|
|
37850
38006
|
id = "ffmpeg";
|
|
37851
38007
|
name = "FFmpeg Decoder";
|
|
37852
38008
|
/** Software decoder — used as fallback when hardware decoders are unavailable. */
|
|
@@ -37854,8 +38010,11 @@ var FfmpegDecoderProvider = class {
|
|
|
37854
38010
|
async supportsCodec(input) {
|
|
37855
38011
|
return SUPPORTED_CODECS.has(input.codec);
|
|
37856
38012
|
}
|
|
38013
|
+
constructor(ffmpegPath) {
|
|
38014
|
+
this.ffmpegPath = ffmpegPath;
|
|
38015
|
+
}
|
|
37857
38016
|
async createSession(config) {
|
|
37858
|
-
return new FfmpegDecoderSession(config);
|
|
38017
|
+
return new FfmpegDecoderSession(config, this.ffmpegPath);
|
|
37859
38018
|
}
|
|
37860
38019
|
};
|
|
37861
38020
|
//#endregion
|