@camstack/addon-post-analysis 0.1.19 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/dist-4mTLJ7BJ.mjs +20750 -0
  2. package/dist/dist-CS2K80so.js +20933 -0
  3. package/dist/embedding-encoder/index.js +977 -902
  4. package/dist/embedding-encoder/index.mjs +967 -860
  5. package/dist/enrichment-engine/index.js +834 -833
  6. package/dist/enrichment-engine/index.mjs +828 -832
  7. package/dist/pipeline-analytics/_stub.js +1680 -1397
  8. package/dist/pipeline-analytics/_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DOSUJ-U0.mjs +156 -0
  9. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DJvmVCso.mjs +26 -0
  10. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-B3Wx5J80.mjs +26 -0
  11. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.js-C0AuF9av.mjs +26 -0
  12. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-Bm-iyjmq.mjs +26 -0
  13. package/dist/pipeline-analytics/dist-CYZr2fwk.mjs +2726 -0
  14. package/dist/pipeline-analytics/hostInit-BazRS2O7.mjs +129 -0
  15. package/dist/pipeline-analytics/index.js +7133 -2558
  16. package/dist/pipeline-analytics/index.mjs +7124 -2557
  17. package/dist/pipeline-analytics/remoteEntry.js +134 -2973
  18. package/dist/pipeline-analytics/remoteEntry.ssr.js +33 -0
  19. package/dist/pipeline-analytics/virtualExposes-BgYzpJZG.mjs +27 -0
  20. package/dist/pipeline-analytics/virtual_mf-exposes-ssr___mfe_internal__addon_pipeline_analytics_widgets__remoteEntry_js-D7qgWCKX.mjs +10 -0
  21. package/dist/resolve-frame-5lMxmeI1.js +57 -0
  22. package/dist/resolve-frame-CT1T1tWy.mjs +44 -0
  23. package/package.json +26 -32
  24. package/dist/embedding-encoder/index.js.map +0 -1
  25. package/dist/embedding-encoder/index.mjs.map +0 -1
  26. package/dist/enrichment-engine/index.js.map +0 -1
  27. package/dist/enrichment-engine/index.mjs.map +0 -1
  28. package/dist/ffmpeg-config-DRONlBsj.mjs +0 -56
  29. package/dist/ffmpeg-config-DRONlBsj.mjs.map +0 -1
  30. package/dist/ffmpeg-config-uANz3sV5.js +0 -73
  31. package/dist/ffmpeg-config-uANz3sV5.js.map +0 -1
  32. package/dist/index-BFbwYH1P.js +0 -14343
  33. package/dist/index-BFbwYH1P.js.map +0 -1
  34. package/dist/index-BrTlzsrE.mjs +0 -14344
  35. package/dist/index-BrTlzsrE.mjs.map +0 -1
  36. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/AudioHistoryChart.d.ts +0 -4
  37. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/AudioMetricsPanel.d.ts +0 -10
  38. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/DetectionHistoryChart.d.ts +0 -4
  39. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/LiveStatsTab.d.ts +0 -5
  40. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/MotionHistoryChart.d.ts +0 -4
  41. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/OccupancyHistoryChart.d.ts +0 -4
  42. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/OccupancyPanel.d.ts +0 -10
  43. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/chart-utils.d.ts +0 -97
  44. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/index.d.ts +0 -29
  45. package/dist/pipeline-analytics/@mf-types/widgets.d.ts +0 -2
  46. package/dist/pipeline-analytics/@mf-types.d.ts +0 -3
  47. package/dist/pipeline-analytics/@mf-types.zip +0 -0
  48. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-h5aXOPSA.mjs +0 -12
  49. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-NjF4kxzW.mjs +0 -19
  50. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-7HAAnpQu.mjs +0 -18
  51. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.mjs-DoWbefqS.mjs +0 -104
  52. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_trpc_mf_1_client__loadShare__.mjs-52bfkwC8.mjs +0 -85
  53. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.mjs-CVrnrGED.mjs +0 -62
  54. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs-BZTB2scQ.mjs +0 -88
  55. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-CJO5YKGV.mjs +0 -29
  56. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.mjs-BsyrX6NO.mjs +0 -36
  57. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom__loadShare__.mjs-Dp8hqYOB.mjs +0 -45
  58. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-B0h0AGOH.mjs +0 -6
  59. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom_mf_1_client__loadShare__.mjs-BZjEt71l.mjs +0 -34
  60. package/dist/pipeline-analytics/_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-kZBmgzMg.mjs +0 -156
  61. package/dist/pipeline-analytics/client-BlxIUpgf.mjs +0 -9836
  62. package/dist/pipeline-analytics/getErrorShape-BPSzUA7W-TlK8ipWe.mjs +0 -211
  63. package/dist/pipeline-analytics/hostInit-qBB1Thhi.mjs +0 -168
  64. package/dist/pipeline-analytics/index-BoL0rgZt.mjs +0 -435
  65. package/dist/pipeline-analytics/index-CR1aiZDH.mjs +0 -185
  66. package/dist/pipeline-analytics/index-CWkKuNLr.mjs +0 -232
  67. package/dist/pipeline-analytics/index-DlhiA9R0.mjs +0 -2603
  68. package/dist/pipeline-analytics/index-DtdgkNgf.mjs +0 -725
  69. package/dist/pipeline-analytics/index-Dw6Q30NI.mjs +0 -1655
  70. package/dist/pipeline-analytics/index-Dy2V7VOm.mjs +0 -14379
  71. package/dist/pipeline-analytics/index-i47purqY.mjs +0 -37880
  72. package/dist/pipeline-analytics/index-xncRG7-x.mjs +0 -2713
  73. package/dist/pipeline-analytics/index.js.map +0 -1
  74. package/dist/pipeline-analytics/index.mjs.map +0 -1
  75. package/dist/pipeline-analytics/jsx-runtime-Dlbl3gpr.mjs +0 -55
  76. package/dist/pipeline-analytics/schemas-ClCuS4qa.mjs +0 -3594
  77. package/dist/pipeline-analytics/virtualExposes-8FzWTdq3.mjs +0 -42
  78. package/dist/playlist-generator-EhPaB7Hn.js +0 -48
  79. package/dist/playlist-generator-EhPaB7Hn.js.map +0 -1
  80. package/dist/playlist-generator-VTkgn53O.mjs +0 -48
  81. package/dist/playlist-generator-VTkgn53O.mjs.map +0 -1
  82. package/dist/recording/index.js +0 -257
  83. package/dist/recording/index.js.map +0 -1
  84. package/dist/recording/index.mjs +0 -235
  85. package/dist/recording/index.mjs.map +0 -1
  86. package/dist/recording-coordinator-BoGr5moz.js +0 -1052
  87. package/dist/recording-coordinator-BoGr5moz.js.map +0 -1
  88. package/dist/recording-coordinator-CsYH9LqF.mjs +0 -1012
  89. package/dist/recording-coordinator-CsYH9LqF.mjs.map +0 -1
  90. package/dist/recording-db-gOgaoQh0.js +0 -348
  91. package/dist/recording-db-gOgaoQh0.js.map +0 -1
  92. package/dist/recording-db-lIkSMTLq.mjs +0 -348
  93. package/dist/recording-db-lIkSMTLq.mjs.map +0 -1
  94. package/dist/recording-service-facade-B9lG6OFn.mjs +0 -123
  95. package/dist/recording-service-facade-B9lG6OFn.mjs.map +0 -1
  96. package/dist/recording-service-facade-Do1PKlAL.js +0 -123
  97. package/dist/recording-service-facade-Do1PKlAL.js.map +0 -1
  98. package/dist/storage-estimator-CRpoQc9j.js +0 -72
  99. package/dist/storage-estimator-CRpoQc9j.js.map +0 -1
  100. package/dist/storage-estimator-DzD8gWJH.mjs +0 -72
  101. package/dist/storage-estimator-DzD8gWJH.mjs.map +0 -1
