@camstack/addon-post-analysis 0.1.10 → 0.1.12

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.
Files changed (34) hide show
  1. package/assets/icon.svg +7 -0
  2. package/dist/embedding-encoder/index.js +23 -17
  3. package/dist/embedding-encoder/index.js.map +1 -1
  4. package/dist/embedding-encoder/index.mjs +18 -12
  5. package/dist/embedding-encoder/index.mjs.map +1 -1
  6. package/dist/enrichment-engine/index.js +46 -47
  7. package/dist/enrichment-engine/index.js.map +1 -1
  8. package/dist/enrichment-engine/index.mjs +46 -47
  9. package/dist/enrichment-engine/index.mjs.map +1 -1
  10. package/dist/index-BJKSB953.js +13565 -0
  11. package/dist/index-BJKSB953.js.map +1 -0
  12. package/dist/index-BThK2F-p.mjs +13566 -0
  13. package/dist/index-BThK2F-p.mjs.map +1 -0
  14. package/dist/pipeline-analytics/@mf-types.zip +0 -0
  15. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-ChoHjdk6.mjs +17 -0
  16. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-B9D0yX32.mjs +15 -0
  17. package/dist/pipeline-analytics/_stub.js +1 -1
  18. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-SishfP0w.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Vw8HvV_Q.mjs} +6 -6
  19. package/dist/pipeline-analytics/{hostInit-BrI-2jV9.mjs → hostInit-PQYMQJ_C.mjs} +6 -6
  20. package/dist/pipeline-analytics/{index-BWpyiREe.mjs → index-BSmxqDqD.mjs} +67 -44
  21. package/dist/pipeline-analytics/{index-BQ_NNiX0.mjs → index-Bpv0NSqI.mjs} +5414 -5170
  22. package/dist/pipeline-analytics/{index-ClZbgOhl.mjs → index-D7qTzYFz.mjs} +4249 -4164
  23. package/dist/pipeline-analytics/index.js +21 -21
  24. package/dist/pipeline-analytics/index.mjs +1 -1
  25. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  26. package/dist/recording/index.js +5 -5
  27. package/dist/recording/index.mjs +2 -2
  28. package/dist/{recording-coordinator-DuP3BUTV.mjs → recording-coordinator-CvJtVs3m.mjs} +4 -4
  29. package/dist/{recording-coordinator-DuP3BUTV.mjs.map → recording-coordinator-CvJtVs3m.mjs.map} +1 -1
  30. package/dist/{recording-coordinator-C2sATEhe.js → recording-coordinator-DH1gmm5G.js} +13 -13
  31. package/dist/{recording-coordinator-C2sATEhe.js.map → recording-coordinator-DH1gmm5G.js.map} +1 -1
  32. package/package.json +15 -6
  33. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-ezH__dL2.mjs +0 -17
  34. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-BMHQXgd2.mjs +0 -15
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const types = require("@camstack/types");
2
+ const index = require("../index-BJKSB953.js");
3
3
  const node_crypto = require("node:crypto");
4
4
  function pointInPolygon(point, polygon) {
5
5
  if (polygon.length < 3) return false;
@@ -1714,7 +1714,7 @@ const TTL_SWEEP_INTERVAL_MS = 5e3;
1714
1714
  const RETENTION_SWEEP_INTERVAL_MS = 5 * 6e4;
1715
1715
  const AUDIO_EVENT_HEARTBEAT_MS = 5e3;
1716
1716
  const MOTION_EVENT_HEARTBEAT_MS = 5e3;
1717
- class PipelineAnalyticsAddon extends types.BaseAddon {
1717
+ class PipelineAnalyticsAddon extends index.BaseAddon {
1718
1718
  processors = /* @__PURE__ */ new Map();
1719
1719
  trackStore = null;
1720
1720
  mediaStore = null;
@@ -1792,19 +1792,19 @@ class PipelineAnalyticsAddon extends types.BaseAddon {
1792
1792
  }
1793
1793
  });
1794
1794
  this.unsubInference = this.ctx.eventBus.subscribe(
1795
- { category: types.EventCategory.PipelineInferenceResult },
1795
+ { category: index.EventCategory.PipelineInferenceResult },
1796
1796
  (ev) => {
1797
1797
  void this.handleInferenceResult(ev.data);
1798
1798
  }
1799
1799
  );
1800
1800
  this.unsubAudio = this.ctx.eventBus.subscribe(
1801
- { category: types.EventCategory.PipelineAudioInferenceResult },
1801
+ { category: index.EventCategory.PipelineAudioInferenceResult },
1802
1802
  (ev) => {
1803
1803
  void this.handleAudioResult(ev.data);
1804
1804
  }
1805
1805
  );
1806
1806
  this.unsubMotion = this.ctx.eventBus.subscribe(
1807
- { category: types.EventCategory.MotionAnalysis },
1807
+ { category: index.EventCategory.MotionAnalysis },
1808
1808
  (ev) => {
1809
1809
  const src = ev.source;
1810
1810
  if (src?.type !== "device" || typeof src.deviceId !== "number") return;
@@ -1812,7 +1812,7 @@ class PipelineAnalyticsAddon extends types.BaseAddon {
1812
1812
  }
1813
1813
  );
1814
1814
  this.unsubMotionOnboard = this.ctx.eventBus.subscribe(
1815
- { category: types.EventCategory.MotionOnMotionChanged },
1815
+ { category: index.EventCategory.MotionOnMotionChanged },
1816
1816
  (ev) => {
1817
1817
  const data = ev.data;
1818
1818
  if (data.source === "analyzer") return;
@@ -1821,7 +1821,7 @@ class PipelineAnalyticsAddon extends types.BaseAddon {
1821
1821
  }
1822
1822
  );
1823
1823
  this.unsubBindings = this.ctx.eventBus.subscribe(
1824
- { category: types.EventCategory.DeviceBindingsChanged },
1824
+ { category: index.EventCategory.DeviceBindingsChanged },
1825
1825
  (ev) => {
1826
1826
  const data = ev.data;
1827
1827
  this.bindingCache?.onBindingsChanged(data);
@@ -1833,7 +1833,7 @@ class PipelineAnalyticsAddon extends types.BaseAddon {
1833
1833
  }
1834
1834
  );
1835
1835
  this.unsubDeviceUnreg = this.ctx.eventBus.subscribe(
1836
- { category: types.EventCategory.DeviceUnregistered },
1836
+ { category: index.EventCategory.DeviceUnregistered },
1837
1837
  (ev) => {
1838
1838
  const { deviceId } = ev.data;
1839
1839
  this.trackStore?.clearDevice(deviceId);
@@ -1942,21 +1942,21 @@ class PipelineAnalyticsAddon extends types.BaseAddon {
1942
1942
  };
1943
1943
  return [
1944
1944
  {
1945
- capability: types.pipelineAnalyticsCapability,
1945
+ capability: index.pipelineAnalyticsCapability,
1946
1946
  provider: this,
1947
1947
  kind: "wrapper",
1948
1948
  defaultActive: true
1949
1949
  },
1950
1950
  {
1951
- capability: types.zoneAnalyticsCapability,
1951
+ capability: index.zoneAnalyticsCapability,
1952
1952
  provider: this.zoneAnalytics
1953
1953
  },
1954
1954
  {
1955
- capability: types.audioMetricsCapability,
1955
+ capability: index.audioMetricsCapability,
1956
1956
  provider: this.audioMetrics
1957
1957
  },
1958
1958
  {
1959
- capability: types.addonWidgetsSourceCapability,
1959
+ capability: index.addonWidgetsSourceCapability,
1960
1960
  provider: widgetsProvider
1961
1961
  }
1962
1962
  ];
@@ -2045,7 +2045,7 @@ class PipelineAnalyticsAddon extends types.BaseAddon {
2045
2045
  id: `pa-${node_crypto.randomUUID()}`,
2046
2046
  timestamp: new Date(result.timestamp),
2047
2047
  source: { type: "addon", id: "pipeline-analytics", addonId: "pipeline-analytics" },
2048
- category: types.EventCategory.PipelineAnalyticsTrackStarted,
2048
+ category: index.EventCategory.PipelineAnalyticsTrackStarted,
2049
2049
  data: { deviceId, trackId: id, className: t.className }
2050
2050
  });
2051
2051
  }
@@ -2058,7 +2058,7 @@ class PipelineAnalyticsAddon extends types.BaseAddon {
2058
2058
  id: `pa-${e.id}`,
2059
2059
  timestamp: new Date(e.timestamp),
2060
2060
  source: { type: "addon", id: "pipeline-analytics", addonId: "pipeline-analytics" },
2061
- category: types.EventCategory.PipelineAnalyticsDetectionEvent,
2061
+ category: index.EventCategory.PipelineAnalyticsDetectionEvent,
2062
2062
  data: { deviceId, kind: "object", eventId: e.id, timestamp: e.timestamp }
2063
2063
  });
2064
2064
  }
@@ -2066,7 +2066,7 @@ class PipelineAnalyticsAddon extends types.BaseAddon {
2066
2066
  id: `pa-${node_crypto.randomUUID()}`,
2067
2067
  timestamp: new Date(result.timestamp),
2068
2068
  source: { type: "addon", id: "pipeline-analytics", addonId: "pipeline-analytics" },
2069
- category: types.EventCategory.PipelineAnalyticsFrameTracked,
2069
+ category: index.EventCategory.PipelineAnalyticsFrameTracked,
2070
2070
  data: {
2071
2071
  deviceId,
2072
2072
  timestamp: result.timestamp,
@@ -2126,7 +2126,7 @@ class PipelineAnalyticsAddon extends types.BaseAddon {
2126
2126
  id: `pa-${ev.id}`,
2127
2127
  timestamp: new Date(ev.timestamp),
2128
2128
  source: { type: "addon", id: "pipeline-analytics", addonId: "pipeline-analytics" },
2129
- category: types.EventCategory.PipelineAnalyticsDetectionEvent,
2129
+ category: index.EventCategory.PipelineAnalyticsDetectionEvent,
2130
2130
  data: { deviceId, kind: "audio", eventId: ev.id, timestamp: ev.timestamp }
2131
2131
  });
2132
2132
  }
@@ -2170,7 +2170,7 @@ class PipelineAnalyticsAddon extends types.BaseAddon {
2170
2170
  id: `pa-${ev.id}`,
2171
2171
  timestamp: new Date(ev.timestamp),
2172
2172
  source: { type: "addon", id: "pipeline-analytics", addonId: "pipeline-analytics" },
2173
- category: types.EventCategory.PipelineAnalyticsDetectionEvent,
2173
+ category: index.EventCategory.PipelineAnalyticsDetectionEvent,
2174
2174
  data: { deviceId, kind: "motion", eventId: ev.id, timestamp: ev.timestamp }
2175
2175
  });
2176
2176
  }
@@ -2221,7 +2221,7 @@ class PipelineAnalyticsAddon extends types.BaseAddon {
2221
2221
  id: `pa-${ev.id}`,
2222
2222
  timestamp: new Date(ev.timestamp),
2223
2223
  source: { type: "addon", id: "pipeline-analytics", addonId: "pipeline-analytics" },
2224
- category: types.EventCategory.PipelineAnalyticsDetectionEvent,
2224
+ category: index.EventCategory.PipelineAnalyticsDetectionEvent,
2225
2225
  data: { deviceId, kind: "motion", eventId: ev.id, timestamp: ev.timestamp }
2226
2226
  });
2227
2227
  }
