@camstack/addon-pipeline 1.2.307 → 1.2.309
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/{default-detection-model-DgdiQtQb.js → default-detection-model-BZMS04Wn.js} +1 -1
- package/dist/{default-detection-model-BV9bIuOo.mjs → default-detection-model-ClQ1gcZ9.mjs} +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-DdVMFlJ6.mjs → dist-B3_sbBDM.mjs} +305 -48
- package/dist/{dist-CyJWk-_f.js → dist-BLf0YjAa.js} +310 -47
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-COu_tHy4.mjs → node-DJFKTJ0B.mjs} +1 -53
- package/dist/{node-HsWG5fGp.js → node-Vwf2PuIT.js} +1 -53
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-Bz3oIMzl.mjs → process-memory-BvwauBlX.mjs} +1 -1
- package/dist/{process-memory-TQXG7qrJ.js → process-memory-DGTVVYlX.js} +1 -1
- package/dist/recorder/index.js +147 -22
- package/dist/recorder/index.mjs +147 -22
- package/dist/{segment-demux-js-BrKa7tpr.js → segment-demux-js-BUAAuSVF.js} +22 -4
- package/dist/{segment-demux-js-CEJn4BR0.mjs → segment-demux-js-C0oz7IVm.mjs} +17 -5
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DdYUVQSA.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-7H0Oxf23.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C6EaWavG.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BhO-szf8.mjs +26 -0
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-D--a9rVM.mjs → hostInit-DEY-Dqqo.mjs} +2 -2
- package/dist/stream-broker/index.js +434 -111
- package/dist/stream-broker/index.mjs +434 -111
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DPHv0v3Q.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CdKbuxqG.mjs +0 -26
|
@@ -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-B3_sbBDM.mjs";
|
|
3
|
+
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-DJFKTJ0B.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-C0oz7IVm.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;
|
|
@@ -16954,6 +17079,21 @@ var DeviceOverridesMapSchema = record(string(), DeviceOverrideSchema);
|
|
|
16954
17079
|
* prune driven by a map that shrinks on every unregister.
|
|
16955
17080
|
*/
|
|
16956
17081
|
var RtspEnabledSchema = record(string(), boolean());
|
|
17082
|
+
/**
|
|
17083
|
+
* How far a {@link StreamBrokerManager.retractDevice} goes.
|
|
17084
|
+
*
|
|
17085
|
+
* The two flags answer different questions and a permanent removal sets BOTH:
|
|
17086
|
+
* `preserveAssignments` is about the per-stream teardown (skip the
|
|
17087
|
+
* re-ranking churn as each stream disappears), `purgeAssignments` is about the
|
|
17088
|
+
* device's profile-slot ENTRY (drop it outright, and persist that).
|
|
17089
|
+
*/
|
|
17090
|
+
/**
|
|
17091
|
+
* The departed-device sweep's second read must be at least this long after
|
|
17092
|
+
* the first mark (D685): one default catalog-poll interval. Two reads closer
|
|
17093
|
+
* than that — the startup pass's 1.5 s re-runs, two overlapping polls — are
|
|
17094
|
+
* one observation.
|
|
17095
|
+
*/
|
|
17096
|
+
var DEPARTURE_CONFIRM_MIN_MS = 3e4;
|
|
16957
17097
|
var PUSH_KINDS = new Set(["push-annexb"]);
|
|
16958
17098
|
/** Walk a profile map and return the slot that points at `camStreamId`, or null. */
|
|
16959
17099
|
function findProfileForStream(map, camStreamId) {
|
|
@@ -17153,6 +17293,13 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17153
17293
|
* outage that put it here.
|
|
17154
17294
|
*/
|
|
17155
17295
|
catalogPullFailures = /* @__PURE__ */ new Map();
|
|
17296
|
+
/**
|
|
17297
|
+
* Devices with registered streams that a successful fleet read did not
|
|
17298
|
+
* list, and WHEN that read was (D685). A mark, not a verdict: the sweep
|
|
17299
|
+
* retracts only when a read at least {@link DEPARTURE_CONFIRM_MIN_MS} later
|
|
17300
|
+
* agrees, and a reappearance clears it (D49).
|
|
17301
|
+
*/
|
|
17302
|
+
departureMarks = /* @__PURE__ */ new Map();
|
|
17156
17303
|
eventBus = null;
|
|
17157
17304
|
/**
|
|
17158
17305
|
* Per-broker stream health flag set by the watchdog. `undefined`
|
|
@@ -17373,12 +17520,14 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17373
17520
|
/**
|
|
17374
17521
|
* Snapshot of the system `ffmpeg` config section (binaryPath / hwAccel /
|
|
17375
17522
|
* threadCount). Refreshed by the host addon via `setFfmpegConfig`; the
|
|
17376
|
-
* transcode manager reads it live per spawn.
|
|
17377
|
-
*
|
|
17378
|
-
*
|
|
17523
|
+
* transcode manager reads it live per spawn. Until the addon resolves the
|
|
17524
|
+
* node's pinned binary there is NONE (`binaryPath: null`) and every child
|
|
17525
|
+
* refuses by name — never a PATH `ffmpeg` (D540). `'copy'` software decode
|
|
17526
|
+
* and auto threads stay the defaults: software decode never hard-fails on a
|
|
17527
|
+
* fragile HEVC source.
|
|
17379
17528
|
*/
|
|
17380
17529
|
ffmpegConfig = {
|
|
17381
|
-
binaryPath:
|
|
17530
|
+
binaryPath: null,
|
|
17382
17531
|
hwAccel: "copy",
|
|
17383
17532
|
threadCount: 0
|
|
17384
17533
|
};
|
|
@@ -17414,6 +17563,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17414
17563
|
},
|
|
17415
17564
|
resolveProfileSource: (deviceId, profile) => this.assignments.get(deviceId)?.map[profile] ?? null,
|
|
17416
17565
|
ffmpegConfig: () => this.ffmpegConfig,
|
|
17566
|
+
onFfmpegMissing: () => this.reportFfmpegMissing(),
|
|
17417
17567
|
resolveStreamHwAccel: (deviceId, camStreamId) => this.getStreamHwAccelOverride(deviceId, camStreamId),
|
|
17418
17568
|
sentryOffered: async (deviceId) => sentryEligible({
|
|
17419
17569
|
batteryOperated: await this.isBatteryOperated(deviceId),
|
|
@@ -17476,6 +17626,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17476
17626
|
});
|
|
17477
17627
|
},
|
|
17478
17628
|
ffmpegConfig: () => this.ffmpegConfig,
|
|
17629
|
+
onFfmpegMissing: () => this.reportFfmpegMissing(),
|
|
17479
17630
|
...this.costRegistry === null ? {} : { costRegistry: this.costRegistry },
|
|
17480
17631
|
resolveDecodeHwAccel: (deviceId, override) => this.resolveEgressHwAccel(deviceId, override),
|
|
17481
17632
|
acquireChannel: (brokerId) => {
|
|
@@ -17851,11 +18002,35 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17851
18002
|
if (localNodeId !== void 0) this.localNodeId = localNodeId;
|
|
17852
18003
|
if (hwaccel !== void 0) this.hwaccel = hwaccel;
|
|
17853
18004
|
}
|
|
17854
|
-
/**
|
|
18005
|
+
/**
|
|
18006
|
+
* Update the node's ffmpeg config snapshot. Every broker READS it per dial
|
|
18007
|
+
* ({@link bindBrokerFfmpeg}), so a re-resolved binary reaches live brokers —
|
|
18008
|
+
* including one created while the re-resolve was in flight — with no copy to
|
|
18009
|
+
* fan out (D540).
|
|
18010
|
+
*/
|
|
17855
18011
|
setFfmpegConfig(cfg) {
|
|
17856
18012
|
this.ffmpegConfig = cfg;
|
|
17857
18013
|
}
|
|
17858
18014
|
/**
|
|
18015
|
+
* Point a broker at the node's LIVE pinned ffmpeg and at the re-resolve
|
|
18016
|
+
* request, so its ffmpeg-parser ingress spawns the current binary and a
|
|
18017
|
+
* refusal asks for it again.
|
|
18018
|
+
*/
|
|
18019
|
+
bindBrokerFfmpeg(broker) {
|
|
18020
|
+
broker.setFfmpegBinaryPathSource(() => this.ffmpegConfig.binaryPath);
|
|
18021
|
+
broker.setFfmpegMissingHandler(() => this.reportFfmpegMissing());
|
|
18022
|
+
}
|
|
18023
|
+
/** Who asks for the pinned binary again after a refusal. Set by the addon. */
|
|
18024
|
+
ffmpegMissingHandler = null;
|
|
18025
|
+
/** Wire the host's re-resolve (`FfmpegReresolver.request`). */
|
|
18026
|
+
setFfmpegMissingHandler(handler) {
|
|
18027
|
+
this.ffmpegMissingHandler = handler;
|
|
18028
|
+
}
|
|
18029
|
+
/** A child was refused for want of the pinned ffmpeg: ask the host again. */
|
|
18030
|
+
reportFfmpegMissing() {
|
|
18031
|
+
this.ffmpegMissingHandler?.();
|
|
18032
|
+
}
|
|
18033
|
+
/**
|
|
17859
18034
|
* The decode `hwaccel` options surfaceable in a (future) UI: the always-valid
|
|
17860
18035
|
* settings (`copy`, `auto`, `none`) plus the concrete hardware backends THIS
|
|
17861
18036
|
* node can use, resolved from the LOCAL `ctx.kernel.hwaccel` (os.platform +
|
|
@@ -18196,7 +18371,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18196
18371
|
let annexB;
|
|
18197
18372
|
try {
|
|
18198
18373
|
annexB = await encodePrestreamIdr({
|
|
18199
|
-
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
18374
|
+
ffmpegPath: requireBrokerFfmpeg(this.ffmpegConfig.binaryPath, {
|
|
18375
|
+
logger,
|
|
18376
|
+
deviceId,
|
|
18377
|
+
purpose: "sentry pre-stream encode",
|
|
18378
|
+
onMissing: () => this.reportFfmpegMissing()
|
|
18379
|
+
}),
|
|
18200
18380
|
plan,
|
|
18201
18381
|
jpeg: picture.jpeg,
|
|
18202
18382
|
logger,
|
|
@@ -18987,6 +19167,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18987
19167
|
this.logger.debug("reconcileAllCatalogs: deviceManager.listAll failed — will retry", { meta: { error: errMsg(err) } });
|
|
18988
19168
|
return;
|
|
18989
19169
|
}
|
|
19170
|
+
await this.sweepDepartedDevices(devices);
|
|
18990
19171
|
const cameras = devices.filter((d) => d.isCamera || d.type === DeviceType.Camera);
|
|
18991
19172
|
for (const d of cameras) if (d.disabled) {
|
|
18992
19173
|
const firstSeen = !this.disabledDevices.has(d.id);
|
|
@@ -19002,6 +19183,71 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19002
19183
|
for (const deviceId of [...this.catalogPullFailures.keys()]) if (!liveIds.has(deviceId)) this.catalogPullFailures.delete(deviceId);
|
|
19003
19184
|
}
|
|
19004
19185
|
/**
|
|
19186
|
+
* Retract and purge a device that left the fleet WITHOUT a
|
|
19187
|
+
* `DeviceUnregistered` (D685).
|
|
19188
|
+
*
|
|
19189
|
+
* The event is the fast path and it may be dropped (D8/D11). Since D685 no
|
|
19190
|
+
* provider retracts on teardown (that hook also runs on every runner
|
|
19191
|
+
* restart), so a lost event used to leave the device's cam streams
|
|
19192
|
+
* registered — and `high` is default-warm, so its prebuffer hold kept
|
|
19193
|
+
* dialling the camera with no consumer at all, until the broker restarted.
|
|
19194
|
+
*
|
|
19195
|
+
* Destroying on a fallible read, so every way the read can be WRONG decides
|
|
19196
|
+
* nothing (D49): the caller has already returned on a failed read, an EMPTY
|
|
19197
|
+
* listing is device-restore not having run, and one absence only MARKS the
|
|
19198
|
+
* device — the retract needs the next successful read to agree. A device
|
|
19199
|
+
* that reappears in between is unmarked.
|
|
19200
|
+
*/
|
|
19201
|
+
async sweepDepartedDevices(devices) {
|
|
19202
|
+
if (devices.length === 0) {
|
|
19203
|
+
if (this.departureMarks.size > 0) this.logger.debug("departed-device sweep skipped — device enumeration came back empty", { meta: { marked: [...this.departureMarks.keys()] } });
|
|
19204
|
+
return;
|
|
19205
|
+
}
|
|
19206
|
+
const listed = new Set(devices.map((d) => d.id));
|
|
19207
|
+
for (const deviceId of [...this.departureMarks.keys()]) {
|
|
19208
|
+
if (!this.cameraStreams.has(deviceId)) {
|
|
19209
|
+
this.departureMarks.delete(deviceId);
|
|
19210
|
+
this.logger.debug("departure mark dropped — the device has no streams any more", { tags: { deviceId } });
|
|
19211
|
+
continue;
|
|
19212
|
+
}
|
|
19213
|
+
if (listed.has(deviceId)) {
|
|
19214
|
+
this.departureMarks.delete(deviceId);
|
|
19215
|
+
this.logger.info("device is back in the fleet listing — departure sweep cancelled", { tags: { deviceId } });
|
|
19216
|
+
}
|
|
19217
|
+
}
|
|
19218
|
+
const nowMs = Date.now();
|
|
19219
|
+
for (const deviceId of [...this.cameraStreams.keys()]) {
|
|
19220
|
+
if (listed.has(deviceId)) continue;
|
|
19221
|
+
const markedAt = this.departureMarks.get(deviceId);
|
|
19222
|
+
if (markedAt === void 0) {
|
|
19223
|
+
this.departureMarks.set(deviceId, nowMs);
|
|
19224
|
+
this.logger.info("device absent from the fleet listing — its streams are swept if a later read agrees", {
|
|
19225
|
+
tags: { deviceId },
|
|
19226
|
+
meta: { confirmAfterMs: DEPARTURE_CONFIRM_MIN_MS }
|
|
19227
|
+
});
|
|
19228
|
+
continue;
|
|
19229
|
+
}
|
|
19230
|
+
if (nowMs - markedAt < DEPARTURE_CONFIRM_MIN_MS) continue;
|
|
19231
|
+
this.departureMarks.delete(deviceId);
|
|
19232
|
+
this.logger.warn("device left the fleet without DeviceUnregistered — swept", {
|
|
19233
|
+
tags: { deviceId },
|
|
19234
|
+
meta: {
|
|
19235
|
+
camStreamIds: [...this.cameraStreams.get(deviceId)?.keys() ?? []],
|
|
19236
|
+
absentForMs: nowMs - markedAt
|
|
19237
|
+
}
|
|
19238
|
+
});
|
|
19239
|
+
await this.retractDevice(deviceId, {
|
|
19240
|
+
preserveAssignments: true,
|
|
19241
|
+
purgeAssignments: true
|
|
19242
|
+
}).catch((err) => {
|
|
19243
|
+
this.logger.warn("departed-device sweep: retract failed — the next two polls try again", {
|
|
19244
|
+
tags: { deviceId },
|
|
19245
|
+
meta: { error: errMsg(err) }
|
|
19246
|
+
});
|
|
19247
|
+
});
|
|
19248
|
+
}
|
|
19249
|
+
}
|
|
19250
|
+
/**
|
|
19005
19251
|
* ONE device's audio-plane reconcile pass. See `audio-plane-reconcile.ts`
|
|
19006
19252
|
* for the decision rules; this half only gathers and acts.
|
|
19007
19253
|
*
|
|
@@ -19448,7 +19694,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19448
19694
|
height: scaledHeight(cut, clipWidth)
|
|
19449
19695
|
});
|
|
19450
19696
|
const gifSpeed = input.gifSpeed ?? 4;
|
|
19451
|
-
|
|
19697
|
+
const gifFfmpeg = this.ffmpegConfig.binaryPath;
|
|
19698
|
+
if (input.kinds.includes("gif") && gifFfmpeg === null) {
|
|
19699
|
+
this.logger?.error("event media gif refused — no pinned ffmpeg on this node (D540)", { tags: { deviceId: input.deviceId } });
|
|
19700
|
+
this.reportFfmpegMissing();
|
|
19701
|
+
}
|
|
19702
|
+
if (input.kinds.includes("gif") && gifFfmpeg !== null) {
|
|
19452
19703
|
const gif = await gifFromMp4({
|
|
19453
19704
|
mp4,
|
|
19454
19705
|
deviceId: input.deviceId,
|
|
@@ -19457,7 +19708,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19457
19708
|
speed: gifDeriveSpeed(gifSpeed, input.speed),
|
|
19458
19709
|
...this.logger !== void 0 ? { logger: this.logger } : {},
|
|
19459
19710
|
spawnFn: spawn,
|
|
19460
|
-
|
|
19711
|
+
ffmpegPath: gifFfmpeg
|
|
19461
19712
|
});
|
|
19462
19713
|
if (gif !== null) {
|
|
19463
19714
|
const height = scaledHeight(cut, input.gifMaxWidth);
|
|
@@ -19626,7 +19877,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19626
19877
|
"pipe",
|
|
19627
19878
|
"pipe"
|
|
19628
19879
|
] }),
|
|
19629
|
-
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
19880
|
+
ffmpegPath: requireBrokerFfmpeg(this.ffmpegConfig.binaryPath, {
|
|
19881
|
+
logger: this.logger,
|
|
19882
|
+
deviceId: cut.deviceId,
|
|
19883
|
+
purpose: "pre-buffer clip render",
|
|
19884
|
+
onMissing: () => this.reportFfmpegMissing()
|
|
19885
|
+
}),
|
|
19630
19886
|
sdpParameterSets: cut.broker.getSdpParameterSets()
|
|
19631
19887
|
});
|
|
19632
19888
|
this.logger?.info("pre-buffer clip rendered", {
|
|
@@ -21704,7 +21960,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21704
21960
|
const source = this.buildSourceFromCamStream(cam);
|
|
21705
21961
|
const broker = new StreamBroker(brokerId, this.buildDecoderCapApi(deviceId), brokerLogger, buildAudioCodecCapApi(this.api), this.localNodeId);
|
|
21706
21962
|
broker.setRestreamUrlResolver(() => this.rtspProvider.getEntry(brokerId)?.mutedUrl ?? null);
|
|
21707
|
-
|
|
21963
|
+
this.bindBrokerFfmpeg(broker);
|
|
21708
21964
|
if (this.costRegistry !== null) broker.setCostRegistry(this.costRegistry);
|
|
21709
21965
|
this.applyPrebufferConfig(broker, deviceId, camStreamId);
|
|
21710
21966
|
this.applyAudioMuteConfig(broker, deviceId);
|
|
@@ -21882,7 +22138,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21882
22138
|
}
|
|
21883
22139
|
});
|
|
21884
22140
|
const feeder = new DerivedStreamFeeder({
|
|
21885
|
-
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
22141
|
+
ffmpegPath: requireBrokerFfmpeg(this.ffmpegConfig.binaryPath, {
|
|
22142
|
+
logger: this.logger,
|
|
22143
|
+
deviceId,
|
|
22144
|
+
purpose: "derived transcode",
|
|
22145
|
+
onMissing: () => this.reportFfmpegMissing()
|
|
22146
|
+
}),
|
|
21886
22147
|
args,
|
|
21887
22148
|
logger: this.logger.child(`derived-ffmpeg:${camStreamId}`).withTags({ deviceId })
|
|
21888
22149
|
}, {
|
|
@@ -33830,7 +34091,7 @@ var offThreadJsParse = (bytes) => {
|
|
|
33830
34091
|
* Drop-in for the session `demuxVideo` hook: JS parse off the live loop,
|
|
33831
34092
|
* ffmpeg subprocess fallback unchanged.
|
|
33832
34093
|
*/
|
|
33833
|
-
async function demuxSegmentPreferJsOffThread(bytes, opts
|
|
34094
|
+
async function demuxSegmentPreferJsOffThread(bytes, opts) {
|
|
33834
34095
|
return demuxSegmentPreferJs(bytes, {
|
|
33835
34096
|
...opts,
|
|
33836
34097
|
jsParse: offThreadJsParse
|
|
@@ -34015,9 +34276,9 @@ function sliceMuLawToPcmuFrames(mulaw) {
|
|
|
34015
34276
|
* can't decode) — ffmpeg exits non-zero, which is tolerated as empty output.
|
|
34016
34277
|
*
|
|
34017
34278
|
* @param bytes - The full `.m4s` fragment.
|
|
34018
|
-
* @param ffmpegPath -
|
|
34279
|
+
* @param ffmpegPath - The node's pinned ffmpeg (D540). Required: no PATH default.
|
|
34019
34280
|
*/
|
|
34020
|
-
async function demuxSegmentToPcmu(bytes, ffmpegPath
|
|
34281
|
+
async function demuxSegmentToPcmu(bytes, ffmpegPath) {
|
|
34021
34282
|
const mulaw = await runFfmpegOnce({
|
|
34022
34283
|
ffmpegPath,
|
|
34023
34284
|
args: SEGMENT_AUDIO_FFMPEG_ARGS,
|
|
@@ -34149,15 +34410,19 @@ var SEGMENT_AUDIO_OPUS_FFMPEG_ARGS = [
|
|
|
34149
34410
|
* best-effort and must never block recorded video. Mirrors the video-side
|
|
34150
34411
|
* `demuxSegmentPreferJs`.
|
|
34151
34412
|
*/
|
|
34152
|
-
async function demuxSegmentAudioOpusPreferJs(bytes, opts
|
|
34413
|
+
async function demuxSegmentAudioOpusPreferJs(bytes, opts) {
|
|
34153
34414
|
try {
|
|
34154
34415
|
const direct = await demuxSegmentAudioOpusJs(bytes);
|
|
34155
34416
|
if (direct) return direct;
|
|
34156
34417
|
} catch {}
|
|
34418
|
+
if (opts.ffmpegPath === null) {
|
|
34419
|
+
opts.onFfmpegFallback?.("audio track is not Opus (legacy fragment) and the ffmpeg fallback is refused — no pinned ffmpeg on this node (D540)");
|
|
34420
|
+
return [];
|
|
34421
|
+
}
|
|
34157
34422
|
opts.onFfmpegFallback?.("audio track is not Opus (legacy fragment)");
|
|
34158
34423
|
try {
|
|
34159
34424
|
const opusFmp4 = await runFfmpegOnce({
|
|
34160
|
-
ffmpegPath: opts.ffmpegPath
|
|
34425
|
+
ffmpegPath: opts.ffmpegPath,
|
|
34161
34426
|
args: SEGMENT_AUDIO_OPUS_FFMPEG_ARGS,
|
|
34162
34427
|
input: bytes,
|
|
34163
34428
|
tolerateNonZeroExit: true
|
|
@@ -35219,12 +35484,25 @@ var BrokerWebrtcServer = class {
|
|
|
35219
35484
|
session.onClosed = () => this.cleanupSession(sessionId);
|
|
35220
35485
|
const locateRecorded = this.locateRecordedSegment;
|
|
35221
35486
|
const readRecorded = this.readRecordedSegmentBytes;
|
|
35222
|
-
const
|
|
35487
|
+
const resolveFfmpegPath = this.resolveFfmpegPath;
|
|
35488
|
+
const resolveFfmpeg = () => resolveFfmpegPath?.() ?? null;
|
|
35489
|
+
let noPinnedFfmpegLogged = false;
|
|
35490
|
+
const reportNoPinnedFfmpeg = (what) => {
|
|
35491
|
+
if (noPinnedFfmpegLogged) return;
|
|
35492
|
+
noPinnedFfmpegLogged = true;
|
|
35493
|
+
sessionLogger.error("recorded playback: ffmpeg fallback refused — no pinned ffmpeg on this node (D540)", {
|
|
35494
|
+
tags: { deviceId },
|
|
35495
|
+
meta: {
|
|
35496
|
+
sessionId,
|
|
35497
|
+
what
|
|
35498
|
+
}
|
|
35499
|
+
});
|
|
35500
|
+
};
|
|
35223
35501
|
const demuxTally = new DemuxPathTally();
|
|
35224
35502
|
let audioOpusFallbackLogged = false;
|
|
35225
35503
|
if (locateRecorded && readRecorded) {
|
|
35226
35504
|
const demuxVideo = (b) => demuxSegmentPreferJsOffThread(b, {
|
|
35227
|
-
ffmpegPath: resolveFfmpeg
|
|
35505
|
+
ffmpegPath: resolveFfmpeg(),
|
|
35228
35506
|
onDemuxPath: (outcome) => {
|
|
35229
35507
|
const report = demuxTally.record(outcome, Date.now());
|
|
35230
35508
|
if (!report) return;
|
|
@@ -35239,6 +35517,9 @@ var BrokerWebrtcServer = class {
|
|
|
35239
35517
|
}
|
|
35240
35518
|
});
|
|
35241
35519
|
}
|
|
35520
|
+
}).catch((err) => {
|
|
35521
|
+
if (err instanceof FfmpegFallbackRefusedError) reportNoPinnedFfmpeg("video");
|
|
35522
|
+
throw err;
|
|
35242
35523
|
});
|
|
35243
35524
|
const scrubCache = new ScrubFragmentCache();
|
|
35244
35525
|
const initialLiveProfile = this.profileTierResolver?.(brokerId) ?? null;
|
|
@@ -35354,9 +35635,11 @@ var BrokerWebrtcServer = class {
|
|
|
35354
35635
|
pushAudioPcmu: (pcmu, ts) => session.writeRecordedAudio(Buffer.from(pcmu), ts)
|
|
35355
35636
|
};
|
|
35356
35637
|
const demuxAudio = opusSession ? async (b) => {
|
|
35638
|
+
const ffmpegPath = resolveFfmpeg();
|
|
35357
35639
|
return (await demuxSegmentAudioOpusPreferJs(b, {
|
|
35358
|
-
ffmpegPath
|
|
35640
|
+
ffmpegPath,
|
|
35359
35641
|
onFfmpegFallback: (reason) => {
|
|
35642
|
+
if (ffmpegPath === null) reportNoPinnedFfmpeg("audio");
|
|
35360
35643
|
if (audioOpusFallbackLogged) return;
|
|
35361
35644
|
audioOpusFallbackLogged = true;
|
|
35362
35645
|
sessionLogger.warn("recorded audio: JS Opus demux missed — ffmpeg AAC→Opus fallback (once/session)", { meta: {
|
|
@@ -35370,7 +35653,14 @@ var BrokerWebrtcServer = class {
|
|
|
35370
35653
|
data: au.data,
|
|
35371
35654
|
ptsMs: au.ptsMs
|
|
35372
35655
|
}));
|
|
35373
|
-
} : (b) =>
|
|
35656
|
+
} : async (b) => {
|
|
35657
|
+
const ffmpegPath = resolveFfmpeg();
|
|
35658
|
+
if (ffmpegPath === null) {
|
|
35659
|
+
reportNoPinnedFfmpeg("audio");
|
|
35660
|
+
return [];
|
|
35661
|
+
}
|
|
35662
|
+
return await demuxSegmentToPcmu(b, ffmpegPath);
|
|
35663
|
+
};
|
|
35374
35664
|
const clipSource = clip && readClip ? createClipFileSource({
|
|
35375
35665
|
handle: clip,
|
|
35376
35666
|
read: readClip,
|
|
@@ -36739,6 +37029,39 @@ var EventLoopStallMonitor = class {
|
|
|
36739
37029
|
}
|
|
36740
37030
|
};
|
|
36741
37031
|
//#endregion
|
|
37032
|
+
//#region src/stream-broker/device-removal.ts
|
|
37033
|
+
/**
|
|
37034
|
+
* The broker's half of an operator's device DELETE.
|
|
37035
|
+
*
|
|
37036
|
+
* Since D685 this subscription is the fast path of a removal: providers no
|
|
37037
|
+
* longer retract their streams from `removeDevice()`, because that hook also
|
|
37038
|
+
* runs on every runner restart. The backstop for a dropped event is the
|
|
37039
|
+
* catalog poll's departed-device sweep (`StreamBrokerManager`,
|
|
37040
|
+
* `sweepDepartedDevices`). Extracted from `addon.ts` so the registration the
|
|
37041
|
+
* addon performs is the one a test drives.
|
|
37042
|
+
*/
|
|
37043
|
+
/** A PERMANENT removal: retract every stream AND purge the slot assignments. */
|
|
37044
|
+
async function retractRemovedDevice(deviceId, deps) {
|
|
37045
|
+
try {
|
|
37046
|
+
await deps.retractDevice(deviceId, {
|
|
37047
|
+
preserveAssignments: true,
|
|
37048
|
+
purgeAssignments: true
|
|
37049
|
+
});
|
|
37050
|
+
} catch (err) {
|
|
37051
|
+
deps.logger.warn("device retract failed", {
|
|
37052
|
+
tags: { deviceId },
|
|
37053
|
+
meta: { error: errMsg(err) }
|
|
37054
|
+
});
|
|
37055
|
+
}
|
|
37056
|
+
}
|
|
37057
|
+
/** Retract a device the moment the device-manager says it was deleted. */
|
|
37058
|
+
function subscribeDeviceRemoval(subscribe, deps) {
|
|
37059
|
+
subscribe({ category: EventCategory.DeviceUnregistered }, (event) => {
|
|
37060
|
+
const deviceId = event.data["deviceId"];
|
|
37061
|
+
if (typeof deviceId === "number") retractRemovedDevice(deviceId, deps());
|
|
37062
|
+
});
|
|
37063
|
+
}
|
|
37064
|
+
//#endregion
|
|
36742
37065
|
//#region src/stream-broker/addon.ts
|
|
36743
37066
|
/**
|
|
36744
37067
|
* Hydrate the persisted RTSP restream state. Tokens come from their own
|
|
@@ -36820,10 +37143,21 @@ var PROBE_SNAPSHOTS_PERSIST_INTERVAL_MS = 6e4;
|
|
|
36820
37143
|
var NETWORK_QUALITY_STATS_TIMEOUT_MS = 2e3;
|
|
36821
37144
|
var StreamBrokerAddon = class extends BaseAddon {
|
|
36822
37145
|
brokerManager = null;
|
|
36823
|
-
/**
|
|
36824
|
-
*
|
|
36825
|
-
*
|
|
36826
|
-
|
|
37146
|
+
/** The node's pinned ffmpeg, mirrored here so the recorded-playback demuxers
|
|
37147
|
+
* (segment-demux/segment-audio) spawn the same binary as the transcode
|
|
37148
|
+
* egress. Kept in sync by the ffmpeg wiring. `null` = none could be resolved:
|
|
37149
|
+
* their ffmpeg fallbacks refuse by name — never a PATH `ffmpeg` (D540). */
|
|
37150
|
+
ffmpegBinaryPath = null;
|
|
37151
|
+
/** The node's pinned build, asked once on success (D540). A failure is not
|
|
37152
|
+
* remembered, so the re-resolve below can succeed later. */
|
|
37153
|
+
pinnedFfmpeg = memoizePinnedFfmpeg(() => this.ctx.deps.ensureFfmpeg());
|
|
37154
|
+
/**
|
|
37155
|
+
* The way back from a failed boot-time resolution: a refused child asks, and
|
|
37156
|
+
* ONE bounded re-resolve hands the binary to the manager config (which every
|
|
37157
|
+
* live broker reads per dial) and the demuxers. Without it one failed
|
|
37158
|
+
* download disabled every broker ffmpeg child until the runner restarted.
|
|
37159
|
+
*/
|
|
37160
|
+
ffmpegWiring = null;
|
|
36827
37161
|
/** Once-per-addon log guard for the readGopBytes → whole-segment downgrade. */
|
|
36828
37162
|
gopReadFallbackLogged = false;
|
|
36829
37163
|
metricsSnapshotTimer = null;
|
|
@@ -36884,36 +37218,7 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
36884
37218
|
async resolveFfmpegBinaryPath() {
|
|
36885
37219
|
const override = this.config.binaryPath;
|
|
36886
37220
|
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
|
-
}
|
|
37221
|
+
return await this.pinnedFfmpeg();
|
|
36917
37222
|
}
|
|
36918
37223
|
async onInitialize() {
|
|
36919
37224
|
this.ctx.logger.info("log channels declared", { meta: { channels: STREAM_BROKER_LOG_CHANNELS.map((c) => c.descriptor.name) } });
|
|
@@ -36943,7 +37248,16 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
36943
37248
|
};
|
|
36944
37249
|
this.brokerManager.setDeviceSignalReader(async (deviceId) => (await readSignals(deviceId)).map((s) => s.code));
|
|
36945
37250
|
this.brokerManager.setDeviceSignalDetailReader(readSignals);
|
|
36946
|
-
|
|
37251
|
+
this.ffmpegWiring = wireBrokerFfmpeg({
|
|
37252
|
+
host: this.brokerManager,
|
|
37253
|
+
resolve: () => this.resolveFfmpegBinaryPath(),
|
|
37254
|
+
hwAccel: () => this.config.hwaccel,
|
|
37255
|
+
onBinaryPath: (binaryPath) => {
|
|
37256
|
+
this.ffmpegBinaryPath = binaryPath;
|
|
37257
|
+
},
|
|
37258
|
+
logger: this.ctx.logger
|
|
37259
|
+
});
|
|
37260
|
+
await this.ffmpegWiring.load();
|
|
36947
37261
|
await this.declareRowCollections();
|
|
36948
37262
|
await this.purgeFlattenedSettingsKeys();
|
|
36949
37263
|
const rtspProvider = this.brokerManager.getRtspRestreamProvider();
|
|
@@ -37112,10 +37426,7 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
37112
37426
|
const deviceId = event.data.deviceId;
|
|
37113
37427
|
if (typeof deviceId === "number") reconcileDevice(deviceId, "device-registered");
|
|
37114
37428
|
});
|
|
37115
|
-
this.subscribe(
|
|
37116
|
-
const deviceId = event.data.deviceId;
|
|
37117
|
-
if (typeof deviceId === "number") this.handleDeviceRemoved(deviceId);
|
|
37118
|
-
});
|
|
37429
|
+
subscribeDeviceRemoval((filter, handler) => this.subscribe(filter, handler), () => this.deviceRemovalDeps());
|
|
37119
37430
|
this.subscribe({ category: EventCategory.StreamParamsChanged }, (event) => {
|
|
37120
37431
|
const deviceId = event.data.deviceId;
|
|
37121
37432
|
if (typeof deviceId !== "number") return;
|
|
@@ -37228,7 +37539,10 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
37228
37539
|
};
|
|
37229
37540
|
}
|
|
37230
37541
|
},
|
|
37231
|
-
resolveFfmpegPath: () =>
|
|
37542
|
+
resolveFfmpegPath: () => {
|
|
37543
|
+
if (this.ffmpegBinaryPath === null) this.ffmpegWiring?.request("a recorded-playback demux needed ffmpeg");
|
|
37544
|
+
return this.ffmpegBinaryPath;
|
|
37545
|
+
}
|
|
37232
37546
|
});
|
|
37233
37547
|
this.brokerManager.setWebrtcServer(webrtcServer);
|
|
37234
37548
|
this.webrtcServer = webrtcServer;
|
|
@@ -37369,18 +37683,13 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
37369
37683
|
* persisted `profileMap` blob, so it returns at every boot. Live ids
|
|
37370
37684
|
* 3217/3218 outlived their cameras exactly that way.
|
|
37371
37685
|
*/
|
|
37372
|
-
|
|
37373
|
-
|
|
37374
|
-
|
|
37375
|
-
|
|
37376
|
-
|
|
37377
|
-
|
|
37378
|
-
}
|
|
37379
|
-
this.ctx.logger.warn("device retract failed", {
|
|
37380
|
-
tags: { deviceId },
|
|
37381
|
-
meta: { error: errMsg(err) }
|
|
37382
|
-
});
|
|
37383
|
-
}
|
|
37686
|
+
deviceRemovalDeps() {
|
|
37687
|
+
return {
|
|
37688
|
+
retractDevice: async (deviceId, opts) => {
|
|
37689
|
+
await this.brokerManager?.retractDevice(deviceId, opts);
|
|
37690
|
+
},
|
|
37691
|
+
logger: this.ctx.logger
|
|
37692
|
+
};
|
|
37384
37693
|
}
|
|
37385
37694
|
/**
|
|
37386
37695
|
* Emit one `stream-broker.metrics-snapshot` event per active broker.
|
|
@@ -37638,6 +37947,8 @@ var FfmpegDecoderSession = class {
|
|
|
37638
37947
|
outputBuffer = Buffer.alloc(0);
|
|
37639
37948
|
destroyed = false;
|
|
37640
37949
|
logger;
|
|
37950
|
+
/** The pinned ffmpeg to spawn (D540). Required: there is no PATH default. */
|
|
37951
|
+
ffmpegPath;
|
|
37641
37952
|
cachedWidth = 0;
|
|
37642
37953
|
cachedHeight = 0;
|
|
37643
37954
|
inputPackets = 0;
|
|
@@ -37646,8 +37957,9 @@ var FfmpegDecoderSession = class {
|
|
|
37646
37957
|
totalDecodeTimeMs = 0;
|
|
37647
37958
|
decodeCount = 0;
|
|
37648
37959
|
startTime = Date.now();
|
|
37649
|
-
constructor(config, logger = noopLogger) {
|
|
37960
|
+
constructor(config, ffmpegPath, logger = noopLogger) {
|
|
37650
37961
|
this.config = { ...config };
|
|
37962
|
+
this.ffmpegPath = ffmpegPath;
|
|
37651
37963
|
this.logger = logger;
|
|
37652
37964
|
this.frameDropper = new FrameDropper(config.maxFps);
|
|
37653
37965
|
this.spawnFfmpeg();
|
|
@@ -37658,7 +37970,8 @@ var FfmpegDecoderSession = class {
|
|
|
37658
37970
|
this.outputBuffer = Buffer.alloc(0);
|
|
37659
37971
|
this.cachedWidth = 0;
|
|
37660
37972
|
this.cachedHeight = 0;
|
|
37661
|
-
const
|
|
37973
|
+
const args = buildFfmpegArgs(this.config);
|
|
37974
|
+
const child = spawn(this.ffmpegPath, args);
|
|
37662
37975
|
this.process = child;
|
|
37663
37976
|
child.stdin?.on("error", () => {});
|
|
37664
37977
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -37791,7 +38104,14 @@ var SUPPORTED_CODECS = new Set([
|
|
|
37791
38104
|
"hevc",
|
|
37792
38105
|
"mjpeg"
|
|
37793
38106
|
]);
|
|
38107
|
+
/**
|
|
38108
|
+
* A static decoder provider for stand-alone broker harnesses. Production
|
|
38109
|
+
* decodes through the `decoder` cap; this exists for the manual streaming
|
|
38110
|
+
* diagnostics. It is handed the pinned ffmpeg (D540) — there is no PATH
|
|
38111
|
+
* default for it to fall back to.
|
|
38112
|
+
*/
|
|
37794
38113
|
var FfmpegDecoderProvider = class {
|
|
38114
|
+
ffmpegPath;
|
|
37795
38115
|
id = "ffmpeg";
|
|
37796
38116
|
name = "FFmpeg Decoder";
|
|
37797
38117
|
/** Software decoder — used as fallback when hardware decoders are unavailable. */
|
|
@@ -37799,8 +38119,11 @@ var FfmpegDecoderProvider = class {
|
|
|
37799
38119
|
async supportsCodec(input) {
|
|
37800
38120
|
return SUPPORTED_CODECS.has(input.codec);
|
|
37801
38121
|
}
|
|
38122
|
+
constructor(ffmpegPath) {
|
|
38123
|
+
this.ffmpegPath = ffmpegPath;
|
|
38124
|
+
}
|
|
37802
38125
|
async createSession(config) {
|
|
37803
|
-
return new FfmpegDecoderSession(config);
|
|
38126
|
+
return new FfmpegDecoderSession(config, this.ffmpegPath);
|
|
37804
38127
|
}
|
|
37805
38128
|
};
|
|
37806
38129
|
//#endregion
|