@@ -1,1052 +0,0 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __copyProps = (to, from, except, desc) => {
9
- if (from && typeof from === "object" || typeof from === "function") {
10
- for (let key of __getOwnPropNames(from))
11
- if (!__hasOwnProp.call(to, key) && key !== except)
12
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
- }
14
- return to;
15
- };
16
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
- // If the importer is in node compatibility mode or this is not an ESM
18
- // file that has been converted to a CommonJS file using a Babel-
19
- // compatible transform (i.e. "__esModule" has not been set), then set
20
- // "default" to the CommonJS "module.exports" for node compatibility.
21
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
- mod
23
- ));
24
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
25
- const index = require("./index-BFbwYH1P.js");
26
- const ffmpegConfig = require("./ffmpeg-config-uANz3sV5.js");
27
- const node_crypto = require("node:crypto");
28
- const node_child_process = require("node:child_process");
29
- const fs = require("node:fs/promises");
30
- const path = require("node:path");
31
- const sharp = require("sharp");
32
- const playlistGenerator = require("./playlist-generator-EhPaB7Hn.js");
33
- const storageEstimator = require("./storage-estimator-CRpoQc9j.js");
34
- function _interopNamespaceDefault(e) {
35
- const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
36
- if (e) {
37
- for (const k in e) {
38
- if (k !== "default") {
39
- const d = Object.getOwnPropertyDescriptor(e, k);
40
- Object.defineProperty(n, k, d.get ? d : {
41
- enumerable: true,
42
- get: () => e[k]
43
- });
44
- }
45
- }
46
- }
47
- n.default = e;
48
- return Object.freeze(n);
49
- }
50
- const fs__namespace = /* @__PURE__ */ _interopNamespaceDefault(fs);
51
- const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path);
52
- class SegmentRingBuffer {
53
- constructor(maxDurationSec) {
54
- this.maxDurationSec = maxDurationSec;
55
- }
56
- segments = [];
57
- totalDurationSec = 0;
58
- push(segment) {
59
- this.segments.push(segment);
60
- this.totalDurationSec += segment.duration;
61
- while (this.totalDurationSec > this.maxDurationSec && this.segments.length > 1) {
62
- const evicted = this.segments.shift();
63
- this.totalDurationSec -= evicted.duration;
64
- }
65
- }
66
- flush() {
67
- const result = [...this.segments];
68
- this.segments = [];
69
- this.totalDurationSec = 0;
70
- return result;
71
- }
72
- get memoryEstimateBytes() {
73
- return this.segments.reduce((sum, s) => sum + s.data.length, 0);
74
- }
75
- }
76
- class SegmentWriter {
77
- constructor(config, logger, eventBus, db, _networkTracker) {
78
- this.config = config;
79
- this.logger = logger;
80
- this.eventBus = eventBus;
81
- this.db = db;
82
- this._mode = config.mode;
83
- this.ringBuffer = new SegmentRingBuffer(config.preBufferSec);
84
- }
85
- _state = "idle";
86
- _mode;
87
- ffmpeg = null;
88
- activeSegment = null;
89
- restartCount = 0;
90
- restartWindowStart = 0;
91
- healthTimer = null;
92
- lastDataTime = 0;
93
- ringBuffer;
94
- restartTimeout = null;
95
- pendingFinalization = null;
96
- paused = false;
97
- detectedCodec = "h264";
98
- detectedHasAudio = false;
99
- static MAX_RESTARTS = 10;
100
- static RESTART_WINDOW_MS = 5 * 60 * 1e3;
101
- static HEALTH_CHECK_INTERVAL_MS = 5e3;
102
- static DATA_TIMEOUT_MS = 15e3;
103
- static MIN_SEGMENT_DURATION_SEC = 0.5;
104
- static CRITICAL_DISK_GB = 1;
105
- get state() {
106
- return this._state;
107
- }
108
- get mode() {
109
- return this._mode;
110
- }
111
- get isPaused() {
112
- return this.paused;
113
- }
114
- // --- Public API ---
115
- async start(rtspUrl) {
116
- if (this._state !== "idle") return;
117
- const segmentDir = path__namespace.join(
118
- this.config.storagePath,
119
- this.config.subDirectory,
120
- String(this.config.deviceId)
121
- );
122
- await fs__namespace.mkdir(segmentDir, { recursive: true });
123
- this._state = "recording";
124
- this.lastDataTime = Date.now();
125
- this.restartCount = 0;
126
- this.restartWindowStart = Date.now();
127
- const segmentPattern = path__namespace.join(segmentDir, "%d.mp4");
128
- const args = SegmentWriter.buildSegmentationArgs(
129
- this.config.ffmpeg,
130
- rtspUrl,
131
- segmentPattern,
132
- this.config.segmentDurationSec
133
- );
134
- this.spawnFfmpeg(args, rtspUrl);
135
- this.startHealthCheck(rtspUrl);
136
- }
137
- async stop() {
138
- if (this._state === "idle") return;
139
- this._state = "stopping";
140
- this.stopHealthCheck();
141
- this.clearRestartTimeout();
142
- this.killFfmpeg();
143
- this.finalizeActiveSegment();
144
- if (this.pendingFinalization) {
145
- await this.pendingFinalization;
146
- }
147
- this._state = "idle";
148
- }
149
- resume(rtspUrl) {
150
- if (!this.paused) return;
151
- this.paused = false;
152
- this.logger.info("Resuming recording after disk space freed", {
153
- tags: { deviceId: this.config.deviceId }
154
- });
155
- this._state = "idle";
156
- void this.start(rtspUrl);
157
- }
158
- async flushAndContinue() {
159
- if (this._mode !== "buffer") return;
160
- const buffered = this.ringBuffer.flush();
161
- this.logger.info("Flushing buffered segments to disk", {
162
- tags: { deviceId: this.config.deviceId },
163
- meta: { count: buffered.length }
164
- });
165
- for (const seg of buffered) {
166
- await this.writeBufferedSegmentToDisk(seg);
167
- }
168
- this._mode = "continuous";
169
- }
170
- switchToBuffer() {
171
- this._mode = "buffer";
172
- this.ringBuffer = new SegmentRingBuffer(this.config.preBufferSec);
173
- }
174
- // --- Static helpers ---
175
- static generateSegmentId(deviceId, streamId, startTime) {
176
- const suffix = node_crypto.randomBytes(2).toString("hex");
177
- return `${deviceId}_${streamId}_${startTime}_${suffix}`;
178
- }
179
- static buildSegmentationArgs(config, inputUrl, outputPattern, segmentDuration) {
180
- const inputArgs = ffmpegConfig.buildFfmpegInputArgs(config, inputUrl);
181
- const outputArgs = ffmpegConfig.buildFfmpegOutputArgs(config);
182
- const segmentArgs = [
183
- "-f",
184
- "segment",
185
- "-segment_time",
186
- String(segmentDuration),
187
- "-segment_format",
188
- "mp4",
189
- "-movflags",
190
- "+frag_keyframe+empty_moov+default_base_moof",
191
- "-reset_timestamps",
192
- "1",
193
- "-strftime",
194
- "0"
195
- ];
196
- return [...inputArgs, ...outputArgs, ...segmentArgs, outputPattern];
197
- }
198
- static async checkDiskSpace(storagePath, statfsFn) {
199
- const doStatfs = statfsFn ?? (async (p) => {
200
- const { statfs: nodeStatfs } = await import("node:fs/promises");
201
- return nodeStatfs(p);
202
- });
203
- try {
204
- const stats = await doStatfs(storagePath);
205
- const availableBytes = stats.bfree * stats.bsize;
206
- const availableGb = availableBytes / (1024 * 1024 * 1024);
207
- return { ok: availableGb >= SegmentWriter.CRITICAL_DISK_GB, availableGb };
208
- } catch {
209
- return { ok: true, availableGb: -1 };
210
- }
211
- }
212
- // --- Private: ffmpeg process management ---
213
- spawnFfmpeg(args, rtspUrl) {
214
- this.ffmpeg = node_child_process.spawn(this.config.ffmpeg.path, args, {
215
- stdio: ["ignore", "pipe", "pipe"]
216
- });
217
- this.ffmpeg.stdout?.on("data", () => {
218
- this.lastDataTime = Date.now();
219
- });
220
- this.ffmpeg.stderr?.on("data", (data) => {
221
- this.lastDataTime = Date.now();
222
- const msg = data.toString().trim();
223
- if (msg) {
224
- this.logger.debug("ffmpeg stderr", { meta: { msg } });
225
- this.parseSegmentOutput(msg);
226
- }
227
- });
228
- this.ffmpeg.on("error", (err) => {
229
- this.logger.warn("ffmpeg process error", { meta: { error: err.message } });
230
- this.handleCrash(rtspUrl);
231
- });
232
- this.ffmpeg.on("exit", (code) => {
233
- if (code !== 0 && code !== null && this._state === "recording") {
234
- this.logger.warn("ffmpeg exited with non-zero code", { meta: { code } });
235
- this.handleCrash(rtspUrl);
236
- }
237
- });
238
- }
239
- handleCrash(rtspUrl) {
240
- this.ffmpeg = null;
241
- const prevFinalization = this.pendingFinalization;
242
- this.pendingFinalization = (prevFinalization ?? Promise.resolve()).then(() => {
243
- return this.finalizeActiveSegment();
244
- });
245
- if (this._state !== "recording") return;
246
- if (this.paused) return;
247
- const now = Date.now();
248
- if (now - this.restartWindowStart > SegmentWriter.RESTART_WINDOW_MS) {
249
- this.restartCount = 0;
250
- this.restartWindowStart = now;
251
- }
252
- this.restartCount++;
253
- this.eventBus.emit({
254
- id: `rec-err-${now}`,
255
- timestamp: /* @__PURE__ */ new Date(),
256
- source: { type: "addon", id: "recording-engine" },
257
- category: index.EventCategory.RecordingError,
258
- data: {
259
- deviceId: this.config.deviceId,
260
- streamId: this.config.streamId,
261
- restartAttempt: this.restartCount
262
- }
263
- });
264
- if (this.restartCount > SegmentWriter.MAX_RESTARTS) {
265
- this.logger.error("Max restarts exceeded", {
266
- tags: { deviceId: this.config.deviceId, streamId: this.config.streamId }
267
- });
268
- this._state = "idle";
269
- this.eventBus.emit({
270
- id: `rec-degraded-${now}`,
271
- timestamp: /* @__PURE__ */ new Date(),
272
- source: { type: "addon", id: "recording-engine" },
273
- category: index.EventCategory.RecordingHealthDegraded,
274
- data: {
275
- deviceId: this.config.deviceId,
276
- streamId: this.config.streamId
277
- }
278
- });
279
- return;
280
- }
281
- const backoffMs = Math.min(3e4, 1e3 * Math.pow(2, this.restartCount - 1));
282
- this.logger.info("Restarting ffmpeg", { meta: { backoffMs, attempt: this.restartCount } });
283
- this.restartTimeout = setTimeout(() => {
284
- this.restartTimeout = null;
285
- if (this._state === "recording") {
286
- const segmentDir = path__namespace.join(
287
- this.config.storagePath,
288
- this.config.subDirectory,
289
- String(this.config.deviceId)
290
- );
291
- const segmentPattern = path__namespace.join(segmentDir, "%d.mp4");
292
- const args = SegmentWriter.buildSegmentationArgs(
293
- this.config.ffmpeg,
294
- rtspUrl,
295
- segmentPattern,
296
- this.config.segmentDurationSec
297
- );
298
- this.spawnFfmpeg(args, rtspUrl);
299
- }
300
- }, backoffMs);
301
- }
302
- // --- Private: health monitoring ---
303
- startHealthCheck(rtspUrl) {
304
- this.healthTimer = setInterval(() => {
305
- if (this._state !== "recording") return;
306
- const elapsed = Date.now() - this.lastDataTime;
307
- if (elapsed > SegmentWriter.DATA_TIMEOUT_MS) {
308
- this.logger.warn("No data received, restarting ffmpeg", { meta: { elapsedMs: elapsed } });
309
- this.killFfmpeg();
310
- this.handleCrash(rtspUrl);
311
- }
312
- }, SegmentWriter.HEALTH_CHECK_INTERVAL_MS);
313
- }
314
- stopHealthCheck() {
315
- if (this.healthTimer) {
316
- clearInterval(this.healthTimer);
317
- this.healthTimer = null;
318
- }
319
- }
320
- clearRestartTimeout() {
321
- if (this.restartTimeout) {
322
- clearTimeout(this.restartTimeout);
323
- this.restartTimeout = null;
324
- }
325
- }
326
- killFfmpeg() {
327
- if (this.ffmpeg) {
328
- this.ffmpeg.kill("SIGTERM");
329
- this.ffmpeg = null;
330
- }
331
- }
332
- // --- Private: segment parsing and finalization ---
333
- parseSegmentOutput(msg) {
334
- const videoMatch = msg.match(/Stream\s+#\d+:\d+.*Video:\s+(h264|hevc|h265)/i);
335
- if (videoMatch) {
336
- const codec = videoMatch[1].toLowerCase();
337
- this.detectedCodec = codec === "hevc" || codec === "h265" ? "h265" : "h264";
338
- }
339
- const audioMatch = msg.match(/Stream\s+#\d+:\d+.*Audio:/i);
340
- if (audioMatch) {
341
- this.detectedHasAudio = true;
342
- }
343
- const openMatch = msg.match(/Opening '(.+\.mp4)' for writing/);
344
- if (openMatch) {
345
- const prevFinalization = this.pendingFinalization;
346
- this.pendingFinalization = (prevFinalization ?? Promise.resolve()).then(() => {
347
- return this.finalizeActiveSegment();
348
- });
349
- const absolutePath = openMatch[1];
350
- const segPath = absolutePath.startsWith(this.config.storagePath) ? absolutePath.slice(this.config.storagePath.length).replace(/^\//, "") : absolutePath;
351
- this.activeSegment = {
352
- id: SegmentWriter.generateSegmentId(
353
- this.config.deviceId,
354
- this.config.streamId,
355
- Date.now()
356
- ),
357
- path: segPath,
358
- startTime: Date.now()
359
- };
360
- }
361
- }
362
- async finalizeActiveSegment() {
363
- if (!this.activeSegment) return;
364
- const seg = this.activeSegment;
365
- this.activeSegment = null;
366
- const endTime = Date.now();
367
- const duration = (endTime - seg.startTime) / 1e3;
368
- if (duration < SegmentWriter.MIN_SEGMENT_DURATION_SEC) return;
369
- if (this._mode === "buffer") {
370
- await this.bufferSegmentFromDisk(seg, endTime, duration);
371
- return;
372
- }
373
- await this.finalizeSegmentToDisk(seg, endTime, duration);
374
- }
375
- async bufferSegmentFromDisk(seg, _endTime, duration) {
376
- try {
377
- const data = await fs__namespace.readFile(seg.path);
378
- this.ringBuffer.push({ data, startTime: seg.startTime, duration });
379
- await fs__namespace.unlink(seg.path).catch(() => {
380
- });
381
- } catch (err) {
382
- this.logger.warn("Failed to buffer segment", { meta: { error: String(err) } });
383
- }
384
- }
385
- async finalizeSegmentToDisk(seg, endTime, duration) {
386
- try {
387
- const diskCheck = await SegmentWriter.checkDiskSpace(this.config.storagePath);
388
- if (!diskCheck.ok) {
389
- this.eventBus.emit({
390
- id: `storage-critical-${Date.now()}`,
391
- timestamp: /* @__PURE__ */ new Date(),
392
- source: { type: "addon", id: "recording-engine" },
393
- category: index.EventCategory.RecordingStorageCritical,
394
- data: {
395
- storageId: this.config.storageName,
396
- availableGB: diskCheck.availableGb
397
- }
398
- });
399
- this.logger.error("Disk space critically low, pausing recording");
400
- this.paused = true;
401
- this.killFfmpeg();
402
- this._state = "idle";
403
- return;
404
- }
405
- let sizeBytes = 0;
406
- try {
407
- const fileStat = await fs__namespace.stat(seg.path);
408
- sizeBytes = fileStat.size;
409
- } catch {
410
- }
411
- const codec = this.detectedCodec;
412
- const hasAudio = this.detectedHasAudio;
413
- const segment = {
414
- id: seg.id,
415
- deviceId: this.config.deviceId,
416
- streamId: this.config.streamId,
417
- startTime: seg.startTime,
418
- endTime,
419
- duration,
420
- path: seg.path,
421
- storageName: this.config.storageName,
422
- subDirectory: this.config.subDirectory,
423
- sizeBytes,
424
- codec,
425
- hasAudio
426
- };
427
- try {
428
- this.db.insertSegment(segment);
429
- this.eventBus.emit({
430
- id: `seg-${seg.id}`,
431
- timestamp: /* @__PURE__ */ new Date(),
432
- source: { type: "addon", id: "recording-engine" },
433
- category: index.EventCategory.RecordingSegmentWritten,
434
- data: {
435
- deviceId: this.config.deviceId,
436
- streamId: this.config.streamId,
437
- segmentId: seg.id,
438
- duration,
439
- sizeBytes
440
- }
441
- });
442
- } catch (err) {
443
- this.logger.error("Failed to insert segment", { meta: { error: String(err) } });
444
- }
445
- } catch (err) {
446
- this.logger.error("Disk space check failed", { meta: { error: String(err) } });
447
- }
448
- }
449
- async writeBufferedSegmentToDisk(buffered) {
450
- const segId = SegmentWriter.generateSegmentId(
451
- this.config.deviceId,
452
- this.config.streamId,
453
- buffered.startTime
454
- );
455
- const relativePath = `${this.config.subDirectory}/${this.config.deviceId}/${segId}.mp4`;
456
- try {
457
- await this.config.fileStorage?.writeFile(relativePath, buffered.data);
458
- const sizeBytes = buffered.data.length;
459
- const segment = {
460
- id: segId,
461
- deviceId: this.config.deviceId,
462
- streamId: this.config.streamId,
463
- startTime: buffered.startTime,
464
- endTime: buffered.startTime + buffered.duration * 1e3,
465
- duration: buffered.duration,
466
- path: relativePath,
467
- storageName: this.config.storageName,
468
- subDirectory: this.config.subDirectory,
469
- sizeBytes,
470
- codec: this.detectedCodec,
471
- hasAudio: this.detectedHasAudio
472
- };
473
- this.db.insertSegment(segment);
474
- this.eventBus.emit({
475
- id: `seg-${segId}`,
476
- timestamp: /* @__PURE__ */ new Date(),
477
- source: { type: "addon", id: "recording-engine" },
478
- category: index.EventCategory.RecordingSegmentWritten,
479
- data: {
480
- deviceId: this.config.deviceId,
481
- streamId: this.config.streamId,
482
- segmentId: segId,
483
- duration: buffered.duration,
484
- sizeBytes
485
- }
486
- });
487
- } catch (err) {
488
- this.logger.error("Failed to write buffered segment to disk", { meta: { error: String(err) } });
489
- }
490
- }
491
- }
492
- class ThumbnailExtractor {
493
- constructor(config, logger, db) {
494
- this.config = config;
495
- this.logger = logger;
496
- this.db = db;
497
- }
498
- id = "thumbnail-extractor";
499
- name = "Thumbnail Extractor";
500
- needsAudio = false;
501
- videoRequirements = {
502
- keyframeOnly: true,
503
- maxFps: 1,
504
- format: "jpeg"
505
- };
506
- unsubscribe = null;
507
- active = false;
508
- attachToPipeline(pipeline, _deviceId) {
509
- this.active = true;
510
- this.unsubscribe = pipeline.onVideoFrame(
511
- (frame) => {
512
- this.handleFrame(frame).catch((err) => this.logger.debug("Thumbnail error", { meta: { error: String(err) } }));
513
- },
514
- this.videoRequirements
515
- );
516
- this.logger.info("ThumbnailExtractor attached", { tags: { deviceId: this.config.deviceId } });
517
- }
518
- detachFromPipeline(_deviceId) {
519
- this.active = false;
520
- if (this.unsubscribe) {
521
- this.unsubscribe();
522
- this.unsubscribe = null;
523
- }
524
- this.logger.info("ThumbnailExtractor detached", { tags: { deviceId: this.config.deviceId } });
525
- }
526
- setActive(active) {
527
- this.active = active;
528
- }
529
- async handleFrame(frame) {
530
- if (!this.active) return;
531
- const timestamp = frame.timestamp || Date.now();
532
- const relativePath = ThumbnailExtractor.thumbnailPath(
533
- this.config.subDirectory,
534
- this.config.deviceId,
535
- timestamp
536
- );
537
- const resized = await sharp(frame.data).resize({ width: this.config.maxWidthPx, withoutEnlargement: true }).jpeg({ quality: this.config.jpegQuality }).toBuffer();
538
- await this.config.fileStorage?.writeFile(relativePath, resized);
539
- this.db.insertThumbnail({
540
- deviceId: this.config.deviceId,
541
- timestamp,
542
- path: relativePath,
543
- storageName: this.config.storageName,
544
- subDirectory: this.config.subDirectory,
545
- sizeBytes: resized.length,
546
- category: "scrub"
547
- });
548
- }
549
- static thumbnailPath(subDirectory, deviceId, timestamp) {
550
- return `${subDirectory}/${deviceId}/${timestamp}.jpg`;
551
- }
552
- }
553
- const NORMAL_INTERVAL_MS = 5 * 60 * 1e3;
554
- const HIGH_USAGE_INTERVAL_MS = 30 * 1e3;
555
- const STORAGE_WARNING_THRESHOLD = 0.8;
556
- const STORAGE_CRITICAL_THRESHOLD = 0.95;
557
- const STORAGE_HIGH_USAGE_THRESHOLD = 0.9;
558
- class RetentionManager {
559
- constructor(db, logger, eventBus, storageProvider) {
560
- this.db = db;
561
- this.logger = logger;
562
- this.eventBus = eventBus;
563
- this.storageProvider = storageProvider;
564
- }
565
- timer = null;
566
- start() {
567
- this.scheduleNextCycle(NORMAL_INTERVAL_MS);
568
- }
569
- stop() {
570
- if (this.timer) {
571
- clearTimeout(this.timer);
572
- this.timer = null;
573
- }
574
- }
575
- async runCycle() {
576
- this.db.resetStaleCleanups();
577
- const policies = this.db.getEnabledPolicies();
578
- let totalFreedBytes = 0;
579
- let totalDeletedSegments = 0;
580
- let highUsage = false;
581
- for (const policy of policies) {
582
- for (const sp of policy.streams) {
583
- const category = `recording:${sp.streamId}`;
584
- const config = this.db.resolveStorageConfig(policy.deviceId, category);
585
- if (!config) continue;
586
- if (config.retentionDays !== null) {
587
- const cutoff = Date.now() - config.retentionDays * 864e5;
588
- const deleted = this.db.deleteSegmentsBefore(policy.deviceId, sp.streamId, cutoff);
589
- totalDeletedSegments += deleted.length;
590
- for (const seg of deleted) {
591
- totalFreedBytes += seg.sizeBytes;
592
- await this.deleteFile(seg.path);
593
- }
594
- this.db.deleteThumbnailsBefore(policy.deviceId, cutoff);
595
- }
596
- if (config.retentionGb !== null) {
597
- const maxBytes = config.retentionGb * 1024 * 1024 * 1024;
598
- let usage = this.db.getStorageUsage(policy.deviceId, sp.streamId);
599
- const usageRatio = usage.totalBytes / maxBytes;
600
- if (usageRatio > STORAGE_CRITICAL_THRESHOLD) {
601
- this.emitStorageEvent("recording.storage.critical", policy.deviceId, sp.streamId, usageRatio);
602
- } else if (usageRatio > STORAGE_WARNING_THRESHOLD) {
603
- this.emitStorageEvent("recording.storage.warning", policy.deviceId, sp.streamId, usageRatio);
604
- }
605
- if (usageRatio > STORAGE_HIGH_USAGE_THRESHOLD) {
606
- highUsage = true;
607
- }
608
- while (usage.totalBytes > maxBytes && usage.segmentCount > 0) {
609
- const oldest = this.db.getOldestSegments(policy.deviceId, sp.streamId, 10);
610
- if (oldest.length === 0) break;
611
- for (const seg of oldest) {
612
- this.db.deleteSegmentsBefore(policy.deviceId, sp.streamId, seg.endTime + 1);
613
- totalFreedBytes += seg.sizeBytes;
614
- totalDeletedSegments++;
615
- await this.deleteFile(seg.path);
616
- }
617
- usage = this.db.getStorageUsage(policy.deviceId, sp.streamId);
618
- }
619
- }
620
- }
621
- }
622
- const pending = this.db.getPendingCleanups();
623
- for (const entry of pending) {
624
- this.db.markCleanupInProgress(entry.deviceId);
625
- try {
626
- const deleted = this.db.deleteSegmentsForDevice(entry.deviceId);
627
- for (const seg of deleted) {
628
- totalFreedBytes += seg.sizeBytes;
629
- totalDeletedSegments++;
630
- await this.deleteFile(seg.path);
631
- }
632
- this.db.deleteThumbnailsForDevice(entry.deviceId);
633
- this.db.markCleanupCompleted(entry.deviceId);
634
- } catch (err) {
635
- this.logger.error("Cleanup failed", { tags: { deviceId: entry.deviceId }, meta: { error: String(err) } });
636
- }
637
- }
638
- if (totalDeletedSegments > 0) {
639
- this.eventBus.emit({
640
- id: `retention-${Date.now()}`,
641
- timestamp: /* @__PURE__ */ new Date(),
642
- source: { type: "addon", id: "recording-engine" },
643
- category: index.EventCategory.RecordingRetentionCompleted,
644
- data: {
645
- freedMB: Math.round(totalFreedBytes / 1024 / 1024),
646
- deletedSegments: totalDeletedSegments
647
- }
648
- });
649
- }
650
- return highUsage;
651
- }
652
- scheduleNextCycle(intervalMs) {
653
- this.timer = setTimeout(async () => {
654
- try {
655
- const storageHighUsage = await this.runCycle();
656
- const nextInterval = storageHighUsage ? HIGH_USAGE_INTERVAL_MS : NORMAL_INTERVAL_MS;
657
- this.scheduleNextCycle(nextInterval);
658
- } catch (err) {
659
- this.logger.error("Retention cycle error", { meta: { error: String(err) } });
660
- this.scheduleNextCycle(NORMAL_INTERVAL_MS);
661
- }
662
- }, intervalMs);
663
- }
664
- emitStorageEvent(category, deviceId, streamId, usageRatio) {
665
- this.eventBus.emit({
666
- id: `${category}-${deviceId}-${Date.now()}`,
667
- timestamp: /* @__PURE__ */ new Date(),
668
- source: { type: "addon", id: "recording-engine" },
669
- category,
670
- data: {
671
- deviceId,
672
- streamId,
673
- usagePercent: Math.round(usageRatio * 100)
674
- }
675
- });
676
- }
677
- async deleteFile(filePath) {
678
- try {
679
- await this.storageProvider.delete({ location: "recordings", relativePath: filePath });
680
- } catch {
681
- }
682
- }
683
- }
684
- const DEFAULT_SEGMENT_DURATION_SEC = 4;
685
- const POLICY_EVAL_INTERVAL_MS = 1e3;
686
- const MOTION_FALLBACK_TIMEOUT_MS = 6e4;
687
- class RecordingCoordinator {
688
- db;
689
- logger;
690
- eventBus;
691
- streamingEngine;
692
- pipelineManager;
693
- networkTracker;
694
- storageProvider;
695
- globalFfmpegConfig;
696
- detectedFfmpegConfig;
697
- segmentDurationSec;
698
- recordings = /* @__PURE__ */ new Map();
699
- policyTimer = null;
700
- retentionManager;
701
- playlistGenerator;
702
- storageEstimator;
703
- constructor(config) {
704
- this.db = config.db;
705
- this.logger = config.logger;
706
- this.eventBus = config.eventBus;
707
- this.streamingEngine = config.streamingEngine;
708
- this.pipelineManager = config.pipelineManager;
709
- this.networkTracker = config.networkTracker;
710
- this.storageProvider = config.storageProvider;
711
- this.globalFfmpegConfig = config.globalFfmpegConfig;
712
- this.detectedFfmpegConfig = config.detectedFfmpegConfig;
713
- this.segmentDurationSec = config.segmentDurationSec ?? DEFAULT_SEGMENT_DURATION_SEC;
714
- this.retentionManager = new RetentionManager(
715
- this.db,
716
- this.logger.child("retention"),
717
- this.eventBus,
718
- this.storageProvider
719
- );
720
- this.playlistGenerator = new playlistGenerator.PlaylistGenerator(this.db);
721
- this.storageEstimator = new storageEstimator.StorageEstimator(this.db, this.networkTracker);
722
- }
723
- async start() {
724
- this.logger.info("RecordingCoordinator starting");
725
- this.retentionManager.start();
726
- const enabledPolicies = this.db.getEnabledPolicies();
727
- for (const policy of enabledPolicies) {
728
- try {
729
- await this.enableRecording(policy.deviceId, {
730
- policy: {
731
- mode: policy.mode,
732
- streams: policy.streams,
733
- enabled: policy.enabled,
734
- preBufferSec: policy.preBufferSec,
735
- postBufferSec: policy.postBufferSec,
736
- scheduleRules: policy.scheduleRules
737
- }
738
- });
739
- } catch (err) {
740
- this.logger.error("Failed to start recording", { tags: { deviceId: policy.deviceId }, meta: { error: String(err) } });
741
- }
742
- }
743
- this.policyTimer = setInterval(() => {
744
- this.evaluatePolicies();
745
- }, POLICY_EVAL_INTERVAL_MS);
746
- this.logger.info("RecordingCoordinator started");
747
- }
748
- stop() {
749
- this.logger.info("RecordingCoordinator stopping");
750
- if (this.policyTimer) {
751
- clearInterval(this.policyTimer);
752
- this.policyTimer = null;
753
- }
754
- this.retentionManager.stop();
755
- for (const [deviceId] of this.recordings) {
756
- this.stopRecordingInternal(deviceId);
757
- }
758
- this.recordings.clear();
759
- this.logger.info("RecordingCoordinator stopped");
760
- }
761
- async enableRecording(deviceId, config) {
762
- if (this.recordings.has(deviceId)) {
763
- this.stopRecordingInternal(deviceId);
764
- this.recordings.delete(deviceId);
765
- }
766
- const policy = {
767
- deviceId,
768
- mode: config.policy.mode,
769
- streams: config.policy.streams,
770
- enabled: config.policy.enabled,
771
- preBufferSec: config.policy.preBufferSec,
772
- postBufferSec: config.policy.postBufferSec,
773
- scheduleRules: config.policy.scheduleRules
774
- };
775
- this.db.upsertPolicy({
776
- deviceId,
777
- enabled: policy.enabled,
778
- mode: policy.mode,
779
- streams: policy.streams,
780
- preBufferSec: policy.preBufferSec,
781
- postBufferSec: policy.postBufferSec,
782
- scheduleRules: policy.scheduleRules
783
- });
784
- this.db.cancelCleanup(deviceId);
785
- const ffmpegConfig$1 = ffmpegConfig.resolveFfmpegConfig(
786
- config.ffmpegOverrides,
787
- this.globalFfmpegConfig,
788
- this.detectedFfmpegConfig
789
- );
790
- const writerMode = policy.mode === "motion" ? "buffer" : "continuous";
791
- const writers = [];
792
- for (const sp of policy.streams) {
793
- const storageConfig = this.db.resolveStorageConfig(deviceId, `recording:${sp.streamId}`);
794
- const storageName = storageConfig?.storageName ?? "recordings";
795
- const subDirectory = storageConfig?.subDirectory ?? `recordings/${sp.streamId}`;
796
- const resolvedStoragePath = await this.storageProvider.resolve({ location: storageName, relativePath: "" });
797
- const writerConfig = {
798
- deviceId,
799
- streamId: sp.streamId,
800
- segmentDurationSec: this.segmentDurationSec,
801
- storagePath: resolvedStoragePath,
802
- storageName,
803
- subDirectory,
804
- ffmpeg: ffmpegConfig$1,
805
- mode: writerMode,
806
- preBufferSec: policy.preBufferSec
807
- };
808
- const writer = new SegmentWriter(
809
- writerConfig,
810
- this.logger.child(`writer:${deviceId}:${sp.streamId}`),
811
- this.eventBus,
812
- this.db,
813
- this.networkTracker
814
- );
815
- const rtspUrl = this.streamingEngine.getStreamUrl(`${policy.deviceId}_${sp.streamId}`, "rtsp");
816
- if (rtspUrl) {
817
- await writer.start(rtspUrl);
818
- }
819
- writers.push(writer);
820
- }
821
- const thumbStorageConfig = this.db.resolveStorageConfig(deviceId, "thumbnail:scrub");
822
- const thumbStorageName = thumbStorageConfig?.storageName ?? "recordings";
823
- const thumbConfig = {
824
- deviceId,
825
- storagePath: await this.storageProvider.resolve({ location: thumbStorageName, relativePath: "" }),
826
- storageName: thumbStorageName,
827
- subDirectory: thumbStorageConfig?.subDirectory ?? "thumbnails/scrub",
828
- maxWidthPx: 160,
829
- jpegQuality: 65
830
- };
831
- const thumbnailExtractor = new ThumbnailExtractor(
832
- thumbConfig,
833
- this.logger.child(`thumb:${deviceId}`),
834
- this.db
835
- );
836
- const pipeline = this.pipelineManager.getPipeline(deviceId);
837
- if (pipeline) {
838
- thumbnailExtractor.attachToPipeline(pipeline, deviceId);
839
- }
840
- if (policy.mode === "motion") {
841
- thumbnailExtractor.setActive(false);
842
- }
843
- const motionUnsubscribe = this.subscribeToMotionEvents(deviceId, policy);
844
- const state = {
845
- deviceId,
846
- policy,
847
- writers,
848
- thumbnailExtractor,
849
- motionUnsubscribe,
850
- motionActive: false,
851
- motionTimeout: null,
852
- motionFallbackTimeout: null,
853
- motionReceived: false
854
- };
855
- this.recordings.set(deviceId, state);
856
- if (policy.mode === "motion") {
857
- state.motionFallbackTimeout = setTimeout(() => {
858
- const currentState = this.recordings.get(deviceId);
859
- if (!currentState || currentState.motionReceived) return;
860
- this.logger.warn("No motion events received — falling back to continuous recording", {
861
- tags: { deviceId },
862
- meta: { timeoutSec: MOTION_FALLBACK_TIMEOUT_MS / 1e3 }
863
- });
864
- this.eventBus.emit({
865
- id: `recording-policy-fallback-${deviceId}-${Date.now()}`,
866
- timestamp: /* @__PURE__ */ new Date(),
867
- source: { type: "addon", id: "recording-engine" },
868
- category: index.EventCategory.RecordingPolicyFallback,
869
- data: {
870
- deviceId,
871
- originalMode: "motion",
872
- fallbackMode: "continuous",
873
- reason: "no_motion_events"
874
- }
875
- });
876
- for (const writer of currentState.writers) {
877
- writer.flushAndContinue().catch((err) => {
878
- this.logger.error("Failed to flush buffer during fallback", { tags: { deviceId }, meta: { error: String(err) } });
879
- });
880
- }
881
- currentState.thumbnailExtractor.setActive(true);
882
- }, MOTION_FALLBACK_TIMEOUT_MS);
883
- }
884
- this.eventBus.emit({
885
- id: `recording-started-${deviceId}-${Date.now()}`,
886
- timestamp: /* @__PURE__ */ new Date(),
887
- source: { type: "addon", id: "recording-engine" },
888
- category: index.EventCategory.RecordingStarted,
889
- data: {
890
- deviceId,
891
- mode: policy.mode,
892
- streams: policy.streams.map((s) => s.streamId)
893
- }
894
- });
895
- this.logger.info("Recording enabled", { tags: { deviceId }, meta: { mode: policy.mode } });
896
- }
897
- async disableRecording(deviceId) {
898
- const state = this.recordings.get(deviceId);
899
- if (!state) {
900
- this.logger.warn("No active recording", { tags: { deviceId } });
901
- return;
902
- }
903
- let totalSegmentCount = 0;
904
- let totalSizeBytes = 0;
905
- for (const sp of state.policy.streams) {
906
- const usage = this.db.getStorageUsage(deviceId, sp.streamId);
907
- totalSegmentCount += usage.segmentCount;
908
- totalSizeBytes += usage.totalBytes;
909
- }
910
- const totalMB = Math.round(totalSizeBytes / 1024 / 1024);
911
- this.stopRecordingInternal(deviceId);
912
- this.recordings.delete(deviceId);
913
- this.db.addToCleanupQueue(deviceId, Date.now());
914
- this.eventBus.emit({
915
- id: `recording-stopped-${deviceId}-${Date.now()}`,
916
- timestamp: /* @__PURE__ */ new Date(),
917
- source: { type: "addon", id: "recording-engine" },
918
- category: index.EventCategory.RecordingStopped,
919
- data: {
920
- deviceId,
921
- segmentCount: totalSegmentCount,
922
- totalMB
923
- }
924
- });
925
- this.logger.info("Recording disabled", { tags: { deviceId }, meta: { segmentCount: totalSegmentCount, totalMB } });
926
- }
927
- isRecording(deviceId) {
928
- return this.recordings.has(deviceId);
929
- }
930
- /** Number of devices currently being recorded. */
931
- getActiveCount() {
932
- return this.recordings.size;
933
- }
934
- evaluatePolicies() {
935
- const now = /* @__PURE__ */ new Date();
936
- for (const [_deviceId, state] of this.recordings) {
937
- const { policy } = state;
938
- if (policy.mode === "scheduled" || policy.mode === "composite") {
939
- if (!policy.scheduleRules || policy.scheduleRules.length === 0) continue;
940
- const matchingRule = policy.scheduleRules.find(
941
- (rule) => RecordingCoordinator.evaluateScheduleRule(rule, now)
942
- );
943
- if (matchingRule) {
944
- const targetMode = matchingRule.mode === "motion" ? "buffer" : "continuous";
945
- for (const writer of state.writers) {
946
- if (writer.mode !== targetMode) {
947
- if (targetMode === "buffer") {
948
- writer.switchToBuffer();
949
- }
950
- }
951
- }
952
- } else {
953
- for (const writer of state.writers) {
954
- if (writer.mode !== "buffer") {
955
- writer.switchToBuffer();
956
- }
957
- }
958
- }
959
- }
960
- }
961
- }
962
- static evaluateScheduleRule(rule, date) {
963
- const dayOfWeek = date.getDay();
964
- const timeMinutes = date.getHours() * 60 + date.getMinutes();
965
- const [startH, startM] = rule.startTime.split(":").map(Number);
966
- const [endH, endM] = rule.endTime.split(":").map(Number);
967
- const startMinutes = startH * 60 + startM;
968
- const endMinutes = endH * 60 + endM;
969
- if (endMinutes > startMinutes) {
970
- return rule.days.includes(dayOfWeek) && timeMinutes >= startMinutes && timeMinutes < endMinutes;
971
- }
972
- if (rule.days.includes(dayOfWeek) && timeMinutes >= startMinutes) {
973
- return true;
974
- }
975
- const previousDay = (dayOfWeek + 6) % 7;
976
- if (rule.days.includes(previousDay) && timeMinutes < endMinutes) {
977
- return true;
978
- }
979
- return false;
980
- }
981
- subscribeToMotionEvents(deviceId, policy) {
982
- if (policy.mode !== "motion" && policy.mode !== "composite") {
983
- return null;
984
- }
985
- return this.eventBus.subscribe(
986
- { category: `motion.${deviceId}` },
987
- (event) => {
988
- this.handleMotionEvent(deviceId, event);
989
- }
990
- );
991
- }
992
- handleMotionEvent(deviceId, event) {
993
- const state = this.recordings.get(deviceId);
994
- if (!state) return;
995
- if (!state.motionReceived) {
996
- state.motionReceived = true;
997
- if (state.motionFallbackTimeout) {
998
- clearTimeout(state.motionFallbackTimeout);
999
- state.motionFallbackTimeout = null;
1000
- }
1001
- }
1002
- const motionDetected = event.data.active === true || event.data.type === "start";
1003
- if (motionDetected) {
1004
- state.motionActive = true;
1005
- if (state.motionTimeout) {
1006
- clearTimeout(state.motionTimeout);
1007
- state.motionTimeout = null;
1008
- }
1009
- for (const writer of state.writers) {
1010
- writer.flushAndContinue().catch((err) => {
1011
- this.logger.error("Failed to flush buffer", { tags: { deviceId }, meta: { error: String(err) } });
1012
- });
1013
- }
1014
- state.thumbnailExtractor.setActive(true);
1015
- } else {
1016
- if (state.motionTimeout) {
1017
- clearTimeout(state.motionTimeout);
1018
- }
1019
- state.motionTimeout = setTimeout(() => {
1020
- state.motionActive = false;
1021
- state.motionTimeout = null;
1022
- for (const writer of state.writers) {
1023
- writer.switchToBuffer();
1024
- }
1025
- if (state.policy.mode === "motion") {
1026
- state.thumbnailExtractor.setActive(false);
1027
- }
1028
- }, state.policy.postBufferSec * 1e3);
1029
- }
1030
- }
1031
- stopRecordingInternal(deviceId) {
1032
- const state = this.recordings.get(deviceId);
1033
- if (!state) return;
1034
- for (const writer of state.writers) {
1035
- writer.stop();
1036
- }
1037
- state.thumbnailExtractor.detachFromPipeline(deviceId);
1038
- if (state.motionUnsubscribe) {
1039
- state.motionUnsubscribe();
1040
- }
1041
- if (state.motionTimeout) {
1042
- clearTimeout(state.motionTimeout);
1043
- state.motionTimeout = null;
1044
- }
1045
- if (state.motionFallbackTimeout) {
1046
- clearTimeout(state.motionFallbackTimeout);
1047
- state.motionFallbackTimeout = null;
1048
- }
1049
- }
1050
- }
1051
- exports.RecordingCoordinator = RecordingCoordinator;
1052
- //# sourceMappingURL=recording-coordinator-BoGr5moz.js.map