@@ -2236,7 +2236,7 @@ class PipelineAnalyticsAddon extends types.BaseAddon {
2236
2236
  id: `pa-end-${t.trackId}`,
2237
2237
  timestamp: new Date(t.lastSeen),
2238
2238
  source: { type: "addon", id: "pipeline-analytics", addonId: "pipeline-analytics" },
2239
- category: types.EventCategory.PipelineAnalyticsTrackEnded,
2239
+ category: index.EventCategory.PipelineAnalyticsTrackEnded,
2240
2240
  data: {
2241
2241
  deviceId: t.deviceId,
2242
2242
  trackId: t.trackId,
@@ -2505,7 +2505,7 @@ class PipelineAnalyticsAddon extends types.BaseAddon {
2505
2505
  if (!await this.isCameraDevice(input.deviceId)) return null;
2506
2506
  const schema = this.globalSettingsSchema();
2507
2507
  const raw = await this.ctx?.settings?.readDeviceStore(input.deviceId) ?? {};
2508
- const baseSections = schema ? types.hydrateSchema(
2508
+ const baseSections = schema ? index.hydrateSchema(
2509
2509
  {
2510
2510
  ...schema,
2511
2511
  sections: schema.sections.map((s) => ({ ...s, tab: s.tab ?? "Analytics" }))
@@ -2553,7 +2553,7 @@ class PipelineAnalyticsAddon extends types.BaseAddon {
2553
2553
  try {
2554
2554
  const dev = await api.deviceManager.getDevice.query({ deviceId });
2555
2555
  if (!dev) return true;
2556
- return dev.type === types.DeviceType.Camera;
2556
+ return dev.type === index.DeviceType.Camera;
2557
2557
  } catch {
2558
2558
  return true;
2559
2559
  }
@@ -1,4 +1,4 @@
1
- import { BaseAddon, EventCategory, pipelineAnalyticsCapability, zoneAnalyticsCapability, audioMetricsCapability, addonWidgetsSourceCapability, hydrateSchema, DeviceType } from "@camstack/types";
1
+ import { B as BaseAddon, E as EventCategory, p as pipelineAnalyticsCapability, z as zoneAnalyticsCapability, h as audioMetricsCapability, i as addonWidgetsSourceCapability, j as hydrateSchema, D as DeviceType } from "../index-BThK2F-p.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  function pointInPolygon(point, polygon) {
4
4
  if (polygon.length < 3) return false;
@@ -2926,7 +2926,7 @@ async function wt(e) {
2926
2926
  }
2927
2927
  }
2928
2928
  async function ba() {
2929
- return Le || (Le = wt(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-SishfP0w.mjs")).catch((e) => {
2929
+ return Le || (Le = wt(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Vw8HvV_Q.mjs")).catch((e) => {
2930
2930
  throw Le = void 0, e;
2931
2931
  })), Le;
2932
2932
  }
@@ -22,8 +22,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  mod
23
23
  ));
24
24
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
25
- const types = require("@camstack/types");
26
- class RecordingAddon extends types.BaseAddon {
25
+ const index = require("../index-BJKSB953.js");
26
+ class RecordingAddon extends index.BaseAddon {
27
27
  coordinator = null;
28
28
  recordingDb = null;
29
29
  sqliteDb = null;
@@ -55,7 +55,7 @@ class RecordingAddon extends types.BaseAddon {
55
55
  const path = await import("node:path");
56
56
  const { detectPlatformDefaults } = await Promise.resolve().then(() => require("../ffmpeg-config-uANz3sV5.js"));
57
57
  const { RecordingDb: RecDb } = await Promise.resolve().then(() => require("../recording-db-gOgaoQh0.js"));
58
- const { RecordingCoordinator: RecCoord } = await Promise.resolve().then(() => require("../recording-coordinator-C2sATEhe.js"));
58
+ const { RecordingCoordinator: RecCoord } = await Promise.resolve().then(() => require("../recording-coordinator-DH1gmm5G.js"));
59
59
  const storage = this.ctx.kernel.storage;
60
60
  const dbPath = path.join(storage?.resolve({ location: "data", relativePath: "" }) ?? "camstack-data", "camstack.db");
61
61
  this.sqliteDb = new Database(dbPath);
@@ -97,9 +97,9 @@ class RecordingAddon extends types.BaseAddon {
97
97
  storageEstimator
98
98
  });
99
99
  this.ctx.logger.info("Recording Engine initialized");
100
- return [{ capability: types.recordingEngineCapability, provider: this.serviceFacade }];
100
+ return [{ capability: index.recordingEngineCapability, provider: this.serviceFacade }];
101
101
  } catch (error) {
102
- const msg = types.errMsg(error);
102
+ const msg = index.errMsg(error);
103
103
  this.ctx.logger.warn("Recording Engine failed to initialize", { meta: { error: msg } });
104
104
  }
105
105
  }
@@ -1,4 +1,4 @@
1
- import { BaseAddon, recordingEngineCapability, errMsg } from "@camstack/types";
1
+ import { B as BaseAddon, r as recordingEngineCapability, e as errMsg } from "../index-BThK2F-p.mjs";
2
2
  class RecordingAddon extends BaseAddon {
3
3
  coordinator = null;
4
4
  recordingDb = null;
@@ -31,7 +31,7 @@ class RecordingAddon extends BaseAddon {
31
31
  const path = await import("node:path");
32
32
  const { detectPlatformDefaults } = await import("../ffmpeg-config-DRONlBsj.mjs");
33
33
  const { RecordingDb: RecDb } = await import("../recording-db-lIkSMTLq.mjs");
34
- const { RecordingCoordinator: RecCoord } = await import("../recording-coordinator-DuP3BUTV.mjs");
34
+ const { RecordingCoordinator: RecCoord } = await import("../recording-coordinator-CvJtVs3m.mjs");
35
35
  const storage = this.ctx.kernel.storage;
36
36
  const dbPath = path.join(storage?.resolve({ location: "data", relativePath: "" }) ?? "camstack-data", "camstack.db");
37
37
  this.sqliteDb = new Database(dbPath);
@@ -1,4 +1,4 @@
1
- import { EventCategory } from "@camstack/types";
1
+ import { E as EventCategory } from "./index-BThK2F-p.mjs";
2
2
  import { buildFfmpegInputArgs, buildFfmpegOutputArgs, resolveFfmpegConfig } from "./ffmpeg-config-DRONlBsj.mjs";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { spawn } from "node:child_process";
@@ -751,7 +751,7 @@ class RecordingCoordinator {
751
751
  const storageConfig = this.db.resolveStorageConfig(deviceId, `recording:${sp.streamId}`);
752
752
  const storageName = storageConfig?.storageName ?? "recordings";
753
753
  const subDirectory = storageConfig?.subDirectory ?? `recordings/${sp.streamId}`;
754
- const resolvedStoragePath = this.storageProvider.resolve({ location: storageName, relativePath: "" });
754
+ const resolvedStoragePath = await this.storageProvider.resolve({ location: storageName, relativePath: "" });
755
755
  const writerConfig = {
756
756
  deviceId,
757
757
  streamId: sp.streamId,
@@ -780,7 +780,7 @@ class RecordingCoordinator {
780
780
  const thumbStorageName = thumbStorageConfig?.storageName ?? "recordings";
781
781
  const thumbConfig = {
782
782
  deviceId,
783
- storagePath: this.storageProvider.resolve({ location: thumbStorageName, relativePath: "" }),
783
+ storagePath: await this.storageProvider.resolve({ location: thumbStorageName, relativePath: "" }),
784
784
  storageName: thumbStorageName,
785
785
  subDirectory: thumbStorageConfig?.subDirectory ?? "thumbnails/scrub",
786
786
  maxWidthPx: 160,
@@ -1009,4 +1009,4 @@ class RecordingCoordinator {
1009
1009
  export {
1010
1010
  RecordingCoordinator
1011
1011
  };
1012
- //# sourceMappingURL=recording-coordinator-DuP3BUTV.mjs.map
1012
+ //# sourceMappingURL=recording-coordinator-CvJtVs3m.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"recording-coordinator-DuP3BUTV.mjs","sources":["../src/recording/recording/segment-writer.ts","../src/recording/recording/thumbnail-extractor.ts","../src/recording/recording/retention-manager.ts","../src/recording/recording/recording-coordinator.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport { spawn, type ChildProcess } from 'node:child_process'\nimport * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\nimport { EventCategory } from '@camstack/types'\nimport type { IAddonFileStorage } from '@camstack/types'\nimport type { IScopedLogger, IEventBus, INetworkQualityTracker, FfmpegConfig } from '@camstack/types'\nimport { buildFfmpegInputArgs, buildFfmpegOutputArgs } from './ffmpeg-config.js'\nimport type { RecordingDb } from './recording-db.js'\nimport type { SegmentWriterState, RecordingSegment } from './types.js'\n\n// --- Ring Buffer for motion pre-buffer (E2) ---\n\nexport interface BufferedSegment {\n readonly data: Buffer\n readonly startTime: number\n readonly duration: number\n}\n\nexport class SegmentRingBuffer {\n private segments: BufferedSegment[] = []\n private totalDurationSec = 0\n\n constructor(private readonly maxDurationSec: number) {}\n\n push(segment: BufferedSegment): void {\n this.segments.push(segment)\n this.totalDurationSec += segment.duration\n while (this.totalDurationSec > this.maxDurationSec && this.segments.length > 1) {\n const evicted = this.segments.shift()!\n this.totalDurationSec -= evicted.duration\n }\n }\n\n flush(): BufferedSegment[] {\n const result = [...this.segments]\n this.segments = []\n this.totalDurationSec = 0\n return result\n }\n\n get memoryEstimateBytes(): number {\n return this.segments.reduce((sum, s) => sum + s.data.length, 0)\n }\n}\n\n// --- Config ---\n\nexport type SegmentWriterMode = 'continuous' | 'buffer'\n\nexport interface SegmentWriterConfig {\n readonly deviceId: number\n readonly streamId: string\n readonly segmentDurationSec: number\n readonly storagePath: string\n readonly storageName: string\n readonly subDirectory: string\n readonly ffmpeg: FfmpegConfig\n readonly mode: SegmentWriterMode\n readonly preBufferSec: number\n /**\n * File storage abstraction for segment persistence.\n * Used by writeBufferedSegmentToDisk for persisting buffered segments.\n * Note: ffmpeg still writes to storagePath directly (needs real filesystem paths).\n */\n readonly fileStorage?: IAddonFileStorage\n}\n\n// --- Disk space check result ---\n\ninterface DiskSpaceResult {\n readonly ok: boolean\n readonly availableGb: number\n}\n\n// --- Active segment tracking ---\n\ninterface ActiveSegment {\n readonly id: string\n readonly path: string\n readonly startTime: number\n}\n\n// --- statfs signature for dependency injection ---\n\ntype StatfsFn = (path: string) => Promise<{ bfree: number; bsize: number }>\n\n// --- SegmentWriter ---\n\nexport class SegmentWriter {\n private _state: SegmentWriterState = 'idle'\n private _mode: SegmentWriterMode\n private ffmpeg: ChildProcess | null = null\n private activeSegment: ActiveSegment | null = null\n private restartCount = 0\n private restartWindowStart = 0\n private healthTimer: ReturnType<typeof setInterval> | null = null\n private lastDataTime = 0\n private ringBuffer: SegmentRingBuffer\n private restartTimeout: ReturnType<typeof setTimeout> | null = null\n private pendingFinalization: Promise<void> | null = null\n private paused = false\n private detectedCodec: 'h264' | 'h265' = 'h264'\n private detectedHasAudio = false\n\n private static readonly MAX_RESTARTS = 10\n private static readonly RESTART_WINDOW_MS = 5 * 60 * 1000\n private static readonly HEALTH_CHECK_INTERVAL_MS = 5000\n private static readonly DATA_TIMEOUT_MS = 15000\n private static readonly MIN_SEGMENT_DURATION_SEC = 0.5\n private static readonly CRITICAL_DISK_GB = 1\n\n constructor(\n private readonly config: SegmentWriterConfig,\n private readonly logger: IScopedLogger,\n private readonly eventBus: IEventBus,\n private readonly db: RecordingDb,\n _networkTracker: INetworkQualityTracker,\n ) {\n this._mode = config.mode\n this.ringBuffer = new SegmentRingBuffer(config.preBufferSec)\n }\n\n get state(): SegmentWriterState {\n return this._state\n }\n\n get mode(): SegmentWriterMode {\n return this._mode\n }\n\n get isPaused(): boolean {\n return this.paused\n }\n\n // --- Public API ---\n\n async start(rtspUrl: string): Promise<void> {\n if (this._state !== 'idle') return\n\n const segmentDir = path.join(\n this.config.storagePath,\n this.config.subDirectory,\n String(this.config.deviceId),\n )\n await fs.mkdir(segmentDir, { recursive: true })\n\n this._state = 'recording'\n this.lastDataTime = Date.now()\n this.restartCount = 0\n this.restartWindowStart = Date.now()\n\n const segmentPattern = path.join(segmentDir, '%d.mp4')\n const args = SegmentWriter.buildSegmentationArgs(\n this.config.ffmpeg,\n rtspUrl,\n segmentPattern,\n this.config.segmentDurationSec,\n )\n\n this.spawnFfmpeg(args, rtspUrl)\n this.startHealthCheck(rtspUrl)\n }\n\n async stop(): Promise<void> {\n if (this._state === 'idle') return\n this._state = 'stopping'\n this.stopHealthCheck()\n this.clearRestartTimeout()\n this.killFfmpeg()\n this.finalizeActiveSegment()\n if (this.pendingFinalization) {\n await this.pendingFinalization\n }\n this._state = 'idle'\n }\n\n resume(rtspUrl: string): void {\n if (!this.paused) return\n this.paused = false\n this.logger.info('Resuming recording after disk space freed', {\n tags: { deviceId: this.config.deviceId },\n })\n this._state = 'idle'\n void this.start(rtspUrl)\n }\n\n async flushAndContinue(): Promise<void> {\n if (this._mode !== 'buffer') return\n\n const buffered = this.ringBuffer.flush()\n this.logger.info('Flushing buffered segments to disk', {\n tags: { deviceId: this.config.deviceId },\n meta: { count: buffered.length },\n })\n\n for (const seg of buffered) {\n await this.writeBufferedSegmentToDisk(seg)\n }\n\n this._mode = 'continuous'\n }\n\n switchToBuffer(): void {\n this._mode = 'buffer'\n this.ringBuffer = new SegmentRingBuffer(this.config.preBufferSec)\n }\n\n // --- Static helpers ---\n\n static generateSegmentId(deviceId: number, streamId: string, startTime: number): string {\n const suffix = randomBytes(2).toString('hex')\n return `${deviceId}_${streamId}_${startTime}_${suffix}`\n }\n\n static buildSegmentationArgs(\n config: FfmpegConfig,\n inputUrl: string,\n outputPattern: string,\n segmentDuration: number,\n ): string[] {\n const inputArgs = buildFfmpegInputArgs(config, inputUrl)\n const outputArgs = buildFfmpegOutputArgs(config)\n\n const segmentArgs = [\n '-f', 'segment',\n '-segment_time', String(segmentDuration),\n '-segment_format', 'mp4',\n '-movflags', '+frag_keyframe+empty_moov+default_base_moof',\n '-reset_timestamps', '1',\n '-strftime', '0',\n ]\n\n return [...inputArgs, ...outputArgs, ...segmentArgs, outputPattern]\n }\n\n static async checkDiskSpace(\n storagePath: string,\n statfsFn?: StatfsFn,\n ): Promise<DiskSpaceResult> {\n const doStatfs = statfsFn ?? (async (p: string) => {\n const { statfs: nodeStatfs } = await import('node:fs/promises')\n return nodeStatfs(p)\n })\n\n try {\n const stats = await doStatfs(storagePath)\n const availableBytes = stats.bfree * stats.bsize\n const availableGb = availableBytes / (1024 * 1024 * 1024)\n return { ok: availableGb >= SegmentWriter.CRITICAL_DISK_GB, availableGb }\n } catch {\n return { ok: true, availableGb: -1 }\n }\n }\n\n // --- Private: ffmpeg process management ---\n\n private spawnFfmpeg(args: string[], rtspUrl: string): void {\n this.ffmpeg = spawn(this.config.ffmpeg.path, args, {\n stdio: ['ignore', 'pipe', 'pipe'],\n })\n\n this.ffmpeg.stdout?.on('data', () => {\n this.lastDataTime = Date.now()\n })\n\n this.ffmpeg.stderr?.on('data', (data: Buffer) => {\n this.lastDataTime = Date.now()\n const msg = data.toString().trim()\n if (msg) {\n this.logger.debug('ffmpeg stderr', { meta: { msg } })\n this.parseSegmentOutput(msg)\n }\n })\n\n this.ffmpeg.on('error', (err) => {\n this.logger.warn('ffmpeg process error', { meta: { error: err.message } })\n this.handleCrash(rtspUrl)\n })\n\n this.ffmpeg.on('exit', (code) => {\n if (code !== 0 && code !== null && this._state === 'recording') {\n this.logger.warn('ffmpeg exited with non-zero code', { meta: { code } })\n this.handleCrash(rtspUrl)\n }\n })\n }\n\n private handleCrash(rtspUrl: string): void {\n this.ffmpeg = null\n const prevFinalization = this.pendingFinalization\n this.pendingFinalization = (prevFinalization ?? Promise.resolve()).then(() => {\n return this.finalizeActiveSegment()\n })\n\n if (this._state !== 'recording') return\n if (this.paused) return\n\n const now = Date.now()\n if (now - this.restartWindowStart > SegmentWriter.RESTART_WINDOW_MS) {\n this.restartCount = 0\n this.restartWindowStart = now\n }\n\n this.restartCount++\n\n this.eventBus.emit({\n id: `rec-err-${now}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingError,\n data: {\n deviceId: this.config.deviceId,\n streamId: this.config.streamId,\n restartAttempt: this.restartCount,\n },\n })\n\n if (this.restartCount > SegmentWriter.MAX_RESTARTS) {\n this.logger.error('Max restarts exceeded', {\n tags: { deviceId: this.config.deviceId, streamId: this.config.streamId },\n })\n this._state = 'idle'\n this.eventBus.emit({\n id: `rec-degraded-${now}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingHealthDegraded,\n data: {\n deviceId: this.config.deviceId,\n streamId: this.config.streamId,\n },\n })\n return\n }\n\n const backoffMs = Math.min(30000, 1000 * Math.pow(2, this.restartCount - 1))\n this.logger.info('Restarting ffmpeg', { meta: { backoffMs, attempt: this.restartCount } })\n\n this.restartTimeout = setTimeout(() => {\n this.restartTimeout = null\n if (this._state === 'recording') {\n const segmentDir = path.join(\n this.config.storagePath,\n this.config.subDirectory,\n String(this.config.deviceId),\n )\n const segmentPattern = path.join(segmentDir, '%d.mp4')\n const args = SegmentWriter.buildSegmentationArgs(\n this.config.ffmpeg,\n rtspUrl,\n segmentPattern,\n this.config.segmentDurationSec,\n )\n this.spawnFfmpeg(args, rtspUrl)\n }\n }, backoffMs)\n }\n\n // --- Private: health monitoring ---\n\n private startHealthCheck(rtspUrl: string): void {\n this.healthTimer = setInterval(() => {\n if (this._state !== 'recording') return\n const elapsed = Date.now() - this.lastDataTime\n if (elapsed > SegmentWriter.DATA_TIMEOUT_MS) {\n this.logger.warn('No data received, restarting ffmpeg', { meta: { elapsedMs: elapsed } })\n this.killFfmpeg()\n this.handleCrash(rtspUrl)\n }\n }, SegmentWriter.HEALTH_CHECK_INTERVAL_MS)\n }\n\n private stopHealthCheck(): void {\n if (this.healthTimer) {\n clearInterval(this.healthTimer)\n this.healthTimer = null\n }\n }\n\n private clearRestartTimeout(): void {\n if (this.restartTimeout) {\n clearTimeout(this.restartTimeout)\n this.restartTimeout = null\n }\n }\n\n private killFfmpeg(): void {\n if (this.ffmpeg) {\n this.ffmpeg.kill('SIGTERM')\n this.ffmpeg = null\n }\n }\n\n // --- Private: segment parsing and finalization ---\n\n private parseSegmentOutput(msg: string): void {\n const videoMatch = msg.match(/Stream\\s+#\\d+:\\d+.*Video:\\s+(h264|hevc|h265)/i)\n if (videoMatch) {\n const codec = videoMatch[1]!.toLowerCase()\n this.detectedCodec = (codec === 'hevc' || codec === 'h265') ? 'h265' : 'h264'\n }\n\n const audioMatch = msg.match(/Stream\\s+#\\d+:\\d+.*Audio:/i)\n if (audioMatch) {\n this.detectedHasAudio = true\n }\n\n const openMatch = msg.match(/Opening '(.+\\.mp4)' for writing/)\n if (openMatch) {\n const prevFinalization = this.pendingFinalization\n this.pendingFinalization = (prevFinalization ?? Promise.resolve()).then(() => {\n return this.finalizeActiveSegment()\n })\n\n const absolutePath = openMatch[1]!\n const segPath = absolutePath.startsWith(this.config.storagePath)\n ? absolutePath.slice(this.config.storagePath.length).replace(/^\\//, '')\n : absolutePath\n this.activeSegment = {\n id: SegmentWriter.generateSegmentId(\n this.config.deviceId,\n this.config.streamId,\n Date.now(),\n ),\n path: segPath,\n startTime: Date.now(),\n }\n }\n }\n\n private async finalizeActiveSegment(): Promise<void> {\n if (!this.activeSegment) return\n const seg = this.activeSegment\n this.activeSegment = null\n\n const endTime = Date.now()\n const duration = (endTime - seg.startTime) / 1000\n\n if (duration < SegmentWriter.MIN_SEGMENT_DURATION_SEC) return\n\n if (this._mode === 'buffer') {\n await this.bufferSegmentFromDisk(seg, endTime, duration)\n return\n }\n\n await this.finalizeSegmentToDisk(seg, endTime, duration)\n }\n\n private async bufferSegmentFromDisk(\n seg: ActiveSegment,\n _endTime: number,\n duration: number,\n ): Promise<void> {\n try {\n const data = await fs.readFile(seg.path)\n this.ringBuffer.push({ data, startTime: seg.startTime, duration })\n await fs.unlink(seg.path).catch(() => {})\n } catch (err) {\n this.logger.warn('Failed to buffer segment', { meta: { error: String(err) } })\n }\n }\n\n private async finalizeSegmentToDisk(\n seg: ActiveSegment,\n endTime: number,\n duration: number,\n ): Promise<void> {\n try {\n const diskCheck = await SegmentWriter.checkDiskSpace(this.config.storagePath)\n\n if (!diskCheck.ok) {\n this.eventBus.emit({\n id: `storage-critical-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingStorageCritical,\n data: {\n storageId: this.config.storageName,\n availableGB: diskCheck.availableGb,\n },\n })\n this.logger.error('Disk space critically low, pausing recording')\n this.paused = true\n this.killFfmpeg()\n this._state = 'idle'\n return\n }\n\n let sizeBytes = 0\n try {\n const fileStat = await fs.stat(seg.path)\n sizeBytes = fileStat.size\n } catch {\n // File may not exist yet or was removed\n }\n\n const codec = this.detectedCodec\n const hasAudio = this.detectedHasAudio\n\n const segment: RecordingSegment = {\n id: seg.id,\n deviceId: this.config.deviceId,\n streamId: this.config.streamId,\n startTime: seg.startTime,\n endTime,\n duration,\n path: seg.path,\n storageName: this.config.storageName,\n subDirectory: this.config.subDirectory,\n sizeBytes,\n codec,\n hasAudio,\n }\n\n try {\n this.db.insertSegment(segment)\n this.eventBus.emit({\n id: `seg-${seg.id}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingSegmentWritten,\n data: {\n deviceId: this.config.deviceId,\n streamId: this.config.streamId,\n segmentId: seg.id,\n duration,\n sizeBytes,\n },\n })\n } catch (err) {\n this.logger.error('Failed to insert segment', { meta: { error: String(err) } })\n }\n } catch (err) {\n this.logger.error('Disk space check failed', { meta: { error: String(err) } })\n }\n }\n\n private async writeBufferedSegmentToDisk(buffered: BufferedSegment): Promise<void> {\n const segId = SegmentWriter.generateSegmentId(\n this.config.deviceId,\n this.config.streamId,\n buffered.startTime,\n )\n const relativePath = `${this.config.subDirectory}/${this.config.deviceId}/${segId}.mp4`\n\n try {\n await this.config.fileStorage?.writeFile(relativePath, buffered.data)\n const sizeBytes = buffered.data.length\n\n const segment: RecordingSegment = {\n id: segId,\n deviceId: this.config.deviceId,\n streamId: this.config.streamId,\n startTime: buffered.startTime,\n endTime: buffered.startTime + buffered.duration * 1000,\n duration: buffered.duration,\n path: relativePath,\n storageName: this.config.storageName,\n subDirectory: this.config.subDirectory,\n sizeBytes,\n codec: this.detectedCodec,\n hasAudio: this.detectedHasAudio,\n }\n\n this.db.insertSegment(segment)\n this.eventBus.emit({\n id: `seg-${segId}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingSegmentWritten,\n data: {\n deviceId: this.config.deviceId,\n streamId: this.config.streamId,\n segmentId: segId,\n duration: buffered.duration,\n sizeBytes,\n },\n })\n } catch (err) {\n this.logger.error('Failed to write buffered segment to disk', { meta: { error: String(err) } })\n }\n }\n}\n","import sharp from 'sharp'\nimport type { IAddonFileStorage } from '@camstack/types'\nimport type { IScopedLogger, ICameraPipeline, IPipelineConsumer, FrameSubscriptionOptions, VideoFrame } from '@camstack/types'\nimport type { RecordingDb } from './recording-db.js'\n\nexport interface ThumbnailExtractorConfig {\n readonly deviceId: number\n readonly storagePath: string\n readonly storageName: string\n readonly subDirectory: string\n readonly maxWidthPx: number\n readonly jpegQuality: number\n /**\n * File storage abstraction for thumbnail persistence.\n * Thumbnails are written via this interface.\n */\n readonly fileStorage?: IAddonFileStorage\n}\n\nexport class ThumbnailExtractor implements IPipelineConsumer {\n readonly id = 'thumbnail-extractor'\n readonly name = 'Thumbnail Extractor'\n readonly needsAudio = false\n\n readonly videoRequirements: FrameSubscriptionOptions = {\n keyframeOnly: true,\n maxFps: 1,\n format: 'jpeg',\n }\n\n private unsubscribe: (() => void) | null = null\n private active = false\n\n constructor(\n private readonly config: ThumbnailExtractorConfig,\n private readonly logger: IScopedLogger,\n private readonly db: RecordingDb,\n ) {}\n\n attachToPipeline(pipeline: ICameraPipeline, _deviceId: number): void {\n this.active = true\n\n this.unsubscribe = pipeline.onVideoFrame(\n (frame) => { this.handleFrame(frame).catch((err) => this.logger.debug('Thumbnail error', { meta: { error: String(err) } })) },\n this.videoRequirements,\n )\n\n this.logger.info('ThumbnailExtractor attached', { tags: { deviceId: this.config.deviceId } })\n }\n\n detachFromPipeline(_deviceId: number): void {\n this.active = false\n if (this.unsubscribe) {\n this.unsubscribe()\n this.unsubscribe = null\n }\n this.logger.info('ThumbnailExtractor detached', { tags: { deviceId: this.config.deviceId } })\n }\n\n setActive(active: boolean): void {\n this.active = active\n }\n\n private async handleFrame(frame: VideoFrame): Promise<void> {\n if (!this.active) return\n\n const timestamp = frame.timestamp || Date.now()\n const relativePath = ThumbnailExtractor.thumbnailPath(\n this.config.subDirectory,\n this.config.deviceId,\n timestamp,\n )\n\n const resized = await sharp(frame.data)\n .resize({ width: this.config.maxWidthPx, withoutEnlargement: true })\n .jpeg({ quality: this.config.jpegQuality })\n .toBuffer()\n\n await this.config.fileStorage?.writeFile(relativePath, resized)\n\n this.db.insertThumbnail({\n deviceId: this.config.deviceId,\n timestamp,\n path: relativePath,\n storageName: this.config.storageName,\n subDirectory: this.config.subDirectory,\n sizeBytes: resized.length,\n category: 'scrub',\n })\n }\n\n static thumbnailPath(subDirectory: string, deviceId: number, timestamp: number): string {\n return `${subDirectory}/${deviceId}/${timestamp}.jpg`\n }\n}\n","import { EventCategory } from '@camstack/types'\nimport type { IScopedLogger, IEventBus, IStorageProvider } from '@camstack/types'\nimport type { RecordingDb } from './recording-db.js'\nimport type { DataCategory } from './types.js'\n\nconst NORMAL_INTERVAL_MS = 5 * 60 * 1000\nconst HIGH_USAGE_INTERVAL_MS = 30 * 1000\nconst STORAGE_WARNING_THRESHOLD = 0.80\nconst STORAGE_CRITICAL_THRESHOLD = 0.95\nconst STORAGE_HIGH_USAGE_THRESHOLD = 0.90\n\nexport class RetentionManager {\n private timer: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n private readonly db: RecordingDb,\n private readonly logger: IScopedLogger,\n private readonly eventBus: IEventBus,\n private readonly storageProvider: IStorageProvider,\n ) {}\n\n start(): void {\n this.scheduleNextCycle(NORMAL_INTERVAL_MS)\n }\n\n stop(): void {\n if (this.timer) {\n clearTimeout(this.timer)\n this.timer = null\n }\n }\n\n async runCycle(): Promise<boolean> {\n this.db.resetStaleCleanups()\n\n const policies = this.db.getEnabledPolicies()\n let totalFreedBytes = 0\n let totalDeletedSegments = 0\n let highUsage = false\n\n for (const policy of policies) {\n for (const sp of policy.streams) {\n const category = `recording:${sp.streamId}` as DataCategory\n const config = this.db.resolveStorageConfig(policy.deviceId, category)\n if (!config) continue\n\n if (config.retentionDays !== null) {\n const cutoff = Date.now() - config.retentionDays * 86400000\n const deleted = this.db.deleteSegmentsBefore(policy.deviceId, sp.streamId, cutoff)\n totalDeletedSegments += deleted.length\n for (const seg of deleted) {\n totalFreedBytes += seg.sizeBytes\n await this.deleteFile(seg.path)\n }\n this.db.deleteThumbnailsBefore(policy.deviceId, cutoff)\n }\n\n if (config.retentionGb !== null) {\n const maxBytes = config.retentionGb * 1024 * 1024 * 1024\n let usage = this.db.getStorageUsage(policy.deviceId, sp.streamId)\n\n const usageRatio = usage.totalBytes / maxBytes\n if (usageRatio > STORAGE_CRITICAL_THRESHOLD) {\n this.emitStorageEvent('recording.storage.critical', policy.deviceId, sp.streamId, usageRatio)\n } else if (usageRatio > STORAGE_WARNING_THRESHOLD) {\n this.emitStorageEvent('recording.storage.warning', policy.deviceId, sp.streamId, usageRatio)\n }\n if (usageRatio > STORAGE_HIGH_USAGE_THRESHOLD) {\n highUsage = true\n }\n\n while (usage.totalBytes > maxBytes && usage.segmentCount > 0) {\n const oldest = this.db.getOldestSegments(policy.deviceId, sp.streamId, 10)\n if (oldest.length === 0) break\n for (const seg of oldest) {\n this.db.deleteSegmentsBefore(policy.deviceId, sp.streamId, seg.endTime + 1)\n totalFreedBytes += seg.sizeBytes\n totalDeletedSegments++\n await this.deleteFile(seg.path)\n }\n usage = this.db.getStorageUsage(policy.deviceId, sp.streamId)\n }\n }\n }\n }\n\n const pending = this.db.getPendingCleanups()\n for (const entry of pending) {\n this.db.markCleanupInProgress(entry.deviceId)\n try {\n const deleted = this.db.deleteSegmentsForDevice(entry.deviceId)\n for (const seg of deleted) {\n totalFreedBytes += seg.sizeBytes\n totalDeletedSegments++\n await this.deleteFile(seg.path)\n }\n this.db.deleteThumbnailsForDevice(entry.deviceId)\n this.db.markCleanupCompleted(entry.deviceId)\n } catch (err) {\n this.logger.error('Cleanup failed', { tags: { deviceId: entry.deviceId }, meta: { error: String(err) } })\n }\n }\n\n if (totalDeletedSegments > 0) {\n this.eventBus.emit({\n id: `retention-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingRetentionCompleted,\n data: {\n freedMB: Math.round(totalFreedBytes / 1024 / 1024),\n deletedSegments: totalDeletedSegments,\n },\n })\n }\n\n return highUsage\n }\n\n private scheduleNextCycle(intervalMs: number): void {\n this.timer = setTimeout(async () => {\n try {\n const storageHighUsage = await this.runCycle()\n const nextInterval = storageHighUsage ? HIGH_USAGE_INTERVAL_MS : NORMAL_INTERVAL_MS\n this.scheduleNextCycle(nextInterval)\n } catch (err) {\n this.logger.error('Retention cycle error', { meta: { error: String(err) } })\n this.scheduleNextCycle(NORMAL_INTERVAL_MS)\n }\n }, intervalMs)\n }\n\n private emitStorageEvent(category: string, deviceId: number, streamId: string, usageRatio: number): void {\n this.eventBus.emit({\n id: `${category}-${deviceId}-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category,\n data: {\n deviceId,\n streamId,\n usagePercent: Math.round(usageRatio * 100),\n },\n })\n }\n\n private async deleteFile(filePath: string): Promise<void> {\n try {\n await this.storageProvider.delete({ location: 'recordings', relativePath: filePath })\n } catch {\n // File may already be deleted\n }\n }\n}\n","import { EventCategory } from '@camstack/types'\nimport type { IScopedLogger, IEventBus, SystemEvent, IStreamingEngine, IPipelineManager, INetworkQualityTracker, FfmpegConfig, IStorageProvider } from '@camstack/types'\nimport { resolveFfmpegConfig } from './ffmpeg-config.js'\nimport type { RecordingDb } from './recording-db.js'\nimport type {\n RecordingPolicy, RecordingEnableConfig, ScheduleRule, DataCategory,\n} from './types.js'\nimport { SegmentWriter, type SegmentWriterConfig } from './segment-writer.js'\nimport { ThumbnailExtractor, type ThumbnailExtractorConfig } from './thumbnail-extractor.js'\nimport { RetentionManager } from './retention-manager.js'\nimport { PlaylistGenerator } from './playlist-generator.js'\nimport { StorageEstimator } from './storage-estimator.js'\n\n// --- Per-device recording state ---\n\ninterface DeviceRecordingState {\n readonly deviceId: number\n readonly policy: RecordingPolicy\n readonly writers: readonly SegmentWriter[]\n readonly thumbnailExtractor: ThumbnailExtractor\n readonly motionUnsubscribe: (() => void) | null\n motionActive: boolean\n motionTimeout: ReturnType<typeof setTimeout> | null\n motionFallbackTimeout: ReturnType<typeof setTimeout> | null\n motionReceived: boolean\n}\n\n// --- Coordinator config ---\n\n/** Default segment duration when not configured (seconds). */\nconst DEFAULT_SEGMENT_DURATION_SEC = 4\n\nexport interface RecordingCoordinatorConfig {\n readonly db: RecordingDb\n readonly logger: IScopedLogger\n readonly eventBus: IEventBus\n readonly streamingEngine: IStreamingEngine\n readonly pipelineManager: IPipelineManager\n readonly networkTracker: INetworkQualityTracker\n /**\n * The capability-based storage provider. Used for ALL file operations:\n * - `resolve('recordings', relativePath)` for FFmpeg output paths\n * - `read('recordings', relativePath)` for buffer mode\n * - `delete('recordings', relativePath)` for retention cleanup\n * - `write('recordings', relativePath, data)` for buffered segment flush\n *\n * Session 7 D.4: replaces legacy `fileStorage` + `storagePath` entirely.\n */\n readonly storageProvider: IStorageProvider\n readonly globalFfmpegConfig: Partial<FfmpegConfig>\n readonly detectedFfmpegConfig: Partial<FfmpegConfig>\n /** Global segment duration from system settings (recording.segmentDurationSeconds). */\n readonly segmentDurationSec?: number\n}\n\n// --- Policy evaluation interval ---\n\nconst POLICY_EVAL_INTERVAL_MS = 1000\n\nconst MOTION_FALLBACK_TIMEOUT_MS = 60_000\n\n// --- RecordingCoordinator ---\n\nexport class RecordingCoordinator {\n private readonly db: RecordingDb\n private readonly logger: IScopedLogger\n private readonly eventBus: IEventBus\n private readonly streamingEngine: IStreamingEngine\n private readonly pipelineManager: IPipelineManager\n private readonly networkTracker: INetworkQualityTracker\n private readonly storageProvider: IStorageProvider\n private readonly globalFfmpegConfig: Partial<FfmpegConfig>\n private readonly detectedFfmpegConfig: Partial<FfmpegConfig>\n private readonly segmentDurationSec: number\n\n private readonly recordings = new Map<number, DeviceRecordingState>()\n private policyTimer: ReturnType<typeof setInterval> | null = null\n private readonly retentionManager: RetentionManager\n\n readonly playlistGenerator: PlaylistGenerator\n readonly storageEstimator: StorageEstimator\n\n constructor(config: RecordingCoordinatorConfig) {\n this.db = config.db\n this.logger = config.logger\n this.eventBus = config.eventBus\n this.streamingEngine = config.streamingEngine\n this.pipelineManager = config.pipelineManager\n this.networkTracker = config.networkTracker\n this.storageProvider = config.storageProvider\n this.globalFfmpegConfig = config.globalFfmpegConfig\n this.detectedFfmpegConfig = config.detectedFfmpegConfig\n this.segmentDurationSec = config.segmentDurationSec ?? DEFAULT_SEGMENT_DURATION_SEC\n\n this.retentionManager = new RetentionManager(\n this.db,\n this.logger.child('retention'),\n this.eventBus,\n this.storageProvider,\n )\n this.playlistGenerator = new PlaylistGenerator(this.db)\n this.storageEstimator = new StorageEstimator(this.db, this.networkTracker)\n }\n\n async start(): Promise<void> {\n this.logger.info('RecordingCoordinator starting')\n this.retentionManager.start()\n\n const enabledPolicies = this.db.getEnabledPolicies()\n for (const policy of enabledPolicies) {\n try {\n await this.enableRecording(policy.deviceId, {\n policy: {\n mode: policy.mode,\n streams: policy.streams,\n enabled: policy.enabled,\n preBufferSec: policy.preBufferSec,\n postBufferSec: policy.postBufferSec,\n scheduleRules: policy.scheduleRules,\n },\n })\n } catch (err) {\n this.logger.error('Failed to start recording', { tags: { deviceId: policy.deviceId }, meta: { error: String(err) } })\n }\n }\n\n this.policyTimer = setInterval(() => {\n this.evaluatePolicies()\n }, POLICY_EVAL_INTERVAL_MS)\n\n this.logger.info('RecordingCoordinator started')\n }\n\n stop(): void {\n this.logger.info('RecordingCoordinator stopping')\n\n if (this.policyTimer) {\n clearInterval(this.policyTimer)\n this.policyTimer = null\n }\n\n this.retentionManager.stop()\n\n for (const [deviceId] of this.recordings) {\n this.stopRecordingInternal(deviceId)\n }\n this.recordings.clear()\n\n this.logger.info('RecordingCoordinator stopped')\n }\n\n async enableRecording(deviceId: number, config: RecordingEnableConfig): Promise<void> {\n if (this.recordings.has(deviceId)) {\n this.stopRecordingInternal(deviceId)\n this.recordings.delete(deviceId)\n }\n\n const policy: RecordingPolicy = {\n deviceId,\n mode: config.policy.mode,\n streams: config.policy.streams,\n enabled: config.policy.enabled,\n preBufferSec: config.policy.preBufferSec,\n postBufferSec: config.policy.postBufferSec,\n scheduleRules: config.policy.scheduleRules,\n }\n\n this.db.upsertPolicy({\n deviceId,\n enabled: policy.enabled,\n mode: policy.mode,\n streams: policy.streams,\n preBufferSec: policy.preBufferSec,\n postBufferSec: policy.postBufferSec,\n scheduleRules: policy.scheduleRules,\n })\n\n this.db.cancelCleanup(deviceId)\n\n const ffmpegConfig = resolveFfmpegConfig(\n config.ffmpegOverrides,\n this.globalFfmpegConfig,\n this.detectedFfmpegConfig,\n )\n\n const writerMode = policy.mode === 'motion' ? 'buffer' as const : 'continuous' as const\n\n const writers: SegmentWriter[] = []\n for (const sp of policy.streams) {\n const storageConfig = this.db.resolveStorageConfig(deviceId, `recording:${sp.streamId}` as DataCategory)\n const storageName = storageConfig?.storageName ?? 'recordings'\n const subDirectory = storageConfig?.subDirectory ?? `recordings/${sp.streamId}`\n\n // storagePath resolved dynamically via storageProvider (D.4).\n // storageName may not be a valid StorageLocationType — cast for now,\n // the full location-type refactor is tracked as D.4 follow-up.\n const resolvedStoragePath = this.storageProvider.resolve({ location: storageName as 'recordings', relativePath: '' })\n\n const writerConfig: SegmentWriterConfig = {\n deviceId,\n streamId: sp.streamId,\n segmentDurationSec: this.segmentDurationSec,\n storagePath: resolvedStoragePath,\n storageName,\n subDirectory,\n ffmpeg: ffmpegConfig,\n mode: writerMode,\n preBufferSec: policy.preBufferSec,\n }\n\n const writer = new SegmentWriter(\n writerConfig,\n this.logger.child(`writer:${deviceId}:${sp.streamId}`),\n this.eventBus,\n this.db,\n this.networkTracker,\n )\n\n const rtspUrl = this.streamingEngine.getStreamUrl(`${policy.deviceId}_${sp.streamId}`, 'rtsp')\n if (rtspUrl) {\n await writer.start(rtspUrl)\n }\n\n writers.push(writer)\n }\n\n const thumbStorageConfig = this.db.resolveStorageConfig(deviceId, 'thumbnail:scrub')\n const thumbStorageName = thumbStorageConfig?.storageName ?? 'recordings'\n const thumbConfig: ThumbnailExtractorConfig = {\n deviceId,\n storagePath: this.storageProvider.resolve({ location: thumbStorageName as 'recordings', relativePath: '' }),\n storageName: thumbStorageName,\n subDirectory: thumbStorageConfig?.subDirectory ?? 'thumbnails/scrub',\n maxWidthPx: 160,\n jpegQuality: 65,\n }\n\n const thumbnailExtractor = new ThumbnailExtractor(\n thumbConfig,\n this.logger.child(`thumb:${deviceId}`),\n this.db,\n )\n\n const pipeline = this.pipelineManager.getPipeline(deviceId)\n if (pipeline) {\n thumbnailExtractor.attachToPipeline(pipeline, deviceId)\n }\n\n if (policy.mode === 'motion') {\n thumbnailExtractor.setActive(false)\n }\n\n const motionUnsubscribe = this.subscribeToMotionEvents(deviceId, policy)\n\n const state: DeviceRecordingState = {\n deviceId,\n policy,\n writers,\n thumbnailExtractor,\n motionUnsubscribe,\n motionActive: false,\n motionTimeout: null,\n motionFallbackTimeout: null,\n motionReceived: false,\n }\n\n this.recordings.set(deviceId, state)\n\n if (policy.mode === 'motion') {\n state.motionFallbackTimeout = setTimeout(() => {\n const currentState = this.recordings.get(deviceId)\n if (!currentState || currentState.motionReceived) return\n\n this.logger.warn('No motion events received — falling back to continuous recording', {\n tags: { deviceId },\n meta: { timeoutSec: MOTION_FALLBACK_TIMEOUT_MS / 1000 },\n })\n\n this.eventBus.emit({\n id: `recording-policy-fallback-${deviceId}-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingPolicyFallback,\n data: {\n deviceId,\n originalMode: 'motion',\n fallbackMode: 'continuous',\n reason: 'no_motion_events',\n },\n })\n\n for (const writer of currentState.writers) {\n writer.flushAndContinue().catch(err => {\n this.logger.error('Failed to flush buffer during fallback', { tags: { deviceId }, meta: { error: String(err) } })\n })\n }\n\n currentState.thumbnailExtractor.setActive(true)\n }, MOTION_FALLBACK_TIMEOUT_MS)\n }\n\n this.eventBus.emit({\n id: `recording-started-${deviceId}-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingStarted,\n data: {\n deviceId,\n mode: policy.mode,\n streams: policy.streams.map(s => s.streamId),\n },\n })\n\n this.logger.info('Recording enabled', { tags: { deviceId }, meta: { mode: policy.mode } })\n }\n\n async disableRecording(deviceId: number): Promise<void> {\n const state = this.recordings.get(deviceId)\n if (!state) {\n this.logger.warn('No active recording', { tags: { deviceId } })\n return\n }\n\n let totalSegmentCount = 0\n let totalSizeBytes = 0\n for (const sp of state.policy.streams) {\n const usage = this.db.getStorageUsage(deviceId, sp.streamId)\n totalSegmentCount += usage.segmentCount\n totalSizeBytes += usage.totalBytes\n }\n const totalMB = Math.round(totalSizeBytes / 1024 / 1024)\n\n this.stopRecordingInternal(deviceId)\n this.recordings.delete(deviceId)\n\n this.db.addToCleanupQueue(deviceId, Date.now())\n\n this.eventBus.emit({\n id: `recording-stopped-${deviceId}-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingStopped,\n data: {\n deviceId,\n segmentCount: totalSegmentCount,\n totalMB,\n },\n })\n\n this.logger.info('Recording disabled', { tags: { deviceId }, meta: { segmentCount: totalSegmentCount, totalMB } })\n }\n\n isRecording(deviceId: number): boolean {\n return this.recordings.has(deviceId)\n }\n\n /** Number of devices currently being recorded. */\n getActiveCount(): number {\n return this.recordings.size\n }\n\n evaluatePolicies(): void {\n const now = new Date()\n\n for (const [_deviceId, state] of this.recordings) {\n const { policy } = state\n\n if (policy.mode === 'scheduled' || policy.mode === 'composite') {\n if (!policy.scheduleRules || policy.scheduleRules.length === 0) continue\n\n const matchingRule = policy.scheduleRules.find(rule =>\n RecordingCoordinator.evaluateScheduleRule(rule, now),\n )\n\n if (matchingRule) {\n const targetMode = matchingRule.mode === 'motion' ? 'buffer' as const : 'continuous' as const\n for (const writer of state.writers) {\n if (writer.mode !== targetMode) {\n if (targetMode === 'buffer') {\n writer.switchToBuffer()\n }\n }\n }\n } else {\n for (const writer of state.writers) {\n if (writer.mode !== 'buffer') {\n writer.switchToBuffer()\n }\n }\n }\n }\n }\n }\n\n static evaluateScheduleRule(rule: ScheduleRule, date: Date): boolean {\n const dayOfWeek = date.getDay()\n const timeMinutes = date.getHours() * 60 + date.getMinutes()\n\n const [startH, startM] = rule.startTime.split(':').map(Number) as [number, number]\n const [endH, endM] = rule.endTime.split(':').map(Number) as [number, number]\n const startMinutes = startH * 60 + startM\n const endMinutes = endH * 60 + endM\n\n if (endMinutes > startMinutes) {\n return rule.days.includes(dayOfWeek)\n && timeMinutes >= startMinutes\n && timeMinutes < endMinutes\n }\n\n if (rule.days.includes(dayOfWeek) && timeMinutes >= startMinutes) {\n return true\n }\n\n const previousDay = (dayOfWeek + 6) % 7\n if (rule.days.includes(previousDay) && timeMinutes < endMinutes) {\n return true\n }\n\n return false\n }\n\n private subscribeToMotionEvents(deviceId: number, policy: RecordingPolicy): (() => void) | null {\n if (policy.mode !== 'motion' && policy.mode !== 'composite') {\n return null\n }\n\n return this.eventBus.subscribe(\n { category: `motion.${deviceId}` },\n (event: SystemEvent) => {\n this.handleMotionEvent(deviceId, event)\n },\n )\n }\n\n private handleMotionEvent(deviceId: number, event: SystemEvent): void {\n const state = this.recordings.get(deviceId)\n if (!state) return\n\n if (!state.motionReceived) {\n state.motionReceived = true\n if (state.motionFallbackTimeout) {\n clearTimeout(state.motionFallbackTimeout)\n state.motionFallbackTimeout = null\n }\n }\n\n const motionDetected = event.data.active === true || event.data.type === 'start'\n\n if (motionDetected) {\n state.motionActive = true\n\n if (state.motionTimeout) {\n clearTimeout(state.motionTimeout)\n state.motionTimeout = null\n }\n\n for (const writer of state.writers) {\n writer.flushAndContinue().catch(err => {\n this.logger.error('Failed to flush buffer', { tags: { deviceId }, meta: { error: String(err) } })\n })\n }\n\n state.thumbnailExtractor.setActive(true)\n } else {\n if (state.motionTimeout) {\n clearTimeout(state.motionTimeout)\n }\n\n state.motionTimeout = setTimeout(() => {\n state.motionActive = false\n state.motionTimeout = null\n\n for (const writer of state.writers) {\n writer.switchToBuffer()\n }\n\n if (state.policy.mode === 'motion') {\n state.thumbnailExtractor.setActive(false)\n }\n }, state.policy.postBufferSec * 1000)\n }\n }\n\n private stopRecordingInternal(deviceId: number): void {\n const state = this.recordings.get(deviceId)\n if (!state) return\n\n for (const writer of state.writers) {\n writer.stop()\n }\n\n state.thumbnailExtractor.detachFromPipeline(deviceId)\n\n if (state.motionUnsubscribe) {\n state.motionUnsubscribe()\n }\n\n if (state.motionTimeout) {\n clearTimeout(state.motionTimeout)\n state.motionTimeout = null\n }\n\n if (state.motionFallbackTimeout) {\n clearTimeout(state.motionFallbackTimeout)\n state.motionFallbackTimeout = null\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"recording-coordinator-CvJtVs3m.mjs","sources":["../src/recording/recording/segment-writer.ts","../src/recording/recording/thumbnail-extractor.ts","../src/recording/recording/retention-manager.ts","../src/recording/recording/recording-coordinator.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport { spawn, type ChildProcess } from 'node:child_process'\nimport * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\nimport { EventCategory } from '@camstack/types'\nimport type { IAddonFileStorage } from '@camstack/types'\nimport type { IScopedLogger, IEventBus, INetworkQualityTracker, FfmpegConfig } from '@camstack/types'\nimport { buildFfmpegInputArgs, buildFfmpegOutputArgs } from './ffmpeg-config.js'\nimport type { RecordingDb } from './recording-db.js'\nimport type { SegmentWriterState, RecordingSegment } from './types.js'\n\n// --- Ring Buffer for motion pre-buffer (E2) ---\n\nexport interface BufferedSegment {\n readonly data: Buffer\n readonly startTime: number\n readonly duration: number\n}\n\nexport class SegmentRingBuffer {\n private segments: BufferedSegment[] = []\n private totalDurationSec = 0\n\n constructor(private readonly maxDurationSec: number) {}\n\n push(segment: BufferedSegment): void {\n this.segments.push(segment)\n this.totalDurationSec += segment.duration\n while (this.totalDurationSec > this.maxDurationSec && this.segments.length > 1) {\n const evicted = this.segments.shift()!\n this.totalDurationSec -= evicted.duration\n }\n }\n\n flush(): BufferedSegment[] {\n const result = [...this.segments]\n this.segments = []\n this.totalDurationSec = 0\n return result\n }\n\n get memoryEstimateBytes(): number {\n return this.segments.reduce((sum, s) => sum + s.data.length, 0)\n }\n}\n\n// --- Config ---\n\nexport type SegmentWriterMode = 'continuous' | 'buffer'\n\nexport interface SegmentWriterConfig {\n readonly deviceId: number\n readonly streamId: string\n readonly segmentDurationSec: number\n readonly storagePath: string\n readonly storageName: string\n readonly subDirectory: string\n readonly ffmpeg: FfmpegConfig\n readonly mode: SegmentWriterMode\n readonly preBufferSec: number\n /**\n * File storage abstraction for segment persistence.\n * Used by writeBufferedSegmentToDisk for persisting buffered segments.\n * Note: ffmpeg still writes to storagePath directly (needs real filesystem paths).\n */\n readonly fileStorage?: IAddonFileStorage\n}\n\n// --- Disk space check result ---\n\ninterface DiskSpaceResult {\n readonly ok: boolean\n readonly availableGb: number\n}\n\n// --- Active segment tracking ---\n\ninterface ActiveSegment {\n readonly id: string\n readonly path: string\n readonly startTime: number\n}\n\n// --- statfs signature for dependency injection ---\n\ntype StatfsFn = (path: string) => Promise<{ bfree: number; bsize: number }>\n\n// --- SegmentWriter ---\n\nexport class SegmentWriter {\n private _state: SegmentWriterState = 'idle'\n private _mode: SegmentWriterMode\n private ffmpeg: ChildProcess | null = null\n private activeSegment: ActiveSegment | null = null\n private restartCount = 0\n private restartWindowStart = 0\n private healthTimer: ReturnType<typeof setInterval> | null = null\n private lastDataTime = 0\n private ringBuffer: SegmentRingBuffer\n private restartTimeout: ReturnType<typeof setTimeout> | null = null\n private pendingFinalization: Promise<void> | null = null\n private paused = false\n private detectedCodec: 'h264' | 'h265' = 'h264'\n private detectedHasAudio = false\n\n private static readonly MAX_RESTARTS = 10\n private static readonly RESTART_WINDOW_MS = 5 * 60 * 1000\n private static readonly HEALTH_CHECK_INTERVAL_MS = 5000\n private static readonly DATA_TIMEOUT_MS = 15000\n private static readonly MIN_SEGMENT_DURATION_SEC = 0.5\n private static readonly CRITICAL_DISK_GB = 1\n\n constructor(\n private readonly config: SegmentWriterConfig,\n private readonly logger: IScopedLogger,\n private readonly eventBus: IEventBus,\n private readonly db: RecordingDb,\n _networkTracker: INetworkQualityTracker,\n ) {\n this._mode = config.mode\n this.ringBuffer = new SegmentRingBuffer(config.preBufferSec)\n }\n\n get state(): SegmentWriterState {\n return this._state\n }\n\n get mode(): SegmentWriterMode {\n return this._mode\n }\n\n get isPaused(): boolean {\n return this.paused\n }\n\n // --- Public API ---\n\n async start(rtspUrl: string): Promise<void> {\n if (this._state !== 'idle') return\n\n const segmentDir = path.join(\n this.config.storagePath,\n this.config.subDirectory,\n String(this.config.deviceId),\n )\n await fs.mkdir(segmentDir, { recursive: true })\n\n this._state = 'recording'\n this.lastDataTime = Date.now()\n this.restartCount = 0\n this.restartWindowStart = Date.now()\n\n const segmentPattern = path.join(segmentDir, '%d.mp4')\n const args = SegmentWriter.buildSegmentationArgs(\n this.config.ffmpeg,\n rtspUrl,\n segmentPattern,\n this.config.segmentDurationSec,\n )\n\n this.spawnFfmpeg(args, rtspUrl)\n this.startHealthCheck(rtspUrl)\n }\n\n async stop(): Promise<void> {\n if (this._state === 'idle') return\n this._state = 'stopping'\n this.stopHealthCheck()\n this.clearRestartTimeout()\n this.killFfmpeg()\n this.finalizeActiveSegment()\n if (this.pendingFinalization) {\n await this.pendingFinalization\n }\n this._state = 'idle'\n }\n\n resume(rtspUrl: string): void {\n if (!this.paused) return\n this.paused = false\n this.logger.info('Resuming recording after disk space freed', {\n tags: { deviceId: this.config.deviceId },\n })\n this._state = 'idle'\n void this.start(rtspUrl)\n }\n\n async flushAndContinue(): Promise<void> {\n if (this._mode !== 'buffer') return\n\n const buffered = this.ringBuffer.flush()\n this.logger.info('Flushing buffered segments to disk', {\n tags: { deviceId: this.config.deviceId },\n meta: { count: buffered.length },\n })\n\n for (const seg of buffered) {\n await this.writeBufferedSegmentToDisk(seg)\n }\n\n this._mode = 'continuous'\n }\n\n switchToBuffer(): void {\n this._mode = 'buffer'\n this.ringBuffer = new SegmentRingBuffer(this.config.preBufferSec)\n }\n\n // --- Static helpers ---\n\n static generateSegmentId(deviceId: number, streamId: string, startTime: number): string {\n const suffix = randomBytes(2).toString('hex')\n return `${deviceId}_${streamId}_${startTime}_${suffix}`\n }\n\n static buildSegmentationArgs(\n config: FfmpegConfig,\n inputUrl: string,\n outputPattern: string,\n segmentDuration: number,\n ): string[] {\n const inputArgs = buildFfmpegInputArgs(config, inputUrl)\n const outputArgs = buildFfmpegOutputArgs(config)\n\n const segmentArgs = [\n '-f', 'segment',\n '-segment_time', String(segmentDuration),\n '-segment_format', 'mp4',\n '-movflags', '+frag_keyframe+empty_moov+default_base_moof',\n '-reset_timestamps', '1',\n '-strftime', '0',\n ]\n\n return [...inputArgs, ...outputArgs, ...segmentArgs, outputPattern]\n }\n\n static async checkDiskSpace(\n storagePath: string,\n statfsFn?: StatfsFn,\n ): Promise<DiskSpaceResult> {\n const doStatfs = statfsFn ?? (async (p: string) => {\n const { statfs: nodeStatfs } = await import('node:fs/promises')\n return nodeStatfs(p)\n })\n\n try {\n const stats = await doStatfs(storagePath)\n const availableBytes = stats.bfree * stats.bsize\n const availableGb = availableBytes / (1024 * 1024 * 1024)\n return { ok: availableGb >= SegmentWriter.CRITICAL_DISK_GB, availableGb }\n } catch {\n return { ok: true, availableGb: -1 }\n }\n }\n\n // --- Private: ffmpeg process management ---\n\n private spawnFfmpeg(args: string[], rtspUrl: string): void {\n this.ffmpeg = spawn(this.config.ffmpeg.path, args, {\n stdio: ['ignore', 'pipe', 'pipe'],\n })\n\n this.ffmpeg.stdout?.on('data', () => {\n this.lastDataTime = Date.now()\n })\n\n this.ffmpeg.stderr?.on('data', (data: Buffer) => {\n this.lastDataTime = Date.now()\n const msg = data.toString().trim()\n if (msg) {\n this.logger.debug('ffmpeg stderr', { meta: { msg } })\n this.parseSegmentOutput(msg)\n }\n })\n\n this.ffmpeg.on('error', (err) => {\n this.logger.warn('ffmpeg process error', { meta: { error: err.message } })\n this.handleCrash(rtspUrl)\n })\n\n this.ffmpeg.on('exit', (code) => {\n if (code !== 0 && code !== null && this._state === 'recording') {\n this.logger.warn('ffmpeg exited with non-zero code', { meta: { code } })\n this.handleCrash(rtspUrl)\n }\n })\n }\n\n private handleCrash(rtspUrl: string): void {\n this.ffmpeg = null\n const prevFinalization = this.pendingFinalization\n this.pendingFinalization = (prevFinalization ?? Promise.resolve()).then(() => {\n return this.finalizeActiveSegment()\n })\n\n if (this._state !== 'recording') return\n if (this.paused) return\n\n const now = Date.now()\n if (now - this.restartWindowStart > SegmentWriter.RESTART_WINDOW_MS) {\n this.restartCount = 0\n this.restartWindowStart = now\n }\n\n this.restartCount++\n\n this.eventBus.emit({\n id: `rec-err-${now}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingError,\n data: {\n deviceId: this.config.deviceId,\n streamId: this.config.streamId,\n restartAttempt: this.restartCount,\n },\n })\n\n if (this.restartCount > SegmentWriter.MAX_RESTARTS) {\n this.logger.error('Max restarts exceeded', {\n tags: { deviceId: this.config.deviceId, streamId: this.config.streamId },\n })\n this._state = 'idle'\n this.eventBus.emit({\n id: `rec-degraded-${now}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingHealthDegraded,\n data: {\n deviceId: this.config.deviceId,\n streamId: this.config.streamId,\n },\n })\n return\n }\n\n const backoffMs = Math.min(30000, 1000 * Math.pow(2, this.restartCount - 1))\n this.logger.info('Restarting ffmpeg', { meta: { backoffMs, attempt: this.restartCount } })\n\n this.restartTimeout = setTimeout(() => {\n this.restartTimeout = null\n if (this._state === 'recording') {\n const segmentDir = path.join(\n this.config.storagePath,\n this.config.subDirectory,\n String(this.config.deviceId),\n )\n const segmentPattern = path.join(segmentDir, '%d.mp4')\n const args = SegmentWriter.buildSegmentationArgs(\n this.config.ffmpeg,\n rtspUrl,\n segmentPattern,\n this.config.segmentDurationSec,\n )\n this.spawnFfmpeg(args, rtspUrl)\n }\n }, backoffMs)\n }\n\n // --- Private: health monitoring ---\n\n private startHealthCheck(rtspUrl: string): void {\n this.healthTimer = setInterval(() => {\n if (this._state !== 'recording') return\n const elapsed = Date.now() - this.lastDataTime\n if (elapsed > SegmentWriter.DATA_TIMEOUT_MS) {\n this.logger.warn('No data received, restarting ffmpeg', { meta: { elapsedMs: elapsed } })\n this.killFfmpeg()\n this.handleCrash(rtspUrl)\n }\n }, SegmentWriter.HEALTH_CHECK_INTERVAL_MS)\n }\n\n private stopHealthCheck(): void {\n if (this.healthTimer) {\n clearInterval(this.healthTimer)\n this.healthTimer = null\n }\n }\n\n private clearRestartTimeout(): void {\n if (this.restartTimeout) {\n clearTimeout(this.restartTimeout)\n this.restartTimeout = null\n }\n }\n\n private killFfmpeg(): void {\n if (this.ffmpeg) {\n this.ffmpeg.kill('SIGTERM')\n this.ffmpeg = null\n }\n }\n\n // --- Private: segment parsing and finalization ---\n\n private parseSegmentOutput(msg: string): void {\n const videoMatch = msg.match(/Stream\\s+#\\d+:\\d+.*Video:\\s+(h264|hevc|h265)/i)\n if (videoMatch) {\n const codec = videoMatch[1]!.toLowerCase()\n this.detectedCodec = (codec === 'hevc' || codec === 'h265') ? 'h265' : 'h264'\n }\n\n const audioMatch = msg.match(/Stream\\s+#\\d+:\\d+.*Audio:/i)\n if (audioMatch) {\n this.detectedHasAudio = true\n }\n\n const openMatch = msg.match(/Opening '(.+\\.mp4)' for writing/)\n if (openMatch) {\n const prevFinalization = this.pendingFinalization\n this.pendingFinalization = (prevFinalization ?? Promise.resolve()).then(() => {\n return this.finalizeActiveSegment()\n })\n\n const absolutePath = openMatch[1]!\n const segPath = absolutePath.startsWith(this.config.storagePath)\n ? absolutePath.slice(this.config.storagePath.length).replace(/^\\//, '')\n : absolutePath\n this.activeSegment = {\n id: SegmentWriter.generateSegmentId(\n this.config.deviceId,\n this.config.streamId,\n Date.now(),\n ),\n path: segPath,\n startTime: Date.now(),\n }\n }\n }\n\n private async finalizeActiveSegment(): Promise<void> {\n if (!this.activeSegment) return\n const seg = this.activeSegment\n this.activeSegment = null\n\n const endTime = Date.now()\n const duration = (endTime - seg.startTime) / 1000\n\n if (duration < SegmentWriter.MIN_SEGMENT_DURATION_SEC) return\n\n if (this._mode === 'buffer') {\n await this.bufferSegmentFromDisk(seg, endTime, duration)\n return\n }\n\n await this.finalizeSegmentToDisk(seg, endTime, duration)\n }\n\n private async bufferSegmentFromDisk(\n seg: ActiveSegment,\n _endTime: number,\n duration: number,\n ): Promise<void> {\n try {\n const data = await fs.readFile(seg.path)\n this.ringBuffer.push({ data, startTime: seg.startTime, duration })\n await fs.unlink(seg.path).catch(() => {})\n } catch (err) {\n this.logger.warn('Failed to buffer segment', { meta: { error: String(err) } })\n }\n }\n\n private async finalizeSegmentToDisk(\n seg: ActiveSegment,\n endTime: number,\n duration: number,\n ): Promise<void> {\n try {\n const diskCheck = await SegmentWriter.checkDiskSpace(this.config.storagePath)\n\n if (!diskCheck.ok) {\n this.eventBus.emit({\n id: `storage-critical-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingStorageCritical,\n data: {\n storageId: this.config.storageName,\n availableGB: diskCheck.availableGb,\n },\n })\n this.logger.error('Disk space critically low, pausing recording')\n this.paused = true\n this.killFfmpeg()\n this._state = 'idle'\n return\n }\n\n let sizeBytes = 0\n try {\n const fileStat = await fs.stat(seg.path)\n sizeBytes = fileStat.size\n } catch {\n // File may not exist yet or was removed\n }\n\n const codec = this.detectedCodec\n const hasAudio = this.detectedHasAudio\n\n const segment: RecordingSegment = {\n id: seg.id,\n deviceId: this.config.deviceId,\n streamId: this.config.streamId,\n startTime: seg.startTime,\n endTime,\n duration,\n path: seg.path,\n storageName: this.config.storageName,\n subDirectory: this.config.subDirectory,\n sizeBytes,\n codec,\n hasAudio,\n }\n\n try {\n this.db.insertSegment(segment)\n this.eventBus.emit({\n id: `seg-${seg.id}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingSegmentWritten,\n data: {\n deviceId: this.config.deviceId,\n streamId: this.config.streamId,\n segmentId: seg.id,\n duration,\n sizeBytes,\n },\n })\n } catch (err) {\n this.logger.error('Failed to insert segment', { meta: { error: String(err) } })\n }\n } catch (err) {\n this.logger.error('Disk space check failed', { meta: { error: String(err) } })\n }\n }\n\n private async writeBufferedSegmentToDisk(buffered: BufferedSegment): Promise<void> {\n const segId = SegmentWriter.generateSegmentId(\n this.config.deviceId,\n this.config.streamId,\n buffered.startTime,\n )\n const relativePath = `${this.config.subDirectory}/${this.config.deviceId}/${segId}.mp4`\n\n try {\n await this.config.fileStorage?.writeFile(relativePath, buffered.data)\n const sizeBytes = buffered.data.length\n\n const segment: RecordingSegment = {\n id: segId,\n deviceId: this.config.deviceId,\n streamId: this.config.streamId,\n startTime: buffered.startTime,\n endTime: buffered.startTime + buffered.duration * 1000,\n duration: buffered.duration,\n path: relativePath,\n storageName: this.config.storageName,\n subDirectory: this.config.subDirectory,\n sizeBytes,\n codec: this.detectedCodec,\n hasAudio: this.detectedHasAudio,\n }\n\n this.db.insertSegment(segment)\n this.eventBus.emit({\n id: `seg-${segId}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingSegmentWritten,\n data: {\n deviceId: this.config.deviceId,\n streamId: this.config.streamId,\n segmentId: segId,\n duration: buffered.duration,\n sizeBytes,\n },\n })\n } catch (err) {\n this.logger.error('Failed to write buffered segment to disk', { meta: { error: String(err) } })\n }\n }\n}\n","import sharp from 'sharp'\nimport type { IAddonFileStorage } from '@camstack/types'\nimport type { IScopedLogger, ICameraPipeline, IPipelineConsumer, FrameSubscriptionOptions, VideoFrame } from '@camstack/types'\nimport type { RecordingDb } from './recording-db.js'\n\nexport interface ThumbnailExtractorConfig {\n readonly deviceId: number\n readonly storagePath: string\n readonly storageName: string\n readonly subDirectory: string\n readonly maxWidthPx: number\n readonly jpegQuality: number\n /**\n * File storage abstraction for thumbnail persistence.\n * Thumbnails are written via this interface.\n */\n readonly fileStorage?: IAddonFileStorage\n}\n\nexport class ThumbnailExtractor implements IPipelineConsumer {\n readonly id = 'thumbnail-extractor'\n readonly name = 'Thumbnail Extractor'\n readonly needsAudio = false\n\n readonly videoRequirements: FrameSubscriptionOptions = {\n keyframeOnly: true,\n maxFps: 1,\n format: 'jpeg',\n }\n\n private unsubscribe: (() => void) | null = null\n private active = false\n\n constructor(\n private readonly config: ThumbnailExtractorConfig,\n private readonly logger: IScopedLogger,\n private readonly db: RecordingDb,\n ) {}\n\n attachToPipeline(pipeline: ICameraPipeline, _deviceId: number): void {\n this.active = true\n\n this.unsubscribe = pipeline.onVideoFrame(\n (frame) => { this.handleFrame(frame).catch((err) => this.logger.debug('Thumbnail error', { meta: { error: String(err) } })) },\n this.videoRequirements,\n )\n\n this.logger.info('ThumbnailExtractor attached', { tags: { deviceId: this.config.deviceId } })\n }\n\n detachFromPipeline(_deviceId: number): void {\n this.active = false\n if (this.unsubscribe) {\n this.unsubscribe()\n this.unsubscribe = null\n }\n this.logger.info('ThumbnailExtractor detached', { tags: { deviceId: this.config.deviceId } })\n }\n\n setActive(active: boolean): void {\n this.active = active\n }\n\n private async handleFrame(frame: VideoFrame): Promise<void> {\n if (!this.active) return\n\n const timestamp = frame.timestamp || Date.now()\n const relativePath = ThumbnailExtractor.thumbnailPath(\n this.config.subDirectory,\n this.config.deviceId,\n timestamp,\n )\n\n const resized = await sharp(frame.data)\n .resize({ width: this.config.maxWidthPx, withoutEnlargement: true })\n .jpeg({ quality: this.config.jpegQuality })\n .toBuffer()\n\n await this.config.fileStorage?.writeFile(relativePath, resized)\n\n this.db.insertThumbnail({\n deviceId: this.config.deviceId,\n timestamp,\n path: relativePath,\n storageName: this.config.storageName,\n subDirectory: this.config.subDirectory,\n sizeBytes: resized.length,\n category: 'scrub',\n })\n }\n\n static thumbnailPath(subDirectory: string, deviceId: number, timestamp: number): string {\n return `${subDirectory}/${deviceId}/${timestamp}.jpg`\n }\n}\n","import { EventCategory } from '@camstack/types'\nimport type { IScopedLogger, IEventBus, IStorageProvider } from '@camstack/types'\nimport type { RecordingDb } from './recording-db.js'\nimport type { DataCategory } from './types.js'\n\nconst NORMAL_INTERVAL_MS = 5 * 60 * 1000\nconst HIGH_USAGE_INTERVAL_MS = 30 * 1000\nconst STORAGE_WARNING_THRESHOLD = 0.80\nconst STORAGE_CRITICAL_THRESHOLD = 0.95\nconst STORAGE_HIGH_USAGE_THRESHOLD = 0.90\n\nexport class RetentionManager {\n private timer: ReturnType<typeof setTimeout> | null = null\n\n constructor(\n private readonly db: RecordingDb,\n private readonly logger: IScopedLogger,\n private readonly eventBus: IEventBus,\n private readonly storageProvider: IStorageProvider,\n ) {}\n\n start(): void {\n this.scheduleNextCycle(NORMAL_INTERVAL_MS)\n }\n\n stop(): void {\n if (this.timer) {\n clearTimeout(this.timer)\n this.timer = null\n }\n }\n\n async runCycle(): Promise<boolean> {\n this.db.resetStaleCleanups()\n\n const policies = this.db.getEnabledPolicies()\n let totalFreedBytes = 0\n let totalDeletedSegments = 0\n let highUsage = false\n\n for (const policy of policies) {\n for (const sp of policy.streams) {\n const category = `recording:${sp.streamId}` as DataCategory\n const config = this.db.resolveStorageConfig(policy.deviceId, category)\n if (!config) continue\n\n if (config.retentionDays !== null) {\n const cutoff = Date.now() - config.retentionDays * 86400000\n const deleted = this.db.deleteSegmentsBefore(policy.deviceId, sp.streamId, cutoff)\n totalDeletedSegments += deleted.length\n for (const seg of deleted) {\n totalFreedBytes += seg.sizeBytes\n await this.deleteFile(seg.path)\n }\n this.db.deleteThumbnailsBefore(policy.deviceId, cutoff)\n }\n\n if (config.retentionGb !== null) {\n const maxBytes = config.retentionGb * 1024 * 1024 * 1024\n let usage = this.db.getStorageUsage(policy.deviceId, sp.streamId)\n\n const usageRatio = usage.totalBytes / maxBytes\n if (usageRatio > STORAGE_CRITICAL_THRESHOLD) {\n this.emitStorageEvent('recording.storage.critical', policy.deviceId, sp.streamId, usageRatio)\n } else if (usageRatio > STORAGE_WARNING_THRESHOLD) {\n this.emitStorageEvent('recording.storage.warning', policy.deviceId, sp.streamId, usageRatio)\n }\n if (usageRatio > STORAGE_HIGH_USAGE_THRESHOLD) {\n highUsage = true\n }\n\n while (usage.totalBytes > maxBytes && usage.segmentCount > 0) {\n const oldest = this.db.getOldestSegments(policy.deviceId, sp.streamId, 10)\n if (oldest.length === 0) break\n for (const seg of oldest) {\n this.db.deleteSegmentsBefore(policy.deviceId, sp.streamId, seg.endTime + 1)\n totalFreedBytes += seg.sizeBytes\n totalDeletedSegments++\n await this.deleteFile(seg.path)\n }\n usage = this.db.getStorageUsage(policy.deviceId, sp.streamId)\n }\n }\n }\n }\n\n const pending = this.db.getPendingCleanups()\n for (const entry of pending) {\n this.db.markCleanupInProgress(entry.deviceId)\n try {\n const deleted = this.db.deleteSegmentsForDevice(entry.deviceId)\n for (const seg of deleted) {\n totalFreedBytes += seg.sizeBytes\n totalDeletedSegments++\n await this.deleteFile(seg.path)\n }\n this.db.deleteThumbnailsForDevice(entry.deviceId)\n this.db.markCleanupCompleted(entry.deviceId)\n } catch (err) {\n this.logger.error('Cleanup failed', { tags: { deviceId: entry.deviceId }, meta: { error: String(err) } })\n }\n }\n\n if (totalDeletedSegments > 0) {\n this.eventBus.emit({\n id: `retention-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingRetentionCompleted,\n data: {\n freedMB: Math.round(totalFreedBytes / 1024 / 1024),\n deletedSegments: totalDeletedSegments,\n },\n })\n }\n\n return highUsage\n }\n\n private scheduleNextCycle(intervalMs: number): void {\n this.timer = setTimeout(async () => {\n try {\n const storageHighUsage = await this.runCycle()\n const nextInterval = storageHighUsage ? HIGH_USAGE_INTERVAL_MS : NORMAL_INTERVAL_MS\n this.scheduleNextCycle(nextInterval)\n } catch (err) {\n this.logger.error('Retention cycle error', { meta: { error: String(err) } })\n this.scheduleNextCycle(NORMAL_INTERVAL_MS)\n }\n }, intervalMs)\n }\n\n private emitStorageEvent(category: string, deviceId: number, streamId: string, usageRatio: number): void {\n this.eventBus.emit({\n id: `${category}-${deviceId}-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category,\n data: {\n deviceId,\n streamId,\n usagePercent: Math.round(usageRatio * 100),\n },\n })\n }\n\n private async deleteFile(filePath: string): Promise<void> {\n try {\n await this.storageProvider.delete({ location: 'recordings', relativePath: filePath })\n } catch {\n // File may already be deleted\n }\n }\n}\n","import { EventCategory } from '@camstack/types'\nimport type { IScopedLogger, IEventBus, SystemEvent, IStreamingEngine, IPipelineManager, INetworkQualityTracker, FfmpegConfig, IStorageProvider } from '@camstack/types'\nimport { resolveFfmpegConfig } from './ffmpeg-config.js'\nimport type { RecordingDb } from './recording-db.js'\nimport type {\n RecordingPolicy, RecordingEnableConfig, ScheduleRule, DataCategory,\n} from './types.js'\nimport { SegmentWriter, type SegmentWriterConfig } from './segment-writer.js'\nimport { ThumbnailExtractor, type ThumbnailExtractorConfig } from './thumbnail-extractor.js'\nimport { RetentionManager } from './retention-manager.js'\nimport { PlaylistGenerator } from './playlist-generator.js'\nimport { StorageEstimator } from './storage-estimator.js'\n\n// --- Per-device recording state ---\n\ninterface DeviceRecordingState {\n readonly deviceId: number\n readonly policy: RecordingPolicy\n readonly writers: readonly SegmentWriter[]\n readonly thumbnailExtractor: ThumbnailExtractor\n readonly motionUnsubscribe: (() => void) | null\n motionActive: boolean\n motionTimeout: ReturnType<typeof setTimeout> | null\n motionFallbackTimeout: ReturnType<typeof setTimeout> | null\n motionReceived: boolean\n}\n\n// --- Coordinator config ---\n\n/** Default segment duration when not configured (seconds). */\nconst DEFAULT_SEGMENT_DURATION_SEC = 4\n\nexport interface RecordingCoordinatorConfig {\n readonly db: RecordingDb\n readonly logger: IScopedLogger\n readonly eventBus: IEventBus\n readonly streamingEngine: IStreamingEngine\n readonly pipelineManager: IPipelineManager\n readonly networkTracker: INetworkQualityTracker\n /**\n * The capability-based storage provider. Used for ALL file operations:\n * - `resolve('recordings', relativePath)` for FFmpeg output paths\n * - `read('recordings', relativePath)` for buffer mode\n * - `delete('recordings', relativePath)` for retention cleanup\n * - `write('recordings', relativePath, data)` for buffered segment flush\n *\n * Session 7 D.4: replaces legacy `fileStorage` + `storagePath` entirely.\n */\n readonly storageProvider: IStorageProvider\n readonly globalFfmpegConfig: Partial<FfmpegConfig>\n readonly detectedFfmpegConfig: Partial<FfmpegConfig>\n /** Global segment duration from system settings (recording.segmentDurationSeconds). */\n readonly segmentDurationSec?: number\n}\n\n// --- Policy evaluation interval ---\n\nconst POLICY_EVAL_INTERVAL_MS = 1000\n\nconst MOTION_FALLBACK_TIMEOUT_MS = 60_000\n\n// --- RecordingCoordinator ---\n\nexport class RecordingCoordinator {\n private readonly db: RecordingDb\n private readonly logger: IScopedLogger\n private readonly eventBus: IEventBus\n private readonly streamingEngine: IStreamingEngine\n private readonly pipelineManager: IPipelineManager\n private readonly networkTracker: INetworkQualityTracker\n private readonly storageProvider: IStorageProvider\n private readonly globalFfmpegConfig: Partial<FfmpegConfig>\n private readonly detectedFfmpegConfig: Partial<FfmpegConfig>\n private readonly segmentDurationSec: number\n\n private readonly recordings = new Map<number, DeviceRecordingState>()\n private policyTimer: ReturnType<typeof setInterval> | null = null\n private readonly retentionManager: RetentionManager\n\n readonly playlistGenerator: PlaylistGenerator\n readonly storageEstimator: StorageEstimator\n\n constructor(config: RecordingCoordinatorConfig) {\n this.db = config.db\n this.logger = config.logger\n this.eventBus = config.eventBus\n this.streamingEngine = config.streamingEngine\n this.pipelineManager = config.pipelineManager\n this.networkTracker = config.networkTracker\n this.storageProvider = config.storageProvider\n this.globalFfmpegConfig = config.globalFfmpegConfig\n this.detectedFfmpegConfig = config.detectedFfmpegConfig\n this.segmentDurationSec = config.segmentDurationSec ?? DEFAULT_SEGMENT_DURATION_SEC\n\n this.retentionManager = new RetentionManager(\n this.db,\n this.logger.child('retention'),\n this.eventBus,\n this.storageProvider,\n )\n this.playlistGenerator = new PlaylistGenerator(this.db)\n this.storageEstimator = new StorageEstimator(this.db, this.networkTracker)\n }\n\n async start(): Promise<void> {\n this.logger.info('RecordingCoordinator starting')\n this.retentionManager.start()\n\n const enabledPolicies = this.db.getEnabledPolicies()\n for (const policy of enabledPolicies) {\n try {\n await this.enableRecording(policy.deviceId, {\n policy: {\n mode: policy.mode,\n streams: policy.streams,\n enabled: policy.enabled,\n preBufferSec: policy.preBufferSec,\n postBufferSec: policy.postBufferSec,\n scheduleRules: policy.scheduleRules,\n },\n })\n } catch (err) {\n this.logger.error('Failed to start recording', { tags: { deviceId: policy.deviceId }, meta: { error: String(err) } })\n }\n }\n\n this.policyTimer = setInterval(() => {\n this.evaluatePolicies()\n }, POLICY_EVAL_INTERVAL_MS)\n\n this.logger.info('RecordingCoordinator started')\n }\n\n stop(): void {\n this.logger.info('RecordingCoordinator stopping')\n\n if (this.policyTimer) {\n clearInterval(this.policyTimer)\n this.policyTimer = null\n }\n\n this.retentionManager.stop()\n\n for (const [deviceId] of this.recordings) {\n this.stopRecordingInternal(deviceId)\n }\n this.recordings.clear()\n\n this.logger.info('RecordingCoordinator stopped')\n }\n\n async enableRecording(deviceId: number, config: RecordingEnableConfig): Promise<void> {\n if (this.recordings.has(deviceId)) {\n this.stopRecordingInternal(deviceId)\n this.recordings.delete(deviceId)\n }\n\n const policy: RecordingPolicy = {\n deviceId,\n mode: config.policy.mode,\n streams: config.policy.streams,\n enabled: config.policy.enabled,\n preBufferSec: config.policy.preBufferSec,\n postBufferSec: config.policy.postBufferSec,\n scheduleRules: config.policy.scheduleRules,\n }\n\n this.db.upsertPolicy({\n deviceId,\n enabled: policy.enabled,\n mode: policy.mode,\n streams: policy.streams,\n preBufferSec: policy.preBufferSec,\n postBufferSec: policy.postBufferSec,\n scheduleRules: policy.scheduleRules,\n })\n\n this.db.cancelCleanup(deviceId)\n\n const ffmpegConfig = resolveFfmpegConfig(\n config.ffmpegOverrides,\n this.globalFfmpegConfig,\n this.detectedFfmpegConfig,\n )\n\n const writerMode = policy.mode === 'motion' ? 'buffer' as const : 'continuous' as const\n\n const writers: SegmentWriter[] = []\n for (const sp of policy.streams) {\n const storageConfig = this.db.resolveStorageConfig(deviceId, `recording:${sp.streamId}` as DataCategory)\n const storageName = storageConfig?.storageName ?? 'recordings'\n const subDirectory = storageConfig?.subDirectory ?? `recordings/${sp.streamId}`\n\n // storagePath resolved dynamically via storageProvider (D.4).\n // storageName may not be a valid StorageLocationType — cast for now,\n // the full location-type refactor is tracked as D.4 follow-up.\n const resolvedStoragePath = await this.storageProvider.resolve({ location: storageName as 'recordings', relativePath: '' })\n\n const writerConfig: SegmentWriterConfig = {\n deviceId,\n streamId: sp.streamId,\n segmentDurationSec: this.segmentDurationSec,\n storagePath: resolvedStoragePath,\n storageName,\n subDirectory,\n ffmpeg: ffmpegConfig,\n mode: writerMode,\n preBufferSec: policy.preBufferSec,\n }\n\n const writer = new SegmentWriter(\n writerConfig,\n this.logger.child(`writer:${deviceId}:${sp.streamId}`),\n this.eventBus,\n this.db,\n this.networkTracker,\n )\n\n const rtspUrl = this.streamingEngine.getStreamUrl(`${policy.deviceId}_${sp.streamId}`, 'rtsp')\n if (rtspUrl) {\n await writer.start(rtspUrl)\n }\n\n writers.push(writer)\n }\n\n const thumbStorageConfig = this.db.resolveStorageConfig(deviceId, 'thumbnail:scrub')\n const thumbStorageName = thumbStorageConfig?.storageName ?? 'recordings'\n const thumbConfig: ThumbnailExtractorConfig = {\n deviceId,\n storagePath: await this.storageProvider.resolve({ location: thumbStorageName as 'recordings', relativePath: '' }),\n storageName: thumbStorageName,\n subDirectory: thumbStorageConfig?.subDirectory ?? 'thumbnails/scrub',\n maxWidthPx: 160,\n jpegQuality: 65,\n }\n\n const thumbnailExtractor = new ThumbnailExtractor(\n thumbConfig,\n this.logger.child(`thumb:${deviceId}`),\n this.db,\n )\n\n const pipeline = this.pipelineManager.getPipeline(deviceId)\n if (pipeline) {\n thumbnailExtractor.attachToPipeline(pipeline, deviceId)\n }\n\n if (policy.mode === 'motion') {\n thumbnailExtractor.setActive(false)\n }\n\n const motionUnsubscribe = this.subscribeToMotionEvents(deviceId, policy)\n\n const state: DeviceRecordingState = {\n deviceId,\n policy,\n writers,\n thumbnailExtractor,\n motionUnsubscribe,\n motionActive: false,\n motionTimeout: null,\n motionFallbackTimeout: null,\n motionReceived: false,\n }\n\n this.recordings.set(deviceId, state)\n\n if (policy.mode === 'motion') {\n state.motionFallbackTimeout = setTimeout(() => {\n const currentState = this.recordings.get(deviceId)\n if (!currentState || currentState.motionReceived) return\n\n this.logger.warn('No motion events received — falling back to continuous recording', {\n tags: { deviceId },\n meta: { timeoutSec: MOTION_FALLBACK_TIMEOUT_MS / 1000 },\n })\n\n this.eventBus.emit({\n id: `recording-policy-fallback-${deviceId}-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingPolicyFallback,\n data: {\n deviceId,\n originalMode: 'motion',\n fallbackMode: 'continuous',\n reason: 'no_motion_events',\n },\n })\n\n for (const writer of currentState.writers) {\n writer.flushAndContinue().catch(err => {\n this.logger.error('Failed to flush buffer during fallback', { tags: { deviceId }, meta: { error: String(err) } })\n })\n }\n\n currentState.thumbnailExtractor.setActive(true)\n }, MOTION_FALLBACK_TIMEOUT_MS)\n }\n\n this.eventBus.emit({\n id: `recording-started-${deviceId}-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingStarted,\n data: {\n deviceId,\n mode: policy.mode,\n streams: policy.streams.map(s => s.streamId),\n },\n })\n\n this.logger.info('Recording enabled', { tags: { deviceId }, meta: { mode: policy.mode } })\n }\n\n async disableRecording(deviceId: number): Promise<void> {\n const state = this.recordings.get(deviceId)\n if (!state) {\n this.logger.warn('No active recording', { tags: { deviceId } })\n return\n }\n\n let totalSegmentCount = 0\n let totalSizeBytes = 0\n for (const sp of state.policy.streams) {\n const usage = this.db.getStorageUsage(deviceId, sp.streamId)\n totalSegmentCount += usage.segmentCount\n totalSizeBytes += usage.totalBytes\n }\n const totalMB = Math.round(totalSizeBytes / 1024 / 1024)\n\n this.stopRecordingInternal(deviceId)\n this.recordings.delete(deviceId)\n\n this.db.addToCleanupQueue(deviceId, Date.now())\n\n this.eventBus.emit({\n id: `recording-stopped-${deviceId}-${Date.now()}`,\n timestamp: new Date(),\n source: { type: 'addon', id: 'recording-engine' },\n category: EventCategory.RecordingStopped,\n data: {\n deviceId,\n segmentCount: totalSegmentCount,\n totalMB,\n },\n })\n\n this.logger.info('Recording disabled', { tags: { deviceId }, meta: { segmentCount: totalSegmentCount, totalMB } })\n }\n\n isRecording(deviceId: number): boolean {\n return this.recordings.has(deviceId)\n }\n\n /** Number of devices currently being recorded. */\n getActiveCount(): number {\n return this.recordings.size\n }\n\n evaluatePolicies(): void {\n const now = new Date()\n\n for (const [_deviceId, state] of this.recordings) {\n const { policy } = state\n\n if (policy.mode === 'scheduled' || policy.mode === 'composite') {\n if (!policy.scheduleRules || policy.scheduleRules.length === 0) continue\n\n const matchingRule = policy.scheduleRules.find(rule =>\n RecordingCoordinator.evaluateScheduleRule(rule, now),\n )\n\n if (matchingRule) {\n const targetMode = matchingRule.mode === 'motion' ? 'buffer' as const : 'continuous' as const\n for (const writer of state.writers) {\n if (writer.mode !== targetMode) {\n if (targetMode === 'buffer') {\n writer.switchToBuffer()\n }\n }\n }\n } else {\n for (const writer of state.writers) {\n if (writer.mode !== 'buffer') {\n writer.switchToBuffer()\n }\n }\n }\n }\n }\n }\n\n static evaluateScheduleRule(rule: ScheduleRule, date: Date): boolean {\n const dayOfWeek = date.getDay()\n const timeMinutes = date.getHours() * 60 + date.getMinutes()\n\n const [startH, startM] = rule.startTime.split(':').map(Number) as [number, number]\n const [endH, endM] = rule.endTime.split(':').map(Number) as [number, number]\n const startMinutes = startH * 60 + startM\n const endMinutes = endH * 60 + endM\n\n if (endMinutes > startMinutes) {\n return rule.days.includes(dayOfWeek)\n && timeMinutes >= startMinutes\n && timeMinutes < endMinutes\n }\n\n if (rule.days.includes(dayOfWeek) && timeMinutes >= startMinutes) {\n return true\n }\n\n const previousDay = (dayOfWeek + 6) % 7\n if (rule.days.includes(previousDay) && timeMinutes < endMinutes) {\n return true\n }\n\n return false\n }\n\n private subscribeToMotionEvents(deviceId: number, policy: RecordingPolicy): (() => void) | null {\n if (policy.mode !== 'motion' && policy.mode !== 'composite') {\n return null\n }\n\n return this.eventBus.subscribe(\n { category: `motion.${deviceId}` },\n (event: SystemEvent) => {\n this.handleMotionEvent(deviceId, event)\n },\n )\n }\n\n private handleMotionEvent(deviceId: number, event: SystemEvent): void {\n const state = this.recordings.get(deviceId)\n if (!state) return\n\n if (!state.motionReceived) {\n state.motionReceived = true\n if (state.motionFallbackTimeout) {\n clearTimeout(state.motionFallbackTimeout)\n state.motionFallbackTimeout = null\n }\n }\n\n const motionDetected = event.data.active === true || event.data.type === 'start'\n\n if (motionDetected) {\n state.motionActive = true\n\n if (state.motionTimeout) {\n clearTimeout(state.motionTimeout)\n state.motionTimeout = null\n }\n\n for (const writer of state.writers) {\n writer.flushAndContinue().catch(err => {\n this.logger.error('Failed to flush buffer', { tags: { deviceId }, meta: { error: String(err) } })\n })\n }\n\n state.thumbnailExtractor.setActive(true)\n } else {\n if (state.motionTimeout) {\n clearTimeout(state.motionTimeout)\n }\n\n state.motionTimeout = setTimeout(() => {\n state.motionActive = false\n state.motionTimeout = null\n\n for (const writer of state.writers) {\n writer.switchToBuffer()\n }\n\n if (state.policy.mode === 'motion') {\n state.thumbnailExtractor.setActive(false)\n }\n }, state.policy.postBufferSec * 1000)\n }\n }\n\n private stopRecordingInternal(deviceId: number): void {\n const state = this.recordings.get(deviceId)\n if (!state) return\n\n for (const writer of state.writers) {\n writer.stop()\n }\n\n state.thumbnailExtractor.detachFromPipeline(deviceId)\n\n if (state.motionUnsubscribe) {\n state.motionUnsubscribe()\n }\n\n if (state.motionTimeout) {\n clearTimeout(state.motionTimeout)\n state.motionTimeout = null\n }\n\n if (state.motionFallbackTimeout) {\n clearTimeout(state.motionFallbackTimeout)\n state.motionFallbackTimeout = null\n }\n }\n}\n"],"names":[],"mappings":"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"}