@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.
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/enrichment-engine/index.js +1 -1
- package/dist/enrichment-engine/index.mjs +1 -1
- package/dist/{index-DafwGlkQ.js → index-B0RhVv1c.js} +3940 -807
- package/dist/index-B0RhVv1c.js.map +1 -0
- package/dist/{index-CIJfmsWX.mjs → index-ot5PeFg_.mjs} +3943 -810
- package/dist/index-ot5PeFg_.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_sdk__loadShare__.mjs-h5aXOPSA.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-lantnv8e.mjs} +1 -1
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-BD3oMNGB.mjs +29 -0
- 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
- 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
- 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
- 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
- package/dist/pipeline-analytics/_stub.js +2 -3
- 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
- package/dist/pipeline-analytics/{client-DHmQcIWy.mjs → client-C6xdgLZU.mjs} +2 -2
- package/dist/pipeline-analytics/{hostInit-CuWzic_f.mjs → hostInit-3cyL9eyG.mjs} +12 -12
- package/dist/pipeline-analytics/{index-BA65ZJOW.mjs → index-BCTHeI2m.mjs} +254 -268
- package/dist/pipeline-analytics/{index-Crs1D0Uu.mjs → index-BuWLz0GG.mjs} +1 -1
- package/dist/pipeline-analytics/{index-gpelkpEE.mjs → index-CIwq-tQL.mjs} +1 -1
- package/dist/pipeline-analytics/{index-CHnXxMRA.mjs → index-CWBMDbou.mjs} +1 -1
- package/dist/pipeline-analytics/index-CZhagnlH.mjs +67784 -0
- package/dist/pipeline-analytics/{index-DicaGC31.mjs → index-D883Q5B8.mjs} +1 -1
- package/dist/pipeline-analytics/index-DtOI1aTU.mjs +18504 -0
- package/dist/pipeline-analytics/index.js +605 -42
- package/dist/pipeline-analytics/index.js.map +1 -1
- package/dist/pipeline-analytics/index.mjs +604 -42
- package/dist/pipeline-analytics/index.mjs.map +1 -1
- package/dist/pipeline-analytics/{jsx-runtime-Wcfyyyt4.mjs → jsx-runtime-DdLhuHmJ.mjs} +1 -1
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/dist/pipeline-analytics/{schemas-ChN4Ih0h.mjs → schemas-B7L0qZtq.mjs} +530 -515
- package/package.json +12 -27
- package/dist/ffmpeg-config-DRONlBsj.mjs +0 -56
- package/dist/ffmpeg-config-DRONlBsj.mjs.map +0 -1
- package/dist/ffmpeg-config-uANz3sV5.js +0 -73
- package/dist/ffmpeg-config-uANz3sV5.js.map +0 -1
- package/dist/index-CIJfmsWX.mjs.map +0 -1
- package/dist/index-DafwGlkQ.js.map +0 -1
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-d8PmLbO2.mjs +0 -19
- 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
- package/dist/pipeline-analytics/index-CUXiTSWS.mjs +0 -13883
- package/dist/pipeline-analytics/index-gbflFMEY.mjs +0 -36403
- package/dist/playlist-generator-EhPaB7Hn.js +0 -48
- package/dist/playlist-generator-EhPaB7Hn.js.map +0 -1
- package/dist/playlist-generator-VTkgn53O.mjs +0 -48
- package/dist/playlist-generator-VTkgn53O.mjs.map +0 -1
- package/dist/recording/index.js +0 -257
- package/dist/recording/index.js.map +0 -1
- package/dist/recording/index.mjs +0 -235
- package/dist/recording/index.mjs.map +0 -1
- package/dist/recording-coordinator-BKsM_JGg.js +0 -1052
- package/dist/recording-coordinator-BKsM_JGg.js.map +0 -1
- package/dist/recording-coordinator-Bw3N1gYu.mjs +0 -1012
- package/dist/recording-coordinator-Bw3N1gYu.mjs.map +0 -1
- package/dist/recording-db-gOgaoQh0.js +0 -348
- package/dist/recording-db-gOgaoQh0.js.map +0 -1
- package/dist/recording-db-lIkSMTLq.mjs +0 -348
- package/dist/recording-db-lIkSMTLq.mjs.map +0 -1
- package/dist/recording-service-facade-B9lG6OFn.mjs +0 -123
- package/dist/recording-service-facade-B9lG6OFn.mjs.map +0 -1
- package/dist/recording-service-facade-Do1PKlAL.js +0 -123
- package/dist/recording-service-facade-Do1PKlAL.js.map +0 -1
- package/dist/storage-estimator-CRpoQc9j.js +0 -72
- package/dist/storage-estimator-CRpoQc9j.js.map +0 -1
- package/dist/storage-estimator-DzD8gWJH.mjs +0 -72
- package/dist/storage-estimator-DzD8gWJH.mjs.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"recording-coordinator-Bw3N1gYu.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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"}
|
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
class RecordingDb {
|
|
4
|
-
constructor(db) {
|
|
5
|
-
this.db = db;
|
|
6
|
-
}
|
|
7
|
-
initialize() {
|
|
8
|
-
this.db.exec(`
|
|
9
|
-
CREATE TABLE IF NOT EXISTS recording_segments (
|
|
10
|
-
id TEXT PRIMARY KEY,
|
|
11
|
-
device_id TEXT NOT NULL,
|
|
12
|
-
stream_id TEXT NOT NULL,
|
|
13
|
-
start_time INTEGER NOT NULL,
|
|
14
|
-
end_time INTEGER NOT NULL,
|
|
15
|
-
duration REAL NOT NULL,
|
|
16
|
-
path TEXT NOT NULL,
|
|
17
|
-
storage_name TEXT NOT NULL,
|
|
18
|
-
sub_directory TEXT NOT NULL,
|
|
19
|
-
size_bytes INTEGER NOT NULL,
|
|
20
|
-
codec TEXT NOT NULL,
|
|
21
|
-
has_audio INTEGER NOT NULL DEFAULT 0
|
|
22
|
-
);
|
|
23
|
-
CREATE INDEX IF NOT EXISTS idx_segments_device_time ON recording_segments(device_id, stream_id, start_time);
|
|
24
|
-
CREATE INDEX IF NOT EXISTS idx_segments_start_time ON recording_segments(start_time);
|
|
25
|
-
|
|
26
|
-
CREATE TABLE IF NOT EXISTS recording_thumbnails (
|
|
27
|
-
device_id TEXT NOT NULL,
|
|
28
|
-
timestamp INTEGER NOT NULL,
|
|
29
|
-
path TEXT NOT NULL,
|
|
30
|
-
storage_name TEXT NOT NULL,
|
|
31
|
-
sub_directory TEXT NOT NULL,
|
|
32
|
-
size_bytes INTEGER NOT NULL,
|
|
33
|
-
category TEXT NOT NULL,
|
|
34
|
-
PRIMARY KEY (device_id, timestamp, category)
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
CREATE TABLE IF NOT EXISTS recording_policies (
|
|
38
|
-
device_id TEXT PRIMARY KEY,
|
|
39
|
-
enabled INTEGER NOT NULL DEFAULT 0,
|
|
40
|
-
mode TEXT NOT NULL,
|
|
41
|
-
streams_json TEXT NOT NULL,
|
|
42
|
-
schedule_json TEXT,
|
|
43
|
-
pre_buffer_sec INTEGER NOT NULL DEFAULT 5,
|
|
44
|
-
post_buffer_sec INTEGER NOT NULL DEFAULT 10,
|
|
45
|
-
created_at INTEGER NOT NULL,
|
|
46
|
-
updated_at INTEGER NOT NULL
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
CREATE TABLE IF NOT EXISTS recording_storage_config (
|
|
50
|
-
device_id TEXT NOT NULL,
|
|
51
|
-
data_category TEXT NOT NULL,
|
|
52
|
-
storage_name TEXT NOT NULL,
|
|
53
|
-
sub_directory TEXT NOT NULL,
|
|
54
|
-
retention_days INTEGER,
|
|
55
|
-
retention_gb REAL,
|
|
56
|
-
PRIMARY KEY (device_id, data_category)
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
CREATE TABLE IF NOT EXISTS recording_cleanup_queue (
|
|
60
|
-
device_id TEXT PRIMARY KEY,
|
|
61
|
-
disabled_at INTEGER NOT NULL,
|
|
62
|
-
cleanup_after INTEGER NOT NULL,
|
|
63
|
-
status TEXT NOT NULL,
|
|
64
|
-
started_at INTEGER
|
|
65
|
-
);
|
|
66
|
-
`);
|
|
67
|
-
}
|
|
68
|
-
// --- Segments ---
|
|
69
|
-
insertSegment(seg) {
|
|
70
|
-
this.db.prepare(`
|
|
71
|
-
INSERT INTO recording_segments (id, device_id, stream_id, start_time, end_time, duration, path, storage_name, sub_directory, size_bytes, codec, has_audio)
|
|
72
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
73
|
-
`).run(seg.id, String(seg.deviceId), seg.streamId, seg.startTime, seg.endTime, seg.duration, seg.path, seg.storageName, seg.subDirectory, seg.sizeBytes, seg.codec, seg.hasAudio ? 1 : 0);
|
|
74
|
-
}
|
|
75
|
-
querySegments(deviceId, streamId, startTime, endTime) {
|
|
76
|
-
const rows = this.db.prepare(`
|
|
77
|
-
SELECT * FROM recording_segments
|
|
78
|
-
WHERE device_id = ? AND stream_id = ? AND start_time < ? AND end_time > ?
|
|
79
|
-
ORDER BY start_time ASC
|
|
80
|
-
`).all(String(deviceId), streamId, endTime, startTime);
|
|
81
|
-
return rows.map(rowToSegment);
|
|
82
|
-
}
|
|
83
|
-
deleteSegmentsBefore(deviceId, streamId, beforeTime) {
|
|
84
|
-
const toDelete = this.db.prepare(`
|
|
85
|
-
SELECT * FROM recording_segments WHERE device_id = ? AND stream_id = ? AND start_time < ?
|
|
86
|
-
`).all(String(deviceId), streamId, beforeTime);
|
|
87
|
-
this.db.prepare(`DELETE FROM recording_segments WHERE device_id = ? AND stream_id = ? AND start_time < ?`).run(String(deviceId), streamId, beforeTime);
|
|
88
|
-
return toDelete.map(rowToSegment);
|
|
89
|
-
}
|
|
90
|
-
deleteSegmentsForDevice(deviceId) {
|
|
91
|
-
const toDelete = this.db.prepare(`SELECT * FROM recording_segments WHERE device_id = ?`).all(String(deviceId));
|
|
92
|
-
this.db.prepare(`DELETE FROM recording_segments WHERE device_id = ?`).run(String(deviceId));
|
|
93
|
-
return toDelete.map(rowToSegment);
|
|
94
|
-
}
|
|
95
|
-
getStorageUsage(deviceId, streamId) {
|
|
96
|
-
const row = this.db.prepare(`
|
|
97
|
-
SELECT COALESCE(SUM(size_bytes), 0) as total_bytes, COUNT(*) as segment_count
|
|
98
|
-
FROM recording_segments WHERE device_id = ? AND stream_id = ?
|
|
99
|
-
`).get(String(deviceId), streamId);
|
|
100
|
-
return { totalBytes: row?.total_bytes ?? 0, segmentCount: row?.segment_count ?? 0 };
|
|
101
|
-
}
|
|
102
|
-
/** Global storage usage across all devices and streams. */
|
|
103
|
-
getGlobalStorageUsage() {
|
|
104
|
-
const row = this.db.prepare(`
|
|
105
|
-
SELECT COALESCE(SUM(size_bytes), 0) as total_bytes, COUNT(*) as segment_count
|
|
106
|
-
FROM recording_segments
|
|
107
|
-
`).get();
|
|
108
|
-
return { totalBytes: row?.total_bytes ?? 0, segmentCount: row?.segment_count ?? 0 };
|
|
109
|
-
}
|
|
110
|
-
getOldestSegments(deviceId, streamId, limit) {
|
|
111
|
-
const rows = this.db.prepare(`
|
|
112
|
-
SELECT * FROM recording_segments WHERE device_id = ? AND stream_id = ?
|
|
113
|
-
ORDER BY start_time ASC LIMIT ?
|
|
114
|
-
`).all(String(deviceId), streamId, limit);
|
|
115
|
-
return rows.map(rowToSegment);
|
|
116
|
-
}
|
|
117
|
-
getAvailability(deviceId, startTime, endTime) {
|
|
118
|
-
const rows = this.db.prepare(`
|
|
119
|
-
SELECT start_time, end_time, stream_id FROM recording_segments
|
|
120
|
-
WHERE device_id = ? AND end_time >= ? AND start_time <= ?
|
|
121
|
-
ORDER BY start_time ASC
|
|
122
|
-
`).all(String(deviceId), startTime, endTime);
|
|
123
|
-
if (rows.length === 0) return [];
|
|
124
|
-
const ranges = [];
|
|
125
|
-
let current = { startTime: rows[0].start_time, endTime: rows[0].end_time, streams: /* @__PURE__ */ new Set([rows[0].stream_id]) };
|
|
126
|
-
for (let i = 1; i < rows.length; i++) {
|
|
127
|
-
const row = rows[i];
|
|
128
|
-
if (row.start_time <= current.endTime) {
|
|
129
|
-
current.endTime = Math.max(current.endTime, row.end_time);
|
|
130
|
-
current.streams.add(row.stream_id);
|
|
131
|
-
} else {
|
|
132
|
-
ranges.push(current);
|
|
133
|
-
current = { startTime: row.start_time, endTime: row.end_time, streams: /* @__PURE__ */ new Set([row.stream_id]) };
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
ranges.push(current);
|
|
137
|
-
return ranges.map((r) => ({ startTime: r.startTime, endTime: r.endTime, streams: [...r.streams] }));
|
|
138
|
-
}
|
|
139
|
-
getMotionStats(deviceId, startTime, endTime) {
|
|
140
|
-
const row = this.db.prepare(`
|
|
141
|
-
SELECT COUNT(*) as total_events, COALESCE(AVG(duration), 0) as avg_duration, COALESCE(SUM(duration), 0) as total_duration
|
|
142
|
-
FROM recording_segments
|
|
143
|
-
WHERE device_id = ? AND start_time >= ? AND end_time <= ?
|
|
144
|
-
`).get(String(deviceId), startTime, endTime);
|
|
145
|
-
const timeRangeMs = endTime - startTime;
|
|
146
|
-
const timeRangeDays = Math.max(timeRangeMs / (24 * 60 * 60 * 1e3), 1);
|
|
147
|
-
const timeRangeSec = Math.max(timeRangeMs / 1e3, 1);
|
|
148
|
-
const totalEvents = row?.total_events ?? 0;
|
|
149
|
-
const avgDuration = row?.avg_duration ?? 0;
|
|
150
|
-
const totalDuration = row?.total_duration ?? 0;
|
|
151
|
-
return {
|
|
152
|
-
totalEvents,
|
|
153
|
-
avgDurationSec: Math.round(avgDuration * 100) / 100,
|
|
154
|
-
avgEventsPerDay: Math.round(totalEvents / timeRangeDays * 100) / 100,
|
|
155
|
-
dutyCyclePercent: Math.round(totalDuration / timeRangeSec * 1e4) / 100
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
// --- Thumbnails ---
|
|
159
|
-
insertThumbnail(thumb) {
|
|
160
|
-
this.db.prepare(`
|
|
161
|
-
INSERT OR REPLACE INTO recording_thumbnails (device_id, timestamp, path, storage_name, sub_directory, size_bytes, category)
|
|
162
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
163
|
-
`).run(String(thumb.deviceId), thumb.timestamp, thumb.path, thumb.storageName, thumb.subDirectory, thumb.sizeBytes, thumb.category);
|
|
164
|
-
}
|
|
165
|
-
findNearestThumbnail(deviceId, timestamp, category) {
|
|
166
|
-
const row = this.db.prepare(`
|
|
167
|
-
SELECT * FROM recording_thumbnails
|
|
168
|
-
WHERE device_id = ? AND category = ?
|
|
169
|
-
ORDER BY ABS(timestamp - ?) ASC LIMIT 1
|
|
170
|
-
`).get(String(deviceId), category, timestamp);
|
|
171
|
-
return row ? rowToThumbnail(row) : null;
|
|
172
|
-
}
|
|
173
|
-
deleteThumbnailsBefore(deviceId, beforeTime) {
|
|
174
|
-
const result = this.db.prepare(`DELETE FROM recording_thumbnails WHERE device_id = ? AND timestamp < ?`).run(String(deviceId), beforeTime);
|
|
175
|
-
return result.changes;
|
|
176
|
-
}
|
|
177
|
-
deleteThumbnailsForDevice(deviceId) {
|
|
178
|
-
const result = this.db.prepare(`DELETE FROM recording_thumbnails WHERE device_id = ?`).run(String(deviceId));
|
|
179
|
-
return result.changes;
|
|
180
|
-
}
|
|
181
|
-
// --- Policies ---
|
|
182
|
-
upsertPolicy(policy) {
|
|
183
|
-
const now = Date.now();
|
|
184
|
-
this.db.prepare(`
|
|
185
|
-
INSERT INTO recording_policies (device_id, enabled, mode, streams_json, schedule_json, pre_buffer_sec, post_buffer_sec, created_at, updated_at)
|
|
186
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
187
|
-
ON CONFLICT(device_id) DO UPDATE SET enabled=?, mode=?, streams_json=?, schedule_json=?, pre_buffer_sec=?, post_buffer_sec=?, updated_at=?
|
|
188
|
-
`).run(
|
|
189
|
-
String(policy.deviceId),
|
|
190
|
-
policy.enabled ? 1 : 0,
|
|
191
|
-
policy.mode,
|
|
192
|
-
JSON.stringify(policy.streams),
|
|
193
|
-
policy.scheduleRules ? JSON.stringify(policy.scheduleRules) : null,
|
|
194
|
-
policy.preBufferSec,
|
|
195
|
-
policy.postBufferSec,
|
|
196
|
-
now,
|
|
197
|
-
now,
|
|
198
|
-
policy.enabled ? 1 : 0,
|
|
199
|
-
policy.mode,
|
|
200
|
-
JSON.stringify(policy.streams),
|
|
201
|
-
policy.scheduleRules ? JSON.stringify(policy.scheduleRules) : null,
|
|
202
|
-
policy.preBufferSec,
|
|
203
|
-
policy.postBufferSec,
|
|
204
|
-
now
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
getPolicy(deviceId) {
|
|
208
|
-
const row = this.db.prepare(`SELECT * FROM recording_policies WHERE device_id = ?`).get(String(deviceId));
|
|
209
|
-
return row ? rowToPolicy(row) : null;
|
|
210
|
-
}
|
|
211
|
-
getEnabledPolicies() {
|
|
212
|
-
const rows = this.db.prepare(`SELECT * FROM recording_policies WHERE enabled = 1`).all();
|
|
213
|
-
return rows.map(rowToPolicy);
|
|
214
|
-
}
|
|
215
|
-
deletePolicy(deviceId) {
|
|
216
|
-
this.db.prepare(`DELETE FROM recording_policies WHERE device_id = ?`).run(String(deviceId));
|
|
217
|
-
}
|
|
218
|
-
// --- Storage Config ---
|
|
219
|
-
upsertStorageConfig(config) {
|
|
220
|
-
this.db.prepare(`
|
|
221
|
-
INSERT INTO recording_storage_config (device_id, data_category, storage_name, sub_directory, retention_days, retention_gb)
|
|
222
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
223
|
-
ON CONFLICT(device_id, data_category) DO UPDATE SET storage_name=?, sub_directory=?, retention_days=?, retention_gb=?
|
|
224
|
-
`).run(
|
|
225
|
-
String(config.deviceId),
|
|
226
|
-
config.dataCategory,
|
|
227
|
-
config.storageName,
|
|
228
|
-
config.subDirectory,
|
|
229
|
-
config.retentionDays,
|
|
230
|
-
config.retentionGb,
|
|
231
|
-
config.storageName,
|
|
232
|
-
config.subDirectory,
|
|
233
|
-
config.retentionDays,
|
|
234
|
-
config.retentionGb
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
resolveStorageConfig(deviceId, category) {
|
|
238
|
-
const specific = this.db.prepare(
|
|
239
|
-
`SELECT * FROM recording_storage_config WHERE device_id = ? AND data_category = ?`
|
|
240
|
-
).get(String(deviceId), category);
|
|
241
|
-
if (specific) return rowToStorageConfig(specific);
|
|
242
|
-
const global = this.db.prepare(
|
|
243
|
-
`SELECT * FROM recording_storage_config WHERE device_id = '*' AND data_category = ?`
|
|
244
|
-
).get(category);
|
|
245
|
-
return global ? rowToStorageConfig(global) : null;
|
|
246
|
-
}
|
|
247
|
-
// --- Cleanup Queue ---
|
|
248
|
-
addToCleanupQueue(deviceId, disabledAt) {
|
|
249
|
-
const cleanupAfter = disabledAt + 24 * 60 * 60 * 1e3;
|
|
250
|
-
this.db.prepare(`
|
|
251
|
-
INSERT OR REPLACE INTO recording_cleanup_queue (device_id, disabled_at, cleanup_after, status, started_at)
|
|
252
|
-
VALUES (?, ?, ?, 'pending', NULL)
|
|
253
|
-
`).run(String(deviceId), disabledAt, cleanupAfter);
|
|
254
|
-
}
|
|
255
|
-
cancelCleanup(deviceId) {
|
|
256
|
-
this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'cancelled' WHERE device_id = ? AND status = 'pending'`).run(String(deviceId));
|
|
257
|
-
}
|
|
258
|
-
getCleanupEntry(deviceId) {
|
|
259
|
-
const row = this.db.prepare(`SELECT * FROM recording_cleanup_queue WHERE device_id = ?`).get(String(deviceId));
|
|
260
|
-
return row ? rowToCleanup(row) : null;
|
|
261
|
-
}
|
|
262
|
-
getPendingCleanups() {
|
|
263
|
-
const now = Date.now();
|
|
264
|
-
const rows = this.db.prepare(
|
|
265
|
-
`SELECT * FROM recording_cleanup_queue WHERE status = 'pending' AND cleanup_after <= ?`
|
|
266
|
-
).all(now);
|
|
267
|
-
return rows.map(rowToCleanup);
|
|
268
|
-
}
|
|
269
|
-
resetStaleCleanups(maxAgeMs = 36e5) {
|
|
270
|
-
const cutoff = Date.now() - maxAgeMs;
|
|
271
|
-
this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'pending', started_at = NULL WHERE status = 'in_progress' AND started_at < ?`).run(cutoff);
|
|
272
|
-
}
|
|
273
|
-
markCleanupInProgress(deviceId) {
|
|
274
|
-
this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'in_progress', started_at = ? WHERE device_id = ?`).run(Date.now(), String(deviceId));
|
|
275
|
-
}
|
|
276
|
-
markCleanupCompleted(deviceId) {
|
|
277
|
-
this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'completed' WHERE device_id = ?`).run(String(deviceId));
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
function rowToSegment(row) {
|
|
281
|
-
return {
|
|
282
|
-
id: row.id,
|
|
283
|
-
deviceId: Number(row.device_id),
|
|
284
|
-
streamId: row.stream_id,
|
|
285
|
-
startTime: row.start_time,
|
|
286
|
-
endTime: row.end_time,
|
|
287
|
-
duration: row.duration,
|
|
288
|
-
path: row.path,
|
|
289
|
-
storageName: row.storage_name,
|
|
290
|
-
subDirectory: row.sub_directory,
|
|
291
|
-
sizeBytes: row.size_bytes,
|
|
292
|
-
codec: row.codec,
|
|
293
|
-
hasAudio: row.has_audio === 1
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
function rowToThumbnail(row) {
|
|
297
|
-
return {
|
|
298
|
-
deviceId: Number(row.device_id),
|
|
299
|
-
timestamp: row.timestamp,
|
|
300
|
-
path: row.path,
|
|
301
|
-
storageName: row.storage_name,
|
|
302
|
-
subDirectory: row.sub_directory,
|
|
303
|
-
sizeBytes: row.size_bytes,
|
|
304
|
-
category: row.category
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
function parseStreamPolicies(json) {
|
|
308
|
-
const parsed = JSON.parse(json);
|
|
309
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
310
|
-
}
|
|
311
|
-
function parseScheduleRules(json) {
|
|
312
|
-
if (json === null) return void 0;
|
|
313
|
-
const parsed = JSON.parse(json);
|
|
314
|
-
return Array.isArray(parsed) ? parsed : void 0;
|
|
315
|
-
}
|
|
316
|
-
function rowToPolicy(row) {
|
|
317
|
-
const mode = row.mode;
|
|
318
|
-
return {
|
|
319
|
-
deviceId: Number(row.device_id),
|
|
320
|
-
enabled: row.enabled === 1,
|
|
321
|
-
mode,
|
|
322
|
-
streams: parseStreamPolicies(row.streams_json),
|
|
323
|
-
preBufferSec: row.pre_buffer_sec,
|
|
324
|
-
postBufferSec: row.post_buffer_sec,
|
|
325
|
-
scheduleRules: parseScheduleRules(row.schedule_json)
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
function rowToStorageConfig(row) {
|
|
329
|
-
return {
|
|
330
|
-
deviceId: Number(row.device_id),
|
|
331
|
-
dataCategory: row.data_category,
|
|
332
|
-
storageName: row.storage_name,
|
|
333
|
-
subDirectory: row.sub_directory,
|
|
334
|
-
retentionDays: row.retention_days,
|
|
335
|
-
retentionGb: row.retention_gb
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
function rowToCleanup(row) {
|
|
339
|
-
return {
|
|
340
|
-
deviceId: Number(row.device_id),
|
|
341
|
-
disabledAt: row.disabled_at,
|
|
342
|
-
cleanupAfter: row.cleanup_after,
|
|
343
|
-
status: row.status,
|
|
344
|
-
startedAt: row.started_at
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
exports.RecordingDb = RecordingDb;
|
|
348
|
-
//# sourceMappingURL=recording-db-gOgaoQh0.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"recording-db-gOgaoQh0.js","sources":["../src/recording/recording/recording-db.ts"],"sourcesContent":["import type Database from 'better-sqlite3'\nimport type {\n RecordingSegment, RecordingThumbnail, RecordingStorageConfig,\n RecordingPolicy, RecordingMode, StreamPolicy, ScheduleRule,\n CleanupQueueEntry, CleanupStatus, DataCategory,\n} from './types.js'\n\nexport interface StorageUsage {\n readonly totalBytes: number\n readonly segmentCount: number\n}\n\nexport interface AvailabilityRange {\n readonly startTime: number\n readonly endTime: number\n readonly streams: readonly string[]\n}\n\n// ---------------------------------------------------------------------------\n// SQL row shapes — typed at the better-sqlite3 prepare<TParams, TRow>() boundary\n// so downstream row mappers receive fully-typed records without casts.\n// ---------------------------------------------------------------------------\n\ninterface SegmentRow {\n readonly id: string\n readonly device_id: string\n readonly stream_id: string\n readonly start_time: number\n readonly end_time: number\n readonly duration: number\n readonly path: string\n readonly storage_name: string\n readonly sub_directory: string\n readonly size_bytes: number\n readonly codec: 'h264' | 'h265'\n readonly has_audio: number\n}\n\ninterface ThumbnailRow {\n readonly device_id: string\n readonly timestamp: number\n readonly path: string\n readonly storage_name: string\n readonly sub_directory: string\n readonly size_bytes: number\n readonly category: 'scrub' | 'event'\n}\n\ninterface PolicyRow {\n readonly device_id: string\n readonly enabled: number\n readonly mode: string\n readonly streams_json: string\n readonly schedule_json: string | null\n readonly pre_buffer_sec: number\n readonly post_buffer_sec: number\n}\n\ninterface StorageConfigRow {\n readonly device_id: string\n readonly data_category: DataCategory\n readonly storage_name: string\n readonly sub_directory: string\n readonly retention_days: number | null\n readonly retention_gb: number | null\n}\n\ninterface CleanupRow {\n readonly device_id: string\n readonly disabled_at: number\n readonly cleanup_after: number\n readonly status: CleanupStatus\n readonly started_at: number | null\n}\n\ninterface StorageUsageRow {\n readonly total_bytes: number\n readonly segment_count: number\n}\n\ninterface MotionStatsRow {\n readonly total_events: number\n readonly avg_duration: number\n readonly total_duration: number\n}\n\ninterface AvailabilityRow {\n readonly start_time: number\n readonly end_time: number\n readonly stream_id: string\n}\n\nexport class RecordingDb {\n constructor(private readonly db: Database.Database) {}\n\n initialize(): void {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS recording_segments (\n id TEXT PRIMARY KEY,\n device_id TEXT NOT NULL,\n stream_id TEXT NOT NULL,\n start_time INTEGER NOT NULL,\n end_time INTEGER NOT NULL,\n duration REAL NOT NULL,\n path TEXT NOT NULL,\n storage_name TEXT NOT NULL,\n sub_directory TEXT NOT NULL,\n size_bytes INTEGER NOT NULL,\n codec TEXT NOT NULL,\n has_audio INTEGER NOT NULL DEFAULT 0\n );\n CREATE INDEX IF NOT EXISTS idx_segments_device_time ON recording_segments(device_id, stream_id, start_time);\n CREATE INDEX IF NOT EXISTS idx_segments_start_time ON recording_segments(start_time);\n\n CREATE TABLE IF NOT EXISTS recording_thumbnails (\n device_id TEXT NOT NULL,\n timestamp INTEGER NOT NULL,\n path TEXT NOT NULL,\n storage_name TEXT NOT NULL,\n sub_directory TEXT NOT NULL,\n size_bytes INTEGER NOT NULL,\n category TEXT NOT NULL,\n PRIMARY KEY (device_id, timestamp, category)\n );\n\n CREATE TABLE IF NOT EXISTS recording_policies (\n device_id TEXT PRIMARY KEY,\n enabled INTEGER NOT NULL DEFAULT 0,\n mode TEXT NOT NULL,\n streams_json TEXT NOT NULL,\n schedule_json TEXT,\n pre_buffer_sec INTEGER NOT NULL DEFAULT 5,\n post_buffer_sec INTEGER NOT NULL DEFAULT 10,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS recording_storage_config (\n device_id TEXT NOT NULL,\n data_category TEXT NOT NULL,\n storage_name TEXT NOT NULL,\n sub_directory TEXT NOT NULL,\n retention_days INTEGER,\n retention_gb REAL,\n PRIMARY KEY (device_id, data_category)\n );\n\n CREATE TABLE IF NOT EXISTS recording_cleanup_queue (\n device_id TEXT PRIMARY KEY,\n disabled_at INTEGER NOT NULL,\n cleanup_after INTEGER NOT NULL,\n status TEXT NOT NULL,\n started_at INTEGER\n );\n `)\n }\n\n // --- Segments ---\n\n insertSegment(seg: RecordingSegment): void {\n this.db.prepare(`\n INSERT INTO recording_segments (id, device_id, stream_id, start_time, end_time, duration, path, storage_name, sub_directory, size_bytes, codec, has_audio)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `).run(seg.id, String(seg.deviceId), seg.streamId, seg.startTime, seg.endTime, seg.duration, seg.path, seg.storageName, seg.subDirectory, seg.sizeBytes, seg.codec, seg.hasAudio ? 1 : 0)\n }\n\n querySegments(deviceId: number, streamId: string, startTime: number, endTime: number): readonly RecordingSegment[] {\n const rows = this.db.prepare<[string, string, number, number], SegmentRow>(`\n SELECT * FROM recording_segments\n WHERE device_id = ? AND stream_id = ? AND start_time < ? AND end_time > ?\n ORDER BY start_time ASC\n `).all(String(deviceId), streamId, endTime, startTime)\n return rows.map(rowToSegment)\n }\n\n deleteSegmentsBefore(deviceId: number, streamId: string, beforeTime: number): readonly RecordingSegment[] {\n const toDelete = this.db.prepare<[string, string, number], SegmentRow>(`\n SELECT * FROM recording_segments WHERE device_id = ? AND stream_id = ? AND start_time < ?\n `).all(String(deviceId), streamId, beforeTime)\n this.db.prepare(`DELETE FROM recording_segments WHERE device_id = ? AND stream_id = ? AND start_time < ?`).run(String(deviceId), streamId, beforeTime)\n return toDelete.map(rowToSegment)\n }\n\n deleteSegmentsForDevice(deviceId: number): readonly RecordingSegment[] {\n const toDelete = this.db.prepare<[string], SegmentRow>(`SELECT * FROM recording_segments WHERE device_id = ?`).all(String(deviceId))\n this.db.prepare(`DELETE FROM recording_segments WHERE device_id = ?`).run(String(deviceId))\n return toDelete.map(rowToSegment)\n }\n\n getStorageUsage(deviceId: number, streamId: string): StorageUsage {\n const row = this.db.prepare<[string, string], StorageUsageRow>(`\n SELECT COALESCE(SUM(size_bytes), 0) as total_bytes, COUNT(*) as segment_count\n FROM recording_segments WHERE device_id = ? AND stream_id = ?\n `).get(String(deviceId), streamId)\n return { totalBytes: row?.total_bytes ?? 0, segmentCount: row?.segment_count ?? 0 }\n }\n\n /** Global storage usage across all devices and streams. */\n getGlobalStorageUsage(): StorageUsage {\n const row = this.db.prepare<[], StorageUsageRow>(`\n SELECT COALESCE(SUM(size_bytes), 0) as total_bytes, COUNT(*) as segment_count\n FROM recording_segments\n `).get()\n return { totalBytes: row?.total_bytes ?? 0, segmentCount: row?.segment_count ?? 0 }\n }\n\n getOldestSegments(deviceId: number, streamId: string, limit: number): readonly RecordingSegment[] {\n const rows = this.db.prepare<[string, string, number], SegmentRow>(`\n SELECT * FROM recording_segments WHERE device_id = ? AND stream_id = ?\n ORDER BY start_time ASC LIMIT ?\n `).all(String(deviceId), streamId, limit)\n return rows.map(rowToSegment)\n }\n\n getAvailability(deviceId: number, startTime: number, endTime: number): readonly AvailabilityRange[] {\n const rows = this.db.prepare<[string, number, number], AvailabilityRow>(`\n SELECT start_time, end_time, stream_id FROM recording_segments\n WHERE device_id = ? AND end_time >= ? AND start_time <= ?\n ORDER BY start_time ASC\n `).all(String(deviceId), startTime, endTime)\n\n if (rows.length === 0) return []\n\n const ranges: Array<{ startTime: number; endTime: number; streams: Set<string> }> = []\n let current = { startTime: rows[0]!.start_time, endTime: rows[0]!.end_time, streams: new Set([rows[0]!.stream_id]) }\n\n for (let i = 1; i < rows.length; i++) {\n const row = rows[i]!\n if (row.start_time <= current.endTime) {\n current.endTime = Math.max(current.endTime, row.end_time)\n current.streams.add(row.stream_id)\n } else {\n ranges.push(current)\n current = { startTime: row.start_time, endTime: row.end_time, streams: new Set([row.stream_id]) }\n }\n }\n ranges.push(current)\n\n return ranges.map(r => ({ startTime: r.startTime, endTime: r.endTime, streams: [...r.streams] }))\n }\n\n getMotionStats(deviceId: number, startTime: number, endTime: number): {\n readonly totalEvents: number\n readonly avgDurationSec: number\n readonly avgEventsPerDay: number\n readonly dutyCyclePercent: number\n } {\n const row = this.db.prepare<[string, number, number], MotionStatsRow>(`\n SELECT COUNT(*) as total_events, COALESCE(AVG(duration), 0) as avg_duration, COALESCE(SUM(duration), 0) as total_duration\n FROM recording_segments\n WHERE device_id = ? AND start_time >= ? AND end_time <= ?\n `).get(String(deviceId), startTime, endTime)\n\n const timeRangeMs = endTime - startTime\n const timeRangeDays = Math.max(timeRangeMs / (24 * 60 * 60 * 1000), 1)\n const timeRangeSec = Math.max(timeRangeMs / 1000, 1)\n\n const totalEvents = row?.total_events ?? 0\n const avgDuration = row?.avg_duration ?? 0\n const totalDuration = row?.total_duration ?? 0\n\n return {\n totalEvents,\n avgDurationSec: Math.round(avgDuration * 100) / 100,\n avgEventsPerDay: Math.round((totalEvents / timeRangeDays) * 100) / 100,\n dutyCyclePercent: Math.round((totalDuration / timeRangeSec) * 10000) / 100,\n }\n }\n\n // --- Thumbnails ---\n\n insertThumbnail(thumb: RecordingThumbnail): void {\n this.db.prepare(`\n INSERT OR REPLACE INTO recording_thumbnails (device_id, timestamp, path, storage_name, sub_directory, size_bytes, category)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `).run(String(thumb.deviceId), thumb.timestamp, thumb.path, thumb.storageName, thumb.subDirectory, thumb.sizeBytes, thumb.category)\n }\n\n findNearestThumbnail(deviceId: number, timestamp: number, category: string): RecordingThumbnail | null {\n const row = this.db.prepare<[string, string, number], ThumbnailRow>(`\n SELECT * FROM recording_thumbnails\n WHERE device_id = ? AND category = ?\n ORDER BY ABS(timestamp - ?) ASC LIMIT 1\n `).get(String(deviceId), category, timestamp)\n return row ? rowToThumbnail(row) : null\n }\n\n deleteThumbnailsBefore(deviceId: number, beforeTime: number): number {\n const result = this.db.prepare(`DELETE FROM recording_thumbnails WHERE device_id = ? AND timestamp < ?`).run(String(deviceId), beforeTime)\n return result.changes\n }\n\n deleteThumbnailsForDevice(deviceId: number): number {\n const result = this.db.prepare(`DELETE FROM recording_thumbnails WHERE device_id = ?`).run(String(deviceId))\n return result.changes\n }\n\n // --- Policies ---\n\n upsertPolicy(policy: { deviceId: number; enabled: boolean; mode: RecordingMode; streams: readonly StreamPolicy[]; preBufferSec: number; postBufferSec: number; scheduleRules?: readonly ScheduleRule[] }): void {\n const now = Date.now()\n this.db.prepare(`\n INSERT INTO recording_policies (device_id, enabled, mode, streams_json, schedule_json, pre_buffer_sec, post_buffer_sec, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(device_id) DO UPDATE SET enabled=?, mode=?, streams_json=?, schedule_json=?, pre_buffer_sec=?, post_buffer_sec=?, updated_at=?\n `).run(\n String(policy.deviceId), policy.enabled ? 1 : 0, policy.mode, JSON.stringify(policy.streams), policy.scheduleRules ? JSON.stringify(policy.scheduleRules) : null, policy.preBufferSec, policy.postBufferSec, now, now,\n policy.enabled ? 1 : 0, policy.mode, JSON.stringify(policy.streams), policy.scheduleRules ? JSON.stringify(policy.scheduleRules) : null, policy.preBufferSec, policy.postBufferSec, now,\n )\n }\n\n getPolicy(deviceId: number): RecordingPolicy | null {\n const row = this.db.prepare<[string], PolicyRow>(`SELECT * FROM recording_policies WHERE device_id = ?`).get(String(deviceId))\n return row ? rowToPolicy(row) : null\n }\n\n getEnabledPolicies(): readonly RecordingPolicy[] {\n const rows = this.db.prepare<[], PolicyRow>(`SELECT * FROM recording_policies WHERE enabled = 1`).all()\n return rows.map(rowToPolicy)\n }\n\n deletePolicy(deviceId: number): void {\n this.db.prepare(`DELETE FROM recording_policies WHERE device_id = ?`).run(String(deviceId))\n }\n\n // --- Storage Config ---\n\n upsertStorageConfig(config: RecordingStorageConfig): void {\n this.db.prepare(`\n INSERT INTO recording_storage_config (device_id, data_category, storage_name, sub_directory, retention_days, retention_gb)\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(device_id, data_category) DO UPDATE SET storage_name=?, sub_directory=?, retention_days=?, retention_gb=?\n `).run(String(config.deviceId), config.dataCategory, config.storageName, config.subDirectory, config.retentionDays, config.retentionGb,\n config.storageName, config.subDirectory, config.retentionDays, config.retentionGb)\n }\n\n resolveStorageConfig(deviceId: number, category: DataCategory): RecordingStorageConfig | null {\n const specific = this.db.prepare<[string, DataCategory], StorageConfigRow>(\n `SELECT * FROM recording_storage_config WHERE device_id = ? AND data_category = ?`,\n ).get(String(deviceId), category)\n if (specific) return rowToStorageConfig(specific)\n const global = this.db.prepare<[DataCategory], StorageConfigRow>(\n `SELECT * FROM recording_storage_config WHERE device_id = '*' AND data_category = ?`,\n ).get(category)\n return global ? rowToStorageConfig(global) : null\n }\n\n // --- Cleanup Queue ---\n\n addToCleanupQueue(deviceId: number, disabledAt: number): void {\n const cleanupAfter = disabledAt + 24 * 60 * 60 * 1000\n this.db.prepare(`\n INSERT OR REPLACE INTO recording_cleanup_queue (device_id, disabled_at, cleanup_after, status, started_at)\n VALUES (?, ?, ?, 'pending', NULL)\n `).run(String(deviceId), disabledAt, cleanupAfter)\n }\n\n cancelCleanup(deviceId: number): void {\n this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'cancelled' WHERE device_id = ? AND status = 'pending'`).run(String(deviceId))\n }\n\n getCleanupEntry(deviceId: number): CleanupQueueEntry | null {\n const row = this.db.prepare<[string], CleanupRow>(`SELECT * FROM recording_cleanup_queue WHERE device_id = ?`).get(String(deviceId))\n return row ? rowToCleanup(row) : null\n }\n\n getPendingCleanups(): readonly CleanupQueueEntry[] {\n const now = Date.now()\n const rows = this.db.prepare<[number], CleanupRow>(\n `SELECT * FROM recording_cleanup_queue WHERE status = 'pending' AND cleanup_after <= ?`,\n ).all(now)\n return rows.map(rowToCleanup)\n }\n\n resetStaleCleanups(maxAgeMs: number = 3600000): void {\n const cutoff = Date.now() - maxAgeMs\n this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'pending', started_at = NULL WHERE status = 'in_progress' AND started_at < ?`).run(cutoff)\n }\n\n markCleanupInProgress(deviceId: number): void {\n this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'in_progress', started_at = ? WHERE device_id = ?`).run(Date.now(), String(deviceId))\n }\n\n markCleanupCompleted(deviceId: number): void {\n this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'completed' WHERE device_id = ?`).run(String(deviceId))\n }\n}\n\n// --- Row mappers ---\n\nfunction rowToSegment(row: SegmentRow): RecordingSegment {\n return {\n id: row.id,\n deviceId: Number(row.device_id),\n streamId: row.stream_id,\n startTime: row.start_time,\n endTime: row.end_time,\n duration: row.duration,\n path: row.path,\n storageName: row.storage_name,\n subDirectory: row.sub_directory,\n sizeBytes: row.size_bytes,\n codec: row.codec,\n hasAudio: row.has_audio === 1,\n }\n}\n\nfunction rowToThumbnail(row: ThumbnailRow): RecordingThumbnail {\n return {\n deviceId: Number(row.device_id),\n timestamp: row.timestamp,\n path: row.path,\n storageName: row.storage_name,\n subDirectory: row.sub_directory,\n sizeBytes: row.size_bytes,\n category: row.category,\n }\n}\n\n/** Parse a policy JSON column with a structural type predicate.\n * JSON columns are serialised at write time via JSON.stringify with the\n * corresponding typed value, so at read time we trust the shape but still\n * narrow via `parseJsonUnknown` to keep the boundary documented. */\nfunction parseStreamPolicies(json: string): StreamPolicy[] {\n const parsed: unknown = JSON.parse(json)\n return Array.isArray(parsed) ? (parsed as StreamPolicy[]) : []\n}\n\nfunction parseScheduleRules(json: string | null): ScheduleRule[] | undefined {\n if (json === null) return undefined\n const parsed: unknown = JSON.parse(json)\n return Array.isArray(parsed) ? (parsed as ScheduleRule[]) : undefined\n}\n\nfunction rowToPolicy(row: PolicyRow): RecordingPolicy {\n // `row.mode` comes from a TEXT column whose values are written with typed\n // RecordingMode — narrowed via single documented bridge at this boundary.\n const mode = row.mode as RecordingMode\n return {\n deviceId: Number(row.device_id),\n enabled: row.enabled === 1,\n mode,\n streams: parseStreamPolicies(row.streams_json),\n preBufferSec: row.pre_buffer_sec,\n postBufferSec: row.post_buffer_sec,\n scheduleRules: parseScheduleRules(row.schedule_json),\n }\n}\n\nfunction rowToStorageConfig(row: StorageConfigRow): RecordingStorageConfig {\n // Note: when device_id == '*' (global config sentinel), Number() yields NaN;\n // callers of resolveStorageConfig only read storageName/subDirectory/retention*,\n // never deviceId, so the sentinel row's deviceId is intentionally not preserved.\n return {\n deviceId: Number(row.device_id),\n dataCategory: row.data_category,\n storageName: row.storage_name,\n subDirectory: row.sub_directory,\n retentionDays: row.retention_days,\n retentionGb: row.retention_gb,\n }\n}\n\nfunction rowToCleanup(row: CleanupRow): CleanupQueueEntry {\n return {\n deviceId: Number(row.device_id),\n disabledAt: row.disabled_at,\n cleanupAfter: row.cleanup_after,\n status: row.status,\n startedAt: row.started_at,\n }\n}\n"],"names":[],"mappings":"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"}
|