@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 CHANGED
@@ -1,42 +1,9 @@
1
1
  "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
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
- var SOI = Buffer.from([255, 216]);
77
- var EOI = Buffer.from([255, 217]);
78
- var FfmpegDecoderSession = class {
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: (0, import_types.maskUrlCredentials)(url), hwAccel: this.activeHwAccel } });
173
- this.process = (0, import_node_child_process.spawn)("ffmpeg", args, { stdio: ["pipe", "pipe", "pipe"] });
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 = (0, import_node_child_process.spawn)("ffmpeg", args);
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
- // src/addon/index.ts
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(import_types2.DEFAULT_DECODER_HWACCEL_CONFIG);
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 \u2014 existing sessions keep the backend they were created with.',
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: [...import_types2.HWACCEL_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: import_types2.decoderCapability, provider: this }];
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 \u2014 returning none");
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 = (0, import_node_crypto.randomUUID)();
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 import_types2.RingBuffer(FRAME_BUFFER_CAPACITY);
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 \u2014 destroying all sessions");
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
- // src/ffmpeg-decoder-provider.ts
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
- // Annotate the CommonJS export names for ESM import in node:
571
- 0 && (module.exports = {
572
- DecoderFfmpegAddon,
573
- FfmpegDecoderProvider,
574
- FfmpegDecoderSession,
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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,yBAA2B;AAU3B,IAAAA,gBAMO;;;AChBP,gCAAyC;AAMzC,mBAAmC;;;ACN5B,IAAM,eAAN,MAAmB;AAAA,EAChB;AAAA,EACA,eAAe;AAAA,EAEvB,YAAY,QAAgB;AAC1B,SAAK,aAAa,SAAS,IAAI,MAAO,SAAS;AAAA,EACjD;AAAA,EAEA,aAAsB;AACpB,QAAI,KAAK,eAAe,EAAG,QAAO;AAElC,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,MAAM,KAAK,gBAAgB,KAAK,YAAY;AAC9C,WAAK,eAAe;AACpB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEA,UAAU,QAAsB;AAC9B,SAAK,aAAa,SAAS,IAAI,MAAO,SAAS;AAAA,EACjD;AACF;;;ADHA,IAAM,aAA4B;AAAA,EAChC,QAAQ;AAAA,EAAC;AAAA,EACT,OAAO;AAAA,EAAC;AAAA,EACR,OAAO;AAAA,EAAC;AAAA,EACR,QAAQ;AAAA,EAAC;AAAA,EACT,QAAQ;AAAE,WAAO;AAAA,EAAW;AAAA,EAC5B,WAAW;AAAE,WAAO;AAAA,EAAW;AACjC;AAEA,IAAM,MAAM,OAAO,KAAK,CAAC,KAAM,GAAI,CAAC;AACpC,IAAM,MAAM,OAAO,KAAK,CAAC,KAAM,GAAI,CAAC;AAE7B,IAAM,uBAAN,MAAsD;AAAA,EACnD;AAAA,EACA;AAAA,EACA,UAA+B;AAAA,EAC/B,iBAAiB,oBAAI,IAAmC;AAAA,EACxD,eAAe,OAAO,MAAM,CAAC;AAAA,EAC7B,YAAY;AAAA,EACH;AAAA;AAAA,EAGT,WAAW;AAAA;AAAA,EAGX,cAAc;AAAA,EACd,eAAe;AAAA;AAAA,EAGf,eAAe;AAAA,EACf,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,YAAY,KAAK,IAAI;AAAA,EAEZ;AAAA,EACA;AAAA;AAAA,EAET,gBAAyC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWzC,kBAAyC;AAAA,EAEjD,YACE,QACA,SAAwB,YACxB,SACA;AACA,SAAK,SAAS,EAAE,GAAG,OAAO;AAC1B,SAAK,SAAS;AACd,SAAK,eAAe,IAAI,aAAa,OAAO,MAAM;AAClD,SAAK,cAAc,SAAS,WAAW;AACvC,SAAK,kBAAkB,SAAS,mBAAmB;AAGnD,SAAK,KAAK,sBAAsB,EAAE,KAAK,CAAC,MAAM;AAAE,WAAK,kBAAkB;AAAA,IAAE,CAAC;AAAA,EAE5E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,wBAAwD;AACpE,QAAI,KAAK,gBAAgB,OAAQ,QAAO;AACxC,UAAM,WACJ,KAAK,gBAAgB,SAAS,OAAO,KAAK;AAC5C,QAAI,CAAC,KAAK,gBAAiB,QAAO;AAClC,UAAM,aAAa,MAAM,KAAK,gBAAgB,QAAQ,QAAQ;AAC9D,WAAO,WAAW,UAAU,CAAC,KAAK;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,WAAW,KAA4B;AAC3C,QAAI,KAAK,UAAW;AACpB,SAAK,WAAW;AAEhB,SAAK,WAAW;AAChB,SAAK,eAAe,OAAO,MAAM,CAAC;AAClC,SAAK,cAAc;AACnB,SAAK,eAAe;AAQpB,UAAM,UAAU,MAAM,KAAK,sBAAsB;AACjD,UAAM,SAAmB,UAAU,CAAC,YAAY,OAAO,IAAI,CAAC;AAC5D,SAAK,gBAAgB,WAAW;AAEhC,UAAM,OAAO;AAAA,MACX;AAAA,MAAgB;AAAA,MAAa;AAAA,MAC7B,GAAG;AAAA,MACH;AAAA,MAAW;AAAA,MACX;AAAA,MAAU;AAAA,MACV;AAAA,MAAc;AAAA,MACd;AAAA,MAAoB;AAAA,MACpB;AAAA,MAAmB;AAAA,MACnB;AAAA,MAAM;AAAA,MACN;AAAA;AAAA,IACF;AAEA,QAAI,KAAK,OAAO,QAAQ,GAAG;AACzB,WAAK,KAAK,OAAO,YAAY,KAAK,OAAO,KAAK,OAAO,KAAK,OAAO,KAAK,EAAE;AAAA,IAC1E;AAEA,SAAK,KAAK,MAAM,cAAc,WAAW,SAAS,QAAQ,KAAK,YAAY,KAAK,QAAQ;AAKxF,SAAK,OAAO,KAAK,gCAAgC,EAAE,MAAM,EAAE,SAAK,iCAAmB,GAAG,GAAG,SAAS,KAAK,cAAc,EAAE,CAAC;AACxH,SAAK,cAAU,iCAAM,UAAU,MAAM,EAAE,OAAO,CAAC,QAAQ,QAAQ,MAAM,EAAE,CAAC;AAExE,SAAK,QAAQ,OAAO,GAAG,SAAS,MAAM;AAAA,IAAC,CAAC;AAExC,SAAK,QAAQ,QAAQ,GAAG,QAAQ,CAAC,UAAkB;AACjD,WAAK,iBAAiB,KAAK;AAAA,IAC7B,CAAC;AAED,SAAK,QAAQ,QAAQ,GAAG,QAAQ,CAAC,SAAiB;AAChD,YAAM,OAAO,KAAK,SAAS,EAAE,KAAK;AAClC,UAAI,KAAM,MAAK,OAAO,KAAK,yBAAyB,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAAA,IACxE,CAAC;AAED,SAAK,QAAQ,GAAG,SAAS,CAAC,QAAe;AACvC,WAAK,OAAO,MAAM,8BAA8B,EAAE,MAAM,EAAE,OAAO,IAAI,QAAQ,EAAE,CAAC;AAAA,IAClF,CAAC;AAED,SAAK,QAAQ,GAAG,SAAS,CAAC,MAAM,WAAW;AACzC,WAAK,OAAO,KAAK,yBAAyB,EAAE,MAAM,EAAE,MAAM,QAAQ,QAAQ,KAAK,aAAa,EAAE,CAAC;AAC/F,UAAI,CAAC,KAAK,WAAW;AACnB,aAAK,UAAU;AAAA,MACjB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,oBAA0B;AAChC,QAAI,KAAK,WAAW,KAAK,aAAa,KAAK,SAAU;AAErD,UAAM,cAAc,KAAK,OAAO,UAAU,UAAU,KAAK,OAAO,UAAU,SACtE,SACA,KAAK,OAAO,UAAU,UAAU,UAAU;AAM9C,UAAM,SAAmB,KAAK,kBAAkB,CAAC,YAAY,KAAK,eAAe,IAAI,CAAC;AACtF,SAAK,gBAAgB,KAAK,mBAAmB;AAE7C,UAAM,OAAO;AAAA,MACX;AAAA,MAAgB;AAAA,MAAa;AAAA,MAC7B,GAAG;AAAA,MACH;AAAA,MAAW;AAAA,MACX;AAAA,MAAU;AAAA,MACV;AAAA,MAAc;AAAA,MACd;AAAA,MAAoB;AAAA,MACpB;AAAA,MAAM;AAAA,MAAa;AAAA,MAAM;AAAA,IAC3B;AAEA,QAAI,KAAK,OAAO,QAAQ,GAAG;AACzB,WAAK,KAAK,OAAO,YAAY,KAAK,OAAO,KAAK,OAAO,KAAK,OAAO,KAAK,EAAE;AAAA,IAC1E;AAEA,SAAK,KAAK,MAAM,cAAc,WAAW,SAAS,QAAQ,KAAK,YAAY,KAAK,QAAQ;AAExF,SAAK,OAAO,KAAK,qCAAqC;AAAA,MACpD,MAAM,EAAE,OAAO,KAAK,OAAO,OAAO,SAAS,KAAK,cAAc;AAAA,IAChE,CAAC;AACD,SAAK,cAAU,iCAAM,UAAU,IAAI;AACnC,SAAK,QAAQ,OAAO,GAAG,SAAS,MAAM;AAAA,IAAC,CAAC;AAExC,SAAK,QAAQ,QAAQ,GAAG,QAAQ,CAAC,UAAkB;AACjD,WAAK,iBAAiB,KAAK;AAAA,IAC7B,CAAC;AAED,SAAK,QAAQ,QAAQ,GAAG,QAAQ,CAAC,SAAiB;AAChD,YAAM,OAAO,KAAK,SAAS,EAAE,KAAK;AAClC,UAAI,KAAM,MAAK,OAAO,KAAK,yBAAyB,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAAA,IACxE,CAAC;AAED,SAAK,QAAQ,GAAG,SAAS,CAAC,QAAe;AACvC,WAAK,OAAO,MAAM,8BAA8B,EAAE,MAAM,EAAE,OAAO,IAAI,QAAQ,EAAE,CAAC;AAAA,IAClF,CAAC;AAED,SAAK,QAAQ,GAAG,SAAS,CAAC,MAAM,WAAW;AACzC,WAAK,OAAO,KAAK,yBAAyB,EAAE,MAAM,EAAE,MAAM,QAAQ,SAAS,KAAK,cAAc,QAAQ,KAAK,aAAa,EAAE,CAAC;AAC3H,UAAI,CAAC,KAAK,WAAW;AACnB,aAAK,UAAU;AAAA,MACjB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,aAAmB;AACzB,QAAI,KAAK,SAAS;AAChB,UAAI;AACF,aAAK,QAAQ,KAAK,SAAS;AAAA,MAC7B,QAAQ;AAAA,MAER;AACA,WAAK,UAAU;AAAA,IACjB;AAAA,EACF;AAAA,EAEQ,iBAAiB,OAAqB;AAC5C,SAAK,eAAe,OAAO,OAAO,CAAC,KAAK,cAAc,KAAK,CAAC;AAG5D,QAAI,aAAa;AACjB,WAAO,MAAM;AACX,YAAM,WAAW,KAAK,aAAa,QAAQ,KAAK,UAAU;AAC1D,UAAI,aAAa,GAAI;AAErB,YAAM,WAAW,KAAK,aAAa,QAAQ,KAAK,WAAW,CAAC;AAC5D,UAAI,aAAa,GAAI;AAErB,YAAM,WAAW,WAAW;AAC5B,YAAM,WAAW,KAAK,aAAa,SAAS,UAAU,QAAQ;AAG9D,mBAAa;AAEb,WAAK,UAAU,OAAO,KAAK,QAAQ,CAAC;AAAA,IACtC;AAGA,QAAI,aAAa,GAAG;AAClB,WAAK,eAAe,OAAO,KAAK,KAAK,aAAa,SAAS,UAAU,CAAC;AAAA,IACxE;AAAA,EACF;AAAA,EAEQ,UAAU,MAAoB;AACpC,UAAM,cAAc,KAAK,IAAI;AAE7B,QAAI,CAAC,KAAK,aAAa,WAAW,GAAG;AACnC,WAAK;AACL;AAAA,IACF;AAEA,UAAM,aAAa,KAAK,IAAI,IAAI;AAChC,SAAK,qBAAqB;AAC1B,SAAK;AACL,SAAK;AAGL,QAAI,KAAK,gBAAgB,GAAG;AAC1B,YAAM,OAAO,oBAAoB,IAAI;AACrC,WAAK,cAAc,KAAK;AACxB,WAAK,eAAe,KAAK;AACzB,WAAK,OAAO,KAAK,uBAAuB,EAAE,MAAM,EAAE,OAAO,KAAK,OAAO,QAAQ,KAAK,QAAQ,QAAQ,QAAQ,OAAO,KAAK,OAAO,EAAE,CAAC;AAAA,IAClI;AAEA,UAAM,QAAsB;AAAA,MAC1B;AAAA,MACA,OAAO,KAAK;AAAA,MACZ,QAAQ,KAAK;AAAA,MACb,QAAQ;AAAA,MACR,WAAW,KAAK,IAAI;AAAA,IACtB;AAEA,eAAW,MAAM,KAAK,gBAAgB;AACpC,SAAG,KAAK;AAAA,IACV;AAAA,EACF;AAAA,EAEA,WAAW,QAA6B;AACtC,QAAI,KAAK,aAAa,KAAK,SAAU;AACrC,SAAK,kBAAkB;AACvB,QAAI,CAAC,KAAK,SAAS,MAAO;AAC1B,SAAK;AACL,QAAI;AACF,WAAK,QAAQ,MAAM,MAAM,OAAO,IAAI;AAAA,IACtC,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,QAAQ,UAAsD;AAC5D,SAAK,eAAe,IAAI,QAAQ;AAChC,WAAO,MAAM;AACX,WAAK,eAAe,OAAO,QAAQ;AAAA,IACrC;AAAA,EACF;AAAA,EAEA,aAAa,QAA6C;AACxD,SAAK,SAAS,EAAE,GAAG,KAAK,QAAQ,GAAG,OAAO;AAC1C,QAAI,OAAO,WAAW,QAAW;AAC/B,WAAK,aAAa,UAAU,OAAO,MAAM;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,MAAM,UAAyB;AAC7B,QAAI,KAAK,UAAW;AACpB,SAAK,YAAY;AACjB,SAAK,WAAW;AAChB,SAAK,eAAe,MAAM;AAAA,EAC5B;AAAA,EAEA,WAAyB;AACvB,UAAM,YAAY,KAAK,KAAK,KAAK,IAAI,IAAI,KAAK,aAAa,KAAM,CAAC;AAClE,WAAO;AAAA,MACL,UAAU,KAAK,eAAe;AAAA,MAC9B,WAAW,KAAK,eAAe;AAAA,MAC/B,iBAAiB,KAAK,cAAc,IAAI,KAAK,oBAAoB,KAAK,cAAc;AAAA,MACpF,eAAe,KAAK;AAAA,IACtB;AAAA,EACF;AACF;AAOA,SAAS,oBAAoB,MAAiD;AAC5E,WAAS,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,KAAK;AACxC,QAAI,KAAK,CAAC,MAAM,QAAS,KAAK,IAAI,CAAC,MAAM,OAAQ,KAAK,IAAI,CAAC,MAAM,MAAO;AACtE,YAAM,SAAU,KAAK,IAAI,CAAC,KAAM,IAAK,KAAK,IAAI,CAAC;AAC/C,YAAM,QAAS,KAAK,IAAI,CAAC,KAAM,IAAK,KAAK,IAAI,CAAC;AAC9C,aAAO,EAAE,OAAO,OAAO;AAAA,IACzB;AAAA,EACF;AACA,SAAO,EAAE,OAAO,GAAG,QAAQ,EAAE;AAC/B;;;ADtTA,IAAM,wBAAwB;AAwB9B,IAAqB,qBAArB,cAAgD,wBAA+D;AAAA,EAC5F,WAAW,oBAAI,IAA6B;AAAA,EAC5C,eAAe,oBAAI,IAAyC;AAAA,EAC5D,gBAAgB,oBAAI,IAAwB;AAAA,EAC5C,cAAc,oBAAI,IAAyB;AAAA,EAE5D,cAAc;AAAE,UAAM,4CAA8B;AAAA,EAAE;AAAA,EAE5C,uBAAuB;AAC/B,WAAO,KAAK,OAAO;AAAA,MACjB,UAAU,CAAC;AAAA,QACT,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,aAAa;AAAA,QACb,QAAQ;AAAA,UACN,KAAK,MAAM;AAAA,YACT,MAAM;AAAA,YACN,KAAK;AAAA,YACL,OAAO;AAAA,YACP,SAAS,CAAC,GAAG,6BAAe;AAAA,YAC5B,SAAS;AAAA,YACT,WAAW;AAAA,UACb,CAAC;AAAA,UACD,KAAK,MAAM;AAAA,YACT,MAAM;AAAA,YACN,KAAK;AAAA,YACL,OAAO;AAAA,YACP,aAAa;AAAA,YACb,UAAU;AAAA,YACV,SAAS;AAAA,UACX,CAAC;AAAA,QACH;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEA,MAAgB,eAAgD;AAC9D,SAAK,IAAI,OAAO,KAAK,kCAAkC;AAEvD,QAAI,CAAC,KAAK,OAAO,mBAAmB;AAClC,WAAK,eAAe,EAAE,MAAM,CAAC,QAAiB;AAC5C,aAAK,IAAI,OAAO,KAAK,uCAAuC;AAAA,UAC1D,MAAM,EAAE,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE;AAAA,QAClE,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AACA,WAAO,CAAC,EAAE,YAAY,iCAAmB,UAAU,KAAK,CAAC;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,qBAAoC;AAC1C,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,iBAA+C;AACnD,UAAM,WAAW,KAAK,IAAI,OAAO;AACjC,QAAI,CAAC,UAAU;AACb,WAAK,IAAI,OAAO,KAAK,kEAA6D;AAClF,YAAM,KAAK,IAAI,UAAU,gBAAgB,EAAE,mBAAmB,OAAO,CAAC;AACtE,aAAO,EAAE,SAAS,OAAO;AAAA,IAC3B;AACA,QAAI;AACF,YAAM,MAAM,MAAM,SAAS,QAAQ;AACnC,YAAM,UAAW,IAAI,UAAU,CAAC,KAAK;AACrC,YAAM,KAAK,IAAI,UAAU,gBAAgB,EAAE,mBAAmB,QAAQ,CAAC;AACvE,WAAK,IAAI,OAAO,KAAK,2CAA2C;AAAA,QAC9D,MAAM,EAAE,SAAS,WAAW,IAAI,WAAW,WAAW,IAAI,UAAU;AAAA,MACtE,CAAC;AACD,aAAO,EAAE,QAAQ;AAAA,IACnB,SAAS,KAAK;AACZ,WAAK,IAAI,OAAO,KAAK,yBAAyB;AAAA,QAC5C,MAAM,EAAE,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE;AAAA,MAClE,CAAC;AACD,YAAM,KAAK,IAAI,UAAU,gBAAgB,EAAE,mBAAmB,OAAO,CAAC;AACtE,aAAO,EAAE,SAAS,OAAO;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,MAAM,cAAc,OAA4C;AAC9D,WAAO,CAAC,QAAQ,QAAQ,QAAQ,OAAO,EAAE,SAAS,MAAM,MAAM,YAAY,CAAC;AAAA,EAC7E;AAAA,EAEA,MAAM,UAA0F;AAC9F,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,YAAY;AAAA,MACZ,UAAU;AAAA,IACZ;AAAA,EACF;AAAA,EAEA,MAAM,cAAc,QAAiF;AACnG,UAAM,gBAAY,+BAAW;AAC7B,UAAM,UAAU,KAAK,mBAAmB;AACxC,UAAM,UAAU,IAAI,qBAAqB,QAAQ,KAAK,IAAI,QAAQ;AAAA,MAChE;AAAA,MACA,iBAAiB,KAAK,IAAI,OAAO;AAAA,IACnC,CAAC;AACD,UAAM,aAAa,IAAI,yBAA4B,qBAAqB;AAExE,UAAM,QAAQ,QAAQ,QAAQ,CAAC,UAAU;AAEvC,YAAM,EAAE,OAAO,IAAI;AACnB,UAAI,WAAW,UAAU,WAAW,SAAS,WAAW,SAAS,WAAW,YAAY,WAAW,OAAQ;AAG3G,YAAM,WAAW,IAAI,YAAY,MAAM,KAAK,UAAU;AACtD,UAAI,WAAW,QAAQ,EAAE,IAAI,MAAM,IAAI;AACvC,YAAM,WAA4B;AAAA,QAChC,MAAM,IAAI,WAAW,QAAQ;AAAA,QAC7B,OAAO,MAAM;AAAA,QACb,QAAQ,MAAM;AAAA,QACd;AAAA,QACA,WAAW,MAAM;AAAA,MACnB;AACA,iBAAW,KAAK,QAAQ;AAAA,IAC1B,CAAC;AAED,SAAK,SAAS,IAAI,WAAW,OAAO;AACpC,SAAK,aAAa,IAAI,WAAW,UAAU;AAC3C,SAAK,cAAc,IAAI,WAAW,KAAK;AACvC,SAAK,YAAY,IAAI,WAAW;AAAA,MAC9B,OAAO,OAAO;AAAA,MACd,cAAc,OAAO;AAAA,MACrB,aAAa,KAAK,IAAI;AAAA,IACxB,CAAC;AAED,SAAK,IAAI,OAAO,KAAK,2BAA2B,EAAE,MAAM,EAAE,WAAW,OAAO,OAAO,OAAO,aAAa,QAAQ,EAAE,CAAC;AAClH,WAAO,EAAE,WAAW,QAAQ,KAAK,IAAI,OAAO,eAAe,QAAQ;AAAA,EACrE;AAAA,EAEA,MAAM,eAAe,OAA6C;AAChE,UAAM,EAAE,UAAU,IAAI;AACtB,UAAM,UAAU,KAAK,SAAS,IAAI,SAAS;AAC3C,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,qCAAqC,SAAS,EAAE;AAAA,IAClE;AAEA,UAAM,QAAQ,KAAK,cAAc,IAAI,SAAS;AAC9C,QAAI,MAAO,OAAM;AAEjB,UAAM,QAAQ,QAAQ;AAEtB,SAAK,SAAS,OAAO,SAAS;AAC9B,SAAK,aAAa,OAAO,SAAS;AAClC,SAAK,cAAc,OAAO,SAAS;AACnC,SAAK,YAAY,OAAO,SAAS;AAEjC,SAAK,IAAI,OAAO,KAAK,6BAA6B,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AAAA,EAC3E;AAAA,EAEA,MAAM,qBAA0H;AAC9H,UAAM,MAA8F,CAAC;AACrG,eAAW,CAAC,WAAW,IAAI,KAAK,KAAK,aAAa;AAChD,UAAI,KAAK,EAAE,WAAW,OAAO,KAAK,OAAO,cAAc,KAAK,cAAc,aAAa,KAAK,YAAY,CAAC;AAAA,IAC3G;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,WAAW,OAAuE;AACtF,UAAM,UAAU,KAAK,SAAS,IAAI,MAAM,SAAS;AACjD,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,qCAAqC,MAAM,SAAS,EAAE;AAAA,IACxE;AAEA,YAAQ,WAAW;AAAA,MACjB,GAAG,MAAM;AAAA,MACT,MAAM,OAAO,KAAK,MAAM,OAAO,IAAI;AAAA,IACrC,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,WAAW,OAA0D;AACzE,UAAM,UAAU,KAAK,SAAS,IAAI,MAAM,SAAS;AACjD,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,qCAAqC,MAAM,SAAS,EAAE;AAAA,IACxE;AACA,QAAI,QAAQ,YAAY;AACtB,YAAM,QAAQ,WAAW,MAAM,GAAG;AAAA,IACpC;AAAA,EACF;AAAA,EAEA,MAAM,WAAW,OAA4E;AAC3F,UAAM,aAAa,KAAK,aAAa,IAAI,MAAM,SAAS;AACxD,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,qCAAqC,MAAM,SAAS,EAAE;AAAA,IACxE;AACA,WAAO,WAAW,MAAM,MAAM,QAAQ;AAAA,EACxC;AAAA,EAEA,MAAM,aAAa,OAAuF;AACxG,UAAM,UAAU,KAAK,SAAS,IAAI,MAAM,SAAS;AACjD,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,qCAAqC,MAAM,SAAS,EAAE;AAAA,IACxE;AACA,YAAQ,aAAa,MAAM,MAAM;AAAA,EACnC;AAAA,EAEA,MAAM,SAAS,OAAqD;AAClE,UAAM,UAAU,KAAK,SAAS,IAAI,MAAM,SAAS;AACjD,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,qCAAqC,MAAM,SAAS,EAAE;AAAA,IACxE;AACA,WAAO,QAAQ,SAAS;AAAA,EAC1B;AAAA,EAEA,MAAgB,aAA4B;AAC1C,SAAK,IAAI,OAAO,KAAK,8DAAyD;AAC9E,UAAM,kBAAmC,CAAC;AAC1C,eAAW,CAAC,WAAW,OAAO,KAAK,KAAK,UAAU;AAChD,YAAM,QAAQ,KAAK,cAAc,IAAI,SAAS;AAC9C,UAAI,MAAO,OAAM;AACjB,sBAAgB,KAAK,QAAQ,QAAQ,CAAC;AAAA,IACxC;AACA,UAAM,QAAQ,IAAI,eAAe;AACjC,SAAK,SAAS,MAAM;AACpB,SAAK,aAAa,MAAM;AACxB,SAAK,YAAY,MAAM;AACvB,SAAK,cAAc,MAAM;AAAA,EAC3B;AACF;;;AGzSA,IAAM,mBAAmB,oBAAI,IAAI,CAAC,QAAQ,QAAQ,QAAQ,OAAO,CAAC;AAE3D,IAAM,wBAAN,MAAwD;AAAA,EACpD,KAAK;AAAA,EACL,OAAO;AAAA,EACP,aAAa;AAAA;AAAA,EAEb,WAAW;AAAA,EACZ,SAA+B;AAAA,EAEvC,UAAU,QAA6B;AACrC,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,MAAM,cAAc,OAA4C;AAC9D,WAAO,iBAAiB,IAAI,MAAM,KAAK;AAAA,EACzC;AAAA,EAEA,MAAM,cAAc,QAAwD;AAC1E,WAAO,IAAI,qBAAqB,QAAQ,KAAK,UAAU,MAAS;AAAA,EAClE;AACF;","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
- // src/addon/index.ts
2
- import { randomUUID } from "crypto";
3
- import {
4
- BaseAddon,
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
- var SOI = Buffer.from([255, 216]);
54
- var EOI = Buffer.from([255, 217]);
55
- var FfmpegDecoderSession = class {
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
- // src/addon/index.ts
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 \u2014 existing sessions keep the backend they were created with.',
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 \u2014 returning none");
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 \u2014 destroying all sessions");
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
- // src/ffmpeg-decoder-provider.ts
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
@@ -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.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": "tsup",
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 };