@camstack/addon-pipeline 1.2.307 → 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-DgdiQtQb.js → default-detection-model-2rHeRf0Y.js} +1 -1
- package/dist/{default-detection-model-BV9bIuOo.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-CyJWk-_f.js → dist-CdRsXAov.js} +79 -1
- package/dist/{dist-DdVMFlJ6.mjs → dist-jy-7xoW5.mjs} +74 -2
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-COu_tHy4.mjs → node-NCge1qZM.mjs} +1 -53
- package/dist/{node-HsWG5fGp.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-Bz3oIMzl.mjs → process-memory-4K7QaQab.mjs} +1 -1
- package/dist/{process-memory-TQXG7qrJ.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-BrKa7tpr.js → segment-demux-js-0bM2Anej.js} +22 -4
- package/dist/{segment-demux-js-CEJn4BR0.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-DdYUVQSA.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-D--a9rVM.mjs → hostInit-D56Gr-hD.mjs} +1 -1
- package/dist/stream-broker/index.js +305 -95
- package/dist/stream-broker/index.mjs +305 -95
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DPHv0v3Q.mjs +0 -26
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { An as
|
|
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
|
}
|
|
@@ -7121,7 +7217,7 @@ async function gifFromMp4(input) {
|
|
|
7121
7217
|
}
|
|
7122
7218
|
function runGif(input, args) {
|
|
7123
7219
|
return new Promise((resolve, reject) => {
|
|
7124
|
-
const proc = input.spawnFn(input.ffmpegPath
|
|
7220
|
+
const proc = input.spawnFn(input.ffmpegPath, [...args], { stdio: [
|
|
7125
7221
|
"pipe",
|
|
7126
7222
|
"pipe",
|
|
7127
7223
|
"pipe"
|
|
@@ -7985,7 +8081,7 @@ async function renderPreBufferClip(input) {
|
|
|
7985
8081
|
hevc,
|
|
7986
8082
|
sourceWidth: input.sourceWidth ?? 0
|
|
7987
8083
|
});
|
|
7988
|
-
const proc = input.spawn(input.ffmpegPath
|
|
8084
|
+
const proc = input.spawn(input.ffmpegPath, clipFfmpegArgs({
|
|
7989
8085
|
...input,
|
|
7990
8086
|
hevc,
|
|
7991
8087
|
copyVideo
|
|
@@ -12802,11 +12898,12 @@ var StreamBroker = class StreamBroker {
|
|
|
12802
12898
|
/** Active "ffmpeg parser" ingress reader when `source.ffmpegParser` is set. */
|
|
12803
12899
|
ffmpegSourceReader = null;
|
|
12804
12900
|
/**
|
|
12805
|
-
*
|
|
12806
|
-
*
|
|
12807
|
-
*
|
|
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).
|
|
12808
12905
|
*/
|
|
12809
|
-
|
|
12906
|
+
ffmpegBinaryPathSource = () => null;
|
|
12810
12907
|
rtpDepacketizer = null;
|
|
12811
12908
|
/** Flag-only depacketizer for the `push-rtp` source-RTP pre-buffer path
|
|
12812
12909
|
* (sets keyframe/param-set flags without re-emitting AnnexB packets). */
|
|
@@ -13389,12 +13486,21 @@ var StreamBroker = class StreamBroker {
|
|
|
13389
13486
|
this.restreamUrlResolver = resolver;
|
|
13390
13487
|
}
|
|
13391
13488
|
/**
|
|
13392
|
-
* Inject
|
|
13393
|
-
*
|
|
13394
|
-
* 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.
|
|
13395
13491
|
*/
|
|
13396
|
-
|
|
13397
|
-
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;
|
|
13398
13504
|
}
|
|
13399
13505
|
/**
|
|
13400
13506
|
* Inject the addon's child-cost registry. Injected rather than constructed
|
|
@@ -15320,18 +15426,28 @@ var StreamBroker = class StreamBroker {
|
|
|
15320
15426
|
transport: source.ffmpegParser?.transport ?? "tcp"
|
|
15321
15427
|
} });
|
|
15322
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
|
+
}
|
|
15323
15440
|
const parser = source.ffmpegParser;
|
|
15324
15441
|
const sourceKind = source.type === "rtmp" ? "rtmp" : source.type === "flv" || source.type === "http" ? "http" : "rtsp";
|
|
15325
|
-
const args = buildFfmpegSourceReaderArgs({
|
|
15326
|
-
sourceUrl: source.url,
|
|
15327
|
-
sourceKind,
|
|
15328
|
-
transport: parser?.transport ?? "tcp",
|
|
15329
|
-
...parser?.inputArgs ? { inputArgs: parser.inputArgs } : {},
|
|
15330
|
-
...parser?.outputArgs ? { outputArgs: parser.outputArgs } : {}
|
|
15331
|
-
});
|
|
15332
15442
|
const reader = new FfmpegSourceReader({
|
|
15333
|
-
ffmpegPath
|
|
15334
|
-
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
|
+
}),
|
|
15335
15451
|
...this.logger ? { logger: this.logger.child("ffmpeg-source") } : {},
|
|
15336
15452
|
claimCost: (pid) => this.claimIngestCost(pid)
|
|
15337
15453
|
}, {
|
|
@@ -16307,7 +16423,7 @@ function hexToBytes(hex) {
|
|
|
16307
16423
|
* pipe is the pattern proven by the WebRTC transcode feed.
|
|
16308
16424
|
*/
|
|
16309
16425
|
var DEFAULT_FFMPEG_CONFIG = {
|
|
16310
|
-
binaryPath:
|
|
16426
|
+
binaryPath: null,
|
|
16311
16427
|
hwAccel: "copy",
|
|
16312
16428
|
threadCount: 0
|
|
16313
16429
|
};
|
|
@@ -16341,6 +16457,7 @@ var TranscodePipelineManager = class {
|
|
|
16341
16457
|
resolveProfileSource;
|
|
16342
16458
|
releaseGraceMs;
|
|
16343
16459
|
ffmpegConfig;
|
|
16460
|
+
onFfmpegMissing;
|
|
16344
16461
|
resolveStreamHwAccel;
|
|
16345
16462
|
spawnFn;
|
|
16346
16463
|
acquireChannel;
|
|
@@ -16353,6 +16470,7 @@ var TranscodePipelineManager = class {
|
|
|
16353
16470
|
this.resolveProfileSource = opts.resolveProfileSource ?? null;
|
|
16354
16471
|
this.releaseGraceMs = opts.releaseGraceMs ?? 5e3;
|
|
16355
16472
|
this.ffmpegConfig = opts.ffmpegConfig ?? (() => DEFAULT_FFMPEG_CONFIG);
|
|
16473
|
+
this.onFfmpegMissing = opts.onFfmpegMissing ?? null;
|
|
16356
16474
|
this.probeFfmpeg = opts.probeFfmpeg ?? probeFfmpegBinary;
|
|
16357
16475
|
this.resolveStreamHwAccel = opts.resolveStreamHwAccel ?? null;
|
|
16358
16476
|
this.spawnFn = opts.spawnFn ?? spawn;
|
|
@@ -16425,11 +16543,17 @@ var TranscodePipelineManager = class {
|
|
|
16425
16543
|
async startTranscode(opts) {
|
|
16426
16544
|
if (!this.acquireChannel) throw new Error("getStreamWithCodec: transcode egress not wired (acquireChannel missing)");
|
|
16427
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
|
+
});
|
|
16428
16552
|
const effectiveHwAccel = this.effectiveHwAccelSetting(opts.deviceId, opts.camStreamId, cfg.hwAccel);
|
|
16429
16553
|
const choice = chooseVideoEncoder({
|
|
16430
16554
|
target: opts.targetCodec,
|
|
16431
16555
|
backend: await this.resolveHardwareBackend(),
|
|
16432
|
-
binaryEncoders: await this.resolveBinaryEncoders(
|
|
16556
|
+
binaryEncoders: await this.resolveBinaryEncoders(ffmpegBinaryPath),
|
|
16433
16557
|
hwAccelSetting: effectiveHwAccel,
|
|
16434
16558
|
renderNode: DEFAULT_VAAPI_RENDER_DEVICE
|
|
16435
16559
|
});
|
|
@@ -16459,7 +16583,7 @@ var TranscodePipelineManager = class {
|
|
|
16459
16583
|
const sidecarCodec = opts.audioEgress ? toAudioSidecarCodec(opts.audio) : null;
|
|
16460
16584
|
const egress = new TranscodeEgress({
|
|
16461
16585
|
logger: this.logger.child("egress"),
|
|
16462
|
-
ffmpegBinaryPath
|
|
16586
|
+
ffmpegBinaryPath,
|
|
16463
16587
|
spawnFn: this.spawnFn,
|
|
16464
16588
|
acquireChannel: this.acquireChannel
|
|
16465
16589
|
}, {
|
|
@@ -16688,11 +16812,12 @@ var TranscodePipelineManager = class {
|
|
|
16688
16812
|
try {
|
|
16689
16813
|
const backend = rankDecodeHwAccels((await this.hwaccel.resolve(prefer)).preferred, [])[0] ?? null;
|
|
16690
16814
|
if (backend === null) return null;
|
|
16691
|
-
const
|
|
16815
|
+
const binaryPath = this.ffmpegConfig().binaryPath;
|
|
16816
|
+
const caps = binaryPath === null ? null : await this.resolveBinaryCapabilities(binaryPath);
|
|
16692
16817
|
if (caps !== null && !caps.hwaccels.has(backend)) {
|
|
16693
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: {
|
|
16694
16819
|
backend,
|
|
16695
|
-
ffmpeg:
|
|
16820
|
+
ffmpeg: binaryPath,
|
|
16696
16821
|
hwaccels: [...caps.hwaccels].join(",") || "none"
|
|
16697
16822
|
} });
|
|
16698
16823
|
return null;
|
|
@@ -17373,12 +17498,14 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17373
17498
|
/**
|
|
17374
17499
|
* Snapshot of the system `ffmpeg` config section (binaryPath / hwAccel /
|
|
17375
17500
|
* threadCount). Refreshed by the host addon via `setFfmpegConfig`; the
|
|
17376
|
-
* transcode manager reads it live per spawn.
|
|
17377
|
-
*
|
|
17378
|
-
*
|
|
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.
|
|
17379
17506
|
*/
|
|
17380
17507
|
ffmpegConfig = {
|
|
17381
|
-
binaryPath:
|
|
17508
|
+
binaryPath: null,
|
|
17382
17509
|
hwAccel: "copy",
|
|
17383
17510
|
threadCount: 0
|
|
17384
17511
|
};
|
|
@@ -17414,6 +17541,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17414
17541
|
},
|
|
17415
17542
|
resolveProfileSource: (deviceId, profile) => this.assignments.get(deviceId)?.map[profile] ?? null,
|
|
17416
17543
|
ffmpegConfig: () => this.ffmpegConfig,
|
|
17544
|
+
onFfmpegMissing: () => this.reportFfmpegMissing(),
|
|
17417
17545
|
resolveStreamHwAccel: (deviceId, camStreamId) => this.getStreamHwAccelOverride(deviceId, camStreamId),
|
|
17418
17546
|
sentryOffered: async (deviceId) => sentryEligible({
|
|
17419
17547
|
batteryOperated: await this.isBatteryOperated(deviceId),
|
|
@@ -17476,6 +17604,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17476
17604
|
});
|
|
17477
17605
|
},
|
|
17478
17606
|
ffmpegConfig: () => this.ffmpegConfig,
|
|
17607
|
+
onFfmpegMissing: () => this.reportFfmpegMissing(),
|
|
17479
17608
|
...this.costRegistry === null ? {} : { costRegistry: this.costRegistry },
|
|
17480
17609
|
resolveDecodeHwAccel: (deviceId, override) => this.resolveEgressHwAccel(deviceId, override),
|
|
17481
17610
|
acquireChannel: (brokerId) => {
|
|
@@ -17851,11 +17980,35 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17851
17980
|
if (localNodeId !== void 0) this.localNodeId = localNodeId;
|
|
17852
17981
|
if (hwaccel !== void 0) this.hwaccel = hwaccel;
|
|
17853
17982
|
}
|
|
17854
|
-
/**
|
|
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
|
+
*/
|
|
17855
17989
|
setFfmpegConfig(cfg) {
|
|
17856
17990
|
this.ffmpegConfig = cfg;
|
|
17857
17991
|
}
|
|
17858
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
|
+
/**
|
|
17859
18012
|
* The decode `hwaccel` options surfaceable in a (future) UI: the always-valid
|
|
17860
18013
|
* settings (`copy`, `auto`, `none`) plus the concrete hardware backends THIS
|
|
17861
18014
|
* node can use, resolved from the LOCAL `ctx.kernel.hwaccel` (os.platform +
|
|
@@ -18196,7 +18349,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18196
18349
|
let annexB;
|
|
18197
18350
|
try {
|
|
18198
18351
|
annexB = await encodePrestreamIdr({
|
|
18199
|
-
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
|
+
}),
|
|
18200
18358
|
plan,
|
|
18201
18359
|
jpeg: picture.jpeg,
|
|
18202
18360
|
logger,
|
|
@@ -19448,7 +19606,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19448
19606
|
height: scaledHeight(cut, clipWidth)
|
|
19449
19607
|
});
|
|
19450
19608
|
const gifSpeed = input.gifSpeed ?? 4;
|
|
19451
|
-
|
|
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) {
|
|
19452
19615
|
const gif = await gifFromMp4({
|
|
19453
19616
|
mp4,
|
|
19454
19617
|
deviceId: input.deviceId,
|
|
@@ -19457,7 +19620,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19457
19620
|
speed: gifDeriveSpeed(gifSpeed, input.speed),
|
|
19458
19621
|
...this.logger !== void 0 ? { logger: this.logger } : {},
|
|
19459
19622
|
spawnFn: spawn,
|
|
19460
|
-
|
|
19623
|
+
ffmpegPath: gifFfmpeg
|
|
19461
19624
|
});
|
|
19462
19625
|
if (gif !== null) {
|
|
19463
19626
|
const height = scaledHeight(cut, input.gifMaxWidth);
|
|
@@ -19626,7 +19789,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19626
19789
|
"pipe",
|
|
19627
19790
|
"pipe"
|
|
19628
19791
|
] }),
|
|
19629
|
-
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
|
+
}),
|
|
19630
19798
|
sdpParameterSets: cut.broker.getSdpParameterSets()
|
|
19631
19799
|
});
|
|
19632
19800
|
this.logger?.info("pre-buffer clip rendered", {
|
|
@@ -21704,7 +21872,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21704
21872
|
const source = this.buildSourceFromCamStream(cam);
|
|
21705
21873
|
const broker = new StreamBroker(brokerId, this.buildDecoderCapApi(deviceId), brokerLogger, buildAudioCodecCapApi(this.api), this.localNodeId);
|
|
21706
21874
|
broker.setRestreamUrlResolver(() => this.rtspProvider.getEntry(brokerId)?.mutedUrl ?? null);
|
|
21707
|
-
|
|
21875
|
+
this.bindBrokerFfmpeg(broker);
|
|
21708
21876
|
if (this.costRegistry !== null) broker.setCostRegistry(this.costRegistry);
|
|
21709
21877
|
this.applyPrebufferConfig(broker, deviceId, camStreamId);
|
|
21710
21878
|
this.applyAudioMuteConfig(broker, deviceId);
|
|
@@ -21882,7 +22050,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21882
22050
|
}
|
|
21883
22051
|
});
|
|
21884
22052
|
const feeder = new DerivedStreamFeeder({
|
|
21885
|
-
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
|
+
}),
|
|
21886
22059
|
args,
|
|
21887
22060
|
logger: this.logger.child(`derived-ffmpeg:${camStreamId}`).withTags({ deviceId })
|
|
21888
22061
|
}, {
|
|
@@ -33830,7 +34003,7 @@ var offThreadJsParse = (bytes) => {
|
|
|
33830
34003
|
* Drop-in for the session `demuxVideo` hook: JS parse off the live loop,
|
|
33831
34004
|
* ffmpeg subprocess fallback unchanged.
|
|
33832
34005
|
*/
|
|
33833
|
-
async function demuxSegmentPreferJsOffThread(bytes, opts
|
|
34006
|
+
async function demuxSegmentPreferJsOffThread(bytes, opts) {
|
|
33834
34007
|
return demuxSegmentPreferJs(bytes, {
|
|
33835
34008
|
...opts,
|
|
33836
34009
|
jsParse: offThreadJsParse
|
|
@@ -34015,9 +34188,9 @@ function sliceMuLawToPcmuFrames(mulaw) {
|
|
|
34015
34188
|
* can't decode) — ffmpeg exits non-zero, which is tolerated as empty output.
|
|
34016
34189
|
*
|
|
34017
34190
|
* @param bytes - The full `.m4s` fragment.
|
|
34018
|
-
* @param ffmpegPath -
|
|
34191
|
+
* @param ffmpegPath - The node's pinned ffmpeg (D540). Required: no PATH default.
|
|
34019
34192
|
*/
|
|
34020
|
-
async function demuxSegmentToPcmu(bytes, ffmpegPath
|
|
34193
|
+
async function demuxSegmentToPcmu(bytes, ffmpegPath) {
|
|
34021
34194
|
const mulaw = await runFfmpegOnce({
|
|
34022
34195
|
ffmpegPath,
|
|
34023
34196
|
args: SEGMENT_AUDIO_FFMPEG_ARGS,
|
|
@@ -34149,15 +34322,19 @@ var SEGMENT_AUDIO_OPUS_FFMPEG_ARGS = [
|
|
|
34149
34322
|
* best-effort and must never block recorded video. Mirrors the video-side
|
|
34150
34323
|
* `demuxSegmentPreferJs`.
|
|
34151
34324
|
*/
|
|
34152
|
-
async function demuxSegmentAudioOpusPreferJs(bytes, opts
|
|
34325
|
+
async function demuxSegmentAudioOpusPreferJs(bytes, opts) {
|
|
34153
34326
|
try {
|
|
34154
34327
|
const direct = await demuxSegmentAudioOpusJs(bytes);
|
|
34155
34328
|
if (direct) return direct;
|
|
34156
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
|
+
}
|
|
34157
34334
|
opts.onFfmpegFallback?.("audio track is not Opus (legacy fragment)");
|
|
34158
34335
|
try {
|
|
34159
34336
|
const opusFmp4 = await runFfmpegOnce({
|
|
34160
|
-
ffmpegPath: opts.ffmpegPath
|
|
34337
|
+
ffmpegPath: opts.ffmpegPath,
|
|
34161
34338
|
args: SEGMENT_AUDIO_OPUS_FFMPEG_ARGS,
|
|
34162
34339
|
input: bytes,
|
|
34163
34340
|
tolerateNonZeroExit: true
|
|
@@ -35219,12 +35396,25 @@ var BrokerWebrtcServer = class {
|
|
|
35219
35396
|
session.onClosed = () => this.cleanupSession(sessionId);
|
|
35220
35397
|
const locateRecorded = this.locateRecordedSegment;
|
|
35221
35398
|
const readRecorded = this.readRecordedSegmentBytes;
|
|
35222
|
-
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
|
+
};
|
|
35223
35413
|
const demuxTally = new DemuxPathTally();
|
|
35224
35414
|
let audioOpusFallbackLogged = false;
|
|
35225
35415
|
if (locateRecorded && readRecorded) {
|
|
35226
35416
|
const demuxVideo = (b) => demuxSegmentPreferJsOffThread(b, {
|
|
35227
|
-
ffmpegPath: resolveFfmpeg
|
|
35417
|
+
ffmpegPath: resolveFfmpeg(),
|
|
35228
35418
|
onDemuxPath: (outcome) => {
|
|
35229
35419
|
const report = demuxTally.record(outcome, Date.now());
|
|
35230
35420
|
if (!report) return;
|
|
@@ -35239,6 +35429,9 @@ var BrokerWebrtcServer = class {
|
|
|
35239
35429
|
}
|
|
35240
35430
|
});
|
|
35241
35431
|
}
|
|
35432
|
+
}).catch((err) => {
|
|
35433
|
+
if (err instanceof FfmpegFallbackRefusedError) reportNoPinnedFfmpeg("video");
|
|
35434
|
+
throw err;
|
|
35242
35435
|
});
|
|
35243
35436
|
const scrubCache = new ScrubFragmentCache();
|
|
35244
35437
|
const initialLiveProfile = this.profileTierResolver?.(brokerId) ?? null;
|
|
@@ -35354,9 +35547,11 @@ var BrokerWebrtcServer = class {
|
|
|
35354
35547
|
pushAudioPcmu: (pcmu, ts) => session.writeRecordedAudio(Buffer.from(pcmu), ts)
|
|
35355
35548
|
};
|
|
35356
35549
|
const demuxAudio = opusSession ? async (b) => {
|
|
35550
|
+
const ffmpegPath = resolveFfmpeg();
|
|
35357
35551
|
return (await demuxSegmentAudioOpusPreferJs(b, {
|
|
35358
|
-
ffmpegPath
|
|
35552
|
+
ffmpegPath,
|
|
35359
35553
|
onFfmpegFallback: (reason) => {
|
|
35554
|
+
if (ffmpegPath === null) reportNoPinnedFfmpeg("audio");
|
|
35360
35555
|
if (audioOpusFallbackLogged) return;
|
|
35361
35556
|
audioOpusFallbackLogged = true;
|
|
35362
35557
|
sessionLogger.warn("recorded audio: JS Opus demux missed — ffmpeg AAC→Opus fallback (once/session)", { meta: {
|
|
@@ -35370,7 +35565,14 @@ var BrokerWebrtcServer = class {
|
|
|
35370
35565
|
data: au.data,
|
|
35371
35566
|
ptsMs: au.ptsMs
|
|
35372
35567
|
}));
|
|
35373
|
-
} : (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
|
+
};
|
|
35374
35576
|
const clipSource = clip && readClip ? createClipFileSource({
|
|
35375
35577
|
handle: clip,
|
|
35376
35578
|
read: readClip,
|
|
@@ -36820,10 +37022,21 @@ var PROBE_SNAPSHOTS_PERSIST_INTERVAL_MS = 6e4;
|
|
|
36820
37022
|
var NETWORK_QUALITY_STATS_TIMEOUT_MS = 2e3;
|
|
36821
37023
|
var StreamBrokerAddon = class extends BaseAddon {
|
|
36822
37024
|
brokerManager = null;
|
|
36823
|
-
/**
|
|
36824
|
-
*
|
|
36825
|
-
*
|
|
36826
|
-
|
|
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;
|
|
36827
37040
|
/** Once-per-addon log guard for the readGopBytes → whole-segment downgrade. */
|
|
36828
37041
|
gopReadFallbackLogged = false;
|
|
36829
37042
|
metricsSnapshotTimer = null;
|
|
@@ -36884,36 +37097,7 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
36884
37097
|
async resolveFfmpegBinaryPath() {
|
|
36885
37098
|
const override = this.config.binaryPath;
|
|
36886
37099
|
if (typeof override === "string" && override.trim().length > 0) return override;
|
|
36887
|
-
return await this.
|
|
36888
|
-
}
|
|
36889
|
-
/**
|
|
36890
|
-
* Push THIS node's ffmpeg config into the broker — all node-local now, NOT the
|
|
36891
|
-
* retired global `ffmpeg` section. `binaryPath` via {@link resolveFfmpegBinaryPath}
|
|
36892
|
-
* (per-node override → ensureFfmpeg); `hwAccel` = the per-node `hwaccel`
|
|
36893
|
-
* preference (default `'copy'` = software, the safe HEVC-compatible choice; the
|
|
36894
|
-
* concrete backend is resolved node-locally by the egress transcode manager,
|
|
36895
|
-
* which asks the DECODER ADDON — never the kernel resolver, whose qsv-first
|
|
36896
|
-
* order is the known-bad one here). `threadCount` is always `0` (auto — ffmpeg uses the
|
|
36897
|
-
* node's own cores; the manual knob was removed).
|
|
36898
|
-
*/
|
|
36899
|
-
async loadFfmpegConfig() {
|
|
36900
|
-
if (!this.brokerManager) return;
|
|
36901
|
-
try {
|
|
36902
|
-
const binaryPath = await this.resolveFfmpegBinaryPath();
|
|
36903
|
-
const hwAccel = this.config.hwaccel;
|
|
36904
|
-
this.brokerManager.setFfmpegConfig({
|
|
36905
|
-
binaryPath,
|
|
36906
|
-
hwAccel,
|
|
36907
|
-
threadCount: 0
|
|
36908
|
-
});
|
|
36909
|
-
this.ffmpegBinaryPath = binaryPath;
|
|
36910
|
-
this.ctx.logger.info("Loaded ffmpeg config", { meta: {
|
|
36911
|
-
binaryPath,
|
|
36912
|
-
hwAccel
|
|
36913
|
-
} });
|
|
36914
|
-
} catch (err) {
|
|
36915
|
-
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) } });
|
|
36916
|
-
}
|
|
37100
|
+
return await this.pinnedFfmpeg();
|
|
36917
37101
|
}
|
|
36918
37102
|
async onInitialize() {
|
|
36919
37103
|
this.ctx.logger.info("log channels declared", { meta: { channels: STREAM_BROKER_LOG_CHANNELS.map((c) => c.descriptor.name) } });
|
|
@@ -36943,7 +37127,16 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
36943
37127
|
};
|
|
36944
37128
|
this.brokerManager.setDeviceSignalReader(async (deviceId) => (await readSignals(deviceId)).map((s) => s.code));
|
|
36945
37129
|
this.brokerManager.setDeviceSignalDetailReader(readSignals);
|
|
36946
|
-
|
|
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();
|
|
36947
37140
|
await this.declareRowCollections();
|
|
36948
37141
|
await this.purgeFlattenedSettingsKeys();
|
|
36949
37142
|
const rtspProvider = this.brokerManager.getRtspRestreamProvider();
|
|
@@ -37228,7 +37421,10 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
37228
37421
|
};
|
|
37229
37422
|
}
|
|
37230
37423
|
},
|
|
37231
|
-
resolveFfmpegPath: () =>
|
|
37424
|
+
resolveFfmpegPath: () => {
|
|
37425
|
+
if (this.ffmpegBinaryPath === null) this.ffmpegWiring?.request("a recorded-playback demux needed ffmpeg");
|
|
37426
|
+
return this.ffmpegBinaryPath;
|
|
37427
|
+
}
|
|
37232
37428
|
});
|
|
37233
37429
|
this.brokerManager.setWebrtcServer(webrtcServer);
|
|
37234
37430
|
this.webrtcServer = webrtcServer;
|
|
@@ -37638,6 +37834,8 @@ var FfmpegDecoderSession = class {
|
|
|
37638
37834
|
outputBuffer = Buffer.alloc(0);
|
|
37639
37835
|
destroyed = false;
|
|
37640
37836
|
logger;
|
|
37837
|
+
/** The pinned ffmpeg to spawn (D540). Required: there is no PATH default. */
|
|
37838
|
+
ffmpegPath;
|
|
37641
37839
|
cachedWidth = 0;
|
|
37642
37840
|
cachedHeight = 0;
|
|
37643
37841
|
inputPackets = 0;
|
|
@@ -37646,8 +37844,9 @@ var FfmpegDecoderSession = class {
|
|
|
37646
37844
|
totalDecodeTimeMs = 0;
|
|
37647
37845
|
decodeCount = 0;
|
|
37648
37846
|
startTime = Date.now();
|
|
37649
|
-
constructor(config, logger = noopLogger) {
|
|
37847
|
+
constructor(config, ffmpegPath, logger = noopLogger) {
|
|
37650
37848
|
this.config = { ...config };
|
|
37849
|
+
this.ffmpegPath = ffmpegPath;
|
|
37651
37850
|
this.logger = logger;
|
|
37652
37851
|
this.frameDropper = new FrameDropper(config.maxFps);
|
|
37653
37852
|
this.spawnFfmpeg();
|
|
@@ -37658,7 +37857,8 @@ var FfmpegDecoderSession = class {
|
|
|
37658
37857
|
this.outputBuffer = Buffer.alloc(0);
|
|
37659
37858
|
this.cachedWidth = 0;
|
|
37660
37859
|
this.cachedHeight = 0;
|
|
37661
|
-
const
|
|
37860
|
+
const args = buildFfmpegArgs(this.config);
|
|
37861
|
+
const child = spawn(this.ffmpegPath, args);
|
|
37662
37862
|
this.process = child;
|
|
37663
37863
|
child.stdin?.on("error", () => {});
|
|
37664
37864
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -37791,7 +37991,14 @@ var SUPPORTED_CODECS = new Set([
|
|
|
37791
37991
|
"hevc",
|
|
37792
37992
|
"mjpeg"
|
|
37793
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
|
+
*/
|
|
37794
38000
|
var FfmpegDecoderProvider = class {
|
|
38001
|
+
ffmpegPath;
|
|
37795
38002
|
id = "ffmpeg";
|
|
37796
38003
|
name = "FFmpeg Decoder";
|
|
37797
38004
|
/** Software decoder — used as fallback when hardware decoders are unavailable. */
|
|
@@ -37799,8 +38006,11 @@ var FfmpegDecoderProvider = class {
|
|
|
37799
38006
|
async supportsCodec(input) {
|
|
37800
38007
|
return SUPPORTED_CODECS.has(input.codec);
|
|
37801
38008
|
}
|
|
38009
|
+
constructor(ffmpegPath) {
|
|
38010
|
+
this.ffmpegPath = ffmpegPath;
|
|
38011
|
+
}
|
|
37802
38012
|
async createSession(config) {
|
|
37803
|
-
return new FfmpegDecoderSession(config);
|
|
38013
|
+
return new FfmpegDecoderSession(config, this.ffmpegPath);
|
|
37804
38014
|
}
|
|
37805
38015
|
};
|
|
37806
38016
|
//#endregion
|