@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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-
|
|
2
|
+
import { An as _enum, Bn as string, Cn as makeProfileBrokerId, Dn as parseProfileBrokerId, Fn as literal, H as addonWidgetsSourceCapability, Hn as EventCategory, In as number, It as rankStreams, K as batteryCapability, Ln as object, M as RATE_CONTROL_TIGHT, Mn as boolean, Nn as discriminatedUnion, Q as declareLogChannel, R as RingBuffer, Sn as isEvent, St as maskUrlCredentials, Tn as nodePin, Tt as memoizePinnedFfmpeg, Ut as resolveEgressDecodeHwAccel, Vn as union, X as createHwAccelCache, Y as clipExportFailureOf, Z as createLogChannelsProvider, Zt as streamBrokerCapability, _n as createEvent, a as BatteryStatusSchema, an as buildFfmpegArgs$1, at as egressTransportFromRequest, bn as expandAudioChunkToF32le, bt as logChannelsCapability, cn as isSoftwareDecode, dn as errMsg, et as deriveBatteryPresence, fn as BOOT_RECOVERY_BACKOFF_MS, ft as ffmpegRate, hn as CamProfileSchema, ht as gifFromMp4Args, it as egressTranscodeSharingKey, j as RATE_CONTROL_RELAXED, jn as array, ln as DeviceFeature, mn as CAM_PROFILE_ORDER, mt as gifDeriveSpeed, nn as AUDIO_PRESETS, o as CLIP_BROKER_PACED_RATES, pn as BaseAddon, q as cameraStreamsCapability, s as CLIP_STILL_REASON_CODE_HEADER, sn as invocationFromEncodeProfile, tn as webrtcSessionCapability, un as DeviceType, w as G711_SCALE_CORRECTION_DB, wn as makeSourceBrokerId, x as EncodeProfileSchema, yt as loadContributionCapability, zn as record } from "../dist-jy-7xoW5.mjs";
|
|
3
|
+
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-NCge1qZM.mjs";
|
|
4
4
|
import { a as parseRestreamPath, d as PREBUFFER_MAX_MS, f as capPreRollRequestMs, i as isLiveEdgeOnlyDial, m as resolvePrebuffer, n as NO_DECLARATION, p as prebufferRetentionMs, s as sdpVariantFor, u as withSentryIntent } from "../restream-intent-DyGmLAU9.mjs";
|
|
5
5
|
import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
|
|
6
6
|
import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
|
|
7
|
-
import { a as
|
|
7
|
+
import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-BaI2VdPX.mjs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import { networkInterfaces, tmpdir } from "node:os";
|
|
@@ -99,6 +99,86 @@ function createClientLogIngest(onEntries) {
|
|
|
99
99
|
});
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
+
var FfmpegReresolver = class {
|
|
103
|
+
deps;
|
|
104
|
+
inFlight = null;
|
|
105
|
+
lastAttemptAt = null;
|
|
106
|
+
constructor(deps) {
|
|
107
|
+
this.deps = deps;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Ask again, unless an attempt is running or one started inside the bound.
|
|
111
|
+
* Never throws; the outcome is logged either way.
|
|
112
|
+
*/
|
|
113
|
+
request(reason) {
|
|
114
|
+
if (this.inFlight !== null) return "in-flight";
|
|
115
|
+
const now = (this.deps.now ?? Date.now)();
|
|
116
|
+
const minIntervalMs = this.deps.minIntervalMs ?? 3e4;
|
|
117
|
+
if (this.lastAttemptAt !== null && now - this.lastAttemptAt < minIntervalMs) return "too-soon";
|
|
118
|
+
this.lastAttemptAt = now;
|
|
119
|
+
this.deps.logger.warn("stream-broker: re-resolving the pinned ffmpeg after a refusal (D540)", { meta: { reason } });
|
|
120
|
+
this.inFlight = this.attempt().finally(() => {
|
|
121
|
+
this.inFlight = null;
|
|
122
|
+
});
|
|
123
|
+
return "started";
|
|
124
|
+
}
|
|
125
|
+
/** The attempt in flight, for a caller (or a spec) that wants to wait. */
|
|
126
|
+
settled() {
|
|
127
|
+
return this.inFlight ?? Promise.resolve();
|
|
128
|
+
}
|
|
129
|
+
async attempt() {
|
|
130
|
+
try {
|
|
131
|
+
const binaryPath = await this.deps.resolve();
|
|
132
|
+
this.deps.apply(binaryPath);
|
|
133
|
+
this.deps.logger.info("stream-broker: pinned ffmpeg resolved on retry — children may spawn", { meta: { binaryPath } });
|
|
134
|
+
} catch (err) {
|
|
135
|
+
this.deps.logger.error("stream-broker: pinned ffmpeg still unavailable — every ffmpeg child keeps refusing", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* The broker addon's whole ffmpeg wiring, in one place a spec can drive: the
|
|
141
|
+
* boot-time resolution, the refusal handler on the manager, and the
|
|
142
|
+
* re-resolve that hands a later success back to the manager's config — which
|
|
143
|
+
* every live broker reads per dial.
|
|
144
|
+
*/
|
|
145
|
+
function wireBrokerFfmpeg(deps) {
|
|
146
|
+
const give = (binaryPath) => {
|
|
147
|
+
deps.host.setFfmpegConfig({
|
|
148
|
+
binaryPath,
|
|
149
|
+
hwAccel: deps.hwAccel(),
|
|
150
|
+
threadCount: 0
|
|
151
|
+
});
|
|
152
|
+
deps.onBinaryPath?.(binaryPath);
|
|
153
|
+
};
|
|
154
|
+
const reresolver = new FfmpegReresolver({
|
|
155
|
+
resolve: deps.resolve,
|
|
156
|
+
apply: give,
|
|
157
|
+
logger: deps.logger,
|
|
158
|
+
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
159
|
+
...deps.minIntervalMs !== void 0 ? { minIntervalMs: deps.minIntervalMs } : {}
|
|
160
|
+
});
|
|
161
|
+
deps.host.setFfmpegMissingHandler(() => {
|
|
162
|
+
reresolver.request("a broker ffmpeg child was refused");
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
load: async () => {
|
|
166
|
+
try {
|
|
167
|
+
const binaryPath = await deps.resolve();
|
|
168
|
+
give(binaryPath);
|
|
169
|
+
deps.logger.info("Loaded ffmpeg config", { meta: {
|
|
170
|
+
binaryPath,
|
|
171
|
+
hwAccel: deps.hwAccel()
|
|
172
|
+
} });
|
|
173
|
+
} catch (err) {
|
|
174
|
+
give(null);
|
|
175
|
+
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) } });
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
request: (reason) => reresolver.request(reason),
|
|
179
|
+
settled: () => reresolver.settled()
|
|
180
|
+
};
|
|
181
|
+
}
|
|
102
182
|
//#endregion
|
|
103
183
|
//#region src/stream-broker/log-channels.ts
|
|
104
184
|
/**
|
|
@@ -5449,6 +5529,16 @@ function classifyDecodeHwAccel(setting) {
|
|
|
5449
5529
|
prefer: setting
|
|
5450
5530
|
};
|
|
5451
5531
|
}
|
|
5532
|
+
/**
|
|
5533
|
+
* The binary to spawn, or a THROW after an ERROR naming the camera and the
|
|
5534
|
+
* child that was refused. Never a PATH name (D540).
|
|
5535
|
+
*/
|
|
5536
|
+
function requireBrokerFfmpeg(binaryPath, ask) {
|
|
5537
|
+
if (binaryPath !== null) return binaryPath;
|
|
5538
|
+
ask.logger.error(`${ask.purpose} refused — no pinned ffmpeg on this node (D540)`, { ...ask.deviceId !== null ? { tags: { deviceId: ask.deviceId } } : {} });
|
|
5539
|
+
ask.onMissing?.();
|
|
5540
|
+
throw new Error(`${ask.purpose}: no pinned ffmpeg resolved on this node`);
|
|
5541
|
+
}
|
|
5452
5542
|
/** Narrow an audio target to a sidecar-capable codec, or `null` (copy/none). */
|
|
5453
5543
|
function toAudioSidecarCodec(audio) {
|
|
5454
5544
|
return audio === "aac" || audio === "opus" || audio === "pcmu" ? audio : null;
|
|
@@ -6566,13 +6656,19 @@ var EgressTranscodeManager = class {
|
|
|
6566
6656
|
}
|
|
6567
6657
|
createChild(spec) {
|
|
6568
6658
|
if (this.opts.createChild) return this.opts.createChild(spec);
|
|
6569
|
-
|
|
6570
|
-
|
|
6659
|
+
const ffmpegBinaryPath = requireBrokerFfmpeg(this.opts.ffmpegConfig().binaryPath, {
|
|
6660
|
+
logger: this.opts.logger,
|
|
6661
|
+
deviceId: spec.deviceId,
|
|
6662
|
+
purpose: "egress transcode",
|
|
6663
|
+
...this.opts.onFfmpegMissing === void 0 ? {} : { onMissing: this.opts.onFfmpegMissing }
|
|
6664
|
+
});
|
|
6665
|
+
if (spec.fragments) return this.createFragmentChild(spec, spec.fragments, ffmpegBinaryPath);
|
|
6666
|
+
if (spec.pushSink) return this.createPushChild(spec, spec.pushSink, ffmpegBinaryPath);
|
|
6571
6667
|
const channel = spec.channel;
|
|
6572
6668
|
if (channel === void 0) throw new Error("egress transcode: a dial child was created without a channel");
|
|
6573
6669
|
const egress = new TranscodeEgress({
|
|
6574
6670
|
logger: this.opts.logger.child("egress"),
|
|
6575
|
-
ffmpegBinaryPath
|
|
6671
|
+
ffmpegBinaryPath,
|
|
6576
6672
|
spawnFn: this.opts.spawnFn ?? spawn,
|
|
6577
6673
|
acquireChannel: () => channel,
|
|
6578
6674
|
...spec.onDied ? { onChildExit: spec.onDied } : {},
|
|
@@ -6604,10 +6700,10 @@ var EgressTranscodeManager = class {
|
|
|
6604
6700
|
* same shape as the other two transports; this wrapper is the adapter, not
|
|
6605
6701
|
* the logic. See the primitive's `ffmpeg/fmp4-fragment-child.ts`.
|
|
6606
6702
|
*/
|
|
6607
|
-
createFragmentChild(spec, fragments) {
|
|
6703
|
+
createFragmentChild(spec, fragments, ffmpegBinaryPath) {
|
|
6608
6704
|
const child = new Fmp4FragmentChild({
|
|
6609
6705
|
logger: this.opts.logger.child("egress-fmp4"),
|
|
6610
|
-
ffmpegBinaryPath
|
|
6706
|
+
ffmpegBinaryPath,
|
|
6611
6707
|
spawnFn: this.opts.spawnFn ?? spawn,
|
|
6612
6708
|
...this.opts.fragmentFirstUnitTimeoutMs === void 0 ? {} : { firstUnitTimeoutMs: this.opts.fragmentFirstUnitTimeoutMs },
|
|
6613
6709
|
...spec.onDied ? { onChildExit: spec.onDied } : {}
|
|
@@ -6657,7 +6753,7 @@ var EgressTranscodeManager = class {
|
|
|
6657
6753
|
* masked the same failure on the dial path with its own software retry, which
|
|
6658
6754
|
* is why only this transport was dark.
|
|
6659
6755
|
*/
|
|
6660
|
-
createPushChild(spec, sink) {
|
|
6756
|
+
createPushChild(spec, sink, ffmpegBinaryPath) {
|
|
6661
6757
|
const logger = this.opts.logger.child("egress-push");
|
|
6662
6758
|
const deviceId = spec.deviceId;
|
|
6663
6759
|
const timeoutMs = this.opts.pushFirstPacketTimeoutMs ?? DEFAULT_PUSH_FIRST_PACKET_TIMEOUT_MS;
|
|
@@ -6675,7 +6771,7 @@ var EgressTranscodeManager = class {
|
|
|
6675
6771
|
const makeReader = (input) => {
|
|
6676
6772
|
if (this.opts.createPushReader) return this.opts.createPushReader(input);
|
|
6677
6773
|
return new FfmpegPushReader({
|
|
6678
|
-
ffmpegPath:
|
|
6774
|
+
ffmpegPath: ffmpegBinaryPath,
|
|
6679
6775
|
args: [...input.args],
|
|
6680
6776
|
logger,
|
|
6681
6777
|
...this.opts.spawnFn ? { spawnFn: this.opts.spawnFn } : {}
|
|
@@ -6702,7 +6798,7 @@ var EgressTranscodeManager = class {
|
|
|
6702
6798
|
meta: {
|
|
6703
6799
|
brokerId: spec.brokerId,
|
|
6704
6800
|
camStreamId: spec.publishedCamStreamId,
|
|
6705
|
-
ffmpegPath:
|
|
6801
|
+
ffmpegPath: ffmpegBinaryPath,
|
|
6706
6802
|
decodeHwAccel: decodeHwAccel ?? "software",
|
|
6707
6803
|
argv: args.join(" ")
|
|
6708
6804
|
}
|
|
@@ -7077,6 +7173,100 @@ function defaultId() {
|
|
|
7077
7173
|
counter += 1;
|
|
7078
7174
|
return `${Date.now().toString(36)}${counter.toString(36)}`;
|
|
7079
7175
|
}
|
|
7176
|
+
/** A derive that has not finished by now will not finish inside the drain. */
|
|
7177
|
+
var GIF_RENDER_TIMEOUT_MS = 2e4;
|
|
7178
|
+
/**
|
|
7179
|
+
* Derive the gif. `null` on every failure — a notification that lost its gif is
|
|
7180
|
+
* a notification, and one that failed is not.
|
|
7181
|
+
*
|
|
7182
|
+
* The child is guaranteed dead before this settles: these run on every
|
|
7183
|
+
* notification that asks for a gif, and a leaked ffmpeg accumulates on the node
|
|
7184
|
+
* until somebody notices the host is slow.
|
|
7185
|
+
*/
|
|
7186
|
+
async function gifFromMp4(input) {
|
|
7187
|
+
const args = gifFromMp4Args({
|
|
7188
|
+
maxWidth: input.maxWidth ?? 640,
|
|
7189
|
+
fps: input.fps ?? 12,
|
|
7190
|
+
speed: input.speed ?? 4
|
|
7191
|
+
});
|
|
7192
|
+
const startedAt = Date.now();
|
|
7193
|
+
try {
|
|
7194
|
+
const bytes = await runGif(input, args);
|
|
7195
|
+
input.logger?.info("nc gif: derived from the notification video", {
|
|
7196
|
+
tags: { deviceId: input.deviceId },
|
|
7197
|
+
meta: {
|
|
7198
|
+
sourceBytes: input.mp4.length,
|
|
7199
|
+
gifBytes: bytes.length,
|
|
7200
|
+
width: input.maxWidth ?? 640,
|
|
7201
|
+
fps: input.fps ?? 12,
|
|
7202
|
+
speed: input.speed ?? 4,
|
|
7203
|
+
encodeMs: Date.now() - startedAt
|
|
7204
|
+
}
|
|
7205
|
+
});
|
|
7206
|
+
return bytes;
|
|
7207
|
+
} catch (err) {
|
|
7208
|
+
input.logger?.warn("nc gif: the derive failed — this notification ships without a gif", {
|
|
7209
|
+
tags: { deviceId: input.deviceId },
|
|
7210
|
+
meta: {
|
|
7211
|
+
sourceBytes: input.mp4.length,
|
|
7212
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7213
|
+
}
|
|
7214
|
+
});
|
|
7215
|
+
return null;
|
|
7216
|
+
}
|
|
7217
|
+
}
|
|
7218
|
+
function runGif(input, args) {
|
|
7219
|
+
return new Promise((resolve, reject) => {
|
|
7220
|
+
const proc = input.spawnFn(input.ffmpegPath, [...args], { stdio: [
|
|
7221
|
+
"pipe",
|
|
7222
|
+
"pipe",
|
|
7223
|
+
"pipe"
|
|
7224
|
+
] });
|
|
7225
|
+
const chunks = [];
|
|
7226
|
+
let stderrTail = "";
|
|
7227
|
+
let done = false;
|
|
7228
|
+
let timer = null;
|
|
7229
|
+
const settle = (err, out) => {
|
|
7230
|
+
if (done) return;
|
|
7231
|
+
done = true;
|
|
7232
|
+
if (timer !== null) clearTimeout(timer);
|
|
7233
|
+
if (proc.exitCode === null && proc.signalCode === null) try {
|
|
7234
|
+
proc.kill("SIGKILL");
|
|
7235
|
+
} catch {}
|
|
7236
|
+
try {
|
|
7237
|
+
proc.stdout?.removeAllListeners();
|
|
7238
|
+
proc.stderr?.removeAllListeners();
|
|
7239
|
+
proc.stdin?.destroy();
|
|
7240
|
+
} catch {}
|
|
7241
|
+
if (err !== null) reject(err);
|
|
7242
|
+
else resolve(out ?? new Uint8Array(0));
|
|
7243
|
+
};
|
|
7244
|
+
timer = setTimeout(() => settle(/* @__PURE__ */ new Error("gif derive timed out")), GIF_RENDER_TIMEOUT_MS);
|
|
7245
|
+
timer.unref?.();
|
|
7246
|
+
proc.stdout?.on("data", (c) => chunks.push(c));
|
|
7247
|
+
proc.stderr?.on("data", (c) => {
|
|
7248
|
+
stderrTail = (stderrTail + c.toString()).slice(-400);
|
|
7249
|
+
});
|
|
7250
|
+
proc.on("error", (err) => settle(err instanceof Error ? err : new Error(String(err))));
|
|
7251
|
+
proc.on("close", (code) => {
|
|
7252
|
+
if (code !== 0) {
|
|
7253
|
+
settle(/* @__PURE__ */ new Error(`ffmpeg gif derive exited ${code}: ${stderrTail}`));
|
|
7254
|
+
return;
|
|
7255
|
+
}
|
|
7256
|
+
const out = Buffer.concat(chunks);
|
|
7257
|
+
if (out.byteLength === 0) {
|
|
7258
|
+
settle(/* @__PURE__ */ new Error("gif derive produced no bytes"));
|
|
7259
|
+
return;
|
|
7260
|
+
}
|
|
7261
|
+
const bytes = new Uint8Array(out.byteLength);
|
|
7262
|
+
bytes.set(out);
|
|
7263
|
+
settle(null, bytes);
|
|
7264
|
+
});
|
|
7265
|
+
proc.stdin?.on("error", () => {});
|
|
7266
|
+
proc.stdin?.write(Buffer.from(input.mp4));
|
|
7267
|
+
proc.stdin?.end();
|
|
7268
|
+
});
|
|
7269
|
+
}
|
|
7080
7270
|
//#endregion
|
|
7081
7271
|
//#region src/stream-broker/webrtc/nal-utils.ts
|
|
7082
7272
|
var NAL_START_CODE_4B = Buffer.from([
|
|
@@ -7783,15 +7973,6 @@ function eventClipEncodeWidth(sourceWidth, speed) {
|
|
|
7783
7973
|
return Math.min(source, EVENT_CLIP_ENCODE_MAX_WIDTH);
|
|
7784
7974
|
}
|
|
7785
7975
|
/**
|
|
7786
|
-
* Format a frame rate for an ffmpeg filter argument.
|
|
7787
|
-
*
|
|
7788
|
-
* `12 / 4` must reach the filter as `3` and `15 / 4` as `3.75`, not as a float
|
|
7789
|
-
* tail that `av_parse_video_rate` then has to guess at.
|
|
7790
|
-
*/
|
|
7791
|
-
function ffmpegRate(rate) {
|
|
7792
|
-
return String(Math.round(rate * 1e3) / 1e3);
|
|
7793
|
-
}
|
|
7794
|
-
/**
|
|
7795
7976
|
* Whether this render can COPY the camera's own H.264 instead of re-encoding it.
|
|
7796
7977
|
*
|
|
7797
7978
|
* The ring already holds the camera's elementary stream. Re-encoding it is only
|
|
@@ -7900,7 +8081,7 @@ async function renderPreBufferClip(input) {
|
|
|
7900
8081
|
hevc,
|
|
7901
8082
|
sourceWidth: input.sourceWidth ?? 0
|
|
7902
8083
|
});
|
|
7903
|
-
const proc = input.spawn(input.ffmpegPath
|
|
8084
|
+
const proc = input.spawn(input.ffmpegPath, clipFfmpegArgs({
|
|
7904
8085
|
...input,
|
|
7905
8086
|
hevc,
|
|
7906
8087
|
copyVideo
|
|
@@ -7946,142 +8127,6 @@ async function renderPreBufferClip(input) {
|
|
|
7946
8127
|
proc.stdin?.end();
|
|
7947
8128
|
});
|
|
7948
8129
|
}
|
|
7949
|
-
/**
|
|
7950
|
-
* The factor the derive must apply ON TOP of the speed the mp4 already carries.
|
|
7951
|
-
*
|
|
7952
|
-
* The gif is derived from the muxed mp4, so if that mux already ran `setpts` at
|
|
7953
|
-
* the caller's `speed`, applying the absolute gif rate again would compound
|
|
7954
|
-
* them — a 4× gif beside a 2× video would come out at 8×. Discounting here is
|
|
7955
|
-
* what keeps {@link GIF_SPEED} an ABSOLUTE rate against real time whatever the
|
|
7956
|
-
* video does.
|
|
7957
|
-
*/
|
|
7958
|
-
function gifDeriveSpeed(gifSpeed, videoSpeed) {
|
|
7959
|
-
if (!Number.isFinite(videoSpeed) || videoSpeed <= 0) return gifSpeed;
|
|
7960
|
-
return gifSpeed / videoSpeed;
|
|
7961
|
-
}
|
|
7962
|
-
/** A derive that has not finished by now will not finish inside the drain. */
|
|
7963
|
-
var GIF_RENDER_TIMEOUT_MS = 2e4;
|
|
7964
|
-
/**
|
|
7965
|
-
* The filter chain. Extracted so a test can assert the palette pair without
|
|
7966
|
-
* spawning ffmpeg — an argv test cannot see quality, but it can see a filter
|
|
7967
|
-
* that somebody deleted.
|
|
7968
|
-
*/
|
|
7969
|
-
function gifFilterComplex(input) {
|
|
7970
|
-
const speed = input.speed > 0 ? input.speed : 1;
|
|
7971
|
-
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`;
|
|
7972
|
-
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`;
|
|
7973
|
-
}
|
|
7974
|
-
/** The full argv, for the same reason. */
|
|
7975
|
-
function gifFromMp4Args(input) {
|
|
7976
|
-
return [
|
|
7977
|
-
"-y",
|
|
7978
|
-
"-v",
|
|
7979
|
-
"error",
|
|
7980
|
-
"-f",
|
|
7981
|
-
"mp4",
|
|
7982
|
-
"-i",
|
|
7983
|
-
"pipe:0",
|
|
7984
|
-
"-filter_complex",
|
|
7985
|
-
gifFilterComplex(input),
|
|
7986
|
-
"-loop",
|
|
7987
|
-
"0",
|
|
7988
|
-
"-f",
|
|
7989
|
-
"gif",
|
|
7990
|
-
"pipe:1"
|
|
7991
|
-
];
|
|
7992
|
-
}
|
|
7993
|
-
/**
|
|
7994
|
-
* Derive the gif. `null` on every failure — a notification that lost its gif is
|
|
7995
|
-
* a notification, and one that failed is not.
|
|
7996
|
-
*
|
|
7997
|
-
* The child is guaranteed dead before this settles: these run on every
|
|
7998
|
-
* notification that asks for a gif, and a leaked ffmpeg accumulates on the node
|
|
7999
|
-
* until somebody notices the host is slow.
|
|
8000
|
-
*/
|
|
8001
|
-
async function gifFromMp4(input) {
|
|
8002
|
-
const args = gifFromMp4Args({
|
|
8003
|
-
maxWidth: input.maxWidth ?? 640,
|
|
8004
|
-
fps: input.fps ?? 12,
|
|
8005
|
-
speed: input.speed ?? 4
|
|
8006
|
-
});
|
|
8007
|
-
const startedAt = Date.now();
|
|
8008
|
-
try {
|
|
8009
|
-
const bytes = await runGif(input, args);
|
|
8010
|
-
input.logger?.info("nc gif: derived from the notification video", {
|
|
8011
|
-
tags: { deviceId: input.deviceId },
|
|
8012
|
-
meta: {
|
|
8013
|
-
sourceBytes: input.mp4.length,
|
|
8014
|
-
gifBytes: bytes.length,
|
|
8015
|
-
width: input.maxWidth ?? 640,
|
|
8016
|
-
fps: input.fps ?? 12,
|
|
8017
|
-
speed: input.speed ?? 4,
|
|
8018
|
-
encodeMs: Date.now() - startedAt
|
|
8019
|
-
}
|
|
8020
|
-
});
|
|
8021
|
-
return bytes;
|
|
8022
|
-
} catch (err) {
|
|
8023
|
-
input.logger?.warn("nc gif: the derive failed — this notification ships without a gif", {
|
|
8024
|
-
tags: { deviceId: input.deviceId },
|
|
8025
|
-
meta: {
|
|
8026
|
-
sourceBytes: input.mp4.length,
|
|
8027
|
-
error: err instanceof Error ? err.message : String(err)
|
|
8028
|
-
}
|
|
8029
|
-
});
|
|
8030
|
-
return null;
|
|
8031
|
-
}
|
|
8032
|
-
}
|
|
8033
|
-
function runGif(input, args) {
|
|
8034
|
-
return new Promise((resolve, reject) => {
|
|
8035
|
-
const proc = input.spawnFn(input.ffmpegPath ?? "ffmpeg", [...args], { stdio: [
|
|
8036
|
-
"pipe",
|
|
8037
|
-
"pipe",
|
|
8038
|
-
"pipe"
|
|
8039
|
-
] });
|
|
8040
|
-
const chunks = [];
|
|
8041
|
-
let stderrTail = "";
|
|
8042
|
-
let done = false;
|
|
8043
|
-
let timer = null;
|
|
8044
|
-
const settle = (err, out) => {
|
|
8045
|
-
if (done) return;
|
|
8046
|
-
done = true;
|
|
8047
|
-
if (timer !== null) clearTimeout(timer);
|
|
8048
|
-
if (proc.exitCode === null && proc.signalCode === null) try {
|
|
8049
|
-
proc.kill("SIGKILL");
|
|
8050
|
-
} catch {}
|
|
8051
|
-
try {
|
|
8052
|
-
proc.stdout?.removeAllListeners();
|
|
8053
|
-
proc.stderr?.removeAllListeners();
|
|
8054
|
-
proc.stdin?.destroy();
|
|
8055
|
-
} catch {}
|
|
8056
|
-
if (err !== null) reject(err);
|
|
8057
|
-
else resolve(out ?? new Uint8Array(0));
|
|
8058
|
-
};
|
|
8059
|
-
timer = setTimeout(() => settle(/* @__PURE__ */ new Error("gif derive timed out")), GIF_RENDER_TIMEOUT_MS);
|
|
8060
|
-
timer.unref?.();
|
|
8061
|
-
proc.stdout?.on("data", (c) => chunks.push(c));
|
|
8062
|
-
proc.stderr?.on("data", (c) => {
|
|
8063
|
-
stderrTail = (stderrTail + c.toString()).slice(-400);
|
|
8064
|
-
});
|
|
8065
|
-
proc.on("error", (err) => settle(err instanceof Error ? err : new Error(String(err))));
|
|
8066
|
-
proc.on("close", (code) => {
|
|
8067
|
-
if (code !== 0) {
|
|
8068
|
-
settle(/* @__PURE__ */ new Error(`ffmpeg gif derive exited ${code}: ${stderrTail}`));
|
|
8069
|
-
return;
|
|
8070
|
-
}
|
|
8071
|
-
const out = Buffer.concat(chunks);
|
|
8072
|
-
if (out.byteLength === 0) {
|
|
8073
|
-
settle(/* @__PURE__ */ new Error("gif derive produced no bytes"));
|
|
8074
|
-
return;
|
|
8075
|
-
}
|
|
8076
|
-
const bytes = new Uint8Array(out.byteLength);
|
|
8077
|
-
bytes.set(out);
|
|
8078
|
-
settle(null, bytes);
|
|
8079
|
-
});
|
|
8080
|
-
proc.stdin?.on("error", () => {});
|
|
8081
|
-
proc.stdin?.write(Buffer.from(input.mp4));
|
|
8082
|
-
proc.stdin?.end();
|
|
8083
|
-
});
|
|
8084
|
-
}
|
|
8085
8130
|
//#endregion
|
|
8086
8131
|
//#region src/stream-broker/stream-broker/sentry-encode.ts
|
|
8087
8132
|
/**
|
|
@@ -12853,11 +12898,12 @@ var StreamBroker = class StreamBroker {
|
|
|
12853
12898
|
/** Active "ffmpeg parser" ingress reader when `source.ffmpegParser` is set. */
|
|
12854
12899
|
ffmpegSourceReader = null;
|
|
12855
12900
|
/**
|
|
12856
|
-
*
|
|
12857
|
-
*
|
|
12858
|
-
*
|
|
12901
|
+
* Where the ffmpeg-parser ingress reads the node's pinned ffmpeg — the
|
|
12902
|
+
* manager's LIVE config, asked at each dial rather than copied at creation,
|
|
12903
|
+
* so a broker created while a re-resolve was in flight still sees its result.
|
|
12904
|
+
* `null` — the ingress REFUSES by name; there is no PATH default (D540).
|
|
12859
12905
|
*/
|
|
12860
|
-
|
|
12906
|
+
ffmpegBinaryPathSource = () => null;
|
|
12861
12907
|
rtpDepacketizer = null;
|
|
12862
12908
|
/** Flag-only depacketizer for the `push-rtp` source-RTP pre-buffer path
|
|
12863
12909
|
* (sets keyframe/param-set flags without re-emitting AnnexB packets). */
|
|
@@ -13440,12 +13486,21 @@ var StreamBroker = class StreamBroker {
|
|
|
13440
13486
|
this.restreamUrlResolver = resolver;
|
|
13441
13487
|
}
|
|
13442
13488
|
/**
|
|
13443
|
-
* Inject
|
|
13444
|
-
*
|
|
13445
|
-
* system-config (`ffmpeg.binaryPath`). Absent = bare `'ffmpeg'` (PATH lookup).
|
|
13489
|
+
* Inject where the "ffmpeg parser" ingress ({@link startFfmpegSourceReader})
|
|
13490
|
+
* reads the node's pinned ffmpeg: the manager's live config, asked per dial.
|
|
13446
13491
|
*/
|
|
13447
|
-
|
|
13448
|
-
this.
|
|
13492
|
+
setFfmpegBinaryPathSource(source) {
|
|
13493
|
+
this.ffmpegBinaryPathSource = source;
|
|
13494
|
+
}
|
|
13495
|
+
/** The pinned ffmpeg this broker would spawn right now (`null` = none). */
|
|
13496
|
+
getFfmpegBinaryPath() {
|
|
13497
|
+
return this.ffmpegBinaryPathSource();
|
|
13498
|
+
}
|
|
13499
|
+
/** Told when the ingress was refused for want of the pinned ffmpeg. */
|
|
13500
|
+
ffmpegMissingHandler = null;
|
|
13501
|
+
/** Wire the manager's re-resolve request. */
|
|
13502
|
+
setFfmpegMissingHandler(handler) {
|
|
13503
|
+
this.ffmpegMissingHandler = handler;
|
|
13449
13504
|
}
|
|
13450
13505
|
/**
|
|
13451
13506
|
* Inject the addon's child-cost registry. Injected rather than constructed
|
|
@@ -15371,18 +15426,28 @@ var StreamBroker = class StreamBroker {
|
|
|
15371
15426
|
transport: source.ffmpegParser?.transport ?? "tcp"
|
|
15372
15427
|
} });
|
|
15373
15428
|
this.destroyFfmpegSourceReader();
|
|
15429
|
+
const ffmpegPath = this.ffmpegBinaryPathSource();
|
|
15430
|
+
if (ffmpegPath === null) {
|
|
15431
|
+
this.logger?.error("ffmpeg source reader refused — no pinned ffmpeg on this node (D540)", {
|
|
15432
|
+
...this.numericDeviceId === void 0 ? {} : { tags: { deviceId: this.numericDeviceId } },
|
|
15433
|
+
meta: { url: maskUrlCredentials(source.url) }
|
|
15434
|
+
});
|
|
15435
|
+
this._status = "error";
|
|
15436
|
+
this.ffmpegMissingHandler?.();
|
|
15437
|
+
this.scheduleReconnect();
|
|
15438
|
+
return;
|
|
15439
|
+
}
|
|
15374
15440
|
const parser = source.ffmpegParser;
|
|
15375
15441
|
const sourceKind = source.type === "rtmp" ? "rtmp" : source.type === "flv" || source.type === "http" ? "http" : "rtsp";
|
|
15376
|
-
const args = buildFfmpegSourceReaderArgs({
|
|
15377
|
-
sourceUrl: source.url,
|
|
15378
|
-
sourceKind,
|
|
15379
|
-
transport: parser?.transport ?? "tcp",
|
|
15380
|
-
...parser?.inputArgs ? { inputArgs: parser.inputArgs } : {},
|
|
15381
|
-
...parser?.outputArgs ? { outputArgs: parser.outputArgs } : {}
|
|
15382
|
-
});
|
|
15383
15442
|
const reader = new FfmpegSourceReader({
|
|
15384
|
-
ffmpegPath
|
|
15385
|
-
args
|
|
15443
|
+
ffmpegPath,
|
|
15444
|
+
args: buildFfmpegSourceReaderArgs({
|
|
15445
|
+
sourceUrl: source.url,
|
|
15446
|
+
sourceKind,
|
|
15447
|
+
transport: parser?.transport ?? "tcp",
|
|
15448
|
+
...parser?.inputArgs ? { inputArgs: parser.inputArgs } : {},
|
|
15449
|
+
...parser?.outputArgs ? { outputArgs: parser.outputArgs } : {}
|
|
15450
|
+
}),
|
|
15386
15451
|
...this.logger ? { logger: this.logger.child("ffmpeg-source") } : {},
|
|
15387
15452
|
claimCost: (pid) => this.claimIngestCost(pid)
|
|
15388
15453
|
}, {
|
|
@@ -16358,7 +16423,7 @@ function hexToBytes(hex) {
|
|
|
16358
16423
|
* pipe is the pattern proven by the WebRTC transcode feed.
|
|
16359
16424
|
*/
|
|
16360
16425
|
var DEFAULT_FFMPEG_CONFIG = {
|
|
16361
|
-
binaryPath:
|
|
16426
|
+
binaryPath: null,
|
|
16362
16427
|
hwAccel: "copy",
|
|
16363
16428
|
threadCount: 0
|
|
16364
16429
|
};
|
|
@@ -16392,6 +16457,7 @@ var TranscodePipelineManager = class {
|
|
|
16392
16457
|
resolveProfileSource;
|
|
16393
16458
|
releaseGraceMs;
|
|
16394
16459
|
ffmpegConfig;
|
|
16460
|
+
onFfmpegMissing;
|
|
16395
16461
|
resolveStreamHwAccel;
|
|
16396
16462
|
spawnFn;
|
|
16397
16463
|
acquireChannel;
|
|
@@ -16404,6 +16470,7 @@ var TranscodePipelineManager = class {
|
|
|
16404
16470
|
this.resolveProfileSource = opts.resolveProfileSource ?? null;
|
|
16405
16471
|
this.releaseGraceMs = opts.releaseGraceMs ?? 5e3;
|
|
16406
16472
|
this.ffmpegConfig = opts.ffmpegConfig ?? (() => DEFAULT_FFMPEG_CONFIG);
|
|
16473
|
+
this.onFfmpegMissing = opts.onFfmpegMissing ?? null;
|
|
16407
16474
|
this.probeFfmpeg = opts.probeFfmpeg ?? probeFfmpegBinary;
|
|
16408
16475
|
this.resolveStreamHwAccel = opts.resolveStreamHwAccel ?? null;
|
|
16409
16476
|
this.spawnFn = opts.spawnFn ?? spawn;
|
|
@@ -16476,11 +16543,17 @@ var TranscodePipelineManager = class {
|
|
|
16476
16543
|
async startTranscode(opts) {
|
|
16477
16544
|
if (!this.acquireChannel) throw new Error("getStreamWithCodec: transcode egress not wired (acquireChannel missing)");
|
|
16478
16545
|
const cfg = this.ffmpegConfig();
|
|
16546
|
+
const ffmpegBinaryPath = requireBrokerFfmpeg(cfg.binaryPath, {
|
|
16547
|
+
logger: this.logger,
|
|
16548
|
+
deviceId: opts.deviceId,
|
|
16549
|
+
purpose: "transcode egress",
|
|
16550
|
+
...this.onFfmpegMissing === null ? {} : { onMissing: this.onFfmpegMissing }
|
|
16551
|
+
});
|
|
16479
16552
|
const effectiveHwAccel = this.effectiveHwAccelSetting(opts.deviceId, opts.camStreamId, cfg.hwAccel);
|
|
16480
16553
|
const choice = chooseVideoEncoder({
|
|
16481
16554
|
target: opts.targetCodec,
|
|
16482
16555
|
backend: await this.resolveHardwareBackend(),
|
|
16483
|
-
binaryEncoders: await this.resolveBinaryEncoders(
|
|
16556
|
+
binaryEncoders: await this.resolveBinaryEncoders(ffmpegBinaryPath),
|
|
16484
16557
|
hwAccelSetting: effectiveHwAccel,
|
|
16485
16558
|
renderNode: DEFAULT_VAAPI_RENDER_DEVICE
|
|
16486
16559
|
});
|
|
@@ -16510,7 +16583,7 @@ var TranscodePipelineManager = class {
|
|
|
16510
16583
|
const sidecarCodec = opts.audioEgress ? toAudioSidecarCodec(opts.audio) : null;
|
|
16511
16584
|
const egress = new TranscodeEgress({
|
|
16512
16585
|
logger: this.logger.child("egress"),
|
|
16513
|
-
ffmpegBinaryPath
|
|
16586
|
+
ffmpegBinaryPath,
|
|
16514
16587
|
spawnFn: this.spawnFn,
|
|
16515
16588
|
acquireChannel: this.acquireChannel
|
|
16516
16589
|
}, {
|
|
@@ -16739,11 +16812,12 @@ var TranscodePipelineManager = class {
|
|
|
16739
16812
|
try {
|
|
16740
16813
|
const backend = rankDecodeHwAccels((await this.hwaccel.resolve(prefer)).preferred, [])[0] ?? null;
|
|
16741
16814
|
if (backend === null) return null;
|
|
16742
|
-
const
|
|
16815
|
+
const binaryPath = this.ffmpegConfig().binaryPath;
|
|
16816
|
+
const caps = binaryPath === null ? null : await this.resolveBinaryCapabilities(binaryPath);
|
|
16743
16817
|
if (caps !== null && !caps.hwaccels.has(backend)) {
|
|
16744
16818
|
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: {
|
|
16745
16819
|
backend,
|
|
16746
|
-
ffmpeg:
|
|
16820
|
+
ffmpeg: binaryPath,
|
|
16747
16821
|
hwaccels: [...caps.hwaccels].join(",") || "none"
|
|
16748
16822
|
} });
|
|
16749
16823
|
return null;
|
|
@@ -17424,12 +17498,14 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17424
17498
|
/**
|
|
17425
17499
|
* Snapshot of the system `ffmpeg` config section (binaryPath / hwAccel /
|
|
17426
17500
|
* threadCount). Refreshed by the host addon via `setFfmpegConfig`; the
|
|
17427
|
-
* transcode manager reads it live per spawn.
|
|
17428
|
-
*
|
|
17429
|
-
*
|
|
17501
|
+
* transcode manager reads it live per spawn. Until the addon resolves the
|
|
17502
|
+
* node's pinned binary there is NONE (`binaryPath: null`) and every child
|
|
17503
|
+
* refuses by name — never a PATH `ffmpeg` (D540). `'copy'` software decode
|
|
17504
|
+
* and auto threads stay the defaults: software decode never hard-fails on a
|
|
17505
|
+
* fragile HEVC source.
|
|
17430
17506
|
*/
|
|
17431
17507
|
ffmpegConfig = {
|
|
17432
|
-
binaryPath:
|
|
17508
|
+
binaryPath: null,
|
|
17433
17509
|
hwAccel: "copy",
|
|
17434
17510
|
threadCount: 0
|
|
17435
17511
|
};
|
|
@@ -17465,6 +17541,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17465
17541
|
},
|
|
17466
17542
|
resolveProfileSource: (deviceId, profile) => this.assignments.get(deviceId)?.map[profile] ?? null,
|
|
17467
17543
|
ffmpegConfig: () => this.ffmpegConfig,
|
|
17544
|
+
onFfmpegMissing: () => this.reportFfmpegMissing(),
|
|
17468
17545
|
resolveStreamHwAccel: (deviceId, camStreamId) => this.getStreamHwAccelOverride(deviceId, camStreamId),
|
|
17469
17546
|
sentryOffered: async (deviceId) => sentryEligible({
|
|
17470
17547
|
batteryOperated: await this.isBatteryOperated(deviceId),
|
|
@@ -17527,6 +17604,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17527
17604
|
});
|
|
17528
17605
|
},
|
|
17529
17606
|
ffmpegConfig: () => this.ffmpegConfig,
|
|
17607
|
+
onFfmpegMissing: () => this.reportFfmpegMissing(),
|
|
17530
17608
|
...this.costRegistry === null ? {} : { costRegistry: this.costRegistry },
|
|
17531
17609
|
resolveDecodeHwAccel: (deviceId, override) => this.resolveEgressHwAccel(deviceId, override),
|
|
17532
17610
|
acquireChannel: (brokerId) => {
|
|
@@ -17902,11 +17980,35 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17902
17980
|
if (localNodeId !== void 0) this.localNodeId = localNodeId;
|
|
17903
17981
|
if (hwaccel !== void 0) this.hwaccel = hwaccel;
|
|
17904
17982
|
}
|
|
17905
|
-
/**
|
|
17983
|
+
/**
|
|
17984
|
+
* Update the node's ffmpeg config snapshot. Every broker READS it per dial
|
|
17985
|
+
* ({@link bindBrokerFfmpeg}), so a re-resolved binary reaches live brokers —
|
|
17986
|
+
* including one created while the re-resolve was in flight — with no copy to
|
|
17987
|
+
* fan out (D540).
|
|
17988
|
+
*/
|
|
17906
17989
|
setFfmpegConfig(cfg) {
|
|
17907
17990
|
this.ffmpegConfig = cfg;
|
|
17908
17991
|
}
|
|
17909
17992
|
/**
|
|
17993
|
+
* Point a broker at the node's LIVE pinned ffmpeg and at the re-resolve
|
|
17994
|
+
* request, so its ffmpeg-parser ingress spawns the current binary and a
|
|
17995
|
+
* refusal asks for it again.
|
|
17996
|
+
*/
|
|
17997
|
+
bindBrokerFfmpeg(broker) {
|
|
17998
|
+
broker.setFfmpegBinaryPathSource(() => this.ffmpegConfig.binaryPath);
|
|
17999
|
+
broker.setFfmpegMissingHandler(() => this.reportFfmpegMissing());
|
|
18000
|
+
}
|
|
18001
|
+
/** Who asks for the pinned binary again after a refusal. Set by the addon. */
|
|
18002
|
+
ffmpegMissingHandler = null;
|
|
18003
|
+
/** Wire the host's re-resolve (`FfmpegReresolver.request`). */
|
|
18004
|
+
setFfmpegMissingHandler(handler) {
|
|
18005
|
+
this.ffmpegMissingHandler = handler;
|
|
18006
|
+
}
|
|
18007
|
+
/** A child was refused for want of the pinned ffmpeg: ask the host again. */
|
|
18008
|
+
reportFfmpegMissing() {
|
|
18009
|
+
this.ffmpegMissingHandler?.();
|
|
18010
|
+
}
|
|
18011
|
+
/**
|
|
17910
18012
|
* The decode `hwaccel` options surfaceable in a (future) UI: the always-valid
|
|
17911
18013
|
* settings (`copy`, `auto`, `none`) plus the concrete hardware backends THIS
|
|
17912
18014
|
* node can use, resolved from the LOCAL `ctx.kernel.hwaccel` (os.platform +
|
|
@@ -18247,7 +18349,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18247
18349
|
let annexB;
|
|
18248
18350
|
try {
|
|
18249
18351
|
annexB = await encodePrestreamIdr({
|
|
18250
|
-
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
18352
|
+
ffmpegPath: requireBrokerFfmpeg(this.ffmpegConfig.binaryPath, {
|
|
18353
|
+
logger,
|
|
18354
|
+
deviceId,
|
|
18355
|
+
purpose: "sentry pre-stream encode",
|
|
18356
|
+
onMissing: () => this.reportFfmpegMissing()
|
|
18357
|
+
}),
|
|
18251
18358
|
plan,
|
|
18252
18359
|
jpeg: picture.jpeg,
|
|
18253
18360
|
logger,
|
|
@@ -19499,7 +19606,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19499
19606
|
height: scaledHeight(cut, clipWidth)
|
|
19500
19607
|
});
|
|
19501
19608
|
const gifSpeed = input.gifSpeed ?? 4;
|
|
19502
|
-
|
|
19609
|
+
const gifFfmpeg = this.ffmpegConfig.binaryPath;
|
|
19610
|
+
if (input.kinds.includes("gif") && gifFfmpeg === null) {
|
|
19611
|
+
this.logger?.error("event media gif refused — no pinned ffmpeg on this node (D540)", { tags: { deviceId: input.deviceId } });
|
|
19612
|
+
this.reportFfmpegMissing();
|
|
19613
|
+
}
|
|
19614
|
+
if (input.kinds.includes("gif") && gifFfmpeg !== null) {
|
|
19503
19615
|
const gif = await gifFromMp4({
|
|
19504
19616
|
mp4,
|
|
19505
19617
|
deviceId: input.deviceId,
|
|
@@ -19508,7 +19620,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19508
19620
|
speed: gifDeriveSpeed(gifSpeed, input.speed),
|
|
19509
19621
|
...this.logger !== void 0 ? { logger: this.logger } : {},
|
|
19510
19622
|
spawnFn: spawn,
|
|
19511
|
-
|
|
19623
|
+
ffmpegPath: gifFfmpeg
|
|
19512
19624
|
});
|
|
19513
19625
|
if (gif !== null) {
|
|
19514
19626
|
const height = scaledHeight(cut, input.gifMaxWidth);
|
|
@@ -19677,7 +19789,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19677
19789
|
"pipe",
|
|
19678
19790
|
"pipe"
|
|
19679
19791
|
] }),
|
|
19680
|
-
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
19792
|
+
ffmpegPath: requireBrokerFfmpeg(this.ffmpegConfig.binaryPath, {
|
|
19793
|
+
logger: this.logger,
|
|
19794
|
+
deviceId: cut.deviceId,
|
|
19795
|
+
purpose: "pre-buffer clip render",
|
|
19796
|
+
onMissing: () => this.reportFfmpegMissing()
|
|
19797
|
+
}),
|
|
19681
19798
|
sdpParameterSets: cut.broker.getSdpParameterSets()
|
|
19682
19799
|
});
|
|
19683
19800
|
this.logger?.info("pre-buffer clip rendered", {
|
|
@@ -21755,7 +21872,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21755
21872
|
const source = this.buildSourceFromCamStream(cam);
|
|
21756
21873
|
const broker = new StreamBroker(brokerId, this.buildDecoderCapApi(deviceId), brokerLogger, buildAudioCodecCapApi(this.api), this.localNodeId);
|
|
21757
21874
|
broker.setRestreamUrlResolver(() => this.rtspProvider.getEntry(brokerId)?.mutedUrl ?? null);
|
|
21758
|
-
|
|
21875
|
+
this.bindBrokerFfmpeg(broker);
|
|
21759
21876
|
if (this.costRegistry !== null) broker.setCostRegistry(this.costRegistry);
|
|
21760
21877
|
this.applyPrebufferConfig(broker, deviceId, camStreamId);
|
|
21761
21878
|
this.applyAudioMuteConfig(broker, deviceId);
|
|
@@ -21933,7 +22050,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21933
22050
|
}
|
|
21934
22051
|
});
|
|
21935
22052
|
const feeder = new DerivedStreamFeeder({
|
|
21936
|
-
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
22053
|
+
ffmpegPath: requireBrokerFfmpeg(this.ffmpegConfig.binaryPath, {
|
|
22054
|
+
logger: this.logger,
|
|
22055
|
+
deviceId,
|
|
22056
|
+
purpose: "derived transcode",
|
|
22057
|
+
onMissing: () => this.reportFfmpegMissing()
|
|
22058
|
+
}),
|
|
21937
22059
|
args,
|
|
21938
22060
|
logger: this.logger.child(`derived-ffmpeg:${camStreamId}`).withTags({ deviceId })
|
|
21939
22061
|
}, {
|
|
@@ -33881,7 +34003,7 @@ var offThreadJsParse = (bytes) => {
|
|
|
33881
34003
|
* Drop-in for the session `demuxVideo` hook: JS parse off the live loop,
|
|
33882
34004
|
* ffmpeg subprocess fallback unchanged.
|
|
33883
34005
|
*/
|
|
33884
|
-
async function demuxSegmentPreferJsOffThread(bytes, opts
|
|
34006
|
+
async function demuxSegmentPreferJsOffThread(bytes, opts) {
|
|
33885
34007
|
return demuxSegmentPreferJs(bytes, {
|
|
33886
34008
|
...opts,
|
|
33887
34009
|
jsParse: offThreadJsParse
|
|
@@ -34066,9 +34188,9 @@ function sliceMuLawToPcmuFrames(mulaw) {
|
|
|
34066
34188
|
* can't decode) — ffmpeg exits non-zero, which is tolerated as empty output.
|
|
34067
34189
|
*
|
|
34068
34190
|
* @param bytes - The full `.m4s` fragment.
|
|
34069
|
-
* @param ffmpegPath -
|
|
34191
|
+
* @param ffmpegPath - The node's pinned ffmpeg (D540). Required: no PATH default.
|
|
34070
34192
|
*/
|
|
34071
|
-
async function demuxSegmentToPcmu(bytes, ffmpegPath
|
|
34193
|
+
async function demuxSegmentToPcmu(bytes, ffmpegPath) {
|
|
34072
34194
|
const mulaw = await runFfmpegOnce({
|
|
34073
34195
|
ffmpegPath,
|
|
34074
34196
|
args: SEGMENT_AUDIO_FFMPEG_ARGS,
|
|
@@ -34200,15 +34322,19 @@ var SEGMENT_AUDIO_OPUS_FFMPEG_ARGS = [
|
|
|
34200
34322
|
* best-effort and must never block recorded video. Mirrors the video-side
|
|
34201
34323
|
* `demuxSegmentPreferJs`.
|
|
34202
34324
|
*/
|
|
34203
|
-
async function demuxSegmentAudioOpusPreferJs(bytes, opts
|
|
34325
|
+
async function demuxSegmentAudioOpusPreferJs(bytes, opts) {
|
|
34204
34326
|
try {
|
|
34205
34327
|
const direct = await demuxSegmentAudioOpusJs(bytes);
|
|
34206
34328
|
if (direct) return direct;
|
|
34207
34329
|
} catch {}
|
|
34330
|
+
if (opts.ffmpegPath === null) {
|
|
34331
|
+
opts.onFfmpegFallback?.("audio track is not Opus (legacy fragment) and the ffmpeg fallback is refused — no pinned ffmpeg on this node (D540)");
|
|
34332
|
+
return [];
|
|
34333
|
+
}
|
|
34208
34334
|
opts.onFfmpegFallback?.("audio track is not Opus (legacy fragment)");
|
|
34209
34335
|
try {
|
|
34210
34336
|
const opusFmp4 = await runFfmpegOnce({
|
|
34211
|
-
ffmpegPath: opts.ffmpegPath
|
|
34337
|
+
ffmpegPath: opts.ffmpegPath,
|
|
34212
34338
|
args: SEGMENT_AUDIO_OPUS_FFMPEG_ARGS,
|
|
34213
34339
|
input: bytes,
|
|
34214
34340
|
tolerateNonZeroExit: true
|
|
@@ -35270,12 +35396,25 @@ var BrokerWebrtcServer = class {
|
|
|
35270
35396
|
session.onClosed = () => this.cleanupSession(sessionId);
|
|
35271
35397
|
const locateRecorded = this.locateRecordedSegment;
|
|
35272
35398
|
const readRecorded = this.readRecordedSegmentBytes;
|
|
35273
|
-
const
|
|
35399
|
+
const resolveFfmpegPath = this.resolveFfmpegPath;
|
|
35400
|
+
const resolveFfmpeg = () => resolveFfmpegPath?.() ?? null;
|
|
35401
|
+
let noPinnedFfmpegLogged = false;
|
|
35402
|
+
const reportNoPinnedFfmpeg = (what) => {
|
|
35403
|
+
if (noPinnedFfmpegLogged) return;
|
|
35404
|
+
noPinnedFfmpegLogged = true;
|
|
35405
|
+
sessionLogger.error("recorded playback: ffmpeg fallback refused — no pinned ffmpeg on this node (D540)", {
|
|
35406
|
+
tags: { deviceId },
|
|
35407
|
+
meta: {
|
|
35408
|
+
sessionId,
|
|
35409
|
+
what
|
|
35410
|
+
}
|
|
35411
|
+
});
|
|
35412
|
+
};
|
|
35274
35413
|
const demuxTally = new DemuxPathTally();
|
|
35275
35414
|
let audioOpusFallbackLogged = false;
|
|
35276
35415
|
if (locateRecorded && readRecorded) {
|
|
35277
35416
|
const demuxVideo = (b) => demuxSegmentPreferJsOffThread(b, {
|
|
35278
|
-
ffmpegPath: resolveFfmpeg
|
|
35417
|
+
ffmpegPath: resolveFfmpeg(),
|
|
35279
35418
|
onDemuxPath: (outcome) => {
|
|
35280
35419
|
const report = demuxTally.record(outcome, Date.now());
|
|
35281
35420
|
if (!report) return;
|
|
@@ -35290,6 +35429,9 @@ var BrokerWebrtcServer = class {
|
|
|
35290
35429
|
}
|
|
35291
35430
|
});
|
|
35292
35431
|
}
|
|
35432
|
+
}).catch((err) => {
|
|
35433
|
+
if (err instanceof FfmpegFallbackRefusedError) reportNoPinnedFfmpeg("video");
|
|
35434
|
+
throw err;
|
|
35293
35435
|
});
|
|
35294
35436
|
const scrubCache = new ScrubFragmentCache();
|
|
35295
35437
|
const initialLiveProfile = this.profileTierResolver?.(brokerId) ?? null;
|
|
@@ -35405,9 +35547,11 @@ var BrokerWebrtcServer = class {
|
|
|
35405
35547
|
pushAudioPcmu: (pcmu, ts) => session.writeRecordedAudio(Buffer.from(pcmu), ts)
|
|
35406
35548
|
};
|
|
35407
35549
|
const demuxAudio = opusSession ? async (b) => {
|
|
35550
|
+
const ffmpegPath = resolveFfmpeg();
|
|
35408
35551
|
return (await demuxSegmentAudioOpusPreferJs(b, {
|
|
35409
|
-
ffmpegPath
|
|
35552
|
+
ffmpegPath,
|
|
35410
35553
|
onFfmpegFallback: (reason) => {
|
|
35554
|
+
if (ffmpegPath === null) reportNoPinnedFfmpeg("audio");
|
|
35411
35555
|
if (audioOpusFallbackLogged) return;
|
|
35412
35556
|
audioOpusFallbackLogged = true;
|
|
35413
35557
|
sessionLogger.warn("recorded audio: JS Opus demux missed — ffmpeg AAC→Opus fallback (once/session)", { meta: {
|
|
@@ -35421,7 +35565,14 @@ var BrokerWebrtcServer = class {
|
|
|
35421
35565
|
data: au.data,
|
|
35422
35566
|
ptsMs: au.ptsMs
|
|
35423
35567
|
}));
|
|
35424
|
-
} : (b) =>
|
|
35568
|
+
} : async (b) => {
|
|
35569
|
+
const ffmpegPath = resolveFfmpeg();
|
|
35570
|
+
if (ffmpegPath === null) {
|
|
35571
|
+
reportNoPinnedFfmpeg("audio");
|
|
35572
|
+
return [];
|
|
35573
|
+
}
|
|
35574
|
+
return await demuxSegmentToPcmu(b, ffmpegPath);
|
|
35575
|
+
};
|
|
35425
35576
|
const clipSource = clip && readClip ? createClipFileSource({
|
|
35426
35577
|
handle: clip,
|
|
35427
35578
|
read: readClip,
|
|
@@ -36871,10 +37022,21 @@ var PROBE_SNAPSHOTS_PERSIST_INTERVAL_MS = 6e4;
|
|
|
36871
37022
|
var NETWORK_QUALITY_STATS_TIMEOUT_MS = 2e3;
|
|
36872
37023
|
var StreamBrokerAddon = class extends BaseAddon {
|
|
36873
37024
|
brokerManager = null;
|
|
36874
|
-
/**
|
|
36875
|
-
*
|
|
36876
|
-
*
|
|
36877
|
-
|
|
37025
|
+
/** The node's pinned ffmpeg, mirrored here so the recorded-playback demuxers
|
|
37026
|
+
* (segment-demux/segment-audio) spawn the same binary as the transcode
|
|
37027
|
+
* egress. Kept in sync by the ffmpeg wiring. `null` = none could be resolved:
|
|
37028
|
+
* their ffmpeg fallbacks refuse by name — never a PATH `ffmpeg` (D540). */
|
|
37029
|
+
ffmpegBinaryPath = null;
|
|
37030
|
+
/** The node's pinned build, asked once on success (D540). A failure is not
|
|
37031
|
+
* remembered, so the re-resolve below can succeed later. */
|
|
37032
|
+
pinnedFfmpeg = memoizePinnedFfmpeg(() => this.ctx.deps.ensureFfmpeg());
|
|
37033
|
+
/**
|
|
37034
|
+
* The way back from a failed boot-time resolution: a refused child asks, and
|
|
37035
|
+
* ONE bounded re-resolve hands the binary to the manager config (which every
|
|
37036
|
+
* live broker reads per dial) and the demuxers. Without it one failed
|
|
37037
|
+
* download disabled every broker ffmpeg child until the runner restarted.
|
|
37038
|
+
*/
|
|
37039
|
+
ffmpegWiring = null;
|
|
36878
37040
|
/** Once-per-addon log guard for the readGopBytes → whole-segment downgrade. */
|
|
36879
37041
|
gopReadFallbackLogged = false;
|
|
36880
37042
|
metricsSnapshotTimer = null;
|
|
@@ -36935,36 +37097,7 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
36935
37097
|
async resolveFfmpegBinaryPath() {
|
|
36936
37098
|
const override = this.config.binaryPath;
|
|
36937
37099
|
if (typeof override === "string" && override.trim().length > 0) return override;
|
|
36938
|
-
return await this.
|
|
36939
|
-
}
|
|
36940
|
-
/**
|
|
36941
|
-
* Push THIS node's ffmpeg config into the broker — all node-local now, NOT the
|
|
36942
|
-
* retired global `ffmpeg` section. `binaryPath` via {@link resolveFfmpegBinaryPath}
|
|
36943
|
-
* (per-node override → ensureFfmpeg); `hwAccel` = the per-node `hwaccel`
|
|
36944
|
-
* preference (default `'copy'` = software, the safe HEVC-compatible choice; the
|
|
36945
|
-
* concrete backend is resolved node-locally by the egress transcode manager,
|
|
36946
|
-
* which asks the DECODER ADDON — never the kernel resolver, whose qsv-first
|
|
36947
|
-
* order is the known-bad one here). `threadCount` is always `0` (auto — ffmpeg uses the
|
|
36948
|
-
* node's own cores; the manual knob was removed).
|
|
36949
|
-
*/
|
|
36950
|
-
async loadFfmpegConfig() {
|
|
36951
|
-
if (!this.brokerManager) return;
|
|
36952
|
-
try {
|
|
36953
|
-
const binaryPath = await this.resolveFfmpegBinaryPath();
|
|
36954
|
-
const hwAccel = this.config.hwaccel;
|
|
36955
|
-
this.brokerManager.setFfmpegConfig({
|
|
36956
|
-
binaryPath,
|
|
36957
|
-
hwAccel,
|
|
36958
|
-
threadCount: 0
|
|
36959
|
-
});
|
|
36960
|
-
this.ffmpegBinaryPath = binaryPath;
|
|
36961
|
-
this.ctx.logger.info("Loaded ffmpeg config", { meta: {
|
|
36962
|
-
binaryPath,
|
|
36963
|
-
hwAccel
|
|
36964
|
-
} });
|
|
36965
|
-
} catch (err) {
|
|
36966
|
-
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: errMsg(err) } });
|
|
36967
|
-
}
|
|
37100
|
+
return await this.pinnedFfmpeg();
|
|
36968
37101
|
}
|
|
36969
37102
|
async onInitialize() {
|
|
36970
37103
|
this.ctx.logger.info("log channels declared", { meta: { channels: STREAM_BROKER_LOG_CHANNELS.map((c) => c.descriptor.name) } });
|
|
@@ -36994,7 +37127,16 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
36994
37127
|
};
|
|
36995
37128
|
this.brokerManager.setDeviceSignalReader(async (deviceId) => (await readSignals(deviceId)).map((s) => s.code));
|
|
36996
37129
|
this.brokerManager.setDeviceSignalDetailReader(readSignals);
|
|
36997
|
-
|
|
37130
|
+
this.ffmpegWiring = wireBrokerFfmpeg({
|
|
37131
|
+
host: this.brokerManager,
|
|
37132
|
+
resolve: () => this.resolveFfmpegBinaryPath(),
|
|
37133
|
+
hwAccel: () => this.config.hwaccel,
|
|
37134
|
+
onBinaryPath: (binaryPath) => {
|
|
37135
|
+
this.ffmpegBinaryPath = binaryPath;
|
|
37136
|
+
},
|
|
37137
|
+
logger: this.ctx.logger
|
|
37138
|
+
});
|
|
37139
|
+
await this.ffmpegWiring.load();
|
|
36998
37140
|
await this.declareRowCollections();
|
|
36999
37141
|
await this.purgeFlattenedSettingsKeys();
|
|
37000
37142
|
const rtspProvider = this.brokerManager.getRtspRestreamProvider();
|
|
@@ -37279,7 +37421,10 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
37279
37421
|
};
|
|
37280
37422
|
}
|
|
37281
37423
|
},
|
|
37282
|
-
resolveFfmpegPath: () =>
|
|
37424
|
+
resolveFfmpegPath: () => {
|
|
37425
|
+
if (this.ffmpegBinaryPath === null) this.ffmpegWiring?.request("a recorded-playback demux needed ffmpeg");
|
|
37426
|
+
return this.ffmpegBinaryPath;
|
|
37427
|
+
}
|
|
37283
37428
|
});
|
|
37284
37429
|
this.brokerManager.setWebrtcServer(webrtcServer);
|
|
37285
37430
|
this.webrtcServer = webrtcServer;
|
|
@@ -37689,6 +37834,8 @@ var FfmpegDecoderSession = class {
|
|
|
37689
37834
|
outputBuffer = Buffer.alloc(0);
|
|
37690
37835
|
destroyed = false;
|
|
37691
37836
|
logger;
|
|
37837
|
+
/** The pinned ffmpeg to spawn (D540). Required: there is no PATH default. */
|
|
37838
|
+
ffmpegPath;
|
|
37692
37839
|
cachedWidth = 0;
|
|
37693
37840
|
cachedHeight = 0;
|
|
37694
37841
|
inputPackets = 0;
|
|
@@ -37697,8 +37844,9 @@ var FfmpegDecoderSession = class {
|
|
|
37697
37844
|
totalDecodeTimeMs = 0;
|
|
37698
37845
|
decodeCount = 0;
|
|
37699
37846
|
startTime = Date.now();
|
|
37700
|
-
constructor(config, logger = noopLogger) {
|
|
37847
|
+
constructor(config, ffmpegPath, logger = noopLogger) {
|
|
37701
37848
|
this.config = { ...config };
|
|
37849
|
+
this.ffmpegPath = ffmpegPath;
|
|
37702
37850
|
this.logger = logger;
|
|
37703
37851
|
this.frameDropper = new FrameDropper(config.maxFps);
|
|
37704
37852
|
this.spawnFfmpeg();
|
|
@@ -37709,7 +37857,8 @@ var FfmpegDecoderSession = class {
|
|
|
37709
37857
|
this.outputBuffer = Buffer.alloc(0);
|
|
37710
37858
|
this.cachedWidth = 0;
|
|
37711
37859
|
this.cachedHeight = 0;
|
|
37712
|
-
const
|
|
37860
|
+
const args = buildFfmpegArgs(this.config);
|
|
37861
|
+
const child = spawn(this.ffmpegPath, args);
|
|
37713
37862
|
this.process = child;
|
|
37714
37863
|
child.stdin?.on("error", () => {});
|
|
37715
37864
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -37842,7 +37991,14 @@ var SUPPORTED_CODECS = new Set([
|
|
|
37842
37991
|
"hevc",
|
|
37843
37992
|
"mjpeg"
|
|
37844
37993
|
]);
|
|
37994
|
+
/**
|
|
37995
|
+
* A static decoder provider for stand-alone broker harnesses. Production
|
|
37996
|
+
* decodes through the `decoder` cap; this exists for the manual streaming
|
|
37997
|
+
* diagnostics. It is handed the pinned ffmpeg (D540) — there is no PATH
|
|
37998
|
+
* default for it to fall back to.
|
|
37999
|
+
*/
|
|
37845
38000
|
var FfmpegDecoderProvider = class {
|
|
38001
|
+
ffmpegPath;
|
|
37846
38002
|
id = "ffmpeg";
|
|
37847
38003
|
name = "FFmpeg Decoder";
|
|
37848
38004
|
/** Software decoder — used as fallback when hardware decoders are unavailable. */
|
|
@@ -37850,8 +38006,11 @@ var FfmpegDecoderProvider = class {
|
|
|
37850
38006
|
async supportsCodec(input) {
|
|
37851
38007
|
return SUPPORTED_CODECS.has(input.codec);
|
|
37852
38008
|
}
|
|
38009
|
+
constructor(ffmpegPath) {
|
|
38010
|
+
this.ffmpegPath = ffmpegPath;
|
|
38011
|
+
}
|
|
37853
38012
|
async createSession(config) {
|
|
37854
|
-
return new FfmpegDecoderSession(config);
|
|
38013
|
+
return new FfmpegDecoderSession(config, this.ffmpegPath);
|
|
37855
38014
|
}
|
|
37856
38015
|
};
|
|
37857
38016
|
//#endregion
|