@camstack/addon-decoder-ffmpeg 0.1.1 → 0.1.3
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 +33 -75
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +20 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/index.d.mts +0 -195
- package/dist/index.d.ts +0 -195
package/dist/index.js
CHANGED
|
@@ -1,42 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/index.ts
|
|
21
|
-
var src_exports = {};
|
|
22
|
-
__export(src_exports, {
|
|
23
|
-
DecoderFfmpegAddon: () => DecoderFfmpegAddon,
|
|
24
|
-
FfmpegDecoderProvider: () => FfmpegDecoderProvider,
|
|
25
|
-
FfmpegDecoderSession: () => FfmpegDecoderSession,
|
|
26
|
-
FrameDropper: () => FrameDropper
|
|
27
|
-
});
|
|
28
|
-
module.exports = __toCommonJS(src_exports);
|
|
29
|
-
|
|
30
|
-
// src/addon/index.ts
|
|
31
|
-
var import_node_crypto = require("crypto");
|
|
32
|
-
var import_types2 = require("@camstack/types");
|
|
33
|
-
|
|
34
|
-
// src/ffmpeg-decoder-session.ts
|
|
35
|
-
var import_node_child_process = require("child_process");
|
|
36
|
-
var import_types = require("@camstack/types");
|
|
37
|
-
|
|
38
|
-
// src/frame-dropper.ts
|
|
39
|
-
var FrameDropper = class {
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const node_crypto = require("node:crypto");
|
|
4
|
+
const types = require("@camstack/types");
|
|
5
|
+
const node_child_process = require("node:child_process");
|
|
6
|
+
class FrameDropper {
|
|
40
7
|
intervalMs;
|
|
41
8
|
lastPassedAt = -Infinity;
|
|
42
9
|
constructor(maxFps) {
|
|
@@ -54,10 +21,8 @@ var FrameDropper = class {
|
|
|
54
21
|
setMaxFps(maxFps) {
|
|
55
22
|
this.intervalMs = maxFps > 0 ? 1e3 / maxFps : 0;
|
|
56
23
|
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// src/ffmpeg-decoder-session.ts
|
|
60
|
-
var noopLogger = {
|
|
24
|
+
}
|
|
25
|
+
const noopLogger = {
|
|
61
26
|
debug() {
|
|
62
27
|
},
|
|
63
28
|
info() {
|
|
@@ -73,9 +38,9 @@ var noopLogger = {
|
|
|
73
38
|
return noopLogger;
|
|
74
39
|
}
|
|
75
40
|
};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
41
|
+
const SOI = Buffer.from([255, 216]);
|
|
42
|
+
const EOI = Buffer.from([255, 217]);
|
|
43
|
+
class FfmpegDecoderSession {
|
|
79
44
|
config;
|
|
80
45
|
frameDropper;
|
|
81
46
|
process = null;
|
|
@@ -169,8 +134,8 @@ var FfmpegDecoderSession = class {
|
|
|
169
134
|
args.push("-vf", `scale=iw/${this.config.scale}:ih/${this.config.scale}`);
|
|
170
135
|
}
|
|
171
136
|
args.push("-f", "image2pipe", "-vcodec", "mjpeg", "-q:v", "3", "-threads", "1", "pipe:1");
|
|
172
|
-
this.logger.info("Opening RTSP stream directly", { meta: { url:
|
|
173
|
-
this.process =
|
|
137
|
+
this.logger.info("Opening RTSP stream directly", { meta: { url: types.maskUrlCredentials(url), hwAccel: this.activeHwAccel } });
|
|
138
|
+
this.process = node_child_process.spawn("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
174
139
|
this.process.stdin?.on("error", () => {
|
|
175
140
|
});
|
|
176
141
|
this.process.stdout?.on("data", (chunk) => {
|
|
@@ -220,7 +185,7 @@ var FfmpegDecoderSession = class {
|
|
|
220
185
|
this.logger.info("Spawning push-mode ffmpeg decoder", {
|
|
221
186
|
meta: { codec: this.config.codec, hwAccel: this.activeHwAccel }
|
|
222
187
|
});
|
|
223
|
-
this.process =
|
|
188
|
+
this.process = node_child_process.spawn("ffmpeg", args);
|
|
224
189
|
this.process.stdin?.on("error", () => {
|
|
225
190
|
});
|
|
226
191
|
this.process.stdout?.on("data", (chunk) => {
|
|
@@ -330,7 +295,7 @@ var FfmpegDecoderSession = class {
|
|
|
330
295
|
droppedFrames: this.droppedFrames
|
|
331
296
|
};
|
|
332
297
|
}
|
|
333
|
-
}
|
|
298
|
+
}
|
|
334
299
|
function parseJpegDimensions(data) {
|
|
335
300
|
for (let i = 0; i < data.length - 8; i++) {
|
|
336
301
|
if (data[i] === 255 && (data[i + 1] === 192 || data[i + 1] === 194)) {
|
|
@@ -341,29 +306,27 @@ function parseJpegDimensions(data) {
|
|
|
341
306
|
}
|
|
342
307
|
return { width: 0, height: 0 };
|
|
343
308
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
var FRAME_BUFFER_CAPACITY = 32;
|
|
347
|
-
var DecoderFfmpegAddon = class extends import_types2.BaseAddon {
|
|
309
|
+
const FRAME_BUFFER_CAPACITY = 32;
|
|
310
|
+
class DecoderFfmpegAddon extends types.BaseAddon {
|
|
348
311
|
sessions = /* @__PURE__ */ new Map();
|
|
349
312
|
frameBuffers = /* @__PURE__ */ new Map();
|
|
350
313
|
unsubscribers = /* @__PURE__ */ new Map();
|
|
351
314
|
sessionMeta = /* @__PURE__ */ new Map();
|
|
352
315
|
constructor() {
|
|
353
|
-
super(
|
|
316
|
+
super(types.DEFAULT_DECODER_HWACCEL_CONFIG);
|
|
354
317
|
}
|
|
355
318
|
globalSettingsSchema() {
|
|
356
319
|
return this.schema({
|
|
357
320
|
sections: [{
|
|
358
321
|
id: "hwaccel",
|
|
359
322
|
title: "Hardware acceleration",
|
|
360
|
-
description: 'Backend appended as `-hwaccel <name>` to the ffmpeg argv. "Auto" defers to the probed best; concrete backends force it. Changes apply to NEW sessions
|
|
323
|
+
description: 'Backend appended as `-hwaccel <name>` to the ffmpeg argv. "Auto" defers to the probed best; concrete backends force it. Changes apply to NEW sessions — existing sessions keep the backend they were created with.',
|
|
361
324
|
fields: [
|
|
362
325
|
this.field({
|
|
363
326
|
type: "select",
|
|
364
327
|
key: "hwaccel",
|
|
365
328
|
label: "Preferred backend",
|
|
366
|
-
options: [...
|
|
329
|
+
options: [...types.HWACCEL_OPTIONS],
|
|
367
330
|
default: "auto",
|
|
368
331
|
immediate: true
|
|
369
332
|
}),
|
|
@@ -388,7 +351,7 @@ var DecoderFfmpegAddon = class extends import_types2.BaseAddon {
|
|
|
388
351
|
});
|
|
389
352
|
});
|
|
390
353
|
}
|
|
391
|
-
return [{ capability:
|
|
354
|
+
return [{ capability: types.decoderCapability, provider: this }];
|
|
392
355
|
}
|
|
393
356
|
/**
|
|
394
357
|
* Resolve the effective hwaccel backend for a new session. Reads
|
|
@@ -406,7 +369,7 @@ var DecoderFfmpegAddon = class extends import_types2.BaseAddon {
|
|
|
406
369
|
async reprobeHwaccel() {
|
|
407
370
|
const resolver = this.ctx.kernel.hwaccel;
|
|
408
371
|
if (!resolver) {
|
|
409
|
-
this.ctx.logger.warn("reprobeHwaccel: no kernel hwaccel resolver
|
|
372
|
+
this.ctx.logger.warn("reprobeHwaccel: no kernel hwaccel resolver — returning none");
|
|
410
373
|
await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: "none" });
|
|
411
374
|
return { backend: "none" };
|
|
412
375
|
}
|
|
@@ -438,13 +401,13 @@ var DecoderFfmpegAddon = class extends import_types2.BaseAddon {
|
|
|
438
401
|
};
|
|
439
402
|
}
|
|
440
403
|
async createSession(config) {
|
|
441
|
-
const sessionId =
|
|
404
|
+
const sessionId = node_crypto.randomUUID();
|
|
442
405
|
const hwaccel = this.resolveHwAccelPref();
|
|
443
406
|
const session = new FfmpegDecoderSession(config, this.ctx.logger, {
|
|
444
407
|
hwaccel,
|
|
445
408
|
hwaccelResolver: this.ctx.kernel.hwaccel
|
|
446
409
|
});
|
|
447
|
-
const ringBuffer = new
|
|
410
|
+
const ringBuffer = new types.RingBuffer(FRAME_BUFFER_CAPACITY);
|
|
448
411
|
const unsub = session.onFrame((frame) => {
|
|
449
412
|
const { format } = frame;
|
|
450
413
|
if (format !== "jpeg" && format !== "rgb" && format !== "bgr" && format !== "yuv420" && format !== "gray") return;
|
|
@@ -533,7 +496,7 @@ var DecoderFfmpegAddon = class extends import_types2.BaseAddon {
|
|
|
533
496
|
return session.getStats();
|
|
534
497
|
}
|
|
535
498
|
async onShutdown() {
|
|
536
|
-
this.ctx.logger.info("FFmpeg decoder addon shutdown
|
|
499
|
+
this.ctx.logger.info("FFmpeg decoder addon shutdown — destroying all sessions");
|
|
537
500
|
const destroyPromises = [];
|
|
538
501
|
for (const [sessionId, session] of this.sessions) {
|
|
539
502
|
const unsub = this.unsubscribers.get(sessionId);
|
|
@@ -546,11 +509,9 @@ var DecoderFfmpegAddon = class extends import_types2.BaseAddon {
|
|
|
546
509
|
this.sessionMeta.clear();
|
|
547
510
|
this.unsubscribers.clear();
|
|
548
511
|
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
var SUPPORTED_CODECS = /* @__PURE__ */ new Set(["h264", "h265", "hevc", "mjpeg"]);
|
|
553
|
-
var FfmpegDecoderProvider = class {
|
|
512
|
+
}
|
|
513
|
+
const SUPPORTED_CODECS = /* @__PURE__ */ new Set(["h264", "h265", "hevc", "mjpeg"]);
|
|
514
|
+
class FfmpegDecoderProvider {
|
|
554
515
|
id = "ffmpeg";
|
|
555
516
|
name = "FFmpeg Decoder";
|
|
556
517
|
isPullMode = false;
|
|
@@ -566,12 +527,9 @@ var FfmpegDecoderProvider = class {
|
|
|
566
527
|
async createSession(config) {
|
|
567
528
|
return new FfmpegDecoderSession(config, this.logger ?? void 0);
|
|
568
529
|
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
FrameDropper
|
|
576
|
-
});
|
|
577
|
-
//# sourceMappingURL=index.js.map
|
|
530
|
+
}
|
|
531
|
+
exports.DecoderFfmpegAddon = DecoderFfmpegAddon;
|
|
532
|
+
exports.FfmpegDecoderProvider = FfmpegDecoderProvider;
|
|
533
|
+
exports.FfmpegDecoderSession = FfmpegDecoderSession;
|
|
534
|
+
exports.FrameDropper = FrameDropper;
|
|
535
|
+
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/addon/index.ts","../src/ffmpeg-decoder-session.ts","../src/frame-dropper.ts","../src/ffmpeg-decoder-provider.ts"],"sourcesContent":["export { default as DecoderFfmpegAddon } from './addon/index.js'\nexport { FfmpegDecoderProvider } from './ffmpeg-decoder-provider.js'\nexport { FfmpegDecoderSession } from './ffmpeg-decoder-session.js'\nexport { FrameDropper } from './frame-dropper.js'\n","import { randomUUID } from 'node:crypto'\nimport type {\n DecoderHwAccelConfig,\n HwAccelChoice,\n ProviderRegistration,\n IDecoderSession,\n DecoderStats,\n IDecoderCapProvider,\n FrameFormat,\n} from '@camstack/types'\nimport {\n BaseAddon,\n DEFAULT_DECODER_HWACCEL_CONFIG,\n HWACCEL_OPTIONS,\n decoderCapability,\n RingBuffer,\n} from '@camstack/types'\nimport { FfmpegDecoderSession } from '../ffmpeg-decoder-session.js'\n\n/** Cap-compatible frame shape — format must match DecodedFrameSchema enum values. */\ntype CapDecodedFrame = {\n data: Uint8Array<ArrayBuffer>\n width: number\n height: number\n format: FrameFormat\n timestamp: number\n}\n\n/** Cap-compatible session config — matches DecoderSessionConfigSchema output type. */\ntype CapDecoderSessionConfig = {\n codec: string\n maxFps: number\n outputFormat: FrameFormat\n scale: number\n width?: number\n height?: number\n}\n\n/** Cap-compatible encoded packet — data is Uint8Array matching EncodedPacketSchema. */\ntype CapEncodedPacket = {\n type: 'video' | 'audio'\n data: Uint8Array<ArrayBuffer>\n pts: number\n dts: number\n keyframe: boolean\n codec: string\n}\n\nconst FRAME_BUFFER_CAPACITY = 32\n\n/** Per-session metadata recorded at creation time, surfaced via `listActiveSessions`. */\ninterface SessionMeta {\n readonly codec: string\n readonly outputFormat: string\n readonly createdAtMs: number\n}\n\n/**\n * FFmpeg decoder addon — H264/HEVC/MJPEG decode via ffmpeg child\n * process.\n *\n * Phase 2d of the pipeline-settings migration — ffmpeg decoder owns\n * its own `hwaccel` choice + `probedBestHwaccel` hint. Sessions\n * resolve the effective backend from this addon's global settings\n * instead of the orchestrator's legacy `AgentPipelineSettings.hwaccel`.\n * Session constructor still appends `-hwaccel <name>` to the ffmpeg\n * argv as before.\n *\n * Implements the sessionId-based IDecoderCapProvider cap interface.\n * Sessions are managed internally via a Map; frames are polled via\n * RingBuffer.\n */\nexport default class DecoderFfmpegAddon extends BaseAddon<DecoderHwAccelConfig> implements IDecoderCapProvider {\n private readonly sessions = new Map<string, IDecoderSession>()\n private readonly frameBuffers = new Map<string, RingBuffer<CapDecodedFrame>>()\n private readonly unsubscribers = new Map<string, () => void>()\n private readonly sessionMeta = new Map<string, SessionMeta>()\n\n constructor() { super(DEFAULT_DECODER_HWACCEL_CONFIG) }\n\n protected globalSettingsSchema() {\n return this.schema({\n sections: [{\n id: 'hwaccel',\n title: 'Hardware acceleration',\n description: 'Backend appended as `-hwaccel <name>` to the ffmpeg argv. \"Auto\" defers to the probed best; concrete backends force it. Changes apply to NEW sessions — existing sessions keep the backend they were created with.',\n fields: [\n this.field({\n type: 'select',\n key: 'hwaccel',\n label: 'Preferred backend',\n options: [...HWACCEL_OPTIONS],\n default: 'auto',\n immediate: true,\n }),\n this.field({\n type: 'text',\n key: 'probedBestHwaccel',\n label: 'Probed best',\n description: 'Auto-detected best backend on this host. Call `reprobeHwaccel` to refresh.',\n disabled: true,\n default: '',\n }),\n ],\n }],\n })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.ctx.logger.info('FFmpeg decoder addon initialized')\n // Auto-seed probedBestHwaccel on first boot.\n if (!this.config.probedBestHwaccel) {\n this.reprobeHwaccel().catch((err: unknown) => {\n this.ctx.logger.warn('ffmpeg: auto-reprobe hwaccel failed', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n })\n }\n return [{ capability: decoderCapability, provider: this }]\n }\n\n /**\n * Resolve the effective hwaccel backend for a new session. Reads\n * this addon's own `hwaccel` setting. `'auto'` defers to the\n * session's local resolver (`ctx.kernel.hwaccel`).\n */\n private resolveHwAccelPref(): HwAccelChoice {\n return this.config.hwaccel\n }\n\n /**\n * Re-run the platform probe on this host and persist the detected\n * backend as `probedBestHwaccel`. Operator `hwaccel` setting is not\n * touched — only the hint.\n */\n async reprobeHwaccel(): Promise<{ backend: string }> {\n const resolver = this.ctx.kernel.hwaccel\n if (!resolver) {\n this.ctx.logger.warn('reprobeHwaccel: no kernel hwaccel resolver — returning none')\n await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: 'none' })\n return { backend: 'none' }\n }\n try {\n const res = await resolver.resolve()\n const backend = (res.preferred[0] ?? 'none') as string\n await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: backend })\n this.ctx.logger.info('reprobeHwaccel: wrote probedBestHwaccel', {\n meta: { backend, rationale: res.rationale, preferred: res.preferred },\n })\n return { backend }\n } catch (err) {\n this.ctx.logger.warn('reprobeHwaccel failed', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: 'none' })\n return { backend: 'none' }\n }\n }\n\n async supportsCodec(input: { codec: string }): Promise<boolean> {\n return ['h264', 'h265', 'hevc', 'mjpeg'].includes(input.codec.toLowerCase())\n }\n\n async getInfo(): Promise<{ id: string; name: string; isPullMode?: boolean; priority?: number }> {\n return {\n id: 'decoder-ffmpeg',\n name: 'Decoder (FFmpeg)',\n isPullMode: false,\n priority: 50,\n }\n }\n\n async createSession(config: CapDecoderSessionConfig): Promise<{ sessionId: string; nodeId: string }> {\n const sessionId = randomUUID()\n const hwaccel = this.resolveHwAccelPref()\n const session = new FfmpegDecoderSession(config, this.ctx.logger, {\n hwaccel,\n hwaccelResolver: this.ctx.kernel.hwaccel,\n })\n const ringBuffer = new RingBuffer<CapDecodedFrame>(FRAME_BUFFER_CAPACITY)\n\n const unsub = session.onFrame((frame) => {\n // Map internal DecodedFrame to cap-compatible shape.\n const { format } = frame\n if (format !== 'jpeg' && format !== 'rgb' && format !== 'bgr' && format !== 'yuv420' && format !== 'gray') return\n // Copy frame data into a fresh ArrayBuffer so the cap-facing Uint8Array\n // has a concrete ArrayBuffer (not a SharedArrayBuffer / Buffer backing).\n const arrayBuf = new ArrayBuffer(frame.data.byteLength)\n new Uint8Array(arrayBuf).set(frame.data)\n const capFrame: CapDecodedFrame = {\n data: new Uint8Array(arrayBuf),\n width: frame.width,\n height: frame.height,\n format,\n timestamp: frame.timestamp,\n }\n ringBuffer.push(capFrame)\n })\n\n this.sessions.set(sessionId, session)\n this.frameBuffers.set(sessionId, ringBuffer)\n this.unsubscribers.set(sessionId, unsub)\n this.sessionMeta.set(sessionId, {\n codec: config.codec,\n outputFormat: config.outputFormat,\n createdAtMs: Date.now(),\n })\n\n this.ctx.logger.info('ffmpeg: created session', { meta: { sessionId, codec: config.codec, hwaccelPref: hwaccel } })\n return { sessionId, nodeId: this.ctx.kernel.localNodeId ?? 'local' }\n }\n\n async destroySession(input: { sessionId: string }): Promise<void> {\n const { sessionId } = input\n const session = this.sessions.get(sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${sessionId}`)\n }\n\n const unsub = this.unsubscribers.get(sessionId)\n if (unsub) unsub()\n\n await session.destroy()\n\n this.sessions.delete(sessionId)\n this.frameBuffers.delete(sessionId)\n this.unsubscribers.delete(sessionId)\n this.sessionMeta.delete(sessionId)\n\n this.ctx.logger.info('ffmpeg: destroyed session', { meta: { sessionId } })\n }\n\n async listActiveSessions(): Promise<readonly { sessionId: string; codec: string; outputFormat: string; createdAtMs: number }[]> {\n const out: Array<{ sessionId: string; codec: string; outputFormat: string; createdAtMs: number }> = []\n for (const [sessionId, meta] of this.sessionMeta) {\n out.push({ sessionId, codec: meta.codec, outputFormat: meta.outputFormat, createdAtMs: meta.createdAtMs })\n }\n return out\n }\n\n async pushPacket(input: { sessionId: string; packet: CapEncodedPacket }): Promise<void> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n // Convert Uint8Array to Buffer at the cap boundary before passing to the internal session.\n session.pushPacket({\n ...input.packet,\n data: Buffer.from(input.packet.data),\n })\n }\n\n async openStream(input: { sessionId: string; url: string }): Promise<void> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n if (session.openStream) {\n await session.openStream(input.url)\n }\n }\n\n async pullFrames(input: { sessionId: string; maxCount: number }): Promise<CapDecodedFrame[]> {\n const ringBuffer = this.frameBuffers.get(input.sessionId)\n if (!ringBuffer) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n return ringBuffer.drain(input.maxCount)\n }\n\n async updateConfig(input: { sessionId: string; config: Partial<CapDecoderSessionConfig> }): Promise<void> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n session.updateConfig(input.config)\n }\n\n async getStats(input: { sessionId: string }): Promise<DecoderStats> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n return session.getStats()\n }\n\n protected async onShutdown(): Promise<void> {\n this.ctx.logger.info('FFmpeg decoder addon shutdown — destroying all sessions')\n const destroyPromises: Promise<void>[] = []\n for (const [sessionId, session] of this.sessions) {\n const unsub = this.unsubscribers.get(sessionId)\n if (unsub) unsub()\n destroyPromises.push(session.destroy())\n }\n await Promise.all(destroyPromises)\n this.sessions.clear()\n this.frameBuffers.clear()\n this.sessionMeta.clear()\n this.unsubscribers.clear()\n }\n}\n","import { spawn, type ChildProcess } from 'node:child_process'\nimport type {\n IDecoderSession, DecoderSessionConfig, DecoderStats, EncodedPacket,\n DecodedFrame, Unsubscribe, IScopedLogger,\n HwAccelBackend, IKernelHwAccel,\n} from '@camstack/types'\nimport { maskUrlCredentials } from '@camstack/types'\nimport { FrameDropper } from './frame-dropper'\n\nexport type HwAccelPref = 'auto' | 'none' | HwAccelBackend\n\nexport interface FfmpegDecoderSessionOptions {\n /** Addon-level hwaccel preference — per-agent. Default `'auto'`. */\n readonly hwaccel?: HwAccelPref\n /** Kernel hwaccel resolver — `ctx.kernel.hwaccel` passed from the addon. */\n readonly hwaccelResolver?: IKernelHwAccel\n}\n\n/** Minimal no-op logger for default parameter */\nconst noopLogger: IScopedLogger = {\n debug() {},\n info() {},\n warn() {},\n error() {},\n child() { return noopLogger },\n withTags() { return noopLogger },\n}\n\nconst SOI = Buffer.from([0xff, 0xd8])\nconst EOI = Buffer.from([0xff, 0xd9])\n\nexport class FfmpegDecoderSession implements IDecoderSession {\n private config: DecoderSessionConfig\n private frameDropper: FrameDropper\n private process: ChildProcess | null = null\n private frameCallbacks = new Set<(frame: DecodedFrame) => void>()\n private outputBuffer = Buffer.alloc(0)\n private destroyed = false\n private readonly logger: IScopedLogger\n\n /** When openStream() is used, we read from RTSP directly (not push mode) */\n private pullMode = false\n\n // Cached dimensions — won't change between frames from the same FFmpeg session\n private cachedWidth = 0\n private cachedHeight = 0\n\n // Stats tracking\n private inputPackets = 0\n private outputFrames = 0\n private droppedFrames = 0\n private totalDecodeTimeMs = 0\n private decodeCount = 0\n private startTime = Date.now()\n\n private readonly hwaccelPref: HwAccelPref\n private readonly hwaccelResolver: IKernelHwAccel | null\n /** The backend we actually passed to ffmpeg `-hwaccel` — `'none'` = software. */\n private activeHwAccel: 'none' | HwAccelBackend = 'none'\n\n /**\n * Backend resolution is async (calls into `ctx.kernel.hwaccel`), but\n * `pushPacket` is sync — we kick the resolve off in the constructor\n * and cache the result. By the time the first keyframe arrives\n * (~30ms for RTSP), the resolver has completed (it's ultimately\n * `os.platform()` + file checks → sub-ms). If `ensurePushProcess`\n * fires before resolve settles, it skips hwaccel for that spawn;\n * reconnect loop gets the flag on subsequent sessions.\n */\n private resolvedBackend: HwAccelBackend | null = null\n\n constructor(\n config: DecoderSessionConfig,\n logger: IScopedLogger = noopLogger,\n options?: FfmpegDecoderSessionOptions,\n ) {\n this.config = { ...config }\n this.logger = logger\n this.frameDropper = new FrameDropper(config.maxFps)\n this.hwaccelPref = options?.hwaccel ?? 'auto'\n this.hwaccelResolver = options?.hwaccelResolver ?? null\n // Pre-warm backend resolution so push-mode spawn doesn't have to\n // await. openStream() calls the async path directly.\n void this.resolveHwAccelBackend().then((b) => { this.resolvedBackend = b })\n // Don't spawn push-mode ffmpeg here; wait for openStream() or first pushPacket()\n }\n\n /**\n * Resolve the preferred backend for this host and return the first\n * hit, or `null` when software is requested / nothing available.\n * FFmpeg CLI accepts the `-hwaccel <name>` identifier directly.\n */\n private async resolveHwAccelBackend(): Promise<HwAccelBackend | null> {\n if (this.hwaccelPref === 'none') return null\n const explicit: HwAccelBackend | null =\n this.hwaccelPref === 'auto' ? null : this.hwaccelPref\n if (!this.hwaccelResolver) return explicit\n const resolution = await this.hwaccelResolver.resolve(explicit)\n return resolution.preferred[0] ?? null\n }\n\n /**\n * Open an RTSP stream directly — ffmpeg reads from the URL and outputs JPEG frames.\n * This avoids the raw h264 pipe which loses SPS/PPS metadata on some cameras.\n */\n async openStream(url: string): Promise<void> {\n if (this.destroyed) return\n this.pullMode = true\n\n this.killFfmpeg()\n this.outputBuffer = Buffer.alloc(0)\n this.cachedWidth = 0\n this.cachedHeight = 0\n\n // Resolve + prepend `-hwaccel <backend>` before input when the\n // host supports any backend. `-hwaccel` must come before `-i` in\n // the argv so ffmpeg knows to pick a HW-capable decoder. If the\n // backend is unavailable or fails at spawn, ffmpeg exits with an\n // error and the session logs it; the reconnect loop at a higher\n // layer will get triggered.\n const backend = await this.resolveHwAccelBackend()\n const hwArgs: string[] = backend ? ['-hwaccel', backend] : []\n this.activeHwAccel = backend ?? 'none'\n\n const args = [\n '-hide_banner', '-loglevel', 'error',\n ...hwArgs,\n '-fflags', '+nobuffer+flush_packets',\n '-flags', 'low_delay',\n '-probesize', '32768',\n '-analyzeduration', '500000',\n '-rtsp_transport', 'tcp',\n '-i', url,\n '-an', // no audio\n ]\n\n if (this.config.scale > 1) {\n args.push('-vf', `scale=iw/${this.config.scale}:ih/${this.config.scale}`)\n }\n\n args.push('-f', 'image2pipe', '-vcodec', 'mjpeg', '-q:v', '3', '-threads', '1', 'pipe:1')\n\n // Software decode via ffmpeg CLI — no `-hwaccel` flag is passed.\n // If hwaccel gets wired (e.g. `-hwaccel videotoolbox` on macOS),\n // update `hwAccel` below to the actual backend used.\n this.logger.info('Opening RTSP stream directly', { meta: { url: maskUrlCredentials(url), hwAccel: this.activeHwAccel } })\n this.process = spawn('ffmpeg', args, { stdio: ['pipe', 'pipe', 'pipe'] })\n\n this.process.stdin?.on('error', () => {})\n\n this.process.stdout?.on('data', (chunk: Buffer) => {\n this.handleOutputData(chunk)\n })\n\n this.process.stderr?.on('data', (data: Buffer) => {\n const line = data.toString().trim()\n if (line) this.logger.warn('ffmpeg decoder stderr', { meta: { line } })\n })\n\n this.process.on('error', (err: Error) => {\n this.logger.error('FFmpeg decoder spawn error', { meta: { error: err.message } })\n })\n\n this.process.on('close', (code, signal) => {\n this.logger.warn('FFmpeg decoder exited', { meta: { code, signal, frames: this.outputFrames } })\n if (!this.destroyed) {\n this.process = null\n }\n })\n }\n\n private ensurePushProcess(): void {\n if (this.process || this.destroyed || this.pullMode) return\n\n const inputFormat = this.config.codec === 'h265' || this.config.codec === 'hevc'\n ? 'hevc'\n : this.config.codec === 'mjpeg' ? 'mjpeg' : 'h264'\n\n // Read the pre-warmed hwaccel backend set by the constructor's\n // fire-and-forget resolve. Null → software (either explicit\n // 'none', no resolver, or resolve hasn't completed yet on the\n // very first spawn after session creation).\n const hwArgs: string[] = this.resolvedBackend ? ['-hwaccel', this.resolvedBackend] : []\n this.activeHwAccel = this.resolvedBackend ?? 'none'\n\n const args = [\n '-hide_banner', '-loglevel', 'error',\n ...hwArgs,\n '-fflags', '+nobuffer+flush_packets',\n '-flags', 'low_delay',\n '-probesize', '32768',\n '-analyzeduration', '500000',\n '-f', inputFormat, '-i', 'pipe:0',\n ]\n\n if (this.config.scale > 1) {\n args.push('-vf', `scale=iw/${this.config.scale}:ih/${this.config.scale}`)\n }\n\n args.push('-f', 'image2pipe', '-vcodec', 'mjpeg', '-q:v', '3', '-threads', '1', 'pipe:1')\n\n this.logger.info('Spawning push-mode ffmpeg decoder', {\n meta: { codec: this.config.codec, hwAccel: this.activeHwAccel },\n })\n this.process = spawn('ffmpeg', args)\n this.process.stdin?.on('error', () => {})\n\n this.process.stdout?.on('data', (chunk: Buffer) => {\n this.handleOutputData(chunk)\n })\n\n this.process.stderr?.on('data', (data: Buffer) => {\n const line = data.toString().trim()\n if (line) this.logger.warn('ffmpeg decoder stderr', { meta: { line } })\n })\n\n this.process.on('error', (err: Error) => {\n this.logger.error('FFmpeg decoder spawn error', { meta: { error: err.message } })\n })\n\n this.process.on('close', (code, signal) => {\n this.logger.warn('FFmpeg decoder exited', { meta: { code, signal, packets: this.inputPackets, frames: this.outputFrames } })\n if (!this.destroyed) {\n this.process = null\n }\n })\n }\n\n private killFfmpeg(): void {\n if (this.process) {\n try {\n this.process.kill('SIGKILL')\n } catch {\n // already dead\n }\n this.process = null\n }\n }\n\n private handleOutputData(chunk: Buffer): void {\n this.outputBuffer = Buffer.concat([this.outputBuffer, chunk])\n\n // Extract complete JPEG frames from the buffer\n let searchFrom = 0\n while (true) {\n const soiIndex = this.outputBuffer.indexOf(SOI, searchFrom)\n if (soiIndex === -1) break\n\n const eoiIndex = this.outputBuffer.indexOf(EOI, soiIndex + 2)\n if (eoiIndex === -1) break\n\n const frameEnd = eoiIndex + 2\n const jpegData = this.outputBuffer.subarray(soiIndex, frameEnd)\n\n // Advance past the consumed frame\n searchFrom = frameEnd\n\n this.emitFrame(Buffer.from(jpegData))\n }\n\n // Keep only unprocessed tail\n if (searchFrom > 0) {\n this.outputBuffer = Buffer.from(this.outputBuffer.subarray(searchFrom))\n }\n }\n\n private emitFrame(data: Buffer): void {\n const decodeStart = Date.now()\n\n if (!this.frameDropper.shouldKeep()) {\n this.droppedFrames++\n return\n }\n\n const decodeTime = Date.now() - decodeStart\n this.totalDecodeTimeMs += decodeTime\n this.decodeCount++\n this.outputFrames++\n\n // Only parse dimensions on first frame or after FFmpeg restart (cached=0)\n if (this.cachedWidth === 0) {\n const dims = parseJpegDimensions(data)\n this.cachedWidth = dims.width\n this.cachedHeight = dims.height\n this.logger.info('First decoded frame', { meta: { width: dims.width, height: dims.height, format: 'jpeg', bytes: data.length } })\n }\n\n const frame: DecodedFrame = {\n data,\n width: this.cachedWidth,\n height: this.cachedHeight,\n format: 'jpeg',\n timestamp: Date.now(),\n }\n\n for (const cb of this.frameCallbacks) {\n cb(frame)\n }\n }\n\n pushPacket(packet: EncodedPacket): void {\n if (this.destroyed || this.pullMode) return\n this.ensurePushProcess()\n if (!this.process?.stdin) return\n this.inputPackets++\n try {\n this.process.stdin.write(packet.data)\n } catch {\n // stdin may be closed if ffmpeg crashed\n }\n }\n\n onFrame(callback: (frame: DecodedFrame) => void): Unsubscribe {\n this.frameCallbacks.add(callback)\n return () => {\n this.frameCallbacks.delete(callback)\n }\n }\n\n updateConfig(update: Partial<DecoderSessionConfig>): void {\n this.config = { ...this.config, ...update }\n if (update.maxFps !== undefined) {\n this.frameDropper.setMaxFps(update.maxFps)\n }\n }\n\n async destroy(): Promise<void> {\n if (this.destroyed) return\n this.destroyed = true\n this.killFfmpeg()\n this.frameCallbacks.clear()\n }\n\n getStats(): DecoderStats {\n const uptimeSec = Math.max((Date.now() - this.startTime) / 1000, 1)\n return {\n inputFps: this.inputPackets / uptimeSec,\n outputFps: this.outputFrames / uptimeSec,\n avgDecodeTimeMs: this.decodeCount > 0 ? this.totalDecodeTimeMs / this.decodeCount : 0,\n droppedFrames: this.droppedFrames,\n }\n }\n}\n\n/**\n * Parse JPEG SOF0/SOF2 marker to extract image dimensions.\n * Scans for 0xFF 0xC0 (baseline) or 0xFF 0xC2 (progressive) markers.\n * Returns {width: 0, height: 0} if not found.\n */\nfunction parseJpegDimensions(data: Buffer): { width: number; height: number } {\n for (let i = 0; i < data.length - 8; i++) {\n if (data[i] === 0xFF && (data[i + 1] === 0xC0 || data[i + 1] === 0xC2)) {\n const height = (data[i + 5]! << 8) | data[i + 6]!\n const width = (data[i + 7]! << 8) | data[i + 8]!\n return { width, height }\n }\n }\n return { width: 0, height: 0 }\n}\n","export class FrameDropper {\n private intervalMs: number\n private lastPassedAt = -Infinity\n\n constructor(maxFps: number) {\n this.intervalMs = maxFps > 0 ? 1000 / maxFps : 0\n }\n\n shouldKeep(): boolean {\n if (this.intervalMs === 0) return true\n\n const now = Date.now()\n if (now - this.lastPassedAt >= this.intervalMs) {\n this.lastPassedAt = now\n return true\n }\n return false\n }\n\n setMaxFps(maxFps: number): void {\n this.intervalMs = maxFps > 0 ? 1000 / maxFps : 0\n }\n}\n","import type { IDecoderProvider, DecoderSessionConfig, IDecoderSession, IScopedLogger } from '@camstack/types'\nimport { FfmpegDecoderSession } from './ffmpeg-decoder-session'\n\nconst SUPPORTED_CODECS = new Set(['h264', 'h265', 'hevc', 'mjpeg'])\n\nexport class FfmpegDecoderProvider implements IDecoderProvider {\n readonly id = 'ffmpeg'\n readonly name = 'FFmpeg Decoder'\n readonly isPullMode = false\n /** Software decoder — used as fallback when hardware decoders are unavailable. */\n readonly priority = 50\n private logger: IScopedLogger | null = null\n\n setLogger(logger: IScopedLogger): void {\n this.logger = logger\n }\n\n async supportsCodec(input: { codec: string }): Promise<boolean> {\n return SUPPORTED_CODECS.has(input.codec)\n }\n\n async createSession(config: DecoderSessionConfig): Promise<IDecoderSession> {\n return new FfmpegDecoderSession(config, this.logger ?? undefined)\n }\n}\n"],"mappings":"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","names":["import_types"]}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/frame-dropper.ts","../src/ffmpeg-decoder-session.ts","../src/addon/index.ts","../src/ffmpeg-decoder-provider.ts"],"sourcesContent":["export class FrameDropper {\n private intervalMs: number\n private lastPassedAt = -Infinity\n\n constructor(maxFps: number) {\n this.intervalMs = maxFps > 0 ? 1000 / maxFps : 0\n }\n\n shouldKeep(): boolean {\n if (this.intervalMs === 0) return true\n\n const now = Date.now()\n if (now - this.lastPassedAt >= this.intervalMs) {\n this.lastPassedAt = now\n return true\n }\n return false\n }\n\n setMaxFps(maxFps: number): void {\n this.intervalMs = maxFps > 0 ? 1000 / maxFps : 0\n }\n}\n","import { spawn, type ChildProcess } from 'node:child_process'\nimport type {\n IDecoderSession, DecoderSessionConfig, DecoderStats, EncodedPacket,\n DecodedFrame, Unsubscribe, IScopedLogger,\n HwAccelBackend, IKernelHwAccel,\n} from '@camstack/types'\nimport { maskUrlCredentials } from '@camstack/types'\nimport { FrameDropper } from './frame-dropper'\n\nexport type HwAccelPref = 'auto' | 'none' | HwAccelBackend\n\nexport interface FfmpegDecoderSessionOptions {\n /** Addon-level hwaccel preference — per-agent. Default `'auto'`. */\n readonly hwaccel?: HwAccelPref\n /** Kernel hwaccel resolver — `ctx.kernel.hwaccel` passed from the addon. */\n readonly hwaccelResolver?: IKernelHwAccel\n}\n\n/** Minimal no-op logger for default parameter */\nconst noopLogger: IScopedLogger = {\n debug() {},\n info() {},\n warn() {},\n error() {},\n child() { return noopLogger },\n withTags() { return noopLogger },\n}\n\nconst SOI = Buffer.from([0xff, 0xd8])\nconst EOI = Buffer.from([0xff, 0xd9])\n\nexport class FfmpegDecoderSession implements IDecoderSession {\n private config: DecoderSessionConfig\n private frameDropper: FrameDropper\n private process: ChildProcess | null = null\n private frameCallbacks = new Set<(frame: DecodedFrame) => void>()\n private outputBuffer = Buffer.alloc(0)\n private destroyed = false\n private readonly logger: IScopedLogger\n\n /** When openStream() is used, we read from RTSP directly (not push mode) */\n private pullMode = false\n\n // Cached dimensions — won't change between frames from the same FFmpeg session\n private cachedWidth = 0\n private cachedHeight = 0\n\n // Stats tracking\n private inputPackets = 0\n private outputFrames = 0\n private droppedFrames = 0\n private totalDecodeTimeMs = 0\n private decodeCount = 0\n private startTime = Date.now()\n\n private readonly hwaccelPref: HwAccelPref\n private readonly hwaccelResolver: IKernelHwAccel | null\n /** The backend we actually passed to ffmpeg `-hwaccel` — `'none'` = software. */\n private activeHwAccel: 'none' | HwAccelBackend = 'none'\n\n /**\n * Backend resolution is async (calls into `ctx.kernel.hwaccel`), but\n * `pushPacket` is sync — we kick the resolve off in the constructor\n * and cache the result. By the time the first keyframe arrives\n * (~30ms for RTSP), the resolver has completed (it's ultimately\n * `os.platform()` + file checks → sub-ms). If `ensurePushProcess`\n * fires before resolve settles, it skips hwaccel for that spawn;\n * reconnect loop gets the flag on subsequent sessions.\n */\n private resolvedBackend: HwAccelBackend | null = null\n\n constructor(\n config: DecoderSessionConfig,\n logger: IScopedLogger = noopLogger,\n options?: FfmpegDecoderSessionOptions,\n ) {\n this.config = { ...config }\n this.logger = logger\n this.frameDropper = new FrameDropper(config.maxFps)\n this.hwaccelPref = options?.hwaccel ?? 'auto'\n this.hwaccelResolver = options?.hwaccelResolver ?? null\n // Pre-warm backend resolution so push-mode spawn doesn't have to\n // await. openStream() calls the async path directly.\n void this.resolveHwAccelBackend().then((b) => { this.resolvedBackend = b })\n // Don't spawn push-mode ffmpeg here; wait for openStream() or first pushPacket()\n }\n\n /**\n * Resolve the preferred backend for this host and return the first\n * hit, or `null` when software is requested / nothing available.\n * FFmpeg CLI accepts the `-hwaccel <name>` identifier directly.\n */\n private async resolveHwAccelBackend(): Promise<HwAccelBackend | null> {\n if (this.hwaccelPref === 'none') return null\n const explicit: HwAccelBackend | null =\n this.hwaccelPref === 'auto' ? null : this.hwaccelPref\n if (!this.hwaccelResolver) return explicit\n const resolution = await this.hwaccelResolver.resolve(explicit)\n return resolution.preferred[0] ?? null\n }\n\n /**\n * Open an RTSP stream directly — ffmpeg reads from the URL and outputs JPEG frames.\n * This avoids the raw h264 pipe which loses SPS/PPS metadata on some cameras.\n */\n async openStream(url: string): Promise<void> {\n if (this.destroyed) return\n this.pullMode = true\n\n this.killFfmpeg()\n this.outputBuffer = Buffer.alloc(0)\n this.cachedWidth = 0\n this.cachedHeight = 0\n\n // Resolve + prepend `-hwaccel <backend>` before input when the\n // host supports any backend. `-hwaccel` must come before `-i` in\n // the argv so ffmpeg knows to pick a HW-capable decoder. If the\n // backend is unavailable or fails at spawn, ffmpeg exits with an\n // error and the session logs it; the reconnect loop at a higher\n // layer will get triggered.\n const backend = await this.resolveHwAccelBackend()\n const hwArgs: string[] = backend ? ['-hwaccel', backend] : []\n this.activeHwAccel = backend ?? 'none'\n\n const args = [\n '-hide_banner', '-loglevel', 'error',\n ...hwArgs,\n '-fflags', '+nobuffer+flush_packets',\n '-flags', 'low_delay',\n '-probesize', '32768',\n '-analyzeduration', '500000',\n '-rtsp_transport', 'tcp',\n '-i', url,\n '-an', // no audio\n ]\n\n if (this.config.scale > 1) {\n args.push('-vf', `scale=iw/${this.config.scale}:ih/${this.config.scale}`)\n }\n\n args.push('-f', 'image2pipe', '-vcodec', 'mjpeg', '-q:v', '3', '-threads', '1', 'pipe:1')\n\n // Software decode via ffmpeg CLI — no `-hwaccel` flag is passed.\n // If hwaccel gets wired (e.g. `-hwaccel videotoolbox` on macOS),\n // update `hwAccel` below to the actual backend used.\n this.logger.info('Opening RTSP stream directly', { meta: { url: maskUrlCredentials(url), hwAccel: this.activeHwAccel } })\n this.process = spawn('ffmpeg', args, { stdio: ['pipe', 'pipe', 'pipe'] })\n\n this.process.stdin?.on('error', () => {})\n\n this.process.stdout?.on('data', (chunk: Buffer) => {\n this.handleOutputData(chunk)\n })\n\n this.process.stderr?.on('data', (data: Buffer) => {\n const line = data.toString().trim()\n if (line) this.logger.warn('ffmpeg decoder stderr', { meta: { line } })\n })\n\n this.process.on('error', (err: Error) => {\n this.logger.error('FFmpeg decoder spawn error', { meta: { error: err.message } })\n })\n\n this.process.on('close', (code, signal) => {\n this.logger.warn('FFmpeg decoder exited', { meta: { code, signal, frames: this.outputFrames } })\n if (!this.destroyed) {\n this.process = null\n }\n })\n }\n\n private ensurePushProcess(): void {\n if (this.process || this.destroyed || this.pullMode) return\n\n const inputFormat = this.config.codec === 'h265' || this.config.codec === 'hevc'\n ? 'hevc'\n : this.config.codec === 'mjpeg' ? 'mjpeg' : 'h264'\n\n // Read the pre-warmed hwaccel backend set by the constructor's\n // fire-and-forget resolve. Null → software (either explicit\n // 'none', no resolver, or resolve hasn't completed yet on the\n // very first spawn after session creation).\n const hwArgs: string[] = this.resolvedBackend ? ['-hwaccel', this.resolvedBackend] : []\n this.activeHwAccel = this.resolvedBackend ?? 'none'\n\n const args = [\n '-hide_banner', '-loglevel', 'error',\n ...hwArgs,\n '-fflags', '+nobuffer+flush_packets',\n '-flags', 'low_delay',\n '-probesize', '32768',\n '-analyzeduration', '500000',\n '-f', inputFormat, '-i', 'pipe:0',\n ]\n\n if (this.config.scale > 1) {\n args.push('-vf', `scale=iw/${this.config.scale}:ih/${this.config.scale}`)\n }\n\n args.push('-f', 'image2pipe', '-vcodec', 'mjpeg', '-q:v', '3', '-threads', '1', 'pipe:1')\n\n this.logger.info('Spawning push-mode ffmpeg decoder', {\n meta: { codec: this.config.codec, hwAccel: this.activeHwAccel },\n })\n this.process = spawn('ffmpeg', args)\n this.process.stdin?.on('error', () => {})\n\n this.process.stdout?.on('data', (chunk: Buffer) => {\n this.handleOutputData(chunk)\n })\n\n this.process.stderr?.on('data', (data: Buffer) => {\n const line = data.toString().trim()\n if (line) this.logger.warn('ffmpeg decoder stderr', { meta: { line } })\n })\n\n this.process.on('error', (err: Error) => {\n this.logger.error('FFmpeg decoder spawn error', { meta: { error: err.message } })\n })\n\n this.process.on('close', (code, signal) => {\n this.logger.warn('FFmpeg decoder exited', { meta: { code, signal, packets: this.inputPackets, frames: this.outputFrames } })\n if (!this.destroyed) {\n this.process = null\n }\n })\n }\n\n private killFfmpeg(): void {\n if (this.process) {\n try {\n this.process.kill('SIGKILL')\n } catch {\n // already dead\n }\n this.process = null\n }\n }\n\n private handleOutputData(chunk: Buffer): void {\n this.outputBuffer = Buffer.concat([this.outputBuffer, chunk])\n\n // Extract complete JPEG frames from the buffer\n let searchFrom = 0\n while (true) {\n const soiIndex = this.outputBuffer.indexOf(SOI, searchFrom)\n if (soiIndex === -1) break\n\n const eoiIndex = this.outputBuffer.indexOf(EOI, soiIndex + 2)\n if (eoiIndex === -1) break\n\n const frameEnd = eoiIndex + 2\n const jpegData = this.outputBuffer.subarray(soiIndex, frameEnd)\n\n // Advance past the consumed frame\n searchFrom = frameEnd\n\n this.emitFrame(Buffer.from(jpegData))\n }\n\n // Keep only unprocessed tail\n if (searchFrom > 0) {\n this.outputBuffer = Buffer.from(this.outputBuffer.subarray(searchFrom))\n }\n }\n\n private emitFrame(data: Buffer): void {\n const decodeStart = Date.now()\n\n if (!this.frameDropper.shouldKeep()) {\n this.droppedFrames++\n return\n }\n\n const decodeTime = Date.now() - decodeStart\n this.totalDecodeTimeMs += decodeTime\n this.decodeCount++\n this.outputFrames++\n\n // Only parse dimensions on first frame or after FFmpeg restart (cached=0)\n if (this.cachedWidth === 0) {\n const dims = parseJpegDimensions(data)\n this.cachedWidth = dims.width\n this.cachedHeight = dims.height\n this.logger.info('First decoded frame', { meta: { width: dims.width, height: dims.height, format: 'jpeg', bytes: data.length } })\n }\n\n const frame: DecodedFrame = {\n data,\n width: this.cachedWidth,\n height: this.cachedHeight,\n format: 'jpeg',\n timestamp: Date.now(),\n }\n\n for (const cb of this.frameCallbacks) {\n cb(frame)\n }\n }\n\n pushPacket(packet: EncodedPacket): void {\n if (this.destroyed || this.pullMode) return\n this.ensurePushProcess()\n if (!this.process?.stdin) return\n this.inputPackets++\n try {\n this.process.stdin.write(packet.data)\n } catch {\n // stdin may be closed if ffmpeg crashed\n }\n }\n\n onFrame(callback: (frame: DecodedFrame) => void): Unsubscribe {\n this.frameCallbacks.add(callback)\n return () => {\n this.frameCallbacks.delete(callback)\n }\n }\n\n updateConfig(update: Partial<DecoderSessionConfig>): void {\n this.config = { ...this.config, ...update }\n if (update.maxFps !== undefined) {\n this.frameDropper.setMaxFps(update.maxFps)\n }\n }\n\n async destroy(): Promise<void> {\n if (this.destroyed) return\n this.destroyed = true\n this.killFfmpeg()\n this.frameCallbacks.clear()\n }\n\n getStats(): DecoderStats {\n const uptimeSec = Math.max((Date.now() - this.startTime) / 1000, 1)\n return {\n inputFps: this.inputPackets / uptimeSec,\n outputFps: this.outputFrames / uptimeSec,\n avgDecodeTimeMs: this.decodeCount > 0 ? this.totalDecodeTimeMs / this.decodeCount : 0,\n droppedFrames: this.droppedFrames,\n }\n }\n}\n\n/**\n * Parse JPEG SOF0/SOF2 marker to extract image dimensions.\n * Scans for 0xFF 0xC0 (baseline) or 0xFF 0xC2 (progressive) markers.\n * Returns {width: 0, height: 0} if not found.\n */\nfunction parseJpegDimensions(data: Buffer): { width: number; height: number } {\n for (let i = 0; i < data.length - 8; i++) {\n if (data[i] === 0xFF && (data[i + 1] === 0xC0 || data[i + 1] === 0xC2)) {\n const height = (data[i + 5]! << 8) | data[i + 6]!\n const width = (data[i + 7]! << 8) | data[i + 8]!\n return { width, height }\n }\n }\n return { width: 0, height: 0 }\n}\n","import { randomUUID } from 'node:crypto'\nimport type {\n DecoderHwAccelConfig,\n HwAccelChoice,\n ProviderRegistration,\n IDecoderSession,\n DecoderStats,\n IDecoderCapProvider,\n FrameFormat,\n} from '@camstack/types'\nimport {\n BaseAddon,\n DEFAULT_DECODER_HWACCEL_CONFIG,\n HWACCEL_OPTIONS,\n decoderCapability,\n RingBuffer,\n} from '@camstack/types'\nimport { FfmpegDecoderSession } from '../ffmpeg-decoder-session.js'\n\n/** Cap-compatible frame shape — format must match DecodedFrameSchema enum values. */\ntype CapDecodedFrame = {\n data: Uint8Array<ArrayBuffer>\n width: number\n height: number\n format: FrameFormat\n timestamp: number\n}\n\n/** Cap-compatible session config — matches DecoderSessionConfigSchema output type. */\ntype CapDecoderSessionConfig = {\n codec: string\n maxFps: number\n outputFormat: FrameFormat\n scale: number\n width?: number\n height?: number\n}\n\n/** Cap-compatible encoded packet — data is Uint8Array matching EncodedPacketSchema. */\ntype CapEncodedPacket = {\n type: 'video' | 'audio'\n data: Uint8Array<ArrayBuffer>\n pts: number\n dts: number\n keyframe: boolean\n codec: string\n}\n\nconst FRAME_BUFFER_CAPACITY = 32\n\n/** Per-session metadata recorded at creation time, surfaced via `listActiveSessions`. */\ninterface SessionMeta {\n readonly codec: string\n readonly outputFormat: string\n readonly createdAtMs: number\n}\n\n/**\n * FFmpeg decoder addon — H264/HEVC/MJPEG decode via ffmpeg child\n * process.\n *\n * Phase 2d of the pipeline-settings migration — ffmpeg decoder owns\n * its own `hwaccel` choice + `probedBestHwaccel` hint. Sessions\n * resolve the effective backend from this addon's global settings\n * instead of the orchestrator's legacy `AgentPipelineSettings.hwaccel`.\n * Session constructor still appends `-hwaccel <name>` to the ffmpeg\n * argv as before.\n *\n * Implements the sessionId-based IDecoderCapProvider cap interface.\n * Sessions are managed internally via a Map; frames are polled via\n * RingBuffer.\n */\nexport default class DecoderFfmpegAddon extends BaseAddon<DecoderHwAccelConfig> implements IDecoderCapProvider {\n private readonly sessions = new Map<string, IDecoderSession>()\n private readonly frameBuffers = new Map<string, RingBuffer<CapDecodedFrame>>()\n private readonly unsubscribers = new Map<string, () => void>()\n private readonly sessionMeta = new Map<string, SessionMeta>()\n\n constructor() { super(DEFAULT_DECODER_HWACCEL_CONFIG) }\n\n protected globalSettingsSchema() {\n return this.schema({\n sections: [{\n id: 'hwaccel',\n title: 'Hardware acceleration',\n description: 'Backend appended as `-hwaccel <name>` to the ffmpeg argv. \"Auto\" defers to the probed best; concrete backends force it. Changes apply to NEW sessions — existing sessions keep the backend they were created with.',\n fields: [\n this.field({\n type: 'select',\n key: 'hwaccel',\n label: 'Preferred backend',\n options: [...HWACCEL_OPTIONS],\n default: 'auto',\n immediate: true,\n }),\n this.field({\n type: 'text',\n key: 'probedBestHwaccel',\n label: 'Probed best',\n description: 'Auto-detected best backend on this host. Call `reprobeHwaccel` to refresh.',\n disabled: true,\n default: '',\n }),\n ],\n }],\n })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.ctx.logger.info('FFmpeg decoder addon initialized')\n // Auto-seed probedBestHwaccel on first boot.\n if (!this.config.probedBestHwaccel) {\n this.reprobeHwaccel().catch((err: unknown) => {\n this.ctx.logger.warn('ffmpeg: auto-reprobe hwaccel failed', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n })\n }\n return [{ capability: decoderCapability, provider: this }]\n }\n\n /**\n * Resolve the effective hwaccel backend for a new session. Reads\n * this addon's own `hwaccel` setting. `'auto'` defers to the\n * session's local resolver (`ctx.kernel.hwaccel`).\n */\n private resolveHwAccelPref(): HwAccelChoice {\n return this.config.hwaccel\n }\n\n /**\n * Re-run the platform probe on this host and persist the detected\n * backend as `probedBestHwaccel`. Operator `hwaccel` setting is not\n * touched — only the hint.\n */\n async reprobeHwaccel(): Promise<{ backend: string }> {\n const resolver = this.ctx.kernel.hwaccel\n if (!resolver) {\n this.ctx.logger.warn('reprobeHwaccel: no kernel hwaccel resolver — returning none')\n await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: 'none' })\n return { backend: 'none' }\n }\n try {\n const res = await resolver.resolve()\n const backend = (res.preferred[0] ?? 'none') as string\n await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: backend })\n this.ctx.logger.info('reprobeHwaccel: wrote probedBestHwaccel', {\n meta: { backend, rationale: res.rationale, preferred: res.preferred },\n })\n return { backend }\n } catch (err) {\n this.ctx.logger.warn('reprobeHwaccel failed', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: 'none' })\n return { backend: 'none' }\n }\n }\n\n async supportsCodec(input: { codec: string }): Promise<boolean> {\n return ['h264', 'h265', 'hevc', 'mjpeg'].includes(input.codec.toLowerCase())\n }\n\n async getInfo(): Promise<{ id: string; name: string; isPullMode?: boolean; priority?: number }> {\n return {\n id: 'decoder-ffmpeg',\n name: 'Decoder (FFmpeg)',\n isPullMode: false,\n priority: 50,\n }\n }\n\n async createSession(config: CapDecoderSessionConfig): Promise<{ sessionId: string; nodeId: string }> {\n const sessionId = randomUUID()\n const hwaccel = this.resolveHwAccelPref()\n const session = new FfmpegDecoderSession(config, this.ctx.logger, {\n hwaccel,\n hwaccelResolver: this.ctx.kernel.hwaccel,\n })\n const ringBuffer = new RingBuffer<CapDecodedFrame>(FRAME_BUFFER_CAPACITY)\n\n const unsub = session.onFrame((frame) => {\n // Map internal DecodedFrame to cap-compatible shape.\n const { format } = frame\n if (format !== 'jpeg' && format !== 'rgb' && format !== 'bgr' && format !== 'yuv420' && format !== 'gray') return\n // Copy frame data into a fresh ArrayBuffer so the cap-facing Uint8Array\n // has a concrete ArrayBuffer (not a SharedArrayBuffer / Buffer backing).\n const arrayBuf = new ArrayBuffer(frame.data.byteLength)\n new Uint8Array(arrayBuf).set(frame.data)\n const capFrame: CapDecodedFrame = {\n data: new Uint8Array(arrayBuf),\n width: frame.width,\n height: frame.height,\n format,\n timestamp: frame.timestamp,\n }\n ringBuffer.push(capFrame)\n })\n\n this.sessions.set(sessionId, session)\n this.frameBuffers.set(sessionId, ringBuffer)\n this.unsubscribers.set(sessionId, unsub)\n this.sessionMeta.set(sessionId, {\n codec: config.codec,\n outputFormat: config.outputFormat,\n createdAtMs: Date.now(),\n })\n\n this.ctx.logger.info('ffmpeg: created session', { meta: { sessionId, codec: config.codec, hwaccelPref: hwaccel } })\n return { sessionId, nodeId: this.ctx.kernel.localNodeId ?? 'local' }\n }\n\n async destroySession(input: { sessionId: string }): Promise<void> {\n const { sessionId } = input\n const session = this.sessions.get(sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${sessionId}`)\n }\n\n const unsub = this.unsubscribers.get(sessionId)\n if (unsub) unsub()\n\n await session.destroy()\n\n this.sessions.delete(sessionId)\n this.frameBuffers.delete(sessionId)\n this.unsubscribers.delete(sessionId)\n this.sessionMeta.delete(sessionId)\n\n this.ctx.logger.info('ffmpeg: destroyed session', { meta: { sessionId } })\n }\n\n async listActiveSessions(): Promise<readonly { sessionId: string; codec: string; outputFormat: string; createdAtMs: number }[]> {\n const out: Array<{ sessionId: string; codec: string; outputFormat: string; createdAtMs: number }> = []\n for (const [sessionId, meta] of this.sessionMeta) {\n out.push({ sessionId, codec: meta.codec, outputFormat: meta.outputFormat, createdAtMs: meta.createdAtMs })\n }\n return out\n }\n\n async pushPacket(input: { sessionId: string; packet: CapEncodedPacket }): Promise<void> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n // Convert Uint8Array to Buffer at the cap boundary before passing to the internal session.\n session.pushPacket({\n ...input.packet,\n data: Buffer.from(input.packet.data),\n })\n }\n\n async openStream(input: { sessionId: string; url: string }): Promise<void> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n if (session.openStream) {\n await session.openStream(input.url)\n }\n }\n\n async pullFrames(input: { sessionId: string; maxCount: number }): Promise<CapDecodedFrame[]> {\n const ringBuffer = this.frameBuffers.get(input.sessionId)\n if (!ringBuffer) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n return ringBuffer.drain(input.maxCount)\n }\n\n async updateConfig(input: { sessionId: string; config: Partial<CapDecoderSessionConfig> }): Promise<void> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n session.updateConfig(input.config)\n }\n\n async getStats(input: { sessionId: string }): Promise<DecoderStats> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n return session.getStats()\n }\n\n protected async onShutdown(): Promise<void> {\n this.ctx.logger.info('FFmpeg decoder addon shutdown — destroying all sessions')\n const destroyPromises: Promise<void>[] = []\n for (const [sessionId, session] of this.sessions) {\n const unsub = this.unsubscribers.get(sessionId)\n if (unsub) unsub()\n destroyPromises.push(session.destroy())\n }\n await Promise.all(destroyPromises)\n this.sessions.clear()\n this.frameBuffers.clear()\n this.sessionMeta.clear()\n this.unsubscribers.clear()\n }\n}\n","import type { IDecoderProvider, DecoderSessionConfig, IDecoderSession, IScopedLogger } from '@camstack/types'\nimport { FfmpegDecoderSession } from './ffmpeg-decoder-session'\n\nconst SUPPORTED_CODECS = new Set(['h264', 'h265', 'hevc', 'mjpeg'])\n\nexport class FfmpegDecoderProvider implements IDecoderProvider {\n readonly id = 'ffmpeg'\n readonly name = 'FFmpeg Decoder'\n readonly isPullMode = false\n /** Software decoder — used as fallback when hardware decoders are unavailable. */\n readonly priority = 50\n private logger: IScopedLogger | null = null\n\n setLogger(logger: IScopedLogger): void {\n this.logger = logger\n }\n\n async supportsCodec(input: { codec: string }): Promise<boolean> {\n return SUPPORTED_CODECS.has(input.codec)\n }\n\n async createSession(config: DecoderSessionConfig): Promise<IDecoderSession> {\n return new FfmpegDecoderSession(config, this.logger ?? undefined)\n }\n}\n"],"names":["maskUrlCredentials","spawn","BaseAddon","DEFAULT_DECODER_HWACCEL_CONFIG","HWACCEL_OPTIONS","decoderCapability","randomUUID","RingBuffer"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
DEFAULT_DECODER_HWACCEL_CONFIG,
|
|
6
|
-
HWACCEL_OPTIONS,
|
|
7
|
-
decoderCapability,
|
|
8
|
-
RingBuffer
|
|
9
|
-
} from "@camstack/types";
|
|
10
|
-
|
|
11
|
-
// src/ffmpeg-decoder-session.ts
|
|
12
|
-
import { spawn } from "child_process";
|
|
13
|
-
import { maskUrlCredentials } from "@camstack/types";
|
|
14
|
-
|
|
15
|
-
// src/frame-dropper.ts
|
|
16
|
-
var FrameDropper = class {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { maskUrlCredentials, BaseAddon, DEFAULT_DECODER_HWACCEL_CONFIG, HWACCEL_OPTIONS, decoderCapability, RingBuffer } from "@camstack/types";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
class FrameDropper {
|
|
17
5
|
intervalMs;
|
|
18
6
|
lastPassedAt = -Infinity;
|
|
19
7
|
constructor(maxFps) {
|
|
@@ -31,10 +19,8 @@ var FrameDropper = class {
|
|
|
31
19
|
setMaxFps(maxFps) {
|
|
32
20
|
this.intervalMs = maxFps > 0 ? 1e3 / maxFps : 0;
|
|
33
21
|
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// src/ffmpeg-decoder-session.ts
|
|
37
|
-
var noopLogger = {
|
|
22
|
+
}
|
|
23
|
+
const noopLogger = {
|
|
38
24
|
debug() {
|
|
39
25
|
},
|
|
40
26
|
info() {
|
|
@@ -50,9 +36,9 @@ var noopLogger = {
|
|
|
50
36
|
return noopLogger;
|
|
51
37
|
}
|
|
52
38
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
39
|
+
const SOI = Buffer.from([255, 216]);
|
|
40
|
+
const EOI = Buffer.from([255, 217]);
|
|
41
|
+
class FfmpegDecoderSession {
|
|
56
42
|
config;
|
|
57
43
|
frameDropper;
|
|
58
44
|
process = null;
|
|
@@ -307,7 +293,7 @@ var FfmpegDecoderSession = class {
|
|
|
307
293
|
droppedFrames: this.droppedFrames
|
|
308
294
|
};
|
|
309
295
|
}
|
|
310
|
-
}
|
|
296
|
+
}
|
|
311
297
|
function parseJpegDimensions(data) {
|
|
312
298
|
for (let i = 0; i < data.length - 8; i++) {
|
|
313
299
|
if (data[i] === 255 && (data[i + 1] === 192 || data[i + 1] === 194)) {
|
|
@@ -318,10 +304,8 @@ function parseJpegDimensions(data) {
|
|
|
318
304
|
}
|
|
319
305
|
return { width: 0, height: 0 };
|
|
320
306
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
var FRAME_BUFFER_CAPACITY = 32;
|
|
324
|
-
var DecoderFfmpegAddon = class extends BaseAddon {
|
|
307
|
+
const FRAME_BUFFER_CAPACITY = 32;
|
|
308
|
+
class DecoderFfmpegAddon extends BaseAddon {
|
|
325
309
|
sessions = /* @__PURE__ */ new Map();
|
|
326
310
|
frameBuffers = /* @__PURE__ */ new Map();
|
|
327
311
|
unsubscribers = /* @__PURE__ */ new Map();
|
|
@@ -334,7 +318,7 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
334
318
|
sections: [{
|
|
335
319
|
id: "hwaccel",
|
|
336
320
|
title: "Hardware acceleration",
|
|
337
|
-
description: 'Backend appended as `-hwaccel <name>` to the ffmpeg argv. "Auto" defers to the probed best; concrete backends force it. Changes apply to NEW sessions
|
|
321
|
+
description: 'Backend appended as `-hwaccel <name>` to the ffmpeg argv. "Auto" defers to the probed best; concrete backends force it. Changes apply to NEW sessions — existing sessions keep the backend they were created with.',
|
|
338
322
|
fields: [
|
|
339
323
|
this.field({
|
|
340
324
|
type: "select",
|
|
@@ -383,7 +367,7 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
383
367
|
async reprobeHwaccel() {
|
|
384
368
|
const resolver = this.ctx.kernel.hwaccel;
|
|
385
369
|
if (!resolver) {
|
|
386
|
-
this.ctx.logger.warn("reprobeHwaccel: no kernel hwaccel resolver
|
|
370
|
+
this.ctx.logger.warn("reprobeHwaccel: no kernel hwaccel resolver — returning none");
|
|
387
371
|
await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: "none" });
|
|
388
372
|
return { backend: "none" };
|
|
389
373
|
}
|
|
@@ -510,7 +494,7 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
510
494
|
return session.getStats();
|
|
511
495
|
}
|
|
512
496
|
async onShutdown() {
|
|
513
|
-
this.ctx.logger.info("FFmpeg decoder addon shutdown
|
|
497
|
+
this.ctx.logger.info("FFmpeg decoder addon shutdown — destroying all sessions");
|
|
514
498
|
const destroyPromises = [];
|
|
515
499
|
for (const [sessionId, session] of this.sessions) {
|
|
516
500
|
const unsub = this.unsubscribers.get(sessionId);
|
|
@@ -523,11 +507,9 @@ var DecoderFfmpegAddon = class extends BaseAddon {
|
|
|
523
507
|
this.sessionMeta.clear();
|
|
524
508
|
this.unsubscribers.clear();
|
|
525
509
|
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
var SUPPORTED_CODECS = /* @__PURE__ */ new Set(["h264", "h265", "hevc", "mjpeg"]);
|
|
530
|
-
var FfmpegDecoderProvider = class {
|
|
510
|
+
}
|
|
511
|
+
const SUPPORTED_CODECS = /* @__PURE__ */ new Set(["h264", "h265", "hevc", "mjpeg"]);
|
|
512
|
+
class FfmpegDecoderProvider {
|
|
531
513
|
id = "ffmpeg";
|
|
532
514
|
name = "FFmpeg Decoder";
|
|
533
515
|
isPullMode = false;
|
|
@@ -543,11 +525,11 @@ var FfmpegDecoderProvider = class {
|
|
|
543
525
|
async createSession(config) {
|
|
544
526
|
return new FfmpegDecoderSession(config, this.logger ?? void 0);
|
|
545
527
|
}
|
|
546
|
-
}
|
|
528
|
+
}
|
|
547
529
|
export {
|
|
548
530
|
DecoderFfmpegAddon,
|
|
549
531
|
FfmpegDecoderProvider,
|
|
550
532
|
FfmpegDecoderSession,
|
|
551
533
|
FrameDropper
|
|
552
534
|
};
|
|
553
|
-
//# sourceMappingURL=index.mjs.map
|
|
535
|
+
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/addon/index.ts","../src/ffmpeg-decoder-session.ts","../src/frame-dropper.ts","../src/ffmpeg-decoder-provider.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport type {\n DecoderHwAccelConfig,\n HwAccelChoice,\n ProviderRegistration,\n IDecoderSession,\n DecoderStats,\n IDecoderCapProvider,\n FrameFormat,\n} from '@camstack/types'\nimport {\n BaseAddon,\n DEFAULT_DECODER_HWACCEL_CONFIG,\n HWACCEL_OPTIONS,\n decoderCapability,\n RingBuffer,\n} from '@camstack/types'\nimport { FfmpegDecoderSession } from '../ffmpeg-decoder-session.js'\n\n/** Cap-compatible frame shape — format must match DecodedFrameSchema enum values. */\ntype CapDecodedFrame = {\n data: Uint8Array<ArrayBuffer>\n width: number\n height: number\n format: FrameFormat\n timestamp: number\n}\n\n/** Cap-compatible session config — matches DecoderSessionConfigSchema output type. */\ntype CapDecoderSessionConfig = {\n codec: string\n maxFps: number\n outputFormat: FrameFormat\n scale: number\n width?: number\n height?: number\n}\n\n/** Cap-compatible encoded packet — data is Uint8Array matching EncodedPacketSchema. */\ntype CapEncodedPacket = {\n type: 'video' | 'audio'\n data: Uint8Array<ArrayBuffer>\n pts: number\n dts: number\n keyframe: boolean\n codec: string\n}\n\nconst FRAME_BUFFER_CAPACITY = 32\n\n/** Per-session metadata recorded at creation time, surfaced via `listActiveSessions`. */\ninterface SessionMeta {\n readonly codec: string\n readonly outputFormat: string\n readonly createdAtMs: number\n}\n\n/**\n * FFmpeg decoder addon — H264/HEVC/MJPEG decode via ffmpeg child\n * process.\n *\n * Phase 2d of the pipeline-settings migration — ffmpeg decoder owns\n * its own `hwaccel` choice + `probedBestHwaccel` hint. Sessions\n * resolve the effective backend from this addon's global settings\n * instead of the orchestrator's legacy `AgentPipelineSettings.hwaccel`.\n * Session constructor still appends `-hwaccel <name>` to the ffmpeg\n * argv as before.\n *\n * Implements the sessionId-based IDecoderCapProvider cap interface.\n * Sessions are managed internally via a Map; frames are polled via\n * RingBuffer.\n */\nexport default class DecoderFfmpegAddon extends BaseAddon<DecoderHwAccelConfig> implements IDecoderCapProvider {\n private readonly sessions = new Map<string, IDecoderSession>()\n private readonly frameBuffers = new Map<string, RingBuffer<CapDecodedFrame>>()\n private readonly unsubscribers = new Map<string, () => void>()\n private readonly sessionMeta = new Map<string, SessionMeta>()\n\n constructor() { super(DEFAULT_DECODER_HWACCEL_CONFIG) }\n\n protected globalSettingsSchema() {\n return this.schema({\n sections: [{\n id: 'hwaccel',\n title: 'Hardware acceleration',\n description: 'Backend appended as `-hwaccel <name>` to the ffmpeg argv. \"Auto\" defers to the probed best; concrete backends force it. Changes apply to NEW sessions — existing sessions keep the backend they were created with.',\n fields: [\n this.field({\n type: 'select',\n key: 'hwaccel',\n label: 'Preferred backend',\n options: [...HWACCEL_OPTIONS],\n default: 'auto',\n immediate: true,\n }),\n this.field({\n type: 'text',\n key: 'probedBestHwaccel',\n label: 'Probed best',\n description: 'Auto-detected best backend on this host. Call `reprobeHwaccel` to refresh.',\n disabled: true,\n default: '',\n }),\n ],\n }],\n })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.ctx.logger.info('FFmpeg decoder addon initialized')\n // Auto-seed probedBestHwaccel on first boot.\n if (!this.config.probedBestHwaccel) {\n this.reprobeHwaccel().catch((err: unknown) => {\n this.ctx.logger.warn('ffmpeg: auto-reprobe hwaccel failed', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n })\n }\n return [{ capability: decoderCapability, provider: this }]\n }\n\n /**\n * Resolve the effective hwaccel backend for a new session. Reads\n * this addon's own `hwaccel` setting. `'auto'` defers to the\n * session's local resolver (`ctx.kernel.hwaccel`).\n */\n private resolveHwAccelPref(): HwAccelChoice {\n return this.config.hwaccel\n }\n\n /**\n * Re-run the platform probe on this host and persist the detected\n * backend as `probedBestHwaccel`. Operator `hwaccel` setting is not\n * touched — only the hint.\n */\n async reprobeHwaccel(): Promise<{ backend: string }> {\n const resolver = this.ctx.kernel.hwaccel\n if (!resolver) {\n this.ctx.logger.warn('reprobeHwaccel: no kernel hwaccel resolver — returning none')\n await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: 'none' })\n return { backend: 'none' }\n }\n try {\n const res = await resolver.resolve()\n const backend = (res.preferred[0] ?? 'none') as string\n await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: backend })\n this.ctx.logger.info('reprobeHwaccel: wrote probedBestHwaccel', {\n meta: { backend, rationale: res.rationale, preferred: res.preferred },\n })\n return { backend }\n } catch (err) {\n this.ctx.logger.warn('reprobeHwaccel failed', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: 'none' })\n return { backend: 'none' }\n }\n }\n\n async supportsCodec(input: { codec: string }): Promise<boolean> {\n return ['h264', 'h265', 'hevc', 'mjpeg'].includes(input.codec.toLowerCase())\n }\n\n async getInfo(): Promise<{ id: string; name: string; isPullMode?: boolean; priority?: number }> {\n return {\n id: 'decoder-ffmpeg',\n name: 'Decoder (FFmpeg)',\n isPullMode: false,\n priority: 50,\n }\n }\n\n async createSession(config: CapDecoderSessionConfig): Promise<{ sessionId: string; nodeId: string }> {\n const sessionId = randomUUID()\n const hwaccel = this.resolveHwAccelPref()\n const session = new FfmpegDecoderSession(config, this.ctx.logger, {\n hwaccel,\n hwaccelResolver: this.ctx.kernel.hwaccel,\n })\n const ringBuffer = new RingBuffer<CapDecodedFrame>(FRAME_BUFFER_CAPACITY)\n\n const unsub = session.onFrame((frame) => {\n // Map internal DecodedFrame to cap-compatible shape.\n const { format } = frame\n if (format !== 'jpeg' && format !== 'rgb' && format !== 'bgr' && format !== 'yuv420' && format !== 'gray') return\n // Copy frame data into a fresh ArrayBuffer so the cap-facing Uint8Array\n // has a concrete ArrayBuffer (not a SharedArrayBuffer / Buffer backing).\n const arrayBuf = new ArrayBuffer(frame.data.byteLength)\n new Uint8Array(arrayBuf).set(frame.data)\n const capFrame: CapDecodedFrame = {\n data: new Uint8Array(arrayBuf),\n width: frame.width,\n height: frame.height,\n format,\n timestamp: frame.timestamp,\n }\n ringBuffer.push(capFrame)\n })\n\n this.sessions.set(sessionId, session)\n this.frameBuffers.set(sessionId, ringBuffer)\n this.unsubscribers.set(sessionId, unsub)\n this.sessionMeta.set(sessionId, {\n codec: config.codec,\n outputFormat: config.outputFormat,\n createdAtMs: Date.now(),\n })\n\n this.ctx.logger.info('ffmpeg: created session', { meta: { sessionId, codec: config.codec, hwaccelPref: hwaccel } })\n return { sessionId, nodeId: this.ctx.kernel.localNodeId ?? 'local' }\n }\n\n async destroySession(input: { sessionId: string }): Promise<void> {\n const { sessionId } = input\n const session = this.sessions.get(sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${sessionId}`)\n }\n\n const unsub = this.unsubscribers.get(sessionId)\n if (unsub) unsub()\n\n await session.destroy()\n\n this.sessions.delete(sessionId)\n this.frameBuffers.delete(sessionId)\n this.unsubscribers.delete(sessionId)\n this.sessionMeta.delete(sessionId)\n\n this.ctx.logger.info('ffmpeg: destroyed session', { meta: { sessionId } })\n }\n\n async listActiveSessions(): Promise<readonly { sessionId: string; codec: string; outputFormat: string; createdAtMs: number }[]> {\n const out: Array<{ sessionId: string; codec: string; outputFormat: string; createdAtMs: number }> = []\n for (const [sessionId, meta] of this.sessionMeta) {\n out.push({ sessionId, codec: meta.codec, outputFormat: meta.outputFormat, createdAtMs: meta.createdAtMs })\n }\n return out\n }\n\n async pushPacket(input: { sessionId: string; packet: CapEncodedPacket }): Promise<void> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n // Convert Uint8Array to Buffer at the cap boundary before passing to the internal session.\n session.pushPacket({\n ...input.packet,\n data: Buffer.from(input.packet.data),\n })\n }\n\n async openStream(input: { sessionId: string; url: string }): Promise<void> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n if (session.openStream) {\n await session.openStream(input.url)\n }\n }\n\n async pullFrames(input: { sessionId: string; maxCount: number }): Promise<CapDecodedFrame[]> {\n const ringBuffer = this.frameBuffers.get(input.sessionId)\n if (!ringBuffer) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n return ringBuffer.drain(input.maxCount)\n }\n\n async updateConfig(input: { sessionId: string; config: Partial<CapDecoderSessionConfig> }): Promise<void> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n session.updateConfig(input.config)\n }\n\n async getStats(input: { sessionId: string }): Promise<DecoderStats> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n return session.getStats()\n }\n\n protected async onShutdown(): Promise<void> {\n this.ctx.logger.info('FFmpeg decoder addon shutdown — destroying all sessions')\n const destroyPromises: Promise<void>[] = []\n for (const [sessionId, session] of this.sessions) {\n const unsub = this.unsubscribers.get(sessionId)\n if (unsub) unsub()\n destroyPromises.push(session.destroy())\n }\n await Promise.all(destroyPromises)\n this.sessions.clear()\n this.frameBuffers.clear()\n this.sessionMeta.clear()\n this.unsubscribers.clear()\n }\n}\n","import { spawn, type ChildProcess } from 'node:child_process'\nimport type {\n IDecoderSession, DecoderSessionConfig, DecoderStats, EncodedPacket,\n DecodedFrame, Unsubscribe, IScopedLogger,\n HwAccelBackend, IKernelHwAccel,\n} from '@camstack/types'\nimport { maskUrlCredentials } from '@camstack/types'\nimport { FrameDropper } from './frame-dropper'\n\nexport type HwAccelPref = 'auto' | 'none' | HwAccelBackend\n\nexport interface FfmpegDecoderSessionOptions {\n /** Addon-level hwaccel preference — per-agent. Default `'auto'`. */\n readonly hwaccel?: HwAccelPref\n /** Kernel hwaccel resolver — `ctx.kernel.hwaccel` passed from the addon. */\n readonly hwaccelResolver?: IKernelHwAccel\n}\n\n/** Minimal no-op logger for default parameter */\nconst noopLogger: IScopedLogger = {\n debug() {},\n info() {},\n warn() {},\n error() {},\n child() { return noopLogger },\n withTags() { return noopLogger },\n}\n\nconst SOI = Buffer.from([0xff, 0xd8])\nconst EOI = Buffer.from([0xff, 0xd9])\n\nexport class FfmpegDecoderSession implements IDecoderSession {\n private config: DecoderSessionConfig\n private frameDropper: FrameDropper\n private process: ChildProcess | null = null\n private frameCallbacks = new Set<(frame: DecodedFrame) => void>()\n private outputBuffer = Buffer.alloc(0)\n private destroyed = false\n private readonly logger: IScopedLogger\n\n /** When openStream() is used, we read from RTSP directly (not push mode) */\n private pullMode = false\n\n // Cached dimensions — won't change between frames from the same FFmpeg session\n private cachedWidth = 0\n private cachedHeight = 0\n\n // Stats tracking\n private inputPackets = 0\n private outputFrames = 0\n private droppedFrames = 0\n private totalDecodeTimeMs = 0\n private decodeCount = 0\n private startTime = Date.now()\n\n private readonly hwaccelPref: HwAccelPref\n private readonly hwaccelResolver: IKernelHwAccel | null\n /** The backend we actually passed to ffmpeg `-hwaccel` — `'none'` = software. */\n private activeHwAccel: 'none' | HwAccelBackend = 'none'\n\n /**\n * Backend resolution is async (calls into `ctx.kernel.hwaccel`), but\n * `pushPacket` is sync — we kick the resolve off in the constructor\n * and cache the result. By the time the first keyframe arrives\n * (~30ms for RTSP), the resolver has completed (it's ultimately\n * `os.platform()` + file checks → sub-ms). If `ensurePushProcess`\n * fires before resolve settles, it skips hwaccel for that spawn;\n * reconnect loop gets the flag on subsequent sessions.\n */\n private resolvedBackend: HwAccelBackend | null = null\n\n constructor(\n config: DecoderSessionConfig,\n logger: IScopedLogger = noopLogger,\n options?: FfmpegDecoderSessionOptions,\n ) {\n this.config = { ...config }\n this.logger = logger\n this.frameDropper = new FrameDropper(config.maxFps)\n this.hwaccelPref = options?.hwaccel ?? 'auto'\n this.hwaccelResolver = options?.hwaccelResolver ?? null\n // Pre-warm backend resolution so push-mode spawn doesn't have to\n // await. openStream() calls the async path directly.\n void this.resolveHwAccelBackend().then((b) => { this.resolvedBackend = b })\n // Don't spawn push-mode ffmpeg here; wait for openStream() or first pushPacket()\n }\n\n /**\n * Resolve the preferred backend for this host and return the first\n * hit, or `null` when software is requested / nothing available.\n * FFmpeg CLI accepts the `-hwaccel <name>` identifier directly.\n */\n private async resolveHwAccelBackend(): Promise<HwAccelBackend | null> {\n if (this.hwaccelPref === 'none') return null\n const explicit: HwAccelBackend | null =\n this.hwaccelPref === 'auto' ? null : this.hwaccelPref\n if (!this.hwaccelResolver) return explicit\n const resolution = await this.hwaccelResolver.resolve(explicit)\n return resolution.preferred[0] ?? null\n }\n\n /**\n * Open an RTSP stream directly — ffmpeg reads from the URL and outputs JPEG frames.\n * This avoids the raw h264 pipe which loses SPS/PPS metadata on some cameras.\n */\n async openStream(url: string): Promise<void> {\n if (this.destroyed) return\n this.pullMode = true\n\n this.killFfmpeg()\n this.outputBuffer = Buffer.alloc(0)\n this.cachedWidth = 0\n this.cachedHeight = 0\n\n // Resolve + prepend `-hwaccel <backend>` before input when the\n // host supports any backend. `-hwaccel` must come before `-i` in\n // the argv so ffmpeg knows to pick a HW-capable decoder. If the\n // backend is unavailable or fails at spawn, ffmpeg exits with an\n // error and the session logs it; the reconnect loop at a higher\n // layer will get triggered.\n const backend = await this.resolveHwAccelBackend()\n const hwArgs: string[] = backend ? ['-hwaccel', backend] : []\n this.activeHwAccel = backend ?? 'none'\n\n const args = [\n '-hide_banner', '-loglevel', 'error',\n ...hwArgs,\n '-fflags', '+nobuffer+flush_packets',\n '-flags', 'low_delay',\n '-probesize', '32768',\n '-analyzeduration', '500000',\n '-rtsp_transport', 'tcp',\n '-i', url,\n '-an', // no audio\n ]\n\n if (this.config.scale > 1) {\n args.push('-vf', `scale=iw/${this.config.scale}:ih/${this.config.scale}`)\n }\n\n args.push('-f', 'image2pipe', '-vcodec', 'mjpeg', '-q:v', '3', '-threads', '1', 'pipe:1')\n\n // Software decode via ffmpeg CLI — no `-hwaccel` flag is passed.\n // If hwaccel gets wired (e.g. `-hwaccel videotoolbox` on macOS),\n // update `hwAccel` below to the actual backend used.\n this.logger.info('Opening RTSP stream directly', { meta: { url: maskUrlCredentials(url), hwAccel: this.activeHwAccel } })\n this.process = spawn('ffmpeg', args, { stdio: ['pipe', 'pipe', 'pipe'] })\n\n this.process.stdin?.on('error', () => {})\n\n this.process.stdout?.on('data', (chunk: Buffer) => {\n this.handleOutputData(chunk)\n })\n\n this.process.stderr?.on('data', (data: Buffer) => {\n const line = data.toString().trim()\n if (line) this.logger.warn('ffmpeg decoder stderr', { meta: { line } })\n })\n\n this.process.on('error', (err: Error) => {\n this.logger.error('FFmpeg decoder spawn error', { meta: { error: err.message } })\n })\n\n this.process.on('close', (code, signal) => {\n this.logger.warn('FFmpeg decoder exited', { meta: { code, signal, frames: this.outputFrames } })\n if (!this.destroyed) {\n this.process = null\n }\n })\n }\n\n private ensurePushProcess(): void {\n if (this.process || this.destroyed || this.pullMode) return\n\n const inputFormat = this.config.codec === 'h265' || this.config.codec === 'hevc'\n ? 'hevc'\n : this.config.codec === 'mjpeg' ? 'mjpeg' : 'h264'\n\n // Read the pre-warmed hwaccel backend set by the constructor's\n // fire-and-forget resolve. Null → software (either explicit\n // 'none', no resolver, or resolve hasn't completed yet on the\n // very first spawn after session creation).\n const hwArgs: string[] = this.resolvedBackend ? ['-hwaccel', this.resolvedBackend] : []\n this.activeHwAccel = this.resolvedBackend ?? 'none'\n\n const args = [\n '-hide_banner', '-loglevel', 'error',\n ...hwArgs,\n '-fflags', '+nobuffer+flush_packets',\n '-flags', 'low_delay',\n '-probesize', '32768',\n '-analyzeduration', '500000',\n '-f', inputFormat, '-i', 'pipe:0',\n ]\n\n if (this.config.scale > 1) {\n args.push('-vf', `scale=iw/${this.config.scale}:ih/${this.config.scale}`)\n }\n\n args.push('-f', 'image2pipe', '-vcodec', 'mjpeg', '-q:v', '3', '-threads', '1', 'pipe:1')\n\n this.logger.info('Spawning push-mode ffmpeg decoder', {\n meta: { codec: this.config.codec, hwAccel: this.activeHwAccel },\n })\n this.process = spawn('ffmpeg', args)\n this.process.stdin?.on('error', () => {})\n\n this.process.stdout?.on('data', (chunk: Buffer) => {\n this.handleOutputData(chunk)\n })\n\n this.process.stderr?.on('data', (data: Buffer) => {\n const line = data.toString().trim()\n if (line) this.logger.warn('ffmpeg decoder stderr', { meta: { line } })\n })\n\n this.process.on('error', (err: Error) => {\n this.logger.error('FFmpeg decoder spawn error', { meta: { error: err.message } })\n })\n\n this.process.on('close', (code, signal) => {\n this.logger.warn('FFmpeg decoder exited', { meta: { code, signal, packets: this.inputPackets, frames: this.outputFrames } })\n if (!this.destroyed) {\n this.process = null\n }\n })\n }\n\n private killFfmpeg(): void {\n if (this.process) {\n try {\n this.process.kill('SIGKILL')\n } catch {\n // already dead\n }\n this.process = null\n }\n }\n\n private handleOutputData(chunk: Buffer): void {\n this.outputBuffer = Buffer.concat([this.outputBuffer, chunk])\n\n // Extract complete JPEG frames from the buffer\n let searchFrom = 0\n while (true) {\n const soiIndex = this.outputBuffer.indexOf(SOI, searchFrom)\n if (soiIndex === -1) break\n\n const eoiIndex = this.outputBuffer.indexOf(EOI, soiIndex + 2)\n if (eoiIndex === -1) break\n\n const frameEnd = eoiIndex + 2\n const jpegData = this.outputBuffer.subarray(soiIndex, frameEnd)\n\n // Advance past the consumed frame\n searchFrom = frameEnd\n\n this.emitFrame(Buffer.from(jpegData))\n }\n\n // Keep only unprocessed tail\n if (searchFrom > 0) {\n this.outputBuffer = Buffer.from(this.outputBuffer.subarray(searchFrom))\n }\n }\n\n private emitFrame(data: Buffer): void {\n const decodeStart = Date.now()\n\n if (!this.frameDropper.shouldKeep()) {\n this.droppedFrames++\n return\n }\n\n const decodeTime = Date.now() - decodeStart\n this.totalDecodeTimeMs += decodeTime\n this.decodeCount++\n this.outputFrames++\n\n // Only parse dimensions on first frame or after FFmpeg restart (cached=0)\n if (this.cachedWidth === 0) {\n const dims = parseJpegDimensions(data)\n this.cachedWidth = dims.width\n this.cachedHeight = dims.height\n this.logger.info('First decoded frame', { meta: { width: dims.width, height: dims.height, format: 'jpeg', bytes: data.length } })\n }\n\n const frame: DecodedFrame = {\n data,\n width: this.cachedWidth,\n height: this.cachedHeight,\n format: 'jpeg',\n timestamp: Date.now(),\n }\n\n for (const cb of this.frameCallbacks) {\n cb(frame)\n }\n }\n\n pushPacket(packet: EncodedPacket): void {\n if (this.destroyed || this.pullMode) return\n this.ensurePushProcess()\n if (!this.process?.stdin) return\n this.inputPackets++\n try {\n this.process.stdin.write(packet.data)\n } catch {\n // stdin may be closed if ffmpeg crashed\n }\n }\n\n onFrame(callback: (frame: DecodedFrame) => void): Unsubscribe {\n this.frameCallbacks.add(callback)\n return () => {\n this.frameCallbacks.delete(callback)\n }\n }\n\n updateConfig(update: Partial<DecoderSessionConfig>): void {\n this.config = { ...this.config, ...update }\n if (update.maxFps !== undefined) {\n this.frameDropper.setMaxFps(update.maxFps)\n }\n }\n\n async destroy(): Promise<void> {\n if (this.destroyed) return\n this.destroyed = true\n this.killFfmpeg()\n this.frameCallbacks.clear()\n }\n\n getStats(): DecoderStats {\n const uptimeSec = Math.max((Date.now() - this.startTime) / 1000, 1)\n return {\n inputFps: this.inputPackets / uptimeSec,\n outputFps: this.outputFrames / uptimeSec,\n avgDecodeTimeMs: this.decodeCount > 0 ? this.totalDecodeTimeMs / this.decodeCount : 0,\n droppedFrames: this.droppedFrames,\n }\n }\n}\n\n/**\n * Parse JPEG SOF0/SOF2 marker to extract image dimensions.\n * Scans for 0xFF 0xC0 (baseline) or 0xFF 0xC2 (progressive) markers.\n * Returns {width: 0, height: 0} if not found.\n */\nfunction parseJpegDimensions(data: Buffer): { width: number; height: number } {\n for (let i = 0; i < data.length - 8; i++) {\n if (data[i] === 0xFF && (data[i + 1] === 0xC0 || data[i + 1] === 0xC2)) {\n const height = (data[i + 5]! << 8) | data[i + 6]!\n const width = (data[i + 7]! << 8) | data[i + 8]!\n return { width, height }\n }\n }\n return { width: 0, height: 0 }\n}\n","export class FrameDropper {\n private intervalMs: number\n private lastPassedAt = -Infinity\n\n constructor(maxFps: number) {\n this.intervalMs = maxFps > 0 ? 1000 / maxFps : 0\n }\n\n shouldKeep(): boolean {\n if (this.intervalMs === 0) return true\n\n const now = Date.now()\n if (now - this.lastPassedAt >= this.intervalMs) {\n this.lastPassedAt = now\n return true\n }\n return false\n }\n\n setMaxFps(maxFps: number): void {\n this.intervalMs = maxFps > 0 ? 1000 / maxFps : 0\n }\n}\n","import type { IDecoderProvider, DecoderSessionConfig, IDecoderSession, IScopedLogger } from '@camstack/types'\nimport { FfmpegDecoderSession } from './ffmpeg-decoder-session'\n\nconst SUPPORTED_CODECS = new Set(['h264', 'h265', 'hevc', 'mjpeg'])\n\nexport class FfmpegDecoderProvider implements IDecoderProvider {\n readonly id = 'ffmpeg'\n readonly name = 'FFmpeg Decoder'\n readonly isPullMode = false\n /** Software decoder — used as fallback when hardware decoders are unavailable. */\n readonly priority = 50\n private logger: IScopedLogger | null = null\n\n setLogger(logger: IScopedLogger): void {\n this.logger = logger\n }\n\n async supportsCodec(input: { codec: string }): Promise<boolean> {\n return SUPPORTED_CODECS.has(input.codec)\n }\n\n async createSession(config: DecoderSessionConfig): Promise<IDecoderSession> {\n return new FfmpegDecoderSession(config, this.logger ?? undefined)\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../src/frame-dropper.ts","../src/ffmpeg-decoder-session.ts","../src/addon/index.ts","../src/ffmpeg-decoder-provider.ts"],"sourcesContent":["export class FrameDropper {\n private intervalMs: number\n private lastPassedAt = -Infinity\n\n constructor(maxFps: number) {\n this.intervalMs = maxFps > 0 ? 1000 / maxFps : 0\n }\n\n shouldKeep(): boolean {\n if (this.intervalMs === 0) return true\n\n const now = Date.now()\n if (now - this.lastPassedAt >= this.intervalMs) {\n this.lastPassedAt = now\n return true\n }\n return false\n }\n\n setMaxFps(maxFps: number): void {\n this.intervalMs = maxFps > 0 ? 1000 / maxFps : 0\n }\n}\n","import { spawn, type ChildProcess } from 'node:child_process'\nimport type {\n IDecoderSession, DecoderSessionConfig, DecoderStats, EncodedPacket,\n DecodedFrame, Unsubscribe, IScopedLogger,\n HwAccelBackend, IKernelHwAccel,\n} from '@camstack/types'\nimport { maskUrlCredentials } from '@camstack/types'\nimport { FrameDropper } from './frame-dropper'\n\nexport type HwAccelPref = 'auto' | 'none' | HwAccelBackend\n\nexport interface FfmpegDecoderSessionOptions {\n /** Addon-level hwaccel preference — per-agent. Default `'auto'`. */\n readonly hwaccel?: HwAccelPref\n /** Kernel hwaccel resolver — `ctx.kernel.hwaccel` passed from the addon. */\n readonly hwaccelResolver?: IKernelHwAccel\n}\n\n/** Minimal no-op logger for default parameter */\nconst noopLogger: IScopedLogger = {\n debug() {},\n info() {},\n warn() {},\n error() {},\n child() { return noopLogger },\n withTags() { return noopLogger },\n}\n\nconst SOI = Buffer.from([0xff, 0xd8])\nconst EOI = Buffer.from([0xff, 0xd9])\n\nexport class FfmpegDecoderSession implements IDecoderSession {\n private config: DecoderSessionConfig\n private frameDropper: FrameDropper\n private process: ChildProcess | null = null\n private frameCallbacks = new Set<(frame: DecodedFrame) => void>()\n private outputBuffer = Buffer.alloc(0)\n private destroyed = false\n private readonly logger: IScopedLogger\n\n /** When openStream() is used, we read from RTSP directly (not push mode) */\n private pullMode = false\n\n // Cached dimensions — won't change between frames from the same FFmpeg session\n private cachedWidth = 0\n private cachedHeight = 0\n\n // Stats tracking\n private inputPackets = 0\n private outputFrames = 0\n private droppedFrames = 0\n private totalDecodeTimeMs = 0\n private decodeCount = 0\n private startTime = Date.now()\n\n private readonly hwaccelPref: HwAccelPref\n private readonly hwaccelResolver: IKernelHwAccel | null\n /** The backend we actually passed to ffmpeg `-hwaccel` — `'none'` = software. */\n private activeHwAccel: 'none' | HwAccelBackend = 'none'\n\n /**\n * Backend resolution is async (calls into `ctx.kernel.hwaccel`), but\n * `pushPacket` is sync — we kick the resolve off in the constructor\n * and cache the result. By the time the first keyframe arrives\n * (~30ms for RTSP), the resolver has completed (it's ultimately\n * `os.platform()` + file checks → sub-ms). If `ensurePushProcess`\n * fires before resolve settles, it skips hwaccel for that spawn;\n * reconnect loop gets the flag on subsequent sessions.\n */\n private resolvedBackend: HwAccelBackend | null = null\n\n constructor(\n config: DecoderSessionConfig,\n logger: IScopedLogger = noopLogger,\n options?: FfmpegDecoderSessionOptions,\n ) {\n this.config = { ...config }\n this.logger = logger\n this.frameDropper = new FrameDropper(config.maxFps)\n this.hwaccelPref = options?.hwaccel ?? 'auto'\n this.hwaccelResolver = options?.hwaccelResolver ?? null\n // Pre-warm backend resolution so push-mode spawn doesn't have to\n // await. openStream() calls the async path directly.\n void this.resolveHwAccelBackend().then((b) => { this.resolvedBackend = b })\n // Don't spawn push-mode ffmpeg here; wait for openStream() or first pushPacket()\n }\n\n /**\n * Resolve the preferred backend for this host and return the first\n * hit, or `null` when software is requested / nothing available.\n * FFmpeg CLI accepts the `-hwaccel <name>` identifier directly.\n */\n private async resolveHwAccelBackend(): Promise<HwAccelBackend | null> {\n if (this.hwaccelPref === 'none') return null\n const explicit: HwAccelBackend | null =\n this.hwaccelPref === 'auto' ? null : this.hwaccelPref\n if (!this.hwaccelResolver) return explicit\n const resolution = await this.hwaccelResolver.resolve(explicit)\n return resolution.preferred[0] ?? null\n }\n\n /**\n * Open an RTSP stream directly — ffmpeg reads from the URL and outputs JPEG frames.\n * This avoids the raw h264 pipe which loses SPS/PPS metadata on some cameras.\n */\n async openStream(url: string): Promise<void> {\n if (this.destroyed) return\n this.pullMode = true\n\n this.killFfmpeg()\n this.outputBuffer = Buffer.alloc(0)\n this.cachedWidth = 0\n this.cachedHeight = 0\n\n // Resolve + prepend `-hwaccel <backend>` before input when the\n // host supports any backend. `-hwaccel` must come before `-i` in\n // the argv so ffmpeg knows to pick a HW-capable decoder. If the\n // backend is unavailable or fails at spawn, ffmpeg exits with an\n // error and the session logs it; the reconnect loop at a higher\n // layer will get triggered.\n const backend = await this.resolveHwAccelBackend()\n const hwArgs: string[] = backend ? ['-hwaccel', backend] : []\n this.activeHwAccel = backend ?? 'none'\n\n const args = [\n '-hide_banner', '-loglevel', 'error',\n ...hwArgs,\n '-fflags', '+nobuffer+flush_packets',\n '-flags', 'low_delay',\n '-probesize', '32768',\n '-analyzeduration', '500000',\n '-rtsp_transport', 'tcp',\n '-i', url,\n '-an', // no audio\n ]\n\n if (this.config.scale > 1) {\n args.push('-vf', `scale=iw/${this.config.scale}:ih/${this.config.scale}`)\n }\n\n args.push('-f', 'image2pipe', '-vcodec', 'mjpeg', '-q:v', '3', '-threads', '1', 'pipe:1')\n\n // Software decode via ffmpeg CLI — no `-hwaccel` flag is passed.\n // If hwaccel gets wired (e.g. `-hwaccel videotoolbox` on macOS),\n // update `hwAccel` below to the actual backend used.\n this.logger.info('Opening RTSP stream directly', { meta: { url: maskUrlCredentials(url), hwAccel: this.activeHwAccel } })\n this.process = spawn('ffmpeg', args, { stdio: ['pipe', 'pipe', 'pipe'] })\n\n this.process.stdin?.on('error', () => {})\n\n this.process.stdout?.on('data', (chunk: Buffer) => {\n this.handleOutputData(chunk)\n })\n\n this.process.stderr?.on('data', (data: Buffer) => {\n const line = data.toString().trim()\n if (line) this.logger.warn('ffmpeg decoder stderr', { meta: { line } })\n })\n\n this.process.on('error', (err: Error) => {\n this.logger.error('FFmpeg decoder spawn error', { meta: { error: err.message } })\n })\n\n this.process.on('close', (code, signal) => {\n this.logger.warn('FFmpeg decoder exited', { meta: { code, signal, frames: this.outputFrames } })\n if (!this.destroyed) {\n this.process = null\n }\n })\n }\n\n private ensurePushProcess(): void {\n if (this.process || this.destroyed || this.pullMode) return\n\n const inputFormat = this.config.codec === 'h265' || this.config.codec === 'hevc'\n ? 'hevc'\n : this.config.codec === 'mjpeg' ? 'mjpeg' : 'h264'\n\n // Read the pre-warmed hwaccel backend set by the constructor's\n // fire-and-forget resolve. Null → software (either explicit\n // 'none', no resolver, or resolve hasn't completed yet on the\n // very first spawn after session creation).\n const hwArgs: string[] = this.resolvedBackend ? ['-hwaccel', this.resolvedBackend] : []\n this.activeHwAccel = this.resolvedBackend ?? 'none'\n\n const args = [\n '-hide_banner', '-loglevel', 'error',\n ...hwArgs,\n '-fflags', '+nobuffer+flush_packets',\n '-flags', 'low_delay',\n '-probesize', '32768',\n '-analyzeduration', '500000',\n '-f', inputFormat, '-i', 'pipe:0',\n ]\n\n if (this.config.scale > 1) {\n args.push('-vf', `scale=iw/${this.config.scale}:ih/${this.config.scale}`)\n }\n\n args.push('-f', 'image2pipe', '-vcodec', 'mjpeg', '-q:v', '3', '-threads', '1', 'pipe:1')\n\n this.logger.info('Spawning push-mode ffmpeg decoder', {\n meta: { codec: this.config.codec, hwAccel: this.activeHwAccel },\n })\n this.process = spawn('ffmpeg', args)\n this.process.stdin?.on('error', () => {})\n\n this.process.stdout?.on('data', (chunk: Buffer) => {\n this.handleOutputData(chunk)\n })\n\n this.process.stderr?.on('data', (data: Buffer) => {\n const line = data.toString().trim()\n if (line) this.logger.warn('ffmpeg decoder stderr', { meta: { line } })\n })\n\n this.process.on('error', (err: Error) => {\n this.logger.error('FFmpeg decoder spawn error', { meta: { error: err.message } })\n })\n\n this.process.on('close', (code, signal) => {\n this.logger.warn('FFmpeg decoder exited', { meta: { code, signal, packets: this.inputPackets, frames: this.outputFrames } })\n if (!this.destroyed) {\n this.process = null\n }\n })\n }\n\n private killFfmpeg(): void {\n if (this.process) {\n try {\n this.process.kill('SIGKILL')\n } catch {\n // already dead\n }\n this.process = null\n }\n }\n\n private handleOutputData(chunk: Buffer): void {\n this.outputBuffer = Buffer.concat([this.outputBuffer, chunk])\n\n // Extract complete JPEG frames from the buffer\n let searchFrom = 0\n while (true) {\n const soiIndex = this.outputBuffer.indexOf(SOI, searchFrom)\n if (soiIndex === -1) break\n\n const eoiIndex = this.outputBuffer.indexOf(EOI, soiIndex + 2)\n if (eoiIndex === -1) break\n\n const frameEnd = eoiIndex + 2\n const jpegData = this.outputBuffer.subarray(soiIndex, frameEnd)\n\n // Advance past the consumed frame\n searchFrom = frameEnd\n\n this.emitFrame(Buffer.from(jpegData))\n }\n\n // Keep only unprocessed tail\n if (searchFrom > 0) {\n this.outputBuffer = Buffer.from(this.outputBuffer.subarray(searchFrom))\n }\n }\n\n private emitFrame(data: Buffer): void {\n const decodeStart = Date.now()\n\n if (!this.frameDropper.shouldKeep()) {\n this.droppedFrames++\n return\n }\n\n const decodeTime = Date.now() - decodeStart\n this.totalDecodeTimeMs += decodeTime\n this.decodeCount++\n this.outputFrames++\n\n // Only parse dimensions on first frame or after FFmpeg restart (cached=0)\n if (this.cachedWidth === 0) {\n const dims = parseJpegDimensions(data)\n this.cachedWidth = dims.width\n this.cachedHeight = dims.height\n this.logger.info('First decoded frame', { meta: { width: dims.width, height: dims.height, format: 'jpeg', bytes: data.length } })\n }\n\n const frame: DecodedFrame = {\n data,\n width: this.cachedWidth,\n height: this.cachedHeight,\n format: 'jpeg',\n timestamp: Date.now(),\n }\n\n for (const cb of this.frameCallbacks) {\n cb(frame)\n }\n }\n\n pushPacket(packet: EncodedPacket): void {\n if (this.destroyed || this.pullMode) return\n this.ensurePushProcess()\n if (!this.process?.stdin) return\n this.inputPackets++\n try {\n this.process.stdin.write(packet.data)\n } catch {\n // stdin may be closed if ffmpeg crashed\n }\n }\n\n onFrame(callback: (frame: DecodedFrame) => void): Unsubscribe {\n this.frameCallbacks.add(callback)\n return () => {\n this.frameCallbacks.delete(callback)\n }\n }\n\n updateConfig(update: Partial<DecoderSessionConfig>): void {\n this.config = { ...this.config, ...update }\n if (update.maxFps !== undefined) {\n this.frameDropper.setMaxFps(update.maxFps)\n }\n }\n\n async destroy(): Promise<void> {\n if (this.destroyed) return\n this.destroyed = true\n this.killFfmpeg()\n this.frameCallbacks.clear()\n }\n\n getStats(): DecoderStats {\n const uptimeSec = Math.max((Date.now() - this.startTime) / 1000, 1)\n return {\n inputFps: this.inputPackets / uptimeSec,\n outputFps: this.outputFrames / uptimeSec,\n avgDecodeTimeMs: this.decodeCount > 0 ? this.totalDecodeTimeMs / this.decodeCount : 0,\n droppedFrames: this.droppedFrames,\n }\n }\n}\n\n/**\n * Parse JPEG SOF0/SOF2 marker to extract image dimensions.\n * Scans for 0xFF 0xC0 (baseline) or 0xFF 0xC2 (progressive) markers.\n * Returns {width: 0, height: 0} if not found.\n */\nfunction parseJpegDimensions(data: Buffer): { width: number; height: number } {\n for (let i = 0; i < data.length - 8; i++) {\n if (data[i] === 0xFF && (data[i + 1] === 0xC0 || data[i + 1] === 0xC2)) {\n const height = (data[i + 5]! << 8) | data[i + 6]!\n const width = (data[i + 7]! << 8) | data[i + 8]!\n return { width, height }\n }\n }\n return { width: 0, height: 0 }\n}\n","import { randomUUID } from 'node:crypto'\nimport type {\n DecoderHwAccelConfig,\n HwAccelChoice,\n ProviderRegistration,\n IDecoderSession,\n DecoderStats,\n IDecoderCapProvider,\n FrameFormat,\n} from '@camstack/types'\nimport {\n BaseAddon,\n DEFAULT_DECODER_HWACCEL_CONFIG,\n HWACCEL_OPTIONS,\n decoderCapability,\n RingBuffer,\n} from '@camstack/types'\nimport { FfmpegDecoderSession } from '../ffmpeg-decoder-session.js'\n\n/** Cap-compatible frame shape — format must match DecodedFrameSchema enum values. */\ntype CapDecodedFrame = {\n data: Uint8Array<ArrayBuffer>\n width: number\n height: number\n format: FrameFormat\n timestamp: number\n}\n\n/** Cap-compatible session config — matches DecoderSessionConfigSchema output type. */\ntype CapDecoderSessionConfig = {\n codec: string\n maxFps: number\n outputFormat: FrameFormat\n scale: number\n width?: number\n height?: number\n}\n\n/** Cap-compatible encoded packet — data is Uint8Array matching EncodedPacketSchema. */\ntype CapEncodedPacket = {\n type: 'video' | 'audio'\n data: Uint8Array<ArrayBuffer>\n pts: number\n dts: number\n keyframe: boolean\n codec: string\n}\n\nconst FRAME_BUFFER_CAPACITY = 32\n\n/** Per-session metadata recorded at creation time, surfaced via `listActiveSessions`. */\ninterface SessionMeta {\n readonly codec: string\n readonly outputFormat: string\n readonly createdAtMs: number\n}\n\n/**\n * FFmpeg decoder addon — H264/HEVC/MJPEG decode via ffmpeg child\n * process.\n *\n * Phase 2d of the pipeline-settings migration — ffmpeg decoder owns\n * its own `hwaccel` choice + `probedBestHwaccel` hint. Sessions\n * resolve the effective backend from this addon's global settings\n * instead of the orchestrator's legacy `AgentPipelineSettings.hwaccel`.\n * Session constructor still appends `-hwaccel <name>` to the ffmpeg\n * argv as before.\n *\n * Implements the sessionId-based IDecoderCapProvider cap interface.\n * Sessions are managed internally via a Map; frames are polled via\n * RingBuffer.\n */\nexport default class DecoderFfmpegAddon extends BaseAddon<DecoderHwAccelConfig> implements IDecoderCapProvider {\n private readonly sessions = new Map<string, IDecoderSession>()\n private readonly frameBuffers = new Map<string, RingBuffer<CapDecodedFrame>>()\n private readonly unsubscribers = new Map<string, () => void>()\n private readonly sessionMeta = new Map<string, SessionMeta>()\n\n constructor() { super(DEFAULT_DECODER_HWACCEL_CONFIG) }\n\n protected globalSettingsSchema() {\n return this.schema({\n sections: [{\n id: 'hwaccel',\n title: 'Hardware acceleration',\n description: 'Backend appended as `-hwaccel <name>` to the ffmpeg argv. \"Auto\" defers to the probed best; concrete backends force it. Changes apply to NEW sessions — existing sessions keep the backend they were created with.',\n fields: [\n this.field({\n type: 'select',\n key: 'hwaccel',\n label: 'Preferred backend',\n options: [...HWACCEL_OPTIONS],\n default: 'auto',\n immediate: true,\n }),\n this.field({\n type: 'text',\n key: 'probedBestHwaccel',\n label: 'Probed best',\n description: 'Auto-detected best backend on this host. Call `reprobeHwaccel` to refresh.',\n disabled: true,\n default: '',\n }),\n ],\n }],\n })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.ctx.logger.info('FFmpeg decoder addon initialized')\n // Auto-seed probedBestHwaccel on first boot.\n if (!this.config.probedBestHwaccel) {\n this.reprobeHwaccel().catch((err: unknown) => {\n this.ctx.logger.warn('ffmpeg: auto-reprobe hwaccel failed', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n })\n }\n return [{ capability: decoderCapability, provider: this }]\n }\n\n /**\n * Resolve the effective hwaccel backend for a new session. Reads\n * this addon's own `hwaccel` setting. `'auto'` defers to the\n * session's local resolver (`ctx.kernel.hwaccel`).\n */\n private resolveHwAccelPref(): HwAccelChoice {\n return this.config.hwaccel\n }\n\n /**\n * Re-run the platform probe on this host and persist the detected\n * backend as `probedBestHwaccel`. Operator `hwaccel` setting is not\n * touched — only the hint.\n */\n async reprobeHwaccel(): Promise<{ backend: string }> {\n const resolver = this.ctx.kernel.hwaccel\n if (!resolver) {\n this.ctx.logger.warn('reprobeHwaccel: no kernel hwaccel resolver — returning none')\n await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: 'none' })\n return { backend: 'none' }\n }\n try {\n const res = await resolver.resolve()\n const backend = (res.preferred[0] ?? 'none') as string\n await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: backend })\n this.ctx.logger.info('reprobeHwaccel: wrote probedBestHwaccel', {\n meta: { backend, rationale: res.rationale, preferred: res.preferred },\n })\n return { backend }\n } catch (err) {\n this.ctx.logger.warn('reprobeHwaccel failed', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n await this.ctx.settings?.writeAddonStore({ probedBestHwaccel: 'none' })\n return { backend: 'none' }\n }\n }\n\n async supportsCodec(input: { codec: string }): Promise<boolean> {\n return ['h264', 'h265', 'hevc', 'mjpeg'].includes(input.codec.toLowerCase())\n }\n\n async getInfo(): Promise<{ id: string; name: string; isPullMode?: boolean; priority?: number }> {\n return {\n id: 'decoder-ffmpeg',\n name: 'Decoder (FFmpeg)',\n isPullMode: false,\n priority: 50,\n }\n }\n\n async createSession(config: CapDecoderSessionConfig): Promise<{ sessionId: string; nodeId: string }> {\n const sessionId = randomUUID()\n const hwaccel = this.resolveHwAccelPref()\n const session = new FfmpegDecoderSession(config, this.ctx.logger, {\n hwaccel,\n hwaccelResolver: this.ctx.kernel.hwaccel,\n })\n const ringBuffer = new RingBuffer<CapDecodedFrame>(FRAME_BUFFER_CAPACITY)\n\n const unsub = session.onFrame((frame) => {\n // Map internal DecodedFrame to cap-compatible shape.\n const { format } = frame\n if (format !== 'jpeg' && format !== 'rgb' && format !== 'bgr' && format !== 'yuv420' && format !== 'gray') return\n // Copy frame data into a fresh ArrayBuffer so the cap-facing Uint8Array\n // has a concrete ArrayBuffer (not a SharedArrayBuffer / Buffer backing).\n const arrayBuf = new ArrayBuffer(frame.data.byteLength)\n new Uint8Array(arrayBuf).set(frame.data)\n const capFrame: CapDecodedFrame = {\n data: new Uint8Array(arrayBuf),\n width: frame.width,\n height: frame.height,\n format,\n timestamp: frame.timestamp,\n }\n ringBuffer.push(capFrame)\n })\n\n this.sessions.set(sessionId, session)\n this.frameBuffers.set(sessionId, ringBuffer)\n this.unsubscribers.set(sessionId, unsub)\n this.sessionMeta.set(sessionId, {\n codec: config.codec,\n outputFormat: config.outputFormat,\n createdAtMs: Date.now(),\n })\n\n this.ctx.logger.info('ffmpeg: created session', { meta: { sessionId, codec: config.codec, hwaccelPref: hwaccel } })\n return { sessionId, nodeId: this.ctx.kernel.localNodeId ?? 'local' }\n }\n\n async destroySession(input: { sessionId: string }): Promise<void> {\n const { sessionId } = input\n const session = this.sessions.get(sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${sessionId}`)\n }\n\n const unsub = this.unsubscribers.get(sessionId)\n if (unsub) unsub()\n\n await session.destroy()\n\n this.sessions.delete(sessionId)\n this.frameBuffers.delete(sessionId)\n this.unsubscribers.delete(sessionId)\n this.sessionMeta.delete(sessionId)\n\n this.ctx.logger.info('ffmpeg: destroyed session', { meta: { sessionId } })\n }\n\n async listActiveSessions(): Promise<readonly { sessionId: string; codec: string; outputFormat: string; createdAtMs: number }[]> {\n const out: Array<{ sessionId: string; codec: string; outputFormat: string; createdAtMs: number }> = []\n for (const [sessionId, meta] of this.sessionMeta) {\n out.push({ sessionId, codec: meta.codec, outputFormat: meta.outputFormat, createdAtMs: meta.createdAtMs })\n }\n return out\n }\n\n async pushPacket(input: { sessionId: string; packet: CapEncodedPacket }): Promise<void> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n // Convert Uint8Array to Buffer at the cap boundary before passing to the internal session.\n session.pushPacket({\n ...input.packet,\n data: Buffer.from(input.packet.data),\n })\n }\n\n async openStream(input: { sessionId: string; url: string }): Promise<void> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n if (session.openStream) {\n await session.openStream(input.url)\n }\n }\n\n async pullFrames(input: { sessionId: string; maxCount: number }): Promise<CapDecodedFrame[]> {\n const ringBuffer = this.frameBuffers.get(input.sessionId)\n if (!ringBuffer) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n return ringBuffer.drain(input.maxCount)\n }\n\n async updateConfig(input: { sessionId: string; config: Partial<CapDecoderSessionConfig> }): Promise<void> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n session.updateConfig(input.config)\n }\n\n async getStats(input: { sessionId: string }): Promise<DecoderStats> {\n const session = this.sessions.get(input.sessionId)\n if (!session) {\n throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`)\n }\n return session.getStats()\n }\n\n protected async onShutdown(): Promise<void> {\n this.ctx.logger.info('FFmpeg decoder addon shutdown — destroying all sessions')\n const destroyPromises: Promise<void>[] = []\n for (const [sessionId, session] of this.sessions) {\n const unsub = this.unsubscribers.get(sessionId)\n if (unsub) unsub()\n destroyPromises.push(session.destroy())\n }\n await Promise.all(destroyPromises)\n this.sessions.clear()\n this.frameBuffers.clear()\n this.sessionMeta.clear()\n this.unsubscribers.clear()\n }\n}\n","import type { IDecoderProvider, DecoderSessionConfig, IDecoderSession, IScopedLogger } from '@camstack/types'\nimport { FfmpegDecoderSession } from './ffmpeg-decoder-session'\n\nconst SUPPORTED_CODECS = new Set(['h264', 'h265', 'hevc', 'mjpeg'])\n\nexport class FfmpegDecoderProvider implements IDecoderProvider {\n readonly id = 'ffmpeg'\n readonly name = 'FFmpeg Decoder'\n readonly isPullMode = false\n /** Software decoder — used as fallback when hardware decoders are unavailable. */\n readonly priority = 50\n private logger: IScopedLogger | null = null\n\n setLogger(logger: IScopedLogger): void {\n this.logger = logger\n }\n\n async supportsCodec(input: { codec: string }): Promise<boolean> {\n return SUPPORTED_CODECS.has(input.codec)\n }\n\n async createSession(config: DecoderSessionConfig): Promise<IDecoderSession> {\n return new FfmpegDecoderSession(config, this.logger ?? undefined)\n }\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-decoder-ffmpeg",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "FFmpeg-based video decoder (software decode via child process, JPEG pipe output)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"dist"
|
|
52
52
|
],
|
|
53
53
|
"scripts": {
|
|
54
|
-
"build": "
|
|
54
|
+
"build": "vite build",
|
|
55
55
|
"typecheck": "tsc --noEmit",
|
|
56
56
|
"test": "vitest run --passWithNoTests"
|
|
57
57
|
},
|
package/dist/index.d.mts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import * as _camstack_types from '@camstack/types';
|
|
2
|
-
import { BaseAddon, DecoderHwAccelConfig, IDecoderCapProvider, ProviderRegistration, FrameFormat, DecoderStats, IDecoderProvider, IScopedLogger, DecoderSessionConfig, IDecoderSession, HwAccelBackend, IKernelHwAccel, EncodedPacket, DecodedFrame, Unsubscribe } from '@camstack/types';
|
|
3
|
-
|
|
4
|
-
/** Cap-compatible frame shape — format must match DecodedFrameSchema enum values. */
|
|
5
|
-
type CapDecodedFrame = {
|
|
6
|
-
data: Uint8Array<ArrayBuffer>;
|
|
7
|
-
width: number;
|
|
8
|
-
height: number;
|
|
9
|
-
format: FrameFormat;
|
|
10
|
-
timestamp: number;
|
|
11
|
-
};
|
|
12
|
-
/** Cap-compatible session config — matches DecoderSessionConfigSchema output type. */
|
|
13
|
-
type CapDecoderSessionConfig = {
|
|
14
|
-
codec: string;
|
|
15
|
-
maxFps: number;
|
|
16
|
-
outputFormat: FrameFormat;
|
|
17
|
-
scale: number;
|
|
18
|
-
width?: number;
|
|
19
|
-
height?: number;
|
|
20
|
-
};
|
|
21
|
-
/** Cap-compatible encoded packet — data is Uint8Array matching EncodedPacketSchema. */
|
|
22
|
-
type CapEncodedPacket = {
|
|
23
|
-
type: 'video' | 'audio';
|
|
24
|
-
data: Uint8Array<ArrayBuffer>;
|
|
25
|
-
pts: number;
|
|
26
|
-
dts: number;
|
|
27
|
-
keyframe: boolean;
|
|
28
|
-
codec: string;
|
|
29
|
-
};
|
|
30
|
-
/**
|
|
31
|
-
* FFmpeg decoder addon — H264/HEVC/MJPEG decode via ffmpeg child
|
|
32
|
-
* process.
|
|
33
|
-
*
|
|
34
|
-
* Phase 2d of the pipeline-settings migration — ffmpeg decoder owns
|
|
35
|
-
* its own `hwaccel` choice + `probedBestHwaccel` hint. Sessions
|
|
36
|
-
* resolve the effective backend from this addon's global settings
|
|
37
|
-
* instead of the orchestrator's legacy `AgentPipelineSettings.hwaccel`.
|
|
38
|
-
* Session constructor still appends `-hwaccel <name>` to the ffmpeg
|
|
39
|
-
* argv as before.
|
|
40
|
-
*
|
|
41
|
-
* Implements the sessionId-based IDecoderCapProvider cap interface.
|
|
42
|
-
* Sessions are managed internally via a Map; frames are polled via
|
|
43
|
-
* RingBuffer.
|
|
44
|
-
*/
|
|
45
|
-
declare class DecoderFfmpegAddon extends BaseAddon<DecoderHwAccelConfig> implements IDecoderCapProvider {
|
|
46
|
-
private readonly sessions;
|
|
47
|
-
private readonly frameBuffers;
|
|
48
|
-
private readonly unsubscribers;
|
|
49
|
-
private readonly sessionMeta;
|
|
50
|
-
constructor();
|
|
51
|
-
protected globalSettingsSchema(): _camstack_types.ConfigUISchema;
|
|
52
|
-
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
53
|
-
/**
|
|
54
|
-
* Resolve the effective hwaccel backend for a new session. Reads
|
|
55
|
-
* this addon's own `hwaccel` setting. `'auto'` defers to the
|
|
56
|
-
* session's local resolver (`ctx.kernel.hwaccel`).
|
|
57
|
-
*/
|
|
58
|
-
private resolveHwAccelPref;
|
|
59
|
-
/**
|
|
60
|
-
* Re-run the platform probe on this host and persist the detected
|
|
61
|
-
* backend as `probedBestHwaccel`. Operator `hwaccel` setting is not
|
|
62
|
-
* touched — only the hint.
|
|
63
|
-
*/
|
|
64
|
-
reprobeHwaccel(): Promise<{
|
|
65
|
-
backend: string;
|
|
66
|
-
}>;
|
|
67
|
-
supportsCodec(input: {
|
|
68
|
-
codec: string;
|
|
69
|
-
}): Promise<boolean>;
|
|
70
|
-
getInfo(): Promise<{
|
|
71
|
-
id: string;
|
|
72
|
-
name: string;
|
|
73
|
-
isPullMode?: boolean;
|
|
74
|
-
priority?: number;
|
|
75
|
-
}>;
|
|
76
|
-
createSession(config: CapDecoderSessionConfig): Promise<{
|
|
77
|
-
sessionId: string;
|
|
78
|
-
nodeId: string;
|
|
79
|
-
}>;
|
|
80
|
-
destroySession(input: {
|
|
81
|
-
sessionId: string;
|
|
82
|
-
}): Promise<void>;
|
|
83
|
-
listActiveSessions(): Promise<readonly {
|
|
84
|
-
sessionId: string;
|
|
85
|
-
codec: string;
|
|
86
|
-
outputFormat: string;
|
|
87
|
-
createdAtMs: number;
|
|
88
|
-
}[]>;
|
|
89
|
-
pushPacket(input: {
|
|
90
|
-
sessionId: string;
|
|
91
|
-
packet: CapEncodedPacket;
|
|
92
|
-
}): Promise<void>;
|
|
93
|
-
openStream(input: {
|
|
94
|
-
sessionId: string;
|
|
95
|
-
url: string;
|
|
96
|
-
}): Promise<void>;
|
|
97
|
-
pullFrames(input: {
|
|
98
|
-
sessionId: string;
|
|
99
|
-
maxCount: number;
|
|
100
|
-
}): Promise<CapDecodedFrame[]>;
|
|
101
|
-
updateConfig(input: {
|
|
102
|
-
sessionId: string;
|
|
103
|
-
config: Partial<CapDecoderSessionConfig>;
|
|
104
|
-
}): Promise<void>;
|
|
105
|
-
getStats(input: {
|
|
106
|
-
sessionId: string;
|
|
107
|
-
}): Promise<DecoderStats>;
|
|
108
|
-
protected onShutdown(): Promise<void>;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
declare class FfmpegDecoderProvider implements IDecoderProvider {
|
|
112
|
-
readonly id = "ffmpeg";
|
|
113
|
-
readonly name = "FFmpeg Decoder";
|
|
114
|
-
readonly isPullMode = false;
|
|
115
|
-
/** Software decoder — used as fallback when hardware decoders are unavailable. */
|
|
116
|
-
readonly priority = 50;
|
|
117
|
-
private logger;
|
|
118
|
-
setLogger(logger: IScopedLogger): void;
|
|
119
|
-
supportsCodec(input: {
|
|
120
|
-
codec: string;
|
|
121
|
-
}): Promise<boolean>;
|
|
122
|
-
createSession(config: DecoderSessionConfig): Promise<IDecoderSession>;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
type HwAccelPref = 'auto' | 'none' | HwAccelBackend;
|
|
126
|
-
interface FfmpegDecoderSessionOptions {
|
|
127
|
-
/** Addon-level hwaccel preference — per-agent. Default `'auto'`. */
|
|
128
|
-
readonly hwaccel?: HwAccelPref;
|
|
129
|
-
/** Kernel hwaccel resolver — `ctx.kernel.hwaccel` passed from the addon. */
|
|
130
|
-
readonly hwaccelResolver?: IKernelHwAccel;
|
|
131
|
-
}
|
|
132
|
-
declare class FfmpegDecoderSession implements IDecoderSession {
|
|
133
|
-
private config;
|
|
134
|
-
private frameDropper;
|
|
135
|
-
private process;
|
|
136
|
-
private frameCallbacks;
|
|
137
|
-
private outputBuffer;
|
|
138
|
-
private destroyed;
|
|
139
|
-
private readonly logger;
|
|
140
|
-
/** When openStream() is used, we read from RTSP directly (not push mode) */
|
|
141
|
-
private pullMode;
|
|
142
|
-
private cachedWidth;
|
|
143
|
-
private cachedHeight;
|
|
144
|
-
private inputPackets;
|
|
145
|
-
private outputFrames;
|
|
146
|
-
private droppedFrames;
|
|
147
|
-
private totalDecodeTimeMs;
|
|
148
|
-
private decodeCount;
|
|
149
|
-
private startTime;
|
|
150
|
-
private readonly hwaccelPref;
|
|
151
|
-
private readonly hwaccelResolver;
|
|
152
|
-
/** The backend we actually passed to ffmpeg `-hwaccel` — `'none'` = software. */
|
|
153
|
-
private activeHwAccel;
|
|
154
|
-
/**
|
|
155
|
-
* Backend resolution is async (calls into `ctx.kernel.hwaccel`), but
|
|
156
|
-
* `pushPacket` is sync — we kick the resolve off in the constructor
|
|
157
|
-
* and cache the result. By the time the first keyframe arrives
|
|
158
|
-
* (~30ms for RTSP), the resolver has completed (it's ultimately
|
|
159
|
-
* `os.platform()` + file checks → sub-ms). If `ensurePushProcess`
|
|
160
|
-
* fires before resolve settles, it skips hwaccel for that spawn;
|
|
161
|
-
* reconnect loop gets the flag on subsequent sessions.
|
|
162
|
-
*/
|
|
163
|
-
private resolvedBackend;
|
|
164
|
-
constructor(config: DecoderSessionConfig, logger?: IScopedLogger, options?: FfmpegDecoderSessionOptions);
|
|
165
|
-
/**
|
|
166
|
-
* Resolve the preferred backend for this host and return the first
|
|
167
|
-
* hit, or `null` when software is requested / nothing available.
|
|
168
|
-
* FFmpeg CLI accepts the `-hwaccel <name>` identifier directly.
|
|
169
|
-
*/
|
|
170
|
-
private resolveHwAccelBackend;
|
|
171
|
-
/**
|
|
172
|
-
* Open an RTSP stream directly — ffmpeg reads from the URL and outputs JPEG frames.
|
|
173
|
-
* This avoids the raw h264 pipe which loses SPS/PPS metadata on some cameras.
|
|
174
|
-
*/
|
|
175
|
-
openStream(url: string): Promise<void>;
|
|
176
|
-
private ensurePushProcess;
|
|
177
|
-
private killFfmpeg;
|
|
178
|
-
private handleOutputData;
|
|
179
|
-
private emitFrame;
|
|
180
|
-
pushPacket(packet: EncodedPacket): void;
|
|
181
|
-
onFrame(callback: (frame: DecodedFrame) => void): Unsubscribe;
|
|
182
|
-
updateConfig(update: Partial<DecoderSessionConfig>): void;
|
|
183
|
-
destroy(): Promise<void>;
|
|
184
|
-
getStats(): DecoderStats;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
declare class FrameDropper {
|
|
188
|
-
private intervalMs;
|
|
189
|
-
private lastPassedAt;
|
|
190
|
-
constructor(maxFps: number);
|
|
191
|
-
shouldKeep(): boolean;
|
|
192
|
-
setMaxFps(maxFps: number): void;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export { DecoderFfmpegAddon, FfmpegDecoderProvider, FfmpegDecoderSession, FrameDropper };
|
package/dist/index.d.ts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import * as _camstack_types from '@camstack/types';
|
|
2
|
-
import { BaseAddon, DecoderHwAccelConfig, IDecoderCapProvider, ProviderRegistration, FrameFormat, DecoderStats, IDecoderProvider, IScopedLogger, DecoderSessionConfig, IDecoderSession, HwAccelBackend, IKernelHwAccel, EncodedPacket, DecodedFrame, Unsubscribe } from '@camstack/types';
|
|
3
|
-
|
|
4
|
-
/** Cap-compatible frame shape — format must match DecodedFrameSchema enum values. */
|
|
5
|
-
type CapDecodedFrame = {
|
|
6
|
-
data: Uint8Array<ArrayBuffer>;
|
|
7
|
-
width: number;
|
|
8
|
-
height: number;
|
|
9
|
-
format: FrameFormat;
|
|
10
|
-
timestamp: number;
|
|
11
|
-
};
|
|
12
|
-
/** Cap-compatible session config — matches DecoderSessionConfigSchema output type. */
|
|
13
|
-
type CapDecoderSessionConfig = {
|
|
14
|
-
codec: string;
|
|
15
|
-
maxFps: number;
|
|
16
|
-
outputFormat: FrameFormat;
|
|
17
|
-
scale: number;
|
|
18
|
-
width?: number;
|
|
19
|
-
height?: number;
|
|
20
|
-
};
|
|
21
|
-
/** Cap-compatible encoded packet — data is Uint8Array matching EncodedPacketSchema. */
|
|
22
|
-
type CapEncodedPacket = {
|
|
23
|
-
type: 'video' | 'audio';
|
|
24
|
-
data: Uint8Array<ArrayBuffer>;
|
|
25
|
-
pts: number;
|
|
26
|
-
dts: number;
|
|
27
|
-
keyframe: boolean;
|
|
28
|
-
codec: string;
|
|
29
|
-
};
|
|
30
|
-
/**
|
|
31
|
-
* FFmpeg decoder addon — H264/HEVC/MJPEG decode via ffmpeg child
|
|
32
|
-
* process.
|
|
33
|
-
*
|
|
34
|
-
* Phase 2d of the pipeline-settings migration — ffmpeg decoder owns
|
|
35
|
-
* its own `hwaccel` choice + `probedBestHwaccel` hint. Sessions
|
|
36
|
-
* resolve the effective backend from this addon's global settings
|
|
37
|
-
* instead of the orchestrator's legacy `AgentPipelineSettings.hwaccel`.
|
|
38
|
-
* Session constructor still appends `-hwaccel <name>` to the ffmpeg
|
|
39
|
-
* argv as before.
|
|
40
|
-
*
|
|
41
|
-
* Implements the sessionId-based IDecoderCapProvider cap interface.
|
|
42
|
-
* Sessions are managed internally via a Map; frames are polled via
|
|
43
|
-
* RingBuffer.
|
|
44
|
-
*/
|
|
45
|
-
declare class DecoderFfmpegAddon extends BaseAddon<DecoderHwAccelConfig> implements IDecoderCapProvider {
|
|
46
|
-
private readonly sessions;
|
|
47
|
-
private readonly frameBuffers;
|
|
48
|
-
private readonly unsubscribers;
|
|
49
|
-
private readonly sessionMeta;
|
|
50
|
-
constructor();
|
|
51
|
-
protected globalSettingsSchema(): _camstack_types.ConfigUISchema;
|
|
52
|
-
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
53
|
-
/**
|
|
54
|
-
* Resolve the effective hwaccel backend for a new session. Reads
|
|
55
|
-
* this addon's own `hwaccel` setting. `'auto'` defers to the
|
|
56
|
-
* session's local resolver (`ctx.kernel.hwaccel`).
|
|
57
|
-
*/
|
|
58
|
-
private resolveHwAccelPref;
|
|
59
|
-
/**
|
|
60
|
-
* Re-run the platform probe on this host and persist the detected
|
|
61
|
-
* backend as `probedBestHwaccel`. Operator `hwaccel` setting is not
|
|
62
|
-
* touched — only the hint.
|
|
63
|
-
*/
|
|
64
|
-
reprobeHwaccel(): Promise<{
|
|
65
|
-
backend: string;
|
|
66
|
-
}>;
|
|
67
|
-
supportsCodec(input: {
|
|
68
|
-
codec: string;
|
|
69
|
-
}): Promise<boolean>;
|
|
70
|
-
getInfo(): Promise<{
|
|
71
|
-
id: string;
|
|
72
|
-
name: string;
|
|
73
|
-
isPullMode?: boolean;
|
|
74
|
-
priority?: number;
|
|
75
|
-
}>;
|
|
76
|
-
createSession(config: CapDecoderSessionConfig): Promise<{
|
|
77
|
-
sessionId: string;
|
|
78
|
-
nodeId: string;
|
|
79
|
-
}>;
|
|
80
|
-
destroySession(input: {
|
|
81
|
-
sessionId: string;
|
|
82
|
-
}): Promise<void>;
|
|
83
|
-
listActiveSessions(): Promise<readonly {
|
|
84
|
-
sessionId: string;
|
|
85
|
-
codec: string;
|
|
86
|
-
outputFormat: string;
|
|
87
|
-
createdAtMs: number;
|
|
88
|
-
}[]>;
|
|
89
|
-
pushPacket(input: {
|
|
90
|
-
sessionId: string;
|
|
91
|
-
packet: CapEncodedPacket;
|
|
92
|
-
}): Promise<void>;
|
|
93
|
-
openStream(input: {
|
|
94
|
-
sessionId: string;
|
|
95
|
-
url: string;
|
|
96
|
-
}): Promise<void>;
|
|
97
|
-
pullFrames(input: {
|
|
98
|
-
sessionId: string;
|
|
99
|
-
maxCount: number;
|
|
100
|
-
}): Promise<CapDecodedFrame[]>;
|
|
101
|
-
updateConfig(input: {
|
|
102
|
-
sessionId: string;
|
|
103
|
-
config: Partial<CapDecoderSessionConfig>;
|
|
104
|
-
}): Promise<void>;
|
|
105
|
-
getStats(input: {
|
|
106
|
-
sessionId: string;
|
|
107
|
-
}): Promise<DecoderStats>;
|
|
108
|
-
protected onShutdown(): Promise<void>;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
declare class FfmpegDecoderProvider implements IDecoderProvider {
|
|
112
|
-
readonly id = "ffmpeg";
|
|
113
|
-
readonly name = "FFmpeg Decoder";
|
|
114
|
-
readonly isPullMode = false;
|
|
115
|
-
/** Software decoder — used as fallback when hardware decoders are unavailable. */
|
|
116
|
-
readonly priority = 50;
|
|
117
|
-
private logger;
|
|
118
|
-
setLogger(logger: IScopedLogger): void;
|
|
119
|
-
supportsCodec(input: {
|
|
120
|
-
codec: string;
|
|
121
|
-
}): Promise<boolean>;
|
|
122
|
-
createSession(config: DecoderSessionConfig): Promise<IDecoderSession>;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
type HwAccelPref = 'auto' | 'none' | HwAccelBackend;
|
|
126
|
-
interface FfmpegDecoderSessionOptions {
|
|
127
|
-
/** Addon-level hwaccel preference — per-agent. Default `'auto'`. */
|
|
128
|
-
readonly hwaccel?: HwAccelPref;
|
|
129
|
-
/** Kernel hwaccel resolver — `ctx.kernel.hwaccel` passed from the addon. */
|
|
130
|
-
readonly hwaccelResolver?: IKernelHwAccel;
|
|
131
|
-
}
|
|
132
|
-
declare class FfmpegDecoderSession implements IDecoderSession {
|
|
133
|
-
private config;
|
|
134
|
-
private frameDropper;
|
|
135
|
-
private process;
|
|
136
|
-
private frameCallbacks;
|
|
137
|
-
private outputBuffer;
|
|
138
|
-
private destroyed;
|
|
139
|
-
private readonly logger;
|
|
140
|
-
/** When openStream() is used, we read from RTSP directly (not push mode) */
|
|
141
|
-
private pullMode;
|
|
142
|
-
private cachedWidth;
|
|
143
|
-
private cachedHeight;
|
|
144
|
-
private inputPackets;
|
|
145
|
-
private outputFrames;
|
|
146
|
-
private droppedFrames;
|
|
147
|
-
private totalDecodeTimeMs;
|
|
148
|
-
private decodeCount;
|
|
149
|
-
private startTime;
|
|
150
|
-
private readonly hwaccelPref;
|
|
151
|
-
private readonly hwaccelResolver;
|
|
152
|
-
/** The backend we actually passed to ffmpeg `-hwaccel` — `'none'` = software. */
|
|
153
|
-
private activeHwAccel;
|
|
154
|
-
/**
|
|
155
|
-
* Backend resolution is async (calls into `ctx.kernel.hwaccel`), but
|
|
156
|
-
* `pushPacket` is sync — we kick the resolve off in the constructor
|
|
157
|
-
* and cache the result. By the time the first keyframe arrives
|
|
158
|
-
* (~30ms for RTSP), the resolver has completed (it's ultimately
|
|
159
|
-
* `os.platform()` + file checks → sub-ms). If `ensurePushProcess`
|
|
160
|
-
* fires before resolve settles, it skips hwaccel for that spawn;
|
|
161
|
-
* reconnect loop gets the flag on subsequent sessions.
|
|
162
|
-
*/
|
|
163
|
-
private resolvedBackend;
|
|
164
|
-
constructor(config: DecoderSessionConfig, logger?: IScopedLogger, options?: FfmpegDecoderSessionOptions);
|
|
165
|
-
/**
|
|
166
|
-
* Resolve the preferred backend for this host and return the first
|
|
167
|
-
* hit, or `null` when software is requested / nothing available.
|
|
168
|
-
* FFmpeg CLI accepts the `-hwaccel <name>` identifier directly.
|
|
169
|
-
*/
|
|
170
|
-
private resolveHwAccelBackend;
|
|
171
|
-
/**
|
|
172
|
-
* Open an RTSP stream directly — ffmpeg reads from the URL and outputs JPEG frames.
|
|
173
|
-
* This avoids the raw h264 pipe which loses SPS/PPS metadata on some cameras.
|
|
174
|
-
*/
|
|
175
|
-
openStream(url: string): Promise<void>;
|
|
176
|
-
private ensurePushProcess;
|
|
177
|
-
private killFfmpeg;
|
|
178
|
-
private handleOutputData;
|
|
179
|
-
private emitFrame;
|
|
180
|
-
pushPacket(packet: EncodedPacket): void;
|
|
181
|
-
onFrame(callback: (frame: DecodedFrame) => void): Unsubscribe;
|
|
182
|
-
updateConfig(update: Partial<DecoderSessionConfig>): void;
|
|
183
|
-
destroy(): Promise<void>;
|
|
184
|
-
getStats(): DecoderStats;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
declare class FrameDropper {
|
|
188
|
-
private intervalMs;
|
|
189
|
-
private lastPassedAt;
|
|
190
|
-
constructor(maxFps: number);
|
|
191
|
-
shouldKeep(): boolean;
|
|
192
|
-
setMaxFps(maxFps: number): void;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export { DecoderFfmpegAddon, FfmpegDecoderProvider, FfmpegDecoderSession, FrameDropper };
|