@camstack/addon-decoder-ffmpeg 1.1.5 → 1.1.6
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 +325 -4
- package/dist/index.mjs +326 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ Object.defineProperties(exports, {
|
|
|
5
5
|
let node_crypto = require("node:crypto");
|
|
6
6
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
7
7
|
let node_child_process = require("node:child_process");
|
|
8
|
+
let node_fs = require("node:fs");
|
|
8
9
|
//#region ../../node_modules/zod/v4/core/core.js
|
|
9
10
|
var _a$1;
|
|
10
11
|
function $constructor(name, initializer, params) {
|
|
@@ -4634,7 +4635,7 @@ function _instanceof(cls, params = {}) {
|
|
|
4634
4635
|
return inst;
|
|
4635
4636
|
}
|
|
4636
4637
|
//#endregion
|
|
4637
|
-
//#region ../types/dist/sleep-
|
|
4638
|
+
//#region ../types/dist/sleep-CZDdRBua.mjs
|
|
4638
4639
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4639
4640
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4640
4641
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -7241,7 +7242,16 @@ var DecoderStatsSchema = object({
|
|
|
7241
7242
|
inputFps: number(),
|
|
7242
7243
|
outputFps: number(),
|
|
7243
7244
|
avgDecodeTimeMs: number(),
|
|
7244
|
-
droppedFrames: number()
|
|
7245
|
+
droppedFrames: number(),
|
|
7246
|
+
/**
|
|
7247
|
+
* Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
|
|
7248
|
+
* lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
|
|
7249
|
+
* the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
|
|
7250
|
+
* is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
|
|
7251
|
+
*/
|
|
7252
|
+
lagMs: number().optional(),
|
|
7253
|
+
effectiveFps: number().optional(),
|
|
7254
|
+
adaptiveFps: number().optional()
|
|
7245
7255
|
});
|
|
7246
7256
|
var DecoderSessionConfigSchema = object({
|
|
7247
7257
|
codec: string(),
|
|
@@ -7282,7 +7292,15 @@ var DecoderSessionConfigSchema = object({
|
|
|
7282
7292
|
* other — `pullFrames` returns nothing for an `'shm'` session and
|
|
7283
7293
|
* `pullHandles` returns nothing for a `'callback'` session.
|
|
7284
7294
|
*/
|
|
7285
|
-
frameSink: _enum(["callback", "shm"]).default("callback")
|
|
7295
|
+
frameSink: _enum(["callback", "shm"]).default("callback"),
|
|
7296
|
+
/**
|
|
7297
|
+
* Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
|
|
7298
|
+
* a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
|
|
7299
|
+
* real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
|
|
7300
|
+
* stream-broker's `streamingDebug` gate — off by default so production logs
|
|
7301
|
+
* stay quiet and the emit path pays zero per-frame cost when disabled.
|
|
7302
|
+
*/
|
|
7303
|
+
debug: boolean().optional()
|
|
7286
7304
|
});
|
|
7287
7305
|
var EncodeProfileSchema = object({
|
|
7288
7306
|
video: object({
|
|
@@ -9440,6 +9458,75 @@ DeviceType.Cover, method(object({ deviceId: number().int().nonnegative() }), _vo
|
|
|
9440
9458
|
auth: "admin"
|
|
9441
9459
|
});
|
|
9442
9460
|
/**
|
|
9461
|
+
* Vendor-neutral day/night (IR-cut) control — the per-camera config cap
|
|
9462
|
+
* shared by reolink / hikvision / amcrest. Models the common firmware
|
|
9463
|
+
* surface: the IR-cut switching MODE plus the two knobs that gate it
|
|
9464
|
+
* (photocell `sensitivity` + `switchDelaySec`). Each vendor maps these
|
|
9465
|
+
* onto its own ISAPI / Baichuan / Dahua-CGI fields; the cap standardises
|
|
9466
|
+
* the shape so ONE derived-form renders every camera.
|
|
9467
|
+
*
|
|
9468
|
+
* Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
|
|
9469
|
+
* `getOptions` advertises per-camera availability, `getStatus` (auto-
|
|
9470
|
+
* injected from `status`) reports the live values, and a single
|
|
9471
|
+
* `setSettings` mutation applies a partial change. No hand-written
|
|
9472
|
+
* settings-contribution methods — the framework derives the UI + save
|
|
9473
|
+
* routing from this surface.
|
|
9474
|
+
*/
|
|
9475
|
+
/** IR-cut switching mode. `schedule` = time-of-day table configured on the camera. */
|
|
9476
|
+
var DayNightModeSchema = _enum([
|
|
9477
|
+
"auto",
|
|
9478
|
+
"day",
|
|
9479
|
+
"night",
|
|
9480
|
+
"schedule"
|
|
9481
|
+
]);
|
|
9482
|
+
/** Normalized numeric range descriptor — `{ min, max, step }` per the
|
|
9483
|
+
* getOptions availability convention. Normalized values are 0–100. */
|
|
9484
|
+
var NormalizedRangeSchema$1 = object({
|
|
9485
|
+
min: number(),
|
|
9486
|
+
max: number(),
|
|
9487
|
+
step: number()
|
|
9488
|
+
});
|
|
9489
|
+
object({
|
|
9490
|
+
mode: DayNightModeSchema,
|
|
9491
|
+
/** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
|
|
9492
|
+
sensitivity: number().optional(),
|
|
9493
|
+
/** Delay before the IR-cut filter flips, in seconds. */
|
|
9494
|
+
switchDelaySec: number().optional(),
|
|
9495
|
+
lastFetchedAt: number()
|
|
9496
|
+
});
|
|
9497
|
+
/**
|
|
9498
|
+
* Per-camera availability descriptor — drives which controls the admin UI
|
|
9499
|
+
* renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
|
|
9500
|
+
* (normalized 0–100); the mode choice-set as an array. A provider returns
|
|
9501
|
+
* honest, camera-probed values — never hardcoded.
|
|
9502
|
+
*/
|
|
9503
|
+
var DayNightOptionsSchema = object({
|
|
9504
|
+
/** Modes this camera accepts. Empty → the camera has no configurable day/night mode. */
|
|
9505
|
+
modes: array(DayNightModeSchema),
|
|
9506
|
+
supportsSensitivity: boolean(),
|
|
9507
|
+
/** Present when `supportsSensitivity` — the normalized 0–100 range. */
|
|
9508
|
+
sensitivity: NormalizedRangeSchema$1.optional(),
|
|
9509
|
+
supportsSwitchDelay: boolean(),
|
|
9510
|
+
/** Present when `supportsSwitchDelay` — the allowed delay range in seconds. */
|
|
9511
|
+
switchDelaySec: NormalizedRangeSchema$1.optional()
|
|
9512
|
+
});
|
|
9513
|
+
/**
|
|
9514
|
+
* Partial change to the day/night config — every field optional. A
|
|
9515
|
+
* provider ignores fields it does not support.
|
|
9516
|
+
*/
|
|
9517
|
+
var DayNightSettingsPatchSchema = object({
|
|
9518
|
+
mode: DayNightModeSchema.optional(),
|
|
9519
|
+
sensitivity: number().optional(),
|
|
9520
|
+
switchDelaySec: number().optional()
|
|
9521
|
+
});
|
|
9522
|
+
DeviceType.Camera, method(object({ deviceId: number() }), DayNightOptionsSchema), method(object({
|
|
9523
|
+
deviceId: number(),
|
|
9524
|
+
settings: DayNightSettingsPatchSchema
|
|
9525
|
+
}), _void(), {
|
|
9526
|
+
kind: "mutation",
|
|
9527
|
+
auth: "admin"
|
|
9528
|
+
});
|
|
9529
|
+
/**
|
|
9443
9530
|
* Identity envelope for a device's upstream-system metadata.
|
|
9444
9531
|
*
|
|
9445
9532
|
* Two jobs:
|
|
@@ -9795,6 +9882,130 @@ object({
|
|
|
9795
9882
|
});
|
|
9796
9883
|
DeviceType.Image;
|
|
9797
9884
|
/**
|
|
9885
|
+
* Vendor-neutral image / picture-adjustment cap — the per-camera config
|
|
9886
|
+
* cap shared by reolink / hikvision / amcrest. Models the common ISP
|
|
9887
|
+
* surface: the four picture sliders (brightness / contrast / saturation /
|
|
9888
|
+
* sharpness), orientation (mirror / flip / rotate), white-balance,
|
|
9889
|
+
* exposure and backlight-compensation modes.
|
|
9890
|
+
*
|
|
9891
|
+
* NORMALIZATION: every slider is a normalized int 0–100. Vendors expose
|
|
9892
|
+
* these natively as 0–100 or 0–255 (or other ranges); each provider maps
|
|
9893
|
+
* its native range to/from this normalized 0–100 space so the cap surface
|
|
9894
|
+
* (and the derived form) is identical across cameras. `warmth` (manual
|
|
9895
|
+
* white-balance) is likewise normalized 0–100.
|
|
9896
|
+
*
|
|
9897
|
+
* Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
|
|
9898
|
+
* `getOptions` advertises per-camera availability, `getStatus` (auto-
|
|
9899
|
+
* injected from `status`) reports the live values, and a single
|
|
9900
|
+
* `setSettings` mutation applies a partial change. No hand-written
|
|
9901
|
+
* settings-contribution methods — the framework derives the UI + save
|
|
9902
|
+
* routing from this surface.
|
|
9903
|
+
*/
|
|
9904
|
+
/** Sensor/image rotation, degrees clockwise. */
|
|
9905
|
+
var ImageRotateSchema = _enum([
|
|
9906
|
+
"0",
|
|
9907
|
+
"90",
|
|
9908
|
+
"180",
|
|
9909
|
+
"270"
|
|
9910
|
+
]);
|
|
9911
|
+
/** White-balance mode. `manual` unlocks the normalized `warmth` knob. */
|
|
9912
|
+
var WhiteBalanceModeSchema = _enum(["auto", "manual"]);
|
|
9913
|
+
/** Exposure mode. */
|
|
9914
|
+
var ExposureModeSchema = _enum(["auto", "manual"]);
|
|
9915
|
+
/**
|
|
9916
|
+
* Backlight-compensation mode:
|
|
9917
|
+
* - `off` — disabled
|
|
9918
|
+
* - `blc` — backlight compensation
|
|
9919
|
+
* - `wdr` — wide dynamic range
|
|
9920
|
+
* - `hlc` — highlight compensation
|
|
9921
|
+
*/
|
|
9922
|
+
var BacklightModeSchema = _enum([
|
|
9923
|
+
"off",
|
|
9924
|
+
"blc",
|
|
9925
|
+
"wdr",
|
|
9926
|
+
"hlc"
|
|
9927
|
+
]);
|
|
9928
|
+
/** Normalized numeric range descriptor — `{ min, max, step }` per the
|
|
9929
|
+
* getOptions availability convention. Slider values are normalized 0–100. */
|
|
9930
|
+
var NormalizedRangeSchema = object({
|
|
9931
|
+
min: number(),
|
|
9932
|
+
max: number(),
|
|
9933
|
+
step: number()
|
|
9934
|
+
});
|
|
9935
|
+
object({
|
|
9936
|
+
/** Normalized 0–100. */
|
|
9937
|
+
brightness: number().optional(),
|
|
9938
|
+
/** Normalized 0–100. */
|
|
9939
|
+
contrast: number().optional(),
|
|
9940
|
+
/** Normalized 0–100. */
|
|
9941
|
+
saturation: number().optional(),
|
|
9942
|
+
/** Normalized 0–100. */
|
|
9943
|
+
sharpness: number().optional(),
|
|
9944
|
+
mirror: boolean().optional(),
|
|
9945
|
+
flip: boolean().optional(),
|
|
9946
|
+
rotate: ImageRotateSchema.optional(),
|
|
9947
|
+
whiteBalance: WhiteBalanceModeSchema.optional(),
|
|
9948
|
+
/** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
|
|
9949
|
+
warmth: number().optional(),
|
|
9950
|
+
exposureMode: ExposureModeSchema.optional(),
|
|
9951
|
+
backlightMode: BacklightModeSchema.optional(),
|
|
9952
|
+
lastFetchedAt: number()
|
|
9953
|
+
});
|
|
9954
|
+
/**
|
|
9955
|
+
* Per-camera availability descriptor — drives which controls the admin UI
|
|
9956
|
+
* renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
|
|
9957
|
+
* (the normalized 0–100 range); enums as arrays of supported values (empty
|
|
9958
|
+
* array → control hidden). A provider returns honest, camera-probed values
|
|
9959
|
+
* — never hardcoded.
|
|
9960
|
+
*/
|
|
9961
|
+
var ImageSettingsOptionsSchema = object({
|
|
9962
|
+
supportsBrightness: boolean(),
|
|
9963
|
+
brightness: NormalizedRangeSchema.optional(),
|
|
9964
|
+
supportsContrast: boolean(),
|
|
9965
|
+
contrast: NormalizedRangeSchema.optional(),
|
|
9966
|
+
supportsSaturation: boolean(),
|
|
9967
|
+
saturation: NormalizedRangeSchema.optional(),
|
|
9968
|
+
supportsSharpness: boolean(),
|
|
9969
|
+
sharpness: NormalizedRangeSchema.optional(),
|
|
9970
|
+
supportsMirror: boolean(),
|
|
9971
|
+
supportsFlip: boolean(),
|
|
9972
|
+
/** Supported rotation values. Empty → rotation not configurable. */
|
|
9973
|
+
rotateOptions: array(ImageRotateSchema),
|
|
9974
|
+
/** Supported white-balance modes. Empty → white-balance not configurable. */
|
|
9975
|
+
whiteBalanceModes: array(WhiteBalanceModeSchema),
|
|
9976
|
+
supportsWarmth: boolean(),
|
|
9977
|
+
/** Present when `supportsWarmth` — the normalized 0–100 range. */
|
|
9978
|
+
warmth: NormalizedRangeSchema.optional(),
|
|
9979
|
+
/** Supported exposure modes. Empty → exposure not configurable. */
|
|
9980
|
+
exposureModes: array(ExposureModeSchema),
|
|
9981
|
+
/** Supported backlight modes. Empty → backlight-compensation not configurable. */
|
|
9982
|
+
backlightModes: array(BacklightModeSchema)
|
|
9983
|
+
});
|
|
9984
|
+
/**
|
|
9985
|
+
* Partial change to the image config — every field optional. Slider values
|
|
9986
|
+
* are normalized 0–100. A provider ignores fields it does not support.
|
|
9987
|
+
*/
|
|
9988
|
+
var ImageSettingsPatchSchema = object({
|
|
9989
|
+
brightness: number().optional(),
|
|
9990
|
+
contrast: number().optional(),
|
|
9991
|
+
saturation: number().optional(),
|
|
9992
|
+
sharpness: number().optional(),
|
|
9993
|
+
mirror: boolean().optional(),
|
|
9994
|
+
flip: boolean().optional(),
|
|
9995
|
+
rotate: ImageRotateSchema.optional(),
|
|
9996
|
+
whiteBalance: WhiteBalanceModeSchema.optional(),
|
|
9997
|
+
warmth: number().optional(),
|
|
9998
|
+
exposureMode: ExposureModeSchema.optional(),
|
|
9999
|
+
backlightMode: BacklightModeSchema.optional()
|
|
10000
|
+
});
|
|
10001
|
+
DeviceType.Camera, method(object({ deviceId: number() }), ImageSettingsOptionsSchema), method(object({
|
|
10002
|
+
deviceId: number(),
|
|
10003
|
+
settings: ImageSettingsPatchSchema
|
|
10004
|
+
}), _void(), {
|
|
10005
|
+
kind: "mutation",
|
|
10006
|
+
auth: "admin"
|
|
10007
|
+
});
|
|
10008
|
+
/**
|
|
9798
10009
|
* Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
|
|
9799
10010
|
* with a mowing lifecycle plus a dock action.
|
|
9800
10011
|
*
|
|
@@ -10689,6 +10900,16 @@ var RunnerCameraConfigSchema = object({
|
|
|
10689
10900
|
* this gate is bypassed.
|
|
10690
10901
|
*/
|
|
10691
10902
|
onboardMotionDrivesAnalyzer: boolean().default(true),
|
|
10903
|
+
/**
|
|
10904
|
+
* Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
|
|
10905
|
+
* never arms the periodic recheck timer, regardless of `occupancyRecheckSec` —
|
|
10906
|
+
* this is off by default because the recheck re-subscribes a detection session
|
|
10907
|
+
* every N seconds while `watching`, a major source of pull-decoder re-dial
|
|
10908
|
+
* churn (each cycle creates+tears a session → RTSP re-dial → latency). The
|
|
10909
|
+
* `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
|
|
10910
|
+
* (and only render) when this is enabled.
|
|
10911
|
+
*/
|
|
10912
|
+
occupancyRecheckEnabled: boolean().default(false),
|
|
10692
10913
|
occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
|
|
10693
10914
|
occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
|
|
10694
10915
|
/**
|
|
@@ -19109,6 +19330,18 @@ Object.freeze({
|
|
|
19109
19330
|
addonId: null,
|
|
19110
19331
|
access: "view"
|
|
19111
19332
|
},
|
|
19333
|
+
"dayNight.getOptions": {
|
|
19334
|
+
capName: "day-night",
|
|
19335
|
+
capScope: "device",
|
|
19336
|
+
addonId: null,
|
|
19337
|
+
access: "view"
|
|
19338
|
+
},
|
|
19339
|
+
"dayNight.setSettings": {
|
|
19340
|
+
capName: "day-night",
|
|
19341
|
+
capScope: "device",
|
|
19342
|
+
addonId: null,
|
|
19343
|
+
access: "create"
|
|
19344
|
+
},
|
|
19112
19345
|
"decoder.createSession": {
|
|
19113
19346
|
capName: "decoder",
|
|
19114
19347
|
capScope: "system",
|
|
@@ -20039,6 +20272,18 @@ Object.freeze({
|
|
|
20039
20272
|
addonId: null,
|
|
20040
20273
|
access: "create"
|
|
20041
20274
|
},
|
|
20275
|
+
"imageSettings.getOptions": {
|
|
20276
|
+
capName: "image-settings",
|
|
20277
|
+
capScope: "device",
|
|
20278
|
+
addonId: null,
|
|
20279
|
+
access: "view"
|
|
20280
|
+
},
|
|
20281
|
+
"imageSettings.setSettings": {
|
|
20282
|
+
capName: "image-settings",
|
|
20283
|
+
capScope: "device",
|
|
20284
|
+
addonId: null,
|
|
20285
|
+
access: "create"
|
|
20286
|
+
},
|
|
20042
20287
|
"integrations.create": {
|
|
20043
20288
|
capName: "integrations",
|
|
20044
20289
|
capScope: "system",
|
|
@@ -23329,10 +23574,16 @@ var RING_BUDGET_BYTES = RING_BUDGET_MB * 1024 * 1024;
|
|
|
23329
23574
|
* segment (resolution change) a distinct name so a stale consumer mapping is
|
|
23330
23575
|
* never silently reused.
|
|
23331
23576
|
*/
|
|
23577
|
+
/**
|
|
23578
|
+
* Shared prefix for every decoder shm segment name. Startup orphan reclamation
|
|
23579
|
+
* (`purgeOrphanSegments`) keys off this to find segments left behind by a
|
|
23580
|
+
* crashed prior instance.
|
|
23581
|
+
*/
|
|
23582
|
+
var SEGMENT_NAME_PREFIX = "csf.";
|
|
23332
23583
|
function makeSegmentName(seed, generation) {
|
|
23333
23584
|
let hash = 5381;
|
|
23334
23585
|
for (let i = 0; i < seed.length; i += 1) hash = (hash << 5) + hash + seed.charCodeAt(i) | 0;
|
|
23335
|
-
return
|
|
23586
|
+
return `${SEGMENT_NAME_PREFIX}${(hash >>> 0).toString(36)}.${generation}`;
|
|
23336
23587
|
}
|
|
23337
23588
|
/**
|
|
23338
23589
|
* The decoder-side owner of one stream's shared-memory frame ring.
|
|
@@ -24175,6 +24426,71 @@ function probeGpuScaleFilters(ffmpegPath, logger) {
|
|
|
24175
24426
|
});
|
|
24176
24427
|
}
|
|
24177
24428
|
//#endregion
|
|
24429
|
+
//#region src/shm-orphan-purge.ts
|
|
24430
|
+
/**
|
|
24431
|
+
* Startup reclamation of orphaned shared-memory segments.
|
|
24432
|
+
*
|
|
24433
|
+
* A decoder writes frames into named `/dev/shm` segments and unlinks each one
|
|
24434
|
+
* on graceful session teardown (`DecoderFrameRingSink.destroy`). When the
|
|
24435
|
+
* decoder process dies *ungracefully* — SIGBUS, OOM-kill, or a SIGKILL during
|
|
24436
|
+
* redeploy — that teardown never runs and the segment is orphaned: it stays in
|
|
24437
|
+
* the tmpfs forever, since nothing else knows its name. Across many
|
|
24438
|
+
* crashes/redeploys these accumulate until `/dev/shm` fills, at which point the
|
|
24439
|
+
* next `mmap` write faults with an uncatchable SIGBUS and the decoder
|
|
24440
|
+
* crash-loops into its circuit breaker (which is exactly the incident this
|
|
24441
|
+
* guards against).
|
|
24442
|
+
*
|
|
24443
|
+
* The reclamation is safe *at process startup*: a freshly-booting decoder owns
|
|
24444
|
+
* no live sessions, so every pre-existing segment with its prefix is by
|
|
24445
|
+
* definition an orphan from a dead instance. `shm_unlink` only removes the
|
|
24446
|
+
* name — any consumer still holding a mapping keeps reading valid memory until
|
|
24447
|
+
* it closes (POSIX deferred reclaim + the ring seqlock), so unlinking is safe
|
|
24448
|
+
* even if a stale reader is momentarily still attached.
|
|
24449
|
+
*
|
|
24450
|
+
* POSIX-only: segments surface as files under `/dev/shm` on Linux. On platforms
|
|
24451
|
+
* without that directory (Windows, macOS) the scan finds nothing — no-op.
|
|
24452
|
+
*
|
|
24453
|
+
* NOTE: this lives inside the decoder addon (not `@camstack/shm-ring`) so it
|
|
24454
|
+
* ships in the self-contained addon bundle via `camstack deploy`, reusing the
|
|
24455
|
+
* already-deployed `unlinkSegment`; no host base-image rebuild required.
|
|
24456
|
+
*/
|
|
24457
|
+
/** Default tmpfs directory where POSIX shared-memory segments appear on Linux. */
|
|
24458
|
+
var DEFAULT_SHM_DIR = "/dev/shm";
|
|
24459
|
+
/**
|
|
24460
|
+
* Unlink every shared-memory segment whose name starts with `prefix`.
|
|
24461
|
+
*
|
|
24462
|
+
* Intended to run ONCE at decoder startup, before any session is created, to
|
|
24463
|
+
* reclaim segments orphaned by a previously-crashed instance. A per-file unlink
|
|
24464
|
+
* failure is swallowed so one stuck segment cannot block reclaiming the rest.
|
|
24465
|
+
*/
|
|
24466
|
+
function purgeOrphanSegments(prefix, options = {}) {
|
|
24467
|
+
const dir = options.dir ?? DEFAULT_SHM_DIR;
|
|
24468
|
+
const unlink = options.unlink ?? _camstack_shm_ring.unlinkSegment;
|
|
24469
|
+
let entries;
|
|
24470
|
+
try {
|
|
24471
|
+
entries = (0, node_fs.readdirSync)(dir);
|
|
24472
|
+
} catch {
|
|
24473
|
+
return {
|
|
24474
|
+
scanned: 0,
|
|
24475
|
+
removed: 0,
|
|
24476
|
+
names: []
|
|
24477
|
+
};
|
|
24478
|
+
}
|
|
24479
|
+
const names = [];
|
|
24480
|
+
for (const name of entries) {
|
|
24481
|
+
if (!name.startsWith(prefix)) continue;
|
|
24482
|
+
try {
|
|
24483
|
+
unlink(name);
|
|
24484
|
+
names.push(name);
|
|
24485
|
+
} catch {}
|
|
24486
|
+
}
|
|
24487
|
+
return {
|
|
24488
|
+
scanned: entries.length,
|
|
24489
|
+
removed: names.length,
|
|
24490
|
+
names
|
|
24491
|
+
};
|
|
24492
|
+
}
|
|
24493
|
+
//#endregion
|
|
24178
24494
|
//#region src/audio-codec/ffmpeg-audio-process.ts
|
|
24179
24495
|
/**
|
|
24180
24496
|
* Minimal ffmpeg subprocess wrapper shared by the audio decode + encode
|
|
@@ -25431,6 +25747,11 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
25431
25747
|
return registrations;
|
|
25432
25748
|
}
|
|
25433
25749
|
this.ctx.logger.info("ffmpeg decoder addon initialized", { meta: { selectedBackend: backend } });
|
|
25750
|
+
const purged = purgeOrphanSegments(SEGMENT_NAME_PREFIX);
|
|
25751
|
+
if (purged.removed > 0) this.ctx.logger.warn("ffmpeg decoder: reclaimed orphaned shm segments at startup", { meta: {
|
|
25752
|
+
removed: purged.removed,
|
|
25753
|
+
scanned: purged.scanned
|
|
25754
|
+
} });
|
|
25434
25755
|
this.frameReaders = new _camstack_shm_ring.FrameRingReaderCache(this.ctx.logger);
|
|
25435
25756
|
this.probedGpuScaleFilters = await probeGpuScaleFilters(this.ffmpegPath, this.ctx.logger);
|
|
25436
25757
|
this.ctx.logger.info("decoder-ffmpeg: probed GPU scale filters", { meta: {
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { FrameRingReaderCache, FrameRingWriter, MIN_RING_SLOTS, computeSegmentSize, computeSlotByteLength, createSegment, deriveSlotCount } from "@camstack/shm-ring";
|
|
2
|
+
import { FrameRingReaderCache, FrameRingWriter, MIN_RING_SLOTS, computeSegmentSize, computeSlotByteLength, createSegment, deriveSlotCount, unlinkSegment } from "@camstack/shm-ring";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
+
import { readdirSync } from "node:fs";
|
|
4
5
|
//#region ../../node_modules/zod/v4/core/core.js
|
|
5
6
|
var _a$1;
|
|
6
7
|
function $constructor(name, initializer, params) {
|
|
@@ -4630,7 +4631,7 @@ function _instanceof(cls, params = {}) {
|
|
|
4630
4631
|
return inst;
|
|
4631
4632
|
}
|
|
4632
4633
|
//#endregion
|
|
4633
|
-
//#region ../types/dist/sleep-
|
|
4634
|
+
//#region ../types/dist/sleep-CZDdRBua.mjs
|
|
4634
4635
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4635
4636
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4636
4637
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -7237,7 +7238,16 @@ var DecoderStatsSchema = object({
|
|
|
7237
7238
|
inputFps: number(),
|
|
7238
7239
|
outputFps: number(),
|
|
7239
7240
|
avgDecodeTimeMs: number(),
|
|
7240
|
-
droppedFrames: number()
|
|
7241
|
+
droppedFrames: number(),
|
|
7242
|
+
/**
|
|
7243
|
+
* Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
|
|
7244
|
+
* lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
|
|
7245
|
+
* the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
|
|
7246
|
+
* is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
|
|
7247
|
+
*/
|
|
7248
|
+
lagMs: number().optional(),
|
|
7249
|
+
effectiveFps: number().optional(),
|
|
7250
|
+
adaptiveFps: number().optional()
|
|
7241
7251
|
});
|
|
7242
7252
|
var DecoderSessionConfigSchema = object({
|
|
7243
7253
|
codec: string(),
|
|
@@ -7278,7 +7288,15 @@ var DecoderSessionConfigSchema = object({
|
|
|
7278
7288
|
* other — `pullFrames` returns nothing for an `'shm'` session and
|
|
7279
7289
|
* `pullHandles` returns nothing for a `'callback'` session.
|
|
7280
7290
|
*/
|
|
7281
|
-
frameSink: _enum(["callback", "shm"]).default("callback")
|
|
7291
|
+
frameSink: _enum(["callback", "shm"]).default("callback"),
|
|
7292
|
+
/**
|
|
7293
|
+
* Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
|
|
7294
|
+
* a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
|
|
7295
|
+
* real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
|
|
7296
|
+
* stream-broker's `streamingDebug` gate — off by default so production logs
|
|
7297
|
+
* stay quiet and the emit path pays zero per-frame cost when disabled.
|
|
7298
|
+
*/
|
|
7299
|
+
debug: boolean().optional()
|
|
7282
7300
|
});
|
|
7283
7301
|
var EncodeProfileSchema = object({
|
|
7284
7302
|
video: object({
|
|
@@ -9436,6 +9454,75 @@ DeviceType.Cover, method(object({ deviceId: number().int().nonnegative() }), _vo
|
|
|
9436
9454
|
auth: "admin"
|
|
9437
9455
|
});
|
|
9438
9456
|
/**
|
|
9457
|
+
* Vendor-neutral day/night (IR-cut) control — the per-camera config cap
|
|
9458
|
+
* shared by reolink / hikvision / amcrest. Models the common firmware
|
|
9459
|
+
* surface: the IR-cut switching MODE plus the two knobs that gate it
|
|
9460
|
+
* (photocell `sensitivity` + `switchDelaySec`). Each vendor maps these
|
|
9461
|
+
* onto its own ISAPI / Baichuan / Dahua-CGI fields; the cap standardises
|
|
9462
|
+
* the shape so ONE derived-form renders every camera.
|
|
9463
|
+
*
|
|
9464
|
+
* Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
|
|
9465
|
+
* `getOptions` advertises per-camera availability, `getStatus` (auto-
|
|
9466
|
+
* injected from `status`) reports the live values, and a single
|
|
9467
|
+
* `setSettings` mutation applies a partial change. No hand-written
|
|
9468
|
+
* settings-contribution methods — the framework derives the UI + save
|
|
9469
|
+
* routing from this surface.
|
|
9470
|
+
*/
|
|
9471
|
+
/** IR-cut switching mode. `schedule` = time-of-day table configured on the camera. */
|
|
9472
|
+
var DayNightModeSchema = _enum([
|
|
9473
|
+
"auto",
|
|
9474
|
+
"day",
|
|
9475
|
+
"night",
|
|
9476
|
+
"schedule"
|
|
9477
|
+
]);
|
|
9478
|
+
/** Normalized numeric range descriptor — `{ min, max, step }` per the
|
|
9479
|
+
* getOptions availability convention. Normalized values are 0–100. */
|
|
9480
|
+
var NormalizedRangeSchema$1 = object({
|
|
9481
|
+
min: number(),
|
|
9482
|
+
max: number(),
|
|
9483
|
+
step: number()
|
|
9484
|
+
});
|
|
9485
|
+
object({
|
|
9486
|
+
mode: DayNightModeSchema,
|
|
9487
|
+
/** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
|
|
9488
|
+
sensitivity: number().optional(),
|
|
9489
|
+
/** Delay before the IR-cut filter flips, in seconds. */
|
|
9490
|
+
switchDelaySec: number().optional(),
|
|
9491
|
+
lastFetchedAt: number()
|
|
9492
|
+
});
|
|
9493
|
+
/**
|
|
9494
|
+
* Per-camera availability descriptor — drives which controls the admin UI
|
|
9495
|
+
* renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
|
|
9496
|
+
* (normalized 0–100); the mode choice-set as an array. A provider returns
|
|
9497
|
+
* honest, camera-probed values — never hardcoded.
|
|
9498
|
+
*/
|
|
9499
|
+
var DayNightOptionsSchema = object({
|
|
9500
|
+
/** Modes this camera accepts. Empty → the camera has no configurable day/night mode. */
|
|
9501
|
+
modes: array(DayNightModeSchema),
|
|
9502
|
+
supportsSensitivity: boolean(),
|
|
9503
|
+
/** Present when `supportsSensitivity` — the normalized 0–100 range. */
|
|
9504
|
+
sensitivity: NormalizedRangeSchema$1.optional(),
|
|
9505
|
+
supportsSwitchDelay: boolean(),
|
|
9506
|
+
/** Present when `supportsSwitchDelay` — the allowed delay range in seconds. */
|
|
9507
|
+
switchDelaySec: NormalizedRangeSchema$1.optional()
|
|
9508
|
+
});
|
|
9509
|
+
/**
|
|
9510
|
+
* Partial change to the day/night config — every field optional. A
|
|
9511
|
+
* provider ignores fields it does not support.
|
|
9512
|
+
*/
|
|
9513
|
+
var DayNightSettingsPatchSchema = object({
|
|
9514
|
+
mode: DayNightModeSchema.optional(),
|
|
9515
|
+
sensitivity: number().optional(),
|
|
9516
|
+
switchDelaySec: number().optional()
|
|
9517
|
+
});
|
|
9518
|
+
DeviceType.Camera, method(object({ deviceId: number() }), DayNightOptionsSchema), method(object({
|
|
9519
|
+
deviceId: number(),
|
|
9520
|
+
settings: DayNightSettingsPatchSchema
|
|
9521
|
+
}), _void(), {
|
|
9522
|
+
kind: "mutation",
|
|
9523
|
+
auth: "admin"
|
|
9524
|
+
});
|
|
9525
|
+
/**
|
|
9439
9526
|
* Identity envelope for a device's upstream-system metadata.
|
|
9440
9527
|
*
|
|
9441
9528
|
* Two jobs:
|
|
@@ -9791,6 +9878,130 @@ object({
|
|
|
9791
9878
|
});
|
|
9792
9879
|
DeviceType.Image;
|
|
9793
9880
|
/**
|
|
9881
|
+
* Vendor-neutral image / picture-adjustment cap — the per-camera config
|
|
9882
|
+
* cap shared by reolink / hikvision / amcrest. Models the common ISP
|
|
9883
|
+
* surface: the four picture sliders (brightness / contrast / saturation /
|
|
9884
|
+
* sharpness), orientation (mirror / flip / rotate), white-balance,
|
|
9885
|
+
* exposure and backlight-compensation modes.
|
|
9886
|
+
*
|
|
9887
|
+
* NORMALIZATION: every slider is a normalized int 0–100. Vendors expose
|
|
9888
|
+
* these natively as 0–100 or 0–255 (or other ranges); each provider maps
|
|
9889
|
+
* its native range to/from this normalized 0–100 space so the cap surface
|
|
9890
|
+
* (and the derived form) is identical across cameras. `warmth` (manual
|
|
9891
|
+
* white-balance) is likewise normalized 0–100.
|
|
9892
|
+
*
|
|
9893
|
+
* Follows the D14 `deviceConfig` archetype (see `stream-params.cap.ts`):
|
|
9894
|
+
* `getOptions` advertises per-camera availability, `getStatus` (auto-
|
|
9895
|
+
* injected from `status`) reports the live values, and a single
|
|
9896
|
+
* `setSettings` mutation applies a partial change. No hand-written
|
|
9897
|
+
* settings-contribution methods — the framework derives the UI + save
|
|
9898
|
+
* routing from this surface.
|
|
9899
|
+
*/
|
|
9900
|
+
/** Sensor/image rotation, degrees clockwise. */
|
|
9901
|
+
var ImageRotateSchema = _enum([
|
|
9902
|
+
"0",
|
|
9903
|
+
"90",
|
|
9904
|
+
"180",
|
|
9905
|
+
"270"
|
|
9906
|
+
]);
|
|
9907
|
+
/** White-balance mode. `manual` unlocks the normalized `warmth` knob. */
|
|
9908
|
+
var WhiteBalanceModeSchema = _enum(["auto", "manual"]);
|
|
9909
|
+
/** Exposure mode. */
|
|
9910
|
+
var ExposureModeSchema = _enum(["auto", "manual"]);
|
|
9911
|
+
/**
|
|
9912
|
+
* Backlight-compensation mode:
|
|
9913
|
+
* - `off` — disabled
|
|
9914
|
+
* - `blc` — backlight compensation
|
|
9915
|
+
* - `wdr` — wide dynamic range
|
|
9916
|
+
* - `hlc` — highlight compensation
|
|
9917
|
+
*/
|
|
9918
|
+
var BacklightModeSchema = _enum([
|
|
9919
|
+
"off",
|
|
9920
|
+
"blc",
|
|
9921
|
+
"wdr",
|
|
9922
|
+
"hlc"
|
|
9923
|
+
]);
|
|
9924
|
+
/** Normalized numeric range descriptor — `{ min, max, step }` per the
|
|
9925
|
+
* getOptions availability convention. Slider values are normalized 0–100. */
|
|
9926
|
+
var NormalizedRangeSchema = object({
|
|
9927
|
+
min: number(),
|
|
9928
|
+
max: number(),
|
|
9929
|
+
step: number()
|
|
9930
|
+
});
|
|
9931
|
+
object({
|
|
9932
|
+
/** Normalized 0–100. */
|
|
9933
|
+
brightness: number().optional(),
|
|
9934
|
+
/** Normalized 0–100. */
|
|
9935
|
+
contrast: number().optional(),
|
|
9936
|
+
/** Normalized 0–100. */
|
|
9937
|
+
saturation: number().optional(),
|
|
9938
|
+
/** Normalized 0–100. */
|
|
9939
|
+
sharpness: number().optional(),
|
|
9940
|
+
mirror: boolean().optional(),
|
|
9941
|
+
flip: boolean().optional(),
|
|
9942
|
+
rotate: ImageRotateSchema.optional(),
|
|
9943
|
+
whiteBalance: WhiteBalanceModeSchema.optional(),
|
|
9944
|
+
/** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
|
|
9945
|
+
warmth: number().optional(),
|
|
9946
|
+
exposureMode: ExposureModeSchema.optional(),
|
|
9947
|
+
backlightMode: BacklightModeSchema.optional(),
|
|
9948
|
+
lastFetchedAt: number()
|
|
9949
|
+
});
|
|
9950
|
+
/**
|
|
9951
|
+
* Per-camera availability descriptor — drives which controls the admin UI
|
|
9952
|
+
* renders. Booleans as `supportsX`; numeric ranges as `{ min, max, step }`
|
|
9953
|
+
* (the normalized 0–100 range); enums as arrays of supported values (empty
|
|
9954
|
+
* array → control hidden). A provider returns honest, camera-probed values
|
|
9955
|
+
* — never hardcoded.
|
|
9956
|
+
*/
|
|
9957
|
+
var ImageSettingsOptionsSchema = object({
|
|
9958
|
+
supportsBrightness: boolean(),
|
|
9959
|
+
brightness: NormalizedRangeSchema.optional(),
|
|
9960
|
+
supportsContrast: boolean(),
|
|
9961
|
+
contrast: NormalizedRangeSchema.optional(),
|
|
9962
|
+
supportsSaturation: boolean(),
|
|
9963
|
+
saturation: NormalizedRangeSchema.optional(),
|
|
9964
|
+
supportsSharpness: boolean(),
|
|
9965
|
+
sharpness: NormalizedRangeSchema.optional(),
|
|
9966
|
+
supportsMirror: boolean(),
|
|
9967
|
+
supportsFlip: boolean(),
|
|
9968
|
+
/** Supported rotation values. Empty → rotation not configurable. */
|
|
9969
|
+
rotateOptions: array(ImageRotateSchema),
|
|
9970
|
+
/** Supported white-balance modes. Empty → white-balance not configurable. */
|
|
9971
|
+
whiteBalanceModes: array(WhiteBalanceModeSchema),
|
|
9972
|
+
supportsWarmth: boolean(),
|
|
9973
|
+
/** Present when `supportsWarmth` — the normalized 0–100 range. */
|
|
9974
|
+
warmth: NormalizedRangeSchema.optional(),
|
|
9975
|
+
/** Supported exposure modes. Empty → exposure not configurable. */
|
|
9976
|
+
exposureModes: array(ExposureModeSchema),
|
|
9977
|
+
/** Supported backlight modes. Empty → backlight-compensation not configurable. */
|
|
9978
|
+
backlightModes: array(BacklightModeSchema)
|
|
9979
|
+
});
|
|
9980
|
+
/**
|
|
9981
|
+
* Partial change to the image config — every field optional. Slider values
|
|
9982
|
+
* are normalized 0–100. A provider ignores fields it does not support.
|
|
9983
|
+
*/
|
|
9984
|
+
var ImageSettingsPatchSchema = object({
|
|
9985
|
+
brightness: number().optional(),
|
|
9986
|
+
contrast: number().optional(),
|
|
9987
|
+
saturation: number().optional(),
|
|
9988
|
+
sharpness: number().optional(),
|
|
9989
|
+
mirror: boolean().optional(),
|
|
9990
|
+
flip: boolean().optional(),
|
|
9991
|
+
rotate: ImageRotateSchema.optional(),
|
|
9992
|
+
whiteBalance: WhiteBalanceModeSchema.optional(),
|
|
9993
|
+
warmth: number().optional(),
|
|
9994
|
+
exposureMode: ExposureModeSchema.optional(),
|
|
9995
|
+
backlightMode: BacklightModeSchema.optional()
|
|
9996
|
+
});
|
|
9997
|
+
DeviceType.Camera, method(object({ deviceId: number() }), ImageSettingsOptionsSchema), method(object({
|
|
9998
|
+
deviceId: number(),
|
|
9999
|
+
settings: ImageSettingsPatchSchema
|
|
10000
|
+
}), _void(), {
|
|
10001
|
+
kind: "mutation",
|
|
10002
|
+
auth: "admin"
|
|
10003
|
+
});
|
|
10004
|
+
/**
|
|
9794
10005
|
* Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
|
|
9795
10006
|
* with a mowing lifecycle plus a dock action.
|
|
9796
10007
|
*
|
|
@@ -10685,6 +10896,16 @@ var RunnerCameraConfigSchema = object({
|
|
|
10685
10896
|
* this gate is bypassed.
|
|
10686
10897
|
*/
|
|
10687
10898
|
onboardMotionDrivesAnalyzer: boolean().default(true),
|
|
10899
|
+
/**
|
|
10900
|
+
* Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
|
|
10901
|
+
* never arms the periodic recheck timer, regardless of `occupancyRecheckSec` —
|
|
10902
|
+
* this is off by default because the recheck re-subscribes a detection session
|
|
10903
|
+
* every N seconds while `watching`, a major source of pull-decoder re-dial
|
|
10904
|
+
* churn (each cycle creates+tears a session → RTSP re-dial → latency). The
|
|
10905
|
+
* `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
|
|
10906
|
+
* (and only render) when this is enabled.
|
|
10907
|
+
*/
|
|
10908
|
+
occupancyRecheckEnabled: boolean().default(false),
|
|
10688
10909
|
occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
|
|
10689
10910
|
occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
|
|
10690
10911
|
/**
|
|
@@ -19105,6 +19326,18 @@ Object.freeze({
|
|
|
19105
19326
|
addonId: null,
|
|
19106
19327
|
access: "view"
|
|
19107
19328
|
},
|
|
19329
|
+
"dayNight.getOptions": {
|
|
19330
|
+
capName: "day-night",
|
|
19331
|
+
capScope: "device",
|
|
19332
|
+
addonId: null,
|
|
19333
|
+
access: "view"
|
|
19334
|
+
},
|
|
19335
|
+
"dayNight.setSettings": {
|
|
19336
|
+
capName: "day-night",
|
|
19337
|
+
capScope: "device",
|
|
19338
|
+
addonId: null,
|
|
19339
|
+
access: "create"
|
|
19340
|
+
},
|
|
19108
19341
|
"decoder.createSession": {
|
|
19109
19342
|
capName: "decoder",
|
|
19110
19343
|
capScope: "system",
|
|
@@ -20035,6 +20268,18 @@ Object.freeze({
|
|
|
20035
20268
|
addonId: null,
|
|
20036
20269
|
access: "create"
|
|
20037
20270
|
},
|
|
20271
|
+
"imageSettings.getOptions": {
|
|
20272
|
+
capName: "image-settings",
|
|
20273
|
+
capScope: "device",
|
|
20274
|
+
addonId: null,
|
|
20275
|
+
access: "view"
|
|
20276
|
+
},
|
|
20277
|
+
"imageSettings.setSettings": {
|
|
20278
|
+
capName: "image-settings",
|
|
20279
|
+
capScope: "device",
|
|
20280
|
+
addonId: null,
|
|
20281
|
+
access: "create"
|
|
20282
|
+
},
|
|
20038
20283
|
"integrations.create": {
|
|
20039
20284
|
capName: "integrations",
|
|
20040
20285
|
capScope: "system",
|
|
@@ -23325,10 +23570,16 @@ var RING_BUDGET_BYTES = RING_BUDGET_MB * 1024 * 1024;
|
|
|
23325
23570
|
* segment (resolution change) a distinct name so a stale consumer mapping is
|
|
23326
23571
|
* never silently reused.
|
|
23327
23572
|
*/
|
|
23573
|
+
/**
|
|
23574
|
+
* Shared prefix for every decoder shm segment name. Startup orphan reclamation
|
|
23575
|
+
* (`purgeOrphanSegments`) keys off this to find segments left behind by a
|
|
23576
|
+
* crashed prior instance.
|
|
23577
|
+
*/
|
|
23578
|
+
var SEGMENT_NAME_PREFIX = "csf.";
|
|
23328
23579
|
function makeSegmentName(seed, generation) {
|
|
23329
23580
|
let hash = 5381;
|
|
23330
23581
|
for (let i = 0; i < seed.length; i += 1) hash = (hash << 5) + hash + seed.charCodeAt(i) | 0;
|
|
23331
|
-
return
|
|
23582
|
+
return `${SEGMENT_NAME_PREFIX}${(hash >>> 0).toString(36)}.${generation}`;
|
|
23332
23583
|
}
|
|
23333
23584
|
/**
|
|
23334
23585
|
* The decoder-side owner of one stream's shared-memory frame ring.
|
|
@@ -24171,6 +24422,71 @@ function probeGpuScaleFilters(ffmpegPath, logger) {
|
|
|
24171
24422
|
});
|
|
24172
24423
|
}
|
|
24173
24424
|
//#endregion
|
|
24425
|
+
//#region src/shm-orphan-purge.ts
|
|
24426
|
+
/**
|
|
24427
|
+
* Startup reclamation of orphaned shared-memory segments.
|
|
24428
|
+
*
|
|
24429
|
+
* A decoder writes frames into named `/dev/shm` segments and unlinks each one
|
|
24430
|
+
* on graceful session teardown (`DecoderFrameRingSink.destroy`). When the
|
|
24431
|
+
* decoder process dies *ungracefully* — SIGBUS, OOM-kill, or a SIGKILL during
|
|
24432
|
+
* redeploy — that teardown never runs and the segment is orphaned: it stays in
|
|
24433
|
+
* the tmpfs forever, since nothing else knows its name. Across many
|
|
24434
|
+
* crashes/redeploys these accumulate until `/dev/shm` fills, at which point the
|
|
24435
|
+
* next `mmap` write faults with an uncatchable SIGBUS and the decoder
|
|
24436
|
+
* crash-loops into its circuit breaker (which is exactly the incident this
|
|
24437
|
+
* guards against).
|
|
24438
|
+
*
|
|
24439
|
+
* The reclamation is safe *at process startup*: a freshly-booting decoder owns
|
|
24440
|
+
* no live sessions, so every pre-existing segment with its prefix is by
|
|
24441
|
+
* definition an orphan from a dead instance. `shm_unlink` only removes the
|
|
24442
|
+
* name — any consumer still holding a mapping keeps reading valid memory until
|
|
24443
|
+
* it closes (POSIX deferred reclaim + the ring seqlock), so unlinking is safe
|
|
24444
|
+
* even if a stale reader is momentarily still attached.
|
|
24445
|
+
*
|
|
24446
|
+
* POSIX-only: segments surface as files under `/dev/shm` on Linux. On platforms
|
|
24447
|
+
* without that directory (Windows, macOS) the scan finds nothing — no-op.
|
|
24448
|
+
*
|
|
24449
|
+
* NOTE: this lives inside the decoder addon (not `@camstack/shm-ring`) so it
|
|
24450
|
+
* ships in the self-contained addon bundle via `camstack deploy`, reusing the
|
|
24451
|
+
* already-deployed `unlinkSegment`; no host base-image rebuild required.
|
|
24452
|
+
*/
|
|
24453
|
+
/** Default tmpfs directory where POSIX shared-memory segments appear on Linux. */
|
|
24454
|
+
var DEFAULT_SHM_DIR = "/dev/shm";
|
|
24455
|
+
/**
|
|
24456
|
+
* Unlink every shared-memory segment whose name starts with `prefix`.
|
|
24457
|
+
*
|
|
24458
|
+
* Intended to run ONCE at decoder startup, before any session is created, to
|
|
24459
|
+
* reclaim segments orphaned by a previously-crashed instance. A per-file unlink
|
|
24460
|
+
* failure is swallowed so one stuck segment cannot block reclaiming the rest.
|
|
24461
|
+
*/
|
|
24462
|
+
function purgeOrphanSegments(prefix, options = {}) {
|
|
24463
|
+
const dir = options.dir ?? DEFAULT_SHM_DIR;
|
|
24464
|
+
const unlink = options.unlink ?? unlinkSegment;
|
|
24465
|
+
let entries;
|
|
24466
|
+
try {
|
|
24467
|
+
entries = readdirSync(dir);
|
|
24468
|
+
} catch {
|
|
24469
|
+
return {
|
|
24470
|
+
scanned: 0,
|
|
24471
|
+
removed: 0,
|
|
24472
|
+
names: []
|
|
24473
|
+
};
|
|
24474
|
+
}
|
|
24475
|
+
const names = [];
|
|
24476
|
+
for (const name of entries) {
|
|
24477
|
+
if (!name.startsWith(prefix)) continue;
|
|
24478
|
+
try {
|
|
24479
|
+
unlink(name);
|
|
24480
|
+
names.push(name);
|
|
24481
|
+
} catch {}
|
|
24482
|
+
}
|
|
24483
|
+
return {
|
|
24484
|
+
scanned: entries.length,
|
|
24485
|
+
removed: names.length,
|
|
24486
|
+
names
|
|
24487
|
+
};
|
|
24488
|
+
}
|
|
24489
|
+
//#endregion
|
|
24174
24490
|
//#region src/audio-codec/ffmpeg-audio-process.ts
|
|
24175
24491
|
/**
|
|
24176
24492
|
* Minimal ffmpeg subprocess wrapper shared by the audio decode + encode
|
|
@@ -25427,6 +25743,11 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
25427
25743
|
return registrations;
|
|
25428
25744
|
}
|
|
25429
25745
|
this.ctx.logger.info("ffmpeg decoder addon initialized", { meta: { selectedBackend: backend } });
|
|
25746
|
+
const purged = purgeOrphanSegments(SEGMENT_NAME_PREFIX);
|
|
25747
|
+
if (purged.removed > 0) this.ctx.logger.warn("ffmpeg decoder: reclaimed orphaned shm segments at startup", { meta: {
|
|
25748
|
+
removed: purged.removed,
|
|
25749
|
+
scanned: purged.scanned
|
|
25750
|
+
} });
|
|
25430
25751
|
this.frameReaders = new FrameRingReaderCache(this.ctx.logger);
|
|
25431
25752
|
this.probedGpuScaleFilters = await probeGpuScaleFilters(this.ffmpegPath, this.ctx.logger);
|
|
25432
25753
|
this.ctx.logger.info("decoder-ffmpeg: probed GPU scale filters", { meta: {
|