@camstack/addon-decoder-nodeav 1.1.4 → 1.1.5
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/index.js +594 -17
- package/dist/index.mjs +595 -18
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { FrameRingReaderCache, FrameRingWriter, MIN_RING_SLOTS, computeSegmentSize, computeSlotByteLength, createSegment, deriveSlotCount } from "@camstack/shm-ring";
|
|
1
|
+
import { FrameRingReaderCache, FrameRingWriter, MIN_RING_SLOTS, computeSegmentSize, computeSlotByteLength, createSegment, deriveSlotCount, unlinkSegment } from "@camstack/shm-ring";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { readdirSync } from "node:fs";
|
|
3
4
|
//#region src/frame-ring-sink.ts
|
|
4
5
|
/**
|
|
5
6
|
* `DecoderFrameRingSink` — the decoder's shared-memory write side (Phase 5 / D9).
|
|
@@ -67,10 +68,16 @@ var RING_BUDGET_BYTES = RING_BUDGET_MB * 1024 * 1024;
|
|
|
67
68
|
* segment (resolution change) a distinct name so a stale consumer mapping is
|
|
68
69
|
* never silently reused.
|
|
69
70
|
*/
|
|
71
|
+
/**
|
|
72
|
+
* Shared prefix for every decoder shm segment name. Startup orphan reclamation
|
|
73
|
+
* (`purgeOrphanSegments`) keys off this to find segments left behind by a
|
|
74
|
+
* crashed prior instance.
|
|
75
|
+
*/
|
|
76
|
+
var SEGMENT_NAME_PREFIX = "csf.";
|
|
70
77
|
function makeSegmentName(seed, generation) {
|
|
71
78
|
let hash = 5381;
|
|
72
79
|
for (let i = 0; i < seed.length; i += 1) hash = (hash << 5) + hash + seed.charCodeAt(i) | 0;
|
|
73
|
-
return
|
|
80
|
+
return `${SEGMENT_NAME_PREFIX}${(hash >>> 0).toString(36)}.${generation}`;
|
|
74
81
|
}
|
|
75
82
|
/**
|
|
76
83
|
* The decoder-side owner of one stream's shared-memory frame ring.
|
|
@@ -4893,7 +4900,7 @@ function _instanceof(cls, params = {}) {
|
|
|
4893
4900
|
return inst;
|
|
4894
4901
|
}
|
|
4895
4902
|
//#endregion
|
|
4896
|
-
//#region ../types/dist/sleep-
|
|
4903
|
+
//#region ../types/dist/sleep-CZDdRBua.mjs
|
|
4897
4904
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4898
4905
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4899
4906
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -7500,7 +7507,16 @@ var DecoderStatsSchema = object({
|
|
|
7500
7507
|
inputFps: number(),
|
|
7501
7508
|
outputFps: number(),
|
|
7502
7509
|
avgDecodeTimeMs: number(),
|
|
7503
|
-
droppedFrames: number()
|
|
7510
|
+
droppedFrames: number(),
|
|
7511
|
+
/**
|
|
7512
|
+
* Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
|
|
7513
|
+
* lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
|
|
7514
|
+
* the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
|
|
7515
|
+
* is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
|
|
7516
|
+
*/
|
|
7517
|
+
lagMs: number().optional(),
|
|
7518
|
+
effectiveFps: number().optional(),
|
|
7519
|
+
adaptiveFps: number().optional()
|
|
7504
7520
|
});
|
|
7505
7521
|
var DecoderSessionConfigSchema = object({
|
|
7506
7522
|
codec: string(),
|
|
@@ -7541,7 +7557,15 @@ var DecoderSessionConfigSchema = object({
|
|
|
7541
7557
|
* other — `pullFrames` returns nothing for an `'shm'` session and
|
|
7542
7558
|
* `pullHandles` returns nothing for a `'callback'` session.
|
|
7543
7559
|
*/
|
|
7544
|
-
frameSink: _enum(["callback", "shm"]).default("callback")
|
|
7560
|
+
frameSink: _enum(["callback", "shm"]).default("callback"),
|
|
7561
|
+
/**
|
|
7562
|
+
* Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
|
|
7563
|
+
* a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
|
|
7564
|
+
* real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
|
|
7565
|
+
* stream-broker's `streamingDebug` gate — off by default so production logs
|
|
7566
|
+
* stay quiet and the emit path pays zero per-frame cost when disabled.
|
|
7567
|
+
*/
|
|
7568
|
+
debug: boolean().optional()
|
|
7545
7569
|
});
|
|
7546
7570
|
var EncodeProfileSchema = object({
|
|
7547
7571
|
video: object({
|
|
@@ -9699,6 +9723,75 @@ DeviceType.Cover, method(object({ deviceId: number().int().nonnegative() }), _vo
|
|
|
9699
9723
|
auth: "admin"
|
|
9700
9724
|
});
|
|
9701
9725
|
/**
|
|
9726
|
+
* Vendor-neutral day/night (IR-cut) control — the per-camera config cap
|
|
9727
|
+
* shared by reolink / hikvision / amcrest. Models the common firmware
|
|
9728
|
+
* surface: the IR-cut switching MODE plus the two knobs that gate it
|
|
9729
|
+
* (photocell `sensitivity` + `switchDelaySec`). Each vendor maps these
|
|
9730
|
+
* onto its own ISAPI / Baichuan / Dahua-CGI fields; the cap standardises
|
|
9731
|
+
* the shape so ONE derived-form renders every camera.
|
|
9732
|
+
*
|
|
9733
|
+
* Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
|
|
9734
|
+
* `getOptions` advertises per-camera availability, `getStatus` (auto-
|
|
9735
|
+
* injected from `status`) reports the live values, and a single
|
|
9736
|
+
* `setSettings` mutation applies a partial change. No hand-written
|
|
9737
|
+
* settings-contribution methods — the framework derives the UI + save
|
|
9738
|
+
* routing from this surface.
|
|
9739
|
+
*/
|
|
9740
|
+
/** IR-cut switching mode. `schedule` = time-of-day table configured on the camera. */
|
|
9741
|
+
var DayNightModeSchema = _enum([
|
|
9742
|
+
"auto",
|
|
9743
|
+
"day",
|
|
9744
|
+
"night",
|
|
9745
|
+
"schedule"
|
|
9746
|
+
]);
|
|
9747
|
+
/** Normalized numeric range descriptor — `{ min, max, step }` per the
|
|
9748
|
+
* getOptions availability convention. Normalized values are 0–100. */
|
|
9749
|
+
var NormalizedRangeSchema$1 = object({
|
|
9750
|
+
min: number(),
|
|
9751
|
+
max: number(),
|
|
9752
|
+
step: number()
|
|
9753
|
+
});
|
|
9754
|
+
object({
|
|
9755
|
+
mode: DayNightModeSchema,
|
|
9756
|
+
/** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
|
|
9757
|
+
sensitivity: number().optional(),
|
|
9758
|
+
/** Delay before the IR-cut filter flips, in seconds. */
|
|
9759
|
+
switchDelaySec: number().optional(),
|
|
9760
|
+
lastFetchedAt: number()
|
|
9761
|
+
});
|
|
9762
|
+
/**
|
|
9763
|
+
* Per-camera availability descriptor — drives which controls the admin UI
|
|
9764
|
+
* renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
|
|
9765
|
+
* (normalized 0–100); the mode choice-set as an array. A provider returns
|
|
9766
|
+
* honest, camera-probed values — never hardcoded.
|
|
9767
|
+
*/
|
|
9768
|
+
var DayNightOptionsSchema = object({
|
|
9769
|
+
/** Modes this camera accepts. Empty → the camera has no configurable day/night mode. */
|
|
9770
|
+
modes: array(DayNightModeSchema),
|
|
9771
|
+
supportsSensitivity: boolean(),
|
|
9772
|
+
/** Present when `supportsSensitivity` — the normalized 0–100 range. */
|
|
9773
|
+
sensitivity: NormalizedRangeSchema$1.optional(),
|
|
9774
|
+
supportsSwitchDelay: boolean(),
|
|
9775
|
+
/** Present when `supportsSwitchDelay` — the allowed delay range in seconds. */
|
|
9776
|
+
switchDelaySec: NormalizedRangeSchema$1.optional()
|
|
9777
|
+
});
|
|
9778
|
+
/**
|
|
9779
|
+
* Partial change to the day/night config — every field optional. A
|
|
9780
|
+
* provider ignores fields it does not support.
|
|
9781
|
+
*/
|
|
9782
|
+
var DayNightSettingsPatchSchema = object({
|
|
9783
|
+
mode: DayNightModeSchema.optional(),
|
|
9784
|
+
sensitivity: number().optional(),
|
|
9785
|
+
switchDelaySec: number().optional()
|
|
9786
|
+
});
|
|
9787
|
+
DeviceType.Camera, method(object({ deviceId: number() }), DayNightOptionsSchema), method(object({
|
|
9788
|
+
deviceId: number(),
|
|
9789
|
+
settings: DayNightSettingsPatchSchema
|
|
9790
|
+
}), _void(), {
|
|
9791
|
+
kind: "mutation",
|
|
9792
|
+
auth: "admin"
|
|
9793
|
+
});
|
|
9794
|
+
/**
|
|
9702
9795
|
* Identity envelope for a device's upstream-system metadata.
|
|
9703
9796
|
*
|
|
9704
9797
|
* Two jobs:
|
|
@@ -10054,6 +10147,130 @@ object({
|
|
|
10054
10147
|
});
|
|
10055
10148
|
DeviceType.Image;
|
|
10056
10149
|
/**
|
|
10150
|
+
* Vendor-neutral image / picture-adjustment cap — the per-camera config
|
|
10151
|
+
* cap shared by reolink / hikvision / amcrest. Models the common ISP
|
|
10152
|
+
* surface: the four picture sliders (brightness / contrast / saturation /
|
|
10153
|
+
* sharpness), orientation (mirror / flip / rotate), white-balance,
|
|
10154
|
+
* exposure and backlight-compensation modes.
|
|
10155
|
+
*
|
|
10156
|
+
* NORMALIZATION: every slider is a normalized int 0–100. Vendors expose
|
|
10157
|
+
* these natively as 0–100 or 0–255 (or other ranges); each provider maps
|
|
10158
|
+
* its native range to/from this normalized 0–100 space so the cap surface
|
|
10159
|
+
* (and the derived form) is identical across cameras. `warmth` (manual
|
|
10160
|
+
* white-balance) is likewise normalized 0–100.
|
|
10161
|
+
*
|
|
10162
|
+
* Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
|
|
10163
|
+
* `getOptions` advertises per-camera availability, `getStatus` (auto-
|
|
10164
|
+
* injected from `status`) reports the live values, and a single
|
|
10165
|
+
* `setSettings` mutation applies a partial change. No hand-written
|
|
10166
|
+
* settings-contribution methods — the framework derives the UI + save
|
|
10167
|
+
* routing from this surface.
|
|
10168
|
+
*/
|
|
10169
|
+
/** Sensor/image rotation, degrees clockwise. */
|
|
10170
|
+
var ImageRotateSchema = _enum([
|
|
10171
|
+
"0",
|
|
10172
|
+
"90",
|
|
10173
|
+
"180",
|
|
10174
|
+
"270"
|
|
10175
|
+
]);
|
|
10176
|
+
/** White-balance mode. `manual` unlocks the normalized `warmth` knob. */
|
|
10177
|
+
var WhiteBalanceModeSchema = _enum(["auto", "manual"]);
|
|
10178
|
+
/** Exposure mode. */
|
|
10179
|
+
var ExposureModeSchema = _enum(["auto", "manual"]);
|
|
10180
|
+
/**
|
|
10181
|
+
* Backlight-compensation mode:
|
|
10182
|
+
* - `off` — disabled
|
|
10183
|
+
* - `blc` — backlight compensation
|
|
10184
|
+
* - `wdr` — wide dynamic range
|
|
10185
|
+
* - `hlc` — highlight compensation
|
|
10186
|
+
*/
|
|
10187
|
+
var BacklightModeSchema = _enum([
|
|
10188
|
+
"off",
|
|
10189
|
+
"blc",
|
|
10190
|
+
"wdr",
|
|
10191
|
+
"hlc"
|
|
10192
|
+
]);
|
|
10193
|
+
/** Normalized numeric range descriptor — `{ min, max, step }` per the
|
|
10194
|
+
* getOptions availability convention. Slider values are normalized 0–100. */
|
|
10195
|
+
var NormalizedRangeSchema = object({
|
|
10196
|
+
min: number(),
|
|
10197
|
+
max: number(),
|
|
10198
|
+
step: number()
|
|
10199
|
+
});
|
|
10200
|
+
object({
|
|
10201
|
+
/** Normalized 0–100. */
|
|
10202
|
+
brightness: number().optional(),
|
|
10203
|
+
/** Normalized 0–100. */
|
|
10204
|
+
contrast: number().optional(),
|
|
10205
|
+
/** Normalized 0–100. */
|
|
10206
|
+
saturation: number().optional(),
|
|
10207
|
+
/** Normalized 0–100. */
|
|
10208
|
+
sharpness: number().optional(),
|
|
10209
|
+
mirror: boolean().optional(),
|
|
10210
|
+
flip: boolean().optional(),
|
|
10211
|
+
rotate: ImageRotateSchema.optional(),
|
|
10212
|
+
whiteBalance: WhiteBalanceModeSchema.optional(),
|
|
10213
|
+
/** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
|
|
10214
|
+
warmth: number().optional(),
|
|
10215
|
+
exposureMode: ExposureModeSchema.optional(),
|
|
10216
|
+
backlightMode: BacklightModeSchema.optional(),
|
|
10217
|
+
lastFetchedAt: number()
|
|
10218
|
+
});
|
|
10219
|
+
/**
|
|
10220
|
+
* Per-camera availability descriptor — drives which controls the admin UI
|
|
10221
|
+
* renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
|
|
10222
|
+
* (the normalized 0–100 range); enums as arrays of supported values (empty
|
|
10223
|
+
* array → control hidden). A provider returns honest, camera-probed values
|
|
10224
|
+
* — never hardcoded.
|
|
10225
|
+
*/
|
|
10226
|
+
var ImageSettingsOptionsSchema = object({
|
|
10227
|
+
supportsBrightness: boolean(),
|
|
10228
|
+
brightness: NormalizedRangeSchema.optional(),
|
|
10229
|
+
supportsContrast: boolean(),
|
|
10230
|
+
contrast: NormalizedRangeSchema.optional(),
|
|
10231
|
+
supportsSaturation: boolean(),
|
|
10232
|
+
saturation: NormalizedRangeSchema.optional(),
|
|
10233
|
+
supportsSharpness: boolean(),
|
|
10234
|
+
sharpness: NormalizedRangeSchema.optional(),
|
|
10235
|
+
supportsMirror: boolean(),
|
|
10236
|
+
supportsFlip: boolean(),
|
|
10237
|
+
/** Supported rotation values. Empty → rotation not configurable. */
|
|
10238
|
+
rotateOptions: array(ImageRotateSchema),
|
|
10239
|
+
/** Supported white-balance modes. Empty → white-balance not configurable. */
|
|
10240
|
+
whiteBalanceModes: array(WhiteBalanceModeSchema),
|
|
10241
|
+
supportsWarmth: boolean(),
|
|
10242
|
+
/** Present when `supportsWarmth` — the normalized 0–100 range. */
|
|
10243
|
+
warmth: NormalizedRangeSchema.optional(),
|
|
10244
|
+
/** Supported exposure modes. Empty → exposure not configurable. */
|
|
10245
|
+
exposureModes: array(ExposureModeSchema),
|
|
10246
|
+
/** Supported backlight modes. Empty → backlight-compensation not configurable. */
|
|
10247
|
+
backlightModes: array(BacklightModeSchema)
|
|
10248
|
+
});
|
|
10249
|
+
/**
|
|
10250
|
+
* Partial change to the image config — every field optional. Slider values
|
|
10251
|
+
* are normalized 0–100. A provider ignores fields it does not support.
|
|
10252
|
+
*/
|
|
10253
|
+
var ImageSettingsPatchSchema = object({
|
|
10254
|
+
brightness: number().optional(),
|
|
10255
|
+
contrast: number().optional(),
|
|
10256
|
+
saturation: number().optional(),
|
|
10257
|
+
sharpness: number().optional(),
|
|
10258
|
+
mirror: boolean().optional(),
|
|
10259
|
+
flip: boolean().optional(),
|
|
10260
|
+
rotate: ImageRotateSchema.optional(),
|
|
10261
|
+
whiteBalance: WhiteBalanceModeSchema.optional(),
|
|
10262
|
+
warmth: number().optional(),
|
|
10263
|
+
exposureMode: ExposureModeSchema.optional(),
|
|
10264
|
+
backlightMode: BacklightModeSchema.optional()
|
|
10265
|
+
});
|
|
10266
|
+
DeviceType.Camera, method(object({ deviceId: number() }), ImageSettingsOptionsSchema), method(object({
|
|
10267
|
+
deviceId: number(),
|
|
10268
|
+
settings: ImageSettingsPatchSchema
|
|
10269
|
+
}), _void(), {
|
|
10270
|
+
kind: "mutation",
|
|
10271
|
+
auth: "admin"
|
|
10272
|
+
});
|
|
10273
|
+
/**
|
|
10057
10274
|
* Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
|
|
10058
10275
|
* with a mowing lifecycle plus a dock action.
|
|
10059
10276
|
*
|
|
@@ -10948,6 +11165,16 @@ var RunnerCameraConfigSchema = object({
|
|
|
10948
11165
|
* this gate is bypassed.
|
|
10949
11166
|
*/
|
|
10950
11167
|
onboardMotionDrivesAnalyzer: boolean().default(true),
|
|
11168
|
+
/**
|
|
11169
|
+
* Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
|
|
11170
|
+
* never arms the periodic recheck timer, regardless of `occupancyRecheckSec` —
|
|
11171
|
+
* this is off by default because the recheck re-subscribes a detection session
|
|
11172
|
+
* every N seconds while `watching`, a major source of pull-decoder re-dial
|
|
11173
|
+
* churn (each cycle creates+tears a session → RTSP re-dial → latency). The
|
|
11174
|
+
* `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
|
|
11175
|
+
* (and only render) when this is enabled.
|
|
11176
|
+
*/
|
|
11177
|
+
occupancyRecheckEnabled: boolean().default(false),
|
|
10951
11178
|
occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
|
|
10952
11179
|
occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
|
|
10953
11180
|
/**
|
|
@@ -19368,6 +19595,18 @@ Object.freeze({
|
|
|
19368
19595
|
addonId: null,
|
|
19369
19596
|
access: "view"
|
|
19370
19597
|
},
|
|
19598
|
+
"dayNight.getOptions": {
|
|
19599
|
+
capName: "day-night",
|
|
19600
|
+
capScope: "device",
|
|
19601
|
+
addonId: null,
|
|
19602
|
+
access: "view"
|
|
19603
|
+
},
|
|
19604
|
+
"dayNight.setSettings": {
|
|
19605
|
+
capName: "day-night",
|
|
19606
|
+
capScope: "device",
|
|
19607
|
+
addonId: null,
|
|
19608
|
+
access: "create"
|
|
19609
|
+
},
|
|
19371
19610
|
"decoder.createSession": {
|
|
19372
19611
|
capName: "decoder",
|
|
19373
19612
|
capScope: "system",
|
|
@@ -20298,6 +20537,18 @@ Object.freeze({
|
|
|
20298
20537
|
addonId: null,
|
|
20299
20538
|
access: "create"
|
|
20300
20539
|
},
|
|
20540
|
+
"imageSettings.getOptions": {
|
|
20541
|
+
capName: "image-settings",
|
|
20542
|
+
capScope: "device",
|
|
20543
|
+
addonId: null,
|
|
20544
|
+
access: "view"
|
|
20545
|
+
},
|
|
20546
|
+
"imageSettings.setSettings": {
|
|
20547
|
+
capName: "image-settings",
|
|
20548
|
+
capScope: "device",
|
|
20549
|
+
addonId: null,
|
|
20550
|
+
access: "create"
|
|
20551
|
+
},
|
|
20301
20552
|
"integrations.create": {
|
|
20302
20553
|
capName: "integrations",
|
|
20303
20554
|
capScope: "system",
|
|
@@ -23076,6 +23327,71 @@ function rankDecodeHwAccels(preferred, supportedMethods) {
|
|
|
23076
23327
|
return candidates.sort((a, b) => decodeHwAccelRankIndex(a) - decodeHwAccelRankIndex(b));
|
|
23077
23328
|
}
|
|
23078
23329
|
//#endregion
|
|
23330
|
+
//#region src/shm-orphan-purge.ts
|
|
23331
|
+
/**
|
|
23332
|
+
* Startup reclamation of orphaned shared-memory segments.
|
|
23333
|
+
*
|
|
23334
|
+
* A decoder writes frames into named `/dev/shm` segments and unlinks each one
|
|
23335
|
+
* on graceful session teardown (`DecoderFrameRingSink.destroy`). When the
|
|
23336
|
+
* decoder process dies *ungracefully* — SIGBUS, OOM-kill, or a SIGKILL during
|
|
23337
|
+
* redeploy — that teardown never runs and the segment is orphaned: it stays in
|
|
23338
|
+
* the tmpfs forever, since nothing else knows its name. Across many
|
|
23339
|
+
* crashes/redeploys these accumulate until `/dev/shm` fills, at which point the
|
|
23340
|
+
* next `mmap` write faults with an uncatchable SIGBUS and the decoder
|
|
23341
|
+
* crash-loops into its circuit breaker (which is exactly the incident this
|
|
23342
|
+
* guards against).
|
|
23343
|
+
*
|
|
23344
|
+
* The reclamation is safe *at process startup*: a freshly-booting decoder owns
|
|
23345
|
+
* no live sessions, so every pre-existing segment with its prefix is by
|
|
23346
|
+
* definition an orphan from a dead instance. `shm_unlink` only removes the
|
|
23347
|
+
* name — any consumer still holding a mapping keeps reading valid memory until
|
|
23348
|
+
* it closes (POSIX deferred reclaim + the ring seqlock), so unlinking is safe
|
|
23349
|
+
* even if a stale reader is momentarily still attached.
|
|
23350
|
+
*
|
|
23351
|
+
* POSIX-only: segments surface as files under `/dev/shm` on Linux. On platforms
|
|
23352
|
+
* without that directory (Windows, macOS) the scan finds nothing — no-op.
|
|
23353
|
+
*
|
|
23354
|
+
* NOTE: this lives inside the decoder addon (not `@camstack/shm-ring`) so it
|
|
23355
|
+
* ships in the self-contained addon bundle via `camstack deploy`, reusing the
|
|
23356
|
+
* already-deployed `unlinkSegment`; no host base-image rebuild required.
|
|
23357
|
+
*/
|
|
23358
|
+
/** Default tmpfs directory where POSIX shared-memory segments appear on Linux. */
|
|
23359
|
+
var DEFAULT_SHM_DIR = "/dev/shm";
|
|
23360
|
+
/**
|
|
23361
|
+
* Unlink every shared-memory segment whose name starts with `prefix`.
|
|
23362
|
+
*
|
|
23363
|
+
* Intended to run ONCE at decoder startup, before any session is created, to
|
|
23364
|
+
* reclaim segments orphaned by a previously-crashed instance. A per-file unlink
|
|
23365
|
+
* failure is swallowed so one stuck segment cannot block reclaiming the rest.
|
|
23366
|
+
*/
|
|
23367
|
+
function purgeOrphanSegments(prefix, options = {}) {
|
|
23368
|
+
const dir = options.dir ?? DEFAULT_SHM_DIR;
|
|
23369
|
+
const unlink = options.unlink ?? unlinkSegment;
|
|
23370
|
+
let entries;
|
|
23371
|
+
try {
|
|
23372
|
+
entries = readdirSync(dir);
|
|
23373
|
+
} catch {
|
|
23374
|
+
return {
|
|
23375
|
+
scanned: 0,
|
|
23376
|
+
removed: 0,
|
|
23377
|
+
names: []
|
|
23378
|
+
};
|
|
23379
|
+
}
|
|
23380
|
+
const names = [];
|
|
23381
|
+
for (const name of entries) {
|
|
23382
|
+
if (!name.startsWith(prefix)) continue;
|
|
23383
|
+
try {
|
|
23384
|
+
unlink(name);
|
|
23385
|
+
names.push(name);
|
|
23386
|
+
} catch {}
|
|
23387
|
+
}
|
|
23388
|
+
return {
|
|
23389
|
+
scanned: entries.length,
|
|
23390
|
+
removed: names.length,
|
|
23391
|
+
names
|
|
23392
|
+
};
|
|
23393
|
+
}
|
|
23394
|
+
//#endregion
|
|
23079
23395
|
//#region src/shared/notifying-ring-buffer.ts
|
|
23080
23396
|
/**
|
|
23081
23397
|
* A {@link RingBuffer} that can notify a single blocked consumer the moment an
|
|
@@ -23261,6 +23577,36 @@ async function resolveDecoderBackend(api, nodeId, logger) {
|
|
|
23261
23577
|
return DEFAULT_DECODER_BACKEND;
|
|
23262
23578
|
}
|
|
23263
23579
|
//#endregion
|
|
23580
|
+
//#region src/pull-demuxer-options.ts
|
|
23581
|
+
/**
|
|
23582
|
+
* libav demuxer options for the node-av PULL decode path — RTP sources
|
|
23583
|
+
* (`isRtpSource()`) dialing the broker's RTSP restream (`rtsp://…/muted`).
|
|
23584
|
+
*
|
|
23585
|
+
* Without low-latency flags libav buffers up to `analyzeduration` (default
|
|
23586
|
+
* **5s**) of the live stream during `avformat_find_stream_info`, and that buffer
|
|
23587
|
+
* becomes a fixed ~5s offset on the decoded-frame timeline — detection overlays
|
|
23588
|
+
* then lag the WebRTC raw-RTP passthrough (which never decodes) by ~5s. Adding
|
|
23589
|
+
* `fflags: nobuffer` + a short `analyzeduration` collapses that offset to
|
|
23590
|
+
* sub-second, mirroring the ffmpeg backend's `-fflags +nobuffer` on its pull
|
|
23591
|
+
* path (decoder-ffmpeg/ffmpeg-args.ts).
|
|
23592
|
+
*
|
|
23593
|
+
* ‼ `analyzeduration`/`probesize` stay SMALL BUT NON-ZERO. Probe-zeroing
|
|
23594
|
+
* (`analyzeduration 0`) on the pull RTSP path STARVES the Reolink rfc4571
|
|
23595
|
+
* restream demuxer (garbage / zero frames — looked like a ~3000fps churn
|
|
23596
|
+
* runaway); that optimisation is push-mode-only. Keep them > 0.
|
|
23597
|
+
*/
|
|
23598
|
+
function buildPullDemuxerOptions() {
|
|
23599
|
+
return {
|
|
23600
|
+
rtsp_transport: "tcp",
|
|
23601
|
+
fflags: "nobuffer",
|
|
23602
|
+
analyzeduration: "1000000",
|
|
23603
|
+
probesize: "1000000",
|
|
23604
|
+
max_delay: "0",
|
|
23605
|
+
reorder_queue_size: "0",
|
|
23606
|
+
user_agent: "decoder"
|
|
23607
|
+
};
|
|
23608
|
+
}
|
|
23609
|
+
//#endregion
|
|
23264
23610
|
//#region src/scaler-geometry.ts
|
|
23265
23611
|
/**
|
|
23266
23612
|
* Decide the scaler action for an incoming frame's source geometry.
|
|
@@ -23283,6 +23629,49 @@ function resolveScalerAction(current, incoming) {
|
|
|
23283
23629
|
* not hot-loop `Demuxer.open`.
|
|
23284
23630
|
*/
|
|
23285
23631
|
var PULL_REDIAL_MS = 3e3;
|
|
23632
|
+
/**
|
|
23633
|
+
* Short re-dial backoff for the adaptive "drop-to-live" path. When the decoder
|
|
23634
|
+
* has fallen hopelessly behind the live edge ({@link DROP_TO_LIVE_LAG_MS}) the
|
|
23635
|
+
* loop tears the input down and re-dials fast so the restream's burst-prime
|
|
23636
|
+
* re-seats it at the live edge, instead of waiting the full {@link PULL_REDIAL_MS}
|
|
23637
|
+
* (which is for transient stream blips, not a deliberate re-seat).
|
|
23638
|
+
*/
|
|
23639
|
+
var PULL_FAST_REDIAL_MS = 250;
|
|
23640
|
+
/** Adaptive controller cadence — recompute emit-fps from lag at most this often. */
|
|
23641
|
+
var ADAPTIVE_TICK_MS = 1e3;
|
|
23642
|
+
/** Lag EWMA above this (ms) → halve the adaptive emit-fps. */
|
|
23643
|
+
var ADAPTIVE_LAG_HIGH_MS = 750;
|
|
23644
|
+
/** Lag EWMA below this (ms), sustained {@link ADAPTIVE_RECOVER_SUSTAIN_MS} → grow emit-fps. */
|
|
23645
|
+
var ADAPTIVE_LAG_LOW_MS = 250;
|
|
23646
|
+
/** How long lag must stay below {@link ADAPTIVE_LAG_LOW_MS} before growing emit-fps. */
|
|
23647
|
+
var ADAPTIVE_RECOVER_SUSTAIN_MS = 5e3;
|
|
23648
|
+
/** Floor the adaptive controller never sheds below (fps). */
|
|
23649
|
+
var ADAPTIVE_MIN_FPS = 2;
|
|
23650
|
+
/**
|
|
23651
|
+
* Ceiling the adaptive controller recovers toward when the subscriber requested
|
|
23652
|
+
* an UNLIMITED rate (`maxFps <= 0`). Source cameras run ≤ this, so the clamp is
|
|
23653
|
+
* a no-op in practice while keeping the /2 shed + *1.5 recover math bounded.
|
|
23654
|
+
*/
|
|
23655
|
+
var ADAPTIVE_FALLBACK_CEILING_FPS = 30;
|
|
23656
|
+
/**
|
|
23657
|
+
* Lag EWMA above this (ms) = hopelessly behind → drop-to-live re-seat.
|
|
23658
|
+
*
|
|
23659
|
+
* This bounds the MAX glass-to-box media staleness: `lagMsEwma` is how far the
|
|
23660
|
+
* decoder's media clock trails live, i.e. how old the *content* of an emitted
|
|
23661
|
+
* frame is (distinct from `frameAge`, which only measures decode→pick pipeline
|
|
23662
|
+
* delay and does NOT capture this media lag). On a host that decodes marginally
|
|
23663
|
+
* below real-time the lag sawtooths up to this ceiling, then re-seats to live —
|
|
23664
|
+
* so this constant directly caps how stale the boxes can get. 1200 ms (was
|
|
23665
|
+
* 2000) trades slightly more frequent 250 ms re-seats for fresher overlays; the
|
|
23666
|
+
* re-seat cost is negligible (~1.4 drops/cam/min at the observed drift). The
|
|
23667
|
+
* real fix — keeping decode at real-time so the lag never builds — is the
|
|
23668
|
+
* co-located / subprocess-decode epic.
|
|
23669
|
+
*/
|
|
23670
|
+
var DROP_TO_LIVE_LAG_MS = 1200;
|
|
23671
|
+
/** Never trigger drop-to-live more than once per this window (ms). */
|
|
23672
|
+
var DROP_TO_LIVE_MIN_INTERVAL_MS = 3e3;
|
|
23673
|
+
/** Decoder DEBUG facility flush cadence (~1Hz). */
|
|
23674
|
+
var DEBUG_FLUSH_MS = 1e3;
|
|
23286
23675
|
/** Map our canonical backend name to the node-av `AV_HWDEVICE_TYPE_*` constant. */
|
|
23287
23676
|
function backendToHwDeviceConst(backend, consts) {
|
|
23288
23677
|
switch (backend) {
|
|
@@ -23444,6 +23833,34 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
|
|
|
23444
23833
|
scalerSrcFmt = -1;
|
|
23445
23834
|
lastEmitTime = 0;
|
|
23446
23835
|
minIntervalMs;
|
|
23836
|
+
/** Gates {@link emitDebugFlash}; set from `config.debug === true`. Off = zero cost. */
|
|
23837
|
+
debugEnabled;
|
|
23838
|
+
/** Wall-clock of the last `decoder debug` flush — throttles the facility to ~1Hz. */
|
|
23839
|
+
lastDebugFlush = 0;
|
|
23840
|
+
/** `droppedFrames` snapshot at the last debug flush — window base for the drop delta. */
|
|
23841
|
+
debugWindowDropped = 0;
|
|
23842
|
+
/** Subscriber-requested emit ceiling (fps). `maxFps<=0` clamps to the fallback ceiling. */
|
|
23843
|
+
ceilingFps;
|
|
23844
|
+
/** Current lag-throttled emit rate (fps) — starts at the ceiling, floored at ADAPTIVE_MIN_FPS. */
|
|
23845
|
+
adaptiveFps;
|
|
23846
|
+
/** Frames actually emitted/sec over the last adaptive-tick window (feeds getStats + debug). */
|
|
23847
|
+
effectiveFps = 0;
|
|
23848
|
+
/** `outputFrames` snapshot at the last adaptive tick — window base for effective-fps. */
|
|
23849
|
+
adaptiveWindowFrames = 0;
|
|
23850
|
+
/** First sampled frame's pts (timebase units); `null` until the current dial seeds it. */
|
|
23851
|
+
basePts = null;
|
|
23852
|
+
/** Wall-clock at `basePts` — the real-time anchor for the drift computation. */
|
|
23853
|
+
baseWall = 0;
|
|
23854
|
+
/** EWMA of real-time drift (ms). Rising = decoder falling behind the live edge. */
|
|
23855
|
+
lagMsEwma = 0;
|
|
23856
|
+
/** Wall-clock the lag EWMA first dropped below ADAPTIVE_LAG_LOW_MS (0 = not low). */
|
|
23857
|
+
lowLagSince = 0;
|
|
23858
|
+
/** Wall-clock of the last adaptive-controller tick. */
|
|
23859
|
+
lastAdaptiveCheck = 0;
|
|
23860
|
+
/** Wall-clock of the last drop-to-live re-seat — rate-limits the re-dial. */
|
|
23861
|
+
lastDropToLive = 0;
|
|
23862
|
+
/** Set by drop-to-live so `runPullLoop` uses the SHORT re-dial backoff for one iteration. */
|
|
23863
|
+
pullFastRedial = false;
|
|
23447
23864
|
inputPackets = 0;
|
|
23448
23865
|
outputFrames = 0;
|
|
23449
23866
|
droppedFrames = 0;
|
|
@@ -23478,6 +23895,9 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
|
|
|
23478
23895
|
if (typeof config.tag === "string" && config.tag.length > 0) sessionTags["tag"] = config.tag;
|
|
23479
23896
|
this.logger = Object.keys(sessionTags).length > 0 ? logger.withTags(sessionTags) : logger;
|
|
23480
23897
|
this.minIntervalMs = config.maxFps > 0 ? 1e3 / config.maxFps : 0;
|
|
23898
|
+
this.debugEnabled = config.debug === true;
|
|
23899
|
+
this.ceilingFps = config.maxFps > 0 ? config.maxFps : ADAPTIVE_FALLBACK_CEILING_FPS;
|
|
23900
|
+
this.adaptiveFps = this.ceilingFps;
|
|
23481
23901
|
this.outputMode = NodeAvDecoderSession.resolveOutputMode(config.outputFormat);
|
|
23482
23902
|
this.hwaccelPref = options?.hwaccel ?? "auto";
|
|
23483
23903
|
this.hwaccelResolver = options?.hwaccelResolver ?? null;
|
|
@@ -23792,7 +24212,9 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
|
|
|
23792
24212
|
this.closePullInput();
|
|
23793
24213
|
}
|
|
23794
24214
|
if (this.destroyed || !this.pullActive) break;
|
|
23795
|
-
|
|
24215
|
+
const backoffMs = this.pullFastRedial ? PULL_FAST_REDIAL_MS : PULL_REDIAL_MS;
|
|
24216
|
+
this.pullFastRedial = false;
|
|
24217
|
+
await this.pullSleep(backoffMs);
|
|
23796
24218
|
}
|
|
23797
24219
|
}
|
|
23798
24220
|
/**
|
|
@@ -23803,7 +24225,7 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
|
|
|
23803
24225
|
* stream ends or the session is torn down.
|
|
23804
24226
|
*/
|
|
23805
24227
|
async pullDialAndDecode(nav, C, url) {
|
|
23806
|
-
const demuxer = await nav.Demuxer.open(url, { options:
|
|
24228
|
+
const demuxer = await nav.Demuxer.open(url, { options: buildPullDemuxerOptions() });
|
|
23807
24229
|
if (this.destroyed || !this.pullActive) {
|
|
23808
24230
|
demuxer[Symbol.dispose]?.();
|
|
23809
24231
|
return;
|
|
@@ -23813,7 +24235,8 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
|
|
|
23813
24235
|
if (!videoStream) throw new Error("node-av decoder: pull input has no video stream");
|
|
23814
24236
|
const decoder = await nav.Decoder.create(videoStream, {
|
|
23815
24237
|
...this.pullHwContext ? { hardware: this.pullHwContext } : {},
|
|
23816
|
-
rescale: { pixelFormat: C.AV_PIX_FMT_YUV420P }
|
|
24238
|
+
rescale: { pixelFormat: C.AV_PIX_FMT_YUV420P },
|
|
24239
|
+
exitOnError: false
|
|
23817
24240
|
});
|
|
23818
24241
|
if (this.destroyed || !this.pullActive) {
|
|
23819
24242
|
decoder[Symbol.dispose]?.();
|
|
@@ -23826,11 +24249,45 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
|
|
|
23826
24249
|
hwAccel: this.activeHwAccel,
|
|
23827
24250
|
streamIndex: videoStream.index
|
|
23828
24251
|
} });
|
|
23829
|
-
|
|
23830
|
-
|
|
23831
|
-
|
|
23832
|
-
|
|
24252
|
+
const probeTb = videoStream.timeBase;
|
|
24253
|
+
const tbNum = probeTb?.num ?? 0;
|
|
24254
|
+
const tbDen = probeTb?.den ?? 0;
|
|
24255
|
+
this.resetLagTracker(Date.now());
|
|
24256
|
+
await this.consumePullFrames(decoder.frames(demuxer.packets(videoStream.index)), tbNum, tbDen);
|
|
24257
|
+
}
|
|
24258
|
+
/**
|
|
24259
|
+
* Consume decoded pull frames until the stream ends or the session is torn
|
|
24260
|
+
* down. Each yielded frame is a node-av `Frame` — for the HW path a `clone`
|
|
24261
|
+
* that refs a VAAPI surface + the decoder's whole `hw_frames_ctx`, so a SINGLE
|
|
24262
|
+
* unfreed frame pins the dial's entire GPU surface pool (i915 GEM/shmem) until
|
|
24263
|
+
* the process dies. Therefore EVERY exit path (teardown break, drop-to-live
|
|
24264
|
+
* re-seat, normal emit, or a throw from the lag/emit calls) MUST `frame.free()`
|
|
24265
|
+
* — the `try/finally` below is the single release point that guarantees it.
|
|
24266
|
+
*
|
|
24267
|
+
* Extracted from {@link pullDialAndDecode} so this free-on-all-paths contract
|
|
24268
|
+
* is unit-testable with a stub async iterable (no live libav / GPU needed).
|
|
24269
|
+
*/
|
|
24270
|
+
async consumePullFrames(frames, tbNum, tbDen) {
|
|
24271
|
+
for await (const frame of frames) {
|
|
24272
|
+
if (!frame) {
|
|
24273
|
+
if (this.destroyed || !this.pullActive) break;
|
|
24274
|
+
continue;
|
|
24275
|
+
}
|
|
23833
24276
|
try {
|
|
24277
|
+
if (this.destroyed || !this.pullActive) break;
|
|
24278
|
+
this.inputPackets++;
|
|
24279
|
+
const wallNow = Date.now();
|
|
24280
|
+
this.updateLagTracker(frame.pts, tbNum, tbDen, wallNow);
|
|
24281
|
+
this.runAdaptiveController(wallNow);
|
|
24282
|
+
this.emitDebugFlash(wallNow);
|
|
24283
|
+
if (this.shouldDropToLive(wallNow)) {
|
|
24284
|
+
this.logger.warn("node-av decoder: lag beyond recovery — dropping to live edge", { meta: {
|
|
24285
|
+
lagMs: Math.round(this.lagMsEwma),
|
|
24286
|
+
redialInMs: PULL_FAST_REDIAL_MS
|
|
24287
|
+
} });
|
|
24288
|
+
this.pullFastRedial = true;
|
|
24289
|
+
break;
|
|
24290
|
+
}
|
|
23834
24291
|
this.emitDecodedFrame(frame);
|
|
23835
24292
|
} finally {
|
|
23836
24293
|
frame.free();
|
|
@@ -23838,10 +24295,11 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
|
|
|
23838
24295
|
}
|
|
23839
24296
|
}
|
|
23840
24297
|
/**
|
|
23841
|
-
* Resolve and build the HW context for pull mode ONCE
|
|
23842
|
-
* single try; `'auto'` → the
|
|
23843
|
-
*
|
|
23844
|
-
* failures fall through to
|
|
24298
|
+
* Resolve and build the HW context for pull mode ONCE (reused across every
|
|
24299
|
+
* re-dial, freed in `destroy`). Explicit backend → single try; `'auto'` → the
|
|
24300
|
+
* kernel resolver's ordered list; no resolver → software. The first
|
|
24301
|
+
* `HardwareContext.create` that succeeds wins; all failures fall through to
|
|
24302
|
+
* software decode (`hardware: null`).
|
|
23845
24303
|
*/
|
|
23846
24304
|
async ensurePullHwContext(nav, C) {
|
|
23847
24305
|
if (this.hwaccelPref === "none") {
|
|
@@ -23861,7 +24319,7 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
|
|
|
23861
24319
|
if (!deviceType) continue;
|
|
23862
24320
|
const hw = nav.HardwareContext.create(deviceType);
|
|
23863
24321
|
if (!hw) {
|
|
23864
|
-
this.logger.
|
|
24322
|
+
this.logger.debug("node-av: pull hwaccel context create failed — trying next", { meta: { backend } });
|
|
23865
24323
|
continue;
|
|
23866
24324
|
}
|
|
23867
24325
|
this.pullHwContext = hw;
|
|
@@ -23896,6 +24354,107 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
|
|
|
23896
24354
|
}, ms);
|
|
23897
24355
|
});
|
|
23898
24356
|
}
|
|
24357
|
+
/**
|
|
24358
|
+
* Re-anchor the real-time lag tracker + per-dial controllers at the start of
|
|
24359
|
+
* each dial. A fresh dial (including a drop-to-live re-seat) begins at the
|
|
24360
|
+
* live edge, so the drift EWMA resets to 0 — otherwise the just-torn dial's
|
|
24361
|
+
* accumulated lag would re-trigger drop-to-live immediately. The learned
|
|
24362
|
+
* `adaptiveFps` is intentionally NOT reset: a re-seat should keep the shed
|
|
24363
|
+
* emit-rate and recover from there once lag stays low.
|
|
24364
|
+
*/
|
|
24365
|
+
resetLagTracker(wallNow) {
|
|
24366
|
+
this.basePts = null;
|
|
24367
|
+
this.baseWall = 0;
|
|
24368
|
+
this.lagMsEwma = 0;
|
|
24369
|
+
this.lowLagSince = 0;
|
|
24370
|
+
this.lastAdaptiveCheck = wallNow;
|
|
24371
|
+
this.adaptiveWindowFrames = this.outputFrames;
|
|
24372
|
+
this.lastDebugFlush = wallNow;
|
|
24373
|
+
this.debugWindowDropped = this.droppedFrames;
|
|
24374
|
+
}
|
|
24375
|
+
/**
|
|
24376
|
+
* Update the EWMA of the decoder's real-time drift from a decoded frame's
|
|
24377
|
+
* pts. The first frame of a dial anchors `basePts`/`baseWall`; every later
|
|
24378
|
+
* frame compares elapsed wall-clock against elapsed media time —
|
|
24379
|
+
* `lagMs = wallElapsed - mediaElapsed`. A rising EWMA means the decoder is
|
|
24380
|
+
* falling behind the live edge (wall time outrunning the media timeline).
|
|
24381
|
+
*/
|
|
24382
|
+
updateLagTracker(pts, tbNum, tbDen, wallNow) {
|
|
24383
|
+
if (tbDen === 0) return;
|
|
24384
|
+
if (this.basePts === null) {
|
|
24385
|
+
this.basePts = pts;
|
|
24386
|
+
this.baseWall = wallNow;
|
|
24387
|
+
return;
|
|
24388
|
+
}
|
|
24389
|
+
const sample = wallNow - this.baseWall - Number(pts - this.basePts) * 1e3 * tbNum / tbDen;
|
|
24390
|
+
this.lagMsEwma = .9 * this.lagMsEwma + .1 * sample;
|
|
24391
|
+
}
|
|
24392
|
+
/**
|
|
24393
|
+
* Lag-driven adaptive emit-fps controller (P1), self-throttled to
|
|
24394
|
+
* {@link ADAPTIVE_TICK_MS}. Under load (lag EWMA high) it halves the emit-fps
|
|
24395
|
+
* — fewer frames transferred + GPU-scaled + emitted, so the decoder stays
|
|
24396
|
+
* real-time instead of accumulating lag; once lag stays low for a sustained
|
|
24397
|
+
* window it grows the emit-fps back toward the subscriber's ceiling.
|
|
24398
|
+
* `minIntervalMs` (the throttle both emit paths honour) is recomputed from
|
|
24399
|
+
* `adaptiveFps`, NOT the raw ceiling.
|
|
24400
|
+
*/
|
|
24401
|
+
runAdaptiveController(wallNow) {
|
|
24402
|
+
const windowMs = wallNow - this.lastAdaptiveCheck;
|
|
24403
|
+
if (windowMs < ADAPTIVE_TICK_MS) return;
|
|
24404
|
+
this.lastAdaptiveCheck = wallNow;
|
|
24405
|
+
const framesDelta = this.outputFrames - this.adaptiveWindowFrames;
|
|
24406
|
+
this.adaptiveWindowFrames = this.outputFrames;
|
|
24407
|
+
this.effectiveFps = windowMs > 0 ? framesDelta * 1e3 / windowMs : 0;
|
|
24408
|
+
const lag = this.lagMsEwma;
|
|
24409
|
+
if (lag > ADAPTIVE_LAG_HIGH_MS) {
|
|
24410
|
+
this.adaptiveFps = Math.max(ADAPTIVE_MIN_FPS, this.adaptiveFps / 2);
|
|
24411
|
+
this.lowLagSince = 0;
|
|
24412
|
+
} else if (lag < ADAPTIVE_LAG_LOW_MS) {
|
|
24413
|
+
if (this.lowLagSince === 0) this.lowLagSince = wallNow;
|
|
24414
|
+
else if (wallNow - this.lowLagSince >= ADAPTIVE_RECOVER_SUSTAIN_MS) {
|
|
24415
|
+
this.adaptiveFps = Math.min(this.ceilingFps, this.adaptiveFps * 1.5);
|
|
24416
|
+
this.lowLagSince = wallNow;
|
|
24417
|
+
}
|
|
24418
|
+
} else this.lowLagSince = 0;
|
|
24419
|
+
this.minIntervalMs = this.adaptiveFps > 0 ? 1e3 / this.adaptiveFps : 0;
|
|
24420
|
+
}
|
|
24421
|
+
/**
|
|
24422
|
+
* Whether the decoder is hopelessly behind the live edge and should tear the
|
|
24423
|
+
* input down for a fast re-seat. Rate-limited to at most once per
|
|
24424
|
+
* {@link DROP_TO_LIVE_MIN_INTERVAL_MS} so a bad stretch can't hot-loop the
|
|
24425
|
+
* dial. Records the trigger time as a side effect when it returns `true`.
|
|
24426
|
+
*/
|
|
24427
|
+
shouldDropToLive(wallNow) {
|
|
24428
|
+
if (this.lagMsEwma <= DROP_TO_LIVE_LAG_MS) return false;
|
|
24429
|
+
if (wallNow - this.lastDropToLive < DROP_TO_LIVE_MIN_INTERVAL_MS) return false;
|
|
24430
|
+
this.lastDropToLive = wallNow;
|
|
24431
|
+
return true;
|
|
24432
|
+
}
|
|
24433
|
+
/**
|
|
24434
|
+
* Per-camera DEBUG facility (mirrors the stream-broker's `streamingDebug`
|
|
24435
|
+
* gate). When `config.debug` is set, flush a single structured `decoder
|
|
24436
|
+
* debug` line at ~1Hz — effective/adaptive fps, real-time lag, dropped-frame
|
|
24437
|
+
* delta, avg decode time, hwaccel. `deviceId`/`tag` ride on the logger tags.
|
|
24438
|
+
* Cheap: all values come from running counters, and the whole method is a
|
|
24439
|
+
* no-op (one comparison) when debug is disabled or the window hasn't elapsed.
|
|
24440
|
+
*/
|
|
24441
|
+
emitDebugFlash(wallNow) {
|
|
24442
|
+
if (!this.debugEnabled) return;
|
|
24443
|
+
if (wallNow - this.lastDebugFlush < DEBUG_FLUSH_MS) return;
|
|
24444
|
+
this.lastDebugFlush = wallNow;
|
|
24445
|
+
const droppedDelta = this.droppedFrames - this.debugWindowDropped;
|
|
24446
|
+
this.debugWindowDropped = this.droppedFrames;
|
|
24447
|
+
const decodeMsAvg = this.outputFrames > 0 ? this.totalDecodeTimeMs / this.outputFrames : 0;
|
|
24448
|
+
this.logger.info("decoder debug", { meta: {
|
|
24449
|
+
effectiveFps: Number(this.effectiveFps.toFixed(1)),
|
|
24450
|
+
adaptiveFps: Number(this.adaptiveFps.toFixed(1)),
|
|
24451
|
+
ceilingFps: this.ceilingFps,
|
|
24452
|
+
lagMs: Math.round(this.lagMsEwma),
|
|
24453
|
+
droppedFrames: droppedDelta,
|
|
24454
|
+
decodeMs: Number(decodeMsAvg.toFixed(2)),
|
|
24455
|
+
hwAccel: this.activeHwAccel
|
|
24456
|
+
} });
|
|
24457
|
+
}
|
|
23899
24458
|
pushPacket(packet) {
|
|
23900
24459
|
if (this.destroyed) return;
|
|
23901
24460
|
if (this.pullActive) {
|
|
@@ -23952,6 +24511,10 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
|
|
|
23952
24511
|
}
|
|
23953
24512
|
emitDecodedFrame(frame) {
|
|
23954
24513
|
const now = performance.now();
|
|
24514
|
+
if (frame.isHwFrame()) {
|
|
24515
|
+
this.droppedFrames++;
|
|
24516
|
+
return;
|
|
24517
|
+
}
|
|
23955
24518
|
if (this.minIntervalMs > 0 && now - this.lastEmitTime < this.minIntervalMs) {
|
|
23956
24519
|
this.droppedFrames++;
|
|
23957
24520
|
return;
|
|
@@ -24321,12 +24884,19 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
|
|
|
24321
24884
|
}
|
|
24322
24885
|
getStats() {
|
|
24323
24886
|
const uptimeSec = Math.max((Date.now() - this.startTime) / 1e3, 1);
|
|
24324
|
-
|
|
24887
|
+
const base = {
|
|
24325
24888
|
inputFps: this.inputPackets / uptimeSec,
|
|
24326
24889
|
outputFps: this.outputFrames / uptimeSec,
|
|
24327
24890
|
avgDecodeTimeMs: this.outputFrames > 0 ? this.totalDecodeTimeMs / this.outputFrames : 0,
|
|
24328
24891
|
droppedFrames: this.droppedFrames
|
|
24329
24892
|
};
|
|
24893
|
+
if (!this.pullActive) return base;
|
|
24894
|
+
return {
|
|
24895
|
+
...base,
|
|
24896
|
+
lagMs: this.lagMsEwma,
|
|
24897
|
+
effectiveFps: this.effectiveFps,
|
|
24898
|
+
adaptiveFps: this.adaptiveFps
|
|
24899
|
+
};
|
|
24330
24900
|
}
|
|
24331
24901
|
get isPullMode() {
|
|
24332
24902
|
return this.pullActive;
|
|
@@ -24836,6 +25406,7 @@ var NodeAvAudioEncodeSession = class {
|
|
|
24836
25406
|
//#region src/audio-codec/provider.ts
|
|
24837
25407
|
var DEFAULT_IDLE_MS = 3e4;
|
|
24838
25408
|
var MAX_PCM_QUEUE_CHUNKS = 500;
|
|
25409
|
+
var MAX_ENCODED_QUEUE_CHUNKS = 500;
|
|
24839
25410
|
var REAPER_INTERVAL_MS = 5e3;
|
|
24840
25411
|
/** Grace wait for the encoder to drain its tail after `flushEncode`. */
|
|
24841
25412
|
var FLUSH_DRAIN_MS = 60;
|
|
@@ -25042,6 +25613,7 @@ var NodeAvAudioCodecProvider = class {
|
|
|
25042
25613
|
...s.config.bitrateKbps !== void 0 ? { bitrateKbps: s.config.bitrateKbps } : {}
|
|
25043
25614
|
}, this.deps.logger, (chunk) => {
|
|
25044
25615
|
s.encodedQueue.push(chunk);
|
|
25616
|
+
if (s.encodedQueue.length > MAX_ENCODED_QUEUE_CHUNKS) s.encodedQueue.splice(0, s.encodedQueue.length - MAX_ENCODED_QUEUE_CHUNKS);
|
|
25045
25617
|
});
|
|
25046
25618
|
}
|
|
25047
25619
|
pcmFormat(format) {
|
|
@@ -25205,6 +25777,11 @@ var DecoderNodeAvAddon = class extends BaseAddon {
|
|
|
25205
25777
|
return registrations;
|
|
25206
25778
|
}
|
|
25207
25779
|
this.ctx.logger.info("node-av decoder addon initialized", { meta: { selectedBackend: backend } });
|
|
25780
|
+
const purged = purgeOrphanSegments(SEGMENT_NAME_PREFIX);
|
|
25781
|
+
if (purged.removed > 0) this.ctx.logger.warn("node-av decoder: reclaimed orphaned shm segments at startup", { meta: {
|
|
25782
|
+
removed: purged.removed,
|
|
25783
|
+
scanned: purged.scanned
|
|
25784
|
+
} });
|
|
25208
25785
|
this.frameReaders = new FrameRingReaderCache(this.ctx.logger);
|
|
25209
25786
|
if (!this.config.probedBestHwaccel) this.reprobeHwaccel().catch((err) => {
|
|
25210
25787
|
this.ctx.logger.warn("nodeav: auto-reprobe hwaccel failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|