@camstack/addon-decoder-nodeav 0.1.1
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/chunk-3GDQP6AS.mjs +16 -0
- package/dist/chunk-3GDQP6AS.mjs.map +1 -0
- package/dist/index.d.mts +293 -0
- package/dist/index.d.ts +293 -0
- package/dist/index.js +7536 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +920 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib-PHLUZNNX.mjs +6588 -0
- package/dist/lib-PHLUZNNX.mjs.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
__require,
|
|
14
|
+
__commonJS
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=chunk-3GDQP6AS.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import * as _camstack_types from '@camstack/types';
|
|
2
|
+
import { BaseAddon, DecoderHwAccelConfig, IDecoderCapProvider, ProviderRegistration, FrameFormat, DecoderStats, IDecoderSession, DecoderSessionConfig, IScopedLogger, HwAccelBackend, IKernelHwAccel, EncodedPacket, DecodedFrame, Unsubscribe } from '@camstack/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Phase 2d of the pipeline-settings migration — decoder addon now
|
|
6
|
+
* owns its own `hwaccel` choice + `probedBestHwaccel` hint. Sessions
|
|
7
|
+
* resolve the effective backend from this addon's global settings
|
|
8
|
+
* instead of the orchestrator's legacy `AgentPipelineSettings.hwaccel`.
|
|
9
|
+
* Shared config shape + UI options live in `@camstack/types`
|
|
10
|
+
* (`DecoderHwAccelConfig` / `HWACCEL_OPTIONS`) so every decoder addon
|
|
11
|
+
* speaks the same vocabulary.
|
|
12
|
+
*/
|
|
13
|
+
/** Cap-compatible frame shape — format must match DecodedFrameSchema enum values. */
|
|
14
|
+
type CapDecodedFrame = {
|
|
15
|
+
data: Uint8Array<ArrayBuffer>;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
format: FrameFormat;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
};
|
|
21
|
+
/** Cap-compatible session config — matches DecoderSessionConfigSchema output type. */
|
|
22
|
+
type CapDecoderSessionConfig = {
|
|
23
|
+
codec: string;
|
|
24
|
+
maxFps: number;
|
|
25
|
+
outputFormat: FrameFormat;
|
|
26
|
+
scale: number;
|
|
27
|
+
width?: number;
|
|
28
|
+
height?: number;
|
|
29
|
+
};
|
|
30
|
+
/** Cap-compatible encoded packet — data is Uint8Array matching EncodedPacketSchema. */
|
|
31
|
+
type CapEncodedPacket = {
|
|
32
|
+
type: 'video' | 'audio';
|
|
33
|
+
data: Uint8Array<ArrayBuffer>;
|
|
34
|
+
pts: number;
|
|
35
|
+
dts: number;
|
|
36
|
+
keyframe: boolean;
|
|
37
|
+
codec: string;
|
|
38
|
+
};
|
|
39
|
+
declare class DecoderNodeAvAddon extends BaseAddon<DecoderHwAccelConfig> implements IDecoderCapProvider {
|
|
40
|
+
private readonly sessions;
|
|
41
|
+
private readonly frameBuffers;
|
|
42
|
+
private readonly unsubscribers;
|
|
43
|
+
private readonly sessionMeta;
|
|
44
|
+
constructor();
|
|
45
|
+
protected globalSettingsSchema(): _camstack_types.ConfigUISchema;
|
|
46
|
+
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the effective hwaccel backend for a new session. Reads
|
|
49
|
+
* this addon's own `hwaccel` setting first. `'auto'` defers to the
|
|
50
|
+
* session's local resolver (`ctx.kernel.hwaccel`) which probes the
|
|
51
|
+
* host and picks. No more orchestrator round-trip — decoder addon
|
|
52
|
+
* is self-sufficient for this setting as of phase 2d.
|
|
53
|
+
*/
|
|
54
|
+
private resolveHwAccelPref;
|
|
55
|
+
/**
|
|
56
|
+
* Re-run the platform probe on this host and persist the detected
|
|
57
|
+
* backend as `probedBestHwaccel`. The operator's `hwaccel` setting
|
|
58
|
+
* is intentionally left alone — the probe only updates the hint.
|
|
59
|
+
*/
|
|
60
|
+
reprobeHwaccel(): Promise<{
|
|
61
|
+
backend: string;
|
|
62
|
+
}>;
|
|
63
|
+
supportsCodec(input: {
|
|
64
|
+
codec: string;
|
|
65
|
+
}): Promise<boolean>;
|
|
66
|
+
getInfo(): Promise<{
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
isPullMode?: boolean;
|
|
70
|
+
priority?: number;
|
|
71
|
+
}>;
|
|
72
|
+
createSession(config: CapDecoderSessionConfig): Promise<{
|
|
73
|
+
sessionId: string;
|
|
74
|
+
nodeId: string;
|
|
75
|
+
}>;
|
|
76
|
+
destroySession(input: {
|
|
77
|
+
sessionId: string;
|
|
78
|
+
}): Promise<void>;
|
|
79
|
+
listActiveSessions(): Promise<readonly {
|
|
80
|
+
sessionId: string;
|
|
81
|
+
codec: string;
|
|
82
|
+
outputFormat: string;
|
|
83
|
+
createdAtMs: number;
|
|
84
|
+
}[]>;
|
|
85
|
+
pushPacket(input: {
|
|
86
|
+
sessionId: string;
|
|
87
|
+
packet: CapEncodedPacket;
|
|
88
|
+
}): Promise<void>;
|
|
89
|
+
openStream(input: {
|
|
90
|
+
sessionId: string;
|
|
91
|
+
url: string;
|
|
92
|
+
}): Promise<void>;
|
|
93
|
+
pullFrames(input: {
|
|
94
|
+
sessionId: string;
|
|
95
|
+
maxCount: number;
|
|
96
|
+
}): Promise<CapDecodedFrame[]>;
|
|
97
|
+
updateConfig(input: {
|
|
98
|
+
sessionId: string;
|
|
99
|
+
config: Partial<CapDecoderSessionConfig>;
|
|
100
|
+
}): Promise<void>;
|
|
101
|
+
getStats(input: {
|
|
102
|
+
sessionId: string;
|
|
103
|
+
}): Promise<DecoderStats>;
|
|
104
|
+
protected onShutdown(): Promise<void>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* NodeAvDecoderSession — IDecoderSession using node-av **push mode**.
|
|
109
|
+
*
|
|
110
|
+
* Receives raw H.264/H.265 Annex-B packets from the StreamBroker and decodes
|
|
111
|
+
* them in-process using the node-av low-level API:
|
|
112
|
+
*
|
|
113
|
+
* CodecParser → splits raw Annex-B bytes into proper AVPackets
|
|
114
|
+
* CodecContext → software decode (sendPacket / receiveFrame)
|
|
115
|
+
* SoftwareScaleContext → scale + convert to target format
|
|
116
|
+
*
|
|
117
|
+
* Output format depends on config.outputFormat:
|
|
118
|
+
* 'jpeg' → scale to RGB24, encode JPEG via sharp (for detection pipeline)
|
|
119
|
+
* 'gray' → scale to GRAY8 raw buffer (for motion detection only)
|
|
120
|
+
*
|
|
121
|
+
* No child process, no duplicate RTSP connection.
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
type HwAccelPref = 'auto' | 'none' | HwAccelBackend;
|
|
125
|
+
interface NodeAvDecoderSessionOptions {
|
|
126
|
+
/** Addon-level hwaccel preference — per-agent. Default `'auto'`. */
|
|
127
|
+
readonly hwaccel?: HwAccelPref;
|
|
128
|
+
/**
|
|
129
|
+
* Kernel hwaccel resolver — typically `ctx.kernel.hwaccel` passed
|
|
130
|
+
* from the addon. When present, `hwaccel: 'auto'` calls
|
|
131
|
+
* `resolver.resolve(null)` to get the platform-ordered backend
|
|
132
|
+
* list; when absent, `'auto'` degrades to software (the session
|
|
133
|
+
* cannot probe on its own because it lives in a package that must
|
|
134
|
+
* not depend on `@camstack/kernel`).
|
|
135
|
+
*/
|
|
136
|
+
readonly hwaccelResolver?: IKernelHwAccel;
|
|
137
|
+
}
|
|
138
|
+
declare class NodeAvDecoderSession implements IDecoderSession {
|
|
139
|
+
private config;
|
|
140
|
+
private readonly logger;
|
|
141
|
+
private frameCallbacks;
|
|
142
|
+
private destroyed;
|
|
143
|
+
private parser;
|
|
144
|
+
private codecCtx;
|
|
145
|
+
private scaler;
|
|
146
|
+
private avPacket;
|
|
147
|
+
private avFrame;
|
|
148
|
+
private dstFrame;
|
|
149
|
+
private EAGAIN;
|
|
150
|
+
private PIX_FMT_GRAY8;
|
|
151
|
+
private PIX_FMT_RGB24;
|
|
152
|
+
private SWS_FAST_BILINEAR;
|
|
153
|
+
/**
|
|
154
|
+
* Decoder output mode. Drives both the scaler's destination pixel
|
|
155
|
+
* format and whether sharp runs the JPEG encode at the end:
|
|
156
|
+
*
|
|
157
|
+
* - `'jpeg'` — scaler→RGB24 → sharp encode → emit JPEG bytes
|
|
158
|
+
* - `'rgb'` — scaler→RGB24 → emit raw RGB24 (no sharp)
|
|
159
|
+
* - `'gray'` — scaler→GRAY8 → emit raw GRAY8 (no sharp)
|
|
160
|
+
*
|
|
161
|
+
* The broker holds the policy decision on which mode to request based
|
|
162
|
+
* on its active subscribers; on-the-fly conversion (e.g. RGB→JPEG for
|
|
163
|
+
* a WebRTC consumer that joined while detection holds the decoder in
|
|
164
|
+
* RGB mode) happens broker-side via the per-frame conversion cache.
|
|
165
|
+
*/
|
|
166
|
+
private outputMode;
|
|
167
|
+
private sharpFn;
|
|
168
|
+
/**
|
|
169
|
+
* Backpressure for the sharp JPEG encode pipeline. The broker
|
|
170
|
+
* currently creates sessions with `maxFps: 0` (unlimited) and relies
|
|
171
|
+
* on per-subscriber throttling, so without a bound the
|
|
172
|
+
* fire-and-forget `sharp(...).toBuffer()` chain would accumulate
|
|
173
|
+
* unboundedly whenever sharp falls behind the decoder. Cap at
|
|
174
|
+
* `MAX_JPEG_INFLIGHT` pending encodes per session — any frame that
|
|
175
|
+
* arrives while the cap is saturated is dropped and counted.
|
|
176
|
+
*/
|
|
177
|
+
private static readonly MAX_JPEG_INFLIGHT;
|
|
178
|
+
private jpegEncodeInFlight;
|
|
179
|
+
/**
|
|
180
|
+
* Map a `DecoderSessionConfig.outputFormat` value to one of the three
|
|
181
|
+
* native scaler/encoder modes the session understands. The cap-level
|
|
182
|
+
* format vocabulary is broader (it accepts `bgr`, `yuv420`) than what
|
|
183
|
+
* libav's scaler is wired for here — anything else degrades to RGB
|
|
184
|
+
* (the canonical raw mode) and the broker is expected to convert
|
|
185
|
+
* downstream if a subscriber needs a different shape.
|
|
186
|
+
*/
|
|
187
|
+
private static resolveOutputMode;
|
|
188
|
+
private initialized;
|
|
189
|
+
private initializing;
|
|
190
|
+
private scalerInitializing;
|
|
191
|
+
/**
|
|
192
|
+
* Monotonic counter incremented by `updateConfig` whenever the
|
|
193
|
+
* scaler + dstFrame get invalidated (e.g. output format toggle).
|
|
194
|
+
* `initScaler` captures the current value at entry and aborts — or
|
|
195
|
+
* disposes the locally-built scaler — if the epoch moved while
|
|
196
|
+
* its async init was in flight. Without this, a toggle racing an
|
|
197
|
+
* in-flight init could leave two scalers allocated natively while
|
|
198
|
+
* `this.scaler` only holds a reference to one → libav leak.
|
|
199
|
+
*/
|
|
200
|
+
private scalerEpoch;
|
|
201
|
+
/**
|
|
202
|
+
* One-shot guard for the "first frame" diagnostic log + raw frame
|
|
203
|
+
* dump. Setting this synchronously inside `emitDecodedFrame`
|
|
204
|
+
* prevents re-entry — without it we were using `outputFrames === 0`
|
|
205
|
+
* which stays true until the async sharp encode callback runs, so
|
|
206
|
+
* several decoded frames could trigger the dump before the first
|
|
207
|
+
* JPEG landed.
|
|
208
|
+
*/
|
|
209
|
+
private firstFrameLogged;
|
|
210
|
+
private outWidth;
|
|
211
|
+
private outHeight;
|
|
212
|
+
private lastEmitTime;
|
|
213
|
+
private minIntervalMs;
|
|
214
|
+
private inputPackets;
|
|
215
|
+
private outputFrames;
|
|
216
|
+
private droppedFrames;
|
|
217
|
+
private totalDecodeTimeMs;
|
|
218
|
+
private startTime;
|
|
219
|
+
private readonly hwaccelPref;
|
|
220
|
+
private readonly hwaccelResolver;
|
|
221
|
+
/** The backend that actually initialised successfully — `'none'` = software fallback. */
|
|
222
|
+
private activeHwAccel;
|
|
223
|
+
private hwDevice;
|
|
224
|
+
private swTransferFrame;
|
|
225
|
+
constructor(config: DecoderSessionConfig, logger?: IScopedLogger, options?: NodeAvDecoderSessionOptions);
|
|
226
|
+
/**
|
|
227
|
+
* Resolve the backend preference list and try each one against
|
|
228
|
+
* node-av's HW context APIs. The first backend whose
|
|
229
|
+
* `HardwareDeviceContext.create()` succeeds gets attached to
|
|
230
|
+
* `codecCtx.hwDeviceCtx` + its hw pixel format registered via
|
|
231
|
+
* `setHardwarePixelFormat`. On any failure, falls through to the
|
|
232
|
+
* next backend; if all fail, returns with `activeHwAccel='none'`
|
|
233
|
+
* and the decoder runs in software on the same context.
|
|
234
|
+
*/
|
|
235
|
+
private tryAttachHwAccel;
|
|
236
|
+
/**
|
|
237
|
+
* Download a HW frame (format == hw pix fmt) into a SW frame so the
|
|
238
|
+
* rest of the pipeline (scaler, JPEG encoder, grayscale passthrough)
|
|
239
|
+
* handles it identically to the pure-software path. Uses the sync
|
|
240
|
+
* variant so the synchronous receive loop below doesn't need to be
|
|
241
|
+
* async-ified. Returns `null` on transfer failure, meaning the
|
|
242
|
+
* caller should drop the frame.
|
|
243
|
+
*/
|
|
244
|
+
private transferHwFrame;
|
|
245
|
+
/**
|
|
246
|
+
* Initialize the decoder pipeline on the first keyframe.
|
|
247
|
+
* After this returns, all hot-path methods are fully synchronous (except JPEG encode).
|
|
248
|
+
*/
|
|
249
|
+
private initDecoder;
|
|
250
|
+
/**
|
|
251
|
+
* Initialize the scaler after the first frame tells us the actual
|
|
252
|
+
* dimensions. Output pixel format: RGB24 for JPEG encoding, GRAY8
|
|
253
|
+
* for raw motion.
|
|
254
|
+
*
|
|
255
|
+
* Builds `scaler` + `dstFrame` on local variables and publishes
|
|
256
|
+
* them onto `this` in a single atomic step at the end. Captures
|
|
257
|
+
* `scalerEpoch` at entry; if `updateConfig` invalidated the scaler
|
|
258
|
+
* while this init was in flight (epoch mismatch), the locally
|
|
259
|
+
* built pair is disposed and discarded so the later init wins.
|
|
260
|
+
* Without the local-first approach, partial state (scaler set,
|
|
261
|
+
* dstFrame still null) could be observed by a concurrent
|
|
262
|
+
* `emitDecodedFrame` call.
|
|
263
|
+
*/
|
|
264
|
+
private initScaler;
|
|
265
|
+
pushPacket(packet: EncodedPacket): void;
|
|
266
|
+
private decodeRawData;
|
|
267
|
+
private decodePacket;
|
|
268
|
+
private emitDecodedFrame;
|
|
269
|
+
/**
|
|
270
|
+
* Extract packed pixel buffer from a decoded frame.
|
|
271
|
+
* FFmpeg's av_frame_get_buffer() may pad each row to alignment (32/64 bytes).
|
|
272
|
+
* Sharp and WASM consumers expect tightly-packed rows (stride = width * channels).
|
|
273
|
+
* If linesize matches expected stride, return the buffer directly (zero-copy).
|
|
274
|
+
*/
|
|
275
|
+
private extractPackedBuffer;
|
|
276
|
+
/**
|
|
277
|
+
* Encode RGB24 raw buffer as JPEG and emit.
|
|
278
|
+
*
|
|
279
|
+
* Drops the frame (and counts it) when `MAX_JPEG_INFLIGHT` encodes
|
|
280
|
+
* are already pending — prevents unbounded growth of the
|
|
281
|
+
* fire-and-forget promise chain when sharp cannot keep up with the
|
|
282
|
+
* decode rate.
|
|
283
|
+
*/
|
|
284
|
+
private encodeAndEmitJpeg;
|
|
285
|
+
private emitRawFrame;
|
|
286
|
+
onFrame(callback: (frame: DecodedFrame) => void): Unsubscribe;
|
|
287
|
+
updateConfig(update: Partial<DecoderSessionConfig>): void;
|
|
288
|
+
destroy(): Promise<void>;
|
|
289
|
+
getStats(): DecoderStats;
|
|
290
|
+
get isPullMode(): boolean;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export { DecoderNodeAvAddon, NodeAvDecoderSession };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import * as _camstack_types from '@camstack/types';
|
|
2
|
+
import { BaseAddon, DecoderHwAccelConfig, IDecoderCapProvider, ProviderRegistration, FrameFormat, DecoderStats, IDecoderSession, DecoderSessionConfig, IScopedLogger, HwAccelBackend, IKernelHwAccel, EncodedPacket, DecodedFrame, Unsubscribe } from '@camstack/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Phase 2d of the pipeline-settings migration — decoder addon now
|
|
6
|
+
* owns its own `hwaccel` choice + `probedBestHwaccel` hint. Sessions
|
|
7
|
+
* resolve the effective backend from this addon's global settings
|
|
8
|
+
* instead of the orchestrator's legacy `AgentPipelineSettings.hwaccel`.
|
|
9
|
+
* Shared config shape + UI options live in `@camstack/types`
|
|
10
|
+
* (`DecoderHwAccelConfig` / `HWACCEL_OPTIONS`) so every decoder addon
|
|
11
|
+
* speaks the same vocabulary.
|
|
12
|
+
*/
|
|
13
|
+
/** Cap-compatible frame shape — format must match DecodedFrameSchema enum values. */
|
|
14
|
+
type CapDecodedFrame = {
|
|
15
|
+
data: Uint8Array<ArrayBuffer>;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
format: FrameFormat;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
};
|
|
21
|
+
/** Cap-compatible session config — matches DecoderSessionConfigSchema output type. */
|
|
22
|
+
type CapDecoderSessionConfig = {
|
|
23
|
+
codec: string;
|
|
24
|
+
maxFps: number;
|
|
25
|
+
outputFormat: FrameFormat;
|
|
26
|
+
scale: number;
|
|
27
|
+
width?: number;
|
|
28
|
+
height?: number;
|
|
29
|
+
};
|
|
30
|
+
/** Cap-compatible encoded packet — data is Uint8Array matching EncodedPacketSchema. */
|
|
31
|
+
type CapEncodedPacket = {
|
|
32
|
+
type: 'video' | 'audio';
|
|
33
|
+
data: Uint8Array<ArrayBuffer>;
|
|
34
|
+
pts: number;
|
|
35
|
+
dts: number;
|
|
36
|
+
keyframe: boolean;
|
|
37
|
+
codec: string;
|
|
38
|
+
};
|
|
39
|
+
declare class DecoderNodeAvAddon extends BaseAddon<DecoderHwAccelConfig> implements IDecoderCapProvider {
|
|
40
|
+
private readonly sessions;
|
|
41
|
+
private readonly frameBuffers;
|
|
42
|
+
private readonly unsubscribers;
|
|
43
|
+
private readonly sessionMeta;
|
|
44
|
+
constructor();
|
|
45
|
+
protected globalSettingsSchema(): _camstack_types.ConfigUISchema;
|
|
46
|
+
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the effective hwaccel backend for a new session. Reads
|
|
49
|
+
* this addon's own `hwaccel` setting first. `'auto'` defers to the
|
|
50
|
+
* session's local resolver (`ctx.kernel.hwaccel`) which probes the
|
|
51
|
+
* host and picks. No more orchestrator round-trip — decoder addon
|
|
52
|
+
* is self-sufficient for this setting as of phase 2d.
|
|
53
|
+
*/
|
|
54
|
+
private resolveHwAccelPref;
|
|
55
|
+
/**
|
|
56
|
+
* Re-run the platform probe on this host and persist the detected
|
|
57
|
+
* backend as `probedBestHwaccel`. The operator's `hwaccel` setting
|
|
58
|
+
* is intentionally left alone — the probe only updates the hint.
|
|
59
|
+
*/
|
|
60
|
+
reprobeHwaccel(): Promise<{
|
|
61
|
+
backend: string;
|
|
62
|
+
}>;
|
|
63
|
+
supportsCodec(input: {
|
|
64
|
+
codec: string;
|
|
65
|
+
}): Promise<boolean>;
|
|
66
|
+
getInfo(): Promise<{
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
isPullMode?: boolean;
|
|
70
|
+
priority?: number;
|
|
71
|
+
}>;
|
|
72
|
+
createSession(config: CapDecoderSessionConfig): Promise<{
|
|
73
|
+
sessionId: string;
|
|
74
|
+
nodeId: string;
|
|
75
|
+
}>;
|
|
76
|
+
destroySession(input: {
|
|
77
|
+
sessionId: string;
|
|
78
|
+
}): Promise<void>;
|
|
79
|
+
listActiveSessions(): Promise<readonly {
|
|
80
|
+
sessionId: string;
|
|
81
|
+
codec: string;
|
|
82
|
+
outputFormat: string;
|
|
83
|
+
createdAtMs: number;
|
|
84
|
+
}[]>;
|
|
85
|
+
pushPacket(input: {
|
|
86
|
+
sessionId: string;
|
|
87
|
+
packet: CapEncodedPacket;
|
|
88
|
+
}): Promise<void>;
|
|
89
|
+
openStream(input: {
|
|
90
|
+
sessionId: string;
|
|
91
|
+
url: string;
|
|
92
|
+
}): Promise<void>;
|
|
93
|
+
pullFrames(input: {
|
|
94
|
+
sessionId: string;
|
|
95
|
+
maxCount: number;
|
|
96
|
+
}): Promise<CapDecodedFrame[]>;
|
|
97
|
+
updateConfig(input: {
|
|
98
|
+
sessionId: string;
|
|
99
|
+
config: Partial<CapDecoderSessionConfig>;
|
|
100
|
+
}): Promise<void>;
|
|
101
|
+
getStats(input: {
|
|
102
|
+
sessionId: string;
|
|
103
|
+
}): Promise<DecoderStats>;
|
|
104
|
+
protected onShutdown(): Promise<void>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* NodeAvDecoderSession — IDecoderSession using node-av **push mode**.
|
|
109
|
+
*
|
|
110
|
+
* Receives raw H.264/H.265 Annex-B packets from the StreamBroker and decodes
|
|
111
|
+
* them in-process using the node-av low-level API:
|
|
112
|
+
*
|
|
113
|
+
* CodecParser → splits raw Annex-B bytes into proper AVPackets
|
|
114
|
+
* CodecContext → software decode (sendPacket / receiveFrame)
|
|
115
|
+
* SoftwareScaleContext → scale + convert to target format
|
|
116
|
+
*
|
|
117
|
+
* Output format depends on config.outputFormat:
|
|
118
|
+
* 'jpeg' → scale to RGB24, encode JPEG via sharp (for detection pipeline)
|
|
119
|
+
* 'gray' → scale to GRAY8 raw buffer (for motion detection only)
|
|
120
|
+
*
|
|
121
|
+
* No child process, no duplicate RTSP connection.
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
type HwAccelPref = 'auto' | 'none' | HwAccelBackend;
|
|
125
|
+
interface NodeAvDecoderSessionOptions {
|
|
126
|
+
/** Addon-level hwaccel preference — per-agent. Default `'auto'`. */
|
|
127
|
+
readonly hwaccel?: HwAccelPref;
|
|
128
|
+
/**
|
|
129
|
+
* Kernel hwaccel resolver — typically `ctx.kernel.hwaccel` passed
|
|
130
|
+
* from the addon. When present, `hwaccel: 'auto'` calls
|
|
131
|
+
* `resolver.resolve(null)` to get the platform-ordered backend
|
|
132
|
+
* list; when absent, `'auto'` degrades to software (the session
|
|
133
|
+
* cannot probe on its own because it lives in a package that must
|
|
134
|
+
* not depend on `@camstack/kernel`).
|
|
135
|
+
*/
|
|
136
|
+
readonly hwaccelResolver?: IKernelHwAccel;
|
|
137
|
+
}
|
|
138
|
+
declare class NodeAvDecoderSession implements IDecoderSession {
|
|
139
|
+
private config;
|
|
140
|
+
private readonly logger;
|
|
141
|
+
private frameCallbacks;
|
|
142
|
+
private destroyed;
|
|
143
|
+
private parser;
|
|
144
|
+
private codecCtx;
|
|
145
|
+
private scaler;
|
|
146
|
+
private avPacket;
|
|
147
|
+
private avFrame;
|
|
148
|
+
private dstFrame;
|
|
149
|
+
private EAGAIN;
|
|
150
|
+
private PIX_FMT_GRAY8;
|
|
151
|
+
private PIX_FMT_RGB24;
|
|
152
|
+
private SWS_FAST_BILINEAR;
|
|
153
|
+
/**
|
|
154
|
+
* Decoder output mode. Drives both the scaler's destination pixel
|
|
155
|
+
* format and whether sharp runs the JPEG encode at the end:
|
|
156
|
+
*
|
|
157
|
+
* - `'jpeg'` — scaler→RGB24 → sharp encode → emit JPEG bytes
|
|
158
|
+
* - `'rgb'` — scaler→RGB24 → emit raw RGB24 (no sharp)
|
|
159
|
+
* - `'gray'` — scaler→GRAY8 → emit raw GRAY8 (no sharp)
|
|
160
|
+
*
|
|
161
|
+
* The broker holds the policy decision on which mode to request based
|
|
162
|
+
* on its active subscribers; on-the-fly conversion (e.g. RGB→JPEG for
|
|
163
|
+
* a WebRTC consumer that joined while detection holds the decoder in
|
|
164
|
+
* RGB mode) happens broker-side via the per-frame conversion cache.
|
|
165
|
+
*/
|
|
166
|
+
private outputMode;
|
|
167
|
+
private sharpFn;
|
|
168
|
+
/**
|
|
169
|
+
* Backpressure for the sharp JPEG encode pipeline. The broker
|
|
170
|
+
* currently creates sessions with `maxFps: 0` (unlimited) and relies
|
|
171
|
+
* on per-subscriber throttling, so without a bound the
|
|
172
|
+
* fire-and-forget `sharp(...).toBuffer()` chain would accumulate
|
|
173
|
+
* unboundedly whenever sharp falls behind the decoder. Cap at
|
|
174
|
+
* `MAX_JPEG_INFLIGHT` pending encodes per session — any frame that
|
|
175
|
+
* arrives while the cap is saturated is dropped and counted.
|
|
176
|
+
*/
|
|
177
|
+
private static readonly MAX_JPEG_INFLIGHT;
|
|
178
|
+
private jpegEncodeInFlight;
|
|
179
|
+
/**
|
|
180
|
+
* Map a `DecoderSessionConfig.outputFormat` value to one of the three
|
|
181
|
+
* native scaler/encoder modes the session understands. The cap-level
|
|
182
|
+
* format vocabulary is broader (it accepts `bgr`, `yuv420`) than what
|
|
183
|
+
* libav's scaler is wired for here — anything else degrades to RGB
|
|
184
|
+
* (the canonical raw mode) and the broker is expected to convert
|
|
185
|
+
* downstream if a subscriber needs a different shape.
|
|
186
|
+
*/
|
|
187
|
+
private static resolveOutputMode;
|
|
188
|
+
private initialized;
|
|
189
|
+
private initializing;
|
|
190
|
+
private scalerInitializing;
|
|
191
|
+
/**
|
|
192
|
+
* Monotonic counter incremented by `updateConfig` whenever the
|
|
193
|
+
* scaler + dstFrame get invalidated (e.g. output format toggle).
|
|
194
|
+
* `initScaler` captures the current value at entry and aborts — or
|
|
195
|
+
* disposes the locally-built scaler — if the epoch moved while
|
|
196
|
+
* its async init was in flight. Without this, a toggle racing an
|
|
197
|
+
* in-flight init could leave two scalers allocated natively while
|
|
198
|
+
* `this.scaler` only holds a reference to one → libav leak.
|
|
199
|
+
*/
|
|
200
|
+
private scalerEpoch;
|
|
201
|
+
/**
|
|
202
|
+
* One-shot guard for the "first frame" diagnostic log + raw frame
|
|
203
|
+
* dump. Setting this synchronously inside `emitDecodedFrame`
|
|
204
|
+
* prevents re-entry — without it we were using `outputFrames === 0`
|
|
205
|
+
* which stays true until the async sharp encode callback runs, so
|
|
206
|
+
* several decoded frames could trigger the dump before the first
|
|
207
|
+
* JPEG landed.
|
|
208
|
+
*/
|
|
209
|
+
private firstFrameLogged;
|
|
210
|
+
private outWidth;
|
|
211
|
+
private outHeight;
|
|
212
|
+
private lastEmitTime;
|
|
213
|
+
private minIntervalMs;
|
|
214
|
+
private inputPackets;
|
|
215
|
+
private outputFrames;
|
|
216
|
+
private droppedFrames;
|
|
217
|
+
private totalDecodeTimeMs;
|
|
218
|
+
private startTime;
|
|
219
|
+
private readonly hwaccelPref;
|
|
220
|
+
private readonly hwaccelResolver;
|
|
221
|
+
/** The backend that actually initialised successfully — `'none'` = software fallback. */
|
|
222
|
+
private activeHwAccel;
|
|
223
|
+
private hwDevice;
|
|
224
|
+
private swTransferFrame;
|
|
225
|
+
constructor(config: DecoderSessionConfig, logger?: IScopedLogger, options?: NodeAvDecoderSessionOptions);
|
|
226
|
+
/**
|
|
227
|
+
* Resolve the backend preference list and try each one against
|
|
228
|
+
* node-av's HW context APIs. The first backend whose
|
|
229
|
+
* `HardwareDeviceContext.create()` succeeds gets attached to
|
|
230
|
+
* `codecCtx.hwDeviceCtx` + its hw pixel format registered via
|
|
231
|
+
* `setHardwarePixelFormat`. On any failure, falls through to the
|
|
232
|
+
* next backend; if all fail, returns with `activeHwAccel='none'`
|
|
233
|
+
* and the decoder runs in software on the same context.
|
|
234
|
+
*/
|
|
235
|
+
private tryAttachHwAccel;
|
|
236
|
+
/**
|
|
237
|
+
* Download a HW frame (format == hw pix fmt) into a SW frame so the
|
|
238
|
+
* rest of the pipeline (scaler, JPEG encoder, grayscale passthrough)
|
|
239
|
+
* handles it identically to the pure-software path. Uses the sync
|
|
240
|
+
* variant so the synchronous receive loop below doesn't need to be
|
|
241
|
+
* async-ified. Returns `null` on transfer failure, meaning the
|
|
242
|
+
* caller should drop the frame.
|
|
243
|
+
*/
|
|
244
|
+
private transferHwFrame;
|
|
245
|
+
/**
|
|
246
|
+
* Initialize the decoder pipeline on the first keyframe.
|
|
247
|
+
* After this returns, all hot-path methods are fully synchronous (except JPEG encode).
|
|
248
|
+
*/
|
|
249
|
+
private initDecoder;
|
|
250
|
+
/**
|
|
251
|
+
* Initialize the scaler after the first frame tells us the actual
|
|
252
|
+
* dimensions. Output pixel format: RGB24 for JPEG encoding, GRAY8
|
|
253
|
+
* for raw motion.
|
|
254
|
+
*
|
|
255
|
+
* Builds `scaler` + `dstFrame` on local variables and publishes
|
|
256
|
+
* them onto `this` in a single atomic step at the end. Captures
|
|
257
|
+
* `scalerEpoch` at entry; if `updateConfig` invalidated the scaler
|
|
258
|
+
* while this init was in flight (epoch mismatch), the locally
|
|
259
|
+
* built pair is disposed and discarded so the later init wins.
|
|
260
|
+
* Without the local-first approach, partial state (scaler set,
|
|
261
|
+
* dstFrame still null) could be observed by a concurrent
|
|
262
|
+
* `emitDecodedFrame` call.
|
|
263
|
+
*/
|
|
264
|
+
private initScaler;
|
|
265
|
+
pushPacket(packet: EncodedPacket): void;
|
|
266
|
+
private decodeRawData;
|
|
267
|
+
private decodePacket;
|
|
268
|
+
private emitDecodedFrame;
|
|
269
|
+
/**
|
|
270
|
+
* Extract packed pixel buffer from a decoded frame.
|
|
271
|
+
* FFmpeg's av_frame_get_buffer() may pad each row to alignment (32/64 bytes).
|
|
272
|
+
* Sharp and WASM consumers expect tightly-packed rows (stride = width * channels).
|
|
273
|
+
* If linesize matches expected stride, return the buffer directly (zero-copy).
|
|
274
|
+
*/
|
|
275
|
+
private extractPackedBuffer;
|
|
276
|
+
/**
|
|
277
|
+
* Encode RGB24 raw buffer as JPEG and emit.
|
|
278
|
+
*
|
|
279
|
+
* Drops the frame (and counts it) when `MAX_JPEG_INFLIGHT` encodes
|
|
280
|
+
* are already pending — prevents unbounded growth of the
|
|
281
|
+
* fire-and-forget promise chain when sharp cannot keep up with the
|
|
282
|
+
* decode rate.
|
|
283
|
+
*/
|
|
284
|
+
private encodeAndEmitJpeg;
|
|
285
|
+
private emitRawFrame;
|
|
286
|
+
onFrame(callback: (frame: DecodedFrame) => void): Unsubscribe;
|
|
287
|
+
updateConfig(update: Partial<DecoderSessionConfig>): void;
|
|
288
|
+
destroy(): Promise<void>;
|
|
289
|
+
getStats(): DecoderStats;
|
|
290
|
+
get isPullMode(): boolean;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export { DecoderNodeAvAddon, NodeAvDecoderSession };
|