@camstack/addon-pipeline 1.2.50 → 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.
Files changed (32) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/detection-pipeline/index.js +2 -2
  4. package/dist/detection-pipeline/index.mjs +2 -2
  5. package/dist/{dist-Ci2-N-XC.js → dist-CzFJeiG1.js} +16214 -16021
  6. package/dist/{dist-hRt5QB5i.mjs → dist-gB3o0cyM.mjs} +16122 -15935
  7. package/dist/{event-loop-stall-monitor-B_EOn2Q1.js → event-loop-stall-monitor-DSPDQYjc.js} +1 -1
  8. package/dist/{event-loop-stall-monitor-DUSB3ymZ.mjs → event-loop-stall-monitor-PxIaVeyh.mjs} +1 -1
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/pipeline-runner/index.js +88 -18
  12. package/dist/pipeline-runner/index.mjs +88 -18
  13. package/dist/recorder/index.js +91 -4
  14. package/dist/recorder/index.mjs +91 -4
  15. package/dist/session-decode/decode-worker-child.js +1 -1
  16. package/dist/session-decode/decode-worker-child.mjs +1 -1
  17. package/dist/stream-broker/_stub.js +1 -1
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DwYxOvHx.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DAeBhR8B.mjs} +2 -2
  19. 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
  20. package/dist/stream-broker/{hostInit-yLEcTDS8.mjs → hostInit-DniAXKLy.mjs} +2 -2
  21. package/dist/stream-broker/index.js +336 -311
  22. package/dist/stream-broker/index.mjs +336 -311
  23. package/dist/stream-broker/remoteEntry.js +1 -1
  24. package/dist/{worker-protocol-CxpdncMB.js → worker-protocol-BQlELGzv.js} +1 -1
  25. package/dist/{worker-protocol-D_6eAFbv.mjs → worker-protocol-BhhX7J4-.mjs} +1 -1
  26. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-kAtChdz2.js → MaskShapeCanvas-DI4BY7W2-B1gpA9sI.js} +1 -1
  27. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DEwkvjzt.js → MotionZonesSettings-NcxxQN8r-DV6vvNDq.js} +1 -1
  28. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-Brv9VL0h.js → PrivacyMaskSettings-APgPLF7p-BmUdRoZ9.js} +1 -1
  29. package/embed-dist/assets/{index-Dv7fSuDA.js → index-DsGzhGCW.js} +12 -12
  30. package/embed-dist/index.html +1 -1
  31. package/package.json +1 -1
  32. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-FL2mrnl7.mjs +0 -26
@@ -3,7 +3,7 @@ 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-Ci2-N-XC.js");
6
+ const require_dist = require("../dist-CzFJeiG1.js");
7
7
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
8
8
  const require_addon_utils = require("../addon-utils-DveoBR6G.js");
9
9
  let node_crypto = require("node:crypto");
@@ -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
- /** Find the byte offset of the next start code (00 00 01 or 00 00 00 01) at or after `from`. */
1228
- findStartCode(from) {
1229
- const buf = this.buffer;
1230
- const len = buf.length - 2;
1231
- for (let i = from; i < len; i++) {
1232
- if (buf[i + 2] > 1) {
1233
- i += 2;
1234
- continue;
1235
- }
1236
- if (buf[i] === 0 && buf[i + 1] === 0) {
1237
- if (buf[i + 2] === 1) return i;
1238
- if (buf[i + 2] === 0 && i + 3 < buf.length && buf[i + 3] === 1) return i;
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 -1;
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
- /** Check if a NAL unit (without start code) is a keyframe. */
1254
- isKeyframeNal(nal) {
1255
- if (nal.length === 0) return false;
1256
- if (this.codec === "h264") return (nal[0] & 31) === 5;
1257
- const type = nal[0] >> 1 & 63;
1258
- return type >= 16 && type <= 21;
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) continue;
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
- if (entry?.url) return entry.url;
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 ? { url: entry.url } : null;
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,