@camstack/addon-pipeline 1.2.51 → 1.2.53
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/{addon-utils-DveoBR6G.js → addon-utils-CQs-AjTJ.js} +177 -177
- package/dist/{addon-utils-QuxtOdK1.mjs → addon-utils-CZ2xo67g.mjs} +177 -177
- package/dist/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +3 -3
- package/dist/detection-pipeline/index.mjs +3 -3
- package/dist/{dist-Dkvk5ykl.mjs → dist-B55xTTsS.mjs} +16337 -15811
- package/dist/{dist-BsBEe_rX.js → dist-D8TX_P_A.js} +16337 -15811
- package/dist/{event-loop-stall-monitor-DqJXi0_B.js → event-loop-stall-monitor-Bs6d-u_F.js} +1 -1
- package/dist/{event-loop-stall-monitor-BvtjRcal.mjs → event-loop-stall-monitor-Cq_KOlfb.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 +160 -40
- package/dist/pipeline-runner/index.mjs +160 -40
- package/dist/recorder/index.js +2 -2
- package/dist/recorder/index.mjs +2 -2
- package/dist/session-decode/decode-worker-child.js +6 -2
- package/dist/session-decode/decode-worker-child.mjs +6 -2
- package/dist/stream-broker/_stub.js +2 -2
- 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-C-9q6y1s.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CAmiVOsA.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-CaGsMvou.mjs +26 -0
- package/dist/stream-broker/{hostInit-B8O23a6l.mjs → hostInit-DiXYwnXH.mjs} +3 -3
- package/dist/stream-broker/index.js +337 -312
- package/dist/stream-broker/index.mjs +337 -312
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-B0c3i2km.mjs → worker-protocol-Bol291_n.mjs} +1 -1
- package/dist/{worker-protocol-CWhkLkHx.js → worker-protocol-C2uctCrd.js} +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-kAtChdz2.js → MaskShapeCanvas-DI4BY7W2-CXHeTkIT.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DEwkvjzt.js → MotionZonesSettings-NcxxQN8r-De5VKf09.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-Brv9VL0h.js → PrivacyMaskSettings-APgPLF7p-Z7dsjOG5.js} +1 -1
- package/embed-dist/assets/{index-Dv7fSuDA.js → index-NJB31mpg.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
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Czrsozsg.mjs +0 -26
|
@@ -3,9 +3,9 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
6
|
+
const require_dist = require("../dist-D8TX_P_A.js");
|
|
7
7
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
8
|
-
const require_addon_utils = require("../addon-utils-
|
|
8
|
+
const require_addon_utils = require("../addon-utils-CQs-AjTJ.js");
|
|
9
9
|
let node_crypto = require("node:crypto");
|
|
10
10
|
node_crypto = require_chunk.__toESM(node_crypto, 1);
|
|
11
11
|
let node_child_process = require("node:child_process");
|
|
@@ -1046,6 +1046,275 @@ function buildDerivedTranscodeArgs(profile, loopbackUrl, decodeHwAccel = null) {
|
|
|
1046
1046
|
}));
|
|
1047
1047
|
}
|
|
1048
1048
|
//#endregion
|
|
1049
|
+
//#region src/stream-broker/paced-rtp-replay.ts
|
|
1050
|
+
var PACED_REPLAY_DEFAULTS = {
|
|
1051
|
+
singleShotMax: 64,
|
|
1052
|
+
tickMs: 5,
|
|
1053
|
+
targetDurationMs: 300,
|
|
1054
|
+
minChunkSize: 32,
|
|
1055
|
+
maxChunkSize: 128
|
|
1056
|
+
};
|
|
1057
|
+
var defaultSchedule = (fn, ms) => {
|
|
1058
|
+
const t = setTimeout(fn, ms);
|
|
1059
|
+
return () => clearTimeout(t);
|
|
1060
|
+
};
|
|
1061
|
+
/**
|
|
1062
|
+
* Compute the per-tick chunk size for a burst of `total` packets: enough to
|
|
1063
|
+
* drain within `targetDurationMs` at one chunk per `tickMs`, clamped to
|
|
1064
|
+
* [minChunkSize, maxChunkSize]. Bursts ≤ `singleShotMax` are one chunk.
|
|
1065
|
+
*/
|
|
1066
|
+
function computeReplayChunkSize(total, tuning) {
|
|
1067
|
+
if (total <= tuning.singleShotMax) return Math.max(total, 1);
|
|
1068
|
+
const maxTicks = Math.max(1, Math.floor(tuning.targetDurationMs / tuning.tickMs));
|
|
1069
|
+
const ideal = Math.ceil(total / maxTicks);
|
|
1070
|
+
return Math.min(tuning.maxChunkSize, Math.max(tuning.minChunkSize, ideal));
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* One in-flight paced replay. Create per burst; not reusable after
|
|
1074
|
+
* completion or abort.
|
|
1075
|
+
*/
|
|
1076
|
+
var PacedRtpReplay = class {
|
|
1077
|
+
send;
|
|
1078
|
+
onDone;
|
|
1079
|
+
tuning;
|
|
1080
|
+
schedule;
|
|
1081
|
+
now;
|
|
1082
|
+
queue = [];
|
|
1083
|
+
cancelTimer = null;
|
|
1084
|
+
started = false;
|
|
1085
|
+
finished = false;
|
|
1086
|
+
startedAt = 0;
|
|
1087
|
+
chunkSize = 0;
|
|
1088
|
+
initialPackets = 0;
|
|
1089
|
+
enqueuedLive = 0;
|
|
1090
|
+
sentCount = 0;
|
|
1091
|
+
chunkCount = 0;
|
|
1092
|
+
constructor(send, onDone, options) {
|
|
1093
|
+
this.send = send;
|
|
1094
|
+
this.onDone = onDone;
|
|
1095
|
+
this.tuning = {
|
|
1096
|
+
...PACED_REPLAY_DEFAULTS,
|
|
1097
|
+
...options?.tuning
|
|
1098
|
+
};
|
|
1099
|
+
this.schedule = options?.schedule ?? defaultSchedule;
|
|
1100
|
+
this.now = options?.now ?? Date.now;
|
|
1101
|
+
}
|
|
1102
|
+
/** True while packets remain to be drained (live packets must be enqueued,
|
|
1103
|
+
* not sent directly, to preserve order). */
|
|
1104
|
+
get active() {
|
|
1105
|
+
return this.started && !this.finished;
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Begin the replay: sends the first chunk synchronously, then paces the
|
|
1109
|
+
* rest. Calling `start` more than once is a no-op.
|
|
1110
|
+
*/
|
|
1111
|
+
start(initial) {
|
|
1112
|
+
if (this.started) return;
|
|
1113
|
+
this.started = true;
|
|
1114
|
+
this.startedAt = this.now();
|
|
1115
|
+
this.initialPackets = initial.length;
|
|
1116
|
+
this.queue = [...initial];
|
|
1117
|
+
this.chunkSize = computeReplayChunkSize(initial.length, this.tuning);
|
|
1118
|
+
this.drainChunk();
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Append a live packet behind the still-queued replay tail. No-op (returns
|
|
1122
|
+
* false) when the replay is not active — the caller must then send the
|
|
1123
|
+
* packet directly.
|
|
1124
|
+
*/
|
|
1125
|
+
enqueue(item) {
|
|
1126
|
+
if (!this.active) return false;
|
|
1127
|
+
this.queue.push(item);
|
|
1128
|
+
this.enqueuedLive++;
|
|
1129
|
+
return true;
|
|
1130
|
+
}
|
|
1131
|
+
/** Cancel the replay: pending packets are dropped, `onDone` fires with
|
|
1132
|
+
* `aborted: true`. Safe to call multiple times / before `start`. */
|
|
1133
|
+
abort() {
|
|
1134
|
+
if (this.finished) return;
|
|
1135
|
+
if (!this.started) {
|
|
1136
|
+
this.started = true;
|
|
1137
|
+
this.startedAt = this.now();
|
|
1138
|
+
}
|
|
1139
|
+
this.finish(true);
|
|
1140
|
+
}
|
|
1141
|
+
drainChunk() {
|
|
1142
|
+
if (this.finished) return;
|
|
1143
|
+
this.chunkCount++;
|
|
1144
|
+
const n = Math.min(this.chunkSize, this.queue.length);
|
|
1145
|
+
for (let i = 0; i < n; i++) {
|
|
1146
|
+
if (this.finished) return;
|
|
1147
|
+
if (this.send(this.queue[i])) this.sentCount++;
|
|
1148
|
+
}
|
|
1149
|
+
this.queue = this.queue.slice(n);
|
|
1150
|
+
if (this.queue.length === 0) {
|
|
1151
|
+
this.finish(false);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
this.cancelTimer = this.schedule(() => {
|
|
1155
|
+
this.cancelTimer = null;
|
|
1156
|
+
this.drainChunk();
|
|
1157
|
+
}, this.tuning.tickMs);
|
|
1158
|
+
}
|
|
1159
|
+
finish(aborted) {
|
|
1160
|
+
if (this.finished) return;
|
|
1161
|
+
this.finished = true;
|
|
1162
|
+
if (this.cancelTimer) {
|
|
1163
|
+
this.cancelTimer();
|
|
1164
|
+
this.cancelTimer = null;
|
|
1165
|
+
}
|
|
1166
|
+
this.queue = [];
|
|
1167
|
+
this.onDone({
|
|
1168
|
+
packets: this.initialPackets,
|
|
1169
|
+
enqueuedLive: this.enqueuedLive,
|
|
1170
|
+
sent: this.sentCount,
|
|
1171
|
+
chunks: this.chunkCount,
|
|
1172
|
+
chunkSize: this.chunkSize,
|
|
1173
|
+
durationMs: this.now() - this.startedAt,
|
|
1174
|
+
aborted
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
//#endregion
|
|
1179
|
+
//#region src/stream-broker/rtsp/annexb-deframer.ts
|
|
1180
|
+
/**
|
|
1181
|
+
* Annex-B NAL unit deframer.
|
|
1182
|
+
*
|
|
1183
|
+
* ffmpeg outputs Annex-B via stdout in arbitrary chunk sizes — a single
|
|
1184
|
+
* `data` event may contain partial NALs, multiple NALs, or NALs split
|
|
1185
|
+
* across events. This class accumulates bytes and emits complete NAL units.
|
|
1186
|
+
*
|
|
1187
|
+
* Usage:
|
|
1188
|
+
* const deframer = new AnnexBDeframer((nal, isKeyframe) => { ... })
|
|
1189
|
+
* proc.stdout.on('data', chunk => deframer.push(chunk))
|
|
1190
|
+
*/
|
|
1191
|
+
var AnnexBDeframer = class {
|
|
1192
|
+
onNal;
|
|
1193
|
+
buffer = Buffer.alloc(0);
|
|
1194
|
+
codec;
|
|
1195
|
+
constructor(codec, onNal) {
|
|
1196
|
+
this.onNal = onNal;
|
|
1197
|
+
this.codec = codec;
|
|
1198
|
+
}
|
|
1199
|
+
/** Push raw Annex-B bytes. Complete NAL units are emitted via onNal callback. */
|
|
1200
|
+
push(chunk) {
|
|
1201
|
+
this.buffer = this.buffer.length > 0 ? Buffer.concat([this.buffer, chunk]) : chunk;
|
|
1202
|
+
this.drain();
|
|
1203
|
+
}
|
|
1204
|
+
/** Flush remaining buffer (call on stream end). */
|
|
1205
|
+
flush() {
|
|
1206
|
+
if (this.buffer.length > 0) {
|
|
1207
|
+
const nal = this.stripLeadingStartCode(this.buffer);
|
|
1208
|
+
if (nal.length > 0) this.onNal(nal, this.isKeyframeNal(nal));
|
|
1209
|
+
this.buffer = Buffer.alloc(0);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
/** Extract complete NAL units from the buffer, leaving partial data for next push. */
|
|
1213
|
+
drain() {
|
|
1214
|
+
while (true) {
|
|
1215
|
+
const firstSc = this.findStartCode(0);
|
|
1216
|
+
if (firstSc < 0) return;
|
|
1217
|
+
const nalStart = firstSc + (this.isStartCode4(firstSc) ? 4 : 3);
|
|
1218
|
+
const nextSc = this.findStartCode(nalStart);
|
|
1219
|
+
if (nextSc < 0) {
|
|
1220
|
+
if (firstSc > 0) this.buffer = Buffer.from(this.buffer.subarray(firstSc));
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const nal = this.buffer.subarray(nalStart, nextSc);
|
|
1224
|
+
if (nal.length > 0) this.onNal(nal, this.isKeyframeNal(nal));
|
|
1225
|
+
this.buffer = Buffer.from(this.buffer.subarray(nextSc));
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
/** Find the byte offset of the next start code (00 00 01 or 00 00 00 01) at or after `from`. */
|
|
1229
|
+
findStartCode(from) {
|
|
1230
|
+
const buf = this.buffer;
|
|
1231
|
+
const len = buf.length - 2;
|
|
1232
|
+
for (let i = from; i < len; i++) {
|
|
1233
|
+
if (buf[i + 2] > 1) {
|
|
1234
|
+
i += 2;
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
if (buf[i] === 0 && buf[i + 1] === 0) {
|
|
1238
|
+
if (buf[i + 2] === 1) return i;
|
|
1239
|
+
if (buf[i + 2] === 0 && i + 3 < buf.length && buf[i + 3] === 1) return i;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return -1;
|
|
1243
|
+
}
|
|
1244
|
+
/** Check if start code at `pos` is 4-byte (00 00 00 01) vs 3-byte (00 00 01). */
|
|
1245
|
+
isStartCode4(pos) {
|
|
1246
|
+
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;
|
|
1247
|
+
}
|
|
1248
|
+
/** Strip leading start code from a buffer (if present). */
|
|
1249
|
+
stripLeadingStartCode(buf) {
|
|
1250
|
+
if (buf.length >= 4 && buf[0] === 0 && buf[1] === 0 && buf[2] === 0 && buf[3] === 1) return buf.subarray(4);
|
|
1251
|
+
if (buf.length >= 3 && buf[0] === 0 && buf[1] === 0 && buf[2] === 1) return buf.subarray(3);
|
|
1252
|
+
return buf;
|
|
1253
|
+
}
|
|
1254
|
+
/** Check if a NAL unit (without start code) is a keyframe. */
|
|
1255
|
+
isKeyframeNal(nal) {
|
|
1256
|
+
if (nal.length === 0) return false;
|
|
1257
|
+
if (this.codec === "h264") return (nal[0] & 31) === 5;
|
|
1258
|
+
const type = nal[0] >> 1 & 63;
|
|
1259
|
+
return type >= 16 && type <= 21;
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
//#endregion
|
|
1263
|
+
//#region src/stream-broker/rtsp/broker-device-id.ts
|
|
1264
|
+
/**
|
|
1265
|
+
* Parse the numeric deviceId out of a canonical brokerId
|
|
1266
|
+
* (`${deviceId}/${camStreamId}` or `${deviceId}/${profile}`). Returns
|
|
1267
|
+
* `undefined` for a malformed id — so every device-scoped log line on the RTSP
|
|
1268
|
+
* plane can carry `tags.deviceId` without risking a bogus tag value.
|
|
1269
|
+
*
|
|
1270
|
+
* Every log line about a device carries `tags: { deviceId }`, always the same
|
|
1271
|
+
* key and always the numeric id: a miss rate, a media gap or a routing fault is
|
|
1272
|
+
* always asked per-camera ("why is 617 worse than 615?"), and a line that omits
|
|
1273
|
+
* the tag cannot answer it.
|
|
1274
|
+
*/
|
|
1275
|
+
function deviceIdFromBrokerId$1(brokerId) {
|
|
1276
|
+
const parsed = Number(brokerId.split("/")[0]);
|
|
1277
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Extract the `m=audio` media block from an ffmpeg-generated SDP, normalised
|
|
1281
|
+
* for grafting (port 0, non-colliding payload type, own `a=control`, explicit
|
|
1282
|
+
* connection line). Returns `null` when the SDP has no audio media section.
|
|
1283
|
+
*/
|
|
1284
|
+
function extractAudioMediaBlock(ffmpegSdp, options = {}) {
|
|
1285
|
+
const trackId = options.trackId ?? 1;
|
|
1286
|
+
const lines = ffmpegSdp.split(/\r?\n/);
|
|
1287
|
+
const startIdx = lines.findIndex((l) => l.startsWith("m=audio"));
|
|
1288
|
+
if (startIdx < 0) return null;
|
|
1289
|
+
const block = [];
|
|
1290
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
1291
|
+
const line = lines[i];
|
|
1292
|
+
if (i > startIdx && line.startsWith("m=")) break;
|
|
1293
|
+
if (line.trim().length === 0) continue;
|
|
1294
|
+
if (line.startsWith("a=control:")) continue;
|
|
1295
|
+
block.push(line);
|
|
1296
|
+
}
|
|
1297
|
+
const mParts = block[0].split(/\s+/);
|
|
1298
|
+
const origPt = Number.parseInt(mParts[3] ?? "", 10);
|
|
1299
|
+
if (!Number.isInteger(origPt)) return null;
|
|
1300
|
+
const newPt = origPt >= 96 ? 97 : origPt;
|
|
1301
|
+
const remapped = block.map((line) => {
|
|
1302
|
+
if (line.startsWith("m=audio")) return `m=audio 0 ${mParts.slice(2).join(" ").replace(String(origPt), String(newPt))}`;
|
|
1303
|
+
if (origPt !== newPt && (line.startsWith("a=rtpmap:") || line.startsWith("a=fmtp:"))) return line.replace(`:${origPt} `, `:${newPt} `);
|
|
1304
|
+
return line;
|
|
1305
|
+
});
|
|
1306
|
+
if (!remapped.some((l) => l.startsWith("c="))) remapped.splice(1, 0, "c=IN IP4 0.0.0.0");
|
|
1307
|
+
remapped.push(`a=control:trackID=${trackId}`);
|
|
1308
|
+
return remapped.join("\r\n");
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Append a grafted audio media block to a video-only SDP, preserving CRLF
|
|
1312
|
+
* framing and the trailing blank line.
|
|
1313
|
+
*/
|
|
1314
|
+
function appendAudioMediaBlock(videoSdp, audioBlock) {
|
|
1315
|
+
return `${videoSdp.replace(/[\r\n]+$/, "")}\r\n${audioBlock}\r\n`;
|
|
1316
|
+
}
|
|
1317
|
+
//#endregion
|
|
1049
1318
|
//#region src/stream-broker/rtsp/rtsp-types.ts
|
|
1050
1319
|
/** RTSP method constants. */
|
|
1051
1320
|
var RTSP_METHODS = [
|
|
@@ -1133,129 +1402,45 @@ var RtpPacketizer = class {
|
|
|
1133
1402
|
packets.push(this.buildRtpPacket(fragment, timestamp, isLast && marker));
|
|
1134
1403
|
offset = end;
|
|
1135
1404
|
}
|
|
1136
|
-
return packets;
|
|
1137
|
-
}
|
|
1138
|
-
/** H.265 FU fragmentation (RFC 7798 Section 4.4.3). */
|
|
1139
|
-
fragmentH265FU(nal, timestamp, marker) {
|
|
1140
|
-
const nalType = nal[0] >> 1 & 63;
|
|
1141
|
-
const tidLayerId = nal[1];
|
|
1142
|
-
const payload = nal.subarray(2);
|
|
1143
|
-
const maxFragment = RTP_MAX_PAYLOAD - 3;
|
|
1144
|
-
const packets = [];
|
|
1145
|
-
let offset = 0;
|
|
1146
|
-
while (offset < payload.length) {
|
|
1147
|
-
const end = Math.min(offset + maxFragment, payload.length);
|
|
1148
|
-
const isFirst = offset === 0;
|
|
1149
|
-
const isLast = end === payload.length;
|
|
1150
|
-
const payloadHdr0 = 98 | nal[0] & 129;
|
|
1151
|
-
const payloadHdr1 = tidLayerId;
|
|
1152
|
-
const fuHeader = (isFirst ? 128 : 0) | (isLast ? 64 : 0) | nalType;
|
|
1153
|
-
const fragment = Buffer.allocUnsafe(3 + (end - offset));
|
|
1154
|
-
fragment[0] = payloadHdr0;
|
|
1155
|
-
fragment[1] = payloadHdr1;
|
|
1156
|
-
fragment[2] = fuHeader;
|
|
1157
|
-
payload.copy(fragment, 3, offset, end);
|
|
1158
|
-
packets.push(this.buildRtpPacket(fragment, timestamp, isLast && marker));
|
|
1159
|
-
offset = end;
|
|
1160
|
-
}
|
|
1161
|
-
return packets;
|
|
1162
|
-
}
|
|
1163
|
-
buildRtpPacket(payload, timestamp, marker) {
|
|
1164
|
-
const header = Buffer.allocUnsafe(12);
|
|
1165
|
-
header[0] = 128;
|
|
1166
|
-
header[1] = (marker ? 128 : 0) | this.payloadType & 127;
|
|
1167
|
-
header.writeUInt16BE(this.sequenceNumber & 65535, 2);
|
|
1168
|
-
this.sequenceNumber = this.sequenceNumber + 1 & 65535;
|
|
1169
|
-
header.writeUInt32BE(timestamp >>> 0, 4);
|
|
1170
|
-
header.writeUInt32BE(this.ssrc >>> 0, 8);
|
|
1171
|
-
return {
|
|
1172
|
-
data: Buffer.concat([header, payload]),
|
|
1173
|
-
marker
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
|
-
};
|
|
1177
|
-
//#endregion
|
|
1178
|
-
//#region src/stream-broker/rtsp/annexb-deframer.ts
|
|
1179
|
-
/**
|
|
1180
|
-
* Annex-B NAL unit deframer.
|
|
1181
|
-
*
|
|
1182
|
-
* ffmpeg outputs Annex-B via stdout in arbitrary chunk sizes — a single
|
|
1183
|
-
* `data` event may contain partial NALs, multiple NALs, or NALs split
|
|
1184
|
-
* across events. This class accumulates bytes and emits complete NAL units.
|
|
1185
|
-
*
|
|
1186
|
-
* Usage:
|
|
1187
|
-
* const deframer = new AnnexBDeframer((nal, isKeyframe) => { ... })
|
|
1188
|
-
* proc.stdout.on('data', chunk => deframer.push(chunk))
|
|
1189
|
-
*/
|
|
1190
|
-
var AnnexBDeframer = class {
|
|
1191
|
-
onNal;
|
|
1192
|
-
buffer = Buffer.alloc(0);
|
|
1193
|
-
codec;
|
|
1194
|
-
constructor(codec, onNal) {
|
|
1195
|
-
this.onNal = onNal;
|
|
1196
|
-
this.codec = codec;
|
|
1197
|
-
}
|
|
1198
|
-
/** Push raw Annex-B bytes. Complete NAL units are emitted via onNal callback. */
|
|
1199
|
-
push(chunk) {
|
|
1200
|
-
this.buffer = this.buffer.length > 0 ? Buffer.concat([this.buffer, chunk]) : chunk;
|
|
1201
|
-
this.drain();
|
|
1202
|
-
}
|
|
1203
|
-
/** Flush remaining buffer (call on stream end). */
|
|
1204
|
-
flush() {
|
|
1205
|
-
if (this.buffer.length > 0) {
|
|
1206
|
-
const nal = this.stripLeadingStartCode(this.buffer);
|
|
1207
|
-
if (nal.length > 0) this.onNal(nal, this.isKeyframeNal(nal));
|
|
1208
|
-
this.buffer = Buffer.alloc(0);
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
/** Extract complete NAL units from the buffer, leaving partial data for next push. */
|
|
1212
|
-
drain() {
|
|
1213
|
-
while (true) {
|
|
1214
|
-
const firstSc = this.findStartCode(0);
|
|
1215
|
-
if (firstSc < 0) return;
|
|
1216
|
-
const nalStart = firstSc + (this.isStartCode4(firstSc) ? 4 : 3);
|
|
1217
|
-
const nextSc = this.findStartCode(nalStart);
|
|
1218
|
-
if (nextSc < 0) {
|
|
1219
|
-
if (firstSc > 0) this.buffer = Buffer.from(this.buffer.subarray(firstSc));
|
|
1220
|
-
return;
|
|
1221
|
-
}
|
|
1222
|
-
const nal = this.buffer.subarray(nalStart, nextSc);
|
|
1223
|
-
if (nal.length > 0) this.onNal(nal, this.isKeyframeNal(nal));
|
|
1224
|
-
this.buffer = Buffer.from(this.buffer.subarray(nextSc));
|
|
1225
|
-
}
|
|
1405
|
+
return packets;
|
|
1226
1406
|
}
|
|
1227
|
-
/**
|
|
1228
|
-
|
|
1229
|
-
const
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1407
|
+
/** H.265 FU fragmentation (RFC 7798 Section 4.4.3). */
|
|
1408
|
+
fragmentH265FU(nal, timestamp, marker) {
|
|
1409
|
+
const nalType = nal[0] >> 1 & 63;
|
|
1410
|
+
const tidLayerId = nal[1];
|
|
1411
|
+
const payload = nal.subarray(2);
|
|
1412
|
+
const maxFragment = RTP_MAX_PAYLOAD - 3;
|
|
1413
|
+
const packets = [];
|
|
1414
|
+
let offset = 0;
|
|
1415
|
+
while (offset < payload.length) {
|
|
1416
|
+
const end = Math.min(offset + maxFragment, payload.length);
|
|
1417
|
+
const isFirst = offset === 0;
|
|
1418
|
+
const isLast = end === payload.length;
|
|
1419
|
+
const payloadHdr0 = 98 | nal[0] & 129;
|
|
1420
|
+
const payloadHdr1 = tidLayerId;
|
|
1421
|
+
const fuHeader = (isFirst ? 128 : 0) | (isLast ? 64 : 0) | nalType;
|
|
1422
|
+
const fragment = Buffer.allocUnsafe(3 + (end - offset));
|
|
1423
|
+
fragment[0] = payloadHdr0;
|
|
1424
|
+
fragment[1] = payloadHdr1;
|
|
1425
|
+
fragment[2] = fuHeader;
|
|
1426
|
+
payload.copy(fragment, 3, offset, end);
|
|
1427
|
+
packets.push(this.buildRtpPacket(fragment, timestamp, isLast && marker));
|
|
1428
|
+
offset = end;
|
|
1240
1429
|
}
|
|
1241
|
-
return
|
|
1242
|
-
}
|
|
1243
|
-
/** Check if start code at `pos` is 4-byte (00 00 00 01) vs 3-byte (00 00 01). */
|
|
1244
|
-
isStartCode4(pos) {
|
|
1245
|
-
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;
|
|
1246
|
-
}
|
|
1247
|
-
/** Strip leading start code from a buffer (if present). */
|
|
1248
|
-
stripLeadingStartCode(buf) {
|
|
1249
|
-
if (buf.length >= 4 && buf[0] === 0 && buf[1] === 0 && buf[2] === 0 && buf[3] === 1) return buf.subarray(4);
|
|
1250
|
-
if (buf.length >= 3 && buf[0] === 0 && buf[1] === 0 && buf[2] === 1) return buf.subarray(3);
|
|
1251
|
-
return buf;
|
|
1430
|
+
return packets;
|
|
1252
1431
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1432
|
+
buildRtpPacket(payload, timestamp, marker) {
|
|
1433
|
+
const header = Buffer.allocUnsafe(12);
|
|
1434
|
+
header[0] = 128;
|
|
1435
|
+
header[1] = (marker ? 128 : 0) | this.payloadType & 127;
|
|
1436
|
+
header.writeUInt16BE(this.sequenceNumber & 65535, 2);
|
|
1437
|
+
this.sequenceNumber = this.sequenceNumber + 1 & 65535;
|
|
1438
|
+
header.writeUInt32BE(timestamp >>> 0, 4);
|
|
1439
|
+
header.writeUInt32BE(this.ssrc >>> 0, 8);
|
|
1440
|
+
return {
|
|
1441
|
+
data: Buffer.concat([header, payload]),
|
|
1442
|
+
marker
|
|
1443
|
+
};
|
|
1259
1444
|
}
|
|
1260
1445
|
};
|
|
1261
1446
|
//#endregion
|
|
@@ -1293,191 +1478,6 @@ function buildSdp(options) {
|
|
|
1293
1478
|
lines.push("");
|
|
1294
1479
|
return lines.join("\r\n");
|
|
1295
1480
|
}
|
|
1296
|
-
/**
|
|
1297
|
-
* Extract the `m=audio` media block from an ffmpeg-generated SDP, normalised
|
|
1298
|
-
* for grafting (port 0, non-colliding payload type, own `a=control`, explicit
|
|
1299
|
-
* connection line). Returns `null` when the SDP has no audio media section.
|
|
1300
|
-
*/
|
|
1301
|
-
function extractAudioMediaBlock(ffmpegSdp, options = {}) {
|
|
1302
|
-
const trackId = options.trackId ?? 1;
|
|
1303
|
-
const lines = ffmpegSdp.split(/\r?\n/);
|
|
1304
|
-
const startIdx = lines.findIndex((l) => l.startsWith("m=audio"));
|
|
1305
|
-
if (startIdx < 0) return null;
|
|
1306
|
-
const block = [];
|
|
1307
|
-
for (let i = startIdx; i < lines.length; i++) {
|
|
1308
|
-
const line = lines[i];
|
|
1309
|
-
if (i > startIdx && line.startsWith("m=")) break;
|
|
1310
|
-
if (line.trim().length === 0) continue;
|
|
1311
|
-
if (line.startsWith("a=control:")) continue;
|
|
1312
|
-
block.push(line);
|
|
1313
|
-
}
|
|
1314
|
-
const mParts = block[0].split(/\s+/);
|
|
1315
|
-
const origPt = Number.parseInt(mParts[3] ?? "", 10);
|
|
1316
|
-
if (!Number.isInteger(origPt)) return null;
|
|
1317
|
-
const newPt = origPt >= 96 ? 97 : origPt;
|
|
1318
|
-
const remapped = block.map((line) => {
|
|
1319
|
-
if (line.startsWith("m=audio")) return `m=audio 0 ${mParts.slice(2).join(" ").replace(String(origPt), String(newPt))}`;
|
|
1320
|
-
if (origPt !== newPt && (line.startsWith("a=rtpmap:") || line.startsWith("a=fmtp:"))) return line.replace(`:${origPt} `, `:${newPt} `);
|
|
1321
|
-
return line;
|
|
1322
|
-
});
|
|
1323
|
-
if (!remapped.some((l) => l.startsWith("c="))) remapped.splice(1, 0, "c=IN IP4 0.0.0.0");
|
|
1324
|
-
remapped.push(`a=control:trackID=${trackId}`);
|
|
1325
|
-
return remapped.join("\r\n");
|
|
1326
|
-
}
|
|
1327
|
-
/**
|
|
1328
|
-
* Append a grafted audio media block to a video-only SDP, preserving CRLF
|
|
1329
|
-
* framing and the trailing blank line.
|
|
1330
|
-
*/
|
|
1331
|
-
function appendAudioMediaBlock(videoSdp, audioBlock) {
|
|
1332
|
-
return `${videoSdp.replace(/[\r\n]+$/, "")}\r\n${audioBlock}\r\n`;
|
|
1333
|
-
}
|
|
1334
|
-
//#endregion
|
|
1335
|
-
//#region src/stream-broker/rtsp/broker-device-id.ts
|
|
1336
|
-
/**
|
|
1337
|
-
* Parse the numeric deviceId out of a canonical brokerId
|
|
1338
|
-
* (`${deviceId}/${camStreamId}` or `${deviceId}/${profile}`). Returns
|
|
1339
|
-
* `undefined` for a malformed id — so every device-scoped log line on the RTSP
|
|
1340
|
-
* plane can carry `tags.deviceId` without risking a bogus tag value.
|
|
1341
|
-
*
|
|
1342
|
-
* Every log line about a device carries `tags: { deviceId }`, always the same
|
|
1343
|
-
* key and always the numeric id: a miss rate, a media gap or a routing fault is
|
|
1344
|
-
* always asked per-camera ("why is 617 worse than 615?"), and a line that omits
|
|
1345
|
-
* the tag cannot answer it.
|
|
1346
|
-
*/
|
|
1347
|
-
function deviceIdFromBrokerId$1(brokerId) {
|
|
1348
|
-
const parsed = Number(brokerId.split("/")[0]);
|
|
1349
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1350
|
-
}
|
|
1351
|
-
//#endregion
|
|
1352
|
-
//#region src/stream-broker/paced-rtp-replay.ts
|
|
1353
|
-
var PACED_REPLAY_DEFAULTS = {
|
|
1354
|
-
singleShotMax: 64,
|
|
1355
|
-
tickMs: 5,
|
|
1356
|
-
targetDurationMs: 300,
|
|
1357
|
-
minChunkSize: 32,
|
|
1358
|
-
maxChunkSize: 128
|
|
1359
|
-
};
|
|
1360
|
-
var defaultSchedule = (fn, ms) => {
|
|
1361
|
-
const t = setTimeout(fn, ms);
|
|
1362
|
-
return () => clearTimeout(t);
|
|
1363
|
-
};
|
|
1364
|
-
/**
|
|
1365
|
-
* Compute the per-tick chunk size for a burst of `total` packets: enough to
|
|
1366
|
-
* drain within `targetDurationMs` at one chunk per `tickMs`, clamped to
|
|
1367
|
-
* [minChunkSize, maxChunkSize]. Bursts ≤ `singleShotMax` are one chunk.
|
|
1368
|
-
*/
|
|
1369
|
-
function computeReplayChunkSize(total, tuning) {
|
|
1370
|
-
if (total <= tuning.singleShotMax) return Math.max(total, 1);
|
|
1371
|
-
const maxTicks = Math.max(1, Math.floor(tuning.targetDurationMs / tuning.tickMs));
|
|
1372
|
-
const ideal = Math.ceil(total / maxTicks);
|
|
1373
|
-
return Math.min(tuning.maxChunkSize, Math.max(tuning.minChunkSize, ideal));
|
|
1374
|
-
}
|
|
1375
|
-
/**
|
|
1376
|
-
* One in-flight paced replay. Create per burst; not reusable after
|
|
1377
|
-
* completion or abort.
|
|
1378
|
-
*/
|
|
1379
|
-
var PacedRtpReplay = class {
|
|
1380
|
-
send;
|
|
1381
|
-
onDone;
|
|
1382
|
-
tuning;
|
|
1383
|
-
schedule;
|
|
1384
|
-
now;
|
|
1385
|
-
queue = [];
|
|
1386
|
-
cancelTimer = null;
|
|
1387
|
-
started = false;
|
|
1388
|
-
finished = false;
|
|
1389
|
-
startedAt = 0;
|
|
1390
|
-
chunkSize = 0;
|
|
1391
|
-
initialPackets = 0;
|
|
1392
|
-
enqueuedLive = 0;
|
|
1393
|
-
sentCount = 0;
|
|
1394
|
-
chunkCount = 0;
|
|
1395
|
-
constructor(send, onDone, options) {
|
|
1396
|
-
this.send = send;
|
|
1397
|
-
this.onDone = onDone;
|
|
1398
|
-
this.tuning = {
|
|
1399
|
-
...PACED_REPLAY_DEFAULTS,
|
|
1400
|
-
...options?.tuning
|
|
1401
|
-
};
|
|
1402
|
-
this.schedule = options?.schedule ?? defaultSchedule;
|
|
1403
|
-
this.now = options?.now ?? Date.now;
|
|
1404
|
-
}
|
|
1405
|
-
/** True while packets remain to be drained (live packets must be enqueued,
|
|
1406
|
-
* not sent directly, to preserve order). */
|
|
1407
|
-
get active() {
|
|
1408
|
-
return this.started && !this.finished;
|
|
1409
|
-
}
|
|
1410
|
-
/**
|
|
1411
|
-
* Begin the replay: sends the first chunk synchronously, then paces the
|
|
1412
|
-
* rest. Calling `start` more than once is a no-op.
|
|
1413
|
-
*/
|
|
1414
|
-
start(initial) {
|
|
1415
|
-
if (this.started) return;
|
|
1416
|
-
this.started = true;
|
|
1417
|
-
this.startedAt = this.now();
|
|
1418
|
-
this.initialPackets = initial.length;
|
|
1419
|
-
this.queue = [...initial];
|
|
1420
|
-
this.chunkSize = computeReplayChunkSize(initial.length, this.tuning);
|
|
1421
|
-
this.drainChunk();
|
|
1422
|
-
}
|
|
1423
|
-
/**
|
|
1424
|
-
* Append a live packet behind the still-queued replay tail. No-op (returns
|
|
1425
|
-
* false) when the replay is not active — the caller must then send the
|
|
1426
|
-
* packet directly.
|
|
1427
|
-
*/
|
|
1428
|
-
enqueue(item) {
|
|
1429
|
-
if (!this.active) return false;
|
|
1430
|
-
this.queue.push(item);
|
|
1431
|
-
this.enqueuedLive++;
|
|
1432
|
-
return true;
|
|
1433
|
-
}
|
|
1434
|
-
/** Cancel the replay: pending packets are dropped, `onDone` fires with
|
|
1435
|
-
* `aborted: true`. Safe to call multiple times / before `start`. */
|
|
1436
|
-
abort() {
|
|
1437
|
-
if (this.finished) return;
|
|
1438
|
-
if (!this.started) {
|
|
1439
|
-
this.started = true;
|
|
1440
|
-
this.startedAt = this.now();
|
|
1441
|
-
}
|
|
1442
|
-
this.finish(true);
|
|
1443
|
-
}
|
|
1444
|
-
drainChunk() {
|
|
1445
|
-
if (this.finished) return;
|
|
1446
|
-
this.chunkCount++;
|
|
1447
|
-
const n = Math.min(this.chunkSize, this.queue.length);
|
|
1448
|
-
for (let i = 0; i < n; i++) {
|
|
1449
|
-
if (this.finished) return;
|
|
1450
|
-
if (this.send(this.queue[i])) this.sentCount++;
|
|
1451
|
-
}
|
|
1452
|
-
this.queue = this.queue.slice(n);
|
|
1453
|
-
if (this.queue.length === 0) {
|
|
1454
|
-
this.finish(false);
|
|
1455
|
-
return;
|
|
1456
|
-
}
|
|
1457
|
-
this.cancelTimer = this.schedule(() => {
|
|
1458
|
-
this.cancelTimer = null;
|
|
1459
|
-
this.drainChunk();
|
|
1460
|
-
}, this.tuning.tickMs);
|
|
1461
|
-
}
|
|
1462
|
-
finish(aborted) {
|
|
1463
|
-
if (this.finished) return;
|
|
1464
|
-
this.finished = true;
|
|
1465
|
-
if (this.cancelTimer) {
|
|
1466
|
-
this.cancelTimer();
|
|
1467
|
-
this.cancelTimer = null;
|
|
1468
|
-
}
|
|
1469
|
-
this.queue = [];
|
|
1470
|
-
this.onDone({
|
|
1471
|
-
packets: this.initialPackets,
|
|
1472
|
-
enqueuedLive: this.enqueuedLive,
|
|
1473
|
-
sent: this.sentCount,
|
|
1474
|
-
chunks: this.chunkCount,
|
|
1475
|
-
chunkSize: this.chunkSize,
|
|
1476
|
-
durationMs: this.now() - this.startedAt,
|
|
1477
|
-
aborted
|
|
1478
|
-
});
|
|
1479
|
-
}
|
|
1480
|
-
};
|
|
1481
1481
|
//#endregion
|
|
1482
1482
|
//#region src/stream-broker/rtsp/rtsp-restreamer.ts
|
|
1483
1483
|
/**
|
|
@@ -1643,6 +1643,20 @@ var RtspRestreamer = class {
|
|
|
1643
1643
|
/** Throttled drop/withhold accounting — see {@link reportDrops}. */
|
|
1644
1644
|
dropsWithheldSpan = 0;
|
|
1645
1645
|
dropsWithheldPackets = 0;
|
|
1646
|
+
/**
|
|
1647
|
+
* Muted (local-decoder loopback) joins refused the ring burst because it was
|
|
1648
|
+
* not near-live — see {@link LIVE_EDGE_MAX_SPAN_MS}. Its own counter: this is
|
|
1649
|
+
* a different rule from the span/packet bound, it fires on a DIFFERENT
|
|
1650
|
+
* population (only decode sessions), and conflating it with
|
|
1651
|
+
* {@link dropsWithheldSpan} would hide that a camera's decoder starts blind
|
|
1652
|
+
* on nearly every dial while its viewers are served normally.
|
|
1653
|
+
*
|
|
1654
|
+
* Counts withhold DECISIONS, not joins — `servePendingSessionsRtp` runs per
|
|
1655
|
+
* packet, so one session withheld for a whole GOP contributes many. Same
|
|
1656
|
+
* semantics as {@link dropsWithheldSpan}; the signal to read is "nonzero on
|
|
1657
|
+
* THIS camera while its viewers are being served".
|
|
1658
|
+
*/
|
|
1659
|
+
dropsWithheldMutedLiveEdge = 0;
|
|
1646
1660
|
dropsPrimeBacklog = 0;
|
|
1647
1661
|
dropsLiveLatched = 0;
|
|
1648
1662
|
dropsLiveClosed = 0;
|
|
@@ -1911,7 +1925,11 @@ var RtspRestreamer = class {
|
|
|
1911
1925
|
const session = this.sessions.get(sessionId);
|
|
1912
1926
|
if (!session) continue;
|
|
1913
1927
|
if (session.isMuted()) {
|
|
1914
|
-
if (!fromRing || spanMs > LIVE_EDGE_MAX_SPAN_MS)
|
|
1928
|
+
if (!fromRing || spanMs > LIVE_EDGE_MAX_SPAN_MS) {
|
|
1929
|
+
this.dropsWithheldMutedLiveEdge++;
|
|
1930
|
+
withheld++;
|
|
1931
|
+
continue;
|
|
1932
|
+
}
|
|
1915
1933
|
} else if (overSpan || overPackets) {
|
|
1916
1934
|
if (overSpan) this.dropsWithheldSpan++;
|
|
1917
1935
|
else this.dropsWithheldPackets++;
|
|
@@ -2034,6 +2052,8 @@ var RtspRestreamer = class {
|
|
|
2034
2052
|
boundPackets: bound.maxPackets,
|
|
2035
2053
|
primeWithheldSpanMs: this.dropsWithheldSpan,
|
|
2036
2054
|
primeWithheldPackets: this.dropsWithheldPackets,
|
|
2055
|
+
primeWithheldMutedLiveEdge: this.dropsWithheldMutedLiveEdge,
|
|
2056
|
+
liveEdgeMaxSpanMs: LIVE_EDGE_MAX_SPAN_MS,
|
|
2037
2057
|
primeAbortedOnBacklog: this.dropsPrimeBacklog,
|
|
2038
2058
|
liveDroppedUntilKeyframe: this.dropsLiveLatched,
|
|
2039
2059
|
clientsClosedOnBacklog: this.dropsLiveClosed,
|
|
@@ -2042,6 +2062,7 @@ var RtspRestreamer = class {
|
|
|
2042
2062
|
});
|
|
2043
2063
|
this.dropsWithheldSpan = 0;
|
|
2044
2064
|
this.dropsWithheldPackets = 0;
|
|
2065
|
+
this.dropsWithheldMutedLiveEdge = 0;
|
|
2045
2066
|
this.dropsPrimeBacklog = 0;
|
|
2046
2067
|
this.dropsLiveLatched = 0;
|
|
2047
2068
|
this.dropsLiveClosed = 0;
|
|
@@ -13066,7 +13087,7 @@ var TranscodePipelineManager = class {
|
|
|
13066
13087
|
}
|
|
13067
13088
|
});
|
|
13068
13089
|
const passVideoCodec = sourceCodec ?? "H264";
|
|
13069
|
-
const url = this.resolveSourceUrl(input.deviceId, sourceStream.camStreamId);
|
|
13090
|
+
const url = this.resolveSourceUrl(input.deviceId, sourceStream.camStreamId, wantedAudio === "none");
|
|
13070
13091
|
const key = pipelineKeyFor(input.deviceId, sourceStream.camStreamId, "copy", wantedAudio, outResolution, outputArgs, false);
|
|
13071
13092
|
return this.shareOrCreatePassthrough(key, () => ({
|
|
13072
13093
|
url,
|
|
@@ -13222,10 +13243,11 @@ var TranscodePipelineManager = class {
|
|
|
13222
13243
|
* source dial. Throws when no restream entry exists — a targeted source
|
|
13223
13244
|
* must be dialed before it can be consumed.
|
|
13224
13245
|
*/
|
|
13225
|
-
resolveSourceUrl(deviceId, camStreamId) {
|
|
13246
|
+
resolveSourceUrl(deviceId, camStreamId, videoOnly = false) {
|
|
13226
13247
|
const brokerId = require_dist.makeSourceBrokerId(deviceId, camStreamId);
|
|
13227
13248
|
const entry = this.rtspEntryLookup(brokerId);
|
|
13228
|
-
|
|
13249
|
+
const url = videoOnly ? entry?.mutedUrl ?? entry?.url : entry?.url;
|
|
13250
|
+
if (url) return url;
|
|
13229
13251
|
throw new Error(`getStreamWithCodec: no broker restream for ${brokerId} (source not dialed)`);
|
|
13230
13252
|
}
|
|
13231
13253
|
/**
|
|
@@ -13842,7 +13864,10 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
13842
13864
|
cameraStreamLookup: (deviceId) => this.getCameraStreamsForDevice(deviceId),
|
|
13843
13865
|
rtspEntryLookup: (brokerId) => {
|
|
13844
13866
|
const entry = this.rtspProvider.getEntry(brokerId);
|
|
13845
|
-
return entry ? {
|
|
13867
|
+
return entry ? {
|
|
13868
|
+
url: entry.url,
|
|
13869
|
+
mutedUrl: entry.mutedUrl
|
|
13870
|
+
} : null;
|
|
13846
13871
|
},
|
|
13847
13872
|
resolveProfileSource: (deviceId, profile) => this.assignments.get(deviceId)?.map[profile] ?? null,
|
|
13848
13873
|
ffmpegConfig: () => this.ffmpegConfig,
|