@camstack/addon-pipeline 1.2.51 → 1.2.52
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 +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +2 -2
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-BsBEe_rX.js → dist-CzFJeiG1.js} +16098 -15935
- package/dist/{dist-Dkvk5ykl.mjs → dist-gB3o0cyM.mjs} +16098 -15935
- package/dist/{event-loop-stall-monitor-DqJXi0_B.js → event-loop-stall-monitor-DSPDQYjc.js} +1 -1
- package/dist/{event-loop-stall-monitor-BvtjRcal.mjs → event-loop-stall-monitor-PxIaVeyh.mjs} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +88 -18
- package/dist/pipeline-runner/index.mjs +88 -18
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/session-decode/decode-worker-child.js +1 -1
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CUZ0nS_r.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DAeBhR8B.mjs} +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Bl9zivOk.mjs +26 -0
- package/dist/stream-broker/{hostInit-B8O23a6l.mjs → hostInit-DniAXKLy.mjs} +1 -1
- package/dist/stream-broker/index.js +336 -311
- package/dist/stream-broker/index.mjs +336 -311
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-CWhkLkHx.js → worker-protocol-BQlELGzv.js} +1 -1
- package/dist/{worker-protocol-B0c3i2km.mjs → worker-protocol-BhhX7J4-.mjs} +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-kAtChdz2.js → MaskShapeCanvas-DI4BY7W2-B1gpA9sI.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DEwkvjzt.js → MotionZonesSettings-NcxxQN8r-DV6vvNDq.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-Brv9VL0h.js → PrivacyMaskSettings-APgPLF7p-BmUdRoZ9.js} +1 -1
- package/embed-dist/assets/{index-Dv7fSuDA.js → index-DsGzhGCW.js} +12 -12
- package/embed-dist/index.html +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-Mv3UeZUE.mjs +0 -26
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as __toESM, t as __commonJSMin } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { $ as webrtcSessionCapability, At as union, B as maskUrlCredentials, C as addonWidgetsSourceCapability, Ct as discriminatedUnion, D as createHwAccelCache, Dt as object, E as cameraStreamsCapability, Et as number, F as egressTransportFromRequest, J as resolveEgressDecodeHwAccel, Ot as record, P as egressTranscodeSharingKey, St as boolean, Tt as literal, Z as streamBrokerCapability, _t as parseProfileBrokerId, at as isSoftwareDecode, bt as _enum, ct as DeviceFeature, et as errMsg, f as EncodeProfileSchema, ft as isEvent, ht as nodePin, it as invocationFromEncodeProfile, jt as EventCategory, kt as string, lt as DeviceType, mt as makeSourceBrokerId, nt as Fmp4BoxSplitter, ot as BaseAddon, pt as makeProfileBrokerId, rt as buildFfmpegArgs$1, st as CAM_PROFILE_ORDER, tt as AUDIO_PRESETS, ut as createEvent, v as RATE_CONTROL_RELAXED, x as RingBuffer, xt as array, y as RATE_CONTROL_TIGHT } from "../dist-
|
|
2
|
+
import { $ as webrtcSessionCapability, At as union, B as maskUrlCredentials, C as addonWidgetsSourceCapability, Ct as discriminatedUnion, D as createHwAccelCache, Dt as object, E as cameraStreamsCapability, Et as number, F as egressTransportFromRequest, J as resolveEgressDecodeHwAccel, Ot as record, P as egressTranscodeSharingKey, St as boolean, Tt as literal, Z as streamBrokerCapability, _t as parseProfileBrokerId, at as isSoftwareDecode, bt as _enum, ct as DeviceFeature, et as errMsg, f as EncodeProfileSchema, ft as isEvent, ht as nodePin, it as invocationFromEncodeProfile, jt as EventCategory, kt as string, lt as DeviceType, mt as makeSourceBrokerId, nt as Fmp4BoxSplitter, ot as BaseAddon, pt as makeProfileBrokerId, rt as buildFfmpegArgs$1, st as CAM_PROFILE_ORDER, tt as AUDIO_PRESETS, ut as createEvent, v as RATE_CONTROL_RELAXED, x as RingBuffer, xt as array, y as RATE_CONTROL_TIGHT } from "../dist-gB3o0cyM.mjs";
|
|
3
3
|
import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
|
|
4
4
|
import { n as createFileDataPlaneHandler } from "../addon-utils-QuxtOdK1.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
@@ -1040,6 +1040,275 @@ function buildDerivedTranscodeArgs(profile, loopbackUrl, decodeHwAccel = null) {
|
|
|
1040
1040
|
}));
|
|
1041
1041
|
}
|
|
1042
1042
|
//#endregion
|
|
1043
|
+
//#region src/stream-broker/paced-rtp-replay.ts
|
|
1044
|
+
var PACED_REPLAY_DEFAULTS = {
|
|
1045
|
+
singleShotMax: 64,
|
|
1046
|
+
tickMs: 5,
|
|
1047
|
+
targetDurationMs: 300,
|
|
1048
|
+
minChunkSize: 32,
|
|
1049
|
+
maxChunkSize: 128
|
|
1050
|
+
};
|
|
1051
|
+
var defaultSchedule = (fn, ms) => {
|
|
1052
|
+
const t = setTimeout(fn, ms);
|
|
1053
|
+
return () => clearTimeout(t);
|
|
1054
|
+
};
|
|
1055
|
+
/**
|
|
1056
|
+
* Compute the per-tick chunk size for a burst of `total` packets: enough to
|
|
1057
|
+
* drain within `targetDurationMs` at one chunk per `tickMs`, clamped to
|
|
1058
|
+
* [minChunkSize, maxChunkSize]. Bursts ≤ `singleShotMax` are one chunk.
|
|
1059
|
+
*/
|
|
1060
|
+
function computeReplayChunkSize(total, tuning) {
|
|
1061
|
+
if (total <= tuning.singleShotMax) return Math.max(total, 1);
|
|
1062
|
+
const maxTicks = Math.max(1, Math.floor(tuning.targetDurationMs / tuning.tickMs));
|
|
1063
|
+
const ideal = Math.ceil(total / maxTicks);
|
|
1064
|
+
return Math.min(tuning.maxChunkSize, Math.max(tuning.minChunkSize, ideal));
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* One in-flight paced replay. Create per burst; not reusable after
|
|
1068
|
+
* completion or abort.
|
|
1069
|
+
*/
|
|
1070
|
+
var PacedRtpReplay = class {
|
|
1071
|
+
send;
|
|
1072
|
+
onDone;
|
|
1073
|
+
tuning;
|
|
1074
|
+
schedule;
|
|
1075
|
+
now;
|
|
1076
|
+
queue = [];
|
|
1077
|
+
cancelTimer = null;
|
|
1078
|
+
started = false;
|
|
1079
|
+
finished = false;
|
|
1080
|
+
startedAt = 0;
|
|
1081
|
+
chunkSize = 0;
|
|
1082
|
+
initialPackets = 0;
|
|
1083
|
+
enqueuedLive = 0;
|
|
1084
|
+
sentCount = 0;
|
|
1085
|
+
chunkCount = 0;
|
|
1086
|
+
constructor(send, onDone, options) {
|
|
1087
|
+
this.send = send;
|
|
1088
|
+
this.onDone = onDone;
|
|
1089
|
+
this.tuning = {
|
|
1090
|
+
...PACED_REPLAY_DEFAULTS,
|
|
1091
|
+
...options?.tuning
|
|
1092
|
+
};
|
|
1093
|
+
this.schedule = options?.schedule ?? defaultSchedule;
|
|
1094
|
+
this.now = options?.now ?? Date.now;
|
|
1095
|
+
}
|
|
1096
|
+
/** True while packets remain to be drained (live packets must be enqueued,
|
|
1097
|
+
* not sent directly, to preserve order). */
|
|
1098
|
+
get active() {
|
|
1099
|
+
return this.started && !this.finished;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Begin the replay: sends the first chunk synchronously, then paces the
|
|
1103
|
+
* rest. Calling `start` more than once is a no-op.
|
|
1104
|
+
*/
|
|
1105
|
+
start(initial) {
|
|
1106
|
+
if (this.started) return;
|
|
1107
|
+
this.started = true;
|
|
1108
|
+
this.startedAt = this.now();
|
|
1109
|
+
this.initialPackets = initial.length;
|
|
1110
|
+
this.queue = [...initial];
|
|
1111
|
+
this.chunkSize = computeReplayChunkSize(initial.length, this.tuning);
|
|
1112
|
+
this.drainChunk();
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Append a live packet behind the still-queued replay tail. No-op (returns
|
|
1116
|
+
* false) when the replay is not active — the caller must then send the
|
|
1117
|
+
* packet directly.
|
|
1118
|
+
*/
|
|
1119
|
+
enqueue(item) {
|
|
1120
|
+
if (!this.active) return false;
|
|
1121
|
+
this.queue.push(item);
|
|
1122
|
+
this.enqueuedLive++;
|
|
1123
|
+
return true;
|
|
1124
|
+
}
|
|
1125
|
+
/** Cancel the replay: pending packets are dropped, `onDone` fires with
|
|
1126
|
+
* `aborted: true`. Safe to call multiple times / before `start`. */
|
|
1127
|
+
abort() {
|
|
1128
|
+
if (this.finished) return;
|
|
1129
|
+
if (!this.started) {
|
|
1130
|
+
this.started = true;
|
|
1131
|
+
this.startedAt = this.now();
|
|
1132
|
+
}
|
|
1133
|
+
this.finish(true);
|
|
1134
|
+
}
|
|
1135
|
+
drainChunk() {
|
|
1136
|
+
if (this.finished) return;
|
|
1137
|
+
this.chunkCount++;
|
|
1138
|
+
const n = Math.min(this.chunkSize, this.queue.length);
|
|
1139
|
+
for (let i = 0; i < n; i++) {
|
|
1140
|
+
if (this.finished) return;
|
|
1141
|
+
if (this.send(this.queue[i])) this.sentCount++;
|
|
1142
|
+
}
|
|
1143
|
+
this.queue = this.queue.slice(n);
|
|
1144
|
+
if (this.queue.length === 0) {
|
|
1145
|
+
this.finish(false);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
this.cancelTimer = this.schedule(() => {
|
|
1149
|
+
this.cancelTimer = null;
|
|
1150
|
+
this.drainChunk();
|
|
1151
|
+
}, this.tuning.tickMs);
|
|
1152
|
+
}
|
|
1153
|
+
finish(aborted) {
|
|
1154
|
+
if (this.finished) return;
|
|
1155
|
+
this.finished = true;
|
|
1156
|
+
if (this.cancelTimer) {
|
|
1157
|
+
this.cancelTimer();
|
|
1158
|
+
this.cancelTimer = null;
|
|
1159
|
+
}
|
|
1160
|
+
this.queue = [];
|
|
1161
|
+
this.onDone({
|
|
1162
|
+
packets: this.initialPackets,
|
|
1163
|
+
enqueuedLive: this.enqueuedLive,
|
|
1164
|
+
sent: this.sentCount,
|
|
1165
|
+
chunks: this.chunkCount,
|
|
1166
|
+
chunkSize: this.chunkSize,
|
|
1167
|
+
durationMs: this.now() - this.startedAt,
|
|
1168
|
+
aborted
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
//#endregion
|
|
1173
|
+
//#region src/stream-broker/rtsp/annexb-deframer.ts
|
|
1174
|
+
/**
|
|
1175
|
+
* Annex-B NAL unit deframer.
|
|
1176
|
+
*
|
|
1177
|
+
* ffmpeg outputs Annex-B via stdout in arbitrary chunk sizes — a single
|
|
1178
|
+
* `data` event may contain partial NALs, multiple NALs, or NALs split
|
|
1179
|
+
* across events. This class accumulates bytes and emits complete NAL units.
|
|
1180
|
+
*
|
|
1181
|
+
* Usage:
|
|
1182
|
+
* const deframer = new AnnexBDeframer((nal, isKeyframe) => { ... })
|
|
1183
|
+
* proc.stdout.on('data', chunk => deframer.push(chunk))
|
|
1184
|
+
*/
|
|
1185
|
+
var AnnexBDeframer = class {
|
|
1186
|
+
onNal;
|
|
1187
|
+
buffer = Buffer.alloc(0);
|
|
1188
|
+
codec;
|
|
1189
|
+
constructor(codec, onNal) {
|
|
1190
|
+
this.onNal = onNal;
|
|
1191
|
+
this.codec = codec;
|
|
1192
|
+
}
|
|
1193
|
+
/** Push raw Annex-B bytes. Complete NAL units are emitted via onNal callback. */
|
|
1194
|
+
push(chunk) {
|
|
1195
|
+
this.buffer = this.buffer.length > 0 ? Buffer.concat([this.buffer, chunk]) : chunk;
|
|
1196
|
+
this.drain();
|
|
1197
|
+
}
|
|
1198
|
+
/** Flush remaining buffer (call on stream end). */
|
|
1199
|
+
flush() {
|
|
1200
|
+
if (this.buffer.length > 0) {
|
|
1201
|
+
const nal = this.stripLeadingStartCode(this.buffer);
|
|
1202
|
+
if (nal.length > 0) this.onNal(nal, this.isKeyframeNal(nal));
|
|
1203
|
+
this.buffer = Buffer.alloc(0);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
/** Extract complete NAL units from the buffer, leaving partial data for next push. */
|
|
1207
|
+
drain() {
|
|
1208
|
+
while (true) {
|
|
1209
|
+
const firstSc = this.findStartCode(0);
|
|
1210
|
+
if (firstSc < 0) return;
|
|
1211
|
+
const nalStart = firstSc + (this.isStartCode4(firstSc) ? 4 : 3);
|
|
1212
|
+
const nextSc = this.findStartCode(nalStart);
|
|
1213
|
+
if (nextSc < 0) {
|
|
1214
|
+
if (firstSc > 0) this.buffer = Buffer.from(this.buffer.subarray(firstSc));
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
const nal = this.buffer.subarray(nalStart, nextSc);
|
|
1218
|
+
if (nal.length > 0) this.onNal(nal, this.isKeyframeNal(nal));
|
|
1219
|
+
this.buffer = Buffer.from(this.buffer.subarray(nextSc));
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
/** Find the byte offset of the next start code (00 00 01 or 00 00 00 01) at or after `from`. */
|
|
1223
|
+
findStartCode(from) {
|
|
1224
|
+
const buf = this.buffer;
|
|
1225
|
+
const len = buf.length - 2;
|
|
1226
|
+
for (let i = from; i < len; i++) {
|
|
1227
|
+
if (buf[i + 2] > 1) {
|
|
1228
|
+
i += 2;
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
if (buf[i] === 0 && buf[i + 1] === 0) {
|
|
1232
|
+
if (buf[i + 2] === 1) return i;
|
|
1233
|
+
if (buf[i + 2] === 0 && i + 3 < buf.length && buf[i + 3] === 1) return i;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return -1;
|
|
1237
|
+
}
|
|
1238
|
+
/** Check if start code at `pos` is 4-byte (00 00 00 01) vs 3-byte (00 00 01). */
|
|
1239
|
+
isStartCode4(pos) {
|
|
1240
|
+
return pos + 3 < this.buffer.length && this.buffer[pos] === 0 && this.buffer[pos + 1] === 0 && this.buffer[pos + 2] === 0 && this.buffer[pos + 3] === 1;
|
|
1241
|
+
}
|
|
1242
|
+
/** Strip leading start code from a buffer (if present). */
|
|
1243
|
+
stripLeadingStartCode(buf) {
|
|
1244
|
+
if (buf.length >= 4 && buf[0] === 0 && buf[1] === 0 && buf[2] === 0 && buf[3] === 1) return buf.subarray(4);
|
|
1245
|
+
if (buf.length >= 3 && buf[0] === 0 && buf[1] === 0 && buf[2] === 1) return buf.subarray(3);
|
|
1246
|
+
return buf;
|
|
1247
|
+
}
|
|
1248
|
+
/** Check if a NAL unit (without start code) is a keyframe. */
|
|
1249
|
+
isKeyframeNal(nal) {
|
|
1250
|
+
if (nal.length === 0) return false;
|
|
1251
|
+
if (this.codec === "h264") return (nal[0] & 31) === 5;
|
|
1252
|
+
const type = nal[0] >> 1 & 63;
|
|
1253
|
+
return type >= 16 && type <= 21;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
//#endregion
|
|
1257
|
+
//#region src/stream-broker/rtsp/broker-device-id.ts
|
|
1258
|
+
/**
|
|
1259
|
+
* Parse the numeric deviceId out of a canonical brokerId
|
|
1260
|
+
* (`${deviceId}/${camStreamId}` or `${deviceId}/${profile}`). Returns
|
|
1261
|
+
* `undefined` for a malformed id — so every device-scoped log line on the RTSP
|
|
1262
|
+
* plane can carry `tags.deviceId` without risking a bogus tag value.
|
|
1263
|
+
*
|
|
1264
|
+
* Every log line about a device carries `tags: { deviceId }`, always the same
|
|
1265
|
+
* key and always the numeric id: a miss rate, a media gap or a routing fault is
|
|
1266
|
+
* always asked per-camera ("why is 617 worse than 615?"), and a line that omits
|
|
1267
|
+
* the tag cannot answer it.
|
|
1268
|
+
*/
|
|
1269
|
+
function deviceIdFromBrokerId$1(brokerId) {
|
|
1270
|
+
const parsed = Number(brokerId.split("/")[0]);
|
|
1271
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Extract the `m=audio` media block from an ffmpeg-generated SDP, normalised
|
|
1275
|
+
* for grafting (port 0, non-colliding payload type, own `a=control`, explicit
|
|
1276
|
+
* connection line). Returns `null` when the SDP has no audio media section.
|
|
1277
|
+
*/
|
|
1278
|
+
function extractAudioMediaBlock(ffmpegSdp, options = {}) {
|
|
1279
|
+
const trackId = options.trackId ?? 1;
|
|
1280
|
+
const lines = ffmpegSdp.split(/\r?\n/);
|
|
1281
|
+
const startIdx = lines.findIndex((l) => l.startsWith("m=audio"));
|
|
1282
|
+
if (startIdx < 0) return null;
|
|
1283
|
+
const block = [];
|
|
1284
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
1285
|
+
const line = lines[i];
|
|
1286
|
+
if (i > startIdx && line.startsWith("m=")) break;
|
|
1287
|
+
if (line.trim().length === 0) continue;
|
|
1288
|
+
if (line.startsWith("a=control:")) continue;
|
|
1289
|
+
block.push(line);
|
|
1290
|
+
}
|
|
1291
|
+
const mParts = block[0].split(/\s+/);
|
|
1292
|
+
const origPt = Number.parseInt(mParts[3] ?? "", 10);
|
|
1293
|
+
if (!Number.isInteger(origPt)) return null;
|
|
1294
|
+
const newPt = origPt >= 96 ? 97 : origPt;
|
|
1295
|
+
const remapped = block.map((line) => {
|
|
1296
|
+
if (line.startsWith("m=audio")) return `m=audio 0 ${mParts.slice(2).join(" ").replace(String(origPt), String(newPt))}`;
|
|
1297
|
+
if (origPt !== newPt && (line.startsWith("a=rtpmap:") || line.startsWith("a=fmtp:"))) return line.replace(`:${origPt} `, `:${newPt} `);
|
|
1298
|
+
return line;
|
|
1299
|
+
});
|
|
1300
|
+
if (!remapped.some((l) => l.startsWith("c="))) remapped.splice(1, 0, "c=IN IP4 0.0.0.0");
|
|
1301
|
+
remapped.push(`a=control:trackID=${trackId}`);
|
|
1302
|
+
return remapped.join("\r\n");
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Append a grafted audio media block to a video-only SDP, preserving CRLF
|
|
1306
|
+
* framing and the trailing blank line.
|
|
1307
|
+
*/
|
|
1308
|
+
function appendAudioMediaBlock(videoSdp, audioBlock) {
|
|
1309
|
+
return `${videoSdp.replace(/[\r\n]+$/, "")}\r\n${audioBlock}\r\n`;
|
|
1310
|
+
}
|
|
1311
|
+
//#endregion
|
|
1043
1312
|
//#region src/stream-broker/rtsp/rtsp-types.ts
|
|
1044
1313
|
/** RTSP method constants. */
|
|
1045
1314
|
var RTSP_METHODS = [
|
|
@@ -1127,129 +1396,45 @@ var RtpPacketizer = class {
|
|
|
1127
1396
|
packets.push(this.buildRtpPacket(fragment, timestamp, isLast && marker));
|
|
1128
1397
|
offset = end;
|
|
1129
1398
|
}
|
|
1130
|
-
return packets;
|
|
1131
|
-
}
|
|
1132
|
-
/** H.265 FU fragmentation (RFC 7798 Section 4.4.3). */
|
|
1133
|
-
fragmentH265FU(nal, timestamp, marker) {
|
|
1134
|
-
const nalType = nal[0] >> 1 & 63;
|
|
1135
|
-
const tidLayerId = nal[1];
|
|
1136
|
-
const payload = nal.subarray(2);
|
|
1137
|
-
const maxFragment = RTP_MAX_PAYLOAD - 3;
|
|
1138
|
-
const packets = [];
|
|
1139
|
-
let offset = 0;
|
|
1140
|
-
while (offset < payload.length) {
|
|
1141
|
-
const end = Math.min(offset + maxFragment, payload.length);
|
|
1142
|
-
const isFirst = offset === 0;
|
|
1143
|
-
const isLast = end === payload.length;
|
|
1144
|
-
const payloadHdr0 = 98 | nal[0] & 129;
|
|
1145
|
-
const payloadHdr1 = tidLayerId;
|
|
1146
|
-
const fuHeader = (isFirst ? 128 : 0) | (isLast ? 64 : 0) | nalType;
|
|
1147
|
-
const fragment = Buffer.allocUnsafe(3 + (end - offset));
|
|
1148
|
-
fragment[0] = payloadHdr0;
|
|
1149
|
-
fragment[1] = payloadHdr1;
|
|
1150
|
-
fragment[2] = fuHeader;
|
|
1151
|
-
payload.copy(fragment, 3, offset, end);
|
|
1152
|
-
packets.push(this.buildRtpPacket(fragment, timestamp, isLast && marker));
|
|
1153
|
-
offset = end;
|
|
1154
|
-
}
|
|
1155
|
-
return packets;
|
|
1156
|
-
}
|
|
1157
|
-
buildRtpPacket(payload, timestamp, marker) {
|
|
1158
|
-
const header = Buffer.allocUnsafe(12);
|
|
1159
|
-
header[0] = 128;
|
|
1160
|
-
header[1] = (marker ? 128 : 0) | this.payloadType & 127;
|
|
1161
|
-
header.writeUInt16BE(this.sequenceNumber & 65535, 2);
|
|
1162
|
-
this.sequenceNumber = this.sequenceNumber + 1 & 65535;
|
|
1163
|
-
header.writeUInt32BE(timestamp >>> 0, 4);
|
|
1164
|
-
header.writeUInt32BE(this.ssrc >>> 0, 8);
|
|
1165
|
-
return {
|
|
1166
|
-
data: Buffer.concat([header, payload]),
|
|
1167
|
-
marker
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
|
-
};
|
|
1171
|
-
//#endregion
|
|
1172
|
-
//#region src/stream-broker/rtsp/annexb-deframer.ts
|
|
1173
|
-
/**
|
|
1174
|
-
* Annex-B NAL unit deframer.
|
|
1175
|
-
*
|
|
1176
|
-
* ffmpeg outputs Annex-B via stdout in arbitrary chunk sizes — a single
|
|
1177
|
-
* `data` event may contain partial NALs, multiple NALs, or NALs split
|
|
1178
|
-
* across events. This class accumulates bytes and emits complete NAL units.
|
|
1179
|
-
*
|
|
1180
|
-
* Usage:
|
|
1181
|
-
* const deframer = new AnnexBDeframer((nal, isKeyframe) => { ... })
|
|
1182
|
-
* proc.stdout.on('data', chunk => deframer.push(chunk))
|
|
1183
|
-
*/
|
|
1184
|
-
var AnnexBDeframer = class {
|
|
1185
|
-
onNal;
|
|
1186
|
-
buffer = Buffer.alloc(0);
|
|
1187
|
-
codec;
|
|
1188
|
-
constructor(codec, onNal) {
|
|
1189
|
-
this.onNal = onNal;
|
|
1190
|
-
this.codec = codec;
|
|
1191
|
-
}
|
|
1192
|
-
/** Push raw Annex-B bytes. Complete NAL units are emitted via onNal callback. */
|
|
1193
|
-
push(chunk) {
|
|
1194
|
-
this.buffer = this.buffer.length > 0 ? Buffer.concat([this.buffer, chunk]) : chunk;
|
|
1195
|
-
this.drain();
|
|
1196
|
-
}
|
|
1197
|
-
/** Flush remaining buffer (call on stream end). */
|
|
1198
|
-
flush() {
|
|
1199
|
-
if (this.buffer.length > 0) {
|
|
1200
|
-
const nal = this.stripLeadingStartCode(this.buffer);
|
|
1201
|
-
if (nal.length > 0) this.onNal(nal, this.isKeyframeNal(nal));
|
|
1202
|
-
this.buffer = Buffer.alloc(0);
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
/** Extract complete NAL units from the buffer, leaving partial data for next push. */
|
|
1206
|
-
drain() {
|
|
1207
|
-
while (true) {
|
|
1208
|
-
const firstSc = this.findStartCode(0);
|
|
1209
|
-
if (firstSc < 0) return;
|
|
1210
|
-
const nalStart = firstSc + (this.isStartCode4(firstSc) ? 4 : 3);
|
|
1211
|
-
const nextSc = this.findStartCode(nalStart);
|
|
1212
|
-
if (nextSc < 0) {
|
|
1213
|
-
if (firstSc > 0) this.buffer = Buffer.from(this.buffer.subarray(firstSc));
|
|
1214
|
-
return;
|
|
1215
|
-
}
|
|
1216
|
-
const nal = this.buffer.subarray(nalStart, nextSc);
|
|
1217
|
-
if (nal.length > 0) this.onNal(nal, this.isKeyframeNal(nal));
|
|
1218
|
-
this.buffer = Buffer.from(this.buffer.subarray(nextSc));
|
|
1219
|
-
}
|
|
1399
|
+
return packets;
|
|
1220
1400
|
}
|
|
1221
|
-
/**
|
|
1222
|
-
|
|
1223
|
-
const
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1401
|
+
/** H.265 FU fragmentation (RFC 7798 Section 4.4.3). */
|
|
1402
|
+
fragmentH265FU(nal, timestamp, marker) {
|
|
1403
|
+
const nalType = nal[0] >> 1 & 63;
|
|
1404
|
+
const tidLayerId = nal[1];
|
|
1405
|
+
const payload = nal.subarray(2);
|
|
1406
|
+
const maxFragment = RTP_MAX_PAYLOAD - 3;
|
|
1407
|
+
const packets = [];
|
|
1408
|
+
let offset = 0;
|
|
1409
|
+
while (offset < payload.length) {
|
|
1410
|
+
const end = Math.min(offset + maxFragment, payload.length);
|
|
1411
|
+
const isFirst = offset === 0;
|
|
1412
|
+
const isLast = end === payload.length;
|
|
1413
|
+
const payloadHdr0 = 98 | nal[0] & 129;
|
|
1414
|
+
const payloadHdr1 = tidLayerId;
|
|
1415
|
+
const fuHeader = (isFirst ? 128 : 0) | (isLast ? 64 : 0) | nalType;
|
|
1416
|
+
const fragment = Buffer.allocUnsafe(3 + (end - offset));
|
|
1417
|
+
fragment[0] = payloadHdr0;
|
|
1418
|
+
fragment[1] = payloadHdr1;
|
|
1419
|
+
fragment[2] = fuHeader;
|
|
1420
|
+
payload.copy(fragment, 3, offset, end);
|
|
1421
|
+
packets.push(this.buildRtpPacket(fragment, timestamp, isLast && marker));
|
|
1422
|
+
offset = end;
|
|
1234
1423
|
}
|
|
1235
|
-
return
|
|
1236
|
-
}
|
|
1237
|
-
/** Check if start code at `pos` is 4-byte (00 00 00 01) vs 3-byte (00 00 01). */
|
|
1238
|
-
isStartCode4(pos) {
|
|
1239
|
-
return pos + 3 < this.buffer.length && this.buffer[pos] === 0 && this.buffer[pos + 1] === 0 && this.buffer[pos + 2] === 0 && this.buffer[pos + 3] === 1;
|
|
1240
|
-
}
|
|
1241
|
-
/** Strip leading start code from a buffer (if present). */
|
|
1242
|
-
stripLeadingStartCode(buf) {
|
|
1243
|
-
if (buf.length >= 4 && buf[0] === 0 && buf[1] === 0 && buf[2] === 0 && buf[3] === 1) return buf.subarray(4);
|
|
1244
|
-
if (buf.length >= 3 && buf[0] === 0 && buf[1] === 0 && buf[2] === 1) return buf.subarray(3);
|
|
1245
|
-
return buf;
|
|
1424
|
+
return packets;
|
|
1246
1425
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1426
|
+
buildRtpPacket(payload, timestamp, marker) {
|
|
1427
|
+
const header = Buffer.allocUnsafe(12);
|
|
1428
|
+
header[0] = 128;
|
|
1429
|
+
header[1] = (marker ? 128 : 0) | this.payloadType & 127;
|
|
1430
|
+
header.writeUInt16BE(this.sequenceNumber & 65535, 2);
|
|
1431
|
+
this.sequenceNumber = this.sequenceNumber + 1 & 65535;
|
|
1432
|
+
header.writeUInt32BE(timestamp >>> 0, 4);
|
|
1433
|
+
header.writeUInt32BE(this.ssrc >>> 0, 8);
|
|
1434
|
+
return {
|
|
1435
|
+
data: Buffer.concat([header, payload]),
|
|
1436
|
+
marker
|
|
1437
|
+
};
|
|
1253
1438
|
}
|
|
1254
1439
|
};
|
|
1255
1440
|
//#endregion
|
|
@@ -1287,191 +1472,6 @@ function buildSdp(options) {
|
|
|
1287
1472
|
lines.push("");
|
|
1288
1473
|
return lines.join("\r\n");
|
|
1289
1474
|
}
|
|
1290
|
-
/**
|
|
1291
|
-
* Extract the `m=audio` media block from an ffmpeg-generated SDP, normalised
|
|
1292
|
-
* for grafting (port 0, non-colliding payload type, own `a=control`, explicit
|
|
1293
|
-
* connection line). Returns `null` when the SDP has no audio media section.
|
|
1294
|
-
*/
|
|
1295
|
-
function extractAudioMediaBlock(ffmpegSdp, options = {}) {
|
|
1296
|
-
const trackId = options.trackId ?? 1;
|
|
1297
|
-
const lines = ffmpegSdp.split(/\r?\n/);
|
|
1298
|
-
const startIdx = lines.findIndex((l) => l.startsWith("m=audio"));
|
|
1299
|
-
if (startIdx < 0) return null;
|
|
1300
|
-
const block = [];
|
|
1301
|
-
for (let i = startIdx; i < lines.length; i++) {
|
|
1302
|
-
const line = lines[i];
|
|
1303
|
-
if (i > startIdx && line.startsWith("m=")) break;
|
|
1304
|
-
if (line.trim().length === 0) continue;
|
|
1305
|
-
if (line.startsWith("a=control:")) continue;
|
|
1306
|
-
block.push(line);
|
|
1307
|
-
}
|
|
1308
|
-
const mParts = block[0].split(/\s+/);
|
|
1309
|
-
const origPt = Number.parseInt(mParts[3] ?? "", 10);
|
|
1310
|
-
if (!Number.isInteger(origPt)) return null;
|
|
1311
|
-
const newPt = origPt >= 96 ? 97 : origPt;
|
|
1312
|
-
const remapped = block.map((line) => {
|
|
1313
|
-
if (line.startsWith("m=audio")) return `m=audio 0 ${mParts.slice(2).join(" ").replace(String(origPt), String(newPt))}`;
|
|
1314
|
-
if (origPt !== newPt && (line.startsWith("a=rtpmap:") || line.startsWith("a=fmtp:"))) return line.replace(`:${origPt} `, `:${newPt} `);
|
|
1315
|
-
return line;
|
|
1316
|
-
});
|
|
1317
|
-
if (!remapped.some((l) => l.startsWith("c="))) remapped.splice(1, 0, "c=IN IP4 0.0.0.0");
|
|
1318
|
-
remapped.push(`a=control:trackID=${trackId}`);
|
|
1319
|
-
return remapped.join("\r\n");
|
|
1320
|
-
}
|
|
1321
|
-
/**
|
|
1322
|
-
* Append a grafted audio media block to a video-only SDP, preserving CRLF
|
|
1323
|
-
* framing and the trailing blank line.
|
|
1324
|
-
*/
|
|
1325
|
-
function appendAudioMediaBlock(videoSdp, audioBlock) {
|
|
1326
|
-
return `${videoSdp.replace(/[\r\n]+$/, "")}\r\n${audioBlock}\r\n`;
|
|
1327
|
-
}
|
|
1328
|
-
//#endregion
|
|
1329
|
-
//#region src/stream-broker/rtsp/broker-device-id.ts
|
|
1330
|
-
/**
|
|
1331
|
-
* Parse the numeric deviceId out of a canonical brokerId
|
|
1332
|
-
* (`${deviceId}/${camStreamId}` or `${deviceId}/${profile}`). Returns
|
|
1333
|
-
* `undefined` for a malformed id — so every device-scoped log line on the RTSP
|
|
1334
|
-
* plane can carry `tags.deviceId` without risking a bogus tag value.
|
|
1335
|
-
*
|
|
1336
|
-
* Every log line about a device carries `tags: { deviceId }`, always the same
|
|
1337
|
-
* key and always the numeric id: a miss rate, a media gap or a routing fault is
|
|
1338
|
-
* always asked per-camera ("why is 617 worse than 615?"), and a line that omits
|
|
1339
|
-
* the tag cannot answer it.
|
|
1340
|
-
*/
|
|
1341
|
-
function deviceIdFromBrokerId$1(brokerId) {
|
|
1342
|
-
const parsed = Number(brokerId.split("/")[0]);
|
|
1343
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1344
|
-
}
|
|
1345
|
-
//#endregion
|
|
1346
|
-
//#region src/stream-broker/paced-rtp-replay.ts
|
|
1347
|
-
var PACED_REPLAY_DEFAULTS = {
|
|
1348
|
-
singleShotMax: 64,
|
|
1349
|
-
tickMs: 5,
|
|
1350
|
-
targetDurationMs: 300,
|
|
1351
|
-
minChunkSize: 32,
|
|
1352
|
-
maxChunkSize: 128
|
|
1353
|
-
};
|
|
1354
|
-
var defaultSchedule = (fn, ms) => {
|
|
1355
|
-
const t = setTimeout(fn, ms);
|
|
1356
|
-
return () => clearTimeout(t);
|
|
1357
|
-
};
|
|
1358
|
-
/**
|
|
1359
|
-
* Compute the per-tick chunk size for a burst of `total` packets: enough to
|
|
1360
|
-
* drain within `targetDurationMs` at one chunk per `tickMs`, clamped to
|
|
1361
|
-
* [minChunkSize, maxChunkSize]. Bursts ≤ `singleShotMax` are one chunk.
|
|
1362
|
-
*/
|
|
1363
|
-
function computeReplayChunkSize(total, tuning) {
|
|
1364
|
-
if (total <= tuning.singleShotMax) return Math.max(total, 1);
|
|
1365
|
-
const maxTicks = Math.max(1, Math.floor(tuning.targetDurationMs / tuning.tickMs));
|
|
1366
|
-
const ideal = Math.ceil(total / maxTicks);
|
|
1367
|
-
return Math.min(tuning.maxChunkSize, Math.max(tuning.minChunkSize, ideal));
|
|
1368
|
-
}
|
|
1369
|
-
/**
|
|
1370
|
-
* One in-flight paced replay. Create per burst; not reusable after
|
|
1371
|
-
* completion or abort.
|
|
1372
|
-
*/
|
|
1373
|
-
var PacedRtpReplay = class {
|
|
1374
|
-
send;
|
|
1375
|
-
onDone;
|
|
1376
|
-
tuning;
|
|
1377
|
-
schedule;
|
|
1378
|
-
now;
|
|
1379
|
-
queue = [];
|
|
1380
|
-
cancelTimer = null;
|
|
1381
|
-
started = false;
|
|
1382
|
-
finished = false;
|
|
1383
|
-
startedAt = 0;
|
|
1384
|
-
chunkSize = 0;
|
|
1385
|
-
initialPackets = 0;
|
|
1386
|
-
enqueuedLive = 0;
|
|
1387
|
-
sentCount = 0;
|
|
1388
|
-
chunkCount = 0;
|
|
1389
|
-
constructor(send, onDone, options) {
|
|
1390
|
-
this.send = send;
|
|
1391
|
-
this.onDone = onDone;
|
|
1392
|
-
this.tuning = {
|
|
1393
|
-
...PACED_REPLAY_DEFAULTS,
|
|
1394
|
-
...options?.tuning
|
|
1395
|
-
};
|
|
1396
|
-
this.schedule = options?.schedule ?? defaultSchedule;
|
|
1397
|
-
this.now = options?.now ?? Date.now;
|
|
1398
|
-
}
|
|
1399
|
-
/** True while packets remain to be drained (live packets must be enqueued,
|
|
1400
|
-
* not sent directly, to preserve order). */
|
|
1401
|
-
get active() {
|
|
1402
|
-
return this.started && !this.finished;
|
|
1403
|
-
}
|
|
1404
|
-
/**
|
|
1405
|
-
* Begin the replay: sends the first chunk synchronously, then paces the
|
|
1406
|
-
* rest. Calling `start` more than once is a no-op.
|
|
1407
|
-
*/
|
|
1408
|
-
start(initial) {
|
|
1409
|
-
if (this.started) return;
|
|
1410
|
-
this.started = true;
|
|
1411
|
-
this.startedAt = this.now();
|
|
1412
|
-
this.initialPackets = initial.length;
|
|
1413
|
-
this.queue = [...initial];
|
|
1414
|
-
this.chunkSize = computeReplayChunkSize(initial.length, this.tuning);
|
|
1415
|
-
this.drainChunk();
|
|
1416
|
-
}
|
|
1417
|
-
/**
|
|
1418
|
-
* Append a live packet behind the still-queued replay tail. No-op (returns
|
|
1419
|
-
* false) when the replay is not active — the caller must then send the
|
|
1420
|
-
* packet directly.
|
|
1421
|
-
*/
|
|
1422
|
-
enqueue(item) {
|
|
1423
|
-
if (!this.active) return false;
|
|
1424
|
-
this.queue.push(item);
|
|
1425
|
-
this.enqueuedLive++;
|
|
1426
|
-
return true;
|
|
1427
|
-
}
|
|
1428
|
-
/** Cancel the replay: pending packets are dropped, `onDone` fires with
|
|
1429
|
-
* `aborted: true`. Safe to call multiple times / before `start`. */
|
|
1430
|
-
abort() {
|
|
1431
|
-
if (this.finished) return;
|
|
1432
|
-
if (!this.started) {
|
|
1433
|
-
this.started = true;
|
|
1434
|
-
this.startedAt = this.now();
|
|
1435
|
-
}
|
|
1436
|
-
this.finish(true);
|
|
1437
|
-
}
|
|
1438
|
-
drainChunk() {
|
|
1439
|
-
if (this.finished) return;
|
|
1440
|
-
this.chunkCount++;
|
|
1441
|
-
const n = Math.min(this.chunkSize, this.queue.length);
|
|
1442
|
-
for (let i = 0; i < n; i++) {
|
|
1443
|
-
if (this.finished) return;
|
|
1444
|
-
if (this.send(this.queue[i])) this.sentCount++;
|
|
1445
|
-
}
|
|
1446
|
-
this.queue = this.queue.slice(n);
|
|
1447
|
-
if (this.queue.length === 0) {
|
|
1448
|
-
this.finish(false);
|
|
1449
|
-
return;
|
|
1450
|
-
}
|
|
1451
|
-
this.cancelTimer = this.schedule(() => {
|
|
1452
|
-
this.cancelTimer = null;
|
|
1453
|
-
this.drainChunk();
|
|
1454
|
-
}, this.tuning.tickMs);
|
|
1455
|
-
}
|
|
1456
|
-
finish(aborted) {
|
|
1457
|
-
if (this.finished) return;
|
|
1458
|
-
this.finished = true;
|
|
1459
|
-
if (this.cancelTimer) {
|
|
1460
|
-
this.cancelTimer();
|
|
1461
|
-
this.cancelTimer = null;
|
|
1462
|
-
}
|
|
1463
|
-
this.queue = [];
|
|
1464
|
-
this.onDone({
|
|
1465
|
-
packets: this.initialPackets,
|
|
1466
|
-
enqueuedLive: this.enqueuedLive,
|
|
1467
|
-
sent: this.sentCount,
|
|
1468
|
-
chunks: this.chunkCount,
|
|
1469
|
-
chunkSize: this.chunkSize,
|
|
1470
|
-
durationMs: this.now() - this.startedAt,
|
|
1471
|
-
aborted
|
|
1472
|
-
});
|
|
1473
|
-
}
|
|
1474
|
-
};
|
|
1475
1475
|
//#endregion
|
|
1476
1476
|
//#region src/stream-broker/rtsp/rtsp-restreamer.ts
|
|
1477
1477
|
/**
|
|
@@ -1637,6 +1637,20 @@ var RtspRestreamer = class {
|
|
|
1637
1637
|
/** Throttled drop/withhold accounting — see {@link reportDrops}. */
|
|
1638
1638
|
dropsWithheldSpan = 0;
|
|
1639
1639
|
dropsWithheldPackets = 0;
|
|
1640
|
+
/**
|
|
1641
|
+
* Muted (local-decoder loopback) joins refused the ring burst because it was
|
|
1642
|
+
* not near-live — see {@link LIVE_EDGE_MAX_SPAN_MS}. Its own counter: this is
|
|
1643
|
+
* a different rule from the span/packet bound, it fires on a DIFFERENT
|
|
1644
|
+
* population (only decode sessions), and conflating it with
|
|
1645
|
+
* {@link dropsWithheldSpan} would hide that a camera's decoder starts blind
|
|
1646
|
+
* on nearly every dial while its viewers are served normally.
|
|
1647
|
+
*
|
|
1648
|
+
* Counts withhold DECISIONS, not joins — `servePendingSessionsRtp` runs per
|
|
1649
|
+
* packet, so one session withheld for a whole GOP contributes many. Same
|
|
1650
|
+
* semantics as {@link dropsWithheldSpan}; the signal to read is "nonzero on
|
|
1651
|
+
* THIS camera while its viewers are being served".
|
|
1652
|
+
*/
|
|
1653
|
+
dropsWithheldMutedLiveEdge = 0;
|
|
1640
1654
|
dropsPrimeBacklog = 0;
|
|
1641
1655
|
dropsLiveLatched = 0;
|
|
1642
1656
|
dropsLiveClosed = 0;
|
|
@@ -1905,7 +1919,11 @@ var RtspRestreamer = class {
|
|
|
1905
1919
|
const session = this.sessions.get(sessionId);
|
|
1906
1920
|
if (!session) continue;
|
|
1907
1921
|
if (session.isMuted()) {
|
|
1908
|
-
if (!fromRing || spanMs > LIVE_EDGE_MAX_SPAN_MS)
|
|
1922
|
+
if (!fromRing || spanMs > LIVE_EDGE_MAX_SPAN_MS) {
|
|
1923
|
+
this.dropsWithheldMutedLiveEdge++;
|
|
1924
|
+
withheld++;
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1909
1927
|
} else if (overSpan || overPackets) {
|
|
1910
1928
|
if (overSpan) this.dropsWithheldSpan++;
|
|
1911
1929
|
else this.dropsWithheldPackets++;
|
|
@@ -2028,6 +2046,8 @@ var RtspRestreamer = class {
|
|
|
2028
2046
|
boundPackets: bound.maxPackets,
|
|
2029
2047
|
primeWithheldSpanMs: this.dropsWithheldSpan,
|
|
2030
2048
|
primeWithheldPackets: this.dropsWithheldPackets,
|
|
2049
|
+
primeWithheldMutedLiveEdge: this.dropsWithheldMutedLiveEdge,
|
|
2050
|
+
liveEdgeMaxSpanMs: LIVE_EDGE_MAX_SPAN_MS,
|
|
2031
2051
|
primeAbortedOnBacklog: this.dropsPrimeBacklog,
|
|
2032
2052
|
liveDroppedUntilKeyframe: this.dropsLiveLatched,
|
|
2033
2053
|
clientsClosedOnBacklog: this.dropsLiveClosed,
|
|
@@ -2036,6 +2056,7 @@ var RtspRestreamer = class {
|
|
|
2036
2056
|
});
|
|
2037
2057
|
this.dropsWithheldSpan = 0;
|
|
2038
2058
|
this.dropsWithheldPackets = 0;
|
|
2059
|
+
this.dropsWithheldMutedLiveEdge = 0;
|
|
2039
2060
|
this.dropsPrimeBacklog = 0;
|
|
2040
2061
|
this.dropsLiveLatched = 0;
|
|
2041
2062
|
this.dropsLiveClosed = 0;
|
|
@@ -13060,7 +13081,7 @@ var TranscodePipelineManager = class {
|
|
|
13060
13081
|
}
|
|
13061
13082
|
});
|
|
13062
13083
|
const passVideoCodec = sourceCodec ?? "H264";
|
|
13063
|
-
const url = this.resolveSourceUrl(input.deviceId, sourceStream.camStreamId);
|
|
13084
|
+
const url = this.resolveSourceUrl(input.deviceId, sourceStream.camStreamId, wantedAudio === "none");
|
|
13064
13085
|
const key = pipelineKeyFor(input.deviceId, sourceStream.camStreamId, "copy", wantedAudio, outResolution, outputArgs, false);
|
|
13065
13086
|
return this.shareOrCreatePassthrough(key, () => ({
|
|
13066
13087
|
url,
|
|
@@ -13216,10 +13237,11 @@ var TranscodePipelineManager = class {
|
|
|
13216
13237
|
* source dial. Throws when no restream entry exists — a targeted source
|
|
13217
13238
|
* must be dialed before it can be consumed.
|
|
13218
13239
|
*/
|
|
13219
|
-
resolveSourceUrl(deviceId, camStreamId) {
|
|
13240
|
+
resolveSourceUrl(deviceId, camStreamId, videoOnly = false) {
|
|
13220
13241
|
const brokerId = makeSourceBrokerId(deviceId, camStreamId);
|
|
13221
13242
|
const entry = this.rtspEntryLookup(brokerId);
|
|
13222
|
-
|
|
13243
|
+
const url = videoOnly ? entry?.mutedUrl ?? entry?.url : entry?.url;
|
|
13244
|
+
if (url) return url;
|
|
13223
13245
|
throw new Error(`getStreamWithCodec: no broker restream for ${brokerId} (source not dialed)`);
|
|
13224
13246
|
}
|
|
13225
13247
|
/**
|
|
@@ -13836,7 +13858,10 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
13836
13858
|
cameraStreamLookup: (deviceId) => this.getCameraStreamsForDevice(deviceId),
|
|
13837
13859
|
rtspEntryLookup: (brokerId) => {
|
|
13838
13860
|
const entry = this.rtspProvider.getEntry(brokerId);
|
|
13839
|
-
return entry ? {
|
|
13861
|
+
return entry ? {
|
|
13862
|
+
url: entry.url,
|
|
13863
|
+
mutedUrl: entry.mutedUrl
|
|
13864
|
+
} : null;
|
|
13840
13865
|
},
|
|
13841
13866
|
resolveProfileSource: (deviceId, profile) => this.assignments.get(deviceId)?.map[profile] ?? null,
|
|
13842
13867
|
ffmpegConfig: () => this.ffmpegConfig,
|