@camstack/addon-pipeline 1.2.199 → 1.2.200
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/{default-detection-model-CMe44Kid.js → default-detection-model-BsmxtyZx.js} +1 -1
- package/dist/{default-detection-model-O1uBusV2.mjs → default-detection-model-c1LY6thP.mjs} +1 -1
- package/dist/detection-pipeline/index.js +5 -5
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-DmCrC2yx.js → dist-D2YTTkRf.js} +424 -426
- package/dist/{dist-B23u1tmN.mjs → dist-mEsrAAc3.mjs} +424 -426
- package/dist/{lazy-sharp-DBHsD2lh.js → lazy-sharp-B5bW34uC.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-D1MLn6oV.mjs → node-CUxep5tc.mjs} +1 -1
- package/dist/{node-DCSTbt6C.js → node-x3YokJkD.js} +1 -1
- package/dist/pipeline-runner/index.js +8 -7
- package/dist/pipeline-runner/index.mjs +7 -6
- package/dist/{process-memory-P24NTDb1.mjs → process-memory-BCqpTruN.mjs} +1 -1
- package/dist/{process-memory-9lvvhHym.js → process-memory-CFc4D9A2.js} +1 -1
- package/dist/recorder/index.js +169 -334
- package/dist/recorder/index.mjs +164 -329
- package/dist/restream-intent-B1Difcnq.js +313 -0
- package/dist/restream-intent-DWe2kInY.mjs +248 -0
- package/dist/{prebuffer-DmBYRQWg.mjs → retire-root-keys-Csb8z2f3.js} +12 -64
- package/dist/{prebuffer-DqdeLSsk.js → retire-root-keys-DZvlso2l.mjs} +1 -99
- package/dist/{segment-demux-js-Ck_F5bsO.js → segment-demux-js-DUZVURQd.js} +1 -1
- package/dist/{segment-demux-js-Bm0woPkn.mjs → segment-demux-js-xHtB450F.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +25 -5
- package/dist/session-decode/decode-worker-child.mjs +24 -4
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Sq93hI-g.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CLXImQ3R.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-NqqfpkYF.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-Dj_qoc1e.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D62O5bRa.mjs} +1 -1
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-COt259tU.mjs → hostInit-D_cZQhBA.mjs} +3 -3
- package/dist/stream-broker/index.js +181 -48
- package/dist/stream-broker/index.mjs +172 -39
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-c1r1Yddg.js → worker-protocol-CRDaBlJl.js} +1 -1
- package/dist/{worker-protocol-BxpGZ0Dt.mjs → worker-protocol-oUamwZSn.mjs} +1 -1
- package/package.json +1 -1
- package/dist/restream-intent-B4BXZra7.mjs +0 -72
- package/dist/restream-intent-Cv9x3jmu.js +0 -89
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BcjLrxhF.mjs +0 -26
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { A as addonWidgetsSourceCapability, C as RATE_CONTROL_RELAXED, Dt as webrtcSessionCapability, F as cameraStreamsCapability, Ft as BaseAddon, H as deriveBatteryPresence, Ht as makeProfileBrokerId, I as createHwAccelCache, It as CAM_PROFILE_ORDER, K as egressTranscodeSharingKey, Kt as parseProfileBrokerId, L as createLogChannelsProvider, Lt as DeviceFeature, Mt as invocationFromEncodeProfile, Nt as isSoftwareDecode, O as RingBuffer, Ot as errMsg, P as batteryCapability, Pt as BOOT_RECOVERY_BACKOFF_MS, Qt as discriminatedUnion, Rt as DeviceType, Ut as makeSourceBrokerId, Vt as isEvent, Wt as nodePin, Xt as array, Yt as _enum, Zt as boolean, an as string, en as literal, h as EncodeProfileSchema, i as BatteryStatusSchema, in as record, it as maskUrlCredentials, jt as buildFfmpegArgs$1, kt as AUDIO_PRESETS, nn as object, nt as logChannelsCapability, on as union, q as egressTransportFromRequest, sn as EventCategory, tn as number, tt as loadContributionCapability, w as RATE_CONTROL_TIGHT, wt as streamBrokerCapability, yt as resolveEgressDecodeHwAccel, z as declareLogChannel, zt as createEvent } from "../dist-
|
|
3
|
-
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-
|
|
4
|
-
import {
|
|
2
|
+
import { A as addonWidgetsSourceCapability, C as RATE_CONTROL_RELAXED, Dt as webrtcSessionCapability, F as cameraStreamsCapability, Ft as BaseAddon, H as deriveBatteryPresence, Ht as makeProfileBrokerId, I as createHwAccelCache, It as CAM_PROFILE_ORDER, K as egressTranscodeSharingKey, Kt as parseProfileBrokerId, L as createLogChannelsProvider, Lt as DeviceFeature, Mt as invocationFromEncodeProfile, Nt as isSoftwareDecode, O as RingBuffer, Ot as errMsg, P as batteryCapability, Pt as BOOT_RECOVERY_BACKOFF_MS, Qt as discriminatedUnion, Rt as DeviceType, Ut as makeSourceBrokerId, Vt as isEvent, Wt as nodePin, Xt as array, Yt as _enum, Zt as boolean, an as string, en as literal, h as EncodeProfileSchema, i as BatteryStatusSchema, in as record, it as maskUrlCredentials, jt as buildFfmpegArgs$1, kt as AUDIO_PRESETS, nn as object, nt as logChannelsCapability, on as union, q as egressTransportFromRequest, sn as EventCategory, tn as number, tt as loadContributionCapability, w as RATE_CONTROL_TIGHT, wt as streamBrokerCapability, yt as resolveEgressDecodeHwAccel, z as declareLogChannel, zt as createEvent } from "../dist-mEsrAAc3.mjs";
|
|
3
|
+
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-CUxep5tc.mjs";
|
|
4
|
+
import { c as PREBUFFER_MAX_MS, d as resolvePrebuffer, i as parseRestreamPath, l as capPreRollRequestMs, n as NO_DECLARATION, u as prebufferRetentionMs } from "../restream-intent-DWe2kInY.mjs";
|
|
5
5
|
import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-
|
|
6
|
+
import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
|
|
7
|
+
import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-xHtB450F.mjs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import { networkInterfaces, tmpdir } from "node:os";
|
|
@@ -2580,6 +2580,18 @@ var DEFAULT_PRIME_BURST_BOUND = {
|
|
|
2580
2580
|
/** Drop/withhold accounting is aggregated and reported at most this often. */
|
|
2581
2581
|
var DROP_REPORT_INTERVAL_MS$1 = 3e4;
|
|
2582
2582
|
/**
|
|
2583
|
+
* How often a HISTORY-FREE attach is reported per stream.
|
|
2584
|
+
*
|
|
2585
|
+
* The baseline population is large and uninteresting one line at a time — a
|
|
2586
|
+
* WebRTC bootstrap, a HomeKit leg, an ffprobe — but it is the control group the
|
|
2587
|
+
* pre-roll cost is measured against, so it cannot be silent either. Throttled
|
|
2588
|
+
* here and NOT throttled for an attach that carried history: that population is
|
|
2589
|
+
* rare (17 of ~130 agganci in the 2026-09-07 census) and it is the one the
|
|
2590
|
+
* question is about, so a rate limit sized for the common case would drop
|
|
2591
|
+
* exactly the samples nobody has.
|
|
2592
|
+
*/
|
|
2593
|
+
var PLAIN_ATTACH_REPORT_INTERVAL_MS = 1e4;
|
|
2594
|
+
/**
|
|
2583
2595
|
* Defensive hard bound on the per-keyframe RTP/NAL cache (both the passthrough
|
|
2584
2596
|
* `lastKeyframeRtpPackets` and the Annex-B `lastKeyframeNals`). A single
|
|
2585
2597
|
* keyframe GOP is at most a few large NALs — well under a megabyte or two even
|
|
@@ -2729,6 +2741,8 @@ var RtspRestreamer = class {
|
|
|
2729
2741
|
dropsLiveLatched = 0;
|
|
2730
2742
|
dropsLiveClosed = 0;
|
|
2731
2743
|
dropReportMs = 0;
|
|
2744
|
+
/** Last history-free attach reported — see {@link PLAIN_ATTACH_REPORT_INTERVAL_MS}. */
|
|
2745
|
+
plainAttachReportMs = 0;
|
|
2732
2746
|
/** Numeric device id parsed once from the brokerId, for `tags.deviceId`. */
|
|
2733
2747
|
deviceId;
|
|
2734
2748
|
constructor(streamName) {
|
|
@@ -2988,7 +3002,7 @@ var RtspRestreamer = class {
|
|
|
2988
3002
|
if (primePackets.length === 0) return;
|
|
2989
3003
|
const firstTs = primePackets[0].data.readUInt32BE(4);
|
|
2990
3004
|
const spanMs = (primePackets[primePackets.length - 1].data.readUInt32BE(4) - firstTs | 0) / 90;
|
|
2991
|
-
const
|
|
3005
|
+
const preRollPlans = /* @__PURE__ */ new Map();
|
|
2992
3006
|
const bound = this.currentPrimeBound();
|
|
2993
3007
|
/**
|
|
2994
3008
|
* Resolve (and memoize) the plan for one intent.
|
|
@@ -3000,28 +3014,33 @@ var RtspRestreamer = class {
|
|
|
3000
3014
|
* current GOP or a supplier bug can, and D57's remedy (join clean at the
|
|
3001
3015
|
* next live keyframe) is the right one for a burst nobody asked for.
|
|
3002
3016
|
*/
|
|
3003
|
-
const planFor = () => {
|
|
3004
|
-
|
|
3017
|
+
const planFor = (requestedMs) => {
|
|
3018
|
+
const memo = preRollPlans.get(requestedMs);
|
|
3019
|
+
if (memo) return memo;
|
|
3005
3020
|
const burst = this.preRollSupplier?.();
|
|
3006
|
-
const window = fromRing && burst && burst.preRollMs > 0 ? burst.packets() : [];
|
|
3021
|
+
const window = fromRing && burst && burst.preRollMs > 0 && requestedMs > 0 ? burst.packets(requestedMs) : [];
|
|
3007
3022
|
const packets = window.length > primePackets.length ? window.map((data) => ({
|
|
3008
3023
|
data,
|
|
3009
3024
|
marker: (data[1] & 128) !== 0
|
|
3010
3025
|
})) : [];
|
|
3011
3026
|
const windowSpanMs = packets.length > 0 ? (packets[packets.length - 1].data.readUInt32BE(4) - packets[0].data.readUInt32BE(4) | 0) / 90 : 0;
|
|
3012
|
-
const
|
|
3027
|
+
const servableMs = Math.min(requestedMs, burst?.preRollMs ?? 0);
|
|
3028
|
+
const windowBound = packets.length > 0 ? preRollSpanBound(bound, servableMs) : bound;
|
|
3013
3029
|
const plan = {
|
|
3014
3030
|
packets,
|
|
3015
3031
|
active: packets.length > 0,
|
|
3016
|
-
|
|
3032
|
+
retainedMs: burst?.preRollMs ?? 0,
|
|
3033
|
+
requestedMs,
|
|
3017
3034
|
spanMs: windowSpanMs,
|
|
3018
3035
|
overBound: packets.length > 0 && (windowSpanMs > windowBound.maxSpanMs || packets.length > windowBound.maxPackets)
|
|
3019
3036
|
};
|
|
3020
|
-
|
|
3037
|
+
preRollPlans.set(requestedMs, plan);
|
|
3021
3038
|
return plan;
|
|
3022
3039
|
};
|
|
3023
3040
|
const served = [];
|
|
3024
3041
|
const servedPreRoll = /* @__PURE__ */ new Map();
|
|
3042
|
+
/** The plan each pre-roll session was actually served, for its attach line. */
|
|
3043
|
+
const servedPlan = /* @__PURE__ */ new Map();
|
|
3025
3044
|
let withheld = 0;
|
|
3026
3045
|
let detectionIntentServed = 0;
|
|
3027
3046
|
for (const sessionId of ready) {
|
|
@@ -3029,8 +3048,9 @@ var RtspRestreamer = class {
|
|
|
3029
3048
|
if (!session) continue;
|
|
3030
3049
|
const detectionIntent = session.isDetectionIntent();
|
|
3031
3050
|
const preRollIntent = session.preRollIntent();
|
|
3032
|
-
|
|
3033
|
-
|
|
3051
|
+
const requestedMs = session.preRollRequestMs();
|
|
3052
|
+
if (preRollIntent !== null && requestedMs > 0) {
|
|
3053
|
+
const plan = planFor(requestedMs);
|
|
3034
3054
|
if (plan.active) {
|
|
3035
3055
|
if (plan.overBound) {
|
|
3036
3056
|
this.dropsWithheldSpan++;
|
|
@@ -3038,6 +3058,7 @@ var RtspRestreamer = class {
|
|
|
3038
3058
|
continue;
|
|
3039
3059
|
}
|
|
3040
3060
|
servedPreRoll.set(sessionId, preRollIntent);
|
|
3061
|
+
servedPlan.set(sessionId, plan);
|
|
3041
3062
|
served.push(sessionId);
|
|
3042
3063
|
this.logger?.info("restream pre-roll served", {
|
|
3043
3064
|
tags: { deviceId: this.deviceId },
|
|
@@ -3046,7 +3067,8 @@ var RtspRestreamer = class {
|
|
|
3046
3067
|
sessionId,
|
|
3047
3068
|
intent: preRollIntent,
|
|
3048
3069
|
consumer: session.getUserAgent() ?? "unknown",
|
|
3049
|
-
|
|
3070
|
+
requestedMs: plan.requestedMs,
|
|
3071
|
+
retainedMs: plan.retainedMs,
|
|
3050
3072
|
spanMs: Math.round(plan.spanMs),
|
|
3051
3073
|
packets: plan.packets.length
|
|
3052
3074
|
}
|
|
@@ -3075,8 +3097,16 @@ var RtspRestreamer = class {
|
|
|
3075
3097
|
}
|
|
3076
3098
|
if (withheld > 0) this.reportDrops(false);
|
|
3077
3099
|
if (served.length === 0) return;
|
|
3078
|
-
/**
|
|
3079
|
-
|
|
3100
|
+
/**
|
|
3101
|
+
* Media actually delivered — the WIDEST window any session took, or `-1`
|
|
3102
|
+
* when nobody asked for history at all.
|
|
3103
|
+
*
|
|
3104
|
+
* A max and not a single plan's span since D382: two sessions on the same
|
|
3105
|
+
* keyframe can be served different windows, and the number this aggregate
|
|
3106
|
+
* line owes its reader is the largest burst this stream let out. The
|
|
3107
|
+
* per-attach truth is on `restream attach live-edge`, one line per session.
|
|
3108
|
+
*/
|
|
3109
|
+
const servedSpanMs = preRollPlans.size === 0 ? -1 : Math.round(Math.max(...[...preRollPlans.values()].map((p) => p.spanMs)));
|
|
3080
3110
|
/** Which declared intents joined this burst — `0` is "none", not "off". */
|
|
3081
3111
|
const intentsServed = new Set(servedPreRoll.values());
|
|
3082
3112
|
this.logger?.info("restream prime (lat-probe)", {
|
|
@@ -3103,15 +3133,30 @@ var RtspRestreamer = class {
|
|
|
3103
3133
|
prime: true,
|
|
3104
3134
|
keyframeAuStart: false
|
|
3105
3135
|
}));
|
|
3106
|
-
const preRollItems =
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3136
|
+
const preRollItems = /* @__PURE__ */ new Map();
|
|
3137
|
+
const itemsFor = (plan) => {
|
|
3138
|
+
const memo = preRollItems.get(plan.requestedMs);
|
|
3139
|
+
if (memo) return memo;
|
|
3140
|
+
const built = plan.packets.map((packet) => ({
|
|
3141
|
+
packet,
|
|
3142
|
+
prime: true,
|
|
3143
|
+
keyframeAuStart: false
|
|
3144
|
+
}));
|
|
3145
|
+
preRollItems.set(plan.requestedMs, built);
|
|
3146
|
+
return built;
|
|
3147
|
+
};
|
|
3111
3148
|
for (const sessionId of served) {
|
|
3112
3149
|
this.pendingKeyframe.delete(sessionId);
|
|
3113
|
-
const intent = servedPreRoll.get(sessionId);
|
|
3114
|
-
|
|
3150
|
+
const intent = servedPreRoll.get(sessionId) ?? null;
|
|
3151
|
+
const plan = servedPlan.get(sessionId) ?? null;
|
|
3152
|
+
this.startPrimeReplay(sessionId, plan ? itemsFor(plan) : items, intent, {
|
|
3153
|
+
intent,
|
|
3154
|
+
requestedMs: plan?.requestedMs ?? 0,
|
|
3155
|
+
retainedMs: plan?.retainedMs ?? this.preRollSupplier?.().preRollMs ?? 0,
|
|
3156
|
+
preRollSpanMs: plan ? Math.round(plan.spanMs - spanMs) : 0,
|
|
3157
|
+
burstSpanMs: Math.round(plan ? plan.spanMs : spanMs),
|
|
3158
|
+
gopSpanMs: Math.round(spanMs)
|
|
3159
|
+
});
|
|
3115
3160
|
}
|
|
3116
3161
|
}
|
|
3117
3162
|
/**
|
|
@@ -3124,7 +3169,7 @@ var RtspRestreamer = class {
|
|
|
3124
3169
|
* session that went away — aborts the whole replay rather than punching holes
|
|
3125
3170
|
* in the GOP: the session is left latched and rejoins at the next keyframe AU.
|
|
3126
3171
|
*/
|
|
3127
|
-
startPrimeReplay(sessionId, items, preRollIntent) {
|
|
3172
|
+
startPrimeReplay(sessionId, items, preRollIntent, attach) {
|
|
3128
3173
|
const replay = new PacedRtpReplay((item) => {
|
|
3129
3174
|
const session = this.sessions.get(sessionId);
|
|
3130
3175
|
if (!session) {
|
|
@@ -3154,6 +3199,7 @@ var RtspRestreamer = class {
|
|
|
3154
3199
|
durationMs: stats.durationMs
|
|
3155
3200
|
}
|
|
3156
3201
|
});
|
|
3202
|
+
this.reportAttach(sessionId, attach, stats);
|
|
3157
3203
|
this.logger?.debug("restream prime drained", {
|
|
3158
3204
|
tags: { deviceId: this.deviceId },
|
|
3159
3205
|
meta: {
|
|
@@ -3172,6 +3218,58 @@ var RtspRestreamer = class {
|
|
|
3172
3218
|
this.activeReplays.set(sessionId, replay);
|
|
3173
3219
|
replay.start(items);
|
|
3174
3220
|
}
|
|
3221
|
+
/**
|
|
3222
|
+
* ONE LINE PER ATTACH: what this session asked for, what it was handed, and
|
|
3223
|
+
* how long it took to reach the live edge.
|
|
3224
|
+
*
|
|
3225
|
+
* The question this exists to answer is "does the pre-roll delay the first
|
|
3226
|
+
* inference, and by how much" — and it could not be answered before. The only
|
|
3227
|
+
* measurement in reach was `pipeline stats`, a 10 s window whose averages
|
|
3228
|
+
* dissolve a 4 s transient by construction, and the broker's own lines were
|
|
3229
|
+
* per-STREAM aggregates that named a window without naming a session or a
|
|
3230
|
+
* duration. Seventeen served bursts inside ninety minutes of ten-second
|
|
3231
|
+
* averages is not a measurement, it is a hope.
|
|
3232
|
+
*
|
|
3233
|
+
* `liveEdgeMs` is measured from PLAY, the first instant the restreamer owes
|
|
3234
|
+
* this session bytes, to the moment its burst has drained and it is on the
|
|
3235
|
+
* live feed. It therefore contains the wait for a keyframe AND the paced
|
|
3236
|
+
* drain — the two things a burst can cost a joiner on this side of the
|
|
3237
|
+
* socket. What it deliberately does NOT contain is the consumer's own decode
|
|
3238
|
+
* of that burst: that is measured where it happens, on
|
|
3239
|
+
* `session-decode: detection first frame`, and the two lines are joined by
|
|
3240
|
+
* `deviceId` plus the per-attach `consumer` tag.
|
|
3241
|
+
*
|
|
3242
|
+
* `aborted` is on the line because a burst that never reached the socket is
|
|
3243
|
+
* not a burst this session paid for, and averaging those in would understate
|
|
3244
|
+
* the cost of the ones that did.
|
|
3245
|
+
*/
|
|
3246
|
+
reportAttach(sessionId, attach, stats) {
|
|
3247
|
+
const session = this.sessions.get(sessionId);
|
|
3248
|
+
const playAt = session?.getPlayAt() ?? 0;
|
|
3249
|
+
if (!(attach.preRollSpanMs > 0)) {
|
|
3250
|
+
const now = Date.now();
|
|
3251
|
+
if (now - this.plainAttachReportMs < PLAIN_ATTACH_REPORT_INTERVAL_MS) return;
|
|
3252
|
+
this.plainAttachReportMs = now;
|
|
3253
|
+
}
|
|
3254
|
+
this.logger?.info("restream attach live-edge", {
|
|
3255
|
+
tags: { deviceId: this.deviceId },
|
|
3256
|
+
meta: {
|
|
3257
|
+
streamName: this.streamName,
|
|
3258
|
+
sessionId,
|
|
3259
|
+
consumer: session?.getUserAgent() ?? "unknown",
|
|
3260
|
+
intent: attach.intent ?? "none",
|
|
3261
|
+
requestedMs: attach.requestedMs,
|
|
3262
|
+
retainedMs: attach.retainedMs,
|
|
3263
|
+
preRollSpanMs: attach.preRollSpanMs,
|
|
3264
|
+
burstSpanMs: attach.burstSpanMs,
|
|
3265
|
+
gopSpanMs: attach.gopSpanMs,
|
|
3266
|
+
liveEdgeMs: playAt > 0 ? Date.now() - playAt : -1,
|
|
3267
|
+
drainMs: Math.round(stats.durationMs),
|
|
3268
|
+
packets: stats.sent,
|
|
3269
|
+
aborted: stats.aborted
|
|
3270
|
+
}
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3175
3273
|
/** Cancel a session's in-flight join replay (teardown / stream reset). */
|
|
3176
3274
|
abortReplay(sessionId) {
|
|
3177
3275
|
const replay = this.activeReplays.get(sessionId);
|
|
@@ -3454,15 +3552,31 @@ var RtspSession = class {
|
|
|
3454
3552
|
/** When true, this session receives video-only (no audio RTP). */
|
|
3455
3553
|
muted;
|
|
3456
3554
|
/**
|
|
3457
|
-
*
|
|
3458
|
-
*
|
|
3459
|
-
*
|
|
3555
|
+
* What this session DECLARED on its dial path (see `restream-intent.ts`): its
|
|
3556
|
+
* consumer purpose, and how many ms of retained history it asked for.
|
|
3557
|
+
* {@link NO_DECLARATION} when it declared nothing — which is every consumer
|
|
3558
|
+
* except the per-session detection decode and the recorder's writer.
|
|
3460
3559
|
*
|
|
3461
3560
|
* Kept separate from {@link muted} deliberately: `isMuted()` is shared by the
|
|
3462
3561
|
* WebRTC/HomeKit loopback legs, and the join rule that keys on it must stay
|
|
3463
3562
|
* true for them. Only an explicit declaration changes behaviour.
|
|
3563
|
+
*
|
|
3564
|
+
* One field and not two, because the purpose and the amount are one
|
|
3565
|
+
* statement (D382): a session that names a purpose without naming an amount
|
|
3566
|
+
* is asking for nothing, and that has to be as unmistakable here as it is on
|
|
3567
|
+
* the wire.
|
|
3464
3568
|
*/
|
|
3465
|
-
|
|
3569
|
+
declaration;
|
|
3570
|
+
/**
|
|
3571
|
+
* Epoch ms at which this session reached PLAY, or `0` before it does.
|
|
3572
|
+
*
|
|
3573
|
+
* The clock the join is measured against. `connectedAt` is the TCP accept,
|
|
3574
|
+
* which includes DESCRIBE/SETUP and, for a cold broker, the whole upstream
|
|
3575
|
+
* dial — so it answers "how slow was the camera", not "what did the burst
|
|
3576
|
+
* cost this session". Time to the live edge is measured from PLAY, because
|
|
3577
|
+
* PLAY is the first instant at which the restreamer owes this session bytes.
|
|
3578
|
+
*/
|
|
3579
|
+
playAt = 0;
|
|
3466
3580
|
/**
|
|
3467
3581
|
* Latch for ANY session that has started dropping video packets on socket
|
|
3468
3582
|
* backlog — it was written for the `muted` decoder loopback, which was the
|
|
@@ -3476,12 +3590,12 @@ var RtspSession = class {
|
|
|
3476
3590
|
/** Consecutive keyframe AUs at which this session was still unwritable.
|
|
3477
3591
|
* Reset by any successful write — the bound is CONSECUTIVE misses. */
|
|
3478
3592
|
missedKeyframes = 0;
|
|
3479
|
-
constructor(socket, sdp, onClose, muted,
|
|
3593
|
+
constructor(socket, sdp, onClose, muted, declaration) {
|
|
3480
3594
|
this.socket = socket;
|
|
3481
3595
|
this.sdp = sdp;
|
|
3482
3596
|
this.onClose = onClose;
|
|
3483
3597
|
this.muted = muted ?? false;
|
|
3484
|
-
this.
|
|
3598
|
+
this.declaration = declaration ?? NO_DECLARATION;
|
|
3485
3599
|
this.sessionId = randomUUID().replace(/-/g, "").slice(0, 16);
|
|
3486
3600
|
socket.on("data", (chunk) => {
|
|
3487
3601
|
if (this.closed) return;
|
|
@@ -3525,7 +3639,23 @@ var RtspSession = class {
|
|
|
3525
3639
|
* construction and drains the burst instead of accumulating it.
|
|
3526
3640
|
*/
|
|
3527
3641
|
isDetectionIntent() {
|
|
3528
|
-
return this.intent === "detection";
|
|
3642
|
+
return this.declaration.intent === "detection";
|
|
3643
|
+
}
|
|
3644
|
+
/**
|
|
3645
|
+
* Ms of retained history this session ASKED for on its dial. `0` = none.
|
|
3646
|
+
*
|
|
3647
|
+
* Read by `RtspRestreamer.servePendingSessionsRtp`, which serves the smaller
|
|
3648
|
+
* of this and what the ring holds. A session that declared an intent and no
|
|
3649
|
+
* amount takes the ordinary current-GOP burst — the intent still buys it
|
|
3650
|
+
* whatever else the intent buys (for detection, the live-edge exemption), and
|
|
3651
|
+
* nothing more.
|
|
3652
|
+
*/
|
|
3653
|
+
preRollRequestMs() {
|
|
3654
|
+
return this.declaration.preRollRequestMs;
|
|
3655
|
+
}
|
|
3656
|
+
/** Epoch ms at which this session reached PLAY; `0` if it has not. */
|
|
3657
|
+
getPlayAt() {
|
|
3658
|
+
return this.playAt;
|
|
3529
3659
|
}
|
|
3530
3660
|
/**
|
|
3531
3661
|
* Does this session get the PRE-ROLL window instead of the current GOP?
|
|
@@ -3542,12 +3672,14 @@ var RtspSession = class {
|
|
|
3542
3672
|
* WHICH pre-roll window this session gets, or `null` when it declared no
|
|
3543
3673
|
* intent and takes the ordinary current-GOP join burst.
|
|
3544
3674
|
*
|
|
3545
|
-
*
|
|
3546
|
-
*
|
|
3547
|
-
*
|
|
3675
|
+
* There is ONE window now (D381) and one ASK per session (D382), so what the
|
|
3676
|
+
* restreamer still needs from this is WHO joined — the identity it prints
|
|
3677
|
+
* beside the window it served, and the reason a recorder and a detector are
|
|
3678
|
+
* distinguishable on a line that reports one number.
|
|
3548
3679
|
*/
|
|
3549
3680
|
preRollIntent() {
|
|
3550
|
-
|
|
3681
|
+
const intent = this.declaration.intent;
|
|
3682
|
+
return intent === "detection" || intent === "recording" ? intent : null;
|
|
3551
3683
|
}
|
|
3552
3684
|
/**
|
|
3553
3685
|
* Diagnostic snapshot consumed by `StreamBroker.listClients` (Phase 10).
|
|
@@ -3768,6 +3900,7 @@ var RtspSession = class {
|
|
|
3768
3900
|
}
|
|
3769
3901
|
handlePlay(req) {
|
|
3770
3902
|
this.playing = true;
|
|
3903
|
+
if (this.playAt === 0) this.playAt = Date.now();
|
|
3771
3904
|
this.respond(req, 200, "OK", {
|
|
3772
3905
|
Session: this.sessionId,
|
|
3773
3906
|
Range: "npt=0.000-"
|
|
@@ -3924,7 +4057,7 @@ var RtspListenServer = class RtspListenServer {
|
|
|
3924
4057
|
async dispatch(socket, requestBuffer) {
|
|
3925
4058
|
const uriMatch = (requestBuffer.split("\r\n")[0] ?? "").match(/\s(rtsp:\/\/[^\s]+)\s/);
|
|
3926
4059
|
const pathMatch = (uriMatch ? uriMatch[1] : "").match(/rtsp:\/\/[^/]+(\/.*?)(?:\s|$)/);
|
|
3927
|
-
const { lookupPath, muted: isMuted,
|
|
4060
|
+
const { lookupPath, muted: isMuted, declaration } = parseRestreamPath(pathMatch ? pathMatch[1].replace(/^\//, "").replace(/\/trackID=\d+$/, "") : "");
|
|
3928
4061
|
const restreamer = this.restreamers.get(lookupPath);
|
|
3929
4062
|
if (!restreamer) {
|
|
3930
4063
|
socket.write(`RTSP/1.0 404 Not Found\r\nCSeq: 1\r\n\r\n`);
|
|
@@ -3953,7 +4086,7 @@ var RtspListenServer = class RtspListenServer {
|
|
|
3953
4086
|
const session = new RtspSession(socket, isMuted ? restreamer.getMutedSdp() ?? restreamer.getSdp() ?? "" : restreamer.getSdp() ?? "", () => {
|
|
3954
4087
|
restreamer.removeSession(session.getSessionId());
|
|
3955
4088
|
releaseOnce();
|
|
3956
|
-
}, isMuted,
|
|
4089
|
+
}, isMuted, declaration);
|
|
3957
4090
|
restreamer.addSession(session);
|
|
3958
4091
|
releaseOnce();
|
|
3959
4092
|
socket.unshift(Buffer.from(requestBuffer, "ascii"));
|
|
@@ -11698,7 +11831,7 @@ var StreamBroker = class StreamBroker {
|
|
|
11698
11831
|
this.rtspRestreamer.setPrimeBoundSupplier(() => this.joinBurstBound);
|
|
11699
11832
|
this.rtspRestreamer.setPreRollSupplier(() => {
|
|
11700
11833
|
return {
|
|
11701
|
-
packets: () => this.getRtpPreRollBuffer(),
|
|
11834
|
+
packets: (requestedMs) => this.getRtpPreRollBuffer(requestedMs),
|
|
11702
11835
|
preRollMs: this.prebufferMs
|
|
11703
11836
|
};
|
|
11704
11837
|
});
|
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CLXImQ3R.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { b as NativeLeaseSettingsSchema, p as DEFAULT_NATIVE_LEASE_SETTINGS, y as NativeLeaseAdmissionSchema } from "./dist-
|
|
1
|
+
import { b as NativeLeaseSettingsSchema, p as DEFAULT_NATIVE_LEASE_SETTINGS, y as NativeLeaseAdmissionSchema } from "./dist-mEsrAAc3.mjs";
|
|
2
2
|
//#region src/session-decode/native-lease-config.ts
|
|
3
3
|
/**
|
|
4
4
|
* Resolution of the native-frame hold / tile knobs for one decode worker.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.200",
|
|
4
4
|
"description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Path suffix for the audio-free variant dialed by a DETECTION decode session.
|
|
3
|
-
* Muted in every respect (no audio, still `isMuted()`), plus exempt from the
|
|
4
|
-
* live-edge join withhold.
|
|
5
|
-
*/
|
|
6
|
-
var DETECTION_MUTED_PATH_SUFFIX = "/muted-detection";
|
|
7
|
-
/**
|
|
8
|
-
* Path suffix for a RECORDING dial — the `events`-band recorder's ffmpeg.
|
|
9
|
-
*
|
|
10
|
-
* NOT muted: a recording keeps its audio, so this cannot ride the detection
|
|
11
|
-
* suffix. What it shares with detection is the reason for existing — it attaches
|
|
12
|
-
* AFTER the trigger, so without the retained history its footage begins after
|
|
13
|
-
* the event. Measured on camera 1438 (audit 2026-08-20): the footage started
|
|
14
|
-
* after the motion in 60 % of episodes, because the writer is spawned at the
|
|
15
|
-
* trigger and ffmpeg's first frame is at best the live edge.
|
|
16
|
-
*/
|
|
17
|
-
var RECORDING_PATH_SUFFIX = "/recording";
|
|
18
|
-
/**
|
|
19
|
-
* Split a restream request path (already stripped of `rtsp://host:port/` and of
|
|
20
|
-
* any `/trackID=N` control suffix) into its token, mute bit and intent.
|
|
21
|
-
*/
|
|
22
|
-
function parseRestreamPath(streamPath) {
|
|
23
|
-
if (streamPath.endsWith("/recording")) return {
|
|
24
|
-
lookupPath: streamPath.slice(0, -10),
|
|
25
|
-
muted: false,
|
|
26
|
-
intent: "recording"
|
|
27
|
-
};
|
|
28
|
-
if (streamPath.endsWith("/muted-detection")) return {
|
|
29
|
-
lookupPath: streamPath.slice(0, -16),
|
|
30
|
-
muted: true,
|
|
31
|
-
intent: "detection"
|
|
32
|
-
};
|
|
33
|
-
if (streamPath.endsWith("/muted")) return {
|
|
34
|
-
lookupPath: streamPath.slice(0, -6),
|
|
35
|
-
muted: true,
|
|
36
|
-
intent: null
|
|
37
|
-
};
|
|
38
|
-
return {
|
|
39
|
-
lookupPath: streamPath,
|
|
40
|
-
muted: false,
|
|
41
|
-
intent: null
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Rewrite an acquired restream URL to declare the detection intent.
|
|
46
|
-
*
|
|
47
|
-
* Only a MUTED url is rewritten. `resolveSourceUrl` falls back to the
|
|
48
|
-
* audio-bearing url when the broker has no muted variant; that session is not
|
|
49
|
-
* muted, the live-edge withhold never applied to it, and appending the suffix
|
|
50
|
-
* would only break the token lookup. Idempotent.
|
|
51
|
-
*/
|
|
52
|
-
function withDetectionIntent(restreamUrl) {
|
|
53
|
-
if (restreamUrl.endsWith("/muted-detection")) return restreamUrl;
|
|
54
|
-
if (!restreamUrl.endsWith("/muted")) return restreamUrl;
|
|
55
|
-
return `${restreamUrl.slice(0, -6)}${DETECTION_MUTED_PATH_SUFFIX}`;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Rewrite an acquired restream URL to declare the RECORDING intent.
|
|
59
|
-
*
|
|
60
|
-
* The mirror image of {@link withDetectionIntent}'s guard: only a NON-muted url
|
|
61
|
-
* is rewritten. A recorder that was handed the muted variant (a device with no
|
|
62
|
-
* audio, or `audio: 'none'`) is already the detection population's shape, and
|
|
63
|
-
* appending this suffix would only break the token lookup. Idempotent.
|
|
64
|
-
*/
|
|
65
|
-
function withRecordingIntent(restreamUrl) {
|
|
66
|
-
if (restreamUrl.endsWith("/recording")) return restreamUrl;
|
|
67
|
-
if (restreamUrl.endsWith("/muted")) return restreamUrl;
|
|
68
|
-
if (restreamUrl.endsWith("/muted-detection")) return restreamUrl;
|
|
69
|
-
return `${restreamUrl}${RECORDING_PATH_SUFFIX}`;
|
|
70
|
-
}
|
|
71
|
-
//#endregion
|
|
72
|
-
export { withDetectionIntent as n, withRecordingIntent as r, parseRestreamPath as t };
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Path suffix for the audio-free variant dialed by a DETECTION decode session.
|
|
3
|
-
* Muted in every respect (no audio, still `isMuted()`), plus exempt from the
|
|
4
|
-
* live-edge join withhold.
|
|
5
|
-
*/
|
|
6
|
-
var DETECTION_MUTED_PATH_SUFFIX = "/muted-detection";
|
|
7
|
-
/**
|
|
8
|
-
* Path suffix for a RECORDING dial — the `events`-band recorder's ffmpeg.
|
|
9
|
-
*
|
|
10
|
-
* NOT muted: a recording keeps its audio, so this cannot ride the detection
|
|
11
|
-
* suffix. What it shares with detection is the reason for existing — it attaches
|
|
12
|
-
* AFTER the trigger, so without the retained history its footage begins after
|
|
13
|
-
* the event. Measured on camera 1438 (audit 2026-08-20): the footage started
|
|
14
|
-
* after the motion in 60 % of episodes, because the writer is spawned at the
|
|
15
|
-
* trigger and ffmpeg's first frame is at best the live edge.
|
|
16
|
-
*/
|
|
17
|
-
var RECORDING_PATH_SUFFIX = "/recording";
|
|
18
|
-
/**
|
|
19
|
-
* Split a restream request path (already stripped of `rtsp://host:port/` and of
|
|
20
|
-
* any `/trackID=N` control suffix) into its token, mute bit and intent.
|
|
21
|
-
*/
|
|
22
|
-
function parseRestreamPath(streamPath) {
|
|
23
|
-
if (streamPath.endsWith("/recording")) return {
|
|
24
|
-
lookupPath: streamPath.slice(0, -10),
|
|
25
|
-
muted: false,
|
|
26
|
-
intent: "recording"
|
|
27
|
-
};
|
|
28
|
-
if (streamPath.endsWith("/muted-detection")) return {
|
|
29
|
-
lookupPath: streamPath.slice(0, -16),
|
|
30
|
-
muted: true,
|
|
31
|
-
intent: "detection"
|
|
32
|
-
};
|
|
33
|
-
if (streamPath.endsWith("/muted")) return {
|
|
34
|
-
lookupPath: streamPath.slice(0, -6),
|
|
35
|
-
muted: true,
|
|
36
|
-
intent: null
|
|
37
|
-
};
|
|
38
|
-
return {
|
|
39
|
-
lookupPath: streamPath,
|
|
40
|
-
muted: false,
|
|
41
|
-
intent: null
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Rewrite an acquired restream URL to declare the detection intent.
|
|
46
|
-
*
|
|
47
|
-
* Only a MUTED url is rewritten. `resolveSourceUrl` falls back to the
|
|
48
|
-
* audio-bearing url when the broker has no muted variant; that session is not
|
|
49
|
-
* muted, the live-edge withhold never applied to it, and appending the suffix
|
|
50
|
-
* would only break the token lookup. Idempotent.
|
|
51
|
-
*/
|
|
52
|
-
function withDetectionIntent(restreamUrl) {
|
|
53
|
-
if (restreamUrl.endsWith("/muted-detection")) return restreamUrl;
|
|
54
|
-
if (!restreamUrl.endsWith("/muted")) return restreamUrl;
|
|
55
|
-
return `${restreamUrl.slice(0, -6)}${DETECTION_MUTED_PATH_SUFFIX}`;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Rewrite an acquired restream URL to declare the RECORDING intent.
|
|
59
|
-
*
|
|
60
|
-
* The mirror image of {@link withDetectionIntent}'s guard: only a NON-muted url
|
|
61
|
-
* is rewritten. A recorder that was handed the muted variant (a device with no
|
|
62
|
-
* audio, or `audio: 'none'`) is already the detection population's shape, and
|
|
63
|
-
* appending this suffix would only break the token lookup. Idempotent.
|
|
64
|
-
*/
|
|
65
|
-
function withRecordingIntent(restreamUrl) {
|
|
66
|
-
if (restreamUrl.endsWith("/recording")) return restreamUrl;
|
|
67
|
-
if (restreamUrl.endsWith("/muted")) return restreamUrl;
|
|
68
|
-
if (restreamUrl.endsWith("/muted-detection")) return restreamUrl;
|
|
69
|
-
return `${restreamUrl}${RECORDING_PATH_SUFFIX}`;
|
|
70
|
-
}
|
|
71
|
-
//#endregion
|
|
72
|
-
Object.defineProperty(exports, "parseRestreamPath", {
|
|
73
|
-
enumerable: true,
|
|
74
|
-
get: function() {
|
|
75
|
-
return parseRestreamPath;
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
Object.defineProperty(exports, "withDetectionIntent", {
|
|
79
|
-
enumerable: true,
|
|
80
|
-
get: function() {
|
|
81
|
-
return withDetectionIntent;
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
Object.defineProperty(exports, "withRecordingIntent", {
|
|
85
|
-
enumerable: true,
|
|
86
|
-
get: function() {
|
|
87
|
-
return withRecordingIntent;
|
|
88
|
-
}
|
|
89
|
-
});
|