@camstack/addon-post-analysis 0.1.11 → 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.
- package/assets/icon.svg +7 -0
- package/dist/embedding-encoder/index.js +6 -6
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/enrichment-engine/index.js +46 -47
- package/dist/enrichment-engine/index.js.map +1 -1
- package/dist/enrichment-engine/index.mjs +46 -47
- package/dist/enrichment-engine/index.mjs.map +1 -1
- package/dist/index-BJKSB953.js +13565 -0
- package/dist/index-BJKSB953.js.map +1 -0
- package/dist/index-BThK2F-p.mjs +13566 -0
- package/dist/index-BThK2F-p.mjs.map +1 -0
- package/dist/pipeline-analytics/@mf-types.zip +0 -0
- 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
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-UzRdrF-t.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Vw8HvV_Q.mjs} +6 -6
- package/dist/pipeline-analytics/{hostInit-Bs26Ch2I.mjs → hostInit-PQYMQJ_C.mjs} +6 -6
- package/dist/pipeline-analytics/{index-DyF0fAsr.mjs → index-BSmxqDqD.mjs} +66 -43
- package/dist/pipeline-analytics/{index-DUJwOcGq.mjs → index-Bpv0NSqI.mjs} +1733 -1569
- package/dist/pipeline-analytics/{index-DkY0uSjx.mjs → index-D7qTzYFz.mjs} +378 -373
- package/dist/pipeline-analytics/index.js +21 -21
- package/dist/pipeline-analytics/index.mjs +1 -1
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/dist/recording/index.js +5 -5
- package/dist/recording/index.mjs +2 -2
- package/dist/{recording-coordinator-DuP3BUTV.mjs → recording-coordinator-CvJtVs3m.mjs} +4 -4
- package/dist/{recording-coordinator-DuP3BUTV.mjs.map → recording-coordinator-CvJtVs3m.mjs.map} +1 -1
- package/dist/{recording-coordinator-C2sATEhe.js → recording-coordinator-DH1gmm5G.js} +13 -13
- package/dist/{recording-coordinator-C2sATEhe.js.map → recording-coordinator-DH1gmm5G.js.map} +1 -1
- package/package.json +15 -6
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-DXo9zkw7.mjs +0 -15
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1945
|
+
capability: index.pipelineAnalyticsCapability,
|
|
1946
1946
|
provider: this,
|
|
1947
1947
|
kind: "wrapper",
|
|
1948
1948
|
defaultActive: true
|
|
1949
1949
|
},
|
|
1950
1950
|
{
|
|
1951
|
-
capability:
|
|
1951
|
+
capability: index.zoneAnalyticsCapability,
|
|
1952
1952
|
provider: this.zoneAnalytics
|
|
1953
1953
|
},
|
|
1954
1954
|
{
|
|
1955
|
-
capability:
|
|
1955
|
+
capability: index.audioMetricsCapability,
|
|
1956
1956
|
provider: this.audioMetrics
|
|
1957
1957
|
},
|
|
1958
1958
|
{
|
|
1959
|
-
capability:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 ?
|
|
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 ===
|
|
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 "
|
|
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-
|
|
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
|
}
|
package/dist/recording/index.js
CHANGED
|
@@ -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
|
|
26
|
-
class RecordingAddon extends
|
|
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-
|
|
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:
|
|
100
|
+
return [{ capability: index.recordingEngineCapability, provider: this.serviceFacade }];
|
|
101
101
|
} catch (error) {
|
|
102
|
-
const msg =
|
|
102
|
+
const msg = index.errMsg(error);
|
|
103
103
|
this.ctx.logger.warn("Recording Engine failed to initialize", { meta: { error: msg } });
|
|
104
104
|
}
|
|
105
105
|
}
|
package/dist/recording/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseAddon, recordingEngineCapability, errMsg } from "
|
|
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-
|
|
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 "
|
|
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-
|
|
1012
|
+
//# sourceMappingURL=recording-coordinator-CvJtVs3m.mjs.map
|
package/dist/{recording-coordinator-DuP3BUTV.mjs.map → recording-coordinator-CvJtVs3m.mjs.map}
RENAMED
|
@@ -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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"}
|