@camstack/addon-pipeline 0.1.1 → 0.1.3
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/addon.cjs +3 -4
- package/dist/addon.cjs.map +1 -1
- package/dist/addon.js +2 -3
- package/dist/addon.js.map +1 -1
- package/dist/index.cjs +3 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
package/dist/addon.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/recording/ffmpeg-config.ts","../src/recording/recording-db.ts","../src/recording/segment-writer.ts","../src/recording/thumbnail-extractor.ts","../src/recording/retention-manager.ts","../src/recording/playlist-generator.ts","../src/recording/storage-estimator.ts","../src/recording/recording-coordinator.ts","../src/stream-broker/decoder-registry.ts","../src/stream-broker/ffmpeg-decoder-session.ts","../src/stream-broker/frame-dropper.ts","../src/stream-broker/ffmpeg-decoder-provider.ts","../src/stream-broker/stream-broker.ts","../src/stream-broker/stream-pipe-server.ts","../src/stream-broker/stream-broker-manager.ts","../src/persistence/event-persistence.ts","../src/persistence/known-faces.ts","../src/persistence/session-tracker.ts","../src/persistence/retention.ts","../src/persistence/track-trail.ts","../src/addon.ts"],"sourcesContent":["import { execFileSync } from 'node:child_process'\nimport * as os from 'node:os'\nimport type { FfmpegConfig } from '@camstack/types'\nimport type { IScopedLogger } from '@camstack/types'\nimport { ensureFfmpeg } from '@camstack/core'\n\n/**\n * Probe ffmpeg binary and detect optimal hwaccel for the host platform.\n * Called once at server startup, result cached.\n */\nexport function detectPlatformDefaults(ffmpegPath: string = 'ffmpeg'): Partial<FfmpegConfig> {\n let hwaccels: string[] = []\n try {\n const output = execFileSync(ffmpegPath, ['-hwaccels', '-hide_banner'], { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })\n hwaccels = output.split('\\n').map(l => l.trim()).filter(l => l && l !== 'Hardware acceleration methods:')\n } catch {}\n\n const platform = os.platform()\n if (platform === 'darwin' && hwaccels.includes('videotoolbox')) return { hwaccel: 'videotoolbox', threads: 0 }\n if (platform === 'linux') {\n for (const hw of ['vaapi', 'qsv', 'cuda', 'v4l2m2m'] as const) {\n if (hwaccels.includes(hw)) return { hwaccel: hw, threads: 0 }\n }\n }\n return { hwaccel: 'none', threads: Math.max(1, Math.floor(os.cpus().length / 2)) }\n}\n\n/**\n * Resolve ffmpeg config for a device.\n * Merge order: detected <- global <- per-device (most specific wins).\n */\nexport function resolveFfmpegConfig(\n deviceConfig: Partial<FfmpegConfig> | undefined,\n globalConfig: Partial<FfmpegConfig>,\n detected: Partial<FfmpegConfig>,\n): FfmpegConfig {\n return {\n path: deviceConfig?.path ?? globalConfig.path ?? detected.path ?? 'ffmpeg',\n hwaccel: deviceConfig?.hwaccel ?? globalConfig.hwaccel ?? detected.hwaccel ?? 'none',\n inputArgs: deviceConfig?.inputArgs ?? globalConfig.inputArgs,\n outputArgs: deviceConfig?.outputArgs ?? globalConfig.outputArgs,\n videoCodec: deviceConfig?.videoCodec ?? globalConfig.videoCodec ?? 'copy',\n audioCodec: deviceConfig?.audioCodec ?? globalConfig.audioCodec ?? 'copy',\n threads: deviceConfig?.threads ?? globalConfig.threads ?? detected.threads,\n }\n}\n\n/**\n * Build common ffmpeg args from a resolved config.\n */\nexport function buildFfmpegInputArgs(config: FfmpegConfig, inputUrl: string): string[] {\n const args: string[] = ['-hide_banner', '-loglevel', 'warning']\n if (config.hwaccel && config.hwaccel !== 'none') args.push('-hwaccel', config.hwaccel)\n if (config.threads !== undefined) args.push('-threads', String(config.threads))\n if (config.inputArgs?.length) args.push(...config.inputArgs)\n args.push('-rtsp_transport', 'tcp', '-i', inputUrl)\n return args\n}\n\nexport function buildFfmpegOutputArgs(config: FfmpegConfig): string[] {\n const args: string[] = ['-c:v', config.videoCodec ?? 'copy', '-c:a', config.audioCodec ?? 'copy']\n if (config.outputArgs?.length) args.push(...config.outputArgs)\n return args\n}\n\n/**\n * Resolve ffmpeg binary path. Priority:\n * 1. Explicit config path\n * 2. Embedded (data/deps/ffmpeg)\n * 3. System PATH\n * 4. Download static build\n */\nexport async function resolveFfmpegBinary(\n configPath: string | undefined,\n dataDir: string,\n logger: IScopedLogger,\n): Promise<string> {\n // Explicit config\n if (configPath && configPath !== 'ffmpeg') {\n return configPath\n }\n\n // Try embedded or download\n return ensureFfmpeg(dataDir, logger)\n}\n","import type Database from 'better-sqlite3'\nimport type {\n RecordingSegment, RecordingThumbnail, RecordingStorageConfig,\n RecordingPolicy, RecordingMode, StreamPolicy, ScheduleRule,\n CleanupQueueEntry, CleanupStatus, DataCategory,\n} from './types'\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\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\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, 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: string, streamId: string, startTime: number, endTime: number): readonly RecordingSegment[] {\n const rows = this.db.prepare(`\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(deviceId, streamId, endTime, startTime) as Array<Record<string, unknown>>\n return rows.map(rowToSegment)\n }\n\n deleteSegmentsBefore(deviceId: string, streamId: string, beforeTime: number): readonly RecordingSegment[] {\n const toDelete = this.db.prepare(`\n SELECT * FROM recording_segments WHERE device_id = ? AND stream_id = ? AND start_time < ?\n `).all(deviceId, streamId, beforeTime) as Array<Record<string, unknown>>\n this.db.prepare(`DELETE FROM recording_segments WHERE device_id = ? AND stream_id = ? AND start_time < ?`).run(deviceId, streamId, beforeTime)\n return toDelete.map(rowToSegment)\n }\n\n deleteSegmentsForDevice(deviceId: string): readonly RecordingSegment[] {\n const toDelete = this.db.prepare(`SELECT * FROM recording_segments WHERE device_id = ?`).all(deviceId) as Array<Record<string, unknown>>\n this.db.prepare(`DELETE FROM recording_segments WHERE device_id = ?`).run(deviceId)\n return toDelete.map(rowToSegment)\n }\n\n getStorageUsage(deviceId: string, streamId: string): StorageUsage {\n const row = this.db.prepare(`\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(deviceId, streamId) as { total_bytes: number; segment_count: number }\n return { totalBytes: row.total_bytes, segmentCount: row.segment_count }\n }\n\n getOldestSegments(deviceId: string, streamId: string, limit: number): readonly RecordingSegment[] {\n const rows = this.db.prepare(`\n SELECT * FROM recording_segments WHERE device_id = ? AND stream_id = ?\n ORDER BY start_time ASC LIMIT ?\n `).all(deviceId, streamId, limit) as Array<Record<string, unknown>>\n return rows.map(rowToSegment)\n }\n\n getAvailability(deviceId: string, startTime: number, endTime: number): readonly AvailabilityRange[] {\n const rows = this.db.prepare(`\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(deviceId, startTime, endTime) as Array<{ start_time: number; end_time: number; stream_id: string }>\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: string, 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(`\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(deviceId, startTime, endTime) as { total_events: number; avg_duration: number; total_duration: number }\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 return {\n totalEvents: row.total_events,\n avgDurationSec: Math.round(row.avg_duration * 100) / 100,\n avgEventsPerDay: Math.round((row.total_events / timeRangeDays) * 100) / 100,\n dutyCyclePercent: Math.round((row.total_duration / 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(thumb.deviceId, thumb.timestamp, thumb.path, thumb.storageName, thumb.subDirectory, thumb.sizeBytes, thumb.category)\n }\n\n findNearestThumbnail(deviceId: string, timestamp: number, category: string): RecordingThumbnail | null {\n const row = this.db.prepare(`\n SELECT * FROM recording_thumbnails\n WHERE device_id = ? AND category = ?\n ORDER BY ABS(timestamp - ?) ASC LIMIT 1\n `).get(deviceId, category, timestamp) as Record<string, unknown> | undefined\n return row ? rowToThumbnail(row) : null\n }\n\n deleteThumbnailsBefore(deviceId: string, beforeTime: number): number {\n const result = this.db.prepare(`DELETE FROM recording_thumbnails WHERE device_id = ? AND timestamp < ?`).run(deviceId, beforeTime)\n return result.changes\n }\n\n deleteThumbnailsForDevice(deviceId: string): number {\n const result = this.db.prepare(`DELETE FROM recording_thumbnails WHERE device_id = ?`).run(deviceId)\n return result.changes\n }\n\n // --- Policies ---\n\n upsertPolicy(policy: { deviceId: string; 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 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: string): RecordingPolicy | null {\n const row = this.db.prepare(`SELECT * FROM recording_policies WHERE device_id = ?`).get(deviceId) as PolicyRow | undefined\n return row ? rowToPolicy(row) : null\n }\n\n getEnabledPolicies(): readonly RecordingPolicy[] {\n const rows = this.db.prepare(`SELECT * FROM recording_policies WHERE enabled = 1`).all() as PolicyRow[]\n return rows.map(rowToPolicy)\n }\n\n deletePolicy(deviceId: string): void {\n this.db.prepare(`DELETE FROM recording_policies WHERE device_id = ?`).run(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(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: string, category: DataCategory): RecordingStorageConfig | null {\n const specific = this.db.prepare(`SELECT * FROM recording_storage_config WHERE device_id = ? AND data_category = ?`).get(deviceId, category) as Record<string, unknown> | undefined\n if (specific) return rowToStorageConfig(specific)\n const global = this.db.prepare(`SELECT * FROM recording_storage_config WHERE device_id = '*' AND data_category = ?`).get(category) as Record<string, unknown> | undefined\n return global ? rowToStorageConfig(global) : null\n }\n\n // --- Cleanup Queue ---\n\n addToCleanupQueue(deviceId: string, 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(deviceId, disabledAt, cleanupAfter)\n }\n\n cancelCleanup(deviceId: string): void {\n this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'cancelled' WHERE device_id = ? AND status = 'pending'`).run(deviceId)\n }\n\n getCleanupEntry(deviceId: string): CleanupQueueEntry | null {\n const row = this.db.prepare(`SELECT * FROM recording_cleanup_queue WHERE device_id = ?`).get(deviceId) as Record<string, unknown> | undefined\n return row ? rowToCleanup(row) : null\n }\n\n getPendingCleanups(): readonly CleanupQueueEntry[] {\n const now = Date.now()\n const rows = this.db.prepare(`SELECT * FROM recording_cleanup_queue WHERE status = 'pending' AND cleanup_after <= ?`).all(now) as Array<Record<string, unknown>>\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: string): void {\n this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'in_progress', started_at = ? WHERE device_id = ?`).run(Date.now(), deviceId)\n }\n\n markCleanupCompleted(deviceId: string): void {\n this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'completed' WHERE device_id = ?`).run(deviceId)\n }\n}\n\n// --- Row mappers ---\n\nfunction rowToSegment(row: Record<string, unknown>): RecordingSegment {\n return {\n id: row.id as string,\n deviceId: row.device_id as string,\n streamId: row.stream_id as string,\n startTime: row.start_time as number,\n endTime: row.end_time as number,\n duration: row.duration as number,\n path: row.path as string,\n storageName: row.storage_name as string,\n subDirectory: row.sub_directory as string,\n sizeBytes: row.size_bytes as number,\n codec: row.codec as 'h264' | 'h265',\n hasAudio: (row.has_audio as number) === 1,\n }\n}\n\nfunction rowToThumbnail(row: Record<string, unknown>): RecordingThumbnail {\n return {\n deviceId: row.device_id as string,\n timestamp: row.timestamp as number,\n path: row.path as string,\n storageName: row.storage_name as string,\n subDirectory: row.sub_directory as string,\n sizeBytes: row.size_bytes as number,\n category: row.category as 'scrub' | 'event',\n }\n}\n\nfunction rowToPolicy(row: PolicyRow): RecordingPolicy {\n return {\n deviceId: row.device_id,\n enabled: row.enabled === 1,\n mode: row.mode as RecordingMode,\n streams: JSON.parse(row.streams_json) as StreamPolicy[],\n preBufferSec: row.pre_buffer_sec,\n postBufferSec: row.post_buffer_sec,\n scheduleRules: row.schedule_json ? JSON.parse(row.schedule_json) as ScheduleRule[] : undefined,\n }\n}\n\nfunction rowToStorageConfig(row: Record<string, unknown>): RecordingStorageConfig {\n return {\n deviceId: row.device_id as string,\n dataCategory: row.data_category as DataCategory,\n storageName: row.storage_name as string,\n subDirectory: row.sub_directory as string,\n retentionDays: row.retention_days as number | null,\n retentionGb: row.retention_gb as number | null,\n }\n}\n\nfunction rowToCleanup(row: Record<string, unknown>): CleanupQueueEntry {\n return {\n deviceId: row.device_id as string,\n disabledAt: row.disabled_at as number,\n cleanupAfter: row.cleanup_after as number,\n status: row.status as CleanupStatus,\n startedAt: row.started_at as number | null,\n }\n}\n","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 type { IAddonFileStorage } from '@camstack/types'\nimport type { IScopedLogger, IEventBus, INetworkQualityTracker, FfmpegConfig } from '@camstack/types'\nimport { buildFfmpegInputArgs, buildFfmpegOutputArgs } from './ffmpeg-config'\nimport type { RecordingDb } from './recording-db'\nimport type { SegmentWriterState, RecordingSegment } from './types'\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: string\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 private readonly _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 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 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.length} buffered segments to disk`, {\n deviceId: this.config.deviceId,\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: string, 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: ${msg}`)\n this.parseSegmentOutput(msg)\n }\n })\n\n this.ffmpeg.on('error', (err) => {\n this.logger.warn(`ffmpeg process 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 code ${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: 'recording.error',\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(\n `Max restarts exceeded for ${this.config.deviceId}/${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: 'recording.health.degraded',\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 in ${backoffMs}ms (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 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 for ${elapsed}ms, restarting ffmpeg`)\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: ${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: 'recording.storage.critical',\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: 'recording.segment.written',\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: ${err}`)\n }\n } catch (err) {\n this.logger.error(`Disk space check failed: ${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: 'recording.segment.written',\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: ${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'\n\nexport interface ThumbnailExtractorConfig {\n readonly deviceId: string\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: string): void {\n this.active = true\n\n this.unsubscribe = pipeline.onVideoFrame(\n (frame) => { this.handleFrame(frame).catch((err) => this.logger.debug(`Thumbnail error: ${err}`)) },\n this.videoRequirements,\n )\n\n this.logger.info(`ThumbnailExtractor attached for ${this.config.deviceId}`)\n }\n\n detachFromPipeline(_deviceId: string): void {\n this.active = false\n if (this.unsubscribe) {\n this.unsubscribe()\n this.unsubscribe = null\n }\n this.logger.info(`ThumbnailExtractor detached for ${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: string, timestamp: number): string {\n return `${subDirectory}/${deviceId}/${timestamp}.jpg`\n }\n}\n","import type { IScopedLogger, IEventBus, IFileStorage } from '@camstack/types'\nimport type { RecordingDb } from './recording-db'\nimport type { DataCategory } from './types'\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 fileStorage: IFileStorage,\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 for ${entry.deviceId}: ${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: 'recording.retention.completed',\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: ${err}`)\n this.scheduleNextCycle(NORMAL_INTERVAL_MS)\n }\n }, intervalMs)\n }\n\n private emitStorageEvent(category: string, deviceId: string, 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.fileStorage.deleteFile(filePath)\n } catch {\n // File may already be deleted\n }\n }\n}\n","import type { RecordingDb } from './recording-db'\nimport type { RecordingSegment } from './types'\n\ninterface PlaylistOptions {\n readonly live?: boolean\n}\n\nconst FALLBACK_STREAMS = ['sub', 'mid', 'main'] as const\n\nexport class PlaylistGenerator {\n constructor(private readonly db: RecordingDb) {}\n\n generate(deviceId: string, streamId: string, startTime: number, endTime: number, options?: PlaylistOptions): string {\n const segments = this.resolveSegments(deviceId, streamId, startTime, endTime)\n return this.buildPlaylist(segments, options)\n }\n\n private resolveSegments(deviceId: string, streamId: string, startTime: number, endTime: number): readonly RecordingSegment[] {\n const segments = this.db.querySegments(deviceId, streamId, startTime, endTime)\n if (segments.length > 0) {\n return segments\n }\n\n const fallbacks = FALLBACK_STREAMS.filter(s => s !== streamId)\n for (const fb of fallbacks) {\n const fallbackSegments = this.db.querySegments(deviceId, fb, startTime, endTime)\n if (fallbackSegments.length > 0) {\n return fallbackSegments\n }\n }\n\n return []\n }\n\n private buildPlaylist(segments: readonly RecordingSegment[], options?: PlaylistOptions): string {\n const targetDuration = segments.length > 0\n ? Math.ceil(Math.max(...segments.map(s => s.duration)))\n : 4\n\n const lines: string[] = [\n '#EXTM3U',\n '#EXT-X-VERSION:7',\n `#EXT-X-TARGETDURATION:${targetDuration}`,\n '#EXT-X-MEDIA-SEQUENCE:0',\n ]\n\n if (!options?.live) {\n lines.push('#EXT-X-PLAYLIST-TYPE:VOD')\n }\n\n for (const seg of segments) {\n lines.push(`#EXTINF:${seg.duration.toFixed(3)},`)\n lines.push(seg.path)\n }\n\n if (!options?.live) {\n lines.push('#EXT-X-ENDLIST')\n }\n\n return lines.join('\\n')\n }\n}\n","import type { INetworkQualityTracker } from '@camstack/types'\nimport type { RecordingDb } from './recording-db'\nimport type { StorageEstimate, StreamEstimate } from './types'\n\ninterface MotionEstimateInput {\n readonly avgEventsPerDay: number\n readonly avgDurationSec: number\n}\n\nexport class StorageEstimator {\n constructor(\n private readonly db: RecordingDb,\n private readonly networkTracker: INetworkQualityTracker,\n ) {}\n\n estimateForDevice(deviceId: string, motionInput?: MotionEstimateInput): StorageEstimate {\n const policy = this.db.getPolicy(deviceId)\n if (!policy) return { perStream: {}, thumbnails: { estimatedGb: 0 }, totalEstimatedGb: 0 }\n\n const stats = this.networkTracker.getDeviceStats(deviceId)\n const perStream: Record<string, StreamEstimate> = {}\n let totalGb = 0\n\n const motionEstimate = motionInput ? {\n avgEventsPerDay: motionInput.avgEventsPerDay,\n avgDurationSec: motionInput.avgDurationSec,\n dutyCyclePercent: (motionInput.avgEventsPerDay * motionInput.avgDurationSec / 86400) * 100,\n } : undefined\n\n for (const sp of policy.streams) {\n const category = `recording:${sp.streamId}` as any\n const config = this.db.resolveStorageConfig(deviceId, category)\n const retentionDays = config?.retentionDays ?? null\n const retentionGb = config?.retentionGb ?? null\n\n const streamStats = stats?.streams[sp.streamId]\n const observedBitrate = streamStats?.observedBitrateKbps ?? 0\n const bitrateKbps = observedBitrate > 0\n ? observedBitrate\n : (streamStats?.nominalBitrateKbps ?? 4000)\n\n let estimatedGb = 0\n if (retentionDays !== null) {\n const retentionSeconds = retentionDays * 86400\n estimatedGb = (bitrateKbps * retentionSeconds) / 8 / 1024 / 1024 / 1024\n }\n\n if (policy.mode === 'motion' && motionEstimate) {\n estimatedGb = estimatedGb * motionEstimate.dutyCyclePercent / 100\n }\n\n let estimatedDaysAtCapacity: number | null = null\n if (retentionGb !== null && estimatedGb > retentionGb) {\n estimatedDaysAtCapacity = retentionDays !== null ? retentionDays * (retentionGb / estimatedGb) : null\n estimatedGb = retentionGb\n }\n\n perStream[sp.streamId] = { bitrateKbps, retentionDays, retentionGb, estimatedGb, estimatedDaysAtCapacity }\n totalGb += estimatedGb\n }\n\n const thumbRetentionDays = policy.streams[0]\n ? (this.db.resolveStorageConfig(deviceId, 'thumbnail:scrub' as any)?.retentionDays ?? 7)\n : 7\n const thumbGb = (2 * 24 * thumbRetentionDays) / 1024\n totalGb += thumbGb\n\n return { perStream, thumbnails: { estimatedGb: thumbGb }, totalEstimatedGb: totalGb, motionEstimate }\n }\n}\n","import type { IScopedLogger, IEventBus, SystemEvent, IStreamingEngine, IPipelineManager, INetworkQualityTracker, IFileStorage, FfmpegConfig } from '@camstack/types'\nimport { resolveFfmpegConfig } from './ffmpeg-config'\nimport type { RecordingDb } from './recording-db'\nimport type {\n RecordingPolicy, RecordingEnableConfig, ScheduleRule,\n SegmentWriterState,\n} from './types'\nimport { SegmentWriter, type SegmentWriterConfig } from './segment-writer'\nimport { ThumbnailExtractor, type ThumbnailExtractorConfig } from './thumbnail-extractor'\nimport { RetentionManager } from './retention-manager'\nimport { PlaylistGenerator } from './playlist-generator'\nimport { StorageEstimator } from './storage-estimator'\n\n// --- Per-device recording state ---\n\ninterface DeviceRecordingState {\n readonly deviceId: string\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 readonly fileStorage: IFileStorage\n readonly storagePath: string\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 fileStorage: IFileStorage\n private readonly storagePath: string\n private readonly globalFfmpegConfig: Partial<FfmpegConfig>\n private readonly detectedFfmpegConfig: Partial<FfmpegConfig>\n private readonly segmentDurationSec: number\n\n private readonly recordings = new Map<string, 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.fileStorage = config.fileStorage\n this.storagePath = config.storagePath\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.fileStorage,\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 for ${policy.deviceId}: ${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: string, 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 any)\n const storageName = storageConfig?.storageName ?? 'recordings'\n const subDirectory = storageConfig?.subDirectory ?? `recordings/${sp.streamId}`\n\n const writerConfig: SegmentWriterConfig = {\n deviceId,\n streamId: sp.streamId,\n segmentDurationSec: this.segmentDurationSec,\n storagePath: this.storagePath,\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 thumbConfig: ThumbnailExtractorConfig = {\n deviceId,\n storagePath: this.storagePath,\n storageName: thumbStorageConfig?.storageName ?? 'recordings',\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 for ${deviceId} within ${MOTION_FALLBACK_TIMEOUT_MS / 1000}s — falling back to continuous recording`)\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: 'recording.policy.fallback',\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 for ${deviceId} during fallback: ${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: 'recording.started',\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 for ${deviceId}`, { mode: policy.mode })\n }\n\n async disableRecording(deviceId: string): Promise<void> {\n const state = this.recordings.get(deviceId)\n if (!state) {\n this.logger.warn(`No active recording for ${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: 'recording.stopped',\n data: {\n deviceId,\n segmentCount: totalSegmentCount,\n totalMB,\n },\n })\n\n this.logger.info(`Recording disabled for ${deviceId}`, { segmentCount: totalSegmentCount, totalMB })\n }\n\n isRecording(deviceId: string): boolean {\n return this.recordings.has(deviceId)\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: string, 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: string, 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 for ${deviceId}: ${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: string): 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","import type { IDecoderProvider } from '@camstack/types'\n\ninterface RegisteredProvider {\n readonly provider: IDecoderProvider\n readonly priority: number\n}\n\nexport class DecoderRegistry {\n private readonly entries: RegisteredProvider[] = []\n\n register(provider: IDecoderProvider, priority: number): void {\n this.entries.push({ provider, priority })\n this.entries.sort((a, b) => a.priority - b.priority)\n }\n\n getDecoder(codec: string): IDecoderProvider | undefined {\n return this.entries.find((e) => e.provider.supportsCodec(codec))?.provider\n }\n}\n","import { spawn, type ChildProcess } from 'node:child_process'\nimport type { IDecoderSession, DecoderSessionConfig, DecoderStats, EncodedPacket, DecodedFrame, Unsubscribe } from '@camstack/types'\nimport { FrameDropper } from './frame-dropper'\n\nconst SOI = Buffer.from([0xff, 0xd8])\nconst EOI = Buffer.from([0xff, 0xd9])\n\nfunction codecToInputFormat(codec: string): string {\n switch (codec) {\n case 'h265':\n case 'hevc':\n return 'hevc'\n case 'mjpeg':\n return 'mjpeg'\n case 'h264':\n default:\n return 'h264'\n }\n}\n\nfunction buildFfmpegArgs(config: DecoderSessionConfig): string[] {\n const inputFormat = codecToInputFormat(config.codec)\n const args = ['-hide_banner', '-loglevel', 'error', '-f', inputFormat, '-i', 'pipe:0']\n\n if (config.scale > 1) {\n args.push('-vf', `scale=iw/${config.scale}:ih/${config.scale}`)\n }\n\n args.push('-f', 'image2pipe', '-vcodec', 'mjpeg', '-q:v', '3', 'pipe:1')\n return args\n}\n\nexport class FfmpegDecoderSession implements IDecoderSession {\n private config: DecoderSessionConfig\n private frameDropper: FrameDropper\n private process: ChildProcess | null = null\n private frameCallbacks = new Set<(frame: DecodedFrame) => void>()\n private outputBuffer = Buffer.alloc(0)\n private destroyed = false\n\n // Stats tracking\n private inputPackets = 0\n private outputFrames = 0\n private droppedFrames = 0\n private totalDecodeTimeMs = 0\n private decodeCount = 0\n private startTime = Date.now()\n\n constructor(config: DecoderSessionConfig) {\n this.config = { ...config }\n this.frameDropper = new FrameDropper(config.maxFps)\n this.spawnFfmpeg()\n }\n\n private spawnFfmpeg(): void {\n if (this.destroyed) return\n\n this.killFfmpeg()\n this.outputBuffer = Buffer.alloc(0)\n\n const args = buildFfmpegArgs(this.config)\n this.process = spawn('ffmpeg', args)\n\n // Swallow EPIPE errors on stdin during shutdown\n this.process.stdin?.on('error', () => {})\n\n this.process.stdout?.on('data', (chunk: Buffer) => {\n this.handleOutputData(chunk)\n })\n\n this.process.stderr?.on('data', (data: Buffer) => {\n // ffmpeg logs errors to stderr; swallow in production, could log here if needed\n void data\n })\n\n this.process.on('error', (_err: Error) => {\n // process failed to spawn or crashed; could handle reconnect logic here\n })\n\n this.process.on('close', () => {\n if (!this.destroyed) {\n this.process = null\n }\n })\n }\n\n private killFfmpeg(): void {\n if (this.process) {\n try {\n this.process.kill('SIGKILL')\n } catch {\n // already dead\n }\n this.process = null\n }\n }\n\n private handleOutputData(chunk: Buffer): void {\n this.outputBuffer = Buffer.concat([this.outputBuffer, chunk])\n\n // Extract complete JPEG frames from the buffer\n let searchFrom = 0\n while (true) {\n const soiIndex = this.outputBuffer.indexOf(SOI, searchFrom)\n if (soiIndex === -1) break\n\n const eoiIndex = this.outputBuffer.indexOf(EOI, soiIndex + 2)\n if (eoiIndex === -1) break\n\n const frameEnd = eoiIndex + 2\n const jpegData = this.outputBuffer.slice(soiIndex, frameEnd)\n\n // Advance past the consumed frame\n searchFrom = frameEnd\n\n this.emitFrame(Buffer.from(jpegData))\n }\n\n // Keep only unprocessed tail\n if (searchFrom > 0) {\n this.outputBuffer = Buffer.from(this.outputBuffer.slice(searchFrom))\n }\n }\n\n private emitFrame(data: Buffer): void {\n const decodeStart = Date.now()\n\n if (!this.frameDropper.shouldKeep()) {\n this.droppedFrames++\n return\n }\n\n const decodeTime = Date.now() - decodeStart\n this.totalDecodeTimeMs += decodeTime\n this.decodeCount++\n this.outputFrames++\n\n const frame: DecodedFrame = {\n data,\n width: 0,\n height: 0,\n format: 'jpeg',\n timestamp: Date.now(),\n }\n\n for (const cb of this.frameCallbacks) {\n cb(frame)\n }\n }\n\n pushPacket(packet: EncodedPacket): void {\n if (this.destroyed || !this.process?.stdin) return\n this.inputPackets++\n try {\n this.process.stdin.write(packet.data)\n } catch {\n // stdin may be closed if ffmpeg crashed\n }\n }\n\n onFrame(callback: (frame: DecodedFrame) => void): Unsubscribe {\n this.frameCallbacks.add(callback)\n return () => {\n this.frameCallbacks.delete(callback)\n }\n }\n\n updateConfig(update: Partial<DecoderSessionConfig>): void {\n const needsRestart =\n (update.scale !== undefined && update.scale !== this.config.scale) ||\n (update.outputFormat !== undefined && update.outputFormat !== this.config.outputFormat) ||\n (update.codec !== undefined && update.codec !== this.config.codec)\n\n this.config = { ...this.config, ...update }\n\n if (update.maxFps !== undefined) {\n this.frameDropper.setMaxFps(update.maxFps)\n }\n\n if (needsRestart) {\n this.spawnFfmpeg()\n }\n }\n\n async destroy(): Promise<void> {\n if (this.destroyed) return\n this.destroyed = true\n this.killFfmpeg()\n this.frameCallbacks.clear()\n }\n\n getStats(): DecoderStats {\n const uptimeSec = Math.max((Date.now() - this.startTime) / 1000, 1)\n return {\n inputFps: this.inputPackets / uptimeSec,\n outputFps: this.outputFrames / uptimeSec,\n avgDecodeTimeMs: this.decodeCount > 0 ? this.totalDecodeTimeMs / this.decodeCount : 0,\n droppedFrames: this.droppedFrames,\n }\n }\n}\n","export class FrameDropper {\n private intervalMs: number\n private lastPassedAt = -Infinity\n\n constructor(maxFps: number) {\n this.intervalMs = maxFps > 0 ? 1000 / maxFps : 0\n }\n\n shouldKeep(): boolean {\n if (this.intervalMs === 0) return true\n\n const now = Date.now()\n if (now - this.lastPassedAt >= this.intervalMs) {\n this.lastPassedAt = now\n return true\n }\n return false\n }\n\n setMaxFps(maxFps: number): void {\n this.intervalMs = maxFps > 0 ? 1000 / maxFps : 0\n }\n}\n","import type { IDecoderProvider, DecoderSessionConfig, IDecoderSession } from '@camstack/types'\nimport { FfmpegDecoderSession } from './ffmpeg-decoder-session'\n\nconst SUPPORTED_CODECS = new Set(['h264', 'h265', 'hevc', 'mjpeg'])\n\nexport class FfmpegDecoderProvider implements IDecoderProvider {\n readonly id = 'ffmpeg'\n readonly name = 'FFmpeg Decoder'\n\n supportsCodec(codec: string): boolean {\n return SUPPORTED_CODECS.has(codec)\n }\n\n createSession(config: DecoderSessionConfig): IDecoderSession {\n return new FfmpegDecoderSession(config)\n }\n}\n","import { spawn, type ChildProcess } from 'node:child_process'\nimport type {\n IStreamBroker,\n StreamSource,\n EncodedPacket,\n DecodedFrame,\n DecodedAudioChunk,\n DecodeOptions,\n BrokerStatus,\n BrokerStats,\n Unsubscribe,\n IDecoderSession,\n IRestreamer,\n IScopedLogger,\n} from '@camstack/types'\nimport type { DecoderRegistry } from './decoder-registry'\nimport { FrameDropper } from './frame-dropper'\nimport { StreamPipeServer } from './stream-pipe-server'\n\nconst DEFAULT_MAX_FPS = 5\nconst DEFAULT_FORMAT = 'jpeg' as const\nconst DEFAULT_SCALE = 1\n\nconst INITIAL_RECONNECT_DELAY_MS = 1_000\nconst MAX_RECONNECT_DELAY_MS = 30_000\nconst DECODER_TEARDOWN_GRACE_MS = 2_000\n\n/** H.264 NAL unit type 5 = IDR frame (keyframe) */\nconst H264_IDR_NAL_TYPE = 5\n/** H.265 NAL unit types 16-21 are IRAP (keyframe) pictures */\nconst H265_IRAP_NAL_TYPE_MIN = 16\nconst H265_IRAP_NAL_TYPE_MAX = 21\n\ninterface DecodedSubscriber {\n readonly callback: (frame: DecodedFrame) => void\n readonly frameDropper: FrameDropper\n}\n\nexport class StreamBroker implements IStreamBroker {\n readonly deviceId: string\n\n private _status: BrokerStatus = 'idle'\n private source: StreamSource | undefined\n private decoderSession: IDecoderSession | null = null\n private decoderUnsubscribe: Unsubscribe | null = null\n private readonly decoderRegistry: DecoderRegistry\n private readonly logger: IScopedLogger | undefined\n private readonly startedAt: number = Date.now()\n\n private ffmpegProcess: ChildProcess | undefined\n private reconnectTimer: ReturnType<typeof setTimeout> | undefined\n private reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS\n private manualStop = false\n private stopping = false\n private decoderTeardownTimer: ReturnType<typeof setTimeout> | undefined\n\n private restreamers: readonly IRestreamer[] = []\n private readonly pipeServer: StreamPipeServer\n\n private readonly encodedCallbacks = new Set<(packet: EncodedPacket) => void>()\n private readonly decodedSubscribers = new Map<symbol, DecodedSubscriber>()\n private readonly audioChunkCallbacks = new Set<(chunk: DecodedAudioChunk) => void>()\n\n constructor(deviceId: string, decoderRegistry: DecoderRegistry, logger?: IScopedLogger) {\n this.deviceId = deviceId\n this.decoderRegistry = decoderRegistry\n this.logger = logger\n this.pipeServer = new StreamPipeServer(logger?.child('pipe-server'))\n }\n\n get status(): BrokerStatus {\n return this._status\n }\n\n setRestreamers(restreamers: readonly IRestreamer[]): void {\n this.restreamers = restreamers\n }\n\n async start(source: StreamSource): Promise<void> {\n this.source = source\n this.manualStop = false\n this.stopping = false\n this._status = 'connecting'\n\n await this.pipeServer.start()\n this.logger?.info(\n `Pipe server started for ${this.deviceId} at ${this.pipeServer.getUrl()}`,\n )\n\n if (source.type === 'rtsp') {\n this.startRtspReader(source)\n } else {\n // For non-RTSP sources (raw TCP, pipe), data is pushed externally\n this._status = 'streaming'\n }\n }\n\n async stop(): Promise<void> {\n this.stopping = true\n this.manualStop = true\n this._status = 'stopped'\n\n this.killFfmpeg()\n\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer)\n this.reconnectTimer = undefined\n }\n\n if (this.decoderTeardownTimer) {\n clearTimeout(this.decoderTeardownTimer)\n this.decoderTeardownTimer = undefined\n }\n\n if (this.decoderSession) {\n await this.destroyDecoder()\n }\n\n await this.pipeServer.stop()\n\n this.encodedCallbacks.clear()\n this.decodedSubscribers.clear()\n this.audioChunkCallbacks.clear()\n }\n\n pushEncodedPacket(packet: EncodedPacket): void {\n if (this.stopping) return\n\n for (const cb of this.encodedCallbacks) {\n cb(packet)\n }\n\n // Broadcast raw video bytes to TCP pipe clients (restreamers)\n if (packet.type === 'video') {\n this.pipeServer.broadcast(packet.data)\n }\n\n for (const restreamer of this.restreamers) {\n const streamId = `${this.deviceId}/video`\n restreamer.pushPacket(streamId, packet)\n }\n\n if (packet.type === 'video' && this.decoderSession) {\n this.decoderSession.pushPacket(packet)\n }\n }\n\n onEncodedData(callback: (packet: EncodedPacket) => void): Unsubscribe {\n this.encodedCallbacks.add(callback)\n\n return () => {\n this.encodedCallbacks.delete(callback)\n }\n }\n\n onDecodedFrame(callback: (frame: DecodedFrame) => void, options?: DecodeOptions): Unsubscribe {\n const maxFps = options?.maxFps ?? DEFAULT_MAX_FPS\n const subscriberId = Symbol('decoded-subscriber')\n const frameDropper = new FrameDropper(maxFps)\n\n const subscriber: DecodedSubscriber = { callback, frameDropper }\n this.decodedSubscribers.set(subscriberId, subscriber)\n\n // Cancel pending teardown if a new subscriber joins while grace period is active\n if (this.decoderTeardownTimer) {\n clearTimeout(this.decoderTeardownTimer)\n this.decoderTeardownTimer = undefined\n }\n\n // Create shared decoder on first subscriber (or reuse existing)\n if (!this.decoderSession && this.source) {\n this.createSharedDecoderSession(options)\n }\n\n return () => {\n this.decodedSubscribers.delete(subscriberId)\n\n // Schedule decoder teardown with grace period when last subscriber leaves\n if (this.decodedSubscribers.size === 0 && this.decoderSession) {\n this.decoderTeardownTimer = setTimeout(() => {\n this.decoderTeardownTimer = undefined\n if (this.decodedSubscribers.size === 0 && this.decoderSession) {\n this.destroyDecoder()\n }\n }, DECODER_TEARDOWN_GRACE_MS)\n }\n }\n }\n\n onDecodedAudioChunk(callback: (chunk: DecodedAudioChunk) => void): Unsubscribe {\n this.audioChunkCallbacks.add(callback)\n\n return () => {\n this.audioChunkCallbacks.delete(callback)\n }\n }\n\n getStats(): BrokerStats {\n const decoderStats = this.decoderSession?.getStats()\n\n return {\n status: this._status,\n inputFps: decoderStats?.inputFps ?? 0,\n decodeFps: decoderStats?.outputFps ?? 0,\n encodedSubscribers: this.encodedCallbacks.size,\n decodedSubscribers: this.decodedSubscribers.size,\n uptimeMs: Date.now() - this.startedAt,\n }\n }\n\n /**\n * Returns the local TCP URL that restreamers should connect to\n * instead of the camera's RTSP URL. This ensures the broker is\n * the sole reader from the camera.\n */\n getLocalStreamUrl(): string {\n return this.pipeServer.getUrl()\n }\n\n /** Returns the number of TCP pipe clients currently connected */\n getPipeClientCount(): number {\n return this.pipeServer.getClientCount()\n }\n\n private startRtspReader(source: StreamSource): void {\n const codec = source.videoCodec ?? 'h264'\n const isHevc = codec === 'h265' || codec === 'hevc'\n\n const args = [\n '-hide_banner',\n '-loglevel', 'error',\n '-rtsp_transport', 'tcp',\n '-i', source.url,\n '-c:v', 'copy',\n '-f', isHevc ? 'hevc' : 'h264',\n '-bsf:v', isHevc ? 'hevc_mp4toannexb' : 'h264_mp4toannexb',\n 'pipe:1',\n ]\n\n this.logger?.debug(`Spawning ffmpeg RTSP reader for ${this.deviceId}`, {\n url: source.url,\n codec,\n })\n\n const proc = spawn('ffmpeg', args, {\n stdio: ['pipe', 'pipe', 'pipe'],\n })\n this.ffmpegProcess = proc\n\n // Swallow EPIPE errors on stdin during shutdown\n proc.stdin?.on('error', () => {})\n\n proc.stdout?.on('data', (chunk: Buffer) => {\n if (this.stopping) return\n\n this._status = 'streaming'\n this.reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS\n\n const keyframe = isHevc\n ? isHevcKeyframe(chunk)\n : isH264Keyframe(chunk)\n\n const packet: EncodedPacket = {\n type: 'video',\n data: chunk,\n pts: Date.now(),\n dts: Date.now(),\n keyframe,\n codec,\n }\n\n this.pushEncodedPacket(packet)\n })\n\n proc.stderr?.on('data', (data: Buffer) => {\n const msg = data.toString().trim()\n if (msg) {\n this.logger?.warn(`ffmpeg RTSP reader stderr [${this.deviceId}]: ${msg}`)\n }\n })\n\n proc.on('exit', (code, signal) => {\n this.ffmpegProcess = undefined\n\n if (this.manualStop) {\n return\n }\n\n this.logger?.warn(`ffmpeg RTSP reader exited for ${this.deviceId}`, {\n code,\n signal,\n })\n\n this._status = 'error'\n this.scheduleReconnect()\n })\n\n proc.on('error', (err) => {\n this.ffmpegProcess = undefined\n\n if (this.manualStop) {\n return\n }\n\n this.logger?.error(`ffmpeg RTSP reader error for ${this.deviceId}: ${err.message}`)\n this._status = 'error'\n this.scheduleReconnect()\n })\n }\n\n private scheduleReconnect(): void {\n if (this.manualStop || !this.source) {\n return\n }\n\n this.logger?.info(\n `Reconnecting RTSP reader for ${this.deviceId} in ${this.reconnectDelayMs}ms`,\n )\n\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = undefined\n if (!this.manualStop && this.source) {\n this._status = 'connecting'\n this.startRtspReader(this.source)\n }\n }, this.reconnectDelayMs)\n\n this.reconnectDelayMs = Math.min(\n this.reconnectDelayMs * 2,\n MAX_RECONNECT_DELAY_MS,\n )\n }\n\n private destroyDecoder(): Promise<void> {\n if (this.decoderUnsubscribe) {\n this.decoderUnsubscribe()\n this.decoderUnsubscribe = null\n }\n const session = this.decoderSession\n this.decoderSession = null\n return session?.destroy() ?? Promise.resolve()\n }\n\n private killFfmpeg(): void {\n if (this.ffmpegProcess) {\n this.ffmpegProcess.kill('SIGTERM')\n this.ffmpegProcess = undefined\n }\n }\n\n private createSharedDecoderSession(options?: DecodeOptions): void {\n const codec = this.source?.videoCodec ?? 'h264'\n const provider = this.decoderRegistry.getDecoder(codec)\n\n if (!provider) {\n return\n }\n\n // Shared decoder runs at unlimited FPS — each subscriber throttles locally\n const config = {\n codec,\n maxFps: 0,\n outputFormat: options?.format ?? DEFAULT_FORMAT,\n scale: options?.scale ?? DEFAULT_SCALE,\n }\n\n this.decoderSession = provider.createSession(config)\n\n // Single onFrame listener that fans out to all subscribers with per-subscriber throttling\n this.decoderUnsubscribe = this.decoderSession.onFrame((frame: DecodedFrame) => {\n for (const subscriber of this.decodedSubscribers.values()) {\n if (subscriber.frameDropper.shouldKeep()) {\n subscriber.callback(frame)\n }\n }\n })\n }\n}\n\n/**\n * Detect H.264 keyframe by scanning for IDR NAL unit (type 5)\n * in an Annex-B bitstream chunk.\n */\nfunction isH264Keyframe(data: Buffer): boolean {\n for (let i = 0; i < data.length - 4; i++) {\n // Look for start code: 0x00 0x00 0x01 or 0x00 0x00 0x00 0x01\n if (data[i] === 0 && data[i + 1] === 0) {\n let nalStart = -1\n if (data[i + 2] === 1) {\n nalStart = i + 3\n } else if (data[i + 2] === 0 && data[i + 3] === 1) {\n nalStart = i + 4\n }\n if (nalStart >= 0 && nalStart < data.length) {\n const nalType = data[nalStart]! & 0x1f\n if (nalType === H264_IDR_NAL_TYPE) {\n return true\n }\n }\n }\n }\n return false\n}\n\n/**\n * Detect H.265/HEVC keyframe by scanning for IRAP NAL units (types 16-21)\n * in an Annex-B bitstream chunk.\n */\nfunction isHevcKeyframe(data: Buffer): boolean {\n for (let i = 0; i < data.length - 5; i++) {\n if (data[i] === 0 && data[i + 1] === 0) {\n let nalStart = -1\n if (data[i + 2] === 1) {\n nalStart = i + 3\n } else if (data[i + 2] === 0 && data[i + 3] === 1) {\n nalStart = i + 4\n }\n if (nalStart >= 0 && nalStart < data.length) {\n const nalType = (data[nalStart]! >> 1) & 0x3f\n if (nalType >= H265_IRAP_NAL_TYPE_MIN && nalType <= H265_IRAP_NAL_TYPE_MAX) {\n return true\n }\n }\n }\n }\n return false\n}\n","import net from 'node:net'\nimport type { IScopedLogger } from '@camstack/types'\n\n/**\n * A lightweight TCP server that broadcasts raw encoded video bytes\n * (H.264/H.265 Annex-B) to all connected clients.\n *\n * Each StreamBroker owns one StreamPipeServer. Restreamers (e.g. go2rtc)\n * connect via `tcp://127.0.0.1:{port}` and receive the same byte stream\n * that the broker reads from the camera, eliminating duplicate RTSP\n * connections.\n */\nexport class StreamPipeServer {\n private readonly server: net.Server\n private readonly clients: Set<net.Socket> = new Set()\n private port = 0\n private started = false\n private readonly logger: IScopedLogger | undefined\n\n constructor(logger?: IScopedLogger) {\n this.logger = logger\n this.server = net.createServer((socket) => {\n this.handleConnection(socket)\n })\n\n this.server.on('error', (err) => {\n this.logger?.error(`StreamPipeServer error: ${err.message}`)\n })\n }\n\n async start(): Promise<void> {\n if (this.started) {\n return\n }\n\n await new Promise<void>((resolve, reject) => {\n this.server.once('error', reject)\n this.server.listen(0, '127.0.0.1', () => {\n this.server.removeListener('error', reject)\n const address = this.server.address() as net.AddressInfo\n this.port = address.port\n this.started = true\n this.logger?.debug(`StreamPipeServer listening on port ${this.port}`)\n resolve()\n })\n })\n }\n\n /**\n * Broadcast raw encoded bytes to all connected clients.\n * Silently drops data for clients that are slow or disconnected.\n */\n broadcast(data: Buffer): void {\n for (const client of this.clients) {\n if (client.destroyed) {\n this.clients.delete(client)\n continue\n }\n\n client.write(data, (err) => {\n if (err) {\n this.logger?.debug(\n `StreamPipeServer write error, removing client: ${err.message}`,\n )\n this.removeClient(client)\n }\n })\n }\n }\n\n getPort(): number {\n return this.port\n }\n\n getUrl(): string {\n return `tcp://127.0.0.1:${this.port}`\n }\n\n getClientCount(): number {\n return this.clients.size\n }\n\n isStarted(): boolean {\n return this.started\n }\n\n async stop(): Promise<void> {\n if (!this.started) {\n return\n }\n\n this.started = false\n\n for (const client of this.clients) {\n client.destroy()\n }\n this.clients.clear()\n\n await new Promise<void>((resolve) => {\n this.server.close(() => {\n this.logger?.debug('StreamPipeServer stopped')\n resolve()\n })\n })\n }\n\n private handleConnection(socket: net.Socket): void {\n this.clients.add(socket)\n this.logger?.debug(\n `StreamPipeServer client connected (total: ${this.clients.size})`,\n )\n\n socket.on('close', () => {\n this.removeClient(socket)\n })\n\n socket.on('error', () => {\n this.removeClient(socket)\n })\n }\n\n private removeClient(socket: net.Socket): void {\n const existed = this.clients.delete(socket)\n if (existed) {\n this.logger?.debug(\n `StreamPipeServer client disconnected (total: ${this.clients.size})`,\n )\n }\n if (!socket.destroyed) {\n socket.destroy()\n }\n }\n}\n","import type { StreamSource, IRestreamer, IScopedLogger } from '@camstack/types'\nimport { StreamBroker } from './stream-broker'\nimport type { DecoderRegistry } from './decoder-registry'\n\nexport class StreamBrokerManager {\n private readonly brokers = new Map<string, StreamBroker>()\n private restreamers: readonly IRestreamer[] = []\n private readonly logger: IScopedLogger\n\n constructor(\n private readonly decoderRegistry: DecoderRegistry,\n logger: IScopedLogger,\n ) {\n this.logger = logger\n }\n\n setRestreamers(restreamers: readonly IRestreamer[]): void {\n this.restreamers = restreamers\n\n for (const broker of this.brokers.values()) {\n broker.setRestreamers(restreamers)\n }\n\n this.logger.info(`Updated restreamers (count: ${restreamers.length})`)\n }\n\n getRestreamers(): readonly IRestreamer[] {\n return this.restreamers\n }\n\n /**\n * Create and start a broker.\n * brokerId can be a simple deviceId (backward compat) or a compound key like `${deviceId}/${streamId}`.\n */\n async createBroker(brokerId: string, source: StreamSource): Promise<StreamBroker> {\n const brokerLogger = this.logger.child(`broker:${brokerId}`)\n const broker = new StreamBroker(brokerId, this.decoderRegistry, brokerLogger)\n broker.setRestreamers(this.restreamers)\n await broker.start(source)\n this.brokers.set(brokerId, broker)\n return broker\n }\n\n getBroker(brokerId: string): StreamBroker | undefined {\n return this.brokers.get(brokerId)\n }\n\n async destroyBroker(brokerId: string): Promise<void> {\n const broker = this.brokers.get(brokerId)\n\n if (broker) {\n await broker.stop()\n this.brokers.delete(brokerId)\n }\n }\n\n /** Destroy all brokers matching a prefix (e.g., `${deviceId}/` to destroy all streams for a device) */\n async destroyBrokersForDevice(deviceId: string): Promise<void> {\n const prefix = `${deviceId}/`\n const toDestroy: string[] = []\n\n for (const brokerId of this.brokers.keys()) {\n if (brokerId === deviceId || brokerId.startsWith(prefix)) {\n toDestroy.push(brokerId)\n }\n }\n\n await Promise.all(toDestroy.map((id) => this.destroyBroker(id)))\n }\n\n listBrokers(): readonly StreamBroker[] {\n return [...this.brokers.values()]\n }\n\n async destroyAll(): Promise<void> {\n const stopPromises = [...this.brokers.values()].map((broker) => broker.stop())\n await Promise.all(stopPromises)\n this.brokers.clear()\n }\n}\n","/**\n * Event Persistence Service — unified persistence for detection events.\n *\n * A detection event is ONE entity with:\n * - Metadata (DB): id, timestamp, device, class, score, severity, description\n * - Media (filesystem): original frame, annotated frame, crops, faces, plates\n *\n * Both are stored together, flushed together, and deleted together by retention.\n *\n * Buffered I/O prevents filesystem flooding:\n * - Events buffered, flushed every 5s or at 50 events\n * - Media buffered, flushed every 3s or at 50MB\n * - High-priority media (thumbnails) flushed first\n */\n\nimport type { IScopedLogger, IStorageLocation, SystemEvent, VideoFrame } from '@camstack/types'\n\n/**\n * Inlined from the removed stages/object-snapshot-stage.ts.\n * These types are used by the persistence layer only.\n */\nexport interface ObjectSnapshotResult {\n readonly trackId: string\n readonly thumbnail?: Buffer\n readonly fullCrop?: Buffer\n}\n\nexport interface AnnotatedSnapshotResult {\n readonly thumbnail: Buffer\n readonly full: Buffer\n}\n\n// --- Types ---\n\nexport interface PersistableEvent {\n readonly id: string\n readonly timestamp: number\n readonly deviceId: string\n readonly category: string\n readonly className: string\n readonly score: number\n readonly trackId: string\n readonly severity: string\n readonly description: string\n readonly data: Record<string, unknown>\n /** IDs of associated media files (for cleanup) */\n readonly mediaFiles: readonly string[]\n}\n\ninterface MediaEntry {\n readonly path: string\n readonly data: Buffer\n readonly priority: 'high' | 'normal'\n readonly eventId: string\n}\n\nexport interface EventBufferStatus {\n readonly eventCount: number\n readonly mediaCount: number\n readonly mediaSizeMB: number\n}\n\nexport type TrackMediaType =\n | 'crop-thumb'\n | 'crop-full'\n | 'debug-annotated-thumb'\n | 'debug-annotated-full'\n | 'original'\n | 'original-thumb'\n | 'inline-crop'\n | 'unknown'\n\nexport interface TrackMediaFile {\n readonly path: string\n readonly type: TrackMediaType\n readonly data: Buffer\n /** Where the data was read from */\n readonly source: 'buffer' | 'storage'\n}\n\nfunction classifyMediaPath(path: string): TrackMediaType {\n if (path.endsWith('-thumb.jpg') && path.includes('/crops/')) return 'crop-thumb'\n if (path.endsWith('-full.jpg') && path.includes('/crops/')) return 'crop-full'\n if (path.includes('debug-annotated-thumb.jpg')) return 'debug-annotated-thumb'\n if (path.includes('debug-annotated-full.jpg')) return 'debug-annotated-full'\n if (path.includes('original-thumb.jpg')) return 'original-thumb'\n if (path.includes('original.jpg')) return 'original'\n if (path.includes('crop.jpg')) return 'inline-crop'\n return 'unknown'\n}\n\n// --- Dependencies ---\n\nexport interface EventPersistenceDeps {\n readonly getStorageLocation: (name: string) => IStorageLocation\n readonly subscribe: (filter: { category?: string }, handler: (event: SystemEvent) => void) => () => void\n readonly logger: IScopedLogger\n}\n\n// --- Service ---\n\nexport class EventPersistenceService {\n private eventBuffer: PersistableEvent[] = []\n private mediaBuffer: MediaEntry[] = []\n\n private eventFlushTimer?: ReturnType<typeof setInterval>\n private mediaFlushTimer?: ReturnType<typeof setInterval>\n\n private readonly EVENT_FLUSH_INTERVAL_MS = 5000\n private readonly MEDIA_FLUSH_INTERVAL_MS = 3000\n private readonly MAX_EVENT_BUFFER = 50\n private readonly MAX_MEDIA_BUFFER_MB = 50\n\n private readonly logger: IScopedLogger\n private readonly getStorageLocation: (name: string) => IStorageLocation\n private unsubscribe?: () => void\n\n constructor(private readonly deps: EventPersistenceDeps) {\n this.logger = deps.logger\n this.getStorageLocation = deps.getStorageLocation\n }\n\n start(): void {\n this.unsubscribe = this.deps.subscribe(\n { category: 'detection.event' },\n (event) => this.bufferEvent(event),\n )\n\n this.eventFlushTimer = setInterval(() => {\n this.flushEvents().catch(e => this.logger.warn(`Event flush failed: ${e}`))\n }, this.EVENT_FLUSH_INTERVAL_MS)\n\n this.mediaFlushTimer = setInterval(() => {\n this.flushMedia().catch(e => this.logger.warn(`Media flush failed: ${e}`))\n }, this.MEDIA_FLUSH_INTERVAL_MS)\n }\n\n stop(): void {\n this.unsubscribe?.()\n clearInterval(this.eventFlushTimer)\n clearInterval(this.mediaFlushTimer)\n this.flushEvents().catch(() => {})\n this.flushMedia().catch(() => {})\n }\n\n // --- Public: save media associated with an event ---\n\n /** Save object crops (thumbnail + full-res) for an event */\n saveDetectionCrops(eventId: string, crops: readonly ObjectSnapshotResult[]): void {\n for (const crop of crops) {\n if (crop.thumbnail) {\n this.bufferMedia(eventId, `events/${eventId}/crops/${crop.trackId}-thumb.jpg`, crop.thumbnail, 'high')\n }\n if (crop.fullCrop) {\n this.bufferMedia(eventId, `events/${eventId}/crops/${crop.trackId}-full.jpg`, crop.fullCrop, 'normal')\n }\n }\n }\n\n /** Save annotated frame (thumbnail + full-res) for an event */\n saveAnnotatedFrame(eventId: string, result: AnnotatedSnapshotResult): void {\n this.bufferMedia(eventId, `events/${eventId}/debug-annotated-thumb.jpg`, result.thumbnail, 'high')\n this.bufferMedia(eventId, `events/${eventId}/debug-annotated-full.jpg`, result.full, 'normal')\n }\n\n /** Save original frame for an event */\n async saveOriginalFrame(eventId: string, frame: VideoFrame): Promise<void> {\n const sharp = (await import('sharp')).default\n\n const jpeg = await sharp(frame.data, {\n raw: { width: frame.width, height: frame.height, channels: 3 },\n }).jpeg({ quality: 85 }).toBuffer()\n\n this.bufferMedia(eventId, `events/${eventId}/original.jpg`, jpeg, 'normal')\n\n const thumb = await sharp(jpeg)\n .resize(320, 240, { fit: 'inside' })\n .jpeg({ quality: 75 })\n .toBuffer()\n\n this.bufferMedia(eventId, `events/${eventId}/original-thumb.jpg`, thumb, 'high')\n }\n\n // --- Public: query track media ---\n\n /**\n * Get all media files for a specific event (from buffer or storage).\n * Returns immediately — serves from in-memory buffer if not yet flushed.\n */\n async getEventMedia(eventId: string): Promise<readonly TrackMediaFile[]> {\n const results: TrackMediaFile[] = []\n\n // 1. Check in-memory buffer first (not yet flushed)\n for (const entry of this.mediaBuffer) {\n if (entry.eventId === eventId) {\n results.push({\n path: entry.path,\n type: classifyMediaPath(entry.path),\n data: entry.data,\n source: 'buffer',\n })\n }\n }\n\n // 2. Check storage (already flushed)\n try {\n const location = this.getStorageLocation('events')\n if (location.files) {\n const files = await location.files.listFiles(`events/${eventId}/`)\n for (const file of files) {\n // Skip if already served from buffer\n if (results.some(r => r.path === file)) continue\n try {\n const data = await location.files.readFile(file)\n results.push({\n path: file,\n type: classifyMediaPath(file),\n data,\n source: 'storage',\n })\n } catch {\n // File may have been deleted by retention\n }\n }\n }\n } catch {\n // Storage not available\n }\n\n return results\n }\n\n /**\n * Get all media for a specific track across all events.\n * Useful for building a track timeline with all snapshots.\n */\n async getTrackMedia(trackId: string): Promise<readonly TrackMediaFile[]> {\n const results: TrackMediaFile[] = []\n\n // 1. Buffer: find media matching trackId in path\n for (const entry of this.mediaBuffer) {\n if (entry.path.includes(trackId)) {\n results.push({\n path: entry.path,\n type: classifyMediaPath(entry.path),\n data: entry.data,\n source: 'buffer',\n })\n }\n }\n\n // 2. Find events for this trackId in buffer\n const bufferedEventIds = this.eventBuffer\n .filter(e => e.trackId === trackId)\n .map(e => e.id)\n\n // 3. Find events for this trackId in storage\n try {\n const location = this.getStorageLocation('events')\n if (location.structured) {\n const stored = await location.structured.query('detection_events', {\n where: { trackId },\n orderBy: { field: 'timestamp', direction: 'asc' },\n limit: 100,\n })\n\n const allEventIds = new Set([\n ...bufferedEventIds,\n ...stored.map(r => r.id),\n ])\n\n // Get media for each event\n for (const eventId of allEventIds) {\n const eventMedia = await this.getEventMedia(eventId)\n for (const media of eventMedia) {\n if (!results.some(r => r.path === media.path)) {\n results.push(media)\n }\n }\n }\n }\n } catch {\n // Storage not available — serve from buffer only\n }\n\n return results\n }\n\n /**\n * Get all media for a device within a time range.\n */\n async getDeviceMedia(\n deviceId: string,\n since?: number,\n until?: number,\n ): Promise<readonly TrackMediaFile[]> {\n const results: TrackMediaFile[] = []\n\n try {\n const location = this.getStorageLocation('events')\n if (!location.structured) return results\n\n const events = await location.structured.query('detection_events', {\n where: { deviceId },\n whereBetween: since || until\n ? { timestamp: [since ?? 0, until ?? Date.now()] }\n : undefined,\n orderBy: { field: 'timestamp', direction: 'desc' },\n limit: 50,\n })\n\n for (const event of events) {\n const media = await this.getEventMedia(event.id)\n results.push(...media)\n }\n } catch {\n // Storage not available\n }\n\n return results\n }\n\n // --- Public: status ---\n\n getBufferStatus(): EventBufferStatus {\n return {\n eventCount: this.eventBuffer.length,\n mediaCount: this.mediaBuffer.length,\n mediaSizeMB: this.mediaBuffer.reduce((sum, m) => sum + m.data.length, 0) / (1024 * 1024),\n }\n }\n\n /** Force flush everything */\n async flush(): Promise<void> {\n await this.flushEvents()\n await this.flushMedia()\n }\n\n // --- Internal: buffering ---\n\n private bufferEvent(event: SystemEvent): void {\n // Collect paths of any media being buffered for this event\n const mediaPaths = this.mediaBuffer\n .filter(m => m.eventId === event.id)\n .map(m => m.path)\n\n const persistable: PersistableEvent = {\n id: event.id,\n timestamp: Date.now(),\n deviceId: (event.data.deviceId as string) ?? '',\n category: event.category,\n className: (event.data.className as string) ?? '',\n score: (event.data.score as number) ?? 0,\n trackId: (event.data.trackId as string) ?? '',\n severity: (event.data.severity as string) ?? '',\n description: (event.data.description as string) ?? '',\n data: event.data,\n mediaFiles: mediaPaths,\n }\n\n this.eventBuffer.push(persistable)\n\n // Buffer inline crop if present\n const detection = event.data.detection as Record<string, unknown> | undefined\n if (detection?.crop && Buffer.isBuffer(detection.crop)) {\n this.bufferMedia(event.id, `events/${event.id}/crop.jpg`, detection.crop as Buffer, 'high')\n }\n\n if (this.eventBuffer.length >= this.MAX_EVENT_BUFFER) {\n this.flushEvents().catch(e => this.logger.warn(`Auto-flush failed: ${e}`))\n }\n }\n\n private bufferMedia(eventId: string, path: string, data: Buffer, priority: 'high' | 'normal'): void {\n this.mediaBuffer.push({ eventId, path, data, priority })\n\n const totalMB = this.mediaBuffer.reduce((sum, m) => sum + m.data.length, 0) / (1024 * 1024)\n if (totalMB >= this.MAX_MEDIA_BUFFER_MB) {\n this.flushMedia().catch(e => this.logger.warn(`Media overflow flush failed: ${e}`))\n }\n }\n\n // --- Internal: flushing ---\n\n private async flushEvents(): Promise<void> {\n if (this.eventBuffer.length === 0) return\n\n const events = [...this.eventBuffer]\n this.eventBuffer = []\n\n try {\n const location = this.getStorageLocation('events')\n if (location.structured) {\n for (const event of events) {\n await location.structured.insert({\n collection: 'detection_events',\n id: event.id,\n data: event as unknown as Record<string, unknown>,\n })\n }\n this.logger.debug(`Flushed ${events.length} events`)\n }\n } catch (err) {\n this.logger.warn(`Failed to flush events: ${err}`)\n if (this.eventBuffer.length < this.MAX_EVENT_BUFFER * 2) {\n this.eventBuffer.push(...events)\n }\n }\n }\n\n private async flushMedia(): Promise<void> {\n if (this.mediaBuffer.length === 0) return\n\n // Sort: high priority first\n const sorted = [...this.mediaBuffer].sort((a, b) => {\n if (a.priority === 'high' && b.priority !== 'high') return -1\n if (a.priority !== 'high' && b.priority === 'high') return 1\n return 0\n })\n this.mediaBuffer = []\n\n try {\n const location = this.getStorageLocation('events')\n if (location.files) {\n for (const entry of sorted) {\n await location.files.writeFile(entry.path, entry.data)\n }\n const totalMB = sorted.reduce((s, e) => s + e.data.length, 0) / (1024 * 1024)\n this.logger.debug(`Flushed ${sorted.length} media files (${totalMB.toFixed(1)}MB)`)\n }\n } catch (err) {\n this.logger.warn(`Failed to flush media: ${err}`)\n }\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport type { IScopedLogger, IStructuredStorage } from '@camstack/types'\n\n/**\n * Inlined from the removed sub-detectors/clip-recognizer.ts.\n * ClipRecognizer interface used by this service for embedding generation.\n */\nexport interface ClipRecognizer {\n getEmbedding(imageBuffer: Buffer): Promise<Float32Array>\n}\n\n/** Cosine similarity between two embedding vectors */\nexport function cosineSimilarity(a: Float32Array, b: Float32Array): number {\n if (a.length !== b.length) return 0\n let dotProduct = 0\n let normA = 0\n let normB = 0\n for (let i = 0; i < a.length; i++) {\n dotProduct += a[i]! * b[i]!\n normA += a[i]! * a[i]!\n normB += b[i]! * b[i]!\n }\n const denom = Math.sqrt(normA) * Math.sqrt(normB)\n return denom === 0 ? 0 : dotProduct / denom\n}\n\nexport interface KnownFaceEntry {\n readonly id: string\n readonly label: string\n readonly group?: string\n readonly embedding: readonly number[]\n readonly cropBase64: string\n readonly createdAt: number\n readonly updatedAt: number\n readonly source?: string\n readonly metadata?: Readonly<Record<string, unknown>>\n}\n\nexport interface KnownFacesDeps {\n readonly getStructuredStorage: () => IStructuredStorage\n readonly logger: IScopedLogger\n}\n\nconst COLLECTION = 'known_faces'\n\nexport class KnownFacesService {\n private readonly logger: IScopedLogger\n private readonly getStructuredStorage: () => IStructuredStorage\n\n constructor(deps: KnownFacesDeps) {\n this.logger = deps.logger\n this.getStructuredStorage = deps.getStructuredStorage\n }\n\n private getStorage(): IStructuredStorage {\n return this.getStructuredStorage()\n }\n\n /** Register a known face */\n async register(entry: KnownFaceEntry): Promise<void> {\n const storage = this.getStorage()\n await storage.insert({\n collection: COLLECTION,\n id: entry.id,\n data: {\n label: entry.label,\n group: entry.group ?? null,\n embedding: JSON.stringify(Array.from(entry.embedding)),\n cropBase64: entry.cropBase64,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n source: entry.source ?? null,\n metadata: entry.metadata ? JSON.stringify(entry.metadata) : null,\n },\n })\n this.logger.info(`Registered known face: ${entry.label} (${entry.id})`)\n }\n\n /** Get all known faces */\n async listAll(): Promise<readonly KnownFaceEntry[]> {\n const storage = this.getStorage()\n const records = await storage.query(COLLECTION)\n return records.map((r) => recordToEntry(r.id, r.data))\n }\n\n /** Delete a known face */\n async delete(id: string): Promise<void> {\n const storage = this.getStorage()\n await storage.delete(COLLECTION, id)\n this.logger.info(`Deleted known face: ${id}`)\n }\n\n /** Update a known face (reassign label or group) */\n async update(id: string, updates: Partial<Pick<KnownFaceEntry, 'label' | 'group'>>): Promise<void> {\n const storage = this.getStorage()\n const data: Record<string, unknown> = { updatedAt: Date.now() }\n if (updates.label !== undefined) data.label = updates.label\n if (updates.group !== undefined) data.group = updates.group\n await storage.update(COLLECTION, id, data)\n this.logger.info(`Updated known face ${id}: ${JSON.stringify(updates)}`)\n }\n\n /** Recalculate embedding for a face (re-run CLIP on the stored crop) */\n async recalculateEmbedding(id: string, clipRecognizer: ClipRecognizer): Promise<void> {\n const storage = this.getStorage()\n const records = await storage.query(COLLECTION, { where: { id } })\n if (records.length === 0) throw new Error(`Known face not found: ${id}`)\n\n const record = records[0]!\n const cropBase64 = record.data.cropBase64 as string\n const cropBuffer = Buffer.from(cropBase64, 'base64')\n\n const embedding = await clipRecognizer.getEmbedding(cropBuffer)\n await storage.update(COLLECTION, id, {\n embedding: JSON.stringify(Array.from(embedding)),\n updatedAt: Date.now(),\n })\n this.logger.info(`Recalculated embedding for known face: ${id}`)\n }\n\n /** Find best match for an embedding */\n async findMatch(\n embedding: Float32Array,\n threshold: number,\n ): Promise<{ readonly entry: KnownFaceEntry; readonly similarity: number } | null> {\n const entries = await this.listAll()\n let bestEntry: KnownFaceEntry | null = null\n let bestSimilarity = -1\n\n for (const entry of entries) {\n const knownEmbedding = new Float32Array(entry.embedding)\n const similarity = cosineSimilarity(embedding, knownEmbedding)\n if (similarity > bestSimilarity) {\n bestSimilarity = similarity\n bestEntry = entry\n }\n }\n\n if (bestEntry && bestSimilarity >= threshold) {\n return { entry: bestEntry, similarity: bestSimilarity }\n }\n\n return null\n }\n\n /** Batch import faces from crops */\n async batchRegister(\n entries: ReadonlyArray<{ readonly label: string; readonly cropBase64: string; readonly group?: string }>,\n clipRecognizer: ClipRecognizer,\n ): Promise<readonly KnownFaceEntry[]> {\n const results: KnownFaceEntry[] = []\n for (const input of entries) {\n const cropBuffer = Buffer.from(input.cropBase64, 'base64')\n const embedding = await clipRecognizer.getEmbedding(cropBuffer)\n const now = Date.now()\n const entry: KnownFaceEntry = {\n id: randomUUID(),\n label: input.label,\n group: input.group,\n embedding: Array.from(embedding),\n cropBase64: input.cropBase64,\n createdAt: now,\n updatedAt: now,\n }\n await this.register(entry)\n results.push(entry)\n }\n this.logger.info(`Batch registered ${results.length} known faces`)\n return results\n }\n}\n\nfunction recordToEntry(id: string, data: Record<string, unknown>): KnownFaceEntry {\n return {\n id,\n label: data.label as string,\n group: (data.group as string) ?? undefined,\n embedding: JSON.parse(data.embedding as string) as number[],\n cropBase64: data.cropBase64 as string,\n createdAt: data.createdAt as number,\n updatedAt: data.updatedAt as number,\n source: (data.source as string) ?? undefined,\n metadata: data.metadata ? JSON.parse(data.metadata as string) : undefined,\n }\n}\n","/**\n * Session Tracker Service — maintains persistent object tracking across\n * multiple analysis pipeline runs, linking tracked objects to their\n * CLIP embeddings for re-identification across cameras.\n */\n\nimport { randomUUID } from 'node:crypto'\nimport type { IScopedLogger, IEventBus, AnalysisContext, ServerTrackedDetection as TrackedDetection } from '@camstack/types'\n\nexport interface SessionTrack {\n readonly trackId: string\n readonly deviceId: string\n readonly className: string\n readonly label?: string\n readonly firstSeen: number\n readonly lastSeen: number\n readonly totalFrames: number\n readonly lastDetection: TrackedDetection\n readonly state: string\n readonly positions: ReadonlyArray<{ readonly x: number; readonly y: number; readonly t: number }>\n readonly embedding?: Float32Array\n readonly globalId?: string\n readonly bestCrop?: Buffer\n}\n\nexport interface GlobalIdentity {\n readonly globalId: string\n readonly embedding: Float32Array\n readonly label?: string\n readonly firstSeen: number\n readonly lastSeen: number\n readonly deviceIds: ReadonlyArray<string>\n}\n\n/** Max positions to keep per track (~5 min at 1fps) */\nconst MAX_POSITIONS = 300\n\n/** Tracks not seen for this many ms are expired */\nconst TRACK_EXPIRY_MS = 30_000\n\nexport interface SessionTrackerDeps {\n readonly eventBus: IEventBus\n readonly logger: IScopedLogger\n}\n\nexport class SessionTrackerService {\n /** Active tracks per device: deviceId -> Map<trackId, SessionTrack> */\n private activeTracks: Map<string, Map<string, SessionTrack>> = new Map()\n\n /** Global re-id pool: globalId -> GlobalIdentity (same person across cameras) */\n private globalPool: Map<string, GlobalIdentity> = new Map()\n\n private readonly logger: IScopedLogger\n private readonly eventBus: IEventBus\n\n constructor(deps: SessionTrackerDeps) {\n this.logger = deps.logger\n this.eventBus = deps.eventBus\n }\n\n /** Update tracks from an analysis result */\n updateFromAnalysis(deviceId: string, ctx: AnalysisContext): void {\n const deviceTracks = this.getDeviceTracks(deviceId)\n const now = ctx.timestamp\n\n for (const td of ctx.trackedDetections) {\n const existing = deviceTracks.get(td.trackId)\n\n if (existing) {\n // Update existing track — create new object (immutable)\n const newPositions = [\n ...existing.positions,\n { x: td.detection.boundingBox?.[0] ?? 0, y: td.detection.boundingBox?.[1] ?? 0, t: now },\n ]\n const trimmedPositions = newPositions.length > MAX_POSITIONS\n ? newPositions.slice(newPositions.length - MAX_POSITIONS)\n : newPositions\n\n const updatedEmbedding = (td.recognitions.length > 0 && td.recognitions[0]?.embedding)\n ? new Float32Array(td.recognitions[0].embedding)\n : existing.embedding\n\n const updated: SessionTrack = {\n ...existing,\n lastSeen: now,\n totalFrames: existing.totalFrames + 1,\n lastDetection: td,\n state: td.tracking.state,\n positions: trimmedPositions,\n embedding: updatedEmbedding,\n }\n deviceTracks.set(td.trackId, updated)\n } else {\n // New track\n const track: SessionTrack = {\n trackId: td.trackId,\n deviceId,\n className: td.detection.className,\n label: td.detection.label,\n firstSeen: now,\n lastSeen: now,\n totalFrames: 1,\n lastDetection: td,\n state: td.tracking.state,\n positions: [{ x: td.detection.boundingBox?.[0] ?? 0, y: td.detection.boundingBox?.[1] ?? 0, t: now }],\n embedding: td.recognitions[0]?.embedding ? new Float32Array(td.recognitions[0].embedding) : undefined,\n globalId: undefined,\n bestCrop: td.crop,\n }\n\n deviceTracks.set(td.trackId, track)\n\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'core', id: 'session-tracker' },\n category: 'session.track.new',\n data: { deviceId, trackId: td.trackId, className: td.detection.className },\n })\n }\n }\n\n // Expire old tracks (not seen for TRACK_EXPIRY_MS)\n for (const [trackId, track] of deviceTracks) {\n if (now - track.lastSeen > TRACK_EXPIRY_MS) {\n deviceTracks.delete(trackId)\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'core', id: 'session-tracker' },\n category: 'session.track.expired',\n data: { deviceId, trackId, className: track.className, duration: now - track.firstSeen },\n })\n }\n }\n }\n\n /** Get all active tracks for a device */\n getActiveTracks(deviceId: string): readonly SessionTrack[] {\n return [...(this.activeTracks.get(deviceId)?.values() ?? [])]\n }\n\n /** Get all active tracks across all devices */\n getAllActiveTracks(): readonly SessionTrack[] {\n const all: SessionTrack[] = []\n for (const tracks of this.activeTracks.values()) {\n all.push(...tracks.values())\n }\n return all\n }\n\n /** Get a specific track */\n getTrack(deviceId: string, trackId: string): SessionTrack | null {\n return this.activeTracks.get(deviceId)?.get(trackId) ?? null\n }\n\n /** Get track count per device */\n getTrackCounts(): Record<string, { total: number; byClass: Record<string, number> }> {\n const counts: Record<string, { total: number; byClass: Record<string, number> }> = {}\n for (const [deviceId, tracks] of this.activeTracks) {\n const byClass: Record<string, number> = {}\n for (const track of tracks.values()) {\n byClass[track.className] = (byClass[track.className] ?? 0) + 1\n }\n counts[deviceId] = { total: tracks.size, byClass }\n }\n return counts\n }\n\n /** Clear all tracks for a device */\n clearDevice(deviceId: string): void {\n this.activeTracks.delete(deviceId)\n }\n\n /** Clear all tracks */\n clearAll(): void {\n this.activeTracks.clear()\n this.globalPool.clear()\n }\n\n /** Get the global identity pool (for diagnostics) */\n getGlobalPool(): ReadonlyMap<string, GlobalIdentity> {\n return this.globalPool\n }\n\n private getDeviceTracks(deviceId: string): Map<string, SessionTrack> {\n if (!this.activeTracks.has(deviceId)) {\n this.activeTracks.set(deviceId, new Map())\n }\n return this.activeTracks.get(deviceId)!\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport type { IScopedLogger, IStorageLocation, IEventBus } from '@camstack/types'\n\n// ---------------------------------------------------------------------------\n// Config & Types\n// ---------------------------------------------------------------------------\n\nexport interface RetentionConfig {\n /** How often to run cleanup (ms). Default: 1 hour */\n readonly cleanupIntervalMs: number\n /** Detection events + associated media retention (days). Default: 30 */\n readonly detectionEventsDays: number\n /** Audio levels retention (days). Default: 7 */\n readonly audioLevelsDays: number\n /** DEPRECATED — snapshots are tied to events, use detectionEventsDays */\n readonly snapshotsDays: number\n /** Per-device overrides */\n readonly deviceOverrides?: Readonly<Record<string, Partial<Pick<RetentionConfig, 'detectionEventsDays' | 'audioLevelsDays' | 'snapshotsDays'>>>>\n}\n\nexport const DEFAULT_RETENTION: RetentionConfig = {\n cleanupIntervalMs: 60 * 60 * 1000, // 1 hour\n detectionEventsDays: 30,\n audioLevelsDays: 7,\n snapshotsDays: 14,\n}\n\nexport interface RetentionReport {\n readonly deletedEvents: number\n readonly deletedAudioRecords: number\n readonly deletedSnapshots: number\n}\n\n// ---------------------------------------------------------------------------\n// Dependencies\n// ---------------------------------------------------------------------------\n\nexport interface RetentionDeps {\n readonly getStorageLocation: (name: string) => IStorageLocation\n readonly getRetentionConfig: () => Partial<RetentionConfig> | undefined\n readonly eventBus: IEventBus\n readonly logger: IScopedLogger\n}\n\n// ---------------------------------------------------------------------------\n// Service\n// ---------------------------------------------------------------------------\n\nexport class RetentionService {\n private cleanupTimer?: ReturnType<typeof setInterval>\n private initialTimer?: ReturnType<typeof setTimeout>\n private config: RetentionConfig\n\n private readonly logger: IScopedLogger\n private readonly getStorageLocation: (name: string) => IStorageLocation\n private readonly eventBus: IEventBus\n\n constructor(private readonly deps: RetentionDeps) {\n this.logger = deps.logger\n this.getStorageLocation = deps.getStorageLocation\n this.eventBus = deps.eventBus\n\n // Load config from app configuration\n const retention = deps.getRetentionConfig()\n this.config = retention\n ? { ...DEFAULT_RETENTION, ...retention }\n : { ...DEFAULT_RETENTION }\n }\n\n start(): void {\n // Run initial cleanup after 5 minutes (let system boot first)\n this.initialTimer = setTimeout(() => {\n this.runCleanup().catch((err) => {\n this.logger.warn(`Initial cleanup failed: ${err}`)\n })\n }, 5 * 60 * 1000)\n\n // Schedule periodic cleanup\n this.cleanupTimer = setInterval(() => {\n this.runCleanup().catch((err) => {\n this.logger.warn(`Periodic cleanup failed: ${err}`)\n })\n }, this.config.cleanupIntervalMs)\n }\n\n stop(): void {\n if (this.initialTimer) {\n clearTimeout(this.initialTimer)\n this.initialTimer = undefined\n }\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer)\n this.cleanupTimer = undefined\n }\n }\n\n /** Run all retention cleanup tasks */\n async runCleanup(): Promise<RetentionReport> {\n const report: RetentionReport = {\n deletedEvents: 0,\n deletedAudioRecords: 0,\n deletedSnapshots: 0,\n }\n\n try {\n // 1. Clean detection events + their associated media files\n const eventCutoff = Date.now() - this.config.detectionEventsDays * 24 * 60 * 60 * 1000\n const { dbDeleted: eventsDeleted, filesDeleted: snapshotsDeleted } =\n await this.cleanEventsWithMedia('detection_events', eventCutoff)\n\n // 2. Clean audio levels (DB only, no media)\n const audioCutoff = Date.now() - this.config.audioLevelsDays * 24 * 60 * 60 * 1000\n const audioDeleted = await this.cleanCollection('audio_levels', audioCutoff)\n\n // Build immutable report\n const finalReport: RetentionReport = {\n deletedEvents: eventsDeleted,\n deletedAudioRecords: audioDeleted,\n deletedSnapshots: snapshotsDeleted,\n }\n\n if (finalReport.deletedEvents > 0 || finalReport.deletedAudioRecords > 0 || finalReport.deletedSnapshots > 0) {\n this.logger.info(\n `Cleanup: ${finalReport.deletedEvents} events, ${finalReport.deletedAudioRecords} audio records, ${finalReport.deletedSnapshots} snapshots`,\n )\n\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'core', id: 'retention' },\n category: 'retention.cleanup',\n data: finalReport as unknown as Record<string, unknown>,\n })\n }\n\n return finalReport\n } catch (err) {\n this.logger.warn(`Retention cleanup failed: ${err}`)\n }\n\n return report\n }\n\n /** Set retention config at runtime */\n setConfig(update: Partial<RetentionConfig>): void {\n this.config = { ...this.config, ...update }\n }\n\n /** Get current retention config */\n getConfig(): RetentionConfig {\n return { ...this.config }\n }\n\n /** Force immediate cleanup */\n async forceCleanup(): Promise<RetentionReport> {\n return this.runCleanup()\n }\n\n /**\n * Delete old events AND their associated media files.\n * Media is stored at events/{eventId}/ — delete the entire directory.\n */\n private async cleanEventsWithMedia(\n collection: string,\n cutoffTimestamp: number,\n ): Promise<{ dbDeleted: number; filesDeleted: number }> {\n let dbDeleted = 0\n let filesDeleted = 0\n\n try {\n const location = this.getStorageLocation('events')\n if (!location.structured) return { dbDeleted, filesDeleted }\n\n const old = await location.structured.query(collection, {\n whereBetween: { timestamp: [0, cutoffTimestamp] },\n limit: 1000,\n })\n\n for (const record of old) {\n const eventId = record.id\n\n // Delete associated media files\n if (location.files) {\n try {\n const files = await location.files.listFiles(`events/${eventId}/`)\n for (const file of files) {\n await location.files.deleteFile(file)\n filesDeleted++\n }\n } catch {\n // Media may not exist for every event\n }\n }\n\n // Delete DB record\n await location.structured.delete(collection, eventId)\n dbDeleted++\n }\n } catch {\n // Swallow errors — retention is best-effort\n }\n\n return { dbDeleted, filesDeleted }\n }\n\n private async cleanCollection(collection: string, cutoffTimestamp: number): Promise<number> {\n try {\n const location = this.getStorageLocation('events')\n if (!location.structured) return 0\n\n // Query old records\n const old = await location.structured.query(collection, {\n whereBetween: { timestamp: [0, cutoffTimestamp] },\n limit: 1000, // batch delete\n })\n\n // Delete each\n let deleted = 0\n for (const record of old) {\n await location.structured.delete(collection, record.id)\n deleted++\n }\n\n return deleted\n } catch {\n return 0\n }\n }\n}\n","/**\n * Track Trail Service — persists the complete trajectory of tracked objects.\n *\n * For each active track, records:\n * - Every position (x, y, timestamp) at detection FPS\n * - Periodic snapshot crops at configurable intervals\n * - Track metadata (class, first/last seen, total distance, zones visited)\n */\n\nimport type { IScopedLogger, IStorageLocation, AnalysisContext, ServerTrackedDetection as TrackedDetection } from '@camstack/types'\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nexport interface TrackCaptureConfig {\n /** Enable trail capture for this device (default: false) */\n readonly enabled: boolean\n /** How often to save a snapshot of the same track (ms, default: 2000 = every 2s) */\n readonly snapshotIntervalMs: number\n /** Max trail entries per track before pruning oldest (default: 500) */\n readonly maxTrailLength: number\n /** Save thumbnail crops in trail snapshots (default: true) */\n readonly saveThumbnails: boolean\n /** Thumbnail size (default: 120x120) */\n readonly thumbnailSize: { readonly width: number; readonly height: number }\n}\n\nconst DEFAULT_CONFIG: TrackCaptureConfig = {\n enabled: false,\n snapshotIntervalMs: 2000,\n maxTrailLength: 500,\n saveThumbnails: true,\n thumbnailSize: { width: 120, height: 120 },\n}\n\n// ---------------------------------------------------------------------------\n// Trail data structures\n// ---------------------------------------------------------------------------\n\nexport interface TrackPosition {\n readonly x: number\n readonly y: number\n readonly timestamp: number\n /** Bounding box at this position [x, y, w, h] normalized */\n readonly bbox: readonly [number, number, number, number]\n}\n\nexport interface TrackSnapshot {\n readonly timestamp: number\n readonly position: TrackPosition\n /** Path to saved thumbnail in storage */\n readonly thumbnailPath: string\n}\n\nexport interface TrackTrail {\n readonly trackId: string\n readonly deviceId: string\n readonly className: string\n readonly label?: string\n readonly firstSeen: number\n readonly lastSeen: number\n /** All recorded positions (at detection FPS) */\n readonly positions: readonly TrackPosition[]\n /** Periodic snapshots (at configured interval) */\n readonly snapshots: readonly TrackSnapshot[]\n /** Total distance traveled (normalized units, 0-1 = full frame width) */\n readonly totalDistance: number\n /** All zones this track has visited */\n readonly zonesVisited: readonly string[]\n /** Whether the track is still active */\n readonly active: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Dependencies\n// ---------------------------------------------------------------------------\n\nexport interface TrackTrailDeps {\n readonly getStorageLocation: (name: string) => IStorageLocation\n readonly logger: IScopedLogger\n}\n\n// ---------------------------------------------------------------------------\n// Service\n// ---------------------------------------------------------------------------\n\nexport class TrackTrailService {\n private readonly logger: IScopedLogger\n private readonly getStorageLocation: (name: string) => IStorageLocation\n\n /** Per-device config */\n private readonly deviceConfigs: Map<string, TrackCaptureConfig> = new Map()\n\n /** Active trails: trackId -> mutable trail state */\n private readonly activeTrails: Map<string, MutableTrail> = new Map()\n\n /** Last snapshot time per track (for interval throttling) */\n private readonly lastSnapshotTime: Map<string, number> = new Map()\n\n constructor(deps: TrackTrailDeps) {\n this.logger = deps.logger\n this.getStorageLocation = deps.getStorageLocation\n }\n\n // --- Config ---\n\n setConfig(deviceId: string, config: Partial<TrackCaptureConfig>): void {\n this.deviceConfigs.set(deviceId, { ...DEFAULT_CONFIG, ...config })\n }\n\n getConfig(deviceId: string): TrackCaptureConfig {\n return this.deviceConfigs.get(deviceId) ?? { ...DEFAULT_CONFIG }\n }\n\n // --- Called after each analysis pass ---\n\n /**\n * Record trail data from an analysis context.\n * Called by the analysis pipeline addon after each frame's analysis.\n */\n async recordFrame(ctx: AnalysisContext): Promise<void> {\n const config = this.getConfig(ctx.deviceId)\n if (!config.enabled) return\n\n const activeTrackIds = new Set<string>()\n\n for (const td of ctx.trackedDetections) {\n activeTrackIds.add(td.trackId)\n await this.updateTrail(ctx, td, config)\n }\n\n // Mark trails as inactive if their track is gone\n for (const [trackId, trail] of this.activeTrails) {\n if (trail.deviceId === ctx.deviceId && !activeTrackIds.has(trackId)) {\n trail.active = false\n // Persist final trail to storage\n await this.persistTrail(trail)\n this.activeTrails.delete(trackId)\n this.lastSnapshotTime.delete(trackId)\n }\n }\n }\n\n // --- Query ---\n\n /** Get the live trail for an active track (from memory) */\n getActiveTrail(trackId: string): TrackTrail | null {\n const trail = this.activeTrails.get(trackId)\n return trail ? this.toImmutable(trail) : null\n }\n\n /** Get all active trails for a device */\n getActiveTrails(deviceId: string): readonly TrackTrail[] {\n const results: TrackTrail[] = []\n for (const trail of this.activeTrails.values()) {\n if (trail.deviceId === deviceId) {\n results.push(this.toImmutable(trail))\n }\n }\n return results\n }\n\n /** Get a persisted trail from storage (after track ended) */\n async getPersistedTrail(trackId: string): Promise<TrackTrail | null> {\n try {\n const location = this.getStorageLocation('events')\n if (!location.structured) return null\n\n const results = await location.structured.query('track_trails', {\n where: { trackId },\n limit: 1,\n })\n\n if (results.length === 0) return null\n return results[0]!.data as unknown as TrackTrail\n } catch {\n return null\n }\n }\n\n /** Get trail from memory (active) or storage (persisted) */\n async getTrail(trackId: string): Promise<TrackTrail | null> {\n return this.getActiveTrail(trackId) ?? this.getPersistedTrail(trackId)\n }\n\n /** List recent persisted trails for a device */\n async listTrails(deviceId: string, options?: {\n since?: number\n until?: number\n limit?: number\n className?: string\n }): Promise<readonly TrackTrail[]> {\n const results: TrackTrail[] = []\n\n // Active trails\n for (const trail of this.activeTrails.values()) {\n if (trail.deviceId === deviceId) {\n if (options?.className && trail.className !== options.className) continue\n results.push(this.toImmutable(trail))\n }\n }\n\n // Persisted trails\n try {\n const location = this.getStorageLocation('events')\n if (location.structured) {\n const stored = await location.structured.query('track_trails', {\n where: { deviceId, ...(options?.className ? { className: options.className } : {}) },\n whereBetween: options?.since || options?.until\n ? { firstSeen: [options.since ?? 0, options.until ?? Date.now()] }\n : undefined,\n orderBy: { field: 'firstSeen', direction: 'desc' },\n limit: options?.limit ?? 50,\n })\n\n for (const record of stored) {\n const trail = record.data as unknown as TrackTrail\n // Don't duplicate active trails\n if (!this.activeTrails.has(trail.trackId)) {\n results.push(trail)\n }\n }\n }\n } catch {\n // Storage not available\n }\n\n return results\n }\n\n // --- Internal ---\n\n private async updateTrail(\n ctx: AnalysisContext,\n td: TrackedDetection,\n config: TrackCaptureConfig,\n ): Promise<void> {\n let trail = this.activeTrails.get(td.trackId)\n\n const bbox = td.detection.boundingBox\n if (!bbox || bbox.length < 4) return\n\n const center = { x: bbox[0]! + bbox[2]! / 2, y: bbox[1]! + bbox[3]! / 2 }\n\n const position: TrackPosition = {\n x: center.x,\n y: center.y,\n timestamp: ctx.timestamp,\n bbox: [bbox[0]!, bbox[1]!, bbox[2]!, bbox[3]!] as readonly [number, number, number, number],\n }\n\n if (!trail) {\n trail = {\n trackId: td.trackId,\n deviceId: ctx.deviceId,\n className: td.detection.className,\n label: td.detection.label,\n firstSeen: ctx.timestamp,\n lastSeen: ctx.timestamp,\n positions: [position],\n snapshots: [],\n totalDistance: 0,\n zonesVisited: [...td.zones],\n active: true,\n }\n this.activeTrails.set(td.trackId, trail)\n } else {\n // Compute distance from last position\n const lastPos = trail.positions[trail.positions.length - 1]\n const dist = lastPos\n ? Math.sqrt((center.x - lastPos.x) ** 2 + (center.y - lastPos.y) ** 2)\n : 0\n\n trail.lastSeen = ctx.timestamp\n trail.totalDistance += dist\n\n // Add position (respect max length)\n if (trail.positions.length >= config.maxTrailLength) {\n // Downsample: keep every other position in the first half\n const half = Math.floor(trail.positions.length / 2)\n const downsampled = trail.positions.filter((_, i) => i >= half || i % 2 === 0)\n trail.positions = [...downsampled, position]\n } else {\n trail.positions = [...trail.positions, position]\n }\n\n // Merge new zones\n for (const zone of td.zones) {\n if (!trail.zonesVisited.includes(zone)) {\n trail.zonesVisited = [...trail.zonesVisited, zone]\n }\n }\n }\n\n // Periodic snapshot capture\n const lastSnapshot = this.lastSnapshotTime.get(td.trackId) ?? 0\n if (ctx.timestamp - lastSnapshot >= config.snapshotIntervalMs && td.crop) {\n await this.captureSnapshot(trail, td, position, config)\n this.lastSnapshotTime.set(td.trackId, ctx.timestamp)\n }\n }\n\n private async captureSnapshot(\n trail: MutableTrail,\n td: TrackedDetection,\n position: TrackPosition,\n config: TrackCaptureConfig,\n ): Promise<void> {\n if (!td.crop || !config.saveThumbnails) return\n\n try {\n const sharp = (await import('sharp')).default\n const thumb = await sharp(td.crop)\n .resize(config.thumbnailSize.width, config.thumbnailSize.height, { fit: 'cover' })\n .jpeg({ quality: 75 })\n .toBuffer()\n\n const path = `debug-trails/${trail.trackId}/${position.timestamp}.jpg`\n\n // Write to storage\n const location = this.getStorageLocation('events')\n if (location.files) {\n await location.files.writeFile(path, thumb)\n }\n\n trail.snapshots = [...trail.snapshots, {\n timestamp: position.timestamp,\n position,\n thumbnailPath: path,\n }]\n } catch {\n // Snapshot capture failed — non-critical, continue\n }\n }\n\n private async persistTrail(trail: MutableTrail): Promise<void> {\n try {\n const location = this.getStorageLocation('events')\n if (!location.structured) return\n\n await location.structured.insert({\n collection: 'track_trails',\n id: trail.trackId,\n data: this.toImmutable(trail) as unknown as Record<string, unknown>,\n })\n\n this.logger.debug(\n `Trail persisted: ${trail.trackId} (${trail.positions.length} positions, ${trail.snapshots.length} snapshots, ${trail.totalDistance.toFixed(3)} distance)`,\n )\n } catch (err) {\n this.logger.warn(`Failed to persist trail ${trail.trackId}: ${err}`)\n }\n }\n\n private toImmutable(trail: MutableTrail): TrackTrail {\n return {\n trackId: trail.trackId,\n deviceId: trail.deviceId,\n className: trail.className,\n label: trail.label,\n firstSeen: trail.firstSeen,\n lastSeen: trail.lastSeen,\n positions: [...trail.positions],\n snapshots: [...trail.snapshots],\n totalDistance: trail.totalDistance,\n zonesVisited: [...trail.zonesVisited],\n active: trail.active,\n }\n }\n}\n\n// Mutable internal state (not exported)\ninterface MutableTrail {\n readonly trackId: string\n readonly deviceId: string\n readonly className: string\n readonly label?: string\n firstSeen: number\n lastSeen: number\n positions: TrackPosition[]\n snapshots: TrackSnapshot[]\n totalDistance: number\n zonesVisited: string[]\n active: boolean\n}\n","import type {\n ICamstackAddon,\n AddonManifest,\n AddonContext,\n CapabilityProviderMap,\n IConfigurable,\n ConfigUISchema,\n FfmpegConfig,\n IStreamingEngine,\n IPipelineManager,\n INetworkQualityTracker,\n} from '@camstack/types'\nimport { DecoderRegistry } from './stream-broker/decoder-registry'\nimport { FfmpegDecoderProvider } from './stream-broker/ffmpeg-decoder-provider'\nimport { StreamBrokerManager } from './stream-broker/stream-broker-manager'\nimport { EventPersistenceService } from './persistence/event-persistence'\nimport { KnownFacesService } from './persistence/known-faces'\nimport { SessionTrackerService } from './persistence/session-tracker'\nimport { RetentionService } from './persistence/retention'\nimport { TrackTrailService } from './persistence/track-trail'\nimport type { IAnalysisDataPersistence } from './persistence/types'\n\n// Recording engine imports — dynamic to avoid hard dep on better-sqlite3 at load time\ntype RecordingCoordinator = import('./recording/recording-coordinator').RecordingCoordinator\ntype RecordingDb = import('./recording/recording-db').RecordingDb\n\nexport interface RecordingEngineDependencies {\n readonly streamingEngine: IStreamingEngine\n readonly pipelineManager: IPipelineManager\n readonly networkTracker: INetworkQualityTracker\n}\n\nexport type RecordingEngineV2Dependencies = RecordingEngineDependencies\n\nexport class PipelineAddon implements ICamstackAddon, IConfigurable {\n readonly manifest: AddonManifest = {\n id: 'pipeline',\n name: 'CamStack Pipeline',\n version: '0.1.0',\n capabilities: [\n { name: 'stream-broker', mode: 'singleton' as const },\n { name: 'recording-engine', mode: 'singleton' as const },\n { name: 'analysis-data-persistence', mode: 'singleton' as const },\n ],\n }\n\n // Stream broker\n private brokerManager: StreamBrokerManager | null = null\n\n // Recording engine\n private coordinator: RecordingCoordinator | null = null\n private recordingDb: RecordingDb | null = null\n private sqliteDb: import('better-sqlite3').Database | null = null\n private recordingDeps: RecordingEngineDependencies | null = null\n private currentRecordingConfig = {\n ffmpegPath: 'ffmpeg',\n hwaccel: undefined as FfmpegConfig['hwaccel'] | undefined,\n threads: undefined as number | undefined,\n segmentDurationSeconds: 4,\n defaultRetentionDays: 30,\n }\n\n // Analysis persistence\n private persistenceFacade: IAnalysisDataPersistence | null = null\n\n setRecordingDependencies(deps: RecordingEngineDependencies): void {\n this.recordingDeps = deps\n }\n\n async initialize(context: AddonContext): Promise<void> {\n // --- 1. Stream broker ---\n const registry = new DecoderRegistry()\n registry.register(new FfmpegDecoderProvider(), 50)\n this.brokerManager = new StreamBrokerManager(registry, context.logger)\n context.logger.info('Stream broker manager initialized')\n\n // --- 2. Recording engine ---\n if (this.recordingDeps) {\n try {\n const Database = (await import('better-sqlite3')).default\n const path = await import('node:path')\n const { detectPlatformDefaults } = await import('./recording/ffmpeg-config')\n const { RecordingDb: RecDb } = await import('./recording/recording-db')\n const { RecordingCoordinator: RecCoord } = await import('./recording/recording-coordinator')\n\n const storagePath = context.locationPaths.recordings\n const dbPath = path.join(context.locationPaths.data, 'camstack.db')\n this.sqliteDb = new Database(dbPath)\n this.recordingDb = new RecDb(this.sqliteDb)\n this.recordingDb.initialize()\n\n const ffmpegPath = (context.addonConfig.ffmpegPath as string) ?? this.currentRecordingConfig.ffmpegPath\n const detectedFfmpegConfig = detectPlatformDefaults(ffmpegPath)\n\n const globalFfmpegConfig: Partial<FfmpegConfig> = {\n path: ffmpegPath,\n hwaccel: (context.addonConfig.hwaccel as FfmpegConfig['hwaccel']) ?? this.currentRecordingConfig.hwaccel,\n threads: (context.addonConfig.threads as number) ?? this.currentRecordingConfig.threads,\n }\n\n const segmentDurationSeconds = (context.addonConfig.segmentDurationSeconds as number)\n ?? this.currentRecordingConfig.segmentDurationSeconds\n const defaultRetentionDays = (context.addonConfig.defaultRetentionDays as number)\n ?? this.currentRecordingConfig.defaultRetentionDays\n\n this.currentRecordingConfig = {\n ffmpegPath,\n hwaccel: globalFfmpegConfig.hwaccel,\n threads: globalFfmpegConfig.threads,\n segmentDurationSeconds,\n defaultRetentionDays,\n }\n\n const fileStorage = context.storage.files\n if (!fileStorage) {\n throw new Error('PipelineAddon:file storage not available in addon context')\n }\n\n this.coordinator = new RecCoord({\n db: this.recordingDb,\n logger: context.logger,\n eventBus: context.eventBus,\n streamingEngine: this.recordingDeps.streamingEngine,\n pipelineManager: this.recordingDeps.pipelineManager,\n networkTracker: this.recordingDeps.networkTracker,\n fileStorage,\n storagePath,\n globalFfmpegConfig,\n detectedFfmpegConfig,\n segmentDurationSec: segmentDurationSeconds,\n })\n await this.coordinator.start()\n context.logger.info('Recording Engine initialized')\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error)\n context.logger.warn(`Recording Engine failed to initialize: ${msg}`)\n }\n }\n\n // --- 3. Analysis persistence ---\n const eventPersistence = new EventPersistenceService({\n getStorageLocation: () => context.storage,\n subscribe: (filter, handler) => context.eventBus.subscribe(filter, handler),\n logger: context.logger.child('EventPersistence'),\n })\n\n const knownFaces = new KnownFacesService({\n getStructuredStorage: () => {\n if (!context.storage.structured) {\n throw new Error('Structured storage not available for analysis persistence')\n }\n return context.storage.structured\n },\n logger: context.logger.child('KnownFaces'),\n })\n\n const sessionTracker = new SessionTrackerService({\n eventBus: context.eventBus,\n logger: context.logger.child('SessionTracker'),\n })\n\n const retention = new RetentionService({\n getStorageLocation: () => context.storage,\n getRetentionConfig: () => context.addonConfig.retention as Record<string, unknown> | undefined,\n eventBus: context.eventBus,\n logger: context.logger.child('Retention'),\n })\n\n const trackTrail = new TrackTrailService({\n getStorageLocation: () => context.storage,\n logger: context.logger.child('TrackTrail'),\n })\n\n eventPersistence.start()\n retention.start()\n\n this.persistenceFacade = {\n eventPersistence,\n knownFaces,\n sessionTracker,\n retention,\n trackTrail,\n }\n\n context.logger.info('Analysis persistence initialized')\n }\n\n async shutdown(): Promise<void> {\n // Stream broker\n await this.brokerManager?.destroyAll()\n this.brokerManager = null\n\n // Recording engine\n if (this.coordinator) {\n this.coordinator.stop()\n this.coordinator = null\n }\n if (this.sqliteDb) {\n this.sqliteDb.close()\n this.sqliteDb = null\n }\n this.recordingDb = null\n\n // Analysis persistence\n if (this.persistenceFacade) {\n this.persistenceFacade.eventPersistence.stop()\n this.persistenceFacade.retention.stop()\n this.persistenceFacade = null\n }\n }\n\n getCapabilityProvider<K extends keyof CapabilityProviderMap>(\n name: K,\n ): CapabilityProviderMap[K] | null {\n switch (name) {\n case 'stream-broker':\n return this.brokerManager as unknown as CapabilityProviderMap[K]\n case 'recording-engine':\n return this.coordinator as unknown as CapabilityProviderMap[K]\n case 'analysis-data-persistence':\n return this.persistenceFacade as unknown as CapabilityProviderMap[K]\n default:\n return null\n }\n }\n\n // --- Recording engine accessors ---\n\n getCoordinator(): RecordingCoordinator {\n if (!this.coordinator) throw new Error('PipelineAddon recording not initialized')\n return this.coordinator\n }\n\n getRecordingDb(): RecordingDb {\n if (!this.recordingDb) throw new Error('PipelineAddon recording not initialized')\n return this.recordingDb\n }\n\n // --- IConfigurable ---\n\n getConfigSchema(): ConfigUISchema {\n return { sections: [] }\n }\n\n getConfig(): Record<string, unknown> {\n return { ...this.currentRecordingConfig }\n }\n\n async onConfigChange(config: Record<string, unknown>): Promise<void> {\n this.currentRecordingConfig = {\n ffmpegPath: (config.ffmpegPath as string) ?? this.currentRecordingConfig.ffmpegPath,\n hwaccel: (config.hwaccel as FfmpegConfig['hwaccel']) ?? this.currentRecordingConfig.hwaccel,\n threads: (config.threads as number) ?? this.currentRecordingConfig.threads,\n segmentDurationSeconds: (config.segmentDurationSeconds as number) ?? this.currentRecordingConfig.segmentDurationSeconds,\n defaultRetentionDays: (config.defaultRetentionDays as number) ?? this.currentRecordingConfig.defaultRetentionDays,\n }\n }\n}\n\nexport default PipelineAddon\n"],"mappings":"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","names":["platform","spawn","spawn","spawn","path","sharp","randomUUID","randomUUID","sharp","path","path","detectPlatformDefaults"]}
|
|
1
|
+
{"version":3,"sources":["../src/recording/ffmpeg-config.ts","../src/recording/recording-db.ts","../src/recording/segment-writer.ts","../src/recording/thumbnail-extractor.ts","../src/recording/retention-manager.ts","../src/recording/playlist-generator.ts","../src/recording/storage-estimator.ts","../src/recording/recording-coordinator.ts","../src/stream-broker/decoder-registry.ts","../src/stream-broker/ffmpeg-decoder-session.ts","../src/stream-broker/frame-dropper.ts","../src/stream-broker/ffmpeg-decoder-provider.ts","../src/stream-broker/stream-broker.ts","../src/stream-broker/stream-pipe-server.ts","../src/stream-broker/stream-broker-manager.ts","../src/persistence/event-persistence.ts","../src/persistence/known-faces.ts","../src/persistence/session-tracker.ts","../src/persistence/retention.ts","../src/persistence/track-trail.ts","../src/addon.ts"],"sourcesContent":["import { execFileSync } from 'node:child_process'\nimport * as os from 'node:os'\nimport type { FfmpegConfig, IAddonDepsManager } from '@camstack/types'\n\n/**\n * Probe ffmpeg binary and detect optimal hwaccel for the host platform.\n * Called once at server startup, result cached.\n */\nexport function detectPlatformDefaults(ffmpegPath: string = 'ffmpeg'): Partial<FfmpegConfig> {\n let hwaccels: string[] = []\n try {\n const output = execFileSync(ffmpegPath, ['-hwaccels', '-hide_banner'], { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })\n hwaccels = output.split('\\n').map(l => l.trim()).filter(l => l && l !== 'Hardware acceleration methods:')\n } catch {}\n\n const platform = os.platform()\n if (platform === 'darwin' && hwaccels.includes('videotoolbox')) return { hwaccel: 'videotoolbox', threads: 0 }\n if (platform === 'linux') {\n for (const hw of ['vaapi', 'qsv', 'cuda', 'v4l2m2m'] as const) {\n if (hwaccels.includes(hw)) return { hwaccel: hw, threads: 0 }\n }\n }\n return { hwaccel: 'none', threads: Math.max(1, Math.floor(os.cpus().length / 2)) }\n}\n\n/**\n * Resolve ffmpeg config for a device.\n * Merge order: detected <- global <- per-device (most specific wins).\n */\nexport function resolveFfmpegConfig(\n deviceConfig: Partial<FfmpegConfig> | undefined,\n globalConfig: Partial<FfmpegConfig>,\n detected: Partial<FfmpegConfig>,\n): FfmpegConfig {\n return {\n path: deviceConfig?.path ?? globalConfig.path ?? detected.path ?? 'ffmpeg',\n hwaccel: deviceConfig?.hwaccel ?? globalConfig.hwaccel ?? detected.hwaccel ?? 'none',\n inputArgs: deviceConfig?.inputArgs ?? globalConfig.inputArgs,\n outputArgs: deviceConfig?.outputArgs ?? globalConfig.outputArgs,\n videoCodec: deviceConfig?.videoCodec ?? globalConfig.videoCodec ?? 'copy',\n audioCodec: deviceConfig?.audioCodec ?? globalConfig.audioCodec ?? 'copy',\n threads: deviceConfig?.threads ?? globalConfig.threads ?? detected.threads,\n }\n}\n\n/**\n * Build common ffmpeg args from a resolved config.\n */\nexport function buildFfmpegInputArgs(config: FfmpegConfig, inputUrl: string): string[] {\n const args: string[] = ['-hide_banner', '-loglevel', 'warning']\n if (config.hwaccel && config.hwaccel !== 'none') args.push('-hwaccel', config.hwaccel)\n if (config.threads !== undefined) args.push('-threads', String(config.threads))\n if (config.inputArgs?.length) args.push(...config.inputArgs)\n args.push('-rtsp_transport', 'tcp', '-i', inputUrl)\n return args\n}\n\nexport function buildFfmpegOutputArgs(config: FfmpegConfig): string[] {\n const args: string[] = ['-c:v', config.videoCodec ?? 'copy', '-c:a', config.audioCodec ?? 'copy']\n if (config.outputArgs?.length) args.push(...config.outputArgs)\n return args\n}\n\n/**\n * Resolve ffmpeg binary path. Priority:\n * 1. Explicit config path\n * 2. Embedded (data/deps/ffmpeg)\n * 3. System PATH\n * 4. Download static build\n */\nexport async function resolveFfmpegBinary(\n configPath: string | undefined,\n deps: IAddonDepsManager,\n): Promise<string> {\n // Explicit config\n if (configPath && configPath !== 'ffmpeg') {\n return configPath\n }\n\n // Try embedded or download via deps manager\n return deps.ensureFfmpeg()\n}\n","import type Database from 'better-sqlite3'\nimport type {\n RecordingSegment, RecordingThumbnail, RecordingStorageConfig,\n RecordingPolicy, RecordingMode, StreamPolicy, ScheduleRule,\n CleanupQueueEntry, CleanupStatus, DataCategory,\n} from './types'\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\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\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, 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: string, streamId: string, startTime: number, endTime: number): readonly RecordingSegment[] {\n const rows = this.db.prepare(`\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(deviceId, streamId, endTime, startTime) as Array<Record<string, unknown>>\n return rows.map(rowToSegment)\n }\n\n deleteSegmentsBefore(deviceId: string, streamId: string, beforeTime: number): readonly RecordingSegment[] {\n const toDelete = this.db.prepare(`\n SELECT * FROM recording_segments WHERE device_id = ? AND stream_id = ? AND start_time < ?\n `).all(deviceId, streamId, beforeTime) as Array<Record<string, unknown>>\n this.db.prepare(`DELETE FROM recording_segments WHERE device_id = ? AND stream_id = ? AND start_time < ?`).run(deviceId, streamId, beforeTime)\n return toDelete.map(rowToSegment)\n }\n\n deleteSegmentsForDevice(deviceId: string): readonly RecordingSegment[] {\n const toDelete = this.db.prepare(`SELECT * FROM recording_segments WHERE device_id = ?`).all(deviceId) as Array<Record<string, unknown>>\n this.db.prepare(`DELETE FROM recording_segments WHERE device_id = ?`).run(deviceId)\n return toDelete.map(rowToSegment)\n }\n\n getStorageUsage(deviceId: string, streamId: string): StorageUsage {\n const row = this.db.prepare(`\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(deviceId, streamId) as { total_bytes: number; segment_count: number }\n return { totalBytes: row.total_bytes, segmentCount: row.segment_count }\n }\n\n getOldestSegments(deviceId: string, streamId: string, limit: number): readonly RecordingSegment[] {\n const rows = this.db.prepare(`\n SELECT * FROM recording_segments WHERE device_id = ? AND stream_id = ?\n ORDER BY start_time ASC LIMIT ?\n `).all(deviceId, streamId, limit) as Array<Record<string, unknown>>\n return rows.map(rowToSegment)\n }\n\n getAvailability(deviceId: string, startTime: number, endTime: number): readonly AvailabilityRange[] {\n const rows = this.db.prepare(`\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(deviceId, startTime, endTime) as Array<{ start_time: number; end_time: number; stream_id: string }>\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: string, 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(`\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(deviceId, startTime, endTime) as { total_events: number; avg_duration: number; total_duration: number }\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 return {\n totalEvents: row.total_events,\n avgDurationSec: Math.round(row.avg_duration * 100) / 100,\n avgEventsPerDay: Math.round((row.total_events / timeRangeDays) * 100) / 100,\n dutyCyclePercent: Math.round((row.total_duration / 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(thumb.deviceId, thumb.timestamp, thumb.path, thumb.storageName, thumb.subDirectory, thumb.sizeBytes, thumb.category)\n }\n\n findNearestThumbnail(deviceId: string, timestamp: number, category: string): RecordingThumbnail | null {\n const row = this.db.prepare(`\n SELECT * FROM recording_thumbnails\n WHERE device_id = ? AND category = ?\n ORDER BY ABS(timestamp - ?) ASC LIMIT 1\n `).get(deviceId, category, timestamp) as Record<string, unknown> | undefined\n return row ? rowToThumbnail(row) : null\n }\n\n deleteThumbnailsBefore(deviceId: string, beforeTime: number): number {\n const result = this.db.prepare(`DELETE FROM recording_thumbnails WHERE device_id = ? AND timestamp < ?`).run(deviceId, beforeTime)\n return result.changes\n }\n\n deleteThumbnailsForDevice(deviceId: string): number {\n const result = this.db.prepare(`DELETE FROM recording_thumbnails WHERE device_id = ?`).run(deviceId)\n return result.changes\n }\n\n // --- Policies ---\n\n upsertPolicy(policy: { deviceId: string; 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 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: string): RecordingPolicy | null {\n const row = this.db.prepare(`SELECT * FROM recording_policies WHERE device_id = ?`).get(deviceId) as PolicyRow | undefined\n return row ? rowToPolicy(row) : null\n }\n\n getEnabledPolicies(): readonly RecordingPolicy[] {\n const rows = this.db.prepare(`SELECT * FROM recording_policies WHERE enabled = 1`).all() as PolicyRow[]\n return rows.map(rowToPolicy)\n }\n\n deletePolicy(deviceId: string): void {\n this.db.prepare(`DELETE FROM recording_policies WHERE device_id = ?`).run(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(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: string, category: DataCategory): RecordingStorageConfig | null {\n const specific = this.db.prepare(`SELECT * FROM recording_storage_config WHERE device_id = ? AND data_category = ?`).get(deviceId, category) as Record<string, unknown> | undefined\n if (specific) return rowToStorageConfig(specific)\n const global = this.db.prepare(`SELECT * FROM recording_storage_config WHERE device_id = '*' AND data_category = ?`).get(category) as Record<string, unknown> | undefined\n return global ? rowToStorageConfig(global) : null\n }\n\n // --- Cleanup Queue ---\n\n addToCleanupQueue(deviceId: string, 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(deviceId, disabledAt, cleanupAfter)\n }\n\n cancelCleanup(deviceId: string): void {\n this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'cancelled' WHERE device_id = ? AND status = 'pending'`).run(deviceId)\n }\n\n getCleanupEntry(deviceId: string): CleanupQueueEntry | null {\n const row = this.db.prepare(`SELECT * FROM recording_cleanup_queue WHERE device_id = ?`).get(deviceId) as Record<string, unknown> | undefined\n return row ? rowToCleanup(row) : null\n }\n\n getPendingCleanups(): readonly CleanupQueueEntry[] {\n const now = Date.now()\n const rows = this.db.prepare(`SELECT * FROM recording_cleanup_queue WHERE status = 'pending' AND cleanup_after <= ?`).all(now) as Array<Record<string, unknown>>\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: string): void {\n this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'in_progress', started_at = ? WHERE device_id = ?`).run(Date.now(), deviceId)\n }\n\n markCleanupCompleted(deviceId: string): void {\n this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'completed' WHERE device_id = ?`).run(deviceId)\n }\n}\n\n// --- Row mappers ---\n\nfunction rowToSegment(row: Record<string, unknown>): RecordingSegment {\n return {\n id: row.id as string,\n deviceId: row.device_id as string,\n streamId: row.stream_id as string,\n startTime: row.start_time as number,\n endTime: row.end_time as number,\n duration: row.duration as number,\n path: row.path as string,\n storageName: row.storage_name as string,\n subDirectory: row.sub_directory as string,\n sizeBytes: row.size_bytes as number,\n codec: row.codec as 'h264' | 'h265',\n hasAudio: (row.has_audio as number) === 1,\n }\n}\n\nfunction rowToThumbnail(row: Record<string, unknown>): RecordingThumbnail {\n return {\n deviceId: row.device_id as string,\n timestamp: row.timestamp as number,\n path: row.path as string,\n storageName: row.storage_name as string,\n subDirectory: row.sub_directory as string,\n sizeBytes: row.size_bytes as number,\n category: row.category as 'scrub' | 'event',\n }\n}\n\nfunction rowToPolicy(row: PolicyRow): RecordingPolicy {\n return {\n deviceId: row.device_id,\n enabled: row.enabled === 1,\n mode: row.mode as RecordingMode,\n streams: JSON.parse(row.streams_json) as StreamPolicy[],\n preBufferSec: row.pre_buffer_sec,\n postBufferSec: row.post_buffer_sec,\n scheduleRules: row.schedule_json ? JSON.parse(row.schedule_json) as ScheduleRule[] : undefined,\n }\n}\n\nfunction rowToStorageConfig(row: Record<string, unknown>): RecordingStorageConfig {\n return {\n deviceId: row.device_id as string,\n dataCategory: row.data_category as DataCategory,\n storageName: row.storage_name as string,\n subDirectory: row.sub_directory as string,\n retentionDays: row.retention_days as number | null,\n retentionGb: row.retention_gb as number | null,\n }\n}\n\nfunction rowToCleanup(row: Record<string, unknown>): CleanupQueueEntry {\n return {\n deviceId: row.device_id as string,\n disabledAt: row.disabled_at as number,\n cleanupAfter: row.cleanup_after as number,\n status: row.status as CleanupStatus,\n startedAt: row.started_at as number | null,\n }\n}\n","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 type { IAddonFileStorage } from '@camstack/types'\nimport type { IScopedLogger, IEventBus, INetworkQualityTracker, FfmpegConfig } from '@camstack/types'\nimport { buildFfmpegInputArgs, buildFfmpegOutputArgs } from './ffmpeg-config'\nimport type { RecordingDb } from './recording-db'\nimport type { SegmentWriterState, RecordingSegment } from './types'\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: string\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 private readonly _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 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 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.length} buffered segments to disk`, {\n deviceId: this.config.deviceId,\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: string, 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: ${msg}`)\n this.parseSegmentOutput(msg)\n }\n })\n\n this.ffmpeg.on('error', (err) => {\n this.logger.warn(`ffmpeg process 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 code ${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: 'recording.error',\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(\n `Max restarts exceeded for ${this.config.deviceId}/${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: 'recording.health.degraded',\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 in ${backoffMs}ms (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 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 for ${elapsed}ms, restarting ffmpeg`)\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: ${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: 'recording.storage.critical',\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: 'recording.segment.written',\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: ${err}`)\n }\n } catch (err) {\n this.logger.error(`Disk space check failed: ${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: 'recording.segment.written',\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: ${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'\n\nexport interface ThumbnailExtractorConfig {\n readonly deviceId: string\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: string): void {\n this.active = true\n\n this.unsubscribe = pipeline.onVideoFrame(\n (frame) => { this.handleFrame(frame).catch((err) => this.logger.debug(`Thumbnail error: ${err}`)) },\n this.videoRequirements,\n )\n\n this.logger.info(`ThumbnailExtractor attached for ${this.config.deviceId}`)\n }\n\n detachFromPipeline(_deviceId: string): void {\n this.active = false\n if (this.unsubscribe) {\n this.unsubscribe()\n this.unsubscribe = null\n }\n this.logger.info(`ThumbnailExtractor detached for ${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: string, timestamp: number): string {\n return `${subDirectory}/${deviceId}/${timestamp}.jpg`\n }\n}\n","import type { IScopedLogger, IEventBus, IFileStorage } from '@camstack/types'\nimport type { RecordingDb } from './recording-db'\nimport type { DataCategory } from './types'\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 fileStorage: IFileStorage,\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 for ${entry.deviceId}: ${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: 'recording.retention.completed',\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: ${err}`)\n this.scheduleNextCycle(NORMAL_INTERVAL_MS)\n }\n }, intervalMs)\n }\n\n private emitStorageEvent(category: string, deviceId: string, 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.fileStorage.deleteFile(filePath)\n } catch {\n // File may already be deleted\n }\n }\n}\n","import type { RecordingDb } from './recording-db'\nimport type { RecordingSegment } from './types'\n\ninterface PlaylistOptions {\n readonly live?: boolean\n}\n\nconst FALLBACK_STREAMS = ['sub', 'mid', 'main'] as const\n\nexport class PlaylistGenerator {\n constructor(private readonly db: RecordingDb) {}\n\n generate(deviceId: string, streamId: string, startTime: number, endTime: number, options?: PlaylistOptions): string {\n const segments = this.resolveSegments(deviceId, streamId, startTime, endTime)\n return this.buildPlaylist(segments, options)\n }\n\n private resolveSegments(deviceId: string, streamId: string, startTime: number, endTime: number): readonly RecordingSegment[] {\n const segments = this.db.querySegments(deviceId, streamId, startTime, endTime)\n if (segments.length > 0) {\n return segments\n }\n\n const fallbacks = FALLBACK_STREAMS.filter(s => s !== streamId)\n for (const fb of fallbacks) {\n const fallbackSegments = this.db.querySegments(deviceId, fb, startTime, endTime)\n if (fallbackSegments.length > 0) {\n return fallbackSegments\n }\n }\n\n return []\n }\n\n private buildPlaylist(segments: readonly RecordingSegment[], options?: PlaylistOptions): string {\n const targetDuration = segments.length > 0\n ? Math.ceil(Math.max(...segments.map(s => s.duration)))\n : 4\n\n const lines: string[] = [\n '#EXTM3U',\n '#EXT-X-VERSION:7',\n `#EXT-X-TARGETDURATION:${targetDuration}`,\n '#EXT-X-MEDIA-SEQUENCE:0',\n ]\n\n if (!options?.live) {\n lines.push('#EXT-X-PLAYLIST-TYPE:VOD')\n }\n\n for (const seg of segments) {\n lines.push(`#EXTINF:${seg.duration.toFixed(3)},`)\n lines.push(seg.path)\n }\n\n if (!options?.live) {\n lines.push('#EXT-X-ENDLIST')\n }\n\n return lines.join('\\n')\n }\n}\n","import type { INetworkQualityTracker } from '@camstack/types'\nimport type { RecordingDb } from './recording-db'\nimport type { StorageEstimate, StreamEstimate } from './types'\n\ninterface MotionEstimateInput {\n readonly avgEventsPerDay: number\n readonly avgDurationSec: number\n}\n\nexport class StorageEstimator {\n constructor(\n private readonly db: RecordingDb,\n private readonly networkTracker: INetworkQualityTracker,\n ) {}\n\n estimateForDevice(deviceId: string, motionInput?: MotionEstimateInput): StorageEstimate {\n const policy = this.db.getPolicy(deviceId)\n if (!policy) return { perStream: {}, thumbnails: { estimatedGb: 0 }, totalEstimatedGb: 0 }\n\n const stats = this.networkTracker.getDeviceStats(deviceId)\n const perStream: Record<string, StreamEstimate> = {}\n let totalGb = 0\n\n const motionEstimate = motionInput ? {\n avgEventsPerDay: motionInput.avgEventsPerDay,\n avgDurationSec: motionInput.avgDurationSec,\n dutyCyclePercent: (motionInput.avgEventsPerDay * motionInput.avgDurationSec / 86400) * 100,\n } : undefined\n\n for (const sp of policy.streams) {\n const category = `recording:${sp.streamId}` as any\n const config = this.db.resolveStorageConfig(deviceId, category)\n const retentionDays = config?.retentionDays ?? null\n const retentionGb = config?.retentionGb ?? null\n\n const streamStats = stats?.streams[sp.streamId]\n const observedBitrate = streamStats?.observedBitrateKbps ?? 0\n const bitrateKbps = observedBitrate > 0\n ? observedBitrate\n : (streamStats?.nominalBitrateKbps ?? 4000)\n\n let estimatedGb = 0\n if (retentionDays !== null) {\n const retentionSeconds = retentionDays * 86400\n estimatedGb = (bitrateKbps * retentionSeconds) / 8 / 1024 / 1024 / 1024\n }\n\n if (policy.mode === 'motion' && motionEstimate) {\n estimatedGb = estimatedGb * motionEstimate.dutyCyclePercent / 100\n }\n\n let estimatedDaysAtCapacity: number | null = null\n if (retentionGb !== null && estimatedGb > retentionGb) {\n estimatedDaysAtCapacity = retentionDays !== null ? retentionDays * (retentionGb / estimatedGb) : null\n estimatedGb = retentionGb\n }\n\n perStream[sp.streamId] = { bitrateKbps, retentionDays, retentionGb, estimatedGb, estimatedDaysAtCapacity }\n totalGb += estimatedGb\n }\n\n const thumbRetentionDays = policy.streams[0]\n ? (this.db.resolveStorageConfig(deviceId, 'thumbnail:scrub' as any)?.retentionDays ?? 7)\n : 7\n const thumbGb = (2 * 24 * thumbRetentionDays) / 1024\n totalGb += thumbGb\n\n return { perStream, thumbnails: { estimatedGb: thumbGb }, totalEstimatedGb: totalGb, motionEstimate }\n }\n}\n","import type { IScopedLogger, IEventBus, SystemEvent, IStreamingEngine, IPipelineManager, INetworkQualityTracker, IFileStorage, FfmpegConfig } from '@camstack/types'\nimport { resolveFfmpegConfig } from './ffmpeg-config'\nimport type { RecordingDb } from './recording-db'\nimport type {\n RecordingPolicy, RecordingEnableConfig, ScheduleRule,\n SegmentWriterState,\n} from './types'\nimport { SegmentWriter, type SegmentWriterConfig } from './segment-writer'\nimport { ThumbnailExtractor, type ThumbnailExtractorConfig } from './thumbnail-extractor'\nimport { RetentionManager } from './retention-manager'\nimport { PlaylistGenerator } from './playlist-generator'\nimport { StorageEstimator } from './storage-estimator'\n\n// --- Per-device recording state ---\n\ninterface DeviceRecordingState {\n readonly deviceId: string\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 readonly fileStorage: IFileStorage\n readonly storagePath: string\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 fileStorage: IFileStorage\n private readonly storagePath: string\n private readonly globalFfmpegConfig: Partial<FfmpegConfig>\n private readonly detectedFfmpegConfig: Partial<FfmpegConfig>\n private readonly segmentDurationSec: number\n\n private readonly recordings = new Map<string, 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.fileStorage = config.fileStorage\n this.storagePath = config.storagePath\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.fileStorage,\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 for ${policy.deviceId}: ${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: string, 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 any)\n const storageName = storageConfig?.storageName ?? 'recordings'\n const subDirectory = storageConfig?.subDirectory ?? `recordings/${sp.streamId}`\n\n const writerConfig: SegmentWriterConfig = {\n deviceId,\n streamId: sp.streamId,\n segmentDurationSec: this.segmentDurationSec,\n storagePath: this.storagePath,\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 thumbConfig: ThumbnailExtractorConfig = {\n deviceId,\n storagePath: this.storagePath,\n storageName: thumbStorageConfig?.storageName ?? 'recordings',\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 for ${deviceId} within ${MOTION_FALLBACK_TIMEOUT_MS / 1000}s — falling back to continuous recording`)\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: 'recording.policy.fallback',\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 for ${deviceId} during fallback: ${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: 'recording.started',\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 for ${deviceId}`, { mode: policy.mode })\n }\n\n async disableRecording(deviceId: string): Promise<void> {\n const state = this.recordings.get(deviceId)\n if (!state) {\n this.logger.warn(`No active recording for ${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: 'recording.stopped',\n data: {\n deviceId,\n segmentCount: totalSegmentCount,\n totalMB,\n },\n })\n\n this.logger.info(`Recording disabled for ${deviceId}`, { segmentCount: totalSegmentCount, totalMB })\n }\n\n isRecording(deviceId: string): boolean {\n return this.recordings.has(deviceId)\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: string, 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: string, 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 for ${deviceId}: ${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: string): 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","import type { IDecoderProvider } from '@camstack/types'\n\ninterface RegisteredProvider {\n readonly provider: IDecoderProvider\n readonly priority: number\n}\n\nexport class DecoderRegistry {\n private readonly entries: RegisteredProvider[] = []\n\n register(provider: IDecoderProvider, priority: number): void {\n this.entries.push({ provider, priority })\n this.entries.sort((a, b) => a.priority - b.priority)\n }\n\n getDecoder(codec: string): IDecoderProvider | undefined {\n return this.entries.find((e) => e.provider.supportsCodec(codec))?.provider\n }\n}\n","import { spawn, type ChildProcess } from 'node:child_process'\nimport type { IDecoderSession, DecoderSessionConfig, DecoderStats, EncodedPacket, DecodedFrame, Unsubscribe } from '@camstack/types'\nimport { FrameDropper } from './frame-dropper'\n\nconst SOI = Buffer.from([0xff, 0xd8])\nconst EOI = Buffer.from([0xff, 0xd9])\n\nfunction codecToInputFormat(codec: string): string {\n switch (codec) {\n case 'h265':\n case 'hevc':\n return 'hevc'\n case 'mjpeg':\n return 'mjpeg'\n case 'h264':\n default:\n return 'h264'\n }\n}\n\nfunction buildFfmpegArgs(config: DecoderSessionConfig): string[] {\n const inputFormat = codecToInputFormat(config.codec)\n const args = ['-hide_banner', '-loglevel', 'error', '-f', inputFormat, '-i', 'pipe:0']\n\n if (config.scale > 1) {\n args.push('-vf', `scale=iw/${config.scale}:ih/${config.scale}`)\n }\n\n args.push('-f', 'image2pipe', '-vcodec', 'mjpeg', '-q:v', '3', 'pipe:1')\n return args\n}\n\nexport class FfmpegDecoderSession implements IDecoderSession {\n private config: DecoderSessionConfig\n private frameDropper: FrameDropper\n private process: ChildProcess | null = null\n private frameCallbacks = new Set<(frame: DecodedFrame) => void>()\n private outputBuffer = Buffer.alloc(0)\n private destroyed = false\n\n // Stats tracking\n private inputPackets = 0\n private outputFrames = 0\n private droppedFrames = 0\n private totalDecodeTimeMs = 0\n private decodeCount = 0\n private startTime = Date.now()\n\n constructor(config: DecoderSessionConfig) {\n this.config = { ...config }\n this.frameDropper = new FrameDropper(config.maxFps)\n this.spawnFfmpeg()\n }\n\n private spawnFfmpeg(): void {\n if (this.destroyed) return\n\n this.killFfmpeg()\n this.outputBuffer = Buffer.alloc(0)\n\n const args = buildFfmpegArgs(this.config)\n this.process = spawn('ffmpeg', args)\n\n // Swallow EPIPE errors on stdin during shutdown\n this.process.stdin?.on('error', () => {})\n\n this.process.stdout?.on('data', (chunk: Buffer) => {\n this.handleOutputData(chunk)\n })\n\n this.process.stderr?.on('data', (data: Buffer) => {\n // ffmpeg logs errors to stderr; swallow in production, could log here if needed\n void data\n })\n\n this.process.on('error', (_err: Error) => {\n // process failed to spawn or crashed; could handle reconnect logic here\n })\n\n this.process.on('close', () => {\n if (!this.destroyed) {\n this.process = null\n }\n })\n }\n\n private killFfmpeg(): void {\n if (this.process) {\n try {\n this.process.kill('SIGKILL')\n } catch {\n // already dead\n }\n this.process = null\n }\n }\n\n private handleOutputData(chunk: Buffer): void {\n this.outputBuffer = Buffer.concat([this.outputBuffer, chunk])\n\n // Extract complete JPEG frames from the buffer\n let searchFrom = 0\n while (true) {\n const soiIndex = this.outputBuffer.indexOf(SOI, searchFrom)\n if (soiIndex === -1) break\n\n const eoiIndex = this.outputBuffer.indexOf(EOI, soiIndex + 2)\n if (eoiIndex === -1) break\n\n const frameEnd = eoiIndex + 2\n const jpegData = this.outputBuffer.slice(soiIndex, frameEnd)\n\n // Advance past the consumed frame\n searchFrom = frameEnd\n\n this.emitFrame(Buffer.from(jpegData))\n }\n\n // Keep only unprocessed tail\n if (searchFrom > 0) {\n this.outputBuffer = Buffer.from(this.outputBuffer.slice(searchFrom))\n }\n }\n\n private emitFrame(data: Buffer): void {\n const decodeStart = Date.now()\n\n if (!this.frameDropper.shouldKeep()) {\n this.droppedFrames++\n return\n }\n\n const decodeTime = Date.now() - decodeStart\n this.totalDecodeTimeMs += decodeTime\n this.decodeCount++\n this.outputFrames++\n\n const frame: DecodedFrame = {\n data,\n width: 0,\n height: 0,\n format: 'jpeg',\n timestamp: Date.now(),\n }\n\n for (const cb of this.frameCallbacks) {\n cb(frame)\n }\n }\n\n pushPacket(packet: EncodedPacket): void {\n if (this.destroyed || !this.process?.stdin) return\n this.inputPackets++\n try {\n this.process.stdin.write(packet.data)\n } catch {\n // stdin may be closed if ffmpeg crashed\n }\n }\n\n onFrame(callback: (frame: DecodedFrame) => void): Unsubscribe {\n this.frameCallbacks.add(callback)\n return () => {\n this.frameCallbacks.delete(callback)\n }\n }\n\n updateConfig(update: Partial<DecoderSessionConfig>): void {\n const needsRestart =\n (update.scale !== undefined && update.scale !== this.config.scale) ||\n (update.outputFormat !== undefined && update.outputFormat !== this.config.outputFormat) ||\n (update.codec !== undefined && update.codec !== this.config.codec)\n\n this.config = { ...this.config, ...update }\n\n if (update.maxFps !== undefined) {\n this.frameDropper.setMaxFps(update.maxFps)\n }\n\n if (needsRestart) {\n this.spawnFfmpeg()\n }\n }\n\n async destroy(): Promise<void> {\n if (this.destroyed) return\n this.destroyed = true\n this.killFfmpeg()\n this.frameCallbacks.clear()\n }\n\n getStats(): DecoderStats {\n const uptimeSec = Math.max((Date.now() - this.startTime) / 1000, 1)\n return {\n inputFps: this.inputPackets / uptimeSec,\n outputFps: this.outputFrames / uptimeSec,\n avgDecodeTimeMs: this.decodeCount > 0 ? this.totalDecodeTimeMs / this.decodeCount : 0,\n droppedFrames: this.droppedFrames,\n }\n }\n}\n","export class FrameDropper {\n private intervalMs: number\n private lastPassedAt = -Infinity\n\n constructor(maxFps: number) {\n this.intervalMs = maxFps > 0 ? 1000 / maxFps : 0\n }\n\n shouldKeep(): boolean {\n if (this.intervalMs === 0) return true\n\n const now = Date.now()\n if (now - this.lastPassedAt >= this.intervalMs) {\n this.lastPassedAt = now\n return true\n }\n return false\n }\n\n setMaxFps(maxFps: number): void {\n this.intervalMs = maxFps > 0 ? 1000 / maxFps : 0\n }\n}\n","import type { IDecoderProvider, DecoderSessionConfig, IDecoderSession } from '@camstack/types'\nimport { FfmpegDecoderSession } from './ffmpeg-decoder-session'\n\nconst SUPPORTED_CODECS = new Set(['h264', 'h265', 'hevc', 'mjpeg'])\n\nexport class FfmpegDecoderProvider implements IDecoderProvider {\n readonly id = 'ffmpeg'\n readonly name = 'FFmpeg Decoder'\n\n supportsCodec(codec: string): boolean {\n return SUPPORTED_CODECS.has(codec)\n }\n\n createSession(config: DecoderSessionConfig): IDecoderSession {\n return new FfmpegDecoderSession(config)\n }\n}\n","import { spawn, type ChildProcess } from 'node:child_process'\nimport type {\n IStreamBroker,\n StreamSource,\n EncodedPacket,\n DecodedFrame,\n DecodedAudioChunk,\n DecodeOptions,\n BrokerStatus,\n BrokerStats,\n Unsubscribe,\n IDecoderSession,\n IRestreamer,\n IScopedLogger,\n} from '@camstack/types'\nimport type { DecoderRegistry } from './decoder-registry'\nimport { FrameDropper } from './frame-dropper'\nimport { StreamPipeServer } from './stream-pipe-server'\n\nconst DEFAULT_MAX_FPS = 5\nconst DEFAULT_FORMAT = 'jpeg' as const\nconst DEFAULT_SCALE = 1\n\nconst INITIAL_RECONNECT_DELAY_MS = 1_000\nconst MAX_RECONNECT_DELAY_MS = 30_000\nconst DECODER_TEARDOWN_GRACE_MS = 2_000\n\n/** H.264 NAL unit type 5 = IDR frame (keyframe) */\nconst H264_IDR_NAL_TYPE = 5\n/** H.265 NAL unit types 16-21 are IRAP (keyframe) pictures */\nconst H265_IRAP_NAL_TYPE_MIN = 16\nconst H265_IRAP_NAL_TYPE_MAX = 21\n\ninterface DecodedSubscriber {\n readonly callback: (frame: DecodedFrame) => void\n readonly frameDropper: FrameDropper\n}\n\nexport class StreamBroker implements IStreamBroker {\n readonly deviceId: string\n\n private _status: BrokerStatus = 'idle'\n private source: StreamSource | undefined\n private decoderSession: IDecoderSession | null = null\n private decoderUnsubscribe: Unsubscribe | null = null\n private readonly decoderRegistry: DecoderRegistry\n private readonly logger: IScopedLogger | undefined\n private readonly startedAt: number = Date.now()\n\n private ffmpegProcess: ChildProcess | undefined\n private reconnectTimer: ReturnType<typeof setTimeout> | undefined\n private reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS\n private manualStop = false\n private stopping = false\n private decoderTeardownTimer: ReturnType<typeof setTimeout> | undefined\n\n private restreamers: readonly IRestreamer[] = []\n private readonly pipeServer: StreamPipeServer\n\n private readonly encodedCallbacks = new Set<(packet: EncodedPacket) => void>()\n private readonly decodedSubscribers = new Map<symbol, DecodedSubscriber>()\n private readonly audioChunkCallbacks = new Set<(chunk: DecodedAudioChunk) => void>()\n\n constructor(deviceId: string, decoderRegistry: DecoderRegistry, logger?: IScopedLogger) {\n this.deviceId = deviceId\n this.decoderRegistry = decoderRegistry\n this.logger = logger\n this.pipeServer = new StreamPipeServer(logger?.child('pipe-server'))\n }\n\n get status(): BrokerStatus {\n return this._status\n }\n\n setRestreamers(restreamers: readonly IRestreamer[]): void {\n this.restreamers = restreamers\n }\n\n async start(source: StreamSource): Promise<void> {\n this.source = source\n this.manualStop = false\n this.stopping = false\n this._status = 'connecting'\n\n await this.pipeServer.start()\n this.logger?.info(\n `Pipe server started for ${this.deviceId} at ${this.pipeServer.getUrl()}`,\n )\n\n if (source.type === 'rtsp') {\n this.startRtspReader(source)\n } else {\n // For non-RTSP sources (raw TCP, pipe), data is pushed externally\n this._status = 'streaming'\n }\n }\n\n async stop(): Promise<void> {\n this.stopping = true\n this.manualStop = true\n this._status = 'stopped'\n\n this.killFfmpeg()\n\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer)\n this.reconnectTimer = undefined\n }\n\n if (this.decoderTeardownTimer) {\n clearTimeout(this.decoderTeardownTimer)\n this.decoderTeardownTimer = undefined\n }\n\n if (this.decoderSession) {\n await this.destroyDecoder()\n }\n\n await this.pipeServer.stop()\n\n this.encodedCallbacks.clear()\n this.decodedSubscribers.clear()\n this.audioChunkCallbacks.clear()\n }\n\n pushEncodedPacket(packet: EncodedPacket): void {\n if (this.stopping) return\n\n for (const cb of this.encodedCallbacks) {\n cb(packet)\n }\n\n // Broadcast raw video bytes to TCP pipe clients (restreamers)\n if (packet.type === 'video') {\n this.pipeServer.broadcast(packet.data)\n }\n\n for (const restreamer of this.restreamers) {\n const streamId = `${this.deviceId}/video`\n restreamer.pushPacket(streamId, packet)\n }\n\n if (packet.type === 'video' && this.decoderSession) {\n this.decoderSession.pushPacket(packet)\n }\n }\n\n onEncodedData(callback: (packet: EncodedPacket) => void): Unsubscribe {\n this.encodedCallbacks.add(callback)\n\n return () => {\n this.encodedCallbacks.delete(callback)\n }\n }\n\n onDecodedFrame(callback: (frame: DecodedFrame) => void, options?: DecodeOptions): Unsubscribe {\n const maxFps = options?.maxFps ?? DEFAULT_MAX_FPS\n const subscriberId = Symbol('decoded-subscriber')\n const frameDropper = new FrameDropper(maxFps)\n\n const subscriber: DecodedSubscriber = { callback, frameDropper }\n this.decodedSubscribers.set(subscriberId, subscriber)\n\n // Cancel pending teardown if a new subscriber joins while grace period is active\n if (this.decoderTeardownTimer) {\n clearTimeout(this.decoderTeardownTimer)\n this.decoderTeardownTimer = undefined\n }\n\n // Create shared decoder on first subscriber (or reuse existing)\n if (!this.decoderSession && this.source) {\n this.createSharedDecoderSession(options)\n }\n\n return () => {\n this.decodedSubscribers.delete(subscriberId)\n\n // Schedule decoder teardown with grace period when last subscriber leaves\n if (this.decodedSubscribers.size === 0 && this.decoderSession) {\n this.decoderTeardownTimer = setTimeout(() => {\n this.decoderTeardownTimer = undefined\n if (this.decodedSubscribers.size === 0 && this.decoderSession) {\n this.destroyDecoder()\n }\n }, DECODER_TEARDOWN_GRACE_MS)\n }\n }\n }\n\n onDecodedAudioChunk(callback: (chunk: DecodedAudioChunk) => void): Unsubscribe {\n this.audioChunkCallbacks.add(callback)\n\n return () => {\n this.audioChunkCallbacks.delete(callback)\n }\n }\n\n getStats(): BrokerStats {\n const decoderStats = this.decoderSession?.getStats()\n\n return {\n status: this._status,\n inputFps: decoderStats?.inputFps ?? 0,\n decodeFps: decoderStats?.outputFps ?? 0,\n encodedSubscribers: this.encodedCallbacks.size,\n decodedSubscribers: this.decodedSubscribers.size,\n uptimeMs: Date.now() - this.startedAt,\n }\n }\n\n /**\n * Returns the local TCP URL that restreamers should connect to\n * instead of the camera's RTSP URL. This ensures the broker is\n * the sole reader from the camera.\n */\n getLocalStreamUrl(): string {\n return this.pipeServer.getUrl()\n }\n\n /** Returns the number of TCP pipe clients currently connected */\n getPipeClientCount(): number {\n return this.pipeServer.getClientCount()\n }\n\n private startRtspReader(source: StreamSource): void {\n const codec = source.videoCodec ?? 'h264'\n const isHevc = codec === 'h265' || codec === 'hevc'\n\n const args = [\n '-hide_banner',\n '-loglevel', 'error',\n '-rtsp_transport', 'tcp',\n '-i', source.url,\n '-c:v', 'copy',\n '-f', isHevc ? 'hevc' : 'h264',\n '-bsf:v', isHevc ? 'hevc_mp4toannexb' : 'h264_mp4toannexb',\n 'pipe:1',\n ]\n\n this.logger?.debug(`Spawning ffmpeg RTSP reader for ${this.deviceId}`, {\n url: source.url,\n codec,\n })\n\n const proc = spawn('ffmpeg', args, {\n stdio: ['pipe', 'pipe', 'pipe'],\n })\n this.ffmpegProcess = proc\n\n // Swallow EPIPE errors on stdin during shutdown\n proc.stdin?.on('error', () => {})\n\n proc.stdout?.on('data', (chunk: Buffer) => {\n if (this.stopping) return\n\n this._status = 'streaming'\n this.reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS\n\n const keyframe = isHevc\n ? isHevcKeyframe(chunk)\n : isH264Keyframe(chunk)\n\n const packet: EncodedPacket = {\n type: 'video',\n data: chunk,\n pts: Date.now(),\n dts: Date.now(),\n keyframe,\n codec,\n }\n\n this.pushEncodedPacket(packet)\n })\n\n proc.stderr?.on('data', (data: Buffer) => {\n const msg = data.toString().trim()\n if (msg) {\n this.logger?.warn(`ffmpeg RTSP reader stderr [${this.deviceId}]: ${msg}`)\n }\n })\n\n proc.on('exit', (code, signal) => {\n this.ffmpegProcess = undefined\n\n if (this.manualStop) {\n return\n }\n\n this.logger?.warn(`ffmpeg RTSP reader exited for ${this.deviceId}`, {\n code,\n signal,\n })\n\n this._status = 'error'\n this.scheduleReconnect()\n })\n\n proc.on('error', (err) => {\n this.ffmpegProcess = undefined\n\n if (this.manualStop) {\n return\n }\n\n this.logger?.error(`ffmpeg RTSP reader error for ${this.deviceId}: ${err.message}`)\n this._status = 'error'\n this.scheduleReconnect()\n })\n }\n\n private scheduleReconnect(): void {\n if (this.manualStop || !this.source) {\n return\n }\n\n this.logger?.info(\n `Reconnecting RTSP reader for ${this.deviceId} in ${this.reconnectDelayMs}ms`,\n )\n\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = undefined\n if (!this.manualStop && this.source) {\n this._status = 'connecting'\n this.startRtspReader(this.source)\n }\n }, this.reconnectDelayMs)\n\n this.reconnectDelayMs = Math.min(\n this.reconnectDelayMs * 2,\n MAX_RECONNECT_DELAY_MS,\n )\n }\n\n private destroyDecoder(): Promise<void> {\n if (this.decoderUnsubscribe) {\n this.decoderUnsubscribe()\n this.decoderUnsubscribe = null\n }\n const session = this.decoderSession\n this.decoderSession = null\n return session?.destroy() ?? Promise.resolve()\n }\n\n private killFfmpeg(): void {\n if (this.ffmpegProcess) {\n this.ffmpegProcess.kill('SIGTERM')\n this.ffmpegProcess = undefined\n }\n }\n\n private createSharedDecoderSession(options?: DecodeOptions): void {\n const codec = this.source?.videoCodec ?? 'h264'\n const provider = this.decoderRegistry.getDecoder(codec)\n\n if (!provider) {\n return\n }\n\n // Shared decoder runs at unlimited FPS — each subscriber throttles locally\n const config = {\n codec,\n maxFps: 0,\n outputFormat: options?.format ?? DEFAULT_FORMAT,\n scale: options?.scale ?? DEFAULT_SCALE,\n }\n\n this.decoderSession = provider.createSession(config)\n\n // Single onFrame listener that fans out to all subscribers with per-subscriber throttling\n this.decoderUnsubscribe = this.decoderSession.onFrame((frame: DecodedFrame) => {\n for (const subscriber of this.decodedSubscribers.values()) {\n if (subscriber.frameDropper.shouldKeep()) {\n subscriber.callback(frame)\n }\n }\n })\n }\n}\n\n/**\n * Detect H.264 keyframe by scanning for IDR NAL unit (type 5)\n * in an Annex-B bitstream chunk.\n */\nfunction isH264Keyframe(data: Buffer): boolean {\n for (let i = 0; i < data.length - 4; i++) {\n // Look for start code: 0x00 0x00 0x01 or 0x00 0x00 0x00 0x01\n if (data[i] === 0 && data[i + 1] === 0) {\n let nalStart = -1\n if (data[i + 2] === 1) {\n nalStart = i + 3\n } else if (data[i + 2] === 0 && data[i + 3] === 1) {\n nalStart = i + 4\n }\n if (nalStart >= 0 && nalStart < data.length) {\n const nalType = data[nalStart]! & 0x1f\n if (nalType === H264_IDR_NAL_TYPE) {\n return true\n }\n }\n }\n }\n return false\n}\n\n/**\n * Detect H.265/HEVC keyframe by scanning for IRAP NAL units (types 16-21)\n * in an Annex-B bitstream chunk.\n */\nfunction isHevcKeyframe(data: Buffer): boolean {\n for (let i = 0; i < data.length - 5; i++) {\n if (data[i] === 0 && data[i + 1] === 0) {\n let nalStart = -1\n if (data[i + 2] === 1) {\n nalStart = i + 3\n } else if (data[i + 2] === 0 && data[i + 3] === 1) {\n nalStart = i + 4\n }\n if (nalStart >= 0 && nalStart < data.length) {\n const nalType = (data[nalStart]! >> 1) & 0x3f\n if (nalType >= H265_IRAP_NAL_TYPE_MIN && nalType <= H265_IRAP_NAL_TYPE_MAX) {\n return true\n }\n }\n }\n }\n return false\n}\n","import net from 'node:net'\nimport type { IScopedLogger } from '@camstack/types'\n\n/**\n * A lightweight TCP server that broadcasts raw encoded video bytes\n * (H.264/H.265 Annex-B) to all connected clients.\n *\n * Each StreamBroker owns one StreamPipeServer. Restreamers (e.g. go2rtc)\n * connect via `tcp://127.0.0.1:{port}` and receive the same byte stream\n * that the broker reads from the camera, eliminating duplicate RTSP\n * connections.\n */\nexport class StreamPipeServer {\n private readonly server: net.Server\n private readonly clients: Set<net.Socket> = new Set()\n private port = 0\n private started = false\n private readonly logger: IScopedLogger | undefined\n\n constructor(logger?: IScopedLogger) {\n this.logger = logger\n this.server = net.createServer((socket) => {\n this.handleConnection(socket)\n })\n\n this.server.on('error', (err) => {\n this.logger?.error(`StreamPipeServer error: ${err.message}`)\n })\n }\n\n async start(): Promise<void> {\n if (this.started) {\n return\n }\n\n await new Promise<void>((resolve, reject) => {\n this.server.once('error', reject)\n this.server.listen(0, '127.0.0.1', () => {\n this.server.removeListener('error', reject)\n const address = this.server.address() as net.AddressInfo\n this.port = address.port\n this.started = true\n this.logger?.debug(`StreamPipeServer listening on port ${this.port}`)\n resolve()\n })\n })\n }\n\n /**\n * Broadcast raw encoded bytes to all connected clients.\n * Silently drops data for clients that are slow or disconnected.\n */\n broadcast(data: Buffer): void {\n for (const client of this.clients) {\n if (client.destroyed) {\n this.clients.delete(client)\n continue\n }\n\n client.write(data, (err) => {\n if (err) {\n this.logger?.debug(\n `StreamPipeServer write error, removing client: ${err.message}`,\n )\n this.removeClient(client)\n }\n })\n }\n }\n\n getPort(): number {\n return this.port\n }\n\n getUrl(): string {\n return `tcp://127.0.0.1:${this.port}`\n }\n\n getClientCount(): number {\n return this.clients.size\n }\n\n isStarted(): boolean {\n return this.started\n }\n\n async stop(): Promise<void> {\n if (!this.started) {\n return\n }\n\n this.started = false\n\n for (const client of this.clients) {\n client.destroy()\n }\n this.clients.clear()\n\n await new Promise<void>((resolve) => {\n this.server.close(() => {\n this.logger?.debug('StreamPipeServer stopped')\n resolve()\n })\n })\n }\n\n private handleConnection(socket: net.Socket): void {\n this.clients.add(socket)\n this.logger?.debug(\n `StreamPipeServer client connected (total: ${this.clients.size})`,\n )\n\n socket.on('close', () => {\n this.removeClient(socket)\n })\n\n socket.on('error', () => {\n this.removeClient(socket)\n })\n }\n\n private removeClient(socket: net.Socket): void {\n const existed = this.clients.delete(socket)\n if (existed) {\n this.logger?.debug(\n `StreamPipeServer client disconnected (total: ${this.clients.size})`,\n )\n }\n if (!socket.destroyed) {\n socket.destroy()\n }\n }\n}\n","import type { StreamSource, IRestreamer, IScopedLogger } from '@camstack/types'\nimport { StreamBroker } from './stream-broker'\nimport type { DecoderRegistry } from './decoder-registry'\n\nexport class StreamBrokerManager {\n private readonly brokers = new Map<string, StreamBroker>()\n private restreamers: readonly IRestreamer[] = []\n private readonly logger: IScopedLogger\n\n constructor(\n private readonly decoderRegistry: DecoderRegistry,\n logger: IScopedLogger,\n ) {\n this.logger = logger\n }\n\n setRestreamers(restreamers: readonly IRestreamer[]): void {\n this.restreamers = restreamers\n\n for (const broker of this.brokers.values()) {\n broker.setRestreamers(restreamers)\n }\n\n this.logger.info(`Updated restreamers (count: ${restreamers.length})`)\n }\n\n getRestreamers(): readonly IRestreamer[] {\n return this.restreamers\n }\n\n /**\n * Create and start a broker.\n * brokerId can be a simple deviceId (backward compat) or a compound key like `${deviceId}/${streamId}`.\n */\n async createBroker(brokerId: string, source: StreamSource): Promise<StreamBroker> {\n const brokerLogger = this.logger.child(`broker:${brokerId}`)\n const broker = new StreamBroker(brokerId, this.decoderRegistry, brokerLogger)\n broker.setRestreamers(this.restreamers)\n await broker.start(source)\n this.brokers.set(brokerId, broker)\n return broker\n }\n\n getBroker(brokerId: string): StreamBroker | undefined {\n return this.brokers.get(brokerId)\n }\n\n async destroyBroker(brokerId: string): Promise<void> {\n const broker = this.brokers.get(brokerId)\n\n if (broker) {\n await broker.stop()\n this.brokers.delete(brokerId)\n }\n }\n\n /** Destroy all brokers matching a prefix (e.g., `${deviceId}/` to destroy all streams for a device) */\n async destroyBrokersForDevice(deviceId: string): Promise<void> {\n const prefix = `${deviceId}/`\n const toDestroy: string[] = []\n\n for (const brokerId of this.brokers.keys()) {\n if (brokerId === deviceId || brokerId.startsWith(prefix)) {\n toDestroy.push(brokerId)\n }\n }\n\n await Promise.all(toDestroy.map((id) => this.destroyBroker(id)))\n }\n\n listBrokers(): readonly StreamBroker[] {\n return [...this.brokers.values()]\n }\n\n async destroyAll(): Promise<void> {\n const stopPromises = [...this.brokers.values()].map((broker) => broker.stop())\n await Promise.all(stopPromises)\n this.brokers.clear()\n }\n}\n","/**\n * Event Persistence Service — unified persistence for detection events.\n *\n * A detection event is ONE entity with:\n * - Metadata (DB): id, timestamp, device, class, score, severity, description\n * - Media (filesystem): original frame, annotated frame, crops, faces, plates\n *\n * Both are stored together, flushed together, and deleted together by retention.\n *\n * Buffered I/O prevents filesystem flooding:\n * - Events buffered, flushed every 5s or at 50 events\n * - Media buffered, flushed every 3s or at 50MB\n * - High-priority media (thumbnails) flushed first\n */\n\nimport type { IScopedLogger, IStorageLocation, SystemEvent, VideoFrame } from '@camstack/types'\n\n/**\n * Inlined from the removed stages/object-snapshot-stage.ts.\n * These types are used by the persistence layer only.\n */\nexport interface ObjectSnapshotResult {\n readonly trackId: string\n readonly thumbnail?: Buffer\n readonly fullCrop?: Buffer\n}\n\nexport interface AnnotatedSnapshotResult {\n readonly thumbnail: Buffer\n readonly full: Buffer\n}\n\n// --- Types ---\n\nexport interface PersistableEvent {\n readonly id: string\n readonly timestamp: number\n readonly deviceId: string\n readonly category: string\n readonly className: string\n readonly score: number\n readonly trackId: string\n readonly severity: string\n readonly description: string\n readonly data: Record<string, unknown>\n /** IDs of associated media files (for cleanup) */\n readonly mediaFiles: readonly string[]\n}\n\ninterface MediaEntry {\n readonly path: string\n readonly data: Buffer\n readonly priority: 'high' | 'normal'\n readonly eventId: string\n}\n\nexport interface EventBufferStatus {\n readonly eventCount: number\n readonly mediaCount: number\n readonly mediaSizeMB: number\n}\n\nexport type TrackMediaType =\n | 'crop-thumb'\n | 'crop-full'\n | 'debug-annotated-thumb'\n | 'debug-annotated-full'\n | 'original'\n | 'original-thumb'\n | 'inline-crop'\n | 'unknown'\n\nexport interface TrackMediaFile {\n readonly path: string\n readonly type: TrackMediaType\n readonly data: Buffer\n /** Where the data was read from */\n readonly source: 'buffer' | 'storage'\n}\n\nfunction classifyMediaPath(path: string): TrackMediaType {\n if (path.endsWith('-thumb.jpg') && path.includes('/crops/')) return 'crop-thumb'\n if (path.endsWith('-full.jpg') && path.includes('/crops/')) return 'crop-full'\n if (path.includes('debug-annotated-thumb.jpg')) return 'debug-annotated-thumb'\n if (path.includes('debug-annotated-full.jpg')) return 'debug-annotated-full'\n if (path.includes('original-thumb.jpg')) return 'original-thumb'\n if (path.includes('original.jpg')) return 'original'\n if (path.includes('crop.jpg')) return 'inline-crop'\n return 'unknown'\n}\n\n// --- Dependencies ---\n\nexport interface EventPersistenceDeps {\n readonly getStorageLocation: (name: string) => IStorageLocation\n readonly subscribe: (filter: { category?: string }, handler: (event: SystemEvent) => void) => () => void\n readonly logger: IScopedLogger\n}\n\n// --- Service ---\n\nexport class EventPersistenceService {\n private eventBuffer: PersistableEvent[] = []\n private mediaBuffer: MediaEntry[] = []\n\n private eventFlushTimer?: ReturnType<typeof setInterval>\n private mediaFlushTimer?: ReturnType<typeof setInterval>\n\n private readonly EVENT_FLUSH_INTERVAL_MS = 5000\n private readonly MEDIA_FLUSH_INTERVAL_MS = 3000\n private readonly MAX_EVENT_BUFFER = 50\n private readonly MAX_MEDIA_BUFFER_MB = 50\n\n private readonly logger: IScopedLogger\n private readonly getStorageLocation: (name: string) => IStorageLocation\n private unsubscribe?: () => void\n\n constructor(private readonly deps: EventPersistenceDeps) {\n this.logger = deps.logger\n this.getStorageLocation = deps.getStorageLocation\n }\n\n start(): void {\n this.unsubscribe = this.deps.subscribe(\n { category: 'detection.event' },\n (event) => this.bufferEvent(event),\n )\n\n this.eventFlushTimer = setInterval(() => {\n this.flushEvents().catch(e => this.logger.warn(`Event flush failed: ${e}`))\n }, this.EVENT_FLUSH_INTERVAL_MS)\n\n this.mediaFlushTimer = setInterval(() => {\n this.flushMedia().catch(e => this.logger.warn(`Media flush failed: ${e}`))\n }, this.MEDIA_FLUSH_INTERVAL_MS)\n }\n\n stop(): void {\n this.unsubscribe?.()\n clearInterval(this.eventFlushTimer)\n clearInterval(this.mediaFlushTimer)\n this.flushEvents().catch(() => {})\n this.flushMedia().catch(() => {})\n }\n\n // --- Public: save media associated with an event ---\n\n /** Save object crops (thumbnail + full-res) for an event */\n saveDetectionCrops(eventId: string, crops: readonly ObjectSnapshotResult[]): void {\n for (const crop of crops) {\n if (crop.thumbnail) {\n this.bufferMedia(eventId, `events/${eventId}/crops/${crop.trackId}-thumb.jpg`, crop.thumbnail, 'high')\n }\n if (crop.fullCrop) {\n this.bufferMedia(eventId, `events/${eventId}/crops/${crop.trackId}-full.jpg`, crop.fullCrop, 'normal')\n }\n }\n }\n\n /** Save annotated frame (thumbnail + full-res) for an event */\n saveAnnotatedFrame(eventId: string, result: AnnotatedSnapshotResult): void {\n this.bufferMedia(eventId, `events/${eventId}/debug-annotated-thumb.jpg`, result.thumbnail, 'high')\n this.bufferMedia(eventId, `events/${eventId}/debug-annotated-full.jpg`, result.full, 'normal')\n }\n\n /** Save original frame for an event */\n async saveOriginalFrame(eventId: string, frame: VideoFrame): Promise<void> {\n const sharp = (await import('sharp')).default\n\n const jpeg = await sharp(frame.data, {\n raw: { width: frame.width, height: frame.height, channels: 3 },\n }).jpeg({ quality: 85 }).toBuffer()\n\n this.bufferMedia(eventId, `events/${eventId}/original.jpg`, jpeg, 'normal')\n\n const thumb = await sharp(jpeg)\n .resize(320, 240, { fit: 'inside' })\n .jpeg({ quality: 75 })\n .toBuffer()\n\n this.bufferMedia(eventId, `events/${eventId}/original-thumb.jpg`, thumb, 'high')\n }\n\n // --- Public: query track media ---\n\n /**\n * Get all media files for a specific event (from buffer or storage).\n * Returns immediately — serves from in-memory buffer if not yet flushed.\n */\n async getEventMedia(eventId: string): Promise<readonly TrackMediaFile[]> {\n const results: TrackMediaFile[] = []\n\n // 1. Check in-memory buffer first (not yet flushed)\n for (const entry of this.mediaBuffer) {\n if (entry.eventId === eventId) {\n results.push({\n path: entry.path,\n type: classifyMediaPath(entry.path),\n data: entry.data,\n source: 'buffer',\n })\n }\n }\n\n // 2. Check storage (already flushed)\n try {\n const location = this.getStorageLocation('events')\n if (location.files) {\n const files = await location.files.listFiles(`events/${eventId}/`)\n for (const file of files) {\n // Skip if already served from buffer\n if (results.some(r => r.path === file)) continue\n try {\n const data = await location.files.readFile(file)\n results.push({\n path: file,\n type: classifyMediaPath(file),\n data,\n source: 'storage',\n })\n } catch {\n // File may have been deleted by retention\n }\n }\n }\n } catch {\n // Storage not available\n }\n\n return results\n }\n\n /**\n * Get all media for a specific track across all events.\n * Useful for building a track timeline with all snapshots.\n */\n async getTrackMedia(trackId: string): Promise<readonly TrackMediaFile[]> {\n const results: TrackMediaFile[] = []\n\n // 1. Buffer: find media matching trackId in path\n for (const entry of this.mediaBuffer) {\n if (entry.path.includes(trackId)) {\n results.push({\n path: entry.path,\n type: classifyMediaPath(entry.path),\n data: entry.data,\n source: 'buffer',\n })\n }\n }\n\n // 2. Find events for this trackId in buffer\n const bufferedEventIds = this.eventBuffer\n .filter(e => e.trackId === trackId)\n .map(e => e.id)\n\n // 3. Find events for this trackId in storage\n try {\n const location = this.getStorageLocation('events')\n if (location.structured) {\n const stored = await location.structured.query('detection_events', {\n where: { trackId },\n orderBy: { field: 'timestamp', direction: 'asc' },\n limit: 100,\n })\n\n const allEventIds = new Set([\n ...bufferedEventIds,\n ...stored.map(r => r.id),\n ])\n\n // Get media for each event\n for (const eventId of allEventIds) {\n const eventMedia = await this.getEventMedia(eventId)\n for (const media of eventMedia) {\n if (!results.some(r => r.path === media.path)) {\n results.push(media)\n }\n }\n }\n }\n } catch {\n // Storage not available — serve from buffer only\n }\n\n return results\n }\n\n /**\n * Get all media for a device within a time range.\n */\n async getDeviceMedia(\n deviceId: string,\n since?: number,\n until?: number,\n ): Promise<readonly TrackMediaFile[]> {\n const results: TrackMediaFile[] = []\n\n try {\n const location = this.getStorageLocation('events')\n if (!location.structured) return results\n\n const events = await location.structured.query('detection_events', {\n where: { deviceId },\n whereBetween: since || until\n ? { timestamp: [since ?? 0, until ?? Date.now()] }\n : undefined,\n orderBy: { field: 'timestamp', direction: 'desc' },\n limit: 50,\n })\n\n for (const event of events) {\n const media = await this.getEventMedia(event.id)\n results.push(...media)\n }\n } catch {\n // Storage not available\n }\n\n return results\n }\n\n // --- Public: status ---\n\n getBufferStatus(): EventBufferStatus {\n return {\n eventCount: this.eventBuffer.length,\n mediaCount: this.mediaBuffer.length,\n mediaSizeMB: this.mediaBuffer.reduce((sum, m) => sum + m.data.length, 0) / (1024 * 1024),\n }\n }\n\n /** Force flush everything */\n async flush(): Promise<void> {\n await this.flushEvents()\n await this.flushMedia()\n }\n\n // --- Internal: buffering ---\n\n private bufferEvent(event: SystemEvent): void {\n // Collect paths of any media being buffered for this event\n const mediaPaths = this.mediaBuffer\n .filter(m => m.eventId === event.id)\n .map(m => m.path)\n\n const persistable: PersistableEvent = {\n id: event.id,\n timestamp: Date.now(),\n deviceId: (event.data.deviceId as string) ?? '',\n category: event.category,\n className: (event.data.className as string) ?? '',\n score: (event.data.score as number) ?? 0,\n trackId: (event.data.trackId as string) ?? '',\n severity: (event.data.severity as string) ?? '',\n description: (event.data.description as string) ?? '',\n data: event.data,\n mediaFiles: mediaPaths,\n }\n\n this.eventBuffer.push(persistable)\n\n // Buffer inline crop if present\n const detection = event.data.detection as Record<string, unknown> | undefined\n if (detection?.crop && Buffer.isBuffer(detection.crop)) {\n this.bufferMedia(event.id, `events/${event.id}/crop.jpg`, detection.crop as Buffer, 'high')\n }\n\n if (this.eventBuffer.length >= this.MAX_EVENT_BUFFER) {\n this.flushEvents().catch(e => this.logger.warn(`Auto-flush failed: ${e}`))\n }\n }\n\n private bufferMedia(eventId: string, path: string, data: Buffer, priority: 'high' | 'normal'): void {\n this.mediaBuffer.push({ eventId, path, data, priority })\n\n const totalMB = this.mediaBuffer.reduce((sum, m) => sum + m.data.length, 0) / (1024 * 1024)\n if (totalMB >= this.MAX_MEDIA_BUFFER_MB) {\n this.flushMedia().catch(e => this.logger.warn(`Media overflow flush failed: ${e}`))\n }\n }\n\n // --- Internal: flushing ---\n\n private async flushEvents(): Promise<void> {\n if (this.eventBuffer.length === 0) return\n\n const events = [...this.eventBuffer]\n this.eventBuffer = []\n\n try {\n const location = this.getStorageLocation('events')\n if (location.structured) {\n for (const event of events) {\n await location.structured.insert({\n collection: 'detection_events',\n id: event.id,\n data: event as unknown as Record<string, unknown>,\n })\n }\n this.logger.debug(`Flushed ${events.length} events`)\n }\n } catch (err) {\n this.logger.warn(`Failed to flush events: ${err}`)\n if (this.eventBuffer.length < this.MAX_EVENT_BUFFER * 2) {\n this.eventBuffer.push(...events)\n }\n }\n }\n\n private async flushMedia(): Promise<void> {\n if (this.mediaBuffer.length === 0) return\n\n // Sort: high priority first\n const sorted = [...this.mediaBuffer].sort((a, b) => {\n if (a.priority === 'high' && b.priority !== 'high') return -1\n if (a.priority !== 'high' && b.priority === 'high') return 1\n return 0\n })\n this.mediaBuffer = []\n\n try {\n const location = this.getStorageLocation('events')\n if (location.files) {\n for (const entry of sorted) {\n await location.files.writeFile(entry.path, entry.data)\n }\n const totalMB = sorted.reduce((s, e) => s + e.data.length, 0) / (1024 * 1024)\n this.logger.debug(`Flushed ${sorted.length} media files (${totalMB.toFixed(1)}MB)`)\n }\n } catch (err) {\n this.logger.warn(`Failed to flush media: ${err}`)\n }\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport type { IScopedLogger, IStructuredStorage } from '@camstack/types'\n\n/**\n * Inlined from the removed sub-detectors/clip-recognizer.ts.\n * ClipRecognizer interface used by this service for embedding generation.\n */\nexport interface ClipRecognizer {\n getEmbedding(imageBuffer: Buffer): Promise<Float32Array>\n}\n\n/** Cosine similarity between two embedding vectors */\nexport function cosineSimilarity(a: Float32Array, b: Float32Array): number {\n if (a.length !== b.length) return 0\n let dotProduct = 0\n let normA = 0\n let normB = 0\n for (let i = 0; i < a.length; i++) {\n dotProduct += a[i]! * b[i]!\n normA += a[i]! * a[i]!\n normB += b[i]! * b[i]!\n }\n const denom = Math.sqrt(normA) * Math.sqrt(normB)\n return denom === 0 ? 0 : dotProduct / denom\n}\n\nexport interface KnownFaceEntry {\n readonly id: string\n readonly label: string\n readonly group?: string\n readonly embedding: readonly number[]\n readonly cropBase64: string\n readonly createdAt: number\n readonly updatedAt: number\n readonly source?: string\n readonly metadata?: Readonly<Record<string, unknown>>\n}\n\nexport interface KnownFacesDeps {\n readonly getStructuredStorage: () => IStructuredStorage\n readonly logger: IScopedLogger\n}\n\nconst COLLECTION = 'known_faces'\n\nexport class KnownFacesService {\n private readonly logger: IScopedLogger\n private readonly getStructuredStorage: () => IStructuredStorage\n\n constructor(deps: KnownFacesDeps) {\n this.logger = deps.logger\n this.getStructuredStorage = deps.getStructuredStorage\n }\n\n private getStorage(): IStructuredStorage {\n return this.getStructuredStorage()\n }\n\n /** Register a known face */\n async register(entry: KnownFaceEntry): Promise<void> {\n const storage = this.getStorage()\n await storage.insert({\n collection: COLLECTION,\n id: entry.id,\n data: {\n label: entry.label,\n group: entry.group ?? null,\n embedding: JSON.stringify(Array.from(entry.embedding)),\n cropBase64: entry.cropBase64,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n source: entry.source ?? null,\n metadata: entry.metadata ? JSON.stringify(entry.metadata) : null,\n },\n })\n this.logger.info(`Registered known face: ${entry.label} (${entry.id})`)\n }\n\n /** Get all known faces */\n async listAll(): Promise<readonly KnownFaceEntry[]> {\n const storage = this.getStorage()\n const records = await storage.query(COLLECTION)\n return records.map((r) => recordToEntry(r.id, r.data))\n }\n\n /** Delete a known face */\n async delete(id: string): Promise<void> {\n const storage = this.getStorage()\n await storage.delete(COLLECTION, id)\n this.logger.info(`Deleted known face: ${id}`)\n }\n\n /** Update a known face (reassign label or group) */\n async update(id: string, updates: Partial<Pick<KnownFaceEntry, 'label' | 'group'>>): Promise<void> {\n const storage = this.getStorage()\n const data: Record<string, unknown> = { updatedAt: Date.now() }\n if (updates.label !== undefined) data.label = updates.label\n if (updates.group !== undefined) data.group = updates.group\n await storage.update(COLLECTION, id, data)\n this.logger.info(`Updated known face ${id}: ${JSON.stringify(updates)}`)\n }\n\n /** Recalculate embedding for a face (re-run CLIP on the stored crop) */\n async recalculateEmbedding(id: string, clipRecognizer: ClipRecognizer): Promise<void> {\n const storage = this.getStorage()\n const records = await storage.query(COLLECTION, { where: { id } })\n if (records.length === 0) throw new Error(`Known face not found: ${id}`)\n\n const record = records[0]!\n const cropBase64 = record.data.cropBase64 as string\n const cropBuffer = Buffer.from(cropBase64, 'base64')\n\n const embedding = await clipRecognizer.getEmbedding(cropBuffer)\n await storage.update(COLLECTION, id, {\n embedding: JSON.stringify(Array.from(embedding)),\n updatedAt: Date.now(),\n })\n this.logger.info(`Recalculated embedding for known face: ${id}`)\n }\n\n /** Find best match for an embedding */\n async findMatch(\n embedding: Float32Array,\n threshold: number,\n ): Promise<{ readonly entry: KnownFaceEntry; readonly similarity: number } | null> {\n const entries = await this.listAll()\n let bestEntry: KnownFaceEntry | null = null\n let bestSimilarity = -1\n\n for (const entry of entries) {\n const knownEmbedding = new Float32Array(entry.embedding)\n const similarity = cosineSimilarity(embedding, knownEmbedding)\n if (similarity > bestSimilarity) {\n bestSimilarity = similarity\n bestEntry = entry\n }\n }\n\n if (bestEntry && bestSimilarity >= threshold) {\n return { entry: bestEntry, similarity: bestSimilarity }\n }\n\n return null\n }\n\n /** Batch import faces from crops */\n async batchRegister(\n entries: ReadonlyArray<{ readonly label: string; readonly cropBase64: string; readonly group?: string }>,\n clipRecognizer: ClipRecognizer,\n ): Promise<readonly KnownFaceEntry[]> {\n const results: KnownFaceEntry[] = []\n for (const input of entries) {\n const cropBuffer = Buffer.from(input.cropBase64, 'base64')\n const embedding = await clipRecognizer.getEmbedding(cropBuffer)\n const now = Date.now()\n const entry: KnownFaceEntry = {\n id: randomUUID(),\n label: input.label,\n group: input.group,\n embedding: Array.from(embedding),\n cropBase64: input.cropBase64,\n createdAt: now,\n updatedAt: now,\n }\n await this.register(entry)\n results.push(entry)\n }\n this.logger.info(`Batch registered ${results.length} known faces`)\n return results\n }\n}\n\nfunction recordToEntry(id: string, data: Record<string, unknown>): KnownFaceEntry {\n return {\n id,\n label: data.label as string,\n group: (data.group as string) ?? undefined,\n embedding: JSON.parse(data.embedding as string) as number[],\n cropBase64: data.cropBase64 as string,\n createdAt: data.createdAt as number,\n updatedAt: data.updatedAt as number,\n source: (data.source as string) ?? undefined,\n metadata: data.metadata ? JSON.parse(data.metadata as string) : undefined,\n }\n}\n","/**\n * Session Tracker Service — maintains persistent object tracking across\n * multiple analysis pipeline runs, linking tracked objects to their\n * CLIP embeddings for re-identification across cameras.\n */\n\nimport { randomUUID } from 'node:crypto'\nimport type { IScopedLogger, IEventBus, AnalysisContext, ServerTrackedDetection as TrackedDetection } from '@camstack/types'\n\nexport interface SessionTrack {\n readonly trackId: string\n readonly deviceId: string\n readonly className: string\n readonly label?: string\n readonly firstSeen: number\n readonly lastSeen: number\n readonly totalFrames: number\n readonly lastDetection: TrackedDetection\n readonly state: string\n readonly positions: ReadonlyArray<{ readonly x: number; readonly y: number; readonly t: number }>\n readonly embedding?: Float32Array\n readonly globalId?: string\n readonly bestCrop?: Buffer\n}\n\nexport interface GlobalIdentity {\n readonly globalId: string\n readonly embedding: Float32Array\n readonly label?: string\n readonly firstSeen: number\n readonly lastSeen: number\n readonly deviceIds: ReadonlyArray<string>\n}\n\n/** Max positions to keep per track (~5 min at 1fps) */\nconst MAX_POSITIONS = 300\n\n/** Tracks not seen for this many ms are expired */\nconst TRACK_EXPIRY_MS = 30_000\n\nexport interface SessionTrackerDeps {\n readonly eventBus: IEventBus\n readonly logger: IScopedLogger\n}\n\nexport class SessionTrackerService {\n /** Active tracks per device: deviceId -> Map<trackId, SessionTrack> */\n private activeTracks: Map<string, Map<string, SessionTrack>> = new Map()\n\n /** Global re-id pool: globalId -> GlobalIdentity (same person across cameras) */\n private globalPool: Map<string, GlobalIdentity> = new Map()\n\n private readonly logger: IScopedLogger\n private readonly eventBus: IEventBus\n\n constructor(deps: SessionTrackerDeps) {\n this.logger = deps.logger\n this.eventBus = deps.eventBus\n }\n\n /** Update tracks from an analysis result */\n updateFromAnalysis(deviceId: string, ctx: AnalysisContext): void {\n const deviceTracks = this.getDeviceTracks(deviceId)\n const now = ctx.timestamp\n\n for (const td of ctx.trackedDetections) {\n const existing = deviceTracks.get(td.trackId)\n\n if (existing) {\n // Update existing track — create new object (immutable)\n const newPositions = [\n ...existing.positions,\n { x: td.detection.boundingBox?.[0] ?? 0, y: td.detection.boundingBox?.[1] ?? 0, t: now },\n ]\n const trimmedPositions = newPositions.length > MAX_POSITIONS\n ? newPositions.slice(newPositions.length - MAX_POSITIONS)\n : newPositions\n\n const updatedEmbedding = (td.recognitions.length > 0 && td.recognitions[0]?.embedding)\n ? new Float32Array(td.recognitions[0].embedding)\n : existing.embedding\n\n const updated: SessionTrack = {\n ...existing,\n lastSeen: now,\n totalFrames: existing.totalFrames + 1,\n lastDetection: td,\n state: td.tracking.state,\n positions: trimmedPositions,\n embedding: updatedEmbedding,\n }\n deviceTracks.set(td.trackId, updated)\n } else {\n // New track\n const track: SessionTrack = {\n trackId: td.trackId,\n deviceId,\n className: td.detection.className,\n label: td.detection.label,\n firstSeen: now,\n lastSeen: now,\n totalFrames: 1,\n lastDetection: td,\n state: td.tracking.state,\n positions: [{ x: td.detection.boundingBox?.[0] ?? 0, y: td.detection.boundingBox?.[1] ?? 0, t: now }],\n embedding: td.recognitions[0]?.embedding ? new Float32Array(td.recognitions[0].embedding) : undefined,\n globalId: undefined,\n bestCrop: td.crop,\n }\n\n deviceTracks.set(td.trackId, track)\n\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'core', id: 'session-tracker' },\n category: 'session.track.new',\n data: { deviceId, trackId: td.trackId, className: td.detection.className },\n })\n }\n }\n\n // Expire old tracks (not seen for TRACK_EXPIRY_MS)\n for (const [trackId, track] of deviceTracks) {\n if (now - track.lastSeen > TRACK_EXPIRY_MS) {\n deviceTracks.delete(trackId)\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'core', id: 'session-tracker' },\n category: 'session.track.expired',\n data: { deviceId, trackId, className: track.className, duration: now - track.firstSeen },\n })\n }\n }\n }\n\n /** Get all active tracks for a device */\n getActiveTracks(deviceId: string): readonly SessionTrack[] {\n return [...(this.activeTracks.get(deviceId)?.values() ?? [])]\n }\n\n /** Get all active tracks across all devices */\n getAllActiveTracks(): readonly SessionTrack[] {\n const all: SessionTrack[] = []\n for (const tracks of this.activeTracks.values()) {\n all.push(...tracks.values())\n }\n return all\n }\n\n /** Get a specific track */\n getTrack(deviceId: string, trackId: string): SessionTrack | null {\n return this.activeTracks.get(deviceId)?.get(trackId) ?? null\n }\n\n /** Get track count per device */\n getTrackCounts(): Record<string, { total: number; byClass: Record<string, number> }> {\n const counts: Record<string, { total: number; byClass: Record<string, number> }> = {}\n for (const [deviceId, tracks] of this.activeTracks) {\n const byClass: Record<string, number> = {}\n for (const track of tracks.values()) {\n byClass[track.className] = (byClass[track.className] ?? 0) + 1\n }\n counts[deviceId] = { total: tracks.size, byClass }\n }\n return counts\n }\n\n /** Clear all tracks for a device */\n clearDevice(deviceId: string): void {\n this.activeTracks.delete(deviceId)\n }\n\n /** Clear all tracks */\n clearAll(): void {\n this.activeTracks.clear()\n this.globalPool.clear()\n }\n\n /** Get the global identity pool (for diagnostics) */\n getGlobalPool(): ReadonlyMap<string, GlobalIdentity> {\n return this.globalPool\n }\n\n private getDeviceTracks(deviceId: string): Map<string, SessionTrack> {\n if (!this.activeTracks.has(deviceId)) {\n this.activeTracks.set(deviceId, new Map())\n }\n return this.activeTracks.get(deviceId)!\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport type { IScopedLogger, IStorageLocation, IEventBus } from '@camstack/types'\n\n// ---------------------------------------------------------------------------\n// Config & Types\n// ---------------------------------------------------------------------------\n\nexport interface RetentionConfig {\n /** How often to run cleanup (ms). Default: 1 hour */\n readonly cleanupIntervalMs: number\n /** Detection events + associated media retention (days). Default: 30 */\n readonly detectionEventsDays: number\n /** Audio levels retention (days). Default: 7 */\n readonly audioLevelsDays: number\n /** DEPRECATED — snapshots are tied to events, use detectionEventsDays */\n readonly snapshotsDays: number\n /** Per-device overrides */\n readonly deviceOverrides?: Readonly<Record<string, Partial<Pick<RetentionConfig, 'detectionEventsDays' | 'audioLevelsDays' | 'snapshotsDays'>>>>\n}\n\nexport const DEFAULT_RETENTION: RetentionConfig = {\n cleanupIntervalMs: 60 * 60 * 1000, // 1 hour\n detectionEventsDays: 30,\n audioLevelsDays: 7,\n snapshotsDays: 14,\n}\n\nexport interface RetentionReport {\n readonly deletedEvents: number\n readonly deletedAudioRecords: number\n readonly deletedSnapshots: number\n}\n\n// ---------------------------------------------------------------------------\n// Dependencies\n// ---------------------------------------------------------------------------\n\nexport interface RetentionDeps {\n readonly getStorageLocation: (name: string) => IStorageLocation\n readonly getRetentionConfig: () => Partial<RetentionConfig> | undefined\n readonly eventBus: IEventBus\n readonly logger: IScopedLogger\n}\n\n// ---------------------------------------------------------------------------\n// Service\n// ---------------------------------------------------------------------------\n\nexport class RetentionService {\n private cleanupTimer?: ReturnType<typeof setInterval>\n private initialTimer?: ReturnType<typeof setTimeout>\n private config: RetentionConfig\n\n private readonly logger: IScopedLogger\n private readonly getStorageLocation: (name: string) => IStorageLocation\n private readonly eventBus: IEventBus\n\n constructor(private readonly deps: RetentionDeps) {\n this.logger = deps.logger\n this.getStorageLocation = deps.getStorageLocation\n this.eventBus = deps.eventBus\n\n // Load config from app configuration\n const retention = deps.getRetentionConfig()\n this.config = retention\n ? { ...DEFAULT_RETENTION, ...retention }\n : { ...DEFAULT_RETENTION }\n }\n\n start(): void {\n // Run initial cleanup after 5 minutes (let system boot first)\n this.initialTimer = setTimeout(() => {\n this.runCleanup().catch((err) => {\n this.logger.warn(`Initial cleanup failed: ${err}`)\n })\n }, 5 * 60 * 1000)\n\n // Schedule periodic cleanup\n this.cleanupTimer = setInterval(() => {\n this.runCleanup().catch((err) => {\n this.logger.warn(`Periodic cleanup failed: ${err}`)\n })\n }, this.config.cleanupIntervalMs)\n }\n\n stop(): void {\n if (this.initialTimer) {\n clearTimeout(this.initialTimer)\n this.initialTimer = undefined\n }\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer)\n this.cleanupTimer = undefined\n }\n }\n\n /** Run all retention cleanup tasks */\n async runCleanup(): Promise<RetentionReport> {\n const report: RetentionReport = {\n deletedEvents: 0,\n deletedAudioRecords: 0,\n deletedSnapshots: 0,\n }\n\n try {\n // 1. Clean detection events + their associated media files\n const eventCutoff = Date.now() - this.config.detectionEventsDays * 24 * 60 * 60 * 1000\n const { dbDeleted: eventsDeleted, filesDeleted: snapshotsDeleted } =\n await this.cleanEventsWithMedia('detection_events', eventCutoff)\n\n // 2. Clean audio levels (DB only, no media)\n const audioCutoff = Date.now() - this.config.audioLevelsDays * 24 * 60 * 60 * 1000\n const audioDeleted = await this.cleanCollection('audio_levels', audioCutoff)\n\n // Build immutable report\n const finalReport: RetentionReport = {\n deletedEvents: eventsDeleted,\n deletedAudioRecords: audioDeleted,\n deletedSnapshots: snapshotsDeleted,\n }\n\n if (finalReport.deletedEvents > 0 || finalReport.deletedAudioRecords > 0 || finalReport.deletedSnapshots > 0) {\n this.logger.info(\n `Cleanup: ${finalReport.deletedEvents} events, ${finalReport.deletedAudioRecords} audio records, ${finalReport.deletedSnapshots} snapshots`,\n )\n\n this.eventBus.emit({\n id: randomUUID(),\n timestamp: new Date(),\n source: { type: 'core', id: 'retention' },\n category: 'retention.cleanup',\n data: finalReport as unknown as Record<string, unknown>,\n })\n }\n\n return finalReport\n } catch (err) {\n this.logger.warn(`Retention cleanup failed: ${err}`)\n }\n\n return report\n }\n\n /** Set retention config at runtime */\n setConfig(update: Partial<RetentionConfig>): void {\n this.config = { ...this.config, ...update }\n }\n\n /** Get current retention config */\n getConfig(): RetentionConfig {\n return { ...this.config }\n }\n\n /** Force immediate cleanup */\n async forceCleanup(): Promise<RetentionReport> {\n return this.runCleanup()\n }\n\n /**\n * Delete old events AND their associated media files.\n * Media is stored at events/{eventId}/ — delete the entire directory.\n */\n private async cleanEventsWithMedia(\n collection: string,\n cutoffTimestamp: number,\n ): Promise<{ dbDeleted: number; filesDeleted: number }> {\n let dbDeleted = 0\n let filesDeleted = 0\n\n try {\n const location = this.getStorageLocation('events')\n if (!location.structured) return { dbDeleted, filesDeleted }\n\n const old = await location.structured.query(collection, {\n whereBetween: { timestamp: [0, cutoffTimestamp] },\n limit: 1000,\n })\n\n for (const record of old) {\n const eventId = record.id\n\n // Delete associated media files\n if (location.files) {\n try {\n const files = await location.files.listFiles(`events/${eventId}/`)\n for (const file of files) {\n await location.files.deleteFile(file)\n filesDeleted++\n }\n } catch {\n // Media may not exist for every event\n }\n }\n\n // Delete DB record\n await location.structured.delete(collection, eventId)\n dbDeleted++\n }\n } catch {\n // Swallow errors — retention is best-effort\n }\n\n return { dbDeleted, filesDeleted }\n }\n\n private async cleanCollection(collection: string, cutoffTimestamp: number): Promise<number> {\n try {\n const location = this.getStorageLocation('events')\n if (!location.structured) return 0\n\n // Query old records\n const old = await location.structured.query(collection, {\n whereBetween: { timestamp: [0, cutoffTimestamp] },\n limit: 1000, // batch delete\n })\n\n // Delete each\n let deleted = 0\n for (const record of old) {\n await location.structured.delete(collection, record.id)\n deleted++\n }\n\n return deleted\n } catch {\n return 0\n }\n }\n}\n","/**\n * Track Trail Service — persists the complete trajectory of tracked objects.\n *\n * For each active track, records:\n * - Every position (x, y, timestamp) at detection FPS\n * - Periodic snapshot crops at configurable intervals\n * - Track metadata (class, first/last seen, total distance, zones visited)\n */\n\nimport type { IScopedLogger, IStorageLocation, AnalysisContext, ServerTrackedDetection as TrackedDetection } from '@camstack/types'\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nexport interface TrackCaptureConfig {\n /** Enable trail capture for this device (default: false) */\n readonly enabled: boolean\n /** How often to save a snapshot of the same track (ms, default: 2000 = every 2s) */\n readonly snapshotIntervalMs: number\n /** Max trail entries per track before pruning oldest (default: 500) */\n readonly maxTrailLength: number\n /** Save thumbnail crops in trail snapshots (default: true) */\n readonly saveThumbnails: boolean\n /** Thumbnail size (default: 120x120) */\n readonly thumbnailSize: { readonly width: number; readonly height: number }\n}\n\nconst DEFAULT_CONFIG: TrackCaptureConfig = {\n enabled: false,\n snapshotIntervalMs: 2000,\n maxTrailLength: 500,\n saveThumbnails: true,\n thumbnailSize: { width: 120, height: 120 },\n}\n\n// ---------------------------------------------------------------------------\n// Trail data structures\n// ---------------------------------------------------------------------------\n\nexport interface TrackPosition {\n readonly x: number\n readonly y: number\n readonly timestamp: number\n /** Bounding box at this position [x, y, w, h] normalized */\n readonly bbox: readonly [number, number, number, number]\n}\n\nexport interface TrackSnapshot {\n readonly timestamp: number\n readonly position: TrackPosition\n /** Path to saved thumbnail in storage */\n readonly thumbnailPath: string\n}\n\nexport interface TrackTrail {\n readonly trackId: string\n readonly deviceId: string\n readonly className: string\n readonly label?: string\n readonly firstSeen: number\n readonly lastSeen: number\n /** All recorded positions (at detection FPS) */\n readonly positions: readonly TrackPosition[]\n /** Periodic snapshots (at configured interval) */\n readonly snapshots: readonly TrackSnapshot[]\n /** Total distance traveled (normalized units, 0-1 = full frame width) */\n readonly totalDistance: number\n /** All zones this track has visited */\n readonly zonesVisited: readonly string[]\n /** Whether the track is still active */\n readonly active: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Dependencies\n// ---------------------------------------------------------------------------\n\nexport interface TrackTrailDeps {\n readonly getStorageLocation: (name: string) => IStorageLocation\n readonly logger: IScopedLogger\n}\n\n// ---------------------------------------------------------------------------\n// Service\n// ---------------------------------------------------------------------------\n\nexport class TrackTrailService {\n private readonly logger: IScopedLogger\n private readonly getStorageLocation: (name: string) => IStorageLocation\n\n /** Per-device config */\n private readonly deviceConfigs: Map<string, TrackCaptureConfig> = new Map()\n\n /** Active trails: trackId -> mutable trail state */\n private readonly activeTrails: Map<string, MutableTrail> = new Map()\n\n /** Last snapshot time per track (for interval throttling) */\n private readonly lastSnapshotTime: Map<string, number> = new Map()\n\n constructor(deps: TrackTrailDeps) {\n this.logger = deps.logger\n this.getStorageLocation = deps.getStorageLocation\n }\n\n // --- Config ---\n\n setConfig(deviceId: string, config: Partial<TrackCaptureConfig>): void {\n this.deviceConfigs.set(deviceId, { ...DEFAULT_CONFIG, ...config })\n }\n\n getConfig(deviceId: string): TrackCaptureConfig {\n return this.deviceConfigs.get(deviceId) ?? { ...DEFAULT_CONFIG }\n }\n\n // --- Called after each analysis pass ---\n\n /**\n * Record trail data from an analysis context.\n * Called by the analysis pipeline addon after each frame's analysis.\n */\n async recordFrame(ctx: AnalysisContext): Promise<void> {\n const config = this.getConfig(ctx.deviceId)\n if (!config.enabled) return\n\n const activeTrackIds = new Set<string>()\n\n for (const td of ctx.trackedDetections) {\n activeTrackIds.add(td.trackId)\n await this.updateTrail(ctx, td, config)\n }\n\n // Mark trails as inactive if their track is gone\n for (const [trackId, trail] of this.activeTrails) {\n if (trail.deviceId === ctx.deviceId && !activeTrackIds.has(trackId)) {\n trail.active = false\n // Persist final trail to storage\n await this.persistTrail(trail)\n this.activeTrails.delete(trackId)\n this.lastSnapshotTime.delete(trackId)\n }\n }\n }\n\n // --- Query ---\n\n /** Get the live trail for an active track (from memory) */\n getActiveTrail(trackId: string): TrackTrail | null {\n const trail = this.activeTrails.get(trackId)\n return trail ? this.toImmutable(trail) : null\n }\n\n /** Get all active trails for a device */\n getActiveTrails(deviceId: string): readonly TrackTrail[] {\n const results: TrackTrail[] = []\n for (const trail of this.activeTrails.values()) {\n if (trail.deviceId === deviceId) {\n results.push(this.toImmutable(trail))\n }\n }\n return results\n }\n\n /** Get a persisted trail from storage (after track ended) */\n async getPersistedTrail(trackId: string): Promise<TrackTrail | null> {\n try {\n const location = this.getStorageLocation('events')\n if (!location.structured) return null\n\n const results = await location.structured.query('track_trails', {\n where: { trackId },\n limit: 1,\n })\n\n if (results.length === 0) return null\n return results[0]!.data as unknown as TrackTrail\n } catch {\n return null\n }\n }\n\n /** Get trail from memory (active) or storage (persisted) */\n async getTrail(trackId: string): Promise<TrackTrail | null> {\n return this.getActiveTrail(trackId) ?? this.getPersistedTrail(trackId)\n }\n\n /** List recent persisted trails for a device */\n async listTrails(deviceId: string, options?: {\n since?: number\n until?: number\n limit?: number\n className?: string\n }): Promise<readonly TrackTrail[]> {\n const results: TrackTrail[] = []\n\n // Active trails\n for (const trail of this.activeTrails.values()) {\n if (trail.deviceId === deviceId) {\n if (options?.className && trail.className !== options.className) continue\n results.push(this.toImmutable(trail))\n }\n }\n\n // Persisted trails\n try {\n const location = this.getStorageLocation('events')\n if (location.structured) {\n const stored = await location.structured.query('track_trails', {\n where: { deviceId, ...(options?.className ? { className: options.className } : {}) },\n whereBetween: options?.since || options?.until\n ? { firstSeen: [options.since ?? 0, options.until ?? Date.now()] }\n : undefined,\n orderBy: { field: 'firstSeen', direction: 'desc' },\n limit: options?.limit ?? 50,\n })\n\n for (const record of stored) {\n const trail = record.data as unknown as TrackTrail\n // Don't duplicate active trails\n if (!this.activeTrails.has(trail.trackId)) {\n results.push(trail)\n }\n }\n }\n } catch {\n // Storage not available\n }\n\n return results\n }\n\n // --- Internal ---\n\n private async updateTrail(\n ctx: AnalysisContext,\n td: TrackedDetection,\n config: TrackCaptureConfig,\n ): Promise<void> {\n let trail = this.activeTrails.get(td.trackId)\n\n const bbox = td.detection.boundingBox\n if (!bbox || bbox.length < 4) return\n\n const center = { x: bbox[0]! + bbox[2]! / 2, y: bbox[1]! + bbox[3]! / 2 }\n\n const position: TrackPosition = {\n x: center.x,\n y: center.y,\n timestamp: ctx.timestamp,\n bbox: [bbox[0]!, bbox[1]!, bbox[2]!, bbox[3]!] as readonly [number, number, number, number],\n }\n\n if (!trail) {\n trail = {\n trackId: td.trackId,\n deviceId: ctx.deviceId,\n className: td.detection.className,\n label: td.detection.label,\n firstSeen: ctx.timestamp,\n lastSeen: ctx.timestamp,\n positions: [position],\n snapshots: [],\n totalDistance: 0,\n zonesVisited: [...td.zones],\n active: true,\n }\n this.activeTrails.set(td.trackId, trail)\n } else {\n // Compute distance from last position\n const lastPos = trail.positions[trail.positions.length - 1]\n const dist = lastPos\n ? Math.sqrt((center.x - lastPos.x) ** 2 + (center.y - lastPos.y) ** 2)\n : 0\n\n trail.lastSeen = ctx.timestamp\n trail.totalDistance += dist\n\n // Add position (respect max length)\n if (trail.positions.length >= config.maxTrailLength) {\n // Downsample: keep every other position in the first half\n const half = Math.floor(trail.positions.length / 2)\n const downsampled = trail.positions.filter((_, i) => i >= half || i % 2 === 0)\n trail.positions = [...downsampled, position]\n } else {\n trail.positions = [...trail.positions, position]\n }\n\n // Merge new zones\n for (const zone of td.zones) {\n if (!trail.zonesVisited.includes(zone)) {\n trail.zonesVisited = [...trail.zonesVisited, zone]\n }\n }\n }\n\n // Periodic snapshot capture\n const lastSnapshot = this.lastSnapshotTime.get(td.trackId) ?? 0\n if (ctx.timestamp - lastSnapshot >= config.snapshotIntervalMs && td.crop) {\n await this.captureSnapshot(trail, td, position, config)\n this.lastSnapshotTime.set(td.trackId, ctx.timestamp)\n }\n }\n\n private async captureSnapshot(\n trail: MutableTrail,\n td: TrackedDetection,\n position: TrackPosition,\n config: TrackCaptureConfig,\n ): Promise<void> {\n if (!td.crop || !config.saveThumbnails) return\n\n try {\n const sharp = (await import('sharp')).default\n const thumb = await sharp(td.crop)\n .resize(config.thumbnailSize.width, config.thumbnailSize.height, { fit: 'cover' })\n .jpeg({ quality: 75 })\n .toBuffer()\n\n const path = `debug-trails/${trail.trackId}/${position.timestamp}.jpg`\n\n // Write to storage\n const location = this.getStorageLocation('events')\n if (location.files) {\n await location.files.writeFile(path, thumb)\n }\n\n trail.snapshots = [...trail.snapshots, {\n timestamp: position.timestamp,\n position,\n thumbnailPath: path,\n }]\n } catch {\n // Snapshot capture failed — non-critical, continue\n }\n }\n\n private async persistTrail(trail: MutableTrail): Promise<void> {\n try {\n const location = this.getStorageLocation('events')\n if (!location.structured) return\n\n await location.structured.insert({\n collection: 'track_trails',\n id: trail.trackId,\n data: this.toImmutable(trail) as unknown as Record<string, unknown>,\n })\n\n this.logger.debug(\n `Trail persisted: ${trail.trackId} (${trail.positions.length} positions, ${trail.snapshots.length} snapshots, ${trail.totalDistance.toFixed(3)} distance)`,\n )\n } catch (err) {\n this.logger.warn(`Failed to persist trail ${trail.trackId}: ${err}`)\n }\n }\n\n private toImmutable(trail: MutableTrail): TrackTrail {\n return {\n trackId: trail.trackId,\n deviceId: trail.deviceId,\n className: trail.className,\n label: trail.label,\n firstSeen: trail.firstSeen,\n lastSeen: trail.lastSeen,\n positions: [...trail.positions],\n snapshots: [...trail.snapshots],\n totalDistance: trail.totalDistance,\n zonesVisited: [...trail.zonesVisited],\n active: trail.active,\n }\n }\n}\n\n// Mutable internal state (not exported)\ninterface MutableTrail {\n readonly trackId: string\n readonly deviceId: string\n readonly className: string\n readonly label?: string\n firstSeen: number\n lastSeen: number\n positions: TrackPosition[]\n snapshots: TrackSnapshot[]\n totalDistance: number\n zonesVisited: string[]\n active: boolean\n}\n","import type {\n ICamstackAddon,\n AddonManifest,\n AddonContext,\n CapabilityProviderMap,\n IConfigurable,\n ConfigUISchema,\n FfmpegConfig,\n IStreamingEngine,\n IPipelineManager,\n INetworkQualityTracker,\n} from '@camstack/types'\nimport { DecoderRegistry } from './stream-broker/decoder-registry'\nimport { FfmpegDecoderProvider } from './stream-broker/ffmpeg-decoder-provider'\nimport { StreamBrokerManager } from './stream-broker/stream-broker-manager'\nimport { EventPersistenceService } from './persistence/event-persistence'\nimport { KnownFacesService } from './persistence/known-faces'\nimport { SessionTrackerService } from './persistence/session-tracker'\nimport { RetentionService } from './persistence/retention'\nimport { TrackTrailService } from './persistence/track-trail'\nimport type { IAnalysisDataPersistence } from './persistence/types'\n\n// Recording engine imports — dynamic to avoid hard dep on better-sqlite3 at load time\ntype RecordingCoordinator = import('./recording/recording-coordinator').RecordingCoordinator\ntype RecordingDb = import('./recording/recording-db').RecordingDb\n\nexport interface RecordingEngineDependencies {\n readonly streamingEngine: IStreamingEngine\n readonly pipelineManager: IPipelineManager\n readonly networkTracker: INetworkQualityTracker\n}\n\nexport type RecordingEngineV2Dependencies = RecordingEngineDependencies\n\nexport class PipelineAddon implements ICamstackAddon, IConfigurable {\n readonly manifest: AddonManifest = {\n id: 'pipeline',\n name: 'CamStack Pipeline',\n version: '0.1.0',\n capabilities: [\n { name: 'stream-broker', mode: 'singleton' as const },\n { name: 'recording-engine', mode: 'singleton' as const },\n { name: 'analysis-data-persistence', mode: 'singleton' as const },\n ],\n }\n\n // Stream broker\n private brokerManager: StreamBrokerManager | null = null\n\n // Recording engine\n private coordinator: RecordingCoordinator | null = null\n private recordingDb: RecordingDb | null = null\n private sqliteDb: import('better-sqlite3').Database | null = null\n private recordingDeps: RecordingEngineDependencies | null = null\n private currentRecordingConfig = {\n ffmpegPath: 'ffmpeg',\n hwaccel: undefined as FfmpegConfig['hwaccel'] | undefined,\n threads: undefined as number | undefined,\n segmentDurationSeconds: 4,\n defaultRetentionDays: 30,\n }\n\n // Analysis persistence\n private persistenceFacade: IAnalysisDataPersistence | null = null\n\n setRecordingDependencies(deps: RecordingEngineDependencies): void {\n this.recordingDeps = deps\n }\n\n async initialize(context: AddonContext): Promise<void> {\n // --- 1. Stream broker ---\n const registry = new DecoderRegistry()\n registry.register(new FfmpegDecoderProvider(), 50)\n this.brokerManager = new StreamBrokerManager(registry, context.logger)\n context.logger.info('Stream broker manager initialized')\n\n // --- 2. Recording engine ---\n if (this.recordingDeps) {\n try {\n const Database = (await import('better-sqlite3')).default\n const path = await import('node:path')\n const { detectPlatformDefaults } = await import('./recording/ffmpeg-config')\n const { RecordingDb: RecDb } = await import('./recording/recording-db')\n const { RecordingCoordinator: RecCoord } = await import('./recording/recording-coordinator')\n\n const storagePath = context.locationPaths.recordings\n const dbPath = path.join(context.locationPaths.data, 'camstack.db')\n this.sqliteDb = new Database(dbPath)\n this.recordingDb = new RecDb(this.sqliteDb)\n this.recordingDb.initialize()\n\n const ffmpegPath = (context.addonConfig.ffmpegPath as string) ?? this.currentRecordingConfig.ffmpegPath\n const detectedFfmpegConfig = detectPlatformDefaults(ffmpegPath)\n\n const globalFfmpegConfig: Partial<FfmpegConfig> = {\n path: ffmpegPath,\n hwaccel: (context.addonConfig.hwaccel as FfmpegConfig['hwaccel']) ?? this.currentRecordingConfig.hwaccel,\n threads: (context.addonConfig.threads as number) ?? this.currentRecordingConfig.threads,\n }\n\n const segmentDurationSeconds = (context.addonConfig.segmentDurationSeconds as number)\n ?? this.currentRecordingConfig.segmentDurationSeconds\n const defaultRetentionDays = (context.addonConfig.defaultRetentionDays as number)\n ?? this.currentRecordingConfig.defaultRetentionDays\n\n this.currentRecordingConfig = {\n ffmpegPath,\n hwaccel: globalFfmpegConfig.hwaccel,\n threads: globalFfmpegConfig.threads,\n segmentDurationSeconds,\n defaultRetentionDays,\n }\n\n const fileStorage = context.storage.files\n if (!fileStorage) {\n throw new Error('PipelineAddon:file storage not available in addon context')\n }\n\n this.coordinator = new RecCoord({\n db: this.recordingDb,\n logger: context.logger,\n eventBus: context.eventBus,\n streamingEngine: this.recordingDeps.streamingEngine,\n pipelineManager: this.recordingDeps.pipelineManager,\n networkTracker: this.recordingDeps.networkTracker,\n fileStorage,\n storagePath,\n globalFfmpegConfig,\n detectedFfmpegConfig,\n segmentDurationSec: segmentDurationSeconds,\n })\n await this.coordinator.start()\n context.logger.info('Recording Engine initialized')\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error)\n context.logger.warn(`Recording Engine failed to initialize: ${msg}`)\n }\n }\n\n // --- 3. Analysis persistence ---\n const eventPersistence = new EventPersistenceService({\n getStorageLocation: () => context.storage,\n subscribe: (filter, handler) => context.eventBus.subscribe(filter, handler),\n logger: context.logger.child('EventPersistence'),\n })\n\n const knownFaces = new KnownFacesService({\n getStructuredStorage: () => {\n if (!context.storage.structured) {\n throw new Error('Structured storage not available for analysis persistence')\n }\n return context.storage.structured\n },\n logger: context.logger.child('KnownFaces'),\n })\n\n const sessionTracker = new SessionTrackerService({\n eventBus: context.eventBus,\n logger: context.logger.child('SessionTracker'),\n })\n\n const retention = new RetentionService({\n getStorageLocation: () => context.storage,\n getRetentionConfig: () => context.addonConfig.retention as Record<string, unknown> | undefined,\n eventBus: context.eventBus,\n logger: context.logger.child('Retention'),\n })\n\n const trackTrail = new TrackTrailService({\n getStorageLocation: () => context.storage,\n logger: context.logger.child('TrackTrail'),\n })\n\n eventPersistence.start()\n retention.start()\n\n this.persistenceFacade = {\n eventPersistence,\n knownFaces,\n sessionTracker,\n retention,\n trackTrail,\n }\n\n context.logger.info('Analysis persistence initialized')\n }\n\n async shutdown(): Promise<void> {\n // Stream broker\n await this.brokerManager?.destroyAll()\n this.brokerManager = null\n\n // Recording engine\n if (this.coordinator) {\n this.coordinator.stop()\n this.coordinator = null\n }\n if (this.sqliteDb) {\n this.sqliteDb.close()\n this.sqliteDb = null\n }\n this.recordingDb = null\n\n // Analysis persistence\n if (this.persistenceFacade) {\n this.persistenceFacade.eventPersistence.stop()\n this.persistenceFacade.retention.stop()\n this.persistenceFacade = null\n }\n }\n\n getCapabilityProvider<K extends keyof CapabilityProviderMap>(\n name: K,\n ): CapabilityProviderMap[K] | null {\n switch (name) {\n case 'stream-broker':\n return this.brokerManager as unknown as CapabilityProviderMap[K]\n case 'recording-engine':\n return this.coordinator as unknown as CapabilityProviderMap[K]\n case 'analysis-data-persistence':\n return this.persistenceFacade as unknown as CapabilityProviderMap[K]\n default:\n return null\n }\n }\n\n // --- Recording engine accessors ---\n\n getCoordinator(): RecordingCoordinator {\n if (!this.coordinator) throw new Error('PipelineAddon recording not initialized')\n return this.coordinator\n }\n\n getRecordingDb(): RecordingDb {\n if (!this.recordingDb) throw new Error('PipelineAddon recording not initialized')\n return this.recordingDb\n }\n\n // --- IConfigurable ---\n\n getConfigSchema(): ConfigUISchema {\n return { sections: [] }\n }\n\n getConfig(): Record<string, unknown> {\n return { ...this.currentRecordingConfig }\n }\n\n async onConfigChange(config: Record<string, unknown>): Promise<void> {\n this.currentRecordingConfig = {\n ffmpegPath: (config.ffmpegPath as string) ?? this.currentRecordingConfig.ffmpegPath,\n hwaccel: (config.hwaccel as FfmpegConfig['hwaccel']) ?? this.currentRecordingConfig.hwaccel,\n threads: (config.threads as number) ?? this.currentRecordingConfig.threads,\n segmentDurationSeconds: (config.segmentDurationSeconds as number) ?? this.currentRecordingConfig.segmentDurationSeconds,\n defaultRetentionDays: (config.defaultRetentionDays as number) ?? this.currentRecordingConfig.defaultRetentionDays,\n }\n }\n}\n\nexport default PipelineAddon\n"],"mappings":";;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAS,oBAAoB;AAC7B,YAAY,QAAQ;AAOb,SAAS,uBAAuB,aAAqB,UAAiC;AAC3F,MAAI,WAAqB,CAAC;AAC1B,MAAI;AACF,UAAM,SAAS,aAAa,YAAY,CAAC,aAAa,cAAc,GAAG,EAAE,UAAU,QAAQ,SAAS,KAAM,OAAO,CAAC,QAAQ,QAAQ,MAAM,EAAE,CAAC;AAC3I,eAAW,OAAO,MAAM,IAAI,EAAE,IAAI,OAAK,EAAE,KAAK,CAAC,EAAE,OAAO,OAAK,KAAK,MAAM,gCAAgC;AAAA,EAC1G,QAAQ;AAAA,EAAC;AAET,QAAMA,YAAc,YAAS;AAC7B,MAAIA,cAAa,YAAY,SAAS,SAAS,cAAc,EAAG,QAAO,EAAE,SAAS,gBAAgB,SAAS,EAAE;AAC7G,MAAIA,cAAa,SAAS;AACxB,eAAW,MAAM,CAAC,SAAS,OAAO,QAAQ,SAAS,GAAY;AAC7D,UAAI,SAAS,SAAS,EAAE,EAAG,QAAO,EAAE,SAAS,IAAI,SAAS,EAAE;AAAA,IAC9D;AAAA,EACF;AACA,SAAO,EAAE,SAAS,QAAQ,SAAS,KAAK,IAAI,GAAG,KAAK,MAAS,QAAK,EAAE,SAAS,CAAC,CAAC,EAAE;AACnF;AAMO,SAAS,oBACd,cACA,cACA,UACc;AACd,SAAO;AAAA,IACL,MAAM,cAAc,QAAQ,aAAa,QAAQ,SAAS,QAAQ;AAAA,IAClE,SAAS,cAAc,WAAW,aAAa,WAAW,SAAS,WAAW;AAAA,IAC9E,WAAW,cAAc,aAAa,aAAa;AAAA,IACnD,YAAY,cAAc,cAAc,aAAa;AAAA,IACrD,YAAY,cAAc,cAAc,aAAa,cAAc;AAAA,IACnE,YAAY,cAAc,cAAc,aAAa,cAAc;AAAA,IACnE,SAAS,cAAc,WAAW,aAAa,WAAW,SAAS;AAAA,EACrE;AACF;AAKO,SAAS,qBAAqB,QAAsB,UAA4B;AACrF,QAAM,OAAiB,CAAC,gBAAgB,aAAa,SAAS;AAC9D,MAAI,OAAO,WAAW,OAAO,YAAY,OAAQ,MAAK,KAAK,YAAY,OAAO,OAAO;AACrF,MAAI,OAAO,YAAY,OAAW,MAAK,KAAK,YAAY,OAAO,OAAO,OAAO,CAAC;AAC9E,MAAI,OAAO,WAAW,OAAQ,MAAK,KAAK,GAAG,OAAO,SAAS;AAC3D,OAAK,KAAK,mBAAmB,OAAO,MAAM,QAAQ;AAClD,SAAO;AACT;AAEO,SAAS,sBAAsB,QAAgC;AACpE,QAAM,OAAiB,CAAC,QAAQ,OAAO,cAAc,QAAQ,QAAQ,OAAO,cAAc,MAAM;AAChG,MAAI,OAAO,YAAY,OAAQ,MAAK,KAAK,GAAG,OAAO,UAAU;AAC7D,SAAO;AACT;AASA,eAAsB,oBACpB,YACA,MACiB;AAEjB,MAAI,cAAc,eAAe,UAAU;AACzC,WAAO;AAAA,EACT;AAGA,SAAO,KAAK,aAAa;AAC3B;AAjFA;AAAA;AAAA;AAAA;AAAA;;;ACAA;AAAA;AAAA;AAAA;AAmTA,SAAS,aAAa,KAAgD;AACpE,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,UAAU,IAAI;AAAA,IACd,UAAU,IAAI;AAAA,IACd,WAAW,IAAI;AAAA,IACf,SAAS,IAAI;AAAA,IACb,UAAU,IAAI;AAAA,IACd,MAAM,IAAI;AAAA,IACV,aAAa,IAAI;AAAA,IACjB,cAAc,IAAI;AAAA,IAClB,WAAW,IAAI;AAAA,IACf,OAAO,IAAI;AAAA,IACX,UAAW,IAAI,cAAyB;AAAA,EAC1C;AACF;AAEA,SAAS,eAAe,KAAkD;AACxE,SAAO;AAAA,IACL,UAAU,IAAI;AAAA,IACd,WAAW,IAAI;AAAA,IACf,MAAM,IAAI;AAAA,IACV,aAAa,IAAI;AAAA,IACjB,cAAc,IAAI;AAAA,IAClB,WAAW,IAAI;AAAA,IACf,UAAU,IAAI;AAAA,EAChB;AACF;AAEA,SAAS,YAAY,KAAiC;AACpD,SAAO;AAAA,IACL,UAAU,IAAI;AAAA,IACd,SAAS,IAAI,YAAY;AAAA,IACzB,MAAM,IAAI;AAAA,IACV,SAAS,KAAK,MAAM,IAAI,YAAY;AAAA,IACpC,cAAc,IAAI;AAAA,IAClB,eAAe,IAAI;AAAA,IACnB,eAAe,IAAI,gBAAgB,KAAK,MAAM,IAAI,aAAa,IAAsB;AAAA,EACvF;AACF;AAEA,SAAS,mBAAmB,KAAsD;AAChF,SAAO;AAAA,IACL,UAAU,IAAI;AAAA,IACd,cAAc,IAAI;AAAA,IAClB,aAAa,IAAI;AAAA,IACjB,cAAc,IAAI;AAAA,IAClB,eAAe,IAAI;AAAA,IACnB,aAAa,IAAI;AAAA,EACnB;AACF;AAEA,SAAS,aAAa,KAAiD;AACrE,SAAO;AAAA,IACL,UAAU,IAAI;AAAA,IACd,YAAY,IAAI;AAAA,IAChB,cAAc,IAAI;AAAA,IAClB,QAAQ,IAAI;AAAA,IACZ,WAAW,IAAI;AAAA,EACjB;AACF;AA/WA,IA4Ba;AA5Bb;AAAA;AAAA;AA4BO,IAAM,cAAN,MAAkB;AAAA,MACvB,YAA6B,IAAuB;AAAvB;AAAA,MAAwB;AAAA,MAErD,aAAmB;AACjB,aAAK,GAAG,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KA0DZ;AAAA,MACH;AAAA;AAAA,MAIA,cAAc,KAA6B;AACzC,aAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAGf,EAAE,IAAI,IAAI,IAAI,IAAI,UAAU,IAAI,UAAU,IAAI,WAAW,IAAI,SAAS,IAAI,UAAU,IAAI,MAAM,IAAI,aAAa,IAAI,cAAc,IAAI,WAAW,IAAI,OAAO,IAAI,WAAW,IAAI,CAAC;AAAA,MAClL;AAAA,MAEA,cAAc,UAAkB,UAAkB,WAAmB,SAA8C;AACjH,cAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,KAI5B,EAAE,IAAI,UAAU,UAAU,SAAS,SAAS;AAC7C,eAAO,KAAK,IAAI,YAAY;AAAA,MAC9B;AAAA,MAEA,qBAAqB,UAAkB,UAAkB,YAAiD;AACxG,cAAM,WAAW,KAAK,GAAG,QAAQ;AAAA;AAAA,KAEhC,EAAE,IAAI,UAAU,UAAU,UAAU;AACrC,aAAK,GAAG,QAAQ,yFAAyF,EAAE,IAAI,UAAU,UAAU,UAAU;AAC7I,eAAO,SAAS,IAAI,YAAY;AAAA,MAClC;AAAA,MAEA,wBAAwB,UAA+C;AACrE,cAAM,WAAW,KAAK,GAAG,QAAQ,sDAAsD,EAAE,IAAI,QAAQ;AACrG,aAAK,GAAG,QAAQ,oDAAoD,EAAE,IAAI,QAAQ;AAClF,eAAO,SAAS,IAAI,YAAY;AAAA,MAClC;AAAA,MAEA,gBAAgB,UAAkB,UAAgC;AAChE,cAAM,MAAM,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAG3B,EAAE,IAAI,UAAU,QAAQ;AACzB,eAAO,EAAE,YAAY,IAAI,aAAa,cAAc,IAAI,cAAc;AAAA,MACxE;AAAA,MAEA,kBAAkB,UAAkB,UAAkB,OAA4C;AAChG,cAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAG5B,EAAE,IAAI,UAAU,UAAU,KAAK;AAChC,eAAO,KAAK,IAAI,YAAY;AAAA,MAC9B;AAAA,MAEA,gBAAgB,UAAkB,WAAmB,SAA+C;AAClG,cAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,KAI5B,EAAE,IAAI,UAAU,WAAW,OAAO;AAEnC,YAAI,KAAK,WAAW,EAAG,QAAO,CAAC;AAE/B,cAAM,SAA8E,CAAC;AACrF,YAAI,UAAU,EAAE,WAAW,KAAK,CAAC,EAAG,YAAY,SAAS,KAAK,CAAC,EAAG,UAAU,SAAS,oBAAI,IAAI,CAAC,KAAK,CAAC,EAAG,SAAS,CAAC,EAAE;AAEnH,iBAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,gBAAM,MAAM,KAAK,CAAC;AAClB,cAAI,IAAI,cAAc,QAAQ,SAAS;AACrC,oBAAQ,UAAU,KAAK,IAAI,QAAQ,SAAS,IAAI,QAAQ;AACxD,oBAAQ,QAAQ,IAAI,IAAI,SAAS;AAAA,UACnC,OAAO;AACL,mBAAO,KAAK,OAAO;AACnB,sBAAU,EAAE,WAAW,IAAI,YAAY,SAAS,IAAI,UAAU,SAAS,oBAAI,IAAI,CAAC,IAAI,SAAS,CAAC,EAAE;AAAA,UAClG;AAAA,QACF;AACA,eAAO,KAAK,OAAO;AAEnB,eAAO,OAAO,IAAI,QAAM,EAAE,WAAW,EAAE,WAAW,SAAS,EAAE,SAAS,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE;AAAA,MAClG;AAAA,MAEA,eAAe,UAAkB,WAAmB,SAKlD;AACA,cAAM,MAAM,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,KAI3B,EAAE,IAAI,UAAU,WAAW,OAAO;AAEnC,cAAM,cAAc,UAAU;AAC9B,cAAM,gBAAgB,KAAK,IAAI,eAAe,KAAK,KAAK,KAAK,MAAO,CAAC;AACrE,cAAM,eAAe,KAAK,IAAI,cAAc,KAAM,CAAC;AAEnD,eAAO;AAAA,UACL,aAAa,IAAI;AAAA,UACjB,gBAAgB,KAAK,MAAM,IAAI,eAAe,GAAG,IAAI;AAAA,UACrD,iBAAiB,KAAK,MAAO,IAAI,eAAe,gBAAiB,GAAG,IAAI;AAAA,UACxE,kBAAkB,KAAK,MAAO,IAAI,iBAAiB,eAAgB,GAAK,IAAI;AAAA,QAC9E;AAAA,MACF;AAAA;AAAA,MAIA,gBAAgB,OAAiC;AAC/C,aAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAGf,EAAE,IAAI,MAAM,UAAU,MAAM,WAAW,MAAM,MAAM,MAAM,aAAa,MAAM,cAAc,MAAM,WAAW,MAAM,QAAQ;AAAA,MAC5H;AAAA,MAEA,qBAAqB,UAAkB,WAAmB,UAA6C;AACrG,cAAM,MAAM,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,KAI3B,EAAE,IAAI,UAAU,UAAU,SAAS;AACpC,eAAO,MAAM,eAAe,GAAG,IAAI;AAAA,MACrC;AAAA,MAEA,uBAAuB,UAAkB,YAA4B;AACnE,cAAM,SAAS,KAAK,GAAG,QAAQ,wEAAwE,EAAE,IAAI,UAAU,UAAU;AACjI,eAAO,OAAO;AAAA,MAChB;AAAA,MAEA,0BAA0B,UAA0B;AAClD,cAAM,SAAS,KAAK,GAAG,QAAQ,sDAAsD,EAAE,IAAI,QAAQ;AACnG,eAAO,OAAO;AAAA,MAChB;AAAA;AAAA,MAIA,aAAa,QAAmM;AAC9M,cAAM,MAAM,KAAK,IAAI;AACrB,aAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,KAIf,EAAE;AAAA,UACD,OAAO;AAAA,UAAU,OAAO,UAAU,IAAI;AAAA,UAAG,OAAO;AAAA,UAAM,KAAK,UAAU,OAAO,OAAO;AAAA,UAAG,OAAO,gBAAgB,KAAK,UAAU,OAAO,aAAa,IAAI;AAAA,UAAM,OAAO;AAAA,UAAc,OAAO;AAAA,UAAe;AAAA,UAAK;AAAA,UAC1M,OAAO,UAAU,IAAI;AAAA,UAAG,OAAO;AAAA,UAAM,KAAK,UAAU,OAAO,OAAO;AAAA,UAAG,OAAO,gBAAgB,KAAK,UAAU,OAAO,aAAa,IAAI;AAAA,UAAM,OAAO;AAAA,UAAc,OAAO;AAAA,UAAe;AAAA,QACtL;AAAA,MACF;AAAA,MAEA,UAAU,UAA0C;AAClD,cAAM,MAAM,KAAK,GAAG,QAAQ,sDAAsD,EAAE,IAAI,QAAQ;AAChG,eAAO,MAAM,YAAY,GAAG,IAAI;AAAA,MAClC;AAAA,MAEA,qBAAiD;AAC/C,cAAM,OAAO,KAAK,GAAG,QAAQ,oDAAoD,EAAE,IAAI;AACvF,eAAO,KAAK,IAAI,WAAW;AAAA,MAC7B;AAAA,MAEA,aAAa,UAAwB;AACnC,aAAK,GAAG,QAAQ,oDAAoD,EAAE,IAAI,QAAQ;AAAA,MACpF;AAAA;AAAA,MAIA,oBAAoB,QAAsC;AACxD,aAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,KAIf,EAAE;AAAA,UAAI,OAAO;AAAA,UAAU,OAAO;AAAA,UAAc,OAAO;AAAA,UAAa,OAAO;AAAA,UAAc,OAAO;AAAA,UAAe,OAAO;AAAA,UACjH,OAAO;AAAA,UAAa,OAAO;AAAA,UAAc,OAAO;AAAA,UAAe,OAAO;AAAA,QAAW;AAAA,MACrF;AAAA,MAEA,qBAAqB,UAAkB,UAAuD;AAC5F,cAAM,WAAW,KAAK,GAAG,QAAQ,kFAAkF,EAAE,IAAI,UAAU,QAAQ;AAC3I,YAAI,SAAU,QAAO,mBAAmB,QAAQ;AAChD,cAAM,SAAS,KAAK,GAAG,QAAQ,oFAAoF,EAAE,IAAI,QAAQ;AACjI,eAAO,SAAS,mBAAmB,MAAM,IAAI;AAAA,MAC/C;AAAA;AAAA,MAIA,kBAAkB,UAAkB,YAA0B;AAC5D,cAAM,eAAe,aAAa,KAAK,KAAK,KAAK;AACjD,aAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAGf,EAAE,IAAI,UAAU,YAAY,YAAY;AAAA,MAC3C;AAAA,MAEA,cAAc,UAAwB;AACpC,aAAK,GAAG,QAAQ,oGAAoG,EAAE,IAAI,QAAQ;AAAA,MACpI;AAAA,MAEA,gBAAgB,UAA4C;AAC1D,cAAM,MAAM,KAAK,GAAG,QAAQ,2DAA2D,EAAE,IAAI,QAAQ;AACrG,eAAO,MAAM,aAAa,GAAG,IAAI;AAAA,MACnC;AAAA,MAEA,qBAAmD;AACjD,cAAM,MAAM,KAAK,IAAI;AACrB,cAAM,OAAO,KAAK,GAAG,QAAQ,uFAAuF,EAAE,IAAI,GAAG;AAC7H,eAAO,KAAK,IAAI,YAAY;AAAA,MAC9B;AAAA,MAEA,mBAAmB,WAAmB,MAAe;AACnD,cAAM,SAAS,KAAK,IAAI,IAAI;AAC5B,aAAK,GAAG,QAAQ,0HAA0H,EAAE,IAAI,MAAM;AAAA,MACxJ;AAAA,MAEA,sBAAsB,UAAwB;AAC5C,aAAK,GAAG,QAAQ,+FAA+F,EAAE,IAAI,KAAK,IAAI,GAAG,QAAQ;AAAA,MAC3I;AAAA,MAEA,qBAAqB,UAAwB;AAC3C,aAAK,GAAG,QAAQ,6EAA6E,EAAE,IAAI,QAAQ;AAAA,MAC7G;AAAA,IACF;AAAA;AAAA;;;AC/SA,SAAS,mBAAmB;AAC5B,SAAS,SAAAC,cAAgC;AACzC,YAAY,QAAQ;AACpB,YAAY,UAAU;AAHtB,IAkBa,mBAsEA;AAxFb;AAAA;AAAA;AAMA;AAYO,IAAM,oBAAN,MAAwB;AAAA,MAI7B,YAA6B,gBAAwB;AAAxB;AAAA,MAAyB;AAAA,MAH9C,WAA8B,CAAC;AAAA,MAC/B,mBAAmB;AAAA,MAI3B,KAAK,SAAgC;AACnC,aAAK,SAAS,KAAK,OAAO;AAC1B,aAAK,oBAAoB,QAAQ;AACjC,eAAO,KAAK,mBAAmB,KAAK,kBAAkB,KAAK,SAAS,SAAS,GAAG;AAC9E,gBAAM,UAAU,KAAK,SAAS,MAAM;AACpC,eAAK,oBAAoB,QAAQ;AAAA,QACnC;AAAA,MACF;AAAA,MAEA,QAA2B;AACzB,cAAM,SAAS,CAAC,GAAG,KAAK,QAAQ;AAChC,aAAK,WAAW,CAAC;AACjB,aAAK,mBAAmB;AACxB,eAAO;AAAA,MACT;AAAA,MAEA,IAAI,sBAA8B;AAChC,eAAO,KAAK,SAAS,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,KAAK,QAAQ,CAAC;AAAA,MAChE;AAAA,IACF;AA6CO,IAAM,gBAAN,MAAM,eAAc;AAAA,MAuBzB,YACmB,QACA,QACA,UACA,IACA,iBACjB;AALiB;AACA;AACA;AACA;AACA;AAEjB,aAAK,QAAQ,OAAO;AACpB,aAAK,aAAa,IAAI,kBAAkB,OAAO,YAAY;AAAA,MAC7D;AAAA,MA/BQ,SAA6B;AAAA,MAC7B;AAAA,MACA,SAA8B;AAAA,MAC9B,gBAAsC;AAAA,MACtC,eAAe;AAAA,MACf,qBAAqB;AAAA,MACrB,cAAqD;AAAA,MACrD,eAAe;AAAA,MACf;AAAA,MACA,iBAAuD;AAAA,MACvD,sBAA4C;AAAA,MAC5C,SAAS;AAAA,MACT,gBAAiC;AAAA,MACjC,mBAAmB;AAAA,MAE3B,OAAwB,eAAe;AAAA,MACvC,OAAwB,oBAAoB,IAAI,KAAK;AAAA,MACrD,OAAwB,2BAA2B;AAAA,MACnD,OAAwB,kBAAkB;AAAA,MAC1C,OAAwB,2BAA2B;AAAA,MACnD,OAAwB,mBAAmB;AAAA,MAa3C,IAAI,QAA4B;AAC9B,eAAO,KAAK;AAAA,MACd;AAAA,MAEA,IAAI,OAA0B;AAC5B,eAAO,KAAK;AAAA,MACd;AAAA,MAEA,IAAI,WAAoB;AACtB,eAAO,KAAK;AAAA,MACd;AAAA;AAAA,MAIA,MAAM,MAAM,SAAgC;AAC1C,YAAI,KAAK,WAAW,OAAQ;AAE5B,cAAM,aAAkB;AAAA,UACtB,KAAK,OAAO;AAAA,UACZ,KAAK,OAAO;AAAA,UACZ,KAAK,OAAO;AAAA,QACd;AACA,cAAS,SAAM,YAAY,EAAE,WAAW,KAAK,CAAC;AAE9C,aAAK,SAAS;AACd,aAAK,eAAe,KAAK,IAAI;AAC7B,aAAK,eAAe;AACpB,aAAK,qBAAqB,KAAK,IAAI;AAEnC,cAAM,iBAAsB,UAAK,YAAY,QAAQ;AACrD,cAAM,OAAO,eAAc;AAAA,UACzB,KAAK,OAAO;AAAA,UACZ;AAAA,UACA;AAAA,UACA,KAAK,OAAO;AAAA,QACd;AAEA,aAAK,YAAY,MAAM,OAAO;AAC9B,aAAK,iBAAiB,OAAO;AAAA,MAC/B;AAAA,MAEA,MAAM,OAAsB;AAC1B,YAAI,KAAK,WAAW,OAAQ;AAC5B,aAAK,SAAS;AACd,aAAK,gBAAgB;AACrB,aAAK,oBAAoB;AACzB,aAAK,WAAW;AAChB,aAAK,sBAAsB;AAC3B,YAAI,KAAK,qBAAqB;AAC5B,gBAAM,KAAK;AAAA,QACb;AACA,aAAK,SAAS;AAAA,MAChB;AAAA,MAEA,OAAO,SAAuB;AAC5B,YAAI,CAAC,KAAK,OAAQ;AAClB,aAAK,SAAS;AACd,aAAK,OAAO,KAAK,6CAA6C;AAAA,UAC5D,UAAU,KAAK,OAAO;AAAA,QACxB,CAAC;AACD,aAAK,SAAS;AACd,aAAK,KAAK,MAAM,OAAO;AAAA,MACzB;AAAA,MAEA,MAAM,mBAAkC;AACtC,YAAI,KAAK,UAAU,SAAU;AAE7B,cAAM,WAAW,KAAK,WAAW,MAAM;AACvC,aAAK,OAAO,KAAK,YAAY,SAAS,MAAM,8BAA8B;AAAA,UACxE,UAAU,KAAK,OAAO;AAAA,QACxB,CAAC;AAED,mBAAW,OAAO,UAAU;AAC1B,gBAAM,KAAK,2BAA2B,GAAG;AAAA,QAC3C;AAEA,aAAK,QAAQ;AAAA,MACf;AAAA,MAEA,iBAAuB;AACrB,aAAK,QAAQ;AACb,aAAK,aAAa,IAAI,kBAAkB,KAAK,OAAO,YAAY;AAAA,MAClE;AAAA;AAAA,MAIA,OAAO,kBAAkB,UAAkB,UAAkB,WAA2B;AACtF,cAAM,SAAS,YAAY,CAAC,EAAE,SAAS,KAAK;AAC5C,eAAO,GAAG,QAAQ,IAAI,QAAQ,IAAI,SAAS,IAAI,MAAM;AAAA,MACvD;AAAA,MAEA,OAAO,sBACL,QACA,UACA,eACA,iBACU;AACV,cAAM,YAAY,qBAAqB,QAAQ,QAAQ;AACvD,cAAM,aAAa,sBAAsB,MAAM;AAE/C,cAAM,cAAc;AAAA,UAClB;AAAA,UAAM;AAAA,UACN;AAAA,UAAiB,OAAO,eAAe;AAAA,UACvC;AAAA,UAAmB;AAAA,UACnB;AAAA,UAAa;AAAA,UACb;AAAA,UAAqB;AAAA,UACrB;AAAA,UAAa;AAAA,QACf;AAEA,eAAO,CAAC,GAAG,WAAW,GAAG,YAAY,GAAG,aAAa,aAAa;AAAA,MACpE;AAAA,MAEA,aAAa,eACX,aACA,UAC0B;AAC1B,cAAM,WAAW,aAAa,OAAO,MAAc;AACjD,gBAAM,EAAE,QAAQ,WAAW,IAAI,MAAM,OAAO,aAAkB;AAC9D,iBAAO,WAAW,CAAC;AAAA,QACrB;AAEA,YAAI;AACF,gBAAM,QAAQ,MAAM,SAAS,WAAW;AACxC,gBAAM,iBAAiB,MAAM,QAAQ,MAAM;AAC3C,gBAAM,cAAc,kBAAkB,OAAO,OAAO;AACpD,iBAAO,EAAE,IAAI,eAAe,eAAc,kBAAkB,YAAY;AAAA,QAC1E,QAAQ;AACN,iBAAO,EAAE,IAAI,MAAM,aAAa,GAAG;AAAA,QACrC;AAAA,MACF;AAAA;AAAA,MAIQ,YAAY,MAAgB,SAAuB;AACzD,aAAK,SAASA,OAAM,KAAK,OAAO,OAAO,MAAM,MAAM;AAAA,UACjD,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,QAClC,CAAC;AAED,aAAK,OAAO,QAAQ,GAAG,QAAQ,MAAM;AACnC,eAAK,eAAe,KAAK,IAAI;AAAA,QAC/B,CAAC;AAED,aAAK,OAAO,QAAQ,GAAG,QAAQ,CAAC,SAAiB;AAC/C,eAAK,eAAe,KAAK,IAAI;AAC7B,gBAAM,MAAM,KAAK,SAAS,EAAE,KAAK;AACjC,cAAI,KAAK;AACP,iBAAK,OAAO,MAAM,WAAW,GAAG,EAAE;AAClC,iBAAK,mBAAmB,GAAG;AAAA,UAC7B;AAAA,QACF,CAAC;AAED,aAAK,OAAO,GAAG,SAAS,CAAC,QAAQ;AAC/B,eAAK,OAAO,KAAK,yBAAyB,IAAI,OAAO,EAAE;AACvD,eAAK,YAAY,OAAO;AAAA,QAC1B,CAAC;AAED,aAAK,OAAO,GAAG,QAAQ,CAAC,SAAS;AAC/B,cAAI,SAAS,KAAK,SAAS,QAAQ,KAAK,WAAW,aAAa;AAC9D,iBAAK,OAAO,KAAK,2BAA2B,IAAI,EAAE;AAClD,iBAAK,YAAY,OAAO;AAAA,UAC1B;AAAA,QACF,CAAC;AAAA,MACH;AAAA,MAEQ,YAAY,SAAuB;AACzC,aAAK,SAAS;AACd,cAAM,mBAAmB,KAAK;AAC9B,aAAK,uBAAuB,oBAAoB,QAAQ,QAAQ,GAAG,KAAK,MAAM;AAC5E,iBAAO,KAAK,sBAAsB;AAAA,QACpC,CAAC;AAED,YAAI,KAAK,WAAW,YAAa;AACjC,YAAI,KAAK,OAAQ;AAEjB,cAAM,MAAM,KAAK,IAAI;AACrB,YAAI,MAAM,KAAK,qBAAqB,eAAc,mBAAmB;AACnE,eAAK,eAAe;AACpB,eAAK,qBAAqB;AAAA,QAC5B;AAEA,aAAK;AAEL,aAAK,SAAS,KAAK;AAAA,UACjB,IAAI,WAAW,GAAG;AAAA,UAClB,WAAW,oBAAI,KAAK;AAAA,UACpB,QAAQ,EAAE,MAAM,SAAS,IAAI,mBAAmB;AAAA,UAChD,UAAU;AAAA,UACV,MAAM;AAAA,YACJ,UAAU,KAAK,OAAO;AAAA,YACtB,UAAU,KAAK,OAAO;AAAA,YACtB,gBAAgB,KAAK;AAAA,UACvB;AAAA,QACF,CAAC;AAED,YAAI,KAAK,eAAe,eAAc,cAAc;AAClD,eAAK,OAAO;AAAA,YACV,6BAA6B,KAAK,OAAO,QAAQ,IAAI,KAAK,OAAO,QAAQ;AAAA,UAC3E;AACA,eAAK,SAAS;AACd,eAAK,SAAS,KAAK;AAAA,YACjB,IAAI,gBAAgB,GAAG;AAAA,YACvB,WAAW,oBAAI,KAAK;AAAA,YACpB,QAAQ,EAAE,MAAM,SAAS,IAAI,mBAAmB;AAAA,YAChD,UAAU;AAAA,YACV,MAAM;AAAA,cACJ,UAAU,KAAK,OAAO;AAAA,cACtB,UAAU,KAAK,OAAO;AAAA,YACxB;AAAA,UACF,CAAC;AACD;AAAA,QACF;AAEA,cAAM,YAAY,KAAK,IAAI,KAAO,MAAO,KAAK,IAAI,GAAG,KAAK,eAAe,CAAC,CAAC;AAC3E,aAAK,OAAO,KAAK,wBAAwB,SAAS,eAAe,KAAK,YAAY,GAAG;AAErF,aAAK,iBAAiB,WAAW,MAAM;AACrC,eAAK,iBAAiB;AACtB,cAAI,KAAK,WAAW,aAAa;AAC/B,kBAAM,aAAkB;AAAA,cACtB,KAAK,OAAO;AAAA,cACZ,KAAK,OAAO;AAAA,cACZ,KAAK,OAAO;AAAA,YACd;AACA,kBAAM,iBAAsB,UAAK,YAAY,QAAQ;AACrD,kBAAM,OAAO,eAAc;AAAA,cACzB,KAAK,OAAO;AAAA,cACZ;AAAA,cACA;AAAA,cACA,KAAK,OAAO;AAAA,YACd;AACA,iBAAK,YAAY,MAAM,OAAO;AAAA,UAChC;AAAA,QACF,GAAG,SAAS;AAAA,MACd;AAAA;AAAA,MAIQ,iBAAiB,SAAuB;AAC9C,aAAK,cAAc,YAAY,MAAM;AACnC,cAAI,KAAK,WAAW,YAAa;AACjC,gBAAM,UAAU,KAAK,IAAI,IAAI,KAAK;AAClC,cAAI,UAAU,eAAc,iBAAiB;AAC3C,iBAAK,OAAO,KAAK,eAAe,OAAO,uBAAuB;AAC9D,iBAAK,WAAW;AAChB,iBAAK,YAAY,OAAO;AAAA,UAC1B;AAAA,QACF,GAAG,eAAc,wBAAwB;AAAA,MAC3C;AAAA,MAEQ,kBAAwB;AAC9B,YAAI,KAAK,aAAa;AACpB,wBAAc,KAAK,WAAW;AAC9B,eAAK,cAAc;AAAA,QACrB;AAAA,MACF;AAAA,MAEQ,sBAA4B;AAClC,YAAI,KAAK,gBAAgB;AACvB,uBAAa,KAAK,cAAc;AAChC,eAAK,iBAAiB;AAAA,QACxB;AAAA,MACF;AAAA,MAEQ,aAAmB;AACzB,YAAI,KAAK,QAAQ;AACf,eAAK,OAAO,KAAK,SAAS;AAC1B,eAAK,SAAS;AAAA,QAChB;AAAA,MACF;AAAA;AAAA,MAIQ,mBAAmB,KAAmB;AAC5C,cAAM,aAAa,IAAI,MAAM,+CAA+C;AAC5E,YAAI,YAAY;AACd,gBAAM,QAAQ,WAAW,CAAC,EAAG,YAAY;AACzC,eAAK,gBAAiB,UAAU,UAAU,UAAU,SAAU,SAAS;AAAA,QACzE;AAEA,cAAM,aAAa,IAAI,MAAM,4BAA4B;AACzD,YAAI,YAAY;AACd,eAAK,mBAAmB;AAAA,QAC1B;AAEA,cAAM,YAAY,IAAI,MAAM,iCAAiC;AAC7D,YAAI,WAAW;AACb,gBAAM,mBAAmB,KAAK;AAC9B,eAAK,uBAAuB,oBAAoB,QAAQ,QAAQ,GAAG,KAAK,MAAM;AAC5E,mBAAO,KAAK,sBAAsB;AAAA,UACpC,CAAC;AAED,gBAAM,eAAe,UAAU,CAAC;AAChC,gBAAM,UAAU,aAAa,WAAW,KAAK,OAAO,WAAW,IAC3D,aAAa,MAAM,KAAK,OAAO,YAAY,MAAM,EAAE,QAAQ,OAAO,EAAE,IACpE;AACJ,eAAK,gBAAgB;AAAA,YACnB,IAAI,eAAc;AAAA,cAChB,KAAK,OAAO;AAAA,cACZ,KAAK,OAAO;AAAA,cACZ,KAAK,IAAI;AAAA,YACX;AAAA,YACA,MAAM;AAAA,YACN,WAAW,KAAK,IAAI;AAAA,UACtB;AAAA,QACF;AAAA,MACF;AAAA,MAEA,MAAc,wBAAuC;AACnD,YAAI,CAAC,KAAK,cAAe;AACzB,cAAM,MAAM,KAAK;AACjB,aAAK,gBAAgB;AAErB,cAAM,UAAU,KAAK,IAAI;AACzB,cAAM,YAAY,UAAU,IAAI,aAAa;AAE7C,YAAI,WAAW,eAAc,yBAA0B;AAEvD,YAAI,KAAK,UAAU,UAAU;AAC3B,gBAAM,KAAK,sBAAsB,KAAK,SAAS,QAAQ;AACvD;AAAA,QACF;AAEA,cAAM,KAAK,sBAAsB,KAAK,SAAS,QAAQ;AAAA,MACzD;AAAA,MAEA,MAAc,sBACZ,KACA,UACA,UACe;AACf,YAAI;AACF,gBAAM,OAAO,MAAS,YAAS,IAAI,IAAI;AACvC,eAAK,WAAW,KAAK,EAAE,MAAM,WAAW,IAAI,WAAW,SAAS,CAAC;AACjE,gBAAS,UAAO,IAAI,IAAI,EAAE,MAAM,MAAM;AAAA,UAAC,CAAC;AAAA,QAC1C,SAAS,KAAK;AACZ,eAAK,OAAO,KAAK,6BAA6B,GAAG,EAAE;AAAA,QACrD;AAAA,MACF;AAAA,MAEA,MAAc,sBACZ,KACA,SACA,UACe;AACf,YAAI;AACF,gBAAM,YAAY,MAAM,eAAc,eAAe,KAAK,OAAO,WAAW;AAE5E,cAAI,CAAC,UAAU,IAAI;AACjB,iBAAK,SAAS,KAAK;AAAA,cACjB,IAAI,oBAAoB,KAAK,IAAI,CAAC;AAAA,cAClC,WAAW,oBAAI,KAAK;AAAA,cACpB,QAAQ,EAAE,MAAM,SAAS,IAAI,mBAAmB;AAAA,cAChD,UAAU;AAAA,cACV,MAAM;AAAA,gBACJ,WAAW,KAAK,OAAO;AAAA,gBACvB,aAAa,UAAU;AAAA,cACzB;AAAA,YACF,CAAC;AACD,iBAAK,OAAO,MAAM,8CAA8C;AAChE,iBAAK,SAAS;AACd,iBAAK,WAAW;AAChB,iBAAK,SAAS;AACd;AAAA,UACF;AAEA,cAAI,YAAY;AAChB,cAAI;AACF,kBAAM,WAAW,MAAS,QAAK,IAAI,IAAI;AACvC,wBAAY,SAAS;AAAA,UACvB,QAAQ;AAAA,UAER;AAEA,gBAAM,QAAQ,KAAK;AACnB,gBAAM,WAAW,KAAK;AAEtB,gBAAM,UAA4B;AAAA,YAChC,IAAI,IAAI;AAAA,YACR,UAAU,KAAK,OAAO;AAAA,YACtB,UAAU,KAAK,OAAO;AAAA,YACtB,WAAW,IAAI;AAAA,YACf;AAAA,YACA;AAAA,YACA,MAAM,IAAI;AAAA,YACV,aAAa,KAAK,OAAO;AAAA,YACzB,cAAc,KAAK,OAAO;AAAA,YAC1B;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAEA,cAAI;AACF,iBAAK,GAAG,cAAc,OAAO;AAC7B,iBAAK,SAAS,KAAK;AAAA,cACjB,IAAI,OAAO,IAAI,EAAE;AAAA,cACjB,WAAW,oBAAI,KAAK;AAAA,cACpB,QAAQ,EAAE,MAAM,SAAS,IAAI,mBAAmB;AAAA,cAChD,UAAU;AAAA,cACV,MAAM;AAAA,gBACJ,UAAU,KAAK,OAAO;AAAA,gBACtB,UAAU,KAAK,OAAO;AAAA,gBACtB,WAAW,IAAI;AAAA,gBACf;AAAA,gBACA;AAAA,cACF;AAAA,YACF,CAAC;AAAA,UACH,SAAS,KAAK;AACZ,iBAAK,OAAO,MAAM,6BAA6B,GAAG,EAAE;AAAA,UACtD;AAAA,QACF,SAAS,KAAK;AACZ,eAAK,OAAO,MAAM,4BAA4B,GAAG,EAAE;AAAA,QACrD;AAAA,MACF;AAAA,MAEA,MAAc,2BAA2B,UAA0C;AACjF,cAAM,QAAQ,eAAc;AAAA,UAC1B,KAAK,OAAO;AAAA,UACZ,KAAK,OAAO;AAAA,UACZ,SAAS;AAAA,QACX;AACA,cAAM,eAAe,GAAG,KAAK,OAAO,YAAY,IAAI,KAAK,OAAO,QAAQ,IAAI,KAAK;AAEjF,YAAI;AACF,gBAAM,KAAK,OAAO,aAAa,UAAU,cAAc,SAAS,IAAI;AACpE,gBAAM,YAAY,SAAS,KAAK;AAEhC,gBAAM,UAA4B;AAAA,YAChC,IAAI;AAAA,YACJ,UAAU,KAAK,OAAO;AAAA,YACtB,UAAU,KAAK,OAAO;AAAA,YACtB,WAAW,SAAS;AAAA,YACpB,SAAS,SAAS,YAAY,SAAS,WAAW;AAAA,YAClD,UAAU,SAAS;AAAA,YACnB,MAAM;AAAA,YACN,aAAa,KAAK,OAAO;AAAA,YACzB,cAAc,KAAK,OAAO;AAAA,YAC1B;AAAA,YACA,OAAO,KAAK;AAAA,YACZ,UAAU,KAAK;AAAA,UACjB;AAEA,eAAK,GAAG,cAAc,OAAO;AAC7B,eAAK,SAAS,KAAK;AAAA,YACjB,IAAI,OAAO,KAAK;AAAA,YAChB,WAAW,oBAAI,KAAK;AAAA,YACpB,QAAQ,EAAE,MAAM,SAAS,IAAI,mBAAmB;AAAA,YAChD,UAAU;AAAA,YACV,MAAM;AAAA,cACJ,UAAU,KAAK,OAAO;AAAA,cACtB,UAAU,KAAK,OAAO;AAAA,cACtB,WAAW;AAAA,cACX,UAAU,SAAS;AAAA,cACnB;AAAA,YACF;AAAA,UACF,CAAC;AAAA,QACH,SAAS,KAAK;AACZ,eAAK,OAAO,MAAM,6CAA6C,GAAG,EAAE;AAAA,QACtE;AAAA,MACF;AAAA,IACF;AAAA;AAAA;;;ACrkBA,OAAO,WAAW;AAAlB,IAmBa;AAnBb;AAAA;AAAA;AAmBO,IAAM,qBAAN,MAAM,oBAAgD;AAAA,MAc3D,YACmB,QACA,QACA,IACjB;AAHiB;AACA;AACA;AAAA,MAChB;AAAA,MAjBM,KAAK;AAAA,MACL,OAAO;AAAA,MACP,aAAa;AAAA,MAEb,oBAA8C;AAAA,QACrD,cAAc;AAAA,QACd,QAAQ;AAAA,QACR,QAAQ;AAAA,MACV;AAAA,MAEQ,cAAmC;AAAA,MACnC,SAAS;AAAA,MAQjB,iBAAiB,UAA2B,WAAyB;AACnE,aAAK,SAAS;AAEd,aAAK,cAAc,SAAS;AAAA,UAC1B,CAAC,UAAU;AAAE,iBAAK,YAAY,KAAK,EAAE,MAAM,CAAC,QAAQ,KAAK,OAAO,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAAA,UAAE;AAAA,UAClG,KAAK;AAAA,QACP;AAEA,aAAK,OAAO,KAAK,mCAAmC,KAAK,OAAO,QAAQ,EAAE;AAAA,MAC5E;AAAA,MAEA,mBAAmB,WAAyB;AAC1C,aAAK,SAAS;AACd,YAAI,KAAK,aAAa;AACpB,eAAK,YAAY;AACjB,eAAK,cAAc;AAAA,QACrB;AACA,aAAK,OAAO,KAAK,mCAAmC,KAAK,OAAO,QAAQ,EAAE;AAAA,MAC5E;AAAA,MAEA,UAAU,QAAuB;AAC/B,aAAK,SAAS;AAAA,MAChB;AAAA,MAEA,MAAc,YAAY,OAAkC;AAC1D,YAAI,CAAC,KAAK,OAAQ;AAElB,cAAM,YAAY,MAAM,aAAa,KAAK,IAAI;AAC9C,cAAM,eAAe,oBAAmB;AAAA,UACtC,KAAK,OAAO;AAAA,UACZ,KAAK,OAAO;AAAA,UACZ;AAAA,QACF;AAEA,cAAM,UAAU,MAAM,MAAM,MAAM,IAAI,EACnC,OAAO,EAAE,OAAO,KAAK,OAAO,YAAY,oBAAoB,KAAK,CAAC,EAClE,KAAK,EAAE,SAAS,KAAK,OAAO,YAAY,CAAC,EACzC,SAAS;AAEZ,cAAM,KAAK,OAAO,aAAa,UAAU,cAAc,OAAO;AAE9D,aAAK,GAAG,gBAAgB;AAAA,UACtB,UAAU,KAAK,OAAO;AAAA,UACtB;AAAA,UACA,MAAM;AAAA,UACN,aAAa,KAAK,OAAO;AAAA,UACzB,cAAc,KAAK,OAAO;AAAA,UAC1B,WAAW,QAAQ;AAAA,UACnB,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAAA,MAEA,OAAO,cAAc,cAAsB,UAAkB,WAA2B;AACtF,eAAO,GAAG,YAAY,IAAI,QAAQ,IAAI,SAAS;AAAA,MACjD;AAAA,IACF;AAAA;AAAA;;;AC9FA,IAIM,oBACA,wBACA,2BACA,4BACA,8BAEO;AAVb;AAAA;AAAA;AAIA,IAAM,qBAAqB,IAAI,KAAK;AACpC,IAAM,yBAAyB,KAAK;AACpC,IAAM,4BAA4B;AAClC,IAAM,6BAA6B;AACnC,IAAM,+BAA+B;AAE9B,IAAM,mBAAN,MAAuB;AAAA,MAG5B,YACmB,IACA,QACA,UACA,aACjB;AAJiB;AACA;AACA;AACA;AAAA,MAChB;AAAA,MAPK,QAA8C;AAAA,MAStD,QAAc;AACZ,aAAK,kBAAkB,kBAAkB;AAAA,MAC3C;AAAA,MAEA,OAAa;AACX,YAAI,KAAK,OAAO;AACd,uBAAa,KAAK,KAAK;AACvB,eAAK,QAAQ;AAAA,QACf;AAAA,MACF;AAAA,MAEA,MAAM,WAA6B;AACjC,aAAK,GAAG,mBAAmB;AAE3B,cAAM,WAAW,KAAK,GAAG,mBAAmB;AAC5C,YAAI,kBAAkB;AACtB,YAAI,uBAAuB;AAC3B,YAAI,YAAY;AAEhB,mBAAW,UAAU,UAAU;AAC7B,qBAAW,MAAM,OAAO,SAAS;AAC/B,kBAAM,WAAW,aAAa,GAAG,QAAQ;AACzC,kBAAM,SAAS,KAAK,GAAG,qBAAqB,OAAO,UAAU,QAAQ;AACrE,gBAAI,CAAC,OAAQ;AAEb,gBAAI,OAAO,kBAAkB,MAAM;AACjC,oBAAM,SAAS,KAAK,IAAI,IAAI,OAAO,gBAAgB;AACnD,oBAAM,UAAU,KAAK,GAAG,qBAAqB,OAAO,UAAU,GAAG,UAAU,MAAM;AACjF,sCAAwB,QAAQ;AAChC,yBAAW,OAAO,SAAS;AACzB,mCAAmB,IAAI;AACvB,sBAAM,KAAK,WAAW,IAAI,IAAI;AAAA,cAChC;AACA,mBAAK,GAAG,uBAAuB,OAAO,UAAU,MAAM;AAAA,YACxD;AAEA,gBAAI,OAAO,gBAAgB,MAAM;AAC/B,oBAAM,WAAW,OAAO,cAAc,OAAO,OAAO;AACpD,kBAAI,QAAQ,KAAK,GAAG,gBAAgB,OAAO,UAAU,GAAG,QAAQ;AAEhE,oBAAM,aAAa,MAAM,aAAa;AACtC,kBAAI,aAAa,4BAA4B;AAC3C,qBAAK,iBAAiB,8BAA8B,OAAO,UAAU,GAAG,UAAU,UAAU;AAAA,cAC9F,WAAW,aAAa,2BAA2B;AACjD,qBAAK,iBAAiB,6BAA6B,OAAO,UAAU,GAAG,UAAU,UAAU;AAAA,cAC7F;AACA,kBAAI,aAAa,8BAA8B;AAC7C,4BAAY;AAAA,cACd;AAEA,qBAAO,MAAM,aAAa,YAAY,MAAM,eAAe,GAAG;AAC5D,sBAAM,SAAS,KAAK,GAAG,kBAAkB,OAAO,UAAU,GAAG,UAAU,EAAE;AACzE,oBAAI,OAAO,WAAW,EAAG;AACzB,2BAAW,OAAO,QAAQ;AACxB,uBAAK,GAAG,qBAAqB,OAAO,UAAU,GAAG,UAAU,IAAI,UAAU,CAAC;AAC1E,qCAAmB,IAAI;AACvB;AACA,wBAAM,KAAK,WAAW,IAAI,IAAI;AAAA,gBAChC;AACA,wBAAQ,KAAK,GAAG,gBAAgB,OAAO,UAAU,GAAG,QAAQ;AAAA,cAC9D;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAEA,cAAM,UAAU,KAAK,GAAG,mBAAmB;AAC3C,mBAAW,SAAS,SAAS;AAC3B,eAAK,GAAG,sBAAsB,MAAM,QAAQ;AAC5C,cAAI;AACF,kBAAM,UAAU,KAAK,GAAG,wBAAwB,MAAM,QAAQ;AAC9D,uBAAW,OAAO,SAAS;AACzB,iCAAmB,IAAI;AACvB;AACA,oBAAM,KAAK,WAAW,IAAI,IAAI;AAAA,YAChC;AACA,iBAAK,GAAG,0BAA0B,MAAM,QAAQ;AAChD,iBAAK,GAAG,qBAAqB,MAAM,QAAQ;AAAA,UAC7C,SAAS,KAAK;AACZ,iBAAK,OAAO,MAAM,sBAAsB,MAAM,QAAQ,KAAK,GAAG,EAAE;AAAA,UAClE;AAAA,QACF;AAEA,YAAI,uBAAuB,GAAG;AAC5B,eAAK,SAAS,KAAK;AAAA,YACjB,IAAI,aAAa,KAAK,IAAI,CAAC;AAAA,YAC3B,WAAW,oBAAI,KAAK;AAAA,YACpB,QAAQ,EAAE,MAAM,SAAS,IAAI,mBAAmB;AAAA,YAChD,UAAU;AAAA,YACV,MAAM;AAAA,cACJ,SAAS,KAAK,MAAM,kBAAkB,OAAO,IAAI;AAAA,cACjD,iBAAiB;AAAA,YACnB;AAAA,UACF,CAAC;AAAA,QACH;AAEA,eAAO;AAAA,MACT;AAAA,MAEQ,kBAAkB,YAA0B;AAClD,aAAK,QAAQ,WAAW,YAAY;AAClC,cAAI;AACF,kBAAM,mBAAmB,MAAM,KAAK,SAAS;AAC7C,kBAAM,eAAe,mBAAmB,yBAAyB;AACjE,iBAAK,kBAAkB,YAAY;AAAA,UACrC,SAAS,KAAK;AACZ,iBAAK,OAAO,MAAM,0BAA0B,GAAG,EAAE;AACjD,iBAAK,kBAAkB,kBAAkB;AAAA,UAC3C;AAAA,QACF,GAAG,UAAU;AAAA,MACf;AAAA,MAEQ,iBAAiB,UAAkB,UAAkB,UAAkB,YAA0B;AACvG,aAAK,SAAS,KAAK;AAAA,UACjB,IAAI,GAAG,QAAQ,IAAI,QAAQ,IAAI,KAAK,IAAI,CAAC;AAAA,UACzC,WAAW,oBAAI,KAAK;AAAA,UACpB,QAAQ,EAAE,MAAM,SAAS,IAAI,mBAAmB;AAAA,UAChD;AAAA,UACA,MAAM;AAAA,YACJ;AAAA,YACA;AAAA,YACA,cAAc,KAAK,MAAM,aAAa,GAAG;AAAA,UAC3C;AAAA,QACF,CAAC;AAAA,MACH;AAAA,MAEA,MAAc,WAAW,UAAiC;AACxD,YAAI;AACF,gBAAM,KAAK,YAAY,WAAW,QAAQ;AAAA,QAC5C,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAAA;AAAA;;;ACxJA,IAOM,kBAEO;AATb;AAAA;AAAA;AAOA,IAAM,mBAAmB,CAAC,OAAO,OAAO,MAAM;AAEvC,IAAM,oBAAN,MAAwB;AAAA,MAC7B,YAA6B,IAAiB;AAAjB;AAAA,MAAkB;AAAA,MAE/C,SAAS,UAAkB,UAAkB,WAAmB,SAAiB,SAAmC;AAClH,cAAM,WAAW,KAAK,gBAAgB,UAAU,UAAU,WAAW,OAAO;AAC5E,eAAO,KAAK,cAAc,UAAU,OAAO;AAAA,MAC7C;AAAA,MAEQ,gBAAgB,UAAkB,UAAkB,WAAmB,SAA8C;AAC3H,cAAM,WAAW,KAAK,GAAG,cAAc,UAAU,UAAU,WAAW,OAAO;AAC7E,YAAI,SAAS,SAAS,GAAG;AACvB,iBAAO;AAAA,QACT;AAEA,cAAM,YAAY,iBAAiB,OAAO,OAAK,MAAM,QAAQ;AAC7D,mBAAW,MAAM,WAAW;AAC1B,gBAAM,mBAAmB,KAAK,GAAG,cAAc,UAAU,IAAI,WAAW,OAAO;AAC/E,cAAI,iBAAiB,SAAS,GAAG;AAC/B,mBAAO;AAAA,UACT;AAAA,QACF;AAEA,eAAO,CAAC;AAAA,MACV;AAAA,MAEQ,cAAc,UAAuC,SAAmC;AAC9F,cAAM,iBAAiB,SAAS,SAAS,IACrC,KAAK,KAAK,KAAK,IAAI,GAAG,SAAS,IAAI,OAAK,EAAE,QAAQ,CAAC,CAAC,IACpD;AAEJ,cAAM,QAAkB;AAAA,UACtB;AAAA,UACA;AAAA,UACA,yBAAyB,cAAc;AAAA,UACvC;AAAA,QACF;AAEA,YAAI,CAAC,SAAS,MAAM;AAClB,gBAAM,KAAK,0BAA0B;AAAA,QACvC;AAEA,mBAAW,OAAO,UAAU;AAC1B,gBAAM,KAAK,WAAW,IAAI,SAAS,QAAQ,CAAC,CAAC,GAAG;AAChD,gBAAM,KAAK,IAAI,IAAI;AAAA,QACrB;AAEA,YAAI,CAAC,SAAS,MAAM;AAClB,gBAAM,KAAK,gBAAgB;AAAA,QAC7B;AAEA,eAAO,MAAM,KAAK,IAAI;AAAA,MACxB;AAAA,IACF;AAAA;AAAA;;;AC7DA,IASa;AATb;AAAA;AAAA;AASO,IAAM,mBAAN,MAAuB;AAAA,MAC5B,YACmB,IACA,gBACjB;AAFiB;AACA;AAAA,MAChB;AAAA,MAEH,kBAAkB,UAAkB,aAAoD;AACtF,cAAM,SAAS,KAAK,GAAG,UAAU,QAAQ;AACzC,YAAI,CAAC,OAAQ,QAAO,EAAE,WAAW,CAAC,GAAG,YAAY,EAAE,aAAa,EAAE,GAAG,kBAAkB,EAAE;AAEzF,cAAM,QAAQ,KAAK,eAAe,eAAe,QAAQ;AACzD,cAAM,YAA4C,CAAC;AACnD,YAAI,UAAU;AAEd,cAAM,iBAAiB,cAAc;AAAA,UACnC,iBAAiB,YAAY;AAAA,UAC7B,gBAAgB,YAAY;AAAA,UAC5B,kBAAmB,YAAY,kBAAkB,YAAY,iBAAiB,QAAS;AAAA,QACzF,IAAI;AAEJ,mBAAW,MAAM,OAAO,SAAS;AAC/B,gBAAM,WAAW,aAAa,GAAG,QAAQ;AACzC,gBAAM,SAAS,KAAK,GAAG,qBAAqB,UAAU,QAAQ;AAC9D,gBAAM,gBAAgB,QAAQ,iBAAiB;AAC/C,gBAAM,cAAc,QAAQ,eAAe;AAE3C,gBAAM,cAAc,OAAO,QAAQ,GAAG,QAAQ;AAC9C,gBAAM,kBAAkB,aAAa,uBAAuB;AAC5D,gBAAM,cAAc,kBAAkB,IAClC,kBACC,aAAa,sBAAsB;AAExC,cAAI,cAAc;AAClB,cAAI,kBAAkB,MAAM;AAC1B,kBAAM,mBAAmB,gBAAgB;AACzC,0BAAe,cAAc,mBAAoB,IAAI,OAAO,OAAO;AAAA,UACrE;AAEA,cAAI,OAAO,SAAS,YAAY,gBAAgB;AAC9C,0BAAc,cAAc,eAAe,mBAAmB;AAAA,UAChE;AAEA,cAAI,0BAAyC;AAC7C,cAAI,gBAAgB,QAAQ,cAAc,aAAa;AACrD,sCAA0B,kBAAkB,OAAO,iBAAiB,cAAc,eAAe;AACjG,0BAAc;AAAA,UAChB;AAEA,oBAAU,GAAG,QAAQ,IAAI,EAAE,aAAa,eAAe,aAAa,aAAa,wBAAwB;AACzG,qBAAW;AAAA,QACb;AAEA,cAAM,qBAAqB,OAAO,QAAQ,CAAC,IACtC,KAAK,GAAG,qBAAqB,UAAU,iBAAwB,GAAG,iBAAiB,IACpF;AACJ,cAAM,UAAW,IAAI,KAAK,qBAAsB;AAChD,mBAAW;AAEX,eAAO,EAAE,WAAW,YAAY,EAAE,aAAa,QAAQ,GAAG,kBAAkB,SAAS,eAAe;AAAA,MACtG;AAAA,IACF;AAAA;AAAA;;;ACrEA;AAAA;AAAA;AAAA;AAAA,IA8BM,8BAmBA,yBAEA,4BAIO;AAvDb;AAAA;AAAA;AACA;AAMA;AACA;AACA;AACA;AACA;AAmBA,IAAM,+BAA+B;AAmBrC,IAAM,0BAA0B;AAEhC,IAAM,6BAA6B;AAI5B,IAAM,uBAAN,MAAM,sBAAqB;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MAEA,aAAa,oBAAI,IAAkC;AAAA,MAC5D,cAAqD;AAAA,MAC5C;AAAA,MAER;AAAA,MACA;AAAA,MAET,YAAY,QAAoC;AAC9C,aAAK,KAAK,OAAO;AACjB,aAAK,SAAS,OAAO;AACrB,aAAK,WAAW,OAAO;AACvB,aAAK,kBAAkB,OAAO;AAC9B,aAAK,kBAAkB,OAAO;AAC9B,aAAK,iBAAiB,OAAO;AAC7B,aAAK,cAAc,OAAO;AAC1B,aAAK,cAAc,OAAO;AAC1B,aAAK,qBAAqB,OAAO;AACjC,aAAK,uBAAuB,OAAO;AACnC,aAAK,qBAAqB,OAAO,sBAAsB;AAEvD,aAAK,mBAAmB,IAAI;AAAA,UAC1B,KAAK;AAAA,UACL,KAAK,OAAO,MAAM,WAAW;AAAA,UAC7B,KAAK;AAAA,UACL,KAAK;AAAA,QACP;AACA,aAAK,oBAAoB,IAAI,kBAAkB,KAAK,EAAE;AACtD,aAAK,mBAAmB,IAAI,iBAAiB,KAAK,IAAI,KAAK,cAAc;AAAA,MAC3E;AAAA,MAEA,MAAM,QAAuB;AAC3B,aAAK,OAAO,KAAK,+BAA+B;AAChD,aAAK,iBAAiB,MAAM;AAE5B,cAAM,kBAAkB,KAAK,GAAG,mBAAmB;AACnD,mBAAW,UAAU,iBAAiB;AACpC,cAAI;AACF,kBAAM,KAAK,gBAAgB,OAAO,UAAU;AAAA,cAC1C,QAAQ;AAAA,gBACN,MAAM,OAAO;AAAA,gBACb,SAAS,OAAO;AAAA,gBAChB,SAAS,OAAO;AAAA,gBAChB,cAAc,OAAO;AAAA,gBACrB,eAAe,OAAO;AAAA,gBACtB,eAAe,OAAO;AAAA,cACxB;AAAA,YACF,CAAC;AAAA,UACH,SAAS,KAAK;AACZ,iBAAK,OAAO,MAAM,iCAAiC,OAAO,QAAQ,KAAK,GAAG,EAAE;AAAA,UAC9E;AAAA,QACF;AAEA,aAAK,cAAc,YAAY,MAAM;AACnC,eAAK,iBAAiB;AAAA,QACxB,GAAG,uBAAuB;AAE1B,aAAK,OAAO,KAAK,8BAA8B;AAAA,MACjD;AAAA,MAEA,OAAa;AACX,aAAK,OAAO,KAAK,+BAA+B;AAEhD,YAAI,KAAK,aAAa;AACpB,wBAAc,KAAK,WAAW;AAC9B,eAAK,cAAc;AAAA,QACrB;AAEA,aAAK,iBAAiB,KAAK;AAE3B,mBAAW,CAAC,QAAQ,KAAK,KAAK,YAAY;AACxC,eAAK,sBAAsB,QAAQ;AAAA,QACrC;AACA,aAAK,WAAW,MAAM;AAEtB,aAAK,OAAO,KAAK,8BAA8B;AAAA,MACjD;AAAA,MAEA,MAAM,gBAAgB,UAAkB,QAA8C;AACpF,YAAI,KAAK,WAAW,IAAI,QAAQ,GAAG;AACjC,eAAK,sBAAsB,QAAQ;AACnC,eAAK,WAAW,OAAO,QAAQ;AAAA,QACjC;AAEA,cAAM,SAA0B;AAAA,UAC9B;AAAA,UACA,MAAM,OAAO,OAAO;AAAA,UACpB,SAAS,OAAO,OAAO;AAAA,UACvB,SAAS,OAAO,OAAO;AAAA,UACvB,cAAc,OAAO,OAAO;AAAA,UAC5B,eAAe,OAAO,OAAO;AAAA,UAC7B,eAAe,OAAO,OAAO;AAAA,QAC/B;AAEA,aAAK,GAAG,aAAa;AAAA,UACnB;AAAA,UACA,SAAS,OAAO;AAAA,UAChB,MAAM,OAAO;AAAA,UACb,SAAS,OAAO;AAAA,UAChB,cAAc,OAAO;AAAA,UACrB,eAAe,OAAO;AAAA,UACtB,eAAe,OAAO;AAAA,QACxB,CAAC;AAED,aAAK,GAAG,cAAc,QAAQ;AAE9B,cAAM,eAAe;AAAA,UACnB,OAAO;AAAA,UACP,KAAK;AAAA,UACL,KAAK;AAAA,QACP;AAEA,cAAM,aAAa,OAAO,SAAS,WAAW,WAAoB;AAElE,cAAM,UAA2B,CAAC;AAClC,mBAAW,MAAM,OAAO,SAAS;AAC/B,gBAAM,gBAAgB,KAAK,GAAG,qBAAqB,UAAU,aAAa,GAAG,QAAQ,EAAS;AAC9F,gBAAM,cAAc,eAAe,eAAe;AAClD,gBAAM,eAAe,eAAe,gBAAgB,cAAc,GAAG,QAAQ;AAE7E,gBAAM,eAAoC;AAAA,YACxC;AAAA,YACA,UAAU,GAAG;AAAA,YACb,oBAAoB,KAAK;AAAA,YACzB,aAAa,KAAK;AAAA,YAClB;AAAA,YACA;AAAA,YACA,QAAQ;AAAA,YACR,MAAM;AAAA,YACN,cAAc,OAAO;AAAA,UACvB;AAEA,gBAAM,SAAS,IAAI;AAAA,YACjB;AAAA,YACA,KAAK,OAAO,MAAM,UAAU,QAAQ,IAAI,GAAG,QAAQ,EAAE;AAAA,YACrD,KAAK;AAAA,YACL,KAAK;AAAA,YACL,KAAK;AAAA,UACP;AAEA,gBAAM,UAAU,KAAK,gBAAgB,aAAa,GAAG,OAAO,QAAQ,IAAI,GAAG,QAAQ,IAAI,MAAM;AAC7F,cAAI,SAAS;AACX,kBAAM,OAAO,MAAM,OAAO;AAAA,UAC5B;AAEA,kBAAQ,KAAK,MAAM;AAAA,QACrB;AAEA,cAAM,qBAAqB,KAAK,GAAG,qBAAqB,UAAU,iBAAiB;AACnF,cAAM,cAAwC;AAAA,UAC5C;AAAA,UACA,aAAa,KAAK;AAAA,UAClB,aAAa,oBAAoB,eAAe;AAAA,UAChD,cAAc,oBAAoB,gBAAgB;AAAA,UAClD,YAAY;AAAA,UACZ,aAAa;AAAA,QACf;AAEA,cAAM,qBAAqB,IAAI;AAAA,UAC7B;AAAA,UACA,KAAK,OAAO,MAAM,SAAS,QAAQ,EAAE;AAAA,UACrC,KAAK;AAAA,QACP;AAEA,cAAM,WAAW,KAAK,gBAAgB,YAAY,QAAQ;AAC1D,YAAI,UAAU;AACZ,6BAAmB,iBAAiB,UAAU,QAAQ;AAAA,QACxD;AAEA,YAAI,OAAO,SAAS,UAAU;AAC5B,6BAAmB,UAAU,KAAK;AAAA,QACpC;AAEA,cAAM,oBAAoB,KAAK,wBAAwB,UAAU,MAAM;AAEvE,cAAM,QAA8B;AAAA,UAClC;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,cAAc;AAAA,UACd,eAAe;AAAA,UACf,uBAAuB;AAAA,UACvB,gBAAgB;AAAA,QAClB;AAEA,aAAK,WAAW,IAAI,UAAU,KAAK;AAEnC,YAAI,OAAO,SAAS,UAAU;AAC5B,gBAAM,wBAAwB,WAAW,MAAM;AAC7C,kBAAM,eAAe,KAAK,WAAW,IAAI,QAAQ;AACjD,gBAAI,CAAC,gBAAgB,aAAa,eAAgB;AAElD,iBAAK,OAAO,KAAK,iCAAiC,QAAQ,WAAW,6BAA6B,GAAI,+CAA0C;AAEhJ,iBAAK,SAAS,KAAK;AAAA,cACjB,IAAI,6BAA6B,QAAQ,IAAI,KAAK,IAAI,CAAC;AAAA,cACvD,WAAW,oBAAI,KAAK;AAAA,cACpB,QAAQ,EAAE,MAAM,SAAS,IAAI,mBAAmB;AAAA,cAChD,UAAU;AAAA,cACV,MAAM;AAAA,gBACJ;AAAA,gBACA,cAAc;AAAA,gBACd,cAAc;AAAA,gBACd,QAAQ;AAAA,cACV;AAAA,YACF,CAAC;AAED,uBAAW,UAAU,aAAa,SAAS;AACzC,qBAAO,iBAAiB,EAAE,MAAM,SAAO;AACrC,qBAAK,OAAO,MAAM,8BAA8B,QAAQ,qBAAqB,GAAG,EAAE;AAAA,cACpF,CAAC;AAAA,YACH;AAEA,yBAAa,mBAAmB,UAAU,IAAI;AAAA,UAChD,GAAG,0BAA0B;AAAA,QAC/B;AAEA,aAAK,SAAS,KAAK;AAAA,UACjB,IAAI,qBAAqB,QAAQ,IAAI,KAAK,IAAI,CAAC;AAAA,UAC/C,WAAW,oBAAI,KAAK;AAAA,UACpB,QAAQ,EAAE,MAAM,SAAS,IAAI,mBAAmB;AAAA,UAChD,UAAU;AAAA,UACV,MAAM;AAAA,YACJ;AAAA,YACA,MAAM,OAAO;AAAA,YACb,SAAS,OAAO,QAAQ,IAAI,OAAK,EAAE,QAAQ;AAAA,UAC7C;AAAA,QACF,CAAC;AAED,aAAK,OAAO,KAAK,yBAAyB,QAAQ,IAAI,EAAE,MAAM,OAAO,KAAK,CAAC;AAAA,MAC7E;AAAA,MAEA,MAAM,iBAAiB,UAAiC;AACtD,cAAM,QAAQ,KAAK,WAAW,IAAI,QAAQ;AAC1C,YAAI,CAAC,OAAO;AACV,eAAK,OAAO,KAAK,2BAA2B,QAAQ,EAAE;AACtD;AAAA,QACF;AAEA,YAAI,oBAAoB;AACxB,YAAI,iBAAiB;AACrB,mBAAW,MAAM,MAAM,OAAO,SAAS;AACrC,gBAAM,QAAQ,KAAK,GAAG,gBAAgB,UAAU,GAAG,QAAQ;AAC3D,+BAAqB,MAAM;AAC3B,4BAAkB,MAAM;AAAA,QAC1B;AACA,cAAM,UAAU,KAAK,MAAM,iBAAiB,OAAO,IAAI;AAEvD,aAAK,sBAAsB,QAAQ;AACnC,aAAK,WAAW,OAAO,QAAQ;AAE/B,aAAK,GAAG,kBAAkB,UAAU,KAAK,IAAI,CAAC;AAE9C,aAAK,SAAS,KAAK;AAAA,UACjB,IAAI,qBAAqB,QAAQ,IAAI,KAAK,IAAI,CAAC;AAAA,UAC/C,WAAW,oBAAI,KAAK;AAAA,UACpB,QAAQ,EAAE,MAAM,SAAS,IAAI,mBAAmB;AAAA,UAChD,UAAU;AAAA,UACV,MAAM;AAAA,YACJ;AAAA,YACA,cAAc;AAAA,YACd;AAAA,UACF;AAAA,QACF,CAAC;AAED,aAAK,OAAO,KAAK,0BAA0B,QAAQ,IAAI,EAAE,cAAc,mBAAmB,QAAQ,CAAC;AAAA,MACrG;AAAA,MAEA,YAAY,UAA2B;AACrC,eAAO,KAAK,WAAW,IAAI,QAAQ;AAAA,MACrC;AAAA,MAEA,mBAAyB;AACvB,cAAM,MAAM,oBAAI,KAAK;AAErB,mBAAW,CAAC,UAAU,KAAK,KAAK,KAAK,YAAY;AAC/C,gBAAM,EAAE,OAAO,IAAI;AAEnB,cAAI,OAAO,SAAS,eAAe,OAAO,SAAS,aAAa;AAC9D,gBAAI,CAAC,OAAO,iBAAiB,OAAO,cAAc,WAAW,EAAG;AAEhE,kBAAM,eAAe,OAAO,cAAc;AAAA,cAAK,UAC7C,sBAAqB,qBAAqB,MAAM,GAAG;AAAA,YACrD;AAEA,gBAAI,cAAc;AAChB,oBAAM,aAAa,aAAa,SAAS,WAAW,WAAoB;AACxE,yBAAW,UAAU,MAAM,SAAS;AAClC,oBAAI,OAAO,SAAS,YAAY;AAC9B,sBAAI,eAAe,UAAU;AAC3B,2BAAO,eAAe;AAAA,kBACxB;AAAA,gBACF;AAAA,cACF;AAAA,YACF,OAAO;AACL,yBAAW,UAAU,MAAM,SAAS;AAClC,oBAAI,OAAO,SAAS,UAAU;AAC5B,yBAAO,eAAe;AAAA,gBACxB;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,MAEA,OAAO,qBAAqB,MAAoB,MAAqB;AACnE,cAAM,YAAY,KAAK,OAAO;AAC9B,cAAM,cAAc,KAAK,SAAS,IAAI,KAAK,KAAK,WAAW;AAE3D,cAAM,CAAC,QAAQ,MAAM,IAAI,KAAK,UAAU,MAAM,GAAG,EAAE,IAAI,MAAM;AAC7D,cAAM,CAAC,MAAM,IAAI,IAAI,KAAK,QAAQ,MAAM,GAAG,EAAE,IAAI,MAAM;AACvD,cAAM,eAAe,SAAS,KAAK;AACnC,cAAM,aAAa,OAAO,KAAK;AAE/B,YAAI,aAAa,cAAc;AAC7B,iBAAO,KAAK,KAAK,SAAS,SAAS,KAC9B,eAAe,gBACf,cAAc;AAAA,QACrB;AAEA,YAAI,KAAK,KAAK,SAAS,SAAS,KAAK,eAAe,cAAc;AAChE,iBAAO;AAAA,QACT;AAEA,cAAM,eAAe,YAAY,KAAK;AACtC,YAAI,KAAK,KAAK,SAAS,WAAW,KAAK,cAAc,YAAY;AAC/D,iBAAO;AAAA,QACT;AAEA,eAAO;AAAA,MACT;AAAA,MAEQ,wBAAwB,UAAkB,QAA8C;AAC9F,YAAI,OAAO,SAAS,YAAY,OAAO,SAAS,aAAa;AAC3D,iBAAO;AAAA,QACT;AAEA,eAAO,KAAK,SAAS;AAAA,UACnB,EAAE,UAAU,UAAU,QAAQ,GAAG;AAAA,UACjC,CAAC,UAAuB;AACtB,iBAAK,kBAAkB,UAAU,KAAK;AAAA,UACxC;AAAA,QACF;AAAA,MACF;AAAA,MAEQ,kBAAkB,UAAkB,OAA0B;AACpE,cAAM,QAAQ,KAAK,WAAW,IAAI,QAAQ;AAC1C,YAAI,CAAC,MAAO;AAEZ,YAAI,CAAC,MAAM,gBAAgB;AACzB,gBAAM,iBAAiB;AACvB,cAAI,MAAM,uBAAuB;AAC/B,yBAAa,MAAM,qBAAqB;AACxC,kBAAM,wBAAwB;AAAA,UAChC;AAAA,QACF;AAEA,cAAM,iBAAiB,MAAM,KAAK,WAAW,QAAQ,MAAM,KAAK,SAAS;AAEzE,YAAI,gBAAgB;AAClB,gBAAM,eAAe;AAErB,cAAI,MAAM,eAAe;AACvB,yBAAa,MAAM,aAAa;AAChC,kBAAM,gBAAgB;AAAA,UACxB;AAEA,qBAAW,UAAU,MAAM,SAAS;AAClC,mBAAO,iBAAiB,EAAE,MAAM,SAAO;AACrC,mBAAK,OAAO,MAAM,8BAA8B,QAAQ,KAAK,GAAG,EAAE;AAAA,YACpE,CAAC;AAAA,UACH;AAEA,gBAAM,mBAAmB,UAAU,IAAI;AAAA,QACzC,OAAO;AACL,cAAI,MAAM,eAAe;AACvB,yBAAa,MAAM,aAAa;AAAA,UAClC;AAEA,gBAAM,gBAAgB,WAAW,MAAM;AACrC,kBAAM,eAAe;AACrB,kBAAM,gBAAgB;AAEtB,uBAAW,UAAU,MAAM,SAAS;AAClC,qBAAO,eAAe;AAAA,YACxB;AAEA,gBAAI,MAAM,OAAO,SAAS,UAAU;AAClC,oBAAM,mBAAmB,UAAU,KAAK;AAAA,YAC1C;AAAA,UACF,GAAG,MAAM,OAAO,gBAAgB,GAAI;AAAA,QACtC;AAAA,MACF;AAAA,MAEQ,sBAAsB,UAAwB;AACpD,cAAM,QAAQ,KAAK,WAAW,IAAI,QAAQ;AAC1C,YAAI,CAAC,MAAO;AAEZ,mBAAW,UAAU,MAAM,SAAS;AAClC,iBAAO,KAAK;AAAA,QACd;AAEA,cAAM,mBAAmB,mBAAmB,QAAQ;AAEpD,YAAI,MAAM,mBAAmB;AAC3B,gBAAM,kBAAkB;AAAA,QAC1B;AAEA,YAAI,MAAM,eAAe;AACvB,uBAAa,MAAM,aAAa;AAChC,gBAAM,gBAAgB;AAAA,QACxB;AAEA,YAAI,MAAM,uBAAuB;AAC/B,uBAAa,MAAM,qBAAqB;AACxC,gBAAM,wBAAwB;AAAA,QAChC;AAAA,MACF;AAAA,IACF;AAAA;AAAA;;;ACheO,IAAM,kBAAN,MAAsB;AAAA,EACV,UAAgC,CAAC;AAAA,EAElD,SAAS,UAA4B,UAAwB;AAC3D,SAAK,QAAQ,KAAK,EAAE,UAAU,SAAS,CAAC;AACxC,SAAK,QAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AAAA,EACrD;AAAA,EAEA,WAAW,OAA6C;AACtD,WAAO,KAAK,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,cAAc,KAAK,CAAC,GAAG;AAAA,EACpE;AACF;;;AClBA,SAAS,aAAgC;;;ACAlC,IAAM,eAAN,MAAmB;AAAA,EAChB;AAAA,EACA,eAAe;AAAA,EAEvB,YAAY,QAAgB;AAC1B,SAAK,aAAa,SAAS,IAAI,MAAO,SAAS;AAAA,EACjD;AAAA,EAEA,aAAsB;AACpB,QAAI,KAAK,eAAe,EAAG,QAAO;AAElC,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,MAAM,KAAK,gBAAgB,KAAK,YAAY;AAC9C,WAAK,eAAe;AACpB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEA,UAAU,QAAsB;AAC9B,SAAK,aAAa,SAAS,IAAI,MAAO,SAAS;AAAA,EACjD;AACF;;;ADlBA,IAAM,MAAM,OAAO,KAAK,CAAC,KAAM,GAAI,CAAC;AACpC,IAAM,MAAM,OAAO,KAAK,CAAC,KAAM,GAAI,CAAC;AAEpC,SAAS,mBAAmB,OAAuB;AACjD,UAAQ,OAAO;AAAA,IACb,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EACX;AACF;AAEA,SAAS,gBAAgB,QAAwC;AAC/D,QAAM,cAAc,mBAAmB,OAAO,KAAK;AACnD,QAAM,OAAO,CAAC,gBAAgB,aAAa,SAAS,MAAM,aAAa,MAAM,QAAQ;AAErF,MAAI,OAAO,QAAQ,GAAG;AACpB,SAAK,KAAK,OAAO,YAAY,OAAO,KAAK,OAAO,OAAO,KAAK,EAAE;AAAA,EAChE;AAEA,OAAK,KAAK,MAAM,cAAc,WAAW,SAAS,QAAQ,KAAK,QAAQ;AACvE,SAAO;AACT;AAEO,IAAM,uBAAN,MAAsD;AAAA,EACnD;AAAA,EACA;AAAA,EACA,UAA+B;AAAA,EAC/B,iBAAiB,oBAAI,IAAmC;AAAA,EACxD,eAAe,OAAO,MAAM,CAAC;AAAA,EAC7B,YAAY;AAAA;AAAA,EAGZ,eAAe;AAAA,EACf,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,YAAY,KAAK,IAAI;AAAA,EAE7B,YAAY,QAA8B;AACxC,SAAK,SAAS,EAAE,GAAG,OAAO;AAC1B,SAAK,eAAe,IAAI,aAAa,OAAO,MAAM;AAClD,SAAK,YAAY;AAAA,EACnB;AAAA,EAEQ,cAAoB;AAC1B,QAAI,KAAK,UAAW;AAEpB,SAAK,WAAW;AAChB,SAAK,eAAe,OAAO,MAAM,CAAC;AAElC,UAAM,OAAO,gBAAgB,KAAK,MAAM;AACxC,SAAK,UAAU,MAAM,UAAU,IAAI;AAGnC,SAAK,QAAQ,OAAO,GAAG,SAAS,MAAM;AAAA,IAAC,CAAC;AAExC,SAAK,QAAQ,QAAQ,GAAG,QAAQ,CAAC,UAAkB;AACjD,WAAK,iBAAiB,KAAK;AAAA,IAC7B,CAAC;AAED,SAAK,QAAQ,QAAQ,GAAG,QAAQ,CAAC,SAAiB;AAEhD,WAAK;AAAA,IACP,CAAC;AAED,SAAK,QAAQ,GAAG,SAAS,CAAC,SAAgB;AAAA,IAE1C,CAAC;AAED,SAAK,QAAQ,GAAG,SAAS,MAAM;AAC7B,UAAI,CAAC,KAAK,WAAW;AACnB,aAAK,UAAU;AAAA,MACjB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,aAAmB;AACzB,QAAI,KAAK,SAAS;AAChB,UAAI;AACF,aAAK,QAAQ,KAAK,SAAS;AAAA,MAC7B,QAAQ;AAAA,MAER;AACA,WAAK,UAAU;AAAA,IACjB;AAAA,EACF;AAAA,EAEQ,iBAAiB,OAAqB;AAC5C,SAAK,eAAe,OAAO,OAAO,CAAC,KAAK,cAAc,KAAK,CAAC;AAG5D,QAAI,aAAa;AACjB,WAAO,MAAM;AACX,YAAM,WAAW,KAAK,aAAa,QAAQ,KAAK,UAAU;AAC1D,UAAI,aAAa,GAAI;AAErB,YAAM,WAAW,KAAK,aAAa,QAAQ,KAAK,WAAW,CAAC;AAC5D,UAAI,aAAa,GAAI;AAErB,YAAM,WAAW,WAAW;AAC5B,YAAM,WAAW,KAAK,aAAa,MAAM,UAAU,QAAQ;AAG3D,mBAAa;AAEb,WAAK,UAAU,OAAO,KAAK,QAAQ,CAAC;AAAA,IACtC;AAGA,QAAI,aAAa,GAAG;AAClB,WAAK,eAAe,OAAO,KAAK,KAAK,aAAa,MAAM,UAAU,CAAC;AAAA,IACrE;AAAA,EACF;AAAA,EAEQ,UAAU,MAAoB;AACpC,UAAM,cAAc,KAAK,IAAI;AAE7B,QAAI,CAAC,KAAK,aAAa,WAAW,GAAG;AACnC,WAAK;AACL;AAAA,IACF;AAEA,UAAM,aAAa,KAAK,IAAI,IAAI;AAChC,SAAK,qBAAqB;AAC1B,SAAK;AACL,SAAK;AAEL,UAAM,QAAsB;AAAA,MAC1B;AAAA,MACA,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,KAAK,IAAI;AAAA,IACtB;AAEA,eAAW,MAAM,KAAK,gBAAgB;AACpC,SAAG,KAAK;AAAA,IACV;AAAA,EACF;AAAA,EAEA,WAAW,QAA6B;AACtC,QAAI,KAAK,aAAa,CAAC,KAAK,SAAS,MAAO;AAC5C,SAAK;AACL,QAAI;AACF,WAAK,QAAQ,MAAM,MAAM,OAAO,IAAI;AAAA,IACtC,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,QAAQ,UAAsD;AAC5D,SAAK,eAAe,IAAI,QAAQ;AAChC,WAAO,MAAM;AACX,WAAK,eAAe,OAAO,QAAQ;AAAA,IACrC;AAAA,EACF;AAAA,EAEA,aAAa,QAA6C;AACxD,UAAM,eACH,OAAO,UAAU,UAAa,OAAO,UAAU,KAAK,OAAO,SAC3D,OAAO,iBAAiB,UAAa,OAAO,iBAAiB,KAAK,OAAO,gBACzE,OAAO,UAAU,UAAa,OAAO,UAAU,KAAK,OAAO;AAE9D,SAAK,SAAS,EAAE,GAAG,KAAK,QAAQ,GAAG,OAAO;AAE1C,QAAI,OAAO,WAAW,QAAW;AAC/B,WAAK,aAAa,UAAU,OAAO,MAAM;AAAA,IAC3C;AAEA,QAAI,cAAc;AAChB,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AAAA,EAEA,MAAM,UAAyB;AAC7B,QAAI,KAAK,UAAW;AACpB,SAAK,YAAY;AACjB,SAAK,WAAW;AAChB,SAAK,eAAe,MAAM;AAAA,EAC5B;AAAA,EAEA,WAAyB;AACvB,UAAM,YAAY,KAAK,KAAK,KAAK,IAAI,IAAI,KAAK,aAAa,KAAM,CAAC;AAClE,WAAO;AAAA,MACL,UAAU,KAAK,eAAe;AAAA,MAC9B,WAAW,KAAK,eAAe;AAAA,MAC/B,iBAAiB,KAAK,cAAc,IAAI,KAAK,oBAAoB,KAAK,cAAc;AAAA,MACpF,eAAe,KAAK;AAAA,IACtB;AAAA,EACF;AACF;;;AErMA,IAAM,mBAAmB,oBAAI,IAAI,CAAC,QAAQ,QAAQ,QAAQ,OAAO,CAAC;AAE3D,IAAM,wBAAN,MAAwD;AAAA,EACpD,KAAK;AAAA,EACL,OAAO;AAAA,EAEhB,cAAc,OAAwB;AACpC,WAAO,iBAAiB,IAAI,KAAK;AAAA,EACnC;AAAA,EAEA,cAAc,QAA+C;AAC3D,WAAO,IAAI,qBAAqB,MAAM;AAAA,EACxC;AACF;;;AChBA,SAAS,SAAAC,cAAgC;;;ACAzC,OAAO,SAAS;AAYT,IAAM,mBAAN,MAAuB;AAAA,EACX;AAAA,EACA,UAA2B,oBAAI,IAAI;AAAA,EAC5C,OAAO;AAAA,EACP,UAAU;AAAA,EACD;AAAA,EAEjB,YAAY,QAAwB;AAClC,SAAK,SAAS;AACd,SAAK,SAAS,IAAI,aAAa,CAAC,WAAW;AACzC,WAAK,iBAAiB,MAAM;AAAA,IAC9B,CAAC;AAED,SAAK,OAAO,GAAG,SAAS,CAAC,QAAQ;AAC/B,WAAK,QAAQ,MAAM,2BAA2B,IAAI,OAAO,EAAE;AAAA,IAC7D,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QAAuB;AAC3B,QAAI,KAAK,SAAS;AAChB;AAAA,IACF;AAEA,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,WAAK,OAAO,KAAK,SAAS,MAAM;AAChC,WAAK,OAAO,OAAO,GAAG,aAAa,MAAM;AACvC,aAAK,OAAO,eAAe,SAAS,MAAM;AAC1C,cAAM,UAAU,KAAK,OAAO,QAAQ;AACpC,aAAK,OAAO,QAAQ;AACpB,aAAK,UAAU;AACf,aAAK,QAAQ,MAAM,sCAAsC,KAAK,IAAI,EAAE;AACpE,gBAAQ;AAAA,MACV,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAU,MAAoB;AAC5B,eAAW,UAAU,KAAK,SAAS;AACjC,UAAI,OAAO,WAAW;AACpB,aAAK,QAAQ,OAAO,MAAM;AAC1B;AAAA,MACF;AAEA,aAAO,MAAM,MAAM,CAAC,QAAQ;AAC1B,YAAI,KAAK;AACP,eAAK,QAAQ;AAAA,YACX,kDAAkD,IAAI,OAAO;AAAA,UAC/D;AACA,eAAK,aAAa,MAAM;AAAA,QAC1B;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,UAAkB;AAChB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,SAAiB;AACf,WAAO,mBAAmB,KAAK,IAAI;AAAA,EACrC;AAAA,EAEA,iBAAyB;AACvB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA,EAEA,YAAqB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,OAAsB;AAC1B,QAAI,CAAC,KAAK,SAAS;AACjB;AAAA,IACF;AAEA,SAAK,UAAU;AAEf,eAAW,UAAU,KAAK,SAAS;AACjC,aAAO,QAAQ;AAAA,IACjB;AACA,SAAK,QAAQ,MAAM;AAEnB,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,WAAK,OAAO,MAAM,MAAM;AACtB,aAAK,QAAQ,MAAM,0BAA0B;AAC7C,gBAAQ;AAAA,MACV,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEQ,iBAAiB,QAA0B;AACjD,SAAK,QAAQ,IAAI,MAAM;AACvB,SAAK,QAAQ;AAAA,MACX,6CAA6C,KAAK,QAAQ,IAAI;AAAA,IAChE;AAEA,WAAO,GAAG,SAAS,MAAM;AACvB,WAAK,aAAa,MAAM;AAAA,IAC1B,CAAC;AAED,WAAO,GAAG,SAAS,MAAM;AACvB,WAAK,aAAa,MAAM;AAAA,IAC1B,CAAC;AAAA,EACH;AAAA,EAEQ,aAAa,QAA0B;AAC7C,UAAM,UAAU,KAAK,QAAQ,OAAO,MAAM;AAC1C,QAAI,SAAS;AACX,WAAK,QAAQ;AAAA,QACX,gDAAgD,KAAK,QAAQ,IAAI;AAAA,MACnE;AAAA,IACF;AACA,QAAI,CAAC,OAAO,WAAW;AACrB,aAAO,QAAQ;AAAA,IACjB;AAAA,EACF;AACF;;;ADjHA,IAAM,kBAAkB;AACxB,IAAM,iBAAiB;AACvB,IAAM,gBAAgB;AAEtB,IAAM,6BAA6B;AACnC,IAAM,yBAAyB;AAC/B,IAAM,4BAA4B;AAGlC,IAAM,oBAAoB;AAE1B,IAAM,yBAAyB;AAC/B,IAAM,yBAAyB;AAOxB,IAAM,eAAN,MAA4C;AAAA,EACxC;AAAA,EAED,UAAwB;AAAA,EACxB;AAAA,EACA,iBAAyC;AAAA,EACzC,qBAAyC;AAAA,EAChC;AAAA,EACA;AAAA,EACA,YAAoB,KAAK,IAAI;AAAA,EAEtC;AAAA,EACA;AAAA,EACA,mBAAmB;AAAA,EACnB,aAAa;AAAA,EACb,WAAW;AAAA,EACX;AAAA,EAEA,cAAsC,CAAC;AAAA,EAC9B;AAAA,EAEA,mBAAmB,oBAAI,IAAqC;AAAA,EAC5D,qBAAqB,oBAAI,IAA+B;AAAA,EACxD,sBAAsB,oBAAI,IAAwC;AAAA,EAEnF,YAAY,UAAkB,iBAAkC,QAAwB;AACtF,SAAK,WAAW;AAChB,SAAK,kBAAkB;AACvB,SAAK,SAAS;AACd,SAAK,aAAa,IAAI,iBAAiB,QAAQ,MAAM,aAAa,CAAC;AAAA,EACrE;AAAA,EAEA,IAAI,SAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,eAAe,aAA2C;AACxD,SAAK,cAAc;AAAA,EACrB;AAAA,EAEA,MAAM,MAAM,QAAqC;AAC/C,SAAK,SAAS;AACd,SAAK,aAAa;AAClB,SAAK,WAAW;AAChB,SAAK,UAAU;AAEf,UAAM,KAAK,WAAW,MAAM;AAC5B,SAAK,QAAQ;AAAA,MACX,2BAA2B,KAAK,QAAQ,OAAO,KAAK,WAAW,OAAO,CAAC;AAAA,IACzE;AAEA,QAAI,OAAO,SAAS,QAAQ;AAC1B,WAAK,gBAAgB,MAAM;AAAA,IAC7B,OAAO;AAEL,WAAK,UAAU;AAAA,IACjB;AAAA,EACF;AAAA,EAEA,MAAM,OAAsB;AAC1B,SAAK,WAAW;AAChB,SAAK,aAAa;AAClB,SAAK,UAAU;AAEf,SAAK,WAAW;AAEhB,QAAI,KAAK,gBAAgB;AACvB,mBAAa,KAAK,cAAc;AAChC,WAAK,iBAAiB;AAAA,IACxB;AAEA,QAAI,KAAK,sBAAsB;AAC7B,mBAAa,KAAK,oBAAoB;AACtC,WAAK,uBAAuB;AAAA,IAC9B;AAEA,QAAI,KAAK,gBAAgB;AACvB,YAAM,KAAK,eAAe;AAAA,IAC5B;AAEA,UAAM,KAAK,WAAW,KAAK;AAE3B,SAAK,iBAAiB,MAAM;AAC5B,SAAK,mBAAmB,MAAM;AAC9B,SAAK,oBAAoB,MAAM;AAAA,EACjC;AAAA,EAEA,kBAAkB,QAA6B;AAC7C,QAAI,KAAK,SAAU;AAEnB,eAAW,MAAM,KAAK,kBAAkB;AACtC,SAAG,MAAM;AAAA,IACX;AAGA,QAAI,OAAO,SAAS,SAAS;AAC3B,WAAK,WAAW,UAAU,OAAO,IAAI;AAAA,IACvC;AAEA,eAAW,cAAc,KAAK,aAAa;AACzC,YAAM,WAAW,GAAG,KAAK,QAAQ;AACjC,iBAAW,WAAW,UAAU,MAAM;AAAA,IACxC;AAEA,QAAI,OAAO,SAAS,WAAW,KAAK,gBAAgB;AAClD,WAAK,eAAe,WAAW,MAAM;AAAA,IACvC;AAAA,EACF;AAAA,EAEA,cAAc,UAAwD;AACpE,SAAK,iBAAiB,IAAI,QAAQ;AAElC,WAAO,MAAM;AACX,WAAK,iBAAiB,OAAO,QAAQ;AAAA,IACvC;AAAA,EACF;AAAA,EAEA,eAAe,UAAyC,SAAsC;AAC5F,UAAM,SAAS,SAAS,UAAU;AAClC,UAAM,eAAe,uBAAO,oBAAoB;AAChD,UAAM,eAAe,IAAI,aAAa,MAAM;AAE5C,UAAM,aAAgC,EAAE,UAAU,aAAa;AAC/D,SAAK,mBAAmB,IAAI,cAAc,UAAU;AAGpD,QAAI,KAAK,sBAAsB;AAC7B,mBAAa,KAAK,oBAAoB;AACtC,WAAK,uBAAuB;AAAA,IAC9B;AAGA,QAAI,CAAC,KAAK,kBAAkB,KAAK,QAAQ;AACvC,WAAK,2BAA2B,OAAO;AAAA,IACzC;AAEA,WAAO,MAAM;AACX,WAAK,mBAAmB,OAAO,YAAY;AAG3C,UAAI,KAAK,mBAAmB,SAAS,KAAK,KAAK,gBAAgB;AAC7D,aAAK,uBAAuB,WAAW,MAAM;AAC3C,eAAK,uBAAuB;AAC5B,cAAI,KAAK,mBAAmB,SAAS,KAAK,KAAK,gBAAgB;AAC7D,iBAAK,eAAe;AAAA,UACtB;AAAA,QACF,GAAG,yBAAyB;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,oBAAoB,UAA2D;AAC7E,SAAK,oBAAoB,IAAI,QAAQ;AAErC,WAAO,MAAM;AACX,WAAK,oBAAoB,OAAO,QAAQ;AAAA,IAC1C;AAAA,EACF;AAAA,EAEA,WAAwB;AACtB,UAAM,eAAe,KAAK,gBAAgB,SAAS;AAEnD,WAAO;AAAA,MACL,QAAQ,KAAK;AAAA,MACb,UAAU,cAAc,YAAY;AAAA,MACpC,WAAW,cAAc,aAAa;AAAA,MACtC,oBAAoB,KAAK,iBAAiB;AAAA,MAC1C,oBAAoB,KAAK,mBAAmB;AAAA,MAC5C,UAAU,KAAK,IAAI,IAAI,KAAK;AAAA,IAC9B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,oBAA4B;AAC1B,WAAO,KAAK,WAAW,OAAO;AAAA,EAChC;AAAA;AAAA,EAGA,qBAA6B;AAC3B,WAAO,KAAK,WAAW,eAAe;AAAA,EACxC;AAAA,EAEQ,gBAAgB,QAA4B;AAClD,UAAM,QAAQ,OAAO,cAAc;AACnC,UAAM,SAAS,UAAU,UAAU,UAAU;AAE7C,UAAM,OAAO;AAAA,MACX;AAAA,MACA;AAAA,MAAa;AAAA,MACb;AAAA,MAAmB;AAAA,MACnB;AAAA,MAAM,OAAO;AAAA,MACb;AAAA,MAAQ;AAAA,MACR;AAAA,MAAM,SAAS,SAAS;AAAA,MACxB;AAAA,MAAU,SAAS,qBAAqB;AAAA,MACxC;AAAA,IACF;AAEA,SAAK,QAAQ,MAAM,mCAAmC,KAAK,QAAQ,IAAI;AAAA,MACrE,KAAK,OAAO;AAAA,MACZ;AAAA,IACF,CAAC;AAED,UAAM,OAAOC,OAAM,UAAU,MAAM;AAAA,MACjC,OAAO,CAAC,QAAQ,QAAQ,MAAM;AAAA,IAChC,CAAC;AACD,SAAK,gBAAgB;AAGrB,SAAK,OAAO,GAAG,SAAS,MAAM;AAAA,IAAC,CAAC;AAEhC,SAAK,QAAQ,GAAG,QAAQ,CAAC,UAAkB;AACzC,UAAI,KAAK,SAAU;AAEnB,WAAK,UAAU;AACf,WAAK,mBAAmB;AAExB,YAAM,WAAW,SACb,eAAe,KAAK,IACpB,eAAe,KAAK;AAExB,YAAM,SAAwB;AAAA,QAC5B,MAAM;AAAA,QACN,MAAM;AAAA,QACN,KAAK,KAAK,IAAI;AAAA,QACd,KAAK,KAAK,IAAI;AAAA,QACd;AAAA,QACA;AAAA,MACF;AAEA,WAAK,kBAAkB,MAAM;AAAA,IAC/B,CAAC;AAED,SAAK,QAAQ,GAAG,QAAQ,CAAC,SAAiB;AACxC,YAAM,MAAM,KAAK,SAAS,EAAE,KAAK;AACjC,UAAI,KAAK;AACP,aAAK,QAAQ,KAAK,8BAA8B,KAAK,QAAQ,MAAM,GAAG,EAAE;AAAA,MAC1E;AAAA,IACF,CAAC;AAED,SAAK,GAAG,QAAQ,CAAC,MAAM,WAAW;AAChC,WAAK,gBAAgB;AAErB,UAAI,KAAK,YAAY;AACnB;AAAA,MACF;AAEA,WAAK,QAAQ,KAAK,iCAAiC,KAAK,QAAQ,IAAI;AAAA,QAClE;AAAA,QACA;AAAA,MACF,CAAC;AAED,WAAK,UAAU;AACf,WAAK,kBAAkB;AAAA,IACzB,CAAC;AAED,SAAK,GAAG,SAAS,CAAC,QAAQ;AACxB,WAAK,gBAAgB;AAErB,UAAI,KAAK,YAAY;AACnB;AAAA,MACF;AAEA,WAAK,QAAQ,MAAM,gCAAgC,KAAK,QAAQ,KAAK,IAAI,OAAO,EAAE;AAClF,WAAK,UAAU;AACf,WAAK,kBAAkB;AAAA,IACzB,CAAC;AAAA,EACH;AAAA,EAEQ,oBAA0B;AAChC,QAAI,KAAK,cAAc,CAAC,KAAK,QAAQ;AACnC;AAAA,IACF;AAEA,SAAK,QAAQ;AAAA,MACX,gCAAgC,KAAK,QAAQ,OAAO,KAAK,gBAAgB;AAAA,IAC3E;AAEA,SAAK,iBAAiB,WAAW,MAAM;AACrC,WAAK,iBAAiB;AACtB,UAAI,CAAC,KAAK,cAAc,KAAK,QAAQ;AACnC,aAAK,UAAU;AACf,aAAK,gBAAgB,KAAK,MAAM;AAAA,MAClC;AAAA,IACF,GAAG,KAAK,gBAAgB;AAExB,SAAK,mBAAmB,KAAK;AAAA,MAC3B,KAAK,mBAAmB;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,iBAAgC;AACtC,QAAI,KAAK,oBAAoB;AAC3B,WAAK,mBAAmB;AACxB,WAAK,qBAAqB;AAAA,IAC5B;AACA,UAAM,UAAU,KAAK;AACrB,SAAK,iBAAiB;AACtB,WAAO,SAAS,QAAQ,KAAK,QAAQ,QAAQ;AAAA,EAC/C;AAAA,EAEQ,aAAmB;AACzB,QAAI,KAAK,eAAe;AACtB,WAAK,cAAc,KAAK,SAAS;AACjC,WAAK,gBAAgB;AAAA,IACvB;AAAA,EACF;AAAA,EAEQ,2BAA2B,SAA+B;AAChE,UAAM,QAAQ,KAAK,QAAQ,cAAc;AACzC,UAAM,WAAW,KAAK,gBAAgB,WAAW,KAAK;AAEtD,QAAI,CAAC,UAAU;AACb;AAAA,IACF;AAGA,UAAM,SAAS;AAAA,MACb;AAAA,MACA,QAAQ;AAAA,MACR,cAAc,SAAS,UAAU;AAAA,MACjC,OAAO,SAAS,SAAS;AAAA,IAC3B;AAEA,SAAK,iBAAiB,SAAS,cAAc,MAAM;AAGnD,SAAK,qBAAqB,KAAK,eAAe,QAAQ,CAAC,UAAwB;AAC7E,iBAAW,cAAc,KAAK,mBAAmB,OAAO,GAAG;AACzD,YAAI,WAAW,aAAa,WAAW,GAAG;AACxC,qBAAW,SAAS,KAAK;AAAA,QAC3B;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAMA,SAAS,eAAe,MAAuB;AAC7C,WAAS,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,KAAK;AAExC,QAAI,KAAK,CAAC,MAAM,KAAK,KAAK,IAAI,CAAC,MAAM,GAAG;AACtC,UAAI,WAAW;AACf,UAAI,KAAK,IAAI,CAAC,MAAM,GAAG;AACrB,mBAAW,IAAI;AAAA,MACjB,WAAW,KAAK,IAAI,CAAC,MAAM,KAAK,KAAK,IAAI,CAAC,MAAM,GAAG;AACjD,mBAAW,IAAI;AAAA,MACjB;AACA,UAAI,YAAY,KAAK,WAAW,KAAK,QAAQ;AAC3C,cAAM,UAAU,KAAK,QAAQ,IAAK;AAClC,YAAI,YAAY,mBAAmB;AACjC,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAMA,SAAS,eAAe,MAAuB;AAC7C,WAAS,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,KAAK;AACxC,QAAI,KAAK,CAAC,MAAM,KAAK,KAAK,IAAI,CAAC,MAAM,GAAG;AACtC,UAAI,WAAW;AACf,UAAI,KAAK,IAAI,CAAC,MAAM,GAAG;AACrB,mBAAW,IAAI;AAAA,MACjB,WAAW,KAAK,IAAI,CAAC,MAAM,KAAK,KAAK,IAAI,CAAC,MAAM,GAAG;AACjD,mBAAW,IAAI;AAAA,MACjB;AACA,UAAI,YAAY,KAAK,WAAW,KAAK,QAAQ;AAC3C,cAAM,UAAW,KAAK,QAAQ,KAAM,IAAK;AACzC,YAAI,WAAW,0BAA0B,WAAW,wBAAwB;AAC1E,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;;;AEtaO,IAAM,sBAAN,MAA0B;AAAA,EAK/B,YACmB,iBACjB,QACA;AAFiB;AAGjB,SAAK,SAAS;AAAA,EAChB;AAAA,EATiB,UAAU,oBAAI,IAA0B;AAAA,EACjD,cAAsC,CAAC;AAAA,EAC9B;AAAA,EASjB,eAAe,aAA2C;AACxD,SAAK,cAAc;AAEnB,eAAW,UAAU,KAAK,QAAQ,OAAO,GAAG;AAC1C,aAAO,eAAe,WAAW;AAAA,IACnC;AAEA,SAAK,OAAO,KAAK,+BAA+B,YAAY,MAAM,GAAG;AAAA,EACvE;AAAA,EAEA,iBAAyC;AACvC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,aAAa,UAAkB,QAA6C;AAChF,UAAM,eAAe,KAAK,OAAO,MAAM,UAAU,QAAQ,EAAE;AAC3D,UAAM,SAAS,IAAI,aAAa,UAAU,KAAK,iBAAiB,YAAY;AAC5E,WAAO,eAAe,KAAK,WAAW;AACtC,UAAM,OAAO,MAAM,MAAM;AACzB,SAAK,QAAQ,IAAI,UAAU,MAAM;AACjC,WAAO;AAAA,EACT;AAAA,EAEA,UAAU,UAA4C;AACpD,WAAO,KAAK,QAAQ,IAAI,QAAQ;AAAA,EAClC;AAAA,EAEA,MAAM,cAAc,UAAiC;AACnD,UAAM,SAAS,KAAK,QAAQ,IAAI,QAAQ;AAExC,QAAI,QAAQ;AACV,YAAM,OAAO,KAAK;AAClB,WAAK,QAAQ,OAAO,QAAQ;AAAA,IAC9B;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,wBAAwB,UAAiC;AAC7D,UAAM,SAAS,GAAG,QAAQ;AAC1B,UAAM,YAAsB,CAAC;AAE7B,eAAW,YAAY,KAAK,QAAQ,KAAK,GAAG;AAC1C,UAAI,aAAa,YAAY,SAAS,WAAW,MAAM,GAAG;AACxD,kBAAU,KAAK,QAAQ;AAAA,MACzB;AAAA,IACF;AAEA,UAAM,QAAQ,IAAI,UAAU,IAAI,CAAC,OAAO,KAAK,cAAc,EAAE,CAAC,CAAC;AAAA,EACjE;AAAA,EAEA,cAAuC;AACrC,WAAO,CAAC,GAAG,KAAK,QAAQ,OAAO,CAAC;AAAA,EAClC;AAAA,EAEA,MAAM,aAA4B;AAChC,UAAM,eAAe,CAAC,GAAG,KAAK,QAAQ,OAAO,CAAC,EAAE,IAAI,CAAC,WAAW,OAAO,KAAK,CAAC;AAC7E,UAAM,QAAQ,IAAI,YAAY;AAC9B,SAAK,QAAQ,MAAM;AAAA,EACrB;AACF;;;ACCA,SAAS,kBAAkBC,OAA8B;AACvD,MAAIA,MAAK,SAAS,YAAY,KAAKA,MAAK,SAAS,SAAS,EAAG,QAAO;AACpE,MAAIA,MAAK,SAAS,WAAW,KAAKA,MAAK,SAAS,SAAS,EAAG,QAAO;AACnE,MAAIA,MAAK,SAAS,2BAA2B,EAAG,QAAO;AACvD,MAAIA,MAAK,SAAS,0BAA0B,EAAG,QAAO;AACtD,MAAIA,MAAK,SAAS,oBAAoB,EAAG,QAAO;AAChD,MAAIA,MAAK,SAAS,cAAc,EAAG,QAAO;AAC1C,MAAIA,MAAK,SAAS,UAAU,EAAG,QAAO;AACtC,SAAO;AACT;AAYO,IAAM,0BAAN,MAA8B;AAAA,EAgBnC,YAA6B,MAA4B;AAA5B;AAC3B,SAAK,SAAS,KAAK;AACnB,SAAK,qBAAqB,KAAK;AAAA,EACjC;AAAA,EAlBQ,cAAkC,CAAC;AAAA,EACnC,cAA4B,CAAC;AAAA,EAE7B;AAAA,EACA;AAAA,EAES,0BAA0B;AAAA,EAC1B,0BAA0B;AAAA,EAC1B,mBAAmB;AAAA,EACnB,sBAAsB;AAAA,EAEtB;AAAA,EACA;AAAA,EACT;AAAA,EAOR,QAAc;AACZ,SAAK,cAAc,KAAK,KAAK;AAAA,MAC3B,EAAE,UAAU,kBAAkB;AAAA,MAC9B,CAAC,UAAU,KAAK,YAAY,KAAK;AAAA,IACnC;AAEA,SAAK,kBAAkB,YAAY,MAAM;AACvC,WAAK,YAAY,EAAE,MAAM,OAAK,KAAK,OAAO,KAAK,uBAAuB,CAAC,EAAE,CAAC;AAAA,IAC5E,GAAG,KAAK,uBAAuB;AAE/B,SAAK,kBAAkB,YAAY,MAAM;AACvC,WAAK,WAAW,EAAE,MAAM,OAAK,KAAK,OAAO,KAAK,uBAAuB,CAAC,EAAE,CAAC;AAAA,IAC3E,GAAG,KAAK,uBAAuB;AAAA,EACjC;AAAA,EAEA,OAAa;AACX,SAAK,cAAc;AACnB,kBAAc,KAAK,eAAe;AAClC,kBAAc,KAAK,eAAe;AAClC,SAAK,YAAY,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AACjC,SAAK,WAAW,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EAClC;AAAA;AAAA;AAAA,EAKA,mBAAmB,SAAiB,OAA8C;AAChF,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,WAAW;AAClB,aAAK,YAAY,SAAS,UAAU,OAAO,UAAU,KAAK,OAAO,cAAc,KAAK,WAAW,MAAM;AAAA,MACvG;AACA,UAAI,KAAK,UAAU;AACjB,aAAK,YAAY,SAAS,UAAU,OAAO,UAAU,KAAK,OAAO,aAAa,KAAK,UAAU,QAAQ;AAAA,MACvG;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,mBAAmB,SAAiB,QAAuC;AACzE,SAAK,YAAY,SAAS,UAAU,OAAO,8BAA8B,OAAO,WAAW,MAAM;AACjG,SAAK,YAAY,SAAS,UAAU,OAAO,6BAA6B,OAAO,MAAM,QAAQ;AAAA,EAC/F;AAAA;AAAA,EAGA,MAAM,kBAAkB,SAAiB,OAAkC;AACzE,UAAMC,UAAS,MAAM,OAAO,OAAO,GAAG;AAEtC,UAAM,OAAO,MAAMA,OAAM,MAAM,MAAM;AAAA,MACnC,KAAK,EAAE,OAAO,MAAM,OAAO,QAAQ,MAAM,QAAQ,UAAU,EAAE;AAAA,IAC/D,CAAC,EAAE,KAAK,EAAE,SAAS,GAAG,CAAC,EAAE,SAAS;AAElC,SAAK,YAAY,SAAS,UAAU,OAAO,iBAAiB,MAAM,QAAQ;AAE1E,UAAM,QAAQ,MAAMA,OAAM,IAAI,EAC3B,OAAO,KAAK,KAAK,EAAE,KAAK,SAAS,CAAC,EAClC,KAAK,EAAE,SAAS,GAAG,CAAC,EACpB,SAAS;AAEZ,SAAK,YAAY,SAAS,UAAU,OAAO,uBAAuB,OAAO,MAAM;AAAA,EACjF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,cAAc,SAAqD;AACvE,UAAM,UAA4B,CAAC;AAGnC,eAAW,SAAS,KAAK,aAAa;AACpC,UAAI,MAAM,YAAY,SAAS;AAC7B,gBAAQ,KAAK;AAAA,UACX,MAAM,MAAM;AAAA,UACZ,MAAM,kBAAkB,MAAM,IAAI;AAAA,UAClC,MAAM,MAAM;AAAA,UACZ,QAAQ;AAAA,QACV,CAAC;AAAA,MACH;AAAA,IACF;AAGA,QAAI;AACF,YAAM,WAAW,KAAK,mBAAmB,QAAQ;AACjD,UAAI,SAAS,OAAO;AAClB,cAAM,QAAQ,MAAM,SAAS,MAAM,UAAU,UAAU,OAAO,GAAG;AACjE,mBAAW,QAAQ,OAAO;AAExB,cAAI,QAAQ,KAAK,OAAK,EAAE,SAAS,IAAI,EAAG;AACxC,cAAI;AACF,kBAAM,OAAO,MAAM,SAAS,MAAM,SAAS,IAAI;AAC/C,oBAAQ,KAAK;AAAA,cACX,MAAM;AAAA,cACN,MAAM,kBAAkB,IAAI;AAAA,cAC5B;AAAA,cACA,QAAQ;AAAA,YACV,CAAC;AAAA,UACH,QAAQ;AAAA,UAER;AAAA,QACF;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAc,SAAqD;AACvE,UAAM,UAA4B,CAAC;AAGnC,eAAW,SAAS,KAAK,aAAa;AACpC,UAAI,MAAM,KAAK,SAAS,OAAO,GAAG;AAChC,gBAAQ,KAAK;AAAA,UACX,MAAM,MAAM;AAAA,UACZ,MAAM,kBAAkB,MAAM,IAAI;AAAA,UAClC,MAAM,MAAM;AAAA,UACZ,QAAQ;AAAA,QACV,CAAC;AAAA,MACH;AAAA,IACF;AAGA,UAAM,mBAAmB,KAAK,YAC3B,OAAO,OAAK,EAAE,YAAY,OAAO,EACjC,IAAI,OAAK,EAAE,EAAE;AAGhB,QAAI;AACF,YAAM,WAAW,KAAK,mBAAmB,QAAQ;AACjD,UAAI,SAAS,YAAY;AACvB,cAAM,SAAS,MAAM,SAAS,WAAW,MAAM,oBAAoB;AAAA,UACjE,OAAO,EAAE,QAAQ;AAAA,UACjB,SAAS,EAAE,OAAO,aAAa,WAAW,MAAM;AAAA,UAChD,OAAO;AAAA,QACT,CAAC;AAED,cAAM,cAAc,oBAAI,IAAI;AAAA,UAC1B,GAAG;AAAA,UACH,GAAG,OAAO,IAAI,OAAK,EAAE,EAAE;AAAA,QACzB,CAAC;AAGD,mBAAW,WAAW,aAAa;AACjC,gBAAM,aAAa,MAAM,KAAK,cAAc,OAAO;AACnD,qBAAW,SAAS,YAAY;AAC9B,gBAAI,CAAC,QAAQ,KAAK,OAAK,EAAE,SAAS,MAAM,IAAI,GAAG;AAC7C,sBAAQ,KAAK,KAAK;AAAA,YACpB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,eACJ,UACA,OACA,OACoC;AACpC,UAAM,UAA4B,CAAC;AAEnC,QAAI;AACF,YAAM,WAAW,KAAK,mBAAmB,QAAQ;AACjD,UAAI,CAAC,SAAS,WAAY,QAAO;AAEjC,YAAM,SAAS,MAAM,SAAS,WAAW,MAAM,oBAAoB;AAAA,QACjE,OAAO,EAAE,SAAS;AAAA,QAClB,cAAc,SAAS,QACnB,EAAE,WAAW,CAAC,SAAS,GAAG,SAAS,KAAK,IAAI,CAAC,EAAE,IAC/C;AAAA,QACJ,SAAS,EAAE,OAAO,aAAa,WAAW,OAAO;AAAA,QACjD,OAAO;AAAA,MACT,CAAC;AAED,iBAAW,SAAS,QAAQ;AAC1B,cAAM,QAAQ,MAAM,KAAK,cAAc,MAAM,EAAE;AAC/C,gBAAQ,KAAK,GAAG,KAAK;AAAA,MACvB;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,WAAO;AAAA,EACT;AAAA;AAAA,EAIA,kBAAqC;AACnC,WAAO;AAAA,MACL,YAAY,KAAK,YAAY;AAAA,MAC7B,YAAY,KAAK,YAAY;AAAA,MAC7B,aAAa,KAAK,YAAY,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,KAAK,QAAQ,CAAC,KAAK,OAAO;AAAA,IACrF;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,QAAuB;AAC3B,UAAM,KAAK,YAAY;AACvB,UAAM,KAAK,WAAW;AAAA,EACxB;AAAA;AAAA,EAIQ,YAAY,OAA0B;AAE5C,UAAM,aAAa,KAAK,YACrB,OAAO,OAAK,EAAE,YAAY,MAAM,EAAE,EAClC,IAAI,OAAK,EAAE,IAAI;AAElB,UAAM,cAAgC;AAAA,MACpC,IAAI,MAAM;AAAA,MACV,WAAW,KAAK,IAAI;AAAA,MACpB,UAAW,MAAM,KAAK,YAAuB;AAAA,MAC7C,UAAU,MAAM;AAAA,MAChB,WAAY,MAAM,KAAK,aAAwB;AAAA,MAC/C,OAAQ,MAAM,KAAK,SAAoB;AAAA,MACvC,SAAU,MAAM,KAAK,WAAsB;AAAA,MAC3C,UAAW,MAAM,KAAK,YAAuB;AAAA,MAC7C,aAAc,MAAM,KAAK,eAA0B;AAAA,MACnD,MAAM,MAAM;AAAA,MACZ,YAAY;AAAA,IACd;AAEA,SAAK,YAAY,KAAK,WAAW;AAGjC,UAAM,YAAY,MAAM,KAAK;AAC7B,QAAI,WAAW,QAAQ,OAAO,SAAS,UAAU,IAAI,GAAG;AACtD,WAAK,YAAY,MAAM,IAAI,UAAU,MAAM,EAAE,aAAa,UAAU,MAAgB,MAAM;AAAA,IAC5F;AAEA,QAAI,KAAK,YAAY,UAAU,KAAK,kBAAkB;AACpD,WAAK,YAAY,EAAE,MAAM,OAAK,KAAK,OAAO,KAAK,sBAAsB,CAAC,EAAE,CAAC;AAAA,IAC3E;AAAA,EACF;AAAA,EAEQ,YAAY,SAAiBD,OAAc,MAAc,UAAmC;AAClG,SAAK,YAAY,KAAK,EAAE,SAAS,MAAAA,OAAM,MAAM,SAAS,CAAC;AAEvD,UAAM,UAAU,KAAK,YAAY,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,KAAK,QAAQ,CAAC,KAAK,OAAO;AACtF,QAAI,WAAW,KAAK,qBAAqB;AACvC,WAAK,WAAW,EAAE,MAAM,OAAK,KAAK,OAAO,KAAK,gCAAgC,CAAC,EAAE,CAAC;AAAA,IACpF;AAAA,EACF;AAAA;AAAA,EAIA,MAAc,cAA6B;AACzC,QAAI,KAAK,YAAY,WAAW,EAAG;AAEnC,UAAM,SAAS,CAAC,GAAG,KAAK,WAAW;AACnC,SAAK,cAAc,CAAC;AAEpB,QAAI;AACF,YAAM,WAAW,KAAK,mBAAmB,QAAQ;AACjD,UAAI,SAAS,YAAY;AACvB,mBAAW,SAAS,QAAQ;AAC1B,gBAAM,SAAS,WAAW,OAAO;AAAA,YAC/B,YAAY;AAAA,YACZ,IAAI,MAAM;AAAA,YACV,MAAM;AAAA,UACR,CAAC;AAAA,QACH;AACA,aAAK,OAAO,MAAM,WAAW,OAAO,MAAM,SAAS;AAAA,MACrD;AAAA,IACF,SAAS,KAAK;AACZ,WAAK,OAAO,KAAK,2BAA2B,GAAG,EAAE;AACjD,UAAI,KAAK,YAAY,SAAS,KAAK,mBAAmB,GAAG;AACvD,aAAK,YAAY,KAAK,GAAG,MAAM;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,aAA4B;AACxC,QAAI,KAAK,YAAY,WAAW,EAAG;AAGnC,UAAM,SAAS,CAAC,GAAG,KAAK,WAAW,EAAE,KAAK,CAAC,GAAG,MAAM;AAClD,UAAI,EAAE,aAAa,UAAU,EAAE,aAAa,OAAQ,QAAO;AAC3D,UAAI,EAAE,aAAa,UAAU,EAAE,aAAa,OAAQ,QAAO;AAC3D,aAAO;AAAA,IACT,CAAC;AACD,SAAK,cAAc,CAAC;AAEpB,QAAI;AACF,YAAM,WAAW,KAAK,mBAAmB,QAAQ;AACjD,UAAI,SAAS,OAAO;AAClB,mBAAW,SAAS,QAAQ;AAC1B,gBAAM,SAAS,MAAM,UAAU,MAAM,MAAM,MAAM,IAAI;AAAA,QACvD;AACA,cAAM,UAAU,OAAO,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,KAAK,QAAQ,CAAC,KAAK,OAAO;AACxE,aAAK,OAAO,MAAM,WAAW,OAAO,MAAM,iBAAiB,QAAQ,QAAQ,CAAC,CAAC,KAAK;AAAA,MACpF;AAAA,IACF,SAAS,KAAK;AACZ,WAAK,OAAO,KAAK,0BAA0B,GAAG,EAAE;AAAA,IAClD;AAAA,EACF;AACF;;;AClbA,SAAS,kBAAkB;AAYpB,SAAS,iBAAiB,GAAiB,GAAyB;AACzE,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,MAAI,aAAa;AACjB,MAAI,QAAQ;AACZ,MAAI,QAAQ;AACZ,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,kBAAc,EAAE,CAAC,IAAK,EAAE,CAAC;AACzB,aAAS,EAAE,CAAC,IAAK,EAAE,CAAC;AACpB,aAAS,EAAE,CAAC,IAAK,EAAE,CAAC;AAAA,EACtB;AACA,QAAM,QAAQ,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK;AAChD,SAAO,UAAU,IAAI,IAAI,aAAa;AACxC;AAmBA,IAAM,aAAa;AAEZ,IAAM,oBAAN,MAAwB;AAAA,EACZ;AAAA,EACA;AAAA,EAEjB,YAAY,MAAsB;AAChC,SAAK,SAAS,KAAK;AACnB,SAAK,uBAAuB,KAAK;AAAA,EACnC;AAAA,EAEQ,aAAiC;AACvC,WAAO,KAAK,qBAAqB;AAAA,EACnC;AAAA;AAAA,EAGA,MAAM,SAAS,OAAsC;AACnD,UAAM,UAAU,KAAK,WAAW;AAChC,UAAM,QAAQ,OAAO;AAAA,MACnB,YAAY;AAAA,MACZ,IAAI,MAAM;AAAA,MACV,MAAM;AAAA,QACJ,OAAO,MAAM;AAAA,QACb,OAAO,MAAM,SAAS;AAAA,QACtB,WAAW,KAAK,UAAU,MAAM,KAAK,MAAM,SAAS,CAAC;AAAA,QACrD,YAAY,MAAM;AAAA,QAClB,WAAW,MAAM;AAAA,QACjB,WAAW,MAAM;AAAA,QACjB,QAAQ,MAAM,UAAU;AAAA,QACxB,UAAU,MAAM,WAAW,KAAK,UAAU,MAAM,QAAQ,IAAI;AAAA,MAC9D;AAAA,IACF,CAAC;AACD,SAAK,OAAO,KAAK,0BAA0B,MAAM,KAAK,KAAK,MAAM,EAAE,GAAG;AAAA,EACxE;AAAA;AAAA,EAGA,MAAM,UAA8C;AAClD,UAAM,UAAU,KAAK,WAAW;AAChC,UAAM,UAAU,MAAM,QAAQ,MAAM,UAAU;AAC9C,WAAO,QAAQ,IAAI,CAAC,MAAM,cAAc,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,EACvD;AAAA;AAAA,EAGA,MAAM,OAAO,IAA2B;AACtC,UAAM,UAAU,KAAK,WAAW;AAChC,UAAM,QAAQ,OAAO,YAAY,EAAE;AACnC,SAAK,OAAO,KAAK,uBAAuB,EAAE,EAAE;AAAA,EAC9C;AAAA;AAAA,EAGA,MAAM,OAAO,IAAY,SAA0E;AACjG,UAAM,UAAU,KAAK,WAAW;AAChC,UAAM,OAAgC,EAAE,WAAW,KAAK,IAAI,EAAE;AAC9D,QAAI,QAAQ,UAAU,OAAW,MAAK,QAAQ,QAAQ;AACtD,QAAI,QAAQ,UAAU,OAAW,MAAK,QAAQ,QAAQ;AACtD,UAAM,QAAQ,OAAO,YAAY,IAAI,IAAI;AACzC,SAAK,OAAO,KAAK,sBAAsB,EAAE,KAAK,KAAK,UAAU,OAAO,CAAC,EAAE;AAAA,EACzE;AAAA;AAAA,EAGA,MAAM,qBAAqB,IAAY,gBAA+C;AACpF,UAAM,UAAU,KAAK,WAAW;AAChC,UAAM,UAAU,MAAM,QAAQ,MAAM,YAAY,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AACjE,QAAI,QAAQ,WAAW,EAAG,OAAM,IAAI,MAAM,yBAAyB,EAAE,EAAE;AAEvE,UAAM,SAAS,QAAQ,CAAC;AACxB,UAAM,aAAa,OAAO,KAAK;AAC/B,UAAM,aAAa,OAAO,KAAK,YAAY,QAAQ;AAEnD,UAAM,YAAY,MAAM,eAAe,aAAa,UAAU;AAC9D,UAAM,QAAQ,OAAO,YAAY,IAAI;AAAA,MACnC,WAAW,KAAK,UAAU,MAAM,KAAK,SAAS,CAAC;AAAA,MAC/C,WAAW,KAAK,IAAI;AAAA,IACtB,CAAC;AACD,SAAK,OAAO,KAAK,0CAA0C,EAAE,EAAE;AAAA,EACjE;AAAA;AAAA,EAGA,MAAM,UACJ,WACA,WACiF;AACjF,UAAM,UAAU,MAAM,KAAK,QAAQ;AACnC,QAAI,YAAmC;AACvC,QAAI,iBAAiB;AAErB,eAAW,SAAS,SAAS;AAC3B,YAAM,iBAAiB,IAAI,aAAa,MAAM,SAAS;AACvD,YAAM,aAAa,iBAAiB,WAAW,cAAc;AAC7D,UAAI,aAAa,gBAAgB;AAC/B,yBAAiB;AACjB,oBAAY;AAAA,MACd;AAAA,IACF;AAEA,QAAI,aAAa,kBAAkB,WAAW;AAC5C,aAAO,EAAE,OAAO,WAAW,YAAY,eAAe;AAAA,IACxD;AAEA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,cACJ,SACA,gBACoC;AACpC,UAAM,UAA4B,CAAC;AACnC,eAAW,SAAS,SAAS;AAC3B,YAAM,aAAa,OAAO,KAAK,MAAM,YAAY,QAAQ;AACzD,YAAM,YAAY,MAAM,eAAe,aAAa,UAAU;AAC9D,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,QAAwB;AAAA,QAC5B,IAAI,WAAW;AAAA,QACf,OAAO,MAAM;AAAA,QACb,OAAO,MAAM;AAAA,QACb,WAAW,MAAM,KAAK,SAAS;AAAA,QAC/B,YAAY,MAAM;AAAA,QAClB,WAAW;AAAA,QACX,WAAW;AAAA,MACb;AACA,YAAM,KAAK,SAAS,KAAK;AACzB,cAAQ,KAAK,KAAK;AAAA,IACpB;AACA,SAAK,OAAO,KAAK,oBAAoB,QAAQ,MAAM,cAAc;AACjE,WAAO;AAAA,EACT;AACF;AAEA,SAAS,cAAc,IAAY,MAA+C;AAChF,SAAO;AAAA,IACL;AAAA,IACA,OAAO,KAAK;AAAA,IACZ,OAAQ,KAAK,SAAoB;AAAA,IACjC,WAAW,KAAK,MAAM,KAAK,SAAmB;AAAA,IAC9C,YAAY,KAAK;AAAA,IACjB,WAAW,KAAK;AAAA,IAChB,WAAW,KAAK;AAAA,IAChB,QAAS,KAAK,UAAqB;AAAA,IACnC,UAAU,KAAK,WAAW,KAAK,MAAM,KAAK,QAAkB,IAAI;AAAA,EAClE;AACF;;;AClLA,SAAS,cAAAE,mBAAkB;AA6B3B,IAAM,gBAAgB;AAGtB,IAAM,kBAAkB;AAOjB,IAAM,wBAAN,MAA4B;AAAA;AAAA,EAEzB,eAAuD,oBAAI,IAAI;AAAA;AAAA,EAG/D,aAA0C,oBAAI,IAAI;AAAA,EAEzC;AAAA,EACA;AAAA,EAEjB,YAAY,MAA0B;AACpC,SAAK,SAAS,KAAK;AACnB,SAAK,WAAW,KAAK;AAAA,EACvB;AAAA;AAAA,EAGA,mBAAmB,UAAkB,KAA4B;AAC/D,UAAM,eAAe,KAAK,gBAAgB,QAAQ;AAClD,UAAM,MAAM,IAAI;AAEhB,eAAW,MAAM,IAAI,mBAAmB;AACtC,YAAM,WAAW,aAAa,IAAI,GAAG,OAAO;AAE5C,UAAI,UAAU;AAEZ,cAAM,eAAe;AAAA,UACnB,GAAG,SAAS;AAAA,UACZ,EAAE,GAAG,GAAG,UAAU,cAAc,CAAC,KAAK,GAAG,GAAG,GAAG,UAAU,cAAc,CAAC,KAAK,GAAG,GAAG,IAAI;AAAA,QACzF;AACA,cAAM,mBAAmB,aAAa,SAAS,gBAC3C,aAAa,MAAM,aAAa,SAAS,aAAa,IACtD;AAEJ,cAAM,mBAAoB,GAAG,aAAa,SAAS,KAAK,GAAG,aAAa,CAAC,GAAG,YACxE,IAAI,aAAa,GAAG,aAAa,CAAC,EAAE,SAAS,IAC7C,SAAS;AAEb,cAAM,UAAwB;AAAA,UAC5B,GAAG;AAAA,UACH,UAAU;AAAA,UACV,aAAa,SAAS,cAAc;AAAA,UACpC,eAAe;AAAA,UACf,OAAO,GAAG,SAAS;AAAA,UACnB,WAAW;AAAA,UACX,WAAW;AAAA,QACb;AACA,qBAAa,IAAI,GAAG,SAAS,OAAO;AAAA,MACtC,OAAO;AAEL,cAAM,QAAsB;AAAA,UAC1B,SAAS,GAAG;AAAA,UACZ;AAAA,UACA,WAAW,GAAG,UAAU;AAAA,UACxB,OAAO,GAAG,UAAU;AAAA,UACpB,WAAW;AAAA,UACX,UAAU;AAAA,UACV,aAAa;AAAA,UACb,eAAe;AAAA,UACf,OAAO,GAAG,SAAS;AAAA,UACnB,WAAW,CAAC,EAAE,GAAG,GAAG,UAAU,cAAc,CAAC,KAAK,GAAG,GAAG,GAAG,UAAU,cAAc,CAAC,KAAK,GAAG,GAAG,IAAI,CAAC;AAAA,UACpG,WAAW,GAAG,aAAa,CAAC,GAAG,YAAY,IAAI,aAAa,GAAG,aAAa,CAAC,EAAE,SAAS,IAAI;AAAA,UAC5F,UAAU;AAAA,UACV,UAAU,GAAG;AAAA,QACf;AAEA,qBAAa,IAAI,GAAG,SAAS,KAAK;AAElC,aAAK,SAAS,KAAK;AAAA,UACjB,IAAIA,YAAW;AAAA,UACf,WAAW,oBAAI,KAAK;AAAA,UACpB,QAAQ,EAAE,MAAM,QAAQ,IAAI,kBAAkB;AAAA,UAC9C,UAAU;AAAA,UACV,MAAM,EAAE,UAAU,SAAS,GAAG,SAAS,WAAW,GAAG,UAAU,UAAU;AAAA,QAC3E,CAAC;AAAA,MACH;AAAA,IACF;AAGA,eAAW,CAAC,SAAS,KAAK,KAAK,cAAc;AAC3C,UAAI,MAAM,MAAM,WAAW,iBAAiB;AAC1C,qBAAa,OAAO,OAAO;AAC3B,aAAK,SAAS,KAAK;AAAA,UACjB,IAAIA,YAAW;AAAA,UACf,WAAW,oBAAI,KAAK;AAAA,UACpB,QAAQ,EAAE,MAAM,QAAQ,IAAI,kBAAkB;AAAA,UAC9C,UAAU;AAAA,UACV,MAAM,EAAE,UAAU,SAAS,WAAW,MAAM,WAAW,UAAU,MAAM,MAAM,UAAU;AAAA,QACzF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,gBAAgB,UAA2C;AACzD,WAAO,CAAC,GAAI,KAAK,aAAa,IAAI,QAAQ,GAAG,OAAO,KAAK,CAAC,CAAE;AAAA,EAC9D;AAAA;AAAA,EAGA,qBAA8C;AAC5C,UAAM,MAAsB,CAAC;AAC7B,eAAW,UAAU,KAAK,aAAa,OAAO,GAAG;AAC/C,UAAI,KAAK,GAAG,OAAO,OAAO,CAAC;AAAA,IAC7B;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,SAAS,UAAkB,SAAsC;AAC/D,WAAO,KAAK,aAAa,IAAI,QAAQ,GAAG,IAAI,OAAO,KAAK;AAAA,EAC1D;AAAA;AAAA,EAGA,iBAAqF;AACnF,UAAM,SAA6E,CAAC;AACpF,eAAW,CAAC,UAAU,MAAM,KAAK,KAAK,cAAc;AAClD,YAAM,UAAkC,CAAC;AACzC,iBAAW,SAAS,OAAO,OAAO,GAAG;AACnC,gBAAQ,MAAM,SAAS,KAAK,QAAQ,MAAM,SAAS,KAAK,KAAK;AAAA,MAC/D;AACA,aAAO,QAAQ,IAAI,EAAE,OAAO,OAAO,MAAM,QAAQ;AAAA,IACnD;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,YAAY,UAAwB;AAClC,SAAK,aAAa,OAAO,QAAQ;AAAA,EACnC;AAAA;AAAA,EAGA,WAAiB;AACf,SAAK,aAAa,MAAM;AACxB,SAAK,WAAW,MAAM;AAAA,EACxB;AAAA;AAAA,EAGA,gBAAqD;AACnD,WAAO,KAAK;AAAA,EACd;AAAA,EAEQ,gBAAgB,UAA6C;AACnE,QAAI,CAAC,KAAK,aAAa,IAAI,QAAQ,GAAG;AACpC,WAAK,aAAa,IAAI,UAAU,oBAAI,IAAI,CAAC;AAAA,IAC3C;AACA,WAAO,KAAK,aAAa,IAAI,QAAQ;AAAA,EACvC;AACF;;;AC/LA,SAAS,cAAAC,mBAAkB;AAoBpB,IAAM,oBAAqC;AAAA,EAChD,mBAAmB,KAAK,KAAK;AAAA;AAAA,EAC7B,qBAAqB;AAAA,EACrB,iBAAiB;AAAA,EACjB,eAAe;AACjB;AAuBO,IAAM,mBAAN,MAAuB;AAAA,EAS5B,YAA6B,MAAqB;AAArB;AAC3B,SAAK,SAAS,KAAK;AACnB,SAAK,qBAAqB,KAAK;AAC/B,SAAK,WAAW,KAAK;AAGrB,UAAM,YAAY,KAAK,mBAAmB;AAC1C,SAAK,SAAS,YACV,EAAE,GAAG,mBAAmB,GAAG,UAAU,IACrC,EAAE,GAAG,kBAAkB;AAAA,EAC7B;AAAA,EAlBQ;AAAA,EACA;AAAA,EACA;AAAA,EAES;AAAA,EACA;AAAA,EACA;AAAA,EAcjB,QAAc;AAEZ,SAAK,eAAe,WAAW,MAAM;AACnC,WAAK,WAAW,EAAE,MAAM,CAAC,QAAQ;AAC/B,aAAK,OAAO,KAAK,2BAA2B,GAAG,EAAE;AAAA,MACnD,CAAC;AAAA,IACH,GAAG,IAAI,KAAK,GAAI;AAGhB,SAAK,eAAe,YAAY,MAAM;AACpC,WAAK,WAAW,EAAE,MAAM,CAAC,QAAQ;AAC/B,aAAK,OAAO,KAAK,4BAA4B,GAAG,EAAE;AAAA,MACpD,CAAC;AAAA,IACH,GAAG,KAAK,OAAO,iBAAiB;AAAA,EAClC;AAAA,EAEA,OAAa;AACX,QAAI,KAAK,cAAc;AACrB,mBAAa,KAAK,YAAY;AAC9B,WAAK,eAAe;AAAA,IACtB;AACA,QAAI,KAAK,cAAc;AACrB,oBAAc,KAAK,YAAY;AAC/B,WAAK,eAAe;AAAA,IACtB;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,aAAuC;AAC3C,UAAM,SAA0B;AAAA,MAC9B,eAAe;AAAA,MACf,qBAAqB;AAAA,MACrB,kBAAkB;AAAA,IACpB;AAEA,QAAI;AAEF,YAAM,cAAc,KAAK,IAAI,IAAI,KAAK,OAAO,sBAAsB,KAAK,KAAK,KAAK;AAClF,YAAM,EAAE,WAAW,eAAe,cAAc,iBAAiB,IAC/D,MAAM,KAAK,qBAAqB,oBAAoB,WAAW;AAGjE,YAAM,cAAc,KAAK,IAAI,IAAI,KAAK,OAAO,kBAAkB,KAAK,KAAK,KAAK;AAC9E,YAAM,eAAe,MAAM,KAAK,gBAAgB,gBAAgB,WAAW;AAG3E,YAAM,cAA+B;AAAA,QACnC,eAAe;AAAA,QACf,qBAAqB;AAAA,QACrB,kBAAkB;AAAA,MACpB;AAEA,UAAI,YAAY,gBAAgB,KAAK,YAAY,sBAAsB,KAAK,YAAY,mBAAmB,GAAG;AAC5G,aAAK,OAAO;AAAA,UACV,YAAY,YAAY,aAAa,YAAY,YAAY,mBAAmB,mBAAmB,YAAY,gBAAgB;AAAA,QACjI;AAEA,aAAK,SAAS,KAAK;AAAA,UACjB,IAAIA,YAAW;AAAA,UACf,WAAW,oBAAI,KAAK;AAAA,UACpB,QAAQ,EAAE,MAAM,QAAQ,IAAI,YAAY;AAAA,UACxC,UAAU;AAAA,UACV,MAAM;AAAA,QACR,CAAC;AAAA,MACH;AAEA,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,WAAK,OAAO,KAAK,6BAA6B,GAAG,EAAE;AAAA,IACrD;AAEA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,UAAU,QAAwC;AAChD,SAAK,SAAS,EAAE,GAAG,KAAK,QAAQ,GAAG,OAAO;AAAA,EAC5C;AAAA;AAAA,EAGA,YAA6B;AAC3B,WAAO,EAAE,GAAG,KAAK,OAAO;AAAA,EAC1B;AAAA;AAAA,EAGA,MAAM,eAAyC;AAC7C,WAAO,KAAK,WAAW;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,qBACZ,YACA,iBACsD;AACtD,QAAI,YAAY;AAChB,QAAI,eAAe;AAEnB,QAAI;AACF,YAAM,WAAW,KAAK,mBAAmB,QAAQ;AACjD,UAAI,CAAC,SAAS,WAAY,QAAO,EAAE,WAAW,aAAa;AAE3D,YAAM,MAAM,MAAM,SAAS,WAAW,MAAM,YAAY;AAAA,QACtD,cAAc,EAAE,WAAW,CAAC,GAAG,eAAe,EAAE;AAAA,QAChD,OAAO;AAAA,MACT,CAAC;AAED,iBAAW,UAAU,KAAK;AACxB,cAAM,UAAU,OAAO;AAGvB,YAAI,SAAS,OAAO;AAClB,cAAI;AACF,kBAAM,QAAQ,MAAM,SAAS,MAAM,UAAU,UAAU,OAAO,GAAG;AACjE,uBAAW,QAAQ,OAAO;AACxB,oBAAM,SAAS,MAAM,WAAW,IAAI;AACpC;AAAA,YACF;AAAA,UACF,QAAQ;AAAA,UAER;AAAA,QACF;AAGA,cAAM,SAAS,WAAW,OAAO,YAAY,OAAO;AACpD;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,WAAO,EAAE,WAAW,aAAa;AAAA,EACnC;AAAA,EAEA,MAAc,gBAAgB,YAAoB,iBAA0C;AAC1F,QAAI;AACF,YAAM,WAAW,KAAK,mBAAmB,QAAQ;AACjD,UAAI,CAAC,SAAS,WAAY,QAAO;AAGjC,YAAM,MAAM,MAAM,SAAS,WAAW,MAAM,YAAY;AAAA,QACtD,cAAc,EAAE,WAAW,CAAC,GAAG,eAAe,EAAE;AAAA,QAChD,OAAO;AAAA;AAAA,MACT,CAAC;AAGD,UAAI,UAAU;AACd,iBAAW,UAAU,KAAK;AACxB,cAAM,SAAS,WAAW,OAAO,YAAY,OAAO,EAAE;AACtD;AAAA,MACF;AAEA,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;ACxMA,IAAM,iBAAqC;AAAA,EACzC,SAAS;AAAA,EACT,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,eAAe,EAAE,OAAO,KAAK,QAAQ,IAAI;AAC3C;AAqDO,IAAM,oBAAN,MAAwB;AAAA,EACZ;AAAA,EACA;AAAA;AAAA,EAGA,gBAAiD,oBAAI,IAAI;AAAA;AAAA,EAGzD,eAA0C,oBAAI,IAAI;AAAA;AAAA,EAGlD,mBAAwC,oBAAI,IAAI;AAAA,EAEjE,YAAY,MAAsB;AAChC,SAAK,SAAS,KAAK;AACnB,SAAK,qBAAqB,KAAK;AAAA,EACjC;AAAA;AAAA,EAIA,UAAU,UAAkB,QAA2C;AACrE,SAAK,cAAc,IAAI,UAAU,EAAE,GAAG,gBAAgB,GAAG,OAAO,CAAC;AAAA,EACnE;AAAA,EAEA,UAAU,UAAsC;AAC9C,WAAO,KAAK,cAAc,IAAI,QAAQ,KAAK,EAAE,GAAG,eAAe;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,YAAY,KAAqC;AACrD,UAAM,SAAS,KAAK,UAAU,IAAI,QAAQ;AAC1C,QAAI,CAAC,OAAO,QAAS;AAErB,UAAM,iBAAiB,oBAAI,IAAY;AAEvC,eAAW,MAAM,IAAI,mBAAmB;AACtC,qBAAe,IAAI,GAAG,OAAO;AAC7B,YAAM,KAAK,YAAY,KAAK,IAAI,MAAM;AAAA,IACxC;AAGA,eAAW,CAAC,SAAS,KAAK,KAAK,KAAK,cAAc;AAChD,UAAI,MAAM,aAAa,IAAI,YAAY,CAAC,eAAe,IAAI,OAAO,GAAG;AACnE,cAAM,SAAS;AAEf,cAAM,KAAK,aAAa,KAAK;AAC7B,aAAK,aAAa,OAAO,OAAO;AAChC,aAAK,iBAAiB,OAAO,OAAO;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA,EAKA,eAAe,SAAoC;AACjD,UAAM,QAAQ,KAAK,aAAa,IAAI,OAAO;AAC3C,WAAO,QAAQ,KAAK,YAAY,KAAK,IAAI;AAAA,EAC3C;AAAA;AAAA,EAGA,gBAAgB,UAAyC;AACvD,UAAM,UAAwB,CAAC;AAC/B,eAAW,SAAS,KAAK,aAAa,OAAO,GAAG;AAC9C,UAAI,MAAM,aAAa,UAAU;AAC/B,gBAAQ,KAAK,KAAK,YAAY,KAAK,CAAC;AAAA,MACtC;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,kBAAkB,SAA6C;AACnE,QAAI;AACF,YAAM,WAAW,KAAK,mBAAmB,QAAQ;AACjD,UAAI,CAAC,SAAS,WAAY,QAAO;AAEjC,YAAM,UAAU,MAAM,SAAS,WAAW,MAAM,gBAAgB;AAAA,QAC9D,OAAO,EAAE,QAAQ;AAAA,QACjB,OAAO;AAAA,MACT,CAAC;AAED,UAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,aAAO,QAAQ,CAAC,EAAG;AAAA,IACrB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,SAAS,SAA6C;AAC1D,WAAO,KAAK,eAAe,OAAO,KAAK,KAAK,kBAAkB,OAAO;AAAA,EACvE;AAAA;AAAA,EAGA,MAAM,WAAW,UAAkB,SAKA;AACjC,UAAM,UAAwB,CAAC;AAG/B,eAAW,SAAS,KAAK,aAAa,OAAO,GAAG;AAC9C,UAAI,MAAM,aAAa,UAAU;AAC/B,YAAI,SAAS,aAAa,MAAM,cAAc,QAAQ,UAAW;AACjE,gBAAQ,KAAK,KAAK,YAAY,KAAK,CAAC;AAAA,MACtC;AAAA,IACF;AAGA,QAAI;AACF,YAAM,WAAW,KAAK,mBAAmB,QAAQ;AACjD,UAAI,SAAS,YAAY;AACvB,cAAM,SAAS,MAAM,SAAS,WAAW,MAAM,gBAAgB;AAAA,UAC7D,OAAO,EAAE,UAAU,GAAI,SAAS,YAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC,EAAG;AAAA,UACnF,cAAc,SAAS,SAAS,SAAS,QACrC,EAAE,WAAW,CAAC,QAAQ,SAAS,GAAG,QAAQ,SAAS,KAAK,IAAI,CAAC,EAAE,IAC/D;AAAA,UACJ,SAAS,EAAE,OAAO,aAAa,WAAW,OAAO;AAAA,UACjD,OAAO,SAAS,SAAS;AAAA,QAC3B,CAAC;AAED,mBAAW,UAAU,QAAQ;AAC3B,gBAAM,QAAQ,OAAO;AAErB,cAAI,CAAC,KAAK,aAAa,IAAI,MAAM,OAAO,GAAG;AACzC,oBAAQ,KAAK,KAAK;AAAA,UACpB;AAAA,QACF;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,WAAO;AAAA,EACT;AAAA;AAAA,EAIA,MAAc,YACZ,KACA,IACA,QACe;AACf,QAAI,QAAQ,KAAK,aAAa,IAAI,GAAG,OAAO;AAE5C,UAAM,OAAO,GAAG,UAAU;AAC1B,QAAI,CAAC,QAAQ,KAAK,SAAS,EAAG;AAE9B,UAAM,SAAS,EAAE,GAAG,KAAK,CAAC,IAAK,KAAK,CAAC,IAAK,GAAG,GAAG,KAAK,CAAC,IAAK,KAAK,CAAC,IAAK,EAAE;AAExE,UAAM,WAA0B;AAAA,MAC9B,GAAG,OAAO;AAAA,MACV,GAAG,OAAO;AAAA,MACV,WAAW,IAAI;AAAA,MACf,MAAM,CAAC,KAAK,CAAC,GAAI,KAAK,CAAC,GAAI,KAAK,CAAC,GAAI,KAAK,CAAC,CAAE;AAAA,IAC/C;AAEA,QAAI,CAAC,OAAO;AACV,cAAQ;AAAA,QACN,SAAS,GAAG;AAAA,QACZ,UAAU,IAAI;AAAA,QACd,WAAW,GAAG,UAAU;AAAA,QACxB,OAAO,GAAG,UAAU;AAAA,QACpB,WAAW,IAAI;AAAA,QACf,UAAU,IAAI;AAAA,QACd,WAAW,CAAC,QAAQ;AAAA,QACpB,WAAW,CAAC;AAAA,QACZ,eAAe;AAAA,QACf,cAAc,CAAC,GAAG,GAAG,KAAK;AAAA,QAC1B,QAAQ;AAAA,MACV;AACA,WAAK,aAAa,IAAI,GAAG,SAAS,KAAK;AAAA,IACzC,OAAO;AAEL,YAAM,UAAU,MAAM,UAAU,MAAM,UAAU,SAAS,CAAC;AAC1D,YAAM,OAAO,UACT,KAAK,MAAM,OAAO,IAAI,QAAQ,MAAM,KAAK,OAAO,IAAI,QAAQ,MAAM,CAAC,IACnE;AAEJ,YAAM,WAAW,IAAI;AACrB,YAAM,iBAAiB;AAGvB,UAAI,MAAM,UAAU,UAAU,OAAO,gBAAgB;AAEnD,cAAM,OAAO,KAAK,MAAM,MAAM,UAAU,SAAS,CAAC;AAClD,cAAM,cAAc,MAAM,UAAU,OAAO,CAAC,GAAG,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC;AAC7E,cAAM,YAAY,CAAC,GAAG,aAAa,QAAQ;AAAA,MAC7C,OAAO;AACL,cAAM,YAAY,CAAC,GAAG,MAAM,WAAW,QAAQ;AAAA,MACjD;AAGA,iBAAW,QAAQ,GAAG,OAAO;AAC3B,YAAI,CAAC,MAAM,aAAa,SAAS,IAAI,GAAG;AACtC,gBAAM,eAAe,CAAC,GAAG,MAAM,cAAc,IAAI;AAAA,QACnD;AAAA,MACF;AAAA,IACF;AAGA,UAAM,eAAe,KAAK,iBAAiB,IAAI,GAAG,OAAO,KAAK;AAC9D,QAAI,IAAI,YAAY,gBAAgB,OAAO,sBAAsB,GAAG,MAAM;AACxE,YAAM,KAAK,gBAAgB,OAAO,IAAI,UAAU,MAAM;AACtD,WAAK,iBAAiB,IAAI,GAAG,SAAS,IAAI,SAAS;AAAA,IACrD;AAAA,EACF;AAAA,EAEA,MAAc,gBACZ,OACA,IACA,UACA,QACe;AACf,QAAI,CAAC,GAAG,QAAQ,CAAC,OAAO,eAAgB;AAExC,QAAI;AACF,YAAMC,UAAS,MAAM,OAAO,OAAO,GAAG;AACtC,YAAM,QAAQ,MAAMA,OAAM,GAAG,IAAI,EAC9B,OAAO,OAAO,cAAc,OAAO,OAAO,cAAc,QAAQ,EAAE,KAAK,QAAQ,CAAC,EAChF,KAAK,EAAE,SAAS,GAAG,CAAC,EACpB,SAAS;AAEZ,YAAMC,QAAO,gBAAgB,MAAM,OAAO,IAAI,SAAS,SAAS;AAGhE,YAAM,WAAW,KAAK,mBAAmB,QAAQ;AACjD,UAAI,SAAS,OAAO;AAClB,cAAM,SAAS,MAAM,UAAUA,OAAM,KAAK;AAAA,MAC5C;AAEA,YAAM,YAAY,CAAC,GAAG,MAAM,WAAW;AAAA,QACrC,WAAW,SAAS;AAAA,QACpB;AAAA,QACA,eAAeA;AAAA,MACjB,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,MAAc,aAAa,OAAoC;AAC7D,QAAI;AACF,YAAM,WAAW,KAAK,mBAAmB,QAAQ;AACjD,UAAI,CAAC,SAAS,WAAY;AAE1B,YAAM,SAAS,WAAW,OAAO;AAAA,QAC/B,YAAY;AAAA,QACZ,IAAI,MAAM;AAAA,QACV,MAAM,KAAK,YAAY,KAAK;AAAA,MAC9B,CAAC;AAED,WAAK,OAAO;AAAA,QACV,oBAAoB,MAAM,OAAO,KAAK,MAAM,UAAU,MAAM,eAAe,MAAM,UAAU,MAAM,eAAe,MAAM,cAAc,QAAQ,CAAC,CAAC;AAAA,MAChJ;AAAA,IACF,SAAS,KAAK;AACZ,WAAK,OAAO,KAAK,2BAA2B,MAAM,OAAO,KAAK,GAAG,EAAE;AAAA,IACrE;AAAA,EACF;AAAA,EAEQ,YAAY,OAAiC;AACnD,WAAO;AAAA,MACL,SAAS,MAAM;AAAA,MACf,UAAU,MAAM;AAAA,MAChB,WAAW,MAAM;AAAA,MACjB,OAAO,MAAM;AAAA,MACb,WAAW,MAAM;AAAA,MACjB,UAAU,MAAM;AAAA,MAChB,WAAW,CAAC,GAAG,MAAM,SAAS;AAAA,MAC9B,WAAW,CAAC,GAAG,MAAM,SAAS;AAAA,MAC9B,eAAe,MAAM;AAAA,MACrB,cAAc,CAAC,GAAG,MAAM,YAAY;AAAA,MACpC,QAAQ,MAAM;AAAA,IAChB;AAAA,EACF;AACF;;;AChVO,IAAM,gBAAN,MAA6D;AAAA,EACzD,WAA0B;AAAA,IACjC,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,SAAS;AAAA,IACT,cAAc;AAAA,MACZ,EAAE,MAAM,iBAAiB,MAAM,YAAqB;AAAA,MACpD,EAAE,MAAM,oBAAoB,MAAM,YAAqB;AAAA,MACvD,EAAE,MAAM,6BAA6B,MAAM,YAAqB;AAAA,IAClE;AAAA,EACF;AAAA;AAAA,EAGQ,gBAA4C;AAAA;AAAA,EAG5C,cAA2C;AAAA,EAC3C,cAAkC;AAAA,EAClC,WAAqD;AAAA,EACrD,gBAAoD;AAAA,EACpD,yBAAyB;AAAA,IAC/B,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,wBAAwB;AAAA,IACxB,sBAAsB;AAAA,EACxB;AAAA;AAAA,EAGQ,oBAAqD;AAAA,EAE7D,yBAAyB,MAAyC;AAChE,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEA,MAAM,WAAW,SAAsC;AAErD,UAAM,WAAW,IAAI,gBAAgB;AACrC,aAAS,SAAS,IAAI,sBAAsB,GAAG,EAAE;AACjD,SAAK,gBAAgB,IAAI,oBAAoB,UAAU,QAAQ,MAAM;AACrE,YAAQ,OAAO,KAAK,mCAAmC;AAGvD,QAAI,KAAK,eAAe;AACtB,UAAI;AACF,cAAM,YAAY,MAAM,OAAO,gBAAgB,GAAG;AAClD,cAAMC,QAAO,MAAM,OAAO,MAAW;AACrC,cAAM,EAAE,wBAAAC,wBAAuB,IAAI,MAAM;AACzC,cAAM,EAAE,aAAa,MAAM,IAAI,MAAM;AACrC,cAAM,EAAE,sBAAsB,SAAS,IAAI,MAAM;AAEjD,cAAM,cAAc,QAAQ,cAAc;AAC1C,cAAM,SAASD,MAAK,KAAK,QAAQ,cAAc,MAAM,aAAa;AAClE,aAAK,WAAW,IAAI,SAAS,MAAM;AACnC,aAAK,cAAc,IAAI,MAAM,KAAK,QAAQ;AAC1C,aAAK,YAAY,WAAW;AAE5B,cAAM,aAAc,QAAQ,YAAY,cAAyB,KAAK,uBAAuB;AAC7F,cAAM,uBAAuBC,wBAAuB,UAAU;AAE9D,cAAM,qBAA4C;AAAA,UAChD,MAAM;AAAA,UACN,SAAU,QAAQ,YAAY,WAAuC,KAAK,uBAAuB;AAAA,UACjG,SAAU,QAAQ,YAAY,WAAsB,KAAK,uBAAuB;AAAA,QAClF;AAEA,cAAM,yBAA0B,QAAQ,YAAY,0BAC/C,KAAK,uBAAuB;AACjC,cAAM,uBAAwB,QAAQ,YAAY,wBAC7C,KAAK,uBAAuB;AAEjC,aAAK,yBAAyB;AAAA,UAC5B;AAAA,UACA,SAAS,mBAAmB;AAAA,UAC5B,SAAS,mBAAmB;AAAA,UAC5B;AAAA,UACA;AAAA,QACF;AAEA,cAAM,cAAc,QAAQ,QAAQ;AACpC,YAAI,CAAC,aAAa;AAChB,gBAAM,IAAI,MAAM,2DAA2D;AAAA,QAC7E;AAEA,aAAK,cAAc,IAAI,SAAS;AAAA,UAC9B,IAAI,KAAK;AAAA,UACT,QAAQ,QAAQ;AAAA,UAChB,UAAU,QAAQ;AAAA,UAClB,iBAAiB,KAAK,cAAc;AAAA,UACpC,iBAAiB,KAAK,cAAc;AAAA,UACpC,gBAAgB,KAAK,cAAc;AAAA,UACnC;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,oBAAoB;AAAA,QACtB,CAAC;AACD,cAAM,KAAK,YAAY,MAAM;AAC7B,gBAAQ,OAAO,KAAK,8BAA8B;AAAA,MACpD,SAAS,OAAgB;AACvB,cAAM,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACjE,gBAAQ,OAAO,KAAK,0CAA0C,GAAG,EAAE;AAAA,MACrE;AAAA,IACF;AAGA,UAAM,mBAAmB,IAAI,wBAAwB;AAAA,MACnD,oBAAoB,MAAM,QAAQ;AAAA,MAClC,WAAW,CAAC,QAAQ,YAAY,QAAQ,SAAS,UAAU,QAAQ,OAAO;AAAA,MAC1E,QAAQ,QAAQ,OAAO,MAAM,kBAAkB;AAAA,IACjD,CAAC;AAED,UAAM,aAAa,IAAI,kBAAkB;AAAA,MACvC,sBAAsB,MAAM;AAC1B,YAAI,CAAC,QAAQ,QAAQ,YAAY;AAC/B,gBAAM,IAAI,MAAM,2DAA2D;AAAA,QAC7E;AACA,eAAO,QAAQ,QAAQ;AAAA,MACzB;AAAA,MACA,QAAQ,QAAQ,OAAO,MAAM,YAAY;AAAA,IAC3C,CAAC;AAED,UAAM,iBAAiB,IAAI,sBAAsB;AAAA,MAC/C,UAAU,QAAQ;AAAA,MAClB,QAAQ,QAAQ,OAAO,MAAM,gBAAgB;AAAA,IAC/C,CAAC;AAED,UAAM,YAAY,IAAI,iBAAiB;AAAA,MACrC,oBAAoB,MAAM,QAAQ;AAAA,MAClC,oBAAoB,MAAM,QAAQ,YAAY;AAAA,MAC9C,UAAU,QAAQ;AAAA,MAClB,QAAQ,QAAQ,OAAO,MAAM,WAAW;AAAA,IAC1C,CAAC;AAED,UAAM,aAAa,IAAI,kBAAkB;AAAA,MACvC,oBAAoB,MAAM,QAAQ;AAAA,MAClC,QAAQ,QAAQ,OAAO,MAAM,YAAY;AAAA,IAC3C,CAAC;AAED,qBAAiB,MAAM;AACvB,cAAU,MAAM;AAEhB,SAAK,oBAAoB;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,YAAQ,OAAO,KAAK,kCAAkC;AAAA,EACxD;AAAA,EAEA,MAAM,WAA0B;AAE9B,UAAM,KAAK,eAAe,WAAW;AACrC,SAAK,gBAAgB;AAGrB,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY,KAAK;AACtB,WAAK,cAAc;AAAA,IACrB;AACA,QAAI,KAAK,UAAU;AACjB,WAAK,SAAS,MAAM;AACpB,WAAK,WAAW;AAAA,IAClB;AACA,SAAK,cAAc;AAGnB,QAAI,KAAK,mBAAmB;AAC1B,WAAK,kBAAkB,iBAAiB,KAAK;AAC7C,WAAK,kBAAkB,UAAU,KAAK;AACtC,WAAK,oBAAoB;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,sBACE,MACiC;AACjC,YAAQ,MAAM;AAAA,MACZ,KAAK;AACH,eAAO,KAAK;AAAA,MACd,KAAK;AACH,eAAO,KAAK;AAAA,MACd,KAAK;AACH,eAAO,KAAK;AAAA,MACd;AACE,eAAO;AAAA,IACX;AAAA,EACF;AAAA;AAAA,EAIA,iBAAuC;AACrC,QAAI,CAAC,KAAK,YAAa,OAAM,IAAI,MAAM,yCAAyC;AAChF,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,iBAA8B;AAC5B,QAAI,CAAC,KAAK,YAAa,OAAM,IAAI,MAAM,yCAAyC;AAChF,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAIA,kBAAkC;AAChC,WAAO,EAAE,UAAU,CAAC,EAAE;AAAA,EACxB;AAAA,EAEA,YAAqC;AACnC,WAAO,EAAE,GAAG,KAAK,uBAAuB;AAAA,EAC1C;AAAA,EAEA,MAAM,eAAe,QAAgD;AACnE,SAAK,yBAAyB;AAAA,MAC5B,YAAa,OAAO,cAAyB,KAAK,uBAAuB;AAAA,MACzE,SAAU,OAAO,WAAuC,KAAK,uBAAuB;AAAA,MACpF,SAAU,OAAO,WAAsB,KAAK,uBAAuB;AAAA,MACnE,wBAAyB,OAAO,0BAAqC,KAAK,uBAAuB;AAAA,MACjG,sBAAuB,OAAO,wBAAmC,KAAK,uBAAuB;AAAA,IAC/F;AAAA,EACF;AACF;AAEA,IAAO,gBAAQ;","names":["platform","spawn","spawn","spawn","path","sharp","randomUUID","randomUUID","sharp","path","path","detectPlatformDefaults"]}
|