@camstack/addon-pipeline 1.2.76 → 1.2.77
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-CTpQfjSR.js → addon-utils-bZcwLaVW.js} +1 -1
- package/dist/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-CZLjObZZ.js → dist-DQZzL0Ri.js} +355 -39
- package/dist/{dist-zksfWnEA.mjs → dist-HhcoV-Ky.mjs} +355 -39
- package/dist/{event-loop-stall-monitor-CcBQAI28.js → event-loop-stall-monitor-CBOo1oh_.js} +1 -1
- package/dist/{event-loop-stall-monitor-BuZA3loB.mjs → event-loop-stall-monitor-CqBEWGSZ.mjs} +1 -1
- package/dist/{lazy-sharp-SWR_D1Um.js → lazy-sharp-DIC1rpea.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/recorder/index.js +368 -18
- package/dist/recorder/index.mjs +367 -17
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Dd6XOtVn.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-De_xzvjU.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DHJSxHH1.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-Dg08SxUW.mjs +26 -0
- package/dist/stream-broker/{hostInit-COc_Q_xX.mjs → hostInit-CDS5frmk.mjs} +3 -3
- package/dist/stream-broker/index.js +306 -71
- package/dist/stream-broker/index.mjs +306 -71
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-DyX_HbaJ.mjs → worker-protocol-BNlLGLBV.mjs} +1 -1
- package/dist/{worker-protocol-DAQ1iZFK.js → worker-protocol-DZVGVAPU.js} +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-RYTOS6B-.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-DfsLsn2e.mjs +0 -26
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as __toESM, t as __commonJSMin } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { At as string, Ct as boolean, D as cameraStreamsCapability, Dt as number, Et as literal, F as egressTranscodeSharingKey, I as egressTransportFromRequest, Mt as EventCategory, O as createHwAccelCache, Ot as object, Q as streamBrokerCapability, S as RingBuffer, St as array, V as maskUrlCredentials, Y as resolveEgressDecodeHwAccel, at as invocationFromEncodeProfile, ct as CAM_PROFILE_ORDER, dt as createEvent, et as webrtcSessionCapability, f as EncodeProfileSchema, gt as nodePin, ht as makeSourceBrokerId, it as buildFfmpegArgs$1, jt as union, kt as record, lt as DeviceFeature, mt as makeProfileBrokerId, nt as AUDIO_PRESETS, ot as isSoftwareDecode, pt as isEvent, rt as Fmp4BoxSplitter, st as BaseAddon, tt as errMsg, ut as DeviceType, v as RATE_CONTROL_RELAXED, vt as parseProfileBrokerId, w as addonWidgetsSourceCapability, wt as discriminatedUnion, xt as _enum, y as RATE_CONTROL_TIGHT } from "../dist-
|
|
2
|
+
import { At as string, Ct as boolean, D as cameraStreamsCapability, Dt as number, Et as literal, F as egressTranscodeSharingKey, I as egressTransportFromRequest, Mt as EventCategory, O as createHwAccelCache, Ot as object, Q as streamBrokerCapability, S as RingBuffer, St as array, V as maskUrlCredentials, Y as resolveEgressDecodeHwAccel, at as invocationFromEncodeProfile, ct as CAM_PROFILE_ORDER, dt as createEvent, et as webrtcSessionCapability, f as EncodeProfileSchema, gt as nodePin, ht as makeSourceBrokerId, it as buildFfmpegArgs$1, jt as union, kt as record, lt as DeviceFeature, mt as makeProfileBrokerId, nt as AUDIO_PRESETS, ot as isSoftwareDecode, pt as isEvent, rt as Fmp4BoxSplitter, st as BaseAddon, tt as errMsg, ut as DeviceType, v as RATE_CONTROL_RELAXED, vt as parseProfileBrokerId, w as addonWidgetsSourceCapability, wt as discriminatedUnion, xt as _enum, y as RATE_CONTROL_TIGHT } from "../dist-HhcoV-Ky.mjs";
|
|
3
3
|
import { n as profileForStreamId, r as parseRestreamPath } from "../remote-restream-Ci7RXNGb.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import * as crypto$1 from "node:crypto";
|
|
@@ -968,7 +968,93 @@ var DerivedStreamFeeder = class extends FfmpegPushReader {
|
|
|
968
968
|
* deleted, and with it the two hacks this module needed to bend it into shape:
|
|
969
969
|
* a sink-retargeting string surgery on the tail of the arg list, and a fake
|
|
970
970
|
* `inputFormat` chosen only to defeat smart-copy.
|
|
971
|
+
*
|
|
972
|
+
* ## The audio rule, and the outage that wrote it
|
|
973
|
+
*
|
|
974
|
+
* A derived stream is a self-contained MPEG-TS, so its audio must travel
|
|
975
|
+
* IN-BAND — `passthrough` (`-an`, right for WebRTC, where werift carries audio
|
|
976
|
+
* out of band) silently drops the track. That was fixed on 2026-07-17 by
|
|
977
|
+
* remapping `passthrough` → `-c:a copy`, and the track stayed missing anyway,
|
|
978
|
+
* because a copy has to survive TWO hops and it only clears the first:
|
|
979
|
+
*
|
|
980
|
+
* 1. the MPEG-TS MUXER. Measured with ffmpeg 7.1: a `pcm_mulaw` track copied
|
|
981
|
+
* into `-f mpegts` does not fail the invocation. ffmpeg logs
|
|
982
|
+
* `Stream 1, codec pcm_mulaw, is muxed as a private data stream and may
|
|
983
|
+
* not be recognized upon reading` and writes stream_type `0x06` — which
|
|
984
|
+
* is why the derived stream looked perfectly healthy: video never
|
|
985
|
+
* stopped;
|
|
986
|
+
* 2. {@link MpegTsDemux}, which adopts an elementary audio PID only for
|
|
987
|
+
* stream_type `0x0f` (AAC) or `0x03`/`0x04` (MP2). `0x06` is not audio to
|
|
988
|
+
* it, so no audio packet was ever forwarded, `onAudioInfo` never fired,
|
|
989
|
+
* the derived broker never learned it had audio, and its probe read
|
|
990
|
+
* `audio: null` forever.
|
|
991
|
+
*
|
|
992
|
+
* Every camera in this fleet speaks G.711 (PCMU/PCMA), so the copy could never
|
|
993
|
+
* work for any of them: no `derived:*` stream has EVER carried audio, and
|
|
994
|
+
* camera 618 — whose `low` (motion, and therefore audio) profile is mapped to
|
|
995
|
+
* a derived stream — went silent the day it was remapped.
|
|
996
|
+
*
|
|
997
|
+
* So the carrier decides, explicitly and loudly, not the profile: source audio
|
|
998
|
+
* is COPIED when it is already AAC, and RE-ENCODED to AAC otherwise (including
|
|
999
|
+
* when the source codec is unknown — the safe direction, since an unknown that
|
|
1000
|
+
* guesses `copy` mutes the camera). The decision is returned to the caller so
|
|
1001
|
+
* it can be logged per camera; it is never taken silently.
|
|
1002
|
+
*/
|
|
1003
|
+
/**
|
|
1004
|
+
* Audio codecs that survive BOTH hops of the derived carrier untouched: a
|
|
1005
|
+
* standard MPEG-TS stream type AND a stream type {@link MpegTsDemux} adopts.
|
|
1006
|
+
*
|
|
1007
|
+
* AAC only, deliberately. MP2 clears both hops as a container matter, but the
|
|
1008
|
+
* broker's push-audio path has no MP2 decode (`StreamBroker.pushAudioInfo` →
|
|
1009
|
+
* `buildAudioCodecSessionConfigFromPush` handles `aac`/`opus`), so a copied
|
|
1010
|
+
* MP2 track would arrive as an announced-but-undecodable stream — a second way
|
|
1011
|
+
* to be silent, with a track descriptor to make it look fine.
|
|
1012
|
+
*/
|
|
1013
|
+
var TS_COPYABLE_AUDIO_CODECS = new Set(["aac"]);
|
|
1014
|
+
/**
|
|
1015
|
+
* The AAC re-encode used when the source cannot be copied. Mono, and
|
|
1016
|
+
* deliberately WITHOUT a forced sample rate: a G.711 camera mic is 8 kHz, and
|
|
1017
|
+
* upsampling it to the 48 kHz camera preset would spend bitrate on nothing.
|
|
1018
|
+
* ffmpeg's native AAC encoder accepts 8 kHz, the ADTS header carries whatever
|
|
1019
|
+
* rate came out, and `parseAdtsAudioParams` reads it back for the broker.
|
|
971
1020
|
*/
|
|
1021
|
+
var DERIVED_AAC_RECODE = {
|
|
1022
|
+
codec: "aac",
|
|
1023
|
+
channels: 1
|
|
1024
|
+
};
|
|
1025
|
+
/** `true` when this profile audio means "carry whatever the source has". */
|
|
1026
|
+
function wantsSourceAudio(audio) {
|
|
1027
|
+
return audio === "passthrough" || audio.codec === "copy";
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Decide the derived stream's audio from the profile's intent and the SOURCE's
|
|
1031
|
+
* observed codec. Pure; see this module's header for why `copy` is not the
|
|
1032
|
+
* default answer to "carry the source's audio".
|
|
1033
|
+
*
|
|
1034
|
+
* `sourceAudioCodec` is matched case-insensitively (`BrokerStats.audio.codec`
|
|
1035
|
+
* is upper-cased, SDP spellings are not).
|
|
1036
|
+
*/
|
|
1037
|
+
function resolveDerivedAudio(profileAudio, sourceAudioCodec) {
|
|
1038
|
+
if (!wantsSourceAudio(profileAudio)) return {
|
|
1039
|
+
audio: profileAudio,
|
|
1040
|
+
decision: { kind: "profile" }
|
|
1041
|
+
};
|
|
1042
|
+
const observed = sourceAudioCodec?.toLowerCase() ?? null;
|
|
1043
|
+
if (observed !== null && TS_COPYABLE_AUDIO_CODECS.has(observed)) return {
|
|
1044
|
+
audio: { codec: "copy" },
|
|
1045
|
+
decision: {
|
|
1046
|
+
kind: "copy",
|
|
1047
|
+
sourceCodec: observed
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
return {
|
|
1051
|
+
audio: DERIVED_AAC_RECODE,
|
|
1052
|
+
decision: {
|
|
1053
|
+
kind: "recode-aac",
|
|
1054
|
+
sourceCodec: observed
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
972
1058
|
/**
|
|
973
1059
|
* Build the ffmpeg argv that reads a source loopback RTSP url, transcodes it
|
|
974
1060
|
* per `profile`, and writes `-f mpegts pipe:1` (what the feeder's `MpegTsDemux`
|
|
@@ -977,37 +1063,33 @@ var DerivedStreamFeeder = class extends FfmpegPushReader {
|
|
|
977
1063
|
* Two things are specific to a derived stream and are expressed as first-class
|
|
978
1064
|
* builder inputs rather than post-hoc edits:
|
|
979
1065
|
*
|
|
980
|
-
* - `forceReencode` — a derived stream ALWAYS applies its profile; that
|
|
981
|
-
* entire reason to exist. Without it the builder's smart-copy would
|
|
982
|
-
* the encode whenever the source already spoke the target codec. A
|
|
983
|
-
* profile still copies (a legitimate derived remux).
|
|
984
|
-
* - audio
|
|
985
|
-
*
|
|
986
|
-
*
|
|
987
|
-
* but silently DROPS the track here.
|
|
988
|
-
*
|
|
989
|
-
* `decodeHwAccel` is threaded from the caller. It used to be hardcoded `null`;
|
|
990
|
-
* the derived source is a local loopback restream, but decoding 4K in software
|
|
991
|
-
* to re-encode it is exactly the cost the hub's working vaapi exists to avoid.
|
|
992
|
-
* The caller resolves it through `resolveEgressDecodeHwAccel`, and the shared
|
|
993
|
-
* `FfmpegProcess` retries in software — loudly — if the hardware child dies.
|
|
1066
|
+
* - `forceReencode` — a derived stream ALWAYS applies its video profile; that
|
|
1067
|
+
* is its entire reason to exist. Without it the builder's smart-copy would
|
|
1068
|
+
* elide the encode whenever the source already spoke the target codec. A
|
|
1069
|
+
* `copy` profile still copies (a legitimate derived remux).
|
|
1070
|
+
* - the audio plan from {@link resolveDerivedAudio} in place of the profile's
|
|
1071
|
+
* own — the MPEG-TS carrier, not the operator, decides whether the source
|
|
1072
|
+
* track can be copied.
|
|
994
1073
|
*/
|
|
995
|
-
function buildDerivedTranscodeArgs(
|
|
996
|
-
const audio = profile.audio
|
|
997
|
-
return
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1074
|
+
function buildDerivedTranscodeArgs(input) {
|
|
1075
|
+
const audio = resolveDerivedAudio(input.profile.audio, input.sourceAudioCodec);
|
|
1076
|
+
return {
|
|
1077
|
+
args: buildFfmpegArgs$1(invocationFromEncodeProfile({
|
|
1078
|
+
profile: {
|
|
1079
|
+
...input.profile,
|
|
1080
|
+
audio: audio.audio
|
|
1081
|
+
},
|
|
1082
|
+
sourceCodec: "h264",
|
|
1083
|
+
forceReencode: true,
|
|
1084
|
+
sourceUrl: input.loopbackUrl,
|
|
1085
|
+
sink: {
|
|
1086
|
+
kind: "stdout",
|
|
1087
|
+
container: "mpegts"
|
|
1088
|
+
},
|
|
1089
|
+
decodeHwAccel: input.decodeHwAccel ?? null
|
|
1090
|
+
})),
|
|
1091
|
+
audio
|
|
1092
|
+
};
|
|
1011
1093
|
}
|
|
1012
1094
|
/**
|
|
1013
1095
|
* Ceiling for a configured pre-roll.
|
|
@@ -17952,7 +18034,19 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17952
18034
|
loopbackUrl = entry.url;
|
|
17953
18035
|
release = async () => {};
|
|
17954
18036
|
}
|
|
17955
|
-
const
|
|
18037
|
+
const sourceAudioCodec = this.observedSourceAudioCodec(deviceId, source);
|
|
18038
|
+
const { args, audio } = buildDerivedTranscodeArgs({
|
|
18039
|
+
profile: encodeProfile,
|
|
18040
|
+
loopbackUrl,
|
|
18041
|
+
sourceAudioCodec
|
|
18042
|
+
});
|
|
18043
|
+
if (audio.decision.kind === "recode-aac") this.logger.info("derived audio re-encoded to AAC — MPEG-TS cannot copy this source codec", {
|
|
18044
|
+
tags: { deviceId },
|
|
18045
|
+
meta: {
|
|
18046
|
+
camStreamId,
|
|
18047
|
+
sourceAudioCodec: audio.decision.sourceCodec ?? "unknown"
|
|
18048
|
+
}
|
|
18049
|
+
});
|
|
17956
18050
|
const feeder = new DerivedStreamFeeder({
|
|
17957
18051
|
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
17958
18052
|
args,
|
|
@@ -17987,6 +18081,21 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17987
18081
|
};
|
|
17988
18082
|
}
|
|
17989
18083
|
/**
|
|
18084
|
+
* The audio codec the derived stream's SOURCE broker has actually observed,
|
|
18085
|
+
* or `null` when there is nothing to read (source not resolved, broker not
|
|
18086
|
+
* created yet, cold dial, no audio track).
|
|
18087
|
+
*
|
|
18088
|
+
* Only ever an input to "can this track be COPIED into the derived MPEG-TS".
|
|
18089
|
+
* `null` resolves to the AAC re-encode, so every failure of this read lands
|
|
18090
|
+
* on the branch that still produces audible audio — the read can never mute
|
|
18091
|
+
* a camera, which is the direction that cost 618 four silent days.
|
|
18092
|
+
*/
|
|
18093
|
+
observedSourceAudioCodec(deviceId, source) {
|
|
18094
|
+
const camStreamId = source.kind === "camStream" ? source.camStreamId : this.assignments.get(deviceId)?.map[source.profile] ?? null;
|
|
18095
|
+
if (camStreamId === null) return null;
|
|
18096
|
+
return this.brokers.get(brokerIdFor(deviceId, camStreamId))?.getStats().audio?.codec ?? null;
|
|
18097
|
+
}
|
|
18098
|
+
/**
|
|
17990
18099
|
* Whether a stream is warmed BY DEFAULT (no explicit override): the device's
|
|
17991
18100
|
* `high` profile, on a non-battery camera, from a source that is worth
|
|
17992
18101
|
* keeping hot. Single source of truth used by both the broker-config default
|
|
@@ -22280,7 +22389,10 @@ var ControlMessageSchema = discriminatedUnion("t", [
|
|
|
22280
22389
|
m: _enum(["fine", "coarse"]).optional(),
|
|
22281
22390
|
v: number().optional()
|
|
22282
22391
|
}),
|
|
22283
|
-
object({
|
|
22392
|
+
object({
|
|
22393
|
+
t: literal("scrubAck"),
|
|
22394
|
+
seq: number().optional()
|
|
22395
|
+
}),
|
|
22284
22396
|
object({
|
|
22285
22397
|
t: literal("warm"),
|
|
22286
22398
|
epoch: number(),
|
|
@@ -22325,7 +22437,8 @@ var ServerMessageSchema = discriminatedUnion("t", [
|
|
|
22325
22437
|
}),
|
|
22326
22438
|
object({
|
|
22327
22439
|
t: literal("position"),
|
|
22328
|
-
epochMs: number()
|
|
22440
|
+
epochMs: number(),
|
|
22441
|
+
seq: number().optional()
|
|
22329
22442
|
}),
|
|
22330
22443
|
object({
|
|
22331
22444
|
t: literal("liveProfile"),
|
|
@@ -22532,6 +22645,7 @@ var ScrubDragStats = class {
|
|
|
22532
22645
|
wholeReads = 0;
|
|
22533
22646
|
readBytes = 0;
|
|
22534
22647
|
parks = 0;
|
|
22648
|
+
ackTimeouts = 0;
|
|
22535
22649
|
failures = 0;
|
|
22536
22650
|
firstPushMs = null;
|
|
22537
22651
|
creditEndedAt = null;
|
|
@@ -22578,6 +22692,10 @@ var ScrubDragStats = class {
|
|
|
22578
22692
|
recordPark() {
|
|
22579
22693
|
this.parks += 1;
|
|
22580
22694
|
}
|
|
22695
|
+
/** Pushes written off after the ack timeout — dropped work, never silent. */
|
|
22696
|
+
recordAckTimeout(pushes) {
|
|
22697
|
+
this.ackTimeouts += pushes;
|
|
22698
|
+
}
|
|
22581
22699
|
/** A swallowed `scrubTick` throw — the branch where a dark failure hides. */
|
|
22582
22700
|
recordFailure() {
|
|
22583
22701
|
this.failures += 1;
|
|
@@ -22627,6 +22745,7 @@ var ScrubDragStats = class {
|
|
|
22627
22745
|
travel: this.travel,
|
|
22628
22746
|
bytes: this.bytes,
|
|
22629
22747
|
parks: this.parks,
|
|
22748
|
+
ackTimeouts: this.ackTimeouts,
|
|
22630
22749
|
failures: this.failures,
|
|
22631
22750
|
creditEndedAt: this.creditEndedAt,
|
|
22632
22751
|
acks: ack.n,
|
|
@@ -22733,16 +22852,37 @@ var SCRUB_PREFETCH_DIR_THRESHOLD = 2;
|
|
|
22733
22852
|
*/
|
|
22734
22853
|
var SCRUB_SWEEP_WINDOW_MS = 4e3;
|
|
22735
22854
|
/**
|
|
22736
|
-
* Receiver-credit cap for scrub pushes: at most this many
|
|
22737
|
-
* OUTSTANDING (
|
|
22738
|
-
*
|
|
22739
|
-
*
|
|
22740
|
-
*
|
|
22741
|
-
*
|
|
22742
|
-
*
|
|
22743
|
-
* `scrubPendingEpochMs` until an ack
|
|
22855
|
+
* Receiver-credit cap for scrub pushes: at most this many PUSHES may be
|
|
22856
|
+
* OUTSTANDING (on the track, not yet acknowledged by the viewer via
|
|
22857
|
+
* `scrubAck`). The coalescing window above shapes the INPUT; on a thin pipe it
|
|
22858
|
+
* is not backpressure — pushing a ~45 KB keyframe every 60 ms floods a link
|
|
22859
|
+
* that delivers one every 500 ms, RTP drops, and nothing paints. Backpressure
|
|
22860
|
+
* is the receiver's: a push spends a credit, the first ack naming it restores
|
|
22861
|
+
* that credit, and with zero credit the newest target PARKS on
|
|
22862
|
+
* `scrubPendingEpochMs` until an ack (or {@link SCRUB_ACK_TIMEOUT_MS}) arrives.
|
|
22863
|
+
*
|
|
22864
|
+
* THE UNIT IS THE PUSH, NOT THE FRAME — and getting that wrong is what made a
|
|
22865
|
+
* slowed drag die. A push carries one AU in `jump` mode but a whole sweep (~25
|
|
22866
|
+
* AUs at 25 fps over a 1 s step) in `sweep` mode, while the viewer emits ONE
|
|
22867
|
+
* position per push. Charging one credit per push and repaying one per FRAME
|
|
22868
|
+
* meant two sweeps drained the cap against a viewer that could only ever send
|
|
22869
|
+
* two acks: the drag parked permanently at the exact moment the operator
|
|
22870
|
+
* slowed down to look at something.
|
|
22744
22871
|
*/
|
|
22745
22872
|
var SCRUB_CREDIT_CAP = 2;
|
|
22873
|
+
/**
|
|
22874
|
+
* How long an outstanding push may go unacknowledged before the feeder writes
|
|
22875
|
+
* it off and takes its credit back.
|
|
22876
|
+
*
|
|
22877
|
+
* The credit gate is released by an ack and by nothing else — which is right
|
|
22878
|
+
* while acks flow and fatal when one is lost: the drag parks with no timer to
|
|
22879
|
+
* release it, and `beginScrub` (the only other reset) is a documented no-op
|
|
22880
|
+
* mid-drag. Generous on purpose: this is not pacing, it is the bound that
|
|
22881
|
+
* makes a lost ack cost one stalled tick instead of the rest of the gesture.
|
|
22882
|
+
* Every write-off is logged and counted (`ackTimeouts` on the drag census) —
|
|
22883
|
+
* dropped work is never silent.
|
|
22884
|
+
*/
|
|
22885
|
+
var SCRUB_ACK_TIMEOUT_MS = 2e3;
|
|
22746
22886
|
/** Minimum gap between two lines of the same pathological kind, per drag. */
|
|
22747
22887
|
var SCRUB_PATHOLOGY_LOG_INTERVAL_MS = 1e3;
|
|
22748
22888
|
/**
|
|
@@ -22840,13 +22980,27 @@ var RecordedFeeder = class {
|
|
|
22840
22980
|
/** Remaining receiver credit for scrub pushes — see {@link SCRUB_CREDIT_CAP}. */
|
|
22841
22981
|
scrubCredit = SCRUB_CREDIT_CAP;
|
|
22842
22982
|
/**
|
|
22843
|
-
*
|
|
22844
|
-
*
|
|
22845
|
-
*
|
|
22846
|
-
*
|
|
22847
|
-
* once per frame for the single credit it cost.
|
|
22983
|
+
* The pushes on the wire and not yet settled, OLDEST FIRST (and therefore
|
|
22984
|
+
* ascending in both `seq` and `pushedAtMs` — every settle and every write-off
|
|
22985
|
+
* takes a prefix). One entry per push, whatever it carried: the first
|
|
22986
|
+
* `scrubAck` naming it returns its credit.
|
|
22848
22987
|
*/
|
|
22849
22988
|
scrubOutstanding = [];
|
|
22989
|
+
/**
|
|
22990
|
+
* The push sequence the viewer echoes on `scrubAck`. Monotonic for the life of
|
|
22991
|
+
* the feeder — see {@link OutstandingPush} for why it must NOT restart per
|
|
22992
|
+
* drag.
|
|
22993
|
+
*/
|
|
22994
|
+
scrubPushSeq = 0;
|
|
22995
|
+
/**
|
|
22996
|
+
* The write-off timer for the oldest outstanding push
|
|
22997
|
+
* ({@link SCRUB_ACK_TIMEOUT_MS}). Separate from `scrubTimer` on purpose: that
|
|
22998
|
+
* one is the coalescing slot, and an ack arriving while this is pending must
|
|
22999
|
+
* be able to schedule a leading-edge tick without waiting on it.
|
|
23000
|
+
*/
|
|
23001
|
+
scrubAckTimer = null;
|
|
23002
|
+
/** One `scrub-feeder:ack-timeout` line per drag — the count is on the census. */
|
|
23003
|
+
scrubAckTimeoutLogged = false;
|
|
22850
23004
|
/** The newest un-processed drag target (null when the tick cycle has drained). */
|
|
22851
23005
|
scrubPendingEpochMs = null;
|
|
22852
23006
|
/** The mapping hint that came with the newest target (latest-wins with it). */
|
|
@@ -22947,6 +23101,7 @@ var RecordedFeeder = class {
|
|
|
22947
23101
|
this.scrubSegment = null;
|
|
22948
23102
|
this.scrubCursor = null;
|
|
22949
23103
|
this.scrubOutstanding = [];
|
|
23104
|
+
this.clearScrubAckTimeout();
|
|
22950
23105
|
this.scrubPendingHint = null;
|
|
22951
23106
|
this.scrubPrevTarget = null;
|
|
22952
23107
|
this.scrubDirRun = 0;
|
|
@@ -23202,6 +23357,8 @@ var RecordedFeeder = class {
|
|
|
23202
23357
|
this.scrubNoFootageLogged = false;
|
|
23203
23358
|
this.scrubParkLogged = false;
|
|
23204
23359
|
this.scrubOutstanding = [];
|
|
23360
|
+
this.clearScrubAckTimeout();
|
|
23361
|
+
this.scrubAckTimeoutLogged = false;
|
|
23205
23362
|
this.scrubPendingHint = null;
|
|
23206
23363
|
this.scrubStartedAtMs = performance.now();
|
|
23207
23364
|
this.scrubStats = new ScrubDragStats(this.scrubStartedAtMs);
|
|
@@ -23228,28 +23385,97 @@ var RecordedFeeder = class {
|
|
|
23228
23385
|
if (this.scrubTimer === null) this.scrubTimer = setTimeout(() => void this.scrubTick(), 0);
|
|
23229
23386
|
}
|
|
23230
23387
|
/**
|
|
23231
|
-
* Receiver credit: the viewer
|
|
23232
|
-
*
|
|
23233
|
-
*
|
|
23234
|
-
*
|
|
23388
|
+
* Receiver credit: the viewer received a pushed scrub frame. Settles the push
|
|
23389
|
+
* `seq` names — and every older one still outstanding, since the channel is
|
|
23390
|
+
* ordered — restores their credit (capped: over-acking never banks a burst)
|
|
23391
|
+
* and, when a target sat parked on an exhausted budget, resumes the tick.
|
|
23392
|
+
* This — not any timer — is what paces scrub pushes to what the link
|
|
23393
|
+
* actually delivers; {@link SCRUB_ACK_TIMEOUT_MS} is only the bound for when
|
|
23394
|
+
* an ack never comes.
|
|
23395
|
+
*
|
|
23396
|
+
* THE UNIT IS THE PUSH. A sweep puts ~25 AUs on the wire under ONE position
|
|
23397
|
+
* message, so a viewer without `requestVideoFrameCallback` can only ever send
|
|
23398
|
+
* ONE ack for it; the old per-FRAME settle waited for 25 and the drag parked
|
|
23399
|
+
* forever the moment the operator slowed down. A viewer WITH rvfc sends one
|
|
23400
|
+
* ack per rendered frame instead — those surplus acks all name the same
|
|
23401
|
+
* settled `seq` and are no-ops, which is what `seq` is for.
|
|
23402
|
+
*
|
|
23403
|
+
* A `seq` no outstanding push matches (a late ack from a drag that already
|
|
23404
|
+
* ended) banks nothing. An ack without `seq` comes from a viewer older than
|
|
23405
|
+
* the field: it settles the oldest outstanding push, which can only repay too
|
|
23406
|
+
* early — never strand the drag.
|
|
23235
23407
|
*/
|
|
23236
|
-
scrubAck() {
|
|
23408
|
+
scrubAck(seq) {
|
|
23237
23409
|
if (this.disposed) return;
|
|
23410
|
+
const settled = seq === void 0 ? this.scrubOutstanding.length > 0 ? 1 : 0 : this.scrubOutstanding.filter((p) => p.seq <= seq).length;
|
|
23411
|
+
if (settled === 0) return;
|
|
23412
|
+
const now = performance.now();
|
|
23413
|
+
for (const push of this.scrubOutstanding.slice(0, settled)) this.scrubStats?.recordAckRtt(Math.round(now - push.pushedAtMs));
|
|
23414
|
+
this.scrubOutstanding = this.scrubOutstanding.slice(settled);
|
|
23415
|
+
this.scrubCredit = Math.min(SCRUB_CREDIT_CAP, this.scrubCredit + settled);
|
|
23416
|
+
this.resumeScrubTick();
|
|
23417
|
+
}
|
|
23418
|
+
/**
|
|
23419
|
+
* Resume a parked drag: the newest target is still pending and the coalescing
|
|
23420
|
+
* slot is free, so run a leading-edge tick. Never touches a LIVE slot — a
|
|
23421
|
+
* pending re-arm is this tick cycle's own coalescing window and pre-empting it
|
|
23422
|
+
* would push faster than {@link SCRUB_COALESCE_MS}.
|
|
23423
|
+
*/
|
|
23424
|
+
resumeScrubTick() {
|
|
23425
|
+
if (this.scrubbing && this.scrubPendingEpochMs !== null && this.scrubTimer === null) this.scrubTimer = setTimeout(() => void this.scrubTick(), 0);
|
|
23426
|
+
}
|
|
23427
|
+
/**
|
|
23428
|
+
* Arm the write-off timer for the OLDEST outstanding push, if one is not
|
|
23429
|
+
* already pending. Newer entries are always due later, so a timer armed for
|
|
23430
|
+
* an entry that gets settled first simply fires early and re-arms — one
|
|
23431
|
+
* harmless wakeup, no bookkeeping.
|
|
23432
|
+
*/
|
|
23433
|
+
armScrubAckTimeout() {
|
|
23434
|
+
if (this.scrubAckTimer !== null) return;
|
|
23238
23435
|
const head = this.scrubOutstanding[0];
|
|
23239
23436
|
if (head === void 0) return;
|
|
23437
|
+
const dueInMs = Math.max(0, SCRUB_ACK_TIMEOUT_MS - (performance.now() - head.pushedAtMs));
|
|
23438
|
+
this.scrubAckTimer = setTimeout(() => {
|
|
23439
|
+
this.scrubAckTimer = null;
|
|
23440
|
+
this.expireScrubAcks();
|
|
23441
|
+
}, dueInMs);
|
|
23442
|
+
}
|
|
23443
|
+
/** Drop the ack timer (drag exit / teardown). */
|
|
23444
|
+
clearScrubAckTimeout() {
|
|
23445
|
+
if (this.scrubAckTimer === null) return;
|
|
23446
|
+
clearTimeout(this.scrubAckTimer);
|
|
23447
|
+
this.scrubAckTimer = null;
|
|
23448
|
+
}
|
|
23449
|
+
/**
|
|
23450
|
+
* Write off every push that has waited longer than
|
|
23451
|
+
* {@link SCRUB_ACK_TIMEOUT_MS}, give its credit back, and resume the drag.
|
|
23452
|
+
*
|
|
23453
|
+
* This is DROPPED WORK — a frame the viewer may never have painted — so it is
|
|
23454
|
+
* counted on the census every time and logged once per drag. Without it a
|
|
23455
|
+
* single lost ack ends the gesture: the gate is released by acks alone, and
|
|
23456
|
+
* `beginScrub` (its only other reset) is a no-op mid-drag.
|
|
23457
|
+
*/
|
|
23458
|
+
expireScrubAcks() {
|
|
23459
|
+
if (this.disposed || !this.scrubbing) return;
|
|
23240
23460
|
const now = performance.now();
|
|
23241
|
-
|
|
23242
|
-
if (
|
|
23243
|
-
this.scrubOutstanding
|
|
23244
|
-
|
|
23245
|
-
|
|
23246
|
-
|
|
23247
|
-
|
|
23248
|
-
|
|
23461
|
+
const expired = this.scrubOutstanding.filter((p) => now - p.pushedAtMs >= SCRUB_ACK_TIMEOUT_MS).length;
|
|
23462
|
+
if (expired > 0) {
|
|
23463
|
+
const frames = this.scrubOutstanding.slice(0, expired).reduce((sum, p) => sum + p.frames, 0);
|
|
23464
|
+
this.scrubOutstanding = this.scrubOutstanding.slice(expired);
|
|
23465
|
+
this.scrubCredit = Math.min(SCRUB_CREDIT_CAP, this.scrubCredit + expired);
|
|
23466
|
+
this.scrubStats?.recordAckTimeout(expired);
|
|
23467
|
+
if (!this.scrubAckTimeoutLogged) {
|
|
23468
|
+
this.scrubAckTimeoutLogged = true;
|
|
23469
|
+
this.deps.onLog?.("scrub-feeder:ack-timeout", {
|
|
23470
|
+
pushes: expired,
|
|
23471
|
+
frames,
|
|
23472
|
+
timeoutMs: SCRUB_ACK_TIMEOUT_MS,
|
|
23473
|
+
pendingEpochMs: this.scrubPendingEpochMs
|
|
23474
|
+
});
|
|
23475
|
+
}
|
|
23476
|
+
this.resumeScrubTick();
|
|
23249
23477
|
}
|
|
23250
|
-
this.
|
|
23251
|
-
this.scrubCredit = Math.min(SCRUB_CREDIT_CAP, this.scrubCredit + 1);
|
|
23252
|
-
if (this.scrubbing && this.scrubPendingEpochMs !== null && this.scrubTimer === null) this.scrubTimer = setTimeout(() => void this.scrubTick(), 0);
|
|
23478
|
+
this.armScrubAckTimeout();
|
|
23253
23479
|
}
|
|
23254
23480
|
/**
|
|
23255
23481
|
* One coalesced feeder-scrub push. Captures the newest pending target + the
|
|
@@ -23422,12 +23648,14 @@ var RecordedFeeder = class {
|
|
|
23422
23648
|
else if (crossesFragment && this.clockHasAdvanced) this.clock.markSeek();
|
|
23423
23649
|
for (const au of aus) this.deps.sink.pushAccessUnit(au.data, this.clock.stamp(au.ptsMs), au.codec);
|
|
23424
23650
|
const pushedAtMs = performance.now();
|
|
23651
|
+
const pushSeq = ++this.scrubPushSeq;
|
|
23425
23652
|
this.scrubOutstanding = [...this.scrubOutstanding, {
|
|
23653
|
+
seq: pushSeq,
|
|
23426
23654
|
frames: jumpTravel.length + aus.length,
|
|
23427
|
-
pushedAtMs
|
|
23428
|
-
firstAckAtMs: null
|
|
23655
|
+
pushedAtMs
|
|
23429
23656
|
}];
|
|
23430
23657
|
this.scrubCredit -= 1;
|
|
23658
|
+
this.armScrubAckTimeout();
|
|
23431
23659
|
this.clockHasAdvanced = true;
|
|
23432
23660
|
let runBytes = 0;
|
|
23433
23661
|
for (const s of jumpTravel) runBytes += s.au.data.length;
|
|
@@ -23474,7 +23702,7 @@ var RecordedFeeder = class {
|
|
|
23474
23702
|
tickMs: Math.round(pushedAtMs - tickStartMs)
|
|
23475
23703
|
});
|
|
23476
23704
|
}
|
|
23477
|
-
this.deps.onPosition(this.clampIntoSegment(seg, effTarget));
|
|
23705
|
+
this.deps.onPosition(this.clampIntoSegment(seg, effTarget), pushSeq);
|
|
23478
23706
|
this.maybeScrubPrefetch(seg, effTarget, gen);
|
|
23479
23707
|
} catch (err) {
|
|
23480
23708
|
this.scrubStats?.recordFailure();
|
|
@@ -24187,6 +24415,7 @@ var RecordedFeeder = class {
|
|
|
24187
24415
|
this.scrubNoFootageLogged = false;
|
|
24188
24416
|
this.scrubParkLogged = false;
|
|
24189
24417
|
this.scrubOutstanding = [];
|
|
24418
|
+
this.clearScrubAckTimeout();
|
|
24190
24419
|
this.scrubPendingHint = null;
|
|
24191
24420
|
if (this.scrubTimer) {
|
|
24192
24421
|
clearTimeout(this.scrubTimer);
|
|
@@ -24266,7 +24495,7 @@ var TimelineSession = class {
|
|
|
24266
24495
|
break;
|
|
24267
24496
|
}
|
|
24268
24497
|
case "scrubAck":
|
|
24269
|
-
if (this.mode === "recorded") this.feeder?.scrubAck();
|
|
24498
|
+
if (this.mode === "recorded") this.feeder?.scrubAck(msg.seq);
|
|
24270
24499
|
break;
|
|
24271
24500
|
case "setRate":
|
|
24272
24501
|
this.rate = msg.rate;
|
|
@@ -24314,12 +24543,18 @@ var TimelineSession = class {
|
|
|
24314
24543
|
state
|
|
24315
24544
|
}));
|
|
24316
24545
|
}
|
|
24317
|
-
/** Emit seam: the broker wires the feeder's onPosition callback to this.
|
|
24318
|
-
|
|
24546
|
+
/** Emit seam: the broker wires the feeder's onPosition callback to this.
|
|
24547
|
+
* `pushSeq` rides along only on the position a scrub push emitted — the
|
|
24548
|
+
* viewer echoes it on `scrubAck` so the credit is repaid to the right push. */
|
|
24549
|
+
_emitPosition(epochMs, pushSeq) {
|
|
24319
24550
|
this.lastPositionMs = epochMs;
|
|
24320
|
-
this.deps.session.sendControl(serializeServerMessage({
|
|
24551
|
+
this.deps.session.sendControl(serializeServerMessage(pushSeq === void 0 ? {
|
|
24321
24552
|
t: "position",
|
|
24322
24553
|
epochMs
|
|
24554
|
+
} : {
|
|
24555
|
+
t: "position",
|
|
24556
|
+
epochMs,
|
|
24557
|
+
seq: pushSeq
|
|
24323
24558
|
}));
|
|
24324
24559
|
}
|
|
24325
24560
|
emitLiveProfile() {
|
|
@@ -32994,7 +33229,7 @@ var BrokerWebrtcServer = class {
|
|
|
32994
33229
|
} });
|
|
32995
33230
|
timeline._emitState(s);
|
|
32996
33231
|
},
|
|
32997
|
-
onPosition: (epochMs) => timeline._emitPosition(epochMs),
|
|
33232
|
+
onPosition: (epochMs, pushSeq) => timeline._emitPosition(epochMs, pushSeq),
|
|
32998
33233
|
onLog: (msg, data) => sessionLogger.info(msg, {
|
|
32999
33234
|
tags: { deviceId },
|
|
33000
33235
|
meta: {
|
|
@@ -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-De_xzvjU.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { g as NativeLeaseSettingsSchema, h as NativeLeaseAdmissionSchema, l as DEFAULT_NATIVE_LEASE_SETTINGS } from "./dist-
|
|
1
|
+
import { g as NativeLeaseSettingsSchema, h as NativeLeaseAdmissionSchema, l as DEFAULT_NATIVE_LEASE_SETTINGS } from "./dist-HhcoV-Ky.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.77",
|
|
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",
|