@camstack/addon-post-analysis 0.1.18 → 0.1.20

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 (68) hide show
  1. package/dist/embedding-encoder/index.js +1 -1
  2. package/dist/embedding-encoder/index.mjs +1 -1
  3. package/dist/enrichment-engine/index.js +1 -1
  4. package/dist/enrichment-engine/index.mjs +1 -1
  5. package/dist/{index-DafwGlkQ.js → index-B0RhVv1c.js} +3940 -807
  6. package/dist/index-B0RhVv1c.js.map +1 -0
  7. package/dist/{index-CIJfmsWX.mjs → index-ot5PeFg_.mjs} +3943 -810
  8. package/dist/index-ot5PeFg_.mjs.map +1 -0
  9. package/dist/pipeline-analytics/@mf-types.zip +0 -0
  10. package/dist/pipeline-analytics/{__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-h5aXOPSA.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-lantnv8e.mjs} +1 -1
  11. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-BD3oMNGB.mjs +29 -0
  12. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-BgOHCakr.mjs +18 -0
  13. package/dist/pipeline-analytics/{__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs-D-USVuHq.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs-D1qPKjvR.mjs} +3 -1
  14. package/dist/pipeline-analytics/{__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-qQCPW8pT.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-B5X50Xa4.mjs} +1 -1
  15. package/dist/pipeline-analytics/{__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-Bv9bYz9E.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-B10b5k5J.mjs} +1 -1
  16. package/dist/pipeline-analytics/_stub.js +2 -3
  17. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-B3kCe2qM.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DWB3apaJ.mjs} +6 -6
  18. package/dist/pipeline-analytics/{client-DHmQcIWy.mjs → client-C6xdgLZU.mjs} +2 -2
  19. package/dist/pipeline-analytics/{hostInit-CuWzic_f.mjs → hostInit-3cyL9eyG.mjs} +12 -12
  20. package/dist/pipeline-analytics/{index-BA65ZJOW.mjs → index-BCTHeI2m.mjs} +254 -268
  21. package/dist/pipeline-analytics/{index-Crs1D0Uu.mjs → index-BuWLz0GG.mjs} +1 -1
  22. package/dist/pipeline-analytics/{index-gpelkpEE.mjs → index-CIwq-tQL.mjs} +1 -1
  23. package/dist/pipeline-analytics/{index-CHnXxMRA.mjs → index-CWBMDbou.mjs} +1 -1
  24. package/dist/pipeline-analytics/index-CZhagnlH.mjs +67784 -0
  25. package/dist/pipeline-analytics/{index-DicaGC31.mjs → index-D883Q5B8.mjs} +1 -1
  26. package/dist/pipeline-analytics/index-DtOI1aTU.mjs +18504 -0
  27. package/dist/pipeline-analytics/index.js +605 -42
  28. package/dist/pipeline-analytics/index.js.map +1 -1
  29. package/dist/pipeline-analytics/index.mjs +604 -42
  30. package/dist/pipeline-analytics/index.mjs.map +1 -1
  31. package/dist/pipeline-analytics/{jsx-runtime-Wcfyyyt4.mjs → jsx-runtime-DdLhuHmJ.mjs} +1 -1
  32. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  33. package/dist/pipeline-analytics/{schemas-ChN4Ih0h.mjs → schemas-B7L0qZtq.mjs} +530 -515
  34. package/package.json +12 -27
  35. package/dist/ffmpeg-config-DRONlBsj.mjs +0 -56
  36. package/dist/ffmpeg-config-DRONlBsj.mjs.map +0 -1
  37. package/dist/ffmpeg-config-uANz3sV5.js +0 -73
  38. package/dist/ffmpeg-config-uANz3sV5.js.map +0 -1
  39. package/dist/index-CIJfmsWX.mjs.map +0 -1
  40. package/dist/index-DafwGlkQ.js.map +0 -1
  41. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-d8PmLbO2.mjs +0 -19
  42. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-BcWYbuKp.mjs +0 -18
  43. package/dist/pipeline-analytics/index-CUXiTSWS.mjs +0 -13883
  44. package/dist/pipeline-analytics/index-gbflFMEY.mjs +0 -36403
  45. package/dist/playlist-generator-EhPaB7Hn.js +0 -48
  46. package/dist/playlist-generator-EhPaB7Hn.js.map +0 -1
  47. package/dist/playlist-generator-VTkgn53O.mjs +0 -48
  48. package/dist/playlist-generator-VTkgn53O.mjs.map +0 -1
  49. package/dist/recording/index.js +0 -257
  50. package/dist/recording/index.js.map +0 -1
  51. package/dist/recording/index.mjs +0 -235
  52. package/dist/recording/index.mjs.map +0 -1
  53. package/dist/recording-coordinator-BKsM_JGg.js +0 -1052
  54. package/dist/recording-coordinator-BKsM_JGg.js.map +0 -1
  55. package/dist/recording-coordinator-Bw3N1gYu.mjs +0 -1012
  56. package/dist/recording-coordinator-Bw3N1gYu.mjs.map +0 -1
  57. package/dist/recording-db-gOgaoQh0.js +0 -348
  58. package/dist/recording-db-gOgaoQh0.js.map +0 -1
  59. package/dist/recording-db-lIkSMTLq.mjs +0 -348
  60. package/dist/recording-db-lIkSMTLq.mjs.map +0 -1
  61. package/dist/recording-service-facade-B9lG6OFn.mjs +0 -123
  62. package/dist/recording-service-facade-B9lG6OFn.mjs.map +0 -1
  63. package/dist/recording-service-facade-Do1PKlAL.js +0 -123
  64. package/dist/recording-service-facade-Do1PKlAL.js.map +0 -1
  65. package/dist/storage-estimator-CRpoQc9j.js +0 -72
  66. package/dist/storage-estimator-CRpoQc9j.js.map +0 -1
  67. package/dist/storage-estimator-DzD8gWJH.mjs +0 -72
  68. 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-DafwGlkQ.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-BKsM_JGg.js.map